@inflector/optima 1.0.0 → 1.0.1

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/src/table.ts DELETED
@@ -1,926 +0,0 @@
1
- import type { DuckDBConnection, DuckDBValue } from "@duckdb/node-api";
2
- import {
3
- SQLBuilder,
4
- type ColumnBuilder,
5
- type Infer,
6
- type InferAdd,
7
- type Prettify,
8
- } from "./schema";
9
- import { createFluentBuilder } from "./fluent";
10
- import {
11
- createProxyHandler,
12
- createQueryBuilder,
13
- createQueryBuilderOne,
14
- type FluentQueryBuilder,
15
- type FluentQueryBuilderOne,
16
- type MapToFalse,
17
- type QueryMethods,
18
- type QueryOneMethods,
19
- } from "./Qfluent";
20
-
21
- // ---------------------------------------------------------
22
- // 1. OPERATOR DEFINITIONS (Unchanged)
23
- // ---------------------------------------------------------
24
-
25
- export const OPS = [
26
- "eq",
27
- "ne",
28
- "gt",
29
- "gte",
30
- "lt",
31
- "lte",
32
- "like",
33
- "notLike",
34
- "in",
35
- "notIn",
36
- "is",
37
- "isNot",
38
- "between",
39
- "notBetween",
40
- "startsWith",
41
- "endsWith",
42
- "contains",
43
- "regexp",
44
- "notRegexp",
45
- ] as const;
46
-
47
- export type OpKey = (typeof OPS)[number];
48
-
49
- // ---------------------------------------------------------
50
- // 2. CONDITION BUILDER TYPES (Unchanged)
51
- // ---------------------------------------------------------
52
-
53
- type ConditionNode = {
54
- type: "condition" | "and" | "or";
55
- op?: OpKey;
56
- value?: any;
57
- left?: ConditionNode;
58
- right?: ConditionNode;
59
- };
60
-
61
- export interface ConditionBuilder<T> {
62
- eq(value: T): ConditionBuilder<T>;
63
- ne(value: T): ConditionBuilder<T>;
64
- gt(value: T): ConditionBuilder<T>;
65
- gte(value: T): ConditionBuilder<T>;
66
- lt(value: T): ConditionBuilder<T>;
67
- lte(value: T): ConditionBuilder<T>;
68
- in(value: T[]): ConditionBuilder<T>;
69
- notIn(value: T[]): ConditionBuilder<T>;
70
- between(value: [T, T]): ConditionBuilder<T>;
71
- notBetween(value: [T, T]): ConditionBuilder<T>;
72
- is(value: T | null): ConditionBuilder<T>;
73
- isNot(value: T | null): ConditionBuilder<T>;
74
- like(value: string): ConditionBuilder<T>;
75
- notLike(value: string): ConditionBuilder<T>;
76
- startsWith(value: string): ConditionBuilder<T>;
77
- endsWith(value: string): ConditionBuilder<T>;
78
- contains(value: string): ConditionBuilder<T>;
79
- regexp(value: string): ConditionBuilder<T>;
80
- notRegexp(value: string): ConditionBuilder<T>;
81
- and(other: ConditionBuilder<T>): ConditionBuilder<T>;
82
- or(other: ConditionBuilder<T>): ConditionBuilder<T>;
83
- __getNode(): ConditionNode;
84
- }
85
-
86
- class ConditionBuilderImpl<T> implements ConditionBuilder<T> {
87
- private node: ConditionNode;
88
-
89
- constructor(node: ConditionNode) {
90
- this.node = node;
91
- }
92
-
93
- private createOp(op: OpKey, value: any): ConditionBuilder<T> {
94
- return new ConditionBuilderImpl<T>({
95
- type: "condition",
96
- op,
97
- value,
98
- });
99
- }
100
-
101
- eq(value: T): ConditionBuilder<T> {
102
- return this.createOp("eq", value);
103
- }
104
- ne(value: T): ConditionBuilder<T> {
105
- return this.createOp("ne", value);
106
- }
107
- gt(value: T): ConditionBuilder<T> {
108
- return this.createOp("gt", value);
109
- }
110
- gte(value: T): ConditionBuilder<T> {
111
- return this.createOp("gte", value);
112
- }
113
- lt(value: T): ConditionBuilder<T> {
114
- return this.createOp("lt", value);
115
- }
116
- lte(value: T): ConditionBuilder<T> {
117
- return this.createOp("lte", value);
118
- }
119
- in(value: T[]): ConditionBuilder<T> {
120
- return this.createOp("in", value);
121
- }
122
- notIn(value: T[]): ConditionBuilder<T> {
123
- return this.createOp("notIn", value);
124
- }
125
- between(value: [T, T]): ConditionBuilder<T> {
126
- return this.createOp("between", value);
127
- }
128
- notBetween(value: [T, T]): ConditionBuilder<T> {
129
- return this.createOp("notBetween", value);
130
- }
131
- is(value: T | null): ConditionBuilder<T> {
132
- return this.createOp("is", value);
133
- }
134
- isNot(value: T | null): ConditionBuilder<T> {
135
- return this.createOp("isNot", value);
136
- }
137
- like(value: string): ConditionBuilder<T> {
138
- return this.createOp("like", value);
139
- }
140
- notLike(value: string): ConditionBuilder<T> {
141
- return this.createOp("notLike", value);
142
- }
143
- startsWith(value: string): ConditionBuilder<T> {
144
- return this.createOp("startsWith", value);
145
- }
146
- endsWith(value: string): ConditionBuilder<T> {
147
- return this.createOp("endsWith", value);
148
- }
149
- contains(value: string): ConditionBuilder<T> {
150
- return this.createOp("contains", value);
151
- }
152
- regexp(value: string): ConditionBuilder<T> {
153
- return this.createOp("regexp", value);
154
- }
155
- notRegexp(value: string): ConditionBuilder<T> {
156
- return this.createOp("notRegexp", value);
157
- }
158
- and(other: ConditionBuilder<T>): ConditionBuilder<T> {
159
- return new ConditionBuilderImpl<T>({
160
- type: "and",
161
- left: this.node,
162
- right: other.__getNode(),
163
- });
164
- }
165
-
166
- or(other: ConditionBuilder<T>): ConditionBuilder<T> {
167
- return new ConditionBuilderImpl<T>({
168
- type: "or",
169
- left: this.node,
170
- right: other.__getNode(),
171
- });
172
- }
173
-
174
- __getNode(): ConditionNode {
175
- return this.node;
176
- }
177
- }
178
-
179
- export function cond<T>(): ConditionBuilder<T> {
180
- return new ConditionBuilderImpl<T>({ type: "condition" }) as any;
181
- }
182
-
183
- // Exports for standalone operators
184
- export const eq = <T>(value: T) => cond<T>().eq(value);
185
- export const ne = <T>(value: T) => cond<T>().ne(value);
186
- export const gt = <T>(value: T) => cond<T>().gt(value);
187
- export const gte = <T>(value: T) => cond<T>().gte(value);
188
- export const lt = <T>(value: T) => cond<T>().lt(value);
189
- export const lte = <T>(value: T) => cond<T>().lte(value);
190
- export const inOp = <T>(value: T[]) => cond<T>().in(value);
191
- export const notIn = <T>(value: T[]) => cond<T>().notIn(value);
192
- export const between = <T>(value: [T, T]) => cond<T>().between(value);
193
- export const notBetween = <T>(value: [T, T]) => cond<T>().notBetween(value);
194
- export const is = <T>(value: T | null) => cond<T>().is(value);
195
- export const isNot = <T>(value: T | null) => cond<T>().isNot(value);
196
- export const like = <T>(value: string) => cond<T>().like(value);
197
- export const notLike = <T>(value: string) => cond<T>().notLike(value);
198
- export const startsWith = <T>(value: string) => cond<T>().startsWith(value);
199
- export const endsWith = <T>(value: string) => cond<T>().endsWith(value);
200
- export const contains = <T>(value: string) => cond<T>().contains(value);
201
- export const regexp = <T>(value: string) => cond<T>().regexp(value);
202
- export const notRegexp = <T>(value: string) => cond<T>().notRegexp(value);
203
-
204
- // ---------------------------------------------------------
205
- // 3. WHERE TYPE DEFINITIONS (Unchanged)
206
- // ---------------------------------------------------------
207
-
208
- export type FieldQuery<T> =
209
- // If it's ConditionBuilder already, allow it directly
210
- T extends ConditionBuilder<infer U>
211
- ? T
212
- : // Arrays should be passed as is (for IN, etc), not recursed
213
- T extends Array<any>
214
- ? T | ConditionBuilder<T>
215
- : // Date is an object in JS, but we treat it as a primitive for SQL
216
- T extends Date
217
- ? T | ConditionBuilder<T>
218
- : // For objects (Structs), allow recursion OR a condition on the whole struct
219
- T extends object
220
- ? { [K in keyof T]?: FieldQuery<T[K]> } | ConditionBuilder<T>
221
- : // Primitives
222
- T | ConditionBuilder<T>;
223
-
224
- export type Where<Schema> = {
225
- [K in keyof Schema]?: FieldQuery<Schema[K]>;
226
- };
227
-
228
- // ---------------------------------------------------------
229
- // 4. SQL GENERATION UTILITIES (Unchanged)
230
- // ---------------------------------------------------------
231
-
232
- const escape = (val: any): string => {
233
- if (val === null || val === undefined) return "NULL";
234
- if (typeof val === "string") return `'${val.replace(/'/g, "''")}'`;
235
- if (val instanceof Date) return `'${val.toISOString()}'`;
236
- if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
237
- if (typeof val === "object")
238
- return `'${JSON.stringify(val).replace(/'/g, "''")}'`;
239
- return String(val);
240
- };
241
-
242
- const SQL_GENERATORS: Record<OpKey, (col: string, val: any) => string> = {
243
- eq: (c, v) => `${c} = ${escape(v)}`,
244
- ne: (c, v) => `${c} != ${escape(v)}`,
245
- gt: (c, v) => `${c} > ${escape(v)}`,
246
- gte: (c, v) => `${c} >= ${escape(v)}`,
247
- lt: (c, v) => `${c} < ${escape(v)}`,
248
- lte: (c, v) => `${c} <= ${escape(v)}`,
249
- in: (c, v) => `${c} IN (${(v as any[]).map(escape).join(", ")})`,
250
- notIn: (c, v) => `${c} NOT IN (${(v as any[]).map(escape).join(", ")})`,
251
- between: (c, v) => `${c} BETWEEN ${escape(v[0])} AND ${escape(v[1])}`,
252
- notBetween: (c, v) => `${c} NOT BETWEEN ${escape(v[0])} AND ${escape(v[1])}`,
253
- is: (c, v) => (v === null ? `${c} IS NULL` : `${c} IS ${escape(v)}`),
254
- isNot: (c, v) =>
255
- v === null ? `${c} IS NOT NULL` : `${c} IS NOT ${escape(v)}`,
256
- like: (c, v) => `${c} LIKE ${escape(v)}`,
257
- notLike: (c, v) => `${c} NOT LIKE ${escape(v)}`,
258
- startsWith: (c, v) => `${c} LIKE ${escape(v + "%")}`,
259
- endsWith: (c, v) => `${c} LIKE ${escape("%" + v)}`,
260
- contains: (c, v) => `${c} LIKE ${escape("%" + v + "%")}`,
261
- regexp: (c, v) => `regexp_matches(${c}, ${escape(v)})`,
262
- notRegexp: (c, v) => `NOT regexp_matches(${c}, ${escape(v)})`,
263
- };
264
-
265
- export type TableSchema<T> = T extends OptimaTable<infer Schema>
266
- ? Prettify<Infer<Schema>>
267
- : never;
268
-
269
- // --- Extension Type Helpers ---
270
-
271
- type GetRefSchema<Col> = Col extends ColumnBuilder<any, any, infer Ref>
272
- ? Ref
273
- : Col extends { config: { reference?: { ref: string; isMany: boolean } } }
274
- ? never
275
- : never;
276
-
277
- type IsManyRef<RS> = RS extends { readonly __refKind: "many" } ? true : false;
278
-
279
- type HasAnyManyRef<TDef> = true extends {
280
- [K in keyof TDef]: GetRefSchema<TDef[K]> extends never
281
- ? never
282
- : IsManyRef<GetRefSchema<TDef[K]>>;
283
- }[keyof TDef]
284
- ? true
285
- : false;
286
-
287
- export type Extension<SourceDef, TargetTable> = TargetTable extends OptimaTable<
288
- infer TargetDef
289
- >
290
- ? TargetDef extends { __tableName: infer Name }
291
- ? Name extends string
292
- ? HasAnyManyRef<SourceDef> extends true
293
- ? { [K in `$${Name}`]: Infer<TargetDef>[] }
294
- : { [K in `$${Name}`]: Infer<TargetDef> }
295
- : {}
296
- : {}
297
- : {};
298
-
299
- type Unsubscribe = () => void;
300
- export class OptimaTable<TDef extends Record<string, any> = any> {
301
- private Name: string;
302
- private Connection: DuckDBConnection;
303
- private Columns: TDef;
304
- private listeners = new Set<
305
- (change: {
306
- event: "Add" | "AddMany" | "Delete" | "Update";
307
- data: Partial<Infer<TDef>>;
308
- time: Date;
309
- }) => void
310
- >();
311
- private constructor(
312
- name: string,
313
- Columns: TDef,
314
- Connection: DuckDBConnection
315
- ) {
316
- this.Name = name;
317
- const filteredCols: any = { ...Columns };
318
- if ("__tableName" in filteredCols) {
319
- delete filteredCols["__tableName"];
320
- }
321
- this.Columns = filteredCols;
322
- this.Connection = Connection;
323
- }
324
-
325
- protected notifyChange(change: any) {
326
- this.listeners.forEach((listener) => listener(change));
327
- }
328
-
329
- /**
330
- * Subscribe to changes to the table.
331
- * The callback receives the latest change as its argument.
332
- * Returns an unsubscribe function.
333
- */
334
- Subscribe(
335
- fn: (change: {
336
- event: "Add" | "AddMany" | "Delete" | "Update";
337
- data: Partial<Infer<TDef>>;
338
- time: Date;
339
- }) => void
340
- ): Unsubscribe {
341
- this.listeners.add(fn);
342
- return () => {
343
- this.listeners.delete(fn);
344
- };
345
- }
346
-
347
- private async InitTable() {
348
- const SQL = SQLBuilder.BuildTable(this.Name, this.Columns);
349
- await this.Connection.run(SQL);
350
- }
351
-
352
- static async create<T extends Record<string, ColumnBuilder<any, any>>>(
353
- name: string,
354
- Columns: T,
355
- Connection: DuckDBConnection
356
- ): Promise<OptimaTable<T>> {
357
- const table = new OptimaTable(name, Columns, Connection);
358
- await table.InitTable();
359
- return table;
360
- }
361
-
362
- // ---------------------------------------------------------
363
- // UPDATED: Get() now uses the Fluent Factory
364
- // ---------------------------------------------------------
365
- Get(): FluentQueryBuilder<
366
- TDef,
367
- Infer<TDef>,
368
- MapToFalse<QueryMethods<TDef>> & { extended: false }
369
- > {
370
- return createQueryBuilder(this);
371
- }
372
-
373
- // ---------------------------------------------------------
374
- // UPDATED: GetOne() now uses the Fluent Factory
375
- // ---------------------------------------------------------
376
- GetOne(): FluentQueryBuilderOne<
377
- TDef,
378
- Infer<TDef>,
379
- MapToFalse<QueryOneMethods<TDef>> & { extended: false }
380
- > {
381
- return createQueryBuilderOne(this);
382
- }
383
-
384
- Exist() {
385
- type TSchema = Infer<TDef>;
386
- return createFluentBuilder<
387
- {
388
- where: Where<TSchema>;
389
- },
390
- boolean
391
- >(async (data) => {
392
- const whereClause = this.BuildWhereClause(data.where);
393
- const sql = `SELECT 1 FROM ${this.Name}${whereClause} LIMIT 1`;
394
-
395
- const Result: Record<string, DuckDBValue>[] = await (
396
- await this.Connection.run(sql)
397
- ).getRowObjects();
398
-
399
- return Result.length > 0;
400
- });
401
- }
402
-
403
- /**
404
- * Add a single record.
405
- * Returns: TSchema[] (containing the inserted record if returning is true)
406
- */
407
- Add(record: InferAdd<TDef>) {
408
- type TSchema = Infer<TDef>;
409
- return createFluentBuilder<
410
- {
411
- returning: boolean;
412
- },
413
- TSchema[]
414
- >(async (data) => {
415
- this.Validate(record);
416
- record = this.Transform(record);
417
- const { sql } = this.BuildInsert(this.Name, [record], data.returning);
418
- const Result: Record<string, DuckDBValue>[] = await (
419
- await this.Connection.run(sql)
420
- ).getRowObjects();
421
- const Res = this.FormatOut(Result) as TSchema[];
422
- if (Res.length != 0) {
423
- if (data.returning) {
424
- this.notifyChange({ event: "Add", data: Res, time: new Date() });
425
- } else {
426
- this.notifyChange({
427
- event: "Add",
428
- data: await this.GetOne().where(record as any),
429
- time: new Date(),
430
- });
431
- }
432
- }
433
- return Res;
434
- });
435
- }
436
-
437
- /**
438
- * Add multiple records.
439
- * Returns: TSchema[]
440
- */
441
- AddMany(records: InferAdd<TDef>[]) {
442
- type TSchema = Infer<TDef>;
443
- return createFluentBuilder<
444
- {
445
- returning: boolean;
446
- },
447
- TSchema[]
448
- >(async (data) => {
449
- records.forEach((v) => this.Validate(v));
450
- const transformedRecords = records.map((v) => this.Transform(v));
451
- const { sql } = this.BuildInsert(
452
- this.Name,
453
- transformedRecords,
454
- data.returning
455
- );
456
- const Result: Record<string, DuckDBValue>[] = await (
457
- await this.Connection.run(sql)
458
- ).getRowObjects();
459
- const Res = this.FormatOut(Result) as TSchema[];
460
- if (Res.length != 0) {
461
- if (data.returning) {
462
- this.notifyChange({ event: "AddMany", data: Res, time: new Date() });
463
- } else {
464
- const Added = transformedRecords;
465
- // Use Promise.all to retrieve the added records
466
- const addedPromises = Added.map(
467
- async (record) => await this.GetOne().where(record as any)
468
- );
469
- const addedRecords = await Promise.all(addedPromises);
470
- this.notifyChange({
471
- event: "Add",
472
- data: addedRecords,
473
- time: new Date(),
474
- });
475
- }
476
- }
477
- return Res;
478
- });
479
- }
480
-
481
- /**
482
- * Update records.
483
- * Returns: TSchema[]
484
- */
485
- Update(record: Partial<Infer<TDef>>) {
486
- type TSchema = Infer<TDef>;
487
- return createFluentBuilder<
488
- {
489
- where: Where<TSchema>;
490
- returning: boolean;
491
- },
492
- TSchema[]
493
- >(async (data) => {
494
- this.Validate(record);
495
- record = this.Transform(record);
496
- const PotentialyUpdated = await this.Get().where(record as any);
497
- if (PotentialyUpdated.length == 0) return [];
498
- else {
499
- const { sql } = this.BuildUpdate(this.Name, record, data);
500
- const Result: Record<string, DuckDBValue>[] = await (
501
- await this.Connection.run(sql)
502
- ).getRowObjects();
503
- if (Result.length != 0) {
504
- if (data.returning) {
505
- this.notifyChange({ event: "AddMany", data: Result, time: new Date() });
506
- } else {
507
- const Updated = PotentialyUpdated as any[];
508
- const UpdatedPromises = Updated.map(
509
- async (record: any) => await this.GetOne().where(record as any)
510
- );
511
- const UpdatedRecords = await Promise.all(UpdatedPromises);
512
- this.notifyChange({
513
- event: "Add",
514
- data: UpdatedRecords,
515
- time: new Date(),
516
- });
517
- }
518
- }
519
- return (data.returning ? this.FormatOut(Result) : Result) as TSchema[];
520
- }
521
-
522
- });
523
- }
524
-
525
- /**
526
- * Delete records.
527
- * Returns: TSchema | undefined (The code logic returns index [0], implies single delete or returning first)
528
- */
529
- Delete() {
530
- type TSchema = Infer<TDef>;
531
- return createFluentBuilder<
532
- {
533
- where: Where<TSchema>;
534
- returning: boolean;
535
- },
536
- TSchema | undefined
537
- >(async (data) => {
538
- // Find what would be potentially deleted using the where clause
539
- const PotentiallyDeleted = await this.Get().where(data.where as any);
540
- if (PotentiallyDeleted.length == 0) return undefined;
541
- const { sql } = this.BuildDelete(this.Name, { ...data });
542
- const Result: Record<string, DuckDBValue>[] = await (
543
- await this.Connection.run(sql)
544
- ).getRowObjects();
545
-
546
- if (Result.length != 0) {
547
- if (data.returning) {
548
- this.notifyChange({ event: "DeleteMany", data: Result, time: new Date() });
549
- } else {
550
- const DeletedRecords = PotentiallyDeleted as any[];
551
- this.notifyChange({
552
- event: "Delete",
553
- data: DeletedRecords,
554
- time: new Date(),
555
- });
556
- }
557
- }
558
- const formatted = this.FormatOut(Result) as TSchema[];
559
- return formatted[0];
560
- });
561
- }
562
-
563
- Count() {
564
- type TSchema = Infer<TDef>;
565
- return createFluentBuilder<{ where: Where<TSchema> }, number>(
566
- async (data) => {
567
- const whereClause = this.BuildWhereClause(data.where);
568
- const sql = `SELECT COUNT(*) as count FROM ${this.Name}${whereClause}`;
569
- const result = await (await this.Connection.run(sql)).getRowObjects();
570
- // @ts-ignore
571
- return Number(result[0].count);
572
- }
573
- );
574
- }
575
-
576
- /**
577
- * Auto-resolves relationships based on schema 'reference'
578
- */
579
- private ResolveJoin(
580
- targetTable: OptimaTable<any>
581
- ): { sql: string; isMany: boolean } | null {
582
- const myName = this.Name;
583
- const targetName = targetTable.Name;
584
-
585
- const getRef = (builder: any) => {
586
- // @ts-ignore
587
- const refConf = builder.config.reference;
588
- if (typeof refConf === "string") return { ref: refConf, isMany: false };
589
- if (refConf && typeof refConf === "object") return refConf;
590
- return null;
591
- };
592
-
593
- // // 1. They reference Me
594
- // for (const [colName, builder] of Object.entries(targetTable.Columns)) {
595
- // const config = getRef(builder);
596
- // if (config && config.ref) {
597
- // const [refTable, refCol] = config.ref.split(".");
598
- // if (refTable === myName) {
599
- // // if (config.isMany) {
600
- // // return `list_contains(${targetName}.${colName}, ${myName}.${refCol})`;
601
- // // }
602
- // return `${targetName}.${colName} = ${myName}.${refCol}`;
603
- // }
604
- // }
605
- // }
606
-
607
- // 2. I reference Them
608
- for (const [colName, builder] of Object.entries(this.Columns)) {
609
- const config = getRef(builder);
610
- if (config && config.ref) {
611
- const [refTable, refCol] = config.ref.split(".");
612
- if (refTable === targetName) {
613
- // if (config.isMany) {
614
- // return `list_contains(${myName}.${colName}, ${targetName}.${refCol})`;
615
- // }
616
- return {
617
- sql: `${myName}.${colName} = ${targetName}.${refCol}`,
618
- isMany: config.isMany,
619
- };
620
- }
621
- }
622
- }
623
-
624
- console.warn(
625
- `Could not resolve relationship between ${myName} and ${targetName}`
626
- );
627
- return null;
628
- }
629
- // -- Validator --
630
- private Validate(data: any) {
631
- const Cols = Object.fromEntries(
632
- //@ts-ignore
633
- Object.entries(this.Columns).map(([col, val]) => [col, val.config])
634
- );
635
- for (const [field, config] of Object.entries(Cols)) {
636
- // For partial updates, skip validation if key is missing
637
- if (data[field] === undefined) continue;
638
-
639
- if ("validate" in config && typeof config.validate === "function") {
640
- const fn = config.validate;
641
- if (!fn(data[field])) {
642
- throw new Error(
643
- `Validation failed for field '${field}' : The value '${JSON.stringify(
644
- data[field]
645
- )}' doesn't pass the validate function`
646
- );
647
- }
648
- }
649
- }
650
- }
651
- // -- Transformer --
652
- private Transform(data: any) {
653
- const Cols = Object.fromEntries(
654
- //@ts-ignore
655
- Object.entries(this.Columns).map(([col, val]) => [col, val.config])
656
- );
657
- const transformed: any = { ...data };
658
- for (const [field, config] of Object.entries(Cols)) {
659
- // For partial updates, skip transform if key is missing
660
- if (transformed[field] === undefined) continue;
661
-
662
- if ("transform" in config && typeof config.transform === "function") {
663
- const fn = config.transform;
664
- transformed[field] = fn(transformed[field]);
665
- }
666
- }
667
- return transformed;
668
- }
669
- // --- SHARED: Where Clause Builder ---
670
- private BuildWhereClause = (
671
- where: Where<Infer<TDef>> | undefined
672
- ): string => {
673
- if (!where) return "";
674
-
675
- const conditions: string[] = [];
676
-
677
- // Helper to generate SQL for specific operators
678
- const nodeToSQL = (node: ConditionNode, colName: string): string => {
679
- if (node.type === "condition" && node.op) {
680
- return SQL_GENERATORS[node.op](colName, node.value);
681
- } else if (node.type === "and" && node.left && node.right) {
682
- return `(${nodeToSQL(node.left, colName)} AND ${nodeToSQL(
683
- node.right,
684
- colName
685
- )})`;
686
- } else if (node.type === "or" && node.left && node.right) {
687
- return `(${nodeToSQL(node.left, colName)} OR ${nodeToSQL(
688
- node.right,
689
- colName
690
- )})`;
691
- }
692
- return "";
693
- };
694
-
695
- // Recursive function to handle nested objects (Structs)
696
- const processLevel = (currentWhere: any, pathPrefix: string) => {
697
- for (const key of Object.keys(currentWhere)) {
698
- const val = currentWhere[key];
699
-
700
- // Build the dot-notation column name (e.g. "address.city")
701
- const colName = pathPrefix ? `${pathPrefix}.${key}` : key;
702
-
703
- // 1. Skip undefined values
704
- if (val === undefined) continue;
705
-
706
- // 2. Check for ConditionBuilder (e.g., eq(5), is(null))
707
- if (
708
- val &&
709
- typeof val === "object" &&
710
- "__getNode" in val &&
711
- typeof val.__getNode === "function"
712
- ) {
713
- const node = (val as ConditionBuilder<any>).__getNode();
714
- const sql = nodeToSQL(node, colName);
715
- if (sql) conditions.push(sql);
716
- }
717
- // 3. Check for Nested Object (Struct recursion)
718
- // Must exclude Arrays and Dates which are treated as values
719
- else if (
720
- val &&
721
- typeof val === "object" &&
722
- !Array.isArray(val) &&
723
- !(val instanceof Date)
724
- ) {
725
- processLevel(val, colName);
726
- }
727
- // 4. Direct Value (Implicit Equality)
728
- else {
729
- conditions.push(SQL_GENERATORS.eq(colName, val));
730
- }
731
- }
732
- };
733
-
734
- processLevel(where, "");
735
-
736
- if (conditions.length > 0) {
737
- return ` WHERE ${conditions.join(" AND ")}`;
738
- }
739
- return "";
740
- };
741
-
742
- private BuildSelect = (
743
- TableName: string,
744
- options: {
745
- limit?: number;
746
- orderBy?: [keyof TDef | Array<keyof TDef>, "ASC" | "DESC"];
747
- offset?: number;
748
- groupBy?: Array<keyof TDef>;
749
- where: Where<Infer<TDef>>;
750
- extend?: OptimaTable<any> | OptimaTable<any>[];
751
- }
752
- ) => {
753
- const { limit, orderBy, offset, groupBy, where, extend } = options;
754
-
755
- // 1. Base Columns
756
- let selectColumns = `${TableName}.*`;
757
-
758
- // 2. Extended Columns (DuckDB Correlated Subquery)
759
- if (extend) {
760
- const extensions = Array.isArray(extend) ? extend : [extend];
761
-
762
- for (const extTable of extensions) {
763
- const joinLogic = this.ResolveJoin(extTable);
764
- if (joinLogic) {
765
- selectColumns += `, (SELECT ${joinLogic.isMany ? "list(" : ""}${extTable.Name
766
- }${joinLogic.isMany ? ")" : ""} FROM ${extTable.Name} WHERE ${joinLogic.sql
767
- }) AS "$${extTable.Name}"`;
768
- }
769
- }
770
- }
771
-
772
- let query = `SELECT ${selectColumns} FROM ${TableName}`;
773
-
774
- query += this.BuildWhereClause(where);
775
-
776
- if (groupBy && groupBy.length > 0)
777
- query += ` GROUP BY ${groupBy.join(", ")}`;
778
-
779
- if (orderBy) {
780
- const [columns, direction] = orderBy;
781
- const columnStr = Array.isArray(columns) ? columns.join(", ") : columns;
782
- query += ` ORDER BY ${String(columnStr)} ${direction}`;
783
- }
784
-
785
- if (limit !== undefined && limit !== null) query += ` LIMIT ${limit}`;
786
- if (offset !== undefined && offset !== null) query += ` OFFSET ${offset}`;
787
-
788
- return { sql: query, args: [] };
789
- };
790
-
791
- private BuildInsert = (
792
- TableName: string,
793
- Records: InferAdd<TDef>[],
794
- isReturning: boolean
795
- ) => {
796
- const columnKeys = Object.keys(this.Columns);
797
- const columnsHeader = `(${columnKeys.join(", ")})`;
798
- const valuesBlock = Records.map((r) => {
799
- const orderedValues = columnKeys.map((key) => {
800
- let v = (r as any)[key];
801
- if (v == null && this.Columns[key].config.default) {
802
- v = this.Columns[key].config.default();
803
- }
804
- return escape(v);
805
- });
806
- return `(${orderedValues.join(",")})`;
807
- }).join(",\n");
808
- const sql = `INSERT INTO ${TableName} ${columnsHeader} VALUES \n${valuesBlock}${isReturning ? "\nRETURNING *" : ""
809
- };`;
810
-
811
- return { sql: sql };
812
- };
813
-
814
- private BuildUpdate = (
815
- TableName: string,
816
- changes: Partial<Infer<TDef>>,
817
- options: {
818
- where: Where<Infer<TDef>>;
819
- returning: boolean;
820
- }
821
- ) => {
822
- const { where, returning } = options;
823
-
824
- const setClauses: string[] = [];
825
- for (const [key, value] of Object.entries(changes)) {
826
- if (value !== undefined) {
827
- setClauses.push(`${key} = ${escape(value)}`);
828
- }
829
- }
830
-
831
- if (setClauses.length === 0) {
832
- throw new Error("No fields provided to update.");
833
- }
834
-
835
- let query = `UPDATE ${TableName} SET ${setClauses.join(", ")}`;
836
-
837
- query += this.BuildWhereClause(where);
838
-
839
- if (returning) {
840
- query += " RETURNING *";
841
- }
842
-
843
- return { sql: query, args: [] };
844
- };
845
-
846
- private BuildDelete = (
847
- TableName: string,
848
- options: {
849
- where: Where<Infer<TDef>>;
850
- returning: boolean;
851
- }
852
- ) => {
853
- const { where, returning } = options;
854
- let query = `DELETE FROM ${TableName}`;
855
-
856
- query += this.BuildWhereClause(where);
857
-
858
- if (returning) {
859
- query += " RETURNING *";
860
- }
861
-
862
- return { sql: query, args: [] };
863
- };
864
-
865
- // Helper to handle the deep recursion
866
- private parseRecursive = (value: any): any => {
867
- // 1. Handle Null/Undefined
868
- if (value === null || value === undefined) {
869
- return value;
870
- }
871
-
872
- // 2. Handle Dates (return as is, don't try to recurse properties)
873
- if (value instanceof Date) {
874
- return value;
875
- }
876
-
877
- // 3. Handle Arrays (recurse over every item)
878
- if (Array.isArray(value)) {
879
- return value.map((item) => this.parseRecursive(item));
880
- }
881
-
882
- // 4. Handle Objects (DuckDB structs, maps, or generic objects)
883
- if (typeof value === "object") {
884
- // DuckDB specific: If it has an 'entries' property, unwrap it and recurse
885
- // @ts-ignore
886
- if (value.entries !== undefined) {
887
- // @ts-ignore
888
- return this.parseRecursive(value.entries);
889
- }
890
-
891
- // DuckDB specific: If it has an 'items' property (lists), unwrap it and recurse
892
- // @ts-ignore
893
- if (value.items !== undefined) {
894
- // @ts-ignore
895
- return this.parseRecursive(value.items);
896
- }
897
-
898
- // Standard Object: recurse over all keys
899
- const parsedObj: Record<string, any> = {};
900
- for (const [k, v] of Object.entries(value)) {
901
- parsedObj[k] = this.parseRecursive(v);
902
- }
903
- return parsedObj;
904
- }
905
-
906
- // 5. Primitives (string, number, boolean)
907
- return value;
908
- };
909
-
910
- private FormatOut = (data: Record<string, DuckDBValue>[]) => {
911
- return data.map((row) => {
912
- const formatted: Record<string, any> = {};
913
-
914
- for (const [key, value] of Object.entries(row)) {
915
- if (key.startsWith("$") && value === null) {
916
- formatted[key] = [];
917
- continue;
918
- }
919
-
920
- // 2. Recursively parse everything else
921
- formatted[key] = this.parseRecursive(value);
922
- }
923
- return formatted;
924
- });
925
- };
926
- }