2
0

handler.go 27 KB

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