meadow-connection-postgresql 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -0
- package/docker-compose.yml +20 -0
- package/docs/.nojekyll +0 -0
- package/docs/README.md +96 -0
- package/docs/_cover.md +17 -0
- package/docs/_sidebar.md +33 -0
- package/docs/_topbar.md +5 -0
- package/docs/api/connect.md +100 -0
- package/docs/api/connectAsync.md +92 -0
- package/docs/api/createTable.md +116 -0
- package/docs/api/createTables.md +128 -0
- package/docs/api/generateCreateTableStatement.md +136 -0
- package/docs/api/generateDropTableStatement.md +71 -0
- package/docs/api/pool.md +171 -0
- package/docs/api/reference.md +112 -0
- package/docs/api.md +3 -0
- package/docs/architecture.md +168 -0
- package/docs/css/docuserve.css +73 -0
- package/docs/index.html +39 -0
- package/docs/quickstart.md +181 -0
- package/docs/retold-catalog.json +62 -0
- package/docs/retold-keyword-index.json +4964 -0
- package/docs/schema.md +148 -0
- package/package.json +5 -2
- package/source/Meadow-Connection-PostgreSQL.js +79 -99
- package/source/Meadow-Schema-PostgreSQL.js +1048 -0
- package/start-postgresql.sh +21 -0
- package/stop-postgresql.sh +9 -0
- package/test/PostgreSQL_tests.js +865 -1
- package/test/docker-init/01-chinook-schema.sql +177 -0
|
@@ -0,0 +1,1048 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meadow PostgreSQL Schema Provider
|
|
3
|
+
*
|
|
4
|
+
* Handles table creation, dropping, and DDL generation for PostgreSQL.
|
|
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 MeadowSchemaPostgreSQL extends libFableServiceProviderBase
|
|
13
|
+
{
|
|
14
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
15
|
+
{
|
|
16
|
+
super(pFable, pOptions, pServiceHash);
|
|
17
|
+
|
|
18
|
+
this.serviceType = 'MeadowSchemaPostgreSQL';
|
|
19
|
+
|
|
20
|
+
// Reference to the connection pool, set by the connection provider
|
|
21
|
+
this._ConnectionPool = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Set the connection pool reference for executing DDL statements.
|
|
26
|
+
* @param {object} pConnectionPool - PostgreSQL connection pool
|
|
27
|
+
* @returns {MeadowSchemaPostgreSQL} this (for chaining)
|
|
28
|
+
*/
|
|
29
|
+
setConnectionPool(pConnectionPool)
|
|
30
|
+
{
|
|
31
|
+
this._ConnectionPool = pConnectionPool;
|
|
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} ...`);
|
|
43
|
+
|
|
44
|
+
let tmpPrimaryKey = false;
|
|
45
|
+
let tmpCreateTableStatement = `-- [ ${pMeadowTableSchema.TableName} ]`;
|
|
46
|
+
|
|
47
|
+
tmpCreateTableStatement += `\nCREATE TABLE IF NOT EXISTS\n "${pMeadowTableSchema.TableName}"\n (`;
|
|
48
|
+
for (let j = 0; j < pMeadowTableSchema.Columns.length; j++)
|
|
49
|
+
{
|
|
50
|
+
let tmpColumn = pMeadowTableSchema.Columns[j];
|
|
51
|
+
|
|
52
|
+
// If we aren't the first column, append a comma.
|
|
53
|
+
if (j > 0)
|
|
54
|
+
{
|
|
55
|
+
tmpCreateTableStatement += `,`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
tmpCreateTableStatement += `\n`;
|
|
59
|
+
// Dump out each column......
|
|
60
|
+
switch (tmpColumn.DataType)
|
|
61
|
+
{
|
|
62
|
+
case 'ID':
|
|
63
|
+
tmpCreateTableStatement += ` "${tmpColumn.Column}" SERIAL PRIMARY KEY`;
|
|
64
|
+
tmpPrimaryKey = tmpColumn.Column;
|
|
65
|
+
break;
|
|
66
|
+
case 'GUID':
|
|
67
|
+
let tmpSize = tmpColumn.hasOwnProperty('Size') ? tmpColumn.Size : 36;
|
|
68
|
+
if (isNaN(tmpSize))
|
|
69
|
+
{
|
|
70
|
+
// Use the old default if Size is improper
|
|
71
|
+
tmpSize = 36;
|
|
72
|
+
}
|
|
73
|
+
tmpCreateTableStatement += ` "${tmpColumn.Column}" VARCHAR(${tmpSize}) DEFAULT '0xDe'`;
|
|
74
|
+
break;
|
|
75
|
+
case 'ForeignKey':
|
|
76
|
+
tmpCreateTableStatement += ` "${tmpColumn.Column}" INTEGER NOT NULL DEFAULT 0`;
|
|
77
|
+
break;
|
|
78
|
+
case 'Numeric':
|
|
79
|
+
tmpCreateTableStatement += ` "${tmpColumn.Column}" INTEGER NOT NULL DEFAULT 0`;
|
|
80
|
+
break;
|
|
81
|
+
case 'Decimal':
|
|
82
|
+
tmpCreateTableStatement += ` "${tmpColumn.Column}" DECIMAL(${tmpColumn.Size})`;
|
|
83
|
+
break;
|
|
84
|
+
case 'String':
|
|
85
|
+
tmpCreateTableStatement += ` "${tmpColumn.Column}" VARCHAR(${tmpColumn.Size}) NOT NULL DEFAULT ''`;
|
|
86
|
+
break;
|
|
87
|
+
case 'Text':
|
|
88
|
+
tmpCreateTableStatement += ` "${tmpColumn.Column}" TEXT`;
|
|
89
|
+
break;
|
|
90
|
+
case 'DateTime':
|
|
91
|
+
tmpCreateTableStatement += ` "${tmpColumn.Column}" TIMESTAMP`;
|
|
92
|
+
break;
|
|
93
|
+
case 'Boolean':
|
|
94
|
+
tmpCreateTableStatement += ` "${tmpColumn.Column}" BOOLEAN NOT NULL DEFAULT false`;
|
|
95
|
+
break;
|
|
96
|
+
case 'JSON':
|
|
97
|
+
tmpCreateTableStatement += ` "${tmpColumn.Column}" TEXT`;
|
|
98
|
+
break;
|
|
99
|
+
case 'JSONProxy':
|
|
100
|
+
tmpCreateTableStatement += ` "${tmpColumn.StorageColumn}" TEXT`;
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// PostgreSQL SERIAL PRIMARY KEY is declared inline; no separate PRIMARY KEY clause needed
|
|
107
|
+
tmpCreateTableStatement += `\n );`;
|
|
108
|
+
|
|
109
|
+
return tmpCreateTableStatement;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
createTables(pMeadowSchema, fCallback)
|
|
113
|
+
{
|
|
114
|
+
this.fable.Utility.eachLimit(pMeadowSchema.Tables, 1,
|
|
115
|
+
(pTable, fCreateComplete) =>
|
|
116
|
+
{
|
|
117
|
+
return this.createTable(pTable, fCreateComplete);
|
|
118
|
+
},
|
|
119
|
+
(pCreateError) =>
|
|
120
|
+
{
|
|
121
|
+
if (pCreateError)
|
|
122
|
+
{
|
|
123
|
+
this.log.error(`Meadow-PostgreSQL Error creating tables from Schema: ${pCreateError}`, pCreateError);
|
|
124
|
+
}
|
|
125
|
+
this.log.info('Done creating tables!');
|
|
126
|
+
return fCallback(pCreateError);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
createTable(pMeadowTableSchema, fCallback)
|
|
131
|
+
{
|
|
132
|
+
let tmpCreateTableStatement = this.generateCreateTableStatement(pMeadowTableSchema);
|
|
133
|
+
this._ConnectionPool.query(tmpCreateTableStatement,
|
|
134
|
+
(pError, pResult) =>
|
|
135
|
+
{
|
|
136
|
+
if (pError)
|
|
137
|
+
{
|
|
138
|
+
// Check for "already exists" type errors
|
|
139
|
+
if (pError.code === '42P07')
|
|
140
|
+
{
|
|
141
|
+
// 42P07 = duplicate_table
|
|
142
|
+
this.log.warn(`Meadow-PostgreSQL CREATE TABLE ${pMeadowTableSchema.TableName} executed but table already existed.`);
|
|
143
|
+
return fCallback();
|
|
144
|
+
}
|
|
145
|
+
else
|
|
146
|
+
{
|
|
147
|
+
this.log.error(`Meadow-PostgreSQL CREATE TABLE ${pMeadowTableSchema.TableName} failed!`, pError);
|
|
148
|
+
return fCallback(pError);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else
|
|
152
|
+
{
|
|
153
|
+
this.log.info(`Meadow-PostgreSQL CREATE TABLE ${pMeadowTableSchema.TableName} executed successfully.`);
|
|
154
|
+
return fCallback();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ========================================================================
|
|
160
|
+
// Index Generation
|
|
161
|
+
// ========================================================================
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Derive index definitions from a Meadow table schema.
|
|
165
|
+
*
|
|
166
|
+
* Automatically generates indices for:
|
|
167
|
+
* - GUID columns -> unique index AK_M_{Column}
|
|
168
|
+
* - ForeignKey columns -> regular index IX_M_{Column}
|
|
169
|
+
*
|
|
170
|
+
* Column-level Indexed property:
|
|
171
|
+
* - Indexed: true -> regular index IX_M_T_{Table}_C_{Column}
|
|
172
|
+
* - Indexed: 'unique' -> unique index AK_M_T_{Table}_C_{Column}
|
|
173
|
+
* - IndexName overrides the auto-generated name (for round-trip fidelity)
|
|
174
|
+
*
|
|
175
|
+
* Also includes any explicit entries from pMeadowTableSchema.Indices[]
|
|
176
|
+
* (for multi-column composite indices).
|
|
177
|
+
*
|
|
178
|
+
* Each index definition is:
|
|
179
|
+
* { Name, TableName, Columns[], Unique, Strategy }
|
|
180
|
+
*
|
|
181
|
+
* @param {object} pMeadowTableSchema - Meadow table schema object
|
|
182
|
+
* @returns {Array} Array of index definition objects
|
|
183
|
+
*/
|
|
184
|
+
getIndexDefinitionsFromSchema(pMeadowTableSchema)
|
|
185
|
+
{
|
|
186
|
+
let tmpIndices = [];
|
|
187
|
+
let tmpTableName = pMeadowTableSchema.TableName;
|
|
188
|
+
|
|
189
|
+
// Auto-detect from column types
|
|
190
|
+
for (let j = 0; j < pMeadowTableSchema.Columns.length; j++)
|
|
191
|
+
{
|
|
192
|
+
let tmpColumn = pMeadowTableSchema.Columns[j];
|
|
193
|
+
|
|
194
|
+
switch (tmpColumn.DataType)
|
|
195
|
+
{
|
|
196
|
+
case 'GUID':
|
|
197
|
+
tmpIndices.push(
|
|
198
|
+
{
|
|
199
|
+
Name: `AK_M_${tmpColumn.Column}`,
|
|
200
|
+
TableName: tmpTableName,
|
|
201
|
+
Columns: [tmpColumn.Column],
|
|
202
|
+
Unique: true,
|
|
203
|
+
Strategy: ''
|
|
204
|
+
});
|
|
205
|
+
break;
|
|
206
|
+
case 'ForeignKey':
|
|
207
|
+
tmpIndices.push(
|
|
208
|
+
{
|
|
209
|
+
Name: `IX_M_${tmpColumn.Column}`,
|
|
210
|
+
TableName: tmpTableName,
|
|
211
|
+
Columns: [tmpColumn.Column],
|
|
212
|
+
Unique: false,
|
|
213
|
+
Strategy: ''
|
|
214
|
+
});
|
|
215
|
+
break;
|
|
216
|
+
default:
|
|
217
|
+
// Column-level Indexed property: generates a single-column index
|
|
218
|
+
// with a consistent naming convention.
|
|
219
|
+
// Indexed: true -> IX_M_T_{Table}_C_{Column} (regular)
|
|
220
|
+
// Indexed: 'unique' -> AK_M_T_{Table}_C_{Column} (unique)
|
|
221
|
+
// Optional IndexName property overrides the auto-generated name.
|
|
222
|
+
if (tmpColumn.Indexed)
|
|
223
|
+
{
|
|
224
|
+
let tmpIsUnique = (tmpColumn.Indexed === 'unique');
|
|
225
|
+
let tmpPrefix = tmpIsUnique ? 'AK_M_T' : 'IX_M_T';
|
|
226
|
+
let tmpAutoName = `${tmpPrefix}_${tmpTableName}_C_${tmpColumn.Column}`;
|
|
227
|
+
tmpIndices.push(
|
|
228
|
+
{
|
|
229
|
+
Name: tmpColumn.IndexName || tmpAutoName,
|
|
230
|
+
TableName: tmpTableName,
|
|
231
|
+
Columns: [tmpColumn.Column],
|
|
232
|
+
Unique: tmpIsUnique,
|
|
233
|
+
Strategy: ''
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Include any explicitly defined indices on the schema
|
|
241
|
+
if (Array.isArray(pMeadowTableSchema.Indices))
|
|
242
|
+
{
|
|
243
|
+
for (let k = 0; k < pMeadowTableSchema.Indices.length; k++)
|
|
244
|
+
{
|
|
245
|
+
let tmpExplicitIndex = pMeadowTableSchema.Indices[k];
|
|
246
|
+
tmpIndices.push(
|
|
247
|
+
{
|
|
248
|
+
Name: tmpExplicitIndex.Name || `IX_${tmpTableName}_${k}`,
|
|
249
|
+
TableName: tmpTableName,
|
|
250
|
+
Columns: Array.isArray(tmpExplicitIndex.Columns) ? tmpExplicitIndex.Columns : [tmpExplicitIndex.Columns],
|
|
251
|
+
Unique: tmpExplicitIndex.Unique || false,
|
|
252
|
+
Strategy: tmpExplicitIndex.Strategy || ''
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return tmpIndices;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Build the column list for an index, double-quoted and comma-separated.
|
|
262
|
+
* @param {Array} pColumns - Array of column name strings
|
|
263
|
+
* @returns {string}
|
|
264
|
+
*/
|
|
265
|
+
_buildColumnList(pColumns)
|
|
266
|
+
{
|
|
267
|
+
return pColumns.map((pCol) => { return '"' + pCol + '"'; }).join(', ');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Generate a full idempotent SQL script for creating all indices on a table.
|
|
272
|
+
*
|
|
273
|
+
* PostgreSQL supports CREATE INDEX IF NOT EXISTS natively, so the
|
|
274
|
+
* idempotent script is straightforward.
|
|
275
|
+
*
|
|
276
|
+
* @param {object} pMeadowTableSchema - Meadow table schema object
|
|
277
|
+
* @returns {string} Complete SQL script
|
|
278
|
+
*/
|
|
279
|
+
generateCreateIndexScript(pMeadowTableSchema)
|
|
280
|
+
{
|
|
281
|
+
let tmpIndices = this.getIndexDefinitionsFromSchema(pMeadowTableSchema);
|
|
282
|
+
let tmpTableName = pMeadowTableSchema.TableName;
|
|
283
|
+
|
|
284
|
+
if (tmpIndices.length === 0)
|
|
285
|
+
{
|
|
286
|
+
return `-- No indices to create for ${tmpTableName}\n`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let tmpScript = `-- Index Definitions for ${tmpTableName} -- Generated ${new Date().toJSON()}\n\n`;
|
|
290
|
+
|
|
291
|
+
for (let i = 0; i < tmpIndices.length; i++)
|
|
292
|
+
{
|
|
293
|
+
let tmpIndex = tmpIndices[i];
|
|
294
|
+
let tmpColumnList = this._buildColumnList(tmpIndex.Columns);
|
|
295
|
+
let tmpCreateKeyword = tmpIndex.Unique ? 'CREATE UNIQUE INDEX' : 'CREATE INDEX';
|
|
296
|
+
let tmpStrategyClause = tmpIndex.Strategy ? ` USING ${tmpIndex.Strategy}` : '';
|
|
297
|
+
|
|
298
|
+
tmpScript += `-- Index: ${tmpIndex.Name}\n`;
|
|
299
|
+
tmpScript += `${tmpCreateKeyword} IF NOT EXISTS "${tmpIndex.Name}" ON "${tmpIndex.TableName}" (${tmpColumnList})${tmpStrategyClause};\n\n`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return tmpScript;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Generate an array of individual CREATE INDEX SQL statements for a table.
|
|
307
|
+
*
|
|
308
|
+
* Each entry is an object with:
|
|
309
|
+
* { Name, Statement, CheckStatement }
|
|
310
|
+
*
|
|
311
|
+
* - Statement: the raw CREATE [UNIQUE] INDEX ... SQL
|
|
312
|
+
* - CheckStatement: a SELECT against pg_indexes that returns the count
|
|
313
|
+
* of matching indices (0 = does not exist)
|
|
314
|
+
*
|
|
315
|
+
* @param {object} pMeadowTableSchema - Meadow table schema object
|
|
316
|
+
* @returns {Array} Array of { Name, Statement, CheckStatement } objects
|
|
317
|
+
*/
|
|
318
|
+
generateCreateIndexStatements(pMeadowTableSchema)
|
|
319
|
+
{
|
|
320
|
+
let tmpIndices = this.getIndexDefinitionsFromSchema(pMeadowTableSchema);
|
|
321
|
+
let tmpStatements = [];
|
|
322
|
+
|
|
323
|
+
for (let i = 0; i < tmpIndices.length; i++)
|
|
324
|
+
{
|
|
325
|
+
let tmpIndex = tmpIndices[i];
|
|
326
|
+
let tmpColumnList = this._buildColumnList(tmpIndex.Columns);
|
|
327
|
+
let tmpCreateKeyword = tmpIndex.Unique ? 'CREATE UNIQUE INDEX' : 'CREATE INDEX';
|
|
328
|
+
let tmpStrategyClause = tmpIndex.Strategy ? ` USING ${tmpIndex.Strategy}` : '';
|
|
329
|
+
|
|
330
|
+
tmpStatements.push(
|
|
331
|
+
{
|
|
332
|
+
Name: tmpIndex.Name,
|
|
333
|
+
Statement: `${tmpCreateKeyword} "${tmpIndex.Name}" ON "${tmpIndex.TableName}" (${tmpColumnList})${tmpStrategyClause}`,
|
|
334
|
+
CheckStatement: `SELECT COUNT(*) AS "IndexExists" FROM pg_indexes WHERE tablename = '${tmpIndex.TableName}' AND indexname = '${tmpIndex.Name}'`
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return tmpStatements;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Programmatically create a single index on the database.
|
|
343
|
+
*
|
|
344
|
+
* Checks pg_indexes first; only runs CREATE INDEX if the index
|
|
345
|
+
* does not yet exist.
|
|
346
|
+
*
|
|
347
|
+
* @param {object} pIndexStatement - Object from generateCreateIndexStatements()
|
|
348
|
+
* @param {Function} fCallback - callback(pError)
|
|
349
|
+
*/
|
|
350
|
+
createIndex(pIndexStatement, fCallback)
|
|
351
|
+
{
|
|
352
|
+
if (!this._ConnectionPool)
|
|
353
|
+
{
|
|
354
|
+
this.log.error(`Meadow-PostgreSQL CREATE INDEX ${pIndexStatement.Name} failed: not connected.`);
|
|
355
|
+
return fCallback(new Error('Not connected to PostgreSQL'));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// First check if the index already exists
|
|
359
|
+
this._ConnectionPool.query(pIndexStatement.CheckStatement,
|
|
360
|
+
(pCheckError, pCheckResult) =>
|
|
361
|
+
{
|
|
362
|
+
if (pCheckError)
|
|
363
|
+
{
|
|
364
|
+
this.log.error(`Meadow-PostgreSQL CHECK INDEX ${pIndexStatement.Name} failed!`, pCheckError);
|
|
365
|
+
return fCallback(pCheckError);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let tmpExists = pCheckResult && pCheckResult.rows && pCheckResult.rows[0] && parseInt(pCheckResult.rows[0].IndexExists) > 0;
|
|
369
|
+
|
|
370
|
+
if (tmpExists)
|
|
371
|
+
{
|
|
372
|
+
this.log.info(`Meadow-PostgreSQL INDEX ${pIndexStatement.Name} already exists, skipping.`);
|
|
373
|
+
return fCallback();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Index does not exist; create it
|
|
377
|
+
this._ConnectionPool.query(pIndexStatement.Statement,
|
|
378
|
+
(pCreateError) =>
|
|
379
|
+
{
|
|
380
|
+
if (pCreateError)
|
|
381
|
+
{
|
|
382
|
+
this.log.error(`Meadow-PostgreSQL CREATE INDEX ${pIndexStatement.Name} failed!`, pCreateError);
|
|
383
|
+
return fCallback(pCreateError);
|
|
384
|
+
}
|
|
385
|
+
this.log.info(`Meadow-PostgreSQL CREATE INDEX ${pIndexStatement.Name} executed successfully.`);
|
|
386
|
+
return fCallback();
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Programmatically create all indices for a single table.
|
|
393
|
+
*
|
|
394
|
+
* @param {object} pMeadowTableSchema - Meadow table schema object
|
|
395
|
+
* @param {Function} fCallback - callback(pError)
|
|
396
|
+
*/
|
|
397
|
+
createIndices(pMeadowTableSchema, fCallback)
|
|
398
|
+
{
|
|
399
|
+
let tmpStatements = this.generateCreateIndexStatements(pMeadowTableSchema);
|
|
400
|
+
|
|
401
|
+
if (tmpStatements.length === 0)
|
|
402
|
+
{
|
|
403
|
+
this.log.info(`No indices to create for ${pMeadowTableSchema.TableName}.`);
|
|
404
|
+
return fCallback();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
this.fable.Utility.eachLimit(tmpStatements, 1,
|
|
408
|
+
(pStatement, fCreateComplete) =>
|
|
409
|
+
{
|
|
410
|
+
return this.createIndex(pStatement, fCreateComplete);
|
|
411
|
+
},
|
|
412
|
+
(pCreateError) =>
|
|
413
|
+
{
|
|
414
|
+
if (pCreateError)
|
|
415
|
+
{
|
|
416
|
+
this.log.error(`Meadow-PostgreSQL Error creating indices for ${pMeadowTableSchema.TableName}: ${pCreateError}`, pCreateError);
|
|
417
|
+
}
|
|
418
|
+
else
|
|
419
|
+
{
|
|
420
|
+
this.log.info(`Done creating indices for ${pMeadowTableSchema.TableName}!`);
|
|
421
|
+
}
|
|
422
|
+
return fCallback(pCreateError);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Programmatically create all indices for all tables in a schema.
|
|
428
|
+
*
|
|
429
|
+
* @param {object} pMeadowSchema - Meadow schema object with Tables array
|
|
430
|
+
* @param {Function} fCallback - callback(pError)
|
|
431
|
+
*/
|
|
432
|
+
createAllIndices(pMeadowSchema, fCallback)
|
|
433
|
+
{
|
|
434
|
+
this.fable.Utility.eachLimit(pMeadowSchema.Tables, 1,
|
|
435
|
+
(pTable, fCreateComplete) =>
|
|
436
|
+
{
|
|
437
|
+
return this.createIndices(pTable, fCreateComplete);
|
|
438
|
+
},
|
|
439
|
+
(pCreateError) =>
|
|
440
|
+
{
|
|
441
|
+
if (pCreateError)
|
|
442
|
+
{
|
|
443
|
+
this.log.error(`Meadow-PostgreSQL Error creating indices from schema: ${pCreateError}`, pCreateError);
|
|
444
|
+
}
|
|
445
|
+
this.log.info('Done creating all indices!');
|
|
446
|
+
return fCallback(pCreateError);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ========================================================================
|
|
451
|
+
// Database Introspection
|
|
452
|
+
// ========================================================================
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* List all user tables in the connected PostgreSQL database.
|
|
456
|
+
*
|
|
457
|
+
* @param {Function} fCallback - callback(pError, pTableNames)
|
|
458
|
+
*/
|
|
459
|
+
listTables(fCallback)
|
|
460
|
+
{
|
|
461
|
+
if (!this._ConnectionPool)
|
|
462
|
+
{
|
|
463
|
+
return fCallback(new Error('Not connected to PostgreSQL'));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
this._ConnectionPool.query(
|
|
467
|
+
"SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename",
|
|
468
|
+
(pError, pResult) =>
|
|
469
|
+
{
|
|
470
|
+
if (pError)
|
|
471
|
+
{
|
|
472
|
+
this.log.error('Meadow-PostgreSQL listTables failed!', pError);
|
|
473
|
+
return fCallback(pError);
|
|
474
|
+
}
|
|
475
|
+
let tmpNames = pResult.rows.map((pRow) => { return pRow.tablename; });
|
|
476
|
+
return fCallback(null, tmpNames);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Map a PostgreSQL native type to a Meadow DataType.
|
|
482
|
+
*
|
|
483
|
+
* @param {object} pColumnInfo - information_schema.columns row
|
|
484
|
+
* @param {boolean} pIsSerial - Whether this column is SERIAL (auto-increment PK)
|
|
485
|
+
* @param {Set} pForeignKeyColumns - Set of column names that have FK constraints
|
|
486
|
+
* @returns {object} { DataType, Size }
|
|
487
|
+
*/
|
|
488
|
+
_mapPostgreSQLTypeToMeadow(pColumnInfo, pIsSerial, pForeignKeyColumns)
|
|
489
|
+
{
|
|
490
|
+
let tmpName = pColumnInfo.column_name;
|
|
491
|
+
let tmpType = (pColumnInfo.data_type || '').toLowerCase().trim();
|
|
492
|
+
|
|
493
|
+
// Priority 1: SERIAL auto-increment → ID
|
|
494
|
+
if (pIsSerial)
|
|
495
|
+
{
|
|
496
|
+
return { DataType: 'ID', Size: '' };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Priority 2: Column name contains "GUID" and type is character → GUID
|
|
500
|
+
if (tmpName.toUpperCase().indexOf('GUID') >= 0 && (tmpType === 'character varying' || tmpType === 'character' || tmpType === 'text'))
|
|
501
|
+
{
|
|
502
|
+
return { DataType: 'GUID', Size: pColumnInfo.character_maximum_length ? String(pColumnInfo.character_maximum_length) : '' };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Priority 3: Has FK constraint → ForeignKey
|
|
506
|
+
if (pForeignKeyColumns && pForeignKeyColumns.has(tmpName))
|
|
507
|
+
{
|
|
508
|
+
return { DataType: 'ForeignKey', Size: '' };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Priority 4: Native type mapping
|
|
512
|
+
if (tmpType === 'numeric' || tmpType === 'decimal')
|
|
513
|
+
{
|
|
514
|
+
let tmpSize = '';
|
|
515
|
+
if (pColumnInfo.numeric_precision)
|
|
516
|
+
{
|
|
517
|
+
tmpSize = String(pColumnInfo.numeric_precision);
|
|
518
|
+
if (pColumnInfo.numeric_scale && pColumnInfo.numeric_scale > 0)
|
|
519
|
+
{
|
|
520
|
+
tmpSize += ',' + String(pColumnInfo.numeric_scale);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return { DataType: 'Decimal', Size: tmpSize };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (tmpType === 'double precision' || tmpType === 'real')
|
|
527
|
+
{
|
|
528
|
+
return { DataType: 'Decimal', Size: '' };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (tmpType === 'timestamp without time zone' || tmpType === 'timestamp with time zone' || tmpType === 'timestamp')
|
|
532
|
+
{
|
|
533
|
+
return { DataType: 'DateTime', Size: '' };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (tmpType === 'boolean')
|
|
537
|
+
{
|
|
538
|
+
return { DataType: 'Boolean', Size: '' };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (tmpType === 'text')
|
|
542
|
+
{
|
|
543
|
+
// Distinguish between String and Text: if NOT NULL with default '' → String, else Text
|
|
544
|
+
if (pColumnInfo.is_nullable === 'NO' && pColumnInfo.column_default === "''::text")
|
|
545
|
+
{
|
|
546
|
+
return { DataType: 'String', Size: '' };
|
|
547
|
+
}
|
|
548
|
+
return { DataType: 'Text', Size: '' };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (tmpType === 'character varying')
|
|
552
|
+
{
|
|
553
|
+
let tmpSize = pColumnInfo.character_maximum_length ? String(pColumnInfo.character_maximum_length) : '';
|
|
554
|
+
return { DataType: 'String', Size: tmpSize };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (tmpType === 'character')
|
|
558
|
+
{
|
|
559
|
+
let tmpSize = pColumnInfo.character_maximum_length ? String(pColumnInfo.character_maximum_length) : '';
|
|
560
|
+
return { DataType: 'String', Size: tmpSize };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (tmpType === 'integer' || tmpType === 'bigint' || tmpType === 'smallint')
|
|
564
|
+
{
|
|
565
|
+
// Check for boolean naming hints
|
|
566
|
+
let tmpLowerName = tmpName.toLowerCase();
|
|
567
|
+
if (pColumnInfo.is_nullable === 'NO' && pColumnInfo.column_default === '0')
|
|
568
|
+
{
|
|
569
|
+
if (tmpLowerName.indexOf('is') === 0 || tmpLowerName.indexOf('has') === 0 ||
|
|
570
|
+
tmpLowerName.indexOf('in') === 0 || tmpLowerName === 'deleted' ||
|
|
571
|
+
tmpLowerName === 'active' || tmpLowerName === 'enabled')
|
|
572
|
+
{
|
|
573
|
+
return { DataType: 'Boolean', Size: '' };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return { DataType: 'Numeric', Size: '' };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Default fallback
|
|
580
|
+
return { DataType: 'Text', Size: '' };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Get column definitions for a single table.
|
|
585
|
+
*
|
|
586
|
+
* @param {string} pTableName - Name of the table
|
|
587
|
+
* @param {Function} fCallback - callback(pError, pColumns)
|
|
588
|
+
*/
|
|
589
|
+
introspectTableColumns(pTableName, fCallback)
|
|
590
|
+
{
|
|
591
|
+
if (!this._ConnectionPool)
|
|
592
|
+
{
|
|
593
|
+
return fCallback(new Error('Not connected to PostgreSQL'));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
let tmpColumnQuery = `SELECT column_name, data_type, character_maximum_length, numeric_precision, numeric_scale, is_nullable, column_default, is_identity FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 ORDER BY ordinal_position`;
|
|
597
|
+
|
|
598
|
+
let tmpFKQuery = `SELECT kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema WHERE tc.table_name = $1 AND tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public'`;
|
|
599
|
+
|
|
600
|
+
this._ConnectionPool.query(tmpColumnQuery, [pTableName],
|
|
601
|
+
(pError, pColumnResult) =>
|
|
602
|
+
{
|
|
603
|
+
if (pError)
|
|
604
|
+
{
|
|
605
|
+
this.log.error(`Meadow-PostgreSQL introspectTableColumns for ${pTableName} failed!`, pError);
|
|
606
|
+
return fCallback(pError);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
this._ConnectionPool.query(tmpFKQuery, [pTableName],
|
|
610
|
+
(pFKError, pFKResult) =>
|
|
611
|
+
{
|
|
612
|
+
if (pFKError)
|
|
613
|
+
{
|
|
614
|
+
this.log.error(`Meadow-PostgreSQL introspectTableColumns FK query for ${pTableName} failed!`, pFKError);
|
|
615
|
+
return fCallback(pFKError);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
let tmpFKColumnSet = new Set(pFKResult.rows.map((pRow) => { return pRow.column_name; }));
|
|
619
|
+
|
|
620
|
+
let tmpResult = [];
|
|
621
|
+
for (let i = 0; i < pColumnResult.rows.length; i++)
|
|
622
|
+
{
|
|
623
|
+
let tmpCol = pColumnResult.rows[i];
|
|
624
|
+
// Detect SERIAL/IDENTITY: integer with nextval() default, or GENERATED AS IDENTITY
|
|
625
|
+
let tmpIsSerial = (tmpCol.data_type === 'integer' && tmpCol.column_default && tmpCol.column_default.indexOf('nextval') >= 0) || (tmpCol.is_identity === 'YES');
|
|
626
|
+
let tmpTypeInfo = this._mapPostgreSQLTypeToMeadow(tmpCol, tmpIsSerial, tmpFKColumnSet);
|
|
627
|
+
|
|
628
|
+
let tmpColumnDef = {
|
|
629
|
+
Column: tmpCol.column_name,
|
|
630
|
+
DataType: tmpTypeInfo.DataType
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
if (tmpTypeInfo.Size)
|
|
634
|
+
{
|
|
635
|
+
tmpColumnDef.Size = tmpTypeInfo.Size;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
tmpResult.push(tmpColumnDef);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return fCallback(null, tmpResult);
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Get raw index definitions for a single table from the database.
|
|
648
|
+
*
|
|
649
|
+
* @param {string} pTableName - Name of the table
|
|
650
|
+
* @param {Function} fCallback - callback(pError, pIndices)
|
|
651
|
+
*/
|
|
652
|
+
introspectTableIndices(pTableName, fCallback)
|
|
653
|
+
{
|
|
654
|
+
if (!this._ConnectionPool)
|
|
655
|
+
{
|
|
656
|
+
return fCallback(new Error('Not connected to PostgreSQL'));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
let tmpQuery = `SELECT i.relname AS index_name, a.attname AS column_name, ix.indisunique, ix.indisprimary FROM pg_class t JOIN pg_index ix ON t.oid = ix.indrelid JOIN pg_class i ON i.oid = ix.indexrelid JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) WHERE t.relname = $1 AND t.relkind = 'r' ORDER BY i.relname, array_position(ix.indkey, a.attnum)`;
|
|
660
|
+
|
|
661
|
+
this._ConnectionPool.query(tmpQuery, [pTableName],
|
|
662
|
+
(pError, pResult) =>
|
|
663
|
+
{
|
|
664
|
+
if (pError)
|
|
665
|
+
{
|
|
666
|
+
this.log.error(`Meadow-PostgreSQL introspectTableIndices for ${pTableName} failed!`, pError);
|
|
667
|
+
return fCallback(pError);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Group by index name, skip primary key indices
|
|
671
|
+
let tmpIndexMap = {};
|
|
672
|
+
for (let i = 0; i < pResult.rows.length; i++)
|
|
673
|
+
{
|
|
674
|
+
let tmpRow = pResult.rows[i];
|
|
675
|
+
if (tmpRow.indisprimary)
|
|
676
|
+
{
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (!tmpIndexMap[tmpRow.index_name])
|
|
681
|
+
{
|
|
682
|
+
tmpIndexMap[tmpRow.index_name] = {
|
|
683
|
+
Name: tmpRow.index_name,
|
|
684
|
+
Columns: [],
|
|
685
|
+
Unique: tmpRow.indisunique
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
tmpIndexMap[tmpRow.index_name].Columns.push(tmpRow.column_name);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
let tmpIndices = Object.values(tmpIndexMap);
|
|
692
|
+
return fCallback(null, tmpIndices);
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Get foreign key relationships for a single table.
|
|
698
|
+
*
|
|
699
|
+
* @param {string} pTableName - Name of the table
|
|
700
|
+
* @param {Function} fCallback - callback(pError, pForeignKeys)
|
|
701
|
+
*/
|
|
702
|
+
introspectTableForeignKeys(pTableName, fCallback)
|
|
703
|
+
{
|
|
704
|
+
if (!this._ConnectionPool)
|
|
705
|
+
{
|
|
706
|
+
return fCallback(new Error('Not connected to PostgreSQL'));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
let tmpQuery = `SELECT kcu.column_name, ccu.table_name AS referenced_table, ccu.column_name AS referenced_column FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema WHERE tc.table_name = $1 AND tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public'`;
|
|
710
|
+
|
|
711
|
+
this._ConnectionPool.query(tmpQuery, [pTableName],
|
|
712
|
+
(pError, pResult) =>
|
|
713
|
+
{
|
|
714
|
+
if (pError)
|
|
715
|
+
{
|
|
716
|
+
this.log.error(`Meadow-PostgreSQL introspectTableForeignKeys for ${pTableName} failed!`, pError);
|
|
717
|
+
return fCallback(pError);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
let tmpResult = [];
|
|
721
|
+
for (let i = 0; i < pResult.rows.length; i++)
|
|
722
|
+
{
|
|
723
|
+
let tmpRow = pResult.rows[i];
|
|
724
|
+
tmpResult.push(
|
|
725
|
+
{
|
|
726
|
+
Column: tmpRow.column_name,
|
|
727
|
+
ReferencedTable: tmpRow.referenced_table,
|
|
728
|
+
ReferencedColumn: tmpRow.referenced_column
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return fCallback(null, tmpResult);
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Classify an index for round-trip fidelity.
|
|
738
|
+
*
|
|
739
|
+
* @param {object} pIndex - { Name, Columns[], Unique }
|
|
740
|
+
* @param {string} pTableName - Table name for pattern matching
|
|
741
|
+
* @returns {object} { type, column, indexed, indexName }
|
|
742
|
+
*/
|
|
743
|
+
_classifyIndex(pIndex, pTableName)
|
|
744
|
+
{
|
|
745
|
+
if (pIndex.Columns.length !== 1)
|
|
746
|
+
{
|
|
747
|
+
return { type: 'explicit' };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
let tmpColumn = pIndex.Columns[0];
|
|
751
|
+
let tmpName = pIndex.Name;
|
|
752
|
+
|
|
753
|
+
if (tmpName === `AK_M_${tmpColumn}`)
|
|
754
|
+
{
|
|
755
|
+
return { type: 'guid-auto', column: tmpColumn };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (tmpName === `IX_M_${tmpColumn}`)
|
|
759
|
+
{
|
|
760
|
+
return { type: 'fk-auto', column: tmpColumn };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
let tmpRegularAutoName = `IX_M_T_${pTableName}_C_${tmpColumn}`;
|
|
764
|
+
if (tmpName === tmpRegularAutoName && !pIndex.Unique)
|
|
765
|
+
{
|
|
766
|
+
return { type: 'column-auto', column: tmpColumn, indexed: true };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
let tmpUniqueAutoName = `AK_M_T_${pTableName}_C_${tmpColumn}`;
|
|
770
|
+
if (tmpName === tmpUniqueAutoName && pIndex.Unique)
|
|
771
|
+
{
|
|
772
|
+
return { type: 'column-auto', column: tmpColumn, indexed: 'unique' };
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return {
|
|
776
|
+
type: 'column-named',
|
|
777
|
+
column: tmpColumn,
|
|
778
|
+
indexed: pIndex.Unique ? 'unique' : true,
|
|
779
|
+
indexName: tmpName
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Generate a complete DDL-level schema for a single table.
|
|
785
|
+
*
|
|
786
|
+
* @param {string} pTableName - Name of the table
|
|
787
|
+
* @param {Function} fCallback - callback(pError, pTableSchema)
|
|
788
|
+
*/
|
|
789
|
+
introspectTableSchema(pTableName, fCallback)
|
|
790
|
+
{
|
|
791
|
+
this.introspectTableColumns(pTableName,
|
|
792
|
+
(pColumnError, pColumns) =>
|
|
793
|
+
{
|
|
794
|
+
if (pColumnError)
|
|
795
|
+
{
|
|
796
|
+
return fCallback(pColumnError);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
this.introspectTableIndices(pTableName,
|
|
800
|
+
(pIndexError, pIndices) =>
|
|
801
|
+
{
|
|
802
|
+
if (pIndexError)
|
|
803
|
+
{
|
|
804
|
+
return fCallback(pIndexError);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
this.introspectTableForeignKeys(pTableName,
|
|
808
|
+
(pFKError, pForeignKeys) =>
|
|
809
|
+
{
|
|
810
|
+
if (pFKError)
|
|
811
|
+
{
|
|
812
|
+
return fCallback(pFKError);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
let tmpColumnMap = {};
|
|
816
|
+
for (let i = 0; i < pColumns.length; i++)
|
|
817
|
+
{
|
|
818
|
+
tmpColumnMap[pColumns[i].Column] = pColumns[i];
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
let tmpExplicitIndices = [];
|
|
822
|
+
|
|
823
|
+
for (let i = 0; i < pIndices.length; i++)
|
|
824
|
+
{
|
|
825
|
+
let tmpClassification = this._classifyIndex(pIndices[i], pTableName);
|
|
826
|
+
|
|
827
|
+
switch (tmpClassification.type)
|
|
828
|
+
{
|
|
829
|
+
case 'column-auto':
|
|
830
|
+
if (tmpColumnMap[tmpClassification.column])
|
|
831
|
+
{
|
|
832
|
+
tmpColumnMap[tmpClassification.column].Indexed = tmpClassification.indexed;
|
|
833
|
+
}
|
|
834
|
+
break;
|
|
835
|
+
case 'column-named':
|
|
836
|
+
if (tmpColumnMap[tmpClassification.column])
|
|
837
|
+
{
|
|
838
|
+
tmpColumnMap[tmpClassification.column].Indexed = tmpClassification.indexed;
|
|
839
|
+
tmpColumnMap[tmpClassification.column].IndexName = tmpClassification.indexName;
|
|
840
|
+
}
|
|
841
|
+
break;
|
|
842
|
+
case 'guid-auto':
|
|
843
|
+
if (tmpColumnMap[tmpClassification.column] &&
|
|
844
|
+
tmpColumnMap[tmpClassification.column].DataType !== 'GUID')
|
|
845
|
+
{
|
|
846
|
+
tmpColumnMap[tmpClassification.column].DataType = 'GUID';
|
|
847
|
+
}
|
|
848
|
+
break;
|
|
849
|
+
case 'fk-auto':
|
|
850
|
+
if (tmpColumnMap[tmpClassification.column] &&
|
|
851
|
+
tmpColumnMap[tmpClassification.column].DataType !== 'ForeignKey')
|
|
852
|
+
{
|
|
853
|
+
tmpColumnMap[tmpClassification.column].DataType = 'ForeignKey';
|
|
854
|
+
}
|
|
855
|
+
break;
|
|
856
|
+
case 'explicit':
|
|
857
|
+
tmpExplicitIndices.push(
|
|
858
|
+
{
|
|
859
|
+
Name: pIndices[i].Name,
|
|
860
|
+
Columns: pIndices[i].Columns,
|
|
861
|
+
Unique: pIndices[i].Unique
|
|
862
|
+
});
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
let tmpSchema = {
|
|
868
|
+
TableName: pTableName,
|
|
869
|
+
Columns: pColumns
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
if (tmpExplicitIndices.length > 0)
|
|
873
|
+
{
|
|
874
|
+
tmpSchema.Indices = tmpExplicitIndices;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (pForeignKeys.length > 0)
|
|
878
|
+
{
|
|
879
|
+
tmpSchema.ForeignKeys = pForeignKeys;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return fCallback(null, tmpSchema);
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Generate DDL schemas for ALL tables in the database.
|
|
890
|
+
*
|
|
891
|
+
* @param {Function} fCallback - callback(pError, { Tables: [...] })
|
|
892
|
+
*/
|
|
893
|
+
introspectDatabaseSchema(fCallback)
|
|
894
|
+
{
|
|
895
|
+
this.listTables(
|
|
896
|
+
(pError, pTableNames) =>
|
|
897
|
+
{
|
|
898
|
+
if (pError)
|
|
899
|
+
{
|
|
900
|
+
return fCallback(pError);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
let tmpTables = [];
|
|
904
|
+
|
|
905
|
+
this.fable.Utility.eachLimit(pTableNames, 1,
|
|
906
|
+
(pTableName, fEachComplete) =>
|
|
907
|
+
{
|
|
908
|
+
this.introspectTableSchema(pTableName,
|
|
909
|
+
(pSchemaError, pSchema) =>
|
|
910
|
+
{
|
|
911
|
+
if (pSchemaError)
|
|
912
|
+
{
|
|
913
|
+
return fEachComplete(pSchemaError);
|
|
914
|
+
}
|
|
915
|
+
tmpTables.push(pSchema);
|
|
916
|
+
return fEachComplete();
|
|
917
|
+
});
|
|
918
|
+
},
|
|
919
|
+
(pEachError) =>
|
|
920
|
+
{
|
|
921
|
+
if (pEachError)
|
|
922
|
+
{
|
|
923
|
+
this.log.error('Meadow-PostgreSQL introspectDatabaseSchema failed!', pEachError);
|
|
924
|
+
return fCallback(pEachError);
|
|
925
|
+
}
|
|
926
|
+
return fCallback(null, { Tables: tmpTables });
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Map a DDL DataType to a Meadow Package schema Type.
|
|
933
|
+
*
|
|
934
|
+
* @param {string} pDataType - The DDL-level DataType
|
|
935
|
+
* @param {string} pColumnName - The column name (for magic column detection)
|
|
936
|
+
* @returns {string} The Meadow Package Type
|
|
937
|
+
*/
|
|
938
|
+
_mapDataTypeToMeadowType(pDataType, pColumnName)
|
|
939
|
+
{
|
|
940
|
+
let tmpLowerName = pColumnName.toLowerCase();
|
|
941
|
+
|
|
942
|
+
if (tmpLowerName === 'createdate') return 'CreateDate';
|
|
943
|
+
if (tmpLowerName === 'creatingiduser') return 'CreateIDUser';
|
|
944
|
+
if (tmpLowerName === 'updatedate') return 'UpdateDate';
|
|
945
|
+
if (tmpLowerName === 'updatingiduser') return 'UpdateIDUser';
|
|
946
|
+
if (tmpLowerName === 'deleted') return 'Deleted';
|
|
947
|
+
if (tmpLowerName === 'deletingiduser') return 'DeleteIDUser';
|
|
948
|
+
if (tmpLowerName === 'deletedate') return 'DeleteDate';
|
|
949
|
+
|
|
950
|
+
switch (pDataType)
|
|
951
|
+
{
|
|
952
|
+
case 'ID': return 'AutoIdentity';
|
|
953
|
+
case 'GUID': return 'AutoGUID';
|
|
954
|
+
case 'ForeignKey': return 'Numeric';
|
|
955
|
+
case 'Numeric': return 'Numeric';
|
|
956
|
+
case 'Decimal': return 'Numeric';
|
|
957
|
+
case 'String': return 'String';
|
|
958
|
+
case 'Text': return 'String';
|
|
959
|
+
case 'DateTime': return 'DateTime';
|
|
960
|
+
case 'Boolean': return 'Boolean';
|
|
961
|
+
case 'JSON': return 'JSON';
|
|
962
|
+
case 'JSONProxy': return 'JSONProxy';
|
|
963
|
+
default: return 'String';
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Get a default value for a given DataType.
|
|
969
|
+
*
|
|
970
|
+
* @param {string} pDataType - The DDL-level DataType
|
|
971
|
+
* @returns {*} The default value
|
|
972
|
+
*/
|
|
973
|
+
_getDefaultValue(pDataType)
|
|
974
|
+
{
|
|
975
|
+
switch (pDataType)
|
|
976
|
+
{
|
|
977
|
+
case 'ID': return 0;
|
|
978
|
+
case 'GUID': return '';
|
|
979
|
+
case 'ForeignKey': return 0;
|
|
980
|
+
case 'Numeric': return 0;
|
|
981
|
+
case 'Decimal': return 0.0;
|
|
982
|
+
case 'String': return '';
|
|
983
|
+
case 'Text': return '';
|
|
984
|
+
case 'DateTime': return '';
|
|
985
|
+
case 'Boolean': return false;
|
|
986
|
+
case 'JSON': return {};
|
|
987
|
+
case 'JSONProxy': return {};
|
|
988
|
+
default: return '';
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Generate a Meadow package JSON for a single table.
|
|
994
|
+
*
|
|
995
|
+
* @param {string} pTableName - Name of the table
|
|
996
|
+
* @param {Function} fCallback - callback(pError, pPackage)
|
|
997
|
+
*/
|
|
998
|
+
generateMeadowPackageFromTable(pTableName, fCallback)
|
|
999
|
+
{
|
|
1000
|
+
this.introspectTableSchema(pTableName,
|
|
1001
|
+
(pError, pSchema) =>
|
|
1002
|
+
{
|
|
1003
|
+
if (pError)
|
|
1004
|
+
{
|
|
1005
|
+
return fCallback(pError);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
let tmpDefaultIdentifier = '';
|
|
1009
|
+
let tmpSchemaEntries = [];
|
|
1010
|
+
let tmpDefaultObject = {};
|
|
1011
|
+
|
|
1012
|
+
for (let i = 0; i < pSchema.Columns.length; i++)
|
|
1013
|
+
{
|
|
1014
|
+
let tmpCol = pSchema.Columns[i];
|
|
1015
|
+
let tmpMeadowType = this._mapDataTypeToMeadowType(tmpCol.DataType, tmpCol.Column);
|
|
1016
|
+
|
|
1017
|
+
if (tmpCol.DataType === 'ID')
|
|
1018
|
+
{
|
|
1019
|
+
tmpDefaultIdentifier = tmpCol.Column;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
let tmpEntry = {
|
|
1023
|
+
Column: tmpCol.Column,
|
|
1024
|
+
Type: tmpMeadowType
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
if (tmpCol.Size)
|
|
1028
|
+
{
|
|
1029
|
+
tmpEntry.Size = tmpCol.Size;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
tmpSchemaEntries.push(tmpEntry);
|
|
1033
|
+
tmpDefaultObject[tmpCol.Column] = this._getDefaultValue(tmpCol.DataType);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
let tmpPackage = {
|
|
1037
|
+
Scope: pTableName,
|
|
1038
|
+
DefaultIdentifier: tmpDefaultIdentifier,
|
|
1039
|
+
Schema: tmpSchemaEntries,
|
|
1040
|
+
DefaultObject: tmpDefaultObject
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
return fCallback(null, tmpPackage);
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
module.exports = MeadowSchemaPostgreSQL;
|