@objectstack/driver-sql 4.0.4 → 4.1.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.
- package/README.md +61 -4
- package/dist/index.d.mts +175 -2
- package/dist/index.d.ts +175 -2
- package/dist/index.js +488 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +488 -24
- package/dist/index.mjs.map +1 -1
- package/package.json +35 -9
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -52
- package/src/index.ts +0 -30
- package/src/sql-driver-advanced.test.ts +0 -499
- package/src/sql-driver-introspection.test.ts +0 -164
- package/src/sql-driver-queryast.test.ts +0 -243
- package/src/sql-driver-schema.test.ts +0 -313
- package/src/sql-driver.test.ts +0 -108
- package/src/sql-driver.ts +0 -1388
- package/tsconfig.json +0 -32
|
@@ -1,499 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
4
|
-
import { SqlDriver } from '../src/index.js';
|
|
5
|
-
|
|
6
|
-
describe('SqlDriver Advanced Operations (SQLite)', () => {
|
|
7
|
-
let driver: SqlDriver;
|
|
8
|
-
let knexInstance: any;
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
driver = new SqlDriver({
|
|
12
|
-
client: 'better-sqlite3',
|
|
13
|
-
connection: { filename: ':memory:' },
|
|
14
|
-
useNullAsDefault: true,
|
|
15
|
-
});
|
|
16
|
-
knexInstance = (driver as any).knex;
|
|
17
|
-
|
|
18
|
-
await knexInstance.schema.createTable('orders', (t: any) => {
|
|
19
|
-
t.string('id').primary();
|
|
20
|
-
t.string('customer');
|
|
21
|
-
t.string('product');
|
|
22
|
-
t.float('amount');
|
|
23
|
-
t.integer('quantity');
|
|
24
|
-
t.string('status');
|
|
25
|
-
t.timestamp('created_at').defaultTo(knexInstance.fn.now());
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
await knexInstance('orders').insert([
|
|
29
|
-
{ id: '1', customer: 'Alice', product: 'Laptop', amount: 1200.0, quantity: 1, status: 'completed' },
|
|
30
|
-
{ id: '2', customer: 'Bob', product: 'Mouse', amount: 25.5, quantity: 2, status: 'completed' },
|
|
31
|
-
{ id: '3', customer: 'Alice', product: 'Keyboard', amount: 75.0, quantity: 1, status: 'pending' },
|
|
32
|
-
{ id: '4', customer: 'Charlie', product: 'Monitor', amount: 350.0, quantity: 1, status: 'completed' },
|
|
33
|
-
{ id: '5', customer: 'Bob', product: 'Laptop', amount: 1200.0, quantity: 1, status: 'cancelled' },
|
|
34
|
-
]);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
afterEach(async () => {
|
|
38
|
-
await knexInstance.destroy();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe('Aggregate Operations', () => {
|
|
42
|
-
it('should sum values', async () => {
|
|
43
|
-
const result = await driver.aggregate('orders', {
|
|
44
|
-
where: { status: 'completed' },
|
|
45
|
-
aggregate: [{ func: 'sum', field: 'amount', alias: 'total_amount' }],
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
expect(result).toHaveLength(1);
|
|
49
|
-
expect(result[0].total_amount).toBe(1575.5);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should count records', async () => {
|
|
53
|
-
const result = await driver.aggregate('orders', {
|
|
54
|
-
aggregate: [{ func: 'count', field: '*', alias: 'total_orders' }],
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
expect(result).toHaveLength(1);
|
|
58
|
-
expect(result[0].total_orders).toBe(5);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should calculate average', async () => {
|
|
62
|
-
const result = await driver.aggregate('orders', {
|
|
63
|
-
where: { status: 'completed' },
|
|
64
|
-
aggregate: [{ func: 'avg', field: 'amount', alias: 'avg_amount' }],
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
expect(result).toHaveLength(1);
|
|
68
|
-
expect(result[0].avg_amount).toBeCloseTo(525.17, 2);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('should find min and max values', async () => {
|
|
72
|
-
const result = await driver.aggregate('orders', {
|
|
73
|
-
aggregate: [
|
|
74
|
-
{ func: 'min', field: 'amount', alias: 'min_amount' },
|
|
75
|
-
{ func: 'max', field: 'amount', alias: 'max_amount' },
|
|
76
|
-
],
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
expect(result).toHaveLength(1);
|
|
80
|
-
expect(result[0].min_amount).toBe(25.5);
|
|
81
|
-
expect(result[0].max_amount).toBe(1200.0);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should group by with aggregates', async () => {
|
|
85
|
-
const result = await driver.aggregate('orders', {
|
|
86
|
-
groupBy: ['customer'],
|
|
87
|
-
aggregate: [
|
|
88
|
-
{ func: 'sum', field: 'amount', alias: 'total_spent' },
|
|
89
|
-
{ func: 'count', field: '*', alias: 'order_count' },
|
|
90
|
-
],
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
expect(result).toHaveLength(3);
|
|
94
|
-
|
|
95
|
-
const alice = result.find((r: any) => r.customer === 'Alice');
|
|
96
|
-
expect(alice.total_spent).toBe(1275.0);
|
|
97
|
-
expect(alice.order_count).toBe(2);
|
|
98
|
-
|
|
99
|
-
const bob = result.find((r: any) => r.customer === 'Bob');
|
|
100
|
-
expect(bob.total_spent).toBe(1225.5);
|
|
101
|
-
expect(bob.order_count).toBe(2);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('should handle multiple group by fields', async () => {
|
|
105
|
-
const result = await driver.aggregate('orders', {
|
|
106
|
-
groupBy: ['customer', 'status'],
|
|
107
|
-
aggregate: [{ func: 'sum', field: 'quantity', alias: 'total_qty' }],
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
expect(result.length).toBeGreaterThan(0);
|
|
111
|
-
|
|
112
|
-
const aliceCompleted = result.find((r: any) => r.customer === 'Alice' && r.status === 'completed');
|
|
113
|
-
expect(aliceCompleted).toBeDefined();
|
|
114
|
-
expect(aliceCompleted.total_qty).toBe(1);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should aggregate with filters and groupBy', async () => {
|
|
118
|
-
const result = await driver.aggregate('orders', {
|
|
119
|
-
where: { status: { $ne: 'cancelled' } },
|
|
120
|
-
groupBy: ['product'],
|
|
121
|
-
aggregate: [{ func: 'sum', field: 'quantity', alias: 'total_quantity' }],
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const laptop = result.find((r: any) => r.product === 'Laptop');
|
|
125
|
-
expect(laptop.total_quantity).toBe(1);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('Bulk Operations', () => {
|
|
130
|
-
it('should create many records', async () => {
|
|
131
|
-
const newOrders = [
|
|
132
|
-
{ id: '6', customer: 'Dave', product: 'Tablet', amount: 500.0, quantity: 1, status: 'pending' },
|
|
133
|
-
{ id: '7', customer: 'Eve', product: 'Phone', amount: 800.0, quantity: 1, status: 'pending' },
|
|
134
|
-
{ id: '8', customer: 'Frank', product: 'Headphones', amount: 150.0, quantity: 2, status: 'completed' },
|
|
135
|
-
];
|
|
136
|
-
|
|
137
|
-
const result = await driver.bulkCreate('orders', newOrders);
|
|
138
|
-
|
|
139
|
-
expect(result).toBeDefined();
|
|
140
|
-
expect(result.length).toBe(3);
|
|
141
|
-
|
|
142
|
-
const count = await driver.count('orders', {});
|
|
143
|
-
expect(count).toBe(8);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('should update many records', async () => {
|
|
147
|
-
const result = await driver.updateMany('orders', { where: { status: 'pending' } } as any, { status: 'processing' });
|
|
148
|
-
|
|
149
|
-
expect(result).toBeGreaterThan(0);
|
|
150
|
-
|
|
151
|
-
const results = await driver.find('orders', { where: { status: 'processing' } });
|
|
152
|
-
expect(results.length).toBe(1);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('should delete many records', async () => {
|
|
156
|
-
const result = await driver.deleteMany('orders', { where: { status: 'cancelled' } } as any);
|
|
157
|
-
|
|
158
|
-
expect(result).toBe(1);
|
|
159
|
-
|
|
160
|
-
const remaining = await driver.count('orders', {});
|
|
161
|
-
expect(remaining).toBe(4);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('should handle empty bulk update and delete', async () => {
|
|
165
|
-
const result = await driver.updateMany('orders', { where: { status: 'nonexistent' } } as any, { status: 'updated' });
|
|
166
|
-
expect(result).toBe(0);
|
|
167
|
-
|
|
168
|
-
const deleteResult = await driver.deleteMany('orders', { where: { id: 'nonexistent' } } as any);
|
|
169
|
-
expect(deleteResult).toBe(0);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe('Transaction Support', () => {
|
|
174
|
-
it('should commit a transaction', async () => {
|
|
175
|
-
const trx = await driver.beginTransaction();
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
await driver.create(
|
|
179
|
-
'orders',
|
|
180
|
-
{
|
|
181
|
-
id: 'trx1',
|
|
182
|
-
customer: 'TxUser',
|
|
183
|
-
product: 'Item',
|
|
184
|
-
amount: 100.0,
|
|
185
|
-
quantity: 1,
|
|
186
|
-
status: 'completed',
|
|
187
|
-
},
|
|
188
|
-
{ transaction: trx },
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
await driver.commitTransaction(trx);
|
|
192
|
-
|
|
193
|
-
const result = await driver.findOne('orders', 'trx1' as any);
|
|
194
|
-
expect(result).toBeDefined();
|
|
195
|
-
expect(result.customer).toBe('TxUser');
|
|
196
|
-
} catch (e) {
|
|
197
|
-
await driver.rollbackTransaction(trx);
|
|
198
|
-
throw e;
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('should rollback a transaction', async () => {
|
|
203
|
-
const trx = await driver.beginTransaction();
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
await driver.create(
|
|
207
|
-
'orders',
|
|
208
|
-
{
|
|
209
|
-
id: 'trx2',
|
|
210
|
-
customer: 'TxUser2',
|
|
211
|
-
product: 'Item2',
|
|
212
|
-
amount: 200.0,
|
|
213
|
-
quantity: 1,
|
|
214
|
-
status: 'completed',
|
|
215
|
-
},
|
|
216
|
-
{ transaction: trx },
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
await driver.rollbackTransaction(trx);
|
|
220
|
-
|
|
221
|
-
const result = await driver.findOne('orders', 'trx2' as any);
|
|
222
|
-
expect(result).toBeNull();
|
|
223
|
-
} catch (e) {
|
|
224
|
-
await driver.rollbackTransaction(trx);
|
|
225
|
-
throw e;
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('should handle multiple operations in a transaction', async () => {
|
|
230
|
-
const trx = await driver.beginTransaction();
|
|
231
|
-
|
|
232
|
-
try {
|
|
233
|
-
await driver.create(
|
|
234
|
-
'orders',
|
|
235
|
-
{
|
|
236
|
-
id: 'trx3',
|
|
237
|
-
customer: 'MultiOp',
|
|
238
|
-
product: 'Product1',
|
|
239
|
-
amount: 100.0,
|
|
240
|
-
quantity: 1,
|
|
241
|
-
status: 'pending',
|
|
242
|
-
},
|
|
243
|
-
{ transaction: trx },
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
await driver.update('orders', '1', { status: 'shipped' }, { transaction: trx });
|
|
247
|
-
|
|
248
|
-
await driver.delete('orders', '5', { transaction: trx });
|
|
249
|
-
|
|
250
|
-
await driver.commitTransaction(trx);
|
|
251
|
-
|
|
252
|
-
const created = await driver.findOne('orders', 'trx3' as any);
|
|
253
|
-
expect(created).toBeDefined();
|
|
254
|
-
|
|
255
|
-
const updated = await driver.findOne('orders', '1' as any);
|
|
256
|
-
expect(updated.status).toBe('shipped');
|
|
257
|
-
|
|
258
|
-
const deleted = await driver.findOne('orders', '5' as any);
|
|
259
|
-
expect(deleted).toBeNull();
|
|
260
|
-
} catch (e) {
|
|
261
|
-
await driver.rollbackTransaction(trx);
|
|
262
|
-
throw e;
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
describe('Edge Cases and Error Handling', () => {
|
|
268
|
-
it('should handle empty filters gracefully', async () => {
|
|
269
|
-
const results = await driver.find('orders', { where: {} });
|
|
270
|
-
expect(results.length).toBe(5);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('should handle undefined query parameters', async () => {
|
|
274
|
-
const results = await driver.find('orders', {});
|
|
275
|
-
expect(results.length).toBe(5);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('should handle null values in data', async () => {
|
|
279
|
-
await knexInstance.schema.createTable('nullable_test', (t: any) => {
|
|
280
|
-
t.string('id').primary();
|
|
281
|
-
t.string('name').nullable();
|
|
282
|
-
t.integer('value').nullable();
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
await driver.create('nullable_test', { id: '1', name: null, value: null });
|
|
286
|
-
|
|
287
|
-
const result = await driver.findOne('nullable_test', '1' as any);
|
|
288
|
-
expect(result).toBeDefined();
|
|
289
|
-
expect(result.name).toBeNull();
|
|
290
|
-
expect(result.value).toBeNull();
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('should handle pagination with offset and limit', async () => {
|
|
294
|
-
const page1 = await driver.find('orders', {
|
|
295
|
-
orderBy: [{ field: 'id', order: 'asc' }],
|
|
296
|
-
offset: 0,
|
|
297
|
-
limit: 2,
|
|
298
|
-
});
|
|
299
|
-
expect(page1.length).toBe(2);
|
|
300
|
-
expect(page1[0].id).toBe('1');
|
|
301
|
-
|
|
302
|
-
const page2 = await driver.find('orders', {
|
|
303
|
-
orderBy: [{ field: 'id', order: 'asc' }],
|
|
304
|
-
offset: 2,
|
|
305
|
-
limit: 2,
|
|
306
|
-
});
|
|
307
|
-
expect(page2.length).toBe(2);
|
|
308
|
-
expect(page2[0].id).toBe('3');
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it('should handle offset beyond total records', async () => {
|
|
312
|
-
const results = await driver.find('orders', { offset: 100, limit: 10 });
|
|
313
|
-
expect(results.length).toBe(0);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it('should handle complex nested filters', async () => {
|
|
317
|
-
const results = await driver.find('orders', {
|
|
318
|
-
where: {
|
|
319
|
-
$or: [
|
|
320
|
-
{ $and: [{ status: 'completed' }, { amount: { $gt: 100 } }] },
|
|
321
|
-
{ $and: [{ customer: 'Alice' }, { status: 'pending' }] },
|
|
322
|
-
],
|
|
323
|
-
},
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
expect(results.length).toBeGreaterThan(0);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it('should handle contains filter', async () => {
|
|
330
|
-
const results = await driver.find('orders', {
|
|
331
|
-
where: { product: { $contains: 'top' } },
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
expect(results.length).toBe(2);
|
|
335
|
-
expect(results.every((r: any) => r.product.toLowerCase().includes('top'))).toBe(true);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it('should handle in filter', async () => {
|
|
339
|
-
const results = await driver.find('orders', {
|
|
340
|
-
where: { status: { $in: ['completed', 'pending'] } },
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
expect(results.length).toBe(4);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('should handle nin (not in) filter', async () => {
|
|
347
|
-
const results = await driver.find('orders', {
|
|
348
|
-
where: { status: { $nin: ['cancelled'] } },
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
expect(results.length).toBe(4);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('should handle findOne with query parameter', async () => {
|
|
355
|
-
const result = await driver.findOne('orders', { where: { customer: 'Charlie' } });
|
|
356
|
-
|
|
357
|
-
expect(result).toBeDefined();
|
|
358
|
-
expect(result.customer).toBe('Charlie');
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('should return null for non-existent record', async () => {
|
|
362
|
-
const result = await driver.findOne('orders', 'nonexistent' as any);
|
|
363
|
-
expect(result).toBeNull();
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it('should handle count with complex filters', async () => {
|
|
367
|
-
const count = await driver.count('orders', {
|
|
368
|
-
where: { $and: [{ status: 'completed' }, { amount: { $gt: 100 } }] },
|
|
369
|
-
} as any);
|
|
370
|
-
|
|
371
|
-
expect(count).toBe(2);
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
describe('Distinct Method', () => {
|
|
376
|
-
it('should get distinct values for a field', async () => {
|
|
377
|
-
const statuses = await driver.distinct('orders', 'status');
|
|
378
|
-
|
|
379
|
-
expect(statuses).toBeDefined();
|
|
380
|
-
expect(Array.isArray(statuses)).toBe(true);
|
|
381
|
-
expect(statuses).toHaveLength(3);
|
|
382
|
-
expect(statuses).toEqual(expect.arrayContaining(['cancelled', 'completed', 'pending']));
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it('should get distinct values with filters', async () => {
|
|
386
|
-
const products = await driver.distinct('orders', 'product', { status: 'completed' });
|
|
387
|
-
|
|
388
|
-
expect(products).toBeDefined();
|
|
389
|
-
expect(products.length).toBe(3);
|
|
390
|
-
expect(products).toContain('Laptop');
|
|
391
|
-
expect(products).toContain('Mouse');
|
|
392
|
-
expect(products).toContain('Monitor');
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('should return empty array for non-existent values', async () => {
|
|
396
|
-
const values = await driver.distinct('orders', 'product', { status: 'nonexistent' });
|
|
397
|
-
expect(values).toEqual([]);
|
|
398
|
-
});
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
describe('Window Functions', () => {
|
|
402
|
-
it('should execute ROW_NUMBER window function', async () => {
|
|
403
|
-
const results = await driver.findWithWindowFunctions('orders', {
|
|
404
|
-
windowFunctions: [
|
|
405
|
-
{
|
|
406
|
-
function: 'ROW_NUMBER',
|
|
407
|
-
alias: 'row_num',
|
|
408
|
-
orderBy: [{ field: 'amount', order: 'desc' }],
|
|
409
|
-
},
|
|
410
|
-
],
|
|
411
|
-
orderBy: [{ field: 'amount', order: 'desc' }],
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
expect(results).toBeDefined();
|
|
415
|
-
expect(results.length).toBe(5);
|
|
416
|
-
expect(results[0].row_num).toBe(1);
|
|
417
|
-
expect(results[0].amount).toBe(1200.0);
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
it('should execute RANK window function', async () => {
|
|
421
|
-
const results = await driver.findWithWindowFunctions('orders', {
|
|
422
|
-
windowFunctions: [
|
|
423
|
-
{
|
|
424
|
-
function: 'RANK',
|
|
425
|
-
alias: 'rank',
|
|
426
|
-
orderBy: [{ field: 'amount', order: 'desc' }],
|
|
427
|
-
},
|
|
428
|
-
],
|
|
429
|
-
orderBy: [{ field: 'amount', order: 'desc' }],
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
expect(results).toBeDefined();
|
|
433
|
-
expect(results.length).toBe(5);
|
|
434
|
-
expect(results[0].rank).toBe(1);
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it('should partition window function by field', async () => {
|
|
438
|
-
const results = await driver.findWithWindowFunctions('orders', {
|
|
439
|
-
windowFunctions: [
|
|
440
|
-
{
|
|
441
|
-
function: 'ROW_NUMBER',
|
|
442
|
-
alias: 'customer_row',
|
|
443
|
-
partitionBy: ['customer'],
|
|
444
|
-
orderBy: [{ field: 'amount', order: 'desc' }],
|
|
445
|
-
},
|
|
446
|
-
],
|
|
447
|
-
orderBy: [{ field: 'customer', order: 'asc' }, { field: 'amount', order: 'desc' }],
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
expect(results).toBeDefined();
|
|
451
|
-
|
|
452
|
-
const aliceOrders = results.filter((r: any) => r.customer === 'Alice');
|
|
453
|
-
expect(aliceOrders.length).toBe(2);
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
it('should apply filters with window functions', async () => {
|
|
457
|
-
const results = await driver.findWithWindowFunctions('orders', {
|
|
458
|
-
where: { status: 'completed' },
|
|
459
|
-
windowFunctions: [
|
|
460
|
-
{
|
|
461
|
-
function: 'ROW_NUMBER',
|
|
462
|
-
alias: 'row_num',
|
|
463
|
-
orderBy: [{ field: 'amount', order: 'desc' }],
|
|
464
|
-
},
|
|
465
|
-
],
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
expect(results).toBeDefined();
|
|
469
|
-
expect(results.length).toBe(3);
|
|
470
|
-
expect(results.every((r: any) => r.status === 'completed')).toBe(true);
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
describe('Query Plan Analysis', () => {
|
|
475
|
-
it('should analyze a simple query', async () => {
|
|
476
|
-
const analysis = await driver.analyzeQuery('orders', {
|
|
477
|
-
where: { status: 'completed' },
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
expect(analysis).toBeDefined();
|
|
481
|
-
expect(analysis.sql).toBeDefined();
|
|
482
|
-
expect(analysis.client).toBe('better-sqlite3');
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it('should analyze a complex query with filters', async () => {
|
|
486
|
-
const analysis = await driver.analyzeQuery('orders', {
|
|
487
|
-
where: {
|
|
488
|
-
$and: [{ status: 'completed' }, { amount: { $gt: 100 } }],
|
|
489
|
-
},
|
|
490
|
-
orderBy: [{ field: 'amount', order: 'desc' }],
|
|
491
|
-
limit: 10,
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
expect(analysis).toBeDefined();
|
|
495
|
-
expect(analysis.sql).toBeDefined();
|
|
496
|
-
expect(analysis.bindings).toBeDefined();
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
});
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
4
|
-
import { SqlDriver } from '../src/index.js';
|
|
5
|
-
|
|
6
|
-
describe('SqlDriver Schema Introspection (SQLite)', () => {
|
|
7
|
-
let driver: SqlDriver;
|
|
8
|
-
let knexInstance: any;
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
driver = new SqlDriver({
|
|
12
|
-
client: 'better-sqlite3',
|
|
13
|
-
connection: { filename: ':memory:' },
|
|
14
|
-
useNullAsDefault: true,
|
|
15
|
-
});
|
|
16
|
-
knexInstance = (driver as any).knex;
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterEach(async () => {
|
|
20
|
-
await knexInstance.destroy();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should introspect empty database', async () => {
|
|
24
|
-
const schema = await driver.introspectSchema();
|
|
25
|
-
|
|
26
|
-
expect(schema).toBeDefined();
|
|
27
|
-
expect(schema.tables).toBeDefined();
|
|
28
|
-
expect(Object.keys(schema.tables).length).toBe(0);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should discover existing tables', async () => {
|
|
32
|
-
await knexInstance.schema.createTable('users', (t: any) => {
|
|
33
|
-
t.string('id').primary();
|
|
34
|
-
t.string('name').notNullable();
|
|
35
|
-
t.string('email').unique();
|
|
36
|
-
t.integer('age');
|
|
37
|
-
t.boolean('active').defaultTo(true);
|
|
38
|
-
t.timestamp('created_at').defaultTo(knexInstance.fn.now());
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
await knexInstance.schema.createTable('posts', (t: any) => {
|
|
42
|
-
t.string('id').primary();
|
|
43
|
-
t.string('title').notNullable();
|
|
44
|
-
t.text('content');
|
|
45
|
-
t.timestamp('created_at').defaultTo(knexInstance.fn.now());
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const schema = await driver.introspectSchema();
|
|
49
|
-
|
|
50
|
-
expect(Object.keys(schema.tables)).toContain('users');
|
|
51
|
-
expect(Object.keys(schema.tables)).toContain('posts');
|
|
52
|
-
expect(Object.keys(schema.tables).length).toBe(2);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should discover column types', async () => {
|
|
56
|
-
await knexInstance.schema.createTable('test_types', (t: any) => {
|
|
57
|
-
t.string('id').primary();
|
|
58
|
-
t.string('text_col');
|
|
59
|
-
t.integer('int_col');
|
|
60
|
-
t.float('float_col');
|
|
61
|
-
t.boolean('bool_col');
|
|
62
|
-
t.date('date_col');
|
|
63
|
-
t.timestamp('timestamp_col');
|
|
64
|
-
t.json('json_col');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const schema = await driver.introspectSchema();
|
|
68
|
-
const table = schema.tables['test_types'];
|
|
69
|
-
|
|
70
|
-
expect(table).toBeDefined();
|
|
71
|
-
expect(table.columns.length).toBeGreaterThan(0);
|
|
72
|
-
|
|
73
|
-
const colNames = table.columns.map((c) => c.name);
|
|
74
|
-
expect(colNames).toContain('text_col');
|
|
75
|
-
expect(colNames).toContain('int_col');
|
|
76
|
-
expect(colNames).toContain('bool_col');
|
|
77
|
-
expect(colNames).toContain('json_col');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should detect primary keys', async () => {
|
|
81
|
-
await knexInstance.schema.createTable('pk_test', (t: any) => {
|
|
82
|
-
t.string('id').primary();
|
|
83
|
-
t.string('name');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const schema = await driver.introspectSchema();
|
|
87
|
-
const table = schema.tables['pk_test'];
|
|
88
|
-
|
|
89
|
-
expect(table.primaryKeys).toContain('id');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should detect foreign keys', async () => {
|
|
93
|
-
await knexInstance.schema.createTable('categories', (t: any) => {
|
|
94
|
-
t.string('id').primary();
|
|
95
|
-
t.string('name');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
await knexInstance.schema.createTable('products', (t: any) => {
|
|
99
|
-
t.string('id').primary();
|
|
100
|
-
t.string('name');
|
|
101
|
-
t.string('category_id');
|
|
102
|
-
t.foreign('category_id').references('categories.id');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
const schema = await driver.introspectSchema();
|
|
106
|
-
const productsTable = schema.tables['products'];
|
|
107
|
-
|
|
108
|
-
expect(productsTable).toBeDefined();
|
|
109
|
-
expect(productsTable.foreignKeys.length).toBeGreaterThan(0);
|
|
110
|
-
|
|
111
|
-
const fk = productsTable.foreignKeys.find((fk) => fk.columnName === 'category_id');
|
|
112
|
-
expect(fk).toBeDefined();
|
|
113
|
-
expect(fk?.referencedTable).toBe('categories');
|
|
114
|
-
expect(fk?.referencedColumn).toBe('id');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should detect nullable and required columns', async () => {
|
|
118
|
-
await knexInstance.schema.createTable('nullable_test', (t: any) => {
|
|
119
|
-
t.string('id').primary();
|
|
120
|
-
t.string('required_field').notNullable();
|
|
121
|
-
t.string('optional_field');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const schema = await driver.introspectSchema();
|
|
125
|
-
const table = schema.tables['nullable_test'];
|
|
126
|
-
|
|
127
|
-
const requiredCol = table.columns.find((c) => c.name === 'required_field');
|
|
128
|
-
const optionalCol = table.columns.find((c) => c.name === 'optional_field');
|
|
129
|
-
|
|
130
|
-
expect(requiredCol?.nullable).toBe(false);
|
|
131
|
-
expect(optionalCol?.nullable).toBe(true);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should handle multiple foreign keys in one table', async () => {
|
|
135
|
-
await knexInstance.schema.createTable('users', (t: any) => {
|
|
136
|
-
t.string('id').primary();
|
|
137
|
-
t.string('name');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
await knexInstance.schema.createTable('teams', (t: any) => {
|
|
141
|
-
t.string('id').primary();
|
|
142
|
-
t.string('name');
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
await knexInstance.schema.createTable('memberships', (t: any) => {
|
|
146
|
-
t.string('id').primary();
|
|
147
|
-
t.string('user_id');
|
|
148
|
-
t.string('team_id');
|
|
149
|
-
t.foreign('user_id').references('users.id');
|
|
150
|
-
t.foreign('team_id').references('teams.id');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const schema = await driver.introspectSchema();
|
|
154
|
-
const table = schema.tables['memberships'];
|
|
155
|
-
|
|
156
|
-
expect(table.foreignKeys.length).toBe(2);
|
|
157
|
-
|
|
158
|
-
const userFk = table.foreignKeys.find((fk) => fk.columnName === 'user_id');
|
|
159
|
-
const teamFk = table.foreignKeys.find((fk) => fk.columnName === 'team_id');
|
|
160
|
-
|
|
161
|
-
expect(userFk?.referencedTable).toBe('users');
|
|
162
|
-
expect(teamFk?.referencedTable).toBe('teams');
|
|
163
|
-
});
|
|
164
|
-
});
|