export.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. package sqlexport
  2. import (
  3. "encoding/hex"
  4. "fmt"
  5. "sort"
  6. "strings"
  7. "time"
  8. "github.com/danfragoso/pizzasql-next/pkg/storage"
  9. )
  10. // ExportOptions configures export behavior.
  11. type ExportOptions struct {
  12. Tables []string // Specific tables to export (empty = all tables)
  13. IncludeData bool // Include INSERT statements (default: true)
  14. DropTables bool // Add DROP TABLE IF EXISTS before CREATE
  15. }
  16. // DefaultExportOptions returns sensible defaults.
  17. func DefaultExportOptions() ExportOptions {
  18. return ExportOptions{
  19. Tables: nil,
  20. IncludeData: true,
  21. DropTables: false,
  22. }
  23. }
  24. // ExportDatabase exports an entire database to SQL text.
  25. func ExportDatabase(schema *storage.SchemaManager, table *storage.TableManager, opts ExportOptions) (string, error) {
  26. var sb strings.Builder
  27. // Write header
  28. sb.WriteString("-- PizzaSQL Export\n")
  29. sb.WriteString(fmt.Sprintf("-- Database: %s\n", schema.GetDatabaseName()))
  30. sb.WriteString(fmt.Sprintf("-- Date: %s\n", time.Now().UTC().Format(time.RFC3339)))
  31. sb.WriteString("\n")
  32. // Get tables to export
  33. tables := opts.Tables
  34. if len(tables) == 0 {
  35. var err error
  36. tables, err = schema.ListTables()
  37. if err != nil {
  38. return "", fmt.Errorf("failed to list tables: %w", err)
  39. }
  40. }
  41. // Sort tables for consistent output
  42. sort.Strings(tables)
  43. // Export each table
  44. for i, tableName := range tables {
  45. tableSQL, err := ExportTable(schema, table, tableName, opts)
  46. if err != nil {
  47. return "", fmt.Errorf("failed to export table %s: %w", tableName, err)
  48. }
  49. sb.WriteString(tableSQL)
  50. // Add separator between tables
  51. if i < len(tables)-1 {
  52. sb.WriteString("\n")
  53. }
  54. }
  55. return sb.String(), nil
  56. }
  57. // ExportTable exports a single table to SQL text.
  58. func ExportTable(schema *storage.SchemaManager, table *storage.TableManager, tableName string, opts ExportOptions) (string, error) {
  59. var sb strings.Builder
  60. // Get table schema
  61. tableSchema, err := schema.GetSchema(tableName)
  62. if err != nil {
  63. return "", fmt.Errorf("failed to get schema for table %s: %w", tableName, err)
  64. }
  65. // Write DROP TABLE if requested
  66. if opts.DropTables {
  67. sb.WriteString(fmt.Sprintf("DROP TABLE IF EXISTS %s;\n", quoteIdentifier(tableName)))
  68. }
  69. // Write CREATE TABLE
  70. createSQL := generateCreateTable(tableSchema)
  71. sb.WriteString(createSQL)
  72. sb.WriteString("\n")
  73. // Write INSERT statements if requested
  74. if opts.IncludeData {
  75. rows, err := table.Select(tableName, nil)
  76. if err != nil {
  77. return "", fmt.Errorf("failed to select data from table %s: %w", tableName, err)
  78. }
  79. if len(rows) > 0 {
  80. sb.WriteString("\n")
  81. insertSQL := generateInserts(tableName, tableSchema, rows)
  82. sb.WriteString(insertSQL)
  83. }
  84. }
  85. return sb.String(), nil
  86. }
  87. // generateCreateTable generates a CREATE TABLE statement from schema.
  88. func generateCreateTable(schema *storage.Schema) string {
  89. var sb strings.Builder
  90. sb.WriteString(fmt.Sprintf("CREATE TABLE %s (\n", quoteIdentifier(schema.Name)))
  91. // Generate column definitions
  92. for i, col := range schema.Columns {
  93. // Skip internal _rowid_ column
  94. if col.Name == "_rowid_" {
  95. continue
  96. }
  97. sb.WriteString(" ")
  98. sb.WriteString(quoteIdentifier(col.Name))
  99. sb.WriteString(" ")
  100. sb.WriteString(col.Type)
  101. // Add PRIMARY KEY constraint
  102. if col.PrimaryKey {
  103. sb.WriteString(" PRIMARY KEY")
  104. }
  105. // Add NOT NULL constraint
  106. if !col.Nullable && !col.PrimaryKey {
  107. sb.WriteString(" NOT NULL")
  108. }
  109. // Add DEFAULT value. A non-constant default is emitted as its stored SQL
  110. // text so the dump keeps evaluating it instead of freezing a value.
  111. if col.DefaultExpr != "" {
  112. sb.WriteString(" DEFAULT ")
  113. sb.WriteString(col.DefaultExpr)
  114. } else if col.Default != nil {
  115. sb.WriteString(" DEFAULT ")
  116. sb.WriteString(formatValue(col.Default, col.Type))
  117. }
  118. // Add comma if not last column
  119. if i < len(schema.Columns)-1 {
  120. // Check if next column is _rowid_
  121. if i+1 < len(schema.Columns) && schema.Columns[i+1].Name != "_rowid_" {
  122. sb.WriteString(",")
  123. } else if i+2 < len(schema.Columns) {
  124. sb.WriteString(",")
  125. }
  126. }
  127. sb.WriteString("\n")
  128. }
  129. sb.WriteString(");")
  130. return sb.String()
  131. }
  132. // generateInserts generates INSERT statements for rows.
  133. func generateInserts(tableName string, schema *storage.Schema, rows []storage.Row) string {
  134. var sb strings.Builder
  135. // Get column names (excluding _rowid_)
  136. var columns []string
  137. var colTypes []string
  138. for _, col := range schema.Columns {
  139. if col.Name != "_rowid_" {
  140. columns = append(columns, col.Name)
  141. colTypes = append(colTypes, col.Type)
  142. }
  143. }
  144. // Generate INSERT for each row
  145. for _, row := range rows {
  146. sb.WriteString(fmt.Sprintf("INSERT INTO %s (", quoteIdentifier(tableName)))
  147. // Column names
  148. for i, col := range columns {
  149. if i > 0 {
  150. sb.WriteString(", ")
  151. }
  152. sb.WriteString(quoteIdentifier(col))
  153. }
  154. sb.WriteString(") VALUES (")
  155. // Values
  156. for i, col := range columns {
  157. if i > 0 {
  158. sb.WriteString(", ")
  159. }
  160. value := row[col]
  161. sb.WriteString(formatValue(value, colTypes[i]))
  162. }
  163. sb.WriteString(");\n")
  164. }
  165. return sb.String()
  166. }
  167. // formatValue formats a Go value as a SQL literal.
  168. func formatValue(value interface{}, colType string) string {
  169. if value == nil {
  170. return "NULL"
  171. }
  172. switch v := value.(type) {
  173. case string:
  174. return formatString(v)
  175. case float64:
  176. // Check if it's actually an integer
  177. if strings.Contains(strings.ToUpper(colType), "INT") {
  178. return fmt.Sprintf("%d", int64(v))
  179. }
  180. return fmt.Sprintf("%g", v)
  181. case int64:
  182. return fmt.Sprintf("%d", v)
  183. case int:
  184. return fmt.Sprintf("%d", v)
  185. case bool:
  186. if v {
  187. return "1"
  188. }
  189. return "0"
  190. case []byte:
  191. return fmt.Sprintf("X'%s'", hex.EncodeToString(v))
  192. default:
  193. // Fallback: treat as string
  194. return formatString(fmt.Sprintf("%v", v))
  195. }
  196. }
  197. // formatString formats a string as a SQL string literal with proper escaping.
  198. func formatString(s string) string {
  199. // Escape single quotes by doubling them
  200. escaped := strings.ReplaceAll(s, "'", "''")
  201. return fmt.Sprintf("'%s'", escaped)
  202. }
  203. // quoteIdentifier quotes a SQL identifier if needed.
  204. func quoteIdentifier(name string) string {
  205. // Check if identifier needs quoting
  206. needsQuote := false
  207. // Check for reserved words or special characters
  208. lower := strings.ToLower(name)
  209. reserved := map[string]bool{
  210. "table": true, "select": true, "insert": true, "update": true,
  211. "delete": true, "create": true, "drop": true, "index": true,
  212. "from": true, "where": true, "and": true, "or": true,
  213. "order": true, "by": true, "group": true, "having": true,
  214. "limit": true, "offset": true, "join": true, "on": true,
  215. "as": true, "null": true, "not": true, "in": true,
  216. "like": true, "between": true, "is": true, "primary": true,
  217. "key": true, "unique": true, "default": true, "values": true,
  218. }
  219. if reserved[lower] {
  220. needsQuote = true
  221. }
  222. // Check for special characters
  223. for _, c := range name {
  224. if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
  225. (c >= '0' && c <= '9') || c == '_') {
  226. needsQuote = true
  227. break
  228. }
  229. }
  230. // Check if starts with digit
  231. if len(name) > 0 && name[0] >= '0' && name[0] <= '9' {
  232. needsQuote = true
  233. }
  234. if needsQuote {
  235. // Use double quotes and escape any existing double quotes
  236. escaped := strings.ReplaceAll(name, "\"", "\"\"")
  237. return fmt.Sprintf("\"%s\"", escaped)
  238. }
  239. return name
  240. }