@atscript/db-memory 0.1.152 → 0.1.153
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 +64 -21
- package/dist/index.d.cts +19 -7
- package/dist/index.d.mts +19 -7
- package/dist/index.mjs +64 -21
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -428,8 +428,8 @@ function buildMemoryPredicate(filter, relationSets) {
|
|
|
428
428
|
* there is exactly ONE implementation. Everything here is a pure function over
|
|
429
429
|
* plain `Record<string, unknown>` rows — no adapter/table state — so other
|
|
430
430
|
* consumers (e.g. moost-db's value-help controller) can reuse the SAME engine
|
|
431
|
-
* instead of hand-rolling a second copy. The adapter
|
|
432
|
-
*
|
|
431
|
+
* instead of hand-rolling a second copy. The adapter appends its own
|
|
432
|
+
* physical primary-key fields to `$sort` before calling in.
|
|
433
433
|
*
|
|
434
434
|
* The dot-path READ / DELETE helpers (`getPath` / `deletePath`) are the
|
|
435
435
|
* core's (`@atscript/db`); the dot-path WRITE helper ({@link setPath}) lives
|
|
@@ -455,6 +455,24 @@ function compareLeaves(a, b) {
|
|
|
455
455
|
return as < bs ? -1 : as > bs ? 1 : 0;
|
|
456
456
|
}
|
|
457
457
|
/**
|
|
458
|
+
* {@link compareLeaves} with a NULL placement: `'first'` / `'last'` put
|
|
459
|
+
* `null`/`undefined` before / after every value, and `desc` reverses only the
|
|
460
|
+
* comparison of two values (since 0.1.153). Without `nulls`, nil is the
|
|
461
|
+
* smallest value and `desc` reverses everything, as before.
|
|
462
|
+
*/
|
|
463
|
+
function compareSortKey(a, b, desc, nulls) {
|
|
464
|
+
if (nulls) {
|
|
465
|
+
const aNil = a === null || a === void 0;
|
|
466
|
+
const bNil = b === null || b === void 0;
|
|
467
|
+
if (aNil || bNil) {
|
|
468
|
+
if (aNil && bNil) return 0;
|
|
469
|
+
return (aNil ? -1 : 1) * (nulls === "first" ? 1 : -1);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const cmp = compareLeaves(a, b);
|
|
473
|
+
return desc ? -cmp : cmp;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
458
476
|
* Dot-path setter used by inclusion projection. Creates intermediate plain
|
|
459
477
|
* objects as needed; overwrites a non-object intermediate. Top-level keys and
|
|
460
478
|
* nested dot-paths both work.
|
|
@@ -479,24 +497,29 @@ function setSegments(target, segments, value) {
|
|
|
479
497
|
* - No `$sort` (or an empty one) → returns the input array UNCHANGED (same
|
|
480
498
|
* reference, insertion order preserved) — the fast path for an unsorted read.
|
|
481
499
|
* - `tieBreak`, when supplied, is the FINAL deterministic tie-break key for a
|
|
482
|
-
* TOTAL order
|
|
483
|
-
*
|
|
500
|
+
* TOTAL order, so rows with equal sort keys still order deterministically
|
|
501
|
+
* (the adapter instead appends its primary-key fields to `$sort`, compared
|
|
502
|
+
* by value like every other key — since 0.1.153). When ABSENT the
|
|
484
503
|
* sort falls back to preserving input order among equal keys (via each row's
|
|
485
504
|
* original index), so a consumer with no primary key keeps insertion order.
|
|
486
505
|
* - `topK` (since 0.1.151), when supplied, asks for only the first `topK` rows
|
|
487
506
|
* of that order (e.g. `$skip + $limit`): a small one is selected in one pass
|
|
488
507
|
* instead of sorting everything. The rows returned are exactly the head of
|
|
489
508
|
* the full sort — same comparator, same stable tie handling.
|
|
509
|
+
* - `nulls` (since 0.1.153) — the `$nulls` control: a `$sort` key with an
|
|
510
|
+
* entry puts `null`/missing values `'first'` or `'last'` in either
|
|
511
|
+
* direction; a key without one keeps them smallest.
|
|
490
512
|
*
|
|
491
513
|
* NEVER mutates the input array. Each row's sort keys are read ONCE (O(n)) and
|
|
492
514
|
* its `tieBreak` only when two rows tie on every key — not inside the
|
|
493
515
|
* O(n log n) comparator.
|
|
494
516
|
*/
|
|
495
|
-
function sortRows(rows, $sort, tieBreak, topK) {
|
|
517
|
+
function sortRows(rows, $sort, tieBreak, topK, nulls) {
|
|
496
518
|
const keys = $sort ? Object.entries($sort) : [];
|
|
497
519
|
if (keys.length === 0) return rows;
|
|
498
520
|
const readers = keys.map(([field]) => pathReader(field));
|
|
499
521
|
const desc = keys.map(([, dir]) => dir === -1);
|
|
522
|
+
const placement = nulls ? keys.map(([field]) => Object.hasOwn(nulls, field) ? nulls[field] : void 0) : [];
|
|
500
523
|
const decorated = rows.map((row, index) => ({
|
|
501
524
|
row,
|
|
502
525
|
index,
|
|
@@ -508,8 +531,8 @@ function sortRows(rows, $sort, tieBreak, topK) {
|
|
|
508
531
|
}));
|
|
509
532
|
const compare = (a, b) => {
|
|
510
533
|
for (let i = 0; i < desc.length; i++) {
|
|
511
|
-
const cmp =
|
|
512
|
-
if (cmp !== 0) return
|
|
534
|
+
const cmp = compareSortKey(a.keys[i], b.keys[i], desc[i], placement[i]);
|
|
535
|
+
if (cmp !== 0) return cmp;
|
|
513
536
|
}
|
|
514
537
|
if (tieBreak) {
|
|
515
538
|
const at = a.tie ??= tieBreak(a.row);
|
|
@@ -790,7 +813,7 @@ function aggregateRows(rows, controls) {
|
|
|
790
813
|
const having = controls.$having;
|
|
791
814
|
if (having) out = out.filter(buildMemoryPredicate(having));
|
|
792
815
|
if (controls.$count) return [{ count: out.length }];
|
|
793
|
-
const paged = paginate(sortRows(out, controls.$sort), controls.$skip, controls.$limit);
|
|
816
|
+
const paged = paginate(sortRows(out, controls.$sort, void 0, void 0, controls.$nulls), controls.$skip, controls.$limit);
|
|
794
817
|
const fields = ($select === void 0 ? groupBy : $select.asArray ?? []).map((field) => [field, pathReader(field)]);
|
|
795
818
|
const outputAliases = $select === void 0 ? [] : [...($select.buckets ?? []).map((b) => b.alias), ...$select.computedAliases];
|
|
796
819
|
return paged.map((row) => {
|
|
@@ -866,18 +889,19 @@ function exprAccumulatorFactory(e) {
|
|
|
866
889
|
/**
|
|
867
890
|
* `first` / `last`: the value of `fl.column` on the group's representative
|
|
868
891
|
* row — the least (`first`) or greatest (`last`) row by `rowOrder` (NULL
|
|
869
|
-
* smallest; the primary key is the final key, so the pick is deterministic).
|
|
892
|
+
* smallest unless the key carries a `nulls` placement; the primary key is the final key, so the pick is deterministic).
|
|
870
893
|
*/
|
|
871
894
|
function firstLastFactory(fl, rowOrder) {
|
|
872
895
|
const keys = rowOrder.map((k) => ({
|
|
873
896
|
read: pathReader(k.column),
|
|
874
|
-
|
|
897
|
+
desc: k.desc,
|
|
898
|
+
nulls: k.nulls
|
|
875
899
|
}));
|
|
876
900
|
const read = pathReader(fl.column);
|
|
877
901
|
const sign = fl.fn === "first" ? -1 : 1;
|
|
878
902
|
const compare = (a, b) => {
|
|
879
903
|
for (const k of keys) {
|
|
880
|
-
const c =
|
|
904
|
+
const c = compareSortKey(k.read(a), k.read(b), k.desc, k.nulls);
|
|
881
905
|
if (c !== 0) return c;
|
|
882
906
|
}
|
|
883
907
|
return 0;
|
|
@@ -1581,14 +1605,28 @@ var MemoryAdapter = class MemoryAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1581
1605
|
return { deletedCount: 1 };
|
|
1582
1606
|
}
|
|
1583
1607
|
/**
|
|
1584
|
-
* Stable multi-key comparator from `$sort`, with
|
|
1585
|
-
*
|
|
1608
|
+
* Stable multi-key comparator from `$sort`, with the primary-key fields it
|
|
1609
|
+
* leaves out appended in the direction of its LAST key and compared by
|
|
1610
|
+
* value (numbers numerically, `Date`s by instant) — the same tie-breaker
|
|
1611
|
+
* the core appends before a read reaches any adapter (since 0.1.153), so a
|
|
1612
|
+
* direct adapter call orders ties the same way. Returns the input unchanged
|
|
1586
1613
|
* (insertion order) when there is no `$sort`. Delegates to the shared pure
|
|
1587
|
-
* {@link sortRows}
|
|
1588
|
-
*
|
|
1614
|
+
* {@link sortRows}; `topK` asks for the head of that order only
|
|
1615
|
+
* (`$skip + $limit`).
|
|
1589
1616
|
*/
|
|
1590
|
-
_sortRows(rows, $sort, topK) {
|
|
1591
|
-
return sortRows(rows, $sort,
|
|
1617
|
+
_sortRows(rows, $sort, $nulls, topK) {
|
|
1618
|
+
return sortRows(rows, this._withPkTieBreak($sort), void 0, topK, $nulls);
|
|
1619
|
+
}
|
|
1620
|
+
/** `$sort` plus the primary-key fields it leaves out, in its last key's direction. */
|
|
1621
|
+
_withPkTieBreak($sort) {
|
|
1622
|
+
if (!$sort || !hasSort($sort)) return $sort;
|
|
1623
|
+
const keys = Object.keys($sort);
|
|
1624
|
+
const missing = this._physicalPkFields().filter((field) => !(field in $sort));
|
|
1625
|
+
if (missing.length === 0) return $sort;
|
|
1626
|
+
const dir = $sort[keys[keys.length - 1]];
|
|
1627
|
+
const next = { ...$sort };
|
|
1628
|
+
for (const field of missing) next[field] = dir;
|
|
1629
|
+
return next;
|
|
1592
1630
|
}
|
|
1593
1631
|
/**
|
|
1594
1632
|
* Compiles the `$select` projection of one read: each returned row is a
|
|
@@ -1616,6 +1654,7 @@ var MemoryAdapter = class MemoryAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1616
1654
|
_readControls(controls) {
|
|
1617
1655
|
return {
|
|
1618
1656
|
$sort: controls.$sort,
|
|
1657
|
+
$nulls: controls.$nulls,
|
|
1619
1658
|
$skip: controls.$skip,
|
|
1620
1659
|
$limit: controls.$limit,
|
|
1621
1660
|
$select: controls.$select
|
|
@@ -1652,9 +1691,9 @@ var MemoryAdapter = class MemoryAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1652
1691
|
return (await own).filter(buildMemoryPredicate(query.filter, sets));
|
|
1653
1692
|
}
|
|
1654
1693
|
async findOne(query) {
|
|
1655
|
-
const { $sort, $skip, $select } = this._readControls(query.controls ?? {});
|
|
1694
|
+
const { $sort, $nulls, $skip, $select } = this._readControls(query.controls ?? {});
|
|
1656
1695
|
const index = $skip ?? 0;
|
|
1657
|
-
const row = (hasSort($sort) ? this._sortRows(await this._filteredRows(query), $sort, index + 1) : await this._filteredRows(query, index + 1))[index];
|
|
1696
|
+
const row = (hasSort($sort) ? this._sortRows(await this._filteredRows(query), $sort, $nulls, index + 1) : await this._filteredRows(query, index + 1))[index];
|
|
1658
1697
|
return row ? this._projector($select)(row) : null;
|
|
1659
1698
|
}
|
|
1660
1699
|
async findMany(query) {
|
|
@@ -1672,10 +1711,10 @@ var MemoryAdapter = class MemoryAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1672
1711
|
*/
|
|
1673
1712
|
async findManyWithCount(query) {
|
|
1674
1713
|
const filtered = await this._filteredRows(query);
|
|
1675
|
-
const { $sort, $skip, $limit, $select } = this._readControls(query.controls ?? {});
|
|
1714
|
+
const { $sort, $nulls, $skip, $limit, $select } = this._readControls(query.controls ?? {});
|
|
1676
1715
|
const topK = $limit === void 0 ? void 0 : ($skip ?? 0) + $limit;
|
|
1677
1716
|
return {
|
|
1678
|
-
data: paginate(this._sortRows(filtered, $sort, topK), $skip, $limit).map(this._projector($select)),
|
|
1717
|
+
data: paginate(this._sortRows(filtered, $sort, $nulls, topK), $skip, $limit).map(this._projector($select)),
|
|
1679
1718
|
count: filtered.length
|
|
1680
1719
|
};
|
|
1681
1720
|
}
|
|
@@ -1709,6 +1748,10 @@ var MemoryAdapter = class MemoryAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1709
1748
|
supportsAggregateExpressions() {
|
|
1710
1749
|
return true;
|
|
1711
1750
|
}
|
|
1751
|
+
/** NULL placement (`$nulls`, `@db.sort.nulls`) — `sortRows` honours it per key. */
|
|
1752
|
+
supportsNullsPlacement() {
|
|
1753
|
+
return true;
|
|
1754
|
+
}
|
|
1712
1755
|
/** Managed views render nothing (non-goal) — both are accepted like `aggregateFns()`. */
|
|
1713
1756
|
viewCapabilities() {
|
|
1714
1757
|
return _atscript_db.ALL_VIEW_CAPABILITIES;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, ResolvedRelationFilter, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TFieldOps, TViewCapability } from "@atscript/db";
|
|
2
2
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
3
|
+
import { NullsPlacement } from "@uniqu/core";
|
|
3
4
|
|
|
4
5
|
//#region src/lib/memory-adapter.d.ts
|
|
5
6
|
/**
|
|
@@ -368,13 +369,18 @@ declare class MemoryAdapter extends BaseDbAdapter {
|
|
|
368
369
|
updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
|
|
369
370
|
deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
|
|
370
371
|
/**
|
|
371
|
-
* Stable multi-key comparator from `$sort`, with
|
|
372
|
-
*
|
|
372
|
+
* Stable multi-key comparator from `$sort`, with the primary-key fields it
|
|
373
|
+
* leaves out appended in the direction of its LAST key and compared by
|
|
374
|
+
* value (numbers numerically, `Date`s by instant) — the same tie-breaker
|
|
375
|
+
* the core appends before a read reaches any adapter (since 0.1.153), so a
|
|
376
|
+
* direct adapter call orders ties the same way. Returns the input unchanged
|
|
373
377
|
* (insertion order) when there is no `$sort`. Delegates to the shared pure
|
|
374
|
-
* {@link sortRows}
|
|
375
|
-
*
|
|
378
|
+
* {@link sortRows}; `topK` asks for the head of that order only
|
|
379
|
+
* (`$skip + $limit`).
|
|
376
380
|
*/
|
|
377
381
|
private _sortRows;
|
|
382
|
+
/** `$sort` plus the primary-key fields it leaves out, in its last key's direction. */
|
|
383
|
+
private _withPkTieBreak;
|
|
378
384
|
/**
|
|
379
385
|
* Compiles the `$select` projection of one read: each returned row is a
|
|
380
386
|
* fresh, deep-cloned object so the store can never be mutated through it.
|
|
@@ -444,6 +450,8 @@ declare class MemoryAdapter extends BaseDbAdapter {
|
|
|
444
450
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
445
451
|
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
446
452
|
supportsAggregateExpressions(): boolean;
|
|
453
|
+
/** NULL placement (`$nulls`, `@db.sort.nulls`) — `sortRows` honours it per key. */
|
|
454
|
+
supportsNullsPlacement(): boolean;
|
|
447
455
|
/** Managed views render nothing (non-goal) — both are accepted like `aggregateFns()`. */
|
|
448
456
|
viewCapabilities(): ReadonlySet<TViewCapability>;
|
|
449
457
|
updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
|
|
@@ -547,20 +555,24 @@ declare function buildMemoryPredicate(filter: FilterExpr, relationSets?: Relatio
|
|
|
547
555
|
* - No `$sort` (or an empty one) → returns the input array UNCHANGED (same
|
|
548
556
|
* reference, insertion order preserved) — the fast path for an unsorted read.
|
|
549
557
|
* - `tieBreak`, when supplied, is the FINAL deterministic tie-break key for a
|
|
550
|
-
* TOTAL order
|
|
551
|
-
*
|
|
558
|
+
* TOTAL order, so rows with equal sort keys still order deterministically
|
|
559
|
+
* (the adapter instead appends its primary-key fields to `$sort`, compared
|
|
560
|
+
* by value like every other key — since 0.1.153). When ABSENT the
|
|
552
561
|
* sort falls back to preserving input order among equal keys (via each row's
|
|
553
562
|
* original index), so a consumer with no primary key keeps insertion order.
|
|
554
563
|
* - `topK` (since 0.1.151), when supplied, asks for only the first `topK` rows
|
|
555
564
|
* of that order (e.g. `$skip + $limit`): a small one is selected in one pass
|
|
556
565
|
* instead of sorting everything. The rows returned are exactly the head of
|
|
557
566
|
* the full sort — same comparator, same stable tie handling.
|
|
567
|
+
* - `nulls` (since 0.1.153) — the `$nulls` control: a `$sort` key with an
|
|
568
|
+
* entry puts `null`/missing values `'first'` or `'last'` in either
|
|
569
|
+
* direction; a key without one keeps them smallest.
|
|
558
570
|
*
|
|
559
571
|
* NEVER mutates the input array. Each row's sort keys are read ONCE (O(n)) and
|
|
560
572
|
* its `tieBreak` only when two rows tie on every key — not inside the
|
|
561
573
|
* O(n log n) comparator.
|
|
562
574
|
*/
|
|
563
|
-
declare function sortRows(rows: Record<string, unknown>[], $sort?: Partial<Record<string, 1 | -1>>, tieBreak?: (row: Record<string, unknown>) => string | number, topK?: number): Record<string, unknown>[];
|
|
575
|
+
declare function sortRows(rows: Record<string, unknown>[], $sort?: Partial<Record<string, 1 | -1>>, tieBreak?: (row: Record<string, unknown>) => string | number, topK?: number, nulls?: Partial<Record<string, NullsPlacement>>): Record<string, unknown>[];
|
|
564
576
|
/** Options for {@link projectRow}. */
|
|
565
577
|
interface ProjectRowOptions {
|
|
566
578
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, ResolvedRelationFilter, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TFieldOps, TViewCapability } from "@atscript/db";
|
|
2
|
+
import { NullsPlacement } from "@uniqu/core";
|
|
2
3
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
3
4
|
|
|
4
5
|
//#region src/lib/memory-adapter.d.ts
|
|
@@ -368,13 +369,18 @@ declare class MemoryAdapter extends BaseDbAdapter {
|
|
|
368
369
|
updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
|
|
369
370
|
deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
|
|
370
371
|
/**
|
|
371
|
-
* Stable multi-key comparator from `$sort`, with
|
|
372
|
-
*
|
|
372
|
+
* Stable multi-key comparator from `$sort`, with the primary-key fields it
|
|
373
|
+
* leaves out appended in the direction of its LAST key and compared by
|
|
374
|
+
* value (numbers numerically, `Date`s by instant) — the same tie-breaker
|
|
375
|
+
* the core appends before a read reaches any adapter (since 0.1.153), so a
|
|
376
|
+
* direct adapter call orders ties the same way. Returns the input unchanged
|
|
373
377
|
* (insertion order) when there is no `$sort`. Delegates to the shared pure
|
|
374
|
-
* {@link sortRows}
|
|
375
|
-
*
|
|
378
|
+
* {@link sortRows}; `topK` asks for the head of that order only
|
|
379
|
+
* (`$skip + $limit`).
|
|
376
380
|
*/
|
|
377
381
|
private _sortRows;
|
|
382
|
+
/** `$sort` plus the primary-key fields it leaves out, in its last key's direction. */
|
|
383
|
+
private _withPkTieBreak;
|
|
378
384
|
/**
|
|
379
385
|
* Compiles the `$select` projection of one read: each returned row is a
|
|
380
386
|
* fresh, deep-cloned object so the store can never be mutated through it.
|
|
@@ -444,6 +450,8 @@ declare class MemoryAdapter extends BaseDbAdapter {
|
|
|
444
450
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
445
451
|
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
446
452
|
supportsAggregateExpressions(): boolean;
|
|
453
|
+
/** NULL placement (`$nulls`, `@db.sort.nulls`) — `sortRows` honours it per key. */
|
|
454
|
+
supportsNullsPlacement(): boolean;
|
|
447
455
|
/** Managed views render nothing (non-goal) — both are accepted like `aggregateFns()`. */
|
|
448
456
|
viewCapabilities(): ReadonlySet<TViewCapability>;
|
|
449
457
|
updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
|
|
@@ -547,20 +555,24 @@ declare function buildMemoryPredicate(filter: FilterExpr, relationSets?: Relatio
|
|
|
547
555
|
* - No `$sort` (or an empty one) → returns the input array UNCHANGED (same
|
|
548
556
|
* reference, insertion order preserved) — the fast path for an unsorted read.
|
|
549
557
|
* - `tieBreak`, when supplied, is the FINAL deterministic tie-break key for a
|
|
550
|
-
* TOTAL order
|
|
551
|
-
*
|
|
558
|
+
* TOTAL order, so rows with equal sort keys still order deterministically
|
|
559
|
+
* (the adapter instead appends its primary-key fields to `$sort`, compared
|
|
560
|
+
* by value like every other key — since 0.1.153). When ABSENT the
|
|
552
561
|
* sort falls back to preserving input order among equal keys (via each row's
|
|
553
562
|
* original index), so a consumer with no primary key keeps insertion order.
|
|
554
563
|
* - `topK` (since 0.1.151), when supplied, asks for only the first `topK` rows
|
|
555
564
|
* of that order (e.g. `$skip + $limit`): a small one is selected in one pass
|
|
556
565
|
* instead of sorting everything. The rows returned are exactly the head of
|
|
557
566
|
* the full sort — same comparator, same stable tie handling.
|
|
567
|
+
* - `nulls` (since 0.1.153) — the `$nulls` control: a `$sort` key with an
|
|
568
|
+
* entry puts `null`/missing values `'first'` or `'last'` in either
|
|
569
|
+
* direction; a key without one keeps them smallest.
|
|
558
570
|
*
|
|
559
571
|
* NEVER mutates the input array. Each row's sort keys are read ONCE (O(n)) and
|
|
560
572
|
* its `tieBreak` only when two rows tie on every key — not inside the
|
|
561
573
|
* O(n log n) comparator.
|
|
562
574
|
*/
|
|
563
|
-
declare function sortRows(rows: Record<string, unknown>[], $sort?: Partial<Record<string, 1 | -1>>, tieBreak?: (row: Record<string, unknown>) => string | number, topK?: number): Record<string, unknown>[];
|
|
575
|
+
declare function sortRows(rows: Record<string, unknown>[], $sort?: Partial<Record<string, 1 | -1>>, tieBreak?: (row: Record<string, unknown>) => string | number, topK?: number, nulls?: Partial<Record<string, NullsPlacement>>): Record<string, unknown>[];
|
|
564
576
|
/** Options for {@link projectRow}. */
|
|
565
577
|
interface ProjectRowOptions {
|
|
566
578
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -427,8 +427,8 @@ function buildMemoryPredicate(filter, relationSets) {
|
|
|
427
427
|
* there is exactly ONE implementation. Everything here is a pure function over
|
|
428
428
|
* plain `Record<string, unknown>` rows — no adapter/table state — so other
|
|
429
429
|
* consumers (e.g. moost-db's value-help controller) can reuse the SAME engine
|
|
430
|
-
* instead of hand-rolling a second copy. The adapter
|
|
431
|
-
*
|
|
430
|
+
* instead of hand-rolling a second copy. The adapter appends its own
|
|
431
|
+
* physical primary-key fields to `$sort` before calling in.
|
|
432
432
|
*
|
|
433
433
|
* The dot-path READ / DELETE helpers (`getPath` / `deletePath`) are the
|
|
434
434
|
* core's (`@atscript/db`); the dot-path WRITE helper ({@link setPath}) lives
|
|
@@ -454,6 +454,24 @@ function compareLeaves(a, b) {
|
|
|
454
454
|
return as < bs ? -1 : as > bs ? 1 : 0;
|
|
455
455
|
}
|
|
456
456
|
/**
|
|
457
|
+
* {@link compareLeaves} with a NULL placement: `'first'` / `'last'` put
|
|
458
|
+
* `null`/`undefined` before / after every value, and `desc` reverses only the
|
|
459
|
+
* comparison of two values (since 0.1.153). Without `nulls`, nil is the
|
|
460
|
+
* smallest value and `desc` reverses everything, as before.
|
|
461
|
+
*/
|
|
462
|
+
function compareSortKey(a, b, desc, nulls) {
|
|
463
|
+
if (nulls) {
|
|
464
|
+
const aNil = a === null || a === void 0;
|
|
465
|
+
const bNil = b === null || b === void 0;
|
|
466
|
+
if (aNil || bNil) {
|
|
467
|
+
if (aNil && bNil) return 0;
|
|
468
|
+
return (aNil ? -1 : 1) * (nulls === "first" ? 1 : -1);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const cmp = compareLeaves(a, b);
|
|
472
|
+
return desc ? -cmp : cmp;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
457
475
|
* Dot-path setter used by inclusion projection. Creates intermediate plain
|
|
458
476
|
* objects as needed; overwrites a non-object intermediate. Top-level keys and
|
|
459
477
|
* nested dot-paths both work.
|
|
@@ -478,24 +496,29 @@ function setSegments(target, segments, value) {
|
|
|
478
496
|
* - No `$sort` (or an empty one) → returns the input array UNCHANGED (same
|
|
479
497
|
* reference, insertion order preserved) — the fast path for an unsorted read.
|
|
480
498
|
* - `tieBreak`, when supplied, is the FINAL deterministic tie-break key for a
|
|
481
|
-
* TOTAL order
|
|
482
|
-
*
|
|
499
|
+
* TOTAL order, so rows with equal sort keys still order deterministically
|
|
500
|
+
* (the adapter instead appends its primary-key fields to `$sort`, compared
|
|
501
|
+
* by value like every other key — since 0.1.153). When ABSENT the
|
|
483
502
|
* sort falls back to preserving input order among equal keys (via each row's
|
|
484
503
|
* original index), so a consumer with no primary key keeps insertion order.
|
|
485
504
|
* - `topK` (since 0.1.151), when supplied, asks for only the first `topK` rows
|
|
486
505
|
* of that order (e.g. `$skip + $limit`): a small one is selected in one pass
|
|
487
506
|
* instead of sorting everything. The rows returned are exactly the head of
|
|
488
507
|
* the full sort — same comparator, same stable tie handling.
|
|
508
|
+
* - `nulls` (since 0.1.153) — the `$nulls` control: a `$sort` key with an
|
|
509
|
+
* entry puts `null`/missing values `'first'` or `'last'` in either
|
|
510
|
+
* direction; a key without one keeps them smallest.
|
|
489
511
|
*
|
|
490
512
|
* NEVER mutates the input array. Each row's sort keys are read ONCE (O(n)) and
|
|
491
513
|
* its `tieBreak` only when two rows tie on every key — not inside the
|
|
492
514
|
* O(n log n) comparator.
|
|
493
515
|
*/
|
|
494
|
-
function sortRows(rows, $sort, tieBreak, topK) {
|
|
516
|
+
function sortRows(rows, $sort, tieBreak, topK, nulls) {
|
|
495
517
|
const keys = $sort ? Object.entries($sort) : [];
|
|
496
518
|
if (keys.length === 0) return rows;
|
|
497
519
|
const readers = keys.map(([field]) => pathReader(field));
|
|
498
520
|
const desc = keys.map(([, dir]) => dir === -1);
|
|
521
|
+
const placement = nulls ? keys.map(([field]) => Object.hasOwn(nulls, field) ? nulls[field] : void 0) : [];
|
|
499
522
|
const decorated = rows.map((row, index) => ({
|
|
500
523
|
row,
|
|
501
524
|
index,
|
|
@@ -507,8 +530,8 @@ function sortRows(rows, $sort, tieBreak, topK) {
|
|
|
507
530
|
}));
|
|
508
531
|
const compare = (a, b) => {
|
|
509
532
|
for (let i = 0; i < desc.length; i++) {
|
|
510
|
-
const cmp =
|
|
511
|
-
if (cmp !== 0) return
|
|
533
|
+
const cmp = compareSortKey(a.keys[i], b.keys[i], desc[i], placement[i]);
|
|
534
|
+
if (cmp !== 0) return cmp;
|
|
512
535
|
}
|
|
513
536
|
if (tieBreak) {
|
|
514
537
|
const at = a.tie ??= tieBreak(a.row);
|
|
@@ -789,7 +812,7 @@ function aggregateRows(rows, controls) {
|
|
|
789
812
|
const having = controls.$having;
|
|
790
813
|
if (having) out = out.filter(buildMemoryPredicate(having));
|
|
791
814
|
if (controls.$count) return [{ count: out.length }];
|
|
792
|
-
const paged = paginate(sortRows(out, controls.$sort), controls.$skip, controls.$limit);
|
|
815
|
+
const paged = paginate(sortRows(out, controls.$sort, void 0, void 0, controls.$nulls), controls.$skip, controls.$limit);
|
|
793
816
|
const fields = ($select === void 0 ? groupBy : $select.asArray ?? []).map((field) => [field, pathReader(field)]);
|
|
794
817
|
const outputAliases = $select === void 0 ? [] : [...($select.buckets ?? []).map((b) => b.alias), ...$select.computedAliases];
|
|
795
818
|
return paged.map((row) => {
|
|
@@ -865,18 +888,19 @@ function exprAccumulatorFactory(e) {
|
|
|
865
888
|
/**
|
|
866
889
|
* `first` / `last`: the value of `fl.column` on the group's representative
|
|
867
890
|
* row — the least (`first`) or greatest (`last`) row by `rowOrder` (NULL
|
|
868
|
-
* smallest; the primary key is the final key, so the pick is deterministic).
|
|
891
|
+
* smallest unless the key carries a `nulls` placement; the primary key is the final key, so the pick is deterministic).
|
|
869
892
|
*/
|
|
870
893
|
function firstLastFactory(fl, rowOrder) {
|
|
871
894
|
const keys = rowOrder.map((k) => ({
|
|
872
895
|
read: pathReader(k.column),
|
|
873
|
-
|
|
896
|
+
desc: k.desc,
|
|
897
|
+
nulls: k.nulls
|
|
874
898
|
}));
|
|
875
899
|
const read = pathReader(fl.column);
|
|
876
900
|
const sign = fl.fn === "first" ? -1 : 1;
|
|
877
901
|
const compare = (a, b) => {
|
|
878
902
|
for (const k of keys) {
|
|
879
|
-
const c =
|
|
903
|
+
const c = compareSortKey(k.read(a), k.read(b), k.desc, k.nulls);
|
|
880
904
|
if (c !== 0) return c;
|
|
881
905
|
}
|
|
882
906
|
return 0;
|
|
@@ -1580,14 +1604,28 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1580
1604
|
return { deletedCount: 1 };
|
|
1581
1605
|
}
|
|
1582
1606
|
/**
|
|
1583
|
-
* Stable multi-key comparator from `$sort`, with
|
|
1584
|
-
*
|
|
1607
|
+
* Stable multi-key comparator from `$sort`, with the primary-key fields it
|
|
1608
|
+
* leaves out appended in the direction of its LAST key and compared by
|
|
1609
|
+
* value (numbers numerically, `Date`s by instant) — the same tie-breaker
|
|
1610
|
+
* the core appends before a read reaches any adapter (since 0.1.153), so a
|
|
1611
|
+
* direct adapter call orders ties the same way. Returns the input unchanged
|
|
1585
1612
|
* (insertion order) when there is no `$sort`. Delegates to the shared pure
|
|
1586
|
-
* {@link sortRows}
|
|
1587
|
-
*
|
|
1613
|
+
* {@link sortRows}; `topK` asks for the head of that order only
|
|
1614
|
+
* (`$skip + $limit`).
|
|
1588
1615
|
*/
|
|
1589
|
-
_sortRows(rows, $sort, topK) {
|
|
1590
|
-
return sortRows(rows, $sort,
|
|
1616
|
+
_sortRows(rows, $sort, $nulls, topK) {
|
|
1617
|
+
return sortRows(rows, this._withPkTieBreak($sort), void 0, topK, $nulls);
|
|
1618
|
+
}
|
|
1619
|
+
/** `$sort` plus the primary-key fields it leaves out, in its last key's direction. */
|
|
1620
|
+
_withPkTieBreak($sort) {
|
|
1621
|
+
if (!$sort || !hasSort($sort)) return $sort;
|
|
1622
|
+
const keys = Object.keys($sort);
|
|
1623
|
+
const missing = this._physicalPkFields().filter((field) => !(field in $sort));
|
|
1624
|
+
if (missing.length === 0) return $sort;
|
|
1625
|
+
const dir = $sort[keys[keys.length - 1]];
|
|
1626
|
+
const next = { ...$sort };
|
|
1627
|
+
for (const field of missing) next[field] = dir;
|
|
1628
|
+
return next;
|
|
1591
1629
|
}
|
|
1592
1630
|
/**
|
|
1593
1631
|
* Compiles the `$select` projection of one read: each returned row is a
|
|
@@ -1615,6 +1653,7 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1615
1653
|
_readControls(controls) {
|
|
1616
1654
|
return {
|
|
1617
1655
|
$sort: controls.$sort,
|
|
1656
|
+
$nulls: controls.$nulls,
|
|
1618
1657
|
$skip: controls.$skip,
|
|
1619
1658
|
$limit: controls.$limit,
|
|
1620
1659
|
$select: controls.$select
|
|
@@ -1651,9 +1690,9 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1651
1690
|
return (await own).filter(buildMemoryPredicate(query.filter, sets));
|
|
1652
1691
|
}
|
|
1653
1692
|
async findOne(query) {
|
|
1654
|
-
const { $sort, $skip, $select } = this._readControls(query.controls ?? {});
|
|
1693
|
+
const { $sort, $nulls, $skip, $select } = this._readControls(query.controls ?? {});
|
|
1655
1694
|
const index = $skip ?? 0;
|
|
1656
|
-
const row = (hasSort($sort) ? this._sortRows(await this._filteredRows(query), $sort, index + 1) : await this._filteredRows(query, index + 1))[index];
|
|
1695
|
+
const row = (hasSort($sort) ? this._sortRows(await this._filteredRows(query), $sort, $nulls, index + 1) : await this._filteredRows(query, index + 1))[index];
|
|
1657
1696
|
return row ? this._projector($select)(row) : null;
|
|
1658
1697
|
}
|
|
1659
1698
|
async findMany(query) {
|
|
@@ -1671,10 +1710,10 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1671
1710
|
*/
|
|
1672
1711
|
async findManyWithCount(query) {
|
|
1673
1712
|
const filtered = await this._filteredRows(query);
|
|
1674
|
-
const { $sort, $skip, $limit, $select } = this._readControls(query.controls ?? {});
|
|
1713
|
+
const { $sort, $nulls, $skip, $limit, $select } = this._readControls(query.controls ?? {});
|
|
1675
1714
|
const topK = $limit === void 0 ? void 0 : ($skip ?? 0) + $limit;
|
|
1676
1715
|
return {
|
|
1677
|
-
data: paginate(this._sortRows(filtered, $sort, topK), $skip, $limit).map(this._projector($select)),
|
|
1716
|
+
data: paginate(this._sortRows(filtered, $sort, $nulls, topK), $skip, $limit).map(this._projector($select)),
|
|
1678
1717
|
count: filtered.length
|
|
1679
1718
|
};
|
|
1680
1719
|
}
|
|
@@ -1708,6 +1747,10 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1708
1747
|
supportsAggregateExpressions() {
|
|
1709
1748
|
return true;
|
|
1710
1749
|
}
|
|
1750
|
+
/** NULL placement (`$nulls`, `@db.sort.nulls`) — `sortRows` honours it per key. */
|
|
1751
|
+
supportsNullsPlacement() {
|
|
1752
|
+
return true;
|
|
1753
|
+
}
|
|
1711
1754
|
/** Managed views render nothing (non-goal) — both are accepted like `aggregateFns()`. */
|
|
1712
1755
|
viewCapabilities() {
|
|
1713
1756
|
return ALL_VIEW_CAPABILITIES;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.153",
|
|
4
4
|
"description": "In-memory adapter for atscript-db.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -36,16 +36,16 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@atscript/core": "^0.1.
|
|
40
|
-
"@atscript/typescript": "^0.1.
|
|
41
|
-
"@uniqu/core": "^0.1.
|
|
42
|
-
"unplugin-atscript": "^0.1.
|
|
39
|
+
"@atscript/core": "^0.1.103",
|
|
40
|
+
"@atscript/typescript": "^0.1.103",
|
|
41
|
+
"@uniqu/core": "^0.1.15",
|
|
42
|
+
"unplugin-atscript": "^0.1.103"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@atscript/core": "^0.1.
|
|
46
|
-
"@atscript/typescript": "^0.1.
|
|
47
|
-
"@uniqu/core": "^0.1.
|
|
48
|
-
"@atscript/db": "^0.1.
|
|
45
|
+
"@atscript/core": "^0.1.103",
|
|
46
|
+
"@atscript/typescript": "^0.1.103",
|
|
47
|
+
"@uniqu/core": "^0.1.15",
|
|
48
|
+
"@atscript/db": "^0.1.153"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"postinstall": "asc -f dts",
|