Skip to content

Commit 2801bd9

Browse files
authored
Merge pull request #1049 from supertokens/feat/tenant-management-docs
Improve the db migration tutorial
2 parents 8eb53a5 + 06641ec commit 2801bd9

File tree

1 file changed

+256
-10
lines changed

1 file changed

+256
-10
lines changed

docs/deployment/migrate-from-mysql.mdx

Lines changed: 256 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,91 @@ Run the following command to export most of your data:
3838
mysqldump supertokens --fields-terminated-by ',' --fields-enclosed-by '"' --fields-escaped-by '\' --no-create-info --tab /var/lib/mysql-files/
3939
```
4040

41+
:::info
42+
43+
If you do not have permissions to write to the database filesystem you can use the following python script to export tables one by one:
44+
<Accordion>
45+
## Export script
46+
```python
47+
#!/usr/bin/env python3
48+
import subprocess
49+
import os
50+
import csv
51+
52+
DB_HOST = "DB_HOST"
53+
DB_PORT = "3306"
54+
DB_USER = "DB_USER"
55+
DB_NAME = "DB_NAME"
56+
DB_PASS = "DB_PASS"
57+
58+
def run_mysql_command(query):
59+
"""Run a mysql command and return the output"""
60+
cmd = [
61+
"mysql",
62+
"-h", DB_HOST,
63+
"-P", DB_PORT,
64+
"-u", DB_USER,
65+
f"-p{DB_PASS}",
66+
"--batch",
67+
"-e", query,
68+
DB_NAME
69+
]
70+
result = subprocess.run(cmd, capture_output=True, text=True)
71+
return result.stdout
72+
73+
def main():
74+
os.makedirs("./mysql", exist_ok=True)
75+
76+
print("Getting list of tables")
77+
cmd = [
78+
"mysql",
79+
"-h", DB_HOST,
80+
"-P", DB_PORT,
81+
"-u", DB_USER,
82+
f"-p{DB_PASS}",
83+
"-N",
84+
"-e", "SHOW TABLES",
85+
DB_NAME
86+
]
87+
result = subprocess.run(cmd, capture_output=True, text=True)
88+
89+
if result.returncode != 0:
90+
print(f"ERROR: Failed to get table list: {result.stderr}")
91+
return
92+
93+
tables = [t.strip() for t in result.stdout.strip().split('\n') if t.strip()]
94+
print(f"Found {len(tables)} tables")
95+
96+
for table in tables:
97+
print(f"Exporting table: {table}")
98+
99+
query = f"SELECT * FROM {table}"
100+
output = run_mysql_command(query)
101+
102+
if not output.strip():
103+
print(f" -> Table {table} is empty, skipping")
104+
continue
105+
106+
lines = output.strip().split('\n')
107+
108+
output_file = f"./mysql/{table}.csv"
109+
with open(output_file, 'w', newline='') as f:
110+
csv_writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL)
111+
112+
for line in lines:
113+
fields = line.split('\t')
114+
csv_writer.writerow(fields)
115+
116+
print(f" -> Exported {len(lines)} rows")
117+
118+
print("Export complete!")
119+
120+
if __name__ == "__main__":
121+
main()
122+
```
123+
</Accordion>
124+
:::
125+
41126
This creates CSV files for all tables in the `/var/lib/mysql-files/` directory.
42127

43128
#### 3.2 Export the WebAuthn credentials table
@@ -72,23 +157,110 @@ Connect to your PostgreSQL database and disable triggers to prevent constraint v
72157
SET session_replication_role = 'replica';
73158
```
74159

160+
:::info
161+
If you cannot disable the triggers, use the order specified in the next step.
162+
The *With Order* shows you how to import everything one by one without triggering the constraints.
163+
:::
164+
75165
#### 5.2 Import the standard tables
76166

77167
For most tables, you can import the data directly.
78168

