handler.go 28 KB

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