@databricks-solutions/lakebase-scm-utils 0.2.50 → 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.
Files changed (78) hide show
  1. package/dist/scripts/index.cjs +2636 -2124
  2. package/dist/scripts/index.d.cts +1 -1
  3. package/dist/scripts/index.d.ts +1 -1
  4. package/dist/scripts/index.js +680 -178
  5. package/dist/scripts/lakebase/ci-resolve-branch.cli.cjs +2460 -2030
  6. package/dist/scripts/lakebase/ci-resolve-branch.cli.js +508 -81
  7. package/dist/scripts/lakebase/collapse-heads.cli.cjs +2429 -2007
  8. package/dist/scripts/lakebase/collapse-heads.cli.js +477 -58
  9. package/dist/scripts/lakebase/detect-language.cli.cjs +2441 -2016
  10. package/dist/scripts/lakebase/detect-language.cli.js +489 -67
  11. package/dist/scripts/lakebase/doctor.cli.cjs +2484 -2040
  12. package/dist/scripts/lakebase/doctor.cli.js +532 -91
  13. package/dist/scripts/lakebase/get-connection.cli.cjs +2 -2
  14. package/dist/scripts/lakebase/get-connection.cli.js +2 -2
  15. package/dist/scripts/lakebase/index.cjs +2633 -2121
  16. package/dist/scripts/lakebase/index.d.cts +59 -10
  17. package/dist/scripts/lakebase/index.d.ts +59 -10
  18. package/dist/scripts/lakebase/index.js +674 -172
  19. package/dist/scripts/lakebase/infra-runner.cli.cjs +2435 -2013
  20. package/dist/scripts/lakebase/infra-runner.cli.js +483 -64
  21. package/dist/scripts/lakebase/new-migration.cli.cjs +2429 -2007
  22. package/dist/scripts/lakebase/new-migration.cli.js +477 -58
  23. package/dist/scripts/lakebase/schema-diff.cli.cjs +2 -2
  24. package/dist/scripts/lakebase/schema-diff.cli.js +2 -2
  25. package/dist/scripts/lakebase/schema-migrate.cli.cjs +2461 -2029
  26. package/dist/scripts/lakebase/schema-migrate.cli.js +509 -80
  27. package/dist/scripts/lakebase/scm-claim-feature.cli.cjs +2460 -2030
  28. package/dist/scripts/lakebase/scm-claim-feature.cli.js +508 -81
  29. package/dist/scripts/lakebase/scm-doctor.cli.cjs +2486 -2056
  30. package/dist/scripts/lakebase/scm-doctor.cli.js +537 -107
  31. package/dist/scripts/lakebase/scm-merge.cli.cjs +2443 -2021
  32. package/dist/scripts/lakebase/scm-merge.cli.js +494 -72
  33. package/dist/scripts/lakebase/scm-reconcile-tier.cli.cjs +2464 -2034
  34. package/dist/scripts/lakebase/scm-reconcile-tier.cli.js +511 -84
  35. package/dist/scripts/util/index.cjs +2 -2
  36. package/dist/scripts/util/index.js +2 -2
  37. package/package.json +1 -1
  38. package/scripts/lakebase/adapters/efcore-adapter.ts +158 -0
  39. package/scripts/lakebase/adapters/flyway-adapter.ts +8 -5
  40. package/scripts/lakebase/collapse-heads.cli.ts +1 -1
  41. package/scripts/lakebase/create-project.ts +14 -12
  42. package/scripts/lakebase/detect-language.cli.ts +2 -2
  43. package/scripts/lakebase/doctor.ts +29 -0
  44. package/scripts/lakebase/dotnet-project.ts +112 -0
  45. package/scripts/lakebase/index.ts +1 -0
  46. package/scripts/lakebase/migration-layout.ts +43 -2
  47. package/scripts/lakebase/new-migration.cli.ts +1 -1
  48. package/scripts/lakebase/scaffold-language.ts +23 -2
  49. package/scripts/lakebase/scaffold.ts +4 -1
  50. package/scripts/lakebase/schema-migrate-runners/efcore.ts +399 -0
  51. package/scripts/lakebase/schema-migrate-runners/flyway.ts +6 -4
  52. package/scripts/lakebase/schema-migrate.cli.ts +3 -1
  53. package/scripts/lakebase/schema-migrate.ts +24 -10
  54. package/templates/project/common/.github/workflows/merge.yml +13 -1
  55. package/templates/project/common/.github/workflows/pr.yml +25 -5
  56. package/templates/project/common/.gitignore.base +7 -0
  57. package/templates/project/common/scripts/flyway-migrate.sh +54 -1
  58. package/templates/project/common/scripts/prepare-schema-diff.sh +5 -0
  59. package/templates/project/common/scripts/run-dev.sh +10 -1
  60. package/templates/project/common/scripts/run-tests.sh +6 -1
  61. package/templates/project/dotnet/.gitignore.extra +8 -0
  62. package/templates/project/dotnet/App.sln +56 -0
  63. package/templates/project/dotnet/Makefile +13 -0
  64. package/templates/project/dotnet/global.json +6 -0
  65. package/templates/project/dotnet/src/App/Data/AppDbContext.cs +12 -0
  66. package/templates/project/dotnet/src/App/Lakebase/DotEnv.cs +48 -0
  67. package/templates/project/dotnet/src/App/Lakebase/LakebaseConnection.cs +225 -0
  68. package/templates/project/dotnet/src/App/Program.cs +34 -0
  69. package/templates/project/dotnet/src/App/appsettings.json +9 -0
  70. package/templates/project/dotnet/tests/App.Tests/App.Tests.csproj +25 -0
  71. package/templates/project/dotnet/tests/App.Tests/HealthTests.cs +18 -0
  72. package/templates/project/dotnet/tests/App.Tests/OpenApiTests.cs +23 -0
  73. package/templates/project/dotnet-efcore/.config/dotnet-tools.json +13 -0
  74. package/templates/project/dotnet-efcore/src/App/App.csproj +22 -0
  75. package/templates/project/dotnet-efcore/src/App/Data/DesignTimeDbContextFactory.cs +24 -0
  76. package/templates/project/dotnet-efcore/src/App/Migrations/README.md +23 -0
  77. package/templates/project/dotnet-flyway/db/migrations/README.md +23 -0
  78. package/templates/project/dotnet-flyway/src/App/App.csproj +17 -0
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // scripts/lakebase/schema-migrate.ts
4
- import * as fs10 from "fs";
5
- import * as path10 from "path";
4
+ import * as fs12 from "fs";
5
+ import * as path13 from "path";
6
6
 
