metal-orm 1.0.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.
Files changed (79) hide show
  1. package/README.md +30 -0
  2. package/ROADMAP.md +125 -0
  3. package/metadata.json +5 -0
  4. package/package.json +45 -0
  5. package/playground/api/playground-api.ts +94 -0
  6. package/playground/index.html +15 -0
  7. package/playground/src/App.css +1 -0
  8. package/playground/src/App.tsx +114 -0
  9. package/playground/src/components/CodeDisplay.tsx +43 -0
  10. package/playground/src/components/QueryExecutor.tsx +189 -0
  11. package/playground/src/components/ResultsTable.tsx +67 -0
  12. package/playground/src/components/ResultsTabs.tsx +105 -0
  13. package/playground/src/components/ScenarioList.tsx +56 -0
  14. package/playground/src/components/logo.svg +45 -0
  15. package/playground/src/data/scenarios.ts +2 -0
  16. package/playground/src/main.tsx +9 -0
  17. package/playground/src/services/PlaygroundApiService.ts +60 -0
  18. package/postcss.config.cjs +5 -0
  19. package/sql_sql-ansi-cheatsheet-2025.md +264 -0
  20. package/src/ast/expression.ts +362 -0
  21. package/src/ast/join.ts +11 -0
  22. package/src/ast/query.ts +63 -0
  23. package/src/builder/hydration-manager.ts +55 -0
  24. package/src/builder/hydration-planner.ts +77 -0
  25. package/src/builder/operations/column-selector.ts +42 -0
  26. package/src/builder/operations/cte-manager.ts +18 -0
  27. package/src/builder/operations/filter-manager.ts +36 -0
  28. package/src/builder/operations/join-manager.ts +26 -0
  29. package/src/builder/operations/pagination-manager.ts +17 -0
  30. package/src/builder/operations/relation-manager.ts +49 -0
  31. package/src/builder/query-ast-service.ts +155 -0
  32. package/src/builder/relation-conditions.ts +39 -0
  33. package/src/builder/relation-projection-helper.ts +59 -0
  34. package/src/builder/relation-service.ts +166 -0
  35. package/src/builder/select-query-builder-deps.ts +33 -0
  36. package/src/builder/select-query-state.ts +107 -0
  37. package/src/builder/select.ts +237 -0
  38. package/src/codegen/typescript.ts +295 -0
  39. package/src/constants/sql.ts +57 -0
  40. package/src/dialect/abstract.ts +221 -0
  41. package/src/dialect/mssql/index.ts +89 -0
  42. package/src/dialect/mysql/index.ts +81 -0
  43. package/src/dialect/sqlite/index.ts +85 -0
  44. package/src/index.ts +12 -0
  45. package/src/playground/features/playground/api/types.ts +16 -0
  46. package/src/playground/features/playground/clients/MockClient.ts +17 -0
  47. package/src/playground/features/playground/clients/SqliteClient.ts +57 -0
  48. package/src/playground/features/playground/common/IDatabaseClient.ts +10 -0
  49. package/src/playground/features/playground/data/scenarios/aggregation.ts +36 -0
  50. package/src/playground/features/playground/data/scenarios/basics.ts +25 -0
  51. package/src/playground/features/playground/data/scenarios/edge_cases.ts +57 -0
  52. package/src/playground/features/playground/data/scenarios/filtering.ts +94 -0
  53. package/src/playground/features/playground/data/scenarios/hydration.ts +15 -0
  54. package/src/playground/features/playground/data/scenarios/index.ts +29 -0
  55. package/src/playground/features/playground/data/scenarios/ordering.ts +25 -0
  56. package/src/playground/features/playground/data/scenarios/pagination.ts +16 -0
  57. package/src/playground/features/playground/data/scenarios/relationships.ts +75 -0
  58. package/src/playground/features/playground/data/scenarios/types.ts +67 -0
  59. package/src/playground/features/playground/data/schema.ts +87 -0
  60. package/src/playground/features/playground/data/seed.ts +104 -0
  61. package/src/playground/features/playground/services/QueryExecutionService.ts +120 -0
  62. package/src/runtime/als.ts +19 -0
  63. package/src/runtime/hydration.ts +43 -0
  64. package/src/schema/column.ts +19 -0
  65. package/src/schema/relation.ts +38 -0
  66. package/src/schema/table.ts +22 -0
  67. package/tests/between.test.ts +43 -0
  68. package/tests/case-expression.test.ts +58 -0
  69. package/tests/complex-exists.test.ts +230 -0
  70. package/tests/cte.test.ts +118 -0
  71. package/tests/exists.test.ts +127 -0
  72. package/tests/like.test.ts +33 -0
  73. package/tests/right-join.test.ts +89 -0
  74. package/tests/subquery-having.test.ts +193 -0
  75. package/tests/window-function.test.ts +137 -0
  76. package/tsconfig.json +30 -0
  77. package/tsup.config.ts +10 -0
  78. package/vite.config.ts +22 -0
  79. package/vitest.config.ts +14 -0
