handler.go 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928
  1. package httpserver
  2. import (
  3. "fmt"
  4. "io"
  5. "net/http"
  6. "strings"
  7. "sync/atomic"
  8. "time"
  9. "github.com/goccy/go-json"
  10. "github.com/danfragoso/pizzasql-next/pkg/csvexport"
  11. "github.com/danfragoso/pizzasql-next/pkg/csvimport"
  12. "github.com/danfragoso/pizzasql-next/pkg/lexer"
  13. "github.com/danfragoso/pizzasql-next/pkg/parser"
  14. "github.com/danfragoso/pizzasql-next/pkg/sqlexport"
  15. "github.com/danfragoso/pizzasql-next/pkg/sqlimport"
  16. "github.com/danfragoso/pizzasql-next/pkg/sqliteimport"
  17. "github.com/danfragoso/pizzasql-next/pkg/version"
  18. )
  19. // QueryRequest represents a single query request.
  20. type QueryRequest struct {
  21. SQL string `json:"sql"`
  22. Params []interface{} `json:"params"`
  23. }
  24. // ExecuteRequest represents a batch execution request.
  25. type ExecuteRequest struct {
  26. Statements []QueryRequest `json:"statements"`
  27. Transaction bool `json:"transaction"`
  28. }
  29. // TransactionRequest represents a transaction management request.
  30. type TransactionRequest struct {
  31. TransactionID string `json:"transactionId"`
  32. }
  33. // handleQuery handles POST /query
  34. func (s *Server) handleQuery(w http.ResponseWriter, r *http.Request) {
  35. if r.Method != http.MethodPost {
  36. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only POST method is allowed", nil)
  37. return
  38. }
  39. var req QueryRequest
  40. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  41. writeError(w, http.StatusBadRequest, "INVALID_JSON", "Invalid JSON in request body", nil)
  42. return
  43. }
  44. if req.SQL == "" {
  45. writeError(w, http.StatusBadRequest, "MISSING_SQL", "SQL query is required", nil)
  46. return
  47. }
  48. // Get database from X-Database header
  49. dbName := r.Header.Get("X-Database")
  50. exec, _, err := s.getExecutorForDatabase(dbName)
  51. if err != nil {
  52. writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
  53. return
  54. }
  55. // Check for pretty print
  56. pretty := r.URL.Query().Get("pretty") == "true"
  57. explain := r.URL.Query().Get("explain") == "true"
  58. readonly := r.URL.Query().Get("readonly") == "true"
  59. // Parse timeout
  60. timeout := 5 * time.Minute
  61. if t := r.URL.Query().Get("timeout"); t != "" {
  62. if d, err := time.ParseDuration(t); err == nil {
  63. timeout = d
  64. }
  65. }
  66. // Execute with timeout
  67. resultChan := make(chan *QueryResponse, 1)
  68. errorChan := make(chan error, 1)
  69. go func() {
  70. start := time.Now()
  71. // Check readonly mode
  72. if readonly {
  73. upper := strings.ToUpper(strings.TrimSpace(req.SQL))
  74. if strings.HasPrefix(upper, "INSERT") ||
  75. strings.HasPrefix(upper, "UPDATE") ||
  76. strings.HasPrefix(upper, "DELETE") ||
  77. strings.HasPrefix(upper, "CREATE") ||
  78. strings.HasPrefix(upper, "DROP") ||
  79. strings.HasPrefix(upper, "ALTER") {
  80. errorChan <- &HTTPError{
  81. Code: "READ_ONLY_MODE",
  82. Message: "Write operations not allowed in read-only mode",
  83. Status: http.StatusForbidden,
  84. }
  85. return
  86. }
  87. }
  88. // Substitute parameters
  89. sql := substituteParams(req.SQL, req.Params)
  90. // Parse SQL
  91. l := lexer.New(sql)
  92. p := parser.New(l)
  93. stmt, err := p.Parse()
  94. if err != nil {
  95. errorChan <- &HTTPError{
  96. Code: "SYNTAX_ERROR",
  97. Message: err.Error(),
  98. Status: http.StatusBadRequest,
  99. }
  100. return
  101. }
  102. // Execute using the database-specific executor
  103. result, err := exec.Execute(stmt)
  104. if err != nil {
  105. errorChan <- &HTTPError{
  106. Code: "EXECUTION_ERROR",
  107. Message: err.Error(),
  108. Status: http.StatusInternalServerError,
  109. }
  110. return
  111. }
  112. duration := time.Since(start)
  113. // Build response
  114. resp := &QueryResponse{
  115. Columns: make([]ColumnInfo, len(result.Columns)),
  116. Rows: result.Rows,
  117. RowsAffected: result.RowsAffected,
  118. LastInsertID: result.LastInsertID,
  119. ExecutionTimeMicro: duration.Microseconds(),
  120. RowsReturned: len(result.Rows),
  121. }
  122. for i, col := range result.Columns {
  123. colType := result.GetColumnType(i)
  124. // If no type info, infer from first row values
  125. if colType == "ANY" && len(result.Rows) > 0 && i < len(result.Rows[0]) {
  126. colType = inferType(result.Rows[0][i])
  127. }
  128. resp.Columns[i] = ColumnInfo{
  129. Name: col,
  130. Type: colType,
  131. }
  132. }
  133. // Calculate bytes read (approximate size of the result set)
  134. // This is the serialized JSON size of the rows data
  135. if jsonBytes, err := json.Marshal(result.Rows); err == nil {
  136. resp.BytesRead = int64(len(jsonBytes))
  137. }
  138. if explain {
  139. resp.QueryPlan = []string{"Full table scan"} // TODO: Real query plan
  140. }
  141. resultChan <- resp
  142. }()
  143. select {
  144. case resp := <-resultChan:
  145. atomic.AddInt64(&s.stats.QueriesExecuted, 1)
  146. atomic.AddInt64(&s.stats.QueriesSuccess, 1)
  147. writeJSON(w, http.StatusOK, resp, pretty)
  148. case err := <-errorChan:
  149. atomic.AddInt64(&s.stats.QueriesExecuted, 1)
  150. atomic.AddInt64(&s.stats.QueriesError, 1)
  151. if httpErr, ok := err.(*HTTPError); ok {
  152. writeError(w, httpErr.Status, httpErr.Code, httpErr.Message, httpErr.Details)
  153. } else {
  154. writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error(), nil)
  155. }
  156. case <-time.After(timeout):
  157. atomic.AddInt64(&s.stats.QueriesExecuted, 1)
  158. atomic.AddInt64(&s.stats.QueriesError, 1)
  159. writeError(w, http.StatusRequestTimeout, "TIMEOUT", "Query execution timeout", nil)
  160. }
  161. }
  162. // handleExecute handles POST /execute for batch operations
  163. func (s *Server) handleExecute(w http.ResponseWriter, r *http.Request) {
  164. if r.Method != http.MethodPost {
  165. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only POST method is allowed", nil)
  166. return
  167. }
  168. var req ExecuteRequest
  169. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  170. writeError(w, http.StatusBadRequest, "INVALID_JSON", "Invalid JSON in request body", nil)
  171. return
  172. }
  173. if len(req.Statements) == 0 {
  174. writeError(w, http.StatusBadRequest, "MISSING_STATEMENTS", "At least one statement is required", nil)
  175. return
  176. }
  177. // Get database from X-Database header
  178. dbName := r.Header.Get("X-Database")
  179. exec, _, err := s.getExecutorForDatabase(dbName)
  180. if err != nil {
  181. writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
  182. return
  183. }
  184. pretty := r.URL.Query().Get("pretty") == "true"
  185. start := time.Now()
  186. results := make([]ExecuteResult, 0, len(req.Statements))
  187. // Start transaction if requested
  188. if req.Transaction {
  189. l := lexer.New("BEGIN")
  190. p := parser.New(l)
  191. stmt, _ := p.Parse()
  192. exec.Execute(stmt)
  193. }
  194. var executeErr error
  195. for _, stmt := range req.Statements {
  196. // Substitute parameters
  197. sql := substituteParams(stmt.SQL, stmt.Params)
  198. l := lexer.New(sql)
  199. p := parser.New(l)
  200. parsed, err := p.Parse()
  201. if err != nil {
  202. executeErr = err
  203. break
  204. }
  205. result, err := exec.Execute(parsed)
  206. if err != nil {
  207. executeErr = err
  208. break
  209. }
  210. results = append(results, ExecuteResult{
  211. RowsAffected: result.RowsAffected,
  212. LastInsertID: result.LastInsertID,
  213. })
  214. }
  215. // Handle transaction
  216. if req.Transaction {
  217. if executeErr != nil {
  218. // Rollback on error
  219. l := lexer.New("ROLLBACK")
  220. p := parser.New(l)
  221. stmt, _ := p.Parse()
  222. exec.Execute(stmt)
  223. writeError(w, http.StatusBadRequest, "TRANSACTION_ERROR", executeErr.Error(), nil)
  224. return
  225. } else {
  226. // Commit on success
  227. l := lexer.New("COMMIT")
  228. p := parser.New(l)
  229. stmt, _ := p.Parse()
  230. exec.Execute(stmt)
  231. }
  232. } else if executeErr != nil {
  233. writeError(w, http.StatusBadRequest, "EXECUTION_ERROR", executeErr.Error(), nil)
  234. return
  235. }
  236. resp := &ExecuteResponse{
  237. Results: results,
  238. ExecutionTime: time.Since(start).String(),
  239. }
  240. writeJSON(w, http.StatusOK, resp, pretty)
  241. }
  242. // handleSchemaTables handles GET /schema/tables
  243. func (s *Server) handleSchemaTables(w http.ResponseWriter, r *http.Request) {
  244. if r.Method != http.MethodGet {
  245. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only GET method is allowed", nil)
  246. return
  247. }
  248. // Get database from X-Database header (trim whitespace)
  249. dbName := strings.TrimSpace(r.Header.Get("X-Database"))
  250. _, schema, err := s.getExecutorForDatabase(dbName)
  251. if err != nil {
  252. writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
  253. return
  254. }
  255. actualDB := schema.GetDatabaseName()
  256. tables, err := schema.ListTables()
  257. if err != nil {
  258. writeError(w, http.StatusInternalServerError, "SCHEMA_ERROR", err.Error(), nil)
  259. return
  260. }
  261. // Include the actual database name and requested name in the response for verification
  262. resp := map[string]interface{}{
  263. "database": actualDB,
  264. "requested_database": dbName,
  265. "tables": tables,
  266. }
  267. pretty := r.URL.Query().Get("pretty") == "true"
  268. writeJSON(w, http.StatusOK, resp, pretty)
  269. }
  270. // handleSchemaTable handles GET /schema/tables/{table}
  271. func (s *Server) handleSchemaTable(w http.ResponseWriter, r *http.Request) {
  272. if r.Method != http.MethodGet {
  273. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only GET method is allowed", nil)
  274. return
  275. }
  276. // Get database from X-Database header
  277. dbName := r.Header.Get("X-Database")
  278. _, schemaManager, err := s.getExecutorForDatabase(dbName)
  279. if err != nil {
  280. writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
  281. return
  282. }
  283. // Extract table name from path
  284. path := strings.TrimPrefix(r.URL.Path, "/schema/tables/")
  285. tableName := strings.TrimSpace(path)
  286. if tableName == "" {
  287. writeError(w, http.StatusBadRequest, "MISSING_TABLE_NAME", "Table name is required", nil)
  288. return
  289. }
  290. schema, err := schemaManager.GetSchema(tableName)
  291. if err != nil {
  292. writeError(w, http.StatusNotFound, "TABLE_NOT_FOUND", fmt.Sprintf("Table '%s' not found", tableName), nil)
  293. return
  294. }
  295. columns := make([]map[string]interface{}, len(schema.Columns))
  296. for i, col := range schema.Columns {
  297. columns[i] = map[string]interface{}{
  298. "name": col.Name,
  299. "type": col.Type,
  300. "nullable": col.Nullable,
  301. "primaryKey": col.PrimaryKey,
  302. "default": col.Default,
  303. }
  304. }
  305. resp := map[string]interface{}{
  306. "name": schema.Name,
  307. "columns": columns,
  308. }
  309. pretty := r.URL.Query().Get("pretty") == "true"
  310. writeJSON(w, http.StatusOK, resp, pretty)
  311. }
  312. // handleHealth handles GET /health
  313. func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
  314. resp := map[string]interface{}{
  315. "status": "ok",
  316. "version": version.String(),
  317. "uptime": time.Since(s.stats.StartTime).String(),
  318. }
  319. pretty := r.URL.Query().Get("pretty") == "true"
  320. writeJSON(w, http.StatusOK, resp, pretty)
  321. }
  322. // handleStats handles GET /stats
  323. func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
  324. // Get database from X-Database header
  325. dbName := r.Header.Get("X-Database")
  326. _, schema, _ := s.getExecutorForDatabase(dbName)
  327. var tables []string
  328. if schema != nil {
  329. tables, _ = schema.ListTables()
  330. }
  331. var avgQueryTime string
  332. if s.stats.QueriesExecuted > 0 {
  333. avgQueryTime = "N/A" // TODO: Track actual query times
  334. } else {
  335. avgQueryTime = "0ms"
  336. }
  337. resp := map[string]interface{}{
  338. "queriesExecuted": atomic.LoadInt64(&s.stats.QueriesExecuted),
  339. "queriesSuccess": atomic.LoadInt64(&s.stats.QueriesSuccess),
  340. "queriesError": atomic.LoadInt64(&s.stats.QueriesError),
  341. "tablesCount": len(tables),
  342. "avgQueryTime": avgQueryTime,
  343. "uptime": time.Since(s.stats.StartTime).String(),
  344. }
  345. pretty := r.URL.Query().Get("pretty") == "true"
  346. writeJSON(w, http.StatusOK, resp, pretty)
  347. }
  348. // handleTransactionBegin handles POST /transaction/begin
  349. func (s *Server) handleTransactionBegin(w http.ResponseWriter, r *http.Request) {
  350. if r.Method != http.MethodPost {
  351. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only POST method is allowed", nil)
  352. return
  353. }
  354. // Get database from X-Database header
  355. dbName := r.Header.Get("X-Database")
  356. exec, _, err := s.getExecutorForDatabase(dbName)
  357. if err != nil {
  358. writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
  359. return
  360. }
  361. l := lexer.New("BEGIN")
  362. p := parser.New(l)
  363. stmt, _ := p.Parse()
  364. _, err = exec.Execute(stmt)
  365. if err != nil {
  366. writeError(w, http.StatusInternalServerError, "TRANSACTION_ERROR", err.Error(), nil)
  367. return
  368. }
  369. // Generate transaction ID (simple implementation)
  370. txID := fmt.Sprintf("tx-%d", time.Now().UnixNano())
  371. resp := map[string]interface{}{
  372. "transactionId": txID,
  373. }
  374. pretty := r.URL.Query().Get("pretty") == "true"
  375. writeJSON(w, http.StatusOK, resp, pretty)
  376. }
  377. // handleTransactionCommit handles POST /transaction/commit
  378. func (s *Server) handleTransactionCommit(w http.ResponseWriter, r *http.Request) {
  379. if r.Method != http.MethodPost {
  380. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only POST method is allowed", nil)
  381. return
  382. }
  383. // Get database from X-Database header
  384. dbName := r.Header.Get("X-Database")
  385. exec, _, err := s.getExecutorForDatabase(dbName)
  386. if err != nil {
  387. writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
  388. return
  389. }
  390. var req TransactionRequest
  391. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  392. // Allow commit without transaction ID for simplicity
  393. }
  394. l := lexer.New("COMMIT")
  395. p := parser.New(l)
  396. stmt, _ := p.Parse()
  397. _, err = exec.Execute(stmt)
  398. if err != nil {
  399. writeError(w, http.StatusInternalServerError, "TRANSACTION_ERROR", err.Error(), nil)
  400. return
  401. }
  402. resp := map[string]interface{}{
  403. "status": "committed",
  404. }
  405. pretty := r.URL.Query().Get("pretty") == "true"
  406. writeJSON(w, http.StatusOK, resp, pretty)
  407. }
  408. // substituteParams replaces ? placeholders with actual parameter values.
  409. // This is a simple implementation that handles basic SQL escaping.
  410. func substituteParams(sql string, params []interface{}) string {
  411. if len(params) == 0 {
  412. return sql
  413. }
  414. result := sql
  415. for _, param := range params {
  416. idx := strings.Index(result, "?")
  417. if idx == -1 {
  418. break
  419. }
  420. var replacement string
  421. switch v := param.(type) {
  422. case nil:
  423. replacement = "NULL"
  424. case string:
  425. // Escape single quotes in strings
  426. escaped := strings.ReplaceAll(v, "'", "''")
  427. replacement = "'" + escaped + "'"
  428. case int, int64, int32, int16, int8:
  429. replacement = fmt.Sprintf("%d", v)
  430. case float64, float32:
  431. replacement = fmt.Sprintf("%g", v)
  432. case bool:
  433. if v {
  434. replacement = "1"
  435. } else {
  436. replacement = "0"
  437. }
  438. default:
  439. // For other types, convert to string
  440. escaped := strings.ReplaceAll(fmt.Sprintf("%v", v), "'", "''")
  441. replacement = "'" + escaped + "'"
  442. }
  443. result = result[:idx] + replacement + result[idx+1:]
  444. }
  445. return result
  446. }
  447. // inferType infers SQL type from a Go value.
  448. func inferType(v interface{}) string {
  449. switch v.(type) {
  450. case nil:
  451. return "NULL"
  452. case int, int64, int32, int16, int8:
  453. return "INTEGER"
  454. case float64, float32:
  455. return "REAL"
  456. case string:
  457. return "TEXT"
  458. case []byte:
  459. return "BLOB"
  460. case bool:
  461. return "INTEGER"
  462. default:
  463. return "ANY"
  464. }
  465. }
  466. // handleTransactionRollback handles POST /transaction/rollback
  467. func (s *Server) handleTransactionRollback(w http.ResponseWriter, r *http.Request) {
  468. if r.Method != http.MethodPost {
  469. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only POST method is allowed", nil)
  470. return
  471. }
  472. // Get database from X-Database header
  473. dbName := r.Header.Get("X-Database")
  474. exec, _, err := s.getExecutorForDatabase(dbName)
  475. if err != nil {
  476. writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
  477. return
  478. }
  479. var req TransactionRequest
  480. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  481. // Allow rollback without transaction ID for simplicity
  482. }
  483. l := lexer.New("ROLLBACK")
  484. p := parser.New(l)
  485. stmt, _ := p.Parse()
  486. _, err = exec.Execute(stmt)
  487. if err != nil {
  488. writeError(w, http.StatusInternalServerError, "TRANSACTION_ERROR", err.Error(), nil)
  489. return
  490. }
  491. resp := map[string]interface{}{
  492. "status": "rolled back",
  493. }
  494. pretty := r.URL.Query().Get("pretty") == "true"
  495. writeJSON(w, http.StatusOK, resp, pretty)
  496. }
  497. // handleMetrics handles GET /metrics in Prometheus format
  498. func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) {
  499. if r.Method != http.MethodGet {
  500. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only GET method is allowed", nil)
  501. return
  502. }
  503. // Get database from X-Database header
  504. dbName := r.Header.Get("X-Database")
  505. _, schema, _ := s.getExecutorForDatabase(dbName)
  506. var tables []string
  507. if schema != nil {
  508. tables, _ = schema.ListTables()
  509. }
  510. uptime := time.Since(s.stats.StartTime).Seconds()
  511. queriesTotal := atomic.LoadInt64(&s.stats.QueriesExecuted)
  512. queriesSuccess := atomic.LoadInt64(&s.stats.QueriesSuccess)
  513. queriesError := atomic.LoadInt64(&s.stats.QueriesError)
  514. w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
  515. // Write Prometheus format metrics
  516. fmt.Fprintf(w, "# HELP pizzasql_queries_total Total number of queries executed\n")
  517. fmt.Fprintf(w, "# TYPE pizzasql_queries_total counter\n")
  518. fmt.Fprintf(w, "pizzasql_queries_total{status=\"success\"} %d\n", queriesSuccess)
  519. fmt.Fprintf(w, "pizzasql_queries_total{status=\"error\"} %d\n", queriesError)
  520. fmt.Fprintf(w, "\n")
  521. fmt.Fprintf(w, "# HELP pizzasql_queries_executed_total Total queries executed (all statuses)\n")
  522. fmt.Fprintf(w, "# TYPE pizzasql_queries_executed_total counter\n")
  523. fmt.Fprintf(w, "pizzasql_queries_executed_total %d\n", queriesTotal)
  524. fmt.Fprintf(w, "\n")
  525. fmt.Fprintf(w, "# HELP pizzasql_tables_count Number of tables in the database\n")
  526. fmt.Fprintf(w, "# TYPE pizzasql_tables_count gauge\n")
  527. fmt.Fprintf(w, "pizzasql_tables_count %d\n", len(tables))
  528. fmt.Fprintf(w, "\n")
  529. fmt.Fprintf(w, "# HELP pizzasql_uptime_seconds Server uptime in seconds\n")
  530. fmt.Fprintf(w, "# TYPE pizzasql_uptime_seconds gauge\n")
  531. fmt.Fprintf(w, "pizzasql_uptime_seconds %.2f\n", uptime)
  532. fmt.Fprintf(w, "\n")
  533. fmt.Fprintf(w, "# HELP pizzasql_info PizzaSQL server information\n")
  534. fmt.Fprintf(w, "# TYPE pizzasql_info gauge\n")
  535. fmt.Fprintf(w, "pizzasql_info{version=\"%s\"} 1\n", version.PrometheusLabel())
  536. }
  537. // handleExport handles GET /export
  538. func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) {
  539. if r.Method != http.MethodGet {
  540. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only GET method is allowed", nil)
  541. return
  542. }
  543. // Get database from X-Database header
  544. dbName := strings.TrimSpace(r.Header.Get("X-Database"))
  545. _, schema, err := s.getExecutorForDatabase(dbName)
  546. if err != nil {
  547. writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
  548. return
  549. }
  550. // Get the table manager from the database instance
  551. dbInstance, err := s.dbManager.GetDatabase(dbName)
  552. if err != nil {
  553. writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
  554. return
  555. }
  556. // Get format parameter (default: sql)
  557. format := strings.ToLower(r.URL.Query().Get("format"))
  558. if format == "" {
  559. format = "sql"
  560. }
  561. tableName := r.URL.Query().Get("table")
  562. switch format {
  563. case "csv":
  564. // CSV export requires a single table
  565. if tableName == "" {
  566. writeError(w, http.StatusBadRequest, "TABLE_REQUIRED", "CSV export requires 'table' parameter", nil)
  567. return
  568. }
  569. csvOpts := csvexport.DefaultExportOptions()
  570. csvOpts.Table = tableName
  571. data, err := csvexport.ExportTableToBytes(schema, dbInstance.Table, csvOpts)
  572. if err != nil {
  573. writeError(w, http.StatusInternalServerError, "EXPORT_ERROR", err.Error(), nil)
  574. return
  575. }
  576. w.Header().Set("Content-Type", "text/csv")
  577. w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.csv\"", tableName))
  578. w.WriteHeader(http.StatusOK)
  579. w.Write(data)
  580. case "sql", "sqlite":
  581. // SQL export (also used for SQLite-compatible export)
  582. opts := sqlexport.DefaultExportOptions()
  583. // Specific table(s) to export
  584. if tableName != "" {
  585. opts.Tables = strings.Split(tableName, ",")
  586. }
  587. // Include data (default: true)
  588. if r.URL.Query().Get("schema_only") == "true" {
  589. opts.IncludeData = false
  590. }
  591. // Include DROP TABLE statements
  592. if r.URL.Query().Get("drop") == "true" {
  593. opts.DropTables = true
  594. }
  595. // Generate SQL export
  596. sql, err := sqlexport.ExportDatabase(schema, dbInstance.Table, opts)
  597. if err != nil {
  598. writeError(w, http.StatusInternalServerError, "EXPORT_ERROR", err.Error(), nil)
  599. return
  600. }
  601. // Determine filename and content type
  602. ext := "sql"
  603. contentType := "application/sql"
  604. if format == "sqlite" {
  605. ext = "sql" // Still SQL text, but sqlite-compatible
  606. }
  607. filename := schema.GetDatabaseName() + "_export." + ext
  608. if len(opts.Tables) == 1 {
  609. filename = opts.Tables[0] + "_export." + ext
  610. }
  611. w.Header().Set("Content-Type", contentType)
  612. w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
  613. w.WriteHeader(http.StatusOK)
  614. w.Write([]byte(sql))
  615. default:
  616. writeError(w, http.StatusBadRequest, "INVALID_FORMAT",
  617. fmt.Sprintf("Invalid format '%s'. Supported formats: sql, csv", format), nil)
  618. }
  619. }
  620. // handleImport handles POST /import
  621. func (s *Server) handleImport(w http.ResponseWriter, r *http.Request) {
  622. if r.Method != http.MethodPost {
  623. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only POST method is allowed", nil)
  624. return
  625. }
  626. // Get database from X-Database header
  627. dbName := strings.TrimSpace(r.Header.Get("X-Database"))
  628. exec, schema, err := s.getExecutorForDatabase(dbName)
  629. if err != nil {
  630. writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
  631. return
  632. }
  633. // Get the table manager from the database instance
  634. dbInstance, err := s.dbManager.GetDatabase(dbName)
  635. if err != nil {
  636. writeError(w, http.StatusBadRequest, "DATABASE_ERROR", fmt.Sprintf("database not found: %s", dbName), nil)
  637. return
  638. }
  639. // Get format parameter (default: sql, can be auto-detected)
  640. format := strings.ToLower(r.URL.Query().Get("format"))
  641. tableName := r.URL.Query().Get("table")
  642. ignoreErrors := r.URL.Query().Get("ignore_errors") == "true"
  643. createTable := r.URL.Query().Get("create_table") == "true"
  644. pretty := r.URL.Query().Get("pretty") == "true"
  645. // Check content type
  646. contentType := r.Header.Get("Content-Type")
  647. var fileContent []byte
  648. var filename string
  649. if strings.HasPrefix(contentType, "multipart/form-data") {
  650. // Handle file upload
  651. if err := r.ParseMultipartForm(32 << 20); err != nil { // 32MB max
  652. writeError(w, http.StatusBadRequest, "INVALID_FORM", "Failed to parse multipart form: "+err.Error(), nil)
  653. return
  654. }
  655. file, header, err := r.FormFile("file")
  656. if err != nil {
  657. writeError(w, http.StatusBadRequest, "MISSING_FILE", "No file uploaded. Use 'file' field name.", nil)
  658. return
  659. }
  660. defer file.Close()
  661. filename = header.Filename
  662. // Read file content
  663. fileContent, err = io.ReadAll(file)
  664. if err != nil {
  665. writeError(w, http.StatusBadRequest, "READ_ERROR", "Failed to read uploaded file: "+err.Error(), nil)
  666. return
  667. }
  668. } else if strings.HasPrefix(contentType, "application/json") {
  669. // Handle JSON body with SQL content
  670. var req struct {
  671. SQL string `json:"sql"`
  672. }
  673. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  674. writeError(w, http.StatusBadRequest, "INVALID_JSON", "Invalid JSON in request body", nil)
  675. return
  676. }
  677. fileContent = []byte(req.SQL)
  678. } else if strings.HasPrefix(contentType, "text/plain") || strings.HasPrefix(contentType, "application/sql") || strings.HasPrefix(contentType, "text/csv") {
  679. // Handle raw content in body
  680. var err error
  681. fileContent, err = io.ReadAll(r.Body)
  682. if err != nil {
  683. writeError(w, http.StatusBadRequest, "READ_ERROR", "Failed to read request body: "+err.Error(), nil)
  684. return
  685. }
  686. // Auto-detect CSV from content type
  687. if strings.HasPrefix(contentType, "text/csv") && format == "" {
  688. format = "csv"
  689. }
  690. } else {
  691. writeError(w, http.StatusBadRequest, "INVALID_CONTENT_TYPE",
  692. "Content-Type must be multipart/form-data, application/json, text/plain, text/csv, or application/sql", nil)
  693. return
  694. }
  695. if len(fileContent) == 0 {
  696. writeError(w, http.StatusBadRequest, "EMPTY_CONTENT", "No content provided", nil)
  697. return
  698. }
  699. // Auto-detect format from filename extension if not specified
  700. if format == "" && filename != "" {
  701. lower := strings.ToLower(filename)
  702. if strings.HasSuffix(lower, ".csv") {
  703. format = "csv"
  704. } else if strings.HasSuffix(lower, ".db") || strings.HasSuffix(lower, ".sqlite") || strings.HasSuffix(lower, ".sqlite3") {
  705. format = "sqlite"
  706. }
  707. }
  708. // Detect binary SQLite from content-type
  709. if format == "" {
  710. ct := strings.ToLower(contentType)
  711. if strings.Contains(ct, "application/x-sqlite3") || strings.Contains(ct, "application/octet-stream") {
  712. format = "sqlite"
  713. }
  714. }
  715. // Check magic bytes: SQLite files start with "SQLite format 3\000"
  716. if format == "" && len(fileContent) >= 16 && string(fileContent[:15]) == "SQLite format 3" {
  717. format = "sqlite"
  718. }
  719. if format == "" {
  720. format = "sql"
  721. }
  722. switch format {
  723. case "csv":
  724. // CSV import requires table name
  725. if tableName == "" {
  726. writeError(w, http.StatusBadRequest, "TABLE_REQUIRED", "CSV import requires 'table' parameter", nil)
  727. return
  728. }
  729. csvOpts := csvimport.DefaultImportOptions()
  730. csvOpts.TableName = tableName
  731. csvOpts.IgnoreErrors = ignoreErrors
  732. csvOpts.CreateTable = createTable
  733. result, err := csvimport.ImportCSV(strings.NewReader(string(fileContent)), schema, dbInstance.Table, csvOpts)
  734. if err != nil && !ignoreErrors {
  735. writeError(w, http.StatusBadRequest, "IMPORT_ERROR", err.Error(), map[string]interface{}{
  736. "rowsImported": result.RowsImported,
  737. "rowsSkipped": result.RowsSkipped,
  738. "tableCreated": result.TableCreated,
  739. "errors": result.Errors,
  740. })
  741. return
  742. }
  743. // Sync catalog after import
  744. exec.SyncCatalog()
  745. writeJSON(w, http.StatusOK, result, pretty)
  746. case "sqlite":
  747. // Binary SQLite .db import
  748. opts := sqliteimport.DefaultImportOptions()
  749. opts.CreateTables = r.URL.Query().Get("create_tables") != "false"
  750. opts.IgnoreErrors = ignoreErrors
  751. result, err := sqliteimport.ImportSQLiteBytes(fileContent, exec, opts)
  752. if err != nil && !ignoreErrors {
  753. writeError(w, http.StatusBadRequest, "IMPORT_ERROR", err.Error(), map[string]interface{}{
  754. "tablesCreated": result.TablesCreated,
  755. "rowsInserted": result.RowsInserted,
  756. "errors": result.Errors,
  757. })
  758. return
  759. }
  760. exec.SyncCatalog()
  761. writeJSON(w, http.StatusOK, result, pretty)
  762. case "sql":
  763. // SQL text import
  764. opts := sqlimport.DefaultImportOptions()
  765. opts.IgnoreErrors = ignoreErrors
  766. result, err := sqlimport.ImportSQL(exec, string(fileContent), opts)
  767. if err != nil && !ignoreErrors {
  768. writeError(w, http.StatusBadRequest, "IMPORT_ERROR", err.Error(), map[string]interface{}{
  769. "statementsExecuted": result.StatementsExecuted,
  770. "tablesCreated": result.TablesCreated,
  771. "rowsInserted": result.RowsInserted,
  772. "errors": result.Errors,
  773. })
  774. return
  775. }
  776. // Sync catalog after import
  777. exec.SyncCatalog()
  778. writeJSON(w, http.StatusOK, result, pretty)
  779. default:
  780. writeError(w, http.StatusBadRequest, "INVALID_FORMAT",
  781. fmt.Sprintf("Invalid format '%s'. Supported formats: sql, csv", format), nil)
  782. }
  783. }