metal-orm 1.0.4 → 1.0.5

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.
@@ -1,6 +1,6 @@
1
- import { CompilerContext, Dialect } from '../abstract';
2
- import { SelectQueryNode } from '../../ast/query';
3
- import { JsonPathNode } from '../../ast/expression';
1
+ import { CompilerContext, Dialect } from '../abstract';
2
+ import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode } from '../../ast/query';
3
+ import { JsonPathNode, ColumnNode } from '../../ast/expression';
4
4
 
5
5
  /**
6
6
  * SQLite dialect implementation
@@ -39,7 +39,7 @@ export class SqliteDialect extends Dialect {
39
39
  * @param ctx - Compiler context
40
40
  * @returns SQLite SQL string
41
41
  */
42
- protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
42
+ protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
43
43
  const columns = ast.columns.map(c => {
44
44
  let expr = '';
45
45
  if (c.type === 'Function') {
@@ -102,6 +102,45 @@ export class SqliteDialect extends Dialect {
102
102
  })()
103
103
  : '';
104
104
 
105
- return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy}${limit}${offset};`;
106
- }
105
+ return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy}${limit}${offset};`;
106
+ }
107
+
108
+ protected compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string {
109
+ const table = this.quoteIdentifier(ast.into.name);
110
+ const columnList = ast.columns.map(column => `${this.quoteIdentifier(column.table)}.${this.quoteIdentifier(column.name)}`).join(', ');
111
+ const values = ast.values.map(row => `(${row.map(value => this.compileOperand(value, ctx)).join(', ')})`).join(', ');
112
+ const returning = this.compileReturning(ast.returning, ctx);
113
+ return `INSERT INTO ${table} (${columnList}) VALUES ${values}${returning};`;
114
+ }
115
+
116
+ protected compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string {
117
+ const table = this.quoteIdentifier(ast.table.name);
118
+ const assignments = ast.set.map(assignment => {
119
+ const col = assignment.column;
120
+ const target = `${this.quoteIdentifier(col.table)}.${this.quoteIdentifier(col.name)}`;
121
+ const value = this.compileOperand(assignment.value, ctx);
122
+ return `${target} = ${value}`;
123
+ }).join(', ');
124
+ const whereClause = this.compileWhere(ast.where, ctx);
125
+ const returning = this.compileReturning(ast.returning, ctx);
126
+ return `UPDATE ${table} SET ${assignments}${whereClause}${returning};`;
127
+ }
128
+
129
+ protected compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string {
130
+ const table = this.quoteIdentifier(ast.from.name);
131
+ const whereClause = this.compileWhere(ast.where, ctx);
132
+ const returning = this.compileReturning(ast.returning, ctx);
133
+ return `DELETE FROM ${table}${whereClause}${returning};`;
134
+ }
135
+
136
+ protected compileReturning(returning: ColumnNode[] | undefined, ctx: CompilerContext): string {
137
+ if (!returning || returning.length === 0) return '';
138
+ const columns = returning
139
+ .map(column => {
140
+ const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : '';
141
+ return `${tablePart}${this.quoteIdentifier(column.name)}`;
142
+ })
143
+ .join(', ');
144
+ return ` RETURNING ${columns}`;
145
+ }
107
146
  }
package/src/index.ts CHANGED
@@ -1,9 +1,12 @@
1
1
 
2
2
  export * from './schema/table';
3
3
  export * from './schema/column';
4
- export * from './schema/relation';
5
- export * from './builder/select';
6
- export * from './ast/expression';
4
+ export * from './schema/relation';
5
+ export * from './builder/select';
6
+ export * from './builder/insert';
7
+ export * from './builder/update';
8
+ export * from './builder/delete';
9
+ export * from './ast/expression';
7
10
  export * from './dialect/mysql';
8
11
  export * from './dialect/mssql';
9
12
  export * from './dialect/sqlite';
@@ -2,14 +2,26 @@ import { eq } from '../../../../../ast/expression';
2
2
  import { Users } from '../schema';
3
3
  import { createScenario } from './types';
4
4
 
