forked from boos/BoosAPI
204 lines
7.8 KiB
Bash
Executable File
204 lines
7.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
source_url="${1:-postgresql:///boosapi_migration_test}"
|
|
output="${2:-migrations_mysql/20260516000001_initial.sql}"
|
|
tmp_dir="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp_dir"' EXIT
|
|
|
|
psql_cmd=(psql -X -qAt -v ON_ERROR_STOP=1 "$source_url")
|
|
|
|
"${psql_cmd[@]}" -F '|' -c "
|
|
SELECT DISTINCT c.relname, a.attname
|
|
FROM pg_index i
|
|
JOIN pg_class c ON c.oid = i.indrelid
|
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
JOIN LATERAL unnest(i.indkey) AS key(attnum) ON true
|
|
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = key.attnum
|
|
WHERE n.nspname = 'public'
|
|
UNION
|
|
SELECT DISTINCT c.relname, a.attname
|
|
FROM pg_constraint con
|
|
JOIN pg_class c ON c.oid = con.conrelid
|
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
JOIN LATERAL unnest(con.conkey) AS key(attnum) ON true
|
|
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = key.attnum
|
|
WHERE n.nspname = 'public' AND con.contype = 'f';
|
|
" > "$tmp_dir/indexed"
|
|
|
|
map_type() {
|
|
local table="$1" column="$2" data_type="$3" default="$4"
|
|
case "$data_type" in
|
|
bigint) printf 'BIGINT' ;;
|
|
boolean) printf 'TINYINT(1)' ;;
|
|
'double precision') printf 'DOUBLE' ;;
|
|
bytea) printf 'LONGBLOB' ;;
|
|
text)
|
|
if grep -Fxq "$table|$column" "$tmp_dir/indexed"; then
|
|
printf 'VARCHAR(128)'
|
|
else
|
|
printf 'LONGTEXT'
|
|
fi
|
|
;;
|
|
*) printf 'LONGTEXT' ;;
|
|
esac
|
|
}
|
|
|
|
map_default() {
|
|
local default="$1" data_type="$2"
|
|
[[ -n "$default" ]] || return 0
|
|
if [[ "$default" == nextval\(* ]]; then
|
|
return 0
|
|
fi
|
|
case "$default" in
|
|
'(CURRENT_TIMESTAMP)::text'|'CURRENT_TIMESTAMP'|'now()')
|
|
printf ' DEFAULT (CAST(UTC_TIMESTAMP() AS CHAR))'
|
|
;;
|
|
true) printf ' DEFAULT 1' ;;
|
|
false) printf ' DEFAULT 0' ;;
|
|
*"::text")
|
|
local literal="${default%::text}"
|
|
if [[ "$data_type" == text && "$literal" == "''" ]]; then
|
|
printf " DEFAULT (CAST('' AS CHAR))"
|
|
elif [[ "$data_type" == text && "$literal" == "'"* ]]; then
|
|
printf ' DEFAULT (%s)' "$literal"
|
|
else
|
|
printf ' DEFAULT %s' "$literal"
|
|
fi
|
|
;;
|
|
*) printf ' DEFAULT %s' "$default" ;;
|
|
esac
|
|
}
|
|
|
|
{
|
|
echo '-- Generated from the canonical PostgreSQL schema by scripts/generate_mysql_schema.sh.'
|
|
echo '-- Compatible with MySQL 8.0+ and MariaDB 10.6+.'
|
|
echo 'SET NAMES utf8mb4;'
|
|
echo 'SET FOREIGN_KEY_CHECKS = 0;'
|
|
echo
|
|
|
|
while IFS= read -r table; do
|
|
echo "CREATE TABLE \`$table\` ("
|
|
mapfile -t columns < <("${psql_cmd[@]}" -F '|' -c "
|
|
SELECT column_name, data_type, is_nullable, COALESCE(column_default, '')
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'public' AND table_name = '$table'
|
|
ORDER BY ordinal_position;")
|
|
definitions=()
|
|
for row in "${columns[@]}"; do
|
|
IFS='|' read -r column data_type nullable default <<< "$row"
|
|
type="$(map_type "$table" "$column" "$data_type" "$default")"
|
|
definition=" \`$column\` $type"
|
|
[[ "$nullable" == YES ]] || definition+=' NOT NULL'
|
|
if [[ "$default" == nextval\(* ]]; then
|
|
definition+=' AUTO_INCREMENT'
|
|
else
|
|
definition+="$(map_default "$default" "$data_type")"
|
|
fi
|
|
definitions+=("$definition")
|
|
done
|
|
|
|
while IFS='|' read -r constraint_name constraint_type column_list; do
|
|
[[ -n "$constraint_name" ]] || continue
|
|
if [[ "$constraint_type" == PRIMARY* ]]; then
|
|
definitions+=(" PRIMARY KEY ($column_list)")
|
|
else
|
|
definitions+=(" CONSTRAINT \`$constraint_name\` UNIQUE ($column_list)")
|
|
fi
|
|
done < <("${psql_cmd[@]}" -F '|' -c "
|
|
SELECT con.conname,
|
|
CASE con.contype WHEN 'p' THEN 'PRIMARY' ELSE 'UNIQUE' END,
|
|
string_agg(format('\\\`%s\\\`', att.attname), ', ' ORDER BY ord.n)
|
|
FROM pg_constraint con
|
|
JOIN pg_class rel ON rel.oid = con.conrelid
|
|
JOIN pg_namespace ns ON ns.oid = rel.relnamespace
|
|
JOIN LATERAL unnest(con.conkey) WITH ORDINALITY ord(attnum, n) ON true
|
|
JOIN pg_attribute att ON att.attrelid = rel.oid AND att.attnum = ord.attnum
|
|
WHERE ns.nspname = 'public' AND rel.relname = '$table' AND con.contype IN ('p', 'u')
|
|
GROUP BY con.conname, con.contype
|
|
ORDER BY con.contype, con.conname;")
|
|
|
|
case "$table" in
|
|
api_request_leases)
|
|
definitions+=(" \`active_user_id\` BIGINT GENERATED ALWAYS AS (CASE WHEN \`status\` = 'active' THEN \`user_id\` ELSE NULL END) VIRTUAL")
|
|
definitions+=(" UNIQUE INDEX \`api_request_leases_one_active_per_user\` (\`active_user_id\`)")
|
|
;;
|
|
agent_runs)
|
|
definitions+=(" \`active_session_id\` VARCHAR(128) GENERATED ALWAYS AS (CASE WHEN \`status\` IN ('starting','running','waiting_approval','waiting_input','cancelling') THEN \`session_id\` ELSE NULL END) VIRTUAL")
|
|
definitions+=(" UNIQUE INDEX \`agent_runs_one_active_per_session\` (\`active_session_id\`)")
|
|
;;
|
|
payment_refund_requests)
|
|
definitions+=(" \`active_payment_order_id\` BIGINT GENERATED ALWAYS AS (CASE WHEN \`status\` IN ('pending','approved','processing') THEN \`payment_order_id\` ELSE NULL END) VIRTUAL")
|
|
definitions+=(" UNIQUE INDEX \`payment_refund_requests_one_active_per_order\` (\`active_payment_order_id\`)")
|
|
;;
|
|
esac
|
|
|
|
for ((i=0; i<${#definitions[@]}; i++)); do
|
|
suffix=','
|
|
(( i + 1 == ${#definitions[@]} )) && suffix=''
|
|
printf '%s%s\n' "${definitions[$i]}" "$suffix"
|
|
done
|
|
echo ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;'
|
|
echo
|
|
done < <("${psql_cmd[@]}" -c "
|
|
SELECT table_name
|
|
FROM information_schema.tables
|
|
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
AND table_name <> '_sqlx_migrations'
|
|
ORDER BY table_name;")
|
|
|
|
"${psql_cmd[@]}" -F '|' -c "
|
|
SELECT format(
|
|
'ALTER TABLE \\\`%s\\\` ADD CONSTRAINT \\\`%s\\\` %s;',
|
|
rel.relname,
|
|
con.conname,
|
|
replace(pg_get_constraintdef(con.oid), 'public.', '')
|
|
)
|
|
FROM pg_constraint con
|
|
JOIN pg_class rel ON rel.oid = con.conrelid
|
|
JOIN pg_namespace ns ON ns.oid = rel.relnamespace
|
|
WHERE ns.nspname = 'public' AND con.contype = 'f'
|
|
ORDER BY rel.relname, con.conname;"
|
|
|
|
echo
|
|
"${psql_cmd[@]}" -F '|' -c "
|
|
SELECT format(
|
|
'CREATE INDEX \\\`%s\\\` ON \\\`%s\\\` (%s);',
|
|
idx.relname,
|
|
tbl.relname,
|
|
string_agg(format('\\\`%s\\\`', att.attname), ', ' ORDER BY ord.n)
|
|
)
|
|
FROM pg_index i
|
|
JOIN pg_class tbl ON tbl.oid = i.indrelid
|
|
JOIN pg_class idx ON idx.oid = i.indexrelid
|
|
JOIN pg_namespace ns ON ns.oid = tbl.relnamespace
|
|
JOIN LATERAL unnest(i.indkey) WITH ORDINALITY ord(attnum, n) ON ord.attnum > 0
|
|
JOIN pg_attribute att ON att.attrelid = tbl.oid AND att.attnum = ord.attnum
|
|
LEFT JOIN pg_constraint con ON con.conindid = i.indexrelid
|
|
WHERE ns.nspname = 'public' AND con.oid IS NULL
|
|
AND NOT i.indisunique AND i.indpred IS NULL AND i.indexprs IS NULL
|
|
GROUP BY idx.relname, tbl.relname
|
|
ORDER BY tbl.relname, idx.relname;"
|
|
|
|
cat <<'SQL'
|
|
|
|
CREATE UNIQUE INDEX `payment_orders_provider_session_unique` ON `payment_orders` (`provider`, `provider_session_id`);
|
|
CREATE UNIQUE INDEX `payment_order_provider_session_unique` ON `payment_order_provider_bindings` (`provider_instance_id`, `provider_session_id`);
|
|
CREATE UNIQUE INDEX `payment_order_provider_payment_unique` ON `payment_order_provider_bindings` (`provider_instance_id`, `provider_payment_id`);
|
|
CREATE UNIQUE INDEX `payment_refunds_provider_reference_unique` ON `payment_refunds` (`provider_instance_id`, `provider_refund_id`);
|
|
|
|
SET FOREIGN_KEY_CHECKS = 1;
|
|
SQL
|
|
} > "$output"
|
|
|
|
sed -i 's/\\`/`/g' "$output"
|
|
|
|
for source_migration in migrations/*.sql; do
|
|
name="$(basename "$source_migration")"
|
|
[[ "$name" == "$(basename "$output")" ]] && continue
|
|
[[ -e "migrations_mysql/$name" ]] && continue
|
|
printf '%s\n' "-- Included in the consolidated MySQL/MariaDB initial schema." > "migrations_mysql/$name"
|
|
done
|
|
|
|
echo "generated $output"
|