@pikku/kysely 0.12.1 → 0.12.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## 0.12.0
2
2
 
3
+ ## 0.12.2
4
+
5
+ ### Patch Changes
6
+
7
+ - ce961b5: fix: improve MySQL compatibility in AI storage service by using varchar columns with explicit lengths instead of text for primary keys, foreign keys, and indexed columns, and handle duplicate index errors gracefully
8
+ - 3e04565: chore: update dependencies to latest minor/patch versions
9
+ - Updated dependencies [cc4c9e9]
10
+ - Updated dependencies [3e04565]
11
+ - @pikku/core@0.12.2
12
+
3
13
  ## 0.12.1
4
14
 
5
15
  ### Patch Changes
@@ -6,6 +6,7 @@ export declare class KyselyAIStorageService implements AIStorageService, AIRunSt
6
6
  private db;
7
7
  private initialized;
8
8
  constructor(db: Kysely<KyselyPikkuDB>);
9
+ private createIndexSafe;
9
10
  init(): Promise<void>;
10
11
  createThread(resourceId: string, options?: {
11
12
  threadId?: string;
@@ -6,6 +6,17 @@ export class KyselyAIStorageService {
6
6
  constructor(db) {
7
7
  this.db = db;
8
8
  }
9
+ async createIndexSafe(builder) {
10
+ try {
11
+ await builder.execute();
12
+ }
13
+ catch (e) {
14
+ // Ignore "index already exists" errors (MySQL doesn't support IF NOT EXISTS for indexes)
15
+ if (e?.code === 'ER_DUP_KEYNAME' || e?.errno === 1061)
16
+ return;
17
+ throw e;
18
+ }
19
+ }
9
20
  async init() {
10
21
  if (this.initialized) {
11
22
  return;
@@ -13,68 +24,60 @@ export class KyselyAIStorageService {
13
24
  await this.db.schema
14
25
  .createTable('ai_threads')
15
26
  .ifNotExists()
16
- .addColumn('id', 'text', (col) => col.primaryKey())
17
- .addColumn('resource_id', 'text', (col) => col.notNull())
27
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
28
+ .addColumn('resource_id', 'varchar(255)', (col) => col.notNull())
18
29
  .addColumn('title', 'text')
19
30
  .addColumn('metadata', 'text')
20
31
  .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
21
32
  .addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
22
33
  .execute();
23
- await this.db.schema
34
+ await this.createIndexSafe(this.db.schema
24
35
  .createIndex('idx_ai_threads_resource')
25
- .ifNotExists()
26
36
  .on('ai_threads')
27
- .column('resource_id')
28
- .execute();
37
+ .column('resource_id'));
29
38
  await this.db.schema
30
39
  .createTable('ai_message')
31
40
  .ifNotExists()
32
- .addColumn('id', 'text', (col) => col.primaryKey())
33
- .addColumn('thread_id', 'text', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
34
- .addColumn('role', 'text', (col) => col.notNull())
41
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
42
+ .addColumn('thread_id', 'varchar(36)', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
43
+ .addColumn('role', 'varchar(50)', (col) => col.notNull())
35
44
  .addColumn('content', 'text')
36
45
  .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
37
46
  .execute();
38
- await this.db.schema
47
+ await this.createIndexSafe(this.db.schema
39
48
  .createIndex('idx_ai_message_thread')
40
- .ifNotExists()
41
49
  .on('ai_message')
42
- .columns(['thread_id', 'created_at'])
43
- .execute();
50
+ .columns(['thread_id', 'created_at']));
44
51
  await this.db.schema
45
52
  .createTable('ai_tool_call')
46
53
  .ifNotExists()
47
- .addColumn('id', 'text', (col) => col.primaryKey())
48
- .addColumn('thread_id', 'text', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
49
- .addColumn('message_id', 'text', (col) => col.notNull().references('ai_message.id').onDelete('cascade'))
50
- .addColumn('run_id', 'text')
51
- .addColumn('tool_name', 'text', (col) => col.notNull())
52
- .addColumn('args', 'text', (col) => col.notNull().defaultTo('{}'))
54
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
55
+ .addColumn('thread_id', 'varchar(36)', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
56
+ .addColumn('message_id', 'varchar(36)', (col) => col.notNull().references('ai_message.id').onDelete('cascade'))
57
+ .addColumn('run_id', 'varchar(36)')
58
+ .addColumn('tool_name', 'varchar(255)', (col) => col.notNull())
59
+ .addColumn('args', 'text', (col) => col.notNull())
53
60
  .addColumn('result', 'text')
54
- .addColumn('approval_status', 'text')
55
- .addColumn('approval_type', 'text')
56
- .addColumn('agent_run_id', 'text')
57
- .addColumn('display_tool_name', 'text')
61
+ .addColumn('approval_status', 'varchar(50)')
62
+ .addColumn('approval_type', 'varchar(50)')
63
+ .addColumn('agent_run_id', 'varchar(36)')
64
+ .addColumn('display_tool_name', 'varchar(255)')
58
65
  .addColumn('display_args', 'text')
59
66
  .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
60
67
  .execute();
61
- await this.db.schema
68
+ await this.createIndexSafe(this.db.schema
62
69
  .createIndex('idx_ai_tool_call_thread')
63
- .ifNotExists()
64
70
  .on('ai_tool_call')
65
- .column('thread_id')
66
- .execute();
67
- await this.db.schema
71
+ .column('thread_id'));
72
+ await this.createIndexSafe(this.db.schema
68
73
  .createIndex('idx_ai_tool_call_message')
69
- .ifNotExists()
70
74
  .on('ai_tool_call')
71
- .column('message_id')
72
- .execute();
75
+ .column('message_id'));
73
76
  await this.db.schema
74
77
  .createTable('ai_working_memory')
75
78
  .ifNotExists()
76
- .addColumn('id', 'text', (col) => col.notNull())
77
- .addColumn('scope', 'text', (col) => col.notNull())
79
+ .addColumn('id', 'varchar(255)', (col) => col.notNull())
80
+ .addColumn('scope', 'varchar(50)', (col) => col.notNull())
78
81
  .addColumn('data', 'text', (col) => col.notNull())
79
82
  .addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
80
83
  .addPrimaryKeyConstraint('ai_working_memory_pk', ['id', 'scope'])
@@ -82,25 +85,23 @@ export class KyselyAIStorageService {
82
85
  await this.db.schema
83
86
  .createTable('ai_run')
84
87
  .ifNotExists()
85
- .addColumn('run_id', 'text', (col) => col.primaryKey())
86
- .addColumn('agent_name', 'text', (col) => col.notNull())
87
- .addColumn('thread_id', 'text', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
88
- .addColumn('resource_id', 'text', (col) => col.notNull())
89
- .addColumn('status', 'text', (col) => col.notNull().defaultTo('running'))
88
+ .addColumn('run_id', 'varchar(36)', (col) => col.primaryKey())
89
+ .addColumn('agent_name', 'varchar(255)', (col) => col.notNull())
90
+ .addColumn('thread_id', 'varchar(36)', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
91
+ .addColumn('resource_id', 'varchar(255)', (col) => col.notNull())
92
+ .addColumn('status', 'varchar(50)', (col) => col.notNull().defaultTo('running'))
90
93
  .addColumn('suspend_reason', 'text')
91
94
  .addColumn('missing_rpcs', 'text')
92
95
  .addColumn('usage_input_tokens', 'integer', (col) => col.notNull().defaultTo(0))
93
96
  .addColumn('usage_output_tokens', 'integer', (col) => col.notNull().defaultTo(0))
94
- .addColumn('usage_model', 'text', (col) => col.notNull().defaultTo(''))
97
+ .addColumn('usage_model', 'varchar(255)', (col) => col.notNull().defaultTo(''))
95
98
  .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
96
99
  .addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
97
100
  .execute();
98
- await this.db.schema
101
+ await this.createIndexSafe(this.db.schema
99
102
  .createIndex('idx_ai_run_thread')
100
- .ifNotExists()
101
103
  .on('ai_run')
102
- .columns(['thread_id', 'created_at'])
103
- .execute();
104
+ .columns(['thread_id', 'created_at']));
104
105
  this.initialized = true;
105
106
  }
106
107
  async createThread(resourceId, options) {
@@ -219,10 +220,26 @@ export class KyselyAIStorageService {
219
220
  }
220
221
  const messages = [];
221
222
  for (const row of msgResult) {
223
+ const rawContent = row.content;
224
+ let parsedContent = rawContent ?? undefined;
225
+ if (rawContent) {
226
+ try {
227
+ const parsed = JSON.parse(rawContent);
228
+ if (Array.isArray(parsed)) {
229
+ parsedContent = parsed;
230
+ }
231
+ else if (typeof parsed === 'string') {
232
+ parsedContent = parsed;
233
+ }
234
+ }
235
+ catch {
236
+ // Not JSON, use raw string
237
+ }
238
+ }
222
239
  const msg = {
223
240
  id: row.id,
224
241
  role: row.role,
225
- content: row.content ?? undefined,
242
+ content: parsedContent,
226
243
  createdAt: new Date(row.created_at),
227
244
  };
228
245
  const tcs = tcByMessage.get(msg.id);
@@ -264,7 +281,7 @@ export class KyselyAIStorageService {
264
281
  id: msg.id,
265
282
  thread_id: threadId,
266
283
  role: msg.role,
267
- content: msg.content ?? null,
284
+ content: msg.content != null ? JSON.stringify(msg.content) : null,
268
285
  created_at: msg.createdAt ?? new Date(),
269
286
  })))
270
287
  .execute();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/kysely",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/src/index.js",
@@ -19,7 +19,7 @@
19
19
  "release": "npm run build && npm test"
20
20
  },
21
21
  "peerDependencies": {
22
- "@pikku/core": "^0.12.1"
22
+ "@pikku/core": "^0.12.2"
23
23
  },
24
24
  "dependencies": {
25
25
  "kysely": "^0.28.11",
@@ -30,7 +30,7 @@
30
30
  "@types/better-sqlite3": "^7.6.13",
31
31
  "better-sqlite3": "^12.6.2",
32
32
  "kysely": "^0.28.11",
33
- "kysely-codegen": "^0.19.0",
33
+ "kysely-codegen": "^0.20.0",
34
34
  "kysely-plugin-serialize": "^0.8.2",
35
35
  "kysely-postgres-js": "^3.0.0",
36
36
  "postgres": "^3.4.8",
@@ -16,6 +16,18 @@ export class KyselyAIStorageService
16
16
 
17
17
  constructor(private db: Kysely<KyselyPikkuDB>) {}
18
18
 
19
+ private async createIndexSafe(builder: {
20
+ execute(): Promise<void>
21
+ }): Promise<void> {
22
+ try {
23
+ await builder.execute()
24
+ } catch (e: any) {
25
+ // Ignore "index already exists" errors (MySQL doesn't support IF NOT EXISTS for indexes)
26
+ if (e?.code === 'ER_DUP_KEYNAME' || e?.errno === 1061) return
27
+ throw e
28
+ }
29
+ }
30
+
19
31
  public async init(): Promise<void> {
20
32
  if (this.initialized) {
21
33
  return
@@ -24,8 +36,8 @@ export class KyselyAIStorageService
24
36
  await this.db.schema
25
37
  .createTable('ai_threads')
26
38
  .ifNotExists()
27
- .addColumn('id', 'text', (col) => col.primaryKey())
28
- .addColumn('resource_id', 'text', (col) => col.notNull())
39
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
40
+ .addColumn('resource_id', 'varchar(255)', (col) => col.notNull())
29
41
  .addColumn('title', 'text')
30
42
  .addColumn('metadata', 'text')
31
43
  .addColumn('created_at', 'timestamp', (col) =>
@@ -36,77 +48,77 @@ export class KyselyAIStorageService
36
48
  )
37
49
  .execute()
38
50
 
39
- await this.db.schema
40
- .createIndex('idx_ai_threads_resource')
41
- .ifNotExists()
42
- .on('ai_threads')
43
- .column('resource_id')
44
- .execute()
51
+ await this.createIndexSafe(
52
+ this.db.schema
53
+ .createIndex('idx_ai_threads_resource')
54
+ .on('ai_threads')
55
+ .column('resource_id')
56
+ )
45
57
 
46
58
  await this.db.schema
47
59
  .createTable('ai_message')
48
60
  .ifNotExists()
49
- .addColumn('id', 'text', (col) => col.primaryKey())
50
- .addColumn('thread_id', 'text', (col) =>
61
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
62
+ .addColumn('thread_id', 'varchar(36)', (col) =>
51
63
  col.notNull().references('ai_threads.id').onDelete('cascade')
52
64
  )
53
- .addColumn('role', 'text', (col) => col.notNull())
65
+ .addColumn('role', 'varchar(50)', (col) => col.notNull())
54
66
  .addColumn('content', 'text')
55
67
  .addColumn('created_at', 'timestamp', (col) =>
56
68
  col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
57
69
  )
58
70
  .execute()
59
71
 
60
- await this.db.schema
61
- .createIndex('idx_ai_message_thread')
62
- .ifNotExists()
63
- .on('ai_message')
64
- .columns(['thread_id', 'created_at'])
65
- .execute()
72
+ await this.createIndexSafe(
73
+ this.db.schema
74
+ .createIndex('idx_ai_message_thread')
75
+ .on('ai_message')
76
+ .columns(['thread_id', 'created_at'])
77
+ )
66
78
 
67
79
  await this.db.schema
68
80
  .createTable('ai_tool_call')
69
81
  .ifNotExists()
70
- .addColumn('id', 'text', (col) => col.primaryKey())
71
- .addColumn('thread_id', 'text', (col) =>
82
+ .addColumn('id', 'varchar(36)', (col) => col.primaryKey())
83
+ .addColumn('thread_id', 'varchar(36)', (col) =>
72
84
  col.notNull().references('ai_threads.id').onDelete('cascade')
73
85
  )
74
- .addColumn('message_id', 'text', (col) =>
86
+ .addColumn('message_id', 'varchar(36)', (col) =>
75
87
  col.notNull().references('ai_message.id').onDelete('cascade')
76
88
  )
77
- .addColumn('run_id', 'text')
78
- .addColumn('tool_name', 'text', (col) => col.notNull())
79
- .addColumn('args', 'text', (col) => col.notNull().defaultTo('{}'))
89
+ .addColumn('run_id', 'varchar(36)')
90
+ .addColumn('tool_name', 'varchar(255)', (col) => col.notNull())
91
+ .addColumn('args', 'text', (col) => col.notNull())
80
92
  .addColumn('result', 'text')
81
- .addColumn('approval_status', 'text')
82
- .addColumn('approval_type', 'text')
83
- .addColumn('agent_run_id', 'text')
84
- .addColumn('display_tool_name', 'text')
93
+ .addColumn('approval_status', 'varchar(50)')
94
+ .addColumn('approval_type', 'varchar(50)')
95
+ .addColumn('agent_run_id', 'varchar(36)')
96
+ .addColumn('display_tool_name', 'varchar(255)')
85
97
  .addColumn('display_args', 'text')
86
98
  .addColumn('created_at', 'timestamp', (col) =>
87
99
  col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
88
100
  )
89
101
  .execute()
90
102
 
91
- await this.db.schema
92
- .createIndex('idx_ai_tool_call_thread')
93
- .ifNotExists()
94
- .on('ai_tool_call')
95
- .column('thread_id')
96
- .execute()
103
+ await this.createIndexSafe(
104
+ this.db.schema
105
+ .createIndex('idx_ai_tool_call_thread')
106
+ .on('ai_tool_call')
107
+ .column('thread_id')
108
+ )
97
109
 
98
- await this.db.schema
99
- .createIndex('idx_ai_tool_call_message')
100
- .ifNotExists()
101
- .on('ai_tool_call')
102
- .column('message_id')
103
- .execute()
110
+ await this.createIndexSafe(
111
+ this.db.schema
112
+ .createIndex('idx_ai_tool_call_message')
113
+ .on('ai_tool_call')
114
+ .column('message_id')
115
+ )
104
116
 
105
117
  await this.db.schema
106
118
  .createTable('ai_working_memory')
107
119
  .ifNotExists()
108
- .addColumn('id', 'text', (col) => col.notNull())
109
- .addColumn('scope', 'text', (col) => col.notNull())
120
+ .addColumn('id', 'varchar(255)', (col) => col.notNull())
121
+ .addColumn('scope', 'varchar(50)', (col) => col.notNull())
110
122
  .addColumn('data', 'text', (col) => col.notNull())
111
123
  .addColumn('updated_at', 'timestamp', (col) =>
112
124
  col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
@@ -117,13 +129,15 @@ export class KyselyAIStorageService
117
129
  await this.db.schema
118
130
  .createTable('ai_run')
119
131
  .ifNotExists()
120
- .addColumn('run_id', 'text', (col) => col.primaryKey())
121
- .addColumn('agent_name', 'text', (col) => col.notNull())
122
- .addColumn('thread_id', 'text', (col) =>
132
+ .addColumn('run_id', 'varchar(36)', (col) => col.primaryKey())
133
+ .addColumn('agent_name', 'varchar(255)', (col) => col.notNull())
134
+ .addColumn('thread_id', 'varchar(36)', (col) =>
123
135
  col.notNull().references('ai_threads.id').onDelete('cascade')
124
136
  )
125
- .addColumn('resource_id', 'text', (col) => col.notNull())
126
- .addColumn('status', 'text', (col) => col.notNull().defaultTo('running'))
137
+ .addColumn('resource_id', 'varchar(255)', (col) => col.notNull())
138
+ .addColumn('status', 'varchar(50)', (col) =>
139
+ col.notNull().defaultTo('running')
140
+ )
127
141
  .addColumn('suspend_reason', 'text')
128
142
  .addColumn('missing_rpcs', 'text')
129
143
  .addColumn('usage_input_tokens', 'integer', (col) =>
@@ -132,7 +146,9 @@ export class KyselyAIStorageService
132
146
  .addColumn('usage_output_tokens', 'integer', (col) =>
133
147
  col.notNull().defaultTo(0)
134
148
  )
135
- .addColumn('usage_model', 'text', (col) => col.notNull().defaultTo(''))
149
+ .addColumn('usage_model', 'varchar(255)', (col) =>
150
+ col.notNull().defaultTo('')
151
+ )
136
152
  .addColumn('created_at', 'timestamp', (col) =>
137
153
  col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
138
154
  )
@@ -141,12 +157,12 @@ export class KyselyAIStorageService
141
157
  )
142
158
  .execute()
143
159
 
144
- await this.db.schema
145
- .createIndex('idx_ai_run_thread')
146
- .ifNotExists()
147
- .on('ai_run')
148
- .columns(['thread_id', 'created_at'])
149
- .execute()
160
+ await this.createIndexSafe(
161
+ this.db.schema
162
+ .createIndex('idx_ai_run_thread')
163
+ .on('ai_run')
164
+ .columns(['thread_id', 'created_at'])
165
+ )
150
166
 
151
167
  this.initialized = true
152
168
  }
@@ -290,10 +306,24 @@ export class KyselyAIStorageService
290
306
 
291
307
  const messages: AIMessage[] = []
292
308
  for (const row of msgResult) {
309
+ const rawContent = row.content as string | undefined
310
+ let parsedContent: AIMessage['content'] = rawContent ?? undefined
311
+ if (rawContent) {
312
+ try {
313
+ const parsed = JSON.parse(rawContent)
314
+ if (Array.isArray(parsed)) {
315
+ parsedContent = parsed
316
+ } else if (typeof parsed === 'string') {
317
+ parsedContent = parsed
318
+ }
319
+ } catch {
320
+ // Not JSON, use raw string
321
+ }
322
+ }
293
323
  const msg: AIMessage = {
294
324
  id: row.id,
295
325
  role: row.role as AIMessage['role'],
296
- content: row.content ?? undefined,
326
+ content: parsedContent,
297
327
  createdAt: new Date(row.created_at),
298
328
  }
299
329
 
@@ -342,7 +372,7 @@ export class KyselyAIStorageService
342
372
  id: msg.id,
343
373
  thread_id: threadId,
344
374
  role: msg.role,
345
- content: msg.content ?? null,
375
+ content: msg.content != null ? JSON.stringify(msg.content) : null,
346
376
  created_at: msg.createdAt ?? new Date(),
347
377
  }))
348
378
  )