@oracle/langchain-oracledb 0.1.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,12 +3,24 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.OracleVS = exports.dropTablePurge = exports.createIndex = exports.createTable = exports.DistanceStrategy = exports.generateWhereClause = void 0;
6
+ exports.OracleVS = exports.dropTablePurge = exports.createIndex = exports.createTable = exports.DistanceStrategy = exports.VectorElementFormat = exports.VectorType = exports.generateWhereClause = exports.isLangChainOracleError = exports.OracleVSErrorCode = exports.OracleVSError = exports.ErrorCode = exports.LangChainOracleError = void 0;
7
+ const node_crypto_1 = __importDefault(require("node:crypto"));
7
8
  const oracledb_1 = __importDefault(require("oracledb"));
8
- const crypto_1 = require("crypto");
9
9
  const vectorstores_1 = require("@langchain/core/vectorstores");
10
10
  const documents_1 = require("@langchain/core/documents");
11
11
  const math_1 = require("@langchain/core/utils/math");
12
+ const errors_js_1 = require("./errors.cjs");
13
+ Object.defineProperty(exports, "LangChainOracleError", { enumerable: true, get: function () { return errors_js_1.LangChainOracleError; } });
14
+ Object.defineProperty(exports, "OracleVSError", { enumerable: true, get: function () { return errors_js_1.LangChainOracleError; } });
15
+ Object.defineProperty(exports, "ErrorCode", { enumerable: true, get: function () { return errors_js_1.ErrorCode; } });
16
+ Object.defineProperty(exports, "OracleVSErrorCode", { enumerable: true, get: function () { return errors_js_1.ErrorCode; } });
17
+ Object.defineProperty(exports, "isLangChainOracleError", { enumerable: true, get: function () { return errors_js_1.isLangChainOracleError; } });
18
+ function validateMetadataKey(column) {
19
+ const pattern = /^[a-zA-Z0-9_.[\],\s*]*$/;
20
+ if (!column || !pattern.test(column)) {
21
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.FILTER_INVALID_METADATA_KEY, column);
22
+ }
23
+ }
12
24
  function generateWhereClause(dbFilter, bindValues) {
13
25
  // Handle $and
14
26
  if ("$and" in dbFilter && Array.isArray(dbFilter.$and)) {
@@ -44,6 +56,7 @@ function generateWhereClause(dbFilter, bindValues) {
44
56
  }
45
57
  exports.generateWhereClause = generateWhereClause;
46
58
  function generateOperatorCondition(column, operator, value, bindValues) {
59
+ validateMetadataKey(column);
47
60
  switch (operator) {
48
61
  case "$eq":
49
62
  return jsonCompare(column, "=", value, bindValues);
@@ -58,20 +71,23 @@ function generateOperatorCondition(column, operator, value, bindValues) {
58
71
  case "$gte":
59
72
  return jsonCompare(column, ">=", value, bindValues);
60
73
  case "$in": {
61
- if (!Array.isArray(value))
62
- throw new Error("$in requires array");
74
+ if (!Array.isArray(value)) {
75
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.FILTER_INVALID_VALUE, "$in requires array");
76
+ }
63
77
  const inClauses = value.map((v) => jsonCompare(column, "=", v, bindValues));
64
78
  return `(${inClauses.join(" OR ")})`;
65
79
  }
66
80
  case "$nin": {
67
- if (!Array.isArray(value))
68
- throw new Error("$nin requires array");
81
+ if (!Array.isArray(value)) {
82
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.FILTER_INVALID_VALUE, "$nin requires array");
83
+ }
69
84
  const ninClauses = value.map((v) => jsonCompare(column, "!=", v, bindValues));
70
85
  return `(${ninClauses.join(" AND ")})`;
71
86
  }
72
87
  case "$between": {
73
- if (!Array.isArray(value) || value.length !== 2)
74
- throw new Error("$between requires [low, high]");
88
+ if (!Array.isArray(value) || value.length !== 2) {
89
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.FILTER_INVALID_VALUE, "$between requires [low, high]");
90
+ }
75
91
  const [low, high] = value;
76
92
  bindValues.push(low, high);
77
93
  const posLow = bindValues.length - 1;
@@ -86,7 +102,7 @@ function generateOperatorCondition(column, operator, value, bindValues) {
86
102
  return `NOT JSON_EXISTS(metadata, '$.${column}')`;
87
103
  }
88
104
  default:
89
- throw new Error(`Unsupported operator: ${operator}`);
105
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.FILTER_UNSUPPORTED_OPERATOR, operator);
90
106
  }
91
107
  }
92
108
  // Helper: build JSON_EXISTS clause for comparison
@@ -97,6 +113,17 @@ function jsonCompare(column, op, value, bindValues) {
97
113
  const operator = op === "=" ? "==" : op; // Oracle requires '==' for equality
98
114
  return `JSON_EXISTS(metadata, '$.${column}?(@ ${operator} $${alias})' PASSING :${pos} AS "${alias}")`;
99
115
  }
