sqlacodegen 4.0.0rc2__tar.gz → 4.0.1__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.
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/CHANGES.rst +40 -2
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/PKG-INFO +14 -1
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/README.rst +13 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/src/sqlacodegen/generators.py +248 -110
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/src/sqlacodegen.egg-info/PKG-INFO +14 -1
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/tests/test_generator_declarative.py +775 -18
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/tests/test_generator_sqlmodel.py +99 -2
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/tests/test_generator_tables.py +156 -4
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/.github/FUNDING.yml +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/.github/ISSUE_TEMPLATE/features_request.yaml +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/.github/dependabot.yml +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/.github/pull_request_template.md +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/.github/workflows/publish.yml +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/.github/workflows/test.yml +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/.gitignore +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/.pre-commit-config.yaml +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/CONTRIBUTING.rst +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/LICENSE +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/pyproject.toml +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/setup.cfg +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/src/sqlacodegen/__init__.py +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/src/sqlacodegen/__main__.py +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/src/sqlacodegen/cli.py +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/src/sqlacodegen/models.py +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/src/sqlacodegen/py.typed +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/src/sqlacodegen/utils.py +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/src/sqlacodegen.egg-info/SOURCES.txt +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/src/sqlacodegen.egg-info/dependency_links.txt +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/src/sqlacodegen.egg-info/entry_points.txt +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/src/sqlacodegen.egg-info/requires.txt +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/src/sqlacodegen.egg-info/top_level.txt +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/tests/__init__.py +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/tests/conftest.py +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/tests/test_cli.py +0 -0
- {sqlacodegen-4.0.0rc2 → sqlacodegen-4.0.1}/tests/test_generator_dataclass.py +0 -0
|
@@ -1,10 +1,48 @@
|
|
|
1
1
|
Version history
|
|
2
2
|
===============
|
|
3
3
|
|
|
4
|
+
**4.0.1**
|
|
5
|
+
|
|
6
|
+
- Fix enum column definitions to explicitly include schema and name if reflected
|
|
7
|
+
via SQLAlchemy's Metadata (pr by @sheinbergon)
|
|
8
|
+
|
|
9
|
+
**4.0.0**
|
|
10
|
+
|
|
11
|
+
- **BACKWARD INCOMPATIBLE** API changes (for those who customize code generation by
|
|
12
|
+
subclassing the existing generators):
|
|
13
|
+
|
|
14
|
+
* Added new optional keyword argument, ``explicit_foreign_keys`` to
|
|
15
|
+
``DeclarativeGenerator``, to force foreign keys to be rendered as
|
|
16
|
+
``ClassName.attribute_name`` string references
|
|
17
|
+
* Removed the ``render_relationship_args()`` method from the SQLModel generator
|
|
18
|
+
* Added two new methods for customizing relationship rendering in
|
|
19
|
+
``DeclarativeGenerator``:
|
|
20
|
+
|
|
21
|
+
* ``render_relationship_annotation()``: returns the appropriate type annotation
|
|
22
|
+
(without the ``Mapped`` wrapper) for the relationship
|
|
23
|
+
* ``render_relationship_arguments()``: returns a dictionary of keyword arguments to
|
|
24
|
+
``sqlalchemy.orm.relationship()``
|
|
25
|
+
|
|
26
|
+
**4.0.0rc3**
|
|
27
|
+
|
|
28
|
+
- **BACKWARD INCOMPATIBLE** Relationship names changed when multiple FKs or junction tables
|
|
29
|
+
connect to the same target table. Regenerating models will break existing code.
|
|
30
|
+
- Added support for generating Python enum classes for ``ARRAY(Enum(...))`` columns
|
|
31
|
+
(e.g., PostgreSQL ``ARRAY(ENUM)``). Supports named/unnamed enums, shared enums across
|
|
32
|
+
columns, and multi-dimensional arrays. Respects ``--options nonativeenums``.
|
|
33
|
+
(PR by @sheinbergon)
|
|
34
|
+
- Improved relationship naming: one-to-many uses FK column names (e.g.,
|
|
35
|
+
``simple_items_parent_container``), many-to-many uses junction table names (e.g.,
|
|
36
|
+
``students_enrollments``). Use ``--options nofknames`` to revert to old behavior. (PR by @sheinbergon)
|
|
37
|
+
- Fixed ``Index`` kwargs (e.g. ``mysql_length``) being ignored during code generation
|
|
38
|
+
(PR by @luliangce)
|
|
39
|
+
- Fixed the SQLModel generator not adding the ``foreign_keys`` parameters when
|
|
40
|
+
generating multiple relationships between the same two tables
|
|
41
|
+
|
|
4
42
|
**4.0.0rc2**
|
|
5
43
|
|
|
6
|
-
|
|
7
|
-
|
|
44
|
+
- Add ``values_callable`` lambda to generated native enums column definitions.
|
|
45
|
+
This allows for proper enum value insertion when working with ORM models (PR by @sheinbergon)
|
|
8
46
|
|
|
9
47
|
**4.0.0rc1**
|
|
10
48
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlacodegen
|
|
3
|
-
Version: 4.0.
|
|
3
|
+
Version: 4.0.1
|
|
4
4
|
Summary: Automatic model code generator for SQLAlchemy
|
|
5
5
|
Author-email: Alex Grönholm <alex.gronholm@nextday.fi>
|
|
6
6
|
Maintainer-email: Idan Sheinberg <ishinberg0@gmail.com>
|
|
@@ -159,6 +159,11 @@ values must be delimited by commas, e.g. ``--options noconstraints,nobidi``):
|
|
|
159
159
|
* ``nobidi``: generate relationships in a unidirectional fashion, so only the
|
|
160
160
|
many-to-one or first side of many-to-many relationships gets a relationship
|
|
161
161
|
attribute, as on v2.X
|
|
162
|
+
* ``nofknames``: disable improved relationship naming when multiple FKs or
|
|
163
|
+
junction tables connect to the same target. By default, uses FK column names
|
|
164
|
+
for one-to-many (e.g., ``simple_items_parent_container``) and junction table
|
|
165
|
+
names for many-to-many (e.g., ``students_enrollments``). Reverts to
|
|
166
|
+
underscore suffixes (``simple_items_``, ``student_``).
|
|
162
167
|
|
|
163
168
|
* ``dataclasses``
|
|
164
169
|
|
|
@@ -226,6 +231,14 @@ due to that ``_id`` suffix.
|
|
|
226
231
|
For self referential relationships, the reverse side of the relationship will be named
|
|
227
232
|
with the ``_reverse`` suffix appended to it.
|
|
228
233
|
|
|
234
|
+
When multiple foreign keys or junction tables connect to the same target table,
|
|
235
|
+
relationships use qualifiers for disambiguation. One-to-many relationships use FK
|
|
236
|
+
column names (e.g., ``simple_items_parent_container``, ``simple_items_top_container``).
|
|
237
|
+
Many-to-many relationships use junction table names (e.g., ``students_enrollments``,
|
|
238
|
+
``students_waitlist``), except for self-referential cases which use FK column names
|
|
239
|
+
(e.g., ``parent``, ``child``). The ``nofknames`` option reverts to underscore suffixes
|
|
240
|
+
(``simple_items_``, ``student_``).
|
|
241
|
+
|
|
229
242
|
Customizing code generation logic
|
|
230
243
|
=================================
|
|
231
244
|
|
|
@@ -122,6 +122,11 @@ values must be delimited by commas, e.g. ``--options noconstraints,nobidi``):
|
|
|
122
122
|
* ``nobidi``: generate relationships in a unidirectional fashion, so only the
|
|
123
123
|
many-to-one or first side of many-to-many relationships gets a relationship
|
|
124
124
|
attribute, as on v2.X
|
|
125
|
+
* ``nofknames``: disable improved relationship naming when multiple FKs or
|
|
126
|
+
junction tables connect to the same target. By default, uses FK column names
|
|
127
|
+
for one-to-many (e.g., ``simple_items_parent_container``) and junction table
|
|
128
|
+
names for many-to-many (e.g., ``students_enrollments``). Reverts to
|
|
129
|
+
underscore suffixes (``simple_items_``, ``student_``).
|
|
125
130
|
|
|
126
131
|
* ``dataclasses``
|
|
127
132
|
|
|
@@ -189,6 +194,14 @@ due to that ``_id`` suffix.
|
|
|
189
194
|
For self referential relationships, the reverse side of the relationship will be named
|
|
190
195
|
with the ``_reverse`` suffix appended to it.
|
|
191
196
|
|
|
197
|
+
When multiple foreign keys or junction tables connect to the same target table,
|
|
198
|
+
relationships use qualifiers for disambiguation. One-to-many relationships use FK
|
|
199
|
+
column names (e.g., ``simple_items_parent_container``, ``simple_items_top_container``).
|
|
200
|
+
Many-to-many relationships use junction table names (e.g., ``students_enrollments``,
|
|
201
|
+
``students_waitlist``), except for self-referential cases which use FK column names
|
|
202
|
+
(e.g., ``parent``, ``child``). The ``nofknames`` option reverts to underscore suffixes
|
|
203
|
+
(``simple_items_``, ``student_``).
|
|
204
|
+
|
|
192
205
|
Customizing code generation logic
|
|
193
206
|
=================================
|
|
194
207
|
|
|
@@ -5,7 +5,7 @@ import re
|
|
|
5
5
|
import sys
|
|
6
6
|
from abc import ABCMeta, abstractmethod
|
|
7
7
|
from collections import defaultdict
|
|
8
|
-
from collections.abc import Collection, Iterable, Sequence
|
|
8
|
+
from collections.abc import Collection, Iterable, Mapping, Sequence
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from importlib import import_module
|
|
11
11
|
from inspect import Parameter
|
|
@@ -424,7 +424,10 @@ class TablesGenerator(CodeGenerator):
|
|
|
424
424
|
|
|
425
425
|
def render_index(self, index: Index) -> str:
|
|
426
426
|
extra_args = [repr(col.name) for col in index.columns]
|
|
427
|
-
kwargs = {
|
|
427
|
+
kwargs = {
|
|
428
|
+
key: repr(value) if isinstance(value, str) else value
|
|
429
|
+
for key, value in sorted(index.kwargs.items(), key=lambda item: item[0])
|
|
430
|
+
}
|
|
428
431
|
if index.unique:
|
|
429
432
|
kwargs["unique"] = True
|
|
430
433
|
|
|
@@ -547,10 +550,38 @@ class TablesGenerator(CodeGenerator):
|
|
|
547
550
|
):
|
|
548
551
|
# Import SQLAlchemy Enum (will be handled in collect_imports)
|
|
549
552
|
self.add_import(Enum)
|
|
550
|
-
|
|
553
|
+
extra_kwargs = ""
|
|
554
|
+
if column_type.name is not None:
|
|
555
|
+
extra_kwargs += f", name={column_type.name!r}"
|
|
556
|
+
|
|
557
|
+
if column_type.schema is not None:
|
|
558
|
+
extra_kwargs += f", schema={column_type.schema!r}"
|
|
559
|
+
|
|
560
|
+
return f"Enum({enum_class_name}, values_callable=lambda cls: [member.value for member in cls]{extra_kwargs})"
|
|
551
561
|
|
|
552
562
|
args = []
|
|
553
563
|
kwargs: dict[str, Any] = {}
|
|
564
|
+
|
|
565
|
+
# Check if this is an ARRAY column with an Enum item type mapped to a Python enum class
|
|
566
|
+
if isinstance(column_type, ARRAY) and isinstance(column_type.item_type, Enum):
|
|
567
|
+
if enum_class_name := self.enum_classes.get(
|
|
568
|
+
(column.table.name, column.name)
|
|
569
|
+
):
|
|
570
|
+
self.add_import(ARRAY)
|
|
571
|
+
self.add_import(Enum)
|
|
572
|
+
extra_kwargs = ""
|
|
573
|
+
if column_type.item_type.name is not None:
|
|
574
|
+
extra_kwargs += f", name={column_type.item_type.name!r}"
|
|
575
|
+
|
|
576
|
+
if column_type.item_type.schema is not None:
|
|
577
|
+
extra_kwargs += f", schema={column_type.item_type.schema!r}"
|
|
578
|
+
|
|
579
|
+
rendered_enum = f"Enum({enum_class_name}, values_callable=lambda cls: [member.value for member in cls]{extra_kwargs})"
|
|
580
|
+
if column_type.dimensions is not None:
|
|
581
|
+
kwargs["dimensions"] = repr(column_type.dimensions)
|
|
582
|
+
|
|
583
|
+
return render_callable("ARRAY", rendered_enum, kwargs=kwargs)
|
|
584
|
+
|
|
554
585
|
sig = inspect.signature(column_type.__class__.__init__)
|
|
555
586
|
defaults = {param.name: param.default for param in sig.parameters.values()}
|
|
556
587
|
missing = object()
|
|
@@ -806,6 +837,31 @@ class TablesGenerator(CodeGenerator):
|
|
|
806
837
|
|
|
807
838
|
def fix_column_types(self, table: Table) -> None:
|
|
808
839
|
"""Adjust the reflected column types."""
|
|
840
|
+
|
|
841
|
+
def fix_enum_column(col_name: str, enum_type: Enum) -> None:
|
|
842
|
+
if (table.name, col_name) in self.enum_classes:
|
|
843
|
+
return
|
|
844
|
+
|
|
845
|
+
if enum_type.name:
|
|
846
|
+
existing_class = None
|
|
847
|
+
for (_, _), cls in self.enum_classes.items():
|
|
848
|
+
if cls == self._enum_name_to_class_name(enum_type.name):
|
|
849
|
+
existing_class = cls
|
|
850
|
+
break
|
|
851
|
+
|
|
852
|
+
if existing_class:
|
|
853
|
+
enum_class_name = existing_class
|
|
854
|
+
else:
|
|
855
|
+
enum_class_name = self._enum_name_to_class_name(enum_type.name)
|
|
856
|
+
if enum_class_name not in self.enum_values:
|
|
857
|
+
self.enum_values[enum_class_name] = list(enum_type.enums)
|
|
858
|
+
else:
|
|
859
|
+
enum_class_name = self._create_enum_class(
|
|
860
|
+
table.name, col_name, list(enum_type.enums)
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
self.enum_classes[(table.name, col_name)] = enum_class_name
|
|
864
|
+
|
|
809
865
|
# Detect check constraints for boolean and enum columns
|
|
810
866
|
for constraint in table.constraints.copy():
|
|
811
867
|
if isinstance(constraint, CheckConstraint):
|
|
@@ -849,37 +905,16 @@ class TablesGenerator(CodeGenerator):
|
|
|
849
905
|
and isinstance(column.type, Enum)
|
|
850
906
|
and column.type.enums
|
|
851
907
|
):
|
|
852
|
-
|
|
853
|
-
# Named enum - create shared enum class if not already created
|
|
854
|
-
if (table.name, column.name) not in self.enum_classes:
|
|
855
|
-
# Check if we've already created an enum for this name
|
|
856
|
-
existing_class = None
|
|
857
|
-
for (t, c), cls in self.enum_classes.items():
|
|
858
|
-
if cls == self._enum_name_to_class_name(column.type.name):
|
|
859
|
-
existing_class = cls
|
|
860
|
-
break
|
|
861
|
-
|
|
862
|
-
if existing_class:
|
|
863
|
-
enum_class_name = existing_class
|
|
864
|
-
else:
|
|
865
|
-
# Create new enum class from the enum's name
|
|
866
|
-
enum_class_name = self._enum_name_to_class_name(
|
|
867
|
-
column.type.name
|
|
868
|
-
)
|
|
869
|
-
# Register the enum values if not already registered
|
|
870
|
-
if enum_class_name not in self.enum_values:
|
|
871
|
-
self.enum_values[enum_class_name] = list(
|
|
872
|
-
column.type.enums
|
|
873
|
-
)
|
|
908
|
+
fix_enum_column(column.name, column.type)
|
|
874
909
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
910
|
+
# Handle ARRAY columns with Enum item types (e.g., PostgreSQL ARRAY(ENUM))
|
|
911
|
+
elif (
|
|
912
|
+
"nonativeenums" not in self.options
|
|
913
|
+
and isinstance(column.type, ARRAY)
|
|
914
|
+
and isinstance(column.type.item_type, Enum)
|
|
915
|
+
and column.type.item_type.enums
|
|
916
|
+
):
|
|
917
|
+
fix_enum_column(column.name, column.type.item_type)
|
|
883
918
|
|
|
884
919
|
if not self.keep_dialect_types:
|
|
885
920
|
try:
|
|
@@ -969,6 +1004,7 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
969
1004
|
"nojoined",
|
|
970
1005
|
"nobidi",
|
|
971
1006
|
"noidsuffix",
|
|
1007
|
+
"nofknames",
|
|
972
1008
|
}
|
|
973
1009
|
|
|
974
1010
|
def __init__(
|
|
@@ -979,10 +1015,12 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
979
1015
|
*,
|
|
980
1016
|
indentation: str = " ",
|
|
981
1017
|
base_class_name: str = "Base",
|
|
1018
|
+
explicit_foreign_keys: bool = False,
|
|
982
1019
|
):
|
|
983
1020
|
super().__init__(metadata, bind, options, indentation=indentation)
|
|
984
1021
|
self.base_class_name: str = base_class_name
|
|
985
1022
|
self.inflect_engine = inflect.engine()
|
|
1023
|
+
self.explicit_foreign_keys = explicit_foreign_keys
|
|
986
1024
|
|
|
987
1025
|
def generate_base(self) -> None:
|
|
988
1026
|
self.base = Base(
|
|
@@ -1290,30 +1328,123 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
1290
1328
|
global_names: set[str],
|
|
1291
1329
|
local_names: set[str],
|
|
1292
1330
|
) -> None:
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1331
|
+
def strip_id_suffix(name: str) -> str:
|
|
1332
|
+
# Strip _id only if at the end or followed by underscore (e.g., "course_id" -> "course", "course_id_1" -> "course_1")
|
|
1333
|
+
# But don't strip from "parent_id1" (where id is followed by a digit without underscore)
|
|
1334
|
+
return re.sub(r"_id(?=_|$)", "", name)
|
|
1335
|
+
|
|
1336
|
+
def get_m2m_qualified_name(default_name: str) -> str:
|
|
1337
|
+
"""Generate qualified name for many-to-many relationship when multiple junction tables exist."""
|
|
1338
|
+
# Check if there are multiple M2M relationships to the same target
|
|
1339
|
+
target_m2m_relationships = [
|
|
1340
|
+
r
|
|
1341
|
+
for r in relationship.source.relationships
|
|
1342
|
+
if r.target is relationship.target
|
|
1343
|
+
and r.type == RelationshipType.MANY_TO_MANY
|
|
1344
|
+
]
|
|
1345
|
+
|
|
1346
|
+
# Only use junction-based naming when there are multiple M2M to same target
|
|
1347
|
+
if len(target_m2m_relationships) > 1:
|
|
1348
|
+
if relationship.source is relationship.target:
|
|
1349
|
+
# Self-referential: use FK column name from junction table
|
|
1350
|
+
# (e.g., "parent_id" -> "parent", "child_id" -> "child")
|
|
1351
|
+
if relationship.constraint:
|
|
1352
|
+
column_names = [c.name for c in relationship.constraint.columns]
|
|
1353
|
+
if len(column_names) == 1:
|
|
1354
|
+
fk_qualifier = strip_id_suffix(column_names[0])
|
|
1355
|
+
else:
|
|
1356
|
+
fk_qualifier = "_".join(
|
|
1357
|
+
strip_id_suffix(col_name) for col_name in column_names
|
|
1358
|
+
)
|
|
1359
|
+
return fk_qualifier
|
|
1360
|
+
elif relationship.association_table:
|
|
1361
|
+
# Normal: use junction table name as qualifier
|
|
1362
|
+
junction_name = relationship.association_table.table.name
|
|
1363
|
+
fk_qualifier = strip_id_suffix(junction_name)
|
|
1364
|
+
return f"{relationship.target.table.name}_{fk_qualifier}"
|
|
1365
|
+
else:
|
|
1366
|
+
# Single M2M: use simple name from junction table FK column
|
|
1367
|
+
# (e.g., "right_id" -> "right" instead of "right_table")
|
|
1368
|
+
if relationship.constraint and "noidsuffix" not in self.options:
|
|
1369
|
+
column_names = [c.name for c in relationship.constraint.columns]
|
|
1370
|
+
if len(column_names) == 1:
|
|
1371
|
+
stripped_name = strip_id_suffix(column_names[0])
|
|
1372
|
+
if stripped_name != column_names[0]:
|
|
1373
|
+
return stripped_name
|
|
1374
|
+
|
|
1375
|
+
return default_name
|
|
1376
|
+
|
|
1377
|
+
def get_fk_qualified_name(constraint: ForeignKeyConstraint) -> str:
|
|
1378
|
+
"""Generate qualified name for one-to-many/one-to-one relationship using FK column names."""
|
|
1379
|
+
column_names = [c.name for c in constraint.columns]
|
|
1380
|
+
|
|
1381
|
+
if len(column_names) == 1:
|
|
1382
|
+
# Single column FK: strip _id suffix if present
|
|
1383
|
+
fk_qualifier = strip_id_suffix(column_names[0])
|
|
1384
|
+
else:
|
|
1385
|
+
# Multi-column FK: concatenate all column names (strip _id from each)
|
|
1386
|
+
fk_qualifier = "_".join(
|
|
1387
|
+
strip_id_suffix(col_name) for col_name in column_names
|
|
1388
|
+
)
|
|
1305
1389
|
|
|
1306
|
-
#
|
|
1307
|
-
|
|
1308
|
-
|
|
1390
|
+
# For self-referential relationships, don't prepend the table name
|
|
1391
|
+
if relationship.source is relationship.target:
|
|
1392
|
+
return fk_qualifier
|
|
1393
|
+
else:
|
|
1394
|
+
return f"{relationship.target.table.name}_{fk_qualifier}"
|
|
1395
|
+
|
|
1396
|
+
def resolve_preferred_name() -> str:
|
|
1397
|
+
resolved_name = relationship.target.table.name
|
|
1398
|
+
|
|
1399
|
+
# For reverse relationships with multiple FKs to the same table, use the FK
|
|
1400
|
+
# column name to create a more descriptive relationship name
|
|
1401
|
+
# For M2M relationships with multiple junction tables, use the junction table name
|
|
1402
|
+
use_fk_based_naming = "nofknames" not in self.options and (
|
|
1403
|
+
(
|
|
1404
|
+
relationship.constraint
|
|
1405
|
+
and relationship.type
|
|
1406
|
+
in (RelationshipType.ONE_TO_MANY, RelationshipType.ONE_TO_ONE)
|
|
1407
|
+
and relationship.foreign_keys
|
|
1408
|
+
)
|
|
1409
|
+
or (
|
|
1410
|
+
relationship.type == RelationshipType.MANY_TO_MANY
|
|
1411
|
+
and relationship.association_table
|
|
1412
|
+
)
|
|
1413
|
+
)
|
|
1414
|
+
|
|
1415
|
+
if use_fk_based_naming:
|
|
1416
|
+
if relationship.type == RelationshipType.MANY_TO_MANY:
|
|
1417
|
+
resolved_name = get_m2m_qualified_name(resolved_name)
|
|
1418
|
+
elif relationship.constraint:
|
|
1419
|
+
resolved_name = get_fk_qualified_name(relationship.constraint)
|
|
1420
|
+
|
|
1421
|
+
# If there's a constraint with a single column that contains "_id", use the
|
|
1422
|
+
# stripped version as the relationship name
|
|
1423
|
+
elif relationship.constraint and "noidsuffix" not in self.options:
|
|
1309
1424
|
is_source = relationship.source.table is relationship.constraint.table
|
|
1310
1425
|
if is_source or relationship.type not in (
|
|
1311
1426
|
RelationshipType.ONE_TO_ONE,
|
|
1312
1427
|
RelationshipType.ONE_TO_MANY,
|
|
1313
1428
|
):
|
|
1314
1429
|
column_names = [c.name for c in relationship.constraint.columns]
|
|
1315
|
-
if len(column_names) == 1
|
|
1316
|
-
|
|
1430
|
+
if len(column_names) == 1:
|
|
1431
|
+
stripped_name = strip_id_suffix(column_names[0])
|
|
1432
|
+
# Only use the stripped name if it actually changed (had _id in it)
|
|
1433
|
+
if stripped_name != column_names[0]:
|
|
1434
|
+
resolved_name = stripped_name
|
|
1435
|
+
else:
|
|
1436
|
+
# For composite FKs, check if there are multiple FKs to the same target
|
|
1437
|
+
target_relationships = [
|
|
1438
|
+
r
|
|
1439
|
+
for r in relationship.source.relationships
|
|
1440
|
+
if r.target is relationship.target
|
|
1441
|
+
and r.type == relationship.type
|
|
1442
|
+
]
|
|
1443
|
+
if len(target_relationships) > 1:
|
|
1444
|
+
# Multiple FKs to same table - use concatenated column names
|
|
1445
|
+
resolved_name = "_".join(
|
|
1446
|
+
strip_id_suffix(col_name) for col_name in column_names
|
|
1447
|
+
)
|
|
1317
1448
|
|
|
1318
1449
|
if "use_inflect" in self.options:
|
|
1319
1450
|
inflected_name: str | Literal[False]
|
|
@@ -1321,12 +1452,25 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
1321
1452
|
RelationshipType.ONE_TO_MANY,
|
|
1322
1453
|
RelationshipType.MANY_TO_MANY,
|
|
1323
1454
|
):
|
|
1324
|
-
if not self.inflect_engine.singular_noun(
|
|
1325
|
-
|
|
1455
|
+
if not self.inflect_engine.singular_noun(resolved_name):
|
|
1456
|
+
resolved_name = self.inflect_engine.plural_noun(resolved_name)
|
|
1326
1457
|
else:
|
|
1327
|
-
inflected_name = self.inflect_engine.singular_noun(
|
|
1458
|
+
inflected_name = self.inflect_engine.singular_noun(resolved_name)
|
|
1328
1459
|
if inflected_name:
|
|
1329
|
-
|
|
1460
|
+
resolved_name = inflected_name
|
|
1461
|
+
|
|
1462
|
+
return resolved_name
|
|
1463
|
+
|
|
1464
|
+
if (
|
|
1465
|
+
relationship.type
|
|
1466
|
+
in (RelationshipType.ONE_TO_MANY, RelationshipType.ONE_TO_ONE)
|
|
1467
|
+
and relationship.source is relationship.target
|
|
1468
|
+
and relationship.backref
|
|
1469
|
+
and relationship.backref.name
|
|
1470
|
+
):
|
|
1471
|
+
preferred_name = relationship.backref.name + "_reverse"
|
|
1472
|
+
else:
|
|
1473
|
+
preferred_name = resolve_preferred_name()
|
|
1330
1474
|
|
|
1331
1475
|
relationship.name = self.find_free_name(
|
|
1332
1476
|
preferred_name, global_names, local_names
|
|
@@ -1498,6 +1642,33 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
1498
1642
|
return f"{column_attr.name}: Mapped[{rendered_column_python_type}] = {rendered_column}"
|
|
1499
1643
|
|
|
1500
1644
|
def render_relationship(self, relationship: RelationshipAttribute) -> str:
|
|
1645
|
+
kwargs = self.render_relationship_arguments(relationship)
|
|
1646
|
+
annotation = self.render_relationship_annotation(relationship)
|
|
1647
|
+
rendered_relationship = render_callable(
|
|
1648
|
+
"relationship", repr(relationship.target.name), kwargs=kwargs
|
|
1649
|
+
)
|
|
1650
|
+
return f"{relationship.name}: Mapped[{annotation}] = {rendered_relationship}"
|
|
1651
|
+
|
|
1652
|
+
def render_relationship_annotation(
|
|
1653
|
+
self, relationship: RelationshipAttribute
|
|
1654
|
+
) -> str:
|
|
1655
|
+
match relationship.type:
|
|
1656
|
+
case RelationshipType.ONE_TO_MANY:
|
|
1657
|
+
return f"list[{relationship.target.name!r}]"
|
|
1658
|
+
case RelationshipType.ONE_TO_ONE | RelationshipType.MANY_TO_ONE:
|
|
1659
|
+
if relationship.constraint and any(
|
|
1660
|
+
col.nullable for col in relationship.constraint.columns
|
|
1661
|
+
):
|
|
1662
|
+
self.add_literal_import("typing", "Optional")
|
|
1663
|
+
return f"Optional[{relationship.target.name!r}]"
|
|
1664
|
+
else:
|
|
1665
|
+
return f"'{relationship.target.name}'"
|
|
1666
|
+
case RelationshipType.MANY_TO_MANY:
|
|
1667
|
+
return f"list[{relationship.target.name!r}]"
|
|
1668
|
+
|
|
1669
|
+
def render_relationship_arguments(
|
|
1670
|
+
self, relationship: RelationshipAttribute
|
|
1671
|
+
) -> Mapping[str, Any]:
|
|
1501
1672
|
def render_column_attrs(column_attrs: list[ColumnAttribute]) -> str:
|
|
1502
1673
|
rendered = []
|
|
1503
1674
|
for attr in column_attrs:
|
|
@@ -1513,7 +1684,7 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
1513
1684
|
render_as_string = False
|
|
1514
1685
|
# Assume that column_attrs are all in relationship.source or none
|
|
1515
1686
|
for attr in column_attrs:
|
|
1516
|
-
if attr.model is relationship.source:
|
|
1687
|
+
if not self.explicit_foreign_keys and attr.model is relationship.source:
|
|
1517
1688
|
rendered.append(attr.name)
|
|
1518
1689
|
else:
|
|
1519
1690
|
rendered.append(f"{attr.model.name}.{attr.name}")
|
|
@@ -1569,33 +1740,7 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
1569
1740
|
if relationship.backref:
|
|
1570
1741
|
kwargs["back_populates"] = repr(relationship.backref.name)
|
|
1571
1742
|
|
|
1572
|
-
|
|
1573
|
-
"relationship", repr(relationship.target.name), kwargs=kwargs
|
|
1574
|
-
)
|
|
1575
|
-
|
|
1576
|
-
relationship_type: str
|
|
1577
|
-
if relationship.type == RelationshipType.ONE_TO_MANY:
|
|
1578
|
-
relationship_type = f"list['{relationship.target.name}']"
|
|
1579
|
-
elif relationship.type in (
|
|
1580
|
-
RelationshipType.ONE_TO_ONE,
|
|
1581
|
-
RelationshipType.MANY_TO_ONE,
|
|
1582
|
-
):
|
|
1583
|
-
relationship_type = f"'{relationship.target.name}'"
|
|
1584
|
-
if relationship.constraint and any(
|
|
1585
|
-
col.nullable for col in relationship.constraint.columns
|
|
1586
|
-
):
|
|
1587
|
-
self.add_literal_import("typing", "Optional")
|
|
1588
|
-
relationship_type = f"Optional[{relationship_type}]"
|
|
1589
|
-
elif relationship.type == RelationshipType.MANY_TO_MANY:
|
|
1590
|
-
relationship_type = f"list['{relationship.target.name}']"
|
|
1591
|
-
else:
|
|
1592
|
-
self.add_literal_import("typing", "Any")
|
|
1593
|
-
relationship_type = "Any"
|
|
1594
|
-
|
|
1595
|
-
return (
|
|
1596
|
-
f"{relationship.name}: Mapped[{relationship_type}] "
|
|
1597
|
-
f"= {rendered_relationship}"
|
|
1598
|
-
)
|
|
1743
|
+
return kwargs
|
|
1599
1744
|
|
|
1600
1745
|
|
|
1601
1746
|
class DataclassGenerator(DeclarativeGenerator):
|
|
@@ -1650,6 +1795,7 @@ class SQLModelGenerator(DeclarativeGenerator):
|
|
|
1650
1795
|
options,
|
|
1651
1796
|
indentation=indentation,
|
|
1652
1797
|
base_class_name=base_class_name,
|
|
1798
|
+
explicit_foreign_keys=True,
|
|
1653
1799
|
)
|
|
1654
1800
|
|
|
1655
1801
|
@property
|
|
@@ -1730,34 +1876,26 @@ class SQLModelGenerator(DeclarativeGenerator):
|
|
|
1730
1876
|
return f"{column_attr.name}: {rendered_column_python_type} = {rendered_field}"
|
|
1731
1877
|
|
|
1732
1878
|
def render_relationship(self, relationship: RelationshipAttribute) -> str:
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1879
|
+
kwargs = self.render_relationship_arguments(relationship)
|
|
1880
|
+
annotation = self.render_relationship_annotation(relationship)
|
|
1881
|
+
|
|
1882
|
+
native_kwargs: dict[str, Any] = {}
|
|
1883
|
+
non_native_kwargs: dict[str, Any] = {}
|
|
1884
|
+
for key, value in kwargs.items():
|
|
1885
|
+
# The following keyword arguments are natively supported in Relationship
|
|
1886
|
+
if key in ("back_populates", "cascade_delete", "passive_deletes"):
|
|
1887
|
+
native_kwargs[key] = value
|
|
1888
|
+
else:
|
|
1889
|
+
non_native_kwargs[key] = value
|
|
1737
1890
|
|
|
1738
|
-
if
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1891
|
+
if non_native_kwargs:
|
|
1892
|
+
native_kwargs["sa_relationship_kwargs"] = (
|
|
1893
|
+
"{"
|
|
1894
|
+
+ ", ".join(
|
|
1895
|
+
f"{key!r}: {value}" for key, value in non_native_kwargs.items()
|
|
1896
|
+
)
|
|
1897
|
+
+ "}"
|
|
1898
|
+
)
|
|
1746
1899
|
|
|
1747
|
-
rendered_field = render_callable("Relationship",
|
|
1900
|
+
rendered_field = render_callable("Relationship", kwargs=native_kwargs)
|
|
1748
1901
|
return f"{relationship.name}: {annotation} = {rendered_field}"
|
|
1749
|
-
|
|
1750
|
-
def render_relationship_args(self, arguments: str) -> list[str]:
|
|
1751
|
-
argument_list = arguments.split(",")
|
|
1752
|
-
# delete ')' and ' ' from args
|
|
1753
|
-
argument_list[-1] = argument_list[-1][:-1]
|
|
1754
|
-
argument_list = [argument[1:] for argument in argument_list]
|
|
1755
|
-
|
|
1756
|
-
rendered_args: list[str] = []
|
|
1757
|
-
for arg in argument_list:
|
|
1758
|
-
if "back_populates" in arg:
|
|
1759
|
-
rendered_args.append(arg)
|
|
1760
|
-
if "uselist=False" in arg:
|
|
1761
|
-
rendered_args.append("sa_relationship_kwargs={'uselist': False}")
|
|
1762
|
-
|
|
1763
|
-
return rendered_args
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlacodegen
|
|
3
|
-
Version: 4.0.
|
|
3
|
+
Version: 4.0.1
|
|
4
4
|
Summary: Automatic model code generator for SQLAlchemy
|
|
5
5
|
Author-email: Alex Grönholm <alex.gronholm@nextday.fi>
|
|
6
6
|
Maintainer-email: Idan Sheinberg <ishinberg0@gmail.com>
|
|
@@ -159,6 +159,11 @@ values must be delimited by commas, e.g. ``--options noconstraints,nobidi``):
|
|
|
159
159
|
* ``nobidi``: generate relationships in a unidirectional fashion, so only the
|
|
160
160
|
many-to-one or first side of many-to-many relationships gets a relationship
|
|
161
161
|
attribute, as on v2.X
|
|
162
|
+
* ``nofknames``: disable improved relationship naming when multiple FKs or
|
|
163
|
+
junction tables connect to the same target. By default, uses FK column names
|
|
164
|
+
for one-to-many (e.g., ``simple_items_parent_container``) and junction table
|
|
165
|
+
names for many-to-many (e.g., ``students_enrollments``). Reverts to
|
|
166
|
+
underscore suffixes (``simple_items_``, ``student_``).
|
|
162
167
|
|
|
163
168
|
* ``dataclasses``
|
|
164
169
|
|
|
@@ -226,6 +231,14 @@ due to that ``_id`` suffix.
|
|
|
226
231
|
For self referential relationships, the reverse side of the relationship will be named
|
|
227
232
|
with the ``_reverse`` suffix appended to it.
|
|
228
233
|
|
|
234
|
+
When multiple foreign keys or junction tables connect to the same target table,
|
|
235
|
+
relationships use qualifiers for disambiguation. One-to-many relationships use FK
|
|
236
|
+
column names (e.g., ``simple_items_parent_container``, ``simple_items_top_container``).
|
|
237
|
+
Many-to-many relationships use junction table names (e.g., ``students_enrollments``,
|
|
238
|
+
``students_waitlist``), except for self-referential cases which use FK column names
|
|
239
|
+
(e.g., ``parent``, ``child``). The ``nofknames`` option reverts to underscore suffixes
|
|
240
|
+
(``simple_items_``, ``student_``).
|
|
241
|
+
|
|
229
242
|
Customizing code generation logic
|
|
230
243
|
=================================
|
|
231
244
|
|