@noormdev/sdk 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +5 -3
- package/dist/index.js +262 -190
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1957,7 +1957,9 @@ declare class ChangeManager {
|
|
|
1957
1957
|
* List all changes with status.
|
|
1958
1958
|
*
|
|
1959
1959
|
* Merges changes from disk with status from database.
|
|
1960
|
-
* Includes orphaned changes (in DB but not on disk)
|
|
1960
|
+
* Includes orphaned changes (in DB but not on disk), and folders on disk
|
|
1961
|
+
* that cannot be parsed: `failed` with the parse error when they never
|
|
1962
|
+
* ran, otherwise their recorded status.
|
|
1961
1963
|
*
|
|
1962
1964
|
* @returns Array of changes with merged status
|
|
1963
1965
|
*/
|
|
@@ -2585,7 +2587,7 @@ export declare class RunNamespace {
|
|
|
2585
2587
|
*
|
|
2586
2588
|
* @example
|
|
2587
2589
|
* ```typescript
|
|
2588
|
-
* const files = await ctx.noorm.run.discover('sql/')
|
|
2590
|
+
* const files = await ctx.noorm.run.discover('db/sql/')
|
|
2589
2591
|
* ```
|
|
2590
2592
|
*/
|
|
2591
2593
|
discover(dirpath?: string): Promise<string[]>;
|
|
@@ -2594,7 +2596,7 @@ export declare class RunNamespace {
|
|
|
2594
2596
|
*
|
|
2595
2597
|
* @example
|
|
2596
2598
|
* ```typescript
|
|
2597
|
-
* const results = await ctx.noorm.run.preview(['sql/001.sql', 'sql/002.sql'])
|
|
2599
|
+
* const results = await ctx.noorm.run.preview(['db/sql/001.sql', 'db/sql/002.sql'])
|
|
2598
2600
|
* ```
|
|
2599
2601
|
*/
|
|
2600
2602
|
preview(filepaths: string[], output?: string | null): Promise<FileResult[]>;
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { __commonJS, __toESM } from './chunk-D2XW2UOP.js';
|
|
|
13
13
|
import { existsSync, accessSync, constants, readFileSync, mkdirSync, chmodSync, createWriteStream, writeFileSync, openSync, writeSync, closeSync, unlinkSync, statSync, copyFileSync, fsyncSync, renameSync } from 'fs';
|
|
14
14
|
import path, { join, dirname, resolve, relative, sep } from 'path';
|
|
15
15
|
import { homedir, availableParallelism, userInfo } from 'os';
|
|
16
|
-
import { stat, mkdir, writeFile, unlink, rename, rm,
|
|
16
|
+
import { stat, mkdir, writeFile, unlink, rename, rm, readFile, readdir, access } from 'fs/promises';
|
|
17
17
|
import { createHash, createPrivateKey, createPublicKey, createDecipheriv, randomBytes, createCipheriv, randomUUID, hkdfSync } from 'crypto';
|
|
18
18
|
import { execSync } from 'child_process';
|
|
19
19
|
import { sql } from 'kysely';
|
|
@@ -1731,9 +1731,12 @@ var DEFAULT_BUILD_CONFIG = {
|
|
|
1731
1731
|
include: [],
|
|
1732
1732
|
exclude: []
|
|
1733
1733
|
};
|
|
1734
|
+
var DEFAULT_DB_DIR = "db";
|
|
1735
|
+
var DEFAULT_SQL_PATH = `./${DEFAULT_DB_DIR}/sql`;
|
|
1736
|
+
var DEFAULT_CHANGES_PATH = `./${DEFAULT_DB_DIR}/changes`;
|
|
1734
1737
|
var DEFAULT_PATH_CONFIG = {
|
|
1735
|
-
sql:
|
|
1736
|
-
changes:
|
|
1738
|
+
sql: DEFAULT_SQL_PATH,
|
|
1739
|
+
changes: DEFAULT_CHANGES_PATH
|
|
1737
1740
|
};
|
|
1738
1741
|
var DEFAULT_STRICT_CONFIG = {
|
|
1739
1742
|
enabled: false,
|
|
@@ -2108,6 +2111,10 @@ var SettingsManager = class {
|
|
|
2108
2111
|
/**
|
|
2109
2112
|
* Initialize a new settings file with defaults.
|
|
2110
2113
|
*
|
|
2114
|
+
* An overwrite keeps the `paths` already in the file: they say where the
|
|
2115
|
+
* project's files are, not a preference, and resetting them would point a
|
|
2116
|
+
* project laid out elsewhere at directories that do not exist.
|
|
2117
|
+
*
|
|
2111
2118
|
* @param force - Overwrite existing file if true
|
|
2112
2119
|
* @throws Error if file exists and force is false
|
|
2113
2120
|
*/
|
|
@@ -2116,7 +2123,10 @@ var SettingsManager = class {
|
|
|
2116
2123
|
if (fileExists2 && !force) {
|
|
2117
2124
|
throw new Error("Settings file already exists. Use force=true to overwrite.");
|
|
2118
2125
|
}
|
|
2126
|
+
const [previous] = fileExists2 ? await attempt(() => this.load()) : [void 0];
|
|
2127
|
+
const keptPaths = previous ? this.#document?.paths : void 0;
|
|
2119
2128
|
this.#document = createDefaultSettings();
|
|
2129
|
+
if (keptPaths) this.#document.paths = clone(keptPaths);
|
|
2120
2130
|
this.#refreshResolved();
|
|
2121
2131
|
this.#loaded = true;
|
|
2122
2132
|
await this.save();
|
|
@@ -4254,7 +4264,7 @@ function buildMssqlTvfWithTvp(rawName, params) {
|
|
|
4254
4264
|
|
|
4255
4265
|
// src/core/change/types.ts
|
|
4256
4266
|
function isPendingChange(item) {
|
|
4257
|
-
return !item.orphaned && (item.status === "pending" || item.status === "reverted" || item.status === "stale");
|
|
4267
|
+
return !item.orphaned && (item.status === "pending" || item.status === "reverted" || item.status === "stale" || item.status === "failed" && item.isNew);
|
|
4258
4268
|
}
|
|
4259
4269
|
var ChangeValidationError = class extends Error {
|
|
4260
4270
|
constructor(changeName, issue) {
|
|
@@ -4340,10 +4350,31 @@ async function parseChange(folderPath, sqlDir) {
|
|
|
4340
4350
|
hasChangelog
|
|
4341
4351
|
};
|
|
4342
4352
|
}
|
|
4353
|
+
function failureListItem(failure, status) {
|
|
4354
|
+
const { date, description } = parseName(failure.name);
|
|
4355
|
+
return {
|
|
4356
|
+
name: failure.name,
|
|
4357
|
+
path: failure.path,
|
|
4358
|
+
date,
|
|
4359
|
+
description,
|
|
4360
|
+
status: status?.status ?? "failed",
|
|
4361
|
+
appliedAt: status?.appliedAt ?? null,
|
|
4362
|
+
appliedBy: status?.appliedBy ?? null,
|
|
4363
|
+
revertedAt: status?.revertedAt ?? null,
|
|
4364
|
+
errorMessage: status ? status.errorMessage : failure.message,
|
|
4365
|
+
appliedHistoryId: status?.appliedHistoryId ?? null,
|
|
4366
|
+
isNew: !status,
|
|
4367
|
+
orphaned: false
|
|
4368
|
+
};
|
|
4369
|
+
}
|
|
4343
4370
|
async function discoverChanges(changesDir, sqlDir) {
|
|
4371
|
+
const { changes } = await scanChanges(changesDir, sqlDir);
|
|
4372
|
+
return changes;
|
|
4373
|
+
}
|
|
4374
|
+
async function scanChanges(changesDir, sqlDir) {
|
|
4344
4375
|
const [exists] = await attempt(() => stat(changesDir));
|
|
4345
4376
|
if (!exists) {
|
|
4346
|
-
return [];
|
|
4377
|
+
return { changes: [], failures: [] };
|
|
4347
4378
|
}
|
|
4348
4379
|
const [entries, readErr] = await attempt(() => readdir(changesDir, { withFileTypes: true }));
|
|
4349
4380
|
if (readErr) {
|
|
@@ -4352,12 +4383,14 @@ async function discoverChanges(changesDir, sqlDir) {
|
|
|
4352
4383
|
error: readErr,
|
|
4353
4384
|
context: { changesDir, operation: "discover" }
|
|
4354
4385
|
});
|
|
4355
|
-
return [];
|
|
4386
|
+
return { changes: [], failures: [] };
|
|
4356
4387
|
}
|
|
4357
4388
|
const folders = entries.filter((e) => e.isDirectory());
|
|
4358
4389
|
const changes = [];
|
|
4390
|
+
const failures = [];
|
|
4359
4391
|
for (const folder of folders) {
|
|
4360
4392
|
const folderPath = path.join(changesDir, folder.name);
|
|
4393
|
+
if (!await looksLikeChange(folderPath)) continue;
|
|
4361
4394
|
const [change, parseErr] = await attempt(() => parseChange(folderPath, sqlDir));
|
|
4362
4395
|
if (parseErr) {
|
|
4363
4396
|
observer.emit("error", {
|
|
@@ -4365,11 +4398,15 @@ async function discoverChanges(changesDir, sqlDir) {
|
|
|
4365
4398
|
error: parseErr,
|
|
4366
4399
|
context: { folder: folder.name, operation: "parse" }
|
|
4367
4400
|
});
|
|
4401
|
+
failures.push({ name: folder.name, path: folderPath, message: parseErr.message });
|
|
4368
4402
|
continue;
|
|
4369
4403
|
}
|
|
4370
4404
|
changes.push(change);
|
|
4371
4405
|
}
|
|
4372
|
-
return
|
|
4406
|
+
return {
|
|
4407
|
+
changes: changes.sort((a, b) => a.name.localeCompare(b.name)),
|
|
4408
|
+
failures
|
|
4409
|
+
};
|
|
4373
4410
|
}
|
|
4374
4411
|
async function resolveManifest(manifestPath, sqlDir) {
|
|
4375
4412
|
const [content, readErr] = await attempt(() => readFile(manifestPath, "utf-8"));
|
|
@@ -4428,6 +4465,13 @@ function parseName(name) {
|
|
|
4428
4465
|
}
|
|
4429
4466
|
return { date: null, description: name };
|
|
4430
4467
|
}
|
|
4468
|
+
async function looksLikeChange(folderPath) {
|
|
4469
|
+
for (const subfolder of ["change", "revert"]) {
|
|
4470
|
+
const [entry] = await attempt(() => stat(path.join(folderPath, subfolder)));
|
|
4471
|
+
if (entry?.isDirectory()) return true;
|
|
4472
|
+
}
|
|
4473
|
+
return false;
|
|
4474
|
+
}
|
|
4431
4475
|
async function scanFolder(folderPath, sqlDir) {
|
|
4432
4476
|
const [entries, err] = await attempt(() => readdir(folderPath, { withFileTypes: true }));
|
|
4433
4477
|
if (err) {
|
|
@@ -6319,6 +6363,165 @@ var StatementWatcher = class {
|
|
|
6319
6363
|
}
|
|
6320
6364
|
};
|
|
6321
6365
|
|
|
6366
|
+
// src/core/runner/sqlite-statements.ts
|
|
6367
|
+
var QUOTE_PAIRS = {
|
|
6368
|
+
"'": "'",
|
|
6369
|
+
'"': '"',
|
|
6370
|
+
"`": "`",
|
|
6371
|
+
"[": "]"
|
|
6372
|
+
};
|
|
6373
|
+
function keywordAt(content, index, keyword) {
|
|
6374
|
+
const end = index + keyword.length;
|
|
6375
|
+
if (content.slice(index, end).toUpperCase() !== keyword) return false;
|
|
6376
|
+
const before = index > 0 ? content[index - 1] : " ";
|
|
6377
|
+
const after = end < content.length ? content[end] : " ";
|
|
6378
|
+
return !/[A-Za-z0-9_$]/.test(before) && !/[A-Za-z0-9_$]/.test(after);
|
|
6379
|
+
}
|
|
6380
|
+
function splitSqliteStatements(content) {
|
|
6381
|
+
const statements = [];
|
|
6382
|
+
let current = "";
|
|
6383
|
+
let closingQuote = null;
|
|
6384
|
+
let inLineComment = false;
|
|
6385
|
+
let inBlockComment = false;
|
|
6386
|
+
let inTrigger = false;
|
|
6387
|
+
let blockDepth = 0;
|
|
6388
|
+
let i = 0;
|
|
6389
|
+
const flush = () => {
|
|
6390
|
+
const trimmed = current.trim();
|
|
6391
|
+
if (trimmed.length > 0 && !isCommentOnly(trimmed)) {
|
|
6392
|
+
statements.push(trimmed);
|
|
6393
|
+
}
|
|
6394
|
+
current = "";
|
|
6395
|
+
};
|
|
6396
|
+
while (i < content.length) {
|
|
6397
|
+
const char = content[i];
|
|
6398
|
+
if (inLineComment) {
|
|
6399
|
+
current += char;
|
|
6400
|
+
if (char === "\n") inLineComment = false;
|
|
6401
|
+
i++;
|
|
6402
|
+
continue;
|
|
6403
|
+
}
|
|
6404
|
+
if (inBlockComment) {
|
|
6405
|
+
current += char;
|
|
6406
|
+
if (char === "*" && content[i + 1] === "/") {
|
|
6407
|
+
current += "/";
|
|
6408
|
+
inBlockComment = false;
|
|
6409
|
+
i += 2;
|
|
6410
|
+
continue;
|
|
6411
|
+
}
|
|
6412
|
+
i++;
|
|
6413
|
+
continue;
|
|
6414
|
+
}
|
|
6415
|
+
if (closingQuote !== null) {
|
|
6416
|
+
current += char;
|
|
6417
|
+
if (char === closingQuote) {
|
|
6418
|
+
if (closingQuote !== "]" && content[i + 1] === closingQuote) {
|
|
6419
|
+
current += closingQuote;
|
|
6420
|
+
i += 2;
|
|
6421
|
+
continue;
|
|
6422
|
+
}
|
|
6423
|
+
closingQuote = null;
|
|
6424
|
+
}
|
|
6425
|
+
i++;
|
|
6426
|
+
continue;
|
|
6427
|
+
}
|
|
6428
|
+
if (char === "-" && content[i + 1] === "-") {
|
|
6429
|
+
inLineComment = true;
|
|
6430
|
+
current += "--";
|
|
6431
|
+
i += 2;
|
|
6432
|
+
continue;
|
|
6433
|
+
}
|
|
6434
|
+
if (char === "/" && content[i + 1] === "*") {
|
|
6435
|
+
inBlockComment = true;
|
|
6436
|
+
current += "/*";
|
|
6437
|
+
i += 2;
|
|
6438
|
+
continue;
|
|
6439
|
+
}
|
|
6440
|
+
if (QUOTE_PAIRS[char]) {
|
|
6441
|
+
closingQuote = QUOTE_PAIRS[char];
|
|
6442
|
+
current += char;
|
|
6443
|
+
i++;
|
|
6444
|
+
continue;
|
|
6445
|
+
}
|
|
6446
|
+
if (!inTrigger && keywordAt(content, i, "TRIGGER")) {
|
|
6447
|
+
inTrigger = true;
|
|
6448
|
+
current += content.slice(i, i + 7);
|
|
6449
|
+
i += 7;
|
|
6450
|
+
continue;
|
|
6451
|
+
}
|
|
6452
|
+
if (inTrigger && (keywordAt(content, i, "BEGIN") || keywordAt(content, i, "CASE"))) {
|
|
6453
|
+
const length = keywordAt(content, i, "BEGIN") ? 5 : 4;
|
|
6454
|
+
blockDepth++;
|
|
6455
|
+
current += content.slice(i, i + length);
|
|
6456
|
+
i += length;
|
|
6457
|
+
continue;
|
|
6458
|
+
}
|
|
6459
|
+
if (inTrigger && blockDepth > 0 && keywordAt(content, i, "END")) {
|
|
6460
|
+
blockDepth--;
|
|
6461
|
+
if (blockDepth === 0) inTrigger = false;
|
|
6462
|
+
current += content.slice(i, i + 3);
|
|
6463
|
+
i += 3;
|
|
6464
|
+
continue;
|
|
6465
|
+
}
|
|
6466
|
+
if (char === ";" && blockDepth === 0) {
|
|
6467
|
+
current += char;
|
|
6468
|
+
inTrigger = false;
|
|
6469
|
+
flush();
|
|
6470
|
+
i++;
|
|
6471
|
+
continue;
|
|
6472
|
+
}
|
|
6473
|
+
current += char;
|
|
6474
|
+
i++;
|
|
6475
|
+
}
|
|
6476
|
+
flush();
|
|
6477
|
+
return statements;
|
|
6478
|
+
}
|
|
6479
|
+
function isCommentOnly(fragment) {
|
|
6480
|
+
const withoutBlockComments = fragment.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
6481
|
+
return withoutBlockComments.split("\n").every((line) => {
|
|
6482
|
+
const trimmed = line.trim();
|
|
6483
|
+
return trimmed.length === 0 || trimmed.startsWith("--");
|
|
6484
|
+
});
|
|
6485
|
+
}
|
|
6486
|
+
|
|
6487
|
+
// src/core/runner/mssql-batches.ts
|
|
6488
|
+
function splitMssqlBatches(content) {
|
|
6489
|
+
const batches = content.split(/^\s*GO\s*$/mi);
|
|
6490
|
+
return batches.map((b) => b.trim()).filter((b) => b.length > 0 && !isCommentOnly2(b));
|
|
6491
|
+
}
|
|
6492
|
+
function isCommentOnly2(batch) {
|
|
6493
|
+
for (const line of batch.split("\n")) {
|
|
6494
|
+
const trimmed = line.trim();
|
|
6495
|
+
if (trimmed.length > 0 && !trimmed.startsWith("--")) {
|
|
6496
|
+
return false;
|
|
6497
|
+
}
|
|
6498
|
+
}
|
|
6499
|
+
return true;
|
|
6500
|
+
}
|
|
6501
|
+
async function executeSqlBody(context, sqlContent) {
|
|
6502
|
+
if (context.dialect === "mssql") {
|
|
6503
|
+
return executeFragments(context, splitMssqlBatches(sqlContent), "batch");
|
|
6504
|
+
}
|
|
6505
|
+
if (context.dialect === "sqlite") {
|
|
6506
|
+
return executeFragments(context, splitSqliteStatements(sqlContent), "statement");
|
|
6507
|
+
}
|
|
6508
|
+
const [, execErr] = await attempt(() => sql.raw(sqlContent).execute(context.db));
|
|
6509
|
+
if (execErr) {
|
|
6510
|
+
return getSqlErrorMessage(execErr);
|
|
6511
|
+
}
|
|
6512
|
+
return null;
|
|
6513
|
+
}
|
|
6514
|
+
async function executeFragments(context, fragments, label) {
|
|
6515
|
+
for (let i = 0; i < fragments.length; i++) {
|
|
6516
|
+
const [, execErr] = await attempt(() => sql.raw(fragments[i]).execute(context.db));
|
|
6517
|
+
if (execErr) {
|
|
6518
|
+
const message = getSqlErrorMessage(execErr);
|
|
6519
|
+
return fragments.length === 1 ? message : `[${label} ${i + 1} of ${fragments.length}] ${message}`;
|
|
6520
|
+
}
|
|
6521
|
+
}
|
|
6522
|
+
return null;
|
|
6523
|
+
}
|
|
6524
|
+
|
|
6322
6525
|
// src/core/lock/types.ts
|
|
6323
6526
|
var DEFAULT_LOCK_OPTIONS = {
|
|
6324
6527
|
dialect: "postgres",
|
|
@@ -7066,12 +7269,12 @@ async function runFileBatch(context, change, expandedFiles, direction, checksum,
|
|
|
7066
7269
|
}
|
|
7067
7270
|
break;
|
|
7068
7271
|
}
|
|
7069
|
-
const [, execErr] = await attempt(
|
|
7070
|
-
() => watcher.run(file.path, executor, (conn) =>
|
|
7272
|
+
const [execMessage, execErr] = await attempt(
|
|
7273
|
+
() => watcher.run(file.path, executor, (conn) => executeSqlBody({ db: conn, dialect: context.dialect }, sqlContent))
|
|
7071
7274
|
);
|
|
7072
7275
|
const durationMs = performance.now() - fileStart;
|
|
7073
|
-
|
|
7074
|
-
|
|
7276
|
+
const errorMessage2 = execErr ? getSqlErrorMessage(execErr) : execMessage;
|
|
7277
|
+
if (errorMessage2) {
|
|
7075
7278
|
failed = true;
|
|
7076
7279
|
failedFile = file.path;
|
|
7077
7280
|
failureError = errorMessage2;
|
|
@@ -7409,17 +7612,31 @@ var ChangeManager = class {
|
|
|
7409
7612
|
* List all changes with status.
|
|
7410
7613
|
*
|
|
7411
7614
|
* Merges changes from disk with status from database.
|
|
7412
|
-
* Includes orphaned changes (in DB but not on disk)
|
|
7615
|
+
* Includes orphaned changes (in DB but not on disk), and folders on disk
|
|
7616
|
+
* that cannot be parsed: `failed` with the parse error when they never
|
|
7617
|
+
* ran, otherwise their recorded status.
|
|
7413
7618
|
*
|
|
7414
7619
|
* @returns Array of changes with merged status
|
|
7415
7620
|
*/
|
|
7416
7621
|
async list() {
|
|
7417
|
-
const
|
|
7622
|
+
const { items } = await this.#scan();
|
|
7623
|
+
return items;
|
|
7624
|
+
}
|
|
7625
|
+
/**
|
|
7626
|
+
* Merge disk and database state, and report the folders that cannot be read.
|
|
7627
|
+
*
|
|
7628
|
+
* An unreadable folder that a batch would run (never run, or `reverted` or
|
|
7629
|
+
* `stale`) is pending work: the batch fails at its position rather than
|
|
7630
|
+
* skipping it and running later changes out of order. `unreadable` holds the
|
|
7631
|
+
* applied ones; they keep their status and only warrant a warning.
|
|
7632
|
+
*/
|
|
7633
|
+
async #scan() {
|
|
7634
|
+
const { changes: diskChanges, failures } = await scanChanges(
|
|
7418
7635
|
this.#context.changesDir,
|
|
7419
7636
|
this.#context.sqlDir
|
|
7420
7637
|
);
|
|
7421
7638
|
const statuses = await this.#history.getAllStatuses();
|
|
7422
|
-
const diskNames = new Set(diskChanges.map((cs) => cs.name));
|
|
7639
|
+
const diskNames = new Set([...diskChanges, ...failures].map((cs) => cs.name));
|
|
7423
7640
|
const orphanedNames = await this.#history.getOrphaned(diskNames);
|
|
7424
7641
|
const result = [];
|
|
7425
7642
|
for (const cs of diskChanges) {
|
|
@@ -7437,6 +7654,15 @@ var ChangeManager = class {
|
|
|
7437
7654
|
orphaned: false
|
|
7438
7655
|
});
|
|
7439
7656
|
}
|
|
7657
|
+
const unreadable = [];
|
|
7658
|
+
for (const failure of failures) {
|
|
7659
|
+
const status = statuses.get(failure.name);
|
|
7660
|
+
const item = failureListItem(failure, status);
|
|
7661
|
+
if (!isPendingChange(item)) {
|
|
7662
|
+
unreadable.push(failure);
|
|
7663
|
+
}
|
|
7664
|
+
result.push(item);
|
|
7665
|
+
}
|
|
7440
7666
|
for (const name of orphanedNames) {
|
|
7441
7667
|
const status = statuses.get(name);
|
|
7442
7668
|
result.push({
|
|
@@ -7458,7 +7684,10 @@ var ChangeManager = class {
|
|
|
7458
7684
|
hasChangelog: void 0
|
|
7459
7685
|
});
|
|
7460
7686
|
}
|
|
7461
|
-
return
|
|
7687
|
+
return {
|
|
7688
|
+
items: result.sort((a, b) => a.name.localeCompare(b.name)),
|
|
7689
|
+
unreadable
|
|
7690
|
+
};
|
|
7462
7691
|
}
|
|
7463
7692
|
// ─────────────────────────────────────────────────────────
|
|
7464
7693
|
// Run Single Change
|
|
@@ -7513,9 +7742,13 @@ var ChangeManager = class {
|
|
|
7513
7742
|
async next(count = 1, options = {}) {
|
|
7514
7743
|
const opts = { ...DEFAULT_BATCH, ...options };
|
|
7515
7744
|
const start = performance.now();
|
|
7516
|
-
const
|
|
7517
|
-
const
|
|
7518
|
-
|
|
7745
|
+
const { items, unreadable } = await this.#scan();
|
|
7746
|
+
const messages = [
|
|
7747
|
+
...await this.#collectWarnings() ?? [],
|
|
7748
|
+
...unreadable.map((f) => `Change '${f.name}' is applied but its files can no longer be read: ${f.message}`)
|
|
7749
|
+
];
|
|
7750
|
+
const warnings = messages.length > 0 ? messages : void 0;
|
|
7751
|
+
const pending = items.filter(isPendingChange).slice(0, count);
|
|
7519
7752
|
if (pending.length === 0) {
|
|
7520
7753
|
return {
|
|
7521
7754
|
status: "success",
|
|
@@ -7583,9 +7816,7 @@ var ChangeManager = class {
|
|
|
7583
7816
|
* @returns Batch result
|
|
7584
7817
|
*/
|
|
7585
7818
|
async ff(options = {}) {
|
|
7586
|
-
|
|
7587
|
-
const pendingCount = list.filter(isPendingChange).length;
|
|
7588
|
-
return this.next(pendingCount, options);
|
|
7819
|
+
return this.next(Number.POSITIVE_INFINITY, options);
|
|
7589
7820
|
}
|
|
7590
7821
|
/**
|
|
7591
7822
|
* Rewind: revert changes in reverse order.
|
|
@@ -7754,6 +7985,8 @@ var ChangeManager = class {
|
|
|
7754
7985
|
() => parseChange(changePath, this.#context.sqlDir)
|
|
7755
7986
|
);
|
|
7756
7987
|
if (err) {
|
|
7988
|
+
const [onDisk] = await attempt(() => stat(changePath));
|
|
7989
|
+
if (onDisk) throw err;
|
|
7757
7990
|
const status = await this.#history.getStatus(name);
|
|
7758
7991
|
if (status) {
|
|
7759
7992
|
throw new ChangeOrphanedError(name);
|
|
@@ -8379,13 +8612,13 @@ var ChangesNamespace = class {
|
|
|
8379
8612
|
get #changesDir() {
|
|
8380
8613
|
return path.join(
|
|
8381
8614
|
this.#state.projectRoot,
|
|
8382
|
-
this.#state.settings.paths?.changes ??
|
|
8615
|
+
this.#state.settings.paths?.changes ?? DEFAULT_CHANGES_PATH
|
|
8383
8616
|
);
|
|
8384
8617
|
}
|
|
8385
8618
|
get #sqlDir() {
|
|
8386
8619
|
return path.join(
|
|
8387
8620
|
this.#state.projectRoot,
|
|
8388
|
-
this.#state.settings.paths?.sql ??
|
|
8621
|
+
this.#state.settings.paths?.sql ?? DEFAULT_SQL_PATH
|
|
8389
8622
|
);
|
|
8390
8623
|
}
|
|
8391
8624
|
get #kysely() {
|
|
@@ -8418,167 +8651,6 @@ var ChangesNamespace = class {
|
|
|
8418
8651
|
return this.#manager;
|
|
8419
8652
|
}
|
|
8420
8653
|
};
|
|
8421
|
-
|
|
8422
|
-
// src/core/runner/sqlite-statements.ts
|
|
8423
|
-
var QUOTE_PAIRS = {
|
|
8424
|
-
"'": "'",
|
|
8425
|
-
'"': '"',
|
|
8426
|
-
"`": "`",
|
|
8427
|
-
"[": "]"
|
|
8428
|
-
};
|
|
8429
|
-
function keywordAt(content, index, keyword) {
|
|
8430
|
-
const end = index + keyword.length;
|
|
8431
|
-
if (content.slice(index, end).toUpperCase() !== keyword) return false;
|
|
8432
|
-
const before = index > 0 ? content[index - 1] : " ";
|
|
8433
|
-
const after = end < content.length ? content[end] : " ";
|
|
8434
|
-
return !/[A-Za-z0-9_$]/.test(before) && !/[A-Za-z0-9_$]/.test(after);
|
|
8435
|
-
}
|
|
8436
|
-
function splitSqliteStatements(content) {
|
|
8437
|
-
const statements = [];
|
|
8438
|
-
let current = "";
|
|
8439
|
-
let closingQuote = null;
|
|
8440
|
-
let inLineComment = false;
|
|
8441
|
-
let inBlockComment = false;
|
|
8442
|
-
let inTrigger = false;
|
|
8443
|
-
let blockDepth = 0;
|
|
8444
|
-
let i = 0;
|
|
8445
|
-
const flush = () => {
|
|
8446
|
-
const trimmed = current.trim();
|
|
8447
|
-
if (trimmed.length > 0 && !isCommentOnly(trimmed)) {
|
|
8448
|
-
statements.push(trimmed);
|
|
8449
|
-
}
|
|
8450
|
-
current = "";
|
|
8451
|
-
};
|
|
8452
|
-
while (i < content.length) {
|
|
8453
|
-
const char = content[i];
|
|
8454
|
-
if (inLineComment) {
|
|
8455
|
-
current += char;
|
|
8456
|
-
if (char === "\n") inLineComment = false;
|
|
8457
|
-
i++;
|
|
8458
|
-
continue;
|
|
8459
|
-
}
|
|
8460
|
-
if (inBlockComment) {
|
|
8461
|
-
current += char;
|
|
8462
|
-
if (char === "*" && content[i + 1] === "/") {
|
|
8463
|
-
current += "/";
|
|
8464
|
-
inBlockComment = false;
|
|
8465
|
-
i += 2;
|
|
8466
|
-
continue;
|
|
8467
|
-
}
|
|
8468
|
-
i++;
|
|
8469
|
-
continue;
|
|
8470
|
-
}
|
|
8471
|
-
if (closingQuote !== null) {
|
|
8472
|
-
current += char;
|
|
8473
|
-
if (char === closingQuote) {
|
|
8474
|
-
if (closingQuote !== "]" && content[i + 1] === closingQuote) {
|
|
8475
|
-
current += closingQuote;
|
|
8476
|
-
i += 2;
|
|
8477
|
-
continue;
|
|
8478
|
-
}
|
|
8479
|
-
closingQuote = null;
|
|
8480
|
-
}
|
|
8481
|
-
i++;
|
|
8482
|
-
continue;
|
|
8483
|
-
}
|
|
8484
|
-
if (char === "-" && content[i + 1] === "-") {
|
|
8485
|
-
inLineComment = true;
|
|
8486
|
-
current += "--";
|
|
8487
|
-
i += 2;
|
|
8488
|
-
continue;
|
|
8489
|
-
}
|
|
8490
|
-
if (char === "/" && content[i + 1] === "*") {
|
|
8491
|
-
inBlockComment = true;
|
|
8492
|
-
current += "/*";
|
|
8493
|
-
i += 2;
|
|
8494
|
-
continue;
|
|
8495
|
-
}
|
|
8496
|
-
if (QUOTE_PAIRS[char]) {
|
|
8497
|
-
closingQuote = QUOTE_PAIRS[char];
|
|
8498
|
-
current += char;
|
|
8499
|
-
i++;
|
|
8500
|
-
continue;
|
|
8501
|
-
}
|
|
8502
|
-
if (!inTrigger && keywordAt(content, i, "TRIGGER")) {
|
|
8503
|
-
inTrigger = true;
|
|
8504
|
-
current += content.slice(i, i + 7);
|
|
8505
|
-
i += 7;
|
|
8506
|
-
continue;
|
|
8507
|
-
}
|
|
8508
|
-
if (inTrigger && (keywordAt(content, i, "BEGIN") || keywordAt(content, i, "CASE"))) {
|
|
8509
|
-
const length = keywordAt(content, i, "BEGIN") ? 5 : 4;
|
|
8510
|
-
blockDepth++;
|
|
8511
|
-
current += content.slice(i, i + length);
|
|
8512
|
-
i += length;
|
|
8513
|
-
continue;
|
|
8514
|
-
}
|
|
8515
|
-
if (inTrigger && blockDepth > 0 && keywordAt(content, i, "END")) {
|
|
8516
|
-
blockDepth--;
|
|
8517
|
-
if (blockDepth === 0) inTrigger = false;
|
|
8518
|
-
current += content.slice(i, i + 3);
|
|
8519
|
-
i += 3;
|
|
8520
|
-
continue;
|
|
8521
|
-
}
|
|
8522
|
-
if (char === ";" && blockDepth === 0) {
|
|
8523
|
-
current += char;
|
|
8524
|
-
inTrigger = false;
|
|
8525
|
-
flush();
|
|
8526
|
-
i++;
|
|
8527
|
-
continue;
|
|
8528
|
-
}
|
|
8529
|
-
current += char;
|
|
8530
|
-
i++;
|
|
8531
|
-
}
|
|
8532
|
-
flush();
|
|
8533
|
-
return statements;
|
|
8534
|
-
}
|
|
8535
|
-
function isCommentOnly(fragment) {
|
|
8536
|
-
const withoutBlockComments = fragment.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
8537
|
-
return withoutBlockComments.split("\n").every((line) => {
|
|
8538
|
-
const trimmed = line.trim();
|
|
8539
|
-
return trimmed.length === 0 || trimmed.startsWith("--");
|
|
8540
|
-
});
|
|
8541
|
-
}
|
|
8542
|
-
|
|
8543
|
-
// src/core/runner/mssql-batches.ts
|
|
8544
|
-
function splitMssqlBatches(content) {
|
|
8545
|
-
const batches = content.split(/^\s*GO\s*$/mi);
|
|
8546
|
-
return batches.map((b) => b.trim()).filter((b) => b.length > 0 && !isCommentOnly2(b));
|
|
8547
|
-
}
|
|
8548
|
-
function isCommentOnly2(batch) {
|
|
8549
|
-
for (const line of batch.split("\n")) {
|
|
8550
|
-
const trimmed = line.trim();
|
|
8551
|
-
if (trimmed.length > 0 && !trimmed.startsWith("--")) {
|
|
8552
|
-
return false;
|
|
8553
|
-
}
|
|
8554
|
-
}
|
|
8555
|
-
return true;
|
|
8556
|
-
}
|
|
8557
|
-
async function executeSqlBody(context, sqlContent) {
|
|
8558
|
-
if (context.dialect === "mssql") {
|
|
8559
|
-
return executeFragments(context, splitMssqlBatches(sqlContent), "batch");
|
|
8560
|
-
}
|
|
8561
|
-
if (context.dialect === "sqlite") {
|
|
8562
|
-
return executeFragments(context, splitSqliteStatements(sqlContent), "statement");
|
|
8563
|
-
}
|
|
8564
|
-
const [, execErr] = await attempt(() => sql.raw(sqlContent).execute(context.db));
|
|
8565
|
-
if (execErr) {
|
|
8566
|
-
return getSqlErrorMessage(execErr);
|
|
8567
|
-
}
|
|
8568
|
-
return null;
|
|
8569
|
-
}
|
|
8570
|
-
async function executeFragments(context, fragments, label) {
|
|
8571
|
-
for (let i = 0; i < fragments.length; i++) {
|
|
8572
|
-
const [, execErr] = await attempt(() => sql.raw(fragments[i]).execute(context.db));
|
|
8573
|
-
if (execErr) {
|
|
8574
|
-
const message = getSqlErrorMessage(execErr);
|
|
8575
|
-
return fragments.length === 1 ? message : `[${label} ${i + 1} of ${fragments.length}] ${message}`;
|
|
8576
|
-
}
|
|
8577
|
-
}
|
|
8578
|
-
return null;
|
|
8579
|
-
}
|
|
8580
|
-
|
|
8581
|
-
// src/core/runner/runner.ts
|
|
8582
8654
|
var SQL_EXTENSIONS2 = [".sql", ".sql.tmpl"];
|
|
8583
8655
|
var FILE_HEADER_TEMPLATE = `-- ============================================================
|
|
8584
8656
|
-- File: %FILE%
|
|
@@ -8918,8 +8990,8 @@ async function prepareFiles(context, files) {
|
|
|
8918
8990
|
for (const file of files) {
|
|
8919
8991
|
const relFilepath = path.relative(context.projectRoot, file.path);
|
|
8920
8992
|
const [loaded, loadError] = await attempt(async () => {
|
|
8921
|
-
const
|
|
8922
|
-
return { sql:
|
|
8993
|
+
const sql27 = await loadAndRenderFile2(context, file.path);
|
|
8994
|
+
return { sql: sql27, checksum: computeChecksumFromContent(sql27) };
|
|
8923
8995
|
});
|
|
8924
8996
|
if (loadError) {
|
|
8925
8997
|
prepared.push({ file, relFilepath, checksum: "", sql: "", loadError });
|
|
@@ -9205,12 +9277,12 @@ var RunNamespace = class {
|
|
|
9205
9277
|
*
|
|
9206
9278
|
* @example
|
|
9207
9279
|
* ```typescript
|
|
9208
|
-
* const files = await ctx.noorm.run.discover('sql/')
|
|
9280
|
+
* const files = await ctx.noorm.run.discover('db/sql/')
|
|
9209
9281
|
* ```
|
|
9210
9282
|
*/
|
|
9211
9283
|
async discover(dirpath) {
|
|
9212
9284
|
const absolutePath = this.#resolvePath(
|
|
9213
|
-
dirpath ?? this.#state.settings.paths?.sql ??
|
|
9285
|
+
dirpath ?? this.#state.settings.paths?.sql ?? DEFAULT_SQL_PATH
|
|
9214
9286
|
);
|
|
9215
9287
|
return discoverFiles(absolutePath);
|
|
9216
9288
|
}
|
|
@@ -9222,7 +9294,7 @@ var RunNamespace = class {
|
|
|
9222
9294
|
*
|
|
9223
9295
|
* @example
|
|
9224
9296
|
* ```typescript
|
|
9225
|
-
* const results = await ctx.noorm.run.preview(['sql/001.sql', 'sql/002.sql'])
|
|
9297
|
+
* const results = await ctx.noorm.run.preview(['db/sql/001.sql', 'db/sql/002.sql'])
|
|
9226
9298
|
* ```
|
|
9227
9299
|
*/
|
|
9228
9300
|
async preview(filepaths, output) {
|
|
@@ -9299,7 +9371,7 @@ var RunNamespace = class {
|
|
|
9299
9371
|
const context = await this.#createRunContext();
|
|
9300
9372
|
const sqlPath = path.join(
|
|
9301
9373
|
this.#state.projectRoot,
|
|
9302
|
-
this.#state.settings.paths?.sql ??
|
|
9374
|
+
this.#state.settings.paths?.sql ?? DEFAULT_SQL_PATH
|
|
9303
9375
|
);
|
|
9304
9376
|
const configForMatch = {
|
|
9305
9377
|
name: this.#state.config.name,
|