@atscript/db-memory 0.1.135 → 0.1.137

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 CHANGED
@@ -348,7 +348,8 @@ function projectRow(row, projection, opts) {
348
348
  * - null and missing form ONE group; a calendar-bucket key is the
349
349
  * `@uniqu/core` kernel label (the SQLite UDF runs the same kernel);
350
350
  * - `sum` / `avg` over no numeric value are `null`; `min` / `max` order with
351
- * the `$sort` comparator; any other `$fn` is `INVALID_QUERY`;
351
+ * the `$sort` comparator; `countDistinct` counts distinct non-null values
352
+ * by group identity (below); any other `$fn` is `INVALID_QUERY`;
352
353
  * - group identity is type-tagged: `1` and `"1"` never share a group (a
353
354
  * number and a bigint of one value do), `Date`s group by instant, JSON
354
355
  * values by a key-order-independent serialization;
@@ -504,6 +505,19 @@ function accumulatorFactory(expr) {
504
505
  };
505
506
  };
506
507
  }
508
+ case "countDistinct": return () => {
509
+ const plain = /* @__PURE__ */ new Set();
510
+ const tokens = /* @__PURE__ */ new Set();
511
+ return {
512
+ add: (row) => {
513
+ const v = read(row);
514
+ if (v == null) return;
515
+ if (typeof v === "string" || typeof v === "boolean") plain.add(v);
516
+ else tokens.add(identityToken(v));
517
+ },
518
+ result: () => plain.size + tokens.size
519
+ };
520
+ };
507
521
  case "min":
