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.
Files changed (37) hide show
  1. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/CHANGES.rst +16 -0
  2. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/PKG-INFO +1 -1
  3. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen/generators.py +128 -24
  4. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen.egg-info/PKG-INFO +1 -1
  5. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/tests/test_generator_declarative.py +106 -0
  6. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/tests/test_generator_sqlmodel.py +103 -0
  7. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/tests/test_generator_tables.py +63 -0
  8. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/FUNDING.yml +0 -0
  9. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
  10. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  11. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/ISSUE_TEMPLATE/features_request.yaml +0 -0
  12. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/dependabot.yml +0 -0
  13. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/pull_request_template.md +0 -0
  14. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/workflows/publish.yml +0 -0
  15. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.github/workflows/test.yml +0 -0
  16. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.gitignore +0 -0
  17. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/.pre-commit-config.yaml +0 -0
  18. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/CONTRIBUTING.rst +0 -0
  19. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/LICENSE +0 -0
  20. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/README.rst +0 -0
  21. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/pyproject.toml +0 -0
  22. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/setup.cfg +0 -0
  23. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen/__init__.py +0 -0
  24. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen/__main__.py +0 -0
  25. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen/cli.py +0 -0
  26. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen/models.py +0 -0
  27. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen/py.typed +0 -0
  28. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen/utils.py +0 -0
  29. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen.egg-info/SOURCES.txt +0 -0
  30. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen.egg-info/dependency_links.txt +0 -0
  31. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen.egg-info/entry_points.txt +0 -0
  32. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen.egg-info/requires.txt +0 -0
  33. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/src/sqlacodegen.egg-info/top_level.txt +0 -0
  34. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/tests/__init__.py +0 -0
  35. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/tests/conftest.py +0 -0
  36. {sqlacodegen-4.0.1 → sqlacodegen-4.0.3}/tests/test_cli.py +0 -0
  37. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlacodegen
3
- Version: 4.0.1
3
+ Version: 4.0.3
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>
@@ -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
- args.append(repr(column.server_default))
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
- elif use_kwargs:
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(repr(f"{attr.model.name}.{attr.name}"))
1772
+ rendered.append(f"{attr.model.name}.{attr.name}")
1773
+ render_as_string = True
1679
1774
 
1680
- return "[" + ", ".join(rendered) + "]"
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlacodegen
3
- Version: 4.0.1
3
+ Version: 4.0.3
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",
@@ -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