| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- package sqlimport
- import (
- "fmt"
- "strings"
- "github.com/danfragoso/pizzasql-next/pkg/executor"
- "github.com/danfragoso/pizzasql-next/pkg/lexer"
- "github.com/danfragoso/pizzasql-next/pkg/parser"
- )
- // ImportOptions configures import behavior.
- type ImportOptions struct {
- IgnoreErrors bool // Continue on individual statement errors
- }
- // DefaultImportOptions returns sensible defaults.
- func DefaultImportOptions() ImportOptions {
- return ImportOptions{
- IgnoreErrors: false,
- }
- }
- // ImportResult contains the results of an import operation.
- type ImportResult struct {
- StatementsExecuted int `json:"statementsExecuted"`
- TablesCreated []string `json:"tablesCreated"`
- TablesDropped []string `json:"tablesDropped"`
- RowsInserted int64 `json:"rowsInserted"`
- Errors []string `json:"errors,omitempty"`
- }
- // ImportSQL executes SQL statements from text.
- func ImportSQL(exec *executor.Executor, sql string, opts ImportOptions) (*ImportResult, error) {
- result := &ImportResult{
- TablesCreated: []string{},
- TablesDropped: []string{},
- Errors: []string{},
- }
- // Split SQL into statements
- statements := splitStatements(sql)
- for _, stmtSQL := range statements {
- stmtSQL = strings.TrimSpace(stmtSQL)
- if stmtSQL == "" || isComment(stmtSQL) {
- continue
- }
- // Parse and execute the statement
- err := executeStatement(exec, stmtSQL, result)
- if err != nil {
- errMsg := fmt.Sprintf("Error executing statement: %s - %v", truncateSQL(stmtSQL), err)
- result.Errors = append(result.Errors, errMsg)
- if !opts.IgnoreErrors {
- return result, fmt.Errorf("import failed: %w", err)
- }
- } else {
- result.StatementsExecuted++
- }
- }
- return result, nil
- }
- // executeStatement parses and executes a single SQL statement.
- func executeStatement(exec *executor.Executor, sql string, result *ImportResult) error {
- // Parse the statement
- l := lexer.New(sql)
- p := parser.New(l)
- stmt, err := p.Parse()
- if err != nil {
- return fmt.Errorf("parse error: %w", err)
- }
- // Execute the statement
- execResult, err := exec.Execute(stmt)
- if err != nil {
- return fmt.Errorf("execution error: %w", err)
- }
- // Track what happened
- upperSQL := strings.ToUpper(strings.TrimSpace(sql))
- if strings.HasPrefix(upperSQL, "CREATE TABLE") {
- tableName := extractTableName(sql, "CREATE TABLE")
- if tableName != "" {
- result.TablesCreated = append(result.TablesCreated, tableName)
- }
- } else if strings.HasPrefix(upperSQL, "DROP TABLE") {
- tableName := extractTableName(sql, "DROP TABLE")
- if tableName != "" {
- result.TablesDropped = append(result.TablesDropped, tableName)
- }
- } else if strings.HasPrefix(upperSQL, "INSERT") {
- result.RowsInserted += execResult.RowsAffected
- }
- return nil
- }
- // splitStatements splits SQL text into individual statements.
- func splitStatements(sql string) []string {
- var statements []string
- var current strings.Builder
- inString := false
- stringChar := byte(0)
- for i := 0; i < len(sql); i++ {
- c := sql[i]
- // Handle string literals
- if (c == '\'' || c == '"') && !inString {
- inString = true
- stringChar = c
- current.WriteByte(c)
- continue
- }
- if inString {
- current.WriteByte(c)
- // Check for escape (doubled quote)
- if c == stringChar {
- if i+1 < len(sql) && sql[i+1] == stringChar {
- // Escaped quote - write next char and skip
- i++
- current.WriteByte(sql[i])
- } else {
- // End of string
- inString = false
- stringChar = 0
- }
- }
- continue
- }
- // Handle statement terminator
- if c == ';' {
- stmt := strings.TrimSpace(current.String())
- if stmt != "" {
- statements = append(statements, stmt)
- }
- current.Reset()
- continue
- }
- // Handle single-line comments
- if c == '-' && i+1 < len(sql) && sql[i+1] == '-' {
- // Skip to end of line
- for i < len(sql) && sql[i] != '\n' {
- i++
- }
- continue
- }
- current.WriteByte(c)
- }
- // Don't forget the last statement if no trailing semicolon
- stmt := strings.TrimSpace(current.String())
- if stmt != "" {
- statements = append(statements, stmt)
- }
- return statements
- }
- // isComment checks if a line is a SQL comment.
- func isComment(line string) bool {
- trimmed := strings.TrimSpace(line)
- return strings.HasPrefix(trimmed, "--") || strings.HasPrefix(trimmed, "/*")
- }
- // extractTableName extracts the table name from a CREATE TABLE or DROP TABLE statement.
- func extractTableName(sql string, prefix string) string {
- // Remove the prefix
- upper := strings.ToUpper(sql)
- prefixUpper := strings.ToUpper(prefix)
- idx := strings.Index(upper, prefixUpper)
- if idx == -1 {
- return ""
- }
- rest := strings.TrimSpace(sql[idx+len(prefix):])
- // Handle IF EXISTS / IF NOT EXISTS
- restUpper := strings.ToUpper(rest)
- if strings.HasPrefix(restUpper, "IF EXISTS") {
- rest = strings.TrimSpace(rest[9:])
- } else if strings.HasPrefix(restUpper, "IF NOT EXISTS") {
- rest = strings.TrimSpace(rest[13:])
- }
- // Extract table name (until space, paren, or end)
- var tableName strings.Builder
- inQuote := false
- quoteChar := byte(0)
- for i := 0; i < len(rest); i++ {
- c := rest[i]
- if (c == '"' || c == '`' || c == '[') && !inQuote {
- inQuote = true
- quoteChar = c
- if c == '[' {
- quoteChar = ']'
- }
- continue
- }
- if inQuote && c == quoteChar {
- inQuote = false
- continue
- }
- if !inQuote && (c == ' ' || c == '\t' || c == '\n' || c == '(' || c == ';') {
- break
- }
- tableName.WriteByte(c)
- }
- return tableName.String()
- }
- // truncateSQL truncates SQL for error messages.
- func truncateSQL(sql string) string {
- sql = strings.ReplaceAll(sql, "\n", " ")
- sql = strings.Join(strings.Fields(sql), " ")
- if len(sql) > 50 {
- return sql[:50] + "..."
- }
- return sql
- }
|