ydb-qdrant 5.2.1 → 6.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.
- package/dist/config/env.d.ts +1 -0
- package/dist/config/env.js +1 -0
- package/dist/repositories/collectionsRepo.d.ts +11 -7
- package/dist/repositories/collectionsRepo.js +81 -4
- package/dist/repositories/collectionsRepo.shared.js +6 -3
- package/dist/services/CollectionService.js +4 -1
- package/dist/services/CollectionService.shared.d.ts +1 -0
- package/dist/services/CollectionService.shared.js +3 -3
- package/dist/services/PointsService.js +4 -1
- package/dist/types.d.ts +13 -0
- package/dist/utils/tenant.d.ts +2 -2
- package/dist/utils/tenant.js +21 -6
- package/dist/ydb/schema.js +27 -3
- package/package.json +1 -1
package/dist/config/env.d.ts
CHANGED
|
@@ -20,3 +20,4 @@ export declare const SESSION_KEEPALIVE_PERIOD_MS: number;
|
|
|
20
20
|
export declare const STARTUP_PROBE_SESSION_TIMEOUT_MS: number;
|
|
21
21
|
export declare const UPSERT_OPERATION_TIMEOUT_MS: number;
|
|
22
22
|
export declare const SEARCH_OPERATION_TIMEOUT_MS: number;
|
|
23
|
+
export declare const LAST_ACCESS_MIN_WRITE_INTERVAL_MS: number;
|
package/dist/config/env.js
CHANGED
|
@@ -71,3 +71,4 @@ export const SESSION_KEEPALIVE_PERIOD_MS = parseIntegerEnv(process.env.YDB_SESSI
|
|
|
71
71
|
export const STARTUP_PROBE_SESSION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_STARTUP_PROBE_SESSION_TIMEOUT_MS, 5000, { min: 1000 });
|
|
72
72
|
export const UPSERT_OPERATION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_UPSERT_TIMEOUT_MS, 5000, { min: 1000 });
|
|
73
73
|
export const SEARCH_OPERATION_TIMEOUT_MS = parseIntegerEnv(process.env.YDB_QDRANT_SEARCH_TIMEOUT_MS, 10000, { min: 1000 });
|
|
74
|
+
export const LAST_ACCESS_MIN_WRITE_INTERVAL_MS = parseIntegerEnv(process.env.YDB_QDRANT_LAST_ACCESS_MIN_WRITE_INTERVAL_MS, 60000, { min: 1000 });
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import type { DistanceKind, VectorType } from "../types";
|
|
1
|
+
import type { DistanceKind, VectorType, CollectionMeta } from "../types";
|
|
2
2
|
export declare function createCollection(metaKey: string, dim: number, distance: DistanceKind, vectorType: VectorType): Promise<void>;
|
|
3
|
-
export declare function getCollectionMeta(metaKey: string): Promise<
|
|
4
|
-
table: string;
|
|
5
|
-
dimension: number;
|
|
6
|
-
distance: DistanceKind;
|
|
7
|
-
vectorType: VectorType;
|
|
8
|
-
} | null>;
|
|
3
|
+
export declare function getCollectionMeta(metaKey: string): Promise<CollectionMeta | null>;
|
|
9
4
|
export declare function verifyCollectionsQueryCompilationForStartup(): Promise<void>;
|
|
10
5
|
export declare function deleteCollection(metaKey: string, uid?: string): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Best-effort metadata update for a collection's last_accessed_at timestamp.
|
|
8
|
+
*
|
|
9
|
+
* - Uses an in-memory throttle (per metaKey) to avoid writing more often than
|
|
10
|
+
* LAST_ACCESS_MIN_WRITE_INTERVAL_MS.
|
|
11
|
+
* - Accepts an optional now parameter (default: current time) to aid testing.
|
|
12
|
+
* - Logs and ignores YDB errors so callers' primary operations are not affected.
|
|
13
|
+
*/
|
|
14
|
+
export declare function touchCollectionLastAccess(metaKey: string, now?: Date): Promise<void>;
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import { TypedValues, withSession, createExecuteQuerySettings, withStartupProbeSession, createExecuteQuerySettingsWithTimeout, } from "../ydb/client.js";
|
|
2
|
-
import { STARTUP_PROBE_SESSION_TIMEOUT_MS } from "../config/env.js";
|
|
2
|
+
import { STARTUP_PROBE_SESSION_TIMEOUT_MS, LAST_ACCESS_MIN_WRITE_INTERVAL_MS, } from "../config/env.js";
|
|
3
|
+
import { logger } from "../logging/logger.js";
|
|
3
4
|
import { uidFor } from "../utils/tenant.js";
|
|
4
5
|
import { createCollectionOneTable, deleteCollectionOneTable, } from "./collectionsRepo.one-table.js";
|
|
5
6
|
import { withRetry, isTransientYdbError } from "../utils/retry.js";
|
|
7
|
+
const lastAccessWriteCache = new Map();
|
|
8
|
+
const LAST_ACCESS_CACHE_MAX_SIZE = 10000;
|
|
9
|
+
function evictOldestLastAccessEntry() {
|
|
10
|
+
if (lastAccessWriteCache.size < LAST_ACCESS_CACHE_MAX_SIZE) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const oldestKey = lastAccessWriteCache.keys().next().value;
|
|
14
|
+
if (oldestKey !== undefined) {
|
|
15
|
+
lastAccessWriteCache.delete(oldestKey);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function shouldWriteLastAccess(nowMs, key) {
|
|
19
|
+
const last = lastAccessWriteCache.get(key);
|
|
20
|
+
if (last === undefined) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return nowMs - last >= LAST_ACCESS_MIN_WRITE_INTERVAL_MS;
|
|
24
|
+
}
|
|
6
25
|
export async function createCollection(metaKey, dim, distance, vectorType) {
|
|
7
26
|
await createCollectionOneTable(metaKey, dim, distance, vectorType);
|
|
8
27
|
}
|
|
9
28
|
export async function getCollectionMeta(metaKey) {
|
|
10
29
|
const qry = `
|
|
11
30
|
DECLARE $collection AS Utf8;
|
|
12
|
-
SELECT
|
|
31
|
+
SELECT
|
|
32
|
+
table_name,
|
|
33
|
+
vector_dimension,
|
|
34
|
+
distance,
|
|
35
|
+
vector_type,
|
|
36
|
+
CAST(last_accessed_at AS Utf8) AS last_accessed_at
|
|
13
37
|
FROM qdr__collections
|
|
14
38
|
WHERE collection = $collection;
|
|
15
39
|
`;
|
|
@@ -27,13 +51,31 @@ export async function getCollectionMeta(metaKey) {
|
|
|
27
51
|
const dimension = Number(row.items?.[1]?.uint32Value ?? row.items?.[1]?.textValue);
|
|
28
52
|
const distance = row.items?.[2]?.textValue ?? "Cosine";
|
|
29
53
|
const vectorType = row.items?.[3]?.textValue ?? "float";
|
|
30
|
-
|
|
54
|
+
const lastAccessRaw = row.items?.[4]?.textValue;
|
|
55
|
+
const lastAccessedAt = typeof lastAccessRaw === "string" && lastAccessRaw.length > 0
|
|
56
|
+
? new Date(lastAccessRaw)
|
|
57
|
+
: undefined;
|
|
58
|
+
const result = {
|
|
59
|
+
table,
|
|
60
|
+
dimension,
|
|
61
|
+
distance,
|
|
62
|
+
vectorType,
|
|
63
|
+
};
|
|
64
|
+
if (lastAccessedAt) {
|
|
65
|
+
result.lastAccessedAt = lastAccessedAt;
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
31
68
|
}
|
|
32
69
|
export async function verifyCollectionsQueryCompilationForStartup() {
|
|
33
70
|
const probeKey = "__startup_probe__/__startup_probe__";
|
|
34
71
|
const qry = `
|
|
35
72
|
DECLARE $collection AS Utf8;
|
|
36
|
-
SELECT
|
|
73
|
+
SELECT
|
|
74
|
+
table_name,
|
|
75
|
+
vector_dimension,
|
|
76
|
+
distance,
|
|
77
|
+
vector_type,
|
|
78
|
+
CAST(last_accessed_at AS Utf8) AS last_accessed_at
|
|
37
79
|
FROM qdr__collections
|
|
38
80
|
WHERE collection = $collection;
|
|
39
81
|
`;
|
|
@@ -69,3 +111,38 @@ export async function deleteCollection(metaKey, uid) {
|
|
|
69
111
|
}
|
|
70
112
|
await deleteCollectionOneTable(metaKey, effectiveUid);
|
|
71
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Best-effort metadata update for a collection's last_accessed_at timestamp.
|
|
116
|
+
*
|
|
117
|
+
* - Uses an in-memory throttle (per metaKey) to avoid writing more often than
|
|
118
|
+
* LAST_ACCESS_MIN_WRITE_INTERVAL_MS.
|
|
119
|
+
* - Accepts an optional now parameter (default: current time) to aid testing.
|
|
120
|
+
* - Logs and ignores YDB errors so callers' primary operations are not affected.
|
|
121
|
+
*/
|
|
122
|
+
export async function touchCollectionLastAccess(metaKey, now = new Date()) {
|
|
123
|
+
const nowMs = now.getTime();
|
|
124
|
+
if (!shouldWriteLastAccess(nowMs, metaKey)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const qry = `
|
|
128
|
+
DECLARE $collection AS Utf8;
|
|
129
|
+
DECLARE $last_accessed AS Timestamp;
|
|
130
|
+
UPDATE qdr__collections
|
|
131
|
+
SET last_accessed_at = $last_accessed
|
|
132
|
+
WHERE collection = $collection;
|
|
133
|
+
`;
|
|
134
|
+
try {
|
|
135
|
+
await withSession(async (s) => {
|
|
136
|
+
const settings = createExecuteQuerySettings();
|
|
137
|
+
await s.executeQuery(qry, {
|
|
138
|
+
$collection: TypedValues.utf8(metaKey),
|
|
139
|
+
$last_accessed: TypedValues.timestamp(now),
|
|
140
|
+
}, undefined, settings);
|
|
141
|
+
});
|
|
142
|
+
evictOldestLastAccessEntry();
|
|
143
|
+
lastAccessWriteCache.set(metaKey, nowMs);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
logger.warn({ err, collection: metaKey }, "touchCollectionLastAccess: failed to update last_accessed_at (ignored)");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TypedValues, withSession } from "../ydb/client.js";
|
|
2
2
|
export async function upsertCollectionMeta(metaKey, dim, distance, vectorType, tableName) {
|
|
3
|
+
const now = new Date();
|
|
3
4
|
const upsertMeta = `
|
|
4
5
|
DECLARE $collection AS Utf8;
|
|
5
6
|
DECLARE $table AS Utf8;
|
|
@@ -7,8 +8,9 @@ export async function upsertCollectionMeta(metaKey, dim, distance, vectorType, t
|
|
|
7
8
|
DECLARE $distance AS Utf8;
|
|
8
9
|
DECLARE $vtype AS Utf8;
|
|
9
10
|
DECLARE $created AS Timestamp;
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
DECLARE $last_accessed AS Timestamp;
|
|
12
|
+
UPSERT INTO qdr__collections (collection, table_name, vector_dimension, distance, vector_type, created_at, last_accessed_at)
|
|
13
|
+
VALUES ($collection, $table, $dim, $distance, $vtype, $created, $last_accessed);
|
|
12
14
|
`;
|
|
13
15
|
await withSession(async (s) => {
|
|
14
16
|
await s.executeQuery(upsertMeta, {
|
|
@@ -17,7 +19,8 @@ export async function upsertCollectionMeta(metaKey, dim, distance, vectorType, t
|
|
|
17
19
|
$dim: TypedValues.uint32(dim),
|
|
18
20
|
$distance: TypedValues.utf8(distance),
|
|
19
21
|
$vtype: TypedValues.utf8(vectorType),
|
|
20
|
-
$created: TypedValues.timestamp(
|
|
22
|
+
$created: TypedValues.timestamp(now),
|
|
23
|
+
$last_accessed: TypedValues.timestamp(now),
|
|
21
24
|
});
|
|
22
25
|
});
|
|
23
26
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CreateCollectionReq } from "../types.js";
|
|
2
2
|
import { ensureMetaTable } from "../ydb/schema.js";
|
|
3
|
-
import { createCollection as repoCreateCollection, deleteCollection as repoDeleteCollection, getCollectionMeta, } from "../repositories/collectionsRepo.js";
|
|
3
|
+
import { createCollection as repoCreateCollection, deleteCollection as repoDeleteCollection, getCollectionMeta, touchCollectionLastAccess, } from "../repositories/collectionsRepo.js";
|
|
4
4
|
import { QdrantServiceError } from "./errors.js";
|
|
5
5
|
import { normalizeCollectionContextShared } from "./CollectionService.shared.js";
|
|
6
6
|
export async function putCollectionIndex(ctx) {
|
|
@@ -13,6 +13,7 @@ export async function putCollectionIndex(ctx) {
|
|
|
13
13
|
error: "collection not found",
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
+
await touchCollectionLastAccess(normalized.metaKey);
|
|
16
17
|
return { acknowledged: true };
|
|
17
18
|
}
|
|
18
19
|
export async function createCollection(ctx, body) {
|
|
@@ -33,6 +34,7 @@ export async function createCollection(ctx, body) {
|
|
|
33
34
|
if (existing.dimension === dim &&
|
|
34
35
|
existing.distance === distance &&
|
|
35
36
|
existing.vectorType === vectorType) {
|
|
37
|
+
await touchCollectionLastAccess(normalized.metaKey);
|
|
36
38
|
return { name: normalized.collection, tenant: normalized.tenant };
|
|
37
39
|
}
|
|
38
40
|
const errorMessage = `Collection already exists with different config: dimension=${existing.dimension}, distance=${existing.distance}, type=${existing.vectorType}`;
|
|
@@ -54,6 +56,7 @@ export async function getCollection(ctx) {
|
|
|
54
56
|
error: "collection not found",
|
|
55
57
|
});
|
|
56
58
|
}
|
|
59
|
+
await touchCollectionLastAccess(normalized.metaKey);
|
|
57
60
|
return {
|
|
58
61
|
name: normalized.collection,
|
|
59
62
|
vectors: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export interface NormalizedCollectionContextLike {
|
|
2
2
|
tenant: string;
|
|
3
3
|
collection: string;
|
|
4
|
+
metaKey: string;
|
|
4
5
|
}
|
|
5
6
|
export declare function tableNameFor(tenantId: string, collection: string): string;
|
|
6
7
|
export declare function uidFor(tenantId: string, collection: string): string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sanitizeCollectionName, sanitizeTenantId, metaKeyFor, tableNameFor as tableNameForInternal, uidFor as uidForInternal, hashApiKey,
|
|
1
|
+
import { sanitizeCollectionName, sanitizeTenantId, metaKeyFor, tableNameFor as tableNameForInternal, uidFor as uidForInternal, hashApiKey, normalizeUserAgent, } from "../utils/tenant.js";
|
|
2
2
|
export function tableNameFor(tenantId, collection) {
|
|
3
3
|
return tableNameForInternal(tenantId, collection);
|
|
4
4
|
}
|
|
@@ -8,8 +8,8 @@ export function uidFor(tenantId, collection) {
|
|
|
8
8
|
export function normalizeCollectionContextShared(tenant, collection, apiKey, userAgent) {
|
|
9
9
|
const normalizedTenant = sanitizeTenantId(tenant);
|
|
10
10
|
const apiKeyHash = hashApiKey(apiKey);
|
|
11
|
-
const
|
|
12
|
-
const normalizedCollection = sanitizeCollectionName(collection, apiKeyHash,
|
|
11
|
+
const userAgentNormalized = normalizeUserAgent(userAgent);
|
|
12
|
+
const normalizedCollection = sanitizeCollectionName(collection, apiKeyHash, userAgentNormalized);
|
|
13
13
|
const metaKey = metaKeyFor(normalizedTenant, normalizedCollection);
|
|
14
14
|
return {
|
|
15
15
|
tenant: normalizedTenant,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { UpsertPointsReq, SearchReq, DeletePointsReq } from "../types.js";
|
|
2
2
|
import { ensureMetaTable } from "../ydb/schema.js";
|
|
3
|
-
import { getCollectionMeta } from "../repositories/collectionsRepo.js";
|
|
3
|
+
import { getCollectionMeta, touchCollectionLastAccess, } from "../repositories/collectionsRepo.js";
|
|
4
4
|
import { deletePoints as repoDeletePoints, searchPoints as repoSearchPoints, upsertPoints as repoUpsertPoints, } from "../repositories/pointsRepo.js";
|
|
5
5
|
import { logger } from "../logging/logger.js";
|
|
6
6
|
import { QdrantServiceError, isVectorDimensionMismatchError, } from "./errors.js";
|
|
@@ -44,6 +44,7 @@ export async function upsertPoints(ctx, body) {
|
|
|
44
44
|
}
|
|
45
45
|
throw err;
|
|
46
46
|
}
|
|
47
|
+
await touchCollectionLastAccess(normalized.metaKey);
|
|
47
48
|
return { upserted };
|
|
48
49
|
}
|
|
49
50
|
async function executeSearch(ctx, normalizedSearch, source) {
|
|
@@ -108,6 +109,7 @@ async function executeSearch(ctx, normalizedSearch, source) {
|
|
|
108
109
|
}
|
|
109
110
|
throw err;
|
|
110
111
|
}
|
|
112
|
+
await touchCollectionLastAccess(normalized.metaKey);
|
|
111
113
|
const threshold = normalizedSearch.scoreThreshold;
|
|
112
114
|
// For Cosine, repository hits use distance scores; convert to a
|
|
113
115
|
// similarity-like score so API consumers and IDE thresholds see
|
|
@@ -162,5 +164,6 @@ export async function deletePoints(ctx, body) {
|
|
|
162
164
|
}
|
|
163
165
|
const { tableName, uid } = await resolvePointsTableAndUidOneTable(normalized);
|
|
164
166
|
const deleted = await repoDeletePoints(tableName, parsed.data.points, uid);
|
|
167
|
+
await touchCollectionLastAccess(normalized.metaKey);
|
|
165
168
|
return { deleted };
|
|
166
169
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export type DistanceKind = "Cosine" | "Euclid" | "Dot" | "Manhattan";
|
|
3
3
|
export type VectorType = "float";
|
|
4
|
+
/**
|
|
5
|
+
* Collection metadata from qdr__collections table.
|
|
6
|
+
*
|
|
7
|
+
* @property lastAccessedAt - Timestamp of last access; undefined for collections
|
|
8
|
+
* created before this feature, null if explicitly unset.
|
|
9
|
+
*/
|
|
10
|
+
export interface CollectionMeta {
|
|
11
|
+
table: string;
|
|
12
|
+
dimension: number;
|
|
13
|
+
distance: DistanceKind;
|
|
14
|
+
vectorType: VectorType;
|
|
15
|
+
lastAccessedAt?: Date | null;
|
|
16
|
+
}
|
|
4
17
|
export declare const CreateCollectionReq: z.ZodObject<{
|
|
5
18
|
vectors: z.ZodObject<{
|
|
6
19
|
size: z.ZodNumber;
|
package/dist/utils/tenant.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare function hashApiKey(apiKey: string | undefined): string | undefined;
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function sanitizeCollectionName(name: string, apiKeyHash?: string,
|
|
2
|
+
export declare function normalizeUserAgent(userAgent: string | undefined): string | undefined;
|
|
3
|
+
export declare function sanitizeCollectionName(name: string, apiKeyHash?: string, userAgentNormalized?: string): string;
|
|
4
4
|
export declare function sanitizeTenantId(tenantId: string | undefined): string;
|
|
5
5
|
export declare function tableNameFor(sanitizedTenant: string, sanitizedCollection: string): string;
|
|
6
6
|
export declare function metaKeyFor(sanitizedTenant: string, sanitizedCollection: string): string;
|
package/dist/utils/tenant.js
CHANGED
|
@@ -5,26 +5,41 @@ export function hashApiKey(apiKey) {
|
|
|
5
5
|
const hash = createHash("sha256").update(apiKey).digest("hex");
|
|
6
6
|
return hash.slice(0, 8);
|
|
7
7
|
}
|
|
8
|
-
export function
|
|
8
|
+
export function normalizeUserAgent(userAgent) {
|
|
9
9
|
if (!userAgent || userAgent.trim() === "")
|
|
10
10
|
return undefined;
|
|
11
|
+
let lowered = userAgent
|
|
12
|
+
.trim()
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9_]/g, "_")
|
|
15
|
+
.replace(/_+/g, "_")
|
|
16
|
+
.replace(/^_+|_+$/g, "");
|
|
17
|
+
if (lowered.length === 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
const MAX_LEN = 32;
|
|
20
|
+
if (lowered.length > MAX_LEN) {
|
|
21
|
+
lowered = lowered.slice(0, MAX_LEN).replace(/_+$/g, "");
|
|
22
|
+
}
|
|
23
|
+
if (lowered.length === 0)
|
|
24
|
+
return undefined;
|
|
11
25
|
const hash = createHash("sha256").update(userAgent).digest("hex");
|
|
12
|
-
|
|
26
|
+
const shortHash = hash.slice(0, 8);
|
|
27
|
+
return `${lowered}_${shortHash}`;
|
|
13
28
|
}
|
|
14
|
-
export function sanitizeCollectionName(name, apiKeyHash,
|
|
29
|
+
export function sanitizeCollectionName(name, apiKeyHash, userAgentNormalized) {
|
|
15
30
|
const cleaned = name.replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_");
|
|
16
31
|
const lowered = cleaned.toLowerCase().replace(/^_+/, "");
|
|
17
32
|
const base = lowered.length > 0 ? lowered : "collection";
|
|
18
33
|
const hasApiKey = apiKeyHash !== undefined && apiKeyHash.trim().length > 0;
|
|
19
|
-
const hasUserAgent =
|
|
34
|
+
const hasUserAgent = userAgentNormalized !== undefined && userAgentNormalized.trim().length > 0;
|
|
20
35
|
if (hasApiKey && hasUserAgent) {
|
|
21
|
-
return `${base}_${apiKeyHash}_${
|
|
36
|
+
return `${base}_${apiKeyHash}_${userAgentNormalized}`;
|
|
22
37
|
}
|
|
23
38
|
else if (hasApiKey) {
|
|
24
39
|
return `${base}_${apiKeyHash}`;
|
|
25
40
|
}
|
|
26
41
|
else if (hasUserAgent) {
|
|
27
|
-
return `${base}_${
|
|
42
|
+
return `${base}_${userAgentNormalized}`;
|
|
28
43
|
}
|
|
29
44
|
return base;
|
|
30
45
|
}
|
package/dist/ydb/schema.js
CHANGED
|
@@ -14,13 +14,32 @@ export async function ensureMetaTable() {
|
|
|
14
14
|
await withSession(async (s) => {
|
|
15
15
|
// If table exists, describeTable will succeed
|
|
16
16
|
try {
|
|
17
|
-
await s.describeTable("qdr__collections");
|
|
17
|
+
const tableDescription = await s.describeTable("qdr__collections");
|
|
18
|
+
const columns = tableDescription.columns ?? [];
|
|
19
|
+
const hasLastAccessedAt = columns.some((col) => col.name === "last_accessed_at");
|
|
20
|
+
if (!hasLastAccessedAt) {
|
|
21
|
+
const alterDdl = `
|
|
22
|
+
ALTER TABLE qdr__collections
|
|
23
|
+
ADD COLUMN last_accessed_at Timestamp;
|
|
24
|
+
`;
|
|
25
|
+
// NOTE: ydb-sdk's public TableSession type does not surface executeSchemeQuery,
|
|
26
|
+
// but the underlying implementation provides it. This cast relies on the
|
|
27
|
+
// current ydb-sdk internals (tested with ydb-sdk v5.11.1) to run ALTER TABLE
|
|
28
|
+
// as a scheme query. If the SDK changes its internal API, this may need to be
|
|
29
|
+
// revisited or replaced with an officially supported migration mechanism.
|
|
30
|
+
const rawSession = s;
|
|
31
|
+
await rawSession.api.executeSchemeQuery({
|
|
32
|
+
sessionId: rawSession.sessionId,
|
|
33
|
+
yqlText: alterDdl,
|
|
34
|
+
});
|
|
35
|
+
logger.info("added last_accessed_at column to metadata table qdr__collections");
|
|
36
|
+
}
|
|
18
37
|
return;
|
|
19
38
|
}
|
|
20
39
|
catch {
|
|
21
40
|
// create via schema API
|
|
22
41
|
const desc = new TableDescription()
|
|
23
|
-
.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))
|
|
42
|
+
.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))
|
|
24
43
|
.withPrimaryKey("collection");
|
|
25
44
|
await s.createTable("qdr__collections", desc);
|
|
26
45
|
logger.info("created metadata table qdr__collections");
|
|
@@ -28,7 +47,7 @@ export async function ensureMetaTable() {
|
|
|
28
47
|
});
|
|
29
48
|
}
|
|
30
49
|
catch (err) {
|
|
31
|
-
logger.
|
|
50
|
+
logger.warn({ err }, "ensureMetaTable: failed to verify or migrate qdr__collections; subsequent operations may fail if schema is incomplete");
|
|
32
51
|
}
|
|
33
52
|
}
|
|
34
53
|
export async function ensureGlobalPointsTable() {
|
|
@@ -67,6 +86,11 @@ export async function ensureGlobalPointsTable() {
|
|
|
67
86
|
ALTER TABLE ${GLOBAL_POINTS_TABLE}
|
|
68
87
|
ADD COLUMN embedding_quantized String;
|
|
69
88
|
`;
|
|
89
|
+
// NOTE: Same rationale as in ensureMetaTable: executeSchemeQuery is not part of
|
|
90
|
+
// the public TableSession TypeScript surface, so we reach into the underlying
|
|
91
|
+
// ydb-sdk implementation (verified with ydb-sdk v5.11.1) to apply schema changes.
|
|
92
|
+
// If future SDK versions alter this shape, this cast and migration path must be
|
|
93
|
+
// updated accordingly.
|
|
70
94
|
const rawSession = s;
|
|
71
95
|
await rawSession.api.executeSchemeQuery({
|
|
72
96
|
sessionId: rawSession.sessionId,
|