@databricks-solutions/lakebase-scm-utils 0.2.51 → 0.3.0
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/scripts/index.cjs +2632 -2120
- package/dist/scripts/index.d.cts +1 -1
- package/dist/scripts/index.d.ts +1 -1
- package/dist/scripts/index.js +676 -174
- package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +2460 -2030
- package/dist/scripts/lakebase/ci-resolve-branch.cli.js +508 -81
- package/dist/scripts/lakebase/collapse-heads.cli.cjs +2429 -2007
- package/dist/scripts/lakebase/collapse-heads.cli.js +477 -58
- package/dist/scripts/lakebase/detect-language.cli.cjs +2441 -2016
- package/dist/scripts/lakebase/detect-language.cli.js +489 -67
- package/dist/scripts/lakebase/doctor.cli.cjs +2484 -2040
- package/dist/scripts/lakebase/doctor.cli.js +532 -91
- package/dist/scripts/lakebase/get-connection.cli.cjs +2 -2
- package/dist/scripts/lakebase/get-connection.cli.js +2 -2
- package/dist/scripts/lakebase/index.cjs +2629 -2117
- package/dist/scripts/lakebase/index.d.cts +59 -10
- package/dist/scripts/lakebase/index.d.ts +59 -10
- package/dist/scripts/lakebase/index.js +670 -168
- package/dist/scripts/lakebase/infra-runner.cli.cjs +2435 -2013
- package/dist/scripts/lakebase/infra-runner.cli.js +483 -64
- package/dist/scripts/lakebase/new-migration.cli.cjs +2429 -2007
- package/dist/scripts/lakebase/new-migration.cli.js +477 -58
- package/dist/scripts/lakebase/schema-diff.cli.cjs +2 -2
- package/dist/scripts/lakebase/schema-diff.cli.js +2 -2
- package/dist/scripts/lakebase/schema-migrate.cli.cjs +2461 -2029
- package/dist/scripts/lakebase/schema-migrate.cli.js +509 -80
- package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +2460 -2030
- package/dist/scripts/lakebase/scm-claim-feature.cli.js +508 -81
- package/dist/scripts/lakebase/scm-doctor.cli.cjs +2486 -2056
- package/dist/scripts/lakebase/scm-doctor.cli.js +537 -107
- package/dist/scripts/lakebase/scm-merge.cli.cjs +2443 -2021
- package/dist/scripts/lakebase/scm-merge.cli.js +494 -72
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +2464 -2034
- package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +511 -84
- package/dist/scripts/util/index.cjs +2 -2
- package/dist/scripts/util/index.js +2 -2
- package/package.json +1 -1
- package/scripts/lakebase/adapters/efcore-adapter.ts +158 -0
- package/scripts/lakebase/adapters/flyway-adapter.ts +8 -5
- package/scripts/lakebase/collapse-heads.cli.ts +1 -1
- package/scripts/lakebase/create-project.ts +6 -2
- package/scripts/lakebase/detect-language.cli.ts +2 -2
- package/scripts/lakebase/doctor.ts +29 -0
- package/scripts/lakebase/dotnet-project.ts +112 -0
- package/scripts/lakebase/index.ts +1 -0
- package/scripts/lakebase/migration-layout.ts +43 -2
- package/scripts/lakebase/new-migration.cli.ts +1 -1
- package/scripts/lakebase/scaffold-language.ts +23 -2
- package/scripts/lakebase/scaffold.ts +4 -1
- package/scripts/lakebase/schema-migrate-runners/efcore.ts +399 -0
- package/scripts/lakebase/schema-migrate-runners/flyway.ts +6 -4
- package/scripts/lakebase/schema-migrate.cli.ts +3 -1
- package/scripts/lakebase/schema-migrate.ts +24 -10
- package/templates/project/common/.github/workflows/merge.yml +13 -1
- package/templates/project/common/.github/workflows/pr.yml +25 -5
- package/templates/project/common/scripts/flyway-migrate.sh +54 -1
- package/templates/project/common/scripts/prepare-schema-diff.sh +5 -0
- package/templates/project/common/scripts/run-dev.sh +10 -1
- package/templates/project/common/scripts/run-tests.sh +6 -1
- package/templates/project/dotnet/.gitignore.extra +8 -0
- package/templates/project/dotnet/App.sln +56 -0
- package/templates/project/dotnet/Makefile +13 -0
- package/templates/project/dotnet/global.json +6 -0
- package/templates/project/dotnet/src/App/Data/AppDbContext.cs +12 -0
- package/templates/project/dotnet/src/App/Lakebase/DotEnv.cs +48 -0
- package/templates/project/dotnet/src/App/Lakebase/LakebaseConnection.cs +225 -0
- package/templates/project/dotnet/src/App/Program.cs +34 -0
- package/templates/project/dotnet/src/App/appsettings.json +9 -0
- package/templates/project/dotnet/tests/App.Tests/App.Tests.csproj +25 -0
- package/templates/project/dotnet/tests/App.Tests/HealthTests.cs +18 -0
- package/templates/project/dotnet/tests/App.Tests/OpenApiTests.cs +23 -0
- package/templates/project/dotnet-efcore/.config/dotnet-tools.json +13 -0
- package/templates/project/dotnet-efcore/src/App/App.csproj +22 -0
- package/templates/project/dotnet-efcore/src/App/Data/DesignTimeDbContextFactory.cs +24 -0
- package/templates/project/dotnet-efcore/src/App/Migrations/README.md +23 -0
- package/templates/project/dotnet-flyway/db/migrations/README.md +23 -0
- package/templates/project/dotnet-flyway/src/App/App.csproj +17 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// scripts/lakebase/doctor.cli.ts
|
|
4
|
-
import * as
|
|
4
|
+
import * as path17 from "path";
|
|
5
5
|
|
|
6
6
|
// scripts/lakebase/doctor.ts
|
|
7
|
-
import * as
|
|
8
|
-
import * as
|
|
7
|
+
import * as fs15 from "fs";
|
|
8
|
+
import * as path16 from "path";
|
|
9
9
|
import * as cp2 from "child_process";
|
|
10
10
|
|
|
11
11
|
// scripts/util/exec.ts
|
|
@@ -351,9 +351,9 @@ function asBranchUid(s) {
|
|
|
351
351
|
}
|
|
352
352
|
return s;
|
|
353
353
|
}
|
|
354
|
-
function branchNameFromResourcePath(
|
|
355
|
-
if (!
|
|
356
|
-
const leaf =
|
|
354
|
+
function branchNameFromResourcePath(path18) {
|
|
355
|
+
if (!path18.includes("/branches/")) return null;
|
|
356
|
+
const leaf = path18.split("/branches/").pop();
|
|
357
357
|
if (!leaf) return null;
|
|
358
358
|
try {
|
|
359
359
|
return asBranchName(leaf);
|
|
@@ -460,8 +460,8 @@ function verifyHooks(projectDir) {
|
|
|
460
460
|
}
|
|
461
461
|
|
|
462
462
|
// scripts/lakebase/schema-migrate.ts
|
|
463
|
-
import * as
|
|
464
|
-
import * as
|
|
463
|
+
import * as fs13 from "fs";
|
|
464
|
+
import * as path14 from "path";
|
|
465
465
|
|
|
466
466
|
// scripts/lakebase/get-connection.ts
|
|
467
467
|
import { createLakebasePool } from "@databricks/lakebase";
|
|
@@ -484,8 +484,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
|
|
|
484
484
|
var cached;
|
|
485
485
|
function substrateSelfVersion() {
|
|
486
486
|
if (cached !== void 0) return cached;
|
|
487
|
-
if ("0.
|
|
488
|
-
cached = "0.
|
|
487
|
+
if ("0.3.0".length > 0) {
|
|
488
|
+
cached = "0.3.0";
|
|
489
489
|
return cached;
|
|
490
490
|
}
|
|
491
491
|
cached = "unknown";
|
|
@@ -594,23 +594,88 @@ function dbcli2(args) {
|
|
|
594
594
|
}
|
|
595
595
|
|
|
596
596
|
// scripts/lakebase/migration-layout.ts
|
|
597
|
+
import * as fs6 from "fs";
|
|
598
|
+
import * as path5 from "path";
|
|
599
|
+
|
|
600
|
+
// scripts/lakebase/dotnet-project.ts
|
|
597
601
|
import * as fs5 from "fs";
|
|
598
602
|
import * as path4 from "path";
|
|
603
|
+
var EF_DESIGN_PACKAGE = "Microsoft.EntityFrameworkCore.Design";
|
|
604
|
+
var DOTNET_FLYWAY_DIR = path4.join("db", "migrations");
|
|
605
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["bin", "obj", "node_modules", ".git", "client", "tests", "test"]);
|
|
606
|
+
function safeReaddir(dir) {
|
|
607
|
+
try {
|
|
608
|
+
return fs5.readdirSync(dir, { withFileTypes: true });
|
|
609
|
+
} catch {
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function hasDotnetMarker(dir) {
|
|
614
|
+
return safeReaddir(dir).some(
|
|
615
|
+
(e) => e.isFile() && /\.(sln|slnx|csproj)$/i.test(e.name)
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
function findCsprojFiles(projectDir) {
|
|
619
|
+
const found = [];
|
|
620
|
+
const walk = (dir, depth) => {
|
|
621
|
+
for (const e of safeReaddir(dir)) {
|
|
622
|
+
const full = path4.join(dir, e.name);
|
|
623
|
+
if (e.isFile() && e.name.toLowerCase().endsWith(".csproj")) {
|
|
624
|
+
found.push(full);
|
|
625
|
+
} else if (e.isDirectory() && depth < 2 && !SKIP_DIRS.has(e.name.toLowerCase()) && !e.name.startsWith(".")) {
|
|
626
|
+
walk(full, depth + 1);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
walk(projectDir, 0);
|
|
631
|
+
return found.sort();
|
|
632
|
+
}
|
|
633
|
+
function findEfCoreProject(projectDir) {
|
|
634
|
+
for (const csproj of findCsprojFiles(projectDir)) {
|
|
635
|
+
let content = "";
|
|
636
|
+
try {
|
|
637
|
+
content = fs5.readFileSync(csproj, "utf-8");
|
|
638
|
+
} catch {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
if (content.includes(EF_DESIGN_PACKAGE)) {
|
|
642
|
+
const dir = path4.dirname(csproj);
|
|
643
|
+
return { csproj, projectDir: dir, migrationsDir: path4.join(dir, "Migrations") };
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
function isDotnetProject(projectDir) {
|
|
649
|
+
return hasDotnetMarker(projectDir);
|
|
650
|
+
}
|
|
651
|
+
function isDotnetFlywayProject(projectDir) {
|
|
652
|
+
return isDotnetProject(projectDir) && fs5.existsSync(path4.join(projectDir, DOTNET_FLYWAY_DIR));
|
|
653
|
+
}
|
|
654
|
+
var MAVEN_FLYWAY_DIR = path4.join("src", "main", "resources", "db", "migration");
|
|
655
|
+
function flywayMigrationsDir(projectDir) {
|
|
656
|
+
const maven = path4.join(projectDir, MAVEN_FLYWAY_DIR);
|
|
657
|
+
if (fs5.existsSync(maven) || fs5.existsSync(path4.join(projectDir, "pom.xml"))) return maven;
|
|
658
|
+
if (isDotnetFlywayProject(projectDir)) return path4.join(projectDir, DOTNET_FLYWAY_DIR);
|
|
659
|
+
return maven;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// scripts/lakebase/migration-layout.ts
|
|
599
663
|
var MIGRATION_LANGUAGES = [
|
|
600
664
|
"java",
|
|
601
665
|
"kotlin",
|
|
602
666
|
"python",
|
|
603
667
|
"nodejs",
|
|
668
|
+
"dotnet",
|
|
604
669
|
"unknown"
|
|
605
670
|
];
|
|
606
671
|
function detectLanguageAt(dir) {
|
|
607
|
-
if (
|
|
608
|
-
const kotlinDir =
|
|
609
|
-
if (
|
|
672
|
+
if (fs6.existsSync(path5.join(dir, "pom.xml"))) {
|
|
673
|
+
const kotlinDir = path5.join(dir, "src", "main", "kotlin");
|
|
674
|
+
if (fs6.existsSync(kotlinDir)) {
|
|
610
675
|
return "kotlin";
|
|
611
676
|
}
|
|
612
677
|
try {
|
|
613
|
-
const pom =
|
|
678
|
+
const pom = fs6.readFileSync(path5.join(dir, "pom.xml"), "utf-8");
|
|
614
679
|
if (pom.includes("kotlin-maven-plugin")) {
|
|
615
680
|
return "kotlin";
|
|
616
681
|
}
|
|
@@ -618,10 +683,13 @@ function detectLanguageAt(dir) {
|
|
|
618
683
|
}
|
|
619
684
|
return "java";
|
|
620
685
|
}
|
|
621
|
-
if (
|
|
686
|
+
if (fs6.existsSync(path5.join(dir, "pyproject.toml")) || fs6.existsSync(path5.join(dir, "requirements.txt")) || fs6.existsSync(path5.join(dir, "alembic.ini"))) {
|
|
622
687
|
return "python";
|
|
623
688
|
}
|
|
624
|
-
if (
|
|
689
|
+
if (hasDotnetMarker(dir)) {
|
|
690
|
+
return "dotnet";
|
|
691
|
+
}
|
|
692
|
+
if (fs6.existsSync(path5.join(dir, "package.json"))) {
|
|
625
693
|
return "nodejs";
|
|
626
694
|
}
|
|
627
695
|
return "unknown";
|
|
@@ -642,14 +710,14 @@ function resolveMigrationLanguage(projectDir, configuredMigrationPath, override)
|
|
|
642
710
|
if (!rel) {
|
|
643
711
|
return "unknown";
|
|
644
712
|
}
|
|
645
|
-
const rootResolved =
|
|
646
|
-
let dir =
|
|
647
|
-
while (dir === rootResolved || dir.startsWith(rootResolved +
|
|
713
|
+
const rootResolved = path5.resolve(projectDir);
|
|
714
|
+
let dir = path5.resolve(projectDir, rel);
|
|
715
|
+
while (dir === rootResolved || dir.startsWith(rootResolved + path5.sep)) {
|
|
648
716
|
const lang = detectLanguageAt(dir);
|
|
649
717
|
if (lang !== "unknown") {
|
|
650
718
|
return lang;
|
|
651
719
|
}
|
|
652
|
-
const parent =
|
|
720
|
+
const parent = path5.dirname(dir);
|
|
653
721
|
if (parent === dir) {
|
|
654
722
|
break;
|
|
655
723
|
}
|
|
@@ -659,21 +727,21 @@ function resolveMigrationLanguage(projectDir, configuredMigrationPath, override)
|
|
|
659
727
|
}
|
|
660
728
|
|
|
661
729
|
// scripts/lakebase/adapters/alembic-adapter.ts
|
|
662
|
-
import * as
|
|
663
|
-
import * as
|
|
730
|
+
import * as fs8 from "fs";
|
|
731
|
+
import * as path7 from "path";
|
|
664
732
|
|
|
665
733
|
// scripts/lakebase/schema-migrate-runners/alembic.ts
|
|
666
734
|
import { spawn } from "child_process";
|
|
667
|
-
import * as
|
|
668
|
-
import * as
|
|
735
|
+
import * as fs7 from "fs";
|
|
736
|
+
import * as path6 from "path";
|
|
669
737
|
function resolveAlembicBin(projectDir) {
|
|
670
738
|
const candidates = [
|
|
671
|
-
|
|
672
|
-
|
|
739
|
+
path6.join(projectDir, ".venv", "bin", "alembic"),
|
|
740
|
+
path6.join(projectDir, "venv", "bin", "alembic")
|
|
673
741
|
];
|
|
674
742
|
for (const candidate of candidates) {
|
|
675
743
|
try {
|
|
676
|
-
if (
|
|
744
|
+
if (fs7.existsSync(candidate)) return candidate;
|
|
677
745
|
} catch {
|
|
678
746
|
}
|
|
679
747
|
}
|
|
@@ -683,7 +751,7 @@ function spawnAlembic(projectDir, args, dsn) {
|
|
|
683
751
|
return new Promise((resolve2, reject) => {
|
|
684
752
|
const bin = resolveAlembicBin(projectDir);
|
|
685
753
|
const env = { ...process.env };
|
|
686
|
-
env.PYTHONPATH = [projectDir, process.env.PYTHONPATH].filter(Boolean).join(
|
|
754
|
+
env.PYTHONPATH = [projectDir, process.env.PYTHONPATH].filter(Boolean).join(path6.delimiter);
|
|
687
755
|
if (dsn) env.DATABASE_URL = dsn;
|
|
688
756
|
const child = spawn(bin, args, {
|
|
689
757
|
cwd: projectDir,
|
|
@@ -731,10 +799,10 @@ async function createAlembicRevision(opts) {
|
|
|
731
799
|
const m = stdout.match(/Generating\s+(\S+\.py)/);
|
|
732
800
|
if (m) return m[1].trim();
|
|
733
801
|
for (const rel of ["migrations/versions", "alembic/versions"]) {
|
|
734
|
-
const dir =
|
|
735
|
-
if (!
|
|
736
|
-
const hit =
|
|
737
|
-
if (hit) return
|
|
802
|
+
const dir = path6.join(opts.projectDir, rel);
|
|
803
|
+
if (!fs7.existsSync(dir)) continue;
|
|
804
|
+
const hit = fs7.readdirSync(dir).find((f) => f.startsWith(`${opts.revId}_`) && f.endsWith(".py"));
|
|
805
|
+
if (hit) return path6.join(dir, hit);
|
|
738
806
|
}
|
|
739
807
|
throw new SchemaMigrationError(
|
|
740
808
|
`alembic revision succeeded but the created file could not be located.
|
|
@@ -845,15 +913,15 @@ async function buildDsn(args) {
|
|
|
845
913
|
}
|
|
846
914
|
function findVersionsDir(projectDir) {
|
|
847
915
|
const candidates = [
|
|
848
|
-
|
|
849
|
-
|
|
916
|
+
path7.join(projectDir, "migrations", "versions"),
|
|
917
|
+
path7.join(projectDir, "alembic", "versions")
|
|
850
918
|
];
|
|
851
|
-
return candidates.find((p) =>
|
|
919
|
+
return candidates.find((p) => fs8.existsSync(p));
|
|
852
920
|
}
|
|
853
921
|
function listAlembicFiles(projectDir) {
|
|
854
922
|
const dir = findVersionsDir(projectDir);
|
|
855
923
|
if (!dir) return [];
|
|
856
|
-
const files =
|
|
924
|
+
const files = fs8.readdirSync(dir).filter((f) => f.endsWith(".py") && !f.startsWith("__"));
|
|
857
925
|
return files.map((filename) => {
|
|
858
926
|
const stem = filename.replace(/\.py$/, "");
|
|
859
927
|
const sep2 = stem.indexOf("_");
|
|
@@ -878,9 +946,9 @@ var AlembicAdapter = {
|
|
|
878
946
|
* here. Callers can still force-select via project.yaml#migration_tool.
|
|
879
947
|
*/
|
|
880
948
|
detect(projectDir) {
|
|
881
|
-
if (
|
|
882
|
-
if (
|
|
883
|
-
if (
|
|
949
|
+
if (fs8.existsSync(path7.join(projectDir, "alembic.ini"))) return true;
|
|
950
|
+
if (fs8.existsSync(path7.join(projectDir, "migrations", "env.py"))) return true;
|
|
951
|
+
if (fs8.existsSync(path7.join(projectDir, "alembic", "env.py"))) return true;
|
|
884
952
|
return false;
|
|
885
953
|
},
|
|
886
954
|
async apply(args) {
|
|
@@ -986,7 +1054,7 @@ var AlembicAdapter = {
|
|
|
986
1054
|
autogenerate: !!args.autogenerate,
|
|
987
1055
|
dsn
|
|
988
1056
|
});
|
|
989
|
-
return { status: "ok", version: revId, filename:
|
|
1057
|
+
return { status: "ok", version: revId, filename: path7.basename(created), path: created };
|
|
990
1058
|
} catch (err) {
|
|
991
1059
|
return {
|
|
992
1060
|
status: "error",
|
|
@@ -1003,7 +1071,7 @@ var AlembicAdapter = {
|
|
|
1003
1071
|
if (heads.length <= 1) return { status: "noop", headsBefore: heads };
|
|
1004
1072
|
if (args.dryRun) return { status: "ok", headsBefore: heads };
|
|
1005
1073
|
const created = await mergeAlembicHeads(args.projectDir, args.message ?? "merge heads");
|
|
1006
|
-
const mergeRevision =
|
|
1074
|
+
const mergeRevision = path7.basename(created).replace(/\.py$/, "").split("_")[0];
|
|
1007
1075
|
return { status: "ok", headsBefore: heads, mergeRevision, path: created };
|
|
1008
1076
|
} catch (err) {
|
|
1009
1077
|
return {
|
|
@@ -1017,12 +1085,12 @@ var AlembicAdapter = {
|
|
|
1017
1085
|
registerSchemaMigrationAdapter(AlembicAdapter);
|
|
1018
1086
|
|
|
1019
1087
|
// scripts/lakebase/adapters/flyway-adapter.ts
|
|
1020
|
-
import * as
|
|
1021
|
-
import * as
|
|
1088
|
+
import * as fs9 from "fs";
|
|
1089
|
+
import * as path9 from "path";
|
|
1022
1090
|
|
|
1023
1091
|
// scripts/lakebase/schema-migrate-runners/flyway.ts
|
|
1024
1092
|
import { spawn as spawn2 } from "child_process";
|
|
1025
|
-
import * as
|
|
1093
|
+
import * as path8 from "path";
|
|
1026
1094
|
function dsnToFlywayEnv(dsn) {
|
|
1027
1095
|
const u = new URL(dsn);
|
|
1028
1096
|
const user = decodeURIComponent(u.username);
|
|
@@ -1032,7 +1100,7 @@ function dsnToFlywayEnv(dsn) {
|
|
|
1032
1100
|
return { url, user, password };
|
|
1033
1101
|
}
|
|
1034
1102
|
function migrationsLocation(projectDir) {
|
|
1035
|
-
return `filesystem:${
|
|
1103
|
+
return `filesystem:${flywayMigrationsDir(projectDir)}`;
|
|
1036
1104
|
}
|
|
1037
1105
|
function runFlyway(ctx, args) {
|
|
1038
1106
|
const { url, user, password } = dsnToFlywayEnv(ctx.dsn);
|
|
@@ -1133,7 +1201,7 @@ async function statusFlyway(ctx) {
|
|
|
1133
1201
|
if (state === "SUCCESS" || state === "BASELINE") {
|
|
1134
1202
|
current = m.version;
|
|
1135
1203
|
} else if (state === "PENDING") {
|
|
1136
|
-
const filename = m.filepath ?
|
|
1204
|
+
const filename = m.filepath ? path8.basename(m.filepath) : `V${m.version}__migration.sql`;
|
|
1137
1205
|
pending.push({
|
|
1138
1206
|
version: m.version,
|
|
1139
1207
|
filename,
|
|
@@ -1156,9 +1224,9 @@ async function buildDsn2(args) {
|
|
|
1156
1224
|
return result.url;
|
|
1157
1225
|
}
|
|
1158
1226
|
function listFlywayFiles(projectDir) {
|
|
1159
|
-
const dir =
|
|
1160
|
-
if (!
|
|
1161
|
-
const files =
|
|
1227
|
+
const dir = flywayMigrationsDir(projectDir);
|
|
1228
|
+
if (!fs9.existsSync(dir)) return [];
|
|
1229
|
+
const files = fs9.readdirSync(dir).filter((f) => /^V\d+(\.\d+)*__.+\.sql$/.test(f));
|
|
1162
1230
|
return files.map((filename) => {
|
|
1163
1231
|
const m = filename.match(/^V(\d+(?:\.\d+)*)__(.+)\.sql$/);
|
|
1164
1232
|
const version = m[1];
|
|
@@ -1179,9 +1247,9 @@ function versionCompare(a, b) {
|
|
|
1179
1247
|
}
|
|
1180
1248
|
var FlywayAdapter = {
|
|
1181
1249
|
id: "flyway",
|
|
1182
|
-
languages: ["java", "kotlin"],
|
|
1250
|
+
languages: ["java", "kotlin", "dotnet"],
|
|
1183
1251
|
detect(projectDir) {
|
|
1184
|
-
return
|
|
1252
|
+
return fs9.existsSync(path9.join(projectDir, "pom.xml")) || isDotnetFlywayProject(projectDir);
|
|
1185
1253
|
},
|
|
1186
1254
|
async apply(args) {
|
|
1187
1255
|
const dsn = await buildDsn2(args);
|
|
@@ -1239,14 +1307,14 @@ var FlywayAdapter = {
|
|
|
1239
1307
|
// optional-protocol shape makes this additive.
|
|
1240
1308
|
async newMigration(args) {
|
|
1241
1309
|
try {
|
|
1242
|
-
const dir =
|
|
1243
|
-
|
|
1310
|
+
const dir = flywayMigrationsDir(args.projectDir);
|
|
1311
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
1244
1312
|
const version = migrationTimestamp();
|
|
1245
1313
|
const slug = migrationSlug2(args.slug);
|
|
1246
1314
|
const filename = `V${version}__${slug}.sql`;
|
|
1247
|
-
const full =
|
|
1248
|
-
if (
|
|
1249
|
-
|
|
1315
|
+
const full = path9.join(dir, filename);
|
|
1316
|
+
if (fs9.existsSync(full)) throw new Error(`${filename} already exists`);
|
|
1317
|
+
fs9.writeFileSync(
|
|
1250
1318
|
full,
|
|
1251
1319
|
`-- V${version}: ${args.slug}
|
|
1252
1320
|
-- Flyway migration (write your DDL/DML below).
|
|
@@ -1268,18 +1336,18 @@ var FlywayAdapter = {
|
|
|
1268
1336
|
registerSchemaMigrationAdapter(FlywayAdapter);
|
|
1269
1337
|
|
|
1270
1338
|
// scripts/lakebase/adapters/knex-adapter.ts
|
|
1271
|
-
import * as
|
|
1272
|
-
import * as
|
|
1339
|
+
import * as fs11 from "fs";
|
|
1340
|
+
import * as path11 from "path";
|
|
1273
1341
|
|
|
1274
1342
|
// scripts/lakebase/schema-migrate-runners/knex.ts
|
|
1275
1343
|
import { spawn as spawn3 } from "child_process";
|
|
1276
|
-
import * as
|
|
1277
|
-
import * as
|
|
1344
|
+
import * as fs10 from "fs";
|
|
1345
|
+
import * as path10 from "path";
|
|
1278
1346
|
var KNEXFILE_VARIANTS = ["knexfile.js", "knexfile.ts", "knexfile.mjs", "knexfile.cjs"];
|
|
1279
1347
|
function findKnexfile(projectDir) {
|
|
1280
1348
|
for (const name of KNEXFILE_VARIANTS) {
|
|
1281
|
-
const p =
|
|
1282
|
-
if (
|
|
1349
|
+
const p = path10.join(projectDir, name);
|
|
1350
|
+
if (fs10.existsSync(p)) return p;
|
|
1283
1351
|
}
|
|
1284
1352
|
return void 0;
|
|
1285
1353
|
}
|
|
@@ -1431,9 +1499,9 @@ async function buildDsn3(args) {
|
|
|
1431
1499
|
}
|
|
1432
1500
|
var KNEXFILE_VARIANTS2 = ["knexfile.js", "knexfile.ts", "knexfile.mjs", "knexfile.cjs"];
|
|
1433
1501
|
function listKnexFiles(projectDir) {
|
|
1434
|
-
const dir =
|
|
1435
|
-
if (!
|
|
1436
|
-
const files =
|
|
1502
|
+
const dir = path11.join(projectDir, "migrations");
|
|
1503
|
+
if (!fs11.existsSync(dir)) return [];
|
|
1504
|
+
const files = fs11.readdirSync(dir).filter((f) => (f.endsWith(".js") || f.endsWith(".ts")) && !f.startsWith("."));
|
|
1437
1505
|
return files.map((filename) => {
|
|
1438
1506
|
const stem = filename.replace(/\.(js|ts)$/, "");
|
|
1439
1507
|
const m = stem.match(/^(\d{14})_(.+)$/);
|
|
@@ -1453,7 +1521,7 @@ var KnexAdapter = {
|
|
|
1453
1521
|
* project.yaml#migration_tool.
|
|
1454
1522
|
*/
|
|
1455
1523
|
detect(projectDir) {
|
|
1456
|
-
return KNEXFILE_VARIANTS2.some((name) =>
|
|
1524
|
+
return KNEXFILE_VARIANTS2.some((name) => fs11.existsSync(path11.join(projectDir, name)));
|
|
1457
1525
|
},
|
|
1458
1526
|
async apply(args) {
|
|
1459
1527
|
const dsn = await buildDsn3(args);
|
|
@@ -1526,9 +1594,9 @@ var KnexAdapter = {
|
|
|
1526
1594
|
async newMigration(args) {
|
|
1527
1595
|
try {
|
|
1528
1596
|
const created = await createKnexMigration({ projectDir: args.projectDir, slug: migrationSlug2(args.slug) });
|
|
1529
|
-
const stem =
|
|
1597
|
+
const stem = path11.basename(created).replace(/\.(js|ts)$/, "");
|
|
1530
1598
|
const version = stem.match(/^(\d{14})_/)?.[1] ?? stem;
|
|
1531
|
-
return { status: "ok", version, filename:
|
|
1599
|
+
return { status: "ok", version, filename: path11.basename(created), path: created };
|
|
1532
1600
|
} catch (err) {
|
|
1533
1601
|
return {
|
|
1534
1602
|
status: "error",
|
|
@@ -1542,6 +1610,360 @@ var KnexAdapter = {
|
|
|
1542
1610
|
};
|
|
1543
1611
|
registerSchemaMigrationAdapter(KnexAdapter);
|
|
1544
1612
|
|
|
1613
|
+
// scripts/lakebase/adapters/efcore-adapter.ts
|
|
1614
|
+
import * as path13 from "path";
|
|
1615
|
+
|
|
1616
|
+
// scripts/lakebase/schema-migrate-runners/efcore.ts
|
|
1617
|
+
import { spawn as spawn4 } from "child_process";
|
|
1618
|
+
import * as fs12 from "fs";
|
|
1619
|
+
import * as path12 from "path";
|
|
1620
|
+
import { Client as Client2 } from "pg";
|
|
1621
|
+
var MIGRATION_ID = /^(\d{14})_(.+)$/;
|
|
1622
|
+
function efVersion(id) {
|
|
1623
|
+
return MIGRATION_ID.exec(id)?.[1] ?? id;
|
|
1624
|
+
}
|
|
1625
|
+
function efDescription(id) {
|
|
1626
|
+
const name = MIGRATION_ID.exec(id)?.[2] ?? id;
|
|
1627
|
+
return name.replace(/_/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2");
|
|
1628
|
+
}
|
|
1629
|
+
function dsnToNpgsql(dsn) {
|
|
1630
|
+
const u = new URL(dsn);
|
|
1631
|
+
const parts = [
|
|
1632
|
+
`Host=${u.hostname}`,
|
|
1633
|
+
`Port=${u.port || "5432"}`,
|
|
1634
|
+
`Database=${decodeURIComponent(u.pathname.replace(/^\//, "")) || "databricks_postgres"}`
|
|
1635
|
+
];
|
|
1636
|
+
if (u.username) parts.push(`Username=${decodeURIComponent(u.username)}`);
|
|
1637
|
+
if (u.password) parts.push(`Password=${decodeURIComponent(u.password)}`);
|
|
1638
|
+
const sslmode = (u.searchParams.get("sslmode") ?? "require").toLowerCase();
|
|
1639
|
+
const ssl = {
|
|
1640
|
+
disable: "Disable",
|
|
1641
|
+
allow: "Allow",
|
|
1642
|
+
prefer: "Prefer",
|
|
1643
|
+
require: "Require",
|
|
1644
|
+
"verify-ca": "VerifyCA",
|
|
1645
|
+
"verify-full": "VerifyFull"
|
|
1646
|
+
};
|
|
1647
|
+
parts.push(`SSL Mode=${ssl[sslmode] ?? "Require"}`);
|
|
1648
|
+
const app = u.searchParams.get("application_name");
|
|
1649
|
+
if (app) parts.push(`Application Name=${app}`);
|
|
1650
|
+
return parts.map((p) => p.replace(/;/g, "")).join(";");
|
|
1651
|
+
}
|
|
1652
|
+
function parseEfJson(stdout) {
|
|
1653
|
+
const body = stdout.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice("data:".length)).join("\n").trim();
|
|
1654
|
+
if (!body) {
|
|
1655
|
+
throw new SchemaMigrationError(`dotnet ef returned no JSON payload: ${stdout.slice(0, 400)}`);
|
|
1656
|
+
}
|
|
1657
|
+
try {
|
|
1658
|
+
return JSON.parse(body);
|
|
1659
|
+
} catch (err) {
|
|
1660
|
+
throw new SchemaMigrationError(
|
|
1661
|
+
`dotnet ef JSON parse failed: ${err instanceof Error ? err.message : String(err)}.
|
|
1662
|
+
Body (first 400 chars): ${body.slice(0, 400)}`
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
function requireProject(projectDir) {
|
|
1667
|
+
const project = findEfCoreProject(projectDir);
|
|
1668
|
+
if (!project) {
|
|
1669
|
+
throw new SchemaMigrationError(
|
|
1670
|
+
`No EF Core project found under ${projectDir}. Expected a *.csproj that references ${EF_DESIGN_PACKAGE}.`
|
|
1671
|
+
);
|
|
1672
|
+
}
|
|
1673
|
+
return project;
|
|
1674
|
+
}
|
|
1675
|
+
function spawnDotnet(projectDir, args, env) {
|
|
1676
|
+
return new Promise((resolve2, reject) => {
|
|
1677
|
+
const child = spawn4("dotnet", args, {
|
|
1678
|
+
cwd: projectDir,
|
|
1679
|
+
env: { ...process.env, DOTNET_NOLOGO: "1", DOTNET_CLI_TELEMETRY_OPTOUT: "1", ...env },
|
|
1680
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1681
|
+
});
|
|
1682
|
+
let stdout = "";
|
|
1683
|
+
let stderr = "";
|
|
1684
|
+
child.stdout.on("data", (chunk) => {
|
|
1685
|
+
stdout += chunk.toString("utf8");
|
|
1686
|
+
});
|
|
1687
|
+
child.stderr.on("data", (chunk) => {
|
|
1688
|
+
stderr += chunk.toString("utf8");
|
|
1689
|
+
});
|
|
1690
|
+
child.on("error", (err) => {
|
|
1691
|
+
reject(
|
|
1692
|
+
new SchemaMigrationError(
|
|
1693
|
+
`Could not spawn dotnet. Is the .NET SDK installed and on PATH? ${err.message}`,
|
|
1694
|
+
err
|
|
1695
|
+
)
|
|
1696
|
+
);
|
|
1697
|
+
});
|
|
1698
|
+
child.on("close", (code) => {
|
|
1699
|
+
if (code === 0) {
|
|
1700
|
+
resolve2({ stdout, stderr });
|
|
1701
|
+
} else {
|
|
1702
|
+
reject(
|
|
1703
|
+
new SchemaMigrationError(
|
|
1704
|
+
`dotnet ${args.join(" ")} exited with code ${code}.
|
|
1705
|
+
stdout: ${stdout}
|
|
1706
|
+
stderr: ${stderr}`
|
|
1707
|
+
)
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
var restored = /* @__PURE__ */ new Set();
|
|
1714
|
+
async function ensureRestored(projectDir, csproj) {
|
|
1715
|
+
if (restored.has(projectDir)) return;
|
|
1716
|
+
if (fs12.existsSync(path12.join(projectDir, ".config", "dotnet-tools.json"))) {
|
|
1717
|
+
await spawnDotnet(projectDir, ["tool", "restore"], {});
|
|
1718
|
+
}
|
|
1719
|
+
await spawnDotnet(projectDir, ["restore", csproj], {});
|
|
1720
|
+
restored.add(projectDir);
|
|
1721
|
+
}
|
|
1722
|
+
async function runEf(projectDir, args, dsn, opts = {}) {
|
|
1723
|
+
const project = requireProject(projectDir);
|
|
1724
|
+
const csproj = path12.relative(projectDir, project.csproj) || project.csproj;
|
|
1725
|
+
await ensureRestored(projectDir, csproj);
|
|
1726
|
+
const full = ["ef", ...args, "--project", csproj, "--prefix-output"];
|
|
1727
|
+
if (opts.noBuild) full.push("--no-build");
|
|
1728
|
+
const env = {};
|
|
1729
|
+
if (dsn) {
|
|
1730
|
+
env.DATABASE_URL = dsn;
|
|
1731
|
+
if (process.env.LAKEBASE_EFCORE_CONNECTION_ARG === "1") full.push("--connection", dsnToNpgsql(dsn));
|
|
1732
|
+
}
|
|
1733
|
+
return spawnDotnet(projectDir, full, env);
|
|
1734
|
+
}
|
|
1735
|
+
async function listEfMigrations(ctx, opts = {}) {
|
|
1736
|
+
const { stdout, stderr } = await runEf(ctx.projectDir, ["migrations", "list", "--json"], ctx.dsn, opts);
|
|
1737
|
+
const entries = parseEfJson(stdout);
|
|
1738
|
+
if (ctx.dsn && entries.some((e) => e.applied === null)) {
|
|
1739
|
+
const warning = stdout.split(/\r?\n/).filter((l) => /^(warn|error):/.test(l)).join("\n");
|
|
1740
|
+
throw new SchemaMigrationError(
|
|
1741
|
+
`dotnet ef could not read migration state from the database (check connectivity and credentials).
|
|
1742
|
+
${warning || stderr}`.trim()
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
return entries;
|
|
1746
|
+
}
|
|
1747
|
+
async function readEfHistory(dsn) {
|
|
1748
|
+
const client = new Client2({ connectionString: dsn, application_name: connectionApplicationName() });
|
|
1749
|
+
await client.connect();
|
|
1750
|
+
try {
|
|
1751
|
+
const exists = await client.query(`SELECT to_regclass('"__EFMigrationsHistory"') IS NOT NULL AS present`);
|
|
1752
|
+
if (!exists.rows[0]?.present) return null;
|
|
1753
|
+
const r = await client.query(`SELECT "MigrationId" FROM "__EFMigrationsHistory" ORDER BY "MigrationId"`);
|
|
1754
|
+
return r.rows.map((row) => row.MigrationId);
|
|
1755
|
+
} finally {
|
|
1756
|
+
await client.end();
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
function toApplied(entries) {
|
|
1760
|
+
return entries.map((e) => ({ version: efVersion(e.id), description: efDescription(e.id) }));
|
|
1761
|
+
}
|
|
1762
|
+
async function statusEfcore(ctx) {
|
|
1763
|
+
const entries = await listEfMigrations(ctx);
|
|
1764
|
+
const history = await (ctx.readHistory ?? readEfHistory)(ctx.dsn);
|
|
1765
|
+
const current = history && history.length > 0 ? efVersion(history[history.length - 1]) : void 0;
|
|
1766
|
+
const pending = entries.filter((e) => e.applied !== true).map((e) => ({ version: efVersion(e.id), filename: `${e.id}.cs`, description: efDescription(e.id) }));
|
|
1767
|
+
return { current, pending, tool: "efcore" };
|
|
1768
|
+
}
|
|
1769
|
+
async function applyEfcore(ctx) {
|
|
1770
|
+
const before = new Set((await listEfMigrations(ctx)).filter((e) => e.applied).map((e) => e.id));
|
|
1771
|
+
await runEf(ctx.projectDir, ["database", "update"], ctx.dsn, { noBuild: true });
|
|
1772
|
+
const after = await listEfMigrations(ctx, { noBuild: true });
|
|
1773
|
+
const applied = toApplied(after.filter((e) => e.applied && !before.has(e.id)));
|
|
1774
|
+
return { applied, alreadyAtLatest: applied.length === 0, tool: "efcore" };
|
|
1775
|
+
}
|
|
1776
|
+
function resolveEfTarget(target, applied) {
|
|
1777
|
+
const t = target.trim();
|
|
1778
|
+
if (t === "0") return "0";
|
|
1779
|
+
const rel = /^-(\d+)$/.exec(t);
|
|
1780
|
+
if (rel) {
|
|
1781
|
+
const steps = Number(rel[1]);
|
|
1782
|
+
if (steps < 1) throw new SchemaMigrationError(`Invalid rollback step: ${target}`);
|
|
1783
|
+
const index = applied.length - 1 - steps;
|
|
1784
|
+
if (index < -1) {
|
|
1785
|
+
throw new SchemaMigrationError(`Cannot roll back ${steps} migration(s); only ${applied.length} applied.`);
|
|
1786
|
+
}
|
|
1787
|
+
return index === -1 ? "0" : applied[index].id;
|
|
1788
|
+
}
|
|
1789
|
+
const match = applied.find((e) => e.id === t || efVersion(e.id) === t || e.name === t);
|
|
1790
|
+
if (!match) throw new SchemaMigrationError(`Rollback target '${target}' is not an applied migration.`);
|
|
1791
|
+
return match.id;
|
|
1792
|
+
}
|
|
1793
|
+
async function rollbackEfcore(ctx) {
|
|
1794
|
+
const beforeEntries = (await listEfMigrations(ctx)).filter((e) => e.applied);
|
|
1795
|
+
const targetId = resolveEfTarget(ctx.target, beforeEntries);
|
|
1796
|
+
await runEf(ctx.projectDir, ["database", "update", targetId], ctx.dsn, { noBuild: true });
|
|
1797
|
+
const stillApplied = new Set((await listEfMigrations(ctx, { noBuild: true })).filter((e) => e.applied).map((e) => e.id));
|
|
1798
|
+
return { rolledBack: toApplied(beforeEntries.filter((e) => !stillApplied.has(e.id)).reverse()), tool: "efcore" };
|
|
1799
|
+
}
|
|
1800
|
+
function efMigrationName(slug) {
|
|
1801
|
+
const name = slug.split(/[^A-Za-z0-9]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
|
|
1802
|
+
if (!name) throw new SchemaMigrationError(`Cannot derive a migration name from '${slug}'.`);
|
|
1803
|
+
return /^[0-9]/.test(name) ? `M${name}` : name;
|
|
1804
|
+
}
|
|
1805
|
+
async function createEfcoreMigration(opts) {
|
|
1806
|
+
const project = requireProject(opts.projectDir);
|
|
1807
|
+
const name = efMigrationName(opts.slug);
|
|
1808
|
+
const existing = new Set(fs12.existsSync(project.migrationsDir) ? fs12.readdirSync(project.migrationsDir) : []);
|
|
1809
|
+
await runEf(opts.projectDir, ["migrations", "add", name]);
|
|
1810
|
+
const created = fs12.readdirSync(project.migrationsDir).filter((f) => !existing.has(f) && MIGRATION_ID.test(f.replace(/\.cs$/, "")) && !f.endsWith(".Designer.cs"));
|
|
1811
|
+
if (created.length !== 1) {
|
|
1812
|
+
throw new SchemaMigrationError(`Expected one new migration file for ${name}, found: ${created.join(", ") || "none"}`);
|
|
1813
|
+
}
|
|
1814
|
+
return { id: created[0].replace(/\.cs$/, ""), path: path12.join(project.migrationsDir, created[0]) };
|
|
1815
|
+
}
|
|
1816
|
+
function localEfMigrationIds(projectDir) {
|
|
1817
|
+
const project = findEfCoreProject(projectDir);
|
|
1818
|
+
if (!project || !fs12.existsSync(project.migrationsDir)) return [];
|
|
1819
|
+
return fs12.readdirSync(project.migrationsDir).filter((f) => f.endsWith(".cs") && !f.endsWith(".Designer.cs") && MIGRATION_ID.test(f.slice(0, -3))).map((f) => f.slice(0, -3)).sort();
|
|
1820
|
+
}
|
|
1821
|
+
function efProductVersion(project) {
|
|
1822
|
+
try {
|
|
1823
|
+
const csproj = fs12.readFileSync(project.csproj, "utf-8");
|
|
1824
|
+
const m = new RegExp(`${EF_DESIGN_PACKAGE.replace(/\./g, "\\.")}"\\s+Version="([^"]+)"`).exec(csproj);
|
|
1825
|
+
if (m) return m[1];
|
|
1826
|
+
} catch {
|
|
1827
|
+
}
|
|
1828
|
+
return "unknown";
|
|
1829
|
+
}
|
|
1830
|
+
async function stampEfcore(ctx) {
|
|
1831
|
+
const project = requireProject(ctx.projectDir);
|
|
1832
|
+
const ids = localEfMigrationIds(ctx.projectDir);
|
|
1833
|
+
if (ids.length === 0) throw new SchemaMigrationError("No local EF Core migrations to stamp.");
|
|
1834
|
+
const rev = ctx.revision.trim();
|
|
1835
|
+
const end = rev === "head" ? ids.length - 1 : ids.findIndex((id) => id === rev || efVersion(id) === rev);
|
|
1836
|
+
if (end < 0) throw new SchemaMigrationError(`Stamp revision '${ctx.revision}' is not a local migration.`);
|
|
1837
|
+
const toStamp = ids.slice(0, end + 1);
|
|
1838
|
+
const productVersion = efProductVersion(project);
|
|
1839
|
+
const client = new Client2({ connectionString: ctx.dsn, application_name: connectionApplicationName() });
|
|
1840
|
+
await client.connect();
|
|
1841
|
+
try {
|
|
1842
|
+
await client.query("BEGIN");
|
|
1843
|
+
await client.query(
|
|
1844
|
+
`CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
|
1845
|
+
"MigrationId" character varying(150) NOT NULL,
|
|
1846
|
+
"ProductVersion" character varying(32) NOT NULL,
|
|
1847
|
+
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId"))`
|
|
1848
|
+
);
|
|
1849
|
+
await client.query(`DELETE FROM "__EFMigrationsHistory" WHERE NOT ("MigrationId" = ANY($1::text[]))`, [toStamp]);
|
|
1850
|
+
for (const id of toStamp) {
|
|
1851
|
+
await client.query(
|
|
1852
|
+
`INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") VALUES ($1, $2)
|
|
1853
|
+
ON CONFLICT ("MigrationId") DO NOTHING`,
|
|
1854
|
+
[id, productVersion]
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1857
|
+
await client.query("COMMIT");
|
|
1858
|
+
} catch (err) {
|
|
1859
|
+
await client.query("ROLLBACK").catch(() => void 0);
|
|
1860
|
+
throw err;
|
|
1861
|
+
} finally {
|
|
1862
|
+
await client.end();
|
|
1863
|
+
}
|
|
1864
|
+
return efVersion(toStamp[toStamp.length - 1]);
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// scripts/lakebase/adapters/efcore-adapter.ts
|
|
1868
|
+
async function buildDsn4(args) {
|
|
1869
|
+
const result = await getConnection({
|
|
1870
|
+
output: "dsn",
|
|
1871
|
+
instance: args.instance,
|
|
1872
|
+
branch: args.branch,
|
|
1873
|
+
database: args.database,
|
|
1874
|
+
endpointName: args.endpointName
|
|
1875
|
+
});
|
|
1876
|
+
return result.url;
|
|
1877
|
+
}
|
|
1878
|
+
function errMsg(err) {
|
|
1879
|
+
return err instanceof Error ? err.message : String(err);
|
|
1880
|
+
}
|
|
1881
|
+
function listEfCoreFiles(projectDir) {
|
|
1882
|
+
return localEfMigrationIds(projectDir).map((id) => ({
|
|
1883
|
+
version: efVersion(id),
|
|
1884
|
+
filename: `${id}.cs`,
|
|
1885
|
+
description: efDescription(id),
|
|
1886
|
+
type: "CSharp",
|
|
1887
|
+
tool: "efcore"
|
|
1888
|
+
}));
|
|
1889
|
+
}
|
|
1890
|
+
var EfCoreAdapter = {
|
|
1891
|
+
id: "efcore",
|
|
1892
|
+
languages: ["dotnet"],
|
|
1893
|
+
/** A .NET project (root marker) with a *.csproj that references Microsoft.EntityFrameworkCore.Design. */
|
|
1894
|
+
detect(projectDir) {
|
|
1895
|
+
return isDotnetProject(projectDir) && findEfCoreProject(projectDir) !== null;
|
|
1896
|
+
},
|
|
1897
|
+
async apply(args) {
|
|
1898
|
+
try {
|
|
1899
|
+
const dsn = await buildDsn4(args);
|
|
1900
|
+
const r = await applyEfcore({ projectDir: args.projectDir, dsn });
|
|
1901
|
+
return {
|
|
1902
|
+
applied_migrations: r.applied,
|
|
1903
|
+
status: r.alreadyAtLatest ? "noop" : "ok",
|
|
1904
|
+
tool_specific: { alreadyAtLatest: r.alreadyAtLatest, tool: r.tool }
|
|
1905
|
+
};
|
|
1906
|
+
} catch (err) {
|
|
1907
|
+
return { applied_migrations: [], status: "error", error: errMsg(err) };
|
|
1908
|
+
}
|
|
1909
|
+
},
|
|
1910
|
+
async rollback(args) {
|
|
1911
|
+
try {
|
|
1912
|
+
const dsn = await buildDsn4(args);
|
|
1913
|
+
const r = await rollbackEfcore({ projectDir: args.projectDir, dsn, target: args.target });
|
|
1914
|
+
return {
|
|
1915
|
+
rolled_back: r.rolledBack,
|
|
1916
|
+
status: r.rolledBack.length === 0 ? "noop" : "ok",
|
|
1917
|
+
tool_specific: { tool: r.tool }
|
|
1918
|
+
};
|
|
1919
|
+
} catch (err) {
|
|
1920
|
+
return { rolled_back: [], status: "error", error: errMsg(err) };
|
|
1921
|
+
}
|
|
1922
|
+
},
|
|
1923
|
+
async stamp(args) {
|
|
1924
|
+
try {
|
|
1925
|
+
const dsn = await buildDsn4(args);
|
|
1926
|
+
const stamped = await stampEfcore({ projectDir: args.projectDir, dsn, revision: args.revision });
|
|
1927
|
+
return { status: "ok", stamped_revision: stamped, tool_specific: { tool: "efcore" } };
|
|
1928
|
+
} catch (err) {
|
|
1929
|
+
return { status: "error", stamped_revision: null, error: errMsg(err) };
|
|
1930
|
+
}
|
|
1931
|
+
},
|
|
1932
|
+
async status(args) {
|
|
1933
|
+
try {
|
|
1934
|
+
const dsn = await buildDsn4(args);
|
|
1935
|
+
const r = await statusEfcore({ projectDir: args.projectDir, dsn });
|
|
1936
|
+
return {
|
|
1937
|
+
applied_version: r.current ?? null,
|
|
1938
|
+
pending: r.pending,
|
|
1939
|
+
applied: [],
|
|
1940
|
+
status: "ok",
|
|
1941
|
+
tool_specific: { tool: r.tool }
|
|
1942
|
+
};
|
|
1943
|
+
} catch (err) {
|
|
1944
|
+
return { applied_version: null, pending: [], applied: [], status: "error", error: errMsg(err) };
|
|
1945
|
+
}
|
|
1946
|
+
},
|
|
1947
|
+
async list(args) {
|
|
1948
|
+
return { files: listEfCoreFiles(args.projectDir) };
|
|
1949
|
+
},
|
|
1950
|
+
// baseline: use stamp (EF has no separate baseline concept).
|
|
1951
|
+
async newMigration(args) {
|
|
1952
|
+
try {
|
|
1953
|
+
const created = await createEfcoreMigration({ projectDir: args.projectDir, slug: args.slug });
|
|
1954
|
+
return {
|
|
1955
|
+
status: "ok",
|
|
1956
|
+
version: efVersion(created.id),
|
|
1957
|
+
filename: path13.basename(created.path),
|
|
1958
|
+
path: created.path
|
|
1959
|
+
};
|
|
1960
|
+
} catch (err) {
|
|
1961
|
+
return { status: "error", version: "", filename: "", path: "", error: errMsg(err) };
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
registerSchemaMigrationAdapter(EfCoreAdapter);
|
|
1966
|
+
|
|
1545
1967
|
// scripts/lakebase/schema-migrate.ts
|
|
1546
1968
|
var SchemaMigrationError = class extends Error {
|
|
1547
1969
|
constructor(message, cause) {
|
|
@@ -1555,7 +1977,7 @@ function detectLanguage(projectDir) {
|
|
|
1555
1977
|
const lang = resolveMigrationLanguage(projectDir);
|
|
1556
1978
|
if (lang === "unknown") {
|
|
1557
1979
|
throw new SchemaMigrationError(
|
|
1558
|
-
`Could not detect project language in ${projectDir}. Expected one of: pom.xml (java/kotlin), pyproject.toml or alembic.ini (python), package.json (nodejs). Pass {language} explicitly to override.`
|
|
1980
|
+
`Could not detect project language in ${projectDir}. Expected one of: pom.xml (java/kotlin), pyproject.toml or alembic.ini (python), package.json (nodejs), *.sln or *.csproj (dotnet). Pass {language} explicitly to override.`
|
|
1559
1981
|
);
|
|
1560
1982
|
}
|
|
1561
1983
|
return lang;
|
|
@@ -1569,12 +1991,12 @@ function migrationSlug2(description) {
|
|
|
1569
1991
|
}
|
|
1570
1992
|
|
|
1571
1993
|
// scripts/lakebase/workflow-drift.ts
|
|
1572
|
-
import * as
|
|
1573
|
-
import * as
|
|
1994
|
+
import * as fs14 from "fs";
|
|
1995
|
+
import * as path15 from "path";
|
|
1574
1996
|
function findKitTemplatesDir(start) {
|
|
1575
1997
|
let dir = start;
|
|
1576
1998
|
for (let i = 0; i < 6; i++) {
|
|
1577
|
-
const candidate =
|
|
1999
|
+
const candidate = path15.join(
|
|
1578
2000
|
dir,
|
|
1579
2001
|
"templates",
|
|
1580
2002
|
"project",
|
|
@@ -1582,8 +2004,8 @@ function findKitTemplatesDir(start) {
|
|
|
1582
2004
|
".github",
|
|
1583
2005
|
"workflows"
|
|
1584
2006
|
);
|
|
1585
|
-
if (
|
|
1586
|
-
const parent =
|
|
2007
|
+
if (fs14.existsSync(candidate)) return candidate;
|
|
2008
|
+
const parent = path15.dirname(dir);
|
|
1587
2009
|
if (parent === dir) break;
|
|
1588
2010
|
dir = parent;
|
|
1589
2011
|
}
|
|
@@ -1607,13 +2029,13 @@ function unifiedDiff(name, projectContent, templateContent) {
|
|
|
1607
2029
|
return out.join("\n");
|
|
1608
2030
|
}
|
|
1609
2031
|
function detectWorkflowDrift(args) {
|
|
1610
|
-
const projectWorkflowsDir =
|
|
2032
|
+
const projectWorkflowsDir = path15.join(
|
|
1611
2033
|
args.projectDir,
|
|
1612
2034
|
".github",
|
|
1613
2035
|
"workflows"
|
|
1614
2036
|
);
|
|
1615
|
-
const here =
|
|
1616
|
-
const kitWorkflowsDir = args.kitDir ?
|
|
2037
|
+
const here = path15.dirname(new URL(import.meta.url).pathname);
|
|
2038
|
+
const kitWorkflowsDir = args.kitDir ? path15.join(
|
|
1617
2039
|
args.kitDir,
|
|
1618
2040
|
"templates",
|
|
1619
2041
|
"project",
|
|
@@ -1621,21 +2043,21 @@ function detectWorkflowDrift(args) {
|
|
|
1621
2043
|
".github",
|
|
1622
2044
|
"workflows"
|
|
1623
2045
|
) : findKitTemplatesDir(here);
|
|
1624
|
-
const templateFiles =
|
|
1625
|
-
const projectFiles =
|
|
2046
|
+
const templateFiles = fs14.existsSync(kitWorkflowsDir) ? fs14.readdirSync(kitWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
|
|
2047
|
+
const projectFiles = fs14.existsSync(projectWorkflowsDir) ? fs14.readdirSync(projectWorkflowsDir).filter((f) => f.endsWith(".yml")) : [];
|
|
1626
2048
|
const version = readKitVersion(kitWorkflowsDir);
|
|
1627
2049
|
const seen = /* @__PURE__ */ new Set();
|
|
1628
2050
|
const files = [];
|
|
1629
2051
|
for (const name of templateFiles) {
|
|
1630
2052
|
seen.add(name);
|
|
1631
|
-
const projectPath2 =
|
|
1632
|
-
const templatePath =
|
|
1633
|
-
if (!
|
|
2053
|
+
const projectPath2 = path15.join(projectWorkflowsDir, name);
|
|
2054
|
+
const templatePath = path15.join(kitWorkflowsDir, name);
|
|
2055
|
+
if (!fs14.existsSync(projectPath2)) {
|
|
1634
2056
|
files.push({ name, status: "missing" });
|
|
1635
2057
|
continue;
|
|
1636
2058
|
}
|
|
1637
|
-
const projectContent =
|
|
1638
|
-
const templateContent = applyPlaceholders(
|
|
2059
|
+
const projectContent = fs14.readFileSync(projectPath2, "utf8");
|
|
2060
|
+
const templateContent = applyPlaceholders(fs14.readFileSync(templatePath, "utf8"), version);
|
|
1639
2061
|
if (projectContent === templateContent) {
|
|
1640
2062
|
files.push({ name, status: "unchanged" });
|
|
1641
2063
|
} else {
|
|
@@ -1666,10 +2088,10 @@ function detectWorkflowDrift(args) {
|
|
|
1666
2088
|
function readKitVersion(kitWorkflowsDir) {
|
|
1667
2089
|
let dir = kitWorkflowsDir;
|
|
1668
2090
|
for (let i = 0; i < 5; i++) {
|
|
1669
|
-
dir =
|
|
2091
|
+
dir = path15.dirname(dir);
|
|
1670
2092
|
}
|
|
1671
2093
|
try {
|
|
1672
|
-
const raw =
|
|
2094
|
+
const raw = fs14.readFileSync(path15.join(dir, "package.json"), "utf-8");
|
|
1673
2095
|
const pkg = JSON.parse(raw);
|
|
1674
2096
|
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
1675
2097
|
} catch {
|
|
@@ -1682,10 +2104,10 @@ function applyPlaceholders(content, version) {
|
|
|
1682
2104
|
|
|
1683
2105
|
// scripts/lakebase/doctor.ts
|
|
1684
2106
|
function readEnvFile(projectDir) {
|
|
1685
|
-
const envPath =
|
|
1686
|
-
if (!
|
|
2107
|
+
const envPath = path16.join(projectDir, ".env");
|
|
2108
|
+
if (!fs15.existsSync(envPath)) return {};
|
|
1687
2109
|
const out = {};
|
|
1688
|
-
for (const line of
|
|
2110
|
+
for (const line of fs15.readFileSync(envPath, "utf8").split("\n")) {
|
|
1689
2111
|
const m = line.match(/^\s*([A-Z][A-Z0-9_]*)\s*=\s*(.*)\s*$/);
|
|
1690
2112
|
if (!m) continue;
|
|
1691
2113
|
let val = m[2];
|
|
@@ -1839,6 +2261,23 @@ async function checkPrereq(spec, run) {
|
|
|
1839
2261
|
detail: { version: trimmed }
|
|
1840
2262
|
};
|
|
1841
2263
|
}
|
|
2264
|
+
var DOTNET_PREREQ = {
|
|
2265
|
+
name: "dotnet-sdk",
|
|
2266
|
+
cmd: "dotnet",
|
|
2267
|
+
versionArgs: ["--version"],
|
|
2268
|
+
minMajor: 10,
|
|
2269
|
+
label: ".NET SDK",
|
|
2270
|
+
hint: "Install the .NET 10 SDK (e.g. `brew install --cask dotnet-sdk` or https://dot.net). EF Core's dotnet-ef is restored per project from .config/dotnet-tools.json."
|
|
2271
|
+
};
|
|
2272
|
+
async function checkLanguagePrerequisites(projectDir, run = defaultVersionRunner) {
|
|
2273
|
+
let lang;
|
|
2274
|
+
try {
|
|
2275
|
+
lang = detectLanguage(projectDir);
|
|
2276
|
+
} catch {
|
|
2277
|
+
return [];
|
|
2278
|
+
}
|
|
2279
|
+
return lang === "dotnet" ? [await checkPrereq(DOTNET_PREREQ, run)] : [];
|
|
2280
|
+
}
|
|
1842
2281
|
async function checkPrerequisites(run = defaultVersionRunner) {
|
|
1843
2282
|
return Promise.all(PREREQS.map((spec) => checkPrereq(spec, run)));
|
|
1844
2283
|
}
|
|
@@ -1940,7 +2379,7 @@ function checkEnv(projectDir) {
|
|
|
1940
2379
|
name: "env-file",
|
|
1941
2380
|
status: "warn",
|
|
1942
2381
|
message: ".env not found",
|
|
1943
|
-
detail: { projectDir, envPath:
|
|
2382
|
+
detail: { projectDir, envPath: path16.join(projectDir, ".env") },
|
|
1944
2383
|
hint: "Run `lakebase-branch sync-env`, or `git checkout <branch>` so the post-checkout hook writes .env (or copy .env.example). The kit stores connection METADATA only , no DB token (the app mints a short-lived credential at runtime)."
|
|
1945
2384
|
};
|
|
1946
2385
|
}
|
|
@@ -2170,6 +2609,7 @@ async function runDoctor(args = {}) {
|
|
|
2170
2609
|
const hooks = checkHooks(projectDir);
|
|
2171
2610
|
const workflowDrift = checkWorkflowDrift(projectDir);
|
|
2172
2611
|
const prereqs = await checkPrerequisites();
|
|
2612
|
+
const languagePrereqs = await checkLanguagePrerequisites(projectDir);
|
|
2173
2613
|
const lakebaseEnabled = auth.status === "ok" ? await checkLakebaseEnabled(profile) : {
|
|
2174
2614
|
name: "lakebase-enabled",
|
|
2175
2615
|
status: "skip",
|
|
@@ -2178,6 +2618,7 @@ async function runDoctor(args = {}) {
|
|
|
2178
2618
|
const checks = [
|
|
2179
2619
|
cli,
|
|
2180
2620
|
...prereqs,
|
|
2621
|
+
...languagePrereqs,
|
|
2181
2622
|
auth,
|
|
2182
2623
|
identity,
|
|
2183
2624
|
lakebaseEnabled,
|
|
@@ -2291,7 +2732,7 @@ async function main() {
|
|
|
2291
2732
|
return 0;
|
|
2292
2733
|
}
|
|
2293
2734
|
if (args.fix) {
|
|
2294
|
-
const envPath =
|
|
2735
|
+
const envPath = path17.join(args.projectDir ?? process.cwd(), ".env");
|
|
2295
2736
|
const res = await ensureProfilePinned({ envPath });
|
|
2296
2737
|
if (!args.json) {
|
|
2297
2738
|
if (res.pinned) {
|