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.
- package/README.md +14 -8
- package/docs/README.md +9 -0
- package/docs/_cover.md +6 -2
- package/docs/_sidebar.md +19 -2
- package/docs/_topbar.md +4 -1
- package/docs/api/SQLite.md +55 -0
- package/docs/api/connect.md +66 -0
- package/docs/api/connectAsync.md +121 -0
- package/docs/api/createTable.md +96 -0
- package/docs/api/createTables.md +91 -0
- package/docs/api/db.md +145 -0
- package/docs/api/generateCreateTableStatement.md +109 -0
- package/docs/api/generateDropTableStatement.md +74 -0
- package/docs/api/preparedStatement.md +67 -0
- package/docs/api/reference.md +192 -0
- package/docs/api.md +13 -252
- package/docs/architecture.md +250 -0
- package/docs/quickstart.md +197 -0
- package/docs/retold-catalog.json +22 -1
- package/docs/retold-keyword-index.json +5818 -5
- package/docs/schema.md +198 -0
- package/package.json +3 -3
- package/source/Meadow-Connection-SQLite.js +79 -79
- package/source/Meadow-Schema-SQLite.js +1036 -0
- package/test/SQLite_tests.js +992 -0
|
@@ -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;
|