5
- export const HYDRATION_SCENARIOS = [
6
- createScenario({
7
- id: 'user_with_orders',
8
- category: 'Hydration',
9
- title: 'Nested User Graph',
10
- description: 'Eager-load orders for a single user and hydrate a JSON graph from flat SQL rows.',
11
- build: (qb) => qb
12
- .include('orders', { columns: ['id', 'total', 'status', 'user_id'] })
13
- .where(eq(Users.columns.id, 1))
14
- })
15
- ];
5
+ export const HYDRATION_SCENARIOS = [
6
+ createScenario({
7
+ id: 'user_with_orders',
8
+ category: 'Hydration',
9
+ title: 'Nested User Graph',
10
+ description: 'Eager-load orders for a single user and hydrate a JSON graph from flat SQL rows.',
11
+ build: (qb) => qb
12
+ .include('orders', { columns: ['id', 'total', 'status', 'user_id'] })
13
+ .where(eq(Users.columns.id, 1))
14
+ }),
15
+ createScenario({
16
+ id: 'user_projects_with_pivot',
17
+ category: 'Hydration',
18
+ title: 'Projects with Pivot Data',
19
+ description: 'Include projects for a user along with pivot metadata stored in `_pivot`.',
20
+ build: (qb) => qb
21
+ .include('projects', {
22
+ columns: ['id', 'name', 'client'],
23
+ pivot: { columns: ['assigned_at', 'role_id'] }
24
+ })
25
+ .where(eq(Users.columns.id, 1))
26
+ })
27
+ ];
@@ -1,6 +1,6 @@
1
1
  import { defineTable } from '../../../../schema/table';
2
2
  import { col } from '../../../../schema/column';
3
- import { hasMany, belongsTo } from '../../../../schema/relation';
3
+ import { hasMany, belongsTo, belongsToMany } from '../../../../schema/relation';
4
4
 
