@atscript/db-memory 0.1.147 → 0.1.148
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 +154 -66
- package/dist/index.d.cts +8 -3
- package/dist/index.d.mts +8 -3
- package/dist/index.mjs +155 -67
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -74,15 +74,26 @@ function stringifyLeaf(v) {
|
|
|
74
74
|
/** `$eq` semantics, factored out so `$ne` can be its exact negation. */
|
|
75
75
|
function evalEq(row, field, value) {
|
|
76
76
|
const fieldValue = (0, _atscript_db.getPath)(row, field);
|
|
77
|
-
if (value === null) return fieldValue
|
|
77
|
+
if (value === null) return matchesScalar(fieldValue, null);
|
|
78
78
|
if (value instanceof RegExp) return fieldValue != null && value.test(stringifyLeaf(fieldValue));
|
|
79
|
+
return matchesScalar(fieldValue, value);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Mongo containment: a scalar compared with an array field matches when ANY element
|
|
83
|
+
* equals it (`{ labels: "bug" }` matches `["bug", "ui"]`). Non-array fields compare as before.
|
|
84
|
+
* `null` also matches a missing field (`undefined`) and an array holding a `null` element,
|
|
85
|
+
* as `{ labels: null }` / `$in: [null]` do in Mongo.
|
|
86
|
+
*/
|
|
87
|
+
function matchesScalar(fieldValue, value) {
|
|
88
|
+
if (value === null) return fieldValue == null || Array.isArray(fieldValue) && fieldValue.includes(null);
|
|
89
|
+
if (Array.isArray(fieldValue) && !Array.isArray(value)) return fieldValue.some((el) => valuesEqual(el, value));
|
|
79
90
|
return valuesEqual(fieldValue, value);
|
|
80
91
|
}
|
|
81
92
|
/** `$in` membership, factored out so `$nin` can be its exact negation. */
|
|
82
93
|
function evalIn(row, field, value) {
|
|
83
94
|
if (!Array.isArray(value)) return false;
|
|
84
95
|
const fieldValue = (0, _atscript_db.getPath)(row, field);
|
|
85
|
-
return value.some((element) =>
|
|
96
|
+
return value.some((element) => matchesScalar(fieldValue, element));
|
|
86
97
|
}
|
|
87
98
|
/**
|
|
88
99
|
* Coerce a leaf to something the JS relational operators can order. `Date`s
|
|
@@ -464,10 +475,28 @@ function projectRow(row, projection, opts) {
|
|
|
464
475
|
function aggregateRows(rows, controls) {
|
|
465
476
|
const $select = controls.$select;
|
|
466
477
|
const groupBy = controls.$groupBy ?? [];
|
|
467
|
-
const
|
|
468
|
-
|
|
478
|
+
const entries = [
|
|
479
|
+
...($select?.aggregates ?? []).map((expr) => ({
|
|
480
|
+
alias: (0, _atscript_db_agg.resolveAlias)(expr),
|
|
481
|
+
create: accumulatorFactory(expr)
|
|
482
|
+
})),
|
|
483
|
+
...($select?.exprAggregates ?? []).map((e) => ({
|
|
484
|
+
alias: e.alias,
|
|
485
|
+
create: exprAccumulatorFactory(e)
|
|
486
|
+
})),
|
|
487
|
+
...($select?.firstLast ?? []).map((fl) => ({
|
|
488
|
+
alias: fl.alias,
|
|
489
|
+
create: firstLastFactory(fl, $select?.rowOrder ?? [])
|
|
490
|
+
}))
|
|
491
|
+
];
|
|
492
|
+
const aliases = entries.map((e) => e.alias);
|
|
493
|
+
const groupExprs = ($select?.exprs ?? []).map((e) => ({
|
|
494
|
+
alias: e.alias,
|
|
495
|
+
expr: e.expr,
|
|
496
|
+
readers: new Map(e.names.map((name) => [name, pathReader(name)]))
|
|
497
|
+
}));
|
|
469
498
|
const keyReaders = groupBy.map((key) => groupKeyReader(key, controls));
|
|
470
|
-
const accumulators =
|
|
499
|
+
const accumulators = entries.map((e) => e.create);
|
|
471
500
|
const newGroup = (values) => ({
|
|
472
501
|
values,
|
|
473
502
|
accs: accumulators.map((create) => create())
|
|
@@ -496,6 +525,7 @@ function aggregateRows(rows, controls) {
|
|
|
496
525
|
const row = {};
|
|
497
526
|
for (let i = 0; i < groupBy.length; i++) setPath(row, groupBy[i], values[i]);
|
|
498
527
|
for (let i = 0; i < aliases.length; i++) row[aliases[i]] = accs[i].result();
|
|
528
|
+
for (const e of groupExprs) row[e.alias] = (0, _atscript_db.evaluateExpr)(e.expr, (name) => Object.hasOwn(row, name) ? row[name] : e.readers.get(name)(row));
|
|
499
529
|
out.push(row);
|
|
500
530
|
}
|
|
501
531
|
const having = controls.$having;
|
|
@@ -503,7 +533,7 @@ function aggregateRows(rows, controls) {
|
|
|
503
533
|
if (controls.$count) return [{ count: out.length }];
|
|
504
534
|
const paged = paginate(sortRows(out, controls.$sort), controls.$skip, controls.$limit);
|
|
505
535
|
const fields = ($select === void 0 ? groupBy : $select.asArray ?? []).map((field) => [field, pathReader(field)]);
|
|
506
|
-
const outputAliases = $select === void 0 ? [] : [...($select.buckets ?? []).map((b) => b.alias),
|
|
536
|
+
const outputAliases = $select === void 0 ? [] : [...($select.buckets ?? []).map((b) => b.alias), ...$select.computedAliases];
|
|
507
537
|
return paged.map((row) => {
|
|
508
538
|
const picked = {};
|
|
509
539
|
for (const [field, read] of fields) setPath(picked, field, copyValue(read(row) ?? null));
|
|
@@ -569,76 +599,109 @@ function numericValue(value) {
|
|
|
569
599
|
return Number.isNaN(n) ? void 0 : n;
|
|
570
600
|
}
|
|
571
601
|
}
|
|
602
|
+
/** `sum` / `avg` / `min` / `max` over a per-row arithmetic expression; NULL results are skipped like NULL fields. */
|
|
603
|
+
function exprAccumulatorFactory(e) {
|
|
604
|
+
const readers = new Map(e.names.map((name) => [name, pathReader(name)]));
|
|
605
|
+
return fnAccumulator(e.fn, (row) => (0, _atscript_db.evaluateExpr)(e.expr, (field) => readers.get(field)(row)));
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* `first` / `last`: the value of `fl.column` on the group's representative
|
|
609
|
+
* row — the least (`first`) or greatest (`last`) row by `rowOrder` (NULL
|
|
610
|
+
* smallest; the primary key is the final key, so the pick is deterministic).
|
|
611
|
+
*/
|
|
612
|
+
function firstLastFactory(fl, rowOrder) {
|
|
613
|
+
const keys = rowOrder.map((k) => ({
|
|
614
|
+
read: pathReader(k.column),
|
|
615
|
+
sign: k.desc ? -1 : 1
|
|
616
|
+
}));
|
|
617
|
+
const read = pathReader(fl.column);
|
|
618
|
+
const sign = fl.fn === "first" ? -1 : 1;
|
|
619
|
+
const compare = (a, b) => {
|
|
620
|
+
for (const k of keys) {
|
|
621
|
+
const c = compareLeaves(k.read(a), k.read(b)) * k.sign;
|
|
622
|
+
if (c !== 0) return c;
|
|
623
|
+
}
|
|
624
|
+
return 0;
|
|
625
|
+
};
|
|
626
|
+
return () => {
|
|
627
|
+
let best;
|
|
628
|
+
return {
|
|
629
|
+
add: (row) => {
|
|
630
|
+
if (best === void 0 || compare(row, best) * sign > 0) best = row;
|
|
631
|
+
},
|
|
632
|
+
result: () => best === void 0 ? null : copyValue(read(best) ?? null)
|
|
633
|
+
};
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* `sum` / `avg` / `min` / `max` over the values `read` yields per row — a
|
|
638
|
+
* field's, or a per-row expression's. `sum` / `avg` skip non-numeric values
|
|
639
|
+
* and are `null` over none; `min` / `max` skip null / missing and order with
|
|
640
|
+
* the `$sort` comparator.
|
|
641
|
+
*/
|
|
642
|
+
function fnAccumulator(fn, read) {
|
|
643
|
+
if (fn === "sum" || fn === "avg") return () => {
|
|
644
|
+
let sum = 0;
|
|
645
|
+
let n = 0;
|
|
646
|
+
return {
|
|
647
|
+
add: (row) => {
|
|
648
|
+
const v = numericValue(read(row));
|
|
649
|
+
if (v !== void 0) {
|
|
650
|
+
sum += v;
|
|
651
|
+
n++;
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
result: () => n === 0 ? null : fn === "avg" ? sum / n : sum
|
|
655
|
+
};
|
|
656
|
+
};
|
|
657
|
+
const sign = fn === "min" ? -1 : 1;
|
|
658
|
+
return () => {
|
|
659
|
+
let best = null;
|
|
660
|
+
return {
|
|
661
|
+
add: (row) => {
|
|
662
|
+
const v = read(row);
|
|
663
|
+
if (v == null) return;
|
|
664
|
+
if (best === null || compareLeaves(v, best) * sign > 0) best = v;
|
|
665
|
+
},
|
|
666
|
+
result: () => best
|
|
667
|
+
};
|
|
668
|
+
};
|
|
669
|
+
}
|
|
572
670
|
/**
|
|
573
|
-
* Resolves
|
|
574
|
-
* per group). Validated up front,
|
|
575
|
-
* there are no rows.
|
|
671
|
+
* Resolves a plain aggregate (`first` / `last` are {@link firstLastFactory}'s)
|
|
672
|
+
* to an accumulator factory (one accumulator per group). Validated up front,
|
|
673
|
+
* so an unknown `$fn` is rejected even when there are no rows.
|
|
576
674
|
*/
|
|
577
675
|
function accumulatorFactory(expr) {
|
|
578
676
|
(0, _atscript_db_agg.assertAggregateFn)(expr.$fn);
|
|
579
677
|
const field = expr.$field;
|
|
580
678
|
const read = pathReader(field);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
let n = 0;
|
|
586
|
-
return {
|
|
587
|
-
add: (row) => {
|
|
588
|
-
if (all || read(row) != null) n++;
|
|
589
|
-
},
|
|
590
|
-
result: () => n
|
|
591
|
-
};
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
case "sum":
|
|
595
|
-
case "avg": {
|
|
596
|
-
const avg = expr.$fn === "avg";
|
|
597
|
-
return () => {
|
|
598
|
-
let sum = 0;
|
|
599
|
-
let n = 0;
|
|
600
|
-
return {
|
|
601
|
-
add: (row) => {
|
|
602
|
-
const v = numericValue(read(row));
|
|
603
|
-
if (v !== void 0) {
|
|
604
|
-
sum += v;
|
|
605
|
-
n++;
|
|
606
|
-
}
|
|
607
|
-
},
|
|
608
|
-
result: () => n === 0 ? null : avg ? sum / n : sum
|
|
609
|
-
};
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
case "countDistinct": return () => {
|
|
613
|
-
const plain = /* @__PURE__ */ new Set();
|
|
614
|
-
const tokens = /* @__PURE__ */ new Set();
|
|
679
|
+
if (expr.$fn === "count") {
|
|
680
|
+
const all = field === "*";
|
|
681
|
+
return () => {
|
|
682
|
+
let n = 0;
|
|
615
683
|
return {
|
|
616
684
|
add: (row) => {
|
|
617
|
-
|
|
618
|
-
if (v == null) return;
|
|
619
|
-
if (typeof v === "string" || typeof v === "boolean") plain.add(v);
|
|
620
|
-
else tokens.add(identityToken(v));
|
|
685
|
+
if (all || read(row) != null) n++;
|
|
621
686
|
},
|
|
622
|
-
result: () =>
|
|
687
|
+
result: () => n
|
|
623
688
|
};
|
|
624
689
|
};
|
|
625
|
-
case "min":
|
|
626
|
-
case "max": {
|
|
627
|
-
const sign = expr.$fn === "min" ? -1 : 1;
|
|
628
|
-
return () => {
|
|
629
|
-
let best = null;
|
|
630
|
-
return {
|
|
631
|
-
add: (row) => {
|
|
632
|
-
const v = read(row);
|
|
633
|
-
if (v == null) return;
|
|
634
|
-
if (best === null || compareLeaves(v, best) * sign > 0) best = v;
|
|
635
|
-
},
|
|
636
|
-
result: () => best
|
|
637
|
-
};
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
default: return expr.$fn;
|
|
641
690
|
}
|
|
691
|
+
if (expr.$fn === "countDistinct") return () => {
|
|
692
|
+
const plain = /* @__PURE__ */ new Set();
|
|
693
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
694
|
+
return {
|
|
695
|
+
add: (row) => {
|
|
696
|
+
const v = read(row);
|
|
697
|
+
if (v == null) return;
|
|
698
|
+
if (typeof v === "string" || typeof v === "boolean") plain.add(v);
|
|
699
|
+
else tokens.add(identityToken(v));
|
|
700
|
+
},
|
|
701
|
+
result: () => plain.size + tokens.size
|
|
702
|
+
};
|
|
703
|
+
};
|
|
704
|
+
return fnAccumulator(expr.$fn, read);
|
|
642
705
|
}
|
|
643
706
|
//#endregion
|
|
644
707
|
//#region src/lib/memory-adapter.ts
|
|
@@ -1060,6 +1123,22 @@ var MemoryAdapter = class MemoryAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1060
1123
|
insertedIds
|
|
1061
1124
|
};
|
|
1062
1125
|
}
|
|
1126
|
+
supportsInsertIgnore() {
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
/** A row colliding on the primary key or a unique index (`CONFLICT`) becomes a skipped slot. */
|
|
1130
|
+
async insertManyIgnore(data) {
|
|
1131
|
+
this._assertWritable();
|
|
1132
|
+
const state = this._state();
|
|
1133
|
+
return data.map((item) => {
|
|
1134
|
+
try {
|
|
1135
|
+
return { insertedId: this._insertRow(state, item) };
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
if ((0, _atscript_db.isConflict)(error)) return null;
|
|
1138
|
+
throw error;
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1063
1142
|
/**
|
|
1064
1143
|
* Selects the stored rows a write should touch, as `{ key, row }` pairs so
|
|
1065
1144
|
* callers can mutate in place and re-key. Stored mode scans the table's Map
|
|
@@ -1309,10 +1388,14 @@ var MemoryAdapter = class MemoryAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1309
1388
|
calendarBucketUnits() {
|
|
1310
1389
|
return _atscript_db.ALL_BUCKET_UNITS;
|
|
1311
1390
|
}
|
|
1312
|
-
/** Every aggregate function
|
|
1391
|
+
/** Every aggregate function: `countDistinct`, `first` and `last` included. */
|
|
1313
1392
|
aggregateFns() {
|
|
1314
1393
|
return _atscript_db.ALL_AGGREGATE_FNS;
|
|
1315
1394
|
}
|
|
1395
|
+
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
1396
|
+
supportsAggregateExpressions() {
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1316
1399
|
/** Managed views render nothing (non-goal) — both are accepted like `aggregateFns()`. */
|
|
1317
1400
|
viewCapabilities() {
|
|
1318
1401
|
return _atscript_db.ALL_VIEW_CAPABILITIES;
|
|
@@ -1435,6 +1518,10 @@ function setMemoryProvider(space, type, fn) {
|
|
|
1435
1518
|
if (!(adapter instanceof MemoryAdapter)) throw new Error("setMemoryProvider: table is not backed by MemoryAdapter");
|
|
1436
1519
|
adapter.setProvider(fn);
|
|
1437
1520
|
}
|
|
1521
|
+
/** @internal Drops the in-memory database of a closed space (called from `createAdapter`'s `onClose`). */
|
|
1522
|
+
function clearMemoryDatabase(space) {
|
|
1523
|
+
databases.get(space)?.clear();
|
|
1524
|
+
}
|
|
1438
1525
|
//#endregion
|
|
1439
1526
|
//#region src/lib/index.ts
|
|
1440
1527
|
/**
|
|
@@ -1447,7 +1534,8 @@ function setMemoryProvider(space, type, fn) {
|
|
|
1447
1534
|
* adapter has been built.
|
|
1448
1535
|
*/
|
|
1449
1536
|
function createAdapter() {
|
|
1450
|
-
|
|
1537
|
+
const space = new _atscript_db.DbSpace(() => new MemoryAdapter(), { onClose: () => clearMemoryDatabase(space) });
|
|
1538
|
+
return space;
|
|
1451
1539
|
}
|
|
1452
1540
|
//#endregion
|
|
1453
1541
|
exports.MemoryAdapter = MemoryAdapter;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, ResolvedRelationFilter, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TFieldOps, TViewCapability } from "@atscript/db";
|
|
1
|
+
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, ResolvedRelationFilter, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TFieldOps, TViewCapability } from "@atscript/db";
|
|
2
2
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
3
3
|
|
|
4
4
|
//#region src/lib/memory-adapter.d.ts
|
|
@@ -269,6 +269,9 @@ declare class MemoryAdapter extends BaseDbAdapter {
|
|
|
269
269
|
private _enforceUniqueIndexes;
|
|
270
270
|
insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
|
|
271
271
|
insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
|
|
272
|
+
supportsInsertIgnore(): boolean;
|
|
273
|
+
/** A row colliding on the primary key or a unique index (`CONFLICT`) becomes a skipped slot. */
|
|
274
|
+
insertManyIgnore(data: Array<Record<string, unknown>>): Promise<TDbInsertIgnoreSlot[]>;
|
|
272
275
|
/**
|
|
273
276
|
* Selects the stored rows a write should touch, as `{ key, row }` pairs so
|
|
274
277
|
* callers can mutate in place and re-key. Stored mode scans the table's Map
|
|
@@ -406,8 +409,10 @@ declare class MemoryAdapter extends BaseDbAdapter {
|
|
|
406
409
|
* there is no zone this adapter cannot resolve).
|
|
407
410
|
*/
|
|
408
411
|
calendarBucketUnits(): ReadonlySet<BucketUnit>;
|
|
409
|
-
/** Every aggregate function
|
|
412
|
+
/** Every aggregate function: `countDistinct`, `first` and `last` included. */
|
|
410
413
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
414
|
+
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
415
|
+
supportsAggregateExpressions(): boolean;
|
|
411
416
|
/** Managed views render nothing (non-goal) — both are accepted like `aggregateFns()`. */
|
|
412
417
|
viewCapabilities(): ReadonlySet<TViewCapability>;
|
|
413
418
|
updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
|
|
@@ -570,4 +575,4 @@ declare function projectRow(row: Record<string, unknown>, projection?: Record<st
|
|
|
570
575
|
*/
|
|
571
576
|
declare function createAdapter(): DbSpace;
|
|
572
577
|
//#endregion
|
|
573
|
-
export { MemoryAdapter, MemoryProviderFn, buildMemoryPredicate, createAdapter, projectRow, setMemoryProvider, sortRows };
|
|
578
|
+
export { MemoryAdapter, type MemoryProviderFn, buildMemoryPredicate, createAdapter, projectRow, setMemoryProvider, sortRows };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, ResolvedRelationFilter, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TFieldOps, TViewCapability } from "@atscript/db";
|
|
1
|
+
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, ResolvedRelationFilter, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TFieldOps, TViewCapability } from "@atscript/db";
|
|
2
2
|
import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
|
|
3
3
|
|
|
4
4
|
//#region src/lib/memory-adapter.d.ts
|
|
@@ -269,6 +269,9 @@ declare class MemoryAdapter extends BaseDbAdapter {
|
|
|
269
269
|
private _enforceUniqueIndexes;
|
|
270
270
|
insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
|
|
271
271
|
insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
|
|
272
|
+
supportsInsertIgnore(): boolean;
|
|
273
|
+
/** A row colliding on the primary key or a unique index (`CONFLICT`) becomes a skipped slot. */
|
|
274
|
+
insertManyIgnore(data: Array<Record<string, unknown>>): Promise<TDbInsertIgnoreSlot[]>;
|
|
272
275
|
/**
|
|
273
276
|
* Selects the stored rows a write should touch, as `{ key, row }` pairs so
|
|
274
277
|
* callers can mutate in place and re-key. Stored mode scans the table's Map
|
|
@@ -406,8 +409,10 @@ declare class MemoryAdapter extends BaseDbAdapter {
|
|
|
406
409
|
* there is no zone this adapter cannot resolve).
|
|
407
410
|
*/
|
|
408
411
|
calendarBucketUnits(): ReadonlySet<BucketUnit>;
|
|
409
|
-
/** Every aggregate function
|
|
412
|
+
/** Every aggregate function: `countDistinct`, `first` and `last` included. */
|
|
410
413
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
414
|
+
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
415
|
+
supportsAggregateExpressions(): boolean;
|
|
411
416
|
/** Managed views render nothing (non-goal) — both are accepted like `aggregateFns()`. */
|
|
412
417
|
viewCapabilities(): ReadonlySet<TViewCapability>;
|
|
413
418
|
updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
|
|
@@ -570,4 +575,4 @@ declare function projectRow(row: Record<string, unknown>, projection?: Record<st
|
|
|
570
575
|
*/
|
|
571
576
|
declare function createAdapter(): DbSpace;
|
|
572
577
|
//#endregion
|
|
573
|
-
export { MemoryAdapter, MemoryProviderFn, buildMemoryPredicate, createAdapter, projectRow, setMemoryProvider, sortRows };
|
|
578
|
+
export { MemoryAdapter, type MemoryProviderFn, buildMemoryPredicate, createAdapter, projectRow, setMemoryProvider, sortRows };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, containsRelationFilter, deletePath, forEachResolvedRelation, getPath, getPath as getPath$1, isAtscriptDbView, isResolvedRelationFilter, walkFilter } from "@atscript/db";
|
|
1
|
+
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, containsRelationFilter, deletePath, evaluateExpr, forEachResolvedRelation, getPath, getPath as getPath$1, isAtscriptDbView, isConflict, isResolvedRelationFilter, walkFilter } from "@atscript/db";
|
|
2
2
|
import { assertAggregateFn, resolveAlias } from "@atscript/db/agg";
|
|
3
3
|
import { bucketer } from "@uniqu/core";
|
|
4
4
|
//#region src/lib/memory-filter.ts
|
|
@@ -73,15 +73,26 @@ function stringifyLeaf(v) {
|
|
|
73
73
|
/** `$eq` semantics, factored out so `$ne` can be its exact negation. */
|
|
74
74
|
function evalEq(row, field, value) {
|
|
75
75
|
const fieldValue = getPath$1(row, field);
|
|
76
|
-
if (value === null) return fieldValue
|
|
76
|
+
if (value === null) return matchesScalar(fieldValue, null);
|
|
77
77
|
if (value instanceof RegExp) return fieldValue != null && value.test(stringifyLeaf(fieldValue));
|
|
78
|
+
return matchesScalar(fieldValue, value);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Mongo containment: a scalar compared with an array field matches when ANY element
|
|
82
|
+
* equals it (`{ labels: "bug" }` matches `["bug", "ui"]`). Non-array fields compare as before.
|
|
83
|
+
* `null` also matches a missing field (`undefined`) and an array holding a `null` element,
|
|
84
|
+
* as `{ labels: null }` / `$in: [null]` do in Mongo.
|
|
85
|
+
*/
|
|
86
|
+
function matchesScalar(fieldValue, value) {
|
|
87
|
+
if (value === null) return fieldValue == null || Array.isArray(fieldValue) && fieldValue.includes(null);
|
|
88
|
+
if (Array.isArray(fieldValue) && !Array.isArray(value)) return fieldValue.some((el) => valuesEqual(el, value));
|
|
78
89
|
return valuesEqual(fieldValue, value);
|
|
79
90
|
}
|
|
80
91
|
/** `$in` membership, factored out so `$nin` can be its exact negation. */
|
|
81
92
|
function evalIn(row, field, value) {
|
|
82
93
|
if (!Array.isArray(value)) return false;
|
|
83
94
|
const fieldValue = getPath$1(row, field);
|
|
84
|
-
return value.some((element) =>
|
|
95
|
+
return value.some((element) => matchesScalar(fieldValue, element));
|
|
85
96
|
}
|
|
86
97
|
/**
|
|
87
98
|
* Coerce a leaf to something the JS relational operators can order. `Date`s
|
|
@@ -463,10 +474,28 @@ function projectRow(row, projection, opts) {
|
|
|
463
474
|
function aggregateRows(rows, controls) {
|
|
464
475
|
const $select = controls.$select;
|
|
465
476
|
const groupBy = controls.$groupBy ?? [];
|
|
466
|
-
const
|
|
467
|
-
|
|
477
|
+
const entries = [
|
|
478
|
+
...($select?.aggregates ?? []).map((expr) => ({
|
|
479
|
+
alias: resolveAlias(expr),
|
|
480
|
+
create: accumulatorFactory(expr)
|
|
481
|
+
})),
|
|
482
|
+
...($select?.exprAggregates ?? []).map((e) => ({
|
|
483
|
+
alias: e.alias,
|
|
484
|
+
create: exprAccumulatorFactory(e)
|
|
485
|
+
})),
|
|
486
|
+
...($select?.firstLast ?? []).map((fl) => ({
|
|
487
|
+
alias: fl.alias,
|
|
488
|
+
create: firstLastFactory(fl, $select?.rowOrder ?? [])
|
|
489
|
+
}))
|
|
490
|
+
];
|
|
491
|
+
const aliases = entries.map((e) => e.alias);
|
|
492
|
+
const groupExprs = ($select?.exprs ?? []).map((e) => ({
|
|
493
|
+
alias: e.alias,
|
|
494
|
+
expr: e.expr,
|
|
495
|
+
readers: new Map(e.names.map((name) => [name, pathReader(name)]))
|
|
496
|
+
}));
|
|
468
497
|
const keyReaders = groupBy.map((key) => groupKeyReader(key, controls));
|
|
469
|
-
const accumulators =
|
|
498
|
+
const accumulators = entries.map((e) => e.create);
|
|
470
499
|
const newGroup = (values) => ({
|
|
471
500
|
values,
|
|
472
501
|
accs: accumulators.map((create) => create())
|
|
@@ -495,6 +524,7 @@ function aggregateRows(rows, controls) {
|
|
|
495
524
|
const row = {};
|
|
496
525
|
for (let i = 0; i < groupBy.length; i++) setPath(row, groupBy[i], values[i]);
|
|
497
526
|
for (let i = 0; i < aliases.length; i++) row[aliases[i]] = accs[i].result();
|
|
527
|
+
for (const e of groupExprs) row[e.alias] = evaluateExpr(e.expr, (name) => Object.hasOwn(row, name) ? row[name] : e.readers.get(name)(row));
|
|
498
528
|
out.push(row);
|
|
499
529
|
}
|
|
500
530
|
const having = controls.$having;
|
|
@@ -502,7 +532,7 @@ function aggregateRows(rows, controls) {
|
|
|
502
532
|
if (controls.$count) return [{ count: out.length }];
|
|
503
533
|
const paged = paginate(sortRows(out, controls.$sort), controls.$skip, controls.$limit);
|
|
504
534
|
const fields = ($select === void 0 ? groupBy : $select.asArray ?? []).map((field) => [field, pathReader(field)]);
|
|
505
|
-
const outputAliases = $select === void 0 ? [] : [...($select.buckets ?? []).map((b) => b.alias),
|
|
535
|
+
const outputAliases = $select === void 0 ? [] : [...($select.buckets ?? []).map((b) => b.alias), ...$select.computedAliases];
|
|
506
536
|
return paged.map((row) => {
|
|
507
537
|
const picked = {};
|
|
508
538
|
for (const [field, read] of fields) setPath(picked, field, copyValue(read(row) ?? null));
|
|
@@ -568,76 +598,109 @@ function numericValue(value) {
|
|
|
568
598
|
return Number.isNaN(n) ? void 0 : n;
|
|
569
599
|
}
|
|
570
600
|
}
|
|
601
|
+
/** `sum` / `avg` / `min` / `max` over a per-row arithmetic expression; NULL results are skipped like NULL fields. */
|
|
602
|
+
function exprAccumulatorFactory(e) {
|
|
603
|
+
const readers = new Map(e.names.map((name) => [name, pathReader(name)]));
|
|
604
|
+
return fnAccumulator(e.fn, (row) => evaluateExpr(e.expr, (field) => readers.get(field)(row)));
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* `first` / `last`: the value of `fl.column` on the group's representative
|
|
608
|
+
* row — the least (`first`) or greatest (`last`) row by `rowOrder` (NULL
|
|
609
|
+
* smallest; the primary key is the final key, so the pick is deterministic).
|
|
610
|
+
*/
|
|
611
|
+
function firstLastFactory(fl, rowOrder) {
|
|
612
|
+
const keys = rowOrder.map((k) => ({
|
|
613
|
+
read: pathReader(k.column),
|
|
614
|
+
sign: k.desc ? -1 : 1
|
|
615
|
+
}));
|
|
616
|
+
const read = pathReader(fl.column);
|
|
617
|
+
const sign = fl.fn === "first" ? -1 : 1;
|
|
618
|
+
const compare = (a, b) => {
|
|
619
|
+
for (const k of keys) {
|
|
620
|
+
const c = compareLeaves(k.read(a), k.read(b)) * k.sign;
|
|
621
|
+
if (c !== 0) return c;
|
|
622
|
+
}
|
|
623
|
+
return 0;
|
|
624
|
+
};
|
|
625
|
+
return () => {
|
|
626
|
+
let best;
|
|
627
|
+
return {
|
|
628
|
+
add: (row) => {
|
|
629
|
+
if (best === void 0 || compare(row, best) * sign > 0) best = row;
|
|
630
|
+
},
|
|
631
|
+
result: () => best === void 0 ? null : copyValue(read(best) ?? null)
|
|
632
|
+
};
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* `sum` / `avg` / `min` / `max` over the values `read` yields per row — a
|
|
637
|
+
* field's, or a per-row expression's. `sum` / `avg` skip non-numeric values
|
|
638
|
+
* and are `null` over none; `min` / `max` skip null / missing and order with
|
|
639
|
+
* the `$sort` comparator.
|
|
640
|
+
*/
|
|
641
|
+
function fnAccumulator(fn, read) {
|
|
642
|
+
if (fn === "sum" || fn === "avg") return () => {
|
|
643
|
+
let sum = 0;
|
|
644
|
+
let n = 0;
|
|
645
|
+
return {
|
|
646
|
+
add: (row) => {
|
|
647
|
+
const v = numericValue(read(row));
|
|
648
|
+
if (v !== void 0) {
|
|
649
|
+
sum += v;
|
|
650
|
+
n++;
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
result: () => n === 0 ? null : fn === "avg" ? sum / n : sum
|
|
654
|
+
};
|
|
655
|
+
};
|
|
656
|
+
const sign = fn === "min" ? -1 : 1;
|
|
657
|
+
return () => {
|
|
658
|
+
let best = null;
|
|
659
|
+
return {
|
|
660
|
+
add: (row) => {
|
|
661
|
+
const v = read(row);
|
|
662
|
+
if (v == null) return;
|
|
663
|
+
if (best === null || compareLeaves(v, best) * sign > 0) best = v;
|
|
664
|
+
},
|
|
665
|
+
result: () => best
|
|
666
|
+
};
|
|
667
|
+
};
|
|
668
|
+
}
|
|
571
669
|
/**
|
|
572
|
-
* Resolves
|
|
573
|
-
* per group). Validated up front,
|
|
574
|
-
* there are no rows.
|
|
670
|
+
* Resolves a plain aggregate (`first` / `last` are {@link firstLastFactory}'s)
|
|
671
|
+
* to an accumulator factory (one accumulator per group). Validated up front,
|
|
672
|
+
* so an unknown `$fn` is rejected even when there are no rows.
|
|
575
673
|
*/
|
|
576
674
|
function accumulatorFactory(expr) {
|
|
577
675
|
assertAggregateFn(expr.$fn);
|
|
578
676
|
const field = expr.$field;
|
|
579
677
|
const read = pathReader(field);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
let n = 0;
|
|
585
|
-
return {
|
|
586
|
-
add: (row) => {
|
|
587
|
-
if (all || read(row) != null) n++;
|
|
588
|
-
},
|
|
589
|
-
result: () => n
|
|
590
|
-
};
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
case "sum":
|
|
594
|
-
case "avg": {
|
|
595
|
-
const avg = expr.$fn === "avg";
|
|
596
|
-
return () => {
|
|
597
|
-
let sum = 0;
|
|
598
|
-
let n = 0;
|
|
599
|
-
return {
|
|
600
|
-
add: (row) => {
|
|
601
|
-
const v = numericValue(read(row));
|
|
602
|
-
if (v !== void 0) {
|
|
603
|
-
sum += v;
|
|
604
|
-
n++;
|
|
605
|
-
}
|
|
606
|
-
},
|
|
607
|
-
result: () => n === 0 ? null : avg ? sum / n : sum
|
|
608
|
-
};
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
case "countDistinct": return () => {
|
|
612
|
-
const plain = /* @__PURE__ */ new Set();
|
|
613
|
-
const tokens = /* @__PURE__ */ new Set();
|
|
678
|
+
if (expr.$fn === "count") {
|
|
679
|
+
const all = field === "*";
|
|
680
|
+
return () => {
|
|
681
|
+
let n = 0;
|
|
614
682
|
return {
|
|
615
683
|
add: (row) => {
|
|
616
|
-
|
|
617
|
-
if (v == null) return;
|
|
618
|
-
if (typeof v === "string" || typeof v === "boolean") plain.add(v);
|
|
619
|
-
else tokens.add(identityToken(v));
|
|
684
|
+
if (all || read(row) != null) n++;
|
|
620
685
|
},
|
|
621
|
-
result: () =>
|
|
686
|
+
result: () => n
|
|
622
687
|
};
|
|
623
688
|
};
|
|
624
|
-
case "min":
|
|
625
|
-
case "max": {
|
|
626
|
-
const sign = expr.$fn === "min" ? -1 : 1;
|
|
627
|
-
return () => {
|
|
628
|
-
let best = null;
|
|
629
|
-
return {
|
|
630
|
-
add: (row) => {
|
|
631
|
-
const v = read(row);
|
|
632
|
-
if (v == null) return;
|
|
633
|
-
if (best === null || compareLeaves(v, best) * sign > 0) best = v;
|
|
634
|
-
},
|
|
635
|
-
result: () => best
|
|
636
|
-
};
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
default: return expr.$fn;
|
|
640
689
|
}
|
|
690
|
+
if (expr.$fn === "countDistinct") return () => {
|
|
691
|
+
const plain = /* @__PURE__ */ new Set();
|
|
692
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
693
|
+
return {
|
|
694
|
+
add: (row) => {
|
|
695
|
+
const v = read(row);
|
|
696
|
+
if (v == null) return;
|
|
697
|
+
if (typeof v === "string" || typeof v === "boolean") plain.add(v);
|
|
698
|
+
else tokens.add(identityToken(v));
|
|
699
|
+
},
|
|
700
|
+
result: () => plain.size + tokens.size
|
|
701
|
+
};
|
|
702
|
+
};
|
|
703
|
+
return fnAccumulator(expr.$fn, read);
|
|
641
704
|
}
|
|
642
705
|
//#endregion
|
|
643
706
|
//#region src/lib/memory-adapter.ts
|
|
@@ -1059,6 +1122,22 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1059
1122
|
insertedIds
|
|
1060
1123
|
};
|
|
1061
1124
|
}
|
|
1125
|
+
supportsInsertIgnore() {
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
1128
|
+
/** A row colliding on the primary key or a unique index (`CONFLICT`) becomes a skipped slot. */
|
|
1129
|
+
async insertManyIgnore(data) {
|
|
1130
|
+
this._assertWritable();
|
|
1131
|
+
const state = this._state();
|
|
1132
|
+
return data.map((item) => {
|
|
1133
|
+
try {
|
|
1134
|
+
return { insertedId: this._insertRow(state, item) };
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
if (isConflict(error)) return null;
|
|
1137
|
+
throw error;
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1062
1141
|
/**
|
|
1063
1142
|
* Selects the stored rows a write should touch, as `{ key, row }` pairs so
|
|
1064
1143
|
* callers can mutate in place and re-key. Stored mode scans the table's Map
|
|
@@ -1308,10 +1387,14 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1308
1387
|
calendarBucketUnits() {
|
|
1309
1388
|
return ALL_BUCKET_UNITS;
|
|
1310
1389
|
}
|
|
1311
|
-
/** Every aggregate function
|
|
1390
|
+
/** Every aggregate function: `countDistinct`, `first` and `last` included. */
|
|
1312
1391
|
aggregateFns() {
|
|
1313
1392
|
return ALL_AGGREGATE_FNS;
|
|
1314
1393
|
}
|
|
1394
|
+
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
1395
|
+
supportsAggregateExpressions() {
|
|
1396
|
+
return true;
|
|
1397
|
+
}
|
|
1315
1398
|
/** Managed views render nothing (non-goal) — both are accepted like `aggregateFns()`. */
|
|
1316
1399
|
viewCapabilities() {
|
|
1317
1400
|
return ALL_VIEW_CAPABILITIES;
|
|
@@ -1434,6 +1517,10 @@ function setMemoryProvider(space, type, fn) {
|
|
|
1434
1517
|
if (!(adapter instanceof MemoryAdapter)) throw new Error("setMemoryProvider: table is not backed by MemoryAdapter");
|
|
1435
1518
|
adapter.setProvider(fn);
|
|
1436
1519
|
}
|
|
1520
|
+
/** @internal Drops the in-memory database of a closed space (called from `createAdapter`'s `onClose`). */
|
|
1521
|
+
function clearMemoryDatabase(space) {
|
|
1522
|
+
databases.get(space)?.clear();
|
|
1523
|
+
}
|
|
1437
1524
|
//#endregion
|
|
1438
1525
|
//#region src/lib/index.ts
|
|
1439
1526
|
/**
|
|
@@ -1446,7 +1533,8 @@ function setMemoryProvider(space, type, fn) {
|
|
|
1446
1533
|
* adapter has been built.
|
|
1447
1534
|
*/
|
|
1448
1535
|
function createAdapter() {
|
|
1449
|
-
|
|
1536
|
+
const space = new DbSpace(() => new MemoryAdapter(), { onClose: () => clearMemoryDatabase(space) });
|
|
1537
|
+
return space;
|
|
1450
1538
|
}
|
|
1451
1539
|
//#endregion
|
|
1452
1540
|
export { MemoryAdapter, buildMemoryPredicate, createAdapter, projectRow, setMemoryProvider, sortRows };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.148",
|
|
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.100",
|
|
40
|
+
"@atscript/typescript": "^0.1.100",
|
|
41
|
+
"@uniqu/core": "^0.1.13",
|
|
42
|
+
"unplugin-atscript": "^0.1.100"
|
|
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.100",
|
|
46
|
+
"@atscript/typescript": "^0.1.100",
|
|
47
|
+
"@uniqu/core": "^0.1.13",
|
|
48
|
+
"@atscript/db": "^0.1.148"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"postinstall": "asc -f dts",
|