2
0

handler.go 25 KB

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