359 lines
9.8 KiB
Go
359 lines
9.8 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
_ "modernc.org/sqlite" // SQLite driver
|
|
|
|
"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.
|
|
//
|
|
// If Metadata["connection_string"] is set (a path to a SQLite database file),
|
|
// the generated DDL is executed directly against that file instead of being
|
|
// written out as a .sql script.
|
|
func (w *Writer) WriteDatabase(db *models.Database) error {
|
|
if dbPath, ok := w.options.Metadata["connection_string"].(string); ok && dbPath != "" {
|
|
return w.executeDatabaseSQL(db, dbPath)
|
|
}
|
|
|
|
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
|
|
return w.writeContent(db)
|
|
}
|
|
|
|
// writeContent writes the header, pragma, and every schema's DDL to w.writer.
|
|
func (w *Writer) writeContent(db *models.Database) error {
|
|
// 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: SQLite has no schema concept; non-default schema names are flattened into table name prefixes (e.g., auth.sessions -> auth_sessions)\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
|
|
}
|
|
|
|
// statementCollector captures each Write call as a single SQL statement (or
|
|
// comment line), matching the writer's convention of one Fprintf per statement.
|
|
type statementCollector struct {
|
|
statements []string
|
|
}
|
|
|
|
func (c *statementCollector) Write(p []byte) (int, error) {
|
|
if s := strings.TrimSpace(string(p)); s != "" {
|
|
c.statements = append(c.statements, s)
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
// executeDatabaseSQL generates the DDL for db and executes it directly
|
|
// against the SQLite database file at dbPath.
|
|
func (w *Writer) executeDatabaseSQL(db *models.Database, dbPath string) error {
|
|
collector := &statementCollector{}
|
|
w.writer = collector
|
|
if err := w.writeContent(db); err != nil {
|
|
return fmt.Errorf("failed to generate SQL statements: %w", err)
|
|
}
|
|
|
|
conn, err := sql.Open("sqlite", dbPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open sqlite database %q: %w", dbPath, err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
ctx := context.Background()
|
|
ignoreErrors := false
|
|
if val, ok := w.options.Metadata["ignore_errors"].(bool); ok {
|
|
ignoreErrors = val
|
|
}
|
|
|
|
total, executed := 0, 0
|
|
var execErrors []string
|
|
for _, stmt := range collector.statements {
|
|
if strings.HasPrefix(stmt, "--") {
|
|
continue
|
|
}
|
|
|
|
total++
|
|
if _, err := conn.ExecContext(ctx, stmt); err != nil {
|
|
execErrors = append(execErrors, fmt.Sprintf("statement %d (%s): %v", total, truncateStatement(stmt), err))
|
|
if !ignoreErrors {
|
|
break
|
|
}
|
|
continue
|
|
}
|
|
executed++
|
|
}
|
|
|
|
w.options.Metadata["execution_total"] = total
|
|
w.options.Metadata["execution_success"] = executed
|
|
w.options.Metadata["execution_failed"] = len(execErrors)
|
|
|
|
if len(execErrors) > 0 {
|
|
return fmt.Errorf("failed to execute %d/%d statement(s) against %q:\n%s", len(execErrors), total, dbPath, strings.Join(execErrors, "\n"))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// truncateStatement shortens a SQL statement for error messages.
|
|
func truncateStatement(stmt string) string {
|
|
const maxLen = 80
|
|
stmt = strings.Join(strings.Fields(stmt), " ")
|
|
if len(stmt) > maxLen {
|
|
return stmt[:maxLen] + "..."
|
|
}
|
|
return stmt
|
|
}
|
|
|
|
// defaultSchemaNames are treated as "no schema" for SQLite output: SQLite has
|
|
// no schema concept, and a lone default schema (e.g. DBML's implicit "public")
|
|
// should produce bare table names rather than a "public_" prefix.
|
|
var defaultSchemaNames = map[string]bool{
|
|
"public": true,
|
|
"main": true,
|
|
}
|
|
|
|
// tableSchemaName returns the schema name to use for table/constraint naming,
|
|
// collapsing default schema names to "" so they aren't prefixed onto table names.
|
|
func tableSchemaName(schema string) string {
|
|
if defaultSchemaNames[strings.ToLower(schema)] {
|
|
return ""
|
|
}
|
|
return schema
|
|
}
|
|
|
|
// WriteSchema writes a single schema as SQLite SQL
|
|
func (w *Writer) WriteSchema(schema *models.Schema) error {
|
|
tableSchema := tableSchemaName(schema.Name)
|
|
|
|
// SQLite doesn't have schemas, so we just write a comment (skip for the
|
|
// default schema, since its tables aren't actually being prefixed)
|
|
if tableSchema != "" {
|
|
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(tableSchema, 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(tableSchema, 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(tableSchema, 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(tableSchema, table); err != nil {
|
|
return fmt.Errorf("failed to write check constraints 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 sortIndexes(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 sortConstraints(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 sortIndexes(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 sortConstraints(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
|
|
}
|