metal-orm 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +341 -146
- package/dist/decorators/index.cjs +2564 -0
- package/dist/decorators/index.cjs.map +1 -0
- package/dist/decorators/index.d.cts +53 -0
- package/dist/decorators/index.d.ts +53 -0
- package/dist/decorators/index.js +2530 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/index.cjs +4227 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +701 -0
- package/dist/index.d.ts +701 -0
- package/dist/index.js +4131 -0
- package/dist/index.js.map +1 -0
- package/dist/select-654m4qy8.d.cts +1522 -0
- package/dist/select-654m4qy8.d.ts +1522 -0
- package/package.json +27 -20
- package/src/codegen/typescript.ts +405 -393
- package/src/core/ast/aggregate-functions.ts +30 -0
- package/src/core/ast/builders.ts +43 -0
- package/src/core/ast/expression-builders.ts +310 -0
- package/src/core/ast/expression-nodes.ts +211 -0
- package/src/core/ast/expression-visitor.ts +99 -0
- package/src/core/ast/expression.ts +5 -0
- package/src/{utils → core/ast}/join-node.ts +20 -20
- package/src/{ast → core/ast}/join.ts +18 -18
- package/src/{ast → core/ast}/query.ts +113 -113
- package/src/core/ast/window-functions.ts +140 -0
- package/src/{dialect → core/dialect}/abstract.ts +94 -94
- package/src/{dialect → core/dialect}/mssql/index.ts +31 -31
- package/src/{dialect → core/dialect}/mysql/index.ts +31 -31
- package/src/{dialect → core/dialect}/postgres/index.ts +45 -45
- package/src/{dialect → core/dialect}/sqlite/index.ts +45 -45
- package/src/{constants → core/sql}/sql-operator-config.ts +39 -39
- package/src/decorators/bootstrap.ts +126 -0
- package/src/decorators/column.ts +78 -0
- package/src/decorators/entity.ts +36 -0
- package/src/decorators/index.ts +4 -0
- package/src/decorators/relations.ts +107 -0
- package/src/global.d.ts +1 -0
- package/src/index.ts +22 -22
- package/src/orm/db-executor.ts +11 -0
- package/src/orm/domain-event-bus.ts +52 -0
- package/src/{runtime → orm}/entity-meta.ts +52 -52
- package/src/orm/entity-metadata.ts +140 -0
- package/src/{runtime → orm}/entity.ts +252 -252
- package/src/{runtime → orm}/execute.ts +36 -36
- package/src/{runtime → orm}/hydration.ts +103 -103
- package/src/orm/identity-map.ts +37 -0
- package/src/{runtime → orm}/lazy-batch.ts +205 -205
- package/src/orm/orm-context.ts +154 -0
- package/src/orm/relation-change-processor.ts +140 -0
- package/src/{runtime → orm}/relations/belongs-to.ts +92 -92
- package/src/{runtime → orm}/relations/has-many.ts +111 -111
- package/src/{runtime → orm}/relations/many-to-many.ts +149 -149
- package/src/orm/runtime-types.ts +39 -0
- package/src/orm/transaction-runner.ts +17 -0
- package/src/orm/unit-of-work.ts +232 -0
- package/src/{builder/operations → query-builder}/column-selector.ts +78 -78
- package/src/{builder → query-builder}/delete-query-state.ts +38 -42
- package/src/{builder → query-builder}/delete.ts +46 -57
- package/src/{builder → query-builder}/hydration-manager.ts +87 -87
- package/src/{builder → query-builder}/hydration-planner.ts +182 -182
- package/src/{builder → query-builder}/insert-query-state.ts +51 -62
- package/src/{builder → query-builder}/insert.ts +48 -59
- package/src/{builder → query-builder}/query-ast-service.ts +208 -226
- package/src/{utils → query-builder}/raw-column-parser.ts +32 -32
- package/src/{builder → query-builder}/relation-conditions.ts +112 -112
- package/src/{builder/operations → query-builder}/relation-manager.ts +82 -82
- package/src/{builder → query-builder}/relation-projection-helper.ts +101 -101
- package/src/{builder → query-builder}/relation-service.ts +284 -284
- package/src/{builder → query-builder}/relation-types.ts +21 -21
- package/src/{builder → query-builder}/relation-utils.ts +12 -12
- package/src/{builder → query-builder}/select-query-builder-deps.ts +112 -94
- package/src/{builder → query-builder}/select-query-state.ts +179 -179
- package/src/{builder → query-builder}/select.ts +78 -69
- package/src/{builder → query-builder}/update-query-state.ts +55 -59
- package/src/{builder → query-builder}/update.ts +50 -61
- package/src/schema/column.ts +25 -25
- package/src/schema/relation.ts +116 -116
- package/src/schema/table.ts +34 -34
- package/src/schema/types.ts +76 -76
- package/.github/workflows/publish-metal-orm.yml +0 -38
- package/ROADMAP.md +0 -125
- package/docs/CHANGES.md +0 -104
- package/docs/advanced-features.md +0 -176
- package/docs/api-reference.md +0 -31
- package/docs/dml-operations.md +0 -156
- package/docs/getting-started.md +0 -171
- package/docs/hydration.md +0 -115
- package/docs/index.md +0 -36
- package/docs/multi-dialect-support.md +0 -59
- package/docs/query-builder.md +0 -135
- package/docs/runtime.md +0 -105
- package/docs/schema-definition.md +0 -112
- package/metadata.json +0 -5
- package/playground/api/playground-api.ts +0 -94
- package/playground/index.html +0 -15
- package/playground/src/App.css +0 -1
- package/playground/src/App.tsx +0 -114
- package/playground/src/components/CodeDisplay.tsx +0 -43
- package/playground/src/components/QueryExecutor.tsx +0 -189
- package/playground/src/components/ResultsTable.tsx +0 -67
- package/playground/src/components/ResultsTabs.tsx +0 -105
- package/playground/src/components/ScenarioList.tsx +0 -56
- package/playground/src/components/logo.svg +0 -45
- package/playground/src/data/scenarios.ts +0 -2
- package/playground/src/main.tsx +0 -9
- package/playground/src/services/PlaygroundApiService.ts +0 -60
- package/postcss.config.cjs +0 -5
- package/sql_sql-ansi-cheatsheet-2025.md +0 -264
- package/src/ast/expression.ts +0 -658
- package/src/builder/operations/cte-manager.ts +0 -34
- package/src/builder/operations/filter-manager.ts +0 -68
- package/src/builder/operations/join-manager.ts +0 -36
- package/src/builder/operations/pagination-manager.ts +0 -36
- package/src/playground/features/playground/api/types.ts +0 -16
- package/src/playground/features/playground/clients/MockClient.ts +0 -17
- package/src/playground/features/playground/clients/SqliteClient.ts +0 -57
- package/src/playground/features/playground/common/IDatabaseClient.ts +0 -10
- package/src/playground/features/playground/data/scenarios/aggregation.ts +0 -36
- package/src/playground/features/playground/data/scenarios/basics.ts +0 -25
- package/src/playground/features/playground/data/scenarios/edge_cases.ts +0 -57
- package/src/playground/features/playground/data/scenarios/filtering.ts +0 -94
- package/src/playground/features/playground/data/scenarios/hydration.ts +0 -27
- package/src/playground/features/playground/data/scenarios/index.ts +0 -29
- package/src/playground/features/playground/data/scenarios/ordering.ts +0 -25
- package/src/playground/features/playground/data/scenarios/pagination.ts +0 -16
- package/src/playground/features/playground/data/scenarios/relationships.ts +0 -75
- package/src/playground/features/playground/data/scenarios/types.ts +0 -70
- package/src/playground/features/playground/data/schema.ts +0 -91
- package/src/playground/features/playground/data/seed.ts +0 -104
- package/src/playground/features/playground/services/QueryExecutionService.ts +0 -121
- package/src/runtime/orm-context.ts +0 -539
- package/tests/belongs-to-many.test.ts +0 -57
- package/tests/between.test.ts +0 -43
- package/tests/case-expression.test.ts +0 -58
- package/tests/complex-exists.test.ts +0 -230
- package/tests/cte.test.ts +0 -118
- package/tests/dml.test.ts +0 -206
- package/tests/exists.test.ts +0 -127
- package/tests/like.test.ts +0 -33
- package/tests/orm-runtime.test.ts +0 -254
- package/tests/postgres.test.ts +0 -30
- package/tests/right-join.test.ts +0 -89
- package/tests/subquery-having.test.ts +0 -193
- package/tests/window-function.test.ts +0 -151
- package/tsconfig.json +0 -30
- package/tsup.config.ts +0 -10
- package/vite.config.ts +0 -22
- package/vitest.config.ts +0 -14
- /package/src/{constants → core/sql}/sql.ts +0 -0
- /package/src/{runtime → orm}/als.ts +0 -0
- /package/src/{utils → query-builder}/relation-alias.ts +0 -0
package/tests/like.test.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { like, notLike } from '../src/ast/expression';
|
|
3
|
-
import { Users } from '../src/playground/features/playground/data/schema';
|
|
4
|
-
import { SqliteDialect } from '../src/dialect/sqlite';
|
|
5
|
-
import { SelectQueryBuilder } from '../src/builder/select';
|
|
6
|
-
|
|
7
|
-
describe('like expressions', () => {
|
|
8
|
-
const dialect = new SqliteDialect();
|
|
9
|
-
|
|
10
|
-
it('compiles LIKE with an escape clause', () => {
|
|
11
|
-
const q = new SelectQueryBuilder(Users)
|
|
12
|
-
.selectRaw('*')
|
|
13
|
-
.where(like(Users.columns.name, 'Admin\\_%', '\\'));
|
|
14
|
-
|
|
15
|
-
const compiled = q.compile(dialect);
|
|
16
|
-
expect(compiled.sql).toBe(
|
|
17
|
-
'SELECT "users"."*" FROM "users" WHERE "users"."name" LIKE ? ESCAPE ?;'
|
|
18
|
-
);
|
|
19
|
-
expect(compiled.params).toEqual(['Admin\\_%', '\\']);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('compiles NOT LIKE with an escape clause', () => {
|
|
23
|
-
const q = new SelectQueryBuilder(Users)
|
|
24
|
-
.selectRaw('*')
|
|
25
|
-
.where(notLike(Users.columns.role, 'admin\\_%', '\\'));
|
|
26
|
-
|
|
27
|
-
const compiled = q.compile(dialect);
|
|
28
|
-
expect(compiled.sql).toBe(
|
|
29
|
-
'SELECT "users"."*" FROM "users" WHERE "users"."role" NOT LIKE ? ESCAPE ?;'
|
|
30
|
-
);
|
|
31
|
-
expect(compiled.params).toEqual(['admin\\_%', '\\']);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
@@ -1,254 +0,0 @@
|
|
|
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
|
-
});
|
package/tests/postgres.test.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { SelectQueryBuilder } from '../src/builder/select';
|
|
3
|
-
import { PostgresDialect } from '../src/dialect/postgres';
|
|
4
|
-
import { Users } from '../src/playground/features/playground/data/schema';
|
|
5
|
-
import { jsonPath, eq } from '../src/ast/expression';
|
|
6
|
-
|
|
7
|
-
describe('PostgresDialect', () => {
|
|
8
|
-
it('should compile a simple select', () => {
|
|
9
|
-
const query = new SelectQueryBuilder(Users).selectRaw('*');
|
|
10
|
-
const dialect = new PostgresDialect();
|
|
11
|
-
const compiled = query.compile(dialect);
|
|
12
|
-
expect(compiled.sql).toBe('SELECT "users"."*" FROM "users";');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should compile a select with a where clause', () => {
|
|
16
|
-
const query = new SelectQueryBuilder(Users).selectRaw('*').where(eq(Users.columns.id, 1));
|
|
17
|
-
const dialect = new PostgresDialect();
|
|
18
|
-
const compiled = query.compile(dialect);
|
|
19
|
-
expect(compiled.sql).toBe('SELECT "users"."*" FROM "users" WHERE "users"."id" = ?;');
|
|
20
|
-
expect(compiled.params).toEqual([1]);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should compile a select with a json path', () => {
|
|
24
|
-
const query = new SelectQueryBuilder(Users).selectRaw('*').where(eq(jsonPath(Users.columns.settings, '$.first'), 'John'));
|
|
25
|
-
const dialect = new PostgresDialect();
|
|
26
|
-
const compiled = query.compile(dialect);
|
|
27
|
-
expect(compiled.sql).toBe('SELECT "users"."*" FROM "users" WHERE "users"."settings"->>\'$.first\' = ?;');
|
|
28
|
-
expect(compiled.params).toEqual(['John']);
|
|
29
|
-
});
|
|
30
|
-
});
|
package/tests/right-join.test.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { SelectQueryBuilder } from '../src/builder/select';
|
|
3
|
-
import { TableDef } from '../src/schema/table';
|
|
4
|
-
import { eq } from '../src/ast/expression';
|
|
5
|
-
import { SqliteDialect } from '../src/dialect/sqlite';
|
|
6
|
-
import { MySqlDialect } from '../src/dialect/mysql';
|
|
7
|
-
import { SqlServerDialect } from '../src/dialect/mssql';
|
|
8
|
-
|
|
9
|
-
const Users: TableDef = {
|
|
10
|
-
name: 'users',
|
|
11
|
-
columns: {
|
|
12
|
-
id: { name: 'id', type: 'integer' },
|
|
13
|
-
name: { name: 'name', type: 'text' }
|
|
14
|
-
},
|
|
15
|
-
relations: {
|
|
16
|
-
orders: {
|
|
17
|
-
type: 'HAS_MANY',
|
|
18
|
-
target: {
|
|
19
|
-
name: 'orders',
|
|
20
|
-
columns: {
|
|
21
|
-
id: { name: 'id', type: 'integer' },
|
|
22
|
-
user_id: { name: 'user_id', type: 'integer' },
|
|
23
|
-
total: { name: 'total', type: 'integer' }
|
|
24
|
-
},
|
|
25
|
-
relations: {}
|
|
26
|
-
},
|
|
27
|
-
foreignKey: 'user_id',
|
|
28
|
-
localKey: 'id'
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const Orders = Users.relations.orders.target;
|
|
34
|
-
|
|
35
|
-
describe('RIGHT JOIN Support', () => {
|
|
36
|
-
it('should generate correct SQL for manual rightJoin', () => {
|
|
37
|
-
const qb = new SelectQueryBuilder(Users)
|
|
38
|
-
.select({
|
|
39
|
-
user: { ...Users.columns.name, table: 'users' },
|
|
40
|
-
total: { ...Orders.columns.total, table: 'orders' }
|
|
41
|
-
})
|
|
42
|
-
.rightJoin(Orders, eq({ ...Users.columns.id, table: 'users' }, { ...Orders.columns.user_id, table: 'orders' }));
|
|
43
|
-
|
|
44
|
-
const sqlite = new SqliteDialect();
|
|
45
|
-
expect(qb.toSql(sqlite)).toBe('SELECT "users"."name" AS "user", "orders"."total" AS "total" FROM "users" RIGHT JOIN "orders" ON "users"."id" = "orders"."user_id";');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('should generate correct SQL for manual leftJoin', () => {
|
|
49
|
-
const qb = new SelectQueryBuilder(Users)
|
|
50
|
-
.select({
|
|
51
|
-
user: { ...Users.columns.name, table: 'users' },
|
|
52
|
-
total: { ...Orders.columns.total, table: 'orders' }
|
|
53
|
-
})
|
|
54
|
-
.leftJoin(Orders, eq({ ...Users.columns.id, table: 'users' }, { ...Orders.columns.user_id, table: 'orders' }));
|
|
55
|
-
|
|
56
|
-
const sqlite = new SqliteDialect();
|
|
57
|
-
expect(qb.toSql(sqlite)).toBe('SELECT "users"."name" AS "user", "orders"."total" AS "total" FROM "users" LEFT JOIN "orders" ON "users"."id" = "orders"."user_id";');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should generate correct SQL for joinRelation with RIGHT kind', () => {
|
|
61
|
-
const qb = new SelectQueryBuilder(Users)
|
|
62
|
-
.select({
|
|
63
|
-
user: { ...Users.columns.name, table: 'users' },
|
|
64
|
-
total: { ...Orders.columns.total, table: 'orders' }
|
|
65
|
-
})
|
|
66
|
-
.joinRelation('orders', 'RIGHT');
|
|
67
|
-
|
|
68
|
-
const sqlite = new SqliteDialect();
|
|
69
|
-
expect(qb.toSql(sqlite)).toBe('SELECT "users"."name" AS "user", "orders"."total" AS "total" FROM "users" RIGHT JOIN "orders" ON "orders"."user_id" = "users"."id";');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should work with MySQL dialect', () => {
|
|
73
|
-
const qb = new SelectQueryBuilder(Users)
|
|
74
|
-
.select({ user: { ...Users.columns.name, table: 'users' } })
|
|
75
|
-
.rightJoin(Orders, eq({ ...Users.columns.id, table: 'users' }, { ...Orders.columns.user_id, table: 'orders' }));
|
|
76
|
-
|
|
77
|
-
const mysql = new MySqlDialect();
|
|
78
|
-
expect(qb.toSql(mysql)).toBe('SELECT `users`.`name` AS `user` FROM `users` RIGHT JOIN `orders` ON `users`.`id` = `orders`.`user_id`;');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should work with MSSQL dialect', () => {
|
|
82
|
-
const qb = new SelectQueryBuilder(Users)
|
|
83
|
-
.select({ user: { ...Users.columns.name, table: 'users' } })
|
|
84
|
-
.rightJoin(Orders, eq({ ...Users.columns.id, table: 'users' }, { ...Orders.columns.user_id, table: 'orders' }));
|
|
85
|
-
|
|
86
|
-
const mssql = new SqlServerDialect();
|
|
87
|
-
expect(qb.toSql(mssql)).toBe('SELECT [users].[name] AS [user] FROM [users] RIGHT JOIN [orders] ON [users].[id] = [orders].[user_id];');
|
|
88
|
-
});
|
|
89
|
-
});
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { SelectQueryBuilder } from '../src/builder/select';
|
|
3
|
-
import { SqliteDialect } from '../src/dialect/sqlite';
|
|
4
|
-
import { MySqlDialect } from '../src/dialect/mysql';
|
|
5
|
-
import { SqlServerDialect } from '../src/dialect/mssql';
|
|
6
|
-
import { TableDef, defineTable } from '../src/schema/table';
|
|
7
|
-
import { col } from '../src/schema/column';
|
|
8
|
-
import { eq, gt, lt, avg, count, sum } from '../src/ast/expression';
|
|
9
|
-
|
|
10
|
-
// Define test schema
|
|
11
|
-
const Users = defineTable('users', {
|
|
12
|
-
id: col.primaryKey(col.int()),
|
|
13
|
-
name: col.varchar(255),
|
|
14
|
-
age: col.int(),
|
|
15
|
-
}, {});
|
|
16
|
-
|
|
17
|
-
const Orders = defineTable('orders', {
|
|
18
|
-
id: col.primaryKey(col.int()),
|
|
19
|
-
user_id: col.int(),
|
|
20
|
-
status: col.varchar(50),
|
|
21
|
-
total: col.int(),
|
|
22
|
-
}, {});
|
|
23
|
-
|
|
24
|
-
const Profiles = defineTable('profiles', {
|
|
25
|
-
id: col.primaryKey(col.int()),
|
|
26
|
-
user_id: col.int(),
|
|
27
|
-
display_name: col.varchar(255),
|
|
28
|
-
}, {});
|
|
29
|
-
|
|
30
|
-
const Contributions = defineTable('contributions', {
|
|
31
|
-
id: col.primaryKey(col.int()),
|
|
32
|
-
project_id: col.int(),
|
|
33
|
-
hours: col.int(),
|
|
34
|
-
}, {});
|
|
35
|
-
|
|
36
|
-
const sqlite = new SqliteDialect();
|
|
37
|
-
const mysql = new MySqlDialect();
|
|
38
|
-
const sqlserver = new SqlServerDialect();
|
|
39
|
-
|
|
40
|
-
describe('Scalar Subqueries', () => {
|
|
41
|
-
describe('selectSubquery in SELECT projection', () => {
|
|
42
|
-
it('should add scalar subquery to SELECT (SQLite)', () => {
|
|
43
|
-
const profileSubquery = new SelectQueryBuilder(Profiles)
|
|
44
|
-
.select({ display_name: Profiles.columns.display_name })
|
|
45
|
-
.where(eq(Profiles.columns.user_id, Users.columns.id));
|
|
46
|
-
|
|
47
|
-
const query = new SelectQueryBuilder(Users)
|
|
48
|
-
.select({ id: Users.columns.id })
|
|
49
|
-
.selectSubquery('profile_name', profileSubquery);
|
|
50
|
-
|
|
51
|
-
const sql = query.toSql(sqlite);
|
|
52
|
-
|
|
53
|
-
expect(sql).toContain('(SELECT');
|
|
54
|
-
expect(sql).toContain('"profiles"."display_name"');
|
|
55
|
-
expect(sql).toContain('AS "profile_name"');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should support multiple scalar subqueries', () => {
|
|
59
|
-
const orderCountSub = new SelectQueryBuilder(Orders)
|
|
60
|
-
.select({ cnt: count(Orders.columns.id) })
|
|
61
|
-
.where(eq(Orders.columns.user_id, Users.columns.id));
|
|
62
|
-
|
|
63
|
-
const totalSpentSub = new SelectQueryBuilder(Orders)
|
|
64
|
-
.select({ total: sum(Orders.columns.total) })
|
|
65
|
-
.where(eq(Orders.columns.user_id, Users.columns.id));
|
|
66
|
-
|
|
67
|
-
const query = new SelectQueryBuilder(Users)
|
|
68
|
-
.select({ id: Users.columns.id })
|
|
69
|
-
.selectSubquery('order_count', orderCountSub)
|
|
70
|
-
.selectSubquery('total_spent', totalSpentSub);
|
|
71
|
-
|
|
72
|
-
const sql = query.toSql(sqlite);
|
|
73
|
-
expect(sql).toContain('AS "order_count"');
|
|
74
|
-
expect(sql).toContain('AS "total_spent"');
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
describe('Scalar subquery in WHERE clause', () => {
|
|
79
|
-
it('should support scalar subquery with gt operator', () => {
|
|
80
|
-
const avgAgeSub = new SelectQueryBuilder(Users)
|
|
81
|
-
.select({ avg_age: avg(Users.columns.age) });
|
|
82
|
-
|
|
83
|
-
const query = new SelectQueryBuilder(Users)
|
|
84
|
-
.select({ id: Users.columns.id, age: Users.columns.age })
|
|
85
|
-
.where(gt(Users.columns.age, {
|
|
86
|
-
type: 'ScalarSubquery',
|
|
87
|
-
query: avgAgeSub.getAST()
|
|
88
|
-
}));
|
|
89
|
-
|
|
90
|
-
const sql = query.toSql(sqlite);
|
|
91
|
-
expect(sql).toContain('"users"."age" >');
|
|
92
|
-
expect(sql).toContain('(SELECT');
|
|
93
|
-
expect(sql).toContain('AVG("users"."age")');
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe('HAVING Clause', () => {
|
|
99
|
-
describe('Basic HAVING with aggregates', () => {
|
|
100
|
-
it('should add HAVING clause with COUNT (SQLite)', () => {
|
|
101
|
-
const query = new SelectQueryBuilder(Contributions)
|
|
102
|
-
.select({ project_id: Contributions.columns.project_id, cnt: count(Contributions.columns.id) })
|
|
103
|
-
.groupBy(Contributions.columns.project_id)
|
|
104
|
-
.having(gt(count(Contributions.columns.id), 10));
|
|
105
|
-
|
|
106
|
-
const compiled = query.compile(sqlite);
|
|
107
|
-
const { sql, params } = compiled;
|
|
108
|
-
|
|
109
|
-
expect(sql).toContain('GROUP BY');
|
|
110
|
-
expect(sql).toContain('HAVING');
|
|
111
|
-
expect(sql).toContain('COUNT("contributions"."id") > ?');
|
|
112
|
-
expect(params).toEqual([10]);
|
|
113
|
-
|
|
114
|
-
const groupByIndex = sql.indexOf('GROUP BY');
|
|
115
|
-
const havingIndex = sql.indexOf('HAVING');
|
|
116
|
-
expect(havingIndex).toBeGreaterThan(groupByIndex);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should add HAVING clause with SUM (MySQL)', () => {
|
|
120
|
-
const query = new SelectQueryBuilder(Contributions)
|
|
121
|
-
.select({ project_id: Contributions.columns.project_id, hours: sum(Contributions.columns.hours) })
|
|
122
|
-
.groupBy(Contributions.columns.project_id)
|
|
123
|
-
.having(gt(sum(Contributions.columns.hours), 100));
|
|
124
|
-
|
|
125
|
-
const compiled = query.compile(mysql);
|
|
126
|
-
const { sql, params } = compiled;
|
|
127
|
-
|
|
128
|
-
expect(sql).toContain('GROUP BY');
|
|
129
|
-
expect(sql).toContain('HAVING');
|
|
130
|
-
expect(sql).toContain('SUM(`contributions`.`hours`) > ?');
|
|
131
|
-
expect(params).toEqual([100]);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should add HAVING clause with AVG (SQL Server)', () => {
|
|
135
|
-
const query = new SelectQueryBuilder(Orders)
|
|
136
|
-
.select({ user_id: Orders.columns.user_id, avg_total: avg(Orders.columns.total) })
|
|
137
|
-
.groupBy(Orders.columns.user_id)
|
|
138
|
-
.having(gt(avg(Orders.columns.total), 500));
|
|
139
|
-
|
|
140
|
-
const compiled = query.compile(sqlserver);
|
|
141
|
-
const { sql, params } = compiled;
|
|
142
|
-
|
|
143
|
-
expect(sql).toContain('GROUP BY');
|
|
144
|
-
expect(sql).toContain('HAVING');
|
|
145
|
-
expect(sql).toContain('AVG([orders].[total]) > @p1');
|
|
146
|
-
expect(params).toEqual([500]);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe('Multiple HAVING conditions', () => {
|
|
151
|
-
it('should combine multiple HAVING calls with AND', () => {
|
|
152
|
-
const query = new SelectQueryBuilder(Contributions)
|
|
153
|
-
.select({ project_id: Contributions.columns.project_id, cnt: count(Contributions.columns.id) })
|
|
154
|
-
.groupBy(Contributions.columns.project_id)
|
|
155
|
-
.having(gt(count(Contributions.columns.id), 5))
|
|
156
|
-
.having(lt(sum(Contributions.columns.hours), 200));
|
|
157
|
-
|
|
158
|
-
const compiled = query.compile(sqlite);
|
|
159
|
-
const { sql, params } = compiled;
|
|
160
|
-
|
|
161
|
-
expect(sql).toContain('HAVING');
|
|
162
|
-
expect(sql).toContain('AND');
|
|
163
|
-
expect(sql).toContain('COUNT("contributions"."id") > ?');
|
|
164
|
-
expect(sql).toContain('SUM("contributions"."hours") < ?');
|
|
165
|
-
expect(params).toEqual([5, 200]);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe('SQL clause ordering', () => {
|
|
170
|
-
it('should maintain correct clause order: WHERE > GROUP BY > HAVING > ORDER BY', () => {
|
|
171
|
-
const query = new SelectQueryBuilder(Orders)
|
|
172
|
-
.select({ status: Orders.columns.status, cnt: count(Orders.columns.id) })
|
|
173
|
-
.where(gt(Orders.columns.total, 0))
|
|
174
|
-
.groupBy(Orders.columns.status)
|
|
175
|
-
.having(gt(count(Orders.columns.id), 10))
|
|
176
|
-
.orderBy(Orders.columns.status, 'ASC');
|
|
177
|
-
|
|
178
|
-
const compiled = query.compile(sqlite);
|
|
179
|
-
const { sql, params } = compiled;
|
|
180
|
-
|
|
181
|
-
const whereIdx = sql.indexOf('WHERE');
|
|
182
|
-
const groupByIdx = sql.indexOf('GROUP BY');
|
|
183
|
-
const havingIdx = sql.indexOf('HAVING');
|
|
184
|
-
const orderByIdx = sql.indexOf('ORDER BY');
|
|
185
|
-
|
|
186
|
-
expect(whereIdx).toBeGreaterThan(-1);
|
|
187
|
-
expect(groupByIdx).toBeGreaterThan(whereIdx);
|
|
188
|
-
expect(havingIdx).toBeGreaterThan(groupByIdx);
|
|
189
|
-
expect(orderByIdx).toBeGreaterThan(havingIdx);
|
|
190
|
-
expect(params).toEqual([0, 10]);
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
});
|