Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
967905f
feat(oracle): initial commit for vectors
hjamil-24 Sep 9, 2024
9745378
feat(oracle): remove commented code
hjamil-24 Sep 16, 2024
f912f9b
feat(oracle): add validation
hjamil-24 Sep 24, 2024
4b91c3d
feat(oracle): move code to racle layer
hjamil-24 Sep 24, 2024
c7b887f
feat(oracle): add hnsw by default for indexing
hjamil-24 Sep 27, 2024
ce2b4ef
feat(oracle): add test cases, type definition
hjamil-24 Sep 30, 2024
27a33c6
feat(oracle): add index test-cases
hjamil-24 Sep 30, 2024
af297cd
Merge branch 'vector_support' of orahub.oci.oraclecorp.com:database-d…
Oct 24, 2024
c461d72
feat(oracle): add test cases for where and orderby
Oct 24, 2024
d0a62a3
feat(oracle): add exports
hjamil-24 Nov 18, 2024
eeac603
feat(oracle): move similarity search functions to QueryGenerator
hjamil-24 Nov 20, 2024
432cfb5
feat(oracle): remove imports in datatype
hjamil-24 Nov 20, 2024
72b0373
feat(oracle): move vector functions to use sequelize.fn()
hjamil-24 Nov 22, 2024
d58b1a4
feat(oracle): update validations
hjamil-24 Nov 25, 2024
59be00f
feat(oracle): fix review comments
hjamil-24 Nov 26, 2024
86b6d02
feat(oracle): fix review comments
hjamil-24 Nov 26, 2024
2418ca5
feat(oracle): fix validation condition and limit test case
hjamil-24 Dec 2, 2024
bf09c80
feat(oracle): revert breaking changes
hjamil-24 Dec 3, 2024
1e8f815
feat(oracle): fix review comments
hjamil-24 Dec 9, 2024
d7d0735
feat(oracle): add integration test
hjamil-24 Dec 10, 2024
18d4ecd
feat(oracle): add target accuracy
hjamil-24 Jan 16, 2025
0e93920
feat(oracle): bump up oracledb
hjamil-24 Feb 4, 2025
c3f71e5
fix(oracle): pin oracledb to '6.6.0' to avoid failures in node 10
hjamil-24 Feb 4, 2025
d70af7a
feat(oracle): bump up the node version
hjamil-24 Feb 5, 2025
598a226
fix(oracle): revert backnode version
hjamil-24 Feb 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
"mysql2": "^2.3.3",
"node-hook": "^1.0.0",
"nyc": "^15.1.0",
"oracledb": "^5.5.0",
"oracledb": "6.6.0",
"p-map": "^4.0.0",
"p-props": "^4.0.0",
"p-settle": "^4.1.1",
Expand Down
5 changes: 5 additions & 0 deletions src/data-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,5 +614,10 @@ export const CITEXT: AbstractDataTypeConstructor;
*/
export const TSVECTOR: AbstractDataTypeConstructor;

/**
* VECTOR. Only available in Oracle Database.
*/
export const VECTOR: AbstractDataTypeConstructor;

// umzug compatibility
export type DataTypeAbstract = AbstractDataTypeConstructor;
19 changes: 18 additions & 1 deletion src/data-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,22 @@ class TSVECTOR extends ABSTRACT {
}
}

/**
* The VECTOR type stores vectors.
*
* Only available for Oracle Database >= 23ai
*
*/
class VECTOR extends ABSTRACT {
constructor(dimension, format) {
super();
const options = typeof dimension === 'object' && dimension || { dimension, format };
this.options = options;
this._format = options.format;
this._length = options.dimension;
}
}