508
522
  case "max": {
509
523
  const sign = expr.$fn === "min" ? -1 : 1;
@@ -525,12 +539,19 @@ function accumulatorFactory(expr) {
525
539
  //#endregion
526
540
  //#region src/lib/memory-adapter.ts
527
541
  /**
542
+ * One database per {@link DbSpace}, shared by every adapter the space builds —
543
+ * the administrative adapter schema sync drops tables through included. A
544
+ * space is the lifetime of an in-memory database: a fresh space starts empty.
545
+ */
546
+ const databases = /* @__PURE__ */ new WeakMap();
547
+ /**
528
548
  * In-memory {@link BaseDbAdapter} implementation.
529
549
  *
530
550
  * Runs in one of two modes:
531
- * - STORED mode (default): storage is a plain `Map` living on the adapter
532
- * instance — adapter instances are 1:1 with a readable (table/view), so the
533
- * Map is this table's whole store. Documents are kept in their nested PHYSICAL
551
+ * - STORED mode (default): rows live in a `Map` of the space's in-memory
552
+ * database, keyed by table name (see {@link databases}), so every adapter of
553
+ * the space that serves a name sees the same table, and the administrative
554
+ * adapter can drop it by name. Documents are kept in their nested PHYSICAL
534
555
  * shape (no flattening), which is why {@link supportsNestedObjects} is `true`.
535
556
  * Full CRUD surface: inserts, reads, update / replace / delete with
536
557
  * optimistic-concurrency CAS.
@@ -542,12 +563,10 @@ function accumulatorFactory(expr) {
542
563
  */
543
564
  var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
544
565
  /**
545
- * The table's store, keyed by {@link pkKey}. Values are the stored rows in
546
- * nested physical shape. An instance field — no `ensureTable` DDL needed.
566
+ * The in-memory database: the space's (see {@link databases}), resolved once
567
+ * in {@link registerSpace}; an adapter used without a space keeps this own one.
547
568
  */
548
- rows = /* @__PURE__ */ new Map();
549
- /** Unique indexes recorded by {@link syncIndexes}. Enforced on insert. */
550
- uniqueIndexes = [];
569
+ _db = /* @__PURE__ */ new Map();
551
570
  /** Memoized physical PK field names — stable for the adapter's lifetime. */
552
571
  _pkFieldsCache;
553
572
  /**
@@ -557,14 +576,6 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
557
576
  */
558
577
  _incrementFieldsCache;
559
578
  /**
560
- * Running per-field auto-increment counters (PHYSICAL name → last value
561
- * handed out). Lives on the adapter INSTANCE, so it resets whenever a new
562
- * DbSpace/adapter is built — correct for an in-memory store (parity with
563
- * SQLite `:memory:`, whose sequence also restarts with a fresh DB). Never
564
- * persisted.
565
- */
566
- _incrementCounters = /* @__PURE__ */ new Map();
567
- /**
568
579
  * Provider (read-through) backing closure. When set, this adapter is
569
580
  * PROVIDER-BACKED: reads recompute rows from this closure per request and all
570
581
  * writes are rejected (see {@link _assertWritable}). `undefined` ⇒ stored mode.
@@ -626,6 +637,43 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
626
637
  }
627
638
  return String(id);
628
639
  }
640
+ /** Joins the space's in-memory database (see {@link databases}), creating it on first use. */
641
+ registerSpace(space) {
642
+ let db = databases.get(space);
643
+ if (!db) {
644
+ db = /* @__PURE__ */ new Map();
645
+ databases.set(space, db);
646
+ }
647
+ this._db = db;
648
+ }
649
+ /**
650
+ * This table's state, or `undefined` while it does not exist (never synced,
651
+ * or dropped) — or when no readable is registered.
652
+ */
653
+ _peekState() {
654
+ const table = this._table;
655
+ return table && this._db.get(table.tableName);
656
+ }
657
+ /**
658
+ * This table's state, created on first use — by `ensureTable`, or by a write
659
+ * to a table that was never synced (implicit create, like a MongoDB
660
+ * collection). Reads never create it.
661
+ */
662
+ _state() {
663
+ const db = this._db;
664
+ const name = this._table.tableName;
665
+ let state = db.get(name);
666
+ if (!state) {
667
+ state = {
668
+ kind: this._table.isView ? "view" : "table",
669
+ rows: /* @__PURE__ */ new Map(),
670
+ uniqueIndexes: [],
671
+ incrementCounters: /* @__PURE__ */ new Map()
672
+ };
673
+ db.set(name, state);
674
+ }
675
+ return state;
676
+ }
629
677
  /**
630
678
  * Physical names of the primary-key field(s). Single `@meta.id` resolves via
631
679
  * {@link AtscriptDbReadable.metaIdPhysical}; a composite key maps each logical
@@ -674,7 +722,9 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
674
722
  * are computed so an increment PRIMARY KEY produces a real `insertedId` and
675
723
  * stores under a real key. The memory analogue of the Mongo adapter's
676
724
  * insert-time increment (Mongo uses an atomic `__atscript_counters`
677
- * collection; an in-memory store just keeps the counter on the instance):
725
+ * collection; an in-memory store keeps the counter in the table's state, so
726
+ * it restarts with a fresh space or when the table is dropped — parity with
727
+ * SQLite `:memory:`; never persisted):
678
728
  *
679
729
  * - No value for the field → assign the next counter value. First use starts
680
730
  * at `start ?? 1` (`max(counter, (start ?? 1) - 1) + 1`); thereafter it is
@@ -684,18 +734,18 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
684
734
  * - Explicit value present → keep it, but advance the counter to
685
735
  * `max(counter, value)` so a later auto value can never collide with it.
686
736
  */
687
- _applyIncrements(row) {
737
+ _applyIncrements(counters, row) {
688
738
  const fields = this._incrementFields();
689
739
  if (fields.size === 0) return;
690
740
  for (const [physical, start] of fields) {
691
741
  const floor = (start ?? 1) - 1;
692
- const base = this._incrementCounters.get(physical) ?? floor;
742
+ const base = counters.get(physical) ?? floor;
693
743
  const current = row[physical];
694
744
  if (current === void 0 || current === null) {
695
745
  const next = base + 1;
696
746
  row[physical] = next;
697
- this._incrementCounters.set(physical, next);
698
- } else if (typeof current === "number") this._incrementCounters.set(physical, Math.max(base, current));
747
+ counters.set(physical, next);
748
+ } else if (typeof current === "number") counters.set(physical, Math.max(base, current));
699
749
  }
700
750
  }
701
751
  /**
@@ -771,7 +821,8 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
771
821
  /**
772
822
  * Snapshot seam for reads. Returns the current rows.
773
823
  *
774
- * - Stored mode reads the instance Map directly (insertion order preserved).
824
+ * - Stored mode reads the table's Map directly (insertion order preserved);
825
+ * a table that does not exist reads as empty.
775
826
  * - Provider (read-through) mode calls {@link _provider} to recompute a fresh
776
827
  * snapshot per read.
777
828
  *
@@ -788,7 +839,7 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
788
839
  */
789
840
  _loadRows() {
790
841
  if (this._provider) return this._provider();
791
- return [...this.rows.values()];
842
+ return [...this._peekState()?.rows.values() ?? []];
792
843
  }
793
844
  /**
794
845
  * Clones the payload in, applies the version default, generates any
@@ -798,15 +849,15 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
798
849
  * the store. Called once per item by both `insertOne` and `insertMany`, so
799
850
  * increment values advance sequentially across a batch.
800
851
  */
801
- _insertRow(data) {
852
+ _insertRow(state, data) {
802
853
  const row = structuredClone(data);
803
854
  const versionColumn = this._table.versionColumn;
804
855
  if (versionColumn !== void 0 && !(versionColumn in row)) row[versionColumn] = 0;
805
- this._applyIncrements(row);
856
+ this._applyIncrements(state.incrementCounters, row);
806
857
  const key = this.pkKey(row);
807
- if (this.rows.has(key)) throw this._pkConflict();
808
- this._enforceUniqueIndexes(row);
809
- this.rows.set(key, row);
858
+ if (state.rows.has(key)) throw this._pkConflict();
859
+ this._enforceUniqueIndexes(state, row);
860
+ state.rows.set(key, row);
810
861
  const fallback = this._table.metaIdPhysical ? void 0 : this._compositeInsertedId(row);
811
862
  return this._resolveInsertedId(row, fallback);
812
863
  }
@@ -819,8 +870,8 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
819
870
  * `excludeKey` (when given) skips the row stored under that {@link pkKey} — so
820
871
  * a row updating its own unique value does not false-conflict with itself.
821
872
  */
822
- _enforceUniqueIndexes(row, excludeKey) {
823
- for (const index of this.uniqueIndexes) {
873
+ _enforceUniqueIndexes(state, row, excludeKey) {
874
+ for (const index of state.uniqueIndexes) {
824
875
  const tuple = [];
825
876
  let skip = false;
826
877
  for (const field of index.fields) {
@@ -832,7 +883,7 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
832
883
  tuple.push(value);
833
884
  }
834
885
  if (skip) continue;
835
- for (const [existingKey, existing] of this.rows) {
886
+ for (const [existingKey, existing] of state.rows) {
836
887
  if (excludeKey !== void 0 && existingKey === excludeKey) continue;
837
888
  let equal = true;
838
889
  for (let i = 0; i < index.fields.length; i++) if (!valuesEqual(getPath(existing, index.fields[i]), tuple[i])) {
@@ -848,11 +899,12 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
848
899
  }
849
900
  async insertOne(data) {
850
901
  this._assertWritable();
851
- return { insertedId: this._insertRow(data) };
902
+ return { insertedId: this._insertRow(this._state(), data) };
852
903
  }
853
904
  async insertMany(data) {
854
905
  this._assertWritable();
855
- const insertedIds = data.map((item) => this._insertRow(item));
906
+ const state = this._state();
907
+ const insertedIds = data.map((item) => this._insertRow(state, item));
856
908
  return {
857
909
  insertedCount: data.length,
858
910
  insertedIds
@@ -860,7 +912,7 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
860
912
  }
861
913
  /**
862
914
  * Selects the stored rows a write should touch, as `{ key, row }` pairs so
863
- * callers can mutate in place and re-key. Stored mode scans the instance Map
915
+ * callers can mutate in place and re-key. Stored mode scans the table's Map
864
916
  * directly (writes are authoritative against the store, unlike reads which go
865
917
  * through the {@link _loadRows} snapshot seam).
866
918
  *
@@ -871,14 +923,15 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
871
923
  * that has no version column is a misconfiguration and throws (mirrors the
872
924
  * Mongo adapter's `_buildCasFilter`).
873
925
  *
874
- * When `many` is `false` at most the first match is returned.
926
+ * When `many` is `false` at most the first match is returned. A table that
927
+ * does not exist (`state` undefined) matches nothing and is not created.
875
928
  */
876
- _selectForWrite(filter, expectedVersion, many) {
929
+ _selectForWrite(state, filter, expectedVersion, many) {
877
930
  const versionColumn = this._table.versionColumn;
878
931
  if (expectedVersion !== void 0 && versionColumn === void 0) throw new Error("expectedVersion requires a versioned table");
879
932
  const match = buildMemoryPredicate(filter);
880
933
  const matched = [];
881
- for (const [key, row] of this.rows) {
934
+ for (const [key, row] of state?.rows ?? []) {
882
935
  if (!match(row)) continue;
883
936
  if (expectedVersion !== void 0 && row[versionColumn] !== expectedVersion) continue;
884
937
  matched.push({
@@ -933,11 +986,11 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
933
986
  * NEW key (some other row already owns it) throws `CONFLICT`. Throws BEFORE
934
987
  * touching the Map so a failed re-key leaves the store unchanged.
935
988
  */
936
- _commitRow(oldKey, next) {
989
+ _commitRow(rows, oldKey, next) {
937
990
  const newKey = this.pkKey(next);
938
- if (newKey !== oldKey && this.rows.has(newKey)) throw this._pkConflict();
939
- if (newKey !== oldKey) this.rows.delete(oldKey);
940
- this.rows.set(newKey, next);
991
+ if (newKey !== oldKey && rows.has(newKey)) throw this._pkConflict();
992
+ if (newKey !== oldKey) rows.delete(oldKey);
993
+ rows.set(newKey, next);
941
994
  }
942
995
  /**
943
996
  * Update-then-commit for a single matched row: clones the pristine `row`,
@@ -947,25 +1000,26 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
947
1000
  * commits. Nothing is written to the store until both the unique and PK checks
948
1001
  * pass, so a conflict leaves the store untouched.
949
1002
  */
950
- _commitUpdate(oldKey, row, data, ops) {
1003
+ _commitUpdate(state, oldKey, row, data, ops) {
951
1004
  const next = structuredClone(row);
952
1005
  this._applyUpdate(next, data, ops);
953
1006
  this._bumpVersion(next, row);
954
- this._enforceUniqueIndexes(next, oldKey);
955
- this._commitRow(oldKey, next);
1007
+ this._enforceUniqueIndexes(state, next, oldKey);
1008
+ this._commitRow(state.rows, oldKey, next);
956
1009
  }
957
1010
  async replaceOne(filter, data, expectedVersion) {
958
1011
  this._assertWritable();
959
- const matched = this._selectForWrite(filter, expectedVersion, false);
960
- if (matched.length === 0) return {
1012
+ const state = this._peekState();
1013
+ const matched = this._selectForWrite(state, filter, expectedVersion, false);
1014
+ if (!state || matched.length === 0) return {
961
1015
  matchedCount: 0,
962
1016
  modifiedCount: 0
963
1017
  };
964
1018
  const { key, row } = matched[0];
965
1019
  const next = structuredClone(data);
966
1020
  this._bumpVersion(next, row);
967
- this._enforceUniqueIndexes(next, key);
968
- this._commitRow(key, next);
1021
+ this._enforceUniqueIndexes(state, next, key);
1022
+ this._commitRow(state.rows, key, next);
969
1023
  return {
970
1024
  matchedCount: 1,
971
1025
  modifiedCount: 1
@@ -973,13 +1027,14 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
973
1027
  }
974
1028
  async updateOne(filter, data, ops, expectedVersion) {
975
1029
  this._assertWritable();
976
- const matched = this._selectForWrite(filter, expectedVersion, false);
977
- if (matched.length === 0) return {
1030
+ const state = this._peekState();
1031
+ const matched = this._selectForWrite(state, filter, expectedVersion, false);
1032
+ if (!state || matched.length === 0) return {
978
1033
  matchedCount: 0,
979
1034
  modifiedCount: 0
980
1035
  };
981
1036
  const { key, row } = matched[0];
982
- this._commitUpdate(key, row, data, ops);
1037
+ this._commitUpdate(state, key, row, data, ops);
983
1038
  return {
984
1039
  matchedCount: 1,
985
1040
  modifiedCount: 1
@@ -987,9 +1042,10 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
987
1042
  }
988
1043
  async deleteOne(filter) {
989
1044
  this._assertWritable();
990
- const matched = this._selectForWrite(filter, void 0, false);
991
- if (matched.length === 0) return { deletedCount: 0 };
992
- this.rows.delete(matched[0].key);
1045
+ const state = this._peekState();
1046
+ const matched = this._selectForWrite(state, filter, void 0, false);
1047
+ if (!state || matched.length === 0) return { deletedCount: 0 };
1048
+ state.rows.delete(matched[0].key);
993
1049
  return { deletedCount: 1 };
994
1050
  }
995
1051
  /**
@@ -1093,10 +1149,19 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
1093
1149
  calendarBucketUnits() {
1094
1150
  return _atscript_db.ALL_BUCKET_UNITS;
1095
1151
  }
1152
+ /** Every aggregate function, `countDistinct` included. */
1153
+ aggregateFns() {
1154
+ return _atscript_db.ALL_AGGREGATE_FNS;
1155
+ }
1096
1156
  async updateMany(filter, data, ops) {
1097
1157
  this._assertWritable();
1098
- const matched = this._selectForWrite(filter, void 0, true);
1099
- for (const { key, row } of matched) this._commitUpdate(key, row, data, ops);
1158
+ const state = this._peekState();
1159
+ if (!state) return {
1160
+ matchedCount: 0,
1161
+ modifiedCount: 0
1162
+ };
1163
+ const matched = this._selectForWrite(state, filter, void 0, true);
1164
+ for (const { key, row } of matched) this._commitUpdate(state, key, row, data, ops);
1100
1165
  return {
1101
1166
  matchedCount: matched.length,
1102
1167
  modifiedCount: matched.length
@@ -1104,8 +1169,13 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
1104
1169
  }
1105
1170
  async replaceMany(filter, data) {
1106
1171
  this._assertWritable();
1107
- const matched = this._selectForWrite(filter, void 0, true);
1108
- for (const { key, row } of matched) this._commitUpdate(key, row, data);
1172
+ const state = this._peekState();
1173
+ if (!state) return {
1174
+ matchedCount: 0,
1175
+ modifiedCount: 0
1176
+ };
1177
+ const matched = this._selectForWrite(state, filter, void 0, true);
1178
+ for (const { key, row } of matched) this._commitUpdate(state, key, row, data);
1109
1179
  return {
1110
1180
  matchedCount: matched.length,
1111
1181
  modifiedCount: matched.length
@@ -1113,8 +1183,10 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
1113
1183
  }
1114
1184
  async deleteMany(filter) {
1115
1185
  this._assertWritable();
1116
- const matched = this._selectForWrite(filter, void 0, true);
1117
- for (const { key } of matched) this.rows.delete(key);
1186
+ const state = this._peekState();
1187
+ if (!state) return { deletedCount: 0 };
1188
+ const matched = this._selectForWrite(state, filter, void 0, true);
1189
+ for (const { key } of matched) state.rows.delete(key);
1118
1190
  return { deletedCount: matched.length };
1119
1191
  }
1120
1192
  /**
@@ -1123,21 +1195,54 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
1123
1195
  * in-memory scan needs no plain/fulltext/geo index to answer queries.
1124
1196
  */
1125
1197
  async syncIndexes() {
1126
- this.uniqueIndexes = [];
1198
+ const uniqueIndexes = [];
1127
1199
  for (const index of this._table.indexes.values()) {
1128
1200
  if (index.type !== "unique") continue;
1129
- this.uniqueIndexes.push({
1201
+ uniqueIndexes.push({
1130
1202
  name: index.name,
1131
1203
  fields: index.fields.map((f) => f.name),
1132
1204
  optionalFields: new Set(index.fields.filter((f) => f.optional).map((f) => f.name))
1133
1205
  });
1134
1206
  }
1207
+ this._state().uniqueIndexes = uniqueIndexes;
1208
+ }
1209
+ /** Creates the table's (empty) state when it does not exist. Idempotent. */
1210
+ async ensureTable() {
1211
+ this._state();
1135
1212
  }
1136
1213
  /**
1137
- * No-op: the store is the instance-level {@link rows} Map, which already
1138
- * exists. Safe to call repeatedly.
1214
+ * Whether the table exists in the space's in-memory database — created by
1215
+ * {@link ensureTable} (or a first write) and not dropped since. Schema sync
1216
+ * reports a table that does not exist as `create`. An external view always
1217
+ * exists: nothing in memory creates one, its rows come from a provider.
1218
+ * @since 0.1.137
1139
1219
  */
1140
- async ensureTable() {}
1220
+ async tableExists() {
1221
+ if ((0, _atscript_db.isAtscriptDbView)(this._table) && this._table.isExternal) return true;
1222
+ return this._peekState() !== void 0;
1223
+ }
1224
+ /**
1225
+ * Drops a table by name: its rows, unique indexes and increment counters go,
1226
+ * so the table added back starts empty. A missing table is not an error;
1227
+ * a view under that name is (`dropViewByName` drops views).
1228
+ * @since 0.1.137
1229
+ */
1230
+ async dropTableByName(tableName) {
1231
+ this._dropByName(tableName, "table");
1232
+ }
1233
+ /**
1234
+ * Drops a view by name — its (empty) state; a view holds no rows here.
1235
+ * A missing view is not an error; a table under that name is.
1236
+ * @since 0.1.137
1237
+ */
1238
+ async dropViewByName(viewName) {
1239
+ this._dropByName(viewName, "view");
1240
+ }
1241
+ _dropByName(name, kind) {
1242
+ const state = this._db.get(name);
1243
+ if (state && state.kind !== kind) throw new Error(`Cannot drop ${kind} "${name}": it is a ${state.kind}`);
1244
+ this._db.delete(name);
1245
+ }
1141
1246
  };
1142
1247
  /**
1143
1248
  * Ergonomic late-binding entry point for provider (read-through) mode. Resolves
@@ -1159,7 +1264,8 @@ function setMemoryProvider(space, type, fn) {
1159
1264
  /**
1160
1265
  * Creates a {@link DbSpace} backed by an in-memory {@link MemoryAdapter}.
1161
1266
  *
1162
- * Tables default to STORED mode (an instance-level `Map`). To make one table
1267
+ * Tables default to STORED mode (a `Map` per table in the space's in-memory
1268
+ * database, shared by every adapter of the space). To make one table
1163
1269
  * read-only and read-through from a runtime closure, call `setMemoryProvider`
1164
1270
  * (re-exported from {@link ./memory-adapter}) on the space AFTER the table's
1165
1271
  * adapter has been built.
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbUpdateResult, TFieldOps } from "@atscript/db";
1
+ import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbUpdateResult, TFieldOps } from "@atscript/db";
2
2
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
3
3
 
4
4
  //#region src/lib/memory-adapter.d.ts
@@ -12,9 +12,10 @@ type MemoryProviderFn = () => Array<Record<string, unknown>> | Promise<Array<Rec
12
12
  * In-memory {@link BaseDbAdapter} implementation.
13
13
  *
14
14
  * Runs in one of two modes:
15
- * - STORED mode (default): storage is a plain `Map` living on the adapter
16
- * instance — adapter instances are 1:1 with a readable (table/view), so the
17
- * Map is this table's whole store. Documents are kept in their nested PHYSICAL
15
+ * - STORED mode (default): rows live in a `Map` of the space's in-memory
16
+ * database, keyed by table name (see {@link databases}), so every adapter of
17
+ * the space that serves a name sees the same table, and the administrative
18
+ * adapter can drop it by name. Documents are kept in their nested PHYSICAL
18
19
  * shape (no flattening), which is why {@link supportsNestedObjects} is `true`.
19
20
  * Full CRUD surface: inserts, reads, update / replace / delete with
20
21
  * optimistic-concurrency CAS.
@@ -26,12 +27,10 @@ type MemoryProviderFn = () => Array<Record<string, unknown>> | Promise<Array<Rec
26
27
  */
27
28
  declare class MemoryAdapter extends BaseDbAdapter {
28
29
  /**
29
- * The table's store, keyed by {@link pkKey}. Values are the stored rows in
30
- * nested physical shape. An instance field — no `ensureTable` DDL needed.
30
+ * The in-memory database: the space's (see {@link databases}), resolved once
31
+ * in {@link registerSpace}; an adapter used without a space keeps this own one.
31
32
  */
32
- private rows;
33
- /** Unique indexes recorded by {@link syncIndexes}. Enforced on insert. */
34
- private uniqueIndexes;
33
+ private _db;
35
34
  /** Memoized physical PK field names — stable for the adapter's lifetime. */
36
35
  private _pkFieldsCache?;
37
36
  /**
@@ -40,14 +39,6 @@ declare class MemoryAdapter extends BaseDbAdapter {
40
39
  * {@link _incrementFields}).
41
40
  */
42
41
  private _incrementFieldsCache?;
43
- /**
44
- * Running per-field auto-increment counters (PHYSICAL name → last value
45
- * handed out). Lives on the adapter INSTANCE, so it resets whenever a new
46
- * DbSpace/adapter is built — correct for an in-memory store (parity with
47
- * SQLite `:memory:`, whose sequence also restarts with a fresh DB). Never
48
- * persisted.
49
- */
50
- private _incrementCounters;
51
42
  /**
52
43
  * Provider (read-through) backing closure. When set, this adapter is
53
44
  * PROVIDER-BACKED: reads recompute rows from this closure per request and all
@@ -100,6 +91,19 @@ declare class MemoryAdapter extends BaseDbAdapter {
100
91
  * coerces via `Number(id)`, everything else via `String(id)`.
101
92
  */
102
93
  prepareId(id: unknown, _fieldType: unknown): unknown;
94
+ /** Joins the space's in-memory database (see {@link databases}), creating it on first use. */
95
+ registerSpace(space: DbSpace): void;
96
+ /**
97
+ * This table's state, or `undefined` while it does not exist (never synced,
98
+ * or dropped) — or when no readable is registered.
99
+ */
100
+ private _peekState;
101
+ /**
102
+ * This table's state, created on first use — by `ensureTable`, or by a write
103
+ * to a table that was never synced (implicit create, like a MongoDB
104
+ * collection). Reads never create it.
105
+ */
106
+ private _state;
103
107
  /**
104
108
  * Physical names of the primary-key field(s). Single `@meta.id` resolves via
105
109
  * {@link AtscriptDbReadable.metaIdPhysical}; a composite key maps each logical
@@ -133,7 +137,9 @@ declare class MemoryAdapter extends BaseDbAdapter {
133
137
  * are computed so an increment PRIMARY KEY produces a real `insertedId` and
134
138
  * stores under a real key. The memory analogue of the Mongo adapter's
135
139
  * insert-time increment (Mongo uses an atomic `__atscript_counters`
136
- * collection; an in-memory store just keeps the counter on the instance):
140
+ * collection; an in-memory store keeps the counter in the table's state, so
141
+ * it restarts with a fresh space or when the table is dropped — parity with
142
+ * SQLite `:memory:`; never persisted):
137
143
  *
138
144
  * - No value for the field → assign the next counter value. First use starts
139
145
  * at `start ?? 1` (`max(counter, (start ?? 1) - 1) + 1`); thereafter it is
@@ -198,7 +204,8 @@ declare class MemoryAdapter extends BaseDbAdapter {
198
204
  /**
199
205
  * Snapshot seam for reads. Returns the current rows.
200
206
  *
201
- * - Stored mode reads the instance Map directly (insertion order preserved).
207
+ * - Stored mode reads the table's Map directly (insertion order preserved);
208
+ * a table that does not exist reads as empty.
202
209
  * - Provider (read-through) mode calls {@link _provider} to recompute a fresh
203
210
  * snapshot per read.
204
211
  *
@@ -237,7 +244,7 @@ declare class MemoryAdapter extends BaseDbAdapter {
237
244
  insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
238
245
  /**
239
246
  * Selects the stored rows a write should touch, as `{ key, row }` pairs so
240
- * callers can mutate in place and re-key. Stored mode scans the instance Map
247
+ * callers can mutate in place and re-key. Stored mode scans the table's Map
241
248
  * directly (writes are authoritative against the store, unlike reads which go
242
249
  * through the {@link _loadRows} snapshot seam).
243
250
  *
@@ -248,7 +255,8 @@ declare class MemoryAdapter extends BaseDbAdapter {
248
255
  * that has no version column is a misconfiguration and throws (mirrors the
249
256
  * Mongo adapter's `_buildCasFilter`).
250
257
  *
251
- * When `many` is `false` at most the first match is returned.
258
+ * When `many` is `false` at most the first match is returned. A table that
259
+ * does not exist (`state` undefined) matches nothing and is not created.
252
260
  */
253
261
  private _selectForWrite;
254
262
  /**
@@ -367,6 +375,8 @@ declare class MemoryAdapter extends BaseDbAdapter {
367
375
  * there is no zone this adapter cannot resolve).
368
376
  */
369
377
  calendarBucketUnits(): ReadonlySet<BucketUnit>;
378
+ /** Every aggregate function, `countDistinct` included. */
379
+ aggregateFns(): ReadonlySet<AggregateFn>;
370
380
  updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
371
381
  replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
372
382
  deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
@@ -376,11 +386,30 @@ declare class MemoryAdapter extends BaseDbAdapter {
376
386
  * in-memory scan needs no plain/fulltext/geo index to answer queries.
377
387
  */
378
388
  syncIndexes(): Promise<void>;
389
+ /** Creates the table's (empty) state when it does not exist. Idempotent. */
390
+ ensureTable(): Promise<void>;
379
391
  /**
380
- * No-op: the store is the instance-level {@link rows} Map, which already
381
- * exists. Safe to call repeatedly.
392
+ * Whether the table exists in the space's in-memory database — created by
393
+ * {@link ensureTable} (or a first write) and not dropped since. Schema sync
394
+ * reports a table that does not exist as `create`. An external view always
395
+ * exists: nothing in memory creates one, its rows come from a provider.
396
+ * @since 0.1.137
382
397
  */
383
- ensureTable(): Promise<void>;
398
+ tableExists(): Promise<boolean>;
399
+ /**
400
+ * Drops a table by name: its rows, unique indexes and increment counters go,
401
+ * so the table added back starts empty. A missing table is not an error;
402
+ * a view under that name is (`dropViewByName` drops views).
403
+ * @since 0.1.137
404
+ */
405
+ dropTableByName(tableName: string): Promise<void>;
406
+ /**
407
+ * Drops a view by name — its (empty) state; a view holds no rows here.
408
+ * A missing view is not an error; a table under that name is.
409
+ * @since 0.1.137
410
+ */
411
+ dropViewByName(viewName: string): Promise<void>;
412
+ private _dropByName;
384
413
  }
385
414
  /**
386
415
  * Ergonomic late-binding entry point for provider (read-through) mode. Resolves
@@ -480,7 +509,8 @@ declare function projectRow(row: Record<string, unknown>, projection?: Record<st
480
509
  /**
481
510
  * Creates a {@link DbSpace} backed by an in-memory {@link MemoryAdapter}.
482
511
  *
483
- * Tables default to STORED mode (an instance-level `Map`). To make one table
512
+ * Tables default to STORED mode (a `Map` per table in the space's in-memory
513
+ * database, shared by every adapter of the space). To make one table
484
514
  * read-only and read-through from a runtime closure, call `setMemoryProvider`
485
515
  * (re-exported from {@link ./memory-adapter}) on the space AFTER the table's
486
516
  * adapter has been built.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbUpdateResult, TFieldOps } from "@atscript/db";
1
+ import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbUpdateResult, TFieldOps } from "@atscript/db";
2
2
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
3
3
 
4
4
  //#region src/lib/memory-adapter.d.ts
@@ -12,9 +12,10 @@ type MemoryProviderFn = () => Array<Record<string, unknown>> | Promise<Array<Rec
12
12
  * In-memory {@link BaseDbAdapter} implementation.
13
13
  *
14
14
  * Runs in one of two modes:
15
- * - STORED mode (default): storage is a plain `Map` living on the adapter
16
- * instance — adapter instances are 1:1 with a readable (table/view), so the
17
- * Map is this table's whole store. Documents are kept in their nested PHYSICAL
15
+ * - STORED mode (default): rows live in a `Map` of the space's in-memory
16
+ * database, keyed by table name (see {@link databases}), so every adapter of
17
+ * the space that serves a name sees the same table, and the administrative
18
+ * adapter can drop it by name. Documents are kept in their nested PHYSICAL
18
19
  * shape (no flattening), which is why {@link supportsNestedObjects} is `true`.
19
20
  * Full CRUD surface: inserts, reads, update / replace / delete with
20
21
  * optimistic-concurrency CAS.
@@ -26,12 +27,10 @@ type MemoryProviderFn = () => Array<Record<string, unknown>> | Promise<Array<Rec
26
27
  */
27
28
  declare class MemoryAdapter extends BaseDbAdapter {
28
29
  /**
29
- * The table's store, keyed by {@link pkKey}. Values are the stored rows in
30
- * nested physical shape. An instance field — no `ensureTable` DDL needed.
30
+ * The in-memory database: the space's (see {@link databases}), resolved once
31
+ * in {@link registerSpace}; an adapter used without a space keeps this own one.
31
32
  */
32
- private rows;
33
- /** Unique indexes recorded by {@link syncIndexes}. Enforced on insert. */
34
- private uniqueIndexes;
33
+ private _db;
35
34
  /** Memoized physical PK field names — stable for the adapter's lifetime. */
36
35
  private _pkFieldsCache?;
37
36
  /**
@@ -40,14 +39,6 @@ declare class MemoryAdapter extends BaseDbAdapter {
40
39
  * {@link _incrementFields}).
41
40
  */
42
41
  private _incrementFieldsCache?;
43
- /**
44
- * Running per-field auto-increment counters (PHYSICAL name → last value
45
- * handed out). Lives on the adapter INSTANCE, so it resets whenever a new
46
- * DbSpace/adapter is built — correct for an in-memory store (parity with
47
- * SQLite `:memory:`, whose sequence also restarts with a fresh DB). Never
48
- * persisted.
49
- */
50
- private _incrementCounters;
51
42
  /**
52
43
  * Provider (read-through) backing closure. When set, this adapter is
53
44
  * PROVIDER-BACKED: reads recompute rows from this closure per request and all
@@ -100,6 +91,19 @@ declare class MemoryAdapter extends BaseDbAdapter {
100
91
  * coerces via `Number(id)`, everything else via `String(id)`.
101
92
  */
102
93
  prepareId(id: unknown, _fieldType: unknown): unknown;
94
+ /** Joins the space's in-memory database (see {@link databases}), creating it on first use. */
95
+ registerSpace(space: DbSpace): void;
96
+ /**
97
+ * This table's state, or `undefined` while it does not exist (never synced,
98
+ * or dropped) — or when no readable is registered.
99
+ */
100
+ private _peekState;
101
+ /**
102
+ * This table's state, created on first use — by `ensureTable`, or by a write
103
+ * to a table that was never synced (implicit create, like a MongoDB
104
+ * collection). Reads never create it.
105
+ */
106
+ private _state;
103
107
  /**
104
108
  * Physical names of the primary-key field(s). Single `@meta.id` resolves via
105
109
  * {@link AtscriptDbReadable.metaIdPhysical}; a composite key maps each logical
@@ -133,7 +137,9 @@ declare class MemoryAdapter extends BaseDbAdapter {
133
137
  * are computed so an increment PRIMARY KEY produces a real `insertedId` and
134
138
  * stores under a real key. The memory analogue of the Mongo adapter's
135
139
  * insert-time increment (Mongo uses an atomic `__atscript_counters`
136
- * collection; an in-memory store just keeps the counter on the instance):
140
+ * collection; an in-memory store keeps the counter in the table's state, so
141
+ * it restarts with a fresh space or when the table is dropped — parity with
142
+ * SQLite `:memory:`; never persisted):
137
143
  *
138
144
  * - No value for the field → assign the next counter value. First use starts
139
145
  * at `start ?? 1` (`max(counter, (start ?? 1) - 1) + 1`); thereafter it is
@@ -198,7 +204,8 @@ declare class MemoryAdapter extends BaseDbAdapter {
198
204
  /**
199
205
  * Snapshot seam for reads. Returns the current rows.
200
206
  *
201
- * - Stored mode reads the instance Map directly (insertion order preserved).
207
+ * - Stored mode reads the table's Map directly (insertion order preserved);
208
+ * a table that does not exist reads as empty.
202
209
  * - Provider (read-through) mode calls {@link _provider} to recompute a fresh
203
210
  * snapshot per read.
204
211
  *
@@ -237,7 +244,7 @@ declare class MemoryAdapter extends BaseDbAdapter {
237
244
  insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
238
245
  /**
239
246
  * Selects the stored rows a write should touch, as `{ key, row }` pairs so
240
- * callers can mutate in place and re-key. Stored mode scans the instance Map
247
+ * callers can mutate in place and re-key. Stored mode scans the table's Map
241
248
  * directly (writes are authoritative against the store, unlike reads which go
242
249
  * through the {@link _loadRows} snapshot seam).
243
250
  *
@@ -248,7 +255,8 @@ declare class MemoryAdapter extends BaseDbAdapter {
248
255
  * that has no version column is a misconfiguration and throws (mirrors the
249
256
  * Mongo adapter's `_buildCasFilter`).
250
257
  *
251
- * When `many` is `false` at most the first match is returned.
258
+ * When `many` is `false` at most the first match is returned. A table that
259
+ * does not exist (`state` undefined) matches nothing and is not created.
252
260
  */
253
261
  private _selectForWrite;
254
262
  /**
@@ -367,6 +375,8 @@ declare class MemoryAdapter extends BaseDbAdapter {
367
375
  * there is no zone this adapter cannot resolve).
368
376
  */
369
377
  calendarBucketUnits(): ReadonlySet<BucketUnit>;
378
+ /** Every aggregate function, `countDistinct` included. */
379
+ aggregateFns(): ReadonlySet<AggregateFn>;
370
380
  updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
371
381
  replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
372
382
  deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
@@ -376,11 +386,30 @@ declare class MemoryAdapter extends BaseDbAdapter {
376
386
  * in-memory scan needs no plain/fulltext/geo index to answer queries.
377
387
  */
378
388
  syncIndexes(): Promise<void>;
389
+ /** Creates the table's (empty) state when it does not exist. Idempotent. */
390
+ ensureTable(): Promise<void>;
379
391
  /**
380
- * No-op: the store is the instance-level {@link rows} Map, which already
381
- * exists. Safe to call repeatedly.
392
+ * Whether the table exists in the space's in-memory database — created by
393
+ * {@link ensureTable} (or a first write) and not dropped since. Schema sync
394
+ * reports a table that does not exist as `create`. An external view always
395
+ * exists: nothing in memory creates one, its rows come from a provider.
396
+ * @since 0.1.137
382
397
  */
383
- ensureTable(): Promise<void>;
398
+ tableExists(): Promise<boolean>;
399
+ /**
400
+ * Drops a table by name: its rows, unique indexes and increment counters go,
401
+ * so the table added back starts empty. A missing table is not an error;
402
+ * a view under that name is (`dropViewByName` drops views).
403
+ * @since 0.1.137
404
+ */
405
+ dropTableByName(tableName: string): Promise<void>;
406
+ /**
407
+ * Drops a view by name — its (empty) state; a view holds no rows here.
408
+ * A missing view is not an error; a table under that name is.
409
+ * @since 0.1.137
410
+ */
411
+ dropViewByName(viewName: string): Promise<void>;
412
+ private _dropByName;
384
413
  }
385
414
  /**
386
415
  * Ergonomic late-binding entry point for provider (read-through) mode. Resolves
@@ -480,7 +509,8 @@ declare function projectRow(row: Record<string, unknown>, projection?: Record<st
480
509
  /**
481
510
  * Creates a {@link DbSpace} backed by an in-memory {@link MemoryAdapter}.
482
511
  *
483
- * Tables default to STORED mode (an instance-level `Map`). To make one table
512
+ * Tables default to STORED mode (a `Map` per table in the space's in-memory
513
+ * database, shared by every adapter of the space). To make one table
484
514
  * read-only and read-through from a runtime closure, call `setMemoryProvider`
485
515
  * (re-exported from {@link ./memory-adapter}) on the space AFTER the table's
486
516
  * adapter has been built.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { ALL_BUCKET_UNITS, BaseDbAdapter, DbError, DbSpace, walkFilter } from "@atscript/db";
1
+ import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, BaseDbAdapter, DbError, DbSpace, isAtscriptDbView, 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
@@ -347,7 +347,8 @@ function projectRow(row, projection, opts) {
347
347
  * - null and missing form ONE group; a calendar-bucket key is the
348
348
  * `@uniqu/core` kernel label (the SQLite UDF runs the same kernel);
349
349
  * - `sum` / `avg` over no numeric value are `null`; `min` / `max` order with
350
- * the `$sort` comparator; any other `$fn` is `INVALID_QUERY`;
350
+ * the `$sort` comparator; `countDistinct` counts distinct non-null values
351
+ * by group identity (below); any other `$fn` is `INVALID_QUERY`;
351
352
  * - group identity is type-tagged: `1` and `"1"` never share a group (a
352
353
  * number and a bigint of one value do), `Date`s group by instant, JSON
353
354
  * values by a key-order-independent serialization;
@@ -503,6 +504,19 @@ function accumulatorFactory(expr) {
503
504
  };
504
505
  };
505
506
  }
507
+ case "countDistinct": return () => {
508
+ const plain = /* @__PURE__ */ new Set();
509
+ const tokens = /* @__PURE__ */ new Set();
510
+ return {
511
+ add: (row) => {
512
+ const v = read(row);
513
+ if (v == null) return;
514
+ if (typeof v === "string" || typeof v === "boolean") plain.add(v);
515
+ else tokens.add(identityToken(v));
516
+ },
517
+ result: () => plain.size + tokens.size
518
+ };
519
+ };
506
520
  case "min":
507
521
  case "max": {
508
522
  const sign = expr.$fn === "min" ? -1 : 1;
@@ -524,12 +538,19 @@ function accumulatorFactory(expr) {
524
538
  //#endregion
525
539
  //#region src/lib/memory-adapter.ts
526
540
  /**
541
+ * One database per {@link DbSpace}, shared by every adapter the space builds —
542
+ * the administrative adapter schema sync drops tables through included. A
543
+ * space is the lifetime of an in-memory database: a fresh space starts empty.
544
+ */
545
+ const databases = /* @__PURE__ */ new WeakMap();
546
+ /**
527
547
  * In-memory {@link BaseDbAdapter} implementation.
528
548
  *
529
549
  * Runs in one of two modes:
530
- * - STORED mode (default): storage is a plain `Map` living on the adapter
531
- * instance — adapter instances are 1:1 with a readable (table/view), so the
532
- * Map is this table's whole store. Documents are kept in their nested PHYSICAL
550
+ * - STORED mode (default): rows live in a `Map` of the space's in-memory
551
+ * database, keyed by table name (see {@link databases}), so every adapter of
552
+ * the space that serves a name sees the same table, and the administrative
553
+ * adapter can drop it by name. Documents are kept in their nested PHYSICAL
533
554
  * shape (no flattening), which is why {@link supportsNestedObjects} is `true`.
534
555
  * Full CRUD surface: inserts, reads, update / replace / delete with
535
556
  * optimistic-concurrency CAS.
@@ -541,12 +562,10 @@ function accumulatorFactory(expr) {
541
562
  */
542
563
  var MemoryAdapter = class extends BaseDbAdapter {
543
564
  /**
544
- * The table's store, keyed by {@link pkKey}. Values are the stored rows in
545
- * nested physical shape. An instance field — no `ensureTable` DDL needed.
565
+ * The in-memory database: the space's (see {@link databases}), resolved once
566
+ * in {@link registerSpace}; an adapter used without a space keeps this own one.
546
567
  */
547
- rows = /* @__PURE__ */ new Map();
548
- /** Unique indexes recorded by {@link syncIndexes}. Enforced on insert. */
549
- uniqueIndexes = [];
568
+ _db = /* @__PURE__ */ new Map();
550
569
  /** Memoized physical PK field names — stable for the adapter's lifetime. */
551
570
  _pkFieldsCache;
552
571
  /**
@@ -556,14 +575,6 @@ var MemoryAdapter = class extends BaseDbAdapter {
556
575
  */
557
576
  _incrementFieldsCache;
558
577
  /**
559
- * Running per-field auto-increment counters (PHYSICAL name → last value
560
- * handed out). Lives on the adapter INSTANCE, so it resets whenever a new
561
- * DbSpace/adapter is built — correct for an in-memory store (parity with
562
- * SQLite `:memory:`, whose sequence also restarts with a fresh DB). Never
563
- * persisted.
564
- */
565
- _incrementCounters = /* @__PURE__ */ new Map();
566
- /**
567
578
  * Provider (read-through) backing closure. When set, this adapter is
568
579
  * PROVIDER-BACKED: reads recompute rows from this closure per request and all
569
580
  * writes are rejected (see {@link _assertWritable}). `undefined` ⇒ stored mode.
@@ -625,6 +636,43 @@ var MemoryAdapter = class extends BaseDbAdapter {
625
636
  }
626
637
  return String(id);
627
638
  }
639
+ /** Joins the space's in-memory database (see {@link databases}), creating it on first use. */
640
+ registerSpace(space) {
641
+ let db = databases.get(space);
642
+ if (!db) {
643
+ db = /* @__PURE__ */ new Map();
644
+ databases.set(space, db);
645
+ }
646
+ this._db = db;
647
+ }
648
+ /**
649
+ * This table's state, or `undefined` while it does not exist (never synced,
650
+ * or dropped) — or when no readable is registered.
651
+ */
652
+ _peekState() {
653
+ const table = this._table;
654
+ return table && this._db.get(table.tableName);
655
+ }
656
+ /**
657
+ * This table's state, created on first use — by `ensureTable`, or by a write
658
+ * to a table that was never synced (implicit create, like a MongoDB
659
+ * collection). Reads never create it.
660
+ */
661
+ _state() {
662
+ const db = this._db;
663
+ const name = this._table.tableName;
664
+ let state = db.get(name);
665
+ if (!state) {
666
+ state = {
667
+ kind: this._table.isView ? "view" : "table",
668
+ rows: /* @__PURE__ */ new Map(),
669
+ uniqueIndexes: [],
670
+ incrementCounters: /* @__PURE__ */ new Map()
671
+ };
672
+ db.set(name, state);
673
+ }
674
+ return state;
675
+ }
628
676
  /**
629
677
  * Physical names of the primary-key field(s). Single `@meta.id` resolves via
630
678
  * {@link AtscriptDbReadable.metaIdPhysical}; a composite key maps each logical
@@ -673,7 +721,9 @@ var MemoryAdapter = class extends BaseDbAdapter {
673
721
  * are computed so an increment PRIMARY KEY produces a real `insertedId` and
674
722
  * stores under a real key. The memory analogue of the Mongo adapter's
675
723
  * insert-time increment (Mongo uses an atomic `__atscript_counters`
676
- * collection; an in-memory store just keeps the counter on the instance):
724
+ * collection; an in-memory store keeps the counter in the table's state, so
725
+ * it restarts with a fresh space or when the table is dropped — parity with
726
+ * SQLite `:memory:`; never persisted):
677
727
  *
678
728
  * - No value for the field → assign the next counter value. First use starts
679
729
  * at `start ?? 1` (`max(counter, (start ?? 1) - 1) + 1`); thereafter it is
@@ -683,18 +733,18 @@ var MemoryAdapter = class extends BaseDbAdapter {
683
733
  * - Explicit value present → keep it, but advance the counter to
684
734
  * `max(counter, value)` so a later auto value can never collide with it.
685
735
  */
686
- _applyIncrements(row) {
736
+ _applyIncrements(counters, row) {
687
737
  const fields = this._incrementFields();
688
738
  if (fields.size === 0) return;
689
739
  for (const [physical, start] of fields) {
690
740
  const floor = (start ?? 1) - 1;
691
- const base = this._incrementCounters.get(physical) ?? floor;
741
+ const base = counters.get(physical) ?? floor;
692
742
  const current = row[physical];
693
743
  if (current === void 0 || current === null) {
694
744
  const next = base + 1;
695
745
  row[physical] = next;
696
- this._incrementCounters.set(physical, next);
697
- } else if (typeof current === "number") this._incrementCounters.set(physical, Math.max(base, current));
746
+ counters.set(physical, next);
747
+ } else if (typeof current === "number") counters.set(physical, Math.max(base, current));
698
748
  }
699
749
  }
700
750
  /**
@@ -770,7 +820,8 @@ var MemoryAdapter = class extends BaseDbAdapter {
770
820
  /**
771
821
  * Snapshot seam for reads. Returns the current rows.
772
822
  *
773
- * - Stored mode reads the instance Map directly (insertion order preserved).
823
+ * - Stored mode reads the table's Map directly (insertion order preserved);
824
+ * a table that does not exist reads as empty.
774
825
  * - Provider (read-through) mode calls {@link _provider} to recompute a fresh
775
826
  * snapshot per read.
776
827
  *
@@ -787,7 +838,7 @@ var MemoryAdapter = class extends BaseDbAdapter {
787
838
  */
788
839
  _loadRows() {
789
840
  if (this._provider) return this._provider();
790
- return [...this.rows.values()];
841
+ return [...this._peekState()?.rows.values() ?? []];
791
842
  }
792
843
  /**
793
844
  * Clones the payload in, applies the version default, generates any
@@ -797,15 +848,15 @@ var MemoryAdapter = class extends BaseDbAdapter {
797
848
  * the store. Called once per item by both `insertOne` and `insertMany`, so
798
849
  * increment values advance sequentially across a batch.
799
850
  */
800
- _insertRow(data) {
851
+ _insertRow(state, data) {
801
852
  const row = structuredClone(data);
802
853
  const versionColumn = this._table.versionColumn;
803
854
  if (versionColumn !== void 0 && !(versionColumn in row)) row[versionColumn] = 0;
804
- this._applyIncrements(row);
855
+ this._applyIncrements(state.incrementCounters, row);
805
856
  const key = this.pkKey(row);
806
- if (this.rows.has(key)) throw this._pkConflict();
807
- this._enforceUniqueIndexes(row);
808
- this.rows.set(key, row);
857
+ if (state.rows.has(key)) throw this._pkConflict();
858
+ this._enforceUniqueIndexes(state, row);
859
+ state.rows.set(key, row);
809
860
  const fallback = this._table.metaIdPhysical ? void 0 : this._compositeInsertedId(row);
810
861
  return this._resolveInsertedId(row, fallback);
811
862
  }
@@ -818,8 +869,8 @@ var MemoryAdapter = class extends BaseDbAdapter {
818
869
  * `excludeKey` (when given) skips the row stored under that {@link pkKey} — so
819
870
  * a row updating its own unique value does not false-conflict with itself.
820
871
  */
821
- _enforceUniqueIndexes(row, excludeKey) {
822
- for (const index of this.uniqueIndexes) {
872
+ _enforceUniqueIndexes(state, row, excludeKey) {
873
+ for (const index of state.uniqueIndexes) {
823
874
  const tuple = [];
824
875
  let skip = false;
825
876
  for (const field of index.fields) {
@@ -831,7 +882,7 @@ var MemoryAdapter = class extends BaseDbAdapter {
831
882
  tuple.push(value);
832
883
  }
833
884
  if (skip) continue;
834
- for (const [existingKey, existing] of this.rows) {
885
+ for (const [existingKey, existing] of state.rows) {
835
886
  if (excludeKey !== void 0 && existingKey === excludeKey) continue;
836
887
  let equal = true;
837
888
  for (let i = 0; i < index.fields.length; i++) if (!valuesEqual(getPath(existing, index.fields[i]), tuple[i])) {
@@ -847,11 +898,12 @@ var MemoryAdapter = class extends BaseDbAdapter {
847
898
  }
848
899
  async insertOne(data) {
849
900
  this._assertWritable();
850
- return { insertedId: this._insertRow(data) };
901
+ return { insertedId: this._insertRow(this._state(), data) };
851
902
  }
852
903
  async insertMany(data) {
853
904
  this._assertWritable();
854
- const insertedIds = data.map((item) => this._insertRow(item));
905
+ const state = this._state();
906
+ const insertedIds = data.map((item) => this._insertRow(state, item));
855
907
  return {
856
908
  insertedCount: data.length,
857
909
  insertedIds
@@ -859,7 +911,7 @@ var MemoryAdapter = class extends BaseDbAdapter {
859
911
  }
860
912
  /**
861
913
  * Selects the stored rows a write should touch, as `{ key, row }` pairs so
862
- * callers can mutate in place and re-key. Stored mode scans the instance Map
914
+ * callers can mutate in place and re-key. Stored mode scans the table's Map
863
915
  * directly (writes are authoritative against the store, unlike reads which go
864
916
  * through the {@link _loadRows} snapshot seam).
865
917
  *
@@ -870,14 +922,15 @@ var MemoryAdapter = class extends BaseDbAdapter {
870
922
  * that has no version column is a misconfiguration and throws (mirrors the
871
923
  * Mongo adapter's `_buildCasFilter`).
872
924
  *
873
- * When `many` is `false` at most the first match is returned.
925
+ * When `many` is `false` at most the first match is returned. A table that
926
+ * does not exist (`state` undefined) matches nothing and is not created.
874
927
  */
875
- _selectForWrite(filter, expectedVersion, many) {
928
+ _selectForWrite(state, filter, expectedVersion, many) {
876
929
  const versionColumn = this._table.versionColumn;
877
930
  if (expectedVersion !== void 0 && versionColumn === void 0) throw new Error("expectedVersion requires a versioned table");
878
931
  const match = buildMemoryPredicate(filter);
879
932
  const matched = [];
880
- for (const [key, row] of this.rows) {
933
+ for (const [key, row] of state?.rows ?? []) {
881
934
  if (!match(row)) continue;
882
935
  if (expectedVersion !== void 0 && row[versionColumn] !== expectedVersion) continue;
883
936
  matched.push({
@@ -932,11 +985,11 @@ var MemoryAdapter = class extends BaseDbAdapter {
932
985
  * NEW key (some other row already owns it) throws `CONFLICT`. Throws BEFORE
933
986
  * touching the Map so a failed re-key leaves the store unchanged.
934
987
  */
935
- _commitRow(oldKey, next) {
988
+ _commitRow(rows, oldKey, next) {
936
989
  const newKey = this.pkKey(next);
937
- if (newKey !== oldKey && this.rows.has(newKey)) throw this._pkConflict();
938
- if (newKey !== oldKey) this.rows.delete(oldKey);
939
- this.rows.set(newKey, next);
990
+ if (newKey !== oldKey && rows.has(newKey)) throw this._pkConflict();
991
+ if (newKey !== oldKey) rows.delete(oldKey);
992
+ rows.set(newKey, next);
940
993
  }
941
994
  /**
942
995
  * Update-then-commit for a single matched row: clones the pristine `row`,
@@ -946,25 +999,26 @@ var MemoryAdapter = class extends BaseDbAdapter {
946
999
  * commits. Nothing is written to the store until both the unique and PK checks
947
1000
  * pass, so a conflict leaves the store untouched.
948
1001
  */
949
- _commitUpdate(oldKey, row, data, ops) {
1002
+ _commitUpdate(state, oldKey, row, data, ops) {
950
1003
  const next = structuredClone(row);
951
1004
  this._applyUpdate(next, data, ops);
952
1005
  this._bumpVersion(next, row);
953
- this._enforceUniqueIndexes(next, oldKey);
954
- this._commitRow(oldKey, next);
1006
+ this._enforceUniqueIndexes(state, next, oldKey);
1007
+ this._commitRow(state.rows, oldKey, next);
955
1008
  }
956
1009
  async replaceOne(filter, data, expectedVersion) {
957
1010
  this._assertWritable();
958
- const matched = this._selectForWrite(filter, expectedVersion, false);
959
- if (matched.length === 0) return {
1011
+ const state = this._peekState();
1012
+ const matched = this._selectForWrite(state, filter, expectedVersion, false);
1013
+ if (!state || matched.length === 0) return {
960
1014
  matchedCount: 0,
961
1015
  modifiedCount: 0
962
1016
  };
963
1017
  const { key, row } = matched[0];
964
1018
  const next = structuredClone(data);
965
1019
  this._bumpVersion(next, row);
966
- this._enforceUniqueIndexes(next, key);
967
- this._commitRow(key, next);
1020
+ this._enforceUniqueIndexes(state, next, key);
1021
+ this._commitRow(state.rows, key, next);
968
1022
  return {
969
1023
  matchedCount: 1,
970
1024
  modifiedCount: 1
@@ -972,13 +1026,14 @@ var MemoryAdapter = class extends BaseDbAdapter {
972
1026
  }
973
1027
  async updateOne(filter, data, ops, expectedVersion) {
974
1028
  this._assertWritable();
975
- const matched = this._selectForWrite(filter, expectedVersion, false);
976
- if (matched.length === 0) return {
1029
+ const state = this._peekState();
1030
+ const matched = this._selectForWrite(state, filter, expectedVersion, false);
1031
+ if (!state || matched.length === 0) return {
977
1032
  matchedCount: 0,
978
1033
  modifiedCount: 0
979
1034
  };
980
1035
  const { key, row } = matched[0];
981
- this._commitUpdate(key, row, data, ops);
1036
+ this._commitUpdate(state, key, row, data, ops);
982
1037
  return {
983
1038
  matchedCount: 1,
984
1039
  modifiedCount: 1
@@ -986,9 +1041,10 @@ var MemoryAdapter = class extends BaseDbAdapter {
986
1041
  }
987
1042
  async deleteOne(filter) {
988
1043
  this._assertWritable();
989
- const matched = this._selectForWrite(filter, void 0, false);
990
- if (matched.length === 0) return { deletedCount: 0 };
991
- this.rows.delete(matched[0].key);
1044
+ const state = this._peekState();
1045
+ const matched = this._selectForWrite(state, filter, void 0, false);
1046
+ if (!state || matched.length === 0) return { deletedCount: 0 };
1047
+ state.rows.delete(matched[0].key);
992
1048
  return { deletedCount: 1 };
993
1049
  }
994
1050
  /**
@@ -1092,10 +1148,19 @@ var MemoryAdapter = class extends BaseDbAdapter {
1092
1148
  calendarBucketUnits() {
1093
1149
  return ALL_BUCKET_UNITS;
1094
1150
  }
1151
+ /** Every aggregate function, `countDistinct` included. */
1152
+ aggregateFns() {
1153
+ return ALL_AGGREGATE_FNS;
1154
+ }
1095
1155
  async updateMany(filter, data, ops) {
1096
1156
  this._assertWritable();
1097
- const matched = this._selectForWrite(filter, void 0, true);
1098
- for (const { key, row } of matched) this._commitUpdate(key, row, data, ops);
1157
+ const state = this._peekState();
1158
+ if (!state) return {
1159
+ matchedCount: 0,
1160
+ modifiedCount: 0
1161
+ };
1162
+ const matched = this._selectForWrite(state, filter, void 0, true);
1163
+ for (const { key, row } of matched) this._commitUpdate(state, key, row, data, ops);
1099
1164
  return {
1100
1165
  matchedCount: matched.length,
1101
1166
  modifiedCount: matched.length
@@ -1103,8 +1168,13 @@ var MemoryAdapter = class extends BaseDbAdapter {
1103
1168
  }
1104
1169
  async replaceMany(filter, data) {
1105
1170
  this._assertWritable();
1106
- const matched = this._selectForWrite(filter, void 0, true);
1107
- for (const { key, row } of matched) this._commitUpdate(key, row, data);
1171
+ const state = this._peekState();
1172
+ if (!state) return {
1173
+ matchedCount: 0,
1174
+ modifiedCount: 0
1175
+ };
1176
+ const matched = this._selectForWrite(state, filter, void 0, true);
1177
+ for (const { key, row } of matched) this._commitUpdate(state, key, row, data);
1108
1178
  return {
1109
1179
  matchedCount: matched.length,
1110
1180
  modifiedCount: matched.length
@@ -1112,8 +1182,10 @@ var MemoryAdapter = class extends BaseDbAdapter {
1112
1182
  }
1113
1183
  async deleteMany(filter) {
1114
1184
  this._assertWritable();
1115
- const matched = this._selectForWrite(filter, void 0, true);
1116
- for (const { key } of matched) this.rows.delete(key);
1185
+ const state = this._peekState();
1186
+ if (!state) return { deletedCount: 0 };
1187
+ const matched = this._selectForWrite(state, filter, void 0, true);
1188
+ for (const { key } of matched) state.rows.delete(key);
1117
1189
  return { deletedCount: matched.length };
1118
1190
  }
1119
1191
  /**
@@ -1122,21 +1194,54 @@ var MemoryAdapter = class extends BaseDbAdapter {
1122
1194
  * in-memory scan needs no plain/fulltext/geo index to answer queries.
1123
1195
  */
1124
1196
  async syncIndexes() {
1125
- this.uniqueIndexes = [];
1197
+ const uniqueIndexes = [];
1126
1198
  for (const index of this._table.indexes.values()) {
1127
1199
  if (index.type !== "unique") continue;
1128
- this.uniqueIndexes.push({
1200
+ uniqueIndexes.push({
1129
1201
  name: index.name,
1130
1202
  fields: index.fields.map((f) => f.name),
1131
1203
  optionalFields: new Set(index.fields.filter((f) => f.optional).map((f) => f.name))
1132
1204
  });
1133
1205
  }
1206
+ this._state().uniqueIndexes = uniqueIndexes;
1207
+ }
1208
+ /** Creates the table's (empty) state when it does not exist. Idempotent. */
1209
+ async ensureTable() {
1210
+ this._state();
1134
1211
  }
1135
1212
  /**
1136
- * No-op: the store is the instance-level {@link rows} Map, which already
1137
- * exists. Safe to call repeatedly.
1213
+ * Whether the table exists in the space's in-memory database — created by
1214
+ * {@link ensureTable} (or a first write) and not dropped since. Schema sync
1215
+ * reports a table that does not exist as `create`. An external view always
1216
+ * exists: nothing in memory creates one, its rows come from a provider.
1217
+ * @since 0.1.137
1138
1218
  */
1139
- async ensureTable() {}
1219
+ async tableExists() {
1220
+ if (isAtscriptDbView(this._table) && this._table.isExternal) return true;
1221
+ return this._peekState() !== void 0;
1222
+ }
1223
+ /**
1224
+ * Drops a table by name: its rows, unique indexes and increment counters go,
1225
+ * so the table added back starts empty. A missing table is not an error;
1226
+ * a view under that name is (`dropViewByName` drops views).
1227
+ * @since 0.1.137
1228
+ */
1229
+ async dropTableByName(tableName) {
1230
+ this._dropByName(tableName, "table");
1231
+ }
1232
+ /**
1233
+ * Drops a view by name — its (empty) state; a view holds no rows here.
1234
+ * A missing view is not an error; a table under that name is.
1235
+ * @since 0.1.137
1236
+ */
1237
+ async dropViewByName(viewName) {
1238
+ this._dropByName(viewName, "view");
1239
+ }
1240
+ _dropByName(name, kind) {
1241
+ const state = this._db.get(name);
1242
+ if (state && state.kind !== kind) throw new Error(`Cannot drop ${kind} "${name}": it is a ${state.kind}`);
1243
+ this._db.delete(name);
1244
+ }
1140
1245
  };
1141
1246
  /**
1142
1247
  * Ergonomic late-binding entry point for provider (read-through) mode. Resolves
@@ -1158,7 +1263,8 @@ function setMemoryProvider(space, type, fn) {
1158
1263
  /**
1159
1264
  * Creates a {@link DbSpace} backed by an in-memory {@link MemoryAdapter}.
1160
1265
  *
1161
- * Tables default to STORED mode (an instance-level `Map`). To make one table
1266
+ * Tables default to STORED mode (a `Map` per table in the space's in-memory
1267
+ * database, shared by every adapter of the space). To make one table
1162
1268
  * read-only and read-through from a runtime closure, call `setMemoryProvider`
1163
1269
  * (re-exported from {@link ./memory-adapter}) on the space AFTER the table's
1164
1270
  * adapter has been built.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-memory",
3
- "version": "0.1.135",
3
+ "version": "0.1.137",
4
4
  "description": "In-memory adapter for atscript-db.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -45,7 +45,7 @@
45
45
  "@atscript/core": "^0.1.92",
46
46
  "@atscript/typescript": "^0.1.92",
47
47
  "@uniqu/core": "^0.1.11",
48
- "@atscript/db": "^0.1.135"
48
+ "@atscript/db": "^0.1.137"
49
49
  },
50
50
  "scripts": {
51
51
  "postinstall": "asc -f dts",