Skip to content

Commit 762eb71

Browse files
hf-kkleinclaudeCopilot
authored
feat: Add SQL persistence for MIGs too (similar to AHBs) (#236)
* docs: add MIG SQLModels design plan Design for adding SQLModel support for Message Implementation Guides, mirroring the existing AHB SQLModel implementation patterns. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * feat: add SQLModel support for Message Implementation Guides (MIG) Add SQL model classes for MIGs mirroring the existing AHB SQLModel implementation patterns: - MigCode, MigDataElement, MigDataElementGroup, MigSegment, MigSegmentGroup, MigSegmentGroupLink, MessageImplementationGuide - from_model() and to_model() conversion methods - Position fields for stable list ordering - Self-referential SegmentGroup relationship via link table - SQL-only fields: gueltig_von, gueltig_bis, edifact_format_version Includes roundtrip tests verifying XML -> Pydantic -> SQL -> Pydantic equality for all example MIG files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * docs: add MIG SQLModels documentation to README Update SQL Models section to include MIG support with code example. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * style: fix formatting and line length issues Apply black/isort formatting and fix line-too-long pylint error. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix: disable pylint no-member for SQLModel Relationship fields Pylint 4.0.4 incorrectly reports that Relationship fields (which are lists at runtime) don't have an 'append' member. This is a false positive specific to SQLModel's typing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * chore: add .claude/ to .gitignore 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Update unittests/test_sqlmodels_messageimplementationguide.py Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Claude Opus 4.5 <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent dec11a4 commit 762eb71

File tree

7 files changed

+730
-2
lines changed

7 files changed

+730
-2
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,6 @@ dmypy.json
136136
src/_your_package_version.py
137137

138138
src/_fundamend_version.py
139+
140+
# Claude Code
141+
.claude/

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ Das Ergebnis sieht dann so aus:
122122
```
123123

124124
### SQL Models
125-
Die Daten aus den XML-Dateien (Stand 2025-02-10 nur AHBs) lassen sich auch in Datenbanken persistieren.
125+
Die Daten aus den XML-Dateien lassen sich auch in Datenbanken persistieren.
126126
Die dazu verwendeten [SQLModel](https://sqlmodel.tiangolo.com/)-Klassen lassen sich mit `fundamend[sqlmodels]` installieren.
127127
Instanzen der Pydantic-Klassen lassen sich in SQL-Models überführen und umgekehrt:
128128
```python
@@ -133,6 +133,15 @@ my_sql_model = SqlAnwendungshandbuch.from_model(pydantic_ahb)
133133
pydantic_ahb = my_sql_model.to_model()
134134
```
135135

136+
Analog dazu funktioniert es auch für MIGs:
137+
```python
138+
from fundamend.models.messageimplementationguide import MessageImplementationGuide as PydanticMig
139+
from fundamend.sqlmodels import MessageImplementationGuide as SqlMig
140+
141+
my_sql_model = SqlMig.from_model(pydantic_mig)
142+
pydantic_mig = my_sql_model.to_model()
143+
```
144+
136145
#### Befüllen einer Datenbank mit AHB-Informationen
137146
In den XML-Rohdaten sind die Informationen aus den AHBs theoretisch beliebig tief verschachtelt, weil jede Segmentgruppe ihrerseits wieder Segmentgruppen enthalten kann.
138147
Diese Rekursion ist so auch in den SQL-Model-Klassen und der Datenbank abgebildet.
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# MIG SQLModels Design
2+
3+
## Overview
4+
5+
Add SQLModel support for Message Implementation Guides (MIGs), mirroring the existing AHB SQLModel implementation. This enables persisting MIG data to SQL databases with the same patterns used for AHBs.
6+
7+
## File Structure
8+
9+
Create new file: `src/fundamend/sqlmodels/messageimplementationguide.py`
10+
11+
## Classes
12+
13+
| SQL Model Class | Pydantic Source | Has `position`? |
14+
|-----------------|-----------------|-----------------|
15+
| `MigCode` | `models.messageimplementationguide.Code` | Yes |
16+
| `MigDataElement` | `models.messageimplementationguide.DataElement` | Yes |
17+
| `MigDataElementGroup` | `models.messageimplementationguide.DataElementGroup` | Yes |
18+
| `MigSegment` | `models.messageimplementationguide.Segment` | Yes |
19+
| `MigSegmentGroupLink` | (artificial join table) | No |
20+
| `MigSegmentGroup` | `models.messageimplementationguide.SegmentGroup` | Yes |
21+
| `MessageImplementationGuide` | `models.messageimplementationguide.MessageImplementationGuide` | No |
22+
23+
## Field Mappings
24+
25+
### MigCode
26+
- `primary_key: UUID`
27+
- `name: str`
28+
- `description: str | None`
29+
- `value: str | None`
30+
- `position: Optional[int]`
31+
- FK: `data_element_primary_key`
32+
33+
### MigDataElement
34+
- `primary_key: UUID`
35+
- `id: str`
36+
- `name: str`
37+
- `description: str | None`
38+
- `status_std: str` (MigStatus stored as string)
39+
- `status_specification: str`
40+
- `format_std: str`
41+
- `format_specification: str`
42+
- `codes: list[MigCode]`
43+
- `position: Optional[int]`
44+
- FK: `data_element_group_primary_key` (optional)
45+
- FK: `segment_primary_key` (optional)
46+
47+
### MigDataElementGroup
48+
- `primary_key: UUID`
49+
- `id: str`
50+
- `name: str`
51+
- `description: str | None`
52+
- `status_std: str`
53+
- `status_specification: str`
54+
- `data_elements: list[MigDataElement]`
55+
- `position: Optional[int]`
56+
- FK: `segment_primary_key`
57+
58+
### MigSegment
59+
- `primary_key: UUID`
60+
- `id: str`
61+
- `name: str`
62+
- `description: str | None`
63+
- `counter: str`
64+
- `level: int`
65+
- `number: str`
66+
- `max_rep_std: int`
67+
- `max_rep_specification: int`
68+
- `status_std: str`
69+
- `status_specification: str`
70+
- `example: str | None`
71+
- `is_on_uebertragungsdatei_level: bool`
72+
- `data_elements: list[MigDataElement]`
73+
- `data_element_groups: list[MigDataElementGroup]`
74+
- `position: Optional[int]`
75+
- FK: `segmentgroup_primary_key` (optional)
76+
- FK: `mig_primary_key` (optional)
77+
78+
### MigSegmentGroupLink
79+
- `parent_id: UUID` (FK to MigSegmentGroup, PK)
80+
- `child_id: UUID` (FK to MigSegmentGroup, PK)
81+
82+
### MigSegmentGroup
83+
- `primary_key: UUID`
84+
- `id: str`
85+
- `name: str`
86+
- `counter: str`
87+
- `level: int`
88+
- `max_rep_std: int`
89+
- `max_rep_specification: int`
90+
- `status_std: str`
91+
- `status_specification: str`
92+
- `segments: list[MigSegment]`
93+
- `segment_groups: list[MigSegmentGroup]` (via MigSegmentGroupLink)
94+
- `parent_segment_group: MigSegmentGroup | None` (via MigSegmentGroupLink)
95+
- `position: Optional[int]`
96+
- FK: `mig_primary_key` (optional)
97+
98+
### MessageImplementationGuide
99+
- `primary_key: UUID`
100+
- `veroeffentlichungsdatum: date`
101+
- `autor: str`
102+
- `versionsnummer: str`
103+
- `format: EdifactFormat`
104+
- `segments: list[MigSegment]`
105+
- `segment_groups: list[MigSegmentGroup]`
106+
- SQL-only fields:
107+
- `gueltig_von: Optional[date]`
108+
- `gueltig_bis: Optional[date]`
109+
- `edifact_format_version: Optional[EdifactFormatVersion]`
110+
111+
## Relationships
112+
113+
```
114+
MessageImplementationGuide
115+
├── segments: list[MigSegment] (FK: mig_primary_key)
116+
└── segment_groups: list[MigSegmentGroup] (FK: mig_primary_key)
117+
118+
MigSegmentGroup
119+
├── segments: list[MigSegment] (FK: segmentgroup_primary_key)
120+
├── segment_groups: list[MigSegmentGroup] (via MigSegmentGroupLink)
121+
└── parent_segment_group: MigSegmentGroup (via MigSegmentGroupLink)
122+
123+
MigSegment
124+
├── data_elements: list[MigDataElement] (FK: segment_primary_key)
125+
└── data_element_groups: list[MigDataElementGroup] (FK: segment_primary_key)
126+
127+
MigDataElementGroup
128+
└── data_elements: list[MigDataElement] (FK: data_element_group_primary_key)
129+
130+
MigDataElement
131+
└── codes: list[MigCode] (FK: data_element_primary_key)
132+
```
133+
134+
## UniqueConstraints
135+
136+
Following AHB pattern - position unique within parent:
137+
- `MigCode`: `(data_element_primary_key, position)`
138+
- `MigDataElementGroup`: `(segment_primary_key, position)`
139+
- `MigSegment`: position unique per parent (MIG or SegmentGroup) - separate constraints
140+
- `MigSegmentGroup`: `(mig_primary_key, position)`
141+
142+
## Conversion Logic
143+
144+
### `from_model()` pattern
145+
- Classmethod taking Pydantic model + optional `position` parameter
146+
- Direct field mappings
147+
- Enumerate children to get position index
148+
- Use `isinstance` checks for union types to route to correct list
149+
150+
### `to_model()` pattern
151+
- Instance method returning Pydantic model
152+
- Merge union-type lists, sort by `position`
153+
- Return `tuple()` for Pydantic tuple fields
154+
- MigStatus enum restored via Pydantic validation
155+
156+
## Exports
157+
158+
Update `sqlmodels/__init__.py`:
159+
```python
160+
from .messageimplementationguide import (
161+
MigCode,
162+
MigDataElement,
163+
MigDataElementGroup,
164+
MigSegment,
165+
MigSegmentGroup,
166+
MessageImplementationGuide,
167+
)
168+
```
169+
170+
## Testing
171+
172+
New file: `unittests/test_sqlmodels_messageimplementationguide.py`
173+
174+
Tests:
175+
1. Single MIG roundtrip: XML → Pydantic → SQL → Pydantic → compare equality
176+
2. All MIGs from private submodule (skip if not checked out)
177+
178+
Reuse `sqlite_session` fixture from conftest.
179+
180+
## Future Work (out of scope)
181+
182+
- `MigHierarchyMaterialized` view for flattening
183+
- Prefix AHB classes (`AhbCode`, `AhbDataElement`, etc.) for consistency

src/fundamend/sqlmodels/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@
2828
Segment,
2929
SegmentGroup,
3030
)
31+
from .messageimplementationguide import (
32+
MessageImplementationGuide,
33+
MigCode,
34+
MigDataElement,
35+
MigDataElementGroup,
36+
MigSegment,
37+
MigSegmentGroup,
38+
MigSegmentGroupLink,
39+
)
3140

3241
__all__ = [
3342
"create_ahb_view",
@@ -46,4 +55,11 @@
4655
"Anwendungsfall",
4756
"Bedingung",
4857
"Anwendungshandbuch",
58+
"MessageImplementationGuide",
59+
"MigCode",
60+
"MigDataElement",
61+
"MigDataElementGroup",
62+
"MigSegment",
63+
"MigSegmentGroup",
64+
"MigSegmentGroupLink",
4965
]

src/fundamend/sqlmodels/anwendungshandbuch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from efoli import EdifactFormat, EdifactFormatVersion
99

10-
# pylint: disable=too-few-public-methods, duplicate-code, missing-function-docstring
10+
# pylint: disable=too-few-public-methods, duplicate-code, missing-function-docstring, no-member
1111

1212
# the structures are similar, still we decided against inheritance, so there's naturally a little bit of duplication
1313

0 commit comments

Comments
 (0)