handler.go 28 KB

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