sqlacodegen 3.1.1__tar.gz → 3.2.0__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-3.1.1 → sqlacodegen-3.2.0}/.github/workflows/test.yml +1 -1
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/.pre-commit-config.yaml +5 -2
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/CHANGES.rst +11 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/PKG-INFO +8 -6
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/README.rst +4 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/pyproject.toml +4 -6
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/src/sqlacodegen/cli.py +1 -5
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/src/sqlacodegen/generators.py +93 -7
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/src/sqlacodegen/models.py +2 -2
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/src/sqlacodegen.egg-info/PKG-INFO +8 -6
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/src/sqlacodegen.egg-info/requires.txt +1 -5
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/tests/test_generator_declarative.py +230 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/tests/test_generator_tables.py +84 -1
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/.github/ISSUE_TEMPLATE/features_request.yaml +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/.github/pull_request_template.md +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/.github/workflows/publish.yml +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/.gitignore +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/CONTRIBUTING.rst +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/LICENSE +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/setup.cfg +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/src/sqlacodegen/__init__.py +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/src/sqlacodegen/__main__.py +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/src/sqlacodegen/py.typed +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/src/sqlacodegen/utils.py +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/src/sqlacodegen.egg-info/SOURCES.txt +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/src/sqlacodegen.egg-info/dependency_links.txt +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/src/sqlacodegen.egg-info/entry_points.txt +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/src/sqlacodegen.egg-info/top_level.txt +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/tests/__init__.py +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/tests/conftest.py +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/tests/test_cli.py +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/tests/test_generator_dataclass.py +0 -0
- {sqlacodegen-3.1.1 → sqlacodegen-3.2.0}/tests/test_generator_sqlmodel.py +0 -0
|
@@ -16,14 +16,14 @@ repos:
|
|
|
16
16
|
- id: trailing-whitespace
|
|
17
17
|
|
|
18
18
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
19
|
-
rev: v0.
|
|
19
|
+
rev: v0.13.3
|
|
20
20
|
hooks:
|
|
21
21
|
- id: ruff
|
|
22
22
|
args: [--fix, --show-fixes]
|
|
23
23
|
- id: ruff-format
|
|
24
24
|
|
|
25
25
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
26
|
-
rev: v1.
|
|
26
|
+
rev: v1.18.2
|
|
27
27
|
hooks:
|
|
28
28
|
- id: mypy
|
|
29
29
|
additional_dependencies:
|
|
@@ -36,3 +36,6 @@ repos:
|
|
|
36
36
|
- id: rst-backticks
|
|
37
37
|
- id: rst-directive-colons
|
|
38
38
|
- id: rst-inline-touching-normal
|
|
39
|
+
|
|
40
|
+
ci:
|
|
41
|
+
autoupdate_schedule: quarterly
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
Version history
|
|
2
2
|
===============
|
|
3
3
|
|
|
4
|
+
**3.2.0**
|
|
5
|
+
|
|
6
|
+
- Dropped support for Python 3.9
|
|
7
|
+
- Fix Postgres ``DOMAIN`` adaptation regression introduced in SQLAlchemy 2.0.42 (PR by @sheinbergon)
|
|
8
|
+
- Support disabling special naming logic for single column many-to-one and one-to-one relationships
|
|
9
|
+
(PR by @Henkhogan, revised by @sheinbergon)
|
|
10
|
+
- Add ``include_dialect_options`` option to render ``Table`` and ``Column``
|
|
11
|
+
dialect-specific kwargs and ``info`` in generated code. (PR by @jaogoy)
|
|
12
|
+
- Add ``keep_dialect_types`` option to preserve dialect-specific column types instead of
|
|
13
|
+
adapting to generic SQLAlchemy types. (PR by @jaogoy)
|
|
14
|
+
|
|
4
15
|
**3.1.1**
|
|
5
16
|
|
|
6
17
|
- Fallback ``NotImplemented`` errors encountered when accessing ``python_type`` for
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlacodegen
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
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>
|
|
@@ -15,18 +15,16 @@ Classifier: Topic :: Database
|
|
|
15
15
|
Classifier: Topic :: Software Development :: Code Generators
|
|
16
16
|
Classifier: Programming Language :: Python
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
-
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
24
|
Description-Content-Type: text/x-rst
|
|
25
25
|
License-File: LICENSE
|
|
26
|
-
Requires-Dist: SQLAlchemy
|
|
26
|
+
Requires-Dist: SQLAlchemy>=2.0.29
|
|
27
27
|
Requires-Dist: inflect>=4.0.0
|
|
28
|
-
Requires-Dist: importlib_metadata; python_version < "3.10"
|
|
29
|
-
Requires-Dist: stdlib-list; python_version < "3.10"
|
|
30
28
|
Provides-Extra: test
|
|
31
29
|
Requires-Dist: sqlacodegen[geoalchemy2,pgvector,sqlmodel]; extra == "test"
|
|
32
30
|
Requires-Dist: pytest>=7.4; extra == "test"
|
|
@@ -148,6 +146,10 @@ values must be delimited by commas, e.g. ``--options noconstraints,nobidi``):
|
|
|
148
146
|
* ``noconstraints``: ignore constraints (foreign key, unique etc.)
|
|
149
147
|
* ``nocomments``: ignore table/column comments
|
|
150
148
|
* ``noindexes``: ignore indexes
|
|
149
|
+
* ``noidsuffix``: prevent the special naming logic for single column many-to-one
|
|
150
|
+
and one-to-one relationships (see `Relationship naming logic`_ for details)
|
|
151
|
+
* ``include_dialect_options``: render a table' dialect options, such as ``starrocks_partition`` for StarRocks' specific options.
|
|
152
|
+
* ``keep_dialect_types``: preserve dialect-specific column types instead of adapting to generic SQLAlchemy types.
|
|
151
153
|
|
|
152
154
|
* ``declarative``
|
|
153
155
|
|
|
@@ -103,6 +103,10 @@ values must be delimited by commas, e.g. ``--options noconstraints,nobidi``):
|
|
|
103
103
|
* ``noconstraints``: ignore constraints (foreign key, unique etc.)
|
|
104
104
|
* ``nocomments``: ignore table/column comments
|
|
105
105
|
* ``noindexes``: ignore indexes
|
|
106
|
+
* ``noidsuffix``: prevent the special naming logic for single column many-to-one
|
|
107
|
+
and one-to-one relationships (see `Relationship naming logic`_ for details)
|
|
108
|
+
* ``include_dialect_options``: render a table' dialect options, such as ``starrocks_partition`` for StarRocks' specific options.
|
|
109
|
+
* ``keep_dialect_types``: preserve dialect-specific column types instead of adapting to generic SQLAlchemy types.
|
|
106
110
|
|
|
107
111
|
* ``declarative``
|
|
108
112
|
|
|
@@ -21,18 +21,16 @@ classifiers = [
|
|
|
21
21
|
"Topic :: Software Development :: Code Generators",
|
|
22
22
|
"Programming Language :: Python",
|
|
23
23
|
"Programming Language :: Python :: 3",
|
|
24
|
-
"Programming Language :: Python :: 3.9",
|
|
25
24
|
"Programming Language :: Python :: 3.10",
|
|
26
25
|
"Programming Language :: Python :: 3.11",
|
|
27
26
|
"Programming Language :: Python :: 3.12",
|
|
28
27
|
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Programming Language :: Python :: 3.14",
|
|
29
29
|
]
|
|
30
|
-
requires-python = ">=3.
|
|
30
|
+
requires-python = ">=3.10"
|
|
31
31
|
dependencies = [
|
|
32
|
-
"SQLAlchemy >= 2.0.29
|
|
32
|
+
"SQLAlchemy >= 2.0.29",
|
|
33
33
|
"inflect >= 4.0.0",
|
|
34
|
-
"importlib_metadata; python_version < '3.10'",
|
|
35
|
-
"stdlib-list; python_version < '3.10'"
|
|
36
34
|
]
|
|
37
35
|
dynamic = ["version"]
|
|
38
36
|
|
|
@@ -95,7 +93,7 @@ relative_files = true
|
|
|
95
93
|
show_missing = true
|
|
96
94
|
|
|
97
95
|
[tool.tox]
|
|
98
|
-
env_list = ["
|
|
96
|
+
env_list = ["py310", "py311", "py312", "py313", "py314"]
|
|
99
97
|
skip_missing_interpreters = true
|
|
100
98
|
|
|
101
99
|
[tool.tox.env_run_base]
|
|
@@ -4,6 +4,7 @@ import argparse
|
|
|
4
4
|
import ast
|
|
5
5
|
import sys
|
|
6
6
|
from contextlib import ExitStack
|
|
7
|
+
from importlib.metadata import entry_points, version
|
|
7
8
|
from typing import Any, TextIO
|
|
8
9
|
|
|
9
10
|
from sqlalchemy.engine import create_engine
|
|
@@ -24,11 +25,6 @@ try:
|
|
|
24
25
|
except ImportError:
|
|
25
26
|
pgvector = None
|
|
26
27
|
|
|
27
|
-
if sys.version_info < (3, 10):
|
|
28
|
-
from importlib_metadata import entry_points, version
|
|
29
|
-
else:
|
|
30
|
-
from importlib.metadata import entry_points, version
|
|
31
|
-
|
|
32
28
|
|
|
33
29
|
def _parse_engine_arg(arg_str: str) -> tuple[str, Any]:
|
|
34
30
|
if "=" not in arg_str:
|
|
@@ -119,7 +119,13 @@ class CodeGenerator(metaclass=ABCMeta):
|
|
|
119
119
|
|
|
120
120
|
@dataclass(eq=False)
|
|
121
121
|
class TablesGenerator(CodeGenerator):
|
|
122
|
-
valid_options: ClassVar[set[str]] = {
|
|
122
|
+
valid_options: ClassVar[set[str]] = {
|
|
123
|
+
"noindexes",
|
|
124
|
+
"noconstraints",
|
|
125
|
+
"nocomments",
|
|
126
|
+
"include_dialect_options",
|
|
127
|
+
"keep_dialect_types",
|
|
128
|
+
}
|
|
123
129
|
stdlib_module_names: ClassVar[set[str]] = get_stdlib_module_names()
|
|
124
130
|
|
|
125
131
|
def __init__(
|
|
@@ -135,6 +141,13 @@ class TablesGenerator(CodeGenerator):
|
|
|
135
141
|
self.imports: dict[str, set[str]] = defaultdict(set)
|
|
136
142
|
self.module_imports: set[str] = set()
|
|
137
143
|
|
|
144
|
+
# Render SchemaItem.info and dialect kwargs (Table/Column) into output
|
|
145
|
+
self.include_dialect_options_and_info: bool = (
|
|
146
|
+
"include_dialect_options" in self.options
|
|
147
|
+
)
|
|
148
|
+
# Keep dialect-specific types instead of adapting to generic SQLAlchemy types
|
|
149
|
+
self.keep_dialect_types: bool = "keep_dialect_types" in self.options
|
|
150
|
+
|
|
138
151
|
@property
|
|
139
152
|
def views_supported(self) -> bool:
|
|
140
153
|
return True
|
|
@@ -393,6 +406,10 @@ class TablesGenerator(CodeGenerator):
|
|
|
393
406
|
if table_comment:
|
|
394
407
|
kwargs["comment"] = repr(table.comment)
|
|
395
408
|
|
|
409
|
+
# add info + dialect kwargs for callable context (opt-in)
|
|
410
|
+
if self.include_dialect_options_and_info:
|
|
411
|
+
self._add_dialect_kwargs_and_info(table, kwargs, values_for_dict=False)
|
|
412
|
+
|
|
396
413
|
return render_callable("Table", *args, kwargs=kwargs, indentation=" ")
|
|
397
414
|
|
|
398
415
|
def render_index(self, index: Index) -> str:
|
|
@@ -498,6 +515,10 @@ class TablesGenerator(CodeGenerator):
|
|
|
498
515
|
if comment:
|
|
499
516
|
kwargs["comment"] = repr(comment)
|
|
500
517
|
|
|
518
|
+
# add column info + dialect kwargs for callable context (opt-in)
|
|
519
|
+
if self.include_dialect_options_and_info:
|
|
520
|
+
self._add_dialect_kwargs_and_info(column, kwargs, values_for_dict=False)
|
|
521
|
+
|
|
501
522
|
return self.render_column_callable(is_table, *args, **kwargs)
|
|
502
523
|
|
|
503
524
|
def render_column_callable(self, is_table: bool, *args: Any, **kwargs: Any) -> str:
|
|
@@ -615,6 +636,51 @@ class TablesGenerator(CodeGenerator):
|
|
|
615
636
|
|
|
616
637
|
return render_callable(constraint.__class__.__name__, *args, kwargs=kwargs)
|
|
617
638
|
|
|
639
|
+
def _add_dialect_kwargs_and_info(
|
|
640
|
+
self, obj: Any, target_kwargs: dict[str, object], *, values_for_dict: bool
|
|
641
|
+
) -> None:
|
|
642
|
+
"""
|
|
643
|
+
Merge SchemaItem-like object's .info and .dialect_kwargs into target_kwargs.
|
|
644
|
+
- values_for_dict=True: keep raw values so pretty-printer emits repr() (for __table_args__ dict)
|
|
645
|
+
- values_for_dict=False: set values to repr() strings (for callable kwargs)
|
|
646
|
+
"""
|
|
647
|
+
info_dict = getattr(obj, "info", None)
|
|
648
|
+
if info_dict:
|
|
649
|
+
target_kwargs["info"] = info_dict if values_for_dict else repr(info_dict)
|
|
650
|
+
|
|
651
|
+
dialect_keys: list[str]
|
|
652
|
+
try:
|
|
653
|
+
dialect_keys = sorted(getattr(obj, "dialect_kwargs"))
|
|
654
|
+
except Exception:
|
|
655
|
+
return
|
|
656
|
+
|
|
657
|
+
dialect_kwargs = getattr(obj, "dialect_kwargs", {})
|
|
658
|
+
for key in dialect_keys:
|
|
659
|
+
try:
|
|
660
|
+
value = dialect_kwargs[key]
|
|
661
|
+
except Exception:
|
|
662
|
+
continue
|
|
663
|
+
|
|
664
|
+
# Render values:
|
|
665
|
+
# - callable context (values_for_dict=False): produce a string expression.
|
|
666
|
+
# primitives use repr(value); custom objects stringify then repr().
|
|
667
|
+
# - dict context (values_for_dict=True): pass raw primitives / str;
|
|
668
|
+
# custom objects become str(value) so pformat quotes them.
|
|
669
|
+
if values_for_dict:
|
|
670
|
+
if isinstance(value, type(None) | bool | int | float):
|
|
671
|
+
target_kwargs[key] = value
|
|
672
|
+
elif isinstance(value, str | dict | list):
|
|
673
|
+
target_kwargs[key] = value
|
|
674
|
+
else:
|
|
675
|
+
target_kwargs[key] = str(value)
|
|
676
|
+
else:
|
|
677
|
+
if isinstance(
|
|
678
|
+
value, type(None) | bool | int | float | str | dict | list
|
|
679
|
+
):
|
|
680
|
+
target_kwargs[key] = repr(value)
|
|
681
|
+
else:
|
|
682
|
+
target_kwargs[key] = repr(str(value))
|
|
683
|
+
|
|
618
684
|
def should_ignore_table(self, table: Table) -> bool:
|
|
619
685
|
# Support for Alembic and sqlalchemy-migrate -- never expose the schema version
|
|
620
686
|
# tables
|
|
@@ -680,10 +746,11 @@ class TablesGenerator(CodeGenerator):
|
|
|
680
746
|
continue
|
|
681
747
|
|
|
682
748
|
for column in table.c:
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
749
|
+
if not self.keep_dialect_types:
|
|
750
|
+
try:
|
|
751
|
+
column.type = self.get_adapted_type(column.type)
|
|
752
|
+
except CompileError:
|
|
753
|
+
continue
|
|
687
754
|
|
|
688
755
|
# PostgreSQL specific fix: detect sequences from server_default
|
|
689
756
|
if column.server_default and self.bind.dialect.name == "postgresql":
|
|
@@ -718,6 +785,20 @@ class TablesGenerator(CodeGenerator):
|
|
|
718
785
|
if coltype.schema:
|
|
719
786
|
kw["schema"] = coltype.schema
|
|
720
787
|
|
|
788
|
+
# Hack to fix Postgres DOMAIN type adaptation, broken as of SQLAlchemy 2.0.42
|
|
789
|
+
# For additional information - https://github.com/agronholm/sqlacodegen/issues/416#issuecomment-3417480599
|
|
790
|
+
if supercls is DOMAIN:
|
|
791
|
+
if coltype.default:
|
|
792
|
+
kw["default"] = coltype.default
|
|
793
|
+
if coltype.constraint_name is not None:
|
|
794
|
+
kw["constraint_name"] = coltype.constraint_name
|
|
795
|
+
if coltype.not_null:
|
|
796
|
+
kw["not_null"] = coltype.not_null
|
|
797
|
+
if coltype.check is not None:
|
|
798
|
+
kw["check"] = coltype.check
|
|
799
|
+
if coltype.create_type:
|
|
800
|
+
kw["create_type"] = coltype.create_type
|
|
801
|
+
|
|
721
802
|
try:
|
|
722
803
|
new_coltype = coltype.adapt(supercls)
|
|
723
804
|
except TypeError:
|
|
@@ -752,6 +833,7 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
752
833
|
"use_inflect",
|
|
753
834
|
"nojoined",
|
|
754
835
|
"nobidi",
|
|
836
|
+
"noidsuffix",
|
|
755
837
|
}
|
|
756
838
|
|
|
757
839
|
def __init__(
|
|
@@ -1088,7 +1170,7 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
1088
1170
|
|
|
1089
1171
|
# If there's a constraint with a single column that ends with "_id", use the
|
|
1090
1172
|
# preceding part as the relationship name
|
|
1091
|
-
if relationship.constraint:
|
|
1173
|
+
if relationship.constraint and "noidsuffix" not in self.options:
|
|
1092
1174
|
is_source = relationship.source.table is relationship.constraint.table
|
|
1093
1175
|
if is_source or relationship.type not in (
|
|
1094
1176
|
RelationshipType.ONE_TO_ONE,
|
|
@@ -1178,7 +1260,7 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
1178
1260
|
|
|
1179
1261
|
def render_table_args(self, table: Table) -> str:
|
|
1180
1262
|
args: list[str] = []
|
|
1181
|
-
kwargs: dict[str,
|
|
1263
|
+
kwargs: dict[str, object] = {}
|
|
1182
1264
|
|
|
1183
1265
|
# Render constraints
|
|
1184
1266
|
for constraint in sorted(table.constraints, key=get_constraint_sort_key):
|
|
@@ -1204,6 +1286,10 @@ class DeclarativeGenerator(TablesGenerator):
|
|
|
1204
1286
|
if table.comment:
|
|
1205
1287
|
kwargs["comment"] = table.comment
|
|
1206
1288
|
|
|
1289
|
+
# add info + dialect kwargs for dict context (__table_args__) (opt-in)
|
|
1290
|
+
if self.include_dialect_options_and_info:
|
|
1291
|
+
self._add_dialect_kwargs_and_info(table, kwargs, values_for_dict=True)
|
|
1292
|
+
|
|
1207
1293
|
if kwargs:
|
|
1208
1294
|
formatted_kwargs = pformat(kwargs)
|
|
1209
1295
|
if not args:
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from enum import Enum, auto
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
from sqlalchemy.sql.schema import Column, ForeignKeyConstraint, Table
|
|
8
8
|
|
|
@@ -52,7 +52,7 @@ class ColumnAttribute:
|
|
|
52
52
|
return self.name
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
JoinType = tuple[Model,
|
|
55
|
+
JoinType = tuple[Model, ColumnAttribute | str, Model, ColumnAttribute | str]
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
@dataclass
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlacodegen
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
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>
|
|
@@ -15,18 +15,16 @@ Classifier: Topic :: Database
|
|
|
15
15
|
Classifier: Topic :: Software Development :: Code Generators
|
|
16
16
|
Classifier: Programming Language :: Python
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
-
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
24
|
Description-Content-Type: text/x-rst
|
|
25
25
|
License-File: LICENSE
|
|
26
|
-
Requires-Dist: SQLAlchemy
|
|
26
|
+
Requires-Dist: SQLAlchemy>=2.0.29
|
|
27
27
|
Requires-Dist: inflect>=4.0.0
|
|
28
|
-
Requires-Dist: importlib_metadata; python_version < "3.10"
|
|
29
|
-
Requires-Dist: stdlib-list; python_version < "3.10"
|
|
30
28
|
Provides-Extra: test
|
|
31
29
|
Requires-Dist: sqlacodegen[geoalchemy2,pgvector,sqlmodel]; extra == "test"
|
|
32
30
|
Requires-Dist: pytest>=7.4; extra == "test"
|
|
@@ -148,6 +146,10 @@ values must be delimited by commas, e.g. ``--options noconstraints,nobidi``):
|
|
|
148
146
|
* ``noconstraints``: ignore constraints (foreign key, unique etc.)
|
|
149
147
|
* ``nocomments``: ignore table/column comments
|
|
150
148
|
* ``noindexes``: ignore indexes
|
|
149
|
+
* ``noidsuffix``: prevent the special naming logic for single column many-to-one
|
|
150
|
+
and one-to-one relationships (see `Relationship naming logic`_ for details)
|
|
151
|
+
* ``include_dialect_options``: render a table' dialect options, such as ``starrocks_partition`` for StarRocks' specific options.
|
|
152
|
+
* ``keep_dialect_types``: preserve dialect-specific column types instead of adapting to generic SQLAlchemy types.
|
|
151
153
|
|
|
152
154
|
* ``declarative``
|
|
153
155
|
|
|
@@ -111,6 +111,180 @@ class SimpleItems(Base):
|
|
|
111
111
|
)
|
|
112
112
|
|
|
113
113
|
|
|
114
|
+
@pytest.mark.parametrize("generator", [["include_dialect_options"]], indirect=True)
|
|
115
|
+
def test_include_dialect_options_and_info_table_and_column(
|
|
116
|
+
generator: CodeGenerator,
|
|
117
|
+
) -> None:
|
|
118
|
+
from .test_generator_tables import _PartitionInfo
|
|
119
|
+
|
|
120
|
+
Table(
|
|
121
|
+
"t_opts",
|
|
122
|
+
generator.metadata,
|
|
123
|
+
Column("id", INTEGER, primary_key=True, starrocks_is_agg_key=True),
|
|
124
|
+
Column("name", VARCHAR, starrocks_agg_type="REPLACE"),
|
|
125
|
+
starrocks_aggregate_key="id",
|
|
126
|
+
starrocks_partition_by=_PartitionInfo("RANGE(id)"),
|
|
127
|
+
starrocks_security="DEFINER",
|
|
128
|
+
starrocks_PROPERTIES={"replication_num": "3", "storage_medium": "SSD"},
|
|
129
|
+
info={
|
|
130
|
+
"table_kind": "MATERIALIZED VIEW",
|
|
131
|
+
"definition": "SELECT id, name FROM t_opts_base_table",
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
validate_code(
|
|
136
|
+
generator.generate(),
|
|
137
|
+
"""\
|
|
138
|
+
from typing import Optional
|
|
139
|
+
|
|
140
|
+
from sqlalchemy import Integer, String
|
|
141
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
142
|
+
|
|
143
|
+
class Base(DeclarativeBase):
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TOpts(Base):
|
|
148
|
+
__tablename__ = 't_opts'
|
|
149
|
+
__table_args__ = {'info': {'definition': 'SELECT id, name FROM t_opts_base_table',
|
|
150
|
+
'table_kind': 'MATERIALIZED VIEW'},
|
|
151
|
+
'starrocks_PROPERTIES': {'replication_num': '3', 'storage_medium': 'SSD'},
|
|
152
|
+
'starrocks_aggregate_key': 'id',
|
|
153
|
+
'starrocks_partition_by': 'RANGE(id)',
|
|
154
|
+
'starrocks_security': 'DEFINER'}
|
|
155
|
+
|
|
156
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True, starrocks_is_agg_key=True)
|
|
157
|
+
name: Mapped[Optional[str]] = mapped_column(String, starrocks_agg_type='REPLACE')
|
|
158
|
+
""",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.parametrize("generator", [["include_dialect_options"]], indirect=True)
|
|
163
|
+
def test_include_dialect_options_and_info_with_hyphen(generator: CodeGenerator) -> None:
|
|
164
|
+
Table(
|
|
165
|
+
"t_opts2",
|
|
166
|
+
generator.metadata,
|
|
167
|
+
Column("id", INTEGER, primary_key=True),
|
|
168
|
+
mysql_engine="InnoDB",
|
|
169
|
+
info={"table_kind": "View"},
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
validate_code(
|
|
173
|
+
generator.generate(),
|
|
174
|
+
"""\
|
|
175
|
+
from sqlalchemy import Integer
|
|
176
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
177
|
+
|
|
178
|
+
class Base(DeclarativeBase):
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class TOpts2(Base):
|
|
183
|
+
__tablename__ = 't_opts2'
|
|
184
|
+
__table_args__ = {'info': {'table_kind': 'View'}, 'mysql_engine': 'InnoDB'}
|
|
185
|
+
|
|
186
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
187
|
+
""",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_include_dialect_options_not_enabled_skips(generator: CodeGenerator) -> None:
|
|
192
|
+
from .test_generator_tables import _PartitionInfo
|
|
193
|
+
|
|
194
|
+
Table(
|
|
195
|
+
"t_plain",
|
|
196
|
+
generator.metadata,
|
|
197
|
+
Column(
|
|
198
|
+
"id",
|
|
199
|
+
INTEGER,
|
|
200
|
+
primary_key=True,
|
|
201
|
+
info={"abc": True},
|
|
202
|
+
starrocks_is_agg_key=True,
|
|
203
|
+
),
|
|
204
|
+
starrocks_engine="OLAP",
|
|
205
|
+
starrocks_partition_by=_PartitionInfo("RANGE(id)"),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
validate_code(
|
|
209
|
+
generator.generate(),
|
|
210
|
+
"""\
|
|
211
|
+
from sqlalchemy import Integer
|
|
212
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
213
|
+
|
|
214
|
+
class Base(DeclarativeBase):
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class TPlain(Base):
|
|
219
|
+
__tablename__ = 't_plain'
|
|
220
|
+
|
|
221
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
222
|
+
""",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_keep_dialect_types_adapts_mysql_integer_default(
|
|
227
|
+
generator: CodeGenerator,
|
|
228
|
+
) -> None:
|
|
229
|
+
from sqlalchemy.dialects.mysql import INTEGER as MYSQL_INTEGER
|
|
230
|
+
|
|
231
|
+
Table(
|
|
232
|
+
"num",
|
|
233
|
+
generator.metadata,
|
|
234
|
+
Column("id", INTEGER, primary_key=True),
|
|
235
|
+
Column("val", MYSQL_INTEGER(), nullable=False),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
validate_code(
|
|
239
|
+
generator.generate(),
|
|
240
|
+
"""\
|
|
241
|
+
from sqlalchemy import Integer
|
|
242
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
243
|
+
|
|
244
|
+
class Base(DeclarativeBase):
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class Num(Base):
|
|
249
|
+
__tablename__ = 'num'
|
|
250
|
+
|
|
251
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
252
|
+
val: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
253
|
+
""",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@pytest.mark.parametrize("generator", [["keep_dialect_types"]], indirect=True)
|
|
258
|
+
def test_keep_dialect_types_keeps_mysql_integer(generator: CodeGenerator) -> None:
|
|
259
|
+
from sqlalchemy.dialects.mysql import INTEGER as MYSQL_INTEGER
|
|
260
|
+
|
|
261
|
+
Table(
|
|
262
|
+
"num2",
|
|
263
|
+
generator.metadata,
|
|
264
|
+
Column("id", INTEGER, primary_key=True),
|
|
265
|
+
Column("val", MYSQL_INTEGER(), nullable=False),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
validate_code(
|
|
269
|
+
generator.generate(),
|
|
270
|
+
"""\
|
|
271
|
+
from sqlalchemy import INTEGER
|
|
272
|
+
from sqlalchemy.dialects.mysql import INTEGER
|
|
273
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
274
|
+
|
|
275
|
+
class Base(DeclarativeBase):
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class Num2(Base):
|
|
280
|
+
__tablename__ = 'num2'
|
|
281
|
+
|
|
282
|
+
id: Mapped[int] = mapped_column(INTEGER, primary_key=True)
|
|
283
|
+
val: Mapped[int] = mapped_column(INTEGER, nullable=False)
|
|
284
|
+
""",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
114
288
|
def test_onetomany(generator: CodeGenerator) -> None:
|
|
115
289
|
Table(
|
|
116
290
|
"simple_items",
|
|
@@ -1563,6 +1737,62 @@ back_populates='simple_items')
|
|
|
1563
1737
|
)
|
|
1564
1738
|
|
|
1565
1739
|
|
|
1740
|
+
@pytest.mark.parametrize("generator", [["noidsuffix"]], indirect=True)
|
|
1741
|
+
def test_named_foreign_key_constraints_with_noidsuffix(
|
|
1742
|
+
generator: CodeGenerator,
|
|
1743
|
+
) -> None:
|
|
1744
|
+
Table(
|
|
1745
|
+
"simple_items",
|
|
1746
|
+
generator.metadata,
|
|
1747
|
+
Column("id", INTEGER, primary_key=True),
|
|
1748
|
+
Column("container_id", INTEGER),
|
|
1749
|
+
ForeignKeyConstraint(
|
|
1750
|
+
["container_id"], ["simple_containers.id"], name="foreignkeytest"
|
|
1751
|
+
),
|
|
1752
|
+
)
|
|
1753
|
+
Table(
|
|
1754
|
+
"simple_containers",
|
|
1755
|
+
generator.metadata,
|
|
1756
|
+
Column("id", INTEGER, primary_key=True),
|
|
1757
|
+
)
|
|
1758
|
+
|
|
1759
|
+
validate_code(
|
|
1760
|
+
generator.generate(),
|
|
1761
|
+
"""\
|
|
1762
|
+
from typing import Optional
|
|
1763
|
+
|
|
1764
|
+
from sqlalchemy import ForeignKeyConstraint, Integer
|
|
1765
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
1766
|
+
|
|
1767
|
+
class Base(DeclarativeBase):
|
|
1768
|
+
pass
|
|
1769
|
+
|
|
1770
|
+
|
|
1771
|
+
class SimpleContainers(Base):
|
|
1772
|
+
__tablename__ = 'simple_containers'
|
|
1773
|
+
|
|
1774
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
1775
|
+
|
|
1776
|
+
simple_items: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
|
|
1777
|
+
back_populates='simple_containers')
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
class SimpleItems(Base):
|
|
1781
|
+
__tablename__ = 'simple_items'
|
|
1782
|
+
__table_args__ = (
|
|
1783
|
+
ForeignKeyConstraint(['container_id'], ['simple_containers.id'], \
|
|
1784
|
+
name='foreignkeytest'),
|
|
1785
|
+
)
|
|
1786
|
+
|
|
1787
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
1788
|
+
container_id: Mapped[Optional[int]] = mapped_column(Integer)
|
|
1789
|
+
|
|
1790
|
+
simple_containers: Mapped[Optional['SimpleContainers']] = relationship('SimpleContainers', \
|
|
1791
|
+
back_populates='simple_items')
|
|
1792
|
+
""",
|
|
1793
|
+
)
|
|
1794
|
+
|
|
1795
|
+
|
|
1566
1796
|
# @pytest.mark.xfail(strict=True)
|
|
1567
1797
|
def test_colname_import_conflict(generator: CodeGenerator) -> None:
|
|
1568
1798
|
Table(
|
|
@@ -5,7 +5,8 @@ from textwrap import dedent
|
|
|
5
5
|
import pytest
|
|
6
6
|
from _pytest.fixtures import FixtureRequest
|
|
7
7
|
from sqlalchemy import TypeDecorator
|
|
8
|
-
from sqlalchemy.dialects import mysql, postgresql
|
|
8
|
+
from sqlalchemy.dialects import mysql, postgresql, registry
|
|
9
|
+
from sqlalchemy.dialects.mysql.pymysql import MySQLDialect_pymysql
|
|
9
10
|
from sqlalchemy.engine import Engine
|
|
10
11
|
from sqlalchemy.schema import (
|
|
11
12
|
CheckConstraint,
|
|
@@ -1090,3 +1091,85 @@ schema='{expected_schema}'), primary_key=True),
|
|
|
1090
1091
|
)
|
|
1091
1092
|
""",
|
|
1092
1093
|
)
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
class MockStarRocksDialect(MySQLDialect_pymysql):
|
|
1097
|
+
name = "starrocks"
|
|
1098
|
+
construct_arguments = [
|
|
1099
|
+
(
|
|
1100
|
+
Column,
|
|
1101
|
+
{
|
|
1102
|
+
"is_agg_key": None,
|
|
1103
|
+
"agg_type": None,
|
|
1104
|
+
"IS_AGG_KEY": None,
|
|
1105
|
+
"AGG_TYPE": None,
|
|
1106
|
+
},
|
|
1107
|
+
),
|
|
1108
|
+
(
|
|
1109
|
+
Table,
|
|
1110
|
+
{
|
|
1111
|
+
"primary_key": None,
|
|
1112
|
+
"aggregate_key": None,
|
|
1113
|
+
"unique_key": None,
|
|
1114
|
+
"duplicate_key": None,
|
|
1115
|
+
"engine": "OLAP",
|
|
1116
|
+
"partition_by": None,
|
|
1117
|
+
"order_by": None,
|
|
1118
|
+
"security": None,
|
|
1119
|
+
"properties": {},
|
|
1120
|
+
"ENGINE": "OLAP",
|
|
1121
|
+
"PARTITION_BY": None,
|
|
1122
|
+
"ORDER_BY": None,
|
|
1123
|
+
"SECURITY": None,
|
|
1124
|
+
"PROPERTIES": {},
|
|
1125
|
+
},
|
|
1126
|
+
),
|
|
1127
|
+
]
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
# Register StarRocksDialect
|
|
1131
|
+
registry.register("starrocks", __name__, "MockStarRocksDialect")
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
class _PartitionInfo:
|
|
1135
|
+
def __init__(self, partition_by: str) -> None:
|
|
1136
|
+
self.partition_by = partition_by
|
|
1137
|
+
|
|
1138
|
+
def __str__(self) -> str:
|
|
1139
|
+
return self.partition_by
|
|
1140
|
+
|
|
1141
|
+
def __repr__(self) -> str:
|
|
1142
|
+
return repr(self.partition_by)
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
@pytest.mark.parametrize("generator", [["include_dialect_options"]], indirect=True)
|
|
1146
|
+
def test_include_dialect_options_starrocks_tables(generator: CodeGenerator) -> None:
|
|
1147
|
+
Table(
|
|
1148
|
+
"t_starrocks",
|
|
1149
|
+
generator.metadata,
|
|
1150
|
+
Column("id", INTEGER, primary_key=True, starrocks_is_agg_key=True),
|
|
1151
|
+
starrocks_ENGINE="OLAP",
|
|
1152
|
+
starrocks_PARTITION_BY=_PartitionInfo("RANGE(id)"),
|
|
1153
|
+
starrocks_ORDER_BY="id, name",
|
|
1154
|
+
starrocks_PROPERTIES={"replication_num": "3", "storage_medium": "SSD"},
|
|
1155
|
+
).info = {"table_kind": "TABLE"}
|
|
1156
|
+
|
|
1157
|
+
validate_code(
|
|
1158
|
+
generator.generate(),
|
|
1159
|
+
"""\
|
|
1160
|
+
from sqlalchemy import Column, Integer, MetaData, Table
|
|
1161
|
+
|
|
1162
|
+
metadata = MetaData()
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
t_t_starrocks = Table(
|
|
1166
|
+
't_starrocks', metadata,
|
|
1167
|
+
Column('id', Integer, primary_key=True, starrocks_is_agg_key=True),
|
|
1168
|
+
info={'table_kind': 'TABLE'},
|
|
1169
|
+
starrocks_ENGINE='OLAP',
|
|
1170
|
+
starrocks_ORDER_BY='id, name',
|
|
1171
|
+
starrocks_PARTITION_BY='RANGE(id)',
|
|
1172
|
+
starrocks_PROPERTIES={'replication_num': '3', 'storage_medium': 'SSD'}
|
|
1173
|
+
)
|
|
1174
|
+
""",
|
|
1175
|
+
)
|
|
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
|