sqlacodegen 4.0.0rc3__tar.gz → 4.0.2__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.0rc3 → sqlacodegen-4.0.2}/CHANGES.rst +34 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/PKG-INFO +1 -1
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen/generators.py +173 -82
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen.egg-info/PKG-INFO +1 -1
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/tests/test_generator_declarative.py +193 -7
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/tests/test_generator_sqlmodel.py +202 -2
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/tests/test_generator_tables.py +108 -6
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/FUNDING.yml +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/ISSUE_TEMPLATE/features_request.yaml +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/dependabot.yml +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/pull_request_template.md +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/workflows/publish.yml +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/workflows/test.yml +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.gitignore +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.pre-commit-config.yaml +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/CONTRIBUTING.rst +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/LICENSE +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/README.rst +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/pyproject.toml +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/setup.cfg +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen/__init__.py +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen/__main__.py +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen/cli.py +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen/models.py +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen/py.typed +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen/utils.py +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen.egg-info/SOURCES.txt +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen.egg-info/dependency_links.txt +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen.egg-info/entry_points.txt +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen.egg-info/requires.txt +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen.egg-info/top_level.txt +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/tests/__init__.py +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/tests/conftest.py +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/tests/test_cli.py +0 -0
- {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/tests/test_generator_dataclass.py +0 -0
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
Version history
|
|
2
2
|
===============
|
|
3
3
|
|
|
4
|
+
**4.0.2**
|
|
5
|
+
|
|
6
|
+
- Fixed rendering of inherited keyword arguments for dialect-specific types that use
|
|
7
|
+
``**kwargs`` in their initializers (such as MySQL ``CHAR`` with ``collation``) while
|
|
8
|
+
preserving existing ``*args`` rendering behavior (PR by @hyoj0942)
|
|
9
|
+
- Fixed missing metadata argument when rendering plain tables with the SQLModel
|
|
10
|
+
- Added support for self-referential tables in the SQLModel generator (PR by @sheinbergon)
|
|
11
|
+
- Fixed empty dialect kwargs (e.g. ``postgresql_include=[]``) being included in
|
|
12
|
+
rendered indexes, tables, and columns (PR by @sheinbergon)
|
|
13
|
+
|
|
14
|
+
**4.0.1**
|
|
15
|
+
|
|
16
|
+
- Fix enum column definitions to explicitly include schema and name if reflected
|
|
17
|
+
via SQLAlchemy's Metadata (pr by @sheinbergon)
|
|
18
|
+
|
|
19
|
+
**4.0.0**
|
|
20
|
+
|
|
21
|
+
- **BACKWARD INCOMPATIBLE** API changes (for those who customize code generation by
|
|
22
|
+
subclassing the existing generators):
|
|
23
|
+
|
|
24
|
+
* Added new optional keyword argument, ``explicit_foreign_keys`` to
|
|
25
|
+
``DeclarativeGenerator``, to force foreign keys to be rendered as
|
|
26
|
+
``ClassName.attribute_name`` string references
|
|
27
|
+
* Removed the ``render_relationship_args()`` method from the SQLModel generator
|
|
28
|
+
* Added two new methods for customizing relationship rendering in
|
|
29
|
+
``DeclarativeGenerator``:
|
|
30
|
+
|
|
31
|
+
* ``render_relationship_annotation()``: returns the appropriate type annotation
|
|
32
|
+
(without the ``Mapped`` wrapper) for the relationship
|
|
33
|
+
* ``render_relationship_arguments()``: returns a dictionary of keyword arguments to
|
|
34
|
+
``sqlalchemy.orm.relationship()``
|
|
35
|
+
|
|
4
36
|
**4.0.0rc3**
|
|
5
37
|
|
|
6
38
|
- **BACKWARD INCOMPATIBLE** Relationship names changed when multiple FKs or junction tables
|
|
@@ -14,6 +46,8 @@ Version history
|
|
|
14
46
|
``students_enrollments``). Use ``--options nofknames`` to revert to old behavior. (PR by @sheinbergon)
|
|
15
47
|
- Fixed ``Index`` kwargs (e.g. ``mysql_length``) being ignored during code generation
|
|
16
48
|
(PR by @luliangce)
|
|
49
|
+
- Fixed the SQLModel generator not adding the ``foreign_keys`` parameters when
|
|
50
|
+
generating multiple relationships between the same two tables
|
|
17
51
|
|
|
18
52
|
**4.0.0rc2**
|
|
19
53
|
|
|
@@ -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
|
|
@@ -427,6 +427,7 @@ class TablesGenerator(CodeGenerator):
|
|
|
427
427
|
kwargs = {
|
|
428
428
|
key: repr(value) if isinstance(value, str) else value
|
|
429
429
|
for key, value in sorted(index.kwargs.items(), key=lambda item: item[0])
|
|
430
|
+
if value not in ([], {})
|
|
430
431
|
}
|
|
431
432
|
if index.unique:
|
|
432
433
|
kwargs["unique"] = True
|
|
@@ -541,6 +542,77 @@ class TablesGenerator(CodeGenerator):
|
|
|
541
542
|
else:
|
|
542
543
|
return render_callable("mapped_column", *args, kwargs=kwargs)
|
|
543
544
|
|
|
545
|
+
def _render_column_type_value(self, value: Any) -> str:
|
|
546
|
+
if isinstance(value, (JSONB, JSON)):
|
|
547
|
+
# Remove astext_type if it's the default
|
|
548
|
+
if isinstance(value.astext_type, Text) and value.astext_type.length is None:
|
|
549
|
+
value.astext_type = None # type: ignore[assignment]
|
|
550
|
+
else:
|
|
551
|
+
self.add_import(Text)
|
|
552
|
+
|
|
553
|
+
if isinstance(value, TextClause):
|
|
554
|
+
self.add_literal_import("sqlalchemy", "text")
|
|
555
|
+
return render_callable("text", repr(value.text))
|
|
556
|
+
|
|
557
|
+
return repr(value)
|
|
558
|
+
|
|
559
|
+
def _collect_inherited_init_kwargs(
|
|
560
|
+
self,
|
|
561
|
+
column_type: Any,
|
|
562
|
+
init_sig: inspect.Signature,
|
|
563
|
+
seen_param_names: set[str],
|
|
564
|
+
missing: object,
|
|
565
|
+
) -> dict[str, str]:
|
|
566
|
+
has_var_keyword = any(
|
|
567
|
+
param.kind is Parameter.VAR_KEYWORD
|
|
568
|
+
for param in init_sig.parameters.values()
|
|
569
|
+
)
|
|
570
|
+
has_var_positional = any(
|
|
571
|
+
param.kind is Parameter.VAR_POSITIONAL
|
|
572
|
+
for param in init_sig.parameters.values()
|
|
573
|
+
)
|
|
574
|
+
if not has_var_keyword or has_var_positional:
|
|
575
|
+
return {}
|
|
576
|
+
|
|
577
|
+
inherited_kwargs: dict[str, str] = {}
|
|
578
|
+
for supercls in column_type.__class__.__mro__[1:]:
|
|
579
|
+
if supercls is object:
|
|
580
|
+
break
|
|
581
|
+
|
|
582
|
+
try:
|
|
583
|
+
super_sig = inspect.signature(supercls.__init__)
|
|
584
|
+
except (TypeError, ValueError):
|
|
585
|
+
continue
|
|
586
|
+
|
|
587
|
+
for super_param in list(super_sig.parameters.values())[1:]:
|
|
588
|
+
if super_param.name.startswith("_"):
|
|
589
|
+
continue
|
|
590
|
+
|
|
591
|
+
if super_param.kind in (
|
|
592
|
+
Parameter.POSITIONAL_ONLY,
|
|
593
|
+
Parameter.VAR_POSITIONAL,
|
|
594
|
+
Parameter.VAR_KEYWORD,
|
|
595
|
+
):
|
|
596
|
+
continue
|
|
597
|
+
|
|
598
|
+
if super_param.name in seen_param_names:
|
|
599
|
+
continue
|
|
600
|
+
|
|
601
|
+
seen_param_names.add(super_param.name)
|
|
602
|
+
value = getattr(column_type, super_param.name, missing)
|
|
603
|
+
if value is missing:
|
|
604
|
+
continue
|
|
605
|
+
|
|
606
|
+
default = super_param.default
|
|
607
|
+
if default is not Parameter.empty and value == default:
|
|
608
|
+
continue
|
|
609
|
+
|
|
610
|
+
inherited_kwargs[super_param.name] = self._render_column_type_value(
|
|
611
|
+
value
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
return inherited_kwargs
|
|
615
|
+
|
|
544
616
|
def render_column_type(self, column: Column[Any]) -> str:
|
|
545
617
|
column_type = column.type
|
|
546
618
|
# Check if this is an enum column with a Python enum class
|
|
@@ -550,7 +622,14 @@ class TablesGenerator(CodeGenerator):
|
|
|
550
622
|
):
|
|
551
623
|
# Import SQLAlchemy Enum (will be handled in collect_imports)
|
|
552
624
|
self.add_import(Enum)
|
|
553
|
-
|
|
625
|
+
extra_kwargs = ""
|
|
626
|
+
if column_type.name is not None:
|
|
627
|
+
extra_kwargs += f", name={column_type.name!r}"
|
|
628
|
+
|
|
629
|
+
if column_type.schema is not None:
|
|
630
|
+
extra_kwargs += f", schema={column_type.schema!r}"
|
|
631
|
+
|
|
632
|
+
return f"Enum({enum_class_name}, values_callable=lambda cls: [member.value for member in cls]{extra_kwargs})"
|
|
554
633
|
|
|
555
634
|
args = []
|
|
556
635
|
kwargs: dict[str, Any] = {}
|
|
@@ -562,7 +641,14 @@ class TablesGenerator(CodeGenerator):
|
|
|
562
641
|
):
|
|
563
642
|
self.add_import(ARRAY)
|
|
564
643
|
self.add_import(Enum)
|
|
565
|
-
|
|
644
|
+
extra_kwargs = ""
|
|
645
|
+
if column_type.item_type.name is not None:
|
|
646
|
+
extra_kwargs += f", name={column_type.item_type.name!r}"
|
|
647
|
+
|
|
648
|
+
if column_type.item_type.schema is not None:
|
|
649
|
+
extra_kwargs += f", schema={column_type.item_type.schema!r}"
|
|
650
|
+
|
|
651
|
+
rendered_enum = f"Enum({enum_class_name}, values_callable=lambda cls: [member.value for member in cls]{extra_kwargs})"
|
|
566
652
|
if column_type.dimensions is not None:
|
|
567
653
|
kwargs["dimensions"] = repr(column_type.dimensions)
|
|
568
654
|
|
|
@@ -572,6 +658,8 @@ class TablesGenerator(CodeGenerator):
|
|
|
572
658
|
defaults = {param.name: param.default for param in sig.parameters.values()}
|
|
573
659
|
missing = object()
|
|
574
660
|
use_kwargs = False
|
|
661
|
+
seen_param_names: set[str] = set()
|
|
662
|
+
|
|
575
663
|
for param in list(sig.parameters.values())[1:]:
|
|
576
664
|
# Remove annoyances like _warn_on_bytestring
|
|
577
665
|
if param.name.startswith("_"):
|
|
@@ -580,32 +668,25 @@ class TablesGenerator(CodeGenerator):
|
|
|
580
668
|
use_kwargs = True
|
|
581
669
|
continue
|
|
582
670
|
|
|
671
|
+
seen_param_names.add(param.name)
|
|
583
672
|
value = getattr(column_type, param.name, missing)
|
|
584
|
-
|
|
585
|
-
if isinstance(value, (JSONB, JSON)):
|
|
586
|
-
# Remove astext_type if it's the default
|
|
587
|
-
if (
|
|
588
|
-
isinstance(value.astext_type, Text)
|
|
589
|
-
and value.astext_type.length is None
|
|
590
|
-
):
|
|
591
|
-
value.astext_type = None # type: ignore[assignment]
|
|
592
|
-
else:
|
|
593
|
-
self.add_import(Text)
|
|
594
|
-
|
|
595
673
|
default = defaults.get(param.name, missing)
|
|
596
|
-
if isinstance(value, TextClause):
|
|
597
|
-
self.add_literal_import("sqlalchemy", "text")
|
|
598
|
-
rendered_value = render_callable("text", repr(value.text))
|
|
599
|
-
else:
|
|
600
|
-
rendered_value = repr(value)
|
|
601
|
-
|
|
602
674
|
if value is missing or value == default:
|
|
603
675
|
use_kwargs = True
|
|
604
|
-
|
|
676
|
+
continue
|
|
677
|
+
|
|
678
|
+
rendered_value = self._render_column_type_value(value)
|
|
679
|
+
if use_kwargs:
|
|
605
680
|
kwargs[param.name] = rendered_value
|
|
606
681
|
else:
|
|
607
682
|
args.append(rendered_value)
|
|
608
683
|
|
|
684
|
+
kwargs.update(
|
|
685
|
+
self._collect_inherited_init_kwargs(
|
|
686
|
+
column_type, sig, seen_param_names, missing
|
|
687
|
+
)
|
|
688
|
+
)
|
|
689
|
+
|
|
609
690
|
vararg = next(
|
|
610
691
|
(
|
|
611
692
|
param.name
|
|
@@ -698,6 +779,9 @@ class TablesGenerator(CodeGenerator):
|
|
|
698
779
|
except Exception:
|
|
699
780
|
continue
|
|
700
781
|
|
|
782
|
+
if isinstance(value, list | dict) and not value:
|
|
783
|
+
continue
|
|
784
|
+
|
|
701
785
|
# Render values:
|
|
702
786
|
# - callable context (values_for_dict=False): produce a string expression.
|
|
703
787
|
# primitives use repr(value); custom objects stringify then repr().
|
|
@@ -1001,10 +1085,12 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
1001
1085
|
*,
|
|
1002
1086
|
indentation: str = " ",
|
|
1003
1087
|
base_class_name: str = "Base",
|
|
1088
|
+
explicit_foreign_keys: bool = False,
|
|
1004
1089
|
):
|
|
1005
1090
|
super().__init__(metadata, bind, options, indentation=indentation)
|
|
1006
1091
|
self.base_class_name: str = base_class_name
|
|
1007
1092
|
self.inflect_engine = inflect.engine()
|
|
1093
|
+
self.explicit_foreign_keys = explicit_foreign_keys
|
|
1008
1094
|
|
|
1009
1095
|
def generate_base(self) -> None:
|
|
1010
1096
|
self.base = Base(
|
|
@@ -1626,22 +1712,52 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
1626
1712
|
return f"{column_attr.name}: Mapped[{rendered_column_python_type}] = {rendered_column}"
|
|
1627
1713
|
|
|
1628
1714
|
def render_relationship(self, relationship: RelationshipAttribute) -> str:
|
|
1715
|
+
kwargs = self.render_relationship_arguments(relationship)
|
|
1716
|
+
annotation = self.render_relationship_annotation(relationship)
|
|
1717
|
+
rendered_relationship = render_callable(
|
|
1718
|
+
"relationship", repr(relationship.target.name), kwargs=kwargs
|
|
1719
|
+
)
|
|
1720
|
+
return f"{relationship.name}: Mapped[{annotation}] = {rendered_relationship}"
|
|
1721
|
+
|
|
1722
|
+
def render_relationship_annotation(
|
|
1723
|
+
self, relationship: RelationshipAttribute
|
|
1724
|
+
) -> str:
|
|
1725
|
+
match relationship.type:
|
|
1726
|
+
case RelationshipType.ONE_TO_MANY:
|
|
1727
|
+
return f"list[{relationship.target.name!r}]"
|
|
1728
|
+
case RelationshipType.ONE_TO_ONE | RelationshipType.MANY_TO_ONE:
|
|
1729
|
+
if relationship.constraint and any(
|
|
1730
|
+
col.nullable for col in relationship.constraint.columns
|
|
1731
|
+
):
|
|
1732
|
+
self.add_literal_import("typing", "Optional")
|
|
1733
|
+
return f"Optional[{relationship.target.name!r}]"
|
|
1734
|
+
else:
|
|
1735
|
+
return f"'{relationship.target.name}'"
|
|
1736
|
+
case RelationshipType.MANY_TO_MANY:
|
|
1737
|
+
return f"list[{relationship.target.name!r}]"
|
|
1738
|
+
|
|
1739
|
+
def render_relationship_arguments(
|
|
1740
|
+
self, relationship: RelationshipAttribute
|
|
1741
|
+
) -> Mapping[str, Any]:
|
|
1629
1742
|
def render_column_attrs(column_attrs: list[ColumnAttribute]) -> str:
|
|
1630
1743
|
rendered = []
|
|
1744
|
+
render_as_string = False
|
|
1631
1745
|
for attr in column_attrs:
|
|
1632
|
-
if attr.model is relationship.source:
|
|
1746
|
+
if not self.explicit_foreign_keys and attr.model is relationship.source:
|
|
1633
1747
|
rendered.append(attr.name)
|
|
1634
1748
|
else:
|
|
1635
|
-
rendered.append(
|
|
1749
|
+
rendered.append(f"{attr.model.name}.{attr.name}")
|
|
1750
|
+
render_as_string = True
|
|
1636
1751
|
|
|
1637
|
-
|
|
1752
|
+
joined = "[" + ", ".join(rendered) + "]"
|
|
1753
|
+
return repr(joined) if render_as_string else joined
|
|
1638
1754
|
|
|
1639
1755
|
def render_foreign_keys(column_attrs: list[ColumnAttribute]) -> str:
|
|
1640
1756
|
rendered = []
|
|
1641
1757
|
render_as_string = False
|
|
1642
1758
|
# Assume that column_attrs are all in relationship.source or none
|
|
1643
1759
|
for attr in column_attrs:
|
|
1644
|
-
if attr.model is relationship.source:
|
|
1760
|
+
if not self.explicit_foreign_keys and attr.model is relationship.source:
|
|
1645
1761
|
rendered.append(attr.name)
|
|
1646
1762
|
else:
|
|
1647
1763
|
rendered.append(f"{attr.model.name}.{attr.name}")
|
|
@@ -1697,33 +1813,7 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
1697
1813
|
if relationship.backref:
|
|
1698
1814
|
kwargs["back_populates"] = repr(relationship.backref.name)
|
|
1699
1815
|
|
|
1700
|
-
|
|
1701
|
-
"relationship", repr(relationship.target.name), kwargs=kwargs
|
|
1702
|
-
)
|
|
1703
|
-
|
|
1704
|
-
relationship_type: str
|
|
1705
|
-
if relationship.type == RelationshipType.ONE_TO_MANY:
|
|
1706
|
-
relationship_type = f"list['{relationship.target.name}']"
|
|
1707
|
-
elif relationship.type in (
|
|
1708
|
-
RelationshipType.ONE_TO_ONE,
|
|
1709
|
-
RelationshipType.MANY_TO_ONE,
|
|
1710
|
-
):
|
|
1711
|
-
relationship_type = f"'{relationship.target.name}'"
|
|
1712
|
-
if relationship.constraint and any(
|
|
1713
|
-
col.nullable for col in relationship.constraint.columns
|
|
1714
|
-
):
|
|
1715
|
-
self.add_literal_import("typing", "Optional")
|
|
1716
|
-
relationship_type = f"Optional[{relationship_type}]"
|
|
1717
|
-
elif relationship.type == RelationshipType.MANY_TO_MANY:
|
|
1718
|
-
relationship_type = f"list['{relationship.target.name}']"
|
|
1719
|
-
else:
|
|
1720
|
-
self.add_literal_import("typing", "Any")
|
|
1721
|
-
relationship_type = "Any"
|
|
1722
|
-
|
|
1723
|
-
return (
|
|
1724
|
-
f"{relationship.name}: Mapped[{relationship_type}] "
|
|
1725
|
-
f"= {rendered_relationship}"
|
|
1726
|
-
)
|
|
1816
|
+
return kwargs
|
|
1727
1817
|
|
|
1728
1818
|
|
|
1729
1819
|
class DataclassGenerator(DeclarativeGenerator):
|
|
@@ -1778,6 +1868,7 @@ class SQLModelGenerator(DeclarativeGenerator):
|
|
|
1778
1868
|
options,
|
|
1779
1869
|
indentation=indentation,
|
|
1780
1870
|
base_class_name=base_class_name,
|
|
1871
|
+
explicit_foreign_keys=True,
|
|
1781
1872
|
)
|
|
1782
1873
|
|
|
1783
1874
|
@property
|
|
@@ -1788,19 +1879,27 @@ class SQLModelGenerator(DeclarativeGenerator):
|
|
|
1788
1879
|
self.add_import(Column)
|
|
1789
1880
|
return render_callable("Column", *args, kwargs=kwargs)
|
|
1790
1881
|
|
|
1882
|
+
def render_table(self, table: Table) -> str:
|
|
1883
|
+
# Hack to fix #465 without breaking backwards compatibility
|
|
1884
|
+
self.base.metadata_ref = "SQLModel.metadata"
|
|
1885
|
+
|
|
1886
|
+
return super().render_table(table)
|
|
1887
|
+
|
|
1791
1888
|
def generate_base(self) -> None:
|
|
1792
1889
|
self.base = Base(
|
|
1793
1890
|
literal_imports=[],
|
|
1794
1891
|
declarations=[],
|
|
1795
|
-
metadata_ref="",
|
|
1892
|
+
metadata_ref="SQLModel.metadata",
|
|
1796
1893
|
)
|
|
1797
1894
|
|
|
1798
1895
|
def collect_imports(self, models: Iterable[Model]) -> None:
|
|
1799
1896
|
super(DeclarativeGenerator, self).collect_imports(models)
|
|
1800
1897
|
if any(isinstance(model, ModelClass) for model in models):
|
|
1898
|
+
self.add_literal_import("sqlmodel", "Field")
|
|
1899
|
+
|
|
1900
|
+
if models:
|
|
1801
1901
|
self.remove_literal_import("sqlalchemy", "MetaData")
|
|
1802
1902
|
self.add_literal_import("sqlmodel", "SQLModel")
|
|
1803
|
-
self.add_literal_import("sqlmodel", "Field")
|
|
1804
1903
|
|
|
1805
1904
|
def collect_imports_for_model(self, model: Model) -> None:
|
|
1806
1905
|
super(DeclarativeGenerator, self).collect_imports_for_model(model)
|
|
@@ -1858,34 +1957,26 @@ class SQLModelGenerator(DeclarativeGenerator):
|
|
|
1858
1957
|
return f"{column_attr.name}: {rendered_column_python_type} = {rendered_field}"
|
|
1859
1958
|
|
|
1860
1959
|
def render_relationship(self, relationship: RelationshipAttribute) -> str:
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1960
|
+
kwargs = self.render_relationship_arguments(relationship)
|
|
1961
|
+
annotation = self.render_relationship_annotation(relationship)
|
|
1962
|
+
|
|
1963
|
+
native_kwargs: dict[str, Any] = {}
|
|
1964
|
+
non_native_kwargs: dict[str, Any] = {}
|
|
1965
|
+
for key, value in kwargs.items():
|
|
1966
|
+
# The following keyword arguments are natively supported in Relationship
|
|
1967
|
+
if key in ("back_populates", "cascade_delete", "passive_deletes"):
|
|
1968
|
+
native_kwargs[key] = value
|
|
1969
|
+
else:
|
|
1970
|
+
non_native_kwargs[key] = value
|
|
1865
1971
|
|
|
1866
|
-
if
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1972
|
+
if non_native_kwargs:
|
|
1973
|
+
native_kwargs["sa_relationship_kwargs"] = (
|
|
1974
|
+
"{"
|
|
1975
|
+
+ ", ".join(
|
|
1976
|
+
f"{key!r}: {value}" for key, value in non_native_kwargs.items()
|
|
1977
|
+
)
|
|
1978
|
+
+ "}"
|
|
1979
|
+
)
|
|
1874
1980
|
|
|
1875
|
-
rendered_field = render_callable("Relationship",
|
|
1981
|
+
rendered_field = render_callable("Relationship", kwargs=native_kwargs)
|
|
1876
1982
|
return f"{relationship.name}: {annotation} = {rendered_field}"
|
|
1877
|
-
|
|
1878
|
-
def render_relationship_args(self, arguments: str) -> list[str]:
|
|
1879
|
-
argument_list = arguments.split(",")
|
|
1880
|
-
# delete ')' and ' ' from args
|
|
1881
|
-
argument_list[-1] = argument_list[-1][:-1]
|
|
1882
|
-
argument_list = [argument[1:] for argument in argument_list]
|
|
1883
|
-
|
|
1884
|
-
rendered_args: list[str] = []
|
|
1885
|
-
for arg in argument_list:
|
|
1886
|
-
if "back_populates" in arg:
|
|
1887
|
-
rendered_args.append(arg)
|
|
1888
|
-
if "uselist=False" in arg:
|
|
1889
|
-
rendered_args.append("sa_relationship_kwargs={'uselist': False}")
|
|
1890
|
-
|
|
1891
|
-
return rendered_args
|
|
@@ -321,6 +321,40 @@ class Num2(Base):
|
|
|
321
321
|
)
|
|
322
322
|
|
|
323
323
|
|
|
324
|
+
@pytest.mark.parametrize("engine", ["mysql"], indirect=["engine"])
|
|
325
|
+
@pytest.mark.parametrize("generator", [["keep_dialect_types"]], indirect=True)
|
|
326
|
+
def test_keep_dialect_types_keeps_mysql_char_collation(
|
|
327
|
+
generator: CodeGenerator,
|
|
328
|
+
) -> None:
|
|
329
|
+
from sqlalchemy.dialects.mysql import CHAR as MYSQL_CHAR
|
|
330
|
+
from sqlalchemy.dialects.mysql import INTEGER as MYSQL_INTEGER
|
|
331
|
+
|
|
332
|
+
Table(
|
|
333
|
+
"result_logs",
|
|
334
|
+
generator.metadata,
|
|
335
|
+
Column("id", MYSQL_INTEGER, primary_key=True),
|
|
336
|
+
Column("result_code", MYSQL_CHAR(1, collation="utf8mb3_bin"), nullable=False),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
validate_code(
|
|
340
|
+
generator.generate(),
|
|
341
|
+
"""\
|
|
342
|
+
from sqlalchemy.dialects.mysql import CHAR, INTEGER
|
|
343
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
344
|
+
|
|
345
|
+
class Base(DeclarativeBase):
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class ResultLogs(Base):
|
|
350
|
+
__tablename__ = 'result_logs'
|
|
351
|
+
|
|
352
|
+
id: Mapped[int] = mapped_column(INTEGER, primary_key=True)
|
|
353
|
+
result_code: Mapped[str] = mapped_column(CHAR(1, collation='utf8mb3_bin'), nullable=False)
|
|
354
|
+
""",
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
324
358
|
def test_onetomany(generator: CodeGenerator) -> None:
|
|
325
359
|
Table(
|
|
326
360
|
"simple_items",
|
|
@@ -2508,14 +2542,14 @@ def test_enum_shared_values(generator: CodeGenerator) -> None:
|
|
|
2508
2542
|
__tablename__ = 'accounts'
|
|
2509
2543
|
|
|
2510
2544
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
2511
|
-
status: Mapped[StatusEnum] = mapped_column(Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls]), nullable=False)
|
|
2545
|
+
status: Mapped[StatusEnum] = mapped_column(Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls], name='status_enum'), nullable=False)
|
|
2512
2546
|
|
|
2513
2547
|
|
|
2514
2548
|
class Users(Base):
|
|
2515
2549
|
__tablename__ = 'users'
|
|
2516
2550
|
|
|
2517
2551
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
2518
|
-
status: Mapped[StatusEnum] = mapped_column(Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls]), nullable=False)
|
|
2552
|
+
status: Mapped[StatusEnum] = mapped_column(Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls], name='status_enum'), nullable=False)
|
|
2519
2553
|
""",
|
|
2520
2554
|
)
|
|
2521
2555
|
|
|
@@ -2851,7 +2885,7 @@ def test_array_enum_named(generator: CodeGenerator) -> None:
|
|
|
2851
2885
|
__tablename__ = 'users'
|
|
2852
2886
|
|
|
2853
2887
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
2854
|
-
roles: Mapped[list[RoleEnum]] = mapped_column(ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls])), nullable=False)
|
|
2888
|
+
roles: Mapped[list[RoleEnum]] = mapped_column(ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls], name='role_enum')), nullable=False)
|
|
2855
2889
|
""",
|
|
2856
2890
|
)
|
|
2857
2891
|
|
|
@@ -2927,7 +2961,7 @@ def test_array_enum_nullable(generator: CodeGenerator) -> None:
|
|
|
2927
2961
|
__tablename__ = 'users'
|
|
2928
2962
|
|
|
2929
2963
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
2930
|
-
roles: Mapped[Optional[list[RoleEnum]]] = mapped_column(ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls])))
|
|
2964
|
+
roles: Mapped[Optional[list[RoleEnum]]] = mapped_column(ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls], name='role_enum')))
|
|
2931
2965
|
""",
|
|
2932
2966
|
)
|
|
2933
2967
|
|
|
@@ -2965,7 +2999,7 @@ def test_array_enum_with_dimensions(generator: CodeGenerator) -> None:
|
|
|
2965
2999
|
__tablename__ = 'items'
|
|
2966
3000
|
|
|
2967
3001
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
2968
|
-
tag_matrix: Mapped[list[list[TagEnum]]] = mapped_column(ARRAY(Enum(TagEnum, values_callable=lambda cls: [member.value for member in cls]), dimensions=2), nullable=False)
|
|
3002
|
+
tag_matrix: Mapped[list[list[TagEnum]]] = mapped_column(ARRAY(Enum(TagEnum, values_callable=lambda cls: [member.value for member in cls], name='tag_enum'), dimensions=2), nullable=False)
|
|
2969
3003
|
""",
|
|
2970
3004
|
)
|
|
2971
3005
|
|
|
@@ -3043,7 +3077,159 @@ def test_array_enum_shared_with_regular_enum(generator: CodeGenerator) -> None:
|
|
|
3043
3077
|
__tablename__ = 'users'
|
|
3044
3078
|
|
|
3045
3079
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
3046
|
-
primary_role: Mapped[RoleEnum] = mapped_column(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls]), nullable=False)
|
|
3047
|
-
all_roles: Mapped[list[RoleEnum]] = mapped_column(ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls])), nullable=False)
|
|
3080
|
+
primary_role: Mapped[RoleEnum] = mapped_column(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls], name='role_enum'), nullable=False)
|
|
3081
|
+
all_roles: Mapped[list[RoleEnum]] = mapped_column(ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls], name='role_enum')), nullable=False)
|
|
3082
|
+
""",
|
|
3083
|
+
)
|
|
3084
|
+
|
|
3085
|
+
|
|
3086
|
+
def test_enum_named_with_schema(generator: CodeGenerator) -> None:
|
|
3087
|
+
Table(
|
|
3088
|
+
"my_table",
|
|
3089
|
+
generator.metadata,
|
|
3090
|
+
Column("id", INTEGER, primary_key=True),
|
|
3091
|
+
Column(
|
|
3092
|
+
"status",
|
|
3093
|
+
SAEnum("active", "inactive", name="status_enum", schema="custom_schema"),
|
|
3094
|
+
nullable=False,
|
|
3095
|
+
),
|
|
3096
|
+
schema="custom_schema",
|
|
3097
|
+
)
|
|
3098
|
+
|
|
3099
|
+
validate_code(
|
|
3100
|
+
generator.generate(),
|
|
3101
|
+
"""\
|
|
3102
|
+
import enum
|
|
3103
|
+
|
|
3104
|
+
from sqlalchemy import Enum, Integer
|
|
3105
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
3106
|
+
|
|
3107
|
+
class Base(DeclarativeBase):
|
|
3108
|
+
pass
|
|
3109
|
+
|
|
3110
|
+
|
|
3111
|
+
class StatusEnum(str, enum.Enum):
|
|
3112
|
+
ACTIVE = 'active'
|
|
3113
|
+
INACTIVE = 'inactive'
|
|
3114
|
+
|
|
3115
|
+
|
|
3116
|
+
class MyTable(Base):
|
|
3117
|
+
__tablename__ = 'my_table'
|
|
3118
|
+
__table_args__ = {'schema': 'custom_schema'}
|
|
3119
|
+
|
|
3120
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
3121
|
+
status: Mapped[StatusEnum] = mapped_column(Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls], name='status_enum', schema='custom_schema'), nullable=False)
|
|
3122
|
+
""",
|
|
3123
|
+
)
|
|
3124
|
+
|
|
3125
|
+
|
|
3126
|
+
def test_array_enum_named_with_schema(generator: CodeGenerator) -> None:
|
|
3127
|
+
Table(
|
|
3128
|
+
"my_table",
|
|
3129
|
+
generator.metadata,
|
|
3130
|
+
Column("id", INTEGER, primary_key=True),
|
|
3131
|
+
Column(
|
|
3132
|
+
"tags",
|
|
3133
|
+
ARRAY(SAEnum("a", "b", name="tag_enum", schema="custom_schema")),
|
|
3134
|
+
nullable=False,
|
|
3135
|
+
),
|
|
3136
|
+
schema="custom_schema",
|
|
3137
|
+
)
|
|
3138
|
+
|
|
3139
|
+
validate_code(
|
|
3140
|
+
generator.generate(),
|
|
3141
|
+
"""\
|
|
3142
|
+
import enum
|
|
3143
|
+
|
|
3144
|
+
from sqlalchemy import ARRAY, Enum, Integer
|
|
3145
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
3146
|
+
|
|
3147
|
+
class Base(DeclarativeBase):
|
|
3148
|
+
pass
|
|
3149
|
+
|
|
3150
|
+
|
|
3151
|
+
class TagEnum(str, enum.Enum):
|
|
3152
|
+
A = 'a'
|
|
3153
|
+
B = 'b'
|
|
3154
|
+
|
|
3155
|
+
|
|
3156
|
+
class MyTable(Base):
|
|
3157
|
+
__tablename__ = 'my_table'
|
|
3158
|
+
__table_args__ = {'schema': 'custom_schema'}
|
|
3159
|
+
|
|
3160
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
3161
|
+
tags: Mapped[list[TagEnum]] = mapped_column(ARRAY(Enum(TagEnum, values_callable=lambda cls: [member.value for member in cls], name='tag_enum', schema='custom_schema')), nullable=False)
|
|
3162
|
+
""",
|
|
3163
|
+
)
|
|
3164
|
+
|
|
3165
|
+
|
|
3166
|
+
def test_index_with_empty_kwargs(generator: CodeGenerator) -> None:
|
|
3167
|
+
simple_items = Table(
|
|
3168
|
+
"simple_items",
|
|
3169
|
+
generator.metadata,
|
|
3170
|
+
Column("id", INTEGER, primary_key=True),
|
|
3171
|
+
Column("name", VARCHAR),
|
|
3172
|
+
)
|
|
3173
|
+
simple_items.indexes.add(
|
|
3174
|
+
Index(
|
|
3175
|
+
"idx_name",
|
|
3176
|
+
simple_items.c.name,
|
|
3177
|
+
postgresql_using="gist",
|
|
3178
|
+
postgresql_include=[],
|
|
3179
|
+
)
|
|
3180
|
+
)
|
|
3181
|
+
|
|
3182
|
+
validate_code(
|
|
3183
|
+
generator.generate(),
|
|
3184
|
+
"""\
|
|
3185
|
+
from typing import Optional
|
|
3186
|
+
|
|
3187
|
+
from sqlalchemy import Index, Integer, String
|
|
3188
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
3189
|
+
|
|
3190
|
+
class Base(DeclarativeBase):
|
|
3191
|
+
pass
|
|
3192
|
+
|
|
3193
|
+
|
|
3194
|
+
class SimpleItems(Base):
|
|
3195
|
+
__tablename__ = 'simple_items'
|
|
3196
|
+
__table_args__ = (
|
|
3197
|
+
Index('idx_name', 'name', postgresql_using='gist'),
|
|
3198
|
+
)
|
|
3199
|
+
|
|
3200
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
3201
|
+
name: Mapped[Optional[str]] = mapped_column(String)
|
|
3202
|
+
""",
|
|
3203
|
+
)
|
|
3204
|
+
|
|
3205
|
+
|
|
3206
|
+
@pytest.mark.parametrize("generator", [["include_dialect_options"]], indirect=True)
|
|
3207
|
+
def test_include_dialect_options_empty_values_skipped(
|
|
3208
|
+
generator: CodeGenerator,
|
|
3209
|
+
) -> None:
|
|
3210
|
+
Table(
|
|
3211
|
+
"t_opts3",
|
|
3212
|
+
generator.metadata,
|
|
3213
|
+
Column("id", INTEGER, primary_key=True),
|
|
3214
|
+
mysql_engine="InnoDB",
|
|
3215
|
+
mysql_partition_by=[],
|
|
3216
|
+
mysql_PROPERTIES={},
|
|
3217
|
+
)
|
|
3218
|
+
|
|
3219
|
+
validate_code(
|
|
3220
|
+
generator.generate(),
|
|
3221
|
+
"""\
|
|
3222
|
+
from sqlalchemy import Integer
|
|
3223
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
3224
|
+
|
|
3225
|
+
class Base(DeclarativeBase):
|
|
3226
|
+
pass
|
|
3227
|
+
|
|
3228
|
+
|
|
3229
|
+
class TOpts3(Base):
|
|
3230
|
+
__tablename__ = 't_opts3'
|
|
3231
|
+
__table_args__ = {'mysql_engine': 'InnoDB'}
|
|
3232
|
+
|
|
3233
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
3048
3234
|
""",
|
|
3049
3235
|
)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
4
|
from _pytest.fixtures import FixtureRequest
|
|
5
|
+
from sqlalchemy import Enum as SAEnum
|
|
5
6
|
from sqlalchemy import Uuid
|
|
6
7
|
from sqlalchemy.engine import Engine
|
|
7
8
|
from sqlalchemy.schema import (
|
|
@@ -13,7 +14,7 @@ from sqlalchemy.schema import (
|
|
|
13
14
|
Table,
|
|
14
15
|
UniqueConstraint,
|
|
15
16
|
)
|
|
16
|
-
from sqlalchemy.types import INTEGER, VARCHAR
|
|
17
|
+
from sqlalchemy.types import ARRAY, INTEGER, VARCHAR
|
|
17
18
|
|
|
18
19
|
from sqlacodegen.generators import CodeGenerator, SQLModelGenerator
|
|
19
20
|
|
|
@@ -142,6 +143,66 @@ back_populates='simple_goods')
|
|
|
142
143
|
)
|
|
143
144
|
|
|
144
145
|
|
|
146
|
+
def test_onetomany_multiref(generator: CodeGenerator) -> None:
|
|
147
|
+
Table(
|
|
148
|
+
"simple_items_multiref",
|
|
149
|
+
generator.metadata,
|
|
150
|
+
Column("id", INTEGER, primary_key=True),
|
|
151
|
+
Column("parent_container_id", INTEGER),
|
|
152
|
+
Column("top_container_id", INTEGER, nullable=False),
|
|
153
|
+
ForeignKeyConstraint(
|
|
154
|
+
["parent_container_id"], ["simple_containers_multiref.id"]
|
|
155
|
+
),
|
|
156
|
+
ForeignKeyConstraint(["top_container_id"], ["simple_containers_multiref.id"]),
|
|
157
|
+
)
|
|
158
|
+
Table(
|
|
159
|
+
"simple_containers_multiref",
|
|
160
|
+
generator.metadata,
|
|
161
|
+
Column("id", INTEGER, primary_key=True),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
validate_code(
|
|
165
|
+
generator.generate(),
|
|
166
|
+
"""\
|
|
167
|
+
from typing import Optional
|
|
168
|
+
|
|
169
|
+
from sqlalchemy import Column, ForeignKey, Integer
|
|
170
|
+
from sqlmodel import Field, Relationship, SQLModel
|
|
171
|
+
|
|
172
|
+
class SimpleContainersMultiref(SQLModel, table=True):
|
|
173
|
+
__tablename__ = 'simple_containers_multiref'
|
|
174
|
+
|
|
175
|
+
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
|
|
176
|
+
|
|
177
|
+
simple_items_multiref_parent_container: list['SimpleItemsMultiref'] = \
|
|
178
|
+
Relationship(back_populates='parent_container', sa_relationship_kwargs={\
|
|
179
|
+
'foreign_keys': '[SimpleItemsMultiref.parent_container_id]'})
|
|
180
|
+
simple_items_multiref_top_container: list['SimpleItemsMultiref'] = \
|
|
181
|
+
Relationship(back_populates='top_container', sa_relationship_kwargs={'foreign_keys': \
|
|
182
|
+
'[SimpleItemsMultiref.top_container_id]'})
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class SimpleItemsMultiref(SQLModel, table=True):
|
|
186
|
+
__tablename__ = 'simple_items_multiref'
|
|
187
|
+
|
|
188
|
+
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
|
|
189
|
+
top_container_id: int = \
|
|
190
|
+
Field(sa_column=Column('top_container_id', \
|
|
191
|
+
ForeignKey('simple_containers_multiref.id'), nullable=False))
|
|
192
|
+
parent_container_id: Optional[int] = \
|
|
193
|
+
Field(default=None, sa_column=Column('parent_container_id', \
|
|
194
|
+
ForeignKey('simple_containers_multiref.id')))
|
|
195
|
+
|
|
196
|
+
parent_container: Optional['SimpleContainersMultiref'] = Relationship(\
|
|
197
|
+
back_populates='simple_items_multiref_parent_container', sa_relationship_kwargs={\
|
|
198
|
+
'foreign_keys': '[SimpleItemsMultiref.parent_container_id]'})
|
|
199
|
+
top_container: 'SimpleContainersMultiref' = Relationship(\
|
|
200
|
+
back_populates='simple_items_multiref_top_container', sa_relationship_kwargs={\
|
|
201
|
+
'foreign_keys': '[SimpleItemsMultiref.top_container_id]'})
|
|
202
|
+
""",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
145
206
|
def test_onetoone(generator: CodeGenerator) -> None:
|
|
146
207
|
Table(
|
|
147
208
|
"simple_onetoone",
|
|
@@ -167,7 +228,7 @@ def test_onetoone(generator: CodeGenerator) -> None:
|
|
|
167
228
|
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
|
|
168
229
|
|
|
169
230
|
simple_onetoone: Optional['SimpleOnetoone'] = Relationship(\
|
|
170
|
-
sa_relationship_kwargs={'uselist': False}
|
|
231
|
+
back_populates='other_item', sa_relationship_kwargs={'uselist': False})
|
|
171
232
|
|
|
172
233
|
|
|
173
234
|
class SimpleOnetoone(SQLModel, table=True):
|
|
@@ -269,3 +330,142 @@ def test_synthetic_enum_generation(generator: CodeGenerator) -> None:
|
|
|
269
330
|
status: AccountsStatus = Field(sa_column=Column('status', Enum(AccountsStatus, values_callable=lambda cls: [member.value for member in cls]), nullable=False))
|
|
270
331
|
""",
|
|
271
332
|
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_array_enum_named_with_schema(generator: CodeGenerator) -> None:
|
|
336
|
+
Table(
|
|
337
|
+
"my_table",
|
|
338
|
+
generator.metadata,
|
|
339
|
+
Column("id", INTEGER, primary_key=True),
|
|
340
|
+
Column(
|
|
341
|
+
"tags",
|
|
342
|
+
ARRAY(SAEnum("a", "b", name="tag_enum", schema="custom_schema")),
|
|
343
|
+
nullable=False,
|
|
344
|
+
),
|
|
345
|
+
schema="custom_schema",
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
validate_code(
|
|
349
|
+
generator.generate(),
|
|
350
|
+
"""\
|
|
351
|
+
import enum
|
|
352
|
+
|
|
353
|
+
from sqlalchemy import ARRAY, Column, Enum, Integer
|
|
354
|
+
from sqlmodel import Field, SQLModel
|
|
355
|
+
|
|
356
|
+
class TagEnum(str, enum.Enum):
|
|
357
|
+
A = 'a'
|
|
358
|
+
B = 'b'
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class MyTable(SQLModel, table=True):
|
|
362
|
+
__tablename__ = 'my_table'
|
|
363
|
+
__table_args__ = {'schema': 'custom_schema'}
|
|
364
|
+
|
|
365
|
+
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
|
|
366
|
+
tags: list[TagEnum] = Field(sa_column=Column('tags', ARRAY(Enum(TagEnum, values_callable=lambda cls: [member.value for member in cls], name='tag_enum', schema='custom_schema')), nullable=False))
|
|
367
|
+
""",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_fallback_table(generator: CodeGenerator) -> None:
|
|
372
|
+
Table(
|
|
373
|
+
"simple_fallback",
|
|
374
|
+
generator.metadata,
|
|
375
|
+
Column("field", VARCHAR(20), nullable=False),
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
validate_code(
|
|
379
|
+
generator.generate(),
|
|
380
|
+
"""\
|
|
381
|
+
from sqlalchemy import Column, String, Table
|
|
382
|
+
from sqlmodel import SQLModel
|
|
383
|
+
|
|
384
|
+
t_simple_fallback = Table(
|
|
385
|
+
'simple_fallback', SQLModel.metadata,
|
|
386
|
+
Column('field', String(20), nullable=False)
|
|
387
|
+
)
|
|
388
|
+
""",
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def test_onetomany_selfref(generator: CodeGenerator) -> None:
|
|
393
|
+
Table(
|
|
394
|
+
"simple_items",
|
|
395
|
+
generator.metadata,
|
|
396
|
+
Column("id", INTEGER, primary_key=True),
|
|
397
|
+
Column("parent_item_id", INTEGER),
|
|
398
|
+
ForeignKeyConstraint(["parent_item_id"], ["simple_items.id"]),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
validate_code(
|
|
402
|
+
generator.generate(),
|
|
403
|
+
"""\
|
|
404
|
+
from typing import Optional
|
|
405
|
+
|
|
406
|
+
from sqlalchemy import Column, ForeignKey, Integer
|
|
407
|
+
from sqlmodel import Field, Relationship, SQLModel
|
|
408
|
+
|
|
409
|
+
class SimpleItems(SQLModel, table=True):
|
|
410
|
+
__tablename__ = 'simple_items'
|
|
411
|
+
|
|
412
|
+
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
|
|
413
|
+
parent_item_id: Optional[int] = Field(default=None, sa_column=Column(\
|
|
414
|
+
'parent_item_id', ForeignKey('simple_items.id')))
|
|
415
|
+
|
|
416
|
+
parent_item: Optional['SimpleItems'] = Relationship(\
|
|
417
|
+
back_populates='parent_item_reverse', sa_relationship_kwargs={\
|
|
418
|
+
'remote_side': '[SimpleItems.id]'})
|
|
419
|
+
parent_item_reverse: list['SimpleItems'] = Relationship(\
|
|
420
|
+
back_populates='parent_item', sa_relationship_kwargs={\
|
|
421
|
+
'remote_side': '[SimpleItems.parent_item_id]'})
|
|
422
|
+
""",
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def test_onetomany_selfref_multi(generator: CodeGenerator) -> None:
|
|
427
|
+
Table(
|
|
428
|
+
"simple_items_selfref",
|
|
429
|
+
generator.metadata,
|
|
430
|
+
Column("id", INTEGER, primary_key=True),
|
|
431
|
+
Column("parent_item_id", INTEGER),
|
|
432
|
+
Column("top_item_id", INTEGER),
|
|
433
|
+
ForeignKeyConstraint(["parent_item_id"], ["simple_items_selfref.id"]),
|
|
434
|
+
ForeignKeyConstraint(["top_item_id"], ["simple_items_selfref.id"]),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
validate_code(
|
|
438
|
+
generator.generate(),
|
|
439
|
+
"""\
|
|
440
|
+
from typing import Optional
|
|
441
|
+
|
|
442
|
+
from sqlalchemy import Column, ForeignKey, Integer
|
|
443
|
+
from sqlmodel import Field, Relationship, SQLModel
|
|
444
|
+
|
|
445
|
+
class SimpleItemsSelfref(SQLModel, table=True):
|
|
446
|
+
__tablename__ = 'simple_items_selfref'
|
|
447
|
+
|
|
448
|
+
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
|
|
449
|
+
parent_item_id: Optional[int] = Field(default=None, sa_column=Column(\
|
|
450
|
+
'parent_item_id', ForeignKey('simple_items_selfref.id')))
|
|
451
|
+
top_item_id: Optional[int] = Field(default=None, sa_column=Column(\
|
|
452
|
+
'top_item_id', ForeignKey('simple_items_selfref.id')))
|
|
453
|
+
|
|
454
|
+
parent_item: Optional['SimpleItemsSelfref'] = Relationship(\
|
|
455
|
+
back_populates='parent_item_reverse', sa_relationship_kwargs={\
|
|
456
|
+
'remote_side': '[SimpleItemsSelfref.id]', \
|
|
457
|
+
'foreign_keys': '[SimpleItemsSelfref.parent_item_id]'})
|
|
458
|
+
parent_item_reverse: list['SimpleItemsSelfref'] = Relationship(\
|
|
459
|
+
back_populates='parent_item', sa_relationship_kwargs={\
|
|
460
|
+
'remote_side': '[SimpleItemsSelfref.parent_item_id]', \
|
|
461
|
+
'foreign_keys': '[SimpleItemsSelfref.parent_item_id]'})
|
|
462
|
+
top_item: Optional['SimpleItemsSelfref'] = Relationship(\
|
|
463
|
+
back_populates='top_item_reverse', sa_relationship_kwargs={\
|
|
464
|
+
'remote_side': '[SimpleItemsSelfref.id]', \
|
|
465
|
+
'foreign_keys': '[SimpleItemsSelfref.top_item_id]'})
|
|
466
|
+
top_item_reverse: list['SimpleItemsSelfref'] = Relationship(\
|
|
467
|
+
back_populates='top_item', sa_relationship_kwargs={\
|
|
468
|
+
'remote_side': '[SimpleItemsSelfref.top_item_id]', \
|
|
469
|
+
'foreign_keys': '[SimpleItemsSelfref.top_item_id]'})
|
|
470
|
+
""",
|
|
471
|
+
)
|
|
@@ -76,7 +76,7 @@ def test_fancy_coltypes(generator: CodeGenerator) -> None:
|
|
|
76
76
|
|
|
77
77
|
t_simple_items = Table(
|
|
78
78
|
'simple_items', metadata,
|
|
79
|
-
Column('enum', Enum(Blah, values_callable=lambda cls: [member.value for member in cls])),
|
|
79
|
+
Column('enum', Enum(Blah, values_callable=lambda cls: [member.value for member in cls], name='blah', schema='someschema')),
|
|
80
80
|
Column('bool', Boolean),
|
|
81
81
|
Column('vector', VECTOR(3)),
|
|
82
82
|
Column('number', Numeric(10, asdecimal=False)),
|
|
@@ -309,13 +309,13 @@ def test_enum_shared_values(generator: CodeGenerator) -> None:
|
|
|
309
309
|
t_accounts = Table(
|
|
310
310
|
'accounts', metadata,
|
|
311
311
|
Column('id', Integer, primary_key=True),
|
|
312
|
-
Column('status', Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls]))
|
|
312
|
+
Column('status', Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls], name='status_enum'))
|
|
313
313
|
)
|
|
314
314
|
|
|
315
315
|
t_users = Table(
|
|
316
316
|
'users', metadata,
|
|
317
317
|
Column('id', Integer, primary_key=True),
|
|
318
|
-
Column('status', Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls]))
|
|
318
|
+
Column('status', Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls], name='status_enum'))
|
|
319
319
|
)
|
|
320
320
|
""",
|
|
321
321
|
)
|
|
@@ -348,7 +348,7 @@ def test_array_enum_named(generator: CodeGenerator) -> None:
|
|
|
348
348
|
t_users = Table(
|
|
349
349
|
'users', metadata,
|
|
350
350
|
Column('id', Integer, primary_key=True),
|
|
351
|
-
Column('roles', ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls])))
|
|
351
|
+
Column('roles', ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls], name='role_enum')))
|
|
352
352
|
)
|
|
353
353
|
""",
|
|
354
354
|
)
|
|
@@ -386,13 +386,87 @@ def test_array_enum_shared(generator: CodeGenerator) -> None:
|
|
|
386
386
|
t_groups = Table(
|
|
387
387
|
'groups', metadata,
|
|
388
388
|
Column('id', Integer, primary_key=True),
|
|
389
|
-
Column('allowed_roles', ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls])))
|
|
389
|
+
Column('allowed_roles', ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls], name='role_enum')))
|
|
390
390
|
)
|
|
391
391
|
|
|
392
392
|
t_users = Table(
|
|
393
393
|
'users', metadata,
|
|
394
394
|
Column('id', Integer, primary_key=True),
|
|
395
|
-
Column('roles', ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls])))
|
|
395
|
+
Column('roles', ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls], name='role_enum')))
|
|
396
|
+
)
|
|
397
|
+
""",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def test_enum_named_with_schema(generator: CodeGenerator) -> None:
|
|
402
|
+
Table(
|
|
403
|
+
"my_table",
|
|
404
|
+
generator.metadata,
|
|
405
|
+
Column("id", INTEGER, primary_key=True),
|
|
406
|
+
Column(
|
|
407
|
+
"status",
|
|
408
|
+
SAEnum("active", "inactive", name="status_enum", schema="custom_schema"),
|
|
409
|
+
),
|
|
410
|
+
schema="custom_schema",
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
validate_code(
|
|
414
|
+
generator.generate(),
|
|
415
|
+
"""\
|
|
416
|
+
import enum
|
|
417
|
+
|
|
418
|
+
from sqlalchemy import Column, Enum, Integer, MetaData, Table
|
|
419
|
+
|
|
420
|
+
metadata = MetaData()
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class StatusEnum(str, enum.Enum):
|
|
424
|
+
ACTIVE = 'active'
|
|
425
|
+
INACTIVE = 'inactive'
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
t_my_table = Table(
|
|
429
|
+
'my_table', metadata,
|
|
430
|
+
Column('id', Integer, primary_key=True),
|
|
431
|
+
Column('status', Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls], name='status_enum', schema='custom_schema')),
|
|
432
|
+
schema='custom_schema'
|
|
433
|
+
)
|
|
434
|
+
""",
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_array_enum_named_with_schema(generator: CodeGenerator) -> None:
|
|
439
|
+
Table(
|
|
440
|
+
"my_table",
|
|
441
|
+
generator.metadata,
|
|
442
|
+
Column("id", INTEGER, primary_key=True),
|
|
443
|
+
Column(
|
|
444
|
+
"tags",
|
|
445
|
+
ARRAY(SAEnum("a", "b", name="tag_enum", schema="custom_schema")),
|
|
446
|
+
),
|
|
447
|
+
schema="custom_schema",
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
validate_code(
|
|
451
|
+
generator.generate(),
|
|
452
|
+
"""\
|
|
453
|
+
import enum
|
|
454
|
+
|
|
455
|
+
from sqlalchemy import ARRAY, Column, Enum, Integer, MetaData, Table
|
|
456
|
+
|
|
457
|
+
metadata = MetaData()
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class TagEnum(str, enum.Enum):
|
|
461
|
+
A = 'a'
|
|
462
|
+
B = 'b'
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
t_my_table = Table(
|
|
466
|
+
'my_table', metadata,
|
|
467
|
+
Column('id', Integer, primary_key=True),
|
|
468
|
+
Column('tags', ARRAY(Enum(TagEnum, values_callable=lambda cls: [member.value for member in cls], name='tag_enum', schema='custom_schema'))),
|
|
469
|
+
schema='custom_schema'
|
|
396
470
|
)
|
|
397
471
|
""",
|
|
398
472
|
)
|
|
@@ -529,6 +603,34 @@ def test_mysql_column_types(generator: CodeGenerator) -> None:
|
|
|
529
603
|
)
|
|
530
604
|
|
|
531
605
|
|
|
606
|
+
@pytest.mark.parametrize("engine", ["mysql"], indirect=["engine"])
|
|
607
|
+
@pytest.mark.parametrize("generator", [["keep_dialect_types"]], indirect=True)
|
|
608
|
+
def test_mysql_char_collation_keep_dialect_types(generator: CodeGenerator) -> None:
|
|
609
|
+
Table(
|
|
610
|
+
"simple_items",
|
|
611
|
+
generator.metadata,
|
|
612
|
+
Column("id", mysql.INTEGER, primary_key=True),
|
|
613
|
+
Column("result_code", mysql.CHAR(1, collation="utf8mb3_bin"), nullable=False),
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
validate_code(
|
|
617
|
+
generator.generate(),
|
|
618
|
+
"""\
|
|
619
|
+
from sqlalchemy import Column, MetaData, Table
|
|
620
|
+
from sqlalchemy.dialects.mysql import CHAR, INTEGER
|
|
621
|
+
|
|
622
|
+
metadata = MetaData()
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
t_simple_items = Table(
|
|
626
|
+
'simple_items', metadata,
|
|
627
|
+
Column('id', INTEGER, primary_key=True),
|
|
628
|
+
Column('result_code', CHAR(1, collation='utf8mb3_bin'), nullable=False)
|
|
629
|
+
)
|
|
630
|
+
""",
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
|
|
532
634
|
def test_constraints(generator: CodeGenerator) -> None:
|
|
533
635
|
Table(
|
|
534
636
|
"simple_items",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|