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