@adobe/data 0.9.59 → 0.9.61

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.
Files changed (173) hide show
  1. package/dist/ecs/database/combine-plugins.d.ts +5 -1
  2. package/dist/ecs/database/combine-plugins.js +3 -3
  3. package/dist/ecs/database/combine-plugins.js.map +1 -1
  4. package/dist/ecs/database/create-plugin.d.ts +17 -14
  5. package/dist/ecs/database/create-plugin.js +2 -1
  6. package/dist/ecs/database/create-plugin.js.map +1 -1
  7. package/dist/ecs/database/database.d.ts +32 -4
  8. package/dist/ecs/database/database.index.performance.test.d.ts +1 -0
  9. package/dist/ecs/database/database.index.performance.test.js +417 -0
  10. package/dist/ecs/database/database.index.performance.test.js.map +1 -0
  11. package/dist/ecs/database/database.index.test.d.ts +1 -0
  12. package/dist/ecs/database/database.index.test.js +762 -0
  13. package/dist/ecs/database/database.index.test.js.map +1 -0
  14. package/dist/ecs/database/database.index.type-test.d.ts +1 -0
  15. package/dist/ecs/database/database.index.type-test.js +275 -0
  16. package/dist/ecs/database/database.index.type-test.js.map +1 -0
  17. package/dist/ecs/database/database.js.map +1 -1
  18. package/dist/ecs/database/deep-extends-chain.type-test.d.ts +256 -256
  19. package/dist/ecs/database/index-api-proof.type-test.d.ts +1 -0
  20. package/dist/ecs/database/index-api-proof.type-test.js +88 -0
  21. package/dist/ecs/database/index-api-proof.type-test.js.map +1 -0
  22. package/dist/ecs/database/index-registry/create-index-registry.d.ts +53 -0
  23. package/dist/ecs/database/index-registry/create-index-registry.js +174 -0
  24. package/dist/ecs/database/index-registry/create-index-registry.js.map +1 -0
  25. package/dist/ecs/database/index-registry/create-index.d.ts +60 -0
  26. package/dist/ecs/database/index-registry/create-index.js +539 -0
  27. package/dist/ecs/database/index-registry/create-index.js.map +1 -0
  28. package/dist/ecs/database/index-registry/index.d.ts +4 -0
  29. package/dist/ecs/database/index-registry/index.js +4 -0
  30. package/dist/ecs/database/index-registry/index.js.map +1 -0
  31. package/dist/ecs/database/public/create-database.js +146 -0
  32. package/dist/ecs/database/public/create-database.js.map +1 -1
  33. package/dist/ecs/database/transactional-store/transactional-store.d.ts +3 -2
  34. package/dist/ecs/plugins/scheduler/scheduler.d.ts +1 -0
  35. package/dist/ecs/store/index-types.d.ts +132 -0
  36. package/dist/ecs/store/index-types.js +3 -0
  37. package/dist/ecs/store/index-types.js.map +1 -0
  38. package/dist/ecs/store/public/create-store.js +189 -3
  39. package/dist/ecs/store/public/create-store.js.map +1 -1
  40. package/dist/ecs/store/store.d.ts +30 -13
  41. package/dist/ecs/store/store.js +5 -4
  42. package/dist/ecs/store/store.js.map +1 -1
  43. package/dist/ecs/store/transaction-functions.d.ts +4 -3
  44. package/dist/equals.js +24 -6
  45. package/dist/equals.js.map +1 -1
  46. package/dist/math/aabb/face/from-position.d.ts +1 -1
  47. package/dist/math/aabb/face/from-position.js +4 -3
  48. package/dist/math/aabb/face/from-position.js.map +1 -1
  49. package/dist/math/aabb/schema.js +1 -1
  50. package/dist/math/aabb/schema.js.map +1 -1
  51. package/dist/schema/fractional-index/between-n.d.ts +2 -0
  52. package/dist/schema/fractional-index/between-n.js +34 -0
  53. package/dist/schema/fractional-index/between-n.js.map +1 -0
  54. package/dist/schema/fractional-index/between-n.test.d.ts +1 -0
  55. package/dist/schema/fractional-index/between-n.test.js +36 -0
  56. package/dist/schema/fractional-index/between-n.test.js.map +1 -0
  57. package/dist/schema/fractional-index/between.d.ts +2 -0
  58. package/dist/schema/fractional-index/between.js +3 -0
  59. package/dist/schema/fractional-index/between.js.map +1 -0
  60. package/dist/schema/fractional-index/between.test.d.ts +1 -0
  61. package/dist/schema/fractional-index/between.test.js +68 -0
  62. package/dist/schema/fractional-index/between.test.js.map +1 -0
  63. package/dist/schema/fractional-index/decrement-integer.d.ts +1 -0
  64. package/dist/schema/fractional-index/decrement-integer.js +32 -0
  65. package/dist/schema/fractional-index/decrement-integer.js.map +1 -0
  66. package/dist/schema/fractional-index/decrement-integer.test.d.ts +1 -0
  67. package/dist/schema/fractional-index/decrement-integer.test.js +31 -0
  68. package/dist/schema/fractional-index/decrement-integer.test.js.map +1 -0
  69. package/dist/schema/fractional-index/digits.d.ts +1 -0
  70. package/dist/schema/fractional-index/digits.js +3 -0
  71. package/dist/schema/fractional-index/digits.js.map +1 -0
  72. package/dist/schema/fractional-index/fractional-index.d.ts +4 -0
  73. package/dist/schema/fractional-index/fractional-index.js +2 -0
  74. package/dist/schema/fractional-index/fractional-index.js.map +1 -0
  75. package/dist/schema/fractional-index/increment-integer.d.ts +1 -0
  76. package/dist/schema/fractional-index/increment-integer.js +32 -0
  77. package/dist/schema/fractional-index/increment-integer.js.map +1 -0
  78. package/dist/schema/fractional-index/increment-integer.test.d.ts +1 -0
  79. package/dist/schema/fractional-index/increment-integer.test.js +34 -0
  80. package/dist/schema/fractional-index/increment-integer.test.js.map +1 -0
  81. package/dist/schema/fractional-index/initial.d.ts +2 -0
  82. package/dist/schema/fractional-index/initial.js +2 -0
  83. package/dist/schema/fractional-index/initial.js.map +1 -0
  84. package/dist/schema/fractional-index/integer-length.d.ts +1 -0
  85. package/dist/schema/fractional-index/integer-length.js +9 -0
  86. package/dist/schema/fractional-index/integer-length.js.map +1 -0
  87. package/dist/schema/fractional-index/integer-part.d.ts +1 -0
  88. package/dist/schema/fractional-index/integer-part.js +9 -0
  89. package/dist/schema/fractional-index/integer-part.js.map +1 -0
  90. package/dist/schema/fractional-index/key-between.d.ts +2 -0
  91. package/dist/schema/fractional-index/key-between.js +39 -0
  92. package/dist/schema/fractional-index/key-between.js.map +1 -0
  93. package/dist/schema/fractional-index/midpoint.d.ts +1 -0
  94. package/dist/schema/fractional-index/midpoint.js +23 -0
  95. package/dist/schema/fractional-index/midpoint.js.map +1 -0
  96. package/dist/schema/fractional-index/midpoint.test.d.ts +1 -0
  97. package/dist/schema/fractional-index/midpoint.test.js +83 -0
  98. package/dist/schema/fractional-index/midpoint.test.js.map +1 -0
  99. package/dist/schema/fractional-index/public.d.ts +4 -0
  100. package/dist/schema/fractional-index/public.js +6 -0
  101. package/dist/schema/fractional-index/public.js.map +1 -0
  102. package/dist/schema/fractional-index/schema.d.ts +4 -0
  103. package/dist/schema/fractional-index/schema.js +5 -0
  104. package/dist/schema/fractional-index/schema.js.map +1 -0
  105. package/dist/schema/fractional-index/smallest-integer.d.ts +1 -0
  106. package/dist/schema/fractional-index/smallest-integer.js +3 -0
  107. package/dist/schema/fractional-index/smallest-integer.js.map +1 -0
  108. package/dist/schema/fractional-index/validate-integer.d.ts +1 -0
  109. package/dist/schema/fractional-index/validate-integer.js +7 -0
  110. package/dist/schema/fractional-index/validate-integer.js.map +1 -0
  111. package/dist/schema/fractional-index/validate-order-key.d.ts +1 -0
  112. package/dist/schema/fractional-index/validate-order-key.js +11 -0
  113. package/dist/schema/fractional-index/validate-order-key.js.map +1 -0
  114. package/dist/schema/guid/create.d.ts +2 -0
  115. package/dist/schema/guid/create.js +10 -0
  116. package/dist/schema/guid/create.js.map +1 -0
  117. package/dist/schema/guid/create.test.d.ts +1 -0
  118. package/dist/schema/guid/create.test.js +39 -0
  119. package/dist/schema/guid/create.test.js.map +1 -0
  120. package/dist/schema/guid/equals.d.ts +2 -0
  121. package/dist/schema/guid/equals.js +3 -0
  122. package/dist/schema/guid/equals.js.map +1 -0
  123. package/dist/schema/guid/equals.test.d.ts +1 -0
  124. package/dist/schema/guid/equals.test.js +30 -0
  125. package/dist/schema/guid/equals.test.js.map +1 -0
  126. package/dist/schema/guid/from-uuid.d.ts +2 -0
  127. package/dist/schema/guid/from-uuid.js +15 -0
  128. package/dist/schema/guid/from-uuid.js.map +1 -0
  129. package/dist/schema/guid/from-uuid.test.d.ts +1 -0
  130. package/dist/schema/guid/from-uuid.test.js +39 -0
  131. package/dist/schema/guid/from-uuid.test.js.map +1 -0
  132. package/dist/schema/guid/guid.performance.test.d.ts +1 -0
  133. package/dist/schema/guid/guid.performance.test.js +297 -0
  134. package/dist/schema/guid/guid.performance.test.js.map +1 -0
  135. package/dist/schema/guid/index.d.ts +4 -0
  136. package/dist/schema/guid/index.js +3 -0
  137. package/dist/schema/guid/index.js.map +1 -0
  138. package/dist/schema/guid/layout.d.ts +1 -0
  139. package/dist/schema/guid/layout.js +5 -0
  140. package/dist/schema/guid/layout.js.map +1 -0
  141. package/dist/schema/guid/nil.d.ts +2 -0
  142. package/dist/schema/guid/nil.js +3 -0
  143. package/dist/schema/guid/nil.js.map +1 -0
  144. package/dist/schema/guid/public.d.ts +8 -0
  145. package/dist/schema/guid/public.js +10 -0
  146. package/dist/schema/guid/public.js.map +1 -0
  147. package/dist/schema/guid/schema.d.ts +12 -0
  148. package/dist/schema/guid/schema.js +10 -0
  149. package/dist/schema/guid/schema.js.map +1 -0
  150. package/dist/schema/guid/schema.test.d.ts +1 -0
  151. package/dist/schema/guid/schema.test.js +35 -0
  152. package/dist/schema/guid/schema.test.js.map +1 -0
  153. package/dist/schema/guid/to-unserializable-key.d.ts +2 -0
  154. package/dist/schema/guid/to-unserializable-key.js +10 -0
  155. package/dist/schema/guid/to-unserializable-key.js.map +1 -0
  156. package/dist/schema/guid/to-uuid.d.ts +2 -0
  157. package/dist/schema/guid/to-uuid.js +15 -0
  158. package/dist/schema/guid/to-uuid.js.map +1 -0
  159. package/dist/schema/guid/to-uuid.test.d.ts +1 -0
  160. package/dist/schema/guid/to-uuid.test.js +31 -0
  161. package/dist/schema/guid/to-uuid.test.js.map +1 -0
  162. package/dist/schema/index.d.ts +2 -0
  163. package/dist/schema/index.js +2 -0
  164. package/dist/schema/index.js.map +1 -1
  165. package/dist/table/select-rows.d.ts +7 -4
  166. package/dist/table/select-rows.js.map +1 -1
  167. package/dist/tsconfig.tsbuildinfo +1 -1
  168. package/package.json +1 -1
  169. package/references/data-lit/package.json +1 -1
  170. package/references/data-lit-tictactoe/package.json +1 -1
  171. package/references/data-react/package.json +1 -1
  172. package/references/data-react-hello/package.json +1 -1
  173. package/references/data-react-pixie/package.json +1 -1
