2
0

handler.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. package httpserver
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "strings"
  7. "sync/atomic"
  8. "time"
  9. "github.com/danfragoso/pizzasql-next/pkg/lexer"
  10. "github.com/danfragoso/pizzasql-next/pkg/parser"
  11. )
  12. // QueryRequest represents a single query request.
  13. type QueryRequest struct {
  14. SQL string `json:"sql"`
  15. Params []interface{} `json:"params"`
  16. }
  17. // ExecuteRequest represents a batch execution request.
  18. type ExecuteRequest struct {
  19. Statements []QueryRequest `json:"statements"`
  20. Transaction bool `json:"transaction"`
  21. }
  22. // TransactionRequest represents a transaction management request.
  23. type TransactionRequest struct {
  24. TransactionID string `json:"transactionId"`
  25. }
  26. // handleQuery handles POST /query
  27. func (s *Server) handleQuery(w http.ResponseWriter, r *http.Request) {
  28. if r.Method != http.MethodPost {
  29. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only POST method is allowed", nil)
  30. return
  31. }
  32. var req QueryRequest
  33. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  34. writeError(w, http.StatusBadRequest, "INVALID_JSON", "Invalid JSON in request body", nil)
  35. return
  36. }
  37. if req.SQL == "" {
  38. writeError(w, http.StatusBadRequest, "MISSING_SQL", "SQL query is required", nil)
  39. return
  40. }
  41. // Check for pretty print
  42. pretty := r.URL.Query().Get("pretty") == "true"
  43. explain := r.URL.Query().Get("explain") == "true"
  44. readonly := r.URL.Query().Get("readonly") == "true"
  45. // Parse timeout
  46. timeout := 5 * time.Minute
  47. if t := r.URL.Query().Get("timeout"); t != "" {
  48. if d, err := time.ParseDuration(t); err == nil {
  49. timeout = d
  50. }
  51. }
  52. // Execute with timeout
  53. resultChan := make(chan *QueryResponse, 1)
  54. errorChan := make(chan error, 1)
  55. go func() {
  56. start := time.Now()
  57. // Check readonly mode
  58. if readonly {
  59. upper := strings.ToUpper(strings.TrimSpace(req.SQL))
  60. if strings.HasPrefix(upper, "INSERT") ||
  61. strings.HasPrefix(upper, "UPDATE") ||
  62. strings.HasPrefix(upper, "DELETE") ||
  63. strings.HasPrefix(upper, "CREATE") ||
  64. strings.HasPrefix(upper, "DROP") ||
  65. strings.HasPrefix(upper, "ALTER") {
  66. errorChan <- &HTTPError{
  67. Code: "READ_ONLY_MODE",
  68. Message: "Write operations not allowed in read-only mode",
  69. Status: http.StatusForbidden,
  70. }
  71. return
  72. }
  73. }
  74. // Substitute parameters
  75. sql := substituteParams(req.SQL, req.Params)
  76. // Parse SQL
  77. l := lexer.New(sql)
  78. p := parser.New(l)
  79. stmt, err := p.Parse()
  80. if err != nil {
  81. errorChan <- &HTTPError{
  82. Code: "SYNTAX_ERROR",
  83. Message: err.Error(),
  84. Status: http.StatusBadRequest,
  85. }
  86. return
  87. }
  88. // Execute
  89. result, err := s.executor.Execute(stmt)
  90. if err != nil {
  91. errorChan <- &HTTPError{
  92. Code: "EXECUTION_ERROR",
  93. Message: err.Error(),
  94. Status: http.StatusInternalServerError,
  95. }
  96. return
  97. }
  98. duration := time.Since(start)
  99. // Build response
  100. resp := &QueryResponse{
  101. Columns: make([]ColumnInfo, len(result.Columns)),
  102. Rows: result.Rows,
  103. RowsAffected: result.RowsAffected,
  104. LastInsertID: result.LastInsertID,
  105. ExecutionTime: duration.String(),
  106. }
  107. for i, col := range result.Columns {
  108. colType := result.GetColumnType(i)
  109. // If no type info, infer from first row values
  110. if colType == "ANY" && len(result.Rows) > 0 && i < len(result.Rows[0]) {
  111. colType = inferType(result.Rows[0][i])
  112. }
  113. resp.Columns[i] = ColumnInfo{
  114. Name: col,
  115. Type: colType,
  116. }
  117. }
  118. if explain {
  119. resp.QueryPlan = []string{"Full table scan"} // TODO: Real query plan
  120. }
  121. resultChan <- resp
  122. }()
  123. select {
  124. case resp := <-resultChan:
  125. atomic.AddInt64(&s.stats.QueriesExecuted, 1)
  126. atomic.AddInt64(&s.stats.QueriesSuccess, 1)
  127. writeJSON(w, http.StatusOK, resp, pretty)
  128. case err := <-errorChan:
  129. atomic.AddInt64(&s.stats.QueriesExecuted, 1)
  130. atomic.AddInt64(&s.stats.QueriesError, 1)
  131. if httpErr, ok := err.(*HTTPError); ok {
  132. writeError(w, httpErr.Status, httpErr.Code, httpErr.Message, httpErr.Details)
  133. } else {
  134. writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR", err.Error(), nil)
  135. }
  136. case <-time.After(timeout):
  137. atomic.AddInt64(&s.stats.QueriesExecuted, 1)
  138. atomic.AddInt64(&s.stats.QueriesError, 1)
  139. writeError(w, http.StatusRequestTimeout, "TIMEOUT", "Query execution timeout", nil)
  140. }
  141. }
  142. // handleExecute handles POST /execute for batch operations
  143. func (s *Server) handleExecute(w http.ResponseWriter, r *http.Request) {
  144. if r.Method != http.MethodPost {
  145. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only POST method is allowed", nil)
  146. return
  147. }
  148. var req ExecuteRequest
  149. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  150. writeError(w, http.StatusBadRequest, "INVALID_JSON", "Invalid JSON in request body", nil)
  151. return
  152. }
  153. if len(req.Statements) == 0 {
  154. writeError(w, http.StatusBadRequest, "MISSING_STATEMENTS", "At least one statement is required", nil)
  155. return
  156. }
  157. pretty := r.URL.Query().Get("pretty") == "true"
  158. start := time.Now()
  159. results := make([]ExecuteResult, 0, len(req.Statements))
  160. // Start transaction if requested
  161. if req.Transaction {
  162. l := lexer.New("BEGIN")
  163. p := parser.New(l)
  164. stmt, _ := p.Parse()
  165. s.executor.Execute(stmt)
  166. }
  167. var executeErr error
  168. for _, stmt := range req.Statements {
  169. // Substitute parameters
  170. sql := substituteParams(stmt.SQL, stmt.Params)
  171. l := lexer.New(sql)
  172. p := parser.New(l)
  173. parsed, err := p.Parse()
  174. if err != nil {
  175. executeErr = err
  176. break
  177. }
  178. result, err := s.executor.Execute(parsed)
  179. if err != nil {
  180. executeErr = err
  181. break
  182. }
  183. results = append(results, ExecuteResult{
  184. RowsAffected: result.RowsAffected,
  185. LastInsertID: result.LastInsertID,
  186. })
  187. }
  188. // Handle transaction
  189. if req.Transaction {
  190. if executeErr != nil {
  191. // Rollback on error
  192. l := lexer.New("ROLLBACK")
  193. p := parser.New(l)
  194. stmt, _ := p.Parse()
  195. s.executor.Execute(stmt)
  196. writeError(w, http.StatusBadRequest, "TRANSACTION_ERROR", executeErr.Error(), nil)
  197. return
  198. } else {
  199. // Commit on success
  200. l := lexer.New("COMMIT")
  201. p := parser.New(l)
  202. stmt, _ := p.Parse()
  203. s.executor.Execute(stmt)
  204. }
  205. } else if executeErr != nil {
  206. writeError(w, http.StatusBadRequest, "EXECUTION_ERROR", executeErr.Error(), nil)
  207. return
  208. }
  209. resp := &ExecuteResponse{
  210. Results: results,
  211. ExecutionTime: time.Since(start).String(),
  212. }
  213. writeJSON(w, http.StatusOK, resp, pretty)
  214. }
  215. // handleSchemaTables handles GET /schema/tables
  216. func (s *Server) handleSchemaTables(w http.ResponseWriter, r *http.Request) {
  217. if r.Method != http.MethodGet {
  218. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only GET method is allowed", nil)
  219. return
  220. }
  221. tables, err := s.schema.ListTables()
  222. if err != nil {
  223. writeError(w, http.StatusInternalServerError, "SCHEMA_ERROR", err.Error(), nil)
  224. return
  225. }
  226. resp := map[string]interface{}{
  227. "tables": tables,
  228. }
  229. pretty := r.URL.Query().Get("pretty") == "true"
  230. writeJSON(w, http.StatusOK, resp, pretty)
  231. }
  232. // handleSchemaTable handles GET /schema/tables/{table}
  233. func (s *Server) handleSchemaTable(w http.ResponseWriter, r *http.Request) {
  234. if r.Method != http.MethodGet {
  235. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only GET method is allowed", nil)
  236. return
  237. }
  238. // Extract table name from path
  239. path := strings.TrimPrefix(r.URL.Path, "/schema/tables/")
  240. tableName := strings.TrimSpace(path)
  241. if tableName == "" {
  242. writeError(w, http.StatusBadRequest, "MISSING_TABLE_NAME", "Table name is required", nil)
  243. return
  244. }
  245. schema, err := s.schema.GetSchema(tableName)
  246. if err != nil {
  247. writeError(w, http.StatusNotFound, "TABLE_NOT_FOUND", fmt.Sprintf("Table '%s' not found", tableName), nil)
  248. return
  249. }
  250. columns := make([]map[string]interface{}, len(schema.Columns))
  251. for i, col := range schema.Columns {
  252. columns[i] = map[string]interface{}{
  253. "name": col.Name,
  254. "type": col.Type,
  255. "nullable": col.Nullable,
  256. "primaryKey": col.PrimaryKey,
  257. "default": col.Default,
  258. }
  259. }
  260. resp := map[string]interface{}{
  261. "name": schema.Name,
  262. "columns": columns,
  263. }
  264. pretty := r.URL.Query().Get("pretty") == "true"
  265. writeJSON(w, http.StatusOK, resp, pretty)
  266. }
  267. // handleHealth handles GET /health
  268. func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
  269. resp := map[string]interface{}{
  270. "status": "ok",
  271. "version": "0.1.0",
  272. "uptime": time.Since(s.stats.StartTime).String(),
  273. }
  274. pretty := r.URL.Query().Get("pretty") == "true"
  275. writeJSON(w, http.StatusOK, resp, pretty)
  276. }
  277. // handleStats handles GET /stats
  278. func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
  279. tables, _ := s.schema.ListTables()
  280. var avgQueryTime string
  281. if s.stats.QueriesExecuted > 0 {
  282. avgQueryTime = "N/A" // TODO: Track actual query times
  283. } else {
  284. avgQueryTime = "0ms"
  285. }
  286. resp := map[string]interface{}{
  287. "queriesExecuted": atomic.LoadInt64(&s.stats.QueriesExecuted),
  288. "queriesSuccess": atomic.LoadInt64(&s.stats.QueriesSuccess),
  289. "queriesError": atomic.LoadInt64(&s.stats.QueriesError),
  290. "tablesCount": len(tables),
  291. "avgQueryTime": avgQueryTime,
  292. "uptime": time.Since(s.stats.StartTime).String(),
  293. }
  294. pretty := r.URL.Query().Get("pretty") == "true"
  295. writeJSON(w, http.StatusOK, resp, pretty)
  296. }
  297. // handleTransactionBegin handles POST /transaction/begin
  298. func (s *Server) handleTransactionBegin(w http.ResponseWriter, r *http.Request) {
  299. if r.Method != http.MethodPost {
  300. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only POST method is allowed", nil)
  301. return
  302. }
  303. l := lexer.New("BEGIN")
  304. p := parser.New(l)
  305. stmt, _ := p.Parse()
  306. _, err := s.executor.Execute(stmt)
  307. if err != nil {
  308. writeError(w, http.StatusInternalServerError, "TRANSACTION_ERROR", err.Error(), nil)
  309. return
  310. }
  311. // Generate transaction ID (simple implementation)
  312. txID := fmt.Sprintf("tx-%d", time.Now().UnixNano())
  313. resp := map[string]interface{}{
  314. "transactionId": txID,
  315. }
  316. pretty := r.URL.Query().Get("pretty") == "true"
  317. writeJSON(w, http.StatusOK, resp, pretty)
  318. }
  319. // handleTransactionCommit handles POST /transaction/commit
  320. func (s *Server) handleTransactionCommit(w http.ResponseWriter, r *http.Request) {
  321. if r.Method != http.MethodPost {
  322. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only POST method is allowed", nil)
  323. return
  324. }
  325. var req TransactionRequest
  326. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  327. // Allow commit without transaction ID for simplicity
  328. }
  329. l := lexer.New("COMMIT")
  330. p := parser.New(l)
  331. stmt, _ := p.Parse()
  332. _, err := s.executor.Execute(stmt)
  333. if err != nil {
  334. writeError(w, http.StatusInternalServerError, "TRANSACTION_ERROR", err.Error(), nil)
  335. return
  336. }
  337. resp := map[string]interface{}{
  338. "status": "committed",
  339. }
  340. pretty := r.URL.Query().Get("pretty") == "true"
  341. writeJSON(w, http.StatusOK, resp, pretty)
  342. }
  343. // substituteParams replaces ? placeholders with actual parameter values.
  344. // This is a simple implementation that handles basic SQL escaping.
  345. func substituteParams(sql string, params []interface{}) string {
  346. if len(params) == 0 {
  347. return sql
  348. }
  349. result := sql
  350. for _, param := range params {
  351. idx := strings.Index(result, "?")
  352. if idx == -1 {
  353. break
  354. }
  355. var replacement string
  356. switch v := param.(type) {
  357. case nil:
  358. replacement = "NULL"
  359. case string:
  360. // Escape single quotes in strings
  361. escaped := strings.ReplaceAll(v, "'", "''")
  362. replacement = "'" + escaped + "'"
  363. case int, int64, int32, int16, int8:
  364. replacement = fmt.Sprintf("%d", v)
  365. case float64, float32:
  366. replacement = fmt.Sprintf("%g", v)
  367. case bool:
  368. if v {
  369. replacement = "1"
  370. } else {
  371. replacement = "0"
  372. }
  373. default:
  374. // For other types, convert to string
  375. escaped := strings.ReplaceAll(fmt.Sprintf("%v", v), "'", "''")
  376. replacement = "'" + escaped + "'"
  377. }
  378. result = result[:idx] + replacement + result[idx+1:]
  379. }
  380. return result
  381. }
  382. // inferType infers SQL type from a Go value.
  383. func inferType(v interface{}) string {
  384. switch v.(type) {
  385. case nil:
  386. return "NULL"
  387. case int, int64, int32, int16, int8:
  388. return "INTEGER"
  389. case float64, float32:
  390. return "REAL"
  391. case string:
  392. return "TEXT"
  393. case []byte:
  394. return "BLOB"
  395. case bool:
  396. return "INTEGER"
  397. default:
  398. return "ANY"
  399. }
  400. }
  401. // handleTransactionRollback handles POST /transaction/rollback
  402. func (s *Server) handleTransactionRollback(w http.ResponseWriter, r *http.Request) {
  403. if r.Method != http.MethodPost {
  404. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only POST method is allowed", nil)
  405. return
  406. }
  407. var req TransactionRequest
  408. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  409. // Allow rollback without transaction ID for simplicity
  410. }
  411. l := lexer.New("ROLLBACK")
  412. p := parser.New(l)
  413. stmt, _ := p.Parse()
  414. _, err := s.executor.Execute(stmt)
  415. if err != nil {
  416. writeError(w, http.StatusInternalServerError, "TRANSACTION_ERROR", err.Error(), nil)
  417. return
  418. }
  419. resp := map[string]interface{}{
  420. "status": "rolled back",
  421. }
  422. pretty := r.URL.Query().Get("pretty") == "true"
  423. writeJSON(w, http.StatusOK, resp, pretty)
  424. }
  425. // handleMetrics handles GET /metrics in Prometheus format
  426. func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) {
  427. if r.Method != http.MethodGet {
  428. writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Only GET method is allowed", nil)
  429. return
  430. }
  431. tables, _ := s.schema.ListTables()
  432. uptime := time.Since(s.stats.StartTime).Seconds()
  433. queriesTotal := atomic.LoadInt64(&s.stats.QueriesExecuted)
  434. queriesSuccess := atomic.LoadInt64(&s.stats.QueriesSuccess)
  435. queriesError := atomic.LoadInt64(&s.stats.QueriesError)
  436. w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
  437. // Write Prometheus format metrics
  438. fmt.Fprintf(w, "# HELP pizzasql_queries_total Total number of queries executed\n")
  439. fmt.Fprintf(w, "# TYPE pizzasql_queries_total counter\n")
  440. fmt.Fprintf(w, "pizzasql_queries_total{status=\"success\"} %d\n", queriesSuccess)
  441. fmt.Fprintf(w, "pizzasql_queries_total{status=\"error\"} %d\n", queriesError)
  442. fmt.Fprintf(w, "\n")
  443. fmt.Fprintf(w, "# HELP pizzasql_queries_executed_total Total queries executed (all statuses)\n")
  444. fmt.Fprintf(w, "# TYPE pizzasql_queries_executed_total counter\n")
  445. fmt.Fprintf(w, "pizzasql_queries_executed_total %d\n", queriesTotal)
  446. fmt.Fprintf(w, "\n")
  447. fmt.Fprintf(w, "# HELP pizzasql_tables_count Number of tables in the database\n")
  448. fmt.Fprintf(w, "# TYPE pizzasql_tables_count gauge\n")
  449. fmt.Fprintf(w, "pizzasql_tables_count %d\n", len(tables))
  450. fmt.Fprintf(w, "\n")
  451. fmt.Fprintf(w, "# HELP pizzasql_uptime_seconds Server uptime in seconds\n")
  452. fmt.Fprintf(w, "# TYPE pizzasql_uptime_seconds gauge\n")
  453. fmt.Fprintf(w, "pizzasql_uptime_seconds %.2f\n", uptime)
  454. fmt.Fprintf(w, "\n")
  455. fmt.Fprintf(w, "# HELP pizzasql_info PizzaSQL server information\n")
  456. fmt.Fprintf(w, "# TYPE pizzasql_info gauge\n")
  457. fmt.Fprintf(w, "pizzasql_info{version=\"0.1.0\"} 1\n")
  458. }