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