2
0

handler.go 29 KB

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