@atscript/db-sql-tools 0.1.163 → 0.1.165
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.cjs +170 -5
- package/dist/index.d.cts +22 -3
- package/dist/index.d.mts +22 -3
- package/dist/index.mjs +170 -6
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -245,6 +245,32 @@ function orFragment(parts, params) {
|
|
|
245
245
|
params
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* The relevance ORDER BY of a fulltext search with no `$sort` (since 0.1.165):
|
|
250
|
+
* rows an exact-number equality matched first (`CASE WHEN … THEN 0 ELSE 1 END`,
|
|
251
|
+
* only next to a text rank — `n` binds each equality once), then `textRank`
|
|
252
|
+
* (its direction included, e.g. `ts_rank(…) DESC`), then the primary-key
|
|
253
|
+
* columns ascending (quoted, after `prefix`). Params: the CASE's, then the
|
|
254
|
+
* text rank's.
|
|
255
|
+
*/
|
|
256
|
+
function searchRankOrder(dialect, opts) {
|
|
257
|
+
const parts = [];
|
|
258
|
+
const params = [];
|
|
259
|
+
const { exactEqualities: eqs, n, textRank } = opts;
|
|
260
|
+
if (textRank && n !== void 0 && eqs.length > 0) {
|
|
261
|
+
parts.push(`CASE WHEN ${eqs.join(" OR ")} THEN 0 ELSE 1 END`);
|
|
262
|
+
for (let i = 0; i < eqs.length; i++) params.push(n);
|
|
263
|
+
}
|
|
264
|
+
if (textRank) {
|
|
265
|
+
parts.push(textRank.sql);
|
|
266
|
+
params.push(...textRank.params);
|
|
267
|
+
}
|
|
268
|
+
for (const col of opts.pkColumns) parts.push(`${opts.prefix ?? ""}${dialect.quoteIdentifier(col)} ASC`);
|
|
269
|
+
return {
|
|
270
|
+
sql: parts.join(", "),
|
|
271
|
+
params
|
|
272
|
+
};
|
|
273
|
+
}
|
|
248
274
|
//#endregion
|
|
249
275
|
//#region src/arith.ts
|
|
250
276
|
/**
|
|
@@ -501,10 +527,140 @@ function getVisitor(dialect) {
|
|
|
501
527
|
*/
|
|
502
528
|
function buildWhere(dialect, filter, opts) {
|
|
503
529
|
if (!filter || Object.keys(filter).length === 0) return EMPTY_AND;
|
|
504
|
-
|
|
530
|
+
const visitor = opts || (0, _atscript_db.containsRelationFilter)(filter) ? createFilterVisitor(dialect, {
|
|
505
531
|
...opts,
|
|
506
532
|
aliasSeq: opts?.aliasSeq ?? { n: 0 }
|
|
507
|
-
}) : getVisitor(dialect)
|
|
533
|
+
}) : getVisitor(dialect);
|
|
534
|
+
return (0, _uniqu_core.walkFilter)(collapseOrEqualities(filter), visitor) ?? EMPTY_AND;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Rewrites every `$or` whose branches include several equalities on ONE
|
|
538
|
+
* field (`{ f: v }`, `{ f: { $eq: v } }`, `{ f: { $in: [...] } }`, v a
|
|
539
|
+
* non-null scalar, all of one JS type) into one `{ f: { $in: [...] } }`
|
|
540
|
+
* branch (since 0.1.165).
|
|
541
|
+
* Same rows, but one predicate instead of an OR chain whose depth grows with
|
|
542
|
+
* the batch: SQLite refuses an expression tree deeper than 1000, which a
|
|
543
|
+
* per-row key filter over a large batch reached. Composite, null, operator
|
|
544
|
+
* and lone branches are kept as they are. Returns `filter` itself when
|
|
545
|
+
* nothing changes.
|
|
546
|
+
*/
|
|
547
|
+
function collapseOrEqualities(filter) {
|
|
548
|
+
if (!filter || typeof filter !== "object") return filter;
|
|
549
|
+
const node = filter;
|
|
550
|
+
let out;
|
|
551
|
+
const and = node.$and;
|
|
552
|
+
if (Array.isArray(and)) {
|
|
553
|
+
const next = collapseEach(and);
|
|
554
|
+
if (next !== and) (out ??= { ...node }).$and = next;
|
|
555
|
+
}
|
|
556
|
+
const or = node.$or;
|
|
557
|
+
if (Array.isArray(or)) {
|
|
558
|
+
let next = collapseEach(or);
|
|
559
|
+
if (next.length > 1 && mayRepeatField(next)) next = mergeEqualityBranches(next);
|
|
560
|
+
if (next !== or) (out ??= { ...node }).$or = next;
|
|
561
|
+
}
|
|
562
|
+
const not = node.$not;
|
|
563
|
+
if (not && typeof not === "object") {
|
|
564
|
+
const next = collapseOrEqualities(not);
|
|
565
|
+
if (next !== not) (out ??= { ...node }).$not = next;
|
|
566
|
+
}
|
|
567
|
+
return out ?? filter;
|
|
568
|
+
}
|
|
569
|
+
/** `list` with each object child collapsed — the same array when none changed (copied on the first change). */
|
|
570
|
+
function collapseEach(list) {
|
|
571
|
+
let copy;
|
|
572
|
+
for (let i = 0; i < list.length; i++) {
|
|
573
|
+
const child = list[i];
|
|
574
|
+
const next = child && typeof child === "object" ? collapseOrEqualities(child) : child;
|
|
575
|
+
if (next !== child) (copy ??= list.slice())[i] = next;
|
|
576
|
+
}
|
|
577
|
+
return copy ?? list;
|
|
578
|
+
}
|
|
579
|
+
/** The sole key of a one-key, non-operator branch — allocation-free. */
|
|
580
|
+
function soleField(branch) {
|
|
581
|
+
if (!branch || typeof branch !== "object" || Array.isArray(branch)) return void 0;
|
|
582
|
+
let field;
|
|
583
|
+
for (const key in branch) {
|
|
584
|
+
if (field !== void 0) return void 0;
|
|
585
|
+
field = key;
|
|
586
|
+
}
|
|
587
|
+
return field === void 0 || field.startsWith("$") ? void 0 : field;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Cheap pre-check for {@link mergeEqualityBranches}: whether two branches
|
|
591
|
+
* share a sole field (long lists go straight to the merge pass).
|
|
592
|
+
*/
|
|
593
|
+
function mayRepeatField(branches) {
|
|
594
|
+
if (branches.length > 64) return true;
|
|
595
|
+
for (let i = 1; i < branches.length; i++) {
|
|
596
|
+
const field = soleField(branches[i]);
|
|
597
|
+
if (field === void 0) continue;
|
|
598
|
+
for (let j = 0; j < i; j++) if (soleField(branches[j]) === field) return true;
|
|
599
|
+
}
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
/** The `$or` branches with the equalities of each repeated field merged into one `$in`. */
|
|
603
|
+
function mergeEqualityBranches(branches) {
|
|
604
|
+
const eqs = branches.map(equalityOf);
|
|
605
|
+
const fields = /* @__PURE__ */ new Map();
|
|
606
|
+
for (const eq of eqs) {
|
|
607
|
+
if (!eq) continue;
|
|
608
|
+
let entry = fields.get(eq.field);
|
|
609
|
+
if (!entry) fields.set(eq.field, entry = { count: 0 });
|
|
610
|
+
entry.count++;
|
|
611
|
+
for (const v of eq.values) {
|
|
612
|
+
const t = typeof v;
|
|
613
|
+
entry.type = entry.type === void 0 || entry.type === t ? t : "mixed";
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const merges = (field) => {
|
|
617
|
+
const entry = fields.get(field);
|
|
618
|
+
return entry.count > 1 && entry.type !== "mixed";
|
|
619
|
+
};
|
|
620
|
+
if (!eqs.some((eq) => eq && merges(eq.field))) return branches;
|
|
621
|
+
const out = [];
|
|
622
|
+
for (let i = 0; i < branches.length; i++) {
|
|
623
|
+
const eq = eqs[i];
|
|
624
|
+
if (!eq || !merges(eq.field)) {
|
|
625
|
+
out.push(branches[i]);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const entry = fields.get(eq.field);
|
|
629
|
+
if (!entry.values) {
|
|
630
|
+
entry.values = [];
|
|
631
|
+
out.push({ [eq.field]: { $in: entry.values } });
|
|
632
|
+
}
|
|
633
|
+
entry.values.push(...eq.values);
|
|
634
|
+
}
|
|
635
|
+
return out;
|
|
636
|
+
}
|
|
637
|
+
/** `{ field, values }` of a one-field equality branch, else `undefined`. */
|
|
638
|
+
function equalityOf(branch) {
|
|
639
|
+
if (!branch || typeof branch !== "object" || Array.isArray(branch)) return void 0;
|
|
640
|
+
const keys = Object.keys(branch);
|
|
641
|
+
if (keys.length !== 1 || keys[0].startsWith("$")) return void 0;
|
|
642
|
+
const field = keys[0];
|
|
643
|
+
const value = branch[field];
|
|
644
|
+
if (isScalar(value)) return {
|
|
645
|
+
field,
|
|
646
|
+
values: [value]
|
|
647
|
+
};
|
|
648
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
649
|
+
const ops = Object.keys(value);
|
|
650
|
+
if (ops.length !== 1) return void 0;
|
|
651
|
+
const operand = value[ops[0]];
|
|
652
|
+
if (ops[0] === "$eq" && isScalar(operand)) return {
|
|
653
|
+
field,
|
|
654
|
+
values: [operand]
|
|
655
|
+
};
|
|
656
|
+
if (ops[0] === "$in" && Array.isArray(operand) && operand.every(isScalar)) return {
|
|
657
|
+
field,
|
|
658
|
+
values: operand
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function isScalar(value) {
|
|
662
|
+
const t = typeof value;
|
|
663
|
+
return t === "string" || t === "number" || t === "boolean" || t === "bigint";
|
|
508
664
|
}
|
|
509
665
|
//#endregion
|
|
510
666
|
//#region src/from-source.ts
|
|
@@ -1255,13 +1411,19 @@ function buildInsertMany(dialect, table, rows, columns = insertManyColumns(rows)
|
|
|
1255
1411
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
1256
1412
|
* `table` may be a derived source (a pruned view read, since 0.1.153).
|
|
1257
1413
|
* `lock` (since 0.1.162) appends a row-locking clause — see {@link readLockSql}.
|
|
1414
|
+
* `defaultOrder` (since 0.1.165) is the ORDER BY list used when `$sort` orders
|
|
1415
|
+
* nothing — a search's relevance order; its params bind after the WHERE's.
|
|
1258
1416
|
*/
|
|
1259
|
-
function buildSelect(dialect, table, where, controls, lock) {
|
|
1417
|
+
function buildSelect(dialect, table, where, controls, lock, defaultOrder) {
|
|
1260
1418
|
const cols = buildProjection(dialect, controls?.$select);
|
|
1261
1419
|
let sql = `SELECT ${fromSourceHint(table)}${cols} FROM ${fromSourceSql(dialect, table)} WHERE ${where.sql}`;
|
|
1262
1420
|
const params = [...where.params];
|
|
1263
1421
|
const orderBy = orderByList(dialect, controls?.$sort, controls?.$nulls);
|
|
1264
1422
|
if (orderBy) sql += ` ORDER BY ${orderBy}`;
|
|
1423
|
+
else if (defaultOrder?.sql) {
|
|
1424
|
+
sql += ` ORDER BY ${defaultOrder.sql}`;
|
|
1425
|
+
params.push(...defaultOrder.params);
|
|
1426
|
+
}
|
|
1265
1427
|
if (controls?.$limit !== void 0) {
|
|
1266
1428
|
sql += ` LIMIT ?`;
|
|
1267
1429
|
params.push(controls.$limit);
|
|
@@ -1357,12 +1519,14 @@ const SQL_DEFAULT = Symbol("SQL_DEFAULT");
|
|
|
1357
1519
|
* and an omitted PK must never be nulled or re-defaulted. Static value
|
|
1358
1520
|
* defaults are filled SDK-side before the adapter sees the row, so only
|
|
1359
1521
|
* native function defaults are flagged. Ignored and derived descriptors in
|
|
1360
|
-
* `fields` are skipped, so `fieldDescriptors` works too.
|
|
1522
|
+
* `fields` are skipped, so `fieldDescriptors` works too. Insert-only columns
|
|
1523
|
+
* (`number.timestamp.created`, since 0.1.165) are left out: a replace keeps
|
|
1524
|
+
* their stored value.
|
|
1361
1525
|
*/
|
|
1362
1526
|
function replaceColumnsFor(fields, nativeFns) {
|
|
1363
1527
|
const out = [];
|
|
1364
1528
|
for (const fd of fields) {
|
|
1365
|
-
if (fd.ignored || fd.isPrimaryKey || fd.derived) continue;
|
|
1529
|
+
if (fd.ignored || fd.isPrimaryKey || fd.derived || fd.insertOnly) continue;
|
|
1366
1530
|
const def = fd.defaultValue;
|
|
1367
1531
|
out.push({
|
|
1368
1532
|
name: fd.physicalName,
|
|
@@ -1911,6 +2075,7 @@ exports.refActionToSql = refActionToSql;
|
|
|
1911
2075
|
exports.renameGeoDistance = renameGeoDistance;
|
|
1912
2076
|
exports.renderArith = renderArith;
|
|
1913
2077
|
exports.replaceColumnsFor = replaceColumnsFor;
|
|
2078
|
+
exports.searchRankOrder = searchRankOrder;
|
|
1914
2079
|
exports.sqlStringLiteral = sqlStringLiteral;
|
|
1915
2080
|
exports.sqlTimeZoneLiteral = sqlTimeZoneLiteral;
|
|
1916
2081
|
exports.stripPartitionRowNumber = stripPartitionRowNumber;
|
package/dist/index.d.cts
CHANGED
|
@@ -269,6 +269,21 @@ declare const EMPTY_AND: TSqlFragment;
|
|
|
269
269
|
declare const EMPTY_OR: TSqlFragment;
|
|
270
270
|
/** ORs predicate parts (parenthesized when several); no parts matches nothing. */
|
|
271
271
|
declare function orFragment(parts: string[], params: unknown[]): TSqlFragment;
|
|
272
|
+
/**
|
|
273
|
+
* The relevance ORDER BY of a fulltext search with no `$sort` (since 0.1.165):
|
|
274
|
+
* rows an exact-number equality matched first (`CASE WHEN … THEN 0 ELSE 1 END`,
|
|
275
|
+
* only next to a text rank — `n` binds each equality once), then `textRank`
|
|
276
|
+
* (its direction included, e.g. `ts_rank(…) DESC`), then the primary-key
|
|
277
|
+
* columns ascending (quoted, after `prefix`). Params: the CASE's, then the
|
|
278
|
+
* text rank's.
|
|
279
|
+
*/
|
|
280
|
+
declare function searchRankOrder(dialect: Pick<SqlDialect, "quoteIdentifier">, opts: {
|
|
281
|
+
/** One `?`-equality per integer member (`"ref_no" = ?`). */exactEqualities: readonly string[]; /** The whole-number term, when the search term is one. */
|
|
282
|
+
n: number | undefined;
|
|
283
|
+
textRank?: TSqlFragment;
|
|
284
|
+
pkColumns: readonly string[]; /** Prepended to each quoted primary-key column (`t.`). */
|
|
285
|
+
prefix?: string;
|
|
286
|
+
}): TSqlFragment;
|
|
272
287
|
//#endregion
|
|
273
288
|
//#region src/arith.d.ts
|
|
274
289
|
/** Why {@link renderArith} cannot render an expression. */
|
|
@@ -566,8 +581,10 @@ declare function buildInsertMany(dialect: SqlDialect, table: string, rows: reado
|
|
|
566
581
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
567
582
|
* `table` may be a derived source (a pruned view read, since 0.1.153).
|
|
568
583
|
* `lock` (since 0.1.162) appends a row-locking clause — see {@link readLockSql}.
|
|
584
|
+
* `defaultOrder` (since 0.1.165) is the ORDER BY list used when `$sort` orders
|
|
585
|
+
* nothing — a search's relevance order; its params bind after the WHERE's.
|
|
569
586
|
*/
|
|
570
|
-
declare function buildSelect(dialect: SqlDialect, table: TSqlFromSource, where: TSqlFragment, controls?: DbControls, lock?: TDbReadLock): TSqlFragment;
|
|
587
|
+
declare function buildSelect(dialect: SqlDialect, table: TSqlFromSource, where: TSqlFragment, controls?: DbControls, lock?: TDbReadLock, defaultOrder?: TSqlFragment): TSqlFragment;
|
|
571
588
|
/**
|
|
572
589
|
* The row-locking clause of a locking read — `" FOR UPDATE"`,
|
|
573
590
|
* `" FOR SHARE NOWAIT"`, `" FOR UPDATE SKIP LOCKED"`, … (PostgreSQL and
|
|
@@ -627,7 +644,9 @@ interface TReplaceColumn {
|
|
|
627
644
|
* and an omitted PK must never be nulled or re-defaulted. Static value
|
|
628
645
|
* defaults are filled SDK-side before the adapter sees the row, so only
|
|
629
646
|
* native function defaults are flagged. Ignored and derived descriptors in
|
|
630
|
-
* `fields` are skipped, so `fieldDescriptors` works too.
|
|
647
|
+
* `fields` are skipped, so `fieldDescriptors` works too. Insert-only columns
|
|
648
|
+
* (`number.timestamp.created`, since 0.1.165) are left out: a replace keeps
|
|
649
|
+
* their stored value.
|
|
631
650
|
*/
|
|
632
651
|
declare function replaceColumnsFor(fields: readonly TDbFieldMeta[], nativeFns: ReadonlySet<TDbDefaultFn>): TReplaceColumn[];
|
|
633
652
|
/**
|
|
@@ -882,4 +901,4 @@ declare function buildJsonColumnCopy(dialect: SqlDialect, table: string, source:
|
|
|
882
901
|
*/
|
|
883
902
|
declare function buildJsonifyText(dialect: SqlDialect, table: string, column: string): string;
|
|
884
903
|
//#endregion
|
|
885
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, InsertSqlCache, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TArithFailure, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlDerivedSource, type TSqlFragment, type TSqlFromSource, type TSqlWriteLimit, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildViewSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, fromSourceHint, fromSourceSql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, nullsOrderSql, nullsPlacementOf, numericOutOfRangeError, orFragment, orderByList, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, readLockSql, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, unquoteSqlStringLiteral, vectorDistanceSource, viewReadSource, writeLimitCount, writeLimitWhere };
|
|
904
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, InsertSqlCache, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TArithFailure, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlDerivedSource, type TSqlFragment, type TSqlFromSource, type TSqlWriteLimit, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildViewSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, fromSourceHint, fromSourceSql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, nullsOrderSql, nullsPlacementOf, numericOutOfRangeError, orFragment, orderByList, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, readLockSql, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, searchRankOrder, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, unquoteSqlStringLiteral, vectorDistanceSource, viewReadSource, writeLimitCount, writeLimitWhere };
|
package/dist/index.d.mts
CHANGED
|
@@ -269,6 +269,21 @@ declare const EMPTY_AND: TSqlFragment;
|
|
|
269
269
|
declare const EMPTY_OR: TSqlFragment;
|
|
270
270
|
/** ORs predicate parts (parenthesized when several); no parts matches nothing. */
|
|
271
271
|
declare function orFragment(parts: string[], params: unknown[]): TSqlFragment;
|
|
272
|
+
/**
|
|
273
|
+
* The relevance ORDER BY of a fulltext search with no `$sort` (since 0.1.165):
|
|
274
|
+
* rows an exact-number equality matched first (`CASE WHEN … THEN 0 ELSE 1 END`,
|
|
275
|
+
* only next to a text rank — `n` binds each equality once), then `textRank`
|
|
276
|
+
* (its direction included, e.g. `ts_rank(…) DESC`), then the primary-key
|
|
277
|
+
* columns ascending (quoted, after `prefix`). Params: the CASE's, then the
|
|
278
|
+
* text rank's.
|
|
279
|
+
*/
|
|
280
|
+
declare function searchRankOrder(dialect: Pick<SqlDialect, "quoteIdentifier">, opts: {
|
|
281
|
+
/** One `?`-equality per integer member (`"ref_no" = ?`). */exactEqualities: readonly string[]; /** The whole-number term, when the search term is one. */
|
|
282
|
+
n: number | undefined;
|
|
283
|
+
textRank?: TSqlFragment;
|
|
284
|
+
pkColumns: readonly string[]; /** Prepended to each quoted primary-key column (`t.`). */
|
|
285
|
+
prefix?: string;
|
|
286
|
+
}): TSqlFragment;
|
|
272
287
|
//#endregion
|
|
273
288
|
//#region src/arith.d.ts
|
|
274
289
|
/** Why {@link renderArith} cannot render an expression. */
|
|
@@ -566,8 +581,10 @@ declare function buildInsertMany(dialect: SqlDialect, table: string, rows: reado
|
|
|
566
581
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
567
582
|
* `table` may be a derived source (a pruned view read, since 0.1.153).
|
|
568
583
|
* `lock` (since 0.1.162) appends a row-locking clause — see {@link readLockSql}.
|
|
584
|
+
* `defaultOrder` (since 0.1.165) is the ORDER BY list used when `$sort` orders
|
|
585
|
+
* nothing — a search's relevance order; its params bind after the WHERE's.
|
|
569
586
|
*/
|
|
570
|
-
declare function buildSelect(dialect: SqlDialect, table: TSqlFromSource, where: TSqlFragment, controls?: DbControls, lock?: TDbReadLock): TSqlFragment;
|
|
587
|
+
declare function buildSelect(dialect: SqlDialect, table: TSqlFromSource, where: TSqlFragment, controls?: DbControls, lock?: TDbReadLock, defaultOrder?: TSqlFragment): TSqlFragment;
|
|
571
588
|
/**
|
|
572
589
|
* The row-locking clause of a locking read — `" FOR UPDATE"`,
|
|
573
590
|
* `" FOR SHARE NOWAIT"`, `" FOR UPDATE SKIP LOCKED"`, … (PostgreSQL and
|
|
@@ -627,7 +644,9 @@ interface TReplaceColumn {
|
|
|
627
644
|
* and an omitted PK must never be nulled or re-defaulted. Static value
|
|
628
645
|
* defaults are filled SDK-side before the adapter sees the row, so only
|
|
629
646
|
* native function defaults are flagged. Ignored and derived descriptors in
|
|
630
|
-
* `fields` are skipped, so `fieldDescriptors` works too.
|
|
647
|
+
* `fields` are skipped, so `fieldDescriptors` works too. Insert-only columns
|
|
648
|
+
* (`number.timestamp.created`, since 0.1.165) are left out: a replace keeps
|
|
649
|
+
* their stored value.
|
|
631
650
|
*/
|
|
632
651
|
declare function replaceColumnsFor(fields: readonly TDbFieldMeta[], nativeFns: ReadonlySet<TDbDefaultFn>): TReplaceColumn[];
|
|
633
652
|
/**
|
|
@@ -882,4 +901,4 @@ declare function buildJsonColumnCopy(dialect: SqlDialect, table: string, source:
|
|
|
882
901
|
*/
|
|
883
902
|
declare function buildJsonifyText(dialect: SqlDialect, table: string, column: string): string;
|
|
884
903
|
//#endregion
|
|
885
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, InsertSqlCache, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TArithFailure, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlDerivedSource, type TSqlFragment, type TSqlFromSource, type TSqlWriteLimit, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildViewSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, fromSourceHint, fromSourceSql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, nullsOrderSql, nullsPlacementOf, numericOutOfRangeError, orFragment, orderByList, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, readLockSql, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, unquoteSqlStringLiteral, vectorDistanceSource, viewReadSource, writeLimitCount, writeLimitWhere };
|
|
904
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, InsertSqlCache, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TArithFailure, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlDerivedSource, type TSqlFragment, type TSqlFromSource, type TSqlWriteLimit, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildViewSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, fromSourceHint, fromSourceSql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, nullsOrderSql, nullsPlacementOf, numericOutOfRangeError, orFragment, orderByList, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, readLockSql, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, searchRankOrder, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, unquoteSqlStringLiteral, vectorDistanceSource, viewReadSource, writeLimitCount, writeLimitWhere };
|
package/dist/index.mjs
CHANGED
|
@@ -244,6 +244,32 @@ function orFragment(parts, params) {
|
|
|
244
244
|
params
|
|
245
245
|
};
|
|
246
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* The relevance ORDER BY of a fulltext search with no `$sort` (since 0.1.165):
|
|
249
|
+
* rows an exact-number equality matched first (`CASE WHEN … THEN 0 ELSE 1 END`,
|
|
250
|
+
* only next to a text rank — `n` binds each equality once), then `textRank`
|
|
251
|
+
* (its direction included, e.g. `ts_rank(…) DESC`), then the primary-key
|
|
252
|
+
* columns ascending (quoted, after `prefix`). Params: the CASE's, then the
|
|
253
|
+
* text rank's.
|
|
254
|
+
*/
|
|
255
|
+
function searchRankOrder(dialect, opts) {
|
|
256
|
+
const parts = [];
|
|
257
|
+
const params = [];
|
|
258
|
+
const { exactEqualities: eqs, n, textRank } = opts;
|
|
259
|
+
if (textRank && n !== void 0 && eqs.length > 0) {
|
|
260
|
+
parts.push(`CASE WHEN ${eqs.join(" OR ")} THEN 0 ELSE 1 END`);
|
|
261
|
+
for (let i = 0; i < eqs.length; i++) params.push(n);
|
|
262
|
+
}
|
|
263
|
+
if (textRank) {
|
|
264
|
+
parts.push(textRank.sql);
|
|
265
|
+
params.push(...textRank.params);
|
|
266
|
+
}
|
|
267
|
+
for (const col of opts.pkColumns) parts.push(`${opts.prefix ?? ""}${dialect.quoteIdentifier(col)} ASC`);
|
|
268
|
+
return {
|
|
269
|
+
sql: parts.join(", "),
|
|
270
|
+
params
|
|
271
|
+
};
|
|
272
|
+
}
|
|
247
273
|
//#endregion
|
|
248
274
|
//#region src/arith.ts
|
|
249
275
|
/**
|
|
@@ -500,10 +526,140 @@ function getVisitor(dialect) {
|
|
|
500
526
|
*/
|
|
501
527
|
function buildWhere(dialect, filter, opts) {
|
|
502
528
|
if (!filter || Object.keys(filter).length === 0) return EMPTY_AND;
|
|
503
|
-
|
|
529
|
+
const visitor = opts || containsRelationFilter(filter) ? createFilterVisitor(dialect, {
|
|
504
530
|
...opts,
|
|
505
531
|
aliasSeq: opts?.aliasSeq ?? { n: 0 }
|
|
506
|
-
}) : getVisitor(dialect)
|
|
532
|
+
}) : getVisitor(dialect);
|
|
533
|
+
return walkFilter(collapseOrEqualities(filter), visitor) ?? EMPTY_AND;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Rewrites every `$or` whose branches include several equalities on ONE
|
|
537
|
+
* field (`{ f: v }`, `{ f: { $eq: v } }`, `{ f: { $in: [...] } }`, v a
|
|
538
|
+
* non-null scalar, all of one JS type) into one `{ f: { $in: [...] } }`
|
|
539
|
+
* branch (since 0.1.165).
|
|
540
|
+
* Same rows, but one predicate instead of an OR chain whose depth grows with
|
|
541
|
+
* the batch: SQLite refuses an expression tree deeper than 1000, which a
|
|
542
|
+
* per-row key filter over a large batch reached. Composite, null, operator
|
|
543
|
+
* and lone branches are kept as they are. Returns `filter` itself when
|
|
544
|
+
* nothing changes.
|
|
545
|
+
*/
|
|
546
|
+
function collapseOrEqualities(filter) {
|
|
547
|
+
if (!filter || typeof filter !== "object") return filter;
|
|
548
|
+
const node = filter;
|
|
549
|
+
let out;
|
|
550
|
+
const and = node.$and;
|
|
551
|
+
if (Array.isArray(and)) {
|
|
552
|
+
const next = collapseEach(and);
|
|
553
|
+
if (next !== and) (out ??= { ...node }).$and = next;
|
|
554
|
+
}
|
|
555
|
+
const or = node.$or;
|
|
556
|
+
if (Array.isArray(or)) {
|
|
557
|
+
let next = collapseEach(or);
|
|
558
|
+
if (next.length > 1 && mayRepeatField(next)) next = mergeEqualityBranches(next);
|
|
559
|
+
if (next !== or) (out ??= { ...node }).$or = next;
|
|
560
|
+
}
|
|
561
|
+
const not = node.$not;
|
|
562
|
+
if (not && typeof not === "object") {
|
|
563
|
+
const next = collapseOrEqualities(not);
|
|
564
|
+
if (next !== not) (out ??= { ...node }).$not = next;
|
|
565
|
+
}
|
|
566
|
+
return out ?? filter;
|
|
567
|
+
}
|
|
568
|
+
/** `list` with each object child collapsed — the same array when none changed (copied on the first change). */
|
|
569
|
+
function collapseEach(list) {
|
|
570
|
+
let copy;
|
|
571
|
+
for (let i = 0; i < list.length; i++) {
|
|
572
|
+
const child = list[i];
|
|
573
|
+
const next = child && typeof child === "object" ? collapseOrEqualities(child) : child;
|
|
574
|
+
if (next !== child) (copy ??= list.slice())[i] = next;
|
|
575
|
+
}
|
|
576
|
+
return copy ?? list;
|
|
577
|
+
}
|
|
578
|
+
/** The sole key of a one-key, non-operator branch — allocation-free. */
|
|
579
|
+
function soleField(branch) {
|
|
580
|
+
if (!branch || typeof branch !== "object" || Array.isArray(branch)) return void 0;
|
|
581
|
+
let field;
|
|
582
|
+
for (const key in branch) {
|
|
583
|
+
if (field !== void 0) return void 0;
|
|
584
|
+
field = key;
|
|
585
|
+
}
|
|
586
|
+
return field === void 0 || field.startsWith("$") ? void 0 : field;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Cheap pre-check for {@link mergeEqualityBranches}: whether two branches
|
|
590
|
+
* share a sole field (long lists go straight to the merge pass).
|
|
591
|
+
*/
|
|
592
|
+
function mayRepeatField(branches) {
|
|
593
|
+
if (branches.length > 64) return true;
|
|
594
|
+
for (let i = 1; i < branches.length; i++) {
|
|
595
|
+
const field = soleField(branches[i]);
|
|
596
|
+
if (field === void 0) continue;
|
|
597
|
+
for (let j = 0; j < i; j++) if (soleField(branches[j]) === field) return true;
|
|
598
|
+
}
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
/** The `$or` branches with the equalities of each repeated field merged into one `$in`. */
|
|
602
|
+
function mergeEqualityBranches(branches) {
|
|
603
|
+
const eqs = branches.map(equalityOf);
|
|
604
|
+
const fields = /* @__PURE__ */ new Map();
|
|
605
|
+
for (const eq of eqs) {
|
|
606
|
+
if (!eq) continue;
|
|
607
|
+
let entry = fields.get(eq.field);
|
|
608
|
+
if (!entry) fields.set(eq.field, entry = { count: 0 });
|
|
609
|
+
entry.count++;
|
|
610
|
+
for (const v of eq.values) {
|
|
611
|
+
const t = typeof v;
|
|
612
|
+
entry.type = entry.type === void 0 || entry.type === t ? t : "mixed";
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const merges = (field) => {
|
|
616
|
+
const entry = fields.get(field);
|
|
617
|
+
return entry.count > 1 && entry.type !== "mixed";
|
|
618
|
+
};
|
|
619
|
+
if (!eqs.some((eq) => eq && merges(eq.field))) return branches;
|
|
620
|
+
const out = [];
|
|
621
|
+
for (let i = 0; i < branches.length; i++) {
|
|
622
|
+
const eq = eqs[i];
|
|
623
|
+
if (!eq || !merges(eq.field)) {
|
|
624
|
+
out.push(branches[i]);
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const entry = fields.get(eq.field);
|
|
628
|
+
if (!entry.values) {
|
|
629
|
+
entry.values = [];
|
|
630
|
+
out.push({ [eq.field]: { $in: entry.values } });
|
|
631
|
+
}
|
|
632
|
+
entry.values.push(...eq.values);
|
|
633
|
+
}
|
|
634
|
+
return out;
|
|
635
|
+
}
|
|
636
|
+
/** `{ field, values }` of a one-field equality branch, else `undefined`. */
|
|
637
|
+
function equalityOf(branch) {
|
|
638
|
+
if (!branch || typeof branch !== "object" || Array.isArray(branch)) return void 0;
|
|
639
|
+
const keys = Object.keys(branch);
|
|
640
|
+
if (keys.length !== 1 || keys[0].startsWith("$")) return void 0;
|
|
641
|
+
const field = keys[0];
|
|
642
|
+
const value = branch[field];
|
|
643
|
+
if (isScalar(value)) return {
|
|
644
|
+
field,
|
|
645
|
+
values: [value]
|
|
646
|
+
};
|
|
647
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
648
|
+
const ops = Object.keys(value);
|
|
649
|
+
if (ops.length !== 1) return void 0;
|
|
650
|
+
const operand = value[ops[0]];
|
|
651
|
+
if (ops[0] === "$eq" && isScalar(operand)) return {
|
|
652
|
+
field,
|
|
653
|
+
values: [operand]
|
|
654
|
+
};
|
|
655
|
+
if (ops[0] === "$in" && Array.isArray(operand) && operand.every(isScalar)) return {
|
|
656
|
+
field,
|
|
657
|
+
values: operand
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function isScalar(value) {
|
|
661
|
+
const t = typeof value;
|
|
662
|
+
return t === "string" || t === "number" || t === "boolean" || t === "bigint";
|
|
507
663
|
}
|
|
508
664
|
//#endregion
|
|
509
665
|
//#region src/from-source.ts
|
|
@@ -1254,13 +1410,19 @@ function buildInsertMany(dialect, table, rows, columns = insertManyColumns(rows)
|
|
|
1254
1410
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
1255
1411
|
* `table` may be a derived source (a pruned view read, since 0.1.153).
|
|
1256
1412
|
* `lock` (since 0.1.162) appends a row-locking clause — see {@link readLockSql}.
|
|
1413
|
+
* `defaultOrder` (since 0.1.165) is the ORDER BY list used when `$sort` orders
|
|
1414
|
+
* nothing — a search's relevance order; its params bind after the WHERE's.
|
|
1257
1415
|
*/
|
|
1258
|
-
function buildSelect(dialect, table, where, controls, lock) {
|
|
1416
|
+
function buildSelect(dialect, table, where, controls, lock, defaultOrder) {
|
|
1259
1417
|
const cols = buildProjection(dialect, controls?.$select);
|
|
1260
1418
|
let sql = `SELECT ${fromSourceHint(table)}${cols} FROM ${fromSourceSql(dialect, table)} WHERE ${where.sql}`;
|
|
1261
1419
|
const params = [...where.params];
|
|
1262
1420
|
const orderBy = orderByList(dialect, controls?.$sort, controls?.$nulls);
|
|
1263
1421
|
if (orderBy) sql += ` ORDER BY ${orderBy}`;
|
|
1422
|
+
else if (defaultOrder?.sql) {
|
|
1423
|
+
sql += ` ORDER BY ${defaultOrder.sql}`;
|
|
1424
|
+
params.push(...defaultOrder.params);
|
|
1425
|
+
}
|
|
1264
1426
|
if (controls?.$limit !== void 0) {
|
|
1265
1427
|
sql += ` LIMIT ?`;
|
|
1266
1428
|
params.push(controls.$limit);
|
|
@@ -1356,12 +1518,14 @@ const SQL_DEFAULT = Symbol("SQL_DEFAULT");
|
|
|
1356
1518
|
* and an omitted PK must never be nulled or re-defaulted. Static value
|
|
1357
1519
|
* defaults are filled SDK-side before the adapter sees the row, so only
|
|
1358
1520
|
* native function defaults are flagged. Ignored and derived descriptors in
|
|
1359
|
-
* `fields` are skipped, so `fieldDescriptors` works too.
|
|
1521
|
+
* `fields` are skipped, so `fieldDescriptors` works too. Insert-only columns
|
|
1522
|
+
* (`number.timestamp.created`, since 0.1.165) are left out: a replace keeps
|
|
1523
|
+
* their stored value.
|
|
1360
1524
|
*/
|
|
1361
1525
|
function replaceColumnsFor(fields, nativeFns) {
|
|
1362
1526
|
const out = [];
|
|
1363
1527
|
for (const fd of fields) {
|
|
1364
|
-
if (fd.ignored || fd.isPrimaryKey || fd.derived) continue;
|
|
1528
|
+
if (fd.ignored || fd.isPrimaryKey || fd.derived || fd.insertOnly) continue;
|
|
1365
1529
|
const def = fd.defaultValue;
|
|
1366
1530
|
out.push({
|
|
1367
1531
|
name: fd.physicalName,
|
|
@@ -1850,4 +2014,4 @@ function buildJsonifyText(dialect, table, column) {
|
|
|
1850
2014
|
return `UPDATE ${dialect.quoteTable(table)} SET ${col} = ${dialect.jsonFromText(col)} WHERE ${col} IS NOT NULL`;
|
|
1851
2015
|
}
|
|
1852
2016
|
//#endregion
|
|
1853
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, InsertSqlCache, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildViewSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, fromSourceHint, fromSourceSql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, nullsOrderSql, nullsPlacementOf, numericOutOfRangeError, orFragment, orderByList, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, readLockSql, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, unquoteSqlStringLiteral, vectorDistanceSource, viewReadSource, writeLimitCount, writeLimitWhere };
|
|
2017
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, InsertSqlCache, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildViewSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, fromSourceHint, fromSourceSql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, nullsOrderSql, nullsPlacementOf, numericOutOfRangeError, orFragment, orderByList, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, readLockSql, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, searchRankOrder, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, unquoteSqlStringLiteral, vectorDistanceSource, viewReadSource, writeLimitCount, writeLimitWhere };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-sql-tools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.165",
|
|
4
4
|
"description": "Shared SQL builder utilities for @atscript database adapters.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@uniqu/core": "^0.1.15",
|
|
41
|
-
"unplugin-atscript": "^0.1
|
|
41
|
+
"unplugin-atscript": "^0.2.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@uniqu/core": "^0.1.15",
|
|
45
|
-
"@atscript/db": "^0.1.
|
|
45
|
+
"@atscript/db": "^0.1.165"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "vp pack",
|