js-bao 0.5.1 → 0.6.0

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.
@@ -229,21 +229,36 @@ var init_CursorManager = __esm({
229
229
  return fields;
230
230
  }
231
231
  /**
232
- * Build ORDER BY clause from sort specification
232
+ * Build ORDER BY clause from sort specification.
233
+ *
234
+ * `direction` is the pagination direction (1 = forward, -1 = backward).
235
+ * For backward pagination each field's effective ORDER BY direction is
236
+ * reversed so the cursor's flipped inequality (built by
237
+ * `buildPaginationConditions`) returns the page adjacent to the cursor,
238
+ * not the globally smallest/largest window. Without this,
239
+ * `field < cursor ... ORDER BY field ASC LIMIT n` returns the n smallest
240
+ * rows overall instead of the n rows just below the cursor.
241
+ *
242
+ * The returned `directions` array carries the *declared* per-field sort
243
+ * directions (unaffected by `direction`) — `buildPaginationConditions`
244
+ * needs the declared directions to pick each field's comparison operator.
233
245
  */
234
- static buildOrderClause(sort, fieldFormatter = (field) => field) {
246
+ static buildOrderClause(sort, fieldFormatter = (field) => field, direction = 1) {
235
247
  const fields = this.extractSortFields(sort);
236
248
  const clauses = [];
237
249
  const directions = [];
250
+ const sqlDir = (declared) => {
251
+ const effective = direction === -1 ? declared === 1 ? -1 : 1 : declared;
252
+ return effective === 1 ? "ASC" : "DESC";
253
+ };
238
254
  if (sort && Object.keys(sort).length > 0) {
239
255
  for (const [field, dir] of Object.entries(sort)) {
240
- const sqlDirection = dir === 1 ? "ASC" : "DESC";
241
- clauses.push(`${fieldFormatter(field)} ${sqlDirection}`);
256
+ clauses.push(`${fieldFormatter(field)} ${sqlDir(dir)}`);
242
257
  directions.push(dir);
243
258
  }
244
259
  }
245
260
  if (!sort || !sort.hasOwnProperty("id")) {
246
- clauses.push(`${fieldFormatter("id")} ASC`);
261
+ clauses.push(`${fieldFormatter("id")} ${sqlDir(1)}`);
247
262
  directions.push(1);
248
263
  } else if (sort && sort.hasOwnProperty("id")) {
249
264
  directions.push(sort["id"]);
@@ -266,18 +281,27 @@ var init_CursorManager = __esm({
266
281
  /**
267
282
  * Generate next and previous cursors from result set
268
283
  */
269
- static generateResultCursors(results, sortFields, _requestedDirection, hasMore, isFirstPage = false) {
284
+ static generateResultCursors(results, sortFields, requestedDirection, hasMore, isFirstPage = false) {
270
285
  if (results.length === 0) {
271
286
  return {};
272
287
  }
273
288
  const cursors = {};
274
289
  if (hasMore && results.length > 0) {
275
290
  const lastResult = results[results.length - 1];
276
- cursors.nextCursor = this.generateCursor(lastResult, sortFields, 1);
291
+ cursors.nextCursor = this.generateCursor(
292
+ lastResult,
293
+ sortFields,
294
+ requestedDirection
295
+ );
277
296
  }
278
297
  if (results.length > 0 && !isFirstPage) {
279
298
  const firstResult = results[0];
280
- cursors.prevCursor = this.generateCursor(firstResult, sortFields, -1);
299
+ const opposite = requestedDirection === 1 ? -1 : 1;
300
+ cursors.prevCursor = this.generateCursor(
301
+ firstResult,
302
+ sortFields,
303
+ opposite
304
+ );
281
305
  }
282
306
  return cursors;
283
307
  }
@@ -329,6 +353,9 @@ var init_patterns = __esm({
329
353
 
330
354
  // src/utils/yjsValues.ts
331
355
  import * as Y from "yjs";
356
+ function isYjsComposite(value) {
357
+ return value instanceof Y.Map || value instanceof Y.Array || value instanceof Y.Text;
358
+ }
332
359
  function isStringSetYMap(value) {
333
360
  if (!(value instanceof Y.Map)) return false;
334
361
  for (const v of value.values()) {
@@ -367,8 +394,33 @@ var init_DocumentQueryTranslator = __esm({
367
394
  }
368
395
  });
369
396
 
370
- // src/models/metaSync.ts
397
+ // src/models/writeLifecycle.ts
371
398
  import * as Y3 from "yjs";
399
+ import { ulid as ulid2 } from "ulid";
400
+ function registerFunctionDefaultGenerator(name, fn) {
401
+ FUNCTION_DEFAULT_GENERATORS.set(name, fn);
402
+ }
403
+ var KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS, UNKNOWN_FUNCTION_DEFAULT;
404
+ var init_writeLifecycle = __esm({
405
+ "src/models/writeLifecycle.ts"() {
406
+ "use strict";
407
+ KNOWN_FIELD_TYPE_MAP = {
408
+ string: true,
409
+ number: true,
410
+ boolean: true,
411
+ date: true,
412
+ id: true,
413
+ stringset: true
414
+ };
415
+ KNOWN_FIELD_TYPES = new Set(Object.keys(KNOWN_FIELD_TYPE_MAP));
416
+ FUNCTION_DEFAULT_GENERATORS = /* @__PURE__ */ new Map();
417
+ registerFunctionDefaultGenerator("generate_ulid", () => ulid2());
418
+ UNKNOWN_FUNCTION_DEFAULT = "$unknown_function";
419
+ }
420
+ });
421
+
422
+ // src/models/metaSync.ts
423
+ import * as Y4 from "yjs";
372
424
  function inferFieldType(value) {
373
425
  if (isStringSetYMap(value)) return "stringset";
374
426
  switch (typeof value) {
@@ -389,7 +441,10 @@ function encodeDefault(value) {
389
441
  if (value === void 0 || value === null) return void 0;
390
442
  if (typeof value === "function") {
391
443
  const name = KNOWN_FUNCTION_DEFAULTS.get(value);
392
- return name ? `$${name}` : "$unknown_function";
444
+ return name ? `$${name}` : UNKNOWN_FUNCTION_DEFAULT;
445
+ }
446
+ if (typeof value === "string" && value.startsWith("$")) {
447
+ return `$${value}`;
393
448
  }
394
449
  return value;
395
450
  }
@@ -415,7 +470,7 @@ function syncModelMeta(yDoc, modelName, schema) {
415
470
  if (compoundConstraints.length > 0) {
416
471
  let constraints = meta.get("_constraints");
417
472
  if (!constraints) {
418
- constraints = new Y3.Map();
473
+ constraints = new Y4.Map();
419
474
  meta.set("_constraints", constraints);
420
475
  }
421
476
  for (const constraint of compoundConstraints) {
@@ -426,7 +481,7 @@ function syncModelMeta(yDoc, modelName, schema) {
426
481
  if (relationships && Object.keys(relationships).length > 0) {
427
482
  let rels = meta.get("_relationships");
428
483
  if (!rels) {
429
- rels = new Y3.Map();
484
+ rels = new Y4.Map();
430
485
  meta.set("_relationships", rels);
431
486
  }
432
487
  for (const [relName, relConfig] of Object.entries(relationships)) {
@@ -438,7 +493,7 @@ function syncModelMeta(yDoc, modelName, schema) {
438
493
  function syncFieldMeta(metaMap, fieldName, fieldOpts) {
439
494
  let fieldMeta = metaMap.get(fieldName);
440
495
  if (!fieldMeta) {
441
- fieldMeta = new Y3.Map();
496
+ fieldMeta = new Y4.Map();
442
497
  metaMap.set(fieldName, fieldMeta);
443
498
  }
444
499
  setIfChanged(fieldMeta, "type", fieldOpts.type);
@@ -455,7 +510,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
455
510
  function syncConstraintMeta(constraintsMap, constraint) {
456
511
  let cMeta = constraintsMap.get(constraint.name);
457
512
  if (!cMeta) {
458
- cMeta = new Y3.Map();
513
+ cMeta = new Y4.Map();
459
514
  constraintsMap.set(constraint.name, cMeta);
460
515
  }
461
516
  setIfChanged(cMeta, "type", "unique");
@@ -465,7 +520,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
465
520
  function syncRelationshipMeta(relsMap, relName, relConfig) {
466
521
  let relMeta = relsMap.get(relName);
467
522
  if (!relMeta) {
468
- relMeta = new Y3.Map();
523
+ relMeta = new Y4.Map();
469
524
  relsMap.set(relName, relMeta);
470
525
  }
471
526
  for (const [key, value] of Object.entries(relConfig)) {
@@ -480,7 +535,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
480
535
  if (fieldName.startsWith("_")) continue;
481
536
  let fieldMeta = meta.get(fieldName);
482
537
  if (!fieldMeta) {
483
- fieldMeta = new Y3.Map();
538
+ fieldMeta = new Y4.Map();
484
539
  meta.set(fieldName, fieldMeta);
485
540
  }
486
541
  if (!fieldMeta.has("type")) {
@@ -501,16 +556,537 @@ var init_metaSync = __esm({
501
556
  "src/models/metaSync.ts"() {
502
557
  "use strict";
503
558
  init_yjsValues();
559
+ init_writeLifecycle();
504
560
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
505
561
  _syncedCache = /* @__PURE__ */ new WeakMap();
506
562
  }
507
563
  });
508
564
 
565
+ // src/utils/uniqueIndex.ts
566
+ import * as Y5 from "yjs";
567
+ function fieldUniqueConstraintName(modelName, fieldName) {
568
+ return `${modelName}_${fieldName}_unique`;
569
+ }
570
+ var init_uniqueIndex = __esm({
571
+ "src/utils/uniqueIndex.ts"() {
572
+ "use strict";
573
+ }
574
+ });
575
+
576
+ // src/utils/largeDocuments.ts
577
+ import * as Y6 from "yjs";
578
+ function encodeOverlaySegment(segment) {
579
+ const escaped = segment.replace(/~/g, "~0").replace(/\//g, "~1");
580
+ return escaped.startsWith("_") ? `~2${escaped.slice(1)}` : escaped;
581
+ }
582
+ function decodeOverlaySegment(segment) {
583
+ const withUnderscore = segment.startsWith("~2") ? `_${segment.slice(2)}` : segment;
584
+ return withUnderscore.replace(/~1/g, "/").replace(/~0/g, "~");
585
+ }
586
+ function overlayFieldKey(recordId, field) {
587
+ return `${encodeOverlaySegment(recordId)}${OVERLAY_KEY_SEPARATOR}${encodeOverlaySegment(field)}`;
588
+ }
589
+ function overlayMemberKey(recordId, field, member) {
590
+ return `${overlayFieldKey(recordId, field)}${OVERLAY_KEY_SEPARATOR}${encodeOverlaySegment(member)}`;
591
+ }
592
+ function overlayMarkerKey(recordId, marker) {
593
+ return `${encodeOverlaySegment(recordId)}${OVERLAY_KEY_SEPARATOR}${marker}`;
594
+ }
595
+ function parseOverlayKey(key) {
596
+ const parts = key.split(OVERLAY_KEY_SEPARATOR);
597
+ if (parts.length === 2) {
598
+ const [rawId, rawSecond] = parts;
599
+ const recordId = decodeOverlaySegment(rawId);
600
+ if (OVERLAY_MARKERS.has(rawSecond)) return { recordId, marker: rawSecond };
601
+ return { recordId, field: decodeOverlaySegment(rawSecond) };
602
+ }
603
+ if (parts.length === 3) {
604
+ return {
605
+ recordId: decodeOverlaySegment(parts[0]),
606
+ field: decodeOverlaySegment(parts[1]),
607
+ member: decodeOverlaySegment(parts[2])
608
+ };
609
+ }
610
+ return null;
611
+ }
612
+ function emptyEntry(id) {
613
+ return { id, fields: {}, stringSets: {}, replace: false, deleted: false };
614
+ }
615
+ function groupOverlayEntries(entries) {
616
+ const out = /* @__PURE__ */ new Map();
617
+ for (const [key, value] of entries) {
618
+ const parsed = parseOverlayKey(key);
619
+ if (!parsed) continue;
620
+ let entry = out.get(parsed.recordId);
621
+ if (!entry) {
622
+ entry = emptyEntry(parsed.recordId);
623
+ out.set(parsed.recordId, entry);
624
+ }
625
+ if (parsed.marker === OVERLAY_MARKER_REPLACE) {
626
+ entry.replace = value === true;
627
+ } else if (parsed.marker === OVERLAY_MARKER_DELETED) {
628
+ entry.deleted = value === true;
629
+ } else if (parsed.member !== void 0) {
630
+ const field = parsed.field;
631
+ (entry.stringSets[field] ||= {})[parsed.member] = value === true;
632
+ } else if (parsed.field !== void 0) {
633
+ entry.fields[parsed.field] = value;
634
+ }
635
+ }
636
+ return out;
637
+ }
638
+ function readOverlayRecord(modelMap, recordId) {
639
+ return collectOverlayRecords(modelMap, /* @__PURE__ */ new Set([recordId])).get(recordId);
640
+ }
641
+ function completeOverlayEntries(modelMap, entries) {
642
+ const rebuild = /* @__PURE__ */ new Set();
643
+ for (const [id, entry] of entries) {
644
+ if (!entry.replace || entry.deleted) continue;
645
+ rebuild.add(id);
646
+ }
647
+ if (rebuild.size === 0) return entries;
648
+ for (const [id, entry] of collectOverlayRecords(modelMap, rebuild)) {
649
+ entries.set(id, entry);
650
+ }
651
+ return entries;
652
+ }
653
+ function collectOverlayRecords(modelMap, recordIds) {
654
+ const wanted = /* @__PURE__ */ new Map();
655
+ for (const id of recordIds) wanted.set(encodeOverlaySegment(id), id);
656
+ const own = /* @__PURE__ */ new Map();
657
+ for (const [key, value] of modelMap.entries()) {
658
+ if (typeof key !== "string") continue;
659
+ const separator = key.indexOf(OVERLAY_KEY_SEPARATOR);
660
+ if (separator <= 0) continue;
661
+ const id = wanted.get(key.slice(0, separator));
662
+ if (id === void 0) continue;
663
+ if (!parseOverlayKey(key)) continue;
664
+ const keys = own.get(id);
665
+ if (keys) keys.push([key, value]);
666
+ else own.set(id, [[key, value]]);
667
+ }
668
+ const out = /* @__PURE__ */ new Map();
669
+ for (const id of recordIds) {
670
+ out.set(id, groupOverlayEntries(own.get(id) ?? []).get(id) ?? emptyEntry(id));
671
+ }
672
+ return out;
673
+ }
674
+ function baseMembers(value) {
675
+ if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
676
+ if (value && typeof value === "object") {
677
+ return Object.entries(value).filter(([, present]) => Boolean(present)).map(([member]) => member);
678
+ }
679
+ return [];
680
+ }
681
+ function materializeOverlayRecord(base, overlay) {
682
+ if (overlay.deleted) return null;
683
+ const out = overlay.replace ? {} : { ...base ?? {} };
684
+ for (const [field, value] of Object.entries(overlay.fields)) {
685
+ if (value === null) delete out[field];
686
+ else out[field] = value;
687
+ }
688
+ for (const [field, members] of Object.entries(overlay.stringSets)) {
689
+ const current = overlay.replace ? [] : baseMembers(out[field]);
690
+ const next = current.slice();
691
+ for (const [member, present] of Object.entries(members)) {
692
+ const at = next.indexOf(member);
693
+ if (present && at === -1) next.push(member);
694
+ else if (!present && at !== -1) next.splice(at, 1);
695
+ }
696
+ out[field] = next;
697
+ }
698
+ out.id = overlay.id;
699
+ return out;
700
+ }
701
+ function encodeOverlayMutation(mutation) {
702
+ const { id, kind } = mutation;
703
+ const entries = [];
704
+ if (kind === "delete") {
705
+ entries.push([overlayMarkerKey(id, OVERLAY_MARKER_DELETED), true]);
706
+ return entries;
707
+ }
708
+ if (kind === "create") {
709
+ entries.push([overlayMarkerKey(id, OVERLAY_MARKER_REPLACE), true]);
710
+ entries.push([overlayMarkerKey(id, OVERLAY_MARKER_DELETED), false]);
711
+ }
712
+ for (const [field, value] of Object.entries(mutation.fields ?? {})) {
713
+ entries.push([overlayFieldKey(id, field), value === void 0 ? null : value]);
714
+ }
715
+ for (const [field, members] of Object.entries(mutation.stringSetDeltas ?? {})) {
716
+ for (const [member, present] of Object.entries(members)) {
717
+ entries.push([overlayMemberKey(id, field, member), present]);
718
+ }
719
+ }
720
+ return entries;
721
+ }
722
+ function applyOverlayMutation(modelMap, mutation) {
723
+ if (mutation.kind === "create") {
724
+ clearStaleRecordOverlay(modelMap, mutation.id);
725
+ }
726
+ for (const [key, value] of encodeOverlayMutation(mutation)) {
727
+ modelMap.set(key, value);
728
+ }
729
+ }
730
+ function clearStaleRecordOverlay(modelMap, recordId) {
731
+ if (!modelMap.has(overlayMarkerKey(recordId, OVERLAY_MARKER_DELETED))) return;
732
+ const prefix = `${encodeOverlaySegment(recordId)}${OVERLAY_KEY_SEPARATOR}`;
733
+ const stale = [];
734
+ for (const key of modelMap.keys()) {
735
+ if (key.startsWith(prefix)) stale.push(key);
736
+ }
737
+ for (const key of stale) modelMap.delete(key);
738
+ }
739
+ function assertNoFormat2CompositeFields(modelName, recordId, fields) {
740
+ if (!fields) return;
741
+ for (const [field, value] of Object.entries(fields)) {
742
+ if (isYjsComposite(value)) {
743
+ throw new Format2CompositeFieldError(modelName, recordId, field);
744
+ }
745
+ }
746
+ }
747
+ function isFormat2StringSetYMap(value) {
748
+ if (!(value instanceof Y6.Map)) return false;
749
+ for (const member of value.values()) {
750
+ if (member !== true && member !== false) return false;
751
+ }
752
+ return true;
753
+ }
754
+ function format2StringSetMembers(value) {
755
+ if (!(value instanceof Y6.Map)) return [];
756
+ return Array.from(value.entries()).filter(([, present]) => present === true).map(([member]) => member);
757
+ }
758
+ var OVERLAY_KEY_SEPARATOR, OVERLAY_MARKER_REPLACE, OVERLAY_MARKER_DELETED, OVERLAY_MARKERS, Format2CompositeFieldError;
759
+ var init_largeDocuments = __esm({
760
+ "src/utils/largeDocuments.ts"() {
761
+ "use strict";
762
+ init_yjsValues();
763
+ OVERLAY_KEY_SEPARATOR = "/";
764
+ OVERLAY_MARKER_REPLACE = "_replace";
765
+ OVERLAY_MARKER_DELETED = "_deleted";
766
+ OVERLAY_MARKERS = /* @__PURE__ */ new Set([
767
+ OVERLAY_MARKER_REPLACE,
768
+ OVERLAY_MARKER_DELETED
769
+ ]);
770
+ Format2CompositeFieldError = class extends Error {
771
+ modelName;
772
+ recordId;
773
+ field;
774
+ constructor(modelName, recordId, field) {
775
+ super(
776
+ `Composite field values are not supported on large documents (format 2): ${modelName}.${field} (id=${recordId})`
777
+ );
778
+ this.name = "Format2CompositeFieldError";
779
+ this.modelName = modelName;
780
+ this.recordId = recordId;
781
+ this.field = field;
782
+ }
783
+ };
784
+ }
785
+ });
786
+
787
+ // src/models/OrmContext.ts
788
+ var OrmContext, defaultOrmContext, liveOrmContexts;
789
+ var init_OrmContext = __esm({
790
+ "src/models/OrmContext.ts"() {
791
+ "use strict";
792
+ OrmContext = class _OrmContext {
793
+ /** Declared before the field that reads it: static initialization is ordered. */
794
+ static instanceCounter = 0;
795
+ /**
796
+ * Which instance this is, for diagnostics. A process holding several clients
797
+ * needs a way to say WHICH one a warning is about, so every context takes the
798
+ * next number as it is created and keeps it for its lifetime.
799
+ */
800
+ instanceId = ++_OrmContext.instanceCounter;
801
+ /** The engine this instance's models read and write through. */
802
+ dbEngine = null;
803
+ connectedDocuments = /* @__PURE__ */ new Map();
804
+ /** `docId_modelName` → the Y.Map that document holds for that model. */
805
+ documentYMaps = /* @__PURE__ */ new Map();
806
+ /** `subscribe()` callbacks, by model name. */
807
+ listenersMap = /* @__PURE__ */ new Map();
808
+ // Default-document resolution
809
+ modelNameToDefaultDocId = /* @__PURE__ */ new Map();
810
+ globalDefaultDocId = void 0;
811
+ defaultDocChangedListeners = /* @__PURE__ */ new Set();
812
+ modelDocMappingChangedListeners = /* @__PURE__ */ new Set();
813
+ /** Format-2 ("large document", #2816) record stores by document id. */
814
+ format2Documents = /* @__PURE__ */ new Map();
815
+ /** Format-2 epoch moves in flight (#2816). */
816
+ format2Transitions = /* @__PURE__ */ new Map();
817
+ /**
818
+ * Nested-Y.Map stringset fields this context has already observed. Per
819
+ * context, not global: a Y.Doc reconnected to a fresh instance has to be
820
+ * observed again by that instance, and a global guard would silently
821
+ * suppress it.
822
+ */
823
+ observedStringSetMaps = /* @__PURE__ */ new WeakSet();
824
+ /**
825
+ * The registry this instance's schema lookups use. `null` on the standing
826
+ * default context, where callers fall back to the global singleton.
827
+ */
828
+ modelRegistry = null;
829
+ /** Model classes routed to this context. */
830
+ boundModels = /* @__PURE__ */ new Set();
831
+ /** True once {@link destroy} has run; bound classes keep pointing here. */
832
+ destroyed = false;
833
+ /** Yjs `unobserve` thunks by owning document id. */
834
+ observerDisposers = /* @__PURE__ */ new Map();
835
+ /** Teardown the owner (the document manager) registers for `destroy()`. */
836
+ teardownHooks = /* @__PURE__ */ new Set();
837
+ static NO_DOCUMENT_KEY = "__no_document__";
838
+ /**
839
+ * Record how to remove an observer this context installed. Every `observe()`
840
+ * in the ORM pairs with one of these: without it a torn-down instance's
841
+ * callbacks keep firing on a Y.Doc that outlives it.
842
+ */
843
+ registerObserverDisposer(docId, dispose) {
844
+ const key = docId ?? _OrmContext.NO_DOCUMENT_KEY;
845
+ const list = this.observerDisposers.get(key);
846
+ if (list) list.push(dispose);
847
+ else this.observerDisposers.set(key, [dispose]);
848
+ }
849
+ /** Remove every observer this context installed for `docId`. */
850
+ disposeObserversForDocument(docId) {
851
+ const list = this.observerDisposers.get(docId);
852
+ if (!list) return;
853
+ this.observerDisposers.delete(docId);
854
+ for (const dispose of list) {
855
+ try {
856
+ dispose();
857
+ } catch {
858
+ }
859
+ }
860
+ }
861
+ disposeAllObservers() {
862
+ const buckets = Array.from(this.observerDisposers.values());
863
+ this.observerDisposers.clear();
864
+ for (const list of buckets) {
865
+ for (const dispose of list) {
866
+ try {
867
+ dispose();
868
+ } catch {
869
+ }
870
+ }
871
+ }
872
+ }
873
+ /** Run `hook` as part of `destroy()` (the format-2 binding registers here). */
874
+ onDestroy(hook) {
875
+ this.teardownHooks.add(hook);
876
+ }
877
+ /** The record store for `docId`, or `null` when the document is format 1. */
878
+ getFormat2Store(docId) {
879
+ if (!docId) return null;
880
+ return this.format2Documents.get(docId) ?? null;
881
+ }
882
+ /**
883
+ * The epoch move a write to `docId` has to wait out (#2816, behavior 10), or
884
+ * `null` when none is in flight.
885
+ */
886
+ format2WritesHeld(docId) {
887
+ if (!docId) return null;
888
+ return this.format2Transitions.get(docId) ?? null;
889
+ }
890
+ /** Run an epoch move with `docId`'s local writes held back (#2816). */
891
+ async holdFormat2Writes(docId, run) {
892
+ const inFlight = this.format2Transitions.get(docId);
893
+ if (inFlight) await inFlight.catch(() => {
894
+ });
895
+ let release;
896
+ const held = new Promise((resolve) => {
897
+ release = resolve;
898
+ });
899
+ this.format2Transitions.set(docId, held);
900
+ try {
901
+ return await run();
902
+ } finally {
903
+ if (this.format2Transitions.get(docId) === held) {
904
+ this.format2Transitions.delete(docId);
905
+ }
906
+ release();
907
+ }
908
+ }
909
+ /** Notify `subscribe()` callbacks registered on this context for `modelName`. */
910
+ notifyListeners(modelName, log, onError) {
911
+ const listeners = this.listenersMap.get(modelName);
912
+ if (!listeners) return;
913
+ log?.(`Notifying ${listeners.size} listeners for ${modelName}`);
914
+ listeners.forEach((callback) => {
915
+ try {
916
+ callback();
917
+ } catch (e) {
918
+ onError?.(e);
919
+ }
920
+ });
921
+ }
922
+ // ---- Default-document resolution (the `BaseModel` statics delegate here) ----
923
+ setModelDefaultDocumentId(modelName, docId) {
924
+ const previous = this.modelNameToDefaultDocId.get(modelName);
925
+ this.modelNameToDefaultDocId.set(modelName, docId);
926
+ this.announceModelDocMapping(modelName, previous, docId);
927
+ }
928
+ removeModelDefaultDocumentId(modelName) {
929
+ const previous = this.modelNameToDefaultDocId.get(modelName);
930
+ this.modelNameToDefaultDocId.delete(modelName);
931
+ this.announceModelDocMapping(modelName, previous, void 0);
932
+ }
933
+ clearModelDefaultDocumentIds() {
934
+ const entries = Array.from(this.modelNameToDefaultDocId.entries());
935
+ this.modelNameToDefaultDocId.clear();
936
+ for (const [modelName, previous] of entries) {
937
+ this.announceModelDocMapping(modelName, previous, void 0);
938
+ }
939
+ }
940
+ setGlobalDefaultDocumentId(docId) {
941
+ const previous = this.globalDefaultDocId;
942
+ this.globalDefaultDocId = docId;
943
+ this.announceDefaultDoc(previous, docId);
944
+ }
945
+ clearGlobalDefaultDocumentId() {
946
+ const previous = this.globalDefaultDocId;
947
+ this.globalDefaultDocId = void 0;
948
+ this.announceDefaultDoc(previous, void 0);
949
+ }
950
+ getModelDefaultDocumentMapping() {
951
+ return Object.fromEntries(this.modelNameToDefaultDocId.entries());
952
+ }
953
+ getDocumentIdForModel(modelName) {
954
+ return this.modelNameToDefaultDocId.get(modelName);
955
+ }
956
+ getGlobalDefaultDocumentId() {
957
+ return this.globalDefaultDocId;
958
+ }
959
+ onDefaultDocChanged(listener) {
960
+ this.defaultDocChangedListeners.add(listener);
961
+ return () => this.defaultDocChangedListeners.delete(listener);
962
+ }
963
+ onModelDocMappingChanged(listener) {
964
+ this.modelDocMappingChangedListeners.add(listener);
965
+ return () => this.modelDocMappingChangedListeners.delete(listener);
966
+ }
967
+ /** Drop the model→document defaults that point at a disconnected document. */
968
+ clearMappingsForDocId(docId) {
969
+ const toRemove = [];
970
+ for (const [modelName, mappedDocId] of this.modelNameToDefaultDocId) {
971
+ if (mappedDocId === docId) toRemove.push(modelName);
972
+ }
973
+ for (const modelName of toRemove) {
974
+ this.removeModelDefaultDocumentId(modelName);
975
+ }
976
+ if (this.globalDefaultDocId === docId) {
977
+ this.clearGlobalDefaultDocumentId();
978
+ }
979
+ }
980
+ announceDefaultDoc(previous, current) {
981
+ for (const listener of this.defaultDocChangedListeners) {
982
+ try {
983
+ listener({ previous, current });
984
+ } catch {
985
+ }
986
+ }
987
+ }
988
+ announceModelDocMapping(modelName, previous, current) {
989
+ for (const listener of this.modelDocMappingChangedListeners) {
990
+ try {
991
+ listener({ modelName, previous, current });
992
+ } catch {
993
+ }
994
+ }
995
+ }
996
+ /**
997
+ * Tear this instance down: observers off, engine destroyed, state cleared.
998
+ *
999
+ * Deliberately does NOT unbind its model classes. They keep resolving this
1000
+ * context, whose engine is now null, so an operation through them throws the
1001
+ * ordinary not-initialized error instead of silently landing on whatever
1002
+ * other instance happens to be live (#3039, behavior 7). Only a later
1003
+ * `initJsBao` may rebind a class whose context is destroyed.
1004
+ */
1005
+ async destroy() {
1006
+ if (this.destroyed) return;
1007
+ this.destroyed = true;
1008
+ this.disposeAllObservers();
1009
+ const hooks = Array.from(this.teardownHooks);
1010
+ this.teardownHooks.clear();
1011
+ for (const hook of hooks) {
1012
+ try {
1013
+ await hook();
1014
+ } catch {
1015
+ }
1016
+ }
1017
+ const engine = this.dbEngine;
1018
+ this.dbEngine = null;
1019
+ if (engine) {
1020
+ try {
1021
+ await engine.destroy();
1022
+ } catch {
1023
+ }
1024
+ }
1025
+ this.connectedDocuments.clear();
1026
+ this.documentYMaps.clear();
1027
+ this.listenersMap.clear();
1028
+ this.modelNameToDefaultDocId.clear();
1029
+ this.globalDefaultDocId = void 0;
1030
+ this.defaultDocChangedListeners.clear();
1031
+ this.modelDocMappingChangedListeners.clear();
1032
+ this.format2Documents.clear();
1033
+ this.format2Transitions.clear();
1034
+ this.observedStringSetMaps = /* @__PURE__ */ new WeakSet();
1035
+ liveOrmContexts.delete(this);
1036
+ if (defaultOrmContext === this) {
1037
+ defaultOrmContext = new _OrmContext();
1038
+ }
1039
+ }
1040
+ };
1041
+ defaultOrmContext = new OrmContext();
1042
+ liveOrmContexts = /* @__PURE__ */ new Set();
1043
+ }
1044
+ });
1045
+
1046
+ // src/models/format2QueryProjection.ts
1047
+ var FORMAT2_QUERY_PROJECTION_DDL;
1048
+ var init_format2QueryProjection = __esm({
1049
+ "src/models/format2QueryProjection.ts"() {
1050
+ "use strict";
1051
+ FORMAT2_QUERY_PROJECTION_DDL = `
1052
+ CREATE TABLE IF NOT EXISTS _query_projection (
1053
+ doc_id TEXT NOT NULL,
1054
+ model TEXT NOT NULL,
1055
+ PRIMARY KEY (doc_id, model)
1056
+ )`.trim();
1057
+ }
1058
+ });
1059
+
1060
+ // src/models/format2Observer.ts
1061
+ import * as Y7 from "yjs";
1062
+ var init_format2Observer = __esm({
1063
+ "src/models/format2Observer.ts"() {
1064
+ "use strict";
1065
+ init_largeDocuments();
1066
+ }
1067
+ });
1068
+
1069
+ // src/models/format2ClientWrite.ts
1070
+ var init_format2ClientWrite = __esm({
1071
+ "src/models/format2ClientWrite.ts"() {
1072
+ "use strict";
1073
+ }
1074
+ });
1075
+
1076
+ // src/models/format2OfflineWindow.ts
1077
+ var DAY_MS;
1078
+ var init_format2OfflineWindow = __esm({
1079
+ "src/models/format2OfflineWindow.ts"() {
1080
+ "use strict";
1081
+ DAY_MS = 24 * 60 * 60 * 1e3;
1082
+ }
1083
+ });
1084
+
509
1085
  // src/models/BaseModel.ts
510
- import * as Y4 from "yjs";
511
- import { ulid as ulid2 } from "ulid";
1086
+ import * as Y8 from "yjs";
1087
+ import { ulid as ulid3 } from "ulid";
512
1088
  function generateULID() {
513
- return ulid2();
1089
+ return ulid3();
514
1090
  }
515
1091
  var init_BaseModel = __esm({
516
1092
  "src/models/BaseModel.ts"() {
@@ -521,7 +1097,15 @@ var init_BaseModel = __esm({
521
1097
  init_CursorManager();
522
1098
  init_sql();
523
1099
  init_metaSync();
1100
+ init_writeLifecycle();
524
1101
  init_yjsValues();
1102
+ init_uniqueIndex();
1103
+ init_largeDocuments();
1104
+ init_OrmContext();
1105
+ init_format2QueryProjection();
1106
+ init_format2Observer();
1107
+ init_format2ClientWrite();
1108
+ init_format2OfflineWindow();
525
1109
  registerFunctionDefault(generateULID, "generate_ulid");
526
1110
  }
527
1111
  });
@@ -540,6 +1124,7 @@ var init_ModelRegistry = __esm({
540
1124
  "use strict";
541
1125
  init_BaseModel();
542
1126
  init_relationshipManager();
1127
+ init_format2QueryProjection();
543
1128
  }
544
1129
  });
545
1130
 
@@ -575,6 +1160,32 @@ var DatabaseEngine = class {
575
1160
  getTableName(_modelName) {
576
1161
  throw new Error("Method not implemented.");
577
1162
  }
1163
+ /**
1164
+ * A SYNCHRONOUS SQL executor over this engine's database, used by the
1165
+ * large-document (format 2) record store. That store commits a merged
1166
+ * record row and its pending-operation entry as one transaction — the
1167
+ * barrier a `save()` resolves behind — which the async `query()` API cannot
1168
+ * express. Engines whose underlying SQLite is synchronous (better-sqlite3,
1169
+ * SQL.js) provide it; anything else leaves it undefined and cannot host a
1170
+ * large document.
1171
+ */
1172
+ createFormat2Sql;
1173
+ /**
1174
+ * Whether what {@link createFormat2Sql} writes survives this process.
1175
+ *
1176
+ * On a large document the merged record view IS the document: the Y.Doc
1177
+ * holds one epoch's overlay, and everything else — including a record a
1178
+ * `save()` has already resolved, and the pending write that still owes it to
1179
+ * the server — lives only in this store. An engine whose database is in
1180
+ * memory therefore cannot host one: a reload does not cost a cache, it costs
1181
+ * the document and the writes nobody has acknowledged yet.
1182
+ *
1183
+ * The platform is asked separately about its storage quota; this answers the
1184
+ * half only the engine knows. An engine that does not implement it is taken
1185
+ * at the platform's word, which is what keeps hosts with storage of their
1186
+ * own — a worker handed a file store, a test — working unchanged.
1187
+ */
1188
+ format2StorageIsDurable;
578
1189
  // Transaction support
579
1190
  async withTransaction(_callback) {
580
1191
  throw new Error("Method not implemented.");
@@ -1551,6 +2162,45 @@ var DurableObjectEngine = class extends JsonSchemaEngine {
1551
2162
  init_queryTypes();
1552
2163
  init_CursorManager();
1553
2164
  init_sql();
2165
+
2166
+ // src/query/recordRow.ts
2167
+ var PROJECTED_JSON_ALIAS_PREFIX = "_json_";
2168
+ function projectedJsonAlias(fieldName) {
2169
+ return `${PROJECTED_JSON_ALIAS_PREFIX}${fieldName}`;
2170
+ }
2171
+ function parseRecordRow(row) {
2172
+ const { _id, _type, _data, ...rest } = row;
2173
+ let parsed = {};
2174
+ if (_data) {
2175
+ try {
2176
+ parsed = JSON.parse(_data);
2177
+ } catch (e) {
2178
+ console.warn("Failed to parse _data:", e);
2179
+ }
2180
+ }
2181
+ return { id: _id, type: _type, ...parsed, ...decodeProjectedColumns(rest) };
2182
+ }
2183
+ function decodeProjectedColumns(columns) {
2184
+ const decoded = {};
2185
+ for (const [column, value] of Object.entries(columns)) {
2186
+ if (!column.startsWith(PROJECTED_JSON_ALIAS_PREFIX)) {
2187
+ decoded[column] = value;
2188
+ continue;
2189
+ }
2190
+ decoded[column.slice(PROJECTED_JSON_ALIAS_PREFIX.length)] = parseJsonColumn(value);
2191
+ }
2192
+ return decoded;
2193
+ }
2194
+ function parseJsonColumn(value) {
2195
+ if (typeof value !== "string") return value;
2196
+ try {
2197
+ return JSON.parse(value);
2198
+ } catch {
2199
+ return value;
2200
+ }
2201
+ }
2202
+
2203
+ // src/query/JsonQueryTranslator.ts
1554
2204
  init_patterns();
1555
2205
  var SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
1556
2206
  var JsonQueryTranslator = class {
@@ -1595,7 +2245,8 @@ var JsonQueryTranslator = class {
1595
2245
  const whereClause = this.translateFilter(filter);
1596
2246
  const orderClause = CursorManager.buildOrderClause(
1597
2247
  options?.sort,
1598
- (field) => this.getFieldSql(field)
2248
+ (field) => this.getFieldSql(field),
2249
+ options?.direction || 1
1599
2250
  );
1600
2251
  const limitClause = this.buildLimitClause(options);
1601
2252
  const paginationClause = this.buildPaginationClause(
@@ -1868,8 +2519,11 @@ var JsonQueryTranslator = class {
1868
2519
  };
1869
2520
  case "$ne":
1870
2521
  this.validateFieldValue(fieldName, value);
2522
+ if (value === null || value === void 0) {
2523
+ return { sql: `${fieldSql} IS NOT NULL`, params: [] };
2524
+ }
1871
2525
  return {
1872
- sql: `${fieldSql} != ?`,
2526
+ sql: `(${fieldSql} IS NULL OR ${fieldSql} != ?)`,
1873
2527
  params: [this.convertValueForSQLite(value)]
1874
2528
  };
1875
2529
  case "$gt":
@@ -1952,12 +2606,23 @@ var JsonQueryTranslator = class {
1952
2606
  if (value.length === 0) {
1953
2607
  return { sql: "", params: [] };
1954
2608
  }
1955
- const ninPlaceholders = value.map(() => "?").join(",");
1956
- const convertedNinValues = value.map(
2609
+ const ninHasNull = value.some((v) => v === null || v === void 0);
2610
+ const ninValues = value.filter((v) => v !== null && v !== void 0);
2611
+ const ninPlaceholders = ninValues.map(() => "?").join(",");
2612
+ const convertedNinValues = ninValues.map(
1957
2613
  (v) => this.convertValueForSQLite(v)
1958
2614
  );
2615
+ if (ninHasNull) {
2616
+ if (ninValues.length === 0) {
2617
+ return { sql: `${fieldSql} IS NOT NULL`, params: [] };
2618
+ }
2619
+ return {
2620
+ sql: `(${fieldSql} IS NOT NULL AND ${fieldSql} NOT IN (${ninPlaceholders}))`,
2621
+ params: convertedNinValues
2622
+ };
2623
+ }
1959
2624
  return {
1960
- sql: `${fieldSql} NOT IN (${ninPlaceholders})`,
2625
+ sql: `(${fieldSql} IS NULL OR ${fieldSql} NOT IN (${ninPlaceholders}))`,
1961
2626
  params: convertedNinValues
1962
2627
  };
1963
2628
  case "$exists":
@@ -2046,7 +2711,7 @@ var JsonQueryTranslator = class {
2046
2711
  for (const field of includeFields) {
2047
2712
  if (field === "id" || field === "type") continue;
2048
2713
  selectParts.push(
2049
- `json_extract(_data, '$.${field}') AS ${quoteIdentifier(field)}`
2714
+ `_data -> '$.${field}' AS ${quoteIdentifier(projectedJsonAlias(field))}`
2050
2715
  );
2051
2716
  }
2052
2717
  return selectParts.join(", ");
@@ -2233,6 +2898,13 @@ var JsonQueryTranslator = class {
2233
2898
  init_CursorManager();
2234
2899
  init_sql();
2235
2900
  import { ulid } from "ulid";
2901
+ var AtomicBatchAbort = class extends Error {
2902
+ constructor(failedIndex) {
2903
+ super(`atomic batch aborted at operation ${failedIndex}`);
2904
+ this.failedIndex = failedIndex;
2905
+ this.name = "AtomicBatchAbort";
2906
+ }
2907
+ };
2236
2908
  function checkReservedFieldName(fieldName) {
2237
2909
  if (fieldName === "id") return null;
2238
2910
  if (fieldName === "type") {
@@ -2277,6 +2949,35 @@ function applyAutoStamps(data, autoStamps, isNew) {
2277
2949
  }
2278
2950
  }
2279
2951
  }
2952
+ function collectAppliedStamps(data, autoStamps, isNew) {
2953
+ if (!autoStamps || !data) return void 0;
2954
+ const stamps = {};
2955
+ const take = (fields) => {
2956
+ if (!fields) return;
2957
+ for (const field of fields) {
2958
+ const value = data[field];
2959
+ if (typeof value === "number" && Number.isFinite(value)) {
2960
+ stamps[field] = value;
2961
+ }
2962
+ }
2963
+ };
2964
+ if (isNew) take(autoStamps.create);
2965
+ take(autoStamps.update);
2966
+ return Object.keys(stamps).length > 0 ? stamps : void 0;
2967
+ }
2968
+ function collectAppliedValues(data, fieldNames) {
2969
+ if (!data || !fieldNames || fieldNames.length === 0) return void 0;
2970
+ const out = {};
2971
+ for (const name of fieldNames) {
2972
+ const value = data[name];
2973
+ if (value !== void 0) out[name] = value;
2974
+ }
2975
+ return Object.keys(out).length > 0 ? out : void 0;
2976
+ }
2977
+ function mergeAppliedFields(stamps, applied) {
2978
+ if (!stamps && !applied) return void 0;
2979
+ return { ...stamps ?? {}, ...applied ?? {} };
2980
+ }
2280
2981
  function createDatabaseDO(config = {}) {
2281
2982
  const hooks = config.hooks;
2282
2983
  return class DocumentDO {
@@ -2434,6 +3135,16 @@ function createDatabaseDO(config = {}) {
2434
3135
  hasMore,
2435
3136
  isFirstPage
2436
3137
  );
3138
+ if (options?.projection && Object.keys(options.projection).length > 0) {
3139
+ const isExclusion = Object.values(options.projection).some(
3140
+ (v) => v === 0
3141
+ );
3142
+ if (isExclusion) {
3143
+ data = data.map(
3144
+ (rec) => this._applyProjection(rec, options.projection)
3145
+ );
3146
+ }
3147
+ }
2437
3148
  const response = {
2438
3149
  data,
2439
3150
  hasMore,
@@ -2536,6 +3247,7 @@ function createDatabaseDO(config = {}) {
2536
3247
  }
2537
3248
  const isNew = !this._engine.recordExists(modelName, id);
2538
3249
  applyAutoStamps(data, autoStamps, isNew);
3250
+ let hookAppliedNames;
2539
3251
  if (hooks?.beforeSave) {
2540
3252
  const ctx = {
2541
3253
  modelName,
@@ -2551,6 +3263,7 @@ function createDatabaseDO(config = {}) {
2551
3263
  if (!result.allow) {
2552
3264
  return this._errorResponse(result.reason || "Save denied", 403);
2553
3265
  }
3266
+ hookAppliedNames = result.serverAppliedFieldNames;
2554
3267
  }
2555
3268
  const existingForUnique = this._engine.execSqlSync(
2556
3269
  "SELECT _data FROM records WHERE _id = ? AND _type = ?",
@@ -2580,6 +3293,11 @@ function createDatabaseDO(config = {}) {
2580
3293
  }
2581
3294
  this._engine.trackModelFields(modelName, data);
2582
3295
  const response = { success: true, id };
3296
+ const appliedFields = mergeAppliedFields(
3297
+ collectAppliedStamps(data, autoStamps, isNew),
3298
+ collectAppliedValues(data, hookAppliedNames)
3299
+ );
3300
+ if (appliedFields) response.appliedFields = appliedFields;
2583
3301
  return Response.json(response);
2584
3302
  }
2585
3303
  /** @internal — Partial update: merge provided fields into existing record */
@@ -2609,6 +3327,7 @@ function createDatabaseDO(config = {}) {
2609
3327
  );
2610
3328
  }
2611
3329
  applyAutoStamps(data, autoStamps, false);
3330
+ let hookAppliedNames;
2612
3331
  if (hooks?.beforeSave) {
2613
3332
  const ctx = {
2614
3333
  modelName,
@@ -2624,6 +3343,7 @@ function createDatabaseDO(config = {}) {
2624
3343
  if (!result.allow) {
2625
3344
  return this._errorResponse(result.reason || "Save denied", 403);
2626
3345
  }
3346
+ hookAppliedNames = result.serverAppliedFieldNames;
2627
3347
  }
2628
3348
  const existing = this._engine.execSqlSync(
2629
3349
  "SELECT _data FROM records WHERE _id = ? AND _type = ?",
@@ -2647,6 +3367,11 @@ function createDatabaseDO(config = {}) {
2647
3367
  }
2648
3368
  this._engine.trackModelFields(modelName, data);
2649
3369
  const response = { success: true, id };
3370
+ const appliedFields = mergeAppliedFields(
3371
+ collectAppliedStamps(data, autoStamps, false),
3372
+ collectAppliedValues(data, hookAppliedNames)
3373
+ );
3374
+ if (appliedFields) response.appliedFields = appliedFields;
2650
3375
  return Response.json(response);
2651
3376
  }
2652
3377
  /** @internal */
@@ -2688,7 +3413,7 @@ function createDatabaseDO(config = {}) {
2688
3413
  /** @internal — Execute multiple save/patch/delete operations in a single transaction */
2689
3414
  async _handleBatch(request, docId) {
2690
3415
  const body = await request.json();
2691
- const { operations } = body;
3416
+ const { operations, atomic = false } = body;
2692
3417
  if (!operations || !Array.isArray(operations) || operations.length === 0) {
2693
3418
  return this._errorResponse("operations array is required and must not be empty", 400);
2694
3419
  }
@@ -2696,6 +3421,8 @@ function createDatabaseDO(config = {}) {
2696
3421
  const resolvedBatchIds = /* @__PURE__ */ new Map();
2697
3422
  const opErrors = /* @__PURE__ */ new Map();
2698
3423
  const batchCreatedIds = /* @__PURE__ */ new Set();
3424
+ const batchIsNew = /* @__PURE__ */ new Map();
3425
+ const batchAppliedNames = /* @__PURE__ */ new Map();
2699
3426
  for (let i = 0; i < operations.length; i++) {
2700
3427
  const op = operations[i];
2701
3428
  if (!op.modelName || !op.id && !(op.op === "save" && op.upsertOn) || !op.op) {
@@ -2769,6 +3496,7 @@ function createDatabaseDO(config = {}) {
2769
3496
  if (op.op === "save" && isNew && effectiveId) {
2770
3497
  batchCreatedIds.add(`${op.modelName}/${effectiveId}`);
2771
3498
  }
3499
+ batchIsNew.set(i, isNew);
2772
3500
  applyAutoStamps(op.data, op.autoStamps, isNew);
2773
3501
  if (hooks?.beforeSave) {
2774
3502
  const ctx = {
@@ -2784,6 +3512,8 @@ function createDatabaseDO(config = {}) {
2784
3512
  const result = await hooks.beforeSave(ctx);
2785
3513
  if (!result.allow) {
2786
3514
  hookDenials.set(i, result.reason || "Save denied");
3515
+ } else if (result.serverAppliedFieldNames) {
3516
+ batchAppliedNames.set(i, result.serverAppliedFieldNames);
2787
3517
  }
2788
3518
  }
2789
3519
  } else if (op.op === "delete") {
@@ -2832,205 +3562,263 @@ function createDatabaseDO(config = {}) {
2832
3562
  }
2833
3563
  const results = [];
2834
3564
  const fieldsToTrack = [];
2835
- this._engine.transactionSync(() => {
2836
- for (let i = 0; i < operations.length; i++) {
2837
- const op = operations[i];
2838
- if (hookDenials.has(i)) {
2839
- const effectiveId = resolvedBatchIds.get(i) || op.id || "";
2840
- results.push({
2841
- success: false,
2842
- id: effectiveId,
2843
- error: hookDenials.get(i)
2844
- });
2845
- continue;
2846
- }
2847
- if (opErrors.has(i)) {
2848
- results.push({
2849
- success: false,
2850
- id: op.id || "",
2851
- error: opErrors.get(i)
2852
- });
2853
- continue;
2854
- }
2855
- const conditionId = resolvedBatchIds.get(i) || op.id;
2856
- if (op.condition && !this._checkCondition(op.modelName, conditionId, op.condition)) {
2857
- results.push({
2858
- success: false,
2859
- id: conditionId,
2860
- error: `Condition not met for ${op.modelName}/${conditionId}`
2861
- });
2862
- continue;
2863
- }
2864
- if (op.op === "save") {
2865
- let opId = resolvedBatchIds.get(i) || op.id;
2866
- if (op.ifNotExists && this._engine.recordExists(op.modelName, opId)) {
2867
- results.push({
2868
- success: false,
2869
- id: opId,
2870
- error: `Record ${op.modelName}/${opId} already exists`
3565
+ const pushFailure = (index, failure) => {
3566
+ results.push({
3567
+ success: false,
3568
+ id: failure.id,
3569
+ error: failure.error,
3570
+ status: failure.status,
3571
+ code: failure.code
3572
+ });
3573
+ if (atomic) throw new AtomicBatchAbort(index);
3574
+ };
3575
+ try {
3576
+ this._engine.transactionSync(() => {
3577
+ for (let i = 0; i < operations.length; i++) {
3578
+ const op = operations[i];
3579
+ if (hookDenials.has(i)) {
3580
+ const effectiveId = resolvedBatchIds.get(i) || op.id || "";
3581
+ pushFailure(i, {
3582
+ id: effectiveId,
3583
+ error: hookDenials.get(i),
3584
+ status: 403,
3585
+ code: "HOOK_DENIED"
2871
3586
  });
2872
3587
  continue;
2873
3588
  }
2874
- if (op.data) {
2875
- const existingForUnique = this._engine.execSqlSync(
2876
- "SELECT _data FROM records WHERE _id = ? AND _type = ?",
2877
- [opId, op.modelName]
2878
- );
2879
- let dataForUnique = op.data;
2880
- if (existingForUnique.length > 0) {
2881
- const existingData = JSON.parse(existingForUnique[0]._data);
2882
- dataForUnique = { ...existingData, ...op.data };
2883
- }
2884
- const violation = this._engine.checkUniqueConstraints(
2885
- op.modelName,
2886
- opId,
2887
- dataForUnique
2888
- );
2889
- if (violation) {
2890
- results.push({
2891
- success: false,
3589
+ if (opErrors.has(i)) {
3590
+ pushFailure(i, {
3591
+ id: op.id || "",
3592
+ error: opErrors.get(i),
3593
+ status: 400,
3594
+ code: "VALIDATION_ERROR"
3595
+ });
3596
+ continue;
3597
+ }
3598
+ const conditionId = resolvedBatchIds.get(i) || op.id;
3599
+ if (op.condition && !this._checkCondition(op.modelName, conditionId, op.condition)) {
3600
+ pushFailure(i, {
3601
+ id: conditionId,
3602
+ error: `Condition not met for ${op.modelName}/${conditionId}`,
3603
+ status: 409,
3604
+ code: "CONDITION_NOT_MET"
3605
+ });
3606
+ continue;
3607
+ }
3608
+ if (op.op === "save") {
3609
+ let opId = resolvedBatchIds.get(i) || op.id;
3610
+ if (op.ifNotExists && this._engine.recordExists(op.modelName, opId)) {
3611
+ pushFailure(i, {
2892
3612
  id: opId,
2893
- error: violation
3613
+ error: `Record ${op.modelName}/${opId} already exists`,
3614
+ status: 409,
3615
+ code: "ALREADY_EXISTS"
2894
3616
  });
2895
3617
  continue;
2896
3618
  }
2897
- }
2898
- if (op.stringSets && Object.keys(op.stringSets).length > 0) {
2899
- const { id: _id, ...fieldsForJson } = op.data;
2900
- const jsonFields = { ...fieldsForJson, ...op.stringSets };
2901
- const dataJson = JSON.stringify(jsonFields);
2902
- this._engine.execSqlSync(
2903
- "INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
2904
- [opId, op.modelName, dataJson]
2905
- );
2906
- for (const [fieldName, values] of Object.entries(op.stringSets)) {
2907
- this._engine.execSqlSync(
2908
- "DELETE FROM stringset_index WHERE _type = ? AND _record_id = ? AND field = ?",
2909
- [op.modelName, opId, fieldName]
3619
+ if (op.data) {
3620
+ const existingForUnique = this._engine.execSqlSync(
3621
+ "SELECT _data FROM records WHERE _id = ? AND _type = ?",
3622
+ [opId, op.modelName]
2910
3623
  );
2911
- for (const value of values) {
2912
- this._engine.execSqlSync(
2913
- "INSERT OR IGNORE INTO stringset_index (_record_id, _type, field, value) VALUES (?, ?, ?, ?)",
2914
- [opId, op.modelName, fieldName, value]
2915
- );
3624
+ let dataForUnique = op.data;
3625
+ if (existingForUnique.length > 0) {
3626
+ const existingData = JSON.parse(existingForUnique[0]._data);
3627
+ dataForUnique = { ...existingData, ...op.data };
2916
3628
  }
2917
- }
2918
- } else {
2919
- const { id: _id, ...fieldsForJson } = op.data;
2920
- const dataJson = JSON.stringify(fieldsForJson);
2921
- this._engine.execSqlSync(
2922
- "INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
2923
- [opId, op.modelName, dataJson]
2924
- );
2925
- }
2926
- if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
2927
- results.push({ success: true, id: opId });
2928
- } else if (op.op === "patch") {
2929
- if (!this._engine.recordExists(op.modelName, op.id)) {
2930
- results.push({
2931
- success: false,
2932
- id: op.id,
2933
- error: `Record ${op.modelName}/${op.id} not found`
2934
- });
2935
- continue;
2936
- }
2937
- if (op.data) {
2938
- const existing = this._engine.execSqlSync(
2939
- "SELECT _data FROM records WHERE _id = ? AND _type = ?",
2940
- [op.id, op.modelName]
2941
- );
2942
- if (existing.length > 0) {
2943
- const existingData = JSON.parse(existing[0]._data);
2944
- const mergedData = { ...existingData, ...op.data };
2945
3629
  const violation = this._engine.checkUniqueConstraints(
2946
3630
  op.modelName,
2947
- op.id,
2948
- mergedData
3631
+ opId,
3632
+ dataForUnique
2949
3633
  );
2950
3634
  if (violation) {
2951
- results.push({
2952
- success: false,
2953
- id: op.id,
2954
- error: violation
3635
+ pushFailure(i, {
3636
+ id: opId,
3637
+ error: violation,
3638
+ status: 409,
3639
+ code: "UNIQUE_VIOLATION"
2955
3640
  });
2956
3641
  continue;
2957
3642
  }
2958
3643
  }
2959
- }
2960
- const found = this._engine.patchRecordRaw(
2961
- op.modelName,
2962
- op.id,
2963
- op.data,
2964
- op.stringSets
2965
- );
2966
- if (!found) {
2967
- results.push({
2968
- success: false,
2969
- id: op.id,
2970
- error: `Record ${op.modelName}/${op.id} not found`
2971
- });
2972
- } else {
3644
+ if (op.stringSets && Object.keys(op.stringSets).length > 0) {
3645
+ const { id: _id, ...fieldsForJson } = op.data;
3646
+ const jsonFields = { ...fieldsForJson, ...op.stringSets };
3647
+ const dataJson = JSON.stringify(jsonFields);
3648
+ this._engine.execSqlSync(
3649
+ "INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
3650
+ [opId, op.modelName, dataJson]
3651
+ );
3652
+ for (const [fieldName, values] of Object.entries(op.stringSets)) {
3653
+ this._engine.execSqlSync(
3654
+ "DELETE FROM stringset_index WHERE _type = ? AND _record_id = ? AND field = ?",
3655
+ [op.modelName, opId, fieldName]
3656
+ );
3657
+ for (const value of values) {
3658
+ this._engine.execSqlSync(
3659
+ "INSERT OR IGNORE INTO stringset_index (_record_id, _type, field, value) VALUES (?, ?, ?, ?)",
3660
+ [opId, op.modelName, fieldName, value]
3661
+ );
3662
+ }
3663
+ }
3664
+ } else {
3665
+ const { id: _id, ...fieldsForJson } = op.data;
3666
+ const dataJson = JSON.stringify(fieldsForJson);
3667
+ this._engine.execSqlSync(
3668
+ "INSERT INTO records (_id, _type, _data) VALUES (?, ?, ?) ON CONFLICT(_type, _id) DO UPDATE SET _data = json_patch(records._data, excluded._data)",
3669
+ [opId, op.modelName, dataJson]
3670
+ );
3671
+ }
2973
3672
  if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
3673
+ const saveResult = { success: true, id: opId };
3674
+ const saveApplied = mergeAppliedFields(
3675
+ collectAppliedStamps(
3676
+ op.data,
3677
+ op.autoStamps,
3678
+ batchIsNew.get(i) ?? false
3679
+ ),
3680
+ collectAppliedValues(op.data, batchAppliedNames.get(i))
3681
+ );
3682
+ if (saveApplied) saveResult.appliedFields = saveApplied;
3683
+ results.push(saveResult);
3684
+ } else if (op.op === "patch") {
3685
+ if (!this._engine.recordExists(op.modelName, op.id)) {
3686
+ pushFailure(i, {
3687
+ id: op.id,
3688
+ error: `Record ${op.modelName}/${op.id} not found`,
3689
+ status: 404,
3690
+ code: "NOT_FOUND"
3691
+ });
3692
+ continue;
3693
+ }
3694
+ if (op.data) {
3695
+ const existing = this._engine.execSqlSync(
3696
+ "SELECT _data FROM records WHERE _id = ? AND _type = ?",
3697
+ [op.id, op.modelName]
3698
+ );
3699
+ if (existing.length > 0) {
3700
+ const existingData = JSON.parse(existing[0]._data);
3701
+ const mergedData = { ...existingData, ...op.data };
3702
+ const violation = this._engine.checkUniqueConstraints(
3703
+ op.modelName,
3704
+ op.id,
3705
+ mergedData
3706
+ );
3707
+ if (violation) {
3708
+ pushFailure(i, {
3709
+ id: op.id,
3710
+ error: violation,
3711
+ status: 409,
3712
+ code: "UNIQUE_VIOLATION"
3713
+ });
3714
+ continue;
3715
+ }
3716
+ }
3717
+ }
3718
+ const found = this._engine.patchRecordRaw(
3719
+ op.modelName,
3720
+ op.id,
3721
+ op.data,
3722
+ op.stringSets
3723
+ );
3724
+ if (!found) {
3725
+ pushFailure(i, {
3726
+ id: op.id,
3727
+ error: `Record ${op.modelName}/${op.id} not found`,
3728
+ status: 404,
3729
+ code: "NOT_FOUND"
3730
+ });
3731
+ } else {
3732
+ if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
3733
+ const patchResult = {
3734
+ success: true,
3735
+ id: op.id
3736
+ };
3737
+ const patchApplied = mergeAppliedFields(
3738
+ collectAppliedStamps(op.data, op.autoStamps, false),
3739
+ collectAppliedValues(op.data, batchAppliedNames.get(i))
3740
+ );
3741
+ if (patchApplied) patchResult.appliedFields = patchApplied;
3742
+ results.push(patchResult);
3743
+ }
3744
+ } else if (op.op === "delete") {
3745
+ this._engine.execSqlSync(
3746
+ "DELETE FROM records WHERE _id = ? AND _type = ?",
3747
+ [op.id, op.modelName]
3748
+ );
3749
+ this._engine.execSqlSync(
3750
+ "DELETE FROM stringset_index WHERE _record_id = ? AND _type = ?",
3751
+ [op.id, op.modelName]
3752
+ );
3753
+ results.push({ success: true, id: op.id });
3754
+ } else if (op.op === "increment") {
3755
+ const newValues = this._engine.incrementFieldsRaw(
3756
+ op.modelName,
3757
+ op.id,
3758
+ op.fields
3759
+ );
3760
+ if (!newValues) {
3761
+ pushFailure(i, {
3762
+ id: op.id,
3763
+ error: `Record ${op.modelName}/${op.id} not found`,
3764
+ status: 404,
3765
+ code: "NOT_FOUND"
3766
+ });
3767
+ } else {
3768
+ results.push({ success: true, id: op.id, values: newValues });
3769
+ }
3770
+ } else if (op.op === "addToSet") {
3771
+ if (!this._engine.recordExists(op.modelName, op.id)) {
3772
+ pushFailure(i, {
3773
+ id: op.id,
3774
+ error: `Record ${op.modelName}/${op.id} not found`,
3775
+ status: 404,
3776
+ code: "NOT_FOUND"
3777
+ });
3778
+ continue;
3779
+ }
3780
+ this._engine.addToStringSetsRaw(
3781
+ op.modelName,
3782
+ op.id,
3783
+ op.stringSets
3784
+ );
3785
+ results.push({ success: true, id: op.id });
3786
+ } else if (op.op === "removeFromSet") {
3787
+ if (!this._engine.recordExists(op.modelName, op.id)) {
3788
+ pushFailure(i, {
3789
+ id: op.id,
3790
+ error: `Record ${op.modelName}/${op.id} not found`,
3791
+ status: 404,
3792
+ code: "NOT_FOUND"
3793
+ });
3794
+ continue;
3795
+ }
3796
+ this._engine.removeFromStringSetsRaw(
3797
+ op.modelName,
3798
+ op.id,
3799
+ op.stringSets
3800
+ );
2974
3801
  results.push({ success: true, id: op.id });
2975
3802
  }
2976
- } else if (op.op === "delete") {
2977
- this._engine.execSqlSync(
2978
- "DELETE FROM records WHERE _id = ? AND _type = ?",
2979
- [op.id, op.modelName]
2980
- );
2981
- this._engine.execSqlSync(
2982
- "DELETE FROM stringset_index WHERE _record_id = ? AND _type = ?",
2983
- [op.id, op.modelName]
2984
- );
2985
- results.push({ success: true, id: op.id });
2986
- } else if (op.op === "increment") {
2987
- const newValues = this._engine.incrementFieldsRaw(
2988
- op.modelName,
2989
- op.id,
2990
- op.fields
2991
- );
2992
- if (!newValues) {
2993
- results.push({
2994
- success: false,
2995
- id: op.id,
2996
- error: `Record ${op.modelName}/${op.id} not found`
2997
- });
2998
- } else {
2999
- results.push({ success: true, id: op.id, values: newValues });
3000
- }
3001
- } else if (op.op === "addToSet") {
3002
- if (!this._engine.recordExists(op.modelName, op.id)) {
3003
- results.push({
3004
- success: false,
3005
- id: op.id,
3006
- error: `Record ${op.modelName}/${op.id} not found`
3007
- });
3008
- continue;
3009
- }
3010
- this._engine.addToStringSetsRaw(
3011
- op.modelName,
3012
- op.id,
3013
- op.stringSets
3014
- );
3015
- results.push({ success: true, id: op.id });
3016
- } else if (op.op === "removeFromSet") {
3017
- if (!this._engine.recordExists(op.modelName, op.id)) {
3018
- results.push({
3019
- success: false,
3020
- id: op.id,
3021
- error: `Record ${op.modelName}/${op.id} not found`
3022
- });
3023
- continue;
3024
- }
3025
- this._engine.removeFromStringSetsRaw(
3026
- op.modelName,
3027
- op.id,
3028
- op.stringSets
3029
- );
3030
- results.push({ success: true, id: op.id });
3031
3803
  }
3804
+ });
3805
+ } catch (err) {
3806
+ if (err instanceof AtomicBatchAbort) {
3807
+ const failed = results[err.failedIndex];
3808
+ const failedOp = operations[err.failedIndex];
3809
+ console.warn(
3810
+ `[js-bao batch] atomic rollback: operation ${err.failedIndex} (${failedOp?.op}) failed \u2014 ${failed?.error ?? "unknown error"}`
3811
+ );
3812
+ const errorBody = {
3813
+ error: `Operation ${err.failedIndex} (${failedOp?.op}) failed: ${failed?.error ?? "unknown error"}`,
3814
+ failedIndex: err.failedIndex,
3815
+ op: failedOp?.op,
3816
+ code: failed?.code
3817
+ };
3818
+ return Response.json(errorBody, { status: failed?.status ?? 409 });
3032
3819
  }
3033
- });
3820
+ throw err;
3821
+ }
3034
3822
  for (const entry of fieldsToTrack) {
3035
3823
  this._engine.trackModelFields(entry.modelName, entry.data);
3036
3824
  }
@@ -3400,6 +4188,18 @@ function createDatabaseDO(config = {}) {
3400
4188
  }
3401
4189
  return facetResult;
3402
4190
  }
4191
+ if (aggOptions.groupBy.length === 0) {
4192
+ const row = results[0];
4193
+ const flat = {};
4194
+ for (const op of aggOptions.operations) {
4195
+ if (op.type === "count") {
4196
+ flat.count = row.count;
4197
+ } else {
4198
+ flat[`${op.type}_${op.field}`] = row[`${op.type}_${op.field}`];
4199
+ }
4200
+ }
4201
+ return flat;
4202
+ }
3403
4203
  const nestedResult = {};
3404
4204
  const aliasLookup = /* @__PURE__ */ new Map();
3405
4205
  for (const entry of aliasMap) {
@@ -3727,16 +4527,7 @@ function createDatabaseDO(config = {}) {
3727
4527
  }
3728
4528
  /** @internal */
3729
4529
  _parseRow(row) {
3730
- const { _id, _type, _data, ...rest } = row;
3731
- let parsed = {};
3732
- if (_data) {
3733
- try {
3734
- parsed = JSON.parse(_data);
3735
- } catch (e) {
3736
- console.warn("Failed to parse _data:", e);
3737
- }
3738
- }
3739
- return { id: _id, type: _type, ...parsed, ...rest };
4530
+ return parseRecordRow(row);
3740
4531
  }
3741
4532
  // ─── Include (Related Data Loading) ──────────────────────────────
3742
4533
  /** @internal — Validate an IncludeSpec, returning an error message or null */
@@ -3866,8 +4657,9 @@ function createDatabaseDO(config = {}) {
3866
4657
  spec.filter,
3867
4658
  { projection: spec.projection }
3868
4659
  );
4660
+ const relatedProjected = spec.projection ? related.map((r) => this._applyProjection(r, spec.projection)) : related;
3869
4661
  const lookupMap = /* @__PURE__ */ new Map();
3870
- for (const r of related) {
4662
+ for (const r of relatedProjected) {
3871
4663
  lookupMap.set(r.id, r);
3872
4664
  }
3873
4665
  for (const rec of records) {
@@ -3900,8 +4692,9 @@ function createDatabaseDO(config = {}) {
3900
4692
  spec.filter,
3901
4693
  { projection: spec.projection }
3902
4694
  );
4695
+ const targetsProjected = spec.projection ? targets.map((t) => this._applyProjection(t, spec.projection)) : targets;
3903
4696
  const targetLookup = /* @__PURE__ */ new Map();
3904
- for (const t of targets) {
4697
+ for (const t of targetsProjected) {
3905
4698
  targetLookup.set(t.id, t);
3906
4699
  }
3907
4700
  for (const rec of records) {
@@ -4045,6 +4838,14 @@ function createDatabaseDO(config = {}) {
4045
4838
  return allResults;
4046
4839
  }
4047
4840
  /** @internal — Apply projection to a single record */
4841
+ // Apply a projection to a single record. Assumes the projection has
4842
+ // already been validated upstream (JsonQueryTranslator.validateProjection /
4843
+ // buildSelectClause reject mixed inclusion+exclusion): `isInclusion` treats
4844
+ // "any value === 1" as inclusion mode, so an unvalidated mixed projection
4845
+ // would silently ignore the excludes. `id` and `type` are system-identity
4846
+ // fields and are always kept in BOTH modes (a record without its `id` is
4847
+ // unusable downstream). `_meta` is NOT forced — an explicit `{ _meta: 0 }`
4848
+ // is honored.
4048
4849
  _applyProjection(record, projection) {
4049
4850
  const fields = Object.entries(projection);
4050
4851
  if (fields.length === 0) return record;
@@ -4063,7 +4864,7 @@ function createDatabaseDO(config = {}) {
4063
4864
  } else {
4064
4865
  const result = { ...record };
4065
4866
  for (const [f, v] of fields) {
4066
- if (v === 0) delete result[f];
4867
+ if (v === 0 && f !== "id" && f !== "type") delete result[f];
4067
4868
  }
4068
4869
  return result;
4069
4870
  }
@@ -4154,6 +4955,12 @@ function discoverSchema(yDoc) {
4154
4955
  }
4155
4956
  return { models };
4156
4957
  }
4958
+ function discoverDeclaredModel(yDoc, modelName) {
4959
+ const key = `_meta_${modelName}`;
4960
+ if (!yDoc.share.has(key)) return void 0;
4961
+ const map = materializeMap(yDoc, key);
4962
+ return map ? readModelMeta(map) : void 0;
4963
+ }
4157
4964
  function discoverModelNames(yDoc) {
4158
4965
  const names = [];
4159
4966
  for (const key of yDoc.share.keys()) {
@@ -4341,6 +5148,7 @@ import { parse as parseToml } from "smol-toml";
4341
5148
  // src/models/schema.ts
4342
5149
  init_BaseModel();
4343
5150
  init_ModelRegistry();
5151
+ init_uniqueIndex();
4344
5152
  function defineModelSchema(input) {
4345
5153
  const { name, fields } = input;
4346
5154
  const options = {
@@ -4383,7 +5191,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
4383
5191
  for (const [fieldName, options] of fields.entries()) {
4384
5192
  if (options.unique) {
4385
5193
  resolved.push({
4386
- name: `${modelName}_${fieldName}_unique`,
5194
+ name: fieldUniqueConstraintName(modelName, fieldName),
4387
5195
  fields: [fieldName]
4388
5196
  });
4389
5197
  }
@@ -4631,24 +5439,302 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
4631
5439
  // src/cloudflare-do.ts
4632
5440
  init_metaSync();
4633
5441
  init_sql();
5442
+ init_largeDocuments();
5443
+
5444
+ // src/utils/overlaySql.ts
5445
+ init_sql();
5446
+ var DEFAULT_OVERLAY_TABLES = {
5447
+ records: "records",
5448
+ stringSetIndex: "stringset_index"
5449
+ };
5450
+ var statementCache = /* @__PURE__ */ new Map();
5451
+ function statementsFor(tables) {
5452
+ const cacheKey = `${tables.records}\0${tables.stringSetIndex}`;
5453
+ const cached = statementCache.get(cacheKey);
5454
+ if (cached) return cached;
5455
+ assertValidIdentifier(tables.records, "overlay records table");
5456
+ assertValidIdentifier(tables.stringSetIndex, "overlay stringset table");
5457
+ const records = tables.records;
5458
+ const members = tables.stringSetIndex;
5459
+ const statements = {
5460
+ deleteRecord: `DELETE FROM ${records} WHERE _type = ? AND _id = ?`,
5461
+ deleteAllMembers: `DELETE FROM ${members} WHERE _type = ? AND _record_id = ?`,
5462
+ deleteMember: `DELETE FROM ${members} WHERE _type = ? AND _record_id = ? AND field = ? AND value = ?`,
5463
+ insertMember: `INSERT OR IGNORE INTO ${members} (_record_id, _type, field, value) VALUES (?, ?, ?, ?)`,
5464
+ selectMembers: `SELECT value FROM ${members} WHERE _type = ? AND _record_id = ? AND field = ? ORDER BY rowid`,
5465
+ mergeUpsert: `
5466
+ INSERT INTO ${records} (_id, _type, _data)
5467
+ VALUES (?, ?, json_patch('{}', ?))
5468
+ ON CONFLICT(_type, _id) DO UPDATE SET
5469
+ _data = json_patch(${records}._data, ?)`.trim(),
5470
+ replaceUpsert: `
5471
+ INSERT INTO ${records} (_id, _type, _data)
5472
+ VALUES (?, ?, json_patch('{}', ?))
5473
+ ON CONFLICT(_type, _id) DO UPDATE SET
5474
+ _data = json_patch('{}', ?)`.trim()
5475
+ };
5476
+ statementCache.set(cacheKey, statements);
5477
+ return statements;
5478
+ }
5479
+ function projectOverlayEntry(sql, modelName, entry, tables = DEFAULT_OVERLAY_TABLES) {
5480
+ const id = entry.id;
5481
+ const {
5482
+ deleteRecord: DELETE_RECORD,
5483
+ deleteAllMembers: DELETE_ALL_MEMBERS,
5484
+ deleteMember: DELETE_MEMBER,
5485
+ insertMember: INSERT_MEMBER,
5486
+ selectMembers: SELECT_MEMBERS,
5487
+ mergeUpsert: MERGE_UPSERT,
5488
+ replaceUpsert: REPLACE_UPSERT
5489
+ } = statementsFor(tables);
5490
+ if (entry.deleted) {
5491
+ sql.exec(DELETE_RECORD, modelName, id);
5492
+ sql.exec(DELETE_ALL_MEMBERS, modelName, id);
5493
+ return;
5494
+ }
5495
+ const touchedFields = Object.keys(entry.fields);
5496
+ const touchedSets = Object.keys(entry.stringSets);
5497
+ if (!entry.replace && touchedFields.length === 0 && touchedSets.length === 0) {
5498
+ return;
5499
+ }
5500
+ if (entry.replace) {
5501
+ sql.exec(DELETE_ALL_MEMBERS, modelName, id);
5502
+ }
5503
+ const patch = { ...entry.fields, id };
5504
+ for (const [field, members] of Object.entries(entry.stringSets)) {
5505
+ for (const [member, present] of Object.entries(members)) {
5506
+ if (present) sql.exec(INSERT_MEMBER, id, modelName, field, member);
5507
+ else sql.exec(DELETE_MEMBER, modelName, id, field, member);
5508
+ }
5509
+ const rows = Array.from(sql.exec(SELECT_MEMBERS, modelName, id, field));
5510
+ patch[field] = rows.map((row) => row.value);
5511
+ }
5512
+ const patchJson = JSON.stringify(patch);
5513
+ sql.exec(
5514
+ entry.replace ? REPLACE_UPSERT : MERGE_UPSERT,
5515
+ id,
5516
+ modelName,
5517
+ patchJson,
5518
+ patchJson
5519
+ );
5520
+ }
5521
+
5522
+ // src/utils/snapshotChunks.ts
5523
+ var SnapshotChunkIntegrityError = class extends Error {
5524
+ constructor(key, reason) {
5525
+ super(`Snapshot chunk ${key} does not match its manifest entry: ${reason}`);
5526
+ this.key = key;
5527
+ this.name = "SnapshotChunkIntegrityError";
5528
+ }
5529
+ };
5530
+ var SnapshotRowFormatError = class extends Error {
5531
+ constructor(model, id) {
5532
+ super(
5533
+ `Record ${model}/${id} cannot be written to a snapshot chunk: its id or data contains a newline or tab, which the line format reserves`
5534
+ );
5535
+ this.name = "SnapshotRowFormatError";
5536
+ }
5537
+ };
5538
+ function snapshotStringSetFields(manifest, model) {
5539
+ const declared = manifest.schema?.[model]?.stringSetFields;
5540
+ return new Set(Array.isArray(declared) ? declared.map(String) : []);
5541
+ }
5542
+ function snapshotDeclaredSchema(manifest, model) {
5543
+ const declared = manifest.schema?.[model] ?? {};
5544
+ const list = (value) => Array.isArray(value) ? value : [];
5545
+ const declaration = declared.declaration && typeof declared.declaration === "object" ? {
5546
+ fields: declared.declaration.fields ?? {},
5547
+ constraints: declared.declaration.constraints ?? {}
5548
+ } : void 0;
5549
+ return {
5550
+ ...declaration ? { declaration } : {},
5551
+ stringSetFields: list(declared.stringSetFields).map(String),
5552
+ uniqueConstraints: list(declared.uniqueConstraints).filter(
5553
+ (constraint) => constraint?.name && Array.isArray(constraint.fields) && constraint.fields.length > 0
5554
+ ).map((constraint) => ({
5555
+ name: String(constraint.name),
5556
+ fields: constraint.fields.map(String)
5557
+ })),
5558
+ indexedFields: list(declared.indexedFields).filter((index) => index?.field).map((index) => ({
5559
+ field: String(index.field),
5560
+ type: String(index.type || "string"),
5561
+ unique: !!index.unique
5562
+ }))
5563
+ };
5564
+ }
5565
+ function snapshotRowAsOverlayEntry(id, data, stringSetFields) {
5566
+ const fields = {};
5567
+ const stringSets = {};
5568
+ for (const [field, value] of Object.entries(data)) {
5569
+ if (field === "id") continue;
5570
+ if (stringSetFields.has(field) && Array.isArray(value)) {
5571
+ const members = {};
5572
+ for (const member of value) members[String(member)] = true;
5573
+ stringSets[field] = members;
5574
+ continue;
5575
+ }
5576
+ fields[field] = value;
5577
+ }
5578
+ return { id, fields, stringSets, replace: true, deleted: false };
5579
+ }
5580
+ var encoder = new TextEncoder();
5581
+ var decoder = new TextDecoder();
5582
+ function assertLineSafe(row) {
5583
+ if (/[\n\r\t]/.test(row._id) || /[\n\r\t]/.test(row._data)) {
5584
+ throw new SnapshotRowFormatError(row._type, row._id);
5585
+ }
5586
+ }
5587
+ function snapshotChunkLines(rows) {
5588
+ let body = "";
5589
+ for (const row of rows) {
5590
+ assertLineSafe(row);
5591
+ body += `${row._id} ${row._data}
5592
+ `;
5593
+ }
5594
+ return body;
5595
+ }
5596
+ async function gzip(bytes) {
5597
+ return await collect(
5598
+ new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"))
5599
+ );
5600
+ }
5601
+ async function gunzip(bytes) {
5602
+ return await collect(
5603
+ new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"))
5604
+ );
5605
+ }
5606
+ async function collect(stream) {
5607
+ const parts = [];
5608
+ let total = 0;
5609
+ const reader = stream.getReader();
5610
+ for (; ; ) {
5611
+ const { done, value } = await reader.read();
5612
+ if (done) break;
5613
+ parts.push(value);
5614
+ total += value.byteLength;
5615
+ }
5616
+ const out = new Uint8Array(total);
5617
+ let at = 0;
5618
+ for (const part of parts) {
5619
+ out.set(part, at);
5620
+ at += part.byteLength;
5621
+ }
5622
+ return out;
5623
+ }
5624
+ async function sha256Hex(bytes) {
5625
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
5626
+ return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
5627
+ }
5628
+ async function encodeSnapshotChunk(input) {
5629
+ const lines = encoder.encode(snapshotChunkLines(input.rows));
5630
+ const body = await gzip(lines);
5631
+ return {
5632
+ body,
5633
+ entry: {
5634
+ key: input.key,
5635
+ model: input.model,
5636
+ bytes: body.byteLength,
5637
+ rawBytes: lines.byteLength,
5638
+ rows: input.rows.length,
5639
+ firstId: input.rows.length > 0 ? input.rows[0]._id : "",
5640
+ lastId: input.rows.length > 0 ? input.rows[input.rows.length - 1]._id : "",
5641
+ sha256: await sha256Hex(body)
5642
+ }
5643
+ };
5644
+ }
5645
+ async function readSnapshotChunk(body, descriptor) {
5646
+ const entry = "sha256" in descriptor ? descriptor : null;
5647
+ if (entry) {
5648
+ if (body.byteLength !== entry.bytes) {
5649
+ throw new SnapshotChunkIntegrityError(
5650
+ entry.key,
5651
+ `expected ${entry.bytes} bytes, got ${body.byteLength}`
5652
+ );
5653
+ }
5654
+ const digest = await sha256Hex(body);
5655
+ if (digest !== entry.sha256) {
5656
+ throw new SnapshotChunkIntegrityError(
5657
+ entry.key,
5658
+ `expected sha256 ${entry.sha256}, got ${digest}`
5659
+ );
5660
+ }
5661
+ }
5662
+ const text = decoder.decode(await gunzip(body));
5663
+ const rows = [];
5664
+ for (const line of text.split("\n")) {
5665
+ if (line.length === 0) continue;
5666
+ const tab = line.indexOf(" ");
5667
+ if (tab < 0) {
5668
+ throw new SnapshotChunkIntegrityError(
5669
+ entry?.key ?? descriptor.model,
5670
+ "a line carries no id/data separator"
5671
+ );
5672
+ }
5673
+ rows.push({
5674
+ _type: descriptor.model,
5675
+ _id: line.slice(0, tab),
5676
+ _data: line.slice(tab + 1)
5677
+ });
5678
+ }
5679
+ if (entry && rows.length !== entry.rows) {
5680
+ throw new SnapshotChunkIntegrityError(
5681
+ entry.key,
5682
+ `expected ${entry.rows} rows, got ${rows.length}`
5683
+ );
5684
+ }
5685
+ return rows;
5686
+ }
4634
5687
  export {
5688
+ DEFAULT_OVERLAY_TABLES,
4635
5689
  DurableObjectEngine,
5690
+ Format2CompositeFieldError,
4636
5691
  JsonQueryTranslator,
4637
5692
  JsonSchemaDDL,
5693
+ OVERLAY_KEY_SEPARATOR,
5694
+ OVERLAY_MARKER_DELETED,
5695
+ OVERLAY_MARKER_REPLACE,
5696
+ PROJECTED_JSON_ALIAS_PREFIX,
5697
+ SnapshotChunkIntegrityError,
5698
+ SnapshotRowFormatError,
5699
+ applyOverlayMutation,
5700
+ assertNoFormat2CompositeFields,
4638
5701
  buildSafeAlias,
4639
5702
  checkAutoStampsReservedFields,
4640
5703
  checkReservedFieldName,
4641
5704
  clearMetaSyncCache,
5705
+ completeOverlayEntries,
4642
5706
  createDatabaseDO,
4643
5707
  createDocumentDO,
5708
+ decodeOverlaySegment,
4644
5709
  deterministicIdentifierHash,
5710
+ discoverDeclaredModel,
4645
5711
  discoverModelNames,
4646
5712
  discoverSchema,
5713
+ encodeOverlayMutation,
5714
+ encodeOverlaySegment,
5715
+ encodeSnapshotChunk,
5716
+ format2StringSetMembers,
5717
+ groupOverlayEntries,
4647
5718
  handleRequest,
4648
5719
  inferFieldType,
5720
+ isFormat2StringSetYMap,
4649
5721
  loadSchemaFromTomlString,
5722
+ materializeOverlayRecord,
5723
+ overlayFieldKey,
5724
+ overlayMarkerKey,
5725
+ overlayMemberKey,
5726
+ parseOverlayKey,
5727
+ parseRecordRow,
5728
+ projectOverlayEntry,
5729
+ projectedJsonAlias,
5730
+ readOverlayRecord,
5731
+ readSnapshotChunk,
4650
5732
  registerFunctionDefault,
4651
5733
  schemaToToml,
5734
+ snapshotChunkLines,
5735
+ snapshotDeclaredSchema,
5736
+ snapshotRowAsOverlayEntry,
5737
+ snapshotStringSetFields,
4652
5738
  syncInferredMeta,
4653
5739
  syncModelMeta
4654
5740
  };