5
5
  export const Users = defineTable('users', {
6
6
  id: col.primaryKey(col.int()),
@@ -52,11 +52,15 @@ export const ProjectAssignments = defineTable('project_assignments', {
52
52
  assigned_at: col.varchar(50)
53
53
  });
54
54
 
55
- Users.relations = {
56
- orders: hasMany(Orders, 'user_id'),
57
- profiles: hasMany(Profiles, 'user_id'),
58
- userRoles: hasMany(UserRoles, 'user_id')
59
- };
55
+ Users.relations = {
56
+ orders: hasMany(Orders, 'user_id'),
57
+ profiles: hasMany(Profiles, 'user_id'),
58
+ userRoles: hasMany(UserRoles, 'user_id'),
59
+ projects: belongsToMany(Projects, ProjectAssignments, {
60
+ pivotForeignKeyToRoot: 'user_id',
61
+ pivotForeignKeyToTarget: 'project_id'
62
+ })
63
+ };
60
64
 
61
65
  Orders.relations = {
62
66
  user: belongsTo(Users, 'user_id')
@@ -26,20 +26,32 @@ export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan):
26
26
 
27
27
  const parent = rootMap.get(rootId);
28
28
 
29
- plan.relations.forEach(rel => {
29
+ plan.relations.forEach(rel => {
30
30
  const childPkKey = makeRelationAlias(rel.aliasPrefix, rel.targetPrimaryKey);
31
31
  const childPk = row[childPkKey];
32
- if (childPk === null || childPk === undefined) return;
33
-
32
+ if (childPk === null || childPk === undefined) return;
33
+
34
34
  const bucket = parent[rel.name] as any[];
35
35
  if (bucket.some(item => item[rel.targetPrimaryKey] === childPk)) return;
36
36
 
37
- const child: Record<string, any> = {};
37
+ const child: Record<string, any> = {};
38
38
  rel.columns.forEach(col => {
39
39
  const key = makeRelationAlias(rel.aliasPrefix, col);
40
40
  child[col] = row[key];
41
41
  });
42
-
42
+
43
+ if (rel.pivot) {
44
+ const pivot: Record<string, any> = {};
45
+ rel.pivot.columns.forEach(col => {
46
+ const key = makeRelationAlias(rel.pivot.aliasPrefix, col);
47
+ pivot[col] = row[key];
48
+ });
49
+
50
+ if (Object.values(pivot).some(v => v !== null && v !== undefined)) {
51
+ (child as any)._pivot = pivot;
52
+ }
53
+ }
54
+
43
55
  bucket.push(child);
44
56
  });
45
57
  });
@@ -8,6 +8,8 @@ export const RelationKinds = {
8
8
  HasMany: 'HAS_MANY',
9
9
  /** Many-to-one relationship */
10
10
  BelongsTo: 'BELONGS_TO',
11
+ /** Many-to-many relationship with pivot metadata */
12
+ BelongsToMany: 'BELONGS_TO_MANY'
11
13
  } as const;
12
14
 
13
15
  /**
@@ -16,41 +18,50 @@ export const RelationKinds = {
16
18
  export type RelationType = (typeof RelationKinds)[keyof typeof RelationKinds];
17
19
 
18
20
  /**
19
- * Base properties common to all relationship types
21
+ * One-to-many relationship definition
20
22
  */
21
- interface BaseRelation {
22
- /** Target table of the relationship */
23
+ export interface HasManyRelation {
24
+ type: typeof RelationKinds.HasMany;
23
25
  target: TableDef;
24
- /** Foreign key column name on the child table */
25
26
  foreignKey: string;
26
- /** Local key column name (usually 'id') */
27
27
  localKey?: string;
28
28
  }
29
29
 
30
30
  /**
31
- * One-to-many relationship definition
31
+ * Many-to-one relationship definition
32
32
  */
33
- export interface HasManyRelation extends BaseRelation {
34
- type: typeof RelationKinds.HasMany;
33
+ export interface BelongsToRelation {
34
+ type: typeof RelationKinds.BelongsTo;
35
+ target: TableDef;
36
+ foreignKey: string;
37
+ localKey?: string;
35
38
  }
36
39
 
37
40
  /**
38
- * Many-to-one relationship definition
41
+ * Many-to-many relationship definition with rich pivot metadata
39
42
  */
40
- export interface BelongsToRelation extends BaseRelation {
41
- type: typeof RelationKinds.BelongsTo;
43
+ export interface BelongsToManyRelation {
44
+ type: typeof RelationKinds.BelongsToMany;
45
+ target: TableDef;
46
+ pivotTable: TableDef;
47
+ pivotForeignKeyToRoot: string;
48
+ pivotForeignKeyToTarget: string;
49
+ localKey?: string;
50
+ targetKey?: string;
51
+ pivotPrimaryKey?: string;
52
+ defaultPivotColumns?: string[];
42
53
  }
43
54
 
44
55
  /**
45
56
  * Union type representing any supported relationship definition
46
57
  */
47
- export type RelationDef = HasManyRelation | BelongsToRelation;
58
+ export type RelationDef = HasManyRelation | BelongsToRelation | BelongsToManyRelation;
48
59
 
49
60
  /**
50
61
  * Creates a one-to-many relationship definition
51
62
  * @param target - Target table of the relationship
52
63
  * @param foreignKey - Foreign key column name on the child table
53
- * @param localKey - Local key column name (defaults to 'id')
64
+ * @param localKey - Local key column name (optional)
54
65
  * @returns HasManyRelation definition
55
66
  *
56
67
  * @example
@@ -58,18 +69,18 @@ export type RelationDef = HasManyRelation | BelongsToRelation;
58
69
  * hasMany(usersTable, 'user_id')
59
70
  * ```
60
71
  */
61
- export const hasMany = (target: TableDef, foreignKey: string, localKey: string = 'id'): HasManyRelation => ({
72
+ export const hasMany = (target: TableDef, foreignKey: string, localKey?: string): HasManyRelation => ({
62
73
  type: RelationKinds.HasMany,
63
74
  target,
64
75
  foreignKey,
65
- localKey,
76
+ localKey
66
77
  });
67
78
 
68
79
  /**
69
80
  * Creates a many-to-one relationship definition
70
81
  * @param target - Target table of the relationship
71
82
  * @param foreignKey - Foreign key column name on the child table
72
- * @param localKey - Local key column name (defaults to 'id')
83
+ * @param localKey - Local key column name (optional)
73
84
  * @returns BelongsToRelation definition
74
85
  *
75
86
  * @example
@@ -77,9 +88,39 @@ export const hasMany = (target: TableDef, foreignKey: string, localKey: string =
77
88
  * belongsTo(usersTable, 'user_id')
78
89
  * ```
79
90
  */
80
- export const belongsTo = (target: TableDef, foreignKey: string, localKey: string = 'id'): BelongsToRelation => ({
91
+ export const belongsTo = (target: TableDef, foreignKey: string, localKey?: string): BelongsToRelation => ({
81
92
  type: RelationKinds.BelongsTo,
82
93
  target,
83
94
  foreignKey,
84
- localKey,
95
+ localKey
96
+ });
97
+
98
+ /**
99
+ * Creates a many-to-many relationship definition with pivot metadata
100
+ * @param target - Target table
101
+ * @param pivotTable - Intermediate pivot table definition
102
+ * @param options - Pivot metadata configuration
103
+ * @returns BelongsToManyRelation definition
104
+ */
105
+ export const belongsToMany = (
106
+ target: TableDef,
107
+ pivotTable: TableDef,
108
+ options: {
109
+ pivotForeignKeyToRoot: string;
110
+ pivotForeignKeyToTarget: string;
111
+ localKey?: string;
112
+ targetKey?: string;
113
+ pivotPrimaryKey?: string;
114
+ defaultPivotColumns?: string[];
115
+ }
116
+ ): BelongsToManyRelation => ({
117
+ type: RelationKinds.BelongsToMany,
118
+ target,
119
+ pivotTable,
120
+ pivotForeignKeyToRoot: options.pivotForeignKeyToRoot,
121
+ pivotForeignKeyToTarget: options.pivotForeignKeyToTarget,
122
+ localKey: options.localKey,
123
+ targetKey: options.targetKey,
124
+ pivotPrimaryKey: options.pivotPrimaryKey,
125
+ defaultPivotColumns: options.defaultPivotColumns
85
126
  });
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { hydrateRows } from '../src/runtime/hydration';
3
+ import { SelectQueryBuilder } from '../src/builder/select';
4
+ import { SqliteDialect } from '../src/dialect/sqlite';
5
+ import { makeRelationAlias } from '../src/utils/relation-alias';
6
+ import { Users } from '../src/playground/features/playground/data/schema';
7
+
8
+ describe('BelongsToMany hydration', () => {
9
+ it('includes pivot metadata for a projects include', () => {
10
+ const builder = new SelectQueryBuilder(Users).include('projects', {
11
+ columns: ['id', 'name', 'client'],
12
+ pivot: { columns: ['assigned_at', 'role_id'] }
13
+ });
14
+
15
+ const compiled = builder.compile(new SqliteDialect());
16
+ expect(compiled.sql).toContain('JOIN "project_assignments"');
17
+ expect(compiled.sql).toContain('JOIN "projects"');
18
+
19
+ const plan = builder.getHydrationPlan();
20
+ expect(plan).toBeDefined();
21
+
22
+ const relationPlan = plan!.relations.find(rel => rel.name === 'projects');
23
+ expect(relationPlan).toBeDefined();
24
+ expect(relationPlan!.pivot).toBeDefined();
25
+ expect(relationPlan!.pivot!.columns).toEqual(['assigned_at', 'role_id']);
26
+ expect(relationPlan!.pivot!.aliasPrefix).toBe('projects_pivot');
27
+
28
+ const row: Record<string, any> = {};
29
+ plan!.rootColumns.forEach(col => {
30
+ row[col] = col === plan!.rootPrimaryKey ? 1 : `root-${col}`;
31
+ });
32
+
33
+ row[makeRelationAlias(relationPlan!.aliasPrefix, relationPlan!.targetPrimaryKey)] = 42;
34
+ relationPlan!.columns.forEach(col => {
35
+ const alias = makeRelationAlias(relationPlan!.aliasPrefix, col);
36
+ row[alias] = col === relationPlan!.targetPrimaryKey ? 42 : `project-${col}`;
37
+ });
38
+
39
+ relationPlan!.pivot!.columns.forEach((col, idx) => {
40
+ const alias = makeRelationAlias(relationPlan!.pivot!.aliasPrefix, col);
41
+ row[alias] = `pivot-${col}-${idx}`;
42
+ });
43
+
44
+ const hydrated = hydrateRows([row], plan);
45
+ expect(hydrated).toHaveLength(1);
46
+ expect(hydrated[0].projects).toHaveLength(1);
47
+ expect(hydrated[0].projects[0]).toEqual({
48
+ id: 42,
49
+ name: 'project-name',
50
+ client: 'project-client',
51
+ _pivot: {
52
+ assigned_at: 'pivot-assigned_at-0',
53
+ role_id: 'pivot-role_id-1'
54
+ }
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,206 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { InsertQueryBuilder, UpdateQueryBuilder, DeleteQueryBuilder } from '../src';
3
+ import { Dialect } from '../src/dialect/abstract';
4
+ import { MySqlDialect } from '../src/dialect/mysql';
5
+ import { PostgresDialect } from '../src/dialect/postgres';
6
+ import { SqliteDialect } from '../src/dialect/sqlite';
7
+ import { SqlServerDialect } from '../src/dialect/mssql';
8
+ import { Users } from '../src/playground/features/playground/data/schema';
9
+ import { eq } from '../src/ast/expression';
10
+ import type { ColumnDef } from '../src/schema/column';
11
+
12
+ type Row = {
13
+ name: string;
14
+ role: string;
15
+ };
16
+
17
+ interface DialectCase {
18
+ name: string;
19
+ dialect: Dialect;
20
+ placeholder: (index: number) => string;
21
+ supportsReturning: boolean;
22
+ }
23
+
24
+ const rowOrder: (keyof Row)[] = ['name', 'role'];
25
+ const insertRows: Row[] = [
26
+ { name: 'alice', role: 'admin' },
27
+ { name: 'bob', role: 'user' }
28
+ ];
29
+
30
+ const returningColumns: ColumnDef[] = [Users.columns.id, Users.columns.name];
31
+ const columnColumns: ColumnDef[] = [Users.columns.name, Users.columns.role];
32
+
33
+ const dialectCases: DialectCase[] = [
34
+ {
35
+ name: 'MySQL',
36
+ dialect: new MySqlDialect(),
37
+ placeholder: () => '?',
38
+ supportsReturning: false
39
+ },
40
+ {
41
+ name: 'Postgres',
42
+ dialect: new PostgresDialect(),
43
+ placeholder: () => '?',
44
+ supportsReturning: true
45
+ },
46
+ {
47
+ name: 'SQLite',
48
+ dialect: new SqliteDialect(),
49
+ placeholder: () => '?',
50
+ supportsReturning: true
51
+ },
52
+ {
53
+ name: 'SQL Server',
54
+ dialect: new SqlServerDialect(),
55
+ placeholder: index => `@p${index}`,
56
+ supportsReturning: false
57
+ }
58
+ ];
59
+
60
+ const qualifyColumn = (dialect: Dialect, column: ColumnDef): string =>
61
+ `${dialect.quoteIdentifier(column.table || Users.name)}.${dialect.quoteIdentifier(column.name)}`;
62
+
63
+ const buildColumnList = (dialect: Dialect, columns: ColumnDef[]): string =>
64
+ columns.map(column => qualifyColumn(dialect, column)).join(', ');
65
+
66
+ const buildReturningClause = (dialect: Dialect, columns: ColumnDef[]): string =>
67
+ columns.length === 0 ? '' : ` RETURNING ${buildColumnList(dialect, columns)}`;
68
+
69
+ const buildValuesClause = (dialectCase: DialectCase, columnCount: number, rowCount: number): string => {
70
+ let index = 1;
71
+ const segments: string[] = [];
72
+ for (let row = 0; row < rowCount; row += 1) {
73
+ const placeholders: string[] = [];
74
+ for (let col = 0; col < columnCount; col += 1) {
75
+ placeholders.push(dialectCase.placeholder(index));
76
+ index += 1;
77
+ }
78
+ segments.push(`(${placeholders.join(', ')})`);
79
+ }
80
+ return segments.join(', ');
81
+ };
82
+
83
+ const buildPlaceholderSequence = (dialectCase: DialectCase, count: number, startIndex = 1): string[] =>
84
+ Array.from({ length: count }, (_, idx) => dialectCase.placeholder(startIndex + idx));
85
+
86
+ const flattenRowValues = (rows: Row[], order: (keyof Row)[]): unknown[] =>
87
+ rows.flatMap(row => order.map(key => row[key]));
88
+
89
+ describe('DML builders', () => {
90
+ dialectCases.forEach(dialectCase => {
91
+ describe(dialectCase.name, () => {
92
+ const dialect = dialectCase.dialect;
93
+ const tableName = Users.name;
94
+ const qualifiedColumns = buildColumnList(dialect, columnColumns);
95
+ const returningSql = buildReturningClause(dialect, returningColumns);
96
+
97
+ it('compiles single-row insert', () => {
98
+ const query = new InsertQueryBuilder(Users).values(insertRows[0]);
99
+ const compiled = query.compile(dialect);
100
+ const valueClause = `(${dialectCase.placeholder(1)}, ${dialectCase.placeholder(2)})`;
101
+ const expectedSql = `INSERT INTO ${dialect.quoteIdentifier(tableName)} (${qualifiedColumns}) VALUES ${valueClause};`;
102
+ expect(compiled.sql).toBe(expectedSql);
103
+ expect(compiled.params).toEqual([insertRows[0].name, insertRows[0].role]);
104
+ });
105
+
106
+ it('compiles multi-row insert with consistent parameter order', () => {
107
+ const query = new InsertQueryBuilder(Users).values(insertRows);
108
+ const compiled = query.compile(dialect);
109
+ const valuesClause = buildValuesClause(dialectCase, columnColumns.length, insertRows.length);
110
+ const expectedSql = `INSERT INTO ${dialect.quoteIdentifier(tableName)} (${qualifiedColumns}) VALUES ${valuesClause};`;
111
+ expect(compiled.sql).toBe(expectedSql);
112
+ expect(compiled.params).toEqual(flattenRowValues(insertRows, rowOrder));
113
+ });
114
+
115
+ if (dialectCase.supportsReturning) {
116
+ it('appends RETURNING for insert when requested', () => {
117
+ const query = new InsertQueryBuilder(Users)
118
+ .values(insertRows[0])
119
+ .returning(Users.columns.id, Users.columns.name);
120
+ const compiled = query.compile(dialect);
121
+ const valueClause = `(${dialectCase.placeholder(1)}, ${dialectCase.placeholder(2)})`;
122
+ const expectedSql = `INSERT INTO ${dialect.quoteIdentifier(tableName)} (${qualifiedColumns}) VALUES ${valueClause}${returningSql};`;
123
+ expect(compiled.sql).toBe(expectedSql);
124
+ expect(compiled.params).toEqual([insertRows[0].name, insertRows[0].role]);
125
+ });
126
+ }
127
+
128
+ it('compiles update with SET values', () => {
129
+ const updateValues = { name: 'ali', role: 'builder' };
130
+ const query = new UpdateQueryBuilder(Users).set(updateValues);
131
+ const compiled = query.compile(dialect);
132
+ const placeholderSeq = buildPlaceholderSequence(dialectCase, columnColumns.length);
133
+ const assignments = columnColumns
134
+ .map((column, idx) => `${qualifyColumn(dialect, column)} = ${placeholderSeq[idx]}`)
135
+ .join(', ');
136
+ const expectedSql = `UPDATE ${dialect.quoteIdentifier(tableName)} SET ${assignments};`;
137
+ expect(compiled.sql).toBe(expectedSql);
138
+ expect(compiled.params).toEqual([updateValues.name, updateValues.role]);
139
+ });
140
+
141
+ it('compiles update with WHERE clause', () => {
142
+ const updateValues = { name: 'gold', role: 'star' };
143
+ const query = new UpdateQueryBuilder(Users)
144
+ .set(updateValues)
145
+ .where(eq(Users.columns.id, 1));
146
+ const compiled = query.compile(dialect);
147
+ const assignmentPlaceholders = buildPlaceholderSequence(dialectCase, columnColumns.length);
148
+ const assignments = columnColumns
149
+ .map((column, idx) => `${qualifyColumn(dialect, column)} = ${assignmentPlaceholders[idx]}`)
150
+ .join(', ');
151
+ const wherePlaceholder = dialectCase.placeholder(columnColumns.length + 1);
152
+ const whereClause = ` WHERE ${qualifyColumn(dialect, Users.columns.id)} = ${wherePlaceholder}`;
153
+ const expectedSql = `UPDATE ${dialect.quoteIdentifier(tableName)} SET ${assignments}${whereClause};`;
154
+ expect(compiled.sql).toBe(expectedSql);
155
+ expect(compiled.params).toEqual([updateValues.name, updateValues.role, 1]);
156
+ });
157
+
158
+ if (dialectCase.supportsReturning) {
159
+ it('appends RETURNING for update when requested', () => {
160
+ const query = new UpdateQueryBuilder(Users)
161
+ .set({ name: 'return' })
162
+ .returning(Users.columns.id, Users.columns.name);
163
+ const compiled = query.compile(dialect);
164
+ const placeholder = dialectCase.placeholder(1);
165
+ const assignment = `${qualifyColumn(dialect, Users.columns.name)} = ${placeholder}`;
166
+ const expectedSql = `UPDATE ${dialect.quoteIdentifier(tableName)} SET ${assignment}${returningSql};`;
167
+ expect(compiled.sql).toBe(expectedSql);
168
+ expect(compiled.params).toEqual(['return']);
169
+ });
170
+ }
171
+
172
+ it('compiles DELETE without WHERE', () => {
173
+ const query = new DeleteQueryBuilder(Users);
174
+ const compiled = query.compile(dialect);
175
+ const expectedSql = `DELETE FROM ${dialect.quoteIdentifier(tableName)};`;
176
+ expect(compiled.sql).toBe(expectedSql);
177
+ expect(compiled.params).toEqual([]);
178
+ });
179
+
180
+ it('compiles DELETE with WHERE clause', () => {
181
+ const query = new DeleteQueryBuilder(Users).where(eq(Users.columns.id, 7));
182
+ const compiled = query.compile(dialect);
183
+ const wherePlaceholder = dialectCase.placeholder(1);
184
+ const whereClause = ` WHERE ${qualifyColumn(dialect, Users.columns.id)} = ${wherePlaceholder}`;
185
+ const expectedSql = `DELETE FROM ${dialect.quoteIdentifier(tableName)}${whereClause};`;
186
+ expect(compiled.sql).toBe(expectedSql);
187
+ expect(compiled.params).toEqual([7]);
188
+ });
189
+
190
+ if (dialectCase.supportsReturning) {
191
+ it('appends RETURNING for delete when requested', () => {
192
+ const query = new DeleteQueryBuilder(Users)
193
+ .where(eq(Users.columns.id, 11))
194
+ .returning(Users.columns.id, Users.columns.name);
195
+ const compiled = query.compile(dialect);
196
+ const wherePlaceholder = dialectCase.placeholder(1);
197
+ const whereClause = ` WHERE ${qualifyColumn(dialect, Users.columns.id)} = ${wherePlaceholder}`;
198
+ const expectedSql = `DELETE FROM ${dialect.quoteIdentifier(tableName)}${whereClause}${returningSql};`;
199
+ expect(compiled.sql).toBe(expectedSql);
200
+ expect(compiled.params).toEqual([11]);
201
+ });
202
+ }
203
+ });
204
+ });
205
+ });
206
+