116
+ exports.VectorType = {
117
+ DENSE: "DENSE",
118
+ SPARSE: "SPARSE",
119
+ };
120
+ exports.VectorElementFormat = {
121
+ INT8: "INT8",
122
+ FLOAT32: "FLOAT32",
123
+ FLOAT64: "FLOAT64",
124
+ BINARY: "BINARY",
125
+ FLEX: "*",
126
+ };
100
127
  exports.DistanceStrategy = {
101
128
  COSINE: "COSINE",
102
129
  DOT_PRODUCT: "DOT",
@@ -106,32 +133,33 @@ exports.DistanceStrategy = {
106
133
  EUCLIDEAN_SQUARED: "EUCLIDEAN_SQUARED",
107
134
  };
108
135
  function handleError(error) {
109
- // Type guard to check if the error is an object and has 'name' and 'message' properties
110
- if (typeof error === "object" &&
136
+ // Preserve LangChainOracleError instances created by this package instead of
137
+ // wrapping them again as generic SYSTEM_ERROR failures.
138
+ if ((0, errors_js_1.isLangChainOracleError)(error)) {
139
+ throw error;
140
+ }
141
+ // Preserve a useful message for both real Error instances and thrown
142
+ // object literals shaped like { message: string }.
143
+ const details = typeof error === "object" &&
111
144
  error !== null &&
112
- "name" in error &&
113
- "message" in error) {
114
- const err = error; // Type assertion based on guarded checks
115
- // Handle specific error types based on the name property
116
- switch (err.name) {
117
- case "RuntimeError":
118
- throw new Error("Database operation failed due to a runtime error.");
119
- case "ValidationError":
120
- throw new Error("Operation failed due to a validation error.");
121
- default:
122
- throw new Error(`An unexpected error occurred during the operation. ${error}`);
123
- }
124
- }
125
- throw new Error(`An unknown and unexpected error occurred. ${error}`);
145
+ "message" in error &&
146
+ typeof error.message === "string"
147
+ ? error.message
148
+ : String(error);
149
+ // Normalize all underlying anomalies into a unified telemetry payload.
150
+ throw (0, errors_js_1.createErrorFromCodeWithCause)(errors_js_1.ErrorCode.SYSTEM_ERROR, error, `An unexpected error occurred during the operation. ${details}`);
126
151
  }
127
152
  function isPool(client) {
128
153
  return "getConnection" in client;
129
154
  }
155
+ function isClientProvider(client) {
156
+ return typeof client === "function";
157
+ }
130
158
  function quoteIdentifier(identifier) {
131
159
  const name = identifier.trim();
132
160
  const validateRegex = /^(?:"[^"]+"|[^".]+)(?:\.(?:"[^"]+"|[^".]+))*$/;
133
161
  if (!validateRegex.test(name)) {
134
- throw new Error(`Identifier name ${identifier} is not valid.`);
162
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VALIDATION_INVALID_IDENTIFIER, identifier);
135
163
  }
136
164
  // extracts parts of the identifier with quoted and unquoted.
137
165
  const matchRegex = /"([^"]+)"|([^".]+)/g;
@@ -142,10 +170,78 @@ function quoteIdentifier(identifier) {
142
170
  const quotedParts = groups.map((g) => `"${g}"`);
143
171
  return quotedParts.join(".");
144
172
  }
