mcdview 0.34.2__tar.gz → 0.34.3__tar.gz
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.
- {mcdview-0.34.2 → mcdview-0.34.3}/PKG-INFO +27 -1
- {mcdview-0.34.2 → mcdview-0.34.3}/README.md +26 -0
- {mcdview-0.34.2 → mcdview-0.34.3}/mcdview.py +172 -19
- {mcdview-0.34.2 → mcdview-0.34.3}/pyproject.toml +1 -1
- {mcdview-0.34.2 → mcdview-0.34.3}/.gitignore +0 -0
- {mcdview-0.34.2 → mcdview-0.34.3}/LICENSE +0 -0
- {mcdview-0.34.2 → mcdview-0.34.3}/logo.svg +0 -0
- {mcdview-0.34.2 → mcdview-0.34.3}/templates/explorateur.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcdview
|
|
3
|
-
Version: 0.34.
|
|
3
|
+
Version: 0.34.3
|
|
4
4
|
Summary: Turn a SQL data model into one self-contained, interactive HTML page
|
|
5
5
|
Project-URL: Homepage, https://github.com/Gheop/mcdview
|
|
6
6
|
Project-URL: Demo, https://gheop.github.io/mcdview/
|
|
@@ -356,6 +356,32 @@ file's header.
|
|
|
356
356
|
|
|
357
357
|
## Changelog
|
|
358
358
|
|
|
359
|
+
### v0.34.3 — `--lint` sees the indexes it used to miss (2026-09-27)
|
|
360
|
+
|
|
361
|
+
- `--lint` flagged foreign keys as `unindexed_fk` although an index covered
|
|
362
|
+
them, because some declared indexes were not read. They now are: Rails
|
|
363
|
+
`t.index` and `add_index`; Drizzle `index()`, `uniqueIndex()` and `unique()`
|
|
364
|
+
with `.on()` or `.onOnly()`, and column-level `.unique()`; SQL `UNIQUE` inside
|
|
365
|
+
`CREATE TABLE` (column or table level); MySQL `KEY` / `INDEX` inside
|
|
366
|
+
`CREATE TABLE`, which `mysqldump` writes for every foreign key. Across the test
|
|
367
|
+
corpus (13,630 files, 94,146 FKs), `unindexed_fk` drops from 64,806 to 44,172:
|
|
368
|
+
Rails 6,849 → 232, Drizzle 15,273 → 7,853, MySQL 8,005 → 2,531.
|
|
369
|
+
- phpMyAdmin exports, which add the primary key and indexes afterwards
|
|
370
|
+
(`ALTER TABLE t ADD PRIMARY KEY (id), ADD KEY k (col)`), lost both: nearly
|
|
371
|
+
every table was reported as `missing_pk`. They are read now; across the
|
|
372
|
+
corpus `missing_pk` drops from 20,820 to 8,505.
|
|
373
|
+
- Drizzle composite primary keys (`primaryKey({ columns: [...] })` and the older
|
|
374
|
+
`primaryKey(t.a, t.b)`) are read.
|
|
375
|
+
- The panel lists these indexes, and a foreign key covered by a single-column
|
|
376
|
+
unique index is shown as 1:1.
|
|
377
|
+
- A Drizzle column declared without a name string took the first string found
|
|
378
|
+
further on as its name, for example `cascade` from `{ onDelete: "cascade" }`
|
|
379
|
+
or `number` from `{ mode: "number" }`. It now takes its field name
|
|
380
|
+
(2,613 columns fixed in the corpus).
|
|
381
|
+
- A crafted Drizzle file with unclosed `pgTable(` calls could take minutes to
|
|
382
|
+
parse (3 min for 130 KB). Parsing is now linear in the file size.
|
|
383
|
+
- Table and foreign-key counts are unchanged on every corpus file.
|
|
384
|
+
|
|
359
385
|
### v0.34.2 — Right SQL dialect on large MySQL and Prisma models (2026-09-23)
|
|
360
386
|
|
|
361
387
|
- `--dialect auto` now chooses the dialect on the whole file. On files over
|
|
@@ -334,6 +334,32 @@ file's header.
|
|
|
334
334
|
|
|
335
335
|
## Changelog
|
|
336
336
|
|
|
337
|
+
### v0.34.3 — `--lint` sees the indexes it used to miss (2026-09-27)
|
|
338
|
+
|
|
339
|
+
- `--lint` flagged foreign keys as `unindexed_fk` although an index covered
|
|
340
|
+
them, because some declared indexes were not read. They now are: Rails
|
|
341
|
+
`t.index` and `add_index`; Drizzle `index()`, `uniqueIndex()` and `unique()`
|
|
342
|
+
with `.on()` or `.onOnly()`, and column-level `.unique()`; SQL `UNIQUE` inside
|
|
343
|
+
`CREATE TABLE` (column or table level); MySQL `KEY` / `INDEX` inside
|
|
344
|
+
`CREATE TABLE`, which `mysqldump` writes for every foreign key. Across the test
|
|
345
|
+
corpus (13,630 files, 94,146 FKs), `unindexed_fk` drops from 64,806 to 44,172:
|
|
346
|
+
Rails 6,849 → 232, Drizzle 15,273 → 7,853, MySQL 8,005 → 2,531.
|
|
347
|
+
- phpMyAdmin exports, which add the primary key and indexes afterwards
|
|
348
|
+
(`ALTER TABLE t ADD PRIMARY KEY (id), ADD KEY k (col)`), lost both: nearly
|
|
349
|
+
every table was reported as `missing_pk`. They are read now; across the
|
|
350
|
+
corpus `missing_pk` drops from 20,820 to 8,505.
|
|
351
|
+
- Drizzle composite primary keys (`primaryKey({ columns: [...] })` and the older
|
|
352
|
+
`primaryKey(t.a, t.b)`) are read.
|
|
353
|
+
- The panel lists these indexes, and a foreign key covered by a single-column
|
|
354
|
+
unique index is shown as 1:1.
|
|
355
|
+
- A Drizzle column declared without a name string took the first string found
|
|
356
|
+
further on as its name, for example `cascade` from `{ onDelete: "cascade" }`
|
|
357
|
+
or `number` from `{ mode: "number" }`. It now takes its field name
|
|
358
|
+
(2,613 columns fixed in the corpus).
|
|
359
|
+
- A crafted Drizzle file with unclosed `pgTable(` calls could take minutes to
|
|
360
|
+
parse (3 min for 130 KB). Parsing is now linear in the file size.
|
|
361
|
+
- Table and foreign-key counts are unchanged on every corpus file.
|
|
362
|
+
|
|
337
363
|
### v0.34.2 — Right SQL dialect on large MySQL and Prisma models (2026-09-23)
|
|
338
364
|
|
|
339
365
|
- `--dialect auto` now chooses the dialect on the whole file. On files over
|
|
@@ -90,6 +90,9 @@ RE_FK_TABLE = re.compile(
|
|
|
90
90
|
r'REFERENCES\s+(?:"?(\w+)"?\.)?"?(\w+)"?(?:\s*\(([^)]+)\))?', re.I)
|
|
91
91
|
RE_FK_INLINE = re.compile(
|
|
92
92
|
r'\bREFERENCES\s+(?:"?(\w+)"?\.)?"?(\w+)"?(?:\s*\(([^)]+)\))?', re.I)
|
|
93
|
+
RE_UNIQUE_TABLE = re.compile(
|
|
94
|
+
r'^\s*(?:CONSTRAINT\s+"?(\w+)"?\s+)?UNIQUE\b[^(]*\(([^)]+)\)', re.I)
|
|
95
|
+
RE_UNIQUE_MOT = re.compile(r'\bUNIQUE\b', re.I)
|
|
93
96
|
# an entry that opens with a table constraint keyword is not a column definition
|
|
94
97
|
RE_DEBUT_CONTRAINTE = re.compile(
|
|
95
98
|
r'^\s*(?:CONSTRAINT\s+"?\w+"?\s+)?'
|
|
@@ -413,15 +416,21 @@ def analyser_sql(chemin, src=None):
|
|
|
413
416
|
vues.add((de, scol, vers, dcol))
|
|
414
417
|
fks.append(nouvelle_fk(de, scol, vers, dcol, nom, od, ou))
|
|
415
418
|
|
|
419
|
+
def ajouter_unique(de, nom, cols):
|
|
420
|
+
if cols and not any(ix['cols'] == cols and ix['unique'] for ix in tables[de]['index']):
|
|
421
|
+
tables[de]['index'].append({'nom': nom, 'cols': cols, 'unique': True})
|
|
422
|
+
|
|
416
423
|
for cle, corps in corps_par_cle.items():
|
|
417
424
|
de = resoudre(cle)
|
|
418
425
|
if de not in tables:
|
|
419
426
|
continue
|
|
420
427
|
# this pass only ever yields FKs from a REFERENCES token (RE_FK_TABLE /
|
|
421
|
-
# RE_FK_INLINE both require it)
|
|
422
|
-
#
|
|
423
|
-
#
|
|
424
|
-
|
|
428
|
+
# RE_FK_INLINE both require it) and indexes from a UNIQUE one. A body
|
|
429
|
+
# with neither — every table in a pg_dump, where constraints live in
|
|
430
|
+
# ALTER TABLE — produces nothing, so skip the per-character body scan
|
|
431
|
+
# entirely (it was ~35 % of the parse time)
|
|
432
|
+
haut = corps.upper()
|
|
433
|
+
if 'REFERENCES' not in haut and 'UNIQUE' not in haut:
|
|
425
434
|
continue
|
|
426
435
|
# terminate the body so decouper_corps emits the final entry too (it only
|
|
427
436
|
# flushes the buffer at a depth-0 ')' or comma; a multi-line body has
|
|
@@ -430,6 +439,16 @@ def analyser_sql(chemin, src=None):
|
|
|
430
439
|
entrees, _ = decouper_corps(corps + '\n)')
|
|
431
440
|
for e in entrees:
|
|
432
441
|
e = RE_COMMENTAIRE.sub('', RE_LITTERAL.sub('', e)).strip()
|
|
442
|
+
# UNIQUE in the body is an index too: table-level `[CONSTRAINT n]
|
|
443
|
+
# UNIQUE (a, b)` or column-level `col type UNIQUE` (pg_dump writes
|
|
444
|
+
# them as ALTER TABLE ... ADD CONSTRAINT, read above)
|
|
445
|
+
mu = RE_UNIQUE_TABLE.match(e)
|
|
446
|
+
if mu:
|
|
447
|
+
ajouter_unique(de, mu.group(1) or '', identifiants(mu.group(2)))
|
|
448
|
+
continue
|
|
449
|
+
cm = RE_COLONNE.match(e)
|
|
450
|
+
if cm and not RE_DEBUT_CONTRAINTE.match(e) and RE_UNIQUE_MOT.search(cm.group(2)):
|
|
451
|
+
ajouter_unique(de, '', [cm.group(1)])
|
|
433
452
|
mt = RE_FK_TABLE.match(e)
|
|
434
453
|
if mt:
|
|
435
454
|
scols, dsch, dtab, dcols = mt.groups()
|
|
@@ -580,6 +599,50 @@ def analyser(chemin, dialecte='auto'):
|
|
|
580
599
|
return (*normaliser_casse(tables, fks), dialecte)
|
|
581
600
|
|
|
582
601
|
|
|
602
|
+
RE_ALTER_TETE = re.compile(
|
|
603
|
+
r'ALTER\s+TABLE\s+(?:ONLY\s+)?(?:[`"\[]?(\w+)[`"\]]?\.)?[`"\[]?(\w+)[`"\]]?\s+ADD\b', re.I)
|
|
604
|
+
RE_ALTER_CLE = re.compile(
|
|
605
|
+
r'\bADD\s+(?:CONSTRAINT\s+[`"\[]?\w+[`"\]]?\s+)?'
|
|
606
|
+
r'(PRIMARY\s+KEY|UNIQUE(?:\s+(?:KEY|INDEX))?|KEY|INDEX)\s*'
|
|
607
|
+
r'(?:[`"\[]?(\w+)[`"\]]?\s*)?(?:USING\s+\w+\s*)?\(((?:[^()]|\([^()]*\))*)\)', re.I)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def ajouter_cles_alter(src, tables):
|
|
611
|
+
"""Primary keys and indexes added by ALTER TABLE, as phpMyAdmin exports them:
|
|
612
|
+
`ALTER TABLE t ADD PRIMARY KEY (id), ADD KEY k (a), ADD UNIQUE KEY u (b);`.
|
|
613
|
+
sqlglot cannot parse several ADD KEY in one statement (it keeps a raw
|
|
614
|
+
Command), so these are read here. A PK only fills a table that has none;
|
|
615
|
+
an index already present is not added twice."""
|
|
616
|
+
# a statement ends at the next ';' or, when that is missing, at the next
|
|
617
|
+
# ALTER: bounding it (and caching the ';' position) keeps many unterminated
|
|
618
|
+
# ALTERs linear instead of each one rescanning the rest of the file
|
|
619
|
+
tetes = list(RE_ALTER_TETE.finditer(src))
|
|
620
|
+
fin = -1
|
|
621
|
+
for n, m in enumerate(tetes):
|
|
622
|
+
cle = f"{m.group(1) or 'public'}.{m.group(2)}"
|
|
623
|
+
if cle not in tables:
|
|
624
|
+
continue
|
|
625
|
+
if fin < m.end():
|
|
626
|
+
fin = src.find(';', m.end())
|
|
627
|
+
if fin < 0:
|
|
628
|
+
fin = len(src)
|
|
629
|
+
borne = min(fin, tetes[n + 1].start() if n + 1 < len(tetes) else len(src))
|
|
630
|
+
t = tables[cle]
|
|
631
|
+
for c in RE_ALTER_CLE.finditer(src, m.end() - 3, borne):
|
|
632
|
+
genre = c.group(1).upper()
|
|
633
|
+
cols = [x for x in identifiants(re.sub(r'\(\d+\)|\s+(?:ASC|DESC)\b', '', c.group(3), flags=re.I))
|
|
634
|
+
if x]
|
|
635
|
+
if not cols:
|
|
636
|
+
continue
|
|
637
|
+
if genre.startswith('PRIMARY'):
|
|
638
|
+
if not t['pk']:
|
|
639
|
+
t['pk'] = cols
|
|
640
|
+
continue
|
|
641
|
+
unique = genre.startswith('UNIQUE')
|
|
642
|
+
if not any(ix['cols'] == cols and ix['unique'] == unique for ix in t['index']):
|
|
643
|
+
t['index'].append({'nom': c.group(2) or '', 'cols': cols, 'unique': unique})
|
|
644
|
+
|
|
645
|
+
|
|
583
646
|
def analyser_sqlglot(chemin, dialecte, strict=True, src=None):
|
|
584
647
|
try:
|
|
585
648
|
import logging
|
|
@@ -689,12 +752,25 @@ def analyser_sqlglot(chemin, dialecte, strict=True, src=None):
|
|
|
689
752
|
ref = fk.args.get('reference')
|
|
690
753
|
if ref:
|
|
691
754
|
ajouter_fk(k, [c.name for c in fk.expressions], ref, nom_fk(fk))
|
|
692
|
-
# inline UNIQUE constraints become indexes
|
|
755
|
+
# inline UNIQUE constraints become indexes: table-level `UNIQUE (a, b)`
|
|
756
|
+
# carries its columns, column-level `col INT UNIQUE` has none (its
|
|
757
|
+
# column is the enclosing definition)
|
|
693
758
|
for u in stmt.find_all(exp.UniqueColumnConstraint):
|
|
694
|
-
|
|
695
|
-
|
|
759
|
+
if u.this is not None:
|
|
760
|
+
noms = [e.name for e in getattr(u.this, 'expressions', [])]
|
|
761
|
+
else:
|
|
762
|
+
cd = u.find_ancestor(exp.ColumnDef)
|
|
763
|
+
noms = [cd.name] if cd else []
|
|
696
764
|
if noms:
|
|
697
765
|
tables[k]['index'].append({'nom': '', 'cols': noms, 'unique': True})
|
|
766
|
+
# MySQL declares plain indexes in the body: KEY / INDEX name (cols);
|
|
767
|
+
# mysqldump writes one for every foreign key
|
|
768
|
+
for ix in stmt.find_all(exp.IndexColumnConstraint):
|
|
769
|
+
noms = [e.name for e in ix.expressions if e.name]
|
|
770
|
+
if noms:
|
|
771
|
+
tables[k]['index'].append(
|
|
772
|
+
{'nom': ix.this.name if ix.this is not None else '',
|
|
773
|
+
'cols': noms, 'unique': False})
|
|
698
774
|
elif isinstance(stmt, exp.Create) and stmt.kind == 'INDEX':
|
|
699
775
|
idx = stmt.this
|
|
700
776
|
tbl = idx.args.get('table') if idx is not None else None
|
|
@@ -722,6 +798,8 @@ def analyser_sqlglot(chemin, dialecte, strict=True, src=None):
|
|
|
722
798
|
tables[k]['index'].append(
|
|
723
799
|
{'nom': cont.name if cont else '', 'cols': noms, 'unique': True})
|
|
724
800
|
|
|
801
|
+
ajouter_cles_alter(src, tables)
|
|
802
|
+
|
|
725
803
|
# a reference without a column list points at the target's primary key
|
|
726
804
|
for f in fks:
|
|
727
805
|
if not f['colcible']:
|
|
@@ -1026,6 +1104,30 @@ def singulariser(mot):
|
|
|
1026
1104
|
return mot
|
|
1027
1105
|
|
|
1028
1106
|
|
|
1107
|
+
def index_rails(args):
|
|
1108
|
+
"""One Rails index declaration from what follows `t.index` / `add_index "t",`:
|
|
1109
|
+
the columns (["a", "b"], %w[a b], "a" or :a) then the options (name:,
|
|
1110
|
+
unique:, also the old :name => / :unique => form). None if unreadable."""
|
|
1111
|
+
args = args.strip()
|
|
1112
|
+
if args.startswith('%w'):
|
|
1113
|
+
mw = re.match(r'%w[\[(]([^\])]*)[\])]', args)
|
|
1114
|
+
cols, reste = (mw.group(1).split(), args[mw.end():]) if mw else ([], '')
|
|
1115
|
+
elif args.startswith('['):
|
|
1116
|
+
fin = args.find(']')
|
|
1117
|
+
if fin < 0:
|
|
1118
|
+
return None
|
|
1119
|
+
cols = [a or b for a, b in re.findall(r'["\']([^"\']+)["\']|:(\w+)', args[:fin])]
|
|
1120
|
+
reste = args[fin + 1:]
|
|
1121
|
+
else:
|
|
1122
|
+
mc = re.match(r'["\']([^"\']+)["\']|:(\w+)', args)
|
|
1123
|
+
cols, reste = ([mc.group(1) or mc.group(2)], args[mc.end():]) if mc else ([], '')
|
|
1124
|
+
if not cols:
|
|
1125
|
+
return None
|
|
1126
|
+
mn = re.search(r'name:\s*["\']([^"\']+)["\']|:name\s*=>\s*["\']([^"\']+)["\']', reste)
|
|
1127
|
+
return {'nom': (mn.group(1) or mn.group(2)) if mn else '', 'cols': cols,
|
|
1128
|
+
'unique': bool(re.search(r'unique:\s*true|:unique\s*=>\s*true', reste))}
|
|
1129
|
+
|
|
1130
|
+
|
|
1029
1131
|
def analyser_schema_rb(chemin):
|
|
1030
1132
|
"""Parse a Rails db/schema.rb natively (the create_table / add_foreign_key
|
|
1031
1133
|
DSL is regular). The implicit `id` primary key is added unless `id: false`;
|
|
@@ -1057,7 +1159,7 @@ def analyser_schema_rb(chemin):
|
|
|
1057
1159
|
for cm in re.finditer(r'\b' + re.escape(var) + r'\.(\w+)\s+["\'"]([^"\'"]+)["\'"]([^\n]*)', corps):
|
|
1058
1160
|
typ, cn, reste = cm.groups()
|
|
1059
1161
|
if typ == 'index':
|
|
1060
|
-
continue
|
|
1162
|
+
continue # read below with the array forms
|
|
1061
1163
|
defc = re.search(r'default:\s*("[^"]*"|\'[^\']*\'|[^,\n]+)', reste)
|
|
1062
1164
|
defaut = defc.group(1).strip('\'"') if defc else ''
|
|
1063
1165
|
if typ in ('references', 'belongs_to'):
|
|
@@ -1066,7 +1168,14 @@ def analyser_schema_rb(chemin):
|
|
|
1066
1168
|
cols.append(nouvelle_colonne(cn + '_type', 'string'))
|
|
1067
1169
|
else:
|
|
1068
1170
|
cols.append(nouvelle_colonne(cn, typ, 'null: false' in reste, defaut))
|
|
1069
|
-
|
|
1171
|
+
index = [ix for im in re.finditer(r'\b' + re.escape(var) + r'\.index\b\(?\s*([^\n]*)', corps)
|
|
1172
|
+
if (ix := index_rails(im.group(1)))]
|
|
1173
|
+
tables[cle] = nouvelle_table('public', nom, cols, pk, index=index)
|
|
1174
|
+
# pre-Rails-5 dumps declare indexes after the tables: add_index "t", [...]
|
|
1175
|
+
for m in re.finditer(r'add_index\s*\(?\s*["\':]([\w.]+)["\']?\s*,\s*([^\n]*)', src):
|
|
1176
|
+
ix = index_rails(m.group(2))
|
|
1177
|
+
if ix and f'public.{m.group(1)}' in tables:
|
|
1178
|
+
tables[f'public.{m.group(1)}']['index'].append(ix)
|
|
1070
1179
|
for m in re.finditer(r'add_foreign_key\s+["\']([^"\']+)["\'],\s*["\']([^"\']+)["\']([^\n]*)', src):
|
|
1071
1180
|
det, verst, reste = m.groups()
|
|
1072
1181
|
mp = re.search(r'primary_key:\s*["\']([^"\']+)["\']', reste)
|
|
@@ -1192,6 +1301,12 @@ def analyser_mermaid(chemin):
|
|
|
1192
1301
|
return tables, fks
|
|
1193
1302
|
|
|
1194
1303
|
|
|
1304
|
+
RE_ACCOLADE = re.compile(r'[{}]')
|
|
1305
|
+
RE_DELIM = re.compile(r'[()\[\]{}]')
|
|
1306
|
+
RE_DZ_INDEX = re.compile(r'\b(uniqueIndex|index|unique)\s*\(\s*(?:["\'`]([^"\'`]*)["\'`])?\s*\)'
|
|
1307
|
+
r'\s*\.on(?:Only)?\(')
|
|
1308
|
+
|
|
1309
|
+
|
|
1195
1310
|
def analyser_drizzle(chemin):
|
|
1196
1311
|
"""Parse a Drizzle ORM schema.ts natively (regular enough): each
|
|
1197
1312
|
`pgTable("name", { field: type("col").notNull().primaryKey()
|
|
@@ -1229,35 +1344,73 @@ def analyser_drizzle(chemin):
|
|
|
1229
1344
|
fabriques += re.findall(
|
|
1230
1345
|
r'(?:const|let|var)\s+(\w+)\s*=\s*(?:pg|mysql|sqlite)TableCreator\s*\(', src)
|
|
1231
1346
|
motif_table = '|'.join(re.escape(f) for f in dict.fromkeys(fabriques))
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1347
|
+
appels = list(re.finditer(
|
|
1348
|
+
r'(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:' + motif_table + r')\s*\(\s*'
|
|
1349
|
+
r'["\'`]([^"\'`]+)["\'`]\s*,\s*\{', src))
|
|
1350
|
+
for n, m in enumerate(appels):
|
|
1235
1351
|
var, nom = m.group(1), m.group(2)
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1352
|
+
# a table's scans stop where the next table starts: on unclosed braces
|
|
1353
|
+
# (crafted upload) each one ran to the end of file, O(n²) (3 min / 130 KiB)
|
|
1354
|
+
borne = appels[n + 1].start() if n + 1 < len(appels) else len(src)
|
|
1355
|
+
i, prof = borne, 0 # scan balanced braces for the body
|
|
1356
|
+
for d in RE_ACCOLADE.finditer(src, m.end() - 1, borne):
|
|
1357
|
+
prof += 1 if d.group() == '{' else -1
|
|
1239
1358
|
if prof == 0:
|
|
1359
|
+
i = d.start()
|
|
1240
1360
|
break
|
|
1241
|
-
i += 1
|
|
1242
1361
|
cle = f'public.{nom}'
|
|
1243
1362
|
var_table[var] = cle
|
|
1244
1363
|
var_cols[var] = {}
|
|
1245
|
-
cols, pk = [], []
|
|
1364
|
+
cols, pk, index = [], [], []
|
|
1246
1365
|
for entree in champs(src[m.end():i]):
|
|
1247
1366
|
cm = re.match(r'\s*(\w+)\s*:\s*(\w+)\s*\(', entree)
|
|
1248
1367
|
if not cm:
|
|
1249
1368
|
continue
|
|
1250
1369
|
champ, typ = cm.groups()
|
|
1251
|
-
|
|
1370
|
+
# the column name is the type call's first argument when it is a
|
|
1371
|
+
# string (`text("user_id")`); otherwise the field name. Not the first
|
|
1372
|
+
# string anywhere: `uuid().references(..., { onDelete: "cascade" })`
|
|
1373
|
+
sm = re.match(r'''\s*["'`]([^"'`]+)["'`]''', entree[cm.end():])
|
|
1252
1374
|
col = sm.group(1) if sm else champ
|
|
1253
1375
|
var_cols[var][champ] = col
|
|
1254
1376
|
cols.append(nouvelle_colonne(col, typ, '.notNull(' in entree))
|
|
1255
1377
|
if '.primaryKey(' in entree:
|
|
1256
1378
|
pk.append(col)
|
|
1379
|
+
if re.search(r'\.unique\(\s*(["\'`][^"\'`]*["\'`])?\s*\)', entree):
|
|
1380
|
+
index.append({'nom': '', 'cols': [col], 'unique': True})
|
|
1257
1381
|
rm = re.search(r'\.references\(\s*\(\s*\)\s*=>\s*(\w+)\.(\w+)', entree)
|
|
1258
1382
|
if rm:
|
|
1259
1383
|
refs.append((cle, col, rm.group(1), rm.group(2)))
|
|
1260
|
-
|
|
1384
|
+
# the optional third argument, `(table) => [...]` or `=> ({...})`, holds
|
|
1385
|
+
# the indexes, unique constraints and composite primary key: read up to
|
|
1386
|
+
# the pgTable call's closing parenthesis
|
|
1387
|
+
j, prof = borne, 0
|
|
1388
|
+
for d in RE_DELIM.finditer(src, i + 1, borne):
|
|
1389
|
+
if prof == 0 and d.group() == ')':
|
|
1390
|
+
j = d.start()
|
|
1391
|
+
break
|
|
1392
|
+
prof += 1 if d.group() in '([{' else -1
|
|
1393
|
+
extra, champ_col = src[i + 1:j], var_cols[var]
|
|
1394
|
+
# the arguments of .on(...) end at the next ')'; that position is cached
|
|
1395
|
+
# so that many unclosed .on( do not each rescan to the end (linear)
|
|
1396
|
+
ferme = -1
|
|
1397
|
+
for im in RE_DZ_INDEX.finditer(extra):
|
|
1398
|
+
if ferme < im.end():
|
|
1399
|
+
ferme = extra.find(')', im.end())
|
|
1400
|
+
if ferme < 0:
|
|
1401
|
+
break
|
|
1402
|
+
ixcols = [champ_col.get(f, f) for f in re.findall(r'\w+\.(\w+)', extra[im.end():ferme])]
|
|
1403
|
+
if ixcols:
|
|
1404
|
+
index.append({'nom': im.group(2) or '', 'cols': ixcols,
|
|
1405
|
+
'unique': im.group(1) != 'index'})
|
|
1406
|
+
mp = None if pk else re.search(r'\bprimaryKey\s*\(', extra)
|
|
1407
|
+
if mp:
|
|
1408
|
+
# primaryKey({ columns: [t.a, t.b] }), or the older primaryKey(t.a, t.b):
|
|
1409
|
+
# the columns are the t.x up to the closing ']' or ')' (first match only)
|
|
1410
|
+
fin = min((k for k in (extra.find(']', mp.end()), extra.find(')', mp.end())) if k >= 0),
|
|
1411
|
+
default=len(extra))
|
|
1412
|
+
pk = [champ_col.get(f, f) for f in re.findall(r'\w+\.(\w+)', extra[mp.end():fin])]
|
|
1413
|
+
tables[cle] = nouvelle_table('public', nom, cols, pk, index=index)
|
|
1261
1414
|
|
|
1262
1415
|
for de, col, tvar, tfield in refs:
|
|
1263
1416
|
vers = var_table.get(tvar)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|