@rljson/db 0.0.11 → 0.0.13
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/README.architecture.md +859 -1
- package/README.contributors.md +534 -20
- package/README.public.md +943 -4
- package/README.trouble.md +49 -0
- package/dist/README.architecture.md +859 -1
- package/dist/README.contributors.md +534 -20
- package/dist/README.public.md +943 -4
- package/dist/README.trouble.md +49 -0
- package/dist/controller/controller.d.ts +2 -2
- package/dist/controller/tree-controller.d.ts +18 -0
- package/dist/db.d.ts +0 -6
- package/dist/db.js +317 -110
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/package.json +16 -16
package/README.architecture.md
CHANGED
|
@@ -1 +1,859 @@
|
|
|
1
|
-
# Architecture
|
|
1
|
+
# @rljson/db Architecture
|
|
2
|
+
|
|
3
|
+
This document provides a deep dive into the architecture, design patterns, and implementation details of @rljson/db.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Architecture Layers](#architecture-layers)
|
|
9
|
+
- [Core Components](#core-components)
|
|
10
|
+
- [Data Flow](#data-flow)
|
|
11
|
+
- [Controller Pattern](#controller-pattern)
|
|
12
|
+
- [Route Resolution](#route-resolution)
|
|
13
|
+
- [Caching Strategy](#caching-strategy)
|
|
14
|
+
- [Tree Processing](#tree-processing)
|
|
15
|
+
- [Join System](#join-system)
|
|
16
|
+
- [Multi-Edit System](#multi-edit-system)
|
|
17
|
+
- [Design Decisions](#design-decisions)
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
@rljson/db is a TypeScript-based database abstraction layer for content-addressed, hierarchical RLJSON data. It provides a high-level query interface on top of pluggable storage backends (`@rljson/io`).
|
|
22
|
+
|
|
23
|
+
### Key Characteristics
|
|
24
|
+
|
|
25
|
+
- **Content-Addressed**: All data identified by SHA-based content hashes
|
|
26
|
+
- **Immutable**: Data is never modified in place; all changes create new versions
|
|
27
|
+
- **Hierarchical**: Native support for tree structures and nested relationships
|
|
28
|
+
- **Type-Safe**: Full TypeScript support with strong typing
|
|
29
|
+
- **Version-Tracked**: Built-in insert history for time-travel queries
|
|
30
|
+
- **Storage-Agnostic**: Works with any `Io` implementation (memory, file, network)
|
|
31
|
+
|
|
32
|
+
## Architecture Layers
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
┌─────────────────────────────────────────────┐
|
|
36
|
+
│ Application Layer │
|
|
37
|
+
│ (User Queries & Mutations) │
|
|
38
|
+
└──────────────────┬──────────────────────────┘
|
|
39
|
+
│
|
|
40
|
+
┌──────────────────▼──────────────────────────┐
|
|
41
|
+
│ Db Layer │
|
|
42
|
+
│ • Route Resolution │
|
|
43
|
+
│ • Query Planning │
|
|
44
|
+
│ • Caching │
|
|
45
|
+
│ • Notification │
|
|
46
|
+
└──────────────────┬──────────────────────────┘
|
|
47
|
+
│
|
|
48
|
+
┌──────────────────▼──────────────────────────┐
|
|
49
|
+
│ Controller Layer │
|
|
50
|
+
│ • BaseController │
|
|
51
|
+
│ • TreeController (path-based expansion) │
|
|
52
|
+
│ • CakeController │
|
|
53
|
+
│ • LayerController │
|
|
54
|
+
│ • ComponentController │
|
|
55
|
+
│ • SliceIdController │
|
|
56
|
+
└──────────────────┬──────────────────────────┘
|
|
57
|
+
│
|
|
58
|
+
┌──────────────────▼──────────────────────────┐
|
|
59
|
+
│ Core Layer │
|
|
60
|
+
│ • Table Management │
|
|
61
|
+
│ • Data Import/Export │
|
|
62
|
+
│ • Validation │
|
|
63
|
+
└──────────────────┬──────────────────────────┘
|
|
64
|
+
│
|
|
65
|
+
┌──────────────────▼──────────────────────────┐
|
|
66
|
+
│ Io Layer │
|
|
67
|
+
│ • IoMem (in-memory) │
|
|
68
|
+
│ • IoFile (file system) │
|
|
69
|
+
│ • IoMulti (redundant storage) │
|
|
70
|
+
└─────────────────────────────────────────────┘
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Core Components
|
|
74
|
+
|
|
75
|
+
### 1. Db Class
|
|
76
|
+
|
|
77
|
+
The main entry point providing high-level query and mutation operations.
|
|
78
|
+
|
|
79
|
+
**Location**: `src/db.ts`
|
|
80
|
+
|
|
81
|
+
**Key Responsibilities**:
|
|
82
|
+
|
|
83
|
+
- Route parsing and resolution
|
|
84
|
+
- Query coordination across controllers
|
|
85
|
+
- Caching query results
|
|
86
|
+
- Insert operation orchestration
|
|
87
|
+
- Notification broadcasting
|
|
88
|
+
|
|
89
|
+
**Key Methods**:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
class Db {
|
|
93
|
+
async get(route, where, filter?, sliceIds?, options?): Promise<ContainerWithControllers>
|
|
94
|
+
async insert(route, data, origin?, refs?): Promise<InsertHistoryRow[]>
|
|
95
|
+
async getInsertHistory(table, options?): Promise<InsertHistoryTable>
|
|
96
|
+
// ... internal methods
|
|
97
|
+
async _get(route, where, controllers, filter, sliceIds, routeAccumulator, options)
|
|
98
|
+
async _getReferenceOfRouteSegment(segment): Promise<string>
|
|
99
|
+
async _resolveSliceIds(table, row): Promise<SliceId[]>
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Data Structures**:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
type Container = {
|
|
107
|
+
rljson: Rljson; // Table data indexed by table name
|
|
108
|
+
tree: Json; // Hierarchical representation
|
|
109
|
+
cell: Cell[]; // Path-value pairs for modifications
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
type Cell = {
|
|
113
|
+
route: Route; // Full route to this cell
|
|
114
|
+
value: JsonValue; // Current value
|
|
115
|
+
row: JsonValue; // Parent row data
|
|
116
|
+
path: Array<Array<string | number>>; // Path segments
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 2. Core Class
|
|
121
|
+
|
|
122
|
+
Low-level data management layer wrapping Io operations.
|
|
123
|
+
|
|
124
|
+
**Location**: `src/core.ts`
|
|
125
|
+
|
|
126
|
+
**Key Responsibilities**:
|
|
127
|
+
|
|
128
|
+
- Table creation and schema management
|
|
129
|
+
- Data import with validation
|
|
130
|
+
- Data export (dump operations)
|
|
131
|
+
- Reading/writing through Io layer
|
|
132
|
+
|
|
133
|
+
**Key Methods**:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
class Core {
|
|
137
|
+
async createTableWithInsertHistory(tableCfg): Promise<void>
|
|
138
|
+
async createTable(tableCfg): Promise<void>
|
|
139
|
+
async import(data): Promise<void>
|
|
140
|
+
async dump(): Promise<Rljson>
|
|
141
|
+
async dumpTable(table): Promise<Rljson>
|
|
142
|
+
async tables(): Promise<Record<string, TableCfg>>
|
|
143
|
+
async readRows(params): Promise<Rljson>
|
|
144
|
+
async write(params): Promise<void>
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 3. Controller Pattern
|
|
149
|
+
|
|
150
|
+
Controllers abstract table-specific operations and provide a uniform interface.
|
|
151
|
+
|
|
152
|
+
**Base Controller** (`src/controller/base-controller.ts`):
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
abstract class Controller<N, R, T> {
|
|
156
|
+
abstract init(): Promise<void>
|
|
157
|
+
abstract get(where, filter?, path?): Promise<Rljson>
|
|
158
|
+
abstract insert(command, value, origin?, refs?): Promise<InsertHistoryRow[]>
|
|
159
|
+
abstract table(): Promise<T>
|
|
160
|
+
abstract tableCfg(): TableCfg
|
|
161
|
+
abstract getChildRefs(hash): Promise<Ref[]>
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Specialized Controllers**:
|
|
166
|
+
|
|
167
|
+
#### TreeController
|
|
168
|
+
|
|
169
|
+
Handles hierarchical tree structures with conditional children expansion.
|
|
170
|
+
|
|
171
|
+
**Critical Fix**: The `path` parameter controls whether children are expanded:
|
|
172
|
+
|
|
173
|
+
- `path === undefined`: WHERE clause query → Returns only requested node
|
|
174
|
+
- `path !== undefined`: Route navigation → Expands children recursively
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
class TreeController extends Controller {
|
|
178
|
+
async get(where, filter?, path?): Promise<Rljson> {
|
|
179
|
+
// ... fetch tree node(s)
|
|
180
|
+
|
|
181
|
+
const shouldExpandChildren = path !== undefined;
|
|
182
|
+
|
|
183
|
+
if (!shouldExpandChildren) {
|
|
184
|
+
// Return only requested node (prevents heap crash)
|
|
185
|
+
return { [tableKey]: { _data: [tree], _type: 'trees' } };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Expand children recursively
|
|
189
|
+
for (const childRef of tree.children ?? []) {
|
|
190
|
+
const child = await this.get(childRef, undefined, treeRoute.deeper().flat);
|
|
191
|
+
children.push(...child[tableKey]._data);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { [tableKey]: { _data: [...children, tree], _type: 'trees' } };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
This design prevents infinite recursion when querying trees by hash while preserving navigation capabilities.
|
|
200
|
+
|
|
201
|
+
#### CakeController
|
|
202
|
+
|
|
203
|
+
Handles multi-dimensional cube data with slicing.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
class CakeController extends Controller {
|
|
207
|
+
// Resolves sliceIds and components
|
|
208
|
+
// Handles dimension-based queries
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### LayerController
|
|
213
|
+
|
|
214
|
+
Manages layered data with inheritance.
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
class LayerController extends Controller {
|
|
218
|
+
// Resolves base layers
|
|
219
|
+
// Handles layer composition
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Data Flow
|
|
224
|
+
|
|
225
|
+
### Query Flow (db.get)
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
1. Parse Route
|
|
229
|
+
Route.fromFlat('/users/projects/tasks')
|
|
230
|
+
↓
|
|
231
|
+
2. Isolate Property Keys
|
|
232
|
+
Extract hash references from route segments
|
|
233
|
+
↓
|
|
234
|
+
3. Index Controllers
|
|
235
|
+
Create/cache controllers for each table in route
|
|
236
|
+
↓
|
|
237
|
+
4. Execute _get (Recursive)
|
|
238
|
+
For each route segment:
|
|
239
|
+
├─ Fetch node data (via controller)
|
|
240
|
+
├─ Apply filters
|
|
241
|
+
├─ Resolve children (if not leaf)
|
|
242
|
+
├─ Recurse to deeper route
|
|
243
|
+
└─ Merge results
|
|
244
|
+
↓
|
|
245
|
+
5. Build Container
|
|
246
|
+
rljson: { users: {...}, projects: {...}, tasks: {...} }
|
|
247
|
+
tree: nested structure
|
|
248
|
+
cell: path-value pairs
|
|
249
|
+
↓
|
|
250
|
+
6. Cache Result
|
|
251
|
+
Store in _cache map by route+where hash
|
|
252
|
+
↓
|
|
253
|
+
7. Return Container + Controllers
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Insert Flow (db.insert)
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
1. Validate Route
|
|
260
|
+
↓
|
|
261
|
+
2. Index Controllers
|
|
262
|
+
↓
|
|
263
|
+
3. Execute _insert (Recursive)
|
|
264
|
+
For each route segment:
|
|
265
|
+
├─ Get target controller
|
|
266
|
+
├─ Extract data for this level
|
|
267
|
+
├─ Call controller.insert()
|
|
268
|
+
├─ Get InsertHistoryRow
|
|
269
|
+
├─ Recurse to children
|
|
270
|
+
└─ Collect history rows
|
|
271
|
+
↓
|
|
272
|
+
4. Notify Callbacks
|
|
273
|
+
Broadcast changes to registered listeners
|
|
274
|
+
↓
|
|
275
|
+
5. Return InsertHistoryRows
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Route Resolution
|
|
279
|
+
|
|
280
|
+
Routes define paths through related data. The route system supports:
|
|
281
|
+
|
|
282
|
+
### Route Syntax
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
/tableName # Root table
|
|
286
|
+
/tableName@hash # Specific row by hash
|
|
287
|
+
/tableName@timeId # Historic version
|
|
288
|
+
/tableName/childTable # Relationship traversal
|
|
289
|
+
/tableName@hash/childTable@hash2 # Nested navigation
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Route Segments
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
type RouteSegment = {
|
|
296
|
+
tableKey: string; // Table name
|
|
297
|
+
ref?: string; // Hash or timeId
|
|
298
|
+
propertyKey?: string; // For tree navigation
|
|
299
|
+
sliceIds?: SliceId[]; // Dimension filters
|
|
300
|
+
};
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Route Resolution Process
|
|
304
|
+
|
|
305
|
+
1. **Parse**: Split flat route string into segments
|
|
306
|
+
2. **Validate**: Check table existence and relationships
|
|
307
|
+
3. **Resolve References**:
|
|
308
|
+
- Look up hash references in insert history
|
|
309
|
+
- Convert timeIds to current hashes
|
|
310
|
+
- Handle default refs (latest version)
|
|
311
|
+
4. **Navigate**: Traverse from root to leaf segment
|
|
312
|
+
|
|
313
|
+
### Example Resolution
|
|
314
|
+
|
|
315
|
+
Route: `/users@hash123/projects`
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
Segment 1: users@hash123
|
|
319
|
+
├─ Table: users
|
|
320
|
+
├─ Ref: hash123
|
|
321
|
+
└─ Query: { _hash: 'hash123' }
|
|
322
|
+
|
|
323
|
+
Segment 2: projects
|
|
324
|
+
├─ Table: projects
|
|
325
|
+
├─ Parent: users (hash123)
|
|
326
|
+
└─ Query: WHERE projects references hash123
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Caching Strategy
|
|
330
|
+
|
|
331
|
+
### Cache Key Generation
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
const cacheKey = hsh({
|
|
335
|
+
route: route.flat,
|
|
336
|
+
where: where,
|
|
337
|
+
filter: filter,
|
|
338
|
+
sliceIds: sliceIds,
|
|
339
|
+
routeAccumulator: routeAccumulator.flat,
|
|
340
|
+
options: options
|
|
341
|
+
})._hash;
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Cache Conditions
|
|
345
|
+
|
|
346
|
+
Caching is enabled when:
|
|
347
|
+
|
|
348
|
+
- Route contains hash references (`@hash`)
|
|
349
|
+
- Filters are applied
|
|
350
|
+
- SliceIds are specified
|
|
351
|
+
|
|
352
|
+
Caching is disabled for:
|
|
353
|
+
|
|
354
|
+
- Empty WHERE clauses
|
|
355
|
+
- Routes without references
|
|
356
|
+
- Time-based queries (to ensure freshness)
|
|
357
|
+
|
|
358
|
+
### Cache Invalidation
|
|
359
|
+
|
|
360
|
+
- Manual: `db.clearCache()`
|
|
361
|
+
- Automatic: On insert operations (via notify callbacks)
|
|
362
|
+
- Size-based: LRU eviction (if implemented)
|
|
363
|
+
|
|
364
|
+
## Tree Processing
|
|
365
|
+
|
|
366
|
+
### Tree Structure
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
type Tree = {
|
|
370
|
+
id: string; // Node identifier
|
|
371
|
+
children: string[]; // Hash references to child nodes
|
|
372
|
+
meta: Json; // Node data
|
|
373
|
+
isParent: boolean; // Has children flag
|
|
374
|
+
_hash: string; // Content hash
|
|
375
|
+
};
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Tree Expansion Algorithm
|
|
379
|
+
|
|
380
|
+
**Problem**: Trees use content-addressed children (hash references). Naively expanding all children causes:
|
|
381
|
+
|
|
382
|
+
- Infinite recursion on circular refs
|
|
383
|
+
- Heap exhaustion on large trees
|
|
384
|
+
- O(n²) complexity on deep trees
|
|
385
|
+
|
|
386
|
+
**Solution**: Conditional expansion based on query context
|
|
387
|
+
|
|
388
|
+
### Tree INSERT and Root Node Creation
|
|
389
|
+
|
|
390
|
+
**Problem**: The `treeFromObject` function (from `@rljson/rljson`) automatically creates an explicit root node with `id='root'`. During INSERT operations, if the tree object already represents an isolated subtree (e.g., from `isolate()`), this creates a double-root structure:
|
|
391
|
+
- Auto-root (id='root') → User-root (id='root') → actual data nodes
|
|
392
|
+
|
|
393
|
+
This causes navigation issues because `TreeController` stops at the first node matching `id='root'`.
|
|
394
|
+
|
|
395
|
+
**Solution**: The `treeFromObject` call in `db.ts` (line 1365) uses a `skipRootCreation` parameter:
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
const trees = treeFromObject(treeObject, true); // true = skip automatic root creation
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
This prevents the extra root wrapper when inserting tree data, allowing the subtree to be inserted with its existing structure intact.
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
async get(where, filter?, path?): Promise<Rljson> {
|
|
405
|
+
// Fetch matching tree nodes
|
|
406
|
+
const trees = await this._core.readRows({...});
|
|
407
|
+
|
|
408
|
+
// Single node check
|
|
409
|
+
if (trees.length === 1) {
|
|
410
|
+
const tree = trees[0];
|
|
411
|
+
|
|
412
|
+
// KEY DECISION: Expand children only if navigating
|
|
413
|
+
const shouldExpandChildren = path !== undefined;
|
|
414
|
+
|
|
415
|
+
if (!shouldExpandChildren) {
|
|
416
|
+
// WHERE clause query - return only this node
|
|
417
|
+
return { [tableKey]: { _data: [tree], _type: 'trees' } };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Route navigation - expand children
|
|
421
|
+
const children = [];
|
|
422
|
+
for (const childHash of tree.children ?? []) {
|
|
423
|
+
const child = await this.get(
|
|
424
|
+
childHash,
|
|
425
|
+
undefined,
|
|
426
|
+
treeRoute.deeper().flat // Pass path to trigger expansion
|
|
427
|
+
);
|
|
428
|
+
children.push(...child[tableKey]._data);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return { [tableKey]: { _data: [...children, tree], _type: 'trees' } };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Multiple nodes or no nodes
|
|
435
|
+
return { [tableKey]: { _data: trees, _type: 'trees' } };
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Tree Memoization
|
|
440
|
+
|
|
441
|
+
The `buildTreeFromTrees` method uses memoization to avoid reprocessing nodes:
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
async buildTreeFromTrees(trees: Tree[]): Promise<Json> {
|
|
445
|
+
const memo = new Map<string, Json>();
|
|
446
|
+
|
|
447
|
+
const buildNode = async (hash: string): Promise<Json> => {
|
|
448
|
+
if (memo.has(hash)) return memo.get(hash)!;
|
|
449
|
+
|
|
450
|
+
const tree = trees.find(t => t._hash === hash);
|
|
451
|
+
if (!tree) throw new Error(`Tree node ${hash} not found`);
|
|
452
|
+
|
|
453
|
+
const node = { ...tree.meta };
|
|
454
|
+
|
|
455
|
+
if (tree.children && tree.children.length > 0) {
|
|
456
|
+
for (const childHash of tree.children) {
|
|
457
|
+
const childTree = trees.find(t => t._hash === childHash);
|
|
458
|
+
if (childTree) {
|
|
459
|
+
node[childTree.id] = await buildNode(childHash);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
memo.set(hash, node);
|
|
465
|
+
return node;
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// Build from root (last element)
|
|
469
|
+
return buildNode(trees[trees.length - 1]._hash);
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Safety Mechanisms
|
|
474
|
+
|
|
475
|
+
1. **Recursion Depth Limit**: 100 levels max
|
|
476
|
+
2. **Path-based Expansion**: Only expand when navigating
|
|
477
|
+
3. **Memoization**: Prevent redundant processing
|
|
478
|
+
4. **Early Returns**: Short-circuit on leaf nodes
|
|
479
|
+
|
|
480
|
+
## Join System
|
|
481
|
+
|
|
482
|
+
The Join system provides SQL-like operations on query results.
|
|
483
|
+
|
|
484
|
+
**Location**: `src/join/join.ts`
|
|
485
|
+
|
|
486
|
+
### Join Operations
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
class Join {
|
|
490
|
+
// Selection (columns)
|
|
491
|
+
select(columnSelection: ColumnSelection): void
|
|
492
|
+
|
|
493
|
+
// Filtering (rows)
|
|
494
|
+
filter(rowFilter: RowFilter): void
|
|
495
|
+
|
|
496
|
+
// Sorting
|
|
497
|
+
sort(rowSort: RowSort): void
|
|
498
|
+
|
|
499
|
+
// Value mutation
|
|
500
|
+
setValue(setValue: SetValue): void
|
|
501
|
+
|
|
502
|
+
// Result access
|
|
503
|
+
async rows(): Promise<JoinRows>
|
|
504
|
+
async columns(): Promise<JoinColumn[]>
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Join Processing Pipeline
|
|
509
|
+
|
|
510
|
+
```
|
|
511
|
+
Container (rljson, tree, cell)
|
|
512
|
+
↓
|
|
513
|
+
Initialize Join
|
|
514
|
+
↓
|
|
515
|
+
┌─────────────┐
|
|
516
|
+
│ Selection │ → Filter columns
|
|
517
|
+
└──────┬──────┘
|
|
518
|
+
↓
|
|
519
|
+
┌─────────────┐
|
|
520
|
+
│ Filter │ → Filter rows
|
|
521
|
+
└──────┬──────┘
|
|
522
|
+
↓
|
|
523
|
+
┌─────────────┐
|
|
524
|
+
│ Sort │ → Order rows
|
|
525
|
+
└──────┬──────┘
|
|
526
|
+
↓
|
|
527
|
+
┌─────────────┐
|
|
528
|
+
│ SetValue │ → Modify values
|
|
529
|
+
└──────┬──────┘
|
|
530
|
+
↓
|
|
531
|
+
Result (JoinRows)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Column Selection
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
class ColumnSelection {
|
|
538
|
+
constructor(
|
|
539
|
+
route: Route,
|
|
540
|
+
columns: ColumnInfo[],
|
|
541
|
+
selectionType: 'include' | 'exclude' = 'include'
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
process(rows: JoinRows): JoinRows
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Row Filtering
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
class RowFilter {
|
|
552
|
+
constructor(
|
|
553
|
+
route: Route,
|
|
554
|
+
filters: Record<string, FilterProcessor>
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
process(rows: JoinRows): JoinRows
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
Supported filter types:
|
|
562
|
+
|
|
563
|
+
- String filters (equals, contains, startsWith, endsWith, regex)
|
|
564
|
+
- Number filters (equals, gt, gte, lt, lte, range)
|
|
565
|
+
- Boolean filters (equals, notEquals)
|
|
566
|
+
- Column filters (compare columns)
|
|
567
|
+
|
|
568
|
+
## Multi-Edit System
|
|
569
|
+
|
|
570
|
+
The MultiEditManager provides transactional editing capabilities.
|
|
571
|
+
|
|
572
|
+
**Location**: `src/edit/multi-edit-manager.ts`
|
|
573
|
+
|
|
574
|
+
### Architecture
|
|
575
|
+
|
|
576
|
+
```
|
|
577
|
+
MultiEditManager
|
|
578
|
+
├─ Tracks head MultiEditProcessor
|
|
579
|
+
├─ Registers as Db notify callback
|
|
580
|
+
├─ Manages edit history
|
|
581
|
+
└─ Publishes completed edits
|
|
582
|
+
|
|
583
|
+
MultiEditProcessor
|
|
584
|
+
├─ Executes individual EditActions
|
|
585
|
+
├─ Maintains intermediate state
|
|
586
|
+
├─ Builds result Join
|
|
587
|
+
└─ Supports rollback
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Edit Actions
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
type EditAction = {
|
|
594
|
+
type: 'columnSelection' | 'rowFilter' | 'rowSort' | 'setValue';
|
|
595
|
+
params: {...};
|
|
596
|
+
};
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
### Transaction Flow
|
|
600
|
+
|
|
601
|
+
```
|
|
602
|
+
1. Create MultiEditManager
|
|
603
|
+
↓
|
|
604
|
+
2. Start multiEdit()
|
|
605
|
+
├─ Get or create head processor
|
|
606
|
+
├─ Execute edit callback
|
|
607
|
+
└─ Return updated head
|
|
608
|
+
↓
|
|
609
|
+
3. Apply EditActions
|
|
610
|
+
head.edit(action1)
|
|
611
|
+
head.edit(action2)
|
|
612
|
+
↓
|
|
613
|
+
4. Publish Head
|
|
614
|
+
manager.publishHead()
|
|
615
|
+
├─ Get result Join
|
|
616
|
+
├─ Insert into Db
|
|
617
|
+
└─ Notify observers
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
## Design Decisions
|
|
621
|
+
|
|
622
|
+
### 1. Content-Addressed Immutability
|
|
623
|
+
|
|
624
|
+
**Decision**: All data is immutable and identified by content hash.
|
|
625
|
+
|
|
626
|
+
**Rationale**:
|
|
627
|
+
|
|
628
|
+
- Eliminates update conflicts
|
|
629
|
+
- Enables perfect caching
|
|
630
|
+
- Supports version history naturally
|
|
631
|
+
- Allows secure data sharing
|
|
632
|
+
|
|
633
|
+
**Trade-offs**:
|
|
634
|
+
|
|
635
|
+
- Higher storage requirements
|
|
636
|
+
- More complex mutation patterns
|
|
637
|
+
- Requires hash computation overhead
|
|
638
|
+
|
|
639
|
+
### 2. Controller Abstraction
|
|
640
|
+
|
|
641
|
+
**Decision**: Each table type has a specialized controller.
|
|
642
|
+
|
|
643
|
+
**Rationale**:
|
|
644
|
+
|
|
645
|
+
- Encapsulates type-specific logic
|
|
646
|
+
- Enables polymorphic operations
|
|
647
|
+
- Simplifies testing and maintenance
|
|
648
|
+
- Supports future table types
|
|
649
|
+
|
|
650
|
+
**Trade-offs**:
|
|
651
|
+
|
|
652
|
+
- More complex class hierarchy
|
|
653
|
+
- Requires careful interface design
|
|
654
|
+
|
|
655
|
+
### 3. Recursive Route Resolution
|
|
656
|
+
|
|
657
|
+
**Decision**: Routes are resolved recursively, segment by segment.
|
|
658
|
+
|
|
659
|
+
**Rationale**:
|
|
660
|
+
|
|
661
|
+
- Handles arbitrary nesting depth
|
|
662
|
+
- Natural fit for hierarchical data
|
|
663
|
+
- Enables lazy loading
|
|
664
|
+
- Supports circular references
|
|
665
|
+
|
|
666
|
+
**Trade-offs**:
|
|
667
|
+
|
|
668
|
+
- Risk of stack overflow (mitigated by depth limit)
|
|
669
|
+
- Complex debugging
|
|
670
|
+
- Performance overhead
|
|
671
|
+
|
|
672
|
+
### 4. Path-Based Tree Expansion
|
|
673
|
+
|
|
674
|
+
**Decision**: TreeController uses `path` parameter to control expansion.
|
|
675
|
+
|
|
676
|
+
**Rationale**:
|
|
677
|
+
|
|
678
|
+
- Prevents heap crashes on large trees
|
|
679
|
+
- Preserves navigation functionality
|
|
680
|
+
- Simple, clear semantics
|
|
681
|
+
- Minimal API changes
|
|
682
|
+
|
|
683
|
+
**Trade-offs**:
|
|
684
|
+
|
|
685
|
+
- Subtle behavior difference between query types
|
|
686
|
+
- Requires understanding of path parameter
|
|
687
|
+
- Documentation burden
|
|
688
|
+
|
|
689
|
+
### 5. Caching by Query Signature
|
|
690
|
+
|
|
691
|
+
**Decision**: Cache results by hash of route+where+filter+options.
|
|
692
|
+
|
|
693
|
+
**Rationale**:
|
|
694
|
+
|
|
695
|
+
- Optimal cache hit rate
|
|
696
|
+
- Automatic cache key generation
|
|
697
|
+
- No manual cache management
|
|
698
|
+
- Supports complex queries
|
|
699
|
+
|
|
700
|
+
**Trade-offs**:
|
|
701
|
+
|
|
702
|
+
- Memory growth on diverse queries
|
|
703
|
+
- No automatic invalidation
|
|
704
|
+
- Hash computation overhead
|
|
705
|
+
|
|
706
|
+
### 6. Join System Design
|
|
707
|
+
|
|
708
|
+
**Decision**: Separate Join class for data transformation.
|
|
709
|
+
|
|
710
|
+
**Rationale**:
|
|
711
|
+
|
|
712
|
+
- Separation of concerns
|
|
713
|
+
- Composable operations
|
|
714
|
+
- Testable in isolation
|
|
715
|
+
- Reusable across contexts
|
|
716
|
+
|
|
717
|
+
**Trade-offs**:
|
|
718
|
+
|
|
719
|
+
- Additional abstraction layer
|
|
720
|
+
- More object creation
|
|
721
|
+
- Steeper learning curve
|
|
722
|
+
|
|
723
|
+
## Performance Considerations
|
|
724
|
+
|
|
725
|
+
### Query Optimization
|
|
726
|
+
|
|
727
|
+
1. **Batch SliceId Resolution**: Resolve all sliceIds in parallel
|
|
728
|
+
2. **Controller Memoization**: Cache controller instances
|
|
729
|
+
3. **Result Caching**: Cache query results by signature
|
|
730
|
+
4. **Lazy Loading**: Only fetch data as needed
|
|
731
|
+
5. **Parallel Fetches**: Fetch independent data concurrently
|
|
732
|
+
|
|
733
|
+
### Memory Management
|
|
734
|
+
|
|
735
|
+
1. **Streaming**: Use streaming for large data exports
|
|
736
|
+
2. **Pagination**: Support paginated queries (future)
|
|
737
|
+
3. **Cache Eviction**: Implement LRU cache policy (future)
|
|
738
|
+
4. **Weak References**: Use WeakMap for temporary data (future)
|
|
739
|
+
|
|
740
|
+
### Recursion Safety
|
|
741
|
+
|
|
742
|
+
1. **Depth Limiting**: Max 100 recursion levels
|
|
743
|
+
2. **Path Checking**: Detect navigation context
|
|
744
|
+
3. **Memoization**: Avoid redundant processing
|
|
745
|
+
4. **Early Exits**: Short-circuit when possible
|
|
746
|
+
|
|
747
|
+
## Testing Strategy
|
|
748
|
+
|
|
749
|
+
### Unit Tests
|
|
750
|
+
|
|
751
|
+
- Controller operations (get, insert, table)
|
|
752
|
+
- Route parsing and validation
|
|
753
|
+
- Filter processing
|
|
754
|
+
- Cache behavior
|
|
755
|
+
|
|
756
|
+
### Integration Tests
|
|
757
|
+
|
|
758
|
+
- Full query flows (db.get)
|
|
759
|
+
- Insert operations (db.insert)
|
|
760
|
+
- Version history
|
|
761
|
+
- Multi-edit transactions
|
|
762
|
+
|
|
763
|
+
### Performance Tests
|
|
764
|
+
|
|
765
|
+
- Large tree queries
|
|
766
|
+
- Deep recursion
|
|
767
|
+
- Cache hit rates
|
|
768
|
+
- Memory usage
|
|
769
|
+
|
|
770
|
+
### Critical Test Cases
|
|
771
|
+
|
|
772
|
+
```typescript
|
|
773
|
+
describe('Tree WHERE clause fix', () => {
|
|
774
|
+
it('should return ONLY ONE NODE when querying by _hash');
|
|
775
|
+
it('should prevent heap crash with large trees');
|
|
776
|
+
it('should expand children when path is provided');
|
|
777
|
+
});
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
## Future Enhancements
|
|
781
|
+
|
|
782
|
+
### Planned Features
|
|
783
|
+
|
|
784
|
+
1. **Query Optimization**
|
|
785
|
+
- Query planner
|
|
786
|
+
- Index support
|
|
787
|
+
- Parallel execution
|
|
788
|
+
|
|
789
|
+
2. **Advanced Caching**
|
|
790
|
+
- LRU eviction
|
|
791
|
+
- Cache warming
|
|
792
|
+
- Distributed caching
|
|
793
|
+
|
|
794
|
+
3. **Pagination**
|
|
795
|
+
- Cursor-based pagination
|
|
796
|
+
- Offset-limit pagination
|
|
797
|
+
- Stream processing
|
|
798
|
+
|
|
799
|
+
4. **Transactions**
|
|
800
|
+
- ACID guarantees
|
|
801
|
+
- Rollback support
|
|
802
|
+
- Conflict resolution
|
|
803
|
+
|
|
804
|
+
5. **Replication**
|
|
805
|
+
- Master-slave replication
|
|
806
|
+
- Multi-master replication
|
|
807
|
+
- Conflict-free replicated data types (CRDTs)
|
|
808
|
+
|
|
809
|
+
## Debugging Tips
|
|
810
|
+
|
|
811
|
+
### Enable Query Logging
|
|
812
|
+
|
|
813
|
+
```typescript
|
|
814
|
+
// Add to Db._get for debugging
|
|
815
|
+
console.log('Route:', route.flat);
|
|
816
|
+
console.log('Where:', JSON.stringify(where));
|
|
817
|
+
console.log('Depth:', depth);
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
### Trace Tree Expansion
|
|
821
|
+
|
|
822
|
+
```typescript
|
|
823
|
+
// Add to TreeController.get
|
|
824
|
+
console.log(`Tree expansion: ${tree.id}, path=${path}`);
|
|
825
|
+
console.log(`Should expand: ${path !== undefined}`);
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
### Cache Analysis
|
|
829
|
+
|
|
830
|
+
```typescript
|
|
831
|
+
console.log('Cache size:', db.cache.size);
|
|
832
|
+
console.log('Cache keys:', Array.from(db.cache.keys()));
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
### Route Inspection
|
|
836
|
+
|
|
837
|
+
```typescript
|
|
838
|
+
console.log('Route segments:', route.segments);
|
|
839
|
+
console.log('Property key:', route.propertyKey);
|
|
840
|
+
console.log('Is valid:', route.isValid);
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
## Contributing
|
|
844
|
+
|
|
845
|
+
When contributing to @rljson/db:
|
|
846
|
+
|
|
847
|
+
1. **Understand the layers**: Know which layer owns which responsibility
|
|
848
|
+
2. **Preserve immutability**: Never modify data in place
|
|
849
|
+
3. **Test tree operations**: Always test with large/deep trees
|
|
850
|
+
4. **Document behavior**: Explain non-obvious design choices
|
|
851
|
+
5. **Benchmark changes**: Profile performance-critical paths
|
|
852
|
+
6. **Update tests**: Add tests for new features and bug fixes
|
|
853
|
+
|
|
854
|
+
## References
|
|
855
|
+
|
|
856
|
+
- [RLJSON Specification](https://github.com/rljson/rljson)
|
|
857
|
+
- [Content-Addressed Storage](https://en.wikipedia.org/wiki/Content-addressable_storage)
|
|
858
|
+
- [Merkle Trees](https://en.wikipedia.org/wiki/Merkle_tree)
|
|
859
|
+
- [Immutable Data Structures](https://en.wikipedia.org/wiki/Persistent_data_structure)
|