feat(package): migrator: ! sqlite support
This commit is contained in:
@@ -28,6 +28,105 @@ sha256sum() {
|
|||||||
cksum --algorithm=sha256 --untagged "$file" | awk '{printf $1}'
|
cksum --algorithm=sha256 --untagged "$file" | awk '{printf $1}'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# detect_db_type()
|
||||||
|
# Returns: "postgresql" or "sqlite"
|
||||||
|
detect_db_type() {
|
||||||
|
case "$DB_URL" in
|
||||||
|
postgresql://*|postgres://*)
|
||||||
|
printf 'postgresql'
|
||||||
|
;;
|
||||||
|
sqlite://*|*.db|*.sqlite|*.sqlite3)
|
||||||
|
printf 'sqlite'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log error "unsupported database URL format: ${WHITE}$DB_URL${NC}"
|
||||||
|
log error "supported formats: postgresql://... or sqlite://... or *.db"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# get_sqlite_path()
|
||||||
|
get_sqlite_path() {
|
||||||
|
case "$DB_URL" in
|
||||||
|
sqlite://*)
|
||||||
|
printf '%s' "$DB_URL" | sed 's|^sqlite://||'
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf '%s' "$DB_URL"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# db_exec(sql)
|
||||||
|
db_exec() {
|
||||||
|
local sql="$1"
|
||||||
|
local db_type
|
||||||
|
db_type=$(detect_db_type)
|
||||||
|
|
||||||
|
case "$db_type" in
|
||||||
|
postgresql)
|
||||||
|
local psql_args
|
||||||
|
psql_args="$(form_psql_args)"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
printf '%s' "$sql" | psql $psql_args "$DB_URL"
|
||||||
|
;;
|
||||||
|
sqlite)
|
||||||
|
local db_path
|
||||||
|
db_path=$(get_sqlite_path)
|
||||||
|
printf '%s' "$sql" | sqlite3 "$db_path"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# db_query(sql)
|
||||||
|
db_query() {
|
||||||
|
local sql="$1"
|
||||||
|
local db_type
|
||||||
|
db_type=$(detect_db_type)
|
||||||
|
|
||||||
|
case "$db_type" in
|
||||||
|
postgresql)
|
||||||
|
psql "$DB_URL" --no-align --tuples-only --quiet --command "$sql" | awk NF
|
||||||
|
;;
|
||||||
|
sqlite)
|
||||||
|
local db_path
|
||||||
|
db_path=$(get_sqlite_path)
|
||||||
|
sqlite3 "$db_path" "$sql"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# db_exec_file(file_path)
|
||||||
|
db_exec_file() {
|
||||||
|
local file_path="$1"
|
||||||
|
local db_type
|
||||||
|
db_type=$(detect_db_type)
|
||||||
|
|
||||||
|
case "$db_type" in
|
||||||
|
postgresql)
|
||||||
|
local psql_args escaped_path
|
||||||
|
psql_args="$(form_psql_args)"
|
||||||
|
escaped_path=$(printf '%s' "$file_path" | sed "s/'/''/g")
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
psql $psql_args "$DB_URL" <<SQL
|
||||||
|
BEGIN;
|
||||||
|
\i '$escaped_path'
|
||||||
|
COMMIT;
|
||||||
|
SQL
|
||||||
|
;;
|
||||||
|
sqlite)
|
||||||
|
local db_path
|
||||||
|
db_path=$(get_sqlite_path)
|
||||||
|
sqlite3 "$db_path" <<SQL
|
||||||
|
BEGIN;
|
||||||
|
.read $file_path
|
||||||
|
COMMIT;
|
||||||
|
SQL
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# shellcheck disable=SC2120
|
# shellcheck disable=SC2120
|
||||||
init() {
|
init() {
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
@@ -59,9 +158,15 @@ init() {
|
|||||||
|
|
||||||
error_handler_no_db_url
|
error_handler_no_db_url
|
||||||
|
|
||||||
psql_args="$(form_psql_args)"
|
db_type=$(detect_db_type)
|
||||||
|
|
||||||
|
# INHERITS is PostgreSQL-only feature
|
||||||
[ ${INHERITS_LIST+x} ] && {
|
[ ${INHERITS_LIST+x} ] && {
|
||||||
|
if [ "$db_type" != "postgresql" ]; then
|
||||||
|
log error "INHERITS is only supported for PostgreSQL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
oldIFS="$IFS"
|
oldIFS="$IFS"
|
||||||
IFS=','
|
IFS=','
|
||||||
check_inherits=
|
check_inherits=
|
||||||
@@ -75,15 +180,13 @@ init() {
|
|||||||
"$check_inherits" \
|
"$check_inherits" \
|
||||||
'COMMIT;')
|
'COMMIT;')
|
||||||
|
|
||||||
# shellcheck disable=SC2086
|
if ! db_exec "$check_inherits"; then
|
||||||
if ! psql $psql_args -c "$check_inherits"; then
|
|
||||||
log error "init failed: ${WHITE}one of inherits table does not exists: ${CYAN}$INHERITS_LIST"
|
log error "init failed: ${WHITE}one of inherits table does not exists: ${CYAN}$INHERITS_LIST"
|
||||||
exit 5
|
exit 5
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# shellcheck disable=SC2086
|
if ! db_exec "$(init_sql)"; then
|
||||||
if ! psql $psql_args -c "$(init_sql)"; then
|
|
||||||
log error "init failed"
|
log error "init failed"
|
||||||
exit 13
|
exit 13
|
||||||
fi
|
fi
|
||||||
@@ -92,10 +195,11 @@ init() {
|
|||||||
# error_handler_no_db_url()
|
# error_handler_no_db_url()
|
||||||
error_handler_no_db_url() {
|
error_handler_no_db_url() {
|
||||||
[ "${DB_URL+x}" ] || { log error "no ${WHITE}DB_URL${NC} or ${WHITE}--db-url${NC} specified"; exit 3; }
|
[ "${DB_URL+x}" ] || { log error "no ${WHITE}DB_URL${NC} or ${WHITE}--db-url${NC} specified"; exit 3; }
|
||||||
|
check_db_dependencies
|
||||||
}
|
}
|
||||||
|
|
||||||
init_sql() {
|
init_sql_postgresql() {
|
||||||
local sql
|
local sql inherits
|
||||||
|
|
||||||
inherits=
|
inherits=
|
||||||
[ ${INHERITS_LIST+x} ] && inherits="$(printf 'INHERITS(%s)' "$INHERITS_LIST")"
|
[ ${INHERITS_LIST+x} ] && inherits="$(printf 'INHERITS(%s)' "$INHERITS_LIST")"
|
||||||
@@ -103,7 +207,7 @@ init_sql() {
|
|||||||
sql="$(cat <<EOF
|
sql="$(cat <<EOF
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
DO \$$
|
DO \$\$
|
||||||
DECLARE
|
DECLARE
|
||||||
version TEXT;
|
version TEXT;
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -120,18 +224,18 @@ BEGIN
|
|||||||
) THEN
|
) THEN
|
||||||
SELECT hectic.version.version FROM hectic.version WHERE name = 'migrator' INTO version;
|
SELECT hectic.version.version FROM hectic.version WHERE name = 'migrator' INTO version;
|
||||||
IF version != '$VERSION' THEN
|
IF version != '$VERSION' THEN
|
||||||
RAISE EXCEPTION 'Incampetible migrator versions: % and $VERSION', version; -- TODO(yukkop): show versions
|
RAISE EXCEPTION 'Incompatible migrator versions: % and $VERSION', version;
|
||||||
END IF;
|
END IF;
|
||||||
ELSE
|
ELSE
|
||||||
CREATE DOMAIN hectic.migration_name AS TEXT CHECK (VALUE ~ '^[0-9]{14}-.*');
|
CREATE DOMAIN hectic.migration_name AS TEXT CHECK (VALUE ~ '^[0-9]{14}-.*');
|
||||||
CREATE DOMAIN hectic.sha256 AS CHAR(64) CHECK (VALUE ~ '^[0-9a-f]{64}$');
|
CREATE DOMAIN hectic.sha256 AS CHAR(64) CHECK (VALUE ~ '^[0-9a-f]{64}\$');
|
||||||
|
|
||||||
CREATE FUNCTION hectic.sha256_lower() RETURNS trigger AS \$fn$
|
CREATE FUNCTION hectic.sha256_lower() RETURNS trigger AS \$fn\$
|
||||||
BEGIN
|
BEGIN
|
||||||
NEW.hash = lower(NEW.hash);
|
NEW.hash = lower(NEW.hash);
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
\$fn$ LANGUAGE plpgsql;
|
\$fn\$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
CREATE TABLE hectic.version (
|
CREATE TABLE hectic.version (
|
||||||
name TEXT PRIMARY KEY,
|
name TEXT PRIMARY KEY,
|
||||||
@@ -153,7 +257,7 @@ BEGIN
|
|||||||
FOR EACH ROW EXECUTE FUNCTION hectic.sha256_lower();
|
FOR EACH ROW EXECUTE FUNCTION hectic.sha256_lower();
|
||||||
END IF;
|
END IF;
|
||||||
END;
|
END;
|
||||||
\$$;
|
\$\$;
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
EOF
|
EOF
|
||||||
@@ -162,6 +266,59 @@ EOF
|
|||||||
printf '%s' "$sql"
|
printf '%s' "$sql"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init_sql_sqlite() {
|
||||||
|
local sql
|
||||||
|
|
||||||
|
sql="$(cat <<'EOF'
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS hectic_version (
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
version TEXT NOT NULL,
|
||||||
|
installed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS hectic_migration (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT UNIQUE NOT NULL CHECK (name GLOB '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-*'),
|
||||||
|
hash TEXT UNIQUE NOT NULL CHECK (length(hash) = 64 AND lower(hash) = hash),
|
||||||
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Check version compatibility
|
||||||
|
INSERT OR IGNORE INTO hectic_version (name, version) VALUES ('migrator', 'VERSION_PLACEHOLDER');
|
||||||
|
|
||||||
|
-- Verify version if it already exists
|
||||||
|
SELECT CASE
|
||||||
|
WHEN version != 'VERSION_PLACEHOLDER' AND name = 'migrator'
|
||||||
|
THEN RAISE(ABORT, 'Incompatible migrator versions')
|
||||||
|
ELSE 1
|
||||||
|
END FROM hectic_version WHERE name = 'migrator';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
|
||||||
|
# Replace version placeholder
|
||||||
|
sql=$(printf '%s' "$sql" | sed "s/VERSION_PLACEHOLDER/$VERSION/g")
|
||||||
|
|
||||||
|
printf '%s' "$sql"
|
||||||
|
}
|
||||||
|
|
||||||
|
init_sql() {
|
||||||
|
local db_type
|
||||||
|
db_type=$(detect_db_type)
|
||||||
|
|
||||||
|
case "$db_type" in
|
||||||
|
postgresql)
|
||||||
|
init_sql_postgresql
|
||||||
|
;;
|
||||||
|
sqlite)
|
||||||
|
init_sql_sqlite
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
help() {
|
help() {
|
||||||
# inherits: List one or more tables the migration table must inherit from
|
# inherits: List one or more tables the migration table must inherit from
|
||||||
echo help
|
echo help
|
||||||
@@ -334,11 +491,16 @@ migrate() {
|
|||||||
|
|
||||||
fs_migrations=$(migration_list)
|
fs_migrations=$(migration_list)
|
||||||
|
|
||||||
db_migrations=$(
|
db_type=$(detect_db_type)
|
||||||
psql "$DB_URL" --no-align --tuples-only --quiet \
|
|
||||||
--command "SELECT name FROM hectic.migration ORDER BY name ASC" \
|
case "$db_type" in
|
||||||
| awk NF
|
postgresql)
|
||||||
)
|
db_migrations=$(db_query "SELECT name FROM hectic.migration ORDER BY name ASC")
|
||||||
|
;;
|
||||||
|
sqlite)
|
||||||
|
db_migrations=$(db_query "SELECT name FROM hectic_migration ORDER BY name ASC")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
log debug "db mig: $db_migrations"
|
log debug "db mig: $db_migrations"
|
||||||
db_mig_count=$(printf '%s' "$db_migrations" | wc -l)
|
db_mig_count=$(printf '%s' "$db_migrations" | wc -l)
|
||||||
@@ -415,6 +577,10 @@ migrate() {
|
|||||||
mig_hash=$(sha256sum "$mig_path")
|
mig_hash=$(sha256sum "$mig_path")
|
||||||
log info "applying migration ${WHITE}$fs_migration${NC} (up)"
|
log info "applying migration ${WHITE}$fs_migration${NC} (up)"
|
||||||
|
|
||||||
|
case "$db_type" in
|
||||||
|
postgresql)
|
||||||
|
local psql_args
|
||||||
|
psql_args="$(form_psql_args)"
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
if ! psql $psql_args "$DB_URL" <<SQL
|
if ! psql $psql_args "$DB_URL" <<SQL
|
||||||
BEGIN;
|
BEGIN;
|
||||||
@@ -426,6 +592,22 @@ SQL
|
|||||||
log error "migration failed: ${WHITE}$fs_migration${NC}"
|
log error "migration failed: ${WHITE}$fs_migration${NC}"
|
||||||
exit 4
|
exit 4
|
||||||
fi
|
fi
|
||||||
|
;;
|
||||||
|
sqlite)
|
||||||
|
local db_path
|
||||||
|
db_path=$(get_sqlite_path)
|
||||||
|
if ! sqlite3 "$db_path" <<SQL
|
||||||
|
BEGIN;
|
||||||
|
.read $mig_path
|
||||||
|
INSERT INTO hectic_migration (name, hash) VALUES ('$escaped_name', '$mig_hash');
|
||||||
|
COMMIT;
|
||||||
|
SQL
|
||||||
|
then
|
||||||
|
log error "migration failed: ${WHITE}$fs_migration${NC}"
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
i=$((i + 1))
|
i=$((i + 1))
|
||||||
done
|
done
|
||||||
@@ -451,6 +633,10 @@ SQL
|
|||||||
|
|
||||||
log info "reverting migration ${WHITE}$fs_migration${NC} (down)"
|
log info "reverting migration ${WHITE}$fs_migration${NC} (down)"
|
||||||
|
|
||||||
|
case "$db_type" in
|
||||||
|
postgresql)
|
||||||
|
local psql_args
|
||||||
|
psql_args="$(form_psql_args)"
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
if ! psql $psql_args "$DB_URL" <<SQL
|
if ! psql $psql_args "$DB_URL" <<SQL
|
||||||
BEGIN;
|
BEGIN;
|
||||||
@@ -462,6 +648,22 @@ SQL
|
|||||||
log error "migration rollback failed: ${WHITE}$fs_migration${NC}"
|
log error "migration rollback failed: ${WHITE}$fs_migration${NC}"
|
||||||
exit 4
|
exit 4
|
||||||
fi
|
fi
|
||||||
|
;;
|
||||||
|
sqlite)
|
||||||
|
local db_path
|
||||||
|
db_path=$(get_sqlite_path)
|
||||||
|
if ! sqlite3 "$db_path" <<SQL
|
||||||
|
BEGIN;
|
||||||
|
.read $mig_path
|
||||||
|
DELETE FROM hectic_migration WHERE name = '$escaped_name';
|
||||||
|
COMMIT;
|
||||||
|
SQL
|
||||||
|
then
|
||||||
|
log error "migration rollback failed: ${WHITE}$fs_migration${NC}"
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
i=$((i - 1))
|
i=$((i - 1))
|
||||||
done
|
done
|
||||||
@@ -506,10 +708,11 @@ SQL
|
|||||||
}
|
}
|
||||||
|
|
||||||
form_psql_args() {
|
form_psql_args() {
|
||||||
psql_args="-d $DB_URL -v ON_ERROR_STOP=1"
|
local psql_args="-v ON_ERROR_STOP=1"
|
||||||
for var in ${VARIABLE_LIST:-}; do
|
for var in ${VARIABLE_LIST:-}; do
|
||||||
psql_args="$psql_args -v $var"
|
psql_args="$psql_args -v $var"
|
||||||
done
|
done
|
||||||
|
printf '%s' "$psql_args"
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
@@ -609,10 +812,28 @@ generate_word() {
|
|||||||
printf '%s' "$w"
|
printf '%s' "$w"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_db_dependencies() {
|
||||||
|
[ "${DB_URL+x}" ] || return 0 # Skip if no DB_URL yet
|
||||||
|
|
||||||
|
db_type=$(detect_db_type)
|
||||||
|
|
||||||
|
case "$db_type" in
|
||||||
|
postgresql)
|
||||||
if ! command -v psql >/dev/null; then
|
if ! command -v psql >/dev/null; then
|
||||||
log error "Required tool (psql) are not installed."
|
log error "Required tool (psql) is not installed."
|
||||||
|
log error "PostgreSQL client tools are required for postgresql:// URLs"
|
||||||
exit 127
|
exit 127
|
||||||
fi
|
fi
|
||||||
|
;;
|
||||||
|
sqlite)
|
||||||
|
if ! command -v sqlite3 >/dev/null; then
|
||||||
|
log error "Required tool (sqlite3) is not installed."
|
||||||
|
log error "SQLite3 client is required for sqlite:// URLs"
|
||||||
|
exit 127
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
if ! [ "${AS_LIBRARY+x}" ]; then
|
if ! [ "${AS_LIBRARY+x}" ]; then
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
|
|||||||
164
test/package/migrator/README.md
Normal file
164
test/package/migrator/README.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Migrator Test Suite
|
||||||
|
|
||||||
|
This directory contains comprehensive tests for the database migration tool supporting both PostgreSQL and SQLite.
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
test/package/migrator/
|
||||||
|
├── default.nix # Nix test builder - auto-detects test type
|
||||||
|
├── lauch.sh # PostgreSQL test launcher
|
||||||
|
├── lauch-sqlite.sh # SQLite test launcher
|
||||||
|
├── util.sh # Shared test utilities
|
||||||
|
└── test/ # Test cases
|
||||||
|
├── <test-name>/ # PostgreSQL tests (default)
|
||||||
|
└── sqlite-<name>/ # SQLite tests (prefix with "sqlite-")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Types
|
||||||
|
|
||||||
|
### PostgreSQL Tests (Default)
|
||||||
|
|
||||||
|
Any test directory or `.sh` file in `test/` will use PostgreSQL by default:
|
||||||
|
- Automatic PostgreSQL setup (initdb, pg_ctl, createdb)
|
||||||
|
- `DATABASE_URL` set to PostgreSQL connection string
|
||||||
|
- Requires: `pkgs.postgresql`
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
- `migrate-up-single/`
|
||||||
|
- `migrate-down-multiple/`
|
||||||
|
- `init-migrator.sh`
|
||||||
|
|
||||||
|
### SQLite Tests
|
||||||
|
|
||||||
|
Tests with names starting with `sqlite-` use SQLite:
|
||||||
|
- Simple file-based database
|
||||||
|
- `DATABASE_URL` set to `sqlite:///path/to/test.db`
|
||||||
|
- Requires: `pkgs.sqlite`
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
- `sqlite-basic/`
|
||||||
|
- `sqlite-migration-test/`
|
||||||
|
|
||||||
|
## Test Categories
|
||||||
|
|
||||||
|
### Core Functionality
|
||||||
|
- `init-migrator.sh` - Initialization
|
||||||
|
- `init-migrator-with-inherits.sh` - PostgreSQL INHERITS feature
|
||||||
|
- `migrate-up-single/` - Single step up migration
|
||||||
|
- `migrate-up-multiple/` - Multiple step up migrations
|
||||||
|
- `migrate-down-single/` - Single step down migration
|
||||||
|
- `migrate-down-multiple/` - Multiple step down migrations
|
||||||
|
- `migrate-to-forward/` - Migrate to specific version (forward)
|
||||||
|
- `migrate-to-backward/` - Migrate to specific version (backward)
|
||||||
|
- `migrate-already-at-target/` - Edge case: no-op migration
|
||||||
|
|
||||||
|
### Existing Database Support
|
||||||
|
- `migrate-existing-database/` - Add migrator to production DB
|
||||||
|
- `migrate-existing-with-conflicts/` - Handle schema conflicts
|
||||||
|
- `migrate-existing-data-migration/` - Transform existing data
|
||||||
|
|
||||||
|
### SQLite Support
|
||||||
|
- `sqlite-basic/` - Basic SQLite functionality
|
||||||
|
|
||||||
|
### Helper Functions
|
||||||
|
- `function-index-of.sh` - Test index_of helper
|
||||||
|
- `function-migration-list.sh` - Test migration_list helper
|
||||||
|
- `function-generate-word.sh` - Test word generator
|
||||||
|
|
||||||
|
### Utilities
|
||||||
|
- `create-migration.sh` - Test migration creation
|
||||||
|
- `migrations-list/` - Test migration listing
|
||||||
|
- `arguments.sh` - Test argument parsing
|
||||||
|
|
||||||
|
## Creating New Tests
|
||||||
|
|
||||||
|
### PostgreSQL Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p test/<test-name>/migration/<timestamp>-<name>
|
||||||
|
cat > test/<test-name>/run.sh <<'EOF'
|
||||||
|
#!/bin/dash
|
||||||
|
HECTIC_NAMESPACE=test-my-test
|
||||||
|
log notice "test case: ${WHITE}my test"
|
||||||
|
|
||||||
|
# $DATABASE_URL is automatically set to PostgreSQL
|
||||||
|
migrator --db-url "$DATABASE_URL" init
|
||||||
|
# ... your test code ...
|
||||||
|
|
||||||
|
log notice "test passed"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create up.sql and down.sql migration files
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQLite Test
|
||||||
|
|
||||||
|
Same as above, but prefix the directory name with `sqlite-`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p test/sqlite-<test-name>/migration/<timestamp>-<name>
|
||||||
|
# ... rest is the same
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
Tests are built and run via Nix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
nix build .#checks.x86_64-linux
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
nix build .#checks.x86_64-linux.migrator-test-<test-name>
|
||||||
|
|
||||||
|
# Run SQLite tests
|
||||||
|
nix build .#checks.x86_64-linux.migrator-test-sqlite-basic
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Isolation
|
||||||
|
|
||||||
|
Each test runs in complete isolation:
|
||||||
|
- **PostgreSQL**: Fresh PostgreSQL cluster per test
|
||||||
|
- **SQLite**: Fresh database file per test
|
||||||
|
- Clean working directory
|
||||||
|
- Independent environment variables
|
||||||
|
|
||||||
|
## Available Test Utilities
|
||||||
|
|
||||||
|
From `util.sh`:
|
||||||
|
- `columns(table)` - Get column names from table
|
||||||
|
- `is_number(var)` - Check if variable is numeric
|
||||||
|
|
||||||
|
From test environment:
|
||||||
|
- `log <level> <message>` - Logging (trace, debug, info, notice, error)
|
||||||
|
- `migrator` - The migrator binary under test
|
||||||
|
- `$DATABASE_URL` - Database connection string (auto-configured)
|
||||||
|
|
||||||
|
## Test Conventions
|
||||||
|
|
||||||
|
1. **Naming**: Use descriptive names with hyphens
|
||||||
|
2. **Logging**: Use `log` for output, not `echo`
|
||||||
|
3. **Exit codes**:
|
||||||
|
- 0 = success
|
||||||
|
- 1 = test failure
|
||||||
|
- Other = specific error conditions
|
||||||
|
4. **Cleanup**: Tests are automatically cleaned up by Nix
|
||||||
|
5. **Assertions**: Explicit checks with meaningful error messages
|
||||||
|
|
||||||
|
## Database-Specific Notes
|
||||||
|
|
||||||
|
### PostgreSQL
|
||||||
|
- Full schema support (`hectic.migration`)
|
||||||
|
- Domains with regex validation
|
||||||
|
- Triggers and functions
|
||||||
|
- INHERITS support
|
||||||
|
- TIMESTAMPTZ support
|
||||||
|
|
||||||
|
### SQLite
|
||||||
|
- Simple table names (`hectic_migration`)
|
||||||
|
- CHECK constraints instead of domains
|
||||||
|
- No triggers needed
|
||||||
|
- TEXT timestamps with datetime()
|
||||||
|
- Table recreation for column removal (older SQLite versions)
|
||||||
|
|
||||||
@@ -1,37 +1,41 @@
|
|||||||
{ inputs, self, pkgs, system, ... }: let
|
{ inputs, self, pkgs, system, ... }: let
|
||||||
lib = inputs.nixpkgs.lib;
|
lib = inputs.nixpkgs.lib;
|
||||||
|
|
||||||
# turn anything under ./test into a derivation that exposes $out/run.sh
|
# turn anything under test directory into a derivation that exposes $out/run.sh
|
||||||
mkTestDrv = name: type:
|
mkTestDrv = folder: name: type:
|
||||||
if type == "directory" then
|
if type == "directory" then
|
||||||
pkgs.runCommand "test-${name}" {} ''
|
pkgs.runCommand "test-${name}" {} ''
|
||||||
if ! [ -f ${./test + "/${name}" + /run.sh} ]; then
|
if ! [ -f ${"${folder}/${name}/run.sh"} ]; then
|
||||||
echo no run.sh in test/${name}
|
echo no run.sh in test/${name}
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
cp -r ${./test + "/${name}"}/* "$out/"
|
cp -r ${"${folder}/${name}"}/* "$out/"
|
||||||
chmod +x "$out/run.sh"
|
chmod +x "$out/run.sh"
|
||||||
''
|
''
|
||||||
else if lib.hasSuffix ".sh" name then
|
else if lib.hasSuffix ".sh" name then
|
||||||
pkgs.runCommand "test-${lib.removeSuffix ".sh" name}" {} ''
|
pkgs.runCommand "test-${lib.removeSuffix ".sh" name}" {} ''
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
install -Dm755 ${./test + "/${name}"} "$out/run.sh"
|
install -Dm755 ${"${folder}/${name}"} "$out/run.sh"
|
||||||
''
|
''
|
||||||
else
|
else
|
||||||
null;
|
null;
|
||||||
|
|
||||||
testDir = builtins.readDir ./test;
|
testDir = folder: builtins.readDir folder;
|
||||||
|
|
||||||
# attrset: testName -> drv with run.sh
|
# attrset: testName -> drv with run.sh
|
||||||
testDrvs =
|
testDrvs = folder:
|
||||||
lib.mapAttrs' (n: v:
|
lib.mapAttrs' (n: v:
|
||||||
lib.nameValuePair (lib.removeSuffix ".sh" n) v
|
lib.nameValuePair (lib.removeSuffix ".sh" n) v
|
||||||
) (lib.filterAttrs (_: v: v != null)
|
) (lib.filterAttrs (_: v: v != null)
|
||||||
(lib.mapAttrs (n: t: mkTestDrv n t) testDir));
|
(lib.mapAttrs (n: t: mkTestDrv folder n t) (testDir folder)));
|
||||||
|
|
||||||
|
postgresqlTestDrvs = testDrvs ./test/postgresql;
|
||||||
|
sqliteTestDrvs = testDrvs ./test/sqlite;
|
||||||
|
|
||||||
migrator = self.packages.${system}.migrator;
|
migrator = self.packages.${system}.migrator;
|
||||||
|
|
||||||
mkPgTest = testName: testDrv: pkgs.runCommand "migrator-test-${testName}"
|
mkPgTest = testName: testDrv: pkgs.runCommand "migrator-test-${testName}"
|
||||||
{
|
{
|
||||||
nativeBuildInputs = [ pkgs.coreutils pkgs.gnugrep pkgs.gnused ];
|
nativeBuildInputs = [ pkgs.coreutils pkgs.gnugrep pkgs.gnused ];
|
||||||
@@ -41,10 +45,26 @@
|
|||||||
test=${testDrv}
|
test=${testDrv}
|
||||||
export HECTIC_LOG=trace
|
export HECTIC_LOG=trace
|
||||||
${builtins.readFile ./util.sh}
|
${builtins.readFile ./util.sh}
|
||||||
${builtins.readFile ./lauch.sh}
|
${builtins.readFile ./lauch-postgresql.sh}
|
||||||
|
|
||||||
# success marker for Nix
|
# success marker for Nix
|
||||||
# shellcheck disable=SC2154
|
# shellcheck disable=SC2154
|
||||||
mkdir -p "$out"
|
mkdir -p "$out"
|
||||||
'';
|
'';
|
||||||
in lib.mapAttrs (name: drv: mkPgTest name drv) testDrvs
|
|
||||||
|
mkSqliteTest = testName: testDrv: pkgs.runCommand "migrator-test-${testName}"
|
||||||
|
{
|
||||||
|
nativeBuildInputs = [ pkgs.coreutils pkgs.gnugrep pkgs.gnused ];
|
||||||
|
buildInputs = [ pkgs.which migrator pkgs.sqlite ];
|
||||||
|
} ''
|
||||||
|
${builtins.readFile self.legacyPackages.${system}.helpers.posix-shell.log}
|
||||||
|
test=${testDrv}
|
||||||
|
export HECTIC_LOG=trace
|
||||||
|
${builtins.readFile ./util.sh}
|
||||||
|
${builtins.readFile ./lauch-sqlite.sh}
|
||||||
|
|
||||||
|
# success marker for Nix
|
||||||
|
# shellcheck disable=SC2154
|
||||||
|
mkdir -p "$out"
|
||||||
|
'';
|
||||||
|
in (lib.mapAttrs (name: drv: mkPgTest name drv) postgresqlTestDrvs) // (lib.mapAttrs (name: drv: mkSqliteTest name drv) sqliteTestDrvs)
|
||||||
|
|||||||
@@ -6,13 +6,12 @@
|
|||||||
HECTIC_NAMESPACE=test-laucher
|
HECTIC_NAMESPACE=test-laucher
|
||||||
export HECTIC_LOG=trace
|
export HECTIC_LOG=trace
|
||||||
|
|
||||||
|
# shellcheck disable=SC2154
|
||||||
test_derivation="$(basename "$test")"
|
test_derivation="$(basename "$test")"
|
||||||
test_name="${test_derivation#*-*-}"
|
test_name="${test_derivation#*-*-}"
|
||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
root_dir="$(dirname $0)"
|
|
||||||
|
|
||||||
HECTIC_LOG=
|
HECTIC_LOG=
|
||||||
|
|
||||||
# save path to pg_ctl in case $PATH will change
|
# save path to pg_ctl in case $PATH will change
|
||||||
@@ -65,8 +64,10 @@ log info "run test ${WHITE}${test_name}${NC}"
|
|||||||
mkdir './test'
|
mkdir './test'
|
||||||
cp -r "$test"/* './test/'
|
cp -r "$test"/* './test/'
|
||||||
cd './test'
|
cd './test'
|
||||||
|
# shellcheck disable=SC1091
|
||||||
. './run.sh'
|
. './run.sh'
|
||||||
|
|
||||||
|
# shellcheck disable=SC2034
|
||||||
HECTIC_NAMESPACE=test-laucher
|
HECTIC_NAMESPACE=test-laucher
|
||||||
|
|
||||||
log info "finish test pipeline"
|
log info "finish test pipeline"
|
||||||
45
test/package/migrator/lauch-sqlite.sh
Normal file
45
test/package/migrator/lauch-sqlite.sh
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/dash
|
||||||
|
|
||||||
|
# $out - nix derivation output
|
||||||
|
# $test - test and assertion file
|
||||||
|
|
||||||
|
HECTIC_NAMESPACE=test-laucher
|
||||||
|
export HECTIC_LOG=trace
|
||||||
|
|
||||||
|
# shellcheck disable=SC2154
|
||||||
|
test_derivation="$(basename "$test")"
|
||||||
|
test_name="${test_derivation#*-*-}"
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
HECTIC_LOG=
|
||||||
|
|
||||||
|
log info 'start test pipeline (SQLite)'
|
||||||
|
|
||||||
|
# temp dirs
|
||||||
|
wd="$PWD"
|
||||||
|
db_file="$wd/test.db"
|
||||||
|
|
||||||
|
# Set up SQLite database URL
|
||||||
|
DATABASE_URL="sqlite://$db_file"
|
||||||
|
export DATABASE_URL
|
||||||
|
|
||||||
|
log info "using SQLite database: $db_file"
|
||||||
|
log info "run test ${WHITE}${test_name}${NC}"
|
||||||
|
|
||||||
|
# run test
|
||||||
|
mkdir './test'
|
||||||
|
cp -r "$test"/* './test/'
|
||||||
|
cd './test'
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. './run.sh'
|
||||||
|
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
HECTIC_NAMESPACE=test-laucher
|
||||||
|
|
||||||
|
log info "finish test pipeline"
|
||||||
|
|
||||||
|
# success marker for Nix
|
||||||
|
# shellcheck disable=SC2154
|
||||||
|
mkdir -p "$out"
|
||||||
|
|
||||||
@@ -2,18 +2,6 @@
|
|||||||
|
|
||||||
HECTIC_NAMESPACE=test-init-migrator
|
HECTIC_NAMESPACE=test-init-migrator
|
||||||
|
|
||||||
### CASE 1
|
|
||||||
log notice "test case: ${WHITE}dry run"
|
|
||||||
# NOTE: does not matter exist inherits tables or not, it must not connect to db
|
|
||||||
|
|
||||||
if ! migration_table_sql="$(migrator --inherits tablename --inherits 'table name' init --dry-run)"; then
|
|
||||||
log error "test failed: ${WHITE}error on migration table init dry run"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s' "$migration_table_sql" | grep -Eq 'INHERITS[[:space:]]*\([[:space:]]*"tablename"[[:space:]]*,[[:space:]]*"table name"[[:space:]]*\)' ||
|
|
||||||
{ log error "test failed: ${WHITE}not correct migration table inherits"; exit 1; }
|
|
||||||
|
|
||||||
### CASE 2
|
### CASE 2
|
||||||
log notice "test case: ${WHITE}error: table inherit tables that not exists"
|
log notice "test case: ${WHITE}error: table inherit tables that not exists"
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
DROP TABLE users;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- SQLite doesn't support DROP COLUMN directly before 3.35.0
|
||||||
|
-- We need to recreate the table
|
||||||
|
CREATE TABLE users_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO users_new (id, name) SELECT id, name FROM users;
|
||||||
|
DROP TABLE users;
|
||||||
|
ALTER TABLE users_new RENAME TO users;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN email TEXT;
|
||||||
|
|
||||||
|
|
||||||
86
test/package/migrator/test/sqlite/sqlite-basic/run.sh
Normal file
86
test/package/migrator/test/sqlite/sqlite-basic/run.sh
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/dash
|
||||||
|
|
||||||
|
HECTIC_NAMESPACE=test-sqlite-basic
|
||||||
|
|
||||||
|
log notice "test case: ${WHITE}SQLite basic migration"
|
||||||
|
|
||||||
|
# Create SQLite database
|
||||||
|
SQLITE_DB="$PWD/test.db"
|
||||||
|
export DB_URL="sqlite://$SQLITE_DB"
|
||||||
|
|
||||||
|
log info "using SQLite database: $SQLITE_DB"
|
||||||
|
|
||||||
|
# Initialize migrator with SQLite
|
||||||
|
if ! migrator --db-url "$DB_URL" init; then
|
||||||
|
log error "test failed: ${WHITE}init failed for SQLite"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify tables were created
|
||||||
|
if ! sqlite3 "$SQLITE_DB" "SELECT name FROM hectic_version WHERE name = 'migrator'" >/dev/null 2>&1; then
|
||||||
|
log error "test failed: ${WHITE}hectic_version table not created"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! sqlite3 "$SQLITE_DB" "SELECT COUNT(*) FROM hectic_migration" >/dev/null 2>&1; then
|
||||||
|
log error "test failed: ${WHITE}hectic_migration table not created"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log info "migrator tables created successfully"
|
||||||
|
|
||||||
|
# Apply first migration
|
||||||
|
if ! migrator --db-url "$DB_URL" migrate up; then
|
||||||
|
log error "test failed: ${WHITE}first migration failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify migration was applied
|
||||||
|
migration_count=$(sqlite3 "$SQLITE_DB" "SELECT COUNT(*) FROM hectic_migration")
|
||||||
|
if [ "$migration_count" != "1" ]; then
|
||||||
|
log error "test failed: ${WHITE}expected 1 migration, got $migration_count"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify table was created
|
||||||
|
if ! sqlite3 "$SQLITE_DB" "SELECT COUNT(*) FROM users" >/dev/null 2>&1; then
|
||||||
|
log error "test failed: ${WHITE}users table not created"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log info "first migration applied successfully"
|
||||||
|
|
||||||
|
# Apply second migration
|
||||||
|
if ! migrator --db-url "$DB_URL" migrate up; then
|
||||||
|
log error "test failed: ${WHITE}second migration failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify email column exists
|
||||||
|
if ! sqlite3 "$SQLITE_DB" "SELECT email FROM users LIMIT 0" >/dev/null 2>&1; then
|
||||||
|
log error "test failed: ${WHITE}email column not added"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Migrate down
|
||||||
|
if ! migrator --db-url "$DB_URL" migrate down; then
|
||||||
|
log error "test failed: ${WHITE}migrate down failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify only 1 migration remains
|
||||||
|
migration_count=$(sqlite3 "$SQLITE_DB" "SELECT COUNT(*) FROM hectic_migration")
|
||||||
|
if [ "$migration_count" != "1" ]; then
|
||||||
|
log error "test failed: ${WHITE}expected 1 migration after down, got $migration_count"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify email column removed
|
||||||
|
if sqlite3 "$SQLITE_DB" "SELECT email FROM users LIMIT 0" >/dev/null 2>&1; then
|
||||||
|
log error "test failed: ${WHITE}email column should be removed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log notice "test passed: SQLite support works correctly"
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user