From 6d8768c33c21413349011f1594df9d73e32d3a1b Mon Sep 17 00:00:00 2001 From: prajalg Date: Fri, 20 Mar 2026 15:30:02 +0530 Subject: [PATCH 01/49] initial commit : added changes from v6 vector support MR --- .../core/src/abstract-dialect/data-types.ts | 96 ++++++++++++ packages/core/src/abstract-dialect/dialect.ts | 2 + .../src/abstract-dialect/query-interface.d.ts | 25 ++- packages/core/src/data-types.ts | 2 + packages/core/src/sequelize.d.ts | 5 + packages/core/src/sequelize.js | 40 +++++ .../dialects/oracle/vector.test.js | 77 ++++++++++ .../test/unit/data-types/string-types.test.ts | 44 ++++++ .../test/unit/dialects/oracle/vector.test.js | 143 ++++++++++++++++++ .../src/_internal/data-types-overrides.ts | 26 ++++ packages/oracle/src/dialect.ts | 1 + .../oracle/src/query-generator.internal.ts | 51 +++++++ packages/oracle/src/query-generator.js | 99 +++++++++++- 13 files changed, 607 insertions(+), 4 deletions(-) create mode 100644 packages/core/test/integration/dialects/oracle/vector.test.js create mode 100644 packages/core/test/unit/dialects/oracle/vector.test.js diff --git a/packages/core/src/abstract-dialect/data-types.ts b/packages/core/src/abstract-dialect/data-types.ts index 26975feafa37..35a1d2aec8ea 100644 --- a/packages/core/src/abstract-dialect/data-types.ts +++ b/packages/core/src/abstract-dialect/data-types.ts @@ -2844,6 +2844,102 @@ export class TSVECTOR extends AbstractDataType { } } +export interface VectorOptions { + dimension?: number; + format?: string; +} + +export type Vector = number[] | Float32Array | Float64Array | Int8Array | Uint8Array; + +/** + * The VECTOR type stores vectors. + * + * Only available for Oracle + * + * __Fallback policy:__ + * If this type is not supported, an error will be raised. + * + * @example + * ```ts + * DataTypes.VECTOR + * DataTypes.VECTOR(1536) + * DataTypes.VECTOR(1536, 'float32') + * ``` + * + * @category DataTypes + */ +export class VECTOR extends AbstractDataType { + /** @hidden */ + static readonly [DataTypeIdentifier]: string = 'VECTOR'; + + readonly options: VectorOptions; + + constructor(); + constructor(dimension: number, format?: string); + constructor(options: VectorOptions); + // we have to define the constructor overloads using tuples due to a TypeScript limitation + // https://github.com/microsoft/TypeScript/issues/29732, to play nice with classToInvokable. + /** @hidden */ + constructor( + ...args: + | [] + | [dimension: number] + | [dimension: number, format: string] + | [options: VectorOptions] + ); + constructor(dimensionOrOptions?: number | VectorOptions, format?: string) { + super(); + + if (typeof dimensionOrOptions === 'object') { + this.options = { + ...(dimensionOrOptions.dimension !== undefined + ? { dimension: dimensionOrOptions.dimension } + : {}), + ...(dimensionOrOptions.format !== undefined ? { format: dimensionOrOptions.format } : {}), + }; + + return; + } + + this.options = { + ...(dimensionOrOptions !== undefined ? { dimension: dimensionOrOptions } : {}), + ...(format !== undefined ? { format } : {}), + }; + } + + validate(value: unknown): asserts value is Vector { + if (Array.isArray(value)) { + for (const item of value) { + if (typeof item !== 'number') { + ValidationErrorItem.throwDataTypeValidationError( + util.format('%O is not a valid vector', value), + ); + } + } + + return; + } + + if (ArrayBuffer.isView(value) && !(value instanceof DataView)) { + return; + } + + ValidationErrorItem.throwDataTypeValidationError( + util.format('%O is not a valid vector', value), + ); + } + + protected _checkOptionSupport(dialect: AbstractDialect) { + if (!dialect.supports.dataTypes.VECTOR) { + throwUnsupportedDataType(dialect, 'VECTOR'); + } + } + + toSql(): string { + return 'VECTOR'; + } +} + function rejectBlobs(value: unknown) { // We have a DataType called BLOB. People might try to use the built-in Blob type with it, which they cannot. // To clarify why it doesn't work, we have a dedicated message for it. diff --git a/packages/core/src/abstract-dialect/dialect.ts b/packages/core/src/abstract-dialect/dialect.ts index 6d96ff32f3c8..516050f550d5 100644 --- a/packages/core/src/abstract-dialect/dialect.ts +++ b/packages/core/src/abstract-dialect/dialect.ts @@ -196,6 +196,7 @@ export type DialectSupports = { GEOGRAPHY: boolean; HSTORE: boolean; TSVECTOR: boolean; + VECTOR: boolean; CIDR: boolean; INET: boolean; MACADDR: boolean; @@ -443,6 +444,7 @@ export abstract class AbstractDialect< GEOGRAPHY: false, HSTORE: false, TSVECTOR: false, + VECTOR: false, DATETIME: { infinity: false, }, diff --git a/packages/core/src/abstract-dialect/query-interface.d.ts b/packages/core/src/abstract-dialect/query-interface.d.ts index c81c6e420119..a655696b3120 100644 --- a/packages/core/src/abstract-dialect/query-interface.d.ts +++ b/packages/core/src/abstract-dialect/query-interface.d.ts @@ -76,7 +76,7 @@ export interface TableNameWithSchema { export type TableName = string | TableNameWithSchema; -export type IndexType = AllowLowercase<'UNIQUE' | 'FULLTEXT' | 'SPATIAL'>; +export type IndexType = AllowLowercase<'UNIQUE' | 'FULLTEXT' | 'SPATIAL' | 'VECTOR'>; export type IndexMethod = 'BTREE' | 'HASH' | 'GIST' | 'SPGIST' | 'GIN' | 'BRIN' | string; export interface IndexField { @@ -116,7 +116,7 @@ export interface IndexOptions { parser?: string | null; /** - * Index type. Only used by mysql. One of `UNIQUE`, `FULLTEXT` and `SPATIAL` + * Index type. One of `UNIQUE`, `FULLTEXT`, `SPATIAL`, and `VECTOR` (Oracle only). */ type?: IndexType | undefined; @@ -167,6 +167,27 @@ export interface IndexOptions { */ prefix?: string; + /** + * Oracle vector index distance metric. + */ + distance?: string; + + /** + * Oracle vector index target accuracy. + */ + accuracy?: number; + + /** + * Oracle vector index parameters. + */ + parameter?: { + neighbor?: number; + efconstruction?: number; + partitions?: number; + samplesPerPartition?: number; + minVectors?: number; + }; + /** * Non-key columns to be added to the lead level of the nonclustered index. */ diff --git a/packages/core/src/data-types.ts b/packages/core/src/data-types.ts index 89b9f22f4754..62fa694c8461 100644 --- a/packages/core/src/data-types.ts +++ b/packages/core/src/data-types.ts @@ -88,3 +88,5 @@ export const MACADDR8 = classToInvokable(DataTypes.MACADDR8); export const CITEXT = classToInvokable(DataTypes.CITEXT); /** This is a simple wrapper to make the DataType constructable without `new`. See the return type for all available options. */ export const TSVECTOR = classToInvokable(DataTypes.TSVECTOR); +/** This is a simple wrapper to make the DataType constructable without `new`. See the return type for all available options. */ +export const VECTOR = classToInvokable(DataTypes.VECTOR); diff --git a/packages/core/src/sequelize.d.ts b/packages/core/src/sequelize.d.ts index 3737a2b35e1b..57987791d205 100644 --- a/packages/core/src/sequelize.d.ts +++ b/packages/core/src/sequelize.d.ts @@ -613,6 +613,11 @@ export class Sequelize< * Get the fn for random based on the dialect */ random(): Fn; + cosineDistance(column: string, value: number[] | ArrayBufferView): Fn; + innerProduct(column: string, value: number[] | ArrayBufferView): Fn; + l1Distance(column: string, value: number[] | ArrayBufferView): Fn; + l2Distance(column: string, value: number[] | ArrayBufferView): Fn; + vectorDistance(column: string, value: number[] | ArrayBufferView): Fn; /** * Execute a query which would set an environment or user variable. The variables are set per connection, diff --git a/packages/core/src/sequelize.js b/packages/core/src/sequelize.js index 47c4cb06da2e..ba3aadde6433 100644 --- a/packages/core/src/sequelize.js +++ b/packages/core/src/sequelize.js @@ -619,6 +619,46 @@ Use Sequelize#query if you wish to use replacements.`); return fn('RAND'); } + cosineDistance(column, value) { + if (this.dialect.name === 'oracle') { + return fn('COSINE_DISTANCE', column, value); + } + + throw new Error(`cosineDistance for dialect "${this.dialect.name}" is not implemented`); + } + + innerProduct(column, value) { + if (this.dialect.name === 'oracle') { + return fn('INNER_PRODUCT', column, value); + } + + throw new Error(`innerProduct for dialect "${this.dialect.name}" is not implemented`); + } + + l1Distance(column, value) { + if (this.dialect.name === 'oracle') { + return fn('L1_DISTANCE', column, value); + } + + throw new Error(`l1Distance for dialect "${this.dialect.name}" is not implemented`); + } + + l2Distance(column, value) { + if (this.dialect.name === 'oracle') { + return fn('L2_DISTANCE', column, value); + } + + throw new Error(`l2Distance for dialect "${this.dialect.name}" is not implemented`); + } + + vectorDistance(column, value) { + if (this.dialect.name === 'oracle') { + return fn('VECTOR_DISTANCE', column, value); + } + + throw new Error(`vectorDistance for dialect "${this.dialect.name}" is not implemented`); + } + // Global exports static Fn = Fn; static Col = Col; diff --git a/packages/core/test/integration/dialects/oracle/vector.test.js b/packages/core/test/integration/dialects/oracle/vector.test.js new file mode 100644 index 000000000000..b39f3719ccef --- /dev/null +++ b/packages/core/test/integration/dialects/oracle/vector.test.js @@ -0,0 +1,77 @@ +'use strict'; + +const { DataTypes, Op, sql } = require('@sequelize/core'); +const { expect } = require('chai'); +const semver = require('semver'); +const { getTestDialect, sequelize } = require('../../../support'); + +if (getTestDialect() === 'oracle') { + describe('[Oracle Specific] vectors', () => { + before(async function () { + const rawVersion = await sequelize.fetchDatabaseVersion(); + const normalized = semver.coerce(rawVersion)?.version; // e.g. 23.26.0 + + if (!normalized || !semver.gte(normalized, '23.4.0')) { + this.skip(); + } + }); + + describe('findAll', () => { + beforeEach(async function () { + this.Item = sequelize.define('Item', { + embeddings: DataTypes.VECTOR(4), + }); + + await this.Item.sync({ force: true }); + await this.Item.create({ embeddings: new Float32Array([1, 1, 1, 1]) }); + await this.Item.create({ embeddings: new Float32Array([1, 2, 3, 3]) }); + }); + + it('fetches rows', async function () { + const result = await this.Item.findAll(); + expect(result).to.have.length(2); + }); + + it('returns typed arrays for vector column', async function () { + const result = await this.Item.findAll(); + expect(result[0].getDataValue('embeddings').BYTES_PER_ELEMENT).to.equal(4); + }); + }); + + describe('similarity search functions', () => { + beforeEach(async function () { + this.Item = sequelize.define('Item', { + embeddings: DataTypes.VECTOR(3), + }); + + await this.Item.sync({ force: true }); + await this.Item.create({ embeddings: new Float32Array([1, 1, 1]) }); + await this.Item.create({ embeddings: new Float32Array([5, 5, 5]) }); + await this.Item.create({ embeddings: new Float32Array([10, 10, 10]) }); + await this.Item.create({ embeddings: new Float32Array([1, 2, 3]) }); + }); + + it('supports l2 distance filtering', async function () { + const queryVector = [1, 2, 3]; + const result = await this.Item.findAll({ + where: sql.where( + sql.fn('L2_DISTANCE', sql.attribute('embeddings'), queryVector), + Op.lt, + 3, + ), + }); + + expect(result.length).to.equal(2); + }); + + it('supports helper methods', async function () { + const queryVector = [1, 2, 3]; + const result = await this.Item.findAll({ + where: sql.where(sequelize.vectorDistance('embeddings', queryVector), Op.lt, 2), + }); + + expect(result.length).to.equal(4); + }); + }); + }); +} diff --git a/packages/core/test/unit/data-types/string-types.test.ts b/packages/core/test/unit/data-types/string-types.test.ts index 5279749b8fe1..0fdea1eb7ba6 100644 --- a/packages/core/test/unit/data-types/string-types.test.ts +++ b/packages/core/test/unit/data-types/string-types.test.ts @@ -214,3 +214,47 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat }); }); }); + +describe('DataTypes.VECTOR', () => { + describe('toSql', () => { + testDataTypeSql('VECTOR', DataTypes.VECTOR, { + default: new Error(`${dialectName} does not support the VECTOR data type. +See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), + oracle: 'VECTOR', + }); + + testDataTypeSql('VECTOR(4)', DataTypes.VECTOR(4), { + default: new Error(`${dialectName} does not support the VECTOR data type. +See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), + oracle: 'VECTOR(4, *)', + }); + + testDataTypeSql("VECTOR(3, 'float32')", DataTypes.VECTOR(3, 'float32'), { + default: new Error(`${dialectName} does not support the VECTOR data type. +See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), + oracle: 'VECTOR(3, FLOAT32)', + }); + }); + + describe('validate', () => { + it('should throw an error if value is invalid', () => { + const type: DataTypeInstance = DataTypes.VECTOR(); + + expect(() => { + type.validate('vector'); + }).to.throw(ValidationErrorItem, "'vector' is not a valid vector"); + }); + + it('should not throw if value is an array', () => { + const type: DataTypeInstance = DataTypes.VECTOR(); + + expect(() => type.validate([1, 2, 3])).not.to.throw(); + }); + + it('should not throw if value is a typed array', () => { + const type: DataTypeInstance = DataTypes.VECTOR(); + + expect(() => type.validate(new Float32Array([1, 2, 3]))).not.to.throw(); + }); + }); +}); diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js new file mode 100644 index 000000000000..c95f89bf3ea6 --- /dev/null +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -0,0 +1,143 @@ +'use strict'; + +const util = require('node:util'); +const Support = require('../../../support'); +const { DataTypes, Op, sql } = require('@sequelize/core'); + +const expectsql = Support.expectsql; +const current = Support.sequelize; +const queryGenerator = current.dialect.queryGenerator; + +if (current.dialect.name === 'oracle') { + describe('[Oracle Specific] VECTOR', () => { + describe('VECTOR datatype', () => { + it('creates table with vector datatype', () => { + const FooUser = current.define('user', { + embedding: { + type: DataTypes.VECTOR, + allowNull: false, + }, + }); + + expectsql( + queryGenerator.createTableQuery( + FooUser.table, + queryGenerator.attributesToSQL(FooUser.getAttributes()), + {}, + ), + { + default: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "users" ("id" NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY, "embedding" VECTOR NOT NULL, "createdAt" TIMESTAMP WITH LOCAL TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH LOCAL TIME ZONE NOT NULL,PRIMARY KEY ("id"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, + }, + ); + }); + }); + + describe('VECTOR datatype with dimension and format', () => { + it('creates table with vector datatype', () => { + const FooUser = current.define('user', { + embedding: { + type: DataTypes.VECTOR(3, 'float32'), + allowNull: false, + }, + }); + + expectsql( + queryGenerator.createTableQuery( + FooUser.table, + queryGenerator.attributesToSQL(FooUser.getAttributes()), + {}, + ), + { + default: `BEGIN EXECUTE IMMEDIATE 'CREATE TABLE "users" ("id" NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY, "embedding" VECTOR(3, FLOAT32) NOT NULL, "createdAt" TIMESTAMP WITH LOCAL TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH LOCAL TIME ZONE NOT NULL,PRIMARY KEY ("id"))'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -955 THEN RAISE; END IF; END;`, + }, + ); + }); + }); + + describe('Vector index', () => { + it('default', () => { + expectsql(queryGenerator.addIndexQuery('Foo', ['vec1'], { type: 'VECTOR' }), { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "Foo" ("vec1") ORGANIZATION INMEMORY NEIGHBOR GRAPH', + }); + }); + + it('ivf with parameters and target accuracy', () => { + expectsql( + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'VECTOR', + using: 'ivf', + accuracy: 95, + parameter: { partitions: 5, samplesPerPartition: 10, minVectors: 10 }, + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION NEIGHBOR PARTITION GRAPH WITH TARGET ACCURACY 95 PARAMETERS (type ivf, NEIGHBOR PARTITION 5, SAMPLES_PER_PARTITION 10, MIN_VECTORS_PER_PARTITION 10)', + }, + ); + }); + }); + + describe('Vector where clause', () => { + const queryVector = [1, 2, 3]; + + const testsql = function (key, value, options, expectation) { + if (expectation === undefined) { + expectation = options; + options = undefined; + } + + it(`${String(key)}: ${util.inspect(value, { depth: 10 })}${options ? `, ${util.inspect(options)}` : ''}`, () => { + return expectsql( + queryGenerator.whereItemsQuery(sql.where(key, value), options), + expectation, + ); + }); + }; + + testsql( + sql.fn('VECTOR_DISTANCE', sql.attribute('embedding'), queryVector), + { + [Op.lt]: 2, + }, + { + oracle: `VECTOR_DISTANCE("embedding", VECTOR('[1,2,3]')) < 2`, + }, + ); + }); + + describe('order by distance with limit', () => { + const queryVector = [1, 2, 3, 4]; + + const User = Support.sequelize.define( + 'User', + { + embedding: { + type: DataTypes.VECTOR(4), + }, + }, + { + tableName: 'user', + }, + ); + + it('generates order by vector distance', () => { + expectsql( + queryGenerator.selectQuery( + User.table, + { + model: User, + attributes: ['embedding'], + order: [sql.fn('VECTOR_DISTANCE', sql.attribute('embedding'), queryVector)], + limit: 5, + }, + User, + ), + { + oracle: `SELECT "embedding" FROM "user" "User" ORDER BY VECTOR_DISTANCE("embedding", VECTOR('[1,2,3,4]')) OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY;`, + }, + ); + }); + }); + }); +} diff --git a/packages/oracle/src/_internal/data-types-overrides.ts b/packages/oracle/src/_internal/data-types-overrides.ts index 1c117f394306..fcb408b30597 100644 --- a/packages/oracle/src/_internal/data-types-overrides.ts +++ b/packages/oracle/src/_internal/data-types-overrides.ts @@ -417,3 +417,29 @@ export class DATEONLY extends BaseTypes.DATEONLY { return options.bindParam(value); } } + +export class VECTOR extends BaseTypes.VECTOR { + toSql() { + if (this.options.dimension && this.options.format) { + return `VECTOR(${this.options.dimension}, ${this.options.format.toUpperCase()})`; + } + + if (this.options.dimension) { + return `VECTOR(${this.options.dimension}, *)`; + } + + return 'VECTOR'; + } + + toBindableValue(value: BaseTypes.Vector) { + if (Array.isArray(value)) { + return Float64Array.from(value); + } + + return value; + } + + _getBindDef(oracledb: Lib) { + return { type: oracledb.DB_TYPE_VECTOR }; + } +} diff --git a/packages/oracle/src/dialect.ts b/packages/oracle/src/dialect.ts index 8643773a70ab..ca594a202201 100644 --- a/packages/oracle/src/dialect.ts +++ b/packages/oracle/src/dialect.ts @@ -49,6 +49,7 @@ export class OracleDialect extends AbstractDialect extends AbstractQueryGeneratorInternal { @@ -48,6 +57,48 @@ export class OracleQueryGeneratorInternal< return `CAST(${castSql} AS ${targetSql})`; } + formatFn(piece: Fn, options?: EscapeOptions): string { + const fnName = piece.fn.toUpperCase(); + if (!VECTOR_FUNCTIONS.has(fnName)) { + return super.formatFn(piece, options); + } + + if (piece.args.length !== 2) { + throw new Error(`${fnName} expects exactly 2 arguments`); + } + + const [columnArg, vectorArg] = piece.args; + const columnSql = + typeof columnArg === 'string' + ? this.queryGenerator.quoteIdentifier(columnArg) + : this.queryGenerator.escape(columnArg, options); + + return `${fnName}(${columnSql}, ${this.#formatVectorArg(vectorArg, fnName)})`; + } + + #formatVectorArg(arg: unknown, fnName: string): string { + if (Array.isArray(arg)) { + return `VECTOR('[${arg.join(',')}]')`; + } + + if ( + arg instanceof Float32Array || + arg instanceof Float64Array || + arg instanceof Int8Array || + arg instanceof Uint8Array + ) { + return `VECTOR('[${[...arg].join(',')}]')`; + } + + if (typeof arg === 'string' && arg.trimStart().toUpperCase().startsWith('VECTOR(')) { + return arg; + } + + throw new Error( + `${fnName} expects the second argument to be a vector array, typed array, or VECTOR literal string`, + ); + } + getAliasToken(): string { return ''; } diff --git a/packages/oracle/src/query-generator.js b/packages/oracle/src/query-generator.js index 6e0eedba7cba..1a4733b23a06 100644 --- a/packages/oracle/src/query-generator.js +++ b/packages/oracle/src/query-generator.js @@ -7,6 +7,7 @@ import forOwn from 'lodash/forOwn'; import includes from 'lodash/includes'; import isPlainObject from 'lodash/isPlainObject'; import toPath from 'lodash/toPath'; +import util from 'node:util'; import oracledb from 'oracledb'; import { DataTypes } from '@sequelize/core'; @@ -15,6 +16,8 @@ import { ADD_COLUMN_QUERY_SUPPORTABLE_OPTIONS, CREATE_TABLE_QUERY_SUPPORTABLE_OPTIONS, } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator.js'; +import { BaseSqlExpression } from '@sequelize/core/_non-semver-use-at-your-own-risk_/expression-builders/base-sql-expression.js'; +import { conformIndex } from '@sequelize/core/_non-semver-use-at-your-own-risk_/model-internals.js'; import { rejectInvalidOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/check.js'; import { quoteIdentifier } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/dialect.js'; import { joinSQLFragments } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/join-sql-fragments.js'; @@ -24,6 +27,7 @@ import { getObjectFromMap, } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/object.js'; import { defaultValueSchemable } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/query-builder-utils.js'; +import { nameIndex } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/string.js'; import { OracleQueryGeneratorTypeScript } from './query-generator-typescript.internal'; const CREATE_TABLE_QUERY_SUPPORTED_OPTIONS = new Set(['uniqueKeys']); @@ -403,11 +407,102 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { @overide */ addIndexQuery(tableName, attributes, options, rawTablename) { - if (typeof tableName !== 'string' && attributes.name) { + if (typeof tableName !== 'string' && attributes?.name) { attributes.name = `${tableName.schema}.${attributes.name}`; } - return super.addIndexQuery(tableName, attributes, options, rawTablename); + const vectorRequested = + options?.type === 'VECTOR' || (!Array.isArray(attributes) && attributes?.type === 'VECTOR'); + if (!vectorRequested) { + return super.addIndexQuery(tableName, attributes, options, rawTablename); + } + + options ||= {}; + + if (!Array.isArray(attributes)) { + options = attributes; + attributes = undefined; + } else { + options.fields = attributes; + } + + options.prefix = options.prefix || rawTablename || tableName; + if (options.prefix && typeof options.prefix === 'string') { + options.prefix = options.prefix.replaceAll('.', '_'); + options.prefix = options.prefix.replaceAll(/("|')/g, ''); + } + + const fieldsSql = options.fields.map(field => { + if (field instanceof BaseSqlExpression) { + return this.formatSqlExpression(field); + } + + if (typeof field === 'string') { + field = { name: field }; + } + + if (field.attribute) { + field.name = field.attribute; + } + + if (!field.name) { + throw new Error(`The following index field has no name: ${util.inspect(field)}`); + } + + let result = this.quoteIdentifier(field.name); + if (field.order) { + result += ` ${field.order}`; + } + + return result; + }); + + if (!options.name) { + options = nameIndex(options, options.prefix); + } + + options = conformIndex(options); + const escapedTableName = this.quoteTable(tableName); + options.using ??= 'hnsw'; + + const parameters = []; + if (options.parameter) { + if (options.using === 'hnsw') { + parameters.push('type hnsw'); + if (options.parameter.neighbor) { + parameters.push(`neighbor ${options.parameter.neighbor}`); + } + + if (options.parameter.efconstruction) { + parameters.push(`efconstruction ${options.parameter.efconstruction}`); + } + } else { + parameters.push('type ivf'); + if (options.parameter.partitions) { + parameters.push(`NEIGHBOR PARTITION ${options.parameter.partitions}`); + } + + if (options.parameter.samplesPerPartition) { + parameters.push(`SAMPLES_PER_PARTITION ${options.parameter.samplesPerPartition}`); + } + + if (options.parameter.minVectors) { + parameters.push(`MIN_VECTORS_PER_PARTITION ${options.parameter.minVectors}`); + } + } + } + + return joinSQLFragments([ + 'CREATE VECTOR INDEX', + this.quoteIdentifiers(options.name), + `ON ${escapedTableName}`, + `(${fieldsSql.join(', ')})`, + 'ORGANIZATION', + options.using === 'hnsw' ? 'INMEMORY NEIGHBOR GRAPH' : 'NEIGHBOR PARTITION GRAPH', + options.distance ? `WITH DISTANCE ${options.distance}` : undefined, + options.accuracy ? `WITH TARGET ACCURACY ${options.accuracy}` : undefined, + parameters.length > 0 ? `PARAMETERS (${parameters.join(', ')})` : undefined, + ]); } // addConstraintQuery(tableName, options) { From 6c40f33ac8f54cd5390520000fe76cb5001cd103 Mon Sep 17 00:00:00 2001 From: prajalg Date: Fri, 20 Mar 2026 16:31:03 +0530 Subject: [PATCH 02/49] added VECTOR export in ESM entry point --- test/esm-named-exports.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/esm-named-exports.test.js b/test/esm-named-exports.test.js index cd2ac968f305..7fe7dd4d87cc 100644 --- a/test/esm-named-exports.test.js +++ b/test/esm-named-exports.test.js @@ -53,6 +53,7 @@ const ignoredCjsKeysMap = { 'TIME', 'TINYINT', 'TSVECTOR', + 'VECTOR', 'UUID', 'UUIDV1', 'UUIDV4', From e9d79e9339f8e4546afe3f9f5aa229701f3f66ab Mon Sep 17 00:00:00 2001 From: prajalg Date: Mon, 23 Mar 2026 11:54:31 +0530 Subject: [PATCH 03/49] added more tests and tight check for vector literal srting for arg of vector functions --- .../dialects/oracle/vector.test.js | 162 +++++++++++++++++- .../test/unit/data-types/string-types.test.ts | 26 +++ .../test/unit/dialects/oracle/vector.test.js | 51 +++++- .../oracle/src/query-generator.internal.ts | 17 +- 4 files changed, 249 insertions(+), 7 deletions(-) diff --git a/packages/core/test/integration/dialects/oracle/vector.test.js b/packages/core/test/integration/dialects/oracle/vector.test.js index b39f3719ccef..9814fdd6540a 100644 --- a/packages/core/test/integration/dialects/oracle/vector.test.js +++ b/packages/core/test/integration/dialects/oracle/vector.test.js @@ -51,6 +51,45 @@ if (getTestDialect() === 'oracle') { await this.Item.create({ embeddings: new Float32Array([1, 2, 3]) }); }); + it('supports cosine distance filtering', async function () { + const queryVector = [1, 2, 3]; + const result = await this.Item.findAll({ + where: sql.where( + sql.fn('COSINE_DISTANCE', sql.attribute('embeddings'), queryVector), + Op.lt, + 0.01, + ), + }); + + expect(result.length).to.equal(1); + }); + + it('supports inner product filtering', async function () { + const queryVector = [1, 2, 3]; + const result = await this.Item.findAll({ + where: sql.where( + sql.fn('INNER_PRODUCT', sql.attribute('embeddings'), queryVector), + Op.gt, + 20, + ), + }); + + expect(result.length).to.equal(2); + }); + + it('supports l1 distance filtering', async function () { + const queryVector = [1, 2, 3]; + const result = await this.Item.findAll({ + where: sql.where( + sql.fn('L1_DISTANCE', sql.attribute('embeddings'), queryVector), + Op.lt, + 10, + ), + }); + + expect(result.length).to.equal(3); + }); + it('supports l2 distance filtering', async function () { const queryVector = [1, 2, 3]; const result = await this.Item.findAll({ @@ -64,14 +103,133 @@ if (getTestDialect() === 'oracle') { expect(result.length).to.equal(2); }); - it('supports helper methods', async function () { + it('supports all helper methods in where filters', async function () { const queryVector = [1, 2, 3]; + const helperCases = [ + { name: 'cosineDistance', operator: Op.lt, threshold: 0.01, expected: 1 }, + { name: 'innerProduct', operator: Op.gt, threshold: 10, expected: 3 }, + { name: 'l1Distance', operator: Op.lt, threshold: 10, expected: 3 }, + { name: 'l2Distance', operator: Op.lt, threshold: 6, expected: 3 }, + { name: 'vectorDistance', operator: Op.lt, threshold: 0.01, expected: 1 }, + ]; + + for (const helperCase of helperCases) { + const result = await this.Item.findAll({ + where: sql.where( + sequelize[helperCase.name]('embeddings', queryVector), + helperCase.operator, + helperCase.threshold, + ), + }); + + expect(result.length, helperCase.name).to.equal(helperCase.expected); + } + }); + + it('accepts valid VECTOR literal strings in vector functions', async function () { const result = await this.Item.findAll({ - where: sql.where(sequelize.vectorDistance('embeddings', queryVector), Op.lt, 2), + where: sql.where( + sql.fn('VECTOR_DISTANCE', sql.attribute('embeddings'), `VECTOR('[1,2,3]')`), + Op.lt, + 2, + ), }); expect(result.length).to.equal(4); }); + + it('rejects malformed VECTOR literal strings in vector functions', async function () { + await expect( + this.Item.findAll({ + where: sql.where( + sql.fn('VECTOR_DISTANCE', sql.attribute('embeddings'), `VECTR('[1,2,3]')`), + Op.lt, + 2, + ), + }), + ).to.be.rejected; + }); + + it('supports all vector helper methods in ORDER BY', async function () { + const queryVector = [1, 2, 3]; + const helpers = [ + 'cosineDistance', + 'innerProduct', + 'l1Distance', + 'l2Distance', + 'vectorDistance', + ]; + + for (const helper of helpers) { + const result = await this.Item.findAll({ + order: [sequelize[helper]('embeddings', queryVector)], + limit: 1, + }); + + expect(result).to.have.length(1); + } + }); + }); + + describe('vector input validation and persistence', () => { + beforeEach(async function () { + this.Item = sequelize.define('VectorInputItem', { + embeddings: DataTypes.VECTOR(3), + }); + + await this.Item.sync({ force: true }); + }); + + const acceptedInputs = [ + { name: 'number array', value: [1, 2, 3] }, + { name: 'Float32Array', value: new Float32Array([1, 2, 3]) }, + { name: 'Float64Array', value: new Float64Array([1, 2, 3]) }, + { name: 'Int8Array', value: new Int8Array([1, 2, 3]) }, + ]; + + for (const { name, value } of acceptedInputs) { + it(`accepts ${name} input`, async function () { + await this.Item.create({ embeddings: value }); + + const row = await this.Item.findOne(); + expect(Array.from(row.getDataValue('embeddings'))).to.deep.equal([1, 2, 3]); + }); + } + + it('rejects DataView input', async function () { + const dataView = new DataView(new ArrayBuffer(3)); + + await expect(this.Item.create({ embeddings: dataView })).to.be.rejectedWith( + Error, + 'is not a valid vector', + ); + }); + + it('rejects string input', async function () { + await expect(this.Item.create({ embeddings: '1,2,3' })).to.be.rejectedWith( + Error, + 'is not a valid vector', + ); + }); + + it('rejects arrays with non-number values', async function () { + await expect(this.Item.create({ embeddings: [1, '2', 3] })).to.be.rejectedWith( + Error, + 'is not a valid vector', + ); + }); + + it('stores Uint8Array in binary vectors with matching bit-dimension', async () => { + const BinaryItem = sequelize.define('BinaryVectorInputItem', { + embeddings: DataTypes.VECTOR(24, 'binary'), + }); + + await BinaryItem.sync({ force: true }); + await BinaryItem.create({ embeddings: new Uint8Array([1, 2, 3]) }); + + const row = await BinaryItem.findOne(); + expect(row).to.not.equal(null); + }); }); }); } diff --git a/packages/core/test/unit/data-types/string-types.test.ts b/packages/core/test/unit/data-types/string-types.test.ts index 0fdea1eb7ba6..4377e6f4ae99 100644 --- a/packages/core/test/unit/data-types/string-types.test.ts +++ b/packages/core/test/unit/data-types/string-types.test.ts @@ -216,6 +216,32 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat }); describe('DataTypes.VECTOR', () => { + describe('constructor', () => { + it('stores empty options when no args are passed', () => { + const type = DataTypes.VECTOR(); + + expect(type.options).to.deep.equal({}); + }); + + it('stores dimension when only dimension is passed', () => { + const type = DataTypes.VECTOR(4); + + expect(type.options).to.deep.equal({ dimension: 4 }); + }); + + it('stores dimension and format when both args are passed', () => { + const type = DataTypes.VECTOR(3, 'float32'); + + expect(type.options).to.deep.equal({ dimension: 3, format: 'float32' }); + }); + + it('supports object-style options', () => { + const type = DataTypes.VECTOR({ dimension: 8, format: 'float64' }); + + expect(type.options).to.deep.equal({ dimension: 8, format: 'float64' }); + }); + }); + describe('toSql', () => { testDataTypeSql('VECTOR', DataTypes.VECTOR, { default: new Error(`${dialectName} does not support the VECTOR data type. diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index c95f89bf3ea6..3dd770a7cafa 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -3,6 +3,7 @@ const util = require('node:util'); const Support = require('../../../support'); const { DataTypes, Op, sql } = require('@sequelize/core'); +const { expect } = require('chai'); const expectsql = Support.expectsql; const current = Support.sequelize; @@ -87,12 +88,16 @@ if (current.dialect.name === 'oracle') { options = undefined; } - it(`${String(key)}: ${util.inspect(value, { depth: 10 })}${options ? `, ${util.inspect(options)}` : ''}`, () => { + const whereObj = sql.where(key, value); + it( + util.inspect(whereObj, { depth: 4 }) + (options ? `, ${util.inspect(options)}` : ''), + () => { return expectsql( - queryGenerator.whereItemsQuery(sql.where(key, value), options), + queryGenerator.whereItemsQuery(whereObj, options), expectation, ); - }); + }, + ); }; testsql( @@ -104,6 +109,46 @@ if (current.dialect.name === 'oracle') { oracle: `VECTOR_DISTANCE("embedding", VECTOR('[1,2,3]')) < 2`, }, ); + + testsql( + sql.fn('VECTOR_DISTANCE', sql.attribute('embedding'), new Float32Array([1, 2, 3])), + { + [Op.lt]: 2, + }, + { + oracle: `VECTOR_DISTANCE("embedding", VECTOR('[1,2,3]')) < 2`, + }, + ); + + testsql( + sql.fn('VECTOR_DISTANCE', sql.attribute('embedding'), `VECTOR('[1,2,3]')`), + { + [Op.lt]: 2, + }, + { + oracle: `VECTOR_DISTANCE("embedding", VECTOR('[1,2,3]')) < 2`, + }, + ); + + it('throws when second argument is not a vector input', () => { + expect(() => + queryGenerator.whereItemsQuery( + sql.where(sql.fn('VECTOR_DISTANCE', sql.attribute('embedding'), 'not-a-vector'), { + [Op.lt]: 2, + }), + ), + ).to.throw(Error, 'Invalid value received for the "where" option'); + }); + + it('throws for malformed VECTOR literal strings', () => { + expect(() => + queryGenerator.whereItemsQuery( + sql.where(sql.fn('VECTOR_DISTANCE', sql.attribute('embedding'), `VECTR('[1,2,3]')`), { + [Op.lt]: 2, + }), + ), + ).to.throw(Error, 'Invalid value received for the "where" option'); + }); }); describe('order by distance with limit', () => { diff --git a/packages/oracle/src/query-generator.internal.ts b/packages/oracle/src/query-generator.internal.ts index efc67960e260..618f39064b6c 100644 --- a/packages/oracle/src/query-generator.internal.ts +++ b/packages/oracle/src/query-generator.internal.ts @@ -90,8 +90,21 @@ export class OracleQueryGeneratorInternal< return `VECTOR('[${[...arg].join(',')}]')`; } - if (typeof arg === 'string' && arg.trimStart().toUpperCase().startsWith('VECTOR(')) { - return arg; + if (typeof arg === 'string') { + const trimmed = arg.trim(); + if (!trimmed.toUpperCase().startsWith('VECTOR(')) { + throw new Error( + `${fnName} expects the second argument to be a vector array, typed array, or VECTOR literal string`, + ); + } + + const vectorLiteralRegex = + /^VECTOR\(\s*'\[\s*(?:[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?\s*(?:,\s*[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?\s*)*)?\]'\s*\)$/i; + if (!vectorLiteralRegex.test(trimmed)) { + throw new Error(`${fnName} expects a well-formed VECTOR literal string`); + } + + return trimmed; } throw new Error( From 37c88b7197c967c2094355b238a18c59797bd3c8 Mon Sep 17 00:00:00 2001 From: prajalg Date: Mon, 23 Mar 2026 12:05:01 +0530 Subject: [PATCH 04/49] lint fix --- packages/core/test/unit/dialects/oracle/vector.test.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index 3dd770a7cafa..63724cb8ab56 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -92,10 +92,7 @@ if (current.dialect.name === 'oracle') { it( util.inspect(whereObj, { depth: 4 }) + (options ? `, ${util.inspect(options)}` : ''), () => { - return expectsql( - queryGenerator.whereItemsQuery(whereObj, options), - expectation, - ); + return expectsql(queryGenerator.whereItemsQuery(whereObj, options), expectation); }, ); }; From 92a3f0503ef736734a4d0bf063dad509b62d091f Mon Sep 17 00:00:00 2001 From: prajalg Date: Mon, 23 Mar 2026 14:34:14 +0530 Subject: [PATCH 05/49] added more tests including related to vector index --- .../dialects/oracle/vector.test.js | 41 ++++++++++++++ .../test/unit/dialects/oracle/vector.test.js | 55 +++++++++++++++++++ .../query-generator-typescript.internal.ts | 2 +- packages/oracle/src/query.js | 19 ++++++- 4 files changed, 113 insertions(+), 4 deletions(-) diff --git a/packages/core/test/integration/dialects/oracle/vector.test.js b/packages/core/test/integration/dialects/oracle/vector.test.js index 9814fdd6540a..c76155c9efb1 100644 --- a/packages/core/test/integration/dialects/oracle/vector.test.js +++ b/packages/core/test/integration/dialects/oracle/vector.test.js @@ -231,5 +231,46 @@ if (getTestDialect() === 'oracle') { expect(row).to.not.equal(null); }); }); + + describe('vector indexes', () => { + it('creates vector index from model indexes option during sync', async () => { + const indexName = 'vector_input_item_embeddings_hnsw_idx'; + const IndexedItem = sequelize.define( + 'VectorIndexedItem', + { + embeddings: DataTypes.VECTOR(3), + }, + { + indexes: [ + { + name: indexName, + type: 'VECTOR', + fields: ['embeddings'], + using: 'hnsw', + parameter: { neighbor: 8, efconstruction: 32 }, + }, + ], + }, + ); + + try { + await IndexedItem.sync({ force: true }); + + const indexes = await sequelize.queryInterface.showIndex(IndexedItem.table); + const vectorIndex = indexes.find( + index => index.name?.toLowerCase() === indexName.toLowerCase(), + ); + + expect(vectorIndex).to.not.equal(undefined); + expect(vectorIndex.type).to.equal('VECTOR'); + } finally { + try { + await sequelize.queryInterface.removeIndex(IndexedItem.table, indexName); + } finally { + await IndexedItem.drop(); + } + } + }); + }); }); } diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index 63724cb8ab56..2a699ac87d0a 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -77,6 +77,47 @@ if (current.dialect.name === 'oracle') { }, ); }); + + it('treats non-hnsw using as ivf organization', () => { + expectsql( + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'VECTOR', + using: 'btree', + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION NEIGHBOR PARTITION GRAPH', + }, + ); + }); + + it('ignores ivf parameters when using hnsw', () => { + expectsql( + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'VECTOR', + using: 'hnsw', + parameter: { partitions: 4 }, + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION INMEMORY NEIGHBOR GRAPH PARAMETERS (type hnsw)', + }, + ); + }); + + it('ignores hnsw parameters when using ivf', () => { + expectsql( + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'VECTOR', + using: 'ivf', + parameter: { neighbor: 8 }, + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION NEIGHBOR PARTITION GRAPH PARAMETERS (type ivf)', + }, + ); + }); }); describe('Vector where clause', () => { @@ -146,6 +187,20 @@ if (current.dialect.name === 'oracle') { ), ).to.throw(Error, 'Invalid value received for the "where" option'); }); + + it('throws when vector function has too few arguments', () => { + expect(() => + queryGenerator.formatSqlExpression(sql.fn('VECTOR_DISTANCE', sql.attribute('embedding'))), + ).to.throw(Error, 'VECTOR_DISTANCE expects exactly 2 arguments'); + }); + + it('throws when vector function has too many arguments', () => { + expect(() => + queryGenerator.formatSqlExpression( + sql.fn('VECTOR_DISTANCE', sql.attribute('embedding'), [1, 2, 3], [4, 5, 6]), + ), + ).to.throw(Error, 'VECTOR_DISTANCE expects exactly 2 arguments'); + }); }); describe('order by distance with limit', () => { diff --git a/packages/oracle/src/query-generator-typescript.internal.ts b/packages/oracle/src/query-generator-typescript.internal.ts index fd5c15c0d97e..8a3ea29006cd 100644 --- a/packages/oracle/src/query-generator-typescript.internal.ts +++ b/packages/oracle/src/query-generator-typescript.internal.ts @@ -109,7 +109,7 @@ export class OracleQueryGeneratorTypeScript extends AbstractQueryGenerator { showIndexesQuery(table: TableNameWithSchema) { const [tableName, owner] = this.getSchemaNameAndTableName(table); const sql = [ - 'SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, i.descend, c.constraint_type ', + 'SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type ', 'FROM all_ind_columns i ', 'INNER JOIN all_indexes u ', 'ON (u.table_name = i.table_name AND u.index_name = i.index_name) ', diff --git a/packages/oracle/src/query.js b/packages/oracle/src/query.js index 462e9e885e01..8f3edfb4b25e 100644 --- a/packages/oracle/src/query.js +++ b/packages/oracle/src/query.js @@ -663,8 +663,7 @@ export class OracleQuery extends AbstractQuery { } isShowIndexesQuery() { - // eslint-disable-next-line unicorn/prefer-includes - return this.sql.indexOf('SELECT i.index_name,i.table_name, i.column_name, u.uniqueness') > -1; + return this.sql.includes('SELECT i.index_name,i.table_name, i.column_name, u.uniqueness'); } isSelectCountQuery() { @@ -678,12 +677,26 @@ export class OracleQuery extends AbstractQuery { data.forEach(indexRecord => { // We create the object if (!acc[indexRecord.INDEX_NAME]) { + const oracleIndexType = indexRecord.INDEX_TYPE?.toUpperCase(); + const oracleIndexImplType = indexRecord.ITYP_NAME?.toUpperCase(); + let type; + + if (oracleIndexType === 'VECTOR') { + type = 'VECTOR'; + } else if (oracleIndexType === 'DOMAIN' && oracleIndexImplType === 'CONTEXT') { + type = 'FULLTEXT'; + } else if (oracleIndexType === 'NORMAL' || !oracleIndexType) { + type = undefined; + } else { + type = oracleIndexType; + } + acc[indexRecord.INDEX_NAME] = { unique: indexRecord.UNIQUENESS === 'UNIQUE', primary: indexRecord.CONSTRAINT_TYPE === 'P', name: indexRecord.INDEX_NAME, tableName: indexRecord.TABLE_NAME.toLowerCase(), - type: undefined, + type, }; acc[indexRecord.INDEX_NAME].fields = []; } From e97ade420eb8e22bfd96b8166f3434f123d7fdef Mon Sep 17 00:00:00 2001 From: prajalg Date: Mon, 23 Mar 2026 14:53:46 +0530 Subject: [PATCH 06/49] fix sql for oracle in show-indexes-query.test.ts --- .../unit/query-generator/show-indexes-query.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/test/unit/query-generator/show-indexes-query.test.ts b/packages/core/test/unit/query-generator/show-indexes-query.test.ts index d85318b1f61d..c89673c31d25 100644 --- a/packages/core/test/unit/query-generator/show-indexes-query.test.ts +++ b/packages/core/test/unit/query-generator/show-indexes-query.test.ts @@ -29,7 +29,7 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = CURRENT SCHEMA and QSYS2.SYSINDEXES.TABLE_NAME = 'myTable'`, - oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, i.descend, c.constraint_type + oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type FROM all_ind_columns i INNER JOIN all_indexes u ON (u.table_name = i.table_name AND u.index_name = i.index_name) LEFT OUTER JOIN all_constraints c ON (c.table_name = i.table_name AND c.index_name = i.index_name) @@ -63,7 +63,7 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = CURRENT SCHEMA and QSYS2.SYSINDEXES.TABLE_NAME = 'MyModels'`, - oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, i.descend, c.constraint_type + oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type FROM all_ind_columns i INNER JOIN all_indexes u ON (u.table_name = i.table_name AND u.index_name = i.index_name) LEFT OUTER JOIN all_constraints c ON (c.table_name = i.table_name AND c.index_name = i.index_name) @@ -98,7 +98,7 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = CURRENT SCHEMA and QSYS2.SYSINDEXES.TABLE_NAME = 'MyModels'`, - oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, i.descend, c.constraint_type FROM all_ind_columns i + oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type FROM all_ind_columns i INNER JOIN all_indexes u ON (u.table_name = i.table_name AND u.index_name = i.index_name) LEFT OUTER JOIN all_constraints c ON (c.table_name = i.table_name AND c.index_name = i.index_name) WHERE i.table_name = 'MyModels' AND u.table_owner = '${dialect.getDefaultSchema()}' ORDER BY index_name, column_position`, }); @@ -128,7 +128,7 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = 'mySchema' and QSYS2.SYSINDEXES.TABLE_NAME = 'myTable'`, - oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, i.descend, c.constraint_type + oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type FROM all_ind_columns i INNER JOIN all_indexes u ON (u.table_name = i.table_name AND u.index_name = i.index_name) LEFT OUTER JOIN all_constraints c ON (c.table_name = i.table_name AND c.index_name = i.index_name) @@ -166,7 +166,7 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = CURRENT SCHEMA and QSYS2.SYSINDEXES.TABLE_NAME = 'myTable'`, - oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, i.descend, c.constraint_type + oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type FROM all_ind_columns i INNER JOIN all_indexes u ON (u.table_name = i.table_name AND u.index_name = i.index_name) LEFT OUTER JOIN all_constraints c ON (c.table_name = i.table_name AND c.index_name = i.index_name) @@ -202,7 +202,7 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = 'mySchema' and QSYS2.SYSINDEXES.TABLE_NAME = 'myTable'`, - oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, i.descend, c.constraint_type + oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type FROM all_ind_columns i INNER JOIN all_indexes u ON (u.table_name = i.table_name AND u.index_name = i.index_name) LEFT OUTER JOIN all_constraints c ON (c.table_name = i.table_name AND c.index_name = i.index_name) From 9716ccf89958b36f454870a9f65a1948a186db12 Mon Sep 17 00:00:00 2001 From: prajalg Date: Thu, 26 Mar 2026 16:50:26 +0530 Subject: [PATCH 07/49] modified framework to also allow other dialects to implement the same --- .../core/src/abstract-dialect/data-types.ts | 100 ++++++++++++++--- packages/core/src/sequelize.js | 38 +++---- .../test/unit/dialects/oracle/vector.test.js | 105 ++++++++++++++++++ .../oracle/src/query-generator.internal.ts | 52 +++++++-- packages/oracle/src/query-generator.js | 28 ++++- packages/oracle/src/query.js | 21 ++++ 6 files changed, 293 insertions(+), 51 deletions(-) diff --git a/packages/core/src/abstract-dialect/data-types.ts b/packages/core/src/abstract-dialect/data-types.ts index 35a1d2aec8ea..34304000e826 100644 --- a/packages/core/src/abstract-dialect/data-types.ts +++ b/packages/core/src/abstract-dialect/data-types.ts @@ -2849,12 +2849,28 @@ export interface VectorOptions { format?: string; } -export type Vector = number[] | Float32Array | Float64Array | Int8Array | Uint8Array; +type TypedArrayView = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array + | BigInt64Array + | BigUint64Array; + +// Dialects may accept plain arrays or any numeric typed array. The generic validator +// inspects ArrayBufferView instances without having to enumerate every concrete type here. +export type Vector = number[] | ArrayBufferView; /** - * The VECTOR type stores vectors. + * The VECTOR type stores ordered numeric vectors. * - * Only available for Oracle + * Availability depends on the dialect (for example Oracle, Snowflake, pgvector, ...); check your dialect’s + * documentation to confirm supported element formats and dimensions. * * __Fallback policy:__ * If this type is not supported, an error will be raised. @@ -2908,38 +2924,92 @@ export class VECTOR extends AbstractDataType { } validate(value: unknown): asserts value is Vector { - if (Array.isArray(value)) { - for (const item of value) { - if (typeof item !== 'number') { - ValidationErrorItem.throwDataTypeValidationError( - util.format('%O is not a valid vector', value), - ); - } + const iterable = this._getVectorIterable(value); + if (iterable) { + for (const item of iterable) { + this._validateVectorElement(item); } return; } - if (ArrayBuffer.isView(value) && !(value instanceof DataView)) { - return; - } - ValidationErrorItem.throwDataTypeValidationError( util.format('%O is not a valid vector', value), ); } + // Dialects override this hook if they need to coerce custom container types into an iterable sequence. + protected _getVectorIterable(value: unknown): Iterable | null { + if (Array.isArray(value)) { + return value; + } + + if (isTypedArrayView(value)) { + return value; + } + + return null; + } + + // Returns the validated numeric value so dialect overrides can add additional checks + // (integer-only, finite only, etc.) while still reusing the base logic. + protected _validateVectorElement(item: unknown): number { + if (typeof item !== 'number' || Number.isNaN(item)) { + ValidationErrorItem.throwDataTypeValidationError( + util.format('%O is not a valid vector', item), + ); + } + + return item; + } + protected _checkOptionSupport(dialect: AbstractDialect) { if (!dialect.supports.dataTypes.VECTOR) { throwUnsupportedDataType(dialect, 'VECTOR'); } } + protected getSqlOptionParts(): string[] { + const parts: string[] = []; + + if (this.options.dimension !== undefined) { + parts.push(String(this.options.dimension)); + } + + if (this.options.format !== undefined) { + parts.push(this.options.format.toUpperCase()); + } + + return parts; + } + toSql(): string { - return 'VECTOR'; + const optionParts = this.getSqlOptionParts(); + + if (optionParts.length === 0) { + return 'VECTOR'; + } + + return `VECTOR(${optionParts.join(', ')})`; } } +function isTypedArrayView(value: unknown): value is TypedArrayView { + return ( + value instanceof Int8Array || + value instanceof Uint8Array || + value instanceof Uint8ClampedArray || + value instanceof Int16Array || + value instanceof Uint16Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Float32Array || + value instanceof Float64Array || + value instanceof BigInt64Array || + value instanceof BigUint64Array + ); +} + function rejectBlobs(value: unknown) { // We have a DataType called BLOB. People might try to use the built-in Blob type with it, which they cannot. // To clarify why it doesn't work, we have a dedicated message for it. diff --git a/packages/core/src/sequelize.js b/packages/core/src/sequelize.js index ba3aadde6433..7e038d17304a 100644 --- a/packages/core/src/sequelize.js +++ b/packages/core/src/sequelize.js @@ -620,43 +620,35 @@ Use Sequelize#query if you wish to use replacements.`); } cosineDistance(column, value) { - if (this.dialect.name === 'oracle') { - return fn('COSINE_DISTANCE', column, value); - } - - throw new Error(`cosineDistance for dialect "${this.dialect.name}" is not implemented`); + return this.#vectorSimilarityFn('COSINE_DISTANCE', column, value); } innerProduct(column, value) { - if (this.dialect.name === 'oracle') { - return fn('INNER_PRODUCT', column, value); - } - - throw new Error(`innerProduct for dialect "${this.dialect.name}" is not implemented`); + return this.#vectorSimilarityFn('INNER_PRODUCT', column, value); } l1Distance(column, value) { - if (this.dialect.name === 'oracle') { - return fn('L1_DISTANCE', column, value); - } - - throw new Error(`l1Distance for dialect "${this.dialect.name}" is not implemented`); + return this.#vectorSimilarityFn('L1_DISTANCE', column, value); } l2Distance(column, value) { - if (this.dialect.name === 'oracle') { - return fn('L2_DISTANCE', column, value); - } - - throw new Error(`l2Distance for dialect "${this.dialect.name}" is not implemented`); + return this.#vectorSimilarityFn('L2_DISTANCE', column, value); } vectorDistance(column, value) { - if (this.dialect.name === 'oracle') { - return fn('VECTOR_DISTANCE', column, value); + return this.#vectorSimilarityFn('VECTOR_DISTANCE', column, value); + } + + #vectorSimilarityFn(fnName, column, value) { + // Each dialect that advertises VECTOR support is expected to translate this generic function + // name in its query generator. If the dialect does not support vectors we fail fast here. + if (!this.dialect.supports?.dataTypes?.VECTOR) { + throw new Error( + `${fnName.toLowerCase()} for dialect "${this.dialect.name}" is not implemented`, + ); } - throw new Error(`vectorDistance for dialect "${this.dialect.name}" is not implemented`); + return fn(fnName, column, value); } // Global exports diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index 2a699ac87d0a..b3f6d325be87 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -3,6 +3,7 @@ const util = require('node:util'); const Support = require('../../../support'); const { DataTypes, Op, sql } = require('@sequelize/core'); +const { OracleQuery } = require('@sequelize/oracle'); const { expect } = require('chai'); const expectsql = Support.expectsql; @@ -63,6 +64,13 @@ if (current.dialect.name === 'oracle') { }); }); + it('accepts lowercase vector type', () => { + expectsql(queryGenerator.addIndexQuery('Foo', ['vec1'], { type: 'vector' }), { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "Foo" ("vec1") ORGANIZATION INMEMORY NEIGHBOR GRAPH', + }); + }); + it('ivf with parameters and target accuracy', () => { expectsql( queryGenerator.addIndexQuery('foo', ['vec1'], { @@ -91,6 +99,32 @@ if (current.dialect.name === 'oracle') { ); }); + it('accepts uppercase using value', () => { + expectsql( + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'VECTOR', + using: 'HNSW', + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION INMEMORY NEIGHBOR GRAPH', + }, + ); + }); + + it('accepts uppercase ivf using value', () => { + expectsql( + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'vector', + using: 'IVF', + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION NEIGHBOR PARTITION GRAPH', + }, + ); + }); + it('ignores ivf parameters when using hnsw', () => { expectsql( queryGenerator.addIndexQuery('foo', ['vec1'], { @@ -118,6 +152,77 @@ if (current.dialect.name === 'oracle') { }, ); }); + + it('throws when no fields are provided', () => { + expect(() => queryGenerator.addIndexQuery('foo', [], { type: 'VECTOR' })).to.throw( + Error, + 'Vector indexes require at least one indexed field.', + ); + }); + }); + + describe('showIndex metadata', () => { + const buildQuery = () => new OracleQuery({}, current, {}); + + it('reports vector method hnsw', () => { + const query = buildQuery(); + const rows = [ + { + INDEX_NAME: 'IDX_HNSW', + INDEX_TYPE: 'VECTOR', + ITYP_NAME: null, + UNIQUENESS: 'NONUNIQUE', + CONSTRAINT_TYPE: null, + TABLE_NAME: 'FOO', + COLUMN_NAME: 'EMBEDDING', + DESCEND: null, + PARAMETERS_LOWER: 'type hnsw neighbor 10', + }, + ]; + + const result = query.handleShowIndexesQuery(rows); + expect(result[0].method).to.equal('hnsw'); + }); + + it('reports vector method ivf', () => { + const query = buildQuery(); + const rows = [ + { + INDEX_NAME: 'IDX_IVF', + INDEX_TYPE: 'VECTOR', + ITYP_NAME: null, + UNIQUENESS: 'NONUNIQUE', + CONSTRAINT_TYPE: null, + TABLE_NAME: 'FOO', + COLUMN_NAME: 'EMBEDDING', + DESCEND: null, + PARAMETERS_LOWER: 'type ivf neighbor partition 8', + }, + ]; + + const result = query.handleShowIndexesQuery(rows); + expect(result[0].method).to.equal('ivf'); + }); + + it('leaves method undefined when parameters missing', () => { + const query = buildQuery(); + const rows = [ + { + INDEX_NAME: 'IDX_UNKNOWN', + INDEX_TYPE: 'VECTOR', + ITYP_NAME: null, + UNIQUENESS: 'NONUNIQUE', + CONSTRAINT_TYPE: null, + TABLE_NAME: 'FOO', + COLUMN_NAME: 'EMBEDDING', + DESCEND: null, + PARAMETERS_LOWER: null, + }, + ]; + + const result = query.handleShowIndexesQuery(rows); + expect(result[0].method).to.equal(undefined); + }); }); describe('Vector where clause', () => { diff --git a/packages/oracle/src/query-generator.internal.ts b/packages/oracle/src/query-generator.internal.ts index 618f39064b6c..765929213edd 100644 --- a/packages/oracle/src/query-generator.internal.ts +++ b/packages/oracle/src/query-generator.internal.ts @@ -7,6 +7,7 @@ import type { AddLimitOffsetOptions } from '@sequelize/core/_non-semver-use-at-y import { wrapAmbiguousWhere } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/where-sql-builder.js'; import type { Cast } from '@sequelize/core/_non-semver-use-at-your-own-risk_/expression-builders/cast.js'; import type { Fn } from '@sequelize/core/_non-semver-use-at-your-own-risk_/expression-builders/fn.js'; +import util from 'node:util'; import type { OracleDialect } from './dialect.js'; const VECTOR_FUNCTIONS = new Set([ @@ -78,16 +79,11 @@ export class OracleQueryGeneratorInternal< #formatVectorArg(arg: unknown, fnName: string): string { if (Array.isArray(arg)) { - return `VECTOR('[${arg.join(',')}]')`; + return this.#formatVectorFromIterable(arg); } - if ( - arg instanceof Float32Array || - arg instanceof Float64Array || - arg instanceof Int8Array || - arg instanceof Uint8Array - ) { - return `VECTOR('[${[...arg].join(',')}]')`; + if (ArrayBuffer.isView(arg) && !(arg instanceof DataView) && isNumericTypedArray(arg)) { + return this.#formatVectorFromIterable(arg); } if (typeof arg === 'string') { @@ -112,7 +108,47 @@ export class OracleQueryGeneratorInternal< ); } + // Oracle expects VECTOR('[1,2,3]') literals. Reuse the iterable path for both plain arrays + // and typed arrays so we only have one place that generates the comma-separated payload. + #formatVectorFromIterable(values: Iterable): string { + const parts: number[] = []; + for (const item of values) { + if (typeof item !== 'number' || Number.isNaN(item)) { + throw new Error(`${util.format('%O is not a valid vector element', item)}`); + } + + parts.push(item); + } + + return `VECTOR('[${parts.join(',')}]')`; + } + getAliasToken(): string { return ''; } } + +function isNumericTypedArray( + value: ArrayBufferView, +): value is + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array { + return ( + value instanceof Int8Array || + value instanceof Uint8Array || + value instanceof Uint8ClampedArray || + value instanceof Int16Array || + value instanceof Uint16Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Float32Array || + value instanceof Float64Array + ); +} diff --git a/packages/oracle/src/query-generator.js b/packages/oracle/src/query-generator.js index 1a4733b23a06..00a68e79b214 100644 --- a/packages/oracle/src/query-generator.js +++ b/packages/oracle/src/query-generator.js @@ -411,8 +411,14 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { attributes.name = `${tableName.schema}.${attributes.name}`; } - const vectorRequested = - options?.type === 'VECTOR' || (!Array.isArray(attributes) && attributes?.type === 'VECTOR'); + const requestedIndexType = + typeof options?.type === 'string' ? options.type.toUpperCase() : undefined; + const requestedAttributeType = + !Array.isArray(attributes) && typeof attributes?.type === 'string' + ? attributes.type.toUpperCase() + : undefined; + + const vectorRequested = requestedIndexType === 'VECTOR' || requestedAttributeType === 'VECTOR'; if (!vectorRequested) { return super.addIndexQuery(tableName, attributes, options, rawTablename); } @@ -426,12 +432,20 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { options.fields = attributes; } + if (typeof options.type === 'string') { + options.type = options.type.toUpperCase(); + } + options.prefix = options.prefix || rawTablename || tableName; if (options.prefix && typeof options.prefix === 'string') { options.prefix = options.prefix.replaceAll('.', '_'); options.prefix = options.prefix.replaceAll(/("|')/g, ''); } + if (!options.fields || options.fields.length === 0) { + throw new Error('Vector indexes require at least one indexed field.'); + } + const fieldsSql = options.fields.map(field => { if (field instanceof BaseSqlExpression) { return this.formatSqlExpression(field); @@ -463,11 +477,15 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { options = conformIndex(options); const escapedTableName = this.quoteTable(tableName); - options.using ??= 'hnsw'; + + const normalizedUsing = + typeof options.using === 'string' ? options.using.toLowerCase() : undefined; + const using = normalizedUsing ?? 'hnsw'; + const usingIsHnsw = using === 'hnsw'; const parameters = []; if (options.parameter) { - if (options.using === 'hnsw') { + if (usingIsHnsw) { parameters.push('type hnsw'); if (options.parameter.neighbor) { parameters.push(`neighbor ${options.parameter.neighbor}`); @@ -498,7 +516,7 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { `ON ${escapedTableName}`, `(${fieldsSql.join(', ')})`, 'ORGANIZATION', - options.using === 'hnsw' ? 'INMEMORY NEIGHBOR GRAPH' : 'NEIGHBOR PARTITION GRAPH', + usingIsHnsw ? 'INMEMORY NEIGHBOR GRAPH' : 'NEIGHBOR PARTITION GRAPH', options.distance ? `WITH DISTANCE ${options.distance}` : undefined, options.accuracy ? `WITH TARGET ACCURACY ${options.accuracy}` : undefined, parameters.length > 0 ? `PARAMETERS (${parameters.join(', ')})` : undefined, diff --git a/packages/oracle/src/query.js b/packages/oracle/src/query.js index 8f3edfb4b25e..c5a316d67b5c 100644 --- a/packages/oracle/src/query.js +++ b/packages/oracle/src/query.js @@ -680,9 +680,11 @@ export class OracleQuery extends AbstractQuery { const oracleIndexType = indexRecord.INDEX_TYPE?.toUpperCase(); const oracleIndexImplType = indexRecord.ITYP_NAME?.toUpperCase(); let type; + let method; if (oracleIndexType === 'VECTOR') { type = 'VECTOR'; + method = this.#determineVectorMethod(indexRecord.PARAMETERS_LOWER); } else if (oracleIndexType === 'DOMAIN' && oracleIndexImplType === 'CONTEXT') { type = 'FULLTEXT'; } else if (oracleIndexType === 'NORMAL' || !oracleIndexType) { @@ -697,6 +699,7 @@ export class OracleQuery extends AbstractQuery { name: indexRecord.INDEX_NAME, tableName: indexRecord.TABLE_NAME.toLowerCase(), type, + method, }; acc[indexRecord.INDEX_NAME].fields = []; } @@ -734,6 +737,24 @@ export class OracleQuery extends AbstractQuery { return returnIndexes; } + // Oracle surfaces vector index configuration as a lower-cased parameter string. + // Parse out the declared index type so consumers can see whether the index is IVF or HNSW. + #determineVectorMethod(parametersLower) { + if (typeof parametersLower !== 'string') { + return undefined; + } + + if (parametersLower.includes('type ivf')) { + return 'ivf'; + } + + if (parametersLower.includes('type hnsw')) { + return 'hnsw'; + } + + return undefined; + } + handleInsertQuery(results, metaData) { if (this.instance && results && results.length > 0) { if ('pkReturnVal' in results[0]) { From 498bfc217fa329ecaf377ed1133ac4e425749932 Mon Sep 17 00:00:00 2001 From: prajalg Date: Mon, 30 Mar 2026 16:52:21 +0530 Subject: [PATCH 08/49] also added some dialects sample implementation + minor core changes --- .../core/src/abstract-dialect/data-types.ts | 5 +- packages/core/src/sequelize.d.ts | 10 +- packages/core/src/sequelize.js | 4 +- .../test/unit/data-types/string-types.test.ts | 6 + .../unit/dialects/postgres/vector.test.js | 42 +++++ .../unit/dialects/snowflake/vector.test.js | 43 +++++ .../src/_internal/data-types-overrides.ts | 12 +- packages/oracle/src/query-generator.js | 26 +-- .../src/_internal/data-types-overrides.ts | 71 +++++++++ packages/postgres/src/dialect.ts | 1 + .../postgres/src/query-generator.internal.ts | 113 +++++++++++++ .../src/_internal/data-types-overrides.ts | 111 +++++++++++++ packages/snowflake/src/dialect.ts | 1 + .../snowflake/src/query-generator.internal.ts | 149 ++++++++++++++++++ 14 files changed, 566 insertions(+), 28 deletions(-) create mode 100644 packages/core/test/unit/dialects/postgres/vector.test.js create mode 100644 packages/core/test/unit/dialects/snowflake/vector.test.js diff --git a/packages/core/src/abstract-dialect/data-types.ts b/packages/core/src/abstract-dialect/data-types.ts index 34304000e826..1977d1891f7a 100644 --- a/packages/core/src/abstract-dialect/data-types.ts +++ b/packages/core/src/abstract-dialect/data-types.ts @@ -2969,7 +2969,8 @@ export class VECTOR extends AbstractDataType { } } - protected getSqlOptionParts(): string[] { + // Dialects can override this hook to customize VECTOR SQL rendering without replacing toSql(). + protected _getSqlOptionParts(): string[] { const parts: string[] = []; if (this.options.dimension !== undefined) { @@ -2984,7 +2985,7 @@ export class VECTOR extends AbstractDataType { } toSql(): string { - const optionParts = this.getSqlOptionParts(); + const optionParts = this._getSqlOptionParts(); if (optionParts.length === 0) { return 'VECTOR'; diff --git a/packages/core/src/sequelize.d.ts b/packages/core/src/sequelize.d.ts index 57987791d205..1ceeb6c79b61 100644 --- a/packages/core/src/sequelize.d.ts +++ b/packages/core/src/sequelize.d.ts @@ -613,11 +613,11 @@ export class Sequelize< * Get the fn for random based on the dialect */ random(): Fn; - cosineDistance(column: string, value: number[] | ArrayBufferView): Fn; - innerProduct(column: string, value: number[] | ArrayBufferView): Fn; - l1Distance(column: string, value: number[] | ArrayBufferView): Fn; - l2Distance(column: string, value: number[] | ArrayBufferView): Fn; - vectorDistance(column: string, value: number[] | ArrayBufferView): Fn; + cosineDistance(column: string, value: number[] | Exclude): Fn; + innerProduct(column: string, value: number[] | Exclude): Fn; + l1Distance(column: string, value: number[] | Exclude): Fn; + l2Distance(column: string, value: number[] | Exclude): Fn; + vectorDistance(column: string, value: number[] | Exclude): Fn; /** * Execute a query which would set an environment or user variable. The variables are set per connection, diff --git a/packages/core/src/sequelize.js b/packages/core/src/sequelize.js index 7e038d17304a..511d42ad8dd3 100644 --- a/packages/core/src/sequelize.js +++ b/packages/core/src/sequelize.js @@ -640,8 +640,8 @@ Use Sequelize#query if you wish to use replacements.`); } #vectorSimilarityFn(fnName, column, value) { - // Each dialect that advertises VECTOR support is expected to translate this generic function - // name in its query generator. If the dialect does not support vectors we fail fast here. + // Dialects that advertise VECTOR support can translate these generic helper names in their + // query generator. Oracle is the primary implementation; other dialects may follow this pattern. if (!this.dialect.supports?.dataTypes?.VECTOR) { throw new Error( `${fnName.toLowerCase()} for dialect "${this.dialect.name}" is not implemented`, diff --git a/packages/core/test/unit/data-types/string-types.test.ts b/packages/core/test/unit/data-types/string-types.test.ts index 4377e6f4ae99..c63b57df268d 100644 --- a/packages/core/test/unit/data-types/string-types.test.ts +++ b/packages/core/test/unit/data-types/string-types.test.ts @@ -247,18 +247,24 @@ describe('DataTypes.VECTOR', () => { default: new Error(`${dialectName} does not support the VECTOR data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR', + postgres: 'VECTOR', + snowflake: new Error('Snowflake VECTOR requires a positive integer "dimension" option.'), }); testDataTypeSql('VECTOR(4)', DataTypes.VECTOR(4), { default: new Error(`${dialectName} does not support the VECTOR data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR(4, *)', + snowflake: 'VECTOR(FLOAT, 4)', + postgres: 'VECTOR(4)', }); testDataTypeSql("VECTOR(3, 'float32')", DataTypes.VECTOR(3, 'float32'), { default: new Error(`${dialectName} does not support the VECTOR data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR(3, FLOAT32)', + snowflake: 'VECTOR(FLOAT, 3)', + postgres: 'VECTOR(3)', }); }); diff --git a/packages/core/test/unit/dialects/postgres/vector.test.js b/packages/core/test/unit/dialects/postgres/vector.test.js new file mode 100644 index 000000000000..89697542f080 --- /dev/null +++ b/packages/core/test/unit/dialects/postgres/vector.test.js @@ -0,0 +1,42 @@ +'use strict'; + +const Support = require('../../../support'); +const { Op, sql } = require('@sequelize/core'); + +const expectsql = Support.expectsql; +const current = Support.sequelize; +const queryGenerator = current.dialect.queryGenerator; + +if (current.dialect.name === 'postgres') { + describe('[Postgres Specific] VECTOR functions', () => { + it('renders l2 distance using pgvector literal', () => { + const where = sql.where(current.l2Distance('embedding', [1, 2, 3]), { + [Op.lt]: 1.5, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + postgres: `vector_l2_distance("embedding", '[1,2,3]'::vector) < 1.5`, + }); + }); + + it('renders cosine distance', () => { + const where = sql.where(current.cosineDistance('embedding', [1, 2, 3]), { + [Op.lt]: 0.5, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + postgres: `vector_cosine_distance("embedding", '[1,2,3]'::vector) < 0.5`, + }); + }); + + it('supports typed arrays', () => { + const where = sql.where(current.innerProduct('embedding', new Float32Array([1, 2, 3])), { + [Op.gt]: 0, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + postgres: `vector_inner_product("embedding", '[1,2,3]'::vector) > 0`, + }); + }); + }); +} diff --git a/packages/core/test/unit/dialects/snowflake/vector.test.js b/packages/core/test/unit/dialects/snowflake/vector.test.js new file mode 100644 index 000000000000..8de4268268c6 --- /dev/null +++ b/packages/core/test/unit/dialects/snowflake/vector.test.js @@ -0,0 +1,43 @@ +'use strict'; + +const Support = require('../../../support'); +const { Op, sql } = require('@sequelize/core'); + +const expectsql = Support.expectsql; +const current = Support.sequelize; +const queryGenerator = current.dialect.queryGenerator; + +if (current.dialect.name === 'snowflake') { + describe('[Snowflake Specific] VECTOR functions', () => { + it('renders L2 distance from array input', () => { + const where = sql.where(current.l2Distance('embedding', [1, 2, 3]), { + [Op.lt]: 5, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + snowflake: 'VECTOR_L2_DISTANCE("embedding", ARRAY_CONSTRUCT(1,2,3)::VECTOR(FLOAT, 3)) < 5', + }); + }); + + it('renders cosine distance using similarity mapping', () => { + const where = sql.where(current.cosineDistance('embedding', [1, 2, 3]), { + [Op.lt]: 0.5, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + snowflake: + '1 - VECTOR_COSINE_SIMILARITY("embedding", ARRAY_CONSTRUCT(1,2,3)::VECTOR(FLOAT, 3)) < 0.5', + }); + }); + + it('supports integer typed arrays', () => { + const where = sql.where(current.l2Distance('embedding', new Int8Array([1, 2, 3])), { + [Op.lt]: 10, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + snowflake: 'VECTOR_L2_DISTANCE("embedding", ARRAY_CONSTRUCT(1,2,3)::VECTOR(INT, 3)) < 10', + }); + }); + }); +} diff --git a/packages/oracle/src/_internal/data-types-overrides.ts b/packages/oracle/src/_internal/data-types-overrides.ts index fcb408b30597..a7b53467461f 100644 --- a/packages/oracle/src/_internal/data-types-overrides.ts +++ b/packages/oracle/src/_internal/data-types-overrides.ts @@ -419,16 +419,12 @@ export class DATEONLY extends BaseTypes.DATEONLY { } export class VECTOR extends BaseTypes.VECTOR { - toSql() { - if (this.options.dimension && this.options.format) { - return `VECTOR(${this.options.dimension}, ${this.options.format.toUpperCase()})`; - } - - if (this.options.dimension) { - return `VECTOR(${this.options.dimension}, *)`; + protected _getSqlOptionParts(): string[] { + if (this.options.dimension == null) { + return []; } - return 'VECTOR'; + return [String(this.options.dimension), this.options.format?.toUpperCase() ?? '*']; } toBindableValue(value: BaseTypes.Vector) { diff --git a/packages/oracle/src/query-generator.js b/packages/oracle/src/query-generator.js index 00a68e79b214..c27178cf1466 100644 --- a/packages/oracle/src/query-generator.js +++ b/packages/oracle/src/query-generator.js @@ -411,12 +411,10 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { attributes.name = `${tableName.schema}.${attributes.name}`; } - const requestedIndexType = - typeof options?.type === 'string' ? options.type.toUpperCase() : undefined; - const requestedAttributeType = - !Array.isArray(attributes) && typeof attributes?.type === 'string' - ? attributes.type.toUpperCase() - : undefined; + const requestedIndexType = toUpperCaseIfString(options?.type); + const requestedAttributeType = !Array.isArray(attributes) + ? toUpperCaseIfString(attributes?.type) + : undefined; const vectorRequested = requestedIndexType === 'VECTOR' || requestedAttributeType === 'VECTOR'; if (!vectorRequested) { @@ -432,8 +430,8 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { options.fields = attributes; } - if (typeof options.type === 'string') { - options.type = options.type.toUpperCase(); + if (requestedIndexType) { + options.type = requestedIndexType; } options.prefix = options.prefix || rawTablename || tableName; @@ -478,9 +476,7 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { options = conformIndex(options); const escapedTableName = this.quoteTable(tableName); - const normalizedUsing = - typeof options.using === 'string' ? options.using.toLowerCase() : undefined; - const using = normalizedUsing ?? 'hnsw'; + const using = toLowerCaseIfString(options.using) ?? 'hnsw'; const usingIsHnsw = using === 'hnsw'; const parameters = []; @@ -1277,6 +1273,14 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { } } +function toUpperCaseIfString(value) { + return typeof value === 'string' ? value.toUpperCase() : undefined; +} + +function toLowerCaseIfString(value) { + return typeof value === 'string' ? value.toLowerCase() : undefined; +} + /* istanbul ignore next */ function throwMethodUndefined(methodName) { throw new Error(`The method "${methodName}" is not defined! Please add it to your sql dialect.`); diff --git a/packages/postgres/src/_internal/data-types-overrides.ts b/packages/postgres/src/_internal/data-types-overrides.ts index da82c5b5cb97..2becce37c31a 100644 --- a/packages/postgres/src/_internal/data-types-overrides.ts +++ b/packages/postgres/src/_internal/data-types-overrides.ts @@ -1,4 +1,5 @@ import type { AbstractDialect, Rangable } from '@sequelize/core'; +import { ValidationErrorItem } from '@sequelize/core'; import { attributeTypeToSql } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/data-types-utils.js'; import type { AbstractDataType, @@ -10,6 +11,7 @@ import * as BaseTypes from '@sequelize/core/_non-semver-use-at-your-own-risk_/ab import { inspect, isBigInt, isNumber, isString } from '@sequelize/utils'; import identity from 'lodash/identity.js'; import assert from 'node:assert'; +import util from 'node:util'; import wkx from 'wkx'; import { PostgresQueryGenerator } from '../query-generator'; import { stringifyHstore } from './hstore.js'; @@ -291,6 +293,75 @@ export class GEOGRAPHY extends BaseTypes.GEOGRAPHY { } } +export class VECTOR extends BaseTypes.VECTOR { + // Sample cross-dialect implementation: pgvector can reuse the shared VECTOR type and only + // add the pgvector-specific validation rules it cares about. + protected _checkOptionSupport(dialect: AbstractDialect): void { + super._checkOptionSupport(dialect); + + if (this.options.format) { + dialect.warnDataTypeIssue( + `${dialect.name} VECTOR ignores the "format" option; pgvector stores values as float vectors.`, + ); + delete this.options.format; + } + } + + validate(value: unknown): asserts value is BaseTypes.Vector { + super.validate(value); + + const dimension = this.options.dimension; + if (dimension == null) { + return; + } + + const length = getVectorLength(value); + if (length !== dimension) { + ValidationErrorItem.throwDataTypeValidationError( + util.format('VECTOR expects values of length %d, but received %d', dimension, length), + ); + } + } + + // pgvector requires finite float values. By reusing the base element validation + // we ensure we still reject non-number inputs before applying the pgvector-specific rules. + protected _validateVectorElement(item: unknown): number { + const numeric = super._validateVectorElement(item); + + if (!Number.isFinite(numeric)) { + ValidationErrorItem.throwDataTypeValidationError( + util.format('VECTOR expects finite numeric elements, but received %O', numeric), + ); + } + + return numeric; + } +} + +function getVectorLength(value: BaseTypes.Vector): number { + if (Array.isArray(value)) { + return value.length; + } + + if ( + value instanceof Int8Array || + value instanceof Uint8Array || + value instanceof Uint8ClampedArray || + value instanceof Int16Array || + value instanceof Uint16Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Float32Array || + value instanceof Float64Array || + value instanceof BigInt64Array || + value instanceof BigUint64Array + ) { + return value.length; + } + + throw new TypeError('Unsupported vector container type'); +} + export class HSTORE extends BaseTypes.HSTORE { toBindableValue(value: AcceptableTypeOf): string { if (value == null) { diff --git a/packages/postgres/src/dialect.ts b/packages/postgres/src/dialect.ts index 912f9a79f421..f614f0920d8d 100644 --- a/packages/postgres/src/dialect.ts +++ b/packages/postgres/src/dialect.ts @@ -113,6 +113,7 @@ export class PostgresDialect extends AbstractDialect< JSONB: true, HSTORE: true, TSVECTOR: true, + VECTOR: true, CITEXT: true, DATETIME: { infinity: true }, DATEONLY: { infinity: true }, diff --git a/packages/postgres/src/query-generator.internal.ts b/packages/postgres/src/query-generator.internal.ts index 137156049876..7a185ced5f82 100644 --- a/packages/postgres/src/query-generator.internal.ts +++ b/packages/postgres/src/query-generator.internal.ts @@ -1,5 +1,8 @@ import { AbstractQueryGeneratorInternal } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-internal.js'; +import type { EscapeOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-typescript.js'; import type { AddLimitOffsetOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator.internal-types.js'; +import type { Fn } from '@sequelize/core/_non-semver-use-at-your-own-risk_/expression-builders/fn.js'; +import util from 'node:util'; import type { PostgresDialect } from './dialect.js'; const TECHNICAL_DATABASE_NAMES = Object.freeze(['postgres']); @@ -33,4 +36,114 @@ export class PostgresQueryGeneratorInternal< return fragment; } + + formatFn(piece: Fn, options?: EscapeOptions): string { + const fnName = piece.fn.toUpperCase(); + const mappedName = POSTGRES_VECTOR_FUNCTION_MAP.get(fnName); + if (!mappedName) { + return super.formatFn(piece, options); + } + + if (piece.args.length !== 2) { + throw new Error(`${fnName} expects exactly 2 arguments`); + } + + const [columnArg, vectorArg] = piece.args; + const columnSql = + typeof columnArg === 'string' + ? this.queryGenerator.quoteIdentifier(columnArg) + : this.queryGenerator.escape(columnArg, options); + const vectorSql = this.#formatVectorArg(vectorArg, fnName); + + return `${mappedName}(${columnSql}, ${vectorSql})`; + } + + #formatVectorArg(arg: unknown, fnName: string): string { + if (Array.isArray(arg)) { + return this.#formatVectorFromArray(arg); + } + + if (ArrayBuffer.isView(arg) && !(arg instanceof DataView) && isNumericTypedArray(arg)) { + return this.#formatVectorFromArray([...arg]); + } + + if (typeof arg === 'string') { + const trimmed = arg.trim(); + if (this.#looksLikeVectorLiteral(trimmed)) { + return trimmed; + } + + if (trimmed.startsWith('[') && trimmed.endsWith(']')) { + try { + const parsed = JSON.parse(trimmed); + if (Array.isArray(parsed)) { + return this.#formatVectorFromArray(parsed); + } + } catch { + // ignore and fall through + } + } + } + + throw new Error( + `${fnName} expects the second argument to be a number array, typed array, or PostgreSQL vector literal`, + ); + } + + // Allow callers to hand-roll literals (e.g. `'[0,1,2]'::vector`) without Sequelize rewriting them. + #looksLikeVectorLiteral(literal: string): boolean { + return literal.endsWith('::vector') || literal.toLowerCase().startsWith('vector('); + } + + // pgvector accepts JSON-ish string literals cast to vector; build one from plain JS arrays. + #formatVectorFromArray(values: unknown[]): string { + if (values.length === 0) { + throw new Error('Vector arguments must contain at least one element'); + } + + const numericValues = values.map(value => { + if (typeof value !== 'number' || Number.isNaN(value)) { + throw new Error(util.format('%O is not a valid vector element', value)); + } + + return value; + }); + + return `'[${numericValues.join(',')}]'::vector`; + } } + +function isNumericTypedArray( + value: ArrayBufferView, +): value is + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array { + return ( + value instanceof Int8Array || + value instanceof Uint8Array || + value instanceof Uint8ClampedArray || + value instanceof Int16Array || + value instanceof Uint16Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Float32Array || + value instanceof Float64Array + ); +} + +// Sample cross-dialect implementation: pgvector can map the generic Sequelize helper names +// onto its own function surface without changing the shared API. +const POSTGRES_VECTOR_FUNCTION_MAP = new Map([ + ['COSINE_DISTANCE', 'vector_cosine_distance'], + ['INNER_PRODUCT', 'vector_inner_product'], + ['L1_DISTANCE', 'vector_l1_distance'], + ['L2_DISTANCE', 'vector_l2_distance'], + ['VECTOR_DISTANCE', 'vector_l2_distance'], +]); diff --git a/packages/snowflake/src/_internal/data-types-overrides.ts b/packages/snowflake/src/_internal/data-types-overrides.ts index 19808726cb6f..301dfccf17cb 100644 --- a/packages/snowflake/src/_internal/data-types-overrides.ts +++ b/packages/snowflake/src/_internal/data-types-overrides.ts @@ -1,7 +1,9 @@ import type { AbstractDialect } from '@sequelize/core'; +import { ValidationErrorItem } from '@sequelize/core'; import type { AcceptedDate } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/data-types.js'; import * as BaseTypes from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/data-types.js'; import maxBy from 'lodash/maxBy.js'; +import util from 'node:util'; export class DATE extends BaseTypes.DATE { toSql() { @@ -95,3 +97,112 @@ export class BIGINT extends BaseTypes.BIGINT { return 'INTEGER'; } } + +// Snowflake accepts VECTOR elements either as FLOAT or INT; normalize the various +// aliases so downstream render & validation logic only has to branch on two cases. +const SNOWFLAKE_FLOAT_FORMATS = new Set(['float', 'float32', 'float64', 'double']); +const SNOWFLAKE_INT_FORMATS = new Set(['int', 'int8', 'int16', 'int32', 'int64', 'integer']); + +export class VECTOR extends BaseTypes.VECTOR { + readonly #elementType: 'FLOAT' | 'INT'; + + constructor(...args: ConstructorParameters) { + super(...args); + + const normalizedFormat = this.#normalizeFormat(this.options.format); + this.#elementType = normalizedFormat === 'int' ? 'INT' : 'FLOAT'; + this.options.format = normalizedFormat; + + // Snowflake requires the dimension to be provided up front; surface a clearer + // error here rather than letting the server reject the DDL later. + if (this.options.dimension == null || !Number.isInteger(this.options.dimension)) { + throw new TypeError('Snowflake VECTOR requires a positive integer "dimension" option.'); + } + + if (this.options.dimension <= 0) { + throw new TypeError( + 'Snowflake VECTOR requires the "dimension" option to be greater than zero.', + ); + } + } + + protected _getSqlOptionParts(): string[] { + // Sample cross-dialect implementation: Snowflake needs VECTOR(type, dimension), + // so it overrides the shared SQL option hook instead of replacing toSql(). + return [this.#elementType, String(this.options.dimension)]; + } + + validate(value: unknown): asserts value is BaseTypes.Vector { + super.validate(value); + + const length = this.#getVectorLength(value); + if (length !== this.options.dimension) { + ValidationErrorItem.throwDataTypeValidationError( + util.format( + 'VECTOR expects values of length %d, but received %d', + this.options.dimension, + length, + ), + ); + } + } + + #normalizeFormat(format: string | undefined): 'float' | 'int' { + if (format == null) { + return 'float'; + } + + const lower = format.toLowerCase(); + if (SNOWFLAKE_FLOAT_FORMATS.has(lower)) { + return 'float'; + } + + if (SNOWFLAKE_INT_FORMATS.has(lower)) { + return 'int'; + } + + throw new TypeError( + `Snowflake VECTOR format "${format}" is not supported. Use "float" (default) or "int".`, + ); + } + + #getVectorLength(value: BaseTypes.Vector): number { + if (Array.isArray(value)) { + return value.length; + } + + if ( + value instanceof Int8Array || + value instanceof Uint8Array || + value instanceof Uint8ClampedArray || + value instanceof Int16Array || + value instanceof Uint16Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Float32Array || + value instanceof Float64Array || + value instanceof BigInt64Array || + value instanceof BigUint64Array + ) { + return value.length; + } + + throw new TypeError('Unsupported vector container type'); + } + + protected _validateVectorElement(item: unknown): number { + const numeric = super._validateVectorElement(item); + + if (this.options.format === 'int' && !Number.isInteger(numeric)) { + ValidationErrorItem.throwDataTypeValidationError( + util.format( + 'VECTOR(INT, %d) only accepts integers, but received %O', + this.options.dimension, + numeric, + ), + ); + } + + return numeric; + } +} diff --git a/packages/snowflake/src/dialect.ts b/packages/snowflake/src/dialect.ts index 2a589dc11f7d..c00f8d55dc06 100644 --- a/packages/snowflake/src/dialect.ts +++ b/packages/snowflake/src/dialect.ts @@ -114,6 +114,7 @@ export class SnowflakeDialect extends AbstractDialect< multiDatabases: true, dataTypes: { COLLATE_BINARY: true, + VECTOR: true, }, REGEXP: true, globalTimeZoneConfig: true, diff --git a/packages/snowflake/src/query-generator.internal.ts b/packages/snowflake/src/query-generator.internal.ts index 6cb037a598fd..a8a2a60e38e6 100644 --- a/packages/snowflake/src/query-generator.internal.ts +++ b/packages/snowflake/src/query-generator.internal.ts @@ -1,5 +1,8 @@ import { AbstractQueryGeneratorInternal } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-internal.js'; +import type { EscapeOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-typescript.js'; import type { AddLimitOffsetOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator.internal-types.js'; +import type { Fn } from '@sequelize/core/_non-semver-use-at-your-own-risk_/expression-builders/fn.js'; +import util from 'node:util'; import type { SnowflakeDialect } from './dialect.js'; const TECHNICAL_SCHEMA_NAMES = Object.freeze([ @@ -32,4 +35,150 @@ export class SnowflakeQueryGeneratorInternal< return fragment; } + + formatFn(piece: Fn, options?: EscapeOptions): string { + const fnName = piece.fn.toUpperCase(); + if (!SNOWFLAKE_VECTOR_FUNCTION_MAP.has(fnName)) { + return super.formatFn(piece, options); + } + + if (piece.args.length !== 2) { + throw new Error(`${fnName} expects exactly 2 arguments`); + } + + const [columnArg, vectorArg] = piece.args; + const columnSql = + typeof columnArg === 'string' + ? this.queryGenerator.quoteIdentifier(columnArg) + : this.queryGenerator.escape(columnArg, options); + const vectorSql = this.#formatVectorArg(vectorArg, fnName); + + const render = SNOWFLAKE_VECTOR_FUNCTION_MAP.get(fnName)!; + + return render(columnSql, vectorSql); + } + + #formatVectorArg(arg: unknown, fnName: string): string { + if (Array.isArray(arg)) { + return this.#formatVectorFromArray(arg, 'FLOAT'); + } + + if (ArrayBuffer.isView(arg) && !(arg instanceof DataView) && isNumericTypedArray(arg)) { + return this.#formatTypedArrayVector(arg); + } + + if (typeof arg === 'string') { + const trimmed = arg.trim(); + if (this.#looksLikeVectorLiteral(trimmed)) { + return trimmed; + } + + if (trimmed.startsWith('[') && trimmed.endsWith(']')) { + try { + const parsed = JSON.parse(trimmed); + if (Array.isArray(parsed)) { + return this.#formatVectorFromArray(parsed, 'FLOAT'); + } + } catch { + // fall through to error below + } + } + } + + throw new Error( + `${fnName} expects the second argument to be a number array, typed array, or VECTOR-compatible SQL literal`, + ); + } + + // Snowflake users can still pass explicit SQL snippets (for instance reusing the result of TO_VECTOR); + // keep the heuristics generous so we do not re-wrap values that already carry proper typing. + #looksLikeVectorLiteral(literal: string): boolean { + return ( + literal.toUpperCase().includes('::VECTOR') || + literal.startsWith('ARRAY_CONSTRUCT') || + literal.startsWith('(') || + literal.startsWith('1 - VECTOR_COSINE_SIMILARITY') || + literal.startsWith('VECTOR_') + ); + } + + // Snowflake needs both an ARRAY_CONSTRUCT expression and an explicit VECTOR(...) cast, + // which we synthesize here to spare API users from having to remember that syntax. + #formatVectorFromArray(values: unknown[], elementType: 'FLOAT' | 'INT'): string { + if (values.length === 0) { + throw new Error('Vector arguments must contain at least one element'); + } + + const numericValues = values.map(value => { + if (typeof value !== 'number' || Number.isNaN(value)) { + throw new Error(util.format('%O is not a valid vector element', value)); + } + + return value; + }); + + return `ARRAY_CONSTRUCT(${numericValues.join(',')})::VECTOR(${elementType}, ${numericValues.length})`; + } + + #formatTypedArrayVector(values: NumericTypedArray): string { + const elementType = isIntegerTypedArray(values) ? 'INT' : 'FLOAT'; + + return this.#formatVectorFromArray([...values], elementType); + } +} + +// Sample cross-dialect implementation: Snowflake can translate the generic Sequelize helper names +// to its own vector functions without changing the shared API surface. +const SNOWFLAKE_VECTOR_FUNCTION_MAP = new Map string>([ + ['COSINE_DISTANCE', (column, vector) => `1 - VECTOR_COSINE_SIMILARITY(${column}, ${vector})`], + ['INNER_PRODUCT', (column, vector) => `VECTOR_INNER_PRODUCT(${column}, ${vector})`], + ['L1_DISTANCE', (column, vector) => `VECTOR_L1_DISTANCE(${column}, ${vector})`], + ['L2_DISTANCE', (column, vector) => `VECTOR_L2_DISTANCE(${column}, ${vector})`], + ['VECTOR_DISTANCE', (column, vector) => `VECTOR_L2_DISTANCE(${column}, ${vector})`], +]); + +function isNumericTypedArray(value: ArrayBufferView): value is NumericTypedArray { + return ( + value instanceof Int8Array || + value instanceof Uint8Array || + value instanceof Uint8ClampedArray || + value instanceof Int16Array || + value instanceof Uint16Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Float32Array || + value instanceof Float64Array + ); +} + +type NumericTypedArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array; + +function isIntegerTypedArray( + value: NumericTypedArray, +): value is + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array { + return ( + value instanceof Int8Array || + value instanceof Uint8Array || + value instanceof Uint8ClampedArray || + value instanceof Int16Array || + value instanceof Uint16Array || + value instanceof Int32Array || + value instanceof Uint32Array + ); } From fa443e4dcdd5b6bccbb0c10588ad59f6f67e1280 Mon Sep 17 00:00:00 2001 From: prajalg Date: Tue, 31 Mar 2026 14:52:48 +0530 Subject: [PATCH 09/49] corrected snowflake sample implementation and unit tests --- .../test/unit/data-types/string-types.test.ts | 2 +- .../unit/dialects/snowflake/vector.test.js | 38 ++++++++++++--- .../src/_internal/data-types-overrides.ts | 11 ++--- .../snowflake/src/query-generator.internal.ts | 46 ++++++++++++------- 4 files changed, 64 insertions(+), 33 deletions(-) diff --git a/packages/core/test/unit/data-types/string-types.test.ts b/packages/core/test/unit/data-types/string-types.test.ts index c63b57df268d..4f54ad71b704 100644 --- a/packages/core/test/unit/data-types/string-types.test.ts +++ b/packages/core/test/unit/data-types/string-types.test.ts @@ -263,7 +263,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat default: new Error(`${dialectName} does not support the VECTOR data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR(3, FLOAT32)', - snowflake: 'VECTOR(FLOAT, 3)', + snowflake: new Error('Snowflake VECTOR format "float32" is not supported. Use "FLOAT" or "INT".'), postgres: 'VECTOR(3)', }); }); diff --git a/packages/core/test/unit/dialects/snowflake/vector.test.js b/packages/core/test/unit/dialects/snowflake/vector.test.js index 8de4268268c6..5f1be25dcc1a 100644 --- a/packages/core/test/unit/dialects/snowflake/vector.test.js +++ b/packages/core/test/unit/dialects/snowflake/vector.test.js @@ -1,5 +1,6 @@ 'use strict'; +const { expect } = require('chai'); const Support = require('../../../support'); const { Op, sql } = require('@sequelize/core'); @@ -15,18 +16,29 @@ if (current.dialect.name === 'snowflake') { }); expectsql(queryGenerator.whereItemsQuery(where), { - snowflake: 'VECTOR_L2_DISTANCE("embedding", ARRAY_CONSTRUCT(1,2,3)::VECTOR(FLOAT, 3)) < 5', + snowflake: 'VECTOR_L2_DISTANCE("embedding", [1,2,3]::VECTOR(FLOAT, 3)) < 5', }); }); - it('renders cosine distance using similarity mapping', () => { - const where = sql.where(current.cosineDistance('embedding', [1, 2, 3]), { - [Op.lt]: 0.5, + it('renders inner product from array input', () => { + const where = sql.where(current.innerProduct('embedding', [1, 2, 3]), { + [Op.gt]: 0, }); expectsql(queryGenerator.whereItemsQuery(where), { - snowflake: - '1 - VECTOR_COSINE_SIMILARITY("embedding", ARRAY_CONSTRUCT(1,2,3)::VECTOR(FLOAT, 3)) < 0.5', + snowflake: 'VECTOR_INNER_PRODUCT("embedding", [1,2,3]::VECTOR(FLOAT, 3)) > 0', + }); + }); + + it('renders documented cosine similarity function', () => { + const where = sql.where( + sql.fn('VECTOR_COSINE_SIMILARITY', sql.attribute('embedding'), [1, 2, 3]), + Op.gt, + 0.5, + ); + + expectsql(queryGenerator.whereItemsQuery(where), { + snowflake: 'VECTOR_COSINE_SIMILARITY("embedding", [1,2,3]::VECTOR(FLOAT, 3)) > 0.5', }); }); @@ -36,8 +48,20 @@ if (current.dialect.name === 'snowflake') { }); expectsql(queryGenerator.whereItemsQuery(where), { - snowflake: 'VECTOR_L2_DISTANCE("embedding", ARRAY_CONSTRUCT(1,2,3)::VECTOR(INT, 3)) < 10', + snowflake: 'VECTOR_L2_DISTANCE("embedding", [1,2,3]::VECTOR(INT, 3)) < 10', }); }); + + it('rejects cosineDistance because Snowflake documents cosine similarity instead', () => { + expect(() => + queryGenerator.formatSqlExpression(current.cosineDistance('embedding', [1, 2, 3])), + ).to.throw(Error, 'COSINE_DISTANCE is not implemented for the Snowflake vector sample'); + }); + + it('rejects vectorDistance because Snowflake does not document a generic vector distance function', () => { + expect(() => + queryGenerator.formatSqlExpression(current.vectorDistance('embedding', [1, 2, 3])), + ).to.throw(Error, 'VECTOR_DISTANCE is not implemented for the Snowflake vector sample'); + }); }); } diff --git a/packages/snowflake/src/_internal/data-types-overrides.ts b/packages/snowflake/src/_internal/data-types-overrides.ts index 301dfccf17cb..f003934719ae 100644 --- a/packages/snowflake/src/_internal/data-types-overrides.ts +++ b/packages/snowflake/src/_internal/data-types-overrides.ts @@ -98,11 +98,6 @@ export class BIGINT extends BaseTypes.BIGINT { } } -// Snowflake accepts VECTOR elements either as FLOAT or INT; normalize the various -// aliases so downstream render & validation logic only has to branch on two cases. -const SNOWFLAKE_FLOAT_FORMATS = new Set(['float', 'float32', 'float64', 'double']); -const SNOWFLAKE_INT_FORMATS = new Set(['int', 'int8', 'int16', 'int32', 'int64', 'integer']); - export class VECTOR extends BaseTypes.VECTOR { readonly #elementType: 'FLOAT' | 'INT'; @@ -153,16 +148,16 @@ export class VECTOR extends BaseTypes.VECTOR { } const lower = format.toLowerCase(); - if (SNOWFLAKE_FLOAT_FORMATS.has(lower)) { + if (lower === 'float') { return 'float'; } - if (SNOWFLAKE_INT_FORMATS.has(lower)) { + if (lower === 'int') { return 'int'; } throw new TypeError( - `Snowflake VECTOR format "${format}" is not supported. Use "float" (default) or "int".`, + `Snowflake VECTOR format "${format}" is not supported. Use "FLOAT" or "INT".`, ); } diff --git a/packages/snowflake/src/query-generator.internal.ts b/packages/snowflake/src/query-generator.internal.ts index a8a2a60e38e6..31e5b88057ee 100644 --- a/packages/snowflake/src/query-generator.internal.ts +++ b/packages/snowflake/src/query-generator.internal.ts @@ -38,7 +38,15 @@ export class SnowflakeQueryGeneratorInternal< formatFn(piece: Fn, options?: EscapeOptions): string { const fnName = piece.fn.toUpperCase(); - if (!SNOWFLAKE_VECTOR_FUNCTION_MAP.has(fnName)) { + const mappedName = + SNOWFLAKE_VECTOR_FUNCTION_MAP.get(fnName) ?? + (SNOWFLAKE_NATIVE_VECTOR_FUNCTIONS.has(fnName) ? fnName : undefined); + + if (!mappedName) { + if (SNOWFLAKE_UNSUPPORTED_VECTOR_FUNCTIONS.has(fnName)) { + throw new Error(`${fnName} is not implemented for the Snowflake vector sample`); + } + return super.formatFn(piece, options); } @@ -53,9 +61,7 @@ export class SnowflakeQueryGeneratorInternal< : this.queryGenerator.escape(columnArg, options); const vectorSql = this.#formatVectorArg(vectorArg, fnName); - const render = SNOWFLAKE_VECTOR_FUNCTION_MAP.get(fnName)!; - - return render(columnSql, vectorSql); + return `${mappedName}(${columnSql}, ${vectorSql})`; } #formatVectorArg(arg: unknown, fnName: string): string { @@ -95,15 +101,14 @@ export class SnowflakeQueryGeneratorInternal< #looksLikeVectorLiteral(literal: string): boolean { return ( literal.toUpperCase().includes('::VECTOR') || - literal.startsWith('ARRAY_CONSTRUCT') || + literal.startsWith('[') || literal.startsWith('(') || - literal.startsWith('1 - VECTOR_COSINE_SIMILARITY') || literal.startsWith('VECTOR_') ); } - // Snowflake needs both an ARRAY_CONSTRUCT expression and an explicit VECTOR(...) cast, - // which we synthesize here to spare API users from having to remember that syntax. + // Snowflake documents vector literals as ARRAY values cast with ::VECTOR(type, dimension), + // e.g. [1,2,3]::VECTOR(FLOAT, 3). #formatVectorFromArray(values: unknown[], elementType: 'FLOAT' | 'INT'): string { if (values.length === 0) { throw new Error('Vector arguments must contain at least one element'); @@ -117,7 +122,7 @@ export class SnowflakeQueryGeneratorInternal< return value; }); - return `ARRAY_CONSTRUCT(${numericValues.join(',')})::VECTOR(${elementType}, ${numericValues.length})`; + return `[${numericValues.join(',')}]::VECTOR(${elementType}, ${numericValues.length})`; } #formatTypedArrayVector(values: NumericTypedArray): string { @@ -127,16 +132,23 @@ export class SnowflakeQueryGeneratorInternal< } } -// Sample cross-dialect implementation: Snowflake can translate the generic Sequelize helper names -// to its own vector functions without changing the shared API surface. -const SNOWFLAKE_VECTOR_FUNCTION_MAP = new Map string>([ - ['COSINE_DISTANCE', (column, vector) => `1 - VECTOR_COSINE_SIMILARITY(${column}, ${vector})`], - ['INNER_PRODUCT', (column, vector) => `VECTOR_INNER_PRODUCT(${column}, ${vector})`], - ['L1_DISTANCE', (column, vector) => `VECTOR_L1_DISTANCE(${column}, ${vector})`], - ['L2_DISTANCE', (column, vector) => `VECTOR_L2_DISTANCE(${column}, ${vector})`], - ['VECTOR_DISTANCE', (column, vector) => `VECTOR_L2_DISTANCE(${column}, ${vector})`], +// Sample cross-dialect implementation: keep only helper mappings that correspond directly to +// documented Snowflake vector functions. +const SNOWFLAKE_VECTOR_FUNCTION_MAP = new Map([ + ['INNER_PRODUCT', 'VECTOR_INNER_PRODUCT'], + ['L1_DISTANCE', 'VECTOR_L1_DISTANCE'], + ['L2_DISTANCE', 'VECTOR_L2_DISTANCE'], ]); +const SNOWFLAKE_NATIVE_VECTOR_FUNCTIONS = new Set([ + 'VECTOR_INNER_PRODUCT', + 'VECTOR_L1_DISTANCE', + 'VECTOR_L2_DISTANCE', + 'VECTOR_COSINE_SIMILARITY', +]); + +const SNOWFLAKE_UNSUPPORTED_VECTOR_FUNCTIONS = new Set(['COSINE_DISTANCE', 'VECTOR_DISTANCE']); + function isNumericTypedArray(value: ArrayBufferView): value is NumericTypedArray { return ( value instanceof Int8Array || From 4ef6f3b1b41728974b760ae327fa189505e2ef24 Mon Sep 17 00:00:00 2001 From: prajalg Date: Tue, 31 Mar 2026 14:58:26 +0530 Subject: [PATCH 10/49] removed sample pg implementation for vector --- .../test/unit/data-types/string-types.test.ts | 7 +- .../unit/dialects/postgres/vector.test.js | 42 ------- .../src/_internal/data-types-overrides.ts | 71 ----------- packages/postgres/src/dialect.ts | 1 - .../postgres/src/query-generator.internal.ts | 113 ------------------ 5 files changed, 3 insertions(+), 231 deletions(-) delete mode 100644 packages/core/test/unit/dialects/postgres/vector.test.js diff --git a/packages/core/test/unit/data-types/string-types.test.ts b/packages/core/test/unit/data-types/string-types.test.ts index 4f54ad71b704..ef18ac64bce8 100644 --- a/packages/core/test/unit/data-types/string-types.test.ts +++ b/packages/core/test/unit/data-types/string-types.test.ts @@ -247,7 +247,6 @@ describe('DataTypes.VECTOR', () => { default: new Error(`${dialectName} does not support the VECTOR data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR', - postgres: 'VECTOR', snowflake: new Error('Snowflake VECTOR requires a positive integer "dimension" option.'), }); @@ -256,15 +255,15 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR(4, *)', snowflake: 'VECTOR(FLOAT, 4)', - postgres: 'VECTOR(4)', }); testDataTypeSql("VECTOR(3, 'float32')", DataTypes.VECTOR(3, 'float32'), { default: new Error(`${dialectName} does not support the VECTOR data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR(3, FLOAT32)', - snowflake: new Error('Snowflake VECTOR format "float32" is not supported. Use "FLOAT" or "INT".'), - postgres: 'VECTOR(3)', + snowflake: new Error( + 'Snowflake VECTOR format "float32" is not supported. Use "FLOAT" or "INT".', + ), }); }); diff --git a/packages/core/test/unit/dialects/postgres/vector.test.js b/packages/core/test/unit/dialects/postgres/vector.test.js deleted file mode 100644 index 89697542f080..000000000000 --- a/packages/core/test/unit/dialects/postgres/vector.test.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -const Support = require('../../../support'); -const { Op, sql } = require('@sequelize/core'); - -const expectsql = Support.expectsql; -const current = Support.sequelize; -const queryGenerator = current.dialect.queryGenerator; - -if (current.dialect.name === 'postgres') { - describe('[Postgres Specific] VECTOR functions', () => { - it('renders l2 distance using pgvector literal', () => { - const where = sql.where(current.l2Distance('embedding', [1, 2, 3]), { - [Op.lt]: 1.5, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - postgres: `vector_l2_distance("embedding", '[1,2,3]'::vector) < 1.5`, - }); - }); - - it('renders cosine distance', () => { - const where = sql.where(current.cosineDistance('embedding', [1, 2, 3]), { - [Op.lt]: 0.5, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - postgres: `vector_cosine_distance("embedding", '[1,2,3]'::vector) < 0.5`, - }); - }); - - it('supports typed arrays', () => { - const where = sql.where(current.innerProduct('embedding', new Float32Array([1, 2, 3])), { - [Op.gt]: 0, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - postgres: `vector_inner_product("embedding", '[1,2,3]'::vector) > 0`, - }); - }); - }); -} diff --git a/packages/postgres/src/_internal/data-types-overrides.ts b/packages/postgres/src/_internal/data-types-overrides.ts index 2becce37c31a..da82c5b5cb97 100644 --- a/packages/postgres/src/_internal/data-types-overrides.ts +++ b/packages/postgres/src/_internal/data-types-overrides.ts @@ -1,5 +1,4 @@ import type { AbstractDialect, Rangable } from '@sequelize/core'; -import { ValidationErrorItem } from '@sequelize/core'; import { attributeTypeToSql } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/data-types-utils.js'; import type { AbstractDataType, @@ -11,7 +10,6 @@ import * as BaseTypes from '@sequelize/core/_non-semver-use-at-your-own-risk_/ab import { inspect, isBigInt, isNumber, isString } from '@sequelize/utils'; import identity from 'lodash/identity.js'; import assert from 'node:assert'; -import util from 'node:util'; import wkx from 'wkx'; import { PostgresQueryGenerator } from '../query-generator'; import { stringifyHstore } from './hstore.js'; @@ -293,75 +291,6 @@ export class GEOGRAPHY extends BaseTypes.GEOGRAPHY { } } -export class VECTOR extends BaseTypes.VECTOR { - // Sample cross-dialect implementation: pgvector can reuse the shared VECTOR type and only - // add the pgvector-specific validation rules it cares about. - protected _checkOptionSupport(dialect: AbstractDialect): void { - super._checkOptionSupport(dialect); - - if (this.options.format) { - dialect.warnDataTypeIssue( - `${dialect.name} VECTOR ignores the "format" option; pgvector stores values as float vectors.`, - ); - delete this.options.format; - } - } - - validate(value: unknown): asserts value is BaseTypes.Vector { - super.validate(value); - - const dimension = this.options.dimension; - if (dimension == null) { - return; - } - - const length = getVectorLength(value); - if (length !== dimension) { - ValidationErrorItem.throwDataTypeValidationError( - util.format('VECTOR expects values of length %d, but received %d', dimension, length), - ); - } - } - - // pgvector requires finite float values. By reusing the base element validation - // we ensure we still reject non-number inputs before applying the pgvector-specific rules. - protected _validateVectorElement(item: unknown): number { - const numeric = super._validateVectorElement(item); - - if (!Number.isFinite(numeric)) { - ValidationErrorItem.throwDataTypeValidationError( - util.format('VECTOR expects finite numeric elements, but received %O', numeric), - ); - } - - return numeric; - } -} - -function getVectorLength(value: BaseTypes.Vector): number { - if (Array.isArray(value)) { - return value.length; - } - - if ( - value instanceof Int8Array || - value instanceof Uint8Array || - value instanceof Uint8ClampedArray || - value instanceof Int16Array || - value instanceof Uint16Array || - value instanceof Int32Array || - value instanceof Uint32Array || - value instanceof Float32Array || - value instanceof Float64Array || - value instanceof BigInt64Array || - value instanceof BigUint64Array - ) { - return value.length; - } - - throw new TypeError('Unsupported vector container type'); -} - export class HSTORE extends BaseTypes.HSTORE { toBindableValue(value: AcceptableTypeOf): string { if (value == null) { diff --git a/packages/postgres/src/dialect.ts b/packages/postgres/src/dialect.ts index f614f0920d8d..912f9a79f421 100644 --- a/packages/postgres/src/dialect.ts +++ b/packages/postgres/src/dialect.ts @@ -113,7 +113,6 @@ export class PostgresDialect extends AbstractDialect< JSONB: true, HSTORE: true, TSVECTOR: true, - VECTOR: true, CITEXT: true, DATETIME: { infinity: true }, DATEONLY: { infinity: true }, diff --git a/packages/postgres/src/query-generator.internal.ts b/packages/postgres/src/query-generator.internal.ts index 7a185ced5f82..137156049876 100644 --- a/packages/postgres/src/query-generator.internal.ts +++ b/packages/postgres/src/query-generator.internal.ts @@ -1,8 +1,5 @@ import { AbstractQueryGeneratorInternal } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-internal.js'; -import type { EscapeOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-typescript.js'; import type { AddLimitOffsetOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator.internal-types.js'; -import type { Fn } from '@sequelize/core/_non-semver-use-at-your-own-risk_/expression-builders/fn.js'; -import util from 'node:util'; import type { PostgresDialect } from './dialect.js'; const TECHNICAL_DATABASE_NAMES = Object.freeze(['postgres']); @@ -36,114 +33,4 @@ export class PostgresQueryGeneratorInternal< return fragment; } - - formatFn(piece: Fn, options?: EscapeOptions): string { - const fnName = piece.fn.toUpperCase(); - const mappedName = POSTGRES_VECTOR_FUNCTION_MAP.get(fnName); - if (!mappedName) { - return super.formatFn(piece, options); - } - - if (piece.args.length !== 2) { - throw new Error(`${fnName} expects exactly 2 arguments`); - } - - const [columnArg, vectorArg] = piece.args; - const columnSql = - typeof columnArg === 'string' - ? this.queryGenerator.quoteIdentifier(columnArg) - : this.queryGenerator.escape(columnArg, options); - const vectorSql = this.#formatVectorArg(vectorArg, fnName); - - return `${mappedName}(${columnSql}, ${vectorSql})`; - } - - #formatVectorArg(arg: unknown, fnName: string): string { - if (Array.isArray(arg)) { - return this.#formatVectorFromArray(arg); - } - - if (ArrayBuffer.isView(arg) && !(arg instanceof DataView) && isNumericTypedArray(arg)) { - return this.#formatVectorFromArray([...arg]); - } - - if (typeof arg === 'string') { - const trimmed = arg.trim(); - if (this.#looksLikeVectorLiteral(trimmed)) { - return trimmed; - } - - if (trimmed.startsWith('[') && trimmed.endsWith(']')) { - try { - const parsed = JSON.parse(trimmed); - if (Array.isArray(parsed)) { - return this.#formatVectorFromArray(parsed); - } - } catch { - // ignore and fall through - } - } - } - - throw new Error( - `${fnName} expects the second argument to be a number array, typed array, or PostgreSQL vector literal`, - ); - } - - // Allow callers to hand-roll literals (e.g. `'[0,1,2]'::vector`) without Sequelize rewriting them. - #looksLikeVectorLiteral(literal: string): boolean { - return literal.endsWith('::vector') || literal.toLowerCase().startsWith('vector('); - } - - // pgvector accepts JSON-ish string literals cast to vector; build one from plain JS arrays. - #formatVectorFromArray(values: unknown[]): string { - if (values.length === 0) { - throw new Error('Vector arguments must contain at least one element'); - } - - const numericValues = values.map(value => { - if (typeof value !== 'number' || Number.isNaN(value)) { - throw new Error(util.format('%O is not a valid vector element', value)); - } - - return value; - }); - - return `'[${numericValues.join(',')}]'::vector`; - } } - -function isNumericTypedArray( - value: ArrayBufferView, -): value is - | Int8Array - | Uint8Array - | Uint8ClampedArray - | Int16Array - | Uint16Array - | Int32Array - | Uint32Array - | Float32Array - | Float64Array { - return ( - value instanceof Int8Array || - value instanceof Uint8Array || - value instanceof Uint8ClampedArray || - value instanceof Int16Array || - value instanceof Uint16Array || - value instanceof Int32Array || - value instanceof Uint32Array || - value instanceof Float32Array || - value instanceof Float64Array - ); -} - -// Sample cross-dialect implementation: pgvector can map the generic Sequelize helper names -// onto its own function surface without changing the shared API. -const POSTGRES_VECTOR_FUNCTION_MAP = new Map([ - ['COSINE_DISTANCE', 'vector_cosine_distance'], - ['INNER_PRODUCT', 'vector_inner_product'], - ['L1_DISTANCE', 'vector_l1_distance'], - ['L2_DISTANCE', 'vector_l2_distance'], - ['VECTOR_DISTANCE', 'vector_l2_distance'], -]); From 362fbc0c33dcc4454bf2fd5b367a7ccbab2cf2fb Mon Sep 17 00:00:00 2001 From: prajalg Date: Thu, 2 Apr 2026 12:12:40 +0530 Subject: [PATCH 11/49] added vector integration test for snowflake --- .../dialects/snowflake/vector.test.js | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 packages/core/test/integration/dialects/snowflake/vector.test.js diff --git a/packages/core/test/integration/dialects/snowflake/vector.test.js b/packages/core/test/integration/dialects/snowflake/vector.test.js new file mode 100644 index 000000000000..c2919c4eeb4a --- /dev/null +++ b/packages/core/test/integration/dialects/snowflake/vector.test.js @@ -0,0 +1,155 @@ +'use strict'; + +const { DataTypes, Op, sql } = require('@sequelize/core'); +const { expect } = require('chai'); +const { getTestDialect, sequelize } = require('../../../support'); + +if (getTestDialect() === 'snowflake') { + describe('[Snowflake Specific] vectors', () => { + describe('FLOAT vectors', () => { + beforeEach(async function () { + this.Item = sequelize.define( + 'SnowflakeVectorItem', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + }, + embeddings: DataTypes.VECTOR(3), + }, + { + tableName: 'SnowflakeVectorItems', + freezeTableName: true, + timestamps: false, + }, + ); + + await this.Item.sync({ force: true }); + await sequelize.query(` + INSERT INTO "SnowflakeVectorItems" ("id", "embeddings") + VALUES + (1, [1,1,1]::VECTOR(FLOAT, 3)), + (2, [5,5,5]::VECTOR(FLOAT, 3)), + (3, [10,10,10]::VECTOR(FLOAT, 3)), + (4, [1,2,3]::VECTOR(FLOAT, 3)) + `); + }); + + it('fetches rows from a vector column', async function () { + const result = await this.Item.findAll({ + order: [['id', 'ASC']], + }); + + expect(result).to.have.length(4); + }); + + it('supports l2 distance filtering through helper methods', async function () { + const result = await this.Item.findAll({ + attributes: ['id'], + where: sql.where(sequelize.l2Distance('embeddings', [1, 2, 3]), Op.lt, 3), + order: [['id', 'ASC']], + }); + + expect(result.map(row => row.get('id'))).to.deep.equal([1, 4]); + }); + + it('supports inner product filtering through helper methods', async function () { + const result = await this.Item.findAll({ + attributes: ['id'], + where: sql.where(sequelize.innerProduct('embeddings', [1, 2, 3]), Op.gt, 20), + order: [['id', 'ASC']], + }); + + expect(result.map(row => row.get('id'))).to.deep.equal([2, 3]); + }); + + it('supports l1 distance filtering through helper methods', async function () { + const result = await this.Item.findAll({ + attributes: ['id'], + where: sql.where(sequelize.l1Distance('embeddings', [1, 2, 3]), Op.lt, 10), + order: [['id', 'ASC']], + }); + + expect(result.map(row => row.get('id'))).to.deep.equal([1, 2, 4]); + }); + + it('supports native cosine similarity filtering', async function () { + const result = await this.Item.findAll({ + attributes: ['id'], + where: sql.where( + sql.fn('VECTOR_COSINE_SIMILARITY', sql.attribute('embeddings'), [1, 2, 3]), + Op.gt, + 0.99, + ), + order: [['id', 'ASC']], + }); + + expect(result.map(row => row.get('id'))).to.deep.equal([4]); + }); + + it('supports typed arrays in vector functions', async function () { + const result = await this.Item.findAll({ + attributes: ['id'], + where: sql.where( + sequelize.l2Distance('embeddings', new Float32Array([1, 2, 3])), + Op.lt, + 3, + ), + order: [['id', 'ASC']], + }); + + expect(result.map(row => row.get('id'))).to.deep.equal([1, 4]); + }); + + it('supports helper methods in ORDER BY', async function () { + const result = await this.Item.findAll({ + attributes: ['id'], + order: [sequelize.l2Distance('embeddings', [1, 2, 3])], + limit: 1, + }); + + expect(result).to.have.length(1); + expect(result[0].get('id')).to.equal(4); + }); + }); + + describe('INT vectors', () => { + beforeEach(async function () { + this.Item = sequelize.define( + 'SnowflakeIntVectorItem', + { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + }, + embeddings: DataTypes.VECTOR(3, 'int'), + }, + { + tableName: 'SnowflakeIntVectorItems', + freezeTableName: true, + timestamps: false, + }, + ); + + await this.Item.sync({ force: true }); + await sequelize.query(` + INSERT INTO "SnowflakeIntVectorItems" ("id", "embeddings") + VALUES + (1, [1,2,3]::VECTOR(INT, 3)), + (2, [4,5,6]::VECTOR(INT, 3)), + (3, [9,9,9]::VECTOR(INT, 3)) + `); + }); + + it('supports integer typed arrays in vector functions', async function () { + const result = await this.Item.findAll({ + attributes: ['id'], + where: sql.where(sequelize.l2Distance('embeddings', new Int32Array([1, 2, 3])), Op.lt, 5), + order: [['id', 'ASC']], + }); + + expect(result.map(row => row.get('id'))).to.deep.equal([1, 2]); + }); + }); + }); +} From 81c828a10e20a383ea495f8ba5fd8b880e96e78b Mon Sep 17 00:00:00 2001 From: prajalg Date: Thu, 2 Apr 2026 15:56:32 +0530 Subject: [PATCH 12/49] added postgres vector sample support --- .../dialects/postgres/vector.test.js | 152 ++++++++++++++++++ .../test/unit/data-types/string-types.test.ts | 3 + .../unit/dialects/postgres/vector.test.js | 142 ++++++++++++++++ .../postgres/src/_internal/data-types-db.ts | 20 +++ .../src/_internal/data-types-overrides.ts | 119 ++++++++++++++ packages/postgres/src/dialect.ts | 1 + .../postgres/src/query-generator.internal.ts | 120 ++++++++++++++ 7 files changed, 557 insertions(+) create mode 100644 packages/core/test/integration/dialects/postgres/vector.test.js create mode 100644 packages/core/test/unit/dialects/postgres/vector.test.js diff --git a/packages/core/test/integration/dialects/postgres/vector.test.js b/packages/core/test/integration/dialects/postgres/vector.test.js new file mode 100644 index 000000000000..7a4b20ace7e1 --- /dev/null +++ b/packages/core/test/integration/dialects/postgres/vector.test.js @@ -0,0 +1,152 @@ +'use strict'; + +const { DataTypes, Op, QueryTypes, sql } = require('@sequelize/core'); +const { expect } = require('chai'); +const semver = require('semver'); +const { getTestDialect, sequelize } = require('../../../support'); + +if (getTestDialect() === 'postgres') { + describe('[Postgres Specific] vectors', () => { + before(async function () { + try { + await sequelize.query('CREATE EXTENSION IF NOT EXISTS vector'); + await sequelize.dialect.connectionManager.refreshDynamicOids(); + } catch { + this.skip(); + } + + const extension = await sequelize.query( + `SELECT extversion FROM pg_extension WHERE extname = 'vector'`, + { type: QueryTypes.SELECT }, + ); + + const rawVersion = extension[0]?.extversion; + this.pgvectorVersion = semver.coerce(rawVersion)?.version ?? null; + }); + + describe('findAll', () => { + beforeEach(async function () { + this.Item = sequelize.define( + 'PgVectorItem', + { + embeddings: DataTypes.VECTOR(4), + }, + { + freezeTableName: true, + timestamps: false, + }, + ); + + await this.Item.sync({ force: true }); + await this.Item.create({ embeddings: [1, 1, 1, 1] }); + await this.Item.create({ embeddings: new Float32Array([1, 2, 3, 3]) }); + }); + + it('fetches rows', async function () { + const result = await this.Item.findAll(); + + expect(result).to.have.length(2); + }); + + it('returns arrays for vector columns', async function () { + const result = await this.Item.findAll({ order: [['id', 'ASC']] }); + + expect(result[0].getDataValue('embeddings')).to.deep.equal([1, 1, 1, 1]); + expect(result[1].getDataValue('embeddings')).to.deep.equal([1, 2, 3, 3]); + }); + }); + + describe('similarity search functions', () => { + beforeEach(async function () { + this.Item = sequelize.define( + 'PgSimilarityItem', + { + embeddings: DataTypes.VECTOR(3), + }, + { + freezeTableName: true, + timestamps: false, + }, + ); + + await this.Item.sync({ force: true }); + await this.Item.create({ embeddings: [1, 1, 1] }); + await this.Item.create({ embeddings: [5, 5, 5] }); + await this.Item.create({ embeddings: [10, 10, 10] }); + await this.Item.create({ embeddings: [1, 2, 3] }); + }); + + it('supports cosine distance filtering', async function () { + const queryVector = [1, 2, 3]; + const result = await this.Item.findAll({ + where: sql.where(sequelize.cosineDistance('embeddings', queryVector), Op.lt, 0.01), + }); + + expect(result).to.have.length(1); + }); + + it('supports inner product filtering', async function () { + const queryVector = [1, 2, 3]; + const result = await this.Item.findAll({ + where: sql.where(sequelize.innerProduct('embeddings', queryVector), Op.gt, 20), + }); + + expect(result).to.have.length(2); + }); + + it('supports l2 distance filtering', async function () { + const queryVector = [1, 2, 3]; + const result = await this.Item.findAll({ + where: sql.where(sequelize.l2Distance('embeddings', queryVector), Op.lt, 3), + }); + + expect(result).to.have.length(2); + }); + + it('supports vectorDistance filtering', async function () { + const queryVector = [1, 2, 3]; + const result = await this.Item.findAll({ + where: sql.where(sequelize.vectorDistance('embeddings', queryVector), Op.lt, 3), + }); + + expect(result).to.have.length(2); + }); + + it('supports l1 distance filtering when pgvector provides it', async function () { + if (!this.pgvectorVersion || !semver.gte(this.pgvectorVersion, '0.7.0')) { + this.skip(); + } + + const queryVector = [1, 2, 3]; + const result = await this.Item.findAll({ + where: sql.where(sequelize.l1Distance('embeddings', queryVector), Op.lt, 10), + }); + + expect(result).to.have.length(3); + }); + + it('accepts typed arrays in vector functions', async function () { + const result = await this.Item.findAll({ + where: sql.where( + sequelize.l2Distance('embeddings', new Float32Array([1, 2, 3])), + Op.lt, + 3, + ), + }); + + expect(result).to.have.length(2); + }); + + it('supports helper methods in ORDER BY', async function () { + const queryVector = [1, 2, 3]; + const result = await this.Item.findAll({ + order: [sequelize.l2Distance('embeddings', queryVector)], + limit: 1, + }); + + expect(result).to.have.length(1); + expect(result[0].getDataValue('embeddings')).to.deep.equal([1, 2, 3]); + }); + }); + }); +} diff --git a/packages/core/test/unit/data-types/string-types.test.ts b/packages/core/test/unit/data-types/string-types.test.ts index ef18ac64bce8..0500eaff41ef 100644 --- a/packages/core/test/unit/data-types/string-types.test.ts +++ b/packages/core/test/unit/data-types/string-types.test.ts @@ -247,6 +247,7 @@ describe('DataTypes.VECTOR', () => { default: new Error(`${dialectName} does not support the VECTOR data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR', + postgres: 'VECTOR', snowflake: new Error('Snowflake VECTOR requires a positive integer "dimension" option.'), }); @@ -254,6 +255,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat default: new Error(`${dialectName} does not support the VECTOR data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR(4, *)', + postgres: 'VECTOR(4)', snowflake: 'VECTOR(FLOAT, 4)', }); @@ -261,6 +263,7 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat default: new Error(`${dialectName} does not support the VECTOR data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR(3, FLOAT32)', + postgres: 'VECTOR(3)', snowflake: new Error( 'Snowflake VECTOR format "float32" is not supported. Use "FLOAT" or "INT".', ), diff --git a/packages/core/test/unit/dialects/postgres/vector.test.js b/packages/core/test/unit/dialects/postgres/vector.test.js new file mode 100644 index 000000000000..d2a60d83742a --- /dev/null +++ b/packages/core/test/unit/dialects/postgres/vector.test.js @@ -0,0 +1,142 @@ +'use strict'; + +const Support = require('../../../support'); +const { DataTypes, Op, sql } = require('@sequelize/core'); +const { expect } = require('chai'); + +const expectsql = Support.expectsql; +const current = Support.sequelize; +const queryGenerator = current.dialect.queryGenerator; + +if (current.dialect.name === 'postgres') { + describe('[Postgres Specific] VECTOR', () => { + describe('VECTOR datatype', () => { + it('renders pgvector SQL without format', () => { + const type = DataTypes.VECTOR(3, 'float32').toDialectDataType(current.dialect); + + expect(type.toSql()).to.equal('VECTOR(3)'); + }); + + it('escapes array values as pgvector literals', () => { + expectsql(queryGenerator.escape([1, 2, 3], { type: DataTypes.VECTOR(3) }), { + postgres: `'[1,2,3]'::vector`, + }); + }); + + it('escapes typed array values as pgvector literals', () => { + expectsql( + queryGenerator.escape(new Float32Array([1, 2, 3]), { type: DataTypes.VECTOR(3) }), + { + postgres: `'[1,2,3]'::vector`, + }, + ); + }); + + it('validates fixed dimensions', () => { + const type = DataTypes.VECTOR(3).toDialectDataType(current.dialect); + + expect(() => type.validate([1, 2, 3])).not.to.throw(); + expect(() => type.validate([1, 2])).to.throw( + Error, + 'VECTOR expects values of length 3, but received 2', + ); + }); + + it('rejects non-finite values', () => { + const type = DataTypes.VECTOR(3).toDialectDataType(current.dialect); + + expect(() => type.validate([1, Infinity, 3])).to.throw( + Error, + 'VECTOR expects finite numeric elements', + ); + }); + + it('parses pgvector values from the database', () => { + const type = DataTypes.VECTOR(3).toDialectDataType(current.dialect); + + expect(type.parseDatabaseValue('[1,2,3]')).to.deep.equal([1, 2, 3]); + }); + }); + + describe('VECTOR functions', () => { + it('renders l2 distance using pgvector operator', () => { + const where = sql.where(current.l2Distance('embedding', [1, 2, 3]), { + [Op.lt]: 1.5, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + postgres: `("embedding" <-> '[1,2,3]'::vector) < 1.5`, + }); + }); + + it('renders cosine distance using pgvector operator', () => { + const where = sql.where(current.cosineDistance('embedding', [1, 2, 3]), { + [Op.lt]: 0.5, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + postgres: `("embedding" <=> '[1,2,3]'::vector) < 0.5`, + }); + }); + + it('renders inner product using pgvector operator', () => { + const where = sql.where(current.innerProduct('embedding', [1, 2, 3]), { + [Op.gt]: 0, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + postgres: `(("embedding" <#> '[1,2,3]'::vector) * -1) > 0`, + }); + }); + + it('renders l1 distance using pgvector operator', () => { + const where = sql.where(current.l1Distance('embedding', [1, 2, 3]), { + [Op.lt]: 10, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + postgres: `("embedding" <+> '[1,2,3]'::vector) < 10`, + }); + }); + + it('maps vectorDistance to l2 distance operator', () => { + const where = sql.where(current.vectorDistance('embedding', [1, 2, 3]), { + [Op.lt]: 1.5, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + postgres: `("embedding" <-> '[1,2,3]'::vector) < 1.5`, + }); + }); + + it('supports typed arrays', () => { + const where = sql.where(current.innerProduct('embedding', new Float32Array([1, 2, 3])), { + [Op.gt]: 0, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + postgres: `(("embedding" <#> '[1,2,3]'::vector) * -1) > 0`, + }); + }); + + it('accepts pgvector literal strings', () => { + const where = sql.where(current.l2Distance('embedding', `'[1,2,3]'::vector`), { + [Op.lt]: 2, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + postgres: `("embedding" <-> '[1,2,3]'::vector) < 2`, + }); + }); + + it('throws for malformed vector literal strings', () => { + expect(() => + queryGenerator.formatSqlExpression(current.l2Distance('embedding', 'not-a-vector')), + ).to.throw( + Error, + 'L2_DISTANCE expects the second argument to be a number array, typed array, or pgvector literal string', + ); + }); + }); + }); +} diff --git a/packages/postgres/src/_internal/data-types-db.ts b/packages/postgres/src/_internal/data-types-db.ts index b3073e4e2ea9..6e2a1798b583 100644 --- a/packages/postgres/src/_internal/data-types-db.ts +++ b/packages/postgres/src/_internal/data-types-db.ts @@ -70,6 +70,12 @@ export function registerPostgresDbDataTypeParsers(dialect: PostgresDialect) { return parseHstore(value); }); + dialect.registerDataTypeParser(['vector'], (value: unknown) => { + assert(typeof value === 'string', 'Expected vector value to be a string'); + + return parseVectorLiteral(value); + }); + const parseInteger = getDataTypeParser(dialect, BaseTypes.INTEGER); dialect.registerDataTypeParser(['int4range'], buildRangeParser(parseInteger)); @@ -86,3 +92,17 @@ export function registerPostgresDbDataTypeParsers(dialect: PostgresDialect) { // The Sequelize DataType specified by the user will do further parsing of the arrays of strings (like convert values to Date objects). dialect.registerDataTypeParser(['tstzrange', 'tsrange', 'daterange'], buildRangeParser(identity)); } + +function parseVectorLiteral(value: string): number[] { + const parsed = JSON.parse(value) as unknown; + assert(Array.isArray(parsed), 'Expected vector value to parse as an array'); + + for (const item of parsed) { + assert( + typeof item === 'number' && Number.isFinite(item), + 'Expected vector elements to be finite numbers', + ); + } + + return parsed; +} diff --git a/packages/postgres/src/_internal/data-types-overrides.ts b/packages/postgres/src/_internal/data-types-overrides.ts index da82c5b5cb97..525515072c7a 100644 --- a/packages/postgres/src/_internal/data-types-overrides.ts +++ b/packages/postgres/src/_internal/data-types-overrides.ts @@ -1,4 +1,5 @@ import type { AbstractDialect, Rangable } from '@sequelize/core'; +import { ValidationErrorItem } from '@sequelize/core'; import { attributeTypeToSql } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/data-types-utils.js'; import type { AbstractDataType, @@ -10,6 +11,7 @@ import * as BaseTypes from '@sequelize/core/_non-semver-use-at-your-own-risk_/ab import { inspect, isBigInt, isNumber, isString } from '@sequelize/utils'; import identity from 'lodash/identity.js'; import assert from 'node:assert'; +import util from 'node:util'; import wkx from 'wkx'; import { PostgresQueryGenerator } from '../query-generator'; import { stringifyHstore } from './hstore.js'; @@ -291,6 +293,103 @@ export class GEOGRAPHY extends BaseTypes.GEOGRAPHY { } } +export class VECTOR extends BaseTypes.VECTOR { + protected _getSqlOptionParts(): string[] { + return this.options.dimension != null ? [String(this.options.dimension)] : []; + } + + protected _checkOptionSupport(dialect: AbstractDialect): void { + super._checkOptionSupport(dialect); + + if ( + this.options.dimension != null && + (!Number.isInteger(this.options.dimension) || this.options.dimension <= 0) + ) { + throw new TypeError( + `${dialect.name} VECTOR requires the "dimension" option to be a positive integer.`, + ); + } + + if (this.options.format) { + dialect.warnDataTypeIssue( + `${dialect.name} VECTOR ignores the "format" option; pgvector uses vector(n).`, + ); + delete this.options.format; + } + } + + validate(value: unknown): asserts value is BaseTypes.Vector { + super.validate(value); + + if (this.options.dimension == null) { + return; + } + + const iterable = this._getVectorIterable(value); + if (iterable == null) { + return; + } + + const values = [...iterable]; + if (values.length !== this.options.dimension) { + ValidationErrorItem.throwDataTypeValidationError( + util.format( + 'VECTOR expects values of length %d, but received %d', + this.options.dimension, + values.length, + ), + ); + } + } + + protected _validateVectorElement(item: unknown): number { + const numeric = super._validateVectorElement(item); + + if (!Number.isFinite(numeric)) { + ValidationErrorItem.throwDataTypeValidationError( + util.format('VECTOR expects finite numeric elements, but received %O', numeric), + ); + } + + return numeric; + } + + toBindableValue(value: BaseTypes.Vector): string { + const iterable = this._getVectorIterable(value); + if (iterable == null) { + throw new TypeError('Unsupported vector container type'); + } + + this.validate(value); + + const values = [...iterable].map(item => this._validateVectorElement(item)); + + return `[${values.join(',')}]`; + } + + escape(value: BaseTypes.Vector): string { + return `${this._getDialect().escapeString(this.toBindableValue(value))}::vector`; + } + + getBindParamSql(value: BaseTypes.Vector, options: BindParamOptions): string { + return `${options.bindParam(this.toBindableValue(value))}::vector`; + } + + parseDatabaseValue(value: unknown): unknown { + if (Array.isArray(value)) { + return value; + } + + if (typeof value === 'string') { + return parseVectorLiteral(value); + } + + throw new Error( + `DataTypes.VECTOR received a non-vector value from the database: ${inspect(value)}`, + ); + } +} + export class HSTORE extends BaseTypes.HSTORE { toBindableValue(value: AcceptableTypeOf): string { if (value == null) { @@ -437,3 +536,23 @@ export class ENUM extends BaseTypes.ENUM { return queryGenerator.pgEnumName(tableName, columnName); } } + +function parseVectorLiteral(value: string): number[] { + const trimmed = value.trim(); + if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) { + throw new Error(`Invalid pgvector literal received from the database: ${value}`); + } + + const parsed = JSON.parse(trimmed) as unknown; + if (!Array.isArray(parsed)) { + throw new Error(`Invalid pgvector literal received from the database: ${value}`); + } + + for (const item of parsed) { + if (typeof item !== 'number' || !Number.isFinite(item)) { + throw new Error(`Invalid pgvector element received from the database: ${inspect(item)}`); + } + } + + return parsed; +} diff --git a/packages/postgres/src/dialect.ts b/packages/postgres/src/dialect.ts index 912f9a79f421..f614f0920d8d 100644 --- a/packages/postgres/src/dialect.ts +++ b/packages/postgres/src/dialect.ts @@ -113,6 +113,7 @@ export class PostgresDialect extends AbstractDialect< JSONB: true, HSTORE: true, TSVECTOR: true, + VECTOR: true, CITEXT: true, DATETIME: { infinity: true }, DATEONLY: { infinity: true }, diff --git a/packages/postgres/src/query-generator.internal.ts b/packages/postgres/src/query-generator.internal.ts index 137156049876..d57fe4467c24 100644 --- a/packages/postgres/src/query-generator.internal.ts +++ b/packages/postgres/src/query-generator.internal.ts @@ -1,5 +1,8 @@ import { AbstractQueryGeneratorInternal } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-internal.js'; +import type { EscapeOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-typescript.js'; import type { AddLimitOffsetOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator.internal-types.js'; +import type { Fn } from '@sequelize/core/_non-semver-use-at-your-own-risk_/expression-builders/fn.js'; +import util from 'node:util'; import type { PostgresDialect } from './dialect.js'; const TECHNICAL_DATABASE_NAMES = Object.freeze(['postgres']); @@ -33,4 +36,121 @@ export class PostgresQueryGeneratorInternal< return fragment; } + + formatFn(piece: Fn, options?: EscapeOptions): string { + const fnName = piece.fn.toUpperCase(); + if (!POSTGRES_VECTOR_FUNCTIONS.has(fnName)) { + return super.formatFn(piece, options); + } + + if (piece.args.length !== 2) { + throw new Error(`${fnName} expects exactly 2 arguments`); + } + + const [columnArg, vectorArg] = piece.args; + const columnSql = + typeof columnArg === 'string' + ? this.queryGenerator.quoteIdentifier(columnArg) + : this.queryGenerator.escape(columnArg, options); + const vectorSql = this.#formatVectorArg(vectorArg, fnName); + + switch (fnName) { + case 'COSINE_DISTANCE': + return `(${columnSql} <=> ${vectorSql})`; + case 'INNER_PRODUCT': + return `((${columnSql} <#> ${vectorSql}) * -1)`; + case 'L1_DISTANCE': + return `(${columnSql} <+> ${vectorSql})`; + case 'L2_DISTANCE': + case 'VECTOR_DISTANCE': + return `(${columnSql} <-> ${vectorSql})`; + default: + return super.formatFn(piece, options); + } + } + + #formatVectorArg(arg: unknown, fnName: string): string { + if (Array.isArray(arg)) { + return this.#formatVectorFromArray(arg); + } + + if (ArrayBuffer.isView(arg) && !(arg instanceof DataView) && isNumericTypedArray(arg)) { + return this.#formatVectorFromArray([...arg]); + } + + if (typeof arg === 'string') { + const trimmed = arg.trim(); + if (this.#looksLikeVectorLiteral(trimmed)) { + return trimmed; + } + + if (trimmed.startsWith('[') && trimmed.endsWith(']')) { + try { + const parsed = JSON.parse(trimmed); + if (Array.isArray(parsed)) { + return this.#formatVectorFromArray(parsed); + } + } catch { + // ignore and fall through + } + } + } + + throw new Error( + `${fnName} expects the second argument to be a number array, typed array, or pgvector literal string`, + ); + } + + #looksLikeVectorLiteral(literal: string): boolean { + return /^'.*'\s*::\s*vector(?:\s*\(\s*\d+\s*\))?$/i.test(literal); + } + + #formatVectorFromArray(values: unknown[]): string { + if (values.length === 0) { + throw new Error('Vector arguments must contain at least one element'); + } + + const numericValues = values.map(value => { + if (typeof value !== 'number' || !Number.isFinite(value)) { + throw new Error(util.format('%O is not a valid vector element', value)); + } + + return value; + }); + + return `'[${numericValues.join(',')}]'::vector`; + } +} + +const POSTGRES_VECTOR_FUNCTIONS = new Set([ + 'COSINE_DISTANCE', + 'INNER_PRODUCT', + 'L1_DISTANCE', + 'L2_DISTANCE', + 'VECTOR_DISTANCE', +]); + +function isNumericTypedArray( + value: ArrayBufferView, +): value is + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array { + return ( + value instanceof Int8Array || + value instanceof Uint8Array || + value instanceof Uint8ClampedArray || + value instanceof Int16Array || + value instanceof Uint16Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Float32Array || + value instanceof Float64Array + ); } From ea99dd61b70b9df25cbd40b383d8d9cb0640a050 Mon Sep 17 00:00:00 2001 From: prajalg Date: Thu, 2 Apr 2026 16:11:11 +0530 Subject: [PATCH 13/49] added console.log for vector extension creation error --- .../core/test/integration/dialects/postgres/vector.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/test/integration/dialects/postgres/vector.test.js b/packages/core/test/integration/dialects/postgres/vector.test.js index 7a4b20ace7e1..eaa1114f2172 100644 --- a/packages/core/test/integration/dialects/postgres/vector.test.js +++ b/packages/core/test/integration/dialects/postgres/vector.test.js @@ -11,8 +11,8 @@ if (getTestDialect() === 'postgres') { try { await sequelize.query('CREATE EXTENSION IF NOT EXISTS vector'); await sequelize.dialect.connectionManager.refreshDynamicOids(); - } catch { - this.skip(); + } catch (error) { + console.log(error); } const extension = await sequelize.query( From f8bb8e0667540cfddce2cfd36141effb1428113b Mon Sep 17 00:00:00 2001 From: prajalg Date: Mon, 6 Apr 2026 14:53:08 +0530 Subject: [PATCH 14/49] removed pg vector integration tests --- .../dialects/postgres/vector.test.js | 152 ------------------ 1 file changed, 152 deletions(-) delete mode 100644 packages/core/test/integration/dialects/postgres/vector.test.js diff --git a/packages/core/test/integration/dialects/postgres/vector.test.js b/packages/core/test/integration/dialects/postgres/vector.test.js deleted file mode 100644 index eaa1114f2172..000000000000 --- a/packages/core/test/integration/dialects/postgres/vector.test.js +++ /dev/null @@ -1,152 +0,0 @@ -'use strict'; - -const { DataTypes, Op, QueryTypes, sql } = require('@sequelize/core'); -const { expect } = require('chai'); -const semver = require('semver'); -const { getTestDialect, sequelize } = require('../../../support'); - -if (getTestDialect() === 'postgres') { - describe('[Postgres Specific] vectors', () => { - before(async function () { - try { - await sequelize.query('CREATE EXTENSION IF NOT EXISTS vector'); - await sequelize.dialect.connectionManager.refreshDynamicOids(); - } catch (error) { - console.log(error); - } - - const extension = await sequelize.query( - `SELECT extversion FROM pg_extension WHERE extname = 'vector'`, - { type: QueryTypes.SELECT }, - ); - - const rawVersion = extension[0]?.extversion; - this.pgvectorVersion = semver.coerce(rawVersion)?.version ?? null; - }); - - describe('findAll', () => { - beforeEach(async function () { - this.Item = sequelize.define( - 'PgVectorItem', - { - embeddings: DataTypes.VECTOR(4), - }, - { - freezeTableName: true, - timestamps: false, - }, - ); - - await this.Item.sync({ force: true }); - await this.Item.create({ embeddings: [1, 1, 1, 1] }); - await this.Item.create({ embeddings: new Float32Array([1, 2, 3, 3]) }); - }); - - it('fetches rows', async function () { - const result = await this.Item.findAll(); - - expect(result).to.have.length(2); - }); - - it('returns arrays for vector columns', async function () { - const result = await this.Item.findAll({ order: [['id', 'ASC']] }); - - expect(result[0].getDataValue('embeddings')).to.deep.equal([1, 1, 1, 1]); - expect(result[1].getDataValue('embeddings')).to.deep.equal([1, 2, 3, 3]); - }); - }); - - describe('similarity search functions', () => { - beforeEach(async function () { - this.Item = sequelize.define( - 'PgSimilarityItem', - { - embeddings: DataTypes.VECTOR(3), - }, - { - freezeTableName: true, - timestamps: false, - }, - ); - - await this.Item.sync({ force: true }); - await this.Item.create({ embeddings: [1, 1, 1] }); - await this.Item.create({ embeddings: [5, 5, 5] }); - await this.Item.create({ embeddings: [10, 10, 10] }); - await this.Item.create({ embeddings: [1, 2, 3] }); - }); - - it('supports cosine distance filtering', async function () { - const queryVector = [1, 2, 3]; - const result = await this.Item.findAll({ - where: sql.where(sequelize.cosineDistance('embeddings', queryVector), Op.lt, 0.01), - }); - - expect(result).to.have.length(1); - }); - - it('supports inner product filtering', async function () { - const queryVector = [1, 2, 3]; - const result = await this.Item.findAll({ - where: sql.where(sequelize.innerProduct('embeddings', queryVector), Op.gt, 20), - }); - - expect(result).to.have.length(2); - }); - - it('supports l2 distance filtering', async function () { - const queryVector = [1, 2, 3]; - const result = await this.Item.findAll({ - where: sql.where(sequelize.l2Distance('embeddings', queryVector), Op.lt, 3), - }); - - expect(result).to.have.length(2); - }); - - it('supports vectorDistance filtering', async function () { - const queryVector = [1, 2, 3]; - const result = await this.Item.findAll({ - where: sql.where(sequelize.vectorDistance('embeddings', queryVector), Op.lt, 3), - }); - - expect(result).to.have.length(2); - }); - - it('supports l1 distance filtering when pgvector provides it', async function () { - if (!this.pgvectorVersion || !semver.gte(this.pgvectorVersion, '0.7.0')) { - this.skip(); - } - - const queryVector = [1, 2, 3]; - const result = await this.Item.findAll({ - where: sql.where(sequelize.l1Distance('embeddings', queryVector), Op.lt, 10), - }); - - expect(result).to.have.length(3); - }); - - it('accepts typed arrays in vector functions', async function () { - const result = await this.Item.findAll({ - where: sql.where( - sequelize.l2Distance('embeddings', new Float32Array([1, 2, 3])), - Op.lt, - 3, - ), - }); - - expect(result).to.have.length(2); - }); - - it('supports helper methods in ORDER BY', async function () { - const queryVector = [1, 2, 3]; - const result = await this.Item.findAll({ - order: [sequelize.l2Distance('embeddings', queryVector)], - limit: 1, - }); - - expect(result).to.have.length(1); - expect(result[0].getDataValue('embeddings')).to.deep.equal([1, 2, 3]); - }); - }); - }); -} From cc852f6203b950f06886fbc7282a138d3926b24f Mon Sep 17 00:00:00 2001 From: prajalg Date: Tue, 7 Apr 2026 12:17:31 +0530 Subject: [PATCH 15/49] addressed review comments --- .../core/src/abstract-dialect/data-types.ts | 61 ++------- .../src/abstract-dialect/query-interface.d.ts | 23 +++- packages/core/src/sequelize.d.ts | 13 ++ .../dialects/oracle/vector.test.js | 75 ++++++----- .../test/unit/data-types/string-types.test.ts | 116 +++++++++--------- .../test/unit/dialects/oracle/vector.test.js | 37 ++++++ .../oracle/src/query-generator.internal.ts | 63 ++++++++-- packages/oracle/src/query.js | 16 +-- 8 files changed, 239 insertions(+), 165 deletions(-) diff --git a/packages/core/src/abstract-dialect/data-types.ts b/packages/core/src/abstract-dialect/data-types.ts index 1977d1891f7a..03084ba14ada 100644 --- a/packages/core/src/abstract-dialect/data-types.ts +++ b/packages/core/src/abstract-dialect/data-types.ts @@ -2849,19 +2849,6 @@ export interface VectorOptions { format?: string; } -type TypedArrayView = - | Int8Array - | Uint8Array - | Uint8ClampedArray - | Int16Array - | Uint16Array - | Int32Array - | Uint32Array - | Float32Array - | Float64Array - | BigInt64Array - | BigUint64Array; - // Dialects may accept plain arrays or any numeric typed array. The generic validator // inspects ArrayBufferView instances without having to enumerate every concrete type here. export type Vector = number[] | ArrayBufferView; @@ -2880,6 +2867,8 @@ export type Vector = number[] | ArrayBufferView; * DataTypes.VECTOR * DataTypes.VECTOR(1536) * DataTypes.VECTOR(1536, 'float32') + * DataTypes.VECTOR({ dimension: 1024, format: 'int' }) + * DataTypes.VECTOR({ dimension: 2048 }) * ``` * * @category DataTypes @@ -2925,6 +2914,7 @@ export class VECTOR extends AbstractDataType { validate(value: unknown): asserts value is Vector { const iterable = this._getVectorIterable(value); + if (iterable) { for (const item of iterable) { this._validateVectorElement(item); @@ -2940,11 +2930,7 @@ export class VECTOR extends AbstractDataType { // Dialects override this hook if they need to coerce custom container types into an iterable sequence. protected _getVectorIterable(value: unknown): Iterable | null { - if (Array.isArray(value)) { - return value; - } - - if (isTypedArrayView(value)) { + if (Array.isArray(value) || isTypedArrayIterable(value)) { return value; } @@ -2971,44 +2957,21 @@ export class VECTOR extends AbstractDataType { // Dialects can override this hook to customize VECTOR SQL rendering without replacing toSql(). protected _getSqlOptionParts(): string[] { - const parts: string[] = []; - - if (this.options.dimension !== undefined) { - parts.push(String(this.options.dimension)); - } - - if (this.options.format !== undefined) { - parts.push(this.options.format.toUpperCase()); - } - - return parts; + return [ + ...(this.options.dimension !== undefined ? [String(this.options.dimension)] : []), + ...(this.options.format !== undefined ? [this.options.format.toUpperCase()] : []), + ]; } toSql(): string { - const optionParts = this._getSqlOptionParts(); + const parts = this._getSqlOptionParts(); - if (optionParts.length === 0) { - return 'VECTOR'; - } - - return `VECTOR(${optionParts.join(', ')})`; + return parts.length ? `VECTOR(${parts.join(', ')})` : 'VECTOR'; } } -function isTypedArrayView(value: unknown): value is TypedArrayView { - return ( - value instanceof Int8Array || - value instanceof Uint8Array || - value instanceof Uint8ClampedArray || - value instanceof Int16Array || - value instanceof Uint16Array || - value instanceof Int32Array || - value instanceof Uint32Array || - value instanceof Float32Array || - value instanceof Float64Array || - value instanceof BigInt64Array || - value instanceof BigUint64Array - ); +function isTypedArrayIterable(value: unknown): value is ArrayBufferView & Iterable { + return ArrayBuffer.isView(value) && !(value instanceof DataView); } function rejectBlobs(value: unknown) { diff --git a/packages/core/src/abstract-dialect/query-interface.d.ts b/packages/core/src/abstract-dialect/query-interface.d.ts index a655696b3120..f879807df9e2 100644 --- a/packages/core/src/abstract-dialect/query-interface.d.ts +++ b/packages/core/src/abstract-dialect/query-interface.d.ts @@ -116,7 +116,7 @@ export interface IndexOptions { parser?: string | null; /** - * Index type. One of `UNIQUE`, `FULLTEXT`, `SPATIAL`, and `VECTOR` (Oracle only). + * Index type. Used by MySQL (`UNIQUE`, `FULLTEXT`, `SPATIAL`) and Oracle (`VECTOR`). */ type?: IndexType | undefined; @@ -178,13 +178,32 @@ export interface IndexOptions { accuracy?: number; /** - * Oracle vector index parameters. + * Implementation-specific options for Oracle VECTOR indexes. */ parameter?: { + /** + * Neighbor list size (HNSW). + */ neighbor?: number; + + /** + * Graph construction effort (HNSW). + */ efconstruction?: number; + + /** + * Number of partitions (IVF). + */ partitions?: number; + + /** + * Sample count per partition (IVF). + */ samplesPerPartition?: number; + + /** + * Minimum vectors per partition (IVF). + */ minVectors?: number; }; diff --git a/packages/core/src/sequelize.d.ts b/packages/core/src/sequelize.d.ts index 1ceeb6c79b61..1d6fca624897 100644 --- a/packages/core/src/sequelize.d.ts +++ b/packages/core/src/sequelize.d.ts @@ -613,10 +613,23 @@ export class Sequelize< * Get the fn for random based on the dialect */ random(): Fn; + + /** + * Build a dialect-specific vector distance function. + * Dialects that advertise `DialectSupportMap.dataTypes.VECTOR` translate this generic helper + * into their native similarity operator (Oracle, Snowflake samples, pgvector, ...). + * + * @param column column name or identifier to compare against + * @param value reference vector as a plain numeric array or typed array + */ cosineDistance(column: string, value: number[] | Exclude): Fn; + innerProduct(column: string, value: number[] | Exclude): Fn; + l1Distance(column: string, value: number[] | Exclude): Fn; + l2Distance(column: string, value: number[] | Exclude): Fn; + vectorDistance(column: string, value: number[] | Exclude): Fn; /** diff --git a/packages/core/test/integration/dialects/oracle/vector.test.js b/packages/core/test/integration/dialects/oracle/vector.test.js index c76155c9efb1..e8fd2167bd12 100644 --- a/packages/core/test/integration/dialects/oracle/vector.test.js +++ b/packages/core/test/integration/dialects/oracle/vector.test.js @@ -16,15 +16,24 @@ if (getTestDialect() === 'oracle') { } }); + const seedModel = async (model, rows) => { + await model.sync({ force: true }); + + for (const row of rows) { + await model.create(row); + } + }; + describe('findAll', () => { beforeEach(async function () { this.Item = sequelize.define('Item', { embeddings: DataTypes.VECTOR(4), }); - await this.Item.sync({ force: true }); - await this.Item.create({ embeddings: new Float32Array([1, 1, 1, 1]) }); - await this.Item.create({ embeddings: new Float32Array([1, 2, 3, 3]) }); + await seedModel(this.Item, [ + { embeddings: new Float32Array([1, 1, 1, 1]) }, + { embeddings: new Float32Array([1, 2, 3, 3]) }, + ]); }); it('fetches rows', async function () { @@ -44,11 +53,12 @@ if (getTestDialect() === 'oracle') { embeddings: DataTypes.VECTOR(3), }); - await this.Item.sync({ force: true }); - await this.Item.create({ embeddings: new Float32Array([1, 1, 1]) }); - await this.Item.create({ embeddings: new Float32Array([5, 5, 5]) }); - await this.Item.create({ embeddings: new Float32Array([10, 10, 10]) }); - await this.Item.create({ embeddings: new Float32Array([1, 2, 3]) }); + await seedModel(this.Item, [ + { embeddings: new Float32Array([1, 1, 1]) }, + { embeddings: new Float32Array([5, 5, 5]) }, + { embeddings: new Float32Array([10, 10, 10]) }, + { embeddings: new Float32Array([1, 2, 3]) }, + ]); }); it('supports cosine distance filtering', async function () { @@ -180,14 +190,12 @@ if (getTestDialect() === 'oracle') { await this.Item.sync({ force: true }); }); - const acceptedInputs = [ - { name: 'number array', value: [1, 2, 3] }, - { name: 'Float32Array', value: new Float32Array([1, 2, 3]) }, - { name: 'Float64Array', value: new Float64Array([1, 2, 3]) }, - { name: 'Int8Array', value: new Int8Array([1, 2, 3]) }, - ]; - - for (const { name, value } of acceptedInputs) { + for (const [name, value] of [ + ['number array', [1, 2, 3]], + ['Float32Array', new Float32Array([1, 2, 3])], + ['Float64Array', new Float64Array([1, 2, 3])], + ['Int8Array', new Int8Array([1, 2, 3])], + ]) { it(`accepts ${name} input`, async function () { await this.Item.create({ embeddings: value }); @@ -196,28 +204,27 @@ if (getTestDialect() === 'oracle') { }); } - it('rejects DataView input', async function () { - const dataView = new DataView(new ArrayBuffer(3)); + it('persists updates performed with a plain number array', async function () { + const item = await this.Item.create({ embeddings: new Float32Array([1, 2, 3]) }); - await expect(this.Item.create({ embeddings: dataView })).to.be.rejectedWith( - Error, - 'is not a valid vector', - ); - }); + await item.update({ embeddings: [4, 5, 6] }); - it('rejects string input', async function () { - await expect(this.Item.create({ embeddings: '1,2,3' })).to.be.rejectedWith( - Error, - 'is not a valid vector', - ); + await item.reload(); + expect(Array.from(item.getDataValue('embeddings'))).to.deep.equal([4, 5, 6]); }); - it('rejects arrays with non-number values', async function () { - await expect(this.Item.create({ embeddings: [1, '2', 3] })).to.be.rejectedWith( - Error, - 'is not a valid vector', - ); - }); + for (const [name, value] of [ + ['DataView', new DataView(new ArrayBuffer(3))], + ['string', '1,2,3'], + ['non-number array', [1, '2', 3]], + ]) { + it(`rejects ${name} input`, async function () { + await expect(this.Item.create({ embeddings: value })).to.be.rejectedWith( + Error, + 'is not a valid vector', + ); + }); + } it('stores Uint8Array in binary vectors with matching bit-dimension', async () => { const BinaryItem = sequelize.define('BinaryVectorInputItem', { diff --git a/packages/core/test/unit/data-types/string-types.test.ts b/packages/core/test/unit/data-types/string-types.test.ts index 0500eaff41ef..4f64b64e1fe0 100644 --- a/packages/core/test/unit/data-types/string-types.test.ts +++ b/packages/core/test/unit/data-types/string-types.test.ts @@ -215,80 +215,82 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat }); }); -describe('DataTypes.VECTOR', () => { - describe('constructor', () => { - it('stores empty options when no args are passed', () => { - const type = DataTypes.VECTOR(); +if (sequelize.dialect.supports?.dataTypes?.VECTOR) { + describe('DataTypes.VECTOR', () => { + describe('constructor', () => { + it('stores empty options when no args are passed', () => { + const type = DataTypes.VECTOR(); - expect(type.options).to.deep.equal({}); - }); + expect(type.options).to.deep.equal({}); + }); - it('stores dimension when only dimension is passed', () => { - const type = DataTypes.VECTOR(4); + it('stores dimension when only dimension is passed', () => { + const type = DataTypes.VECTOR(4); - expect(type.options).to.deep.equal({ dimension: 4 }); - }); + expect(type.options).to.deep.equal({ dimension: 4 }); + }); - it('stores dimension and format when both args are passed', () => { - const type = DataTypes.VECTOR(3, 'float32'); + it('stores dimension and format when both args are passed', () => { + const type = DataTypes.VECTOR(3, 'float32'); - expect(type.options).to.deep.equal({ dimension: 3, format: 'float32' }); - }); + expect(type.options).to.deep.equal({ dimension: 3, format: 'float32' }); + }); - it('supports object-style options', () => { - const type = DataTypes.VECTOR({ dimension: 8, format: 'float64' }); + it('supports object-style options', () => { + const type = DataTypes.VECTOR({ dimension: 8, format: 'float64' }); - expect(type.options).to.deep.equal({ dimension: 8, format: 'float64' }); + expect(type.options).to.deep.equal({ dimension: 8, format: 'float64' }); + }); }); - }); - describe('toSql', () => { - testDataTypeSql('VECTOR', DataTypes.VECTOR, { - default: new Error(`${dialectName} does not support the VECTOR data type. -See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), - oracle: 'VECTOR', - postgres: 'VECTOR', - snowflake: new Error('Snowflake VECTOR requires a positive integer "dimension" option.'), - }); + describe('toSql', () => { + testDataTypeSql('VECTOR', DataTypes.VECTOR, { + default: new Error(`${dialectName} does not support the VECTOR data type. + See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), + oracle: 'VECTOR', + postgres: 'VECTOR', + snowflake: new Error('Snowflake VECTOR requires a positive integer "dimension" option.'), + }); - testDataTypeSql('VECTOR(4)', DataTypes.VECTOR(4), { - default: new Error(`${dialectName} does not support the VECTOR data type. -See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), - oracle: 'VECTOR(4, *)', - postgres: 'VECTOR(4)', - snowflake: 'VECTOR(FLOAT, 4)', - }); + testDataTypeSql('VECTOR(4)', DataTypes.VECTOR(4), { + default: new Error(`${dialectName} does not support the VECTOR data type. + See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), + oracle: 'VECTOR(4, *)', + postgres: 'VECTOR(4)', + snowflake: 'VECTOR(FLOAT, 4)', + }); - testDataTypeSql("VECTOR(3, 'float32')", DataTypes.VECTOR(3, 'float32'), { - default: new Error(`${dialectName} does not support the VECTOR data type. -See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), - oracle: 'VECTOR(3, FLOAT32)', - postgres: 'VECTOR(3)', - snowflake: new Error( - 'Snowflake VECTOR format "float32" is not supported. Use "FLOAT" or "INT".', - ), + testDataTypeSql("VECTOR(3, 'float32')", DataTypes.VECTOR(3, 'float32'), { + default: new Error(`${dialectName} does not support the VECTOR data type. + See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), + oracle: 'VECTOR(3, FLOAT32)', + postgres: 'VECTOR(3)', + snowflake: new Error( + 'Snowflake VECTOR format "float32" is not supported. Use "FLOAT" or "INT".', + ), + }); }); - }); - describe('validate', () => { - it('should throw an error if value is invalid', () => { - const type: DataTypeInstance = DataTypes.VECTOR(); + describe('validate', () => { + it('should throw an error if value is invalid', () => { + const type: DataTypeInstance = DataTypes.VECTOR(); - expect(() => { - type.validate('vector'); - }).to.throw(ValidationErrorItem, "'vector' is not a valid vector"); - }); + expect(() => { + type.validate('vector'); + }).to.throw(ValidationErrorItem, "'vector' is not a valid vector"); + }); - it('should not throw if value is an array', () => { - const type: DataTypeInstance = DataTypes.VECTOR(); + it('should not throw if value is an array', () => { + const type: DataTypeInstance = DataTypes.VECTOR(); - expect(() => type.validate([1, 2, 3])).not.to.throw(); - }); + expect(() => type.validate([1, 2, 3])).not.to.throw(); + }); - it('should not throw if value is a typed array', () => { - const type: DataTypeInstance = DataTypes.VECTOR(); + it('should not throw if value is a typed array', () => { + const type: DataTypeInstance = DataTypes.VECTOR(); - expect(() => type.validate(new Float32Array([1, 2, 3]))).not.to.throw(); + expect(() => type.validate(new Float32Array([1, 2, 3]))).not.to.throw(); + }); }); }); -}); +} diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index b3f6d325be87..d304fcfaf935 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -71,6 +71,20 @@ if (current.dialect.name === 'oracle') { }); }); + it('accepts object-form index definition with lowercase vector type', () => { + expectsql( + queryGenerator.addIndexQuery('Foo', { + fields: ['vec1'], + type: 'vector', + using: 'ivf', + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "Foo" ("vec1") ORGANIZATION NEIGHBOR PARTITION GRAPH', + }, + ); + }); + it('ivf with parameters and target accuracy', () => { expectsql( queryGenerator.addIndexQuery('foo', ['vec1'], { @@ -273,6 +287,16 @@ if (current.dialect.name === 'oracle') { }, ); + testsql( + sql.fn('VECTOR_DISTANCE', sql.attribute('embedding'), new Uint8Array([1, 2, 3])), + { + [Op.lt]: 2, + }, + { + oracle: `VECTOR_DISTANCE("embedding", VECTOR('[1,2,3]')) < 2`, + }, + ); + it('throws when second argument is not a vector input', () => { expect(() => queryGenerator.whereItemsQuery( @@ -293,6 +317,19 @@ if (current.dialect.name === 'oracle') { ).to.throw(Error, 'Invalid value received for the "where" option'); }); + it('throws for malformed VECTOR literal payloads', () => { + expect(() => + queryGenerator.whereItemsQuery( + sql.where( + sql.fn('VECTOR_DISTANCE', sql.attribute('embedding'), `VECTOR('[1,foo,3]')`), + { + [Op.lt]: 2, + }, + ), + ), + ).to.throw(Error, 'Invalid value received for the "where" option'); + }); + it('throws when vector function has too few arguments', () => { expect(() => queryGenerator.formatSqlExpression(sql.fn('VECTOR_DISTANCE', sql.attribute('embedding'))), diff --git a/packages/oracle/src/query-generator.internal.ts b/packages/oracle/src/query-generator.internal.ts index 765929213edd..425e2a556f43 100644 --- a/packages/oracle/src/query-generator.internal.ts +++ b/packages/oracle/src/query-generator.internal.ts @@ -18,6 +18,9 @@ const VECTOR_FUNCTIONS = new Set([ 'VECTOR_DISTANCE', ]); +const VECTOR_ARG_ERROR = + 'expects the second argument to be a vector array, typed array, or VECTOR literal string'; + export class OracleQueryGeneratorInternal< Dialect extends OracleDialect = OracleDialect, > extends AbstractQueryGeneratorInternal { @@ -88,24 +91,20 @@ export class OracleQueryGeneratorInternal< if (typeof arg === 'string') { const trimmed = arg.trim(); - if (!trimmed.toUpperCase().startsWith('VECTOR(')) { - throw new Error( - `${fnName} expects the second argument to be a vector array, typed array, or VECTOR literal string`, - ); + const parsed = parseVectorLiteral(trimmed); + + if (parsed) { + return this.#formatVectorFromIterable(parsed); } - const vectorLiteralRegex = - /^VECTOR\(\s*'\[\s*(?:[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?\s*(?:,\s*[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?\s*)*)?\]'\s*\)$/i; - if (!vectorLiteralRegex.test(trimmed)) { - throw new Error(`${fnName} expects a well-formed VECTOR literal string`); + if (!looksLikeVectorLiteral(trimmed)) { + throw new Error(`${fnName} ${VECTOR_ARG_ERROR}`); } - return trimmed; + throw new Error(`${fnName} expects a well-formed VECTOR literal string`); } - throw new Error( - `${fnName} expects the second argument to be a vector array, typed array, or VECTOR literal string`, - ); + throw new Error(`${fnName} ${VECTOR_ARG_ERROR}`); } // Oracle expects VECTOR('[1,2,3]') literals. Reuse the iterable path for both plain arrays @@ -128,6 +127,46 @@ export class OracleQueryGeneratorInternal< } } +function looksLikeVectorLiteral(input: string): boolean { + const trimmed = input.trim(); + + return trimmed.toUpperCase().startsWith('VECTOR(') && trimmed.endsWith(')'); +} + +function parseVectorLiteral(input: string): number[] | null { + const trimmed = input.trim(); + if (!looksLikeVectorLiteral(trimmed)) { + return null; + } + + const openParenIndex = trimmed.indexOf('('); + const inner = trimmed.slice(openParenIndex + 1, -1).trim(); + if (!inner.startsWith("'") || !inner.endsWith("'")) { + return null; + } + + const body = inner.slice(1, -1); + if (!body.startsWith('[') || !body.endsWith(']')) { + return null; + } + + let values: unknown; + try { + values = JSON.parse(body); + } catch { + return null; + } + + if ( + !Array.isArray(values) || + values.some(value => typeof value !== 'number' || Number.isNaN(value)) + ) { + return null; + } + + return values; +} + function isNumericTypedArray( value: ArrayBufferView, ): value is diff --git a/packages/oracle/src/query.js b/packages/oracle/src/query.js index c5a316d67b5c..1ec8123a3d6f 100644 --- a/packages/oracle/src/query.js +++ b/packages/oracle/src/query.js @@ -678,15 +678,12 @@ export class OracleQuery extends AbstractQuery { // We create the object if (!acc[indexRecord.INDEX_NAME]) { const oracleIndexType = indexRecord.INDEX_TYPE?.toUpperCase(); - const oracleIndexImplType = indexRecord.ITYP_NAME?.toUpperCase(); let type; let method; if (oracleIndexType === 'VECTOR') { type = 'VECTOR'; method = this.#determineVectorMethod(indexRecord.PARAMETERS_LOWER); - } else if (oracleIndexType === 'DOMAIN' && oracleIndexImplType === 'CONTEXT') { - type = 'FULLTEXT'; } else if (oracleIndexType === 'NORMAL' || !oracleIndexType) { type = undefined; } else { @@ -741,18 +738,15 @@ export class OracleQuery extends AbstractQuery { // Parse out the declared index type so consumers can see whether the index is IVF or HNSW. #determineVectorMethod(parametersLower) { if (typeof parametersLower !== 'string') { - return undefined; - } - - if (parametersLower.includes('type ivf')) { - return 'ivf'; + return; } - if (parametersLower.includes('type hnsw')) { - return 'hnsw'; + for (const type of ['ivf', 'hnsw']) { + if (parametersLower.includes(`type ${type}`)) { + return type; + } } - return undefined; } handleInsertQuery(results, metaData) { From a1b6ccc5f5b7f0121051662658ec79a20c624bfe Mon Sep 17 00:00:00 2001 From: prajalg Date: Tue, 7 Apr 2026 12:31:39 +0530 Subject: [PATCH 16/49] lint fix --- packages/oracle/src/query.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/oracle/src/query.js b/packages/oracle/src/query.js index 1ec8123a3d6f..6463ddbecd59 100644 --- a/packages/oracle/src/query.js +++ b/packages/oracle/src/query.js @@ -746,7 +746,6 @@ export class OracleQuery extends AbstractQuery { return type; } } - } handleInsertQuery(results, metaData) { From 7a61a37620fbec08ebbb9cb09e7b8b8905ba8e67 Mon Sep 17 00:00:00 2001 From: prajalg Date: Tue, 7 Apr 2026 13:13:29 +0530 Subject: [PATCH 17/49] made subfunctions within addIndexQuery --- packages/oracle/src/query-generator.js | 191 +++++++++++++++---------- 1 file changed, 114 insertions(+), 77 deletions(-) diff --git a/packages/oracle/src/query-generator.js b/packages/oracle/src/query-generator.js index c27178cf1466..4163e9507d5d 100644 --- a/packages/oracle/src/query-generator.js +++ b/packages/oracle/src/query-generator.js @@ -31,6 +31,7 @@ import { nameIndex } from '@sequelize/core/_non-semver-use-at-your-own-risk_/uti import { OracleQueryGeneratorTypeScript } from './query-generator-typescript.internal'; const CREATE_TABLE_QUERY_SUPPORTED_OPTIONS = new Set(['uniqueKeys']); +const VECTOR_ORGANIZATION_DEFAULT = 'hnsw'; /** * list of reserved words in Oracle DB 21c @@ -412,111 +413,104 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { } const requestedIndexType = toUpperCaseIfString(options?.type); - const requestedAttributeType = !Array.isArray(attributes) - ? toUpperCaseIfString(attributes?.type) - : undefined; + const requestedAttributeType = Array.isArray(attributes) + ? undefined + : toUpperCaseIfString(attributes?.type); - const vectorRequested = requestedIndexType === 'VECTOR' || requestedAttributeType === 'VECTOR'; - if (!vectorRequested) { + if (requestedIndexType !== 'VECTOR' && requestedAttributeType !== 'VECTOR') { return super.addIndexQuery(tableName, attributes, options, rawTablename); } - options ||= {}; + const normalizedOptions = this.#buildVectorIndexOptions( + tableName, + attributes, + options, + rawTablename, + requestedIndexType, + ); - if (!Array.isArray(attributes)) { - options = attributes; - attributes = undefined; - } else { - options.fields = attributes; - } + return joinSQLFragments([ + 'CREATE VECTOR INDEX', + this.quoteIdentifiers(normalizedOptions.name), + `ON ${this.quoteTable(tableName)}`, + `(${normalizedOptions.fieldsSql.join(', ')})`, + 'ORGANIZATION', + normalizedOptions.usingIsHnsw ? 'INMEMORY NEIGHBOR GRAPH' : 'NEIGHBOR PARTITION GRAPH', + normalizedOptions.distance, + normalizedOptions.accuracy, + normalizedOptions.parameters, + ]); + } + + /** + * Normalizes options and SQL fragments needed to build an Oracle VECTOR index. + * + * @param {string|object} tableName + * @param {Array|object|undefined} attributes + * @param {object|undefined} options + * @param {string|undefined} rawTablename + * @param {string|undefined} requestedIndexType + */ + #buildVectorIndexOptions(tableName, attributes, options, rawTablename, requestedIndexType) { + const normalizedOptions = Array.isArray(attributes) + ? { ...options, fields: attributes } + : { ...attributes }; if (requestedIndexType) { - options.type = requestedIndexType; + normalizedOptions.type = requestedIndexType; } - options.prefix = options.prefix || rawTablename || tableName; - if (options.prefix && typeof options.prefix === 'string') { - options.prefix = options.prefix.replaceAll('.', '_'); - options.prefix = options.prefix.replaceAll(/("|')/g, ''); - } + normalizedOptions.prefix = sanitizePrefix( + normalizedOptions.prefix ?? rawTablename ?? tableName, + ); - if (!options.fields || options.fields.length === 0) { + const fields = normalizedOptions.fields ?? []; + if (fields.length === 0) { throw new Error('Vector indexes require at least one indexed field.'); } - const fieldsSql = options.fields.map(field => { + const fieldsSql = fields.map(field => { if (field instanceof BaseSqlExpression) { return this.formatSqlExpression(field); } - if (typeof field === 'string') { - field = { name: field }; - } + const normalizedField = typeof field === 'string' ? { name: field } : { ...field }; - if (field.attribute) { - field.name = field.attribute; + if (normalizedField.attribute) { + normalizedField.name = normalizedField.attribute; } - if (!field.name) { + if (!normalizedField.name) { throw new Error(`The following index field has no name: ${util.inspect(field)}`); } - let result = this.quoteIdentifier(field.name); - if (field.order) { - result += ` ${field.order}`; - } - - return result; + return normalizedField.order + ? `${this.quoteIdentifier(normalizedField.name)} ${normalizedField.order}` + : this.quoteIdentifier(normalizedField.name); }); - if (!options.name) { - options = nameIndex(options, options.prefix); + if (!normalizedOptions.name) { + normalizedOptions.name = nameIndex(normalizedOptions, normalizedOptions.prefix).name; } - options = conformIndex(options); - const escapedTableName = this.quoteTable(tableName); - - const using = toLowerCaseIfString(options.using) ?? 'hnsw'; + const finalizedOptions = conformIndex(normalizedOptions); + const using = toLowerCaseIfString(finalizedOptions.using) ?? VECTOR_ORGANIZATION_DEFAULT; const usingIsHnsw = using === 'hnsw'; - - const parameters = []; - if (options.parameter) { - if (usingIsHnsw) { - parameters.push('type hnsw'); - if (options.parameter.neighbor) { - parameters.push(`neighbor ${options.parameter.neighbor}`); - } - - if (options.parameter.efconstruction) { - parameters.push(`efconstruction ${options.parameter.efconstruction}`); - } - } else { - parameters.push('type ivf'); - if (options.parameter.partitions) { - parameters.push(`NEIGHBOR PARTITION ${options.parameter.partitions}`); - } - - if (options.parameter.samplesPerPartition) { - parameters.push(`SAMPLES_PER_PARTITION ${options.parameter.samplesPerPartition}`); - } - - if (options.parameter.minVectors) { - parameters.push(`MIN_VECTORS_PER_PARTITION ${options.parameter.minVectors}`); - } - } - } - - return joinSQLFragments([ - 'CREATE VECTOR INDEX', - this.quoteIdentifiers(options.name), - `ON ${escapedTableName}`, - `(${fieldsSql.join(', ')})`, - 'ORGANIZATION', - usingIsHnsw ? 'INMEMORY NEIGHBOR GRAPH' : 'NEIGHBOR PARTITION GRAPH', - options.distance ? `WITH DISTANCE ${options.distance}` : undefined, - options.accuracy ? `WITH TARGET ACCURACY ${options.accuracy}` : undefined, - parameters.length > 0 ? `PARAMETERS (${parameters.join(', ')})` : undefined, - ]); + const parameterFragments = buildVectorParameters(finalizedOptions.parameter, usingIsHnsw); + + return { + name: finalizedOptions.name, + fieldsSql, + usingIsHnsw, + distance: finalizedOptions.distance + ? `WITH DISTANCE ${finalizedOptions.distance}` + : undefined, + accuracy: finalizedOptions.accuracy + ? `WITH TARGET ACCURACY ${finalizedOptions.accuracy}` + : undefined, + parameters: + parameterFragments.length > 0 ? `PARAMETERS (${parameterFragments.join(', ')})` : undefined, + }; } // addConstraintQuery(tableName, options) { @@ -1281,6 +1275,49 @@ function toLowerCaseIfString(value) { return typeof value === 'string' ? value.toLowerCase() : undefined; } +function sanitizePrefix(prefix) { + if (typeof prefix !== 'string') { + return prefix; + } + + return prefix.replaceAll('.', '_').replaceAll(/("|')/g, ''); +} + +function buildVectorParameters(parameter, usingIsHnsw) { + const parameters = []; + if (!parameter) { + return parameters; + } + + if (usingIsHnsw) { + parameters.push('type hnsw'); + if (parameter.neighbor) { + parameters.push(`neighbor ${parameter.neighbor}`); + } + + if (parameter.efconstruction) { + parameters.push(`efconstruction ${parameter.efconstruction}`); + } + + return parameters; + } + + parameters.push('type ivf'); + if (parameter.partitions) { + parameters.push(`NEIGHBOR PARTITION ${parameter.partitions}`); + } + + if (parameter.samplesPerPartition) { + parameters.push(`SAMPLES_PER_PARTITION ${parameter.samplesPerPartition}`); + } + + if (parameter.minVectors) { + parameters.push(`MIN_VECTORS_PER_PARTITION ${parameter.minVectors}`); + } + + return parameters; +} + /* istanbul ignore next */ function throwMethodUndefined(methodName) { throw new Error(`The method "${methodName}" is not defined! Please add it to your sql dialect.`); From db072b789e2d7fc2b09b3f86ddd327e24e2b6e35 Mon Sep 17 00:00:00 2001 From: prajalg Date: Tue, 7 Apr 2026 13:28:20 +0530 Subject: [PATCH 18/49] added vector insert/update sql generation unit tests --- .../dialects/oracle/query-generator.test.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/core/test/unit/dialects/oracle/query-generator.test.js b/packages/core/test/unit/dialects/oracle/query-generator.test.js index ff6973519049..919fbe64edae 100644 --- a/packages/core/test/unit/dialects/oracle/query-generator.test.js +++ b/packages/core/test/unit/dialects/oracle/query-generator.test.js @@ -17,6 +17,7 @@ if (dialect.startsWith('oracle')) { const sequelize = Support.createSequelizeInstance(); const dialect = sequelize.dialect; const integerDialect = new DataTypes.INTEGER().toDialectDataType(dialect); + const vectorDialect = DataTypes.VECTOR(3).toDialectDataType(dialect); const suites = { attributesToSQL: [ @@ -408,6 +409,17 @@ if (dialect.startsWith('oracle')) { }, needsSequelize: true, }, + { + arguments: [ + 'myTable', + { embedding: [1, 2, 3] }, + { embedding: { type: vectorDialect } }, + ], + expectation: { + query: 'INSERT INTO "myTable" ("embedding") VALUES ($sequelize_1);', + bind: { sequelize_1: Float64Array.from([1, 2, 3]) }, + }, + }, // Variants when quoteIdentifiers is false { @@ -641,6 +653,19 @@ if (dialect.startsWith('oracle')) { }, needsSequelize: true, }, + { + arguments: [ + 'myTable', + { embedding: [4, 5, 6] }, + { id: 1 }, + {}, + { embedding: { type: vectorDialect } }, + ], + expectation: { + query: 'UPDATE "myTable" SET "embedding"=$sequelize_1 WHERE "id" = $sequelize_2', + bind: { sequelize_1: Float64Array.from([4, 5, 6]), sequelize_2: 1 }, + }, + }, { arguments: [ 'myTable', From cb2570af0cccda7b00b592bad5a22e007cfc9eca Mon Sep 17 00:00:00 2001 From: prajalg Date: Mon, 13 Apr 2026 16:14:43 +0530 Subject: [PATCH 19/49] added more tests --- .../test/unit/dialects/oracle/vector.test.js | 128 ++++++++++++++++++ .../unit/dialects/postgres/vector.test.js | 48 +++++++ .../unit/dialects/snowflake/vector.test.js | 67 +++++++++ packages/core/test/unit/sequelize.test.ts | 17 +++ 4 files changed, 260 insertions(+) diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index d304fcfaf935..4ab64c66ce5c 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -100,6 +100,19 @@ if (current.dialect.name === 'oracle') { ); }); + it('supports explicit vector distance metric', () => { + expectsql( + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'VECTOR', + distance: 'COSINE', + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION INMEMORY NEIGHBOR GRAPH WITH DISTANCE COSINE', + }, + ); + }); + it('treats non-hnsw using as ivf organization', () => { expectsql( queryGenerator.addIndexQuery('foo', ['vec1'], { @@ -167,12 +180,62 @@ if (current.dialect.name === 'oracle') { ); }); + it('supports hnsw parameters in SQL', () => { + expectsql( + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'VECTOR', + using: 'hnsw', + parameter: { neighbor: 8, efconstruction: 32 }, + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION INMEMORY NEIGHBOR GRAPH PARAMETERS (type hnsw, neighbor 8, efconstruction 32)', + }, + ); + }); + + it('supports ordered index fields', () => { + expectsql( + queryGenerator.addIndexQuery('foo', { + fields: [{ name: 'vec1', order: 'DESC' }], + type: 'VECTOR', + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1" DESC) ORGANIZATION INMEMORY NEIGHBOR GRAPH', + }, + ); + }); + + it('supports SQL expression index fields', () => { + expectsql( + queryGenerator.addIndexQuery('foo', { + name: 'foo_vec_expr', + fields: [sql.literal('LOWER("vec1")')], + type: 'VECTOR', + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec_expr" ON "foo" (LOWER("vec1")) ORGANIZATION INMEMORY NEIGHBOR GRAPH', + }, + ); + }); + it('throws when no fields are provided', () => { expect(() => queryGenerator.addIndexQuery('foo', [], { type: 'VECTOR' })).to.throw( Error, 'Vector indexes require at least one indexed field.', ); }); + + it('throws when a vector index field has no name', () => { + expect(() => + queryGenerator.addIndexQuery('foo', { + fields: [{}], + type: 'VECTOR', + }), + ).to.throw(Error, 'The following index field has no name'); + }); }); describe('showIndex metadata', () => { @@ -237,6 +300,71 @@ if (current.dialect.name === 'oracle') { const result = query.handleShowIndexesQuery(rows); expect(result[0].method).to.equal(undefined); }); + + it('maps NORMAL index type to undefined', () => { + const query = buildQuery(); + const rows = [ + { + INDEX_NAME: 'IDX_NORMAL', + INDEX_TYPE: 'NORMAL', + ITYP_NAME: null, + UNIQUENESS: 'NONUNIQUE', + CONSTRAINT_TYPE: null, + TABLE_NAME: 'FOO', + COLUMN_NAME: 'ID', + DESCEND: null, + PARAMETERS_LOWER: null, + }, + ]; + + const result = query.handleShowIndexesQuery(rows); + expect(result[0].type).to.equal(undefined); + }); + + it('keeps non-vector index types', () => { + const query = buildQuery(); + const rows = [ + { + INDEX_NAME: 'IDX_BITMAP', + INDEX_TYPE: 'BITMAP', + ITYP_NAME: null, + UNIQUENESS: 'NONUNIQUE', + CONSTRAINT_TYPE: null, + TABLE_NAME: 'FOO', + COLUMN_NAME: 'ID', + DESCEND: null, + PARAMETERS_LOWER: null, + }, + ]; + + const result = query.handleShowIndexesQuery(rows); + expect(result[0].type).to.equal('BITMAP'); + }); + }); + + describe('VECTOR datatype override hooks', () => { + it('converts array input to Float64Array bind values', () => { + const type = DataTypes.VECTOR(3).toDialectDataType(current.dialect); + const bindValue = type.toBindableValue([1, 2, 3]); + + expect(bindValue).to.be.instanceOf(Float64Array); + expect(Array.from(bindValue)).to.deep.equal([1, 2, 3]); + }); + + it('keeps typed array input as-is for binding', () => { + const type = DataTypes.VECTOR(3).toDialectDataType(current.dialect); + const typed = new Float32Array([1, 2, 3]); + + expect(type.toBindableValue(typed)).to.equal(typed); + }); + + it('returns the Oracle VECTOR bind definition', () => { + const type = DataTypes.VECTOR(3).toDialectDataType(current.dialect); + + expect(type._getBindDef({ DB_TYPE_VECTOR: 'DB_TYPE_VECTOR' })).to.deep.equal({ + type: 'DB_TYPE_VECTOR', + }); + }); }); describe('Vector where clause', () => { diff --git a/packages/core/test/unit/dialects/postgres/vector.test.js b/packages/core/test/unit/dialects/postgres/vector.test.js index d2a60d83742a..c46656f4ab58 100644 --- a/packages/core/test/unit/dialects/postgres/vector.test.js +++ b/packages/core/test/unit/dialects/postgres/vector.test.js @@ -129,6 +129,16 @@ if (current.dialect.name === 'postgres') { }); }); + it('accepts JSON array strings by normalizing to pgvector literals', () => { + const where = sql.where(current.l2Distance('embedding', '[1,2,3]'), { + [Op.lt]: 2, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + postgres: `("embedding" <-> '[1,2,3]'::vector) < 2`, + }); + }); + it('throws for malformed vector literal strings', () => { expect(() => queryGenerator.formatSqlExpression(current.l2Distance('embedding', 'not-a-vector')), @@ -137,6 +147,44 @@ if (current.dialect.name === 'postgres') { 'L2_DISTANCE expects the second argument to be a number array, typed array, or pgvector literal string', ); }); + + it('throws when vector function has too few arguments', () => { + expect(() => + queryGenerator.formatSqlExpression(sql.fn('L2_DISTANCE', sql.attribute('embedding'))), + ).to.throw(Error, 'L2_DISTANCE expects exactly 2 arguments'); + }); + + it('throws when vector function has too many arguments', () => { + expect(() => + queryGenerator.formatSqlExpression( + sql.fn('L2_DISTANCE', sql.attribute('embedding'), [1, 2, 3], [4, 5, 6]), + ), + ).to.throw(Error, 'L2_DISTANCE expects exactly 2 arguments'); + }); + + it('throws for empty vector arrays', () => { + expect(() => queryGenerator.formatSqlExpression(current.l2Distance('embedding', []))).to.throw( + Error, + 'Vector arguments must contain at least one element', + ); + }); + + it('throws for invalid vector array elements', () => { + expect(() => + queryGenerator.formatSqlExpression(current.l2Distance('embedding', [1, '2', 3])), + ).to.throw(Error, 'is not a valid vector element'); + }); + + it('rejects DataView inputs as vector arguments', () => { + expect(() => + queryGenerator.formatSqlExpression( + current.l2Distance('embedding', new DataView(new ArrayBuffer(8))), + ), + ).to.throw( + Error, + 'L2_DISTANCE expects the second argument to be a number array, typed array, or pgvector literal string', + ); + }); }); }); } diff --git a/packages/core/test/unit/dialects/snowflake/vector.test.js b/packages/core/test/unit/dialects/snowflake/vector.test.js index 5f1be25dcc1a..1c179ed26684 100644 --- a/packages/core/test/unit/dialects/snowflake/vector.test.js +++ b/packages/core/test/unit/dialects/snowflake/vector.test.js @@ -52,6 +52,73 @@ if (current.dialect.name === 'snowflake') { }); }); + it('accepts JSON array strings by normalizing to VECTOR literals', () => { + const where = sql.where(current.l2Distance('embedding', '[1,2,3]'), { + [Op.lt]: 10, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + snowflake: 'VECTOR_L2_DISTANCE("embedding", [1,2,3]) < 10', + }); + }); + + it('accepts explicit VECTOR-compatible SQL literals', () => { + const where = sql.where(current.l2Distance('embedding', '[1,2,3]::VECTOR(FLOAT, 3)'), { + [Op.lt]: 10, + }); + + expectsql(queryGenerator.whereItemsQuery(where), { + snowflake: 'VECTOR_L2_DISTANCE("embedding", [1,2,3]::VECTOR(FLOAT, 3)) < 10', + }); + }); + + it('throws when vector function has too few arguments', () => { + expect(() => + queryGenerator.formatSqlExpression(sql.fn('L2_DISTANCE', sql.attribute('embedding'))), + ).to.throw(Error, 'L2_DISTANCE expects exactly 2 arguments'); + }); + + it('throws when vector function has too many arguments', () => { + expect(() => + queryGenerator.formatSqlExpression( + sql.fn('L2_DISTANCE', sql.attribute('embedding'), [1, 2, 3], [4, 5, 6]), + ), + ).to.throw(Error, 'L2_DISTANCE expects exactly 2 arguments'); + }); + + it('throws for empty vector arrays', () => { + expect(() => queryGenerator.formatSqlExpression(current.l2Distance('embedding', []))).to.throw( + Error, + 'Vector arguments must contain at least one element', + ); + }); + + it('throws for invalid vector array elements', () => { + expect(() => + queryGenerator.formatSqlExpression(current.l2Distance('embedding', [1, '2', 3])), + ).to.throw(Error, 'is not a valid vector element'); + }); + + it('throws when second argument is not a vector input', () => { + expect(() => + queryGenerator.formatSqlExpression(current.l2Distance('embedding', 'not-a-vector')), + ).to.throw( + Error, + 'L2_DISTANCE expects the second argument to be a number array, typed array, or VECTOR-compatible SQL literal', + ); + }); + + it('rejects DataView inputs as vector arguments', () => { + expect(() => + queryGenerator.formatSqlExpression( + current.l2Distance('embedding', new DataView(new ArrayBuffer(8))), + ), + ).to.throw( + Error, + 'L2_DISTANCE expects the second argument to be a number array, typed array, or VECTOR-compatible SQL literal', + ); + }); + it('rejects cosineDistance because Snowflake documents cosine similarity instead', () => { expect(() => queryGenerator.formatSqlExpression(current.cosineDistance('embedding', [1, 2, 3])), diff --git a/packages/core/test/unit/sequelize.test.ts b/packages/core/test/unit/sequelize.test.ts index be37de1de609..61754265fa14 100644 --- a/packages/core/test/unit/sequelize.test.ts +++ b/packages/core/test/unit/sequelize.test.ts @@ -106,4 +106,21 @@ describe('Sequelize', () => { }); }); }); + + describe('vector helpers', () => { + it('throws on dialects that do not support VECTOR', () => { + if (sequelize.dialect.supports.dataTypes.VECTOR) { + const fn = sequelize.vectorDistance('embedding', [1, 2, 3]); + + expect(fn.fn).to.equal('VECTOR_DISTANCE'); + + return; + } + + expect(() => sequelize.vectorDistance('embedding', [1, 2, 3])).to.throw( + Error, + `vector_distance for dialect "${sequelize.dialect.name}" is not implemented`, + ); + }); + }); }); From 24682ce1ae7cf18e3420fe72cbaf50b1cf9b37e6 Mon Sep 17 00:00:00 2001 From: prajalg Date: Mon, 13 Apr 2026 16:15:44 +0530 Subject: [PATCH 20/49] fix:prettier --- .../core/test/unit/dialects/oracle/query-generator.test.js | 6 +----- packages/core/test/unit/dialects/postgres/vector.test.js | 7 +++---- packages/core/test/unit/dialects/snowflake/vector.test.js | 7 +++---- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/core/test/unit/dialects/oracle/query-generator.test.js b/packages/core/test/unit/dialects/oracle/query-generator.test.js index 919fbe64edae..94c1e48fe909 100644 --- a/packages/core/test/unit/dialects/oracle/query-generator.test.js +++ b/packages/core/test/unit/dialects/oracle/query-generator.test.js @@ -410,11 +410,7 @@ if (dialect.startsWith('oracle')) { needsSequelize: true, }, { - arguments: [ - 'myTable', - { embedding: [1, 2, 3] }, - { embedding: { type: vectorDialect } }, - ], + arguments: ['myTable', { embedding: [1, 2, 3] }, { embedding: { type: vectorDialect } }], expectation: { query: 'INSERT INTO "myTable" ("embedding") VALUES ($sequelize_1);', bind: { sequelize_1: Float64Array.from([1, 2, 3]) }, diff --git a/packages/core/test/unit/dialects/postgres/vector.test.js b/packages/core/test/unit/dialects/postgres/vector.test.js index c46656f4ab58..ea9b2c4eab25 100644 --- a/packages/core/test/unit/dialects/postgres/vector.test.js +++ b/packages/core/test/unit/dialects/postgres/vector.test.js @@ -163,10 +163,9 @@ if (current.dialect.name === 'postgres') { }); it('throws for empty vector arrays', () => { - expect(() => queryGenerator.formatSqlExpression(current.l2Distance('embedding', []))).to.throw( - Error, - 'Vector arguments must contain at least one element', - ); + expect(() => + queryGenerator.formatSqlExpression(current.l2Distance('embedding', [])), + ).to.throw(Error, 'Vector arguments must contain at least one element'); }); it('throws for invalid vector array elements', () => { diff --git a/packages/core/test/unit/dialects/snowflake/vector.test.js b/packages/core/test/unit/dialects/snowflake/vector.test.js index 1c179ed26684..8c5938fcf743 100644 --- a/packages/core/test/unit/dialects/snowflake/vector.test.js +++ b/packages/core/test/unit/dialects/snowflake/vector.test.js @@ -87,10 +87,9 @@ if (current.dialect.name === 'snowflake') { }); it('throws for empty vector arrays', () => { - expect(() => queryGenerator.formatSqlExpression(current.l2Distance('embedding', []))).to.throw( - Error, - 'Vector arguments must contain at least one element', - ); + expect(() => + queryGenerator.formatSqlExpression(current.l2Distance('embedding', [])), + ).to.throw(Error, 'Vector arguments must contain at least one element'); }); it('throws for invalid vector array elements', () => { From 2b682107801def8954728f186561f7eab96bbd45 Mon Sep 17 00:00:00 2001 From: prajalg Date: Tue, 14 Apr 2026 15:54:31 +0530 Subject: [PATCH 21/49] removed isNumericTypedArray function and added test for binds in vector distance function --- .../dialects/oracle/vector.test.js | 29 ++++++++++++++++++- .../test/unit/dialects/oracle/vector.test.js | 10 +++++++ .../oracle/src/query-generator.internal.ts | 27 +---------------- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/packages/core/test/integration/dialects/oracle/vector.test.js b/packages/core/test/integration/dialects/oracle/vector.test.js index e8fd2167bd12..bccb26fb2ec1 100644 --- a/packages/core/test/integration/dialects/oracle/vector.test.js +++ b/packages/core/test/integration/dialects/oracle/vector.test.js @@ -1,6 +1,6 @@ 'use strict'; -const { DataTypes, Op, sql } = require('@sequelize/core'); +const { DataTypes, Op, QueryTypes, sql } = require('@sequelize/core'); const { expect } = require('chai'); const semver = require('semver'); const { getTestDialect, sequelize } = require('../../../support'); @@ -136,6 +136,21 @@ if (getTestDialect() === 'oracle') { } }); + it('supports raw SQL bind parameters for VECTOR_DISTANCE query vectors', async () => { + const rows = await sequelize.query( + `SELECT "id" FROM "Items" WHERE VECTOR_DISTANCE("embeddings", $queryVector) < $threshold ORDER BY "id"`, + { + bind: { + queryVector: Float32Array.from([1, 2, 3]), + threshold: 0.05, + }, + type: QueryTypes.SELECT, + }, + ); + + expect(rows).to.have.length(1); + }); + it('accepts valid VECTOR literal strings in vector functions', async function () { const result = await this.Item.findAll({ where: sql.where( @@ -160,6 +175,18 @@ if (getTestDialect() === 'oracle') { ).to.be.rejected; }); + it('rejects unsupported integer typed arrays as the query vector argument', async function () { + await expect( + this.Item.findAll({ + where: sql.where( + sql.fn('VECTOR_DISTANCE', sql.attribute('embeddings'), new Int8Array([1, 2, 3])), + Op.lt, + 2, + ), + }), + ).to.be.rejected; + }); + it('supports all vector helper methods in ORDER BY', async function () { const queryVector = [1, 2, 3]; const helpers = [ diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index 4ab64c66ce5c..4e5fba3865c2 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -425,6 +425,16 @@ if (current.dialect.name === 'oracle') { }, ); + it('throws when second argument is an unsupported integer typed array', () => { + expect(() => + queryGenerator.whereItemsQuery( + sql.where(sql.fn('VECTOR_DISTANCE', sql.attribute('embedding'), new Int8Array([1])), { + [Op.lt]: 2, + }), + ), + ).to.throw(Error, 'Invalid value received for the "where" option'); + }); + it('throws when second argument is not a vector input', () => { expect(() => queryGenerator.whereItemsQuery( diff --git a/packages/oracle/src/query-generator.internal.ts b/packages/oracle/src/query-generator.internal.ts index 425e2a556f43..73e09658581e 100644 --- a/packages/oracle/src/query-generator.internal.ts +++ b/packages/oracle/src/query-generator.internal.ts @@ -85,7 +85,7 @@ export class OracleQueryGeneratorInternal< return this.#formatVectorFromIterable(arg); } - if (ArrayBuffer.isView(arg) && !(arg instanceof DataView) && isNumericTypedArray(arg)) { + if (arg instanceof Float32Array || arg instanceof Float64Array || arg instanceof Uint8Array) { return this.#formatVectorFromIterable(arg); } @@ -166,28 +166,3 @@ function parseVectorLiteral(input: string): number[] | null { return values; } - -function isNumericTypedArray( - value: ArrayBufferView, -): value is - | Int8Array - | Uint8Array - | Uint8ClampedArray - | Int16Array - | Uint16Array - | Int32Array - | Uint32Array - | Float32Array - | Float64Array { - return ( - value instanceof Int8Array || - value instanceof Uint8Array || - value instanceof Uint8ClampedArray || - value instanceof Int16Array || - value instanceof Uint16Array || - value instanceof Int32Array || - value instanceof Uint32Array || - value instanceof Float32Array || - value instanceof Float64Array - ); -} From e1625c761602c00392093d310f01b18a526a725a Mon Sep 17 00:00:00 2001 From: prajalg Date: Thu, 16 Apr 2026 10:03:25 +0530 Subject: [PATCH 22/49] expanded acronyms + lint fix --- packages/core/src/abstract-dialect/query-interface.d.ts | 4 ++++ packages/oracle/src/query-generator.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/abstract-dialect/query-interface.d.ts b/packages/core/src/abstract-dialect/query-interface.d.ts index f879807df9e2..030a442f3ff0 100644 --- a/packages/core/src/abstract-dialect/query-interface.d.ts +++ b/packages/core/src/abstract-dialect/query-interface.d.ts @@ -179,6 +179,10 @@ export interface IndexOptions { /** * Implementation-specific options for Oracle VECTOR indexes. + * + * Acronyms: + * - HNSW: Hierarchical Navigable Small World + * - IVF: Inverted File Flat */ parameter?: { /** diff --git a/packages/oracle/src/query-generator.js b/packages/oracle/src/query-generator.js index 97006974a631..441758456cb8 100644 --- a/packages/oracle/src/query-generator.js +++ b/packages/oracle/src/query-generator.js @@ -27,8 +27,8 @@ import { getObjectFromMap, } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/object.js'; import { defaultValueSchemable } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/query-builder-utils.js'; -import { pojo } from '@sequelize/utils'; import { nameIndex } from '@sequelize/core/_non-semver-use-at-your-own-risk_/utils/string.js'; +import { pojo } from '@sequelize/utils'; import { OracleQueryGeneratorTypeScript } from './query-generator-typescript.internal'; const CREATE_TABLE_QUERY_SUPPORTED_OPTIONS = new Set(['uniqueKeys']); From 3f0d6877db57794c536be1659a110a48ae0c934e Mon Sep 17 00:00:00 2001 From: prajalg Date: Sat, 18 Apr 2026 02:20:16 +0530 Subject: [PATCH 23/49] restructured + added more tests --- .../core/src/abstract-dialect/data-types.ts | 136 +++++++++++------- .../dialects/oracle/vector.test.js | 36 +++++ .../test/unit/data-types/string-types.test.ts | 64 ++++++++- .../test/unit/dialects/oracle/vector.test.js | 37 +++++ .../unit/dialects/postgres/vector.test.js | 12 +- .../unit/dialects/snowflake/vector.test.js | 22 ++- .../src/_internal/data-types-overrides.ts | 40 +++++- .../oracle/src/query-generator.internal.ts | 4 +- .../src/_internal/data-types-overrides.ts | 34 +++-- .../src/_internal/data-types-overrides.ts | 79 +++++----- .../snowflake/src/query-generator.internal.ts | 2 +- 11 files changed, 346 insertions(+), 120 deletions(-) diff --git a/packages/core/src/abstract-dialect/data-types.ts b/packages/core/src/abstract-dialect/data-types.ts index 4572b8c098d9..782aab2daba1 100644 --- a/packages/core/src/abstract-dialect/data-types.ts +++ b/packages/core/src/abstract-dialect/data-types.ts @@ -2884,44 +2884,8 @@ export type Vector = number[] | ArrayBufferView; * * @category DataTypes */ -export class VECTOR extends AbstractDataType { - /** @hidden */ - static readonly [DataTypeIdentifier]: string = 'VECTOR'; - - readonly options: VectorOptions; - - constructor(); - constructor(dimension: number, format?: string); - constructor(options: VectorOptions); - // we have to define the constructor overloads using tuples due to a TypeScript limitation - // https://github.com/microsoft/TypeScript/issues/29732, to play nice with classToInvokable. - /** @hidden */ - constructor( - ...args: - | [] - | [dimension: number] - | [dimension: number, format: string] - | [options: VectorOptions] - ); - constructor(dimensionOrOptions?: number | VectorOptions, format?: string) { - super(); - - if (typeof dimensionOrOptions === 'object') { - this.options = { - ...(dimensionOrOptions.dimension !== undefined - ? { dimension: dimensionOrOptions.dimension } - : {}), - ...(dimensionOrOptions.format !== undefined ? { format: dimensionOrOptions.format } : {}), - }; - - return; - } - - this.options = { - ...(dimensionOrOptions !== undefined ? { dimension: dimensionOrOptions } : {}), - ...(format !== undefined ? { format } : {}), - }; - } +export abstract class AbstractVECTORBase extends AbstractDataType { + readonly options!: TOptions; validate(value: unknown): asserts value is Vector { const iterable = this._getVectorIterable(value); @@ -2939,7 +2903,6 @@ export class VECTOR extends AbstractDataType { ); } - // Dialects override this hook if they need to coerce custom container types into an iterable sequence. protected _getVectorIterable(value: unknown): Iterable | null { if (Array.isArray(value) || isTypedArrayIterable(value)) { return value; @@ -2948,10 +2911,8 @@ export class VECTOR extends AbstractDataType { return null; } - // Returns the validated numeric value so dialect overrides can add additional checks - // (integer-only, finite only, etc.) while still reusing the base logic. protected _validateVectorElement(item: unknown): number { - if (typeof item !== 'number' || Number.isNaN(item)) { + if (typeof item !== 'number' || !Number.isFinite(item)) { ValidationErrorItem.throwDataTypeValidationError( util.format('%O is not a valid vector', item), ); @@ -2960,24 +2921,103 @@ export class VECTOR extends AbstractDataType { return item; } + protected _validateDimension(dimension: number, max?: number): number { + if (!Number.isInteger(dimension) || dimension <= 0) { + throw new TypeError(`Invalid VECTOR dimension: ${dimension}`); + } + + if (max !== undefined && dimension > max) { + throw new TypeError(`Invalid VECTOR dimension: ${dimension} (max ${max})`); + } + + return dimension; + } + protected _checkOptionSupport(dialect: AbstractDialect) { if (!dialect.supports.dataTypes.VECTOR) { throwUnsupportedDataType(dialect, 'VECTOR'); } } - // Dialects can override this hook to customize VECTOR SQL rendering without replacing toSql(). + protected abstract _getTypeName(): string; + protected _getSqlOptionParts(): string[] { - return [ - ...(this.options.dimension !== undefined ? [String(this.options.dimension)] : []), - ...(this.options.format !== undefined ? [this.options.format.toUpperCase()] : []), - ]; + return []; } toSql(): string { const parts = this._getSqlOptionParts(); - return parts.length ? `VECTOR(${parts.join(', ')})` : 'VECTOR'; + return parts.length ? `${this._getTypeName()}(${parts.join(', ')})` : this._getTypeName(); + } +} + +export class VECTOR extends AbstractVECTORBase { + /** @hidden */ + static readonly [DataTypeIdentifier]: string = 'VECTOR'; + + readonly options: VectorOptions; + + constructor(); + constructor(dimension: number, format?: string); + constructor(options: VectorOptions); + /** @hidden */ + constructor( + ...args: + | [] + | [dimension: number] + | [dimension: number, format: string] + | [options: VectorOptions] + ); + constructor(dimensionOrOptions?: number | VectorOptions, format?: string) { + super(); + + if (typeof dimensionOrOptions === 'object' && dimensionOrOptions !== null) { + this.options = { + ...(dimensionOrOptions.dimension !== undefined + ? { dimension: this._validateDimension(dimensionOrOptions.dimension) } + : {}), + ...(dimensionOrOptions.format !== undefined + ? { format: this._validateFormat(dimensionOrOptions.format) } + : {}), + }; + + return; + } + + this.options = { + ...(dimensionOrOptions !== undefined + ? { dimension: this._validateDimension(dimensionOrOptions) } + : {}), + ...(format !== undefined ? { format: this._validateFormat(format) } : {}), + }; + } + + protected _getTypeName(): string { + return 'VECTOR'; + } + + protected _validateFormat(format: string): string { + const normalized = format.trim().toLowerCase(); + + switch (normalized) { + case 'float32': + case 'float64': + case 'int8': + case 'int16': + case 'int32': + case 'binary': + return normalized; + default: + throw new TypeError(`Invalid VECTOR format: ${format}`); + } + } + + protected override _getSqlOptionParts(): string[] { + return [ + ...(this.options.dimension !== undefined ? [String(this.options.dimension)] : []), + ...(this.options.format !== undefined ? [this.options.format.toUpperCase()] : []), + ]; } } diff --git a/packages/core/test/integration/dialects/oracle/vector.test.js b/packages/core/test/integration/dialects/oracle/vector.test.js index bccb26fb2ec1..2b8b682d4ee4 100644 --- a/packages/core/test/integration/dialects/oracle/vector.test.js +++ b/packages/core/test/integration/dialects/oracle/vector.test.js @@ -175,6 +175,18 @@ if (getTestDialect() === 'oracle') { ).to.be.rejected; }); + it('rejects non-finite vector elements in vector functions', async function () { + await expect( + this.Item.findAll({ + where: sql.where( + sql.fn('VECTOR_DISTANCE', sql.attribute('embeddings'), [1, Infinity, 3]), + Op.lt, + 2, + ), + }), + ).to.be.rejected; + }); + it('rejects unsupported integer typed arrays as the query vector argument', async function () { await expect( this.Item.findAll({ @@ -264,6 +276,30 @@ if (getTestDialect() === 'oracle') { const row = await BinaryItem.findOne(); expect(row).to.not.equal(null); }); + + it('accepts object-style binary format options', async () => { + const BinaryItem = sequelize.define('BinaryVectorInputItemObjectStyle', { + embeddings: DataTypes.VECTOR({ dimension: 24, format: 'binary' }), + }); + + await BinaryItem.sync({ force: true }); + await BinaryItem.create({ embeddings: new Uint8Array([1, 2, 3]) }); + + const row = await BinaryItem.findOne(); + expect(row).to.not.equal(null); + }); + + it('accepts uppercase binary format in constructor arguments', async () => { + const BinaryItem = sequelize.define('BinaryVectorInputItemUppercase', { + embeddings: DataTypes.VECTOR(24, 'BINARY'), + }); + + await BinaryItem.sync({ force: true }); + await BinaryItem.create({ embeddings: new Uint8Array([1, 2, 3]) }); + + const row = await BinaryItem.findOne(); + expect(row).to.not.equal(null); + }); }); describe('vector indexes', () => { diff --git a/packages/core/test/unit/data-types/string-types.test.ts b/packages/core/test/unit/data-types/string-types.test.ts index 4f64b64e1fe0..c90e66e34db9 100644 --- a/packages/core/test/unit/data-types/string-types.test.ts +++ b/packages/core/test/unit/data-types/string-types.test.ts @@ -241,6 +241,47 @@ if (sequelize.dialect.supports?.dataTypes?.VECTOR) { expect(type.options).to.deep.equal({ dimension: 8, format: 'float64' }); }); + + it('supports object-style options with only dimension', () => { + const type = DataTypes.VECTOR({ dimension: 8 }); + + expect(type.options).to.deep.equal({ dimension: 8 }); + }); + + it('normalizes binary format casing in constructor options', () => { + const type = DataTypes.VECTOR(24, 'BINARY'); + + expect(type.options).to.deep.equal({ dimension: 24, format: 'binary' }); + }); + + it('rejects invalid dimensions', () => { + expect(() => DataTypes.VECTOR(0)).to.throw(TypeError, 'Invalid VECTOR dimension'); + }); + + it('rejects invalid object-style dimensions', () => { + expect(() => DataTypes.VECTOR({ dimension: 0 })).to.throw( + TypeError, + 'Invalid VECTOR dimension', + ); + expect(() => DataTypes.VECTOR({ dimension: 1.5 })).to.throw( + TypeError, + 'Invalid VECTOR dimension', + ); + }); + + it('rejects unknown formats', () => { + expect(() => DataTypes.VECTOR(3, 'float32) DROP TABLE x; --')).to.throw( + TypeError, + 'Invalid VECTOR format', + ); + }); + + it('rejects unknown object-style formats', () => { + expect(() => DataTypes.VECTOR({ dimension: 3, format: 'drop table x' })).to.throw( + TypeError, + 'Invalid VECTOR format', + ); + }); }); describe('toSql', () => { @@ -255,7 +296,7 @@ if (sequelize.dialect.supports?.dataTypes?.VECTOR) { testDataTypeSql('VECTOR(4)', DataTypes.VECTOR(4), { default: new Error(`${dialectName} does not support the VECTOR data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), - oracle: 'VECTOR(4, *)', + oracle: 'VECTOR(4)', postgres: 'VECTOR(4)', snowflake: 'VECTOR(FLOAT, 4)', }); @@ -265,9 +306,15 @@ if (sequelize.dialect.supports?.dataTypes?.VECTOR) { See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR(3, FLOAT32)', postgres: 'VECTOR(3)', - snowflake: new Error( - 'Snowflake VECTOR format "float32" is not supported. Use "FLOAT" or "INT".', - ), + snowflake: new Error('Invalid Snowflake VECTOR format: float32'), + }); + + testDataTypeSql("VECTOR(24, 'binary')", DataTypes.VECTOR(24, 'binary'), { + default: new Error(`${dialectName} does not support the VECTOR data type. + See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), + oracle: 'VECTOR(24, BINARY)', + postgres: 'VECTOR(24)', + snowflake: new Error('Invalid Snowflake VECTOR format: binary'), }); }); @@ -291,6 +338,15 @@ if (sequelize.dialect.supports?.dataTypes?.VECTOR) { expect(() => type.validate(new Float32Array([1, 2, 3]))).not.to.throw(); }); + + it('should throw if value contains non-finite numbers', () => { + const type: DataTypeInstance = DataTypes.VECTOR(); + + expect(() => type.validate([1, Infinity, 3])).to.throw( + ValidationErrorItem, + 'is not a valid vector', + ); + }); }); }); } diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index 4e5fba3865c2..50b157df5a67 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -365,6 +365,33 @@ if (current.dialect.name === 'oracle') { type: 'DB_TYPE_VECTOR', }); }); + + it('rejects unsupported Oracle VECTOR formats', () => { + expect(() => DataTypes.VECTOR(3, 'int16').toDialectDataType(current.dialect)).to.throw( + TypeError, + 'Invalid Oracle VECTOR format: int16', + ); + }); + + it('accepts Oracle binary format and renders BINARY in SQL', () => { + const type = DataTypes.VECTOR(24, 'binary').toDialectDataType(current.dialect); + + expect(type.toSql()).to.equal('VECTOR(24, BINARY)'); + }); + + it('renders object-style binary options in SQL', () => { + const type = DataTypes.VECTOR({ dimension: 24, format: 'binary' }).toDialectDataType( + current.dialect, + ); + + expect(type.toSql()).to.equal('VECTOR(24, BINARY)'); + }); + + it('keeps object-style dimension-only SQL concise', () => { + const type = DataTypes.VECTOR({ dimension: 24 }).toDialectDataType(current.dialect); + + expect(type.toSql()).to.equal('VECTOR(24)'); + }); }); describe('Vector where clause', () => { @@ -445,6 +472,16 @@ if (current.dialect.name === 'oracle') { ).to.throw(Error, 'Invalid value received for the "where" option'); }); + it('throws for non-finite vector array elements', () => { + expect(() => + queryGenerator.whereItemsQuery( + sql.where(sql.fn('VECTOR_DISTANCE', sql.attribute('embedding'), [1, Infinity, 3]), { + [Op.lt]: 2, + }), + ), + ).to.throw(Error, 'Invalid value received for the "where" option'); + }); + it('throws for malformed VECTOR literal strings', () => { expect(() => queryGenerator.whereItemsQuery( diff --git a/packages/core/test/unit/dialects/postgres/vector.test.js b/packages/core/test/unit/dialects/postgres/vector.test.js index ea9b2c4eab25..611d35755f68 100644 --- a/packages/core/test/unit/dialects/postgres/vector.test.js +++ b/packages/core/test/unit/dialects/postgres/vector.test.js @@ -45,9 +45,15 @@ if (current.dialect.name === 'postgres') { it('rejects non-finite values', () => { const type = DataTypes.VECTOR(3).toDialectDataType(current.dialect); - expect(() => type.validate([1, Infinity, 3])).to.throw( - Error, - 'VECTOR expects finite numeric elements', + expect(() => type.validate([1, Infinity, 3])).to.throw(Error, 'is not a valid vector'); + }); + + it('rejects dimensions above pgvector max size', () => { + const type = DataTypes.VECTOR(16_001).toDialectDataType(current.dialect); + + expect(() => type.toSql()).to.throw( + TypeError, + 'Invalid VECTOR dimension: 16001 (max 16000)', ); }); diff --git a/packages/core/test/unit/dialects/snowflake/vector.test.js b/packages/core/test/unit/dialects/snowflake/vector.test.js index 8c5938fcf743..38d53fc59281 100644 --- a/packages/core/test/unit/dialects/snowflake/vector.test.js +++ b/packages/core/test/unit/dialects/snowflake/vector.test.js @@ -2,13 +2,27 @@ const { expect } = require('chai'); const Support = require('../../../support'); -const { Op, sql } = require('@sequelize/core'); +const { DataTypes, Op, sql } = require('@sequelize/core'); const expectsql = Support.expectsql; const current = Support.sequelize; const queryGenerator = current.dialect.queryGenerator; if (current.dialect.name === 'snowflake') { + describe('[Snowflake Specific] VECTOR datatype', () => { + it('renders SQL with default FLOAT element type', () => { + const type = DataTypes.VECTOR(3).toDialectDataType(current.dialect); + + expect(type.toSql()).to.equal('VECTOR(FLOAT, 3)'); + }); + + it('rejects dimensions above Snowflake max size', () => { + const type = DataTypes.VECTOR(4097).toDialectDataType(current.dialect); + + expect(() => type.toSql()).to.throw(TypeError, 'Invalid VECTOR dimension: 4097 (max 4096)'); + }); + }); + describe('[Snowflake Specific] VECTOR functions', () => { it('renders L2 distance from array input', () => { const where = sql.where(current.l2Distance('embedding', [1, 2, 3]), { @@ -98,6 +112,12 @@ if (current.dialect.name === 'snowflake') { ).to.throw(Error, 'is not a valid vector element'); }); + it('throws for non-finite vector array elements', () => { + expect(() => + queryGenerator.formatSqlExpression(current.l2Distance('embedding', [1, Infinity, 3])), + ).to.throw(Error, 'is not a valid vector element'); + }); + it('throws when second argument is not a vector input', () => { expect(() => queryGenerator.formatSqlExpression(current.l2Distance('embedding', 'not-a-vector')), diff --git a/packages/oracle/src/_internal/data-types-overrides.ts b/packages/oracle/src/_internal/data-types-overrides.ts index 71afd3ef40b2..ac3f61c0e93f 100644 --- a/packages/oracle/src/_internal/data-types-overrides.ts +++ b/packages/oracle/src/_internal/data-types-overrides.ts @@ -183,6 +183,12 @@ export class DATE extends BaseTypes.DATE { } type AcceptedNumber = number | bigint | boolean | string | null; +export type OracleVectorFormat = 'INT8' | 'FLOAT32' | 'FLOAT64' | 'BINARY' | '*'; + +export interface OracleVectorOptions { + dimension?: number | '*'; + format?: OracleVectorFormat; +} export class DECIMAL extends BaseTypes.DECIMAL { toSql() { @@ -421,12 +427,38 @@ export class DATEONLY extends BaseTypes.DATEONLY { } export class VECTOR extends BaseTypes.VECTOR { - protected _getSqlOptionParts(): string[] { - if (this.options.dimension == null) { - return []; + protected _validateFormat(format: string): OracleVectorFormat { + const normalized = format.trim().toUpperCase(); + + switch (normalized) { + case 'INT8': + case 'FLOAT32': + case 'FLOAT64': + case 'BINARY': + case '*': + return normalized; + default: + throw new TypeError(`Invalid Oracle VECTOR format: ${format}`); } + } - return [String(this.options.dimension), this.options.format?.toUpperCase() ?? '*']; + protected _validateOracleDimension(dimension: number | '*'): number | '*' { + if (dimension === '*') { + return dimension; + } + + return this._validateDimension(dimension); + } + + protected _getSqlOptionParts(): string[] { + const options = this.options as OracleVectorOptions; + + return [ + ...(options.dimension !== undefined + ? [String(this._validateOracleDimension(options.dimension))] + : []), + ...(options.format !== undefined ? [this._validateFormat(options.format)] : []), + ]; } toBindableValue(value: BaseTypes.Vector) { diff --git a/packages/oracle/src/query-generator.internal.ts b/packages/oracle/src/query-generator.internal.ts index 73e09658581e..4d4281a08dc6 100644 --- a/packages/oracle/src/query-generator.internal.ts +++ b/packages/oracle/src/query-generator.internal.ts @@ -112,7 +112,7 @@ export class OracleQueryGeneratorInternal< #formatVectorFromIterable(values: Iterable): string { const parts: number[] = []; for (const item of values) { - if (typeof item !== 'number' || Number.isNaN(item)) { + if (typeof item !== 'number' || !Number.isFinite(item)) { throw new Error(`${util.format('%O is not a valid vector element', item)}`); } @@ -159,7 +159,7 @@ function parseVectorLiteral(input: string): number[] | null { if ( !Array.isArray(values) || - values.some(value => typeof value !== 'number' || Number.isNaN(value)) + values.some(value => typeof value !== 'number' || !Number.isFinite(value)) ) { return null; } diff --git a/packages/postgres/src/_internal/data-types-overrides.ts b/packages/postgres/src/_internal/data-types-overrides.ts index 525515072c7a..0dc10aca60a8 100644 --- a/packages/postgres/src/_internal/data-types-overrides.ts +++ b/packages/postgres/src/_internal/data-types-overrides.ts @@ -17,6 +17,10 @@ import { PostgresQueryGenerator } from '../query-generator'; import { stringifyHstore } from './hstore.js'; import { buildRangeParser, stringifyRange } from './range.js'; +export interface PgVectorOptions { + dimension: number; +} + function removeUnsupportedIntegerOptions( dataType: BaseTypes.BaseIntegerDataType, dialect: AbstractDialect, @@ -295,21 +299,14 @@ export class GEOGRAPHY extends BaseTypes.GEOGRAPHY { export class VECTOR extends BaseTypes.VECTOR { protected _getSqlOptionParts(): string[] { - return this.options.dimension != null ? [String(this.options.dimension)] : []; + const options = this.#getPgOptions(); + + return options ? [String(options.dimension)] : []; } protected _checkOptionSupport(dialect: AbstractDialect): void { super._checkOptionSupport(dialect); - if ( - this.options.dimension != null && - (!Number.isInteger(this.options.dimension) || this.options.dimension <= 0) - ) { - throw new TypeError( - `${dialect.name} VECTOR requires the "dimension" option to be a positive integer.`, - ); - } - if (this.options.format) { dialect.warnDataTypeIssue( `${dialect.name} VECTOR ignores the "format" option; pgvector uses vector(n).`, @@ -320,8 +317,9 @@ export class VECTOR extends BaseTypes.VECTOR { validate(value: unknown): asserts value is BaseTypes.Vector { super.validate(value); + const options = this.#getPgOptions(); - if (this.options.dimension == null) { + if (options == null) { return; } @@ -331,11 +329,11 @@ export class VECTOR extends BaseTypes.VECTOR { } const values = [...iterable]; - if (values.length !== this.options.dimension) { + if (values.length !== options.dimension) { ValidationErrorItem.throwDataTypeValidationError( util.format( 'VECTOR expects values of length %d, but received %d', - this.options.dimension, + options.dimension, values.length, ), ); @@ -388,6 +386,16 @@ export class VECTOR extends BaseTypes.VECTOR { `DataTypes.VECTOR received a non-vector value from the database: ${inspect(value)}`, ); } + + #getPgOptions(): PgVectorOptions | null { + if (this.options.dimension == null) { + return null; + } + + return { + dimension: this._validateDimension(this.options.dimension, 16_000), + }; + } } export class HSTORE extends BaseTypes.HSTORE { diff --git a/packages/snowflake/src/_internal/data-types-overrides.ts b/packages/snowflake/src/_internal/data-types-overrides.ts index f003934719ae..af2aff87c146 100644 --- a/packages/snowflake/src/_internal/data-types-overrides.ts +++ b/packages/snowflake/src/_internal/data-types-overrides.ts @@ -5,6 +5,13 @@ import * as BaseTypes from '@sequelize/core/_non-semver-use-at-your-own-risk_/ab import maxBy from 'lodash/maxBy.js'; import util from 'node:util'; +export type SnowflakeVectorFormat = 'INT' | 'FLOAT'; + +export interface SnowflakeVectorOptions { + dimension: number; + format: SnowflakeVectorFormat; +} + export class DATE extends BaseTypes.DATE { toSql() { return `TIMESTAMP${this.options.precision != null ? `(${this.options.precision})` : ''}`; @@ -99,66 +106,38 @@ export class BIGINT extends BaseTypes.BIGINT { } export class VECTOR extends BaseTypes.VECTOR { - readonly #elementType: 'FLOAT' | 'INT'; - - constructor(...args: ConstructorParameters) { - super(...args); - - const normalizedFormat = this.#normalizeFormat(this.options.format); - this.#elementType = normalizedFormat === 'int' ? 'INT' : 'FLOAT'; - this.options.format = normalizedFormat; - - // Snowflake requires the dimension to be provided up front; surface a clearer - // error here rather than letting the server reject the DDL later. - if (this.options.dimension == null || !Number.isInteger(this.options.dimension)) { - throw new TypeError('Snowflake VECTOR requires a positive integer "dimension" option.'); - } - - if (this.options.dimension <= 0) { - throw new TypeError( - 'Snowflake VECTOR requires the "dimension" option to be greater than zero.', - ); - } - } - protected _getSqlOptionParts(): string[] { - // Sample cross-dialect implementation: Snowflake needs VECTOR(type, dimension), - // so it overrides the shared SQL option hook instead of replacing toSql(). - return [this.#elementType, String(this.options.dimension)]; + const options = this.#getSnowflakeOptions(); + + return [options.format, String(options.dimension)]; } validate(value: unknown): asserts value is BaseTypes.Vector { super.validate(value); + const options = this.#getSnowflakeOptions(); const length = this.#getVectorLength(value); - if (length !== this.options.dimension) { + if (length !== options.dimension) { ValidationErrorItem.throwDataTypeValidationError( util.format( 'VECTOR expects values of length %d, but received %d', - this.options.dimension, + options.dimension, length, ), ); } } - #normalizeFormat(format: string | undefined): 'float' | 'int' { - if (format == null) { - return 'float'; - } - - const lower = format.toLowerCase(); - if (lower === 'float') { - return 'float'; - } + protected _validateFormat(format: string): SnowflakeVectorFormat { + const normalized = format.trim().toUpperCase(); - if (lower === 'int') { - return 'int'; + switch (normalized) { + case 'INT': + case 'FLOAT': + return normalized; + default: + throw new TypeError(`Invalid Snowflake VECTOR format: ${format}`); } - - throw new TypeError( - `Snowflake VECTOR format "${format}" is not supported. Use "FLOAT" or "INT".`, - ); } #getVectorLength(value: BaseTypes.Vector): number { @@ -187,12 +166,13 @@ export class VECTOR extends BaseTypes.VECTOR { protected _validateVectorElement(item: unknown): number { const numeric = super._validateVectorElement(item); + const options = this.#getSnowflakeOptions(); - if (this.options.format === 'int' && !Number.isInteger(numeric)) { + if (options.format === 'INT' && !Number.isInteger(numeric)) { ValidationErrorItem.throwDataTypeValidationError( util.format( 'VECTOR(INT, %d) only accepts integers, but received %O', - this.options.dimension, + options.dimension, numeric, ), ); @@ -200,4 +180,15 @@ export class VECTOR extends BaseTypes.VECTOR { return numeric; } + + #getSnowflakeOptions(): SnowflakeVectorOptions { + if (this.options.dimension == null) { + throw new TypeError('Snowflake VECTOR requires a positive integer "dimension" option.'); + } + + const dimension = this._validateDimension(this.options.dimension, 4096); + const format = this._validateFormat(this.options.format ?? 'FLOAT'); + + return { dimension, format }; + } } diff --git a/packages/snowflake/src/query-generator.internal.ts b/packages/snowflake/src/query-generator.internal.ts index 31e5b88057ee..206984c0e4da 100644 --- a/packages/snowflake/src/query-generator.internal.ts +++ b/packages/snowflake/src/query-generator.internal.ts @@ -115,7 +115,7 @@ export class SnowflakeQueryGeneratorInternal< } const numericValues = values.map(value => { - if (typeof value !== 'number' || Number.isNaN(value)) { + if (typeof value !== 'number' || !Number.isFinite(value)) { throw new Error(util.format('%O is not a valid vector element', value)); } From 7c14284731d413da660b42946f050761f41ad9b0 Mon Sep 17 00:00:00 2001 From: sudarshan12s Date: Wed, 20 May 2026 16:24:44 +0530 Subject: [PATCH 24/49] - explicitly make abstractbase so that oracle can add its own vector options --- .../core/src/abstract-dialect/data-types.ts | 59 ++++++--- packages/core/src/sequelize.d.ts | 12 +- .../dialects/oracle/vector.test.js | 1 + .../test/unit/data-types/string-types.test.ts | 7 +- .../test/unit/dialects/oracle/vector.test.js | 59 ++++++++- .../unit/dialects/postgres/vector.test.js | 25 +++- .../unit/dialects/snowflake/vector.test.js | 26 +++- .../src/_internal/data-types-overrides.ts | 114 +++++++++++++++++- .../src/_internal/data-types-overrides.ts | 13 +- .../postgres/src/query-generator.internal.ts | 36 +++++- .../src/_internal/data-types-overrides.ts | 47 ++------ .../snowflake/src/query-generator.internal.ts | 40 ++++-- 12 files changed, 347 insertions(+), 92 deletions(-) diff --git a/packages/core/src/abstract-dialect/data-types.ts b/packages/core/src/abstract-dialect/data-types.ts index 782aab2daba1..4af58def0bd3 100644 --- a/packages/core/src/abstract-dialect/data-types.ts +++ b/packages/core/src/abstract-dialect/data-types.ts @@ -2860,9 +2860,20 @@ export interface VectorOptions { format?: string; } -// Dialects may accept plain arrays or any numeric typed array. The generic validator -// inspects ArrayBufferView instances without having to enumerate every concrete type here. -export type Vector = number[] | ArrayBufferView; +export type NumericTypedArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array; + +// Dialects may accept plain arrays or any number-backed typed array. The generic validator +// checks only the container shape; dialects are responsible for validating dimensions and values. +export type VectorValue = number[] | NumericTypedArray; /** * The VECTOR type stores ordered numeric vectors. @@ -2873,28 +2884,28 @@ export type Vector = number[] | ArrayBufferView; * __Fallback policy:__ * If this type is not supported, an error will be raised. * + * This shared base exists for dialect-specific VECTOR implementations. Dialects that can use Sequelize's + * generic {@link VectorOptions} may extend {@link VECTOR}; dialects with a different option shape may extend + * this class directly with their own options type. + * * @example * ```ts * DataTypes.VECTOR * DataTypes.VECTOR(1536) * DataTypes.VECTOR(1536, 'float32') - * DataTypes.VECTOR({ dimension: 1024, format: 'int' }) + * DataTypes.VECTOR({ dimension: 1024, format: 'int8' }) * DataTypes.VECTOR({ dimension: 2048 }) * ``` * * @category DataTypes */ -export abstract class AbstractVECTORBase extends AbstractDataType { +export abstract class AbstractVECTORBase< + TOptions extends object, +> extends AbstractDataType { readonly options!: TOptions; - validate(value: unknown): asserts value is Vector { - const iterable = this._getVectorIterable(value); - - if (iterable) { - for (const item of iterable) { - this._validateVectorElement(item); - } - + validate(value: unknown): asserts value is VectorValue { + if (this._getVectorIterable(value)) { return; } @@ -2903,7 +2914,7 @@ export abstract class AbstractVECTORBase extends Abstra ); } - protected _getVectorIterable(value: unknown): Iterable | null { + protected _getVectorIterable(value: unknown): VectorValue | null { if (Array.isArray(value) || isTypedArrayIterable(value)) { return value; } @@ -2952,6 +2963,10 @@ export abstract class AbstractVECTORBase extends Abstra } } +/** + * Generic VECTOR implementation used by dialects whose options fit {@link VectorOptions}. + * Dialects with stricter option models can extend {@link AbstractVECTORBase} directly. + */ export class VECTOR extends AbstractVECTORBase { /** @hidden */ static readonly [DataTypeIdentifier]: string = 'VECTOR'; @@ -3001,8 +3016,10 @@ export class VECTOR extends AbstractVECTORBase { const normalized = format.trim().toLowerCase(); switch (normalized) { + case 'float': case 'float32': case 'float64': + case 'int': case 'int8': case 'int16': case 'int32': @@ -3021,8 +3038,18 @@ export class VECTOR extends AbstractVECTORBase { } } -function isTypedArrayIterable(value: unknown): value is ArrayBufferView & Iterable { - return ArrayBuffer.isView(value) && !(value instanceof DataView); +function isTypedArrayIterable(value: unknown): value is NumericTypedArray { + return ( + value instanceof Int8Array || + value instanceof Uint8Array || + value instanceof Uint8ClampedArray || + value instanceof Int16Array || + value instanceof Uint16Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Float32Array || + value instanceof Float64Array + ); } function rejectBlobs(value: unknown) { diff --git a/packages/core/src/sequelize.d.ts b/packages/core/src/sequelize.d.ts index 3b119aab5323..b992db43ec60 100644 --- a/packages/core/src/sequelize.d.ts +++ b/packages/core/src/sequelize.d.ts @@ -1,6 +1,6 @@ import type { Options as RetryAsPromisedOptions } from 'retry-as-promised'; import type { DataTypes, Op, Options, QueryTypes } from '.'; -import type { DataType } from './abstract-dialect/data-types.js'; +import type { DataType, VectorValue } from './abstract-dialect/data-types.js'; import type { AbstractDialect, ConnectionOptions } from './abstract-dialect/dialect.js'; import type { ColumnsDescription, @@ -619,15 +619,15 @@ export class Sequelize< * @param column column name or identifier to compare against * @param value reference vector as a plain numeric array or typed array */ - cosineDistance(column: string, value: number[] | Exclude): Fn; + cosineDistance(column: string, value: VectorValue): Fn; - innerProduct(column: string, value: number[] | Exclude): Fn; + innerProduct(column: string, value: VectorValue): Fn; - l1Distance(column: string, value: number[] | Exclude): Fn; + l1Distance(column: string, value: VectorValue): Fn; - l2Distance(column: string, value: number[] | Exclude): Fn; + l2Distance(column: string, value: VectorValue): Fn; - vectorDistance(column: string, value: number[] | Exclude): Fn; + vectorDistance(column: string, value: VectorValue): Fn; /** * Execute a query which would set an environment or user variable. The variables are set per connection, diff --git a/packages/core/test/integration/dialects/oracle/vector.test.js b/packages/core/test/integration/dialects/oracle/vector.test.js index 2b8b682d4ee4..645a4f0e285a 100644 --- a/packages/core/test/integration/dialects/oracle/vector.test.js +++ b/packages/core/test/integration/dialects/oracle/vector.test.js @@ -149,6 +149,7 @@ if (getTestDialect() === 'oracle') { ); expect(rows).to.have.length(1); + expect(rows[0].id).to.equal(4); }); it('accepts valid VECTOR literal strings in vector functions', async function () { diff --git a/packages/core/test/unit/data-types/string-types.test.ts b/packages/core/test/unit/data-types/string-types.test.ts index c90e66e34db9..3f05bb86f3ee 100644 --- a/packages/core/test/unit/data-types/string-types.test.ts +++ b/packages/core/test/unit/data-types/string-types.test.ts @@ -339,13 +339,10 @@ if (sequelize.dialect.supports?.dataTypes?.VECTOR) { expect(() => type.validate(new Float32Array([1, 2, 3]))).not.to.throw(); }); - it('should throw if value contains non-finite numbers', () => { + it('should not validate vector elements in the base type', () => { const type: DataTypeInstance = DataTypes.VECTOR(); - expect(() => type.validate([1, Infinity, 3])).to.throw( - ValidationErrorItem, - 'is not a valid vector', - ); + expect(() => type.validate([1, Infinity, 3])).not.to.throw(); }); }); }); diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index 50b157df5a67..8115492452e8 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -3,6 +3,7 @@ const util = require('node:util'); const Support = require('../../../support'); const { DataTypes, Op, sql } = require('@sequelize/core'); +const BaseTypes = require('@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/data-types.js'); const { OracleQuery } = require('@sequelize/oracle'); const { expect } = require('chai'); @@ -358,6 +359,30 @@ if (current.dialect.name === 'oracle') { expect(type.toBindableValue(typed)).to.equal(typed); }); + it('accepts SparseVector-like input and keeps it as-is for binding', () => { + const type = DataTypes.VECTOR(5).toDialectDataType(current.dialect); + const sparseVector = { + values: new Float64Array([1, 2]), + indices: new Uint32Array([2, 4]), + numDimensions: 5, + }; + + expect(() => type.validate(sparseVector)).not.to.throw(); + expect(type.toBindableValue(sparseVector)).to.equal(sparseVector); + }); + + it('rejects invalid SparseVector-like input', () => { + const type = DataTypes.VECTOR(5).toDialectDataType(current.dialect); + + expect(() => + type.validate({ + values: new Float64Array([1, 2]), + indices: new Uint32Array([2]), + numDimensions: 5, + }), + ).to.throw(Error, 'is not a valid vector'); + }); + it('returns the Oracle VECTOR bind definition', () => { const type = DataTypes.VECTOR(3).toDialectDataType(current.dialect); @@ -392,12 +417,28 @@ if (current.dialect.name === 'oracle') { expect(type.toSql()).to.equal('VECTOR(24)'); }); + + it('renders Oracle-specific sparse storage when using the Oracle VECTOR override directly', () => { + const OracleVector = current.dialect.getDataTypeForDialect(BaseTypes.VECTOR); + const type = new OracleVector({ dimension: 8, format: 'int8', storage: 'sparse' }); + + expect(type.toSql()).to.equal('VECTOR(8, INT8, SPARSE)'); + }); + + it('rejects unsupported Oracle VECTOR storage options', () => { + const OracleVector = current.dialect.getDataTypeForDialect(BaseTypes.VECTOR); + + expect(() => new OracleVector({ dimension: 8, storage: 'dense' })).to.throw( + TypeError, + 'Invalid Oracle VECTOR storage: dense', + ); + }); }); describe('Vector where clause', () => { const queryVector = [1, 2, 3]; - const testsql = function (key, value, options, expectation) { + const testsql = (key, value, options, expectation) => { if (expectation === undefined) { expectation = options; options = undefined; @@ -492,6 +533,22 @@ if (current.dialect.name === 'oracle') { ).to.throw(Error, 'Invalid value received for the "where" option'); }); + it('throws for unsafe VECTOR literal strings', () => { + for (const payload of [ + `VECTOR('[1,2,3]'); DROP TABLE users; --')`, + `VECTOR('[1,2,3]') || 'x'`, + `VECTOR_DISTANCE("embedding", VECTOR('[1,2,3]'))`, + ]) { + expect(() => + queryGenerator.whereItemsQuery( + sql.where(sql.fn('VECTOR_DISTANCE', sql.attribute('embedding'), payload), { + [Op.lt]: 2, + }), + ), + ).to.throw(Error, 'Invalid value received for the "where" option'); + } + }); + it('throws for malformed VECTOR literal payloads', () => { expect(() => queryGenerator.whereItemsQuery( diff --git a/packages/core/test/unit/dialects/postgres/vector.test.js b/packages/core/test/unit/dialects/postgres/vector.test.js index 611d35755f68..5d03df17c70d 100644 --- a/packages/core/test/unit/dialects/postgres/vector.test.js +++ b/packages/core/test/unit/dialects/postgres/vector.test.js @@ -42,10 +42,16 @@ if (current.dialect.name === 'postgres') { ); }); - it('rejects non-finite values', () => { + it('does not validate elements when validating dimensions', () => { const type = DataTypes.VECTOR(3).toDialectDataType(current.dialect); - expect(() => type.validate([1, Infinity, 3])).to.throw(Error, 'is not a valid vector'); + expect(() => type.validate([1, Infinity, 3])).not.to.throw(); + }); + + it('rejects non-finite values when serializing values', () => { + expect(() => + queryGenerator.escape([1, Infinity, 3], { type: DataTypes.VECTOR(3) }), + ).to.throw(Error, 'is not a valid vector'); }); it('rejects dimensions above pgvector max size', () => { @@ -135,6 +141,21 @@ if (current.dialect.name === 'postgres') { }); }); + it('rejects unsafe pgvector literal strings', () => { + for (const payload of [ + "'[1,2,3]'); DROP TABLE users; --'::vector", + "'[1,2,3]'::vector; DROP TABLE users; --", + '[1,2,3]; DROP TABLE users; --', + ]) { + expect(() => + queryGenerator.formatSqlExpression(current.l2Distance('embedding', payload)), + ).to.throw( + Error, + 'L2_DISTANCE expects the second argument to be a number array, typed array, or pgvector literal string', + ); + } + }); + it('accepts JSON array strings by normalizing to pgvector literals', () => { const where = sql.where(current.l2Distance('embedding', '[1,2,3]'), { [Op.lt]: 2, diff --git a/packages/core/test/unit/dialects/snowflake/vector.test.js b/packages/core/test/unit/dialects/snowflake/vector.test.js index 38d53fc59281..08aeea6b2cea 100644 --- a/packages/core/test/unit/dialects/snowflake/vector.test.js +++ b/packages/core/test/unit/dialects/snowflake/vector.test.js @@ -16,6 +16,15 @@ if (current.dialect.name === 'snowflake') { expect(type.toSql()).to.equal('VECTOR(FLOAT, 3)'); }); + it('normalizes Snowflake vector format aliases', () => { + expect(DataTypes.VECTOR(3, 'int').toDialectDataType(current.dialect).toSql()).to.equal( + 'VECTOR(INT, 3)', + ); + expect(DataTypes.VECTOR(3, 'float32').toDialectDataType(current.dialect).toSql()).to.equal( + 'VECTOR(FLOAT, 3)', + ); + }); + it('rejects dimensions above Snowflake max size', () => { const type = DataTypes.VECTOR(4097).toDialectDataType(current.dialect); @@ -72,7 +81,7 @@ if (current.dialect.name === 'snowflake') { }); expectsql(queryGenerator.whereItemsQuery(where), { - snowflake: 'VECTOR_L2_DISTANCE("embedding", [1,2,3]) < 10', + snowflake: 'VECTOR_L2_DISTANCE("embedding", [1,2,3]::VECTOR(FLOAT, 3)) < 10', }); }); @@ -86,6 +95,21 @@ if (current.dialect.name === 'snowflake') { }); }); + it('rejects unsafe VECTOR literal strings', () => { + for (const payload of [ + '[1,2,3]); DROP TABLE users; --', + '[1,2,3]::VECTOR(FLOAT, 3); DROP TABLE users; --', + 'VECTOR_L2_DISTANCE("embedding", [1,2,3]::VECTOR(FLOAT, 3))', + ]) { + expect(() => + queryGenerator.formatSqlExpression(current.l2Distance('embedding', payload)), + ).to.throw( + Error, + 'L2_DISTANCE expects the second argument to be a number array, typed array, or VECTOR-compatible SQL literal', + ); + } + }); + it('throws when vector function has too few arguments', () => { expect(() => queryGenerator.formatSqlExpression(sql.fn('L2_DISTANCE', sql.attribute('embedding'))), diff --git a/packages/oracle/src/_internal/data-types-overrides.ts b/packages/oracle/src/_internal/data-types-overrides.ts index ac3f61c0e93f..c43604de5657 100644 --- a/packages/oracle/src/_internal/data-types-overrides.ts +++ b/packages/oracle/src/_internal/data-types-overrides.ts @@ -184,10 +184,18 @@ export class DATE extends BaseTypes.DATE { type AcceptedNumber = number | bigint | boolean | string | null; export type OracleVectorFormat = 'INT8' | 'FLOAT32' | 'FLOAT64' | 'BINARY' | '*'; +export type OracleVectorStorage = 'SPARSE'; export interface OracleVectorOptions { dimension?: number | '*'; format?: OracleVectorFormat; + storage?: OracleVectorStorage; +} + +export interface OracleSparseVectorInput { + values: number[] | BaseTypes.NumericTypedArray; + indices: number[] | Uint32Array; + numDimensions: number; } export class DECIMAL extends BaseTypes.DECIMAL { @@ -426,7 +434,51 @@ export class DATEONLY extends BaseTypes.DATEONLY { } } -export class VECTOR extends BaseTypes.VECTOR { +export class VECTOR extends BaseTypes.AbstractVECTORBase { + /** @hidden */ + static readonly [BaseTypes.DataTypeIdentifier]: string = 'VECTOR'; + + readonly options: OracleVectorOptions; + + constructor(); + constructor(dimension: number | '*', format?: string, storage?: string); + constructor(options: OracleVectorOptions); + constructor( + dimensionOrOptions?: number | '*' | OracleVectorOptions, + format?: string, + storage?: string, + ) { + super(); + + if (typeof dimensionOrOptions === 'object' && dimensionOrOptions !== null) { + this.options = { + ...(dimensionOrOptions.dimension !== undefined + ? { dimension: this._validateOracleDimension(dimensionOrOptions.dimension) } + : {}), + ...(dimensionOrOptions.format !== undefined + ? { format: this._validateFormat(dimensionOrOptions.format) } + : {}), + ...(dimensionOrOptions.storage !== undefined + ? { storage: this._validateStorage(dimensionOrOptions.storage) } + : {}), + }; + + return; + } + + this.options = { + ...(dimensionOrOptions !== undefined + ? { dimension: this._validateOracleDimension(dimensionOrOptions) } + : {}), + ...(format !== undefined ? { format: this._validateFormat(format) } : {}), + ...(storage !== undefined ? { storage: this._validateStorage(storage) } : {}), + }; + } + + protected _getTypeName(): string { + return 'VECTOR'; + } + protected _validateFormat(format: string): OracleVectorFormat { const normalized = format.trim().toUpperCase(); @@ -450,18 +502,38 @@ export class VECTOR extends BaseTypes.VECTOR { return this._validateDimension(dimension); } + protected _validateStorage(storage: string): OracleVectorStorage { + const normalized = storage.trim().toUpperCase(); + + switch (normalized) { + case 'SPARSE': + return normalized; + default: + throw new TypeError(`Invalid Oracle VECTOR storage: ${storage}`); + } + } + protected _getSqlOptionParts(): string[] { - const options = this.options as OracleVectorOptions; + const options = this.options; return [ ...(options.dimension !== undefined ? [String(this._validateOracleDimension(options.dimension))] : []), ...(options.format !== undefined ? [this._validateFormat(options.format)] : []), + ...(options.storage !== undefined ? [this._validateStorage(options.storage)] : []), ]; } - toBindableValue(value: BaseTypes.Vector) { + validate(value: unknown): asserts value is BaseTypes.VectorValue | OracleSparseVectorInput { + if (isOracleSparseVectorInput(value)) { + return; + } + + super.validate(value); + } + + toBindableValue(value: BaseTypes.VectorValue | OracleSparseVectorInput) { if (Array.isArray(value)) { return Float64Array.from(value); } @@ -473,3 +545,39 @@ export class VECTOR extends BaseTypes.VECTOR { return { type: oracledb.DB_TYPE_VECTOR }; } } + +function isOracleSparseVectorInput(value: unknown): value is OracleSparseVectorInput { + if (typeof value !== 'object' || value === null) { + return false; + } + + const sparseVector = value as Partial; + const { indices, numDimensions, values } = sparseVector; + + return ( + isVectorComponent(values) && + isVectorComponent(indices) && + Number.isInteger(numDimensions) && + numDimensions !== undefined && + numDimensions > 0 && + values.length === indices.length + ); +} + +function isVectorComponent(value: unknown): value is number[] | BaseTypes.NumericTypedArray { + return Array.isArray(value) || isNumericTypedArray(value); +} + +function isNumericTypedArray(value: unknown): value is BaseTypes.NumericTypedArray { + return ( + value instanceof Int8Array || + value instanceof Uint8Array || + value instanceof Uint8ClampedArray || + value instanceof Int16Array || + value instanceof Uint16Array || + value instanceof Int32Array || + value instanceof Uint32Array || + value instanceof Float32Array || + value instanceof Float64Array + ); +} diff --git a/packages/postgres/src/_internal/data-types-overrides.ts b/packages/postgres/src/_internal/data-types-overrides.ts index 0dc10aca60a8..d647ca080849 100644 --- a/packages/postgres/src/_internal/data-types-overrides.ts +++ b/packages/postgres/src/_internal/data-types-overrides.ts @@ -315,7 +315,7 @@ export class VECTOR extends BaseTypes.VECTOR { } } - validate(value: unknown): asserts value is BaseTypes.Vector { + validate(value: unknown): asserts value is BaseTypes.VectorValue { super.validate(value); const options = this.#getPgOptions(); @@ -328,13 +328,12 @@ export class VECTOR extends BaseTypes.VECTOR { return; } - const values = [...iterable]; - if (values.length !== options.dimension) { + if (iterable.length !== options.dimension) { ValidationErrorItem.throwDataTypeValidationError( util.format( 'VECTOR expects values of length %d, but received %d', options.dimension, - values.length, + iterable.length, ), ); } @@ -352,7 +351,7 @@ export class VECTOR extends BaseTypes.VECTOR { return numeric; } - toBindableValue(value: BaseTypes.Vector): string { + toBindableValue(value: BaseTypes.VectorValue): string { const iterable = this._getVectorIterable(value); if (iterable == null) { throw new TypeError('Unsupported vector container type'); @@ -365,11 +364,11 @@ export class VECTOR extends BaseTypes.VECTOR { return `[${values.join(',')}]`; } - escape(value: BaseTypes.Vector): string { + escape(value: BaseTypes.VectorValue): string { return `${this._getDialect().escapeString(this.toBindableValue(value))}::vector`; } - getBindParamSql(value: BaseTypes.Vector, options: BindParamOptions): string { + getBindParamSql(value: BaseTypes.VectorValue, options: BindParamOptions): string { return `${options.bindParam(this.toBindableValue(value))}::vector`; } diff --git a/packages/postgres/src/query-generator.internal.ts b/packages/postgres/src/query-generator.internal.ts index d57fe4467c24..5ec68767ae6e 100644 --- a/packages/postgres/src/query-generator.internal.ts +++ b/packages/postgres/src/query-generator.internal.ts @@ -80,8 +80,9 @@ export class PostgresQueryGeneratorInternal< if (typeof arg === 'string') { const trimmed = arg.trim(); - if (this.#looksLikeVectorLiteral(trimmed)) { - return trimmed; + const literal = this.#parseVectorLiteral(trimmed); + if (literal) { + return literal; } if (trimmed.startsWith('[') && trimmed.endsWith(']')) { @@ -101,8 +102,35 @@ export class PostgresQueryGeneratorInternal< ); } - #looksLikeVectorLiteral(literal: string): boolean { - return /^'.*'\s*::\s*vector(?:\s*\(\s*\d+\s*\))?$/i.test(literal); + #parseVectorLiteral(literal: string): string | null { + const match = literal.match(/^'([^']+)'\s*::\s*vector(?:\s*\(\s*(\d+)\s*\))?$/i); + if (!match) { + return null; + } + + const [, body, dimension] = match; + if (!body.startsWith('[') || !body.endsWith(']')) { + return null; + } + + let parsed: unknown; + try { + parsed = JSON.parse(body); + } catch { + return null; + } + + if (!Array.isArray(parsed)) { + return null; + } + + if (dimension != null && parsed.length !== Number(dimension)) { + throw new Error( + `Vector literal dimension ${dimension} does not match ${parsed.length} values`, + ); + } + + return this.#formatVectorFromArray(parsed); } #formatVectorFromArray(values: unknown[]): string { diff --git a/packages/snowflake/src/_internal/data-types-overrides.ts b/packages/snowflake/src/_internal/data-types-overrides.ts index af2aff87c146..d63c3ccd89f5 100644 --- a/packages/snowflake/src/_internal/data-types-overrides.ts +++ b/packages/snowflake/src/_internal/data-types-overrides.ts @@ -112,7 +112,7 @@ export class VECTOR extends BaseTypes.VECTOR { return [options.format, String(options.dimension)]; } - validate(value: unknown): asserts value is BaseTypes.Vector { + validate(value: unknown): asserts value is BaseTypes.VectorValue { super.validate(value); const options = this.#getSnowflakeOptions(); @@ -133,52 +133,25 @@ export class VECTOR extends BaseTypes.VECTOR { switch (normalized) { case 'INT': + case 'INT8': + case 'INT16': + case 'INT32': + return 'INT'; case 'FLOAT': - return normalized; + case 'FLOAT32': + case 'FLOAT64': + return 'FLOAT'; default: throw new TypeError(`Invalid Snowflake VECTOR format: ${format}`); } } - #getVectorLength(value: BaseTypes.Vector): number { + #getVectorLength(value: BaseTypes.VectorValue): number { if (Array.isArray(value)) { return value.length; } - if ( - value instanceof Int8Array || - value instanceof Uint8Array || - value instanceof Uint8ClampedArray || - value instanceof Int16Array || - value instanceof Uint16Array || - value instanceof Int32Array || - value instanceof Uint32Array || - value instanceof Float32Array || - value instanceof Float64Array || - value instanceof BigInt64Array || - value instanceof BigUint64Array - ) { - return value.length; - } - - throw new TypeError('Unsupported vector container type'); - } - - protected _validateVectorElement(item: unknown): number { - const numeric = super._validateVectorElement(item); - const options = this.#getSnowflakeOptions(); - - if (options.format === 'INT' && !Number.isInteger(numeric)) { - ValidationErrorItem.throwDataTypeValidationError( - util.format( - 'VECTOR(INT, %d) only accepts integers, but received %O', - options.dimension, - numeric, - ), - ); - } - - return numeric; + return value.length; } #getSnowflakeOptions(): SnowflakeVectorOptions { diff --git a/packages/snowflake/src/query-generator.internal.ts b/packages/snowflake/src/query-generator.internal.ts index 206984c0e4da..86a897f542bd 100644 --- a/packages/snowflake/src/query-generator.internal.ts +++ b/packages/snowflake/src/query-generator.internal.ts @@ -75,8 +75,9 @@ export class SnowflakeQueryGeneratorInternal< if (typeof arg === 'string') { const trimmed = arg.trim(); - if (this.#looksLikeVectorLiteral(trimmed)) { - return trimmed; + const literal = this.#parseVectorLiteral(trimmed); + if (literal) { + return literal; } if (trimmed.startsWith('[') && trimmed.endsWith(']')) { @@ -96,15 +97,34 @@ export class SnowflakeQueryGeneratorInternal< ); } - // Snowflake users can still pass explicit SQL snippets (for instance reusing the result of TO_VECTOR); - // keep the heuristics generous so we do not re-wrap values that already carry proper typing. - #looksLikeVectorLiteral(literal: string): boolean { - return ( - literal.toUpperCase().includes('::VECTOR') || - literal.startsWith('[') || - literal.startsWith('(') || - literal.startsWith('VECTOR_') + #parseVectorLiteral(literal: string): string | null { + const match = literal.match( + /^(\[[^\]]+\])\s*::\s*VECTOR\s*\(\s*(FLOAT|INT)\s*,\s*(\d+)\s*\)$/i, ); + if (!match) { + return null; + } + + const [, body, elementType, dimension] = match; + + let parsed: unknown; + try { + parsed = JSON.parse(body); + } catch { + return null; + } + + if (!Array.isArray(parsed)) { + return null; + } + + if (parsed.length !== Number(dimension)) { + throw new Error( + `Vector literal dimension ${dimension} does not match ${parsed.length} values`, + ); + } + + return this.#formatVectorFromArray(parsed, elementType.toUpperCase() as 'FLOAT' | 'INT'); } // Snowflake documents vector literals as ARRAY values cast with ::VECTOR(type, dimension), From a95f900c0be308ffd4202a47fc5b9a76b9eadc70 Mon Sep 17 00:00:00 2001 From: sudarshan12s Date: Wed, 20 May 2026 16:26:34 +0530 Subject: [PATCH 25/49] - minor change in test --- .../test/integration/dialects/oracle/vector.test.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/core/test/integration/dialects/oracle/vector.test.js b/packages/core/test/integration/dialects/oracle/vector.test.js index 645a4f0e285a..20d2578d10d1 100644 --- a/packages/core/test/integration/dialects/oracle/vector.test.js +++ b/packages/core/test/integration/dialects/oracle/vector.test.js @@ -253,16 +253,13 @@ if (getTestDialect() === 'oracle') { expect(Array.from(item.getDataValue('embeddings'))).to.deep.equal([4, 5, 6]); }); - for (const [name, value] of [ - ['DataView', new DataView(new ArrayBuffer(3))], - ['string', '1,2,3'], - ['non-number array', [1, '2', 3]], + for (const [name, value, error] of [ + ['DataView', new DataView(new ArrayBuffer(3)), 'is not a valid vector'], + ['string', '1,2,3', 'is not a valid vector'], + ['non-number array', [1, 'a', 3], 'ORA-51805'], ]) { it(`rejects ${name} input`, async function () { - await expect(this.Item.create({ embeddings: value })).to.be.rejectedWith( - Error, - 'is not a valid vector', - ); + await expect(this.Item.create({ embeddings: value })).to.be.rejectedWith(Error, error); }); } From ae828d6ec63d1782be2d71afffab9fd96e47d7c1 Mon Sep 17 00:00:00 2001 From: sudarshan12s Date: Wed, 20 May 2026 17:58:57 +0530 Subject: [PATCH 26/49] reject unsupported VECTOR index options like unique, where, include, etc. validate using to only allow hnsw or ivf allowlist distance metrics before emitting WITH DISTANCE require numeric, finite, positive values for accuracy and vector index parameters quote dropConstraintQuery constraint names --- .../test/unit/dialects/oracle/vector.test.js | 60 +++++++- packages/oracle/src/query-generator.js | 143 +++++++++++++++--- 2 files changed, 179 insertions(+), 24 deletions(-) diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index 8115492452e8..7f77afcaf390 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -4,6 +4,9 @@ const util = require('node:util'); const Support = require('../../../support'); const { DataTypes, Op, sql } = require('@sequelize/core'); const BaseTypes = require('@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/data-types.js'); +const { + buildInvalidOptionReceivedError, +} = require('@sequelize/core/_non-semver-use-at-your-own-risk_/utils/check.js'); const { OracleQuery } = require('@sequelize/oracle'); const { expect } = require('chai'); @@ -114,17 +117,13 @@ if (current.dialect.name === 'oracle') { ); }); - it('treats non-hnsw using as ivf organization', () => { - expectsql( + it('rejects unsupported vector index using values', () => { + expect(() => queryGenerator.addIndexQuery('foo', ['vec1'], { type: 'VECTOR', using: 'btree', }), - { - default: - 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION NEIGHBOR PARTITION GRAPH', - }, - ); + ).to.throw(TypeError, 'Oracle VECTOR index using must be either "hnsw" or "ivf".'); }); it('accepts uppercase using value', () => { @@ -195,6 +194,45 @@ if (current.dialect.name === 'oracle') { ); }); + it('rejects unsupported vector index options instead of ignoring them', () => { + expect(() => + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'VECTOR', + unique: true, + }), + ).to.throw(buildInvalidOptionReceivedError('addIndexQuery', 'oracle', ['unique']).message); + }); + + it('rejects unsafe vector index distance fragments', () => { + expect(() => + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'VECTOR', + distance: 'COSINE) PARAMETERS (type hnsw', + }), + ).to.throw(TypeError, 'Oracle VECTOR index distance must be one of'); + }); + + it('rejects unsafe vector index accuracy fragments', () => { + expect(() => + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'VECTOR', + accuracy: '95) PARAMETERS (type hnsw', + }), + ).to.throw( + TypeError, + 'Oracle VECTOR index accuracy must be a positive number less than or equal to 100.', + ); + }); + + it('rejects unsafe vector index parameter fragments', () => { + expect(() => + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'VECTOR', + parameter: { neighbor: '8) PARAMETERS (type ivf' }, + }), + ).to.throw(TypeError, 'Oracle VECTOR index parameter.neighbor must be a positive integer.'); + }); + it('supports ordered index fields', () => { expectsql( queryGenerator.addIndexQuery('foo', { @@ -239,6 +277,14 @@ if (current.dialect.name === 'oracle') { }); }); + describe('dropConstraintQuery', () => { + it('quotes constraint names', () => { + expectsql(queryGenerator.dropConstraintQuery('foo', 'my_constraint'), { + default: 'ALTER TABLE "foo" DROP CONSTRAINT "my_constraint"', + }); + }); + }); + describe('showIndex metadata', () => { const buildQuery = () => new OracleQuery({}, current, {}); diff --git a/packages/oracle/src/query-generator.js b/packages/oracle/src/query-generator.js index 441758456cb8..8188d1ac4fd7 100644 --- a/packages/oracle/src/query-generator.js +++ b/packages/oracle/src/query-generator.js @@ -33,6 +33,42 @@ import { OracleQueryGeneratorTypeScript } from './query-generator-typescript.int const CREATE_TABLE_QUERY_SUPPORTED_OPTIONS = new Set(['uniqueKeys']); const VECTOR_ORGANIZATION_DEFAULT = 'hnsw'; +const VECTOR_INDEX_QUERY_SUPPORTABLE_OPTIONS = new Set([ + 'name', + 'parser', + 'type', + 'unique', + 'msg', + 'concurrently', + 'fields', + 'using', + 'operator', + 'where', + 'prefix', + 'distance', + 'accuracy', + 'parameter', + 'include', +]); +const VECTOR_INDEX_QUERY_SUPPORTED_OPTIONS = new Set([ + 'name', + 'type', + 'fields', + 'using', + 'prefix', + 'distance', + 'accuracy', + 'parameter', +]); +const VECTOR_INDEX_USING = new Set(['hnsw', 'ivf']); +const VECTOR_INDEX_DISTANCE_METRICS = new Set([ + 'COSINE', + 'DOT', + 'EUCLIDEAN', + 'EUCLIDEAN_SQUARED', + 'HAMMING', + 'MANHATTAN', +]); /** * list of reserved words in Oracle DB 21c @@ -470,6 +506,14 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { throw new Error('Vector indexes require at least one indexed field.'); } + rejectInvalidOptions( + 'addIndexQuery', + this.dialect, + VECTOR_INDEX_QUERY_SUPPORTABLE_OPTIONS, + VECTOR_INDEX_QUERY_SUPPORTED_OPTIONS, + normalizedOptions, + ); + const fieldsSql = fields.map(field => { if (field instanceof BaseSqlExpression) { return this.formatSqlExpression(field); @@ -496,6 +540,11 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { const finalizedOptions = conformIndex(normalizedOptions); const using = toLowerCaseIfString(finalizedOptions.using) ?? VECTOR_ORGANIZATION_DEFAULT; + validateVectorIndexUsing(using); + const distance = normalizeVectorIndexDistance(finalizedOptions.distance); + const accuracy = validateVectorIndexPositiveNumber(finalizedOptions.accuracy, 'accuracy', { + max: 100, + }); const usingIsHnsw = using === 'hnsw'; const parameterFragments = buildVectorParameters(finalizedOptions.parameter, usingIsHnsw); @@ -503,12 +552,8 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { name: finalizedOptions.name, fieldsSql, usingIsHnsw, - distance: finalizedOptions.distance - ? `WITH DISTANCE ${finalizedOptions.distance}` - : undefined, - accuracy: finalizedOptions.accuracy - ? `WITH TARGET ACCURACY ${finalizedOptions.accuracy}` - : undefined, + distance: distance ? `WITH DISTANCE ${distance}` : undefined, + accuracy: accuracy ? `WITH TARGET ACCURACY ${accuracy}` : undefined, parameters: parameterFragments.length > 0 ? `PARAMETERS (${parameterFragments.join(', ')})` : undefined, }; @@ -1135,7 +1180,7 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { } dropConstraintQuery(tableName, constraintName) { - return `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${constraintName}`; + return `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${this.quoteIdentifier(constraintName, true)}`; } _checkValidJsonStatement(stmt) { @@ -1292,33 +1337,97 @@ function buildVectorParameters(parameter, usingIsHnsw) { if (usingIsHnsw) { parameters.push('type hnsw'); - if (parameter.neighbor) { - parameters.push(`neighbor ${parameter.neighbor}`); + const neighbor = validateVectorIndexPositiveInteger(parameter.neighbor, 'parameter.neighbor'); + const efconstruction = validateVectorIndexPositiveInteger( + parameter.efconstruction, + 'parameter.efconstruction', + ); + + if (neighbor) { + parameters.push(`neighbor ${neighbor}`); } - if (parameter.efconstruction) { - parameters.push(`efconstruction ${parameter.efconstruction}`); + if (efconstruction) { + parameters.push(`efconstruction ${efconstruction}`); } return parameters; } parameters.push('type ivf'); - if (parameter.partitions) { - parameters.push(`NEIGHBOR PARTITION ${parameter.partitions}`); + const partitions = validateVectorIndexPositiveInteger(parameter.partitions, 'parameter.partitions'); + const samplesPerPartition = validateVectorIndexPositiveInteger( + parameter.samplesPerPartition, + 'parameter.samplesPerPartition', + ); + const minVectors = validateVectorIndexPositiveInteger(parameter.minVectors, 'parameter.minVectors'); + + if (partitions) { + parameters.push(`NEIGHBOR PARTITION ${partitions}`); } - if (parameter.samplesPerPartition) { - parameters.push(`SAMPLES_PER_PARTITION ${parameter.samplesPerPartition}`); + if (samplesPerPartition) { + parameters.push(`SAMPLES_PER_PARTITION ${samplesPerPartition}`); } - if (parameter.minVectors) { - parameters.push(`MIN_VECTORS_PER_PARTITION ${parameter.minVectors}`); + if (minVectors) { + parameters.push(`MIN_VECTORS_PER_PARTITION ${minVectors}`); } return parameters; } +function validateVectorIndexUsing(using) { + if (VECTOR_INDEX_USING.has(using)) { + return; + } + + throw new TypeError('Oracle VECTOR index using must be either "hnsw" or "ivf".'); +} + +function normalizeVectorIndexDistance(distance) { + if (distance == null) { + return undefined; + } + + if (typeof distance !== 'string') { + throw new TypeError('Oracle VECTOR index distance must be a string.'); + } + + const normalizedDistance = distance.toUpperCase(); + if (VECTOR_INDEX_DISTANCE_METRICS.has(normalizedDistance)) { + return normalizedDistance; + } + + throw new TypeError( + `Oracle VECTOR index distance must be one of: ${[...VECTOR_INDEX_DISTANCE_METRICS].join(', ')}.`, + ); +} + +function validateVectorIndexPositiveInteger(value, optionName) { + return validateVectorIndexPositiveNumber(value, optionName, { integer: true }); +} + +function validateVectorIndexPositiveNumber(value, optionName, { integer = false, max } = {}) { + if (value == null) { + return undefined; + } + + if ( + typeof value !== 'number' || + !Number.isFinite(value) || + value <= 0 || + (integer && !Number.isSafeInteger(value)) || + (max != null && value > max) + ) { + const type = integer ? 'positive integer' : 'positive number'; + const suffix = max == null ? '' : ` less than or equal to ${max}`; + throw new TypeError(`Oracle VECTOR index ${optionName} must be a ${type}${suffix}.`); + } + + return value; +} + /* istanbul ignore next */ function throwMethodUndefined(methodName) { throw new Error(`The method "${methodName}" is not defined! Please add it to your sql dialect.`); From 2bef55ff0126335fb6f6c55c5f3ac4d0b01f5c2a Mon Sep 17 00:00:00 2001 From: prajalg Date: Wed, 20 May 2026 18:37:53 +0530 Subject: [PATCH 27/49] lint fix --- packages/oracle/src/query-generator.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/oracle/src/query-generator.js b/packages/oracle/src/query-generator.js index 8188d1ac4fd7..e59365a9ab68 100644 --- a/packages/oracle/src/query-generator.js +++ b/packages/oracle/src/query-generator.js @@ -1355,12 +1355,18 @@ function buildVectorParameters(parameter, usingIsHnsw) { } parameters.push('type ivf'); - const partitions = validateVectorIndexPositiveInteger(parameter.partitions, 'parameter.partitions'); + const partitions = validateVectorIndexPositiveInteger( + parameter.partitions, + 'parameter.partitions', + ); const samplesPerPartition = validateVectorIndexPositiveInteger( parameter.samplesPerPartition, 'parameter.samplesPerPartition', ); - const minVectors = validateVectorIndexPositiveInteger(parameter.minVectors, 'parameter.minVectors'); + const minVectors = validateVectorIndexPositiveInteger( + parameter.minVectors, + 'parameter.minVectors', + ); if (partitions) { parameters.push(`NEIGHBOR PARTITION ${partitions}`); From ec5ba746fa05aafee60f2b5fa766d74ab75ef3b7 Mon Sep 17 00:00:00 2001 From: prajalg Date: Wed, 20 May 2026 18:59:06 +0530 Subject: [PATCH 28/49] minor test fix --- packages/core/test/unit/data-types/string-types.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/unit/data-types/string-types.test.ts b/packages/core/test/unit/data-types/string-types.test.ts index 3f05bb86f3ee..1eb4721004e8 100644 --- a/packages/core/test/unit/data-types/string-types.test.ts +++ b/packages/core/test/unit/data-types/string-types.test.ts @@ -306,7 +306,7 @@ if (sequelize.dialect.supports?.dataTypes?.VECTOR) { See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR(3, FLOAT32)', postgres: 'VECTOR(3)', - snowflake: new Error('Invalid Snowflake VECTOR format: float32'), + snowflake: 'VECTOR(FLOAT, 3)', }); testDataTypeSql("VECTOR(24, 'binary')", DataTypes.VECTOR(24, 'binary'), { From ee40489897c30fee26b7c3d7d9cdf91b3b90e33b Mon Sep 17 00:00:00 2001 From: prajalg Date: Wed, 20 May 2026 19:57:01 +0530 Subject: [PATCH 29/49] add stricter check for using (only allow "hnsw" or "ivf") and order ("ASC" or "DESC") --- .../test/unit/dialects/oracle/vector.test.js | 18 ++++++++++ packages/oracle/src/query-generator.js | 34 ++++++++++++++----- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index 7f77afcaf390..9d1eb75c252b 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -126,6 +126,15 @@ if (current.dialect.name === 'oracle') { ).to.throw(TypeError, 'Oracle VECTOR index using must be either "hnsw" or "ivf".'); }); + it('rejects non-string vector index using values', () => { + expect(() => + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'VECTOR', + using: 123, + }), + ).to.throw(TypeError, 'Oracle VECTOR index using must be either "hnsw" or "ivf".'); + }); + it('accepts uppercase using value', () => { expectsql( queryGenerator.addIndexQuery('foo', ['vec1'], { @@ -246,6 +255,15 @@ if (current.dialect.name === 'oracle') { ); }); + it('rejects unsafe vector index field order fragments', () => { + expect(() => + queryGenerator.addIndexQuery('foo', { + fields: [{ name: 'vec1', order: 'DESC) PARAMETERS (type ivf --' }], + type: 'VECTOR', + }), + ).to.throw(TypeError, 'Oracle VECTOR index field order must be either "ASC" or "DESC".'); + }); + it('supports SQL expression index fields', () => { expectsql( queryGenerator.addIndexQuery('foo', { diff --git a/packages/oracle/src/query-generator.js b/packages/oracle/src/query-generator.js index e59365a9ab68..b39f2f63f097 100644 --- a/packages/oracle/src/query-generator.js +++ b/packages/oracle/src/query-generator.js @@ -529,9 +529,13 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { throw new Error(`The following index field has no name: ${util.inspect(field)}`); } - return normalizedField.order - ? `${this.quoteIdentifier(normalizedField.name)} ${normalizedField.order}` - : this.quoteIdentifier(normalizedField.name); + if (!normalizedField.order) { + return this.quoteIdentifier(normalizedField.name); + } + + const order = normalizeVectorIndexFieldOrder(normalizedField.order); + + return `${this.quoteIdentifier(normalizedField.name)} ${order}`; }); if (!normalizedOptions.name) { @@ -539,7 +543,12 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { } const finalizedOptions = conformIndex(normalizedOptions); - const using = toLowerCaseIfString(finalizedOptions.using) ?? VECTOR_ORGANIZATION_DEFAULT; + const rawUsing = finalizedOptions.using; + if (rawUsing != null && typeof rawUsing !== 'string') { + throw new TypeError('Oracle VECTOR index using must be either "hnsw" or "ivf".'); + } + + const using = rawUsing == null ? VECTOR_ORGANIZATION_DEFAULT : rawUsing.toLowerCase(); validateVectorIndexUsing(using); const distance = normalizeVectorIndexDistance(finalizedOptions.distance); const accuracy = validateVectorIndexPositiveNumber(finalizedOptions.accuracy, 'accuracy', { @@ -1317,10 +1326,6 @@ function toUpperCaseIfString(value) { return typeof value === 'string' ? value.toUpperCase() : undefined; } -function toLowerCaseIfString(value) { - return typeof value === 'string' ? value.toLowerCase() : undefined; -} - function sanitizePrefix(prefix) { if (typeof prefix !== 'string') { return prefix; @@ -1391,6 +1396,19 @@ function validateVectorIndexUsing(using) { throw new TypeError('Oracle VECTOR index using must be either "hnsw" or "ivf".'); } +function normalizeVectorIndexFieldOrder(order) { + if (typeof order !== 'string') { + throw new TypeError('Oracle VECTOR index field order must be either "ASC" or "DESC".'); + } + + const normalizedOrder = order.toUpperCase(); + if (['ASC', 'DESC'].includes(normalizedOrder)) { + return normalizedOrder; + } + + throw new TypeError('Oracle VECTOR index field order must be either "ASC" or "DESC".'); +} + function normalizeVectorIndexDistance(distance) { if (distance == null) { return undefined; From 292b3482c521fa377d577d95f9a8800efcd5ced7 Mon Sep 17 00:00:00 2001 From: prajalg Date: Wed, 20 May 2026 20:07:26 +0530 Subject: [PATCH 30/49] removed custom isShowIndexesQuery() for oracle --- packages/oracle/src/query.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/oracle/src/query.js b/packages/oracle/src/query.js index 5bf92466c0e0..3b7a5cd81e2c 100644 --- a/packages/oracle/src/query.js +++ b/packages/oracle/src/query.js @@ -663,10 +663,6 @@ export class OracleQuery extends AbstractQuery { return new DatabaseError(err); } - isShowIndexesQuery() { - return this.sql.includes('SELECT i.index_name,i.table_name, i.column_name, u.uniqueness'); - } - isSelectCountQuery() { return this.sql.toUpperCase().includes('SELECT COUNT('); } From f8a701bcd2e9d695ff976a06620bb044de503856 Mon Sep 17 00:00:00 2001 From: prajalg Date: Wed, 20 May 2026 20:25:10 +0530 Subject: [PATCH 31/49] minor cleanups --- .../test/unit/dialects/oracle/vector.test.js | 26 +++++++++++++++++++ packages/oracle/src/query-generator.js | 14 ++++++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index 9d1eb75c252b..a85aa33c0b33 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -68,6 +68,19 @@ if (current.dialect.name === 'oracle') { }); }); + it('defaults using to hnsw when not provided', () => { + expectsql( + queryGenerator.addIndexQuery('foo', { + fields: ['vec1'], + type: 'VECTOR', + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION INMEMORY NEIGHBOR GRAPH', + }, + ); + }); + it('accepts lowercase vector type', () => { expectsql(queryGenerator.addIndexQuery('Foo', ['vec1'], { type: 'vector' }), { default: @@ -255,6 +268,19 @@ if (current.dialect.name === 'oracle') { ); }); + it('normalizes lowercase index field order to uppercase', () => { + expectsql( + queryGenerator.addIndexQuery('foo', { + fields: [{ name: 'vec1', order: 'asc' }], + type: 'VECTOR', + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1" ASC) ORGANIZATION INMEMORY NEIGHBOR GRAPH', + }, + ); + }); + it('rejects unsafe vector index field order fragments', () => { expect(() => queryGenerator.addIndexQuery('foo', { diff --git a/packages/oracle/src/query-generator.js b/packages/oracle/src/query-generator.js index b39f2f63f097..96390843e0d7 100644 --- a/packages/oracle/src/query-generator.js +++ b/packages/oracle/src/query-generator.js @@ -61,6 +61,7 @@ const VECTOR_INDEX_QUERY_SUPPORTED_OPTIONS = new Set([ 'parameter', ]); const VECTOR_INDEX_USING = new Set(['hnsw', 'ivf']); +const VECTOR_INDEX_USING_ERROR = 'Oracle VECTOR index using must be either "hnsw" or "ivf".'; const VECTOR_INDEX_DISTANCE_METRICS = new Set([ 'COSINE', 'DOT', @@ -69,6 +70,9 @@ const VECTOR_INDEX_DISTANCE_METRICS = new Set([ 'HAMMING', 'MANHATTAN', ]); +const VECTOR_INDEX_FIELD_ORDERS = new Set(['ASC', 'DESC']); +const VECTOR_INDEX_FIELD_ORDER_ERROR = + 'Oracle VECTOR index field order must be either "ASC" or "DESC".'; /** * list of reserved words in Oracle DB 21c @@ -545,7 +549,7 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { const finalizedOptions = conformIndex(normalizedOptions); const rawUsing = finalizedOptions.using; if (rawUsing != null && typeof rawUsing !== 'string') { - throw new TypeError('Oracle VECTOR index using must be either "hnsw" or "ivf".'); + throw new TypeError(VECTOR_INDEX_USING_ERROR); } const using = rawUsing == null ? VECTOR_ORGANIZATION_DEFAULT : rawUsing.toLowerCase(); @@ -1393,20 +1397,20 @@ function validateVectorIndexUsing(using) { return; } - throw new TypeError('Oracle VECTOR index using must be either "hnsw" or "ivf".'); + throw new TypeError(VECTOR_INDEX_USING_ERROR); } function normalizeVectorIndexFieldOrder(order) { if (typeof order !== 'string') { - throw new TypeError('Oracle VECTOR index field order must be either "ASC" or "DESC".'); + throw new TypeError(VECTOR_INDEX_FIELD_ORDER_ERROR); } const normalizedOrder = order.toUpperCase(); - if (['ASC', 'DESC'].includes(normalizedOrder)) { + if (VECTOR_INDEX_FIELD_ORDERS.has(normalizedOrder)) { return normalizedOrder; } - throw new TypeError('Oracle VECTOR index field order must be either "ASC" or "DESC".'); + throw new TypeError(VECTOR_INDEX_FIELD_ORDER_ERROR); } function normalizeVectorIndexDistance(distance) { From 407de39cb98335b965ad96a242f9b606212bc630 Mon Sep 17 00:00:00 2001 From: sudarshan12s Date: Thu, 21 May 2026 12:16:21 +0530 Subject: [PATCH 32/49] - correct reading index subtype --- .../test/unit/dialects/oracle/vector.test.js | 7 ++----- .../src/query-generator-typescript.internal.ts | 2 +- packages/oracle/src/query.js | 18 ++++++++---------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index 7f77afcaf390..140737a256dd 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -294,13 +294,13 @@ if (current.dialect.name === 'oracle') { { INDEX_NAME: 'IDX_HNSW', INDEX_TYPE: 'VECTOR', + INDEX_SUBTYPE: 'INMEMORY_NEIGHBOR_GRAPH_HNSW', ITYP_NAME: null, UNIQUENESS: 'NONUNIQUE', CONSTRAINT_TYPE: null, TABLE_NAME: 'FOO', COLUMN_NAME: 'EMBEDDING', DESCEND: null, - PARAMETERS_LOWER: 'type hnsw neighbor 10', }, ]; @@ -314,13 +314,13 @@ if (current.dialect.name === 'oracle') { { INDEX_NAME: 'IDX_IVF', INDEX_TYPE: 'VECTOR', + INDEX_SUBTYPE: 'NEIGHBOR_PARTITION_GRAPH_IVF', ITYP_NAME: null, UNIQUENESS: 'NONUNIQUE', CONSTRAINT_TYPE: null, TABLE_NAME: 'FOO', COLUMN_NAME: 'EMBEDDING', DESCEND: null, - PARAMETERS_LOWER: 'type ivf neighbor partition 8', }, ]; @@ -340,7 +340,6 @@ if (current.dialect.name === 'oracle') { TABLE_NAME: 'FOO', COLUMN_NAME: 'EMBEDDING', DESCEND: null, - PARAMETERS_LOWER: null, }, ]; @@ -360,7 +359,6 @@ if (current.dialect.name === 'oracle') { TABLE_NAME: 'FOO', COLUMN_NAME: 'ID', DESCEND: null, - PARAMETERS_LOWER: null, }, ]; @@ -380,7 +378,6 @@ if (current.dialect.name === 'oracle') { TABLE_NAME: 'FOO', COLUMN_NAME: 'ID', DESCEND: null, - PARAMETERS_LOWER: null, }, ]; diff --git a/packages/oracle/src/query-generator-typescript.internal.ts b/packages/oracle/src/query-generator-typescript.internal.ts index 18b6440061d8..a6a62b0abb50 100644 --- a/packages/oracle/src/query-generator-typescript.internal.ts +++ b/packages/oracle/src/query-generator-typescript.internal.ts @@ -109,7 +109,7 @@ export class OracleQueryGeneratorTypeScript extends AbstractQueryGenerator { showIndexesQuery(table: TableNameWithSchema) { const [tableName, owner] = this.getSchemaNameAndTableName(table); const sql = [ - 'SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type ', + 'SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.index_subtype, u.ityp_name, i.descend, c.constraint_type ', 'FROM all_ind_columns i ', 'INNER JOIN all_indexes u ', 'ON (u.table_name = i.table_name AND u.index_name = i.index_name) ', diff --git a/packages/oracle/src/query.js b/packages/oracle/src/query.js index 5bf92466c0e0..5a25cf594987 100644 --- a/packages/oracle/src/query.js +++ b/packages/oracle/src/query.js @@ -684,7 +684,7 @@ export class OracleQuery extends AbstractQuery { if (oracleIndexType === 'VECTOR') { type = 'VECTOR'; - method = this.#determineVectorMethod(indexRecord.PARAMETERS_LOWER); + method = this.#determineVectorMethod(indexRecord.INDEX_SUBTYPE); } else if (oracleIndexType === 'NORMAL' || !oracleIndexType) { type = undefined; } else { @@ -735,17 +735,15 @@ export class OracleQuery extends AbstractQuery { return returnIndexes; } - // Oracle surfaces vector index configuration as a lower-cased parameter string. - // Parse out the declared index type so consumers can see whether the index is IVF or HNSW. - #determineVectorMethod(parametersLower) { - if (typeof parametersLower !== 'string') { - return; + // Oracle exposes vector index method in INDEX_SUBTYPE. + #determineVectorMethod(indexSubtype) { + const subtype = typeof indexSubtype === 'string' ? indexSubtype.toLowerCase() : undefined; + if (subtype?.includes('hnsw')) { + return 'hnsw'; } - for (const type of ['ivf', 'hnsw']) { - if (parametersLower.includes(`type ${type}`)) { - return type; - } + if (subtype?.includes('ivf')) { + return 'ivf'; } } From 27fbf1954c0be4681f2ad1f8c9a3d2db01edc436 Mon Sep 17 00:00:00 2001 From: sudarshan12s Date: Thu, 21 May 2026 12:33:35 +0530 Subject: [PATCH 33/49] - fix unit test for index sql change --- .../unit/query-generator/show-indexes-query.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/test/unit/query-generator/show-indexes-query.test.ts b/packages/core/test/unit/query-generator/show-indexes-query.test.ts index c89673c31d25..be16bda708eb 100644 --- a/packages/core/test/unit/query-generator/show-indexes-query.test.ts +++ b/packages/core/test/unit/query-generator/show-indexes-query.test.ts @@ -29,7 +29,7 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = CURRENT SCHEMA and QSYS2.SYSINDEXES.TABLE_NAME = 'myTable'`, - oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type + oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.index_subtype, u.ityp_name, i.descend, c.constraint_type FROM all_ind_columns i INNER JOIN all_indexes u ON (u.table_name = i.table_name AND u.index_name = i.index_name) LEFT OUTER JOIN all_constraints c ON (c.table_name = i.table_name AND c.index_name = i.index_name) @@ -63,7 +63,7 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = CURRENT SCHEMA and QSYS2.SYSINDEXES.TABLE_NAME = 'MyModels'`, - oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type + oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.index_subtype, u.ityp_name, i.descend, c.constraint_type FROM all_ind_columns i INNER JOIN all_indexes u ON (u.table_name = i.table_name AND u.index_name = i.index_name) LEFT OUTER JOIN all_constraints c ON (c.table_name = i.table_name AND c.index_name = i.index_name) @@ -98,7 +98,7 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = CURRENT SCHEMA and QSYS2.SYSINDEXES.TABLE_NAME = 'MyModels'`, - oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type FROM all_ind_columns i + oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.index_subtype, u.ityp_name, i.descend, c.constraint_type FROM all_ind_columns i INNER JOIN all_indexes u ON (u.table_name = i.table_name AND u.index_name = i.index_name) LEFT OUTER JOIN all_constraints c ON (c.table_name = i.table_name AND c.index_name = i.index_name) WHERE i.table_name = 'MyModels' AND u.table_owner = '${dialect.getDefaultSchema()}' ORDER BY index_name, column_position`, }); @@ -128,7 +128,7 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = 'mySchema' and QSYS2.SYSINDEXES.TABLE_NAME = 'myTable'`, - oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type + oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.index_subtype, u.ityp_name, i.descend, c.constraint_type FROM all_ind_columns i INNER JOIN all_indexes u ON (u.table_name = i.table_name AND u.index_name = i.index_name) LEFT OUTER JOIN all_constraints c ON (c.table_name = i.table_name AND c.index_name = i.index_name) @@ -166,7 +166,7 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = CURRENT SCHEMA and QSYS2.SYSINDEXES.TABLE_NAME = 'myTable'`, - oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type + oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.index_subtype, u.ityp_name, i.descend, c.constraint_type FROM all_ind_columns i INNER JOIN all_indexes u ON (u.table_name = i.table_name AND u.index_name = i.index_name) LEFT OUTER JOIN all_constraints c ON (c.table_name = i.table_name AND c.index_name = i.index_name) @@ -202,7 +202,7 @@ describe('QueryGenerator#showIndexesQuery', () => { QSYS2.SYSKEYS.COLUMN_NAME, CAST('INDEX' AS VARCHAR(11)), QSYS2.SYSINDEXES.TABLE_SCHEMA, QSYS2.SYSINDEXES.TABLE_NAME from QSYS2.SYSKEYS left outer join QSYS2.SYSINDEXES on QSYS2.SYSKEYS.INDEX_NAME = QSYS2.SYSINDEXES.INDEX_NAME where QSYS2.SYSINDEXES.TABLE_SCHEMA = 'mySchema' and QSYS2.SYSINDEXES.TABLE_NAME = 'myTable'`, - oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type + oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.index_subtype, u.ityp_name, i.descend, c.constraint_type FROM all_ind_columns i INNER JOIN all_indexes u ON (u.table_name = i.table_name AND u.index_name = i.index_name) LEFT OUTER JOIN all_constraints c ON (c.table_name = i.table_name AND c.index_name = i.index_name) From b05b9f2e62c58f6a569daa0bf096169ba5c200a0 Mon Sep 17 00:00:00 2001 From: sudarshan12s Date: Thu, 21 May 2026 12:56:00 +0530 Subject: [PATCH 34/49] - remove unused code --- packages/core/test/unit/dialects/oracle/vector.test.js | 8 -------- packages/oracle/src/query-generator.js | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index d0c8c9844ea3..77e39cae0a91 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -321,14 +321,6 @@ if (current.dialect.name === 'oracle') { }); }); - describe('dropConstraintQuery', () => { - it('quotes constraint names', () => { - expectsql(queryGenerator.dropConstraintQuery('foo', 'my_constraint'), { - default: 'ALTER TABLE "foo" DROP CONSTRAINT "my_constraint"', - }); - }); - }); - describe('showIndex metadata', () => { const buildQuery = () => new OracleQuery({}, current, {}); diff --git a/packages/oracle/src/query-generator.js b/packages/oracle/src/query-generator.js index 96390843e0d7..a4bf6e9b0f1d 100644 --- a/packages/oracle/src/query-generator.js +++ b/packages/oracle/src/query-generator.js @@ -1193,7 +1193,7 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { } dropConstraintQuery(tableName, constraintName) { - return `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${this.quoteIdentifier(constraintName, true)}`; + return `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${constraintName}`; } _checkValidJsonStatement(stmt) { From e2939f0eab77a55a4e86827579978b7f5927562d Mon Sep 17 00:00:00 2001 From: sudarshan12s Date: Thu, 21 May 2026 14:05:25 +0530 Subject: [PATCH 35/49] - correct some index sql generations --- .../dialects/oracle/vector.test.js | 27 +++++++++ .../test/unit/dialects/oracle/vector.test.js | 57 +++++++++---------- packages/oracle/src/query-generator.js | 37 ++++-------- 3 files changed, 66 insertions(+), 55 deletions(-) diff --git a/packages/core/test/integration/dialects/oracle/vector.test.js b/packages/core/test/integration/dialects/oracle/vector.test.js index 20d2578d10d1..e7b3139db400 100644 --- a/packages/core/test/integration/dialects/oracle/vector.test.js +++ b/packages/core/test/integration/dialects/oracle/vector.test.js @@ -339,6 +339,33 @@ if (getTestDialect() === 'oracle') { } } }); + + it('rejects ordered vector index fields during sync', async () => { + const IndexedItem = sequelize.define( + 'VectorOrderedIndexedItem', + { + embeddings: DataTypes.VECTOR(3), + }, + { + indexes: [ + { + name: 'vector_input_item_embeddings_desc_idx', + type: 'VECTOR', + fields: [{ name: 'embeddings', order: 'desc' }], + using: 'hnsw', + }, + ], + }, + ); + + try { + await expect(IndexedItem.sync({ force: true })).to.be.rejectedWith( + 'Oracle VECTOR indexes do not support ordered fields.', + ); + } finally { + await IndexedItem.drop(); + } + }); }); }); } diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index 77e39cae0a91..b7493c9f9c7c 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -97,7 +97,7 @@ if (current.dialect.name === 'oracle') { }), { default: - 'CREATE VECTOR INDEX "foo_vec1" ON "Foo" ("vec1") ORGANIZATION NEIGHBOR PARTITION GRAPH', + 'CREATE VECTOR INDEX "foo_vec1" ON "Foo" ("vec1") ORGANIZATION NEIGHBOR PARTITIONS', }, ); }); @@ -112,7 +112,7 @@ if (current.dialect.name === 'oracle') { }), { default: - 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION NEIGHBOR PARTITION GRAPH WITH TARGET ACCURACY 95 PARAMETERS (type ivf, NEIGHBOR PARTITION 5, SAMPLES_PER_PARTITION 10, MIN_VECTORS_PER_PARTITION 10)', + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION NEIGHBOR PARTITIONS WITH TARGET ACCURACY 95 PARAMETERS (type ivf, NEIGHBOR PARTITIONS 5, SAMPLES_PER_PARTITION 10, MIN_VECTORS_PER_PARTITION 10)', }, ); }); @@ -169,7 +169,7 @@ if (current.dialect.name === 'oracle') { }), { default: - 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION NEIGHBOR PARTITION GRAPH', + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION NEIGHBOR PARTITIONS', }, ); }); @@ -197,7 +197,7 @@ if (current.dialect.name === 'oracle') { }), { default: - 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION NEIGHBOR PARTITION GRAPH PARAMETERS (type ivf)', + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION NEIGHBOR PARTITIONS PARAMETERS (type ivf)', }, ); }); @@ -216,6 +216,20 @@ if (current.dialect.name === 'oracle') { ); }); + it('supports parallel degree in SQL', () => { + expectsql( + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'VECTOR', + using: 'ivf', + parallel: 4, + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION NEIGHBOR PARTITIONS PARALLEL 4', + }, + ); + }); + it('rejects unsupported vector index options instead of ignoring them', () => { expect(() => queryGenerator.addIndexQuery('foo', ['vec1'], { @@ -242,7 +256,7 @@ if (current.dialect.name === 'oracle') { }), ).to.throw( TypeError, - 'Oracle VECTOR index accuracy must be a positive number less than or equal to 100.', + 'Oracle VECTOR index accuracy must be a positive number.', ); }); @@ -255,39 +269,22 @@ if (current.dialect.name === 'oracle') { ).to.throw(TypeError, 'Oracle VECTOR index parameter.neighbor must be a positive integer.'); }); - it('supports ordered index fields', () => { - expectsql( - queryGenerator.addIndexQuery('foo', { - fields: [{ name: 'vec1', order: 'DESC' }], - type: 'VECTOR', - }), - { - default: - 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1" DESC) ORGANIZATION INMEMORY NEIGHBOR GRAPH', - }, - ); - }); - - it('normalizes lowercase index field order to uppercase', () => { - expectsql( - queryGenerator.addIndexQuery('foo', { - fields: [{ name: 'vec1', order: 'asc' }], + it('rejects unsafe vector index parallel fragments', () => { + expect(() => + queryGenerator.addIndexQuery('foo', ['vec1'], { type: 'VECTOR', + parallel: '4 PARAMETERS (type hnsw)', }), - { - default: - 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1" ASC) ORGANIZATION INMEMORY NEIGHBOR GRAPH', - }, - ); + ).to.throw(TypeError, 'Oracle VECTOR index parallel must be a positive integer.'); }); - it('rejects unsafe vector index field order fragments', () => { + it('rejects ordered vector index fields', () => { expect(() => queryGenerator.addIndexQuery('foo', { - fields: [{ name: 'vec1', order: 'DESC) PARAMETERS (type ivf --' }], + fields: [{ name: 'vec1', order: 'DESC' }], type: 'VECTOR', }), - ).to.throw(TypeError, 'Oracle VECTOR index field order must be either "ASC" or "DESC".'); + ).to.throw(TypeError, 'Oracle VECTOR indexes do not support ordered fields.'); }); it('supports SQL expression index fields', () => { diff --git a/packages/oracle/src/query-generator.js b/packages/oracle/src/query-generator.js index a4bf6e9b0f1d..9ed97781957d 100644 --- a/packages/oracle/src/query-generator.js +++ b/packages/oracle/src/query-generator.js @@ -48,6 +48,7 @@ const VECTOR_INDEX_QUERY_SUPPORTABLE_OPTIONS = new Set([ 'distance', 'accuracy', 'parameter', + 'parallel', 'include', ]); const VECTOR_INDEX_QUERY_SUPPORTED_OPTIONS = new Set([ @@ -59,6 +60,7 @@ const VECTOR_INDEX_QUERY_SUPPORTED_OPTIONS = new Set([ 'distance', 'accuracy', 'parameter', + 'parallel', ]); const VECTOR_INDEX_USING = new Set(['hnsw', 'ivf']); const VECTOR_INDEX_USING_ERROR = 'Oracle VECTOR index using must be either "hnsw" or "ivf".'; @@ -70,9 +72,8 @@ const VECTOR_INDEX_DISTANCE_METRICS = new Set([ 'HAMMING', 'MANHATTAN', ]); -const VECTOR_INDEX_FIELD_ORDERS = new Set(['ASC', 'DESC']); const VECTOR_INDEX_FIELD_ORDER_ERROR = - 'Oracle VECTOR index field order must be either "ASC" or "DESC".'; + 'Oracle VECTOR indexes do not support ordered fields.'; /** * list of reserved words in Oracle DB 21c @@ -476,10 +477,11 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { `ON ${this.quoteTable(tableName)}`, `(${normalizedOptions.fieldsSql.join(', ')})`, 'ORGANIZATION', - normalizedOptions.usingIsHnsw ? 'INMEMORY NEIGHBOR GRAPH' : 'NEIGHBOR PARTITION GRAPH', + normalizedOptions.usingIsHnsw ? 'INMEMORY NEIGHBOR GRAPH' : 'NEIGHBOR PARTITIONS', normalizedOptions.distance, normalizedOptions.accuracy, normalizedOptions.parameters, + normalizedOptions.parallel, ]); } @@ -533,13 +535,11 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { throw new Error(`The following index field has no name: ${util.inspect(field)}`); } - if (!normalizedField.order) { - return this.quoteIdentifier(normalizedField.name); + if (normalizedField.order) { + throw new TypeError(VECTOR_INDEX_FIELD_ORDER_ERROR); } - const order = normalizeVectorIndexFieldOrder(normalizedField.order); - - return `${this.quoteIdentifier(normalizedField.name)} ${order}`; + return this.quoteIdentifier(normalizedField.name); }); if (!normalizedOptions.name) { @@ -555,9 +555,8 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { const using = rawUsing == null ? VECTOR_ORGANIZATION_DEFAULT : rawUsing.toLowerCase(); validateVectorIndexUsing(using); const distance = normalizeVectorIndexDistance(finalizedOptions.distance); - const accuracy = validateVectorIndexPositiveNumber(finalizedOptions.accuracy, 'accuracy', { - max: 100, - }); + const accuracy = validateVectorIndexPositiveNumber(finalizedOptions.accuracy, 'accuracy'); + const parallel = validateVectorIndexPositiveInteger(finalizedOptions.parallel, 'parallel'); const usingIsHnsw = using === 'hnsw'; const parameterFragments = buildVectorParameters(finalizedOptions.parameter, usingIsHnsw); @@ -569,6 +568,7 @@ export class OracleQueryGenerator extends OracleQueryGeneratorTypeScript { accuracy: accuracy ? `WITH TARGET ACCURACY ${accuracy}` : undefined, parameters: parameterFragments.length > 0 ? `PARAMETERS (${parameterFragments.join(', ')})` : undefined, + parallel: parallel ? `PARALLEL ${parallel}` : undefined, }; } @@ -1378,7 +1378,7 @@ function buildVectorParameters(parameter, usingIsHnsw) { ); if (partitions) { - parameters.push(`NEIGHBOR PARTITION ${partitions}`); + parameters.push(`NEIGHBOR PARTITIONS ${partitions}`); } if (samplesPerPartition) { @@ -1400,19 +1400,6 @@ function validateVectorIndexUsing(using) { throw new TypeError(VECTOR_INDEX_USING_ERROR); } -function normalizeVectorIndexFieldOrder(order) { - if (typeof order !== 'string') { - throw new TypeError(VECTOR_INDEX_FIELD_ORDER_ERROR); - } - - const normalizedOrder = order.toUpperCase(); - if (VECTOR_INDEX_FIELD_ORDERS.has(normalizedOrder)) { - return normalizedOrder; - } - - throw new TypeError(VECTOR_INDEX_FIELD_ORDER_ERROR); -} - function normalizeVectorIndexDistance(distance) { if (distance == null) { return undefined; From 00ccd154798cc3be46f0408998d86b39cb9f3f72 Mon Sep 17 00:00:00 2001 From: sudarshan12s Date: Thu, 21 May 2026 14:31:22 +0530 Subject: [PATCH 36/49] - add more tests --- .../dialects/oracle/vector.test.js | 39 +++++++++++++++++++ .../test/unit/dialects/oracle/vector.test.js | 13 +++++++ 2 files changed, 52 insertions(+) diff --git a/packages/core/test/integration/dialects/oracle/vector.test.js b/packages/core/test/integration/dialects/oracle/vector.test.js index e7b3139db400..d86e3bf364ca 100644 --- a/packages/core/test/integration/dialects/oracle/vector.test.js +++ b/packages/core/test/integration/dialects/oracle/vector.test.js @@ -340,6 +340,45 @@ if (getTestDialect() === 'oracle') { } }); + it('creates vector index with lowercase distance metric during sync', async () => { + const indexName = 'vector_input_item_embeddings_cosine_idx'; + const IndexedItem = sequelize.define( + 'VectorDistanceIndexedItem', + { + embeddings: DataTypes.VECTOR(3), + }, + { + indexes: [ + { + name: indexName, + type: 'VECTOR', + fields: ['embeddings'], + using: 'hnsw', + distance: 'cosine', + }, + ], + }, + ); + + try { + await IndexedItem.sync({ force: true }); + + const indexes = await sequelize.queryInterface.showIndex(IndexedItem.table); + const vectorIndex = indexes.find( + index => index.name?.toLowerCase() === indexName.toLowerCase(), + ); + + expect(vectorIndex).to.not.equal(undefined); + expect(vectorIndex.type).to.equal('VECTOR'); + } finally { + try { + await sequelize.queryInterface.removeIndex(IndexedItem.table, indexName); + } finally { + await IndexedItem.drop(); + } + } + }); + it('rejects ordered vector index fields during sync', async () => { const IndexedItem = sequelize.define( 'VectorOrderedIndexedItem', diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index b7493c9f9c7c..b91fbece3184 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -130,6 +130,19 @@ if (current.dialect.name === 'oracle') { ); }); + it('normalizes lowercase vector distance metric', () => { + expectsql( + queryGenerator.addIndexQuery('foo', ['vec1'], { + type: 'VECTOR', + distance: 'cosine', + }), + { + default: + 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORGANIZATION INMEMORY NEIGHBOR GRAPH WITH DISTANCE COSINE', + }, + ); + }); + it('rejects unsupported vector index using values', () => { expect(() => queryGenerator.addIndexQuery('foo', ['vec1'], { From e06bb13a1260c6a934924a1a44c4048bde1bb5d0 Mon Sep 17 00:00:00 2001 From: sudarshan12s Date: Thu, 21 May 2026 14:34:29 +0530 Subject: [PATCH 37/49] - remove oracle specifc index options in common layer --- .../src/abstract-dialect/query-interface.d.ts | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/packages/core/src/abstract-dialect/query-interface.d.ts b/packages/core/src/abstract-dialect/query-interface.d.ts index 030a442f3ff0..b4c5eea985ff 100644 --- a/packages/core/src/abstract-dialect/query-interface.d.ts +++ b/packages/core/src/abstract-dialect/query-interface.d.ts @@ -167,50 +167,6 @@ export interface IndexOptions { */ prefix?: string; - /** - * Oracle vector index distance metric. - */ - distance?: string; - - /** - * Oracle vector index target accuracy. - */ - accuracy?: number; - - /** - * Implementation-specific options for Oracle VECTOR indexes. - * - * Acronyms: - * - HNSW: Hierarchical Navigable Small World - * - IVF: Inverted File Flat - */ - parameter?: { - /** - * Neighbor list size (HNSW). - */ - neighbor?: number; - - /** - * Graph construction effort (HNSW). - */ - efconstruction?: number; - - /** - * Number of partitions (IVF). - */ - partitions?: number; - - /** - * Sample count per partition (IVF). - */ - samplesPerPartition?: number; - - /** - * Minimum vectors per partition (IVF). - */ - minVectors?: number; - }; - /** * Non-key columns to be added to the lead level of the nonclustered index. */ From 3831c6723afdac75b8c5815889a0f17e54a88605 Mon Sep 17 00:00:00 2001 From: sudarshan12s Date: Thu, 21 May 2026 14:57:44 +0530 Subject: [PATCH 38/49] - add more tests --- .../test/unit/dialects/oracle/vector.test.js | 13 ++++++++++ packages/oracle/src/query-generator.js | 24 +++++++++---------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index b91fbece3184..f60eb1ba4bb0 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -81,6 +81,19 @@ if (current.dialect.name === 'oracle') { ); }); + it('sanitizes dotted and quoted prefixes when generating vector index names', () => { + expectsql( + queryGenerator.addIndexQuery('ignored', ['vec1'], { + type: 'VECTOR', + prefix: '"schema"."foo"', + }), + { + default: + 'CREATE VECTOR INDEX "schema_foo_vec1" ON "ignored" ("vec1") ORGANIZATION INMEMORY NEIGHBOR GRAPH', + }, + ); + }); + it('accepts lowercase vector type', () => { expectsql(queryGenerator.addIndexQuery('Foo', ['vec1'], { type: 'vector' }), { default: diff --git a/packages/oracle/src/query-generator.js b/packages/oracle/src/query-generator.js index 9ed97781957d..debaa984f994 100644 --- a/packages/oracle/src/query-generator.js +++ b/packages/oracle/src/query-generator.js @@ -1420,24 +1420,24 @@ function normalizeVectorIndexDistance(distance) { } function validateVectorIndexPositiveInteger(value, optionName) { - return validateVectorIndexPositiveNumber(value, optionName, { integer: true }); + if (value == null) { + return undefined; + } + + if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) { + throw new TypeError(`Oracle VECTOR index ${optionName} must be a positive integer.`); + } + + return value; } -function validateVectorIndexPositiveNumber(value, optionName, { integer = false, max } = {}) { +function validateVectorIndexPositiveNumber(value, optionName) { if (value == null) { return undefined; } - if ( - typeof value !== 'number' || - !Number.isFinite(value) || - value <= 0 || - (integer && !Number.isSafeInteger(value)) || - (max != null && value > max) - ) { - const type = integer ? 'positive integer' : 'positive number'; - const suffix = max == null ? '' : ` less than or equal to ${max}`; - throw new TypeError(`Oracle VECTOR index ${optionName} must be a ${type}${suffix}.`); + if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) { + throw new TypeError(`Oracle VECTOR index ${optionName} must be a positive number.`); } return value; From 52894fe8928b220b977975d09ba9e8c537ef922c Mon Sep 17 00:00:00 2001 From: prajalg Date: Thu, 21 May 2026 15:21:57 +0530 Subject: [PATCH 39/49] lint fix --- packages/core/test/unit/dialects/oracle/vector.test.js | 5 +---- packages/oracle/src/query-generator.js | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/core/test/unit/dialects/oracle/vector.test.js b/packages/core/test/unit/dialects/oracle/vector.test.js index f60eb1ba4bb0..f2a87b0aa53c 100644 --- a/packages/core/test/unit/dialects/oracle/vector.test.js +++ b/packages/core/test/unit/dialects/oracle/vector.test.js @@ -280,10 +280,7 @@ if (current.dialect.name === 'oracle') { type: 'VECTOR', accuracy: '95) PARAMETERS (type hnsw', }), - ).to.throw( - TypeError, - 'Oracle VECTOR index accuracy must be a positive number.', - ); + ).to.throw(TypeError, 'Oracle VECTOR index accuracy must be a positive number.'); }); it('rejects unsafe vector index parameter fragments', () => { diff --git a/packages/oracle/src/query-generator.js b/packages/oracle/src/query-generator.js index debaa984f994..ef5cb87130b2 100644 --- a/packages/oracle/src/query-generator.js +++ b/packages/oracle/src/query-generator.js @@ -72,8 +72,7 @@ const VECTOR_INDEX_DISTANCE_METRICS = new Set([ 'HAMMING', 'MANHATTAN', ]); -const VECTOR_INDEX_FIELD_ORDER_ERROR = - 'Oracle VECTOR indexes do not support ordered fields.'; +const VECTOR_INDEX_FIELD_ORDER_ERROR = 'Oracle VECTOR indexes do not support ordered fields.'; /** * list of reserved words in Oracle DB 21c From ef8751e683d0210837168324bb2ac83a9c3c14fb Mon Sep 17 00:00:00 2001 From: prajalg Date: Thu, 21 May 2026 18:46:32 +0530 Subject: [PATCH 40/49] added db version check for oracle INDEX_SUBTYPE column --- .../show-indexes-query.test.ts | 18 ++++++++++++++ .../query-generator-typescript.internal.ts | 24 ++++++++++++++++++- packages/oracle/src/query-interface.js | 10 ++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/packages/core/test/unit/query-generator/show-indexes-query.test.ts b/packages/core/test/unit/query-generator/show-indexes-query.test.ts index be16bda708eb..332a6451e64d 100644 --- a/packages/core/test/unit/query-generator/show-indexes-query.test.ts +++ b/packages/core/test/unit/query-generator/show-indexes-query.test.ts @@ -37,6 +37,24 @@ describe('QueryGenerator#showIndexesQuery', () => { }); }); + it('uses a compatibility projection for Oracle versions without INDEX_SUBTYPE', () => { + if (dialect.name !== 'oracle') { + return; + } + + const oracle19Sequelize = createSequelizeInstance(); + oracle19Sequelize.setDatabaseVersion('19.0.0'); + const oracle19QueryGenerator = oracle19Sequelize.queryGenerator; + + expectsql(() => oracle19QueryGenerator.showIndexesQuery('myTable'), { + oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, NULL AS index_subtype, u.ityp_name, i.descend, c.constraint_type + FROM all_ind_columns i + INNER JOIN all_indexes u ON (u.table_name = i.table_name AND u.index_name = i.index_name) + LEFT OUTER JOIN all_constraints c ON (c.table_name = i.table_name AND c.index_name = i.index_name) + WHERE i.table_name = 'myTable' AND u.table_owner = '${dialect.getDefaultSchema()}' ORDER BY index_name, column_position`, + }); + }); + it('produces a SHOW INDEX query from a model', () => { const MyModel = sequelize.define('MyModel', {}); diff --git a/packages/oracle/src/query-generator-typescript.internal.ts b/packages/oracle/src/query-generator-typescript.internal.ts index a6a62b0abb50..c2b7dfe2333c 100644 --- a/packages/oracle/src/query-generator-typescript.internal.ts +++ b/packages/oracle/src/query-generator-typescript.internal.ts @@ -108,8 +108,13 @@ export class OracleQueryGeneratorTypeScript extends AbstractQueryGenerator { showIndexesQuery(table: TableNameWithSchema) { const [tableName, owner] = this.getSchemaNameAndTableName(table); + const indexSubtypeSelection = this.#supportsIndexSubtypeColumn() + ? 'u.index_subtype' + : 'NULL AS index_subtype'; const sql = [ - 'SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.index_subtype, u.ityp_name, i.descend, c.constraint_type ', + // INDEX_SUBTYPE is available on newer Oracle versions (e.g. 23c+). + // Older versions (e.g. 19c) do not expose this column in ALL_INDEXES. + `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, ${indexSubtypeSelection}, u.ityp_name, i.descend, c.constraint_type `, 'FROM all_ind_columns i ', 'INNER JOIN all_indexes u ', 'ON (u.table_name = i.table_name AND u.index_name = i.index_name) ', @@ -124,6 +129,23 @@ export class OracleQueryGeneratorTypeScript extends AbstractQueryGenerator { return sql.join(''); } + /** + * Checks if INDEX_SUBTYPE column is supported in the current Oracle Database version. + */ + #supportsIndexSubtypeColumn(): boolean { + const version = this.sequelize.getDatabaseVersionIfExist(); + if (version == null) { + return true; + } + + const major = Number.parseInt(version.split('.', 1)[0], 10); + if (!Number.isFinite(major)) { + return true; + } + + return major >= 23; + } + /** * Returns the tableName and schemaName as it is stored the Oracle DB * diff --git a/packages/oracle/src/query-interface.js b/packages/oracle/src/query-interface.js index ff28df10984b..5f8946688df3 100644 --- a/packages/oracle/src/query-interface.js +++ b/packages/oracle/src/query-interface.js @@ -6,6 +6,16 @@ import intersection from 'lodash/intersection.js'; import uniq from 'lodash/uniq.js'; export class OracleQueryInterface extends AbstractQueryInterface { + async showIndex(tableName, options) { + if (this.sequelize.getDatabaseVersionIfExist() == null) { + // Ensure version is initialized before SQL generation so showIndexesQuery + // can check INDEX_SUBTYPE usage by Oracle version. + await this.sequelize.authenticate(); + } + + return super.showIndex(tableName, options); + } + async upsert(tableName, insertValues, updateValues, where, options) { if (options.bind) { assertNoReservedBind(options.bind); From 9efae86db33572b72d9cf78f2cd90056df748e81 Mon Sep 17 00:00:00 2001 From: prajalg Date: Thu, 21 May 2026 19:44:16 +0530 Subject: [PATCH 41/49] removed early actual connection acquisition in showIndex() --- packages/oracle/src/query-interface.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/oracle/src/query-interface.js b/packages/oracle/src/query-interface.js index 5f8946688df3..ff28df10984b 100644 --- a/packages/oracle/src/query-interface.js +++ b/packages/oracle/src/query-interface.js @@ -6,16 +6,6 @@ import intersection from 'lodash/intersection.js'; import uniq from 'lodash/uniq.js'; export class OracleQueryInterface extends AbstractQueryInterface { - async showIndex(tableName, options) { - if (this.sequelize.getDatabaseVersionIfExist() == null) { - // Ensure version is initialized before SQL generation so showIndexesQuery - // can check INDEX_SUBTYPE usage by Oracle version. - await this.sequelize.authenticate(); - } - - return super.showIndex(tableName, options); - } - async upsert(tableName, insertValues, updateValues, where, options) { if (options.bind) { assertNoReservedBind(options.bind); From ba2f30008c5d8f4213a7f5b8853fd242de5e8690 Mon Sep 17 00:00:00 2001 From: prajalg Date: Fri, 22 May 2026 00:58:27 +0530 Subject: [PATCH 42/49] removed NULL literal and generated sql dynamically --- packages/oracle/src/query-generator-typescript.internal.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/oracle/src/query-generator-typescript.internal.ts b/packages/oracle/src/query-generator-typescript.internal.ts index c2b7dfe2333c..f50196142363 100644 --- a/packages/oracle/src/query-generator-typescript.internal.ts +++ b/packages/oracle/src/query-generator-typescript.internal.ts @@ -108,13 +108,11 @@ export class OracleQueryGeneratorTypeScript extends AbstractQueryGenerator { showIndexesQuery(table: TableNameWithSchema) { const [tableName, owner] = this.getSchemaNameAndTableName(table); - const indexSubtypeSelection = this.#supportsIndexSubtypeColumn() - ? 'u.index_subtype' - : 'NULL AS index_subtype'; + const indexSubtypeColumn = this.#supportsIndexSubtypeColumn() ? ', u.index_subtype' : ''; const sql = [ // INDEX_SUBTYPE is available on newer Oracle versions (e.g. 23c+). // Older versions (e.g. 19c) do not expose this column in ALL_INDEXES. - `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, ${indexSubtypeSelection}, u.ityp_name, i.descend, c.constraint_type `, + `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type${indexSubtypeColumn}, u.ityp_name, i.descend, c.constraint_type `, 'FROM all_ind_columns i ', 'INNER JOIN all_indexes u ', 'ON (u.table_name = i.table_name AND u.index_name = i.index_name) ', From 46139c88e695e81a07157fdf33abb7505b6cc5f8 Mon Sep 17 00:00:00 2001 From: prajalg Date: Fri, 22 May 2026 01:07:50 +0530 Subject: [PATCH 43/49] corrected INDEX_SUBTYPE compatibility unit test --- .../core/test/unit/query-generator/show-indexes-query.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/unit/query-generator/show-indexes-query.test.ts b/packages/core/test/unit/query-generator/show-indexes-query.test.ts index 332a6451e64d..fec3b7bbd429 100644 --- a/packages/core/test/unit/query-generator/show-indexes-query.test.ts +++ b/packages/core/test/unit/query-generator/show-indexes-query.test.ts @@ -47,7 +47,7 @@ describe('QueryGenerator#showIndexesQuery', () => { const oracle19QueryGenerator = oracle19Sequelize.queryGenerator; expectsql(() => oracle19QueryGenerator.showIndexesQuery('myTable'), { - oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, NULL AS index_subtype, u.ityp_name, i.descend, c.constraint_type + oracle: `SELECT i.index_name,i.table_name, i.column_name, u.uniqueness, u.index_type, u.ityp_name, i.descend, c.constraint_type FROM all_ind_columns i INNER JOIN all_indexes u ON (u.table_name = i.table_name AND u.index_name = i.index_name) LEFT OUTER JOIN all_constraints c ON (c.table_name = i.table_name AND c.index_name = i.index_name) From 00e77e1a25aa89c99bf00a8980ebb192e48b9b85 Mon Sep 17 00:00:00 2001 From: prajalg Date: Fri, 22 May 2026 16:00:36 +0530 Subject: [PATCH 44/49] removed pg and snowflake changes --- .../dialects/snowflake/vector.test.js | 155 ------------- .../unit/dialects/postgres/vector.test.js | 216 ------------------ .../unit/dialects/snowflake/vector.test.js | 177 -------------- .../postgres/src/_internal/data-types-db.ts | 20 -- .../src/_internal/data-types-overrides.ts | 126 ---------- packages/postgres/src/dialect.ts | 1 - .../postgres/src/query-generator.internal.ts | 148 ------------ .../src/_internal/data-types-overrides.ts | 70 ------ packages/snowflake/src/dialect.ts | 1 - .../snowflake/src/query-generator.internal.ts | 181 --------------- 10 files changed, 1095 deletions(-) delete mode 100644 packages/core/test/integration/dialects/snowflake/vector.test.js delete mode 100644 packages/core/test/unit/dialects/postgres/vector.test.js delete mode 100644 packages/core/test/unit/dialects/snowflake/vector.test.js diff --git a/packages/core/test/integration/dialects/snowflake/vector.test.js b/packages/core/test/integration/dialects/snowflake/vector.test.js deleted file mode 100644 index c2919c4eeb4a..000000000000 --- a/packages/core/test/integration/dialects/snowflake/vector.test.js +++ /dev/null @@ -1,155 +0,0 @@ -'use strict'; - -const { DataTypes, Op, sql } = require('@sequelize/core'); -const { expect } = require('chai'); -const { getTestDialect, sequelize } = require('../../../support'); - -if (getTestDialect() === 'snowflake') { - describe('[Snowflake Specific] vectors', () => { - describe('FLOAT vectors', () => { - beforeEach(async function () { - this.Item = sequelize.define( - 'SnowflakeVectorItem', - { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - }, - embeddings: DataTypes.VECTOR(3), - }, - { - tableName: 'SnowflakeVectorItems', - freezeTableName: true, - timestamps: false, - }, - ); - - await this.Item.sync({ force: true }); - await sequelize.query(` - INSERT INTO "SnowflakeVectorItems" ("id", "embeddings") - VALUES - (1, [1,1,1]::VECTOR(FLOAT, 3)), - (2, [5,5,5]::VECTOR(FLOAT, 3)), - (3, [10,10,10]::VECTOR(FLOAT, 3)), - (4, [1,2,3]::VECTOR(FLOAT, 3)) - `); - }); - - it('fetches rows from a vector column', async function () { - const result = await this.Item.findAll({ - order: [['id', 'ASC']], - }); - - expect(result).to.have.length(4); - }); - - it('supports l2 distance filtering through helper methods', async function () { - const result = await this.Item.findAll({ - attributes: ['id'], - where: sql.where(sequelize.l2Distance('embeddings', [1, 2, 3]), Op.lt, 3), - order: [['id', 'ASC']], - }); - - expect(result.map(row => row.get('id'))).to.deep.equal([1, 4]); - }); - - it('supports inner product filtering through helper methods', async function () { - const result = await this.Item.findAll({ - attributes: ['id'], - where: sql.where(sequelize.innerProduct('embeddings', [1, 2, 3]), Op.gt, 20), - order: [['id', 'ASC']], - }); - - expect(result.map(row => row.get('id'))).to.deep.equal([2, 3]); - }); - - it('supports l1 distance filtering through helper methods', async function () { - const result = await this.Item.findAll({ - attributes: ['id'], - where: sql.where(sequelize.l1Distance('embeddings', [1, 2, 3]), Op.lt, 10), - order: [['id', 'ASC']], - }); - - expect(result.map(row => row.get('id'))).to.deep.equal([1, 2, 4]); - }); - - it('supports native cosine similarity filtering', async function () { - const result = await this.Item.findAll({ - attributes: ['id'], - where: sql.where( - sql.fn('VECTOR_COSINE_SIMILARITY', sql.attribute('embeddings'), [1, 2, 3]), - Op.gt, - 0.99, - ), - order: [['id', 'ASC']], - }); - - expect(result.map(row => row.get('id'))).to.deep.equal([4]); - }); - - it('supports typed arrays in vector functions', async function () { - const result = await this.Item.findAll({ - attributes: ['id'], - where: sql.where( - sequelize.l2Distance('embeddings', new Float32Array([1, 2, 3])), - Op.lt, - 3, - ), - order: [['id', 'ASC']], - }); - - expect(result.map(row => row.get('id'))).to.deep.equal([1, 4]); - }); - - it('supports helper methods in ORDER BY', async function () { - const result = await this.Item.findAll({ - attributes: ['id'], - order: [sequelize.l2Distance('embeddings', [1, 2, 3])], - limit: 1, - }); - - expect(result).to.have.length(1); - expect(result[0].get('id')).to.equal(4); - }); - }); - - describe('INT vectors', () => { - beforeEach(async function () { - this.Item = sequelize.define( - 'SnowflakeIntVectorItem', - { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - }, - embeddings: DataTypes.VECTOR(3, 'int'), - }, - { - tableName: 'SnowflakeIntVectorItems', - freezeTableName: true, - timestamps: false, - }, - ); - - await this.Item.sync({ force: true }); - await sequelize.query(` - INSERT INTO "SnowflakeIntVectorItems" ("id", "embeddings") - VALUES - (1, [1,2,3]::VECTOR(INT, 3)), - (2, [4,5,6]::VECTOR(INT, 3)), - (3, [9,9,9]::VECTOR(INT, 3)) - `); - }); - - it('supports integer typed arrays in vector functions', async function () { - const result = await this.Item.findAll({ - attributes: ['id'], - where: sql.where(sequelize.l2Distance('embeddings', new Int32Array([1, 2, 3])), Op.lt, 5), - order: [['id', 'ASC']], - }); - - expect(result.map(row => row.get('id'))).to.deep.equal([1, 2]); - }); - }); - }); -} diff --git a/packages/core/test/unit/dialects/postgres/vector.test.js b/packages/core/test/unit/dialects/postgres/vector.test.js deleted file mode 100644 index 5d03df17c70d..000000000000 --- a/packages/core/test/unit/dialects/postgres/vector.test.js +++ /dev/null @@ -1,216 +0,0 @@ -'use strict'; - -const Support = require('../../../support'); -const { DataTypes, Op, sql } = require('@sequelize/core'); -const { expect } = require('chai'); - -const expectsql = Support.expectsql; -const current = Support.sequelize; -const queryGenerator = current.dialect.queryGenerator; - -if (current.dialect.name === 'postgres') { - describe('[Postgres Specific] VECTOR', () => { - describe('VECTOR datatype', () => { - it('renders pgvector SQL without format', () => { - const type = DataTypes.VECTOR(3, 'float32').toDialectDataType(current.dialect); - - expect(type.toSql()).to.equal('VECTOR(3)'); - }); - - it('escapes array values as pgvector literals', () => { - expectsql(queryGenerator.escape([1, 2, 3], { type: DataTypes.VECTOR(3) }), { - postgres: `'[1,2,3]'::vector`, - }); - }); - - it('escapes typed array values as pgvector literals', () => { - expectsql( - queryGenerator.escape(new Float32Array([1, 2, 3]), { type: DataTypes.VECTOR(3) }), - { - postgres: `'[1,2,3]'::vector`, - }, - ); - }); - - it('validates fixed dimensions', () => { - const type = DataTypes.VECTOR(3).toDialectDataType(current.dialect); - - expect(() => type.validate([1, 2, 3])).not.to.throw(); - expect(() => type.validate([1, 2])).to.throw( - Error, - 'VECTOR expects values of length 3, but received 2', - ); - }); - - it('does not validate elements when validating dimensions', () => { - const type = DataTypes.VECTOR(3).toDialectDataType(current.dialect); - - expect(() => type.validate([1, Infinity, 3])).not.to.throw(); - }); - - it('rejects non-finite values when serializing values', () => { - expect(() => - queryGenerator.escape([1, Infinity, 3], { type: DataTypes.VECTOR(3) }), - ).to.throw(Error, 'is not a valid vector'); - }); - - it('rejects dimensions above pgvector max size', () => { - const type = DataTypes.VECTOR(16_001).toDialectDataType(current.dialect); - - expect(() => type.toSql()).to.throw( - TypeError, - 'Invalid VECTOR dimension: 16001 (max 16000)', - ); - }); - - it('parses pgvector values from the database', () => { - const type = DataTypes.VECTOR(3).toDialectDataType(current.dialect); - - expect(type.parseDatabaseValue('[1,2,3]')).to.deep.equal([1, 2, 3]); - }); - }); - - describe('VECTOR functions', () => { - it('renders l2 distance using pgvector operator', () => { - const where = sql.where(current.l2Distance('embedding', [1, 2, 3]), { - [Op.lt]: 1.5, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - postgres: `("embedding" <-> '[1,2,3]'::vector) < 1.5`, - }); - }); - - it('renders cosine distance using pgvector operator', () => { - const where = sql.where(current.cosineDistance('embedding', [1, 2, 3]), { - [Op.lt]: 0.5, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - postgres: `("embedding" <=> '[1,2,3]'::vector) < 0.5`, - }); - }); - - it('renders inner product using pgvector operator', () => { - const where = sql.where(current.innerProduct('embedding', [1, 2, 3]), { - [Op.gt]: 0, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - postgres: `(("embedding" <#> '[1,2,3]'::vector) * -1) > 0`, - }); - }); - - it('renders l1 distance using pgvector operator', () => { - const where = sql.where(current.l1Distance('embedding', [1, 2, 3]), { - [Op.lt]: 10, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - postgres: `("embedding" <+> '[1,2,3]'::vector) < 10`, - }); - }); - - it('maps vectorDistance to l2 distance operator', () => { - const where = sql.where(current.vectorDistance('embedding', [1, 2, 3]), { - [Op.lt]: 1.5, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - postgres: `("embedding" <-> '[1,2,3]'::vector) < 1.5`, - }); - }); - - it('supports typed arrays', () => { - const where = sql.where(current.innerProduct('embedding', new Float32Array([1, 2, 3])), { - [Op.gt]: 0, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - postgres: `(("embedding" <#> '[1,2,3]'::vector) * -1) > 0`, - }); - }); - - it('accepts pgvector literal strings', () => { - const where = sql.where(current.l2Distance('embedding', `'[1,2,3]'::vector`), { - [Op.lt]: 2, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - postgres: `("embedding" <-> '[1,2,3]'::vector) < 2`, - }); - }); - - it('rejects unsafe pgvector literal strings', () => { - for (const payload of [ - "'[1,2,3]'); DROP TABLE users; --'::vector", - "'[1,2,3]'::vector; DROP TABLE users; --", - '[1,2,3]; DROP TABLE users; --', - ]) { - expect(() => - queryGenerator.formatSqlExpression(current.l2Distance('embedding', payload)), - ).to.throw( - Error, - 'L2_DISTANCE expects the second argument to be a number array, typed array, or pgvector literal string', - ); - } - }); - - it('accepts JSON array strings by normalizing to pgvector literals', () => { - const where = sql.where(current.l2Distance('embedding', '[1,2,3]'), { - [Op.lt]: 2, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - postgres: `("embedding" <-> '[1,2,3]'::vector) < 2`, - }); - }); - - it('throws for malformed vector literal strings', () => { - expect(() => - queryGenerator.formatSqlExpression(current.l2Distance('embedding', 'not-a-vector')), - ).to.throw( - Error, - 'L2_DISTANCE expects the second argument to be a number array, typed array, or pgvector literal string', - ); - }); - - it('throws when vector function has too few arguments', () => { - expect(() => - queryGenerator.formatSqlExpression(sql.fn('L2_DISTANCE', sql.attribute('embedding'))), - ).to.throw(Error, 'L2_DISTANCE expects exactly 2 arguments'); - }); - - it('throws when vector function has too many arguments', () => { - expect(() => - queryGenerator.formatSqlExpression( - sql.fn('L2_DISTANCE', sql.attribute('embedding'), [1, 2, 3], [4, 5, 6]), - ), - ).to.throw(Error, 'L2_DISTANCE expects exactly 2 arguments'); - }); - - it('throws for empty vector arrays', () => { - expect(() => - queryGenerator.formatSqlExpression(current.l2Distance('embedding', [])), - ).to.throw(Error, 'Vector arguments must contain at least one element'); - }); - - it('throws for invalid vector array elements', () => { - expect(() => - queryGenerator.formatSqlExpression(current.l2Distance('embedding', [1, '2', 3])), - ).to.throw(Error, 'is not a valid vector element'); - }); - - it('rejects DataView inputs as vector arguments', () => { - expect(() => - queryGenerator.formatSqlExpression( - current.l2Distance('embedding', new DataView(new ArrayBuffer(8))), - ), - ).to.throw( - Error, - 'L2_DISTANCE expects the second argument to be a number array, typed array, or pgvector literal string', - ); - }); - }); - }); -} diff --git a/packages/core/test/unit/dialects/snowflake/vector.test.js b/packages/core/test/unit/dialects/snowflake/vector.test.js deleted file mode 100644 index 08aeea6b2cea..000000000000 --- a/packages/core/test/unit/dialects/snowflake/vector.test.js +++ /dev/null @@ -1,177 +0,0 @@ -'use strict'; - -const { expect } = require('chai'); -const Support = require('../../../support'); -const { DataTypes, Op, sql } = require('@sequelize/core'); - -const expectsql = Support.expectsql; -const current = Support.sequelize; -const queryGenerator = current.dialect.queryGenerator; - -if (current.dialect.name === 'snowflake') { - describe('[Snowflake Specific] VECTOR datatype', () => { - it('renders SQL with default FLOAT element type', () => { - const type = DataTypes.VECTOR(3).toDialectDataType(current.dialect); - - expect(type.toSql()).to.equal('VECTOR(FLOAT, 3)'); - }); - - it('normalizes Snowflake vector format aliases', () => { - expect(DataTypes.VECTOR(3, 'int').toDialectDataType(current.dialect).toSql()).to.equal( - 'VECTOR(INT, 3)', - ); - expect(DataTypes.VECTOR(3, 'float32').toDialectDataType(current.dialect).toSql()).to.equal( - 'VECTOR(FLOAT, 3)', - ); - }); - - it('rejects dimensions above Snowflake max size', () => { - const type = DataTypes.VECTOR(4097).toDialectDataType(current.dialect); - - expect(() => type.toSql()).to.throw(TypeError, 'Invalid VECTOR dimension: 4097 (max 4096)'); - }); - }); - - describe('[Snowflake Specific] VECTOR functions', () => { - it('renders L2 distance from array input', () => { - const where = sql.where(current.l2Distance('embedding', [1, 2, 3]), { - [Op.lt]: 5, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - snowflake: 'VECTOR_L2_DISTANCE("embedding", [1,2,3]::VECTOR(FLOAT, 3)) < 5', - }); - }); - - it('renders inner product from array input', () => { - const where = sql.where(current.innerProduct('embedding', [1, 2, 3]), { - [Op.gt]: 0, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - snowflake: 'VECTOR_INNER_PRODUCT("embedding", [1,2,3]::VECTOR(FLOAT, 3)) > 0', - }); - }); - - it('renders documented cosine similarity function', () => { - const where = sql.where( - sql.fn('VECTOR_COSINE_SIMILARITY', sql.attribute('embedding'), [1, 2, 3]), - Op.gt, - 0.5, - ); - - expectsql(queryGenerator.whereItemsQuery(where), { - snowflake: 'VECTOR_COSINE_SIMILARITY("embedding", [1,2,3]::VECTOR(FLOAT, 3)) > 0.5', - }); - }); - - it('supports integer typed arrays', () => { - const where = sql.where(current.l2Distance('embedding', new Int8Array([1, 2, 3])), { - [Op.lt]: 10, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - snowflake: 'VECTOR_L2_DISTANCE("embedding", [1,2,3]::VECTOR(INT, 3)) < 10', - }); - }); - - it('accepts JSON array strings by normalizing to VECTOR literals', () => { - const where = sql.where(current.l2Distance('embedding', '[1,2,3]'), { - [Op.lt]: 10, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - snowflake: 'VECTOR_L2_DISTANCE("embedding", [1,2,3]::VECTOR(FLOAT, 3)) < 10', - }); - }); - - it('accepts explicit VECTOR-compatible SQL literals', () => { - const where = sql.where(current.l2Distance('embedding', '[1,2,3]::VECTOR(FLOAT, 3)'), { - [Op.lt]: 10, - }); - - expectsql(queryGenerator.whereItemsQuery(where), { - snowflake: 'VECTOR_L2_DISTANCE("embedding", [1,2,3]::VECTOR(FLOAT, 3)) < 10', - }); - }); - - it('rejects unsafe VECTOR literal strings', () => { - for (const payload of [ - '[1,2,3]); DROP TABLE users; --', - '[1,2,3]::VECTOR(FLOAT, 3); DROP TABLE users; --', - 'VECTOR_L2_DISTANCE("embedding", [1,2,3]::VECTOR(FLOAT, 3))', - ]) { - expect(() => - queryGenerator.formatSqlExpression(current.l2Distance('embedding', payload)), - ).to.throw( - Error, - 'L2_DISTANCE expects the second argument to be a number array, typed array, or VECTOR-compatible SQL literal', - ); - } - }); - - it('throws when vector function has too few arguments', () => { - expect(() => - queryGenerator.formatSqlExpression(sql.fn('L2_DISTANCE', sql.attribute('embedding'))), - ).to.throw(Error, 'L2_DISTANCE expects exactly 2 arguments'); - }); - - it('throws when vector function has too many arguments', () => { - expect(() => - queryGenerator.formatSqlExpression( - sql.fn('L2_DISTANCE', sql.attribute('embedding'), [1, 2, 3], [4, 5, 6]), - ), - ).to.throw(Error, 'L2_DISTANCE expects exactly 2 arguments'); - }); - - it('throws for empty vector arrays', () => { - expect(() => - queryGenerator.formatSqlExpression(current.l2Distance('embedding', [])), - ).to.throw(Error, 'Vector arguments must contain at least one element'); - }); - - it('throws for invalid vector array elements', () => { - expect(() => - queryGenerator.formatSqlExpression(current.l2Distance('embedding', [1, '2', 3])), - ).to.throw(Error, 'is not a valid vector element'); - }); - - it('throws for non-finite vector array elements', () => { - expect(() => - queryGenerator.formatSqlExpression(current.l2Distance('embedding', [1, Infinity, 3])), - ).to.throw(Error, 'is not a valid vector element'); - }); - - it('throws when second argument is not a vector input', () => { - expect(() => - queryGenerator.formatSqlExpression(current.l2Distance('embedding', 'not-a-vector')), - ).to.throw( - Error, - 'L2_DISTANCE expects the second argument to be a number array, typed array, or VECTOR-compatible SQL literal', - ); - }); - - it('rejects DataView inputs as vector arguments', () => { - expect(() => - queryGenerator.formatSqlExpression( - current.l2Distance('embedding', new DataView(new ArrayBuffer(8))), - ), - ).to.throw( - Error, - 'L2_DISTANCE expects the second argument to be a number array, typed array, or VECTOR-compatible SQL literal', - ); - }); - - it('rejects cosineDistance because Snowflake documents cosine similarity instead', () => { - expect(() => - queryGenerator.formatSqlExpression(current.cosineDistance('embedding', [1, 2, 3])), - ).to.throw(Error, 'COSINE_DISTANCE is not implemented for the Snowflake vector sample'); - }); - - it('rejects vectorDistance because Snowflake does not document a generic vector distance function', () => { - expect(() => - queryGenerator.formatSqlExpression(current.vectorDistance('embedding', [1, 2, 3])), - ).to.throw(Error, 'VECTOR_DISTANCE is not implemented for the Snowflake vector sample'); - }); - }); -} diff --git a/packages/postgres/src/_internal/data-types-db.ts b/packages/postgres/src/_internal/data-types-db.ts index 6e2a1798b583..b3073e4e2ea9 100644 --- a/packages/postgres/src/_internal/data-types-db.ts +++ b/packages/postgres/src/_internal/data-types-db.ts @@ -70,12 +70,6 @@ export function registerPostgresDbDataTypeParsers(dialect: PostgresDialect) { return parseHstore(value); }); - dialect.registerDataTypeParser(['vector'], (value: unknown) => { - assert(typeof value === 'string', 'Expected vector value to be a string'); - - return parseVectorLiteral(value); - }); - const parseInteger = getDataTypeParser(dialect, BaseTypes.INTEGER); dialect.registerDataTypeParser(['int4range'], buildRangeParser(parseInteger)); @@ -92,17 +86,3 @@ export function registerPostgresDbDataTypeParsers(dialect: PostgresDialect) { // The Sequelize DataType specified by the user will do further parsing of the arrays of strings (like convert values to Date objects). dialect.registerDataTypeParser(['tstzrange', 'tsrange', 'daterange'], buildRangeParser(identity)); } - -function parseVectorLiteral(value: string): number[] { - const parsed = JSON.parse(value) as unknown; - assert(Array.isArray(parsed), 'Expected vector value to parse as an array'); - - for (const item of parsed) { - assert( - typeof item === 'number' && Number.isFinite(item), - 'Expected vector elements to be finite numbers', - ); - } - - return parsed; -} diff --git a/packages/postgres/src/_internal/data-types-overrides.ts b/packages/postgres/src/_internal/data-types-overrides.ts index d647ca080849..da82c5b5cb97 100644 --- a/packages/postgres/src/_internal/data-types-overrides.ts +++ b/packages/postgres/src/_internal/data-types-overrides.ts @@ -1,5 +1,4 @@ import type { AbstractDialect, Rangable } from '@sequelize/core'; -import { ValidationErrorItem } from '@sequelize/core'; import { attributeTypeToSql } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/data-types-utils.js'; import type { AbstractDataType, @@ -11,16 +10,11 @@ import * as BaseTypes from '@sequelize/core/_non-semver-use-at-your-own-risk_/ab import { inspect, isBigInt, isNumber, isString } from '@sequelize/utils'; import identity from 'lodash/identity.js'; import assert from 'node:assert'; -import util from 'node:util'; import wkx from 'wkx'; import { PostgresQueryGenerator } from '../query-generator'; import { stringifyHstore } from './hstore.js'; import { buildRangeParser, stringifyRange } from './range.js'; -export interface PgVectorOptions { - dimension: number; -} - function removeUnsupportedIntegerOptions( dataType: BaseTypes.BaseIntegerDataType, dialect: AbstractDialect, @@ -297,106 +291,6 @@ export class GEOGRAPHY extends BaseTypes.GEOGRAPHY { } } -export class VECTOR extends BaseTypes.VECTOR { - protected _getSqlOptionParts(): string[] { - const options = this.#getPgOptions(); - - return options ? [String(options.dimension)] : []; - } - - protected _checkOptionSupport(dialect: AbstractDialect): void { - super._checkOptionSupport(dialect); - - if (this.options.format) { - dialect.warnDataTypeIssue( - `${dialect.name} VECTOR ignores the "format" option; pgvector uses vector(n).`, - ); - delete this.options.format; - } - } - - validate(value: unknown): asserts value is BaseTypes.VectorValue { - super.validate(value); - const options = this.#getPgOptions(); - - if (options == null) { - return; - } - - const iterable = this._getVectorIterable(value); - if (iterable == null) { - return; - } - - if (iterable.length !== options.dimension) { - ValidationErrorItem.throwDataTypeValidationError( - util.format( - 'VECTOR expects values of length %d, but received %d', - options.dimension, - iterable.length, - ), - ); - } - } - - protected _validateVectorElement(item: unknown): number { - const numeric = super._validateVectorElement(item); - - if (!Number.isFinite(numeric)) { - ValidationErrorItem.throwDataTypeValidationError( - util.format('VECTOR expects finite numeric elements, but received %O', numeric), - ); - } - - return numeric; - } - - toBindableValue(value: BaseTypes.VectorValue): string { - const iterable = this._getVectorIterable(value); - if (iterable == null) { - throw new TypeError('Unsupported vector container type'); - } - - this.validate(value); - - const values = [...iterable].map(item => this._validateVectorElement(item)); - - return `[${values.join(',')}]`; - } - - escape(value: BaseTypes.VectorValue): string { - return `${this._getDialect().escapeString(this.toBindableValue(value))}::vector`; - } - - getBindParamSql(value: BaseTypes.VectorValue, options: BindParamOptions): string { - return `${options.bindParam(this.toBindableValue(value))}::vector`; - } - - parseDatabaseValue(value: unknown): unknown { - if (Array.isArray(value)) { - return value; - } - - if (typeof value === 'string') { - return parseVectorLiteral(value); - } - - throw new Error( - `DataTypes.VECTOR received a non-vector value from the database: ${inspect(value)}`, - ); - } - - #getPgOptions(): PgVectorOptions | null { - if (this.options.dimension == null) { - return null; - } - - return { - dimension: this._validateDimension(this.options.dimension, 16_000), - }; - } -} - export class HSTORE extends BaseTypes.HSTORE { toBindableValue(value: AcceptableTypeOf): string { if (value == null) { @@ -543,23 +437,3 @@ export class ENUM extends BaseTypes.ENUM { return queryGenerator.pgEnumName(tableName, columnName); } } - -function parseVectorLiteral(value: string): number[] { - const trimmed = value.trim(); - if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) { - throw new Error(`Invalid pgvector literal received from the database: ${value}`); - } - - const parsed = JSON.parse(trimmed) as unknown; - if (!Array.isArray(parsed)) { - throw new Error(`Invalid pgvector literal received from the database: ${value}`); - } - - for (const item of parsed) { - if (typeof item !== 'number' || !Number.isFinite(item)) { - throw new Error(`Invalid pgvector element received from the database: ${inspect(item)}`); - } - } - - return parsed; -} diff --git a/packages/postgres/src/dialect.ts b/packages/postgres/src/dialect.ts index f614f0920d8d..912f9a79f421 100644 --- a/packages/postgres/src/dialect.ts +++ b/packages/postgres/src/dialect.ts @@ -113,7 +113,6 @@ export class PostgresDialect extends AbstractDialect< JSONB: true, HSTORE: true, TSVECTOR: true, - VECTOR: true, CITEXT: true, DATETIME: { infinity: true }, DATEONLY: { infinity: true }, diff --git a/packages/postgres/src/query-generator.internal.ts b/packages/postgres/src/query-generator.internal.ts index 5ec68767ae6e..137156049876 100644 --- a/packages/postgres/src/query-generator.internal.ts +++ b/packages/postgres/src/query-generator.internal.ts @@ -1,8 +1,5 @@ import { AbstractQueryGeneratorInternal } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-internal.js'; -import type { EscapeOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-typescript.js'; import type { AddLimitOffsetOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator.internal-types.js'; -import type { Fn } from '@sequelize/core/_non-semver-use-at-your-own-risk_/expression-builders/fn.js'; -import util from 'node:util'; import type { PostgresDialect } from './dialect.js'; const TECHNICAL_DATABASE_NAMES = Object.freeze(['postgres']); @@ -36,149 +33,4 @@ export class PostgresQueryGeneratorInternal< return fragment; } - - formatFn(piece: Fn, options?: EscapeOptions): string { - const fnName = piece.fn.toUpperCase(); - if (!POSTGRES_VECTOR_FUNCTIONS.has(fnName)) { - return super.formatFn(piece, options); - } - - if (piece.args.length !== 2) { - throw new Error(`${fnName} expects exactly 2 arguments`); - } - - const [columnArg, vectorArg] = piece.args; - const columnSql = - typeof columnArg === 'string' - ? this.queryGenerator.quoteIdentifier(columnArg) - : this.queryGenerator.escape(columnArg, options); - const vectorSql = this.#formatVectorArg(vectorArg, fnName); - - switch (fnName) { - case 'COSINE_DISTANCE': - return `(${columnSql} <=> ${vectorSql})`; - case 'INNER_PRODUCT': - return `((${columnSql} <#> ${vectorSql}) * -1)`; - case 'L1_DISTANCE': - return `(${columnSql} <+> ${vectorSql})`; - case 'L2_DISTANCE': - case 'VECTOR_DISTANCE': - return `(${columnSql} <-> ${vectorSql})`; - default: - return super.formatFn(piece, options); - } - } - - #formatVectorArg(arg: unknown, fnName: string): string { - if (Array.isArray(arg)) { - return this.#formatVectorFromArray(arg); - } - - if (ArrayBuffer.isView(arg) && !(arg instanceof DataView) && isNumericTypedArray(arg)) { - return this.#formatVectorFromArray([...arg]); - } - - if (typeof arg === 'string') { - const trimmed = arg.trim(); - const literal = this.#parseVectorLiteral(trimmed); - if (literal) { - return literal; - } - - if (trimmed.startsWith('[') && trimmed.endsWith(']')) { - try { - const parsed = JSON.parse(trimmed); - if (Array.isArray(parsed)) { - return this.#formatVectorFromArray(parsed); - } - } catch { - // ignore and fall through - } - } - } - - throw new Error( - `${fnName} expects the second argument to be a number array, typed array, or pgvector literal string`, - ); - } - - #parseVectorLiteral(literal: string): string | null { - const match = literal.match(/^'([^']+)'\s*::\s*vector(?:\s*\(\s*(\d+)\s*\))?$/i); - if (!match) { - return null; - } - - const [, body, dimension] = match; - if (!body.startsWith('[') || !body.endsWith(']')) { - return null; - } - - let parsed: unknown; - try { - parsed = JSON.parse(body); - } catch { - return null; - } - - if (!Array.isArray(parsed)) { - return null; - } - - if (dimension != null && parsed.length !== Number(dimension)) { - throw new Error( - `Vector literal dimension ${dimension} does not match ${parsed.length} values`, - ); - } - - return this.#formatVectorFromArray(parsed); - } - - #formatVectorFromArray(values: unknown[]): string { - if (values.length === 0) { - throw new Error('Vector arguments must contain at least one element'); - } - - const numericValues = values.map(value => { - if (typeof value !== 'number' || !Number.isFinite(value)) { - throw new Error(util.format('%O is not a valid vector element', value)); - } - - return value; - }); - - return `'[${numericValues.join(',')}]'::vector`; - } -} - -const POSTGRES_VECTOR_FUNCTIONS = new Set([ - 'COSINE_DISTANCE', - 'INNER_PRODUCT', - 'L1_DISTANCE', - 'L2_DISTANCE', - 'VECTOR_DISTANCE', -]); - -function isNumericTypedArray( - value: ArrayBufferView, -): value is - | Int8Array - | Uint8Array - | Uint8ClampedArray - | Int16Array - | Uint16Array - | Int32Array - | Uint32Array - | Float32Array - | Float64Array { - return ( - value instanceof Int8Array || - value instanceof Uint8Array || - value instanceof Uint8ClampedArray || - value instanceof Int16Array || - value instanceof Uint16Array || - value instanceof Int32Array || - value instanceof Uint32Array || - value instanceof Float32Array || - value instanceof Float64Array - ); } diff --git a/packages/snowflake/src/_internal/data-types-overrides.ts b/packages/snowflake/src/_internal/data-types-overrides.ts index d63c3ccd89f5..19808726cb6f 100644 --- a/packages/snowflake/src/_internal/data-types-overrides.ts +++ b/packages/snowflake/src/_internal/data-types-overrides.ts @@ -1,16 +1,7 @@ import type { AbstractDialect } from '@sequelize/core'; -import { ValidationErrorItem } from '@sequelize/core'; import type { AcceptedDate } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/data-types.js'; import * as BaseTypes from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/data-types.js'; import maxBy from 'lodash/maxBy.js'; -import util from 'node:util'; - -export type SnowflakeVectorFormat = 'INT' | 'FLOAT'; - -export interface SnowflakeVectorOptions { - dimension: number; - format: SnowflakeVectorFormat; -} export class DATE extends BaseTypes.DATE { toSql() { @@ -104,64 +95,3 @@ export class BIGINT extends BaseTypes.BIGINT { return 'INTEGER'; } } - -export class VECTOR extends BaseTypes.VECTOR { - protected _getSqlOptionParts(): string[] { - const options = this.#getSnowflakeOptions(); - - return [options.format, String(options.dimension)]; - } - - validate(value: unknown): asserts value is BaseTypes.VectorValue { - super.validate(value); - const options = this.#getSnowflakeOptions(); - - const length = this.#getVectorLength(value); - if (length !== options.dimension) { - ValidationErrorItem.throwDataTypeValidationError( - util.format( - 'VECTOR expects values of length %d, but received %d', - options.dimension, - length, - ), - ); - } - } - - protected _validateFormat(format: string): SnowflakeVectorFormat { - const normalized = format.trim().toUpperCase(); - - switch (normalized) { - case 'INT': - case 'INT8': - case 'INT16': - case 'INT32': - return 'INT'; - case 'FLOAT': - case 'FLOAT32': - case 'FLOAT64': - return 'FLOAT'; - default: - throw new TypeError(`Invalid Snowflake VECTOR format: ${format}`); - } - } - - #getVectorLength(value: BaseTypes.VectorValue): number { - if (Array.isArray(value)) { - return value.length; - } - - return value.length; - } - - #getSnowflakeOptions(): SnowflakeVectorOptions { - if (this.options.dimension == null) { - throw new TypeError('Snowflake VECTOR requires a positive integer "dimension" option.'); - } - - const dimension = this._validateDimension(this.options.dimension, 4096); - const format = this._validateFormat(this.options.format ?? 'FLOAT'); - - return { dimension, format }; - } -} diff --git a/packages/snowflake/src/dialect.ts b/packages/snowflake/src/dialect.ts index cb75b6b214c0..d5def091d259 100644 --- a/packages/snowflake/src/dialect.ts +++ b/packages/snowflake/src/dialect.ts @@ -133,7 +133,6 @@ export class SnowflakeDialect extends AbstractDialect< multiDatabases: true, dataTypes: { COLLATE_BINARY: true, - VECTOR: true, }, REGEXP: true, globalTimeZoneConfig: true, diff --git a/packages/snowflake/src/query-generator.internal.ts b/packages/snowflake/src/query-generator.internal.ts index 86a897f542bd..6cb037a598fd 100644 --- a/packages/snowflake/src/query-generator.internal.ts +++ b/packages/snowflake/src/query-generator.internal.ts @@ -1,8 +1,5 @@ import { AbstractQueryGeneratorInternal } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-internal.js'; -import type { EscapeOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator-typescript.js'; import type { AddLimitOffsetOptions } from '@sequelize/core/_non-semver-use-at-your-own-risk_/abstract-dialect/query-generator.internal-types.js'; -import type { Fn } from '@sequelize/core/_non-semver-use-at-your-own-risk_/expression-builders/fn.js'; -import util from 'node:util'; import type { SnowflakeDialect } from './dialect.js'; const TECHNICAL_SCHEMA_NAMES = Object.freeze([ @@ -35,182 +32,4 @@ export class SnowflakeQueryGeneratorInternal< return fragment; } - - formatFn(piece: Fn, options?: EscapeOptions): string { - const fnName = piece.fn.toUpperCase(); - const mappedName = - SNOWFLAKE_VECTOR_FUNCTION_MAP.get(fnName) ?? - (SNOWFLAKE_NATIVE_VECTOR_FUNCTIONS.has(fnName) ? fnName : undefined); - - if (!mappedName) { - if (SNOWFLAKE_UNSUPPORTED_VECTOR_FUNCTIONS.has(fnName)) { - throw new Error(`${fnName} is not implemented for the Snowflake vector sample`); - } - - return super.formatFn(piece, options); - } - - if (piece.args.length !== 2) { - throw new Error(`${fnName} expects exactly 2 arguments`); - } - - const [columnArg, vectorArg] = piece.args; - const columnSql = - typeof columnArg === 'string' - ? this.queryGenerator.quoteIdentifier(columnArg) - : this.queryGenerator.escape(columnArg, options); - const vectorSql = this.#formatVectorArg(vectorArg, fnName); - - return `${mappedName}(${columnSql}, ${vectorSql})`; - } - - #formatVectorArg(arg: unknown, fnName: string): string { - if (Array.isArray(arg)) { - return this.#formatVectorFromArray(arg, 'FLOAT'); - } - - if (ArrayBuffer.isView(arg) && !(arg instanceof DataView) && isNumericTypedArray(arg)) { - return this.#formatTypedArrayVector(arg); - } - - if (typeof arg === 'string') { - const trimmed = arg.trim(); - const literal = this.#parseVectorLiteral(trimmed); - if (literal) { - return literal; - } - - if (trimmed.startsWith('[') && trimmed.endsWith(']')) { - try { - const parsed = JSON.parse(trimmed); - if (Array.isArray(parsed)) { - return this.#formatVectorFromArray(parsed, 'FLOAT'); - } - } catch { - // fall through to error below - } - } - } - - throw new Error( - `${fnName} expects the second argument to be a number array, typed array, or VECTOR-compatible SQL literal`, - ); - } - - #parseVectorLiteral(literal: string): string | null { - const match = literal.match( - /^(\[[^\]]+\])\s*::\s*VECTOR\s*\(\s*(FLOAT|INT)\s*,\s*(\d+)\s*\)$/i, - ); - if (!match) { - return null; - } - - const [, body, elementType, dimension] = match; - - let parsed: unknown; - try { - parsed = JSON.parse(body); - } catch { - return null; - } - - if (!Array.isArray(parsed)) { - return null; - } - - if (parsed.length !== Number(dimension)) { - throw new Error( - `Vector literal dimension ${dimension} does not match ${parsed.length} values`, - ); - } - - return this.#formatVectorFromArray(parsed, elementType.toUpperCase() as 'FLOAT' | 'INT'); - } - - // Snowflake documents vector literals as ARRAY values cast with ::VECTOR(type, dimension), - // e.g. [1,2,3]::VECTOR(FLOAT, 3). - #formatVectorFromArray(values: unknown[], elementType: 'FLOAT' | 'INT'): string { - if (values.length === 0) { - throw new Error('Vector arguments must contain at least one element'); - } - - const numericValues = values.map(value => { - if (typeof value !== 'number' || !Number.isFinite(value)) { - throw new Error(util.format('%O is not a valid vector element', value)); - } - - return value; - }); - - return `[${numericValues.join(',')}]::VECTOR(${elementType}, ${numericValues.length})`; - } - - #formatTypedArrayVector(values: NumericTypedArray): string { - const elementType = isIntegerTypedArray(values) ? 'INT' : 'FLOAT'; - - return this.#formatVectorFromArray([...values], elementType); - } -} - -// Sample cross-dialect implementation: keep only helper mappings that correspond directly to -// documented Snowflake vector functions. -const SNOWFLAKE_VECTOR_FUNCTION_MAP = new Map([ - ['INNER_PRODUCT', 'VECTOR_INNER_PRODUCT'], - ['L1_DISTANCE', 'VECTOR_L1_DISTANCE'], - ['L2_DISTANCE', 'VECTOR_L2_DISTANCE'], -]); - -const SNOWFLAKE_NATIVE_VECTOR_FUNCTIONS = new Set([ - 'VECTOR_INNER_PRODUCT', - 'VECTOR_L1_DISTANCE', - 'VECTOR_L2_DISTANCE', - 'VECTOR_COSINE_SIMILARITY', -]); - -const SNOWFLAKE_UNSUPPORTED_VECTOR_FUNCTIONS = new Set(['COSINE_DISTANCE', 'VECTOR_DISTANCE']); - -function isNumericTypedArray(value: ArrayBufferView): value is NumericTypedArray { - return ( - value instanceof Int8Array || - value instanceof Uint8Array || - value instanceof Uint8ClampedArray || - value instanceof Int16Array || - value instanceof Uint16Array || - value instanceof Int32Array || - value instanceof Uint32Array || - value instanceof Float32Array || - value instanceof Float64Array - ); -} - -type NumericTypedArray = - | Int8Array - | Uint8Array - | Uint8ClampedArray - | Int16Array - | Uint16Array - | Int32Array - | Uint32Array - | Float32Array - | Float64Array; - -function isIntegerTypedArray( - value: NumericTypedArray, -): value is - | Int8Array - | Uint8Array - | Uint8ClampedArray - | Int16Array - | Uint16Array - | Int32Array - | Uint32Array { - return ( - value instanceof Int8Array || - value instanceof Uint8Array || - value instanceof Uint8ClampedArray || - value instanceof Int16Array || - value instanceof Uint16Array || - value instanceof Int32Array || - value instanceof Uint32Array - ); } From 484a3d9638a12e4bd049de774f70041d304ef1b3 Mon Sep 17 00:00:00 2001 From: prajalg Date: Fri, 22 May 2026 16:30:03 +0530 Subject: [PATCH 45/49] added some comments --- .../core/src/abstract-dialect/data-types.ts | 14 ++++++++ packages/core/src/sequelize.d.ts | 34 ++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/core/src/abstract-dialect/data-types.ts b/packages/core/src/abstract-dialect/data-types.ts index 4af58def0bd3..781055a9e700 100644 --- a/packages/core/src/abstract-dialect/data-types.ts +++ b/packages/core/src/abstract-dialect/data-types.ts @@ -2856,10 +2856,19 @@ export class TSVECTOR extends AbstractDataType { } export interface VectorOptions { + /** + * Optional vector dimension. + */ dimension?: number; + /** + * Optional dialect-specific element format (for example float32, int8, binary). + */ format?: string; } +/** + * Numeric typed arrays accepted as vector values. + */ export type NumericTypedArray = | Int8Array | Uint8Array @@ -3038,6 +3047,11 @@ export class VECTOR extends AbstractVECTORBase { } } +/** + * Returns true when the input is one of Sequelize's accepted numeric typed arrays. + * + * @param value + */ function isTypedArrayIterable(value: unknown): value is NumericTypedArray { return ( value instanceof Int8Array || diff --git a/packages/core/src/sequelize.d.ts b/packages/core/src/sequelize.d.ts index b992db43ec60..b6cab01f490b 100644 --- a/packages/core/src/sequelize.d.ts +++ b/packages/core/src/sequelize.d.ts @@ -612,7 +612,7 @@ export class Sequelize< random(): Fn; /** - * Build a dialect-specific vector distance function. + * Build a dialect-specific cosine distance function. * Dialects that advertise `DialectSupportMap.dataTypes.VECTOR` translate this generic helper * into their native similarity operator (Oracle, Snowflake samples, pgvector, ...). * @@ -621,12 +621,44 @@ export class Sequelize< */ cosineDistance(column: string, value: VectorValue): Fn; + /** + * Build a dialect-specific inner product function. + * Dialects that advertise `DialectSupportMap.dataTypes.VECTOR` translate this generic helper + * into their native similarity operator (Oracle, Snowflake samples, pgvector, ...). + * + * @param column column name or identifier to compare against + * @param value reference vector as a plain numeric array or typed array + */ innerProduct(column: string, value: VectorValue): Fn; + /** + * Build a dialect-specific L1 distance function. + * Dialects that advertise `DialectSupportMap.dataTypes.VECTOR` translate this generic helper + * into their native similarity operator (Oracle, Snowflake samples, pgvector, ...). + * + * @param column column name or identifier to compare against + * @param value reference vector as a plain numeric array or typed array + */ l1Distance(column: string, value: VectorValue): Fn; + /** + * Build a dialect-specific L2 distance function. + * Dialects that advertise `DialectSupportMap.dataTypes.VECTOR` translate this generic helper + * into their native similarity operator (Oracle, Snowflake samples, pgvector, ...). + * + * @param column column name or identifier to compare against + * @param value reference vector as a plain numeric array or typed array + */ l2Distance(column: string, value: VectorValue): Fn; + /** + * Build a dialect-specific default vector distance function. + * Dialects that advertise `DialectSupportMap.dataTypes.VECTOR` translate this generic helper + * into their native similarity operator (Oracle, Snowflake samples, pgvector, ...). + * + * @param column column name or identifier to compare against + * @param value reference vector as a plain numeric array or typed array + */ vectorDistance(column: string, value: VectorValue): Fn; /** From 550ebf261d9e3eab1a60cea25ae8fa546785f415 Mon Sep 17 00:00:00 2001 From: prajalg Date: Thu, 28 May 2026 13:51:02 +0530 Subject: [PATCH 46/49] address bot comments --- .../test/unit/data-types/string-types.test.ts | 226 +++++++++--------- .../src/_internal/data-types-overrides.ts | 56 ++++- 2 files changed, 167 insertions(+), 115 deletions(-) diff --git a/packages/core/test/unit/data-types/string-types.test.ts b/packages/core/test/unit/data-types/string-types.test.ts index 1eb4721004e8..81e2101f94ac 100644 --- a/packages/core/test/unit/data-types/string-types.test.ts +++ b/packages/core/test/unit/data-types/string-types.test.ts @@ -215,135 +215,133 @@ See https://sequelize.org/docs/v7/models/data-types/ for a list of supported dat }); }); -if (sequelize.dialect.supports?.dataTypes?.VECTOR) { - describe('DataTypes.VECTOR', () => { - describe('constructor', () => { - it('stores empty options when no args are passed', () => { - const type = DataTypes.VECTOR(); - - expect(type.options).to.deep.equal({}); - }); - - it('stores dimension when only dimension is passed', () => { - const type = DataTypes.VECTOR(4); - - expect(type.options).to.deep.equal({ dimension: 4 }); - }); - - it('stores dimension and format when both args are passed', () => { - const type = DataTypes.VECTOR(3, 'float32'); - - expect(type.options).to.deep.equal({ dimension: 3, format: 'float32' }); - }); - - it('supports object-style options', () => { - const type = DataTypes.VECTOR({ dimension: 8, format: 'float64' }); - - expect(type.options).to.deep.equal({ dimension: 8, format: 'float64' }); - }); - - it('supports object-style options with only dimension', () => { - const type = DataTypes.VECTOR({ dimension: 8 }); - - expect(type.options).to.deep.equal({ dimension: 8 }); - }); - - it('normalizes binary format casing in constructor options', () => { - const type = DataTypes.VECTOR(24, 'BINARY'); - - expect(type.options).to.deep.equal({ dimension: 24, format: 'binary' }); - }); - - it('rejects invalid dimensions', () => { - expect(() => DataTypes.VECTOR(0)).to.throw(TypeError, 'Invalid VECTOR dimension'); - }); - - it('rejects invalid object-style dimensions', () => { - expect(() => DataTypes.VECTOR({ dimension: 0 })).to.throw( - TypeError, - 'Invalid VECTOR dimension', - ); - expect(() => DataTypes.VECTOR({ dimension: 1.5 })).to.throw( - TypeError, - 'Invalid VECTOR dimension', - ); - }); - - it('rejects unknown formats', () => { - expect(() => DataTypes.VECTOR(3, 'float32) DROP TABLE x; --')).to.throw( - TypeError, - 'Invalid VECTOR format', - ); - }); - - it('rejects unknown object-style formats', () => { - expect(() => DataTypes.VECTOR({ dimension: 3, format: 'drop table x' })).to.throw( - TypeError, - 'Invalid VECTOR format', - ); - }); - }); - - describe('toSql', () => { - testDataTypeSql('VECTOR', DataTypes.VECTOR, { - default: new Error(`${dialectName} does not support the VECTOR data type. +describe('DataTypes.VECTOR', () => { + describe('constructor', () => { + it('stores empty options when no args are passed', () => { + const type = DataTypes.VECTOR(); + + expect(type.options).to.deep.equal({}); + }); + + it('stores dimension when only dimension is passed', () => { + const type = DataTypes.VECTOR(4); + + expect(type.options).to.deep.equal({ dimension: 4 }); + }); + + it('stores dimension and format when both args are passed', () => { + const type = DataTypes.VECTOR(3, 'float32'); + + expect(type.options).to.deep.equal({ dimension: 3, format: 'float32' }); + }); + + it('supports object-style options', () => { + const type = DataTypes.VECTOR({ dimension: 8, format: 'float64' }); + + expect(type.options).to.deep.equal({ dimension: 8, format: 'float64' }); + }); + + it('supports object-style options with only dimension', () => { + const type = DataTypes.VECTOR({ dimension: 8 }); + + expect(type.options).to.deep.equal({ dimension: 8 }); + }); + + it('normalizes binary format casing in constructor options', () => { + const type = DataTypes.VECTOR(24, 'BINARY'); + + expect(type.options).to.deep.equal({ dimension: 24, format: 'binary' }); + }); + + it('rejects invalid dimensions', () => { + expect(() => DataTypes.VECTOR(0)).to.throw(TypeError, 'Invalid VECTOR dimension'); + }); + + it('rejects invalid object-style dimensions', () => { + expect(() => DataTypes.VECTOR({ dimension: 0 })).to.throw( + TypeError, + 'Invalid VECTOR dimension', + ); + expect(() => DataTypes.VECTOR({ dimension: 1.5 })).to.throw( + TypeError, + 'Invalid VECTOR dimension', + ); + }); + + it('rejects unknown formats', () => { + expect(() => DataTypes.VECTOR(3, 'float32) DROP TABLE x; --')).to.throw( + TypeError, + 'Invalid VECTOR format', + ); + }); + + it('rejects unknown object-style formats', () => { + expect(() => DataTypes.VECTOR({ dimension: 3, format: 'drop table x' })).to.throw( + TypeError, + 'Invalid VECTOR format', + ); + }); + }); + + describe('toSql', () => { + testDataTypeSql('VECTOR', DataTypes.VECTOR, { + default: new Error(`${dialectName} does not support the VECTOR data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), - oracle: 'VECTOR', - postgres: 'VECTOR', - snowflake: new Error('Snowflake VECTOR requires a positive integer "dimension" option.'), - }); + oracle: 'VECTOR', + postgres: 'VECTOR', + snowflake: new Error('Snowflake VECTOR requires a positive integer "dimension" option.'), + }); - testDataTypeSql('VECTOR(4)', DataTypes.VECTOR(4), { - default: new Error(`${dialectName} does not support the VECTOR data type. + testDataTypeSql('VECTOR(4)', DataTypes.VECTOR(4), { + default: new Error(`${dialectName} does not support the VECTOR data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), - oracle: 'VECTOR(4)', - postgres: 'VECTOR(4)', - snowflake: 'VECTOR(FLOAT, 4)', - }); + oracle: 'VECTOR(4)', + postgres: 'VECTOR(4)', + snowflake: 'VECTOR(FLOAT, 4)', + }); - testDataTypeSql("VECTOR(3, 'float32')", DataTypes.VECTOR(3, 'float32'), { - default: new Error(`${dialectName} does not support the VECTOR data type. + testDataTypeSql("VECTOR(3, 'float32')", DataTypes.VECTOR(3, 'float32'), { + default: new Error(`${dialectName} does not support the VECTOR data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), - oracle: 'VECTOR(3, FLOAT32)', - postgres: 'VECTOR(3)', - snowflake: 'VECTOR(FLOAT, 3)', - }); + oracle: 'VECTOR(3, FLOAT32)', + postgres: 'VECTOR(3)', + snowflake: 'VECTOR(FLOAT, 3)', + }); - testDataTypeSql("VECTOR(24, 'binary')", DataTypes.VECTOR(24, 'binary'), { - default: new Error(`${dialectName} does not support the VECTOR data type. + testDataTypeSql("VECTOR(24, 'binary')", DataTypes.VECTOR(24, 'binary'), { + default: new Error(`${dialectName} does not support the VECTOR data type. See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), - oracle: 'VECTOR(24, BINARY)', - postgres: 'VECTOR(24)', - snowflake: new Error('Invalid Snowflake VECTOR format: binary'), - }); + oracle: 'VECTOR(24, BINARY)', + postgres: 'VECTOR(24)', + snowflake: new Error('Invalid Snowflake VECTOR format: binary'), }); + }); - describe('validate', () => { - it('should throw an error if value is invalid', () => { - const type: DataTypeInstance = DataTypes.VECTOR(); + describe('validate', () => { + it('should throw an error if value is invalid', () => { + const type: DataTypeInstance = DataTypes.VECTOR(); - expect(() => { - type.validate('vector'); - }).to.throw(ValidationErrorItem, "'vector' is not a valid vector"); - }); + expect(() => { + type.validate('vector'); + }).to.throw(ValidationErrorItem, "'vector' is not a valid vector"); + }); - it('should not throw if value is an array', () => { - const type: DataTypeInstance = DataTypes.VECTOR(); + it('should not throw if value is an array', () => { + const type: DataTypeInstance = DataTypes.VECTOR(); - expect(() => type.validate([1, 2, 3])).not.to.throw(); - }); + expect(() => type.validate([1, 2, 3])).not.to.throw(); + }); - it('should not throw if value is a typed array', () => { - const type: DataTypeInstance = DataTypes.VECTOR(); + it('should not throw if value is a typed array', () => { + const type: DataTypeInstance = DataTypes.VECTOR(); - expect(() => type.validate(new Float32Array([1, 2, 3]))).not.to.throw(); - }); + expect(() => type.validate(new Float32Array([1, 2, 3]))).not.to.throw(); + }); - it('should not validate vector elements in the base type', () => { - const type: DataTypeInstance = DataTypes.VECTOR(); + it('should not validate vector elements in the base type', () => { + const type: DataTypeInstance = DataTypes.VECTOR(); - expect(() => type.validate([1, Infinity, 3])).not.to.throw(); - }); + expect(() => type.validate([1, Infinity, 3])).not.to.throw(); }); }); -} +}); diff --git a/packages/oracle/src/_internal/data-types-overrides.ts b/packages/oracle/src/_internal/data-types-overrides.ts index c43604de5657..c8d8d861adfb 100644 --- a/packages/oracle/src/_internal/data-types-overrides.ts +++ b/packages/oracle/src/_internal/data-types-overrides.ts @@ -434,6 +434,9 @@ export class DATEONLY extends BaseTypes.DATEONLY { } } +/** + * Oracle VECTOR data type implementation. + */ export class VECTOR extends BaseTypes.AbstractVECTORBase { /** @hidden */ static readonly [BaseTypes.DataTypeIdentifier]: string = 'VECTOR'; @@ -475,10 +478,18 @@ export class VECTOR extends BaseTypes.AbstractVECTORBase { }; } + /** + * Returns the SQL type name. + */ protected _getTypeName(): string { return 'VECTOR'; } + /** + * Validates the Oracle VECTOR format option. + * + * @param format + */ protected _validateFormat(format: string): OracleVectorFormat { const normalized = format.trim().toUpperCase(); @@ -494,6 +505,11 @@ export class VECTOR extends BaseTypes.AbstractVECTORBase { } } + /** + * Validates the Oracle VECTOR dimension option. + * + * @param dimension + */ protected _validateOracleDimension(dimension: number | '*'): number | '*' { if (dimension === '*') { return dimension; @@ -502,6 +518,11 @@ export class VECTOR extends BaseTypes.AbstractVECTORBase { return this._validateDimension(dimension); } + /** + * Validates the Oracle VECTOR storage option. + * + * @param storage + */ protected _validateStorage(storage: string): OracleVectorStorage { const normalized = storage.trim().toUpperCase(); @@ -513,6 +534,9 @@ export class VECTOR extends BaseTypes.AbstractVECTORBase { } } + /** + * Returns SQL option parts for Oracle VECTOR. + */ protected _getSqlOptionParts(): string[] { const options = this.options; @@ -525,6 +549,11 @@ export class VECTOR extends BaseTypes.AbstractVECTORBase { ]; } + /** + * Validates supported dense and sparse vector input values. + * + * @param value + */ validate(value: unknown): asserts value is BaseTypes.VectorValue | OracleSparseVectorInput { if (isOracleSparseVectorInput(value)) { return; @@ -533,6 +562,11 @@ export class VECTOR extends BaseTypes.AbstractVECTORBase { super.validate(value); } + /** + * Converts values to Oracle bindable vector values. + * + * @param value + */ toBindableValue(value: BaseTypes.VectorValue | OracleSparseVectorInput) { if (Array.isArray(value)) { return Float64Array.from(value); @@ -541,11 +575,21 @@ export class VECTOR extends BaseTypes.AbstractVECTORBase { return value; } + /** + * Returns Oracle bind definition for vector values. + * + * @param oracledb + */ _getBindDef(oracledb: Lib) { return { type: oracledb.DB_TYPE_VECTOR }; } } +/** + * Checks whether a value matches sparse vector input shape. + * + * @param value + */ function isOracleSparseVectorInput(value: unknown): value is OracleSparseVectorInput { if (typeof value !== 'object' || value === null) { return false; @@ -557,17 +601,27 @@ function isOracleSparseVectorInput(value: unknown): value is OracleSparseVectorI return ( isVectorComponent(values) && isVectorComponent(indices) && + typeof numDimensions === 'number' && Number.isInteger(numDimensions) && - numDimensions !== undefined && numDimensions > 0 && values.length === indices.length ); } +/** + * Checks whether a value can be used as a sparse vector component. + * + * @param value + */ function isVectorComponent(value: unknown): value is number[] | BaseTypes.NumericTypedArray { return Array.isArray(value) || isNumericTypedArray(value); } +/** + * Checks whether a value is supported numeric typed array. + * + * @param value + */ function isNumericTypedArray(value: unknown): value is BaseTypes.NumericTypedArray { return ( value instanceof Int8Array || From 9f1579e63881214b81a744c18ffb68f795813a3d Mon Sep 17 00:00:00 2001 From: prajalg Date: Fri, 29 May 2026 08:07:05 +0530 Subject: [PATCH 47/49] updated test expectations for vector --- .../test/unit/data-types/string-types.test.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/core/test/unit/data-types/string-types.test.ts b/packages/core/test/unit/data-types/string-types.test.ts index 81e2101f94ac..6b55096fe8e7 100644 --- a/packages/core/test/unit/data-types/string-types.test.ts +++ b/packages/core/test/unit/data-types/string-types.test.ts @@ -286,34 +286,26 @@ describe('DataTypes.VECTOR', () => { describe('toSql', () => { testDataTypeSql('VECTOR', DataTypes.VECTOR, { default: new Error(`${dialectName} does not support the VECTOR data type. - See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), +See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR', - postgres: 'VECTOR', - snowflake: new Error('Snowflake VECTOR requires a positive integer "dimension" option.'), }); testDataTypeSql('VECTOR(4)', DataTypes.VECTOR(4), { default: new Error(`${dialectName} does not support the VECTOR data type. - See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), +See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR(4)', - postgres: 'VECTOR(4)', - snowflake: 'VECTOR(FLOAT, 4)', }); testDataTypeSql("VECTOR(3, 'float32')", DataTypes.VECTOR(3, 'float32'), { default: new Error(`${dialectName} does not support the VECTOR data type. - See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), +See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR(3, FLOAT32)', - postgres: 'VECTOR(3)', - snowflake: 'VECTOR(FLOAT, 3)', }); testDataTypeSql("VECTOR(24, 'binary')", DataTypes.VECTOR(24, 'binary'), { default: new Error(`${dialectName} does not support the VECTOR data type. - See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), +See https://sequelize.org/docs/v7/models/data-types/ for a list of supported data types.`), oracle: 'VECTOR(24, BINARY)', - postgres: 'VECTOR(24)', - snowflake: new Error('Invalid Snowflake VECTOR format: binary'), }); }); From f42eebebbf8f2edc553c91cbd880363a310fcce4 Mon Sep 17 00:00:00 2001 From: prajalg Date: Fri, 29 May 2026 17:55:49 +0530 Subject: [PATCH 48/49] some minor changes + addressed bot comments --- .../core/test/integration/dialects/oracle/vector.test.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/core/test/integration/dialects/oracle/vector.test.js b/packages/core/test/integration/dialects/oracle/vector.test.js index d86e3bf364ca..4369c7dbebda 100644 --- a/packages/core/test/integration/dialects/oracle/vector.test.js +++ b/packages/core/test/integration/dialects/oracle/vector.test.js @@ -136,9 +136,10 @@ if (getTestDialect() === 'oracle') { } }); - it('supports raw SQL bind parameters for VECTOR_DISTANCE query vectors', async () => { + it('supports raw SQL bind parameters for VECTOR_DISTANCE query vectors', async function () { + const tableName = sequelize.queryGenerator.quoteTable(this.Item); const rows = await sequelize.query( - `SELECT "id" FROM "Items" WHERE VECTOR_DISTANCE("embeddings", $queryVector) < $threshold ORDER BY "id"`, + `SELECT "id" FROM ${tableName} WHERE VECTOR_DISTANCE("embeddings", $queryVector) < $threshold ORDER BY "id"`, { bind: { queryVector: Float32Array.from([1, 2, 3]), @@ -402,7 +403,7 @@ if (getTestDialect() === 'oracle') { 'Oracle VECTOR indexes do not support ordered fields.', ); } finally { - await IndexedItem.drop(); + await IndexedItem.drop().catch(() => {}); } }); }); From c7700944925914dd936e96ad2dabb4dbdf7e9477 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 09:58:36 +0000 Subject: [PATCH 49/49] meta: update dependency esbuild to v0.28.1 [security] (#18237) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 220 +++++++++++++++++++++++++-------------------------- 2 files changed, 111 insertions(+), 111 deletions(-) diff --git a/package.json b/package.json index ca4b45b150bd..55fa348856fe 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "chai": "4.5.0", "concurrently": "9.2.1", "cross-env": "10.1.0", - "esbuild": "0.28.0", + "esbuild": "0.28.1", "eslint": "8.57.1", "eslint-plugin-jsdoc": "61.7.1", "eslint-plugin-mocha": "10.5.0", diff --git a/yarn.lock b/yarn.lock index ed963870961b..35ca6e1867cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1345,184 +1345,184 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/aix-ppc64@npm:0.28.0" +"@esbuild/aix-ppc64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/aix-ppc64@npm:0.28.1" conditions: os=aix & cpu=ppc64 languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/android-arm64@npm:0.28.0" +"@esbuild/android-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-arm64@npm:0.28.1" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@esbuild/android-arm@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/android-arm@npm:0.28.0" +"@esbuild/android-arm@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-arm@npm:0.28.1" conditions: os=android & cpu=arm languageName: node linkType: hard -"@esbuild/android-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/android-x64@npm:0.28.0" +"@esbuild/android-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-x64@npm:0.28.1" conditions: os=android & cpu=x64 languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/darwin-arm64@npm:0.28.0" +"@esbuild/darwin-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/darwin-arm64@npm:0.28.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/darwin-x64@npm:0.28.0" +"@esbuild/darwin-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/darwin-x64@npm:0.28.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/freebsd-arm64@npm:0.28.0" +"@esbuild/freebsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/freebsd-arm64@npm:0.28.1" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/freebsd-x64@npm:0.28.0" +"@esbuild/freebsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/freebsd-x64@npm:0.28.1" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-arm64@npm:0.28.0" +"@esbuild/linux-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-arm64@npm:0.28.1" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-arm@npm:0.28.0" +"@esbuild/linux-arm@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-arm@npm:0.28.1" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-ia32@npm:0.28.0" +"@esbuild/linux-ia32@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-ia32@npm:0.28.1" conditions: os=linux & cpu=ia32 languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-loong64@npm:0.28.0" +"@esbuild/linux-loong64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-loong64@npm:0.28.1" conditions: os=linux & cpu=loong64 languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-mips64el@npm:0.28.0" +"@esbuild/linux-mips64el@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-mips64el@npm:0.28.1" conditions: os=linux & cpu=mips64el languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-ppc64@npm:0.28.0" +"@esbuild/linux-ppc64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-ppc64@npm:0.28.1" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-riscv64@npm:0.28.0" +"@esbuild/linux-riscv64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-riscv64@npm:0.28.1" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-s390x@npm:0.28.0" +"@esbuild/linux-s390x@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-s390x@npm:0.28.1" conditions: os=linux & cpu=s390x languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/linux-x64@npm:0.28.0" +"@esbuild/linux-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-x64@npm:0.28.1" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/netbsd-arm64@npm:0.28.0" +"@esbuild/netbsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/netbsd-arm64@npm:0.28.1" conditions: os=netbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/netbsd-x64@npm:0.28.0" +"@esbuild/netbsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/netbsd-x64@npm:0.28.1" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/openbsd-arm64@npm:0.28.0" +"@esbuild/openbsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openbsd-arm64@npm:0.28.1" conditions: os=openbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/openbsd-x64@npm:0.28.0" +"@esbuild/openbsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openbsd-x64@npm:0.28.1" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/openharmony-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/openharmony-arm64@npm:0.28.0" +"@esbuild/openharmony-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openharmony-arm64@npm:0.28.1" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/sunos-x64@npm:0.28.0" +"@esbuild/sunos-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/sunos-x64@npm:0.28.1" conditions: os=sunos & cpu=x64 languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/win32-arm64@npm:0.28.0" +"@esbuild/win32-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-arm64@npm:0.28.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/win32-ia32@npm:0.28.0" +"@esbuild/win32-ia32@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-ia32@npm:0.28.1" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.28.0": - version: 0.28.0 - resolution: "@esbuild/win32-x64@npm:0.28.0" +"@esbuild/win32-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-x64@npm:0.28.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -3129,7 +3129,7 @@ __metadata: chai: "npm:4.5.0" concurrently: "npm:9.2.1" cross-env: "npm:10.1.0" - esbuild: "npm:0.28.0" + esbuild: "npm:0.28.1" eslint: "npm:8.57.1" eslint-plugin-jsdoc: "npm:61.7.1" eslint-plugin-mocha: "npm:10.5.0" @@ -7250,36 +7250,36 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:0.28.0": - version: 0.28.0 - resolution: "esbuild@npm:0.28.0" - dependencies: - "@esbuild/aix-ppc64": "npm:0.28.0" - "@esbuild/android-arm": "npm:0.28.0" - "@esbuild/android-arm64": "npm:0.28.0" - "@esbuild/android-x64": "npm:0.28.0" - "@esbuild/darwin-arm64": "npm:0.28.0" - "@esbuild/darwin-x64": "npm:0.28.0" - "@esbuild/freebsd-arm64": "npm:0.28.0" - "@esbuild/freebsd-x64": "npm:0.28.0" - "@esbuild/linux-arm": "npm:0.28.0" - "@esbuild/linux-arm64": "npm:0.28.0" - "@esbuild/linux-ia32": "npm:0.28.0" - "@esbuild/linux-loong64": "npm:0.28.0" - "@esbuild/linux-mips64el": "npm:0.28.0" - "@esbuild/linux-ppc64": "npm:0.28.0" - "@esbuild/linux-riscv64": "npm:0.28.0" - "@esbuild/linux-s390x": "npm:0.28.0" - "@esbuild/linux-x64": "npm:0.28.0" - "@esbuild/netbsd-arm64": "npm:0.28.0" - "@esbuild/netbsd-x64": "npm:0.28.0" - "@esbuild/openbsd-arm64": "npm:0.28.0" - "@esbuild/openbsd-x64": "npm:0.28.0" - "@esbuild/openharmony-arm64": "npm:0.28.0" - "@esbuild/sunos-x64": "npm:0.28.0" - "@esbuild/win32-arm64": "npm:0.28.0" - "@esbuild/win32-ia32": "npm:0.28.0" - "@esbuild/win32-x64": "npm:0.28.0" +"esbuild@npm:0.28.1": + version: 0.28.1 + resolution: "esbuild@npm:0.28.1" + dependencies: + "@esbuild/aix-ppc64": "npm:0.28.1" + "@esbuild/android-arm": "npm:0.28.1" + "@esbuild/android-arm64": "npm:0.28.1" + "@esbuild/android-x64": "npm:0.28.1" + "@esbuild/darwin-arm64": "npm:0.28.1" + "@esbuild/darwin-x64": "npm:0.28.1" + "@esbuild/freebsd-arm64": "npm:0.28.1" + "@esbuild/freebsd-x64": "npm:0.28.1" + "@esbuild/linux-arm": "npm:0.28.1" + "@esbuild/linux-arm64": "npm:0.28.1" + "@esbuild/linux-ia32": "npm:0.28.1" + "@esbuild/linux-loong64": "npm:0.28.1" + "@esbuild/linux-mips64el": "npm:0.28.1" + "@esbuild/linux-ppc64": "npm:0.28.1" + "@esbuild/linux-riscv64": "npm:0.28.1" + "@esbuild/linux-s390x": "npm:0.28.1" + "@esbuild/linux-x64": "npm:0.28.1" + "@esbuild/netbsd-arm64": "npm:0.28.1" + "@esbuild/netbsd-x64": "npm:0.28.1" + "@esbuild/openbsd-arm64": "npm:0.28.1" + "@esbuild/openbsd-x64": "npm:0.28.1" + "@esbuild/openharmony-arm64": "npm:0.28.1" + "@esbuild/sunos-x64": "npm:0.28.1" + "@esbuild/win32-arm64": "npm:0.28.1" + "@esbuild/win32-ia32": "npm:0.28.1" + "@esbuild/win32-x64": "npm:0.28.1" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -7335,7 +7335,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10c0/8acd95c238ec6c4a9d16163277faf228a8994b642d187b3fe667ffbb469008e6748cde144fdc3c175bf8e78ee49e15a0ed9b9f183fdb5fcea1772f87fb1372a4 + checksum: 10c0/29cd456a79ce35ac2c7e05fe871330416b2c395c045d849653f843e51378d6e0d6e774d6dcd01b35f4e83238a29bf8decd04fcd34b3780c589a250b21e5f92bb languageName: node linkType: hard