ormbridge 0.4.0 → 0.6.0
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 +60 -7
- package/dist/cliRunner.js +13 -0
- package/dist/cliRunner.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/emitters/jsonSchema.d.ts +13 -0
- package/dist/emitters/jsonSchema.js +78 -4
- package/dist/emitters/jsonSchema.js.map +1 -1
- package/dist/emitters/sqlalchemy.d.ts +30 -0
- package/dist/emitters/sqlalchemy.js +1725 -0
- package/dist/emitters/sqlalchemy.js.map +1 -0
- package/dist/formats.d.ts +7 -0
- package/dist/formats.js +22 -0
- package/dist/formats.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/io.js +27 -1
- package/dist/io.js.map +1 -1
- package/dist/parsers/jsonSchema.js +7 -0
- package/dist/parsers/jsonSchema.js.map +1 -1
- package/dist/parsers/pythonSyntax.d.ts +7 -1
- package/dist/parsers/pythonSyntax.js +47 -8
- package/dist/parsers/pythonSyntax.js.map +1 -1
- package/dist/parsers/sqlalchemy.d.ts +12 -0
- package/dist/parsers/sqlalchemy.js +3191 -0
- package/dist/parsers/sqlalchemy.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,3191 @@
|
|
|
1
|
+
import { singularize, toCamelCase, toPascalCase, toSnakeCase, } from '../naming.js';
|
|
2
|
+
import { describeThrown, err, ok } from '../result.js';
|
|
3
|
+
import { evaluateNode, getPythonParser, lastSegment, } from './pythonSyntax.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Limits and tables of known names
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/** Files larger than this are skipped with a warning, so hostile input cannot stall the parser. */
|
|
8
|
+
const MAX_SOURCE_LENGTH = 2_000_000;
|
|
9
|
+
const MAX_WARNINGS = 500;
|
|
10
|
+
const MAX_TYPE_DEPTH = 24;
|
|
11
|
+
const MAX_STRING_ANNOTATION = 400;
|
|
12
|
+
const MAX_ALIAS_DEPTH = 8;
|
|
13
|
+
const MAX_INHERITANCE_DEPTH = 64;
|
|
14
|
+
const MAX_COLUMNS_PER_TABLE = 2000;
|
|
15
|
+
const MAX_RELATIONSHIPS_PER_TABLE = 2000;
|
|
16
|
+
/** SQLAlchemy and dialect column types, keyed by their lower-cased class name. */
|
|
17
|
+
const SA_TYPES = {
|
|
18
|
+
integer: { type: 'int' },
|
|
19
|
+
int: { type: 'int' },
|
|
20
|
+
smallinteger: { type: 'int' },
|
|
21
|
+
smallint: { type: 'int' },
|
|
22
|
+
tinyint: { type: 'int' },
|
|
23
|
+
mediumint: { type: 'int' },
|
|
24
|
+
biginteger: { type: 'bigInt' },
|
|
25
|
+
bigint: { type: 'bigInt' },
|
|
26
|
+
string: { type: 'string', length: true },
|
|
27
|
+
varchar: { type: 'string', length: true },
|
|
28
|
+
nvarchar: { type: 'string', length: true },
|
|
29
|
+
unicode: { type: 'string', length: true },
|
|
30
|
+
char: { type: 'string', length: true },
|
|
31
|
+
nchar: { type: 'string', length: true },
|
|
32
|
+
text: { type: 'text' },
|
|
33
|
+
unicodetext: { type: 'text' },
|
|
34
|
+
clob: { type: 'text' },
|
|
35
|
+
longtext: { type: 'text' },
|
|
36
|
+
mediumtext: { type: 'text' },
|
|
37
|
+
tinytext: { type: 'text' },
|
|
38
|
+
ntext: { type: 'text' },
|
|
39
|
+
boolean: { type: 'boolean' },
|
|
40
|
+
bool: { type: 'boolean' },
|
|
41
|
+
float: { type: 'float' },
|
|
42
|
+
real: { type: 'float' },
|
|
43
|
+
double: { type: 'float' },
|
|
44
|
+
double_precision: { type: 'float' },
|
|
45
|
+
numeric: { type: 'decimal', numeric: true },
|
|
46
|
+
decimal: { type: 'decimal', numeric: true },
|
|
47
|
+
datetime: { type: 'dateTime' },
|
|
48
|
+
timestamp: { type: 'dateTime' },
|
|
49
|
+
date: { type: 'date' },
|
|
50
|
+
time: { type: 'time' },
|
|
51
|
+
interval: { type: 'duration' },
|
|
52
|
+
largebinary: { type: 'bytes' },
|
|
53
|
+
binary: { type: 'bytes' },
|
|
54
|
+
varbinary: { type: 'bytes' },
|
|
55
|
+
blob: { type: 'bytes' },
|
|
56
|
+
bytea: { type: 'bytes' },
|
|
57
|
+
pickletype: { type: 'bytes' },
|
|
58
|
+
json: { type: 'json' },
|
|
59
|
+
jsonb: { type: 'json' },
|
|
60
|
+
uuid: { type: 'uuid' },
|
|
61
|
+
guid: { type: 'uuid' },
|
|
62
|
+
uniqueidentifier: { type: 'uuid' },
|
|
63
|
+
inet: { type: 'ipAddress' },
|
|
64
|
+
cidr: { type: 'ipAddress' },
|
|
65
|
+
hstore: { type: 'hstore' },
|
|
66
|
+
int4range: { type: 'range', rangeOf: 'int' },
|
|
67
|
+
int8range: { type: 'range', rangeOf: 'bigInt' },
|
|
68
|
+
numrange: { type: 'range', rangeOf: 'decimal' },
|
|
69
|
+
daterange: { type: 'range', rangeOf: 'date' },
|
|
70
|
+
tsrange: { type: 'range', rangeOf: 'dateTime' },
|
|
71
|
+
tstzrange: { type: 'range', rangeOf: 'dateTime' },
|
|
72
|
+
};
|
|
73
|
+
/** Python types a `Mapped[...]` annotation or SQLModel field can name without an explicit column type. */
|
|
74
|
+
const PY_TYPES = {
|
|
75
|
+
int: { type: 'int' },
|
|
76
|
+
str: { type: 'string' },
|
|
77
|
+
float: { type: 'float' },
|
|
78
|
+
bool: { type: 'boolean' },
|
|
79
|
+
bytes: { type: 'bytes' },
|
|
80
|
+
bytearray: { type: 'bytes' },
|
|
81
|
+
datetime: { type: 'dateTime' },
|
|
82
|
+
date: { type: 'date' },
|
|
83
|
+
time: { type: 'time' },
|
|
84
|
+
timedelta: { type: 'duration' },
|
|
85
|
+
Decimal: { type: 'decimal' },
|
|
86
|
+
UUID: { type: 'uuid' },
|
|
87
|
+
IPv4Address: { type: 'ipAddress' },
|
|
88
|
+
IPv6Address: { type: 'ipAddress' },
|
|
89
|
+
IPv4Network: { type: 'ipAddress' },
|
|
90
|
+
IPv6Network: { type: 'ipAddress' },
|
|
91
|
+
};
|
|
92
|
+
/** Calls found among the positional arguments of a column that are not its type. */
|
|
93
|
+
const NON_TYPE_CALLS = new Set([
|
|
94
|
+
'foreignkey',
|
|
95
|
+
'identity',
|
|
96
|
+
'sequence',
|
|
97
|
+
'computed',
|
|
98
|
+
'fetchedvalue',
|
|
99
|
+
'checkconstraint',
|
|
100
|
+
'foreignkeyconstraint',
|
|
101
|
+
'uniqueconstraint',
|
|
102
|
+
'primarykeyconstraint',
|
|
103
|
+
'index',
|
|
104
|
+
'defaultclause',
|
|
105
|
+
'persisted',
|
|
106
|
+
]);
|
|
107
|
+
const ROOT_BASE_NAMES = new Set([
|
|
108
|
+
'DeclarativeBase',
|
|
109
|
+
'DeclarativeBaseNoMeta',
|
|
110
|
+
]);
|
|
111
|
+
const ENUM_BASE_NAMES = new Set([
|
|
112
|
+
'Enum',
|
|
113
|
+
'IntEnum',
|
|
114
|
+
'StrEnum',
|
|
115
|
+
'Flag',
|
|
116
|
+
'IntFlag',
|
|
117
|
+
'ReprEnum',
|
|
118
|
+
]);
|
|
119
|
+
const COLLECTION_TYPES = new Set([
|
|
120
|
+
'list',
|
|
121
|
+
'set',
|
|
122
|
+
'sequence',
|
|
123
|
+
'collection',
|
|
124
|
+
'mutablesequence',
|
|
125
|
+
'frozenset',
|
|
126
|
+
'iterable',
|
|
127
|
+
'dict',
|
|
128
|
+
'mapping',
|
|
129
|
+
'defaultdict',
|
|
130
|
+
'ordereddict',
|
|
131
|
+
]);
|
|
132
|
+
const ON_ACTION_MAP = {
|
|
133
|
+
CASCADE: 'cascade',
|
|
134
|
+
'SET NULL': 'setNull',
|
|
135
|
+
RESTRICT: 'restrict',
|
|
136
|
+
'NO ACTION': 'noAction',
|
|
137
|
+
'SET DEFAULT': 'setDefault',
|
|
138
|
+
};
|
|
139
|
+
/** Mapped-attribute constructors that are not columns or relationships and are not converted. */
|
|
140
|
+
const UNSUPPORTED_ATTRIBUTES = new Set([
|
|
141
|
+
'column_property',
|
|
142
|
+
'composite',
|
|
143
|
+
'synonym',
|
|
144
|
+
'deferred',
|
|
145
|
+
'query_expression',
|
|
146
|
+
'with_polymorphic',
|
|
147
|
+
]);
|
|
148
|
+
/** Constructors that never describe schema (read-only helpers on a model). */
|
|
149
|
+
const IGNORED_ATTRIBUTES = new Set([
|
|
150
|
+
'association_proxy',
|
|
151
|
+
'hybrid_property',
|
|
152
|
+
'hybrid_method',
|
|
153
|
+
'validates',
|
|
154
|
+
'reconstructor',
|
|
155
|
+
]);
|
|
156
|
+
const CURRENT_TIMESTAMP_EXPRESSIONS = new Set([
|
|
157
|
+
'now()',
|
|
158
|
+
'current_timestamp',
|
|
159
|
+
'current_timestamp()',
|
|
160
|
+
'localtimestamp',
|
|
161
|
+
'localtimestamp()',
|
|
162
|
+
'getdate()',
|
|
163
|
+
'sysdatetime()',
|
|
164
|
+
'sysutcdatetime()',
|
|
165
|
+
"datetime('now')",
|
|
166
|
+
"(datetime('now'))",
|
|
167
|
+
'transaction_timestamp()',
|
|
168
|
+
'statement_timestamp()',
|
|
169
|
+
'clock_timestamp()',
|
|
170
|
+
'current_date',
|
|
171
|
+
'current_date()',
|
|
172
|
+
'current_time',
|
|
173
|
+
]);
|
|
174
|
+
const UUID_EXPRESSIONS = new Set([
|
|
175
|
+
'gen_random_uuid()',
|
|
176
|
+
'uuid_generate_v4()',
|
|
177
|
+
'uuid()',
|
|
178
|
+
'newid()',
|
|
179
|
+
'uuid_generate_v1()',
|
|
180
|
+
'newsequentialid()',
|
|
181
|
+
]);
|
|
182
|
+
function warn(ctx, message) {
|
|
183
|
+
if (ctx.warnings.length < MAX_WARNINGS) {
|
|
184
|
+
ctx.warnings.push(message);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
ctx.suppressed += 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Entry point
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
/** Parses SQLAlchemy and SQLModel model files into the shared IR using tree-sitter (no Python required). */
|
|
194
|
+
export async function parseSqlAlchemy(sources, options) {
|
|
195
|
+
const parserResult = await getPythonParser();
|
|
196
|
+
if (!parserResult.ok) {
|
|
197
|
+
return parserResult;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
return buildFromSources(parserResult.value, sources, options);
|
|
201
|
+
}
|
|
202
|
+
catch (thrown) {
|
|
203
|
+
return err('PARSE_FAILED', `Failed to read the SQLAlchemy models: ${describeThrown(thrown)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function buildFromSources(parser, sources, options) {
|
|
207
|
+
const ctx = {
|
|
208
|
+
parser,
|
|
209
|
+
options,
|
|
210
|
+
warnings: [],
|
|
211
|
+
suppressed: 0,
|
|
212
|
+
classes: new Map(),
|
|
213
|
+
classList: [],
|
|
214
|
+
baseVars: new Set(),
|
|
215
|
+
aliases: new Map(),
|
|
216
|
+
typeMap: new Map(),
|
|
217
|
+
tableDefs: [],
|
|
218
|
+
indexCalls: [],
|
|
219
|
+
imperative: [],
|
|
220
|
+
enumInfos: new Map(),
|
|
221
|
+
enums: [],
|
|
222
|
+
tables: [],
|
|
223
|
+
byModel: new Map(),
|
|
224
|
+
byVar: new Map(),
|
|
225
|
+
counter: 0,
|
|
226
|
+
enumClassMemo: new Map(),
|
|
227
|
+
tablesByName: new Map(),
|
|
228
|
+
tableDefByVar: new Map(),
|
|
229
|
+
stubTables: new Map(),
|
|
230
|
+
metadataSchemas: new Map(),
|
|
231
|
+
baseVarSchemas: new Map(),
|
|
232
|
+
};
|
|
233
|
+
for (const source of sources) {
|
|
234
|
+
readSource(ctx, source);
|
|
235
|
+
}
|
|
236
|
+
buildTables(ctx);
|
|
237
|
+
applyModuleIndexes(ctx);
|
|
238
|
+
resolveRelationships(ctx);
|
|
239
|
+
const models = finalizeAll(ctx);
|
|
240
|
+
if (ctx.suppressed > 0) {
|
|
241
|
+
ctx.warnings.push(`${ctx.suppressed} more warnings were left out to keep the output readable.`);
|
|
242
|
+
}
|
|
243
|
+
if (models.length === 0) {
|
|
244
|
+
const checkedPaths = sources
|
|
245
|
+
.map((source) => source.path)
|
|
246
|
+
.join(', ');
|
|
247
|
+
return err('NO_MODELS_FOUND', `No SQLAlchemy or SQLModel models were found in: ${checkedPaths}. A model is a class with a ` +
|
|
248
|
+
`__tablename__ (or a SQLModel class declared with table=True) that declares mapped columns, ` +
|
|
249
|
+
`or a Table(...) assigned to a variable.`);
|
|
250
|
+
}
|
|
251
|
+
return ok({ models, enums: ctx.enums, warnings: ctx.warnings });
|
|
252
|
+
}
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Reading files
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
function readSource(ctx, source) {
|
|
257
|
+
if (typeof source.text !== 'string') {
|
|
258
|
+
warn(ctx, `${source.path}: the file has no text and was skipped.`);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (source.text.length > MAX_SOURCE_LENGTH) {
|
|
262
|
+
warn(ctx, `${source.path}: the file is larger than ${MAX_SOURCE_LENGTH} characters and was skipped.`);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
let tree = null;
|
|
266
|
+
try {
|
|
267
|
+
tree = ctx.parser.parse(source.text);
|
|
268
|
+
if (tree.rootNode.hasError) {
|
|
269
|
+
warn(ctx, `${source.path}: the file contains Python syntax errors; some models or columns may be missing from the output.`);
|
|
270
|
+
}
|
|
271
|
+
for (const statement of tree.rootNode.namedChildren) {
|
|
272
|
+
readTopLevel(ctx, source.path, statement);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (thrown) {
|
|
276
|
+
warn(ctx, `${source.path}: the file could not be read completely (${describeThrown(thrown)}).`);
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
if (tree !== null) {
|
|
280
|
+
tree.delete();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function readTopLevel(ctx, filePath, statement) {
|
|
285
|
+
switch (statement.type) {
|
|
286
|
+
case 'class_definition':
|
|
287
|
+
readClass(ctx, filePath, statement);
|
|
288
|
+
return;
|
|
289
|
+
case 'decorated_definition': {
|
|
290
|
+
const definition = statement.childForFieldName('definition');
|
|
291
|
+
if (definition !== null && definition.type === 'class_definition') {
|
|
292
|
+
readClass(ctx, filePath, definition);
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
case 'expression_statement':
|
|
297
|
+
readModuleExpression(ctx, filePath, statement);
|
|
298
|
+
return;
|
|
299
|
+
case 'if_statement':
|
|
300
|
+
case 'try_statement': {
|
|
301
|
+
// Models are sometimes declared under `if not TYPE_CHECKING:` or after a guarded import.
|
|
302
|
+
const body = statement.childForFieldName('consequence');
|
|
303
|
+
if (body !== null) {
|
|
304
|
+
for (const nested of body.namedChildren) {
|
|
305
|
+
readTopLevel(ctx, filePath, nested);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
default:
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function assignmentParts(statement) {
|
|
315
|
+
const assignment = statement.namedChildren[0];
|
|
316
|
+
if (assignment === undefined || assignment.type !== 'assignment') {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
const left = assignment.childForFieldName('left');
|
|
320
|
+
if (left === null) {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
left,
|
|
325
|
+
right: assignment.childForFieldName('right'),
|
|
326
|
+
type: assignment.childForFieldName('type'),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function readModuleExpression(ctx, filePath, statement) {
|
|
330
|
+
const parts = assignmentParts(statement);
|
|
331
|
+
if (parts === undefined) {
|
|
332
|
+
const expression = statement.namedChildren[0];
|
|
333
|
+
if (expression !== undefined && expression.type === 'call') {
|
|
334
|
+
const value = evaluateNode(expression);
|
|
335
|
+
if (value.kind === 'call') {
|
|
336
|
+
const callee = lastSegment(value.callee);
|
|
337
|
+
if (callee === 'Index') {
|
|
338
|
+
ctx.indexCalls.push({ call: value, filePath });
|
|
339
|
+
}
|
|
340
|
+
else if (callee === 'map_imperatively') {
|
|
341
|
+
readImperative(ctx, value);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (parts.left.type !== 'identifier' || parts.right === null) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const name = parts.left.text;
|
|
351
|
+
if (parts.right.type === 'call') {
|
|
352
|
+
const value = evaluateNode(parts.right);
|
|
353
|
+
if (value.kind !== 'call') {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const callee = lastSegment(value.callee);
|
|
357
|
+
if (callee === 'Table') {
|
|
358
|
+
ctx.tableDefs.push({ varName: name, call: value, filePath });
|
|
359
|
+
ctx.tableDefByVar.set(name, value);
|
|
360
|
+
}
|
|
361
|
+
else if (callee === 'MetaData') {
|
|
362
|
+
const schema = stringOf(value.kwargs['schema']);
|
|
363
|
+
if (schema !== undefined) {
|
|
364
|
+
ctx.metadataSchemas.set(name, schema);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else if (callee === 'declarative_base' ||
|
|
368
|
+
callee === 'automap_base' ||
|
|
369
|
+
callee === 'generate_base') {
|
|
370
|
+
ctx.baseVars.add(name);
|
|
371
|
+
const metadata = value.kwargs['metadata'];
|
|
372
|
+
const schema = metadata === undefined ? undefined : metadataSchemaOf(ctx, metadata);
|
|
373
|
+
if (schema !== undefined) {
|
|
374
|
+
ctx.metadataSchemas.set(`${name}.metadata`, schema);
|
|
375
|
+
ctx.baseVarSchemas.set(name, schema);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (parts.right.type === 'subscript' ||
|
|
381
|
+
parts.right.type === 'generic_type' ||
|
|
382
|
+
parts.right.type === 'binary_operator') {
|
|
383
|
+
const alias = toTypeExpr(ctx, parts.right, 0);
|
|
384
|
+
if (alias.kind === 'sub' || alias.kind === 'union') {
|
|
385
|
+
ctx.aliases.set(name, alias);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function readImperative(ctx, call) {
|
|
390
|
+
const classArg = call.args[0];
|
|
391
|
+
const tableArg = call.args[1];
|
|
392
|
+
if (classArg !== undefined &&
|
|
393
|
+
classArg.kind === 'name' &&
|
|
394
|
+
tableArg !== undefined &&
|
|
395
|
+
tableArg.kind === 'name') {
|
|
396
|
+
ctx.imperative.push({
|
|
397
|
+
className: lastSegment(classArg.value),
|
|
398
|
+
tableVar: tableArg.value,
|
|
399
|
+
hasProperties: call.kwargs['properties'] !== undefined,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// Classes
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
function stringOf(value) {
|
|
407
|
+
return value !== undefined && value.kind === 'string'
|
|
408
|
+
? value.value
|
|
409
|
+
: undefined;
|
|
410
|
+
}
|
|
411
|
+
function boolOf(value) {
|
|
412
|
+
return value !== undefined && value.kind === 'bool' ? value.value : undefined;
|
|
413
|
+
}
|
|
414
|
+
function numberOf(value) {
|
|
415
|
+
return value !== undefined && value.kind === 'number'
|
|
416
|
+
? value.value
|
|
417
|
+
: undefined;
|
|
418
|
+
}
|
|
419
|
+
function readClass(ctx, filePath, node) {
|
|
420
|
+
const nameNode = node.childForFieldName('name');
|
|
421
|
+
if (nameNode === null) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const name = nameNode.text;
|
|
425
|
+
const bases = [];
|
|
426
|
+
const keywords = {};
|
|
427
|
+
const superclasses = node.childForFieldName('superclasses');
|
|
428
|
+
if (superclasses !== null) {
|
|
429
|
+
for (const child of superclasses.namedChildren) {
|
|
430
|
+
if (child.type === 'keyword_argument') {
|
|
431
|
+
const keyNode = child.childForFieldName('name');
|
|
432
|
+
const valueNode = child.childForFieldName('value');
|
|
433
|
+
if (keyNode !== null && valueNode !== null) {
|
|
434
|
+
keywords[keyNode.text] = evaluateNode(valueNode);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else if (child.type !== 'comment') {
|
|
438
|
+
bases.push(child.text.replace(/\s+/g, ''));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const raw = {
|
|
443
|
+
name,
|
|
444
|
+
filePath,
|
|
445
|
+
order: ctx.counter,
|
|
446
|
+
bases,
|
|
447
|
+
keywords,
|
|
448
|
+
members: [],
|
|
449
|
+
isAbstract: false,
|
|
450
|
+
enumMembers: [],
|
|
451
|
+
};
|
|
452
|
+
ctx.counter += 1;
|
|
453
|
+
const body = node.childForFieldName('body');
|
|
454
|
+
for (const statement of body === null ? [] : body.namedChildren) {
|
|
455
|
+
readClassStatement(ctx, raw, statement);
|
|
456
|
+
}
|
|
457
|
+
if (ctx.classes.has(name)) {
|
|
458
|
+
warn(ctx, `Duplicate class name "${name}" (${filePath}); only the first definition was converted.`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
ctx.classes.set(name, raw);
|
|
462
|
+
ctx.classList.push(raw);
|
|
463
|
+
}
|
|
464
|
+
function flattenItems(value) {
|
|
465
|
+
return value.kind === 'list' ? value.items : [value];
|
|
466
|
+
}
|
|
467
|
+
function readClassStatement(ctx, raw, statement) {
|
|
468
|
+
if (statement.type === 'decorated_definition') {
|
|
469
|
+
const definition = statement.childForFieldName('definition');
|
|
470
|
+
if (definition !== null && definition.type === 'function_definition') {
|
|
471
|
+
readClassFunction(ctx, raw, definition, statement);
|
|
472
|
+
}
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (statement.type !== 'expression_statement') {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const parts = assignmentParts(statement);
|
|
479
|
+
if (parts === undefined || parts.left.type !== 'identifier') {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const name = parts.left.text;
|
|
483
|
+
const value = parts.right === null ? undefined : evaluateNode(parts.right);
|
|
484
|
+
const annotationNode = parts.type?.namedChildren[0];
|
|
485
|
+
if (name === '__tablename__') {
|
|
486
|
+
raw.tableName = stringOf(value);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (name === '__abstract__') {
|
|
490
|
+
raw.isAbstract = boolOf(value) === true;
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (name === '__table_args__') {
|
|
494
|
+
if (value !== undefined) {
|
|
495
|
+
raw.tableArgs = flattenItems(value);
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (name === '__table__') {
|
|
500
|
+
if (value !== undefined && value.kind === 'call') {
|
|
501
|
+
raw.tableExpr = value;
|
|
502
|
+
}
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (name === 'impl' && value !== undefined) {
|
|
506
|
+
raw.impl = value;
|
|
507
|
+
}
|
|
508
|
+
if (name === 'metadata' && value !== undefined) {
|
|
509
|
+
const schema = metadataSchemaOf(ctx, value);
|
|
510
|
+
if (schema !== undefined) {
|
|
511
|
+
raw.metadataSchema = schema;
|
|
512
|
+
}
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (name === 'type_annotation_map' && value !== undefined) {
|
|
516
|
+
if (value.kind === 'dict') {
|
|
517
|
+
for (const entry of value.entries) {
|
|
518
|
+
const key = normalizeTypeText(valueText(entry.key));
|
|
519
|
+
if (key !== '' && !ctx.typeMap.has(key)) {
|
|
520
|
+
ctx.typeMap.set(key, entry.value);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (name.startsWith('__')) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
raw.enumMembers.push({ name, value });
|
|
530
|
+
const member = { name };
|
|
531
|
+
if (annotationNode !== undefined) {
|
|
532
|
+
member.annotation = toTypeExpr(ctx, annotationNode, 0);
|
|
533
|
+
}
|
|
534
|
+
if (value !== undefined) {
|
|
535
|
+
member.value = value;
|
|
536
|
+
}
|
|
537
|
+
raw.members.push(member);
|
|
538
|
+
}
|
|
539
|
+
function metadataSchemaOf(ctx, value) {
|
|
540
|
+
if (value.kind === 'call' && lastSegment(value.callee) === 'MetaData') {
|
|
541
|
+
return stringOf(value.kwargs['schema']);
|
|
542
|
+
}
|
|
543
|
+
if (value.kind === 'name') {
|
|
544
|
+
return ctx.metadataSchemas.get(value.value);
|
|
545
|
+
}
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
function valueText(value) {
|
|
549
|
+
switch (value.kind) {
|
|
550
|
+
case 'name':
|
|
551
|
+
return value.value;
|
|
552
|
+
case 'other':
|
|
553
|
+
return value.text;
|
|
554
|
+
case 'string':
|
|
555
|
+
return value.value;
|
|
556
|
+
case 'call':
|
|
557
|
+
return value.text;
|
|
558
|
+
default:
|
|
559
|
+
return '';
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function readClassFunction(ctx, raw, definition, decorated) {
|
|
563
|
+
const nameNode = definition.childForFieldName('name');
|
|
564
|
+
if (nameNode === null) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const functionName = nameNode.text;
|
|
568
|
+
const isDeclaredAttr = decorated.namedChildren.some((child) => child.type === 'decorator' && /declared_attr/.test(child.text));
|
|
569
|
+
if (!isDeclaredAttr) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (functionName === '__tablename__') {
|
|
573
|
+
const body = definition.childForFieldName('body');
|
|
574
|
+
const returned = body?.namedChildren.find((child) => child.type === 'return_statement');
|
|
575
|
+
const expression = returned?.namedChildren[0];
|
|
576
|
+
const text = expression === undefined ? '' : expression.text.replace(/\s+/g, '');
|
|
577
|
+
if (/^cls\.__name__\.lower\(\)$/.test(text)) {
|
|
578
|
+
raw.tableNameRule = 'lower';
|
|
579
|
+
}
|
|
580
|
+
else if (/^cls\.__name__$/.test(text)) {
|
|
581
|
+
raw.tableNameRule = 'exact';
|
|
582
|
+
}
|
|
583
|
+
else if (/__name__/.test(text) && /snake|underscore|re\.sub/.test(text)) {
|
|
584
|
+
raw.tableNameRule = 'snake';
|
|
585
|
+
}
|
|
586
|
+
else if (expression !== undefined && expression.type === 'string') {
|
|
587
|
+
raw.tableName = stringOf(evaluateNode(expression));
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
warn(ctx, `${raw.name}.__tablename__: the @declared_attr could not be evaluated; the table name is derived from the class name.`);
|
|
591
|
+
raw.tableNameRule = 'snake';
|
|
592
|
+
}
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
warn(ctx, `${raw.name}.${functionName}: @declared_attr members are not supported and were skipped.`);
|
|
596
|
+
}
|
|
597
|
+
// ---------------------------------------------------------------------------
|
|
598
|
+
// Type expressions (annotations)
|
|
599
|
+
// ---------------------------------------------------------------------------
|
|
600
|
+
function toTypeExpr(ctx, node, depth) {
|
|
601
|
+
if (depth > MAX_TYPE_DEPTH) {
|
|
602
|
+
return { kind: 'other', text: node.text.slice(0, 100) };
|
|
603
|
+
}
|
|
604
|
+
const next = depth + 1;
|
|
605
|
+
switch (node.type) {
|
|
606
|
+
case 'type':
|
|
607
|
+
case 'parenthesized_expression': {
|
|
608
|
+
const inner = node.namedChildren[0];
|
|
609
|
+
return inner === undefined
|
|
610
|
+
? { kind: 'other', text: node.text }
|
|
611
|
+
: toTypeExpr(ctx, inner, next);
|
|
612
|
+
}
|
|
613
|
+
case 'identifier':
|
|
614
|
+
case 'attribute':
|
|
615
|
+
return { kind: 'name', name: node.text.replace(/\s+/g, '') };
|
|
616
|
+
case 'none':
|
|
617
|
+
return { kind: 'none' };
|
|
618
|
+
case 'string':
|
|
619
|
+
case 'concatenated_string': {
|
|
620
|
+
const value = evaluateNode(node);
|
|
621
|
+
return value.kind === 'string'
|
|
622
|
+
? { kind: 'str', value: value.value }
|
|
623
|
+
: { kind: 'other', text: node.text.slice(0, 100) };
|
|
624
|
+
}
|
|
625
|
+
case 'binary_operator': {
|
|
626
|
+
const left = node.childForFieldName('left');
|
|
627
|
+
const right = node.childForFieldName('right');
|
|
628
|
+
const operator = node.childForFieldName('operator');
|
|
629
|
+
if (left === null ||
|
|
630
|
+
right === null ||
|
|
631
|
+
operator === null ||
|
|
632
|
+
operator.text !== '|') {
|
|
633
|
+
return { kind: 'other', text: node.text.slice(0, 100) };
|
|
634
|
+
}
|
|
635
|
+
const members = [];
|
|
636
|
+
for (const side of [left, right]) {
|
|
637
|
+
const member = toTypeExpr(ctx, side, next);
|
|
638
|
+
if (member.kind === 'union') {
|
|
639
|
+
members.push(...member.members);
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
members.push(member);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return { kind: 'union', members };
|
|
646
|
+
}
|
|
647
|
+
case 'generic_type': {
|
|
648
|
+
const baseNode = node.namedChildren[0];
|
|
649
|
+
const parameters = node.namedChildren.find((child) => child.type === 'type_parameter');
|
|
650
|
+
if (baseNode === undefined) {
|
|
651
|
+
return { kind: 'other', text: node.text.slice(0, 100) };
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
kind: 'sub',
|
|
655
|
+
base: baseNode.text.replace(/\s+/g, ''),
|
|
656
|
+
args: (parameters === undefined ? [] : parameters.namedChildren).map((child) => toTypeExpr(ctx, child, next)),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
case 'subscript': {
|
|
660
|
+
const baseNode = node.childForFieldName('value');
|
|
661
|
+
if (baseNode === null) {
|
|
662
|
+
return { kind: 'other', text: node.text.slice(0, 100) };
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
kind: 'sub',
|
|
666
|
+
base: baseNode.text.replace(/\s+/g, ''),
|
|
667
|
+
args: node
|
|
668
|
+
.childrenForFieldName('subscript')
|
|
669
|
+
.filter((child) => child.isNamed)
|
|
670
|
+
.map((child) => toTypeExpr(ctx, child, next)),
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
case 'call':
|
|
674
|
+
return { kind: 'value', value: evaluateNode(node) };
|
|
675
|
+
default:
|
|
676
|
+
return { kind: 'other', text: node.text.slice(0, 100) };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/** Parses the text of a string annotation such as "Mapped[list['Post']]" once. */
|
|
680
|
+
function typeFromString(ctx, text) {
|
|
681
|
+
const trimmed = text.trim();
|
|
682
|
+
if (trimmed.length === 0 || trimmed.length > MAX_STRING_ANNOTATION) {
|
|
683
|
+
return { kind: 'name', name: trimmed.slice(0, MAX_STRING_ANNOTATION) };
|
|
684
|
+
}
|
|
685
|
+
if (/^[A-Za-z_][\w.]*$/.test(trimmed)) {
|
|
686
|
+
return { kind: 'name', name: trimmed };
|
|
687
|
+
}
|
|
688
|
+
let tree = null;
|
|
689
|
+
try {
|
|
690
|
+
tree = ctx.parser.parse(trimmed);
|
|
691
|
+
const statement = tree.rootNode.namedChildren[0];
|
|
692
|
+
const expression = statement?.namedChildren[0];
|
|
693
|
+
if (statement === undefined ||
|
|
694
|
+
statement.type !== 'expression_statement' ||
|
|
695
|
+
expression === undefined) {
|
|
696
|
+
return { kind: 'other', text: trimmed };
|
|
697
|
+
}
|
|
698
|
+
return toTypeExpr(ctx, expression, MAX_TYPE_DEPTH - 6);
|
|
699
|
+
}
|
|
700
|
+
finally {
|
|
701
|
+
if (tree !== null) {
|
|
702
|
+
tree.delete();
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/** Replaces string forward references by the types they name. */
|
|
707
|
+
function expandStrings(ctx, expression) {
|
|
708
|
+
return expression.kind === 'str'
|
|
709
|
+
? typeFromString(ctx, expression.value)
|
|
710
|
+
: expression;
|
|
711
|
+
}
|
|
712
|
+
function renderType(expression) {
|
|
713
|
+
switch (expression.kind) {
|
|
714
|
+
case 'name':
|
|
715
|
+
return expression.name;
|
|
716
|
+
case 'sub':
|
|
717
|
+
return `${expression.base}[${expression.args.map(renderType).join(',')}]`;
|
|
718
|
+
case 'union':
|
|
719
|
+
return expression.members.map(renderType).join('|');
|
|
720
|
+
case 'str':
|
|
721
|
+
return expression.value;
|
|
722
|
+
case 'none':
|
|
723
|
+
return 'None';
|
|
724
|
+
case 'value':
|
|
725
|
+
return valueText(expression.value);
|
|
726
|
+
case 'other':
|
|
727
|
+
return expression.text;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
function normalizeTypeText(text) {
|
|
731
|
+
return text.replace(/\s+/g, '').replace(/\b\w+\./g, '');
|
|
732
|
+
}
|
|
733
|
+
/** Unwraps Mapped[...], Optional[...], `X | None`, Annotated[...] and aliases. */
|
|
734
|
+
function peelType(ctx, expression, depth = 0) {
|
|
735
|
+
const result = {
|
|
736
|
+
core: expression ?? { kind: 'other', text: '' },
|
|
737
|
+
optional: false,
|
|
738
|
+
mapped: false,
|
|
739
|
+
metadata: [],
|
|
740
|
+
};
|
|
741
|
+
if (expression === undefined) {
|
|
742
|
+
return result;
|
|
743
|
+
}
|
|
744
|
+
let current = expandStrings(ctx, expression);
|
|
745
|
+
for (let step = 0; step < MAX_ALIAS_DEPTH + 8; step += 1) {
|
|
746
|
+
if (depth > MAX_ALIAS_DEPTH) {
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
if (current.kind === 'sub') {
|
|
750
|
+
const base = lastSegment(current.base);
|
|
751
|
+
const first = current.args[0];
|
|
752
|
+
if ((base === 'Mapped' ||
|
|
753
|
+
base === 'WriteOnlyMapped' ||
|
|
754
|
+
base === 'DynamicMapped') &&
|
|
755
|
+
first !== undefined) {
|
|
756
|
+
result.mapped = true;
|
|
757
|
+
current = expandStrings(ctx, first);
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (base === 'Optional' && first !== undefined) {
|
|
761
|
+
result.optional = true;
|
|
762
|
+
current = expandStrings(ctx, first);
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (base === 'Union') {
|
|
766
|
+
const members = current.args.map((arg) => expandStrings(ctx, arg));
|
|
767
|
+
const rest = members.filter((member) => member.kind !== 'none');
|
|
768
|
+
if (rest.length < members.length) {
|
|
769
|
+
result.optional = true;
|
|
770
|
+
}
|
|
771
|
+
if (rest.length === 1 && rest[0] !== undefined) {
|
|
772
|
+
current = rest[0];
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
if (base === 'Annotated' && first !== undefined) {
|
|
778
|
+
for (const extra of current.args.slice(1)) {
|
|
779
|
+
if (extra.kind === 'value' && extra.value.kind === 'call') {
|
|
780
|
+
result.metadata.push(extra.value);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
current = expandStrings(ctx, first);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
if (current.kind === 'union') {
|
|
789
|
+
const rest = current.members
|
|
790
|
+
.map((member) => expandStrings(ctx, member))
|
|
791
|
+
.filter((member) => member.kind !== 'none');
|
|
792
|
+
if (rest.length < current.members.length) {
|
|
793
|
+
result.optional = true;
|
|
794
|
+
}
|
|
795
|
+
if (rest.length === 1 && rest[0] !== undefined) {
|
|
796
|
+
current = rest[0];
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
if (current.kind === 'name') {
|
|
802
|
+
const alias = ctx.aliases.get(current.name);
|
|
803
|
+
if (alias !== undefined) {
|
|
804
|
+
const inner = peelType(ctx, alias, depth + 1);
|
|
805
|
+
result.optional = result.optional || inner.optional;
|
|
806
|
+
result.mapped = result.mapped || inner.mapped;
|
|
807
|
+
result.metadata.unshift(...inner.metadata);
|
|
808
|
+
current = inner.core;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
result.core = current;
|
|
814
|
+
return result;
|
|
815
|
+
}
|
|
816
|
+
/** The class name a relationship annotation points at, and whether it is a collection. */
|
|
817
|
+
function relationshipAnnotation(ctx, annotation) {
|
|
818
|
+
if (annotation === undefined) {
|
|
819
|
+
return { target: undefined, collection: undefined };
|
|
820
|
+
}
|
|
821
|
+
let peeled = peelType(ctx, annotation);
|
|
822
|
+
let collection = false;
|
|
823
|
+
for (let step = 0; step < 4; step += 1) {
|
|
824
|
+
const core = peeled.core;
|
|
825
|
+
if (core.kind === 'sub' &&
|
|
826
|
+
COLLECTION_TYPES.has(lastSegment(core.base).toLowerCase())) {
|
|
827
|
+
collection = true;
|
|
828
|
+
const element = core.args[core.args.length - 1] ?? core.args[0];
|
|
829
|
+
peeled = peelType(ctx, element);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
const core = peeled.core;
|
|
835
|
+
if (core.kind === 'name') {
|
|
836
|
+
if (COLLECTION_TYPES.has(lastSegment(core.name).toLowerCase())) {
|
|
837
|
+
return { target: undefined, collection: true };
|
|
838
|
+
}
|
|
839
|
+
return { target: lastSegment(core.name), collection };
|
|
840
|
+
}
|
|
841
|
+
return { target: undefined, collection };
|
|
842
|
+
}
|
|
843
|
+
// ---------------------------------------------------------------------------
|
|
844
|
+
// Inheritance and model detection
|
|
845
|
+
// ---------------------------------------------------------------------------
|
|
846
|
+
function resolveBase(ctx, baseName) {
|
|
847
|
+
return ctx.classes.get(lastSegment(baseName));
|
|
848
|
+
}
|
|
849
|
+
function isEnumClass(ctx, cls, depth = 0) {
|
|
850
|
+
const memo = ctx.enumClassMemo.get(cls.name);
|
|
851
|
+
if (memo !== undefined) {
|
|
852
|
+
return memo;
|
|
853
|
+
}
|
|
854
|
+
if (depth > MAX_INHERITANCE_DEPTH) {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
let result = false;
|
|
858
|
+
for (const base of cls.bases) {
|
|
859
|
+
if (ENUM_BASE_NAMES.has(lastSegment(base))) {
|
|
860
|
+
result = true;
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
const parent = resolveBase(ctx, base);
|
|
864
|
+
if (parent !== undefined &&
|
|
865
|
+
parent !== cls &&
|
|
866
|
+
isEnumClass(ctx, parent, depth + 1)) {
|
|
867
|
+
result = true;
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
ctx.enumClassMemo.set(cls.name, result);
|
|
872
|
+
return result;
|
|
873
|
+
}
|
|
874
|
+
/** True when the class derives from a declarative base, SQLModel or Flask-SQLAlchemy's db.Model. */
|
|
875
|
+
function derivesFrom(ctx, cls, predicate, visiting = new Set()) {
|
|
876
|
+
if (visiting.has(cls.name) || visiting.size > MAX_INHERITANCE_DEPTH) {
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
visiting.add(cls.name);
|
|
880
|
+
for (const base of cls.bases) {
|
|
881
|
+
if (predicate(base)) {
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
const parent = resolveBase(ctx, base);
|
|
885
|
+
if (parent !== undefined && derivesFrom(ctx, parent, predicate, visiting)) {
|
|
886
|
+
return true;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return false;
|
|
890
|
+
}
|
|
891
|
+
function isSqlModelBase(baseName) {
|
|
892
|
+
return lastSegment(baseName) === 'SQLModel';
|
|
893
|
+
}
|
|
894
|
+
function isFlaskModelBase(baseName) {
|
|
895
|
+
return /^db\.Model$/.test(baseName);
|
|
896
|
+
}
|
|
897
|
+
function isDeclarativeBase(ctx, baseName) {
|
|
898
|
+
return (ROOT_BASE_NAMES.has(lastSegment(baseName)) ||
|
|
899
|
+
ctx.baseVars.has(baseName) ||
|
|
900
|
+
isSqlModelBase(baseName) ||
|
|
901
|
+
isFlaskModelBase(baseName));
|
|
902
|
+
}
|
|
903
|
+
/** A class declared directly on DeclarativeBase is the base itself, never a model. */
|
|
904
|
+
function isRootClass(cls) {
|
|
905
|
+
return cls.bases.some((base) => ROOT_BASE_NAMES.has(lastSegment(base)));
|
|
906
|
+
}
|
|
907
|
+
function inheritedRule(ctx, cls, visiting = new Set()) {
|
|
908
|
+
if (cls.tableNameRule !== undefined) {
|
|
909
|
+
return cls.tableNameRule;
|
|
910
|
+
}
|
|
911
|
+
if (visiting.has(cls.name) || visiting.size > MAX_INHERITANCE_DEPTH) {
|
|
912
|
+
return undefined;
|
|
913
|
+
}
|
|
914
|
+
visiting.add(cls.name);
|
|
915
|
+
for (const base of cls.bases) {
|
|
916
|
+
const parent = resolveBase(ctx, base);
|
|
917
|
+
if (parent !== undefined) {
|
|
918
|
+
const rule = inheritedRule(ctx, parent, visiting);
|
|
919
|
+
if (rule !== undefined) {
|
|
920
|
+
return rule;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return undefined;
|
|
925
|
+
}
|
|
926
|
+
function hasAnyColumns(cls) {
|
|
927
|
+
return cls.members.length > 0;
|
|
928
|
+
}
|
|
929
|
+
/** Decides how a class maps to a table: its name, or undefined when it is not a model. */
|
|
930
|
+
function tableNameOf(ctx, cls) {
|
|
931
|
+
if (cls.isAbstract || isEnumClass(ctx, cls) || isRootClass(cls)) {
|
|
932
|
+
return undefined;
|
|
933
|
+
}
|
|
934
|
+
if (cls.keywords['table'] !== undefined) {
|
|
935
|
+
return boolOf(cls.keywords['table']) === true
|
|
936
|
+
? (cls.tableName ?? cls.name.toLowerCase())
|
|
937
|
+
: undefined;
|
|
938
|
+
}
|
|
939
|
+
if (derivesFrom(ctx, cls, isSqlModelBase)) {
|
|
940
|
+
// A SQLModel class without table=True is a plain data model (a base for table classes).
|
|
941
|
+
return undefined;
|
|
942
|
+
}
|
|
943
|
+
if (cls.tableName !== undefined) {
|
|
944
|
+
return cls.tableName;
|
|
945
|
+
}
|
|
946
|
+
if (cls.tableExpr !== undefined) {
|
|
947
|
+
return stringOf(cls.tableExpr.args[0]) ?? toSnakeCase(cls.name);
|
|
948
|
+
}
|
|
949
|
+
const rule = inheritedRule(ctx, cls);
|
|
950
|
+
if (rule !== undefined &&
|
|
951
|
+
derivesFrom(ctx, cls, (base) => isDeclarativeBase(ctx, base)) &&
|
|
952
|
+
hasAnyColumns(cls)) {
|
|
953
|
+
return rule === 'lower'
|
|
954
|
+
? cls.name.toLowerCase()
|
|
955
|
+
: rule === 'exact'
|
|
956
|
+
? cls.name
|
|
957
|
+
: toSnakeCase(cls.name);
|
|
958
|
+
}
|
|
959
|
+
if (derivesFrom(ctx, cls, isFlaskModelBase) && hasAnyColumns(cls)) {
|
|
960
|
+
return toSnakeCase(cls.name);
|
|
961
|
+
}
|
|
962
|
+
return undefined;
|
|
963
|
+
}
|
|
964
|
+
/** Collects the members of a class and of the mixins and abstract bases it inherits from. */
|
|
965
|
+
function gatherMembers(ctx, cls, gathered, depth) {
|
|
966
|
+
if (depth > MAX_INHERITANCE_DEPTH || gathered.seen.has(cls.name)) {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
gathered.seen.add(cls.name);
|
|
970
|
+
for (const base of cls.bases) {
|
|
971
|
+
const parent = resolveBase(ctx, base);
|
|
972
|
+
if (parent === undefined || parent === cls) {
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
// A concrete table is a parent in joined inheritance: its columns live in its own table.
|
|
976
|
+
if (tableNameOf(ctx, parent) !== undefined) {
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
gatherMembers(ctx, parent, gathered, depth + 1);
|
|
980
|
+
}
|
|
981
|
+
for (const member of cls.members) {
|
|
982
|
+
gathered.members.delete(member.name);
|
|
983
|
+
gathered.members.set(member.name, member);
|
|
984
|
+
}
|
|
985
|
+
if (cls.tableArgs !== undefined) {
|
|
986
|
+
gathered.tableArgs = cls.tableArgs;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
// ---------------------------------------------------------------------------
|
|
990
|
+
// Building tables
|
|
991
|
+
// ---------------------------------------------------------------------------
|
|
992
|
+
function newTable(ctx, init) {
|
|
993
|
+
const table = {
|
|
994
|
+
columns: [],
|
|
995
|
+
columnIndex: new Map(),
|
|
996
|
+
fks: [],
|
|
997
|
+
indexes: [],
|
|
998
|
+
rels: [],
|
|
999
|
+
m2m: [],
|
|
1000
|
+
consumed: false,
|
|
1001
|
+
order: ctx.counter,
|
|
1002
|
+
...init,
|
|
1003
|
+
};
|
|
1004
|
+
ctx.counter += 1;
|
|
1005
|
+
return table;
|
|
1006
|
+
}
|
|
1007
|
+
function buildTables(ctx) {
|
|
1008
|
+
const imperativeByTable = new Map();
|
|
1009
|
+
for (const mapping of ctx.imperative) {
|
|
1010
|
+
imperativeByTable.set(mapping.tableVar, mapping.className);
|
|
1011
|
+
if (mapping.hasProperties) {
|
|
1012
|
+
warn(ctx, `${mapping.className}: the properties of map_imperatively(...) (relationships, column overrides) are not read.`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
for (const cls of ctx.classList) {
|
|
1016
|
+
const tableName = tableNameOf(ctx, cls);
|
|
1017
|
+
const parentModel = cls.bases
|
|
1018
|
+
.map((base) => resolveBase(ctx, base))
|
|
1019
|
+
.find((parent) => parent !== undefined &&
|
|
1020
|
+
parent !== cls &&
|
|
1021
|
+
tableNameOf(ctx, parent) !== undefined);
|
|
1022
|
+
if (tableName === undefined) {
|
|
1023
|
+
if (parentModel !== undefined &&
|
|
1024
|
+
!cls.isAbstract &&
|
|
1025
|
+
!isEnumClass(ctx, cls) &&
|
|
1026
|
+
!derivesFrom(ctx, cls, isSqlModelBase) &&
|
|
1027
|
+
cls.members.some((member) => memberLooksMapped(ctx, member))) {
|
|
1028
|
+
warn(ctx, `${cls.name}: single-table inheritance from ${parentModel.name} is not supported; the subclass was skipped.`);
|
|
1029
|
+
}
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
// Flask-SQLAlchemy and similar: a subclass of a concrete model with no table of its own.
|
|
1033
|
+
if (parentModel !== undefined &&
|
|
1034
|
+
cls.tableName === undefined &&
|
|
1035
|
+
cls.tableExpr === undefined &&
|
|
1036
|
+
cls.keywords['table'] === undefined &&
|
|
1037
|
+
inheritedRule(ctx, cls) === undefined) {
|
|
1038
|
+
warn(ctx, `${cls.name}: single-table inheritance from ${parentModel.name} is not supported; the subclass was skipped.`);
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
const table = newTable(ctx, {
|
|
1042
|
+
modelName: cls.name,
|
|
1043
|
+
tableName,
|
|
1044
|
+
label: cls.name,
|
|
1045
|
+
cls,
|
|
1046
|
+
});
|
|
1047
|
+
buildClassTable(ctx, table, cls);
|
|
1048
|
+
ctx.byModel.set(cls.name, table);
|
|
1049
|
+
ctx.tables.push(table);
|
|
1050
|
+
}
|
|
1051
|
+
// Module-level Table(...) objects: association tables, imperative mappings or plain tables.
|
|
1052
|
+
for (const definition of ctx.tableDefs) {
|
|
1053
|
+
const tableName = stringOf(definition.call.args[0]);
|
|
1054
|
+
if (tableName === undefined) {
|
|
1055
|
+
warn(ctx, `${definition.varName}: the name of the Table is not a string literal; it was skipped.`);
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
const mappedClass = imperativeByTable.get(definition.varName);
|
|
1059
|
+
const table = newTable(ctx, {
|
|
1060
|
+
modelName: mappedClass ?? toPascalCase(singularize(toSnakeCase(tableName))),
|
|
1061
|
+
tableName,
|
|
1062
|
+
label: mappedClass ?? tableName,
|
|
1063
|
+
varName: definition.varName,
|
|
1064
|
+
});
|
|
1065
|
+
const schema = stringOf(definition.call.kwargs['schema']);
|
|
1066
|
+
if (schema !== undefined) {
|
|
1067
|
+
table.schema = schema;
|
|
1068
|
+
}
|
|
1069
|
+
buildFromTableCall(ctx, table, definition.call);
|
|
1070
|
+
ctx.byVar.set(definition.varName, table);
|
|
1071
|
+
ctx.tables.push(table);
|
|
1072
|
+
if (!ctx.byModel.has(table.modelName)) {
|
|
1073
|
+
ctx.byModel.set(table.modelName, table);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
for (const table of ctx.tables) {
|
|
1077
|
+
const same = ctx.tablesByName.get(table.tableName) ?? [];
|
|
1078
|
+
same.push(table);
|
|
1079
|
+
ctx.tablesByName.set(table.tableName, same);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
/** True when a class member declares a column or relationship (so a table-less subclass is worth a warning). */
|
|
1083
|
+
function memberLooksMapped(ctx, member) {
|
|
1084
|
+
if (member.value !== undefined && member.value.kind === 'call') {
|
|
1085
|
+
const lowered = lowerCallee(member.value);
|
|
1086
|
+
return (lowered === 'mapped_column' ||
|
|
1087
|
+
lowered === 'column' ||
|
|
1088
|
+
lowered === 'relationship');
|
|
1089
|
+
}
|
|
1090
|
+
return (member.annotation !== undefined && peelType(ctx, member.annotation).mapped);
|
|
1091
|
+
}
|
|
1092
|
+
function buildFromTableCall(ctx, table, call) {
|
|
1093
|
+
const rest = call.args.slice(2);
|
|
1094
|
+
for (const item of rest) {
|
|
1095
|
+
if (item.kind !== 'call') {
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
const callee = lastSegment(item.callee);
|
|
1099
|
+
if (callee === 'Column') {
|
|
1100
|
+
const spec = specFromCall(item);
|
|
1101
|
+
addColumn(ctx, table, spec.columnName ?? '', spec, undefined, 'classic');
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
for (const item of rest) {
|
|
1105
|
+
if (item.kind === 'call' && lastSegment(item.callee) !== 'Column') {
|
|
1106
|
+
applyTableItem(ctx, table, item);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
if (call.kwargs['autoload_with'] !== undefined) {
|
|
1110
|
+
warn(ctx, `${table.label}: the Table is reflected from a database (autoload_with); no columns are known.`);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
function buildClassTable(ctx, table, cls) {
|
|
1114
|
+
const gathered = {
|
|
1115
|
+
members: new Map(),
|
|
1116
|
+
tableArgs: undefined,
|
|
1117
|
+
seen: new Set(),
|
|
1118
|
+
};
|
|
1119
|
+
gatherMembers(ctx, cls, gathered, 0);
|
|
1120
|
+
const isSqlModel = derivesFrom(ctx, cls, isSqlModelBase);
|
|
1121
|
+
const defaultSchema = inheritedSchema(ctx, cls);
|
|
1122
|
+
if (defaultSchema !== undefined) {
|
|
1123
|
+
table.schema = defaultSchema;
|
|
1124
|
+
}
|
|
1125
|
+
if (cls.tableExpr !== undefined) {
|
|
1126
|
+
buildFromTableCall(ctx, table, cls.tableExpr);
|
|
1127
|
+
const schema = stringOf(cls.tableExpr.kwargs['schema']);
|
|
1128
|
+
if (schema !== undefined) {
|
|
1129
|
+
table.schema = schema;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
for (const member of gathered.members.values()) {
|
|
1133
|
+
readMember(ctx, table, member, isSqlModel);
|
|
1134
|
+
}
|
|
1135
|
+
if (gathered.tableArgs !== undefined) {
|
|
1136
|
+
for (const item of gathered.tableArgs) {
|
|
1137
|
+
if (item.kind === 'dict') {
|
|
1138
|
+
for (const entry of item.entries) {
|
|
1139
|
+
if (stringOf(entry.key) === 'schema') {
|
|
1140
|
+
const schema = stringOf(entry.value);
|
|
1141
|
+
if (schema !== undefined) {
|
|
1142
|
+
table.schema = schema;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
else if (item.kind === 'call') {
|
|
1148
|
+
applyTableItem(ctx, table, item);
|
|
1149
|
+
}
|
|
1150
|
+
else if (item.kind !== 'string') {
|
|
1151
|
+
warn(ctx, `${table.label}.__table_args__: an entry that is not a constraint, index or dict was ignored.`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
/** The schema of the MetaData a class's base was created with. */
|
|
1157
|
+
function inheritedSchema(ctx, cls, visiting = new Set()) {
|
|
1158
|
+
if (cls.metadataSchema !== undefined) {
|
|
1159
|
+
return cls.metadataSchema;
|
|
1160
|
+
}
|
|
1161
|
+
if (visiting.has(cls.name) || visiting.size > MAX_INHERITANCE_DEPTH) {
|
|
1162
|
+
return undefined;
|
|
1163
|
+
}
|
|
1164
|
+
visiting.add(cls.name);
|
|
1165
|
+
for (const base of cls.bases) {
|
|
1166
|
+
const fromVar = ctx.baseVarSchemas.get(base);
|
|
1167
|
+
if (fromVar !== undefined) {
|
|
1168
|
+
return fromVar;
|
|
1169
|
+
}
|
|
1170
|
+
const parent = resolveBase(ctx, base);
|
|
1171
|
+
if (parent !== undefined) {
|
|
1172
|
+
const schema = inheritedSchema(ctx, parent, visiting);
|
|
1173
|
+
if (schema !== undefined) {
|
|
1174
|
+
return schema;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return undefined;
|
|
1179
|
+
}
|
|
1180
|
+
function lowerCallee(call) {
|
|
1181
|
+
return lastSegment(call.callee).toLowerCase();
|
|
1182
|
+
}
|
|
1183
|
+
function readMember(ctx, table, member, isSqlModel) {
|
|
1184
|
+
const location = `${table.label}.${member.name}`;
|
|
1185
|
+
const value = member.value;
|
|
1186
|
+
if (value !== undefined && value.kind === 'call') {
|
|
1187
|
+
const callee = lastSegment(value.callee);
|
|
1188
|
+
const lowered = callee.toLowerCase();
|
|
1189
|
+
if (lowered === 'relationship' || callee === 'Relationship') {
|
|
1190
|
+
addRelationship(ctx, table, member, value);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
if (UNSUPPORTED_ATTRIBUTES.has(callee)) {
|
|
1194
|
+
warn(ctx, `${location}: ${callee}(...) is not supported and was skipped.`);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (IGNORED_ATTRIBUTES.has(callee)) {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (lowered === 'mapped_column' || lowered === 'column') {
|
|
1201
|
+
const spec = mergeAnnotationSpec(ctx, specFromCall(value), member.annotation);
|
|
1202
|
+
addColumn(ctx, table, member.name, spec, member.annotation, lowered === 'column' ? 'classic' : 'mapped');
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (callee === 'Field') {
|
|
1206
|
+
addSqlModelField(ctx, table, member, value);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (member.annotation !== undefined && isSqlModel) {
|
|
1210
|
+
// SQLModel field with a default produced by a call: `created: datetime = datetime.utcnow()`.
|
|
1211
|
+
addColumn(ctx, table, member.name, mergeAnnotationSpec(ctx, { foreignKeys: [], default: value }, member.annotation), member.annotation, 'sqlmodel');
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
if (member.annotation !== undefined) {
|
|
1215
|
+
const peeled = peelType(ctx, member.annotation);
|
|
1216
|
+
if (peeled.mapped) {
|
|
1217
|
+
warn(ctx, `${location}: ${callee}(...) is not a column or relationship constructor ormbridge reads; the attribute was skipped.`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (member.annotation === undefined) {
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
const peeled = peelType(ctx, member.annotation);
|
|
1226
|
+
if (peeled.mapped) {
|
|
1227
|
+
// `name: Mapped[int]` with no mapped_column(...): a plain column.
|
|
1228
|
+
addColumn(ctx, table, member.name, mergeAnnotationSpec(ctx, { foreignKeys: [] }, member.annotation), member.annotation, 'mapped');
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
if (isSqlModel) {
|
|
1232
|
+
if (/^ClassVar/.test(renderType(member.annotation)) ||
|
|
1233
|
+
isModelReference(ctx, member.annotation)) {
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
const spec = mergeAnnotationSpec(ctx, value === undefined
|
|
1237
|
+
? { foreignKeys: [] }
|
|
1238
|
+
: { foreignKeys: [], default: value }, member.annotation);
|
|
1239
|
+
addColumn(ctx, table, member.name, spec, member.annotation, 'sqlmodel');
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
/** True when an annotation names another model class (a SQLModel relationship type). */
|
|
1243
|
+
function isModelReference(ctx, annotation) {
|
|
1244
|
+
const target = relationshipAnnotation(ctx, annotation).target;
|
|
1245
|
+
if (target === undefined) {
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
const cls = ctx.classes.get(target);
|
|
1249
|
+
return cls !== undefined && !isEnumClass(ctx, cls);
|
|
1250
|
+
}
|
|
1251
|
+
// ---------------------------------------------------------------------------
|
|
1252
|
+
// Column specifications
|
|
1253
|
+
// ---------------------------------------------------------------------------
|
|
1254
|
+
function mergeSpecs(base, over) {
|
|
1255
|
+
const merged = {
|
|
1256
|
+
...base,
|
|
1257
|
+
foreignKeys: [...base.foreignKeys, ...over.foreignKeys],
|
|
1258
|
+
};
|
|
1259
|
+
for (const [key, value] of Object.entries(over)) {
|
|
1260
|
+
if (key !== 'foreignKeys' && value !== undefined) {
|
|
1261
|
+
merged[key] = value;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return merged;
|
|
1265
|
+
}
|
|
1266
|
+
/** Adds the mapped_column(...) calls found in Annotated[...] aliases beneath the column's own arguments. */
|
|
1267
|
+
function mergeAnnotationSpec(ctx, spec, annotation) {
|
|
1268
|
+
const peeled = peelType(ctx, annotation);
|
|
1269
|
+
let merged = { foreignKeys: [] };
|
|
1270
|
+
for (const call of peeled.metadata) {
|
|
1271
|
+
const lowered = lowerCallee(call);
|
|
1272
|
+
if (lowered === 'mapped_column' || lowered === 'column') {
|
|
1273
|
+
merged = mergeSpecs(merged, specFromCall(call));
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return mergeSpecs(merged, spec);
|
|
1277
|
+
}
|
|
1278
|
+
function specFromCall(call) {
|
|
1279
|
+
const spec = { foreignKeys: [] };
|
|
1280
|
+
call.args.forEach((argument, index) => {
|
|
1281
|
+
if (argument.kind === 'string') {
|
|
1282
|
+
if (index === 0) {
|
|
1283
|
+
spec.columnName = argument.value;
|
|
1284
|
+
}
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
if (argument.kind === 'call') {
|
|
1288
|
+
const lowered = lowerCallee(argument);
|
|
1289
|
+
if (lowered === 'foreignkey') {
|
|
1290
|
+
spec.foreignKeys.push(argument);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
if (lowered === 'identity' || lowered === 'sequence') {
|
|
1294
|
+
spec.identity = true;
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (lowered === 'computed') {
|
|
1298
|
+
spec.computed = argument;
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (lowered === 'checkconstraint') {
|
|
1302
|
+
spec.hasCheck = true;
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (NON_TYPE_CALLS.has(lowered)) {
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if ((argument.kind === 'call' || argument.kind === 'name') &&
|
|
1310
|
+
spec.typeValue === undefined) {
|
|
1311
|
+
spec.typeValue = argument;
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
const kwargs = call.kwargs;
|
|
1315
|
+
const name = stringOf(kwargs['name']);
|
|
1316
|
+
if (name !== undefined) {
|
|
1317
|
+
spec.columnName = name;
|
|
1318
|
+
}
|
|
1319
|
+
if (kwargs['type_'] !== undefined) {
|
|
1320
|
+
spec.typeValue = kwargs['type_'];
|
|
1321
|
+
}
|
|
1322
|
+
const flags = [
|
|
1323
|
+
['primaryKey', 'primary_key'],
|
|
1324
|
+
['nullable', 'nullable'],
|
|
1325
|
+
['unique', 'unique'],
|
|
1326
|
+
['index', 'index'],
|
|
1327
|
+
];
|
|
1328
|
+
for (const [target, key] of flags) {
|
|
1329
|
+
const flag = boolOf(kwargs[key]);
|
|
1330
|
+
if (flag !== undefined) {
|
|
1331
|
+
spec[target] = flag;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const autoincrement = kwargs['autoincrement'];
|
|
1335
|
+
if (autoincrement !== undefined) {
|
|
1336
|
+
spec.autoincrement = boolOf(autoincrement) ?? true;
|
|
1337
|
+
}
|
|
1338
|
+
spec.default = kwargs['default'] ?? kwargs['insert_default'];
|
|
1339
|
+
if (kwargs['default_factory'] !== undefined) {
|
|
1340
|
+
spec.default = kwargs['default_factory'];
|
|
1341
|
+
}
|
|
1342
|
+
spec.serverDefault = kwargs['server_default'];
|
|
1343
|
+
spec.onUpdate = kwargs['onupdate'];
|
|
1344
|
+
return stripUndefined(spec);
|
|
1345
|
+
}
|
|
1346
|
+
function stripUndefined(value) {
|
|
1347
|
+
const cleaned = {};
|
|
1348
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1349
|
+
if (entry !== undefined) {
|
|
1350
|
+
cleaned[key] = entry;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return cleaned;
|
|
1354
|
+
}
|
|
1355
|
+
function addSqlModelField(ctx, table, member, call) {
|
|
1356
|
+
const kwargs = call.kwargs;
|
|
1357
|
+
let spec = { foreignKeys: [] };
|
|
1358
|
+
const flags = [
|
|
1359
|
+
['primaryKey', 'primary_key'],
|
|
1360
|
+
['nullable', 'nullable'],
|
|
1361
|
+
['unique', 'unique'],
|
|
1362
|
+
['index', 'index'],
|
|
1363
|
+
];
|
|
1364
|
+
for (const [target, key] of flags) {
|
|
1365
|
+
const flag = boolOf(kwargs[key]);
|
|
1366
|
+
if (flag !== undefined) {
|
|
1367
|
+
spec[target] = flag;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
const maxLength = numberOf(kwargs['max_length']);
|
|
1371
|
+
if (maxLength !== undefined) {
|
|
1372
|
+
spec.maxLength = maxLength;
|
|
1373
|
+
}
|
|
1374
|
+
const maxDigits = numberOf(kwargs['max_digits']);
|
|
1375
|
+
if (maxDigits !== undefined) {
|
|
1376
|
+
spec.maxDigits = maxDigits;
|
|
1377
|
+
}
|
|
1378
|
+
const decimalPlaces = numberOf(kwargs['decimal_places']);
|
|
1379
|
+
if (decimalPlaces !== undefined) {
|
|
1380
|
+
spec.decimalPlaces = decimalPlaces;
|
|
1381
|
+
}
|
|
1382
|
+
if (kwargs['foreign_key'] !== undefined) {
|
|
1383
|
+
spec.foreignKeyString = kwargs['foreign_key'];
|
|
1384
|
+
}
|
|
1385
|
+
if (kwargs['ondelete'] !== undefined) {
|
|
1386
|
+
spec.foreignKeyOnDelete = kwargs['ondelete'];
|
|
1387
|
+
}
|
|
1388
|
+
spec.default = kwargs['default'] ?? kwargs['default_factory'];
|
|
1389
|
+
if (kwargs['default_factory'] !== undefined) {
|
|
1390
|
+
spec.default = kwargs['default_factory'];
|
|
1391
|
+
}
|
|
1392
|
+
if (kwargs['sa_type'] !== undefined) {
|
|
1393
|
+
spec.typeValue = kwargs['sa_type'];
|
|
1394
|
+
}
|
|
1395
|
+
const columnKwargs = kwargs['sa_column_kwargs'];
|
|
1396
|
+
if (columnKwargs !== undefined && columnKwargs.kind === 'dict') {
|
|
1397
|
+
const extra = {};
|
|
1398
|
+
for (const entry of columnKwargs.entries) {
|
|
1399
|
+
const key = stringOf(entry.key);
|
|
1400
|
+
if (key !== undefined) {
|
|
1401
|
+
extra[key] = entry.value;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
spec = mergeSpecs(spec, specFromCall({
|
|
1405
|
+
kind: 'call',
|
|
1406
|
+
callee: 'Column',
|
|
1407
|
+
args: [],
|
|
1408
|
+
kwargs: extra,
|
|
1409
|
+
text: '',
|
|
1410
|
+
}));
|
|
1411
|
+
}
|
|
1412
|
+
const columnArgs = kwargs['sa_column_args'];
|
|
1413
|
+
if (columnArgs !== undefined && columnArgs.kind === 'list') {
|
|
1414
|
+
spec = mergeSpecs(spec, specFromCall({
|
|
1415
|
+
kind: 'call',
|
|
1416
|
+
callee: 'Column',
|
|
1417
|
+
args: columnArgs.items,
|
|
1418
|
+
kwargs: {},
|
|
1419
|
+
text: '',
|
|
1420
|
+
}));
|
|
1421
|
+
}
|
|
1422
|
+
const saColumn = kwargs['sa_column'];
|
|
1423
|
+
if (saColumn !== undefined) {
|
|
1424
|
+
if (saColumn.kind === 'call') {
|
|
1425
|
+
// SQLModel uses the Column as it is: the other Field arguments only describe the pydantic field.
|
|
1426
|
+
spec = mergeSpecs({
|
|
1427
|
+
foreignKeys: [],
|
|
1428
|
+
...(spec.maxLength === undefined
|
|
1429
|
+
? {}
|
|
1430
|
+
: { maxLength: spec.maxLength }),
|
|
1431
|
+
rawColumn: true,
|
|
1432
|
+
}, specFromCall(saColumn));
|
|
1433
|
+
}
|
|
1434
|
+
else {
|
|
1435
|
+
warn(ctx, `${table.label}.${member.name}: sa_column could not be evaluated; the Field arguments were used.`);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
addColumn(ctx, table, member.name, mergeAnnotationSpec(ctx, spec, member.annotation), member.annotation, 'sqlmodel');
|
|
1439
|
+
}
|
|
1440
|
+
function addColumn(ctx, table, key, spec, annotation, style) {
|
|
1441
|
+
if (key === '') {
|
|
1442
|
+
warn(ctx, `${table.label}: a Column without a name was skipped (the first argument of Column(...) in a Table must be the column name).`);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
const location = `${table.label}.${key}`;
|
|
1446
|
+
const peeled = peelType(ctx, annotation);
|
|
1447
|
+
if (spec.hasCheck === true) {
|
|
1448
|
+
warn(ctx, `${location}: a CheckConstraint on the column was ignored (check constraints are not part of the shared model).`);
|
|
1449
|
+
}
|
|
1450
|
+
const inferFromForeignKey = spec.typeValue === undefined &&
|
|
1451
|
+
annotation === undefined &&
|
|
1452
|
+
(spec.foreignKeys.length > 0 || spec.foreignKeyString !== undefined);
|
|
1453
|
+
const resolved = inferFromForeignKey
|
|
1454
|
+
? { type: 'int' }
|
|
1455
|
+
: resolveColumnType(ctx, spec, peeled, table, key, location);
|
|
1456
|
+
const isPrimary = spec.primaryKey === true;
|
|
1457
|
+
const isNullable = isPrimary
|
|
1458
|
+
? false
|
|
1459
|
+
: (spec.nullable ??
|
|
1460
|
+
(spec.rawColumn !== true && (peeled.mapped || style === 'sqlmodel')
|
|
1461
|
+
? peeled.optional
|
|
1462
|
+
: true));
|
|
1463
|
+
const field = {
|
|
1464
|
+
name: key,
|
|
1465
|
+
columnName: spec.columnName ?? key,
|
|
1466
|
+
type: resolved.type,
|
|
1467
|
+
isPrimaryKey: isPrimary,
|
|
1468
|
+
isUnique: spec.unique === true,
|
|
1469
|
+
isNullable,
|
|
1470
|
+
isAutoUpdated: false,
|
|
1471
|
+
};
|
|
1472
|
+
if (resolved.maxLength !== undefined) {
|
|
1473
|
+
field.maxLength = resolved.maxLength;
|
|
1474
|
+
}
|
|
1475
|
+
if (resolved.maxDigits !== undefined) {
|
|
1476
|
+
field.maxDigits = resolved.maxDigits;
|
|
1477
|
+
}
|
|
1478
|
+
if (resolved.decimalPlaces !== undefined) {
|
|
1479
|
+
field.decimalPlaces = resolved.decimalPlaces;
|
|
1480
|
+
}
|
|
1481
|
+
if (resolved.enumName !== undefined) {
|
|
1482
|
+
field.enumName = resolved.enumName;
|
|
1483
|
+
}
|
|
1484
|
+
if (resolved.arrayDepth !== undefined) {
|
|
1485
|
+
field.arrayDepth = resolved.arrayDepth;
|
|
1486
|
+
}
|
|
1487
|
+
if (resolved.rangeOf !== undefined) {
|
|
1488
|
+
field.rangeOf = resolved.rangeOf;
|
|
1489
|
+
}
|
|
1490
|
+
if (resolved.unsupportedType !== undefined) {
|
|
1491
|
+
field.unsupportedType = resolved.unsupportedType;
|
|
1492
|
+
}
|
|
1493
|
+
if (spec.maxLength !== undefined && field.maxLength === undefined) {
|
|
1494
|
+
field.maxLength = spec.maxLength;
|
|
1495
|
+
}
|
|
1496
|
+
if (spec.maxDigits !== undefined && field.maxDigits === undefined) {
|
|
1497
|
+
field.maxDigits = spec.maxDigits;
|
|
1498
|
+
}
|
|
1499
|
+
if (spec.decimalPlaces !== undefined && field.decimalPlaces === undefined) {
|
|
1500
|
+
field.decimalPlaces = spec.decimalPlaces;
|
|
1501
|
+
}
|
|
1502
|
+
if (spec.computed !== undefined) {
|
|
1503
|
+
const expression = spec.computed.args[0];
|
|
1504
|
+
field.generated = {
|
|
1505
|
+
expression: expression === undefined ? '' : valueText(expression) || '',
|
|
1506
|
+
isStored: boolOf(spec.computed.kwargs['persisted']) === true,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
applyDefaults(ctx, field, spec, resolved, location);
|
|
1510
|
+
const column = {
|
|
1511
|
+
key,
|
|
1512
|
+
field,
|
|
1513
|
+
...(inferFromForeignKey ? { inferFromForeignKey: true } : {}),
|
|
1514
|
+
wantsAutoIncrement: spec.identity === true ||
|
|
1515
|
+
spec.autoincrement === true ||
|
|
1516
|
+
(spec.autoincrement !== false &&
|
|
1517
|
+
(resolved.type === 'int' || resolved.type === 'bigInt') &&
|
|
1518
|
+
resolved.arrayDepth === undefined),
|
|
1519
|
+
};
|
|
1520
|
+
if (spec.identity === true || spec.autoincrement === true) {
|
|
1521
|
+
column.wantsAutoIncrement = true;
|
|
1522
|
+
field.default ??= { kind: 'autoIncrement' };
|
|
1523
|
+
}
|
|
1524
|
+
const existing = table.columnIndex.get(key);
|
|
1525
|
+
if (existing !== undefined) {
|
|
1526
|
+
table.columns[existing] = column;
|
|
1527
|
+
}
|
|
1528
|
+
else if (table.columns.length >= MAX_COLUMNS_PER_TABLE) {
|
|
1529
|
+
if (table.capped !== true) {
|
|
1530
|
+
table.capped = true;
|
|
1531
|
+
warn(ctx, `${table.label}: more than ${MAX_COLUMNS_PER_TABLE} columns; the rest were skipped.`);
|
|
1532
|
+
}
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
else {
|
|
1536
|
+
table.columnIndex.set(key, table.columns.length);
|
|
1537
|
+
table.columns.push(column);
|
|
1538
|
+
}
|
|
1539
|
+
// Foreign keys.
|
|
1540
|
+
const foreignKeyCalls = spec.foreignKeys;
|
|
1541
|
+
if (foreignKeyCalls.length > 1) {
|
|
1542
|
+
warn(ctx, `${location}: several ForeignKey(...) arguments on one column; only the first was kept.`);
|
|
1543
|
+
}
|
|
1544
|
+
const first = foreignKeyCalls[0];
|
|
1545
|
+
if (first !== undefined) {
|
|
1546
|
+
addColumnForeignKey(ctx, table, key, first, location);
|
|
1547
|
+
}
|
|
1548
|
+
else if (spec.foreignKeyString !== undefined) {
|
|
1549
|
+
const target = parseFkTarget(ctx, spec.foreignKeyString, table);
|
|
1550
|
+
if (target === undefined) {
|
|
1551
|
+
warn(ctx, `${location}: foreign_key could not be evaluated; the column is kept without a foreign key.`);
|
|
1552
|
+
}
|
|
1553
|
+
else {
|
|
1554
|
+
table.fks.push({
|
|
1555
|
+
columns: [key],
|
|
1556
|
+
target,
|
|
1557
|
+
onDelete: actionOf(ctx, spec.foreignKeyOnDelete, location, 'ondelete'),
|
|
1558
|
+
onUpdate: undefined,
|
|
1559
|
+
name: undefined,
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (spec.index === true && spec.unique !== true) {
|
|
1564
|
+
table.indexes.push({
|
|
1565
|
+
columns: [key],
|
|
1566
|
+
isUnique: false,
|
|
1567
|
+
name: undefined,
|
|
1568
|
+
method: undefined,
|
|
1569
|
+
fieldOptions: {},
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
function addColumnForeignKey(ctx, table, key, call, location) {
|
|
1574
|
+
const targetArgument = call.args[0] ?? call.kwargs['column'];
|
|
1575
|
+
const target = targetArgument === undefined
|
|
1576
|
+
? undefined
|
|
1577
|
+
: parseFkTarget(ctx, targetArgument, table);
|
|
1578
|
+
if (target === undefined) {
|
|
1579
|
+
warn(ctx, `${location}: the target of ForeignKey(...) could not be evaluated; the column is kept without a foreign key.`);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
table.fks.push({
|
|
1583
|
+
columns: [key],
|
|
1584
|
+
target,
|
|
1585
|
+
onDelete: actionOf(ctx, call.kwargs['ondelete'], location, 'ondelete'),
|
|
1586
|
+
onUpdate: actionOf(ctx, call.kwargs['onupdate'], location, 'onupdate'),
|
|
1587
|
+
name: stringOf(call.kwargs['name']),
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
function actionOf(ctx, value, location, label) {
|
|
1591
|
+
const text = stringOf(value);
|
|
1592
|
+
if (text === undefined) {
|
|
1593
|
+
return undefined;
|
|
1594
|
+
}
|
|
1595
|
+
const mapped = ON_ACTION_MAP[text.trim().toUpperCase().replace(/_/g, ' ')];
|
|
1596
|
+
if (mapped === undefined) {
|
|
1597
|
+
warn(ctx, `${location}: ${label}="${text}" has no equivalent and was converted to NO ACTION.`);
|
|
1598
|
+
return 'noAction';
|
|
1599
|
+
}
|
|
1600
|
+
return mapped;
|
|
1601
|
+
}
|
|
1602
|
+
/** Reads the target of ForeignKey("table.column"), ForeignKey(Model.column) or ForeignKey(table.c.column). */
|
|
1603
|
+
function parseFkTarget(ctx, value, owner) {
|
|
1604
|
+
if (value.kind === 'string') {
|
|
1605
|
+
const parts = value.value
|
|
1606
|
+
.split('.')
|
|
1607
|
+
.map((part) => part.trim());
|
|
1608
|
+
if (parts.length === 0 || parts.some((part) => part === '')) {
|
|
1609
|
+
return undefined;
|
|
1610
|
+
}
|
|
1611
|
+
if (parts.length === 1) {
|
|
1612
|
+
return { table: parts[0], columns: [] };
|
|
1613
|
+
}
|
|
1614
|
+
if (parts.length === 2) {
|
|
1615
|
+
return { table: parts[0], columns: [parts[1]] };
|
|
1616
|
+
}
|
|
1617
|
+
const column = parts[parts.length - 1];
|
|
1618
|
+
const tableName = parts[parts.length - 2];
|
|
1619
|
+
return {
|
|
1620
|
+
schema: parts.slice(0, -2).join('.'),
|
|
1621
|
+
table: tableName,
|
|
1622
|
+
columns: [column],
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
if (value.kind === 'name') {
|
|
1626
|
+
const parts = value.value
|
|
1627
|
+
.split('.')
|
|
1628
|
+
.filter((part) => part !== '__table__' && part !== 'c');
|
|
1629
|
+
if (parts.length !== 2) {
|
|
1630
|
+
return undefined;
|
|
1631
|
+
}
|
|
1632
|
+
const holder = parts[0];
|
|
1633
|
+
const column = parts[1];
|
|
1634
|
+
const viaClass = ctx.classes.get(holder);
|
|
1635
|
+
if (viaClass !== undefined) {
|
|
1636
|
+
const name = tableNameOf(ctx, viaClass);
|
|
1637
|
+
const columnName = attributeColumnName(viaClass, column);
|
|
1638
|
+
return name === undefined
|
|
1639
|
+
? undefined
|
|
1640
|
+
: { table: name, columns: [columnName] };
|
|
1641
|
+
}
|
|
1642
|
+
const viaVar = ctx.tableDefByVar.get(holder);
|
|
1643
|
+
const varTable = stringOf(viaVar?.args[0]);
|
|
1644
|
+
if (varTable !== undefined) {
|
|
1645
|
+
return { table: varTable, columns: [column] };
|
|
1646
|
+
}
|
|
1647
|
+
if (holder === owner.modelName) {
|
|
1648
|
+
return { table: owner.tableName, columns: [column] };
|
|
1649
|
+
}
|
|
1650
|
+
return undefined;
|
|
1651
|
+
}
|
|
1652
|
+
return undefined;
|
|
1653
|
+
}
|
|
1654
|
+
/** The database column name of an attribute of a class (mapped_column("name") overrides the attribute name). */
|
|
1655
|
+
function attributeColumnName(cls, attribute) {
|
|
1656
|
+
const member = cls.members.find((candidate) => candidate.name === attribute);
|
|
1657
|
+
if (member !== undefined &&
|
|
1658
|
+
member.value !== undefined &&
|
|
1659
|
+
member.value.kind === 'call') {
|
|
1660
|
+
const explicit = stringOf(member.value.args[0]) ?? stringOf(member.value.kwargs['name']);
|
|
1661
|
+
if (explicit !== undefined) {
|
|
1662
|
+
return explicit;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return attribute;
|
|
1666
|
+
}
|
|
1667
|
+
// ---------------------------------------------------------------------------
|
|
1668
|
+
// Types
|
|
1669
|
+
// ---------------------------------------------------------------------------
|
|
1670
|
+
function resolveColumnType(ctx, spec, peeled, table, key, location) {
|
|
1671
|
+
if (spec.typeValue !== undefined) {
|
|
1672
|
+
return resolveSaType(ctx, spec.typeValue, table, key, location, 0);
|
|
1673
|
+
}
|
|
1674
|
+
return resolvePythonType(ctx, peeled, table, key, location);
|
|
1675
|
+
}
|
|
1676
|
+
function typeNameOf(value) {
|
|
1677
|
+
if (value.kind === 'call') {
|
|
1678
|
+
return lastSegment(value.callee);
|
|
1679
|
+
}
|
|
1680
|
+
if (value.kind === 'name') {
|
|
1681
|
+
return lastSegment(value.value);
|
|
1682
|
+
}
|
|
1683
|
+
return undefined;
|
|
1684
|
+
}
|
|
1685
|
+
function resolveSaType(ctx, value, table, key, location, depth) {
|
|
1686
|
+
const name = typeNameOf(value);
|
|
1687
|
+
if (name === undefined || depth > MAX_ALIAS_DEPTH) {
|
|
1688
|
+
warn(ctx, `${location}: the column type could not be evaluated.`);
|
|
1689
|
+
return { type: 'unsupported', unsupportedType: valueText(value) };
|
|
1690
|
+
}
|
|
1691
|
+
const args = value.kind === 'call' ? value.args : [];
|
|
1692
|
+
const kwargs = value.kind === 'call' ? value.kwargs : {};
|
|
1693
|
+
const lowered = name.toLowerCase();
|
|
1694
|
+
if (lowered === 'enum') {
|
|
1695
|
+
return resolveEnumType(ctx, args, kwargs, table, key, location);
|
|
1696
|
+
}
|
|
1697
|
+
if (lowered === 'array') {
|
|
1698
|
+
const inner = args[0];
|
|
1699
|
+
if (inner === undefined) {
|
|
1700
|
+
warn(ctx, `${location}: ARRAY(...) has no item type.`);
|
|
1701
|
+
return { type: 'unsupported', unsupportedType: 'ARRAY' };
|
|
1702
|
+
}
|
|
1703
|
+
const element = resolveSaType(ctx, inner, table, key, location, depth + 1);
|
|
1704
|
+
const dimensions = numberOf(kwargs['dimensions']) ?? 1;
|
|
1705
|
+
return {
|
|
1706
|
+
...element,
|
|
1707
|
+
arrayDepth: (element.arrayDepth ?? 0) + Math.max(1, Math.floor(dimensions)),
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
const decorator = ctx.classes.get(name);
|
|
1711
|
+
if (decorator !== undefined) {
|
|
1712
|
+
if (decorator.impl !== undefined) {
|
|
1713
|
+
return resolveSaType(ctx, decorator.impl, table, key, location, depth + 1);
|
|
1714
|
+
}
|
|
1715
|
+
if (isEnumClass(ctx, decorator)) {
|
|
1716
|
+
return resolveEnumType(ctx, [value], kwargs, table, key, location);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
const spec = SA_TYPES[lowered];
|
|
1720
|
+
if (spec === undefined) {
|
|
1721
|
+
warn(ctx, `${location}: the column type ${name} is not known; the column was kept as an unsupported type.`);
|
|
1722
|
+
return {
|
|
1723
|
+
type: 'unsupported',
|
|
1724
|
+
unsupportedType: describeTypeCall(value, name),
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
const resolved = { type: spec.type };
|
|
1728
|
+
if (spec.rangeOf !== undefined) {
|
|
1729
|
+
resolved.rangeOf = spec.rangeOf;
|
|
1730
|
+
}
|
|
1731
|
+
if (spec.length === true) {
|
|
1732
|
+
const length = numberOf(args[0]) ?? numberOf(kwargs['length']);
|
|
1733
|
+
if (length !== undefined) {
|
|
1734
|
+
resolved.maxLength = length;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
if (spec.numeric === true) {
|
|
1738
|
+
const precision = numberOf(args[0]) ?? numberOf(kwargs['precision']);
|
|
1739
|
+
const scale = numberOf(args[1]) ?? numberOf(kwargs['scale']);
|
|
1740
|
+
if (precision !== undefined) {
|
|
1741
|
+
resolved.maxDigits = precision;
|
|
1742
|
+
}
|
|
1743
|
+
if (scale !== undefined) {
|
|
1744
|
+
resolved.decimalPlaces = scale;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return resolved;
|
|
1748
|
+
}
|
|
1749
|
+
/** `Geometry("POINT")` -> `Geometry(POINT)`: the database type text without quotes. */
|
|
1750
|
+
function describeTypeCall(value, name) {
|
|
1751
|
+
if (value.kind !== 'call' || value.args.length === 0) {
|
|
1752
|
+
return name;
|
|
1753
|
+
}
|
|
1754
|
+
const parts = value.args.map((argument) => argument.kind === 'string' ? argument.value : valueText(argument));
|
|
1755
|
+
return `${name}(${parts.join(', ').replace(/["\\]/g, '')})`.slice(0, 100);
|
|
1756
|
+
}
|
|
1757
|
+
function resolvePythonType(ctx, peeled, table, key, location) {
|
|
1758
|
+
const core = peeled.core;
|
|
1759
|
+
const mapped = ctx.typeMap.get(normalizeTypeText(renderType(core)));
|
|
1760
|
+
if (mapped !== undefined) {
|
|
1761
|
+
return resolveSaType(ctx, mapped, table, key, location, 1);
|
|
1762
|
+
}
|
|
1763
|
+
if (core.kind === 'sub' && lastSegment(core.base) === 'Literal') {
|
|
1764
|
+
const literals = core.args.flatMap((arg) => arg.kind === 'str' ? [{ kind: 'string', value: arg.value }] : []);
|
|
1765
|
+
if (literals.length > 0 && literals.length === core.args.length) {
|
|
1766
|
+
return resolveEnumType(ctx, literals, {}, table, key, location);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
if (core.kind === 'name') {
|
|
1770
|
+
const name = lastSegment(core.name);
|
|
1771
|
+
const cls = ctx.classes.get(name);
|
|
1772
|
+
if (cls !== undefined && isEnumClass(ctx, cls)) {
|
|
1773
|
+
return resolveEnumType(ctx, [{ kind: 'name', value: name }], {}, table, key, location);
|
|
1774
|
+
}
|
|
1775
|
+
const spec = PY_TYPES[name];
|
|
1776
|
+
if (spec !== undefined) {
|
|
1777
|
+
return { type: spec.type };
|
|
1778
|
+
}
|
|
1779
|
+
warn(ctx, `${location}: the Python type ${renderType(core)} has no known column type; the column was kept as an unsupported type (give mapped_column a SQLAlchemy type).`);
|
|
1780
|
+
return { type: 'unsupported', unsupportedType: renderType(core) };
|
|
1781
|
+
}
|
|
1782
|
+
const text = renderType(core);
|
|
1783
|
+
if (text === '') {
|
|
1784
|
+
warn(ctx, `${location}: the column has no type.`);
|
|
1785
|
+
return { type: 'unsupported', unsupportedType: 'unknown' };
|
|
1786
|
+
}
|
|
1787
|
+
warn(ctx, `${location}: the type ${text} has no known column type; the column was kept as an unsupported type.`);
|
|
1788
|
+
return { type: 'unsupported', unsupportedType: text.slice(0, 100) };
|
|
1789
|
+
}
|
|
1790
|
+
function enumMemberName(value, used) {
|
|
1791
|
+
let name = value
|
|
1792
|
+
.replace(/[^A-Za-z0-9]+/g, '_')
|
|
1793
|
+
.replace(/^_+|_+$/g, '')
|
|
1794
|
+
.toUpperCase();
|
|
1795
|
+
if (name.length === 0 || /^[0-9]/.test(name)) {
|
|
1796
|
+
name = `V_${name}`;
|
|
1797
|
+
}
|
|
1798
|
+
let candidate = name;
|
|
1799
|
+
let suffix = 2;
|
|
1800
|
+
while (used.has(candidate)) {
|
|
1801
|
+
candidate = `${name}_${suffix}`;
|
|
1802
|
+
suffix += 1;
|
|
1803
|
+
}
|
|
1804
|
+
used.add(candidate);
|
|
1805
|
+
return candidate;
|
|
1806
|
+
}
|
|
1807
|
+
function resolveEnumType(ctx, args, kwargs, table, key, location) {
|
|
1808
|
+
const explicitName = stringOf(kwargs['name']);
|
|
1809
|
+
const valuesCallable = kwargs['values_callable'];
|
|
1810
|
+
const first = args[0];
|
|
1811
|
+
if (first !== undefined && first.kind === 'name') {
|
|
1812
|
+
const cls = ctx.classes.get(lastSegment(first.value));
|
|
1813
|
+
if (cls === undefined || !isEnumClass(ctx, cls)) {
|
|
1814
|
+
warn(ctx, `${location}: the enum ${first.value} is not defined in the input; the column was kept as a string.`);
|
|
1815
|
+
return { type: 'string' };
|
|
1816
|
+
}
|
|
1817
|
+
const useValues = valuesCallable !== undefined &&
|
|
1818
|
+
/\.value\b|\bvalue\b/.test(valueText(valuesCallable));
|
|
1819
|
+
const known = ctx.enumInfos.get(cls.name);
|
|
1820
|
+
if (known !== undefined) {
|
|
1821
|
+
return { type: 'string', enumName: known.name };
|
|
1822
|
+
}
|
|
1823
|
+
const members = enumMembersOf(ctx, cls);
|
|
1824
|
+
const values = members.map((member) => {
|
|
1825
|
+
const raw = member.value;
|
|
1826
|
+
const stored = useValues && raw !== undefined
|
|
1827
|
+
? raw.kind === 'string'
|
|
1828
|
+
? raw.value
|
|
1829
|
+
: raw.kind === 'number'
|
|
1830
|
+
? String(raw.value)
|
|
1831
|
+
: member.name
|
|
1832
|
+
: member.name;
|
|
1833
|
+
return { name: member.name, dbValue: stored };
|
|
1834
|
+
});
|
|
1835
|
+
return registerEnum(ctx, cls.name, values, explicitName);
|
|
1836
|
+
}
|
|
1837
|
+
const literals = [];
|
|
1838
|
+
for (const argument of args) {
|
|
1839
|
+
if (argument.kind === 'string') {
|
|
1840
|
+
literals.push(argument.value);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
if (literals.length === 0 || literals.length !== args.length) {
|
|
1844
|
+
warn(ctx, `${location}: the enum values could not be evaluated; the column was kept as a string.`);
|
|
1845
|
+
return { type: 'string' };
|
|
1846
|
+
}
|
|
1847
|
+
const enumName = explicitName === undefined
|
|
1848
|
+
? `${table.modelName}${toPascalCase(key)}`
|
|
1849
|
+
: toPascalCase(explicitName);
|
|
1850
|
+
const used = new Set();
|
|
1851
|
+
const values = literals.map((literal) => ({
|
|
1852
|
+
name: enumMemberName(literal, used),
|
|
1853
|
+
dbValue: literal,
|
|
1854
|
+
}));
|
|
1855
|
+
return registerEnum(ctx, enumName, values, explicitName);
|
|
1856
|
+
}
|
|
1857
|
+
function enumMembersOf(ctx, cls) {
|
|
1858
|
+
const members = [];
|
|
1859
|
+
const seen = new Set();
|
|
1860
|
+
const visit = (current, depth) => {
|
|
1861
|
+
if (depth > MAX_INHERITANCE_DEPTH) {
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
for (const base of current.bases) {
|
|
1865
|
+
const parent = resolveBase(ctx, base);
|
|
1866
|
+
if (parent !== undefined && parent !== current) {
|
|
1867
|
+
visit(parent, depth + 1);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
for (const member of current.enumMembers) {
|
|
1871
|
+
if (member.name.startsWith('_') ||
|
|
1872
|
+
seen.has(member.name) ||
|
|
1873
|
+
(member.value !== undefined &&
|
|
1874
|
+
member.value.kind === 'call' &&
|
|
1875
|
+
lastSegment(member.value.callee) === 'property')) {
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
seen.add(member.name);
|
|
1879
|
+
members.push(member);
|
|
1880
|
+
}
|
|
1881
|
+
};
|
|
1882
|
+
visit(cls, 0);
|
|
1883
|
+
return members;
|
|
1884
|
+
}
|
|
1885
|
+
function registerEnum(ctx, name, values, dbName) {
|
|
1886
|
+
const existing = ctx.enumInfos.get(name);
|
|
1887
|
+
if (existing !== undefined) {
|
|
1888
|
+
return { type: 'string', enumName: existing.name };
|
|
1889
|
+
}
|
|
1890
|
+
ctx.enumInfos.set(name, { name, values });
|
|
1891
|
+
ctx.enums.push({
|
|
1892
|
+
name,
|
|
1893
|
+
values,
|
|
1894
|
+
...(dbName === undefined || dbName === name ? {} : { dbName }),
|
|
1895
|
+
});
|
|
1896
|
+
return { type: 'string', enumName: name };
|
|
1897
|
+
}
|
|
1898
|
+
// ---------------------------------------------------------------------------
|
|
1899
|
+
// Defaults
|
|
1900
|
+
// ---------------------------------------------------------------------------
|
|
1901
|
+
const NOW_CALLABLES = new Set([
|
|
1902
|
+
'now',
|
|
1903
|
+
'utcnow',
|
|
1904
|
+
'today',
|
|
1905
|
+
'datetime.now',
|
|
1906
|
+
'datetime.utcnow',
|
|
1907
|
+
'datetime.datetime.now',
|
|
1908
|
+
'datetime.datetime.utcnow',
|
|
1909
|
+
'date.today',
|
|
1910
|
+
'datetime.date.today',
|
|
1911
|
+
'timezone.now',
|
|
1912
|
+
'func.now',
|
|
1913
|
+
'func.current_timestamp',
|
|
1914
|
+
'func.getdate',
|
|
1915
|
+
'func.localtimestamp',
|
|
1916
|
+
'sa.func.now',
|
|
1917
|
+
'sa.func.current_timestamp',
|
|
1918
|
+
'sqlalchemy.func.now',
|
|
1919
|
+
'sqlalchemy.func.current_timestamp',
|
|
1920
|
+
]);
|
|
1921
|
+
/** `datetime.utcnow`, `dt.date.today`, `timezone.now` ... */
|
|
1922
|
+
const NOW_NAME_PATTERN = /(?:^|\.)(?:now|utcnow|today|current_timestamp|localtimestamp)$/;
|
|
1923
|
+
function stripFuncPrefix(text) {
|
|
1924
|
+
return text.replace(/^(?:sa\.|sqlalchemy\.)?func\./, '');
|
|
1925
|
+
}
|
|
1926
|
+
function applyDefaults(ctx, field, spec, resolved, location) {
|
|
1927
|
+
if (spec.serverDefault !== undefined) {
|
|
1928
|
+
const converted = convertDefault(ctx, spec.serverDefault, field, resolved, true, location);
|
|
1929
|
+
if (converted !== undefined) {
|
|
1930
|
+
field.default = converted;
|
|
1931
|
+
field.isDbDefault = true;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
if (resolved.enumName !== undefined &&
|
|
1935
|
+
field.default !== undefined &&
|
|
1936
|
+
field.default.kind === 'literal' &&
|
|
1937
|
+
typeof field.default.value === 'string') {
|
|
1938
|
+
const member = enumValueFor(ctx, resolved.enumName, field.default.value);
|
|
1939
|
+
if (member !== undefined) {
|
|
1940
|
+
field.default = member;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
if (field.default === undefined && spec.default !== undefined) {
|
|
1944
|
+
const converted = convertDefault(ctx, spec.default, field, resolved, false, location);
|
|
1945
|
+
if (converted !== undefined) {
|
|
1946
|
+
field.default = converted;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
if (spec.onUpdate !== undefined) {
|
|
1950
|
+
const onUpdate = convertDefault(ctx, spec.onUpdate, field, resolved, false, location);
|
|
1951
|
+
if (onUpdate !== undefined && onUpdate.kind === 'now') {
|
|
1952
|
+
field.isAutoUpdated = true;
|
|
1953
|
+
// The timestamp is set on insert and on every update; the default would only repeat that.
|
|
1954
|
+
if (field.default !== undefined && field.default.kind === 'now') {
|
|
1955
|
+
delete field.default;
|
|
1956
|
+
delete field.isDbDefault;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
else {
|
|
1960
|
+
warn(ctx, `${location}: onupdate is only kept for timestamps (func.now() and datetime callables); it was dropped.`);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
function enumValueFor(ctx, enumName, stored) {
|
|
1965
|
+
const info = ctx.enumInfos.get(enumName);
|
|
1966
|
+
if (info === undefined) {
|
|
1967
|
+
return undefined;
|
|
1968
|
+
}
|
|
1969
|
+
const member = info.values.find((candidate) => candidate.dbValue === stored || candidate.name === stored);
|
|
1970
|
+
return member === undefined
|
|
1971
|
+
? undefined
|
|
1972
|
+
: { kind: 'enumValue', value: member.name };
|
|
1973
|
+
}
|
|
1974
|
+
function textExpressionDefault(expression, field, isServer) {
|
|
1975
|
+
const trimmed = expression.trim();
|
|
1976
|
+
const unwrapped = trimmed.replace(/^\((.*)\)$/s, '$1').trim();
|
|
1977
|
+
const lowered = unwrapped.toLowerCase();
|
|
1978
|
+
if (CURRENT_TIMESTAMP_EXPRESSIONS.has(lowered)) {
|
|
1979
|
+
return { kind: 'now' };
|
|
1980
|
+
}
|
|
1981
|
+
if (/^current_timestamp\(\d*\)$/.test(lowered) || /^now\(\)$/.test(lowered)) {
|
|
1982
|
+
return { kind: 'now' };
|
|
1983
|
+
}
|
|
1984
|
+
if (UUID_EXPRESSIONS.has(lowered)) {
|
|
1985
|
+
return { kind: 'uuid' };
|
|
1986
|
+
}
|
|
1987
|
+
const quoted = /^'((?:[^']|'')*)'(?:::[\w\s."]+)?$/.exec(unwrapped);
|
|
1988
|
+
if (quoted !== null) {
|
|
1989
|
+
const body = (quoted[1] ?? '').replace(/''/g, "'");
|
|
1990
|
+
if (field.type === 'int' ||
|
|
1991
|
+
field.type === 'bigInt' ||
|
|
1992
|
+
field.type === 'float' ||
|
|
1993
|
+
field.type === 'decimal') {
|
|
1994
|
+
const asNumber = Number(body);
|
|
1995
|
+
if (body.trim() !== '' && !Number.isNaN(asNumber)) {
|
|
1996
|
+
return { kind: 'literal', value: asNumber };
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
if (field.type === 'boolean') {
|
|
2000
|
+
if (/^(true|t|1)$/i.test(body)) {
|
|
2001
|
+
return { kind: 'literal', value: true };
|
|
2002
|
+
}
|
|
2003
|
+
if (/^(false|f|0)$/i.test(body)) {
|
|
2004
|
+
return { kind: 'literal', value: false };
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
return { kind: 'literal', value: body };
|
|
2008
|
+
}
|
|
2009
|
+
if (/^-?\d+(\.\d+)?$/.test(unwrapped)) {
|
|
2010
|
+
if (field.type === 'boolean') {
|
|
2011
|
+
return { kind: 'literal', value: Number(unwrapped) !== 0 };
|
|
2012
|
+
}
|
|
2013
|
+
return { kind: 'literal', value: Number(unwrapped) };
|
|
2014
|
+
}
|
|
2015
|
+
if (/^(true|false)$/i.test(unwrapped)) {
|
|
2016
|
+
return { kind: 'literal', value: lowered === 'true' };
|
|
2017
|
+
}
|
|
2018
|
+
if (isServer) {
|
|
2019
|
+
return { kind: 'dbExpression', expression: trimmed };
|
|
2020
|
+
}
|
|
2021
|
+
return { kind: 'dbExpression', expression: trimmed };
|
|
2022
|
+
}
|
|
2023
|
+
function convertDefault(ctx, value, field, resolved, isServer, location) {
|
|
2024
|
+
switch (value.kind) {
|
|
2025
|
+
case 'none':
|
|
2026
|
+
return undefined;
|
|
2027
|
+
case 'bool':
|
|
2028
|
+
return { kind: 'literal', value: value.value };
|
|
2029
|
+
case 'number':
|
|
2030
|
+
return { kind: 'literal', value: value.value };
|
|
2031
|
+
case 'string': {
|
|
2032
|
+
if (resolved.enumName !== undefined) {
|
|
2033
|
+
const member = enumValueFor(ctx, resolved.enumName, value.value);
|
|
2034
|
+
if (member !== undefined) {
|
|
2035
|
+
return member;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
if (isServer) {
|
|
2039
|
+
return textExpressionDefault(`'${value.value.replace(/'/g, "''")}'`, field, true);
|
|
2040
|
+
}
|
|
2041
|
+
return { kind: 'literal', value: value.value };
|
|
2042
|
+
}
|
|
2043
|
+
case 'list':
|
|
2044
|
+
if (value.items.length === 0) {
|
|
2045
|
+
return { kind: 'literal', value: '[]' };
|
|
2046
|
+
}
|
|
2047
|
+
break;
|
|
2048
|
+
case 'dict':
|
|
2049
|
+
if (value.entries.length === 0) {
|
|
2050
|
+
return { kind: 'literal', value: '{}' };
|
|
2051
|
+
}
|
|
2052
|
+
break;
|
|
2053
|
+
case 'name': {
|
|
2054
|
+
const dotted = value.value;
|
|
2055
|
+
if (dotted === 'uuid.uuid4' ||
|
|
2056
|
+
dotted === 'uuid4' ||
|
|
2057
|
+
dotted === 'uuid.uuid1' ||
|
|
2058
|
+
dotted === 'uuid7' ||
|
|
2059
|
+
dotted === 'uuid.uuid7') {
|
|
2060
|
+
return { kind: 'uuid' };
|
|
2061
|
+
}
|
|
2062
|
+
if (NOW_CALLABLES.has(dotted) || NOW_NAME_PATTERN.test(dotted)) {
|
|
2063
|
+
return { kind: 'now' };
|
|
2064
|
+
}
|
|
2065
|
+
if (dotted === 'dict') {
|
|
2066
|
+
return { kind: 'literal', value: '{}' };
|
|
2067
|
+
}
|
|
2068
|
+
if (dotted === 'list' || dotted === 'set') {
|
|
2069
|
+
return { kind: 'literal', value: '[]' };
|
|
2070
|
+
}
|
|
2071
|
+
const member = enumMemberDefault(ctx, dotted);
|
|
2072
|
+
if (member !== undefined) {
|
|
2073
|
+
return member;
|
|
2074
|
+
}
|
|
2075
|
+
break;
|
|
2076
|
+
}
|
|
2077
|
+
case 'call': {
|
|
2078
|
+
const callee = value.callee;
|
|
2079
|
+
const lowered = lastSegment(callee).toLowerCase();
|
|
2080
|
+
if (lowered === 'text') {
|
|
2081
|
+
const argument = value.args[0];
|
|
2082
|
+
const expression = stringOf(argument);
|
|
2083
|
+
if (expression !== undefined) {
|
|
2084
|
+
return textExpressionDefault(expression, field, isServer);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
if (/(^|\.)func\./.test(callee) || callee.startsWith('func.')) {
|
|
2088
|
+
const converted = textExpressionDefault(stripFuncPrefix(value.text), field, isServer);
|
|
2089
|
+
if (converted.kind !== 'dbExpression' || isServer) {
|
|
2090
|
+
return converted;
|
|
2091
|
+
}
|
|
2092
|
+
break;
|
|
2093
|
+
}
|
|
2094
|
+
if ((lowered === 'true' || lowered === 'false') &&
|
|
2095
|
+
value.args.length === 0) {
|
|
2096
|
+
return { kind: 'literal', value: lowered === 'true' };
|
|
2097
|
+
}
|
|
2098
|
+
if (lowered === 'decimal' || lowered === 'int' || lowered === 'float') {
|
|
2099
|
+
const argument = value.args[0];
|
|
2100
|
+
const numeric = argument === undefined
|
|
2101
|
+
? undefined
|
|
2102
|
+
: argument.kind === 'number'
|
|
2103
|
+
? argument.value
|
|
2104
|
+
: argument.kind === 'string' &&
|
|
2105
|
+
!Number.isNaN(Number(argument.value))
|
|
2106
|
+
? Number(argument.value)
|
|
2107
|
+
: undefined;
|
|
2108
|
+
if (numeric !== undefined) {
|
|
2109
|
+
return { kind: 'literal', value: numeric };
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
if (NOW_CALLABLES.has(callee) || NOW_NAME_PATTERN.test(callee)) {
|
|
2113
|
+
return { kind: 'now' };
|
|
2114
|
+
}
|
|
2115
|
+
if (callee === 'uuid.uuid4' ||
|
|
2116
|
+
callee === 'uuid4' ||
|
|
2117
|
+
callee === 'uuid.uuid1') {
|
|
2118
|
+
return { kind: 'uuid' };
|
|
2119
|
+
}
|
|
2120
|
+
break;
|
|
2121
|
+
}
|
|
2122
|
+
case 'other': {
|
|
2123
|
+
const text = value.text.replace(/\s+/g, ' ');
|
|
2124
|
+
if (/^lambda\s*:/.test(text)) {
|
|
2125
|
+
const body = text.replace(/^lambda\s*:\s*/, '');
|
|
2126
|
+
if (/\b(utcnow|now|today)\s*\(/.test(body) ||
|
|
2127
|
+
/\bfunc\.now\(/.test(body)) {
|
|
2128
|
+
return { kind: 'now' };
|
|
2129
|
+
}
|
|
2130
|
+
if (/\buuid[14]?\s*\(/.test(body)) {
|
|
2131
|
+
return { kind: 'uuid' };
|
|
2132
|
+
}
|
|
2133
|
+
if (body === '{}' || body === 'dict()') {
|
|
2134
|
+
return { kind: 'literal', value: '{}' };
|
|
2135
|
+
}
|
|
2136
|
+
if (body === '[]' || body === 'list()') {
|
|
2137
|
+
return { kind: 'literal', value: '[]' };
|
|
2138
|
+
}
|
|
2139
|
+
if (/^-?\d+(\.\d+)?$/.test(body)) {
|
|
2140
|
+
return { kind: 'literal', value: Number(body) };
|
|
2141
|
+
}
|
|
2142
|
+
const stringBody = /^(['"])(.*)\1$/.exec(body);
|
|
2143
|
+
if (stringBody !== null) {
|
|
2144
|
+
return { kind: 'literal', value: stringBody[2] ?? '' };
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
break;
|
|
2148
|
+
}
|
|
2149
|
+
default:
|
|
2150
|
+
break;
|
|
2151
|
+
}
|
|
2152
|
+
warn(ctx, `${location}: ${isServer ? 'server_default' : 'default'}=${valueText(value) || value.kind} is not representable and was dropped.`);
|
|
2153
|
+
return undefined;
|
|
2154
|
+
}
|
|
2155
|
+
/** `PostStatus.DRAFT` and `PostStatus.DRAFT.value` -> enumValue. */
|
|
2156
|
+
function enumMemberDefault(ctx, dotted) {
|
|
2157
|
+
const segments = dotted.split('.');
|
|
2158
|
+
if (segments[segments.length - 1] === 'value') {
|
|
2159
|
+
segments.pop();
|
|
2160
|
+
}
|
|
2161
|
+
const memberName = segments[1];
|
|
2162
|
+
const className = segments[0];
|
|
2163
|
+
if (className === undefined || memberName === undefined) {
|
|
2164
|
+
return undefined;
|
|
2165
|
+
}
|
|
2166
|
+
const info = ctx.enumInfos.get(className);
|
|
2167
|
+
if (info === undefined) {
|
|
2168
|
+
return undefined;
|
|
2169
|
+
}
|
|
2170
|
+
return info.values.some((candidate) => candidate.name === memberName)
|
|
2171
|
+
? { kind: 'enumValue', value: memberName }
|
|
2172
|
+
: undefined;
|
|
2173
|
+
}
|
|
2174
|
+
// ---------------------------------------------------------------------------
|
|
2175
|
+
// Table constraints and indexes
|
|
2176
|
+
// ---------------------------------------------------------------------------
|
|
2177
|
+
function findColumn(table, reference) {
|
|
2178
|
+
const index = table.columnIndex.get(reference);
|
|
2179
|
+
if (index !== undefined) {
|
|
2180
|
+
return table.columns[index];
|
|
2181
|
+
}
|
|
2182
|
+
return table.columns.find((column) => column.field.columnName === reference);
|
|
2183
|
+
}
|
|
2184
|
+
/** A column named in a constraint: a string, `Model.attribute`, or `table.c.column`. */
|
|
2185
|
+
function constraintColumn(value) {
|
|
2186
|
+
if (value.kind === 'string') {
|
|
2187
|
+
return { name: value.value };
|
|
2188
|
+
}
|
|
2189
|
+
if (value.kind === 'name') {
|
|
2190
|
+
const parts = value.value.split('.');
|
|
2191
|
+
return { name: parts[parts.length - 1] };
|
|
2192
|
+
}
|
|
2193
|
+
if (value.kind === 'call') {
|
|
2194
|
+
const parts = value.callee.split('.');
|
|
2195
|
+
const method = (parts[parts.length - 1] ?? '').toLowerCase();
|
|
2196
|
+
if ((method === 'desc' || method === 'asc') && parts.length >= 2) {
|
|
2197
|
+
return {
|
|
2198
|
+
name: parts[parts.length - 2],
|
|
2199
|
+
sort: method === 'desc' ? 'desc' : 'asc',
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
return { expression: true };
|
|
2204
|
+
}
|
|
2205
|
+
function applyTableItem(ctx, table, call) {
|
|
2206
|
+
const callee = lastSegment(call.callee);
|
|
2207
|
+
const lowered = callee.toLowerCase();
|
|
2208
|
+
const label = `${table.label}.__table_args__`;
|
|
2209
|
+
switch (lowered) {
|
|
2210
|
+
case 'index':
|
|
2211
|
+
applyIndex(ctx, table, call, label);
|
|
2212
|
+
return;
|
|
2213
|
+
case 'uniqueconstraint':
|
|
2214
|
+
applyUnique(ctx, table, call, label);
|
|
2215
|
+
return;
|
|
2216
|
+
case 'primarykeyconstraint': {
|
|
2217
|
+
const columns = [];
|
|
2218
|
+
for (const argument of call.args) {
|
|
2219
|
+
const column = constraintColumn(argument);
|
|
2220
|
+
const found = column.name === undefined
|
|
2221
|
+
? undefined
|
|
2222
|
+
: findColumn(table, column.name);
|
|
2223
|
+
if (found === undefined) {
|
|
2224
|
+
warn(ctx, `${label}: PrimaryKeyConstraint names a column that does not exist; it was ignored.`);
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
columns.push(found.key);
|
|
2228
|
+
}
|
|
2229
|
+
if (columns.length > 0) {
|
|
2230
|
+
table.pk = columns;
|
|
2231
|
+
const name = stringOf(call.kwargs['name']);
|
|
2232
|
+
if (name !== undefined) {
|
|
2233
|
+
table.pkName = name;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
case 'foreignkeyconstraint':
|
|
2239
|
+
applyForeignKeyConstraint(ctx, table, call, label);
|
|
2240
|
+
return;
|
|
2241
|
+
case 'checkconstraint':
|
|
2242
|
+
warn(ctx, `${label}: a CheckConstraint${stringOf(call.kwargs['name']) === undefined ? '' : ` ("${stringOf(call.kwargs['name'])}")`} was ignored (check constraints are not part of the shared model).`);
|
|
2243
|
+
return;
|
|
2244
|
+
case 'excludeconstraint':
|
|
2245
|
+
warn(ctx, `${label}: an ExcludeConstraint was ignored.`);
|
|
2246
|
+
return;
|
|
2247
|
+
default:
|
|
2248
|
+
warn(ctx, `${label}: ${callee}(...) is not supported and was ignored.`);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
function applyForeignKeyConstraint(ctx, table, call, label) {
|
|
2252
|
+
const localArgument = call.args[0] ?? call.kwargs['columns'];
|
|
2253
|
+
const targetArgument = call.args[1] ?? call.kwargs['refcolumns'];
|
|
2254
|
+
if (localArgument === undefined ||
|
|
2255
|
+
targetArgument === undefined ||
|
|
2256
|
+
localArgument.kind !== 'list' ||
|
|
2257
|
+
targetArgument.kind !== 'list') {
|
|
2258
|
+
warn(ctx, `${label}: a ForeignKeyConstraint could not be evaluated and was ignored.`);
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
const local = [];
|
|
2262
|
+
for (const item of localArgument.items) {
|
|
2263
|
+
const found = item.kind === 'string' ? findColumn(table, item.value) : undefined;
|
|
2264
|
+
if (found === undefined) {
|
|
2265
|
+
warn(ctx, `${label}: a ForeignKeyConstraint names a column that does not exist; it was ignored.`);
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
local.push(found.key);
|
|
2269
|
+
}
|
|
2270
|
+
const targets = [];
|
|
2271
|
+
for (const item of targetArgument.items) {
|
|
2272
|
+
const target = parseFkTarget(ctx, item, table);
|
|
2273
|
+
if (target === undefined) {
|
|
2274
|
+
warn(ctx, `${label}: a ForeignKeyConstraint target could not be evaluated and was ignored.`);
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
targets.push(target);
|
|
2278
|
+
}
|
|
2279
|
+
const head = targets[0];
|
|
2280
|
+
if (head === undefined ||
|
|
2281
|
+
targets.length !== local.length ||
|
|
2282
|
+
targets.some((target) => target.table !== head.table)) {
|
|
2283
|
+
warn(ctx, `${label}: a ForeignKeyConstraint with mismatched or mixed targets was ignored.`);
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
table.fks.push({
|
|
2287
|
+
columns: local,
|
|
2288
|
+
target: {
|
|
2289
|
+
...(head.schema === undefined ? {} : { schema: head.schema }),
|
|
2290
|
+
table: head.table,
|
|
2291
|
+
columns: targets.flatMap((target) => target.columns),
|
|
2292
|
+
},
|
|
2293
|
+
onDelete: actionOf(ctx, call.kwargs['ondelete'], label, 'ondelete'),
|
|
2294
|
+
onUpdate: actionOf(ctx, call.kwargs['onupdate'], label, 'onupdate'),
|
|
2295
|
+
name: stringOf(call.kwargs['name']),
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
function applyUnique(ctx, table, call, label) {
|
|
2299
|
+
const columns = [];
|
|
2300
|
+
for (const argument of call.args) {
|
|
2301
|
+
const column = constraintColumn(argument);
|
|
2302
|
+
const found = column.name === undefined ? undefined : findColumn(table, column.name);
|
|
2303
|
+
if (found === undefined) {
|
|
2304
|
+
warn(ctx, `${label}: a UniqueConstraint names a column that does not exist; it was ignored.`);
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
columns.push(found.key);
|
|
2308
|
+
}
|
|
2309
|
+
if (columns.length === 0) {
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
const name = stringOf(call.kwargs['name']);
|
|
2313
|
+
const only = columns[0];
|
|
2314
|
+
if (columns.length === 1 && only !== undefined) {
|
|
2315
|
+
const column = findColumn(table, only);
|
|
2316
|
+
if (column !== undefined) {
|
|
2317
|
+
column.field.isUnique = true;
|
|
2318
|
+
if (name !== undefined) {
|
|
2319
|
+
column.field.uniqueName = name;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
table.indexes.push({
|
|
2325
|
+
columns,
|
|
2326
|
+
isUnique: true,
|
|
2327
|
+
name,
|
|
2328
|
+
method: undefined,
|
|
2329
|
+
fieldOptions: {},
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
function applyIndex(ctx, table, call, label) {
|
|
2333
|
+
const nameArgument = call.args[0];
|
|
2334
|
+
const name = stringOf(nameArgument) ?? stringOf(call.kwargs['name']);
|
|
2335
|
+
const columns = [];
|
|
2336
|
+
const fieldOptions = {};
|
|
2337
|
+
for (const argument of call.args.slice(nameArgument !== undefined &&
|
|
2338
|
+
(nameArgument.kind === 'string' || nameArgument.kind === 'none')
|
|
2339
|
+
? 1
|
|
2340
|
+
: 0)) {
|
|
2341
|
+
const column = constraintColumn(argument);
|
|
2342
|
+
const found = column.name === undefined ? undefined : findColumn(table, column.name);
|
|
2343
|
+
if (column.expression === true || found === undefined) {
|
|
2344
|
+
warn(ctx, `${label}: ${name === undefined ? 'an index' : `the index "${name}"`} on an expression or on a column that does not exist was ignored.`);
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
columns.push(found.key);
|
|
2348
|
+
if (column.sort === 'desc') {
|
|
2349
|
+
fieldOptions[found.key] = { sort: 'desc' };
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
if (columns.length === 0) {
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
const where = Object.entries(call.kwargs).find(([key]) => /(^|_)where$/.test(key))?.[1];
|
|
2356
|
+
if (where !== undefined) {
|
|
2357
|
+
warn(ctx, `${label}: the partial index${name === undefined ? '' : ` "${name}"`} (with a WHERE clause) was ignored.`);
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
const usingEntry = Object.entries(call.kwargs).find(([key]) => /(^|_)using$/.test(key));
|
|
2361
|
+
const method = stringOf(usingEntry?.[1]);
|
|
2362
|
+
table.indexes.push({
|
|
2363
|
+
columns,
|
|
2364
|
+
isUnique: boolOf(call.kwargs['unique']) === true,
|
|
2365
|
+
name,
|
|
2366
|
+
method,
|
|
2367
|
+
fieldOptions,
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
/** Names an expression refers to first: `Post.title.desc()` -> Post, `func.lower(Post.title)` -> func, Post. */
|
|
2371
|
+
function holderNames(value, depth = 0) {
|
|
2372
|
+
if (depth > 4) {
|
|
2373
|
+
return [];
|
|
2374
|
+
}
|
|
2375
|
+
if (value.kind === 'name') {
|
|
2376
|
+
return [(value.value.split('.')[0] ?? '').trim()];
|
|
2377
|
+
}
|
|
2378
|
+
if (value.kind === 'call') {
|
|
2379
|
+
return [
|
|
2380
|
+
(value.callee.split('.')[0] ?? '').trim(),
|
|
2381
|
+
...value.args.flatMap((argument) => holderNames(argument, depth + 1)),
|
|
2382
|
+
];
|
|
2383
|
+
}
|
|
2384
|
+
return [];
|
|
2385
|
+
}
|
|
2386
|
+
/** `Index("name", Model.a, Model.b)` written after the class body. */
|
|
2387
|
+
function applyModuleIndexes(ctx) {
|
|
2388
|
+
for (const { call } of ctx.indexCalls) {
|
|
2389
|
+
let table;
|
|
2390
|
+
for (const argument of call.args) {
|
|
2391
|
+
for (const holder of holderNames(argument)) {
|
|
2392
|
+
table = ctx.byModel.get(holder) ?? ctx.byVar.get(holder);
|
|
2393
|
+
if (table !== undefined) {
|
|
2394
|
+
break;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
if (table !== undefined) {
|
|
2398
|
+
break;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
if (table === undefined) {
|
|
2402
|
+
warn(ctx, `Index(...) at module level could not be matched to a model or table (${call.text.slice(0, 80)}); it was ignored.`);
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
applyIndex(ctx, table, call, `${table.label}`);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
// ---------------------------------------------------------------------------
|
|
2409
|
+
// Relationships
|
|
2410
|
+
// ---------------------------------------------------------------------------
|
|
2411
|
+
function colRefsOf(value) {
|
|
2412
|
+
if (value === undefined) {
|
|
2413
|
+
return [];
|
|
2414
|
+
}
|
|
2415
|
+
const refs = [];
|
|
2416
|
+
const text = value.kind === 'list' ? '' : valueText(value);
|
|
2417
|
+
if (value.kind === 'list') {
|
|
2418
|
+
for (const item of value.items) {
|
|
2419
|
+
refs.push(...colRefsOf(item));
|
|
2420
|
+
}
|
|
2421
|
+
return refs;
|
|
2422
|
+
}
|
|
2423
|
+
for (const match of text.matchAll(/([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?/g)) {
|
|
2424
|
+
const first = match[1] ?? '';
|
|
2425
|
+
const second = match[2];
|
|
2426
|
+
if (first === 'lambda' || first === 'foreign' || first === 'remote') {
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
2429
|
+
refs.push(second === undefined ? { col: first } : { cls: first, col: second });
|
|
2430
|
+
}
|
|
2431
|
+
return refs;
|
|
2432
|
+
}
|
|
2433
|
+
function relationshipTarget(value) {
|
|
2434
|
+
if (value === undefined) {
|
|
2435
|
+
return undefined;
|
|
2436
|
+
}
|
|
2437
|
+
switch (value.kind) {
|
|
2438
|
+
case 'string': {
|
|
2439
|
+
const cleaned = value.value.trim().replace(/^['"]|['"]$/g, '');
|
|
2440
|
+
return /^[\w.]+$/.test(cleaned) ? lastSegment(cleaned) : undefined;
|
|
2441
|
+
}
|
|
2442
|
+
case 'name':
|
|
2443
|
+
return lastSegment(value.value);
|
|
2444
|
+
case 'other': {
|
|
2445
|
+
const lambda = /^lambda\s*:\s*([\w.]+)/.exec(value.text);
|
|
2446
|
+
return lambda === null ? undefined : lastSegment(lambda[1] ?? '');
|
|
2447
|
+
}
|
|
2448
|
+
default:
|
|
2449
|
+
return undefined;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
function addRelationship(ctx, table, member, call) {
|
|
2453
|
+
let kwargs = { ...call.kwargs };
|
|
2454
|
+
const extra = kwargs['sa_relationship_kwargs'];
|
|
2455
|
+
if (extra !== undefined && extra.kind === 'dict') {
|
|
2456
|
+
for (const entry of extra.entries) {
|
|
2457
|
+
const key = stringOf(entry.key);
|
|
2458
|
+
if (key !== undefined && kwargs[key] === undefined) {
|
|
2459
|
+
kwargs[key] = entry.value;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
const inner = kwargs['sa_relationship'];
|
|
2464
|
+
let effective = call;
|
|
2465
|
+
if (inner !== undefined && inner.kind === 'call') {
|
|
2466
|
+
effective = inner;
|
|
2467
|
+
kwargs = { ...inner.kwargs, ...kwargs };
|
|
2468
|
+
}
|
|
2469
|
+
const fromAnnotation = relationshipAnnotation(ctx, member.annotation);
|
|
2470
|
+
const targetName = relationshipTarget(effective.args[0]) ??
|
|
2471
|
+
relationshipTarget(kwargs['argument']) ??
|
|
2472
|
+
fromAnnotation.target;
|
|
2473
|
+
const backref = kwargs['backref'];
|
|
2474
|
+
let backrefInfo;
|
|
2475
|
+
if (backref !== undefined) {
|
|
2476
|
+
if (backref.kind === 'string') {
|
|
2477
|
+
backrefInfo = { name: backref.value };
|
|
2478
|
+
}
|
|
2479
|
+
else if (backref.kind === 'call') {
|
|
2480
|
+
const name = stringOf(backref.args[0]);
|
|
2481
|
+
if (name !== undefined) {
|
|
2482
|
+
const uselist = boolOf(backref.kwargs['uselist']);
|
|
2483
|
+
backrefInfo = { name, ...(uselist === undefined ? {} : { uselist }) };
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
const uselist = boolOf(kwargs['uselist']);
|
|
2488
|
+
const rel = {
|
|
2489
|
+
table,
|
|
2490
|
+
key: member.name,
|
|
2491
|
+
order: ctx.counter,
|
|
2492
|
+
targetName,
|
|
2493
|
+
foreignKeys: colRefsOf(kwargs['foreign_keys']),
|
|
2494
|
+
joinRefs: [
|
|
2495
|
+
...colRefsOf(kwargs['primaryjoin']),
|
|
2496
|
+
...colRefsOf(kwargs['secondaryjoin']),
|
|
2497
|
+
],
|
|
2498
|
+
remoteSide: kwargs['remote_side'] !== undefined,
|
|
2499
|
+
viewonly: boolOf(kwargs['viewonly']) === true,
|
|
2500
|
+
synthetic: false,
|
|
2501
|
+
};
|
|
2502
|
+
ctx.counter += 1;
|
|
2503
|
+
const backPopulates = stringOf(kwargs['back_populates']);
|
|
2504
|
+
if (backPopulates !== undefined) {
|
|
2505
|
+
rel.backPopulates = backPopulates;
|
|
2506
|
+
}
|
|
2507
|
+
if (backrefInfo !== undefined) {
|
|
2508
|
+
rel.backref = backrefInfo;
|
|
2509
|
+
}
|
|
2510
|
+
if (kwargs['secondary'] !== undefined) {
|
|
2511
|
+
rel.secondary = kwargs['secondary'];
|
|
2512
|
+
}
|
|
2513
|
+
const linkModel = kwargs['link_model'];
|
|
2514
|
+
if (linkModel !== undefined) {
|
|
2515
|
+
const linkName = relationshipTarget(linkModel);
|
|
2516
|
+
if (linkName !== undefined) {
|
|
2517
|
+
rel.linkModel = linkName;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
if (uselist !== undefined) {
|
|
2521
|
+
rel.uselist = uselist;
|
|
2522
|
+
}
|
|
2523
|
+
const collection = uselist ?? fromAnnotation.collection;
|
|
2524
|
+
if (collection !== undefined) {
|
|
2525
|
+
rel.collection = collection;
|
|
2526
|
+
}
|
|
2527
|
+
if (targetName === undefined) {
|
|
2528
|
+
warn(ctx, `${table.label}.${member.name}: the target model of the relationship could not be determined; it was skipped.`);
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
if (table.rels.length >= MAX_RELATIONSHIPS_PER_TABLE) {
|
|
2532
|
+
if (table.relsCapped !== true) {
|
|
2533
|
+
table.relsCapped = true;
|
|
2534
|
+
warn(ctx, `${table.label}: more than ${MAX_RELATIONSHIPS_PER_TABLE} relationships; the rest were skipped.`);
|
|
2535
|
+
}
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
table.rels.push(rel);
|
|
2539
|
+
}
|
|
2540
|
+
function tableOfRef(ctx, rel, reference) {
|
|
2541
|
+
return reference.cls === undefined
|
|
2542
|
+
? rel.table
|
|
2543
|
+
: (ctx.byModel.get(reference.cls) ?? ctx.byVar.get(reference.cls));
|
|
2544
|
+
}
|
|
2545
|
+
function fkWithColumn(table, key) {
|
|
2546
|
+
return table.fks.find((fk) => fk.columns.includes(key));
|
|
2547
|
+
}
|
|
2548
|
+
function tableFor(ctx, target) {
|
|
2549
|
+
const matches = (ctx.tablesByName.get(target.table) ?? []).filter((candidate) => target.schema === undefined || candidate.schema === target.schema);
|
|
2550
|
+
if (matches.length === 0 && target.schema !== undefined) {
|
|
2551
|
+
const unqualified = ctx.tablesByName.get(target.table) ?? [];
|
|
2552
|
+
return unqualified.length === 1 ? unqualified[0] : undefined;
|
|
2553
|
+
}
|
|
2554
|
+
if (matches.length <= 1) {
|
|
2555
|
+
return matches[0];
|
|
2556
|
+
}
|
|
2557
|
+
// Prefer a class-mapped table when a Table() variable has the same name.
|
|
2558
|
+
return (matches.find((candidate) => candidate.cls !== undefined) ??
|
|
2559
|
+
matches[0]);
|
|
2560
|
+
}
|
|
2561
|
+
function fksBetween(ctx, from, to) {
|
|
2562
|
+
return from.fks.filter((fk) => tableFor(ctx, fk.target) === to);
|
|
2563
|
+
}
|
|
2564
|
+
function resolveRelationships(ctx) {
|
|
2565
|
+
// backref="name" declares the other side implicitly.
|
|
2566
|
+
for (const table of [...ctx.tables]) {
|
|
2567
|
+
for (const rel of [...table.rels]) {
|
|
2568
|
+
if (rel.backref === undefined ||
|
|
2569
|
+
rel.viewonly ||
|
|
2570
|
+
rel.targetName === undefined) {
|
|
2571
|
+
continue;
|
|
2572
|
+
}
|
|
2573
|
+
const target = ctx.byModel.get(rel.targetName);
|
|
2574
|
+
if (target === undefined ||
|
|
2575
|
+
target.rels.some((other) => other.key === rel.backref?.name)) {
|
|
2576
|
+
continue;
|
|
2577
|
+
}
|
|
2578
|
+
const synthetic = {
|
|
2579
|
+
table: target,
|
|
2580
|
+
key: rel.backref.name,
|
|
2581
|
+
order: rel.order + 0.5,
|
|
2582
|
+
targetName: table.modelName,
|
|
2583
|
+
backPopulates: rel.key,
|
|
2584
|
+
foreignKeys: rel.foreignKeys,
|
|
2585
|
+
joinRefs: rel.joinRefs,
|
|
2586
|
+
remoteSide: false,
|
|
2587
|
+
viewonly: false,
|
|
2588
|
+
synthetic: true,
|
|
2589
|
+
...(rel.secondary === undefined ? {} : { secondary: rel.secondary }),
|
|
2590
|
+
...(rel.linkModel === undefined ? {} : { linkModel: rel.linkModel }),
|
|
2591
|
+
...(rel.backref.uselist === undefined
|
|
2592
|
+
? {}
|
|
2593
|
+
: { uselist: rel.backref.uselist, collection: rel.backref.uselist }),
|
|
2594
|
+
};
|
|
2595
|
+
target.rels.push(synthetic);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
// Owning sides first, so reverse sides can find them by name.
|
|
2599
|
+
const reverse = [];
|
|
2600
|
+
const manyToMany = [];
|
|
2601
|
+
for (const table of ctx.tables) {
|
|
2602
|
+
for (const rel of table.rels) {
|
|
2603
|
+
if (rel.viewonly) {
|
|
2604
|
+
continue;
|
|
2605
|
+
}
|
|
2606
|
+
if (rel.secondary !== undefined || rel.linkModel !== undefined) {
|
|
2607
|
+
manyToMany.push(rel);
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
classifyRelationship(ctx, rel, reverse);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
for (const rel of reverse) {
|
|
2614
|
+
linkReverse(ctx, rel);
|
|
2615
|
+
}
|
|
2616
|
+
linkManyToMany(ctx, manyToMany);
|
|
2617
|
+
}
|
|
2618
|
+
function sideOf(ctx, rel, target) {
|
|
2619
|
+
const own = rel.table;
|
|
2620
|
+
const hints = rel.foreignKeys.length > 0 ? rel.foreignKeys : [];
|
|
2621
|
+
if (hints.length > 0) {
|
|
2622
|
+
const hinted = hints[0];
|
|
2623
|
+
const holder = hinted === undefined ? undefined : tableOfRef(ctx, rel, hinted);
|
|
2624
|
+
if (holder !== undefined && hinted !== undefined) {
|
|
2625
|
+
const fk = fkWithColumn(holder, hinted.col);
|
|
2626
|
+
if (fk !== undefined) {
|
|
2627
|
+
if (own === target) {
|
|
2628
|
+
return {
|
|
2629
|
+
side: rel.remoteSide ? 'owner' : 'reverse',
|
|
2630
|
+
candidates: [fk],
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
return { side: holder === own ? 'owner' : 'reverse', candidates: [fk] };
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
// primaryjoin hints: columns that carry a foreign key between the two tables.
|
|
2638
|
+
const joinHints = [];
|
|
2639
|
+
for (const reference of rel.joinRefs) {
|
|
2640
|
+
const holder = tableOfRef(ctx, rel, reference);
|
|
2641
|
+
const fk = holder === undefined ? undefined : fkWithColumn(holder, reference.col);
|
|
2642
|
+
if (holder !== undefined &&
|
|
2643
|
+
fk !== undefined &&
|
|
2644
|
+
((holder === own && tableFor(ctx, fk.target) === target) ||
|
|
2645
|
+
(holder === target && tableFor(ctx, fk.target) === own))) {
|
|
2646
|
+
joinHints.push(fk);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
const firstJoin = joinHints[0];
|
|
2650
|
+
if (firstJoin !== undefined) {
|
|
2651
|
+
const owns = own.fks.includes(firstJoin);
|
|
2652
|
+
if (own === target) {
|
|
2653
|
+
return {
|
|
2654
|
+
side: rel.remoteSide ? 'owner' : 'reverse',
|
|
2655
|
+
candidates: [firstJoin],
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
return { side: owns ? 'owner' : 'reverse', candidates: [firstJoin] };
|
|
2659
|
+
}
|
|
2660
|
+
const forward = fksBetween(ctx, own, target);
|
|
2661
|
+
if (own === target) {
|
|
2662
|
+
return { side: rel.remoteSide ? 'owner' : 'reverse', candidates: forward };
|
|
2663
|
+
}
|
|
2664
|
+
const backward = fksBetween(ctx, target, own);
|
|
2665
|
+
if (forward.length > 0 && backward.length === 0) {
|
|
2666
|
+
return { side: 'owner', candidates: forward };
|
|
2667
|
+
}
|
|
2668
|
+
if (backward.length > 0 && forward.length === 0) {
|
|
2669
|
+
return { side: 'reverse', candidates: backward };
|
|
2670
|
+
}
|
|
2671
|
+
if (forward.length > 0 && backward.length > 0) {
|
|
2672
|
+
return rel.collection === true
|
|
2673
|
+
? { side: 'reverse', candidates: backward }
|
|
2674
|
+
: { side: 'owner', candidates: forward };
|
|
2675
|
+
}
|
|
2676
|
+
return { side: 'none', candidates: [] };
|
|
2677
|
+
}
|
|
2678
|
+
/** Picks the foreign key a relationship uses when several fit: by name, then by order. */
|
|
2679
|
+
function pickForeignKey(rel, candidates, taken) {
|
|
2680
|
+
if (candidates.length <= 1) {
|
|
2681
|
+
return candidates[0];
|
|
2682
|
+
}
|
|
2683
|
+
const stripped = (key) => key.replace(/(_id|Id|ID)$/, '');
|
|
2684
|
+
const byName = candidates.find((fk) => fk.columns.some((column) => column === rel.key || stripped(column) === rel.key));
|
|
2685
|
+
if (byName !== undefined) {
|
|
2686
|
+
return byName;
|
|
2687
|
+
}
|
|
2688
|
+
return candidates.find((fk) => !taken(fk)) ?? candidates[0];
|
|
2689
|
+
}
|
|
2690
|
+
function classifyRelationship(ctx, rel, reverse) {
|
|
2691
|
+
const location = `${rel.table.label}.${rel.key}`;
|
|
2692
|
+
const target = rel.targetName === undefined ? undefined : ctx.byModel.get(rel.targetName);
|
|
2693
|
+
if (target === undefined) {
|
|
2694
|
+
warn(ctx, `${location}: the relationship points at "${rel.targetName ?? ''}", which is not a model in the input; it was skipped.`);
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
const { side, candidates } = sideOf(ctx, rel, target);
|
|
2698
|
+
if (side === 'none') {
|
|
2699
|
+
warn(ctx, `${location}: no foreign key links ${rel.table.modelName} and ${target.modelName}; the relationship was skipped.`);
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
if (side === 'reverse') {
|
|
2703
|
+
reverse.push(rel);
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
const fk = pickForeignKey(rel, candidates, (candidate) => candidate.ownerName !== undefined);
|
|
2707
|
+
if (fk === undefined) {
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
if (candidates.length > 1 && rel.foreignKeys.length === 0) {
|
|
2711
|
+
warn(ctx, `${location}: several foreign keys link ${rel.table.modelName} and ${target.modelName}; add foreign_keys=[...] to tell them apart.`);
|
|
2712
|
+
}
|
|
2713
|
+
if (fk.ownerName === undefined) {
|
|
2714
|
+
fk.ownerName = rel.key;
|
|
2715
|
+
}
|
|
2716
|
+
const declared = rel.backPopulates ?? rel.backref?.name;
|
|
2717
|
+
if (declared !== undefined && fk.declaredRelatedName === undefined) {
|
|
2718
|
+
fk.declaredRelatedName = declared;
|
|
2719
|
+
}
|
|
2720
|
+
if (rel.uselist === false &&
|
|
2721
|
+
rel.collection === false &&
|
|
2722
|
+
fk.reverseScalar === undefined) {
|
|
2723
|
+
// uselist=False on the owning side is the default for many-to-one; it says nothing about one-to-one.
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
function linkReverse(ctx, rel) {
|
|
2727
|
+
const location = `${rel.table.label}.${rel.key}`;
|
|
2728
|
+
const target = rel.targetName === undefined ? undefined : ctx.byModel.get(rel.targetName);
|
|
2729
|
+
if (target === undefined) {
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
const { candidates } = sideOf(ctx, rel, target);
|
|
2733
|
+
let fk;
|
|
2734
|
+
if (rel.backPopulates !== undefined) {
|
|
2735
|
+
fk = candidates.find((candidate) => candidate.ownerName === rel.backPopulates);
|
|
2736
|
+
}
|
|
2737
|
+
if (fk === undefined) {
|
|
2738
|
+
fk = pickForeignKey(rel, candidates.filter((candidate) => candidate.reverseName === undefined), (candidate) => candidate.reverseName !== undefined);
|
|
2739
|
+
}
|
|
2740
|
+
if (fk === undefined) {
|
|
2741
|
+
warn(ctx, `${location}: the relationship could not be matched to a foreign key of ${target.modelName}; the reverse side was not linked.`);
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
if (fk.reverseName === undefined) {
|
|
2745
|
+
fk.reverseName = rel.key;
|
|
2746
|
+
if (rel.collection === false || rel.uselist === false) {
|
|
2747
|
+
fk.reverseScalar = true;
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
// ---------------------------------------------------------------------------
|
|
2752
|
+
// Many-to-many
|
|
2753
|
+
// ---------------------------------------------------------------------------
|
|
2754
|
+
function associationTableOf(ctx, rel) {
|
|
2755
|
+
if (rel.linkModel !== undefined) {
|
|
2756
|
+
return ctx.byModel.get(rel.linkModel);
|
|
2757
|
+
}
|
|
2758
|
+
const secondary = rel.secondary;
|
|
2759
|
+
if (secondary === undefined) {
|
|
2760
|
+
return undefined;
|
|
2761
|
+
}
|
|
2762
|
+
if (secondary.kind === 'name') {
|
|
2763
|
+
const variable = secondary.value;
|
|
2764
|
+
return (ctx.byVar.get(variable) ??
|
|
2765
|
+
ctx.byVar.get(lastSegment(variable)) ??
|
|
2766
|
+
ctx.byModel.get(lastSegment(variable)));
|
|
2767
|
+
}
|
|
2768
|
+
if (secondary.kind === 'string') {
|
|
2769
|
+
const name = lastSegment(secondary.value);
|
|
2770
|
+
return ctx.tablesByName.get(name)?.[0];
|
|
2771
|
+
}
|
|
2772
|
+
if (secondary.kind === 'other') {
|
|
2773
|
+
const lambda = /^lambda\s*:\s*([\w.]+)/.exec(secondary.text);
|
|
2774
|
+
if (lambda !== null) {
|
|
2775
|
+
const variable = lambda[1] ?? '';
|
|
2776
|
+
return ctx.byVar.get(variable) ?? ctx.byModel.get(lastSegment(variable));
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
return undefined;
|
|
2780
|
+
}
|
|
2781
|
+
function linkManyToMany(ctx, relations) {
|
|
2782
|
+
const ordered = [...relations].sort((first, second) => first.order - second.order);
|
|
2783
|
+
for (const rel of ordered) {
|
|
2784
|
+
if (rel.folded === true) {
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2787
|
+
const location = `${rel.table.label}.${rel.key}`;
|
|
2788
|
+
const target = rel.targetName === undefined
|
|
2789
|
+
? undefined
|
|
2790
|
+
: ctx.byModel.get(rel.targetName);
|
|
2791
|
+
if (target === undefined) {
|
|
2792
|
+
warn(ctx, `${location}: the many-to-many points at "${rel.targetName ?? ''}", which is not a model in the input; it was skipped.`);
|
|
2793
|
+
continue;
|
|
2794
|
+
}
|
|
2795
|
+
const association = associationTableOf(ctx, rel);
|
|
2796
|
+
const secondaryText = rel.linkModel ??
|
|
2797
|
+
(rel.secondary === undefined ? '' : valueText(rel.secondary));
|
|
2798
|
+
if (association === undefined) {
|
|
2799
|
+
warn(ctx, `${location}: the association table ${secondaryText === '' ? '' : `"${secondaryText}" `}is not defined in the input; the many-to-many was kept without checking its columns.`);
|
|
2800
|
+
}
|
|
2801
|
+
else if (!isPureAssociation(ctx, association, rel.table, target)) {
|
|
2802
|
+
if (rel.linkModel !== undefined) {
|
|
2803
|
+
warn(ctx, `${location}: the link model ${association.modelName} has columns besides its two foreign keys, so it stays a model and the many-to-many was skipped.`);
|
|
2804
|
+
continue;
|
|
2805
|
+
}
|
|
2806
|
+
warn(ctx, `${location}: the association table ${association.tableName} has columns besides its two foreign keys; they are not represented.`);
|
|
2807
|
+
association.consumed = true;
|
|
2808
|
+
}
|
|
2809
|
+
else {
|
|
2810
|
+
association.consumed = true;
|
|
2811
|
+
}
|
|
2812
|
+
// Pair with the other side so the relation is written once, with the other name as its reverse.
|
|
2813
|
+
const partner = target.rels.find((other) => other !== rel &&
|
|
2814
|
+
other.folded !== true &&
|
|
2815
|
+
(other.secondary !== undefined || other.linkModel !== undefined) &&
|
|
2816
|
+
((rel.backPopulates !== undefined && other.key === rel.backPopulates) ||
|
|
2817
|
+
(other.backPopulates === rel.key &&
|
|
2818
|
+
other.targetName === rel.table.modelName)));
|
|
2819
|
+
if (partner !== undefined) {
|
|
2820
|
+
partner.folded = true;
|
|
2821
|
+
}
|
|
2822
|
+
const relatedName = partner?.key ?? rel.backPopulates ?? rel.backref?.name;
|
|
2823
|
+
rel.table.m2m.push({
|
|
2824
|
+
name: rel.key,
|
|
2825
|
+
kind: 'manyToMany',
|
|
2826
|
+
targetModel: target.modelName,
|
|
2827
|
+
columnName: '',
|
|
2828
|
+
isNullable: false,
|
|
2829
|
+
onDelete: 'cascade',
|
|
2830
|
+
...(relatedName === undefined ? {} : { relatedName }),
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
/** True when the table is made of the two foreign keys of a many-to-many and nothing else. */
|
|
2835
|
+
function isPureAssociation(ctx, association, first, second) {
|
|
2836
|
+
const keyed = new Set(association.fks.flatMap((fk) => fk.columns));
|
|
2837
|
+
const hasFirst = association.fks.some((fk) => tableFor(ctx, fk.target) === first);
|
|
2838
|
+
const hasSecond = association.fks.some((fk) => tableFor(ctx, fk.target) === second);
|
|
2839
|
+
return (hasFirst &&
|
|
2840
|
+
hasSecond &&
|
|
2841
|
+
association.columns.every((column) => keyed.has(column.key)));
|
|
2842
|
+
}
|
|
2843
|
+
// ---------------------------------------------------------------------------
|
|
2844
|
+
// Finalizing
|
|
2845
|
+
// ---------------------------------------------------------------------------
|
|
2846
|
+
/** Gives columns declared as `Column("id", ForeignKey("t.id"))` the type of the column they reference. */
|
|
2847
|
+
function inferForeignKeyTypes(ctx) {
|
|
2848
|
+
for (const table of ctx.tables) {
|
|
2849
|
+
for (const column of table.columns) {
|
|
2850
|
+
if (column.inferFromForeignKey !== true) {
|
|
2851
|
+
continue;
|
|
2852
|
+
}
|
|
2853
|
+
const fk = table.fks.find((candidate) => candidate.columns.length === 1 && candidate.columns[0] === column.key);
|
|
2854
|
+
const target = fk === undefined ? undefined : tableFor(ctx, fk.target);
|
|
2855
|
+
const referenced = fk === undefined ? undefined : fk.target.columns[0];
|
|
2856
|
+
const source = target === undefined
|
|
2857
|
+
? undefined
|
|
2858
|
+
: target.columns.find((candidate) => referenced === undefined
|
|
2859
|
+
? candidate.field.isPrimaryKey
|
|
2860
|
+
: candidate.field.columnName === referenced);
|
|
2861
|
+
if (source === undefined) {
|
|
2862
|
+
warn(ctx, `${table.label}.${column.key}: the column has no type and the column its foreign key points at is not known; it was read as an integer.`);
|
|
2863
|
+
continue;
|
|
2864
|
+
}
|
|
2865
|
+
for (const property of [
|
|
2866
|
+
'type',
|
|
2867
|
+
'maxLength',
|
|
2868
|
+
'maxDigits',
|
|
2869
|
+
'decimalPlaces',
|
|
2870
|
+
'enumName',
|
|
2871
|
+
'arrayDepth',
|
|
2872
|
+
'rangeOf',
|
|
2873
|
+
'unsupportedType',
|
|
2874
|
+
]) {
|
|
2875
|
+
const value = source.field[property];
|
|
2876
|
+
if (value === undefined) {
|
|
2877
|
+
delete column.field[property];
|
|
2878
|
+
}
|
|
2879
|
+
else {
|
|
2880
|
+
column.field[property] =
|
|
2881
|
+
value;
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
function finalizeAll(ctx) {
|
|
2888
|
+
inferForeignKeyTypes(ctx);
|
|
2889
|
+
const warnings = ctx.warnings;
|
|
2890
|
+
const consumed = new Set(ctx.tables.filter((table) => table.consumed));
|
|
2891
|
+
const models = [];
|
|
2892
|
+
const names = new Set();
|
|
2893
|
+
for (const table of ctx.tables) {
|
|
2894
|
+
if (consumed.has(table)) {
|
|
2895
|
+
continue;
|
|
2896
|
+
}
|
|
2897
|
+
if (table.varName !== undefined && table.cls === undefined) {
|
|
2898
|
+
const mapped = ctx.imperative.some((mapping) => mapping.tableVar === table.varName);
|
|
2899
|
+
if (!mapped) {
|
|
2900
|
+
warn(ctx, `${table.tableName}: the Table is not mapped to a class and not used as a secondary table; it is converted as the model ${table.modelName}.`);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
if (names.has(table.modelName)) {
|
|
2904
|
+
warn(ctx, `Two tables are named ${table.modelName}; the second one was skipped.`);
|
|
2905
|
+
continue;
|
|
2906
|
+
}
|
|
2907
|
+
names.add(table.modelName);
|
|
2908
|
+
models.push(finalizeModel(ctx, table));
|
|
2909
|
+
}
|
|
2910
|
+
addStubModels(ctx, models, warnings);
|
|
2911
|
+
return models;
|
|
2912
|
+
}
|
|
2913
|
+
function stripIdSuffix(key) {
|
|
2914
|
+
return key.replace(/(_id|Id|ID)$/, '');
|
|
2915
|
+
}
|
|
2916
|
+
function modelNameForTable(ctx, target) {
|
|
2917
|
+
const found = tableFor(ctx, target);
|
|
2918
|
+
if (found !== undefined) {
|
|
2919
|
+
return found.modelName;
|
|
2920
|
+
}
|
|
2921
|
+
const stubName = toPascalCase(singularize(toSnakeCase(target.table)));
|
|
2922
|
+
ctx.stubTables.set(stubName, target.table);
|
|
2923
|
+
return stubName;
|
|
2924
|
+
}
|
|
2925
|
+
function fieldNameForColumn(target, columnName) {
|
|
2926
|
+
if (target === undefined) {
|
|
2927
|
+
return columnName;
|
|
2928
|
+
}
|
|
2929
|
+
const column = target.columns.find((candidate) => candidate.field.columnName === columnName);
|
|
2930
|
+
return column === undefined ? columnName : column.key;
|
|
2931
|
+
}
|
|
2932
|
+
function primaryKeyOf(table) {
|
|
2933
|
+
if (table === undefined) {
|
|
2934
|
+
return ['id'];
|
|
2935
|
+
}
|
|
2936
|
+
if (table.pk !== undefined) {
|
|
2937
|
+
return table.pk;
|
|
2938
|
+
}
|
|
2939
|
+
const keys = table.columns
|
|
2940
|
+
.filter((column) => column.field.isPrimaryKey)
|
|
2941
|
+
.map((column) => column.key);
|
|
2942
|
+
return keys.length > 0 ? keys : ['id'];
|
|
2943
|
+
}
|
|
2944
|
+
function sameList(first, second) {
|
|
2945
|
+
return (first.length === second.length &&
|
|
2946
|
+
first.every((item, index) => item === second[index]));
|
|
2947
|
+
}
|
|
2948
|
+
function finalizeModel(ctx, table) {
|
|
2949
|
+
const label = table.label;
|
|
2950
|
+
const columns = table.columns;
|
|
2951
|
+
const pkKeys = table.pk ??
|
|
2952
|
+
columns
|
|
2953
|
+
.filter((column) => column.field.isPrimaryKey)
|
|
2954
|
+
.map((column) => column.key);
|
|
2955
|
+
const uniqueKeys = new Set(columns
|
|
2956
|
+
.filter((column) => column.field.isUnique)
|
|
2957
|
+
.map((column) => column.key));
|
|
2958
|
+
for (const index of table.indexes) {
|
|
2959
|
+
const only = index.columns[0];
|
|
2960
|
+
if (index.isUnique && index.columns.length === 1 && only !== undefined) {
|
|
2961
|
+
uniqueKeys.add(only);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
const takenNames = new Set(columns
|
|
2965
|
+
.filter((column) => !table.fks.some((fk) => fk.columns.length === 1 && fk.columns[0] === column.key))
|
|
2966
|
+
.map((column) => column.key));
|
|
2967
|
+
for (const relation of table.m2m) {
|
|
2968
|
+
takenNames.add(relation.name);
|
|
2969
|
+
}
|
|
2970
|
+
const relations = [];
|
|
2971
|
+
const compositeForeignKeys = [];
|
|
2972
|
+
const renamed = new Map();
|
|
2973
|
+
const merged = new Set();
|
|
2974
|
+
for (const fk of table.fks) {
|
|
2975
|
+
const target = tableFor(ctx, fk.target);
|
|
2976
|
+
const targetModel = modelNameForTable(ctx, fk.target);
|
|
2977
|
+
const keyColumns = fk.columns.flatMap((key) => {
|
|
2978
|
+
const column = columns.find((candidate) => candidate.key === key);
|
|
2979
|
+
return column === undefined ? [] : [column];
|
|
2980
|
+
});
|
|
2981
|
+
if (keyColumns.length !== fk.columns.length || keyColumns.length === 0) {
|
|
2982
|
+
warn(ctx, `${label}: the foreign key on (${fk.columns.join(', ')}) names a column that does not exist; it was skipped.`);
|
|
2983
|
+
continue;
|
|
2984
|
+
}
|
|
2985
|
+
const relatedName = fk.reverseName ?? fk.declaredRelatedName;
|
|
2986
|
+
if (fk.columns.length > 1) {
|
|
2987
|
+
const references = fk.target.columns.length > 0
|
|
2988
|
+
? fk.target.columns.map((name) => fieldNameForColumn(target, name))
|
|
2989
|
+
: primaryKeyOf(target);
|
|
2990
|
+
let name = fk.ownerName ?? toCamelCase(targetModel);
|
|
2991
|
+
while (takenNames.has(name)) {
|
|
2992
|
+
name = `${name}_`;
|
|
2993
|
+
}
|
|
2994
|
+
takenNames.add(name);
|
|
2995
|
+
compositeForeignKeys.push({
|
|
2996
|
+
name,
|
|
2997
|
+
targetModel,
|
|
2998
|
+
fields: fk.columns,
|
|
2999
|
+
references,
|
|
3000
|
+
kind: 'foreignKey',
|
|
3001
|
+
isNullable: keyColumns.some((column) => column.field.isNullable),
|
|
3002
|
+
onDelete: fk.onDelete ?? 'noAction',
|
|
3003
|
+
...(fk.onUpdate === undefined ? {} : { onUpdate: fk.onUpdate }),
|
|
3004
|
+
...(relatedName === undefined ? {} : { relatedName }),
|
|
3005
|
+
...(fk.name === undefined ? {} : { constraintName: fk.name }),
|
|
3006
|
+
});
|
|
3007
|
+
continue;
|
|
3008
|
+
}
|
|
3009
|
+
const backing = keyColumns[0];
|
|
3010
|
+
if (merged.has(backing.key)) {
|
|
3011
|
+
warn(ctx, `${label}: the column "${backing.key}" has more than one foreign key; only the first was kept.`);
|
|
3012
|
+
continue;
|
|
3013
|
+
}
|
|
3014
|
+
const stripped = stripIdSuffix(backing.key);
|
|
3015
|
+
let name = fk.ownerName ??
|
|
3016
|
+
(stripped === '' || takenNames.has(stripped) ? backing.key : stripped);
|
|
3017
|
+
if (takenNames.has(name) && name !== backing.key) {
|
|
3018
|
+
name = backing.key;
|
|
3019
|
+
}
|
|
3020
|
+
takenNames.add(name);
|
|
3021
|
+
merged.add(backing.key);
|
|
3022
|
+
renamed.set(backing.key, name);
|
|
3023
|
+
const isPrimary = pkKeys.length === 1 && pkKeys[0] === backing.key;
|
|
3024
|
+
const referenced = fk.target.columns[0];
|
|
3025
|
+
const referencedField = referenced === undefined
|
|
3026
|
+
? undefined
|
|
3027
|
+
: fieldNameForColumn(target, referenced);
|
|
3028
|
+
const isOneToOne = isPrimary || uniqueKeys.has(backing.key) || fk.reverseScalar === true;
|
|
3029
|
+
relations.push({
|
|
3030
|
+
name,
|
|
3031
|
+
kind: isOneToOne ? 'oneToOne' : 'foreignKey',
|
|
3032
|
+
targetModel,
|
|
3033
|
+
columnName: backing.field.columnName,
|
|
3034
|
+
isNullable: backing.field.isNullable && !isPrimary,
|
|
3035
|
+
onDelete: fk.onDelete ?? 'noAction',
|
|
3036
|
+
...(relatedName === undefined ? {} : { relatedName }),
|
|
3037
|
+
...(referencedField === undefined ||
|
|
3038
|
+
target === undefined ||
|
|
3039
|
+
sameList(primaryKeyOf(target), [referencedField])
|
|
3040
|
+
? {}
|
|
3041
|
+
: { toField: referencedField }),
|
|
3042
|
+
...(isPrimary ? { isPrimaryKey: true } : {}),
|
|
3043
|
+
...(fk.onUpdate === undefined ? {} : { onUpdate: fk.onUpdate }),
|
|
3044
|
+
...(fk.name === undefined ? {} : { constraintName: fk.name }),
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
3047
|
+
relations.push(...table.m2m);
|
|
3048
|
+
const fields = columns
|
|
3049
|
+
.filter((column) => !merged.has(column.key))
|
|
3050
|
+
.map((column) => column.field);
|
|
3051
|
+
const fieldByKey = new Map(columns.map((column) => [column.key, column.field]));
|
|
3052
|
+
// Indexes, with columns that became relations renamed.
|
|
3053
|
+
const known = new Set([
|
|
3054
|
+
...fields.map((field) => field.name),
|
|
3055
|
+
...relations.map((relation) => relation.name),
|
|
3056
|
+
...compositeForeignKeys.map((composite) => composite.name),
|
|
3057
|
+
]);
|
|
3058
|
+
const indexes = [];
|
|
3059
|
+
for (const raw of table.indexes) {
|
|
3060
|
+
const indexNames = raw.columns.map((key) => renamed.get(key) ?? key);
|
|
3061
|
+
if (indexNames.some((name) => !known.has(name) && !fieldByKey.has(name))) {
|
|
3062
|
+
continue;
|
|
3063
|
+
}
|
|
3064
|
+
const onRelation = indexNames.length === 1 &&
|
|
3065
|
+
relations.some((relation) => relation.name === indexNames[0]);
|
|
3066
|
+
if (raw.isUnique && onRelation && raw.name === undefined) {
|
|
3067
|
+
continue;
|
|
3068
|
+
}
|
|
3069
|
+
const single = indexNames.length === 1
|
|
3070
|
+
? fields.find((field) => field.name === indexNames[0])
|
|
3071
|
+
: undefined;
|
|
3072
|
+
if (raw.isUnique &&
|
|
3073
|
+
raw.name === undefined &&
|
|
3074
|
+
single !== undefined &&
|
|
3075
|
+
!single.isPrimaryKey) {
|
|
3076
|
+
single.isUnique = true;
|
|
3077
|
+
continue;
|
|
3078
|
+
}
|
|
3079
|
+
const fieldOptions = {};
|
|
3080
|
+
for (const [key, option] of Object.entries(raw.fieldOptions)) {
|
|
3081
|
+
fieldOptions[renamed.get(key) ?? key] = option;
|
|
3082
|
+
}
|
|
3083
|
+
indexes.push({
|
|
3084
|
+
fields: indexNames,
|
|
3085
|
+
isUnique: raw.isUnique,
|
|
3086
|
+
...(raw.name === undefined ? {} : { name: raw.name }),
|
|
3087
|
+
...(raw.method === undefined ? {} : { method: raw.method }),
|
|
3088
|
+
...(Object.keys(fieldOptions).length === 0 ? {} : { fieldOptions }),
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
// Primary key.
|
|
3092
|
+
let compositePrimaryKey;
|
|
3093
|
+
if (pkKeys.length === 0) {
|
|
3094
|
+
warn(ctx, `${label}: the table has no primary key; SQLAlchemy needs one to map a class, so add primary_key=True to a column.`);
|
|
3095
|
+
}
|
|
3096
|
+
else if (pkKeys.length > 1) {
|
|
3097
|
+
compositePrimaryKey = pkKeys.map((key) => renamed.get(key) ?? key);
|
|
3098
|
+
for (const key of pkKeys) {
|
|
3099
|
+
const field = fieldByKey.get(key);
|
|
3100
|
+
if (field !== undefined) {
|
|
3101
|
+
field.isPrimaryKey = false;
|
|
3102
|
+
field.isUnique = false;
|
|
3103
|
+
field.isNullable = false;
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
for (const relation of relations) {
|
|
3107
|
+
delete relation.isPrimaryKey;
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
else if (table.pk !== undefined) {
|
|
3111
|
+
const field = fields.find((candidate) => candidate.name === table.pk?.[0]);
|
|
3112
|
+
if (field !== undefined) {
|
|
3113
|
+
field.isPrimaryKey = true;
|
|
3114
|
+
field.isNullable = false;
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
// A lone integer primary key counts up unless the column says otherwise.
|
|
3118
|
+
if (pkKeys.length === 1) {
|
|
3119
|
+
const onlyKey = pkKeys[0];
|
|
3120
|
+
const column = columns.find((candidate) => candidate.key === onlyKey);
|
|
3121
|
+
if (column !== undefined &&
|
|
3122
|
+
column.wantsAutoIncrement &&
|
|
3123
|
+
!merged.has(column.key) &&
|
|
3124
|
+
column.field.default === undefined &&
|
|
3125
|
+
column.field.generated === undefined) {
|
|
3126
|
+
column.field.default = { kind: 'autoIncrement' };
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
return {
|
|
3130
|
+
name: table.modelName,
|
|
3131
|
+
tableName: table.tableName,
|
|
3132
|
+
appLabel: ctx.options.appLabel,
|
|
3133
|
+
fields,
|
|
3134
|
+
relations,
|
|
3135
|
+
indexes,
|
|
3136
|
+
...(compositePrimaryKey === undefined ? {} : { compositePrimaryKey }),
|
|
3137
|
+
...(table.pkName !== undefined && compositePrimaryKey !== undefined
|
|
3138
|
+
? { primaryKeyName: table.pkName }
|
|
3139
|
+
: {}),
|
|
3140
|
+
...(table.schema === undefined ? {} : { schema: table.schema }),
|
|
3141
|
+
...(compositeForeignKeys.length === 0 ? {} : { compositeForeignKeys }),
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
function addStubModels(ctx, models, warnings) {
|
|
3145
|
+
const known = new Set(models.map((model) => model.name));
|
|
3146
|
+
const stubs = [];
|
|
3147
|
+
for (const model of models) {
|
|
3148
|
+
const references = [
|
|
3149
|
+
...model.relations
|
|
3150
|
+
.filter((relation) => relation.kind !== 'manyToMany')
|
|
3151
|
+
.map((relation) => ({
|
|
3152
|
+
via: relation.name,
|
|
3153
|
+
target: relation.targetModel,
|
|
3154
|
+
})),
|
|
3155
|
+
...(model.compositeForeignKeys ?? []).map((composite) => ({
|
|
3156
|
+
via: composite.name,
|
|
3157
|
+
target: composite.targetModel,
|
|
3158
|
+
})),
|
|
3159
|
+
];
|
|
3160
|
+
for (const { via, target } of references) {
|
|
3161
|
+
if (known.has(target)) {
|
|
3162
|
+
continue;
|
|
3163
|
+
}
|
|
3164
|
+
known.add(target);
|
|
3165
|
+
stubs.push({
|
|
3166
|
+
name: target,
|
|
3167
|
+
tableName: ctx.stubTables.get(target) ?? toSnakeCase(target),
|
|
3168
|
+
appLabel: ctx.options.appLabel,
|
|
3169
|
+
fields: [
|
|
3170
|
+
{
|
|
3171
|
+
name: 'id',
|
|
3172
|
+
columnName: 'id',
|
|
3173
|
+
type: 'int',
|
|
3174
|
+
isPrimaryKey: true,
|
|
3175
|
+
isUnique: false,
|
|
3176
|
+
isNullable: false,
|
|
3177
|
+
isAutoUpdated: false,
|
|
3178
|
+
default: { kind: 'autoIncrement' },
|
|
3179
|
+
},
|
|
3180
|
+
],
|
|
3181
|
+
relations: [],
|
|
3182
|
+
indexes: [],
|
|
3183
|
+
});
|
|
3184
|
+
warnings.push(`${model.name}.${via} references "${target}", which is not a table in the input. ` +
|
|
3185
|
+
`A stub model with an auto-increment id was generated; replace it with the real definition.`);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
// Many-to-many targets are checked when they are linked, so only foreign keys can dangle.
|
|
3189
|
+
models.push(...stubs);
|
|
3190
|
+
}
|
|
3191
|
+
//# sourceMappingURL=sqlalchemy.js.map
|