@@ -0,0 +1,120 @@
1
+ import { performance } from 'node:perf_hooks';
2
+ import { SelectQueryBuilder } from '../../../../builder/select';
3
+ import { Users } from '../data/schema';
4
+ import { SqliteDialect } from '../../../../dialect/sqlite';
5
+ import { hydrateRows } from '../../../../runtime/hydration';
6
+ import type { IDatabaseClient } from '../common/IDatabaseClient';
7
+ import type { QueryExecutionResult } from '../api/types';
8
+ import type { Scenario } from '../data/scenarios';
9
+
10
+ /**
11
+ * Extracts the TypeScript code from a build function
12
+ */
13
+ function extractTypeScriptCode(buildFn: (builder: SelectQueryBuilder<any>) => SelectQueryBuilder<any>): string {
14
+ const fnString = buildFn.toString();
15
+
16
+ // Remove the function wrapper and return statement
17
+ const bodyMatch = fnString.match(/=>\s*\{?\s*(.*?)\s*\}?$/s);
18
+ if (bodyMatch) {
19
+ let code = bodyMatch[1].trim();
20
+
21
+ // Remove trailing semicolon if present
22
+ code = code.replace(/;$/, '');
23
+
24
+ // Clean up indentation (remove common leading spaces)
25
+ const lines = code.split('\n');
26
+ if (lines.length > 1) {
27
+ // Find the minimum indentation of non-empty lines
28
+ const nonEmptyLines = lines.filter(line => line.trim().length > 0);
29
+ const minIndent = Math.min(...nonEmptyLines.map(line => {
30
+ const match = line.match(/^(\s*)/);
31
+ return match ? match[1].length : 0;
32
+ }));
33
+
34
+ // Remove the common indentation
35
+ code = lines.map(line => line.slice(minIndent)).join('\n').trim();
36
+ }
37
+
38
+ return code;
39
+ }
40
+
41
+ return fnString;
42
+ }
43
+
44
+ export class QueryExecutionService {
45
+ constructor(private dbClient: IDatabaseClient) { }
46
+
47
+ async executeScenario(scenario: Scenario): Promise<QueryExecutionResult> {
48
+ const startTime = performance.now();
49
+
50
+ try {
51
+ if (!this.dbClient.isReady) {
52
+ return {
53
+ sql: '',
54
+ params: [],
55
+ typescriptCode: scenario.typescriptCode || extractTypeScriptCode(scenario.build),
56
+ results: [],
57
+ error: 'Database not ready',
58
+ executionTime: 0
59
+ };
60
+ }
61
+
62
+ const queryBuilder = new SelectQueryBuilder(Users);
63
+ const builtQuery = scenario.build(queryBuilder);
64
+
65
+ const dialect = new SqliteDialect();
66
+ const compiled = builtQuery.compile(dialect);
67
+
68
+ const results = await this.dbClient.executeSql(compiled.sql, compiled.params);
69
+ const executionTime = performance.now() - startTime;
70
+
71
+ // Check if hydration is needed
72
+ const hydrationPlan = builtQuery.getHydrationPlan();
73
+ let hydratedResults: Record<string, any>[] | undefined;
74
+
75
+ if (hydrationPlan && results.length > 0) {
76
+ // Convert QueryResult[] to Record<string, any>[] for hydration
77
+ const rows: Record<string, any>[] = [];
78
+ const { columns, values } = results[0];
79
+
80
+ for (const row of values) {
81
+ const obj: Record<string, any> = {};
82
+ columns.forEach((col, idx) => {
83
+ obj[col] = row[idx];
84
+ });
85
+ rows.push(obj);
86
+ }
87
+
88
+ hydratedResults = hydrateRows(rows, hydrationPlan);
89
+ }
90
+
91
+ return {
92
+ sql: compiled.sql,
93
+ params: compiled.params,
94
+ typescriptCode: scenario.typescriptCode || extractTypeScriptCode(scenario.build),
95
+ results,
96
+ hydratedResults,
97
+ error: this.dbClient.error,
98
+ executionTime
99
+ };
100
+ } catch (error) {
101
+ const executionTime = performance.now() - startTime;
102
+ return {
103
+ sql: '',
104
+ params: [],
105
+ typescriptCode: scenario.typescriptCode || extractTypeScriptCode(scenario.build),
106
+ results: [],
107
+ error: error instanceof Error ? error.message : 'Unknown error',
108
+ executionTime
109
+ };
110
+ }
111
+ }
112
+
113
+ isDatabaseReady(): boolean {
114
+ return this.dbClient.isReady;
115
+ }
116
+
117
+ getLastError(): string | null {
118
+ return this.dbClient.error;
119
+ }
120
+ }
@@ -0,0 +1,19 @@
1
+ // In a real Node environment: import { AsyncLocalStorage } from 'node:async_hooks';
2
+
3
+ // Browser Shim
4
+ export class AsyncLocalStorage<T> {
5
+ private store: T | undefined;
6
+
7
+ run<R>(store: T, callback: () => R): R {
8
+ this.store = store;
9
+ try {
10
+ return callback();
11
+ } finally {
12
+ this.store = undefined;
13
+ }
14
+ }
15
+
16
+ getStore(): T | undefined {
17
+ return this.store;
18
+ }
19
+ }
@@ -0,0 +1,43 @@
1
+ import { HydrationPlan } from '../ast/query';
2
+
3
+ const isRelationKey = (key: string) => key.includes('__');
4
+
5
+ export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan): Record<string, any>[] => {
6
+ if (!plan || !rows.length) return rows;
7
+
8
+ const rootMap = new Map<any, any>();
9
+
10
+ rows.forEach(row => {
11
+ const rootId = row[plan.rootPrimaryKey];
12
+ if (rootId === undefined) return;
13
+
14
+ if (!rootMap.has(rootId)) {
15
+ const base: Record<string, any> = {};
16
+ const baseKeys = plan.rootColumns.length ? plan.rootColumns : Object.keys(row).filter(k => !isRelationKey(k));
17
+ baseKeys.forEach(key => { base[key] = row[key]; });
18
+ plan.relations.forEach(rel => { base[rel.name] = []; });
19
+ rootMap.set(rootId, base);
20
+ }
21
+
22
+ const parent = rootMap.get(rootId);
23
+
24
+ plan.relations.forEach(rel => {
25
+ const childPkKey = `${rel.aliasPrefix}__${rel.targetPrimaryKey}`;
26
+ const childPk = row[childPkKey];
27
+ if (childPk === null || childPk === undefined) return;
28
+
29
+ const bucket = parent[rel.name] as any[];
30
+ if (bucket.some(item => item[rel.targetPrimaryKey] === childPk)) return;
31
+
32
+ const child: Record<string, any> = {};
33
+ rel.columns.forEach(col => {
34
+ const key = `${rel.aliasPrefix}__${col}`;
35
+ child[col] = row[key];
36
+ });
37
+
38
+ bucket.push(child);
39
+ });
40
+ });
41
+
42
+ return Array.from(rootMap.values());
43
+ };
@@ -0,0 +1,19 @@
1
+ export type ColumnType = 'INT' | 'VARCHAR' | 'JSON' | 'ENUM' | 'BOOLEAN';
2
+
3
+ export interface ColumnDef {
4
+ name: string; // filled at runtime by defineTable
5
+ type: ColumnType;
6
+ primary?: boolean;
7
+ notNull?: boolean;
8
+ args?: any[]; // for varchar length etc
9
+ table?: string; // Filled at runtime by defineTable
10
+ }
11
+
12
+ // Column Factory
13
+ export const col = {
14
+ int: (): ColumnDef => ({ name: '', type: 'INT' }),
15
+ varchar: (length: number): ColumnDef => ({ name: '', type: 'VARCHAR', args: [length] }),
16
+ json: (): ColumnDef => ({ name: '', type: 'JSON' }),
17
+ boolean: (): ColumnDef => ({ name: '', type: 'BOOLEAN' }),
18
+ primaryKey: (def: ColumnDef): ColumnDef => ({ ...def, primary: true })
19
+ };
@@ -0,0 +1,38 @@
1
+ import { TableDef } from './table';
2
+
3
+ export const RelationKinds = {
4
+ HasMany: 'HAS_MANY',
5
+ BelongsTo: 'BELONGS_TO',
6
+ } as const;
7
+
8
+ export type RelationType = (typeof RelationKinds)[keyof typeof RelationKinds];
9
+
10
+ interface BaseRelation {
11
+ target: TableDef;
12
+ foreignKey: string; // The column on the child table
13
+ localKey?: string; // Usually 'id'
14
+ }
15
+
16
+ export interface HasManyRelation extends BaseRelation {
17
+ type: typeof RelationKinds.HasMany;
18
+ }
19
+
20
+ export interface BelongsToRelation extends BaseRelation {
21
+ type: typeof RelationKinds.BelongsTo;
22
+ }
23
+
24
+ export type RelationDef = HasManyRelation | BelongsToRelation;
25
+
26
+ export const hasMany = (target: TableDef, foreignKey: string, localKey: string = 'id'): HasManyRelation => ({
27
+ type: RelationKinds.HasMany,
28
+ target,
29
+ foreignKey,
30
+ localKey,
31
+ });
32
+
33
+ export const belongsTo = (target: TableDef, foreignKey: string, localKey: string = 'id'): BelongsToRelation => ({
34
+ type: RelationKinds.BelongsTo,
35
+ target,
36
+ foreignKey,
37
+ localKey,
38
+ });
@@ -0,0 +1,22 @@
1
+ import { ColumnDef } from './column';
2
+ import { RelationDef } from './relation';
3
+
4
+ export interface TableDef<T extends Record<string, ColumnDef> = Record<string, ColumnDef>> {
5
+ name: string;
6
+ columns: T;
7
+ relations: Record<string, RelationDef>;
8
+ }
9
+
10
+ export const defineTable = <T extends Record<string, ColumnDef>>(
11
+ name: string,
12
+ columns: T,
13
+ relations: Record<string, RelationDef> = {}
14
+ ): TableDef<T> => {
15
+ // Runtime mutability to assign names to column definitions for convenience
16
+ const colsWithNames = Object.entries(columns).reduce((acc, [key, def]) => {
17
+ (acc as any)[key] = { ...def, name: key, table: name };
18
+ return acc;
19
+ }, {} as T);
20
+
21
+ return { name, columns: colsWithNames, relations };
22
+ };
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { between, notBetween, eq } from '../src/ast/expression';
3
+ import { Users, Orders } from '../src/playground/features/playground/data/schema';
4
+ import { SqliteDialect } from '../src/dialect/sqlite';
5
+ import { SelectQueryBuilder } from '../src/builder/select';
6
+
7
+ describe('between', () => {
8
+ const dialect = new SqliteDialect();
9
+
10
+ it('should handle a basic BETWEEN condition', () => {
11
+ const q = new SelectQueryBuilder(Users)
12
+ .selectRaw('*')
13
+ .where(between(Users.columns.id, 1, 10));
14
+ const compiled = q.compile(dialect);
15
+ expect(compiled.sql).toBe(
16
+ 'SELECT "users"."*" FROM "users" WHERE "users"."id" BETWEEN ? AND ?;'
17
+ );
18
+ expect(compiled.params).toEqual([1, 10]);
19
+ });
20
+
21
+ it('should handle a NOT BETWEEN condition', () => {
22
+ const q = new SelectQueryBuilder(Users)
23
+ .selectRaw('*')
24
+ .where(notBetween(Users.columns.id, 1, 10));
25
+ const compiled = q.compile(dialect);
26
+ expect(compiled.sql).toBe(
27
+ 'SELECT "users"."*" FROM "users" WHERE "users"."id" NOT BETWEEN ? AND ?;'
28
+ );
29
+ expect(compiled.params).toEqual([1, 10]);
30
+ });
31
+
32
+ it('should handle multiple conditions', () => {
33
+ const q = new SelectQueryBuilder(Orders)
34
+ .selectRaw('*')
35
+ .where(between(Orders.columns.total, 100, 200))
36
+ .where(eq(Orders.columns.user_id, 1));
37
+ const compiled = q.compile(dialect);
38
+ expect(compiled.sql).toBe(
39
+ 'SELECT "orders"."*" FROM "orders" WHERE "orders"."total" BETWEEN ? AND ? AND "orders"."user_id" = ?;'
40
+ );
41
+ expect(compiled.params).toEqual([100, 200, 1]);
42
+ });
43
+ });
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Users } from '../src/playground/features/playground/data/schema';
3
+ import { caseWhen, gt, eq } from '../src/ast/expression';
4
+ import { SqliteDialect } from '../src/dialect/sqlite';
5
+ import { SelectQueryBuilder } from '../src/builder/select';
6
+
7
+ describe('CASE Expressions', () => {
8
+ const dialect = new SqliteDialect();
9
+
10
+ it('should compile simple CASE WHEN ... THEN ... ELSE ... END', () => {
11
+ const query = new SelectQueryBuilder(Users)
12
+ .select({
13
+ status: caseWhen([
14
+ { when: gt(Users.columns.id, 10), then: 'High' },
15
+ { when: gt(Users.columns.id, 5), then: 'Medium' }
16
+ ], 'Low')
17
+ });
18
+
19
+ const compiled = query.compile(dialect);
20
+ const { sql, params } = compiled;
21
+
22
+ expect(sql).toContain('CASE WHEN "users"."id" > ? THEN ? WHEN "users"."id" > ? THEN ? ELSE ? END');
23
+ expect(params).toEqual([10, 'High', 5, 'Medium', 'Low']);
24
+ });
25
+
26
+ it('should compile CASE without ELSE', () => {
27
+ const query = new SelectQueryBuilder(Users)
28
+ .select({
29
+ status: caseWhen([
30
+ { when: eq(Users.columns.name, 'Alice'), then: 'Admin' }
31
+ ])
32
+ });
33
+
34
+ const compiled = query.compile(dialect);
35
+ const { sql, params } = compiled;
36
+
37
+ expect(sql).toContain('CASE WHEN "users"."name" = ? THEN ? END');
38
+ expect(params).toEqual(['Alice', 'Admin']);
39
+ });
40
+
41
+ it('should work in WHERE clause', () => {
42
+ const query = new SelectQueryBuilder(Users)
43
+ .where(
44
+ eq(
45
+ caseWhen([
46
+ { when: gt(Users.columns.id, 10), then: 'High' }
47
+ ], 'Low'),
48
+ 'High'
49
+ )
50
+ );
51
+
52
+ const compiled = query.compile(dialect);
53
+ const { sql, params } = compiled;
54
+
55
+ expect(sql).toContain('WHERE CASE WHEN "users"."id" > ? THEN ? ELSE ? END = ?');
56
+ expect(params).toEqual([10, 'High', 'Low', 'High']);
57
+ });
58
+ });
@@ -0,0 +1,230 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SelectQueryBuilder } from '../src/builder/select';
3
+ import { SqliteDialect } from '../src/dialect/sqlite';
4
+ import { TableDef, defineTable } from '../src/schema/table';
5
+ import { col } from '../src/schema/column';
6
+ import { eq, exists, and } from '../src/ast/expression';
7
+
8
+ // Define test schema: Clientes, Pedidos and Fidelidade
9
+ const Clientes = defineTable('clientes', {
10
+ id: col.primaryKey(col.int()),
11
+ nome: col.varchar(255),
12
+ email: col.varchar(255),
13
+ }, {});
14
+
15
+ const Pedidos = defineTable('pedidos', {
16
+ id: col.primaryKey(col.int()),
17
+ cliente_id: col.int(),
18
+ data_pedido: col.varchar(50), // Usando varchar em vez de date
19
+ status: col.varchar(50),
20
+ total: col.int(),
21
+ }, {});
22
+
23
+ const Fidelidade = defineTable('fidelidade', {
24
+ id: col.primaryKey(col.int()),
25
+ cliente_id: col.int(),
26
+ status: col.varchar(50),
27
+ data_inicio: col.varchar(50), // Usando varchar em vez de date
28
+ }, {});
29
+
30
+ // Define relationships
31
+ Clientes.relations = {
32
+ pedidos: {
33
+ type: 'HAS_MANY',
34
+ target: Pedidos,
35
+ foreignKey: 'cliente_id',
36
+ localKey: 'id'
37
+ },
38
+ fidelidade: {
39
+ type: 'HAS_MANY',
40
+ target: Fidelidade,
41
+ foreignKey: 'cliente_id',
42
+ localKey: 'id'
43
+ }
44
+ };
45
+
46
+ Pedidos.relations = {
47
+ cliente: {
48
+ type: 'BELONGS_TO',
49
+ target: Clientes,
50
+ foreignKey: 'cliente_id',
51
+ localKey: 'id'
52
+ }
53
+ };
54
+
55
+ Fidelidade.relations = {
56
+ cliente: {
57
+ type: 'BELONGS_TO',
58
+ target: Clientes,
59
+ foreignKey: 'cliente_id',
60
+ localKey: 'id'
61
+ }
62
+ };
63
+
64
+ const dialect = new SqliteDialect();
65
+
66
+ describe('Complex EXISTS Query Support', () => {
67
+ it('should support EXISTS with date range AND another EXISTS', () => {
68
+ // Construindo a subquery para pedidos em 2024
69
+ const pedidosEm2024 = new SelectQueryBuilder(Pedidos)
70
+ .select({ dummy: col.int() }) // SELECT 1 (dummy)
71
+ .where(and(
72
+ eq(Pedidos.columns.cliente_id, { type: 'Column', table: 'clientes', name: 'id' }),
73
+ // BETWEEN não é suportado diretamente, então usamos >= e <=
74
+ and(
75
+ eq(Pedidos.columns.data_pedido, '2024-01-01'),
76
+ eq(Pedidos.columns.data_pedido, '2024-12-31')
77
+ )
78
+ ));
79
+
80
+ // Construindo a subquery para fidelidade ativa
81
+ const fidelidadeAtiva = new SelectQueryBuilder(Fidelidade)
82
+ .select({ dummy: col.int() }) // SELECT 1 (dummy)
83
+ .where(and(
84
+ eq(Fidelidade.columns.cliente_id, { type: 'Column', table: 'clientes', name: 'id' }),
85
+ eq(Fidelidade.columns.status, 'Ativo')
86
+ ));
87
+
88
+ // Consulta principal
89
+ const query = new SelectQueryBuilder(Clientes)
90
+ .select({ nome: Clientes.columns.nome })
91
+ .where(and(
92
+ exists(pedidosEm2024.getAST()),
93
+ exists(fidelidadeAtiva.getAST())
94
+ ));
95
+
96
+ const compiled = query.compile(dialect);
97
+ const { sql, params } = compiled;
98
+
99
+ console.log('Generated SQL:', sql);
100
+ console.log('Parameters:', params);
101
+
102
+ expect(sql).toContain('EXISTS');
103
+ expect(sql).toContain('SELECT 1 FROM "pedidos"');
104
+ expect(sql).toContain('"pedidos"."cliente_id" = "clientes"."id"');
105
+ expect(sql).toContain('"pedidos"."data_pedido"');
106
+ expect(sql).toContain('EXISTS');
107
+ expect(sql).toContain('SELECT 1 FROM "fidelidade"');
108
+ expect(sql).toContain('"fidelidade"."cliente_id" = "clientes"."id"');
109
+ expect(sql).toContain('"fidelidade"."status" = ?');
110
+ expect(params).toContain('Ativo');
111
+ });
112
+
113
+ it('should support whereHas for both conditions', () => {
114
+ // Usando whereHas para simplificar a construção das subqueries EXISTS
115
+ const query = new SelectQueryBuilder(Clientes)
116
+ .select({ nome: Clientes.columns.nome })
117
+ .whereHas('pedidos', (pedidosQb) =>
118
+ pedidosQb.where(and(
119
+ eq(Pedidos.columns.data_pedido, '2024-01-01'),
120
+ eq(Pedidos.columns.data_pedido, '2024-12-31')
121
+ ))
122
+ )
123
+ .whereHas('fidelidade', (fidelidadeQb) =>
124
+ fidelidadeQb.where(eq(Fidelidade.columns.status, 'Ativo'))
125
+ );
126
+
127
+ const compiled = query.compile(dialect);
128
+ const { sql, params } = compiled;
129
+
130
+ console.log('Generated SQL with whereHas:', sql);
131
+ console.log('Parameters:', params);
132
+
133
+ expect(sql).toContain('EXISTS');
134
+ expect(sql).toContain('SELECT 1 FROM "pedidos"');
135
+ expect(sql).toContain('"pedidos"."cliente_id" = "clientes"."id"');
136
+ expect(sql).toContain('"pedidos"."data_pedido"');
137
+ expect(sql).toContain('EXISTS');
138
+ expect(sql).toContain('SELECT 1 FROM "fidelidade"');
139
+ expect(sql).toContain('"fidelidade"."cliente_id" = "clientes"."id"');
140
+ expect(sql).toContain('"fidelidade"."status" = ?');
141
+ expect(params).toContain('Ativo');
142
+ });
143
+
144
+ it('should generate correct SQL structure for complex EXISTS query', () => {
145
+ // Teste para verificar se o SQL gerado tem a estrutura correta
146
+ const query = new SelectQueryBuilder(Clientes)
147
+ .select({ nome: Clientes.columns.nome })
148
+ .where(and(
149
+ exists({
150
+ type: 'SelectQuery',
151
+ from: { type: 'Table', name: 'pedidos' },
152
+ columns: [{ type: 'Column', table: 'pedidos', name: 'id' }],
153
+ joins: [],
154
+ where: {
155
+ type: 'LogicalExpression',
156
+ operator: 'AND',
157
+ operands: [
158
+ {
159
+ type: 'BinaryExpression',
160
+ left: { type: 'Column', table: 'pedidos', name: 'cliente_id' },
161
+ operator: '=',
162
+ right: { type: 'Column', table: 'clientes', name: 'id' }
163
+ },
164
+ {
165
+ type: 'LogicalExpression',
166
+ operator: 'AND',
167
+ operands: [
168
+ {
169
+ type: 'BinaryExpression',
170
+ left: { type: 'Column', table: 'pedidos', name: 'data_pedido' },
171
+ operator: '>=',
172
+ right: { type: 'Literal', value: '2024-01-01' }
173
+ },
174
+ {
175
+ type: 'BinaryExpression',
176
+ left: { type: 'Column', table: 'pedidos', name: 'data_pedido' },
177
+ operator: '<=',
178
+ right: { type: 'Literal', value: '2024-12-31' }
179
+ }
180
+ ]
181
+ }
182
+ ]
183
+ }
184
+ }),
185
+ exists({
186
+ type: 'SelectQuery',
187
+ from: { type: 'Table', name: 'fidelidade' },
188
+ columns: [{ type: 'Column', table: 'fidelidade', name: 'id' }],
189
+ joins: [],
190
+ where: {
191
+ type: 'LogicalExpression',
192
+ operator: 'AND',
193
+ operands: [
194
+ {
195
+ type: 'BinaryExpression',
196
+ left: { type: 'Column', table: 'fidelidade', name: 'cliente_id' },
197
+ operator: '=',
198
+ right: { type: 'Column', table: 'clientes', name: 'id' }
199
+ },
200
+ {
201
+ type: 'BinaryExpression',
202
+ left: { type: 'Column', table: 'fidelidade', name: 'status' },
203
+ operator: '=',
204
+ right: { type: 'Literal', value: 'Ativo' }
205
+ }
206
+ ]
207
+ }
208
+ })
209
+ ));
210
+
211
+ const compiled = query.compile(dialect);
212
+ const { sql, params } = compiled;
213
+
214
+ console.log('Generated SQL structure:', sql);
215
+ console.log('Parameters:', params);
216
+
217
+ // Verifica a estrutura geral da consulta
218
+ expect(sql).toContain('SELECT "clientes"."nome" AS "nome" FROM "clientes" WHERE');
219
+ expect(sql).toContain('EXISTS');
220
+ expect(sql).toContain('SELECT 1 FROM "pedidos"');
221
+ expect(sql).toContain('"pedidos"."cliente_id" = "clientes"."id"');
222
+ expect(sql).toContain('"pedidos"."data_pedido" >= ?');
223
+ expect(sql).toContain('"pedidos"."data_pedido" <= ?');
224
+ expect(sql).toContain('EXISTS');
225
+ expect(sql).toContain('SELECT 1 FROM "fidelidade"');
226
+ expect(sql).toContain('"fidelidade"."cliente_id" = "clientes"."id"');
227
+ expect(sql).toContain('"fidelidade"."status" = ?');
228
+ expect(params).toEqual(['2024-01-01', '2024-12-31', 'Ativo']);
229
+ });
230
+ });