metal-orm 1.0.5 → 1.0.7

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 (36) hide show
  1. package/README.md +299 -113
  2. package/docs/CHANGES.md +104 -0
  3. package/docs/advanced-features.md +92 -1
  4. package/docs/api-reference.md +13 -4
  5. package/docs/dml-operations.md +156 -0
  6. package/docs/getting-started.md +122 -55
  7. package/docs/hydration.md +78 -13
  8. package/docs/index.md +19 -14
  9. package/docs/multi-dialect-support.md +25 -0
  10. package/docs/query-builder.md +60 -0
  11. package/docs/runtime.md +105 -0
  12. package/docs/schema-definition.md +52 -1
  13. package/package.json +1 -1
  14. package/src/ast/expression.ts +38 -18
  15. package/src/builder/hydration-planner.ts +74 -74
  16. package/src/builder/select.ts +427 -395
  17. package/src/constants/sql-operator-config.ts +3 -0
  18. package/src/constants/sql.ts +38 -32
  19. package/src/index.ts +16 -8
  20. package/src/playground/features/playground/data/scenarios/types.ts +18 -15
  21. package/src/playground/features/playground/data/schema.ts +10 -10
  22. package/src/playground/features/playground/services/QueryExecutionService.ts +2 -1
  23. package/src/runtime/entity-meta.ts +52 -0
  24. package/src/runtime/entity.ts +252 -0
  25. package/src/runtime/execute.ts +36 -0
  26. package/src/runtime/hydration.ts +99 -49
  27. package/src/runtime/lazy-batch.ts +205 -0
  28. package/src/runtime/orm-context.ts +539 -0
  29. package/src/runtime/relations/belongs-to.ts +92 -0
  30. package/src/runtime/relations/has-many.ts +111 -0
  31. package/src/runtime/relations/many-to-many.ts +149 -0
  32. package/src/schema/column.ts +15 -1
  33. package/src/schema/relation.ts +82 -58
  34. package/src/schema/table.ts +34 -22
  35. package/src/schema/types.ts +76 -0
  36. package/tests/orm-runtime.test.ts +254 -0