/**
* A convenience class holding commonly used data types. The data types are used when defining a new model using `Sequelize.define`, like this:
* ```js
Expand Down Expand Up @@ -1041,7 +1057,8 @@ const DataTypes = module.exports = {
INET,
MACADDR,
CITEXT,
TSVECTOR
TSVECTOR,
VECTOR
};

_.each(DataTypes, (dataType, name) => {
Expand Down
38 changes: 37 additions & 1 deletion src/dialects/oracle/data-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

'use strict';

const util = require('util');
const moment = require('moment');
const momentTz = require('moment-timezone');
const sequelizeErrors = require('../../errors');

module.exports = BaseTypes => {
const warn = BaseTypes.ABSTRACT.warn.bind(
Expand All @@ -30,6 +32,7 @@ module.exports = BaseTypes => {
BaseTypes.REAL.types.oracle = ['BINARY_DOUBLE'];
BaseTypes.DOUBLE.types.oracle = ['BINARY_DOUBLE'];
BaseTypes.JSON.types.oracle = ['BLOB'];
BaseTypes.VECTOR.types.oracle = ['VECTOR'];
BaseTypes.GEOMETRY.types.oracle = false;

class STRING extends BaseTypes.STRING {
Expand Down Expand Up @@ -459,6 +462,38 @@ module.exports = BaseTypes => {

DATEONLY.prototype.escape = false;

class VECTOR extends BaseTypes.VECTOR {
toSql() {
if (this._length && this._format) {
return `VECTOR(${this._length}, ${this._format.toUpperCase()})`;
}
if (this._length) {
return `VECTOR(${this._length}, *)`;
}

return 'VECTOR';
}

validate(value) {
// BYTES_PER_ELEMENT is static property only available in typedArrays.
if (!value.constructor.BYTE_PER_ELEMENT || !Array.isArray(value)) {
throw new sequelizeErrors.ValidationError(util.format('%j is not a valid array', value));
}
return true;
}

_stringify(value, options) {
if (Array.isArray(value)) {
return Float64Array.from(value, val => val);
}
return value;
}

_getBindDef(oracledb) {
return { type: oracledb.DB_TYPE_VECTOR };
}
}

return {
BOOLEAN,
'DOUBLE PRECISION': DOUBLE,
Expand All @@ -481,6 +516,7 @@ module.exports = BaseTypes => {
CHAR,
JSON: JSONTYPE,
REAL,
DECIMAL
DECIMAL,
VECTOR
};
};
5 changes: 3 additions & 2 deletions src/dialects/oracle/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ OracleDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype
collate: false,
length: false,
parser: false,
type: false,
using: false
type: true,
operator: false,
using: true
},
constraints: {
restrict: false
Expand Down
139 changes: 138 additions & 1 deletion src/dialects/oracle/query-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const DataTypes = require('../../data-types');
const AbstractQueryGenerator = require('../abstract/query-generator');
const _ = require('lodash');
const util = require('util');
const Model = require('../../model');
const Transaction = require('../../transaction');

/**
Expand Down Expand Up @@ -387,7 +388,114 @@ export class OracleQueryGenerator extends AbstractQueryGenerator {
if (typeof tableName !== 'string' && attributes.name) {
attributes.name = `${tableName.schema}.${attributes.name}`;
}
return super.addIndexQuery(tableName, attributes, options, rawTablename);

options = 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.replace(/\./g, '_');
options.prefix = options.prefix.replace(/("|')/g, '');
}

const fieldsSql = options.fields.map(field => {
if (field instanceof Utils.SequelizeMethod) {
return this.handleSequelizeMethod(field);
}
if (typeof field === 'string') {
field = {
name: field
};
}
let result = '';

if (field.attribute) {
field.name = field.attribute;
}

if (!field.name) {
throw new Error(`The following index field has no name: ${util.inspect(field)}`);
}

result += this.quoteIdentifier(field.name);

if (field.order) {
result += ` ${field.order}`;
}

return result;
});

if (!options.name) {
// Mostly for cases where addIndex is called directly by the user without an options object (for example in migrations)
// All calls that go through sequelize should already have a name
options = Utils.nameIndex(options, options.prefix);
}

options = Model._conformIndex(options);

if (typeof tableName === 'string') {
tableName = this.quoteIdentifiers(tableName);
} else {
tableName = this.quoteTable(tableName);
}

let ind = ['CREATE'];

if (options.type === 'VECTOR') {
let idxParameter = 'PARAMETERS (type ';
options.using = options.using || 'hnsw';
if (options.parameter) {
if (options.using === 'hnsw') {
idxParameter += 'hnsw';
if (options.parameter.neighbor) {
idxParameter += `, neighbor ${options.parameter.neighbor}`;
}
if (options.parameter.efconstruction) {
idxParameter += `, efconstruction ${options.parameter.efconstruction}`;
}
} else {
idxParameter += 'ivf';
if (options.parameter.partitions) {
idxParameter += `, NEIGHBOR PARTITION ${options.parameter.partitions}`;
}
if (options.parameter.samplesPerPartition) {
idxParameter += `, SAMPLES_PER_PARTITION ${options.parameter.samplesPerPartition}`;
}
if (options.parameter.minVectors) {
idxParameter += `, MIN_VECORS_PER_PARTITIONS ${options.parameter.minVectors}`;
}
}
idxParameter += ')';
}
ind = ind.concat(
options.type, 'INDEX',
this.quoteIdentifiers(options.name),
`ON ${tableName}`,
`(${fieldsSql.join(', ')})`,
'ORAGANIZATION ',
options.using === 'hnsw' ? 'INMEMORY NEIGHBOR GRAPH ' : 'NEIGHBOR PARTITION GRAPH ',
options.distance ? `WITH DISTANCE ${options.distance}` : '',
options.accuracy ? `WITH TARGET ACCURACY ${options.accuracy}` : '',
options.parameter ? idxParameter : ''
);
} else {
ind = ind.concat(
options.unique ? 'UNIQUE' : '',
'INDEX',
this.quoteIdentifiers(options.name),
`ON ${tableName}`,
`(${fieldsSql.join(', ')})`
);
}

return _.compact(ind).join(' ');
}

addConstraintQuery(tableName, options) {
Expand Down Expand Up @@ -1137,6 +1245,35 @@ export class OracleQueryGenerator extends AbstractQueryGenerator {
}
}
}
const vectorFunctions = [
'COSINE_DISTANCE',
'INNER_PRODUCT',
'L1_DISTANCE',
'L2_DISTANCE',
'VECTOR_DISTANCE'
];
if (smth instanceof Utils.Fn && vectorFunctions.includes(smth.fn)) {
if (smth.args.length > 2) {
throw new Error('Too many arguments passed to similarity search function');
}

if (typeof smth.args[1] === 'string') {
if (!smth.args[1].startsWith('VECTOR')) {
throw new Error('Unexpected second argument');
}
} else if (!Array.isArray(smth.args[1])) {
throw new Error('Unexpected second argument');
}
// The first argument is expected to be column name
// The second argument is expected to be array.
smth.args[0] = this.quoteIdentifier(smth.args[0]);
if (Array.isArray(smth.args[1])) {
smth.args[1] = `VECTOR('[${smth.args[1]}]')`;
}
return `${smth.fn}(${
smth.args.join(', ')
})`;
}
return super.handleSequelizeMethod(smth, tableName, factory, options, prepend);
}

Expand Down
1 change: 1 addition & 0 deletions src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const INET = Pkg.INET;
export const MACADDR = Pkg.MACADDR;
export const CITEXT = Pkg.CITEXT;
export const TSVECTOR = Pkg.TSVECTOR;
export const VECTOR = Pkg.VECTOR;

// export * from './lib/model';
export const Model = Pkg.Model;
Expand Down
75 changes: 75 additions & 0 deletions src/sequelize.js
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,81 @@ class Sequelize {
return this.fn('RAND');
}

/**
* Generates the cosineDistance clause for Vector Columns
*
* @param {string} column
* @param {Array} value
*
* @returns {Sequelize.fn}
*/
cosineDistance(column, value) {
if (['oracle'].includes(this.getDialect())) {
return this.fn('COSINE_DISTANCE', column, value);
}
throw new Error(`cosineDistance for Dialect "${this.getDialect()}" is not implemented`);
}

