@kernl-sdk/libsql 0.1.38 → 0.1.39
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/.turbo/turbo-build.log +5 -4
- package/CHANGELOG.md +8 -0
- package/README.md +225 -0
- package/dist/__tests__/constraints.test.d.ts +2 -0
- package/dist/__tests__/constraints.test.d.ts.map +1 -0
- package/dist/__tests__/constraints.test.js +97 -0
- package/dist/__tests__/helpers.d.ts +36 -0
- package/dist/__tests__/helpers.d.ts.map +1 -0
- package/dist/__tests__/helpers.js +80 -0
- package/dist/__tests__/memory.create-get.test.d.ts +2 -0
- package/dist/__tests__/memory.create-get.test.d.ts.map +1 -0
- package/dist/__tests__/memory.create-get.test.js +8 -0
- package/dist/__tests__/memory.delete.test.d.ts +2 -0
- package/dist/__tests__/memory.delete.test.d.ts.map +1 -0
- package/dist/__tests__/memory.delete.test.js +6 -0
- package/dist/__tests__/memory.list.test.d.ts +2 -0
- package/dist/__tests__/memory.list.test.d.ts.map +1 -0
- package/dist/__tests__/memory.list.test.js +8 -0
- package/dist/__tests__/memory.update.test.d.ts +2 -0
- package/dist/__tests__/memory.update.test.d.ts.map +1 -0
- package/dist/__tests__/memory.update.test.js +8 -0
- package/dist/__tests__/migrations.test.d.ts +2 -0
- package/dist/__tests__/migrations.test.d.ts.map +1 -0
- package/dist/__tests__/migrations.test.js +68 -0
- package/dist/__tests__/row-codecs.test.d.ts +2 -0
- package/dist/__tests__/row-codecs.test.d.ts.map +1 -0
- package/dist/__tests__/row-codecs.test.js +175 -0
- package/dist/__tests__/sql-utils.test.d.ts +2 -0
- package/dist/__tests__/sql-utils.test.d.ts.map +1 -0
- package/dist/__tests__/sql-utils.test.js +45 -0
- package/dist/__tests__/storage.init.test.d.ts +2 -0
- package/dist/__tests__/storage.init.test.d.ts.map +1 -0
- package/dist/__tests__/storage.init.test.js +63 -0
- package/dist/__tests__/thread.lifecycle.test.d.ts +2 -0
- package/dist/__tests__/thread.lifecycle.test.d.ts.map +1 -0
- package/dist/__tests__/thread.lifecycle.test.js +172 -0
- package/dist/__tests__/transaction.test.d.ts +2 -0
- package/dist/__tests__/transaction.test.d.ts.map +1 -0
- package/dist/__tests__/transaction.test.js +16 -0
- package/dist/__tests__/utils.test.d.ts +2 -0
- package/dist/__tests__/utils.test.d.ts.map +1 -0
- package/dist/__tests__/utils.test.js +31 -0
- package/dist/client.d.ts +46 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +46 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/memory/__tests__/create-get.test.d.ts +2 -0
- package/dist/memory/__tests__/create-get.test.d.ts.map +1 -0
- package/dist/memory/__tests__/create-get.test.js +126 -0
- package/dist/memory/__tests__/delete.test.d.ts +2 -0
- package/dist/memory/__tests__/delete.test.d.ts.map +1 -0
- package/dist/memory/__tests__/delete.test.js +96 -0
- package/dist/memory/__tests__/list.test.d.ts +2 -0
- package/dist/memory/__tests__/list.test.d.ts.map +1 -0
- package/dist/memory/__tests__/list.test.js +168 -0
- package/dist/memory/__tests__/sql.test.d.ts +2 -0
- package/dist/memory/__tests__/sql.test.d.ts.map +1 -0
- package/dist/memory/__tests__/sql.test.js +159 -0
- package/dist/memory/__tests__/update.test.d.ts +2 -0
- package/dist/memory/__tests__/update.test.d.ts.map +1 -0
- package/dist/memory/__tests__/update.test.js +113 -0
- package/dist/memory/row.d.ts +11 -0
- package/dist/memory/row.d.ts.map +1 -0
- package/dist/memory/row.js +29 -0
- package/dist/memory/sql.d.ts +34 -0
- package/dist/memory/sql.d.ts.map +1 -0
- package/dist/memory/sql.js +109 -0
- package/dist/memory/store.d.ts +41 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +132 -0
- package/dist/migrations.d.ts +32 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +157 -0
- package/dist/sql.d.ts +28 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +22 -0
- package/dist/storage.d.ts +75 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +123 -0
- package/dist/thread/__tests__/append.test.d.ts +2 -0
- package/dist/thread/__tests__/append.test.d.ts.map +1 -0
- package/dist/thread/__tests__/append.test.js +141 -0
- package/dist/thread/__tests__/history.test.d.ts +2 -0
- package/dist/thread/__tests__/history.test.d.ts.map +1 -0
- package/dist/thread/__tests__/history.test.js +146 -0
- package/dist/thread/__tests__/sql.test.d.ts +2 -0
- package/dist/thread/__tests__/sql.test.d.ts.map +1 -0
- package/dist/thread/__tests__/sql.test.js +129 -0
- package/dist/thread/__tests__/store.test.d.ts +2 -0
- package/dist/thread/__tests__/store.test.d.ts.map +1 -0
- package/dist/thread/__tests__/store.test.js +170 -0
- package/dist/thread/row.d.ts +19 -0
- package/dist/thread/row.d.ts.map +1 -0
- package/dist/thread/row.js +65 -0
- package/dist/thread/sql.d.ts +33 -0
- package/dist/thread/sql.d.ts.map +1 -0
- package/dist/thread/sql.js +112 -0
- package/dist/thread/store.d.ts +67 -0
- package/dist/thread/store.d.ts.map +1 -0
- package/dist/thread/store.js +282 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +21 -0
- package/package.json +15 -11
- package/src/__tests__/constraints.test.ts +123 -0
- package/src/__tests__/helpers.ts +98 -0
- package/src/__tests__/migrations.test.ts +114 -0
- package/src/__tests__/row-codecs.test.ts +201 -0
- package/src/__tests__/sql-utils.test.ts +52 -0
- package/src/__tests__/storage.init.test.ts +92 -0
- package/src/__tests__/thread.lifecycle.test.ts +234 -0
- package/src/__tests__/transaction.test.ts +25 -0
- package/src/__tests__/utils.test.ts +38 -0
- package/src/client.ts +71 -0
- package/src/index.ts +10 -0
- package/src/memory/__tests__/create-get.test.ts +161 -0
- package/src/memory/__tests__/delete.test.ts +124 -0
- package/src/memory/__tests__/list.test.ts +198 -0
- package/src/memory/__tests__/sql.test.ts +186 -0
- package/src/memory/__tests__/update.test.ts +148 -0
- package/src/memory/row.ts +36 -0
- package/src/memory/sql.ts +142 -0
- package/src/memory/store.ts +173 -0
- package/src/migrations.ts +206 -0
- package/src/sql.ts +35 -0
- package/src/storage.ts +170 -0
- package/src/thread/__tests__/append.test.ts +201 -0
- package/src/thread/__tests__/history.test.ts +198 -0
- package/src/thread/__tests__/sql.test.ts +154 -0
- package/src/thread/__tests__/store.test.ts +219 -0
- package/src/thread/row.ts +77 -0
- package/src/thread/sql.ts +153 -0
- package/src/thread/store.ts +381 -0
- package/src/utils.ts +20 -0
- package/LICENSE +0 -201
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database migrations for LibSQL.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Client, Transaction } from "@libsql/client";
|
|
6
|
+
import type { Table, Column } from "@kernl-sdk/storage";
|
|
7
|
+
import {
|
|
8
|
+
KERNL_SCHEMA_NAME,
|
|
9
|
+
TABLE_THREADS,
|
|
10
|
+
TABLE_THREAD_EVENTS,
|
|
11
|
+
TABLE_MEMORIES,
|
|
12
|
+
} from "@kernl-sdk/storage";
|
|
13
|
+
|
|
14
|
+
import { SQL_IDENTIFIER_REGEX } from "./sql";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Migration context with helpers.
|
|
18
|
+
*/
|
|
19
|
+
export interface MigrationContext {
|
|
20
|
+
client: Client | Transaction;
|
|
21
|
+
createTable: (table: Table<string, Record<string, Column>>) => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Migration {
|
|
25
|
+
id: string;
|
|
26
|
+
up: (ctx: MigrationContext) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* List of all migrations in order.
|
|
31
|
+
*/
|
|
32
|
+
export const MIGRATIONS: Migration[] = [
|
|
33
|
+
{
|
|
34
|
+
id: "001_threads",
|
|
35
|
+
async up(ctx) {
|
|
36
|
+
await ctx.createTable(TABLE_THREADS);
|
|
37
|
+
await ctx.createTable(TABLE_THREAD_EVENTS);
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "002_memories",
|
|
42
|
+
async up(ctx) {
|
|
43
|
+
await ctx.createTable(TABLE_MEMORIES);
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Minimum schema version required by this version of @kernl/libsql.
|
|
50
|
+
*/
|
|
51
|
+
export const REQUIRED_SCHEMA_VERSION = "0001_initial";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Map PostgreSQL types to SQLite types.
|
|
55
|
+
*/
|
|
56
|
+
function mapColumnType(type: string): string {
|
|
57
|
+
switch (type.toLowerCase()) {
|
|
58
|
+
case "jsonb":
|
|
59
|
+
return "TEXT"; // Store JSON as TEXT
|
|
60
|
+
case "bigint":
|
|
61
|
+
return "INTEGER"; // SQLite INTEGER can hold 64-bit
|
|
62
|
+
case "boolean":
|
|
63
|
+
return "INTEGER"; // SQLite uses 0/1 for boolean
|
|
64
|
+
default:
|
|
65
|
+
return type.toUpperCase();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Encode a value for use in a DEFAULT clause.
|
|
71
|
+
*/
|
|
72
|
+
function encodeDefault(col: Column, value: unknown): string {
|
|
73
|
+
if (value === null || value === undefined) return "NULL";
|
|
74
|
+
|
|
75
|
+
const type = col.type.toLowerCase();
|
|
76
|
+
switch (type) {
|
|
77
|
+
case "text":
|
|
78
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
79
|
+
case "integer":
|
|
80
|
+
case "bigint":
|
|
81
|
+
return String(value);
|
|
82
|
+
case "boolean":
|
|
83
|
+
return value ? "1" : "0";
|
|
84
|
+
case "jsonb":
|
|
85
|
+
return `'${JSON.stringify(value)}'`;
|
|
86
|
+
default:
|
|
87
|
+
return String(value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a table from its definition.
|
|
93
|
+
*
|
|
94
|
+
* This function handles the conversion from the generic table definition
|
|
95
|
+
* to SQLite-compatible DDL.
|
|
96
|
+
*/
|
|
97
|
+
export async function createTable(
|
|
98
|
+
client: Client | Transaction,
|
|
99
|
+
table: Table<string, Record<string, Column>>,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
if (!SQL_IDENTIFIER_REGEX.test(table.name)) {
|
|
102
|
+
throw new Error(`Invalid table name: ${table.name}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const columns: string[] = [];
|
|
106
|
+
const tableConstraints: string[] = [];
|
|
107
|
+
const indexes: { name: string; columns: string[]; unique?: boolean }[] = [];
|
|
108
|
+
|
|
109
|
+
// build column definitions
|
|
110
|
+
for (const name in table.columns) {
|
|
111
|
+
const col = table.columns[name];
|
|
112
|
+
const sqlType = mapColumnType(col.type);
|
|
113
|
+
|
|
114
|
+
const constraints: string[] = [];
|
|
115
|
+
if (col._pk) constraints.push("PRIMARY KEY");
|
|
116
|
+
if (col._unique) constraints.push("UNIQUE");
|
|
117
|
+
if (!col._nullable && !col._pk) constraints.push("NOT NULL");
|
|
118
|
+
if (col._default !== undefined) {
|
|
119
|
+
constraints.push(`DEFAULT ${encodeDefault(col, col._default)}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// foreign key reference
|
|
123
|
+
if (col._fk) {
|
|
124
|
+
let ref = `REFERENCES "${KERNL_SCHEMA_NAME}_${col._fk.table}" ("${col._fk.column}")`;
|
|
125
|
+
if (col._onDelete) {
|
|
126
|
+
ref += ` ON DELETE ${col._onDelete}`;
|
|
127
|
+
}
|
|
128
|
+
constraints.push(ref);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
columns.push(
|
|
132
|
+
`"${name}" ${sqlType} ${constraints.join(" ")}`.trim(),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// table-level constraints
|
|
137
|
+
if (table.constraints) {
|
|
138
|
+
for (const constraint of table.constraints) {
|
|
139
|
+
switch (constraint.kind) {
|
|
140
|
+
case "unique": {
|
|
141
|
+
const name =
|
|
142
|
+
constraint.name ??
|
|
143
|
+
`${table.name}_${constraint.columns.join("_")}_unique`;
|
|
144
|
+
const cols = constraint.columns.map((c) => `"${c}"`).join(", ");
|
|
145
|
+
tableConstraints.push(`CONSTRAINT "${name}" UNIQUE (${cols})`);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
case "pkey": {
|
|
150
|
+
const name = constraint.name ?? `${table.name}_pkey`;
|
|
151
|
+
const cols = constraint.columns.map((c) => `"${c}"`).join(", ");
|
|
152
|
+
tableConstraints.push(`CONSTRAINT "${name}" PRIMARY KEY (${cols})`);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case "fkey": {
|
|
157
|
+
throw new Error(
|
|
158
|
+
"Composite foreign keys not yet supported. Use column-level .references() for single-column FKs.",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case "check": {
|
|
163
|
+
const name = constraint.name ?? `${table.name}_check`;
|
|
164
|
+
tableConstraints.push(
|
|
165
|
+
`CONSTRAINT "${name}" CHECK (${constraint.expression})`,
|
|
166
|
+
);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case "index": {
|
|
171
|
+
// collect indexes to create after table
|
|
172
|
+
indexes.push({
|
|
173
|
+
name: `idx_${table.name}_${constraint.columns.join("_")}`,
|
|
174
|
+
columns: constraint.columns,
|
|
175
|
+
unique: constraint.unique,
|
|
176
|
+
});
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const allConstraints = [...columns, ...tableConstraints];
|
|
184
|
+
|
|
185
|
+
// Use schema prefix in table name (SQLite doesn't have schemas)
|
|
186
|
+
const fullTableName = `${KERNL_SCHEMA_NAME}_${table.name}`;
|
|
187
|
+
|
|
188
|
+
const sql = `
|
|
189
|
+
CREATE TABLE IF NOT EXISTS "${fullTableName}" (
|
|
190
|
+
${allConstraints.join(",\n ")}
|
|
191
|
+
)
|
|
192
|
+
`.trim();
|
|
193
|
+
|
|
194
|
+
await client.execute(sql);
|
|
195
|
+
|
|
196
|
+
// create indexes
|
|
197
|
+
for (const index of indexes) {
|
|
198
|
+
const uniqueKeyword = index.unique ? "UNIQUE " : "";
|
|
199
|
+
const cols = index.columns.map((c) => `"${c}"`).join(", ");
|
|
200
|
+
const indexSql = `
|
|
201
|
+
CREATE ${uniqueKeyword}INDEX IF NOT EXISTS "${index.name}"
|
|
202
|
+
ON "${fullTableName}" (${cols})
|
|
203
|
+
`.trim();
|
|
204
|
+
await client.execute(indexSql);
|
|
205
|
+
}
|
|
206
|
+
}
|
package/src/sql.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL utilities for safe query construction.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SQL identifier regex - alphanumeric + underscore, must start with letter/underscore.
|
|
7
|
+
*/
|
|
8
|
+
export const SQL_IDENTIFIER_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* SQL clause with placeholder parameters.
|
|
12
|
+
*/
|
|
13
|
+
export interface SQLClause {
|
|
14
|
+
sql: string;
|
|
15
|
+
params: unknown[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Expand an array into individual ? placeholders for IN clause.
|
|
20
|
+
*
|
|
21
|
+
* LibSQL/SQLite doesn't support array parameters like PostgreSQL's ANY($1).
|
|
22
|
+
* This helper expands arrays into individual placeholders.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* expandArray(['a', 'b', 'c']) => { placeholders: '?, ?, ?', params: ['a', 'b', 'c'] }
|
|
26
|
+
*/
|
|
27
|
+
export function expandarray(arr: unknown[]): {
|
|
28
|
+
placeholders: string;
|
|
29
|
+
params: unknown[];
|
|
30
|
+
} {
|
|
31
|
+
return {
|
|
32
|
+
placeholders: arr.map(() => "?").join(", "),
|
|
33
|
+
params: arr,
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LibSQL storage adapter.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Client } from "@libsql/client";
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
IAgentRegistry,
|
|
9
|
+
IModelRegistry,
|
|
10
|
+
KernlStorage,
|
|
11
|
+
Transaction,
|
|
12
|
+
} from "kernl";
|
|
13
|
+
import type { Table, Column } from "@kernl-sdk/storage";
|
|
14
|
+
import { KERNL_SCHEMA_NAME, TABLE_MIGRATIONS } from "@kernl-sdk/storage";
|
|
15
|
+
import { UnimplementedError } from "@kernl-sdk/shared/lib";
|
|
16
|
+
|
|
17
|
+
import { LibSQLThreadStore } from "./thread/store";
|
|
18
|
+
import { LibSQLMemoryStore } from "./memory/store";
|
|
19
|
+
import { MIGRATIONS, createTable } from "./migrations";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* LibSQL storage configuration.
|
|
23
|
+
*/
|
|
24
|
+
export interface LibSQLStorageConfig {
|
|
25
|
+
/**
|
|
26
|
+
* LibSQL client instance.
|
|
27
|
+
*/
|
|
28
|
+
client: Client;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Original connection URL (optional).
|
|
32
|
+
*
|
|
33
|
+
* Used to detect local file databases for setting SQLite PRAGMAs.
|
|
34
|
+
* Not needed if using a pre-configured client.
|
|
35
|
+
*/
|
|
36
|
+
url?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* LibSQL storage adapter.
|
|
41
|
+
*
|
|
42
|
+
* Storage is lazily initialized on first use via `ensureInit()`. This means
|
|
43
|
+
* callers don't need to explicitly call `init()` - it happens automatically.
|
|
44
|
+
*/
|
|
45
|
+
export class LibSQLStorage implements KernlStorage {
|
|
46
|
+
private client: Client;
|
|
47
|
+
private url?: string;
|
|
48
|
+
private initPromise: Promise<void> | null = null;
|
|
49
|
+
|
|
50
|
+
threads: LibSQLThreadStore;
|
|
51
|
+
memories: LibSQLMemoryStore;
|
|
52
|
+
|
|
53
|
+
constructor(config: LibSQLStorageConfig) {
|
|
54
|
+
this.client = config.client;
|
|
55
|
+
this.url = config.url;
|
|
56
|
+
this.threads = new LibSQLThreadStore(this.client, () => this.ensureInit());
|
|
57
|
+
this.memories = new LibSQLMemoryStore(this.client, () => this.ensureInit());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Ensure storage is initialized before any operation.
|
|
62
|
+
*
|
|
63
|
+
* Safe to call multiple times - initialization only runs once.
|
|
64
|
+
*/
|
|
65
|
+
private async ensureInit(): Promise<void> {
|
|
66
|
+
if (!this.initPromise) {
|
|
67
|
+
this.initPromise = this.init().catch((err) => {
|
|
68
|
+
this.initPromise = null;
|
|
69
|
+
throw err;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return this.initPromise;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Bind runtime registries to storage.
|
|
77
|
+
*/
|
|
78
|
+
bind(registries: { agents: IAgentRegistry; models: IModelRegistry }): void {
|
|
79
|
+
this.threads.bind(registries);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Execute a function within a transaction.
|
|
84
|
+
*/
|
|
85
|
+
async transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T> {
|
|
86
|
+
throw new UnimplementedError();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Initialize the storage backend.
|
|
91
|
+
*/
|
|
92
|
+
async init(): Promise<void> {
|
|
93
|
+
// Set SQLite PRAGMAs for local file databases
|
|
94
|
+
// (Turso handles these automatically for remote connections)
|
|
95
|
+
if (this.isLocal()) {
|
|
96
|
+
await this.client.execute("PRAGMA journal_mode = WAL");
|
|
97
|
+
await this.client.execute("PRAGMA busy_timeout = 5000");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Create migrations table first
|
|
101
|
+
await this.createTable(TABLE_MIGRATIONS);
|
|
102
|
+
await this.migrate();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if this is a local file or in-memory database.
|
|
107
|
+
*/
|
|
108
|
+
private isLocal(): boolean {
|
|
109
|
+
if (!this.url) return false;
|
|
110
|
+
return this.url.startsWith("file:") || this.url === ":memory:";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Close the storage backend and cleanup resources.
|
|
115
|
+
*/
|
|
116
|
+
async close(): Promise<void> {
|
|
117
|
+
this.client.close();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Run migrations to ensure all required tables exist.
|
|
122
|
+
*/
|
|
123
|
+
async migrate(): Promise<void> {
|
|
124
|
+
const tx = await this.client.transaction("write");
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// read applied migration IDs
|
|
128
|
+
const migrationsTable = `${KERNL_SCHEMA_NAME}_migrations`;
|
|
129
|
+
const result = await tx.execute(
|
|
130
|
+
`SELECT id FROM "${migrationsTable}" ORDER BY applied_at ASC`,
|
|
131
|
+
);
|
|
132
|
+
const applied = new Set(result.rows.map((row) => row.id as string));
|
|
133
|
+
|
|
134
|
+
// filter pending migrations
|
|
135
|
+
const pending = MIGRATIONS.filter((m) => !applied.has(m.id));
|
|
136
|
+
if (pending.length === 0) {
|
|
137
|
+
await tx.commit();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// run pending migrations + insert into migrations table
|
|
142
|
+
for (const migration of pending) {
|
|
143
|
+
await migration.up({
|
|
144
|
+
client: tx,
|
|
145
|
+
createTable: async (table: Table<string, Record<string, Column>>) => {
|
|
146
|
+
await createTable(tx, table);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
await tx.execute({
|
|
150
|
+
sql: `INSERT INTO "${migrationsTable}" (id, applied_at) VALUES (?, ?)`,
|
|
151
|
+
args: [migration.id, Date.now()],
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await tx.commit();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
await tx.rollback();
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Create a table from its definition.
|
|
164
|
+
*/
|
|
165
|
+
private async createTable(
|
|
166
|
+
table: Table<string, Record<string, Column>>,
|
|
167
|
+
): Promise<void> {
|
|
168
|
+
await createTable(this.client, table);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import type { Client } from "@libsql/client";
|
|
3
|
+
import { message, IN_PROGRESS, COMPLETED } from "@kernl-sdk/protocol";
|
|
4
|
+
import type { ThreadEvent } from "kernl/internal";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
create_client,
|
|
8
|
+
create_storage,
|
|
9
|
+
create_mock_registries,
|
|
10
|
+
testid,
|
|
11
|
+
} from "../../__tests__/helpers";
|
|
12
|
+
import { LibSQLStorage } from "../../storage";
|
|
13
|
+
|
|
14
|
+
/** Create a ThreadEvent from a message */
|
|
15
|
+
function evt(
|
|
16
|
+
id: string,
|
|
17
|
+
tid: string,
|
|
18
|
+
seq: number,
|
|
19
|
+
timestamp: Date,
|
|
20
|
+
role: "user" | "assistant" = "user",
|
|
21
|
+
text: string = `msg-${seq}`,
|
|
22
|
+
metadata: Record<string, unknown> = {},
|
|
23
|
+
): ThreadEvent {
|
|
24
|
+
return {
|
|
25
|
+
...message({ role, text }),
|
|
26
|
+
id,
|
|
27
|
+
tid,
|
|
28
|
+
seq,
|
|
29
|
+
timestamp,
|
|
30
|
+
metadata,
|
|
31
|
+
} as ThreadEvent;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Create a tool.call ThreadEvent */
|
|
35
|
+
function toolCallEvt(
|
|
36
|
+
id: string,
|
|
37
|
+
tid: string,
|
|
38
|
+
seq: number,
|
|
39
|
+
timestamp: Date,
|
|
40
|
+
): ThreadEvent {
|
|
41
|
+
return {
|
|
42
|
+
kind: "tool.call",
|
|
43
|
+
id,
|
|
44
|
+
tid,
|
|
45
|
+
seq,
|
|
46
|
+
timestamp,
|
|
47
|
+
callId: `call-${seq}`,
|
|
48
|
+
toolId: "test-tool",
|
|
49
|
+
state: IN_PROGRESS,
|
|
50
|
+
arguments: "{}",
|
|
51
|
+
metadata: {},
|
|
52
|
+
} as ThreadEvent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Create a tool.result ThreadEvent */
|
|
56
|
+
function toolResultEvt(
|
|
57
|
+
id: string,
|
|
58
|
+
tid: string,
|
|
59
|
+
seq: number,
|
|
60
|
+
timestamp: Date,
|
|
61
|
+
): ThreadEvent {
|
|
62
|
+
return {
|
|
63
|
+
kind: "tool.result",
|
|
64
|
+
id,
|
|
65
|
+
tid,
|
|
66
|
+
seq,
|
|
67
|
+
timestamp,
|
|
68
|
+
callId: `call-${seq - 1}`,
|
|
69
|
+
toolId: "test-tool",
|
|
70
|
+
state: COMPLETED,
|
|
71
|
+
result: null,
|
|
72
|
+
error: null,
|
|
73
|
+
metadata: {},
|
|
74
|
+
} as ThreadEvent;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe("LibSQLThreadStore append", () => {
|
|
78
|
+
let client: Client;
|
|
79
|
+
let storage: LibSQLStorage;
|
|
80
|
+
let tid: string;
|
|
81
|
+
|
|
82
|
+
beforeEach(async () => {
|
|
83
|
+
client = create_client();
|
|
84
|
+
storage = create_storage(client);
|
|
85
|
+
storage.bind(create_mock_registries());
|
|
86
|
+
await storage.memories.list(); // init
|
|
87
|
+
|
|
88
|
+
tid = testid("thread");
|
|
89
|
+
await storage.threads.insert({
|
|
90
|
+
id: tid,
|
|
91
|
+
namespace: "default",
|
|
92
|
+
agentId: "test-agent",
|
|
93
|
+
model: "test/model",
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
client.close();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("appends events in order", async () => {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
|
|
104
|
+
await storage.threads.append([
|
|
105
|
+
evt("e1", tid, 1, new Date(now), "user"),
|
|
106
|
+
evt("e2", tid, 2, new Date(now + 1), "assistant"),
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const history = await storage.threads.history(tid);
|
|
110
|
+
|
|
111
|
+
expect(history.length).toBe(2);
|
|
112
|
+
expect(history[0].id).toBe("e1");
|
|
113
|
+
expect(history[0].seq).toBe(1);
|
|
114
|
+
expect(history[1].id).toBe("e2");
|
|
115
|
+
expect(history[1].seq).toBe(2);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("preserves event data and metadata", async () => {
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
const metadata = { source: "api", version: 2 };
|
|
121
|
+
|
|
122
|
+
await storage.threads.append([
|
|
123
|
+
evt("e1", tid, 1, new Date(now), "user", "Hello, world!", metadata),
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const history = await storage.threads.history(tid);
|
|
127
|
+
const event = history[0];
|
|
128
|
+
|
|
129
|
+
expect(event.kind).toBe("message");
|
|
130
|
+
if (event.kind === "message") {
|
|
131
|
+
expect(event.role).toBe("user");
|
|
132
|
+
expect(event.content).toEqual([{ kind: "text", text: "Hello, world!" }]);
|
|
133
|
+
}
|
|
134
|
+
expect(event.metadata).toEqual(metadata);
|
|
135
|
+
expect(event.timestamp.getTime()).toBe(now);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("is idempotent on duplicate ids", async () => {
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
|
|
141
|
+
// First append
|
|
142
|
+
await storage.threads.append([evt("e1", tid, 1, new Date(now))]);
|
|
143
|
+
|
|
144
|
+
// Duplicate append (same id)
|
|
145
|
+
await storage.threads.append([evt("e1", tid, 1, new Date(now + 100))]);
|
|
146
|
+
|
|
147
|
+
const history = await storage.threads.history(tid);
|
|
148
|
+
|
|
149
|
+
// Should only have one event
|
|
150
|
+
expect(history.length).toBe(1);
|
|
151
|
+
// Original timestamp should be preserved
|
|
152
|
+
expect(history[0].timestamp.getTime()).toBe(now);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("handles empty event list", async () => {
|
|
156
|
+
await storage.threads.append([]);
|
|
157
|
+
|
|
158
|
+
const history = await storage.threads.history(tid);
|
|
159
|
+
expect(history.length).toBe(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("handles multiple appends", async () => {
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
|
|
165
|
+
await storage.threads.append([evt("e1", tid, 1, new Date(now))]);
|
|
166
|
+
|
|
167
|
+
await storage.threads.append([
|
|
168
|
+
toolCallEvt("e2", tid, 2, new Date(now + 1)),
|
|
169
|
+
toolResultEvt("e3", tid, 3, new Date(now + 2)),
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
await storage.threads.append([evt("e4", tid, 4, new Date(now + 3))]);
|
|
173
|
+
|
|
174
|
+
const history = await storage.threads.history(tid);
|
|
175
|
+
|
|
176
|
+
expect(history.length).toBe(4);
|
|
177
|
+
expect(history.map((e) => e.id)).toEqual(["e1", "e2", "e3", "e4"]);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("handles mixed duplicate and new events", async () => {
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
|
|
183
|
+
await storage.threads.append([
|
|
184
|
+
evt("e1", tid, 1, new Date(now)),
|
|
185
|
+
evt("e2", tid, 2, new Date(now + 1)),
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
// Append with one duplicate and one new
|
|
189
|
+
await storage.threads.append([
|
|
190
|
+
evt("e2", tid, 2, new Date(now + 100)), // duplicate
|
|
191
|
+
evt("e3", tid, 3, new Date(now + 2)), // new
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const history = await storage.threads.history(tid);
|
|
195
|
+
|
|
196
|
+
expect(history.length).toBe(3);
|
|
197
|
+
expect(history.map((e) => e.id)).toEqual(["e1", "e2", "e3"]);
|
|
198
|
+
// e2 should have original timestamp
|
|
199
|
+
expect(history[1].timestamp.getTime()).toBe(now + 1);
|
|
200
|
+
});
|
|
201
|
+
});
|