@utaba/deep-memory-storage-sqlserver 0.15.0 → 0.17.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/LICENSE +1 -1
- package/README.md +217 -42
- package/dist/index.cjs +12 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +12 -8
- package/dist/index.js.map +1 -1
- package/package.json +4 -5
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @utaba/deep-memory-storage-sqlserver
|
|
2
2
|
|
|
3
|
-
SQL Server storage provider for [
|
|
3
|
+
SQL Server storage provider for [`@utaba/deep-memory`](https://www.npmjs.com/package/@utaba/deep-memory). Provides persistent, multi-tenant graph storage backed by SQL Server (2016+).
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,7 +8,9 @@ SQL Server storage provider for [@utaba/deep-memory](../core/README.md).
|
|
|
8
8
|
pnpm add @utaba/deep-memory @utaba/deep-memory-storage-sqlserver
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**Runtime dependency:** [`mssql`](https://www.npmjs.com/package/mssql) (the Node.js SQL Server driver).
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
12
14
|
|
|
13
15
|
```typescript
|
|
14
16
|
import { DeepMemory } from '@utaba/deep-memory';
|
|
@@ -17,13 +19,12 @@ import { SqlServerStorageProvider } from '@utaba/deep-memory-storage-sqlserver';
|
|
|
17
19
|
const provider = new SqlServerStorageProvider({
|
|
18
20
|
connection: {
|
|
19
21
|
server: 'localhost',
|
|
20
|
-
port:
|
|
22
|
+
port: 1435,
|
|
21
23
|
database: 'deep-memory',
|
|
22
24
|
user: 'sa',
|
|
23
25
|
password: 'YourPassword',
|
|
24
26
|
options: { trustServerCertificate: true },
|
|
25
27
|
},
|
|
26
|
-
schema: 'dbo', // optional, default 'dbo'
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
const dm = new DeepMemory({ storage: provider });
|
|
@@ -32,17 +33,44 @@ const dm = new DeepMemory({ storage: provider });
|
|
|
32
33
|
await dm.ensureSchema();
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
## Configuration
|
|
37
|
+
|
|
38
|
+
### `SqlServerStorageProviderConfig`
|
|
39
|
+
|
|
40
|
+
| Option | Type | Default | Description |
|
|
41
|
+
|--------|------|---------|-------------|
|
|
42
|
+
| `connection` | `sql.config \| sql.ConnectionPool` | *required* | Either an `mssql` config object, a connection-string config, or an existing `ConnectionPool` instance. |
|
|
43
|
+
| `schema` | `string` | `'dbo'` | SQL Server schema name. Must already exist in the database. |
|
|
44
|
+
|
|
45
|
+
### Connection options
|
|
46
|
+
|
|
47
|
+
**Config object:**
|
|
36
48
|
|
|
37
49
|
```typescript
|
|
38
50
|
const provider = new SqlServerStorageProvider({
|
|
39
51
|
connection: {
|
|
40
|
-
|
|
52
|
+
server: 'localhost',
|
|
53
|
+
port: 1435,
|
|
54
|
+
database: 'deep-memory',
|
|
55
|
+
user: 'sa',
|
|
56
|
+
password: 'YourPassword',
|
|
57
|
+
options: { trustServerCertificate: true },
|
|
41
58
|
},
|
|
42
59
|
});
|
|
43
60
|
```
|
|
44
61
|
|
|
45
|
-
|
|
62
|
+
**Connection string:**
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const provider = new SqlServerStorageProvider({
|
|
66
|
+
connection: {
|
|
67
|
+
connectionString:
|
|
68
|
+
'Server=localhost,1435;Database=deep-memory;User Id=sa;Password=YourPassword;TrustServerCertificate=true',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Existing connection pool** (shared with your application):
|
|
46
74
|
|
|
47
75
|
```typescript
|
|
48
76
|
import sql from 'mssql';
|
|
@@ -53,73 +81,220 @@ await pool.connect();
|
|
|
53
81
|
const provider = new SqlServerStorageProvider({ connection: pool });
|
|
54
82
|
```
|
|
55
83
|
|
|
84
|
+
When you pass an existing pool, the provider will not close it on `dispose()` — your application retains ownership.
|
|
85
|
+
|
|
86
|
+
## Lifecycle
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// 1. Create the provider
|
|
90
|
+
const provider = new SqlServerStorageProvider({ connection: config });
|
|
91
|
+
|
|
92
|
+
// 2. Initialise — connects to SQL Server
|
|
93
|
+
await provider.initialise();
|
|
94
|
+
|
|
95
|
+
// 3. Use via DeepMemory
|
|
96
|
+
const dm = new DeepMemory({ storage: provider });
|
|
97
|
+
const repo = await dm.createRepository({ ... });
|
|
98
|
+
|
|
99
|
+
// 4. Dispose — closes the connection pool (if provider owns it)
|
|
100
|
+
await provider.dispose();
|
|
101
|
+
```
|
|
102
|
+
|
|
56
103
|
## Database Schema
|
|
57
104
|
|
|
58
|
-
###
|
|
105
|
+
### Table overview
|
|
106
|
+
|
|
107
|
+
All tables use the `dm_` prefix to avoid collisions when sharing a database with other applications.
|
|
108
|
+
|
|
109
|
+
| Table | Purpose |
|
|
110
|
+
|-------|---------|
|
|
111
|
+
| `dm_meta` | Schema version tracking (single row) |
|
|
112
|
+
| `dm_repositories` | Repository definitions and governance config |
|
|
113
|
+
| `dm_vocabularies` | One vocabulary JSON document per repository |
|
|
114
|
+
| `dm_vocabulary_change_log` | Audit trail for vocabulary changes |
|
|
115
|
+
| `dm_entities` | Graph nodes with typed properties, optional data/embeddings, and provenance |
|
|
116
|
+
| `dm_relationships` | Graph edges with typed properties, directionality, and provenance |
|
|
117
|
+
|
|
118
|
+
### Entity-relationship diagram
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
dm_repositories
|
|
122
|
+
PK: repository_id
|
|
123
|
+
│
|
|
124
|
+
├──< dm_vocabularies (1:1)
|
|
125
|
+
│ PK/FK: repository_id
|
|
126
|
+
│
|
|
127
|
+
├──< dm_vocabulary_change_log (1:N)
|
|
128
|
+
│ PK: (repository_id, change_id)
|
|
129
|
+
│ FK: repository_id → dm_repositories
|
|
130
|
+
│
|
|
131
|
+
├──< dm_entities (1:N)
|
|
132
|
+
│ PK: (repository_id, entity_id)
|
|
133
|
+
│ FK: repository_id → dm_repositories
|
|
134
|
+
│ IX: (repository_id, entity_type)
|
|
135
|
+
│ IX: (repository_id, label)
|
|
136
|
+
│ IX: (repository_id, modified_at DESC)
|
|
137
|
+
│
|
|
138
|
+
└──< dm_relationships (1:N)
|
|
139
|
+
PK: (repository_id, relationship_id)
|
|
140
|
+
FK: repository_id → dm_repositories (CASCADE DELETE)
|
|
141
|
+
FK: (repository_id, source_entity_id) → dm_entities
|
|
142
|
+
FK: (repository_id, target_entity_id) → dm_entities
|
|
143
|
+
IX: (repository_id, source_entity_id) INCLUDE (relationship_type, target_entity_id, bidirectional)
|
|
144
|
+
IX: (repository_id, target_entity_id) INCLUDE (relationship_type, source_entity_id, bidirectional)
|
|
145
|
+
IX: (repository_id, relationship_type)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Naming conventions
|
|
149
|
+
|
|
150
|
+
| Element | Convention | Example |
|
|
151
|
+
|---------|-----------|---------|
|
|
152
|
+
| Tables | `dm_` prefix + plural snake_case | `dm_entities` |
|
|
153
|
+
| Columns | snake_case | `entity_type`, `created_at` |
|
|
154
|
+
| Primary keys | `pk_{table}` | `pk_dm_entities` |
|
|
155
|
+
| Foreign keys | `fk_{table}_{referenced_table}` | `fk_dm_relationships_entities` |
|
|
156
|
+
| Indexes | `ix_{table}_{columns}` | `ix_dm_entities_type` |
|
|
157
|
+
|
|
158
|
+
### Multi-tenancy
|
|
59
159
|
|
|
60
|
-
|
|
160
|
+
All data is scoped by `repository_id`. Each repository is an isolated knowledge graph — entities, relationships, vocabulary, and change log are all partitioned by this key. This supports multi-tenant deployments where each agent or domain has its own repository within a shared database.
|
|
61
161
|
|
|
62
|
-
###
|
|
162
|
+
### Key size limits
|
|
63
163
|
|
|
64
|
-
|
|
164
|
+
Composite primary keys are sized to stay within SQL Server's 900-byte clustered index limit:
|
|
65
165
|
|
|
66
|
-
|
|
166
|
+
| Column | Max Length | Bytes (NVARCHAR) |
|
|
167
|
+
|--------|-----------|-----------------|
|
|
168
|
+
| `repository_id` | 128 chars | 256 bytes |
|
|
169
|
+
| `entity_id` | 300 chars | 600 bytes |
|
|
170
|
+
| `relationship_id` | 300 chars | 600 bytes |
|
|
171
|
+
| `change_id` | 128 chars | 256 bytes |
|
|
172
|
+
|
|
173
|
+
Largest PK: `(repository_id, entity_id)` = 856 bytes (under 900 limit).
|
|
174
|
+
|
|
175
|
+
### Cascade deletes
|
|
176
|
+
|
|
177
|
+
Deleting a repository cascades to vocabularies, vocabulary change log, entities, and relationships. Entity deletion explicitly removes related relationships before removing the entity.
|
|
178
|
+
|
|
179
|
+
## Schema Management
|
|
180
|
+
|
|
181
|
+
### Automatic (default)
|
|
182
|
+
|
|
183
|
+
Call `dm.ensureSchema()` once at startup or deployment. The provider checks for existing tables and creates them if missing. Schema version is tracked in `dm_meta`. This is **not** called automatically — the consuming application decides when to run it.
|
|
184
|
+
|
|
185
|
+
### Manual
|
|
186
|
+
|
|
187
|
+
For production environments with managed migrations, export the DDL and run it yourself:
|
|
67
188
|
|
|
68
189
|
```typescript
|
|
69
|
-
import { getSchemaSQL } from '@utaba/deep-memory-storage-sqlserver';
|
|
190
|
+
import { getSchemaSQL, SCHEMA_VERSION } from '@utaba/deep-memory-storage-sqlserver';
|
|
70
191
|
|
|
71
|
-
//
|
|
192
|
+
// Get DDL for default schema (dbo)
|
|
72
193
|
const ddl = getSchemaSQL();
|
|
73
194
|
|
|
74
|
-
//
|
|
195
|
+
// Get DDL for a custom schema
|
|
75
196
|
const ddl = getSchemaSQL('my_schema');
|
|
76
197
|
```
|
|
77
198
|
|
|
78
|
-
|
|
199
|
+
From the command line:
|
|
79
200
|
|
|
80
201
|
```bash
|
|
81
|
-
|
|
82
|
-
node -e "import('./dist/index.js').then(m => console.log(m.getSchemaSQL()))"
|
|
202
|
+
node -e "import('@utaba/deep-memory-storage-sqlserver').then(m => console.log(m.getSchemaSQL()))" > schema.sql
|
|
83
203
|
```
|
|
84
204
|
|
|
85
|
-
|
|
205
|
+
### Static schema file
|
|
86
206
|
|
|
87
|
-
|
|
88
|
-
node -e "import('./dist/index.js').then(m => console.log(m.getSchemaSQL()))" > schema.sql
|
|
89
|
-
```
|
|
207
|
+
A pre-generated copy of the full DDL (schema + search procedure) lives at `schemas/deep-memory-schema-v1.0.sql` inside the package. It is generated from runtime code and must never be hand-edited.
|
|
90
208
|
|
|
91
|
-
###
|
|
209
|
+
### Version checking
|
|
92
210
|
|
|
93
|
-
|
|
211
|
+
On startup, the provider reads `schema_version` from `dm_meta`:
|
|
94
212
|
|
|
95
|
-
|
|
213
|
+
- **Same version** — no action needed
|
|
214
|
+
- **Database newer than provider** — throws `ProviderError` (update the package)
|
|
215
|
+
- **Database older than provider** — future migrations will run here; currently creates from scratch
|
|
96
216
|
|
|
97
|
-
|
|
217
|
+
## Query Capabilities
|
|
98
218
|
|
|
99
|
-
|
|
100
|
-
|-------|-------------|
|
|
101
|
-
| `dm_meta` | Schema version tracking |
|
|
102
|
-
| `dm_repositories` | Repository definitions and governance config |
|
|
103
|
-
| `dm_vocabularies` | Vocabulary JSON document per repository |
|
|
104
|
-
| `dm_vocabulary_change_log` | Vocabulary change audit trail |
|
|
105
|
-
| `dm_entities` | Graph nodes with provenance and optional embeddings |
|
|
106
|
-
| `dm_relationships` | Graph edges with provenance |
|
|
219
|
+
### Entity search
|
|
107
220
|
|
|
108
|
-
|
|
221
|
+
`findEntities()` supports:
|
|
222
|
+
|
|
223
|
+
- **Type filter** — restrict to specific entity types
|
|
224
|
+
- **Text search** — case-insensitive `LIKE` on label and summary columns
|
|
225
|
+
- **Property filter** — exact match via `JSON_VALUE()` on the JSON properties column
|
|
226
|
+
- **Pagination** — `OFFSET` / `FETCH NEXT` with total count
|
|
227
|
+
|
|
228
|
+
### Relationship queries
|
|
229
|
+
|
|
230
|
+
`getEntityRelationships()` supports:
|
|
231
|
+
|
|
232
|
+
- **Direction** — `outbound`, `inbound`, or `both` (default)
|
|
233
|
+
- **Relationship type filter** — restrict to specific types
|
|
234
|
+
- **Bidirectional handling** — bidirectional relationships appear in both directions
|
|
235
|
+
- **Pagination** — same `OFFSET` / `FETCH NEXT` pattern
|
|
236
|
+
|
|
237
|
+
### Graph traversal
|
|
238
|
+
|
|
239
|
+
- **`exploreNeighbourhood()`** — multi-hop BFS exploration from a centre entity, with depth, direction, entity type, and relationship type filters. Results are grouped by relationship type per layer.
|
|
240
|
+
- **`findPaths()`** — BFS path finding between two entities, with max depth and relationship type filters. Returns all paths up to the configured limit.
|
|
241
|
+
|
|
242
|
+
### Timeline
|
|
109
243
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
244
|
+
`getTimeline()` returns creation and modification events for an entity plus its relationship creation events, with optional time range and event type filters.
|
|
245
|
+
|
|
246
|
+
## Bulk Operations
|
|
247
|
+
|
|
248
|
+
### Export
|
|
249
|
+
|
|
250
|
+
`exportAll()` returns an async iterable of chunks (batches of 100), first entities then relationships. Suitable for streaming large repositories without loading everything into memory.
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
for await (const chunk of provider.exportAll(repositoryId)) {
|
|
254
|
+
// chunk.type: 'entities' | 'relationships'
|
|
255
|
+
// chunk.data: StoredEntity[] | StoredRelationship[]
|
|
256
|
+
// chunk.isLast: boolean
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Import
|
|
261
|
+
|
|
262
|
+
`importBulk()` uses SQL Server `MERGE` statements for upsert semantics — existing records are updated, new records are inserted. Returns a count of imported entities/relationships and any errors.
|
|
263
|
+
|
|
264
|
+
## Error Handling
|
|
265
|
+
|
|
266
|
+
All errors are typed using the `@utaba/deep-memory` error hierarchy:
|
|
267
|
+
|
|
268
|
+
| Error | When |
|
|
269
|
+
|-------|------|
|
|
270
|
+
| `ProviderError` | Connection failure, schema issues, SQL errors |
|
|
271
|
+
| `RepositoryNotFoundError` | Repository ID doesn't exist |
|
|
272
|
+
| `DuplicateRepositoryError` | Repository ID already exists |
|
|
273
|
+
| `EntityNotFoundError` | Entity ID doesn't exist in repository |
|
|
274
|
+
| `DuplicateEntityError` | Entity ID already exists in repository |
|
|
275
|
+
| `RelationshipNotFoundError` | Relationship ID doesn't exist |
|
|
276
|
+
| `DuplicateRelationshipError` | Relationship ID already exists |
|
|
115
277
|
|
|
116
278
|
## Testing
|
|
117
279
|
|
|
118
|
-
The conformance test suite requires a running SQL Server instance:
|
|
280
|
+
The conformance test suite requires a running SQL Server instance. Set the connection string via environment variable:
|
|
119
281
|
|
|
120
282
|
```bash
|
|
121
|
-
MSSQL_CONNECTION_STRING="Server=localhost,
|
|
283
|
+
MSSQL_CONNECTION_STRING="Server=localhost,1435;Database=deep-memory;User Id=sa;Password=YourPassword;TrustServerCertificate=true" \
|
|
122
284
|
pnpm --filter @utaba/deep-memory-storage-sqlserver test
|
|
123
285
|
```
|
|
124
286
|
|
|
125
287
|
Without `MSSQL_CONNECTION_STRING`, tests are skipped.
|
|
288
|
+
|
|
289
|
+
## Exports
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// Provider class
|
|
293
|
+
import { SqlServerStorageProvider } from '@utaba/deep-memory-storage-sqlserver';
|
|
294
|
+
|
|
295
|
+
// Config type
|
|
296
|
+
import type { SqlServerStorageProviderConfig } from '@utaba/deep-memory-storage-sqlserver';
|
|
297
|
+
|
|
298
|
+
// Schema utilities
|
|
299
|
+
import { getSchemaSQL, SCHEMA_VERSION } from '@utaba/deep-memory-storage-sqlserver';
|
|
300
|
+
```
|
package/dist/index.cjs
CHANGED
|
@@ -917,33 +917,36 @@ var SqlServerStorageProvider = class {
|
|
|
917
917
|
`);
|
|
918
918
|
return entity;
|
|
919
919
|
}
|
|
920
|
-
async getEntity(repositoryId, entityId) {
|
|
920
|
+
async getEntity(repositoryId, entityId, options) {
|
|
921
921
|
const pool = this.getPool();
|
|
922
|
+
const cols = options?.loadEmbeddings ? ENTITY_COLS_FULL : ENTITY_COLS_LIGHT;
|
|
922
923
|
const result = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityId", import_mssql.default.NVarChar, entityId).query(
|
|
923
|
-
`SELECT ${
|
|
924
|
+
`SELECT ${cols} FROM ${this.t("dm_entities")}
|
|
924
925
|
WHERE [repository_id] = @repoId AND [entity_id] = @entityId`
|
|
925
926
|
);
|
|
926
927
|
const row = result.recordset[0];
|
|
927
928
|
if (!row) return null;
|
|
928
929
|
return entityFromRow(row);
|
|
929
930
|
}
|
|
930
|
-
async getEntityBySlug(repositoryId, slug) {
|
|
931
|
+
async getEntityBySlug(repositoryId, slug, options) {
|
|
931
932
|
const pool = this.getPool();
|
|
933
|
+
const cols = options?.loadEmbeddings ? ENTITY_COLS_FULL : ENTITY_COLS_LIGHT;
|
|
932
934
|
const result = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("slug", import_mssql.default.NVarChar, slug).query(
|
|
933
|
-
`SELECT ${
|
|
935
|
+
`SELECT ${cols} FROM ${this.t("dm_entities")}
|
|
934
936
|
WHERE [repository_id] = @repoId AND [slug] = @slug`
|
|
935
937
|
);
|
|
936
938
|
const row = result.recordset[0];
|
|
937
939
|
if (!row) return null;
|
|
938
940
|
return entityFromRow(row);
|
|
939
941
|
}
|
|
940
|
-
async getEntities(repositoryId, entityIds) {
|
|
942
|
+
async getEntities(repositoryId, entityIds, options) {
|
|
941
943
|
const pool = this.getPool();
|
|
942
944
|
const result = /* @__PURE__ */ new Map();
|
|
943
945
|
if (entityIds.length === 0) return result;
|
|
946
|
+
const cols = options?.loadEmbeddings ? ENTITY_COLS_FULL : ENTITY_COLS_LIGHT;
|
|
944
947
|
const tvp = this.createIdListTvp(entityIds);
|
|
945
948
|
const rows = await pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId).input("entityIds", tvp).query(
|
|
946
|
-
`SELECT ${
|
|
949
|
+
`SELECT ${cols} FROM ${this.t("dm_entities")}
|
|
947
950
|
WHERE [repository_id] = @repoId
|
|
948
951
|
AND [entity_id] IN (SELECT [id] FROM @entityIds)`
|
|
949
952
|
);
|
|
@@ -1055,7 +1058,7 @@ var SqlServerStorageProvider = class {
|
|
|
1055
1058
|
const deletedEntities = entResult.rowsAffected[0] ?? 0;
|
|
1056
1059
|
return { deletedEntities, deletedRelationships };
|
|
1057
1060
|
}
|
|
1058
|
-
async findEntities(repositoryId, query) {
|
|
1061
|
+
async findEntities(repositoryId, query, options) {
|
|
1059
1062
|
const pool = this.getPool();
|
|
1060
1063
|
const req = pool.request().input("repoId", import_mssql.default.UniqueIdentifier, repositoryId);
|
|
1061
1064
|
const conditions = ["[repository_id] = @repoId"];
|
|
@@ -1115,8 +1118,9 @@ var SqlServerStorageProvider = class {
|
|
|
1115
1118
|
const total = totalResult.recordset[0]?.cnt ?? 0;
|
|
1116
1119
|
req.input("limit", import_mssql.default.Int, query.limit);
|
|
1117
1120
|
req.input("offset", import_mssql.default.Int, query.offset);
|
|
1121
|
+
const cols = options?.loadEmbeddings ? ENTITY_COLS_FULL : ENTITY_COLS_LIGHT;
|
|
1118
1122
|
const result = await req.query(
|
|
1119
|
-
`SELECT ${
|
|
1123
|
+
`SELECT ${cols} FROM ${this.t("dm_entities")}
|
|
1120
1124
|
WHERE ${where}
|
|
1121
1125
|
ORDER BY [entity_id]
|
|
1122
1126
|
OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY`
|