145
- async function createTable(connection, tableName, embeddingDim) {
173
+ const isFloat32Array = (value) =>
174
+ // eslint-disable-next-line no-instanceof/no-instanceof
175
+ value instanceof Float32Array;
176
+ const isFloat64Array = (value) =>
177
+ // eslint-disable-next-line no-instanceof/no-instanceof
178
+ value instanceof Float64Array;
179
+ const isInt8Array = (value) =>
180
+ // eslint-disable-next-line no-instanceof/no-instanceof
181
+ value instanceof Int8Array;
182
+ const isUint8Array = (value) =>
183
+ // eslint-disable-next-line no-instanceof/no-instanceof
184
+ value instanceof Uint8Array;
185
+ const isSparseVector = (value) =>
186
+ // eslint-disable-next-line no-instanceof/no-instanceof
187
+ value instanceof oracledb_1.default.SparseVector;
188
+ function normalizeVectorTypeValue(value) {
189
+ const normalized = value?.toUpperCase();
190
+ if (normalized === exports.VectorType.DENSE || normalized === exports.VectorType.SPARSE) {
191
+ return normalized;
192
+ }
193
+ return exports.VectorType.DENSE;
194
+ }
195
+ function normalizeVectorFormat(value, vectorType) {
196
+ if (!value) {
197
+ return exports.VectorElementFormat.FLOAT32;
198
+ }
199
+ const normalizedValue = value === exports.VectorElementFormat.FLEX
200
+ ? exports.VectorElementFormat.FLEX
201
+ : value.toUpperCase();
202
+ const validFormats = Object.values(exports.VectorElementFormat);
203
+ const format = validFormats.includes(normalizedValue)
204
+ ? normalizedValue
205
+ : exports.VectorElementFormat.FLOAT32;
206
+ if (vectorType === exports.VectorType.SPARSE && format === exports.VectorElementFormat.BINARY) {
207
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VECTOR_INVALID_CONFIGURATION, "BINARY format is not supported for SPARSE vectors.");
208
+ }
209
+ return format;
210
+ }
211
+ function buildVectorColumnDefinition(embeddingDim, customization) {
212
+ if (embeddingDim === undefined || embeddingDim === null) {
213
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VECTOR_INVALID_CONFIGURATION, "Embedding dimension is required to create the vector column.");
214
+ }
215
+ if (!Number.isInteger(embeddingDim) || embeddingDim <= 0) {
216
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VECTOR_INVALID_CONFIGURATION, "Embedding dimension must be a positive integer.");
217
+ }
218
+ const vectorType = normalizeVectorTypeValue(customization?.vectorType);
219
+ if (vectorType === exports.VectorType.SPARSE && customization?.format === undefined) {
220
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VECTOR_INVALID_CONFIGURATION, "Sparse vector type requires a vector format to be specified.");
221
+ }
222
+ const format = normalizeVectorFormat(customization?.format, vectorType);
223
+ if (format === exports.VectorElementFormat.BINARY && embeddingDim % 8 !== 0) {
224
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VECTOR_INVALID_CONFIGURATION, "BINARY vector format requires dimensions to be a multiple of 8.");
225
+ }
226
+ const dimensionSegment = String(embeddingDim);
227
+ const formatSegment = format ?? exports.VectorElementFormat.FLOAT32;
228
+ if (vectorType === exports.VectorType.SPARSE) {
229
+ return `VECTOR(${dimensionSegment}, ${formatSegment}, SPARSE)`;
230
+ }
231
+ return `VECTOR(${dimensionSegment}, ${formatSegment})`;
232
+ }
233
+ function escapeCommentText(comment) {
234
+ return comment.replace(/'/g, "''");
235
+ }
236
+ function escapePlSqlLiteral(sql) {
237
+ return sql.replace(/'/g, "''");
238
+ }
239
+ async function createTable(connection, tableName, embeddingDim, customization) {
240
+ const tableIdentifier = quoteIdentifier(tableName);
146
241
  const colsDict = {
147
242
  id: "RAW(16) DEFAULT SYS_GUID() PRIMARY KEY",
148
- embedding: `vector(${embeddingDim}, FLOAT32)`,
243
+ external_id: `VARCHAR2(36) UNIQUE`,
244
+ embedding: buildVectorColumnDefinition(embeddingDim, customization),
149
245
  text: "CLOB",
150
246
  metadata: "JSON",
151
247
  };
@@ -153,11 +249,33 @@ async function createTable(connection, tableName, embeddingDim) {
153
249
  const ddlBody = Object.entries(colsDict)
154
250
  .map(([colName, colType]) => `${colName} ${colType}`)
155
251
  .join(", ");
156
- const ddl = `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(tableName)}
252
+ const createTableSql = `CREATE TABLE IF NOT EXISTS ${tableIdentifier}
157
253
  (
158
254
  ${ddlBody}
159
255
  )`;
160
- await connection.execute(ddl);
256
+ const statements = [
257
+ `EXECUTE IMMEDIATE '${escapePlSqlLiteral(createTableSql)}';`,
258
+ ];
259
+ const tableDescription = customization?.description?.trim();
260
+ if (tableDescription) {
261
+ const escapedDescription = escapeCommentText(tableDescription);
262
+ statements.push(`EXECUTE IMMEDIATE 'COMMENT ON TABLE ${tableIdentifier} IS ''${escapedDescription}''';`);
263
+ }
264
+ if (customization?.annotations) {
265
+ for (const [columnName, note] of Object.entries(customization.annotations)) {
266
+ const trimmedNote = note?.trim();
267
+ if (!trimmedNote)
268
+ continue;
269
+ const columnKey = columnName.trim().toLowerCase();
270
+ if (!(columnKey in colsDict))
271
+ continue;
272
+ const normalizedColumnIdentifier = `${tableIdentifier}.${quoteIdentifier(columnKey.toUpperCase())}`;
273
+ const escapedNote = escapeCommentText(trimmedNote);
274
+ statements.push(`EXECUTE IMMEDIATE 'COMMENT ON COLUMN ${normalizedColumnIdentifier} IS ''${escapedNote}''';`);
275
+ }
276
+ }
277
+ const block = `BEGIN\n ${statements.join("\n ")}\nEND;`;
278
+ await connection.execute(block);
161
279
  }
162
280
  catch (error) {
163
281
  handleError(error);
@@ -165,11 +283,63 @@ async function createTable(connection, tableName, embeddingDim) {
165
283
  }
166
284
  exports.createTable = createTable;
167
285
  function _getIndexName(baseName) {
168
- const uniqueId = crypto.randomUUID().replace(/-/g, "");
286
+ const uniqueId = node_crypto_1.default.randomUUID().replace(/-/g, "");
169
287
  return `${baseName}_${uniqueId}`;
170
288
  }
289
+ // Packs an array of 0/1 numbers into a big-endian Uint8Array for BINARY VECTOR storage.
290
+ function packBinaryVector(bits) {
291
+ const byteLength = Math.ceil(bits.length / 8);
292
+ const buffer = new Uint8Array(byteLength);
293
+ for (let i = 0; i < bits.length; i += 1) {
294
+ const bit = bits[i] > 0 ? 1 : 0;
295
+ if (bit === 1) {
296
+ const byteIndex = Math.floor(i / 8);
297
+ const bitOffset = 7 - (i % 8);
298
+ buffer[byteIndex] |= 1 << bitOffset;
299
+ }
300
+ }
301
+ return buffer;
302
+ }
303
+ // Converts a dense numeric vector into the typed array that matches the configured element format.
304
+ function convertDenseVectorForFormat(values, format) {
305
+ switch (format) {
306
+ case exports.VectorElementFormat.FLOAT32:
307
+ case undefined:
308
+ return new Float32Array(values);
309
+ case exports.VectorElementFormat.FLOAT64:
310
+ return new Float64Array(values);
311
+ case exports.VectorElementFormat.INT8: {
312
+ const clamped = new Int8Array(values.length);
313
+ for (let i = 0; i < values.length; i += 1) {
314
+ const rounded = Math.round(values[i]);
315
+ if (rounded < -128 || rounded > 127) {
316
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VECTOR_INVALID_VALUE, "INT8 vector values must be within [-128, 127].");
317
+ }
318
+ clamped[i] = rounded;
319
+ }
320
+ return clamped;
321
+ }
322
+ case exports.VectorElementFormat.BINARY:
323
+ return packBinaryVector(values);
324
+ default:
325
+ return new Float64Array(values);
326
+ }
327
+ }
328
+ function unpackBinaryVector(bytesLike, dimension) {
329
+ const bytes = isUint8Array(bytesLike)
330
+ ? bytesLike
331
+ : Uint8Array.from(bytesLike);
332
+ const result = new Float32Array(dimension);
333
+ for (let i = 0; i < dimension; i += 1) {
334
+ const byteIndex = Math.floor(i / 8);
335
+ const bitOffset = 7 - (i % 8);
336
+ const byte = bytes[byteIndex] ?? 0;
337
+ result[i] = (byte >> bitOffset) & 1;
338
+ }
339
+ return result;
340
+ }
171
341
  async function createIndex(client, vectorStore, params) {
172
- const idxType = params?.idxType || "HNSW";
342
+ const idxType = (params?.idxType ?? "HNSW").toUpperCase();
173
343
  if (idxType === "IVF") {
174
344
  await createIVFIndex(client, vectorStore, params);
175
345
  }
@@ -178,122 +348,48 @@ async function createIndex(client, vectorStore, params) {
178
348
  }
179
349
  }
180
350
  exports.createIndex = createIndex;
181
- async function createHNSWIndex(connection, oraclevs, params) {
351
+ async function createHNSWIndex(connection, oraclevs, params = {}) {
182
352
  try {
183
- const defaults = {
184
- idxName: "HNSW",
185
- idxType: "HNSW",
186
- neighbors: 32,
187
- efConstruction: 200,
188
- accuracy: 90,
189
- parallel: 8,
190
- };
191
- // if params then copy params to config
192
- const config = params
193
- ? { ...params }
194
- : { ...defaults };
195
- // Ensure compulsory parts are included
196
- const compulsoryKeys = ["idxName", "parallel"];
197
- for (const key of compulsoryKeys) {
198
- if (!(key in config)) {
199
- if (key === "idxName") {
200
- config[key] = _getIndexName(defaults[key]);
201
- }
202
- else {
203
- config[key] = defaults[key];
204
- }
205
- }
206
- }
207
- // Validate keys in config against defaults
208
- for (const key of Object.keys(config)) {
209
- if (!(key in defaults)) {
210
- throw new Error(`Invalid parameter: ${key}`);
211
- }
212
- }
213
- const { idxName } = config;
214
- const baseSql = `CREATE VECTOR INDEX IF NOT EXISTS ${quoteIdentifier(idxName)}
215
- ON ${oraclevs.tableName}(embedding)
216
- ORGANIZATION INMEMORY NEIGHBOR GRAPH`;
217
- const accuracyPart = config.accuracy
218
- ? ` WITH TARGET ACCURACY ${config.accuracy}`
219
- : "";
220
- const distancePart = ` DISTANCE ${oraclevs.distanceStrategy}`;
221
- let parametersPart = "";
222
- if ("neighbors" in config && "efConstruction" in config) {
223
- parametersPart = ` parameters (type ${config.idxType},
224
- neighbors ${config.neighbors},
225
- efConstruction ${config.efConstruction})`;
226
- }
227
- else if ("neighbors" in config && !("efConstruction" in config)) {
228
- config.efConstruction = defaults.efConstruction;
229
- parametersPart = ` parameters (type ${config.idxType},
230
- neighbors ${config.neighbors},
231
- efConstruction ${config.efConstruction})`;
232
- }
233
- else if (!("neighbors" in config) && "efConstruction" in config) {
234
- config.neighbors = defaults.neighbors;
235
- parametersPart = ` parameters (type ${config.idxType},
236
- neighbors ${config.neighbors},
237
- efConstruction ${config.efConstruction})`;
238
- }
239
- const parallelPart = ` PARALLEL ${config.parallel}`;
240
- const ddl = baseSql + accuracyPart + distancePart + parametersPart + parallelPart;
353
+ const { idxName = _getIndexName("HNSW"), idxType = "HNSW", neighbors = 32, efConstruction = 200, accuracy = 90, parallel = 8, ...extra } = params;
354
+ const invalidKeys = Object.keys(extra);
355
+ if (invalidKeys.length > 0) {
356
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VECTOR_INVALID_INDEX_PARAMETERS, invalidKeys);
357
+ }
358
+ const ddl = `
359
+ CREATE VECTOR INDEX IF NOT EXISTS ${quoteIdentifier(idxName)}
360
+ ON ${oraclevs.tableName}(embedding)
361
+ ORGANIZATION INMEMORY NEIGHBOR GRAPH
362
+ WITH TARGET ACCURACY ${accuracy}
363
+ DISTANCE ${oraclevs.distanceStrategy}
364
+ PARAMETERS (type ${idxType}, neighbors ${neighbors}, efConstruction ${efConstruction})
365
+ PARALLEL ${parallel}
366
+ `
367
+ .trim()
368
+ .replace(/\s+/g, " ");
241
369
  await connection.execute(ddl);
242
370
  }
243
371
  catch (error) {
244
372
  handleError(error);
245
373
  }
246
374
  }
247
- async function createIVFIndex(connection, oraclevs, params) {
375
+ async function createIVFIndex(connection, oraclevs, params = {}) {
248
376
  try {
249
- const defaults = {
250
- idxName: "IVF",
251
- idxType: "IVF",
252
- neighborPart: 32,
253
- accuracy: 90,
254
- parallel: 8,
255
- };
256
- // Combine defaults with any provided params. Note: params could contain keys not explicitly declared in IndexConfig
257
- const config = params
258
- ? { ...params }
259
- : { ...defaults };
260
- // Ensure compulsory parts are included
261
- const compulsoryKeys = ["idxName", "parallel"];
262
- for (const key of compulsoryKeys) {
263
- if (!(key in config)) {
264
- if (key === "idxName") {
265
- config[key] = _getIndexName(defaults[key]);
266
- }
267
- else {
268
- config[key] = defaults[key];
269
- }
270
- }
271
- }
272
- // Validate keys in config against defaults
273
- for (const key of Object.keys(config)) {
274
- if (!(key in defaults)) {
275
- throw new Error(`Invalid parameter: ${key}`);
276
- }
277
- }
278
- // Base SQL statement
279
- const { idxName } = config;
280
- const baseSql = `CREATE VECTOR INDEX IF NOT EXISTS ${quoteIdentifier(idxName)}
281
- ON ${oraclevs.tableName}(embedding)
282
- ORGANIZATION NEIGHBOR PARTITIONS`;
283
- // Optional parts depending on parameters
284
- const accuracyPart = config.accuracy
285
- ? ` WITH TARGET ACCURACY ${config.accuracy}`
286
- : "";
287
- const distancePart = ` DISTANCE ${oraclevs.distanceStrategy}`;
288
- let parametersPart = "";
289
- if ("idxType" in config && "neighborPart" in config) {
290
- parametersPart = ` PARAMETERS (type ${config.idxType},
291
- neighbor partitions ${config.neighborPart})`;
292
- }
293
- // Always included part for parallel - assuming parallel is compulsory and always included
294
- const parallelPart = ` PARALLEL ${config.parallel}`;
295
- // Combine all parts
296
- const ddl = baseSql + accuracyPart + distancePart + parametersPart + parallelPart;
377
+ const { idxName = _getIndexName("IVF"), idxType = "IVF", neighborPart = 32, accuracy = 90, parallel = 8, ...extra } = params;
378
+ const invalidKeys = Object.keys(extra);
379
+ if (invalidKeys.length > 0) {
380
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VECTOR_INVALID_INDEX_PARAMETERS, invalidKeys);
381
+ }
382
+ const ddl = `
383
+ CREATE VECTOR INDEX IF NOT EXISTS ${quoteIdentifier(idxName)}
384
+ ON ${oraclevs.tableName}(embedding)
385
+ ORGANIZATION NEIGHBOR PARTITIONS
386
+ WITH TARGET ACCURACY ${accuracy}
387
+ DISTANCE ${oraclevs.distanceStrategy}
388
+ PARAMETERS (type ${idxType}, neighbor partitions ${neighborPart})
389
+ PARALLEL ${parallel}
390
+ `
391
+ .trim()
392
+ .replace(/\s+/g, " ");
297
393
  await connection.execute(ddl);
298
394
  }
299
395
  catch (error) {
@@ -322,6 +418,14 @@ class OracleVS extends vectorstores_1.VectorStore {
322
418
  writable: true,
323
419
  value: void 0
324
420
  });
421
+ // Tracks connections borrowed from a pool so retConnection() can return them
422
+ // without closing direct connections owned by the caller.
423
+ Object.defineProperty(this, "poolConnections", {
424
+ enumerable: true,
425
+ configurable: true,
426
+ writable: true,
427
+ value: new WeakSet()
428
+ });
325
429
  Object.defineProperty(this, "embeddingDimension", {
326
430
  enumerable: true,
327
431
  configurable: true,
@@ -346,6 +450,30 @@ class OracleVS extends vectorstores_1.VectorStore {
346
450
  writable: true,
347
451
  value: void 0
348
452
  });
453
+ Object.defineProperty(this, "description", {
454
+ enumerable: true,
455
+ configurable: true,
456
+ writable: true,
457
+ value: void 0
458
+ });
459
+ Object.defineProperty(this, "annotations", {
460
+ enumerable: true,
461
+ configurable: true,
462
+ writable: true,
463
+ value: void 0
464
+ });
465
+ Object.defineProperty(this, "vectorType", {
466
+ enumerable: true,
467
+ configurable: true,
468
+ writable: true,
469
+ value: void 0
470
+ });
471
+ Object.defineProperty(this, "vectorFormat", {
472
+ enumerable: true,
473
+ configurable: true,
474
+ writable: true,
475
+ value: void 0
476
+ });
349
477
  Object.defineProperty(this, "query", {
350
478
  enumerable: true,
351
479
  configurable: true,
@@ -359,11 +487,120 @@ class OracleVS extends vectorstores_1.VectorStore {
359
487
  dbConfig.distanceStrategy ?? this.distanceStrategy;
360
488
  this.query = dbConfig.query;
361
489
  this.filter = dbConfig.filter;
490
+ this.description = dbConfig.description;
491
+ this.annotations = dbConfig.annotations
492
+ ? { ...dbConfig.annotations }
493
+ : undefined;
494
+ this.vectorType = dbConfig.vectorType;
495
+ this.vectorFormat = dbConfig.format;
362
496
  }
363
497
  catch (error) {
364
498
  handleError(error);
365
499
  }
366
500
  }
501
+ getActiveVectorType() {
502
+ return this.vectorType ? normalizeVectorTypeValue(this.vectorType) : exports.VectorType.DENSE;
503
+ }
504
+ ensureEmbeddingDimension() {
505
+ if (this.embeddingDimension === undefined || this.embeddingDimension === null) {
506
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.STATE_INVALID, "Embedding dimension is not initialized for this vector store.");
507
+ }
508
+ return this.embeddingDimension;
509
+ }
510
+ prepareVectorForStorage(vector) {
511
+ if (this.embeddingDimension === undefined || this.embeddingDimension === null) {
512
+ this.embeddingDimension = vector.length;
513
+ }
514
+ const dimension = this.ensureEmbeddingDimension();
515
+ const vectorType = this.getActiveVectorType();
516
+ const format = this.vectorFormat
517
+ ? normalizeVectorFormat(this.vectorFormat, vectorType)
518
+ : exports.VectorElementFormat.FLOAT32;
519
+ if (vectorType === exports.VectorType.SPARSE) {
520
+ if (vector.length !== dimension) {
521
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VECTOR_INVALID_VALUE, "Sparse vectors must supply full-dimension arrays for conversion.");
522
+ }
523
+ let sparseInput = vector;
524
+ if (format === exports.VectorElementFormat.INT8) {
525
+ const clamped = new Int8Array(vector.length);
526
+ for (let i = 0; i < vector.length; i += 1) {
527
+ const rounded = Math.round(vector[i]);
528
+ if (rounded < -128 || rounded > 127) {
529
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VECTOR_INVALID_VALUE, "INT8 sparse vector values must be within [-128, 127].");
530
+ }
531
+ clamped[i] = rounded;
532
+ }
533
+ sparseInput = clamped;
534
+ }
535
+ else if (format === exports.VectorElementFormat.FLOAT64) {
536
+ sparseInput = new Float64Array(vector);
537
+ }
538
+ else if (format === exports.VectorElementFormat.FLOAT32) {
539
+ sparseInput = new Float32Array(vector);
540
+ }
541
+ return new oracledb_1.default.SparseVector(sparseInput);
542
+ }
543
+ if (vector.length !== dimension) {
544
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VECTOR_INVALID_VALUE, "Vector length does not match the embedding dimension.");
545
+ }
546
+ return convertDenseVectorForFormat(vector, format);
547
+ }
548
+ prepareQueryVector(vector) {
549
+ return this.prepareVectorForStorage(vector);
550
+ }
551
+ // Preserves the native driver representation, only expanding sparse vectors to dense form.
552
+ // Preserves the native driver representation, only expanding sparse vectors to dense form.
553
+ normalizeReturnedEmbedding(value) {
554
+ if (isSparseVector(value)) {
555
+ const dense = value.dense?.();
556
+ if (!dense) {
557
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VECTOR_UNSUPPORTED_REPRESENTATION, "Unable to expand sparse vector to dense representation.");
558
+ }
559
+ if (isFloat32Array(dense)) {
560
+ return dense;
561
+ }
562
+ if (isFloat64Array(dense)) {
563
+ return new Float32Array(dense);
564
+ }
565
+ if (isInt8Array(dense)) {
566
+ return Float32Array.from(dense);
567
+ }
568
+ if (Array.isArray(dense)) {
569
+ return Float32Array.from(dense);
570
+ }
571
+ return Float32Array.from(dense);
572
+ }
573
+ if (isFloat32Array(value) || isFloat64Array(value) || isInt8Array(value) || isUint8Array(value)) {
574
+ return value;
575
+ }
576
+ if (Array.isArray(value)) {
577
+ return value;
578
+ }
579
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VECTOR_UNSUPPORTED_REPRESENTATION, "Received unsupported vector representation from the database.");
580
+ }
581
+ // Normalizes any supported vector representation into Float32Array so downstream math
582
+ // (MMR, distance calculations) can assume a consistent element type without forcing
583
+ // callers to give up the native shape returned by node-oracledb.
584
+ coerceEmbeddingToFloat32(value) {
585
+ if (isFloat32Array(value)) {
586
+ return value;
587
+ }
588
+ if (isFloat64Array(value)) {
589
+ return new Float32Array(value);
590
+ }
591
+ if (isInt8Array(value)) {
592
+ return Float32Array.from(value);
593
+ }
594
+ if (isUint8Array(value)) {
595
+ const byteLength = value.length;
596
+ const dimension = this.embeddingDimension ?? byteLength * 8;
597
+ return unpackBinaryVector(value, dimension);
598
+ }
599
+ if (Array.isArray(value)) {
600
+ return Float32Array.from(value);
601
+ }
602
+ return this.coerceEmbeddingToFloat32(this.normalizeReturnedEmbedding(value));
603
+ }
367
604
  async getEmbeddingDimension(query) {
368
605
  const embeddingVector = await this.embeddings.embedQuery(query);
369
606
  return embeddingVector.length;
@@ -373,7 +610,12 @@ class OracleVS extends vectorstores_1.VectorStore {
373
610
  try {
374
611
  this.embeddingDimension = await this.getEmbeddingDimension(this.query);
375
612
  connection = await this.getConnection();
376
- await createTable(connection, this.tableName, this.embeddingDimension);
613
+ await createTable(connection, this.tableName, this.embeddingDimension, {
614
+ description: this.description,
615
+ annotations: this.annotations,
616
+ vectorType: this.vectorType,
617
+ format: this.vectorFormat
618
+ });
377
619
  }
378
620
  catch (error) {
379
621
  handleError(error);
@@ -383,12 +625,20 @@ class OracleVS extends vectorstores_1.VectorStore {
383
625
  await this.retConnection(connection);
384
626
  }
385
627
  }
628
+ async resolveClient() {
629
+ return isClientProvider(this.client)
630
+ ? await this.client()
631
+ : this.client;
632
+ }
386
633
  async getConnection() {
387
634
  try {
388
- if (isPool(this.client)) {
389
- return await this.client.getConnection();
635
+ const client = await this.resolveClient();
636
+ if (isPool(client)) {
637
+ const connection = await client.getConnection();
638
+ this.poolConnections.add(connection);
639
+ return connection;
390
640
  }
391
- return this.client;
641
+ return client;
392
642
  }
393
643
  catch (error) {
394
644
  handleError(error);
@@ -397,9 +647,10 @@ class OracleVS extends vectorstores_1.VectorStore {
397
647
  // Close connection or return it to the pool
398
648
  async retConnection(connection) {
399
649
  try {
400
- // If the client is a pool, close the connection (return it to the pool)
401
- if (isPool(this.client)) {
650
+ // If this public connection came from any pool, close it to return it to the pool.
651
+ if (this.poolConnections.has(connection)) {
402
652
  await connection.close();
653
+ this.poolConnections.delete(connection);
403
654
  }
404
655
  }
405
656
  catch (error) {
@@ -412,73 +663,79 @@ class OracleVS extends vectorstores_1.VectorStore {
412
663
  * @param vectors The vectors to add.
413
664
  * @param documents The documents associated with the vectors.
414
665
  * @param options
666
+ * ** Add { mutateOnDuplicate?: boolean } to do upsert
415
667
  * @returns Promise that resolves when the vectors have been added.
416
668
  */
417
669
  async addVectors(vectors, documents, options) {
418
670
  if (vectors.length === 0) {
419
- throw new Error("Vectors input null. Nothing to add...");
671
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VALIDATION_INVALID_INPUT, "Vectors input null. Nothing to add...");
420
672
  }
421
- const ids = options?.ids;
673
+ const inputIds = options?.ids;
422
674
  let connection = null;
423
675
  try {
424
676
  // Ensure there are IDs for all documents
425
- if (ids !== undefined && ids.length !== vectors.length) {
426
- throw new Error("The number of ids must match the number of vectors provided.");
677
+ if (inputIds !== undefined && inputIds.length !== vectors.length) {
678
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VALIDATION_INVALID_INPUT, "The number of ids must match the number of vectors provided.");
427
679
  }
428
- connection = await this.getConnection();
680
+ const finalIds = [];
429
681
  const binds = [];
430
682
  for (let index = 0; index < documents.length; index += 1) {
431
683
  const doc = documents[index];
432
- let processedId;
433
- if (ids) {
434
- processedId = (0, crypto_1.createHash)("sha256")
435
- .update(ids[index])
436
- .digest("hex")
437
- .substring(0, 16)
438
- .toUpperCase();
439
- }
440
- else {
441
- const sourceId = doc.metadata.id ?? crypto.randomUUID();
442
- processedId = (0, crypto_1.createHash)("sha256")
443
- .update(sourceId)
444
- .digest("hex")
445
- .substring(0, 16)
446
- .toUpperCase();
447
- }
448
- const idBuffer = Buffer.from(processedId, "hex");
449
- const bind = {
450
- id: idBuffer,
684
+ // Generate ID if not provided: Priority (Options -> Metadata -> Random)
685
+ const externalId = inputIds?.[index] ?? doc.metadata.id ?? node_crypto_1.default.randomUUID();
686
+ finalIds.push(externalId);
687
+ const preparedEmbedding = this.prepareVectorForStorage(vectors[index]);
688
+ const row = {
689
+ ext_id: externalId,
451
690
  text: doc.pageContent,
452
691
  metadata: doc.metadata,
453
- embedding: new Float32Array(vectors[index]),
692
+ embedding: preparedEmbedding,
454
693
  };
455
- binds.push(bind);
694
+ binds.push(row);
456
695
  }
457
- const sql = `INSERT INTO ${this.tableName} (id, embedding, text, metadata )
458
- VALUES (:id, :embedding, :text, :metadata)`;
459
- const options = {
696
+ const isMutateOnDuplicate = options?.mutateOnDuplicate ?? false; // Default to false for better performance
697
+ const insertSql = `
698
+ INSERT INTO ${this.tableName} (external_id, embedding, text, metadata)
699
+ VALUES (:ext_id, :embedding, :text, :metadata)`;
700
+ const mergeSql = `
701
+ MERGE INTO ${this.tableName} t
702
+ USING (
703
+ SELECT :ext_id as external_id, :embedding as embedding,
704
+ :metadata as metadata, :text as text FROM DUAL
705
+ ) s
706
+ ON (t.external_id = s.external_id)
707
+ WHEN MATCHED THEN
708
+ UPDATE SET
709
+ t.embedding = s.embedding,
710
+ t.metadata = s.metadata,
711
+ t.text = s.text
712
+ WHEN NOT MATCHED THEN
713
+ INSERT (external_id, embedding, metadata, text)
714
+ VALUES (s.external_id, s.embedding, s.metadata, s.text)`;
715
+ const sql = isMutateOnDuplicate ? mergeSql : insertSql;
716
+ const executeOptions = {
460
717
  bindDefs: {
461
- id: { type: oracledb_1.default.BUFFER, maxSize: 24 },
718
+ ext_id: { type: oracledb_1.default.STRING, maxSize: 255 },
462
719
  text: { type: oracledb_1.default.STRING, maxSize: 10000000 },
463
720
  metadata: { type: oracledb_1.default.DB_TYPE_JSON },
464
721
  embedding: { type: oracledb_1.default.DB_TYPE_VECTOR },
465
722
  },
466
723
  autoCommit: false,
467
724
  };
468
- await connection.executeMany(sql, binds, options);
725
+ connection = await this.getConnection();
726
+ await connection.executeMany(sql, binds, executeOptions);
469
727
  // Commit once all inserts are queued up
470
728
  await connection.commit();
471
- console.log("All documents have been inserted and committed.");
729
+ return finalIds;
472
730
  }
473
731
  catch (error) {
474
- handleError(error);
732
+ return handleError(error);
475
733
  }
476
734
  finally {
477
735
  if (connection) {
478
736
  await this.retConnection(connection);
479
737
  }
480
738
  }
481
- return ids;
482
739
  }
483
740
  async addDocuments(documents, options) {
484
741
  const texts = documents.map(({ pageContent }) => pageContent);
@@ -495,16 +752,23 @@ class OracleVS extends vectorstores_1.VectorStore {
495
752
  const docsScoresAndEmbeddings = [];
496
753
  let connection = null;
497
754
  try {
498
- const convertedEmbedding = new Float32Array(query);
499
- const bindValues = [convertedEmbedding];
755
+ const bindValues = [this.prepareQueryVector(query)];
756
+ const hasFilter = !!filter && Object.keys(filter).length > 0;
757
+ // Keep the vector index hint for unfiltered searches. When a JSON filter
758
+ // is present, forcing VECTOR_INDEX_TRANSFORM can change result semantics
759
+ // by doing approximate top-k before the filter is applied.
760
+ const selectClause = hasFilter
761
+ ? "SELECT"
762
+ : `SELECT /*+ VECTOR_INDEX_TRANSFORM(${this.tableName}) */`;
500
763
  let sqlQuery = `
501
- SELECT id,
764
+ ${selectClause}
765
+ external_id,
502
766
  text,
503
767
  metadata,
504
768
  vector_distance(embedding, :1, ${this.distanceStrategy}) as distance,
505
769
  embedding
506
770
  FROM ${this.tableName} `;
507
- if (filter && Object.keys(filter).length > 0) {
771
+ if (hasFilter) {
508
772
  sqlQuery += ` WHERE ${generateWhereClause(filter, bindValues)}`;
509
773
  }
510
774
  bindValues.push(k);
@@ -523,23 +787,19 @@ class OracleVS extends vectorstores_1.VectorStore {
523
787
  const text = row[1];
524
788
  const metadata = row[2];
525
789
  const distance = row[3];
526
- const embedding = row[4];
790
+ const embedding = this.normalizeReturnedEmbedding(row[4]);
527
791
  const document = new documents_1.Document({
528
792
  pageContent: text || "",
529
793
  metadata: metadata || {},
530
- id: row[0].toString("hex"),
794
+ id: row[0],
531
795
  });
532
796
  docsScoresAndEmbeddings.push([document, distance, embedding]);
533
797
  }
534
798
  }
535
- else {
536
- // Throw an exception if no rows are found
537
- throw new Error("No rows found.");
538
- }
539
799
  }
540
800
  finally {
541
801
  if (connection) {
542
- await connection.close();
802
+ await this.retConnection(connection);
543
803
  }
544
804
  }
545
805
  return docsScoresAndEmbeddings;
@@ -586,10 +846,8 @@ class OracleVS extends vectorstores_1.VectorStore {
586
846
  // Split documents, scores, and embeddings
587
847
  const documents = docsScoresEmbeddings.map(([document]) => document);
588
848
  const scores = docsScoresEmbeddings.map(([, score]) => score);
589
- const embeddings = docsScoresEmbeddings.map(([, , embedding]) => new Float32Array(embedding));
590
- // Convert all embeddings to Float32Array for consistency
591
- const consistentEmbeddings = embeddings.map((embedding) => Array.from(embedding));
592
- const queryEmbedding = Array.from(embedding);
849
+ const consistentEmbeddings = docsScoresEmbeddings.map(([, , emb]) => Array.from(this.coerceEmbeddingToFloat32(emb)));
850
+ const queryEmbedding = Array.from(this.coerceEmbeddingToFloat32(embedding));
593
851
  // Ensure lambdaMult has a default value if not provided
594
852
  const lambdaMult = options.lambda ?? 0.5;
595
853
  const mmrSelectedIndices = (0, math_1.maximalMarginalRelevance)(queryEmbedding, consistentEmbeddings, lambdaMult, options.k);
@@ -622,21 +880,23 @@ class OracleVS extends vectorstores_1.VectorStore {
622
880
  handleError(error);
623
881
  }
624
882
  finally {
625
- if (connection)
626
- await connection.close();
883
+ if (connection) {
884
+ await this.retConnection(connection);
885
+ }
627
886
  }
628
887
  }
629
- static async fromDocuments(documents, embeddings, dbConfig) {
888
+ static async fromDocuments(documents, embeddings, dbConfig, options) {
630
889
  const { client } = dbConfig;
631
- if (!client)
632
- throw new Error("client parameter is required...");
890
+ if (!client) {
891
+ (0, errors_js_1.throwError)(errors_js_1.ErrorCode.VALIDATION_MISSING_REQUIRED_PARAMETER, "client");
892
+ }
633
893
  try {
634
894
  const vss = new OracleVS(embeddings, dbConfig);
635
895
  await vss.initialize();
636
896
  const texts = documents.map(({ pageContent }) => pageContent);
637
897
  const vectors = await embeddings.embedDocuments(texts);
638
898
  // Assuming a method exists to handle adding texts and metadata appropriately
639
- await vss.addVectors(vectors, documents);
899
+ await vss.addVectors(vectors, documents, options);
640
900
  return vss;
641
901
  }
642
902
  catch (error) {
@@ -649,8 +909,8 @@ class OracleVS extends vectorstores_1.VectorStore {
649
909
  * inside the pool are terminated.
650
910
  */
651
911
  async end() {
652
- if (isPool(this.client)) {
653
- await this.client?.close();
912
+ if (!isClientProvider(this.client) && isPool(this.client)) {
913
+ await this.client.close();
654
914
  }
655
915
  }
656
916
  }