local-lite 0.0.5 → 0.0.6

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.
@@ -35,6 +35,143 @@ function formatColumnIdentifier(col) {
35
35
  }
36
36
  return `"${trimmed.replace(/"/g, '""')}"`;
37
37
  }
38
+ // ---------------------------------------------------------------------------
39
+ // Module-scoped SQL compilation helpers (defined once, reused on every query)
40
+ // ---------------------------------------------------------------------------
41
+ /**
42
+ * Compiles a single column comparison value into one or more SQL fragments.
43
+ * Supports null, boolean, scalar, and operator-object forms ($eq, $gt, $in, …).
44
+ */
45
+ function compileComparison(safeCol, val, params) {
46
+ if (val === null) {
47
+ return `${safeCol} IS NULL`;
48
+ }
49
+ if (typeof val === 'boolean') {
50
+ params.push(val ? 1 : 0);
51
+ return `${safeCol} = ?`;
52
+ }
53
+ if (typeof val === 'string' || typeof val === 'number') {
54
+ params.push(val);
55
+ return `${safeCol} = ?`;
56
+ }
57
+ if (typeof val === 'object' && !Array.isArray(val)) {
58
+ const ops = val;
59
+ const subClauses = [];
60
+ const eqVal = ops.$eq !== undefined ? ops.$eq : ops.eq;
61
+ if (eqVal !== undefined) {
62
+ subClauses.push(`${safeCol} = ?`);
63
+ params.push(typeof eqVal === 'boolean' ? (eqVal ? 1 : 0) : eqVal);
64
+ }
65
+ const neVal = ops.$ne !== undefined ? ops.$ne : ops.ne;
66
+ if (neVal !== undefined) {
67
+ subClauses.push(`${safeCol} != ?`);
68
+ params.push(typeof neVal === 'boolean' ? (neVal ? 1 : 0) : neVal);
69
+ }
70
+ const gtVal = ops.$gt !== undefined ? ops.$gt : ops.gt;
71
+ if (gtVal !== undefined) {
72
+ subClauses.push(`${safeCol} > ?`);
73
+ params.push(gtVal);
74
+ }
75
+ const gteVal = ops.$gte !== undefined ? ops.$gte : ops.gte;
76
+ if (gteVal !== undefined) {
77
+ subClauses.push(`${safeCol} >= ?`);
78
+ params.push(gteVal);
79
+ }
80
+ const ltVal = ops.$lt !== undefined ? ops.$lt : ops.lt;
81
+ if (ltVal !== undefined) {
82
+ subClauses.push(`${safeCol} < ?`);
83
+ params.push(ltVal);
84
+ }
85
+ const lteVal = ops.$lte !== undefined ? ops.$lte : ops.lte;
86
+ if (lteVal !== undefined) {
87
+ subClauses.push(`${safeCol} <= ?`);
88
+ params.push(lteVal);
89
+ }
90
+ const inVal = ops.$in !== undefined ? ops.$in : ops.in;
91
+ if (Array.isArray(inVal) && inVal.length > 0) {
92
+ const placeholders = inVal.map(() => '?').join(', ');
93
+ subClauses.push(`${safeCol} IN (${placeholders})`);
94
+ params.push(...inVal);
95
+ }
96
+ const ninVal = ops.$nin !== undefined ? ops.$nin : ops.nin;
97
+ if (Array.isArray(ninVal) && ninVal.length > 0) {
98
+ const placeholders = ninVal.map(() => '?').join(', ');
99
+ subClauses.push(`${safeCol} NOT IN (${placeholders})`);
100
+ params.push(...ninVal);
101
+ }
102
+ const containsVal = ops.$contains !== undefined ? ops.$contains : ops.contains;
103
+ if (containsVal !== undefined) {
104
+ subClauses.push(`${safeCol} LIKE ?`);
105
+ params.push(`%${containsVal}%`);
106
+ }
107
+ const startsWithVal = ops.$startsWith !== undefined ? ops.$startsWith : ops.startsWith;
108
+ if (startsWithVal !== undefined) {
109
+ subClauses.push(`${safeCol} LIKE ?`);
110
+ params.push(`${startsWithVal}%`);
111
+ }
112
+ const endsWithVal = ops.$endsWith !== undefined ? ops.$endsWith : ops.endsWith;
113
+ if (endsWithVal !== undefined) {
114
+ subClauses.push(`${safeCol} LIKE ?`);
115
+ params.push(`%${endsWithVal}`);
116
+ }
117
+ const nullVal = ops.$null !== undefined ? ops.$null : ops.null;
118
+ if (nullVal !== undefined) {
119
+ subClauses.push(nullVal ? `${safeCol} IS NULL` : `${safeCol} IS NOT NULL`);
120
+ }
121
+ if (subClauses.length > 0) {
122
+ return subClauses.join(' AND ');
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+ /**
128
+ * Recursively compiles a where-conditions object (including $or / $and) into SQL clauses.
129
+ */
130
+ function compileWhereConditions(where, params) {
131
+ const clauses = [];
132
+ // Handle $or — compile each branch, join with OR
133
+ if (Array.isArray(where.$or) && where.$or.length > 0) {
134
+ const orClauses = [];
135
+ for (const sub of where.$or) {
136
+ if (typeof sub === 'object' && sub !== null) {
137
+ const subList = compileWhereConditions(sub, params);
138
+ if (subList.length > 0) {
139
+ orClauses.push(`(${subList.join(' AND ')})`);
140
+ }
141
+ }
142
+ }
143
+ if (orClauses.length > 0) {
144
+ clauses.push(`(${orClauses.join(' OR ')})`);
145
+ }
146
+ }
147
+ // Handle $and — compile each branch, join with AND
148
+ if (Array.isArray(where.$and) && where.$and.length > 0) {
149
+ const andClauses = [];
150
+ for (const sub of where.$and) {
151
+ if (typeof sub === 'object' && sub !== null) {
152
+ const subList = compileWhereConditions(sub, params);
153
+ if (subList.length > 0) {
154
+ andClauses.push(`(${subList.join(' AND ')})`);
155
+ }
156
+ }
157
+ }
158
+ if (andClauses.length > 0) {
159
+ clauses.push(`(${andClauses.join(' AND ')})`);
160
+ }
161
+ }
162
+ // Handle regular column conditions
163
+ for (const [col, val] of Object.entries(where)) {
164
+ if (col === '$or' || col === '$and')
165
+ continue;
166
+ const safeCol = formatColumnIdentifier(col);
167
+ const clause = compileComparison(safeCol, val, params);
168
+ if (clause) {
169
+ clauses.push(clause);
170
+ }
171
+ }
172
+ return clauses;
173
+ }
174
+ // ---------------------------------------------------------------------------
38
175
  /**
39
176
  * Fluent, type-safe SQL Query Builder for local-first SQLite operations.
40
177
  * Allows writing expressive JavaScript code that compiles and executes high-performance SQL under the hood.
@@ -171,6 +308,7 @@ class QueryBuilder {
171
308
  }
172
309
  /**
173
310
  * Compiles the current builder state into an executable SQL string and parameter list.
311
+ * Uses module-scoped compileComparison / compileWhereConditions helpers (defined once).
174
312
  */
175
313
  toSQL() {
176
314
  const params = [];
@@ -184,79 +322,10 @@ class QueryBuilder {
184
322
  const col2 = formatColumnIdentifier(j.secondCol);
185
323
  sql += ` ${j.type} JOIN ${joinTable} ON ${col1} = ${col2}`;
186
324
  }
187
- // Build WHERE clauses
325
+ // Build WHERE clauses (delegates to module-scoped helpers)
188
326
  const whereClauses = [];
189
327
  if (this.whereConditions && typeof this.whereConditions === 'object') {
190
- for (const [col, val] of Object.entries(this.whereConditions)) {
191
- if (col === '$or' || col === '$and')
192
- continue;
193
- const safeCol = formatColumnIdentifier(col);
194
- if (val === null) {
195
- whereClauses.push(`${safeCol} IS NULL`);
196
- }
197
- else if (typeof val === 'boolean') {
198
- whereClauses.push(`${safeCol} = ?`);
199
- params.push(val ? 1 : 0);
200
- }
201
- else if (typeof val === 'string' || typeof val === 'number') {
202
- whereClauses.push(`${safeCol} = ?`);
203
- params.push(val);
204
- }
205
- else if (typeof val === 'object' && !Array.isArray(val)) {
206
- const ops = val;
207
- const eqVal = ops.$eq !== undefined ? ops.$eq : ops.eq;
208
- if (eqVal !== undefined) {
209
- whereClauses.push(`${safeCol} = ?`);
210
- params.push(typeof eqVal === 'boolean' ? (eqVal ? 1 : 0) : eqVal);
211
- }
212
- const neVal = ops.$ne !== undefined ? ops.$ne : ops.ne;
213
- if (neVal !== undefined) {
214
- whereClauses.push(`${safeCol} != ?`);
215
- params.push(typeof neVal === 'boolean' ? (neVal ? 1 : 0) : neVal);
216
- }
217
- const gtVal = ops.$gt !== undefined ? ops.$gt : ops.gt;
218
- if (gtVal !== undefined) {
219
- whereClauses.push(`${safeCol} > ?`);
220
- params.push(gtVal);
221
- }
222
- const gteVal = ops.$gte !== undefined ? ops.$gte : ops.gte;
223
- if (gteVal !== undefined) {
224
- whereClauses.push(`${safeCol} >= ?`);
225
- params.push(gteVal);
226
- }
227
- const ltVal = ops.$lt !== undefined ? ops.$lt : ops.lt;
228
- if (ltVal !== undefined) {
229
- whereClauses.push(`${safeCol} < ?`);
230
- params.push(ltVal);
231
- }
232
- const lteVal = ops.$lte !== undefined ? ops.$lte : ops.lte;
233
- if (lteVal !== undefined) {
234
- whereClauses.push(`${safeCol} <= ?`);
235
- params.push(lteVal);
236
- }
237
- const inVal = ops.$in !== undefined ? ops.$in : ops.in;
238
- if (Array.isArray(inVal) && inVal.length > 0) {
239
- const placeholders = inVal.map(() => '?').join(', ');
240
- whereClauses.push(`${safeCol} IN (${placeholders})`);
241
- params.push(...inVal);
242
- }
243
- const ninVal = ops.$nin !== undefined ? ops.$nin : ops.nin;
244
- if (Array.isArray(ninVal) && ninVal.length > 0) {
245
- const placeholders = ninVal.map(() => '?').join(', ');
246
- whereClauses.push(`${safeCol} NOT IN (${placeholders})`);
247
- params.push(...ninVal);
248
- }
249
- const containsVal = ops.$contains !== undefined ? ops.$contains : ops.contains;
250
- if (containsVal !== undefined) {
251
- whereClauses.push(`${safeCol} LIKE ?`);
252
- params.push(`%${containsVal}%`);
253
- }
254
- const nullVal = ops.$null !== undefined ? ops.$null : ops.null;
255
- if (nullVal !== undefined) {
256
- whereClauses.push(nullVal ? `${safeCol} IS NULL` : `${safeCol} IS NOT NULL`);
257
- }
258
- }
259
- }
328
+ whereClauses.push(...compileWhereConditions(this.whereConditions, params));
260
329
  }
261
330
  // Push down soft-deletion check on primary table if deleted column exists and not explicitly overridden
262
331
  const schemaManager = 'schemaManager' in this.client
@@ -48,26 +48,14 @@ function matchesField(fieldValue, filter) {
48
48
  if (ops.$lte !== undefined && !(fieldValue <= ops.$lte)) {
49
49
  return false;
50
50
  }
51
- if (ops.$contains !== undefined) {
52
- const str = fieldValue === null || fieldValue === undefined ? '' : String(fieldValue).toLowerCase();
53
- const search = String(ops.$contains).toLowerCase();
54
- if (!str.includes(search)) {
51
+ if (ops.$contains !== undefined || ops.$startsWith !== undefined || ops.$endsWith !== undefined) {
52
+ const str = fieldValue == null ? '' : String(fieldValue).toLowerCase();
53
+ if (ops.$contains !== undefined && !str.includes(String(ops.$contains).toLowerCase()))
55
54
  return false;
56
- }
57
- }
58
- if (ops.$startsWith !== undefined) {
59
- const str = fieldValue === null || fieldValue === undefined ? '' : String(fieldValue).toLowerCase();
60
- const prefix = String(ops.$startsWith).toLowerCase();
61
- if (!str.startsWith(prefix)) {
55
+ if (ops.$startsWith !== undefined && !str.startsWith(String(ops.$startsWith).toLowerCase()))
62
56
  return false;
63
- }
64
- }
65
- if (ops.$endsWith !== undefined) {
66
- const str = fieldValue === null || fieldValue === undefined ? '' : String(fieldValue).toLowerCase();
67
- const suffix = String(ops.$endsWith).toLowerCase();
68
- if (!str.endsWith(suffix)) {
57
+ if (ops.$endsWith !== undefined && !str.endsWith(String(ops.$endsWith).toLowerCase()))
69
58
  return false;
70
- }
71
59
  }
72
60
  if (ops.$null !== undefined) {
73
61
  const isNull = fieldValue === null || fieldValue === undefined;
@@ -131,18 +119,28 @@ function hasExplicitDeletedFilter(where) {
131
119
  function applyQueryOptions(rows, options) {
132
120
  if (!rows || rows.length === 0)
133
121
  return [];
134
- let result = [...rows];
135
- // 0. Automatic soft-delete filtering: omit deleted records unless includeDeleted is true
136
- // or the where filter explicitly targets 'deleted'.
137
- if (!options?.includeDeleted && !hasExplicitDeletedFilter(options?.where)) {
122
+ const needsDeleteFilter = !options?.includeDeleted && !hasExplicitDeletedFilter(options?.where);
123
+ const needsWhere = Boolean(options?.where);
124
+ const needsSort = Boolean(options?.orderBy);
125
+ const needsSlice = (options?.offset !== undefined && options.offset > 0) ||
126
+ (options?.limit !== undefined && options.limit > 0);
127
+ const needsSelect = Array.isArray(options?.select) && options.select.length > 0;
128
+ // Fast-path: nothing to do
129
+ if (!needsDeleteFilter && !needsWhere && !needsSort && !needsSlice && !needsSelect) {
130
+ return rows;
131
+ }
132
+ let result = rows;
133
+ // 0. Soft-delete filtering
134
+ if (needsDeleteFilter) {
138
135
  result = result.filter((row) => !isDeletedRecord(row));
139
136
  }
140
137
  // 1. Where filtering
141
- if (options?.where) {
138
+ if (needsWhere) {
142
139
  result = result.filter((row) => matchesWhere(row, options.where));
143
140
  }
144
141
  // 2. Order by
145
- if (options?.orderBy) {
142
+ if (needsSort) {
143
+ result = [...result]; // copy only before sort mutation
146
144
  const key = options.orderBy;
147
145
  const isAsc = (options.order || 'asc').toLowerCase() === 'asc';
148
146
  result.sort((a, b) => {
@@ -164,21 +162,19 @@ function applyQueryOptions(rows, options) {
164
162
  });
165
163
  }
166
164
  // 3. Offset and Limit
167
- if (options?.offset !== undefined && options.offset > 0) {
168
- result = result.slice(options.offset);
169
- }
170
- if (options?.limit !== undefined && options.limit > 0) {
171
- result = result.slice(0, options.limit);
165
+ if (needsSlice) {
166
+ const offset = options?.offset && options.offset > 0 ? options.offset : 0;
167
+ const limit = options?.limit && options.limit > 0 ? options.limit : undefined;
168
+ result = limit !== undefined ? result.slice(offset, offset + limit) : result.slice(offset);
172
169
  }
173
- // 4. Column Projection
174
- if (options?.select && Array.isArray(options.select) && options.select.length > 0) {
170
+ // 4. Column projection
171
+ if (needsSelect) {
175
172
  const fields = options.select;
176
173
  result = result.map((row) => {
177
174
  const projected = {};
178
175
  for (const field of fields) {
179
- if (field in row) {
176
+ if (field in row)
180
177
  projected[field] = row[field];
181
- }
182
178
  }
183
179
  return projected;
184
180
  });
@@ -3,15 +3,6 @@ export interface DBExecutable {
3
3
  }
4
4
  /**
5
5
  * Detects the appropriate SQLite column type affinity from a JavaScript runtime value.
6
- *
7
- * Mapping:
8
- * - boolean -> INTEGER (0 or 1)
9
- * - integer -> INTEGER
10
- * - float/double -> REAL
11
- * - string -> TEXT
12
- * - object/array -> TEXT (JSON formatted)
13
- * - Uint8Array -> BLOB
14
- * - null/undefined -> TEXT (flexible SQLite type affinity)
15
6
  */
16
7
  export declare function detectSQLiteType(value: any): 'INTEGER' | 'REAL' | 'TEXT' | 'BLOB';
17
8
  /**
@@ -24,18 +15,16 @@ export declare class SchemaManager {
24
15
  private knownTables;
25
16
  private knownColumns;
26
17
  private knownIndexes;
27
- /**
28
- * Resets the schema cache (useful for testing or full database resets).
29
- */
30
18
  clearCache(): void;
31
- /**
32
- * Checks whether a column is known to exist on the specified table.
33
- */
34
19
  hasColumn(tableName: string, column: string): boolean;
35
20
  /**
36
21
  * Introspects table columns via PRAGMA table_info if not previously cached.
37
22
  */
38
23
  ensureTableIntrospected(db: DBExecutable, tableName: string): Promise<void>;
24
+ /**
25
+ * Automatically creates single-column and composite FK+deleted indexes for given columns.
26
+ */
27
+ private autoIndexColumns;
39
28
  /**
40
29
  * Ensures that the target table exists in SQLite with the specified primary key.
41
30
  */
@@ -6,36 +6,14 @@ exports.formatValueForStorage = formatValueForStorage;
6
6
  const sql_js_1 = require("../utils/sql.js");
7
7
  /**
8
8
  * Detects the appropriate SQLite column type affinity from a JavaScript runtime value.
9
- *
10
- * Mapping:
11
- * - boolean -> INTEGER (0 or 1)
12
- * - integer -> INTEGER
13
- * - float/double -> REAL
14
- * - string -> TEXT
15
- * - object/array -> TEXT (JSON formatted)
16
- * - Uint8Array -> BLOB
17
- * - null/undefined -> TEXT (flexible SQLite type affinity)
18
9
  */
19
10
  function detectSQLiteType(value) {
20
- if (value === null || value === undefined) {
21
- return 'TEXT';
22
- }
23
- const type = typeof value;
24
- if (type === 'boolean') {
11
+ if (typeof value === 'boolean')
25
12
  return 'INTEGER';
26
- }
27
- if (type === 'number') {
13
+ if (typeof value === 'number')
28
14
  return Number.isInteger(value) ? 'INTEGER' : 'REAL';
29
- }
30
- if (type === 'string') {
31
- return 'TEXT';
32
- }
33
- if (value instanceof Uint8Array) {
15
+ if (value instanceof Uint8Array)
34
16
  return 'BLOB';
35
- }
36
- if (type === 'object') {
37
- return 'TEXT';
38
- }
39
17
  return 'TEXT';
40
18
  }
41
19
  /**
@@ -44,32 +22,23 @@ function detectSQLiteType(value) {
44
22
  * - Plain objects and arrays are JSON serialized
45
23
  */
46
24
  function formatValueForStorage(value) {
47
- if (value === null || value === undefined) {
25
+ if (value === null || value === undefined)
48
26
  return null;
49
- }
50
- if (typeof value === 'boolean') {
27
+ if (typeof value === 'boolean')
51
28
  return value ? 1 : 0;
52
- }
53
- if (typeof value === 'object' && !(value instanceof Uint8Array)) {
29
+ if (typeof value === 'object' && !(value instanceof Uint8Array))
54
30
  return JSON.stringify(value);
55
- }
56
31
  return value;
57
32
  }
58
33
  class SchemaManager {
59
34
  knownTables = new Set();
60
35
  knownColumns = new Set();
61
36
  knownIndexes = new Set();
62
- /**
63
- * Resets the schema cache (useful for testing or full database resets).
64
- */
65
37
  clearCache() {
66
38
  this.knownTables.clear();
67
39
  this.knownColumns.clear();
68
40
  this.knownIndexes.clear();
69
41
  }
70
- /**
71
- * Checks whether a column is known to exist on the specified table.
72
- */
73
42
  hasColumn(tableName, column) {
74
43
  return this.knownColumns.has(`${tableName}:${column.toLowerCase()}`);
75
44
  }
@@ -84,57 +53,54 @@ class SchemaManager {
84
53
  const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
85
54
  try {
86
55
  const tableInfo = await db.execute(`PRAGMA table_info(${safeTable});`);
56
+ const cols = [];
87
57
  if (Array.isArray(tableInfo)) {
88
58
  for (const col of tableInfo) {
89
- if (col && col.name) {
59
+ if (col?.name) {
90
60
  this.knownColumns.add(`${tableName}:${String(col.name).toLowerCase()}`);
61
+ cols.push(String(col.name));
91
62
  }
92
63
  }
93
64
  }
94
- // Auto-create high performance single and composite indexes for foreign keys (_id) and soft-deletes (deleted)
95
- const hasDeleted = this.knownColumns.has(`${tableName}:deleted`);
96
- for (const entry of this.knownColumns) {
97
- if (!entry.startsWith(`${tableName}:`))
98
- continue;
99
- const colName = entry.slice(tableName.length + 1);
100
- if (colName.endsWith('_id') && colName !== 'id') {
101
- const fkIndexKey = `${tableName}:${colName}`;
102
- if (!this.knownIndexes.has(fkIndexKey)) {
103
- this.knownIndexes.add(fkIndexKey);
104
- try {
105
- const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${colName}`);
106
- const safeFk = (0, sql_js_1.escapeIdentifier)(colName);
107
- await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeFk});`);
108
- }
109
- catch { }
65
+ await this.autoIndexColumns(db, tableName, safeTable, cols);
66
+ }
67
+ catch { }
68
+ }
69
+ /**
70
+ * Automatically creates single-column and composite FK+deleted indexes for given columns.
71
+ */
72
+ async autoIndexColumns(db, tableName, safeTable, columns) {
73
+ const hasDeleted = this.knownColumns.has(`${tableName}:deleted`);
74
+ for (const key of columns) {
75
+ const lower = key.toLowerCase();
76
+ if (lower.endsWith('_id') || lower === 'deleted' || lower === 'created_at' || lower === 'updated_at') {
77
+ const indexKey = `${tableName}:${lower}`;
78
+ if (!this.knownIndexes.has(indexKey)) {
79
+ this.knownIndexes.add(indexKey);
80
+ try {
81
+ await db.execute(`CREATE INDEX IF NOT EXISTS ${(0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${key}`)} ON ${safeTable} (${(0, sql_js_1.escapeIdentifier)(key)});`);
110
82
  }
111
- if (hasDeleted) {
112
- const compositeKey = `${tableName}:${colName}_deleted`;
113
- if (!this.knownIndexes.has(compositeKey)) {
114
- this.knownIndexes.add(compositeKey);
115
- try {
116
- const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${colName}_del`);
117
- const safeFk = (0, sql_js_1.escapeIdentifier)(colName);
118
- const safeDel = (0, sql_js_1.escapeIdentifier)('deleted');
119
- await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeFk}, ${safeDel});`);
120
- }
121
- catch { }
122
- }
83
+ catch { }
84
+ }
85
+ }
86
+ if (hasDeleted && lower.endsWith('_id') && lower !== 'id') {
87
+ const compositeKey = `${tableName}:${lower}_deleted`;
88
+ if (!this.knownIndexes.has(compositeKey)) {
89
+ this.knownIndexes.add(compositeKey);
90
+ try {
91
+ await db.execute(`CREATE INDEX IF NOT EXISTS ${(0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${lower}_del`)} ON ${safeTable} (${(0, sql_js_1.escapeIdentifier)(key)}, "deleted");`);
123
92
  }
93
+ catch { }
124
94
  }
125
95
  }
126
96
  }
127
- catch {
128
- // Ignore
129
- }
130
97
  }
131
98
  /**
132
99
  * Ensures that the target table exists in SQLite with the specified primary key.
133
100
  */
134
101
  async ensureTable(db, tableName, primaryKey = 'id') {
135
- if (this.knownTables.has(tableName)) {
102
+ if (this.knownTables.has(tableName))
136
103
  return;
137
- }
138
104
  const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
139
105
  const safePk = (0, sql_js_1.escapeIdentifier)(primaryKey);
140
106
  await db.execute(`CREATE TABLE IF NOT EXISTS ${safeTable} (${safePk} TEXT PRIMARY KEY);`);
@@ -147,102 +113,46 @@ class SchemaManager {
147
113
  */
148
114
  async ensureColumns(db, tableName, payload, primaryKey = 'id') {
149
115
  await this.ensureTable(db, tableName, primaryKey);
150
- if (!payload || typeof payload !== 'object') {
116
+ if (!payload || typeof payload !== 'object')
151
117
  return {};
152
- }
153
118
  const keys = Object.keys(payload);
154
- if (keys.length === 0) {
119
+ if (keys.length === 0)
155
120
  return {};
156
- }
157
121
  const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
158
122
  let cacheMiss = false;
159
123
  for (const key of keys) {
160
- if (key === primaryKey)
161
- continue;
162
- const cacheKey = `${tableName}:${key.toLowerCase()}`;
163
- if (!this.knownColumns.has(cacheKey)) {
124
+ if (key !== primaryKey && !this.knownColumns.has(`${tableName}:${key.toLowerCase()}`)) {
164
125
  cacheMiss = true;
165
126
  break;
166
127
  }
167
128
  }
168
129
  if (cacheMiss) {
169
- // Query PRAGMA table_info to discover existing columns from SQLite
170
130
  try {
171
131
  const tableInfo = await db.execute(`PRAGMA table_info(${safeTable});`);
172
132
  if (Array.isArray(tableInfo)) {
173
133
  for (const col of tableInfo) {
174
- if (col && col.name) {
134
+ if (col?.name) {
175
135
  this.knownColumns.add(`${tableName}:${String(col.name).toLowerCase()}`);
176
136
  }
177
137
  }
178
138
  }
179
139
  }
180
- catch (err) {
181
- // Fallback for mocks or custom drivers where PRAGMA table_info might not be supported
182
- }
183
- // Check for columns that still do not exist, and alter table
140
+ catch { }
184
141
  for (const key of keys) {
185
142
  if (key === primaryKey)
186
143
  continue;
187
144
  const cacheKey = `${tableName}:${key.toLowerCase()}`;
188
145
  if (!this.knownColumns.has(cacheKey)) {
189
146
  const detectedType = detectSQLiteType(payload[key]);
190
- const safeCol = (0, sql_js_1.escapeIdentifier)(key);
191
147
  try {
192
- await db.execute(`ALTER TABLE ${safeTable} ADD COLUMN ${safeCol} ${detectedType};`);
193
- }
194
- catch (err) {
195
- // Ignore if column already exists (e.g. concurrent race)
196
- const msg = String(err?.message || err || '');
197
- if (!msg.toLowerCase().includes('duplicate column')) {
198
- // Ignore or rethrow
199
- }
148
+ await db.execute(`ALTER TABLE ${safeTable} ADD COLUMN ${(0, sql_js_1.escapeIdentifier)(key)} ${detectedType};`);
200
149
  }
150
+ catch { }
201
151
  this.knownColumns.add(cacheKey);
202
152
  }
203
- // Auto-index foreign keys and common query columns
204
- const lowerKey = key.toLowerCase();
205
- if (lowerKey.endsWith('_id') ||
206
- lowerKey === 'deleted' ||
207
- lowerKey === 'created_at' ||
208
- lowerKey === 'updated_at') {
209
- const indexKey = `${tableName}:${lowerKey}`;
210
- if (!this.knownIndexes.has(indexKey)) {
211
- this.knownIndexes.add(indexKey);
212
- try {
213
- const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${key}`);
214
- const safeCol = (0, sql_js_1.escapeIdentifier)(key);
215
- await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeCol});`);
216
- }
217
- catch {
218
- // Ignore if indexes are not supported by adapter or already exist
219
- }
220
- }
221
- }
222
- }
223
- }
224
- // Auto-create high performance composite index for foreign keys with soft-deletion
225
- if (this.knownColumns.has(`${tableName}:deleted`)) {
226
- for (const key of keys) {
227
- const lowerKey = key.toLowerCase();
228
- if (lowerKey.endsWith('_id') && lowerKey !== 'id') {
229
- const compositeKey = `${tableName}:${lowerKey}_deleted`;
230
- if (!this.knownIndexes.has(compositeKey)) {
231
- this.knownIndexes.add(compositeKey);
232
- try {
233
- const idxName = (0, sql_js_1.escapeIdentifier)(`idx_${tableName}_${lowerKey}_del`);
234
- const safeFk = (0, sql_js_1.escapeIdentifier)(key);
235
- const safeDel = (0, sql_js_1.escapeIdentifier)('deleted');
236
- await db.execute(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${safeTable} (${safeFk}, ${safeDel});`);
237
- }
238
- catch {
239
- // Ignore
240
- }
241
- }
242
- }
243
153
  }
154
+ await this.autoIndexColumns(db, tableName, safeTable, keys);
244
155
  }
245
- // Format all values for SQLite storage
246
156
  const formatted = {};
247
157
  for (const key of keys) {
248
158
  formatted[key] = formatValueForStorage(payload[key]);