@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 fs9 from "fs";
5
- import * as path9 from "path";
4
+ import * as fs11 from "fs";
5
+ import * as path12 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(path10) {
284
- if (!path10.includes("/branches/")) return null;
285
- const leaf = path10.split("/branches/").pop();
283
+ function branchNameFromResourcePath(path13) {
284
+ if (!path13.includes("/branches/")) return null;
285
+ const leaf = path13.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";
@@ -502,22 +502,84 @@ function dbcli2(args) {
502
502
  return runDatabricksSync(args, { timeout: KIT_TIMEOUTS.cliDefault });
503
503
  }
504
504
 
505
+ // scripts/lakebase/dotnet-project.ts
506
+ import * as fs4 from "fs";
507
+ import * as path3 from "path";
508
+ var EF_DESIGN_PACKAGE = "Microsoft.EntityFrameworkCore.Design";
509
+ var DOTNET_FLYWAY_DIR = path3.join("db", "migrations");
510
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["bin", "obj", "node_modules", ".git", "client", "tests", "test"]);
511
+ function safeReaddir(dir) {
512
+ try {
513
+ return fs4.readdirSync(dir, { withFileTypes: true });
514
+ } catch {
515
+ return [];
516
+ }
517
+ }
518
+ function hasDotnetMarker(dir) {
519
+ return safeReaddir(dir).some(
520
+ (e) => e.isFile() && /\.(sln|slnx|csproj)$/i.test(e.name)
521
+ );
522
+ }
523
+ function findCsprojFiles(projectDir) {
524
+ const found = [];
525
+ const walk = (dir, depth) => {
526
+ for (const e of safeReaddir(dir)) {
527
+ const full = path3.join(dir, e.name);
528
+ if (e.isFile() && e.name.toLowerCase().endsWith(".csproj")) {
529
+ found.push(full);
530
+ } else if (e.isDirectory() && depth < 2 && !SKIP_DIRS.has(e.name.toLowerCase()) && !e.name.startsWith(".")) {
531
+ walk(full, depth + 1);
532
+ }
533
+ }
534
+ };
535
+ walk(projectDir, 0);
536
+ return found.sort();
537
+ }
538
+ function findEfCoreProject(projectDir) {
539
+ for (const csproj of findCsprojFiles(projectDir)) {
540
+ let content = "";
541
+ try {
542
+ content = fs4.readFileSync(csproj, "utf-8");
543
+ } catch {
544
+ continue;
545
+ }
546
+ if (content.includes(EF_DESIGN_PACKAGE)) {
547
+ const dir = path3.dirname(csproj);
548
+ return { csproj, projectDir: dir, migrationsDir: path3.join(dir, "Migrations") };
549
+ }
550
+ }
551
+ return null;
552
+ }
553
+ function isDotnetProject(projectDir) {
554
+ return hasDotnetMarker(projectDir);
555
+ }
556
+ function isDotnetFlywayProject(projectDir) {
557
+ return isDotnetProject(projectDir) && fs4.existsSync(path3.join(projectDir, DOTNET_FLYWAY_DIR));
558
+ }
559
+ var MAVEN_FLYWAY_DIR = path3.join("src", "main", "resources", "db", "migration");
560
+ function flywayMigrationsDir(projectDir) {
561
+ const maven = path3.join(projectDir, MAVEN_FLYWAY_DIR);
562
+ if (fs4.existsSync(maven) || fs4.existsSync(path3.join(projectDir, "pom.xml"))) return maven;
563
+ if (isDotnetFlywayProject(projectDir)) return path3.join(projectDir, DOTNET_FLYWAY_DIR);
564
+ return maven;
565
+ }
566
+
505
567
  // scripts/lakebase/adapters/alembic-adapter.ts
506
- import * as fs5 from "fs";
507
- import * as path4 from "path";
568
+ import * as fs6 from "fs";
569
+ import * as path5 from "path";
508
570
 
509
571
  // scripts/lakebase/schema-migrate-runners/alembic.ts
510
572
  import { spawn } from "child_process";
511
- import * as fs4 from "fs";
512
- import * as path3 from "path";
573
+ import * as fs5 from "fs";
574
+ import * as path4 from "path";
513
575
  function resolveAlembicBin(projectDir) {
514
576
  const candidates = [
515
- path3.join(projectDir, ".venv", "bin", "alembic"),
516
- path3.join(projectDir, "venv", "bin", "alembic")
577
+ path4.join(projectDir, ".venv", "bin", "alembic"),
578
+ path4.join(projectDir, "venv", "bin", "alembic")
517
579
  ];
518
580
  for (const candidate of candidates) {
519
581
  try {
520
- if (fs4.existsSync(candidate)) return candidate;
582
+ if (fs5.existsSync(candidate)) return candidate;
521
583
  } catch {
522
584
  }
523
585
  }
@@ -527,7 +589,7 @@ function spawnAlembic(projectDir, args, dsn) {
527
589
  return new Promise((resolve, reject) => {
528
590
  const bin = resolveAlembicBin(projectDir);
529
591
  const env = { ...process.env };
530
- env.PYTHONPATH = [projectDir, process.env.PYTHONPATH].filter(Boolean).join(path3.delimiter);
592
+ env.PYTHONPATH = [projectDir, process.env.PYTHONPATH].filter(Boolean).join(path4.delimiter);
531
593
  if (dsn) env.DATABASE_URL = dsn;
532
594
  const child = spawn(bin, args, {
533
595
  cwd: projectDir,
@@ -575,10 +637,10 @@ async function createAlembicRevision(opts) {
575
637
  const m = stdout.match(/Generating\s+(\S+\.py)/);
576
638
  if (m) return m[1].trim();
577
639
  for (const rel of ["migrations/versions", "alembic/versions"]) {
578
- const dir = path3.join(opts.projectDir, rel);
579
- if (!fs4.existsSync(dir)) continue;
580
- const hit = fs4.readdirSync(dir).find((f) => f.startsWith(`${opts.revId}_`) && f.endsWith(".py"));
581
- if (hit) return path3.join(dir, hit);
640
+ const dir = path4.join(opts.projectDir, rel);
641
+ if (!fs5.existsSync(dir)) continue;
642
+ const hit = fs5.readdirSync(dir).find((f) => f.startsWith(`${opts.revId}_`) && f.endsWith(".py"));
643
+ if (hit) return path4.join(dir, hit);
582
644
  }
583
645
  throw new SchemaMigrationError(
584
646
  `alembic revision succeeded but the created file could not be located.
@@ -712,15 +774,15 @@ async function buildDsn(args) {
712
774
  }
713
775
  function findVersionsDir(projectDir) {
714
776
  const candidates = [
715
- path4.join(projectDir, "migrations", "versions"),
716
- path4.join(projectDir, "alembic", "versions")
777
+ path5.join(projectDir, "migrations", "versions"),
778
+ path5.join(projectDir, "alembic", "versions")
717
779
  ];
718
- return candidates.find((p) => fs5.existsSync(p));
780
+ return candidates.find((p) => fs6.existsSync(p));
719
781
  }
720
782
  function listAlembicFiles(projectDir) {
721
783
  const dir = findVersionsDir(projectDir);
722
784
  if (!dir) return [];
723
- const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".py") && !f.startsWith("__"));
785
+ const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".py") && !f.startsWith("__"));
724
786
  return files.map((filename) => {
725
787
  const stem = filename.replace(/\.py$/, "");
726
788
  const sep = stem.indexOf("_");
@@ -745,9 +807,9 @@ var AlembicAdapter = {
745
807
  * here. Callers can still force-select via project.yaml#migration_tool.
746
808
  */
747
809
  detect(projectDir) {
748
- if (fs5.existsSync(path4.join(projectDir, "alembic.ini"))) return true;
749
- if (fs5.existsSync(path4.join(projectDir, "migrations", "env.py"))) return true;
750
- if (fs5.existsSync(path4.join(projectDir, "alembic", "env.py"))) return true;
810
+ if (fs6.existsSync(path5.join(projectDir, "alembic.ini"))) return true;
811
+ if (fs6.existsSync(path5.join(projectDir, "migrations", "env.py"))) return true;
812
+ if (fs6.existsSync(path5.join(projectDir, "alembic", "env.py"))) return true;
751
813
  return false;
752
814
  },
753
815
  async apply(args) {
@@ -853,7 +915,7 @@ var AlembicAdapter = {
853
915
  autogenerate: !!args.autogenerate,
854
916
  dsn
855
917
  });
856
- return { status: "ok", version: revId, filename: path4.basename(created), path: created };
918
+ return { status: "ok", version: revId, filename: path5.basename(created), path: created };
857
919
  } catch (err) {
858
920
  return {
859
921
  status: "error",
@@ -870,7 +932,7 @@ var AlembicAdapter = {
870
932
  if (heads.length <= 1) return { status: "noop", headsBefore: heads };
871
933
  if (args.dryRun) return { status: "ok", headsBefore: heads };
872
934
  const created = await mergeAlembicHeads(args.projectDir, args.message ?? "merge heads");
873
- const mergeRevision = path4.basename(created).replace(/\.py$/, "").split("_")[0];
935
+ const mergeRevision = path5.basename(created).replace(/\.py$/, "").split("_")[0];
874
936
  return { status: "ok", headsBefore: heads, mergeRevision, path: created };
875
937
  } catch (err) {
876
938
  return {
@@ -884,12 +946,12 @@ var AlembicAdapter = {
884
946
  registerSchemaMigrationAdapter(AlembicAdapter);
885
947
 
886
948
  // scripts/lakebase/adapters/flyway-adapter.ts
887
- import * as fs6 from "fs";
888
- import * as path6 from "path";
949
+ import * as fs7 from "fs";
950
+ import * as path7 from "path";
889
951
 
890
952
  // scripts/lakebase/schema-migrate-runners/flyway.ts
891
953
  import { spawn as spawn2 } from "child_process";
892
- import * as path5 from "path";
954
+ import * as path6 from "path";
893
955
  function dsnToFlywayEnv(dsn) {
894
956
  const u = new URL(dsn);
895
957
  const user = decodeURIComponent(u.username);
@@ -899,7 +961,7 @@ function dsnToFlywayEnv(dsn) {
899
961
  return { url, user, password };
900
962
  }
901
963
  function migrationsLocation(projectDir) {
902
- return `filesystem:${path5.join(projectDir, "src", "main", "resources", "db", "migration")}`;
964
+ return `filesystem:${flywayMigrationsDir(projectDir)}`;
903
965
  }
904
966
  function runFlyway(ctx, args) {
905
967
  const { url, user, password } = dsnToFlywayEnv(ctx.dsn);
@@ -1000,7 +1062,7 @@ async function statusFlyway(ctx) {
1000
1062
  if (state === "SUCCESS" || state === "BASELINE") {
1001
1063
  current = m.version;
1002
1064
  } else if (state === "PENDING") {
1003
- const filename = m.filepath ? path5.basename(m.filepath) : `V${m.version}__migration.sql`;
1065
+ const filename = m.filepath ? path6.basename(m.filepath) : `V${m.version}__migration.sql`;
1004
1066
  pending.push({
1005
1067
  version: m.version,
1006
1068
  filename,
@@ -1023,9 +1085,9 @@ async function buildDsn2(args) {
1023
1085
  return result.url;
1024
1086
  }
1025
1087
  function listFlywayFiles(projectDir) {
1026
- const dir = path6.join(projectDir, "src", "main", "resources", "db", "migration");
1027
- if (!fs6.existsSync(dir)) return [];
1028
- const files = fs6.readdirSync(dir).filter((f) => /^V\d+(\.\d+)*__.+\.sql$/.test(f));
1088
+ const dir = flywayMigrationsDir(projectDir);
1089
+ if (!fs7.existsSync(dir)) return [];
1090
+ const files = fs7.readdirSync(dir).filter((f) => /^V\d+(\.\d+)*__.+\.sql$/.test(f));
1029
1091
  return files.map((filename) => {
1030
1092
  const m = filename.match(/^V(\d+(?:\.\d+)*)__(.+)\.sql$/);
1031
1093
  const version = m[1];
@@ -1046,9 +1108,9 @@ function versionCompare(a, b) {
1046
1108
  }
1047
1109
  var FlywayAdapter = {
1048
1110
  id: "flyway",
1049
- languages: ["java", "kotlin"],
1111
+ languages: ["java", "kotlin", "dotnet"],
1050
1112
  detect(projectDir) {
1051
- return fs6.existsSync(path6.join(projectDir, "pom.xml"));
1113
+ return fs7.existsSync(path7.join(projectDir, "pom.xml")) || isDotnetFlywayProject(projectDir);
1052
1114
  },
1053
1115
  async apply(args) {
1054
1116
  const dsn = await buildDsn2(args);
@@ -1106,14 +1168,14 @@ var FlywayAdapter = {
1106
1168
  // optional-protocol shape makes this additive.
1107
1169
  async newMigration(args) {
1108
1170
  try {
1109
- const dir = path6.join(args.projectDir, "src", "main", "resources", "db", "migration");
1110
- fs6.mkdirSync(dir, { recursive: true });
1171
+ const dir = flywayMigrationsDir(args.projectDir);
1172
+ fs7.mkdirSync(dir, { recursive: true });
1111
1173
  const version = migrationTimestamp();
1112
1174
  const slug = migrationSlug2(args.slug);
1113
1175
  const filename = `V${version}__${slug}.sql`;
1114
- const full = path6.join(dir, filename);
1115
- if (fs6.existsSync(full)) throw new Error(`${filename} already exists`);
1116
- fs6.writeFileSync(
1176
+ const full = path7.join(dir, filename);
1177
+ if (fs7.existsSync(full)) throw new Error(`${filename} already exists`);
1178
+ fs7.writeFileSync(
1117
1179
  full,
1118
1180
  `-- V${version}: ${args.slug}
1119
1181
  -- Flyway migration (write your DDL/DML below).
@@ -1135,18 +1197,18 @@ var FlywayAdapter = {
1135
1197
  registerSchemaMigrationAdapter(FlywayAdapter);
1136
1198
 
1137
1199
  // scripts/lakebase/adapters/knex-adapter.ts
1138
- import * as fs8 from "fs";
1139
- import * as path8 from "path";
1200
+ import * as fs9 from "fs";
1201
+ import * as path9 from "path";
1140
1202
 
1141
1203
  // scripts/lakebase/schema-migrate-runners/knex.ts
1142
1204
  import { spawn as spawn3 } from "child_process";
1143
- import * as fs7 from "fs";
1144
- import * as path7 from "path";
1205
+ import * as fs8 from "fs";
1206
+ import * as path8 from "path";
1145
1207
  var KNEXFILE_VARIANTS = ["knexfile.js", "knexfile.ts", "knexfile.mjs", "knexfile.cjs"];
1146
1208
  function findKnexfile(projectDir) {
1147
1209
  for (const name of KNEXFILE_VARIANTS) {
1148
- const p = path7.join(projectDir, name);
1149
- if (fs7.existsSync(p)) return p;
1210
+ const p = path8.join(projectDir, name);
1211
+ if (fs8.existsSync(p)) return p;
1150
1212
  }
1151
1213
  return void 0;
1152
1214
  }
@@ -1298,9 +1360,9 @@ async function buildDsn3(args) {
1298
1360
  }
1299
1361
  var KNEXFILE_VARIANTS2 = ["knexfile.js", "knexfile.ts", "knexfile.mjs", "knexfile.cjs"];
1300
1362
  function listKnexFiles(projectDir) {
1301
- const dir = path8.join(projectDir, "migrations");
1302
- if (!fs8.existsSync(dir)) return [];
1303
- const files = fs8.readdirSync(dir).filter((f) => (f.endsWith(".js") || f.endsWith(".ts")) && !f.startsWith("."));
1363
+ const dir = path9.join(projectDir, "migrations");
1364
+ if (!fs9.existsSync(dir)) return [];
1365
+ const files = fs9.readdirSync(dir).filter((f) => (f.endsWith(".js") || f.endsWith(".ts")) && !f.startsWith("."));
1304
1366
  return files.map((filename) => {
1305
1367
  const stem = filename.replace(/\.(js|ts)$/, "");
1306
1368
  const m = stem.match(/^(\d{14})_(.+)$/);
@@ -1320,7 +1382,7 @@ var KnexAdapter = {
1320
1382
  * project.yaml#migration_tool.
1321
1383
  */
1322
1384
  detect(projectDir) {
1323
- return KNEXFILE_VARIANTS2.some((name) => fs8.existsSync(path8.join(projectDir, name)));
1385
+ return KNEXFILE_VARIANTS2.some((name) => fs9.existsSync(path9.join(projectDir, name)));
1324
1386
  },
1325
1387
  async apply(args) {
1326
1388
  const dsn = await buildDsn3(args);
@@ -1393,9 +1455,9 @@ var KnexAdapter = {
1393
1455
  async newMigration(args) {
1394
1456
  try {
1395
1457
  const created = await createKnexMigration({ projectDir: args.projectDir, slug: migrationSlug2(args.slug) });
1396
- const stem = path8.basename(created).replace(/\.(js|ts)$/, "");
1458
+ const stem = path9.basename(created).replace(/\.(js|ts)$/, "");
1397
1459
  const version = stem.match(/^(\d{14})_/)?.[1] ?? stem;
1398
- return { status: "ok", version, filename: path8.basename(created), path: created };
1460
+ return { status: "ok", version, filename: path9.basename(created), path: created };
1399
1461
  } catch (err) {
1400
1462
  return {
1401
1463
  status: "error",
@@ -1409,6 +1471,360 @@ var KnexAdapter = {
1409
1471
  };
1410
1472
  registerSchemaMigrationAdapter(KnexAdapter);
1411
1473
 
1474
+ // scripts/lakebase/adapters/efcore-adapter.ts
1475
+ import * as path11 from "path";
1476
+
1477
+ // scripts/lakebase/schema-migrate-runners/efcore.ts
1478
+ import { spawn as spawn4 } from "child_process";
1479
+ import * as fs10 from "fs";
1480
+ import * as path10 from "path";
1481
+ import { Client as Client2 } from "pg";
1482
+ var MIGRATION_ID = /^(\d{14})_(.+)$/;
1483
+ function efVersion(id) {
1484
+ return MIGRATION_ID.exec(id)?.[1] ?? id;
1485
+ }
1486
+ function efDescription(id) {
1487
+ const name = MIGRATION_ID.exec(id)?.[2] ?? id;
1488
+ return name.replace(/_/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2");
1489
+ }
1490
+ function dsnToNpgsql(dsn) {
1491
+ const u = new URL(dsn);
1492
+ const parts = [
1493
+ `Host=${u.hostname}`,
1494
+ `Port=${u.port || "5432"}`,
1495
+ `Database=${decodeURIComponent(u.pathname.replace(/^\//, "")) || "databricks_postgres"}`
1496
+ ];
1497
+ if (u.username) parts.push(`Username=${decodeURIComponent(u.username)}`);
1498
+ if (u.password) parts.push(`Password=${decodeURIComponent(u.password)}`);
1499
+ const sslmode = (u.searchParams.get("sslmode") ?? "require").toLowerCase();
1500
+ const ssl = {
1501
+ disable: "Disable",
1502
+ allow: "Allow",
1503
+ prefer: "Prefer",
1504
+ require: "Require",
1505
+ "verify-ca": "VerifyCA",
1506
+ "verify-full": "VerifyFull"
1507
+ };
1508
+ parts.push(`SSL Mode=${ssl[sslmode] ?? "Require"}`);
1509
+ const app = u.searchParams.get("application_name");
1510
+ if (app) parts.push(`Application Name=${app}`);
1511
+ return parts.map((p) => p.replace(/;/g, "")).join(";");
1512
+ }
1513
+ function parseEfJson(stdout) {
1514
+ const body = stdout.split(/\r?\n/).filter((l) => l.startsWith("data:")).map((l) => l.slice("data:".length)).join("\n").trim();
1515
+ if (!body) {
1516
+ throw new SchemaMigrationError(`dotnet ef returned no JSON payload: ${stdout.slice(0, 400)}`);
1517
+ }
1518
+ try {
1519
+ return JSON.parse(body);
1520
+ } catch (err) {
1521
+ throw new SchemaMigrationError(
1522
+ `dotnet ef JSON parse failed: ${err instanceof Error ? err.message : String(err)}.
1523
+ Body (first 400 chars): ${body.slice(0, 400)}`
1524
+ );
1525
+ }
1526
+ }
1527
+ function requireProject(projectDir) {
1528
+ const project = findEfCoreProject(projectDir);
1529
+ if (!project) {
1530
+ throw new SchemaMigrationError(
1531
+ `No EF Core project found under ${projectDir}. Expected a *.csproj that references ${EF_DESIGN_PACKAGE}.`
1532
+ );
1533
+ }
1534
+ return project;
1535
+ }
1536
+ function spawnDotnet(projectDir, args, env) {
1537
+ return new Promise((resolve, reject) => {
1538
+ const child = spawn4("dotnet", args, {
1539
+ cwd: projectDir,
1540
+ env: { ...process.env, DOTNET_NOLOGO: "1", DOTNET_CLI_TELEMETRY_OPTOUT: "1", ...env },
1541
+ stdio: ["ignore", "pipe", "pipe"]
1542
+ });
1543
+ let stdout = "";
1544
+ let stderr = "";
1545
+ child.stdout.on("data", (chunk) => {
1546
+ stdout += chunk.toString("utf8");
1547
+ });
1548
+ child.stderr.on("data", (chunk) => {
1549
+ stderr += chunk.toString("utf8");
1550
+ });
1551
+ child.on("error", (err) => {
1552
+ reject(
1553
+ new SchemaMigrationError(
1554
+ `Could not spawn dotnet. Is the .NET SDK installed and on PATH? ${err.message}`,
1555
+ err
1556
+ )
1557
+ );
1558
+ });
1559
+ child.on("close", (code) => {
1560
+ if (code === 0) {
1561
+ resolve({ stdout, stderr });
1562
+ } else {
1563
+ reject(
1564
+ new SchemaMigrationError(
1565
+ `dotnet ${args.join(" ")} exited with code ${code}.
1566
+ stdout: ${stdout}
1567
+ stderr: ${stderr}`
1568
+ )
1569
+ );
1570
+ }
1571
+ });
1572
+ });
1573
+ }
1574
+ var restored = /* @__PURE__ */ new Set();
1575
+ async function ensureRestored(projectDir, csproj) {
1576
+ if (restored.has(projectDir)) return;
1577
+ if (fs10.existsSync(path10.join(projectDir, ".config", "dotnet-tools.json"))) {
1578
+ await spawnDotnet(projectDir, ["tool", "restore"], {});
1579
+ }
1580
+ await spawnDotnet(projectDir, ["restore", csproj], {});
1581
+ restored.add(projectDir);
1582
+ }
1583
+ async function runEf(projectDir, args, dsn, opts = {}) {
1584
+ const project = requireProject(projectDir);
1585
+ const csproj = path10.relative(projectDir, project.csproj) || project.csproj;
1586
+ await ensureRestored(projectDir, csproj);
1587
+ const full = ["ef", ...args, "--project", csproj, "--prefix-output"];
1588
+ if (opts.noBuild) full.push("--no-build");
1589
+ const env = {};
1590
+ if (dsn) {
1591
+ env.DATABASE_URL = dsn;
1592
+ if (process.env.LAKEBASE_EFCORE_CONNECTION_ARG === "1") full.push("--connection", dsnToNpgsql(dsn));
1593
+ }
1594
+ return spawnDotnet(projectDir, full, env);
1595
+ }
1596
+ async function listEfMigrations(ctx, opts = {}) {
1597
+ const { stdout, stderr } = await runEf(ctx.projectDir, ["migrations", "list", "--json"], ctx.dsn, opts);
1598
+ const entries = parseEfJson(stdout);
1599
+ if (ctx.dsn && entries.some((e) => e.applied === null)) {
1600
+ const warning = stdout.split(/\r?\n/).filter((l) => /^(warn|error):/.test(l)).join("\n");
1601
+ throw new SchemaMigrationError(
1602
+ `dotnet ef could not read migration state from the database (check connectivity and credentials).
1603
+ ${warning || stderr}`.trim()
1604
+ );
1605
+ }
1606
+ return entries;
1607
+ }
1608
+ async function readEfHistory(dsn) {
1609
+ const client = new Client2({ connectionString: dsn, application_name: connectionApplicationName() });
1610
+ await client.connect();
1611
+ try {
1612
+ const exists = await client.query(`SELECT to_regclass('"__EFMigrationsHistory"') IS NOT NULL AS present`);
1613
+ if (!exists.rows[0]?.present) return null;
1614
+ const r = await client.query(`SELECT "MigrationId" FROM "__EFMigrationsHistory" ORDER BY "MigrationId"`);
1615
+ return r.rows.map((row) => row.MigrationId);
1616
+ } finally {
1617
+ await client.end();
1618
+ }
1619
+ }
1620
+ function toApplied(entries) {
1621
+ return entries.map((e) => ({ version: efVersion(e.id), description: efDescription(e.id) }));
1622
+ }
1623
+ async function statusEfcore(ctx) {
1624
+ const entries = await listEfMigrations(ctx);
1625
+ const history = await (ctx.readHistory ?? readEfHistory)(ctx.dsn);
1626
+ const current = history && history.length > 0 ? efVersion(history[history.length - 1]) : void 0;
1627
+ const pending = entries.filter((e) => e.applied !== true).map((e) => ({ version: efVersion(e.id), filename: `${e.id}.cs`, description: efDescription(e.id) }));
1628
+ return { current, pending, tool: "efcore" };
1629
+ }
1630
+ async function applyEfcore(ctx) {
1631
+ const before = new Set((await listEfMigrations(ctx)).filter((e) => e.applied).map((e) => e.id));
1632
+ await runEf(ctx.projectDir, ["database", "update"], ctx.dsn, { noBuild: true });
1633
+ const after = await listEfMigrations(ctx, { noBuild: true });
1634
+ const applied = toApplied(after.filter((e) => e.applied && !before.has(e.id)));
1635
+ return { applied, alreadyAtLatest: applied.length === 0, tool: "efcore" };
1636
+ }
1637
+ function resolveEfTarget(target, applied) {
1638
+ const t = target.trim();
1639
+ if (t === "0") return "0";
1640
+ const rel = /^-(\d+)$/.exec(t);
1641
+ if (rel) {
1642
+ const steps = Number(rel[1]);
1643
+ if (steps < 1) throw new SchemaMigrationError(`Invalid rollback step: ${target}`);
1644
+ const index = applied.length - 1 - steps;
1645
+ if (index < -1) {
1646
+ throw new SchemaMigrationError(`Cannot roll back ${steps} migration(s); only ${applied.length} applied.`);
1647
+ }
1648
+ return index === -1 ? "0" : applied[index].id;
1649
+ }
1650
+ const match = applied.find((e) => e.id === t || efVersion(e.id) === t || e.name === t);
1651
+ if (!match) throw new SchemaMigrationError(`Rollback target '${target}' is not an applied migration.`);
1652
+ return match.id;
1653
+ }
1654
+ async function rollbackEfcore(ctx) {
1655
+ const beforeEntries = (await listEfMigrations(ctx)).filter((e) => e.applied);
1656
+ const targetId = resolveEfTarget(ctx.target, beforeEntries);
1657
+ await runEf(ctx.projectDir, ["database", "update", targetId], ctx.dsn, { noBuild: true });
1658
+ const stillApplied = new Set((await listEfMigrations(ctx, { noBuild: true })).filter((e) => e.applied).map((e) => e.id));
1659
+ return { rolledBack: toApplied(beforeEntries.filter((e) => !stillApplied.has(e.id)).reverse()), tool: "efcore" };
1660
+ }
1661
+ function efMigrationName(slug) {
1662
+ const name = slug.split(/[^A-Za-z0-9]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
1663
+ if (!name) throw new SchemaMigrationError(`Cannot derive a migration name from '${slug}'.`);
1664
+ return /^[0-9]/.test(name) ? `M${name}` : name;
1665
+ }
1666
+ async function createEfcoreMigration(opts) {
1667
+ const project = requireProject(opts.projectDir);
1668
+ const name = efMigrationName(opts.slug);
1669
+ const existing = new Set(fs10.existsSync(project.migrationsDir) ? fs10.readdirSync(project.migrationsDir) : []);
1670
+ await runEf(opts.projectDir, ["migrations", "add", name]);
1671
+ const created = fs10.readdirSync(project.migrationsDir).filter((f) => !existing.has(f) && MIGRATION_ID.test(f.replace(/\.cs$/, "")) && !f.endsWith(".Designer.cs"));
1672
+ if (created.length !== 1) {
1673
+ throw new SchemaMigrationError(`Expected one new migration file for ${name}, found: ${created.join(", ") || "none"}`);
1674
+ }
1675
+ return { id: created[0].replace(/\.cs$/, ""), path: path10.join(project.migrationsDir, created[0]) };
1676
+ }
1677
+ function localEfMigrationIds(projectDir) {
1678
+ const project = findEfCoreProject(projectDir);
1679
+ if (!project || !fs10.existsSync(project.migrationsDir)) return [];
1680
+ return fs10.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();
1681
+ }
1682
+ function efProductVersion(project) {
1683
+ try {
1684
+ const csproj = fs10.readFileSync(project.csproj, "utf-8");
1685
+ const m = new RegExp(`${EF_DESIGN_PACKAGE.replace(/\./g, "\\.")}"\\s+Version="([^"]+)"`).exec(csproj);
1686
+ if (m) return m[1];
1687
+ } catch {
1688
+ }
1689
+ return "unknown";
1690
+ }
1691
+ async function stampEfcore(ctx) {
1692
+ const project = requireProject(ctx.projectDir);
1693
+ const ids = localEfMigrationIds(ctx.projectDir);
1694
+ if (ids.length === 0) throw new SchemaMigrationError("No local EF Core migrations to stamp.");
1695
+ const rev = ctx.revision.trim();
1696
+ const end = rev === "head" ? ids.length - 1 : ids.findIndex((id) => id === rev || efVersion(id) === rev);
1697
+ if (end < 0) throw new SchemaMigrationError(`Stamp revision '${ctx.revision}' is not a local migration.`);
1698
+ const toStamp = ids.slice(0, end + 1);
1699
+ const productVersion = efProductVersion(project);
1700
+ const client = new Client2({ connectionString: ctx.dsn, application_name: connectionApplicationName() });
1701
+ await client.connect();
1702
+ try {
1703
+ await client.query("BEGIN");
1704
+ await client.query(
1705
+ `CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
1706
+ "MigrationId" character varying(150) NOT NULL,
1707
+ "ProductVersion" character varying(32) NOT NULL,
1708
+ CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId"))`
1709
+ );
1710
+ await client.query(`DELETE FROM "__EFMigrationsHistory" WHERE NOT ("MigrationId" = ANY($1::text[]))`, [toStamp]);
1711
+ for (const id of toStamp) {
1712
+ await client.query(
1713
+ `INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") VALUES ($1, $2)
1714
+ ON CONFLICT ("MigrationId") DO NOTHING`,
1715
+ [id, productVersion]
1716
+ );
1717
+ }
1718
+ await client.query("COMMIT");
1719
+ } catch (err) {
1720
+ await client.query("ROLLBACK").catch(() => void 0);
1721
+ throw err;
1722
+ } finally {
1723
+ await client.end();
1724
+ }
1725
+ return efVersion(toStamp[toStamp.length - 1]);
1726
+ }
1727
+
1728
+ // scripts/lakebase/adapters/efcore-adapter.ts
1729
+ async function buildDsn4(args) {
1730
+ const result = await getConnection({
1731
+ output: "dsn",
1732
+ instance: args.instance,
1733
+ branch: args.branch,
1734
+ database: args.database,
1735
+ endpointName: args.endpointName
1736
+ });
1737
+ return result.url;
1738
+ }
1739
+ function errMsg(err) {
1740
+ return err instanceof Error ? err.message : String(err);
1741
+ }
1742
+ function listEfCoreFiles(projectDir) {
1743
+ return localEfMigrationIds(projectDir).map((id) => ({
1744
+ version: efVersion(id),
1745
+ filename: `${id}.cs`,
1746
+ description: efDescription(id),
1747
+ type: "CSharp",
1748
+ tool: "efcore"
1749
+ }));
1750
+ }
1751
+ var EfCoreAdapter = {
1752
+ id: "efcore",
1753
+ languages: ["dotnet"],
1754
+ /** A .NET project (root marker) with a *.csproj that references Microsoft.EntityFrameworkCore.Design. */
1755
+ detect(projectDir) {
1756
+ return isDotnetProject(projectDir) && findEfCoreProject(projectDir) !== null;
1757
+ },
1758
+ async apply(args) {
1759
+ try {
1760
+ const dsn = await buildDsn4(args);
1761
+ const r = await applyEfcore({ projectDir: args.projectDir, dsn });
1762
+ return {
1763
+ applied_migrations: r.applied,
1764
+ status: r.alreadyAtLatest ? "noop" : "ok",
1765
+ tool_specific: { alreadyAtLatest: r.alreadyAtLatest, tool: r.tool }
1766
+ };
1767
+ } catch (err) {
1768
+ return { applied_migrations: [], status: "error", error: errMsg(err) };
1769
+ }
1770
+ },
1771
+ async rollback(args) {
1772
+ try {
1773
+ const dsn = await buildDsn4(args);
1774
+ const r = await rollbackEfcore({ projectDir: args.projectDir, dsn, target: args.target });
1775
+ return {
1776
+ rolled_back: r.rolledBack,
1777
+ status: r.rolledBack.length === 0 ? "noop" : "ok",
1778
+ tool_specific: { tool: r.tool }
1779
+ };
1780
+ } catch (err) {
1781
+ return { rolled_back: [], status: "error", error: errMsg(err) };
1782
+ }
1783
+ },
1784
+ async stamp(args) {
1785
+ try {
1786
+ const dsn = await buildDsn4(args);
1787
+ const stamped = await stampEfcore({ projectDir: args.projectDir, dsn, revision: args.revision });
1788
+ return { status: "ok", stamped_revision: stamped, tool_specific: { tool: "efcore" } };
1789
+ } catch (err) {
1790
+ return { status: "error", stamped_revision: null, error: errMsg(err) };
1791
+ }
1792
+ },
1793
+ async status(args) {
1794
+ try {
1795
+ const dsn = await buildDsn4(args);
1796
+ const r = await statusEfcore({ projectDir: args.projectDir, dsn });
1797
+ return {
1798
+ applied_version: r.current ?? null,
1799
+ pending: r.pending,
1800
+ applied: [],
1801
+ status: "ok",
1802
+ tool_specific: { tool: r.tool }
1803
+ };
1804
+ } catch (err) {
1805
+ return { applied_version: null, pending: [], applied: [], status: "error", error: errMsg(err) };
1806
+ }
1807
+ },
1808
+ async list(args) {
1809
+ return { files: listEfCoreFiles(args.projectDir) };
1810
+ },
1811
+ // baseline: use stamp (EF has no separate baseline concept).
1812
+ async newMigration(args) {
1813
+ try {
1814
+ const created = await createEfcoreMigration({ projectDir: args.projectDir, slug: args.slug });
1815
+ return {
1816
+ status: "ok",
1817
+ version: efVersion(created.id),
1818
+ filename: path11.basename(created.path),
1819
+ path: created.path
1820
+ };
1821
+ } catch (err) {
1822
+ return { status: "error", version: "", filename: "", path: "", error: errMsg(err) };
1823
+ }
1824
+ }
1825
+ };
1826
+ registerSchemaMigrationAdapter(EfCoreAdapter);
1827
+
1412
1828
  // scripts/lakebase/schema-migrate.ts
1413
1829
  var SchemaMigrationError = class extends Error {
1414
1830
  constructor(message, cause) {
@@ -1418,8 +1834,11 @@ var SchemaMigrationError = class extends Error {
1418
1834
  }
1419
1835
  cause;
1420
1836
  };
1421
- function toolForLanguage(language) {
1837
+ function toolForLanguage(language, projectDir) {
1422
1838
  switch (language) {
1839
+ case "dotnet":
1840
+ if (projectDir && isDotnetFlywayProject(projectDir)) return "flyway";
1841
+ return "efcore";
1423
1842
  case "java":
1424
1843
  case "kotlin":
1425
1844
  return "flyway";
@@ -1430,7 +1849,7 @@ function toolForLanguage(language) {
1430
1849
  }
1431
1850
  }
1432
1851
  function adapterFor(projectDir, language) {
1433
- const override = language ? toolForLanguage(language) : void 0;
1852
+ const override = language ? toolForLanguage(language, projectDir) : void 0;
1434
1853
  return resolveSchemaMigrationAdapter(projectDir, override);
1435
1854
  }
1436
1855
  function migrationTimestamp(now = /* @__PURE__ */ new Date()) {
@@ -1518,7 +1937,7 @@ Usage:
1518
1937
  Flags:
1519
1938
  --name, -m <text> Human description; slugified into the filename (required)
1520
1939
  --project-dir <dir> Project root (default: cwd)
1521
- --language <lang> java | kotlin | python | nodejs (default: auto-detect)
1940
+ --language <lang> java | kotlin | python | nodejs | dotnet (default: auto-detect)
1522
1941
  --autogenerate Alembic only: diff models vs the branch DB to populate
1523
1942
  the body (requires --instance + --branch)
1524
1943
  --instance <id> Lakebase project id (only with --autogenerate)