@utaba/deep-memory-storage-sqlserver 0.1.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/index.cjs ADDED
@@ -0,0 +1,1684 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ SCHEMA_VERSION: () => SCHEMA_VERSION,
34
+ SqlServerSearchProvider: () => SqlServerSearchProvider,
35
+ SqlServerStorageProvider: () => SqlServerStorageProvider,
36
+ getSchemaSQL: () => getSchemaSQL,
37
+ getSearchProcSQL: () => getSearchProcSQL
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/SqlServerStorageProvider.ts
42
+ var import_mssql = __toESM(require("mssql"), 1);
43
+ var import_deep_memory = require("@utaba/deep-memory");
44
+
45
+ // src/schema.ts
46
+ var SCHEMA_VERSION = 1;
47
+ function getSchemaSQL(schema = "dbo") {
48
+ const s = schema;
49
+ return `
50
+ -- ============================================================
51
+ -- @utaba/deep-memory SQL Server schema v${SCHEMA_VERSION}
52
+ -- ============================================================
53
+
54
+ -- Schema version tracking
55
+ IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'dm_meta' AND schema_id = SCHEMA_ID('${s}'))
56
+ BEGIN
57
+ CREATE TABLE [${s}].[dm_meta] (
58
+ [key] NVARCHAR(100) NOT NULL,
59
+ [value] NVARCHAR(MAX) NOT NULL,
60
+ CONSTRAINT [pk_dm_meta] PRIMARY KEY ([key])
61
+ );
62
+ INSERT INTO [${s}].[dm_meta] ([key], [value])
63
+ VALUES ('schema_version', '${SCHEMA_VERSION}');
64
+ END;
65
+
66
+ -- Repositories
67
+ IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'dm_repositories' AND schema_id = SCHEMA_ID('${s}'))
68
+ CREATE TABLE [${s}].[dm_repositories] (
69
+ [repository_id] UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
70
+ [type] NVARCHAR(128) NULL,
71
+ [label] NVARCHAR(500) NOT NULL,
72
+ [description] NVARCHAR(MAX) NULL,
73
+ [legal] NVARCHAR(MAX) NULL,
74
+ [owner] NVARCHAR(500) NULL,
75
+ [governance_config] NVARCHAR(MAX) NOT NULL, -- JSON
76
+ [metadata] NVARCHAR(MAX) NULL, -- JSON
77
+ [created_at] NVARCHAR(50) NOT NULL, -- ISO 8601
78
+ [created_by] NVARCHAR(255) NOT NULL,
79
+ CONSTRAINT [pk_dm_repositories] PRIMARY KEY ([repository_id])
80
+ );
81
+
82
+ -- Vocabularies (one JSON document per repository)
83
+ IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'dm_vocabularies' AND schema_id = SCHEMA_ID('${s}'))
84
+ CREATE TABLE [${s}].[dm_vocabularies] (
85
+ [repository_id] UNIQUEIDENTIFIER NOT NULL,
86
+ [vocabulary] NVARCHAR(MAX) NOT NULL, -- JSON (MemoryVocabulary)
87
+ CONSTRAINT [pk_dm_vocabularies] PRIMARY KEY ([repository_id]),
88
+ CONSTRAINT [fk_dm_vocabularies_repositories]
89
+ FOREIGN KEY ([repository_id]) REFERENCES [${s}].[dm_repositories]([repository_id])
90
+ ON DELETE CASCADE
91
+ );
92
+
93
+ -- Vocabulary change log
94
+ IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'dm_vocabulary_change_log' AND schema_id = SCHEMA_ID('${s}'))
95
+ CREATE TABLE [${s}].[dm_vocabulary_change_log] (
96
+ [change_id] NVARCHAR(128) NOT NULL,
97
+ [repository_id] UNIQUEIDENTIFIER NOT NULL,
98
+ [change_type] NVARCHAR(50) NOT NULL,
99
+ [type_name] NVARCHAR(255) NOT NULL,
100
+ [previous_version] NVARCHAR(50) NULL,
101
+ [new_version] NVARCHAR(50) NOT NULL,
102
+ [proposed_by] NVARCHAR(255) NOT NULL,
103
+ [proposed_at] NVARCHAR(50) NOT NULL, -- ISO 8601
104
+ [approved_by] NVARCHAR(255) NULL,
105
+ [approved_at] NVARCHAR(50) NULL, -- ISO 8601
106
+ [reason] NVARCHAR(MAX) NOT NULL,
107
+ CONSTRAINT [pk_dm_vocabulary_change_log] PRIMARY KEY ([repository_id], [change_id]),
108
+ CONSTRAINT [fk_dm_vocabulary_change_log_repositories]
109
+ FOREIGN KEY ([repository_id]) REFERENCES [${s}].[dm_repositories]([repository_id])
110
+ ON DELETE CASCADE
111
+ );
112
+
113
+ CREATE NONCLUSTERED INDEX [ix_dm_vocabulary_change_log_proposed_at]
114
+ ON [${s}].[dm_vocabulary_change_log] ([repository_id], [proposed_at] DESC);
115
+
116
+ -- Entities
117
+ IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'dm_entities' AND schema_id = SCHEMA_ID('${s}'))
118
+ CREATE TABLE [${s}].[dm_entities] (
119
+ [ft_key] INT NOT NULL IDENTITY(1,1), -- surrogate for FT index
120
+ [entity_id] NVARCHAR(300) NOT NULL,
121
+ [slug] NVARCHAR(300) NOT NULL,
122
+ [repository_id] UNIQUEIDENTIFIER NOT NULL,
123
+ [entity_type] NVARCHAR(255) NOT NULL,
124
+ [label] NVARCHAR(500) NOT NULL,
125
+ [summary] NVARCHAR(MAX) NULL,
126
+ [properties] NVARCHAR(MAX) NOT NULL, -- JSON
127
+ [data] NVARCHAR(MAX) NULL,
128
+ [data_format] NVARCHAR(100) NULL,
129
+ [embedding] NVARCHAR(MAX) NULL, -- JSON array of numbers
130
+ -- Provenance
131
+ [created_by] NVARCHAR(255) NOT NULL,
132
+ [created_by_type] NVARCHAR(10) NOT NULL, -- 'user' | 'agent'
133
+ [created_at] NVARCHAR(50) NOT NULL, -- ISO 8601
134
+ [created_in_conversation] NVARCHAR(255) NULL,
135
+ [created_from_message] NVARCHAR(255) NULL,
136
+ [modified_by] NVARCHAR(255) NOT NULL,
137
+ [modified_by_type] NVARCHAR(10) NOT NULL,
138
+ [modified_at] NVARCHAR(50) NOT NULL, -- ISO 8601
139
+ [modified_in_conversation] NVARCHAR(255) NULL,
140
+ [modified_from_message] NVARCHAR(255) NULL,
141
+ CONSTRAINT [pk_dm_entities] PRIMARY KEY ([repository_id], [entity_id]),
142
+ CONSTRAINT [uq_dm_entities_ft_key] UNIQUE ([ft_key]),
143
+ CONSTRAINT [fk_dm_entities_repositories]
144
+ FOREIGN KEY ([repository_id]) REFERENCES [${s}].[dm_repositories]([repository_id])
145
+ ON DELETE CASCADE
146
+ );
147
+
148
+ CREATE NONCLUSTERED INDEX [ix_dm_entities_type]
149
+ ON [${s}].[dm_entities] ([repository_id], [entity_type]);
150
+ CREATE NONCLUSTERED INDEX [ix_dm_entities_label]
151
+ ON [${s}].[dm_entities] ([repository_id], [label]);
152
+
153
+ CREATE UNIQUE NONCLUSTERED INDEX [ix_dm_entities_slug]
154
+ ON [${s}].[dm_entities] ([repository_id], [slug]);
155
+
156
+ -- Provenance indexes for conversation/actor/temporal queries
157
+ CREATE NONCLUSTERED INDEX [ix_dm_entities_created_conversation]
158
+ ON [${s}].[dm_entities] ([repository_id], [created_in_conversation])
159
+ INCLUDE ([entity_id], [entity_type], [label], [summary]);
160
+
161
+ CREATE NONCLUSTERED INDEX [ix_dm_entities_modified_conversation]
162
+ ON [${s}].[dm_entities] ([repository_id], [modified_in_conversation])
163
+ INCLUDE ([entity_id], [entity_type], [label], [summary]);
164
+
165
+ CREATE NONCLUSTERED INDEX [ix_dm_entities_created_by]
166
+ ON [${s}].[dm_entities] ([repository_id], [created_by])
167
+ INCLUDE ([entity_id], [entity_type], [label], [summary]);
168
+
169
+ CREATE NONCLUSTERED INDEX [ix_dm_entities_modified_at]
170
+ ON [${s}].[dm_entities] ([repository_id], [modified_at] DESC)
171
+ INCLUDE ([entity_id], [entity_type], [label], [summary]);
172
+
173
+ -- Full-text catalog and index on entities
174
+ IF NOT EXISTS (SELECT 1 FROM sys.fulltext_catalogs WHERE name = 'dm_fulltext_catalog')
175
+ CREATE FULLTEXT CATALOG [dm_fulltext_catalog] AS DEFAULT;
176
+
177
+ IF NOT EXISTS (
178
+ SELECT 1 FROM sys.fulltext_indexes fi
179
+ JOIN sys.tables t ON fi.object_id = t.object_id
180
+ WHERE t.name = 'dm_entities' AND t.schema_id = SCHEMA_ID('${s}')
181
+ )
182
+ CREATE FULLTEXT INDEX ON [${s}].[dm_entities] (
183
+ [label] LANGUAGE 1033,
184
+ [summary] LANGUAGE 1033,
185
+ [data] LANGUAGE 1033,
186
+ [properties] LANGUAGE 1033
187
+ )
188
+ KEY INDEX [uq_dm_entities_ft_key]
189
+ ON [dm_fulltext_catalog]
190
+ WITH CHANGE_TRACKING AUTO;
191
+
192
+ -- Relationships
193
+ IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'dm_relationships' AND schema_id = SCHEMA_ID('${s}'))
194
+ CREATE TABLE [${s}].[dm_relationships] (
195
+ [relationship_id] NVARCHAR(300) NOT NULL,
196
+ [repository_id] UNIQUEIDENTIFIER NOT NULL,
197
+ [relationship_type] NVARCHAR(255) NOT NULL,
198
+ [source_entity_id] NVARCHAR(300) NOT NULL,
199
+ [target_entity_id] NVARCHAR(300) NOT NULL,
200
+ [properties] NVARCHAR(MAX) NOT NULL, -- JSON
201
+ [bidirectional] BIT NOT NULL DEFAULT 0,
202
+ -- Provenance
203
+ [created_by] NVARCHAR(255) NOT NULL,
204
+ [created_by_type] NVARCHAR(10) NOT NULL,
205
+ [created_at] NVARCHAR(50) NOT NULL,
206
+ [created_in_conversation] NVARCHAR(255) NULL,
207
+ [created_from_message] NVARCHAR(255) NULL,
208
+ [modified_by] NVARCHAR(255) NOT NULL,
209
+ [modified_by_type] NVARCHAR(10) NOT NULL,
210
+ [modified_at] NVARCHAR(50) NOT NULL,
211
+ [modified_in_conversation] NVARCHAR(255) NULL,
212
+ [modified_from_message] NVARCHAR(255) NULL,
213
+ CONSTRAINT [pk_dm_relationships] PRIMARY KEY ([repository_id], [relationship_id]),
214
+ CONSTRAINT [fk_dm_relationships_repositories]
215
+ FOREIGN KEY ([repository_id]) REFERENCES [${s}].[dm_repositories]([repository_id])
216
+ ON DELETE CASCADE,
217
+ CONSTRAINT [fk_dm_relationships_source]
218
+ FOREIGN KEY ([repository_id], [source_entity_id]) REFERENCES [${s}].[dm_entities]([repository_id], [entity_id]),
219
+ CONSTRAINT [fk_dm_relationships_target]
220
+ FOREIGN KEY ([repository_id], [target_entity_id]) REFERENCES [${s}].[dm_entities]([repository_id], [entity_id])
221
+ );
222
+
223
+ CREATE NONCLUSTERED INDEX [ix_dm_relationships_source]
224
+ ON [${s}].[dm_relationships] ([repository_id], [source_entity_id], [relationship_type])
225
+ INCLUDE ([target_entity_id], [bidirectional]);
226
+ CREATE NONCLUSTERED INDEX [ix_dm_relationships_target]
227
+ ON [${s}].[dm_relationships] ([repository_id], [target_entity_id], [relationship_type])
228
+ INCLUDE ([source_entity_id], [bidirectional]);
229
+ CREATE NONCLUSTERED INDEX [ix_dm_relationships_type]
230
+ ON [${s}].[dm_relationships] ([repository_id], [relationship_type]);
231
+
232
+ -- Relationship provenance indexes
233
+ CREATE NONCLUSTERED INDEX [ix_dm_relationships_created_conversation]
234
+ ON [${s}].[dm_relationships] ([repository_id], [created_in_conversation]);
235
+
236
+ CREATE NONCLUSTERED INDEX [ix_dm_relationships_modified_at]
237
+ ON [${s}].[dm_relationships] ([repository_id], [modified_at] DESC);
238
+
239
+ -- Table-valued parameter type for batch ID lookups (eliminates plan cache bloat)
240
+ IF NOT EXISTS (SELECT 1 FROM sys.types WHERE name = 'dm_id_list' AND is_table_type = 1)
241
+ CREATE TYPE [${s}].[dm_id_list] AS TABLE ([id] NVARCHAR(300) NOT NULL);
242
+ `.trim();
243
+ }
244
+ function getSearchProcSQL(schema = "dbo") {
245
+ const s = schema;
246
+ return `
247
+ CREATE OR ALTER PROCEDURE [${s}].[dm_search_entities]
248
+ @RepositoryId UNIQUEIDENTIFIER,
249
+ @Query NVARCHAR(4000),
250
+ @EntityTypes NVARCHAR(MAX) = NULL, -- JSON array of strings, e.g. '["person","project"]'
251
+ @Limit INT = 20,
252
+ @Offset INT = 0,
253
+ @UseContains BIT = 0 -- 0 = FREETEXTTABLE (natural language), 1 = CONTAINSTABLE (exact/prefix)
254
+ AS
255
+ BEGIN
256
+ SET NOCOUNT ON;
257
+
258
+ -- Parse entity type filter from JSON array into a temp table
259
+ CREATE TABLE #entity_types ([type_name] NVARCHAR(255));
260
+ IF @EntityTypes IS NOT NULL AND @EntityTypes <> '[]'
261
+ BEGIN
262
+ INSERT INTO #entity_types ([type_name])
263
+ SELECT TRIM(value) FROM OPENJSON(@EntityTypes);
264
+ END;
265
+
266
+ DECLARE @HasTypeFilter BIT = CASE WHEN EXISTS (SELECT 1 FROM #entity_types) THEN 1 ELSE 0 END;
267
+
268
+ -- CTE: ranked full-text matches
269
+ ;WITH ft_results AS (
270
+ SELECT
271
+ e.[entity_id],
272
+ e.[slug],
273
+ e.[entity_type],
274
+ e.[label],
275
+ e.[summary],
276
+ e.[data],
277
+ e.[properties],
278
+ ft.[RANK] AS ft_rank
279
+ FROM (
280
+ SELECT [KEY], [RANK]
281
+ FROM FREETEXTTABLE([${s}].[dm_entities], ([label], [summary], [data], [properties]), @Query)
282
+ WHERE @UseContains = 0
283
+ UNION ALL
284
+ SELECT [KEY], [RANK]
285
+ FROM CONTAINSTABLE([${s}].[dm_entities], ([label], [summary], [data], [properties]), @Query)
286
+ WHERE @UseContains = 1
287
+ ) AS ft
288
+ INNER JOIN [${s}].[dm_entities] e ON e.[ft_key] = ft.[KEY]
289
+ WHERE e.[repository_id] = @RepositoryId
290
+ AND (@HasTypeFilter = 0 OR e.[entity_type] IN (SELECT [type_name] FROM #entity_types))
291
+ ),
292
+ -- Count total matching rows before pagination
293
+ ft_counted AS (
294
+ SELECT *, COUNT(*) OVER () AS total_count FROM ft_results
295
+ )
296
+ SELECT
297
+ [entity_id],
298
+ [slug],
299
+ [entity_type],
300
+ [label],
301
+ [summary],
302
+ [data],
303
+ [properties],
304
+ [ft_rank],
305
+ [total_count],
306
+ -- Highlight snippets: return the first 200 chars of each matched field
307
+ -- (the application layer will do finer-grained highlighting using the query terms)
308
+ CASE WHEN FREETEXT([label], @Query) THEN LEFT([label], 200) ELSE NULL END AS [hl_label],
309
+ CASE WHEN FREETEXT([summary], @Query) THEN LEFT(CAST([summary] AS NVARCHAR(200)), 200) ELSE NULL END AS [hl_summary],
310
+ CASE WHEN FREETEXT([data], @Query) THEN LEFT(CAST([data] AS NVARCHAR(200)), 200) ELSE NULL END AS [hl_data],
311
+ CASE WHEN FREETEXT([properties], @Query) THEN LEFT(CAST([properties] AS NVARCHAR(200)), 200) ELSE NULL END AS [hl_properties]
312
+ FROM ft_counted
313
+ ORDER BY [ft_rank] DESC
314
+ OFFSET @Offset ROWS FETCH NEXT @Limit ROWS ONLY;
315
+
316
+ DROP TABLE #entity_types;
317
+ END;
318
+ `.trim();
319
+ }
320
+
321
+ // src/SqlServerStorageProvider.ts
322
+ var ENTITY_COLS_LIGHT = [
323
+ "entity_id",
324
+ "slug",
325
+ "repository_id",
326
+ "entity_type",
327
+ "label",
328
+ "summary",
329
+ "properties",
330
+ "data",
331
+ "data_format",
332
+ "created_by",
333
+ "created_by_type",
334
+ "created_at",
335
+ "created_in_conversation",
336
+ "created_from_message",
337
+ "modified_by",
338
+ "modified_by_type",
339
+ "modified_at",
340
+ "modified_in_conversation",
341
+ "modified_from_message"
342
+ ].map((c) => `[${c}]`).join(", ");
343
+ var ENTITY_COLS_FULL = `${ENTITY_COLS_LIGHT}, [embedding]`;
344
+ function provenanceFromRow(row) {
345
+ const r = row;
346
+ return {
347
+ createdBy: r["created_by"],
348
+ createdByType: r["created_by_type"],
349
+ createdAt: r["created_at"],
350
+ createdInConversation: r["created_in_conversation"] || void 0,
351
+ createdFromMessage: r["created_from_message"] || void 0,
352
+ modifiedBy: r["modified_by"],
353
+ modifiedByType: r["modified_by_type"],
354
+ modifiedAt: r["modified_at"],
355
+ modifiedInConversation: r["modified_in_conversation"] || void 0,
356
+ modifiedFromMessage: r["modified_from_message"] || void 0
357
+ };
358
+ }
359
+ function addProvenanceConditions(req, prov, conditions, prefix) {
360
+ if (prov.conversationIds && prov.conversationIds.length > 0) {
361
+ const placeholders = prov.conversationIds.map((id, i) => {
362
+ req.input(`${prefix}ConvId${i}`, import_mssql.default.NVarChar, id);
363
+ return `@${prefix}ConvId${i}`;
364
+ });
365
+ const inClause = placeholders.join(",");
366
+ conditions.push(`([created_in_conversation] IN (${inClause}) OR [modified_in_conversation] IN (${inClause}))`);
367
+ }
368
+ if (prov.actors && prov.actors.length > 0) {
369
+ const placeholders = prov.actors.map((a, i) => {
370
+ req.input(`${prefix}Actor${i}`, import_mssql.default.NVarChar, a);
371
+ return `@${prefix}Actor${i}`;
372
+ });
373
+ const inClause = placeholders.join(",");
374
+ conditions.push(`([created_by] IN (${inClause}) OR [modified_by] IN (${inClause}))`);
375
+ }
376
+ if (prov.dateRange) {
377
+ req.input(`${prefix}DateFrom`, import_mssql.default.NVarChar, prov.dateRange.from);
378
+ req.input(`${prefix}DateTo`, import_mssql.default.NVarChar, prov.dateRange.to);
379
+ conditions.push(`([created_at] >= @${prefix}DateFrom AND [created_at] <= @${prefix}DateTo) OR ([modified_at] >= @${prefix}DateFrom AND [modified_at] <= @${prefix}DateTo)`);
380
+ }
381
+ }
382
+ function entityFromRow(row) {
383
+ const r = row;
384
+ return {
385
+ id: r["entity_id"],
386
+ slug: r["slug"],
387
+ entityType: r["entity_type"],
388
+ label: r["label"],
389
+ summary: r["summary"] || void 0,
390
+ properties: JSON.parse(r["properties"] || "{}"),
391
+ data: r["data"] || void 0,
392
+ dataFormat: r["data_format"] || void 0,
393
+ provenance: provenanceFromRow(row),
394
+ embedding: "embedding" in r && r["embedding"] ? JSON.parse(r["embedding"]) : void 0
395
+ };
396
+ }
397
+ function relationshipFromRow(row) {
398
+ const r = row;
399
+ return {
400
+ id: r["relationship_id"],
401
+ relationshipType: r["relationship_type"],
402
+ sourceEntityId: r["source_entity_id"],
403
+ targetEntityId: r["target_entity_id"],
404
+ properties: JSON.parse(r["properties"] || "{}"),
405
+ bidirectional: r["bidirectional"] === true || r["bidirectional"] === 1,
406
+ provenance: provenanceFromRow(row)
407
+ };
408
+ }
409
+ function changeRecordFromRow(row) {
410
+ const r = row;
411
+ return {
412
+ changeId: r["change_id"],
413
+ changeType: r["change_type"],
414
+ typeName: r["type_name"],
415
+ previousVersion: r["previous_version"] || void 0,
416
+ newVersion: r["new_version"],
417
+ proposedBy: r["proposed_by"],
418
+ proposedAt: r["proposed_at"],
419
+ approvedBy: r["approved_by"] || void 0,
420
+ approvedAt: r["approved_at"] || void 0,
421
+ reason: r["reason"]
422
+ };
423
+ }
424
+ var SqlServerStorageProvider = class {
425
+ pool = null;
426
+ ownsPool;
427
+ config;
428
+ schema;
429
+ constructor(config) {
430
+ this.config = config;
431
+ this.schema = config.schema ?? "dbo";
432
+ this.ownsPool = !(config.connection instanceof import_mssql.default.ConnectionPool);
433
+ }
434
+ t(table) {
435
+ return `[${this.schema}].[${table}]`;
436
+ }
437
+ getPool() {
438
+ if (!this.pool) {
439
+ throw new import_deep_memory.ProviderError(
440
+ "SqlServerStorageProvider not initialised. Call initialise() first."
441
+ );
442
+ }
443
+ return this.pool;
444
+ }
445
+ // ─── Lifecycle ────────────────────────────────────────────────────
446
+ async initialise() {
447
+ if (this.config.connection instanceof import_mssql.default.ConnectionPool) {
448
+ this.pool = this.config.connection;
449
+ this.ownsPool = false;
450
+ if (!this.pool.connected) {
451
+ await this.pool.connect();
452
+ }
453
+ } else {
454
+ this.pool = new import_mssql.default.ConnectionPool(this.config.connection);
455
+ this.ownsPool = true;
456
+ await this.pool.connect();
457
+ }
458
+ }
459
+ async dispose() {
460
+ if (this.ownsPool && this.pool) {
461
+ await this.pool.close();
462
+ }
463
+ this.pool = null;
464
+ }
465
+ async ensureSchema() {
466
+ const pool = this.getPool();
467
+ const metaCheck = await pool.request().query(
468
+ `SELECT COUNT(*) AS cnt FROM sys.tables WHERE name = 'dm_meta' AND schema_id = SCHEMA_ID('${this.schema}')`
469
+ );
470
+ const metaExists = (metaCheck.recordset[0]?.cnt ?? 0) > 0;
471
+ if (metaExists) {
472
+ const versionResult = await pool.request().query(
473
+ `SELECT [value] FROM ${this.t("dm_meta")} WHERE [key] = 'schema_version'`
474
+ );
475
+ const currentVersion = parseInt(versionResult.recordset[0]?.value ?? "0", 10);
476
+ if (currentVersion > SCHEMA_VERSION) {
477
+ throw new import_deep_memory.ProviderError(
478
+ `Database schema version ${currentVersion} is newer than provider version ${SCHEMA_VERSION}. Update the provider package.`
479
+ );
480
+ }
481
+ if (currentVersion === SCHEMA_VERSION) {
482
+ return;
483
+ }
484
+ }
485
+ const ddl = getSchemaSQL(this.schema);
486
+ const statements = ddl.split(/\n\n+/).map((s) => s.split("\n").filter((line) => !line.trimStart().startsWith("--")).join("\n").trim()).filter((s) => s.length > 0);
487
+ for (const stmt of statements) {
488
+ try {
489
+ await pool.request().query(stmt);
490
+ } catch (err) {
491
+ const msg = err instanceof Error ? err.message : String(err);
492
+ if (!msg.includes("already exists")) {
493
+ throw new import_deep_memory.ProviderError(`Schema creation failed: ${msg}`);
494
+ }
495
+ }
496
+ }
497
+ }
498
+ // ─── Repository ──────────────────────────────────────────────────
499
+ async createRepository(config) {
500
+ const pool = this.getPool();
501
+ const existing = await pool.request().input("id", import_mssql.default.UniqueIdentifier, config.repositoryId).query(
502
+ `SELECT [repository_id] FROM ${this.t("dm_repositories")} WHERE [repository_id] = @id`
503
+ );
504
+ if (existing.recordset.length > 0) {
505
+ throw new import_deep_memory.DuplicateRepositoryError(config.repositoryId);
506
+ }
507
+ await pool.request().input("id", import_mssql.default.UniqueIdentifier, config.repositoryId).input("type", import_mssql.default.NVarChar, config.type ?? null).input("label", import_mssql.default.NVarChar, config.label).input("description", import_mssql.default.NVarChar, config.description ?? null).input("legal", import_mssql.default.NVarChar, config.legal ?? null).input("owner", import_mssql.default.NVarChar, config.owner ?? null).input("governanceConfig", import_mssql.default.NVarChar, JSON.stringify(config.governanceConfig)).input("metadata", import_mssql.default.NVarChar, config.metadata ? JSON.stringify(config.metadata) : null).input("createdAt", import_mssql.default.NVarChar, config.createdAt).input("createdBy", import_mssql.default.NVarChar, config.createdBy).query(`
508
+ INSERT INTO ${this.t("dm_repositories")}
509
+ ([repository_id], [type], [label], [description], [legal], [owner], [governance_config], [metadata], [created_at], [created_by])
510
+ VALUES (@id, @type, @label, @description, @legal, @owner, @governanceConfig, @metadata, @createdAt, @createdBy)
511
+ `);
512
+ const emptyVocabulary = {
513
+ version: "0.0.0",
514
+ lastModified: config.createdAt,
515
+ modifiedBy: config.createdBy,
516
+ entityTypes: [],
517
+ relationshipTypes: []
518
+ };
519
+ await pool.request().input("id", import_mssql.default.UniqueIdentifier, config.repositoryId).input("vocabulary", import_mssql.default.NVarChar, JSON.stringify(emptyVocabulary)).query(`
520
+ INSERT INTO ${this.t("dm_vocabularies")} ([repository_id], [vocabulary])
521
+ VALUES (@id, @vocabulary)
522
+ `);
523
+ return {
524
+ repositoryId: config.repositoryId,
525
+ type: config.type,
526
+ label: config.label,
527
+ description: config.description,
528
+ legal: config.legal,
529
+ owner: config.owner,
530
+ governanceConfig: config.governanceConfig,
531
+ metadata: config.metadata,
532
+ createdAt: config.createdAt,
533
+ createdBy: config.createdBy
534
+ };
535
+ }
536
+ async getRepository(repositoryId) {
537
+ const pool = this.getPool();
538
+ const result = await pool.request().input("id", import_mssql.default.UniqueIdentifier, repositoryId).query(
539
+ `SELECT * FROM ${this.t("dm_repositories")} WHERE [repository_id] = @id`
540
+ );
541
+ const row = result.recordset[0];
542
+ if (!row) return null;
543
+ const metadataRaw = row["metadata"];
544
+ return {
545
+ repositoryId: row["repository_id"],
546
+ type: row["type"] || void 0,
547
+ label: row["label"],
548
+ description: row["description"] || void 0,
549
+ legal: row["legal"] || void 0,
550
+ owner: row["owner"] || void 0,
551
+ governanceConfig: JSON.parse(row["governance_config"]),
552
+ metadata: metadataRaw ? JSON.parse(metadataRaw) : void 0,
553
+ createdAt: row["created_at"],
554
+ createdBy: row["created_by"]
555
+ };
556
+ }
557
+ async listRepositories(filter) {
558
+ const pool = this.getPool();
559
+ const offset = filter?.offset ?? 0;
560
+ const limit = filter?.limit ?? 20;
561
+ const countReq = pool.request();
562
+ const dataReq = pool.request();
563
+ let where = "";
564
+ if (filter?.type) {
565
+ countReq.input("type", import_mssql.default.NVarChar, filter.type);
566
+ dataReq.input("type", import_mssql.default.NVarChar, filter.type);
567
+ where = "WHERE [type] = @type";
568
+ }
569
+ const countResult = await countReq.query(
570
+ `SELECT COUNT(*) AS [cnt] FROM ${this.t("dm_repositories")} ${where}`
571
+ );
572
+ const total = countResult.recordset[0]?.["cnt"] ?? 0;
573
+ dataReq.input("offset", import_mssql.default.Int, offset);
574
+ dataReq.input("limit", import_mssql.default.Int, limit);
575
+ const result = await dataReq.query(
576
+ `SELECT [repository_id], [type], [label], [description], [governance_config]
577
+ FROM ${this.t("dm_repositories")} ${where}
578
+ ORDER BY [label]
579
+ OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY`
580
+ );
581
+ const items = result.recordset.map((row) => ({
582
+ repositoryId: row["repository_id"],
583
+ type: row["type"] || void 0,
584
+ label: row["label"],
585
+ description: row["description"] || void 0,
586
+ governanceConfig: JSON.parse(row["governance_config"])
587
+ }));
588
+ return {
589
+ items,
590
+ total,
591
+ hasMore: offset + items.length < total,
592
+ limit,
593
+ offset
594
+ };
595
+ }
596
+ async updateRepository(repositoryId, updates) {
597
+ const pool = this.getPool();
598
+ const setClauses = [];
599
+ const request = pool.request().input("id", import_mssql.default.UniqueIdentifier, repositoryId);
600
+ if (updates.label !== void 0) {
601
+ setClauses.push("[label] = @label");
602
+ request.input("label", import_mssql.default.NVarChar, updates.label);
603
+ }
604
+ if (updates.description !== void 0) {
605
+ setClauses.push("[description] = @description");
606
+ request.input("description", import_mssql.default.NVarChar, updates.description);
607
+ }
608
+ if (updates.type !== void 0) {
609
+ setClauses.push("[type] = @type");
610
+ request.input("type", import_mssql.default.NVarChar, updates.type);
611
+ }
612
+ if (updates.legal !== void 0) {
613
+ setClauses.push("[legal] = @legal");
614
+ request.input("legal", import_mssql.default.NVarChar, updates.legal);
615
+ }
616
+ if (updates.owner !== void 0) {
617
+ setClauses.push("[owner] = @owner");
618
+ request.input("owner", import_mssql.default.NVarChar, updates.owner);
619
+ }
620
+ if (updates.governanceConfig !== void 0) {
621
+ setClauses.push("[governance_config] = @governanceConfig");
622
+ request.input("governanceConfig", import_mssql.default.NVarChar, JSON.stringify(updates.governanceConfig));
623
+ }
624
+ if (updates.metadata !== void 0) {
625
+ const existing = await this.getRepository(repositoryId);
626
+ if (!existing) throw new import_deep_memory.RepositoryNotFoundError(repositoryId);
627
+ const merged = { ...existing.metadata, ...updates.metadata };
628
+ setClauses.push("[metadata] = @metadata");
629
+ request.input("metadata", import_mssql.default.NVarChar, JSON.stringify(merged));
630
+ }
631
+ if (setClauses.length === 0) {
632
+ const existing = await this.getRepository(repositoryId);
633
+ if (!existing) throw new import_deep_memory.RepositoryNotFoundError(repositoryId);
634
+ return existing;
635
+ }
636
+ const result = await request.query(
637
+ `UPDATE ${this.t("dm_repositories")} SET ${setClauses.join(", ")} WHERE [repository_id] = @id`
638
+ );
639
+ if (result.rowsAffected[0] === 0) {
640
+ throw new import_deep_memory.RepositoryNotFoundError(repositoryId);
641
+ }
642
+ const updated = await this.getRepository(repositoryId);
643
+ if (!updated) throw new import_deep_memory.RepositoryNotFoundError(repositoryId);
644
+ return updated;
645
+ }
646
+ async deleteRepository(repositoryId) {
647
+ const pool = this.getPool();
648
+ const result = await pool.request().input("id", import_mssql.default.UniqueIdentifier, repositoryId).query(`DELETE FROM ${this.t("dm_repositories")} WHERE [repository_id] = @id`);
649
+ if (result.rowsAffected[0] === 0) {
650
+ throw new import_deep_memory.RepositoryNotFoundError(repositoryId);
651
+ }
652
+ }
653
+ async getRepositoryStats(repositoryId) {
654
+ await this.assertRepository(repositoryId);
655
+ const pool = this.getPool();
656
+ const [entityStats, relStats, vocab] = await Promise.all([
657
+ pool.request().input("id", import_mssql.default.UniqueIdentifier, repositoryId).query(
658
+ `SELECT [entity_type], COUNT(*) AS cnt
659
+ FROM ${this.t("dm_entities")} WHERE [repository_id] = @id
660
+ GROUP BY [entity_type]`
661
+ ),
662
+ pool.request().input("id", import_mssql.default.UniqueIdentifier, repositoryId).query(
663
+ `SELECT [relationship_type], COUNT(*) AS cnt
664
+ FROM ${this.t("dm_relationships")} WHERE [repository_id] = @id
665
+ GROUP BY [relationship_type]`
666
+ ),
667
+ this.getVocabulary(repositoryId)
668
+ ]);
669
+ const entityTypeBreakdown = {};
670
+ let entityCount = 0;
671
+ for (const row of entityStats.recordset) {
672
+ entityTypeBreakdown[row.entity_type] = row.cnt;
673
+ entityCount += row.cnt;
674
+ }
675
+ const relationshipTypeBreakdown = {};
676
+ let relationshipCount = 0;
677
+ for (const row of relStats.recordset) {
678
+ relationshipTypeBreakdown[row.relationship_type] = row.cnt;
679
+ relationshipCount += row.cnt;
680
+ }
681
+ return {
682
+ entityCount,
683
+ relationshipCount,
684
+ vocabularyVersion: vocab.version,
685
+ entityTypeBreakdown,
686
+ relationshipTypeBreakdown
687
+ };
688
+ }
689
+ // ─── Vocabulary ──────────────────────────────────────────────────
690
+ async getVocabulary(repositoryId) {
691
+ await this.assertRepository(repositoryId);
692
+ const pool = this.getPool();
693
+ const result = await pool.request().input("id", import_mssql.default.UniqueIdentifier, repositoryId).query(
694
+ `SELECT [vocabulary] FROM ${this.t("dm_vocabularies")} WHERE [repository_id] = @id`
695
+ );
696
+ const row = result.recordset[0];
697
+ if (!row) {
698
+ return {
699
+ version: "0.0.0",
700
+ lastModified: (/* @__PURE__ */ new Date()).toISOString(),
701
+ modifiedBy: "system",
702
+ entityTypes: [],
703
+ relationshipTypes: []
704
+ };
705
+ }
706
+ return JSON.parse(row.vocabulary);
707
+ }
708
+ async saveVocabulary(repositoryId, vocabulary) {
709
+ await this.assertRepository(repositoryId);
710
+ const pool = this.getPool();
711
+ await pool.request().input("id", import_mssql.default.UniqueIdentifier, repositoryId).input("vocabulary", import_mssql.default.NVarChar, JSON.stringify(vocabulary)).query(`
712
+ UPDATE ${this.t("dm_vocabularies")}
713
+ SET [vocabulary] = @vocabulary
714
+ WHERE [repository_id] = @id
715
+ `);
716
+ }
717
+ async getVocabularyChangeLog(repositoryId, options) {
718
+ await this.assertRepository(repositoryId);
719
+ const pool = this.getPool();
720
+ const limit = options?.limit ?? 10;
721
+ const offset = options?.offset ?? 0;
722
+ const countResult = await pool.request().input("id", import_mssql.default.UniqueIdentifier, repositoryId).query(
723
+ `SELECT COUNT(*) AS cnt FROM ${this.t("dm_vocabulary_change_log")} WHERE [repository_id] = @id`
724
+ );
725
+ const total = countResult.recordset[0]?.cnt ?? 0;
726
+ const result = await pool.request().input("id", import_mssql.default.UniqueIdentifier, repositoryId).input("limit", import_mssql.default.Int, limit).input("offset", import_mssql.default.Int, offset).query(
727
+ `SELECT * FROM ${this.t("dm_vocabulary_change_log")}
728
+ WHERE [repository_id] = @id
729
+ ORDER BY [proposed_at] DESC
730
+ OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY`
731
+ );
732
+ return {
733
+ items: result.recordset.map(changeRecordFromRow),
734
+ total,
735
+ hasMore: offset + limit < total,
736
+ limit,
737
+ offset
738
+ };
739
+ }
740
+ // ─── Entities ────────────────────────────────────────────────────
741
+ async createEntity(repositoryId, entity) {
742
+ await this.assertRepository(repositoryId);
743
+ const pool = this.getPool();
744
+ const existing = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityId", import_mssql.default.NVarChar, entity.id).query(
745
+ `SELECT [entity_id] FROM ${this.t("dm_entities")}
746
+ WHERE [repository_id] = @repoId AND [entity_id] = @entityId`
747
+ );
748
+ if (existing.recordset.length > 0) {
749
+ throw new import_deep_memory.DuplicateEntityError(entity.id);
750
+ }
751
+ const req = pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityId", import_mssql.default.NVarChar, entity.id).input("slug", import_mssql.default.NVarChar, entity.slug).input("entityType", import_mssql.default.NVarChar, entity.entityType).input("label", import_mssql.default.NVarChar, entity.label).input("summary", import_mssql.default.NVarChar, entity.summary ?? null).input("properties", import_mssql.default.NVarChar, JSON.stringify(entity.properties)).input("data", import_mssql.default.NVarChar, entity.data ?? null).input("dataFormat", import_mssql.default.NVarChar, entity.dataFormat ?? null).input("embedding", import_mssql.default.NVarChar, entity.embedding ? JSON.stringify(entity.embedding) : null);
752
+ this.addProvenanceInputs(req, entity.provenance);
753
+ await req.query(`
754
+ INSERT INTO ${this.t("dm_entities")} (
755
+ [repository_id], [entity_id], [slug], [entity_type], [label], [summary],
756
+ [properties], [data], [data_format], [embedding],
757
+ [created_by], [created_by_type], [created_at],
758
+ [created_in_conversation], [created_from_message],
759
+ [modified_by], [modified_by_type], [modified_at],
760
+ [modified_in_conversation], [modified_from_message]
761
+ ) VALUES (
762
+ @repoId, @entityId, @slug, @entityType, @label, @summary,
763
+ @properties, @data, @dataFormat, @embedding,
764
+ @createdBy, @createdByType, @createdAt,
765
+ @createdInConversation, @createdFromMessage,
766
+ @modifiedBy, @modifiedByType, @modifiedAt,
767
+ @modifiedInConversation, @modifiedFromMessage
768
+ )
769
+ `);
770
+ return entity;
771
+ }
772
+ async getEntity(repositoryId, entityId) {
773
+ const pool = this.getPool();
774
+ const result = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityId", import_mssql.default.NVarChar, entityId).query(
775
+ `SELECT ${ENTITY_COLS_LIGHT} FROM ${this.t("dm_entities")}
776
+ WHERE [repository_id] = @repoId AND [entity_id] = @entityId`
777
+ );
778
+ const row = result.recordset[0];
779
+ if (!row) return null;
780
+ return entityFromRow(row);
781
+ }
782
+ async getEntityBySlug(repositoryId, slug) {
783
+ const pool = this.getPool();
784
+ const result = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("slug", import_mssql.default.NVarChar, slug).query(
785
+ `SELECT ${ENTITY_COLS_LIGHT} FROM ${this.t("dm_entities")}
786
+ WHERE [repository_id] = @repoId AND [slug] = @slug`
787
+ );
788
+ const row = result.recordset[0];
789
+ if (!row) return null;
790
+ return entityFromRow(row);
791
+ }
792
+ async getEntities(repositoryId, entityIds) {
793
+ const pool = this.getPool();
794
+ const result = /* @__PURE__ */ new Map();
795
+ if (entityIds.length === 0) return result;
796
+ const tvp = this.createIdListTvp(entityIds);
797
+ const rows = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityIds", tvp).query(
798
+ `SELECT ${ENTITY_COLS_LIGHT} FROM ${this.t("dm_entities")}
799
+ WHERE [repository_id] = @repoId
800
+ AND [entity_id] IN (SELECT [id] FROM @entityIds)`
801
+ );
802
+ for (const row of rows.recordset) {
803
+ const entity = entityFromRow(row);
804
+ result.set(entity.id, entity);
805
+ }
806
+ return result;
807
+ }
808
+ async updateEntity(repositoryId, entityId, updates) {
809
+ const pool = this.getPool();
810
+ const existing = await this.getEntity(repositoryId, entityId);
811
+ if (!existing) {
812
+ throw new import_deep_memory.EntityNotFoundError(entityId);
813
+ }
814
+ const updated = {
815
+ ...existing,
816
+ label: updates.label ?? existing.label,
817
+ slug: updates.slug ?? existing.slug,
818
+ summary: updates.summary !== void 0 ? updates.summary : existing.summary,
819
+ properties: updates.properties ?? existing.properties,
820
+ data: updates.data !== void 0 ? updates.data : existing.data,
821
+ dataFormat: updates.dataFormat !== void 0 ? updates.dataFormat : existing.dataFormat,
822
+ provenance: updates.provenance,
823
+ embedding: updates.embedding ?? existing.embedding
824
+ };
825
+ const req = pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityId", import_mssql.default.NVarChar, entityId).input("slug", import_mssql.default.NVarChar, updated.slug).input("label", import_mssql.default.NVarChar, updated.label).input("summary", import_mssql.default.NVarChar, updated.summary ?? null).input("properties", import_mssql.default.NVarChar, JSON.stringify(updated.properties)).input("data", import_mssql.default.NVarChar, updated.data ?? null).input("dataFormat", import_mssql.default.NVarChar, updated.dataFormat ?? null).input("embedding", import_mssql.default.NVarChar, updated.embedding ? JSON.stringify(updated.embedding) : null).input("modifiedBy", import_mssql.default.NVarChar, updates.provenance.modifiedBy).input("modifiedByType", import_mssql.default.NVarChar, updates.provenance.modifiedByType).input("modifiedAt", import_mssql.default.NVarChar, updates.provenance.modifiedAt).input("modifiedInConversation", import_mssql.default.NVarChar, updates.provenance.modifiedInConversation ?? null).input("modifiedFromMessage", import_mssql.default.NVarChar, updates.provenance.modifiedFromMessage ?? null);
826
+ await req.query(`
827
+ UPDATE ${this.t("dm_entities")} SET
828
+ [slug] = @slug,
829
+ [label] = @label,
830
+ [summary] = @summary,
831
+ [properties] = @properties,
832
+ [data] = @data,
833
+ [data_format] = @dataFormat,
834
+ [embedding] = @embedding,
835
+ [modified_by] = @modifiedBy,
836
+ [modified_by_type] = @modifiedByType,
837
+ [modified_at] = @modifiedAt,
838
+ [modified_in_conversation] = @modifiedInConversation,
839
+ [modified_from_message] = @modifiedFromMessage
840
+ WHERE [repository_id] = @repoId AND [entity_id] = @entityId
841
+ `);
842
+ return updated;
843
+ }
844
+ async deleteEntity(repositoryId, entityId) {
845
+ const pool = this.getPool();
846
+ const delReq = pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityId", import_mssql.default.NVarChar, entityId);
847
+ await delReq.query(`
848
+ DELETE FROM ${this.t("dm_relationships")}
849
+ WHERE [repository_id] = @repoId AND [source_entity_id] = @entityId;
850
+ DELETE FROM ${this.t("dm_relationships")}
851
+ WHERE [repository_id] = @repoId AND [target_entity_id] = @entityId;
852
+ `);
853
+ const result = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityId", import_mssql.default.NVarChar, entityId).query(
854
+ `DELETE FROM ${this.t("dm_entities")}
855
+ WHERE [repository_id] = @repoId AND [entity_id] = @entityId`
856
+ );
857
+ if (result.rowsAffected[0] === 0) {
858
+ throw new import_deep_memory.EntityNotFoundError(entityId);
859
+ }
860
+ }
861
+ async deleteEntitiesByType(repositoryId, entityType) {
862
+ await this.assertRepository(repositoryId);
863
+ const pool = this.getPool();
864
+ const srcResult = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityType", import_mssql.default.NVarChar, entityType).query(`
865
+ DELETE r FROM ${this.t("dm_relationships")} r
866
+ INNER JOIN ${this.t("dm_entities")} e
867
+ ON r.[repository_id] = e.[repository_id]
868
+ AND r.[source_entity_id] = e.[entity_id]
869
+ WHERE e.[repository_id] = @repoId AND e.[entity_type] = @entityType
870
+ `);
871
+ const tgtResult = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityType", import_mssql.default.NVarChar, entityType).query(`
872
+ DELETE r FROM ${this.t("dm_relationships")} r
873
+ INNER JOIN ${this.t("dm_entities")} e
874
+ ON r.[repository_id] = e.[repository_id]
875
+ AND r.[target_entity_id] = e.[entity_id]
876
+ WHERE e.[repository_id] = @repoId AND e.[entity_type] = @entityType
877
+ `);
878
+ const deletedRelationships = (srcResult.rowsAffected[0] ?? 0) + (tgtResult.rowsAffected[0] ?? 0);
879
+ const entResult = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityType", import_mssql.default.NVarChar, entityType).query(
880
+ `DELETE FROM ${this.t("dm_entities")}
881
+ WHERE [repository_id] = @repoId AND [entity_type] = @entityType`
882
+ );
883
+ const deletedEntities = entResult.rowsAffected[0] ?? 0;
884
+ return { deletedEntities, deletedRelationships };
885
+ }
886
+ async findEntities(repositoryId, query) {
887
+ const pool = this.getPool();
888
+ const req = pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId);
889
+ const conditions = ["[repository_id] = @repoId"];
890
+ if (query.entityTypes && query.entityTypes.length > 0) {
891
+ const typePlaceholders = query.entityTypes.map((t, i) => {
892
+ req.input(`et${i}`, import_mssql.default.NVarChar, t);
893
+ return `@et${i}`;
894
+ });
895
+ conditions.push(`[entity_type] IN (${typePlaceholders.join(",")})`);
896
+ }
897
+ if (query.searchTerm) {
898
+ req.input("searchTerm", import_mssql.default.NVarChar, `%${query.searchTerm}%`);
899
+ conditions.push(`([label] LIKE @searchTerm OR [summary] LIKE @searchTerm)`);
900
+ }
901
+ if (query.properties) {
902
+ const entries = Object.entries(query.properties);
903
+ for (let i = 0; i < entries.length; i++) {
904
+ const [key, value] = entries[i];
905
+ req.input(`propKey${i}`, import_mssql.default.NVarChar, `$.${key}`);
906
+ req.input(`propVal${i}`, import_mssql.default.NVarChar, String(value));
907
+ conditions.push(`JSON_VALUE([properties], @propKey${i}) = @propVal${i}`);
908
+ }
909
+ }
910
+ if (query.provenance) {
911
+ addProvenanceConditions(req, query.provenance, conditions, "data");
912
+ }
913
+ const where = conditions.join(" AND ");
914
+ const countReq = pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId);
915
+ const countConditions = ["[repository_id] = @repoId"];
916
+ if (query.entityTypes && query.entityTypes.length > 0) {
917
+ const typePlaceholders = query.entityTypes.map((t, i) => {
918
+ countReq.input(`et${i}`, import_mssql.default.NVarChar, t);
919
+ return `@et${i}`;
920
+ });
921
+ countConditions.push(`[entity_type] IN (${typePlaceholders.join(",")})`);
922
+ }
923
+ if (query.searchTerm) {
924
+ countReq.input("searchTerm", import_mssql.default.NVarChar, `%${query.searchTerm}%`);
925
+ countConditions.push(`([label] LIKE @searchTerm OR [summary] LIKE @searchTerm)`);
926
+ }
927
+ if (query.properties) {
928
+ const entries = Object.entries(query.properties);
929
+ for (let i = 0; i < entries.length; i++) {
930
+ const [key, value] = entries[i];
931
+ countReq.input(`propKey${i}`, import_mssql.default.NVarChar, `$.${key}`);
932
+ countReq.input(`propVal${i}`, import_mssql.default.NVarChar, String(value));
933
+ countConditions.push(`JSON_VALUE([properties], @propKey${i}) = @propVal${i}`);
934
+ }
935
+ }
936
+ if (query.provenance) {
937
+ addProvenanceConditions(countReq, query.provenance, countConditions, "count");
938
+ }
939
+ const countWhere = countConditions.join(" AND ");
940
+ const totalResult = await countReq.query(
941
+ `SELECT COUNT(*) AS cnt FROM ${this.t("dm_entities")} WHERE ${countWhere}`
942
+ );
943
+ const total = totalResult.recordset[0]?.cnt ?? 0;
944
+ req.input("limit", import_mssql.default.Int, query.limit);
945
+ req.input("offset", import_mssql.default.Int, query.offset);
946
+ const result = await req.query(
947
+ `SELECT ${ENTITY_COLS_LIGHT} FROM ${this.t("dm_entities")}
948
+ WHERE ${where}
949
+ ORDER BY [entity_id]
950
+ OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY`
951
+ );
952
+ return {
953
+ items: result.recordset.map(entityFromRow),
954
+ total,
955
+ hasMore: query.offset + query.limit < total,
956
+ limit: query.limit,
957
+ offset: query.offset
958
+ };
959
+ }
960
+ // ─── Relationships ──────────────────────────────────────────────
961
+ async createRelationship(repositoryId, relationship) {
962
+ await this.assertRepository(repositoryId);
963
+ const pool = this.getPool();
964
+ const existing = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("relId", import_mssql.default.NVarChar, relationship.id).query(
965
+ `SELECT [relationship_id] FROM ${this.t("dm_relationships")}
966
+ WHERE [repository_id] = @repoId AND [relationship_id] = @relId`
967
+ );
968
+ if (existing.recordset.length > 0) {
969
+ throw new import_deep_memory.DuplicateRelationshipError(relationship.id);
970
+ }
971
+ const req = pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("relId", import_mssql.default.NVarChar, relationship.id).input("relType", import_mssql.default.NVarChar, relationship.relationshipType).input("sourceId", import_mssql.default.NVarChar, relationship.sourceEntityId).input("targetId", import_mssql.default.NVarChar, relationship.targetEntityId).input("properties", import_mssql.default.NVarChar, JSON.stringify(relationship.properties)).input("bidirectional", import_mssql.default.Bit, relationship.bidirectional ? 1 : 0);
972
+ this.addProvenanceInputs(req, relationship.provenance);
973
+ await req.query(`
974
+ INSERT INTO ${this.t("dm_relationships")} (
975
+ [repository_id], [relationship_id], [relationship_type],
976
+ [source_entity_id], [target_entity_id], [properties], [bidirectional],
977
+ [created_by], [created_by_type], [created_at],
978
+ [created_in_conversation], [created_from_message],
979
+ [modified_by], [modified_by_type], [modified_at],
980
+ [modified_in_conversation], [modified_from_message]
981
+ ) VALUES (
982
+ @repoId, @relId, @relType,
983
+ @sourceId, @targetId, @properties, @bidirectional,
984
+ @createdBy, @createdByType, @createdAt,
985
+ @createdInConversation, @createdFromMessage,
986
+ @modifiedBy, @modifiedByType, @modifiedAt,
987
+ @modifiedInConversation, @modifiedFromMessage
988
+ )
989
+ `);
990
+ return relationship;
991
+ }
992
+ async getRelationship(repositoryId, relationshipId) {
993
+ const pool = this.getPool();
994
+ const result = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("relId", import_mssql.default.NVarChar, relationshipId).query(
995
+ `SELECT * FROM ${this.t("dm_relationships")}
996
+ WHERE [repository_id] = @repoId AND [relationship_id] = @relId`
997
+ );
998
+ const row = result.recordset[0];
999
+ if (!row) return null;
1000
+ return relationshipFromRow(row);
1001
+ }
1002
+ async getEntityRelationships(repositoryId, entityId, options) {
1003
+ const pool = this.getPool();
1004
+ const direction = options?.direction ?? "both";
1005
+ const limit = options?.limit ?? 10;
1006
+ const offset = options?.offset ?? 0;
1007
+ const tbl = this.t("dm_relationships");
1008
+ let rtFilter = "";
1009
+ const rtInputs = [];
1010
+ if (options?.relationshipTypes && options.relationshipTypes.length > 0) {
1011
+ const placeholders = options.relationshipTypes.map((t, i) => {
1012
+ rtInputs.push({ name: `rt${i}`, value: t });
1013
+ return `@rt${i}`;
1014
+ });
1015
+ rtFilter = ` AND [relationship_type] IN (${placeholders.join(",")})`;
1016
+ }
1017
+ const unionBranches = this.buildRelationshipUnion(tbl, direction, rtFilter);
1018
+ const addParams = (req) => {
1019
+ req.input("repoId", import_mssql.default.UniqueIdentifier, repositoryId);
1020
+ req.input("entityId", import_mssql.default.NVarChar, entityId);
1021
+ for (const p of rtInputs) req.input(p.name, import_mssql.default.NVarChar, p.value);
1022
+ return req;
1023
+ };
1024
+ const countReq = addParams(pool.request());
1025
+ const totalResult = await countReq.query(
1026
+ `SELECT COUNT(*) AS cnt FROM (${unionBranches}) AS _u`
1027
+ );
1028
+ const total = totalResult.recordset[0]?.cnt ?? 0;
1029
+ const fetchReq = addParams(pool.request());
1030
+ fetchReq.input("limit", import_mssql.default.Int, limit);
1031
+ fetchReq.input("offset", import_mssql.default.Int, offset);
1032
+ const result = await fetchReq.query(
1033
+ `SELECT * FROM (${unionBranches}) AS _u
1034
+ ORDER BY [relationship_id]
1035
+ OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY`
1036
+ );
1037
+ let items = result.recordset.map(relationshipFromRow);
1038
+ if (options?.propertyFilters && options.propertyFilters.length > 0) {
1039
+ items = items.filter((r) => (0, import_deep_memory.matchesPropertyFilters)(r.properties, options.propertyFilters));
1040
+ const filteredTotal = total;
1041
+ return {
1042
+ items,
1043
+ total: filteredTotal,
1044
+ hasMore: offset + limit < filteredTotal,
1045
+ limit,
1046
+ offset
1047
+ };
1048
+ }
1049
+ return {
1050
+ items,
1051
+ total,
1052
+ hasMore: offset + limit < total,
1053
+ limit,
1054
+ offset
1055
+ };
1056
+ }
1057
+ /**
1058
+ * Build a UNION ALL query that seeks on source and target indexes separately,
1059
+ * avoiding OR-based index scans. Each branch produces an index seek at scale.
1060
+ */
1061
+ buildRelationshipUnion(tbl, direction, rtFilter) {
1062
+ const srcBase = `SELECT * FROM ${tbl} WHERE [repository_id] = @repoId AND [source_entity_id] = @entityId${rtFilter}`;
1063
+ const tgtBase = `SELECT * FROM ${tbl} WHERE [repository_id] = @repoId AND [target_entity_id] = @entityId${rtFilter}`;
1064
+ const tgtBidi = `SELECT * FROM ${tbl} WHERE [repository_id] = @repoId AND [target_entity_id] = @entityId AND [bidirectional] = 1${rtFilter}`;
1065
+ const srcBidi = `SELECT * FROM ${tbl} WHERE [repository_id] = @repoId AND [source_entity_id] = @entityId AND [bidirectional] = 1${rtFilter}`;
1066
+ switch (direction) {
1067
+ case "outbound":
1068
+ return `${srcBase} UNION ALL ${tgtBidi} AND [source_entity_id] <> @entityId`;
1069
+ case "inbound":
1070
+ return `${tgtBase} UNION ALL ${srcBidi} AND [target_entity_id] <> @entityId`;
1071
+ case "both":
1072
+ default:
1073
+ return `${srcBase} UNION ALL ${tgtBase} AND [source_entity_id] <> @entityId`;
1074
+ }
1075
+ }
1076
+ async deleteRelationship(repositoryId, relationshipId) {
1077
+ const pool = this.getPool();
1078
+ const result = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("relId", import_mssql.default.NVarChar, relationshipId).query(
1079
+ `DELETE FROM ${this.t("dm_relationships")}
1080
+ WHERE [repository_id] = @repoId AND [relationship_id] = @relId`
1081
+ );
1082
+ if (result.rowsAffected[0] === 0) {
1083
+ throw new import_deep_memory.RelationshipNotFoundError(relationshipId);
1084
+ }
1085
+ }
1086
+ async deleteRelationshipsByType(repositoryId, relationshipType) {
1087
+ await this.assertRepository(repositoryId);
1088
+ const pool = this.getPool();
1089
+ const result = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("relType", import_mssql.default.NVarChar, relationshipType).query(
1090
+ `DELETE FROM ${this.t("dm_relationships")}
1091
+ WHERE [repository_id] = @repoId AND [relationship_type] = @relType`
1092
+ );
1093
+ return { deletedRelationships: result.rowsAffected[0] ?? 0 };
1094
+ }
1095
+ // ─── Graph Traversal ────────────────────────────────────────────
1096
+ async exploreNeighbourhood(repositoryId, entityId, options) {
1097
+ const centre = await this.getEntityLight(repositoryId, entityId);
1098
+ if (!centre) {
1099
+ throw new import_deep_memory.EntityNotFoundError(entityId);
1100
+ }
1101
+ const layers = [];
1102
+ const visited = /* @__PURE__ */ new Set([entityId]);
1103
+ let currentFrontier = /* @__PURE__ */ new Set([entityId]);
1104
+ for (let depth = 0; depth < options.depth; depth++) {
1105
+ const layer = {};
1106
+ const nextFrontier = /* @__PURE__ */ new Set();
1107
+ const frontierIds = [...currentFrontier];
1108
+ const relsByEntity = await this.getRelationshipsForEntities(
1109
+ repositoryId,
1110
+ frontierIds,
1111
+ options.direction,
1112
+ options.relationshipTypes
1113
+ );
1114
+ const connectedIdsToFetch = /* @__PURE__ */ new Set();
1115
+ const pendingRels = [];
1116
+ for (const frontierEntityId of frontierIds) {
1117
+ const rels = relsByEntity.get(frontierEntityId) ?? [];
1118
+ for (const rel of rels) {
1119
+ let connectedEntityId;
1120
+ if (rel.sourceEntityId === frontierEntityId) {
1121
+ connectedEntityId = rel.targetEntityId;
1122
+ } else if (rel.targetEntityId === frontierEntityId) {
1123
+ connectedEntityId = rel.sourceEntityId;
1124
+ }
1125
+ if (!connectedEntityId || visited.has(connectedEntityId)) continue;
1126
+ if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
1127
+ if (!(0, import_deep_memory.matchesPropertyFilters)(rel.properties, options.relationshipPropertyFilters)) {
1128
+ continue;
1129
+ }
1130
+ }
1131
+ connectedIdsToFetch.add(connectedEntityId);
1132
+ pendingRels.push({ frontierEntityId, rel, connectedEntityId });
1133
+ }
1134
+ }
1135
+ const connectedEntities = await this.getEntitiesLight(repositoryId, [...connectedIdsToFetch]);
1136
+ for (const { rel, connectedEntityId } of pendingRels) {
1137
+ if (visited.has(connectedEntityId)) continue;
1138
+ const connectedEntity = connectedEntities.get(connectedEntityId);
1139
+ if (!connectedEntity) continue;
1140
+ if (options.entityTypes && !options.entityTypes.includes(connectedEntity.entityType)) {
1141
+ continue;
1142
+ }
1143
+ const relType = rel.relationshipType;
1144
+ if (!layer[relType]) {
1145
+ layer[relType] = { total: 0, entities: [], relationships: [] };
1146
+ }
1147
+ const group = layer[relType];
1148
+ group.total++;
1149
+ if (group.entities.length < options.limitPerType) {
1150
+ group.entities.push(connectedEntity);
1151
+ group.relationships.push(rel);
1152
+ }
1153
+ nextFrontier.add(connectedEntityId);
1154
+ visited.add(connectedEntityId);
1155
+ }
1156
+ layers.push(layer);
1157
+ currentFrontier = nextFrontier;
1158
+ if (nextFrontier.size === 0) break;
1159
+ }
1160
+ return { centreId: entityId, layers };
1161
+ }
1162
+ async findPaths(repositoryId, sourceId, targetId, options) {
1163
+ const [source, target] = await Promise.all([
1164
+ this.getEntityLight(repositoryId, sourceId),
1165
+ this.getEntityLight(repositoryId, targetId)
1166
+ ]);
1167
+ if (!source) throw new import_deep_memory.EntityNotFoundError(sourceId);
1168
+ if (!target) throw new import_deep_memory.EntityNotFoundError(targetId);
1169
+ if (sourceId === targetId) {
1170
+ return { paths: [{ entityIds: [sourceId], relationshipIds: [] }], totalPaths: 1 };
1171
+ }
1172
+ const paths = [];
1173
+ let currentLevel = [
1174
+ { entityId: sourceId, path: [sourceId], relPath: [] }
1175
+ ];
1176
+ const visitedAtDepth = /* @__PURE__ */ new Map();
1177
+ visitedAtDepth.set(sourceId, 0);
1178
+ for (let depth = 0; depth <= options.maxDepth && currentLevel.length > 0 && paths.length < options.limit + options.offset; depth++) {
1179
+ const frontierIds = [...new Set(currentLevel.map((e) => e.entityId))];
1180
+ const relsByEntity = await this.getRelationshipsForEntities(
1181
+ repositoryId,
1182
+ frontierIds,
1183
+ "both",
1184
+ options.relationshipTypes
1185
+ );
1186
+ const nextLevel = [];
1187
+ for (const current of currentLevel) {
1188
+ if (paths.length >= options.limit + options.offset) break;
1189
+ const rels = relsByEntity.get(current.entityId) ?? [];
1190
+ for (const rel of rels) {
1191
+ if (options.relationshipPropertyFilters && options.relationshipPropertyFilters.length > 0) {
1192
+ if (!(0, import_deep_memory.matchesPropertyFilters)(rel.properties, options.relationshipPropertyFilters)) {
1193
+ continue;
1194
+ }
1195
+ }
1196
+ let nextEntityId;
1197
+ if (rel.sourceEntityId === current.entityId) {
1198
+ nextEntityId = rel.targetEntityId;
1199
+ } else if (rel.targetEntityId === current.entityId) {
1200
+ nextEntityId = rel.sourceEntityId;
1201
+ }
1202
+ if (!nextEntityId) continue;
1203
+ if (current.path.includes(nextEntityId) && nextEntityId !== targetId) continue;
1204
+ const newPath = [...current.path, nextEntityId];
1205
+ const newRelPath = [...current.relPath, rel.id];
1206
+ if (nextEntityId === targetId) {
1207
+ paths.push({ entityIds: newPath, relationshipIds: newRelPath });
1208
+ } else if (newPath.length <= options.maxDepth) {
1209
+ const prevDepth = visitedAtDepth.get(nextEntityId);
1210
+ if (prevDepth === void 0 || prevDepth >= newPath.length - 1) {
1211
+ visitedAtDepth.set(nextEntityId, newPath.length - 1);
1212
+ nextLevel.push({ entityId: nextEntityId, path: newPath, relPath: newRelPath });
1213
+ }
1214
+ }
1215
+ }
1216
+ }
1217
+ if (options.entityTypes && nextLevel.length > 0) {
1218
+ const nextIds = [...new Set(nextLevel.map((e) => e.entityId))];
1219
+ const entityMap = await this.getEntitiesLight(repositoryId, nextIds);
1220
+ const allowedIds = /* @__PURE__ */ new Set();
1221
+ for (const [id, e] of entityMap) {
1222
+ if (options.entityTypes.includes(e.entityType)) {
1223
+ allowedIds.add(id);
1224
+ }
1225
+ }
1226
+ currentLevel = nextLevel.filter((e) => allowedIds.has(e.entityId));
1227
+ } else {
1228
+ currentLevel = nextLevel;
1229
+ }
1230
+ }
1231
+ const paginatedPaths = paths.slice(options.offset, options.offset + options.limit);
1232
+ return { paths: paginatedPaths, totalPaths: paths.length };
1233
+ }
1234
+ // ─── Timeline ───────────────────────────────────────────────────
1235
+ async getTimeline(repositoryId, entityId, options) {
1236
+ const entity = await this.getEntityLight(repositoryId, entityId);
1237
+ if (!entity) {
1238
+ throw new import_deep_memory.EntityNotFoundError(entityId);
1239
+ }
1240
+ const events = [];
1241
+ events.push({
1242
+ timestamp: entity.provenance.createdAt,
1243
+ eventType: "entity:created",
1244
+ entityId
1245
+ });
1246
+ if (entity.provenance.modifiedAt !== entity.provenance.createdAt) {
1247
+ events.push({
1248
+ timestamp: entity.provenance.modifiedAt,
1249
+ eventType: "entity:updated",
1250
+ entityId
1251
+ });
1252
+ }
1253
+ const pool = this.getPool();
1254
+ const relReq = pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityId", import_mssql.default.NVarChar, entityId).input("from", import_mssql.default.NVarChar, options.timeRange?.from ?? null).input("to", import_mssql.default.NVarChar, options.timeRange?.to ?? null);
1255
+ const relResult = await relReq.query(
1256
+ `SELECT [relationship_id], [created_at] FROM ${this.t("dm_relationships")}
1257
+ WHERE [repository_id] = @repoId AND [source_entity_id] = @entityId
1258
+ AND (@from IS NULL OR [created_at] >= @from)
1259
+ AND (@to IS NULL OR [created_at] <= @to)
1260
+ UNION ALL
1261
+ SELECT [relationship_id], [created_at] FROM ${this.t("dm_relationships")}
1262
+ WHERE [repository_id] = @repoId AND [target_entity_id] = @entityId
1263
+ AND [source_entity_id] <> @entityId
1264
+ AND (@from IS NULL OR [created_at] >= @from)
1265
+ AND (@to IS NULL OR [created_at] <= @to)`
1266
+ );
1267
+ for (const row of relResult.recordset) {
1268
+ events.push({
1269
+ timestamp: row["created_at"],
1270
+ eventType: "relationship:created",
1271
+ entityId,
1272
+ relationshipId: row["relationship_id"]
1273
+ });
1274
+ }
1275
+ let filtered = events;
1276
+ if (options.timeRange) {
1277
+ const from = new Date(options.timeRange.from).getTime();
1278
+ const to = new Date(options.timeRange.to).getTime();
1279
+ filtered = filtered.filter((e) => {
1280
+ const t = new Date(e.timestamp).getTime();
1281
+ return t >= from && t <= to;
1282
+ });
1283
+ }
1284
+ if (options.eventTypes && options.eventTypes.length > 0) {
1285
+ filtered = filtered.filter((e) => options.eventTypes.includes(e.eventType));
1286
+ }
1287
+ filtered.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
1288
+ const total = filtered.length;
1289
+ const items = filtered.slice(options.offset, options.offset + options.limit);
1290
+ return { events: items, total };
1291
+ }
1292
+ // ─── Bulk Operations ────────────────────────────────────────────
1293
+ async *exportAll(repositoryId) {
1294
+ await this.assertRepository(repositoryId);
1295
+ const pool = this.getPool();
1296
+ const batchSize = 100;
1297
+ const entityCount = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).query(
1298
+ `SELECT COUNT(*) AS cnt FROM ${this.t("dm_entities")} WHERE [repository_id] = @repoId`
1299
+ );
1300
+ const totalEntities = entityCount.recordset[0]?.cnt ?? 0;
1301
+ if (totalEntities === 0) {
1302
+ yield { type: "entities", data: [], sequence: 0, isLast: true };
1303
+ } else {
1304
+ for (let offset = 0; offset < totalEntities; offset += batchSize) {
1305
+ const batch = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("offset", import_mssql.default.Int, offset).input("limit", import_mssql.default.Int, batchSize).query(
1306
+ `SELECT ${ENTITY_COLS_FULL} FROM ${this.t("dm_entities")}
1307
+ WHERE [repository_id] = @repoId
1308
+ ORDER BY [entity_id]
1309
+ OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY`
1310
+ );
1311
+ yield {
1312
+ type: "entities",
1313
+ data: batch.recordset.map(entityFromRow),
1314
+ sequence: Math.floor(offset / batchSize),
1315
+ isLast: offset + batchSize >= totalEntities
1316
+ };
1317
+ }
1318
+ }
1319
+ const relCount = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).query(
1320
+ `SELECT COUNT(*) AS cnt FROM ${this.t("dm_relationships")} WHERE [repository_id] = @repoId`
1321
+ );
1322
+ const totalRels = relCount.recordset[0]?.cnt ?? 0;
1323
+ if (totalRels === 0) {
1324
+ yield { type: "relationships", data: [], sequence: 0, isLast: true };
1325
+ } else {
1326
+ for (let offset = 0; offset < totalRels; offset += batchSize) {
1327
+ const batch = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("offset", import_mssql.default.Int, offset).input("limit", import_mssql.default.Int, batchSize).query(
1328
+ `SELECT * FROM ${this.t("dm_relationships")}
1329
+ WHERE [repository_id] = @repoId
1330
+ ORDER BY [relationship_id]
1331
+ OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY`
1332
+ );
1333
+ yield {
1334
+ type: "relationships",
1335
+ data: batch.recordset.map(relationshipFromRow),
1336
+ sequence: Math.floor(offset / batchSize),
1337
+ isLast: offset + batchSize >= totalRels
1338
+ };
1339
+ }
1340
+ }
1341
+ }
1342
+ async importBulk(repositoryId, data) {
1343
+ await this.assertRepository(repositoryId);
1344
+ const pool = this.getPool();
1345
+ let entitiesImported = 0;
1346
+ let relationshipsImported = 0;
1347
+ const transaction = pool.transaction();
1348
+ await transaction.begin();
1349
+ try {
1350
+ for (const chunk of data) {
1351
+ if (chunk.entities) {
1352
+ for (const entity of chunk.entities) {
1353
+ const req = transaction.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityId", import_mssql.default.NVarChar, entity.id).input("slug", import_mssql.default.NVarChar, entity.slug).input("entityType", import_mssql.default.NVarChar, entity.entityType).input("label", import_mssql.default.NVarChar, entity.label).input("summary", import_mssql.default.NVarChar, entity.summary ?? null).input("properties", import_mssql.default.NVarChar, JSON.stringify(entity.properties)).input("data", import_mssql.default.NVarChar, entity.data ?? null).input("dataFormat", import_mssql.default.NVarChar, entity.dataFormat ?? null).input("embedding", import_mssql.default.NVarChar, entity.embedding ? JSON.stringify(entity.embedding) : null);
1354
+ this.addProvenanceInputs(req, entity.provenance);
1355
+ await req.query(`
1356
+ MERGE ${this.t("dm_entities")} AS target
1357
+ USING (SELECT @repoId AS repository_id, @entityId AS entity_id) AS source
1358
+ ON target.[repository_id] = source.repository_id AND target.[entity_id] = source.entity_id
1359
+ WHEN MATCHED THEN UPDATE SET
1360
+ [entity_type] = @entityType, [slug] = @slug, [label] = @label, [summary] = @summary,
1361
+ [properties] = @properties, [data] = @data, [data_format] = @dataFormat,
1362
+ [embedding] = @embedding,
1363
+ [modified_by] = @modifiedBy, [modified_by_type] = @modifiedByType,
1364
+ [modified_at] = @modifiedAt, [modified_in_conversation] = @modifiedInConversation,
1365
+ [modified_from_message] = @modifiedFromMessage
1366
+ WHEN NOT MATCHED THEN INSERT (
1367
+ [repository_id], [entity_id], [slug], [entity_type], [label], [summary],
1368
+ [properties], [data], [data_format], [embedding],
1369
+ [created_by], [created_by_type], [created_at],
1370
+ [created_in_conversation], [created_from_message],
1371
+ [modified_by], [modified_by_type], [modified_at],
1372
+ [modified_in_conversation], [modified_from_message]
1373
+ ) VALUES (
1374
+ @repoId, @entityId, @slug, @entityType, @label, @summary,
1375
+ @properties, @data, @dataFormat, @embedding,
1376
+ @createdBy, @createdByType, @createdAt,
1377
+ @createdInConversation, @createdFromMessage,
1378
+ @modifiedBy, @modifiedByType, @modifiedAt,
1379
+ @modifiedInConversation, @modifiedFromMessage
1380
+ );
1381
+ `);
1382
+ entitiesImported++;
1383
+ }
1384
+ }
1385
+ if (chunk.relationships) {
1386
+ for (const rel of chunk.relationships) {
1387
+ const req = transaction.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("relId", import_mssql.default.NVarChar, rel.id).input("relType", import_mssql.default.NVarChar, rel.relationshipType).input("sourceId", import_mssql.default.NVarChar, rel.sourceEntityId).input("targetId", import_mssql.default.NVarChar, rel.targetEntityId).input("properties", import_mssql.default.NVarChar, JSON.stringify(rel.properties)).input("bidirectional", import_mssql.default.Bit, rel.bidirectional ? 1 : 0);
1388
+ this.addProvenanceInputs(req, rel.provenance);
1389
+ await req.query(`
1390
+ MERGE ${this.t("dm_relationships")} AS target
1391
+ USING (SELECT @repoId AS repository_id, @relId AS relationship_id) AS source
1392
+ ON target.[repository_id] = source.repository_id AND target.[relationship_id] = source.relationship_id
1393
+ WHEN MATCHED THEN UPDATE SET
1394
+ [relationship_type] = @relType, [source_entity_id] = @sourceId,
1395
+ [target_entity_id] = @targetId, [properties] = @properties,
1396
+ [bidirectional] = @bidirectional,
1397
+ [modified_by] = @modifiedBy, [modified_by_type] = @modifiedByType,
1398
+ [modified_at] = @modifiedAt, [modified_in_conversation] = @modifiedInConversation,
1399
+ [modified_from_message] = @modifiedFromMessage
1400
+ WHEN NOT MATCHED THEN INSERT (
1401
+ [repository_id], [relationship_id], [relationship_type],
1402
+ [source_entity_id], [target_entity_id], [properties], [bidirectional],
1403
+ [created_by], [created_by_type], [created_at],
1404
+ [created_in_conversation], [created_from_message],
1405
+ [modified_by], [modified_by_type], [modified_at],
1406
+ [modified_in_conversation], [modified_from_message]
1407
+ ) VALUES (
1408
+ @repoId, @relId, @relType,
1409
+ @sourceId, @targetId, @properties, @bidirectional,
1410
+ @createdBy, @createdByType, @createdAt,
1411
+ @createdInConversation, @createdFromMessage,
1412
+ @modifiedBy, @modifiedByType, @modifiedAt,
1413
+ @modifiedInConversation, @modifiedFromMessage
1414
+ );
1415
+ `);
1416
+ relationshipsImported++;
1417
+ }
1418
+ }
1419
+ }
1420
+ await transaction.commit();
1421
+ } catch (err) {
1422
+ await transaction.rollback();
1423
+ throw err;
1424
+ }
1425
+ return { entitiesImported, relationshipsImported, errors: [] };
1426
+ }
1427
+ // ─── Private helpers ────────────────────────────────────────────
1428
+ /** Creates a TVP table from an array of string IDs (eliminates plan cache bloat from IN clauses). */
1429
+ createIdListTvp(ids) {
1430
+ const tvp = new import_mssql.default.Table(`${this.schema}.dm_id_list`);
1431
+ tvp.columns.add("id", import_mssql.default.NVarChar(300));
1432
+ for (const id of ids) tvp.rows.add(id);
1433
+ return tvp;
1434
+ }
1435
+ /** Returns a StoredEntity without the embedding column (lighter I/O). */
1436
+ async getEntityLight(repositoryId, entityId) {
1437
+ const pool = this.getPool();
1438
+ const result = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityId", import_mssql.default.NVarChar, entityId).query(
1439
+ `SELECT ${ENTITY_COLS_LIGHT} FROM ${this.t("dm_entities")}
1440
+ WHERE [repository_id] = @repoId AND [entity_id] = @entityId`
1441
+ );
1442
+ const row = result.recordset[0];
1443
+ if (!row) return null;
1444
+ return entityFromRow(row);
1445
+ }
1446
+ /** Returns multiple entities without embedding columns, using TVP for batch lookup. */
1447
+ async getEntitiesLight(repositoryId, entityIds) {
1448
+ const pool = this.getPool();
1449
+ const result = /* @__PURE__ */ new Map();
1450
+ if (entityIds.length === 0) return result;
1451
+ const tvp = this.createIdListTvp(entityIds);
1452
+ const rows = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityIds", tvp).query(
1453
+ `SELECT ${ENTITY_COLS_LIGHT} FROM ${this.t("dm_entities")}
1454
+ WHERE [repository_id] = @repoId
1455
+ AND [entity_id] IN (SELECT [id] FROM @entityIds)`
1456
+ );
1457
+ for (const row of rows.recordset) {
1458
+ const entity = entityFromRow(row);
1459
+ result.set(entity.id, entity);
1460
+ }
1461
+ return result;
1462
+ }
1463
+ /**
1464
+ * Fetches relationships for multiple entity IDs in a single query using TVP.
1465
+ * Returns results grouped by the frontier entity ID.
1466
+ */
1467
+ async getRelationshipsForEntities(repositoryId, entityIds, direction, relationshipTypes) {
1468
+ const pool = this.getPool();
1469
+ const result = /* @__PURE__ */ new Map();
1470
+ if (entityIds.length === 0) return result;
1471
+ const tbl = this.t("dm_relationships");
1472
+ const tvp = this.createIdListTvp(entityIds);
1473
+ let rtFilter = "";
1474
+ const rtInputs = [];
1475
+ if (relationshipTypes && relationshipTypes.length > 0) {
1476
+ const placeholders = relationshipTypes.map((t, i) => {
1477
+ rtInputs.push({ name: `rt${i}`, value: t });
1478
+ return `@rt${i}`;
1479
+ });
1480
+ rtFilter = ` AND [relationship_type] IN (${placeholders.join(",")})`;
1481
+ }
1482
+ const srcBase = `SELECT * FROM ${tbl} WHERE [repository_id] = @repoId AND [source_entity_id] IN (SELECT [id] FROM @entityIds)${rtFilter}`;
1483
+ const tgtBase = `SELECT * FROM ${tbl} WHERE [repository_id] = @repoId AND [target_entity_id] IN (SELECT [id] FROM @entityIds)${rtFilter}`;
1484
+ const tgtBidi = `SELECT * FROM ${tbl} WHERE [repository_id] = @repoId AND [target_entity_id] IN (SELECT [id] FROM @entityIds) AND [bidirectional] = 1${rtFilter}`;
1485
+ const srcBidi = `SELECT * FROM ${tbl} WHERE [repository_id] = @repoId AND [source_entity_id] IN (SELECT [id] FROM @entityIds) AND [bidirectional] = 1${rtFilter}`;
1486
+ let unionQuery;
1487
+ switch (direction) {
1488
+ case "outbound":
1489
+ unionQuery = `${srcBase} UNION ALL ${tgtBidi}`;
1490
+ break;
1491
+ case "inbound":
1492
+ unionQuery = `${tgtBase} UNION ALL ${srcBidi}`;
1493
+ break;
1494
+ case "both":
1495
+ default:
1496
+ unionQuery = `${srcBase} UNION ALL ${tgtBase}`;
1497
+ break;
1498
+ }
1499
+ const req = pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityIds", tvp);
1500
+ for (const p of rtInputs) req.input(p.name, import_mssql.default.NVarChar, p.value);
1501
+ const rows = await req.query(unionQuery);
1502
+ const entityIdSet = new Set(entityIds);
1503
+ for (const row of rows.recordset) {
1504
+ const rel = relationshipFromRow(row);
1505
+ const frontierIds = [];
1506
+ if (entityIdSet.has(rel.sourceEntityId)) frontierIds.push(rel.sourceEntityId);
1507
+ if (entityIdSet.has(rel.targetEntityId)) frontierIds.push(rel.targetEntityId);
1508
+ for (const fid of frontierIds) {
1509
+ let list = result.get(fid);
1510
+ if (!list) {
1511
+ list = [];
1512
+ result.set(fid, list);
1513
+ }
1514
+ list.push(rel);
1515
+ }
1516
+ }
1517
+ return result;
1518
+ }
1519
+ async assertRepository(repositoryId) {
1520
+ const repo = await this.getRepository(repositoryId);
1521
+ if (!repo) {
1522
+ throw new import_deep_memory.RepositoryNotFoundError(repositoryId);
1523
+ }
1524
+ }
1525
+ addProvenanceInputs(req, provenance) {
1526
+ req.input("createdBy", import_mssql.default.NVarChar, provenance.createdBy);
1527
+ req.input("createdByType", import_mssql.default.NVarChar, provenance.createdByType);
1528
+ req.input("createdAt", import_mssql.default.NVarChar, provenance.createdAt);
1529
+ req.input("createdInConversation", import_mssql.default.NVarChar, provenance.createdInConversation ?? null);
1530
+ req.input("createdFromMessage", import_mssql.default.NVarChar, provenance.createdFromMessage ?? null);
1531
+ req.input("modifiedBy", import_mssql.default.NVarChar, provenance.modifiedBy);
1532
+ req.input("modifiedByType", import_mssql.default.NVarChar, provenance.modifiedByType);
1533
+ req.input("modifiedAt", import_mssql.default.NVarChar, provenance.modifiedAt);
1534
+ req.input("modifiedInConversation", import_mssql.default.NVarChar, provenance.modifiedInConversation ?? null);
1535
+ req.input("modifiedFromMessage", import_mssql.default.NVarChar, provenance.modifiedFromMessage ?? null);
1536
+ }
1537
+ };
1538
+
1539
+ // src/SqlServerSearchProvider.ts
1540
+ var import_mssql2 = __toESM(require("mssql"), 1);
1541
+ var import_deep_memory2 = require("@utaba/deep-memory");
1542
+ function normaliseRank(rank) {
1543
+ return Math.min(1, Math.max(0, rank / 1e3));
1544
+ }
1545
+ function buildHighlights(query, row) {
1546
+ const highlights = {};
1547
+ const terms = query.toLowerCase().split(/\s+/).filter((t) => t.length > 0);
1548
+ const fields = [
1549
+ { key: "label", hlCol: "hl_label" },
1550
+ { key: "summary", hlCol: "hl_summary" },
1551
+ { key: "data", hlCol: "hl_data" },
1552
+ { key: "properties", hlCol: "hl_properties" }
1553
+ ];
1554
+ for (const { key, hlCol } of fields) {
1555
+ const snippet = row[hlCol];
1556
+ if (!snippet) continue;
1557
+ const lowerSnippet = snippet.toLowerCase();
1558
+ for (const term of terms) {
1559
+ const idx = lowerSnippet.indexOf(term);
1560
+ if (idx !== -1) {
1561
+ const windowStart = Math.max(0, idx - 40);
1562
+ const windowEnd = Math.min(snippet.length, idx + term.length + 40);
1563
+ const prefix = windowStart > 0 ? "..." : "";
1564
+ const suffix = windowEnd < snippet.length ? "..." : "";
1565
+ const window = prefix + snippet.slice(windowStart, windowEnd) + suffix;
1566
+ if (!highlights[key]) highlights[key] = [];
1567
+ highlights[key].push(window);
1568
+ break;
1569
+ }
1570
+ }
1571
+ }
1572
+ return Object.keys(highlights).length > 0 ? highlights : void 0;
1573
+ }
1574
+ var SqlServerSearchProvider = class {
1575
+ pool;
1576
+ schema;
1577
+ procedureCreated = false;
1578
+ constructor(config) {
1579
+ this.pool = config.pool;
1580
+ this.schema = config.schema ?? "dbo";
1581
+ }
1582
+ /**
1583
+ * Ensure the stored procedure exists. Called lazily on first search.
1584
+ * Idempotent — uses CREATE OR ALTER.
1585
+ */
1586
+ async ensureProcedure() {
1587
+ if (this.procedureCreated) return;
1588
+ try {
1589
+ const ddl = getSearchProcSQL(this.schema);
1590
+ await this.pool.request().query(ddl);
1591
+ this.procedureCreated = true;
1592
+ } catch (err) {
1593
+ throw new import_deep_memory2.ProviderError(
1594
+ `Failed to create search stored procedure: ${err instanceof Error ? err.message : String(err)}`
1595
+ );
1596
+ }
1597
+ }
1598
+ /**
1599
+ * Index an entity for full-text search.
1600
+ *
1601
+ * No-op: the FT index on dm_entities uses CHANGE_TRACKING AUTO,
1602
+ * so SQL Server automatically indexes rows on insert/update.
1603
+ */
1604
+ async indexEntity(_repositoryId, _entity) {
1605
+ }
1606
+ /**
1607
+ * Remove an entity from the search index.
1608
+ *
1609
+ * No-op: the FT index auto-removes when the row is deleted from dm_entities.
1610
+ */
1611
+ async removeEntity(_repositoryId, _entityId) {
1612
+ }
1613
+ /**
1614
+ * Full-text search across entity labels, summaries, properties, and data.
1615
+ * Calls the dm_search_entities stored procedure using FREETEXTTABLE
1616
+ * for natural-language queries.
1617
+ */
1618
+ async search(repositoryId, query, options) {
1619
+ await this.ensureProcedure();
1620
+ const limit = options?.limit ?? 20;
1621
+ const offset = options?.offset ?? 0;
1622
+ const entityTypes = options?.entityTypes;
1623
+ try {
1624
+ const request = this.pool.request();
1625
+ request.input("RepositoryId", import_mssql2.default.UniqueIdentifier, repositoryId);
1626
+ request.input("Query", import_mssql2.default.NVarChar(4e3), query);
1627
+ request.input(
1628
+ "EntityTypes",
1629
+ import_mssql2.default.NVarChar(import_mssql2.default.MAX),
1630
+ entityTypes && entityTypes.length > 0 ? JSON.stringify(entityTypes) : null
1631
+ );
1632
+ request.input("Limit", import_mssql2.default.Int, limit);
1633
+ request.input("Offset", import_mssql2.default.Int, offset);
1634
+ request.input("UseContains", import_mssql2.default.Bit, 0);
1635
+ const result = await request.execute(
1636
+ `[${this.schema}].[dm_search_entities]`
1637
+ );
1638
+ const rows = result.recordset;
1639
+ const total = rows.length > 0 ? rows[0]["total_count"] : 0;
1640
+ const items = rows.map((row) => ({
1641
+ id: row["entity_id"],
1642
+ score: normaliseRank(row["ft_rank"]),
1643
+ highlights: buildHighlights(query, row)
1644
+ }));
1645
+ return {
1646
+ items,
1647
+ total,
1648
+ hasMore: offset + items.length < total,
1649
+ limit,
1650
+ offset
1651
+ };
1652
+ } catch (err) {
1653
+ throw new import_deep_memory2.ProviderError(
1654
+ `Full-text search failed: ${err instanceof Error ? err.message : String(err)}`
1655
+ );
1656
+ }
1657
+ }
1658
+ /**
1659
+ * Re-index an entire repository.
1660
+ *
1661
+ * Since the FT index is auto-maintained, this triggers a manual
1662
+ * population of the full-text catalog to pick up any pending changes.
1663
+ */
1664
+ async reindexRepository(_repositoryId, _entities) {
1665
+ try {
1666
+ await this.pool.request().query(
1667
+ `ALTER FULLTEXT CATALOG [dm_fulltext_catalog] REORGANIZE`
1668
+ );
1669
+ } catch (err) {
1670
+ throw new import_deep_memory2.ProviderError(
1671
+ `Full-text reindex failed: ${err instanceof Error ? err.message : String(err)}`
1672
+ );
1673
+ }
1674
+ }
1675
+ };
1676
+ // Annotate the CommonJS export names for ESM import in node:
1677
+ 0 && (module.exports = {
1678
+ SCHEMA_VERSION,
1679
+ SqlServerSearchProvider,
1680
+ SqlServerStorageProvider,
1681
+ getSchemaSQL,
1682
+ getSearchProcSQL
1683
+ });
1684
+ //# sourceMappingURL=index.cjs.map