79169
<Tabs>
80170
<TabItem value="without-order" label="Without specifying column order">
81171
```sql
82-
COPY app_id_to_user_id FROM '/pg-data-host/app_id_to_user_id.txt'
172+
COPY <table_name> FROM '/pg-data-host/<table_name>.csv'
83173
CSV DELIMITER ',' QUOTE '"' ESCAPE '\' NULL as '\N';
84174
```
85175
</TabItem>
86176
87177
<TabItem value="with-order" label="With specifying column order">
88-
```sql
89-
COPY app_id_to_user_id(app_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id)
90-
FROM '/pg-data-host/app_id_to_user_id.txt'
91-
CSV DELIMITER ',' QUOTE '"' ESCAPE '\' NULL as '\N';
178+
```bash
179+
#!/bin/bash
180+
181+
PG_HOST="PG_HOST"
182+
PG_PORT="5432"
183+
PG_USER="PG_USER"
184+
PG_DB="PG_DB"
185+
PG_PASS="PG_PASS"
186+
CSV_DIR="./mysql"
187+
188+
import_table() {
189+
local table=$1
190+
local columns=$2
191+
local csv_file="${CSV_DIR}/${table}.csv"
192+
193+
if [ ! -f "$csv_file" ]; then
194+
echo "File not found: $csv_file, skipping"
195+
return
196+
fi
197+
198+
echo "Importing table: $table"
199+
200+
if [ -z "$columns" ] || [ "$columns" = "*" ]; then
201+
PGPASSWORD=$PG_PASS psql -h $PG_HOST -p $PG_PORT -U $PG_USER -d $PG_DB -c \
202+
"\\COPY $table FROM '$csv_file' WITH (FORMAT csv, HEADER true);"
203+
else
204+
PGPASSWORD=$PG_PASS psql -h $PG_HOST -p $PG_PORT -U $PG_USER -d $PG_DB -c \
205+
"\\COPY $table ($columns) FROM '$csv_file' WITH (FORMAT csv, HEADER true);"
206+
fi
207+
208+
if [ $? -eq 0 ]; then
209+
echo "Successfully imported $table"
210+
else
211+
echo "ERROR importing $table"
212+
fi
213+
}
214+
215+
echo "Starting PostgreSQL import"
216+
217+
import_table "apps" ""
218+
import_table "tenants" ""
219+
import_table "key_value" "app_id, tenant_id, name, value, created_at_time"
220+
import_table "all_auth_recipe_users" "app_id, tenant_id, user_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user, recipe_id, time_joined, primary_or_recipe_user_time_joined"
221+
import_table "app_id_to_user_id" "app_id, user_id, recipe_id, primary_or_recipe_user_id, is_linked_or_is_a_primary_user"
222+
import_table "bulk_import_users" "id, app_id, primary_user_id, raw_data, status, error_msg, created_at, updated_at"
223+
import_table "dashboard_user_sessions" "app_id, session_id, user_id, time_created, expiry"
224+
import_table "dashboard_users" "app_id, user_id, email, password_hash, time_joined"
225+
import_table "emailpassword_pswd_reset_tokens" "app_id, user_id, token, email, token_expiry"
226+
import_table "emailpassword_user_to_tenant" "app_id, tenant_id, user_id, email"
227+
import_table "emailpassword_users" "app_id, user_id, email, password_hash, time_joined"
228+
import_table "emailverification_tokens" "app_id, tenant_id, user_id, email, token, token_expiry"
229+
import_table "emailverification_verified_emails" "app_id, user_id, email"
230+
import_table "jwt_signing_keys" "app_id, key_id, key_string, algorithm, created_at"
231+
import_table "oauth_clients" "app_id, client_id, client_secret, enable_refresh_token_rotation, is_client_credentials_only"
232+
import_table "oauth_logout_challenges" "app_id, challenge, client_id, post_logout_redirect_uri, session_handle, state, time_created"
233+
import_table "oauth_m2m_tokens" "app_id, client_id, iat, exp"
234+
import_table "oauth_sessions" "gid, app_id, client_id, session_handle, external_refresh_token, internal_refresh_token, jti, exp"
235+
import_table "passwordless_codes" "app_id, tenant_id, code_id, device_id_hash, link_code_hash, created_at"
236+
import_table "passwordless_devices" "app_id, tenant_id, device_id_hash, email, phone_number, link_code_salt, failed_attempts"
237+
import_table "passwordless_user_to_tenant" "app_id, tenant_id, user_id, email, phone_number"
238+
import_table "passwordless_users" "app_id, user_id, email, phone_number, time_joined"
239+
import_table "role_permissions" "app_id, role, permission"
240+
import_table "roles" "app_id, role"
241+
import_table "session_access_token_signing_keys" "app_id, created_at_time, value"
242+
import_table "session_info" "app_id, tenant_id, session_handle, user_id, refresh_token_hash_2, session_data, expires_at, created_at_time, jwt_user_payload, use_static_key"
243+
import_table "tenant_first_factors" "connection_uri_domain, app_id, tenant_id, factor_id"
244+
import_table "tenant_required_secondary_factors" "connection_uri_domain, app_id, tenant_id, factor_id"
245+
import_table "tenant_thirdparty_providers" "connection_uri_domain, app_id, tenant_id, third_party_id, name, authorization_endpoint, authorization_endpoint_query_params, token_endpoint, token_endpoint_body_params, user_info_endpoint, user_info_endpoint_query_params, user_info_endpoint_headers, jwks_uri, oidc_discovery_endpoint, require_email, user_info_map_from_id_token_payload_user_id, user_info_map_from_id_token_payload_email, user_info_map_from_id_token_payload_email_verified, user_info_map_from_user_info_endpoint_user_id, user_info_map_from_user_info_endpoint_email, user_info_map_from_user_info_endpoint_email_verified"
246+
import_table "thirdparty_user_to_tenant" "app_id, tenant_id, user_id, third_party_id, third_party_user_id"
247+
import_table "thirdparty_users" "app_id, third_party_id, third_party_user_id, user_id, email, time_joined"
248+
import_table "totp_used_codes" "app_id, tenant_id, user_id, code, is_valid, expiry_time_ms, created_time_ms"
249+
import_table "tenant_configs" "connection_uri_domain, app_id, tenant_id, core_config, email_password_enabled, passwordless_enabled, third_party_enabled, is_first_factors_null"
250+
import_table "totp_user_devices" "app_id, user_id, device_name, secret_key, period, skew, verified, created_at"
251+
import_table "totp_users" "app_id, user_id"
252+
import_table "user_last_active" "app_id, user_id, last_active_time"
253+
import_table "user_metadata" "app_id, user_id, user_metadata"
254+
import_table "user_roles" "app_id, tenant_id, user_id, role"
255+
import_table "userid_mapping" "app_id, supertokens_user_id, external_user_id, external_user_id_info"
256+
import_table "webauthn_account_recovery_tokens" "app_id, tenant_id, user_id, email, token, expires_at"
257+
import_table "webauthn_generated_options" "app_id, tenant_id, id, challenge, email, rp_id, rp_name, origin, expires_at, created_at, user_presence_required, user_verification"
258+
import_table "webauthn_user_to_tenant" "app_id, tenant_id, user_id, email"
259+
import_table "webauthn_users" "app_id, user_id, email, rp_id, time_joined"
260+
261+
echo ""
262+
echo "Import complete!"
263+
92264
```
93265
</TabItem>
94266
@@ -210,11 +382,85 @@ SET session_replication_role = 'origin';
210382
211383
Verify that all data migrated successfully by comparing record counts between your MySQL and PostgreSQL databases:
212384
213-
```sql
214-
-- Run on both databases
215-
SELECT COUNT(*) FROM users;
216-
SELECT COUNT(*) FROM sessions;
217-
-- Add other tables as needed
385+
```bash
386+
#!/bin/bash
387+
388+
MYSQL_HOST="DB_HOST"
389+
MYSQL_PORT="3306"
390+
MYSQL_USER="DB_USER"
391+
MYSQL_DB="DB_NAME"
392+
MYSQL_PASS="DB_PASS"
393+
394+
PG_HOST="PG_HOST"
395+
PG_PORT="5432"
396+
PG_USER="PG_USER"
397+
PG_DB="PG_DB"
398+
PG_PASS="PG_PASS"
399+
400+
echo "Comparing table row counts between MySQL and PostgreSQL"
401+
402+
echo "Getting table list from MySQL"
403+
TABLES=$(mysql -h $MYSQL_HOST -P $MYSQL_PORT -u $MYSQL_USER -p$MYSQL_PASS \
404+
-N -e "SHOW TABLES" $MYSQL_DB)
405+
406+
if [ $? -ne 0 ]; then
407+
echo "ERROR: Failed to get table list from MySQL"
408+
exit 1
409+
fi
410+
411+
TOTAL_TABLES=$(echo "$TABLES" | wc -l)
412+
echo "Found $TOTAL_TABLES tables"
413+
echo ""
414+
415+
MATCH_COUNT=0
416+
MISMATCH_COUNT=0
417+
ERROR_COUNT=0
418+
419+
for TABLE in $TABLES; do
420+
MYSQL_COUNT=$(mysql -h $MYSQL_HOST -P $MYSQL_PORT -u $MYSQL_USER -p$MYSQL_PASS \
421+
-N -e "SELECT COUNT(*) FROM $TABLE" $MYSQL_DB 2>&1)
422+
423+
if [ $? -ne 0 ]; then
424+
echo "$TABLE - MySQL: ERROR, PostgreSQL: -, Status: ERROR"
425+
ERROR_COUNT=$((ERROR_COUNT + 1))
426+
continue
427+
fi
428+
429+
PG_COUNT=$(PGPASSWORD=$PG_PASS psql -h $PG_HOST -p $PG_PORT -U $PG_USER -d $PG_DB \
430+
-t -c "SELECT COUNT(*) FROM $TABLE" 2>&1)
431+
432+
if [ $? -ne 0 ]; then
433+
echo "$TABLE - MySQL: $MYSQL_COUNT, PostgreSQL: ERROR, Status: ERROR"
434+
ERROR_COUNT=$((ERROR_COUNT + 1))
435+
continue
436+
fi
437+
438+
MYSQL_COUNT=$(echo $MYSQL_COUNT | xargs)
439+
PG_COUNT=$(echo $PG_COUNT | xargs)
440+
441+
if [ "$MYSQL_COUNT" = "$PG_COUNT" ]; then
442+
echo "$TABLE - MySQL: $MYSQL_COUNT, PostgreSQL: $PG_COUNT, Status: ✓ MATCH"
443+
MATCH_COUNT=$((MATCH_COUNT + 1))
444+
else
445+
echo "$TABLE - MySQL: $MYSQL_COUNT, PostgreSQL: $PG_COUNT, Status: ✗ MISMATCH"
446+
MISMATCH_COUNT=$((MISMATCH_COUNT + 1))
447+
fi
448+
done
449+
450+
echo "Summary:"
451+
echo " Total tables: $TOTAL_TABLES"
452+
echo " Matching: $MATCH_COUNT"
453+
echo " Mismatched: $MISMATCH_COUNT"
454+
echo " Errors: $ERROR_COUNT"
455+
echo ""
456+
457+
if [ $MISMATCH_COUNT -eq 0 ] && [ $ERROR_COUNT -eq 0 ]; then
458+
echo "✓ All tables match!"
459+
exit 0
460+
else
461+
echo "✗ Some tables have mismatches or errors"
462+
exit 1
463+
fi
218464
```
219465
220466
If the numbers match, you have successfully migrated your SuperTokens data from `MySQL` to `PostgreSQL` :tada:

0 commit comments

Comments
 (0)