ydb-qdrant 8.1.0 → 9.0.3

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.
Files changed (87) hide show
  1. package/README.md +20 -18
  2. package/dist/SmokeTest.js +2 -2
  3. package/dist/compute/ComputePool.d.ts +5 -0
  4. package/dist/compute/ComputePool.js +64 -0
  5. package/dist/compute/ComputeWorker.d.ts +36 -0
  6. package/dist/compute/ComputeWorker.js +84 -0
  7. package/dist/config/env.d.ts +24 -7
  8. package/dist/config/env.js +65 -35
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +92 -2
  11. package/dist/logging/DeployLogFormatter.d.ts +2 -0
  12. package/dist/logging/DeployLogFormatter.js +131 -0
  13. package/dist/logging/logger.js +13 -1
  14. package/dist/logging/requestContext.d.ts +17 -0
  15. package/dist/logging/requestContext.js +43 -0
  16. package/dist/middleware/requestLogger.js +134 -6
  17. package/dist/middleware/upsertBodyPhase.d.ts +6 -0
  18. package/dist/middleware/upsertBodyPhase.js +184 -0
  19. package/dist/middleware/upsertRequestTimeout.d.ts +16 -0
  20. package/dist/middleware/upsertRequestTimeout.js +158 -0
  21. package/dist/package/api.d.ts +20 -12
  22. package/dist/package/api.js +57 -28
  23. package/dist/qdrant/QdrantRestTypes.d.ts +4 -0
  24. package/dist/qdrant/Requests.d.ts +97 -0
  25. package/dist/qdrant/Requests.js +72 -0
  26. package/dist/repositories/collectionsRepo.d.ts +18 -2
  27. package/dist/repositories/collectionsRepo.js +103 -7
  28. package/dist/repositories/collectionsRepo.one-table.d.ts +4 -3
  29. package/dist/repositories/collectionsRepo.one-table.js +99 -36
  30. package/dist/repositories/collectionsRepo.shared.d.ts +2 -2
  31. package/dist/repositories/collectionsRepo.shared.js +9 -4
  32. package/dist/repositories/pointsRepo.d.ts +6 -4
  33. package/dist/repositories/pointsRepo.js +8 -7
  34. package/dist/repositories/pointsRepo.one-table/Delete.d.ts +2 -2
  35. package/dist/repositories/pointsRepo.one-table/Delete.js +157 -60
  36. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +7 -5
  37. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +44 -13
  38. package/dist/repositories/pointsRepo.one-table/Retrieve.d.ts +6 -0
  39. package/dist/repositories/pointsRepo.one-table/Retrieve.js +69 -0
  40. package/dist/repositories/pointsRepo.one-table/Search.d.ts +2 -3
  41. package/dist/repositories/pointsRepo.one-table/Search.js +102 -124
  42. package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -2
  43. package/dist/repositories/pointsRepo.one-table/Upsert.js +244 -48
  44. package/dist/repositories/pointsRepo.one-table.d.ts +1 -0
  45. package/dist/repositories/pointsRepo.one-table.js +1 -0
  46. package/dist/routes/collections.js +45 -36
  47. package/dist/routes/points.js +145 -56
  48. package/dist/server.js +42 -6
  49. package/dist/services/CollectionService.d.ts +7 -5
  50. package/dist/services/CollectionService.js +12 -9
  51. package/dist/services/CollectionService.one-table.js +1 -2
  52. package/dist/services/CollectionService.shared.d.ts +6 -5
  53. package/dist/services/CollectionService.shared.js +28 -12
  54. package/dist/services/PointsService.d.ts +8 -0
  55. package/dist/services/PointsService.js +132 -15
  56. package/dist/types.d.ts +4 -94
  57. package/dist/types.js +1 -54
  58. package/dist/utils/EnvParsers.d.ts +5 -0
  59. package/dist/utils/EnvParsers.js +30 -0
  60. package/dist/utils/PayloadSign.d.ts +4 -0
  61. package/dist/utils/PayloadSign.js +18 -0
  62. package/dist/utils/distance.d.ts +1 -12
  63. package/dist/utils/distance.js +0 -21
  64. package/dist/utils/pathPrefix.d.ts +3 -0
  65. package/dist/utils/pathPrefix.js +47 -0
  66. package/dist/utils/prefixExpansion.d.ts +1 -0
  67. package/dist/utils/prefixExpansion.js +11 -0
  68. package/dist/utils/qdrantResponse.d.ts +13 -0
  69. package/dist/utils/qdrantResponse.js +12 -0
  70. package/dist/utils/requestIdentity.d.ts +8 -0
  71. package/dist/utils/requestIdentity.js +52 -0
  72. package/dist/utils/retry.d.ts +2 -0
  73. package/dist/utils/retry.js +55 -11
  74. package/dist/utils/tenant.d.ts +12 -6
  75. package/dist/utils/tenant.js +41 -32
  76. package/dist/utils/vectorBinary.d.ts +0 -1
  77. package/dist/utils/vectorBinary.js +0 -98
  78. package/dist/utils/ydbErrors.d.ts +1 -0
  79. package/dist/utils/ydbErrors.js +14 -0
  80. package/dist/ydb/bootstrapMetaTable.js +14 -2
  81. package/dist/ydb/client.d.ts +10 -2
  82. package/dist/ydb/client.js +83 -24
  83. package/dist/ydb/helpers.d.ts +0 -1
  84. package/dist/ydb/helpers.js +1 -2
  85. package/dist/ydb/schema.d.ts +2 -0
  86. package/dist/ydb/schema.js +84 -7
  87. package/package.json +10 -5
