| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- package executor
- import (
- "testing"
- )
- // TestGoatCounterSQLiteMigrations runs the shapes of GoatCounter's SQLite
- // migrations that exercise the engine features added for the release-2.7 port:
- // JSON1 mutation/extraction, bitwise OR, and table-level UNIQUE ... ON CONFLICT
- // REPLACE.
- func TestGoatCounterSQLiteMigrations(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, `CREATE TABLE sites (
- site_id INTEGER PRIMARY KEY AUTOINCREMENT,
- settings TEXT NOT NULL DEFAULT '{}',
- user_defaults TEXT NOT NULL DEFAULT '{}'
- )`)
- execMust(t, e, `CREATE TABLE users (
- user_id INTEGER PRIMARY KEY AUTOINCREMENT,
- settings TEXT NOT NULL DEFAULT '{}'
- )`)
- execMust(t, e, `INSERT INTO sites (site_id, settings, user_defaults) VALUES
- (1, '{"public": 1, "collect": 0, "widgets": []}', '{"widgets": []}')`)
- execMust(t, e, `INSERT INTO users (user_id, settings) VALUES (1, '{"widgets": []}')`)
- // db/migrate/2021-06-27-1-public-sqlite.sql
- execMust(t, e, `UPDATE sites SET settings = json_set(settings, '$.public', 'public') WHERE json_extract(settings, '$.public') = 1`)
- execMust(t, e, `UPDATE sites SET settings = json_set(settings, '$.public', 'private') WHERE json_extract(settings, '$.public') = 0`)
- res := execMust(t, e, `SELECT json_extract(settings, '$.public') FROM sites WHERE site_id = 1`)
- if res.Rows[0][0] != "public" {
- t.Fatalf("public flag = %v, want public", res.Rows[0][0])
- }
- // db/migrate/2021-12-02-2-language-enable-sqlite.sql
- execMust(t, e, `UPDATE sites SET
- settings = json_replace(settings, '$.collect', json_extract(settings, '$.collect') | 64),
- user_defaults = json_replace(user_defaults, '$.widgets', json_insert(json_extract(user_defaults, '$.widgets'), '$[#]', json('{"n":"languages"}')))`)
- execMust(t, e, `UPDATE users SET
- settings = json_replace(settings, '$.widgets', json_insert(json_extract(settings, '$.widgets'), '$[#]', json('{"n":"languages"}')))`)
- res = execMust(t, e, `SELECT json_extract(settings, '$.collect') FROM sites WHERE site_id = 1`)
- if res.Rows[0][0] != int64(64) {
- t.Fatalf("collect flag = %v, want 64", res.Rows[0][0])
- }
- res = execMust(t, e, `SELECT json_extract(user_defaults, '$.widgets[0].n') FROM sites WHERE site_id = 1`)
- if res.Rows[0][0] != "languages" {
- t.Fatalf("widgets[0].n = %v, want languages", res.Rows[0][0])
- }
- // A GoatCounter stats table as created by 2022-01-13-1-unfk-sqlite.sql:
- // composite UNIQUE with ON CONFLICT REPLACE and no primary key.
- execMust(t, e, `CREATE TABLE hit_counts (
- site_id INTEGER NOT NULL,
- path_id INTEGER NOT NULL,
- hour TEXT NOT NULL,
- total INTEGER NOT NULL,
- CONSTRAINT "hit_counts#site_id#path_id#hour" UNIQUE(site_id, path_id, hour) ON CONFLICT REPLACE
- )`)
- execMust(t, e, `INSERT INTO hit_counts (site_id, path_id, hour, total) VALUES (1, 1, '2024-01-01 00:00:00', 5)`)
- execMust(t, e, `INSERT INTO hit_counts (site_id, path_id, hour, total) VALUES (1, 1, '2024-01-01 00:00:00', 9)`)
- res = execMust(t, e, `SELECT total FROM hit_counts WHERE site_id = 1 AND path_id = 1`)
- if res.RowCount != 1 || res.Rows[0][0] != int64(9) {
- t.Fatalf("hit_counts replace = %v rows %v", res.Rows, res.Rows)
- }
- }
- // TestGoatCounterMigrationRebuildShape covers the table-rebuild dance used by
- // 2021-12-09-1-email-reports-sqlite.sql and 2022-01-13-1-unfk-sqlite.sql:
- // create a replacement table, copy rows with INSERT ... SELECT, drop and rename,
- // then build indexes (including composite expression unique indexes).
- func TestGoatCounterMigrationRebuildShape(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE users (user_id INTEGER PRIMARY KEY AUTOINCREMENT, site_id INTEGER, email TEXT, seen_updates_at TIMESTAMP)")
- execMust(t, e, "INSERT INTO users (site_id, email) VALUES (1, 'A@x.com'), (1, 'b@x.com'), (2, 'a@x.com')")
- execMust(t, e, `CREATE TABLE users2 (
- user_id INTEGER PRIMARY KEY AUTOINCREMENT,
- site_id INTEGER NOT NULL,
- email VARCHAR NOT NULL,
- last_report_at TIMESTAMP NOT NULL DEFAULT current_timestamp
- )`)
- execMust(t, e, `INSERT INTO users2 (user_id, site_id, email)
- SELECT user_id, site_id, email FROM users`)
- execMust(t, e, "DROP TABLE users")
- execMust(t, e, "ALTER TABLE users2 RENAME TO users")
- execMust(t, e, `CREATE INDEX "users#site_id" ON users(site_id)`)
- execMust(t, e, `CREATE UNIQUE INDEX "users#site_id#email" ON users(site_id, lower(email))`)
- res := execMust(t, e, "SELECT count(*) FROM users")
- if res.Rows[0][0] != int64(3) {
- t.Fatalf("copied row count = %v, want 3", res.Rows[0][0])
- }
- // Case-insensitive uniqueness across the composite expression index.
- if _, err := execSQL(e, "INSERT INTO users (site_id, email) VALUES (1, 'a@x.com')"); err == nil {
- t.Fatal("expected composite expression unique index to reject a duplicate")
- }
- }
- func TestGoatCounterInsertWithSelectMigrationShape(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE old_sizes (size TEXT)")
- execMust(t, e, "CREATE TABLE sizes (width INTEGER, height INTEGER)")
- execMust(t, e, "INSERT INTO old_sizes VALUES ('10,20'), ('30,40')")
- execMust(t, e, `INSERT INTO sizes (width, height)
- WITH source AS (
- SELECT size FROM old_sizes GROUP BY size
- )
- SELECT
- CAST(substr(size, 1, instr(size, ',') - 1) AS INTEGER),
- CAST(substr(size, instr(size, ',') + 1) AS INTEGER)
- FROM source`)
- res := execMust(t, e, "SELECT width, height FROM sizes ORDER BY width")
- if res.RowCount != 2 || res.Rows[0][0] != int64(10) || res.Rows[1][1] != int64(40) {
- t.Fatalf("migrated sizes = %v", res.Rows)
- }
- }
- // TestGoatCounterDropSizesShape covers 2025-06-21-2-drop-sizes.sql, which adds a
- // column, backfills it with a correlated scalar subquery, drops a column, and
- // drops the source table.
- func TestGoatCounterDropSizesShape(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE sizes (size_id INTEGER PRIMARY KEY, width INTEGER)")
- execMust(t, e, "CREATE TABLE hits (id INTEGER PRIMARY KEY, size_id INTEGER)")
- execMust(t, e, "INSERT INTO sizes VALUES (1, 480), (2, 720)")
- execMust(t, e, "INSERT INTO hits (id, size_id) VALUES (1, 1), (2, 2)")
- execMust(t, e, "ALTER TABLE hits ADD COLUMN width SMALLINT NULL")
- execMust(t, e, "UPDATE hits SET width = (SELECT width FROM sizes WHERE size_id = hits.size_id)")
- execMust(t, e, "ALTER TABLE hits DROP COLUMN size_id")
- execMust(t, e, "DROP TABLE sizes")
- res := execMust(t, e, "SELECT id, width FROM hits ORDER BY id")
- if res.Rows[0][1] != int64(480) || res.Rows[1][1] != int64(720) {
- t.Fatalf("backfilled widths = %v", res.Rows)
- }
- if _, err := execSQL(e, "SELECT size_id FROM hits"); err == nil {
- t.Fatal("size_id should have been dropped")
- }
- }
- func TestGoatCounterDropThenRenameColumnMigration(t *testing.T) {
- _, schema, table := newTestDB(t)
- e := newExec(schema, table)
- execMust(t, e, "CREATE TABLE hit_counts (id INTEGER PRIMARY KEY, total INTEGER, total_unique INTEGER)")
- execMust(t, e, "INSERT INTO hit_counts VALUES (1, 111, 222)")
- execMust(t, e, "ALTER TABLE hit_counts DROP COLUMN total")
- execMust(t, e, "ALTER TABLE hit_counts RENAME COLUMN total_unique TO total")
- res := execMust(t, e, "SELECT total FROM hit_counts WHERE id = 1")
- if res.Rows[0][0] != int64(222) {
- t.Fatalf("renamed total = %v, want 222", res.Rows[0][0])
- }
- }
|