/**
* Generates the innerProduct clause for Vector Columns
*
* @param {string} column
* @param {Array} value
*
* @returns {Sequelize.fn}
*/
innerProduct(column, value) {
if (['oracle'].includes(this.getDialect())) {
return this.fn('INNER_PRODUCT', column, value);
}
throw new Error(`innerProduct for Dialect "${this.getDialect()}" is not implemented`);
}

/**
* Generates the l1Distance clause for Vector Columns
*
* @param {string} column
* @param {Array} value
*
* @returns {Sequelize.fn}
*/
l1Distance(column, value) {
if (['oracle'].includes(this.getDialect())) {
return this.fn('L1_DISTANCE', column, value);
}
throw new Error(`l1Distance for Dialect "${this.getDialect()}" is not implemented`);
}

/**
* Generates the cl2Distance clause for Vector Columns
*
* @param {string} column
* @param {Array} value
*
* @returns {Sequelize.fn}
*/
l2Distance(column, value) {
if (['oracle'].includes(this.getDialect())) {
return this.fn('L2_DISTANCE', column, value);
}
throw new Error(`l2Distance for Dialect "${this.getDialect()}" is not implemented`);
}

/**
* Generates the vectorDistance clause for Vector Columns
*
* @param {string} column
* @param {Array} value
*
* @returns {Sequelize.fn}
*/
vectorDistance(column, value) {
if (['oracle'].includes(this.getDialect())) {
return this.fn('VECTOR_DISTANCE', column, value);
}
throw new Error(`vectorDistance for Dialect "${this.getDialect()}" is not implemented`);
}

/**
* Creates an object representing a database function. This can be used in search queries, both in where and order parts, and as default values in column definitions.
* If you want to refer to columns in your function, you should use `sequelize.col`, so that the columns are properly interpreted as columns and not a strings.
Expand Down
Loading