diff --git a/package.json b/package.json index bc69e634ae69..7bc27e082919 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/data-types.d.ts b/src/data-types.d.ts index 2eb62626a331..083f9b7959e5 100644 --- a/src/data-types.d.ts +++ b/src/data-types.d.ts @@ -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; diff --git a/src/data-types.js b/src/data-types.js index f75ee2124ba7..6f9fd98e9662 100644 --- a/src/data-types.js +++ b/src/data-types.js @@ -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 @@ -1041,7 +1057,8 @@ const DataTypes = module.exports = { INET, MACADDR, CITEXT, - TSVECTOR + TSVECTOR, + VECTOR }; _.each(DataTypes, (dataType, name) => { diff --git a/src/dialects/oracle/data-types.js b/src/dialects/oracle/data-types.js index 2bb99a00debc..5328bc1943ba 100644 --- a/src/dialects/oracle/data-types.js +++ b/src/dialects/oracle/data-types.js @@ -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( @@ -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 { @@ -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, @@ -481,6 +516,7 @@ module.exports = BaseTypes => { CHAR, JSON: JSONTYPE, REAL, - DECIMAL + DECIMAL, + VECTOR }; }; diff --git a/src/dialects/oracle/index.js b/src/dialects/oracle/index.js index 2e03d9f1585b..80a30b11d78a 100644 --- a/src/dialects/oracle/index.js +++ b/src/dialects/oracle/index.js @@ -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 diff --git a/src/dialects/oracle/query-generator.js b/src/dialects/oracle/query-generator.js index 4392d722b003..1b58d2222cec 100644 --- a/src/dialects/oracle/query-generator.js +++ b/src/dialects/oracle/query-generator.js @@ -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'); /** @@ -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) { @@ -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); } diff --git a/src/index.mjs b/src/index.mjs index 5752a6422721..406364477ada 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -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; diff --git a/src/sequelize.js b/src/sequelize.js index 3098dd0c81ea..03d49b30c703 100644 --- a/src/sequelize.js +++ b/src/sequelize.js @@ -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. diff --git a/test/integration/dialects/oracle/vector.test.js b/test/integration/dialects/oracle/vector.test.js new file mode 100644 index 000000000000..d7942a9bd6f5 --- /dev/null +++ b/test/integration/dialects/oracle/vector.test.js @@ -0,0 +1,112 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +'use strict'; + +const chai = require('chai'), + Sequelize = require('sequelize'), + Op = Sequelize.Op, + expect = chai.expect, + Support = require('../../support'), + current = Support.sequelize, + DataTypes = require('sequelize/lib/data-types'), + dialect = Support.getTestDialect(), + semver = require('semver'); + +if (dialect === 'oracle') { + + describe('vectors', () => { + before(async function() { + const version = await current.queryInterface.databaseVersion(); + const supportedVersion = '23.4.0'; + if (semver.gte(version, supportedVersion) === false) { + this.skip(); + } + }); + + describe('findAll', () => { + beforeEach(async function() { + this.Item = this.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 the rows from database', async function() { + const Item = this.sequelize.define('Item', { embeddings: Sequelize.VECTOR(4) }); + const result = await Item.findAll(); + expect(result.length).to.equal(2); + }); + + it('returns typed array for vector column', async function() { + const Item = this.sequelize.define('Item', { embeddings: Sequelize.VECTOR(4) }); + const result = await Item.findAll(); + // typed array property that differentiate it from other buffer view. + expect(result[0].getDataValue('embeddings').BYTES_PER_ELEMENT).to.equal(4); + }); + }); + + describe('similarity search functions', () => { + beforeEach(async function() { + this.Item = this.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('l1 distance', async function() { + const Item = this.sequelize.define('Item', { embeddings: Sequelize.VECTOR(3) }); + const queryVector = [1, 2, 3]; + const result = await Item.findAll({ + where: current.where(current.fn('L1_DISTANCE', 'embeddings', queryVector), { + [Op.lt]: 2 + }) + }); + expect(result.length).to.equal(1); + }); + + it('l2 distance', async function() { + const Item = this.sequelize.define('Item', { embeddings: Sequelize.VECTOR(3) }); + const queryVector = [1, 2, 3]; + const result = await Item.findAll({ + where: current.where(current.fn('L2_DISTANCE', 'embeddings', queryVector), { + [Op.lt]: 3 + }) + }); + expect(result.length).to.equal(2); + }); + + it('inner product', async function() { + const Item = this.sequelize.define('Item', { embeddings: Sequelize.VECTOR(3) }); + const queryVector = [1, 2, 3]; + const result = await Item.findAll({ + where: current.where(current.fn('INNER_PRODUCT', 'embeddings', queryVector), { + [Op.lt]: 3 + }) + }); + expect(result.length).to.equal(0); + }); + + it('cosine distance', async function() { + const Item = this.sequelize.define('Item', { embeddings: Sequelize.VECTOR(3) }); + const queryVector = [1, 2, 3]; + const result = await Item.findAll({ + where: current.where(current.fn('COSINE_DISTANCE', 'embeddings', queryVector), { + [Op.gt]: 1 + }) + }); + expect(result.length).to.equal(0); + }); + }); + + }); +} diff --git a/test/unit/dialects/oracle/vector.test.js b/test/unit/dialects/oracle/vector.test.js new file mode 100644 index 000000000000..e220912c3b62 --- /dev/null +++ b/test/unit/dialects/oracle/vector.test.js @@ -0,0 +1,181 @@ +// Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved + +'use strict'; + +const util = require('util'); +const Support = require('../../support'); +const DataTypes = require('sequelize/lib/data-types'); +const expectsql = Support.expectsql; +const current = Support.sequelize; +const sql = current.dialect.queryGenerator; +const Op = Support.Sequelize.Op; + +if (current.dialect.name === 'oracle') { + describe('VECTORS', () => { + describe('VECTOR datatype', () => { + const FooUser = current.define('user', { + embedding: { + type: DataTypes.VECTOR, + allowNull: false + } + }); + + it('creates table with vector datatype', () => { + expectsql(sql.createTableQuery(FooUser.getTableName(), sql.attributesToSQL(FooUser.rawAttributes), { }), { + 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', () => { + const FooUser = current.define('user', { + embedding: { + type: DataTypes.VECTOR(3, 'float32'), + allowNull: false + } + }); + + it('creates table with vector datatype', () => { + expectsql(sql.createTableQuery(FooUser.getTableName(), sql.attributesToSQL(FooUser.rawAttributes), { }), { + 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 datatype(binary)', () => { + const FooUser = current.define('user', { + embedding: { + type: DataTypes.VECTOR(16, 'binary'), + allowNull: false + } + }); + + it('creates table with vector datatype', () => { + expectsql(sql.createTableQuery(FooUser.getTableName(), sql.attributesToSQL(FooUser.rawAttributes), { }), { + default: 'BEGIN EXECUTE IMMEDIATE \'CREATE TABLE "users" ("id" NUMBER(*,0) GENERATED BY DEFAULT ON NULL AS IDENTITY, "embedding" VECTOR(16, BINARY) 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(sql.addIndexQuery('Foo', ['vec1'], { type: 'VECTOR' }), { + default: 'CREATE VECTOR INDEX "foo_vec1" ON "Foo" ("vec1") ORAGANIZATION INMEMORY NEIGHBOR GRAPH' }); + }); + + it('type and using(hnsw)', () => { + expectsql(sql.addIndexQuery('foo', ['vec1'], { type: 'VECTOR', using: 'hnsw' }), { + default: 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORAGANIZATION INMEMORY NEIGHBOR GRAPH' }); + }); + + it('type and using(ivf)', () => { + expectsql(sql.addIndexQuery('foo', ['vec1'], { type: 'VECTOR', using: 'ivf' }), { + default: 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORAGANIZATION NEIGHBOR PARTITION GRAPH' }); + }); + + it('hnsw parameter', () => { + expectsql(sql.addIndexQuery('foo', ['vec1'], { type: 'VECTOR', using: 'hnsw', parameter: { neighbor: 10, efconstruction: 10 } }), { + default: 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORAGANIZATION INMEMORY NEIGHBOR GRAPH PARAMETERS (type hnsw, neighbor 10, efconstruction 10)' }); + }); + + it('ivf parameter', () => { + expectsql(sql.addIndexQuery('foo', ['vec1'], { type: 'VECTOR', using: 'ivf', parameter: { partitions: 5, samplesPerPartition: 10, minVectors: 10 } }), { + default: 'CREATE VECTOR INDEX "foo_vec1" ON "foo" ("vec1") ORAGANIZATION NEIGHBOR PARTITION GRAPH PARAMETERS (type ivf, NEIGHBOR PARTITION 5, SAMPLES_PER_PARTITION 10, MIN_VECORS_PER_PARTITIONS 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(sql.whereItemQuery(key, value, options), expectation); + }); + }; + + testsql(current.fn('VECTOR_DISTANCE', 'embedding', queryVector), { + [Op.lt]: 2 + }, { + oracle: 'VECTOR_DISTANCE("embedding", VECTOR(\'[1,2,3]\')) < 2' + }); + }); + + describe('order by distances', () => { + const queryVector = [1, 2, 3, 4]; + const testsql = (options, expectation) => { + const model = options.model; + + it(util.inspect(options, { depth: 2 }), () => { + return expectsql( + sql.selectQuery( + options.table || model && model.getTableName(), + options, + options.model + ), + expectation + ); + }); + }; + + const User = Support.sequelize.define('User', { + embedding: { + type: DataTypes.VECTOR(4) + } + }, { + tableName: 'user' + }); + + testsql({ + model: User, + attributes: ['embedding'], + order: [ + current.fn('VECTOR_DISTANCE', 'embedding', queryVector) + ] + }, { + oracle: 'SELECT "embedding" FROM "user" "User" ORDER BY VECTOR_DISTANCE("embedding", VECTOR(\'[1,2,3,4]\'));' + }); + }); + + describe('limit', () => { + const queryVector = [1, 2, 3, 4]; + const testsql = (options, expectation) => { + const model = options.model; + + it(util.inspect(options, { depth: 2 }), () => { + return expectsql( + sql.selectQuery( + options.table || model && model.getTableName(), + options, + options.model + ), + expectation + ); + }); + }; + + const User = Support.sequelize.define('User', { + embedding: { + type: DataTypes.VECTOR(4) + } + }, { + tableName: 'user' + }); + + testsql({ + model: User, + attributes: ['embedding'], + order: [ + current.fn('VECTOR_DISTANCE', 'embedding', queryVector) + ], + limit: 5 + }, { + oracle: 'SELECT "embedding" FROM "user" "User" ORDER BY VECTOR_DISTANCE("embedding", VECTOR(\'[1,2,3,4]\')) OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY;' + }); + }); + }); +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index df0630d12ab0..c8eb408f3804 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7047,10 +7047,10 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" -oracledb@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/oracledb/-/oracledb-5.5.0.tgz#0cf9af5d0c0815f74849ae9ed56aee823514d71b" - integrity sha512-i5cPvMENpZP8nnqptB6l0pjiOyySj1IISkbM4Hr3yZEDdANo2eezarwZb9NQ8fTh5pRjmgpZdSyIbnn9N3AENw== +oracledb@6.6.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/oracledb/-/oracledb-6.6.0.tgz#bb40adbe81a84a1e544c48af9f120c61f030e936" + integrity sha512-T3dx+o3j+tVN53wQyr4yGTmoPHLy+a2V8yb1T2PmWrsj3ZlSt2Yu1BgV2yTDqnmBZYpRi/I3yJXRCOHHD7PiyA== ordered-read-streams@^1.0.0: version "1.0.1"