@@ -0,0 +1,254 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { SqliteDialect } from '../src/dialect/sqlite';
3
+ import { SelectQueryBuilder } from '../src/builder/select';
4
+ import { OrmContext, DbExecutor, QueryResult } from '../src/runtime/orm-context';
5
+ import { createEntityFromRow } from '../src/runtime/entity';
6
+ import { Users, Orders } from '../src/playground/features/playground/data/schema';
7
+ import { defineTable } from '../src/schema/table';
8
+ import { col } from '../src/schema/column';
9
+ import { hasMany, belongsTo, belongsToMany } from '../src/schema/relation';
10
+
11
+ const createMockExecutor = (responses: QueryResult[][]): {
12
+ executor: DbExecutor;
13
+ executed: Array<{ sql: string; params?: unknown[] }>;
14
+ } => {
15
+ const executed: Array<{ sql: string; params?: unknown[] }> = [];
16
+ let callIndex = 0;
17
+ const executor: DbExecutor = {
18
+ async executeSql(sql, params) {
19
+ executed.push({ sql, params });
20
+ const result = responses[callIndex] ?? [];
21
+ callIndex += 1;
22
+ return result;
23
+ }
24
+ };
25
+
26
+ return { executor, executed };
27
+ };
28
+
29
+ const TestOrders = defineTable('test_orders', {
30
+ id: col.primaryKey(col.int()),
31
+ user_id: col.int(),
32
+ total: col.int(),
33
+ status: col.varchar(50)
34
+ });
35
+
36
+ const TestProjects = defineTable('test_projects', {
37
+ id: col.primaryKey(col.int()),
38
+ name: col.varchar(255),
39
+ client: col.varchar(255)
40
+ });
41
+
42
+ const TestUserProjects = defineTable('test_user_projects', {
43
+ id: col.primaryKey(col.int()),
44
+ user_id: col.int(),
45
+ project_id: col.int()
46
+ });
47
+
48
+ const TestUsers = defineTable('test_users', {
49
+ id: col.primaryKey(col.int()),
50
+ name: col.varchar(255)
51
+ });
52
+
53
+ TestUsers.relations = {
54
+ orders: hasMany(TestOrders, 'user_id', undefined, 'remove'),
55
+ projects: belongsToMany(TestProjects, TestUserProjects, {
56
+ pivotForeignKeyToRoot: 'user_id',
57
+ pivotForeignKeyToTarget: 'project_id',
58
+ cascade: 'link'
59
+ })
60
+ };
61
+
62
+ TestOrders.relations = {
63
+ user: belongsTo(TestUsers, 'user_id')
64
+ };
65
+
66
+ TestProjects.relations = {};
67
+ TestUserProjects.relations = {};
68
+
69
+ describe('OrmContext entity graphs', () => {
70
+ it('lazy loads has-many relations in batches', async () => {
71
+ const responses: QueryResult[][] = [
72
+ [
73
+ {
74
+ columns: ['id', 'name'],
75
+ values: [[1, 'Alice']]
76
+ }
77
+ ],
78
+ [
79
+ {
80
+ columns: ['id', 'user_id', 'total', 'status'],
81
+ values: [
82
+ [10, 1, 100, 'open'],
83
+ [11, 1, 200, 'shipped']
84
+ ]
85
+ }
86
+ ]
87
+ ];
88
+ const { executor, executed } = createMockExecutor(responses);
89
+ const ctx = new OrmContext({ dialect: new SqliteDialect(), executor });
90
+
91
+ const builder = new SelectQueryBuilder(Users)
92
+ .select({
93
+ id: Users.columns.id,
94
+ name: Users.columns.name
95
+ })
96
+ .includeLazy('orders');
97
+
98
+ const [user] = await builder.execute(ctx);
99
+ expect(user).toBeDefined();
100
+ expect(executed).toHaveLength(1);
101
+
102
+ const orders = await user.orders.load();
103
+ expect(orders).toHaveLength(2);
104
+ expect(orders.map(order => order.total)).toEqual([100, 200]);
105
+ expect(executed[1].sql).toContain('"orders"');
106
+ });
107
+
108
+ it('preloads eager hydration data for has-many relations without extra queries', async () => {
109
+ const { executor, executed } = createMockExecutor([]);
110
+ const ctx = new OrmContext({ dialect: new SqliteDialect(), executor });
111
+
112
+ const row = {
113
+ id: 1,
114
+ name: 'Alice',
115
+ role: 'admin',
116
+ settings: '{}',
117
+ deleted_at: null,
118
+ orders: [
119
+ { id: 10, user_id: 1, total: 120, status: 'open' },
120
+ { id: 11, user_id: 1, total: 230, status: 'completed' }
121
+ ]
122
+ };
123
+
124
+ const user = createEntityFromRow(ctx, Users, row);
125
+ expect(user.orders.getItems()).toHaveLength(2);
126
+
127
+ const orders = await user.orders.load();
128
+ expect(orders).toHaveLength(2);
129
+ expect(orders.map(order => order.total)).toEqual([120, 230]);
130
+ expect(executed).toHaveLength(0);
131
+ });
132
+
133
+ it('reuses eagerly hydrated belongs-to data and lazily loads belongs-to-many', async () => {
134
+ const rootRow: QueryResult = {
135
+ columns: [
136
+ 'id',
137
+ 'user_id',
138
+ 'total',
139
+ 'status',
140
+ 'user__id',
141
+ 'user__name',
142
+ 'user__role',
143
+ 'user__settings',
144
+ 'user__deleted_at'
145
+ ],
146
+ values: [[42, 1, 15, 'pending', 1, 'Alice', 'admin', '{}', null]]
147
+ };
148
+
149
+ const responses: QueryResult[][] = [
150
+ [rootRow],
151
+ [
152
+ {
153
+ columns: ['id', 'project_id', 'user_id', 'role_id', 'assigned_at'],
154
+ values: [[1, 10, 1, 3, '2025-12-03']]
155
+ }
156
+ ],
157
+ [
158
+ {
159
+ columns: ['id', 'name', 'client'],
160
+ values: [[10, 'Apollo', 'Acme Corp']]
161
+ }
162
+ ]
163
+ ];
164
+
165
+ const { executor, executed } = createMockExecutor(responses);
166
+ const ctx = new OrmContext({ dialect: new SqliteDialect(), executor });
167
+
168
+ const builder = new SelectQueryBuilder(Orders)
169
+ .select({
170
+ id: Orders.columns.id,
171
+ user_id: Orders.columns.user_id,
172
+ total: Orders.columns.total,
173
+ status: Orders.columns.status
174
+ })
175
+ .include('user');
176
+
177
+ const [order] = await builder.execute(ctx);
178
+ expect(order).toBeDefined();
179
+ expect(executed).toHaveLength(1);
180
+
181
+ const user = await order.user.load();
182
+ expect(user).toBeDefined();
183
+ expect(user?.name).toBe('Alice');
184
+ expect(executed).toHaveLength(1);
185
+
186
+ const projects = await user!.projects.load();
187
+ expect(executed).toHaveLength(3);
188
+ expect(projects).toHaveLength(1);
189
+ expect(projects[0].name).toBe('Apollo');
190
+ expect((projects[0] as any)._pivot).toMatchObject({
191
+ project_id: 10,
192
+ user_id: 1,
193
+ id: 1
194
+ });
195
+ expect(executed[1].sql).toContain('"project_assignments"');
196
+ expect(executed[2].sql).toContain('"projects"');
197
+ });
198
+
199
+ it('flushes relation mutations through saveChanges with cascading SQL', async () => {
200
+ const responses: QueryResult[][] = [
201
+ [
202
+ {
203
+ columns: ['id', 'name'],
204
+ values: [[1, 'Manager']]
205
+ }
206
+ ],
207
+ [
208
+ {
209
+ columns: ['id', 'user_id', 'total', 'status'],
210
+ values: [[10, 1, 180, 'open']]
211
+ }
212
+ ],
213
+ [
214
+ {
215
+ columns: ['id', 'project_id', 'user_id'],
216
+ values: [[1, 100, 1]]
217
+ }
218
+ ],
219
+ [
220
+ {
221
+ columns: ['id', 'name', 'client'],
222
+ values: [[100, 'Apollo', 'Acme Corp']]
223
+ }
224
+ ]
225
+ ];
226
+
227
+ const { executor, executed } = createMockExecutor(responses);
228
+ const ctx = new OrmContext({ dialect: new SqliteDialect(), executor });
229
+
230
+ const [user] = await new SelectQueryBuilder(TestUsers)
231
+ .select({
232
+ id: TestUsers.columns.id,
233
+ name: TestUsers.columns.name
234
+ })
235
+ .execute(ctx);
236
+
237
+ const orders = await user.orders.load();
238
+ expect(orders).toHaveLength(1);
239
+
240
+ const removedOrder = orders[0];
241
+ user.orders.remove(removedOrder);
242
+ user.orders.add({ total: 999, status: 'pending' });
243
+
244
+ await user.projects.syncByIds([200]);
245
+
246
+ await ctx.saveChanges();
247
+
248
+ const payload = executed.slice(-4);
249
+ expect(payload[0].sql).toContain('INSERT INTO "test_orders"');
250
+ expect(payload[1].sql).toContain('INSERT INTO "test_user_projects"');
251
+ expect(payload[2].sql).toContain('DELETE FROM "test_user_projects"');
252
+ expect(payload[3].sql).toContain('DELETE FROM "test_orders"');
253
+ });
254
+ });