export.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  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. compositeKey := schema.IsCompositePrimaryKey()
  90. defs := make([]string, 0, len(schema.Columns)+1)
  91. for _, col := range schema.Columns {
  92. // Skip internal _rowid_ column
  93. if col.Name == "_rowid_" {
  94. continue
  95. }
  96. var b strings.Builder
  97. b.WriteString(quoteIdentifier(col.Name))
  98. b.WriteString(" ")
  99. b.WriteString(col.Type)
  100. // A single-column key keeps its inline PRIMARY KEY; a composite key is
  101. // emitted once at the end so no column claims the key by itself.
  102. inlineKey := col.PrimaryKey && !compositeKey
  103. if inlineKey {
  104. b.WriteString(" PRIMARY KEY")
  105. }
  106. if !col.Nullable && !inlineKey {
  107. b.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. b.WriteString(" DEFAULT ")
  113. b.WriteString(col.DefaultExpr)
  114. } else if col.Default != nil {
  115. b.WriteString(" DEFAULT ")
  116. b.WriteString(formatValue(col.Default, col.Type))
  117. }
  118. defs = append(defs, " "+b.String())
  119. }
  120. if compositeKey {
  121. columns := schema.PrimaryKeyList()
  122. quoted := make([]string, len(columns))
  123. for i, name := range columns {
  124. quoted[i] = quoteIdentifier(name)
  125. }
  126. defs = append(defs, " PRIMARY KEY ("+strings.Join(quoted, ", ")+")")
  127. }
  128. return fmt.Sprintf("CREATE TABLE %s (\n%s\n);", quoteIdentifier(schema.Name), strings.Join(defs, ",\n"))
  129. }
  130. // generateInserts generates INSERT statements for rows.
  131. func generateInserts(tableName string, schema *storage.Schema, rows []storage.Row) string {
  132. var sb strings.Builder
  133. // Get column names (excluding _rowid_)
  134. var columns []string
  135. var colTypes []string
  136. for _, col := range schema.Columns {
  137. if col.Name != "_rowid_" {
  138. columns = append(columns, col.Name)
  139. colTypes = append(colTypes, col.Type)
  140. }
  141. }
  142. // Generate INSERT for each row
  143. for _, row := range rows {
  144. sb.WriteString(fmt.Sprintf("INSERT INTO %s (", quoteIdentifier(tableName)))
  145. // Column names
  146. for i, col := range columns {
  147. if i > 0 {
  148. sb.WriteString(", ")
  149. }
  150. sb.WriteString(quoteIdentifier(col))
  151. }
  152. sb.WriteString(") VALUES (")
  153. // Values
  154. for i, col := range columns {
  155. if i > 0 {
  156. sb.WriteString(", ")
  157. }
  158. value := row[col]
  159. sb.WriteString(formatValue(value, colTypes[i]))
  160. }
  161. sb.WriteString(");\n")
  162. }
  163. return sb.String()
  164. }
  165. // formatValue formats a Go value as a SQL literal.
  166. func formatValue(value interface{}, colType string) string {
  167. if value == nil {
  168. return "NULL"
  169. }
  170. switch v := value.(type) {
  171. case string:
  172. return formatString(v)
  173. case float64:
  174. // Check if it's actually an integer
  175. if strings.Contains(strings.ToUpper(colType), "INT") {
  176. return fmt.Sprintf("%d", int64(v))
  177. }
  178. return fmt.Sprintf("%g", v)
  179. case int64:
  180. return fmt.Sprintf("%d", v)
  181. case int:
  182. return fmt.Sprintf("%d", v)
  183. case bool:
  184. if v {
  185. return "1"
  186. }
  187. return "0"
  188. case []byte:
  189. return fmt.Sprintf("X'%s'", hex.EncodeToString(v))
  190. default:
  191. // Fallback: treat as string
  192. return formatString(fmt.Sprintf("%v", v))
  193. }
  194. }
  195. // formatString formats a string as a SQL string literal with proper escaping.
  196. func formatString(s string) string {
  197. // Escape single quotes by doubling them
  198. escaped := strings.ReplaceAll(s, "'", "''")
  199. return fmt.Sprintf("'%s'", escaped)
  200. }
  201. // quoteIdentifier quotes a SQL identifier if needed.
  202. func quoteIdentifier(name string) string {
  203. // Check if identifier needs quoting
  204. needsQuote := false
  205. // Check for reserved words or special characters
  206. lower := strings.ToLower(name)
  207. reserved := map[string]bool{
  208. "table": true, "select": true, "insert": true, "update": true,
  209. "delete": true, "create": true, "drop": true, "index": true,
  210. "from": true, "where": true, "and": true, "or": true,
  211. "order": true, "by": true, "group": true, "having": true,
  212. "limit": true, "offset": true, "join": true, "on": true,
  213. "as": true, "null": true, "not": true, "in": true,
  214. "like": true, "between": true, "is": true, "primary": true,
  215. "key": true, "unique": true, "default": true, "values": true,
  216. }
  217. if reserved[lower] {
  218. needsQuote = true
  219. }
  220. // Check for special characters
  221. for _, c := range name {
  222. if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
  223. (c >= '0' && c <= '9') || c == '_') {
  224. needsQuote = true
  225. break
  226. }
  227. }
  228. // Check if starts with digit
  229. if len(name) > 0 && name[0] >= '0' && name[0] <= '9' {
  230. needsQuote = true
  231. }
  232. if needsQuote {
  233. // Use double quotes and escape any existing double quotes
  234. escaped := strings.ReplaceAll(name, "\"", "\"\"")
  235. return fmt.Sprintf("\"%s\"", escaped)
  236. }
  237. return name
  238. }