handler.go 30 KB

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