fix(sql): fix sql script

This commit is contained in:
2026-01-31 11:11:55 +08:00
parent ce12b997f4
commit d071e1a27d
32 changed files with 1687 additions and 133 deletions

99
scripts/db/README.md Normal file
View File

@@ -0,0 +1,99 @@
# 数据库脚本(版本化迁移)
## 目录结构
- `migrations/`:按版本顺序的升级脚本(只增不改)
- `rollback/`:与 migration 一一对应的回滚脚本(按版本号匹配)
- `migrate.sh`:按顺序执行未应用的 migrations
- `rollback.sh`:按版本回滚(需要显式确认)
- `verify.sh`:执行校验脚本,确认表结构与种子数据满足要求
- `reset.sh`:开发/测试用清库重置(会删除所有 IAM 表与迁移记录,需显式确认)
- `rebuild_iam_db.sh`:开发环境一键重建(会清库重建,不适合生产)
## 版本与执行顺序
| 版本 | 脚本 | 说明 |
|---|---|---|
| 0001 | `migrations/0001_core.sql` | IAM 核心表tenant/user/role/permission 等)与基础种子数据 |
| 0002 | `migrations/0002_enabled_apps.sql` | enabled_apps租户应用开通、平台租户与平台权限SuperAdmin |
校验脚本映射(与 migrations 一一对应):
| 版本 | 校验脚本 | 说明 |
|---|---|---|
| 0001 | `scripts/db/verify/0001_core.sql` | 校验核心表、索引与基础种子 |
| 0002 | `scripts/db/verify/0002_enabled_apps.sql` | 校验 enabled_apps 相关表与平台种子 |
## 执行方式
环境变量:
- `DATABASE_URL`:必填(也可放在项目根目录 `.env` 中)
依赖:
- 需要安装 PostgreSQL 客户端工具(`psql`);可选 `pg_dump`(仅当你要做备份)。
执行迁移:
```bash
./scripts/db/migrate.sh
./scripts/db/verify.sh
```
开发/测试环境清库重置(危险):
```bash
CONFIRM=1 ./scripts/db/reset.sh
```
回滚到指定版本(示例:回滚到 0001
```bash
CONFIRM=1 TARGET_VERSION=0001 ./scripts/db/rollback.sh
./scripts/db/verify.sh
```
验证步骤
- 执行 `./scripts/db/verify.sh`,应无错误退出
- 启动服务后通过 Scalar 访问 `/scalar`,检查 OpenAPI 中包含:
- `/platform/tenants/{tenant_id}/enabled-apps`
- `/users/{id}/roles`
## 首次环境准备DB/User
在具有足够权限的数据库账号下执行(示例):
```sql
CREATE USER iam_service_user WITH PASSWORD 'iam_service_password';
CREATE DATABASE iam_service_db OWNER iam_service_user;
GRANT ALL PRIVILEGES ON DATABASE iam_service_db TO iam_service_user;
```
说明:
- 生产环境不要在仓库内硬编码密码;应由密钥管理系统注入并轮换。
- 如需启用扩展(如 `uuid-ossp`),请确认应用账号是否有权限,或由 DBA 预先安装。
## 常见问题
### `.env` 配了 DATABASE_URL 但脚本报 “DATABASE_URL is required”
`.env` 只是文件不会自动变成进程环境变量。Rust 程序会通过 `dotenvy` 读取 `.env`,但 bash 脚本默认不会。
解决方式:
- 直接 `export DATABASE_URL=...` 后再执行脚本;或
- 保持项目根目录存在 `.env`,脚本会尝试读取其中的 `DATABASE_URL`
### 执行脚本报 “psql not found”
原因:系统未安装 PostgreSQL 客户端工具(`psql`/`pg_dump`),或不在 `PATH` 中。
安装方式:
- Ubuntu/Debian`sudo apt-get update && sudo apt-get install -y postgresql-client`
- RHEL/CentOS/Fedora`sudo dnf install -y postgresql`
- Alpine`sudo apk add postgresql-client`
- macOSHomebrew`brew install libpq && brew link --force libpq`
验证方式:
```bash
psql --version
```

74
scripts/db/migrate.sh Normal file
View File

@@ -0,0 +1,74 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
load_database_url_from_env_file() {
local env_file="$1"
local line value
while IFS= read -r line || [[ -n "${line}" ]]; do
line="${line#"${line%%[![:space:]]*}"}"
[[ -z "${line}" || "${line}" == \#* ]] && continue
line="${line#export }"
if [[ "${line}" == DATABASE_URL=* ]]; then
value="${line#DATABASE_URL=}"
value="${value%$'\r'}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
printf '%s' "${value}"
return 0
fi
done < "${env_file}"
return 1
}
DATABASE_URL="${DATABASE_URL:-}"
if [[ -z "${DATABASE_URL}" && -f "${ROOT_DIR}/.env" ]]; then
DATABASE_URL="$(load_database_url_from_env_file "${ROOT_DIR}/.env" || true)"
fi
if [[ -z "${DATABASE_URL}" ]]; then
echo "DATABASE_URL is required (export it, or set it in ${ROOT_DIR}/.env)"
exit 1
fi
if ! command -v psql >/dev/null 2>&1; then
echo "psql not found in PATH"
exit 127
fi
TARGET_VERSION="${TARGET_VERSION:-}"
DRY_RUN="${DRY_RUN:-0}"
psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c "CREATE TABLE IF NOT EXISTS iam_schema_migrations (version TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW())"
shopt -s nullglob
migrations=( "${SCRIPT_DIR}/migrations/"*.sql )
if [[ ${#migrations[@]} -eq 0 ]]; then
echo "No migration files found: ${SCRIPT_DIR}/migrations/*.sql"
exit 1
fi
for file in "${migrations[@]}"; do
base="$(basename "${file}")"
version="${base%%_*}"
if [[ -n "${TARGET_VERSION}" && "${version}" > "${TARGET_VERSION}" ]]; then
continue
fi
applied="$(psql "${DATABASE_URL}" -At -c "SELECT 1 FROM iam_schema_migrations WHERE version='${version}' LIMIT 1" || true)"
if [[ "${applied}" == "1" ]]; then
continue
fi
echo "Applying ${version} (${base})"
if [[ "${DRY_RUN}" == "1" ]]; then
continue
fi
psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${file}"
psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c "INSERT INTO iam_schema_migrations(version) VALUES ('${version}') ON CONFLICT DO NOTHING"
done
echo "Migrations completed"

View File

@@ -0,0 +1,117 @@
BEGIN;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE IF NOT EXISTS tenants (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
status VARCHAR(50) DEFAULT 'active',
config JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(100),
phone_number VARCHAR(20),
is_active BOOLEAN DEFAULT TRUE,
is_verified BOOLEAN DEFAULT FALSE,
mfa_enabled BOOLEAN DEFAULT FALSE,
mfa_secret VARCHAR(255),
last_login_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_tenant_email ON users(tenant_id, email);
CREATE TABLE IF NOT EXISTS roles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
name VARCHAR(50) NOT NULL,
description TEXT,
is_system BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(tenant_id, name)
);
CREATE TABLE IF NOT EXISTS permissions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
code VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
resource VARCHAR(50),
action VARCHAR(50),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS role_permissions (
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (role_id, permission_id)
);
CREATE TABLE IF NOT EXISTS user_roles (
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
PRIMARY KEY (user_id, role_id)
);
CREATE TABLE IF NOT EXISTS refresh_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
is_revoked BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
replaced_by_token_hash VARCHAR(255)
);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID REFERENCES tenants(id),
user_id UUID,
action VARCHAR(100) NOT NULL,
resource VARCHAR(100),
ip_address VARCHAR(45),
user_agent TEXT,
status VARCHAR(20) NOT NULL,
details JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
INSERT INTO tenants (id, name)
VALUES ('11111111-1111-1111-1111-111111111111', 'Default Corp')
ON CONFLICT (id) DO NOTHING;
INSERT INTO permissions (code, description, resource, action) VALUES
('user:read', 'View users', 'user', 'read'),
('user:write', 'Create/Update/Delete users', 'user', 'write'),
('role:read', 'View roles', 'role', 'read'),
('role:write', 'Manage roles', 'role', 'write'),
('tenant:read', 'View tenant info', 'tenant', 'read'),
('tenant:write', 'Manage tenant settings and status', 'tenant', 'write')
ON CONFLICT (code) DO NOTHING;
INSERT INTO roles (tenant_id, name, description, is_system)
VALUES ('11111111-1111-1111-1111-111111111111', 'Admin', 'Administrator with full access', TRUE)
ON CONFLICT (tenant_id, name) DO NOTHING;
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r, permissions p
WHERE r.name = 'Admin'
AND r.tenant_id = '11111111-1111-1111-1111-111111111111'
ON CONFLICT DO NOTHING;
COMMIT;

View File

@@ -0,0 +1,88 @@
BEGIN;
CREATE TABLE IF NOT EXISTS apps (
id VARCHAR(32) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS tenant_entitlements (
tenant_id UUID PRIMARY KEY REFERENCES tenants(id) ON DELETE CASCADE,
enabled_apps TEXT[] NOT NULL DEFAULT '{}',
version INTEGER NOT NULL DEFAULT 0,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS tenant_enabled_apps_history (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
version INTEGER NOT NULL,
enabled_apps TEXT[] NOT NULL,
actor_user_id UUID,
reason TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
INSERT INTO tenants (id, name, config)
VALUES (
'00000000-0000-0000-0000-000000000001',
'Platform',
jsonb_build_object('enabled_apps', jsonb_build_array(), 'enabled_apps_version', 0)
)
ON CONFLICT (id) DO NOTHING;
INSERT INTO apps (id, name, description) VALUES
('iam', 'IAM', 'Identity and Access Management'),
('cms', 'CMS', 'Content Management Platform'),
('tms', 'TMS', 'Task Management Platform')
ON CONFLICT (id) DO NOTHING;
INSERT INTO tenant_entitlements (tenant_id, enabled_apps, version)
SELECT t.id, '{}'::text[], 0
FROM tenants t
ON CONFLICT (tenant_id) DO NOTHING;
UPDATE tenant_entitlements
SET enabled_apps = ARRAY['tms']::text[]
WHERE tenant_id = '11111111-1111-1111-1111-111111111111'
AND enabled_apps = '{}'::text[];
UPDATE tenants
SET config =
jsonb_set(
jsonb_set(COALESCE(config, '{}'::jsonb), '{enabled_apps}', to_jsonb(te.enabled_apps), true),
'{enabled_apps_version}', to_jsonb(te.version), true
)
FROM tenant_entitlements te
WHERE tenants.id = te.tenant_id;
INSERT INTO permissions (code, description, resource, action) VALUES
('iam:tenant:enabled_apps:read', 'Read tenant enabled apps', 'tenant_enabled_apps', 'read'),
('iam:tenant:enabled_apps:write', 'Manage tenant enabled apps', 'tenant_enabled_apps', 'write')
ON CONFLICT (code) DO NOTHING;
INSERT INTO roles (tenant_id, name, description, is_system)
VALUES ('00000000-0000-0000-0000-000000000001', 'SuperAdmin', 'Platform super administrator', TRUE)
ON CONFLICT (tenant_id, name) DO NOTHING;
DELETE FROM role_permissions rp
USING roles r, permissions p
WHERE rp.role_id = r.id
AND rp.permission_id = p.id
AND r.name = 'Admin'
AND r.tenant_id <> '00000000-0000-0000-0000-000000000001'
AND p.code LIKE 'iam:%';
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r, permissions p
WHERE r.name = 'SuperAdmin'
AND r.tenant_id = '00000000-0000-0000-0000-000000000001'
AND p.code IN ('iam:tenant:enabled_apps:read', 'iam:tenant:enabled_apps:write')
ON CONFLICT DO NOTHING;
COMMIT;

View File

@@ -82,8 +82,8 @@ if [[ "${BACKUP}" == "1" ]]; then
echo "Backup written: ${backup_file}"
fi
psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${ROOT_DIR}/sql/drop_iam_schema.sql"
psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${ROOT_DIR}/sql/schema_post_init.sql"
psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${ROOT_DIR}/sql/verify_iam_schema.sql"
CONFIRM=1 "${ROOT_DIR}/scripts/db/reset.sh"
"${ROOT_DIR}/scripts/db/migrate.sh"
"${ROOT_DIR}/scripts/db/verify.sh"
echo "Rebuild completed"

52
scripts/db/reset.sh Normal file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
load_database_url_from_env_file() {
local env_file="$1"
local line value
while IFS= read -r line || [[ -n "${line}" ]]; do
line="${line#"${line%%[![:space:]]*}"}"
[[ -z "${line}" || "${line}" == \#* ]] && continue
line="${line#export }"
if [[ "${line}" == DATABASE_URL=* ]]; then
value="${line#DATABASE_URL=}"
value="${value%$'\r'}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
printf '%s' "${value}"
return 0
fi
done < "${env_file}"
return 1
}
DATABASE_URL="${DATABASE_URL:-}"
if [[ -z "${DATABASE_URL}" && -f "${ROOT_DIR}/.env" ]]; then
DATABASE_URL="$(load_database_url_from_env_file "${ROOT_DIR}/.env" || true)"
fi
if [[ -z "${DATABASE_URL}" ]]; then
echo "DATABASE_URL is required (export it, or set it in ${ROOT_DIR}/.env)"
exit 1
fi
if ! command -v psql >/dev/null 2>&1; then
echo "psql not found in PATH"
exit 127
fi
CONFIRM="${CONFIRM:-0}"
if [[ "${CONFIRM}" != "1" ]]; then
echo "Refusing to reset database without CONFIRM=1"
exit 1
fi
CONFIRM=1 TARGET_VERSION=0000 "${SCRIPT_DIR}/rollback.sh" || true
psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c "DROP TABLE IF EXISTS iam_schema_migrations"
echo "Reset completed"

101
scripts/db/rollback.sh Normal file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
load_database_url_from_env_file() {
local env_file="$1"
local line value
while IFS= read -r line || [[ -n "${line}" ]]; do
line="${line#"${line%%[![:space:]]*}"}"
[[ -z "${line}" || "${line}" == \#* ]] && continue
line="${line#export }"
if [[ "${line}" == DATABASE_URL=* ]]; then
value="${line#DATABASE_URL=}"
value="${value%$'\r'}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
printf '%s' "${value}"
return 0
fi
done < "${env_file}"
return 1
}
DATABASE_URL="${DATABASE_URL:-}"
if [[ -z "${DATABASE_URL}" && -f "${ROOT_DIR}/.env" ]]; then
DATABASE_URL="$(load_database_url_from_env_file "${ROOT_DIR}/.env" || true)"
fi
if [[ -z "${DATABASE_URL}" ]]; then
echo "DATABASE_URL is required (export it, or set it in ${ROOT_DIR}/.env)"
exit 1
fi
if ! command -v psql >/dev/null 2>&1; then
echo "psql not found in PATH"
exit 127
fi
CONFIRM="${CONFIRM:-0}"
if [[ "${CONFIRM}" != "1" ]]; then
echo "Refusing to run rollback without CONFIRM=1"
exit 1
fi
TARGET_VERSION="${TARGET_VERSION:-}"
STEPS="${STEPS:-1}"
if ! psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c "SELECT 1 FROM iam_schema_migrations LIMIT 1" >/dev/null 2>&1; then
echo "No iam_schema_migrations table found; nothing to rollback"
exit 0
fi
applied_versions=()
while IFS= read -r v; do
[[ -n "${v}" ]] && applied_versions+=( "${v}" )
done < <(psql "${DATABASE_URL}" -At -c "SELECT version FROM iam_schema_migrations ORDER BY version DESC")
if [[ ${#applied_versions[@]} -eq 0 ]]; then
echo "No applied migrations; nothing to rollback"
exit 0
fi
to_rollback=()
if [[ -n "${TARGET_VERSION}" ]]; then
for v in "${applied_versions[@]}"; do
if [[ "${v}" > "${TARGET_VERSION}" ]]; then
to_rollback+=( "${v}" )
fi
done
else
count=0
for v in "${applied_versions[@]}"; do
to_rollback+=( "${v}" )
count=$((count + 1))
if [[ "${count}" -ge "${STEPS}" ]]; then
break
fi
done
fi
if [[ ${#to_rollback[@]} -eq 0 ]]; then
echo "No migrations to rollback"
exit 0
fi
for v in "${to_rollback[@]}"; do
down_file="${SCRIPT_DIR}/rollback/${v}.down.sql"
if [[ ! -f "${down_file}" ]]; then
echo "Missing rollback script: ${down_file}"
exit 1
fi
echo "Rolling back ${v}"
psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${down_file}"
psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c "DELETE FROM iam_schema_migrations WHERE version='${v}'"
done
echo "Rollback completed"

View File

@@ -0,0 +1,13 @@
BEGIN;
DROP TABLE IF EXISTS role_permissions;
DROP TABLE IF EXISTS user_roles;
DROP TABLE IF EXISTS refresh_tokens;
DROP TABLE IF EXISTS audit_logs;
DROP TABLE IF EXISTS roles;
DROP TABLE IF EXISTS permissions;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS tenants;
COMMIT;

View File

@@ -0,0 +1,26 @@
BEGIN;
DELETE FROM role_permissions rp
USING roles r, permissions p
WHERE rp.role_id = r.id
AND rp.permission_id = p.id
AND r.name = 'SuperAdmin'
AND r.tenant_id = '00000000-0000-0000-0000-000000000001'
AND p.code IN ('iam:tenant:enabled_apps:read', 'iam:tenant:enabled_apps:write');
DELETE FROM roles
WHERE tenant_id = '00000000-0000-0000-0000-000000000001'
AND name = 'SuperAdmin';
DELETE FROM permissions
WHERE code IN ('iam:tenant:enabled_apps:read', 'iam:tenant:enabled_apps:write');
DROP TABLE IF EXISTS tenant_enabled_apps_history;
DROP TABLE IF EXISTS tenant_entitlements;
DROP TABLE IF EXISTS apps;
DELETE FROM tenants
WHERE id = '00000000-0000-0000-0000-000000000001';
COMMIT;

88
scripts/db/verify.sh Normal file
View File

@@ -0,0 +1,88 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
load_database_url_from_env_file() {
local env_file="$1"
local line value
while IFS= read -r line || [[ -n "${line}" ]]; do
line="${line#"${line%%[![:space:]]*}"}"
[[ -z "${line}" || "${line}" == \#* ]] && continue
line="${line#export }"
if [[ "${line}" == DATABASE_URL=* ]]; then
value="${line#DATABASE_URL=}"
value="${value%$'\r'}"
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
printf '%s' "${value}"
return 0
fi
done < "${env_file}"
return 1
}
DATABASE_URL="${DATABASE_URL:-}"
if [[ -z "${DATABASE_URL}" && -f "${ROOT_DIR}/.env" ]]; then
DATABASE_URL="$(load_database_url_from_env_file "${ROOT_DIR}/.env" || true)"
fi
if [[ -z "${DATABASE_URL}" ]]; then
echo "DATABASE_URL is required (export it, or set it in ${ROOT_DIR}/.env)"
exit 1
fi
if ! command -v psql >/dev/null 2>&1; then
echo "psql not found in PATH"
exit 127
fi
if ! psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -c "SELECT 1 FROM iam_schema_migrations LIMIT 1" >/dev/null 2>&1; then
echo "iam_schema_migrations not found"
echo "Run migrations first: ./scripts/db/migrate.sh"
exit 1
fi
STRICT="${STRICT:-1}"
verify_dir="${SCRIPT_DIR}/verify"
if [[ ! -d "${verify_dir}" ]]; then
echo "Missing verify directory: ${verify_dir}"
exit 1
fi
applied_versions=()
while IFS= read -r v; do
[[ -n "${v}" ]] && applied_versions+=( "${v}" )
done < <(psql "${DATABASE_URL}" -At -c "SELECT version FROM iam_schema_migrations ORDER BY version ASC")
declare -A verify_by_version=()
shopt -s nullglob
for f in "${verify_dir}/"*.sql; do
base="$(basename "${f}")"
version="${base%%_*}"
version="${version%%.*}"
verify_by_version["${version}"]="${f}"
done
if [[ "${STRICT}" == "1" ]]; then
for v in "${applied_versions[@]}"; do
if [[ -z "${verify_by_version[${v}]:-}" ]]; then
echo "Missing verify script for applied migration version: ${v}"
echo "Expected: ${verify_dir}/${v}_*.sql"
exit 1
fi
done
fi
for v in "${applied_versions[@]}"; do
f="${verify_by_version[${v}]:-}"
if [[ -z "${f}" ]]; then
continue
fi
echo "Verifying ${v} ($(basename "${f}"))"
psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${f}"
done
echo "Verify completed"

View File

@@ -0,0 +1,74 @@
DO $$
BEGIN
IF to_regclass('public.tenants') IS NULL THEN
RAISE EXCEPTION 'missing table: tenants';
END IF;
IF to_regclass('public.users') IS NULL THEN
RAISE EXCEPTION 'missing table: users';
END IF;
IF to_regclass('public.roles') IS NULL THEN
RAISE EXCEPTION 'missing table: roles';
END IF;
IF to_regclass('public.permissions') IS NULL THEN
RAISE EXCEPTION 'missing table: permissions';
END IF;
IF to_regclass('public.user_roles') IS NULL THEN
RAISE EXCEPTION 'missing table: user_roles';
END IF;
IF to_regclass('public.role_permissions') IS NULL THEN
RAISE EXCEPTION 'missing table: role_permissions';
END IF;
IF to_regclass('public.refresh_tokens') IS NULL THEN
RAISE EXCEPTION 'missing table: refresh_tokens';
END IF;
IF to_regclass('public.audit_logs') IS NULL THEN
RAISE EXCEPTION 'missing table: audit_logs';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'tenants' AND column_name = 'status'
) THEN
RAISE EXCEPTION 'tenants.status missing';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'tenants' AND column_name = 'config'
) THEN
RAISE EXCEPTION 'tenants.config missing';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'users' AND column_name = 'mfa_enabled'
) THEN
RAISE EXCEPTION 'users.mfa_enabled missing';
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_indexes
WHERE schemaname = 'public' AND tablename = 'users' AND indexname = 'idx_users_tenant_email'
) THEN
RAISE EXCEPTION 'missing index: idx_users_tenant_email';
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint c
JOIN pg_class t ON t.oid = c.conrelid
WHERE t.relname = 'users' AND c.contype = 'f' AND pg_get_constraintdef(c.oid) LIKE 'FOREIGN KEY (tenant_id)%'
) THEN
RAISE EXCEPTION 'missing foreign key users.tenant_id -> tenants.id';
END IF;
IF NOT EXISTS (
SELECT 1 FROM tenants WHERE id = '11111111-1111-1111-1111-111111111111'
) THEN
RAISE EXCEPTION 'missing seed tenant Default Corp';
END IF;
IF NOT EXISTS (SELECT 1 FROM permissions WHERE code = 'user:read') THEN
RAISE EXCEPTION 'missing seed permission user:read';
END IF;
END $$;

View File

@@ -0,0 +1,31 @@
DO $$
BEGIN
IF to_regclass('public.apps') IS NULL THEN
RAISE EXCEPTION 'missing table: apps';
END IF;
IF to_regclass('public.tenant_entitlements') IS NULL THEN
RAISE EXCEPTION 'missing table: tenant_entitlements';
END IF;
IF to_regclass('public.tenant_enabled_apps_history') IS NULL THEN
RAISE EXCEPTION 'missing table: tenant_enabled_apps_history';
END IF;
IF NOT EXISTS (
SELECT 1 FROM tenants WHERE id = '00000000-0000-0000-0000-000000000001'
) THEN
RAISE EXCEPTION 'missing seed tenant Platform';
END IF;
IF NOT EXISTS (SELECT 1 FROM permissions WHERE code = 'iam:tenant:enabled_apps:write') THEN
RAISE EXCEPTION 'missing seed permission iam:tenant:enabled_apps:write';
END IF;
IF NOT EXISTS (SELECT 1 FROM apps WHERE id = 'tms') THEN
RAISE EXCEPTION 'missing seed app tms';
END IF;
IF NOT EXISTS (SELECT 1 FROM tenant_entitlements WHERE tenant_id = '11111111-1111-1111-1111-111111111111') THEN
RAISE EXCEPTION 'missing tenant_entitlements row for Default Corp';
END IF;
END $$;