meadow-connection-sqlite 1.0.13 → 1.0.15

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.
@@ -0,0 +1,1036 @@
1
+ /**
2
+ * Meadow SQLite Schema Provider
3
+ *
4
+ * Handles table creation, dropping, and DDL generation for SQLite.
5
+ * Separated from the connection provider to allow independent extension
6
+ * for indexing, foreign keys, and other schema operations.
7
+ *
8
+ * @author Steven Velozo <steven@velozo.com>
9
+ */
10
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
11
+
12
+ class MeadowSchemaSQLite extends libFableServiceProviderBase
13
+ {
14
+ constructor(pFable, pOptions, pServiceHash)
15
+ {
16
+ super(pFable, pOptions, pServiceHash);
17
+
18
+ this.serviceType = 'MeadowSchemaSQLite';
19
+
20
+ // Reference to the database connection, set by the connection provider
21
+ this._Database = false;
22
+ }
23
+
24
+ /**
25
+ * Set the database reference for executing DDL statements.
26
+ * @param {object} pDatabase - better-sqlite3 database instance
27
+ * @returns {MeadowSchemaSQLite} this (for chaining)
28
+ */
29
+ setDatabase(pDatabase)
30
+ {
31
+ this._Database = pDatabase;
32
+ return this;
33
+ }
34
+
35
+ generateDropTableStatement(pTableName)
36
+ {
37
+ return `DROP TABLE IF EXISTS ${pTableName};`;
38
+ }
39
+
40
+ generateCreateTableStatement(pMeadowTableSchema)
41
+ {
42
+ this.log.info(`--> Building the table create string for ${pMeadowTableSchema.TableName} ...`);
43
+
44
+ let tmpPrimaryKey = false;
45
+ let tmpCreateTableStatement = `-- [ ${pMeadowTableSchema.TableName} ]`;
46
+ tmpCreateTableStatement += `\nCREATE TABLE IF NOT EXISTS ${pMeadowTableSchema.TableName}\n (`;
47
+ for (let j = 0; j < pMeadowTableSchema.Columns.length; j++)
48
+ {
49
+ let tmpColumn = pMeadowTableSchema.Columns[j];
50
+
51
+ // If we aren't the first column, append a comma.
52
+ if (j > 0)
53
+ {
54
+ tmpCreateTableStatement += `,`;
55
+ }
56
+
57
+ tmpCreateTableStatement += `\n`;
58
+ switch (tmpColumn.DataType)
59
+ {
60
+ case 'ID':
61
+ tmpCreateTableStatement += ` ${tmpColumn.Column} INTEGER PRIMARY KEY AUTOINCREMENT`;
62
+ tmpPrimaryKey = tmpColumn.Column;
63
+ break;
64
+ case 'GUID':
65
+ tmpCreateTableStatement += ` ${tmpColumn.Column} TEXT DEFAULT '00000000-0000-0000-0000-000000000000'`;
66
+ break;
67
+ case 'ForeignKey':
68
+ tmpCreateTableStatement += ` ${tmpColumn.Column} INTEGER NOT NULL DEFAULT 0`;
69
+ break;
70
+ case 'Numeric':
71
+ tmpCreateTableStatement += ` ${tmpColumn.Column} INTEGER NOT NULL DEFAULT 0`;
72
+ break;
73
+ case 'Decimal':
74
+ tmpCreateTableStatement += ` ${tmpColumn.Column} REAL`;
75
+ break;
76
+ case 'String':
77
+ tmpCreateTableStatement += ` ${tmpColumn.Column} TEXT NOT NULL DEFAULT ''`;
78
+ break;
79
+ case 'Text':
80
+ tmpCreateTableStatement += ` ${tmpColumn.Column} TEXT`;
81
+ break;
82
+ case 'DateTime':
83
+ tmpCreateTableStatement += ` ${tmpColumn.Column} TEXT`;
84
+ break;
85
+ case 'Boolean':
86
+ tmpCreateTableStatement += ` ${tmpColumn.Column} INTEGER NOT NULL DEFAULT 0`;
87
+ break;
88
+ default:
89
+ break;
90
+ }
91
+ }
92
+ tmpCreateTableStatement += `\n );`;
93
+
94
+ this.log.info(`Generated Create Table Statement: ${tmpCreateTableStatement}`);
95
+
96
+ return tmpCreateTableStatement;
97
+ }
98
+
99
+ createTables(pMeadowSchema, fCallback)
100
+ {
101
+ this.fable.Utility.eachLimit(pMeadowSchema.Tables, 1,
102
+ (pTable, fCreateComplete) =>
103
+ {
104
+ return this.createTable(pTable, fCreateComplete)
105
+ },
106
+ (pCreateError) =>
107
+ {
108
+ if (pCreateError)
109
+ {
110
+ this.log.error(`Meadow-SQLite Error creating tables from Schema: ${pCreateError}`,pCreateError);
111
+ }
112
+ this.log.info('Done creating tables!');
113
+ return fCallback(pCreateError);
114
+ });
115
+ }
116
+
117
+ createTable(pMeadowTableSchema, fCallback)
118
+ {
119
+ let tmpCreateTableStatement = this.generateCreateTableStatement(pMeadowTableSchema);
120
+ try
121
+ {
122
+ this._Database.exec(tmpCreateTableStatement);
123
+ this.log.info(`Meadow-SQLite CREATE TABLE ${pMeadowTableSchema.TableName} Success`);
124
+ return fCallback();
125
+ }
126
+ catch (pError)
127
+ {
128
+ this.log.error(`Meadow-SQLite CREATE TABLE ${pMeadowTableSchema.TableName} failed!`, pError);
129
+ return fCallback(pError);
130
+ }
131
+ }
132
+
133
+ // ========================================================================
134
+ // Index Generation
135
+ // ========================================================================
136
+
137
+ /**
138
+ * Derive index definitions from a Meadow table schema.
139
+ *
140
+ * Automatically generates indices for:
141
+ * - GUID columns -> unique index AK_M_{Column}
142
+ * - ForeignKey columns -> regular index IX_M_{Column}
143
+ *
144
+ * Column-level Indexed property:
145
+ * - Indexed: true -> regular index IX_M_T_{Table}_C_{Column}
146
+ * - Indexed: 'unique' -> unique index AK_M_T_{Table}_C_{Column}
147
+ * - IndexName overrides the auto-generated name (for round-trip fidelity)
148
+ *
149
+ * Also includes any explicit entries from pMeadowTableSchema.Indices[]
150
+ * (for multi-column composite indices).
151
+ *
152
+ * Each index definition is:
153
+ * { Name, TableName, Columns[], Unique, Strategy }
154
+ *
155
+ * @param {object} pMeadowTableSchema - Meadow table schema object
156
+ * @returns {Array} Array of index definition objects
157
+ */
158
+ getIndexDefinitionsFromSchema(pMeadowTableSchema)
159
+ {
160
+ let tmpIndices = [];
161
+ let tmpTableName = pMeadowTableSchema.TableName;
162
+
163
+ // Auto-detect from column types
164
+ for (let j = 0; j < pMeadowTableSchema.Columns.length; j++)
165
+ {
166
+ let tmpColumn = pMeadowTableSchema.Columns[j];
167
+
168
+ switch (tmpColumn.DataType)
169
+ {
170
+ case 'GUID':
171
+ tmpIndices.push(
172
+ {
173
+ Name: `AK_M_${tmpColumn.Column}`,
174
+ TableName: tmpTableName,
175
+ Columns: [tmpColumn.Column],
176
+ Unique: true,
177
+ Strategy: ''
178
+ });
179
+ break;
180
+ case 'ForeignKey':
181
+ tmpIndices.push(
182
+ {
183
+ Name: `IX_M_${tmpColumn.Column}`,
184
+ TableName: tmpTableName,
185
+ Columns: [tmpColumn.Column],
186
+ Unique: false,
187
+ Strategy: ''
188
+ });
189
+ break;
190
+ default:
191
+ // Column-level Indexed property: generates a single-column index
192
+ // with a consistent naming convention.
193
+ // Indexed: true -> IX_M_T_{Table}_C_{Column} (regular)
194
+ // Indexed: 'unique' -> AK_M_T_{Table}_C_{Column} (unique)
195
+ // Optional IndexName property overrides the auto-generated name.
196
+ if (tmpColumn.Indexed)
197
+ {
198
+ let tmpIsUnique = (tmpColumn.Indexed === 'unique');
199
+ let tmpPrefix = tmpIsUnique ? 'AK_M_T' : 'IX_M_T';
200
+ let tmpAutoName = `${tmpPrefix}_${tmpTableName}_C_${tmpColumn.Column}`;
201
+ tmpIndices.push(
202
+ {
203
+ Name: tmpColumn.IndexName || tmpAutoName,
204
+ TableName: tmpTableName,
205
+ Columns: [tmpColumn.Column],
206
+ Unique: tmpIsUnique,
207
+ Strategy: ''
208
+ });
209
+ }
210
+ break;
211
+ }
212
+ }
213
+
214
+ // Include any explicitly defined indices on the schema
215
+ if (Array.isArray(pMeadowTableSchema.Indices))
216
+ {
217
+ for (let k = 0; k < pMeadowTableSchema.Indices.length; k++)
218
+ {
219
+ let tmpExplicitIndex = pMeadowTableSchema.Indices[k];
220
+ tmpIndices.push(
221
+ {
222
+ Name: tmpExplicitIndex.Name || `IX_${tmpTableName}_${k}`,
223
+ TableName: tmpTableName,
224
+ Columns: Array.isArray(tmpExplicitIndex.Columns) ? tmpExplicitIndex.Columns : [tmpExplicitIndex.Columns],
225
+ Unique: tmpExplicitIndex.Unique || false,
226
+ Strategy: tmpExplicitIndex.Strategy || ''
227
+ });
228
+ }
229
+ }
230
+
231
+ return tmpIndices;
232
+ }
233
+
234
+ /**
235
+ * Build the column list for an index, comma-separated.
236
+ * @param {Array} pColumns - Array of column name strings
237
+ * @returns {string}
238
+ */
239
+ _buildColumnList(pColumns)
240
+ {
241
+ return pColumns.join(', ');
242
+ }
243
+
244
+ /**
245
+ * Generate a full idempotent SQL script for creating all indices on a table.
246
+ *
247
+ * SQLite supports CREATE INDEX IF NOT EXISTS natively, so the
248
+ * idempotent script is straightforward.
249
+ *
250
+ * @param {object} pMeadowTableSchema - Meadow table schema object
251
+ * @returns {string} Complete SQL script
252
+ */
253
+ generateCreateIndexScript(pMeadowTableSchema)
254
+ {
255
+ let tmpIndices = this.getIndexDefinitionsFromSchema(pMeadowTableSchema);
256
+ let tmpTableName = pMeadowTableSchema.TableName;
257
+
258
+ if (tmpIndices.length === 0)
259
+ {
260
+ return `-- No indices to create for ${tmpTableName}\n`;
261
+ }
262
+
263
+ let tmpScript = `-- Index Definitions for ${tmpTableName} -- Generated ${new Date().toJSON()}\n\n`;
264
+
265
+ for (let i = 0; i < tmpIndices.length; i++)
266
+ {
267
+ let tmpIndex = tmpIndices[i];
268
+ let tmpColumnList = this._buildColumnList(tmpIndex.Columns);
269
+ let tmpCreateKeyword = tmpIndex.Unique ? 'CREATE UNIQUE INDEX' : 'CREATE INDEX';
270
+
271
+ tmpScript += `-- Index: ${tmpIndex.Name}\n`;
272
+ tmpScript += `${tmpCreateKeyword} IF NOT EXISTS ${tmpIndex.Name} ON ${tmpIndex.TableName}(${tmpColumnList});\n\n`;
273
+ }
274
+
275
+ return tmpScript;
276
+ }
277
+
278
+ /**
279
+ * Generate an array of individual CREATE INDEX SQL statements for a table.
280
+ *
281
+ * Each entry is an object with:
282
+ * { Name, Statement, CheckStatement }
283
+ *
284
+ * - Statement: the raw CREATE [UNIQUE] INDEX ... SQL
285
+ * - CheckStatement: a SELECT against sqlite_master that returns the count
286
+ * of matching indices (0 = does not exist)
287
+ *
288
+ * @param {object} pMeadowTableSchema - Meadow table schema object
289
+ * @returns {Array} Array of { Name, Statement, CheckStatement } objects
290
+ */
291
+ generateCreateIndexStatements(pMeadowTableSchema)
292
+ {
293
+ let tmpIndices = this.getIndexDefinitionsFromSchema(pMeadowTableSchema);
294
+ let tmpStatements = [];
295
+
296
+ for (let i = 0; i < tmpIndices.length; i++)
297
+ {
298
+ let tmpIndex = tmpIndices[i];
299
+ let tmpColumnList = this._buildColumnList(tmpIndex.Columns);
300
+ let tmpCreateKeyword = tmpIndex.Unique ? 'CREATE UNIQUE INDEX' : 'CREATE INDEX';
301
+
302
+ tmpStatements.push(
303
+ {
304
+ Name: tmpIndex.Name,
305
+ Statement: `${tmpCreateKeyword} ${tmpIndex.Name} ON ${tmpIndex.TableName}(${tmpColumnList})`,
306
+ CheckStatement: `SELECT COUNT(*) AS IndexExists FROM sqlite_master WHERE type = 'index' AND name = '${tmpIndex.Name}'`
307
+ });
308
+ }
309
+
310
+ return tmpStatements;
311
+ }
312
+
313
+ /**
314
+ * Programmatically create a single index on the database.
315
+ *
316
+ * Uses CREATE INDEX IF NOT EXISTS for idempotent execution.
317
+ * SQLite is synchronous via better-sqlite3.
318
+ *
319
+ * @param {object} pIndexStatement - Object from generateCreateIndexStatements()
320
+ * @param {Function} fCallback - callback(pError)
321
+ */
322
+ createIndex(pIndexStatement, fCallback)
323
+ {
324
+ if (!this._Database)
325
+ {
326
+ this.log.error(`Meadow-SQLite CREATE INDEX ${pIndexStatement.Name} failed: not connected.`);
327
+ return fCallback(new Error('Not connected to SQLite'));
328
+ }
329
+
330
+ try
331
+ {
332
+ // Inject IF NOT EXISTS for idempotent execution
333
+ let tmpStatement = pIndexStatement.Statement.replace('CREATE UNIQUE INDEX ', 'CREATE UNIQUE INDEX IF NOT EXISTS ').replace('CREATE INDEX ', 'CREATE INDEX IF NOT EXISTS ');
334
+ this._Database.exec(tmpStatement);
335
+ this.log.info(`Meadow-SQLite CREATE INDEX ${pIndexStatement.Name} executed successfully.`);
336
+ return fCallback();
337
+ }
338
+ catch (pError)
339
+ {
340
+ this.log.error(`Meadow-SQLite CREATE INDEX ${pIndexStatement.Name} failed!`, pError);
341
+ return fCallback(pError);
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Programmatically create all indices for a single table.
347
+ *
348
+ * @param {object} pMeadowTableSchema - Meadow table schema object
349
+ * @param {Function} fCallback - callback(pError)
350
+ */
351
+ createIndices(pMeadowTableSchema, fCallback)
352
+ {
353
+ let tmpStatements = this.generateCreateIndexStatements(pMeadowTableSchema);
354
+
355
+ if (tmpStatements.length === 0)
356
+ {
357
+ this.log.info(`No indices to create for ${pMeadowTableSchema.TableName}.`);
358
+ return fCallback();
359
+ }
360
+
361
+ this.fable.Utility.eachLimit(tmpStatements, 1,
362
+ (pStatement, fCreateComplete) =>
363
+ {
364
+ return this.createIndex(pStatement, fCreateComplete);
365
+ },
366
+ (pCreateError) =>
367
+ {
368
+ if (pCreateError)
369
+ {
370
+ this.log.error(`Meadow-SQLite Error creating indices for ${pMeadowTableSchema.TableName}: ${pCreateError}`, pCreateError);
371
+ }
372
+ else
373
+ {
374
+ this.log.info(`Done creating indices for ${pMeadowTableSchema.TableName}!`);
375
+ }
376
+ return fCallback(pCreateError);
377
+ });
378
+ }
379
+
380
+ /**
381
+ * Programmatically create all indices for all tables in a schema.
382
+ *
383
+ * @param {object} pMeadowSchema - Meadow schema object with Tables array
384
+ * @param {Function} fCallback - callback(pError)
385
+ */
386
+ createAllIndices(pMeadowSchema, fCallback)
387
+ {
388
+ this.fable.Utility.eachLimit(pMeadowSchema.Tables, 1,
389
+ (pTable, fCreateComplete) =>
390
+ {
391
+ return this.createIndices(pTable, fCreateComplete);
392
+ },
393
+ (pCreateError) =>
394
+ {
395
+ if (pCreateError)
396
+ {
397
+ this.log.error(`Meadow-SQLite Error creating indices from schema: ${pCreateError}`, pCreateError);
398
+ }
399
+ this.log.info('Done creating all indices!');
400
+ return fCallback(pCreateError);
401
+ });
402
+ }
403
+ // ========================================================================
404
+ // Database Introspection
405
+ // ========================================================================
406
+
407
+ /**
408
+ * List all user tables in the connected SQLite database.
409
+ *
410
+ * @param {Function} fCallback - callback(pError, pTableNames)
411
+ */
412
+ listTables(fCallback)
413
+ {
414
+ if (!this._Database)
415
+ {
416
+ return fCallback(new Error('Not connected to SQLite'));
417
+ }
418
+
419
+ try
420
+ {
421
+ let tmpRows = this._Database.prepare(
422
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
423
+ ).all();
424
+ let tmpNames = tmpRows.map((pRow) => { return pRow.name; });
425
+ return fCallback(null, tmpNames);
426
+ }
427
+ catch (pError)
428
+ {
429
+ this.log.error('Meadow-SQLite listTables failed!', pError);
430
+ return fCallback(pError);
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Map a SQLite native type string to a Meadow DataType.
436
+ *
437
+ * Uses conservative heuristics:
438
+ * 1. Primary key with AUTOINCREMENT → ID
439
+ * 2. Column name contains "GUID" and type is TEXT → GUID
440
+ * 3. Foreign key constraint exists → ForeignKey
441
+ * 4. Native type mapping for straightforward cases
442
+ *
443
+ * @param {object} pColumnInfo - PRAGMA table_info row
444
+ * @param {string} pColumnInfo.name - Column name
445
+ * @param {string} pColumnInfo.type - Native SQLite type (e.g. 'TEXT', 'INTEGER')
446
+ * @param {number} pColumnInfo.pk - 1 if primary key, 0 otherwise
447
+ * @param {boolean} pIsAutoIncrement - Whether this column has AUTOINCREMENT
448
+ * @param {Set} pForeignKeyColumns - Set of column names that have FK constraints
449
+ * @returns {object} { DataType, Size }
450
+ */
451
+ _mapSQLiteTypeToMeadow(pColumnInfo, pIsAutoIncrement, pForeignKeyColumns)
452
+ {
453
+ let tmpName = pColumnInfo.name;
454
+ let tmpType = (pColumnInfo.type || '').toUpperCase().trim();
455
+
456
+ // Priority 1: Primary key with auto-increment → ID
457
+ if (pColumnInfo.pk === 1 && pIsAutoIncrement)
458
+ {
459
+ return { DataType: 'ID', Size: '' };
460
+ }
461
+
462
+ // Priority 2: Column name contains "GUID" and type is TEXT-like → GUID
463
+ if (tmpName.toUpperCase().indexOf('GUID') >= 0 && (tmpType === 'TEXT' || tmpType === '' || tmpType.indexOf('VARCHAR') >= 0 || tmpType.indexOf('CHAR') >= 0))
464
+ {
465
+ return { DataType: 'GUID', Size: '' };
466
+ }
467
+
468
+ // Priority 3: Has FK constraint → ForeignKey
469
+ if (pForeignKeyColumns && pForeignKeyColumns.has(tmpName))
470
+ {
471
+ return { DataType: 'ForeignKey', Size: '' };
472
+ }
473
+
474
+ // Priority 4: Native type mapping
475
+ if (tmpType === 'REAL' || tmpType.indexOf('DOUBLE') >= 0 || tmpType.indexOf('FLOAT') >= 0)
476
+ {
477
+ return { DataType: 'Decimal', Size: '' };
478
+ }
479
+
480
+ if (tmpType.indexOf('DECIMAL') >= 0 || tmpType.indexOf('NUMERIC') >= 0)
481
+ {
482
+ // Extract precision from DECIMAL(p,s)
483
+ let tmpMatch = tmpType.match(/\((\d+(?:,\d+)?)\)/);
484
+ return { DataType: 'Decimal', Size: tmpMatch ? tmpMatch[1] : '' };
485
+ }
486
+
487
+ if (tmpType === 'TEXT')
488
+ {
489
+ // Distinguish between String and Text: if notnull with default '' → String, else Text
490
+ if (pColumnInfo.notnull === 1 && pColumnInfo.dflt_value === "''")
491
+ {
492
+ return { DataType: 'String', Size: '' };
493
+ }
494
+ return { DataType: 'Text', Size: '' };
495
+ }
496
+
497
+ if (tmpType.indexOf('VARCHAR') >= 0 || tmpType.indexOf('CHAR') >= 0)
498
+ {
499
+ let tmpMatch = tmpType.match(/\((\d+)\)/);
500
+ return { DataType: 'String', Size: tmpMatch ? tmpMatch[1] : '' };
501
+ }
502
+
503
+ if (tmpType === 'INTEGER' || tmpType === 'INT' || tmpType.indexOf('INT') >= 0)
504
+ {
505
+ // Could be Boolean or Numeric; check for boolean hints
506
+ if (pColumnInfo.notnull === 1 && pColumnInfo.dflt_value === '0')
507
+ {
508
+ // Check for boolean naming patterns
509
+ let tmpLowerName = tmpName.toLowerCase();
510
+ if (tmpLowerName.indexOf('is') === 0 || tmpLowerName.indexOf('has') === 0 ||
511
+ tmpLowerName.indexOf('in') === 0 || tmpLowerName === 'deleted' ||
512
+ tmpLowerName === 'active' || tmpLowerName === 'enabled')
513
+ {
514
+ return { DataType: 'Boolean', Size: '' };
515
+ }
516
+ }
517
+ return { DataType: 'Numeric', Size: '' };
518
+ }
519
+
520
+ // Default fallback
521
+ return { DataType: 'Text', Size: '' };
522
+ }
523
+
524
+ /**
525
+ * Get column definitions for a single table.
526
+ *
527
+ * Returns DDL-level column objects with DataType inferred from native types.
528
+ *
529
+ * @param {string} pTableName - Name of the table
530
+ * @param {Function} fCallback - callback(pError, pColumns)
531
+ */
532
+ introspectTableColumns(pTableName, fCallback)
533
+ {
534
+ if (!this._Database)
535
+ {
536
+ return fCallback(new Error('Not connected to SQLite'));
537
+ }
538
+
539
+ try
540
+ {
541
+ // Get column info
542
+ let tmpColumns = this._Database.prepare(`PRAGMA table_info('${pTableName}')`).all();
543
+
544
+ // Check if the table has AUTOINCREMENT by inspecting sqlite_master
545
+ let tmpCreateSQL = this._Database.prepare(
546
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?"
547
+ ).get(pTableName);
548
+ let tmpHasAutoIncrement = tmpCreateSQL && tmpCreateSQL.sql &&
549
+ tmpCreateSQL.sql.toUpperCase().indexOf('AUTOINCREMENT') >= 0;
550
+
551
+ // Get foreign keys to identify FK columns
552
+ let tmpForeignKeys = this._Database.prepare(`PRAGMA foreign_key_list('${pTableName}')`).all();
553
+ let tmpFKColumnSet = new Set(tmpForeignKeys.map((pFK) => { return pFK.from; }));
554
+
555
+ let tmpResult = [];
556
+ for (let i = 0; i < tmpColumns.length; i++)
557
+ {
558
+ let tmpCol = tmpColumns[i];
559
+ let tmpIsAutoIncrement = tmpHasAutoIncrement && tmpCol.pk === 1;
560
+ let tmpTypeInfo = this._mapSQLiteTypeToMeadow(tmpCol, tmpIsAutoIncrement, tmpFKColumnSet);
561
+
562
+ let tmpColumnDef = {
563
+ Column: tmpCol.name,
564
+ DataType: tmpTypeInfo.DataType
565
+ };
566
+
567
+ if (tmpTypeInfo.Size)
568
+ {
569
+ tmpColumnDef.Size = tmpTypeInfo.Size;
570
+ }
571
+
572
+ tmpResult.push(tmpColumnDef);
573
+ }
574
+
575
+ return fCallback(null, tmpResult);
576
+ }
577
+ catch (pError)
578
+ {
579
+ this.log.error(`Meadow-SQLite introspectTableColumns for ${pTableName} failed!`, pError);
580
+ return fCallback(pError);
581
+ }
582
+ }
583
+
584
+ /**
585
+ * Get raw index definitions for a single table from the database.
586
+ *
587
+ * Returns each index as: { Name, Columns[], Unique }
588
+ *
589
+ * @param {string} pTableName - Name of the table
590
+ * @param {Function} fCallback - callback(pError, pIndices)
591
+ */
592
+ introspectTableIndices(pTableName, fCallback)
593
+ {
594
+ if (!this._Database)
595
+ {
596
+ return fCallback(new Error('Not connected to SQLite'));
597
+ }
598
+
599
+ try
600
+ {
601
+ let tmpIndexList = this._Database.prepare(`PRAGMA index_list('${pTableName}')`).all();
602
+ let tmpIndices = [];
603
+
604
+ for (let i = 0; i < tmpIndexList.length; i++)
605
+ {
606
+ let tmpIdx = tmpIndexList[i];
607
+
608
+ // Skip auto-generated indices (origin 'pk' for primary key)
609
+ if (tmpIdx.origin === 'pk')
610
+ {
611
+ continue;
612
+ }
613
+
614
+ let tmpIndexInfo = this._Database.prepare(`PRAGMA index_info('${tmpIdx.name}')`).all();
615
+ let tmpColumnNames = tmpIndexInfo.map((pInfo) => { return pInfo.name; });
616
+
617
+ tmpIndices.push(
618
+ {
619
+ Name: tmpIdx.name,
620
+ Columns: tmpColumnNames,
621
+ Unique: tmpIdx.unique === 1
622
+ });
623
+ }
624
+
625
+ return fCallback(null, tmpIndices);
626
+ }
627
+ catch (pError)
628
+ {
629
+ this.log.error(`Meadow-SQLite introspectTableIndices for ${pTableName} failed!`, pError);
630
+ return fCallback(pError);
631
+ }
632
+ }
633
+
634
+ /**
635
+ * Get foreign key relationships for a single table.
636
+ *
637
+ * @param {string} pTableName - Name of the table
638
+ * @param {Function} fCallback - callback(pError, pForeignKeys)
639
+ */
640
+ introspectTableForeignKeys(pTableName, fCallback)
641
+ {
642
+ if (!this._Database)
643
+ {
644
+ return fCallback(new Error('Not connected to SQLite'));
645
+ }
646
+
647
+ try
648
+ {
649
+ let tmpForeignKeys = this._Database.prepare(`PRAGMA foreign_key_list('${pTableName}')`).all();
650
+ let tmpResult = [];
651
+
652
+ for (let i = 0; i < tmpForeignKeys.length; i++)
653
+ {
654
+ let tmpFK = tmpForeignKeys[i];
655
+ tmpResult.push(
656
+ {
657
+ Column: tmpFK.from,
658
+ ReferencedTable: tmpFK.table,
659
+ ReferencedColumn: tmpFK.to
660
+ });
661
+ }
662
+
663
+ return fCallback(null, tmpResult);
664
+ }
665
+ catch (pError)
666
+ {
667
+ this.log.error(`Meadow-SQLite introspectTableForeignKeys for ${pTableName} failed!`, pError);
668
+ return fCallback(pError);
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Classify an index for round-trip fidelity.
674
+ *
675
+ * Determines how a database index should be represented in the Meadow
676
+ * schema: as a column-level Indexed property (with or without IndexName),
677
+ * as a GUID/FK auto-index (skip), or as an explicit Indices[] entry.
678
+ *
679
+ * @param {object} pIndex - { Name, Columns[], Unique }
680
+ * @param {string} pTableName - Table name for pattern matching
681
+ * @returns {object} { type, column, indexed, indexName }
682
+ * type: 'column-auto' | 'column-named' | 'guid-auto' | 'fk-auto' | 'explicit'
683
+ */
684
+ _classifyIndex(pIndex, pTableName)
685
+ {
686
+ // Multi-column indices always go in Indices[]
687
+ if (pIndex.Columns.length !== 1)
688
+ {
689
+ return { type: 'explicit' };
690
+ }
691
+
692
+ let tmpColumn = pIndex.Columns[0];
693
+ let tmpName = pIndex.Name;
694
+
695
+ // Check for auto-detected GUID index: AK_M_{Column}
696
+ if (tmpName === `AK_M_${tmpColumn}`)
697
+ {
698
+ return { type: 'guid-auto', column: tmpColumn };
699
+ }
700
+
701
+ // Check for auto-detected FK index: IX_M_{Column}
702
+ if (tmpName === `IX_M_${tmpColumn}`)
703
+ {
704
+ return { type: 'fk-auto', column: tmpColumn };
705
+ }
706
+
707
+ // Check for auto-generated column-level index: IX_M_T_{Table}_C_{Column}
708
+ let tmpRegularAutoName = `IX_M_T_${pTableName}_C_${tmpColumn}`;
709
+ if (tmpName === tmpRegularAutoName && !pIndex.Unique)
710
+ {
711
+ return { type: 'column-auto', column: tmpColumn, indexed: true };
712
+ }
713
+
714
+ // Check for auto-generated unique column-level index: AK_M_T_{Table}_C_{Column}
715
+ let tmpUniqueAutoName = `AK_M_T_${pTableName}_C_${tmpColumn}`;
716
+ if (tmpName === tmpUniqueAutoName && pIndex.Unique)
717
+ {
718
+ return { type: 'column-auto', column: tmpColumn, indexed: 'unique' };
719
+ }
720
+
721
+ // Any other single-column index → column-level with IndexName
722
+ return {
723
+ type: 'column-named',
724
+ column: tmpColumn,
725
+ indexed: pIndex.Unique ? 'unique' : true,
726
+ indexName: tmpName
727
+ };
728
+ }
729
+
730
+ /**
731
+ * Generate a complete DDL-level schema for a single table.
732
+ *
733
+ * Combines introspected columns + indices + foreign keys.
734
+ * Single-column indices are folded into column Indexed/IndexName properties.
735
+ * Multi-column indices go in the Indices[] array.
736
+ *
737
+ * @param {string} pTableName - Name of the table
738
+ * @param {Function} fCallback - callback(pError, pTableSchema)
739
+ */
740
+ introspectTableSchema(pTableName, fCallback)
741
+ {
742
+ this.introspectTableColumns(pTableName,
743
+ (pColumnError, pColumns) =>
744
+ {
745
+ if (pColumnError)
746
+ {
747
+ return fCallback(pColumnError);
748
+ }
749
+
750
+ this.introspectTableIndices(pTableName,
751
+ (pIndexError, pIndices) =>
752
+ {
753
+ if (pIndexError)
754
+ {
755
+ return fCallback(pIndexError);
756
+ }
757
+
758
+ this.introspectTableForeignKeys(pTableName,
759
+ (pFKError, pForeignKeys) =>
760
+ {
761
+ if (pFKError)
762
+ {
763
+ return fCallback(pFKError);
764
+ }
765
+
766
+ // Build a column lookup for folding index info
767
+ let tmpColumnMap = {};
768
+ for (let i = 0; i < pColumns.length; i++)
769
+ {
770
+ tmpColumnMap[pColumns[i].Column] = pColumns[i];
771
+ }
772
+
773
+ let tmpExplicitIndices = [];
774
+
775
+ // Classify and fold each index
776
+ for (let i = 0; i < pIndices.length; i++)
777
+ {
778
+ let tmpClassification = this._classifyIndex(pIndices[i], pTableName);
779
+
780
+ switch (tmpClassification.type)
781
+ {
782
+ case 'column-auto':
783
+ if (tmpColumnMap[tmpClassification.column])
784
+ {
785
+ tmpColumnMap[tmpClassification.column].Indexed = tmpClassification.indexed;
786
+ }
787
+ break;
788
+ case 'column-named':
789
+ if (tmpColumnMap[tmpClassification.column])
790
+ {
791
+ tmpColumnMap[tmpClassification.column].Indexed = tmpClassification.indexed;
792
+ tmpColumnMap[tmpClassification.column].IndexName = tmpClassification.indexName;
793
+ }
794
+ break;
795
+ case 'guid-auto':
796
+ // If the column wasn't detected as GUID,
797
+ // upgrade it based on AK_M_{Column} naming evidence.
798
+ if (tmpColumnMap[tmpClassification.column] &&
799
+ tmpColumnMap[tmpClassification.column].DataType !== 'GUID')
800
+ {
801
+ tmpColumnMap[tmpClassification.column].DataType = 'GUID';
802
+ }
803
+ break;
804
+ case 'fk-auto':
805
+ // If the column wasn't detected as ForeignKey
806
+ // (e.g. no REFERENCES clause in SQLite), upgrade it
807
+ // based on IX_M_{Column} naming pattern evidence.
808
+ if (tmpColumnMap[tmpClassification.column] &&
809
+ tmpColumnMap[tmpClassification.column].DataType !== 'ForeignKey')
810
+ {
811
+ tmpColumnMap[tmpClassification.column].DataType = 'ForeignKey';
812
+ }
813
+ // Skip — handled by DataType
814
+ break;
815
+ case 'explicit':
816
+ tmpExplicitIndices.push(
817
+ {
818
+ Name: pIndices[i].Name,
819
+ Columns: pIndices[i].Columns,
820
+ Unique: pIndices[i].Unique
821
+ });
822
+ break;
823
+ }
824
+ }
825
+
826
+ let tmpSchema = {
827
+ TableName: pTableName,
828
+ Columns: pColumns
829
+ };
830
+
831
+ if (tmpExplicitIndices.length > 0)
832
+ {
833
+ tmpSchema.Indices = tmpExplicitIndices;
834
+ }
835
+
836
+ if (pForeignKeys.length > 0)
837
+ {
838
+ tmpSchema.ForeignKeys = pForeignKeys;
839
+ }
840
+
841
+ return fCallback(null, tmpSchema);
842
+ });
843
+ });
844
+ });
845
+ }
846
+
847
+ /**
848
+ * Generate DDL schemas for ALL tables in the database.
849
+ *
850
+ * @param {Function} fCallback - callback(pError, pSchema)
851
+ */
852
+ introspectDatabaseSchema(fCallback)
853
+ {
854
+ this.listTables(
855
+ (pListError, pTableNames) =>
856
+ {
857
+ if (pListError)
858
+ {
859
+ return fCallback(pListError);
860
+ }
861
+
862
+ let tmpTables = [];
863
+ this.fable.Utility.eachLimit(pTableNames, 1,
864
+ (pTableName, fNext) =>
865
+ {
866
+ this.introspectTableSchema(pTableName,
867
+ (pTableError, pTableSchema) =>
868
+ {
869
+ if (pTableError)
870
+ {
871
+ return fNext(pTableError);
872
+ }
873
+ tmpTables.push(pTableSchema);
874
+ return fNext();
875
+ });
876
+ },
877
+ (pError) =>
878
+ {
879
+ if (pError)
880
+ {
881
+ this.log.error(`Meadow-SQLite introspectDatabaseSchema failed: ${pError}`, pError);
882
+ return fCallback(pError);
883
+ }
884
+ return fCallback(null, { Tables: tmpTables });
885
+ });
886
+ });
887
+ }
888
+
889
+ /**
890
+ * Map a Meadow DataType to a Meadow Package JSON Type.
891
+ *
892
+ * @param {string} pDataType - Meadow DDL-level DataType
893
+ * @param {string} pColumnName - Column name (for magic column detection)
894
+ * @returns {string} Meadow Package Type
895
+ */
896
+ _mapDataTypeToMeadowType(pDataType, pColumnName)
897
+ {
898
+ // Magic column detection
899
+ let tmpLowerName = pColumnName.toLowerCase();
900
+ switch (tmpLowerName)
901
+ {
902
+ case 'createdate':
903
+ return 'CreateDate';
904
+ case 'creatingiduser':
905
+ return 'CreateIDUser';
906
+ case 'updatedate':
907
+ return 'UpdateDate';
908
+ case 'updatingiduser':
909
+ return 'UpdateIDUser';
910
+ case 'deletedate':
911
+ return 'DeleteDate';
912
+ case 'deletingiduser':
913
+ return 'DeleteIDUser';
914
+ case 'deleted':
915
+ return 'Deleted';
916
+ }
917
+
918
+ switch (pDataType)
919
+ {
920
+ case 'ID':
921
+ return 'AutoIdentity';
922
+ case 'GUID':
923
+ return 'AutoGUID';
924
+ case 'ForeignKey':
925
+ return 'Numeric';
926
+ case 'Numeric':
927
+ return 'Numeric';
928
+ case 'Decimal':
929
+ return 'Numeric';
930
+ case 'String':
931
+ return 'String';
932
+ case 'Text':
933
+ return 'String';
934
+ case 'DateTime':
935
+ return 'DateTime';
936
+ case 'Boolean':
937
+ return 'Boolean';
938
+ default:
939
+ return 'String';
940
+ }
941
+ }
942
+
943
+ /**
944
+ * Get a sensible default value for a Meadow DataType.
945
+ *
946
+ * @param {string} pDataType - Meadow DDL-level DataType
947
+ * @returns {*} Default value
948
+ */
949
+ _getDefaultValue(pDataType)
950
+ {
951
+ switch (pDataType)
952
+ {
953
+ case 'ID':
954
+ return 0;
955
+ case 'GUID':
956
+ return '';
957
+ case 'ForeignKey':
958
+ return 0;
959
+ case 'Numeric':
960
+ return 0;
961
+ case 'Decimal':
962
+ return 0.0;
963
+ case 'String':
964
+ return '';
965
+ case 'Text':
966
+ return '';
967
+ case 'DateTime':
968
+ return '';
969
+ case 'Boolean':
970
+ return false;
971
+ default:
972
+ return '';
973
+ }
974
+ }
975
+
976
+ /**
977
+ * Generate a Meadow package JSON for a single table.
978
+ *
979
+ * Produces the format consumed by Meadow core and FoxHound:
980
+ * { Scope, DefaultIdentifier, Schema[], DefaultObject, JsonSchema }
981
+ *
982
+ * @param {string} pTableName - Name of the table
983
+ * @param {Function} fCallback - callback(pError, pPackage)
984
+ */
985
+ generateMeadowPackageFromTable(pTableName, fCallback)
986
+ {
987
+ this.introspectTableSchema(pTableName,
988
+ (pError, pTableSchema) =>
989
+ {
990
+ if (pError)
991
+ {
992
+ return fCallback(pError);
993
+ }
994
+
995
+ let tmpScope = pTableName;
996
+ let tmpDefaultIdentifier = '';
997
+ let tmpSchema = [];
998
+ let tmpDefaultObject = {};
999
+
1000
+ for (let i = 0; i < pTableSchema.Columns.length; i++)
1001
+ {
1002
+ let tmpCol = pTableSchema.Columns[i];
1003
+ let tmpMeadowType = this._mapDataTypeToMeadowType(tmpCol.DataType, tmpCol.Column);
1004
+
1005
+ if (tmpCol.DataType === 'ID')
1006
+ {
1007
+ tmpDefaultIdentifier = tmpCol.Column;
1008
+ }
1009
+
1010
+ let tmpSchemaEntry = {
1011
+ Column: tmpCol.Column,
1012
+ Type: tmpMeadowType
1013
+ };
1014
+
1015
+ if (tmpCol.Size)
1016
+ {
1017
+ tmpSchemaEntry.Size = tmpCol.Size;
1018
+ }
1019
+
1020
+ tmpSchema.push(tmpSchemaEntry);
1021
+ tmpDefaultObject[tmpCol.Column] = this._getDefaultValue(tmpCol.DataType);
1022
+ }
1023
+
1024
+ let tmpPackage = {
1025
+ Scope: tmpScope,
1026
+ DefaultIdentifier: tmpDefaultIdentifier,
1027
+ Schema: tmpSchema,
1028
+ DefaultObject: tmpDefaultObject
1029
+ };
1030
+
1031
+ return fCallback(null, tmpPackage);
1032
+ });
1033
+ }
1034
+ }
1035
+
1036
+ module.exports = MeadowSchemaSQLite;