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