2
0

handler.go 30 KB

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