ai-database 0.0.0-development → 0.2.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.
Files changed (79) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-test.log +102 -0
  3. package/README.md +402 -47
  4. package/TESTING.md +410 -0
  5. package/TEST_SUMMARY.md +250 -0
  6. package/TODO.md +128 -0
  7. package/dist/ai-promise-db.d.ts +370 -0
  8. package/dist/ai-promise-db.d.ts.map +1 -0
  9. package/dist/ai-promise-db.js +839 -0
  10. package/dist/ai-promise-db.js.map +1 -0
  11. package/dist/authorization.d.ts +531 -0
  12. package/dist/authorization.d.ts.map +1 -0
  13. package/dist/authorization.js +632 -0
  14. package/dist/authorization.js.map +1 -0
  15. package/dist/durable-clickhouse.d.ts +193 -0
  16. package/dist/durable-clickhouse.d.ts.map +1 -0
  17. package/dist/durable-clickhouse.js +422 -0
  18. package/dist/durable-clickhouse.js.map +1 -0
  19. package/dist/durable-promise.d.ts +182 -0
  20. package/dist/durable-promise.d.ts.map +1 -0
  21. package/dist/durable-promise.js +409 -0
  22. package/dist/durable-promise.js.map +1 -0
  23. package/dist/execution-queue.d.ts +239 -0
  24. package/dist/execution-queue.d.ts.map +1 -0
  25. package/dist/execution-queue.js +400 -0
  26. package/dist/execution-queue.js.map +1 -0
  27. package/dist/index.d.ts +54 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +79 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/linguistic.d.ts +115 -0
  32. package/dist/linguistic.d.ts.map +1 -0
  33. package/dist/linguistic.js +379 -0
  34. package/dist/linguistic.js.map +1 -0
  35. package/dist/memory-provider.d.ts +304 -0
  36. package/dist/memory-provider.d.ts.map +1 -0
  37. package/dist/memory-provider.js +785 -0
  38. package/dist/memory-provider.js.map +1 -0
  39. package/dist/schema.d.ts +899 -0
  40. package/dist/schema.d.ts.map +1 -0
  41. package/dist/schema.js +1165 -0
  42. package/dist/schema.js.map +1 -0
  43. package/dist/tests.d.ts +107 -0
  44. package/dist/tests.d.ts.map +1 -0
  45. package/dist/tests.js +568 -0
  46. package/dist/tests.js.map +1 -0
  47. package/dist/types.d.ts +972 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +126 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +37 -23
  52. package/src/ai-promise-db.ts +1243 -0
  53. package/src/authorization.ts +1102 -0
  54. package/src/durable-clickhouse.ts +596 -0
  55. package/src/durable-promise.ts +582 -0
  56. package/src/execution-queue.ts +608 -0
  57. package/src/index.test.ts +868 -0
  58. package/src/index.ts +337 -0
  59. package/src/linguistic.ts +404 -0
  60. package/src/memory-provider.test.ts +1036 -0
  61. package/src/memory-provider.ts +1119 -0
  62. package/src/schema.test.ts +1254 -0
  63. package/src/schema.ts +2296 -0
  64. package/src/tests.ts +725 -0
  65. package/src/types.ts +1177 -0
  66. package/test/README.md +153 -0
  67. package/test/edge-cases.test.ts +646 -0
  68. package/test/provider-resolution.test.ts +402 -0
  69. package/tsconfig.json +9 -0
  70. package/vitest.config.ts +19 -0
  71. package/LICENSE +0 -21
  72. package/dist/types/database.d.ts +0 -46
  73. package/dist/types/document.d.ts +0 -15
  74. package/dist/types/index.d.ts +0 -5
  75. package/dist/types/mdxdb/embedding.d.ts +0 -7
  76. package/dist/types/mdxdb/types.d.ts +0 -59
  77. package/dist/types/synthetic.d.ts +0 -9
  78. package/dist/types/tools.d.ts +0 -10
  79. package/dist/types/vector.d.ts +0 -16
