Skip to main content

Database Examples

These examples show the same migration patterns across Queen's supported drivers:

  • a clean SQL migration;
  • a Go-function migration;
  • a mixed migration where schema is SQL and data movement is Go.

Use the examples as starting points, then adjust SQL types and DDL to your database version.

PostgreSQL

PostgreSQL is Queen's reference production path. It supports transactional DDL for common schema changes and Queen records the migration row in the same transaction as the migration body.

SQL migration

q.MustAdd(queen.M{
Version: "001",
Name: "create_users",
UpSQL: `
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
`,
DownSQL: `DROP TABLE users;`,
})

Function migration

q.MustAdd(queen.M{
Version: "002",
Name: "normalize_user_emails",
ManualChecksum: "normalize-user-emails-v1",
UpFunc: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
UPDATE users
SET email = LOWER(TRIM(email))
`)
return err
},
})

Mixed migration

q.MustAdd(queen.M{
Version: "003",
Name: "add_profiles",
UpSQL: `
CREATE TABLE profiles (
user_id BIGINT PRIMARY KEY REFERENCES users(id),
display_name TEXT NOT NULL
);
`,
UpFunc: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO profiles (user_id, display_name)
SELECT id, split_part(email, '@', 1)
FROM users
`)
return err
},
DownSQL: `DROP TABLE profiles;`,
})

MySQL

MySQL uses ? placeholders. DDL transaction behavior depends on the statement and MySQL version, so test rollback on the same engine you run in production.

SQL migration

q.MustAdd(queen.M{
Version: "001",
Name: "create_users",
UpSQL: `
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(320) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`,
DownSQL: `DROP TABLE users;`,
})

Function migration

q.MustAdd(queen.M{
Version: "002",
Name: "normalize_user_emails",
ManualChecksum: "normalize-user-emails-v1",
UpFunc: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
UPDATE users
SET email = LOWER(TRIM(email))
`)
return err
},
})

Mixed migration

q.MustAdd(queen.M{
Version: "003",
Name: "add_profiles",
UpSQL: `
CREATE TABLE profiles (
user_id BIGINT PRIMARY KEY,
display_name VARCHAR(320) NOT NULL,
CONSTRAINT profiles_user_id_fk FOREIGN KEY (user_id) REFERENCES users(id)
);
`,
UpFunc: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO profiles (user_id, display_name)
SELECT id, SUBSTRING_INDEX(email, '@', 1)
FROM users
`)
return err
},
DownSQL: `DROP TABLE profiles;`,
})

SQLite

SQLite is best for local tools, tests, and single-process applications. Use INTEGER PRIMARY KEY for rowid-backed IDs.

SQL migration

q.MustAdd(queen.M{
Version: "001",
Name: "create_users",
UpSQL: `
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`,
DownSQL: `DROP TABLE users;`,
})

Function migration

q.MustAdd(queen.M{
Version: "002",
Name: "normalize_user_emails",
ManualChecksum: "normalize-user-emails-v1",
UpFunc: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
UPDATE users
SET email = LOWER(TRIM(email))
`)
return err
},
})

Mixed migration

q.MustAdd(queen.M{
Version: "003",
Name: "add_profiles",
UpSQL: `
CREATE TABLE profiles (
user_id INTEGER PRIMARY KEY,
display_name TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
);
`,
UpFunc: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO profiles (user_id, display_name)
SELECT id, substr(email, 1, instr(email, '@') - 1)
FROM users
`)
return err
},
DownSQL: `DROP TABLE profiles;`,
})

ClickHouse

ClickHouse is column-oriented and does not behave like an OLTP database. Prefer append-oriented schema changes and serialize migrator runs outside Queen.

SQL migration

q.MustAdd(queen.M{
Version: "001",
Name: "create_events",
UpSQL: `
CREATE TABLE events (
id UUID,
user_id UInt64,
event_name String,
created_at DateTime
)
ENGINE = MergeTree()
ORDER BY (created_at, user_id);
`,
DownSQL: `DROP TABLE events;`,
})

Function migration

q.MustAdd(queen.M{
Version: "002",
Name: "backfill_event_names",
ManualChecksum: "backfill-event-names-v1",
UpFunc: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
ALTER TABLE events
UPDATE event_name = 'unknown'
WHERE event_name = ''
`)
return err
},
})

Mixed migration

q.MustAdd(queen.M{
Version: "003",
Name: "add_events_materialized_view",
UpSQL: `
CREATE TABLE event_counts (
event_name String,
count UInt64
)
ENGINE = SummingMergeTree()
ORDER BY event_name;
`,
UpFunc: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO event_counts
SELECT event_name, count()
FROM events
GROUP BY event_name
`)
return err
},
DownSQL: `DROP TABLE event_counts;`,
})

CockroachDB

CockroachDB uses PostgreSQL-like SQL syntax, but serializable conflicts are normal and retryable. Deploy scripts should retry failed migration commands when the error is retryable.

SQL migration

q.MustAdd(queen.M{
Version: "001",
Name: "create_users",
UpSQL: `
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email STRING NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
`,
DownSQL: `DROP TABLE users;`,
})

Function migration

q.MustAdd(queen.M{
Version: "002",
Name: "normalize_user_emails",
ManualChecksum: "normalize-user-emails-v1",
UpFunc: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
UPDATE users
SET email = lower(trim(email))
`)
return err
},
})

Mixed migration

q.MustAdd(queen.M{
Version: "003",
Name: "add_profiles",
UpSQL: `
CREATE TABLE profiles (
user_id UUID PRIMARY KEY REFERENCES users(id),
display_name STRING NOT NULL
);
`,
UpFunc: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO profiles (user_id, display_name)
SELECT id, split_part(email, '@', 1)
FROM users
`)
return err
},
DownSQL: `DROP TABLE profiles;`,
})

MSSQL

MSSQL uses @p1, @p2, ... placeholders internally. Write SQL Server DDL and test rollback behavior on the same compatibility level you deploy.

SQL migration

q.MustAdd(queen.M{
Version: "001",
Name: "create_users",
UpSQL: `
CREATE TABLE users (
id BIGINT IDENTITY(1,1) PRIMARY KEY,
email NVARCHAR(320) NOT NULL UNIQUE,
created_at DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
);
`,
DownSQL: `DROP TABLE users;`,
})

Function migration

q.MustAdd(queen.M{
Version: "002",
Name: "normalize_user_emails",
ManualChecksum: "normalize-user-emails-v1",
UpFunc: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
UPDATE users
SET email = LOWER(LTRIM(RTRIM(email)))
`)
return err
},
})

Mixed migration

q.MustAdd(queen.M{
Version: "003",
Name: "add_profiles",
UpSQL: `
CREATE TABLE profiles (
user_id BIGINT PRIMARY KEY,
display_name NVARCHAR(320) NOT NULL,
CONSTRAINT profiles_user_id_fk FOREIGN KEY (user_id) REFERENCES users(id)
);
`,
UpFunc: func(ctx context.Context, tx *sql.Tx) error {
_, err := tx.ExecContext(ctx, `
INSERT INTO profiles (user_id, display_name)
SELECT id, LEFT(email, CHARINDEX('@', email + '@') - 1)
FROM users
`)
return err
},
DownSQL: `DROP TABLE profiles;`,
})