@supacloud/cli 0.12.0 → 0.12.2
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 +80 -17
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6455,13 +6455,47 @@ 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 migrationVersionFromFilename(file) {
|
|
6461
6466
|
const match = basename(file).match(/^(\d{8,20})[_-]/);
|
|
6462
|
-
if (match)
|
|
6463
|
-
|
|
6464
|
-
|
|
6467
|
+
if (match) {
|
|
6468
|
+
const version = BigInt(match[1]);
|
|
6469
|
+
if (version < 1n || version > MAX_MIGRATION_VERSION) {
|
|
6470
|
+
throw new Error(`Invalid migration version '${match[1]}' in ${basename(file)}: expected 1..${MAX_MIGRATION_VERSION}`);
|
|
6471
|
+
}
|
|
6472
|
+
if (version >= FALLBACK_MIGRATION_VERSION_BASE && version < FALLBACK_MIGRATION_VERSION_LIMIT) {
|
|
6473
|
+
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`);
|
|
6474
|
+
}
|
|
6475
|
+
return version.toString();
|
|
6476
|
+
}
|
|
6477
|
+
const digest = createHash("sha256").update(basename(file)).digest("hex");
|
|
6478
|
+
const offset = BigInt(`0x${digest}`) % FALLBACK_MIGRATION_VERSION_RANGE;
|
|
6479
|
+
return (FALLBACK_MIGRATION_VERSION_BASE + offset).toString();
|
|
6480
|
+
}
|
|
6481
|
+
function sortMigrationFiles(migrations) {
|
|
6482
|
+
const sorted = [...migrations].sort((a, b) => {
|
|
6483
|
+
const versionA = BigInt(a.version);
|
|
6484
|
+
const versionB = BigInt(b.version);
|
|
6485
|
+
if (versionA < versionB)
|
|
6486
|
+
return -1;
|
|
6487
|
+
if (versionA > versionB)
|
|
6488
|
+
return 1;
|
|
6489
|
+
return a.file.localeCompare(b.file);
|
|
6490
|
+
});
|
|
6491
|
+
for (let i = 1;i < sorted.length; i += 1) {
|
|
6492
|
+
const previous = sorted[i - 1];
|
|
6493
|
+
const current = sorted[i];
|
|
6494
|
+
if (previous.version === current.version && (previous.file !== current.file || previous.name !== current.name)) {
|
|
6495
|
+
throw new Error(`Migration version collision for ${current.version}: ${previous.file} and ${current.file}`);
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
return sorted;
|
|
6465
6499
|
}
|
|
6466
6500
|
function appliedMigrationKeys(data) {
|
|
6467
6501
|
const rows = Array.isArray(data) ? data : data?.rows || [];
|
|
@@ -6477,6 +6511,30 @@ function appliedMigrationKeys(data) {
|
|
|
6477
6511
|
}
|
|
6478
6512
|
return keys;
|
|
6479
6513
|
}
|
|
6514
|
+
function migrationRows(data) {
|
|
6515
|
+
const rows = Array.isArray(data) ? data : data?.rows || [];
|
|
6516
|
+
return Array.isArray(rows) ? rows.filter((row) => Boolean(row && typeof row === "object")) : [];
|
|
6517
|
+
}
|
|
6518
|
+
function baselineMigrationKey(version, name) {
|
|
6519
|
+
return `${version}\x00${name}`;
|
|
6520
|
+
}
|
|
6521
|
+
function baselineMigrationKeys(data) {
|
|
6522
|
+
const keys = new Set;
|
|
6523
|
+
for (const row of migrationRows(data)) {
|
|
6524
|
+
if (row.version == null || row.name == null || !Array.isArray(row.statements))
|
|
6525
|
+
continue;
|
|
6526
|
+
if (row.statements.length !== 1 || row.statements[0] !== `baseline:${String(row.name)}`)
|
|
6527
|
+
continue;
|
|
6528
|
+
keys.add(baselineMigrationKey(String(row.version), String(row.name)));
|
|
6529
|
+
}
|
|
6530
|
+
return keys;
|
|
6531
|
+
}
|
|
6532
|
+
function isAlreadyAppliedMigrationResponse(response) {
|
|
6533
|
+
if (response.status !== 409 || !response.data || typeof response.data !== "object")
|
|
6534
|
+
return false;
|
|
6535
|
+
const body = response.data;
|
|
6536
|
+
return body.code === "409" && body.message === "Migration already applied";
|
|
6537
|
+
}
|
|
6480
6538
|
function sqlReferencesVector(sql) {
|
|
6481
6539
|
return /\bvector\s*\(\s*\d+\s*\)/i.test(sql) || /::\s*vector\b/i.test(sql) || /\bvector_(cosine|l2|ip)_ops\b/i.test(sql) || /<=>|<#>|<->/.test(sql);
|
|
6482
6540
|
}
|
|
@@ -6689,18 +6747,18 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
6689
6747
|
text = `No .sql migration files found in ${dir}`;
|
|
6690
6748
|
break;
|
|
6691
6749
|
}
|
|
6692
|
-
const migrationFiles = files.map((file
|
|
6750
|
+
const migrationFiles = sortMigrationFiles(files.map((file) => ({
|
|
6693
6751
|
file,
|
|
6694
6752
|
name: basename(file, ".sql"),
|
|
6695
|
-
version: migrationVersionFromFilename(file
|
|
6696
|
-
}));
|
|
6753
|
+
version: migrationVersionFromFilename(file)
|
|
6754
|
+
})));
|
|
6755
|
+
const migrationsResult = await http.get(`/v1/projects/${ref}/database/migrations`);
|
|
6756
|
+
if (!migrationsResult.ok) {
|
|
6757
|
+
text = `❌ Failed to load applied migrations (${migrationsResult.status}): ${JSON.stringify(migrationsResult.data)}`;
|
|
6758
|
+
break;
|
|
6759
|
+
}
|
|
6697
6760
|
if (args.dry_run) {
|
|
6698
|
-
const
|
|
6699
|
-
if (!r.ok) {
|
|
6700
|
-
text = `❌ Failed to load applied migrations (${r.status}): ${JSON.stringify(r.data)}`;
|
|
6701
|
-
break;
|
|
6702
|
-
}
|
|
6703
|
-
const appliedKeys = appliedMigrationKeys(r.data);
|
|
6761
|
+
const appliedKeys = appliedMigrationKeys(migrationsResult.data);
|
|
6704
6762
|
const pending = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
|
|
6705
6763
|
const alreadyApplied = migrationFiles.filter(({ name, version }) => appliedKeys.has(name) || appliedKeys.has(String(version)));
|
|
6706
6764
|
const pendingWithSql = pending.map((migration) => ({
|
|
@@ -6729,12 +6787,17 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
6729
6787
|
}
|
|
6730
6788
|
const applied = [];
|
|
6731
6789
|
const skipped = [];
|
|
6790
|
+
const baselineKeys = baselineMigrationKeys(migrationsResult.data);
|
|
6732
6791
|
for (const { file, name, version } of migrationFiles) {
|
|
6792
|
+
if (baselineKeys.has(baselineMigrationKey(version, name))) {
|
|
6793
|
+
skipped.push(file);
|
|
6794
|
+
continue;
|
|
6795
|
+
}
|
|
6733
6796
|
const sql = readFileSync2(join(dir, file), "utf-8");
|
|
6734
6797
|
const r = await http.post(`/v1/projects/${ref}/database/migrations`, { name, sql, version });
|
|
6735
6798
|
if (r.ok) {
|
|
6736
6799
|
applied.push(file);
|
|
6737
|
-
} else if (r
|
|
6800
|
+
} else if (isAlreadyAppliedMigrationResponse(r)) {
|
|
6738
6801
|
skipped.push(file);
|
|
6739
6802
|
} else {
|
|
6740
6803
|
text = [
|
|
@@ -6768,11 +6831,11 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
6768
6831
|
text = `No .sql migration files found in ${dir}`;
|
|
6769
6832
|
break;
|
|
6770
6833
|
}
|
|
6771
|
-
const migrationFiles = files.map((file
|
|
6834
|
+
const migrationFiles = sortMigrationFiles(files.map((file) => ({
|
|
6772
6835
|
file,
|
|
6773
6836
|
name: basename(file, ".sql"),
|
|
6774
|
-
version: migrationVersionFromFilename(file
|
|
6775
|
-
}));
|
|
6837
|
+
version: migrationVersionFromFilename(file)
|
|
6838
|
+
})));
|
|
6776
6839
|
const r = await http.get(`/v1/projects/${ref}/database/migrations`);
|
|
6777
6840
|
if (!r.ok) {
|
|
6778
6841
|
text = `❌ Failed to load applied migrations (${r.status}): ${JSON.stringify(r.data)}`;
|