From 8abfb0fe4819e878f916055ce3993c12856190eb Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Sun, 24 Apr 2022 22:24:12 +0800 Subject: [PATCH 01/12] Add test cases for declarative --- tests/test_generators.py | 118 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/tests/test_generators.py b/tests/test_generators.py index c01ea013..c8d0dac0 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -19,6 +19,7 @@ MetaData, Table, UniqueConstraint, + conv, ) from sqlalchemy.sql.expression import text from sqlalchemy.sql.sqltypes import NullType @@ -2247,6 +2248,123 @@ class Simple(Base): """, ) + def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: + generator.metadata.naming_convention = { + "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", + "ck": "CHECK_%(table_name)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s" + "_%(referred_table_name)s_%(referred_column_0_label)s", + "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", + } + + Table( + "items", + generator.metadata, + Column("id", INTEGER), + Column("name", VARCHAR), + Column("container_id", INTEGER), + PrimaryKeyConstraint("id", "name", name="PRIMARY_items_idname"), + UniqueConstraint("id", name="UNIQUE_items_id"), + ForeignKeyConstraint( + ["container_id"], + ["containers.id"], + name="FOREIGN_items_container_id_containers_id", + ), + ) + Table( + "containers", + generator.metadata, + Column("id", INTEGER), + Column("name", VARCHAR), + PrimaryKeyConstraint("id", name="PRIMARY_containers_id"), + UniqueConstraint("id", "name", name="UNIQUE_containers_id_name"), + CheckConstraint("id > 0", name="CHECK_containers"), + ) + + validate_code( + generator.generate(), + """\ + from sqlalchemy import CheckConstraint, Column, ForeignKey, \ +Integer, String, UniqueConstraint + from sqlalchemy.orm import declarative_base, relationship + + metadata = MetaData.naming_convention = { + "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", + "ck": "CHECK_%(table_name)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_\ +%(referred_table_name)s_%(referred_column_0_label)s", + "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", + } + + Base = declarative_base(metadata=metadata) + + + class Containers(Base): + __tablename__ = 'containers' + __table_args__ = ( + CheckConstraint('id > 0'), + UniqueConstraint('id', 'name') + ) + + id = Column(Integer, primary_key=True) + name = Column(String) + + items = relationship('Items', back_populates='container') + + + class Items(Base): + __tablename__ = 'items' + + id = Column(Integer, primary_key=True, nullable=False, unique=True) + name = Column(String, primary_key=True, nullable=False) + container_id = Column(ForeignKey('containers.id')) + + container = relationship('Containers', back_populates='items') + """, + ) + + def test_constraint_name_token(self, generator: CodeGenerator) -> None: + generator.metadata.naming_convention = { + "ck": "ck_%(table_name)s_%(constraint_name)s", + "pk": "pk_%(table_name)s", + } + + Table( + "simple", + generator.metadata, + Column("id", INTEGER), + Column("number", INTEGER), + PrimaryKeyConstraint("id", name="pk_simple"), + CheckConstraint("id > 0", name=conv("ck_simple_idcheck")), + CheckConstraint("number > 0", name=conv("non_default_name")), + ) + + validate_code( + generator.generate(), + """\ + from sqlalchemy import CheckConstraint, Column, Integer + from sqlalchemy.orm import declarative_base + + metadata = MetaData.naming_convention = { + "ck": "ck_%(table_name)s_%(constraint_name)s", + "pk": "pk_%(table_name)s", + } + + Base = declarative_base(metadata=metadata) + + + class Simple(Base): + __tablename__ = 'simple' + __table_args__ = ( + CheckConstraint('id > 0', name='idcheck'), + CheckConstraint('number > 0', name=conv('non_default_name')) + ) + + id = Column(Integer, primary_key=True) + number = Column(Integer) + """, + ) + class TestDataclassGenerator: @pytest.fixture From ea907fee1507a8a43519d334f6e785c84db98676 Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Sun, 24 Apr 2022 23:45:24 +0800 Subject: [PATCH 02/12] Fix test cases --- tests/test_generators.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/test_generators.py b/tests/test_generators.py index c8d0dac0..b158d861 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -19,8 +19,8 @@ MetaData, Table, UniqueConstraint, - conv, ) +from sqlalchemy.sql.elements import conv from sqlalchemy.sql.expression import text from sqlalchemy.sql.sqltypes import NullType from sqlalchemy.types import INTEGER, NUMERIC, SMALLINT, VARCHAR, Text @@ -2285,16 +2285,18 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: generator.generate(), """\ from sqlalchemy import CheckConstraint, Column, ForeignKey, \ -Integer, String, UniqueConstraint +Integer, String, UniqueConstraint, MetaData from sqlalchemy.orm import declarative_base, relationship - metadata = MetaData.naming_convention = { - "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", - "ck": "CHECK_%(table_name)s", - "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_\ + metadata = MetaData( + naming_convention={ + "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", + "ck": "CHECK_%(table_name)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_\ %(referred_table_name)s_%(referred_column_0_label)s", - "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", - } + "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", + } + ) Base = declarative_base(metadata=metadata) @@ -2342,13 +2344,16 @@ def test_constraint_name_token(self, generator: CodeGenerator) -> None: validate_code( generator.generate(), """\ - from sqlalchemy import CheckConstraint, Column, Integer + from sqlalchemy import CheckConstraint, Column, Integer, MetaData from sqlalchemy.orm import declarative_base + from sqlalchemy.sql.elements import conv - metadata = MetaData.naming_convention = { - "ck": "ck_%(table_name)s_%(constraint_name)s", - "pk": "pk_%(table_name)s", - } + metadata = MetaData( + naming_convention={ + "ck": "ck_%(table_name)s_%(constraint_name)s", + "pk": "pk_%(table_name)s", + } + ) Base = declarative_base(metadata=metadata) From 14200bf22f52247ba4a27c4f10d139e86f4d33c3 Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Mon, 25 Apr 2022 00:35:44 +0800 Subject: [PATCH 03/12] Add tests for declarative and dataclass --- tests/test_generators.py | 150 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/tests/test_generators.py b/tests/test_generators.py index b158d861..0aa18526 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -967,6 +967,73 @@ def test_postgresql_sequence_with_schema(self, generator: CodeGenerator) -> None """, ) + def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: + generator.metadata.naming_convention = { + "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", + "ck": "CHECK_%(table_name)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s" + "_%(referred_table_name)s_%(referred_column_0_label)s", + "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", + } + + Table( + "items", + generator.metadata, + Column("id", INTEGER), + Column("name", VARCHAR), + Column("container_id", INTEGER), + PrimaryKeyConstraint("id", "name", name="PRIMARY_items_idname"), + UniqueConstraint("id", name="UNIQUE_items_id"), + ForeignKeyConstraint( + ["container_id"], + ["containers.id"], + name="FOREIGN_items_container_id_containers_id", + ), + ) + Table( + "containers", + generator.metadata, + Column("id", INTEGER), + Column("name", VARCHAR), + PrimaryKeyConstraint("id", name="PRIMARY_containers_id"), + UniqueConstraint("id", "name", name="UNIQUE_containers_id_name"), + CheckConstraint("id > 0", name="CHECK_containers"), + ) + + validate_code( + generator.generate(), + """\ + from sqlalchemy import CheckConstraint, Column, ForeignKey, \ +Integer, MetaData, String, Table, UniqueConstraint + + metadata = MetaData( + naming_convention={ + "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", + "ck": "CHECK_%(table_name)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_\ +%(referred_table_name)s_%(referred_column_0_label)s", + "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", + } + ) + + + t_containers = Table( + 'containers', metadata, + Column('id', Integer, primary_key=True), + Column('name', String), + CheckConstraint('id > 0'), + UniqueConstraint('id', 'name') + ) + + t_items = Table( + 'items', metadata, + Column('id', Integer, primary_key=True, nullable=False, unique=True), + Column('name', String, primary_key=True, nullable=False), + Column('container_id', ForeignKey('containers.id')) + ) + """, + ) + class TestDeclarativeGenerator: @pytest.fixture @@ -2672,6 +2739,89 @@ class Simple: """, ) + def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: + generator.metadata.naming_convention = { + "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", + "ck": "CHECK_%(table_name)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s" + "_%(referred_table_name)s_%(referred_column_0_label)s", + "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", + } + + Table( + "items", + generator.metadata, + Column("id", INTEGER), + Column("name", VARCHAR), + Column("container_id", INTEGER), + PrimaryKeyConstraint("id", "name", name="PRIMARY_items_idname"), + UniqueConstraint("id", name="UNIQUE_items_id"), + ForeignKeyConstraint( + ["container_id"], + ["containers.id"], + name="FOREIGN_items_container_id_containers_id", + ), + ) + Table( + "containers", + generator.metadata, + Column("id", INTEGER), + Column("name", VARCHAR), + PrimaryKeyConstraint("id", name="PRIMARY_containers_id"), + UniqueConstraint("id", "name", name="UNIQUE_containers_id_name"), + CheckConstraint("id > 0", name="CHECK_containers"), + ) + + validate_code( + generator.generate(), + """\ + from __future__ import annotations + from dataclasses import dataclass, field + from typing import List, Optional + from sqlalchemy import CheckConstraint, Column, ForeignKey, \ +Integer, String, UniqueConstraint + from sqlalchemy.orm import registry, relationship + metadata = MetaData( + naming_convention={ + "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", + "ck": "CHECK_%(table_name)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_\ + %(referred_table_name)s_%(referred_column_0_label)s", + "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", + } + ) + mapper_registry = registry(metadata=metadata) + @mapper_registry.mapped + @dataclass + class Containers: + __tablename__ = 'containers' + __table_args__ = ( + CheckConstraint('id > 0'), + UniqueConstraint('id', 'name') + ) + __sa_dataclass_metadata_key__ = 'sa' + id: int = field(init=False, metadata={'sa': \ +Column(Integer, primary_key=True)}) + name: Optional[str] = field(default=None, metadata={'sa': \ +Column(String)}) + items: List[Items] = field(default_factory=list, metadata={'sa': \ +relationship('Items', back_populates='container')}) + @mapper_registry.mapped + @dataclass + class Items: + __tablename__ = 'items' + __sa_dataclass_metadata_key__ = 'sa' + id: int = field(init=False, metadata={'sa': \ +Column(Integer, primary_key=True, nullable=False, unique=True)}) + name: str = field(init=False, metadata={'sa': \ +Column(String, primary_key=True, nullable=False)}) + container_id: Optional[int] = field(default=None, metadata={'sa': \ +Column(ForeignKey('containers.id'))}) + container: Optional[Containers] = field(default=None, metadata={'sa': \ +relationship('Containers', back_populates='items')}) + """, + ) + class TestSQLModelGenerator: @pytest.fixture From b327cb75d72ad3d689b8fafb51b05051bc7b8413 Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Tue, 26 Apr 2022 15:25:19 +0800 Subject: [PATCH 04/12] Implement naming conventions for TablesGenerator --- src/sqlacodegen/generators.py | 9 ++++++- tests/test_generators.py | 50 +++++++++++++++++------------------ 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/sqlacodegen/generators.py b/src/sqlacodegen/generators.py index 55297970..87f5c21d 100644 --- a/src/sqlacodegen/generators.py +++ b/src/sqlacodegen/generators.py @@ -42,6 +42,7 @@ from sqlalchemy.engine import Connection, Engine from sqlalchemy.exc import CompileError from sqlalchemy.sql.elements import TextClause +from sqlalchemy.sql.schema import DEFAULT_NAMING_CONVENTION from .models import ( ColumnAttribute, @@ -303,7 +304,13 @@ def generate_model_name(self, model: Model, global_names: set[str]) -> None: model.name = self.find_free_name(preferred_name, global_names) def render_module_variables(self, models: list[Model]) -> str: - return "metadata = MetaData()" + module_vars = ["metadata = MetaData()"] + if self.metadata.naming_convention != DEFAULT_NAMING_CONVENTION: + formatted_naming_convention = pformat(self.metadata.naming_convention) + module_vars.append( + f"metadata.naming_convention = {formatted_naming_convention}" + ) + return "\n".join(module_vars) def render_models(self, models: list[Model]) -> str: rendered = [] diff --git a/tests/test_generators.py b/tests/test_generators.py index 0aa18526..3eff2d0a 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -971,8 +971,7 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: generator.metadata.naming_convention = { "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", "ck": "CHECK_%(table_name)s", - "fk": "FOREIGN_%(table_name)s_%(column_0_key)s" - "_%(referred_table_name)s_%(referred_column_0_label)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s", "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", } @@ -987,7 +986,7 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: ForeignKeyConstraint( ["container_id"], ["containers.id"], - name="FOREIGN_items_container_id_containers_id", + name="FOREIGN_items_container_id_containers", ), ) Table( @@ -1003,34 +1002,30 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: validate_code( generator.generate(), """\ - from sqlalchemy import CheckConstraint, Column, ForeignKey, \ +from sqlalchemy import CheckConstraint, Column, ForeignKey, \ Integer, MetaData, String, Table, UniqueConstraint - metadata = MetaData( - naming_convention={ - "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", - "ck": "CHECK_%(table_name)s", - "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_\ -%(referred_table_name)s_%(referred_column_0_label)s", - "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", - } - ) +metadata = MetaData() +metadata.naming_convention = {'ck': 'CHECK_%(table_name)s', + 'fk': 'FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s', + 'pk': 'PRIMARY_%(table_name)s_%(column_0N_name)s', + 'uq': 'UNIQUE_%(table_name)s_%(column_0_N_name)s'} - t_containers = Table( - 'containers', metadata, - Column('id', Integer, primary_key=True), - Column('name', String), - CheckConstraint('id > 0'), - UniqueConstraint('id', 'name') - ) +t_containers = Table( + 'containers', metadata, + Column('id', Integer, primary_key=True), + Column('name', String), + CheckConstraint('id > 0'), + UniqueConstraint('id', 'name') +) - t_items = Table( - 'items', metadata, - Column('id', Integer, primary_key=True, nullable=False, unique=True), - Column('name', String, primary_key=True, nullable=False), - Column('container_id', ForeignKey('containers.id')) - ) +t_items = Table( + 'items', metadata, + Column('id', Integer, primary_key=True, nullable=False, unique=True), + Column('name', String, primary_key=True, nullable=False), + Column('container_id', ForeignKey('containers.id')) +) """, ) @@ -2315,6 +2310,7 @@ class Simple(Base): """, ) + @pytest.mark.xfail def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: generator.metadata.naming_convention = { "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", @@ -2392,6 +2388,7 @@ class Items(Base): """, ) + @pytest.mark.xfail def test_constraint_name_token(self, generator: CodeGenerator) -> None: generator.metadata.naming_convention = { "ck": "ck_%(table_name)s_%(constraint_name)s", @@ -2739,6 +2736,7 @@ class Simple: """, ) + @pytest.mark.xfail def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: generator.metadata.naming_convention = { "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", From ac26d509652cb7f9c10d38ace29949b7a6e23953 Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Tue, 26 Apr 2022 15:34:35 +0800 Subject: [PATCH 05/12] Implement naming conventions for DeclarativeGenerator --- src/sqlacodegen/generators.py | 7 +++++ tests/test_generators.py | 58 +++++++++++++++-------------------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/sqlacodegen/generators.py b/src/sqlacodegen/generators.py index 87f5c21d..58e816f5 100644 --- a/src/sqlacodegen/generators.py +++ b/src/sqlacodegen/generators.py @@ -1024,6 +1024,13 @@ def render_module_variables(self, models: list[Model]) -> str: return super().render_module_variables(models) declarations = [f"{self.base_class_name} = declarative_base()"] + + if self.metadata.naming_convention != DEFAULT_NAMING_CONVENTION: + formatted_naming_convention = pformat(self.metadata.naming_convention) + declarations.append( + f"Base.metadata.naming_convention = {formatted_naming_convention}" + ) + if any(not isinstance(model, ModelClass) for model in models): declarations.append(f"metadata = {self.base_class_name}.metadata") diff --git a/tests/test_generators.py b/tests/test_generators.py index 3eff2d0a..6e977cd9 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -2310,13 +2310,11 @@ class Simple(Base): """, ) - @pytest.mark.xfail def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: generator.metadata.naming_convention = { "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", "ck": "CHECK_%(table_name)s", - "fk": "FOREIGN_%(table_name)s_%(column_0_key)s" - "_%(referred_table_name)s_%(referred_column_0_label)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s", "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", } @@ -2331,7 +2329,7 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: ForeignKeyConstraint( ["container_id"], ["containers.id"], - name="FOREIGN_items_container_id_containers_id", + name="FOREIGN_items_container_id_containers", ), ) Table( @@ -2347,44 +2345,38 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: validate_code( generator.generate(), """\ - from sqlalchemy import CheckConstraint, Column, ForeignKey, \ -Integer, String, UniqueConstraint, MetaData - from sqlalchemy.orm import declarative_base, relationship - - metadata = MetaData( - naming_convention={ - "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", - "ck": "CHECK_%(table_name)s", - "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_\ -%(referred_table_name)s_%(referred_column_0_label)s", - "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", - } - ) +from sqlalchemy import CheckConstraint, Column, ForeignKey, \ +Integer, String, UniqueConstraint +from sqlalchemy.orm import declarative_base, relationship - Base = declarative_base(metadata=metadata) +Base = declarative_base() +Base.metadata.naming_convention = {'ck': 'CHECK_%(table_name)s', + 'fk': 'FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s', + 'pk': 'PRIMARY_%(table_name)s_%(column_0N_name)s', + 'uq': 'UNIQUE_%(table_name)s_%(column_0_N_name)s'} - class Containers(Base): - __tablename__ = 'containers' - __table_args__ = ( - CheckConstraint('id > 0'), - UniqueConstraint('id', 'name') - ) +class Containers(Base): + __tablename__ = 'containers' + __table_args__ = ( + CheckConstraint('id > 0'), + UniqueConstraint('id', 'name') + ) - id = Column(Integer, primary_key=True) - name = Column(String) + id = Column(Integer, primary_key=True) + name = Column(String) - items = relationship('Items', back_populates='container') + items = relationship('Items', back_populates='container') - class Items(Base): - __tablename__ = 'items' +class Items(Base): + __tablename__ = 'items' - id = Column(Integer, primary_key=True, nullable=False, unique=True) - name = Column(String, primary_key=True, nullable=False) - container_id = Column(ForeignKey('containers.id')) + id = Column(Integer, primary_key=True, nullable=False, unique=True) + name = Column(String, primary_key=True, nullable=False) + container_id = Column(ForeignKey('containers.id')) - container = relationship('Containers', back_populates='items') + container = relationship('Containers', back_populates='items') """, ) From 9364e452ba7878f6264e8dd40d1392a514051741 Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Tue, 26 Apr 2022 15:45:42 +0800 Subject: [PATCH 06/12] Implement naming conventions for DataclassGenerator --- src/sqlacodegen/generators.py | 9 ++++ tests/test_generators.py | 86 ++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/src/sqlacodegen/generators.py b/src/sqlacodegen/generators.py index 58e816f5..2bbbe6c2 100644 --- a/src/sqlacodegen/generators.py +++ b/src/sqlacodegen/generators.py @@ -1286,6 +1286,15 @@ def render_module_variables(self, models: list[Model]) -> str: return super().render_module_variables(models) declarations: list[str] = ["mapper_registry = registry()"] + + if self.metadata.naming_convention != DEFAULT_NAMING_CONVENTION: + formatted_naming_convention = pformat(self.metadata.naming_convention) + declarations.append( + "mapper_registry.metadata.naming_convention = {}".format( + formatted_naming_convention + ) + ) + if any(not isinstance(model, ModelClass) for model in models): declarations.append("metadata = mapper_registry.metadata") diff --git a/tests/test_generators.py b/tests/test_generators.py index 6e977cd9..702ec5c0 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -2728,13 +2728,11 @@ class Simple: """, ) - @pytest.mark.xfail def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: generator.metadata.naming_convention = { "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", "ck": "CHECK_%(table_name)s", - "fk": "FOREIGN_%(table_name)s_%(column_0_key)s" - "_%(referred_table_name)s_%(referred_column_0_label)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s", "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", } @@ -2749,7 +2747,7 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: ForeignKeyConstraint( ["container_id"], ["containers.id"], - name="FOREIGN_items_container_id_containers_id", + name="FOREIGN_items_container_id_containers", ), ) Table( @@ -2765,49 +2763,53 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: validate_code( generator.generate(), """\ - from __future__ import annotations - from dataclasses import dataclass, field - from typing import List, Optional - from sqlalchemy import CheckConstraint, Column, ForeignKey, \ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import List, Optional + +from sqlalchemy import CheckConstraint, Column, ForeignKey, \ Integer, String, UniqueConstraint - from sqlalchemy.orm import registry, relationship - metadata = MetaData( - naming_convention={ - "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", - "ck": "CHECK_%(table_name)s", - "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_\ - %(referred_table_name)s_%(referred_column_0_label)s", - "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", - } - ) - mapper_registry = registry(metadata=metadata) - @mapper_registry.mapped - @dataclass - class Containers: - __tablename__ = 'containers' - __table_args__ = ( - CheckConstraint('id > 0'), - UniqueConstraint('id', 'name') - ) - __sa_dataclass_metadata_key__ = 'sa' - id: int = field(init=False, metadata={'sa': \ -Column(Integer, primary_key=True)}) - name: Optional[str] = field(default=None, metadata={'sa': \ -Column(String)}) - items: List[Items] = field(default_factory=list, metadata={'sa': \ +from sqlalchemy.orm import registry, relationship + +mapper_registry = registry() +mapper_registry.metadata.naming_convention = {'ck': 'CHECK_%(table_name)s', + 'fk': 'FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s', + 'pk': 'PRIMARY_%(table_name)s_%(column_0N_name)s', + 'uq': 'UNIQUE_%(table_name)s_%(column_0_N_name)s'} + + +@mapper_registry.mapped +@dataclass +class Containers: + __tablename__ = 'containers' + __table_args__ = ( + CheckConstraint('id > 0'), + UniqueConstraint('id', 'name') + ) + __sa_dataclass_metadata_key__ = 'sa' + + id: int = field(init=False, metadata={'sa': Column(Integer, primary_key=True)}) + name: Optional[str] = field(default=None, metadata={'sa': Column(String)}) + + items: List[Items] = field(default_factory=list, metadata={'sa': \ relationship('Items', back_populates='container')}) - @mapper_registry.mapped - @dataclass - class Items: - __tablename__ = 'items' - __sa_dataclass_metadata_key__ = 'sa' - id: int = field(init=False, metadata={'sa': \ + + +@mapper_registry.mapped +@dataclass +class Items: + __tablename__ = 'items' + __sa_dataclass_metadata_key__ = 'sa' + + id: int = field(init=False, metadata={'sa': \ Column(Integer, primary_key=True, nullable=False, unique=True)}) - name: str = field(init=False, metadata={'sa': \ + name: str = field(init=False, metadata={'sa': \ Column(String, primary_key=True, nullable=False)}) - container_id: Optional[int] = field(default=None, metadata={'sa': \ + container_id: Optional[int] = field(default=None, metadata={'sa': \ Column(ForeignKey('containers.id'))}) - container: Optional[Containers] = field(default=None, metadata={'sa': \ + + container: Optional[Containers] = field(default=None, metadata={'sa': \ relationship('Containers', back_populates='items')}) """, ) From 287fb5df8105c287e38d14ab0c33827ee5bd5414 Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Tue, 26 Apr 2022 16:24:45 +0800 Subject: [PATCH 07/12] Fix constraint name token test case --- tests/test_generators.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/tests/test_generators.py b/tests/test_generators.py index 702ec5c0..6dd7e506 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -2400,29 +2400,24 @@ def test_constraint_name_token(self, generator: CodeGenerator) -> None: validate_code( generator.generate(), """\ - from sqlalchemy import CheckConstraint, Column, Integer, MetaData - from sqlalchemy.orm import declarative_base - from sqlalchemy.sql.elements import conv - - metadata = MetaData( - naming_convention={ - "ck": "ck_%(table_name)s_%(constraint_name)s", - "pk": "pk_%(table_name)s", - } - ) +from sqlalchemy import CheckConstraint, Column, Integer, MetaData +from sqlalchemy.orm import declarative_base +from sqlalchemy.sql.elements import conv - Base = declarative_base(metadata=metadata) +Base = declarative_base() +Base.metadata.naming_convention = \ +{'ck': 'ck_%(table_name)s_%(constraint_name)s', 'pk': 'pk_%(table_name)s'} - class Simple(Base): - __tablename__ = 'simple' - __table_args__ = ( - CheckConstraint('id > 0', name='idcheck'), - CheckConstraint('number > 0', name=conv('non_default_name')) - ) +class Simple(Base): + __tablename__ = 'simple' + __table_args__ = ( + CheckConstraint('id > 0', name='idcheck'), + CheckConstraint('number > 0', name=conv('non_default_name')) + ) - id = Column(Integer, primary_key=True) - number = Column(Integer) + id = Column(Integer, primary_key=True) + number = Column(Integer) """, ) From 78e322d31aadc7297b78eebf93254bfad0377f87 Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Tue, 26 Apr 2022 20:15:32 +0800 Subject: [PATCH 08/12] Add naming constraint as cli args --- src/sqlacodegen/cli.py | 26 +++++++++++++++++++++++++- tests/test_cli.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/sqlacodegen/cli.py b/src/sqlacodegen/cli.py index 3e191a0d..f6a5435f 100644 --- a/src/sqlacodegen/cli.py +++ b/src/sqlacodegen/cli.py @@ -3,7 +3,7 @@ import argparse import sys from contextlib import ExitStack -from typing import TextIO +from typing import Sequence, TextIO from sqlalchemy.engine import create_engine from sqlalchemy.schema import MetaData @@ -14,6 +14,18 @@ from importlib.metadata import entry_points, version +def parse_naming_convs(naming_convs: Sequence[str]) -> dict[str, str]: + d = {} + for naming_conv in naming_convs: + try: + key, value = naming_conv.split("=", 1) + except ValueError: + raise ValueError('Naming convention must be in "key=template" format') + + d[key] = value + return d + + def main() -> None: generators = {ep.name: ep for ep in entry_points(group="sqlacodegen.generators")} parser = argparse.ArgumentParser( @@ -40,6 +52,13 @@ def main() -> None: ) parser.add_argument("--noviews", action="store_true", help="ignore views") parser.add_argument("--outfile", help="file to write output to (default: stdout)") + parser.add_argument( + "--conv", + nargs="*", + help='constraint naming conventions in "key=template" format \ +e.g., --conv "pk=pk_%%(table_name)s" "uq=uq_%%(table_name)s_%%(column_0_name)s"', + ) + args = parser.parse_args() if args.version: @@ -58,6 +77,11 @@ def main() -> None: for schema in schemas: metadata.reflect(engine, schema, not args.noviews, tables) + # Naming convention must be added after reflection to + # avoid the token %(constraint_name)s duplicating the name + if args.conv: + metadata.naming_convention = parse_naming_convs(args.conv) + # Instantiate the generator generator_class = generators[args.generator].load() generator = generator_class(metadata, engine, set(args.option or ())) diff --git a/tests/test_cli.py b/tests/test_cli.py index fde265b2..3a82edb9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -172,3 +172,36 @@ def test_main() -> None: check=True, ) assert completed.stdout.decode().strip() == expected_version + + +@pytest.fixture +def empty_db_path(tmp_path: Path) -> Path: + path = tmp_path / "test.db" + + return path + + +def test_naming_convention(empty_db_path: Path, tmp_path: Path) -> None: + output_path = tmp_path / "outfile" + subprocess.run( + [ + "sqlacodegen", + f"sqlite:///{empty_db_path}", + "--outfile", + str(output_path), + "--conv", + "pk=pk_%(table_name)s", + ], + check=True, + ) + + assert ( + output_path.read_text() + == """\ +from sqlalchemy import MetaData + +metadata = MetaData() +metadata.naming_convention = {'pk': 'pk_%(table_name)s'} + +""" + ) From 7e0ef7d5e8c528ceabe9acaf77710a57b4430a38 Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Wed, 27 Apr 2022 11:24:18 +0800 Subject: [PATCH 09/12] Implement naming conventions for SQLModelGenerator --- src/sqlacodegen/generators.py | 9 +++++ tests/test_generators.py | 73 +++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/src/sqlacodegen/generators.py b/src/sqlacodegen/generators.py index 2bbbe6c2..20d33e9d 100644 --- a/src/sqlacodegen/generators.py +++ b/src/sqlacodegen/generators.py @@ -1417,6 +1417,15 @@ def collect_imports_for_column(self, column: Column[Any]) -> None: def render_module_variables(self, models: list[Model]) -> str: declarations: list[str] = [] + + if self.metadata.naming_convention != DEFAULT_NAMING_CONVENTION: + formatted_naming_convention = pformat(self.metadata.naming_convention) + declarations.append( + "{}.metadata.naming_convention = {}".format( + self.base_class_name, formatted_naming_convention + ) + ) + if any(not isinstance(model, ModelClass) for model in models): declarations.append(f"metadata = {self.base_class_name}.metadata") diff --git a/tests/test_generators.py b/tests/test_generators.py index 6dd7e506..e001bfe2 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -2967,3 +2967,76 @@ class SimpleOnetoone(SQLModel, table=True): back_populates='simple_onetoone') """, ) + + def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: + generator.metadata.naming_convention = { + "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", + "ck": "CHECK_%(table_name)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s", + "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", + } + + Table( + "items", + generator.metadata, + Column("id", INTEGER), + Column("name", VARCHAR), + Column("container_id", INTEGER), + PrimaryKeyConstraint("id", "name", name="PRIMARY_items_idname"), + UniqueConstraint("id", name="UNIQUE_items_id"), + ForeignKeyConstraint( + ["container_id"], + ["containers.id"], + name="FOREIGN_items_container_id_containers", + ), + ) + Table( + "containers", + generator.metadata, + Column("id", INTEGER), + Column("name", VARCHAR), + PrimaryKeyConstraint("id", name="PRIMARY_containers_id"), + UniqueConstraint("id", "name", name="UNIQUE_containers_id_name"), + CheckConstraint("id > 0", name="CHECK_containers"), + ) + + validate_code( + generator.generate(), + """\ +from typing import List, Optional + +from sqlalchemy import CheckConstraint, Column, ForeignKey, Integer, \ +String, UniqueConstraint +from sqlmodel import Field, Relationship, SQLModel + +SQLModel.metadata.naming_convention = {'ck': 'CHECK_%(table_name)s', + 'fk': 'FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s', + 'pk': 'PRIMARY_%(table_name)s_%(column_0N_name)s', + 'uq': 'UNIQUE_%(table_name)s_%(column_0_N_name)s'} + + +class Containers(SQLModel, table=True): + __table_args__ = ( + CheckConstraint('id > 0'), + UniqueConstraint('id', 'name') + ) + + id: Optional[int] = Field(default=None, sa_column=Column(\ +'id', Integer, primary_key=True)) + name: Optional[str] = Field(default=None, sa_column=Column(\ +'name', String)) + + items: List['Items'] = Relationship(back_populates='container') + + +class Items(SQLModel, table=True): + id: Optional[int] = Field(default=None, sa_column=Column(\ +'id', Integer, primary_key=True, nullable=False, unique=True)) + name: Optional[str] = Field(default=None, sa_column=Column(\ +'name', String, primary_key=True, nullable=False)) + container_id: Optional[int] = Field(default=None, sa_column=Column(\ +'container_id', ForeignKey('containers.id'))) + + container: Optional['Containers'] = Relationship(back_populates='items') + """, + ) From e60f480ef452c67f08e140e0a8da5b52e1cd0dc9 Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Wed, 27 Apr 2022 18:04:23 +0800 Subject: [PATCH 10/12] Add referred_column_0_name to test cases --- tests/test_generators.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/tests/test_generators.py b/tests/test_generators.py index e001bfe2..f5356c07 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -971,7 +971,8 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: generator.metadata.naming_convention = { "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", "ck": "CHECK_%(table_name)s", - "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_\ +%(referred_table_name)s_%(referred_column_0_name)s", "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", } @@ -986,7 +987,7 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: ForeignKeyConstraint( ["container_id"], ["containers.id"], - name="FOREIGN_items_container_id_containers", + name="FOREIGN_items_container_id_containers_id", ), ) Table( @@ -1007,7 +1008,8 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: metadata = MetaData() metadata.naming_convention = {'ck': 'CHECK_%(table_name)s', - 'fk': 'FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s', + 'fk': 'FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s_\ +%(referred_column_0_name)s', 'pk': 'PRIMARY_%(table_name)s_%(column_0N_name)s', 'uq': 'UNIQUE_%(table_name)s_%(column_0_N_name)s'} @@ -2314,7 +2316,8 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: generator.metadata.naming_convention = { "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", "ck": "CHECK_%(table_name)s", - "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_\ +%(referred_table_name)s_%(referred_column_0_name)s", "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", } @@ -2329,7 +2332,7 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: ForeignKeyConstraint( ["container_id"], ["containers.id"], - name="FOREIGN_items_container_id_containers", + name="FOREIGN_items_container_id_containers_id", ), ) Table( @@ -2351,7 +2354,8 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: Base = declarative_base() Base.metadata.naming_convention = {'ck': 'CHECK_%(table_name)s', - 'fk': 'FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s', + 'fk': 'FOREIGN_%(table_name)s_%(column_0_key)s_\ +%(referred_table_name)s_%(referred_column_0_name)s', 'pk': 'PRIMARY_%(table_name)s_%(column_0N_name)s', 'uq': 'UNIQUE_%(table_name)s_%(column_0_N_name)s'} @@ -2727,7 +2731,8 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: generator.metadata.naming_convention = { "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", "ck": "CHECK_%(table_name)s", - "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s\ +_%(referred_table_name)s_%(referred_column_0_name)s", "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", } @@ -2742,7 +2747,7 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: ForeignKeyConstraint( ["container_id"], ["containers.id"], - name="FOREIGN_items_container_id_containers", + name="FOREIGN_items_container_id_containers_id", ), ) Table( @@ -2769,7 +2774,8 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: mapper_registry = registry() mapper_registry.metadata.naming_convention = {'ck': 'CHECK_%(table_name)s', - 'fk': 'FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s', + 'fk': 'FOREIGN_%(table_name)s_%(column_0_key)s_\ +%(referred_table_name)s_%(referred_column_0_name)s', 'pk': 'PRIMARY_%(table_name)s_%(column_0N_name)s', 'uq': 'UNIQUE_%(table_name)s_%(column_0_N_name)s'} @@ -2972,7 +2978,8 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: generator.metadata.naming_convention = { "uq": "UNIQUE_%(table_name)s_%(column_0_N_name)s", "ck": "CHECK_%(table_name)s", - "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s", + "fk": "FOREIGN_%(table_name)s_%(column_0_key)s_\ +%(referred_table_name)s_%(referred_column_0_name)s", "pk": "PRIMARY_%(table_name)s_%(column_0N_name)s", } @@ -2987,7 +2994,7 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: ForeignKeyConstraint( ["container_id"], ["containers.id"], - name="FOREIGN_items_container_id_containers", + name="FOREIGN_items_container_id_containers_id", ), ) Table( @@ -3010,7 +3017,8 @@ def test_constraints_with_default_names(self, generator: CodeGenerator) -> None: from sqlmodel import Field, Relationship, SQLModel SQLModel.metadata.naming_convention = {'ck': 'CHECK_%(table_name)s', - 'fk': 'FOREIGN_%(table_name)s_%(column_0_key)s_%(referred_table_name)s', + 'fk': 'FOREIGN_%(table_name)s_%(column_0_key)s_\ +%(referred_table_name)s_%(referred_column_0_name)s', 'pk': 'PRIMARY_%(table_name)s_%(column_0N_name)s', 'uq': 'UNIQUE_%(table_name)s_%(column_0_N_name)s'} From bd5c166327d9224bc5f062edc37ffad415cdd04e Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Wed, 27 Apr 2022 20:53:56 +0800 Subject: [PATCH 11/12] Implement constraint_name naming convention token --- src/sqlacodegen/generators.py | 42 +++++++++++++++----------- src/sqlacodegen/utils.py | 56 +++++++++++++++++++++++++++++++---- tests/test_generators.py | 7 +++-- 3 files changed, 79 insertions(+), 26 deletions(-) diff --git a/src/sqlacodegen/generators.py b/src/sqlacodegen/generators.py index 20d33e9d..761783e9 100644 --- a/src/sqlacodegen/generators.py +++ b/src/sqlacodegen/generators.py @@ -41,7 +41,7 @@ from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.engine import Connection, Engine from sqlalchemy.exc import CompileError -from sqlalchemy.sql.elements import TextClause +from sqlalchemy.sql.elements import TextClause, conv from sqlalchemy.sql.schema import DEFAULT_NAMING_CONVENTION from .models import ( @@ -57,9 +57,9 @@ get_common_fk_constraints, get_compiled_expression, get_constraint_sort_key, + get_explicit_name, qualified_table_name, render_callable, - uses_default_name, ) if sys.version_info < (3, 10): @@ -210,22 +210,25 @@ def collect_imports_for_column(self, column: Column[Any]) -> None: def collect_imports_for_constraint(self, constraint: Constraint | Index) -> None: if isinstance(constraint, Index): - if len(constraint.columns) > 1 or not uses_default_name(constraint): + if len(constraint.columns) > 1 or get_explicit_name(constraint): self.add_literal_import("sqlalchemy", "Index") elif isinstance(constraint, PrimaryKeyConstraint): - if not uses_default_name(constraint): + if get_explicit_name(constraint): self.add_literal_import("sqlalchemy", "PrimaryKeyConstraint") elif isinstance(constraint, UniqueConstraint): - if len(constraint.columns) > 1 or not uses_default_name(constraint): + if len(constraint.columns) > 1 or get_explicit_name(constraint): self.add_literal_import("sqlalchemy", "UniqueConstraint") elif isinstance(constraint, ForeignKeyConstraint): - if len(constraint.columns) > 1 or not uses_default_name(constraint): + if len(constraint.columns) > 1 or get_explicit_name(constraint): self.add_literal_import("sqlalchemy", "ForeignKeyConstraint") else: self.add_import(ForeignKey) else: self.add_import(constraint) + if isinstance(get_explicit_name(constraint), conv): + self.add_literal_import("sqlalchemy.sql.elements", "conv") + def add_import(self, obj: Any) -> None: # Don't store builtin imports if getattr(obj, "__module__", "builtins") == "builtins": @@ -329,7 +332,7 @@ def render_table(self, table: Table) -> str: args.append(self.render_column(column, True)) for constraint in sorted(table.constraints, key=get_constraint_sort_key): - if uses_default_name(constraint): + if not get_explicit_name(constraint): if isinstance(constraint, PrimaryKeyConstraint): continue elif isinstance(constraint, (ForeignKeyConstraint, UniqueConstraint)): @@ -340,7 +343,7 @@ def render_table(self, table: Table) -> str: for index in sorted(table.indexes, key=lambda i: i.name): # One-column indexes should be rendered as index=True on columns - if len(index.columns) > 1 or not uses_default_name(index): + if len(index.columns) > 1 or get_explicit_name(index): args.append(self.render_index(index)) if table.schema: @@ -370,26 +373,26 @@ def render_column(self, column: Column[Any], show_name: bool) -> str: for c in column.foreign_keys if c.constraint and len(c.constraint.columns) == 1 - and uses_default_name(c.constraint) + and not get_explicit_name(c.constraint) ] is_unique = any( isinstance(c, UniqueConstraint) and set(c.columns) == {column} - and uses_default_name(c) + and not get_explicit_name(c) for c in column.table.constraints ) is_unique = is_unique or any( - i.unique and set(i.columns) == {column} and uses_default_name(i) + i.unique and set(i.columns) == {column} and not get_explicit_name(i) for i in column.table.indexes ) is_primary = any( isinstance(c, PrimaryKeyConstraint) and column.name in c.columns - and uses_default_name(c) + and not get_explicit_name(c) for c in column.table.constraints ) has_index = any( - set(i.columns) == {column} and uses_default_name(i) + set(i.columns) == {column} and not get_explicit_name(i) for i in column.table.indexes ) @@ -529,8 +532,13 @@ def add_fk_options(*opts: Any) -> None: f"Cannot render constraint of type {constraint.__class__.__name__}" ) - if isinstance(constraint, Constraint) and not uses_default_name(constraint): - kwargs["name"] = repr(constraint.name) + if isinstance(constraint, Constraint): + explicit_name = get_explicit_name(constraint) + if explicit_name: + if isinstance(explicit_name, conv): + kwargs["name"] = render_callable(conv.__name__, repr(explicit_name)) + else: + kwargs["name"] = repr(explicit_name) return render_callable(constraint.__class__.__name__, *args, kwargs=kwargs) @@ -1103,7 +1111,7 @@ def render_table_args(self, table: Table) -> str: # Render constraints for constraint in sorted(table.constraints, key=get_constraint_sort_key): - if uses_default_name(constraint): + if not get_explicit_name(constraint): if isinstance(constraint, PrimaryKeyConstraint): continue if ( @@ -1116,7 +1124,7 @@ def render_table_args(self, table: Table) -> str: # Render indexes for index in sorted(table.indexes, key=lambda i: i.name): - if len(index.columns) > 1 or not uses_default_name(index): + if len(index.columns) > 1 or get_explicit_name(index): args.append(self.render_index(index)) if table.schema: diff --git a/src/sqlacodegen/utils.py b/src/sqlacodegen/utils.py index ba1671d8..1b03dd9a 100644 --- a/src/sqlacodegen/utils.py +++ b/src/sqlacodegen/utils.py @@ -1,10 +1,12 @@ from __future__ import annotations +import re from collections.abc import Mapping from sqlalchemy import PrimaryKeyConstraint, UniqueConstraint from sqlalchemy.engine import Connectable from sqlalchemy.sql import ClauseElement +from sqlalchemy.sql.elements import conv from sqlalchemy.sql.schema import ( CheckConstraint, ColumnCollectionConstraint, @@ -53,12 +55,46 @@ def get_common_fk_constraints( return c1.union(c2) -def uses_default_name(constraint: Constraint | Index) -> bool: +def _handle_constraint_name_token( + constraint_name: str, + convention: str, + values: dict[str, str], +) -> str | conv: + """ + Get explicit name for conventions with the token `constraint_name` using regex + + Replace first occurence of the token with (\\w+) and subsequent ones with (\1), + then add ^ and $ for exact match + + Example: + If `convention` is `abc_%(constraint_name)s_123`, the regex pattern will + be `^abc_(\\w+)_123$`, the first (and only) matched group will then be returned + + """ + placeholder = "%(constraint_name)s" + try: + pattern = convention % {**values, **{"constraint_name": placeholder}} + except KeyError: + return conv(constraint_name) + + pattern = re.escape(pattern) + escaped_placeholder = re.escape(placeholder) + + # Replace first occurence with (\w+) and subsequent ones with (\1), then add ^ and $ + pattern = pattern.replace(escaped_placeholder, r"(\w+)", 1) + pattern = pattern.replace(escaped_placeholder, r"(\1)") + pattern = "".join(["^", pattern, "$"]) + + match = re.match(pattern, constraint_name) + return conv(constraint_name) if match is None else match[1] + + +def get_explicit_name(constraint: Constraint | Index) -> str: if not constraint.name or constraint.table is None: - return True + return "" table = constraint.table - values = {"table_name": table.name, "constraint_name": constraint.name} + values = {"table_name": table.name} if isinstance(constraint, (Index, ColumnCollectionConstraint)): values.update( { @@ -130,11 +166,19 @@ def uses_default_name(constraint: Constraint | Index) -> bool: else: raise TypeError(f"Unknown constraint type: {constraint.__class__.__qualname__}") + if key not in table.metadata.naming_convention: + return constraint.name + + convention: str = table.metadata.naming_convention[key] + if "%(constraint_name)s" in convention: + return _handle_constraint_name_token(constraint.name, convention, values) + try: - convention: str = table.metadata.naming_convention[key] - return constraint.name == (convention % values) + parsed = convention % values + # No explicit name needed if constraint name already follows naming convention + return "" if constraint.name == parsed else constraint.name except KeyError: - return False + return constraint.name def render_callable( diff --git a/tests/test_generators.py b/tests/test_generators.py index f5356c07..02495bc9 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -2384,7 +2384,6 @@ class Items(Base): """, ) - @pytest.mark.xfail def test_constraint_name_token(self, generator: CodeGenerator) -> None: generator.metadata.naming_convention = { "ck": "ck_%(table_name)s_%(constraint_name)s", @@ -2399,12 +2398,13 @@ def test_constraint_name_token(self, generator: CodeGenerator) -> None: PrimaryKeyConstraint("id", name="pk_simple"), CheckConstraint("id > 0", name=conv("ck_simple_idcheck")), CheckConstraint("number > 0", name=conv("non_default_name")), + CheckConstraint("number > 1", name=conv("non_default_name2")), ) validate_code( generator.generate(), """\ -from sqlalchemy import CheckConstraint, Column, Integer, MetaData +from sqlalchemy import CheckConstraint, Column, Integer from sqlalchemy.orm import declarative_base from sqlalchemy.sql.elements import conv @@ -2417,7 +2417,8 @@ class Simple(Base): __tablename__ = 'simple' __table_args__ = ( CheckConstraint('id > 0', name='idcheck'), - CheckConstraint('number > 0', name=conv('non_default_name')) + CheckConstraint('number > 0', name=conv('non_default_name')), + CheckConstraint('number > 1', name=conv('non_default_name2')) ) id = Column(Integer, primary_key=True) From f5016973bffd3c98708d46bf63071cb38a5cd4e2 Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Wed, 27 Apr 2022 20:59:29 +0800 Subject: [PATCH 12/12] Add more docs and fix type hint --- src/sqlacodegen/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sqlacodegen/utils.py b/src/sqlacodegen/utils.py index 1b03dd9a..cc088523 100644 --- a/src/sqlacodegen/utils.py +++ b/src/sqlacodegen/utils.py @@ -66,6 +66,10 @@ def _handle_constraint_name_token( Replace first occurence of the token with (\\w+) and subsequent ones with (\1), then add ^ and $ for exact match + :param constraint_name: name of constraint + :param convention: naming convention of the constraint as defined in metadata + :param values: mapping of token key and value + Example: If `convention` is `abc_%(constraint_name)s_123`, the regex pattern will be `^abc_(\\w+)_123$`, the first (and only) matched group will then be returned @@ -89,7 +93,7 @@ def _handle_constraint_name_token( return conv(constraint_name) if match is None else match[1] -def get_explicit_name(constraint: Constraint | Index) -> str: +def get_explicit_name(constraint: Constraint | Index) -> str | conv: if not constraint.name or constraint.table is None: return ""