@@ -0,0 +1,52 @@
1
+ import { AnonymousIdentityError, deriveAnonymousUserUid, deriveUserUidFromApiKey, sanitizeUserUid, } from "./tenant.js";
2
+ export function isAnonymousIdentityError(err) {
3
+ return err instanceof AnonymousIdentityError;
4
+ }
5
+ function sanitizeTenantId(tenantId) {
6
+ const raw = (tenantId ?? "default").toString();
7
+ const cleaned = raw.replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_");
8
+ const lowered = cleaned.toLowerCase().replace(/^_+/, "");
9
+ return lowered.length > 0 ? lowered : "default";
10
+ }
11
+ export function getRequestApiKey(req) {
12
+ const apiKey = req.header("api-key")?.trim();
13
+ return apiKey && apiKey.length > 0 ? apiKey : undefined;
14
+ }
15
+ export function getRequestClientIp(req) {
16
+ const reqIp = typeof req.ip === "string" && req.ip.trim().length > 0
17
+ ? req.ip.trim()
18
+ : undefined;
19
+ if (reqIp) {
20
+ return reqIp;
21
+ }
22
+ const remoteAddress = req.socket.remoteAddress;
23
+ return typeof remoteAddress === "string" && remoteAddress.trim().length > 0
24
+ ? remoteAddress.trim()
25
+ : undefined;
26
+ }
27
+ export function resolveRequestUserUid(req) {
28
+ const apiKey = getRequestApiKey(req);
29
+ if (apiKey) {
30
+ return deriveUserUidFromApiKey(apiKey);
31
+ }
32
+ return deriveAnonymousUserUid({
33
+ clientIp: getRequestClientIp(req),
34
+ userAgent: req.header("User-Agent") ?? undefined,
35
+ });
36
+ }
37
+ export function resolveRequestNamespaceUserUid(req) {
38
+ const baseUserUid = resolveRequestUserUid(req);
39
+ const tenantId = sanitizeTenantId(req.header("X-Tenant-Id") ?? undefined);
40
+ return sanitizeUserUid(`${baseUserUid}_${tenantId}`);
41
+ }
42
+ export function resolveRequestSigningKey(req, userUid) {
43
+ const apiKey = getRequestApiKey(req);
44
+ if (apiKey) {
45
+ return apiKey;
46
+ }
47
+ const fallbackUserUid = userUid?.trim();
48
+ if (fallbackUserUid) {
49
+ return fallbackUserUid;
50
+ }
51
+ return resolveRequestUserUid(req);
52
+ }
@@ -1,8 +1,10 @@
1
1
  export interface RetryOptions {
2
2
  maxRetries?: number;
3
3
  baseDelayMs?: number;
4
+ maxBackoffMs?: number;
4
5
  isTransient?: (error: unknown) => boolean;
5
6
  context?: Record<string, unknown>;
6
7
  }
7
8
  export declare function isTransientYdbError(error: unknown): boolean;
9
+ export declare function isTransientYdbErrorInAcquiredSession(error: unknown): boolean;
8
10
  export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
@@ -1,25 +1,68 @@
1
1
  import { logger } from "../logging/logger.js";
2
2
  const DEFAULT_MAX_RETRIES = 6;
3
3
  const DEFAULT_BASE_DELAY_MS = 250;
