Files
relspecgo/pkg/writers/sqlite/writer_test.go
T
Hein 2b6bb7f948 fix(sqlite): emit inline foreign keys, bare-name default schema, direct exec
SQLite can't ALTER TABLE ADD CONSTRAINT, so foreign keys are now written
as inline FOREIGN KEY clauses in CREATE TABLE instead of commented-out
ALTER statements. The default schema (public/main) now produces bare
table names instead of a "public_" prefix; other schemas are still
prefixed to avoid collisions. Also adds direct-to-file execution: the
sqlite writer can now apply generated DDL straight to a .db file via
Metadata["connection_string"], wired into `relspec merge --output-conn`.
2026-08-24 11:52:24 +02:00

424 lines
9.0 KiB
Go

package sqlite
import (
"bytes"
"strings"
"testing"
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/writers"
)
func TestNewWriter(t *testing.T) {
opts := &writers.WriterOptions{
OutputPath: "/tmp/test.sql",
FlattenSchema: false, // Should be forced to true
}
writer := NewWriter(opts)
if !writer.options.FlattenSchema {
t.Error("Expected FlattenSchema to be forced to true for SQLite")
}
}
func TestWriteDatabase(t *testing.T) {
db := &models.Database{
Name: "testdb",
Schemas: []*models.Schema{
{
Name: "public",
Tables: []*models.Table{
{
Name: "users",
Columns: map[string]*models.Column{
"id": {
Name: "id",
Type: "serial",
NotNull: true,
IsPrimaryKey: true,
Default: "nextval('users_id_seq'::regclass)",
},
"email": {
Name: "email",
Type: "varchar(255)",
NotNull: true,
},
"active": {
Name: "active",
Type: "boolean",
NotNull: true,
Default: "true",
},
},
Constraints: map[string]*models.Constraint{
"pk_users": {
Name: "pk_users",
Type: models.PrimaryKeyConstraint,
Columns: []string{"id"},
},
},
},
},
},
},
}
var buf bytes.Buffer
opts := &writers.WriterOptions{}
writer := NewWriter(opts)
writer.writer = &buf
err := writer.WriteDatabase(db)
if err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
// Check for expected elements
if !strings.Contains(output, "PRAGMA foreign_keys = ON") {
t.Error("Expected PRAGMA foreign_keys statement")
}
if !strings.Contains(output, "CREATE TABLE") {
t.Error("Expected CREATE TABLE statement")
}
if !strings.Contains(output, "\"users\"") {
t.Error("Expected bare table name users (default schema should not be prefixed)")
}
if strings.Contains(output, "\"public_users\"") {
t.Error("Did not expect flattened table name public_users for the default public schema")
}
if !strings.Contains(output, "INTEGER PRIMARY KEY AUTOINCREMENT") {
t.Error("Expected autoincrement for serial primary key")
}
if !strings.Contains(output, "TEXT") {
t.Error("Expected TEXT type for varchar")
}
// Boolean should be mapped to INTEGER with default 1
if !strings.Contains(output, "active") {
t.Error("Expected active column")
}
}
func TestDataTypeMapping(t *testing.T) {
tests := []struct {
pgType string
expected string
}{
{"varchar(255)", "TEXT"},
{"text", "TEXT"},
{"integer", "INTEGER"},
{"bigint", "INTEGER"},
{"serial", "INTEGER"},
{"boolean", "INTEGER"},
{"real", "REAL"},
{"double precision", "REAL"},
{"numeric(10,2)", "NUMERIC"},
{"decimal", "NUMERIC"},
{"bytea", "BLOB"},
{"timestamp", "TEXT"},
{"uuid", "TEXT"},
{"json", "TEXT"},
{"jsonb", "TEXT"},
}
for _, tt := range tests {
result := MapPostgreSQLType(tt.pgType)
if result != tt.expected {
t.Errorf("MapPostgreSQLType(%q) = %q, want %q", tt.pgType, result, tt.expected)
}
}
}
func TestIsAutoIncrementCandidate(t *testing.T) {
tests := []struct {
name string
col *models.Column
expected bool
}{
{
name: "serial primary key",
col: &models.Column{
Name: "id",
Type: "serial",
IsPrimaryKey: true,
Default: "nextval('seq')",
},
expected: true,
},
{
name: "integer primary key with nextval",
col: &models.Column{
Name: "id",
Type: "integer",
IsPrimaryKey: true,
Default: "nextval('users_id_seq'::regclass)",
},
expected: true,
},
{
name: "integer not primary key",
col: &models.Column{
Name: "count",
Type: "integer",
IsPrimaryKey: false,
Default: "0",
},
expected: false,
},
{
name: "varchar primary key",
col: &models.Column{
Name: "code",
Type: "varchar",
IsPrimaryKey: true,
},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsAutoIncrementCandidate(tt.col)
if result != tt.expected {
t.Errorf("IsAutoIncrementCandidate() = %v, want %v", result, tt.expected)
}
})
}
}
func TestFormatDefault(t *testing.T) {
tests := []struct {
name string
col *models.Column
expected string
}{
{
name: "current_timestamp",
col: &models.Column{
Type: "timestamp",
Default: "CURRENT_TIMESTAMP",
},
expected: "CURRENT_TIMESTAMP",
},
{
name: "now()",
col: &models.Column{
Type: "timestamp",
Default: "now()",
},
expected: "CURRENT_TIMESTAMP",
},
{
name: "boolean true",
col: &models.Column{
Type: "boolean",
Default: "true",
},
expected: "1",
},
{
name: "boolean false",
col: &models.Column{
Type: "boolean",
Default: "false",
},
expected: "0",
},
{
name: "serial autoincrement",
col: &models.Column{
Type: "serial",
IsPrimaryKey: true,
Default: "nextval('seq')",
},
expected: "",
},
{
name: "uuid default removed",
col: &models.Column{
Type: "uuid",
Default: "gen_random_uuid()",
},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := FormatDefault(tt.col)
if result != tt.expected {
t.Errorf("FormatDefault() = %q, want %q", result, tt.expected)
}
})
}
}
func TestWriteSchema_MultiSchema(t *testing.T) {
db := &models.Database{
Name: "testdb",
Schemas: []*models.Schema{
{
Name: "auth",
Tables: []*models.Table{
{
Name: "sessions",
Columns: map[string]*models.Column{
"id": {
Name: "id",
Type: "uuid",
NotNull: true,
IsPrimaryKey: true,
},
},
Constraints: map[string]*models.Constraint{
"pk_sessions": {
Name: "pk_sessions",
Type: models.PrimaryKeyConstraint,
Columns: []string{"id"},
},
},
},
},
},
{
Name: "public",
Tables: []*models.Table{
{
Name: "posts",
Columns: map[string]*models.Column{
"id": {
Name: "id",
Type: "integer",
NotNull: true,
IsPrimaryKey: true,
},
},
Constraints: map[string]*models.Constraint{
"pk_posts": {
Name: "pk_posts",
Type: models.PrimaryKeyConstraint,
Columns: []string{"id"},
},
},
},
},
},
},
}
var buf bytes.Buffer
opts := &writers.WriterOptions{}
writer := NewWriter(opts)
writer.writer = &buf
err := writer.WriteDatabase(db)
if err != nil {
t.Fatalf("WriteDatabase failed: %v", err)
}
output := buf.String()
// Non-default schemas are still prefixed to avoid name collisions...
if !strings.Contains(output, "\"auth_sessions\"") {
t.Error("Expected flattened table name auth_sessions")
}
// ...but the default "public" schema is not, since it's typically the
// only schema and bare names read better (and match e.g. DBML output).
if !strings.Contains(output, "\"posts\"") {
t.Error("Expected bare table name posts")
}
}
func TestWriteIndexes(t *testing.T) {
table := &models.Table{
Name: "users",
Columns: map[string]*models.Column{
"email": {
Name: "email",
Type: "varchar(255)",
},
},
Indexes: map[string]*models.Index{
"idx_users_email": {
Name: "idx_users_email",
Columns: []string{"email"},
},
},
}
var buf bytes.Buffer
opts := &writers.WriterOptions{}
writer := NewWriter(opts)
writer.writer = &buf
err := writer.writeIndexes("public", table)
if err != nil {
t.Fatalf("writeIndexes failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "CREATE INDEX") {
t.Error("Expected CREATE INDEX statement")
}
if !strings.Contains(output, "public_users_idx_users_email") {
t.Errorf("Expected flattened index name public_users_idx_users_email, got output:\n%s", output)
}
}
func TestWriteUniqueConstraints(t *testing.T) {
table := &models.Table{
Name: "users",
Constraints: map[string]*models.Constraint{
"uk_users_email": {
Name: "uk_users_email",
Type: models.UniqueConstraint,
Columns: []string{"email"},
},
},
}
var buf bytes.Buffer
opts := &writers.WriterOptions{}
writer := NewWriter(opts)
writer.writer = &buf
err := writer.writeUniqueConstraints("public", table)
if err != nil {
t.Fatalf("writeUniqueConstraints failed: %v", err)
}
output := buf.String()
if !strings.Contains(output, "CREATE UNIQUE INDEX") {
t.Error("Expected CREATE UNIQUE INDEX statement")
}
}
func TestQuoteIdentifier(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"users", `"users"`},
{"public_users", `"public_users"`},
{`user"name`, `"user""name"`}, // Double quotes should be escaped
}
for _, tt := range tests {
result := QuoteIdentifier(tt.input)
if result != tt.expected {
t.Errorf("QuoteIdentifier(%q) = %q, want %q", tt.input, result, tt.expected)
}
}
}