feat(sqlite): add SQLite writer for converting PostgreSQL schemas
All checks were successful
All checks were successful
- Implement SQLite DDL writer to convert PostgreSQL schemas to SQLite-compatible SQL statements. - Include automatic schema flattening, type mapping, auto-increment detection, and function translation. - Add templates for creating tables, indexes, unique constraints, check constraints, and foreign keys. - Implement tests for writer functionality and data type mapping.
This commit is contained in:
291
pkg/writers/sqlite/writer.go
Normal file
291
pkg/writers/sqlite/writer.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
||||
)
|
||||
|
||||
// Writer implements the Writer interface for SQLite SQL output
|
||||
type Writer struct {
|
||||
options *writers.WriterOptions
|
||||
writer io.Writer
|
||||
executor *TemplateExecutor
|
||||
}
|
||||
|
||||
// NewWriter creates a new SQLite SQL writer
|
||||
// SQLite doesn't support schemas, so FlattenSchema is automatically enabled
|
||||
func NewWriter(options *writers.WriterOptions) *Writer {
|
||||
// Force schema flattening for SQLite
|
||||
options.FlattenSchema = true
|
||||
|
||||
executor, _ := NewTemplateExecutor(options)
|
||||
return &Writer{
|
||||
options: options,
|
||||
executor: executor,
|
||||
}
|
||||
}
|
||||
|
||||
// WriteDatabase writes the entire database schema as SQLite SQL
|
||||
func (w *Writer) WriteDatabase(db *models.Database) error {
|
||||
var writer io.Writer
|
||||
var file *os.File
|
||||
var err error
|
||||
|
||||
// Use existing writer if already set (for testing)
|
||||
if w.writer != nil {
|
||||
writer = w.writer
|
||||
} else if w.options.OutputPath != "" {
|
||||
// Determine output destination
|
||||
file, err = os.Create(w.options.OutputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
writer = file
|
||||
} else {
|
||||
writer = os.Stdout
|
||||
}
|
||||
|
||||
w.writer = writer
|
||||
|
||||
// Write header comment
|
||||
fmt.Fprintf(w.writer, "-- SQLite Database Schema\n")
|
||||
fmt.Fprintf(w.writer, "-- Database: %s\n", db.Name)
|
||||
fmt.Fprintf(w.writer, "-- Generated by RelSpec\n")
|
||||
fmt.Fprintf(w.writer, "-- Note: Schema names have been flattened (e.g., public.users -> public_users)\n\n")
|
||||
|
||||
// Enable foreign keys
|
||||
pragma, err := w.executor.ExecutePragmaForeignKeys()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate pragma statement: %w", err)
|
||||
}
|
||||
fmt.Fprintf(w.writer, "%s\n", pragma)
|
||||
|
||||
// Process each schema in the database
|
||||
for _, schema := range db.Schemas {
|
||||
if err := w.WriteSchema(schema); err != nil {
|
||||
return fmt.Errorf("failed to write schema %s: %w", schema.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteSchema writes a single schema as SQLite SQL
|
||||
func (w *Writer) WriteSchema(schema *models.Schema) error {
|
||||
// SQLite doesn't have schemas, so we just write a comment
|
||||
if schema.Name != "" {
|
||||
fmt.Fprintf(w.writer, "-- Schema: %s (flattened into table names)\n\n", schema.Name)
|
||||
}
|
||||
|
||||
// Phase 1: Create tables
|
||||
for _, table := range schema.Tables {
|
||||
if err := w.writeTable(schema.Name, table); err != nil {
|
||||
return fmt.Errorf("failed to write table %s: %w", table.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Create indexes
|
||||
for _, table := range schema.Tables {
|
||||
if err := w.writeIndexes(schema.Name, table); err != nil {
|
||||
return fmt.Errorf("failed to write indexes for table %s: %w", table.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Create unique constraints (as unique indexes)
|
||||
for _, table := range schema.Tables {
|
||||
if err := w.writeUniqueConstraints(schema.Name, table); err != nil {
|
||||
return fmt.Errorf("failed to write unique constraints for table %s: %w", table.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: Check constraints (as comments, since SQLite requires them in CREATE TABLE)
|
||||
for _, table := range schema.Tables {
|
||||
if err := w.writeCheckConstraints(schema.Name, table); err != nil {
|
||||
return fmt.Errorf("failed to write check constraints for table %s: %w", table.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5: Foreign keys (as comments for compatibility)
|
||||
for _, table := range schema.Tables {
|
||||
if err := w.writeForeignKeys(schema.Name, table); err != nil {
|
||||
return fmt.Errorf("failed to write foreign keys for table %s: %w", table.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteTable writes a single table as SQLite SQL
|
||||
func (w *Writer) WriteTable(table *models.Table) error {
|
||||
return w.writeTable("", table)
|
||||
}
|
||||
|
||||
// writeTable is the internal implementation
|
||||
func (w *Writer) writeTable(schema string, table *models.Table) error {
|
||||
// Build table template data
|
||||
data := BuildTableTemplateData(schema, table)
|
||||
|
||||
// Execute template
|
||||
sql, err := w.executor.ExecuteCreateTable(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute create table template: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w.writer, "%s\n", sql)
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeIndexes writes indexes for a table
|
||||
func (w *Writer) writeIndexes(schema string, table *models.Table) error {
|
||||
for _, index := range table.Indexes {
|
||||
// Skip primary key indexes
|
||||
if strings.HasSuffix(index.Name, "_pkey") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip unique indexes (handled separately as unique constraints)
|
||||
if index.Unique {
|
||||
continue
|
||||
}
|
||||
|
||||
data := IndexTemplateData{
|
||||
Schema: schema,
|
||||
Table: table.Name,
|
||||
Name: index.Name,
|
||||
Columns: index.Columns,
|
||||
}
|
||||
|
||||
sql, err := w.executor.ExecuteCreateIndex(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute create index template: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w.writer, "%s\n", sql)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeUniqueConstraints writes unique constraints as unique indexes
|
||||
func (w *Writer) writeUniqueConstraints(schema string, table *models.Table) error {
|
||||
for _, constraint := range table.Constraints {
|
||||
if constraint.Type != models.UniqueConstraint {
|
||||
continue
|
||||
}
|
||||
|
||||
data := ConstraintTemplateData{
|
||||
Schema: schema,
|
||||
Table: table.Name,
|
||||
Name: constraint.Name,
|
||||
Columns: constraint.Columns,
|
||||
}
|
||||
|
||||
sql, err := w.executor.ExecuteCreateUniqueConstraint(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute create unique constraint template: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w.writer, "%s\n", sql)
|
||||
}
|
||||
|
||||
// Also handle unique indexes from the Indexes map
|
||||
for _, index := range table.Indexes {
|
||||
if !index.Unique {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if already handled as a constraint
|
||||
alreadyHandled := false
|
||||
for _, constraint := range table.Constraints {
|
||||
if constraint.Type == models.UniqueConstraint && constraint.Name == index.Name {
|
||||
alreadyHandled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if alreadyHandled {
|
||||
continue
|
||||
}
|
||||
|
||||
data := ConstraintTemplateData{
|
||||
Schema: schema,
|
||||
Table: table.Name,
|
||||
Name: index.Name,
|
||||
Columns: index.Columns,
|
||||
}
|
||||
|
||||
sql, err := w.executor.ExecuteCreateUniqueConstraint(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute create unique index template: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w.writer, "%s\n", sql)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeCheckConstraints writes check constraints as comments
|
||||
func (w *Writer) writeCheckConstraints(schema string, table *models.Table) error {
|
||||
for _, constraint := range table.Constraints {
|
||||
if constraint.Type != models.CheckConstraint {
|
||||
continue
|
||||
}
|
||||
|
||||
data := ConstraintTemplateData{
|
||||
Schema: schema,
|
||||
Table: table.Name,
|
||||
Name: constraint.Name,
|
||||
Expression: constraint.Expression,
|
||||
}
|
||||
|
||||
sql, err := w.executor.ExecuteCreateCheckConstraint(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute create check constraint template: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w.writer, "%s\n", sql)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeForeignKeys writes foreign keys as comments
|
||||
func (w *Writer) writeForeignKeys(schema string, table *models.Table) error {
|
||||
for _, constraint := range table.Constraints {
|
||||
if constraint.Type != models.ForeignKeyConstraint {
|
||||
continue
|
||||
}
|
||||
|
||||
refSchema := constraint.ReferencedSchema
|
||||
if refSchema == "" {
|
||||
refSchema = schema
|
||||
}
|
||||
|
||||
data := ConstraintTemplateData{
|
||||
Schema: schema,
|
||||
Table: table.Name,
|
||||
Name: constraint.Name,
|
||||
Columns: constraint.Columns,
|
||||
ForeignSchema: refSchema,
|
||||
ForeignTable: constraint.ReferencedTable,
|
||||
ForeignColumns: constraint.ReferencedColumns,
|
||||
OnDelete: constraint.OnDelete,
|
||||
OnUpdate: constraint.OnUpdate,
|
||||
}
|
||||
|
||||
sql, err := w.executor.ExecuteCreateForeignKey(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute create foreign key template: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w.writer, "%s\n", sql)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user