#!/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"