@supacloud/cli 0.12.1 → 0.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +102 -26
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6455,13 +6455,57 @@ class HttpTransport {
|
|
|
6455
6455
|
}
|
|
6456
6456
|
|
|
6457
6457
|
// src/shared/tools/database-tools.ts
|
|
6458
|
+
import { createHash } from "node:crypto";
|
|
6458
6459
|
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
6459
6460
|
import { basename, join } from "node:path";
|
|
6460
|
-
|
|
6461
|
+
var MAX_MIGRATION_VERSION = 9223372036854775807n;
|
|
6462
|
+
var FALLBACK_MIGRATION_VERSION_BASE = 8000000000000000000n;
|
|
6463
|
+
var FALLBACK_MIGRATION_VERSION_RANGE = 1000000000000000000n;
|
|
6464
|
+
var FALLBACK_MIGRATION_VERSION_LIMIT = FALLBACK_MIGRATION_VERSION_BASE + FALLBACK_MIGRATION_VERSION_RANGE;
|
|
6465
|
+
function readMigrationFile(dir, file) {
|
|
6466
|
+
const rawBytes = readFileSync2(join(dir, file));
|
|
6467
|
+
return {
|
|
6468
|
+
file,
|
|
6469
|
+
name: basename(file, ".sql"),
|
|
6470
|
+
version: migrationVersionFromFilename(file),
|
|
6471
|
+
sql: Buffer.from(rawBytes).toString("utf8"),
|
|
6472
|
+
rawBytes
|
|
6473
|
+
};
|
|
6474
|
+
}
|
|
6475
|
+
function migrationVersionFromFilename(file) {
|
|
6461
6476
|
const match = basename(file).match(/^(\d{8,20})[_-]/);
|
|
6462
|
-
if (match)
|
|
6463
|
-
|
|
6464
|
-
|
|
6477
|
+
if (match) {
|
|
6478
|
+
const version = BigInt(match[1]);
|
|
6479
|
+
if (version < 1n || version > MAX_MIGRATION_VERSION) {
|
|
6480
|
+
throw new Error(`Invalid migration version '${match[1]}' in ${basename(file)}: expected 1..${MAX_MIGRATION_VERSION}`);
|
|
6481
|
+
}
|
|
6482
|
+
if (version >= FALLBACK_MIGRATION_VERSION_BASE && version < FALLBACK_MIGRATION_VERSION_LIMIT) {
|
|
6483
|
+
throw new Error(`Invalid migration version '${match[1]}' in ${basename(file)}: version range ${FALLBACK_MIGRATION_VERSION_BASE}..${FALLBACK_MIGRATION_VERSION_LIMIT - 1n} is reserved for non-timestamp migrations`);
|
|
6484
|
+
}
|
|
6485
|
+
return version.toString();
|
|
6486
|
+
}
|
|
6487
|
+
const digest = createHash("sha256").update(basename(file)).digest("hex");
|
|
6488
|
+
const offset = BigInt(`0x${digest}`) % FALLBACK_MIGRATION_VERSION_RANGE;
|
|
6489
|
+
return (FALLBACK_MIGRATION_VERSION_BASE + offset).toString();
|
|
6490
|
+
}
|
|
6491
|
+
function sortMigrationFiles(migrations) {
|
|
6492
|
+
const sorted = [...migrations].sort((a, b) => {
|
|
6493
|
+
const versionA = BigInt(a.version);
|
|
6494
|
+
const versionB = BigInt(b.version);
|
|
6495
|
+
if (versionA < versionB)
|
|
6496
|
+
return -1;
|
|
6497
|
+
if (versionA > versionB)
|
|
6498
|
+
return 1;
|
|
6499
|
+
return a.file.localeCompare(b.file);
|
|
6500
|
+
});
|
|
6501
|
+
for (let i = 1;i < sorted.length; i += 1) {
|
|
6502
|
+
const previous = sorted[i - 1];
|
|
6503
|
+
const current = sorted[i];
|
|
6504
|
+
if (previous.version === current.version && (previous.file !== current.file || previous.name !== current.name)) {
|
|
6505
|
+
throw new Error(`Migration version collision for ${current.version}: ${previous.file} and ${current.file}`);
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
return sorted;
|
|
6465
6509
|
}
|
|
6466
6510
|
function appliedMigrationKeys(data) {
|
|
6467
6511
|
const rows = Array.isArray(data) ? data : data?.rows || [];
|
|
@@ -6477,6 +6521,43 @@ function appliedMigrationKeys(data) {
|
|
|
6477
6521
|
}
|
|
6478
6522
|
return keys;
|
|
6479
6523
|
}
|
|
6524
|
+
function migrationRows(data) {
|
|
6525
|
+
const rows = Array.isArray(data) ? data : data?.rows || [];
|
|
6526
|
+
return Array.isArray(rows) ? rows.filter((row) => Boolean(row && typeof row === "object")) : [];
|
|
6527
|
+
}
|
|
6528
|
+
function baselineMigrationKey(version, name) {
|
|
6529
|
+
return `${version}\x00${name}`;
|
|
6530
|
+
}
|
|
6531
|
+
function baselineMigrationKeys(data) {
|
|
6532
|
+
const keys = new Set;
|
|
6533
|
+
for (const row of migrationRows(data)) {
|
|
6534
|
+
if (row.version == null || row.name == null || !Array.isArray(row.statements))
|
|
6535
|
+
continue;
|
|
6536
|
+
if (row.statements.length !== 1 || row.statements[0] !== `baseline:${String(row.name)}`)
|
|
6537
|
+
continue;
|
|
6538
|
+
keys.add(baselineMigrationKey(String(row.version), String(row.name)));
|
|
6539
|
+
}
|
|
6540
|
+
return keys;
|
|
6541
|
+
}
|
|
6542
|
+
function historicalChecksumMarkerKeys(data, migrationFiles) {
|
|
6543
|
+
const filesByKey = new Map(migrationFiles.map((migration) => [baselineMigrationKey(migration.version, migration.name), migration]));
|
|
6544
|
+
const keys = new Set;
|
|
6545
|
+
for (const row of migrationRows(data)) {
|
|
6546
|
+
if (row.version == null || row.name == null || !Array.isArray(row.statements) || row.statements.length !== 1)
|
|
6547
|
+
continue;
|
|
6548
|
+
const marker = row.statements[0];
|
|
6549
|
+
if (typeof marker !== "string" || !/^sha256:[0-9a-f]{64}$/.test(marker))
|
|
6550
|
+
continue;
|
|
6551
|
+
const key = baselineMigrationKey(String(row.version), String(row.name));
|
|
6552
|
+
const migration = filesByKey.get(key);
|
|
6553
|
+
if (!migration)
|
|
6554
|
+
continue;
|
|
6555
|
+
const checksum = createHash("sha256").update(migration.rawBytes).digest("hex");
|
|
6556
|
+
if (marker === `sha256:${checksum}`)
|
|
6557
|
+
keys.add(key);
|
|
6558
|
+
}
|
|
6559
|
+
return keys;
|
|
6560
|
+
}
|
|
6480
6561
|
function isAlreadyAppliedMigrationResponse(response) {
|
|
6481
6562
|
if (response.status !== 409 || !response.data || typeof response.data !== "object")
|
|
6482
6563
|
return false;
|
|
@@ -6695,24 +6776,17 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
6695
6776
|
text = `No .sql migration files found in ${dir}`;
|
|
6696
6777
|
break;
|
|
6697
6778
|
}
|
|
6698
|
-
const migrationFiles = files.map((file
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6779
|
+
const migrationFiles = sortMigrationFiles(files.map((file) => readMigrationFile(dir, file)));
|
|
6780
|
+
const migrationsResult = await http.get(`/v1/projects/${ref}/database/migrations`);
|
|
6781
|
+
if (!migrationsResult.ok) {
|
|
6782
|
+
text = `❌ Failed to load applied migrations (${migrationsResult.status}): ${JSON.stringify(migrationsResult.data)}`;
|
|
6783
|
+
break;
|
|
6784
|
+
}
|
|
6703
6785
|
if (args.dry_run) {
|
|
6704
|
-
const
|
|
6705
|
-
if (!r.ok) {
|
|
6706
|
-
text = `❌ Failed to load applied migrations (${r.status}): ${JSON.stringify(r.data)}`;
|
|
6707
|
-
break;
|
|
6708
|
-
}
|
|
6709
|
-
const appliedKeys = appliedMigrationKeys(r.data);
|
|
6786
|
+
const appliedKeys = appliedMigrationKeys(migrationsResult.data);
|
|
6710
6787
|
const pending = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
|
|
6711
6788
|
const alreadyApplied = migrationFiles.filter(({ name, version }) => appliedKeys.has(name) || appliedKeys.has(String(version)));
|
|
6712
|
-
const pendingWithSql = pending
|
|
6713
|
-
...migration,
|
|
6714
|
-
sql: readFileSync2(join(dir, migration.file), "utf-8")
|
|
6715
|
-
}));
|
|
6789
|
+
const pendingWithSql = pending;
|
|
6716
6790
|
let vectorEnabled = null;
|
|
6717
6791
|
if (pendingWithSql.some(({ sql }) => sqlReferencesVector(sql) || sqlCreatesVectorExtension(sql))) {
|
|
6718
6792
|
const extResult = await execSql("SELECT extname AS name FROM pg_extension WHERE extname = 'vector';");
|
|
@@ -6735,8 +6809,14 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
6735
6809
|
}
|
|
6736
6810
|
const applied = [];
|
|
6737
6811
|
const skipped = [];
|
|
6738
|
-
|
|
6739
|
-
|
|
6812
|
+
const baselineKeys = baselineMigrationKeys(migrationsResult.data);
|
|
6813
|
+
const historicalChecksumKeys = historicalChecksumMarkerKeys(migrationsResult.data, migrationFiles);
|
|
6814
|
+
for (const { file, name, version, sql } of migrationFiles) {
|
|
6815
|
+
const migrationKey = baselineMigrationKey(version, name);
|
|
6816
|
+
if (baselineKeys.has(migrationKey) || historicalChecksumKeys.has(migrationKey)) {
|
|
6817
|
+
skipped.push(file);
|
|
6818
|
+
continue;
|
|
6819
|
+
}
|
|
6740
6820
|
const r = await http.post(`/v1/projects/${ref}/database/migrations`, { name, sql, version });
|
|
6741
6821
|
if (r.ok) {
|
|
6742
6822
|
applied.push(file);
|
|
@@ -6774,11 +6854,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
6774
6854
|
text = `No .sql migration files found in ${dir}`;
|
|
6775
6855
|
break;
|
|
6776
6856
|
}
|
|
6777
|
-
const migrationFiles = files.map((file
|
|
6778
|
-
file,
|
|
6779
|
-
name: basename(file, ".sql"),
|
|
6780
|
-
version: migrationVersionFromFilename(file, i)
|
|
6781
|
-
}));
|
|
6857
|
+
const migrationFiles = sortMigrationFiles(files.map((file) => readMigrationFile(dir, file)));
|
|
6782
6858
|
const r = await http.get(`/v1/projects/${ref}/database/migrations`);
|
|
6783
6859
|
if (!r.ok) {
|
|
6784
6860
|
text = `❌ Failed to load applied migrations (${r.status}): ${JSON.stringify(r.data)}`;
|