4
- export function isTransientYdbError(error) {
4
+ const UNDETERMINED_OPERATION_UNKNOWN_RE = /Undetermined \(code 400170\)|State of operation is unknown|Failed to d(?:eliver|eviler) message|issueCode["']?\s*:\s*2026/i;
5
+ const SESSION_RECREATION_ERROR_RE = /BadSession \(code 400100\)|Session is under shutdown|SessionBusy|SESSION_BUSY/i;
6
+ const SESSION_EXPIRED_ERROR_RE = /SessionExpired|SESSION_EXPIRED/i;
7
+ const SESSION_POOL_CONTENTION_RE = /SESSION_POOL_EMPTY|No session became available within timeout/i;
8
+ const SAME_SESSION_TRANSIENT_YDB_ERROR_RE = /Aborted|Unavailable \(code 400050\)|schema version mismatch|Table metadata loading|Failed to load metadata|overloaded|is in process of split|wrong shard state|Rejecting data TxId/i;
9
+ const TIMEOUT_ERROR_RE = /Timeout \(code 400090\)/i;
10
+ function normalizeMaxBackoffMs(value) {
11
+ if (value === undefined) {
12
+ return Number.POSITIVE_INFINITY;
13
+ }
14
+ if (!Number.isFinite(value) || value < 0) {
15
+ return Number.POSITIVE_INFINITY;
16
+ }
17
+ return value;
18
+ }
19
+ function getErrorMessageAndIssuesText(error) {
5
20
  const msg = error instanceof Error ? error.message : String(error);
6
- if (/Aborted|schema version mismatch|Table metadata loading|Failed to load metadata|overloaded|is in process of split|wrong shard state|Rejecting data TxId/i.test(msg)) {
7
- return true;
21
+ if (typeof error !== "object" || error === null) {
22
+ return { msg };
8
23
  }
9
- if (typeof error === "object" && error !== null) {
10
- const issues = error.issues;
11
- if (issues !== undefined) {
12
- const issuesText = typeof issues === "string" ? issues : JSON.stringify(issues);
13
- if (/overloaded|is in process of split|wrong shard state|Rejecting data TxId/i.test(issuesText)) {
14
- return true;
15
- }
24
+ const issues = error.issues;
25
+ if (issues === undefined) {
26
+ return { msg };
27
+ }
28
+ return {
29
+ msg,
30
+ issuesText: typeof issues === "string" ? issues : JSON.stringify(issues),
31
+ };
32
+ }
33
+ function matchesPatternSet(error, patterns) {
34
+ const { msg, issuesText } = getErrorMessageAndIssuesText(error);
35
+ for (const pattern of patterns) {
36
+ if (pattern.test(msg)) {
37
+ return true;
38
+ }
39
+ if (issuesText !== undefined && pattern.test(issuesText)) {
40
+ return true;
16
41
  }
17
42
  }
18
43
  return false;
19
44
  }
45
+ export function isTransientYdbError(error) {
46
+ return matchesPatternSet(error, [
47
+ UNDETERMINED_OPERATION_UNKNOWN_RE,
48
+ SESSION_RECREATION_ERROR_RE,
49
+ SESSION_EXPIRED_ERROR_RE,
50
+ SESSION_POOL_CONTENTION_RE,
51
+ SAME_SESSION_TRANSIENT_YDB_ERROR_RE,
52
+ TIMEOUT_ERROR_RE,
53
+ ]);
54
+ }
55
+ export function isTransientYdbErrorInAcquiredSession(error) {
56
+ return matchesPatternSet(error, [
57
+ UNDETERMINED_OPERATION_UNKNOWN_RE,
58
+ SAME_SESSION_TRANSIENT_YDB_ERROR_RE,
59
+ TIMEOUT_ERROR_RE,
60
+ ]);
61
+ }
20
62
  export async function withRetry(fn, options = {}) {
21
63
  const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
22
64
  const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
65
+ const maxBackoffMs = normalizeMaxBackoffMs(options.maxBackoffMs);
23
66
  const isTransient = options.isTransient ?? isTransientYdbError;
24
67
  const context = options.context ?? {};
25
68
  let attempt = 0;
@@ -31,7 +74,8 @@ export async function withRetry(fn, options = {}) {
31
74
  if (!isTransient(e) || attempt >= maxRetries) {
32
75
  throw e;
33
76
  }
34
- const backoffMs = Math.floor(baseDelayMs * Math.pow(2, attempt) + Math.random() * 100);
77
+ const computedBackoffMs = Math.floor(baseDelayMs * Math.pow(2, attempt) + Math.random() * 100);
78
+ const backoffMs = Math.min(computedBackoffMs, maxBackoffMs);
35
79
  logger.warn({
36
80
  ...context,
37
81
  attempt,
@@ -1,7 +1,13 @@
1
- export declare function hashApiKey(apiKey: string | undefined): string | undefined;
1
+ export declare class AnonymousIdentityError extends Error {
2
+ constructor();
3
+ }
2
4
  export declare function normalizeUserAgent(userAgent: string | undefined): string | undefined;
3
- export declare function sanitizeCollectionName(name: string, apiKeyHash?: string, userAgentNormalized?: string): string;
4
- export declare function sanitizeTenantId(tenantId: string | undefined): string;
5
- export declare function tableNameFor(sanitizedTenant: string, sanitizedCollection: string): string;
6
- export declare function metaKeyFor(sanitizedTenant: string, sanitizedCollection: string): string;
7
- export declare function uidFor(sanitizedTenant: string, sanitizedCollection: string): string;
5
+ export declare function sanitizeCollectionName(name: string, _userAgentNormalized?: string): string;
6
+ export declare function sanitizeUserUid(userUid: string): string;
7
+ export declare function deriveUserUidFromApiKey(apiKey: string): string;
8
+ export declare function deriveAnonymousUserUid(args: {
9
+ clientIp?: string;
10
+ userAgent?: string;
11
+ }): string;
12
+ export declare function metaKeyFor(sanitizedUserUid: string, sanitizedCollection: string): string;
13
+ export declare function uidFor(sanitizedUserUid: string, sanitizedCollection: string): string;
@@ -1,9 +1,15 @@
1
- import { createHash } from "crypto";
2
- export function hashApiKey(apiKey) {
3
- if (!apiKey || apiKey.trim() === "")
4
- return undefined;
5
- const hash = createHash("sha256").update(apiKey).digest("hex");
6
- return hash.slice(0, 8);
1
+ import { createHash } from "node:crypto";
2
+ const API_KEY_UID_PREFIX = "ak";
3
+ const ANONYMOUS_UID_PREFIX = "anon";
4
+ const UID_HASH_LENGTH = 16;
5
+ export class AnonymousIdentityError extends Error {
6
+ constructor() {
7
+ super("Anonymous requests require api-key or identifiable client metadata.");
8
+ this.name = "AnonymousIdentityError";
9
+ }
10
+ }
11
+ function shortHash(value) {
12
+ return createHash("sha256").update(value).digest("hex").slice(0, UID_HASH_LENGTH);
7
13
  }
8
14
  export function normalizeUserAgent(userAgent) {
9
15
  if (!userAgent || userAgent.trim() === "")
@@ -22,39 +28,42 @@ export function normalizeUserAgent(userAgent) {
22
28
  }
23
29
  if (lowered.length === 0)
24
30
  return undefined;
25
- const hash = createHash("sha256").update(userAgent).digest("hex");
26
- const shortHash = hash.slice(0, 8);
27
- return `${lowered}_${shortHash}`;
31
+ return lowered;
28
32
  }
29
- export function sanitizeCollectionName(name, apiKeyHash, userAgentNormalized) {
33
+ export function sanitizeCollectionName(name, _userAgentNormalized) {
34
+ void _userAgentNormalized;
30
35
  const cleaned = name.replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_");
31
36
  const lowered = cleaned.toLowerCase().replace(/^_+/, "");
32
- const base = lowered.length > 0 ? lowered : "collection";
33
- const hasApiKey = apiKeyHash !== undefined && apiKeyHash.trim().length > 0;
34
- const hasUserAgent = userAgentNormalized !== undefined && userAgentNormalized.trim().length > 0;
35
- if (hasApiKey && hasUserAgent) {
36
- return `${base}_${apiKeyHash}_${userAgentNormalized}`;
37
- }
38
- else if (hasApiKey) {
39
- return `${base}_${apiKeyHash}`;
40
- }
41
- else if (hasUserAgent) {
42
- return `${base}_${userAgentNormalized}`;
43
- }
44
- return base;
37
+ return lowered.length > 0 ? lowered : "collection";
45
38
  }
46
- export function sanitizeTenantId(tenantId) {
47
- const raw = (tenantId ?? "default").toString();
39
+ export function sanitizeUserUid(userUid) {
40
+ const raw = userUid.toString();
48
41
  const cleaned = raw.replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_");
49
42
  const lowered = cleaned.toLowerCase().replace(/^_+/, "");
50
- return lowered.length > 0 ? lowered : "default";
43
+ return lowered.length > 0 ? lowered : "anonymous";
44
+ }
45
+ export function deriveUserUidFromApiKey(apiKey) {
46
+ const normalizedApiKey = apiKey.trim();
47
+ if (normalizedApiKey.length === 0) {
48
+ throw new Error("deriveUserUidFromApiKey: apiKey is empty");
49
+ }
50
+ return sanitizeUserUid(`${API_KEY_UID_PREFIX}_${shortHash(normalizedApiKey)}`);
51
51
  }
52
- export function tableNameFor(sanitizedTenant, sanitizedCollection) {
53
- return `qdr_${sanitizedTenant}__${sanitizedCollection}`;
52
+ export function deriveAnonymousUserUid(args) {
53
+ const normalizedUserAgent = normalizeUserAgent(args.userAgent);
54
+ const normalizedClientIp = args.clientIp?.trim();
55
+ if (!normalizedClientIp && !normalizedUserAgent) {
56
+ throw new AnonymousIdentityError();
57
+ }
58
+ const identitySeed = [
59
+ normalizedClientIp || "unknown_ip",
60
+ normalizedUserAgent || "unknown_ua",
61
+ ].join("|");
62
+ return sanitizeUserUid(`${ANONYMOUS_UID_PREFIX}_${shortHash(identitySeed)}`);
54
63
  }
55
- export function metaKeyFor(sanitizedTenant, sanitizedCollection) {
56
- return `${sanitizedTenant}/${sanitizedCollection}`;
64
+ export function metaKeyFor(sanitizedUserUid, sanitizedCollection) {
65
+ return `${sanitizedUserUid}/${sanitizedCollection}`;
57
66
  }
58
- export function uidFor(sanitizedTenant, sanitizedCollection) {
59
- return tableNameFor(sanitizedTenant, sanitizedCollection);
67
+ export function uidFor(sanitizedUserUid, sanitizedCollection) {
68
+ return metaKeyFor(sanitizedUserUid, sanitizedCollection);
60
69
  }
@@ -1,2 +1 @@
1
1
  export declare function vectorToFloatBinary(vector: number[]): Buffer;
2
- export declare function vectorToBitBinary(vector: number[]): Buffer;
@@ -19,101 +19,3 @@ export function vectorToFloatBinary(vector) {
19
19
  buffer.writeUInt8(1, vector.length * 4);
20
20
  return buffer;
21
21
  }
22
- export function vectorToBitBinary(vector) {
23
- // Mirrors YDB's TKnnBitVectorSerializer (Knn::ToBinaryStringBit) layout:
24
- // - Packed bits as integers written in native endianness
25
- // - Then 1 byte: count of unused bits in the last data byte
26
- // - Then 1 byte: format marker (10 = BitVector)
27
- //
28
- // Source: https://raw.githubusercontent.com/ydb-platform/ydb/0b506f56e399e0b4e6a6a4267799da68a3164bf7/ydb/library/yql/udfs/common/knn/knn-serializer.h
29
- const bitLen = vector.length;
30
- const dataByteLen = Math.ceil(bitLen / 8);
31
- const totalLen = dataByteLen + 2; // +1 unused-bit-count +1 format marker
32
- const buffer = Buffer.alloc(totalLen);
33
- let offset = 0;
34
- const writeU64 = (v) => {
35
- if (IS_LITTLE_ENDIAN) {
36
- buffer.writeBigUInt64LE(v, offset);
37
- }
38
- else {
39
- buffer.writeBigUInt64BE(v, offset);
40
- }
41
- offset += 8;
42
- };
43
- const writeU32 = (v) => {
44
- if (IS_LITTLE_ENDIAN) {
45
- buffer.writeUInt32LE(v, offset);
46
- }
47
- else {
48
- buffer.writeUInt32BE(v, offset);
49
- }
50
- offset += 4;
51
- };
52
- const writeU16 = (v) => {
53
- if (IS_LITTLE_ENDIAN) {
54
- buffer.writeUInt16LE(v, offset);
55
- }
56
- else {
57
- buffer.writeUInt16BE(v, offset);
58
- }
59
- offset += 2;
60
- };
61
- const writeU8 = (v) => {
62
- buffer.writeUInt8(v, offset);
63
- offset += 1;
64
- };
65
- let accumulator = 0n;
66
- let filledBits = 0;
67
- for (let i = 0; i < vector.length; i += 1) {
68
- const value = vector[i];
69
- if (!Number.isFinite(value)) {
70
- throw new Error(`Non-finite value in vector at index ${i}: ${value}`);
71
- }
72
- if (value > 0) {
73
- accumulator |= 1n;
74
- }
75
- filledBits += 1;
76
- if (filledBits === 64) {
77
- writeU64(accumulator);
78
- accumulator = 0n;
79
- filledBits = 0;
80
- }
81
- accumulator <<= 1n;
82
- }
83
- accumulator >>= 1n;
84
- filledBits += 7;
85
- const tailWriteIf = (bits) => {
86
- if (filledBits < bits) {
87
- return;
88
- }
89
- if (bits === 64) {
90
- writeU64(accumulator & 0xffffffffffffffffn);
91
- filledBits -= 64;
92
- return;
93
- }
94
- if (bits === 32) {
95
- writeU32(Number(accumulator & 0xffffffffn));
96
- accumulator >>= 32n;
97
- filledBits -= 32;
98
- return;
99
- }
100
- if (bits === 16) {
101
- writeU16(Number(accumulator & 0xffffn));
102
- accumulator >>= 16n;
103
- filledBits -= 16;
104
- return;
105
- }
106
- writeU8(Number(accumulator & 0xffn));
107
- accumulator >>= 8n;
108
- filledBits -= 8;
109
- };
110
- tailWriteIf(64);
111
- tailWriteIf(32);
112
- tailWriteIf(16);
113
- tailWriteIf(8);
114
- // After tail writes, we must have < 8 "filledBits" left.
115
- const unusedBitsInLastByte = 7 - filledBits;
116
- writeU8(unusedBitsInLastByte);
117
- writeU8(10);
118
- return buffer;
119
- }
@@ -0,0 +1 @@
1
+ export declare function isOutOfBufferMemoryYdbError(error: unknown): boolean;
@@ -0,0 +1,14 @@
1
+ export function isOutOfBufferMemoryYdbError(error) {
2
+ const msg = error instanceof Error ? error.message : String(error);
3
+ if (/Out of buffer memory/i.test(msg)) {
4
+ return true;
5
+ }
6
+ if (typeof error === "object" && error !== null && "issues" in error) {
7
+ const issues = error.issues;
8
+ if (issues !== undefined) {
9
+ const issuesText = typeof issues === "string" ? issues : JSON.stringify(issues);
10
+ return /Out of buffer memory/i.test(issuesText);
11
+ }
12
+ }
13
+ return false;
14
+ }
@@ -1,3 +1,4 @@
1
+ import { basename } from "node:path";
1
2
  import { Column, TableDescription, Types, destroyDriver, readyOrThrow, withSession, } from "./client.js";
2
3
  function isTableNotFoundError(err) {
3
4
  const msg = err instanceof Error ? err.message : String(err);
@@ -30,7 +31,7 @@ export async function bootstrapMetaTable() {
30
31
  throw err;
31
32
  }
32
33
  const td = new TableDescription()
33
- .withColumns(new Column("collection", Types.UTF8), new Column("table_name", Types.UTF8), new Column("vector_dimension", Types.UINT32), new Column("distance", Types.UTF8), new Column("vector_type", Types.UTF8), new Column("created_at", Types.TIMESTAMP), new Column("last_accessed_at", Types.TIMESTAMP))
34
+ .withColumns(new Column("collection", Types.UTF8), new Column("table_name", Types.UTF8), new Column("vector_dimension", Types.UINT32), new Column("distance", Types.UTF8), new Column("vector_type", Types.UTF8), new Column("created_at", Types.TIMESTAMP), new Column("last_accessed_at", Types.TIMESTAMP), new Column("user_uid", Types.optional(Types.UTF8)))
34
35
  .withPrimaryKey("collection");
35
36
  try {
36
37
  await s.createTable("qdr__collections", td);
@@ -48,10 +49,21 @@ export async function bootstrapMetaTable() {
48
49
  if (!hasLastAccessedAt) {
49
50
  throw new Error("bootstrapMetaTable: qdr__collections exists but is missing required column last_accessed_at");
50
51
  }
52
+ const hasUserUid = cols.some((c) => c.name === "user_uid");
53
+ if (!hasUserUid) {
54
+ throw new Error("bootstrapMetaTable: qdr__collections exists but is missing required column user_uid");
55
+ }
51
56
  });
52
57
  }
58
+ function isBootstrapMetaTableEntrypoint(argv1) {
59
+ if (!argv1) {
60
+ return false;
61
+ }
62
+ const entry = basename(argv1);
63
+ return (entry === "bootstrapMetaTable.ts" || entry === "bootstrapMetaTable.js");
64
+ }
53
65
  // CLI entrypoint
54
- if (process.argv[1]?.endsWith("bootstrapMetaTable.js")) {
66
+ if (isBootstrapMetaTableEntrypoint(process.argv[1])) {
55
67
  const main = async () => {
56
68
  await bootstrapMetaTable();
57
69
  // Important: ydb-sdk driver/session pool keeps timers alive; explicitly
@@ -1,6 +1,6 @@
1
1
  import type { Session, IAuthService, ExecuteQuerySettings as YdbExecuteQuerySettings, BulkUpsertSettings as YdbBulkUpsertSettings, QuerySession } from "ydb-sdk";
2
- declare const Types: typeof import("ydb-sdk").Types, TypedValues: typeof import("ydb-sdk").TypedValues, TableDescription: typeof import("ydb-sdk").TableDescription, Column: typeof import("ydb-sdk").Column, ExecuteQuerySettings: typeof YdbExecuteQuerySettings, BulkUpsertSettings: typeof YdbBulkUpsertSettings, Ydb: typeof import("ydb-sdk-proto").Ydb;
3
- export { Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, BulkUpsertSettings, Ydb, };
2
+ declare const Types: typeof import("ydb-sdk").Types, TypedValues: typeof import("ydb-sdk").TypedValues, TableDescription: typeof import("ydb-sdk").TableDescription, Column: typeof import("ydb-sdk").Column, TableIndex: typeof import("ydb-sdk").TableIndex, AlterTableDescription: typeof import("ydb-sdk").AlterTableDescription, ExecuteQuerySettings: typeof YdbExecuteQuerySettings, BulkUpsertSettings: typeof YdbBulkUpsertSettings, Ydb: typeof import("ydb-sdk-proto").Ydb;
3
+ export { Types, TypedValues, TableDescription, Column, TableIndex, AlterTableDescription, ExecuteQuerySettings, BulkUpsertSettings, Ydb, };
4
4
  export declare function createExecuteQuerySettings(options?: {
5
5
  keepInCache?: boolean;
6
6
  idempotent?: boolean;
@@ -26,6 +26,14 @@ export declare function __resetRefreshStateForTests(): void;
26
26
  export declare function configureDriver(config: DriverConfig): void;
27
27
  export declare function readyOrThrow(): Promise<void>;
28
28
  export declare function withSession<T>(fn: (s: Session) => Promise<T>): Promise<T>;
29
+ /**
30
+ * Runs a callback in a single session without re-running the callback on session errors.
31
+ *
32
+ * Use this for multi-step callbacks where rerunning from the beginning would cause
33
+ * incorrect counters or repeated side effects. Prefer `withSession()` for single-shot
34
+ * operations to auto-retry on BadSession/SessionBusy via ydb-sdk.
35
+ */
36
+ export declare function withSessionOnce<T>(fn: (s: Session) => Promise<T>): Promise<T>;
29
37
  export declare function withQuerySession<T>(fn: (s: QuerySession) => Promise<T>, options?: {
30
38
  timeoutMs?: number;
31
39
  idempotent?: boolean;
@@ -1,9 +1,9 @@
1
1
  import { createRequire } from "module";
2
- import { YDB_DATABASE, YDB_ENDPOINT, SESSION_POOL_MIN_SIZE, SESSION_POOL_MAX_SIZE, SESSION_KEEPALIVE_PERIOD_MS, STARTUP_PROBE_SESSION_TIMEOUT_MS, } from "../config/env.js";
2
+ import { SESSION_POOL_MIN_SIZE, SESSION_POOL_MAX_SIZE, SESSION_KEEPALIVE_PERIOD_MS, STARTUP_PROBE_SESSION_TIMEOUT_MS, YDB_SESSION_RETRY_MAX_RETRIES, TABLE_SESSION_TIMEOUT_MS, resolveYdbConnectionConfig, } from "../config/env.js";
3
3
  import { logger } from "../logging/logger.js";
4
4
  const require = createRequire(import.meta.url);
5
- const { Driver, getCredentialsFromEnv, Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, BulkUpsertSettings, OperationParams, Ydb, } = require("ydb-sdk");
6
- export { Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, BulkUpsertSettings, Ydb, };
5
+ const { Driver, getCredentialsFromEnv, Types, TypedValues, TableDescription, Column, TableIndex, AlterTableDescription, ExecuteQuerySettings, BulkUpsertSettings, OperationParams, Ydb, } = require("ydb-sdk");
6
+ export { Types, TypedValues, TableDescription, Column, TableIndex, AlterTableDescription, ExecuteQuerySettings, BulkUpsertSettings, Ydb, };
7
7
  export function createExecuteQuerySettings(options) {
8
8
  const { keepInCache = true, idempotent = true } = options ?? {};
9
9
  const settings = new ExecuteQuerySettings();
@@ -36,9 +36,9 @@ export function createBulkUpsertSettingsWithTimeout(options) {
36
36
  return settings;
37
37
  }
38
38
  const DRIVER_READY_TIMEOUT_MS = 15000;
39
- const TABLE_SESSION_TIMEOUT_MS = 20000;
40
39
  const YDB_HEALTHCHECK_READY_TIMEOUT_MS = 5000;
41
40
  const DRIVER_REFRESH_COOLDOWN_MS = 30000;
41
+ const DRIVER_DESTROY_GRACE_PERIOD_MS = TABLE_SESSION_TIMEOUT_MS + 5000;
42
42
  let overrideConfig;
43
43
  let driver;
44
44
  let lastDriverRefreshAt = 0;
@@ -50,8 +50,8 @@ export function isCompilationTimeoutError(error) {
50
50
  return false;
51
51
  }
52
52
  const msg = error.message ?? "";
53
- if (/Timeout \(code 400090\)/i.test(msg) ||
54
- /Query compilation timed out/i.test(msg)) {
53
+ if (/Query compilation timed out/i.test(msg) ||
54
+ (/Timeout \(code 400090\)/i.test(msg) && /compilation/i.test(msg))) {
55
55
  return true;
56
56
  }
57
57
  // Startup probe uses explicit cancel-after; YDB returns Cancelled with
@@ -130,17 +130,13 @@ export function configureDriver(config) {
130
130
  }
131
131
  overrideConfig = config;
132
132
  }
133
- function getOrCreateDriver() {
134
- if (driver) {
135
- return driver;
136
- }
137
- const base = overrideConfig?.connectionString != null
138
- ? { connectionString: overrideConfig.connectionString }
139
- : {
140
- endpoint: overrideConfig?.endpoint ?? YDB_ENDPOINT,
141
- database: overrideConfig?.database ?? YDB_DATABASE,
142
- };
143
- const driverConfig = {
133
+ function getDriverConfig() {
134
+ const base = resolveYdbConnectionConfig({
135
+ endpoint: overrideConfig?.endpoint,
136
+ database: overrideConfig?.database,
137
+ connectionString: overrideConfig?.connectionString,
138
+ });
139
+ return {
144
140
  ...base,
145
141
  authService: overrideConfig?.authService ?? getCredentialsFromEnv(),
146
142
  poolSettings: {
@@ -149,7 +145,10 @@ function getOrCreateDriver() {
149
145
  keepAlivePeriod: SESSION_KEEPALIVE_PERIOD_MS,
150
146
  },
151
147
  };
152
- driver = driverFactoryOverride
148
+ }
149
+ function createDriverInstance() {
150
+ const driverConfig = getDriverConfig();
151
+ const created = driverFactoryOverride
153
152
  ? driverFactoryOverride(driverConfig)
154
153
  : new Driver(driverConfig);
155
154
  logger.info({
@@ -157,6 +156,13 @@ function getOrCreateDriver() {
157
156
  poolMaxSize: SESSION_POOL_MAX_SIZE,
158
157
  keepAlivePeriodMs: SESSION_KEEPALIVE_PERIOD_MS,
159
158
  }, "YDB driver created with session pool settings");
159
+ return created;
160
+ }
161
+ function getOrCreateDriver() {
162
+ if (driver) {
163
+ return driver;
164
+ }
165
+ driver = createDriverInstance();
160
166
  return driver;
161
167
  }
162
168
  export async function readyOrThrow() {
@@ -167,6 +173,23 @@ export async function readyOrThrow() {
167
173
  }
168
174
  }
169
175
  export async function withSession(fn) {
176
+ const d = getOrCreateDriver();
177
+ try {
178
+ return await d.tableClient.withSessionRetry(fn, TABLE_SESSION_TIMEOUT_MS, YDB_SESSION_RETRY_MAX_RETRIES);
179
+ }
180
+ catch (err) {
181
+ void maybeRefreshDriverOnSessionError(err);
182
+ throw err;
183
+ }
184
+ }
185
+ /**
186
+ * Runs a callback in a single session without re-running the callback on session errors.
187
+ *
188
+ * Use this for multi-step callbacks where rerunning from the beginning would cause
189
+ * incorrect counters or repeated side effects. Prefer `withSession()` for single-shot
190
+ * operations to auto-retry on BadSession/SessionBusy via ydb-sdk.
191
+ */
192
+ export async function withSessionOnce(fn) {
170
193
  const d = getOrCreateDriver();
171
194
  try {
172
195
  return await d.tableClient.withSession(fn, TABLE_SESSION_TIMEOUT_MS);
@@ -194,7 +217,7 @@ export async function withQuerySession(fn, options) {
194
217
  export async function withStartupProbeSession(fn) {
195
218
  const d = getOrCreateDriver();
196
219
  try {
197
- return await d.tableClient.withSession(fn, STARTUP_PROBE_SESSION_TIMEOUT_MS);
220
+ return await d.tableClient.withSessionRetry(fn, STARTUP_PROBE_SESSION_TIMEOUT_MS, YDB_SESSION_RETRY_MAX_RETRIES);
198
221
  }
199
222
  catch (err) {
200
223
  void maybeRefreshDriverOnSessionError(err);
@@ -219,13 +242,14 @@ export async function destroyDriver() {
219
242
  return;
220
243
  }
221
244
  logger.info("Destroying YDB driver and session pool");
245
+ const toDestroy = driver;
246
+ driver = undefined;
222
247
  try {
223
- await driver.destroy();
248
+ await toDestroy.destroy();
224
249
  }
225
250
  catch (err) {
226
251
  logger.warn({ err }, "Error during driver destruction (ignored)");
227
252
  }
228
- driver = undefined;
229
253
  }
230
254
  /**
231
255
  * Destroys the current driver and immediately creates a fresh one.
@@ -233,7 +257,42 @@ export async function destroyDriver() {
233
257
  */
234
258
  export async function refreshDriver() {
235
259
  logger.info("Refreshing YDB driver");
236
- await destroyDriver();
237
- await readyOrThrow();
238
- logger.info("YDB driver refreshed successfully");
260
+ const oldDriver = driver;
261
+ const newDriver = createDriverInstance();
262
+ let ok;
263
+ try {
264
+ ok = await newDriver.ready(DRIVER_READY_TIMEOUT_MS);
265
+ }
266
+ catch (err) {
267
+ try {
268
+ await newDriver.destroy();
269
+ }
270
+ catch (destroyErr) {
271
+ logger.warn({ err: destroyErr }, "Error during fresh driver destruction after failed ready() (ignored)");
272
+ }
273
+ throw err;
274
+ }
275
+ if (!ok) {
276
+ try {
277
+ await newDriver.destroy();
278
+ }
279
+ catch (err) {
280
+ logger.warn({ err }, "Error during fresh driver destruction after failed ready() (ignored)");
281
+ }
282
+ throw new Error(`YDB driver is not ready in ${DRIVER_READY_TIMEOUT_MS / 1000}s. Check connectivity and credentials.`);
283
+ }
284
+ driver = newDriver;
285
+ logger.info({ gracePeriodMs: DRIVER_DESTROY_GRACE_PERIOD_MS }, "YDB driver swapped successfully; scheduling old driver destruction");
286
+ if (oldDriver) {
287
+ setTimeout(() => {
288
+ void (async () => {
289
+ try {
290
+ await oldDriver.destroy();
291
+ }
292
+ catch (err) {
293
+ logger.warn({ err }, "Error during delayed old driver destruction (ignored)");
294
+ }
295
+ })();
296
+ }, DRIVER_DESTROY_GRACE_PERIOD_MS);
297
+ }
239
298
  }
@@ -1,4 +1,3 @@
1
1
  export declare function buildVectorBinaryParams(vector: number[]): {
2
2
  float: Buffer<ArrayBufferLike>;
3
- bit: Buffer<ArrayBufferLike>;
4
3
  };
@@ -1,7 +1,6 @@
1
- import { vectorToFloatBinary, vectorToBitBinary, } from "../utils/vectorBinary.js";
1
+ import { vectorToFloatBinary } from "../utils/vectorBinary.js";
2
2
  export function buildVectorBinaryParams(vector) {
3
3
  return {
4
4
  float: vectorToFloatBinary(vector),
5
- bit: vectorToBitBinary(vector),
6
5
  };
7
6
  }
@@ -1,4 +1,6 @@
1
1
  export declare const GLOBAL_POINTS_TABLE = "qdrant_all_points";
2
+ export declare const POINTS_BY_FILE_LOOKUP_TABLE = "qdrant_points_by_file";
2
3
  export { UPSERT_BATCH_SIZE } from "../config/env.js";
3
4
  export declare function ensureMetaTable(): Promise<void>;
4
5
  export declare function ensureGlobalPointsTable(): Promise<void>;
6
+ export declare function ensurePointsByFileTable(): Promise<void>;