@@ -0,0 +1,762 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { describe, it, expect } from "vitest";
3
+ import { Database } from "./database.js";
4
+ // ============================================================================
5
+ // Catalogue pattern coverage — each describe corresponds to one of the 12
6
+ // patterns in `packages/data/src/ecs/README.md`. The tests assert runtime
7
+ // behavior. The type-level surface is covered by `index-api-proof.type-test.ts`
8
+ // and `database.index.type-test.ts`.
9
+ // ============================================================================
10
+ describe("Pattern 1 — single-column unique lookup (byEmail)", () => {
11
+ const plugin = () => Database.Plugin.create({
12
+ components: { email: { type: "string" } },
13
+ archetypes: { User: ["email"] },
14
+ indexes: {
15
+ byEmail: { key: "email", unique: true },
16
+ },
17
+ transactions: {
18
+ add: (t, email) => t.archetypes.User.insert({ email }),
19
+ },
20
+ });
21
+ it("get returns the entity for a known scalar key", () => {
22
+ const db = Database.create(plugin());
23
+ const e = db.transactions.add("alice@a.com");
24
+ expect(db.indexes.byEmail.get("alice@a.com")).toBe(e);
25
+ });
26
+ it("get returns null for an unknown key (known absent)", () => {
27
+ const db = Database.create(plugin());
28
+ expect(db.indexes.byEmail.get("missing@x.com")).toBeNull();
29
+ });
30
+ it("duplicate insert throws atomically — original row stays intact", () => {
31
+ const db = Database.create(plugin());
32
+ const first = db.transactions.add("shared@x.com");
33
+ expect(() => db.transactions.add("shared@x.com")).toThrow(/Unique index conflict/);
34
+ expect(db.indexes.byEmail.get("shared@x.com")).toBe(first);
35
+ });
36
+ });
37
+ describe("Pattern 2 — multi-column compound unique (playerSlot)", () => {
38
+ const plugin = () => Database.Plugin.create({
39
+ components: {
40
+ team: { type: "number" },
41
+ position: { type: "string" },
42
+ name: { type: "string" },
43
+ },
44
+ archetypes: { Player: ["team", "position", "name"] },
45
+ indexes: {
46
+ playerSlot: { key: ["team", "position"], unique: true },
47
+ },
48
+ transactions: {
49
+ add: (t, args) => t.archetypes.Player.insert(args),
50
+ },
51
+ });
52
+ it("get takes the column-keyed object and returns the entity", () => {
53
+ const db = Database.create(plugin());
54
+ const e = db.transactions.add({ team: 1, position: "qb", name: "alice" });
55
+ expect(db.indexes.playerSlot.get({ team: 1, position: "qb" })).toBe(e);
56
+ expect(db.indexes.playerSlot.get({ team: 1, position: "rb" })).toBeNull();
57
+ });
58
+ it("same component values in different teams do not collide", () => {
59
+ const db = Database.create(plugin());
60
+ const a = db.transactions.add({ team: 1, position: "qb", name: "alice" });
61
+ const b = db.transactions.add({ team: 2, position: "qb", name: "bob" });
62
+ expect(db.indexes.playerSlot.get({ team: 1, position: "qb" })).toBe(a);
63
+ expect(db.indexes.playerSlot.get({ team: 2, position: "qb" })).toBe(b);
64
+ });
65
+ });
66
+ describe("Pattern 3 — non-unique by single column (childrenOf)", () => {
67
+ const plugin = () => Database.Plugin.create({
68
+ components: {
69
+ parent: { type: "number" },
70
+ label: { type: "string" },
71
+ },
72
+ archetypes: { Child: ["parent", "label"] },
73
+ indexes: {
74
+ childrenOf: { key: "parent" },
75
+ },
76
+ transactions: {
77
+ add: (t, args) => t.archetypes.Child.insert(args),
78
+ delete: (t, e) => t.delete(e),
79
+ move: (t, args) => t.update(args.entity, { parent: args.newParent }),
80
+ },
81
+ });
82
+ it("find returns every entity sharing the key", () => {
83
+ const db = Database.create(plugin());
84
+ const a = db.transactions.add({ parent: 7, label: "a" });
85
+ const b = db.transactions.add({ parent: 7, label: "b" });
86
+ db.transactions.add({ parent: 9, label: "c" });
87
+ expect([...db.indexes.childrenOf.find(7)].sort()).toEqual([a, b].sort());
88
+ });
89
+ it("delete removes the entity from the bucket", () => {
90
+ const db = Database.create(plugin());
91
+ const e = db.transactions.add({ parent: 7, label: "a" });
92
+ expect(db.indexes.childrenOf.find(7)).toEqual([e]);
93
+ db.transactions.delete(e);
94
+ expect(db.indexes.childrenOf.find(7)).toEqual([]);
95
+ });
96
+ it("update moves the entity between buckets", () => {
97
+ const db = Database.create(plugin());
98
+ const e = db.transactions.add({ parent: 7, label: "a" });
99
+ db.transactions.move({ entity: e, newParent: 9 });
100
+ expect(db.indexes.childrenOf.find(7)).toEqual([]);
101
+ expect(db.indexes.childrenOf.find(9)).toEqual([e]);
102
+ });
103
+ it("`get` is absent on non-unique handles at runtime and types", () => {
104
+ const db = Database.create(plugin());
105
+ expect("get" in db.indexes.childrenOf).toBe(false);
106
+ });
107
+ });
108
+ describe("Pattern 4 — sorted children (orderedChildrenOf)", () => {
109
+ const plugin = () => Database.Plugin.create({
110
+ components: {
111
+ parent: { type: "number" },
112
+ fractIndex: { type: "string" },
113
+ },
114
+ archetypes: { Child: ["parent", "fractIndex"] },
115
+ indexes: {
116
+ orderedChildrenOf: {
117
+ key: "parent",
118
+ order: { by: ["fractIndex"] },
119
+ },
120
+ },
121
+ transactions: {
122
+ add: (t, args) => t.archetypes.Child.insert(args),
123
+ move: (t, args) => t.update(args.entity, { fractIndex: args.fractIndex }),
124
+ delete: (t, e) => t.delete(e),
125
+ },
126
+ });
127
+ it("returns entities sorted by the order key (asc by default)", () => {
128
+ const db = Database.create(plugin());
129
+ const c = db.transactions.add({ parent: 7, fractIndex: "c" });
130
+ const a = db.transactions.add({ parent: 7, fractIndex: "a" });
131
+ const b = db.transactions.add({ parent: 7, fractIndex: "b" });
132
+ expect(db.indexes.orderedChildrenOf.find(7)).toEqual([a, b, c]);
133
+ });
134
+ it("update on the sort key repositions the entity in place", () => {
135
+ const db = Database.create(plugin());
136
+ const a = db.transactions.add({ parent: 7, fractIndex: "a" });
137
+ const b = db.transactions.add({ parent: 7, fractIndex: "b" });
138
+ const c = db.transactions.add({ parent: 7, fractIndex: "c" });
139
+ db.transactions.move({ entity: b, fractIndex: "z" });
140
+ expect(db.indexes.orderedChildrenOf.find(7)).toEqual([a, c, b]);
141
+ db.transactions.move({ entity: a, fractIndex: "d" });
142
+ expect(db.indexes.orderedChildrenOf.find(7)).toEqual([c, a, b]);
143
+ });
144
+ it("delete preserves sort order of the remaining entities", () => {
145
+ const db = Database.create(plugin());
146
+ const a = db.transactions.add({ parent: 7, fractIndex: "a" });
147
+ const b = db.transactions.add({ parent: 7, fractIndex: "b" });
148
+ const c = db.transactions.add({ parent: 7, fractIndex: "c" });
149
+ db.transactions.delete(b);
150
+ expect(db.indexes.orderedChildrenOf.find(7)).toEqual([a, c]);
151
+ });
152
+ it("populates and sorts when the index is registered after data exists", () => {
153
+ const base = Database.Plugin.create({
154
+ components: {
155
+ parent: { type: "number" },
156
+ fractIndex: { type: "string" },
157
+ },
158
+ archetypes: { Child: ["parent", "fractIndex"] },
159
+ transactions: {
160
+ add: (t, args) => t.archetypes.Child.insert(args),
161
+ },
162
+ });
163
+ const db = Database.create(base);
164
+ const c = db.transactions.add({ parent: 5, fractIndex: "c" });
165
+ const a = db.transactions.add({ parent: 5, fractIndex: "a" });
166
+ const b = db.transactions.add({ parent: 5, fractIndex: "b" });
167
+ const indexed = Database.Plugin.create({
168
+ extends: base,
169
+ indexes: {
170
+ orderedChildrenOf: { key: "parent", order: { by: ["fractIndex"] } },
171
+ },
172
+ });
173
+ const ext = db.extend(indexed);
174
+ expect(ext.indexes.orderedChildrenOf.find(5)).toEqual([a, b, c]);
175
+ });
176
+ });
177
+ describe("Pattern 5 — multi-value (array column → fan-out): tasksByAssignee", () => {
178
+ const plugin = () => Database.Plugin.create({
179
+ components: {
180
+ title: { type: "string" },
181
+ assigned: { type: "array", items: { type: "string" } },
182
+ },
183
+ archetypes: { Task: ["title", "assigned"] },
184
+ indexes: {
185
+ tasksByAssignee: { key: "assigned" },
186
+ },
187
+ transactions: {
188
+ add: (t, args) => t.archetypes.Task.insert(args),
189
+ reassign: (t, args) => t.update(args.entity, { assigned: args.assigned }),
190
+ delete: (t, e) => t.delete(e),
191
+ },
192
+ });
193
+ it("each array element becomes its own bucket entry", () => {
194
+ const db = Database.create(plugin());
195
+ const e = db.transactions.add({ title: "ship", assigned: ["joe", "bob"] });
196
+ expect(db.indexes.tasksByAssignee.find("joe")).toEqual([e]);
197
+ expect(db.indexes.tasksByAssignee.find("bob")).toEqual([e]);
198
+ expect(db.indexes.tasksByAssignee.find("carol")).toEqual([]);
199
+ });
200
+ it("multiple entities sharing an element are both returned", () => {
201
+ const db = Database.create(plugin());
202
+ const a = db.transactions.add({ title: "A", assigned: ["joe", "bob"] });
203
+ const b = db.transactions.add({ title: "B", assigned: ["joe", "carol"] });
204
+ db.transactions.add({ title: "C", assigned: ["diane"] });
205
+ expect([...db.indexes.tasksByAssignee.find("joe")].sort()).toEqual([a, b].sort());
206
+ });
207
+ it("empty array contributes no bucket entries", () => {
208
+ const db = Database.create(plugin());
209
+ db.transactions.add({ title: "Unassigned", assigned: [] });
210
+ expect(db.indexes.tasksByAssignee.find("joe")).toEqual([]);
211
+ });
212
+ it("update set-diffs the bucket membership", () => {
213
+ const db = Database.create(plugin());
214
+ const e = db.transactions.add({ title: "T", assigned: ["joe", "bob"] });
215
+ db.transactions.reassign({ entity: e, assigned: ["joe", "carol"] });
216
+ expect(db.indexes.tasksByAssignee.find("joe")).toEqual([e]);
217
+ expect(db.indexes.tasksByAssignee.find("bob")).toEqual([]);
218
+ expect(db.indexes.tasksByAssignee.find("carol")).toEqual([e]);
219
+ });
220
+ it("delete drops the entity from every bucket it was in", () => {
221
+ const db = Database.create(plugin());
222
+ const e = db.transactions.add({ title: "T", assigned: ["joe", "bob"] });
223
+ db.transactions.delete(e);
224
+ expect(db.indexes.tasksByAssignee.find("joe")).toEqual([]);
225
+ expect(db.indexes.tasksByAssignee.find("bob")).toEqual([]);
226
+ });
227
+ });
228
+ describe("Pattern 6 — computed scalar (byEmailCi)", () => {
229
+ const plugin = () => Database.Plugin.create({
230
+ components: { email: { type: "string" } },
231
+ archetypes: { User: ["email"] },
232
+ indexes: {
233
+ byEmailCi: { key: (email) => email.toLowerCase(), components: ["email"] },
234
+ },
235
+ transactions: {
236
+ add: (t, email) => t.archetypes.User.insert({ email }),
237
+ rename: (t, args) => t.update(args.entity, { email: args.email }),
238
+ delete: (t, e) => t.delete(e),
239
+ },
240
+ });
241
+ it("looks up by the computed (lowercased) key", () => {
242
+ const db = Database.create(plugin());
243
+ const e = db.transactions.add("Alice@Example.com");
244
+ expect(db.indexes.byEmailCi.find("alice@example.com")).toEqual([e]);
245
+ // The original-case input is not a key.
246
+ expect(db.indexes.byEmailCi.find("Alice@Example.com")).toEqual([]);
247
+ });
248
+ it("update re-derives the key", () => {
249
+ const db = Database.create(plugin());
250
+ const e = db.transactions.add("alice@a.com");
251
+ expect(db.indexes.byEmailCi.find("alice@a.com")).toEqual([e]);
252
+ db.transactions.rename({ entity: e, email: "alice@b.com" });
253
+ expect(db.indexes.byEmailCi.find("alice@a.com")).toEqual([]);
254
+ expect(db.indexes.byEmailCi.find("alice@b.com")).toEqual([e]);
255
+ });
256
+ it("delete removes the computed bucket entry", () => {
257
+ const db = Database.create(plugin());
258
+ const e = db.transactions.add("alice@a.com");
259
+ db.transactions.delete(e);
260
+ expect(db.indexes.byEmailCi.find("alice@a.com")).toEqual([]);
261
+ });
262
+ });
263
+ describe("Pattern 7 — multi-value computed (docsByKeyword)", () => {
264
+ const plugin = () => Database.Plugin.create({
265
+ components: { body: { type: "string" } },
266
+ archetypes: { Doc: ["body"] },
267
+ indexes: {
268
+ docsByKeyword: {
269
+ key: (body) => body.toLowerCase().split(/\s+/).filter((s) => s.length > 0),
270
+ components: ["body"],
271
+ },
272
+ },
273
+ transactions: {
274
+ add: (t, body) => t.archetypes.Doc.insert({ body }),
275
+ },
276
+ });
277
+ it("fans out each computed array element into its own bucket", () => {
278
+ const db = Database.create(plugin());
279
+ const d = db.transactions.add("the quick brown fox");
280
+ expect(db.indexes.docsByKeyword.find("quick")).toEqual([d]);
281
+ expect(db.indexes.docsByKeyword.find("brown")).toEqual([d]);
282
+ expect(db.indexes.docsByKeyword.find("missing")).toEqual([]);
283
+ });
284
+ it("findRange supports operator filters on the scalar element", () => {
285
+ const db = Database.create(plugin());
286
+ const d = db.transactions.add("alpha beta gamma");
287
+ const inRange = [...db.indexes.docsByKeyword.findRange({ ">=": "b", "<": "z" })].sort();
288
+ expect(inRange).toEqual([d]);
289
+ });
290
+ });
291
+ describe("Pattern 8 — compound from nested data (playerByRoster)", () => {
292
+ const plugin = () => Database.Plugin.create({
293
+ components: {
294
+ roster: {
295
+ type: "object",
296
+ properties: {
297
+ team: { type: "number" },
298
+ position: { type: "string" },
299
+ },
300
+ required: ["team", "position"],
301
+ },
302
+ name: { type: "string" },
303
+ },
304
+ archetypes: { Player: ["roster", "name"] },
305
+ indexes: {
306
+ playerByRoster: {
307
+ key: {
308
+ team: (r) => r.team,
309
+ position: (r) => r.position,
310
+ },
311
+ unique: true,
312
+ components: ["roster"],
313
+ },
314
+ },
315
+ transactions: {
316
+ add: (t, args) => t.archetypes.Player.insert(args),
317
+ },
318
+ });
319
+ it("get takes a slot-keyed object and returns the entity", () => {
320
+ const db = Database.create(plugin());
321
+ const e = db.transactions.add({
322
+ roster: { team: 1, position: "qb" },
323
+ name: "alice",
324
+ });
325
+ expect(db.indexes.playerByRoster.get({ team: 1, position: "qb" })).toBe(e);
326
+ expect(db.indexes.playerByRoster.get({ team: 1, position: "rb" })).toBeNull();
327
+ });
328
+ });
329
+ describe("Pattern 10 — mixed identity + derived slots (playerByTeamRole)", () => {
330
+ const plugin = () => Database.Plugin.create({
331
+ components: {
332
+ team: { type: "number" },
333
+ roster: {
334
+ type: "object",
335
+ properties: { role: { type: "string" } },
336
+ required: ["role"],
337
+ },
338
+ name: { type: "string" },
339
+ },
340
+ archetypes: { Player: ["team", "roster", "name"] },
341
+ indexes: {
342
+ playerByTeamRole: {
343
+ key: {
344
+ team: "team",
345
+ role: (r) => r.role,
346
+ },
347
+ unique: true,
348
+ components: ["roster"],
349
+ },
350
+ },
351
+ transactions: {
352
+ add: (t, args) => t.archetypes.Player.insert(args),
353
+ },
354
+ });
355
+ it("identity-slot reads the column directly; derived-slot computes from nested data", () => {
356
+ const db = Database.create(plugin());
357
+ const e = db.transactions.add({
358
+ team: 1,
359
+ roster: { role: "qb" },
360
+ name: "alice",
361
+ });
362
+ expect(db.indexes.playerByTeamRole.get({ team: 1, role: "qb" })).toBe(e);
363
+ expect(db.indexes.playerByTeamRole.get({ team: 1, role: "rb" })).toBeNull();
364
+ expect(db.indexes.playerByTeamRole.get({ team: 2, role: "qb" })).toBeNull();
365
+ });
366
+ });
367
+ describe("Pattern 12 — custom comparator (tasksByPriority)", () => {
368
+ const plugin = () => Database.Plugin.create({
369
+ components: {
370
+ owner: { type: "number" },
371
+ priority: { type: "number" },
372
+ due: { type: "number" },
373
+ },
374
+ archetypes: { Task: ["owner", "priority", "due"] },
375
+ indexes: {
376
+ tasksByPriority: {
377
+ key: "owner",
378
+ order: {
379
+ by: ["priority", "due"],
380
+ // priority desc, due asc tie-breaker. `compare`'s `a` / `b`
381
+ // types are not contextually inferred today (the literal
382
+ // `by` tuple does not flow back into the method
383
+ // signature's `Pick<C, By[number]>`), so we annotate
384
+ // explicitly. The compiler still validates that the
385
+ // annotation is a subset of `Pick<C, By[number]>`.
386
+ compare: (a, b) => b.priority - a.priority || a.due - b.due,
387
+ },
388
+ },
389
+ },
390
+ transactions: {
391
+ add: (t, args) => t.archetypes.Task.insert(args),
392
+ },
393
+ });
394
+ it("applies the custom comparator within each bucket", () => {
395
+ const db = Database.create(plugin());
396
+ const e1 = db.transactions.add({ owner: 1, priority: 1, due: 100 });
397
+ const e3 = db.transactions.add({ owner: 1, priority: 3, due: 50 });
398
+ const e2a = db.transactions.add({ owner: 1, priority: 2, due: 10 });
399
+ const e2b = db.transactions.add({ owner: 1, priority: 2, due: 20 });
400
+ // Priority desc, then due asc.
401
+ expect(db.indexes.tasksByPriority.find(1)).toEqual([e3, e2a, e2b, e1]);
402
+ });
403
+ });
404
+ // ============================================================================
405
+ // Cross-cutting concerns
406
+ // ============================================================================
407
+ describe("findRange — operator filters on the bucket key", () => {
408
+ it("filters by range operators on a tuple-keyed index", () => {
409
+ const plugin = Database.Plugin.create({
410
+ components: {
411
+ name: { type: "string" },
412
+ score: { type: "number" },
413
+ },
414
+ archetypes: { User: ["name", "score"] },
415
+ indexes: {
416
+ byNameScore: { key: ["name", "score"] },
417
+ },
418
+ transactions: {
419
+ add: (t, args) => t.archetypes.User.insert(args),
420
+ },
421
+ });
422
+ const db = Database.create(plugin);
423
+ db.transactions.add({ name: "alice", score: 10 });
424
+ const high = db.transactions.add({ name: "alice", score: 20 });
425
+ const higher = db.transactions.add({ name: "alice", score: 50 });
426
+ const hits = [...db.indexes.byNameScore.findRange({ score: { ">=": 20 } })].sort();
427
+ expect(hits).toEqual([high, higher].sort());
428
+ });
429
+ });
430
+ describe("auto-routing of db.select", () => {
431
+ const plugin = () => Database.Plugin.create({
432
+ components: {
433
+ name: { type: "string" },
434
+ email: { type: "string" },
435
+ active: { type: "boolean" },
436
+ },
437
+ archetypes: { User: ["name", "email", "active"] },
438
+ indexes: {
439
+ byName: { key: "name" },
440
+ },
441
+ transactions: {
442
+ add: (t, args) => t.archetypes.User.insert(args),
443
+ },
444
+ });
445
+ it("routes pure equality where to a matching index", () => {
446
+ const db = Database.create(plugin());
447
+ const e1 = db.transactions.add({ name: "alice", email: "a@a.com", active: true });
448
+ const e2 = db.transactions.add({ name: "alice", email: "a@b.com", active: true });
449
+ db.transactions.add({ name: "bob", email: "b@a.com", active: true });
450
+ const idx = db.indexes.byName;
451
+ const original = idx.find;
452
+ let calls = 0;
453
+ idx.find = (v) => { calls += 1; return original(v); };
454
+ try {
455
+ const result = db.select(["name"], { where: { name: "alice" } });
456
+ expect([...result].sort()).toEqual([e1, e2].sort());
457
+ expect(calls).toBe(1);
458
+ }
459
+ finally {
460
+ idx.find = original;
461
+ }
462
+ });
463
+ it("falls back to scan when where keys don't match any index", () => {
464
+ const db = Database.create(plugin());
465
+ db.transactions.add({ name: "alice", email: "a@a.com", active: true });
466
+ db.transactions.add({ name: "alice", email: "a@b.com", active: false });
467
+ const result = db.select(["active"], { where: { active: false } });
468
+ expect(result).toHaveLength(1);
469
+ });
470
+ it("falls back to scan for a non-equality operator (>= on indexed col)", () => {
471
+ const db = Database.create(plugin());
472
+ db.transactions.add({ name: "alice", email: "a@a.com", active: true });
473
+ db.transactions.add({ name: "bob", email: "b@a.com", active: true });
474
+ const idx = db.indexes.byName;
475
+ const original = idx.find;
476
+ let calls = 0;
477
+ idx.find = (v) => { calls += 1; return original(v); };
478
+ try {
479
+ const result = db.select(["name"], { where: { name: { ">=": "b" } } });
480
+ expect(result).toHaveLength(1);
481
+ expect(calls).toBe(0);
482
+ }
483
+ finally {
484
+ idx.find = original;
485
+ }
486
+ });
487
+ it("does not auto-route to a computed (function-key) or slot-map index", () => {
488
+ const computedPlugin = Database.Plugin.create({
489
+ components: { name: { type: "string" } },
490
+ archetypes: { U: ["name"] },
491
+ indexes: {
492
+ byLowerName: {
493
+ key: (name) => name.toLowerCase(),
494
+ components: ["name"],
495
+ },
496
+ },
497
+ transactions: {
498
+ add: (t, name) => t.archetypes.U.insert({ name }),
499
+ },
500
+ });
501
+ const db = Database.create(computedPlugin);
502
+ db.transactions.add("Alice");
503
+ const idx = db.indexes.byLowerName;
504
+ const original = idx.find;
505
+ let calls = 0;
506
+ idx.find = (v) => { calls += 1; return original(v); };
507
+ try {
508
+ // Raw-where equality is in source-column space, not derived-key space.
509
+ const result = db.select(["name"], { where: { name: "Alice" } });
510
+ expect(result).toHaveLength(1);
511
+ expect(calls).toBe(0);
512
+ }
513
+ finally {
514
+ idx.find = original;
515
+ }
516
+ });
517
+ });
518
+ describe("auto-routing of db.observe.select", () => {
519
+ const plugin = () => Database.Plugin.create({
520
+ components: { name: { type: "string" } },
521
+ archetypes: { U: ["name"] },
522
+ indexes: {
523
+ byName: { key: "name" },
524
+ },
525
+ transactions: {
526
+ add: (t, name) => t.archetypes.U.insert({ name }),
527
+ },
528
+ });
529
+ it("routes the initial snapshot through the index", () => {
530
+ const db = Database.create(plugin());
531
+ const e1 = db.transactions.add("alice");
532
+ const e2 = db.transactions.add("alice");
533
+ db.transactions.add("bob");
534
+ const idx = db.indexes.byName;
535
+ const original = idx.find;
536
+ let calls = 0;
537
+ idx.find = (v) => { calls += 1; return original(v); };
538
+ try {
539
+ let emitted = [];
540
+ const unsub = db.observe.select(["name"], { where: { name: "alice" } })((entities) => { emitted = entities; });
541
+ expect([...emitted].sort()).toEqual([e1, e2].sort());
542
+ expect(calls).toBe(1);
543
+ unsub();
544
+ }
545
+ finally {
546
+ idx.find = original;
547
+ }
548
+ });
549
+ it("routes the requery after a relevant transaction", async () => {
550
+ const db = Database.create(plugin());
551
+ const e1 = db.transactions.add("alice");
552
+ db.transactions.add("bob");
553
+ const idx = db.indexes.byName;
554
+ const original = idx.find;
555
+ let calls = 0;
556
+ idx.find = (v) => { calls += 1; return original(v); };
557
+ try {
558
+ const emissions = [];
559
+ const unsub = db.observe.select(["name"], { where: { name: "alice" } })((entities) => { emissions.push([...entities]); });
560
+ expect(calls).toBe(1);
561
+ const e3 = db.transactions.add("alice");
562
+ await Promise.resolve();
563
+ expect(calls).toBe(2);
564
+ expect([...emissions[emissions.length - 1]].sort()).toEqual([e1, e3].sort());
565
+ unsub();
566
+ }
567
+ finally {
568
+ idx.find = original;
569
+ }
570
+ });
571
+ });
572
+ describe("t.indexes — eager maintenance inside transactions", () => {
573
+ const plugin = () => Database.Plugin.create({
574
+ components: {
575
+ name: { type: "string" },
576
+ email: { type: "string" },
577
+ },
578
+ archetypes: { User: ["name", "email"] },
579
+ indexes: {
580
+ byName: { key: "name" },
581
+ uniqueByEmail: { key: "email", unique: true },
582
+ },
583
+ transactions: {
584
+ add: (t, args) => t.archetypes.User.insert(args),
585
+ addIfNew: (t, args) => {
586
+ const existing = t.indexes.uniqueByEmail.get(args.email);
587
+ if (existing !== null)
588
+ return existing;
589
+ return t.archetypes.User.insert(args);
590
+ },
591
+ renameAndLookup: (t, args) => {
592
+ t.update(args.entity, { name: args.newName });
593
+ const found = t.indexes.byName.find(args.newName);
594
+ if (!found.includes(args.entity)) {
595
+ throw new Error("name index stale after t.update");
596
+ }
597
+ },
598
+ deleteAndLookup: (t, args) => {
599
+ t.delete(args.entity);
600
+ const stillThere = t.indexes.byName.find(args.name);
601
+ if (stillThere.includes(args.entity)) {
602
+ throw new Error("index still references deleted entity");
603
+ }
604
+ },
605
+ updateEmail: (t, args) => t.update(args.entity, { email: args.newEmail }),
606
+ },
607
+ });
608
+ it("get on a unique index sees a row inserted earlier in the same transaction", () => {
609
+ const db = Database.create(plugin());
610
+ const e = db.transactions.addIfNew({ name: "alice", email: "a@a.com" });
611
+ // A second call finds the just-inserted row.
612
+ const same = db.transactions.addIfNew({ name: "alice2", email: "a@a.com" });
613
+ expect(same).toBe(e);
614
+ });
615
+ it("find sees an update made earlier in the same transaction body", () => {
616
+ const db = Database.create(plugin());
617
+ const e = db.transactions.add({ name: "alice", email: "a@a.com" });
618
+ expect(() => db.transactions.renameAndLookup({ entity: e, newName: "alex" })).not.toThrow();
619
+ expect(db.indexes.byName.find("alex")).toEqual([e]);
620
+ expect(db.indexes.byName.find("alice")).toEqual([]);
621
+ });
622
+ it("find does not see a row deleted earlier in the same transaction body", () => {
623
+ const db = Database.create(plugin());
624
+ const e = db.transactions.add({ name: "alice", email: "a@a.com" });
625
+ expect(() => db.transactions.deleteAndLookup({ entity: e, name: "alice" })).not.toThrow();
626
+ expect(db.indexes.byName.find("alice")).toEqual([]);
627
+ });
628
+ it("unique conflict on insert is caught up-front — no partial store or index mutation", () => {
629
+ const db = Database.create(plugin());
630
+ const first = db.transactions.add({ name: "alice", email: "shared@x.com" });
631
+ // Snapshot pre-throw state so we can assert no drift.
632
+ const beforeRows = db.select(["email"]);
633
+ expect(() => db.transactions.add({ name: "alex", email: "shared@x.com" })).toThrow(/Unique index conflict/);
634
+ // The unique-key bucket still points at the original entity.
635
+ expect(db.indexes.uniqueByEmail.get("shared@x.com")).toBe(first);
636
+ // No phantom row landed in either index or store. byName.find("alex")
637
+ // returning [] proves the secondary index never saw the row;
638
+ // db.select asserts the store itself never grew.
639
+ expect(db.indexes.byName.find("alex")).toEqual([]);
640
+ const afterRows = db.select(["email"]);
641
+ expect([...afterRows].sort()).toEqual([...beforeRows].sort());
642
+ });
643
+ it("unique conflict on update is caught up-front — both stores stay consistent", () => {
644
+ const db = Database.create(plugin());
645
+ const e1 = db.transactions.add({ name: "alice", email: "a@a.com" });
646
+ const e2 = db.transactions.add({ name: "bob", email: "b@b.com" });
647
+ // Snapshot pre-throw state.
648
+ const e1BeforeRead = db.read(e1);
649
+ const e2BeforeRead = db.read(e2);
650
+ expect(() => db.transactions.updateEmail({ entity: e1, newEmail: "b@b.com" })).toThrow(/Unique index conflict/);
651
+ // Index untouched on both keys.
652
+ expect(db.indexes.uniqueByEmail.get("a@a.com")).toBe(e1);
653
+ expect(db.indexes.uniqueByEmail.get("b@b.com")).toBe(e2);
654
+ // Underlying store untouched — e1's email is still "a@a.com",
655
+ // proving the pre-check fired before `core.update` ran.
656
+ expect(db.read(e1)).toEqual(e1BeforeRead);
657
+ expect(db.read(e2)).toEqual(e2BeforeRead);
658
+ });
659
+ });
660
+ describe("registration rules", () => {
661
+ it("extending twice with the same plugin instance is a no-op", () => {
662
+ const plugin = Database.Plugin.create({
663
+ components: { name: { type: "string" } },
664
+ archetypes: { N: ["name"] },
665
+ indexes: { byName: { key: "name" } },
666
+ transactions: { add: (t, name) => t.archetypes.N.insert({ name }) },
667
+ });
668
+ const db = Database.create(plugin);
669
+ expect(() => db.extend(plugin)).not.toThrow();
670
+ expect(Object.keys(db.indexes)).toEqual(["byName"]);
671
+ });
672
+ it("combining plugins via Plugin.combine with a shared index decl registers once", () => {
673
+ const components = { email: { type: "string" } };
674
+ const shared = { key: "email", unique: true };
675
+ const a = Database.Plugin.create({ components, indexes: { byEmail: shared } });
676
+ const b = Database.Plugin.create({ components, indexes: { byEmail: shared } });
677
+ const combined = Database.Plugin.combine(a, b);
678
+ const db = Database.create(combined);
679
+ expect(Object.keys(db.indexes)).toEqual(["byEmail"]);
680
+ });
681
+ it("distinct names with distinct shapes both register", () => {
682
+ const plugin = Database.Plugin.create({
683
+ components: {
684
+ name: { type: "string" },
685
+ email: { type: "string" },
686
+ },
687
+ indexes: {
688
+ byName: { key: "name" },
689
+ byEmail: { key: "email" },
690
+ },
691
+ });
692
+ const db = Database.create(plugin);
693
+ expect(Object.keys(db.indexes).sort()).toEqual(["byEmail", "byName"]);
694
+ });
695
+ it("same name with different declaration objects across plugins throws", () => {
696
+ const components = { email: { type: "string" } };
697
+ const a = Database.Plugin.create({
698
+ components,
699
+ indexes: { byEmail: { key: "email", unique: true } },
700
+ });
701
+ const b = Database.Plugin.create({
702
+ components,
703
+ // Structurally identical, but a different object identity.
704
+ indexes: { byEmail: { key: "email", unique: true } },
705
+ });
706
+ const db = Database.create(a);
707
+ expect(() => db.extend(b)).toThrow(/different declaration object/);
708
+ });
709
+ it("different names with identical shape across plugins throws", () => {
710
+ const components = { email: { type: "string" } };
711
+ const a = Database.Plugin.create({
712
+ components,
713
+ indexes: { byEmail: { key: "email", unique: true } },
714
+ });
715
+ const b = Database.Plugin.create({
716
+ components,
717
+ indexes: { lookupByEmail: { key: "email", unique: true } },
718
+ });
719
+ const db = Database.create(a);
720
+ expect(() => db.extend(b)).toThrow(/identical shape/);
721
+ });
722
+ it("identical shape inside a single plugin throws", () => {
723
+ const plugin = Database.Plugin.create({
724
+ components: { email: { type: "string" } },
725
+ indexes: {
726
+ byEmail: { key: "email", unique: true },
727
+ lookupByEmail: { key: "email", unique: true },
728
+ },
729
+ });
730
+ expect(() => Database.create(plugin)).toThrow(/identical shape/);
731
+ });
732
+ it("same key tuple with different `unique` flag is NOT a structural duplicate", () => {
733
+ const plugin = Database.Plugin.create({
734
+ components: { email: { type: "string" } },
735
+ indexes: {
736
+ byEmailLookup: { key: "email" },
737
+ uniqueByEmail: { key: "email", unique: true },
738
+ },
739
+ });
740
+ const db = Database.create(plugin);
741
+ expect(Object.keys(db.indexes).sort()).toEqual(["byEmailLookup", "uniqueByEmail"]);
742
+ });
743
+ });
744
+ describe("registry maintenance", () => {
745
+ it("populates from existing entities when a plugin registers an index later", () => {
746
+ const base = Database.Plugin.create({
747
+ components: { name: { type: "string" } },
748
+ archetypes: { N: ["name"] },
749
+ transactions: { add: (t, name) => t.archetypes.N.insert({ name }) },
750
+ });
751
+ const indexed = Database.Plugin.create({
752
+ extends: base,
753
+ indexes: { byName: { key: "name" } },
754
+ });
755
+ const db = Database.create(base);
756
+ const e1 = db.transactions.add("alice");
757
+ const e2 = db.transactions.add("alice");
758
+ const ext = db.extend(indexed);
759
+ expect([...ext.indexes.byName.find("alice")].sort()).toEqual([e1, e2].sort());
760
+ });
761
+ });
762
+ //# sourceMappingURL=database.index.test.js.map