2
0

handler.go 33 KB

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