ormbridge 0.3.0 → 0.5.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 +239 -37
- package/dist/cliRunner.js +14 -1
- 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/drizzle.d.ts +19 -0
- package/dist/emitters/drizzle.js +1337 -0
- package/dist/emitters/drizzle.js.map +1 -0
- package/dist/emitters/jsonSchema.d.ts +20 -0
- package/dist/emitters/jsonSchema.js +503 -0
- package/dist/emitters/jsonSchema.js.map +1 -0
- package/dist/emitters/sqlDdl.d.ts +29 -0
- package/dist/emitters/sqlDdl.js +1741 -0
- package/dist/emitters/sqlDdl.js.map +1 -0
- 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/emitters/typeorm.js +53 -1
- package/dist/emitters/typeorm.js.map +1 -1
- package/dist/emitters/zod.d.ts +40 -0
- package/dist/emitters/zod.js +498 -0
- package/dist/emitters/zod.js.map +1 -0
- package/dist/formats.d.ts +11 -1
- package/dist/formats.js +84 -0
- package/dist/formats.js.map +1 -1
- package/dist/index.d.ts +18 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/io.js +82 -8
- package/dist/io.js.map +1 -1
- package/dist/parsers/drizzle.d.ts +12 -0
- package/dist/parsers/drizzle.js +2040 -0
- package/dist/parsers/drizzle.js.map +1 -0
- package/dist/parsers/jsonSchema.d.ts +21 -0
- package/dist/parsers/jsonSchema.js +1432 -0
- package/dist/parsers/jsonSchema.js.map +1 -0
- 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/sqlDdl.d.ts +18 -0
- package/dist/parsers/sqlDdl.js +2302 -0
- package/dist/parsers/sqlDdl.js.map +1 -0
- package/dist/parsers/sqlSyntax.d.ts +79 -0
- package/dist/parsers/sqlSyntax.js +698 -0
- package/dist/parsers/sqlSyntax.js.map +1 -0
- package/dist/parsers/sqlTypes.d.ts +53 -0
- package/dist/parsers/sqlTypes.js +742 -0
- package/dist/parsers/sqlTypes.js.map +1 -0
- package/dist/parsers/sqlalchemy.d.ts +12 -0
- package/dist/parsers/sqlalchemy.js +3191 -0
- package/dist/parsers/sqlalchemy.js.map +1 -0
- package/dist/parsers/typeorm.js +22 -7
- package/dist/parsers/typeorm.js.map +1 -1
- package/dist/prismaOnlyConstructs.d.ts +2 -2
- package/dist/prismaOnlyConstructs.js +5 -5
- package/dist/prismaOnlyConstructs.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,1725 @@
|
|
|
1
|
+
import { prismaOnlyWarnings } from '../prismaOnlyConstructs.js';
|
|
2
|
+
import { limitIdentifier, toPascalCase, toSnakeCase } from '../naming.js';
|
|
3
|
+
import { normalizeSchema } from '../transforms.js';
|
|
4
|
+
export const SQLALCHEMY_STYLES = [
|
|
5
|
+
'sqlalchemy',
|
|
6
|
+
'sqlmodel',
|
|
7
|
+
];
|
|
8
|
+
/** True when a value is one of the supported `--style` values. */
|
|
9
|
+
export function isSqlAlchemyStyle(value) {
|
|
10
|
+
return SQLALCHEMY_STYLES.includes(value);
|
|
11
|
+
}
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Source text building: a tiny expression tree that wraps long calls like Black does
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const MAX_LINE_WIDTH = 88;
|
|
16
|
+
const INDENT = ' ';
|
|
17
|
+
function group(open, items, close = ')') {
|
|
18
|
+
return { open, items, close };
|
|
19
|
+
}
|
|
20
|
+
function call(name, items) {
|
|
21
|
+
return group(`${name}(`, items);
|
|
22
|
+
}
|
|
23
|
+
function flat(node) {
|
|
24
|
+
if (typeof node === 'string') {
|
|
25
|
+
return node;
|
|
26
|
+
}
|
|
27
|
+
return `${node.open}${node.items.map(flat).join(', ')}${node.close}`;
|
|
28
|
+
}
|
|
29
|
+
function renderNode(node, column, indent) {
|
|
30
|
+
if (typeof node === 'string') {
|
|
31
|
+
return node;
|
|
32
|
+
}
|
|
33
|
+
const inline = flat(node);
|
|
34
|
+
if (node.items.length === 0 ||
|
|
35
|
+
(node.force !== true && column + inline.length < MAX_LINE_WIDTH)) {
|
|
36
|
+
return inline;
|
|
37
|
+
}
|
|
38
|
+
const pad = ' '.repeat(indent + INDENT.length);
|
|
39
|
+
const lines = node.items.map((item) => `${pad}${renderNode(item, pad.length, pad.length)},`);
|
|
40
|
+
return `${node.open}\n${lines.join('\n')}\n${' '.repeat(indent)}${node.close}`;
|
|
41
|
+
}
|
|
42
|
+
/** Renders a statement at an indentation level. */
|
|
43
|
+
function statement(node, level) {
|
|
44
|
+
const indent = level * INDENT.length;
|
|
45
|
+
return `${' '.repeat(indent)}${renderNode(node, indent, indent)}`;
|
|
46
|
+
}
|
|
47
|
+
function pyString(value) {
|
|
48
|
+
return JSON.stringify(value);
|
|
49
|
+
}
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Names
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
const PYTHON_KEYWORDS = [
|
|
54
|
+
'False',
|
|
55
|
+
'None',
|
|
56
|
+
'True',
|
|
57
|
+
'and',
|
|
58
|
+
'as',
|
|
59
|
+
'assert',
|
|
60
|
+
'async',
|
|
61
|
+
'await',
|
|
62
|
+
'break',
|
|
63
|
+
'class',
|
|
64
|
+
'continue',
|
|
65
|
+
'def',
|
|
66
|
+
'del',
|
|
67
|
+
'elif',
|
|
68
|
+
'else',
|
|
69
|
+
'except',
|
|
70
|
+
'finally',
|
|
71
|
+
'for',
|
|
72
|
+
'from',
|
|
73
|
+
'global',
|
|
74
|
+
'if',
|
|
75
|
+
'import',
|
|
76
|
+
'in',
|
|
77
|
+
'is',
|
|
78
|
+
'lambda',
|
|
79
|
+
'nonlocal',
|
|
80
|
+
'not',
|
|
81
|
+
'or',
|
|
82
|
+
'pass',
|
|
83
|
+
'raise',
|
|
84
|
+
'return',
|
|
85
|
+
'try',
|
|
86
|
+
'while',
|
|
87
|
+
'with',
|
|
88
|
+
'yield',
|
|
89
|
+
];
|
|
90
|
+
/** Names the generated module imports or relies on; no class, enum or table variable may take them. */
|
|
91
|
+
const MODULE_RESERVED = [
|
|
92
|
+
...PYTHON_KEYWORDS,
|
|
93
|
+
'ARRAY',
|
|
94
|
+
'Any',
|
|
95
|
+
'BigInteger',
|
|
96
|
+
'Base',
|
|
97
|
+
'Boolean',
|
|
98
|
+
'Column',
|
|
99
|
+
'Date',
|
|
100
|
+
'DateTime',
|
|
101
|
+
'DeclarativeBase',
|
|
102
|
+
'Enum',
|
|
103
|
+
'Field',
|
|
104
|
+
'Float',
|
|
105
|
+
'ForeignKey',
|
|
106
|
+
'ForeignKeyConstraint',
|
|
107
|
+
'HSTORE',
|
|
108
|
+
'INET',
|
|
109
|
+
'INT4RANGE',
|
|
110
|
+
'INT8RANGE',
|
|
111
|
+
'DATERANGE',
|
|
112
|
+
'NUMRANGE',
|
|
113
|
+
'TSTZRANGE',
|
|
114
|
+
'Index',
|
|
115
|
+
'Integer',
|
|
116
|
+
'Interval',
|
|
117
|
+
'JSON',
|
|
118
|
+
'LargeBinary',
|
|
119
|
+
'Mapped',
|
|
120
|
+
'Numeric',
|
|
121
|
+
'Optional',
|
|
122
|
+
'PrimaryKeyConstraint',
|
|
123
|
+
'Range',
|
|
124
|
+
'Relationship',
|
|
125
|
+
'SQLModel',
|
|
126
|
+
'String',
|
|
127
|
+
'Table',
|
|
128
|
+
'Text',
|
|
129
|
+
'Time',
|
|
130
|
+
'UniqueConstraint',
|
|
131
|
+
'Uuid',
|
|
132
|
+
'bool',
|
|
133
|
+
'bytes',
|
|
134
|
+
'datetime',
|
|
135
|
+
'decimal',
|
|
136
|
+
'dict',
|
|
137
|
+
'enum',
|
|
138
|
+
'false',
|
|
139
|
+
'float',
|
|
140
|
+
'func',
|
|
141
|
+
'int',
|
|
142
|
+
'list',
|
|
143
|
+
'mapped_column',
|
|
144
|
+
'object',
|
|
145
|
+
'relationship',
|
|
146
|
+
'str',
|
|
147
|
+
'text',
|
|
148
|
+
'true',
|
|
149
|
+
'uuid',
|
|
150
|
+
];
|
|
151
|
+
/** Names that, as an attribute of a class, would shadow something the class body still needs. */
|
|
152
|
+
const CLASS_RESERVED = [
|
|
153
|
+
...MODULE_RESERVED,
|
|
154
|
+
'metadata',
|
|
155
|
+
'registry',
|
|
156
|
+
'self',
|
|
157
|
+
'cls',
|
|
158
|
+
];
|
|
159
|
+
/** Attribute names of `SQLModel` (pydantic's `BaseModel` and SQLModel itself) a field must not shadow. */
|
|
160
|
+
const SQLMODEL_RESERVED = [
|
|
161
|
+
'construct',
|
|
162
|
+
'copy',
|
|
163
|
+
'dict',
|
|
164
|
+
'fields',
|
|
165
|
+
'from_orm',
|
|
166
|
+
'json',
|
|
167
|
+
'parse_file',
|
|
168
|
+
'parse_obj',
|
|
169
|
+
'parse_raw',
|
|
170
|
+
'schema',
|
|
171
|
+
'schema_json',
|
|
172
|
+
'update_forward_refs',
|
|
173
|
+
'validate',
|
|
174
|
+
];
|
|
175
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
176
|
+
/** The first free name: `base`, then `base2`, `base3`, ... A name also counts as taken when `others` holds it. */
|
|
177
|
+
function uniqueName(base, used, others = new Set()) {
|
|
178
|
+
let candidate = base;
|
|
179
|
+
let suffix = 2;
|
|
180
|
+
while (used.has(candidate) || others.has(candidate)) {
|
|
181
|
+
candidate = `${base}${suffix}`;
|
|
182
|
+
suffix += 1;
|
|
183
|
+
}
|
|
184
|
+
used.add(candidate);
|
|
185
|
+
return candidate;
|
|
186
|
+
}
|
|
187
|
+
function pluralize(word) {
|
|
188
|
+
if (/(s|x|z|ch|sh)$/i.test(word)) {
|
|
189
|
+
return `${word}es`;
|
|
190
|
+
}
|
|
191
|
+
if (/[^aeiou]y$/i.test(word)) {
|
|
192
|
+
return `${word.slice(0, -1)}ies`;
|
|
193
|
+
}
|
|
194
|
+
return `${word}s`;
|
|
195
|
+
}
|
|
196
|
+
/** A snake_case Python identifier built from any text. */
|
|
197
|
+
function snakeIdentifier(raw, fallback) {
|
|
198
|
+
const snake = toSnakeCase(raw);
|
|
199
|
+
const safe = snake.replace(/[^A-Za-z0-9_]/g, '_');
|
|
200
|
+
if (safe === '') {
|
|
201
|
+
return fallback;
|
|
202
|
+
}
|
|
203
|
+
return /^[0-9]/.test(safe) ? `${fallback}_${safe}` : safe;
|
|
204
|
+
}
|
|
205
|
+
function sa(context, name) {
|
|
206
|
+
(context.collector?.sa ?? context.saImports).add(name);
|
|
207
|
+
return name;
|
|
208
|
+
}
|
|
209
|
+
function pg(context, name) {
|
|
210
|
+
(context.collector?.pg ?? context.pgImports).add(name);
|
|
211
|
+
return name;
|
|
212
|
+
}
|
|
213
|
+
/** Builds a column type, collecting the imports it needs instead of registering them. */
|
|
214
|
+
function collectTypes(context, build) {
|
|
215
|
+
const uses = { sa: new Set(), pg: new Set() };
|
|
216
|
+
const previous = context.collector;
|
|
217
|
+
context.collector = uses;
|
|
218
|
+
try {
|
|
219
|
+
return { ...build(), uses };
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
context.collector = previous;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** Registers the imports of a column type that is written to the output. */
|
|
226
|
+
function commitTypes(context, type) {
|
|
227
|
+
for (const name of type.uses.sa) {
|
|
228
|
+
context.saImports.add(name);
|
|
229
|
+
}
|
|
230
|
+
for (const name of type.uses.pg) {
|
|
231
|
+
context.pgImports.add(name);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function framework(context, name) {
|
|
235
|
+
context.frameworkImports.add(name);
|
|
236
|
+
return name;
|
|
237
|
+
}
|
|
238
|
+
function mod(context, name) {
|
|
239
|
+
context.modules.add(name);
|
|
240
|
+
return name;
|
|
241
|
+
}
|
|
242
|
+
function optional(context, annotation) {
|
|
243
|
+
context.typingImports.add('Optional');
|
|
244
|
+
return `Optional[${annotation}]`;
|
|
245
|
+
}
|
|
246
|
+
function metadataExpression(context) {
|
|
247
|
+
return context.style === 'sqlmodel' ? 'SQLModel.metadata' : 'Base.metadata';
|
|
248
|
+
}
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Entry point
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
/** Writes the IR as a SQLAlchemy 2.0 (or SQLModel) models.py. */
|
|
253
|
+
export function emitSqlAlchemy(schema, options) {
|
|
254
|
+
const style = options.style ?? 'sqlalchemy';
|
|
255
|
+
const prepared = options.naming === 'normalize' ? normalizeSchema(schema) : schema;
|
|
256
|
+
const context = {
|
|
257
|
+
schema: prepared,
|
|
258
|
+
style,
|
|
259
|
+
naming: options.naming,
|
|
260
|
+
// Views, composite foreign keys, onUpdate actions and database default expressions are written here.
|
|
261
|
+
warnings: prismaOnlyWarnings(prepared, true).filter((warning) => !warning.includes('the onUpdate action') &&
|
|
262
|
+
!warning.includes('has no equivalent here; the relation was dropped') &&
|
|
263
|
+
!warning.includes('the database default expression')),
|
|
264
|
+
moduleNames: new Set(MODULE_RESERVED),
|
|
265
|
+
classes: new Map(),
|
|
266
|
+
enums: new Map(),
|
|
267
|
+
links: [],
|
|
268
|
+
constraintNames: new Set(),
|
|
269
|
+
attributes: new Set(),
|
|
270
|
+
saImports: new Set(),
|
|
271
|
+
pgImports: new Set(),
|
|
272
|
+
collector: undefined,
|
|
273
|
+
frameworkImports: new Set(),
|
|
274
|
+
modules: new Set(),
|
|
275
|
+
typingImports: new Set(),
|
|
276
|
+
associations: [],
|
|
277
|
+
};
|
|
278
|
+
const mapped = prepared.models.filter((model) => model.isView !== true);
|
|
279
|
+
allocateClasses(context, mapped);
|
|
280
|
+
allocateEnums(context);
|
|
281
|
+
allocateAttributes(context, mirroredManyToMany(mapped));
|
|
282
|
+
buildLinks(context, mapped);
|
|
283
|
+
const ordered = orderClasses(context, mapped);
|
|
284
|
+
const blocks = [];
|
|
285
|
+
if (style === 'sqlalchemy') {
|
|
286
|
+
framework(context, 'DeclarativeBase');
|
|
287
|
+
blocks.push('class Base(DeclarativeBase):\n pass');
|
|
288
|
+
}
|
|
289
|
+
for (const info of context.enums.values()) {
|
|
290
|
+
blocks.push(renderEnum(context, info));
|
|
291
|
+
}
|
|
292
|
+
for (const association of context.associations) {
|
|
293
|
+
blocks.push(renderAssociation(context, association));
|
|
294
|
+
}
|
|
295
|
+
for (const info of ordered) {
|
|
296
|
+
blocks.push(renderClass(context, info));
|
|
297
|
+
}
|
|
298
|
+
for (const model of prepared.models) {
|
|
299
|
+
if (model.isView === true) {
|
|
300
|
+
blocks.push(renderView(context, model));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const header = '# Generated by ormbridge. Review the output before creating tables or migrations.';
|
|
304
|
+
const importBlock = buildImports(context);
|
|
305
|
+
const text = `${header}\n\n${importBlock}\n\n\n${blocks.join('\n\n\n')}\n`;
|
|
306
|
+
return { text, warnings: context.warnings };
|
|
307
|
+
}
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Allocation of names
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
function classNameOf(context, model) {
|
|
312
|
+
let name = model.name;
|
|
313
|
+
if (!IDENTIFIER_PATTERN.test(name)) {
|
|
314
|
+
name = toPascalCase(model.name).replace(/[^A-Za-z0-9_]/g, '_');
|
|
315
|
+
if (name === '' || /^[0-9]/.test(name)) {
|
|
316
|
+
name = `Model${name}`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (context.moduleNames.has(name)) {
|
|
320
|
+
const renamed = uniqueName(`${name}Model`, context.moduleNames);
|
|
321
|
+
context.warnings.push(`${model.name}: the name collides with a Python or SQLAlchemy name used by the module; the class was written as ${renamed}.`);
|
|
322
|
+
return renamed;
|
|
323
|
+
}
|
|
324
|
+
context.moduleNames.add(name);
|
|
325
|
+
if (name !== model.name) {
|
|
326
|
+
context.warnings.push(`${model.name}: not a valid Python class name; the class was written as ${name}.`);
|
|
327
|
+
}
|
|
328
|
+
return name;
|
|
329
|
+
}
|
|
330
|
+
function allocateClasses(context, models) {
|
|
331
|
+
for (const model of models) {
|
|
332
|
+
context.classes.set(model.name, {
|
|
333
|
+
model,
|
|
334
|
+
className: classNameOf(context, model),
|
|
335
|
+
used: new Set(context.style === 'sqlmodel'
|
|
336
|
+
? [...CLASS_RESERVED, ...SQLMODEL_RESERVED]
|
|
337
|
+
: CLASS_RESERVED),
|
|
338
|
+
fieldAttrs: new Map(),
|
|
339
|
+
columnAttrs: new Map(),
|
|
340
|
+
relationAttrs: new Map(),
|
|
341
|
+
incoming: [],
|
|
342
|
+
position: 0,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function allocateEnums(context) {
|
|
347
|
+
for (const definition of context.schema.enums) {
|
|
348
|
+
if (definition.values.length === 0) {
|
|
349
|
+
context.warnings.push(`enum ${definition.name}: an enum without values cannot be written; fields that use it became plain strings.`);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
let className = IDENTIFIER_PATTERN.test(definition.name)
|
|
353
|
+
? definition.name
|
|
354
|
+
: toPascalCase(definition.name).replace(/[^A-Za-z0-9_]/g, '_') || 'Enum_';
|
|
355
|
+
if (/^[0-9]/.test(className)) {
|
|
356
|
+
className = `Enum${className}`;
|
|
357
|
+
}
|
|
358
|
+
if (context.moduleNames.has(className)) {
|
|
359
|
+
className = `${className}Enum`;
|
|
360
|
+
}
|
|
361
|
+
className = uniqueName(className, context.moduleNames);
|
|
362
|
+
if (className !== definition.name) {
|
|
363
|
+
context.warnings.push(`enum ${definition.name}: the Python class was named ${className}.`);
|
|
364
|
+
}
|
|
365
|
+
const dbName = definition.dbName ?? definition.name;
|
|
366
|
+
const used = new Set();
|
|
367
|
+
const memberNames = [];
|
|
368
|
+
for (const value of definition.values) {
|
|
369
|
+
let member = value.name.replace(/[^A-Za-z0-9_]/g, '_');
|
|
370
|
+
if (member === '' || /^[0-9]/.test(member) || member.startsWith('_')) {
|
|
371
|
+
member = `v_${member.replace(/^_+/, '')}`;
|
|
372
|
+
}
|
|
373
|
+
if (PYTHON_KEYWORDS.includes(member) ||
|
|
374
|
+
member === 'name' ||
|
|
375
|
+
member === 'value' ||
|
|
376
|
+
member === 'mro') {
|
|
377
|
+
member = `${member}_`;
|
|
378
|
+
}
|
|
379
|
+
const allocated = uniqueName(member, used);
|
|
380
|
+
if (allocated !== value.name) {
|
|
381
|
+
context.warnings.push(`enum ${definition.name}: the member "${value.name}" was written as ${allocated}.`);
|
|
382
|
+
}
|
|
383
|
+
memberNames.push(allocated);
|
|
384
|
+
}
|
|
385
|
+
context.enums.set(definition.name, {
|
|
386
|
+
definition,
|
|
387
|
+
className,
|
|
388
|
+
dbName: context.naming === 'normalize' ? toSnakeCase(dbName) || dbName : dbName,
|
|
389
|
+
memberNames,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/** A snake_case attribute for a name of the source, repaired when it would shadow or collide. */
|
|
394
|
+
function attributeName(context, info, raw, owner) {
|
|
395
|
+
const base = snakeIdentifier(raw, 'field');
|
|
396
|
+
let candidate = base;
|
|
397
|
+
if (info.used.has(candidate) || context.moduleNames.has(candidate)) {
|
|
398
|
+
candidate = `${base}_`;
|
|
399
|
+
}
|
|
400
|
+
const allocated = uniqueName(candidate, info.used, context.moduleNames);
|
|
401
|
+
context.attributes.add(allocated);
|
|
402
|
+
if (allocated !== base && !CLASS_RESERVED.includes(base)) {
|
|
403
|
+
context.warnings.push(`${owner}: the name "${raw}" collides with another attribute of the class or a name used by the module; it was written as ${allocated}.`);
|
|
404
|
+
}
|
|
405
|
+
else if (allocated !== base) {
|
|
406
|
+
context.warnings.push(`${owner}: "${raw}" would shadow a name the class needs; the attribute was written as ${allocated}.`);
|
|
407
|
+
}
|
|
408
|
+
return allocated;
|
|
409
|
+
}
|
|
410
|
+
function allocateAttributes(context, mirrored) {
|
|
411
|
+
for (const info of context.classes.values()) {
|
|
412
|
+
const model = info.model;
|
|
413
|
+
for (const field of model.fields) {
|
|
414
|
+
info.fieldAttrs.set(field.name, attributeName(context, info, field.name, `${model.name}.${field.name}`));
|
|
415
|
+
}
|
|
416
|
+
for (const key of model.compositeForeignKeys ?? []) {
|
|
417
|
+
info.relationAttrs.set(key.name, attributeName(context, info, key.name, `${model.name}.${key.name}`));
|
|
418
|
+
}
|
|
419
|
+
for (const relation of model.relations) {
|
|
420
|
+
if (mirrored.has(`${model.name}.${relation.name}`)) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
info.relationAttrs.set(relation.name, attributeName(context, info, relation.name, `${model.name}.${relation.name}`));
|
|
424
|
+
}
|
|
425
|
+
for (const relation of model.relations) {
|
|
426
|
+
if (relation.kind === 'manyToMany') {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
// The foreign-key column keeps its own name unless the relationship already took it.
|
|
430
|
+
const wanted = snakeIdentifier(relation.columnName, 'column');
|
|
431
|
+
const column = info.used.has(wanted) ? `${wanted}_id` : wanted;
|
|
432
|
+
info.columnAttrs.set(relation.name, attributeName(context, info, column, `${model.name}.${relation.name}`));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
// Keys and links
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
function isKeyRelation(model, relation) {
|
|
440
|
+
return (relation.isPrimaryKey === true ||
|
|
441
|
+
(model.compositePrimaryKey?.includes(relation.name) ?? false));
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Finds the column a foreign key points at: the named column of the target, otherwise its primary key.
|
|
445
|
+
* A key that is itself a relation (multi-table inheritance) is followed to the scalar underneath.
|
|
446
|
+
*/
|
|
447
|
+
function resolveKey(context, modelName, name, depth = 0) {
|
|
448
|
+
const info = context.classes.get(modelName);
|
|
449
|
+
if (info === undefined || depth > 8) {
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
const model = info.model;
|
|
453
|
+
let wanted = name;
|
|
454
|
+
if (wanted === undefined) {
|
|
455
|
+
const primary = model.fields.find((field) => field.isPrimaryKey);
|
|
456
|
+
if (primary !== undefined) {
|
|
457
|
+
wanted = primary.name;
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
const keyRelation = model.relations.find((relation) => relation.isPrimaryKey === true);
|
|
461
|
+
wanted =
|
|
462
|
+
keyRelation?.name ??
|
|
463
|
+
(model.compositePrimaryKey?.length === 1
|
|
464
|
+
? model.compositePrimaryKey[0]
|
|
465
|
+
: undefined);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (wanted === undefined) {
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
const field = model.fields.find((candidate) => candidate.name === wanted) ??
|
|
472
|
+
model.fields.find((candidate) => candidate.columnName === wanted);
|
|
473
|
+
if (field !== undefined) {
|
|
474
|
+
return {
|
|
475
|
+
field,
|
|
476
|
+
columnName: field.columnName,
|
|
477
|
+
attr: info.fieldAttrs.get(field.name) ?? field.name,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const relation = model.relations.find((candidate) => candidate.kind !== 'manyToMany' && candidate.name === wanted);
|
|
481
|
+
if (relation === undefined) {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
const inner = resolveKey(context, relation.targetModel, relation.toField, depth + 1);
|
|
485
|
+
if (inner === undefined) {
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
field: inner.field,
|
|
490
|
+
columnName: relation.columnName,
|
|
491
|
+
attr: info.columnAttrs.get(relation.name) ?? relation.columnName,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function pairKey(first, second) {
|
|
495
|
+
return first <= second
|
|
496
|
+
? `${first}\u0000${second}`
|
|
497
|
+
: `${second}\u0000${first}`;
|
|
498
|
+
}
|
|
499
|
+
function defaultReverseName(owner, kind, sharedTarget, relationName) {
|
|
500
|
+
const base = snakeIdentifier(owner.model.name, 'model');
|
|
501
|
+
const name = kind === 'oneToOne' ? base : pluralize(base);
|
|
502
|
+
return sharedTarget
|
|
503
|
+
? `${name}_${snakeIdentifier(relationName, 'rel')}`
|
|
504
|
+
: name;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* A schema may list both sides of one many-to-many relation (each names the other as its related
|
|
508
|
+
* name). The first side is the owner; the other is the reverse side and gets no relation of its own.
|
|
509
|
+
*/
|
|
510
|
+
function mirroredManyToMany(models) {
|
|
511
|
+
const mirrored = new Set();
|
|
512
|
+
for (const model of models) {
|
|
513
|
+
for (const relation of model.relations) {
|
|
514
|
+
if (relation.kind !== 'manyToMany' ||
|
|
515
|
+
relation.relatedName === undefined ||
|
|
516
|
+
mirrored.has(`${model.name}.${relation.name}`)) {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const other = models.find((candidate) => candidate.name === relation.targetModel);
|
|
520
|
+
const counterpart = other?.relations.find((candidate) => candidate.kind === 'manyToMany' &&
|
|
521
|
+
candidate.name === relation.relatedName &&
|
|
522
|
+
candidate.targetModel === model.name);
|
|
523
|
+
if (other !== undefined && counterpart !== undefined && other !== model) {
|
|
524
|
+
mirrored.add(`${other.name}.${counterpart.name}`);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return mirrored;
|
|
529
|
+
}
|
|
530
|
+
function buildLinks(context, models) {
|
|
531
|
+
const raw = [];
|
|
532
|
+
const mirrored = mirroredManyToMany(models);
|
|
533
|
+
for (const model of models) {
|
|
534
|
+
const owner = context.classes.get(model.name);
|
|
535
|
+
if (owner === undefined) {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
for (const relation of model.relations) {
|
|
539
|
+
const target = context.classes.get(relation.targetModel);
|
|
540
|
+
if (target === undefined) {
|
|
541
|
+
context.warnings.push(`${model.name}.${relation.name}: the target model ${relation.targetModel} is not a mapped table (it is missing or a view); ${relation.kind === 'manyToMany' ? 'the relation was dropped' : 'the foreign key column was written without a ForeignKey or a relationship'}.`);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (relation.kind === 'manyToMany') {
|
|
545
|
+
if (mirrored.has(`${model.name}.${relation.name}`)) {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
raw.push(manyToManyLink(context, owner, target, relation));
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
raw.push(foreignKeyLink(context, owner, target, relation));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
for (const key of model.compositeForeignKeys ?? []) {
|
|
555
|
+
const target = context.classes.get(key.targetModel);
|
|
556
|
+
if (target === undefined) {
|
|
557
|
+
context.warnings.push(`${model.name}.${key.name}: the target model ${key.targetModel} is not a mapped table; the composite foreign key was dropped and its columns kept as plain columns.`);
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
raw.push(compositeLink(context, owner, target, key));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const links = raw.filter((link) => link.localAttrs.length > 0 || link.association !== undefined);
|
|
564
|
+
// More than one foreign key between a pair of classes needs `foreign_keys` on every side.
|
|
565
|
+
const counts = new Map();
|
|
566
|
+
for (const link of links) {
|
|
567
|
+
if (link.kind === 'manyToMany') {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
const key = pairKey(link.owner.model.name, link.target.model.name);
|
|
571
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
572
|
+
}
|
|
573
|
+
// Reverse names: a model that points twice at the same target needs distinct reverse names.
|
|
574
|
+
const sharedTargets = new Map();
|
|
575
|
+
for (const link of links) {
|
|
576
|
+
const key = `${link.owner.model.name}\u0000${link.target.model.name}\u0000${link.kind}`;
|
|
577
|
+
sharedTargets.set(key, (sharedTargets.get(key) ?? 0) + 1);
|
|
578
|
+
}
|
|
579
|
+
for (const link of links) {
|
|
580
|
+
if (link.kind !== 'manyToMany') {
|
|
581
|
+
link.ambiguous =
|
|
582
|
+
(counts.get(pairKey(link.owner.model.name, link.target.model.name)) ??
|
|
583
|
+
0) > 1;
|
|
584
|
+
}
|
|
585
|
+
const sharedKey = `${link.owner.model.name}\u0000${link.target.model.name}\u0000${link.kind}`;
|
|
586
|
+
const shared = (sharedTargets.get(sharedKey) ?? 0) > 1;
|
|
587
|
+
const wanted = link.relatedName === undefined
|
|
588
|
+
? defaultReverseName(link.owner, link.kind, shared, link.name)
|
|
589
|
+
: snakeIdentifier(link.relatedName, 'related');
|
|
590
|
+
const base = link.target.used.has(wanted) || context.moduleNames.has(wanted)
|
|
591
|
+
? `${wanted}_`
|
|
592
|
+
: wanted;
|
|
593
|
+
link.reverseAttr = uniqueName(base, link.target.used, context.moduleNames);
|
|
594
|
+
context.attributes.add(link.reverseAttr);
|
|
595
|
+
link.target.incoming.push(link);
|
|
596
|
+
}
|
|
597
|
+
context.links = links;
|
|
598
|
+
for (const association of context.associations) {
|
|
599
|
+
association.variable = uniqueName(snakeIdentifier(association.tableName, 'association'), context.moduleNames, context.attributes);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function foreignKeyLink(context, owner, target, relation) {
|
|
603
|
+
const key = resolveKey(context, target.model.name, relation.toField);
|
|
604
|
+
if (key === undefined) {
|
|
605
|
+
context.warnings.push(`${owner.model.name}.${relation.name}: ${target.model.name} has no ${relation.toField === undefined ? 'primary key' : `column ${relation.toField}`} to reference; the foreign key points at ${target.model.tableName}.id.`);
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
owner,
|
|
609
|
+
target,
|
|
610
|
+
kind: relation.kind === 'oneToOne' ? 'oneToOne' : 'foreignKey',
|
|
611
|
+
name: relation.name,
|
|
612
|
+
isNullable: relation.isNullable,
|
|
613
|
+
onDelete: relation.onDelete,
|
|
614
|
+
onUpdate: relation.onUpdate,
|
|
615
|
+
constraintName: relation.constraintName,
|
|
616
|
+
relatedName: relation.relatedName,
|
|
617
|
+
forwardAttr: owner.relationAttrs.get(relation.name) ?? relation.name,
|
|
618
|
+
reverseAttr: '',
|
|
619
|
+
localColumns: [relation.columnName],
|
|
620
|
+
localAttrs: [owner.columnAttrs.get(relation.name) ?? relation.columnName],
|
|
621
|
+
remoteColumns: [key?.columnName ?? 'id'],
|
|
622
|
+
remoteAttrs: [key?.attr ?? 'id'],
|
|
623
|
+
composite: false,
|
|
624
|
+
ambiguous: false,
|
|
625
|
+
selfReference: owner === target,
|
|
626
|
+
association: undefined,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
function compositeLink(context, owner, target, key) {
|
|
630
|
+
const localColumns = [];
|
|
631
|
+
const localAttrs = [];
|
|
632
|
+
for (const name of key.fields) {
|
|
633
|
+
const field = owner.model.fields.find((candidate) => candidate.name === name);
|
|
634
|
+
localColumns.push(field?.columnName ?? name);
|
|
635
|
+
localAttrs.push(owner.fieldAttrs.get(name) ?? name);
|
|
636
|
+
}
|
|
637
|
+
const remoteColumns = [];
|
|
638
|
+
const remoteAttrs = [];
|
|
639
|
+
for (const name of key.references) {
|
|
640
|
+
const resolved = resolveKey(context, target.model.name, name);
|
|
641
|
+
remoteColumns.push(resolved?.columnName ?? name);
|
|
642
|
+
remoteAttrs.push(resolved?.attr ?? name);
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
owner,
|
|
646
|
+
target,
|
|
647
|
+
kind: key.kind,
|
|
648
|
+
name: key.name,
|
|
649
|
+
isNullable: key.isNullable,
|
|
650
|
+
onDelete: key.onDelete,
|
|
651
|
+
onUpdate: key.onUpdate,
|
|
652
|
+
constraintName: key.constraintName,
|
|
653
|
+
relatedName: key.relatedName,
|
|
654
|
+
forwardAttr: owner.relationAttrs.get(key.name) ?? key.name,
|
|
655
|
+
reverseAttr: '',
|
|
656
|
+
localColumns,
|
|
657
|
+
localAttrs,
|
|
658
|
+
remoteColumns,
|
|
659
|
+
remoteAttrs,
|
|
660
|
+
composite: true,
|
|
661
|
+
ambiguous: false,
|
|
662
|
+
selfReference: owner === target,
|
|
663
|
+
association: undefined,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function manyToManyLink(context, owner, target, relation) {
|
|
667
|
+
const selfReference = owner === target;
|
|
668
|
+
const ownerSnake = snakeIdentifier(owner.model.name, 'model');
|
|
669
|
+
const targetSnake = snakeIdentifier(target.model.name, 'model');
|
|
670
|
+
const ownerColumn = selfReference
|
|
671
|
+
? `from_${ownerSnake}_id`
|
|
672
|
+
: `${ownerSnake}_id`;
|
|
673
|
+
const targetColumn = selfReference
|
|
674
|
+
? `to_${targetSnake}_id`
|
|
675
|
+
: `${targetSnake}_id`;
|
|
676
|
+
const tableName = `${owner.model.tableName}_${relation.name}`;
|
|
677
|
+
const ownerKey = resolveKey(context, owner.model.name, undefined);
|
|
678
|
+
const targetKey = resolveKey(context, target.model.name, undefined);
|
|
679
|
+
for (const [info, key] of [
|
|
680
|
+
[owner, ownerKey],
|
|
681
|
+
[target, targetKey],
|
|
682
|
+
]) {
|
|
683
|
+
if (key === undefined) {
|
|
684
|
+
context.warnings.push(`${owner.model.name}.${relation.name}: ${info.model.name} has no single-column primary key; the association table column references ${info.model.tableName}.id.`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const association = {
|
|
688
|
+
variable: '',
|
|
689
|
+
tableName,
|
|
690
|
+
ownerColumn,
|
|
691
|
+
targetColumn,
|
|
692
|
+
ownerKey,
|
|
693
|
+
targetKey,
|
|
694
|
+
selfReference,
|
|
695
|
+
};
|
|
696
|
+
context.associations.push(association);
|
|
697
|
+
return {
|
|
698
|
+
owner,
|
|
699
|
+
target,
|
|
700
|
+
kind: 'manyToMany',
|
|
701
|
+
name: relation.name,
|
|
702
|
+
isNullable: false,
|
|
703
|
+
onDelete: 'cascade',
|
|
704
|
+
onUpdate: undefined,
|
|
705
|
+
constraintName: undefined,
|
|
706
|
+
relatedName: relation.relatedName,
|
|
707
|
+
forwardAttr: owner.relationAttrs.get(relation.name) ?? relation.name,
|
|
708
|
+
reverseAttr: '',
|
|
709
|
+
localColumns: [],
|
|
710
|
+
localAttrs: [],
|
|
711
|
+
remoteColumns: [],
|
|
712
|
+
remoteAttrs: [],
|
|
713
|
+
composite: false,
|
|
714
|
+
ambiguous: false,
|
|
715
|
+
selfReference,
|
|
716
|
+
association,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
/** Orders the classes so that a class follows the classes its foreign keys point at where it can. */
|
|
720
|
+
function orderClasses(context, models) {
|
|
721
|
+
const ordered = [];
|
|
722
|
+
const done = new Set();
|
|
723
|
+
const visiting = new Set();
|
|
724
|
+
const visit = (model) => {
|
|
725
|
+
if (done.has(model.name) || visiting.has(model.name)) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
visiting.add(model.name);
|
|
729
|
+
for (const link of context.links) {
|
|
730
|
+
if (link.owner.model === model &&
|
|
731
|
+
link.kind !== 'manyToMany' &&
|
|
732
|
+
link.target.model !== model) {
|
|
733
|
+
visit(link.target.model);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
visiting.delete(model.name);
|
|
737
|
+
done.add(model.name);
|
|
738
|
+
const info = context.classes.get(model.name);
|
|
739
|
+
if (info !== undefined) {
|
|
740
|
+
info.position = ordered.length;
|
|
741
|
+
ordered.push(info);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
for (const model of models) {
|
|
745
|
+
visit(model);
|
|
746
|
+
}
|
|
747
|
+
return ordered;
|
|
748
|
+
}
|
|
749
|
+
// ---------------------------------------------------------------------------
|
|
750
|
+
// Enums
|
|
751
|
+
// ---------------------------------------------------------------------------
|
|
752
|
+
function renderEnum(context, info) {
|
|
753
|
+
mod(context, 'enum');
|
|
754
|
+
const lines = [`class ${info.className}(enum.Enum):`];
|
|
755
|
+
info.definition.values.forEach((value, position) => {
|
|
756
|
+
lines.push(`${INDENT}${info.memberNames[position] ?? value.name} = ${pyString(value.dbValue)}`);
|
|
757
|
+
});
|
|
758
|
+
return lines.join('\n');
|
|
759
|
+
}
|
|
760
|
+
function enumOf(context, field) {
|
|
761
|
+
return field.enumName === undefined
|
|
762
|
+
? undefined
|
|
763
|
+
: context.enums.get(field.enumName);
|
|
764
|
+
}
|
|
765
|
+
function enumType(context, info) {
|
|
766
|
+
mod(context, 'enum');
|
|
767
|
+
return call(sa(context, 'Enum'), [
|
|
768
|
+
info.className,
|
|
769
|
+
`name=${pyString(info.dbName)}`,
|
|
770
|
+
'values_callable=lambda members: [member.value for member in members]',
|
|
771
|
+
]);
|
|
772
|
+
}
|
|
773
|
+
const RANGE_TYPES = {
|
|
774
|
+
int: ['INT4RANGE', 'int'],
|
|
775
|
+
bigInt: ['INT8RANGE', 'int'],
|
|
776
|
+
decimal: ['NUMRANGE', 'decimal.Decimal'],
|
|
777
|
+
date: ['DATERANGE', 'datetime.date'],
|
|
778
|
+
dateTime: ['TSTZRANGE', 'datetime.datetime'],
|
|
779
|
+
};
|
|
780
|
+
function withSqliteFallback(context, type, fallback) {
|
|
781
|
+
return call(`${flat(type)}.with_variant`, [fallback, '"sqlite"']);
|
|
782
|
+
}
|
|
783
|
+
function scalarType(context, model, field) {
|
|
784
|
+
const label = `${model.name}.${field.name}`;
|
|
785
|
+
const enumInfo = enumOf(context, field);
|
|
786
|
+
if (enumInfo !== undefined) {
|
|
787
|
+
return {
|
|
788
|
+
sa: enumType(context, enumInfo),
|
|
789
|
+
py: enumInfo.className,
|
|
790
|
+
needsSaType: true,
|
|
791
|
+
fieldArgs: [],
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
switch (field.type) {
|
|
795
|
+
case 'string':
|
|
796
|
+
return {
|
|
797
|
+
sa: field.maxLength === undefined
|
|
798
|
+
? sa(context, 'String')
|
|
799
|
+
: call(sa(context, 'String'), [String(field.maxLength)]),
|
|
800
|
+
py: 'str',
|
|
801
|
+
needsSaType: false,
|
|
802
|
+
fieldArgs: field.maxLength === undefined
|
|
803
|
+
? []
|
|
804
|
+
: [`max_length=${String(field.maxLength)}`],
|
|
805
|
+
};
|
|
806
|
+
case 'text':
|
|
807
|
+
return {
|
|
808
|
+
sa: sa(context, 'Text'),
|
|
809
|
+
py: 'str',
|
|
810
|
+
needsSaType: true,
|
|
811
|
+
fieldArgs: [],
|
|
812
|
+
};
|
|
813
|
+
case 'int':
|
|
814
|
+
return {
|
|
815
|
+
sa: sa(context, 'Integer'),
|
|
816
|
+
py: 'int',
|
|
817
|
+
needsSaType: false,
|
|
818
|
+
fieldArgs: [],
|
|
819
|
+
};
|
|
820
|
+
case 'bigInt':
|
|
821
|
+
return {
|
|
822
|
+
sa: sa(context, 'BigInteger'),
|
|
823
|
+
py: 'int',
|
|
824
|
+
needsSaType: true,
|
|
825
|
+
fieldArgs: [],
|
|
826
|
+
};
|
|
827
|
+
case 'float':
|
|
828
|
+
return {
|
|
829
|
+
sa: sa(context, 'Float'),
|
|
830
|
+
py: 'float',
|
|
831
|
+
needsSaType: false,
|
|
832
|
+
fieldArgs: [],
|
|
833
|
+
};
|
|
834
|
+
case 'decimal': {
|
|
835
|
+
const hasPrecision = field.maxDigits !== undefined;
|
|
836
|
+
return {
|
|
837
|
+
sa: hasPrecision
|
|
838
|
+
? call(sa(context, 'Numeric'), [
|
|
839
|
+
String(field.maxDigits),
|
|
840
|
+
String(field.decimalPlaces ?? 0),
|
|
841
|
+
])
|
|
842
|
+
: sa(context, 'Numeric'),
|
|
843
|
+
py: `${mod(context, 'decimal')}.Decimal`,
|
|
844
|
+
needsSaType: false,
|
|
845
|
+
fieldArgs: hasPrecision
|
|
846
|
+
? [
|
|
847
|
+
`max_digits=${String(field.maxDigits)}`,
|
|
848
|
+
`decimal_places=${String(field.decimalPlaces ?? 0)}`,
|
|
849
|
+
]
|
|
850
|
+
: [],
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
case 'boolean':
|
|
854
|
+
return {
|
|
855
|
+
sa: sa(context, 'Boolean'),
|
|
856
|
+
py: 'bool',
|
|
857
|
+
needsSaType: false,
|
|
858
|
+
fieldArgs: [],
|
|
859
|
+
};
|
|
860
|
+
case 'dateTime':
|
|
861
|
+
return {
|
|
862
|
+
sa: call(sa(context, 'DateTime'), ['timezone=True']),
|
|
863
|
+
py: `${mod(context, 'datetime')}.datetime`,
|
|
864
|
+
needsSaType: true,
|
|
865
|
+
fieldArgs: [],
|
|
866
|
+
};
|
|
867
|
+
case 'date':
|
|
868
|
+
return {
|
|
869
|
+
sa: sa(context, 'Date'),
|
|
870
|
+
py: `${mod(context, 'datetime')}.date`,
|
|
871
|
+
needsSaType: false,
|
|
872
|
+
fieldArgs: [],
|
|
873
|
+
};
|
|
874
|
+
case 'time':
|
|
875
|
+
return {
|
|
876
|
+
sa: sa(context, 'Time'),
|
|
877
|
+
py: `${mod(context, 'datetime')}.time`,
|
|
878
|
+
needsSaType: false,
|
|
879
|
+
fieldArgs: [],
|
|
880
|
+
};
|
|
881
|
+
case 'duration':
|
|
882
|
+
return {
|
|
883
|
+
sa: sa(context, 'Interval'),
|
|
884
|
+
py: `${mod(context, 'datetime')}.timedelta`,
|
|
885
|
+
needsSaType: false,
|
|
886
|
+
fieldArgs: [],
|
|
887
|
+
};
|
|
888
|
+
case 'uuid':
|
|
889
|
+
return {
|
|
890
|
+
sa: sa(context, 'Uuid'),
|
|
891
|
+
py: `${mod(context, 'uuid')}.UUID`,
|
|
892
|
+
needsSaType: false,
|
|
893
|
+
fieldArgs: [],
|
|
894
|
+
};
|
|
895
|
+
case 'json':
|
|
896
|
+
context.typingImports.add('Any');
|
|
897
|
+
return {
|
|
898
|
+
// None means SQL NULL for a nullable column, not the JSON value null.
|
|
899
|
+
sa: field.isNullable
|
|
900
|
+
? call(sa(context, 'JSON'), ['none_as_null=True'])
|
|
901
|
+
: sa(context, 'JSON'),
|
|
902
|
+
py: 'Any',
|
|
903
|
+
needsSaType: true,
|
|
904
|
+
fieldArgs: [],
|
|
905
|
+
};
|
|
906
|
+
case 'bytes':
|
|
907
|
+
return {
|
|
908
|
+
sa: sa(context, 'LargeBinary'),
|
|
909
|
+
py: 'bytes',
|
|
910
|
+
needsSaType: false,
|
|
911
|
+
fieldArgs: [],
|
|
912
|
+
};
|
|
913
|
+
case 'ipAddress':
|
|
914
|
+
return {
|
|
915
|
+
sa: withSqliteFallback(context, call(`${pg(context, 'INET')}`, []), call(sa(context, 'String'), ['45'])),
|
|
916
|
+
py: 'str',
|
|
917
|
+
needsSaType: true,
|
|
918
|
+
fieldArgs: [],
|
|
919
|
+
};
|
|
920
|
+
case 'hstore':
|
|
921
|
+
return {
|
|
922
|
+
sa: withSqliteFallback(context, call(pg(context, 'HSTORE'), []), call(sa(context, 'JSON'), [])),
|
|
923
|
+
py: 'dict[str, str]',
|
|
924
|
+
needsSaType: true,
|
|
925
|
+
fieldArgs: [],
|
|
926
|
+
};
|
|
927
|
+
case 'range': {
|
|
928
|
+
const entry = RANGE_TYPES[field.rangeOf ?? 'int'];
|
|
929
|
+
if (field.rangeOf === undefined) {
|
|
930
|
+
context.warnings.push(`${label}: the range field has no element type; it was written as INT4RANGE.`);
|
|
931
|
+
}
|
|
932
|
+
const [rangeName, element] = entry ?? ['INT4RANGE', 'int'];
|
|
933
|
+
const elementModule = element.split('.')[0] ?? '';
|
|
934
|
+
if (elementModule === 'decimal' || elementModule === 'datetime') {
|
|
935
|
+
mod(context, elementModule);
|
|
936
|
+
}
|
|
937
|
+
return {
|
|
938
|
+
sa: withSqliteFallback(context, call(pg(context, rangeName), []), call(sa(context, 'JSON'), [])),
|
|
939
|
+
py: `${pg(context, 'Range')}[${element}]`,
|
|
940
|
+
needsSaType: true,
|
|
941
|
+
fieldArgs: [],
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
default:
|
|
945
|
+
return {
|
|
946
|
+
sa: sa(context, 'String'),
|
|
947
|
+
py: 'str',
|
|
948
|
+
needsSaType: false,
|
|
949
|
+
fieldArgs: [],
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/** Wraps the element type of an array column, one level per dimension. */
|
|
954
|
+
function columnType(context, model, field) {
|
|
955
|
+
return collectTypes(context, () => arrayOrScalarType(context, model, field));
|
|
956
|
+
}
|
|
957
|
+
function arrayOrScalarType(context, model, field) {
|
|
958
|
+
const element = scalarType(context, model, field);
|
|
959
|
+
const depth = field.arrayDepth ?? 0;
|
|
960
|
+
if (depth === 0) {
|
|
961
|
+
return element;
|
|
962
|
+
}
|
|
963
|
+
let py = element.py;
|
|
964
|
+
for (let level = 0; level < depth; level += 1) {
|
|
965
|
+
py = `list[${py}]`;
|
|
966
|
+
}
|
|
967
|
+
const arguments_ = [element.sa];
|
|
968
|
+
if (depth > 1) {
|
|
969
|
+
arguments_.push(`dimensions=${String(depth)}`);
|
|
970
|
+
}
|
|
971
|
+
return {
|
|
972
|
+
sa: withSqliteFallback(context, call(sa(context, 'ARRAY'), arguments_), call(sa(context, 'JSON'), [])),
|
|
973
|
+
py,
|
|
974
|
+
needsSaType: true,
|
|
975
|
+
fieldArgs: [],
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
/** The type of a foreign-key column: the type of the key it references. */
|
|
979
|
+
function keyColumnType(context, model, key) {
|
|
980
|
+
if (key === undefined) {
|
|
981
|
+
return collectTypes(context, () => ({
|
|
982
|
+
sa: sa(context, 'Integer'),
|
|
983
|
+
py: 'int',
|
|
984
|
+
needsSaType: false,
|
|
985
|
+
fieldArgs: [],
|
|
986
|
+
}));
|
|
987
|
+
}
|
|
988
|
+
// A key never needs the autoincrement-friendly variant, and an enum or array key is rare enough to share the path.
|
|
989
|
+
return columnType(context, model, { ...key.field });
|
|
990
|
+
}
|
|
991
|
+
/** A Python expression for a JSON text, as a factory that returns a fresh value; undefined when it is not JSON. */
|
|
992
|
+
function jsonFactory(text) {
|
|
993
|
+
let parsed;
|
|
994
|
+
try {
|
|
995
|
+
parsed = JSON.parse(text);
|
|
996
|
+
}
|
|
997
|
+
catch {
|
|
998
|
+
return undefined;
|
|
999
|
+
}
|
|
1000
|
+
if (Array.isArray(parsed) && parsed.length === 0) {
|
|
1001
|
+
return 'list';
|
|
1002
|
+
}
|
|
1003
|
+
if (typeof parsed === 'object' &&
|
|
1004
|
+
parsed !== null &&
|
|
1005
|
+
!Array.isArray(parsed) &&
|
|
1006
|
+
Object.keys(parsed).length === 0) {
|
|
1007
|
+
return 'dict';
|
|
1008
|
+
}
|
|
1009
|
+
return `lambda: ${pythonLiteral(parsed)}`;
|
|
1010
|
+
}
|
|
1011
|
+
function pythonLiteral(value) {
|
|
1012
|
+
if (value === null) {
|
|
1013
|
+
return 'None';
|
|
1014
|
+
}
|
|
1015
|
+
if (typeof value === 'boolean') {
|
|
1016
|
+
return value ? 'True' : 'False';
|
|
1017
|
+
}
|
|
1018
|
+
if (typeof value === 'number') {
|
|
1019
|
+
return String(value);
|
|
1020
|
+
}
|
|
1021
|
+
if (typeof value === 'string') {
|
|
1022
|
+
return pyString(value);
|
|
1023
|
+
}
|
|
1024
|
+
if (Array.isArray(value)) {
|
|
1025
|
+
return `[${value.map(pythonLiteral).join(', ')}]`;
|
|
1026
|
+
}
|
|
1027
|
+
const entries = Object.entries(value).map(([key, entry]) => `${pyString(key)}: ${pythonLiteral(entry)}`);
|
|
1028
|
+
return `{${entries.join(', ')}}`;
|
|
1029
|
+
}
|
|
1030
|
+
function literalDefault(context, model, field, value) {
|
|
1031
|
+
const label = `${model.name}.${field.name}`;
|
|
1032
|
+
const isArray = (field.arrayDepth ?? 0) > 0;
|
|
1033
|
+
if (typeof value === 'boolean' || field.type === 'boolean') {
|
|
1034
|
+
const truth = typeof value === 'boolean'
|
|
1035
|
+
? value
|
|
1036
|
+
: String(value).toLowerCase() === 'true';
|
|
1037
|
+
return {
|
|
1038
|
+
server: call(sa(context, truth ? 'true' : 'false'), []),
|
|
1039
|
+
literal: truth ? 'True' : 'False',
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
if (isArray) {
|
|
1043
|
+
if (value === '[]' || value === '{}') {
|
|
1044
|
+
return { client: 'list', factory: 'list' };
|
|
1045
|
+
}
|
|
1046
|
+
context.warnings.push(`${label}: the array default ${pyString(String(value))} has no portable form and was dropped.`);
|
|
1047
|
+
return {};
|
|
1048
|
+
}
|
|
1049
|
+
if (field.type === 'json') {
|
|
1050
|
+
const text = String(value);
|
|
1051
|
+
const result = {
|
|
1052
|
+
server: call(sa(context, 'text'), [
|
|
1053
|
+
pyString(`'${text.replace(/'/g, "''")}'`),
|
|
1054
|
+
]),
|
|
1055
|
+
};
|
|
1056
|
+
// SQLModel passes the Python default of every field to the INSERT, and a JSON column would
|
|
1057
|
+
// store its None as the JSON value null, so the same value is also given as a factory.
|
|
1058
|
+
const factory = jsonFactory(text);
|
|
1059
|
+
if (factory !== undefined) {
|
|
1060
|
+
result.factory = factory;
|
|
1061
|
+
}
|
|
1062
|
+
return result;
|
|
1063
|
+
}
|
|
1064
|
+
if (field.type === 'hstore') {
|
|
1065
|
+
return value === '{}' ? { client: 'dict', factory: 'dict' } : {};
|
|
1066
|
+
}
|
|
1067
|
+
const numeric = field.type === 'int' ||
|
|
1068
|
+
field.type === 'bigInt' ||
|
|
1069
|
+
field.type === 'float' ||
|
|
1070
|
+
field.type === 'decimal';
|
|
1071
|
+
if (numeric && /^-?\d+(\.\d+)?$/.test(String(value))) {
|
|
1072
|
+
return {
|
|
1073
|
+
server: call(sa(context, 'text'), [pyString(String(value))]),
|
|
1074
|
+
literal: field.type === 'decimal' ? undefined : String(value),
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
const text = String(value);
|
|
1078
|
+
return {
|
|
1079
|
+
server: pyString(text),
|
|
1080
|
+
literal: field.type === 'string' || field.type === 'text'
|
|
1081
|
+
? pyString(text)
|
|
1082
|
+
: undefined,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
function nowExpression(context, field) {
|
|
1086
|
+
const funcName = sa(context, 'func');
|
|
1087
|
+
if (field.type === 'date') {
|
|
1088
|
+
return `${funcName}.current_date()`;
|
|
1089
|
+
}
|
|
1090
|
+
if (field.type === 'time') {
|
|
1091
|
+
return `${funcName}.current_time()`;
|
|
1092
|
+
}
|
|
1093
|
+
return `${funcName}.now()`;
|
|
1094
|
+
}
|
|
1095
|
+
function defaultOf(context, model, field) {
|
|
1096
|
+
const label = `${model.name}.${field.name}`;
|
|
1097
|
+
const result = {};
|
|
1098
|
+
const value = field.default;
|
|
1099
|
+
if (value !== undefined) {
|
|
1100
|
+
switch (value.kind) {
|
|
1101
|
+
case 'autoIncrement':
|
|
1102
|
+
break;
|
|
1103
|
+
case 'now':
|
|
1104
|
+
result.server = nowExpression(context, field);
|
|
1105
|
+
break;
|
|
1106
|
+
case 'uuid':
|
|
1107
|
+
result.client = `${mod(context, 'uuid')}.uuid4`;
|
|
1108
|
+
result.factory = `${mod(context, 'uuid')}.uuid4`;
|
|
1109
|
+
if (value.version !== undefined && value.version !== 4) {
|
|
1110
|
+
context.warnings.push(`${label}: the UUID version ${String(value.version)} default was written as uuid.uuid4.`);
|
|
1111
|
+
}
|
|
1112
|
+
break;
|
|
1113
|
+
case 'enumValue': {
|
|
1114
|
+
const info = enumOf(context, field);
|
|
1115
|
+
const position = info?.definition.values.findIndex((entry) => entry.name === value.value) ?? -1;
|
|
1116
|
+
const member = info?.definition.values[position];
|
|
1117
|
+
if (info !== undefined && member !== undefined) {
|
|
1118
|
+
result.server = pyString(member.dbValue);
|
|
1119
|
+
result.literal = `${info.className}.${info.memberNames[position] ?? member.name}`;
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
context.warnings.push(`${label}: the enum default ${value.value} is not a member of the enum and was dropped.`);
|
|
1123
|
+
}
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
case 'literal':
|
|
1127
|
+
Object.assign(result, literalDefault(context, model, field, value.value));
|
|
1128
|
+
break;
|
|
1129
|
+
case 'dbExpression':
|
|
1130
|
+
if (value.isFunction === true) {
|
|
1131
|
+
context.warnings.push(`${label}: the Prisma default function ${value.expression} has no equivalent here and was dropped.`);
|
|
1132
|
+
}
|
|
1133
|
+
else {
|
|
1134
|
+
result.server = call(sa(context, 'text'), [
|
|
1135
|
+
pyString(value.expression),
|
|
1136
|
+
]);
|
|
1137
|
+
}
|
|
1138
|
+
break;
|
|
1139
|
+
default:
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (field.isAutoUpdated) {
|
|
1144
|
+
result.onUpdate = nowExpression(context, field);
|
|
1145
|
+
if (result.server === undefined && result.client === undefined) {
|
|
1146
|
+
result.client = nowExpression(context, field);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return result;
|
|
1150
|
+
}
|
|
1151
|
+
const ACTIONS = {
|
|
1152
|
+
cascade: 'CASCADE',
|
|
1153
|
+
setNull: 'SET NULL',
|
|
1154
|
+
restrict: 'RESTRICT',
|
|
1155
|
+
noAction: 'NO ACTION',
|
|
1156
|
+
setDefault: 'SET DEFAULT',
|
|
1157
|
+
};
|
|
1158
|
+
function foreignKeyNode(context, spec) {
|
|
1159
|
+
const args = [pyString(spec.target)];
|
|
1160
|
+
args.push(`ondelete=${pyString(ACTIONS[spec.onDelete])}`);
|
|
1161
|
+
if (spec.onUpdate !== undefined) {
|
|
1162
|
+
args.push(`onupdate=${pyString(ACTIONS[spec.onUpdate])}`);
|
|
1163
|
+
}
|
|
1164
|
+
if (spec.name !== undefined) {
|
|
1165
|
+
args.push(`name=${pyString(spec.name)}`);
|
|
1166
|
+
}
|
|
1167
|
+
return call(sa(context, 'ForeignKey'), args);
|
|
1168
|
+
}
|
|
1169
|
+
function hasSingleKey(model) {
|
|
1170
|
+
return (model.compositePrimaryKey === undefined &&
|
|
1171
|
+
model.primaryKeyName === undefined);
|
|
1172
|
+
}
|
|
1173
|
+
function modelHasPrimaryKey(model) {
|
|
1174
|
+
return ((model.compositePrimaryKey?.length ?? 0) > 0 ||
|
|
1175
|
+
model.fields.some((field) => field.isPrimaryKey) ||
|
|
1176
|
+
model.relations.some((relation) => relation.isPrimaryKey === true));
|
|
1177
|
+
}
|
|
1178
|
+
function scalarColumn(context, info, field) {
|
|
1179
|
+
const model = info.model;
|
|
1180
|
+
const label = `${model.name}.${field.name}`;
|
|
1181
|
+
let type = columnType(context, model, field);
|
|
1182
|
+
if (field.generated !== undefined) {
|
|
1183
|
+
context.warnings.push(`${label}: the computed column expression ${field.generated.expression} is not SQL and was dropped; the column was written as a plain column.`);
|
|
1184
|
+
}
|
|
1185
|
+
const isAutoKey = field.default?.kind === 'autoIncrement' &&
|
|
1186
|
+
(field.type === 'int' || field.type === 'bigInt');
|
|
1187
|
+
const singlePrimary = field.isPrimaryKey && hasSingleKey(model);
|
|
1188
|
+
if (field.type === 'bigInt' && isAutoKey && field.isPrimaryKey) {
|
|
1189
|
+
// SQLite only counts up an INTEGER PRIMARY KEY.
|
|
1190
|
+
type = {
|
|
1191
|
+
...type,
|
|
1192
|
+
sa: call('BigInteger().with_variant', ['Integer', '"sqlite"']),
|
|
1193
|
+
};
|
|
1194
|
+
type.uses.sa.add('BigInteger').add('Integer');
|
|
1195
|
+
}
|
|
1196
|
+
if (field.default?.kind === 'autoIncrement' && !field.isPrimaryKey) {
|
|
1197
|
+
context.warnings.push(`${label}: an auto-increment default on a column that is not the primary key has no equivalent here and was dropped.`);
|
|
1198
|
+
}
|
|
1199
|
+
let autoincrement;
|
|
1200
|
+
if (isAutoKey && field.isPrimaryKey) {
|
|
1201
|
+
autoincrement = singlePrimary ? true : undefined;
|
|
1202
|
+
}
|
|
1203
|
+
else if (field.isPrimaryKey &&
|
|
1204
|
+
(field.type === 'int' || field.type === 'bigInt') &&
|
|
1205
|
+
(model.compositePrimaryKey === undefined ||
|
|
1206
|
+
model.compositePrimaryKey.length === 0)) {
|
|
1207
|
+
autoincrement = false;
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
attr: info.fieldAttrs.get(field.name) ?? field.name,
|
|
1211
|
+
name: field.columnName,
|
|
1212
|
+
type,
|
|
1213
|
+
isNullable: field.isNullable && !field.isPrimaryKey,
|
|
1214
|
+
primaryKey: singlePrimary,
|
|
1215
|
+
autoincrement,
|
|
1216
|
+
isUnique: field.isUnique && !field.isPrimaryKey && field.uniqueName === undefined,
|
|
1217
|
+
defaults: defaultOf(context, model, field),
|
|
1218
|
+
foreignKey: undefined,
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
function relationColumn(context, info, relation) {
|
|
1222
|
+
const link = context.links.find((candidate) => candidate.owner === info &&
|
|
1223
|
+
candidate.name === relation.name &&
|
|
1224
|
+
candidate.kind !== 'manyToMany');
|
|
1225
|
+
const target = link?.target;
|
|
1226
|
+
const key = target === undefined || relation.kind === 'manyToMany'
|
|
1227
|
+
? undefined
|
|
1228
|
+
: resolveKey(context, target.model.name, relation.toField);
|
|
1229
|
+
const type = keyColumnType(context, info.model, key);
|
|
1230
|
+
let onDelete = relation.onDelete;
|
|
1231
|
+
if (onDelete === 'setNull' && !relation.isNullable) {
|
|
1232
|
+
context.warnings.push(`${info.model.name}.${relation.name}: onDelete SetNull needs a nullable column; it was written as RESTRICT.`);
|
|
1233
|
+
onDelete = 'restrict';
|
|
1234
|
+
}
|
|
1235
|
+
const asKey = isKeyRelation(info.model, relation);
|
|
1236
|
+
const foreignKey = link === undefined
|
|
1237
|
+
? undefined
|
|
1238
|
+
: {
|
|
1239
|
+
target: `${link.target.model.tableName}.${link.remoteColumns[0] ?? 'id'}`,
|
|
1240
|
+
onDelete,
|
|
1241
|
+
onUpdate: relation.onUpdate,
|
|
1242
|
+
name: relation.constraintName,
|
|
1243
|
+
};
|
|
1244
|
+
return {
|
|
1245
|
+
attr: info.columnAttrs.get(relation.name) ?? relation.columnName,
|
|
1246
|
+
name: relation.columnName,
|
|
1247
|
+
type,
|
|
1248
|
+
isNullable: relation.isNullable && !asKey,
|
|
1249
|
+
primaryKey: relation.isPrimaryKey === true &&
|
|
1250
|
+
hasSingleKey(info.model) &&
|
|
1251
|
+
info.model.fields.every((field) => !field.isPrimaryKey),
|
|
1252
|
+
autoincrement: undefined,
|
|
1253
|
+
isUnique: relation.kind === 'oneToOne' && relation.isPrimaryKey !== true && !asKey,
|
|
1254
|
+
defaults: {},
|
|
1255
|
+
foreignKey,
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
// ---------------------------------------------------------------------------
|
|
1259
|
+
// Rendering of a column: SQLAlchemy style
|
|
1260
|
+
// ---------------------------------------------------------------------------
|
|
1261
|
+
function annotationOf(context, column, nullable) {
|
|
1262
|
+
return nullable ? optional(context, column.type.py) : column.type.py;
|
|
1263
|
+
}
|
|
1264
|
+
/** `name=value`, keeping a long call as the value breakable. */
|
|
1265
|
+
function keyword(name, value) {
|
|
1266
|
+
return typeof value === 'string'
|
|
1267
|
+
? `${name}=${value}`
|
|
1268
|
+
: group(`${name}=${value.open}`, value.items, value.close);
|
|
1269
|
+
}
|
|
1270
|
+
function serverDefaultArgument(server) {
|
|
1271
|
+
return keyword('server_default', server);
|
|
1272
|
+
}
|
|
1273
|
+
function sqlAlchemyColumn(context, column) {
|
|
1274
|
+
const args = [];
|
|
1275
|
+
if (column.attr !== column.name) {
|
|
1276
|
+
args.push(pyString(column.name));
|
|
1277
|
+
}
|
|
1278
|
+
commitTypes(context, column.type);
|
|
1279
|
+
args.push(column.type.sa);
|
|
1280
|
+
if (column.foreignKey !== undefined) {
|
|
1281
|
+
args.push(foreignKeyNode(context, column.foreignKey));
|
|
1282
|
+
}
|
|
1283
|
+
if (column.primaryKey) {
|
|
1284
|
+
args.push('primary_key=True');
|
|
1285
|
+
}
|
|
1286
|
+
if (column.autoincrement !== undefined) {
|
|
1287
|
+
args.push(`autoincrement=${column.autoincrement ? 'True' : 'False'}`);
|
|
1288
|
+
}
|
|
1289
|
+
if (column.isUnique) {
|
|
1290
|
+
args.push('unique=True');
|
|
1291
|
+
}
|
|
1292
|
+
if (column.defaults.client !== undefined) {
|
|
1293
|
+
args.push(`default=${flat(column.defaults.client)}`);
|
|
1294
|
+
}
|
|
1295
|
+
if (column.defaults.server !== undefined) {
|
|
1296
|
+
args.push(serverDefaultArgument(column.defaults.server));
|
|
1297
|
+
}
|
|
1298
|
+
if (column.defaults.onUpdate !== undefined) {
|
|
1299
|
+
args.push(`onupdate=${flat(column.defaults.onUpdate)}`);
|
|
1300
|
+
}
|
|
1301
|
+
const annotation = `${framework(context, 'Mapped')}[${annotationOf(context, column, column.isNullable)}]`;
|
|
1302
|
+
return statement(call(`${column.attr}: ${annotation} = ${framework(context, 'mapped_column')}`, args), 1);
|
|
1303
|
+
}
|
|
1304
|
+
// ---------------------------------------------------------------------------
|
|
1305
|
+
// Rendering of a column: SQLModel style
|
|
1306
|
+
// ---------------------------------------------------------------------------
|
|
1307
|
+
function isPlainAction(action) {
|
|
1308
|
+
return action === 'cascade' || action === 'setNull' || action === 'restrict';
|
|
1309
|
+
}
|
|
1310
|
+
function sqlModelColumn(context, column) {
|
|
1311
|
+
const fk = column.foreignKey;
|
|
1312
|
+
// `Field` knows foreign_key and ondelete, but not onupdate, a constraint name or NO ACTION.
|
|
1313
|
+
const needsColumn = fk !== undefined &&
|
|
1314
|
+
(fk.onUpdate !== undefined ||
|
|
1315
|
+
fk.name !== undefined ||
|
|
1316
|
+
!isPlainAction(fk.onDelete));
|
|
1317
|
+
const args = [];
|
|
1318
|
+
// What the constructor sees, and the nullability SQLModel derives from it.
|
|
1319
|
+
let defaultIsNone = false;
|
|
1320
|
+
if (column.defaults.factory !== undefined) {
|
|
1321
|
+
args.push(`default_factory=${column.defaults.factory}`);
|
|
1322
|
+
}
|
|
1323
|
+
else if (column.defaults.literal !== undefined) {
|
|
1324
|
+
args.push(`default=${column.defaults.literal}`);
|
|
1325
|
+
}
|
|
1326
|
+
else if (column.isNullable ||
|
|
1327
|
+
(column.primaryKey && column.autoincrement === true) ||
|
|
1328
|
+
column.defaults.server !== undefined ||
|
|
1329
|
+
column.defaults.client !== undefined ||
|
|
1330
|
+
column.defaults.onUpdate !== undefined) {
|
|
1331
|
+
// The column is optional, or the database (or the ORM) fills the value in.
|
|
1332
|
+
args.push('default=None');
|
|
1333
|
+
defaultIsNone = true;
|
|
1334
|
+
}
|
|
1335
|
+
const derivedNullable = !column.primaryKey && defaultIsNone;
|
|
1336
|
+
const optionalAnnotation = defaultIsNone || column.isNullable;
|
|
1337
|
+
if (needsColumn && fk !== undefined) {
|
|
1338
|
+
commitTypes(context, column.type);
|
|
1339
|
+
const parts = [
|
|
1340
|
+
pyString(column.name),
|
|
1341
|
+
column.type.sa,
|
|
1342
|
+
foreignKeyNode(context, fk),
|
|
1343
|
+
];
|
|
1344
|
+
if (column.primaryKey) {
|
|
1345
|
+
parts.push('primary_key=True');
|
|
1346
|
+
}
|
|
1347
|
+
parts.push(`nullable=${column.isNullable ? 'True' : 'False'}`);
|
|
1348
|
+
if (column.isUnique) {
|
|
1349
|
+
parts.push('unique=True');
|
|
1350
|
+
}
|
|
1351
|
+
if (column.defaults.client !== undefined &&
|
|
1352
|
+
column.defaults.factory === undefined) {
|
|
1353
|
+
parts.push(`default=${flat(column.defaults.client)}`);
|
|
1354
|
+
}
|
|
1355
|
+
if (column.defaults.server !== undefined) {
|
|
1356
|
+
parts.push(serverDefaultArgument(column.defaults.server));
|
|
1357
|
+
}
|
|
1358
|
+
if (column.defaults.onUpdate !== undefined) {
|
|
1359
|
+
parts.push(`onupdate=${flat(column.defaults.onUpdate)}`);
|
|
1360
|
+
}
|
|
1361
|
+
args.push(call(`sa_column=${sa(context, 'Column')}`, parts));
|
|
1362
|
+
}
|
|
1363
|
+
else {
|
|
1364
|
+
if (column.primaryKey) {
|
|
1365
|
+
args.push('primary_key=True');
|
|
1366
|
+
}
|
|
1367
|
+
if (fk !== undefined) {
|
|
1368
|
+
args.push(`foreign_key=${pyString(fk.target)}`, `ondelete=${pyString(ACTIONS[fk.onDelete])}`);
|
|
1369
|
+
}
|
|
1370
|
+
if (!column.primaryKey && derivedNullable !== column.isNullable) {
|
|
1371
|
+
args.push(`nullable=${column.isNullable ? 'True' : 'False'}`);
|
|
1372
|
+
}
|
|
1373
|
+
if (column.isUnique) {
|
|
1374
|
+
args.push('unique=True');
|
|
1375
|
+
}
|
|
1376
|
+
args.push(...column.type.fieldArgs);
|
|
1377
|
+
if (column.type.needsSaType) {
|
|
1378
|
+
commitTypes(context, column.type);
|
|
1379
|
+
args.push(keyword('sa_type', column.type.sa));
|
|
1380
|
+
}
|
|
1381
|
+
const kwargs = [];
|
|
1382
|
+
if (column.name !== column.attr) {
|
|
1383
|
+
kwargs.push(`"name": ${pyString(column.name)}`);
|
|
1384
|
+
}
|
|
1385
|
+
if (column.autoincrement === false) {
|
|
1386
|
+
kwargs.push('"autoincrement": False');
|
|
1387
|
+
}
|
|
1388
|
+
if (column.defaults.client !== undefined &&
|
|
1389
|
+
column.defaults.factory === undefined) {
|
|
1390
|
+
kwargs.push(`"default": ${flat(column.defaults.client)}`);
|
|
1391
|
+
}
|
|
1392
|
+
if (column.defaults.server !== undefined) {
|
|
1393
|
+
kwargs.push(`"server_default": ${flat(column.defaults.server)}`);
|
|
1394
|
+
}
|
|
1395
|
+
if (column.defaults.onUpdate !== undefined) {
|
|
1396
|
+
kwargs.push(`"onupdate": ${flat(column.defaults.onUpdate)}`);
|
|
1397
|
+
}
|
|
1398
|
+
if (kwargs.length > 0) {
|
|
1399
|
+
args.push(group('sa_column_kwargs={', kwargs, '}'));
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
const annotation = annotationOf(context, column, optionalAnnotation);
|
|
1403
|
+
if (args.length === 0) {
|
|
1404
|
+
return `${INDENT}${column.attr}: ${annotation}`;
|
|
1405
|
+
}
|
|
1406
|
+
framework(context, 'Field');
|
|
1407
|
+
return statement(call(`${column.attr}: ${annotation} = Field`, args), 1);
|
|
1408
|
+
}
|
|
1409
|
+
// ---------------------------------------------------------------------------
|
|
1410
|
+
// Relationships
|
|
1411
|
+
// ---------------------------------------------------------------------------
|
|
1412
|
+
function columnListExpression(info, attrs) {
|
|
1413
|
+
return `[${attrs.map((attr) => `${info.className}.${attr}`).join(', ')}]`;
|
|
1414
|
+
}
|
|
1415
|
+
function relationshipArguments(context, link, side) {
|
|
1416
|
+
const kwargs = [];
|
|
1417
|
+
const back = side === 'forward' ? link.reverseAttr : link.forwardAttr;
|
|
1418
|
+
if (link.association !== undefined) {
|
|
1419
|
+
const association = link.association;
|
|
1420
|
+
kwargs.push(`secondary=${association.variable}`);
|
|
1421
|
+
if (link.selfReference) {
|
|
1422
|
+
const from = side === 'forward' ? association.ownerColumn : association.targetColumn;
|
|
1423
|
+
const to = side === 'forward' ? association.targetColumn : association.ownerColumn;
|
|
1424
|
+
const keyAttr = association.ownerKey?.attr ?? 'id';
|
|
1425
|
+
const cls = link.owner.className;
|
|
1426
|
+
kwargs.push(`primaryjoin=lambda: ${cls}.${keyAttr} == ${association.variable}.c.${from}`, `secondaryjoin=lambda: ${cls}.${keyAttr} == ${association.variable}.c.${to}`);
|
|
1427
|
+
}
|
|
1428
|
+
return orderKwargs(context, back, kwargs);
|
|
1429
|
+
}
|
|
1430
|
+
if (link.ambiguous) {
|
|
1431
|
+
kwargs.push(`foreign_keys=${pyString(columnListExpression(link.owner, link.localAttrs))}`);
|
|
1432
|
+
}
|
|
1433
|
+
if (side === 'forward' && link.selfReference) {
|
|
1434
|
+
kwargs.push(`remote_side=${pyString(link.remoteAttrs.length === 1 ? `${link.target.className}.${link.remoteAttrs[0] ?? 'id'}` : columnListExpression(link.target, link.remoteAttrs))}`);
|
|
1435
|
+
}
|
|
1436
|
+
if (side === 'reverse' &&
|
|
1437
|
+
(link.onDelete === 'cascade' || link.onDelete === 'setNull')) {
|
|
1438
|
+
kwargs.push('passive_deletes=True');
|
|
1439
|
+
}
|
|
1440
|
+
return orderKwargs(context, back, kwargs);
|
|
1441
|
+
}
|
|
1442
|
+
function orderKwargs(_context, back, kwargs) {
|
|
1443
|
+
return [`back_populates=${pyString(back)}`, ...kwargs];
|
|
1444
|
+
}
|
|
1445
|
+
function relationshipStatement(context, link, side) {
|
|
1446
|
+
const attr = side === 'forward' ? link.forwardAttr : link.reverseAttr;
|
|
1447
|
+
const targetClass = side === 'forward' ? link.target.className : link.owner.className;
|
|
1448
|
+
const collection = link.kind === 'manyToMany' ||
|
|
1449
|
+
(side === 'reverse' && link.kind === 'foreignKey');
|
|
1450
|
+
const nullable = side === 'forward'
|
|
1451
|
+
? link.isNullable && link.kind !== 'manyToMany'
|
|
1452
|
+
: link.kind === 'oneToOne';
|
|
1453
|
+
const reference = pyString(targetClass);
|
|
1454
|
+
const annotation = collection
|
|
1455
|
+
? `list[${reference}]`
|
|
1456
|
+
: nullable
|
|
1457
|
+
? optional(context, reference)
|
|
1458
|
+
: reference;
|
|
1459
|
+
const args = relationshipArguments(context, link, side);
|
|
1460
|
+
if (context.style === 'sqlmodel') {
|
|
1461
|
+
const direct = [args[0] ?? ''];
|
|
1462
|
+
const extra = args.slice(1).map((entry) => {
|
|
1463
|
+
const text = flat(entry);
|
|
1464
|
+
const separator = text.indexOf('=');
|
|
1465
|
+
const name = text.slice(0, separator);
|
|
1466
|
+
return `${pyString(name)}: ${text.slice(separator + 1)}`;
|
|
1467
|
+
});
|
|
1468
|
+
if (extra.length > 0) {
|
|
1469
|
+
direct.push(group('sa_relationship_kwargs={', extra, '}'));
|
|
1470
|
+
}
|
|
1471
|
+
framework(context, 'Relationship');
|
|
1472
|
+
return statement(call(`${attr}: ${annotation} = Relationship`, direct), 1);
|
|
1473
|
+
}
|
|
1474
|
+
const mappedAnnotation = `${framework(context, 'Mapped')}[${annotation}]`;
|
|
1475
|
+
return statement(call(`${attr}: ${mappedAnnotation} = ${framework(context, 'relationship')}`, args), 1);
|
|
1476
|
+
}
|
|
1477
|
+
// ---------------------------------------------------------------------------
|
|
1478
|
+
// Constraints and indexes
|
|
1479
|
+
// ---------------------------------------------------------------------------
|
|
1480
|
+
function columnNameOf(info, name) {
|
|
1481
|
+
const field = info.model.fields.find((candidate) => candidate.name === name);
|
|
1482
|
+
if (field !== undefined) {
|
|
1483
|
+
return [field.columnName];
|
|
1484
|
+
}
|
|
1485
|
+
const relation = info.model.relations.find((candidate) => candidate.kind !== 'manyToMany' && candidate.name === name);
|
|
1486
|
+
if (relation !== undefined) {
|
|
1487
|
+
return [relation.columnName];
|
|
1488
|
+
}
|
|
1489
|
+
const composite = (info.model.compositeForeignKeys ?? []).find((candidate) => candidate.name === name);
|
|
1490
|
+
if (composite !== undefined) {
|
|
1491
|
+
return composite.fields.flatMap((fieldName) => columnNameOf(info, fieldName));
|
|
1492
|
+
}
|
|
1493
|
+
return [name];
|
|
1494
|
+
}
|
|
1495
|
+
function tableArguments(context, info) {
|
|
1496
|
+
const model = info.model;
|
|
1497
|
+
const items = [];
|
|
1498
|
+
if (!hasSingleKey(model) && modelHasPrimaryKey(model)) {
|
|
1499
|
+
const keyNames = model.compositePrimaryKey ?? [
|
|
1500
|
+
...model.fields
|
|
1501
|
+
.filter((field) => field.isPrimaryKey)
|
|
1502
|
+
.map((field) => field.name),
|
|
1503
|
+
...model.relations
|
|
1504
|
+
.filter((relation) => relation.isPrimaryKey === true)
|
|
1505
|
+
.map((relation) => relation.name),
|
|
1506
|
+
];
|
|
1507
|
+
const columns = keyNames.flatMap((name) => columnNameOf(info, name));
|
|
1508
|
+
const args = columns.map(pyString);
|
|
1509
|
+
if (model.primaryKeyName !== undefined) {
|
|
1510
|
+
args.push(`name=${pyString(model.primaryKeyName)}`);
|
|
1511
|
+
}
|
|
1512
|
+
items.push(call(sa(context, 'PrimaryKeyConstraint'), args));
|
|
1513
|
+
}
|
|
1514
|
+
for (const link of context.links) {
|
|
1515
|
+
if (link.owner !== info || !link.composite) {
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
const args = [
|
|
1519
|
+
group('[', link.localColumns.map(pyString), ']'),
|
|
1520
|
+
group('[', link.remoteColumns.map((column) => pyString(`${link.target.model.tableName}.${column}`)), ']'),
|
|
1521
|
+
];
|
|
1522
|
+
if (link.constraintName !== undefined) {
|
|
1523
|
+
args.push(`name=${pyString(link.constraintName)}`);
|
|
1524
|
+
}
|
|
1525
|
+
args.push(`ondelete=${pyString(ACTIONS[link.onDelete])}`);
|
|
1526
|
+
if (link.onUpdate !== undefined) {
|
|
1527
|
+
args.push(`onupdate=${pyString(ACTIONS[link.onUpdate])}`);
|
|
1528
|
+
}
|
|
1529
|
+
items.push(call(sa(context, 'ForeignKeyConstraint'), args));
|
|
1530
|
+
}
|
|
1531
|
+
for (const field of model.fields) {
|
|
1532
|
+
if (field.isUnique &&
|
|
1533
|
+
!field.isPrimaryKey &&
|
|
1534
|
+
field.uniqueName !== undefined) {
|
|
1535
|
+
const name = field.uniqueName;
|
|
1536
|
+
context.constraintNames.add(name);
|
|
1537
|
+
items.push(call(sa(context, 'UniqueConstraint'), [
|
|
1538
|
+
pyString(field.columnName),
|
|
1539
|
+
`name=${pyString(name)}`,
|
|
1540
|
+
]));
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
for (const index of model.indexes) {
|
|
1544
|
+
items.push(indexNode(context, info, index));
|
|
1545
|
+
}
|
|
1546
|
+
return items;
|
|
1547
|
+
}
|
|
1548
|
+
function indexNode(context, info, index) {
|
|
1549
|
+
const columns = index.fields.flatMap((name) => columnNameOf(info, name));
|
|
1550
|
+
const table = info.model.tableName;
|
|
1551
|
+
const prefix = index.isUnique ? 'uq' : 'ix';
|
|
1552
|
+
const requested = index.name ?? `${prefix}_${table}_${columns.join('_')}`;
|
|
1553
|
+
const name = uniqueName(limitIdentifier(requested, 63), context.constraintNames);
|
|
1554
|
+
if (index.isUnique) {
|
|
1555
|
+
return call(sa(context, 'UniqueConstraint'), [
|
|
1556
|
+
...columns.map(pyString),
|
|
1557
|
+
`name=${pyString(name)}`,
|
|
1558
|
+
]);
|
|
1559
|
+
}
|
|
1560
|
+
return call(sa(context, 'Index'), [pyString(name), ...columns.map(pyString)]);
|
|
1561
|
+
}
|
|
1562
|
+
// ---------------------------------------------------------------------------
|
|
1563
|
+
// Classes
|
|
1564
|
+
// ---------------------------------------------------------------------------
|
|
1565
|
+
function renderClass(context, info) {
|
|
1566
|
+
const model = info.model;
|
|
1567
|
+
const lines = [];
|
|
1568
|
+
lines.push(context.style === 'sqlmodel'
|
|
1569
|
+
? `class ${info.className}(SQLModel, table=True):`
|
|
1570
|
+
: `class ${info.className}(Base):`);
|
|
1571
|
+
lines.push(`${INDENT}__tablename__ = ${pyString(model.tableName)}`);
|
|
1572
|
+
const tableArgs = tableArguments(context, info);
|
|
1573
|
+
const hasKey = modelHasPrimaryKey(model);
|
|
1574
|
+
const columns = [];
|
|
1575
|
+
for (const field of model.fields) {
|
|
1576
|
+
columns.push(scalarColumn(context, info, field));
|
|
1577
|
+
}
|
|
1578
|
+
const foreignColumns = [];
|
|
1579
|
+
for (const relation of model.relations) {
|
|
1580
|
+
if (relation.kind === 'manyToMany') {
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
foreignColumns.push(relationColumn(context, info, relation));
|
|
1584
|
+
}
|
|
1585
|
+
const postgresOnly = model.fields
|
|
1586
|
+
.filter((field) => (field.arrayDepth ?? 0) > 0 ||
|
|
1587
|
+
field.type === 'hstore' ||
|
|
1588
|
+
field.type === 'range' ||
|
|
1589
|
+
field.type === 'ipAddress')
|
|
1590
|
+
.map((field) => field.name);
|
|
1591
|
+
if (postgresOnly.length > 0) {
|
|
1592
|
+
context.warnings.push(`${model.name}.${postgresOnly.join(', ')}: array, HSTORE, range and IP address columns are PostgreSQL types; they fall back to JSON (or a string) on SQLite.`);
|
|
1593
|
+
}
|
|
1594
|
+
if (!hasKey) {
|
|
1595
|
+
context.warnings.push(`${model.name}: the table has no primary key, which ${context.style === 'sqlmodel' ? 'SQLModel' : 'the SQLAlchemy ORM'} requires; ${context.style === 'sqlmodel' ? 'the first column was marked as the primary key' : 'every required column was declared as the mapper key (the table itself keeps no primary key)'}.`);
|
|
1596
|
+
}
|
|
1597
|
+
const mapperKey = [];
|
|
1598
|
+
const all = [...columns, ...foreignColumns];
|
|
1599
|
+
if (!hasKey && all.length > 0) {
|
|
1600
|
+
if (context.style === 'sqlmodel') {
|
|
1601
|
+
const first = all[0];
|
|
1602
|
+
if (first !== undefined) {
|
|
1603
|
+
first.primaryKey = true;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
else {
|
|
1607
|
+
const required = all.filter((column) => !column.isNullable);
|
|
1608
|
+
for (const column of required.length > 0 ? required : all) {
|
|
1609
|
+
mapperKey.push(column.attr);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
const body = [];
|
|
1614
|
+
for (const column of all) {
|
|
1615
|
+
body.push(context.style === 'sqlmodel'
|
|
1616
|
+
? sqlModelColumn(context, column)
|
|
1617
|
+
: sqlAlchemyColumn(context, column));
|
|
1618
|
+
}
|
|
1619
|
+
const relationships = [];
|
|
1620
|
+
for (const link of context.links) {
|
|
1621
|
+
if (link.owner === info) {
|
|
1622
|
+
relationships.push(relationshipStatement(context, link, 'forward'));
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
for (const link of info.incoming) {
|
|
1626
|
+
relationships.push(relationshipStatement(context, link, 'reverse'));
|
|
1627
|
+
}
|
|
1628
|
+
if (tableArgs.length > 0) {
|
|
1629
|
+
lines.push(statement({
|
|
1630
|
+
open: '__table_args__ = (',
|
|
1631
|
+
items: tableArgs,
|
|
1632
|
+
close: ')',
|
|
1633
|
+
force: true,
|
|
1634
|
+
}, 1));
|
|
1635
|
+
}
|
|
1636
|
+
if (mapperKey.length > 0) {
|
|
1637
|
+
lines.push(`${INDENT}__mapper_args__ = {"primary_key": [${mapperKey.join(', ')}]}`);
|
|
1638
|
+
}
|
|
1639
|
+
lines.push('');
|
|
1640
|
+
lines.push(...body);
|
|
1641
|
+
if (relationships.length > 0) {
|
|
1642
|
+
lines.push('', ...relationships);
|
|
1643
|
+
}
|
|
1644
|
+
if (body.length === 0 && relationships.length === 0) {
|
|
1645
|
+
lines.push(`${INDENT}pass`);
|
|
1646
|
+
}
|
|
1647
|
+
while (lines[lines.length - 1] === '') {
|
|
1648
|
+
lines.pop();
|
|
1649
|
+
}
|
|
1650
|
+
return lines.join('\n');
|
|
1651
|
+
}
|
|
1652
|
+
function renderAssociation(context, association) {
|
|
1653
|
+
const key = (column, resolved, table) => {
|
|
1654
|
+
const target = `${table ?? 'unknown'}.${resolved?.columnName ?? 'id'}`;
|
|
1655
|
+
return call(sa(context, 'Column'), [
|
|
1656
|
+
pyString(column),
|
|
1657
|
+
call(sa(context, 'ForeignKey'), [pyString(target), 'ondelete="CASCADE"']),
|
|
1658
|
+
'primary_key=True',
|
|
1659
|
+
]);
|
|
1660
|
+
};
|
|
1661
|
+
const link = context.links.find((candidate) => candidate.association === association);
|
|
1662
|
+
const node = call(`${association.variable} = ${sa(context, 'Table')}`, [
|
|
1663
|
+
pyString(association.tableName),
|
|
1664
|
+
metadataExpression(context),
|
|
1665
|
+
key(association.ownerColumn, association.ownerKey, link?.owner.model.tableName),
|
|
1666
|
+
key(association.targetColumn, association.targetKey, link?.target.model.tableName),
|
|
1667
|
+
]);
|
|
1668
|
+
return statement(node, 0);
|
|
1669
|
+
}
|
|
1670
|
+
function renderView(context, model) {
|
|
1671
|
+
context.warnings.push(`${model.name}: this is a database view; SQLAlchemy has no declarative view, so it was written as a comment. Create the view in the database and reflect it (Table(${pyString(model.tableName)}, ${metadataExpression(context)}, autoload_with=engine)).`);
|
|
1672
|
+
const lines = [
|
|
1673
|
+
`# View ${model.name} (${model.tableName}) is not mapped: SQLAlchemy has no declarative view construct.`,
|
|
1674
|
+
`# Create the view in the database, then reflect it with`,
|
|
1675
|
+
`# Table(${pyString(model.tableName)}, ${metadataExpression(context)}, autoload_with=engine)`,
|
|
1676
|
+
`# Columns: ${model.fields.map((field) => field.columnName).join(', ') || '(none)'}`,
|
|
1677
|
+
];
|
|
1678
|
+
return lines.join('\n');
|
|
1679
|
+
}
|
|
1680
|
+
// ---------------------------------------------------------------------------
|
|
1681
|
+
// Imports
|
|
1682
|
+
// ---------------------------------------------------------------------------
|
|
1683
|
+
function importLine(module, names) {
|
|
1684
|
+
// isort's default: CONSTANTS first, then Classes, then functions.
|
|
1685
|
+
const rank = (name) => /^[A-Z0-9_]+$/.test(name) && name.length > 1
|
|
1686
|
+
? 0
|
|
1687
|
+
: /^[A-Z]/.test(name)
|
|
1688
|
+
? 1
|
|
1689
|
+
: 2;
|
|
1690
|
+
const sorted = [...names].sort((first, second) => rank(first) - rank(second) || first.localeCompare(second, 'en'));
|
|
1691
|
+
const inline = `from ${module} import ${sorted.join(', ')}`;
|
|
1692
|
+
if (inline.length <= MAX_LINE_WIDTH) {
|
|
1693
|
+
return inline;
|
|
1694
|
+
}
|
|
1695
|
+
return `from ${module} import (\n${sorted.map((name) => `${INDENT}${name},`).join('\n')}\n)`;
|
|
1696
|
+
}
|
|
1697
|
+
function buildImports(context) {
|
|
1698
|
+
const standard = [...context.modules]
|
|
1699
|
+
.sort()
|
|
1700
|
+
.map((name) => `import ${name}`);
|
|
1701
|
+
if (context.typingImports.size > 0) {
|
|
1702
|
+
standard.push(importLine('typing', [...context.typingImports]));
|
|
1703
|
+
}
|
|
1704
|
+
const thirdParty = [];
|
|
1705
|
+
if (context.saImports.size > 0) {
|
|
1706
|
+
thirdParty.push(importLine('sqlalchemy', [...context.saImports]));
|
|
1707
|
+
}
|
|
1708
|
+
if (context.pgImports.size > 0) {
|
|
1709
|
+
thirdParty.push(importLine('sqlalchemy.dialects.postgresql', [...context.pgImports]));
|
|
1710
|
+
}
|
|
1711
|
+
if (context.style === 'sqlalchemy') {
|
|
1712
|
+
if (context.frameworkImports.size > 0) {
|
|
1713
|
+
thirdParty.push(importLine('sqlalchemy.orm', [...context.frameworkImports]));
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
else {
|
|
1717
|
+
const names = new Set(context.frameworkImports);
|
|
1718
|
+
names.add('SQLModel');
|
|
1719
|
+
thirdParty.push(importLine('sqlmodel', [...names]));
|
|
1720
|
+
}
|
|
1721
|
+
return [standard.join('\n'), thirdParty.join('\n')]
|
|
1722
|
+
.filter((block) => block !== '')
|
|
1723
|
+
.join('\n\n');
|
|
1724
|
+
}
|
|
1725
|
+
//# sourceMappingURL=sqlalchemy.js.map
|