Skip to content

Commit fa1fbe5

Browse files
committed
fix: add missing relation and sort tables
fix: lint
1 parent ecb5612 commit fa1fbe5

2 files changed

Lines changed: 129 additions & 47 deletions

File tree

src/generator/dbdocs.py

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from pathlib import Path
2323
from typing import Dict, List, Optional
2424

25+
import re
2526
import click
2627

2728

@@ -73,19 +74,22 @@ def _generate_relations_section(self) -> List[str]:
7374

7475
dbml_content = ["// Relationships", ""]
7576

76-
# Filter relations to only include those where both source and target
77-
# tables exist
78-
valid_relations = []
79-
for relation in self.relations:
80-
source_table = relation['source_table']
81-
target_table = relation['target_table']
77+
# Keep only relations where both endpoint tables exist.
78+
valid_relations = [
79+
r for r in self.relations
80+
if self._table_exists(r.get('source_table', ''))
81+
and self._table_exists(r.get('target_table', ''))
82+
]
8283

83-
# Check if tables exist, handling the "_" prefix convention
84-
source_exists = self._table_exists(source_table)
85-
target_exists = self._table_exists(target_table)
84+
# Add inferred simple relations discovered from column names.
85+
inferred = self._infer_relations_from_columns(valid_relations)
86+
if inferred:
87+
valid_relations.extend(inferred)
8688

87-
if source_exists and target_exists:
88-
valid_relations.append(relation)
89+
# Sort relations by normalized source table name, then target table,
90+
# then by source column(s) to produce a stable, grouped ordering in
91+
# the generated DBML file.
92+
valid_relations.sort(key=self._relation_sort_key)
8993

9094
for relation in valid_relations:
9195
relation_line = self._format_relation(relation)
@@ -261,6 +265,66 @@ def _format_relation(self, relation: Dict) -> str:
261265
f'{target_table_normalized}'
262266
)
263267

268+
def _relation_sort_key(self, rel: Dict):
269+
"""Sorting key for relations: source, target, source columns."""
270+
src = self._get_normalized_table_name(rel.get('source_table', ''))
271+
tgt = self._get_normalized_table_name(rel.get('target_table', ''))
272+
if 'source_column' in rel:
273+
sc = rel.get('source_column') or ''
274+
else:
275+
sc = ','.join(rel.get('source_columns', []) or [])
276+
return (src.lower(), tgt.lower(), sc)
277+
278+
def _infer_relations_from_columns(self, existing_relations: List[Dict]) -> List[Dict]:
279+
"""Infer simple FK relations from table column names.
280+
281+
Scans `self.tables` for columns matching `<base>_id` or
282+
`<base>_id_*` and, when a candidate referenced table exists,
283+
yields a simple relation if not already present in
284+
`existing_relations`.
285+
"""
286+
existing_single = set()
287+
for rel in existing_relations:
288+
if 'source_column' in rel:
289+
src_norm = self._get_normalized_table_name(rel['source_table'])
290+
existing_single.add((src_norm, rel['source_column']))
291+
292+
inferred = []
293+
for table_name, table_info in self.tables.items():
294+
src_norm = self._get_normalized_table_name(table_name)
295+
for col in table_info.get('columns', []):
296+
col_name = col.get('name')
297+
if not col_name:
298+
continue
299+
300+
m = re.match(r"^([A-Za-z0-9_]+)_id(?:_.*)?$", col_name)
301+
if not m:
302+
continue
303+
304+
base = m.group(1)
305+
candidates = [f"glpi_{base}", base]
306+
ref_table = None
307+
for cand in candidates:
308+
if self._table_exists(cand):
309+
ref_table = self._get_normalized_table_name(cand)
310+
break
311+
312+
if not ref_table:
313+
continue
314+
315+
if (src_norm, col_name) in existing_single:
316+
continue
317+
318+
inferred.append({
319+
'source_table': table_name,
320+
'target_table': ref_table,
321+
'source_column': col_name,
322+
'target_column': 'id',
323+
'relation_type': '1:n',
324+
})
325+
326+
return inferred
327+
264328
def save_dbdocs_file(self, output_path: str):
265329
"""Save dbdocs configuration to file."""
266330
dbml_content = self.generate_dbdocs_dbml()

