@noormdev/sdk 1.0.0-alpha.0 → 1.0.0-alpha.10

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,22 +1,41 @@
1
- import { observer, processFile, isTemplate } from './chunk-MERTHCAJ.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-GR57BRV4.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';
8
- import { createRequire } from 'module';
9
8
  import { z } from 'zod';
10
9
  import { parse, stringify } from 'yaml';
11
10
  import { execSync } from 'child_process';
12
11
  import { sql } from 'kysely';
13
12
  import 'dayjs';
14
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';
15
18
 
16
19
  var NOORM_HOME = join(homedir(), ".noorm");
17
20
  var PRIVATE_KEY_PATH = join(NOORM_HOME, "identity.key");
18
- join(NOORM_HOME, "identity.pub");
19
- 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
+ }
20
39
  async function loadPrivateKey() {
21
40
  const [content, err] = await attempt(() => readFile(PRIVATE_KEY_PATH, { encoding: "utf8" }));
22
41
  if (err) {
@@ -27,6 +46,16 @@ async function loadPrivateKey() {
27
46
  }
28
47
  return content.trim();
29
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
+ }
30
59
  function deriveStateKey(privateKey) {
31
60
  const privateKeyBuffer = Buffer.from(privateKey, "hex");
32
61
  return Buffer.from(
@@ -120,14 +149,15 @@ function needsMigration(state, currentVersion) {
120
149
  if (!("knownUsers" in obj)) return true;
121
150
  return false;
122
151
  }
123
- var require2 = createRequire(import.meta.url);
152
+
153
+ // src/core/state/version.ts
154
+ var VERSION = typeof __CLI_VERSION__ !== "undefined" ? __CLI_VERSION__ : "0.0.0-dev";
124
155
  function getPackageVersion() {
125
- const pkg = require2("../../../package.json");
126
- return pkg.version;
156
+ return VERSION;
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
  });
@@ -6539,6 +6951,11 @@ async function executeDryRun(context, change, files, direction, startTime) {
6539
6951
  const [checksum] = await attempt(() => computeChecksum(file.path));
6540
6952
  const [sqlContent, loadErr] = await attempt(() => loadAndRenderFile(context, file.path));
6541
6953
  if (loadErr) {
6954
+ observer.emit("file:dry-run", {
6955
+ filepath: file.path,
6956
+ status: "failed",
6957
+ error: loadErr.message
6958
+ });
6542
6959
  results.push({
6543
6960
  filepath: file.path,
6544
6961
  checksum: checksum ?? "",
@@ -6559,6 +6976,12 @@ async function executeDryRun(context, change, files, direction, startTime) {
6559
6976
  context: { filepath: file.path, operation: "dry-run-write" }
6560
6977
  });
6561
6978
  }
6979
+ const outputPath = getDryRunOutputPath(context.projectRoot, file.path);
6980
+ observer.emit("file:dry-run", {
6981
+ filepath: file.path,
6982
+ status: "success",
6983
+ outputPath
6984
+ });
6562
6985
  results.push({
6563
6986
  filepath: file.path,
6564
6987
  checksum: checksum ?? "",
@@ -6576,13 +6999,13 @@ async function executeDryRun(context, change, files, direction, startTime) {
6576
6999
  };
6577
7000
  }
6578
7001
  function getDryRunOutputPath(projectRoot, filepath) {
6579
- const relativePath = path6.relative(projectRoot, filepath);
7002
+ const relativePath = path7.relative(projectRoot, filepath);
6580
7003
  const outputRelativePath = relativePath.endsWith(".tmpl") ? relativePath.slice(0, -5) : relativePath;
6581
- return path6.join(projectRoot, "tmp", outputRelativePath);
7004
+ return path7.join(projectRoot, "tmp", outputRelativePath);
6582
7005
  }
6583
7006
  async function writeDryRunOutput(projectRoot, filepath, content) {
6584
7007
  const outputPath = getDryRunOutputPath(projectRoot, filepath);
6585
- const outputDir = path6.dirname(outputPath);
7008
+ const outputDir = path7.dirname(outputPath);
6586
7009
  await mkdir(outputDir, { recursive: true });
6587
7010
  await writeFile(outputPath, content, "utf-8");
6588
7011
  }
@@ -6908,7 +7331,7 @@ var ChangeManager = class {
6908
7331
  */
6909
7332
  async remove(name, options) {
6910
7333
  if (options.disk) {
6911
- const changePath = path6.join(this.#context.changesDir, name);
7334
+ const changePath = path7.join(this.#context.changesDir, name);
6912
7335
  const [change, loadErr] = await attempt(
6913
7336
  () => parseChange(changePath, this.#context.sqlDir)
6914
7337
  );
@@ -6943,7 +7366,7 @@ var ChangeManager = class {
6943
7366
  * Load a change from disk by name.
6944
7367
  */
6945
7368
  async #loadChange(name) {
6946
- const changePath = path6.join(this.#context.changesDir, name);
7369
+ const changePath = path7.join(this.#context.changesDir, name);
6947
7370
  const [change, err] = await attempt(
6948
7371
  () => parseChange(changePath, this.#context.sqlDir)
6949
7372
  );
@@ -7177,17 +7600,21 @@ var FILE_HEADER_TEMPLATE = `-- =================================================
7177
7600
  -- ============================================================
7178
7601
 
7179
7602
  `;
7180
- async function runBuild(context, sqlPath, options = {}) {
7603
+ async function runBuild(context, sqlPath, options = {}, preFilteredFiles) {
7181
7604
  const start = performance.now();
7182
7605
  const opts = { ...DEFAULT_RUN_OPTIONS_INTERNAL, ...options };
7183
- const [files, discoverErr] = await attempt(() => discoverFiles(sqlPath));
7184
- if (discoverErr) {
7185
- observer.emit("error", {
7186
- source: "runner",
7187
- error: discoverErr,
7188
- context: { sqlPath, operation: "discover-files" }
7189
- });
7190
- 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;
7191
7618
  }
7192
7619
  observer.emit("build:start", {
7193
7620
  sqlPath,
@@ -7708,6 +8135,11 @@ async function executeDryRun2(context, files) {
7708
8135
  const start = performance.now();
7709
8136
  const [checksum, checksumErr] = await attempt(() => computeChecksum(filepath));
7710
8137
  if (checksumErr) {
8138
+ observer.emit("file:dry-run", {
8139
+ filepath,
8140
+ status: "failed",
8141
+ error: checksumErr.message
8142
+ });
7711
8143
  results.push({
7712
8144
  filepath,
7713
8145
  checksum: "",
@@ -7719,6 +8151,11 @@ async function executeDryRun2(context, files) {
7719
8151
  }
7720
8152
  const [sqlContent, loadErr] = await attempt(() => loadAndRenderFile2(context, filepath));
7721
8153
  if (loadErr) {
8154
+ observer.emit("file:dry-run", {
8155
+ filepath,
8156
+ status: "failed",
8157
+ error: loadErr.message
8158
+ });
7722
8159
  results.push({
7723
8160
  filepath,
7724
8161
  checksum,
@@ -7742,6 +8179,7 @@ async function executeDryRun2(context, files) {
7742
8179
  const outputPath = getDryRunOutputPath2(context.projectRoot, filepath);
7743
8180
  observer.emit("file:dry-run", {
7744
8181
  filepath,
8182
+ status: "success",
7745
8183
  outputPath
7746
8184
  });
7747
8185
  results.push({
@@ -7755,13 +8193,13 @@ async function executeDryRun2(context, files) {
7755
8193
  return results;
7756
8194
  }
7757
8195
  function getDryRunOutputPath2(projectRoot, filepath) {
7758
- const relativePath = path6.relative(projectRoot, filepath);
8196
+ const relativePath = path7.relative(projectRoot, filepath);
7759
8197
  const outputRelativePath = relativePath.endsWith(".tmpl") ? relativePath.slice(0, -5) : relativePath;
7760
- return path6.join(projectRoot, "tmp", outputRelativePath);
8198
+ return path7.join(projectRoot, "tmp", outputRelativePath);
7761
8199
  }
7762
8200
  async function writeDryRunOutput2(projectRoot, filepath, content) {
7763
8201
  const outputPath = getDryRunOutputPath2(projectRoot, filepath);
7764
- const outputDir = path6.dirname(outputPath);
8202
+ const outputDir = path7.dirname(outputPath);
7765
8203
  await mkdir(outputDir, { recursive: true });
7766
8204
  await writeFile(outputPath, content, "utf-8");
7767
8205
  }
@@ -7773,7 +8211,7 @@ async function discoverFiles(dirpath) {
7773
8211
  throw new Error(`Failed to read directory: ${dir}`, { cause: err });
7774
8212
  }
7775
8213
  for (const entry of entries) {
7776
- const fullPath = path6.join(dir, entry.name);
8214
+ const fullPath = path7.join(dir, entry.name);
7777
8215
  if (entry.isDirectory()) {
7778
8216
  await scan(fullPath);
7779
8217
  } else if (entry.isFile() && isSqlFile(entry.name)) {
@@ -7797,200 +8235,2916 @@ function createFailedBatchResult(error, durationMs) {
7797
8235
  durationMs
7798
8236
  };
7799
8237
  }
7800
-
7801
- // src/sdk/guards.ts
7802
- var RequireTestError = class extends Error {
7803
- constructor(configName) {
7804
- super(`Config "${configName}" does not have isTest: true`);
7805
- this.configName = configName;
7806
- }
7807
- name = "RequireTestError";
7808
- };
7809
- var ProtectedConfigError = class extends Error {
7810
- constructor(configName, operation) {
7811
- super(`Cannot ${operation} on protected config "${configName}"`);
7812
- this.configName = configName;
7813
- 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);
7814
8280
  }
7815
- name = "ProtectedConfigError";
7816
- };
7817
- function checkRequireTest(config, options) {
7818
- if (options.requireTest && !config.isTest) {
7819
- throw new RequireTestError(config.name);
8281
+ if (destConn) {
8282
+ const [, closeErr] = await attempt(() => destConn.destroy());
8283
+ if (closeErr) cleanupErrors.push(closeErr);
7820
8284
  }
7821
- }
7822
- function checkProtectedConfig(config, operation, options) {
7823
- if (config.protected && !options.allowProtected) {
7824
- 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) });
7825
8291
  }
8292
+ return [result, err];
7826
8293
  }
7827
8294
 
7828
- // src/sdk/context.ts
7829
- var Context = class {
7830
- #connection = null;
7831
- #config;
7832
- #settings;
7833
- #identity;
7834
- #options;
7835
- #projectRoot;
7836
- #changeManager = null;
7837
- constructor(config, settings, identity, options, projectRoot) {
7838
- this.#config = config;
7839
- this.#settings = settings;
7840
- this.#identity = identity;
7841
- this.#options = options;
7842
- this.#projectRoot = projectRoot;
7843
- }
7844
- // ─────────────────────────────────────────────────────────
7845
- // Read-only Properties
7846
- // ─────────────────────────────────────────────────────────
7847
- get config() {
7848
- return this.#config;
7849
- }
7850
- get settings() {
7851
- 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";
7852
8307
  }
7853
- get identity() {
7854
- return this.#identity;
8308
+ return h;
8309
+ }
8310
+ function isSameServer(source, dest) {
8311
+ if (source.dialect !== dest.dialect) {
8312
+ return false;
7855
8313
  }
7856
- get dialect() {
7857
- return this.#config.connection.dialect;
8314
+ if (source.dialect === "sqlite") {
8315
+ return false;
7858
8316
  }
7859
- get connected() {
7860
- 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;
7861
8323
  }
7862
- get observer() {
7863
- return observer;
8324
+ if (source.dialect === "postgres") {
8325
+ return source.database === dest.database;
7864
8326
  }
7865
- get kysely() {
7866
- if (!this.#connection) {
7867
- 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;
7868
8382
  }
7869
- return this.#connection.db;
7870
- }
7871
- // ─────────────────────────────────────────────────────────
7872
- // Lifecycle
7873
- // ─────────────────────────────────────────────────────────
7874
- async connect() {
7875
- if (this.#connection) return;
7876
- this.#connection = await createConnection(
7877
- this.#config.connection,
7878
- this.#config.name
7879
- );
7880
- }
7881
- async disconnect() {
7882
- if (!this.#connection) return;
7883
- await this.#connection.destroy();
7884
- this.#connection = null;
7885
- this.#changeManager = null;
7886
- }
7887
- // ─────────────────────────────────────────────────────────
7888
- // SQL Execution
7889
- // ─────────────────────────────────────────────────────────
7890
- async query(sqlStr, _params) {
7891
- const db = this.kysely;
7892
- const result = await sql.raw(sqlStr).execute(db);
7893
- return result.rows ?? [];
7894
- }
7895
- async execute(sqlStr, _params) {
7896
- const db = this.kysely;
7897
- const result = await sql.raw(sqlStr).execute(db);
7898
- return {
7899
- rowsAffected: result.numAffectedRows ? Number(result.numAffectedRows) : void 0
7900
- };
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);
7901
8396
  }
7902
- async transaction(fn) {
7903
- const db = this.kysely;
7904
- return db.transaction().execute(async (trx) => {
7905
- const tx = {
7906
- async query(s, _p) {
7907
- const r = await sql.raw(s).execute(trx);
7908
- return r.rows ?? [];
7909
- },
7910
- async execute(s, _p) {
7911
- const r = await sql.raw(s).execute(trx);
7912
- return {
7913
- rowsAffected: r.numAffectedRows ? Number(r.numAffectedRows) : void 0
7914
- };
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}`;
7915
8430
  }
7916
- };
7917
- return fn(tx);
7918
- });
7919
- }
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);
8448
+ }
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
+ // Text types (large variable-length text with smart compression)
8595
+ { pattern: /^text$/i, universalType: "text", native: true },
8596
+ // String types (short text-like types)
8597
+ { pattern: /^varchar/i, universalType: "string", native: true },
8598
+ { pattern: /^character varying/i, universalType: "string", native: true },
8599
+ { pattern: /^char/i, universalType: "string", native: true },
8600
+ { pattern: /^character\b/i, universalType: "string", native: true },
8601
+ { pattern: /^citext$/i, universalType: "string", native: true },
8602
+ { pattern: /^name$/i, universalType: "string", native: true },
8603
+ // Everything else → custom
8604
+ { pattern: /.*/, universalType: "custom", native: false }
8605
+ ];
8606
+ var UNIVERSAL_TO_POSTGRES = {
8607
+ string: "text",
8608
+ text: "text",
8609
+ int: "integer",
8610
+ bigint: "bigint",
8611
+ float: "double precision",
8612
+ decimal: "numeric",
8613
+ bool: "boolean",
8614
+ timestamp: "timestamptz",
8615
+ date: "date",
8616
+ uuid: "uuid",
8617
+ json: "jsonb",
8618
+ binary: "bytea",
8619
+ vector: "vector",
8620
+ array: "text[]",
8621
+ custom: "text"
8622
+ };
8623
+
8624
+ // src/core/dt/dialects/mysql.ts
8625
+ var MYSQL_TO_UNIVERSAL = [
8626
+ // Boolean (must come before generic TINYINT)
8627
+ { pattern: /^tinyint\(1\)$/i, universalType: "bool", native: true },
8628
+ { pattern: /^boolean$/i, universalType: "bool", native: true },
8629
+ { pattern: /^bool$/i, universalType: "bool", native: true },
8630
+ // Integer types
8631
+ { pattern: /^tinyint/i, universalType: "int", native: true },
8632
+ { pattern: /^smallint/i, universalType: "int", native: true },
8633
+ { pattern: /^mediumint/i, universalType: "int", native: true },
8634
+ { pattern: /^int\b/i, universalType: "int", native: true },
8635
+ { pattern: /^integer/i, universalType: "int", native: true },
8636
+ { pattern: /^year$/i, universalType: "int", native: true },
8637
+ // Bigint
8638
+ { pattern: /^bigint/i, universalType: "bigint", native: true },
8639
+ // Float
8640
+ { pattern: /^float/i, universalType: "float", native: true },
8641
+ { pattern: /^double/i, universalType: "float", native: true },
8642
+ // Decimal
8643
+ { pattern: /^decimal/i, universalType: "decimal", native: true },
8644
+ { pattern: /^numeric/i, universalType: "decimal", native: true },
8645
+ // Date/time
8646
+ { pattern: /^datetime/i, universalType: "timestamp", native: true },
8647
+ { pattern: /^timestamp/i, universalType: "timestamp", native: true },
8648
+ { pattern: /^date$/i, universalType: "date", native: true },
8649
+ { pattern: /^time/i, universalType: "string", native: true },
8650
+ // JSON
8651
+ { pattern: /^json$/i, universalType: "json", native: true },
8652
+ // Vector (MySQL 9.0+)
8653
+ { pattern: /^vector/i, universalType: "vector", native: true },
8654
+ // Binary types
8655
+ { pattern: /^bit/i, universalType: "binary", native: true },
8656
+ { pattern: /^binary/i, universalType: "binary", native: true },
8657
+ { pattern: /^varbinary/i, universalType: "binary", native: true },
8658
+ { pattern: /^tinyblob$/i, universalType: "binary", native: true },
8659
+ { pattern: /^blob$/i, universalType: "binary", native: true },
8660
+ { pattern: /^mediumblob$/i, universalType: "binary", native: true },
8661
+ { pattern: /^longblob$/i, universalType: "binary", native: true },
8662
+ // Enum/Set → custom
8663
+ { pattern: /^enum/i, universalType: "custom", native: true },
8664
+ { pattern: /^set/i, universalType: "custom", native: true },
8665
+ // Text types (large variable-length text with smart compression)
8666
+ { pattern: /^text$/i, universalType: "text", native: true },
8667
+ { pattern: /^mediumtext$/i, universalType: "text", native: true },
8668
+ { pattern: /^longtext$/i, universalType: "text", native: true },
8669
+ // String types (short text-like types)
8670
+ { pattern: /^char/i, universalType: "string", native: true },
8671
+ { pattern: /^varchar/i, universalType: "string", native: true },
8672
+ { pattern: /^tinytext$/i, universalType: "string", native: true },
8673
+ // Everything else → custom
8674
+ { pattern: /.*/, universalType: "custom", native: false }
8675
+ ];
8676
+ function getUniversalToMysql(universalType, version) {
8677
+ const major = version?.major ?? 8;
8678
+ switch (universalType) {
8679
+ case "string":
8680
+ return "varchar(255)";
8681
+ case "text":
8682
+ return "longtext";
8683
+ case "int":
8684
+ return "int";
8685
+ case "bigint":
8686
+ return "bigint";
8687
+ case "float":
8688
+ return "double";
8689
+ case "decimal":
8690
+ return "decimal(38,10)";
8691
+ case "bool":
8692
+ return "tinyint(1)";
8693
+ case "timestamp":
8694
+ return "datetime(6)";
8695
+ case "date":
8696
+ return "date";
8697
+ case "uuid":
8698
+ return "char(36)";
8699
+ case "json":
8700
+ return "json";
8701
+ case "binary":
8702
+ return "longblob";
8703
+ case "vector":
8704
+ return major >= 9 ? "vector(2048)" : "json";
8705
+ case "array":
8706
+ return "json";
8707
+ case "custom":
8708
+ return "text";
8709
+ default:
8710
+ return "text";
8711
+ }
8712
+ }
8713
+
8714
+ // src/core/dt/dialects/mssql.ts
8715
+ var MSSQL_TO_UNIVERSAL = [
8716
+ // Boolean
8717
+ { pattern: /^bit$/i, universalType: "bool", native: true },
8718
+ // Integer types
8719
+ { pattern: /^tinyint$/i, universalType: "int", native: true },
8720
+ { pattern: /^smallint$/i, universalType: "int", native: true },
8721
+ { pattern: /^int$/i, universalType: "int", native: true },
8722
+ // Bigint
8723
+ { pattern: /^bigint$/i, universalType: "bigint", native: true },
8724
+ // Float
8725
+ { pattern: /^float/i, universalType: "float", native: true },
8726
+ { pattern: /^real$/i, universalType: "float", native: true },
8727
+ // Decimal
8728
+ { pattern: /^decimal/i, universalType: "decimal", native: true },
8729
+ { pattern: /^numeric/i, universalType: "decimal", native: true },
8730
+ { pattern: /^money$/i, universalType: "decimal", native: true },
8731
+ { pattern: /^smallmoney$/i, universalType: "decimal", native: true },
8732
+ // UUID
8733
+ { pattern: /^uniqueidentifier$/i, universalType: "uuid", native: true },
8734
+ // Date/time
8735
+ { pattern: /^datetime2/i, universalType: "timestamp", native: true },
8736
+ { pattern: /^datetimeoffset/i, universalType: "timestamp", native: true },
8737
+ { pattern: /^datetime$/i, universalType: "timestamp", native: true },
8738
+ { pattern: /^smalldatetime$/i, universalType: "timestamp", native: true },
8739
+ { pattern: /^date$/i, universalType: "date", native: true },
8740
+ { pattern: /^time/i, universalType: "string", native: true },
8741
+ // Native JSON (SQL Server 2025+)
8742
+ { pattern: /^json$/i, universalType: "json", native: true },
8743
+ // Native VECTOR (SQL Server 2025+)
8744
+ { pattern: /^vector/i, universalType: "vector", native: true },
8745
+ // Binary types
8746
+ { pattern: /^binary/i, universalType: "binary", native: true },
8747
+ { pattern: /^varbinary/i, universalType: "binary", native: true },
8748
+ { pattern: /^image$/i, universalType: "binary", native: true },
8749
+ { pattern: /^rowversion$/i, universalType: "binary", native: true },
8750
+ { pattern: /^timestamp$/i, universalType: "binary", native: true },
8751
+ // XML → custom
8752
+ { pattern: /^xml$/i, universalType: "custom", native: true },
8753
+ { pattern: /^sql_variant$/i, universalType: "custom", native: true },
8754
+ { pattern: /^hierarchyid$/i, universalType: "custom", native: true },
8755
+ // Text types (large variable-length text with smart compression)
8756
+ { pattern: /^nvarchar\(max\)$/i, universalType: "text", native: true },
8757
+ { pattern: /^varchar\(max\)$/i, universalType: "text", native: true },
8758
+ { pattern: /^ntext$/i, universalType: "text", native: true },
8759
+ { pattern: /^text$/i, universalType: "text", native: true },
8760
+ // String types (short text-like types)
8761
+ { pattern: /^nvarchar/i, universalType: "string", native: true },
8762
+ { pattern: /^varchar/i, universalType: "string", native: true },
8763
+ { pattern: /^nchar/i, universalType: "string", native: true },
8764
+ { pattern: /^char/i, universalType: "string", native: true },
8765
+ // Everything else → custom
8766
+ { pattern: /.*/, universalType: "custom", native: false }
8767
+ ];
8768
+ function getUniversalToMssql(universalType, version) {
8769
+ const major = version?.major ?? 2022;
8770
+ switch (universalType) {
8771
+ case "string":
8772
+ return "nvarchar(255)";
8773
+ case "text":
8774
+ return "nvarchar(max)";
8775
+ case "int":
8776
+ return "int";
8777
+ case "bigint":
8778
+ return "bigint";
8779
+ case "float":
8780
+ return "float";
8781
+ case "decimal":
8782
+ return "decimal(38,10)";
8783
+ case "bool":
8784
+ return "bit";
8785
+ case "timestamp":
8786
+ return "datetime2(7)";
8787
+ case "date":
8788
+ return "date";
8789
+ case "uuid":
8790
+ return "uniqueidentifier";
8791
+ case "json":
8792
+ return major >= 2025 ? "json" : "nvarchar(max)";
8793
+ case "binary":
8794
+ return "varbinary(max)";
8795
+ case "vector":
8796
+ return major >= 2025 ? "vector(1998)" : "nvarchar(max)";
8797
+ case "array":
8798
+ return "nvarchar(max)";
8799
+ case "custom":
8800
+ return "nvarchar(max)";
8801
+ default:
8802
+ return "nvarchar(max)";
8803
+ }
8804
+ }
8805
+
8806
+ // src/core/dt/dialects/index.ts
8807
+ function getDialectPatterns(dialect) {
8808
+ switch (dialect) {
8809
+ case "postgres":
8810
+ return POSTGRES_TO_UNIVERSAL;
8811
+ case "mysql":
8812
+ return MYSQL_TO_UNIVERSAL;
8813
+ case "mssql":
8814
+ return MSSQL_TO_UNIVERSAL;
8815
+ default:
8816
+ return [];
8817
+ }
8818
+ }
8819
+ function getDialectTargetType(universalType, dialect, version) {
8820
+ switch (dialect) {
8821
+ case "postgres":
8822
+ return UNIVERSAL_TO_POSTGRES[universalType] ?? "text";
8823
+ case "mysql":
8824
+ return getUniversalToMysql(universalType, version);
8825
+ case "mssql":
8826
+ return getUniversalToMssql(universalType, version);
8827
+ default:
8828
+ return "text";
8829
+ }
8830
+ }
8831
+
8832
+ // src/core/dt/type-map.ts
8833
+ function toUniversalType(options) {
8834
+ const { dbType, dialect } = options;
8835
+ const patterns = getDialectPatterns(dialect);
8836
+ for (const entry of patterns) {
8837
+ if (entry.pattern.test(dbType)) {
8838
+ return {
8839
+ universalType: entry.universalType,
8840
+ native: entry.native
8841
+ };
8842
+ }
8843
+ }
8844
+ return { universalType: "custom", native: false };
8845
+ }
8846
+ function toDialectType(options) {
8847
+ const { universalType, dialect, version } = options;
8848
+ return getDialectTargetType(universalType, dialect, version);
8849
+ }
8850
+ function isEncodedType(type) {
8851
+ return ENCODED_TYPES[type] === true;
8852
+ }
8853
+ async function queryDatabaseVersion(options) {
8854
+ const { db, dialect } = options;
8855
+ switch (dialect) {
8856
+ case "postgres":
8857
+ return queryPostgresVersion(db);
8858
+ case "mysql":
8859
+ return queryMysqlVersion(db);
8860
+ case "mssql":
8861
+ return queryMssqlVersion(db);
8862
+ default:
8863
+ return [null, new Error(`Unsupported dialect for version detection: ${dialect}`)];
8864
+ }
8865
+ }
8866
+ async function queryPostgresVersion(db) {
8867
+ const [result, err] = await attempt(
8868
+ () => sql`SELECT version() as version`.execute(db)
8869
+ );
8870
+ if (err) {
8871
+ return [null, err];
8872
+ }
8873
+ const raw = result.rows[0]?.version ?? "";
8874
+ const match = raw.match(/PostgreSQL\s+(\d+)\.(\d+)/i);
8875
+ if (!match) {
8876
+ return [null, new Error(`Cannot parse PostgreSQL version from: ${raw}`)];
8877
+ }
8878
+ return [{
8879
+ dialect: "postgres",
8880
+ major: parseInt(match[1], 10),
8881
+ minor: parseInt(match[2], 10),
8882
+ raw
8883
+ }, null];
8884
+ }
8885
+ async function queryMysqlVersion(db) {
8886
+ const [result, err] = await attempt(
8887
+ () => sql`SELECT version() as version`.execute(db)
8888
+ );
8889
+ if (err) {
8890
+ return [null, err];
8891
+ }
8892
+ const raw = result.rows[0]?.version ?? "";
8893
+ const match = raw.match(/^(\d+)\.(\d+)/);
8894
+ if (!match) {
8895
+ return [null, new Error(`Cannot parse MySQL version from: ${raw}`)];
8896
+ }
8897
+ return [{
8898
+ dialect: "mysql",
8899
+ major: parseInt(match[1], 10),
8900
+ minor: parseInt(match[2], 10),
8901
+ raw
8902
+ }, null];
8903
+ }
8904
+ async function queryMssqlVersion(db) {
8905
+ const [result, err] = await attempt(
8906
+ () => sql`
8907
+ SELECT
8908
+ CAST(SERVERPROPERTY('ProductMajorVersion') AS VARCHAR(10)) as major,
8909
+ CAST(SERVERPROPERTY('ProductMinorVersion') AS VARCHAR(10)) as minor,
8910
+ CAST(SERVERPROPERTY('ProductVersion') AS VARCHAR(50)) as full_version
8911
+ `.execute(db)
8912
+ );
8913
+ if (err) {
8914
+ return [null, err];
8915
+ }
8916
+ const row = result.rows[0];
8917
+ if (!row) {
8918
+ return [null, new Error("No version info returned from MSSQL")];
8919
+ }
8920
+ const internalMajor = parseInt(row.major, 10);
8921
+ const internalMinor = parseInt(row.minor, 10);
8922
+ const marketingYear = mssqlInternalToYear(internalMajor);
8923
+ return [{
8924
+ dialect: "mssql",
8925
+ major: marketingYear,
8926
+ minor: internalMinor,
8927
+ raw: row.full_version
8928
+ }, null];
8929
+ }
8930
+ function mssqlInternalToYear(internalMajor) {
8931
+ const mapping = {
8932
+ 11: 2012,
8933
+ 12: 2014,
8934
+ 13: 2016,
8935
+ 14: 2017,
8936
+ 15: 2019,
8937
+ 16: 2022,
8938
+ 17: 2025
8939
+ };
8940
+ return mapping[internalMajor] ?? internalMajor;
8941
+ }
8942
+
8943
+ // src/core/dt/schema.ts
8944
+ async function buildDtSchema(options) {
8945
+ const { db, dialect, tableName, schema } = options;
8946
+ const kyselyDb = db;
8947
+ let version = options.version;
8948
+ if (!version) {
8949
+ const [detected] = await queryDatabaseVersion({ db: kyselyDb, dialect });
8950
+ version = detected ?? void 0;
8951
+ }
8952
+ const [columns, err] = await queryColumns(kyselyDb, dialect, tableName, schema);
8953
+ if (err) {
8954
+ return [null, err];
8955
+ }
8956
+ const dtColumns = columns.map((col) => {
8957
+ const mapping = toUniversalType({
8958
+ dbType: col.dataType,
8959
+ dialect});
8960
+ const dtCol = {
8961
+ name: col.name,
8962
+ type: mapping.universalType
8963
+ };
8964
+ if (col.dataType.toLowerCase() !== mapping.universalType) {
8965
+ dtCol.sourceType = col.dataType;
8966
+ }
8967
+ if (!col.nullable) {
8968
+ dtCol.nullable = false;
8969
+ }
8970
+ return dtCol;
8971
+ });
8972
+ const dtSchema = {
8973
+ v: FORMAT_VERSION,
8974
+ d: dialect === "postgres" ? "postgresql" : dialect,
8975
+ dv: version ? `${version.major}.${version.minor}` : "unknown",
8976
+ t: tableName,
8977
+ columns: dtColumns
8978
+ };
8979
+ return [dtSchema, null];
8980
+ }
8981
+ async function validateSchema(options) {
8982
+ const { dtSchema, targetDb, targetDialect, targetVersion } = options;
8983
+ const kyselyDb = targetDb;
8984
+ const tableName = dtSchema.t;
8985
+ const errors = [];
8986
+ const warnings = [];
8987
+ if (!tableName) {
8988
+ errors.push("Schema has no table name (t field)");
8989
+ return [{ valid: false, errors, warnings }, null];
8990
+ }
8991
+ const [targetColumns, queryErr] = await queryColumns(kyselyDb, targetDialect, tableName);
8992
+ if (queryErr) {
8993
+ errors.push(`Target table "${tableName}" not found or inaccessible: ${queryErr.message}`);
8994
+ return [{ valid: false, errors, warnings }, null];
8995
+ }
8996
+ if (targetColumns.length === 0) {
8997
+ errors.push(`Target table "${tableName}" has no columns or does not exist`);
8998
+ return [{ valid: false, errors, warnings }, null];
8999
+ }
9000
+ const targetByName = {};
9001
+ for (const col of targetColumns) {
9002
+ targetByName[col.name] = col;
9003
+ }
9004
+ for (const dtCol of dtSchema.columns) {
9005
+ const targetCol = targetByName[dtCol.name];
9006
+ if (!targetCol) {
9007
+ errors.push(`Column "${dtCol.name}" exists in .dt but not in target table`);
9008
+ continue;
9009
+ }
9010
+ const targetMapping = toUniversalType({
9011
+ dbType: targetCol.dataType,
9012
+ dialect: targetDialect
9013
+ });
9014
+ if (targetMapping.universalType !== dtCol.type) {
9015
+ const targetTypeStr = toDialectType({
9016
+ universalType: dtCol.type,
9017
+ dialect: targetDialect,
9018
+ version: targetVersion
9019
+ });
9020
+ warnings.push(
9021
+ `Column "${dtCol.name}": source type "${dtCol.type}" maps to "${targetTypeStr}" in target, but target column is "${targetCol.dataType}" (${targetMapping.universalType})`
9022
+ );
9023
+ }
9024
+ }
9025
+ return [{
9026
+ valid: errors.length === 0,
9027
+ errors,
9028
+ warnings
9029
+ }, null];
9030
+ }
9031
+ async function queryColumns(db, dialect, tableName, schema) {
9032
+ switch (dialect) {
9033
+ case "postgres":
9034
+ return queryPostgresColumns(db, tableName, schema ?? "public");
9035
+ case "mysql":
9036
+ return queryMysqlColumns(db, tableName);
9037
+ case "mssql":
9038
+ return queryMssqlColumns(db, tableName);
9039
+ default:
9040
+ return [[], new Error(`Unsupported dialect: ${dialect}`)];
9041
+ }
9042
+ }
9043
+ async function queryPostgresColumns(db, tableName, schema) {
9044
+ const [result, err] = await attempt(
9045
+ () => sql`
9046
+ SELECT column_name, data_type, udt_name, is_nullable
9047
+ FROM information_schema.columns
9048
+ WHERE table_schema = ${schema}
9049
+ AND table_name = ${tableName}
9050
+ ORDER BY ordinal_position
9051
+ `.execute(db)
9052
+ );
9053
+ if (err) {
9054
+ return [[], err];
9055
+ }
9056
+ return [result.rows.map((r) => ({
9057
+ name: r.column_name,
9058
+ // Use udt_name for more specific types (e.g., 'jsonb' vs 'USER-DEFINED')
9059
+ dataType: r.data_type === "USER-DEFINED" ? r.udt_name : r.data_type,
9060
+ nullable: r.is_nullable === "YES"
9061
+ })), null];
9062
+ }
9063
+ async function queryMysqlColumns(db, tableName) {
9064
+ const [result, err] = await attempt(
9065
+ () => sql`
9066
+ SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE
9067
+ FROM INFORMATION_SCHEMA.COLUMNS
9068
+ WHERE TABLE_SCHEMA = DATABASE()
9069
+ AND TABLE_NAME = ${tableName}
9070
+ ORDER BY ORDINAL_POSITION
9071
+ `.execute(db)
9072
+ );
9073
+ if (err) {
9074
+ return [[], err];
9075
+ }
9076
+ return [result.rows.map((r) => ({
9077
+ name: r.COLUMN_NAME,
9078
+ // Use COLUMN_TYPE for precision (e.g., 'tinyint(1)' for bool)
9079
+ dataType: r.COLUMN_TYPE ?? r.DATA_TYPE,
9080
+ nullable: r.IS_NULLABLE === "YES"
9081
+ })), null];
9082
+ }
9083
+ async function queryMssqlColumns(db, tableName) {
9084
+ const [result, err] = await attempt(
9085
+ () => sql`
9086
+ SELECT
9087
+ c.name,
9088
+ CASE
9089
+ WHEN TYPE_NAME(c.user_type_id) IN ('varchar', 'nvarchar', 'varbinary')
9090
+ AND c.max_length = -1
9091
+ THEN TYPE_NAME(c.user_type_id) + '(max)'
9092
+ ELSE TYPE_NAME(c.user_type_id)
9093
+ END as type_name,
9094
+ c.is_nullable
9095
+ FROM sys.columns c
9096
+ JOIN sys.tables t ON c.object_id = t.object_id
9097
+ WHERE t.name = ${tableName}
9098
+ ORDER BY c.column_id
9099
+ `.execute(db)
9100
+ );
9101
+ if (err) {
9102
+ return [[], err];
9103
+ }
9104
+ return [result.rows.map((r) => ({
9105
+ name: r.name,
9106
+ dataType: r.type_name,
9107
+ nullable: r.is_nullable === 1
9108
+ })), null];
9109
+ }
9110
+
9111
+ // src/core/transfer/planner.ts
9112
+ async function planTransfer(ctx, options = {}) {
9113
+ observer.emit("transfer:planning", {
9114
+ source: ctx.source.config.name,
9115
+ destination: ctx.destination.config.name
9116
+ });
9117
+ const { dialect } = ctx.source;
9118
+ if (!isTransferSupported(dialect)) {
9119
+ return [null, new Error(`Transfer not supported for dialect: ${dialect}`)];
9120
+ }
9121
+ const crossDialect = ctx.source.dialect !== ctx.destination.dialect;
9122
+ if (!isTransferSupported(ctx.destination.dialect)) {
9123
+ return [null, new Error(`Transfer not supported for dialect: ${ctx.destination.dialect}`)];
9124
+ }
9125
+ const warnings = [];
9126
+ if (crossDialect) {
9127
+ warnings.push(`Cross-dialect transfer: ${ctx.source.dialect} \u2192 ${ctx.destination.dialect}. Type conversion will be applied.`);
9128
+ }
9129
+ const [allTables, tablesErr] = await listUserTables(ctx.source.db, dialect, options);
9130
+ if (tablesErr) {
9131
+ return [null, tablesErr];
9132
+ }
9133
+ const [fkRelations, fkErr] = await getForeignKeyRelations(ctx.source.db, dialect);
9134
+ if (fkErr) {
9135
+ return [null, fkErr];
9136
+ }
9137
+ const dependencyMap = buildDependencyMap(allTables, fkRelations);
9138
+ const [sortedNames, sortErr] = topologicalSort(allTables.map((t) => t.name), dependencyMap);
9139
+ if (sortErr) {
9140
+ warnings.push(`Circular FK dependency detected: ${sortErr.message}. Using original order.`);
9141
+ }
9142
+ const tablesByName = {};
9143
+ for (const t of allTables) {
9144
+ tablesByName[t.name] = t;
9145
+ }
9146
+ const orderedNames = sortErr ? allTables.map((t) => t.name) : sortedNames;
9147
+ const tablePlans = [];
9148
+ for (const name of orderedNames) {
9149
+ const meta = tablesByName[name];
9150
+ if (!meta) continue;
9151
+ tablePlans.push({
9152
+ name: meta.name,
9153
+ schema: meta.schema,
9154
+ rowCount: meta.rowCount,
9155
+ hasIdentity: meta.hasIdentity,
9156
+ identityColumn: meta.identityColumn,
9157
+ primaryKey: meta.primaryKey,
9158
+ columns: meta.columns,
9159
+ dependsOn: dependencyMap.get(name) ?? []
9160
+ });
9161
+ }
9162
+ const [destTables, destErr] = await listUserTables(ctx.destination.db, dialect, { tables: options.tables });
9163
+ if (destErr) {
9164
+ return [null, new Error(`Failed to read destination schema: ${destErr.message}`)];
9165
+ }
9166
+ const destTableNames = new Set(destTables.map((t) => t.name));
9167
+ for (const plan2 of tablePlans) {
9168
+ if (!destTableNames.has(plan2.name)) {
9169
+ warnings.push(`Table "${plan2.name}" exists in source but not destination`);
9170
+ }
9171
+ }
9172
+ if (crossDialect) {
9173
+ for (const tablePlan of tablePlans) {
9174
+ const [dtSchema] = await buildDtSchema({
9175
+ db: ctx.source.db,
9176
+ dialect: ctx.source.dialect,
9177
+ tableName: tablePlan.name,
9178
+ schema: tablePlan.schema
9179
+ });
9180
+ if (dtSchema) {
9181
+ tablePlan.columnTypes = dtSchema.columns;
9182
+ }
9183
+ }
9184
+ }
9185
+ const sameServer = crossDialect ? false : isSameServer(ctx.source.config.connection, ctx.destination.config.connection);
9186
+ const estimatedRows = tablePlans.reduce((sum, t) => sum + t.rowCount, 0);
9187
+ const plan = {
9188
+ tables: tablePlans,
9189
+ sameServer,
9190
+ estimatedRows,
9191
+ warnings,
9192
+ crossDialect,
9193
+ sourceDialect: ctx.source.dialect,
9194
+ destinationDialect: ctx.destination.dialect
9195
+ };
9196
+ observer.emit("transfer:plan:ready", {
9197
+ sameServer,
9198
+ tableCount: tablePlans.length,
9199
+ estimatedRows,
9200
+ warnings
9201
+ });
9202
+ return [plan, null];
9203
+ }
9204
+ async function listUserTables(db, dialect, options = {}) {
9205
+ const [tables, err] = await attempt(() => queryTables(db, dialect));
9206
+ if (err) {
9207
+ return [[], err];
9208
+ }
9209
+ let filtered = tables.filter((t) => !t.name.startsWith("__noorm_"));
9210
+ if (options.tables && options.tables.length > 0) {
9211
+ const requested = new Set(options.tables);
9212
+ filtered = filtered.filter((t) => requested.has(t.name));
9213
+ }
9214
+ return [filtered, null];
9215
+ }
9216
+ async function queryTables(db, dialect) {
9217
+ switch (dialect) {
9218
+ case "postgres":
9219
+ return queryPostgresTables(db);
9220
+ case "mysql":
9221
+ return queryMysqlTables(db);
9222
+ case "mssql":
9223
+ return queryMssqlTables(db);
9224
+ default:
9225
+ return [];
9226
+ }
9227
+ }
9228
+ async function queryPostgresTables(db) {
9229
+ const result = await sql`
9230
+ SELECT
9231
+ t.table_name,
9232
+ t.table_schema,
9233
+ COALESCE(c.reltuples::bigint::text, '0') as row_estimate,
9234
+ (
9235
+ SELECT string_agg(column_name, ',' ORDER BY ordinal_position)
9236
+ FROM information_schema.columns col
9237
+ WHERE col.table_schema = t.table_schema
9238
+ AND col.table_name = t.table_name
9239
+ ) as column_names,
9240
+ (
9241
+ SELECT column_name
9242
+ FROM information_schema.columns col
9243
+ WHERE col.table_schema = t.table_schema
9244
+ AND col.table_name = t.table_name
9245
+ AND (col.column_default LIKE 'nextval%' OR col.is_identity = 'YES')
9246
+ LIMIT 1
9247
+ ) as identity_column,
9248
+ (
9249
+ SELECT string_agg(kcu.column_name, ',' ORDER BY kcu.ordinal_position)
9250
+ FROM information_schema.table_constraints tc
9251
+ JOIN information_schema.key_column_usage kcu
9252
+ ON tc.constraint_name = kcu.constraint_name
9253
+ AND tc.table_schema = kcu.table_schema
9254
+ WHERE tc.constraint_type = 'PRIMARY KEY'
9255
+ AND tc.table_schema = t.table_schema
9256
+ AND tc.table_name = t.table_name
9257
+ ) as pk_columns
9258
+ FROM information_schema.tables t
9259
+ LEFT JOIN pg_class c ON c.relname = t.table_name
9260
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = t.table_schema
9261
+ WHERE t.table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
9262
+ AND t.table_type = 'BASE TABLE'
9263
+ ORDER BY t.table_schema, t.table_name
9264
+ `.execute(db);
9265
+ return result.rows.map((row) => ({
9266
+ name: row.table_name,
9267
+ schema: row.table_schema,
9268
+ rowCount: Math.max(0, parseInt(row.row_estimate, 10)),
9269
+ hasIdentity: row.identity_column !== null,
9270
+ identityColumn: row.identity_column ?? void 0,
9271
+ primaryKey: row.pk_columns ? row.pk_columns.split(",") : [],
9272
+ columns: row.column_names ? row.column_names.split(",") : []
9273
+ }));
9274
+ }
9275
+ async function queryMysqlTables(db) {
9276
+ const result = await sql`
9277
+ SELECT
9278
+ t.TABLE_NAME as table_name,
9279
+ t.TABLE_ROWS as row_estimate,
9280
+ (
9281
+ SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY ORDINAL_POSITION)
9282
+ FROM INFORMATION_SCHEMA.COLUMNS c
9283
+ WHERE c.TABLE_SCHEMA = t.TABLE_SCHEMA
9284
+ AND c.TABLE_NAME = t.TABLE_NAME
9285
+ ) as column_names,
9286
+ (
9287
+ SELECT COLUMN_NAME
9288
+ FROM INFORMATION_SCHEMA.COLUMNS c
9289
+ WHERE c.TABLE_SCHEMA = t.TABLE_SCHEMA
9290
+ AND c.TABLE_NAME = t.TABLE_NAME
9291
+ AND c.EXTRA LIKE '%auto_increment%'
9292
+ LIMIT 1
9293
+ ) as identity_column,
9294
+ (
9295
+ SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY ORDINAL_POSITION)
9296
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
9297
+ WHERE kcu.TABLE_SCHEMA = t.TABLE_SCHEMA
9298
+ AND kcu.TABLE_NAME = t.TABLE_NAME
9299
+ AND kcu.CONSTRAINT_NAME = 'PRIMARY'
9300
+ ) as pk_columns
9301
+ FROM INFORMATION_SCHEMA.TABLES t
9302
+ WHERE t.TABLE_SCHEMA = DATABASE()
9303
+ AND t.TABLE_TYPE = 'BASE TABLE'
9304
+ ORDER BY t.TABLE_NAME
9305
+ `.execute(db);
9306
+ return result.rows.map((row) => ({
9307
+ name: row.table_name,
9308
+ rowCount: Math.max(0, parseInt(row.row_estimate ?? "0", 10)),
9309
+ hasIdentity: row.identity_column !== null,
9310
+ identityColumn: row.identity_column ?? void 0,
9311
+ primaryKey: row.pk_columns ? row.pk_columns.split(",") : [],
9312
+ columns: row.column_names ? row.column_names.split(",") : []
9313
+ }));
9314
+ }
9315
+ async function queryMssqlTables(db) {
9316
+ const result = await sql`
9317
+ SELECT
9318
+ t.name as table_name,
9319
+ s.name as table_schema,
9320
+ COALESCE(SUM(p.rows), 0) as row_estimate,
9321
+ (
9322
+ SELECT STRING_AGG(c.name, ',') WITHIN GROUP (ORDER BY c.column_id)
9323
+ FROM sys.columns c
9324
+ WHERE c.object_id = t.object_id
9325
+ ) as column_names,
9326
+ (
9327
+ SELECT c.name
9328
+ FROM sys.columns c
9329
+ WHERE c.object_id = t.object_id
9330
+ AND c.is_identity = 1
9331
+ ) as identity_column,
9332
+ (
9333
+ SELECT STRING_AGG(c.name, ',') WITHIN GROUP (ORDER BY ic.key_ordinal)
9334
+ FROM sys.indexes i
9335
+ JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
9336
+ JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
9337
+ WHERE i.object_id = t.object_id
9338
+ AND i.is_primary_key = 1
9339
+ ) as pk_columns
9340
+ FROM sys.tables t
9341
+ JOIN sys.schemas s ON t.schema_id = s.schema_id
9342
+ LEFT JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
9343
+ WHERE t.is_ms_shipped = 0
9344
+ GROUP BY t.object_id, t.name, s.name
9345
+ ORDER BY s.name, t.name
9346
+ `.execute(db);
9347
+ return result.rows.map((row) => ({
9348
+ name: row.table_name,
9349
+ schema: row.table_schema,
9350
+ rowCount: Math.max(0, parseInt(row.row_estimate ?? "0", 10)),
9351
+ hasIdentity: row.identity_column !== null,
9352
+ identityColumn: row.identity_column ?? void 0,
9353
+ primaryKey: row.pk_columns ? row.pk_columns.split(",") : [],
9354
+ columns: row.column_names ? row.column_names.split(",") : []
9355
+ }));
9356
+ }
9357
+ async function getForeignKeyRelations(db, dialect) {
9358
+ let queryFn;
9359
+ switch (dialect) {
9360
+ case "postgres":
9361
+ queryFn = () => queryPostgresFKs(db);
9362
+ break;
9363
+ case "mysql":
9364
+ queryFn = () => queryMysqlFKs(db);
9365
+ break;
9366
+ case "mssql":
9367
+ queryFn = () => queryMssqlFKs(db);
9368
+ break;
9369
+ default:
9370
+ return [[], null];
9371
+ }
9372
+ const [relations, err] = await attempt(queryFn);
9373
+ if (err) {
9374
+ return [[], err];
9375
+ }
9376
+ return [relations, null];
9377
+ }
9378
+ async function queryPostgresFKs(db) {
9379
+ const result = await sql`
9380
+ SELECT DISTINCT
9381
+ tc.table_name as from_table,
9382
+ ccu.table_name as to_table
9383
+ FROM information_schema.table_constraints tc
9384
+ JOIN information_schema.constraint_column_usage ccu
9385
+ ON tc.constraint_name = ccu.constraint_name
9386
+ AND tc.table_schema = ccu.table_schema
9387
+ WHERE tc.constraint_type = 'FOREIGN KEY'
9388
+ AND tc.table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
9389
+ `.execute(db);
9390
+ return result.rows.map((r) => ({
9391
+ fromTable: r.from_table,
9392
+ toTable: r.to_table
9393
+ }));
9394
+ }
9395
+ async function queryMysqlFKs(db) {
9396
+ const result = await sql`
9397
+ SELECT DISTINCT
9398
+ TABLE_NAME as from_table,
9399
+ REFERENCED_TABLE_NAME as to_table
9400
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
9401
+ WHERE TABLE_SCHEMA = DATABASE()
9402
+ AND REFERENCED_TABLE_NAME IS NOT NULL
9403
+ `.execute(db);
9404
+ return result.rows.map((r) => ({
9405
+ fromTable: r.from_table,
9406
+ toTable: r.to_table
9407
+ }));
9408
+ }
9409
+ async function queryMssqlFKs(db) {
9410
+ const result = await sql`
9411
+ SELECT DISTINCT
9412
+ OBJECT_NAME(fk.parent_object_id) as from_table,
9413
+ OBJECT_NAME(fk.referenced_object_id) as to_table
9414
+ FROM sys.foreign_keys fk
9415
+ `.execute(db);
9416
+ return result.rows.map((r) => ({
9417
+ fromTable: r.from_table,
9418
+ toTable: r.to_table
9419
+ }));
9420
+ }
9421
+ function buildDependencyMap(tables, relations) {
9422
+ const tableNames = new Set(tables.map((t) => t.name));
9423
+ const deps = /* @__PURE__ */ new Map();
9424
+ for (const name of tableNames) {
9425
+ deps.set(name, []);
9426
+ }
9427
+ for (const rel of relations) {
9428
+ if (tableNames.has(rel.fromTable) && tableNames.has(rel.toTable)) {
9429
+ const current = deps.get(rel.fromTable) ?? [];
9430
+ if (!current.includes(rel.toTable) && rel.fromTable !== rel.toTable) {
9431
+ current.push(rel.toTable);
9432
+ deps.set(rel.fromTable, current);
9433
+ }
9434
+ }
9435
+ }
9436
+ return deps;
9437
+ }
9438
+ function topologicalSort(nodes, deps) {
9439
+ const result = [];
9440
+ const visited = /* @__PURE__ */ new Set();
9441
+ const visiting = /* @__PURE__ */ new Set();
9442
+ function visit(node) {
9443
+ if (visited.has(node)) return null;
9444
+ if (visiting.has(node)) {
9445
+ return new Error(`Circular dependency involving "${node}"`);
9446
+ }
9447
+ visiting.add(node);
9448
+ const nodeDeps = deps.get(node) ?? [];
9449
+ for (const dep of nodeDeps) {
9450
+ const err = visit(dep);
9451
+ if (err) return err;
9452
+ }
9453
+ visiting.delete(node);
9454
+ visited.add(node);
9455
+ result.push(node);
9456
+ return null;
9457
+ }
9458
+ for (const node of nodes) {
9459
+ const err = visit(node);
9460
+ if (err) return [[], err];
9461
+ }
9462
+ return [result, null];
9463
+ }
9464
+ var DEFAULT_BATCH_SIZE = 100;
9465
+ var DEFAULT_MAX_BATCH_BYTES = gigabytes(1);
9466
+ var DtStreamer = class {
9467
+ #sourceDialect;
9468
+ #sourceVersion;
9469
+ #targetDialect;
9470
+ #targetVersion;
9471
+ #columns;
9472
+ #batchSize;
9473
+ #maxBatchBytes;
9474
+ /**
9475
+ * Create a new DtStreamer for cross-dialect conversion.
9476
+ *
9477
+ * @param options - Source/target dialects, columns, and batch limits
9478
+ */
9479
+ constructor(options) {
9480
+ this.#sourceDialect = options.sourceDialect;
9481
+ this.#sourceVersion = options.sourceVersion;
9482
+ this.#targetDialect = options.targetDialect;
9483
+ this.#targetVersion = options.targetVersion;
9484
+ this.#columns = options.columns;
9485
+ this.#batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
9486
+ this.#maxBatchBytes = options.maxBatchBytes ?? DEFAULT_MAX_BATCH_BYTES;
9487
+ }
9488
+ /**
9489
+ * The soft batch size limit.
9490
+ */
9491
+ get batchSize() {
9492
+ return this.#batchSize;
9493
+ }
9494
+ /**
9495
+ * Convert a batch of source-dialect rows to target-dialect rows.
9496
+ *
9497
+ * Performs in-memory type conversion without any file I/O or encoding.
9498
+ *
9499
+ * @param rows - Source database rows
9500
+ * @returns Target-dialect rows ready for insertion
9501
+ */
9502
+ convertBatch(rows) {
9503
+ const result = [];
9504
+ for (const row of rows) {
9505
+ result.push(this.#convertRow(row));
9506
+ }
9507
+ return result;
9508
+ }
9509
+ /**
9510
+ * Check if accumulated rows should be flushed.
9511
+ *
9512
+ * Returns true when row count exceeds batchSize OR estimated memory
9513
+ * exceeds maxBatchBytes. Prevents OOM on tables with large values.
9514
+ *
9515
+ * @param rows - Currently accumulated rows
9516
+ * @returns True if the batch should be flushed
9517
+ */
9518
+ shouldFlush(rows) {
9519
+ if (rows.length >= this.#batchSize) {
9520
+ return true;
9521
+ }
9522
+ return estimateRowsBytes(rows) >= this.#maxBatchBytes;
9523
+ }
9524
+ /**
9525
+ * Convert a single row from source to target dialect.
9526
+ */
9527
+ #convertRow(row) {
9528
+ const result = {};
9529
+ for (const col of this.#columns) {
9530
+ const value = row[col.name];
9531
+ if (value === null || value === void 0) {
9532
+ result[col.name] = null;
9533
+ continue;
9534
+ }
9535
+ if (isEncodedType(col.type)) {
9536
+ result[col.name] = this.#convertEncodedValue(value, col.type);
9537
+ } else {
9538
+ result[col.name] = this.#convertSimpleValue(value, col.type);
9539
+ }
9540
+ }
9541
+ return result;
9542
+ }
9543
+ /**
9544
+ * Convert an encoded-type value between dialects.
9545
+ *
9546
+ * No serialization overhead: works directly with native objects.
9547
+ */
9548
+ #convertEncodedValue(value, type) {
9549
+ switch (type) {
9550
+ case "json":
9551
+ return this.#convertJson(value);
9552
+ case "text":
9553
+ return value;
9554
+ case "binary":
9555
+ return value;
9556
+ case "vector":
9557
+ return this.#convertVector(value);
9558
+ case "array":
9559
+ return this.#convertArray(value);
9560
+ case "custom":
9561
+ if (this.#targetDialect !== this.#sourceDialect && typeof value === "object") {
9562
+ return JSON.stringify(value);
9563
+ }
9564
+ return value;
9565
+ default:
9566
+ return value;
9567
+ }
9568
+ }
9569
+ /**
9570
+ * Convert JSON between dialects.
9571
+ */
9572
+ #convertJson(value) {
9573
+ if (this.#targetDialect === "mssql") {
9574
+ const major = this.#targetVersion?.major ?? 2022;
9575
+ if (major < 2025) {
9576
+ return typeof value === "string" ? value : JSON.stringify(value);
9577
+ }
9578
+ }
9579
+ if (this.#sourceDialect === "mssql" && typeof value === "string") {
9580
+ const major = this.#sourceVersion?.major ?? 2022;
9581
+ if (major < 2025) {
9582
+ return JSON.parse(value);
9583
+ }
9584
+ }
9585
+ return value;
9586
+ }
9587
+ /**
9588
+ * Convert vector between dialects.
9589
+ */
9590
+ #convertVector(value) {
9591
+ let arr;
9592
+ if (typeof value === "string") {
9593
+ const trimmed = value.replace(/^\[/, "").replace(/\]$/, "");
9594
+ arr = trimmed.split(",").map(Number);
9595
+ } else {
9596
+ arr = value;
9597
+ }
9598
+ if (this.#targetDialect === "postgres") {
9599
+ return Array.isArray(arr) ? `[${arr.join(",")}]` : String(value);
9600
+ }
9601
+ if (this.#targetDialect === "mysql") {
9602
+ const major = this.#targetVersion?.major ?? 8;
9603
+ if (major >= 9) {
9604
+ return Array.isArray(arr) ? `[${arr.join(",")}]` : String(value);
9605
+ }
9606
+ return Array.isArray(arr) ? JSON.stringify(arr) : String(value);
9607
+ }
9608
+ return Array.isArray(arr) ? JSON.stringify(arr) : String(value);
9609
+ }
9610
+ /**
9611
+ * Convert array between dialects.
9612
+ */
9613
+ #convertArray(value) {
9614
+ if (this.#targetDialect === "postgres") {
9615
+ if (typeof value === "string") return JSON.parse(value);
9616
+ return value;
9617
+ }
9618
+ if (Array.isArray(value)) return JSON.stringify(value);
9619
+ return value;
9620
+ }
9621
+ /**
9622
+ * Convert a simple-type value between dialects.
9623
+ */
9624
+ #convertSimpleValue(value, type) {
9625
+ switch (type) {
9626
+ case "bool":
9627
+ if (this.#targetDialect === "mssql" || this.#targetDialect === "mysql") {
9628
+ if (typeof value === "boolean") return value ? 1 : 0;
9629
+ } else if (this.#sourceDialect === "mssql" || this.#sourceDialect === "mysql") {
9630
+ if (typeof value === "number") return value !== 0;
9631
+ }
9632
+ return value;
9633
+ case "uuid":
9634
+ return value;
9635
+ default:
9636
+ return value;
9637
+ }
9638
+ }
9639
+ };
9640
+ function estimateRowsBytes(rows) {
9641
+ let total = 0;
9642
+ for (const row of rows) {
9643
+ for (const key in row) {
9644
+ const val = row[key];
9645
+ if (val === null || val === void 0) {
9646
+ total += 8;
9647
+ } else if (Buffer.isBuffer(val)) {
9648
+ total += val.length;
9649
+ } else if (typeof val === "string") {
9650
+ total += val.length * 2;
9651
+ } else if (typeof val === "object") {
9652
+ total += JSON.stringify(val).length * 2;
9653
+ } else {
9654
+ total += 8;
9655
+ }
9656
+ }
9657
+ }
9658
+ return total;
9659
+ }
9660
+
9661
+ // src/core/transfer/executor.ts
9662
+ var DEFAULT_BATCH_SIZE2 = 1e3;
9663
+ async function executeTransfer(ctx, plan, options = {}) {
9664
+ const startTime = Date.now();
9665
+ const { dialect: _srcDialect } = ctx.source;
9666
+ const destDialect = ctx.destination.dialect;
9667
+ const ops = getTransferOperations(destDialect);
9668
+ if (!ops) {
9669
+ return [null, new Error(`Unsupported dialect: ${destDialect}`)];
9670
+ }
9671
+ const tableResults = [];
9672
+ let totalRows = 0;
9673
+ let hasFailures = false;
9674
+ observer.emit("transfer:starting", {
9675
+ tableCount: plan.tables.length,
9676
+ sameServer: plan.sameServer
9677
+ });
9678
+ if (options.disableForeignKeys !== false) {
9679
+ const [, disableErr] = await attempt(
9680
+ () => ops.executeDisableFK(
9681
+ ctx.destination.db,
9682
+ plan.tables.map((t) => t.name)
9683
+ )
9684
+ );
9685
+ if (disableErr) {
9686
+ return [null, new Error(`Failed to disable FK checks: ${disableErr.message}`)];
9687
+ }
9688
+ }
9689
+ for (let i = 0; i < plan.tables.length; i++) {
9690
+ const tablePlan = plan.tables[i];
9691
+ observer.emit("transfer:table:before", {
9692
+ table: tablePlan.name,
9693
+ index: i,
9694
+ total: plan.tables.length,
9695
+ rowCount: tablePlan.rowCount
9696
+ });
9697
+ const tableStart = Date.now();
9698
+ let result2;
9699
+ const strategy = options.onConflict ?? "fail";
9700
+ const useSameServer = plan.sameServer && strategy === "fail" && !plan.crossDialect;
9701
+ const useCrossDialect = plan.crossDialect && tablePlan.columnTypes;
9702
+ let tableResult = null;
9703
+ let tableErr = null;
9704
+ if (useSameServer) {
9705
+ [tableResult, tableErr] = await transferTableSameServer(
9706
+ ctx,
9707
+ tablePlan,
9708
+ options,
9709
+ ops
9710
+ );
9711
+ } else if (useCrossDialect) {
9712
+ [tableResult, tableErr] = await transferTableCrossDialect(
9713
+ ctx,
9714
+ tablePlan,
9715
+ plan,
9716
+ options
9717
+ );
9718
+ } else {
9719
+ [tableResult, tableErr] = await transferTableCrossServer(
9720
+ ctx,
9721
+ tablePlan,
9722
+ options,
9723
+ ops
9724
+ );
9725
+ }
9726
+ if (tableErr) {
9727
+ result2 = {
9728
+ table: tablePlan.name,
9729
+ status: "failed",
9730
+ rowsTransferred: 0,
9731
+ rowsSkipped: 0,
9732
+ durationMs: Date.now() - tableStart,
9733
+ error: tableErr.message
9734
+ };
9735
+ hasFailures = true;
9736
+ } else {
9737
+ result2 = tableResult;
9738
+ }
9739
+ tableResults.push(result2);
9740
+ totalRows += result2.rowsTransferred;
9741
+ observer.emit("transfer:table:after", {
9742
+ table: tablePlan.name,
9743
+ status: result2.status,
9744
+ rowsTransferred: result2.rowsTransferred,
9745
+ rowsSkipped: result2.rowsSkipped,
9746
+ durationMs: result2.durationMs,
9747
+ error: result2.error
9748
+ });
9749
+ }
9750
+ if (options.disableForeignKeys !== false) {
9751
+ const [, enableErr] = await attempt(
9752
+ () => ops.executeEnableFK(
9753
+ ctx.destination.db,
9754
+ plan.tables.map((t) => t.name)
9755
+ )
9756
+ );
9757
+ if (enableErr) {
9758
+ observer.emit("error", {
9759
+ source: "transfer",
9760
+ error: enableErr,
9761
+ context: { phase: "enable-fk" }
9762
+ });
9763
+ }
9764
+ }
9765
+ const durationMs = Date.now() - startTime;
9766
+ const allSuccess = tableResults.every((r) => r.status === "success");
9767
+ const result = {
9768
+ status: hasFailures ? allSuccess ? "partial" : "failed" : "success",
9769
+ tables: tableResults,
9770
+ totalRows,
9771
+ durationMs
9772
+ };
9773
+ observer.emit("transfer:complete", {
9774
+ status: result.status,
9775
+ totalRows,
9776
+ tableCount: plan.tables.length,
9777
+ durationMs
9778
+ });
9779
+ return [result, null];
9780
+ }
9781
+ async function transferTableSameServer(ctx, plan, options, ops) {
9782
+ const startTime = Date.now();
9783
+ if (!ops) {
9784
+ return [null, new Error("No dialect operations")];
9785
+ }
9786
+ if (options.truncateFirst) {
9787
+ const [, truncateErr] = await truncateTable(
9788
+ ctx.destination.db,
9789
+ ctx.destination.dialect,
9790
+ plan.name
9791
+ );
9792
+ if (truncateErr) {
9793
+ return [null, new Error(`Failed to truncate: ${truncateErr.message}`)];
9794
+ }
9795
+ }
9796
+ if (options.preserveIdentity !== false && plan.hasIdentity) {
9797
+ const enableSql = ops.getEnableIdentityInsertSql(plan.name);
9798
+ if (enableSql) {
9799
+ const [, enableErr] = await attempt(
9800
+ () => sql.raw(enableSql).execute(ctx.destination.db)
9801
+ );
9802
+ if (enableErr) {
9803
+ return [null, new Error(`Failed to enable identity insert: ${enableErr.message}`)];
9804
+ }
9805
+ }
9806
+ }
9807
+ const transferSql = ops.buildDirectTransfer(
9808
+ ctx.source.config.connection.database,
9809
+ plan.name,
9810
+ plan.name,
9811
+ plan.columns,
9812
+ plan.schema,
9813
+ plan.schema
9814
+ );
9815
+ const [, transferErr] = await attempt(
9816
+ () => sql.raw(transferSql).execute(ctx.destination.db)
9817
+ );
9818
+ if (options.preserveIdentity !== false && plan.hasIdentity) {
9819
+ const disableSql = ops.getDisableIdentityInsertSql(plan.name);
9820
+ if (disableSql) {
9821
+ await attempt(() => sql.raw(disableSql).execute(ctx.destination.db));
9822
+ }
9823
+ if (plan.identityColumn) {
9824
+ const resetSql = ops.getResetSequenceSql(plan.name, plan.identityColumn, plan.schema);
9825
+ if (resetSql) {
9826
+ await attempt(() => sql.raw(resetSql).execute(ctx.destination.db));
9827
+ }
9828
+ }
9829
+ }
9830
+ if (transferErr) {
9831
+ return [null, new Error(`Transfer failed: ${transferErr.message}`)];
9832
+ }
9833
+ const rowsTransferred = plan.rowCount;
9834
+ observer.emit("transfer:table:progress", {
9835
+ table: plan.name,
9836
+ rowsTransferred,
9837
+ rowsTotal: plan.rowCount,
9838
+ rowsSkipped: 0
9839
+ });
9840
+ return [{
9841
+ table: plan.name,
9842
+ status: "success",
9843
+ rowsTransferred,
9844
+ rowsSkipped: 0,
9845
+ durationMs: Date.now() - startTime
9846
+ }, null];
9847
+ }
9848
+ async function transferTableCrossServer(ctx, plan, options, ops) {
9849
+ const startTime = Date.now();
9850
+ const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE2;
9851
+ const strategy = options.onConflict ?? "fail";
9852
+ if (!ops) {
9853
+ return [null, new Error("No dialect operations")];
9854
+ }
9855
+ if (options.truncateFirst) {
9856
+ const [, truncateErr] = await truncateTable(
9857
+ ctx.destination.db,
9858
+ ctx.destination.dialect,
9859
+ plan.name
9860
+ );
9861
+ if (truncateErr) {
9862
+ return [null, new Error(`Failed to truncate: ${truncateErr.message}`)];
9863
+ }
9864
+ }
9865
+ if (options.preserveIdentity !== false && plan.hasIdentity) {
9866
+ const enableSql = ops.getEnableIdentityInsertSql(plan.name);
9867
+ if (enableSql) {
9868
+ const [, enableErr] = await attempt(
9869
+ () => sql.raw(enableSql).execute(ctx.destination.db)
9870
+ );
9871
+ if (enableErr) {
9872
+ return [null, new Error(`Failed to enable identity insert: ${enableErr.message}`)];
9873
+ }
9874
+ }
9875
+ }
9876
+ let rowsTransferred = 0;
9877
+ let rowsSkipped = 0;
9878
+ let offset = 0;
9879
+ let transferError = null;
9880
+ while (true) {
9881
+ const [rows, fetchErr] = await attempt(
9882
+ () => fetchBatch(
9883
+ ctx.source.db,
9884
+ ctx.source.dialect,
9885
+ plan.name,
9886
+ plan.columns,
9887
+ batchSize,
9888
+ offset,
9889
+ plan.schema
9890
+ )
9891
+ );
9892
+ if (fetchErr) {
9893
+ transferError = new Error(`Failed to fetch batch: ${fetchErr.message}`);
9894
+ break;
9895
+ }
9896
+ if (rows.length === 0) {
9897
+ break;
9898
+ }
9899
+ const [batchResult, insertErr] = await insertBatch(
9900
+ ctx.destination.db,
9901
+ ctx.destination.dialect,
9902
+ plan.name,
9903
+ plan.columns,
9904
+ plan.primaryKey,
9905
+ rows,
9906
+ strategy,
9907
+ ops
9908
+ );
9909
+ if (insertErr) {
9910
+ transferError = new Error(`Failed to insert batch: ${insertErr.message}`);
9911
+ break;
9912
+ }
9913
+ rowsTransferred += batchResult.inserted;
9914
+ rowsSkipped += batchResult.skipped;
9915
+ offset += rows.length;
9916
+ observer.emit("transfer:table:progress", {
9917
+ table: plan.name,
9918
+ rowsTransferred,
9919
+ rowsTotal: plan.rowCount,
9920
+ rowsSkipped
9921
+ });
9922
+ if (rows.length < batchSize) {
9923
+ break;
9924
+ }
9925
+ }
9926
+ if (options.preserveIdentity !== false && plan.hasIdentity) {
9927
+ const disableSql = ops.getDisableIdentityInsertSql(plan.name);
9928
+ if (disableSql) {
9929
+ await attempt(() => sql.raw(disableSql).execute(ctx.destination.db));
9930
+ }
9931
+ if (plan.identityColumn) {
9932
+ const resetSql = ops.getResetSequenceSql(plan.name, plan.identityColumn, plan.schema);
9933
+ if (resetSql) {
9934
+ await attempt(() => sql.raw(resetSql).execute(ctx.destination.db));
9935
+ }
9936
+ }
9937
+ }
9938
+ if (transferError) {
9939
+ return [null, transferError];
9940
+ }
9941
+ return [{
9942
+ table: plan.name,
9943
+ status: "success",
9944
+ rowsTransferred,
9945
+ rowsSkipped,
9946
+ durationMs: Date.now() - startTime
9947
+ }, null];
9948
+ }
9949
+ async function transferTableCrossDialect(ctx, plan, transferPlan, options) {
9950
+ const startTime = Date.now();
9951
+ const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE2;
9952
+ const destOps = getTransferOperations(ctx.destination.dialect);
9953
+ if (!destOps || !plan.columnTypes) {
9954
+ return [null, new Error("Missing dialect operations or column types for cross-dialect transfer")];
9955
+ }
9956
+ const [srcVersion] = await queryDatabaseVersion({ db: ctx.source.db, dialect: ctx.source.dialect });
9957
+ const [dstVersion] = await queryDatabaseVersion({ db: ctx.destination.db, dialect: ctx.destination.dialect });
9958
+ const streamer = new DtStreamer({
9959
+ sourceDialect: ctx.source.dialect,
9960
+ sourceVersion: srcVersion ?? void 0,
9961
+ targetDialect: ctx.destination.dialect,
9962
+ targetVersion: dstVersion ?? void 0,
9963
+ columns: plan.columnTypes,
9964
+ batchSize
9965
+ });
9966
+ if (options.truncateFirst) {
9967
+ const [, truncateErr] = await truncateTable(
9968
+ ctx.destination.db,
9969
+ ctx.destination.dialect,
9970
+ plan.name
9971
+ );
9972
+ if (truncateErr) {
9973
+ return [null, new Error(`Failed to truncate: ${truncateErr.message}`)];
9974
+ }
9975
+ }
9976
+ if (options.preserveIdentity !== false && plan.hasIdentity) {
9977
+ const enableSql = destOps.getEnableIdentityInsertSql(plan.name);
9978
+ if (enableSql) {
9979
+ const [, enableErr] = await attempt(
9980
+ () => sql.raw(enableSql).execute(ctx.destination.db)
9981
+ );
9982
+ if (enableErr) {
9983
+ return [null, new Error(`Failed to enable identity insert: ${enableErr.message}`)];
9984
+ }
9985
+ }
9986
+ }
9987
+ let rowsTransferred = 0;
9988
+ let rowsSkipped = 0;
9989
+ let offset = 0;
9990
+ let transferError = null;
9991
+ observer.emit("dt:stream:start", {
9992
+ table: plan.name,
9993
+ sourceDialect: ctx.source.dialect,
9994
+ targetDialect: ctx.destination.dialect
9995
+ });
9996
+ while (true) {
9997
+ const [rows, fetchErr] = await attempt(
9998
+ () => fetchBatch(
9999
+ ctx.source.db,
10000
+ ctx.source.dialect,
10001
+ plan.name,
10002
+ plan.columns,
10003
+ batchSize,
10004
+ offset,
10005
+ plan.schema
10006
+ )
10007
+ );
10008
+ if (fetchErr) {
10009
+ transferError = new Error(`Failed to fetch batch: ${fetchErr.message}`);
10010
+ break;
10011
+ }
10012
+ if (rows.length === 0) break;
10013
+ const convertedRows = streamer.convertBatch(rows);
10014
+ for (const row of convertedRows) {
10015
+ const [, insertErr] = await attempt(
10016
+ () => ctx.destination.db.insertInto(plan.name).values(row).execute()
10017
+ );
10018
+ if (insertErr) {
10019
+ const lower = insertErr.message.toLowerCase();
10020
+ const isDuplicate = lower.includes("duplicate") || lower.includes("unique") || lower.includes("primary key");
10021
+ if (isDuplicate && options.onConflict === "skip") {
10022
+ rowsSkipped++;
10023
+ } else if (options.onConflict !== "fail") {
10024
+ rowsSkipped++;
10025
+ } else {
10026
+ transferError = new Error(`Failed to insert row: ${insertErr.message}`);
10027
+ break;
10028
+ }
10029
+ } else {
10030
+ rowsTransferred++;
10031
+ }
10032
+ }
10033
+ if (transferError) break;
10034
+ offset += rows.length;
10035
+ observer.emit("transfer:table:progress", {
10036
+ table: plan.name,
10037
+ rowsTransferred,
10038
+ rowsTotal: plan.rowCount,
10039
+ rowsSkipped
10040
+ });
10041
+ observer.emit("dt:stream:progress", {
10042
+ table: plan.name,
10043
+ rowsConverted: rowsTransferred + rowsSkipped
10044
+ });
10045
+ if (rows.length < batchSize) break;
10046
+ }
10047
+ if (options.preserveIdentity !== false && plan.hasIdentity) {
10048
+ const disableSql = destOps.getDisableIdentityInsertSql(plan.name);
10049
+ if (disableSql) {
10050
+ await attempt(() => sql.raw(disableSql).execute(ctx.destination.db));
10051
+ }
10052
+ }
10053
+ const durationMs = Date.now() - startTime;
10054
+ observer.emit("dt:stream:complete", {
10055
+ table: plan.name,
10056
+ rowsConverted: rowsTransferred + rowsSkipped,
10057
+ durationMs
10058
+ });
10059
+ if (transferError) {
10060
+ return [null, transferError];
10061
+ }
10062
+ return [{
10063
+ table: plan.name,
10064
+ status: "success",
10065
+ rowsTransferred,
10066
+ rowsSkipped,
10067
+ durationMs
10068
+ }, null];
10069
+ }
10070
+ async function fetchBatch(db, dialect, table, columns, limit, offset, _schema) {
10071
+ const quoteIdent4 = dialect === "mssql" ? (c) => `[${c}]` : dialect === "mysql" ? (c) => `\`${c}\`` : (c) => `"${c}"`;
10072
+ const columnList = columns.map(quoteIdent4).join(", ");
10073
+ if (dialect === "mssql") {
10074
+ const orderCol = quoteIdent4(columns[0]);
10075
+ const result2 = await sql`
10076
+ SELECT ${sql.raw(columnList)}
10077
+ FROM ${sql.table(table)}
10078
+ ORDER BY ${sql.raw(orderCol)}
10079
+ OFFSET ${offset} ROWS
10080
+ FETCH NEXT ${limit} ROWS ONLY
10081
+ `.execute(db);
10082
+ return result2.rows;
10083
+ }
10084
+ const result = await sql`
10085
+ SELECT ${sql.raw(columnList)}
10086
+ FROM ${sql.table(table)}
10087
+ LIMIT ${limit}
10088
+ OFFSET ${offset}
10089
+ `.execute(db);
10090
+ return result.rows;
10091
+ }
10092
+ async function insertBatch(db, dialect, table, columns, primaryKey, rows, strategy, ops) {
10093
+ if (rows.length === 0) {
10094
+ return [{ inserted: 0, skipped: 0 }, null];
10095
+ }
10096
+ if ((dialect === "mssql" || dialect === "mysql") && strategy !== "fail" && primaryKey.length > 0 && ops) {
10097
+ return insertBatchRawSql(db, dialect, table, columns, primaryKey, rows, strategy, ops);
10098
+ }
10099
+ let inserted = 0;
10100
+ let skipped = 0;
10101
+ for (const row of rows) {
10102
+ const query = db.insertInto(table).values(row);
10103
+ if (strategy === "skip" && primaryKey.length > 0) {
10104
+ const [, err] = await attempt(
10105
+ () => query.onConflict((oc) => oc.columns(primaryKey).doNothing()).execute()
10106
+ );
10107
+ if (err) {
10108
+ if (isDuplicateKeyError(err.message)) {
10109
+ skipped++;
10110
+ } else {
10111
+ return [{ inserted, skipped }, err];
10112
+ }
10113
+ } else {
10114
+ inserted++;
10115
+ }
10116
+ } else if (strategy === "update" && primaryKey.length > 0) {
10117
+ const updateSet = {};
10118
+ for (const col of columns) {
10119
+ if (!primaryKey.includes(col)) {
10120
+ updateSet[col] = row[col];
10121
+ }
10122
+ }
10123
+ const [, err] = await attempt(
10124
+ () => query.onConflict((oc) => oc.columns(primaryKey).doUpdateSet(updateSet)).execute()
10125
+ );
10126
+ if (err) {
10127
+ if (isDuplicateKeyError(err.message)) {
10128
+ skipped++;
10129
+ } else {
10130
+ return [{ inserted, skipped }, err];
10131
+ }
10132
+ } else {
10133
+ inserted++;
10134
+ }
10135
+ } else {
10136
+ const [, err] = await attempt(() => query.execute());
10137
+ if (err) {
10138
+ if (strategy === "fail") {
10139
+ return [{ inserted, skipped }, err];
10140
+ }
10141
+ skipped++;
10142
+ } else {
10143
+ inserted++;
10144
+ }
10145
+ }
10146
+ }
10147
+ return [{ inserted, skipped }, null];
10148
+ }
10149
+ async function insertBatchRawSql(db, dialect, table, columns, primaryKey, rows, strategy, ops) {
10150
+ let inserted = 0;
10151
+ let skipped = 0;
10152
+ const sqlTemplate = ops.buildConflictInsert(table, columns, primaryKey, strategy);
10153
+ for (const row of rows) {
10154
+ const values = columns.map((col) => row[col]);
10155
+ const sqlParts = [];
10156
+ const sqlValues = [];
10157
+ if (dialect === "mssql") {
10158
+ let match;
10159
+ const paramRegex = /@p(\d+)/g;
10160
+ let lastIndex = 0;
10161
+ paramRegex.lastIndex = 0;
10162
+ while ((match = paramRegex.exec(sqlTemplate)) !== null) {
10163
+ sqlParts.push(sqlTemplate.slice(lastIndex, match.index));
10164
+ sqlValues.push(values[parseInt(match[1], 10)]);
10165
+ lastIndex = match.index + match[0].length;
10166
+ }
10167
+ sqlParts.push(sqlTemplate.slice(lastIndex));
10168
+ } else {
10169
+ const parts = sqlTemplate.split("?");
10170
+ let paramIdx = 0;
10171
+ for (let i = 0; i < parts.length; i++) {
10172
+ sqlParts.push(parts[i]);
10173
+ if (i < parts.length - 1) {
10174
+ sqlValues.push(values[paramIdx++]);
10175
+ }
10176
+ }
10177
+ }
10178
+ const finalSql = sql.join(
10179
+ sqlParts.map((part, i) => {
10180
+ if (i < sqlValues.length) {
10181
+ return sql`${sql.raw(part)}${sql.val(sqlValues[i])}`;
10182
+ }
10183
+ return sql.raw(part);
10184
+ }),
10185
+ sql.raw("")
10186
+ );
10187
+ const [, err] = await attempt(() => finalSql.execute(db));
10188
+ if (err) {
10189
+ if (isDuplicateKeyError(err.message)) {
10190
+ skipped++;
10191
+ } else {
10192
+ return [{ inserted, skipped }, err];
10193
+ }
10194
+ } else {
10195
+ inserted++;
10196
+ }
10197
+ }
10198
+ return [{ inserted, skipped }, null];
10199
+ }
10200
+ function isDuplicateKeyError(message) {
10201
+ const lower = message.toLowerCase();
10202
+ 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");
10203
+ }
10204
+ async function truncateTable(db, dialect, table) {
10205
+ let truncateSql;
10206
+ switch (dialect) {
10207
+ case "postgres":
10208
+ truncateSql = `TRUNCATE TABLE "${table}" CASCADE`;
10209
+ break;
10210
+ case "mssql":
10211
+ truncateSql = `DELETE FROM [${table}]`;
10212
+ break;
10213
+ case "mysql":
10214
+ truncateSql = `TRUNCATE TABLE \`${table}\``;
10215
+ break;
10216
+ default:
10217
+ truncateSql = `TRUNCATE TABLE ${table}`;
10218
+ }
10219
+ const [, err] = await attempt(() => sql.raw(truncateSql).execute(db));
10220
+ return [void 0, err];
10221
+ }
10222
+
10223
+ // src/core/transfer/index.ts
10224
+ async function transferData(sourceConfig, destConfig, options = {}) {
10225
+ const srcDialect = sourceConfig.connection.dialect;
10226
+ const dstDialect = destConfig.connection.dialect;
10227
+ if (!isTransferSupported(srcDialect)) {
10228
+ return [null, new Error(`Transfer not supported for dialect: ${srcDialect}. Supported: ${TRANSFER_SUPPORTED_DIALECTS.join(", ")}`)];
10229
+ }
10230
+ if (!isTransferSupported(dstDialect)) {
10231
+ return [null, new Error(`Transfer not supported for dialect: ${dstDialect}. Supported: ${TRANSFER_SUPPORTED_DIALECTS.join(", ")}`)];
10232
+ }
10233
+ return withDualConnection(
10234
+ {
10235
+ sourceConfig,
10236
+ destConfig,
10237
+ ensureSchema: false
10238
+ // Don't create noorm tables on destination
10239
+ },
10240
+ async (ctx) => {
10241
+ const [plan, planErr] = await planTransfer(ctx, options);
10242
+ if (planErr) {
10243
+ throw planErr;
10244
+ }
10245
+ if (!plan || plan.tables.length === 0) {
10246
+ return {
10247
+ status: "success",
10248
+ tables: [],
10249
+ totalRows: 0,
10250
+ durationMs: 0
10251
+ };
10252
+ }
10253
+ if (options.dryRun) {
10254
+ return {
10255
+ status: "success",
10256
+ tables: plan.tables.map((t) => ({
10257
+ table: t.name,
10258
+ status: "skipped",
10259
+ rowsTransferred: 0,
10260
+ rowsSkipped: 0,
10261
+ durationMs: 0
10262
+ })),
10263
+ totalRows: 0,
10264
+ durationMs: 0
10265
+ };
10266
+ }
10267
+ const [result, execErr] = await executeTransfer(ctx, plan, options);
10268
+ if (execErr) {
10269
+ throw execErr;
10270
+ }
10271
+ return result;
10272
+ }
10273
+ );
10274
+ }
10275
+ async function getTransferPlan(sourceConfig, destConfig, options = {}) {
10276
+ const srcDialect = sourceConfig.connection.dialect;
10277
+ const dstDialect = destConfig.connection.dialect;
10278
+ if (!isTransferSupported(srcDialect)) {
10279
+ return [null, new Error(`Transfer not supported for dialect: ${srcDialect}`)];
10280
+ }
10281
+ if (!isTransferSupported(dstDialect)) {
10282
+ return [null, new Error(`Transfer not supported for dialect: ${dstDialect}`)];
10283
+ }
10284
+ return withDualConnection(
10285
+ {
10286
+ sourceConfig,
10287
+ destConfig,
10288
+ ensureSchema: false
10289
+ },
10290
+ async (ctx) => {
10291
+ const [plan, err] = await planTransfer(ctx, options);
10292
+ if (err) {
10293
+ throw err;
10294
+ }
10295
+ return plan;
10296
+ }
10297
+ );
10298
+ }
10299
+ var DtWriter = class {
10300
+ #filepath;
10301
+ #schema;
10302
+ #passphrase;
10303
+ #extension;
10304
+ #stream = null;
10305
+ #passthrough = null;
10306
+ #pipelinePromise = null;
10307
+ #bytesWritten = 0;
10308
+ #rowsWritten = 0;
10309
+ #buffer = null;
10310
+ /**
10311
+ * Create a new DtWriter.
10312
+ *
10313
+ * @param options - File path, schema, and optional passphrase
10314
+ */
10315
+ constructor(options) {
10316
+ this.#filepath = options.filepath;
10317
+ this.#schema = options.schema;
10318
+ this.#passphrase = options.passphrase;
10319
+ this.#extension = path7.extname(options.filepath).toLowerCase();
10320
+ }
10321
+ /** Total bytes written to disk. */
10322
+ get bytesWritten() {
10323
+ return this.#bytesWritten;
10324
+ }
10325
+ /** Total rows written. */
10326
+ get rowsWritten() {
10327
+ return this.#rowsWritten;
10328
+ }
10329
+ /**
10330
+ * Open the writer and write the schema header.
10331
+ *
10332
+ * Must be called before writing any rows.
10333
+ */
10334
+ async open() {
10335
+ if (this.#extension === DT_EXTENSIONS.ENCRYPTED) {
10336
+ if (!this.#passphrase) {
10337
+ throw new Error("Passphrase required for .dtzx files");
10338
+ }
10339
+ this.#buffer = [];
10340
+ const schemaLine2 = JSON5.stringify(this.#schema) + "\n";
10341
+ this.#buffer.push(Buffer.from(schemaLine2, "utf8"));
10342
+ return;
10343
+ }
10344
+ const fileStream = createWriteStream(this.#filepath);
10345
+ const schemaLine = JSON5.stringify(this.#schema) + "\n";
10346
+ if (this.#extension === DT_EXTENSIONS.COMPRESSED) {
10347
+ this.#passthrough = new PassThrough();
10348
+ const gzip = createGzip();
10349
+ this.#pipelinePromise = pipeline(this.#passthrough, gzip, fileStream);
10350
+ this.#stream = this.#passthrough;
10351
+ this.#writeToStream(schemaLine);
10352
+ } else {
10353
+ this.#stream = fileStream;
10354
+ this.#writeToStream(schemaLine);
10355
+ }
10356
+ }
10357
+ /**
10358
+ * Write a single serialized row.
10359
+ *
10360
+ * @param values - Serialized .dt values in column order
10361
+ */
10362
+ writeRow(values) {
10363
+ const line = JSON5.stringify(values) + "\n";
10364
+ if (this.#buffer) {
10365
+ this.#buffer.push(Buffer.from(line, "utf8"));
10366
+ } else {
10367
+ this.#writeToStream(line);
10368
+ }
10369
+ this.#rowsWritten++;
10370
+ }
10371
+ /**
10372
+ * Write multiple serialized rows.
10373
+ *
10374
+ * @param rows - Array of serialized row value arrays
10375
+ */
10376
+ writeRows(rows) {
10377
+ for (const row of rows) {
10378
+ this.writeRow(row);
10379
+ }
10380
+ }
10381
+ /**
10382
+ * Close the writer and finalize the file.
10383
+ *
10384
+ * For .dtzx files, this is where compression and encryption happen.
10385
+ */
10386
+ async close() {
10387
+ if (this.#buffer) {
10388
+ const { gzipSync: gzipSync2 } = await import('zlib');
10389
+ const raw = Buffer.concat(this.#buffer);
10390
+ const compressed = gzipSync2(raw);
10391
+ const payload = encryptWithPassphrase(compressed, this.#passphrase);
10392
+ const payloadJson = JSON.stringify(payload);
10393
+ writeFileSync(this.#filepath, payloadJson, "utf8");
10394
+ this.#bytesWritten = Buffer.byteLength(payloadJson, "utf8");
10395
+ this.#buffer = null;
10396
+ return;
10397
+ }
10398
+ if (this.#passthrough) {
10399
+ this.#passthrough.end();
10400
+ await this.#pipelinePromise;
10401
+ } else if (this.#stream) {
10402
+ await new Promise((resolve, reject) => {
10403
+ this.#stream.end(() => resolve());
10404
+ this.#stream.on("error", reject);
10405
+ });
10406
+ }
10407
+ }
10408
+ /**
10409
+ * Write a string to the active stream and track bytes.
10410
+ */
10411
+ #writeToStream(data2) {
10412
+ if (!this.#stream) {
10413
+ throw new Error("Writer not opened");
10414
+ }
10415
+ const buf = Buffer.from(data2, "utf8");
10416
+ this.#bytesWritten += buf.length;
10417
+ this.#stream.write(buf);
10418
+ }
10419
+ };
10420
+ function serializeRow(options) {
10421
+ const { row, columns } = options;
10422
+ const result = [];
10423
+ for (const col of columns) {
10424
+ const value = row[col.name];
10425
+ result.push(serializeValue(value, col));
10426
+ }
10427
+ return result;
10428
+ }
10429
+ function serializeValue(value, column) {
10430
+ if (value === null || value === void 0) {
10431
+ return null;
10432
+ }
10433
+ if (isEncodedType(column.type)) {
10434
+ return encodeValue(value, column.type);
10435
+ }
10436
+ return serializeSimple(value, column.type);
10437
+ }
10438
+ function serializeSimple(value, type) {
10439
+ switch (type) {
10440
+ case "bigint":
10441
+ return String(value);
10442
+ case "decimal":
10443
+ return String(value);
10444
+ case "bool":
10445
+ if (typeof value === "number") return value !== 0;
10446
+ if (typeof value === "string") return value === "1" || value.toLowerCase() === "true";
10447
+ return Boolean(value);
10448
+ case "timestamp": {
10449
+ if (value instanceof Date) return value.toISOString();
10450
+ return String(value);
10451
+ }
10452
+ case "date": {
10453
+ if (value instanceof Date) return value.toISOString().split("T")[0];
10454
+ return String(value);
10455
+ }
10456
+ case "uuid":
10457
+ return String(value);
10458
+ case "int":
10459
+ case "float":
10460
+ return typeof value === "string" ? Number(value) : value;
10461
+ case "string":
10462
+ default:
10463
+ return String(value);
10464
+ }
10465
+ }
10466
+ function encodeValue(value, type) {
10467
+ if (type === "binary") {
10468
+ return encodeBinary(value);
10469
+ }
10470
+ return encodeJsonLike(value);
10471
+ }
10472
+ function encodeBinary(value) {
10473
+ const buf = toBuffer(value);
10474
+ const byteLength = buf.length;
10475
+ if (byteLength < GZIP_THRESHOLD) {
10476
+ return [buf.toString("base64"), "b64"];
10477
+ }
10478
+ const compressed = gzipSync(buf);
10479
+ if (compressed.length < byteLength * GZIP_RATIO_THRESHOLD) {
10480
+ return [compressed.toString("base64"), "gz64"];
10481
+ }
10482
+ return [buf.toString("base64"), "b64"];
10483
+ }
10484
+ function encodeJsonLike(value) {
10485
+ const jsonStr = JSON.stringify(value);
10486
+ const byteLength = Buffer.byteLength(jsonStr, "utf8");
10487
+ if (byteLength < GZIP_THRESHOLD) {
10488
+ return [value, "raw"];
10489
+ }
10490
+ const buf = Buffer.from(jsonStr, "utf8");
10491
+ const compressed = gzipSync(buf);
10492
+ if (compressed.length < byteLength * GZIP_RATIO_THRESHOLD) {
10493
+ return [compressed.toString("base64"), "gz64"];
10494
+ }
10495
+ return [value, "raw"];
10496
+ }
10497
+ function toBuffer(value) {
10498
+ if (Buffer.isBuffer(value)) return value;
10499
+ if (value instanceof Uint8Array) return Buffer.from(value);
10500
+ if (typeof value === "string") return Buffer.from(value, "base64");
10501
+ return Buffer.from(String(value), "utf8");
10502
+ }
10503
+ function deserializeRow(options) {
10504
+ const { values, columns, targetDialect, targetVersion } = options;
10505
+ const result = {};
10506
+ for (let i = 0; i < columns.length; i++) {
10507
+ const col = columns[i];
10508
+ const value = values[i];
10509
+ result[col.name] = deserializeValue(value, col, targetDialect, targetVersion);
10510
+ }
10511
+ return result;
10512
+ }
10513
+ function deserializeValue(value, column, targetDialect, targetVersion) {
10514
+ if (value === null || value === void 0) {
10515
+ return null;
10516
+ }
10517
+ if (isEncodedType(column.type)) {
10518
+ const decoded = decodeTuple(value);
10519
+ return convertEncodedForTarget(decoded, column.type, targetDialect, targetVersion);
10520
+ }
10521
+ return convertSimpleForTarget(value, column.type, targetDialect);
10522
+ }
10523
+ function decodeTuple(tuple) {
10524
+ if (!Array.isArray(tuple) || tuple.length < 2) {
10525
+ return tuple;
10526
+ }
10527
+ const [value, encoding] = tuple;
10528
+ switch (encoding) {
10529
+ case "raw":
10530
+ return value;
10531
+ case "b64":
10532
+ return Buffer.from(value, "base64");
10533
+ case "gz64": {
10534
+ const compressed = Buffer.from(value, "base64");
10535
+ const decompressed = gunzipSync(compressed);
10536
+ const str = decompressed.toString("utf8");
10537
+ if (/^[[{"\d]/.test(str)) {
10538
+ return JSON.parse(str);
10539
+ }
10540
+ return decompressed;
10541
+ }
10542
+ default:
10543
+ return value;
10544
+ }
10545
+ }
10546
+ function convertEncodedForTarget(decoded, type, targetDialect, targetVersion) {
10547
+ switch (type) {
10548
+ case "json":
10549
+ return convertJsonForTarget(decoded, targetDialect, targetVersion);
10550
+ case "text":
10551
+ return decoded;
10552
+ case "binary":
10553
+ return decoded;
10554
+ case "vector":
10555
+ return convertVectorForTarget(decoded, targetDialect, targetVersion);
10556
+ case "array":
10557
+ return convertArrayForTarget(decoded, targetDialect);
10558
+ case "custom":
10559
+ if (typeof decoded === "object") return JSON.stringify(decoded);
10560
+ return decoded;
10561
+ default:
10562
+ return decoded;
10563
+ }
10564
+ }
10565
+ function convertJsonForTarget(decoded, targetDialect, targetVersion) {
10566
+ if (targetDialect === "mssql") {
10567
+ const major = targetVersion?.major ?? 2022;
10568
+ if (major < 2025) {
10569
+ return typeof decoded === "string" ? decoded : JSON.stringify(decoded);
10570
+ }
10571
+ }
10572
+ return decoded;
10573
+ }
10574
+ function convertVectorForTarget(decoded, targetDialect, targetVersion) {
10575
+ const arr = Array.isArray(decoded) ? decoded : decoded;
10576
+ if (targetDialect === "postgres") {
10577
+ return Array.isArray(arr) ? `[${arr.join(",")}]` : String(arr);
10578
+ }
10579
+ if (targetDialect === "mysql") {
10580
+ const major = targetVersion?.major ?? 8;
10581
+ if (major >= 9) {
10582
+ return Array.isArray(arr) ? `[${arr.join(",")}]` : String(arr);
10583
+ }
10584
+ return Array.isArray(arr) ? JSON.stringify(arr) : String(arr);
10585
+ }
10586
+ return Array.isArray(arr) ? JSON.stringify(arr) : String(arr);
10587
+ }
10588
+ function convertArrayForTarget(decoded, targetDialect) {
10589
+ if (targetDialect === "postgres") {
10590
+ return decoded;
10591
+ }
10592
+ return Array.isArray(decoded) ? JSON.stringify(decoded) : String(decoded);
10593
+ }
10594
+ function convertSimpleForTarget(value, type, targetDialect) {
10595
+ switch (type) {
10596
+ case "bigint":
10597
+ if (typeof value === "string") {
10598
+ const num = Number(value);
10599
+ if (Number.isSafeInteger(num)) return num;
10600
+ }
10601
+ return value;
10602
+ case "decimal":
10603
+ return value;
10604
+ case "bool":
10605
+ if (targetDialect === "mssql") return value ? 1 : 0;
10606
+ if (targetDialect === "mysql") return value ? 1 : 0;
10607
+ return value;
10608
+ case "timestamp":
10609
+ if (typeof value === "string") return new Date(value);
10610
+ return value;
10611
+ case "date":
10612
+ return value;
10613
+ case "uuid":
10614
+ return value;
10615
+ case "int":
10616
+ case "float":
10617
+ return value;
10618
+ case "string":
10619
+ default:
10620
+ return value;
10621
+ }
10622
+ }
10623
+
10624
+ // src/core/dt/index.ts
10625
+ async function exportTable(options) {
10626
+ const startTime = Date.now();
10627
+ const { db, dialect, tableName, filepath, passphrase, schema: schemaName } = options;
10628
+ const kyselyDb = db;
10629
+ const batchSize = options.batchSize ?? 1e3;
10630
+ const [dtSchema, schemaErr] = await buildDtSchema({
10631
+ db: kyselyDb,
10632
+ dialect,
10633
+ tableName,
10634
+ version: options.version,
10635
+ schema: schemaName
10636
+ });
10637
+ if (schemaErr || !dtSchema) {
10638
+ return [null, schemaErr ?? new Error("Failed to build schema")];
10639
+ }
10640
+ observer.emit("dt:export:start", {
10641
+ filepath,
10642
+ table: tableName,
10643
+ columnCount: dtSchema.columns.length
10644
+ });
10645
+ const writer = new DtWriter({ filepath, schema: dtSchema, passphrase });
10646
+ const [, openErr] = await attempt(() => writer.open());
10647
+ if (openErr) {
10648
+ return [null, openErr];
10649
+ }
10650
+ let offset = 0;
10651
+ const columns = dtSchema.columns.map((c) => c.name);
10652
+ const quoteIdent4 = dialect === "mssql" ? (c) => `[${c}]` : dialect === "mysql" ? (c) => `\`${c}\`` : (c) => `"${c}"`;
10653
+ const columnList = columns.map(quoteIdent4).join(", ");
10654
+ while (true) {
10655
+ const [rows, fetchErr] = await attempt(() => {
10656
+ if (dialect === "mssql") {
10657
+ const orderCol = quoteIdent4(columns[0]);
10658
+ return sql`
10659
+ SELECT ${sql.raw(columnList)}
10660
+ FROM ${sql.table(tableName)}
10661
+ ORDER BY ${sql.raw(orderCol)}
10662
+ OFFSET ${offset} ROWS
10663
+ FETCH NEXT ${batchSize} ROWS ONLY
10664
+ `.execute(kyselyDb);
10665
+ }
10666
+ return sql`
10667
+ SELECT ${sql.raw(columnList)}
10668
+ FROM ${sql.table(tableName)}
10669
+ LIMIT ${batchSize}
10670
+ OFFSET ${offset}
10671
+ `.execute(kyselyDb);
10672
+ });
10673
+ if (fetchErr) {
10674
+ return [null, fetchErr];
10675
+ }
10676
+ if (rows.rows.length === 0) break;
10677
+ for (const row of rows.rows) {
10678
+ const values = serializeRow({ row, columns: dtSchema.columns });
10679
+ writer.writeRow(values);
10680
+ }
10681
+ offset += rows.rows.length;
10682
+ observer.emit("dt:export:progress", {
10683
+ filepath,
10684
+ table: tableName,
10685
+ rowsWritten: writer.rowsWritten,
10686
+ bytesWritten: writer.bytesWritten
10687
+ });
10688
+ if (rows.rows.length < batchSize) break;
10689
+ }
10690
+ const [, closeErr] = await attempt(() => writer.close());
10691
+ if (closeErr) {
10692
+ return [null, closeErr];
10693
+ }
10694
+ const durationMs = Date.now() - startTime;
10695
+ observer.emit("dt:export:complete", {
10696
+ filepath,
10697
+ table: tableName,
10698
+ rowsWritten: writer.rowsWritten,
10699
+ bytesWritten: writer.bytesWritten,
10700
+ durationMs
10701
+ });
10702
+ return [{ rowsWritten: writer.rowsWritten, bytesWritten: writer.bytesWritten }, null];
10703
+ }
10704
+ async function importDtFile(options) {
10705
+ const startTime = Date.now();
10706
+ const { filepath, db, dialect, passphrase, version } = options;
10707
+ const kyselyDb = db;
10708
+ const batchSize = options.batchSize ?? 1e3;
10709
+ const reader = new DtReader({ filepath, passphrase });
10710
+ const [, openErr] = await attempt(() => reader.open());
10711
+ if (openErr) {
10712
+ observer.emit("error", { source: "dt:import", error: openErr, context: { filepath } });
10713
+ return [null, openErr];
10714
+ }
10715
+ const dtSchema = reader.schema;
10716
+ if (!dtSchema) {
10717
+ const err = new Error("Failed to read .dt schema");
10718
+ observer.emit("error", { source: "dt:import", error: err, context: { filepath } });
10719
+ return [null, err];
10720
+ }
10721
+ const tableName = dtSchema.t ?? "unknown";
10722
+ observer.emit("dt:import:start", {
10723
+ filepath,
10724
+ sourceDialect: dtSchema.d,
10725
+ sourceVersion: dtSchema.dv,
10726
+ table: tableName
10727
+ });
10728
+ const [validation, validateErr] = await validateSchema({
10729
+ dtSchema,
10730
+ targetDb: kyselyDb,
10731
+ targetDialect: dialect,
10732
+ targetVersion: version
10733
+ });
10734
+ if (validateErr) {
10735
+ reader.close();
10736
+ observer.emit("error", { source: "dt:import", error: validateErr, context: { filepath, table: tableName } });
10737
+ return [null, validateErr];
10738
+ }
10739
+ observer.emit("dt:import:schema", {
10740
+ filepath,
10741
+ table: tableName,
10742
+ columns: dtSchema.columns.length,
10743
+ validation
10744
+ });
10745
+ if (validation && !validation.valid) {
10746
+ reader.close();
10747
+ const err = new Error(`Schema validation failed: ${validation.errors.join("; ")}`);
10748
+ observer.emit("error", { source: "dt:import", error: err, context: { filepath, table: tableName } });
10749
+ return [null, err];
10750
+ }
10751
+ if (options.truncate && tableName !== "unknown") {
10752
+ const truncateSql = dialect === "postgres" ? `TRUNCATE TABLE "${tableName}" CASCADE` : dialect === "mssql" ? `DELETE FROM [${tableName}]` : `TRUNCATE TABLE \`${tableName}\``;
10753
+ const [, truncErr] = await attempt(() => sql.raw(truncateSql).execute(kyselyDb));
10754
+ if (truncErr) {
10755
+ reader.close();
10756
+ const err = new Error(`Failed to truncate: ${truncErr.message}`);
10757
+ observer.emit("error", { source: "dt:import", error: err, context: { filepath, table: tableName } });
10758
+ return [null, err];
10759
+ }
10760
+ }
10761
+ let rowsImported = 0;
10762
+ let rowsSkipped = 0;
10763
+ let batch = [];
10764
+ for await (const values of reader.rows()) {
10765
+ const row = deserializeRow({
10766
+ values,
10767
+ columns: dtSchema.columns,
10768
+ targetDialect: dialect,
10769
+ targetVersion: version
10770
+ });
10771
+ batch.push(row);
10772
+ if (batch.length >= batchSize) {
10773
+ const columnNames = dtSchema.columns.map((c) => c.name);
10774
+ const [batchResult, insertErr] = await insertImportBatch(
10775
+ kyselyDb,
10776
+ tableName,
10777
+ batch,
10778
+ options.onConflict ?? "fail",
10779
+ dialect,
10780
+ columnNames
10781
+ );
10782
+ if (insertErr) {
10783
+ reader.close();
10784
+ observer.emit("error", { source: "dt:import", error: insertErr, context: { filepath, table: tableName } });
10785
+ return [null, insertErr];
10786
+ }
10787
+ rowsImported += batchResult.inserted + batchResult.updated;
10788
+ rowsSkipped += batchResult.skipped;
10789
+ batch = [];
10790
+ observer.emit("dt:import:progress", {
10791
+ filepath,
10792
+ table: tableName,
10793
+ rowsImported,
10794
+ rowsSkipped
10795
+ });
10796
+ }
10797
+ }
10798
+ if (batch.length > 0) {
10799
+ const columnNames = dtSchema.columns.map((c) => c.name);
10800
+ const [batchResult, insertErr] = await insertImportBatch(
10801
+ kyselyDb,
10802
+ tableName,
10803
+ batch,
10804
+ options.onConflict ?? "fail",
10805
+ dialect,
10806
+ columnNames
10807
+ );
10808
+ if (insertErr) {
10809
+ reader.close();
10810
+ observer.emit("error", { source: "dt:import", error: insertErr, context: { filepath, table: tableName } });
10811
+ return [null, insertErr];
10812
+ }
10813
+ rowsImported += batchResult.inserted + batchResult.updated;
10814
+ rowsSkipped += batchResult.skipped;
10815
+ }
10816
+ reader.close();
10817
+ const durationMs = Date.now() - startTime;
10818
+ observer.emit("dt:import:complete", {
10819
+ filepath,
10820
+ table: tableName,
10821
+ rowsImported,
10822
+ rowsSkipped,
10823
+ durationMs
10824
+ });
10825
+ return [{ rowsImported, rowsSkipped }, null];
10826
+ }
10827
+ async function insertImportBatch(db, table, rows, onConflict, dialect, columns) {
10828
+ let inserted = 0;
10829
+ let skipped = 0;
10830
+ let updated = 0;
10831
+ for (const row of rows) {
10832
+ const [, err] = await attempt(
10833
+ () => db.insertInto(table).values(row).execute()
10834
+ );
10835
+ if (err) {
10836
+ if (!isDuplicateError(err.message)) {
10837
+ if (onConflict === "fail") {
10838
+ return [{ inserted, skipped, updated }, err];
10839
+ }
10840
+ skipped++;
10841
+ continue;
10842
+ }
10843
+ if (onConflict === "skip") {
10844
+ skipped++;
10845
+ } else if (onConflict === "update") {
10846
+ const [updateResult, updateErr] = await attemptUpdate(db, table, row, dialect, columns);
10847
+ if (updateErr) {
10848
+ return [{ inserted, skipped, updated }, updateErr];
10849
+ }
10850
+ if (updateResult) {
10851
+ updated++;
10852
+ } else {
10853
+ skipped++;
10854
+ }
10855
+ } else if (onConflict === "fail") {
10856
+ return [{ inserted, skipped, updated }, err];
10857
+ } else {
10858
+ skipped++;
10859
+ }
10860
+ } else {
10861
+ inserted++;
10862
+ }
10863
+ }
10864
+ return [{ inserted, skipped, updated }, null];
10865
+ }
10866
+ async function attemptUpdate(db, table, row, _dialect, columns) {
10867
+ const pkColumn = columns[0];
10868
+ if (!pkColumn || row[pkColumn] === void 0) {
10869
+ return [false, null];
10870
+ }
10871
+ const pkValue = row[pkColumn];
10872
+ const updateValues = {};
10873
+ for (const col of columns) {
10874
+ if (col !== pkColumn && row[col] !== void 0) {
10875
+ updateValues[col] = row[col];
10876
+ }
10877
+ }
10878
+ if (Object.keys(updateValues).length === 0) {
10879
+ return [true, null];
10880
+ }
10881
+ const [, err] = await attempt(
10882
+ () => db.updateTable(table).set(updateValues).where(pkColumn, "=", pkValue).execute()
10883
+ );
10884
+ if (err) {
10885
+ return [false, err];
10886
+ }
10887
+ return [true, null];
10888
+ }
10889
+ function isDuplicateError(message) {
10890
+ const lower = message.toLowerCase();
10891
+ 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");
10892
+ }
10893
+
10894
+ // src/sdk/guards.ts
10895
+ var RequireTestError = class extends Error {
10896
+ constructor(configName) {
10897
+ super(`Config "${configName}" does not have isTest: true`);
10898
+ this.configName = configName;
10899
+ }
10900
+ name = "RequireTestError";
10901
+ };
10902
+ var ProtectedConfigError = class extends Error {
10903
+ constructor(configName, operation) {
10904
+ super(`Cannot ${operation} on protected config "${configName}"`);
10905
+ this.configName = configName;
10906
+ this.operation = operation;
10907
+ }
10908
+ name = "ProtectedConfigError";
10909
+ };
10910
+ function checkRequireTest(config, options) {
10911
+ if (options.requireTest && !config.isTest) {
10912
+ throw new RequireTestError(config.name);
10913
+ }
10914
+ }
10915
+ function checkProtectedConfig(config, operation, options) {
10916
+ if (config.protected && !options.allowProtected) {
10917
+ throw new ProtectedConfigError(config.name, operation);
10918
+ }
10919
+ }
10920
+
10921
+ // src/sdk/noorm-ops.ts
10922
+ var NoormOps = class {
10923
+ #state;
10924
+ constructor(state) {
10925
+ this.#state = state;
10926
+ }
10927
+ // ─────────────────────────────────────────────────────────
10928
+ // Read-only Properties
10929
+ // ─────────────────────────────────────────────────────────
10930
+ get config() {
10931
+ return this.#state.config;
10932
+ }
10933
+ get settings() {
10934
+ return this.#state.settings;
10935
+ }
10936
+ get identity() {
10937
+ return this.#state.identity;
10938
+ }
10939
+ get observer() {
10940
+ return observer;
10941
+ }
10942
+ // ─────────────────────────────────────────────────────────
10943
+ // Private Accessors
10944
+ // ─────────────────────────────────────────────────────────
10945
+ get #kysely() {
10946
+ if (!this.#state.connection) {
10947
+ throw new Error("Not connected. Call connect() first.");
10948
+ }
10949
+ return this.#state.connection.db;
10950
+ }
10951
+ get #dialect() {
10952
+ return this.#state.config.connection.dialect;
10953
+ }
7920
10954
  // ─────────────────────────────────────────────────────────
7921
10955
  // Explore
7922
10956
  // ─────────────────────────────────────────────────────────
10957
+ /**
10958
+ * List all tables in the database.
10959
+ *
10960
+ * @example
10961
+ * ```typescript
10962
+ * const tables = await ctx.noorm.listTables()
10963
+ * ```
10964
+ */
7923
10965
  async listTables() {
7924
- return fetchList(this.kysely, this.dialect, "tables");
10966
+ return fetchList(this.#kysely, this.#dialect, "tables");
7925
10967
  }
10968
+ /**
10969
+ * Get detailed information about a table.
10970
+ *
10971
+ * @example
10972
+ * ```typescript
10973
+ * const detail = await ctx.noorm.describeTable('users')
10974
+ * ```
10975
+ */
7926
10976
  async describeTable(name, schema) {
7927
- return fetchDetail(this.kysely, this.dialect, "tables", name, schema);
10977
+ return fetchDetail(this.#kysely, this.#dialect, "tables", name, schema);
7928
10978
  }
10979
+ /**
10980
+ * Get database overview with counts of all object types.
10981
+ *
10982
+ * @example
10983
+ * ```typescript
10984
+ * const overview = await ctx.noorm.overview()
10985
+ * ```
10986
+ */
7929
10987
  async overview() {
7930
- return fetchOverview(this.kysely, this.dialect);
10988
+ return fetchOverview(this.#kysely, this.#dialect);
7931
10989
  }
7932
10990
  // ─────────────────────────────────────────────────────────
7933
10991
  // Schema Operations
7934
10992
  // ─────────────────────────────────────────────────────────
10993
+ /**
10994
+ * Wipe all data, keeping the schema intact.
10995
+ *
10996
+ * @example
10997
+ * ```typescript
10998
+ * const result = await ctx.noorm.truncate()
10999
+ * ```
11000
+ */
7935
11001
  async truncate() {
7936
- checkProtectedConfig(this.#config, "truncate", this.#options);
7937
- return truncateData(this.kysely, this.dialect);
11002
+ checkProtectedConfig(this.#state.config, "truncate", this.#state.options);
11003
+ return truncateData(this.#kysely, this.#dialect);
7938
11004
  }
11005
+ /**
11006
+ * Drop all database objects except noorm tracking tables.
11007
+ *
11008
+ * @example
11009
+ * ```typescript
11010
+ * const result = await ctx.noorm.teardown()
11011
+ * ```
11012
+ */
7939
11013
  async teardown() {
7940
- checkProtectedConfig(this.#config, "teardown", this.#options);
7941
- return teardownSchema(this.kysely, this.dialect, {
7942
- configName: this.#config.name,
7943
- executedBy: formatIdentity(this.#identity)
11014
+ checkProtectedConfig(this.#state.config, "teardown", this.#state.options);
11015
+ return teardownSchema(this.#kysely, this.#dialect, {
11016
+ configName: this.#state.config.name,
11017
+ executedBy: formatIdentity(this.#state.identity)
7944
11018
  });
7945
11019
  }
11020
+ /**
11021
+ * Execute all SQL files in the schema directory.
11022
+ *
11023
+ * @example
11024
+ * ```typescript
11025
+ * const result = await ctx.noorm.build({ force: true })
11026
+ * ```
11027
+ */
7946
11028
  async build(options) {
7947
11029
  const runContext = this.#createRunContext();
7948
- const sqlPath = path6.join(
7949
- this.#projectRoot,
7950
- this.#config.paths.sql
11030
+ const sqlPath = path7.join(
11031
+ this.#state.projectRoot,
11032
+ this.#state.config.paths.sql
7951
11033
  );
7952
11034
  return runBuild(runContext, sqlPath, { force: options?.force });
7953
11035
  }
11036
+ /**
11037
+ * Full rebuild: teardown + build.
11038
+ *
11039
+ * @example
11040
+ * ```typescript
11041
+ * await ctx.noorm.reset()
11042
+ * ```
11043
+ */
7954
11044
  async reset() {
7955
- checkProtectedConfig(this.#config, "reset", this.#options);
11045
+ checkProtectedConfig(this.#state.config, "reset", this.#state.options);
7956
11046
  await this.teardown();
7957
11047
  await this.build({ force: true });
7958
11048
  }
7959
11049
  // ─────────────────────────────────────────────────────────
7960
11050
  // File Runner
7961
11051
  // ─────────────────────────────────────────────────────────
11052
+ /**
11053
+ * Execute a single SQL file.
11054
+ *
11055
+ * @example
11056
+ * ```typescript
11057
+ * await ctx.noorm.runFile('seeds/test-data.sql')
11058
+ * ```
11059
+ */
7962
11060
  async runFile(filepath, options) {
7963
11061
  const runContext = this.#createRunContext();
7964
- const absolutePath = path6.isAbsolute(filepath) ? filepath : path6.join(this.#projectRoot, filepath);
11062
+ const absolutePath = path7.isAbsolute(filepath) ? filepath : path7.join(this.#state.projectRoot, filepath);
7965
11063
  return runFile(runContext, absolutePath, options);
7966
11064
  }
11065
+ /**
11066
+ * Execute multiple SQL files sequentially.
11067
+ *
11068
+ * @example
11069
+ * ```typescript
11070
+ * await ctx.noorm.runFiles(['functions/utils.sql', 'triggers/audit.sql'])
11071
+ * ```
11072
+ */
7967
11073
  async runFiles(filepaths, options) {
7968
11074
  const runContext = this.#createRunContext();
7969
11075
  const absolutePaths = filepaths.map(
7970
- (fp) => path6.isAbsolute(fp) ? fp : path6.join(this.#projectRoot, fp)
11076
+ (fp) => path7.isAbsolute(fp) ? fp : path7.join(this.#state.projectRoot, fp)
7971
11077
  );
7972
11078
  return runFiles(runContext, absolutePaths, options);
7973
11079
  }
11080
+ /**
11081
+ * Execute all SQL files in a directory.
11082
+ *
11083
+ * @example
11084
+ * ```typescript
11085
+ * await ctx.noorm.runDir('seeds/')
11086
+ * ```
11087
+ */
7974
11088
  async runDir(dirpath, options) {
7975
11089
  const runContext = this.#createRunContext();
7976
- const absolutePath = path6.isAbsolute(dirpath) ? dirpath : path6.join(this.#projectRoot, dirpath);
11090
+ const absolutePath = path7.isAbsolute(dirpath) ? dirpath : path7.join(this.#state.projectRoot, dirpath);
7977
11091
  return runDir(runContext, absolutePath, options);
7978
11092
  }
7979
11093
  // ─────────────────────────────────────────────────────────
7980
11094
  // Changes
7981
11095
  // ─────────────────────────────────────────────────────────
11096
+ /**
11097
+ * Apply a specific change.
11098
+ *
11099
+ * @example
11100
+ * ```typescript
11101
+ * const result = await ctx.noorm.applyChange('2024-01-15-add-users')
11102
+ * ```
11103
+ */
7982
11104
  async applyChange(name, options) {
7983
11105
  return this.#getChangeManager().run(name, options);
7984
11106
  }
11107
+ /**
11108
+ * Revert a specific change.
11109
+ *
11110
+ * @example
11111
+ * ```typescript
11112
+ * const result = await ctx.noorm.revertChange('2024-01-15-add-users')
11113
+ * ```
11114
+ */
7985
11115
  async revertChange(name, options) {
7986
11116
  return this.#getChangeManager().revert(name, options);
7987
11117
  }
11118
+ /**
11119
+ * Apply all pending changes.
11120
+ *
11121
+ * @example
11122
+ * ```typescript
11123
+ * const result = await ctx.noorm.fastForward()
11124
+ * ```
11125
+ */
7988
11126
  async fastForward() {
7989
11127
  return this.#getChangeManager().ff();
7990
11128
  }
11129
+ /**
11130
+ * Get status of all changes.
11131
+ *
11132
+ * @example
11133
+ * ```typescript
11134
+ * const changes = await ctx.noorm.getChangeStatus()
11135
+ * ```
11136
+ */
7991
11137
  async getChangeStatus() {
7992
11138
  return this.#getChangeManager().list();
7993
11139
  }
11140
+ /**
11141
+ * Get only pending changes.
11142
+ *
11143
+ * @example
11144
+ * ```typescript
11145
+ * const pending = await ctx.noorm.getPendingChanges()
11146
+ * ```
11147
+ */
7994
11148
  async getPendingChanges() {
7995
11149
  const all = await this.getChangeStatus();
7996
11150
  return all.filter(
@@ -8000,122 +11154,437 @@ var Context = class {
8000
11154
  // ─────────────────────────────────────────────────────────
8001
11155
  // Secrets
8002
11156
  // ─────────────────────────────────────────────────────────
11157
+ /**
11158
+ * Get a config-scoped secret.
11159
+ *
11160
+ * @example
11161
+ * ```typescript
11162
+ * const apiKey = ctx.noorm.getSecret('API_KEY')
11163
+ * ```
11164
+ */
8003
11165
  getSecret(key) {
8004
- const state = getStateManager(this.#projectRoot);
8005
- const value = state.getSecret(this.#config.name, key);
11166
+ const state = getStateManager(this.#state.projectRoot);
11167
+ const value = state.getSecret(this.#state.config.name, key);
8006
11168
  return value ?? void 0;
8007
11169
  }
8008
11170
  // ─────────────────────────────────────────────────────────
8009
11171
  // Locks
8010
11172
  // ─────────────────────────────────────────────────────────
11173
+ /**
11174
+ * Acquire a database lock.
11175
+ *
11176
+ * @example
11177
+ * ```typescript
11178
+ * const lock = await ctx.noorm.acquireLock({ timeout: 60000 })
11179
+ * ```
11180
+ */
8011
11181
  async acquireLock(options) {
8012
11182
  const lockManager = getLockManager();
8013
- const identityStr = formatIdentity(this.#identity);
11183
+ const identityStr = formatIdentity(this.#state.identity);
8014
11184
  return lockManager.acquire(
8015
- this.kysely,
8016
- this.#config.name,
11185
+ this.#kysely,
11186
+ this.#state.config.name,
8017
11187
  identityStr,
8018
- { ...options, dialect: this.#config.connection.dialect }
11188
+ { ...options, dialect: this.#state.config.connection.dialect }
8019
11189
  );
8020
11190
  }
11191
+ /**
11192
+ * Release the current lock.
11193
+ *
11194
+ * @example
11195
+ * ```typescript
11196
+ * await ctx.noorm.releaseLock()
11197
+ * ```
11198
+ */
8021
11199
  async releaseLock() {
8022
11200
  const lockManager = getLockManager();
8023
- const identityStr = formatIdentity(this.#identity);
11201
+ const identityStr = formatIdentity(this.#state.identity);
8024
11202
  await lockManager.release(
8025
- this.kysely,
8026
- this.#config.name,
11203
+ this.#kysely,
11204
+ this.#state.config.name,
8027
11205
  identityStr
8028
11206
  );
8029
11207
  }
11208
+ /**
11209
+ * Get current lock status.
11210
+ *
11211
+ * @example
11212
+ * ```typescript
11213
+ * const status = await ctx.noorm.getLockStatus()
11214
+ * ```
11215
+ */
8030
11216
  async getLockStatus() {
8031
11217
  const lockManager = getLockManager();
8032
11218
  return lockManager.status(
8033
- this.kysely,
8034
- this.#config.name,
8035
- this.#config.connection.dialect
11219
+ this.#kysely,
11220
+ this.#state.config.name,
11221
+ this.#state.config.connection.dialect
8036
11222
  );
8037
11223
  }
11224
+ /**
11225
+ * Execute an operation with automatic lock acquisition and release.
11226
+ *
11227
+ * @example
11228
+ * ```typescript
11229
+ * await ctx.noorm.withLock(async () => {
11230
+ * await ctx.noorm.build()
11231
+ * await ctx.noorm.fastForward()
11232
+ * })
11233
+ * ```
11234
+ */
8038
11235
  async withLock(fn, options) {
8039
11236
  const lockManager = getLockManager();
8040
- const identityStr = formatIdentity(this.#identity);
11237
+ const identityStr = formatIdentity(this.#state.identity);
8041
11238
  return lockManager.withLock(
8042
- this.kysely,
8043
- this.#config.name,
11239
+ this.#kysely,
11240
+ this.#state.config.name,
8044
11241
  identityStr,
8045
11242
  fn,
8046
- { ...options, dialect: this.#config.connection.dialect }
11243
+ { ...options, dialect: this.#state.config.connection.dialect }
8047
11244
  );
8048
11245
  }
11246
+ /**
11247
+ * Force release any database lock regardless of ownership.
11248
+ *
11249
+ * @example
11250
+ * ```typescript
11251
+ * await ctx.noorm.forceReleaseLock()
11252
+ * ```
11253
+ */
8049
11254
  async forceReleaseLock() {
8050
11255
  const lockManager = getLockManager();
8051
11256
  return lockManager.forceRelease(
8052
- this.kysely,
8053
- this.#config.name
11257
+ this.#kysely,
11258
+ this.#state.config.name
8054
11259
  );
8055
11260
  }
8056
11261
  // ─────────────────────────────────────────────────────────
8057
11262
  // Templates
8058
11263
  // ─────────────────────────────────────────────────────────
11264
+ /**
11265
+ * Render a template file without executing.
11266
+ *
11267
+ * @example
11268
+ * ```typescript
11269
+ * const result = await ctx.noorm.renderTemplate('sql/001_users.sql.tmpl')
11270
+ * ```
11271
+ */
8059
11272
  async renderTemplate(filepath) {
8060
- const absolutePath = path6.isAbsolute(filepath) ? filepath : path6.join(this.#projectRoot, filepath);
8061
- const state = getStateManager(this.#projectRoot);
11273
+ const absolutePath = path7.isAbsolute(filepath) ? filepath : path7.join(this.#state.projectRoot, filepath);
11274
+ const state = getStateManager(this.#state.projectRoot);
8062
11275
  return processFile(absolutePath, {
8063
- projectRoot: this.#projectRoot,
8064
- config: this.#config,
8065
- secrets: state.getAllSecrets(this.#config.name),
11276
+ projectRoot: this.#state.projectRoot,
11277
+ config: this.#state.config,
11278
+ secrets: state.getAllSecrets(this.#state.config.name),
8066
11279
  globalSecrets: state.getAllGlobalSecrets()
8067
11280
  });
8068
11281
  }
8069
11282
  // ─────────────────────────────────────────────────────────
8070
11283
  // History
8071
11284
  // ─────────────────────────────────────────────────────────
11285
+ /**
11286
+ * Get execution history.
11287
+ *
11288
+ * @example
11289
+ * ```typescript
11290
+ * const history = await ctx.noorm.getHistory(10)
11291
+ * ```
11292
+ */
8072
11293
  async getHistory(limit) {
8073
11294
  return this.#getChangeManager().getHistory(void 0, limit);
8074
11295
  }
8075
11296
  // ─────────────────────────────────────────────────────────
8076
11297
  // Utilities
8077
11298
  // ─────────────────────────────────────────────────────────
11299
+ /**
11300
+ * Compute SHA-256 checksum for a file.
11301
+ *
11302
+ * @example
11303
+ * ```typescript
11304
+ * const checksum = await ctx.noorm.computeChecksum('sql/001_users.sql')
11305
+ * ```
11306
+ */
8078
11307
  async computeChecksum(filepath) {
8079
- const absolutePath = path6.isAbsolute(filepath) ? filepath : path6.join(this.#projectRoot, filepath);
11308
+ const absolutePath = path7.isAbsolute(filepath) ? filepath : path7.join(this.#state.projectRoot, filepath);
8080
11309
  return computeChecksum(absolutePath);
8081
11310
  }
11311
+ /**
11312
+ * Tests if the connection can be established.
11313
+ *
11314
+ * @example
11315
+ * ```typescript
11316
+ * const result = await ctx.noorm.testConnection()
11317
+ * ```
11318
+ */
8082
11319
  async testConnection() {
8083
- return testConnection(this.#config.connection);
11320
+ return testConnection(this.#state.config.connection);
11321
+ }
11322
+ // ─────────────────────────────────────────────────────────
11323
+ // Transfer
11324
+ // ─────────────────────────────────────────────────────────
11325
+ /**
11326
+ * Transfer data from this context's database to a destination context.
11327
+ *
11328
+ * Both contexts must be connected. Uses each context's config for
11329
+ * connection management.
11330
+ *
11331
+ * @example
11332
+ * ```typescript
11333
+ * const [result, err] = await source.noorm.transferTo(dest, {
11334
+ * tables: ['users', 'posts'],
11335
+ * onConflict: 'skip',
11336
+ * })
11337
+ * ```
11338
+ */
11339
+ async transferTo(destConfig, options) {
11340
+ return transferData(this.#state.config, destConfig, options);
11341
+ }
11342
+ /**
11343
+ * Generate a transfer plan without executing.
11344
+ *
11345
+ * @example
11346
+ * ```typescript
11347
+ * const [plan, err] = await source.noorm.transferPlan(destConfig)
11348
+ * ```
11349
+ */
11350
+ async transferPlan(destConfig, options) {
11351
+ return getTransferPlan(this.#state.config, destConfig, options);
11352
+ }
11353
+ // ─────────────────────────────────────────────────────────
11354
+ // DT File Operations
11355
+ // ─────────────────────────────────────────────────────────
11356
+ /**
11357
+ * Export a table to a .dt file.
11358
+ *
11359
+ * @example
11360
+ * ```typescript
11361
+ * const [result, err] = await ctx.noorm.exportTable('users', './exports/users.dtz')
11362
+ * ```
11363
+ */
11364
+ async exportTable(tableName, filepath, options) {
11365
+ return exportTable({
11366
+ db: this.#kysely,
11367
+ dialect: this.#dialect,
11368
+ tableName,
11369
+ filepath,
11370
+ schema: options?.schema,
11371
+ passphrase: options?.passphrase,
11372
+ batchSize: options?.batchSize
11373
+ });
11374
+ }
11375
+ /**
11376
+ * Import a .dt file into the connected database.
11377
+ *
11378
+ * @example
11379
+ * ```typescript
11380
+ * const [result, err] = await ctx.noorm.importFile('./exports/users.dtz', {
11381
+ * onConflict: 'skip',
11382
+ * })
11383
+ * ```
11384
+ */
11385
+ async importFile(filepath, options) {
11386
+ return importDtFile({
11387
+ filepath,
11388
+ db: this.#kysely,
11389
+ dialect: this.#dialect,
11390
+ passphrase: options?.passphrase,
11391
+ batchSize: options?.batchSize,
11392
+ onConflict: options?.onConflict,
11393
+ truncate: options?.truncate
11394
+ });
8084
11395
  }
8085
11396
  // ─────────────────────────────────────────────────────────
8086
11397
  // Private Helpers
8087
11398
  // ─────────────────────────────────────────────────────────
8088
11399
  #createRunContext() {
8089
- const state = getStateManager(this.#projectRoot);
11400
+ const state = getStateManager(this.#state.projectRoot);
8090
11401
  return {
8091
- db: this.kysely,
8092
- configName: this.#config.name,
8093
- identity: this.#identity,
8094
- projectRoot: this.#projectRoot,
8095
- config: this.#config,
8096
- secrets: state.getAllSecrets(this.#config.name),
11402
+ db: this.#kysely,
11403
+ configName: this.#state.config.name,
11404
+ identity: this.#state.identity,
11405
+ projectRoot: this.#state.projectRoot,
11406
+ config: this.#state.config,
11407
+ secrets: state.getAllSecrets(this.#state.config.name),
8097
11408
  globalSecrets: state.getAllGlobalSecrets()
8098
11409
  };
8099
11410
  }
8100
11411
  #createChangeContext() {
8101
- const state = getStateManager(this.#projectRoot);
11412
+ const state = getStateManager(this.#state.projectRoot);
8102
11413
  return {
8103
- db: this.kysely,
8104
- configName: this.#config.name,
8105
- identity: this.#identity,
8106
- projectRoot: this.#projectRoot,
8107
- changesDir: path6.join(this.#projectRoot, this.#config.paths.changes),
8108
- sqlDir: path6.join(this.#projectRoot, this.#config.paths.sql),
8109
- config: this.#config,
8110
- secrets: state.getAllSecrets(this.#config.name),
11414
+ db: this.#kysely,
11415
+ configName: this.#state.config.name,
11416
+ identity: this.#state.identity,
11417
+ projectRoot: this.#state.projectRoot,
11418
+ changesDir: path7.join(this.#state.projectRoot, this.#state.config.paths.changes),
11419
+ sqlDir: path7.join(this.#state.projectRoot, this.#state.config.paths.sql),
11420
+ config: this.#state.config,
11421
+ secrets: state.getAllSecrets(this.#state.config.name),
8111
11422
  globalSecrets: state.getAllGlobalSecrets()
8112
11423
  };
8113
11424
  }
8114
11425
  #getChangeManager() {
8115
- if (!this.#changeManager) {
8116
- this.#changeManager = new ChangeManager(this.#createChangeContext());
11426
+ if (!this.#state.changeManager) {
11427
+ this.#state.changeManager = new ChangeManager(this.#createChangeContext());
11428
+ }
11429
+ return this.#state.changeManager;
11430
+ }
11431
+ };
11432
+
11433
+ // src/sdk/context.ts
11434
+ var Context = class {
11435
+ #state;
11436
+ #noorm = null;
11437
+ constructor(config, settings, identity, options, projectRoot) {
11438
+ this.#state = {
11439
+ connection: null,
11440
+ config,
11441
+ settings,
11442
+ identity,
11443
+ options,
11444
+ projectRoot,
11445
+ changeManager: null
11446
+ };
11447
+ }
11448
+ // ─────────────────────────────────────────────────────────
11449
+ // Read-only Properties
11450
+ // ─────────────────────────────────────────────────────────
11451
+ get dialect() {
11452
+ return this.#state.config.connection.dialect;
11453
+ }
11454
+ get connected() {
11455
+ return this.#state.connection !== null;
11456
+ }
11457
+ get kysely() {
11458
+ if (!this.#state.connection) {
11459
+ throw new Error("Not connected. Call connect() first.");
11460
+ }
11461
+ return this.#state.connection.db;
11462
+ }
11463
+ // ─────────────────────────────────────────────────────────
11464
+ // Noorm Namespace
11465
+ // ─────────────────────────────────────────────────────────
11466
+ /**
11467
+ * Noorm management operations.
11468
+ *
11469
+ * Lazily instantiated on first access. Returns the same instance
11470
+ * on repeated access (singleton per Context).
11471
+ *
11472
+ * @example
11473
+ * ```typescript
11474
+ * await ctx.noorm.build()
11475
+ * await ctx.noorm.fastForward()
11476
+ * const tables = await ctx.noorm.listTables()
11477
+ * ```
11478
+ */
11479
+ get noorm() {
11480
+ if (!this.#noorm) {
11481
+ this.#noorm = new NoormOps(this.#state);
11482
+ }
11483
+ return this.#noorm;
11484
+ }
11485
+ // ─────────────────────────────────────────────────────────
11486
+ // Lifecycle
11487
+ // ─────────────────────────────────────────────────────────
11488
+ async connect() {
11489
+ if (this.#state.connection) return;
11490
+ this.#state.connection = await createConnection(
11491
+ this.#state.config.connection,
11492
+ this.#state.config.name
11493
+ );
11494
+ }
11495
+ async disconnect() {
11496
+ if (!this.#state.connection) return;
11497
+ await this.#state.connection.destroy();
11498
+ this.#state.connection = null;
11499
+ this.#state.changeManager = null;
11500
+ }
11501
+ // ─────────────────────────────────────────────────────────
11502
+ // Transactions
11503
+ // ─────────────────────────────────────────────────────────
11504
+ /**
11505
+ * Execute operations within a database transaction.
11506
+ *
11507
+ * The callback receives a full Kysely Transaction instance with
11508
+ * query builder, `sql` template literal, and all Kysely features.
11509
+ *
11510
+ * @example
11511
+ * ```typescript
11512
+ * await ctx.transaction(async (trx) => {
11513
+ * await trx
11514
+ * .updateTable('accounts')
11515
+ * .set({ balance: sql`balance - ${amount}` })
11516
+ * .where('id', '=', fromId)
11517
+ * .execute();
11518
+ * await trx
11519
+ * .updateTable('accounts')
11520
+ * .set({ balance: sql`balance + ${amount}` })
11521
+ * .where('id', '=', toId)
11522
+ * .execute();
11523
+ * });
11524
+ * ```
11525
+ */
11526
+ async transaction(fn) {
11527
+ return this.kysely.transaction().execute(fn);
11528
+ }
11529
+ // ─────────────────────────────────────────────────────────
11530
+ // Stored Procedures & Functions
11531
+ // ─────────────────────────────────────────────────────────
11532
+ /**
11533
+ * Call a stored procedure and return the result set.
11534
+ *
11535
+ * Generates dialect-specific SQL: EXEC (MSSQL), CALL (PG/MySQL).
11536
+ * Named params use dialect-appropriate syntax; MySQL falls back
11537
+ * to positional. SQLite throws — no procedure support.
11538
+ *
11539
+ * @example
11540
+ * ```typescript
11541
+ * // Named params
11542
+ * const users = await ctx.proc<User>('get_users', { department_id: 1 });
11543
+ *
11544
+ * // Positional params
11545
+ * await ctx.proc('simple_proc', [42, 'hello']);
11546
+ *
11547
+ * // No params
11548
+ * await ctx.proc('refresh_cache');
11549
+ * ```
11550
+ */
11551
+ async proc(name, ...args) {
11552
+ if (this.dialect === "sqlite") {
11553
+ throw new Error("SQLite does not support stored procedures.");
11554
+ }
11555
+ const params = args[0];
11556
+ const query = buildProcCall(this.dialect, name, params);
11557
+ const result = await query.execute(this.kysely);
11558
+ return result.rows ?? [];
11559
+ }
11560
+ /**
11561
+ * Call a database function and return the scalar result.
11562
+ *
11563
+ * Generates SELECT name(...) AS column. Named params only on PG;
11564
+ * other dialects fall back to positional. SQLite throws.
11565
+ *
11566
+ * @example
11567
+ * ```typescript
11568
+ * // Named params + column alias
11569
+ * const result = await ctx.func<{ total: number }>('calc_total', { order_id: 42 }, 'total');
11570
+ *
11571
+ * // Positional params + column alias
11572
+ * const sum = await ctx.func<{ result: number }>('add_numbers', [1, 2], 'result');
11573
+ *
11574
+ * // No params — just column alias
11575
+ * const ver = await ctx.func<{ v: string }>('get_version', 'v');
11576
+ * ```
11577
+ */
11578
+ async func(name, ...args) {
11579
+ if (this.dialect === "sqlite") {
11580
+ throw new Error("SQLite does not support database function calls.");
8117
11581
  }
8118
- return this.#changeManager;
11582
+ const hasParams = !(args.length === 1 && typeof args[0] === "string");
11583
+ const params = hasParams ? args[0] : void 0;
11584
+ const column = hasParams ? args[1] : args[0];
11585
+ const query = buildFuncCall(this.dialect, name, column, params);
11586
+ const result = await query.execute(this.kysely);
11587
+ return result.rows?.[0] ?? null;
8119
11588
  }
8120
11589
  };
8121
11590
 
@@ -8145,6 +11614,6 @@ async function createContext(options = {}) {
8145
11614
  return new Context(config, settings, identity, options, projectRoot);
8146
11615
  }
8147
11616
 
8148
- export { Context, LockAcquireError, LockExpiredError, ProtectedConfigError, RequireTestError, createContext };
11617
+ export { Context, LockAcquireError, LockExpiredError, NoormOps, ProtectedConfigError, RequireTestError, createContext };
8149
11618
  //# sourceMappingURL=index.js.map
8150
11619
  //# sourceMappingURL=index.js.map