metal-orm 1.0.81 → 1.0.83

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.
@@ -26,25 +26,28 @@ import {
26
26
  BetweenExpressionNode,
27
27
  ArithmeticExpressionNode,
28
28
  BitwiseExpressionNode,
29
- CollateExpressionNode,
30
- AliasRefNode,
31
- isOperandNode
32
- } from '../ast/expression.js';
29
+ CollateExpressionNode,
30
+ AliasRefNode,
31
+ isOperandNode,
32
+ ParamNode
33
+ } from '../ast/expression.js';
33
34
  import { DialectName } from '../sql/sql.js';
34
35
  import type { FunctionStrategy } from '../functions/types.js';
35
36
  import { StandardFunctionStrategy } from '../functions/standard-strategy.js';
36
37
  import type { TableFunctionStrategy } from '../functions/table-types.js';
37
38
  import { StandardTableFunctionStrategy } from '../functions/standard-table-strategy.js';
38
39
 
39
- /**
40
- * Context for SQL compilation with parameter management
41
- */
42
- export interface CompilerContext {
43
- /** Array of parameters */
44
- params: unknown[];
45
- /** Function to add a parameter and get its placeholder */
46
- addParameter(value: unknown): string;
47
- }
40
+ /**
41
+ * Context for SQL compilation with parameter management
42
+ */
43
+ export interface CompilerContext {
44
+ /** Array of parameters */
45
+ params: unknown[];
46
+ /** Function to add a parameter and get its placeholder */
47
+ addParameter(value: unknown): string;
48
+ /** Whether Param operands are allowed (for schema generation) */
49
+ allowParams?: boolean;
50
+ }
48
51
 
49
52
  /**
50
53
  * Result of SQL compilation
@@ -85,18 +88,29 @@ export abstract class Dialect
85
88
  * @param ast - Query AST to compile
86
89
  * @returns Compiled query with SQL and parameters
87
90
  */
