@jungjaehoon/mama-core 1.0.1
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.md +171 -0
- package/package.json +68 -0
- package/src/config-loader.js +218 -0
- package/src/db-adapter/README.md +110 -0
- package/src/db-adapter/base-adapter.js +91 -0
- package/src/db-adapter/index.js +31 -0
- package/src/db-adapter/sqlite-adapter.js +364 -0
- package/src/db-adapter/statement.js +127 -0
- package/src/db-manager.js +671 -0
- package/src/debug-logger.js +86 -0
- package/src/decision-formatter.js +1276 -0
- package/src/decision-tracker.js +621 -0
- package/src/embedding-cache.js +222 -0
- package/src/embedding-client.js +141 -0
- package/src/embedding-server/index.js +424 -0
- package/src/embedding-server/mobile/auth.js +160 -0
- package/src/embedding-server/mobile/daemon.js +313 -0
- package/src/embedding-server/mobile/output-parser.js +281 -0
- package/src/embedding-server/mobile/session-api.js +279 -0
- package/src/embedding-server/mobile/session-manager.js +377 -0
- package/src/embedding-server/mobile/websocket-handler.js +389 -0
- package/src/embeddings.js +305 -0
- package/src/errors.js +326 -0
- package/src/index.js +41 -0
- package/src/mama-api.js +2614 -0
- package/src/memory-inject.js +174 -0
- package/src/memory-store.js +89 -0
- package/src/notification-manager.js +3 -0
- package/src/ollama-client.js +391 -0
- package/src/outcome-tracker.js +351 -0
- package/src/progress-indicator.js +110 -0
- package/src/query-intent.js +237 -0
- package/src/relevance-scorer.js +286 -0
- package/src/tier-validator.js +269 -0
- package/src/time-formatter.js +98 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite Database Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements DatabaseAdapter interface using better-sqlite3 + sqlite-vec
|
|
5
|
+
* This is the current production implementation extracted from memory-store.js
|
|
6
|
+
*
|
|
7
|
+
* @module sqlite-adapter
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { DatabaseAdapter } = require('./base-adapter');
|
|
11
|
+
const { SQLiteStatement } = require('./statement');
|
|
12
|
+
const { info, warn, error: logError } = require('../debug-logger');
|
|
13
|
+
const { cosineSimilarity } = require('../embeddings');
|
|
14
|
+
const Database = require('better-sqlite3');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
let sqliteVec = null;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
sqliteVec = require('sqlite-vec');
|
|
22
|
+
} catch (err) {
|
|
23
|
+
// Defer logging until connect() so we have logger context initialized
|
|
24
|
+
sqliteVec = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Database paths
|
|
28
|
+
const LEGACY_DB_PATH = path.join(os.homedir(), '.spinelift', 'memories.db');
|
|
29
|
+
// Default to ~/.claude/mama-memory.db for Claude Code/Desktop compatibility
|
|
30
|
+
const DEFAULT_DB_PATH = path.join(os.homedir(), '.claude', 'mama-memory.db');
|
|
31
|
+
|
|
32
|
+
class SQLiteAdapter extends DatabaseAdapter {
|
|
33
|
+
constructor(config = {}) {
|
|
34
|
+
super();
|
|
35
|
+
this.config = config;
|
|
36
|
+
this.db = null;
|
|
37
|
+
this.lastRowid = null;
|
|
38
|
+
this.vectorSearchEnabled = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get database path with backward compatibility
|
|
43
|
+
*/
|
|
44
|
+
getDbPath() {
|
|
45
|
+
// Support both MAMA_DB_PATH and MAMA_DATABASE_PATH for backward compatibility
|
|
46
|
+
const envPath = process.env.MAMA_DB_PATH || process.env.MAMA_DATABASE_PATH;
|
|
47
|
+
const configPath = this.config.dbPath;
|
|
48
|
+
|
|
49
|
+
// Expand ${HOME} or ~ in environment variable
|
|
50
|
+
let expandedEnvPath = envPath;
|
|
51
|
+
if (envPath) {
|
|
52
|
+
expandedEnvPath = envPath.replace(/\$\{HOME\}/g, os.homedir()).replace(/^~/, os.homedir());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Priority: config > env > default
|
|
56
|
+
const targetPath = configPath || expandedEnvPath || DEFAULT_DB_PATH;
|
|
57
|
+
|
|
58
|
+
// Backward compatibility: Check legacy path if not explicitly set
|
|
59
|
+
if (!configPath && !envPath && fs.existsSync(LEGACY_DB_PATH)) {
|
|
60
|
+
info(
|
|
61
|
+
'[sqlite-adapter] Found legacy database at ~/.spinelift/memories.db, using it for backward compatibility'
|
|
62
|
+
);
|
|
63
|
+
return LEGACY_DB_PATH;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return targetPath;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Connect to SQLite database
|
|
71
|
+
*/
|
|
72
|
+
connect() {
|
|
73
|
+
if (this.db) {
|
|
74
|
+
return this.db;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const dbPath = this.getDbPath();
|
|
78
|
+
const dbDir = path.dirname(dbPath);
|
|
79
|
+
|
|
80
|
+
// Ensure directory exists
|
|
81
|
+
if (!fs.existsSync(dbDir)) {
|
|
82
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
83
|
+
info(`[sqlite-adapter] Created database directory: ${dbDir}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Open database
|
|
87
|
+
this.db = new Database(dbPath, { verbose: null });
|
|
88
|
+
info(`[sqlite-adapter] Opened database at: ${dbPath}`);
|
|
89
|
+
|
|
90
|
+
// Production configuration
|
|
91
|
+
this.db.pragma('journal_mode = WAL');
|
|
92
|
+
this.db.pragma('busy_timeout = 5000');
|
|
93
|
+
this.db.pragma('synchronous = NORMAL');
|
|
94
|
+
this.db.pragma('cache_size = -64000'); // 64MB cache
|
|
95
|
+
this.db.pragma('temp_store = MEMORY');
|
|
96
|
+
this.db.pragma('foreign_keys = ON');
|
|
97
|
+
|
|
98
|
+
// Load sqlite-vec extension (graceful degradation if unavailable)
|
|
99
|
+
if (sqliteVec) {
|
|
100
|
+
try {
|
|
101
|
+
sqliteVec.load(this.db);
|
|
102
|
+
this.vectorSearchEnabled = true;
|
|
103
|
+
info('[sqlite-adapter] Loaded sqlite-vec extension');
|
|
104
|
+
} catch (err) {
|
|
105
|
+
this.vectorSearchEnabled = false;
|
|
106
|
+
warn(`[sqlite-adapter] sqlite-vec unavailable (Tier 2 fallback): ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
this.vectorSearchEnabled = false;
|
|
110
|
+
warn('[sqlite-adapter] sqlite-vec package not installed; vector search disabled');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return this.db;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Disconnect from database
|
|
118
|
+
*/
|
|
119
|
+
disconnect() {
|
|
120
|
+
if (this.db) {
|
|
121
|
+
this.db.close();
|
|
122
|
+
this.db = null;
|
|
123
|
+
info('[sqlite-adapter] Disconnected from database');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if connected
|
|
129
|
+
*/
|
|
130
|
+
isConnected() {
|
|
131
|
+
return this.db !== null && this.db.open;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Prepare a SQL statement
|
|
136
|
+
*/
|
|
137
|
+
prepare(sql) {
|
|
138
|
+
if (!this.isConnected()) {
|
|
139
|
+
throw new Error('Database not connected');
|
|
140
|
+
}
|
|
141
|
+
const stmt = this.db.prepare(sql);
|
|
142
|
+
return new SQLiteStatement(stmt);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Execute raw SQL
|
|
147
|
+
*/
|
|
148
|
+
exec(sql) {
|
|
149
|
+
if (!this.isConnected()) {
|
|
150
|
+
throw new Error('Database not connected');
|
|
151
|
+
}
|
|
152
|
+
return this.db.exec(sql);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Execute function in transaction
|
|
157
|
+
*/
|
|
158
|
+
transaction(fn) {
|
|
159
|
+
if (!this.isConnected()) {
|
|
160
|
+
throw new Error('Database not connected');
|
|
161
|
+
}
|
|
162
|
+
const txn = this.db.transaction(fn);
|
|
163
|
+
return txn();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Vector similarity search using sqlite-vec (vec0 virtual table)
|
|
168
|
+
*/
|
|
169
|
+
vectorSearch(embedding, limit = 5) {
|
|
170
|
+
if (!this.isConnected()) {
|
|
171
|
+
throw new Error('Database not connected');
|
|
172
|
+
}
|
|
173
|
+
if (!this.vectorSearchEnabled) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const embeddingJson = JSON.stringify(Array.from(embedding));
|
|
178
|
+
const stmt = this.prepare(`
|
|
179
|
+
SELECT
|
|
180
|
+
rowid,
|
|
181
|
+
embedding,
|
|
182
|
+
distance
|
|
183
|
+
FROM vss_memories
|
|
184
|
+
WHERE embedding MATCH vec_f32(?)
|
|
185
|
+
LIMIT ?
|
|
186
|
+
`);
|
|
187
|
+
|
|
188
|
+
const queryVector =
|
|
189
|
+
embedding instanceof Float32Array ? embedding : Float32Array.from(embedding);
|
|
190
|
+
const results = stmt.all(embeddingJson, Math.max(limit, 1));
|
|
191
|
+
|
|
192
|
+
return results
|
|
193
|
+
.map((row) => {
|
|
194
|
+
const candidate = bufferToVector(row.embedding);
|
|
195
|
+
if (!candidate) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const similarity = cosineSimilarity(candidate, queryVector);
|
|
199
|
+
return {
|
|
200
|
+
rowid: row.rowid,
|
|
201
|
+
similarity,
|
|
202
|
+
distance: 1 - similarity,
|
|
203
|
+
};
|
|
204
|
+
})
|
|
205
|
+
.filter(Boolean);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Insert vector embedding
|
|
210
|
+
*/
|
|
211
|
+
insertEmbedding(rowid, embedding) {
|
|
212
|
+
if (!this.isConnected()) {
|
|
213
|
+
throw new Error('Database not connected');
|
|
214
|
+
}
|
|
215
|
+
if (!this.vectorSearchEnabled) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const embeddingJson = JSON.stringify(Array.from(embedding));
|
|
220
|
+
|
|
221
|
+
// CRITICAL FIX: sqlite-vec virtual tables accept rowid as literal but not via ? placeholder
|
|
222
|
+
// Using template literal with Number() cast for safety (prevents SQL injection)
|
|
223
|
+
const safeRowid = Number(rowid);
|
|
224
|
+
if (!Number.isInteger(safeRowid) || safeRowid < 1) {
|
|
225
|
+
throw new Error(`Invalid rowid: ${rowid}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const stmt = this.prepare(`
|
|
229
|
+
INSERT OR REPLACE INTO vss_memories(rowid, embedding)
|
|
230
|
+
VALUES (${safeRowid}, ?)
|
|
231
|
+
`);
|
|
232
|
+
|
|
233
|
+
return stmt.run(embeddingJson);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get last inserted row ID
|
|
238
|
+
*/
|
|
239
|
+
getLastInsertRowid() {
|
|
240
|
+
if (!this.isConnected()) {
|
|
241
|
+
throw new Error('Database not connected');
|
|
242
|
+
}
|
|
243
|
+
// better-sqlite3 provides this via Database instance
|
|
244
|
+
return this.db.prepare('SELECT last_insert_rowid() as rowid').get().rowid;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Run migrations
|
|
249
|
+
*/
|
|
250
|
+
runMigrations(migrationsDir) {
|
|
251
|
+
if (!this.isConnected()) {
|
|
252
|
+
throw new Error('Database not connected');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check if schema_version table exists
|
|
256
|
+
const tables = this.prepare(
|
|
257
|
+
`
|
|
258
|
+
SELECT name FROM sqlite_master
|
|
259
|
+
WHERE type='table' AND name='schema_version'
|
|
260
|
+
`
|
|
261
|
+
).all();
|
|
262
|
+
|
|
263
|
+
let currentVersion = 0;
|
|
264
|
+
if (tables.length > 0) {
|
|
265
|
+
const version = this.prepare('SELECT MAX(version) as version FROM schema_version').get();
|
|
266
|
+
currentVersion = version?.version || 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
info(`[sqlite-adapter] Current schema version: ${currentVersion}`);
|
|
270
|
+
|
|
271
|
+
// Get all migration files
|
|
272
|
+
const migrationFiles = fs
|
|
273
|
+
.readdirSync(migrationsDir)
|
|
274
|
+
.filter((file) => file.endsWith('.sql'))
|
|
275
|
+
.sort();
|
|
276
|
+
|
|
277
|
+
// Apply migrations
|
|
278
|
+
for (const file of migrationFiles) {
|
|
279
|
+
const versionMatch = file.match(/^(\d+)-/);
|
|
280
|
+
if (!versionMatch) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const version = parseInt(versionMatch[1], 10);
|
|
285
|
+
if (version <= currentVersion) {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const migrationPath = path.join(migrationsDir, file);
|
|
290
|
+
const migrationSQL = fs.readFileSync(migrationPath, 'utf8');
|
|
291
|
+
|
|
292
|
+
info(`[sqlite-adapter] Applying migration: ${file}`);
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
this.exec('BEGIN TRANSACTION');
|
|
296
|
+
this.exec(migrationSQL);
|
|
297
|
+
this.exec('COMMIT');
|
|
298
|
+
|
|
299
|
+
// Record migration in schema_version table (outside transaction for idempotency)
|
|
300
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(version);
|
|
301
|
+
info(`[sqlite-adapter] Migration ${file} applied successfully`);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
this.exec('ROLLBACK');
|
|
304
|
+
|
|
305
|
+
// Handle duplicate column errors as idempotent (migration 003)
|
|
306
|
+
if (err.message && err.message.includes('duplicate column')) {
|
|
307
|
+
warn(`[sqlite-adapter] Migration ${file} skipped (duplicate column - already applied)`);
|
|
308
|
+
// Record migration as applied
|
|
309
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(version);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (err.message && err.message.includes('no such table')) {
|
|
314
|
+
// Only skip if this migration contains an ALTER TABLE (adding column to optional table)
|
|
315
|
+
// Note: migrationSQL may contain comments, so check if ALTER TABLE exists anywhere
|
|
316
|
+
const hasAlterTable = migrationSQL.toUpperCase().includes('ALTER TABLE');
|
|
317
|
+
if (!hasAlterTable) {
|
|
318
|
+
logError(`[sqlite-adapter] Migration ${file} failed (missing required table):`, err);
|
|
319
|
+
throw new Error(`Migration ${file} failed: ${err.message}`);
|
|
320
|
+
}
|
|
321
|
+
warn(
|
|
322
|
+
`[sqlite-adapter] Migration ${file} skipped: ALTER TABLE on non-existent table (${err.message})`
|
|
323
|
+
);
|
|
324
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(version);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
logError(`[sqlite-adapter] Migration ${file} failed:`, err);
|
|
329
|
+
throw new Error(`Migration ${file} failed: ${err.message}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Create vss_memories table if not exists
|
|
334
|
+
if (this.vectorSearchEnabled) {
|
|
335
|
+
const vssTables = this.prepare(
|
|
336
|
+
`
|
|
337
|
+
SELECT name FROM sqlite_master
|
|
338
|
+
WHERE type='table' AND name='vss_memories'
|
|
339
|
+
`
|
|
340
|
+
).all();
|
|
341
|
+
|
|
342
|
+
if (vssTables.length === 0) {
|
|
343
|
+
info('[sqlite-adapter] Creating vss_memories virtual table via sqlite-vec');
|
|
344
|
+
this.exec(`
|
|
345
|
+
CREATE VIRTUAL TABLE vss_memories USING vec0(
|
|
346
|
+
embedding float[384]
|
|
347
|
+
)
|
|
348
|
+
`);
|
|
349
|
+
}
|
|
350
|
+
} else {
|
|
351
|
+
warn('[sqlite-adapter] Skipping vss_memories creation (sqlite-vec unavailable)');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
module.exports = SQLiteAdapter;
|
|
357
|
+
|
|
358
|
+
function bufferToVector(buffer) {
|
|
359
|
+
if (!buffer) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
363
|
+
return new Float32Array(arrayBuffer);
|
|
364
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Statement Interface
|
|
3
|
+
*
|
|
4
|
+
* Wraps database-specific prepared statements to provide consistent API
|
|
5
|
+
* Compatible with better-sqlite3 and pg
|
|
6
|
+
*
|
|
7
|
+
* @module statement
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Base statement interface
|
|
12
|
+
* All statement wrappers must implement these methods
|
|
13
|
+
*/
|
|
14
|
+
class Statement {
|
|
15
|
+
/**
|
|
16
|
+
* Execute statement and return all rows
|
|
17
|
+
* @param {...*} _params - Query parameters
|
|
18
|
+
* @returns {Array<Object>} All matching rows
|
|
19
|
+
*/
|
|
20
|
+
all(..._params) {
|
|
21
|
+
throw new Error('all() must be implemented by subclass');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Execute statement and return first row
|
|
26
|
+
* @param {...*} _params - Query parameters
|
|
27
|
+
* @returns {Object|undefined} First matching row or undefined
|
|
28
|
+
*/
|
|
29
|
+
get(..._params) {
|
|
30
|
+
throw new Error('get() must be implemented by subclass');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Execute statement without returning rows
|
|
35
|
+
* @param {...*} _params - Query parameters
|
|
36
|
+
* @returns {Object} Execution info (changes, lastInsertRowid)
|
|
37
|
+
*/
|
|
38
|
+
run(..._params) {
|
|
39
|
+
throw new Error('run() must be implemented by subclass');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Release statement resources
|
|
44
|
+
*/
|
|
45
|
+
finalize() {
|
|
46
|
+
// Optional: Some drivers don't require cleanup
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* SQLite statement wrapper (better-sqlite3)
|
|
52
|
+
*/
|
|
53
|
+
class SQLiteStatement extends Statement {
|
|
54
|
+
constructor(stmt) {
|
|
55
|
+
super();
|
|
56
|
+
this.stmt = stmt;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
all(...params) {
|
|
60
|
+
return this.stmt.all(...params);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get(...params) {
|
|
64
|
+
return this.stmt.get(...params);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
run(...params) {
|
|
68
|
+
return this.stmt.run(...params);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
finalize() {
|
|
72
|
+
// better-sqlite3 statements don't need explicit cleanup
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* PostgreSQL statement wrapper (pg)
|
|
78
|
+
*
|
|
79
|
+
* Maps pg's async query interface to synchronous-like API
|
|
80
|
+
* Note: This requires careful handling in the adapter
|
|
81
|
+
*/
|
|
82
|
+
class PostgreSQLStatement extends Statement {
|
|
83
|
+
constructor(client, sql, paramMapping) {
|
|
84
|
+
super();
|
|
85
|
+
this.client = client;
|
|
86
|
+
this.sql = sql;
|
|
87
|
+
this.paramMapping = paramMapping; // Map ? placeholders to $1, $2, etc.
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Convert SQLite ? placeholders to PostgreSQL $1, $2, ...
|
|
92
|
+
* @param {string} sql - SQL with ? placeholders
|
|
93
|
+
* @returns {string} SQL with $N placeholders
|
|
94
|
+
*/
|
|
95
|
+
static convertPlaceholders(sql) {
|
|
96
|
+
let index = 0;
|
|
97
|
+
return sql.replace(/\?/g, () => `$${++index}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async all(...params) {
|
|
101
|
+
const result = await this.client.query(this.sql, params);
|
|
102
|
+
return result.rows;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async get(...params) {
|
|
106
|
+
const result = await this.client.query(this.sql, params);
|
|
107
|
+
return result.rows[0];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async run(...params) {
|
|
111
|
+
const result = await this.client.query(this.sql, params);
|
|
112
|
+
return {
|
|
113
|
+
changes: result.rowCount,
|
|
114
|
+
lastInsertRowid: result.rows[0]?.id || null, // PostgreSQL doesn't have rowid
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
finalize() {
|
|
119
|
+
// pg statements don't need explicit cleanup
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
Statement,
|
|
125
|
+
SQLiteStatement,
|
|
126
|
+
PostgreSQLStatement,
|
|
127
|
+
};
|