7
7
  // scripts/lakebase/databricks-cli.ts
8
8
  import { execFile, execFileSync as execFileSync2 } from "child_process";
@@ -280,9 +280,9 @@ function asBranchUid(s) {
280
280
  }
281
281
  return s;
282
282
  }
283
- function branchNameFromResourcePath(path11) {
284
- if (!path11.includes("/branches/")) return null;
285
- const leaf = path11.split("/branches/").pop();
283
+ function branchNameFromResourcePath(path14) {
284
+ if (!path14.includes("/branches/")) return null;
285
+ const leaf = path14.split("/branches/").pop();
286
286
  if (!leaf) return null;
287
287
  try {
288
288
  return asBranchName(leaf);
@@ -393,8 +393,8 @@ var PKG_NAME = "@databricks-solutions/lakebase-scm-utils";
393
393
  var cached;
394
394
  function substrateSelfVersion() {
395
395
  if (cached !== void 0) return cached;
396
- if ("0.2.50".length > 0) {
397
- cached = "0.2.50";
396
+ if ("0.3.0".length > 0) {
397
+ cached = "0.3.0";
398
398
  return cached;
399
399
  }
400
400
  cached = "unknown";
@@ -503,23 +503,88 @@ function dbcli2(args) {
503
503
  }
504
504
 
505
505
  // scripts/lakebase/migration-layout.ts
506
+ import * as fs5 from "fs";
507
+ import * as path4 from "path";
508
+
509
+ // scripts/lakebase/dotnet-project.ts
506
510
  import * as fs4 from "fs";
507
511
  import * as path3 from "path";
512
+ var EF_DESIGN_PACKAGE = "Microsoft.EntityFrameworkCore.Design";
513
+ var DOTNET_FLYWAY_DIR = path3.join("db", "migrations");
514
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["bin", "obj", "node_modules", ".git", "client", "tests", "test"]);
515
+ function safeReaddir(dir) {
516
+ try {
517
+ return fs4.readdirSync(dir, { withFileTypes: true });
518
+ } catch {
519
+ return [];
520
+ }
521
+ }
522
+ function hasDotnetMarker(dir) {
523
+ return safeReaddir(dir).some(
524
+ (e) => e.isFile() && /\.(sln|slnx|csproj)$/i.test(e.name)
525
+ );
526
+ }
527
+ function findCsprojFiles(projectDir2) {
528
+ const found = [];
529
+ const walk = (dir, depth) => {
530
+ for (const e of safeReaddir(dir)) {
531
+ const full = path3.join(dir, e.name);
532
+ if (e.isFile() && e.name.toLowerCase().endsWith(".csproj")) {
533
+ found.push(full);
534
+ } else if (e.isDirectory() && depth < 2 && !SKIP_DIRS.has(e.name.toLowerCase()) && !e.name.startsWith(".")) {
535
+ walk(full, depth + 1);
536
+ }
537
+ }
538
+ };
539
+ walk(projectDir2, 0);
540
+ return found.sort();
541
+ }
542
+ function findEfCoreProject(projectDir2) {
543
+ for (const csproj of findCsprojFiles(projectDir2)) {
544
+ let content = "";
545
+ try {
546
+ content = fs4.readFileSync(csproj, "utf-8");
547
+ } catch {
548
+ continue;
549
+ }
550
+ if (content.includes(EF_DESIGN_PACKAGE)) {
551
+ const dir = path3.dirname(csproj);
552
+ return { csproj, projectDir: dir, migrationsDir: path3.join(dir, "Migrations") };
553
+ }
554
+ }
555
+ return null;
556
+ }
557
+ function isDotnetProject(projectDir2) {
558
+ return hasDotnetMarker(projectDir2);
559
+ }
560
+ function isDotnetFlywayProject(projectDir2) {
561
+ return isDotnetProject(projectDir2) && fs4.existsSync(path3.join(projectDir2, DOTNET_FLYWAY_DIR));
562
+ }
563
+ var MAVEN_FLYWAY_DIR = path3.join("src", "main", "resources", "db", "migration");
564
+ function flywayMigrationsDir(projectDir2) {
565
+ const maven = path3.join(projectDir2, MAVEN_FLYWAY_DIR);
566
+ if (fs4.existsSync(maven) || fs4.existsSync(path3.join(projectDir2, "pom.xml"))) return maven;
567
+ if (isDotnetFlywayProject(projectDir2)) return path3.join(projectDir2, DOTNET_FLYWAY_DIR);
568
+ return maven;
569
+ }
570
+
571
+ // scripts/lakebase/migration-layout.ts
508
572
  var MIGRATION_LANGUAGES = [
509
573
  "java",
510
574
  "kotlin",
511
575
  "python",
512
576
  "nodejs",
577
+ "dotnet",
513
578
  "unknown"
514
579
  ];
515
580
  function detectLanguageAt(dir) {
516
- if (fs4.existsSync(path3.join(dir, "pom.xml"))) {
517
- const kotlinDir = path3.join(dir, "src", "main", "kotlin");
518
- if (fs4.existsSync(kotlinDir)) {
581
+ if (fs5.existsSync(path4.join(dir, "pom.xml"))) {
582
+ const kotlinDir = path4.join(dir, "src", "main", "kotlin");
583
+ if (fs5.existsSync(kotlinDir)) {
519
584
  return "kotlin";
520
585
  }
521
586
  try {
522
- const pom = fs4.readFileSync(path3.join(dir, "pom.xml"), "utf-8");
587
+ const pom = fs5.readFileSync(path4.join(dir, "pom.xml"), "utf-8");
523
588
  if (pom.includes("kotlin-maven-plugin")) {
524
589
  return "kotlin";
525
590
  }
@@ -527,10 +592,13 @@ function detectLanguageAt(dir) {
527
592
  }
528
593
  return "java";
529
594
  }
530
- if (fs4.existsSync(path3.join(dir, "pyproject.toml")) || fs4.existsSync(path3.join(dir, "requirements.txt")) || fs4.existsSync(path3.join(dir, "alembic.ini"))) {
595
+ if (fs5.existsSync(path4.join(dir, "pyproject.toml")) || fs5.existsSync(path4.join(dir, "requirements.txt")) || fs5.existsSync(path4.join(dir, "alembic.ini"))) {
531
596
  return "python";
532
597
  }
533
- if (fs4.existsSync(path3.join(dir, "package.json"))) {
598
+ if (hasDotnetMarker(dir)) {
599
+ return "dotnet";
600
+ }
601
+ if (fs5.existsSync(path4.join(dir, "package.json"))) {
534
602
  return "nodejs";
535
603
  }
536
604
  return "unknown";
@@ -551,14 +619,14 @@ function resolveMigrationLanguage(projectDir2, configuredMigrationPath, override
551
619
  if (!rel) {
552
620
  return "unknown";
553
621
  }
554
- const rootResolved = path3.resolve(projectDir2);
555
- let dir = path3.resolve(projectDir2, rel);
556
- while (dir === rootResolved || dir.startsWith(rootResolved + path3.sep)) {
622
+ const rootResolved = path4.resolve(projectDir2);
623
+ let dir = path4.resolve(projectDir2, rel);
624
+ while (dir === rootResolved || dir.startsWith(rootResolved + path4.sep)) {
557
625
  const lang = detectLanguageAt(dir);
558
626
  if (lang !== "unknown") {
559
627
  return lang;
560
628
  }
561
- const parent = path3.dirname(dir);
629
+ const parent = path4.dirname(dir);
562
630
  if (parent === dir) {
563
631
  break;
564
632
  }
@@ -568,21 +636,21 @@ function resolveMigrationLanguage(projectDir2, configuredMigrationPath, override
568
636
  }
569
637
 
570
638
  // scripts/lakebase/adapters/alembic-adapter.ts
571
- import * as fs6 from "fs";
572
- import * as path5 from "path";
639
+ import * as fs7 from "fs";
640
+ import * as path6 from "path";
573
641
 
574
642
  // scripts/lakebase/schema-migrate-runners/alembic.ts
575
643
  import { spawn } from "child_process";
576
- import * as fs5 from "fs";
577
- import * as path4 from "path";
644
+ import * as fs6 from "fs";
645
+ import * as path5 from "path";
578
646
  function resolveAlembicBin(projectDir2) {
579
647
  const candidates = [
580
- path4.join(projectDir2, ".venv", "bin", "alembic"),
581
- path4.join(projectDir2, "venv", "bin", "alembic")
648
+ path5.join(projectDir2, ".venv", "bin", "alembic"),
649
+ path5.join(projectDir2, "venv", "bin", "alembic")
582
650
  ];
583
651
  for (const candidate of candidates) {
584
652
  try {
585
- if (fs5.existsSync(candidate)) return candidate;
653
+ if (fs6.existsSync(candidate)) return candidate;
586
654
  } catch {
587
655
  }
588
656
  }
@@ -592,7 +660,7 @@ function spawnAlembic(projectDir2, args, dsn) {
592
660
  return new Promise((resolve2, reject) => {
593
661
  const bin = resolveAlembicBin(projectDir2);
594
662
  const env = { ...process.env };
595
- env.PYTHONPATH = [projectDir2, process.env.PYTHONPATH].filter(Boolean).join(path4.delimiter);
663
+ env.PYTHONPATH = [projectDir2, process.env.PYTHONPATH].filter(Boolean).join(path5.delimiter);
596
664
  if (dsn) env.DATABASE_URL = dsn;
597
665
  const child = spawn(bin, args, {
598
666
  cwd: projectDir2,
@@ -640,10 +708,10 @@ async function createAlembicRevision(opts) {
640
708
  const m = stdout.match(/Generating\s+(\S+\.py)/);
641
709
  if (m) return m[1].trim();
642
710
  for (const rel of ["migrations/versions", "alembic/versions"]) {
643
- const dir = path4.join(opts.projectDir, rel);
644
- if (!fs5.existsSync(dir)) continue;
645
- const hit = fs5.readdirSync(dir).find((f) => f.startsWith(`${opts.revId}_`) && f.endsWith(".py"));
646
- if (hit) return path4.join(dir, hit);
711
+ const dir = path5.join(opts.projectDir, rel);
712
+ if (!fs6.existsSync(dir)) continue;
713
+ const hit = fs6.readdirSync(dir).find((f) => f.startsWith(`${opts.revId}_`) && f.endsWith(".py"));
714
+ if (hit) return path5.join(dir, hit);
647
715
  }
648
716
  throw new SchemaMigrationError(
649
717
  `alembic revision succeeded but the created file could not be located.
@@ -754,15 +822,15 @@ async function buildDsn(args) {
754
822
  }
755
823
  function findVersionsDir(projectDir2) {
756
824
  const candidates = [
757
- path5.join(projectDir2, "migrations", "versions"),
758
- path5.join(projectDir2, "alembic", "versions")
825
+ path6.join(projectDir2, "migrations", "versions"),
826
+ path6.join(projectDir2, "alembic", "versions")
759
827
  ];
760
- return candidates.find((p) => fs6.existsSync(p));
828
+ return candidates.find((p) => fs7.existsSync(p));
761
829
  }
762
830
  function listAlembicFiles(projectDir2) {
763
831
  const dir = findVersionsDir(projectDir2);
764
832
  if (!dir) return [];
765
- const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".py") && !f.startsWith("__"));
833
+ const files = fs7.readdirSync(dir).filter((f) => f.endsWith(".py") && !f.startsWith("__"));
766
834
  return files.map((filename) => {
767
835
  const stem = filename.replace(/\.py$/, "");
768
836
  const sep2 = stem.indexOf("_");
@@ -787,9 +855,9 @@ var AlembicAdapter = {
787
855
  * here. Callers can still force-select via project.yaml#migration_tool.
788
856
  */
789
857
  detect(projectDir2) {
790
- if (fs6.existsSync(path5.join(projectDir2, "alembic.ini"))) return true;
791
- if (fs6.existsSync(path5.join(projectDir2, "migrations", "env.py"))) return true;
792
- if (fs6.existsSync(path5.join(projectDir2, "alembic", "env.py"))) return true;
858
+ if (fs7.existsSync(path6.join(projectDir2, "alembic.ini"))) return true;
859
+ if (fs7.existsSync(path6.join(projectDir2, "migrations", "env.py"))) return true;
860
+ if (fs7.existsSync(path6.join(projectDir2, "alembic", "env.py"))) return true;
793
861
  return false;
794
862
  },
795
863
  async apply(args) {
@@ -895,7 +963,7 @@ var AlembicAdapter = {
895
963
  autogenerate: !!args.autogenerate,
896
964
  dsn
897
965
  });
898
- return { status: "ok", version: revId, filename: path5.basename(created), path: created };
966
+ return { status: "ok", version: revId, filename: path6.basename(created), path: created };
899
967
  } catch (err) {
900
968
  return {
901
969
  status: "error",
@@ -912,7 +980,7 @@ var AlembicAdapter = {
912
980
  if (heads.length <= 1) return { status: "noop", headsBefore: heads };
913
981
  if (args.dryRun) return { status: "ok", headsBefore: heads };
914
982
  const created = await mergeAlembicHeads(args.projectDir, args.message ?? "merge heads");
915
- const mergeRevision = path5.basename(created).replace(/\.py$/, "").split("_")[0];
983
+ const mergeRevision = path6.basename(created).replace(/\.py$/, "").split("_")[0];
916
984
  return { status: "ok", headsBefore: heads, mergeRevision, path: created };
917
985
  } catch (err) {
918
986
  return {
@@ -926,12 +994,12 @@ var AlembicAdapter = {
926
994
  registerSchemaMigrationAdapter(AlembicAdapter);
927
995
 
928
996
  // scripts/lakebase/adapters/flyway-adapter.ts
929
- import * as fs7 from "fs";
930
- import * as path7 from "path";
997
+ import * as fs8 from "fs";
998
+ import * as path8 from "path";
931
999
 
932
1000
  // scripts/lakebase/schema-migrate-runners/flyway.ts
933
1001
  import { spawn as spawn2 } from "child_process";
934
- import * as path6 from "path";
1002
+ import * as path7 from "path";
935
1003
  function dsnToFlywayEnv(dsn) {
936
1004
  const u = new URL(dsn);
937
1005
  const user = decodeURIComponent(u.username);
@@ -941,7 +1009,7 @@ function dsnToFlywayEnv(dsn) {
941
1009
  return { url, user, password };
942
1010
  }
943
1011
  function migrationsLocation(projectDir2) {
944
- return `filesystem:${path6.join(projectDir2, "src", "main", "resources", "db", "migration")}`;
1012
+ return `filesystem:${flywayMigrationsDir(projectDir2)}`;
945
1013
  }
946
1014
  function runFlyway(ctx, args) {
947
1015
  const { url, user, password } = dsnToFlywayEnv(ctx.dsn);
@@ -1042,7 +1110,7 @@ async function statusFlyway(ctx) {
1042
1110
  if (state === "SUCCESS" || state === "BASELINE") {
1043
1111
  current = m.version;
1044
1112
  } else if (state === "PENDING") {
1045
- const filename = m.filepath ? path6.basename(m.filepath) : `V${m.version}__migration.sql`;
1113
+ const filename = m.filepath ? path7.basename(m.filepath) : `V${m.version}__migration.sql`;
1046
1114
  pending.push({
1047
1115
  version: m.version,
1048
1116
  filename,
@@ -1065,9 +1133,9 @@ async function buildDsn2(args) {
1065
1133
  return result.url;
1066
1134
  }
1067
1135
  function listFlywayFiles(projectDir2) {
1068
- const dir = path7.join(projectDir2, "src", "main", "resources", "db", "migration");
1069
- if (!fs7.existsSync(dir)) return [];
1070
- const files = fs7.readdirSync(dir).filter((f) => /^V\d+(\.\d+)*__.+\.sql$/.test(f));
1136
+ const dir = flywayMigrationsDir(projectDir2);
1137
+ if (!fs8.existsSync(dir)) return [];
1138
+ const files = fs8.readdirSync(dir).filter((f) => /^V\d+(\.\d+)*__.+\.sql$/.test(f));
1071
1139
  return files.map((filename) => {
1072
1140
  const m = filename.match(/^V(\d+(?:\.\d+)*)__(.+)\.sql$/);
1073
1141
  const version = m[1];
@@ -1088,9 +1156,9 @@ function versionCompare(a, b) {
1088
1156
  }
1089
1157
  var FlywayAdapter = {
1090
1158
  id: "flyway",
1091
- languages: ["java", "kotlin"],
1159
+ languages: ["java", "kotlin", "dotnet"],
1092
1160
  detect(projectDir2) {
1093
- return fs7.existsSync(path7.join(projectDir2, "pom.xml"));
1161
+ return fs8.existsSync(path8.join(projectDir2, "pom.xml")) || isDotnetFlywayProject(projectDir2);
1094
1162
  },
1095
1163
  async apply(args) {
1096
1164
  const dsn = await buildDsn2(args);
@@ -1148,14 +1216,14 @@ var FlywayAdapter = {
1148
1216
  // optional-protocol shape makes this additive.
1149
1217
  async newMigration(args) {
1150
1218
  try {
1151
- const dir = path7.join(args.projectDir, "src", "main", "resources", "db", "migration");
1152
- fs7.mkdirSync(dir, { recursive: true });
1219
+ const dir = flywayMigrationsDir(args.projectDir);
1220
+ fs8.mkdirSync(dir, { recursive: true });
1153
1221
  const version = migrationTimestamp();
1154
1222
  const slug = migrationSlug2(args.slug);
1155
1223
  const filename = `V${version}__${slug}.sql`;
1156
- const full = path7.join(dir, filename);
1157
- if (fs7.existsSync(full)) throw new Error(`${filename} already exists`);
1158
- fs7.writeFileSync(
1224
+ const full = path8.join(dir, filename);
1225
+ if (fs8.existsSync(full)) throw new Error(`${filename} already exists`);
1226
+ fs8.writeFileSync(
1159
1227
  full,
1160
1228
  `-- V${version}: ${args.slug}
1161
1229
  -- Flyway migration (write your DDL/DML below).
@@ -1177,18 +1245,18 @@ var FlywayAdapter = {
1177
1245
  registerSchemaMigrationAdapter(FlywayAdapter);
1178
1246
 
1179
1247
  // scripts/lakebase/adapters/knex-adapter.ts
1180
- import * as fs9 from "fs";
1181
- import * as path9 from "path";
1248
+ import * as fs10 from "fs";
1249
+ import * as path10 from "path";
1182
1250
 
1183
1251
  // scripts/lakebase/schema-migrate-runners/knex.ts
1184
1252
  import { spawn as spawn3 } from "child_process";
1185
- import * as fs8 from "fs";
1186
- import * as path8 from "path";
1253
+ import * as fs9 from "fs";
1254
+ import * as path9 from "path";
1187
1255
  var KNEXFILE_VARIANTS = ["knexfile.js", "knexfile.ts", "knexfile.mjs", "knexfile.cjs"];
1188
1256
  function findKnexfile(projectDir2) {
1189
1257
  for (const name of KNEXFILE_VARIANTS) {
1190
- const p = path8.join(projectDir2, name);
1191
- if (fs8.existsSync(p)) return p;
1258
+ const p = path9.join(projectDir2, name);
1259
+ if (fs9.existsSync(p)) return p;
1192
1260
  }
1193
1261
  return void 0;
1194
1262
  }
@@ -1340,9 +1408,9 @@ async function buildDsn3(args) {
1340
1408
  }
1341
1409
  var KNEXFILE_VARIANTS2 = ["knexfile.js", "knexfile.ts", "knexfile.mjs", "knexfile.cjs"];
1342
1410
  function listKnexFiles(projectDir2) {
1343
- const dir = path9.join(projectDir2, "migrations");
1344
- if (!fs9.existsSync(dir)) return [];
1345
- const files = fs9.readdirSync(dir).filter((f) => (f.endsWith(".js") || f.endsWith(".ts")) && !f.startsWith("."));
1411
+ const dir = path10.join(projectDir2, "migrations");
1412
+ if (!fs10.existsSync(dir)) return [];
1413
+ const files = fs10.readdirSync(dir).filter((f) => (f.endsWith(".js") || f.endsWith(".ts")) && !f.startsWith("."));
1346
1414
  return files.map((filename) => {
1347
1415
  const stem = filename.replace(/\.(js|ts)$/, "");
1348
1416
  const m = stem.match(/^(\d{14})_(.+)$/);
@@ -1362,7 +1430,7 @@ var KnexAdapter = {
1362
1430
  * project.yaml#migration_tool.
1363
1431
  */
1364
1432
  detect(projectDir2) {
1365
- return KNEXFILE_VARIANTS2.some((name) => fs9.existsSync(path9.join(projectDir2, name)));
1433
+ return KNEXFILE_VARIANTS2.some((name) => fs10.existsSync(path10.join(projectDir2, name)));
1366
1434
  },
1367
1435
  async apply(args) {
1368
1436
  const dsn = await buildDsn3(args);
@@ -1435,9 +1503,9 @@ var KnexAdapter = {
1435
1503
  async newMigration(args) {
1436
1504
  try {
1437
1505
  const created = await createKnexMigration({ projectDir: args.projectDir, slug: migrationSlug2(args.slug) });
1438
- const stem = path9.basename(created).replace(/\.(js|ts)$/, "");
1506
+ const stem = path10.basename(created).replace(/\.(js|ts)$/, "");
1439
1507
  const version = stem.match(/^(\d{14})_/)?.[1] ?? stem;
1440
- return { status: "ok", version, filename: path9.basename(created), path: created };
1508
+ return { status: "ok", version, filename: path10.basename(created), path: created };
1441
1509
  } catch (err) {
1442
1510
  return {
1443
1511
  status: "error",
@@ -1451,6 +1519,360 @@ var KnexAdapter = {
1451
1519
  };
1452
1520
  registerSchemaMigrationAdapter(KnexAdapter);
1453
1521
 
1522
+ // scripts/lakebase/adapters/efcore-adapter.ts
1523
+ import * as path12 from "path";
1524
+
1525
+ // scripts/lakebase/schema-migrate-runners/efcore.ts
1526
+ import { spawn as spawn4 } from "child_process";
1527
+ import * as fs11 from "fs";
1528
+ import * as path11 from "path";
1529
+ import { Client as Client2 } from "pg";
1530
+ var MIGRATION_ID = /^(\d{14})_(.+)$/;
1531
+ function efVersion(id) {
1532
+ return MIGRATION_ID.exec(id)?.[1] ?? id;
1533
+ }
1534
+ function efDescription(id) {
1535
+ const name = MIGRATION_ID.exec(id)?.[2] ?? id;
1536
+ return name.replace(/_/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2");
1537
+ }
1538
+ function dsnToNpgsql(dsn) {
1539
+ const u = new URL(dsn);
1540
+ const parts = [
1541
+ `Host=${u.hostname}`,
1542
+ `Port=${u.port || "5432"}`,
1543
+ `Database=${decodeURIComponent(u.pathname.replace(/^\//, "")) || "databricks_postgres"}`
1544
+ ];
1545
+ if (u.username) parts.push(`Username=${decodeURIComponent(u.username)}`);
1546
+ if (u.password) parts.push(`Password=${decodeURIComponent(u.password)}`);
1547
+ const sslmode = (u.searchParams.get("sslmode") ?? "require").toLowerCase();
1548
+ const ssl = {
1549
+ disable: "Disable",
1550
+ allow: "Allow",
1551
+ prefer: "Prefer",
1552
+ require: "Require",
1553
+ "verify-ca": "VerifyCA",
1554
+ "verify-full": "VerifyFull"
1555
+ };
1556
+ parts.push(`SSL Mode=${ssl[sslmode] ?? "Require"}`);
1557
+ const app = u.searchParams.get("application_name");
1558
+ if (app) parts.push(`Application Name=${app}`);
1559
+ return parts.map((p) => p.replace(/;/g, "")).join(";");
1560
+ }
1561
+ function parseEfJson(stdout) {
1562
+ const body = stdout.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice("data:".length)).join("\n").trim();
1563
+ if (!body) {
1564
+ throw new SchemaMigrationError(`dotnet ef returned no JSON payload: ${stdout.slice(0, 400)}`);
1565
+ }
1566
+ try {
1567
+ return JSON.parse(body);
1568
+ } catch (err) {
1569
+ throw new SchemaMigrationError(
1570
+ `dotnet ef JSON parse failed: ${err instanceof Error ? err.message : String(err)}.
1571
+ Body (first 400 chars): ${body.slice(0, 400)}`
1572
+ );
1573
+ }
1574
+ }
1575
+ function requireProject(projectDir2) {
1576
+ const project = findEfCoreProject(projectDir2);
1577
+ if (!project) {
1578
+ throw new SchemaMigrationError(
1579
+ `No EF Core project found under ${projectDir2}. Expected a *.csproj that references ${EF_DESIGN_PACKAGE}.`
1580
+ );
1581
+ }
1582
+ return project;
1583
+ }
1584
+ function spawnDotnet(projectDir2, args, env) {
1585
+ return new Promise((resolve2, reject) => {
1586
+ const child = spawn4("dotnet", args, {
1587
+ cwd: projectDir2,
1588
+ env: { ...process.env, DOTNET_NOLOGO: "1", DOTNET_CLI_TELEMETRY_OPTOUT: "1", ...env },
1589
+ stdio: ["ignore", "pipe", "pipe"]
1590
+ });
1591
+ let stdout = "";
1592
+ let stderr = "";
1593
+ child.stdout.on("data", (chunk) => {
1594
+ stdout += chunk.toString("utf8");
1595
+ });
1596
+ child.stderr.on("data", (chunk) => {
1597
+ stderr += chunk.toString("utf8");
1598
+ });
1599
+ child.on("error", (err) => {
1600
+ reject(
1601
+ new SchemaMigrationError(
1602
+ `Could not spawn dotnet. Is the .NET SDK installed and on PATH? ${err.message}`,
1603
+ err
1604
+ )
1605
+ );
1606
+ });
1607
+ child.on("close", (code) => {
1608
+ if (code === 0) {
1609
+ resolve2({ stdout, stderr });
1610
+ } else {
1611
+ reject(
1612
+ new SchemaMigrationError(
1613
+ `dotnet ${args.join(" ")} exited with code ${code}.
1614
+ stdout: ${stdout}
1615
+ stderr: ${stderr}`
1616
+ )
1617
+ );
1618
+ }
1619
+ });
1620
+ });
1621
+ }
1622
+ var restored = /* @__PURE__ */ new Set();
1623
+ async function ensureRestored(projectDir2, csproj) {
1624
+ if (restored.has(projectDir2)) return;
1625
+ if (fs11.existsSync(path11.join(projectDir2, ".config", "dotnet-tools.json"))) {
1626
+ await spawnDotnet(projectDir2, ["tool", "restore"], {});
1627
+ }
1628
+ await spawnDotnet(projectDir2, ["restore", csproj], {});
1629
+ restored.add(projectDir2);
1630
+ }
1631
+ async function runEf(projectDir2, args, dsn, opts = {}) {
1632
+ const project = requireProject(projectDir2);
1633
+ const csproj = path11.relative(projectDir2, project.csproj) || project.csproj;
1634
+ await ensureRestored(projectDir2, csproj);
1635
+ const full = ["ef", ...args, "--project", csproj, "--prefix-output"];
1636
+ if (opts.noBuild) full.push("--no-build");
1637
+ const env = {};
1638
+ if (dsn) {
1639
+ env.DATABASE_URL = dsn;
1640
+ if (process.env.LAKEBASE_EFCORE_CONNECTION_ARG === "1") full.push("--connection", dsnToNpgsql(dsn));
1641
+ }
1642
+ return spawnDotnet(projectDir2, full, env);
1643
+ }
1644
+ async function listEfMigrations(ctx, opts = {}) {
1645
+ const { stdout, stderr } = await runEf(ctx.projectDir, ["migrations", "list", "--json"], ctx.dsn, opts);
1646
+ const entries = parseEfJson(stdout);
1647
+ if (ctx.dsn && entries.some((e) => e.applied === null)) {
1648
+ const warning = stdout.split(/\r?\n/).filter((l) => /^(warn|error):/.test(l)).join("\n");
1649
+ throw new SchemaMigrationError(
1650
+ `dotnet ef could not read migration state from the database (check connectivity and credentials).
1651
+ ${warning || stderr}`.trim()
1652
+ );
1653
+ }
1654
+ return entries;
1655
+ }
1656
+ async function readEfHistory(dsn) {
1657
+ const client = new Client2({ connectionString: dsn, application_name: connectionApplicationName() });
1658
+ await client.connect();
1659
+ try {
1660
+ const exists = await client.query(`SELECT to_regclass('"__EFMigrationsHistory"') IS NOT NULL AS present`);
1661
+ if (!exists.rows[0]?.present) return null;
1662
+ const r = await client.query(`SELECT "MigrationId" FROM "__EFMigrationsHistory" ORDER BY "MigrationId"`);
1663
+ return r.rows.map((row) => row.MigrationId);
1664
+ } finally {
1665
+ await client.end();
1666
+ }
1667
+ }
1668
+ function toApplied(entries) {
1669
+ return entries.map((e) => ({ version: efVersion(e.id), description: efDescription(e.id) }));
1670
+ }
1671
+ async function statusEfcore(ctx) {
1672
+ const entries = await listEfMigrations(ctx);
1673
+ const history = await (ctx.readHistory ?? readEfHistory)(ctx.dsn);
1674
+ const current = history && history.length > 0 ? efVersion(history[history.length - 1]) : void 0;
1675
+ const pending = entries.filter((e) => e.applied !== true).map((e) => ({ version: efVersion(e.id), filename: `${e.id}.cs`, description: efDescription(e.id) }));
1676
+ return { current, pending, tool: "efcore" };
1677
+ }
1678
+ async function applyEfcore(ctx) {
1679
+ const before = new Set((await listEfMigrations(ctx)).filter((e) => e.applied).map((e) => e.id));
1680
+ await runEf(ctx.projectDir, ["database", "update"], ctx.dsn, { noBuild: true });
1681
+ const after = await listEfMigrations(ctx, { noBuild: true });
1682
+ const applied = toApplied(after.filter((e) => e.applied && !before.has(e.id)));
1683
+ return { applied, alreadyAtLatest: applied.length === 0, tool: "efcore" };
1684
+ }
1685
+ function resolveEfTarget(target, applied) {
1686
+ const t = target.trim();
1687
+ if (t === "0") return "0";
1688
+ const rel = /^-(\d+)$/.exec(t);
1689
+ if (rel) {
1690
+ const steps = Number(rel[1]);
1691
+ if (steps < 1) throw new SchemaMigrationError(`Invalid rollback step: ${target}`);
1692
+ const index = applied.length - 1 - steps;
1693
+ if (index < -1) {
1694
+ throw new SchemaMigrationError(`Cannot roll back ${steps} migration(s); only ${applied.length} applied.`);
1695
+ }
1696
+ return index === -1 ? "0" : applied[index].id;
1697
+ }
1698
+ const match = applied.find((e) => e.id === t || efVersion(e.id) === t || e.name === t);
1699
+ if (!match) throw new SchemaMigrationError(`Rollback target '${target}' is not an applied migration.`);
1700
+ return match.id;
1701
+ }
1702
+ async function rollbackEfcore(ctx) {
1703
+ const beforeEntries = (await listEfMigrations(ctx)).filter((e) => e.applied);
1704
+ const targetId = resolveEfTarget(ctx.target, beforeEntries);
1705
+ await runEf(ctx.projectDir, ["database", "update", targetId], ctx.dsn, { noBuild: true });
1706
+ const stillApplied = new Set((await listEfMigrations(ctx, { noBuild: true })).filter((e) => e.applied).map((e) => e.id));
1707
+ return { rolledBack: toApplied(beforeEntries.filter((e) => !stillApplied.has(e.id)).reverse()), tool: "efcore" };
1708
+ }
1709
+ function efMigrationName(slug) {
1710
+ const name = slug.split(/[^A-Za-z0-9]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
1711
+ if (!name) throw new SchemaMigrationError(`Cannot derive a migration name from '${slug}'.`);
1712
+ return /^[0-9]/.test(name) ? `M${name}` : name;
1713
+ }
1714
+ async function createEfcoreMigration(opts) {
1715
+ const project = requireProject(opts.projectDir);
1716
+ const name = efMigrationName(opts.slug);
1717
+ const existing = new Set(fs11.existsSync(project.migrationsDir) ? fs11.readdirSync(project.migrationsDir) : []);
1718
+ await runEf(opts.projectDir, ["migrations", "add", name]);
1719
+ const created = fs11.readdirSync(project.migrationsDir).filter((f) => !existing.has(f) && MIGRATION_ID.test(f.replace(/\.cs$/, "")) && !f.endsWith(".Designer.cs"));
1720
+ if (created.length !== 1) {
1721
+ throw new SchemaMigrationError(`Expected one new migration file for ${name}, found: ${created.join(", ") || "none"}`);
1722
+ }
1723
+ return { id: created[0].replace(/\.cs$/, ""), path: path11.join(project.migrationsDir, created[0]) };
1724
+ }
1725
+ function localEfMigrationIds(projectDir2) {
1726
+ const project = findEfCoreProject(projectDir2);
1727
+ if (!project || !fs11.existsSync(project.migrationsDir)) return [];
1728
+ return fs11.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();
1729
+ }
1730
+ function efProductVersion(project) {
1731
+ try {
1732
+ const csproj = fs11.readFileSync(project.csproj, "utf-8");
1733
+ const m = new RegExp(`${EF_DESIGN_PACKAGE.replace(/\./g, "\\.")}"\\s+Version="([^"]+)"`).exec(csproj);
1734
+ if (m) return m[1];
1735
+ } catch {
1736
+ }
1737
+ return "unknown";
1738
+ }
1739
+ async function stampEfcore(ctx) {
1740
+ const project = requireProject(ctx.projectDir);
1741
+ const ids = localEfMigrationIds(ctx.projectDir);
1742
+ if (ids.length === 0) throw new SchemaMigrationError("No local EF Core migrations to stamp.");
1743
+ const rev = ctx.revision.trim();
1744
+ const end = rev === "head" ? ids.length - 1 : ids.findIndex((id) => id === rev || efVersion(id) === rev);
1745
+ if (end < 0) throw new SchemaMigrationError(`Stamp revision '${ctx.revision}' is not a local migration.`);
1746
+ const toStamp = ids.slice(0, end + 1);
1747
+ const productVersion = efProductVersion(project);
1748
+ const client = new Client2({ connectionString: ctx.dsn, application_name: connectionApplicationName() });
1749
+ await client.connect();
1750
+ try {
1751
+ await client.query("BEGIN");
1752
+ await client.query(
1753
+ `CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
1754
+ "MigrationId" character varying(150) NOT NULL,
1755
+ "ProductVersion" character varying(32) NOT NULL,
1756
+ CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId"))`
1757
+ );
1758
+ await client.query(`DELETE FROM "__EFMigrationsHistory" WHERE NOT ("MigrationId" = ANY($1::text[]))`, [toStamp]);
1759
+ for (const id of toStamp) {
1760
+ await client.query(
1761
+ `INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") VALUES ($1, $2)
1762
+ ON CONFLICT ("MigrationId") DO NOTHING`,
1763
+ [id, productVersion]
1764
+ );
1765
+ }
1766
+ await client.query("COMMIT");
1767
+ } catch (err) {
1768
+ await client.query("ROLLBACK").catch(() => void 0);
1769
+ throw err;
1770
+ } finally {
1771
+ await client.end();
1772
+ }
1773
+ return efVersion(toStamp[toStamp.length - 1]);
1774
+ }
1775
+
1776
+ // scripts/lakebase/adapters/efcore-adapter.ts
1777
+ async function buildDsn4(args) {
1778
+ const result = await getConnection({
1779
+ output: "dsn",
1780
+ instance: args.instance,
1781
+ branch: args.branch,
1782
+ database: args.database,
1783
+ endpointName: args.endpointName
1784
+ });
1785
+ return result.url;
1786
+ }
1787
+ function errMsg(err) {
1788
+ return err instanceof Error ? err.message : String(err);
1789
+ }
1790
+ function listEfCoreFiles(projectDir2) {
1791
+ return localEfMigrationIds(projectDir2).map((id) => ({
1792
+ version: efVersion(id),
1793
+ filename: `${id}.cs`,
1794
+ description: efDescription(id),
1795
+ type: "CSharp",
1796
+ tool: "efcore"
1797
+ }));
1798
+ }
1799
+ var EfCoreAdapter = {
1800
+ id: "efcore",
1801
+ languages: ["dotnet"],
1802
+ /** A .NET project (root marker) with a *.csproj that references Microsoft.EntityFrameworkCore.Design. */
1803
+ detect(projectDir2) {
1804
+ return isDotnetProject(projectDir2) && findEfCoreProject(projectDir2) !== null;
1805
+ },
1806
+ async apply(args) {
1807
+ try {
1808
+ const dsn = await buildDsn4(args);
1809
+ const r = await applyEfcore({ projectDir: args.projectDir, dsn });
1810
+ return {
1811
+ applied_migrations: r.applied,
1812
+ status: r.alreadyAtLatest ? "noop" : "ok",
1813
+ tool_specific: { alreadyAtLatest: r.alreadyAtLatest, tool: r.tool }
1814
+ };
1815
+ } catch (err) {
1816
+ return { applied_migrations: [], status: "error", error: errMsg(err) };
1817
+ }
1818
+ },
1819
+ async rollback(args) {
1820
+ try {
1821
+ const dsn = await buildDsn4(args);
1822
+ const r = await rollbackEfcore({ projectDir: args.projectDir, dsn, target: args.target });
1823
+ return {
1824
+ rolled_back: r.rolledBack,
1825
+ status: r.rolledBack.length === 0 ? "noop" : "ok",
1826
+ tool_specific: { tool: r.tool }
1827
+ };
1828
+ } catch (err) {
1829
+ return { rolled_back: [], status: "error", error: errMsg(err) };
1830
+ }
1831
+ },
1832
+ async stamp(args) {
1833
+ try {
1834
+ const dsn = await buildDsn4(args);
1835
+ const stamped = await stampEfcore({ projectDir: args.projectDir, dsn, revision: args.revision });
1836
+ return { status: "ok", stamped_revision: stamped, tool_specific: { tool: "efcore" } };
1837
+ } catch (err) {
1838
+ return { status: "error", stamped_revision: null, error: errMsg(err) };
1839
+ }
1840
+ },
1841
+ async status(args) {
1842
+ try {
1843
+ const dsn = await buildDsn4(args);
1844
+ const r = await statusEfcore({ projectDir: args.projectDir, dsn });
1845
+ return {
1846
+ applied_version: r.current ?? null,
1847
+ pending: r.pending,
1848
+ applied: [],
1849
+ status: "ok",
1850
+ tool_specific: { tool: r.tool }
1851
+ };
1852
+ } catch (err) {
1853
+ return { applied_version: null, pending: [], applied: [], status: "error", error: errMsg(err) };
1854
+ }
1855
+ },
1856
+ async list(args) {
1857
+ return { files: listEfCoreFiles(args.projectDir) };
1858
+ },
1859
+ // baseline: use stamp (EF has no separate baseline concept).
1860
+ async newMigration(args) {
1861
+ try {
1862
+ const created = await createEfcoreMigration({ projectDir: args.projectDir, slug: args.slug });
1863
+ return {
1864
+ status: "ok",
1865
+ version: efVersion(created.id),
1866
+ filename: path12.basename(created.path),
1867
+ path: created.path
1868
+ };
1869
+ } catch (err) {
1870
+ return { status: "error", version: "", filename: "", path: "", error: errMsg(err) };
1871
+ }
1872
+ }
1873
+ };
1874
+ registerSchemaMigrationAdapter(EfCoreAdapter);
1875
+
1454
1876
  // scripts/lakebase/schema-migrate.ts
1455
1877
  var SchemaMigrationError = class extends Error {
1456
1878
  constructor(message, cause) {
@@ -1464,7 +1886,7 @@ function detectLanguage(projectDir2) {
1464
1886
  const lang = resolveMigrationLanguage(projectDir2);
1465
1887
  if (lang === "unknown") {
1466
1888
  throw new SchemaMigrationError(
1467
- `Could not detect project language in ${projectDir2}. Expected one of: pom.xml (java/kotlin), pyproject.toml or alembic.ini (python), package.json (nodejs). Pass {language} explicitly to override.`
1889
+ `Could not detect project language in ${projectDir2}. 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.`
1468
1890
  );
1469
1891
  }
1470
1892
  return lang;
@@ -1496,7 +1918,7 @@ function printHelpAndExit() {
1496
1918
  Usage:
1497
1919
  lakebase-detect-language [--project-dir <path>]
1498
1920
 
1499
- Output (stdout): one of "java", "python", "nodejs"
1921
+ Output (stdout): one of "java", "kotlin", "python", "nodejs", "dotnet"
1500
1922
  Exits 1 with an error message on stderr if no marker found.
1501
1923
  `
1502
1924
  );