2
0

handler.go 33 KB

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