@noormdev/sdk 1.0.0-alpha.7 → 1.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { observer, processFile, isTemplate } from './chunk-AT6SZ6UD.js';
2
- import { existsSync, readFileSync, mkdirSync, writeFileSync, constants } from 'fs';
3
- import path6, { join, dirname } from 'path';
4
- import { makeNestedConfig, attempt, retry, clone, merge, attemptSync } from '@logosdx/utils';
1
+ import { observer, processFile, DtReader, FORMAT_VERSION, DT_EXTENSIONS, encryptWithPassphrase, isTemplate, ENCODED_TYPES, GZIP_THRESHOLD, GZIP_RATIO_THRESHOLD } from './chunk-BMB5MF2T.js';
2
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, createWriteStream, constants } from 'fs';
3
+ import path7, { join, dirname } from 'path';
4
+ import { makeNestedConfig, gigabytes, attempt, retry, clone, merge, attemptSync } from '@logosdx/utils';
5
5
  import { homedir, userInfo } from 'os';
6
6
  import { readFile, stat, readdir, rm, access, mkdir, writeFile } from 'fs/promises';
7
7
  import { createHash, createDecipheriv, randomBytes, createCipheriv, hkdfSync } from 'crypto';
@@ -11,11 +11,31 @@ import { execSync } from 'child_process';
11
11
  import { sql } from 'kysely';
12
12
  import 'dayjs';
13
13
  import ansis from 'ansis';
14
+ import { createGzip, gunzipSync, gzipSync } from 'zlib';
15
+ import { pipeline } from 'stream/promises';
16
+ import { PassThrough } from 'stream';
17
+ import JSON5 from 'json5';
14
18
 
15
19
  var NOORM_HOME = join(homedir(), ".noorm");
16
20
  var PRIVATE_KEY_PATH = join(NOORM_HOME, "identity.key");