@@ -0,0 +1,596 @@
1
+ /**
2
+ * ClickHouse-backed Durable Promise Provider
3
+ *
4
+ * Uses @mdxdb/clickhouse as the persistence layer for DurablePromise.
5
+ * Provides full durability, crash recovery, and observability.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import type { ExecutionPriority, DurablePromiseOptions, BatchScheduler } from './durable-promise.js'
11
+ import { DurablePromise, getCurrentContext, setBatchScheduler } from './durable-promise.js'
12
+ import { Semaphore } from './memory-provider.js'
13
+
14
+ // =============================================================================
15
+ // Types
16
+ // =============================================================================
17
+
18
+ /**
19
+ * ClickHouse action row structure (minimal fields needed)
20
+ */
21
+ interface ActionRow {
22
+ id: string
23
+ ns: string
24
+ actor: string
25
+ act: string
26
+ action: string
27
+ activity: string
28
+ object: string
29
+ objectData: Record<string, unknown>
30
+ status: 'pending' | 'active' | 'completed' | 'failed' | 'cancelled'
31
+ progress: number
32
+ total: number
33
+ result: Record<string, unknown>
34
+ error: string
35
+ data: Record<string, unknown>
36
+ meta: Record<string, unknown>
37
+ priority: number
38
+ batch: string
39
+ batchIndex: number
40
+ batchTotal: number
41
+ dependencies: string[]
42
+ scheduledAt: string | null
43
+ startedAt: string | null
44
+ completedAt: string | null
45
+ createdAt: string
46
+ updatedAt: string
47
+ }
48
+
49
+ /**
50
+ * Executor interface for ClickHouse operations
51
+ */
52
+ export interface ClickHouseExecutor {
53
+ query<T = unknown>(sql: string): Promise<T[]>
54
+ command(sql: string): Promise<void>
55
+ insert<T>(table: string, values: T[]): Promise<void>
56
+ close(): Promise<void>
57
+ }
58
+
59
+ /**
60
+ * Configuration for the ClickHouse durable provider
61
+ */
62
+ export interface ClickHouseDurableConfig {
63
+ /** ClickHouse executor instance */
64
+ executor: ClickHouseExecutor
65
+
66
+ /** Default namespace for actions */
67
+ namespace?: string
68
+
69
+ /** Concurrency limits by priority tier */
70
+ concurrency?: {
71
+ priority?: number
72
+ standard?: number
73
+ flex?: number
74
+ batch?: number
75
+ }
76
+
77
+ /** Batch window in milliseconds */
78
+ batchWindow?: number
79
+
80
+ /** Maximum batch size before auto-flush */
81
+ maxBatchSize?: number
82
+
83
+ /** Poll interval for checking action status (ms) */
84
+ pollInterval?: number
85
+
86
+ /** Auto-recover pending actions on start */
87
+ autoRecover?: boolean
88
+ }
89
+
90
+ /**
91
+ * Priority tier to numeric priority mapping
92
+ * Lower number = higher priority
93
+ */
94
+ const PRIORITY_MAP: Record<ExecutionPriority, number> = {
95
+ priority: 1,
96
+ standard: 5,
97
+ flex: 7,
98
+ batch: 9,
99
+ }
100
+
101
+ /**
102
+ * Numeric priority to tier mapping
103
+ */
104
+ const PRIORITY_REVERSE: Record<number, ExecutionPriority> = {
105
+ 1: 'priority',
106
+ 5: 'standard',
107
+ 7: 'flex',
108
+ 9: 'batch',
109
+ }
110
+
111
+ // =============================================================================
112
+ // ClickHouse Durable Provider
113
+ // =============================================================================
114
+
115
+ /**
116
+ * ClickHouse-backed provider for durable promises
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * import { createClickHouseDatabase } from '@mdxdb/clickhouse'
121
+ * import { ClickHouseDurableProvider } from 'ai-database'
122
+ *
123
+ * const db = await createClickHouseDatabase({ url: 'http://localhost:8123' })
124
+ * const provider = new ClickHouseDurableProvider({
125
+ * executor: db.getExecutor(),
126
+ * namespace: 'myapp.example.com',
127
+ * })
128
+ *
129
+ * // Use context for automatic persistence
130
+ * await provider.withContext({ priority: 'batch' }, async () => {
131
+ * const result = await ai.generate({ prompt: 'Hello' })
132
+ * })
133
+ *
134
+ * // Flush batched operations
135
+ * await provider.flush()
136
+ * ```
137
+ */
138
+ export class ClickHouseDurableProvider implements BatchScheduler {
139
+ private readonly executor: ClickHouseExecutor
140
+ private readonly namespace: string
141
+ private readonly semaphores: Record<ExecutionPriority, Semaphore>
142
+ private readonly config: Required<Omit<ClickHouseDurableConfig, 'executor'>>
143
+
144
+ // Batch queue
145
+ private readonly batchQueue: Map<string, DurablePromise<unknown>> = new Map()
146
+ private batchTimer: ReturnType<typeof setTimeout> | null = null
147
+
148
+ // Tracking
149
+ private pendingCount = 0
150
+ private activeCount = 0
151
+ private completedCount = 0
152
+ private failedCount = 0
153
+
154
+ constructor(config: ClickHouseDurableConfig) {
155
+ this.executor = config.executor
156
+ this.namespace = config.namespace ?? 'default'
157
+
158
+ this.config = {
159
+ namespace: this.namespace,
160
+ concurrency: {
161
+ priority: config.concurrency?.priority ?? 50,
162
+ standard: config.concurrency?.standard ?? 20,
163
+ flex: config.concurrency?.flex ?? 10,
164
+ batch: config.concurrency?.batch ?? 1000,
165
+ },
166
+ batchWindow: config.batchWindow ?? 60000,
167
+ maxBatchSize: config.maxBatchSize ?? 10000,
168
+ pollInterval: config.pollInterval ?? 1000,
169
+ autoRecover: config.autoRecover ?? true,
170
+ }
171
+
172
+ // Initialize semaphores
173
+ this.semaphores = {
174
+ priority: new Semaphore(this.config.concurrency.priority!),
175
+ standard: new Semaphore(this.config.concurrency.standard!),
176
+ flex: new Semaphore(this.config.concurrency.flex!),
177
+ batch: new Semaphore(this.config.concurrency.batch!),
178
+ }
179
+
180
+ // Register as global batch scheduler
181
+ setBatchScheduler(this)
182
+
183
+ // Auto-recover on init
184
+ if (this.config.autoRecover) {
185
+ this.recover().catch(console.error)
186
+ }
187
+ }
188
+
189
+ // ===========================================================================
190
+ // Action Creation
191
+ // ===========================================================================
192
+
193
+ /**
194
+ * Create an action in ClickHouse
195
+ */
196
+ async createAction(options: {
197
+ id: string
198
+ method: string
199
+ args?: unknown[]
200
+ priority: ExecutionPriority
201
+ actor?: string
202
+ dependsOn?: string[]
203
+ deferUntil?: Date
204
+ meta?: Record<string, unknown>
205
+ }): Promise<ActionRow> {
206
+ const now = new Date().toISOString()
207
+ const verb = this.parseVerb(options.method)
208
+
209
+ const action: Partial<ActionRow> = {
210
+ id: options.id,
211
+ ns: this.namespace,
212
+ actor: options.actor ?? getCurrentContext()?.actor ?? 'system',
213
+ act: verb,
214
+ action: `${verb}s`,
215
+ activity: `${verb}ing`,
216
+ object: options.method,
217
+ objectData: {
218
+ method: options.method,
219
+ args: options.args,
220
+ },
221
+ status: 'pending',
222
+ progress: 0,
223
+ total: 1,
224
+ result: {},
225
+ error: '',
226
+ data: {},
227
+ meta: options.meta ?? {},
228
+ priority: PRIORITY_MAP[options.priority],
229
+ batch: '',
230
+ batchIndex: 0,
231
+ batchTotal: 0,
232
+ dependencies: options.dependsOn ?? [],
233
+ scheduledAt: options.deferUntil?.toISOString() ?? null,
234
+ startedAt: null,
235
+ completedAt: null,
236
+ createdAt: now,
237
+ updatedAt: now,
238
+ }
239
+
240
+ await this.executor.insert('Actions', [action])
241
+ this.pendingCount++
242
+
243
+ return action as ActionRow
244
+ }
245
+
246
+ /**
247
+ * Update action status
248
+ */
249
+ async updateAction(
250
+ id: string,
251
+ updates: Partial<Pick<ActionRow, 'status' | 'progress' | 'result' | 'error' | 'startedAt' | 'completedAt'>>
252
+ ): Promise<void> {
253
+ const now = new Date().toISOString()
254
+
255
+ // Get existing action
256
+ const rows = await this.executor.query<ActionRow>(
257
+ `SELECT * FROM Actions FINAL WHERE id = '${this.escapeString(id)}' AND ns = '${this.namespace}' ORDER BY updatedAt DESC LIMIT 1`
258
+ )
259
+
260
+ if (rows.length === 0) {
261
+ throw new Error(`Action not found: ${id}`)
262
+ }
263
+
264
+ const existing = rows[0]!
265
+
266
+ // Track status changes
267
+ if (updates.status && updates.status !== existing.status) {
268
+ if (existing.status === 'pending') this.pendingCount--
269
+ if (existing.status === 'active') this.activeCount--
270
+
271
+ if (updates.status === 'active') this.activeCount++
272
+ if (updates.status === 'completed') this.completedCount++
273
+ if (updates.status === 'failed') this.failedCount++
274
+ }
275
+
276
+ // Insert new row with updates (ReplacingMergeTree will handle dedup)
277
+ await this.executor.insert('Actions', [{
278
+ ...existing,
279
+ ...updates,
280
+ updatedAt: now,
281
+ }])
282
+ }
283
+
284
+ /**
285
+ * Get action by ID
286
+ */
287
+ async getAction(id: string): Promise<ActionRow | null> {
288
+ const rows = await this.executor.query<ActionRow>(
289
+ `SELECT * FROM Actions FINAL WHERE id = '${this.escapeString(id)}' AND ns = '${this.namespace}' ORDER BY updatedAt DESC LIMIT 1`
290
+ )
291
+ return rows[0] ?? null
292
+ }
293
+
294
+ /**
295
+ * List actions by status
296
+ */
297
+ async listActions(options: {
298
+ status?: ActionRow['status'] | ActionRow['status'][]
299
+ priority?: ExecutionPriority
300
+ limit?: number
301
+ } = {}): Promise<ActionRow[]> {
302
+ const conditions: string[] = [`ns = '${this.namespace}'`]
303
+
304
+ if (options.status) {
305
+ if (Array.isArray(options.status)) {
306
+ const statuses = options.status.map(s => `'${s}'`).join(', ')
307
+ conditions.push(`status IN (${statuses})`)
308
+ } else {
309
+ conditions.push(`status = '${options.status}'`)
310
+ }
311
+ }
312
+
313
+ if (options.priority) {
314
+ conditions.push(`priority = ${PRIORITY_MAP[options.priority]}`)
315
+ }
316
+
317
+ const limit = options.limit ? `LIMIT ${options.limit}` : ''
318
+
319
+ return this.executor.query<ActionRow>(
320
+ `SELECT * FROM Actions FINAL WHERE ${conditions.join(' AND ')} ORDER BY createdAt ASC ${limit}`
321
+ )
322
+ }
323
+
324
+ // ===========================================================================
325
+ // Batch Scheduler Interface
326
+ // ===========================================================================
327
+
328
+ /**
329
+ * Add a promise to the batch queue
330
+ */
331
+ enqueue(promise: DurablePromise<unknown>): void {
332
+ this.batchQueue.set(promise.actionId, promise)
333
+ this.startBatchTimer()
334
+
335
+ if (this.batchQueue.size >= this.config.maxBatchSize) {
336
+ this.flush()
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Get pending count
342
+ */
343
+ get pending(): number {
344
+ return this.batchQueue.size + this.pendingCount
345
+ }
346
+
347
+ /**
348
+ * Flush all pending batch operations
349
+ */
350
+ async flush(): Promise<void> {
351
+ if (this.batchTimer) {
352
+ clearTimeout(this.batchTimer)
353
+ this.batchTimer = null
354
+ }
355
+
356
+ const promises = Array.from(this.batchQueue.values())
357
+ this.batchQueue.clear()
358
+
359
+ if (promises.length === 0) return
360
+
361
+ // Group by method prefix (provider)
362
+ const groups = new Map<string, DurablePromise<unknown>[]>()
363
+ for (const promise of promises) {
364
+ const provider = promise.method.split('.')[0] ?? 'default'
365
+ const existing = groups.get(provider) ?? []
366
+ existing.push(promise)
367
+ groups.set(provider, existing)
368
+ }
369
+
370
+ // Update batch metadata in ClickHouse
371
+ for (const [provider, batch] of groups) {
372
+ const batchId = crypto.randomUUID()
373
+
374
+ for (let i = 0; i < batch.length; i++) {
375
+ await this.executor.insert('Actions', [{
376
+ id: batch[i]!.actionId,
377
+ ns: this.namespace,
378
+ batch: batchId,
379
+ batchIndex: i,
380
+ batchTotal: batch.length,
381
+ updatedAt: new Date().toISOString(),
382
+ }])
383
+ }
384
+
385
+ console.log(`Batch ${batchId}: ${batch.length} ${provider} operations queued`)
386
+ }
387
+
388
+ // Execute non-batch immediately with concurrency control
389
+ await Promise.all(
390
+ promises.map(async (promise) => {
391
+ await this.semaphores.batch.run(async () => {
392
+ try {
393
+ await promise
394
+ } catch {
395
+ // Error is handled by the promise itself
396
+ }
397
+ })
398
+ })
399
+ )
400
+ }
401
+
402
+ private startBatchTimer(): void {
403
+ if (this.batchTimer) return
404
+
405
+ this.batchTimer = setTimeout(async () => {
406
+ this.batchTimer = null
407
+ await this.flush()
408
+ }, this.config.batchWindow)
409
+ }
410
+
411
+ // ===========================================================================
412
+ // Recovery
413
+ // ===========================================================================
414
+
415
+ /**
416
+ * Recover pending/active actions from ClickHouse after crash
417
+ */
418
+ async recover(): Promise<number> {
419
+ const actions = await this.listActions({
420
+ status: ['pending', 'active'],
421
+ })
422
+
423
+ console.log(`Recovering ${actions.length} actions from ClickHouse`)
424
+
425
+ let recovered = 0
426
+ for (const action of actions) {
427
+ // Mark active as failed (we don't know if it completed)
428
+ if (action.status === 'active') {
429
+ await this.updateAction(action.id, {
430
+ status: 'failed',
431
+ error: 'Recovered after crash - execution interrupted',
432
+ completedAt: new Date().toISOString(),
433
+ })
434
+ recovered++
435
+ }
436
+ // Pending actions can be retried
437
+ else if (action.status === 'pending') {
438
+ this.pendingCount++
439
+ }
440
+ }
441
+
442
+ return recovered
443
+ }
444
+
445
+ /**
446
+ * Retry failed actions
447
+ */
448
+ async retryFailed(filter?: { method?: string; since?: Date }): Promise<number> {
449
+ const conditions: string[] = [
450
+ `ns = '${this.namespace}'`,
451
+ `status = 'failed'`,
452
+ ]
453
+
454
+ if (filter?.method) {
455
+ conditions.push(`object = '${this.escapeString(filter.method)}'`)
456
+ }
457
+
458
+ if (filter?.since) {
459
+ conditions.push(`completedAt > '${filter.since.toISOString()}'`)
460
+ }
461
+
462
+ const failed = await this.executor.query<ActionRow>(
463
+ `SELECT * FROM Actions FINAL WHERE ${conditions.join(' AND ')}`
464
+ )
465
+
466
+ for (const action of failed) {
467
+ await this.updateAction(action.id, {
468
+ status: 'pending',
469
+ error: '',
470
+ completedAt: null,
471
+ })
472
+ }
473
+
474
+ return failed.length
475
+ }
476
+
477
+ // ===========================================================================
478
+ // Context
479
+ // ===========================================================================
480
+
481
+ /**
482
+ * Create a DurablePromise with ClickHouse persistence
483
+ */
484
+ createPromise<T>(options: Omit<DurablePromiseOptions<T>, 'provider'>): DurablePromise<T> {
485
+ // The provider will be used via context
486
+ return new DurablePromise({
487
+ ...options,
488
+ // Inject ourselves as the context provider
489
+ })
490
+ }
491
+
492
+ // ===========================================================================
493
+ // Stats
494
+ // ===========================================================================
495
+
496
+ /**
497
+ * Get current statistics
498
+ */
499
+ async getStats(): Promise<{
500
+ pending: number
501
+ active: number
502
+ completed: number
503
+ failed: number
504
+ byPriority: Record<ExecutionPriority, { pending: number; active: number; completed: number }>
505
+ batchQueue: number
506
+ }> {
507
+ // Query ClickHouse for accurate counts
508
+ const statusCounts = await this.executor.query<{ status: string; priority: number; count: string }>(
509
+ `SELECT status, priority, count() as count FROM Actions FINAL WHERE ns = '${this.namespace}' GROUP BY status, priority`
510
+ )
511
+
512
+ const byPriority: Record<ExecutionPriority, { pending: number; active: number; completed: number }> = {
513
+ priority: { pending: 0, active: 0, completed: 0 },
514
+ standard: { pending: 0, active: 0, completed: 0 },
515
+ flex: { pending: 0, active: 0, completed: 0 },
516
+ batch: { pending: 0, active: 0, completed: 0 },
517
+ }
518
+
519
+ let pending = 0
520
+ let active = 0
521
+ let completed = 0
522
+ let failed = 0
523
+
524
+ for (const row of statusCounts) {
525
+ const count = parseInt(row.count, 10)
526
+ const tier = PRIORITY_REVERSE[row.priority] ?? 'standard'
527
+
528
+ if (row.status === 'pending') {
529
+ pending += count
530
+ byPriority[tier].pending += count
531
+ } else if (row.status === 'active') {
532
+ active += count
533
+ byPriority[tier].active += count
534
+ } else if (row.status === 'completed') {
535
+ completed += count
536
+ byPriority[tier].completed += count
537
+ } else if (row.status === 'failed') {
538
+ failed += count
539
+ }
540
+ }
541
+
542
+ return {
543
+ pending,
544
+ active,
545
+ completed,
546
+ failed,
547
+ byPriority,
548
+ batchQueue: this.batchQueue.size,
549
+ }
550
+ }
551
+
552
+ // ===========================================================================
553
+ // Helpers
554
+ // ===========================================================================
555
+
556
+ private parseVerb(method: string): string {
557
+ const parts = method.split('.')
558
+ return parts[parts.length - 1] ?? 'process'
559
+ }
560
+
561
+ private escapeString(str: string): string {
562
+ return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
563
+ }
564
+
565
+ // ===========================================================================
566
+ // Cleanup
567
+ // ===========================================================================
568
+
569
+ /**
570
+ * Close the provider
571
+ */
572
+ async close(): Promise<void> {
573
+ if (this.batchTimer) {
574
+ clearTimeout(this.batchTimer)
575
+ this.batchTimer = null
576
+ }
577
+
578
+ // Flush any remaining batch
579
+ await this.flush()
580
+
581
+ setBatchScheduler(null)
582
+ }
583
+ }
584
+
585
+ // =============================================================================
586
+ // Factory
587
+ // =============================================================================
588
+
589
+ /**
590
+ * Create a ClickHouse durable provider
591
+ */
592
+ export function createClickHouseDurableProvider(
593
+ config: ClickHouseDurableConfig
594
+ ): ClickHouseDurableProvider {
595
+ return new ClickHouseDurableProvider(config)
596
+ }