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.
Files changed (37) hide show
  1. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/CHANGES.rst +34 -0
  2. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/PKG-INFO +1 -1
  3. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen/generators.py +173 -82
  4. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen.egg-info/PKG-INFO +1 -1
  5. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/tests/test_generator_declarative.py +193 -7
  6. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/tests/test_generator_sqlmodel.py +202 -2
  7. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/tests/test_generator_tables.py +108 -6
  8. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/FUNDING.yml +0 -0
  9. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
  10. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  11. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/ISSUE_TEMPLATE/features_request.yaml +0 -0
  12. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/dependabot.yml +0 -0
  13. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/pull_request_template.md +0 -0
  14. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/workflows/publish.yml +0 -0
  15. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.github/workflows/test.yml +0 -0
  16. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.gitignore +0 -0
  17. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/.pre-commit-config.yaml +0 -0
  18. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/CONTRIBUTING.rst +0 -0
  19. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/LICENSE +0 -0
  20. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/README.rst +0 -0
  21. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/pyproject.toml +0 -0
  22. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/setup.cfg +0 -0
  23. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen/__init__.py +0 -0
  24. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen/__main__.py +0 -0
  25. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen/cli.py +0 -0
  26. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen/models.py +0 -0
  27. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen/py.typed +0 -0
  28. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen/utils.py +0 -0
  29. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen.egg-info/SOURCES.txt +0 -0
  30. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen.egg-info/dependency_links.txt +0 -0
  31. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen.egg-info/entry_points.txt +0 -0
  32. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen.egg-info/requires.txt +0 -0
  33. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/src/sqlacodegen.egg-info/top_level.txt +0 -0
  34. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/tests/__init__.py +0 -0
  35. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/tests/conftest.py +0 -0
  36. {sqlacodegen-4.0.0rc3 → sqlacodegen-4.0.2}/tests/test_cli.py +0 -0
  37. {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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlacodegen
3
- Version: 4.0.0rc3
3
+ Version: 4.0.2
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>
@@ -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
- return f"Enum({enum_class_name}, values_callable=lambda cls: [member.value for member in cls])"
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
- rendered_enum = f"Enum({enum_class_name}, values_callable=lambda cls: [member.value for member in cls])"
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
- elif use_kwargs:
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(repr(f"{attr.model.name}.{attr.name}"))
1749
+ rendered.append(f"{attr.model.name}.{attr.name}")
1750
+ render_as_string = True
1636
1751
 
1637
- return "[" + ", ".join(rendered) + "]"
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
- rendered_relationship = render_callable(
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
- rendered = super().render_relationship(relationship).partition(" = ")[2]
1862
- args = self.render_relationship_args(rendered)
1863
- kwargs: dict[str, Any] = {}
1864
- annotation = repr(relationship.target.name)
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 relationship.type in (
1867
- RelationshipType.ONE_TO_MANY,
1868
- RelationshipType.MANY_TO_MANY,
1869
- ):
1870
- annotation = f"list[{annotation}]"
1871
- else:
1872
- self.add_literal_import("typing", "Optional")
1873
- annotation = f"Optional[{annotation}]"
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", *args, kwargs=kwargs)
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlacodegen
3
- Version: 4.0.0rc3
3
+ Version: 4.0.2
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>
@@ -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}, back_populates='other_item')
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