| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551 |
- package main
- import (
- "bufio"
- "context"
- "flag"
- "fmt"
- "net/http"
- "os"
- "os/signal"
- "strings"
- "syscall"
- "time"
- "github.com/danfragoso/pizzasql-next/pkg/executor"
- "github.com/danfragoso/pizzasql-next/pkg/httpserver"
- "github.com/danfragoso/pizzasql-next/pkg/lexer"
- "github.com/danfragoso/pizzasql-next/pkg/parser"
- "github.com/danfragoso/pizzasql-next/pkg/storage"
- )
- var (
- kvAddr = flag.String("kv", "localhost:8085", "PizzaKV server address")
- database = flag.String("db", "pizzasql", "Database name")
- poolSize = flag.Int("pool", 5, "Connection pool size")
- timeout = flag.Duration("timeout", 30*time.Second, "Query timeout")
- httpEnable = flag.Bool("http", false, "Enable HTTP server")
- httpHost = flag.String("http-host", "localhost", "HTTP server host")
- httpPort = flag.Int("http-port", 8080, "HTTP server port")
- httpCORS = flag.Bool("http-cors", true, "Enable CORS")
- httpAuth = flag.Bool("http-auth", false, "Enable authentication")
- apiKeys = flag.String("api-keys", "", "Comma-separated API keys")
- )
- func main() {
- flag.Parse()
- // Check if HTTP server mode is enabled
- if *httpEnable {
- runHTTPServer()
- return
- }
- // Check for command-line SQL
- args := flag.Args()
- if len(args) > 0 {
- // Execute single SQL statement
- sql := strings.Join(args, " ")
- executeSingle(sql)
- return
- }
- // Check for piped input
- stat, _ := os.Stdin.Stat()
- if (stat.Mode() & os.ModeCharDevice) == 0 {
- // Input is from pipe
- executePipe()
- return
- }
- // Interactive REPL mode
- runREPL()
- }
- func executeSingle(sql string) {
- // Try to connect to PizzaKV
- pool, err := storage.NewKVPool(*kvAddr, *poolSize, *timeout)
- if err != nil {
- // Fall back to expression-only mode
- executeExpressionOnly(sql)
- return
- }
- defer pool.Close()
- schema := storage.NewSchemaManager(pool, *database)
- table := storage.NewTableManager(pool, schema, *database)
- exec := executor.New(schema, table)
- exec.SyncCatalog()
- result, err := executeSQL(exec, sql)
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error: %v\n", err)
- os.Exit(1)
- }
- fmt.Print(result.String())
- }
- func executePipe() {
- // Try to connect to PizzaKV
- pool, err := storage.NewKVPool(*kvAddr, *poolSize, *timeout)
- if err != nil {
- // Fall back to expression-only mode
- scanner := bufio.NewScanner(os.Stdin)
- for scanner.Scan() {
- sql := strings.TrimSpace(scanner.Text())
- if sql == "" || strings.HasPrefix(sql, "--") {
- continue
- }
- executeExpressionOnly(sql)
- }
- return
- }
- defer pool.Close()
- schema := storage.NewSchemaManager(pool, *database)
- table := storage.NewTableManager(pool, schema, *database)
- exec := executor.New(schema, table)
- exec.SyncCatalog()
- scanner := bufio.NewScanner(os.Stdin)
- for scanner.Scan() {
- sql := strings.TrimSpace(scanner.Text())
- if sql == "" || strings.HasPrefix(sql, "--") {
- continue
- }
- result, err := executeSQL(exec, sql)
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error: %v\n", err)
- continue
- }
- fmt.Print(result.String())
- }
- }
- func runREPL() {
- fmt.Println("PizzaSQL - SQL-92 compatible database")
- fmt.Println("Type 'help' for usage, 'quit' to exit")
- fmt.Println()
- // Try to connect to PizzaKV
- var pool *storage.KVPool
- var schema *storage.SchemaManager
- var table *storage.TableManager
- var exec *executor.Executor
- pool, err := storage.NewKVPool(*kvAddr, *poolSize, *timeout)
- if err != nil {
- fmt.Printf("Warning: Cannot connect to PizzaKV at %s\n", *kvAddr)
- fmt.Println("Running in expression-only mode (no table storage)")
- fmt.Println()
- } else {
- schema = storage.NewSchemaManager(pool, *database)
- table = storage.NewTableManager(pool, schema, *database)
- exec = executor.New(schema, table)
- exec.SyncCatalog()
- fmt.Printf("Connected to PizzaKV at %s (database: %s)\n\n", *kvAddr, *database)
- }
- reader := bufio.NewReader(os.Stdin)
- var sqlBuffer strings.Builder
- for {
- if sqlBuffer.Len() == 0 {
- fmt.Print("pizzasql> ")
- } else {
- fmt.Print(" -> ")
- }
- line, err := reader.ReadString('\n')
- if err != nil {
- fmt.Println()
- break
- }
- line = strings.TrimSpace(line)
- // Handle special commands
- switch strings.ToLower(line) {
- case "quit", "exit", "\\q":
- fmt.Println("Goodbye!")
- if pool != nil {
- pool.Close()
- }
- return
- case "help", "\\h":
- printHelp()
- continue
- case "tables", "\\dt":
- if schema != nil {
- listTables(schema)
- } else {
- fmt.Println("Not connected to database")
- }
- continue
- case "clear", "\\c":
- sqlBuffer.Reset()
- fmt.Println("Buffer cleared")
- continue
- }
- // Skip empty lines and comments
- if line == "" || strings.HasPrefix(line, "--") {
- continue
- }
- // Accumulate SQL
- if sqlBuffer.Len() > 0 {
- sqlBuffer.WriteString(" ")
- }
- sqlBuffer.WriteString(line)
- // Check if statement is complete (ends with semicolon)
- sql := sqlBuffer.String()
- if !strings.HasSuffix(sql, ";") {
- continue
- }
- // Remove semicolon and execute
- sql = strings.TrimSuffix(sql, ";")
- sqlBuffer.Reset()
- if exec != nil {
- result, err := executeSQL(exec, sql)
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error: %v\n", err)
- continue
- }
- fmt.Print(result.String())
- } else {
- executeExpressionOnly(sql)
- }
- }
- }
- func executeSQL(exec *executor.Executor, sql string) (*executor.Result, error) {
- l := lexer.New(sql)
- p := parser.New(l)
- stmt, err := p.Parse()
- if err != nil {
- return nil, fmt.Errorf("parse error: %w", err)
- }
- return exec.Execute(stmt)
- }
- func executeExpressionOnly(sql string) {
- l := lexer.New(sql)
- p := parser.New(l)
- stmt, err := p.Parse()
- if err != nil {
- fmt.Fprintf(os.Stderr, "Parse error: %v\n", err)
- return
- }
- // For SELECT statements without FROM, we can evaluate expressions
- if sel, ok := stmt.(*parser.SelectStmt); ok && len(sel.From) == 0 {
- exec := &executor.Executor{}
- result, err := executeSelectExpr(exec, sel)
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error: %v\n", err)
- return
- }
- fmt.Print(result.String())
- return
- }
- // For other statements, just print what was parsed
- switch s := stmt.(type) {
- case *parser.SelectStmt:
- fmt.Printf("SELECT statement with %d columns\n", len(s.Columns))
- if len(s.From) > 0 {
- fmt.Printf(" FROM: %s\n", s.From[0].Name)
- }
- if s.Where != nil {
- fmt.Println(" WHERE: <condition>")
- }
- fmt.Println("(Not connected to database - cannot execute)")
- case *parser.InsertStmt:
- fmt.Printf("INSERT into %s (%d rows)\n", s.Table.Name, len(s.Values))
- fmt.Println("(Not connected to database - cannot execute)")
- case *parser.UpdateStmt:
- fmt.Printf("UPDATE %s (%d assignments)\n", s.Table.Name, len(s.Set))
- fmt.Println("(Not connected to database - cannot execute)")
- case *parser.DeleteStmt:
- fmt.Printf("DELETE from %s\n", s.Table.Name)
- fmt.Println("(Not connected to database - cannot execute)")
- case *parser.CreateTableStmt:
- fmt.Printf("CREATE TABLE %s (%d columns)\n", s.Table.Name, len(s.Columns))
- fmt.Println("(Not connected to database - cannot execute)")
- case *parser.DropTableStmt:
- fmt.Printf("DROP TABLE %s\n", s.Tables[0].Name)
- fmt.Println("(Not connected to database - cannot execute)")
- default:
- fmt.Printf("Parsed: %T\n", stmt)
- }
- }
- // executeSelectExpr handles SELECT without FROM (expression evaluation)
- func executeSelectExpr(exec *executor.Executor, stmt *parser.SelectStmt) (*executor.Result, error) {
- result := executor.NewResult("SELECT")
- // Determine columns
- for i, col := range stmt.Columns {
- if col.Alias != "" {
- result.AddColumn(col.Alias)
- } else {
- result.AddColumn(fmt.Sprintf("column%d", i+1))
- }
- }
- // Evaluate expressions using reflection to access private method
- // For simplicity, we'll use a minimal evaluator here
- values := make([]interface{}, len(stmt.Columns))
- for i, col := range stmt.Columns {
- val, err := evalExprSimple(col.Expr)
- if err != nil {
- return nil, err
- }
- values[i] = val
- }
- result.AddRow(values...)
- return result, nil
- }
- // evalExprSimple is a simplified expression evaluator for standalone expressions
- func evalExprSimple(expr parser.Expr) (interface{}, error) {
- switch e := expr.(type) {
- case *parser.LiteralExpr:
- switch e.Type {
- case lexer.TokenNumber:
- if strings.Contains(e.Value, ".") {
- var f float64
- fmt.Sscanf(e.Value, "%f", &f)
- return f, nil
- }
- var i int64
- fmt.Sscanf(e.Value, "%d", &i)
- return i, nil
- case lexer.TokenString:
- return e.Value, nil
- case lexer.TokenNULL:
- return nil, nil
- case lexer.TokenTRUE:
- return true, nil
- case lexer.TokenFALSE:
- return false, nil
- }
- case *parser.BinaryExpr:
- left, err := evalExprSimple(e.Left)
- if err != nil {
- return nil, err
- }
- right, err := evalExprSimple(e.Right)
- if err != nil {
- return nil, err
- }
- return evalBinarySimple(e.Op, left, right)
- case *parser.UnaryExpr:
- val, err := evalExprSimple(e.Operand)
- if err != nil {
- return nil, err
- }
- switch e.Op {
- case lexer.TokenMinus:
- return -toFloatSimple(val), nil
- case lexer.TokenNOT:
- return !toBoolSimple(val), nil
- }
- return val, nil
- case *parser.ParenExpr:
- return evalExprSimple(e.Expr)
- }
- return nil, fmt.Errorf("unsupported expression type: %T", expr)
- }
- func evalBinarySimple(op lexer.TokenType, left, right interface{}) (interface{}, error) {
- switch op {
- case lexer.TokenPlus:
- return toFloatSimple(left) + toFloatSimple(right), nil
- case lexer.TokenMinus:
- return toFloatSimple(left) - toFloatSimple(right), nil
- case lexer.TokenStar:
- return toFloatSimple(left) * toFloatSimple(right), nil
- case lexer.TokenSlash:
- r := toFloatSimple(right)
- if r == 0 {
- return nil, nil
- }
- return toFloatSimple(left) / r, nil
- case lexer.TokenEq:
- return compareSimple(left, right) == 0, nil
- case lexer.TokenNeq:
- return compareSimple(left, right) != 0, nil
- case lexer.TokenLt:
- return compareSimple(left, right) < 0, nil
- case lexer.TokenGt:
- return compareSimple(left, right) > 0, nil
- case lexer.TokenLte:
- return compareSimple(left, right) <= 0, nil
- case lexer.TokenGte:
- return compareSimple(left, right) >= 0, nil
- case lexer.TokenAND:
- return toBoolSimple(left) && toBoolSimple(right), nil
- case lexer.TokenOR:
- return toBoolSimple(left) || toBoolSimple(right), nil
- }
- return nil, fmt.Errorf("unsupported operator: %v", op)
- }
- func toFloatSimple(v interface{}) float64 {
- switch val := v.(type) {
- case int64:
- return float64(val)
- case float64:
- return val
- case bool:
- if val {
- return 1
- }
- return 0
- }
- return 0
- }
- func toBoolSimple(v interface{}) bool {
- switch val := v.(type) {
- case bool:
- return val
- case int64:
- return val != 0
- case float64:
- return val != 0
- }
- return false
- }
- func compareSimple(a, b interface{}) int {
- fa := toFloatSimple(a)
- fb := toFloatSimple(b)
- if fa < fb {
- return -1
- }
- if fa > fb {
- return 1
- }
- return 0
- }
- func printHelp() {
- fmt.Println("PizzaSQL Commands:")
- fmt.Println(" help, \\h Show this help")
- fmt.Println(" quit, \\q Exit the program")
- fmt.Println(" tables, \\dt List all tables")
- fmt.Println(" clear, \\c Clear the input buffer")
- fmt.Println()
- fmt.Println("SQL Statements (end with semicolon):")
- fmt.Println(" SELECT ... FROM ... WHERE ...")
- fmt.Println(" INSERT INTO table (cols) VALUES (...)")
- fmt.Println(" UPDATE table SET col = val WHERE ...")
- fmt.Println(" DELETE FROM table WHERE ...")
- fmt.Println(" CREATE TABLE table (col TYPE, ...)")
- fmt.Println(" DROP TABLE table")
- fmt.Println()
- fmt.Println("Expression Mode (SELECT without FROM):")
- fmt.Println(" SELECT 1 + 2 * 3;")
- fmt.Println(" SELECT UPPER('hello');")
- }
- func listTables(schema *storage.SchemaManager) {
- tables, err := schema.ListTables()
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error: %v\n", err)
- return
- }
- if len(tables) == 0 {
- fmt.Println("No tables found")
- return
- }
- fmt.Println("Tables:")
- for _, t := range tables {
- fmt.Printf(" %s\n", t)
- }
- }
- func runHTTPServer() {
- // Connect to PizzaKV
- pool, err := storage.NewKVPool(*kvAddr, *poolSize, *timeout)
- if err != nil {
- fmt.Fprintf(os.Stderr, "Failed to connect to PizzaKV at %s: %v\n", *kvAddr, err)
- fmt.Fprintf(os.Stderr, "Make sure PizzaKV is running: pizzakv\n")
- os.Exit(1)
- }
- defer pool.Close()
- // Create schema and executor
- schema := storage.NewSchemaManager(pool, *database)
- table := storage.NewTableManager(pool, schema, *database)
- exec := executor.New(schema, table)
- // Configure HTTP server
- config := httpserver.DefaultConfig()
- config.Host = *httpHost
- config.Port = *httpPort
- config.EnableCORS = *httpCORS
- config.EnableAuth = *httpAuth
- if *apiKeys != "" {
- config.APIKeys = strings.Split(*apiKeys, ",")
- }
- // Create and start server
- server := httpserver.New(config, exec, schema)
- // Handle graceful shutdown
- stop := make(chan os.Signal, 1)
- signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
- // Start server in goroutine
- go func() {
- if err := server.Start(); err != nil && err != http.ErrServerClosed {
- fmt.Fprintf(os.Stderr, "HTTP server error: %v\n", err)
- os.Exit(1)
- }
- }()
- fmt.Printf("PizzaSQL HTTP server started on http://%s:%d\n", *httpHost, *httpPort)
- fmt.Printf("Database: %s\n", *database)
- fmt.Printf("PizzaKV: %s\n", *kvAddr)
- fmt.Println()
- fmt.Println("Endpoints:")
- fmt.Println(" POST /query - Execute SQL query")
- fmt.Println(" POST /execute - Batch execution")
- fmt.Println(" GET /schema/tables - List tables")
- fmt.Println(" GET /schema/tables/{name} - Table schema")
- fmt.Println(" GET /health - Health check")
- fmt.Println(" GET /stats - Statistics")
- fmt.Println(" GET /metrics - Prometheus metrics")
- fmt.Println(" POST /transaction/begin - Begin transaction")
- fmt.Println(" POST /transaction/commit - Commit transaction")
- fmt.Println(" POST /transaction/rollback - Rollback transaction")
- fmt.Println()
- fmt.Println("Example:")
- fmt.Printf(" curl -X POST http://%s:%d/query -H 'Content-Type: application/json' -d '{\"sql\":\"SELECT 1+1\"}'\n", *httpHost, *httpPort)
- fmt.Println()
- fmt.Println("Press Ctrl+C to stop")
- <-stop
- fmt.Println("\nShutting down server...")
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- if err := server.Shutdown(ctx); err != nil {
- fmt.Fprintf(os.Stderr, "Error during shutdown: %v\n", err)
- }
- fmt.Println("Server stopped")
- }
|