88
- compileSelect(ast: SelectQueryNode): CompiledQuery {
89
- const ctx = this.createCompilerContext();
90
- const normalized = this.normalizeSelectAst(ast);
91
- const rawSql = this.compileSelectAst(normalized, ctx).trim();
92
- const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
93
- return {
94
- sql,
95
- params: [...ctx.params]
96
- };
97
- }
98
-
99
- compileInsert(ast: InsertQueryNode): CompiledQuery {
91
+ compileSelect(ast: SelectQueryNode): CompiledQuery {
92
+ const ctx = this.createCompilerContext();
93
+ const normalized = this.normalizeSelectAst(ast);
94
+ const rawSql = this.compileSelectAst(normalized, ctx).trim();
95
+ const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
96
+ return {
97
+ sql,
98
+ params: [...ctx.params]
99
+ };
100
+ }
101
+
102
+ compileSelectWithOptions(ast: SelectQueryNode, options: { allowParams?: boolean } = {}): CompiledQuery {
103
+ const ctx = this.createCompilerContext(options);
104
+ const normalized = this.normalizeSelectAst(ast);
105
+ const rawSql = this.compileSelectAst(normalized, ctx).trim();
106
+ const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
107
+ return {
108
+ sql,
109
+ params: [...ctx.params]
110
+ };
111
+ }
112
+
113
+ compileInsert(ast: InsertQueryNode): CompiledQuery {
100
114
  const ctx = this.createCompilerContext();
101
115
  const rawSql = this.compileInsertAst(ast, ctx).trim();
102
116
  const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
@@ -197,22 +211,24 @@ export abstract class Dialect
197
211
  return `SELECT 1${tail}`;
198
212
  }
199
213
 
200
- /**
201
- * Creates a new compiler context
202
- * @returns Compiler context with parameter management
203
- */
204
- protected createCompilerContext(): CompilerContext {
205
- const params: unknown[] = [];
206
- let counter = 0;
207
- return {
208
- params,
209
- addParameter: (value: unknown) => {
210
- counter += 1;
211
- params.push(value);
212
- return this.formatPlaceholder(counter);
213
- }
214
- };
215
- }
214
+ /**
215
+ * Creates a new compiler context
216
+ * @param options - Optional compiler context options
217
+ * @returns Compiler context with parameter management
218
+ */
219
+ protected createCompilerContext(options: { allowParams?: boolean } = {}): CompilerContext {
220
+ const params: unknown[] = [];
221
+ let counter = 0;
222
+ return {
223
+ params,
224
+ allowParams: options.allowParams ?? false,
225
+ addParameter: (value: unknown) => {
226
+ counter += 1;
227
+ params.push(value);
228
+ return this.formatPlaceholder(counter);
229
+ }
230
+ };
231
+ }
216
232
 
217
233
  /**
218
234
  * Formats a parameter placeholder
@@ -452,13 +468,19 @@ export abstract class Dialect
452
468
  });
453
469
  }
454
470
 
455
- private registerDefaultOperandCompilers(): void {
456
- this.registerOperandCompiler('Literal', (literal: LiteralNode, ctx) => ctx.addParameter(literal.value));
457
-
458
- this.registerOperandCompiler('AliasRef', (alias: AliasRefNode, _ctx) => {
459
- void _ctx;
460
- return this.quoteIdentifier(alias.name);
461
- });
471
+ private registerDefaultOperandCompilers(): void {
472
+ this.registerOperandCompiler('Literal', (literal: LiteralNode, ctx) => ctx.addParameter(literal.value));
473
+ this.registerOperandCompiler('Param', (_param: ParamNode, ctx) => {
474
+ if (!ctx.allowParams) {
475
+ throw new Error('Cannot compile query with Param operands. Param proxies are only for schema generation (getSchema()). If you need real parameters, use literal values.');
476
+ }
477
+ return ctx.addParameter(null);
478
+ });
479
+
480
+ this.registerOperandCompiler('AliasRef', (alias: AliasRefNode, _ctx) => {
481
+ void _ctx;
482
+ return this.quoteIdentifier(alias.name);
483
+ });
462
484
 
463
485
  this.registerOperandCompiler('Column', (column: ColumnNode, _ctx) => {
464
486
  void _ctx;
@@ -117,6 +117,7 @@ const collectFilterColumns = (
117
117
  }
118
118
  case 'AliasRef':
119
119
  case 'Literal':
120
+ case 'Param':
120
121
  return;
121
122
  default:
122
123
  return;
@@ -144,6 +144,7 @@ const collectFromOperand = (node: OperandNode, collector: FilterTableCollector):
144
144
  break;
145
145
  case 'Literal':
146
146
  case 'AliasRef':
147
+ case 'Param':
147
148
  break;
148
149
  default:
149
150
  break;
@@ -68,6 +68,7 @@ import { SelectCTEFacet } from './select/cte-facet.js';
68
68
  import { SelectSetOpFacet } from './select/setop-facet.js';
69
69
  import { SelectRelationFacet } from './select/relation-facet.js';
70
70
  import { buildFilterParameters, extractSchema, SchemaOptions, OpenApiSchemaBundle } from '../openapi/index.js';
71
+ import { hasParamOperandsInQuery } from '../core/ast/ast-validation.js';
71
72
 
72
73
  type ColumnSelectionValue =
73
74
  | ColumnDef
@@ -713,120 +714,138 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
713
714
  return this.env.table as TTable;
714
715
  }
715
716
 
716
- /**
717
- * Ensures that if no columns are selected, all columns from the table are selected by default.
718
- */
719
- private ensureDefaultSelection(): SelectQueryBuilder<T, TTable> {
720
- const columns = this.context.state.ast.columns;
721
- if (!columns || columns.length === 0) {
722
- const columnKeys = Object.keys(this.env.table.columns) as (keyof TTable['columns'] & string)[];
723
- return this.select(...columnKeys);
724
- }
725
- return this;
726
- }
727
-
728
- /**
729
- * Executes the query and returns hydrated results.
730
- * If the builder was created with an entity constructor (e.g. via selectFromEntity),
731
- * this will automatically return fully materialized entity instances.
732
- *
733
- * @param ctx - ORM session context
734
- * @returns Promise of entity instances (or objects if generic T is not an entity)
735
- * @example
736
- * const users = await selectFromEntity(User).execute(session);
737
- * // users is User[]
738
- * users[0] instanceof User; // true
739
- */
740
- async execute(ctx: OrmSession): Promise<T[]> {
741
- if (this.entityConstructor) {
742
- return this.executeAs(this.entityConstructor, ctx) as unknown as T[];
743
- }
744
- const builder = this.ensureDefaultSelection();
745
- return executeHydrated(ctx, builder) as unknown as T[];
746
- }
717
+ /**
718
+ * Ensures that if no columns are selected, all columns from the table are selected by default.
719
+ */
720
+ private ensureDefaultSelection(): SelectQueryBuilder<T, TTable> {
721
+ const columns = this.context.state.ast.columns;
722
+ if (!columns || columns.length === 0) {
723
+ const columnKeys = Object.keys(this.env.table.columns) as (keyof TTable['columns'] & string)[];
724
+ return this.select(...columnKeys);
725
+ }
726
+ return this;
727
+ }
728
+
729
+ /**
730
+ * Validates that the query does not contain Param operands.
731
+ * Param proxies are only for schema generation, not execution.
732
+ */
733
+ private validateNoParamOperands(): void {
734
+ const ast = this.context.hydration.applyToAst(this.context.state.ast);
735
+ const hasParams = hasParamOperandsInQuery(ast);
736
+ if (hasParams) {
737
+ throw new Error('Cannot execute query containing Param operands. Param proxies are only for schema generation (getSchema()). If you need real parameters, use literal values.');
738
+ }
739
+ }
747
740
 
748
- /**
749
- * Executes the query and returns plain row objects (POJOs), ignoring any entity materialization.
750
- * Use this if you want raw data even when using selectFromEntity.
751
- *
752
- * @param ctx - ORM session context
753
- * @returns Promise of plain entity instances
754
- * @example
755
- * const rows = await selectFromEntity(User).executePlain(session);
756
- * // rows is EntityInstance<UserTable>[] (plain objects)
757
- * rows[0] instanceof User; // false
758
- */
759
- async executePlain(ctx: OrmSession): Promise<EntityInstance<TTable>[]> {
760
- const builder = this.ensureDefaultSelection();
761
- const rows = await executeHydratedPlain(ctx, builder);
762
- return rows as EntityInstance<TTable>[];
763
- }
741
+ /**
742
+ * Executes the query and returns hydrated results.
743
+ * If the builder was created with an entity constructor (e.g. via selectFromEntity),
744
+ * this will automatically return fully materialized entity instances.
745
+ *
746
+ * @param ctx - ORM session context
747
+ * @returns Promise of entity instances (or objects if generic T is not an entity)
748
+ * @example
749
+ * const users = await selectFromEntity(User).execute(session);
750
+ * // users is User[]
751
+ * users[0] instanceof User; // true
752
+ */
753
+ async execute(ctx: OrmSession): Promise<T[]> {
754
+ this.validateNoParamOperands();
755
+ if (this.entityConstructor) {
756
+ return this.executeAs(this.entityConstructor, ctx) as unknown as T[];
757
+ }
758
+ const builder = this.ensureDefaultSelection();
759
+ return executeHydrated(ctx, builder) as unknown as T[];
760
+ }
764
761
 
765
- /**
766
- * Executes the query and returns results as real class instances.
767
- * Unlike execute(), this returns actual instances of the decorated entity class
768
- * with working methods and proper instanceof checks.
769
- * @param entityClass - The entity class constructor
770
- * @param ctx - ORM session context
771
- * @returns Promise of entity class instances
772
- * @example
773
- * const users = await selectFromEntity(User)
774
- * .include('posts')
775
- * .executeAs(User, session);
776
- * users[0] instanceof User; // true!
777
- * users[0].getFullName(); // works!
778
- */
779
- async executeAs<TEntity extends object>(
780
- entityClass: EntityConstructor<TEntity>,
781
- ctx: OrmSession
782
- ): Promise<TEntity[]> {
783
- const builder = this.ensureDefaultSelection();
784
- const results = await executeHydrated(ctx, builder);
785
- return materializeAs(entityClass, results as unknown as Record<string, unknown>[]);
786
- }
762
+ /**
763
+ * Executes the query and returns plain row objects (POJOs), ignoring any entity materialization.
764
+ * Use this if you want raw data even when using selectFromEntity.
765
+ *
766
+ * @param ctx - ORM session context
767
+ * @returns Promise of plain entity instances
768
+ * @example
769
+ * const rows = await selectFromEntity(User).executePlain(session);
770
+ * // rows is EntityInstance<UserTable>[] (plain objects)
771
+ * rows[0] instanceof User; // false
772
+ */
773
+ async executePlain(ctx: OrmSession): Promise<EntityInstance<TTable>[]> {
774
+ this.validateNoParamOperands();
775
+ const builder = this.ensureDefaultSelection();
776
+ const rows = await executeHydratedPlain(ctx, builder);
777
+ return rows as EntityInstance<TTable>[];
778
+ }
787
779
 
788
- /**
789
- * Executes a count query for the current builder without LIMIT/OFFSET clauses.
790
- *
791
- * @example
792
- * const total = await qb.count(session);
793
- */
794
- async count(session: OrmSession): Promise<number> {
795
- return executeCount(this.context, this.env, session);
796
- }
780
+ /**
781
+ * Executes the query and returns results as real class instances.
782
+ * Unlike execute(), this returns actual instances of the decorated entity class
783
+ * with working methods and proper instanceof checks.
784
+ * @param entityClass - The entity class constructor
785
+ * @param ctx - ORM session context
786
+ * @returns Promise of entity class instances
787
+ * @example
788
+ * const users = await selectFromEntity(User)
789
+ * .include('posts')
790
+ * .executeAs(User, session);
791
+ * users[0] instanceof User; // true!
792
+ * users[0].getFullName(); // works!
793
+ */
794
+ async executeAs<TEntity extends object>(
795
+ entityClass: EntityConstructor<TEntity>,
796
+ ctx: OrmSession
797
+ ): Promise<TEntity[]> {
798
+ this.validateNoParamOperands();
799
+ const builder = this.ensureDefaultSelection();
800
+ const results = await executeHydrated(ctx, builder);
801
+ return materializeAs(entityClass, results as unknown as Record<string, unknown>[]);
802
+ }
797
803
 
798
- /**
799
- * Executes the query and returns both the paged items and the total.
800
- *
801
- * @example
802
- * const { items, totalItems, page, pageSize } = await qb.executePaged(session, { page: 1, pageSize: 20 });
803
- */
804
- async executePaged(
805
- session: OrmSession,
806
- options: { page: number; pageSize: number }
807
- ): Promise<PaginatedResult<T>> {
808
- const builder = this.ensureDefaultSelection();
809
- return executePagedQuery(builder, session, options, sess => builder.count(sess));
810
- }
804
+ /**
805
+ * Executes a count query for the current builder without LIMIT/OFFSET clauses.
806
+ *
807
+ * @example
808
+ * const total = await qb.count(session);
809
+ */
810
+ async count(session: OrmSession): Promise<number> {
811
+ this.validateNoParamOperands();
812
+ return executeCount(this.context, this.env, session);
813
+ }
814
+
815
+ /**
816
+ * Executes the query and returns both the paged items and the total.
817
+ *
818
+ * @example
819
+ * const { items, totalItems, page, pageSize } = await qb.executePaged(session, { page: 1, pageSize: 20 });
820
+ */
821
+ async executePaged(
822
+ session: OrmSession,
823
+ options: { page: number; pageSize: number }
824
+ ): Promise<PaginatedResult<T>> {
825
+ this.validateNoParamOperands();
826
+ const builder = this.ensureDefaultSelection();
827
+ return executePagedQuery(builder, session, options, sess => builder.count(sess));
828
+ }
811
829
 
812
- /**
813
- * Executes the query with provided execution and hydration contexts
814
- * @param execCtx - Execution context
815
- * @param hydCtx - Hydration context
816
- * @returns Promise of entity instances
817
- * @example
818
- * const execCtx = new ExecutionContext(session);
819
- * const hydCtx = new HydrationContext();
820
- * const users = await qb.executeWithContexts(execCtx, hydCtx);
821
- */
822
- async executeWithContexts(execCtx: ExecutionContext, hydCtx: HydrationContext): Promise<T[]> {
823
- const builder = this.ensureDefaultSelection();
824
- const results = await executeHydratedWithContexts(execCtx, hydCtx, builder);
825
- if (this.entityConstructor) {
826
- return materializeAs(this.entityConstructor, results as unknown as Record<string, unknown>[]) as unknown as T[];
827
- }
828
- return results as unknown as T[];
829
- }
830
+ /**
831
+ * Executes the query with provided execution and hydration contexts
832
+ * @param execCtx - Execution context
833
+ * @param hydCtx - Hydration context
834
+ * @returns Promise of entity instances
835
+ * @example
836
+ * const execCtx = new ExecutionContext(session);
837
+ * const hydCtx = new HydrationContext();
838
+ * const users = await qb.executeWithContexts(execCtx, hydCtx);
839
+ */
840
+ async executeWithContexts(execCtx: ExecutionContext, hydCtx: HydrationContext): Promise<T[]> {
841
+ this.validateNoParamOperands();
842
+ const builder = this.ensureDefaultSelection();
843
+ const results = await executeHydratedWithContexts(execCtx, hydCtx, builder);
844
+ if (this.entityConstructor) {
845
+ return materializeAs(this.entityConstructor, results as unknown as Record<string, unknown>[]) as unknown as T[];
846
+ }
847
+ return results as unknown as T[];
848
+ }
830
849
 
831
850
  /**
832
851
  * Adds a WHERE condition to the query