sqlacodegen 4.0.1__tar.gz → 4.0.3__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.1 → sqlacodegen-4.0.3}/CHANGES.rst +16 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/PKG-INFO +1 -1
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen/generators.py +128 -24
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen.egg-info/PKG-INFO +1 -1
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/tests/test_generator_declarative.py +106 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/tests/test_generator_sqlmodel.py +103 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/tests/test_generator_tables.py +63 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/FUNDING.yml +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/ISSUE_TEMPLATE/features_request.yaml +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/dependabot.yml +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/pull_request_template.md +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/workflows/publish.yml +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/workflows/test.yml +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.gitignore +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.pre-commit-config.yaml +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/CONTRIBUTING.rst +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/LICENSE +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/README.rst +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/pyproject.toml +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/setup.cfg +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen/__init__.py +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen/__main__.py +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen/cli.py +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen/models.py +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen/py.typed +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen/utils.py +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen.egg-info/SOURCES.txt +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen.egg-info/dependency_links.txt +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen.egg-info/entry_points.txt +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen.egg-info/requires.txt +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen.egg-info/top_level.txt +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/tests/__init__.py +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/tests/conftest.py +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/tests/test_cli.py +0 -0
- {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/tests/test_generator_dataclass.py +0 -0
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
Version history
|
|
2
2
|
===============
|
|
3
3
|
|
|
4
|
+
**4.0.3**
|
|
5
|
+
|
|
6
|
+
- Improved rendering of ``Identity`` server defaults by explicitly rendering
|
|
7
|
+
non-default parameters; ``Decimal`` values (as returned by some databases) are
|
|
8
|
+
now cast to ``int`` (PR by @NotCarlosSerrano)
|
|
9
|
+
|
|
10
|
+
**4.0.2**
|
|
11
|
+
|
|
12
|
+
- Fixed rendering of inherited keyword arguments for dialect-specific types that use
|
|
13
|
+
``**kwargs`` in their initializers (such as MySQL ``CHAR`` with ``collation``) while
|
|
14
|
+
preserving existing ``*args`` rendering behavior (PR by @hyoj0942)
|
|
15
|
+
- Fixed missing metadata argument when rendering plain tables with the SQLModel
|
|
16
|
+
- Added support for self-referential tables in the SQLModel generator (PR by @sheinbergon)
|
|
17
|
+
- Fixed empty dialect kwargs (e.g. ``postgresql_include=[]``) being included in
|
|
18
|
+
rendered indexes, tables, and columns (PR by @sheinbergon)
|
|
19
|
+
|
|
4
20
|
**4.0.1**
|
|
5
21
|
|
|
6
22
|
- Fix enum column definitions to explicitly include schema and name if reflected
|
|
@@ -7,6 +7,7 @@ from abc import ABCMeta, abstractmethod
|
|
|
7
7
|
from collections import defaultdict
|
|
8
8
|
from collections.abc import Collection, Iterable, Mapping, Sequence
|
|
9
9
|
from dataclasses import dataclass
|
|
10
|
+
from decimal import Decimal
|
|
10
11
|
from importlib import import_module
|
|
11
12
|
from inspect import Parameter
|
|
12
13
|
from itertools import count
|
|
@@ -427,6 +428,7 @@ class TablesGenerator(CodeGenerator):
|
|
|
427
428
|
kwargs = {
|
|
428
429
|
key: repr(value) if isinstance(value, str) else value
|
|
429
430
|
for key, value in sorted(index.kwargs.items(), key=lambda item: item[0])
|
|
431
|
+
if value not in ([], {})
|
|
430
432
|
}
|
|
431
433
|
if index.unique:
|
|
432
434
|
kwargs["unique"] = True
|
|
@@ -520,7 +522,29 @@ class TablesGenerator(CodeGenerator):
|
|
|
520
522
|
render_callable("Computed", repr(expression), kwargs=computed_kwargs)
|
|
521
523
|
)
|
|
522
524
|
elif isinstance(column.server_default, Identity):
|
|
523
|
-
|
|
525
|
+
identity = column.server_default
|
|
526
|
+
identity_kwargs: dict[str, Any] = {}
|
|
527
|
+
|
|
528
|
+
for name, param in inspect.signature(Identity).parameters.items():
|
|
529
|
+
if name == "self" or param.kind in (
|
|
530
|
+
Parameter.VAR_POSITIONAL,
|
|
531
|
+
Parameter.VAR_KEYWORD,
|
|
532
|
+
):
|
|
533
|
+
continue
|
|
534
|
+
|
|
535
|
+
value = getattr(identity, name, None)
|
|
536
|
+
if value is None:
|
|
537
|
+
continue
|
|
538
|
+
|
|
539
|
+
if isinstance(value, Decimal):
|
|
540
|
+
value = int(value)
|
|
541
|
+
|
|
542
|
+
if param.default is not Parameter.empty and value == param.default:
|
|
543
|
+
continue
|
|
544
|
+
|
|
545
|
+
identity_kwargs[name] = value
|
|
546
|
+
|
|
547
|
+
args.append(render_callable("Identity", kwargs=identity_kwargs))
|
|
524
548
|
elif column.server_default:
|
|
525
549
|
kwargs["server_default"] = repr(column.server_default)
|
|
526
550
|
|
|
@@ -541,6 +565,77 @@ class TablesGenerator(CodeGenerator):
|
|
|
541
565
|
else:
|
|
542
566
|
return render_callable("mapped_column", *args, kwargs=kwargs)
|
|
543
567
|
|
|
568
|
+
def _render_column_type_value(self, value: Any) -> str:
|
|
569
|
+
if isinstance(value, (JSONB, JSON)):
|
|
570
|
+
# Remove astext_type if it's the default
|
|
571
|
+
if isinstance(value.astext_type, Text) and value.astext_type.length is None:
|
|
572
|
+
value.astext_type = None # type: ignore[assignment]
|
|
573
|
+
else:
|
|
574
|
+
self.add_import(Text)
|
|
575
|
+
|
|
576
|
+
if isinstance(value, TextClause):
|
|
577
|
+
self.add_literal_import("sqlalchemy", "text")
|
|
578
|
+
return render_callable("text", repr(value.text))
|
|
579
|
+
|
|
580
|
+
return repr(value)
|
|
581
|
+
|
|
582
|
+
def _collect_inherited_init_kwargs(
|
|
583
|
+
self,
|
|
584
|
+
column_type: Any,
|
|
585
|
+
init_sig: inspect.Signature,
|
|
586
|
+
seen_param_names: set[str],
|
|
587
|
+
missing: object,
|
|
588
|
+
) -> dict[str, str]:
|
|
589
|
+
has_var_keyword = any(
|
|
590
|
+
param.kind is Parameter.VAR_KEYWORD
|
|
591
|
+
for param in init_sig.parameters.values()
|
|
592
|
+
)
|
|
593
|
+
has_var_positional = any(
|
|
594
|
+
param.kind is Parameter.VAR_POSITIONAL
|
|
595
|
+
for param in init_sig.parameters.values()
|
|
596
|
+
)
|
|
597
|
+
if not has_var_keyword or has_var_positional:
|
|
598
|
+
return {}
|
|
599
|
+
|
|
600
|
+
inherited_kwargs: dict[str, str] = {}
|
|
601
|
+
for supercls in column_type.__class__.__mro__[1:]:
|
|
602
|
+
if supercls is object:
|
|
603
|
+
break
|
|
604
|
+
|
|
605
|
+
try:
|
|
606
|
+
super_sig = inspect.signature(supercls.__init__)
|
|
607
|
+
except (TypeError, ValueError):
|
|
608
|
+
continue
|
|
609
|
+
|
|
610
|
+
for super_param in list(super_sig.parameters.values())[1:]:
|
|
611
|
+
if super_param.name.startswith("_"):
|
|
612
|
+
continue
|
|
613
|
+
|
|
614
|
+
if super_param.kind in (
|
|
615
|
+
Parameter.POSITIONAL_ONLY,
|
|
616
|
+
Parameter.VAR_POSITIONAL,
|
|
617
|
+
Parameter.VAR_KEYWORD,
|
|
618
|
+
):
|
|
619
|
+
continue
|
|
620
|
+
|
|
621
|
+
if super_param.name in seen_param_names:
|
|
622
|
+
continue
|
|
623
|
+
|
|
624
|
+
seen_param_names.add(super_param.name)
|
|
625
|
+
value = getattr(column_type, super_param.name, missing)
|
|
626
|
+
if value is missing:
|
|
627
|
+
continue
|
|
628
|
+
|
|
629
|
+
default = super_param.default
|
|
630
|
+
if default is not Parameter.empty and value == default:
|
|
631
|
+
continue
|
|
632
|
+
|
|
633
|
+
inherited_kwargs[super_param.name] = self._render_column_type_value(
|
|
634
|
+
value
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
return inherited_kwargs
|
|
638
|
+
|
|
544
639
|
def render_column_type(self, column: Column[Any]) -> str:
|
|
545
640
|
column_type = column.type
|
|
546
641
|
# Check if this is an enum column with a Python enum class
|
|
@@ -586,6 +681,8 @@ class TablesGenerator(CodeGenerator):
|
|
|
586
681
|
defaults = {param.name: param.default for param in sig.parameters.values()}
|
|
587
682
|
missing = object()
|
|
588
683
|
use_kwargs = False
|
|
684
|
+
seen_param_names: set[str] = set()
|
|
685
|
+
|
|
589
686
|
for param in list(sig.parameters.values())[1:]:
|
|
590
687
|
# Remove annoyances like _warn_on_bytestring
|
|
591
688
|
if param.name.startswith("_"):
|
|
@@ -594,32 +691,25 @@ class TablesGenerator(CodeGenerator):
|
|
|
594
691
|
use_kwargs = True
|
|
595
692
|
continue
|
|
596
693
|
|
|
694
|
+
seen_param_names.add(param.name)
|
|
597
695
|
value = getattr(column_type, param.name, missing)
|
|
598
|
-
|
|
599
|
-
if isinstance(value, (JSONB, JSON)):
|
|
600
|
-
# Remove astext_type if it's the default
|
|
601
|
-
if (
|
|
602
|
-
isinstance(value.astext_type, Text)
|
|
603
|
-
and value.astext_type.length is None
|
|
604
|
-
):
|
|
605
|
-
value.astext_type = None # type: ignore[assignment]
|
|
606
|
-
else:
|
|
607
|
-
self.add_import(Text)
|
|
608
|
-
|
|
609
696
|
default = defaults.get(param.name, missing)
|
|
610
|
-
if isinstance(value, TextClause):
|
|
611
|
-
self.add_literal_import("sqlalchemy", "text")
|
|
612
|
-
rendered_value = render_callable("text", repr(value.text))
|
|
613
|
-
else:
|
|
614
|
-
rendered_value = repr(value)
|
|
615
|
-
|
|
616
697
|
if value is missing or value == default:
|
|
617
698
|
use_kwargs = True
|
|
618
|
-
|
|
699
|
+
continue
|
|
700
|
+
|
|
701
|
+
rendered_value = self._render_column_type_value(value)
|
|
702
|
+
if use_kwargs:
|
|
619
703
|
kwargs[param.name] = rendered_value
|
|
620
704
|
else:
|
|
621
705
|
args.append(rendered_value)
|
|
622
706
|
|
|
707
|
+
kwargs.update(
|
|
708
|
+
self._collect_inherited_init_kwargs(
|
|
709
|
+
column_type, sig, seen_param_names, missing
|
|
710
|
+
)
|
|
711
|
+
)
|
|
712
|
+
|
|
623
713
|
vararg = next(
|
|
624
714
|
(
|
|
625
715
|
param.name
|
|
@@ -712,6 +802,9 @@ class TablesGenerator(CodeGenerator):
|
|
|
712
802
|
except Exception:
|
|
713
803
|
continue
|
|
714
804
|
|
|
805
|
+
if isinstance(value, list | dict) and not value:
|
|
806
|
+
continue
|
|
807
|
+
|
|
715
808
|
# Render values:
|
|
716
809
|
# - callable context (values_for_dict=False): produce a string expression.
|
|
717
810
|
# primitives use repr(value); custom objects stringify then repr().
|
|
@@ -1671,13 +1764,16 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
1671
1764
|
) -> Mapping[str, Any]:
|
|
1672
1765
|
def render_column_attrs(column_attrs: list[ColumnAttribute]) -> str:
|
|
1673
1766
|
rendered = []
|
|
1767
|
+
render_as_string = False
|
|
1674
1768
|
for attr in column_attrs:
|
|
1675
|
-
if attr.model is relationship.source:
|
|
1769
|
+
if not self.explicit_foreign_keys and attr.model is relationship.source:
|
|
1676
1770
|
rendered.append(attr.name)
|
|
1677
1771
|
else:
|
|
1678
|
-
rendered.append(
|
|
1772
|
+
rendered.append(f"{attr.model.name}.{attr.name}")
|
|
1773
|
+
render_as_string = True
|
|
1679
1774
|
|
|
1680
|
-
|
|
1775
|
+
joined = "[" + ", ".join(rendered) + "]"
|
|
1776
|
+
return repr(joined) if render_as_string else joined
|
|
1681
1777
|
|
|
1682
1778
|
def render_foreign_keys(column_attrs: list[ColumnAttribute]) -> str:
|
|
1683
1779
|
rendered = []
|
|
@@ -1806,19 +1902,27 @@ class SQLModelGenerator(DeclarativeGenerator):
|
|
|
1806
1902
|
self.add_import(Column)
|
|
1807
1903
|
return render_callable("Column", *args, kwargs=kwargs)
|
|
1808
1904
|
|
|
1905
|
+
def render_table(self, table: Table) -> str:
|
|
1906
|
+
# Hack to fix #465 without breaking backwards compatibility
|
|
1907
|
+
self.base.metadata_ref = "SQLModel.metadata"
|
|
1908
|
+
|
|
1909
|
+
return super().render_table(table)
|
|
1910
|
+
|
|
1809
1911
|
def generate_base(self) -> None:
|
|
1810
1912
|
self.base = Base(
|
|
1811
1913
|
literal_imports=[],
|
|
1812
1914
|
declarations=[],
|
|
1813
|
-
metadata_ref="",
|
|
1915
|
+
metadata_ref="SQLModel.metadata",
|
|
1814
1916
|
)
|
|
1815
1917
|
|
|
1816
1918
|
def collect_imports(self, models: Iterable[Model]) -> None:
|
|
1817
1919
|
super(DeclarativeGenerator, self).collect_imports(models)
|
|
1818
1920
|
if any(isinstance(model, ModelClass) for model in models):
|
|
1921
|
+
self.add_literal_import("sqlmodel", "Field")
|
|
1922
|
+
|
|
1923
|
+
if models:
|
|
1819
1924
|
self.remove_literal_import("sqlalchemy", "MetaData")
|
|
1820
1925
|
self.add_literal_import("sqlmodel", "SQLModel")
|
|
1821
|
-
self.add_literal_import("sqlmodel", "Field")
|
|
1822
1926
|
|
|
1823
1927
|
def collect_imports_for_model(self, model: Model) -> None:
|
|
1824
1928
|
super(DeclarativeGenerator, self).collect_imports_for_model(model)
|
|
@@ -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",
|
|
@@ -3127,3 +3161,75 @@ def test_array_enum_named_with_schema(generator: CodeGenerator) -> None:
|
|
|
3127
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)
|
|
3128
3162
|
""",
|
|
3129
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)
|
|
3234
|
+
""",
|
|
3235
|
+
)
|
|
@@ -366,3 +366,106 @@ def test_array_enum_named_with_schema(generator: CodeGenerator) -> None:
|
|
|
366
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
367
|
""",
|
|
368
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
|
+
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from decimal import Decimal
|
|
3
4
|
from textwrap import dedent
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
@@ -603,6 +604,34 @@ def test_mysql_column_types(generator: CodeGenerator) -> None:
|
|
|
603
604
|
)
|
|
604
605
|
|
|
605
606
|
|
|
607
|
+
@pytest.mark.parametrize("engine", ["mysql"], indirect=["engine"])
|
|
608
|
+
@pytest.mark.parametrize("generator", [["keep_dialect_types"]], indirect=True)
|
|
609
|
+
def test_mysql_char_collation_keep_dialect_types(generator: CodeGenerator) -> None:
|
|
610
|
+
Table(
|
|
611
|
+
"simple_items",
|
|
612
|
+
generator.metadata,
|
|
613
|
+
Column("id", mysql.INTEGER, primary_key=True),
|
|
614
|
+
Column("result_code", mysql.CHAR(1, collation="utf8mb3_bin"), nullable=False),
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
validate_code(
|
|
618
|
+
generator.generate(),
|
|
619
|
+
"""\
|
|
620
|
+
from sqlalchemy import Column, MetaData, Table
|
|
621
|
+
from sqlalchemy.dialects.mysql import CHAR, INTEGER
|
|
622
|
+
|
|
623
|
+
metadata = MetaData()
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
t_simple_items = Table(
|
|
627
|
+
'simple_items', metadata,
|
|
628
|
+
Column('id', INTEGER, primary_key=True),
|
|
629
|
+
Column('result_code', CHAR(1, collation='utf8mb3_bin'), nullable=False)
|
|
630
|
+
)
|
|
631
|
+
""",
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
|
|
606
635
|
def test_constraints(generator: CodeGenerator) -> None:
|
|
607
636
|
Table(
|
|
608
637
|
"simple_items",
|
|
@@ -1189,6 +1218,40 @@ def test_identity_column(generator: CodeGenerator) -> None:
|
|
|
1189
1218
|
)
|
|
1190
1219
|
|
|
1191
1220
|
|
|
1221
|
+
def test_identity_column_decimal_values(generator: CodeGenerator) -> None:
|
|
1222
|
+
# MSSQL reflects Identity column parameters (start, increment) as Decimal
|
|
1223
|
+
# values instead of integers. This test ensures those are serialized correctly.
|
|
1224
|
+
identity = Identity(start=1, increment=2)
|
|
1225
|
+
# Simulate database reflection returning Decimal values (as MSSQL does)
|
|
1226
|
+
identity.start = Decimal("1") # type: ignore[assignment]
|
|
1227
|
+
identity.increment = Decimal("2") # type: ignore[assignment]
|
|
1228
|
+
Table(
|
|
1229
|
+
"simple_items",
|
|
1230
|
+
generator.metadata,
|
|
1231
|
+
Column(
|
|
1232
|
+
"id",
|
|
1233
|
+
INTEGER,
|
|
1234
|
+
primary_key=True,
|
|
1235
|
+
server_default=identity,
|
|
1236
|
+
),
|
|
1237
|
+
)
|
|
1238
|
+
|
|
1239
|
+
validate_code(
|
|
1240
|
+
generator.generate(),
|
|
1241
|
+
"""\
|
|
1242
|
+
from sqlalchemy import Column, Identity, Integer, MetaData, Table
|
|
1243
|
+
|
|
1244
|
+
metadata = MetaData()
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
t_simple_items = Table(
|
|
1248
|
+
'simple_items', metadata,
|
|
1249
|
+
Column('id', Integer, Identity(start=1, increment=2), primary_key=True)
|
|
1250
|
+
)
|
|
1251
|
+
""",
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
|
|
1192
1255
|
def test_multiline_column_comment(generator: CodeGenerator) -> None:
|
|
1193
1256
|
Table(
|
|
1194
1257
|
"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
|