metal-orm 1.1.19 → 1.1.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.1.19",
3
+ "version": "1.1.21",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -73,6 +73,15 @@ export class MSSqlSchemaDialect extends BaseSchemaDialect {
73
73
  return 'VARBINARY(MAX)';
74
74
  case 'enum':
75
75
  return 'NVARCHAR(255)';
76
+ case 'vector': {
77
+ const dim = column.vectorOptions?.dimensions ?? column.args?.[0] ?? 3;
78
+ const elemType = column.vectorOptions?.elementType;
79
+ return elemType ? `VECTOR(${dim}, ${elemType})` : `VECTOR(${dim})`;
80
+ }
81
+ case 'halfvec': {
82
+ const dim = column.vectorOptions?.dimensions ?? column.args?.[0] ?? 3;
83
+ return `VECTOR(${dim}, float16)`;
84
+ }
76
85
  default:
77
86
  return renderTypeWithArgs(String(type).toUpperCase(), column.args);
78
87
  }
@@ -71,6 +71,11 @@ export class MySqlSchemaDialect extends BaseSchemaDialect {
71
71
  return column.args && Array.isArray(column.args) && column.args.length
72
72
  ? `ENUM(${column.args.map((v: string) => `'${escapeSqlString(v)}'`).join(',')})`
73
73
  : 'ENUM';
74
+ case 'vector':
75
+ case 'halfvec': {
76
+ const dim = column.vectorOptions?.dimensions ?? column.args?.[0] ?? 3;
77
+ return `VECTOR(${dim})`;
78
+ }
74
79
  default:
75
80
  return renderTypeWithArgs(String(type).toUpperCase(), column.args);
76
81
  }
@@ -66,6 +66,14 @@ export class PostgresSchemaDialect extends BaseSchemaDialect {
66
66
  case 'blob':
67
67
  case 'bytea':
68
68
  return 'bytea';
69
+ case 'vector':
70
+ return column.vectorOptions?.elementType === 'float16'
71
+ ? `halfvec(${column.vectorOptions.dimensions})`
72
+ : column.args?.length
73
+ ? `vector(${column.args[0]})`
74
+ : 'vector';
75
+ case 'halfvec':
76
+ return column.args?.length ? `halfvec(${column.args[0]})` : 'halfvec';
69
77
  default:
70
78
  return renderTypeWithArgs(String(type).toLowerCase(), column.args);
71
79
  }