src/parser/relation_parser.py

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -77,30 +77,44 @@ def _parse_php_array(self, content: str) -> Dict:
7777
"""
7878
relations = {}
7979

80-
# Split by table entries (looking for 'table_name' => [)
81-
table_entries = re.findall(
82-
r"'([^']+)'\s*=>\s*\[(.*?)(?=\n\s*'[^']+'\s*=>\s*\[|\n\s*\],?\s*$)",
83-
content,
84-
re.DOTALL
85-
)
86-
87-
for table_name, table_relations in table_entries:
88-
relations[table_name] = self._parse_table_relations(
80+
# Find all top-level keys in the array
81+
key_iter = re.finditer(r"'([^']+)'\s*=>\s*\[", content)
82+
for m in key_iter:
83+
key_name = m.group(1)
84+
85+
# Check nesting level to ensure it's a top-level array
86+
prefix = content[:m.start()]
87+
nesting = prefix.count('[') - prefix.count(']')
88+
if nesting != 0:
89+
# This is an inner array (nested). Skip it for top-level
90+
# table extraction.
91+
continue
92+
93+
# Find matching closing bracket for this '[' starting at m.end()-1
94+
start_idx = content.find('[', m.start())
95+
if start_idx == -1:
96+
continue
97+
98+
depth = 0
99+
end_idx = None
100+
for i in range(start_idx, len(content)):
101+
ch = content[i]
102+
if ch == '[':
103+
depth += 1
104+
elif ch == ']':
105+
depth -= 1
106+
if depth == 0:
107+
end_idx = i
108+
break
109+
110+
if end_idx is None:
111+
# Unbalanced brackets — skip this key
112+
continue
113+
114+
table_relations = content[start_idx + 1:end_idx]
115+
relations[key_name] = self._parse_table_relations(
89116
table_relations)
90117

91-
# If no matches found, try a simpler approach
92-
if not table_entries:
93-
# Look for individual table entries
94-
table_entries = re.findall(
95-
r"'([^']+)'\s*=>\s*\[(.*?)\],",
96-
content,
97-
re.DOTALL
98-
)
99-
100-
for table_name, table_relations in table_entries:
101-
relations[table_name] = self._parse_table_relations(
102-
table_relations)
103-
104118
return relations
105119

106120
def _parse_table_relations(self, table_relations: str) -> List[Dict]:
@@ -196,26 +210,30 @@ def get_dbml_relations(self) -> List[Dict]:
196210
continue
197211

198212
# For simple foreign key relations, create DBML ref
199-
# The target table is the one with the foreign key, source is
200-
# the referenced table (corrected logic)
213+
# We want the DBML `Ref` to have the table containing the
214+
# foreign key on the left and the referenced table (usually
215+
# with primary key `id`) on the right. In our parsed structure
216+
# `source_table` is the outer key from the PHP array (the
217+
# referenced table) and `target_table` is the inner key (the
218+
# table that contains the foreign key columns). To produce
219+
# DBML like `appliances.users_id > users.id` we set the
220+
# left-hand side (source_table) to the table with the FK.
201221
if len(foreign_keys) == 1:
202222
dbml_relations.append({
203-
'source_table': source_table, # Referenced table
204-
'target_table': target_table, # Table with foreign key
205-
'source_column': 'id', # Primary key of referenced table
206-
'target_column': foreign_keys[0], # Foreign key column
223+
'source_table': target_table, # Table with foreign key
224+
'target_table': source_table, # Referenced table
225+
'source_column': foreign_keys[0], # Foreign key column
226+
'target_column': 'id', # PK of referenced table
207227
'relation_type': '1:n' # One-to-many relationship
208228
})
209229
elif len(foreign_keys) == 2:
210-
# Handle composite foreign keys (like many-to-many relations)
211-
# This is a simplified approach - in reality, these might
212-
# be more complex
230+
# Handle composite foreign keys (simplified)
213231
dbml_relations.append({
214-
'source_table': source_table, # Referenced table
215-
'target_table': target_table, # Table with foreign keys
216-
# Assuming both point to id columns
217-
'source_columns': ['id', 'id'],
218-
'target_columns': foreign_keys, # Foreign key columns
232+
'source_table': target_table, # Table with foreign keys
233+
'target_table': source_table, # Referenced table
234+
# Assuming both reference id on referenced table
235+
'source_columns': foreign_keys, # Foreign key columns
236+
'target_columns': ['id', 'id'],
219237
'relation_type': 'm:n'
220238
})
221239

0 commit comments

Comments
 (0)