js-bao 0.7.0-alpha.9 → 0.11.1

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/README.md CHANGED
@@ -1553,3 +1553,19 @@ See the `examples/` directory for complete working examples:
1553
1553
 
1554
1554
  - `examples/simple-node-test.mjs` - Basic Node.js usage without models
1555
1555
  - `examples/node-example.mjs` - Complete Node.js example with models
1556
+
1557
+ ## Release notes
1558
+
1559
+ Changes to the on-disk behaviour of `js-bao/cloudflare/do` (`DurableObjectEngine`)
1560
+ that a store's owner can observe. Newest first.
1561
+
1562
+ - **`_indexes.created_at` reports the first registration** (#3875). Re-registering
1563
+ a field index whose `field_type` and `is_unique` are unchanged no longer
1564
+ rewrites its `_indexes` row: it writes no row, and `created_at` keeps the time
1565
+ of the first registration. A registration that changes either column still
1566
+ rewrites the row and refreshes `created_at`, as before.
1567
+ - **`replaceWithStringSets` is internal** (#3875). The engine gained
1568
+ `replaceWithStringSets` / `replaceWithStringSetsRaw`, a guarded replace used
1569
+ only by the platform's plain-document projection rebuild. They are marked
1570
+ `@internal` and are not a public API; saves keep the merging
1571
+ `insertWithStringSets`.
package/dist/browser.cjs CHANGED
@@ -1410,12 +1410,18 @@ function fieldUniqueConstraintName(modelName, fieldName) {
1410
1410
  }
1411
1411
  function resolveUniqueConstraints(modelName, fields, customConstraints) {
1412
1412
  const resolved = [];
1413
+ const stringSetFields = /* @__PURE__ */ new Set();
1414
+ for (const [fieldName, options] of fields.entries()) {
1415
+ if (options.type === "stringset") stringSetFields.add(fieldName);
1416
+ }
1413
1417
  for (const [fieldName, options] of fields.entries()) {
1414
1418
  if (options.unique) {
1415
- resolved.push({
1416
- name: fieldUniqueConstraintName(modelName, fieldName),
1417
- fields: [fieldName]
1418
- });
1419
+ const name = fieldUniqueConstraintName(modelName, fieldName);
1420
+ if (stringSetFields.has(fieldName)) {
1421
+ warnStringsetConstraintSkipped(modelName, name, fieldName);
1422
+ continue;
1423
+ }
1424
+ resolved.push({ name, fields: [fieldName] });
1419
1425
  }
1420
1426
  }
1421
1427
  if (customConstraints) {
@@ -1429,6 +1435,13 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
1429
1435
  );
1430
1436
  continue;
1431
1437
  }
1438
+ const stringSetField = constraint.fields.find(
1439
+ (field) => stringSetFields.has(field)
1440
+ );
1441
+ if (stringSetField) {
1442
+ warnStringsetConstraintSkipped(modelName, constraint.name, stringSetField);
1443
+ continue;
1444
+ }
1432
1445
  if (resolved.some((item) => item.name === constraint.name)) {
1433
1446
  console.warn(
1434
1447
  `[defineModelSchema] Duplicate unique constraint name "${constraint.name}" in model "${modelName}".`
@@ -1442,11 +1455,65 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
1442
1455
  }
1443
1456
  return resolved;
1444
1457
  }
1445
- var Y2;
1458
+ function warnStringsetConstraintSkipped(modelName, constraintName, fieldName) {
1459
+ console.warn(
1460
+ `[defineModelSchema] Unique constraint "${constraintName}" for model "${modelName}" names the stringset field "${fieldName}", which cannot be unique. Skipping.`
1461
+ );
1462
+ }
1463
+ function uniqueStringsetViolations(modelName, fields, customConstraints) {
1464
+ const entries = fields instanceof Map ? [...fields.entries()] : Object.entries(fields);
1465
+ const stringSetFields = /* @__PURE__ */ new Set();
1466
+ const violations = [];
1467
+ for (const [fieldName, options] of entries) {
1468
+ if (options?.type !== "stringset") continue;
1469
+ stringSetFields.add(fieldName);
1470
+ if (options.unique === true) {
1471
+ violations.push({
1472
+ kind: "field",
1473
+ field: fieldName,
1474
+ constraint: fieldUniqueConstraintName(modelName, fieldName)
1475
+ });
1476
+ }
1477
+ }
1478
+ for (const constraint of customConstraints ?? []) {
1479
+ const field = (constraint?.fields ?? []).find(
1480
+ (name) => stringSetFields.has(name)
1481
+ );
1482
+ if (field !== void 0) {
1483
+ violations.push({ kind: "compound", field, constraint: constraint.name });
1484
+ }
1485
+ }
1486
+ return violations;
1487
+ }
1488
+ function uniqueStringsetMessage(modelName, violation) {
1489
+ const reason = "A unique constraint applies to scalar fields only.";
1490
+ if (violation.kind === "field") {
1491
+ return `Model "${modelName}": field "${violation.field}" is a stringset and cannot be unique. ${reason}`;
1492
+ }
1493
+ return `Model "${modelName}": unique constraint "${violation.constraint}" names the stringset field "${violation.field}", which cannot be unique. ${reason}`;
1494
+ }
1495
+ function omitStringsetConstraints(constraints, stringSetFields) {
1496
+ const stringSets = new Set(stringSetFields);
1497
+ const kept = [];
1498
+ const dropped = [];
1499
+ for (const constraint of constraints) {
1500
+ const field = (constraint.fields ?? []).find((name) => stringSets.has(name));
1501
+ if (field === void 0) kept.push(constraint);
1502
+ else dropped.push({ constraint, field });
1503
+ }
1504
+ return { kept, dropped };
1505
+ }
1506
+ var Y2, UniqueStringsetError;
1446
1507
  var init_uniqueIndex = __esm({
1447
1508
  "src/utils/uniqueIndex.ts"() {
1448
1509
  "use strict";
1449
1510
  Y2 = __toESM(require("yjs"), 1);
1511
+ UniqueStringsetError = class extends Error {
1512
+ constructor(message) {
1513
+ super(message);
1514
+ this.name = "UniqueStringsetError";
1515
+ }
1516
+ };
1450
1517
  }
1451
1518
  });
1452
1519
 
@@ -1727,6 +1794,80 @@ function groupOverlayEntries(entries) {
1727
1794
  function readOverlayRecord(modelMap, recordId) {
1728
1795
  return collectOverlayRecords(modelMap, /* @__PURE__ */ new Set([recordId])).get(recordId);
1729
1796
  }
1797
+ function recordSegmentOf(key) {
1798
+ const separator = key.indexOf(OVERLAY_KEY_SEPARATOR);
1799
+ return separator > 0 ? key.slice(0, separator) : null;
1800
+ }
1801
+ function reindexKey(index, map, key) {
1802
+ const segment = recordSegmentOf(key);
1803
+ if (segment === null) return;
1804
+ if (map.has(key)) {
1805
+ let keys = index.get(segment);
1806
+ if (!keys) index.set(segment, keys = /* @__PURE__ */ new Set());
1807
+ keys.add(key);
1808
+ } else {
1809
+ const keys = index.get(segment);
1810
+ if (!keys) return;
1811
+ keys.delete(key);
1812
+ if (keys.size === 0) index.delete(segment);
1813
+ }
1814
+ }
1815
+ function recordKeyIndexOf(source) {
1816
+ const map = source;
1817
+ if (typeof map.observe !== "function" || typeof map.has !== "function" || typeof map.get !== "function" || typeof map.keys !== "function") {
1818
+ return null;
1819
+ }
1820
+ const live = map;
1821
+ let index = recordKeyIndexes.get(source);
1822
+ if (!index) {
1823
+ const built = /* @__PURE__ */ new Map();
1824
+ for (const key of live.keys()) {
1825
+ if (typeof key === "string") reindexKey(built, live, key);
1826
+ }
1827
+ live.observe((event) => {
1828
+ for (const key of event.keysChanged) {
1829
+ if (typeof key === "string") reindexKey(built, live, key);
1830
+ }
1831
+ });
1832
+ recordKeyIndexes.set(source, built);
1833
+ index = built;
1834
+ }
1835
+ const pending = live.doc?._transaction?.changed.get(source);
1836
+ if (pending) {
1837
+ for (const key of pending) {
1838
+ if (typeof key === "string") reindexKey(index, live, key);
1839
+ }
1840
+ }
1841
+ return index;
1842
+ }
1843
+ function foldTouchedOverlayKeys(modelMap, touched) {
1844
+ const present = [];
1845
+ const vanished = /* @__PURE__ */ new Set();
1846
+ const tombstoneTouched = /* @__PURE__ */ new Set();
1847
+ for (const key of touched) {
1848
+ const parsed = parseOverlayKey(key);
1849
+ if (!parsed) continue;
1850
+ if (parsed.marker === OVERLAY_MARKER_DELETED) tombstoneTouched.add(parsed.recordId);
1851
+ if (modelMap.has(key)) present.push([key, modelMap.get(key)]);
1852
+ else vanished.add(parsed.recordId);
1853
+ }
1854
+ const entries = groupOverlayEntries(present);
1855
+ for (const id of vanished) if (!entries.has(id)) entries.set(id, emptyEntry(id));
1856
+ const rebuild = /* @__PURE__ */ new Set();
1857
+ for (const [id, entry] of entries) {
1858
+ if (modelMap.get(overlayMarkerKey(id, OVERLAY_MARKER_DELETED)) === true) {
1859
+ entries.set(id, { ...emptyEntry(id), deleted: true });
1860
+ continue;
1861
+ }
1862
+ if (entry.replace || vanished.has(id) || tombstoneTouched.has(id)) rebuild.add(id);
1863
+ }
1864
+ if (rebuild.size > 0) {
1865
+ for (const [id, entry] of collectOverlayRecords(modelMap, rebuild)) {
1866
+ entries.set(id, entry);
1867
+ }
1868
+ }
1869
+ return entries;
1870
+ }
1730
1871
  function completeOverlayEntries(modelMap, entries) {
1731
1872
  const rebuild = /* @__PURE__ */ new Set();
1732
1873
  for (const [id, entry] of entries) {
@@ -1743,6 +1884,23 @@ function collectOverlayRecords(modelMap, recordIds) {
1743
1884
  const wanted = /* @__PURE__ */ new Map();
1744
1885
  for (const id of recordIds) wanted.set(encodeOverlaySegment(id), id);
1745
1886
  const own = /* @__PURE__ */ new Map();
1887
+ const index = recordKeyIndexOf(modelMap);
1888
+ if (index) {
1889
+ const map = modelMap;
1890
+ for (const [segment, id] of wanted) {
1891
+ for (const key of index.get(segment) ?? []) {
1892
+ if (!map.has(key) || !parseOverlayKey(key)) continue;
1893
+ const keys = own.get(id);
1894
+ if (keys) keys.push([key, map.get(key)]);
1895
+ else own.set(id, [[key, map.get(key)]]);
1896
+ }
1897
+ }
1898
+ const out2 = /* @__PURE__ */ new Map();
1899
+ for (const id of recordIds) {
1900
+ out2.set(id, groupOverlayEntries(own.get(id) ?? []).get(id) ?? emptyEntry(id));
1901
+ }
1902
+ return out2;
1903
+ }
1746
1904
  for (const [key, value] of modelMap.entries()) {
1747
1905
  if (typeof key !== "string") continue;
1748
1906
  const separator = key.indexOf(OVERLAY_KEY_SEPARATOR);
@@ -1827,7 +1985,9 @@ function clearStaleRecordOverlay(modelMap, recordId, keep) {
1827
1985
  if (!modelMap.has(overlayMarkerKey(recordId, OVERLAY_MARKER_DELETED))) return;
1828
1986
  const prefix = overlayRecordPrefix(recordId);
1829
1987
  const stale = [];
1830
- for (const key of modelMap.keys()) {
1988
+ const index = recordKeyIndexOf(modelMap);
1989
+ const candidates = index ? index.get(encodeOverlaySegment(recordId)) ?? [] : modelMap.keys();
1990
+ for (const key of candidates) {
1831
1991
  if (key.startsWith(prefix) && keep?.has(key) !== true) stale.push(key);
1832
1992
  }
1833
1993
  for (const key of stale) modelMap.delete(key);
@@ -1851,7 +2011,7 @@ function format2StringSetMembers(value) {
1851
2011
  if (!(value instanceof Y5.Map)) return [];
1852
2012
  return Array.from(value.entries()).filter(([, present]) => present === true).map(([member]) => member);
1853
2013
  }
1854
- var Y5, OVERLAY_KEY_SEPARATOR, OVERLAY_MARKER_REPLACE, OVERLAY_MARKER_DELETED, OVERLAY_MARKERS, Format2CompositeFieldError;
2014
+ var Y5, OVERLAY_KEY_SEPARATOR, OVERLAY_MARKER_REPLACE, OVERLAY_MARKER_DELETED, OVERLAY_MARKERS, recordKeyIndexes, Format2CompositeFieldError;
1855
2015
  var init_largeDocuments = __esm({
1856
2016
  "src/utils/largeDocuments.ts"() {
1857
2017
  "use strict";
@@ -1864,6 +2024,7 @@ var init_largeDocuments = __esm({
1864
2024
  OVERLAY_MARKER_REPLACE,
1865
2025
  OVERLAY_MARKER_DELETED
1866
2026
  ]);
2027
+ recordKeyIndexes = /* @__PURE__ */ new WeakMap();
1867
2028
  Format2CompositeFieldError = class extends Error {
1868
2029
  modelName;
1869
2030
  recordId;
@@ -2243,6 +2404,7 @@ var FORMAT2_MANIFEST_VERSION, FORMAT2_MIN_MANIFEST_VERSION, INGEST_CHUNK_LIMITS,
2243
2404
  var init_snapshotChunks = __esm({
2244
2405
  "src/utils/snapshotChunks.ts"() {
2245
2406
  "use strict";
2407
+ init_uniqueIndex();
2246
2408
  FORMAT2_MANIFEST_VERSION = 3;
2247
2409
  FORMAT2_MIN_MANIFEST_VERSION = 2;
2248
2410
  INGEST_CHUNK_LIMITS = {
@@ -2313,16 +2475,26 @@ function statementsFor(tables) {
2313
2475
  deleteMember: `DELETE FROM ${members} WHERE _type = ? AND _record_id = ? AND field = ? AND value = ?`,
2314
2476
  insertMember: `INSERT OR IGNORE INTO ${members} (_record_id, _type, field, value) VALUES (?, ?, ?, ?)`,
2315
2477
  selectMembers: `SELECT value FROM ${members} WHERE _type = ? AND _record_id = ? AND field = ? ORDER BY rowid`,
2478
+ // `field, rowid`, not `rowid` alone: it is the order the per-record index
2479
+ // `(_type, _record_id, field)` yields, so the planner reads the record's
2480
+ // rows through it. With a bare `rowid` the covering UNIQUE
2481
+ // `(_type, field, _record_id, value)` wins on its `_type` prefix and walks
2482
+ // every member row of the model per record (#3688's figure, broken by
2483
+ // #3874). Stored order within a field, which is all the comparison reads,
2484
+ // is unchanged.
2485
+ selectAllMembers: `SELECT field, value FROM ${members} WHERE _type = ? AND _record_id = ? ORDER BY field, rowid`,
2316
2486
  mergeUpsert: `
2317
2487
  INSERT INTO ${records} (_id, _type, _data)
2318
2488
  VALUES (?, ?, json_patch('{}', ?))
2319
2489
  ON CONFLICT(_type, _id) DO UPDATE SET
2320
- _data = json_patch(${records}._data, ?)`.trim(),
2490
+ _data = json_patch(${records}._data, ?)
2491
+ WHERE json_patch(${records}._data, ?) IS NOT ${records}._data`.trim(),
2321
2492
  replaceUpsert: `
2322
2493
  INSERT INTO ${records} (_id, _type, _data)
2323
2494
  VALUES (?, ?, json_patch('{}', ?))
2324
2495
  ON CONFLICT(_type, _id) DO UPDATE SET
2325
- _data = json_patch('{}', ?)`.trim()
2496
+ _data = json_patch('{}', ?)
2497
+ WHERE json_patch('{}', ?) IS NOT ${records}._data`.trim()
2326
2498
  };
2327
2499
  statementCache.set(cacheKey, statements);
2328
2500
  return statements;
@@ -2335,6 +2507,7 @@ function projectOverlayEntry(sql, modelName, entry, tables = DEFAULT_OVERLAY_TAB
2335
2507
  deleteMember: DELETE_MEMBER,
2336
2508
  insertMember: INSERT_MEMBER,
2337
2509
  selectMembers: SELECT_MEMBERS,
2510
+ selectAllMembers: SELECT_ALL_MEMBERS,
2338
2511
  mergeUpsert: MERGE_UPSERT,
2339
2512
  replaceUpsert: REPLACE_UPSERT
2340
2513
  } = statementsFor(tables);
@@ -2348,10 +2521,20 @@ function projectOverlayEntry(sql, modelName, entry, tables = DEFAULT_OVERLAY_TAB
2348
2521
  if (!entry.replace && touchedFields.length === 0 && touchedSets.length === 0) {
2349
2522
  return;
2350
2523
  }
2524
+ const patch = { ...entry.fields, id };
2351
2525
  if (entry.replace) {
2526
+ const unchanged = unchangedReplaceMembers(
2527
+ sql,
2528
+ SELECT_ALL_MEMBERS,
2529
+ modelName,
2530
+ entry
2531
+ );
2532
+ if (unchanged) {
2533
+ for (const [field, values] of unchanged) patch[field] = values;
2534
+ return upsert(sql, REPLACE_UPSERT, modelName, id, patch);
2535
+ }
2352
2536
  sql.exec(DELETE_ALL_MEMBERS, modelName, id);
2353
2537
  }
2354
- const patch = { ...entry.fields, id };
2355
2538
  for (const [field, members] of Object.entries(entry.stringSets)) {
2356
2539
  for (const [member, present] of Object.entries(members)) {
2357
2540
  if (present) sql.exec(INSERT_MEMBER, id, modelName, field, member);
@@ -2360,14 +2543,40 @@ function projectOverlayEntry(sql, modelName, entry, tables = DEFAULT_OVERLAY_TAB
2360
2543
  const rows = Array.from(sql.exec(SELECT_MEMBERS, modelName, id, field));
2361
2544
  patch[field] = rows.map((row) => row.value);
2362
2545
  }
2546
+ upsert(sql, entry.replace ? REPLACE_UPSERT : MERGE_UPSERT, modelName, id, patch);
2547
+ }
2548
+ function upsert(sql, statement, modelName, id, patch) {
2363
2549
  const patchJson = JSON.stringify(patch);
2364
- sql.exec(
2365
- entry.replace ? REPLACE_UPSERT : MERGE_UPSERT,
2366
- id,
2367
- modelName,
2368
- patchJson,
2369
- patchJson
2370
- );
2550
+ sql.exec(statement, id, modelName, patchJson, patchJson, patchJson);
2551
+ }
2552
+ function unchangedReplaceMembers(sql, selectAllMembers, modelName, entry) {
2553
+ const wanted = /* @__PURE__ */ new Map();
2554
+ let present = 0;
2555
+ for (const [field, members] of Object.entries(entry.stringSets)) {
2556
+ const values = Object.entries(members).filter(([, isPresent]) => isPresent).map(([member]) => member);
2557
+ wanted.set(field, values);
2558
+ present += values.length;
2559
+ }
2560
+ if (present === 0) return null;
2561
+ const current = /* @__PURE__ */ new Map();
2562
+ for (const row of sql.exec(selectAllMembers, modelName, entry.id)) {
2563
+ const field = String(row.field);
2564
+ const values = current.get(field);
2565
+ if (values) values.push(String(row.value));
2566
+ else current.set(field, [String(row.value)]);
2567
+ }
2568
+ if (current.size === 0) return null;
2569
+ for (const [field, values] of wanted) {
2570
+ const held = current.get(field) ?? [];
2571
+ if (held.length !== values.length) return null;
2572
+ for (let i = 0; i < values.length; i++) {
2573
+ if (held[i] !== values[i]) return null;
2574
+ }
2575
+ }
2576
+ for (const field of current.keys()) {
2577
+ if (!wanted.has(field)) return null;
2578
+ }
2579
+ return wanted;
2371
2580
  }
2372
2581
  var DEFAULT_OVERLAY_TABLES, statementCache;
2373
2582
  var init_overlaySql = __esm({
@@ -4505,15 +4714,7 @@ function materializeOverlayEntries(doc, touched) {
4505
4714
  const out = [];
4506
4715
  for (const [modelName, keys] of touched) {
4507
4716
  const modelMap = doc.getMap(modelName);
4508
- const delta = [];
4509
- for (const key of keys) {
4510
- if (!parseOverlayKey(key) || !modelMap.has(key)) continue;
4511
- delta.push([key, modelMap.get(key)]);
4512
- }
4513
- const grouped = completeOverlayEntries(
4514
- modelMap,
4515
- groupOverlayEntries(delta)
4516
- );
4717
+ const grouped = foldTouchedOverlayKeys(modelMap, keys);
4517
4718
  for (const entry of grouped.values()) out.push({ model: modelName, entry });
4518
4719
  }
4519
4720
  return out;
@@ -12430,7 +12631,40 @@ var init_BaseModel = __esm({
12430
12631
  return { sql, params, aliasMetadata };
12431
12632
  }
12432
12633
  /**
12433
- * Process aggregation results into nested structure
12634
+ * Read one row's operation values, collapsing a single-operation aggregate
12635
+ * to the bare value of that operation.
12636
+ *
12637
+ * This is the platform's one aggregate leaf rule, shared with the DO engine's
12638
+ * `_extractOpValue` (`engines/cloudflare/createDocumentDO.ts`, behind
12639
+ * database aggregates, registered `aggregate` operations, workflow pipeline
12640
+ * steps and `connectDoDb` bindings) and its mirror `aggregateRowsToResult`
12641
+ * in the platform's `src/document-projection.ts` (the document HTTP
12642
+ * aggregate). These model statics used to collapse a lone `count` only and
12643
+ * keep `<type>_<field>` for a lone `sum`/`avg`/`min`/`max`, so the same
12644
+ * declared aggregation answered differently depending on the path taken;
12645
+ * they were the outlier and moved to the shared rule in #3076 (js-bao 0.10.0).
12646
+ *
12647
+ * Both result-shaping branches below call this, so the StringSet-facet path
12648
+ * and the general path cannot drift apart again. Pinned by
12649
+ * `cli/tests/unit/aggregate-shaper-divergence-3076.test.ts` and, end to end
12650
+ * against the HTTP aggregate, by
12651
+ * `tests/api/http/aggregate-shape-convergence-3076.test.ts`.
12652
+ */
12653
+ static extractAggregationLeaf(row, operations) {
12654
+ const valueFor = (operation) => operation.type === "count" ? row.count : row[`${operation.type}_${operation.field}`];
12655
+ if (operations.length === 1) return valueFor(operations[0]);
12656
+ const leaf = {};
12657
+ for (const operation of operations) {
12658
+ if (operation.type === "count") leaf.count = row.count;
12659
+ else leaf[`${operation.type}_${operation.field}`] = valueFor(operation);
12660
+ }
12661
+ return leaf;
12662
+ }
12663
+ /**
12664
+ * Process aggregation results into nested structure.
12665
+ *
12666
+ * The leaf under the last group value follows the one platform rule — see
12667
+ * {@link BaseModel.extractAggregationLeaf}.
12434
12668
  */
12435
12669
  static processAggregationResults(results, options, aliasMetadata) {
12436
12670
  if (results.length === 0) {
@@ -12446,19 +12680,10 @@ var init_BaseModel = __esm({
12446
12680
  if (options.groupBy.length === 1 && typeof options.groupBy[0] === "string" && results[0].hasOwnProperty("group_key")) {
12447
12681
  const facetResult = {};
12448
12682
  for (const row of results) {
12449
- const key = row.group_key;
12450
- facetResult[key] = {};
12451
- for (const operation of options.operations) {
12452
- if (operation.type === "count") {
12453
- facetResult[key].count = row.count;
12454
- } else {
12455
- const operationKey = `${operation.type}_${operation.field}`;
12456
- facetResult[key][operationKey] = row[operationKey];
12457
- }
12458
- }
12459
- if (options.operations.length === 1 && options.operations[0].type === "count") {
12460
- facetResult[key] = row.count;
12461
- }
12683
+ facetResult[row.group_key] = this.extractAggregationLeaf(
12684
+ row,
12685
+ options.operations
12686
+ );
12462
12687
  }
12463
12688
  return facetResult;
12464
12689
  }
@@ -12486,18 +12711,7 @@ var init_BaseModel = __esm({
12486
12711
  key = row[resolvedAlias];
12487
12712
  }
12488
12713
  if (i === options.groupBy.length - 1) {
12489
- current[key] = {};
12490
- for (const operation of options.operations) {
12491
- if (operation.type === "count") {
12492
- current[key].count = row.count;
12493
- } else {
12494
- const operationKey = `${operation.type}_${operation.field}`;
12495
- current[key][operationKey] = row[operationKey];
12496
- }
12497
- }
12498
- if (options.operations.length === 1 && options.operations[0].type === "count") {
12499
- current[key] = row.count;
12500
- }
12714
+ current[key] = this.extractAggregationLeaf(row, options.operations);
12501
12715
  } else {
12502
12716
  if (!current[key]) {
12503
12717
  current[key] = {};
@@ -13337,6 +13551,7 @@ __export(browser_exports, {
13337
13551
  SqljsEngine: () => SqljsEngine,
13338
13552
  StringSet: () => StringSet,
13339
13553
  UniqueConstraintViolationError: () => UniqueConstraintViolationError,
13554
+ UniqueStringsetError: () => UniqueStringsetError,
13340
13555
  applyFastForward: () => applyFastForward,
13341
13556
  applyOverlayMutation: () => applyOverlayMutation,
13342
13557
  assertNoFormat2CompositeFields: () => assertNoFormat2CompositeFields,
@@ -13364,7 +13579,9 @@ __export(browser_exports, {
13364
13579
  estimateMaterializedBytes: () => estimateMaterializedBytes,
13365
13580
  features: () => features,
13366
13581
  findReservedFieldNameInToml: () => findReservedFieldNameInToml,
13582
+ findUniqueStringsetInToml: () => findUniqueStringsetInToml,
13367
13583
  foldFormat2Overlay: () => foldFormat2Overlay,
13584
+ foldTouchedOverlayKeys: () => foldTouchedOverlayKeys,
13368
13585
  followEpoch: () => followEpoch,
13369
13586
  format2ClientDDL: () => format2ClientDDL,
13370
13587
  format2EpochDocName: () => format2EpochDocName,
@@ -13391,6 +13608,7 @@ __export(browser_exports, {
13391
13608
  observeFormat2Overlay: () => observeFormat2Overlay,
13392
13609
  offlineReplayRecordKey: () => offlineReplayRecordKey,
13393
13610
  offlineWindowStatus: () => offlineWindowStatus,
13611
+ omitStringsetConstraints: () => omitStringsetConstraints,
13394
13612
  overlayFieldKey: () => overlayFieldKey,
13395
13613
  overlayKeysSince: () => overlayKeysSince,
13396
13614
  overlayMarkerKey: () => overlayMarkerKey,
@@ -13420,6 +13638,8 @@ __export(browser_exports, {
13420
13638
  suppressSupersededKeys: () => suppressSupersededKeys,
13421
13639
  syncInferredMeta: () => syncInferredMeta,
13422
13640
  syncModelMeta: () => syncModelMeta,
13641
+ uniqueStringsetMessage: () => uniqueStringsetMessage,
13642
+ uniqueStringsetViolations: () => uniqueStringsetViolations,
13423
13643
  validateSnapshotManifest: () => validateSnapshotManifest,
13424
13644
  writeCarriedOverlay: () => writeCarriedOverlay
13425
13645
  });
@@ -19395,6 +19615,17 @@ init_BaseModel();
19395
19615
  init_ModelRegistry();
19396
19616
  init_uniqueIndex();
19397
19617
  function defineModelSchema(input) {
19618
+ const [violation] = uniqueStringsetViolations(
19619
+ input.name,
19620
+ input.fields,
19621
+ input.options?.uniqueConstraints
19622
+ );
19623
+ if (violation) {
19624
+ throw new UniqueStringsetError(uniqueStringsetMessage(input.name, violation));
19625
+ }
19626
+ return buildDefinedModelSchema(input);
19627
+ }
19628
+ function buildDefinedModelSchema(input) {
19398
19629
  const { name, fields } = input;
19399
19630
  const options = {
19400
19631
  name,
@@ -19722,6 +19953,7 @@ var ReservedFieldNameError = class extends Error {
19722
19953
  };
19723
19954
 
19724
19955
  // src/models/tomlLoader.ts
19956
+ init_uniqueIndex();
19725
19957
  var VALID_FIELD_TYPES = /* @__PURE__ */ new Set([
19726
19958
  "string",
19727
19959
  "number",
@@ -19869,6 +20101,7 @@ function parseRelationship(raw, context, strict) {
19869
20101
  function loadSchemaFromTomlString(tomlString, options = {}) {
19870
20102
  const strict = options.strict !== false;
19871
20103
  const rejectReserved = options.rejectReservedFieldNames === true;
20104
+ const rejectUniqueStringset = options.rejectUniqueStringset !== false;
19872
20105
  const parsed = (0, import_smol_toml.parse)(tomlString);
19873
20106
  const models = parsed.models;
19874
20107
  if (!models || typeof models !== "object") {
@@ -19883,6 +20116,7 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
19883
20116
  strict
19884
20117
  );
19885
20118
  const fields = {};
20119
+ const declaredFields = /* @__PURE__ */ new Map();
19886
20120
  if (modelDef.fields) {
19887
20121
  for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
19888
20122
  const context = `[models.${modelName}.fields.${fieldName}]`;
@@ -19892,7 +20126,9 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
19892
20126
  throw new ReservedFieldNameError(`${context}: ${reserved}`);
19893
20127
  }
19894
20128
  }
19895
- fields[fieldName] = parseFieldOptions(fieldDef, context, strict);
20129
+ const fieldOptions = parseFieldOptions(fieldDef, context, strict);
20130
+ declaredFields.set(fieldName, fieldOptions);
20131
+ fields[fieldName] = fieldOptions;
19896
20132
  }
19897
20133
  }
19898
20134
  let relationships;
@@ -19925,8 +20161,20 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
19925
20161
  }
19926
20162
  }
19927
20163
  const className = typeof modelDef.class_name === "string" ? modelDef.class_name : void 0;
20164
+ if (rejectUniqueStringset) {
20165
+ const [violation] = uniqueStringsetViolations(
20166
+ modelName,
20167
+ declaredFields,
20168
+ uniqueConstraints
20169
+ );
20170
+ if (violation) {
20171
+ throw new UniqueStringsetError(
20172
+ uniqueStringsetMessage(modelName, violation)
20173
+ );
20174
+ }
20175
+ }
19928
20176
  schemas.push(
19929
- defineModelSchema({
20177
+ buildDefinedModelSchema({
19930
20178
  name: modelName,
19931
20179
  fields,
19932
20180
  options: {
@@ -19943,7 +20191,10 @@ function findReservedFieldNameInToml(tomlString) {
19943
20191
  try {
19944
20192
  loadSchemaFromTomlString(tomlString, {
19945
20193
  strict: false,
19946
- rejectReservedFieldNames: true
20194
+ rejectReservedFieldNames: true,
20195
+ // Independent of the #3719 check: a unique stringset in one model must
20196
+ // not hide a reserved name in the next.
20197
+ rejectUniqueStringset: false
19947
20198
  });
19948
20199
  return null;
19949
20200
  } catch (error) {
@@ -19951,8 +20202,18 @@ function findReservedFieldNameInToml(tomlString) {
19951
20202
  return null;
19952
20203
  }
19953
20204
  }
20205
+ function findUniqueStringsetInToml(tomlString) {
20206
+ try {
20207
+ loadSchemaFromTomlString(tomlString, { strict: false });
20208
+ return null;
20209
+ } catch (error) {
20210
+ if (error instanceof UniqueStringsetError) return error.message;
20211
+ return null;
20212
+ }
20213
+ }
19954
20214
 
19955
20215
  // src/browser.ts
20216
+ init_uniqueIndex();
19956
20217
  init_metaSync();
19957
20218
  init_format2RecordStore();
19958
20219
  init_format2RecordStoreAsync();