import.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. package sqlimport
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/danfragoso/pizzasql-next/pkg/executor"
  6. "github.com/danfragoso/pizzasql-next/pkg/lexer"
  7. "github.com/danfragoso/pizzasql-next/pkg/parser"
  8. )
  9. // ImportOptions configures import behavior.
  10. type ImportOptions struct {
  11. IgnoreErrors bool // Continue on individual statement errors
  12. }
  13. // DefaultImportOptions returns sensible defaults.
  14. func DefaultImportOptions() ImportOptions {
  15. return ImportOptions{
  16. IgnoreErrors: false,
  17. }
  18. }
  19. // ImportResult contains the results of an import operation.
  20. type ImportResult struct {
  21. StatementsExecuted int `json:"statementsExecuted"`
  22. TablesCreated []string `json:"tablesCreated"`
  23. TablesDropped []string `json:"tablesDropped"`
  24. RowsInserted int64 `json:"rowsInserted"`
  25. Errors []string `json:"errors,omitempty"`
  26. }
  27. // ImportSQL executes SQL statements from text.
  28. func ImportSQL(exec *executor.Executor, sql string, opts ImportOptions) (*ImportResult, error) {
  29. result := &ImportResult{
  30. TablesCreated: []string{},
  31. TablesDropped: []string{},
  32. Errors: []string{},
  33. }
  34. // Split SQL into statements
  35. statements := splitStatements(sql)
  36. for _, stmtSQL := range statements {
  37. stmtSQL = strings.TrimSpace(stmtSQL)
  38. if stmtSQL == "" || isComment(stmtSQL) {
  39. continue
  40. }
  41. // Parse and execute the statement
  42. err := executeStatement(exec, stmtSQL, result)
  43. if err != nil {
  44. errMsg := fmt.Sprintf("Error executing statement: %s - %v", truncateSQL(stmtSQL), err)
  45. result.Errors = append(result.Errors, errMsg)
  46. if !opts.IgnoreErrors {
  47. return result, fmt.Errorf("import failed: %w", err)
  48. }
  49. } else {
  50. result.StatementsExecuted++
  51. }
  52. }
  53. return result, nil
  54. }
  55. // executeStatement parses and executes a single SQL statement.
  56. func executeStatement(exec *executor.Executor, sql string, result *ImportResult) error {
  57. // Parse the statement
  58. l := lexer.New(sql)
  59. p := parser.New(l)
  60. stmt, err := p.Parse()
  61. if err != nil {
  62. return fmt.Errorf("parse error: %w", err)
  63. }
  64. // Execute the statement
  65. execResult, err := exec.Execute(stmt)
  66. if err != nil {
  67. return fmt.Errorf("execution error: %w", err)
  68. }
  69. // Track what happened
  70. upperSQL := strings.ToUpper(strings.TrimSpace(sql))
  71. if strings.HasPrefix(upperSQL, "CREATE TABLE") {
  72. tableName := extractTableName(sql, "CREATE TABLE")
  73. if tableName != "" {
  74. result.TablesCreated = append(result.TablesCreated, tableName)
  75. }
  76. } else if strings.HasPrefix(upperSQL, "DROP TABLE") {
  77. tableName := extractTableName(sql, "DROP TABLE")
  78. if tableName != "" {
  79. result.TablesDropped = append(result.TablesDropped, tableName)
  80. }
  81. } else if strings.HasPrefix(upperSQL, "INSERT") {
  82. result.RowsInserted += execResult.RowsAffected
  83. }
  84. return nil
  85. }
  86. // splitStatements splits SQL text into individual statements.
  87. func splitStatements(sql string) []string {
  88. var statements []string
  89. var current strings.Builder
  90. inString := false
  91. stringChar := byte(0)
  92. for i := 0; i < len(sql); i++ {
  93. c := sql[i]
  94. // Handle string literals
  95. if (c == '\'' || c == '"') && !inString {
  96. inString = true
  97. stringChar = c
  98. current.WriteByte(c)
  99. continue
  100. }
  101. if inString {
  102. current.WriteByte(c)
  103. // Check for escape (doubled quote)
  104. if c == stringChar {
  105. if i+1 < len(sql) && sql[i+1] == stringChar {
  106. // Escaped quote - write next char and skip
  107. i++
  108. current.WriteByte(sql[i])
  109. } else {
  110. // End of string
  111. inString = false
  112. stringChar = 0
  113. }
  114. }
  115. continue
  116. }
  117. // Handle statement terminator
  118. if c == ';' {
  119. stmt := strings.TrimSpace(current.String())
  120. if stmt != "" {
  121. statements = append(statements, stmt)
  122. }
  123. current.Reset()
  124. continue
  125. }
  126. // Handle single-line comments
  127. if c == '-' && i+1 < len(sql) && sql[i+1] == '-' {
  128. // Skip to end of line
  129. for i < len(sql) && sql[i] != '\n' {
  130. i++
  131. }
  132. continue
  133. }
  134. current.WriteByte(c)
  135. }
  136. // Don't forget the last statement if no trailing semicolon
  137. stmt := strings.TrimSpace(current.String())
  138. if stmt != "" {
  139. statements = append(statements, stmt)
  140. }
  141. return statements
  142. }
  143. // isComment checks if a line is a SQL comment.
  144. func isComment(line string) bool {
  145. trimmed := strings.TrimSpace(line)
  146. return strings.HasPrefix(trimmed, "--") || strings.HasPrefix(trimmed, "/*")
  147. }
  148. // extractTableName extracts the table name from a CREATE TABLE or DROP TABLE statement.
  149. func extractTableName(sql string, prefix string) string {
  150. // Remove the prefix
  151. upper := strings.ToUpper(sql)
  152. prefixUpper := strings.ToUpper(prefix)
  153. idx := strings.Index(upper, prefixUpper)
  154. if idx == -1 {
  155. return ""
  156. }
  157. rest := strings.TrimSpace(sql[idx+len(prefix):])
  158. // Handle IF EXISTS / IF NOT EXISTS
  159. restUpper := strings.ToUpper(rest)
  160. if strings.HasPrefix(restUpper, "IF EXISTS") {
  161. rest = strings.TrimSpace(rest[9:])
  162. } else if strings.HasPrefix(restUpper, "IF NOT EXISTS") {
  163. rest = strings.TrimSpace(rest[13:])
  164. }
  165. // Extract table name (until space, paren, or end)
  166. var tableName strings.Builder
  167. inQuote := false
  168. quoteChar := byte(0)
  169. for i := 0; i < len(rest); i++ {
  170. c := rest[i]
  171. if (c == '"' || c == '`' || c == '[') && !inQuote {
  172. inQuote = true
  173. quoteChar = c
  174. if c == '[' {
  175. quoteChar = ']'
  176. }
  177. continue
  178. }
  179. if inQuote && c == quoteChar {
  180. inQuote = false
  181. continue
  182. }
  183. if !inQuote && (c == ' ' || c == '\t' || c == '\n' || c == '(' || c == ';') {
  184. break
  185. }
  186. tableName.WriteByte(c)
  187. }
  188. return tableName.String()
  189. }
  190. // truncateSQL truncates SQL for error messages.
  191. func truncateSQL(sql string) string {
  192. sql = strings.ReplaceAll(sql, "\n", " ")
  193. sql = strings.Join(strings.Fields(sql), " ")
  194. if len(sql) > 50 {
  195. return sql[:50] + "..."
  196. }
  197. return sql
  198. }