17
- join(NOORM_HOME, "identity.pub");
18
- join(NOORM_HOME, "identity.json");
21
+ var PUBLIC_KEY_PATH = join(NOORM_HOME, "identity.pub");
22
+ var IDENTITY_METADATA_PATH = join(NOORM_HOME, "identity.json");
23
+ async function loadIdentityMetadata() {
24
+ const [content, err] = await attempt(
25
+ () => readFile(IDENTITY_METADATA_PATH, { encoding: "utf8" })
26
+ );
27
+ if (err) {
28
+ if (err.code === "ENOENT") {
29
+ return null;
30
+ }
31
+ throw new Error(`Failed to read identity metadata: ${err.message}`);
32
+ }
33
+ const [parsed, parseErr] = attemptSync(() => JSON.parse(content));
34
+ if (parseErr) {
35
+ return null;
36
+ }
37
+ return parsed;
38
+ }
19
39
  async function loadPrivateKey() {
20
40
  const [content, err] = await attempt(() => readFile(PRIVATE_KEY_PATH, { encoding: "utf8" }));
21
41
  if (err) {
@@ -26,6 +46,16 @@ async function loadPrivateKey() {
26
46
  }
27
47
  return content.trim();
28
48
  }
49
+ async function loadPublicKey() {
50
+ const [content, err] = await attempt(() => readFile(PUBLIC_KEY_PATH, { encoding: "utf8" }));
51
+ if (err) {
52
+ if (err.code === "ENOENT") {
53
+ return null;
54
+ }
55
+ throw new Error(`Failed to read public key: ${err.message}`);
56
+ }
57
+ return content.trim();
58
+ }
29
59
  function deriveStateKey(privateKey) {
30
60
  const privateKeyBuffer = Buffer.from(privateKey, "hex");
31
61
  return Buffer.from(
@@ -127,7 +157,7 @@ function getPackageVersion() {
127
157
  }
128
158
 
129
159
  // src/core/state/manager.ts
130
- var DEFAULT_STATE_DIR = ".noorm";
160
+ var DEFAULT_STATE_DIR = ".noorm/state";
131
161
  var DEFAULT_STATE_FILE = "state.enc";
132
162
  var StateManager = class {
133
163
  constructor(projectRoot, options = {}) {
@@ -632,7 +662,7 @@ var StrictConfigSchema = z.object({
632
662
  var LoggingConfigSchema = z.object({
633
663
  enabled: z.boolean().default(true),
634
664
  level: LogLevelSchema.default("info"),
635
- file: z.string().default(".noorm/noorm.log"),
665
+ file: z.string().default(".noorm/state/noorm.log"),
636
666
  maxSize: FileSizeSchema.default("10mb"),
637
667
  maxFiles: z.number().int().min(1).default(5)
638
668
  });
@@ -682,7 +712,7 @@ var DEFAULT_STRICT_CONFIG = {
682
712
  var DEFAULT_LOGGING_CONFIG = {
683
713
  enabled: true,
684
714
  level: "info",
685
- file: ".noorm/noorm.log",
715
+ file: ".noorm/state/noorm.log",
686
716
  maxSize: "10mb",
687
717
  maxFiles: 5
688
718
  };
@@ -746,13 +776,13 @@ function evaluateRules(rules, config) {
746
776
  const result = evaluateRule(rule, config);
747
777
  if (result.matched) {
748
778
  matchedRules.push(rule);
749
- for (const path7 of result.include) {
750
- includeSet.add(path7);
751
- excludeSet.delete(path7);
779
+ for (const path8 of result.include) {
780
+ includeSet.add(path8);
781
+ excludeSet.delete(path8);
752
782
  }
753
- for (const path7 of result.exclude) {
754
- excludeSet.add(path7);
755
- includeSet.delete(path7);
783
+ for (const path8 of result.exclude) {
784
+ excludeSet.add(path8);
785
+ includeSet.delete(path8);
756
786
  }
757
787
  }
758
788
  }
@@ -765,13 +795,13 @@ function evaluateRules(rules, config) {
765
795
  function mergeWithBuildConfig(buildInclude, buildExclude, ruleResult) {
766
796
  const includeSet = new Set(buildInclude);
767
797
  const excludeSet = new Set(buildExclude);
768
- for (const path7 of ruleResult.include) {
769
- includeSet.add(path7);
770
- excludeSet.delete(path7);
798
+ for (const path8 of ruleResult.include) {
799
+ includeSet.add(path8);
800
+ excludeSet.delete(path8);
771
801
  }
772
- for (const path7 of ruleResult.exclude) {
773
- excludeSet.add(path7);
774
- includeSet.delete(path7);
802
+ for (const path8 of ruleResult.exclude) {
803
+ excludeSet.add(path8);
804
+ includeSet.delete(path8);
775
805
  }
776
806
  return {
777
807
  include: Array.from(includeSet),
@@ -1441,6 +1471,20 @@ function formatIdentity(identity) {
1441
1471
  }
1442
1472
  return identity.name;
1443
1473
  }
1474
+ async function loadExistingIdentity() {
1475
+ const metadata = await loadIdentityMetadata();
1476
+ if (!metadata) {
1477
+ return null;
1478
+ }
1479
+ const publicKey = await loadPublicKey();
1480
+ if (!publicKey) {
1481
+ return null;
1482
+ }
1483
+ return {
1484
+ ...metadata,
1485
+ publicKey
1486
+ };
1487
+ }
1444
1488
  var ConnectionManager = class {
1445
1489
  #cached = /* @__PURE__ */ new Map();
1446
1490
  #tracked = /* @__PURE__ */ new Map();
@@ -1653,6 +1697,7 @@ async function createConnection(config, configName = "__default__") {
1653
1697
  destroy: wrappedDestroy
1654
1698
  };
1655
1699
  observer.emit("connection:open", { configName, dialect: config.dialect });
1700
+ await waitForIdentityToLoad(trackedConn.db);
1656
1701
  return trackedConn;
1657
1702
  }
1658
1703
  var SYSTEM_DATABASES = {
@@ -1680,6 +1725,223 @@ async function testConnection(config, options = {}) {
1680
1725
  return { ok: true };
1681
1726
  }
1682
1727
 
1728
+ // src/core/version/types.ts
1729
+ var CURRENT_VERSIONS = Object.freeze({
1730
+ /** Database tracking tables schema version */
1731
+ schema: 1,
1732
+ /** State file (state.enc) schema version */
1733
+ state: 1,
1734
+ /** Settings file (settings.yml) schema version */
1735
+ settings: 1
1736
+ });
1737
+ var VersionMismatchError = class extends Error {
1738
+ constructor(layer, current, expected) {
1739
+ super(
1740
+ `${layer} version ${current} is newer than CLI supports (${expected}). Please upgrade noorm.`
1741
+ );
1742
+ this.layer = layer;
1743
+ this.current = current;
1744
+ this.expected = expected;
1745
+ this.name = "VersionMismatchError";
1746
+ }
1747
+ };
1748
+ var MigrationError = class extends Error {
1749
+ constructor(layer, version, cause) {
1750
+ super(`${layer} migration v${version} failed: ${cause.message}`);
1751
+ this.layer = layer;
1752
+ this.version = version;
1753
+ this.cause = cause;
1754
+ this.name = "MigrationError";
1755
+ }
1756
+ };
1757
+ function addIdColumn(builder, dialect) {
1758
+ if (dialect === "postgres") {
1759
+ return builder.addColumn("id", "serial", (col) => col.primaryKey());
1760
+ }
1761
+ return builder.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement());
1762
+ }
1763
+ var v1 = {
1764
+ version: 1,
1765
+ description: "Create initial tracking tables",
1766
+ async up(db, dialect) {
1767
+ await addIdColumn(db.schema.createTable("__noorm_version__"), dialect).addColumn("cli_version", "varchar(50)", (col) => col.notNull()).addColumn("noorm_version", "integer", (col) => col.notNull()).addColumn("state_version", "integer", (col) => col.notNull()).addColumn("settings_version", "integer", (col) => col.notNull()).addColumn(
1768
+ "installed_at",
1769
+ "timestamp",
1770
+ (col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
1771
+ ).addColumn(
1772
+ "upgraded_at",
1773
+ "timestamp",
1774
+ (col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
1775
+ ).execute();
1776
+ await addIdColumn(db.schema.createTable("__noorm_change__"), dialect).addColumn("name", "varchar(255)", (col) => col.notNull()).addColumn("change_type", "varchar(50)", (col) => col.notNull()).addColumn("direction", "varchar(50)", (col) => col.notNull()).addColumn("checksum", "varchar(64)", (col) => col.notNull().defaultTo("")).addColumn(
1777
+ "executed_at",
1778
+ "timestamp",
1779
+ (col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
1780
+ ).addColumn("executed_by", "varchar(255)", (col) => col.notNull().defaultTo("")).addColumn("config_name", "varchar(255)", (col) => col.notNull().defaultTo("")).addColumn("cli_version", "varchar(50)", (col) => col.notNull().defaultTo("")).addColumn("status", "varchar(50)", (col) => col.notNull()).addColumn("error_message", "text", (col) => col.notNull().defaultTo("")).addColumn("duration_ms", "integer", (col) => col.notNull().defaultTo(0)).execute();
1781
+ await addIdColumn(db.schema.createTable("__noorm_executions__"), dialect).addColumn(
1782
+ "change_id",
1783
+ "integer",
1784
+ (col) => col.notNull().references("__noorm_change__.id").onDelete("cascade")
1785
+ ).addColumn("filepath", "varchar(500)", (col) => col.notNull()).addColumn("file_type", "varchar(10)", (col) => col.notNull()).addColumn("checksum", "varchar(64)", (col) => col.notNull().defaultTo("")).addColumn("cli_version", "varchar(50)", (col) => col.notNull().defaultTo("")).addColumn("status", "varchar(50)", (col) => col.notNull()).addColumn("error_message", "text", (col) => col.notNull().defaultTo("")).addColumn("skip_reason", "varchar(100)", (col) => col.notNull().defaultTo("")).addColumn("duration_ms", "integer", (col) => col.notNull().defaultTo(0)).execute();
1786
+ await addIdColumn(db.schema.createTable("__noorm_lock__"), dialect).addColumn("config_name", "varchar(255)", (col) => col.notNull().unique()).addColumn("locked_by", "varchar(255)", (col) => col.notNull()).addColumn(
1787
+ "locked_at",
1788
+ "timestamp",
1789
+ (col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
1790
+ ).addColumn("expires_at", "timestamp", (col) => col.notNull()).addColumn("reason", "varchar(255)", (col) => col.notNull().defaultTo("")).execute();
1791
+ await addIdColumn(db.schema.createTable("__noorm_identities__"), dialect).addColumn("identity_hash", "varchar(64)", (col) => col.notNull().unique()).addColumn("email", "varchar(255)", (col) => col.notNull()).addColumn("name", "varchar(255)", (col) => col.notNull()).addColumn("machine", "varchar(255)", (col) => col.notNull()).addColumn("os", "varchar(255)", (col) => col.notNull()).addColumn("public_key", "text", (col) => col.notNull()).addColumn("encrypted_vault_key", "text").addColumn(
1792
+ "registered_at",
1793
+ "timestamp",
1794
+ (col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
1795
+ ).addColumn(
1796
+ "last_seen_at",
1797
+ "timestamp",
1798
+ (col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
1799
+ ).execute();
1800
+ await addIdColumn(db.schema.createTable("__noorm_vault__"), dialect).addColumn("secret_key", "varchar(255)", (col) => col.notNull().unique()).addColumn("encrypted_value", "text", (col) => col.notNull()).addColumn("set_by", "varchar(255)", (col) => col.notNull()).addColumn(
1801
+ "created_at",
1802
+ "timestamp",
1803
+ (col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
1804
+ ).addColumn(
1805
+ "updated_at",
1806
+ "timestamp",
1807
+ (col) => col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
1808
+ ).execute();
1809
+ await db.schema.createIndex("idx_executions_change_id").on("__noorm_executions__").column("change_id").execute();
1810
+ await db.schema.createIndex("idx_change_name_config").on("__noorm_change__").columns(["name", "config_name"]).execute();
1811
+ await db.schema.createIndex("idx_vault_secret_key").on("__noorm_vault__").column("secret_key").execute();
1812
+ },
1813
+ async down(db, _dialect) {
1814
+ await db.schema.dropIndex("idx_change_name_config").execute();
1815
+ await db.schema.dropIndex("idx_executions_change_id").execute();
1816
+ await db.schema.dropIndex("idx_vault_secret_key").execute();
1817
+ await db.schema.dropTable("__noorm_vault__").execute();
1818
+ await db.schema.dropTable("__noorm_identities__").execute();
1819
+ await db.schema.dropTable("__noorm_lock__").execute();
1820
+ await db.schema.dropTable("__noorm_executions__").execute();
1821
+ await db.schema.dropTable("__noorm_change__").execute();
1822
+ await db.schema.dropTable("__noorm_version__").execute();
1823
+ }
1824
+ };
1825
+
1826
+ // src/core/update/checker.ts
1827
+ function getCurrentVersion() {
1828
+ return typeof __CLI_VERSION__ !== "undefined" ? __CLI_VERSION__ : "0.0.0-dev";
1829
+ }
1830
+
1831
+ // src/core/version/schema/index.ts
1832
+ var MIGRATIONS = [v1];
1833
+ async function tablesExist(db) {
1834
+ const [result, err] = await attempt(async () => {
1835
+ await sql`SELECT 1 FROM __noorm_version__ LIMIT 1`.execute(db);
1836
+ return true;
1837
+ });
1838
+ if (err) return false;
1839
+ return result;
1840
+ }
1841
+ async function getSchemaVersion(db) {
1842
+ const exists = await tablesExist(db);
1843
+ if (!exists) return 0;
1844
+ const [result, err] = await attempt(async () => {
1845
+ return db.selectFrom("__noorm_version__").select("noorm_version").orderBy("id", "desc").limit(1).executeTakeFirst();
1846
+ });
1847
+ if (err) return 0;
1848
+ return result?.noorm_version ?? 0;
1849
+ }
1850
+ async function checkSchemaVersion(db) {
1851
+ const current = await getSchemaVersion(db);
1852
+ const expected = CURRENT_VERSIONS.schema;
1853
+ observer.emit("version:schema:checking", { current });
1854
+ return {
1855
+ current,
1856
+ expected,
1857
+ needsMigration: current < expected,
1858
+ isNewer: current > expected
1859
+ };
1860
+ }
1861
+ async function bootstrapSchema(db, dialect, options) {
1862
+ const start = performance.now();
1863
+ observer.emit("version:schema:migrating", {
1864
+ from: 0,
1865
+ to: CURRENT_VERSIONS.schema
1866
+ });
1867
+ for (const migration of MIGRATIONS) {
1868
+ const [, err] = await attempt(() => migration.up(db, dialect));
1869
+ if (err) {
1870
+ throw new MigrationError("schema", migration.version, err);
1871
+ }
1872
+ }
1873
+ await db.insertInto("__noorm_version__").values({
1874
+ cli_version: getCurrentVersion(),
1875
+ noorm_version: CURRENT_VERSIONS.schema,
1876
+ state_version: CURRENT_VERSIONS.state,
1877
+ settings_version: CURRENT_VERSIONS.settings
1878
+ }).execute();
1879
+ const durationMs = performance.now() - start;
1880
+ observer.emit("version:schema:migrated", {
1881
+ from: 0,
1882
+ to: CURRENT_VERSIONS.schema,
1883
+ durationMs
1884
+ });
1885
+ await waitForIdentityToLoad(db);
1886
+ }
1887
+ async function getLatestVersionRecord(db) {
1888
+ const exists = await tablesExist(db);
1889
+ if (!exists) return null;
1890
+ const [result, err] = await attempt(async () => {
1891
+ return db.selectFrom("__noorm_version__").select(["state_version", "settings_version"]).orderBy("id", "desc").limit(1).executeTakeFirst();
1892
+ });
1893
+ if (err || !result) return null;
1894
+ return {
1895
+ stateVersion: result.state_version,
1896
+ settingsVersion: result.settings_version
1897
+ };
1898
+ }
1899
+ async function migrateSchema(db, dialect, options) {
1900
+ const status = await checkSchemaVersion(db);
1901
+ if (status.isNewer) {
1902
+ observer.emit("version:mismatch", {
1903
+ layer: "schema",
1904
+ current: status.current,
1905
+ expected: status.expected
1906
+ });
1907
+ throw new VersionMismatchError("schema", status.current, status.expected);
1908
+ }
1909
+ if (!status.needsMigration) return;
1910
+ if (status.current === 0) {
1911
+ await bootstrapSchema(db, dialect);
1912
+ return;
1913
+ }
1914
+ const start = performance.now();
1915
+ observer.emit("version:schema:migrating", {
1916
+ from: status.current,
1917
+ to: CURRENT_VERSIONS.schema
1918
+ });
1919
+ const existing = await getLatestVersionRecord(db);
1920
+ const pendingMigrations = MIGRATIONS.filter((m) => m.version > status.current);
1921
+ for (const migration of pendingMigrations) {
1922
+ const [, err] = await attempt(() => migration.up(db, dialect));
1923
+ if (err) {
1924
+ throw new MigrationError("schema", migration.version, err);
1925
+ }
1926
+ }
1927
+ await db.insertInto("__noorm_version__").values({
1928
+ cli_version: getCurrentVersion(),
1929
+ noorm_version: CURRENT_VERSIONS.schema,
1930
+ state_version: existing?.stateVersion ?? CURRENT_VERSIONS.state,
1931
+ settings_version: existing?.settingsVersion ?? CURRENT_VERSIONS.settings
1932
+ }).execute();
1933
+ const durationMs = performance.now() - start;
1934
+ observer.emit("version:schema:migrated", {
1935
+ from: status.current,
1936
+ to: CURRENT_VERSIONS.schema,
1937
+ durationMs
1938
+ });
1939
+ await waitForIdentityToLoad(db);
1940
+ }
1941
+ async function ensureSchemaVersion(db, dialect, options) {
1942
+ await migrateSchema(db, dialect);
1943
+ }
1944
+
1683
1945
  // src/core/shared/tables.ts
1684
1946
  var NOORM_TABLES = Object.freeze({
1685
1947
  /** Version tracking table */
@@ -1691,7 +1953,9 @@ var NOORM_TABLES = Object.freeze({
1691
1953
  /** Concurrent operation lock table */
1692
1954
  lock: "__noorm_lock__",
1693
1955
  /** Team member identity table */
1694
- identities: "__noorm_identities__"
1956
+ identities: "__noorm_identities__",
1957
+ /** Vault secrets table */
1958
+ vault: "__noorm_vault__"
1695
1959
  });
1696
1960
 
1697
1961
  // src/core/environment.ts
@@ -1824,6 +2088,71 @@ addMaskedFields([
1824
2088
  "session_secret"
1825
2089
  ]);
1826
2090
 
2091
+ // src/core/identity/sync.ts
2092
+ async function registerIdentity(db, identity) {
2093
+ const [existing, selectErr] = await attempt(
2094
+ () => db.selectFrom("__noorm_identities__").select(["id"]).where("identity_hash", "=", identity.identityHash).executeTakeFirst()
2095
+ );
2096
+ if (selectErr) {
2097
+ observer.emit("error", {
2098
+ source: "identity:register:select",
2099
+ error: selectErr,
2100
+ context: {
2101
+ identityHash: identity.identityHash,
2102
+ name: identity.name,
2103
+ email: identity.email
2104
+ }
2105
+ });
2106
+ return { ok: false, registered: false, error: selectErr.message };
2107
+ }
2108
+ if (existing) {
2109
+ const [, updateErr] = await attempt(
2110
+ () => db.updateTable("__noorm_identities__").set({ last_seen_at: /* @__PURE__ */ new Date() }).where("identity_hash", "=", identity.identityHash).execute()
2111
+ );
2112
+ if (updateErr) {
2113
+ observer.emit("error", {
2114
+ source: "identity:register:update",
2115
+ error: updateErr,
2116
+ context: {
2117
+ identityHash: identity.identityHash,
2118
+ name: identity.name,
2119
+ email: identity.email
2120
+ }
2121
+ });
2122
+ return { ok: false, registered: false, error: updateErr.message };
2123
+ }
2124
+ return { ok: true, registered: false };
2125
+ }
2126
+ const [, insertErr] = await attempt(
2127
+ () => db.insertInto("__noorm_identities__").values({
2128
+ identity_hash: identity.identityHash,
2129
+ email: identity.email,
2130
+ name: identity.name,
2131
+ machine: identity.machine,
2132
+ os: identity.os,
2133
+ public_key: identity.publicKey
2134
+ }).execute()
2135
+ );
2136
+ if (insertErr) {
2137
+ observer.emit("error", {
2138
+ source: "identity:register:insert",
2139
+ error: insertErr,
2140
+ context: {
2141
+ identityHash: identity.identityHash,
2142
+ name: identity.name,
2143
+ email: identity.email
2144
+ }
2145
+ });
2146
+ return { ok: false, registered: false, error: insertErr.message };
2147
+ }
2148
+ observer.emit("identity:registered", {
2149
+ identityHash: identity.identityHash,
2150
+ name: identity.name,
2151
+ email: identity.email
2152
+ });
2153
+ return { ok: true, registered: true };
2154
+ }
2155
+
1827
2156
  // src/core/identity/index.ts
1828
2157
  var cachedIdentity = null;
1829
2158
  function resolveIdentity2(options = {}) {
@@ -1838,6 +2167,17 @@ function resolveIdentity2(options = {}) {
1838
2167
  function getIdentityForConfig(config) {
1839
2168
  return resolveIdentity2({ configIdentity: config.identity });
1840
2169
  }
2170
+ async function waitForIdentityToLoad(db) {
2171
+ const identity = await loadExistingIdentity();
2172
+ if (!identity) observer.emit("identity:not-found");
2173
+ if (!identity) return;
2174
+ const [, err] = await attempt(() => registerIdentity(db, identity));
2175
+ if (err) observer.emit("error", {
2176
+ error: err,
2177
+ source: "identity:ensure",
2178
+ context: { identity }
2179
+ });
2180
+ }
1841
2181
  var DialectSchema2 = z.enum(["postgres", "mysql", "sqlite", "mssql"]);
1842
2182
  var ConfigNameSchema = z.string().min(1, "Config name is required").regex(
1843
2183
  /^[a-z0-9_-]+$/i,
@@ -2034,6 +2374,78 @@ function resolveFromEnvOnly(envConfig, flags, stageName, settings) {
2034
2374
  }
2035
2375
  return parseConfig(merged);
2036
2376
  }
2377
+ function buildProcCall(dialect, name, params) {
2378
+ if (dialect === "sqlite") {
2379
+ throw new Error("SQLite does not support stored procedures.");
2380
+ }
2381
+ const rawName = sql.raw(name);
2382
+ if (!params || Array.isArray(params) && params.length === 0 || !Array.isArray(params) && Object.keys(params).length === 0) {
2383
+ if (dialect === "mssql") {
2384
+ return sql`EXEC ${rawName}`;
2385
+ }
2386
+ return sql`CALL ${rawName}()`;
2387
+ }
2388
+ if (dialect === "mssql") {
2389
+ return buildMssqlProc(rawName, params);
2390
+ }
2391
+ if (dialect === "postgres") {
2392
+ return buildPostgresProc(rawName, params);
2393
+ }
2394
+ return buildMysqlProc(rawName, params);
2395
+ }
2396
+ function buildFuncCall(dialect, name, column, params) {
2397
+ if (dialect === "sqlite") {
2398
+ throw new Error("SQLite does not support database function calls.");
2399
+ }
2400
+ const rawName = sql.raw(name);
2401
+ const rawCol = sql.raw(column);
2402
+ if (!params || Array.isArray(params) && params.length === 0 || !Array.isArray(params) && Object.keys(params).length === 0) {
2403
+ return sql`SELECT ${rawName}() AS ${rawCol}`;
2404
+ }
2405
+ if (dialect === "postgres" && !Array.isArray(params)) {
2406
+ const parts = Object.entries(params).map(
2407
+ ([key, val]) => sql`${sql.raw(key)} => ${val}`
2408
+ );
2409
+ return sql`SELECT ${rawName}(${sql.join(parts)}) AS ${rawCol}`;
2410
+ }
2411
+ if (dialect === "mssql" && !Array.isArray(params)) {
2412
+ const parts = Object.entries(params).map(
2413
+ ([key, val]) => sql`${sql.raw(`@${key}`)} = ${val}`
2414
+ );
2415
+ return sql`DECLARE @__result sql_variant; EXEC @__result = ${rawName} ${sql.join(parts)}; SELECT @__result AS ${rawCol}`;
2416
+ }
2417
+ if (dialect === "mysql" && !Array.isArray(params)) {
2418
+ throw new Error("MySQL does not support named parameters in function calls. Use positional parameters (array) instead.");
2419
+ }
2420
+ const values = Array.isArray(params) ? params : Object.values(params);
2421
+ const joined = sql.join(values.map((v) => sql`${v}`));
2422
+ return sql`SELECT ${rawName}(${joined}) AS ${rawCol}`;
2423
+ }
2424
+ function buildMssqlProc(rawName, params) {
2425
+ if (Array.isArray(params)) {
2426
+ const joined = sql.join(params.map((v) => sql`${v}`));
2427
+ return sql`EXEC ${rawName} ${joined}`;
2428
+ }
2429
+ const parts = Object.entries(params).map(
2430
+ ([key, val]) => sql`${sql.raw(`@${key}`)} = ${val}`
2431
+ );
2432
+ return sql`EXEC ${rawName} ${sql.join(parts)}`;
2433
+ }
2434
+ function buildPostgresProc(rawName, params) {
2435
+ if (Array.isArray(params)) {
2436
+ const joined = sql.join(params.map((v) => sql`${v}`));
2437
+ return sql`CALL ${rawName}(${joined})`;
2438
+ }
2439
+ const parts = Object.entries(params).map(
2440
+ ([key, val]) => sql`${sql.raw(key)} => ${val}`
2441
+ );
2442
+ return sql`CALL ${rawName}(${sql.join(parts)})`;
2443
+ }
2444
+ function buildMysqlProc(rawName, params) {
2445
+ const values = Array.isArray(params) ? params : Object.values(params);
2446
+ const joined = sql.join(values.map((v) => sql`${v}`));
2447
+ return sql`CALL ${rawName}(${joined})`;
2448
+ }
2037
2449
  var EXCLUDED_SCHEMAS = ["pg_catalog", "information_schema", "pg_toast"];
2038
2450
  var postgresExploreOperations = {
2039
2451
  async getOverview(db) {
@@ -4495,21 +4907,21 @@ var DATE_PREFIX_REGEX = /^(\d{4}-\d{2}-\d{2})-(.+)$/;
4495
4907
  async function parseChange(folderPath, sqlDir) {
4496
4908
  const [folderStat, statErr] = await attempt(() => stat(folderPath));
4497
4909
  if (statErr || !folderStat?.isDirectory()) {
4498
- throw new ChangeNotFoundError(path6.basename(folderPath));
4910
+ throw new ChangeNotFoundError(path7.basename(folderPath));
4499
4911
  }
4500
- const name = path6.basename(folderPath);
4912
+ const name = path7.basename(folderPath);
4501
4913
  const { date, description } = parseName(name);
4502
- const changePath = path6.join(folderPath, "change");
4914
+ const changePath = path7.join(folderPath, "change");
4503
4915
  const [changeFiles, changeErr] = await attempt(() => scanFolder(changePath, sqlDir));
4504
4916
  if (changeErr && !isNotFoundError(changeErr)) {
4505
4917
  throw changeErr;
4506
4918
  }
4507
- const revertPath = path6.join(folderPath, "revert");
4919
+ const revertPath = path7.join(folderPath, "revert");
4508
4920
  const [revertFiles, revertErr] = await attempt(() => scanFolder(revertPath, sqlDir));
4509
4921
  if (revertErr && !isNotFoundError(revertErr)) {
4510
4922
  throw revertErr;
4511
4923
  }
4512
- const changelogPath = path6.join(folderPath, "changelog.md");
4924
+ const changelogPath = path7.join(folderPath, "changelog.md");
4513
4925
  const hasChangelog = await fileExists(changelogPath);
4514
4926
  if ((!changeFiles || changeFiles.length === 0) && (!revertFiles || revertFiles.length === 0)) {
4515
4927
  throw new ChangeValidationError(
@@ -4544,7 +4956,7 @@ async function discoverChanges(changesDir, sqlDir) {
4544
4956
  const folders = entries.filter((e) => e.isDirectory());
4545
4957
  const changes = [];
4546
4958
  for (const folder of folders) {
4547
- const folderPath = path6.join(changesDir, folder.name);
4959
+ const folderPath = path7.join(changesDir, folder.name);
4548
4960
  const [change, parseErr] = await attempt(() => parseChange(folderPath, sqlDir));
4549
4961
  if (parseErr) {
4550
4962
  observer.emit("error", {
@@ -4566,13 +4978,13 @@ async function resolveManifest(manifestPath, sqlDir) {
4566
4978
  const lines = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
4567
4979
  if (lines.length === 0) {
4568
4980
  throw new ChangeValidationError(
4569
- path6.basename(path6.dirname(path6.dirname(manifestPath))),
4570
- `Empty manifest file: ${path6.basename(manifestPath)}`
4981
+ path7.basename(path7.dirname(path7.dirname(manifestPath))),
4982
+ `Empty manifest file: ${path7.basename(manifestPath)}`
4571
4983
  );
4572
4984
  }
4573
4985
  const resolvedPaths = [];
4574
4986
  for (const relativePath of lines) {
4575
- const absolutePath = path6.join(sqlDir, relativePath);
4987
+ const absolutePath = path7.join(sqlDir, relativePath);
4576
4988
  const [exists] = await attempt(() => access(absolutePath, constants.R_OK));
4577
4989
  if (exists === void 0) {
4578
4990
  resolvedPaths.push(absolutePath);
@@ -4624,7 +5036,7 @@ async function scanFolder(folderPath, sqlDir) {
4624
5036
  for (const entry of entries) {
4625
5037
  if (!entry.isFile()) continue;
4626
5038
  const filename = entry.name;
4627
- const filePath = path6.join(folderPath, filename);
5039
+ const filePath = path7.join(folderPath, filename);
4628
5040
  const type = getFileType(filename);
4629
5041
  if (!type) continue;
4630
5042
  const file = {
@@ -6377,7 +6789,7 @@ async function executeFiles(context, change, files, direction, checksum, history
6377
6789
  });
6378
6790
  }
6379
6791
  if (failed) {
6380
- const skipReason = failedFile ? `${path6.basename(failedFile)} failed: ${failureError ?? "unknown error"}` : "change failed";
6792
+ const skipReason = failedFile ? `${path7.basename(failedFile)} failed: ${failureError ?? "unknown error"}` : "change failed";
6381
6793
  const skipError = await history.skipRemainingFiles(operationId, skipReason);
6382
6794
  if (skipError) {
6383
6795
  observer.emit("error", {
@@ -6389,7 +6801,7 @@ async function executeFiles(context, change, files, direction, checksum, history
6389
6801
  }
6390
6802
  const totalDurationMs = performance.now() - startTime;
6391
6803
  const executionStatus = failed ? "failed" : "success";
6392
- const errorMessage = failedFile ? `${path6.basename(failedFile)}: ${failureError ?? "unknown error"}` : failureError;
6804
+ const errorMessage = failedFile ? `${path7.basename(failedFile)}: ${failureError ?? "unknown error"}` : failureError;
6393
6805
  const finalizeError = await history.finalizeOperation(
6394
6806
  operationId,
6395
6807
  executionStatus,
@@ -6462,7 +6874,7 @@ async function expandFiles(files, sqlDir) {
6462
6874
  if (file.resolvedPaths) {
6463
6875
  for (const resolvedPath of file.resolvedPaths) {
6464
6876
  expanded.push({
6465
- filename: path6.basename(resolvedPath),
6877
+ filename: path7.basename(resolvedPath),
6466
6878
  path: resolvedPath,
6467
6879
  type: "sql"
6468
6880
  });
@@ -6471,7 +6883,7 @@ async function expandFiles(files, sqlDir) {
6471
6883
  const resolved = await resolveManifest(file.path, sqlDir);
6472
6884
  for (const resolvedPath of resolved) {
6473
6885
  expanded.push({
6474
- filename: path6.basename(resolvedPath),
6886
+ filename: path7.basename(resolvedPath),
6475
6887
  path: resolvedPath,
6476
6888
  type: "sql"
6477
6889
  });
@@ -6587,13 +6999,13 @@ async function executeDryRun(context, change, files, direction, startTime) {
6587
6999
  };
6588
7000
  }
6589
7001
  function getDryRunOutputPath(projectRoot, filepath) {
6590
- const relativePath = path6.relative(projectRoot, filepath);
7002
+ const relativePath = path7.relative(projectRoot, filepath);
6591
7003
  const outputRelativePath = relativePath.endsWith(".tmpl") ? relativePath.slice(0, -5) : relativePath;
6592
- return path6.join(projectRoot, "tmp", outputRelativePath);
7004
+ return path7.join(projectRoot, "tmp", outputRelativePath);
6593
7005
  }
6594
7006
  async function writeDryRunOutput(projectRoot, filepath, content) {
6595
7007
  const outputPath = getDryRunOutputPath(projectRoot, filepath);
6596
- const outputDir = path6.dirname(outputPath);
7008
+ const outputDir = path7.dirname(outputPath);
6597
7009
  await mkdir(outputDir, { recursive: true });
6598
7010
  await writeFile(outputPath, content, "utf-8");
6599
7011
  }
@@ -6919,7 +7331,7 @@ var ChangeManager = class {
6919
7331
  */
6920
7332
  async remove(name, options) {
6921
7333
  if (options.disk) {
6922
- const changePath = path6.join(this.#context.changesDir, name);
7334
+ const changePath = path7.join(this.#context.changesDir, name);
6923
7335
  const [change, loadErr] = await attempt(
6924
7336
  () => parseChange(changePath, this.#context.sqlDir)
6925
7337
  );
@@ -6954,7 +7366,7 @@ var ChangeManager = class {
6954
7366
  * Load a change from disk by name.
6955
7367
  */
6956
7368
  async #loadChange(name) {
6957
- const changePath = path6.join(this.#context.changesDir, name);
7369
+ const changePath = path7.join(this.#context.changesDir, name);
6958
7370
  const [change, err] = await attempt(
6959
7371
  () => parseChange(changePath, this.#context.sqlDir)
6960
7372
  );
@@ -7188,17 +7600,21 @@ var FILE_HEADER_TEMPLATE = `-- =================================================
7188
7600
  -- ============================================================
7189
7601
 
7190
7602
  `;
7191
- async function runBuild(context, sqlPath, options = {}) {
7603
+ async function runBuild(context, sqlPath, options = {}, preFilteredFiles) {
7192
7604
  const start = performance.now();
7193
7605
  const opts = { ...DEFAULT_RUN_OPTIONS_INTERNAL, ...options };
7194
- const [files, discoverErr] = await attempt(() => discoverFiles(sqlPath));
7195
- if (discoverErr) {
7196
- observer.emit("error", {
7197
- source: "runner",
7198
- error: discoverErr,
7199
- context: { sqlPath, operation: "discover-files" }
7200
- });
7201
- return createFailedBatchResult(discoverErr.message, performance.now() - start);
7606
+ let files;
7607
+ {
7608
+ const [discovered, discoverErr] = await attempt(() => discoverFiles(sqlPath));
7609
+ if (discoverErr) {
7610
+ observer.emit("error", {
7611
+ source: "runner",
7612
+ error: discoverErr,
7613
+ context: { sqlPath, operation: "discover-files" }
7614
+ });
7615
+ return createFailedBatchResult(discoverErr.message, performance.now() - start);
7616
+ }
7617
+ files = discovered;
7202
7618
  }
7203
7619
  observer.emit("build:start", {
7204
7620
  sqlPath,
@@ -7777,13 +8193,13 @@ async function executeDryRun2(context, files) {
7777
8193
  return results;
7778
8194
  }
7779
8195
  function getDryRunOutputPath2(projectRoot, filepath) {
7780
- const relativePath = path6.relative(projectRoot, filepath);
8196
+ const relativePath = path7.relative(projectRoot, filepath);
7781
8197
  const outputRelativePath = relativePath.endsWith(".tmpl") ? relativePath.slice(0, -5) : relativePath;
7782
- return path6.join(projectRoot, "tmp", outputRelativePath);
8198
+ return path7.join(projectRoot, "tmp", outputRelativePath);
7783
8199
  }
7784
8200
  async function writeDryRunOutput2(projectRoot, filepath, content) {
7785
8201
  const outputPath = getDryRunOutputPath2(projectRoot, filepath);
7786
- const outputDir = path6.dirname(outputPath);
8202
+ const outputDir = path7.dirname(outputPath);
7787
8203
  await mkdir(outputDir, { recursive: true });
7788
8204
  await writeFile(outputPath, content, "utf-8");
7789
8205
  }
@@ -7795,7 +8211,7 @@ async function discoverFiles(dirpath) {
7795
8211
  throw new Error(`Failed to read directory: ${dir}`, { cause: err });
7796
8212
  }
7797
8213
  for (const entry of entries) {
7798
- const fullPath = path6.join(dir, entry.name);
8214
+ const fullPath = path7.join(dir, entry.name);
7799
8215
  if (entry.isDirectory()) {
7800
8216
  await scan(fullPath);
7801
8217
  } else if (entry.isFile() && isSqlFile(entry.name)) {
@@ -7819,200 +8235,2899 @@ function createFailedBatchResult(error, durationMs) {
7819
8235
  durationMs
7820
8236
  };
7821
8237
  }
7822
-
7823
- // src/sdk/guards.ts
7824
- var RequireTestError = class extends Error {
7825
- constructor(configName) {
7826
- super(`Config "${configName}" does not have isTest: true`);
7827
- this.configName = configName;
7828
- }
7829
- name = "RequireTestError";
7830
- };
7831
- var ProtectedConfigError = class extends Error {
7832
- constructor(configName, operation) {
7833
- super(`Cannot ${operation} on protected config "${configName}"`);
7834
- this.configName = configName;
7835
- this.operation = operation;
8238
+ async function withDualConnection(options, fn) {
8239
+ const { sourceConfig, destConfig, ensureSchema = true } = options;
8240
+ let sourceConn = null;
8241
+ let destConn = null;
8242
+ observer.emit("db:dual:connecting", {
8243
+ source: sourceConfig.name,
8244
+ destination: destConfig.name
8245
+ });
8246
+ const [result, err] = await attempt(async () => {
8247
+ sourceConn = await createConnection(sourceConfig.connection, sourceConfig.name);
8248
+ destConn = await createConnection(destConfig.connection, destConfig.name);
8249
+ observer.emit("db:dual:connected", {
8250
+ source: sourceConfig.name,
8251
+ destination: destConfig.name
8252
+ });
8253
+ if (ensureSchema) {
8254
+ await ensureSchemaVersion(
8255
+ destConn.db,
8256
+ destConn.dialect
8257
+ );
8258
+ }
8259
+ return fn({
8260
+ source: {
8261
+ config: sourceConfig,
8262
+ db: sourceConn.db,
8263
+ dialect: sourceConn.dialect
8264
+ },
8265
+ destination: {
8266
+ config: destConfig,
8267
+ db: destConn.db,
8268
+ dialect: destConn.dialect
8269
+ }
8270
+ });
8271
+ });
8272
+ const cleanupErrors = [];
8273
+ observer.emit("db:dual:disconnecting", {
8274
+ source: sourceConfig.name,
8275
+ destination: destConfig.name
8276
+ });
8277
+ if (sourceConn) {
8278
+ const [, closeErr] = await attempt(() => sourceConn.destroy());
8279
+ if (closeErr) cleanupErrors.push(closeErr);
7836
8280
  }
7837
- name = "ProtectedConfigError";
7838
- };
7839
- function checkRequireTest(config, options) {
7840
- if (options.requireTest && !config.isTest) {
7841
- throw new RequireTestError(config.name);
8281
+ if (destConn) {
8282
+ const [, closeErr] = await attempt(() => destConn.destroy());
8283
+ if (closeErr) cleanupErrors.push(closeErr);
7842
8284
  }
7843
- }
7844
- function checkProtectedConfig(config, operation, options) {
7845
- if (config.protected && !options.allowProtected) {
7846
- throw new ProtectedConfigError(config.name, operation);
8285
+ observer.emit("db:dual:disconnected", {
8286
+ source: sourceConfig.name,
8287
+ destination: destConfig.name
8288
+ });
8289
+ if (!err && cleanupErrors.length > 0) {
8290
+ observer.emit("db:dual:cleanup-warning", { errors: cleanupErrors.map((e) => e.message) });
7847
8291
  }
8292
+ return [result, err];
7848
8293
  }
7849
8294
 
7850
- // src/sdk/context.ts
7851
- var Context = class {
7852
- #connection = null;
7853
- #config;
7854
- #settings;
7855
- #identity;
7856
- #options;
7857
- #projectRoot;
7858
- #changeManager = null;
7859
- constructor(config, settings, identity, options, projectRoot) {
7860
- this.#config = config;
7861
- this.#settings = settings;
7862
- this.#identity = identity;
7863
- this.#options = options;
7864
- this.#projectRoot = projectRoot;
7865
- }
7866
- // ─────────────────────────────────────────────────────────
7867
- // Read-only Properties
7868
- // ─────────────────────────────────────────────────────────
7869
- get config() {
7870
- return this.#config;
7871
- }
7872
- get settings() {
7873
- return this.#settings;
8295
+ // src/core/transfer/same-server.ts
8296
+ var DEFAULT_PORTS = {
8297
+ postgres: 5432,
8298
+ mysql: 3306,
8299
+ mssql: 1433,
8300
+ sqlite: 0
8301
+ // Not applicable
8302
+ };
8303
+ function normalizeHost(host) {
8304
+ const h = (host ?? "localhost").toLowerCase();
8305
+ if (h === "127.0.0.1" || h === "::1" || h === "localhost.localdomain") {
8306
+ return "localhost";
7874
8307
  }
7875
- get identity() {
7876
- return this.#identity;
8308
+ return h;
8309
+ }
8310
+ function isSameServer(source, dest) {
8311
+ if (source.dialect !== dest.dialect) {
8312
+ return false;
7877
8313
  }
7878
- get dialect() {
7879
- return this.#config.connection.dialect;
8314
+ if (source.dialect === "sqlite") {
8315
+ return false;
7880
8316
  }
7881
- get connected() {
7882
- return this.#connection !== null;
8317
+ const srcHost = normalizeHost(source.host);
8318
+ const dstHost = normalizeHost(dest.host);
8319
+ const srcPort = source.port ?? DEFAULT_PORTS[source.dialect];
8320
+ const dstPort = dest.port ?? DEFAULT_PORTS[dest.dialect];
8321
+ if (srcHost !== dstHost || srcPort !== dstPort) {
8322
+ return false;
7883
8323
  }
7884
- get observer() {
7885
- return observer;
8324
+ if (source.dialect === "postgres") {
8325
+ return source.database === dest.database;
7886
8326
  }
7887
- get kysely() {
7888
- if (!this.#connection) {
7889
- throw new Error("Not connected. Call connect() first.");
8327
+ return true;
8328
+ }
8329
+ function quoteIdent(name) {
8330
+ return `"${name.replace(/"/g, '""')}"`;
8331
+ }
8332
+ var postgresTransferOperations = {
8333
+ getDisableFKSql() {
8334
+ return "SET session_replication_role = replica";
8335
+ },
8336
+ getEnableFKSql() {
8337
+ return "SET session_replication_role = DEFAULT";
8338
+ },
8339
+ getEnableIdentityInsertSql(_table) {
8340
+ return null;
8341
+ },
8342
+ getDisableIdentityInsertSql(_table) {
8343
+ return null;
8344
+ },
8345
+ getResetSequenceSql(table, column, schema) {
8346
+ const fullTable = schema ? `${quoteIdent(schema)}.${quoteIdent(table)}` : quoteIdent(table);
8347
+ return `
8348
+ SELECT setval(
8349
+ pg_get_serial_sequence('${schema ? schema + "." : ""}${table}', '${column}'),
8350
+ COALESCE((SELECT MAX(${quoteIdent(column)}) FROM ${fullTable}), 0) + 1,
8351
+ false
8352
+ )
8353
+ `.trim();
8354
+ },
8355
+ buildConflictInsert(table, columns, primaryKey, strategy) {
8356
+ const quotedCols = columns.map(quoteIdent).join(", ");
8357
+ const placeholders = columns.map((_, i) => `$${i + 1}`).join(", ");
8358
+ const pkCols = primaryKey.map(quoteIdent).join(", ");
8359
+ let insertSql = `INSERT INTO ${quoteIdent(table)} (${quotedCols}) OVERRIDING SYSTEM VALUE VALUES (${placeholders})`;
8360
+ switch (strategy) {
8361
+ case "fail":
8362
+ break;
8363
+ case "skip":
8364
+ insertSql += ` ON CONFLICT (${pkCols}) DO NOTHING`;
8365
+ break;
8366
+ case "update": {
8367
+ const updateCols = columns.filter((c) => !primaryKey.includes(c));
8368
+ if (updateCols.length > 0) {
8369
+ const setClauses = updateCols.map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`).join(", ");
8370
+ insertSql += ` ON CONFLICT (${pkCols}) DO UPDATE SET ${setClauses}`;
8371
+ } else {
8372
+ insertSql += ` ON CONFLICT (${pkCols}) DO NOTHING`;
8373
+ }
8374
+ break;
8375
+ }
8376
+ case "replace":
8377
+ {
8378
+ const setClauses = columns.map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`).join(", ");
8379
+ insertSql += ` ON CONFLICT (${pkCols}) DO UPDATE SET ${setClauses}`;
8380
+ }
8381
+ break;
7890
8382
  }
7891
- return this.#connection.db;
7892
- }
7893
- // ─────────────────────────────────────────────────────────
7894
- // Lifecycle
7895
- // ─────────────────────────────────────────────────────────
7896
- async connect() {
7897
- if (this.#connection) return;
7898
- this.#connection = await createConnection(
7899
- this.#config.connection,
7900
- this.#config.name
7901
- );
7902
- }
7903
- async disconnect() {
7904
- if (!this.#connection) return;
7905
- await this.#connection.destroy();
7906
- this.#connection = null;
7907
- this.#changeManager = null;
7908
- }
7909
- // ─────────────────────────────────────────────────────────
7910
- // SQL Execution
7911
- // ─────────────────────────────────────────────────────────
7912
- async query(sqlStr, _params) {
7913
- const db = this.kysely;
7914
- const result = await sql.raw(sqlStr).execute(db);
7915
- return result.rows ?? [];
7916
- }
7917
- async execute(sqlStr, _params) {
7918
- const db = this.kysely;
7919
- const result = await sql.raw(sqlStr).execute(db);
7920
- return {
7921
- rowsAffected: result.numAffectedRows ? Number(result.numAffectedRows) : void 0
7922
- };
8383
+ return insertSql;
8384
+ },
8385
+ buildDirectTransfer(srcDb, srcTable, dstTable, columns, srcSchema = "public", dstSchema = "public") {
8386
+ const quotedCols = columns.map(quoteIdent).join(", ");
8387
+ const srcFull = `${quoteIdent(srcSchema)}.${quoteIdent(srcTable)}`;
8388
+ const dstFull = `${quoteIdent(dstSchema)}.${quoteIdent(dstTable)}`;
8389
+ return `INSERT INTO ${dstFull} (${quotedCols}) OVERRIDING SYSTEM VALUE SELECT ${quotedCols} FROM ${srcFull}`;
8390
+ },
8391
+ async executeDisableFK(db) {
8392
+ await sql.raw(this.getDisableFKSql()).execute(db);
8393
+ },
8394
+ async executeEnableFK(db) {
8395
+ await sql.raw(this.getEnableFKSql()).execute(db);
7923
8396
  }
7924
- async transaction(fn) {
7925
- const db = this.kysely;
7926
- return db.transaction().execute(async (trx) => {
7927
- const tx = {
7928
- async query(s, _p) {
7929
- const r = await sql.raw(s).execute(trx);
7930
- return r.rows ?? [];
7931
- },
7932
- async execute(s, _p) {
7933
- const r = await sql.raw(s).execute(trx);
7934
- return {
7935
- rowsAffected: r.numAffectedRows ? Number(r.numAffectedRows) : void 0
7936
- };
8397
+ };
8398
+ function quoteIdent2(name) {
8399
+ return `\`${name.replace(/`/g, "``")}\``;
8400
+ }
8401
+ var mysqlTransferOperations = {
8402
+ getDisableFKSql() {
8403
+ return "SET FOREIGN_KEY_CHECKS = 0";
8404
+ },
8405
+ getEnableFKSql() {
8406
+ return "SET FOREIGN_KEY_CHECKS = 1";
8407
+ },
8408
+ getEnableIdentityInsertSql(_table) {
8409
+ return null;
8410
+ },
8411
+ getDisableIdentityInsertSql(_table) {
8412
+ return null;
8413
+ },
8414
+ getResetSequenceSql(table, _column, _schema) {
8415
+ return `ALTER TABLE ${quoteIdent2(table)} AUTO_INCREMENT = 1`;
8416
+ },
8417
+ buildConflictInsert(table, columns, primaryKey, strategy) {
8418
+ const quotedCols = columns.map(quoteIdent2).join(", ");
8419
+ const placeholders = columns.map(() => "?").join(", ");
8420
+ switch (strategy) {
8421
+ case "fail":
8422
+ return `INSERT INTO ${quoteIdent2(table)} (${quotedCols}) VALUES (${placeholders})`;
8423
+ case "skip":
8424
+ return `INSERT IGNORE INTO ${quoteIdent2(table)} (${quotedCols}) VALUES (${placeholders})`;
8425
+ case "update": {
8426
+ const updateCols = columns.filter((c) => !primaryKey.includes(c));
8427
+ if (updateCols.length > 0) {
8428
+ const setClauses = updateCols.map((c) => `${quoteIdent2(c)} = VALUES(${quoteIdent2(c)})`).join(", ");
8429
+ return `INSERT INTO ${quoteIdent2(table)} (${quotedCols}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${setClauses}`;
7937
8430
  }
7938
- };
7939
- return fn(tx);
7940
- });
7941
- }
7942
- // ─────────────────────────────────────────────────────────
7943
- // Explore
7944
- // ─────────────────────────────────────────────────────────
7945
- async listTables() {
7946
- return fetchList(this.kysely, this.dialect, "tables");
7947
- }
7948
- async describeTable(name, schema) {
7949
- return fetchDetail(this.kysely, this.dialect, "tables", name, schema);
7950
- }
7951
- async overview() {
7952
- return fetchOverview(this.kysely, this.dialect);
8431
+ return `INSERT IGNORE INTO ${quoteIdent2(table)} (${quotedCols}) VALUES (${placeholders})`;
8432
+ }
8433
+ case "replace":
8434
+ return `REPLACE INTO ${quoteIdent2(table)} (${quotedCols}) VALUES (${placeholders})`;
8435
+ }
8436
+ },
8437
+ buildDirectTransfer(srcDb, srcTable, dstTable, columns, _srcSchema, _dstSchema) {
8438
+ const quotedCols = columns.map(quoteIdent2).join(", ");
8439
+ const srcFull = `${quoteIdent2(srcDb)}.${quoteIdent2(srcTable)}`;
8440
+ const dstFull = quoteIdent2(dstTable);
8441
+ return `INSERT INTO ${dstFull} (${quotedCols}) SELECT ${quotedCols} FROM ${srcFull}`;
8442
+ },
8443
+ async executeDisableFK(db) {
8444
+ await sql.raw(this.getDisableFKSql()).execute(db);
8445
+ },
8446
+ async executeEnableFK(db) {
8447
+ await sql.raw(this.getEnableFKSql()).execute(db);
7953
8448
  }
7954
- // ─────────────────────────────────────────────────────────
7955
- // Schema Operations
7956
- // ─────────────────────────────────────────────────────────
8449
+ };
8450
+ function quoteIdent3(name) {
8451
+ return `[${name.replace(/\]/g, "]]")}]`;
8452
+ }
8453
+ var mssqlTransferOperations = {
8454
+ getDisableFKSql() {
8455
+ return "-- FK disable per-table";
8456
+ },
8457
+ getEnableFKSql() {
8458
+ return "-- FK enable per-table";
8459
+ },
8460
+ getEnableIdentityInsertSql(table) {
8461
+ return `SET IDENTITY_INSERT ${quoteIdent3(table)} ON`;
8462
+ },
8463
+ getDisableIdentityInsertSql(table) {
8464
+ return `SET IDENTITY_INSERT ${quoteIdent3(table)} OFF`;
8465
+ },
8466
+ getResetSequenceSql(table, _column, _schema) {
8467
+ return `DBCC CHECKIDENT ('${table}', RESEED)`;
8468
+ },
8469
+ buildConflictInsert(table, columns, primaryKey, strategy) {
8470
+ const quotedCols = columns.map(quoteIdent3).join(", ");
8471
+ const placeholders = columns.map((_, i) => `@p${i}`).join(", ");
8472
+ switch (strategy) {
8473
+ case "fail":
8474
+ return `INSERT INTO ${quoteIdent3(table)} (${quotedCols}) VALUES (${placeholders})`;
8475
+ case "skip":
8476
+ case "update":
8477
+ case "replace": {
8478
+ const pkConditions = primaryKey.map((pk) => `target.${quoteIdent3(pk)} = source.${quoteIdent3(pk)}`).join(" AND ");
8479
+ const sourceValues = columns.map((c, i) => `@p${i} AS ${quoteIdent3(c)}`).join(", ");
8480
+ let mergeSql = `
8481
+ MERGE INTO ${quoteIdent3(table)} AS target
8482
+ USING (SELECT ${sourceValues}) AS source
8483
+ ON (${pkConditions})
8484
+ `.trim();
8485
+ if (strategy === "skip") {
8486
+ mergeSql += `
8487
+ WHEN NOT MATCHED THEN
8488
+ INSERT (${quotedCols})
8489
+ VALUES (${columns.map((c) => `source.${quoteIdent3(c)}`).join(", ")})
8490
+ `;
8491
+ } else if (strategy === "update") {
8492
+ const updateCols = columns.filter((c) => !primaryKey.includes(c));
8493
+ if (updateCols.length > 0) {
8494
+ const setClauses = updateCols.map((c) => `target.${quoteIdent3(c)} = source.${quoteIdent3(c)}`).join(", ");
8495
+ mergeSql += `
8496
+ WHEN MATCHED THEN
8497
+ UPDATE SET ${setClauses}
8498
+ `;
8499
+ }
8500
+ mergeSql += `
8501
+ WHEN NOT MATCHED THEN
8502
+ INSERT (${quotedCols})
8503
+ VALUES (${columns.map((c) => `source.${quoteIdent3(c)}`).join(", ")})
8504
+ `;
8505
+ } else {
8506
+ const setClauses = columns.map((c) => `target.${quoteIdent3(c)} = source.${quoteIdent3(c)}`).join(", ");
8507
+ mergeSql += `
8508
+ WHEN MATCHED THEN
8509
+ UPDATE SET ${setClauses}
8510
+ WHEN NOT MATCHED THEN
8511
+ INSERT (${quotedCols})
8512
+ VALUES (${columns.map((c) => `source.${quoteIdent3(c)}`).join(", ")})
8513
+ `;
8514
+ }
8515
+ mergeSql += ";";
8516
+ return mergeSql.trim();
8517
+ }
8518
+ }
8519
+ },
8520
+ buildDirectTransfer(srcDb, srcTable, dstTable, columns, srcSchema = "dbo", dstSchema = "dbo") {
8521
+ const quotedCols = columns.map(quoteIdent3).join(", ");
8522
+ const srcFull = `${quoteIdent3(srcDb)}.${quoteIdent3(srcSchema)}.${quoteIdent3(srcTable)}`;
8523
+ const dstFull = `${quoteIdent3(dstSchema)}.${quoteIdent3(dstTable)}`;
8524
+ return `INSERT INTO ${dstFull} (${quotedCols}) SELECT ${quotedCols} FROM ${srcFull}`;
8525
+ },
8526
+ async executeDisableFK(db, tables) {
8527
+ for (const table of tables) {
8528
+ await sql.raw(`ALTER TABLE ${quoteIdent3(table)} NOCHECK CONSTRAINT ALL`).execute(db);
8529
+ }
8530
+ },
8531
+ async executeEnableFK(db, tables) {
8532
+ for (const table of tables) {
8533
+ await sql.raw(`ALTER TABLE ${quoteIdent3(table)} WITH CHECK CHECK CONSTRAINT ALL`).execute(db);
8534
+ }
8535
+ }
8536
+ };
8537
+
8538
+ // src/core/transfer/dialects/index.ts
8539
+ var dialectOperations2 = {
8540
+ postgres: postgresTransferOperations,
8541
+ mysql: mysqlTransferOperations,
8542
+ mssql: mssqlTransferOperations
8543
+ };
8544
+ var TRANSFER_SUPPORTED_DIALECTS = ["postgres", "mysql", "mssql"];
8545
+ function isTransferSupported(dialect) {
8546
+ return TRANSFER_SUPPORTED_DIALECTS.includes(dialect);
8547
+ }
8548
+ function getTransferOperations(dialect) {
8549
+ return dialectOperations2[dialect] ?? null;
8550
+ }
8551
+
8552
+ // src/core/dt/dialects/postgres.ts
8553
+ var POSTGRES_TO_UNIVERSAL = [
8554
+ // Boolean
8555
+ { pattern: /^boolean$/i, universalType: "bool", native: true },
8556
+ { pattern: /^bool$/i, universalType: "bool", native: true },
8557
+ // Integer types
8558
+ { pattern: /^smallint$/i, universalType: "int", native: true },
8559
+ { pattern: /^int2$/i, universalType: "int", native: true },
8560
+ { pattern: /^integer$/i, universalType: "int", native: true },
8561
+ { pattern: /^int$/i, universalType: "int", native: true },
8562
+ { pattern: /^int4$/i, universalType: "int", native: true },
8563
+ { pattern: /^serial$/i, universalType: "int", native: true },
8564
+ { pattern: /^smallserial$/i, universalType: "int", native: true },
8565
+ // Bigint
8566
+ { pattern: /^bigint$/i, universalType: "bigint", native: true },
8567
+ { pattern: /^int8$/i, universalType: "bigint", native: true },
8568
+ { pattern: /^bigserial$/i, universalType: "bigint", native: true },
8569
+ // Float
8570
+ { pattern: /^real$/i, universalType: "float", native: true },
8571
+ { pattern: /^float4$/i, universalType: "float", native: true },
8572
+ { pattern: /^double precision$/i, universalType: "float", native: true },
8573
+ { pattern: /^float8$/i, universalType: "float", native: true },
8574
+ // Decimal
8575
+ { pattern: /^numeric/i, universalType: "decimal", native: true },
8576
+ { pattern: /^decimal/i, universalType: "decimal", native: true },
8577
+ // UUID
8578
+ { pattern: /^uuid$/i, universalType: "uuid", native: true },
8579
+ // Date/time
8580
+ { pattern: /^timestamptz/i, universalType: "timestamp", native: true },
8581
+ { pattern: /^timestamp/i, universalType: "timestamp", native: true },
8582
+ { pattern: /^date$/i, universalType: "date", native: true },
8583
+ { pattern: /^time/i, universalType: "string", native: true },
8584
+ // JSON
8585
+ { pattern: /^jsonb$/i, universalType: "json", native: true },
8586
+ { pattern: /^json$/i, universalType: "json", native: true },
8587
+ // Binary
8588
+ { pattern: /^bytea$/i, universalType: "binary", native: true },
8589
+ // Vector (pgvector extension)
8590
+ { pattern: /^vector/i, universalType: "vector", native: true },
8591
+ // Array types (e.g., integer[], text[], etc.)
8592
+ { pattern: /\[\]$/i, universalType: "array", native: true },
8593
+ { pattern: /^ARRAY$/i, universalType: "array", native: true },
8594
+ // String types (catch-all for text-like types)
8595
+ { pattern: /^text$/i, universalType: "string", native: true },
8596
+ { pattern: /^varchar/i, universalType: "string", native: true },
8597
+ { pattern: /^character varying/i, universalType: "string", native: true },
8598
+ { pattern: /^char/i, universalType: "string", native: true },
8599
+ { pattern: /^character\b/i, universalType: "string", native: true },
8600
+ { pattern: /^citext$/i, universalType: "string", native: true },
8601
+ { pattern: /^name$/i, universalType: "string", native: true },
8602
+ // Everything else → custom
8603
+ { pattern: /.*/, universalType: "custom", native: false }
8604
+ ];
8605
+ var UNIVERSAL_TO_POSTGRES = {
8606
+ string: "text",
8607
+ int: "integer",
8608
+ bigint: "bigint",
8609
+ float: "double precision",
8610
+ decimal: "numeric",
8611
+ bool: "boolean",
8612
+ timestamp: "timestamptz",
8613
+ date: "date",
8614
+ uuid: "uuid",
8615
+ json: "jsonb",
8616
+ binary: "bytea",
8617
+ vector: "vector",
8618
+ array: "text[]",
8619
+ custom: "text"
8620
+ };
8621
+
8622
+ // src/core/dt/dialects/mysql.ts
8623
+ var MYSQL_TO_UNIVERSAL = [
8624
+ // Boolean (must come before generic TINYINT)
8625
+ { pattern: /^tinyint\(1\)$/i, universalType: "bool", native: true },
8626
+ { pattern: /^boolean$/i, universalType: "bool", native: true },
8627
+ { pattern: /^bool$/i, universalType: "bool", native: true },
8628
+ // Integer types
8629
+ { pattern: /^tinyint/i, universalType: "int", native: true },
8630
+ { pattern: /^smallint/i, universalType: "int", native: true },
8631
+ { pattern: /^mediumint/i, universalType: "int", native: true },
8632
+ { pattern: /^int\b/i, universalType: "int", native: true },
8633
+ { pattern: /^integer/i, universalType: "int", native: true },
8634
+ { pattern: /^year$/i, universalType: "int", native: true },
8635
+ // Bigint
8636
+ { pattern: /^bigint/i, universalType: "bigint", native: true },
8637
+ // Float
8638
+ { pattern: /^float/i, universalType: "float", native: true },
8639
+ { pattern: /^double/i, universalType: "float", native: true },
8640
+ // Decimal
8641
+ { pattern: /^decimal/i, universalType: "decimal", native: true },
8642
+ { pattern: /^numeric/i, universalType: "decimal", native: true },
8643
+ // Date/time
8644
+ { pattern: /^datetime/i, universalType: "timestamp", native: true },
8645
+ { pattern: /^timestamp/i, universalType: "timestamp", native: true },
8646
+ { pattern: /^date$/i, universalType: "date", native: true },
8647
+ { pattern: /^time/i, universalType: "string", native: true },
8648
+ // JSON
8649
+ { pattern: /^json$/i, universalType: "json", native: true },
8650
+ // Vector (MySQL 9.0+)
8651
+ { pattern: /^vector/i, universalType: "vector", native: true },
8652
+ // Binary types
8653
+ { pattern: /^bit/i, universalType: "binary", native: true },
8654
+ { pattern: /^binary/i, universalType: "binary", native: true },
8655
+ { pattern: /^varbinary/i, universalType: "binary", native: true },
8656
+ { pattern: /^tinyblob$/i, universalType: "binary", native: true },
8657
+ { pattern: /^blob$/i, universalType: "binary", native: true },
8658
+ { pattern: /^mediumblob$/i, universalType: "binary", native: true },
8659
+ { pattern: /^longblob$/i, universalType: "binary", native: true },
8660
+ // Enum/Set → custom
8661
+ { pattern: /^enum/i, universalType: "custom", native: true },
8662
+ { pattern: /^set/i, universalType: "custom", native: true },
8663
+ // String types
8664
+ { pattern: /^char/i, universalType: "string", native: true },
8665
+ { pattern: /^varchar/i, universalType: "string", native: true },
8666
+ { pattern: /^tinytext$/i, universalType: "string", native: true },
8667
+ { pattern: /^text$/i, universalType: "string", native: true },
8668
+ { pattern: /^mediumtext$/i, universalType: "string", native: true },
8669
+ { pattern: /^longtext$/i, universalType: "string", native: true },
8670
+ // Everything else → custom
8671
+ { pattern: /.*/, universalType: "custom", native: false }
8672
+ ];
8673
+ function getUniversalToMysql(universalType, version) {
8674
+ const major = version?.major ?? 8;
8675
+ switch (universalType) {
8676
+ case "string":
8677
+ return "varchar(255)";
8678
+ case "int":
8679
+ return "int";
8680
+ case "bigint":
8681
+ return "bigint";
8682
+ case "float":
8683
+ return "double";
8684
+ case "decimal":
8685
+ return "decimal(38,10)";
8686
+ case "bool":
8687
+ return "tinyint(1)";
8688
+ case "timestamp":
8689
+ return "datetime(6)";
8690
+ case "date":
8691
+ return "date";
8692
+ case "uuid":
8693
+ return "char(36)";
8694
+ case "json":
8695
+ return "json";
8696
+ case "binary":
8697
+ return "longblob";
8698
+ case "vector":
8699
+ return major >= 9 ? "vector(2048)" : "json";
8700
+ case "array":
8701
+ return "json";
8702
+ case "custom":
8703
+ return "text";
8704
+ default:
8705
+ return "text";
8706
+ }
8707
+ }
8708
+
8709
+ // src/core/dt/dialects/mssql.ts
8710
+ var MSSQL_TO_UNIVERSAL = [
8711
+ // Boolean
8712
+ { pattern: /^bit$/i, universalType: "bool", native: true },
8713
+ // Integer types
8714
+ { pattern: /^tinyint$/i, universalType: "int", native: true },
8715
+ { pattern: /^smallint$/i, universalType: "int", native: true },
8716
+ { pattern: /^int$/i, universalType: "int", native: true },
8717
+ // Bigint
8718
+ { pattern: /^bigint$/i, universalType: "bigint", native: true },
8719
+ // Float
8720
+ { pattern: /^float/i, universalType: "float", native: true },
8721
+ { pattern: /^real$/i, universalType: "float", native: true },
8722
+ // Decimal
8723
+ { pattern: /^decimal/i, universalType: "decimal", native: true },
8724
+ { pattern: /^numeric/i, universalType: "decimal", native: true },
8725
+ { pattern: /^money$/i, universalType: "decimal", native: true },
8726
+ { pattern: /^smallmoney$/i, universalType: "decimal", native: true },
8727
+ // UUID
8728
+ { pattern: /^uniqueidentifier$/i, universalType: "uuid", native: true },
8729
+ // Date/time
8730
+ { pattern: /^datetime2/i, universalType: "timestamp", native: true },
8731
+ { pattern: /^datetimeoffset/i, universalType: "timestamp", native: true },
8732
+ { pattern: /^datetime$/i, universalType: "timestamp", native: true },
8733
+ { pattern: /^smalldatetime$/i, universalType: "timestamp", native: true },
8734
+ { pattern: /^date$/i, universalType: "date", native: true },
8735
+ { pattern: /^time/i, universalType: "string", native: true },
8736
+ // Native JSON (SQL Server 2025+)
8737
+ { pattern: /^json$/i, universalType: "json", native: true },
8738
+ // Native VECTOR (SQL Server 2025+)
8739
+ { pattern: /^vector/i, universalType: "vector", native: true },
8740
+ // Binary types
8741
+ { pattern: /^binary/i, universalType: "binary", native: true },
8742
+ { pattern: /^varbinary/i, universalType: "binary", native: true },
8743
+ { pattern: /^image$/i, universalType: "binary", native: true },
8744
+ { pattern: /^rowversion$/i, universalType: "binary", native: true },
8745
+ { pattern: /^timestamp$/i, universalType: "binary", native: true },
8746
+ // XML → custom
8747
+ { pattern: /^xml$/i, universalType: "custom", native: true },
8748
+ { pattern: /^sql_variant$/i, universalType: "custom", native: true },
8749
+ { pattern: /^hierarchyid$/i, universalType: "custom", native: true },
8750
+ // String types
8751
+ { pattern: /^nvarchar\(max\)$/i, universalType: "string", native: true },
8752
+ { pattern: /^varchar\(max\)$/i, universalType: "string", native: true },
8753
+ { pattern: /^nvarchar/i, universalType: "string", native: true },
8754
+ { pattern: /^varchar/i, universalType: "string", native: true },
8755
+ { pattern: /^nchar/i, universalType: "string", native: true },
8756
+ { pattern: /^char/i, universalType: "string", native: true },
8757
+ { pattern: /^ntext$/i, universalType: "string", native: true },
8758
+ { pattern: /^text$/i, universalType: "string", native: true },
8759
+ // Everything else → custom
8760
+ { pattern: /.*/, universalType: "custom", native: false }
8761
+ ];
8762
+ function getUniversalToMssql(universalType, version) {
8763
+ const major = version?.major ?? 2022;
8764
+ switch (universalType) {
8765
+ case "string":
8766
+ return "nvarchar(255)";
8767
+ case "int":
8768
+ return "int";
8769
+ case "bigint":
8770
+ return "bigint";
8771
+ case "float":
8772
+ return "float";
8773
+ case "decimal":
8774
+ return "decimal(38,10)";
8775
+ case "bool":
8776
+ return "bit";
8777
+ case "timestamp":
8778
+ return "datetime2(7)";
8779
+ case "date":
8780
+ return "date";
8781
+ case "uuid":
8782
+ return "uniqueidentifier";
8783
+ case "json":
8784
+ return major >= 2025 ? "json" : "nvarchar(max)";
8785
+ case "binary":
8786
+ return "varbinary(max)";
8787
+ case "vector":
8788
+ return major >= 2025 ? "vector(1998)" : "nvarchar(max)";
8789
+ case "array":
8790
+ return "nvarchar(max)";
8791
+ case "custom":
8792
+ return "nvarchar(max)";
8793
+ default:
8794
+ return "nvarchar(max)";
8795
+ }
8796
+ }
8797
+
8798
+ // src/core/dt/dialects/index.ts
8799
+ function getDialectPatterns(dialect) {
8800
+ switch (dialect) {
8801
+ case "postgres":
8802
+ return POSTGRES_TO_UNIVERSAL;
8803
+ case "mysql":
8804
+ return MYSQL_TO_UNIVERSAL;
8805
+ case "mssql":
8806
+ return MSSQL_TO_UNIVERSAL;
8807
+ default:
8808
+ return [];
8809
+ }
8810
+ }
8811
+ function getDialectTargetType(universalType, dialect, version) {
8812
+ switch (dialect) {
8813
+ case "postgres":
8814
+ return UNIVERSAL_TO_POSTGRES[universalType] ?? "text";
8815
+ case "mysql":
8816
+ return getUniversalToMysql(universalType, version);
8817
+ case "mssql":
8818
+ return getUniversalToMssql(universalType, version);
8819
+ default:
8820
+ return "text";
8821
+ }
8822
+ }
8823
+
8824
+ // src/core/dt/type-map.ts
8825
+ function toUniversalType(options) {
8826
+ const { dbType, dialect } = options;
8827
+ const patterns = getDialectPatterns(dialect);
8828
+ for (const entry of patterns) {
8829
+ if (entry.pattern.test(dbType)) {
8830
+ return {
8831
+ universalType: entry.universalType,
8832
+ native: entry.native
8833
+ };
8834
+ }
8835
+ }
8836
+ return { universalType: "custom", native: false };
8837
+ }
8838
+ function toDialectType(options) {
8839
+ const { universalType, dialect, version } = options;
8840
+ return getDialectTargetType(universalType, dialect, version);
8841
+ }
8842
+ function isEncodedType(type) {
8843
+ return ENCODED_TYPES[type] === true;
8844
+ }
8845
+ async function queryDatabaseVersion(options) {
8846
+ const { db, dialect } = options;
8847
+ switch (dialect) {
8848
+ case "postgres":
8849
+ return queryPostgresVersion(db);
8850
+ case "mysql":
8851
+ return queryMysqlVersion(db);
8852
+ case "mssql":
8853
+ return queryMssqlVersion(db);
8854
+ default:
8855
+ return [null, new Error(`Unsupported dialect for version detection: ${dialect}`)];
8856
+ }
8857
+ }
8858
+ async function queryPostgresVersion(db) {
8859
+ const [result, err] = await attempt(
8860
+ () => sql`SELECT version() as version`.execute(db)
8861
+ );
8862
+ if (err) {
8863
+ return [null, err];
8864
+ }
8865
+ const raw = result.rows[0]?.version ?? "";
8866
+ const match = raw.match(/PostgreSQL\s+(\d+)\.(\d+)/i);
8867
+ if (!match) {
8868
+ return [null, new Error(`Cannot parse PostgreSQL version from: ${raw}`)];
8869
+ }
8870
+ return [{
8871
+ dialect: "postgres",
8872
+ major: parseInt(match[1], 10),
8873
+ minor: parseInt(match[2], 10),
8874
+ raw
8875
+ }, null];
8876
+ }
8877
+ async function queryMysqlVersion(db) {
8878
+ const [result, err] = await attempt(
8879
+ () => sql`SELECT version() as version`.execute(db)
8880
+ );
8881
+ if (err) {
8882
+ return [null, err];
8883
+ }
8884
+ const raw = result.rows[0]?.version ?? "";
8885
+ const match = raw.match(/^(\d+)\.(\d+)/);
8886
+ if (!match) {
8887
+ return [null, new Error(`Cannot parse MySQL version from: ${raw}`)];
8888
+ }
8889
+ return [{
8890
+ dialect: "mysql",
8891
+ major: parseInt(match[1], 10),
8892
+ minor: parseInt(match[2], 10),
8893
+ raw
8894
+ }, null];
8895
+ }
8896
+ async function queryMssqlVersion(db) {
8897
+ const [result, err] = await attempt(
8898
+ () => sql`
8899
+ SELECT
8900
+ CAST(SERVERPROPERTY('ProductMajorVersion') AS VARCHAR(10)) as major,
8901
+ CAST(SERVERPROPERTY('ProductMinorVersion') AS VARCHAR(10)) as minor,
8902
+ CAST(SERVERPROPERTY('ProductVersion') AS VARCHAR(50)) as full_version
8903
+ `.execute(db)
8904
+ );
8905
+ if (err) {
8906
+ return [null, err];
8907
+ }
8908
+ const row = result.rows[0];
8909
+ if (!row) {
8910
+ return [null, new Error("No version info returned from MSSQL")];
8911
+ }
8912
+ const internalMajor = parseInt(row.major, 10);
8913
+ const internalMinor = parseInt(row.minor, 10);
8914
+ const marketingYear = mssqlInternalToYear(internalMajor);
8915
+ return [{
8916
+ dialect: "mssql",
8917
+ major: marketingYear,
8918
+ minor: internalMinor,
8919
+ raw: row.full_version
8920
+ }, null];
8921
+ }
8922
+ function mssqlInternalToYear(internalMajor) {
8923
+ const mapping = {
8924
+ 11: 2012,
8925
+ 12: 2014,
8926
+ 13: 2016,
8927
+ 14: 2017,
8928
+ 15: 2019,
8929
+ 16: 2022,
8930
+ 17: 2025
8931
+ };
8932
+ return mapping[internalMajor] ?? internalMajor;
8933
+ }
8934
+
8935
+ // src/core/dt/schema.ts
8936
+ async function buildDtSchema(options) {
8937
+ const { db, dialect, tableName, schema } = options;
8938
+ const kyselyDb = db;
8939
+ let version = options.version;
8940
+ if (!version) {
8941
+ const [detected] = await queryDatabaseVersion({ db: kyselyDb, dialect });
8942
+ version = detected ?? void 0;
8943
+ }
8944
+ const [columns, err] = await queryColumns(kyselyDb, dialect, tableName, schema);
8945
+ if (err) {
8946
+ return [null, err];
8947
+ }
8948
+ const dtColumns = columns.map((col) => {
8949
+ const mapping = toUniversalType({
8950
+ dbType: col.dataType,
8951
+ dialect});
8952
+ const dtCol = {
8953
+ name: col.name,
8954
+ type: mapping.universalType
8955
+ };
8956
+ if (col.dataType.toLowerCase() !== mapping.universalType) {
8957
+ dtCol.sourceType = col.dataType;
8958
+ }
8959
+ if (!col.nullable) {
8960
+ dtCol.nullable = false;
8961
+ }
8962
+ return dtCol;
8963
+ });
8964
+ const dtSchema = {
8965
+ v: FORMAT_VERSION,
8966
+ d: dialect === "postgres" ? "postgresql" : dialect,
8967
+ dv: version ? `${version.major}.${version.minor}` : "unknown",
8968
+ t: tableName,
8969
+ columns: dtColumns
8970
+ };
8971
+ return [dtSchema, null];
8972
+ }
8973
+ async function validateSchema(options) {
8974
+ const { dtSchema, targetDb, targetDialect, targetVersion } = options;
8975
+ const kyselyDb = targetDb;
8976
+ const tableName = dtSchema.t;
8977
+ const errors = [];
8978
+ const warnings = [];
8979
+ if (!tableName) {
8980
+ errors.push("Schema has no table name (t field)");
8981
+ return [{ valid: false, errors, warnings }, null];
8982
+ }
8983
+ const [targetColumns, queryErr] = await queryColumns(kyselyDb, targetDialect, tableName);
8984
+ if (queryErr) {
8985
+ errors.push(`Target table "${tableName}" not found or inaccessible: ${queryErr.message}`);
8986
+ return [{ valid: false, errors, warnings }, null];
8987
+ }
8988
+ if (targetColumns.length === 0) {
8989
+ errors.push(`Target table "${tableName}" has no columns or does not exist`);
8990
+ return [{ valid: false, errors, warnings }, null];
8991
+ }
8992
+ const targetByName = {};
8993
+ for (const col of targetColumns) {
8994
+ targetByName[col.name] = col;
8995
+ }
8996
+ for (const dtCol of dtSchema.columns) {
8997
+ const targetCol = targetByName[dtCol.name];
8998
+ if (!targetCol) {
8999
+ errors.push(`Column "${dtCol.name}" exists in .dt but not in target table`);
9000
+ continue;
9001
+ }
9002
+ const targetMapping = toUniversalType({
9003
+ dbType: targetCol.dataType,
9004
+ dialect: targetDialect
9005
+ });
9006
+ if (targetMapping.universalType !== dtCol.type) {
9007
+ const targetTypeStr = toDialectType({
9008
+ universalType: dtCol.type,
9009
+ dialect: targetDialect,
9010
+ version: targetVersion
9011
+ });
9012
+ warnings.push(
9013
+ `Column "${dtCol.name}": source type "${dtCol.type}" maps to "${targetTypeStr}" in target, but target column is "${targetCol.dataType}" (${targetMapping.universalType})`
9014
+ );
9015
+ }
9016
+ }
9017
+ return [{
9018
+ valid: errors.length === 0,
9019
+ errors,
9020
+ warnings
9021
+ }, null];
9022
+ }
9023
+ async function queryColumns(db, dialect, tableName, schema) {
9024
+ switch (dialect) {
9025
+ case "postgres":
9026
+ return queryPostgresColumns(db, tableName, schema ?? "public");
9027
+ case "mysql":
9028
+ return queryMysqlColumns(db, tableName);
9029
+ case "mssql":
9030
+ return queryMssqlColumns(db, tableName);
9031
+ default:
9032
+ return [[], new Error(`Unsupported dialect: ${dialect}`)];
9033
+ }
9034
+ }
9035
+ async function queryPostgresColumns(db, tableName, schema) {
9036
+ const [result, err] = await attempt(
9037
+ () => sql`
9038
+ SELECT column_name, data_type, udt_name, is_nullable
9039
+ FROM information_schema.columns
9040
+ WHERE table_schema = ${schema}
9041
+ AND table_name = ${tableName}
9042
+ ORDER BY ordinal_position
9043
+ `.execute(db)
9044
+ );
9045
+ if (err) {
9046
+ return [[], err];
9047
+ }
9048
+ return [result.rows.map((r) => ({
9049
+ name: r.column_name,
9050
+ // Use udt_name for more specific types (e.g., 'jsonb' vs 'USER-DEFINED')
9051
+ dataType: r.data_type === "USER-DEFINED" ? r.udt_name : r.data_type,
9052
+ nullable: r.is_nullable === "YES"
9053
+ })), null];
9054
+ }
9055
+ async function queryMysqlColumns(db, tableName) {
9056
+ const [result, err] = await attempt(
9057
+ () => sql`
9058
+ SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE
9059
+ FROM INFORMATION_SCHEMA.COLUMNS
9060
+ WHERE TABLE_SCHEMA = DATABASE()
9061
+ AND TABLE_NAME = ${tableName}
9062
+ ORDER BY ORDINAL_POSITION
9063
+ `.execute(db)
9064
+ );
9065
+ if (err) {
9066
+ return [[], err];
9067
+ }
9068
+ return [result.rows.map((r) => ({
9069
+ name: r.COLUMN_NAME,
9070
+ // Use COLUMN_TYPE for precision (e.g., 'tinyint(1)' for bool)
9071
+ dataType: r.COLUMN_TYPE ?? r.DATA_TYPE,
9072
+ nullable: r.IS_NULLABLE === "YES"
9073
+ })), null];
9074
+ }
9075
+ async function queryMssqlColumns(db, tableName) {
9076
+ const [result, err] = await attempt(
9077
+ () => sql`
9078
+ SELECT
9079
+ c.name,
9080
+ TYPE_NAME(c.user_type_id) as type_name,
9081
+ c.is_nullable
9082
+ FROM sys.columns c
9083
+ JOIN sys.tables t ON c.object_id = t.object_id
9084
+ WHERE t.name = ${tableName}
9085
+ ORDER BY c.column_id
9086
+ `.execute(db)
9087
+ );
9088
+ if (err) {
9089
+ return [[], err];
9090
+ }
9091
+ return [result.rows.map((r) => ({
9092
+ name: r.name,
9093
+ dataType: r.type_name,
9094
+ nullable: r.is_nullable === 1
9095
+ })), null];
9096
+ }
9097
+
9098
+ // src/core/transfer/planner.ts
9099
+ async function planTransfer(ctx, options = {}) {
9100
+ observer.emit("transfer:planning", {
9101
+ source: ctx.source.config.name,
9102
+ destination: ctx.destination.config.name
9103
+ });
9104
+ const { dialect } = ctx.source;
9105
+ if (!isTransferSupported(dialect)) {
9106
+ return [null, new Error(`Transfer not supported for dialect: ${dialect}`)];
9107
+ }
9108
+ const crossDialect = ctx.source.dialect !== ctx.destination.dialect;
9109
+ if (!isTransferSupported(ctx.destination.dialect)) {
9110
+ return [null, new Error(`Transfer not supported for dialect: ${ctx.destination.dialect}`)];
9111
+ }
9112
+ const warnings = [];
9113
+ if (crossDialect) {
9114
+ warnings.push(`Cross-dialect transfer: ${ctx.source.dialect} \u2192 ${ctx.destination.dialect}. Type conversion will be applied.`);
9115
+ }
9116
+ const [allTables, tablesErr] = await listUserTables(ctx.source.db, dialect, options);
9117
+ if (tablesErr) {
9118
+ return [null, tablesErr];
9119
+ }
9120
+ const [fkRelations, fkErr] = await getForeignKeyRelations(ctx.source.db, dialect);
9121
+ if (fkErr) {
9122
+ return [null, fkErr];
9123
+ }
9124
+ const dependencyMap = buildDependencyMap(allTables, fkRelations);
9125
+ const [sortedNames, sortErr] = topologicalSort(allTables.map((t) => t.name), dependencyMap);
9126
+ if (sortErr) {
9127
+ warnings.push(`Circular FK dependency detected: ${sortErr.message}. Using original order.`);
9128
+ }
9129
+ const tablesByName = {};
9130
+ for (const t of allTables) {
9131
+ tablesByName[t.name] = t;
9132
+ }
9133
+ const orderedNames = sortErr ? allTables.map((t) => t.name) : sortedNames;
9134
+ const tablePlans = [];
9135
+ for (const name of orderedNames) {
9136
+ const meta = tablesByName[name];
9137
+ if (!meta) continue;
9138
+ tablePlans.push({
9139
+ name: meta.name,
9140
+ schema: meta.schema,
9141
+ rowCount: meta.rowCount,
9142
+ hasIdentity: meta.hasIdentity,
9143
+ identityColumn: meta.identityColumn,
9144
+ primaryKey: meta.primaryKey,
9145
+ columns: meta.columns,
9146
+ dependsOn: dependencyMap.get(name) ?? []
9147
+ });
9148
+ }
9149
+ const [destTables, destErr] = await listUserTables(ctx.destination.db, dialect, { tables: options.tables });
9150
+ if (destErr) {
9151
+ return [null, new Error(`Failed to read destination schema: ${destErr.message}`)];
9152
+ }
9153
+ const destTableNames = new Set(destTables.map((t) => t.name));
9154
+ for (const plan2 of tablePlans) {
9155
+ if (!destTableNames.has(plan2.name)) {
9156
+ warnings.push(`Table "${plan2.name}" exists in source but not destination`);
9157
+ }
9158
+ }
9159
+ if (crossDialect) {
9160
+ for (const tablePlan of tablePlans) {
9161
+ const [dtSchema] = await buildDtSchema({
9162
+ db: ctx.source.db,
9163
+ dialect: ctx.source.dialect,
9164
+ tableName: tablePlan.name,
9165
+ schema: tablePlan.schema
9166
+ });
9167
+ if (dtSchema) {
9168
+ tablePlan.columnTypes = dtSchema.columns;
9169
+ }
9170
+ }
9171
+ }
9172
+ const sameServer = crossDialect ? false : isSameServer(ctx.source.config.connection, ctx.destination.config.connection);
9173
+ const estimatedRows = tablePlans.reduce((sum, t) => sum + t.rowCount, 0);
9174
+ const plan = {
9175
+ tables: tablePlans,
9176
+ sameServer,
9177
+ estimatedRows,
9178
+ warnings,
9179
+ crossDialect,
9180
+ sourceDialect: ctx.source.dialect,
9181
+ destinationDialect: ctx.destination.dialect
9182
+ };
9183
+ observer.emit("transfer:plan:ready", {
9184
+ sameServer,
9185
+ tableCount: tablePlans.length,
9186
+ estimatedRows,
9187
+ warnings
9188
+ });
9189
+ return [plan, null];
9190
+ }
9191
+ async function listUserTables(db, dialect, options = {}) {
9192
+ const [tables, err] = await attempt(() => queryTables(db, dialect));
9193
+ if (err) {
9194
+ return [[], err];
9195
+ }
9196
+ let filtered = tables.filter((t) => !t.name.startsWith("__noorm_"));
9197
+ if (options.tables && options.tables.length > 0) {
9198
+ const requested = new Set(options.tables);
9199
+ filtered = filtered.filter((t) => requested.has(t.name));
9200
+ }
9201
+ return [filtered, null];
9202
+ }
9203
+ async function queryTables(db, dialect) {
9204
+ switch (dialect) {
9205
+ case "postgres":
9206
+ return queryPostgresTables(db);
9207
+ case "mysql":
9208
+ return queryMysqlTables(db);
9209
+ case "mssql":
9210
+ return queryMssqlTables(db);
9211
+ default:
9212
+ return [];
9213
+ }
9214
+ }
9215
+ async function queryPostgresTables(db) {
9216
+ const result = await sql`
9217
+ SELECT
9218
+ t.table_name,
9219
+ t.table_schema,
9220
+ COALESCE(c.reltuples::bigint::text, '0') as row_estimate,
9221
+ (
9222
+ SELECT string_agg(column_name, ',' ORDER BY ordinal_position)
9223
+ FROM information_schema.columns col
9224
+ WHERE col.table_schema = t.table_schema
9225
+ AND col.table_name = t.table_name
9226
+ ) as column_names,
9227
+ (
9228
+ SELECT column_name
9229
+ FROM information_schema.columns col
9230
+ WHERE col.table_schema = t.table_schema
9231
+ AND col.table_name = t.table_name
9232
+ AND (col.column_default LIKE 'nextval%' OR col.is_identity = 'YES')
9233
+ LIMIT 1
9234
+ ) as identity_column,
9235
+ (
9236
+ SELECT string_agg(kcu.column_name, ',' ORDER BY kcu.ordinal_position)
9237
+ FROM information_schema.table_constraints tc
9238
+ JOIN information_schema.key_column_usage kcu
9239
+ ON tc.constraint_name = kcu.constraint_name
9240
+ AND tc.table_schema = kcu.table_schema
9241
+ WHERE tc.constraint_type = 'PRIMARY KEY'
9242
+ AND tc.table_schema = t.table_schema
9243
+ AND tc.table_name = t.table_name
9244
+ ) as pk_columns
9245
+ FROM information_schema.tables t
9246
+ LEFT JOIN pg_class c ON c.relname = t.table_name
9247
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = t.table_schema
9248
+ WHERE t.table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
9249
+ AND t.table_type = 'BASE TABLE'
9250
+ ORDER BY t.table_schema, t.table_name
9251
+ `.execute(db);
9252
+ return result.rows.map((row) => ({
9253
+ name: row.table_name,
9254
+ schema: row.table_schema,
9255
+ rowCount: Math.max(0, parseInt(row.row_estimate, 10)),
9256
+ hasIdentity: row.identity_column !== null,
9257
+ identityColumn: row.identity_column ?? void 0,
9258
+ primaryKey: row.pk_columns ? row.pk_columns.split(",") : [],
9259
+ columns: row.column_names ? row.column_names.split(",") : []
9260
+ }));
9261
+ }
9262
+ async function queryMysqlTables(db) {
9263
+ const result = await sql`
9264
+ SELECT
9265
+ t.TABLE_NAME as table_name,
9266
+ t.TABLE_ROWS as row_estimate,
9267
+ (
9268
+ SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY ORDINAL_POSITION)
9269
+ FROM INFORMATION_SCHEMA.COLUMNS c
9270
+ WHERE c.TABLE_SCHEMA = t.TABLE_SCHEMA
9271
+ AND c.TABLE_NAME = t.TABLE_NAME
9272
+ ) as column_names,
9273
+ (
9274
+ SELECT COLUMN_NAME
9275
+ FROM INFORMATION_SCHEMA.COLUMNS c
9276
+ WHERE c.TABLE_SCHEMA = t.TABLE_SCHEMA
9277
+ AND c.TABLE_NAME = t.TABLE_NAME
9278
+ AND c.EXTRA LIKE '%auto_increment%'
9279
+ LIMIT 1
9280
+ ) as identity_column,
9281
+ (
9282
+ SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY ORDINAL_POSITION)
9283
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
9284
+ WHERE kcu.TABLE_SCHEMA = t.TABLE_SCHEMA
9285
+ AND kcu.TABLE_NAME = t.TABLE_NAME
9286
+ AND kcu.CONSTRAINT_NAME = 'PRIMARY'
9287
+ ) as pk_columns
9288
+ FROM INFORMATION_SCHEMA.TABLES t
9289
+ WHERE t.TABLE_SCHEMA = DATABASE()
9290
+ AND t.TABLE_TYPE = 'BASE TABLE'
9291
+ ORDER BY t.TABLE_NAME
9292
+ `.execute(db);
9293
+ return result.rows.map((row) => ({
9294
+ name: row.table_name,
9295
+ rowCount: Math.max(0, parseInt(row.row_estimate ?? "0", 10)),
9296
+ hasIdentity: row.identity_column !== null,
9297
+ identityColumn: row.identity_column ?? void 0,
9298
+ primaryKey: row.pk_columns ? row.pk_columns.split(",") : [],
9299
+ columns: row.column_names ? row.column_names.split(",") : []
9300
+ }));
9301
+ }
9302
+ async function queryMssqlTables(db) {
9303
+ const result = await sql`
9304
+ SELECT
9305
+ t.name as table_name,
9306
+ s.name as table_schema,
9307
+ COALESCE(SUM(p.rows), 0) as row_estimate,
9308
+ (
9309
+ SELECT STRING_AGG(c.name, ',') WITHIN GROUP (ORDER BY c.column_id)
9310
+ FROM sys.columns c
9311
+ WHERE c.object_id = t.object_id
9312
+ ) as column_names,
9313
+ (
9314
+ SELECT c.name
9315
+ FROM sys.columns c
9316
+ WHERE c.object_id = t.object_id
9317
+ AND c.is_identity = 1
9318
+ ) as identity_column,
9319
+ (
9320
+ SELECT STRING_AGG(c.name, ',') WITHIN GROUP (ORDER BY ic.key_ordinal)
9321
+ FROM sys.indexes i
9322
+ JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
9323
+ JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
9324
+ WHERE i.object_id = t.object_id
9325
+ AND i.is_primary_key = 1
9326
+ ) as pk_columns
9327
+ FROM sys.tables t
9328
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
9329
+ LEFT JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
9330
+ WHERE t.is_ms_shipped = 0
9331
+ GROUP BY t.object_id, t.name, s.name
9332
+ ORDER BY s.name, t.name
9333
+ `.execute(db);
9334
+ return result.rows.map((row) => ({
9335
+ name: row.table_name,
9336
+ schema: row.table_schema,
9337
+ rowCount: Math.max(0, parseInt(row.row_estimate ?? "0", 10)),
9338
+ hasIdentity: row.identity_column !== null,
9339
+ identityColumn: row.identity_column ?? void 0,
9340
+ primaryKey: row.pk_columns ? row.pk_columns.split(",") : [],
9341
+ columns: row.column_names ? row.column_names.split(",") : []
9342
+ }));
9343
+ }
9344
+ async function getForeignKeyRelations(db, dialect) {
9345
+ let queryFn;
9346
+ switch (dialect) {
9347
+ case "postgres":
9348
+ queryFn = () => queryPostgresFKs(db);
9349
+ break;
9350
+ case "mysql":
9351
+ queryFn = () => queryMysqlFKs(db);
9352
+ break;
9353
+ case "mssql":
9354
+ queryFn = () => queryMssqlFKs(db);
9355
+ break;
9356
+ default:
9357
+ return [[], null];
9358
+ }
9359
+ const [relations, err] = await attempt(queryFn);
9360
+ if (err) {
9361
+ return [[], err];
9362
+ }
9363
+ return [relations, null];
9364
+ }
9365
+ async function queryPostgresFKs(db) {
9366
+ const result = await sql`
9367
+ SELECT DISTINCT
9368
+ tc.table_name as from_table,
9369
+ ccu.table_name as to_table
9370
+ FROM information_schema.table_constraints tc
9371
+ JOIN information_schema.constraint_column_usage ccu
9372
+ ON tc.constraint_name = ccu.constraint_name
9373
+ AND tc.table_schema = ccu.table_schema
9374
+ WHERE tc.constraint_type = 'FOREIGN KEY'
9375
+ AND tc.table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
9376
+ `.execute(db);
9377
+ return result.rows.map((r) => ({
9378
+ fromTable: r.from_table,
9379
+ toTable: r.to_table
9380
+ }));
9381
+ }
9382
+ async function queryMysqlFKs(db) {
9383
+ const result = await sql`
9384
+ SELECT DISTINCT
9385
+ TABLE_NAME as from_table,
9386
+ REFERENCED_TABLE_NAME as to_table
9387
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
9388
+ WHERE TABLE_SCHEMA = DATABASE()
9389
+ AND REFERENCED_TABLE_NAME IS NOT NULL
9390
+ `.execute(db);
9391
+ return result.rows.map((r) => ({
9392
+ fromTable: r.from_table,
9393
+ toTable: r.to_table
9394
+ }));
9395
+ }
9396
+ async function queryMssqlFKs(db) {
9397
+ const result = await sql`
9398
+ SELECT DISTINCT
9399
+ OBJECT_NAME(fk.parent_object_id) as from_table,
9400
+ OBJECT_NAME(fk.referenced_object_id) as to_table
9401
+ FROM sys.foreign_keys fk
9402
+ `.execute(db);
9403
+ return result.rows.map((r) => ({
9404
+ fromTable: r.from_table,
9405
+ toTable: r.to_table
9406
+ }));
9407
+ }
9408
+ function buildDependencyMap(tables, relations) {
9409
+ const tableNames = new Set(tables.map((t) => t.name));
9410
+ const deps = /* @__PURE__ */ new Map();
9411
+ for (const name of tableNames) {
9412
+ deps.set(name, []);
9413
+ }
9414
+ for (const rel of relations) {
9415
+ if (tableNames.has(rel.fromTable) && tableNames.has(rel.toTable)) {
9416
+ const current = deps.get(rel.fromTable) ?? [];
9417
+ if (!current.includes(rel.toTable) && rel.fromTable !== rel.toTable) {
9418
+ current.push(rel.toTable);
9419
+ deps.set(rel.fromTable, current);
9420
+ }
9421
+ }
9422
+ }
9423
+ return deps;
9424
+ }
9425
+ function topologicalSort(nodes, deps) {
9426
+ const result = [];
9427
+ const visited = /* @__PURE__ */ new Set();
9428
+ const visiting = /* @__PURE__ */ new Set();
9429
+ function visit(node) {
9430
+ if (visited.has(node)) return null;
9431
+ if (visiting.has(node)) {
9432
+ return new Error(`Circular dependency involving "${node}"`);
9433
+ }
9434
+ visiting.add(node);
9435
+ const nodeDeps = deps.get(node) ?? [];
9436
+ for (const dep of nodeDeps) {
9437
+ const err = visit(dep);
9438
+ if (err) return err;
9439
+ }
9440
+ visiting.delete(node);
9441
+ visited.add(node);
9442
+ result.push(node);
9443
+ return null;
9444
+ }
9445
+ for (const node of nodes) {
9446
+ const err = visit(node);
9447
+ if (err) return [[], err];
9448
+ }
9449
+ return [result, null];
9450
+ }
9451
+ var DEFAULT_BATCH_SIZE = 100;
9452
+ var DEFAULT_MAX_BATCH_BYTES = gigabytes(1);
9453
+ var DtStreamer = class {
9454
+ #sourceDialect;
9455
+ #sourceVersion;
9456
+ #targetDialect;
9457
+ #targetVersion;
9458
+ #columns;
9459
+ #batchSize;
9460
+ #maxBatchBytes;
9461
+ /**
9462
+ * Create a new DtStreamer for cross-dialect conversion.
9463
+ *
9464
+ * @param options - Source/target dialects, columns, and batch limits
9465
+ */
9466
+ constructor(options) {
9467
+ this.#sourceDialect = options.sourceDialect;
9468
+ this.#sourceVersion = options.sourceVersion;
9469
+ this.#targetDialect = options.targetDialect;
9470
+ this.#targetVersion = options.targetVersion;
9471
+ this.#columns = options.columns;
9472
+ this.#batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
9473
+ this.#maxBatchBytes = options.maxBatchBytes ?? DEFAULT_MAX_BATCH_BYTES;
9474
+ }
9475
+ /**
9476
+ * The soft batch size limit.
9477
+ */
9478
+ get batchSize() {
9479
+ return this.#batchSize;
9480
+ }
9481
+ /**
9482
+ * Convert a batch of source-dialect rows to target-dialect rows.
9483
+ *
9484
+ * Performs in-memory type conversion without any file I/O or encoding.
9485
+ *
9486
+ * @param rows - Source database rows
9487
+ * @returns Target-dialect rows ready for insertion
9488
+ */
9489
+ convertBatch(rows) {
9490
+ const result = [];
9491
+ for (const row of rows) {
9492
+ result.push(this.#convertRow(row));
9493
+ }
9494
+ return result;
9495
+ }
9496
+ /**
9497
+ * Check if accumulated rows should be flushed.
9498
+ *
9499
+ * Returns true when row count exceeds batchSize OR estimated memory
9500
+ * exceeds maxBatchBytes. Prevents OOM on tables with large values.
9501
+ *
9502
+ * @param rows - Currently accumulated rows
9503
+ * @returns True if the batch should be flushed
9504
+ */
9505
+ shouldFlush(rows) {
9506
+ if (rows.length >= this.#batchSize) {
9507
+ return true;
9508
+ }
9509
+ return estimateRowsBytes(rows) >= this.#maxBatchBytes;
9510
+ }
9511
+ /**
9512
+ * Convert a single row from source to target dialect.
9513
+ */
9514
+ #convertRow(row) {
9515
+ const result = {};
9516
+ for (const col of this.#columns) {
9517
+ const value = row[col.name];
9518
+ if (value === null || value === void 0) {
9519
+ result[col.name] = null;
9520
+ continue;
9521
+ }
9522
+ if (isEncodedType(col.type)) {
9523
+ result[col.name] = this.#convertEncodedValue(value, col.type);
9524
+ } else {
9525
+ result[col.name] = this.#convertSimpleValue(value, col.type);
9526
+ }
9527
+ }
9528
+ return result;
9529
+ }
9530
+ /**
9531
+ * Convert an encoded-type value between dialects.
9532
+ *
9533
+ * No serialization overhead: works directly with native objects.
9534
+ */
9535
+ #convertEncodedValue(value, type) {
9536
+ switch (type) {
9537
+ case "json":
9538
+ return this.#convertJson(value);
9539
+ case "binary":
9540
+ return value;
9541
+ case "vector":
9542
+ return this.#convertVector(value);
9543
+ case "array":
9544
+ return this.#convertArray(value);
9545
+ case "custom":
9546
+ if (this.#targetDialect !== this.#sourceDialect && typeof value === "object") {
9547
+ return JSON.stringify(value);
9548
+ }
9549
+ return value;
9550
+ default:
9551
+ return value;
9552
+ }
9553
+ }
9554
+ /**
9555
+ * Convert JSON between dialects.
9556
+ */
9557
+ #convertJson(value) {
9558
+ if (this.#targetDialect === "mssql") {
9559
+ const major = this.#targetVersion?.major ?? 2022;
9560
+ if (major < 2025) {
9561
+ return typeof value === "string" ? value : JSON.stringify(value);
9562
+ }
9563
+ }
9564
+ if (this.#sourceDialect === "mssql" && typeof value === "string") {
9565
+ const major = this.#sourceVersion?.major ?? 2022;
9566
+ if (major < 2025) {
9567
+ return JSON.parse(value);
9568
+ }
9569
+ }
9570
+ return value;
9571
+ }
9572
+ /**
9573
+ * Convert vector between dialects.
9574
+ */
9575
+ #convertVector(value) {
9576
+ let arr;
9577
+ if (typeof value === "string") {
9578
+ const trimmed = value.replace(/^\[/, "").replace(/\]$/, "");
9579
+ arr = trimmed.split(",").map(Number);
9580
+ } else {
9581
+ arr = value;
9582
+ }
9583
+ if (this.#targetDialect === "postgres") {
9584
+ return Array.isArray(arr) ? `[${arr.join(",")}]` : String(value);
9585
+ }
9586
+ if (this.#targetDialect === "mysql") {
9587
+ const major = this.#targetVersion?.major ?? 8;
9588
+ if (major >= 9) {
9589
+ return Array.isArray(arr) ? `[${arr.join(",")}]` : String(value);
9590
+ }
9591
+ return Array.isArray(arr) ? JSON.stringify(arr) : String(value);
9592
+ }
9593
+ return Array.isArray(arr) ? JSON.stringify(arr) : String(value);
9594
+ }
9595
+ /**
9596
+ * Convert array between dialects.
9597
+ */
9598
+ #convertArray(value) {
9599
+ if (this.#targetDialect === "postgres") {
9600
+ if (typeof value === "string") return JSON.parse(value);
9601
+ return value;
9602
+ }
9603
+ if (Array.isArray(value)) return JSON.stringify(value);
9604
+ return value;
9605
+ }
9606
+ /**
9607
+ * Convert a simple-type value between dialects.
9608
+ */
9609
+ #convertSimpleValue(value, type) {
9610
+ switch (type) {
9611
+ case "bool":
9612
+ if (this.#targetDialect === "mssql" || this.#targetDialect === "mysql") {
9613
+ if (typeof value === "boolean") return value ? 1 : 0;
9614
+ } else if (this.#sourceDialect === "mssql" || this.#sourceDialect === "mysql") {
9615
+ if (typeof value === "number") return value !== 0;
9616
+ }
9617
+ return value;
9618
+ case "uuid":
9619
+ return value;
9620
+ default:
9621
+ return value;
9622
+ }
9623
+ }
9624
+ };
9625
+ function estimateRowsBytes(rows) {
9626
+ let total = 0;
9627
+ for (const row of rows) {
9628
+ for (const key in row) {
9629
+ const val = row[key];
9630
+ if (val === null || val === void 0) {
9631
+ total += 8;
9632
+ } else if (Buffer.isBuffer(val)) {
9633
+ total += val.length;
9634
+ } else if (typeof val === "string") {
9635
+ total += val.length * 2;
9636
+ } else if (typeof val === "object") {
9637
+ total += JSON.stringify(val).length * 2;
9638
+ } else {
9639
+ total += 8;
9640
+ }
9641
+ }
9642
+ }
9643
+ return total;
9644
+ }
9645
+
9646
+ // src/core/transfer/executor.ts
9647
+ var DEFAULT_BATCH_SIZE2 = 1e3;
9648
+ async function executeTransfer(ctx, plan, options = {}) {
9649
+ const startTime = Date.now();
9650
+ const { dialect: _srcDialect } = ctx.source;
9651
+ const destDialect = ctx.destination.dialect;
9652
+ const ops = getTransferOperations(destDialect);
9653
+ if (!ops) {
9654
+ return [null, new Error(`Unsupported dialect: ${destDialect}`)];
9655
+ }
9656
+ const tableResults = [];
9657
+ let totalRows = 0;
9658
+ let hasFailures = false;
9659
+ observer.emit("transfer:starting", {
9660
+ tableCount: plan.tables.length,
9661
+ sameServer: plan.sameServer
9662
+ });
9663
+ if (options.disableForeignKeys !== false) {
9664
+ const [, disableErr] = await attempt(
9665
+ () => ops.executeDisableFK(
9666
+ ctx.destination.db,
9667
+ plan.tables.map((t) => t.name)
9668
+ )
9669
+ );
9670
+ if (disableErr) {
9671
+ return [null, new Error(`Failed to disable FK checks: ${disableErr.message}`)];
9672
+ }
9673
+ }
9674
+ for (let i = 0; i < plan.tables.length; i++) {
9675
+ const tablePlan = plan.tables[i];
9676
+ observer.emit("transfer:table:before", {
9677
+ table: tablePlan.name,
9678
+ index: i,
9679
+ total: plan.tables.length,
9680
+ rowCount: tablePlan.rowCount
9681
+ });
9682
+ const tableStart = Date.now();
9683
+ let result2;
9684
+ const strategy = options.onConflict ?? "fail";
9685
+ const useSameServer = plan.sameServer && strategy === "fail" && !plan.crossDialect;
9686
+ const useCrossDialect = plan.crossDialect && tablePlan.columnTypes;
9687
+ let tableResult = null;
9688
+ let tableErr = null;
9689
+ if (useSameServer) {
9690
+ [tableResult, tableErr] = await transferTableSameServer(
9691
+ ctx,
9692
+ tablePlan,
9693
+ options,
9694
+ ops
9695
+ );
9696
+ } else if (useCrossDialect) {
9697
+ [tableResult, tableErr] = await transferTableCrossDialect(
9698
+ ctx,
9699
+ tablePlan,
9700
+ plan,
9701
+ options
9702
+ );
9703
+ } else {
9704
+ [tableResult, tableErr] = await transferTableCrossServer(
9705
+ ctx,
9706
+ tablePlan,
9707
+ options,
9708
+ ops
9709
+ );
9710
+ }
9711
+ if (tableErr) {
9712
+ result2 = {
9713
+ table: tablePlan.name,
9714
+ status: "failed",
9715
+ rowsTransferred: 0,
9716
+ rowsSkipped: 0,
9717
+ durationMs: Date.now() - tableStart,
9718
+ error: tableErr.message
9719
+ };
9720
+ hasFailures = true;
9721
+ } else {
9722
+ result2 = tableResult;
9723
+ }
9724
+ tableResults.push(result2);
9725
+ totalRows += result2.rowsTransferred;
9726
+ observer.emit("transfer:table:after", {
9727
+ table: tablePlan.name,
9728
+ status: result2.status,
9729
+ rowsTransferred: result2.rowsTransferred,
9730
+ rowsSkipped: result2.rowsSkipped,
9731
+ durationMs: result2.durationMs,
9732
+ error: result2.error
9733
+ });
9734
+ }
9735
+ if (options.disableForeignKeys !== false) {
9736
+ const [, enableErr] = await attempt(
9737
+ () => ops.executeEnableFK(
9738
+ ctx.destination.db,
9739
+ plan.tables.map((t) => t.name)
9740
+ )
9741
+ );
9742
+ if (enableErr) {
9743
+ observer.emit("error", {
9744
+ source: "transfer",
9745
+ error: enableErr,
9746
+ context: { phase: "enable-fk" }
9747
+ });
9748
+ }
9749
+ }
9750
+ const durationMs = Date.now() - startTime;
9751
+ const allSuccess = tableResults.every((r) => r.status === "success");
9752
+ const result = {
9753
+ status: hasFailures ? allSuccess ? "partial" : "failed" : "success",
9754
+ tables: tableResults,
9755
+ totalRows,
9756
+ durationMs
9757
+ };
9758
+ observer.emit("transfer:complete", {
9759
+ status: result.status,
9760
+ totalRows,
9761
+ tableCount: plan.tables.length,
9762
+ durationMs
9763
+ });
9764
+ return [result, null];
9765
+ }
9766
+ async function transferTableSameServer(ctx, plan, options, ops) {
9767
+ const startTime = Date.now();
9768
+ if (!ops) {
9769
+ return [null, new Error("No dialect operations")];
9770
+ }
9771
+ if (options.truncateFirst) {
9772
+ const [, truncateErr] = await truncateTable(
9773
+ ctx.destination.db,
9774
+ ctx.destination.dialect,
9775
+ plan.name
9776
+ );
9777
+ if (truncateErr) {
9778
+ return [null, new Error(`Failed to truncate: ${truncateErr.message}`)];
9779
+ }
9780
+ }
9781
+ if (options.preserveIdentity !== false && plan.hasIdentity) {
9782
+ const enableSql = ops.getEnableIdentityInsertSql(plan.name);
9783
+ if (enableSql) {
9784
+ const [, enableErr] = await attempt(
9785
+ () => sql.raw(enableSql).execute(ctx.destination.db)
9786
+ );
9787
+ if (enableErr) {
9788
+ return [null, new Error(`Failed to enable identity insert: ${enableErr.message}`)];
9789
+ }
9790
+ }
9791
+ }
9792
+ const transferSql = ops.buildDirectTransfer(
9793
+ ctx.source.config.connection.database,
9794
+ plan.name,
9795
+ plan.name,
9796
+ plan.columns,
9797
+ plan.schema,
9798
+ plan.schema
9799
+ );
9800
+ const [, transferErr] = await attempt(
9801
+ () => sql.raw(transferSql).execute(ctx.destination.db)
9802
+ );
9803
+ if (options.preserveIdentity !== false && plan.hasIdentity) {
9804
+ const disableSql = ops.getDisableIdentityInsertSql(plan.name);
9805
+ if (disableSql) {
9806
+ await attempt(() => sql.raw(disableSql).execute(ctx.destination.db));
9807
+ }
9808
+ if (plan.identityColumn) {
9809
+ const resetSql = ops.getResetSequenceSql(plan.name, plan.identityColumn, plan.schema);
9810
+ if (resetSql) {
9811
+ await attempt(() => sql.raw(resetSql).execute(ctx.destination.db));
9812
+ }
9813
+ }
9814
+ }
9815
+ if (transferErr) {
9816
+ return [null, new Error(`Transfer failed: ${transferErr.message}`)];
9817
+ }
9818
+ const rowsTransferred = plan.rowCount;
9819
+ observer.emit("transfer:table:progress", {
9820
+ table: plan.name,
9821
+ rowsTransferred,
9822
+ rowsTotal: plan.rowCount,
9823
+ rowsSkipped: 0
9824
+ });
9825
+ return [{
9826
+ table: plan.name,
9827
+ status: "success",
9828
+ rowsTransferred,
9829
+ rowsSkipped: 0,
9830
+ durationMs: Date.now() - startTime
9831
+ }, null];
9832
+ }
9833
+ async function transferTableCrossServer(ctx, plan, options, ops) {
9834
+ const startTime = Date.now();
9835
+ const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE2;
9836
+ const strategy = options.onConflict ?? "fail";
9837
+ if (!ops) {
9838
+ return [null, new Error("No dialect operations")];
9839
+ }
9840
+ if (options.truncateFirst) {
9841
+ const [, truncateErr] = await truncateTable(
9842
+ ctx.destination.db,
9843
+ ctx.destination.dialect,
9844
+ plan.name
9845
+ );
9846
+ if (truncateErr) {
9847
+ return [null, new Error(`Failed to truncate: ${truncateErr.message}`)];
9848
+ }
9849
+ }
9850
+ if (options.preserveIdentity !== false && plan.hasIdentity) {
9851
+ const enableSql = ops.getEnableIdentityInsertSql(plan.name);
9852
+ if (enableSql) {
9853
+ const [, enableErr] = await attempt(
9854
+ () => sql.raw(enableSql).execute(ctx.destination.db)
9855
+ );
9856
+ if (enableErr) {
9857
+ return [null, new Error(`Failed to enable identity insert: ${enableErr.message}`)];
9858
+ }
9859
+ }
9860
+ }
9861
+ let rowsTransferred = 0;
9862
+ let rowsSkipped = 0;
9863
+ let offset = 0;
9864
+ let transferError = null;
9865
+ while (true) {
9866
+ const [rows, fetchErr] = await attempt(
9867
+ () => fetchBatch(
9868
+ ctx.source.db,
9869
+ ctx.source.dialect,
9870
+ plan.name,
9871
+ plan.columns,
9872
+ batchSize,
9873
+ offset,
9874
+ plan.schema
9875
+ )
9876
+ );
9877
+ if (fetchErr) {
9878
+ transferError = new Error(`Failed to fetch batch: ${fetchErr.message}`);
9879
+ break;
9880
+ }
9881
+ if (rows.length === 0) {
9882
+ break;
9883
+ }
9884
+ const [batchResult, insertErr] = await insertBatch(
9885
+ ctx.destination.db,
9886
+ ctx.destination.dialect,
9887
+ plan.name,
9888
+ plan.columns,
9889
+ plan.primaryKey,
9890
+ rows,
9891
+ strategy,
9892
+ ops
9893
+ );
9894
+ if (insertErr) {
9895
+ transferError = new Error(`Failed to insert batch: ${insertErr.message}`);
9896
+ break;
9897
+ }
9898
+ rowsTransferred += batchResult.inserted;
9899
+ rowsSkipped += batchResult.skipped;
9900
+ offset += rows.length;
9901
+ observer.emit("transfer:table:progress", {
9902
+ table: plan.name,
9903
+ rowsTransferred,
9904
+ rowsTotal: plan.rowCount,
9905
+ rowsSkipped
9906
+ });
9907
+ if (rows.length < batchSize) {
9908
+ break;
9909
+ }
9910
+ }
9911
+ if (options.preserveIdentity !== false && plan.hasIdentity) {
9912
+ const disableSql = ops.getDisableIdentityInsertSql(plan.name);
9913
+ if (disableSql) {
9914
+ await attempt(() => sql.raw(disableSql).execute(ctx.destination.db));
9915
+ }
9916
+ if (plan.identityColumn) {
9917
+ const resetSql = ops.getResetSequenceSql(plan.name, plan.identityColumn, plan.schema);
9918
+ if (resetSql) {
9919
+ await attempt(() => sql.raw(resetSql).execute(ctx.destination.db));
9920
+ }
9921
+ }
9922
+ }
9923
+ if (transferError) {
9924
+ return [null, transferError];
9925
+ }
9926
+ return [{
9927
+ table: plan.name,
9928
+ status: "success",
9929
+ rowsTransferred,
9930
+ rowsSkipped,
9931
+ durationMs: Date.now() - startTime
9932
+ }, null];
9933
+ }
9934
+ async function transferTableCrossDialect(ctx, plan, transferPlan, options) {
9935
+ const startTime = Date.now();
9936
+ const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE2;
9937
+ const destOps = getTransferOperations(ctx.destination.dialect);
9938
+ if (!destOps || !plan.columnTypes) {
9939
+ return [null, new Error("Missing dialect operations or column types for cross-dialect transfer")];
9940
+ }
9941
+ const [srcVersion] = await queryDatabaseVersion({ db: ctx.source.db, dialect: ctx.source.dialect });
9942
+ const [dstVersion] = await queryDatabaseVersion({ db: ctx.destination.db, dialect: ctx.destination.dialect });
9943
+ const streamer = new DtStreamer({
9944
+ sourceDialect: ctx.source.dialect,
9945
+ sourceVersion: srcVersion ?? void 0,
9946
+ targetDialect: ctx.destination.dialect,
9947
+ targetVersion: dstVersion ?? void 0,
9948
+ columns: plan.columnTypes,
9949
+ batchSize
9950
+ });
9951
+ if (options.truncateFirst) {
9952
+ const [, truncateErr] = await truncateTable(
9953
+ ctx.destination.db,
9954
+ ctx.destination.dialect,
9955
+ plan.name
9956
+ );
9957
+ if (truncateErr) {
9958
+ return [null, new Error(`Failed to truncate: ${truncateErr.message}`)];
9959
+ }
9960
+ }
9961
+ if (options.preserveIdentity !== false && plan.hasIdentity) {
9962
+ const enableSql = destOps.getEnableIdentityInsertSql(plan.name);
9963
+ if (enableSql) {
9964
+ const [, enableErr] = await attempt(
9965
+ () => sql.raw(enableSql).execute(ctx.destination.db)
9966
+ );
9967
+ if (enableErr) {
9968
+ return [null, new Error(`Failed to enable identity insert: ${enableErr.message}`)];
9969
+ }
9970
+ }
9971
+ }
9972
+ let rowsTransferred = 0;
9973
+ let rowsSkipped = 0;
9974
+ let offset = 0;
9975
+ let transferError = null;
9976
+ observer.emit("dt:stream:start", {
9977
+ table: plan.name,
9978
+ sourceDialect: ctx.source.dialect,
9979
+ targetDialect: ctx.destination.dialect
9980
+ });
9981
+ while (true) {
9982
+ const [rows, fetchErr] = await attempt(
9983
+ () => fetchBatch(
9984
+ ctx.source.db,
9985
+ ctx.source.dialect,
9986
+ plan.name,
9987
+ plan.columns,
9988
+ batchSize,
9989
+ offset,
9990
+ plan.schema
9991
+ )
9992
+ );
9993
+ if (fetchErr) {
9994
+ transferError = new Error(`Failed to fetch batch: ${fetchErr.message}`);
9995
+ break;
9996
+ }
9997
+ if (rows.length === 0) break;
9998
+ const convertedRows = streamer.convertBatch(rows);
9999
+ for (const row of convertedRows) {
10000
+ const [, insertErr] = await attempt(
10001
+ () => ctx.destination.db.insertInto(plan.name).values(row).execute()
10002
+ );
10003
+ if (insertErr) {
10004
+ const lower = insertErr.message.toLowerCase();
10005
+ const isDuplicate = lower.includes("duplicate") || lower.includes("unique") || lower.includes("primary key");
10006
+ if (isDuplicate && options.onConflict === "skip") {
10007
+ rowsSkipped++;
10008
+ } else if (options.onConflict !== "fail") {
10009
+ rowsSkipped++;
10010
+ } else {
10011
+ transferError = new Error(`Failed to insert row: ${insertErr.message}`);
10012
+ break;
10013
+ }
10014
+ } else {
10015
+ rowsTransferred++;
10016
+ }
10017
+ }
10018
+ if (transferError) break;
10019
+ offset += rows.length;
10020
+ observer.emit("transfer:table:progress", {
10021
+ table: plan.name,
10022
+ rowsTransferred,
10023
+ rowsTotal: plan.rowCount,
10024
+ rowsSkipped
10025
+ });
10026
+ observer.emit("dt:stream:progress", {
10027
+ table: plan.name,
10028
+ rowsConverted: rowsTransferred + rowsSkipped
10029
+ });
10030
+ if (rows.length < batchSize) break;
10031
+ }
10032
+ if (options.preserveIdentity !== false && plan.hasIdentity) {
10033
+ const disableSql = destOps.getDisableIdentityInsertSql(plan.name);
10034
+ if (disableSql) {
10035
+ await attempt(() => sql.raw(disableSql).execute(ctx.destination.db));
10036
+ }
10037
+ }
10038
+ const durationMs = Date.now() - startTime;
10039
+ observer.emit("dt:stream:complete", {
10040
+ table: plan.name,
10041
+ rowsConverted: rowsTransferred + rowsSkipped,
10042
+ durationMs
10043
+ });
10044
+ if (transferError) {
10045
+ return [null, transferError];
10046
+ }
10047
+ return [{
10048
+ table: plan.name,
10049
+ status: "success",
10050
+ rowsTransferred,
10051
+ rowsSkipped,
10052
+ durationMs
10053
+ }, null];
10054
+ }
10055
+ async function fetchBatch(db, dialect, table, columns, limit, offset, _schema) {
10056
+ const quoteIdent4 = dialect === "mssql" ? (c) => `[${c}]` : dialect === "mysql" ? (c) => `\`${c}\`` : (c) => `"${c}"`;
10057
+ const columnList = columns.map(quoteIdent4).join(", ");
10058
+ if (dialect === "mssql") {
10059
+ const orderCol = quoteIdent4(columns[0]);
10060
+ const result2 = await sql`
10061
+ SELECT ${sql.raw(columnList)}
10062
+ FROM ${sql.table(table)}
10063
+ ORDER BY ${sql.raw(orderCol)}
10064
+ OFFSET ${offset} ROWS
10065
+ FETCH NEXT ${limit} ROWS ONLY
10066
+ `.execute(db);
10067
+ return result2.rows;
10068
+ }
10069
+ const result = await sql`
10070
+ SELECT ${sql.raw(columnList)}
10071
+ FROM ${sql.table(table)}
10072
+ LIMIT ${limit}
10073
+ OFFSET ${offset}
10074
+ `.execute(db);
10075
+ return result.rows;
10076
+ }
10077
+ async function insertBatch(db, dialect, table, columns, primaryKey, rows, strategy, ops) {
10078
+ if (rows.length === 0) {
10079
+ return [{ inserted: 0, skipped: 0 }, null];
10080
+ }
10081
+ if ((dialect === "mssql" || dialect === "mysql") && strategy !== "fail" && primaryKey.length > 0 && ops) {
10082
+ return insertBatchRawSql(db, dialect, table, columns, primaryKey, rows, strategy, ops);
10083
+ }
10084
+ let inserted = 0;
10085
+ let skipped = 0;
10086
+ for (const row of rows) {
10087
+ const query = db.insertInto(table).values(row);
10088
+ if (strategy === "skip" && primaryKey.length > 0) {
10089
+ const [, err] = await attempt(
10090
+ () => query.onConflict((oc) => oc.columns(primaryKey).doNothing()).execute()
10091
+ );
10092
+ if (err) {
10093
+ if (isDuplicateKeyError(err.message)) {
10094
+ skipped++;
10095
+ } else {
10096
+ return [{ inserted, skipped }, err];
10097
+ }
10098
+ } else {
10099
+ inserted++;
10100
+ }
10101
+ } else if (strategy === "update" && primaryKey.length > 0) {
10102
+ const updateSet = {};
10103
+ for (const col of columns) {
10104
+ if (!primaryKey.includes(col)) {
10105
+ updateSet[col] = row[col];
10106
+ }
10107
+ }
10108
+ const [, err] = await attempt(
10109
+ () => query.onConflict((oc) => oc.columns(primaryKey).doUpdateSet(updateSet)).execute()
10110
+ );
10111
+ if (err) {
10112
+ if (isDuplicateKeyError(err.message)) {
10113
+ skipped++;
10114
+ } else {
10115
+ return [{ inserted, skipped }, err];
10116
+ }
10117
+ } else {
10118
+ inserted++;
10119
+ }
10120
+ } else {
10121
+ const [, err] = await attempt(() => query.execute());
10122
+ if (err) {
10123
+ if (strategy === "fail") {
10124
+ return [{ inserted, skipped }, err];
10125
+ }
10126
+ skipped++;
10127
+ } else {
10128
+ inserted++;
10129
+ }
10130
+ }
10131
+ }
10132
+ return [{ inserted, skipped }, null];
10133
+ }
10134
+ async function insertBatchRawSql(db, dialect, table, columns, primaryKey, rows, strategy, ops) {
10135
+ let inserted = 0;
10136
+ let skipped = 0;
10137
+ const sqlTemplate = ops.buildConflictInsert(table, columns, primaryKey, strategy);
10138
+ for (const row of rows) {
10139
+ const values = columns.map((col) => row[col]);
10140
+ const sqlParts = [];
10141
+ const sqlValues = [];
10142
+ if (dialect === "mssql") {
10143
+ let match;
10144
+ const paramRegex = /@p(\d+)/g;
10145
+ let lastIndex = 0;
10146
+ paramRegex.lastIndex = 0;
10147
+ while ((match = paramRegex.exec(sqlTemplate)) !== null) {
10148
+ sqlParts.push(sqlTemplate.slice(lastIndex, match.index));
10149
+ sqlValues.push(values[parseInt(match[1], 10)]);
10150
+ lastIndex = match.index + match[0].length;
10151
+ }
10152
+ sqlParts.push(sqlTemplate.slice(lastIndex));
10153
+ } else {
10154
+ const parts = sqlTemplate.split("?");
10155
+ let paramIdx = 0;
10156
+ for (let i = 0; i < parts.length; i++) {
10157
+ sqlParts.push(parts[i]);
10158
+ if (i < parts.length - 1) {
10159
+ sqlValues.push(values[paramIdx++]);
10160
+ }
10161
+ }
10162
+ }
10163
+ const finalSql = sql.join(
10164
+ sqlParts.map((part, i) => {
10165
+ if (i < sqlValues.length) {
10166
+ return sql`${sql.raw(part)}${sql.val(sqlValues[i])}`;
10167
+ }
10168
+ return sql.raw(part);
10169
+ }),
10170
+ sql.raw("")
10171
+ );
10172
+ const [, err] = await attempt(() => finalSql.execute(db));
10173
+ if (err) {
10174
+ if (isDuplicateKeyError(err.message)) {
10175
+ skipped++;
10176
+ } else {
10177
+ return [{ inserted, skipped }, err];
10178
+ }
10179
+ } else {
10180
+ inserted++;
10181
+ }
10182
+ }
10183
+ return [{ inserted, skipped }, null];
10184
+ }
10185
+ function isDuplicateKeyError(message) {
10186
+ const lower = message.toLowerCase();
10187
+ return lower.includes("duplicate") || lower.includes("unique constraint") || lower.includes("primary key") || lower.includes("violates unique") || lower.includes("cannot insert duplicate") || lower.includes("duplicate entry");
10188
+ }
10189
+ async function truncateTable(db, dialect, table) {
10190
+ let truncateSql;
10191
+ switch (dialect) {
10192
+ case "postgres":
10193
+ truncateSql = `TRUNCATE TABLE "${table}" CASCADE`;
10194
+ break;
10195
+ case "mssql":
10196
+ truncateSql = `DELETE FROM [${table}]`;
10197
+ break;
10198
+ case "mysql":
10199
+ truncateSql = `TRUNCATE TABLE \`${table}\``;
10200
+ break;
10201
+ default:
10202
+ truncateSql = `TRUNCATE TABLE ${table}`;
10203
+ }
10204
+ const [, err] = await attempt(() => sql.raw(truncateSql).execute(db));
10205
+ return [void 0, err];
10206
+ }
10207
+
10208
+ // src/core/transfer/index.ts
10209
+ async function transferData(sourceConfig, destConfig, options = {}) {
10210
+ const srcDialect = sourceConfig.connection.dialect;
10211
+ const dstDialect = destConfig.connection.dialect;
10212
+ if (!isTransferSupported(srcDialect)) {
10213
+ return [null, new Error(`Transfer not supported for dialect: ${srcDialect}. Supported: ${TRANSFER_SUPPORTED_DIALECTS.join(", ")}`)];
10214
+ }
10215
+ if (!isTransferSupported(dstDialect)) {
10216
+ return [null, new Error(`Transfer not supported for dialect: ${dstDialect}. Supported: ${TRANSFER_SUPPORTED_DIALECTS.join(", ")}`)];
10217
+ }
10218
+ return withDualConnection(
10219
+ {
10220
+ sourceConfig,
10221
+ destConfig,
10222
+ ensureSchema: false
10223
+ // Don't create noorm tables on destination
10224
+ },
10225
+ async (ctx) => {
10226
+ const [plan, planErr] = await planTransfer(ctx, options);
10227
+ if (planErr) {
10228
+ throw planErr;
10229
+ }
10230
+ if (!plan || plan.tables.length === 0) {
10231
+ return {
10232
+ status: "success",
10233
+ tables: [],
10234
+ totalRows: 0,
10235
+ durationMs: 0
10236
+ };
10237
+ }
10238
+ if (options.dryRun) {
10239
+ return {
10240
+ status: "success",
10241
+ tables: plan.tables.map((t) => ({
10242
+ table: t.name,
10243
+ status: "skipped",
10244
+ rowsTransferred: 0,
10245
+ rowsSkipped: 0,
10246
+ durationMs: 0
10247
+ })),
10248
+ totalRows: 0,
10249
+ durationMs: 0
10250
+ };
10251
+ }
10252
+ const [result, execErr] = await executeTransfer(ctx, plan, options);
10253
+ if (execErr) {
10254
+ throw execErr;
10255
+ }
10256
+ return result;
10257
+ }
10258
+ );
10259
+ }
10260
+ async function getTransferPlan(sourceConfig, destConfig, options = {}) {
10261
+ const srcDialect = sourceConfig.connection.dialect;
10262
+ const dstDialect = destConfig.connection.dialect;
10263
+ if (!isTransferSupported(srcDialect)) {
10264
+ return [null, new Error(`Transfer not supported for dialect: ${srcDialect}`)];
10265
+ }
10266
+ if (!isTransferSupported(dstDialect)) {
10267
+ return [null, new Error(`Transfer not supported for dialect: ${dstDialect}`)];
10268
+ }
10269
+ return withDualConnection(
10270
+ {
10271
+ sourceConfig,
10272
+ destConfig,
10273
+ ensureSchema: false
10274
+ },
10275
+ async (ctx) => {
10276
+ const [plan, err] = await planTransfer(ctx, options);
10277
+ if (err) {
10278
+ throw err;
10279
+ }
10280
+ return plan;
10281
+ }
10282
+ );
10283
+ }
10284
+ var DtWriter = class {
10285
+ #filepath;
10286
+ #schema;
10287
+ #passphrase;
10288
+ #extension;
10289
+ #stream = null;
10290
+ #passthrough = null;
10291
+ #pipelinePromise = null;
10292
+ #bytesWritten = 0;
10293
+ #rowsWritten = 0;
10294
+ #buffer = null;
10295
+ /**
10296
+ * Create a new DtWriter.
10297
+ *
10298
+ * @param options - File path, schema, and optional passphrase
10299
+ */
10300
+ constructor(options) {
10301
+ this.#filepath = options.filepath;
10302
+ this.#schema = options.schema;
10303
+ this.#passphrase = options.passphrase;
10304
+ this.#extension = path7.extname(options.filepath).toLowerCase();
10305
+ }
10306
+ /** Total bytes written to disk. */
10307
+ get bytesWritten() {
10308
+ return this.#bytesWritten;
10309
+ }
10310
+ /** Total rows written. */
10311
+ get rowsWritten() {
10312
+ return this.#rowsWritten;
10313
+ }
10314
+ /**
10315
+ * Open the writer and write the schema header.
10316
+ *
10317
+ * Must be called before writing any rows.
10318
+ */
10319
+ async open() {
10320
+ if (this.#extension === DT_EXTENSIONS.ENCRYPTED) {
10321
+ if (!this.#passphrase) {
10322
+ throw new Error("Passphrase required for .dtzx files");
10323
+ }
10324
+ this.#buffer = [];
10325
+ const schemaLine2 = JSON5.stringify(this.#schema) + "\n";
10326
+ this.#buffer.push(Buffer.from(schemaLine2, "utf8"));
10327
+ return;
10328
+ }
10329
+ const fileStream = createWriteStream(this.#filepath);
10330
+ const schemaLine = JSON5.stringify(this.#schema) + "\n";
10331
+ if (this.#extension === DT_EXTENSIONS.COMPRESSED) {
10332
+ this.#passthrough = new PassThrough();
10333
+ const gzip = createGzip();
10334
+ this.#pipelinePromise = pipeline(this.#passthrough, gzip, fileStream);
10335
+ this.#stream = this.#passthrough;
10336
+ this.#writeToStream(schemaLine);
10337
+ } else {
10338
+ this.#stream = fileStream;
10339
+ this.#writeToStream(schemaLine);
10340
+ }
10341
+ }
10342
+ /**
10343
+ * Write a single serialized row.
10344
+ *
10345
+ * @param values - Serialized .dt values in column order
10346
+ */
10347
+ writeRow(values) {
10348
+ const line = JSON5.stringify(values) + "\n";
10349
+ if (this.#buffer) {
10350
+ this.#buffer.push(Buffer.from(line, "utf8"));
10351
+ } else {
10352
+ this.#writeToStream(line);
10353
+ }
10354
+ this.#rowsWritten++;
10355
+ }
10356
+ /**
10357
+ * Write multiple serialized rows.
10358
+ *
10359
+ * @param rows - Array of serialized row value arrays
10360
+ */
10361
+ writeRows(rows) {
10362
+ for (const row of rows) {
10363
+ this.writeRow(row);
10364
+ }
10365
+ }
10366
+ /**
10367
+ * Close the writer and finalize the file.
10368
+ *
10369
+ * For .dtzx files, this is where compression and encryption happen.
10370
+ */
10371
+ async close() {
10372
+ if (this.#buffer) {
10373
+ const { gzipSync: gzipSync2 } = await import('zlib');
10374
+ const raw = Buffer.concat(this.#buffer);
10375
+ const compressed = gzipSync2(raw);
10376
+ const payload = encryptWithPassphrase(compressed, this.#passphrase);
10377
+ const payloadJson = JSON.stringify(payload);
10378
+ writeFileSync(this.#filepath, payloadJson, "utf8");
10379
+ this.#bytesWritten = Buffer.byteLength(payloadJson, "utf8");
10380
+ this.#buffer = null;
10381
+ return;
10382
+ }
10383
+ if (this.#passthrough) {
10384
+ this.#passthrough.end();
10385
+ await this.#pipelinePromise;
10386
+ } else if (this.#stream) {
10387
+ await new Promise((resolve, reject) => {
10388
+ this.#stream.end(() => resolve());
10389
+ this.#stream.on("error", reject);
10390
+ });
10391
+ }
10392
+ }
10393
+ /**
10394
+ * Write a string to the active stream and track bytes.
10395
+ */
10396
+ #writeToStream(data2) {
10397
+ if (!this.#stream) {
10398
+ throw new Error("Writer not opened");
10399
+ }
10400
+ const buf = Buffer.from(data2, "utf8");
10401
+ this.#bytesWritten += buf.length;
10402
+ this.#stream.write(buf);
10403
+ }
10404
+ };
10405
+ function serializeRow(options) {
10406
+ const { row, columns } = options;
10407
+ const result = [];
10408
+ for (const col of columns) {
10409
+ const value = row[col.name];
10410
+ result.push(serializeValue(value, col));
10411
+ }
10412
+ return result;
10413
+ }
10414
+ function serializeValue(value, column) {
10415
+ if (value === null || value === void 0) {
10416
+ return null;
10417
+ }
10418
+ if (isEncodedType(column.type)) {
10419
+ return encodeValue(value, column.type);
10420
+ }
10421
+ return serializeSimple(value, column.type);
10422
+ }
10423
+ function serializeSimple(value, type) {
10424
+ switch (type) {
10425
+ case "bigint":
10426
+ return String(value);
10427
+ case "decimal":
10428
+ return String(value);
10429
+ case "bool":
10430
+ if (typeof value === "number") return value !== 0;
10431
+ if (typeof value === "string") return value === "1" || value.toLowerCase() === "true";
10432
+ return Boolean(value);
10433
+ case "timestamp": {
10434
+ if (value instanceof Date) return value.toISOString();
10435
+ return String(value);
10436
+ }
10437
+ case "date": {
10438
+ if (value instanceof Date) return value.toISOString().split("T")[0];
10439
+ return String(value);
10440
+ }
10441
+ case "uuid":
10442
+ return String(value);
10443
+ case "int":
10444
+ case "float":
10445
+ return typeof value === "string" ? Number(value) : value;
10446
+ case "string":
10447
+ default:
10448
+ return String(value);
10449
+ }
10450
+ }
10451
+ function encodeValue(value, type) {
10452
+ if (type === "binary") {
10453
+ return encodeBinary(value);
10454
+ }
10455
+ return encodeJsonLike(value);
10456
+ }
10457
+ function encodeBinary(value) {
10458
+ const buf = toBuffer(value);
10459
+ const byteLength = buf.length;
10460
+ if (byteLength < GZIP_THRESHOLD) {
10461
+ return [buf.toString("base64"), "b64"];
10462
+ }
10463
+ const compressed = gzipSync(buf);
10464
+ if (compressed.length < byteLength * GZIP_RATIO_THRESHOLD) {
10465
+ return [compressed.toString("base64"), "gz64"];
10466
+ }
10467
+ return [buf.toString("base64"), "b64"];
10468
+ }
10469
+ function encodeJsonLike(value) {
10470
+ const jsonStr = JSON.stringify(value);
10471
+ const byteLength = Buffer.byteLength(jsonStr, "utf8");
10472
+ if (byteLength < GZIP_THRESHOLD) {
10473
+ return [value, "raw"];
10474
+ }
10475
+ const buf = Buffer.from(jsonStr, "utf8");
10476
+ const compressed = gzipSync(buf);
10477
+ if (compressed.length < byteLength * GZIP_RATIO_THRESHOLD) {
10478
+ return [compressed.toString("base64"), "gz64"];
10479
+ }
10480
+ return [value, "raw"];
10481
+ }
10482
+ function toBuffer(value) {
10483
+ if (Buffer.isBuffer(value)) return value;
10484
+ if (value instanceof Uint8Array) return Buffer.from(value);
10485
+ if (typeof value === "string") return Buffer.from(value, "base64");
10486
+ return Buffer.from(String(value), "utf8");
10487
+ }
10488
+ function deserializeRow(options) {
10489
+ const { values, columns, targetDialect, targetVersion } = options;
10490
+ const result = {};
10491
+ for (let i = 0; i < columns.length; i++) {
10492
+ const col = columns[i];
10493
+ const value = values[i];
10494
+ result[col.name] = deserializeValue(value, col, targetDialect, targetVersion);
10495
+ }
10496
+ return result;
10497
+ }
10498
+ function deserializeValue(value, column, targetDialect, targetVersion) {
10499
+ if (value === null || value === void 0) {
10500
+ return null;
10501
+ }
10502
+ if (isEncodedType(column.type)) {
10503
+ const decoded = decodeTuple(value);
10504
+ return convertEncodedForTarget(decoded, column.type, targetDialect, targetVersion);
10505
+ }
10506
+ return convertSimpleForTarget(value, column.type, targetDialect);
10507
+ }
10508
+ function decodeTuple(tuple) {
10509
+ if (!Array.isArray(tuple) || tuple.length < 2) {
10510
+ return tuple;
10511
+ }
10512
+ const [value, encoding] = tuple;
10513
+ switch (encoding) {
10514
+ case "raw":
10515
+ return value;
10516
+ case "b64":
10517
+ return Buffer.from(value, "base64");
10518
+ case "gz64": {
10519
+ const compressed = Buffer.from(value, "base64");
10520
+ const decompressed = gunzipSync(compressed);
10521
+ const str = decompressed.toString("utf8");
10522
+ if (/^[[{"\d]/.test(str)) {
10523
+ return JSON.parse(str);
10524
+ }
10525
+ return decompressed;
10526
+ }
10527
+ default:
10528
+ return value;
10529
+ }
10530
+ }
10531
+ function convertEncodedForTarget(decoded, type, targetDialect, targetVersion) {
10532
+ switch (type) {
10533
+ case "json":
10534
+ return convertJsonForTarget(decoded, targetDialect, targetVersion);
10535
+ case "binary":
10536
+ return decoded;
10537
+ case "vector":
10538
+ return convertVectorForTarget(decoded, targetDialect, targetVersion);
10539
+ case "array":
10540
+ return convertArrayForTarget(decoded, targetDialect);
10541
+ case "custom":
10542
+ if (typeof decoded === "object") return JSON.stringify(decoded);
10543
+ return decoded;
10544
+ default:
10545
+ return decoded;
10546
+ }
10547
+ }
10548
+ function convertJsonForTarget(decoded, targetDialect, targetVersion) {
10549
+ if (targetDialect === "mssql") {
10550
+ const major = targetVersion?.major ?? 2022;
10551
+ if (major < 2025) {
10552
+ return typeof decoded === "string" ? decoded : JSON.stringify(decoded);
10553
+ }
10554
+ }
10555
+ return decoded;
10556
+ }
10557
+ function convertVectorForTarget(decoded, targetDialect, targetVersion) {
10558
+ const arr = Array.isArray(decoded) ? decoded : decoded;
10559
+ if (targetDialect === "postgres") {
10560
+ return Array.isArray(arr) ? `[${arr.join(",")}]` : String(arr);
10561
+ }
10562
+ if (targetDialect === "mysql") {
10563
+ const major = targetVersion?.major ?? 8;
10564
+ if (major >= 9) {
10565
+ return Array.isArray(arr) ? `[${arr.join(",")}]` : String(arr);
10566
+ }
10567
+ return Array.isArray(arr) ? JSON.stringify(arr) : String(arr);
10568
+ }
10569
+ return Array.isArray(arr) ? JSON.stringify(arr) : String(arr);
10570
+ }
10571
+ function convertArrayForTarget(decoded, targetDialect) {
10572
+ if (targetDialect === "postgres") {
10573
+ return decoded;
10574
+ }
10575
+ return Array.isArray(decoded) ? JSON.stringify(decoded) : String(decoded);
10576
+ }
10577
+ function convertSimpleForTarget(value, type, targetDialect) {
10578
+ switch (type) {
10579
+ case "bigint":
10580
+ if (typeof value === "string") {
10581
+ const num = Number(value);
10582
+ if (Number.isSafeInteger(num)) return num;
10583
+ }
10584
+ return value;
10585
+ case "decimal":
10586
+ return value;
10587
+ case "bool":
10588
+ if (targetDialect === "mssql") return value ? 1 : 0;
10589
+ if (targetDialect === "mysql") return value ? 1 : 0;
10590
+ return value;
10591
+ case "timestamp":
10592
+ if (typeof value === "string") return new Date(value);
10593
+ return value;
10594
+ case "date":
10595
+ return value;
10596
+ case "uuid":
10597
+ return value;
10598
+ case "int":
10599
+ case "float":
10600
+ return value;
10601
+ case "string":
10602
+ default:
10603
+ return value;
10604
+ }
10605
+ }
10606
+
10607
+ // src/core/dt/index.ts
10608
+ async function exportTable(options) {
10609
+ const startTime = Date.now();
10610
+ const { db, dialect, tableName, filepath, passphrase, schema: schemaName } = options;
10611
+ const kyselyDb = db;
10612
+ const batchSize = options.batchSize ?? 1e3;
10613
+ const [dtSchema, schemaErr] = await buildDtSchema({
10614
+ db: kyselyDb,
10615
+ dialect,
10616
+ tableName,
10617
+ version: options.version,
10618
+ schema: schemaName
10619
+ });
10620
+ if (schemaErr || !dtSchema) {
10621
+ return [null, schemaErr ?? new Error("Failed to build schema")];
10622
+ }
10623
+ observer.emit("dt:export:start", {
10624
+ filepath,
10625
+ table: tableName,
10626
+ columnCount: dtSchema.columns.length
10627
+ });
10628
+ const writer = new DtWriter({ filepath, schema: dtSchema, passphrase });
10629
+ const [, openErr] = await attempt(() => writer.open());
10630
+ if (openErr) {
10631
+ return [null, openErr];
10632
+ }
10633
+ let offset = 0;
10634
+ const columns = dtSchema.columns.map((c) => c.name);
10635
+ const quoteIdent4 = dialect === "mssql" ? (c) => `[${c}]` : dialect === "mysql" ? (c) => `\`${c}\`` : (c) => `"${c}"`;
10636
+ const columnList = columns.map(quoteIdent4).join(", ");
10637
+ while (true) {
10638
+ const [rows, fetchErr] = await attempt(() => {
10639
+ if (dialect === "mssql") {
10640
+ const orderCol = quoteIdent4(columns[0]);
10641
+ return sql`
10642
+ SELECT ${sql.raw(columnList)}
10643
+ FROM ${sql.table(tableName)}
10644
+ ORDER BY ${sql.raw(orderCol)}
10645
+ OFFSET ${offset} ROWS
10646
+ FETCH NEXT ${batchSize} ROWS ONLY
10647
+ `.execute(kyselyDb);
10648
+ }
10649
+ return sql`
10650
+ SELECT ${sql.raw(columnList)}
10651
+ FROM ${sql.table(tableName)}
10652
+ LIMIT ${batchSize}
10653
+ OFFSET ${offset}
10654
+ `.execute(kyselyDb);
10655
+ });
10656
+ if (fetchErr) {
10657
+ return [null, fetchErr];
10658
+ }
10659
+ if (rows.rows.length === 0) break;
10660
+ for (const row of rows.rows) {
10661
+ const values = serializeRow({ row, columns: dtSchema.columns });
10662
+ writer.writeRow(values);
10663
+ }
10664
+ offset += rows.rows.length;
10665
+ observer.emit("dt:export:progress", {
10666
+ filepath,
10667
+ table: tableName,
10668
+ rowsWritten: writer.rowsWritten,
10669
+ bytesWritten: writer.bytesWritten
10670
+ });
10671
+ if (rows.rows.length < batchSize) break;
10672
+ }
10673
+ const [, closeErr] = await attempt(() => writer.close());
10674
+ if (closeErr) {
10675
+ return [null, closeErr];
10676
+ }
10677
+ const durationMs = Date.now() - startTime;
10678
+ observer.emit("dt:export:complete", {
10679
+ filepath,
10680
+ table: tableName,
10681
+ rowsWritten: writer.rowsWritten,
10682
+ bytesWritten: writer.bytesWritten,
10683
+ durationMs
10684
+ });
10685
+ return [{ rowsWritten: writer.rowsWritten, bytesWritten: writer.bytesWritten }, null];
10686
+ }
10687
+ async function importDtFile(options) {
10688
+ const startTime = Date.now();
10689
+ const { filepath, db, dialect, passphrase, version } = options;
10690
+ const kyselyDb = db;
10691
+ const batchSize = options.batchSize ?? 1e3;
10692
+ const reader = new DtReader({ filepath, passphrase });
10693
+ const [, openErr] = await attempt(() => reader.open());
10694
+ if (openErr) {
10695
+ observer.emit("error", { source: "dt:import", error: openErr, context: { filepath } });
10696
+ return [null, openErr];
10697
+ }
10698
+ const dtSchema = reader.schema;
10699
+ if (!dtSchema) {
10700
+ const err = new Error("Failed to read .dt schema");
10701
+ observer.emit("error", { source: "dt:import", error: err, context: { filepath } });
10702
+ return [null, err];
10703
+ }
10704
+ const tableName = dtSchema.t ?? "unknown";
10705
+ observer.emit("dt:import:start", {
10706
+ filepath,
10707
+ sourceDialect: dtSchema.d,
10708
+ sourceVersion: dtSchema.dv,
10709
+ table: tableName
10710
+ });
10711
+ const [validation, validateErr] = await validateSchema({
10712
+ dtSchema,
10713
+ targetDb: kyselyDb,
10714
+ targetDialect: dialect,
10715
+ targetVersion: version
10716
+ });
10717
+ if (validateErr) {
10718
+ reader.close();
10719
+ observer.emit("error", { source: "dt:import", error: validateErr, context: { filepath, table: tableName } });
10720
+ return [null, validateErr];
10721
+ }
10722
+ observer.emit("dt:import:schema", {
10723
+ filepath,
10724
+ table: tableName,
10725
+ columns: dtSchema.columns.length,
10726
+ validation
10727
+ });
10728
+ if (validation && !validation.valid) {
10729
+ reader.close();
10730
+ const err = new Error(`Schema validation failed: ${validation.errors.join("; ")}`);
10731
+ observer.emit("error", { source: "dt:import", error: err, context: { filepath, table: tableName } });
10732
+ return [null, err];
10733
+ }
10734
+ if (options.truncate && tableName !== "unknown") {
10735
+ const truncateSql = dialect === "postgres" ? `TRUNCATE TABLE "${tableName}" CASCADE` : dialect === "mssql" ? `DELETE FROM [${tableName}]` : `TRUNCATE TABLE \`${tableName}\``;
10736
+ const [, truncErr] = await attempt(() => sql.raw(truncateSql).execute(kyselyDb));
10737
+ if (truncErr) {
10738
+ reader.close();
10739
+ const err = new Error(`Failed to truncate: ${truncErr.message}`);
10740
+ observer.emit("error", { source: "dt:import", error: err, context: { filepath, table: tableName } });
10741
+ return [null, err];
10742
+ }
10743
+ }
10744
+ let rowsImported = 0;
10745
+ let rowsSkipped = 0;
10746
+ let batch = [];
10747
+ for await (const values of reader.rows()) {
10748
+ const row = deserializeRow({
10749
+ values,
10750
+ columns: dtSchema.columns,
10751
+ targetDialect: dialect,
10752
+ targetVersion: version
10753
+ });
10754
+ batch.push(row);
10755
+ if (batch.length >= batchSize) {
10756
+ const columnNames = dtSchema.columns.map((c) => c.name);
10757
+ const [batchResult, insertErr] = await insertImportBatch(
10758
+ kyselyDb,
10759
+ tableName,
10760
+ batch,
10761
+ options.onConflict ?? "fail",
10762
+ dialect,
10763
+ columnNames
10764
+ );
10765
+ if (insertErr) {
10766
+ reader.close();
10767
+ observer.emit("error", { source: "dt:import", error: insertErr, context: { filepath, table: tableName } });
10768
+ return [null, insertErr];
10769
+ }
10770
+ rowsImported += batchResult.inserted + batchResult.updated;
10771
+ rowsSkipped += batchResult.skipped;
10772
+ batch = [];
10773
+ observer.emit("dt:import:progress", {
10774
+ filepath,
10775
+ table: tableName,
10776
+ rowsImported,
10777
+ rowsSkipped
10778
+ });
10779
+ }
10780
+ }
10781
+ if (batch.length > 0) {
10782
+ const columnNames = dtSchema.columns.map((c) => c.name);
10783
+ const [batchResult, insertErr] = await insertImportBatch(
10784
+ kyselyDb,
10785
+ tableName,
10786
+ batch,
10787
+ options.onConflict ?? "fail",
10788
+ dialect,
10789
+ columnNames
10790
+ );
10791
+ if (insertErr) {
10792
+ reader.close();
10793
+ observer.emit("error", { source: "dt:import", error: insertErr, context: { filepath, table: tableName } });
10794
+ return [null, insertErr];
10795
+ }
10796
+ rowsImported += batchResult.inserted + batchResult.updated;
10797
+ rowsSkipped += batchResult.skipped;
10798
+ }
10799
+ reader.close();
10800
+ const durationMs = Date.now() - startTime;
10801
+ observer.emit("dt:import:complete", {
10802
+ filepath,
10803
+ table: tableName,
10804
+ rowsImported,
10805
+ rowsSkipped,
10806
+ durationMs
10807
+ });
10808
+ return [{ rowsImported, rowsSkipped }, null];
10809
+ }
10810
+ async function insertImportBatch(db, table, rows, onConflict, dialect, columns) {
10811
+ let inserted = 0;
10812
+ let skipped = 0;
10813
+ let updated = 0;
10814
+ for (const row of rows) {
10815
+ const [, err] = await attempt(
10816
+ () => db.insertInto(table).values(row).execute()
10817
+ );
10818
+ if (err) {
10819
+ if (!isDuplicateError(err.message)) {
10820
+ if (onConflict === "fail") {
10821
+ return [{ inserted, skipped, updated }, err];
10822
+ }
10823
+ skipped++;
10824
+ continue;
10825
+ }
10826
+ if (onConflict === "skip") {
10827
+ skipped++;
10828
+ } else if (onConflict === "update") {
10829
+ const [updateResult, updateErr] = await attemptUpdate(db, table, row, dialect, columns);
10830
+ if (updateErr) {
10831
+ return [{ inserted, skipped, updated }, updateErr];
10832
+ }
10833
+ if (updateResult) {
10834
+ updated++;
10835
+ } else {
10836
+ skipped++;
10837
+ }
10838
+ } else if (onConflict === "fail") {
10839
+ return [{ inserted, skipped, updated }, err];
10840
+ } else {
10841
+ skipped++;
10842
+ }
10843
+ } else {
10844
+ inserted++;
10845
+ }
10846
+ }
10847
+ return [{ inserted, skipped, updated }, null];
10848
+ }
10849
+ async function attemptUpdate(db, table, row, _dialect, columns) {
10850
+ const pkColumn = columns[0];
10851
+ if (!pkColumn || row[pkColumn] === void 0) {
10852
+ return [false, null];
10853
+ }
10854
+ const pkValue = row[pkColumn];
10855
+ const updateValues = {};
10856
+ for (const col of columns) {
10857
+ if (col !== pkColumn && row[col] !== void 0) {
10858
+ updateValues[col] = row[col];
10859
+ }
10860
+ }
10861
+ if (Object.keys(updateValues).length === 0) {
10862
+ return [true, null];
10863
+ }
10864
+ const [, err] = await attempt(
10865
+ () => db.updateTable(table).set(updateValues).where(pkColumn, "=", pkValue).execute()
10866
+ );
10867
+ if (err) {
10868
+ return [false, err];
10869
+ }
10870
+ return [true, null];
10871
+ }
10872
+ function isDuplicateError(message) {
10873
+ const lower = message.toLowerCase();
10874
+ return lower.includes("duplicate") || lower.includes("unique constraint") || lower.includes("primary key") || lower.includes("violates unique") || lower.includes("cannot insert duplicate") || lower.includes("duplicate entry");
10875
+ }
10876
+
10877
+ // src/sdk/guards.ts
10878
+ var RequireTestError = class extends Error {
10879
+ constructor(configName) {
10880
+ super(`Config "${configName}" does not have isTest: true`);
10881
+ this.configName = configName;
10882
+ }
10883
+ name = "RequireTestError";
10884
+ };
10885
+ var ProtectedConfigError = class extends Error {
10886
+ constructor(configName, operation) {
10887
+ super(`Cannot ${operation} on protected config "${configName}"`);
10888
+ this.configName = configName;
10889
+ this.operation = operation;
10890
+ }
10891
+ name = "ProtectedConfigError";
10892
+ };
10893
+ function checkRequireTest(config, options) {
10894
+ if (options.requireTest && !config.isTest) {
10895
+ throw new RequireTestError(config.name);
10896
+ }
10897
+ }
10898
+ function checkProtectedConfig(config, operation, options) {
10899
+ if (config.protected && !options.allowProtected) {
10900
+ throw new ProtectedConfigError(config.name, operation);
10901
+ }
10902
+ }
10903
+
10904
+ // src/sdk/noorm-ops.ts
10905
+ var NoormOps = class {
10906
+ #state;
10907
+ constructor(state) {
10908
+ this.#state = state;
10909
+ }
10910
+ // ─────────────────────────────────────────────────────────
10911
+ // Read-only Properties
10912
+ // ─────────────────────────────────────────────────────────
10913
+ get config() {
10914
+ return this.#state.config;
10915
+ }
10916
+ get settings() {
10917
+ return this.#state.settings;
10918
+ }
10919
+ get identity() {
10920
+ return this.#state.identity;
10921
+ }
10922
+ get observer() {
10923
+ return observer;
10924
+ }
10925
+ // ─────────────────────────────────────────────────────────
10926
+ // Private Accessors
10927
+ // ─────────────────────────────────────────────────────────
10928
+ get #kysely() {
10929
+ if (!this.#state.connection) {
10930
+ throw new Error("Not connected. Call connect() first.");
10931
+ }
10932
+ return this.#state.connection.db;
10933
+ }
10934
+ get #dialect() {
10935
+ return this.#state.config.connection.dialect;
10936
+ }
10937
+ // ─────────────────────────────────────────────────────────
10938
+ // Explore
10939
+ // ─────────────────────────────────────────────────────────
10940
+ /**
10941
+ * List all tables in the database.
10942
+ *
10943
+ * @example
10944
+ * ```typescript
10945
+ * const tables = await ctx.noorm.listTables()
10946
+ * ```
10947
+ */
10948
+ async listTables() {
10949
+ return fetchList(this.#kysely, this.#dialect, "tables");
10950
+ }
10951
+ /**
10952
+ * Get detailed information about a table.
10953
+ *
10954
+ * @example
10955
+ * ```typescript
10956
+ * const detail = await ctx.noorm.describeTable('users')
10957
+ * ```
10958
+ */
10959
+ async describeTable(name, schema) {
10960
+ return fetchDetail(this.#kysely, this.#dialect, "tables", name, schema);
10961
+ }
10962
+ /**
10963
+ * Get database overview with counts of all object types.
10964
+ *
10965
+ * @example
10966
+ * ```typescript
10967
+ * const overview = await ctx.noorm.overview()
10968
+ * ```
10969
+ */
10970
+ async overview() {
10971
+ return fetchOverview(this.#kysely, this.#dialect);
10972
+ }
10973
+ // ─────────────────────────────────────────────────────────
10974
+ // Schema Operations
10975
+ // ─────────────────────────────────────────────────────────
10976
+ /**
10977
+ * Wipe all data, keeping the schema intact.
10978
+ *
10979
+ * @example
10980
+ * ```typescript
10981
+ * const result = await ctx.noorm.truncate()
10982
+ * ```
10983
+ */
7957
10984
  async truncate() {
7958
- checkProtectedConfig(this.#config, "truncate", this.#options);
7959
- return truncateData(this.kysely, this.dialect);
10985
+ checkProtectedConfig(this.#state.config, "truncate", this.#state.options);
10986
+ return truncateData(this.#kysely, this.#dialect);
7960
10987
  }
10988
+ /**
10989
+ * Drop all database objects except noorm tracking tables.
10990
+ *
10991
+ * @example
10992
+ * ```typescript
10993
+ * const result = await ctx.noorm.teardown()
10994
+ * ```
10995
+ */
7961
10996
  async teardown() {
7962
- checkProtectedConfig(this.#config, "teardown", this.#options);
7963
- return teardownSchema(this.kysely, this.dialect, {
7964
- configName: this.#config.name,
7965
- executedBy: formatIdentity(this.#identity)
10997
+ checkProtectedConfig(this.#state.config, "teardown", this.#state.options);
10998
+ return teardownSchema(this.#kysely, this.#dialect, {
10999
+ configName: this.#state.config.name,
11000
+ executedBy: formatIdentity(this.#state.identity)
7966
11001
  });
7967
11002
  }
11003
+ /**
11004
+ * Execute all SQL files in the schema directory.
11005
+ *
11006
+ * @example
11007
+ * ```typescript
11008
+ * const result = await ctx.noorm.build({ force: true })
11009
+ * ```
11010
+ */
7968
11011
  async build(options) {
7969
11012
  const runContext = this.#createRunContext();
7970
- const sqlPath = path6.join(
7971
- this.#projectRoot,
7972
- this.#config.paths.sql
11013
+ const sqlPath = path7.join(
11014
+ this.#state.projectRoot,
11015
+ this.#state.config.paths.sql
7973
11016
  );
7974
11017
  return runBuild(runContext, sqlPath, { force: options?.force });
7975
11018
  }
11019
+ /**
11020
+ * Full rebuild: teardown + build.
11021
+ *
11022
+ * @example
11023
+ * ```typescript
11024
+ * await ctx.noorm.reset()
11025
+ * ```
11026
+ */
7976
11027
  async reset() {
7977
- checkProtectedConfig(this.#config, "reset", this.#options);
11028
+ checkProtectedConfig(this.#state.config, "reset", this.#state.options);
7978
11029
  await this.teardown();
7979
11030
  await this.build({ force: true });
7980
11031
  }
7981
11032
  // ─────────────────────────────────────────────────────────
7982
11033
  // File Runner
7983
11034
  // ─────────────────────────────────────────────────────────
11035
+ /**
11036
+ * Execute a single SQL file.
11037
+ *
11038
+ * @example
11039
+ * ```typescript
11040
+ * await ctx.noorm.runFile('seeds/test-data.sql')
11041
+ * ```
11042
+ */
7984
11043
  async runFile(filepath, options) {
7985
11044
  const runContext = this.#createRunContext();
7986
- const absolutePath = path6.isAbsolute(filepath) ? filepath : path6.join(this.#projectRoot, filepath);
11045
+ const absolutePath = path7.isAbsolute(filepath) ? filepath : path7.join(this.#state.projectRoot, filepath);
7987
11046
  return runFile(runContext, absolutePath, options);
7988
11047
  }
11048
+ /**
11049
+ * Execute multiple SQL files sequentially.
11050
+ *
11051
+ * @example
11052
+ * ```typescript
11053
+ * await ctx.noorm.runFiles(['functions/utils.sql', 'triggers/audit.sql'])
11054
+ * ```
11055
+ */
7989
11056
  async runFiles(filepaths, options) {
7990
11057
  const runContext = this.#createRunContext();
7991
11058
  const absolutePaths = filepaths.map(
7992
- (fp) => path6.isAbsolute(fp) ? fp : path6.join(this.#projectRoot, fp)
11059
+ (fp) => path7.isAbsolute(fp) ? fp : path7.join(this.#state.projectRoot, fp)
7993
11060
  );
7994
11061
  return runFiles(runContext, absolutePaths, options);
7995
11062
  }
11063
+ /**
11064
+ * Execute all SQL files in a directory.
11065
+ *
11066
+ * @example
11067
+ * ```typescript
11068
+ * await ctx.noorm.runDir('seeds/')
11069
+ * ```
11070
+ */
7996
11071
  async runDir(dirpath, options) {
7997
11072
  const runContext = this.#createRunContext();
7998
- const absolutePath = path6.isAbsolute(dirpath) ? dirpath : path6.join(this.#projectRoot, dirpath);
11073
+ const absolutePath = path7.isAbsolute(dirpath) ? dirpath : path7.join(this.#state.projectRoot, dirpath);
7999
11074
  return runDir(runContext, absolutePath, options);
8000
11075
  }
8001
11076
  // ─────────────────────────────────────────────────────────
8002
11077
  // Changes
8003
11078
  // ─────────────────────────────────────────────────────────
11079
+ /**
11080
+ * Apply a specific change.
11081
+ *
11082
+ * @example
11083
+ * ```typescript
11084
+ * const result = await ctx.noorm.applyChange('2024-01-15-add-users')
11085
+ * ```
11086
+ */
8004
11087
  async applyChange(name, options) {
8005
11088
  return this.#getChangeManager().run(name, options);
8006
11089
  }
11090
+ /**
11091
+ * Revert a specific change.
11092
+ *
11093
+ * @example
11094
+ * ```typescript
11095
+ * const result = await ctx.noorm.revertChange('2024-01-15-add-users')
11096
+ * ```
11097
+ */
8007
11098
  async revertChange(name, options) {
8008
11099
  return this.#getChangeManager().revert(name, options);
8009
11100
  }
11101
+ /**
11102
+ * Apply all pending changes.
11103
+ *
11104
+ * @example
11105
+ * ```typescript
11106
+ * const result = await ctx.noorm.fastForward()
11107
+ * ```
11108
+ */
8010
11109
  async fastForward() {
8011
11110
  return this.#getChangeManager().ff();
8012
11111
  }
11112
+ /**
11113
+ * Get status of all changes.
11114
+ *
11115
+ * @example
11116
+ * ```typescript
11117
+ * const changes = await ctx.noorm.getChangeStatus()
11118
+ * ```
11119
+ */
8013
11120
  async getChangeStatus() {
8014
11121
  return this.#getChangeManager().list();
8015
11122
  }
11123
+ /**
11124
+ * Get only pending changes.
11125
+ *
11126
+ * @example
11127
+ * ```typescript
11128
+ * const pending = await ctx.noorm.getPendingChanges()
11129
+ * ```
11130
+ */
8016
11131
  async getPendingChanges() {
8017
11132
  const all = await this.getChangeStatus();
8018
11133
  return all.filter(
@@ -8022,122 +11137,437 @@ var Context = class {
8022
11137
  // ─────────────────────────────────────────────────────────
8023
11138
  // Secrets
8024
11139
  // ─────────────────────────────────────────────────────────
11140
+ /**
11141
+ * Get a config-scoped secret.
11142
+ *
11143
+ * @example
11144
+ * ```typescript
11145
+ * const apiKey = ctx.noorm.getSecret('API_KEY')
11146
+ * ```
11147
+ */
8025
11148
  getSecret(key) {
8026
- const state = getStateManager(this.#projectRoot);
8027
- const value = state.getSecret(this.#config.name, key);
11149
+ const state = getStateManager(this.#state.projectRoot);
11150
+ const value = state.getSecret(this.#state.config.name, key);
8028
11151
  return value ?? void 0;
8029
11152
  }
8030
11153
  // ─────────────────────────────────────────────────────────
8031
11154
  // Locks
8032
11155
  // ─────────────────────────────────────────────────────────
11156
+ /**
11157
+ * Acquire a database lock.
11158
+ *
11159
+ * @example
11160
+ * ```typescript
11161
+ * const lock = await ctx.noorm.acquireLock({ timeout: 60000 })
11162
+ * ```
11163
+ */
8033
11164
  async acquireLock(options) {
8034
11165
  const lockManager = getLockManager();
8035
- const identityStr = formatIdentity(this.#identity);
11166
+ const identityStr = formatIdentity(this.#state.identity);
8036
11167
  return lockManager.acquire(
8037
- this.kysely,
8038
- this.#config.name,
11168
+ this.#kysely,
11169
+ this.#state.config.name,
8039
11170
  identityStr,
8040
- { ...options, dialect: this.#config.connection.dialect }
11171
+ { ...options, dialect: this.#state.config.connection.dialect }
8041
11172
  );
8042
11173
  }
11174
+ /**
11175
+ * Release the current lock.
11176
+ *
11177
+ * @example
11178
+ * ```typescript
11179
+ * await ctx.noorm.releaseLock()
11180
+ * ```
11181
+ */
8043
11182
  async releaseLock() {
8044
11183
  const lockManager = getLockManager();
8045
- const identityStr = formatIdentity(this.#identity);
11184
+ const identityStr = formatIdentity(this.#state.identity);
8046
11185
  await lockManager.release(
8047
- this.kysely,
8048
- this.#config.name,
11186
+ this.#kysely,
11187
+ this.#state.config.name,
8049
11188
  identityStr
8050
11189
  );
8051
11190
  }
11191
+ /**
11192
+ * Get current lock status.
11193
+ *
11194
+ * @example
11195
+ * ```typescript
11196
+ * const status = await ctx.noorm.getLockStatus()
11197
+ * ```
11198
+ */
8052
11199
  async getLockStatus() {
8053
11200
  const lockManager = getLockManager();
8054
11201
  return lockManager.status(
8055
- this.kysely,
8056
- this.#config.name,
8057
- this.#config.connection.dialect
11202
+ this.#kysely,
11203
+ this.#state.config.name,
11204
+ this.#state.config.connection.dialect
8058
11205
  );
8059
11206
  }
11207
+ /**
11208
+ * Execute an operation with automatic lock acquisition and release.
11209
+ *
11210
+ * @example
11211
+ * ```typescript
11212
+ * await ctx.noorm.withLock(async () => {
11213
+ * await ctx.noorm.build()
11214
+ * await ctx.noorm.fastForward()
11215
+ * })
11216
+ * ```
11217
+ */
8060
11218
  async withLock(fn, options) {
8061
11219
  const lockManager = getLockManager();
8062
- const identityStr = formatIdentity(this.#identity);
11220
+ const identityStr = formatIdentity(this.#state.identity);
8063
11221
  return lockManager.withLock(
8064
- this.kysely,
8065
- this.#config.name,
11222
+ this.#kysely,
11223
+ this.#state.config.name,
8066
11224
  identityStr,
8067
11225
  fn,
8068
- { ...options, dialect: this.#config.connection.dialect }
11226
+ { ...options, dialect: this.#state.config.connection.dialect }
8069
11227
  );
8070
11228
  }
11229
+ /**
11230
+ * Force release any database lock regardless of ownership.
11231
+ *
11232
+ * @example
11233
+ * ```typescript
11234
+ * await ctx.noorm.forceReleaseLock()
11235
+ * ```
11236
+ */
8071
11237
  async forceReleaseLock() {
8072
11238
  const lockManager = getLockManager();
8073
11239
  return lockManager.forceRelease(
8074
- this.kysely,
8075
- this.#config.name
11240
+ this.#kysely,
11241
+ this.#state.config.name
8076
11242
  );
8077
11243
  }
8078
11244
  // ─────────────────────────────────────────────────────────
8079
11245
  // Templates
8080
11246
  // ─────────────────────────────────────────────────────────
11247
+ /**
11248
+ * Render a template file without executing.
11249
+ *
11250
+ * @example
11251
+ * ```typescript
11252
+ * const result = await ctx.noorm.renderTemplate('sql/001_users.sql.tmpl')
11253
+ * ```
11254
+ */
8081
11255
  async renderTemplate(filepath) {
8082
- const absolutePath = path6.isAbsolute(filepath) ? filepath : path6.join(this.#projectRoot, filepath);
8083
- const state = getStateManager(this.#projectRoot);
11256
+ const absolutePath = path7.isAbsolute(filepath) ? filepath : path7.join(this.#state.projectRoot, filepath);
11257
+ const state = getStateManager(this.#state.projectRoot);
8084
11258
  return processFile(absolutePath, {
8085
- projectRoot: this.#projectRoot,
8086
- config: this.#config,
8087
- secrets: state.getAllSecrets(this.#config.name),
11259
+ projectRoot: this.#state.projectRoot,
11260
+ config: this.#state.config,
11261
+ secrets: state.getAllSecrets(this.#state.config.name),
8088
11262
  globalSecrets: state.getAllGlobalSecrets()
8089
11263
  });
8090
11264
  }
8091
11265
  // ─────────────────────────────────────────────────────────
8092
11266
  // History
8093
11267
  // ─────────────────────────────────────────────────────────
11268
+ /**
11269
+ * Get execution history.
11270
+ *
11271
+ * @example
11272
+ * ```typescript
11273
+ * const history = await ctx.noorm.getHistory(10)
11274
+ * ```
11275
+ */
8094
11276
  async getHistory(limit) {
8095
11277
  return this.#getChangeManager().getHistory(void 0, limit);
8096
11278
  }
8097
11279
  // ─────────────────────────────────────────────────────────
8098
11280
  // Utilities
8099
11281
  // ─────────────────────────────────────────────────────────
11282
+ /**
11283
+ * Compute SHA-256 checksum for a file.
11284
+ *
11285
+ * @example
11286
+ * ```typescript
11287
+ * const checksum = await ctx.noorm.computeChecksum('sql/001_users.sql')
11288
+ * ```
11289
+ */
8100
11290
  async computeChecksum(filepath) {
8101
- const absolutePath = path6.isAbsolute(filepath) ? filepath : path6.join(this.#projectRoot, filepath);
11291
+ const absolutePath = path7.isAbsolute(filepath) ? filepath : path7.join(this.#state.projectRoot, filepath);
8102
11292
  return computeChecksum(absolutePath);
8103
11293
  }
11294
+ /**
11295
+ * Tests if the connection can be established.
11296
+ *
11297
+ * @example
11298
+ * ```typescript
11299
+ * const result = await ctx.noorm.testConnection()
11300
+ * ```
11301
+ */
8104
11302
  async testConnection() {
8105
- return testConnection(this.#config.connection);
11303
+ return testConnection(this.#state.config.connection);
11304
+ }
11305
+ // ─────────────────────────────────────────────────────────
11306
+ // Transfer
11307
+ // ─────────────────────────────────────────────────────────
11308
+ /**
11309
+ * Transfer data from this context's database to a destination context.
11310
+ *
11311
+ * Both contexts must be connected. Uses each context's config for
11312
+ * connection management.
11313
+ *
11314
+ * @example
11315
+ * ```typescript
11316
+ * const [result, err] = await source.noorm.transferTo(dest, {
11317
+ * tables: ['users', 'posts'],
11318
+ * onConflict: 'skip',
11319
+ * })
11320
+ * ```
11321
+ */
11322
+ async transferTo(destConfig, options) {
11323
+ return transferData(this.#state.config, destConfig, options);
11324
+ }
11325
+ /**
11326
+ * Generate a transfer plan without executing.
11327
+ *
11328
+ * @example
11329
+ * ```typescript
11330
+ * const [plan, err] = await source.noorm.transferPlan(destConfig)
11331
+ * ```
11332
+ */
11333
+ async transferPlan(destConfig, options) {
11334
+ return getTransferPlan(this.#state.config, destConfig, options);
11335
+ }
11336
+ // ─────────────────────────────────────────────────────────
11337
+ // DT File Operations
11338
+ // ─────────────────────────────────────────────────────────
11339
+ /**
11340
+ * Export a table to a .dt file.
11341
+ *
11342
+ * @example
11343
+ * ```typescript
11344
+ * const [result, err] = await ctx.noorm.exportTable('users', './exports/users.dtz')
11345
+ * ```
11346
+ */
11347
+ async exportTable(tableName, filepath, options) {
11348
+ return exportTable({
11349
+ db: this.#kysely,
11350
+ dialect: this.#dialect,
11351
+ tableName,
11352
+ filepath,
11353
+ schema: options?.schema,
11354
+ passphrase: options?.passphrase,
11355
+ batchSize: options?.batchSize
11356
+ });
11357
+ }
11358
+ /**
11359
+ * Import a .dt file into the connected database.
11360
+ *
11361
+ * @example
11362
+ * ```typescript
11363
+ * const [result, err] = await ctx.noorm.importFile('./exports/users.dtz', {
11364
+ * onConflict: 'skip',
11365
+ * })
11366
+ * ```
11367
+ */
11368
+ async importFile(filepath, options) {
11369
+ return importDtFile({
11370
+ filepath,
11371
+ db: this.#kysely,
11372
+ dialect: this.#dialect,
11373
+ passphrase: options?.passphrase,
11374
+ batchSize: options?.batchSize,
11375
+ onConflict: options?.onConflict,
11376
+ truncate: options?.truncate
11377
+ });
8106
11378
  }
8107
11379
  // ─────────────────────────────────────────────────────────
8108
11380
  // Private Helpers
8109
11381
  // ─────────────────────────────────────────────────────────
8110
11382
  #createRunContext() {
8111
- const state = getStateManager(this.#projectRoot);
11383
+ const state = getStateManager(this.#state.projectRoot);
8112
11384
  return {
8113
- db: this.kysely,
8114
- configName: this.#config.name,
8115
- identity: this.#identity,
8116
- projectRoot: this.#projectRoot,
8117
- config: this.#config,
8118
- secrets: state.getAllSecrets(this.#config.name),
11385
+ db: this.#kysely,
11386
+ configName: this.#state.config.name,
11387
+ identity: this.#state.identity,
11388
+ projectRoot: this.#state.projectRoot,
11389
+ config: this.#state.config,
11390
+ secrets: state.getAllSecrets(this.#state.config.name),
8119
11391
  globalSecrets: state.getAllGlobalSecrets()
8120
11392
  };
8121
11393
  }
8122
11394
  #createChangeContext() {
8123
- const state = getStateManager(this.#projectRoot);
11395
+ const state = getStateManager(this.#state.projectRoot);
8124
11396
  return {
8125
- db: this.kysely,
8126
- configName: this.#config.name,
8127
- identity: this.#identity,
8128
- projectRoot: this.#projectRoot,
8129
- changesDir: path6.join(this.#projectRoot, this.#config.paths.changes),
8130
- sqlDir: path6.join(this.#projectRoot, this.#config.paths.sql),
8131
- config: this.#config,
8132
- secrets: state.getAllSecrets(this.#config.name),
11397
+ db: this.#kysely,
11398
+ configName: this.#state.config.name,
11399
+ identity: this.#state.identity,
11400
+ projectRoot: this.#state.projectRoot,
11401
+ changesDir: path7.join(this.#state.projectRoot, this.#state.config.paths.changes),
11402
+ sqlDir: path7.join(this.#state.projectRoot, this.#state.config.paths.sql),
11403
+ config: this.#state.config,
11404
+ secrets: state.getAllSecrets(this.#state.config.name),
8133
11405
  globalSecrets: state.getAllGlobalSecrets()
8134
11406
  };
8135
11407
  }
8136
11408
  #getChangeManager() {
8137
- if (!this.#changeManager) {
8138
- this.#changeManager = new ChangeManager(this.#createChangeContext());
11409
+ if (!this.#state.changeManager) {
11410
+ this.#state.changeManager = new ChangeManager(this.#createChangeContext());
11411
+ }
11412
+ return this.#state.changeManager;
11413
+ }
11414
+ };
11415
+
11416
+ // src/sdk/context.ts
11417
+ var Context = class {
11418
+ #state;
11419
+ #noorm = null;
11420
+ constructor(config, settings, identity, options, projectRoot) {
11421
+ this.#state = {
11422
+ connection: null,
11423
+ config,
11424
+ settings,
11425
+ identity,
11426
+ options,
11427
+ projectRoot,
11428
+ changeManager: null
11429
+ };
11430
+ }
11431
+ // ─────────────────────────────────────────────────────────
11432
+ // Read-only Properties
11433
+ // ─────────────────────────────────────────────────────────
11434
+ get dialect() {
11435
+ return this.#state.config.connection.dialect;
11436
+ }
11437
+ get connected() {
11438
+ return this.#state.connection !== null;
11439
+ }
11440
+ get kysely() {
11441
+ if (!this.#state.connection) {
11442
+ throw new Error("Not connected. Call connect() first.");
11443
+ }
11444
+ return this.#state.connection.db;
11445
+ }
11446
+ // ─────────────────────────────────────────────────────────
11447
+ // Noorm Namespace
11448
+ // ─────────────────────────────────────────────────────────
11449
+ /**
11450
+ * Noorm management operations.
11451
+ *
11452
+ * Lazily instantiated on first access. Returns the same instance
11453
+ * on repeated access (singleton per Context).
11454
+ *
11455
+ * @example
11456
+ * ```typescript
11457
+ * await ctx.noorm.build()
11458
+ * await ctx.noorm.fastForward()
11459
+ * const tables = await ctx.noorm.listTables()
11460
+ * ```
11461
+ */
11462
+ get noorm() {
11463
+ if (!this.#noorm) {
11464
+ this.#noorm = new NoormOps(this.#state);
11465
+ }
11466
+ return this.#noorm;
11467
+ }
11468
+ // ─────────────────────────────────────────────────────────
11469
+ // Lifecycle
11470
+ // ─────────────────────────────────────────────────────────
11471
+ async connect() {
11472
+ if (this.#state.connection) return;
11473
+ this.#state.connection = await createConnection(
11474
+ this.#state.config.connection,
11475
+ this.#state.config.name
11476
+ );
11477
+ }
11478
+ async disconnect() {
11479
+ if (!this.#state.connection) return;
11480
+ await this.#state.connection.destroy();
11481
+ this.#state.connection = null;
11482
+ this.#state.changeManager = null;
11483
+ }
11484
+ // ─────────────────────────────────────────────────────────
11485
+ // Transactions
11486
+ // ─────────────────────────────────────────────────────────
11487
+ /**
11488
+ * Execute operations within a database transaction.
11489
+ *
11490
+ * The callback receives a full Kysely Transaction instance with
11491
+ * query builder, `sql` template literal, and all Kysely features.
11492
+ *
11493
+ * @example
11494
+ * ```typescript
11495
+ * await ctx.transaction(async (trx) => {
11496
+ * await trx
11497
+ * .updateTable('accounts')
11498
+ * .set({ balance: sql`balance - ${amount}` })
11499
+ * .where('id', '=', fromId)
11500
+ * .execute();
11501
+ * await trx
11502
+ * .updateTable('accounts')
11503
+ * .set({ balance: sql`balance + ${amount}` })
11504
+ * .where('id', '=', toId)
11505
+ * .execute();
11506
+ * });
11507
+ * ```
11508
+ */
11509
+ async transaction(fn) {
11510
+ return this.kysely.transaction().execute(fn);
11511
+ }
11512
+ // ─────────────────────────────────────────────────────────
11513
+ // Stored Procedures & Functions
11514
+ // ─────────────────────────────────────────────────────────
11515
+ /**
11516
+ * Call a stored procedure and return the result set.
11517
+ *
11518
+ * Generates dialect-specific SQL: EXEC (MSSQL), CALL (PG/MySQL).
11519
+ * Named params use dialect-appropriate syntax; MySQL falls back
11520
+ * to positional. SQLite throws — no procedure support.
11521
+ *
11522
+ * @example
11523
+ * ```typescript
11524
+ * // Named params
11525
+ * const users = await ctx.proc<User>('get_users', { department_id: 1 });
11526
+ *
11527
+ * // Positional params
11528
+ * await ctx.proc('simple_proc', [42, 'hello']);
11529
+ *
11530
+ * // No params
11531
+ * await ctx.proc('refresh_cache');
11532
+ * ```
11533
+ */
11534
+ async proc(name, ...args) {
11535
+ if (this.dialect === "sqlite") {
11536
+ throw new Error("SQLite does not support stored procedures.");
11537
+ }
11538
+ const params = args[0];
11539
+ const query = buildProcCall(this.dialect, name, params);
11540
+ const result = await query.execute(this.kysely);
11541
+ return result.rows ?? [];
11542
+ }
11543
+ /**
11544
+ * Call a database function and return the scalar result.
11545
+ *
11546
+ * Generates SELECT name(...) AS column. Named params only on PG;
11547
+ * other dialects fall back to positional. SQLite throws.
11548
+ *
11549
+ * @example
11550
+ * ```typescript
11551
+ * // Named params + column alias
11552
+ * const result = await ctx.func<{ total: number }>('calc_total', { order_id: 42 }, 'total');
11553
+ *
11554
+ * // Positional params + column alias
11555
+ * const sum = await ctx.func<{ result: number }>('add_numbers', [1, 2], 'result');
11556
+ *
11557
+ * // No params — just column alias
11558
+ * const ver = await ctx.func<{ v: string }>('get_version', 'v');
11559
+ * ```
11560
+ */
11561
+ async func(name, ...args) {
11562
+ if (this.dialect === "sqlite") {
11563
+ throw new Error("SQLite does not support database function calls.");
8139
11564
  }
8140
- return this.#changeManager;
11565
+ const hasParams = !(args.length === 1 && typeof args[0] === "string");
11566
+ const params = hasParams ? args[0] : void 0;
11567
+ const column = hasParams ? args[1] : args[0];
11568
+ const query = buildFuncCall(this.dialect, name, column, params);
11569
+ const result = await query.execute(this.kysely);
11570
+ return result.rows?.[0] ?? null;
8141
11571
  }
8142
11572
  };
8143
11573
 
@@ -8167,6 +11597,6 @@ async function createContext(options = {}) {
8167
11597
  return new Context(config, settings, identity, options, projectRoot);
8168
11598
  }
8169
11599
 
8170
- export { Context, LockAcquireError, LockExpiredError, ProtectedConfigError, RequireTestError, createContext };
11600
+ export { Context, LockAcquireError, LockExpiredError, NoormOps, ProtectedConfigError, RequireTestError, createContext };
8171
11601
  //# sourceMappingURL=index.js.map
8172
11602
  //# sourceMappingURL=index.js.map