From c1888be1b9811ec66d02c28f5890869e7944c1fe Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Tue, 3 Mar 2026 00:26:28 +0100 Subject: [PATCH 1/2] feat: add ddl builder support --- builder/alter_table_builder.go | 507 +++++++++++++++++ builder/create_index_builder.go | 124 ++++ builder/create_schema_builder.go | 42 ++ builder/create_table_builder.go | 315 +++++++++++ builder/ddl_common.go | 134 +++++ builder/drop_builder.go | 123 ++++ ddl/ddl.go | 33 ++ ddl/ddl_test.go | 936 +++++++++++++++++++++++++++++++ 8 files changed, 2214 insertions(+) create mode 100644 builder/alter_table_builder.go create mode 100644 builder/create_index_builder.go create mode 100644 builder/create_schema_builder.go create mode 100644 builder/create_table_builder.go create mode 100644 builder/ddl_common.go create mode 100644 builder/drop_builder.go create mode 100644 ddl/ddl.go create mode 100644 ddl/ddl_test.go diff --git a/builder/alter_table_builder.go b/builder/alter_table_builder.go new file mode 100644 index 0000000..07716dd --- /dev/null +++ b/builder/alter_table_builder.go @@ -0,0 +1,507 @@ +package builder + +type alterActionKind int + +const ( + alterActionAddColumn alterActionKind = iota + alterActionDropColumn + alterActionAddConstraint + alterActionDropConstraint + alterActionRenameColumn + alterActionRenameTo + alterActionAlterColumnType + alterActionAlterColumnSetDefault + alterActionAlterColumnDropDefault + alterActionAlterColumnSetNotNull + alterActionAlterColumnDropNotNull +) + +type alterAction struct { + kind alterActionKind + column columnDef + ifNotExists bool // for ADD COLUMN IF NOT EXISTS + ifExists bool // for DROP COLUMN IF EXISTS, DROP CONSTRAINT IF EXISTS + constraint tableConstraint + oldName string + newName string + columnName string + typeName string + defaultExp Exp +} + +// AlterTable starts building an ALTER TABLE statement. +func AlterTable(tableName Identer) AlterTableBuilder { + return AlterTableBuilder{ + tableName: tableName, + } +} + +// AlterTableBuilder builds an ALTER TABLE statement. +type AlterTableBuilder struct { + tableName Identer + ifExists bool + actions []alterAction +} + +// IfExists adds IF EXISTS to the ALTER TABLE statement. +func (b AlterTableBuilder) IfExists() AlterTableBuilder { + newBuilder := b + newBuilder.ifExists = true + return newBuilder +} + +// AddColumn adds an ADD COLUMN action. +func (b AlterTableBuilder) AddColumn(name string, typeName string) AddColumnAlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionAddColumn, + column: columnDef{name: name, typeName: typeName}, + }) + return AddColumnAlterTableBuilder{AlterTableBuilder: newBuilder} +} + +// AddColumnIfNotExists adds an ADD COLUMN IF NOT EXISTS action. +func (b AlterTableBuilder) AddColumnIfNotExists(name string, typeName string) AddColumnAlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionAddColumn, + column: columnDef{name: name, typeName: typeName}, + ifNotExists: true, + }) + return AddColumnAlterTableBuilder{AlterTableBuilder: newBuilder} +} + +// DropColumn adds a DROP COLUMN action. +func (b AlterTableBuilder) DropColumn(name string) AlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionDropColumn, + columnName: name, + }) + return newBuilder +} + +// DropColumnIfExists adds a DROP COLUMN IF EXISTS action. +func (b AlterTableBuilder) DropColumnIfExists(name string) AlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionDropColumn, + columnName: name, + ifExists: true, + }) + return newBuilder +} + +// AddConstraint starts adding a named constraint. +func (b AlterTableBuilder) AddConstraint(name string) AddConstraintAlterTableBuilder { + return AddConstraintAlterTableBuilder{ + AlterTableBuilder: b, + constraintName: name, + } +} + +// DropConstraint adds a DROP CONSTRAINT action. +func (b AlterTableBuilder) DropConstraint(name string) AlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionDropConstraint, + constraint: tableConstraint{ + constraintName: name, + }, + }) + return newBuilder +} + +// DropConstraintIfExists adds a DROP CONSTRAINT IF EXISTS action. +func (b AlterTableBuilder) DropConstraintIfExists(name string) AlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionDropConstraint, + ifExists: true, + constraint: tableConstraint{ + constraintName: name, + }, + }) + return newBuilder +} + +// RenameColumn adds a RENAME COLUMN action. +func (b AlterTableBuilder) RenameColumn(oldName, newName string) AlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionRenameColumn, + oldName: oldName, + newName: newName, + }) + return newBuilder +} + +// RenameTo adds a RENAME TO action. +func (b AlterTableBuilder) RenameTo(newName string) AlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionRenameTo, + newName: newName, + }) + return newBuilder +} + +// AlterColumn starts an ALTER COLUMN sub-builder. +func (b AlterTableBuilder) AlterColumn(name string) AlterColumnBuilder { + return AlterColumnBuilder{ + AlterTableBuilder: b, + columnName: name, + } +} + +// WriteSQL writes the ALTER TABLE statement. +func (b AlterTableBuilder) WriteSQL(sb *SQLBuilder) { + sb.WriteString("ALTER TABLE ") + if b.ifExists { + sb.WriteString("IF EXISTS ") + } + b.tableName.WriteSQL(sb) + for i, action := range b.actions { + if i > 0 { + sb.WriteString(",") + } else { + sb.WriteRune(' ') + } + action.writeSQL(sb) + } +} + +func (a alterAction) writeSQL(sb *SQLBuilder) { + switch a.kind { + case alterActionAddColumn: + sb.WriteString("ADD COLUMN ") + if a.ifNotExists { + sb.WriteString("IF NOT EXISTS ") + } + a.column.writeSQL(sb) + case alterActionDropColumn: + sb.WriteString("DROP COLUMN ") + if a.ifExists { + sb.WriteString("IF EXISTS ") + } + sb.WriteString(quoteIdentifierIfKeyword(a.columnName)) + case alterActionAddConstraint: + sb.WriteString("ADD ") + a.constraint.writeSQL(sb) + case alterActionDropConstraint: + sb.WriteString("DROP CONSTRAINT ") + if a.ifExists { + sb.WriteString("IF EXISTS ") + } + sb.WriteString(quoteIdentifierIfKeyword(a.constraint.constraintName)) + case alterActionRenameColumn: + sb.WriteString("RENAME COLUMN ") + sb.WriteString(quoteIdentifierIfKeyword(a.oldName)) + sb.WriteString(" TO ") + sb.WriteString(quoteIdentifierIfKeyword(a.newName)) + case alterActionRenameTo: + sb.WriteString("RENAME TO ") + sb.WriteString(quoteIdentifierIfKeyword(a.newName)) + case alterActionAlterColumnType: + sb.WriteString("ALTER COLUMN ") + sb.WriteString(quoteIdentifierIfKeyword(a.columnName)) + sb.WriteString(" TYPE ") + sb.WriteString(a.typeName) + case alterActionAlterColumnSetDefault: + sb.WriteString("ALTER COLUMN ") + sb.WriteString(quoteIdentifierIfKeyword(a.columnName)) + sb.WriteString(" SET DEFAULT ") + a.defaultExp.WriteSQL(sb) + case alterActionAlterColumnDropDefault: + sb.WriteString("ALTER COLUMN ") + sb.WriteString(quoteIdentifierIfKeyword(a.columnName)) + sb.WriteString(" DROP DEFAULT") + case alterActionAlterColumnSetNotNull: + sb.WriteString("ALTER COLUMN ") + sb.WriteString(quoteIdentifierIfKeyword(a.columnName)) + sb.WriteString(" SET NOT NULL") + case alterActionAlterColumnDropNotNull: + sb.WriteString("ALTER COLUMN ") + sb.WriteString(quoteIdentifierIfKeyword(a.columnName)) + sb.WriteString(" DROP NOT NULL") + } +} + +// --- AddColumnAlterTableBuilder --- + +// AddColumnAlterTableBuilder provides column constraint methods after AddColumn. +type AddColumnAlterTableBuilder struct { + AlterTableBuilder +} + +// NotNull adds NOT NULL to the last added column. +func (b AddColumnAlterTableBuilder) NotNull() AddColumnAlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 0) + lastIdx := len(newBuilder.actions) - 1 + newBuilder.actions[lastIdx].column.notNull = true + return newBuilder +} + +// Default adds a DEFAULT expression to the last added column. +func (b AddColumnAlterTableBuilder) Default(exp Exp) AddColumnAlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 0) + lastIdx := len(newBuilder.actions) - 1 + newBuilder.actions[lastIdx].column.defaultExp = exp + return newBuilder +} + +// Unique adds a UNIQUE constraint to the last added column. +func (b AddColumnAlterTableBuilder) Unique() AddColumnAlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 0) + lastIdx := len(newBuilder.actions) - 1 + newBuilder.actions[lastIdx].column.unique = true + return newBuilder +} + +// PrimaryKey adds a PRIMARY KEY constraint to the last added column. +func (b AddColumnAlterTableBuilder) PrimaryKey() AddColumnAlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 0) + lastIdx := len(newBuilder.actions) - 1 + newBuilder.actions[lastIdx].column.primaryKey = true + return newBuilder +} + +// Check adds a CHECK constraint to the last added column. +func (b AddColumnAlterTableBuilder) Check(exp Exp) AddColumnAlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 0) + lastIdx := len(newBuilder.actions) - 1 + newBuilder.actions[lastIdx].column.check = exp + return newBuilder +} + +// References adds a REFERENCES constraint to the last added column. +func (b AddColumnAlterTableBuilder) References(table Identer, columns ...string) ReferencesAlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 0) + lastIdx := len(newBuilder.actions) - 1 + newBuilder.actions[lastIdx].column.references = &columnReference{ + table: table, + columns: columns, + } + return ReferencesAlterTableBuilder{AlterTableBuilder: newBuilder.AlterTableBuilder} +} + +// --- ReferencesAlterTableBuilder --- + +// ReferencesAlterTableBuilder provides ON DELETE/ON UPDATE for ALTER TABLE ADD COLUMN REFERENCES. +type ReferencesAlterTableBuilder struct { + AlterTableBuilder +} + +// OnDelete adds ON DELETE action. +func (b ReferencesAlterTableBuilder) OnDelete(action string) ReferencesAlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 0) + lastIdx := len(newBuilder.actions) - 1 + ref := *newBuilder.actions[lastIdx].column.references + ref.onDelete = action + newBuilder.actions[lastIdx].column.references = &ref + return newBuilder +} + +// OnUpdate adds ON UPDATE action. +func (b ReferencesAlterTableBuilder) OnUpdate(action string) ReferencesAlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 0) + lastIdx := len(newBuilder.actions) - 1 + ref := *newBuilder.actions[lastIdx].column.references + ref.onUpdate = action + newBuilder.actions[lastIdx].column.references = &ref + return newBuilder +} + +// --- AddConstraintAlterTableBuilder --- + +// AddConstraintAlterTableBuilder holds a constraint name and provides methods to define the constraint kind. +type AddConstraintAlterTableBuilder struct { + AlterTableBuilder + constraintName string +} + +// PrimaryKey adds a named PRIMARY KEY constraint. +func (b AddConstraintAlterTableBuilder) PrimaryKey(columns ...string) AlterTableBuilder { + newBuilder := b.AlterTableBuilder + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionAddConstraint, + constraint: tableConstraint{ + constraintName: b.constraintName, + kind: tableConstraintPrimaryKey, + columns: columns, + }, + }) + return newBuilder +} + +// Unique adds a named UNIQUE constraint. +func (b AddConstraintAlterTableBuilder) Unique(columns ...string) AlterTableBuilder { + newBuilder := b.AlterTableBuilder + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionAddConstraint, + constraint: tableConstraint{ + constraintName: b.constraintName, + kind: tableConstraintUnique, + columns: columns, + }, + }) + return newBuilder +} + +// ForeignKey starts a named FOREIGN KEY constraint. +func (b AddConstraintAlterTableBuilder) ForeignKey(columns ...string) AddForeignKeyAlterTableBuilder { + return AddForeignKeyAlterTableBuilder{ + AlterTableBuilder: b.AlterTableBuilder, + constraintName: b.constraintName, + columns: columns, + } +} + +// Check adds a named CHECK constraint. +func (b AddConstraintAlterTableBuilder) Check(exp Exp) AlterTableBuilder { + newBuilder := b.AlterTableBuilder + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionAddConstraint, + constraint: tableConstraint{ + constraintName: b.constraintName, + kind: tableConstraintCheck, + checkExp: exp, + }, + }) + return newBuilder +} + +// --- AddForeignKeyAlterTableBuilder --- + +// AddForeignKeyAlterTableBuilder provides References method for ALTER TABLE ADD CONSTRAINT ... FOREIGN KEY. +type AddForeignKeyAlterTableBuilder struct { + AlterTableBuilder + constraintName string + columns []string +} + +// References specifies the referenced table and columns. +func (b AddForeignKeyAlterTableBuilder) References(table Identer, columns ...string) ReferencesConstraintAlterTableBuilder { + newBuilder := b.AlterTableBuilder + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionAddConstraint, + constraint: tableConstraint{ + constraintName: b.constraintName, + kind: tableConstraintForeignKey, + columns: b.columns, + refTable: table, + refColumns: columns, + }, + }) + return ReferencesConstraintAlterTableBuilder{AlterTableBuilder: newBuilder} +} + +// --- ReferencesConstraintAlterTableBuilder --- + +// ReferencesConstraintAlterTableBuilder provides ON DELETE/ON UPDATE for ALTER TABLE ADD CONSTRAINT ... FOREIGN KEY ... REFERENCES. +type ReferencesConstraintAlterTableBuilder struct { + AlterTableBuilder +} + +// OnDelete adds ON DELETE action. +func (b ReferencesConstraintAlterTableBuilder) OnDelete(action string) ReferencesConstraintAlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 0) + lastIdx := len(newBuilder.actions) - 1 + newBuilder.actions[lastIdx].constraint.onDelete = action + return newBuilder +} + +// OnUpdate adds ON UPDATE action. +func (b ReferencesConstraintAlterTableBuilder) OnUpdate(action string) ReferencesConstraintAlterTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.actions, b.actions, 0) + lastIdx := len(newBuilder.actions) - 1 + newBuilder.actions[lastIdx].constraint.onUpdate = action + return newBuilder +} + +// --- AlterColumnBuilder --- + +// AlterColumnBuilder provides ALTER COLUMN sub-actions. +type AlterColumnBuilder struct { + AlterTableBuilder + columnName string +} + +// Type adds an ALTER COLUMN ... TYPE action. +func (b AlterColumnBuilder) Type(typeName string) AlterTableBuilder { + newBuilder := b.AlterTableBuilder + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionAlterColumnType, + columnName: b.columnName, + typeName: typeName, + }) + return newBuilder +} + +// SetDefault adds an ALTER COLUMN ... SET DEFAULT action. +func (b AlterColumnBuilder) SetDefault(exp Exp) AlterTableBuilder { + newBuilder := b.AlterTableBuilder + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionAlterColumnSetDefault, + columnName: b.columnName, + defaultExp: exp, + }) + return newBuilder +} + +// DropDefault adds an ALTER COLUMN ... DROP DEFAULT action. +func (b AlterColumnBuilder) DropDefault() AlterTableBuilder { + newBuilder := b.AlterTableBuilder + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionAlterColumnDropDefault, + columnName: b.columnName, + }) + return newBuilder +} + +// SetNotNull adds an ALTER COLUMN ... SET NOT NULL action. +func (b AlterColumnBuilder) SetNotNull() AlterTableBuilder { + newBuilder := b.AlterTableBuilder + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionAlterColumnSetNotNull, + columnName: b.columnName, + }) + return newBuilder +} + +// DropNotNull adds an ALTER COLUMN ... DROP NOT NULL action. +func (b AlterColumnBuilder) DropNotNull() AlterTableBuilder { + newBuilder := b.AlterTableBuilder + cloneSlice(&newBuilder.actions, b.actions, 1) + newBuilder.actions = append(newBuilder.actions, alterAction{ + kind: alterActionAlterColumnDropNotNull, + columnName: b.columnName, + }) + return newBuilder +} diff --git a/builder/create_index_builder.go b/builder/create_index_builder.go new file mode 100644 index 0000000..126c108 --- /dev/null +++ b/builder/create_index_builder.go @@ -0,0 +1,124 @@ +package builder + +// CreateIndex starts building a CREATE INDEX statement. +func CreateIndex(indexName string) CreateIndexBuilder { + return CreateIndexBuilder{ + indexName: indexName, + } +} + +// CreateIndexBuilder builds a CREATE INDEX statement. +type CreateIndexBuilder struct { + indexName string + unique bool + concurrently bool + ifNotExists bool + tableName Identer + using string + columns []Exp + include []string + where []Exp +} + +// Unique adds UNIQUE to the CREATE INDEX statement. +func (b CreateIndexBuilder) Unique() CreateIndexBuilder { + newBuilder := b + newBuilder.unique = true + return newBuilder +} + +// Concurrently adds CONCURRENTLY to the CREATE INDEX statement. +func (b CreateIndexBuilder) Concurrently() CreateIndexBuilder { + newBuilder := b + newBuilder.concurrently = true + return newBuilder +} + +// IfNotExists adds IF NOT EXISTS to the CREATE INDEX statement. +func (b CreateIndexBuilder) IfNotExists() CreateIndexBuilder { + newBuilder := b + newBuilder.ifNotExists = true + return newBuilder +} + +// On sets the table for the index. +func (b CreateIndexBuilder) On(tableName Identer) CreateIndexBuilder { + newBuilder := b + newBuilder.tableName = tableName + return newBuilder +} + +// Using sets the index method (e.g. btree, hash, gin, gist). +func (b CreateIndexBuilder) Using(method string) CreateIndexBuilder { + newBuilder := b + newBuilder.using = method + return newBuilder +} + +// Columns sets the indexed columns or expressions. +func (b CreateIndexBuilder) Columns(columns ...Exp) CreateIndexBuilder { + newBuilder := b + cloneSlice(&newBuilder.columns, b.columns, len(columns)) + newBuilder.columns = append(newBuilder.columns, columns...) + return newBuilder +} + +// Include adds columns to the INCLUDE clause. +func (b CreateIndexBuilder) Include(columns ...string) CreateIndexBuilder { + newBuilder := b + cloneSlice(&newBuilder.include, b.include, len(columns)) + newBuilder.include = append(newBuilder.include, columns...) + return newBuilder +} + +// Where adds a WHERE condition for a partial index. +// Multiple calls to Where are joined with AND. +func (b CreateIndexBuilder) Where(cond Exp) CreateIndexBuilder { + newBuilder := b + cloneSlice(&newBuilder.where, b.where, 1) + newBuilder.where = append(newBuilder.where, cond) + return newBuilder +} + +// WriteSQL writes the CREATE INDEX statement. +func (b CreateIndexBuilder) WriteSQL(sb *SQLBuilder) { + sb.WriteString("CREATE ") + if b.unique { + sb.WriteString("UNIQUE ") + } + sb.WriteString("INDEX ") + if b.concurrently { + sb.WriteString("CONCURRENTLY ") + } + if b.ifNotExists { + sb.WriteString("IF NOT EXISTS ") + } + sb.WriteString(quoteIdentifierIfKeyword(b.indexName)) + if b.tableName != nil { + sb.WriteString(" ON ") + b.tableName.WriteSQL(sb) + } + if b.using != "" { + sb.WriteString(" USING ") + sb.WriteString(b.using) + } + if len(b.columns) > 0 { + sb.WriteString(" (") + for i, col := range b.columns { + if i > 0 { + sb.WriteString(",") + } + col.WriteSQL(sb) + } + sb.WriteRune(')') + } + if len(b.include) > 0 { + sb.WriteString(" INCLUDE (") + writeColumnList(sb, b.include) + sb.WriteRune(')') + } + if len(b.where) > 0 { + sb.WriteString(" WHERE ") + And(b.where...).WriteSQL(sb) + } +} diff --git a/builder/create_schema_builder.go b/builder/create_schema_builder.go new file mode 100644 index 0000000..081fd05 --- /dev/null +++ b/builder/create_schema_builder.go @@ -0,0 +1,42 @@ +package builder + +// CreateSchema starts building a CREATE SCHEMA statement. +func CreateSchema(schemaName Identer) CreateSchemaBuilder { + return CreateSchemaBuilder{ + schemaName: schemaName, + } +} + +// CreateSchemaBuilder builds a CREATE SCHEMA statement. +type CreateSchemaBuilder struct { + schemaName Identer + ifNotExists bool + authorization string +} + +// IfNotExists adds IF NOT EXISTS to the CREATE SCHEMA statement. +func (b CreateSchemaBuilder) IfNotExists() CreateSchemaBuilder { + newBuilder := b + newBuilder.ifNotExists = true + return newBuilder +} + +// Authorization adds AUTHORIZATION to the CREATE SCHEMA statement. +func (b CreateSchemaBuilder) Authorization(role string) CreateSchemaBuilder { + newBuilder := b + newBuilder.authorization = role + return newBuilder +} + +// WriteSQL writes the CREATE SCHEMA statement. +func (b CreateSchemaBuilder) WriteSQL(sb *SQLBuilder) { + sb.WriteString("CREATE SCHEMA ") + if b.ifNotExists { + sb.WriteString("IF NOT EXISTS ") + } + b.schemaName.WriteSQL(sb) + if b.authorization != "" { + sb.WriteString(" AUTHORIZATION ") + sb.WriteString(quoteIdentifierIfKeyword(b.authorization)) + } +} diff --git a/builder/create_table_builder.go b/builder/create_table_builder.go new file mode 100644 index 0000000..2ccc61c --- /dev/null +++ b/builder/create_table_builder.go @@ -0,0 +1,315 @@ +package builder + +// CreateTable starts building a CREATE TABLE statement. +func CreateTable(tableName Identer) CreateTableBuilder { + return CreateTableBuilder{ + tableName: tableName, + } +} + +// CreateTableBuilder builds a CREATE TABLE statement. +type CreateTableBuilder struct { + tableName Identer + ifNotExists bool + unlogged bool + columns []columnDef + constraints []tableConstraint +} + +// IfNotExists adds IF NOT EXISTS to the CREATE TABLE statement. +func (b CreateTableBuilder) IfNotExists() CreateTableBuilder { + newBuilder := b + newBuilder.ifNotExists = true + return newBuilder +} + +// Unlogged adds UNLOGGED to the CREATE TABLE statement. +func (b CreateTableBuilder) Unlogged() CreateTableBuilder { + newBuilder := b + newBuilder.unlogged = true + return newBuilder +} + +// Column adds a column definition to the CREATE TABLE statement. +func (b CreateTableBuilder) Column(name string, typeName string) ColumnCreateTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.columns, b.columns, 1) + newBuilder.columns = append(newBuilder.columns, columnDef{ + name: name, + typeName: typeName, + }) + return ColumnCreateTableBuilder{CreateTableBuilder: newBuilder} +} + +// Constraint adds a named table-level constraint. +func (b CreateTableBuilder) Constraint(name string) ConstraintCreateTableBuilder { + return ConstraintCreateTableBuilder{ + CreateTableBuilder: b, + constraintName: name, + } +} + +// PrimaryKey adds an anonymous table-level PRIMARY KEY constraint. +func (b CreateTableBuilder) PrimaryKey(columns ...string) CreateTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.constraints, b.constraints, 1) + newBuilder.constraints = append(newBuilder.constraints, tableConstraint{ + kind: tableConstraintPrimaryKey, + columns: columns, + }) + return newBuilder +} + +// Unique adds an anonymous table-level UNIQUE constraint. +func (b CreateTableBuilder) Unique(columns ...string) CreateTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.constraints, b.constraints, 1) + newBuilder.constraints = append(newBuilder.constraints, tableConstraint{ + kind: tableConstraintUnique, + columns: columns, + }) + return newBuilder +} + +// ForeignKey starts an anonymous table-level FOREIGN KEY constraint. +func (b CreateTableBuilder) ForeignKey(columns ...string) ForeignKeyCreateTableBuilder { + return ForeignKeyCreateTableBuilder{ + CreateTableBuilder: b, + columns: columns, + } +} + +// Check adds an anonymous table-level CHECK constraint. +func (b CreateTableBuilder) Check(exp Exp) CreateTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.constraints, b.constraints, 1) + newBuilder.constraints = append(newBuilder.constraints, tableConstraint{ + kind: tableConstraintCheck, + checkExp: exp, + }) + return newBuilder +} + +// WriteSQL writes the CREATE TABLE statement. +func (b CreateTableBuilder) WriteSQL(sb *SQLBuilder) { + sb.WriteString("CREATE ") + if b.unlogged { + sb.WriteString("UNLOGGED ") + } + sb.WriteString("TABLE ") + if b.ifNotExists { + sb.WriteString("IF NOT EXISTS ") + } + b.tableName.WriteSQL(sb) + sb.WriteString(" (") + idx := 0 + for _, col := range b.columns { + if idx > 0 { + sb.WriteString(",") + } + col.writeSQL(sb) + idx++ + } + for _, c := range b.constraints { + if idx > 0 { + sb.WriteString(",") + } + c.writeSQL(sb) + idx++ + } + sb.WriteRune(')') +} + +// --- ColumnCreateTableBuilder --- + +// ColumnCreateTableBuilder is returned after adding a column, providing column constraint methods. +type ColumnCreateTableBuilder struct { + CreateTableBuilder +} + +// NotNull adds a NOT NULL constraint to the last column. +func (b ColumnCreateTableBuilder) NotNull() ColumnCreateTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.columns, b.columns, 0) + lastIdx := len(newBuilder.columns) - 1 + newBuilder.columns[lastIdx].notNull = true + return newBuilder +} + +// Default adds a DEFAULT expression to the last column. +func (b ColumnCreateTableBuilder) Default(exp Exp) ColumnCreateTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.columns, b.columns, 0) + lastIdx := len(newBuilder.columns) - 1 + newBuilder.columns[lastIdx].defaultExp = exp + return newBuilder +} + +// PrimaryKey adds a PRIMARY KEY constraint to the last column. +func (b ColumnCreateTableBuilder) PrimaryKey() ColumnCreateTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.columns, b.columns, 0) + lastIdx := len(newBuilder.columns) - 1 + newBuilder.columns[lastIdx].primaryKey = true + return newBuilder +} + +// Unique adds a UNIQUE constraint to the last column. +func (b ColumnCreateTableBuilder) Unique() ColumnCreateTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.columns, b.columns, 0) + lastIdx := len(newBuilder.columns) - 1 + newBuilder.columns[lastIdx].unique = true + return newBuilder +} + +// Check adds a CHECK constraint to the last column. +func (b ColumnCreateTableBuilder) Check(exp Exp) ColumnCreateTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.columns, b.columns, 0) + lastIdx := len(newBuilder.columns) - 1 + newBuilder.columns[lastIdx].check = exp + return newBuilder +} + +// References adds a REFERENCES constraint to the last column. +func (b ColumnCreateTableBuilder) References(table Identer, columns ...string) ReferencesCreateTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.columns, b.columns, 0) + lastIdx := len(newBuilder.columns) - 1 + newBuilder.columns[lastIdx].references = &columnReference{ + table: table, + columns: columns, + } + return ReferencesCreateTableBuilder{CreateTableBuilder: newBuilder.CreateTableBuilder} +} + +// --- ReferencesCreateTableBuilder --- + +// ReferencesCreateTableBuilder is returned after adding REFERENCES, providing ON DELETE/ON UPDATE methods. +type ReferencesCreateTableBuilder struct { + CreateTableBuilder +} + +// OnDelete adds ON DELETE action to the last column's REFERENCES constraint. +func (b ReferencesCreateTableBuilder) OnDelete(action string) ReferencesCreateTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.columns, b.columns, 0) + lastIdx := len(newBuilder.columns) - 1 + ref := *newBuilder.columns[lastIdx].references + ref.onDelete = action + newBuilder.columns[lastIdx].references = &ref + return newBuilder +} + +// OnUpdate adds ON UPDATE action to the last column's REFERENCES constraint. +func (b ReferencesCreateTableBuilder) OnUpdate(action string) ReferencesCreateTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.columns, b.columns, 0) + lastIdx := len(newBuilder.columns) - 1 + ref := *newBuilder.columns[lastIdx].references + ref.onUpdate = action + newBuilder.columns[lastIdx].references = &ref + return newBuilder +} + +// --- ConstraintCreateTableBuilder --- + +// ConstraintCreateTableBuilder holds a constraint name and provides methods to define the constraint kind. +type ConstraintCreateTableBuilder struct { + CreateTableBuilder + constraintName string +} + +// PrimaryKey creates a named PRIMARY KEY constraint. +func (b ConstraintCreateTableBuilder) PrimaryKey(columns ...string) CreateTableBuilder { + newBuilder := b.CreateTableBuilder + cloneSlice(&newBuilder.constraints, b.constraints, 1) + newBuilder.constraints = append(newBuilder.constraints, tableConstraint{ + constraintName: b.constraintName, + kind: tableConstraintPrimaryKey, + columns: columns, + }) + return newBuilder +} + +// Unique creates a named UNIQUE constraint. +func (b ConstraintCreateTableBuilder) Unique(columns ...string) CreateTableBuilder { + newBuilder := b.CreateTableBuilder + cloneSlice(&newBuilder.constraints, b.constraints, 1) + newBuilder.constraints = append(newBuilder.constraints, tableConstraint{ + constraintName: b.constraintName, + kind: tableConstraintUnique, + columns: columns, + }) + return newBuilder +} + +// ForeignKey creates a named FOREIGN KEY constraint. +func (b ConstraintCreateTableBuilder) ForeignKey(columns ...string) ForeignKeyCreateTableBuilder { + return ForeignKeyCreateTableBuilder{ + CreateTableBuilder: b.CreateTableBuilder, + constraintName: b.constraintName, + columns: columns, + } +} + +// Check creates a named CHECK constraint. +func (b ConstraintCreateTableBuilder) Check(exp Exp) CreateTableBuilder { + newBuilder := b.CreateTableBuilder + cloneSlice(&newBuilder.constraints, b.constraints, 1) + newBuilder.constraints = append(newBuilder.constraints, tableConstraint{ + constraintName: b.constraintName, + kind: tableConstraintCheck, + checkExp: exp, + }) + return newBuilder +} + +// --- ForeignKeyCreateTableBuilder --- + +// ForeignKeyCreateTableBuilder holds foreign key columns and provides References method. +type ForeignKeyCreateTableBuilder struct { + CreateTableBuilder + constraintName string + columns []string +} + +// References specifies the referenced table and columns for a FOREIGN KEY constraint. +func (b ForeignKeyCreateTableBuilder) References(table Identer, columns ...string) ReferencesConstraintCreateTableBuilder { + newBuilder := b.CreateTableBuilder + cloneSlice(&newBuilder.constraints, b.constraints, 1) + newBuilder.constraints = append(newBuilder.constraints, tableConstraint{ + constraintName: b.constraintName, + kind: tableConstraintForeignKey, + columns: b.columns, + refTable: table, + refColumns: columns, + }) + return ReferencesConstraintCreateTableBuilder{CreateTableBuilder: newBuilder} +} + +// --- ReferencesConstraintCreateTableBuilder --- + +// ReferencesConstraintCreateTableBuilder provides ON DELETE/ON UPDATE for table-level FOREIGN KEY constraints. +type ReferencesConstraintCreateTableBuilder struct { + CreateTableBuilder +} + +// OnDelete adds ON DELETE action to the last table-level FOREIGN KEY constraint. +func (b ReferencesConstraintCreateTableBuilder) OnDelete(action string) ReferencesConstraintCreateTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.constraints, b.constraints, 0) + lastIdx := len(newBuilder.constraints) - 1 + newBuilder.constraints[lastIdx].onDelete = action + return newBuilder +} + +// OnUpdate adds ON UPDATE action to the last table-level FOREIGN KEY constraint. +func (b ReferencesConstraintCreateTableBuilder) OnUpdate(action string) ReferencesConstraintCreateTableBuilder { + newBuilder := b + cloneSlice(&newBuilder.constraints, b.constraints, 0) + lastIdx := len(newBuilder.constraints) - 1 + newBuilder.constraints[lastIdx].onUpdate = action + return newBuilder +} diff --git a/builder/ddl_common.go b/builder/ddl_common.go new file mode 100644 index 0000000..d7c3d3e --- /dev/null +++ b/builder/ddl_common.go @@ -0,0 +1,134 @@ +package builder + +// columnDef represents a column definition in a CREATE TABLE or ALTER TABLE ADD COLUMN statement. +type columnDef struct { + name string + typeName string // raw SQL type string + notNull bool + defaultExp Exp + primaryKey bool + unique bool + check Exp + references *columnReference +} + +type columnReference struct { + table Identer + columns []string + onDelete string + onUpdate string +} + +func (c columnDef) writeSQL(sb *SQLBuilder) { + sb.WriteString(quoteIdentifierIfKeyword(c.name)) + sb.WriteRune(' ') + sb.WriteString(c.typeName) + if c.notNull { + sb.WriteString(" NOT NULL") + } + if c.defaultExp != nil { + sb.WriteString(" DEFAULT ") + c.defaultExp.WriteSQL(sb) + } + if c.primaryKey { + sb.WriteString(" PRIMARY KEY") + } + if c.unique { + sb.WriteString(" UNIQUE") + } + if c.check != nil { + sb.WriteString(" CHECK (") + c.check.WriteSQL(sb) + sb.WriteRune(')') + } + if c.references != nil { + c.references.writeSQL(sb) + } +} + +func (r columnReference) writeSQL(sb *SQLBuilder) { + sb.WriteString(" REFERENCES ") + r.table.WriteSQL(sb) + if len(r.columns) > 0 { + sb.WriteString(" (") + writeColumnList(sb, r.columns) + sb.WriteRune(')') + } + if r.onDelete != "" { + sb.WriteString(" ON DELETE ") + sb.WriteString(r.onDelete) + } + if r.onUpdate != "" { + sb.WriteString(" ON UPDATE ") + sb.WriteString(r.onUpdate) + } +} + +type tableConstraintKind int + +const ( + tableConstraintPrimaryKey tableConstraintKind = iota + tableConstraintUnique + tableConstraintForeignKey + tableConstraintCheck +) + +type tableConstraint struct { + constraintName string + kind tableConstraintKind + columns []string + refTable Identer + refColumns []string + onDelete string + onUpdate string + checkExp Exp +} + +func (c tableConstraint) writeSQL(sb *SQLBuilder) { + if c.constraintName != "" { + sb.WriteString("CONSTRAINT ") + sb.WriteString(quoteIdentifierIfKeyword(c.constraintName)) + sb.WriteRune(' ') + } + switch c.kind { + case tableConstraintPrimaryKey: + sb.WriteString("PRIMARY KEY (") + writeColumnList(sb, c.columns) + sb.WriteRune(')') + case tableConstraintUnique: + sb.WriteString("UNIQUE (") + writeColumnList(sb, c.columns) + sb.WriteRune(')') + case tableConstraintForeignKey: + sb.WriteString("FOREIGN KEY (") + writeColumnList(sb, c.columns) + sb.WriteString(") REFERENCES ") + c.refTable.WriteSQL(sb) + if len(c.refColumns) > 0 { + sb.WriteString(" (") + writeColumnList(sb, c.refColumns) + sb.WriteRune(')') + } + if c.onDelete != "" { + sb.WriteString(" ON DELETE ") + sb.WriteString(c.onDelete) + } + if c.onUpdate != "" { + sb.WriteString(" ON UPDATE ") + sb.WriteString(c.onUpdate) + } + case tableConstraintCheck: + sb.WriteString("CHECK (") + c.checkExp.WriteSQL(sb) + sb.WriteRune(')') + } +} + +func writeColumnList(sb *SQLBuilder, columns []string) { + for i, col := range columns { + if i > 0 { + sb.WriteString(",") + } + sb.WriteString(quoteIdentifierIfKeyword(col)) + } +} diff --git a/builder/drop_builder.go b/builder/drop_builder.go new file mode 100644 index 0000000..2c10c0b --- /dev/null +++ b/builder/drop_builder.go @@ -0,0 +1,123 @@ +package builder + +// DropTable starts building a DROP TABLE statement. +func DropTable(tableName Identer, rest ...Identer) DropTableBuilder { + names := make([]Identer, 0, 1+len(rest)) + names = append(names, tableName) + names = append(names, rest...) + return DropTableBuilder{ + tableNames: names, + } +} + +// DropTableBuilder builds a DROP TABLE statement. +type DropTableBuilder struct { + tableNames []Identer + ifExists bool + cascade bool + restrict bool +} + +// IfExists adds IF EXISTS to the DROP TABLE statement. +func (b DropTableBuilder) IfExists() DropTableBuilder { + newBuilder := b + newBuilder.ifExists = true + return newBuilder +} + +// Cascade adds CASCADE to the DROP TABLE statement. +func (b DropTableBuilder) Cascade() DropTableBuilder { + newBuilder := b + newBuilder.cascade = true + newBuilder.restrict = false + return newBuilder +} + +// Restrict adds RESTRICT to the DROP TABLE statement. +func (b DropTableBuilder) Restrict() DropTableBuilder { + newBuilder := b + newBuilder.restrict = true + newBuilder.cascade = false + return newBuilder +} + +// WriteSQL writes the DROP TABLE statement. +func (b DropTableBuilder) WriteSQL(sb *SQLBuilder) { + sb.WriteString("DROP TABLE ") + if b.ifExists { + sb.WriteString("IF EXISTS ") + } + for i, name := range b.tableNames { + if i > 0 { + sb.WriteString(",") + } + name.WriteSQL(sb) + } + if b.cascade { + sb.WriteString(" CASCADE") + } + if b.restrict { + sb.WriteString(" RESTRICT") + } +} + +// DropSchema starts building a DROP SCHEMA statement. +func DropSchema(schemaName Identer, rest ...Identer) DropSchemaBuilder { + names := make([]Identer, 0, 1+len(rest)) + names = append(names, schemaName) + names = append(names, rest...) + return DropSchemaBuilder{ + schemaNames: names, + } +} + +// DropSchemaBuilder builds a DROP SCHEMA statement. +type DropSchemaBuilder struct { + schemaNames []Identer + ifExists bool + cascade bool + restrict bool +} + +// IfExists adds IF EXISTS to the DROP SCHEMA statement. +func (b DropSchemaBuilder) IfExists() DropSchemaBuilder { + newBuilder := b + newBuilder.ifExists = true + return newBuilder +} + +// Cascade adds CASCADE to the DROP SCHEMA statement. +func (b DropSchemaBuilder) Cascade() DropSchemaBuilder { + newBuilder := b + newBuilder.cascade = true + newBuilder.restrict = false + return newBuilder +} + +// Restrict adds RESTRICT to the DROP SCHEMA statement. +func (b DropSchemaBuilder) Restrict() DropSchemaBuilder { + newBuilder := b + newBuilder.restrict = true + newBuilder.cascade = false + return newBuilder +} + +// WriteSQL writes the DROP SCHEMA statement. +func (b DropSchemaBuilder) WriteSQL(sb *SQLBuilder) { + sb.WriteString("DROP SCHEMA ") + if b.ifExists { + sb.WriteString("IF EXISTS ") + } + for i, name := range b.schemaNames { + if i > 0 { + sb.WriteString(",") + } + name.WriteSQL(sb) + } + if b.cascade { + sb.WriteString(" CASCADE") + } + if b.restrict { + sb.WriteString(" RESTRICT") + } +} diff --git a/ddl/ddl.go b/ddl/ddl.go new file mode 100644 index 0000000..1db0a19 --- /dev/null +++ b/ddl/ddl.go @@ -0,0 +1,33 @@ +package ddl + +import "github.com/networkteam/qrb/builder" + +// CreateTable starts building a CREATE TABLE statement. +func CreateTable(tableName builder.Identer) builder.CreateTableBuilder { + return builder.CreateTable(tableName) +} + +// CreateSchema starts building a CREATE SCHEMA statement. +func CreateSchema(schemaName builder.Identer) builder.CreateSchemaBuilder { + return builder.CreateSchema(schemaName) +} + +// CreateIndex starts building a CREATE INDEX statement. +func CreateIndex(indexName string) builder.CreateIndexBuilder { + return builder.CreateIndex(indexName) +} + +// DropTable starts building a DROP TABLE statement. +func DropTable(tableName builder.Identer, rest ...builder.Identer) builder.DropTableBuilder { + return builder.DropTable(tableName, rest...) +} + +// DropSchema starts building a DROP SCHEMA statement. +func DropSchema(schemaName builder.Identer, rest ...builder.Identer) builder.DropSchemaBuilder { + return builder.DropSchema(schemaName, rest...) +} + +// AlterTable starts building an ALTER TABLE statement. +func AlterTable(tableName builder.Identer) builder.AlterTableBuilder { + return builder.AlterTable(tableName) +} diff --git a/ddl/ddl_test.go b/ddl/ddl_test.go new file mode 100644 index 0000000..bd742de --- /dev/null +++ b/ddl/ddl_test.go @@ -0,0 +1,936 @@ +package ddl_test + +import ( + "testing" + + "github.com/networkteam/qrb" + "github.com/networkteam/qrb/ddl" + "github.com/networkteam/qrb/internal/testhelper" +) + +// --- CREATE TABLE --- + +func TestCreateTable(t *testing.T) { + t.Run("single column", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("users")). + Column("id", "INTEGER") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE users (id INTEGER)`, + nil, q, + ) + }) + + t.Run("multiple columns", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("users")). + Column("id", "INTEGER"). + Column("name", "TEXT"). + Column("email", "VARCHAR(255)") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE users (id INTEGER, name TEXT, email VARCHAR(255))`, + nil, q, + ) + }) + + t.Run("column with NOT NULL", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("users")). + Column("name", "TEXT").NotNull() + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE users (name TEXT NOT NULL)`, + nil, q, + ) + }) + + t.Run("column with DEFAULT", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("users")). + Column("active", "BOOLEAN").Default(qrb.Bool(true)) + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE users (active BOOLEAN DEFAULT true)`, + nil, q, + ) + }) + + t.Run("column with PRIMARY KEY", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("users")). + Column("id", "INTEGER").PrimaryKey() + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE users (id INTEGER PRIMARY KEY)`, + nil, q, + ) + }) + + t.Run("column with UNIQUE", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("users")). + Column("email", "TEXT").Unique() + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE users (email TEXT UNIQUE)`, + nil, q, + ) + }) + + t.Run("column with CHECK", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("products")). + Column("price", "NUMERIC").Check(qrb.N("price").Gt(qrb.Int(0))) + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE products (price NUMERIC CHECK (price > 0))`, + nil, q, + ) + }) + + t.Run("column with REFERENCES", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("orders")). + Column("user_id", "INTEGER").References(qrb.N("users"), "id") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE orders (user_id INTEGER REFERENCES users (id))`, + nil, q, + ) + }) + + t.Run("column with REFERENCES ON DELETE CASCADE", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("orders")). + Column("user_id", "INTEGER").References(qrb.N("users"), "id").OnDelete("CASCADE") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE orders (user_id INTEGER REFERENCES users (id) ON DELETE CASCADE)`, + nil, q, + ) + }) + + t.Run("column with REFERENCES ON DELETE and ON UPDATE", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("orders")). + Column("user_id", "INTEGER").References(qrb.N("users"), "id"). + OnDelete("CASCADE").OnUpdate("SET NULL") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE orders (user_id INTEGER REFERENCES users (id) ON DELETE CASCADE ON UPDATE SET NULL)`, + nil, q, + ) + }) + + t.Run("all column constraints", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("users")). + Column("id", "SERIAL").PrimaryKey(). + Column("name", "TEXT").NotNull(). + Column("email", "TEXT").NotNull().Unique(). + Column("age", "INTEGER").Check(qrb.N("age").Gte(qrb.Int(0))). + Column("dept_id", "INTEGER").References(qrb.N("departments"), "id").OnDelete("SET NULL") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + age INTEGER CHECK (age >= 0), + dept_id INTEGER REFERENCES departments (id) ON DELETE SET NULL + )`, + nil, q, + ) + }) + + t.Run("table-level PRIMARY KEY", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("order_items")). + Column("order_id", "INTEGER"). + Column("item_id", "INTEGER").CreateTableBuilder. + PrimaryKey("order_id", "item_id") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE order_items (order_id INTEGER, item_id INTEGER, PRIMARY KEY (order_id, item_id))`, + nil, q, + ) + }) + + t.Run("table-level UNIQUE", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("users")). + Column("first_name", "TEXT"). + Column("last_name", "TEXT").CreateTableBuilder. + Unique("first_name", "last_name") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE users (first_name TEXT, last_name TEXT, UNIQUE (first_name, last_name))`, + nil, q, + ) + }) + + t.Run("table-level FOREIGN KEY", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("orders")). + Column("user_id", "INTEGER"). + ForeignKey("user_id").References(qrb.N("users"), "id") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE orders (user_id INTEGER, FOREIGN KEY (user_id) REFERENCES users (id))`, + nil, q, + ) + }) + + t.Run("table-level FOREIGN KEY with ON DELETE and ON UPDATE", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("orders")). + Column("user_id", "INTEGER"). + ForeignKey("user_id").References(qrb.N("users"), "id"). + OnDelete("CASCADE").OnUpdate("RESTRICT") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE orders (user_id INTEGER, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT)`, + nil, q, + ) + }) + + t.Run("table-level CHECK", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("products")). + Column("price", "NUMERIC"). + Column("discounted_price", "NUMERIC").CreateTableBuilder. + Check(qrb.N("discounted_price").Lt(qrb.N("price"))) + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE products (price NUMERIC, discounted_price NUMERIC, CHECK (discounted_price < price))`, + nil, q, + ) + }) + + t.Run("named constraint", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("users")). + Column("id", "INTEGER"). + Constraint("users_pk").PrimaryKey("id") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE users (id INTEGER, CONSTRAINT users_pk PRIMARY KEY (id))`, + nil, q, + ) + }) + + t.Run("named FOREIGN KEY constraint", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("orders")). + Column("user_id", "INTEGER"). + Constraint("fk_user").ForeignKey("user_id").References(qrb.N("users"), "id") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE orders (user_id INTEGER, CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id))`, + nil, q, + ) + }) + + t.Run("named CHECK constraint", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("products")). + Column("price", "NUMERIC"). + Constraint("positive_price").Check(qrb.N("price").Gt(qrb.Int(0))) + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE products (price NUMERIC, CONSTRAINT positive_price CHECK (price > 0))`, + nil, q, + ) + }) + + t.Run("IF NOT EXISTS", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("users")).IfNotExists(). + Column("id", "INTEGER") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE IF NOT EXISTS users (id INTEGER)`, + nil, q, + ) + }) + + t.Run("UNLOGGED", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("sessions")).Unlogged(). + Column("id", "TEXT") + + testhelper.AssertSQLWriterEquals(t, + `CREATE UNLOGGED TABLE sessions (id TEXT)`, + nil, q, + ) + }) + + t.Run("reserved keyword column name", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("t")). + Column("select", "TEXT"). + Column("from", "TEXT") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE t ("select" TEXT, "from" TEXT)`, + nil, q, + ) + }) + + t.Run("schema-qualified table name", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("myschema.users")). + Column("id", "INTEGER") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE myschema.users (id INTEGER)`, + nil, q, + ) + }) + + t.Run("column with DEFAULT expression", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("events")). + Column("created_at", "TIMESTAMP WITH TIME ZONE").Default(qrb.Func("now")) + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE events (created_at TIMESTAMP WITH TIME ZONE DEFAULT now())`, + nil, q, + ) + }) + + t.Run("REFERENCES without columns", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("orders")). + Column("user_id", "INTEGER").References(qrb.N("users")) + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE orders (user_id INTEGER REFERENCES users)`, + nil, q, + ) + }) + + t.Run("column after references chain continues correctly", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("orders")). + Column("user_id", "INTEGER").References(qrb.N("users"), "id").OnDelete("CASCADE"). + Column("product_id", "INTEGER") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE orders (user_id INTEGER REFERENCES users (id) ON DELETE CASCADE, product_id INTEGER)`, + nil, q, + ) + }) + + t.Run("named UNIQUE constraint", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("users")). + Column("first_name", "TEXT"). + Column("last_name", "TEXT"). + Constraint("uq_name").Unique("first_name", "last_name") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE users (first_name TEXT, last_name TEXT, CONSTRAINT uq_name UNIQUE (first_name, last_name))`, + nil, q, + ) + }) + + t.Run("table-level FOREIGN KEY with ON UPDATE only", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("orders")). + Column("user_id", "INTEGER"). + ForeignKey("user_id").References(qrb.N("users"), "id"). + OnUpdate("CASCADE") + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE orders (user_id INTEGER, FOREIGN KEY (user_id) REFERENCES users (id) ON UPDATE CASCADE)`, + nil, q, + ) + }) + + t.Run("columns and table constraints combined", func(t *testing.T) { + q := ddl.CreateTable(qrb.N("order_items")). + Column("order_id", "INTEGER").NotNull(). + Column("product_id", "INTEGER").NotNull(). + Column("quantity", "INTEGER").Default(qrb.Int(1)).CreateTableBuilder. + PrimaryKey("order_id", "product_id"). + ForeignKey("order_id").References(qrb.N("orders"), "id").OnDelete("CASCADE"). + ForeignKey("product_id").References(qrb.N("products"), "id").OnDelete("RESTRICT"). + Check(qrb.N("quantity").Gt(qrb.Int(0))) + + testhelper.AssertSQLWriterEquals(t, + `CREATE TABLE order_items ( + order_id INTEGER NOT NULL, + product_id INTEGER NOT NULL, + quantity INTEGER DEFAULT 1, + PRIMARY KEY (order_id, product_id), + FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE, + FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE RESTRICT, + CHECK (quantity > 0) + )`, + nil, q, + ) + }) +} + +// --- CREATE SCHEMA --- + +func TestCreateSchema(t *testing.T) { + t.Run("basic", func(t *testing.T) { + q := ddl.CreateSchema(qrb.N("myschema")) + + testhelper.AssertSQLWriterEquals(t, + `CREATE SCHEMA myschema`, + nil, q, + ) + }) + + t.Run("IF NOT EXISTS", func(t *testing.T) { + q := ddl.CreateSchema(qrb.N("myschema")).IfNotExists() + + testhelper.AssertSQLWriterEquals(t, + `CREATE SCHEMA IF NOT EXISTS myschema`, + nil, q, + ) + }) + + t.Run("AUTHORIZATION", func(t *testing.T) { + q := ddl.CreateSchema(qrb.N("myschema")).Authorization("admin") + + testhelper.AssertSQLWriterEquals(t, + `CREATE SCHEMA myschema AUTHORIZATION admin`, + nil, q, + ) + }) + + t.Run("IF NOT EXISTS with AUTHORIZATION", func(t *testing.T) { + q := ddl.CreateSchema(qrb.N("myschema")).IfNotExists().Authorization("admin") + + testhelper.AssertSQLWriterEquals(t, + `CREATE SCHEMA IF NOT EXISTS myschema AUTHORIZATION admin`, + nil, q, + ) + }) +} + +// --- CREATE INDEX --- + +func TestCreateIndex(t *testing.T) { + t.Run("basic", func(t *testing.T) { + q := ddl.CreateIndex("idx_users_name"). + On(qrb.N("users")). + Columns(qrb.N("name")) + + testhelper.AssertSQLWriterEquals(t, + `CREATE INDEX idx_users_name ON users (name)`, + nil, q, + ) + }) + + t.Run("UNIQUE", func(t *testing.T) { + q := ddl.CreateIndex("idx_users_email").Unique(). + On(qrb.N("users")). + Columns(qrb.N("email")) + + testhelper.AssertSQLWriterEquals(t, + `CREATE UNIQUE INDEX idx_users_email ON users (email)`, + nil, q, + ) + }) + + t.Run("CONCURRENTLY", func(t *testing.T) { + q := ddl.CreateIndex("idx_users_name").Concurrently(). + On(qrb.N("users")). + Columns(qrb.N("name")) + + testhelper.AssertSQLWriterEquals(t, + `CREATE INDEX CONCURRENTLY idx_users_name ON users (name)`, + nil, q, + ) + }) + + t.Run("IF NOT EXISTS", func(t *testing.T) { + q := ddl.CreateIndex("idx_users_name").IfNotExists(). + On(qrb.N("users")). + Columns(qrb.N("name")) + + testhelper.AssertSQLWriterEquals(t, + `CREATE INDEX IF NOT EXISTS idx_users_name ON users (name)`, + nil, q, + ) + }) + + t.Run("USING method", func(t *testing.T) { + q := ddl.CreateIndex("idx_users_data"). + On(qrb.N("users")). + Using("gin"). + Columns(qrb.N("data")) + + testhelper.AssertSQLWriterEquals(t, + `CREATE INDEX idx_users_data ON users USING gin (data)`, + nil, q, + ) + }) + + t.Run("multiple columns", func(t *testing.T) { + q := ddl.CreateIndex("idx_users_name_email"). + On(qrb.N("users")). + Columns(qrb.N("first_name"), qrb.N("last_name")) + + testhelper.AssertSQLWriterEquals(t, + `CREATE INDEX idx_users_name_email ON users (first_name, last_name)`, + nil, q, + ) + }) + + t.Run("INCLUDE", func(t *testing.T) { + q := ddl.CreateIndex("idx_users_name"). + On(qrb.N("users")). + Columns(qrb.N("name")). + Include("email") + + testhelper.AssertSQLWriterEquals(t, + `CREATE INDEX idx_users_name ON users (name) INCLUDE (email)`, + nil, q, + ) + }) + + t.Run("WHERE partial index", func(t *testing.T) { + q := ddl.CreateIndex("idx_active_users"). + On(qrb.N("users")). + Columns(qrb.N("name")). + Where(qrb.N("active").Eq(qrb.Bool(true))) + + testhelper.AssertSQLWriterEquals(t, + `CREATE INDEX idx_active_users ON users (name) WHERE active = true`, + nil, q, + ) + }) + + t.Run("combined options", func(t *testing.T) { + q := ddl.CreateIndex("idx_active_users_email"). + Unique().Concurrently().IfNotExists(). + On(qrb.N("users")). + Using("btree"). + Columns(qrb.N("email")). + Include("name"). + Where(qrb.N("active").Eq(qrb.Bool(true))) + + testhelper.AssertSQLWriterEquals(t, + `CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS idx_active_users_email ON users USING btree (email) INCLUDE (name) WHERE active = true`, + nil, q, + ) + }) + + t.Run("schema-qualified table", func(t *testing.T) { + q := ddl.CreateIndex("idx_name"). + On(qrb.N("myschema.users")). + Columns(qrb.N("name")) + + testhelper.AssertSQLWriterEquals(t, + `CREATE INDEX idx_name ON myschema.users (name)`, + nil, q, + ) + }) +} + +// --- DROP TABLE --- + +func TestDropTable(t *testing.T) { + t.Run("basic", func(t *testing.T) { + q := ddl.DropTable(qrb.N("users")) + + testhelper.AssertSQLWriterEquals(t, + `DROP TABLE users`, + nil, q, + ) + }) + + t.Run("IF EXISTS", func(t *testing.T) { + q := ddl.DropTable(qrb.N("users")).IfExists() + + testhelper.AssertSQLWriterEquals(t, + `DROP TABLE IF EXISTS users`, + nil, q, + ) + }) + + t.Run("CASCADE", func(t *testing.T) { + q := ddl.DropTable(qrb.N("users")).Cascade() + + testhelper.AssertSQLWriterEquals(t, + `DROP TABLE users CASCADE`, + nil, q, + ) + }) + + t.Run("RESTRICT", func(t *testing.T) { + q := ddl.DropTable(qrb.N("users")).Restrict() + + testhelper.AssertSQLWriterEquals(t, + `DROP TABLE users RESTRICT`, + nil, q, + ) + }) + + t.Run("multiple targets", func(t *testing.T) { + q := ddl.DropTable(qrb.N("users"), qrb.N("orders")) + + testhelper.AssertSQLWriterEquals(t, + `DROP TABLE users, orders`, + nil, q, + ) + }) + + t.Run("IF EXISTS CASCADE multiple targets", func(t *testing.T) { + q := ddl.DropTable(qrb.N("users"), qrb.N("orders")).IfExists().Cascade() + + testhelper.AssertSQLWriterEquals(t, + `DROP TABLE IF EXISTS users, orders CASCADE`, + nil, q, + ) + }) +} + +// --- DROP SCHEMA --- + +func TestDropSchema(t *testing.T) { + t.Run("basic", func(t *testing.T) { + q := ddl.DropSchema(qrb.N("myschema")) + + testhelper.AssertSQLWriterEquals(t, + `DROP SCHEMA myschema`, + nil, q, + ) + }) + + t.Run("IF EXISTS", func(t *testing.T) { + q := ddl.DropSchema(qrb.N("myschema")).IfExists() + + testhelper.AssertSQLWriterEquals(t, + `DROP SCHEMA IF EXISTS myschema`, + nil, q, + ) + }) + + t.Run("CASCADE", func(t *testing.T) { + q := ddl.DropSchema(qrb.N("myschema")).Cascade() + + testhelper.AssertSQLWriterEquals(t, + `DROP SCHEMA myschema CASCADE`, + nil, q, + ) + }) + + t.Run("RESTRICT", func(t *testing.T) { + q := ddl.DropSchema(qrb.N("myschema")).Restrict() + + testhelper.AssertSQLWriterEquals(t, + `DROP SCHEMA myschema RESTRICT`, + nil, q, + ) + }) + + t.Run("multiple targets", func(t *testing.T) { + q := ddl.DropSchema(qrb.N("schema1"), qrb.N("schema2")) + + testhelper.AssertSQLWriterEquals(t, + `DROP SCHEMA schema1, schema2`, + nil, q, + ) + }) +} + +// --- ALTER TABLE --- + +func TestAlterTable(t *testing.T) { + t.Run("ADD COLUMN", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AddColumn("email", "TEXT") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ADD COLUMN email TEXT`, + nil, q, + ) + }) + + t.Run("ADD COLUMN with NOT NULL", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AddColumn("email", "TEXT").NotNull() + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ADD COLUMN email TEXT NOT NULL`, + nil, q, + ) + }) + + t.Run("ADD COLUMN with DEFAULT", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AddColumn("active", "BOOLEAN").Default(qrb.Bool(true)) + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ADD COLUMN active BOOLEAN DEFAULT true`, + nil, q, + ) + }) + + t.Run("ADD COLUMN with UNIQUE", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AddColumn("email", "TEXT").Unique() + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ADD COLUMN email TEXT UNIQUE`, + nil, q, + ) + }) + + t.Run("ADD COLUMN with REFERENCES", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("orders")). + AddColumn("user_id", "INTEGER").References(qrb.N("users"), "id") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE orders ADD COLUMN user_id INTEGER REFERENCES users (id)`, + nil, q, + ) + }) + + t.Run("ADD COLUMN with REFERENCES ON DELETE CASCADE", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("orders")). + AddColumn("user_id", "INTEGER").References(qrb.N("users"), "id").OnDelete("CASCADE") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE orders ADD COLUMN user_id INTEGER REFERENCES users (id) ON DELETE CASCADE`, + nil, q, + ) + }) + + t.Run("ADD COLUMN IF NOT EXISTS", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AddColumnIfNotExists("email", "TEXT") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ADD COLUMN IF NOT EXISTS email TEXT`, + nil, q, + ) + }) + + t.Run("DROP COLUMN", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + DropColumn("email") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users DROP COLUMN email`, + nil, q, + ) + }) + + t.Run("DROP COLUMN IF EXISTS", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + DropColumnIfExists("email") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users DROP COLUMN IF EXISTS email`, + nil, q, + ) + }) + + t.Run("ADD CONSTRAINT PRIMARY KEY", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AddConstraint("users_pk").PrimaryKey("id") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ADD CONSTRAINT users_pk PRIMARY KEY (id)`, + nil, q, + ) + }) + + t.Run("ADD CONSTRAINT UNIQUE", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AddConstraint("users_email_unique").Unique("email") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email)`, + nil, q, + ) + }) + + t.Run("ADD CONSTRAINT FOREIGN KEY", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("orders")). + AddConstraint("fk_user").ForeignKey("user_id").References(qrb.N("users"), "id") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id)`, + nil, q, + ) + }) + + t.Run("ADD CONSTRAINT FOREIGN KEY with ON DELETE", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("orders")). + AddConstraint("fk_user").ForeignKey("user_id").References(qrb.N("users"), "id"). + OnDelete("CASCADE") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE`, + nil, q, + ) + }) + + t.Run("ADD CONSTRAINT CHECK", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("products")). + AddConstraint("positive_price").Check(qrb.N("price").Gt(qrb.Int(0))) + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE products ADD CONSTRAINT positive_price CHECK (price > 0)`, + nil, q, + ) + }) + + t.Run("DROP CONSTRAINT", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + DropConstraint("users_pk") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users DROP CONSTRAINT users_pk`, + nil, q, + ) + }) + + t.Run("DROP CONSTRAINT IF EXISTS", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + DropConstraintIfExists("users_pk") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users DROP CONSTRAINT IF EXISTS users_pk`, + nil, q, + ) + }) + + t.Run("RENAME COLUMN", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + RenameColumn("name", "full_name") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users RENAME COLUMN name TO full_name`, + nil, q, + ) + }) + + t.Run("RENAME TO", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + RenameTo("people") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users RENAME TO people`, + nil, q, + ) + }) + + t.Run("ALTER COLUMN TYPE", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AlterColumn("name").Type("VARCHAR(255)") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ALTER COLUMN name TYPE VARCHAR(255)`, + nil, q, + ) + }) + + t.Run("ALTER COLUMN SET DEFAULT", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AlterColumn("active").SetDefault(qrb.Bool(true)) + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ALTER COLUMN active SET DEFAULT true`, + nil, q, + ) + }) + + t.Run("ALTER COLUMN DROP DEFAULT", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AlterColumn("active").DropDefault() + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ALTER COLUMN active DROP DEFAULT`, + nil, q, + ) + }) + + t.Run("ALTER COLUMN SET NOT NULL", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AlterColumn("name").SetNotNull() + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ALTER COLUMN name SET NOT NULL`, + nil, q, + ) + }) + + t.Run("ALTER COLUMN DROP NOT NULL", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AlterColumn("name").DropNotNull() + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ALTER COLUMN name DROP NOT NULL`, + nil, q, + ) + }) + + t.Run("IF EXISTS", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")).IfExists(). + AddColumn("email", "TEXT") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE IF EXISTS users ADD COLUMN email TEXT`, + nil, q, + ) + }) + + t.Run("multiple actions", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AddColumn("email", "TEXT"). + DropColumn("old_email") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ADD COLUMN email TEXT, DROP COLUMN old_email`, + nil, q, + ) + }) + + t.Run("reserved keyword column names", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("t")). + RenameColumn("select", "from") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE t RENAME COLUMN "select" TO "from"`, + nil, q, + ) + }) + + t.Run("ADD COLUMN with PRIMARY KEY", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AddColumn("id", "SERIAL").PrimaryKey() + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ADD COLUMN id SERIAL PRIMARY KEY`, + nil, q, + ) + }) + + t.Run("ADD COLUMN with CHECK", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("products")). + AddColumn("price", "NUMERIC").Check(qrb.N("price").Gt(qrb.Int(0))) + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE products ADD COLUMN price NUMERIC CHECK (price > 0)`, + nil, q, + ) + }) + + t.Run("ADD COLUMN chaining to more actions", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AddColumn("email", "TEXT").NotNull(). + AddColumn("age", "INTEGER").Check(qrb.N("age").Gte(qrb.Int(0))) + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ADD COLUMN email TEXT NOT NULL, ADD COLUMN age INTEGER CHECK (age >= 0)`, + nil, q, + ) + }) + + t.Run("ALTER COLUMN combined with other actions", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("users")). + AlterColumn("name").Type("VARCHAR(255)"). + AlterColumn("name").SetNotNull(). + AlterColumn("email").SetDefault(qrb.String("unknown")) + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE users ALTER COLUMN name TYPE VARCHAR(255), ALTER COLUMN name SET NOT NULL, ALTER COLUMN email SET DEFAULT 'unknown'`, + nil, q, + ) + }) + + t.Run("ADD CONSTRAINT FOREIGN KEY with ON DELETE and ON UPDATE", func(t *testing.T) { + q := ddl.AlterTable(qrb.N("orders")). + AddConstraint("fk_user").ForeignKey("user_id").References(qrb.N("users"), "id"). + OnDelete("CASCADE").OnUpdate("SET NULL") + + testhelper.AssertSQLWriterEquals(t, + `ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ON UPDATE SET NULL`, + nil, q, + ) + }) +} From 28fc3580bcba2b8c61763e333714d3ace71e19fb Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Tue, 3 Mar 2026 14:33:12 +0100 Subject: [PATCH 2/2] fix: linter issue --- builder/alter_table_builder.go | 2 +- builder/create_table_builder.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/builder/alter_table_builder.go b/builder/alter_table_builder.go index 07716dd..d97a2c9 100644 --- a/builder/alter_table_builder.go +++ b/builder/alter_table_builder.go @@ -296,7 +296,7 @@ func (b AddColumnAlterTableBuilder) References(table Identer, columns ...string) table: table, columns: columns, } - return ReferencesAlterTableBuilder{AlterTableBuilder: newBuilder.AlterTableBuilder} + return ReferencesAlterTableBuilder(newBuilder) } // --- ReferencesAlterTableBuilder --- diff --git a/builder/create_table_builder.go b/builder/create_table_builder.go index 2ccc61c..58802e6 100644 --- a/builder/create_table_builder.go +++ b/builder/create_table_builder.go @@ -181,7 +181,7 @@ func (b ColumnCreateTableBuilder) References(table Identer, columns ...string) R table: table, columns: columns, } - return ReferencesCreateTableBuilder{CreateTableBuilder: newBuilder.CreateTableBuilder} + return ReferencesCreateTableBuilder(newBuilder) } // --- ReferencesCreateTableBuilder ---