@@ -79,10 +87,23 @@ export class PostgresSchemaDialect extends BaseSchemaDialect {
79
87
 
80
88
  renderIndex(table: TableDef, index: IndexDef): string {
81
89
  const name = index.name || deriveIndexName(table, index);
82
- const cols = renderIndexColumns(this, index.columns);
90
+ let cols = renderIndexColumns(this, index.columns);
91
+ if (index.ops) {
92
+ cols = `${cols} ${index.ops}`;
93
+ }
83
94
  const unique = index.unique ? 'UNIQUE ' : '';
95
+ const using = index.using ? ` USING ${index.using}` : '';
96
+ let withClause = '';
97
+ if (index.with) {
98
+ if (typeof index.with === 'string') {
99
+ withClause = ` WITH (${index.with})`;
100
+ } else {
101
+ const params = Object.entries(index.with).map(([k, v]) => `${k} = ${v}`).join(', ');
102
+ withClause = ` WITH (${params})`;
103
+ }
104
+ }
84
105
  const where = index.where ? ` WHERE ${index.where}` : '';
85
- return `CREATE ${unique}INDEX IF NOT EXISTS ${this.quoteIdentifier(name)} ON ${this.formatTableName(table)} (${cols})${where};`;
106
+ return `CREATE ${unique}INDEX IF NOT EXISTS ${this.quoteIdentifier(name)} ON ${this.formatTableName(table)}${using} (${cols})${withClause}${where};`;
86
107
  }
87
108
 
88
109
  supportsPartialIndexes(): boolean {
@@ -57,6 +57,15 @@ export class SQLiteSchemaDialect extends BaseSchemaDialect {
57
57
  case 'blob':
58
58
  case 'bytea':
59
59
  return 'BLOB';
60
+ case 'halfvec': {
61
+ const dim = column.vectorOptions?.dimensions ?? column.args?.[0] ?? 3;
62
+ return `float16[${dim}]`;
63
+ }
64
+ case 'vector': {
65
+ const dim = column.vectorOptions?.dimensions ?? column.args?.[0] ?? 3;
66
+ const elemType = column.vectorOptions?.elementType === 'float16' ? 'float16' : 'float32';
67
+ return `${elemType}[${dim}]`;
68
+ }
60
69
  default:
61
70
  return 'TEXT';
62
71
  }
@@ -143,5 +143,15 @@ export class MssqlFunctionStrategy extends StandardFunctionStrategy {
143
143
  this.add('ARRAY_APPEND', () => {
144
144
  throw new Error('ARRAY_APPEND is not supported on SQL Server');
145
145
  });
146
+
147
+ this.add('VECTOR_DISTANCE', ({ node, compiledArgs }) => {
148
+ if (compiledArgs.length !== 3) throw new Error('VECTOR_DISTANCE expects 3 arguments (metric, v1, v2)');
149
+ let metric = node.args[0]?.type === 'Literal' ? String(node.args[0].value).toLowerCase() : compiledArgs[0].replace(/['"]/g, '').toLowerCase();
150
+ if (metric === 'l2') metric = 'euclidean';
151
+ if (metric === 'l1') metric = 'manhattan';
152
+ if (metric === 'inner_product') metric = 'dot';
153
+ const [, v1, v2] = compiledArgs;
154
+ return `VECTOR_DISTANCE('${metric}', ${v1}, ${v2})`;
155
+ });
146
156
  }
147
157
  }
@@ -110,5 +110,14 @@ export class MysqlFunctionStrategy extends StandardFunctionStrategy {
110
110
  if (compiledArgs.length !== 2) throw new Error('ARRAY_APPEND expects 2 arguments (array, value)');
111
111
  return `JSON_ARRAY_APPEND(${compiledArgs[0]}, '$', ${compiledArgs[1]})`;
112
112
  });
113
+
114
+ this.add('VECTOR_DISTANCE', ({ node, compiledArgs }) => {
115
+ if (compiledArgs.length !== 3) throw new Error('VECTOR_DISTANCE expects 3 arguments (metric, v1, v2)');
116
+ let metric = node.args[0]?.type === 'Literal' ? String(node.args[0].value).toUpperCase() : compiledArgs[0].replace(/['"]/g, '').toUpperCase();
117
+ if (metric === 'L2') metric = 'EUCLIDEAN';
118
+ if (metric === 'INNER_PRODUCT') metric = 'DOT';
119
+ const [, v1, v2] = compiledArgs;
120
+ return `DISTANCE(${v1}, ${v2}, '${metric}')`;
121
+ });
113
122
  }
114
123
  }
@@ -140,6 +140,27 @@ export class PostgresFunctionStrategy extends StandardFunctionStrategy {
140
140
  const pathArray = this.formatJsonbPathArray(pathNode);
141
141
  return `jsonb_set(${compiledArgs[0]}, ${pathArray}, ${compiledArgs[2]}::jsonb, true)`;
142
142
  });
143
+
144
+ this.add('VECTOR_DISTANCE', ({ node, compiledArgs }) => {
145
+ if (compiledArgs.length !== 3) throw new Error('VECTOR_DISTANCE expects 3 arguments (metric, v1, v2)');
146
+ const metric = node.args[0]?.type === 'Literal' ? String(node.args[0].value).toLowerCase() : compiledArgs[0].replace(/['"]/g, '').toLowerCase();
147
+ const [, v1, v2] = compiledArgs;
148
+ switch (metric) {
149
+ case 'cosine':
150
+ return `(${v1} <=> ${v2})`;
151
+ case 'euclidean':
152
+ case 'l2':
153
+ return `(${v1} <-> ${v2})`;
154
+ case 'dot':
155
+ case 'inner_product':
156
+ return `(${v1} <#> ${v2})`;
157
+ case 'manhattan':
158
+ case 'l1':
159
+ return `(${v1} <~> ${v2})`;
160
+ default:
161
+ return `(${v1} <=> ${v2})`;
162
+ }
163
+ });
143
164
  }
144
165
 
145
166
  private formatJsonbPathArray(pathNode: LiteralNode): string {
@@ -151,5 +151,15 @@ export class SqliteFunctionStrategy extends StandardFunctionStrategy {
151
151
  });
152
152
 
153
153
  this.add('CHR', ({ compiledArgs }) => `CHAR(${compiledArgs[0]})`);
154
+
155
+ this.add('VECTOR_DISTANCE', ({ node, compiledArgs }) => {
156
+ if (compiledArgs.length !== 3) throw new Error('VECTOR_DISTANCE expects 3 arguments (metric, v1, v2)');
157
+ const metric = node.args[0]?.type === 'Literal' ? String(node.args[0].value).toLowerCase() : compiledArgs[0].replace(/['"]/g, '').toLowerCase();
158
+ const [, v1, v2] = compiledArgs;
159
+ if (metric === 'euclidean' || metric === 'l2') {
160
+ return `vec_distance_L2(${v1}, ${v2})`;
161
+ }
162
+ return `vec_distance_cosine(${v1}, ${v2})`;
163
+ });
154
164
  }
155
165
  }
@@ -0,0 +1,141 @@
1
+ // Pure AST Builders - No Dialect Logic Here!
2
+
3
+ import { ColumnDef } from '../../schema/column-types.js';
4
+ import { columnOperand, valueToOperand } from '../ast/expression-builders.js';
5
+ import { FunctionNode, OperandNode, isOperandNode, TypedExpression, asType } from '../ast/expression.js';
6
+ import { BinaryExpressionNode, LogicalExpressionNode } from '../ast/expression-nodes.js';
7
+ import { SqlOperator } from '../sql/sql.js';
8
+
9
+ export type VectorMetric =
10
+ | 'cosine'
11
+ | 'euclidean'
12
+ | 'l2'
13
+ | 'dot'
14
+ | 'inner_product'
15
+ | 'manhattan'
16
+ | 'l1';
17
+
18
+ export type VectorInput =
19
+ | OperandNode
20
+ | ColumnDef
21
+ | number[]
22
+ | Float32Array
23
+ | string;
24
+
25
+ const isColumnDef = (val: unknown): val is ColumnDef =>
26
+ !!val && typeof val === 'object' && 'type' in val && 'name' in val;
27
+
28
+ const toOperand = (input: VectorInput): OperandNode => {
29
+ if (isOperandNode(input)) return input;
30
+ if (isColumnDef(input)) return columnOperand(input);
31
+
32
+ if (Array.isArray(input) || input instanceof Float32Array) {
33
+ const formatted = `[${Array.from(input).join(', ')}]`;
34
+ return valueToOperand(formatted);
35
+ }
36
+
37
+ return valueToOperand(input);
38
+ };
39
+
40
+ const fn = (key: string, args: OperandNode[]): FunctionNode => ({
41
+ type: 'Function',
42
+ name: key,
43
+ fn: key,
44
+ args
45
+ });
46
+
47
+ /**
48
+ * Calculates vector distance using a specific distance metric ('cosine', 'euclidean', 'l2', 'dot', 'inner_product', 'manhattan', 'l1').
49
+ * Compiles to dialect-native vector functions / operators:
50
+ * - SQL Server: VECTOR_DISTANCE('cosine', v1, v2)
51
+ * - MySQL: DISTANCE(v1, v2, 'COSINE')
52
+ * - PostgreSQL: (v1 <=> v2), (v1 <-> v2), (v1 <#> v2), (v1 <~> v2)
53
+ * - SQLite: vec_distance_cosine(v1, v2), vec_distance_L2(v1, v2)
54
+ */
55
+ export const vectorDistance = (
56
+ metric: VectorMetric,
57
+ v1: VectorInput,
58
+ v2: VectorInput
59
+ ): TypedExpression<number> => {
60
+ const metricOp = valueToOperand(metric.toLowerCase());
61
+ return asType<number>(fn('VECTOR_DISTANCE', [metricOp, toOperand(v1), toOperand(v2)]));
62
+ };
63
+
64
+ /**
65
+ * Calculates Cosine distance between two vectors.
66
+ */
67
+ export const cosineDistance = (v1: VectorInput, v2: VectorInput): TypedExpression<number> =>
68
+ vectorDistance('cosine', v1, v2);
69
+
70
+ /**
71
+ * Calculates Euclidean (L2) distance between two vectors.
72
+ */
73
+ export const l2Distance = (v1: VectorInput, v2: VectorInput): TypedExpression<number> =>
74
+ vectorDistance('euclidean', v1, v2);
75
+
76
+ /**
77
+ * Alias for l2Distance. Calculates Euclidean distance between two vectors.
78
+ */
79
+ export const euclideanDistance = (v1: VectorInput, v2: VectorInput): TypedExpression<number> =>
80
+ vectorDistance('euclidean', v1, v2);
81
+
82
+ /**
83
+ * Calculates Inner / Dot product distance between two vectors.
84
+ */
85
+ export const innerProduct = (v1: VectorInput, v2: VectorInput): TypedExpression<number> =>
86
+ vectorDistance('dot', v1, v2);
87
+
88
+ /**
89
+ * Alias for innerProduct. Calculates Dot product distance between two vectors.
90
+ */
91
+ export const dotProduct = (v1: VectorInput, v2: VectorInput): TypedExpression<number> =>
92
+ vectorDistance('dot', v1, v2);
93
+
94
+ /**
95
+ * Calculates Manhattan (L1) distance between two vectors.
96
+ */
97
+ export const l1Distance = (v1: VectorInput, v2: VectorInput): TypedExpression<number> =>
98
+ vectorDistance('manhattan', v1, v2);
99
+
100
+ /**
101
+ * Alias for l1Distance. Calculates Manhattan distance between two vectors.
102
+ */
103
+ export const manhattanDistance = (v1: VectorInput, v2: VectorInput): TypedExpression<number> =>
104
+ vectorDistance('manhattan', v1, v2);
105
+
106
+ /**
107
+ * Builds a SQLite `sqlite-vec` KNN virtual table query predicate:
108
+ * `col MATCH '[...]' AND k = n`
109
+ *
110
+ * @param column - The vector virtual table column to match against.
111
+ * @param vector - The query vector(s) to match (raw array/string is inlined as a literal).
112
+ * @param k - Number of nearest neighbors to return.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * where(vectorMatch(items.embedding, [0.1, 2, 30], 5))
117
+ * // => "embedding" MATCH '[0.1, 2, 30]' AND k = 5
118
+ * ```
119
+ */
120
+ export const vectorMatch = (
121
+ column: VectorInput,
122
+ vector: VectorInput,
123
+ k: number
124
+ ): LogicalExpressionNode => {
125
+ const match: BinaryExpressionNode = {
126
+ type: 'BinaryExpression',
127
+ left: toOperand(column),
128
+ operator: 'MATCH' as SqlOperator,
129
+ right: toOperand(vector)
130
+ };
131
+
132
+ const kNode: BinaryExpressionNode = {
133
+ type: 'BinaryExpression',
134
+ left: valueToOperand('k'),
135
+ operator: '=',
136
+ right: valueToOperand(k)
137
+ };
138
+
139
+ return { type: 'LogicalExpression', operator: 'AND', operands: [match, kNode] };
140
+ };
141
+
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ export * from './core/functions/datetime.js';
32
32
  export * from './core/functions/control-flow.js';
33
33
  export * from './core/functions/json.js';
34
34
  export * from './core/functions/array.js';
35
+ export * from './core/functions/vector.js';
35
36
  export * from './orm/als.js';
36
37
  export * from './orm/hydration.js';
37
38
  export * from './codegen/typescript.js';
@@ -7,7 +7,6 @@ import { UpdateQueryBuilder } from '../query-builder/update.js';
7
7
  import { findPrimaryKey } from '../query-builder/hydration-planner.js';
8
8
  import type { BelongsToManyRelation, HasManyRelation, HasOneRelation, MorphOneRelation, MorphManyRelation, MorphToRelation } from '../schema/relation.js';
9
9
  import { RelationKinds } from '../schema/relation.js';
10
- import type { TableDef } from '../schema/table.js';
11
10
  import type { DbExecutor } from '../core/execution/db-executor.js';
12
11
  import type { RelationChangeEntry } from './runtime-types.js';
13
12
  import { UnitOfWork } from './unit-of-work.js';
@@ -153,7 +152,10 @@ export class RelationChangeProcessor {
153
152
  const rootId = entry.root[rootKey];
154
153
  if (rootId === undefined || rootId === null) return;
155
154
 
156
- const targetId = this.resolvePrimaryKeyValue(entry.change.entity as Record<string, unknown>, relation.target);
155
+ const targetId = this.resolveBelongsToManyTargetValue(
156
+ entry.change.entity as Record<string, unknown>,
157
+ relation
158
+ );
157
159
  if (targetId === null) return;
158
160
 
159
161
  const pivotPayload = this.buildPivotPayload(relation, entry.change.pivot);
@@ -235,7 +237,7 @@ export class RelationChangeProcessor {
235
237
  * Inserts a pivot row for belongs-to-many relations.
236
238
  * @param relation - The belongs-to-many relation
237
239
  * @param rootId - The root entity's primary key value
238
- * @param targetId - The target entity's primary key value
240
+ * @param targetId - The target entity's relation key value
239
241
  */
240
242
  private async insertPivotRow(
241
243
  relation: BelongsToManyRelation,
@@ -258,7 +260,7 @@ export class RelationChangeProcessor {
258
260
  * Updates a pivot row for belongs-to-many relations.
259
261
  * @param relation - The belongs-to-many relation
260
262
  * @param rootId - The root entity's primary key value
261
- * @param targetId - The target entity's primary key value
263
+ * @param targetId - The target entity's relation key value
262
264
  * @param pivotPayload - The pivot columns to update
263
265
  */
264
266
  private async updatePivotRow(
@@ -282,7 +284,7 @@ export class RelationChangeProcessor {
282
284
  * Deletes a pivot row for belongs-to-many relations.
283
285
  * @param relation - The belongs-to-many relation
284
286
  * @param rootId - The root entity's primary key value
285
- * @param targetId - The target entity's primary key value
287
+ * @param targetId - The target entity's relation key value
286
288
  */
287
289
  private async deletePivotRow(relation: BelongsToManyRelation, rootId: string | number, targetId: string | number): Promise<void> {
288
290
  const rootCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
@@ -297,14 +299,16 @@ export class RelationChangeProcessor {
297
299
  }
298
300
 
299
301
  /**
300
- * Resolves the primary key value from an entity.
301
- * @param entity - The entity
302
- * @param table - The table definition
303
- * @returns The primary key value or null
302
+ * Resolves the target-side key value used by a belongs-to-many relation.
303
+ * The declared targetKey is part of the relation contract and takes
304
+ * precedence over the target table primary key.
304
305
  */
305
- private resolvePrimaryKeyValue(entity: Record<string, unknown>, table: TableDef): string | number | null {
306
+ private resolveBelongsToManyTargetValue(
307
+ entity: Record<string, unknown>,
308
+ relation: BelongsToManyRelation
309
+ ): string | number | null {
306
310
  if (!entity) return null;
307
- const key = findPrimaryKey(table);
311
+ const key = relation.targetKey || findPrimaryKey(relation.target);
308
312
  const value = entity[key];
309
313
  if (value === undefined || value === null) return null;
310
314
  return (value as string | number | null | undefined) ?? null;
@@ -21,7 +21,9 @@ export const STANDARD_COLUMN_TYPES = [
21
21
  'DATETIME',
22
22
  'TIMESTAMP',
23
23
  'TIMESTAMPTZ',
24
- 'BOOLEAN'
24
+ 'BOOLEAN',
25
+ 'VECTOR',
26
+ 'HALFVEC'
25
27
  ] as const;
26
28
 
27
29
  /** Known logical types the ORM understands. */
@@ -113,6 +115,11 @@ export interface ColumnDef<T extends ColumnType = ColumnType, TRuntime = unknown
113
115
  comment?: string;
114
116
  /** Additional arguments for the column type (e.g., VARCHAR length) */
115
117
  args?: (string | number)[];
118
+ /** Options specific to vector columns (dimensions, float16 vs float32, etc.) */
119
+ vectorOptions?: {
120
+ dimensions: number;
121
+ elementType?: 'float16' | 'float32' | 'int8' | 'bit';
122
+ };
116
123
  /** Table name this column belongs to (filled at runtime by defineTable) */
117
124
  table?: string;
118
125
  }
@@ -236,6 +243,32 @@ export const col = {
236
243
  */
237
244
  enum: (values: string[]): ColumnDef<'ENUM'> => ({ name: '', type: 'ENUM', args: values }),
238
245
 
246
+ /**
247
+ * Creates a vector column definition
248
+ * @param dimensions - Vector dimensions
249
+ * @param options - Vector options (e.g. elementType: 'float16' | 'float32')
250
+ */
251
+ vector: (
252
+ dimensions: number,
253
+ options?: { elementType?: 'float16' | 'float32' | 'int8' | 'bit' }
254
+ ): ColumnDef<'VECTOR', number[]> => ({
255
+ name: '',
256
+ type: 'VECTOR',
257
+ args: [dimensions],
258
+ vectorOptions: { dimensions, ...options }
259
+ }),
260
+
261
+ /**
262
+ * Creates a half-precision (float16) vector column definition (pgvector halfvec / SQL Server float16 vector / sqlite-vec float16)
263
+ * @param dimensions - Vector dimensions
264
+ */
265
+ halfvec: (dimensions: number): ColumnDef<'HALFVEC', number[]> => ({
266
+ name: '',
267
+ type: 'HALFVEC',
268
+ args: [dimensions],
269
+ vectorOptions: { dimensions, elementType: 'float16' }
270
+ }),
271
+
239
272
  /**
240
273
  * Creates a column definition with a custom SQL type.
241
274
  * Useful for dialect-specific types without polluting the standard set.
@@ -12,6 +12,12 @@ export interface IndexDef {
12
12
  columns: (string | IndexColumn)[];
13
13
  unique?: boolean;
14
14
  where?: string;
15
+ /** Index method / access method, e.g. 'hnsw', 'ivfflat', 'btree' */
16
+ using?: string;
17
+ /** Operator class for vector distance, e.g. 'vector_cosine_ops', 'vector_l2_ops', 'halfvec_cosine_ops' */
18
+ ops?: string;
19
+ /** Index parameters / WITH clause options, e.g. { m: 16, ef_construction: 64 } */
20
+ with?: Record<string, unknown> | string;
15
21
  }
16
22
 
17
23
  export interface CheckConstraint {