handler.go 34 KB

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