uql-orm 0.7.2 → 0.7.3
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/CHANGELOG.md +18 -11
- package/dist/browser/{cc-BEf4wTUm.js → cc-BVTDOeZ_.js} +54 -23
- package/dist/browser/cc-BVTDOeZ_.js.map +1 -0
- package/dist/browser/uql-browser.min.js +1766 -86
- package/dist/browser/uql-browser.min.js.map +1 -1
- package/dist/bunSql/bunSql.util.d.ts.map +1 -1
- package/dist/bunSql/bunSql.util.js +15 -8
- package/dist/bunSql/bunSql.util.js.map +1 -1
- package/dist/migrate/sync/schemaSync.js +1 -1
- package/dist/migrate/sync/schemaSync.js.map +1 -1
- package/package.json +3 -3
- package/dist/browser/cc-BEf4wTUm.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as _apply_decs_2203_r } from './cc-
|
|
1
|
+
import { _ as _apply_decs_2203_r } from './cc-BVTDOeZ_.js';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { Id, Field, ManyToOne, Entity, OneToOne, OneToMany, ManyToMany } from '../entity/index.js';
|
|
4
4
|
import '../type/index.js';
|
|
@@ -2160,15 +2160,30 @@ function findPlugin(plugins, val) {
|
|
|
2160
2160
|
return null;
|
|
2161
2161
|
}
|
|
2162
2162
|
function printer(val, config, indentation, depth, refs, hasCalledToJSON) {
|
|
2163
|
+
let result;
|
|
2163
2164
|
const plugin = findPlugin(config.plugins, val);
|
|
2164
2165
|
if (plugin !== null) {
|
|
2165
|
-
|
|
2166
|
+
result = printPlugin(plugin, val, config, indentation, depth, refs);
|
|
2167
|
+
} else {
|
|
2168
|
+
const basicResult = printBasicValue(val, config.printFunctionName, config.escapeRegex, config.escapeString);
|
|
2169
|
+
if (basicResult !== null) {
|
|
2170
|
+
result = basicResult;
|
|
2171
|
+
} else {
|
|
2172
|
+
result = printComplexValue(val, config, indentation, depth, refs, hasCalledToJSON);
|
|
2173
|
+
}
|
|
2166
2174
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2175
|
+
// Per-depth output budget (inspired by Node's util.inspect).
|
|
2176
|
+
// Each depth level tracks output independently, so nested results
|
|
2177
|
+
// don't inflate a single counter (which would undercount by ~Nx for
|
|
2178
|
+
// N levels of nesting). Nodes at the same depth produce disjoint spans
|
|
2179
|
+
// in the output string, so each bucket accurately reflects output at
|
|
2180
|
+
// that level. Total output is bounded by maxDepth × maxOutputLength.
|
|
2181
|
+
config._outputLengthPerDepth[depth] ??= 0;
|
|
2182
|
+
config._outputLengthPerDepth[depth] += result.length;
|
|
2183
|
+
if (config._outputLengthPerDepth[depth] > config.maxOutputLength) {
|
|
2184
|
+
config.maxDepth = 0;
|
|
2170
2185
|
}
|
|
2171
|
-
return
|
|
2186
|
+
return result;
|
|
2172
2187
|
}
|
|
2173
2188
|
const DEFAULT_THEME = {
|
|
2174
2189
|
comment: "gray",
|
|
@@ -2186,6 +2201,7 @@ const DEFAULT_OPTIONS = {
|
|
|
2186
2201
|
highlight: false,
|
|
2187
2202
|
indent: 2,
|
|
2188
2203
|
maxDepth: Number.POSITIVE_INFINITY,
|
|
2204
|
+
maxOutputLength: 1e6,
|
|
2189
2205
|
maxWidth: Number.POSITIVE_INFINITY,
|
|
2190
2206
|
min: false,
|
|
2191
2207
|
plugins: [],
|
|
@@ -2250,7 +2266,9 @@ function getConfig(options) {
|
|
|
2250
2266
|
printFunctionName: getPrintFunctionName(options),
|
|
2251
2267
|
printShadowRoot: options?.printShadowRoot ?? true,
|
|
2252
2268
|
spacingInner: options?.min ? " " : "\n",
|
|
2253
|
-
spacingOuter: options?.min ? "" : "\n"
|
|
2269
|
+
spacingOuter: options?.min ? "" : "\n",
|
|
2270
|
+
maxOutputLength: options?.maxOutputLength ?? DEFAULT_OPTIONS.maxOutputLength,
|
|
2271
|
+
_outputLengthPerDepth: []
|
|
2254
2272
|
};
|
|
2255
2273
|
}
|
|
2256
2274
|
function createIndent(indent) {
|
|
@@ -3479,79 +3497,6 @@ function parseSingleV8Stack(raw) {
|
|
|
3479
3497
|
};
|
|
3480
3498
|
}
|
|
3481
3499
|
|
|
3482
|
-
const kChainableContext = Symbol("kChainableContext");
|
|
3483
|
-
function getChainableContext(chainable) {
|
|
3484
|
-
return chainable?.[kChainableContext];
|
|
3485
|
-
}
|
|
3486
|
-
function createChainable(keys, fn, context) {
|
|
3487
|
-
function create(context) {
|
|
3488
|
-
const chain = function(...args) {
|
|
3489
|
-
return fn.apply(context, args);
|
|
3490
|
-
};
|
|
3491
|
-
Object.assign(chain, fn);
|
|
3492
|
-
Object.defineProperty(chain, kChainableContext, {
|
|
3493
|
-
value: {
|
|
3494
|
-
withContext: ()=>chain.bind(context),
|
|
3495
|
-
getFixtures: ()=>context.fixtures,
|
|
3496
|
-
setContext: (key, value)=>{
|
|
3497
|
-
context[key] = value;
|
|
3498
|
-
},
|
|
3499
|
-
mergeContext: (ctx)=>{
|
|
3500
|
-
Object.assign(context, ctx);
|
|
3501
|
-
}
|
|
3502
|
-
},
|
|
3503
|
-
enumerable: false
|
|
3504
|
-
});
|
|
3505
|
-
for (const key of keys){
|
|
3506
|
-
Object.defineProperty(chain, key, {
|
|
3507
|
-
get () {
|
|
3508
|
-
return create({
|
|
3509
|
-
...context,
|
|
3510
|
-
[key]: true
|
|
3511
|
-
});
|
|
3512
|
-
}
|
|
3513
|
-
});
|
|
3514
|
-
}
|
|
3515
|
-
return chain;
|
|
3516
|
-
}
|
|
3517
|
-
const chain = create(context ?? {});
|
|
3518
|
-
Object.defineProperty(chain, "fn", {
|
|
3519
|
-
value: fn,
|
|
3520
|
-
enumerable: false
|
|
3521
|
-
});
|
|
3522
|
-
return chain;
|
|
3523
|
-
}
|
|
3524
|
-
function findTestFileStackTrace(testFilePath, error) {
|
|
3525
|
-
// first line is the error message
|
|
3526
|
-
const lines = error.split("\n").slice(1);
|
|
3527
|
-
for (const line of lines){
|
|
3528
|
-
const stack = parseSingleStack(line);
|
|
3529
|
-
if (stack && stack.file === testFilePath) {
|
|
3530
|
-
return stack;
|
|
3531
|
-
}
|
|
3532
|
-
}
|
|
3533
|
-
}
|
|
3534
|
-
function validateTags(config, tags) {
|
|
3535
|
-
if (!config.strictTags) {
|
|
3536
|
-
return;
|
|
3537
|
-
}
|
|
3538
|
-
const availableTags = new Set(config.tags.map((tag)=>tag.name));
|
|
3539
|
-
for (const tag of tags){
|
|
3540
|
-
if (!availableTags.has(tag)) {
|
|
3541
|
-
throw createNoTagsError(config.tags, tag);
|
|
3542
|
-
}
|
|
3543
|
-
}
|
|
3544
|
-
}
|
|
3545
|
-
function createNoTagsError(availableTags, tag, prefix = "tag") {
|
|
3546
|
-
if (!availableTags.length) {
|
|
3547
|
-
throw new Error(`The Vitest config does't define any "tags", cannot apply "${tag}" ${prefix} for this test. See: https://vitest.dev/guide/test-tags`);
|
|
3548
|
-
}
|
|
3549
|
-
throw new Error(`The ${prefix} "${tag}" is not defined in the configuration. Available tags are:\n${availableTags.map((t)=>`- ${t.name}${t.description ? `: ${t.description}` : ""}`).join("\n")}`);
|
|
3550
|
-
}
|
|
3551
|
-
function createTaskName(names, separator = " > ") {
|
|
3552
|
-
return names.filter((name)=>name !== undefined).join(separator);
|
|
3553
|
-
}
|
|
3554
|
-
|
|
3555
3500
|
class PendingError extends Error {
|
|
3556
3501
|
constructor(message, task, note){
|
|
3557
3502
|
super(message), this.code = "VITEST_PENDING";
|
|
@@ -3594,6 +3539,7 @@ function setHooks(key, hooks) {
|
|
|
3594
3539
|
function getHooks(key) {
|
|
3595
3540
|
return hooksMap.get(key);
|
|
3596
3541
|
}
|
|
3542
|
+
const FIXTURE_STACK_TRACE_KEY = Symbol.for("VITEST_FIXTURE_STACK_TRACE");
|
|
3597
3543
|
class TestFixtures {
|
|
3598
3544
|
static{
|
|
3599
3545
|
this._definitions = [];
|
|
@@ -3636,6 +3582,9 @@ class TestFixtures {
|
|
|
3636
3582
|
static getFileContexts(file) {
|
|
3637
3583
|
return TestFixtures._definitions.map((f)=>f.getFileContext(file));
|
|
3638
3584
|
}
|
|
3585
|
+
static isFixtureOptions(obj) {
|
|
3586
|
+
return isObject(obj) && Object.keys(obj).some((key)=>TestFixtures._fixtureOptionKeys.includes(key));
|
|
3587
|
+
}
|
|
3639
3588
|
constructor(registrations){
|
|
3640
3589
|
this._overrides = new WeakMap();
|
|
3641
3590
|
this._registrations = registrations ?? new Map();
|
|
@@ -3697,7 +3646,7 @@ class TestFixtures {
|
|
|
3697
3646
|
let options;
|
|
3698
3647
|
let value;
|
|
3699
3648
|
let _options;
|
|
3700
|
-
if (Array.isArray(fn) && fn.length >= 2 &&
|
|
3649
|
+
if (Array.isArray(fn) && fn.length >= 2 && TestFixtures.isFixtureOptions(fn[1])) {
|
|
3701
3650
|
_options = fn[1];
|
|
3702
3651
|
options = {
|
|
3703
3652
|
auto: _options.auto ?? false,
|
|
@@ -3745,6 +3694,11 @@ class TestFixtures {
|
|
|
3745
3694
|
deps,
|
|
3746
3695
|
parent
|
|
3747
3696
|
};
|
|
3697
|
+
if (isFixtureFunction(value)) {
|
|
3698
|
+
Object.assign(value, {
|
|
3699
|
+
[FIXTURE_STACK_TRACE_KEY]: new Error("STACK_TRACE_ERROR")
|
|
3700
|
+
});
|
|
3701
|
+
}
|
|
3748
3702
|
registrations.set(name, item);
|
|
3749
3703
|
if (item.scope === "worker" && (runner.pool === "vmThreads" || runner.pool === "vmForks")) {
|
|
3750
3704
|
item.scope = "file";
|
|
@@ -3870,7 +3824,7 @@ function resolveTestFixtureValue(fixture, context, cleanupFnArray) {
|
|
|
3870
3824
|
if (!isFixtureFunction(fixture.value)) {
|
|
3871
3825
|
return fixture.value;
|
|
3872
3826
|
}
|
|
3873
|
-
return resolveFixtureFunction(fixture.value, context, cleanupFnArray);
|
|
3827
|
+
return resolveFixtureFunction(fixture.value, fixture.name, context, cleanupFnArray);
|
|
3874
3828
|
}
|
|
3875
3829
|
const scopedFixturePromiseCache = new WeakMap();
|
|
3876
3830
|
async function resolveScopeFixtureValue(fixtures, suite, fixture) {
|
|
@@ -3891,7 +3845,7 @@ async function resolveScopeFixtureValue(fixtures, suite, fixture) {
|
|
|
3891
3845
|
cleanupFnArrayMap.set(fixtureContext, []);
|
|
3892
3846
|
}
|
|
3893
3847
|
const cleanupFnFileArray = cleanupFnArrayMap.get(fixtureContext);
|
|
3894
|
-
const promise = resolveFixtureFunction(fixture.value, fixture.scope === "file" ? {
|
|
3848
|
+
const promise = resolveFixtureFunction(fixture.value, fixture.name, fixture.scope === "file" ? {
|
|
3895
3849
|
...workerContext,
|
|
3896
3850
|
...fileContext
|
|
3897
3851
|
} : fixtureContext, cleanupFnFileArray).then((value)=>{
|
|
@@ -3902,9 +3856,10 @@ async function resolveScopeFixtureValue(fixtures, suite, fixture) {
|
|
|
3902
3856
|
scopedFixturePromiseCache.set(fixture, promise);
|
|
3903
3857
|
return promise;
|
|
3904
3858
|
}
|
|
3905
|
-
async function resolveFixtureFunction(fixtureFn, context, cleanupFnArray) {
|
|
3859
|
+
async function resolveFixtureFunction(fixtureFn, fixtureName, context, cleanupFnArray) {
|
|
3906
3860
|
// wait for `use` call to extract fixture value
|
|
3907
3861
|
const useFnArgPromise = createDefer();
|
|
3862
|
+
const stackTraceError = FIXTURE_STACK_TRACE_KEY in fixtureFn && fixtureFn[FIXTURE_STACK_TRACE_KEY] instanceof Error ? fixtureFn[FIXTURE_STACK_TRACE_KEY] : undefined;
|
|
3908
3863
|
let isUseFnArgResolved = false;
|
|
3909
3864
|
const fixtureReturn = fixtureFn(context, async (useFnArg)=>{
|
|
3910
3865
|
// extract `use` argument
|
|
@@ -3919,6 +3874,15 @@ async function resolveFixtureFunction(fixtureFn, context, cleanupFnArray) {
|
|
|
3919
3874
|
await fixtureReturn;
|
|
3920
3875
|
});
|
|
3921
3876
|
await useReturnPromise;
|
|
3877
|
+
}).then(()=>{
|
|
3878
|
+
// fixture returned without calling use()
|
|
3879
|
+
if (!isUseFnArgResolved) {
|
|
3880
|
+
const error = new Error(`Fixture "${fixtureName}" returned without calling "use". Make sure to call "use" in every code path of the fixture function.`);
|
|
3881
|
+
if (stackTraceError?.stack) {
|
|
3882
|
+
error.stack = error.message + stackTraceError.stack.replace(stackTraceError.message, "");
|
|
3883
|
+
}
|
|
3884
|
+
useFnArgPromise.reject(error);
|
|
3885
|
+
}
|
|
3922
3886
|
}).catch((e)=>{
|
|
3923
3887
|
// treat fixture setup error as test failure
|
|
3924
3888
|
if (!isUseFnArgResolved) {
|
|
@@ -4055,6 +4019,48 @@ function splitByComma(s) {
|
|
|
4055
4019
|
}
|
|
4056
4020
|
return result;
|
|
4057
4021
|
}
|
|
4022
|
+
const kChainableContext = Symbol("kChainableContext");
|
|
4023
|
+
function getChainableContext(chainable) {
|
|
4024
|
+
return chainable?.[kChainableContext];
|
|
4025
|
+
}
|
|
4026
|
+
function createChainable(keys, fn, context) {
|
|
4027
|
+
function create(context) {
|
|
4028
|
+
const chain = function(...args) {
|
|
4029
|
+
return fn.apply(context, args);
|
|
4030
|
+
};
|
|
4031
|
+
Object.assign(chain, fn);
|
|
4032
|
+
Object.defineProperty(chain, kChainableContext, {
|
|
4033
|
+
value: {
|
|
4034
|
+
withContext: ()=>chain.bind(context),
|
|
4035
|
+
getFixtures: ()=>context.fixtures,
|
|
4036
|
+
setContext: (key, value)=>{
|
|
4037
|
+
context[key] = value;
|
|
4038
|
+
},
|
|
4039
|
+
mergeContext: (ctx)=>{
|
|
4040
|
+
Object.assign(context, ctx);
|
|
4041
|
+
}
|
|
4042
|
+
},
|
|
4043
|
+
enumerable: false
|
|
4044
|
+
});
|
|
4045
|
+
for (const key of keys){
|
|
4046
|
+
Object.defineProperty(chain, key, {
|
|
4047
|
+
get () {
|
|
4048
|
+
return create({
|
|
4049
|
+
...context,
|
|
4050
|
+
[key]: true
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
});
|
|
4054
|
+
}
|
|
4055
|
+
return chain;
|
|
4056
|
+
}
|
|
4057
|
+
const chain = create(context ?? {});
|
|
4058
|
+
Object.defineProperty(chain, "fn", {
|
|
4059
|
+
value: fn,
|
|
4060
|
+
enumerable: false
|
|
4061
|
+
});
|
|
4062
|
+
return chain;
|
|
4063
|
+
}
|
|
4058
4064
|
function getDefaultHookTimeout() {
|
|
4059
4065
|
return getRunner().config.hookTimeout;
|
|
4060
4066
|
}
|
|
@@ -4278,6 +4284,36 @@ function withSuiteFixtures(suiteHook, fn, context, stackTraceError, contextIndex
|
|
|
4278
4284
|
return fixtured();
|
|
4279
4285
|
};
|
|
4280
4286
|
}
|
|
4287
|
+
function findTestFileStackTrace(testFilePath, error) {
|
|
4288
|
+
// first line is the error message
|
|
4289
|
+
const lines = error.split("\n").slice(1);
|
|
4290
|
+
for (const line of lines){
|
|
4291
|
+
const stack = parseSingleStack(line);
|
|
4292
|
+
if (stack && stack.file === testFilePath) {
|
|
4293
|
+
return stack;
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
function validateTags(config, tags) {
|
|
4298
|
+
if (!config.strictTags) {
|
|
4299
|
+
return;
|
|
4300
|
+
}
|
|
4301
|
+
const availableTags = new Set(config.tags.map((tag)=>tag.name));
|
|
4302
|
+
for (const tag of tags){
|
|
4303
|
+
if (!availableTags.has(tag)) {
|
|
4304
|
+
throw createNoTagsError(config.tags, tag);
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
function createNoTagsError(availableTags, tag, prefix = "tag") {
|
|
4309
|
+
if (!availableTags.length) {
|
|
4310
|
+
throw new Error(`The Vitest config does't define any "tags", cannot apply "${tag}" ${prefix} for this test. See: https://vitest.dev/guide/test-tags`);
|
|
4311
|
+
}
|
|
4312
|
+
throw new Error(`The ${prefix} "${tag}" is not defined in the configuration. Available tags are:\n${availableTags.map((t)=>`- ${t.name}${t.description ? `: ${t.description}` : ""}`).join("\n")}`);
|
|
4313
|
+
}
|
|
4314
|
+
function createTaskName(names, separator = " > ") {
|
|
4315
|
+
return names.filter((name)=>name !== undefined).join(separator);
|
|
4316
|
+
}
|
|
4281
4317
|
/**
|
|
4282
4318
|
* Creates a suite of tests, allowing for grouping and hierarchical organization of tests.
|
|
4283
4319
|
* Suites can contain both tests and other suites, enabling complex test structures.
|
|
@@ -4846,7 +4882,7 @@ function createTaskCollector(fn) {
|
|
|
4846
4882
|
} else {
|
|
4847
4883
|
// (name, value) or (name, fn)
|
|
4848
4884
|
// Check if optionsOrFn looks like fixture options (has scope or auto)
|
|
4849
|
-
if (optionsOrFn !== null && typeof optionsOrFn === "object" && !Array.isArray(optionsOrFn) && (
|
|
4885
|
+
if (optionsOrFn !== null && typeof optionsOrFn === "object" && !Array.isArray(optionsOrFn) && TestFixtures.isFixtureOptions(optionsOrFn)) {
|
|
4850
4886
|
// (name, options) with no value - treat as empty object fixture
|
|
4851
4887
|
fixtureOptions = optionsOrFn;
|
|
4852
4888
|
fixtureValue = {};
|
|
@@ -5207,7 +5243,7 @@ function sendTasksUpdate(runner) {
|
|
|
5207
5243
|
const p = runner.onTaskUpdate?.(taskPacks, eventsPacks);
|
|
5208
5244
|
if (p) {
|
|
5209
5245
|
pendingTasksUpdates.push(p);
|
|
5210
|
-
// remove successful promise to not grow array
|
|
5246
|
+
// remove successful promise to not grow array indefinitely,
|
|
5211
5247
|
// but keep rejections so finishSendTasksUpdate can handle them
|
|
5212
5248
|
p.then(()=>pendingTasksUpdates.splice(pendingTasksUpdates.indexOf(p), 1), ()=>{});
|
|
5213
5249
|
}
|
|
@@ -5418,3 +5454,1647 @@ const hooks = {
|
|
|
5418
5454
|
|
|
5419
5455
|
export { BaseEntity, _Company as Company, _InventoryAdjustment as InventoryAdjustment, _Item as Item, _ItemAdjustment as ItemAdjustment, _ItemTag as ItemTag, _LedgerAccount as LedgerAccount, _MeasureUnit as MeasureUnit, _MeasureUnitCategory as MeasureUnitCategory, _Profile as Profile, _Storehouse as Storehouse, _Tag as Tag, _Tax as Tax, _TaxCategory as TaxCategory, _User as User, _UserWithNonUpdatableId as UserWithNonUpdatableId, _VectorItem as VectorItem, clearTables, createSpec, createTables, dropTables };
|
|
5420
5456
|
//# sourceMappingURL=uql-browser.min.js.map
|
|
5457
|
+
ADE';
|
|
5458
|
+
case 'SET NULL':
|
|
5459
|
+
return 'SET NULL';
|
|
5460
|
+
case 'RESTRICT':
|
|
5461
|
+
return 'RESTRICT';
|
|
5462
|
+
case 'NO ACTION':
|
|
5463
|
+
return 'NO ACTION';
|
|
5464
|
+
default:
|
|
5465
|
+
return undefined;
|
|
5466
|
+
}
|
|
5467
|
+
}
|
|
5468
|
+
/**
|
|
5469
|
+
* Convert bigint/null values to number safely.
|
|
5470
|
+
*/ toNumber(value) {
|
|
5471
|
+
if (value == null || value === '') {
|
|
5472
|
+
return undefined;
|
|
5473
|
+
}
|
|
5474
|
+
return Number(value);
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
/**
|
|
5479
|
+
* MongoDB schema introspector.
|
|
5480
|
+
* MongoDB doesn't have a fixed schema, so this primarily focuses on collections and indexes.
|
|
5481
|
+
*/ class MongoSchemaIntrospector {
|
|
5482
|
+
constructor(pool){
|
|
5483
|
+
this.pool = pool;
|
|
5484
|
+
}
|
|
5485
|
+
async introspect() {
|
|
5486
|
+
const tableNames = await this.getTableNames();
|
|
5487
|
+
const ast = new SchemaAST();
|
|
5488
|
+
for (const name of tableNames){
|
|
5489
|
+
const schema = await this.getTableSchema(name);
|
|
5490
|
+
if (schema) {
|
|
5491
|
+
const columns = new Map();
|
|
5492
|
+
const table = {
|
|
5493
|
+
name,
|
|
5494
|
+
columns,
|
|
5495
|
+
primaryKey: [],
|
|
5496
|
+
indexes: [],
|
|
5497
|
+
schema: ast,
|
|
5498
|
+
incomingRelations: [],
|
|
5499
|
+
outgoingRelations: []
|
|
5500
|
+
};
|
|
5501
|
+
if (schema.indexes) {
|
|
5502
|
+
for (const idx of schema.indexes){
|
|
5503
|
+
const indexColumns = [];
|
|
5504
|
+
for (const colName of idx.columns){
|
|
5505
|
+
let column = columns.get(colName);
|
|
5506
|
+
if (!column) {
|
|
5507
|
+
column = {
|
|
5508
|
+
name: colName,
|
|
5509
|
+
type: {
|
|
5510
|
+
category: 'string'
|
|
5511
|
+
},
|
|
5512
|
+
nullable: true,
|
|
5513
|
+
isPrimaryKey: false,
|
|
5514
|
+
isAutoIncrement: false,
|
|
5515
|
+
isUnique: false,
|
|
5516
|
+
table,
|
|
5517
|
+
referencedBy: []
|
|
5518
|
+
};
|
|
5519
|
+
columns.set(colName, column);
|
|
5520
|
+
}
|
|
5521
|
+
indexColumns.push(column);
|
|
5522
|
+
}
|
|
5523
|
+
table.indexes.push({
|
|
5524
|
+
name: idx.name,
|
|
5525
|
+
table,
|
|
5526
|
+
columns: indexColumns,
|
|
5527
|
+
unique: idx.unique
|
|
5528
|
+
});
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
ast.addTable(table);
|
|
5532
|
+
}
|
|
5533
|
+
}
|
|
5534
|
+
return ast;
|
|
5535
|
+
}
|
|
5536
|
+
async getTableSchema(tableName) {
|
|
5537
|
+
const querier = await this.pool.getQuerier();
|
|
5538
|
+
try {
|
|
5539
|
+
const { db } = querier;
|
|
5540
|
+
const collections = await db.listCollections({
|
|
5541
|
+
name: tableName
|
|
5542
|
+
}).toArray();
|
|
5543
|
+
if (collections.length === 0) {
|
|
5544
|
+
return undefined;
|
|
5545
|
+
}
|
|
5546
|
+
// MongoDB doesn't have a fixed schema, but we can look at the indexes
|
|
5547
|
+
const indexes = await db.collection(tableName).indexes();
|
|
5548
|
+
return {
|
|
5549
|
+
name: tableName,
|
|
5550
|
+
columns: [],
|
|
5551
|
+
indexes: indexes.map((idx)=>({
|
|
5552
|
+
name: idx.name ?? Object.keys(idx.key).join('_'),
|
|
5553
|
+
columns: Object.keys(idx.key),
|
|
5554
|
+
unique: !!idx.unique
|
|
5555
|
+
}))
|
|
5556
|
+
};
|
|
5557
|
+
} finally{
|
|
5558
|
+
await querier.release();
|
|
5559
|
+
}
|
|
5560
|
+
}
|
|
5561
|
+
async getTableNames() {
|
|
5562
|
+
const querier = await this.pool.getQuerier();
|
|
5563
|
+
try {
|
|
5564
|
+
const { db } = querier;
|
|
5565
|
+
const collections = await db.listCollections().toArray();
|
|
5566
|
+
return collections.map((c)=>c.name);
|
|
5567
|
+
} finally{
|
|
5568
|
+
await querier.release();
|
|
5569
|
+
}
|
|
5570
|
+
}
|
|
5571
|
+
async tableExists(tableName) {
|
|
5572
|
+
const names = await this.getTableNames();
|
|
5573
|
+
return names.includes(tableName);
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
|
|
5577
|
+
/**
|
|
5578
|
+
* MySQL/MariaDB schema introspector.
|
|
5579
|
+
* Works with both MySQL and MariaDB as they share the same information_schema structure.
|
|
5580
|
+
*/ class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
5581
|
+
constructor(pool){
|
|
5582
|
+
super('mysql');
|
|
5583
|
+
this.pool = pool;
|
|
5584
|
+
}
|
|
5585
|
+
// ============================================================================
|
|
5586
|
+
// SQL Queries (dialect-specific)
|
|
5587
|
+
// ============================================================================
|
|
5588
|
+
getTableNamesQuery() {
|
|
5589
|
+
return /*sql*/ `
|
|
5590
|
+
SELECT TABLE_NAME as table_name
|
|
5591
|
+
FROM information_schema.TABLES
|
|
5592
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
5593
|
+
AND TABLE_TYPE = 'BASE TABLE'
|
|
5594
|
+
ORDER BY TABLE_NAME
|
|
5595
|
+
`;
|
|
5596
|
+
}
|
|
5597
|
+
tableExistsQuery() {
|
|
5598
|
+
return /*sql*/ `
|
|
5599
|
+
SELECT COUNT(*) as count
|
|
5600
|
+
FROM information_schema.TABLES
|
|
5601
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
5602
|
+
AND TABLE_NAME = ?
|
|
5603
|
+
`;
|
|
5604
|
+
}
|
|
5605
|
+
parseTableExistsResult(results) {
|
|
5606
|
+
const row = results[0];
|
|
5607
|
+
if (row?.count !== undefined) {
|
|
5608
|
+
return (this.toNumber(row.count) ?? 0) > 0;
|
|
5609
|
+
}
|
|
5610
|
+
return false;
|
|
5611
|
+
}
|
|
5612
|
+
getColumnsQuery(_tableName) {
|
|
5613
|
+
return /*sql*/ `
|
|
5614
|
+
SELECT
|
|
5615
|
+
COLUMN_NAME as column_name,
|
|
5616
|
+
DATA_TYPE as data_type,
|
|
5617
|
+
COLUMN_TYPE as column_type,
|
|
5618
|
+
IS_NULLABLE as is_nullable,
|
|
5619
|
+
COLUMN_DEFAULT as column_default,
|
|
5620
|
+
CHARACTER_MAXIMUM_LENGTH as character_maximum_length,
|
|
5621
|
+
NUMERIC_PRECISION as numeric_precision,
|
|
5622
|
+
NUMERIC_SCALE as numeric_scale,
|
|
5623
|
+
COLUMN_KEY as column_key,
|
|
5624
|
+
EXTRA as extra,
|
|
5625
|
+
COLUMN_COMMENT as column_comment
|
|
5626
|
+
FROM information_schema.COLUMNS
|
|
5627
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
5628
|
+
AND TABLE_NAME = ?
|
|
5629
|
+
ORDER BY ORDINAL_POSITION
|
|
5630
|
+
`;
|
|
5631
|
+
}
|
|
5632
|
+
getIndexesQuery(_tableName) {
|
|
5633
|
+
return /*sql*/ `
|
|
5634
|
+
SELECT
|
|
5635
|
+
INDEX_NAME as index_name,
|
|
5636
|
+
GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as columns,
|
|
5637
|
+
NOT NON_UNIQUE as is_unique
|
|
5638
|
+
FROM information_schema.STATISTICS
|
|
5639
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
5640
|
+
AND TABLE_NAME = ?
|
|
5641
|
+
AND INDEX_NAME != 'PRIMARY'
|
|
5642
|
+
GROUP BY INDEX_NAME, NON_UNIQUE
|
|
5643
|
+
ORDER BY INDEX_NAME
|
|
5644
|
+
`;
|
|
5645
|
+
}
|
|
5646
|
+
getForeignKeysQuery(_tableName) {
|
|
5647
|
+
return /*sql*/ `
|
|
5648
|
+
SELECT
|
|
5649
|
+
kcu.CONSTRAINT_NAME as constraint_name,
|
|
5650
|
+
GROUP_CONCAT(kcu.COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION) as columns,
|
|
5651
|
+
kcu.REFERENCED_TABLE_NAME as referenced_table,
|
|
5652
|
+
GROUP_CONCAT(kcu.REFERENCED_COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION) as referenced_columns,
|
|
5653
|
+
rc.DELETE_RULE as delete_rule,
|
|
5654
|
+
rc.UPDATE_RULE as update_rule
|
|
5655
|
+
FROM information_schema.KEY_COLUMN_USAGE kcu
|
|
5656
|
+
JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
|
|
5657
|
+
ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
|
|
5658
|
+
AND kcu.TABLE_SCHEMA = rc.CONSTRAINT_SCHEMA
|
|
5659
|
+
WHERE kcu.TABLE_SCHEMA = DATABASE()
|
|
5660
|
+
AND kcu.TABLE_NAME = ?
|
|
5661
|
+
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
|
5662
|
+
GROUP BY kcu.CONSTRAINT_NAME, kcu.REFERENCED_TABLE_NAME, rc.DELETE_RULE, rc.UPDATE_RULE
|
|
5663
|
+
ORDER BY kcu.CONSTRAINT_NAME
|
|
5664
|
+
`;
|
|
5665
|
+
}
|
|
5666
|
+
getPrimaryKeyQuery(_tableName) {
|
|
5667
|
+
return /*sql*/ `
|
|
5668
|
+
SELECT COLUMN_NAME as column_name
|
|
5669
|
+
FROM information_schema.KEY_COLUMN_USAGE
|
|
5670
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
5671
|
+
AND TABLE_NAME = ?
|
|
5672
|
+
AND CONSTRAINT_NAME = 'PRIMARY'
|
|
5673
|
+
ORDER BY ORDINAL_POSITION
|
|
5674
|
+
`;
|
|
5675
|
+
}
|
|
5676
|
+
// ============================================================================
|
|
5677
|
+
// Internal Types
|
|
5678
|
+
// ============================================================================
|
|
5679
|
+
mapTableNameRow(row) {
|
|
5680
|
+
return row.table_name;
|
|
5681
|
+
}
|
|
5682
|
+
async mapColumnsResult(_querier, _tableName, results) {
|
|
5683
|
+
return results.map((row)=>({
|
|
5684
|
+
name: row.column_name,
|
|
5685
|
+
type: (row.column_type || '').toUpperCase(),
|
|
5686
|
+
nullable: row.is_nullable === 'YES',
|
|
5687
|
+
defaultValue: this.parseDefaultValue(row.column_default),
|
|
5688
|
+
isPrimaryKey: row.column_key === 'PRI',
|
|
5689
|
+
isAutoIncrement: (row.extra || '').toLowerCase().includes('auto_increment'),
|
|
5690
|
+
isUnique: row.column_key === 'UNI',
|
|
5691
|
+
length: this.toNumber(row.character_maximum_length),
|
|
5692
|
+
precision: this.toNumber(row.numeric_precision),
|
|
5693
|
+
scale: this.toNumber(row.numeric_scale),
|
|
5694
|
+
comment: row.column_comment || undefined
|
|
5695
|
+
}));
|
|
5696
|
+
}
|
|
5697
|
+
async mapIndexesResult(_querier, _tableName, results) {
|
|
5698
|
+
return results.map((row)=>({
|
|
5699
|
+
name: row.index_name,
|
|
5700
|
+
columns: (row.columns || '').split(','),
|
|
5701
|
+
unique: Boolean(row.is_unique)
|
|
5702
|
+
}));
|
|
5703
|
+
}
|
|
5704
|
+
async mapForeignKeysResult(_querier, _tableName, results) {
|
|
5705
|
+
return results.map((row)=>({
|
|
5706
|
+
name: row.constraint_name,
|
|
5707
|
+
columns: (row.columns || '').split(','),
|
|
5708
|
+
referencedTable: row.referenced_table,
|
|
5709
|
+
referencedColumns: (row.referenced_columns || '').split(','),
|
|
5710
|
+
onDelete: this.normalizeReferentialAction(row.delete_rule),
|
|
5711
|
+
onUpdate: this.normalizeReferentialAction(row.update_rule)
|
|
5712
|
+
}));
|
|
5713
|
+
}
|
|
5714
|
+
mapPrimaryKeyResult(results) {
|
|
5715
|
+
if (results.length === 0) {
|
|
5716
|
+
return undefined;
|
|
5717
|
+
}
|
|
5718
|
+
return results.map((r)=>r.column_name);
|
|
5719
|
+
}
|
|
5720
|
+
parseDefaultValue(defaultValue) {
|
|
5721
|
+
if (defaultValue === null) {
|
|
5722
|
+
return undefined;
|
|
5723
|
+
}
|
|
5724
|
+
const normalized = defaultValue.toUpperCase();
|
|
5725
|
+
if (normalized === 'NULL') {
|
|
5726
|
+
return null;
|
|
5727
|
+
}
|
|
5728
|
+
if (normalized === 'CURRENT_TIMESTAMP' || normalized === 'CURRENT_TIMESTAMP()') {
|
|
5729
|
+
return 'CURRENT_TIMESTAMP';
|
|
5730
|
+
}
|
|
5731
|
+
if (/^-?\d+$/.test(defaultValue)) {
|
|
5732
|
+
return Number.parseInt(defaultValue, 10);
|
|
5733
|
+
}
|
|
5734
|
+
if (/^-?\d+\.\d+$/.test(defaultValue)) {
|
|
5735
|
+
return Number.parseFloat(defaultValue);
|
|
5736
|
+
}
|
|
5737
|
+
if (defaultValue?.startsWith("'") && defaultValue?.endsWith("'")) {
|
|
5738
|
+
return defaultValue.slice(1, -1);
|
|
5739
|
+
}
|
|
5740
|
+
return defaultValue;
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5743
|
+
/**
|
|
5744
|
+
* Alias for MysqlSchemaIntrospector.
|
|
5745
|
+
* MariaDB uses the same information_schema structure as MySQL.
|
|
5746
|
+
*/ const MariadbSchemaIntrospector = MysqlSchemaIntrospector;
|
|
5747
|
+
|
|
5748
|
+
/**
|
|
5749
|
+
* PostgreSQL schema introspector
|
|
5750
|
+
*/ class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
5751
|
+
constructor(pool){
|
|
5752
|
+
super('postgres');
|
|
5753
|
+
this.pool = pool;
|
|
5754
|
+
}
|
|
5755
|
+
// ============================================================================
|
|
5756
|
+
// SQL Queries (dialect-specific)
|
|
5757
|
+
// ============================================================================
|
|
5758
|
+
getTableNamesQuery() {
|
|
5759
|
+
return /*sql*/ `
|
|
5760
|
+
SELECT table_name
|
|
5761
|
+
FROM information_schema.tables
|
|
5762
|
+
WHERE table_schema = 'public'
|
|
5763
|
+
AND table_type = 'BASE TABLE'
|
|
5764
|
+
ORDER BY table_name
|
|
5765
|
+
`;
|
|
5766
|
+
}
|
|
5767
|
+
tableExistsQuery() {
|
|
5768
|
+
return /*sql*/ `
|
|
5769
|
+
SELECT EXISTS (
|
|
5770
|
+
SELECT FROM information_schema.tables
|
|
5771
|
+
WHERE table_schema = 'public'
|
|
5772
|
+
AND table_name = $1
|
|
5773
|
+
) AS exists
|
|
5774
|
+
`;
|
|
5775
|
+
}
|
|
5776
|
+
parseTableExistsResult(results) {
|
|
5777
|
+
return results[0]?.['exists'] ?? false;
|
|
5778
|
+
}
|
|
5779
|
+
getColumnsQuery(_tableName) {
|
|
5780
|
+
return /*sql*/ `
|
|
5781
|
+
SELECT
|
|
5782
|
+
c.column_name,
|
|
5783
|
+
c.data_type,
|
|
5784
|
+
c.udt_name,
|
|
5785
|
+
c.is_nullable,
|
|
5786
|
+
c.column_default,
|
|
5787
|
+
c.character_maximum_length,
|
|
5788
|
+
c.numeric_precision,
|
|
5789
|
+
c.numeric_scale,
|
|
5790
|
+
c.is_identity,
|
|
5791
|
+
c.identity_generation,
|
|
5792
|
+
COALESCE(
|
|
5793
|
+
(SELECT TRUE FROM information_schema.table_constraints tc
|
|
5794
|
+
JOIN information_schema.key_column_usage kcu
|
|
5795
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
5796
|
+
WHERE tc.table_name = c.table_name
|
|
5797
|
+
AND tc.constraint_type = 'PRIMARY KEY'
|
|
5798
|
+
AND kcu.column_name = c.column_name
|
|
5799
|
+
LIMIT 1),
|
|
5800
|
+
FALSE
|
|
5801
|
+
) AS is_primary_key,
|
|
5802
|
+
COALESCE(
|
|
5803
|
+
(SELECT TRUE FROM information_schema.table_constraints tc
|
|
5804
|
+
JOIN information_schema.key_column_usage kcu
|
|
5805
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
5806
|
+
WHERE tc.table_name = c.table_name
|
|
5807
|
+
AND tc.constraint_type = 'UNIQUE'
|
|
5808
|
+
AND kcu.column_name = c.column_name
|
|
5809
|
+
LIMIT 1),
|
|
5810
|
+
FALSE
|
|
5811
|
+
) AS is_unique,
|
|
5812
|
+
pg_catalog.col_description(
|
|
5813
|
+
(SELECT oid FROM pg_catalog.pg_class WHERE relname = c.table_name),
|
|
5814
|
+
c.ordinal_position
|
|
5815
|
+
) AS column_comment
|
|
5816
|
+
FROM information_schema.columns c
|
|
5817
|
+
WHERE c.table_schema = 'public'
|
|
5818
|
+
AND c.table_name = $1
|
|
5819
|
+
ORDER BY c.ordinal_position
|
|
5820
|
+
`;
|
|
5821
|
+
}
|
|
5822
|
+
getIndexesQuery(_tableName) {
|
|
5823
|
+
return /*sql*/ `
|
|
5824
|
+
SELECT
|
|
5825
|
+
i.relname AS index_name,
|
|
5826
|
+
array_to_json(array_agg(a.attname ORDER BY k.n)) AS columns,
|
|
5827
|
+
ix.indisunique AS is_unique
|
|
5828
|
+
FROM pg_class t
|
|
5829
|
+
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
5830
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
5831
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
5832
|
+
CROSS JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n)
|
|
5833
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
5834
|
+
WHERE t.relname = $1
|
|
5835
|
+
AND n.nspname = 'public'
|
|
5836
|
+
AND NOT ix.indisprimary
|
|
5837
|
+
GROUP BY i.relname, ix.indisunique
|
|
5838
|
+
ORDER BY i.relname
|
|
5839
|
+
`;
|
|
5840
|
+
}
|
|
5841
|
+
getForeignKeysQuery(_tableName) {
|
|
5842
|
+
return /*sql*/ `
|
|
5843
|
+
SELECT
|
|
5844
|
+
tc.constraint_name,
|
|
5845
|
+
array_to_json(array_agg(kcu.column_name ORDER BY kcu.ordinal_position)) AS columns,
|
|
5846
|
+
ccu.table_name AS referenced_table,
|
|
5847
|
+
array_to_json(array_agg(ccu.column_name ORDER BY kcu.ordinal_position)) AS referenced_columns,
|
|
5848
|
+
rc.delete_rule,
|
|
5849
|
+
rc.update_rule
|
|
5850
|
+
FROM information_schema.table_constraints tc
|
|
5851
|
+
JOIN information_schema.key_column_usage kcu
|
|
5852
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
5853
|
+
AND tc.table_schema = kcu.table_schema
|
|
5854
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
5855
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
5856
|
+
AND ccu.table_schema = tc.table_schema
|
|
5857
|
+
JOIN information_schema.referential_constraints rc
|
|
5858
|
+
ON rc.constraint_name = tc.constraint_name
|
|
5859
|
+
AND rc.constraint_schema = tc.table_schema
|
|
5860
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
5861
|
+
AND tc.table_name = $1
|
|
5862
|
+
AND tc.table_schema = 'public'
|
|
5863
|
+
GROUP BY tc.constraint_name, ccu.table_name, rc.delete_rule, rc.update_rule
|
|
5864
|
+
ORDER BY tc.constraint_name
|
|
5865
|
+
`;
|
|
5866
|
+
}
|
|
5867
|
+
getPrimaryKeyQuery(_tableName) {
|
|
5868
|
+
return /*sql*/ `
|
|
5869
|
+
SELECT kcu.column_name
|
|
5870
|
+
FROM information_schema.table_constraints tc
|
|
5871
|
+
JOIN information_schema.key_column_usage kcu
|
|
5872
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
5873
|
+
AND tc.table_schema = kcu.table_schema
|
|
5874
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
5875
|
+
AND tc.table_name = $1
|
|
5876
|
+
AND tc.table_schema = 'public'
|
|
5877
|
+
ORDER BY kcu.ordinal_position
|
|
5878
|
+
`;
|
|
5879
|
+
}
|
|
5880
|
+
// ============================================================================
|
|
5881
|
+
// Internal Types
|
|
5882
|
+
// ============================================================================
|
|
5883
|
+
mapTableNameRow(row) {
|
|
5884
|
+
return row.table_name;
|
|
5885
|
+
}
|
|
5886
|
+
async mapColumnsResult(_querier, _tableName, results) {
|
|
5887
|
+
return results.map((row)=>({
|
|
5888
|
+
name: row.column_name,
|
|
5889
|
+
type: this.normalizeType(row.data_type, row.udt_name),
|
|
5890
|
+
nullable: row.is_nullable === 'YES',
|
|
5891
|
+
defaultValue: this.parseDefaultValue(row.column_default),
|
|
5892
|
+
isPrimaryKey: row.is_primary_key,
|
|
5893
|
+
isAutoIncrement: this.isAutoIncrement(row.column_default, row.is_identity),
|
|
5894
|
+
isUnique: row.is_unique,
|
|
5895
|
+
length: row.character_maximum_length ?? undefined,
|
|
5896
|
+
precision: row.numeric_precision ?? undefined,
|
|
5897
|
+
scale: row.numeric_scale ?? undefined,
|
|
5898
|
+
comment: row.column_comment ?? undefined
|
|
5899
|
+
}));
|
|
5900
|
+
}
|
|
5901
|
+
async mapIndexesResult(_querier, _tableName, results) {
|
|
5902
|
+
return results.map((row)=>({
|
|
5903
|
+
name: row.index_name,
|
|
5904
|
+
columns: row.columns,
|
|
5905
|
+
unique: row.is_unique
|
|
5906
|
+
}));
|
|
5907
|
+
}
|
|
5908
|
+
async mapForeignKeysResult(_querier, _tableName, results) {
|
|
5909
|
+
return results.map((row)=>({
|
|
5910
|
+
name: row.constraint_name,
|
|
5911
|
+
columns: row.columns,
|
|
5912
|
+
referencedTable: row.referenced_table,
|
|
5913
|
+
referencedColumns: row.referenced_columns,
|
|
5914
|
+
onDelete: this.normalizeReferentialAction(row.delete_rule),
|
|
5915
|
+
onUpdate: this.normalizeReferentialAction(row.update_rule)
|
|
5916
|
+
}));
|
|
5917
|
+
}
|
|
5918
|
+
mapPrimaryKeyResult(results) {
|
|
5919
|
+
if (results.length === 0) {
|
|
5920
|
+
return undefined;
|
|
5921
|
+
}
|
|
5922
|
+
return results.map((r)=>r.column_name);
|
|
5923
|
+
}
|
|
5924
|
+
// ============================================================================
|
|
5925
|
+
// PostgreSQL-specific helpers
|
|
5926
|
+
// ============================================================================
|
|
5927
|
+
normalizeType(dataType, udtName) {
|
|
5928
|
+
// Handle user-defined types and arrays
|
|
5929
|
+
if (dataType === 'USER-DEFINED') {
|
|
5930
|
+
return udtName.toUpperCase();
|
|
5931
|
+
}
|
|
5932
|
+
if (dataType === 'ARRAY') {
|
|
5933
|
+
return `${udtName.replace(/^_/, '').toUpperCase()}[]`;
|
|
5934
|
+
}
|
|
5935
|
+
return dataType.toUpperCase();
|
|
5936
|
+
}
|
|
5937
|
+
parseDefaultValue(defaultValue) {
|
|
5938
|
+
if (!defaultValue) {
|
|
5939
|
+
return undefined;
|
|
5940
|
+
}
|
|
5941
|
+
// Remove type casting (e.g., ::text, ::character varying, ::text[])
|
|
5942
|
+
const cleaned = defaultValue.replace(/::[a-z_]+(\s+[a-z_]+)?(\[\])?/gi, '').trim();
|
|
5943
|
+
if (cleaned.startsWith("'") && cleaned.endsWith("'")) {
|
|
5944
|
+
return cleaned.slice(1, -1);
|
|
5945
|
+
}
|
|
5946
|
+
if (cleaned === 'true' || cleaned === 'false') {
|
|
5947
|
+
return cleaned === 'true';
|
|
5948
|
+
}
|
|
5949
|
+
if (cleaned === 'NULL') {
|
|
5950
|
+
return null;
|
|
5951
|
+
}
|
|
5952
|
+
if (/^-?\d+$/.test(cleaned)) {
|
|
5953
|
+
return Number.parseInt(cleaned, 10);
|
|
5954
|
+
}
|
|
5955
|
+
if (/^-?\d+\.\d+$/.test(cleaned)) {
|
|
5956
|
+
return Number.parseFloat(cleaned);
|
|
5957
|
+
}
|
|
5958
|
+
// Return cleaned value for functions like CURRENT_TIMESTAMP, nextval(), etc.
|
|
5959
|
+
return cleaned;
|
|
5960
|
+
}
|
|
5961
|
+
isAutoIncrement(columnDefault, isIdentity) {
|
|
5962
|
+
// PostgreSQL identity columns (GENERATED ... AS IDENTITY)
|
|
5963
|
+
if (isIdentity === 'YES') {
|
|
5964
|
+
return true;
|
|
5965
|
+
}
|
|
5966
|
+
// Serial/bigserial columns use nextval()
|
|
5967
|
+
return columnDefault?.includes('nextval(') ?? false;
|
|
5968
|
+
}
|
|
5969
|
+
}
|
|
5970
|
+
|
|
5971
|
+
/**
|
|
5972
|
+
* SQLite schema introspector
|
|
5973
|
+
*/ class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
5974
|
+
constructor(pool){
|
|
5975
|
+
super('sqlite');
|
|
5976
|
+
this.pool = pool;
|
|
5977
|
+
}
|
|
5978
|
+
// ============================================================================
|
|
5979
|
+
// SQL Queries (dialect-specific)
|
|
5980
|
+
// ============================================================================
|
|
5981
|
+
getTableNamesQuery() {
|
|
5982
|
+
return /*sql*/ `
|
|
5983
|
+
SELECT name
|
|
5984
|
+
FROM sqlite_master
|
|
5985
|
+
WHERE type = 'table'
|
|
5986
|
+
AND name NOT LIKE 'sqlite_%'
|
|
5987
|
+
ORDER BY name
|
|
5988
|
+
`;
|
|
5989
|
+
}
|
|
5990
|
+
tableExistsQuery() {
|
|
5991
|
+
return /*sql*/ `
|
|
5992
|
+
SELECT COUNT(*) as count
|
|
5993
|
+
FROM sqlite_master
|
|
5994
|
+
WHERE type = 'table'
|
|
5995
|
+
AND name = ?
|
|
5996
|
+
`;
|
|
5997
|
+
}
|
|
5998
|
+
parseTableExistsResult(results) {
|
|
5999
|
+
const row = results[0];
|
|
6000
|
+
if (row?.count !== undefined) {
|
|
6001
|
+
return (this.toNumber(row.count) ?? 0) > 0;
|
|
6002
|
+
}
|
|
6003
|
+
return false;
|
|
6004
|
+
}
|
|
6005
|
+
// SQLite uses PRAGMA which doesn't use parameterized queries in the same way
|
|
6006
|
+
getColumnsQuery(tableName) {
|
|
6007
|
+
return `PRAGMA table_info(${this.escapeId(tableName)})`;
|
|
6008
|
+
}
|
|
6009
|
+
getIndexesQuery(tableName) {
|
|
6010
|
+
return `PRAGMA index_list(${this.escapeId(tableName)})`;
|
|
6011
|
+
}
|
|
6012
|
+
getForeignKeysQuery(tableName) {
|
|
6013
|
+
return `PRAGMA foreign_key_list(${this.escapeId(tableName)})`;
|
|
6014
|
+
}
|
|
6015
|
+
getPrimaryKeyQuery(tableName) {
|
|
6016
|
+
return `PRAGMA table_info(${this.escapeId(tableName)})`;
|
|
6017
|
+
}
|
|
6018
|
+
getColumnsParams(_tableName) {
|
|
6019
|
+
return [];
|
|
6020
|
+
}
|
|
6021
|
+
getIndexesParams(_tableName) {
|
|
6022
|
+
return [];
|
|
6023
|
+
}
|
|
6024
|
+
getForeignKeysParams(_tableName) {
|
|
6025
|
+
return [];
|
|
6026
|
+
}
|
|
6027
|
+
getPrimaryKeyParams(_tableName) {
|
|
6028
|
+
return [];
|
|
6029
|
+
}
|
|
6030
|
+
// ============================================================================
|
|
6031
|
+
// Row Mapping (dialect-specific)
|
|
6032
|
+
// ============================================================================
|
|
6033
|
+
mapTableNameRow(row) {
|
|
6034
|
+
return row.name;
|
|
6035
|
+
}
|
|
6036
|
+
async mapColumnsResult(querier, tableName, results) {
|
|
6037
|
+
// Get unique columns from indexes
|
|
6038
|
+
const uniqueColumns = await this.getUniqueColumns(querier, tableName);
|
|
6039
|
+
return results.map((row)=>({
|
|
6040
|
+
name: row.name,
|
|
6041
|
+
type: this.normalizeType(row.type),
|
|
6042
|
+
nullable: row.notnull === 0,
|
|
6043
|
+
defaultValue: this.parseDefaultValue(row.dflt_value),
|
|
6044
|
+
isPrimaryKey: row.pk > 0,
|
|
6045
|
+
isAutoIncrement: row.pk > 0 && row.type.toUpperCase() === 'INTEGER',
|
|
6046
|
+
isUnique: uniqueColumns.has(row.name),
|
|
6047
|
+
length: this.extractLength(row.type),
|
|
6048
|
+
precision: undefined,
|
|
6049
|
+
scale: undefined,
|
|
6050
|
+
comment: undefined
|
|
6051
|
+
}));
|
|
6052
|
+
}
|
|
6053
|
+
async mapIndexesResult(querier, _tableName, results) {
|
|
6054
|
+
const indexSchemas = [];
|
|
6055
|
+
for (const index of results){
|
|
6056
|
+
const columns = await querier.all(`PRAGMA index_info(${this.escapeId(index.name)})`);
|
|
6057
|
+
// Include user-created indexes ('c') and multi-column unique constraints ('u')
|
|
6058
|
+
// Skip primary key indexes ('pk') and single-column unique constraints
|
|
6059
|
+
const isUserCreated = index.origin === 'c';
|
|
6060
|
+
const isCompositeUnique = index.origin === 'u' && columns.length > 1;
|
|
6061
|
+
if (isUserCreated || isCompositeUnique) {
|
|
6062
|
+
indexSchemas.push({
|
|
6063
|
+
name: index.name,
|
|
6064
|
+
columns: columns.map((c)=>c.name),
|
|
6065
|
+
unique: Boolean(index.unique)
|
|
6066
|
+
});
|
|
6067
|
+
}
|
|
6068
|
+
}
|
|
6069
|
+
return indexSchemas;
|
|
6070
|
+
}
|
|
6071
|
+
async mapForeignKeysResult(_querier, tableName, results) {
|
|
6072
|
+
// Group by id to handle composite foreign keys
|
|
6073
|
+
const grouped = new Map();
|
|
6074
|
+
for (const row of results){
|
|
6075
|
+
const id = row.id;
|
|
6076
|
+
const existing = grouped.get(id) ?? [];
|
|
6077
|
+
existing.push(row);
|
|
6078
|
+
grouped.set(id, existing);
|
|
6079
|
+
}
|
|
6080
|
+
return Array.from(grouped.entries()).map(([id, rows])=>{
|
|
6081
|
+
const first = rows[0];
|
|
6082
|
+
return {
|
|
6083
|
+
name: `fk_${tableName}_${id}`,
|
|
6084
|
+
columns: rows.map((r)=>r.from),
|
|
6085
|
+
referencedTable: first.table,
|
|
6086
|
+
referencedColumns: rows.map((r)=>r.to),
|
|
6087
|
+
onDelete: this.normalizeReferentialAction(first.on_delete),
|
|
6088
|
+
onUpdate: this.normalizeReferentialAction(first.on_update)
|
|
6089
|
+
};
|
|
6090
|
+
});
|
|
6091
|
+
}
|
|
6092
|
+
mapPrimaryKeyResult(results) {
|
|
6093
|
+
const pkColumns = results.filter((r)=>r.pk > 0).sort((a, b)=>a.pk - b.pk);
|
|
6094
|
+
if (pkColumns.length === 0) {
|
|
6095
|
+
return undefined;
|
|
6096
|
+
}
|
|
6097
|
+
return pkColumns.map((r)=>r.name);
|
|
6098
|
+
}
|
|
6099
|
+
// ============================================================================
|
|
6100
|
+
// SQLite-specific helpers
|
|
6101
|
+
// ============================================================================
|
|
6102
|
+
async getUniqueColumns(querier, tableName) {
|
|
6103
|
+
const results = await querier.all(`PRAGMA index_list(${this.escapeId(tableName)})`);
|
|
6104
|
+
const uniqueColumns = new Set();
|
|
6105
|
+
for (const index of results){
|
|
6106
|
+
if (index.unique) {
|
|
6107
|
+
const indexInfo = await querier.all(`PRAGMA index_info(${this.escapeId(index.name)})`);
|
|
6108
|
+
// Only single-column unique constraints
|
|
6109
|
+
if (indexInfo.length === 1) {
|
|
6110
|
+
uniqueColumns.add(indexInfo[0].name);
|
|
6111
|
+
}
|
|
6112
|
+
}
|
|
6113
|
+
}
|
|
6114
|
+
return uniqueColumns;
|
|
6115
|
+
}
|
|
6116
|
+
normalizeType(type) {
|
|
6117
|
+
// Extract base type without length/precision
|
|
6118
|
+
const match = type.match(/^([A-Za-z]+)/);
|
|
6119
|
+
return match ? match[1].toUpperCase() : type.toUpperCase();
|
|
6120
|
+
}
|
|
6121
|
+
extractLength(type) {
|
|
6122
|
+
const match = type.match(/\((\d+)\)/);
|
|
6123
|
+
return match ? Number.parseInt(match[1], 10) : undefined;
|
|
6124
|
+
}
|
|
6125
|
+
parseDefaultValue(defaultValue) {
|
|
6126
|
+
if (defaultValue === null) {
|
|
6127
|
+
return undefined;
|
|
6128
|
+
}
|
|
6129
|
+
if (defaultValue === 'NULL') {
|
|
6130
|
+
return null;
|
|
6131
|
+
}
|
|
6132
|
+
if (defaultValue === 'CURRENT_TIMESTAMP' || defaultValue === 'CURRENT_DATE' || defaultValue === 'CURRENT_TIME') {
|
|
6133
|
+
return defaultValue;
|
|
6134
|
+
}
|
|
6135
|
+
if (/^'.*'$/.test(defaultValue)) {
|
|
6136
|
+
return defaultValue.slice(1, -1);
|
|
6137
|
+
}
|
|
6138
|
+
if (/^-?\d+$/.test(defaultValue)) {
|
|
6139
|
+
return Number.parseInt(defaultValue, 10);
|
|
6140
|
+
}
|
|
6141
|
+
if (/^-?\d+\.\d+$/.test(defaultValue)) {
|
|
6142
|
+
return Number.parseFloat(defaultValue);
|
|
6143
|
+
}
|
|
6144
|
+
return defaultValue;
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
6147
|
+
|
|
6148
|
+
/**
|
|
6149
|
+
* Schema AST Module
|
|
6150
|
+
*
|
|
6151
|
+
* Provides a unified graph representation of database schema for:
|
|
6152
|
+
* - Schema diffing and migration generation
|
|
6153
|
+
* - Entity code generation from database
|
|
6154
|
+
* - Drift detection
|
|
6155
|
+
* - Smart relation inference
|
|
6156
|
+
*/ // Dialect configuration
|
|
6157
|
+
/**
|
|
6158
|
+
* Introspect the database and build a SchemaAST from it.
|
|
6159
|
+
*
|
|
6160
|
+
* @param introspector - The schema introspector to use
|
|
6161
|
+
* @returns The SchemaAST representing the database schema
|
|
6162
|
+
*/ async function introspectSchema(introspector) {
|
|
6163
|
+
return introspector.introspect();
|
|
6164
|
+
}
|
|
6165
|
+
|
|
6166
|
+
/**
|
|
6167
|
+
* Stores migration state in a database table.
|
|
6168
|
+
* Uses the querier's dialect for escaping and placeholders.
|
|
6169
|
+
*/ class DatabaseMigrationStorage {
|
|
6170
|
+
constructor(pool, options = {}){
|
|
6171
|
+
this.pool = pool;
|
|
6172
|
+
this.storageInitialized = false;
|
|
6173
|
+
this.tableName = options.tableName ?? 'uql_migrations';
|
|
6174
|
+
}
|
|
6175
|
+
async ensureStorage() {
|
|
6176
|
+
if (this.storageInitialized) {
|
|
6177
|
+
return;
|
|
6178
|
+
}
|
|
6179
|
+
const querier = await this.pool.getQuerier();
|
|
6180
|
+
if (!isSqlQuerier(querier)) {
|
|
6181
|
+
await querier.release();
|
|
6182
|
+
throw new Error('DatabaseMigrationStorage requires a SQL-based querier');
|
|
6183
|
+
}
|
|
6184
|
+
try {
|
|
6185
|
+
await this.createTableIfNotExists(querier);
|
|
6186
|
+
this.storageInitialized = true;
|
|
6187
|
+
} finally{
|
|
6188
|
+
await querier.release();
|
|
6189
|
+
}
|
|
6190
|
+
}
|
|
6191
|
+
async createTableIfNotExists(querier) {
|
|
6192
|
+
const { dialect } = querier;
|
|
6193
|
+
const sql = `
|
|
6194
|
+
CREATE TABLE IF NOT EXISTS ${dialect.escapeId(this.tableName)} (
|
|
6195
|
+
${dialect.escapeId('name')} VARCHAR(255) PRIMARY KEY,
|
|
6196
|
+
${dialect.escapeId('executed_at')} TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
6197
|
+
)
|
|
6198
|
+
`;
|
|
6199
|
+
await querier.run(sql);
|
|
6200
|
+
}
|
|
6201
|
+
async executed() {
|
|
6202
|
+
await this.ensureStorage();
|
|
6203
|
+
const querier = await this.pool.getQuerier();
|
|
6204
|
+
if (!isSqlQuerier(querier)) {
|
|
6205
|
+
await querier.release();
|
|
6206
|
+
throw new Error('DatabaseMigrationStorage requires a SQL-based querier');
|
|
6207
|
+
}
|
|
6208
|
+
try {
|
|
6209
|
+
const { dialect } = querier;
|
|
6210
|
+
const sql = `SELECT ${dialect.escapeId('name')} FROM ${dialect.escapeId(this.tableName)} ORDER BY ${dialect.escapeId('name')} ASC`;
|
|
6211
|
+
const results = await querier.all(sql);
|
|
6212
|
+
return results.map((r)=>r.name);
|
|
6213
|
+
} finally{
|
|
6214
|
+
await querier.release();
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
6217
|
+
/**
|
|
6218
|
+
* Log a migration as executed - uses provided querier (within transaction)
|
|
6219
|
+
*/ async logWithQuerier(querier, migrationName) {
|
|
6220
|
+
await this.ensureStorage();
|
|
6221
|
+
const { dialect } = querier;
|
|
6222
|
+
const sql = `INSERT INTO ${dialect.escapeId(this.tableName)} (${dialect.escapeId('name')}) VALUES (${dialect.placeholder(1)})`;
|
|
6223
|
+
await querier.run(sql, [
|
|
6224
|
+
migrationName
|
|
6225
|
+
]);
|
|
6226
|
+
}
|
|
6227
|
+
/**
|
|
6228
|
+
* Unlog a migration - uses provided querier (within transaction)
|
|
6229
|
+
*/ async unlogWithQuerier(querier, migrationName) {
|
|
6230
|
+
await this.ensureStorage();
|
|
6231
|
+
const { dialect } = querier;
|
|
6232
|
+
const sql = `DELETE FROM ${dialect.escapeId(this.tableName)} WHERE ${dialect.escapeId('name')} = ${dialect.placeholder(1)}`;
|
|
6233
|
+
await querier.run(sql, [
|
|
6234
|
+
migrationName
|
|
6235
|
+
]);
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
|
|
6239
|
+
/**
|
|
6240
|
+
* Main class for managing database migrations
|
|
6241
|
+
*/ class Migrator {
|
|
6242
|
+
get logger() {
|
|
6243
|
+
return this._logger;
|
|
6244
|
+
}
|
|
6245
|
+
set logger(value) {
|
|
6246
|
+
this._logger = new LoggerWrapper(value);
|
|
6247
|
+
}
|
|
6248
|
+
get entities() {
|
|
6249
|
+
return this._entities ?? getEntities();
|
|
6250
|
+
}
|
|
6251
|
+
constructor(pool, options = {}){
|
|
6252
|
+
this.pool = pool;
|
|
6253
|
+
this.dialect = options.dialect ?? pool.dialect ?? 'postgres';
|
|
6254
|
+
this.storage = options.storage ?? new DatabaseMigrationStorage(pool, {
|
|
6255
|
+
tableName: options.tableName
|
|
6256
|
+
});
|
|
6257
|
+
this.migrationsPath = options.migrationsPath ?? './migrations';
|
|
6258
|
+
this._logger = new LoggerWrapper(options.logger, options.slowQuery);
|
|
6259
|
+
this._entities = options.entities;
|
|
6260
|
+
this.schemaIntrospector = this.createIntrospector();
|
|
6261
|
+
this.schemaGenerator = options.schemaGenerator ?? this.createGenerator(options.namingStrategy);
|
|
6262
|
+
}
|
|
6263
|
+
/**
|
|
6264
|
+
* Set the schema generator for DDL operations
|
|
6265
|
+
*/ setSchemaGenerator(generator) {
|
|
6266
|
+
this.schemaGenerator = generator;
|
|
6267
|
+
}
|
|
6268
|
+
createIntrospector() {
|
|
6269
|
+
switch(this.dialect){
|
|
6270
|
+
case 'cockroachdb':
|
|
6271
|
+
case 'postgres':
|
|
6272
|
+
return new PostgresSchemaIntrospector(this.pool);
|
|
6273
|
+
case 'mysql':
|
|
6274
|
+
case 'mariadb':
|
|
6275
|
+
return new MysqlSchemaIntrospector(this.pool);
|
|
6276
|
+
case 'sqlite':
|
|
6277
|
+
return new SqliteSchemaIntrospector(this.pool);
|
|
6278
|
+
case 'mongodb':
|
|
6279
|
+
return new MongoSchemaIntrospector(this.pool);
|
|
6280
|
+
default:
|
|
6281
|
+
return undefined;
|
|
6282
|
+
}
|
|
6283
|
+
}
|
|
6284
|
+
createGenerator(namingStrategy) {
|
|
6285
|
+
return createSchemaGenerator(this.dialect, namingStrategy);
|
|
6286
|
+
}
|
|
6287
|
+
/**
|
|
6288
|
+
* Get the SQL dialect
|
|
6289
|
+
*/ getDialect() {
|
|
6290
|
+
return this.dialect;
|
|
6291
|
+
}
|
|
6292
|
+
/**
|
|
6293
|
+
* Get all discovered migrations from the migrations directory
|
|
6294
|
+
*/ async getMigrations() {
|
|
6295
|
+
const files = await this.getMigrationFiles();
|
|
6296
|
+
const migrations = [];
|
|
6297
|
+
for (const file of files){
|
|
6298
|
+
const migration = await this.loadMigration(file);
|
|
6299
|
+
if (migration) {
|
|
6300
|
+
migrations.push(migration);
|
|
6301
|
+
}
|
|
6302
|
+
}
|
|
6303
|
+
// Sort by name (which typically includes timestamp)
|
|
6304
|
+
return migrations.sort((a, b)=>a.name.localeCompare(b.name));
|
|
6305
|
+
}
|
|
6306
|
+
/**
|
|
6307
|
+
* Get list of pending migrations (not yet executed)
|
|
6308
|
+
*/ async pending() {
|
|
6309
|
+
const [migrations, executed] = await Promise.all([
|
|
6310
|
+
this.getMigrations(),
|
|
6311
|
+
this.storage.executed()
|
|
6312
|
+
]);
|
|
6313
|
+
const executedSet = new Set(executed);
|
|
6314
|
+
return migrations.filter((m)=>!executedSet.has(m.name));
|
|
6315
|
+
}
|
|
6316
|
+
/**
|
|
6317
|
+
* Get list of executed migrations
|
|
6318
|
+
*/ async executed() {
|
|
6319
|
+
return this.storage.executed();
|
|
6320
|
+
}
|
|
6321
|
+
/**
|
|
6322
|
+
* Run all pending migrations
|
|
6323
|
+
*/ async up(options = {}) {
|
|
6324
|
+
const pendingMigrations = await this.pending();
|
|
6325
|
+
const results = [];
|
|
6326
|
+
let migrationsToRun = pendingMigrations;
|
|
6327
|
+
if (options.to) {
|
|
6328
|
+
const toIndex = migrationsToRun.findIndex((m)=>m.name === options.to);
|
|
6329
|
+
if (toIndex === -1) {
|
|
6330
|
+
throw new Error(`Migration '${options.to}' not found`);
|
|
6331
|
+
}
|
|
6332
|
+
migrationsToRun = migrationsToRun.slice(0, toIndex + 1);
|
|
6333
|
+
}
|
|
6334
|
+
if (options.step !== undefined) {
|
|
6335
|
+
migrationsToRun = migrationsToRun.slice(0, options.step);
|
|
6336
|
+
}
|
|
6337
|
+
for (const migration of migrationsToRun){
|
|
6338
|
+
const result = await this.runMigration(migration, 'up');
|
|
6339
|
+
results.push(result);
|
|
6340
|
+
if (!result.success) {
|
|
6341
|
+
break; // Stop on first failure
|
|
6342
|
+
}
|
|
6343
|
+
}
|
|
6344
|
+
return results;
|
|
6345
|
+
}
|
|
6346
|
+
/**
|
|
6347
|
+
* Rollback migrations
|
|
6348
|
+
*/ async down(options = {}) {
|
|
6349
|
+
const [migrations, executed] = await Promise.all([
|
|
6350
|
+
this.getMigrations(),
|
|
6351
|
+
this.storage.executed()
|
|
6352
|
+
]);
|
|
6353
|
+
const executedSet = new Set(executed);
|
|
6354
|
+
const executedMigrations = migrations.filter((m)=>executedSet.has(m.name)).reverse(); // Rollback in reverse order
|
|
6355
|
+
const results = [];
|
|
6356
|
+
let migrationsToRun = executedMigrations;
|
|
6357
|
+
if (options.to) {
|
|
6358
|
+
const toIndex = migrationsToRun.findIndex((m)=>m.name === options.to);
|
|
6359
|
+
if (toIndex === -1) {
|
|
6360
|
+
throw new Error(`Migration '${options.to}' not found`);
|
|
6361
|
+
}
|
|
6362
|
+
migrationsToRun = migrationsToRun.slice(0, toIndex + 1);
|
|
6363
|
+
}
|
|
6364
|
+
if (options.step !== undefined) {
|
|
6365
|
+
migrationsToRun = migrationsToRun.slice(0, options.step);
|
|
6366
|
+
}
|
|
6367
|
+
for (const migration of migrationsToRun){
|
|
6368
|
+
const result = await this.runMigration(migration, 'down');
|
|
6369
|
+
results.push(result);
|
|
6370
|
+
if (!result.success) {
|
|
6371
|
+
break; // Stop on first failure
|
|
6372
|
+
}
|
|
6373
|
+
}
|
|
6374
|
+
return results;
|
|
6375
|
+
}
|
|
6376
|
+
/**
|
|
6377
|
+
* Run a single migration within a transaction
|
|
6378
|
+
*/ async runMigration(migration, direction) {
|
|
6379
|
+
const startTime = Date.now();
|
|
6380
|
+
const querier = await this.pool.getQuerier();
|
|
6381
|
+
if (!isSqlQuerier(querier)) {
|
|
6382
|
+
await querier.release();
|
|
6383
|
+
throw new Error('Migrator requires a SQL-based querier');
|
|
6384
|
+
}
|
|
6385
|
+
try {
|
|
6386
|
+
this.logger.logMigration(`${direction === 'up' ? 'Running' : 'Reverting'} migration: ${migration.name}`);
|
|
6387
|
+
await querier.beginTransaction();
|
|
6388
|
+
if (direction === 'up') {
|
|
6389
|
+
await migration.up(querier);
|
|
6390
|
+
// Log within the same transaction
|
|
6391
|
+
await this.storage.logWithQuerier(querier, migration.name);
|
|
6392
|
+
} else {
|
|
6393
|
+
await migration.down(querier);
|
|
6394
|
+
// Unlog within the same transaction
|
|
6395
|
+
await this.storage.unlogWithQuerier(querier, migration.name);
|
|
6396
|
+
}
|
|
6397
|
+
await querier.commitTransaction();
|
|
6398
|
+
const duration = Date.now() - startTime;
|
|
6399
|
+
this.logger.logMigration(`Migration ${migration.name} ${direction === 'up' ? 'applied' : 'reverted'} in ${duration}ms`);
|
|
6400
|
+
return {
|
|
6401
|
+
name: migration.name,
|
|
6402
|
+
direction,
|
|
6403
|
+
duration,
|
|
6404
|
+
success: true
|
|
6405
|
+
};
|
|
6406
|
+
} catch (error) {
|
|
6407
|
+
await querier.rollbackTransaction();
|
|
6408
|
+
const duration = Date.now() - startTime;
|
|
6409
|
+
this.logger.logError(`Migration ${migration.name} failed: ${error.message}`, error);
|
|
6410
|
+
return {
|
|
6411
|
+
name: migration.name,
|
|
6412
|
+
direction,
|
|
6413
|
+
duration,
|
|
6414
|
+
success: false,
|
|
6415
|
+
error: error
|
|
6416
|
+
};
|
|
6417
|
+
} finally{
|
|
6418
|
+
await querier.release();
|
|
6419
|
+
}
|
|
6420
|
+
}
|
|
6421
|
+
/**
|
|
6422
|
+
* Generate a new migration file
|
|
6423
|
+
*/ async generate(name) {
|
|
6424
|
+
const timestamp = this.getTimestamp();
|
|
6425
|
+
const fileName = `${timestamp}_${this.slugify(name)}.ts`;
|
|
6426
|
+
const filePath = join(this.migrationsPath, fileName);
|
|
6427
|
+
const content = this.generateMigrationContent(name);
|
|
6428
|
+
await mkdir(this.migrationsPath, {
|
|
6429
|
+
recursive: true
|
|
6430
|
+
});
|
|
6431
|
+
await writeFile(filePath, content, 'utf-8');
|
|
6432
|
+
this.logger.logInfo(`Created migration: ${filePath}`);
|
|
6433
|
+
return filePath;
|
|
6434
|
+
}
|
|
6435
|
+
/**
|
|
6436
|
+
* Generate a migration based on entity schema differences
|
|
6437
|
+
*/ async generateFromEntities(name) {
|
|
6438
|
+
if (!this.schemaGenerator) {
|
|
6439
|
+
throw new Error('Schema generator not set. Call setSchemaGenerator() first.');
|
|
6440
|
+
}
|
|
6441
|
+
const diffs = await this.getDiffs();
|
|
6442
|
+
const upStatements = [];
|
|
6443
|
+
const downStatements = [];
|
|
6444
|
+
for (const diff of diffs){
|
|
6445
|
+
if (diff.type === 'create') {
|
|
6446
|
+
const entity = this.findEntityForTable(diff.tableName);
|
|
6447
|
+
if (entity) {
|
|
6448
|
+
upStatements.push(this.schemaGenerator.generateCreateTable(entity));
|
|
6449
|
+
downStatements.push(this.schemaGenerator.generateDropTable(entity));
|
|
6450
|
+
}
|
|
6451
|
+
} else if (diff.type === 'alter') {
|
|
6452
|
+
const alterStatements = this.schemaGenerator.generateAlterTable(diff);
|
|
6453
|
+
upStatements.push(...alterStatements);
|
|
6454
|
+
const alterDownStatements = this.schemaGenerator.generateAlterTableDown(diff);
|
|
6455
|
+
downStatements.push(...alterDownStatements);
|
|
6456
|
+
}
|
|
6457
|
+
}
|
|
6458
|
+
if (upStatements.length === 0) {
|
|
6459
|
+
this.logger.logInfo('No schema changes detected.');
|
|
6460
|
+
return '';
|
|
6461
|
+
}
|
|
6462
|
+
const timestamp = this.getTimestamp();
|
|
6463
|
+
const fileName = `${timestamp}_${this.slugify(name)}.ts`;
|
|
6464
|
+
const filePath = join(this.migrationsPath, fileName);
|
|
6465
|
+
const content = this.generateMigrationContentWithStatements(name, upStatements, downStatements.reverse());
|
|
6466
|
+
await mkdir(this.migrationsPath, {
|
|
6467
|
+
recursive: true
|
|
6468
|
+
});
|
|
6469
|
+
await writeFile(filePath, content, 'utf-8');
|
|
6470
|
+
this.logger.logInfo(`Created migration from entities: ${filePath}`);
|
|
6471
|
+
return filePath;
|
|
6472
|
+
}
|
|
6473
|
+
/**
|
|
6474
|
+
* Get all schema differences between entities and database
|
|
6475
|
+
*/ async getDiffs() {
|
|
6476
|
+
if (!this.schemaGenerator || !this.schemaIntrospector) {
|
|
6477
|
+
throw new Error('Schema generator and introspector must be set');
|
|
6478
|
+
}
|
|
6479
|
+
const ast = await introspectSchema(this.schemaIntrospector);
|
|
6480
|
+
const diffs = [];
|
|
6481
|
+
for (const entity of this.entities){
|
|
6482
|
+
const meta = getMeta$1(entity);
|
|
6483
|
+
const tableName = this.schemaGenerator.resolveTableName(entity, meta);
|
|
6484
|
+
const currentTable = ast.getTable(tableName);
|
|
6485
|
+
const diff = this.schemaGenerator.diffSchema(entity, currentTable);
|
|
6486
|
+
if (diff) {
|
|
6487
|
+
diffs.push(diff);
|
|
6488
|
+
}
|
|
6489
|
+
}
|
|
6490
|
+
return diffs;
|
|
6491
|
+
}
|
|
6492
|
+
findEntityForTable(tableName) {
|
|
6493
|
+
for (const entity of this.entities){
|
|
6494
|
+
const meta = getMeta$1(entity);
|
|
6495
|
+
const name = this.schemaGenerator.resolveTableName(entity, meta);
|
|
6496
|
+
if (name === tableName) {
|
|
6497
|
+
return entity;
|
|
6498
|
+
}
|
|
6499
|
+
}
|
|
6500
|
+
return undefined;
|
|
6501
|
+
}
|
|
6502
|
+
/**
|
|
6503
|
+
* Sync schema directly (for development only - not for production!)
|
|
6504
|
+
*/ async sync(options = {}) {
|
|
6505
|
+
if (options.force) {
|
|
6506
|
+
return this.syncForce();
|
|
6507
|
+
}
|
|
6508
|
+
return this.autoSync({
|
|
6509
|
+
safe: true
|
|
6510
|
+
});
|
|
6511
|
+
}
|
|
6512
|
+
/**
|
|
6513
|
+
* Drops and recreates all tables (Development only!)
|
|
6514
|
+
*/ async syncForce() {
|
|
6515
|
+
if (!this.schemaGenerator) {
|
|
6516
|
+
throw new Error('Schema generator not set. Call setSchemaGenerator() first.');
|
|
6517
|
+
}
|
|
6518
|
+
const querier = await this.pool.getQuerier();
|
|
6519
|
+
if (!isSqlQuerier(querier)) {
|
|
6520
|
+
await querier.release();
|
|
6521
|
+
throw new Error('Migrator requires a SQL-based querier');
|
|
6522
|
+
}
|
|
6523
|
+
try {
|
|
6524
|
+
await querier.beginTransaction();
|
|
6525
|
+
// Drop all tables first (in reverse order for foreign keys)
|
|
6526
|
+
for (const entity of [
|
|
6527
|
+
...this.entities
|
|
6528
|
+
].reverse()){
|
|
6529
|
+
const dropSql = this.schemaGenerator.generateDropTable(entity);
|
|
6530
|
+
this.logger.logSchema(`Executing: ${dropSql}`);
|
|
6531
|
+
await querier.run(dropSql);
|
|
6532
|
+
}
|
|
6533
|
+
// Create all tables
|
|
6534
|
+
for (const entity of this.entities){
|
|
6535
|
+
const createSql = this.schemaGenerator.generateCreateTable(entity);
|
|
6536
|
+
this.logger.logSchema(`Executing: ${createSql}`);
|
|
6537
|
+
await querier.run(createSql);
|
|
6538
|
+
}
|
|
6539
|
+
await querier.commitTransaction();
|
|
6540
|
+
this.logger.logSchema('Schema sync (force) completed');
|
|
6541
|
+
} catch (error) {
|
|
6542
|
+
await querier.rollbackTransaction();
|
|
6543
|
+
throw error;
|
|
6544
|
+
} finally{
|
|
6545
|
+
await querier.release();
|
|
6546
|
+
}
|
|
6547
|
+
}
|
|
6548
|
+
/**
|
|
6549
|
+
* Safely synchronizes the schema by only adding missing tables and columns.
|
|
6550
|
+
*/ async autoSync(options = {}) {
|
|
6551
|
+
if (!this.schemaGenerator || !this.schemaIntrospector) {
|
|
6552
|
+
throw new Error('Schema generator and introspector must be set');
|
|
6553
|
+
}
|
|
6554
|
+
const diffs = await this.getDiffs();
|
|
6555
|
+
const statements = [];
|
|
6556
|
+
for (const diff of diffs){
|
|
6557
|
+
if (diff.type === 'create') {
|
|
6558
|
+
const entity = this.findEntityForTable(diff.tableName);
|
|
6559
|
+
if (entity) {
|
|
6560
|
+
statements.push(this.schemaGenerator.generateCreateTable(entity));
|
|
6561
|
+
}
|
|
6562
|
+
} else if (diff.type === 'alter') {
|
|
6563
|
+
const filteredDiff = this.filterDiff(diff, options);
|
|
6564
|
+
const alterStatements = this.schemaGenerator.generateAlterTable(filteredDiff);
|
|
6565
|
+
statements.push(...alterStatements);
|
|
6566
|
+
}
|
|
6567
|
+
}
|
|
6568
|
+
if (statements.length === 0) {
|
|
6569
|
+
if (options.logging) this.logger.logSchema('Schema is already in sync.');
|
|
6570
|
+
return;
|
|
6571
|
+
}
|
|
6572
|
+
await this.executeSyncStatements(statements, options);
|
|
6573
|
+
}
|
|
6574
|
+
filterDiff(diff, options) {
|
|
6575
|
+
const filteredDiff = {
|
|
6576
|
+
...diff
|
|
6577
|
+
};
|
|
6578
|
+
if (options.safe !== false) {
|
|
6579
|
+
// In safe mode, we only allow additions (creating tables/columns)
|
|
6580
|
+
// We block drops and alterations to prevent accidental data loss
|
|
6581
|
+
if (filteredDiff.columnsToDrop?.length) {
|
|
6582
|
+
this.logger.logSkippedMigration(`[AutoSync] Skipped dropping ${filteredDiff.columnsToDrop.length} columns in table '${diff.tableName}': ${filteredDiff.columnsToDrop.join(', ')} (safe mode active)`);
|
|
6583
|
+
delete filteredDiff.columnsToDrop;
|
|
6584
|
+
}
|
|
6585
|
+
if (filteredDiff.columnsToAlter?.length) {
|
|
6586
|
+
this.logger.logSkippedMigration(`[AutoSync] Skipped altering ${filteredDiff.columnsToAlter.length} columns in table '${diff.tableName}': ${filteredDiff.columnsToAlter.map((c)=>c.to.name).join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
|
|
6587
|
+
delete filteredDiff.columnsToAlter;
|
|
6588
|
+
}
|
|
6589
|
+
delete filteredDiff.indexesToDrop;
|
|
6590
|
+
delete filteredDiff.foreignKeysToDrop;
|
|
6591
|
+
}
|
|
6592
|
+
if (!options.drop && filteredDiff.columnsToDrop?.length) {
|
|
6593
|
+
this.logger.logSkippedMigration(`[AutoSync] Skipped dropping ${filteredDiff.columnsToDrop.length} columns in table '${diff.tableName}' (drop: false). Use { drop: true } to apply.`);
|
|
6594
|
+
delete filteredDiff.columnsToDrop;
|
|
6595
|
+
}
|
|
6596
|
+
return filteredDiff;
|
|
6597
|
+
}
|
|
6598
|
+
async executeSyncStatements(statements, options) {
|
|
6599
|
+
const querier = await this.pool.getQuerier();
|
|
6600
|
+
try {
|
|
6601
|
+
if (this.dialect === 'mongodb') {
|
|
6602
|
+
await this.executeMongoSyncStatements(statements, options, querier);
|
|
6603
|
+
} else {
|
|
6604
|
+
await this.executeSqlSyncStatements(statements, options, querier);
|
|
6605
|
+
}
|
|
6606
|
+
if (options.logging) this.logger.logSchema('Schema synchronization completed');
|
|
6607
|
+
} catch (error) {
|
|
6608
|
+
if (this.dialect !== 'mongodb' && isSqlQuerier(querier)) {
|
|
6609
|
+
await querier.rollbackTransaction();
|
|
6610
|
+
}
|
|
6611
|
+
throw error;
|
|
6612
|
+
} finally{
|
|
6613
|
+
await querier.release();
|
|
6614
|
+
}
|
|
6615
|
+
}
|
|
6616
|
+
async executeMongoSyncStatements(statements, options, querier) {
|
|
6617
|
+
const db = querier.db;
|
|
6618
|
+
for (const stmt of statements){
|
|
6619
|
+
const cmd = JSON.parse(stmt);
|
|
6620
|
+
if (options.logging) this.logger.logSchema(`Executing MongoDB: ${stmt}`);
|
|
6621
|
+
const collectionName = cmd.name || cmd.collection;
|
|
6622
|
+
if (!collectionName) {
|
|
6623
|
+
throw new Error(`MongoDB command missing collection name: ${stmt}`);
|
|
6624
|
+
}
|
|
6625
|
+
const collection = db.collection(collectionName);
|
|
6626
|
+
if (cmd.action === 'createCollection') {
|
|
6627
|
+
await db.createCollection(cmd.name);
|
|
6628
|
+
if (cmd.indexes?.length) {
|
|
6629
|
+
for (const idx of cmd.indexes){
|
|
6630
|
+
const key = Object.fromEntries(idx.columns.map((c)=>[
|
|
6631
|
+
c,
|
|
6632
|
+
1
|
|
6633
|
+
]));
|
|
6634
|
+
await collection.createIndex(key, {
|
|
6635
|
+
unique: idx.unique,
|
|
6636
|
+
name: idx.name
|
|
6637
|
+
});
|
|
6638
|
+
}
|
|
6639
|
+
}
|
|
6640
|
+
} else if (cmd.action === 'dropCollection') {
|
|
6641
|
+
await collection.drop();
|
|
6642
|
+
} else if (cmd.action === 'createIndex') {
|
|
6643
|
+
await collection.createIndex(cmd.key, cmd.options);
|
|
6644
|
+
} else if (cmd.action === 'dropIndex') {
|
|
6645
|
+
await collection.dropIndex(cmd.name);
|
|
6646
|
+
}
|
|
6647
|
+
}
|
|
6648
|
+
}
|
|
6649
|
+
async executeSqlSyncStatements(statements, options, querier) {
|
|
6650
|
+
if (!isSqlQuerier(querier)) {
|
|
6651
|
+
throw new Error('Migrator requires a SQL-based querier for this dialect');
|
|
6652
|
+
}
|
|
6653
|
+
await querier.beginTransaction();
|
|
6654
|
+
for (const sql of statements){
|
|
6655
|
+
if (options.logging) this.logger.logSchema(`Executing: ${sql}`);
|
|
6656
|
+
await querier.run(sql);
|
|
6657
|
+
}
|
|
6658
|
+
await querier.commitTransaction();
|
|
6659
|
+
}
|
|
6660
|
+
/**
|
|
6661
|
+
* Get migration status
|
|
6662
|
+
*/ async status() {
|
|
6663
|
+
const [pending, executed] = await Promise.all([
|
|
6664
|
+
this.pending().then((m)=>m.map((x)=>x.name)),
|
|
6665
|
+
this.executed()
|
|
6666
|
+
]);
|
|
6667
|
+
return {
|
|
6668
|
+
pending,
|
|
6669
|
+
executed
|
|
6670
|
+
};
|
|
6671
|
+
}
|
|
6672
|
+
/**
|
|
6673
|
+
* Get migration files from the migrations directory
|
|
6674
|
+
*/ async getMigrationFiles() {
|
|
6675
|
+
try {
|
|
6676
|
+
const files = await readdir(this.migrationsPath);
|
|
6677
|
+
return files.filter((f)=>/\.(ts|js|mjs)$/.test(f)).filter((f)=>!f.endsWith('.d.ts')).sort();
|
|
6678
|
+
} catch (error) {
|
|
6679
|
+
if (error.code === 'ENOENT') {
|
|
6680
|
+
return [];
|
|
6681
|
+
}
|
|
6682
|
+
throw error;
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
/**
|
|
6686
|
+
* Load a migration from a file
|
|
6687
|
+
*/ async loadMigration(fileName) {
|
|
6688
|
+
const filePath = join(this.migrationsPath, fileName);
|
|
6689
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
6690
|
+
try {
|
|
6691
|
+
const module = await import(fileUrl);
|
|
6692
|
+
const migration = module.default ?? module;
|
|
6693
|
+
if (this.isMigration(migration)) {
|
|
6694
|
+
return {
|
|
6695
|
+
name: this.getMigrationName(fileName),
|
|
6696
|
+
up: migration.up.bind(migration),
|
|
6697
|
+
down: migration.down.bind(migration)
|
|
6698
|
+
};
|
|
6699
|
+
}
|
|
6700
|
+
this.logger.logWarn(`Warning: ${fileName} is not a valid migration`);
|
|
6701
|
+
return undefined;
|
|
6702
|
+
} catch (error) {
|
|
6703
|
+
this.logger.logError(`Error loading migration ${fileName}: ${error.message}`, error);
|
|
6704
|
+
return undefined;
|
|
6705
|
+
}
|
|
6706
|
+
}
|
|
6707
|
+
/**
|
|
6708
|
+
* Check if an object is a valid migration
|
|
6709
|
+
*/ isMigration(obj) {
|
|
6710
|
+
return typeof obj === 'object' && obj !== undefined && obj !== null && typeof obj.up === 'function' && typeof obj.down === 'function';
|
|
6711
|
+
}
|
|
6712
|
+
/**
|
|
6713
|
+
* Extract migration name from filename
|
|
6714
|
+
*/ getMigrationName(fileName) {
|
|
6715
|
+
return basename(fileName, extname(fileName));
|
|
6716
|
+
}
|
|
6717
|
+
/**
|
|
6718
|
+
* Generate timestamp string for migration names
|
|
6719
|
+
*/ getTimestamp() {
|
|
6720
|
+
const now = new Date();
|
|
6721
|
+
return [
|
|
6722
|
+
now.getFullYear(),
|
|
6723
|
+
String(now.getMonth() + 1).padStart(2, '0'),
|
|
6724
|
+
String(now.getDate()).padStart(2, '0'),
|
|
6725
|
+
String(now.getHours()).padStart(2, '0'),
|
|
6726
|
+
String(now.getMinutes()).padStart(2, '0'),
|
|
6727
|
+
String(now.getSeconds()).padStart(2, '0')
|
|
6728
|
+
].join('');
|
|
6729
|
+
}
|
|
6730
|
+
/**
|
|
6731
|
+
* Convert a string to a slug for filenames
|
|
6732
|
+
*/ slugify(text) {
|
|
6733
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
6734
|
+
}
|
|
6735
|
+
/**
|
|
6736
|
+
* Generate migration file content
|
|
6737
|
+
*/ generateMigrationContent(name) {
|
|
6738
|
+
return /*ts*/ `import type { SqlQuerier } from 'uql-orm/migrate';
|
|
6739
|
+
|
|
6740
|
+
/**
|
|
6741
|
+
* Migration: ${name}
|
|
6742
|
+
* Created: ${new Date().toISOString()}
|
|
6743
|
+
*/
|
|
6744
|
+
export default {
|
|
6745
|
+
async up(querier: SqlQuerier): Promise<void> {
|
|
6746
|
+
// Add your migration logic here
|
|
6747
|
+
// Example:
|
|
6748
|
+
// await querier.run(\`
|
|
6749
|
+
// CREATE TABLE "users" (
|
|
6750
|
+
// "id" SERIAL PRIMARY KEY,
|
|
6751
|
+
// "name" VARCHAR(255) NOT NULL,
|
|
6752
|
+
// "email" VARCHAR(255) UNIQUE NOT NULL,
|
|
6753
|
+
// "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
6754
|
+
// )
|
|
6755
|
+
// \`);
|
|
6756
|
+
},
|
|
6757
|
+
|
|
6758
|
+
async down(querier: SqlQuerier): Promise<void> {
|
|
6759
|
+
// Add your rollback logic here
|
|
6760
|
+
// Example:
|
|
6761
|
+
// await querier.run(\`DROP TABLE IF EXISTS "users"\`);
|
|
6762
|
+
},
|
|
6763
|
+
};
|
|
6764
|
+
`;
|
|
6765
|
+
}
|
|
6766
|
+
/**
|
|
6767
|
+
* Generate migration file content with SQL statements
|
|
6768
|
+
*/ generateMigrationContentWithStatements(name, upStatements, downStatements) {
|
|
6769
|
+
const upSql = upStatements.map((s)=>/*ts*/ ` await querier.run(\`${s}\`);`).join('\n');
|
|
6770
|
+
const downSql = downStatements.map((s)=>/*ts*/ ` await querier.run(\`${s}\`);`).join('\n');
|
|
6771
|
+
return /*ts*/ `import type { SqlQuerier } from 'uql-orm/migrate';
|
|
6772
|
+
|
|
6773
|
+
/**
|
|
6774
|
+
* Migration: ${name}
|
|
6775
|
+
* Created: ${new Date().toISOString()}
|
|
6776
|
+
* Generated from entity definitions
|
|
6777
|
+
*/
|
|
6778
|
+
export default {
|
|
6779
|
+
async up(querier: SqlQuerier): Promise<void> {
|
|
6780
|
+
${upSql}
|
|
6781
|
+
},
|
|
6782
|
+
|
|
6783
|
+
async down(querier: SqlQuerier): Promise<void> {
|
|
6784
|
+
${downSql}
|
|
6785
|
+
},
|
|
6786
|
+
};
|
|
6787
|
+
`;
|
|
6788
|
+
}
|
|
6789
|
+
}
|
|
6790
|
+
/**
|
|
6791
|
+
* Helper function to define a migration with proper typing
|
|
6792
|
+
*/ function defineMigration(migration) {
|
|
6793
|
+
return migration;
|
|
6794
|
+
}
|
|
6795
|
+
/**
|
|
6796
|
+
* Define a migration using the type-safe builder API.
|
|
6797
|
+
*
|
|
6798
|
+
* @example
|
|
6799
|
+
* ```ts
|
|
6800
|
+
* export default defineBuilderMigration({
|
|
6801
|
+
* async up(m) {
|
|
6802
|
+
* await m.createTable('users', (table) => {
|
|
6803
|
+
* table.id();
|
|
6804
|
+
* table.string('email', 255).unique();
|
|
6805
|
+
* table.timestamps();
|
|
6806
|
+
* });
|
|
6807
|
+
* },
|
|
6808
|
+
* async down(m) {
|
|
6809
|
+
* await m.dropTable('users');
|
|
6810
|
+
* }
|
|
6811
|
+
* });
|
|
6812
|
+
* ```
|
|
6813
|
+
*/ function defineBuilderMigration(migration) {
|
|
6814
|
+
return migration;
|
|
6815
|
+
}
|
|
6816
|
+
|
|
6817
|
+
/**
|
|
6818
|
+
* Stores migration state in a JSON file.
|
|
6819
|
+
* Useful for development or environments without a database.
|
|
6820
|
+
*/ class JsonMigrationStorage {
|
|
6821
|
+
constructor(filePath = './migrations/.uql-migrations.json'){
|
|
6822
|
+
this.executedMigrations = [];
|
|
6823
|
+
this.filePath = filePath;
|
|
6824
|
+
}
|
|
6825
|
+
async ensureStorage() {
|
|
6826
|
+
const content = await fs.readFile(this.filePath, 'utf-8').catch(async (error)=>{
|
|
6827
|
+
if (error.code === 'ENOENT') {
|
|
6828
|
+
const initial = '[]';
|
|
6829
|
+
await fs.writeFile(this.filePath, initial, 'utf-8');
|
|
6830
|
+
return initial;
|
|
6831
|
+
}
|
|
6832
|
+
throw error;
|
|
6833
|
+
});
|
|
6834
|
+
this.executedMigrations = JSON.parse(content);
|
|
6835
|
+
}
|
|
6836
|
+
async executed() {
|
|
6837
|
+
await this.ensureStorage();
|
|
6838
|
+
return this.executedMigrations;
|
|
6839
|
+
}
|
|
6840
|
+
async logWithQuerier(_querier, migrationName) {
|
|
6841
|
+
await this.ensureStorage();
|
|
6842
|
+
if (!this.executedMigrations.includes(migrationName)) {
|
|
6843
|
+
this.executedMigrations.push(migrationName);
|
|
6844
|
+
await this.save();
|
|
6845
|
+
}
|
|
6846
|
+
}
|
|
6847
|
+
async unlogWithQuerier(_querier, migrationName) {
|
|
6848
|
+
await this.ensureStorage();
|
|
6849
|
+
this.executedMigrations = this.executedMigrations.filter((m)=>m !== migrationName);
|
|
6850
|
+
await this.save();
|
|
6851
|
+
}
|
|
6852
|
+
async save() {
|
|
6853
|
+
await fs.writeFile(this.filePath, JSON.stringify(this.executedMigrations, null, 2), 'utf-8');
|
|
6854
|
+
}
|
|
6855
|
+
}
|
|
6856
|
+
|
|
6857
|
+
/**
|
|
6858
|
+
* Service for synchronizing schemas between entities and database.
|
|
6859
|
+
*/ class SchemaSync {
|
|
6860
|
+
constructor(options){
|
|
6861
|
+
this.options = options;
|
|
6862
|
+
}
|
|
6863
|
+
/**
|
|
6864
|
+
* Execute schema synchronization.
|
|
6865
|
+
*/ async sync() {
|
|
6866
|
+
const direction = this.options.direction ?? 'bidirectional';
|
|
6867
|
+
switch(direction){
|
|
6868
|
+
case 'entity-to-db':
|
|
6869
|
+
return this.syncEntityToDb();
|
|
6870
|
+
case 'db-to-entity':
|
|
6871
|
+
return this.syncDbToEntity();
|
|
6872
|
+
case 'bidirectional':
|
|
6873
|
+
return this.syncBidirectional();
|
|
6874
|
+
}
|
|
6875
|
+
}
|
|
6876
|
+
/**
|
|
6877
|
+
* Sync from entities to database (push).
|
|
6878
|
+
*/ async syncEntityToDb() {
|
|
6879
|
+
const entityAST = this.buildEntityAST();
|
|
6880
|
+
const dbAST = await this.buildDatabaseAST();
|
|
6881
|
+
const differ = new SchemaASTDiffer();
|
|
6882
|
+
const diff = differ.diff(entityAST, dbAST);
|
|
6883
|
+
// In safe mode, filter out destructive changes
|
|
6884
|
+
const safeDiff = this.options.safe ? this.filterDestructiveChanges(diff) : diff;
|
|
6885
|
+
return {
|
|
6886
|
+
direction: 'entity-to-db',
|
|
6887
|
+
success: true,
|
|
6888
|
+
dbChanges: safeDiff,
|
|
6889
|
+
entityChanges: undefined,
|
|
6890
|
+
conflicts: [],
|
|
6891
|
+
summary: this.formatSummary('entity-to-db', safeDiff),
|
|
6892
|
+
details: {
|
|
6893
|
+
db: this.getDiffCounts(safeDiff),
|
|
6894
|
+
entity: {
|
|
6895
|
+
created: 0,
|
|
6896
|
+
dropped: 0,
|
|
6897
|
+
altered: 0
|
|
6898
|
+
}
|
|
6899
|
+
}
|
|
6900
|
+
};
|
|
6901
|
+
}
|
|
6902
|
+
/**
|
|
6903
|
+
* Sync from database to entities (pull).
|
|
6904
|
+
*/ async syncDbToEntity() {
|
|
6905
|
+
const entityAST = this.buildEntityAST();
|
|
6906
|
+
const dbAST = await this.buildDatabaseAST();
|
|
6907
|
+
const differ = new SchemaASTDiffer();
|
|
6908
|
+
// Diff in opposite direction: DB is source, entity is target
|
|
6909
|
+
const diff = differ.diff(dbAST, entityAST);
|
|
6910
|
+
// Apply safe mode if enabled
|
|
6911
|
+
const safeDiff = this.options.safe ? this.filterDestructiveChanges(diff) : diff;
|
|
6912
|
+
return {
|
|
6913
|
+
direction: 'db-to-entity',
|
|
6914
|
+
success: true,
|
|
6915
|
+
dbChanges: undefined,
|
|
6916
|
+
entityChanges: safeDiff,
|
|
6917
|
+
conflicts: [],
|
|
6918
|
+
summary: this.formatSummary('db-to-entity', safeDiff),
|
|
6919
|
+
details: {
|
|
6920
|
+
db: {
|
|
6921
|
+
created: 0,
|
|
6922
|
+
dropped: 0,
|
|
6923
|
+
altered: 0
|
|
6924
|
+
},
|
|
6925
|
+
entity: this.getDiffCounts(safeDiff)
|
|
6926
|
+
}
|
|
6927
|
+
};
|
|
6928
|
+
}
|
|
6929
|
+
/**
|
|
6930
|
+
* Bidirectional sync with conflict detection.
|
|
6931
|
+
*/ async syncBidirectional() {
|
|
6932
|
+
const entityAST = this.buildEntityAST();
|
|
6933
|
+
const dbAST = await this.buildDatabaseAST();
|
|
6934
|
+
const differ = new SchemaASTDiffer();
|
|
6935
|
+
// Get both directions
|
|
6936
|
+
const entityToDb = differ.diff(entityAST, dbAST);
|
|
6937
|
+
const dbToEntity = differ.diff(dbAST, entityAST);
|
|
6938
|
+
// Detect conflicts (changes in both directions for same elements)
|
|
6939
|
+
const conflicts = this.detectConflicts(entityToDb, dbToEntity);
|
|
6940
|
+
// Filter out conflicting changes
|
|
6941
|
+
let safeEntityToDb = this.filterConflictingChanges(entityToDb, conflicts);
|
|
6942
|
+
let safeDbToEntity = this.filterConflictingChanges(dbToEntity, conflicts);
|
|
6943
|
+
// Apply safe mode if enabled
|
|
6944
|
+
if (this.options.safe) {
|
|
6945
|
+
safeEntityToDb = this.filterDestructiveChanges(safeEntityToDb);
|
|
6946
|
+
safeDbToEntity = this.filterDestructiveChanges(safeDbToEntity);
|
|
6947
|
+
}
|
|
6948
|
+
return {
|
|
6949
|
+
direction: 'bidirectional',
|
|
6950
|
+
success: conflicts.length === 0,
|
|
6951
|
+
dbChanges: safeEntityToDb,
|
|
6952
|
+
entityChanges: safeDbToEntity,
|
|
6953
|
+
conflicts,
|
|
6954
|
+
summary: this.formatBidirectionalSummary(safeEntityToDb, safeDbToEntity, conflicts),
|
|
6955
|
+
details: {
|
|
6956
|
+
db: this.getDiffCounts(safeEntityToDb),
|
|
6957
|
+
entity: this.getDiffCounts(safeDbToEntity)
|
|
6958
|
+
}
|
|
6959
|
+
};
|
|
6960
|
+
}
|
|
6961
|
+
/**
|
|
6962
|
+
* Build SchemaAST from entities.
|
|
6963
|
+
*/ buildEntityAST() {
|
|
6964
|
+
const builder = new SchemaASTBuilder();
|
|
6965
|
+
return builder.fromEntities(this.options.entities);
|
|
6966
|
+
}
|
|
6967
|
+
/**
|
|
6968
|
+
* Build SchemaAST from database.
|
|
6969
|
+
*/ async buildDatabaseAST() {
|
|
6970
|
+
return this.options.introspector.introspect();
|
|
6971
|
+
}
|
|
6972
|
+
/**
|
|
6973
|
+
* Filter destructive changes for safe mode.
|
|
6974
|
+
*/ filterDestructiveChanges(diff) {
|
|
6975
|
+
return {
|
|
6976
|
+
...diff,
|
|
6977
|
+
tablesToDrop: [],
|
|
6978
|
+
columnDiffs: diff.columnDiffs.filter((d)=>d.type !== 'drop'),
|
|
6979
|
+
indexDiffs: diff.indexDiffs.filter((d)=>d.type !== 'drop'),
|
|
6980
|
+
relationshipDiffs: diff.relationshipDiffs.filter((d)=>d.type !== 'drop')
|
|
6981
|
+
};
|
|
6982
|
+
}
|
|
6983
|
+
/**
|
|
6984
|
+
* Detect conflicts between bidirectional changes.
|
|
6985
|
+
*/ detectConflicts(entityToDb, dbToEntity) {
|
|
6986
|
+
const conflicts = [];
|
|
6987
|
+
// Find column alterations that exist in both directions (type conflicts)
|
|
6988
|
+
const entityColAlters = new Map(entityToDb.columnDiffs.filter((d)=>d.type === 'alter').map((d)=>[
|
|
6989
|
+
`${d.table}.${d.column}`,
|
|
6990
|
+
d
|
|
6991
|
+
]));
|
|
6992
|
+
for (const dbColDiff of dbToEntity.columnDiffs.filter((d)=>d.type === 'alter')){
|
|
6993
|
+
const key = `${dbColDiff.table}.${dbColDiff.column}`;
|
|
6994
|
+
const entityColDiff = entityColAlters.get(key);
|
|
6995
|
+
if (entityColDiff) {
|
|
6996
|
+
// Both sides want to alter the same column - conflict
|
|
6997
|
+
conflicts.push({
|
|
6998
|
+
type: 'type_mismatch',
|
|
6999
|
+
table: dbColDiff.table,
|
|
7000
|
+
column: dbColDiff.column,
|
|
7001
|
+
entityValue: entityColDiff.expected?.type,
|
|
7002
|
+
dbValue: dbColDiff.expected?.type,
|
|
7003
|
+
suggestion: `Column has different types. Entity: ${this.formatType(entityColDiff.expected?.type)}, DB: ${this.formatType(dbColDiff.expected?.type)}`
|
|
7004
|
+
});
|
|
7005
|
+
}
|
|
7006
|
+
}
|
|
7007
|
+
return conflicts;
|
|
7008
|
+
}
|
|
7009
|
+
/**
|
|
7010
|
+
* Filter out conflicting changes.
|
|
7011
|
+
*/ filterConflictingChanges(diff, conflicts) {
|
|
7012
|
+
const conflictKeys = new Set(conflicts.map((c)=>`${c.table}.${c.column ?? ''}`));
|
|
7013
|
+
return {
|
|
7014
|
+
...diff,
|
|
7015
|
+
columnDiffs: diff.columnDiffs.filter((d)=>!conflictKeys.has(`${d.table}.${d.column}`)),
|
|
7016
|
+
tablesToAlter: diff.tablesToAlter.filter((t)=>!conflictKeys.has(`${t.name}.`))
|
|
7017
|
+
};
|
|
7018
|
+
}
|
|
7019
|
+
/**
|
|
7020
|
+
* Get counts of changes from a diff.
|
|
7021
|
+
*/ getDiffCounts(diff) {
|
|
7022
|
+
return {
|
|
7023
|
+
created: diff.tablesToCreate.length + diff.columnDiffs.filter((d)=>d.type === 'add').length,
|
|
7024
|
+
dropped: diff.tablesToDrop.length + diff.columnDiffs.filter((d)=>d.type === 'drop').length,
|
|
7025
|
+
altered: diff.columnDiffs.filter((d)=>d.type === 'alter').length
|
|
7026
|
+
};
|
|
7027
|
+
}
|
|
7028
|
+
/**
|
|
7029
|
+
* Format summary for single-direction sync.
|
|
7030
|
+
*/ formatSummary(direction, diff) {
|
|
7031
|
+
const lines = [];
|
|
7032
|
+
if (direction === 'entity-to-db') {
|
|
7033
|
+
lines.push('Entity → Database Changes:');
|
|
7034
|
+
} else {
|
|
7035
|
+
lines.push('Database → Entity Changes:');
|
|
7036
|
+
}
|
|
7037
|
+
if (diff.tablesToCreate.length > 0) {
|
|
7038
|
+
lines.push(` + ${diff.tablesToCreate.length} table(s) to create`);
|
|
7039
|
+
}
|
|
7040
|
+
if (diff.tablesToDrop.length > 0) {
|
|
7041
|
+
lines.push(` - ${diff.tablesToDrop.length} table(s) to drop`);
|
|
7042
|
+
}
|
|
7043
|
+
if (diff.columnDiffs.length > 0) {
|
|
7044
|
+
const adds = diff.columnDiffs.filter((d)=>d.type === 'add').length;
|
|
7045
|
+
const drops = diff.columnDiffs.filter((d)=>d.type === 'drop').length;
|
|
7046
|
+
const alters = diff.columnDiffs.filter((d)=>d.type === 'alter').length;
|
|
7047
|
+
if (adds) lines.push(` + ${adds} column(s) to add`);
|
|
7048
|
+
if (drops) lines.push(` - ${drops} column(s) to drop`);
|
|
7049
|
+
if (alters) lines.push(` ~ ${alters} column(s) to alter`);
|
|
7050
|
+
}
|
|
7051
|
+
if (diff.indexDiffs.length > 0) {
|
|
7052
|
+
lines.push(` ${diff.indexDiffs.length} index change(s)`);
|
|
7053
|
+
}
|
|
7054
|
+
if (!diff.hasDifferences) {
|
|
7055
|
+
lines.push(' Schema is already in sync.');
|
|
7056
|
+
}
|
|
7057
|
+
return lines.join('\n');
|
|
7058
|
+
}
|
|
7059
|
+
/**
|
|
7060
|
+
* Format summary for bidirectional sync.
|
|
7061
|
+
*/ formatBidirectionalSummary(entityToDb, dbToEntity, conflicts) {
|
|
7062
|
+
const lines = [];
|
|
7063
|
+
lines.push('Bidirectional Sync Report');
|
|
7064
|
+
lines.push('========================\n');
|
|
7065
|
+
if (conflicts.length > 0) {
|
|
7066
|
+
lines.push(`⚠️ ${conflicts.length} conflict(s) require manual resolution:\n`);
|
|
7067
|
+
for (const conflict of conflicts){
|
|
7068
|
+
lines.push(` • ${conflict.table}.${conflict.column ?? ''}: ${conflict.suggestion}`);
|
|
7069
|
+
}
|
|
7070
|
+
lines.push('');
|
|
7071
|
+
}
|
|
7072
|
+
if (entityToDb.hasDifferences) {
|
|
7073
|
+
lines.push('Entity → Database:');
|
|
7074
|
+
lines.push(this.formatSummary('entity-to-db', entityToDb).split('\n').slice(1).join('\n'));
|
|
7075
|
+
lines.push('');
|
|
7076
|
+
}
|
|
7077
|
+
if (dbToEntity.hasDifferences) {
|
|
7078
|
+
lines.push('Database → Entity:');
|
|
7079
|
+
lines.push(this.formatSummary('db-to-entity', dbToEntity).split('\n').slice(1).join('\n'));
|
|
7080
|
+
}
|
|
7081
|
+
if (!entityToDb.hasDifferences && !dbToEntity.hasDifferences && conflicts.length === 0) {
|
|
7082
|
+
lines.push('✓ Schema is in sync.');
|
|
7083
|
+
}
|
|
7084
|
+
return lines.join('\n');
|
|
7085
|
+
}
|
|
7086
|
+
/**
|
|
7087
|
+
* Format type for display.
|
|
7088
|
+
*/ formatType(type) {
|
|
7089
|
+
if (!type?.category) return 'unknown';
|
|
7090
|
+
return type.length ? `${type.category}(${type.length})` : type.category;
|
|
7091
|
+
}
|
|
7092
|
+
}
|
|
7093
|
+
/**
|
|
7094
|
+
* Create a SchemaSync instance.
|
|
7095
|
+
*/ function createSchemaSync(options) {
|
|
7096
|
+
return new SchemaSync(options);
|
|
7097
|
+
}
|
|
7098
|
+
|
|
7099
|
+
export { AbstractSqlSchemaIntrospector, ColumnBuilder, DatabaseMigrationStorage, DriftDetector, EntityCodeGenerator, EntityMerger, JsonMigrationStorage, MariadbSchemaIntrospector, MigrationBuilder, MigrationCodeGenerator, Migrator, MongoSchemaGenerator, MongoSchemaIntrospector, MysqlSchemaIntrospector, OperationRecorder, PostgresSchemaIntrospector, SchemaSync, SmartRelationDetector, SqlExpression, SqlSchemaGenerator, SqliteSchemaIntrospector, TableBuilder, createDriftDetector, createDryRunBuilder, createEntityCodeGenerator, createEntityMerger, createMigrationCodeGenerator, createRelationDetector, createSchemaGenerator, createSchemaSync, defineBuilderMigration, defineMigration, detectDrift, formatDefaultValue, loadConfig, t };
|
|
7100
|
+
//# sourceMappingURL=uql-browser.min.js.map
|