pax_global_header 0000666 0000000 0000000 00000000064 15215155472 0014521 g ustar 00root root 0000000 0000000 52 comment=1b321d1bc5a16a54ebf1f68a3a07fd9c21f86cde
agronholm-sqlacodegen-1b321d1/ 0000775 0000000 0000000 00000000000 15215155472 0016311 5 ustar 00root root 0000000 0000000 agronholm-sqlacodegen-1b321d1/.github/ 0000775 0000000 0000000 00000000000 15215155472 0017651 5 ustar 00root root 0000000 0000000 agronholm-sqlacodegen-1b321d1/.github/FUNDING.yml 0000664 0000000 0000000 00000000033 15215155472 0021462 0 ustar 00root root 0000000 0000000 tidelift: pypi/sqlacodegen
agronholm-sqlacodegen-1b321d1/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15215155472 0022034 5 ustar 00root root 0000000 0000000 agronholm-sqlacodegen-1b321d1/.github/ISSUE_TEMPLATE/bug_report.yaml 0000664 0000000 0000000 00000003457 15215155472 0025101 0 ustar 00root root 0000000 0000000 name: Bug Report
description: File a bug report
labels: ["bug"]
body:
- type: markdown
attributes:
value: >
If you observed a crash in the project, or saw unexpected behavior in it, report
your findings here.
- type: checkboxes
attributes:
label: Things to check first
options:
- label: >
I have searched the existing issues and didn't find my bug already reported
there
required: true
- label: >
I have checked that my bug is still present in the latest release
required: true
- type: input
id: project-version
attributes:
label: Sqlacodegen version
description: What version of Sqlacodegen were you running?
validations:
required: true
- type: input
id: sqlalchemy-version
attributes:
label: SQLAlchemy version
description: What version of SQLAlchemy were you running?
validations:
required: true
- type: dropdown
id: rdbms
attributes:
label: RDBMS vendor
description: >
What RDBMS (relational database management system) did you run the tool against?
options:
- PostgreSQL
- MySQL (or compatible)
- SQLite
- MSSQL
- Oracle
- DB2
- Other
- N/A
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: >
Unless you are reporting a crash, tell us what you expected to happen instead.
validations:
required: true
- type: textarea
id: schema
attributes:
label: Database schema for reproducing the bug
description: >
If applicable, paste the database schema (as a series of `CREATE TABLE` and
other SQL commands) here.
agronholm-sqlacodegen-1b321d1/.github/ISSUE_TEMPLATE/config.yml 0000664 0000000 0000000 00000000034 15215155472 0024021 0 ustar 00root root 0000000 0000000 blank_issues_enabled: false
agronholm-sqlacodegen-1b321d1/.github/ISSUE_TEMPLATE/features_request.yaml 0000664 0000000 0000000 00000002034 15215155472 0026305 0 ustar 00root root 0000000 0000000 name: Feature request
description: Suggest a new feature
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: >
If you have thought of a new feature that would increase the usefulness of this
project, please use this form to send us your idea.
- type: checkboxes
attributes:
label: Things to check first
options:
- label: >
I have searched the existing issues and didn't find my feature already
requested there
required: true
- type: textarea
id: feature
attributes:
label: Feature description
description: >
Describe the feature in detail. The more specific the description you can give,
the easier it should be to implement this feature.
validations:
required: true
- type: textarea
id: usecase
attributes:
label: Use case
description: >
Explain why you need this feature, and why you think it would be useful to
others too.
validations:
required: true
agronholm-sqlacodegen-1b321d1/.github/dependabot.yml 0000664 0000000 0000000 00000001106 15215155472 0022477 0 ustar 00root root 0000000 0000000 # Keep GitHub Actions up to date with GitHub's Dependabot...
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
version: 2
updates:
- package-ecosystem: github-actions
directory: /
groups:
github-actions:
patterns:
- "*" # Group all Actions updates into a single larger pull request
schedule:
interval: quarterly
agronholm-sqlacodegen-1b321d1/.github/pull_request_template.md 0000664 0000000 0000000 00000002010 15215155472 0024603 0 ustar 00root root 0000000 0000000
## Changes
Fixes #.
## Checklist
If this is a user-facing code change, like a bugfix or a new feature, please ensure that
you've fulfilled the following conditions (where applicable):
- [ ] You've added tests (in `tests/`) which would fail without your patch
- [ ] You've added a new changelog entry (in `CHANGES.rst`).
If this is a trivial change, like a typo fix or a code reformatting, then you can ignore
these instructions.
### Updating the changelog
If there are no entries after the last release, use `**UNRELEASED**` as the version.
If, say, your patch fixes issue #123, the entry should look like this:
```
- Fix big bad boo-boo in task groups
(`#123 `_; PR by @yourgithubaccount)
```
If there's no issue linked, just link to your pull request instead by updating the
changelog after you've created the PR.
agronholm-sqlacodegen-1b321d1/.github/workflows/ 0000775 0000000 0000000 00000000000 15215155472 0021706 5 ustar 00root root 0000000 0000000 agronholm-sqlacodegen-1b321d1/.github/workflows/publish.yml 0000664 0000000 0000000 00000002661 15215155472 0024104 0 ustar 00root root 0000000 0000000 name: Publish packages to PyPI
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+.post[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+[a-b][0-9]+"
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
jobs:
build:
name: Build the source tarball and the wheel
runs-on: ubuntu-latest
environment: release
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Install dependencies
run: pip install build
- name: Create packages
run: python -m build
- name: Archive packages
uses: actions/upload-artifact@v7
with:
name: dist
path: dist
publish:
name: Publish build artifacts to the PyPI
needs: build
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write
steps:
- name: Retrieve packages
uses: actions/download-artifact@v8
with:
name: dist
path: dist
- name: Upload packages
uses: pypa/gh-action-pypi-publish@release/v1
release:
name: Create a GitHub release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- id: changelog
uses: agronholm/release-notes@v1
with:
path: CHANGES.rst
- uses: ncipollo/release-action@v1
with:
body: ${{ steps.changelog.outputs.changelog }}
agronholm-sqlacodegen-1b321d1/.github/workflows/test.yml 0000664 0000000 0000000 00000001755 15215155472 0023420 0 ustar 00root root 0000000 0000000 name: test suite
on:
push:
branches: [master]
pull_request:
jobs:
test:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: pip
cache-dependency-path: pyproject.toml
- name: Install dependencies
run: pip install --group test -e .[sqlmodel,citext,geoalchemy2,pgvector]
- name: Test with pytest
run: coverage run -m pytest
- name: Upload Coverage
uses: coverallsapp/github-action@v2
with:
parallel: true
coveralls:
name: Finish Coveralls
needs: test
runs-on: ubuntu-latest
steps:
- name: Finished
uses: coverallsapp/github-action@v2
with:
parallel-finished: true
agronholm-sqlacodegen-1b321d1/.gitignore 0000664 0000000 0000000 00000000225 15215155472 0020300 0 ustar 00root root 0000000 0000000 *.egg-info
*.pyc
.project
.pydevproject
.coverage
.settings
.tox
.idea
.vscode
.cache
.pytest_cache
.mypy_cache
dist
build
venv*
docker-compose.yaml
agronholm-sqlacodegen-1b321d1/.pre-commit-config.yaml 0000664 0000000 0000000 00000002057 15215155472 0022576 0 ustar 00root root 0000000 0000000 # This is the configuration file for pre-commit (https://pre-commit.com/).
# To use:
# * Install pre-commit (https://pre-commit.com/#installation)
# * Copy this file as ".pre-commit-config.yaml"
# * Run "pre-commit install".
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-toml
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
- id: mixed-line-ending
args: [ "--fix=lf" ]
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.9
hooks:
- id: ruff
args: [--fix, --show-fixes]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.20.0
hooks:
- id: mypy
additional_dependencies:
- pytest
- "SQLAlchemy >= 2.0.29"
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- id: rst-backticks
- id: rst-directive-colons
- id: rst-inline-touching-normal
ci:
autoupdate_schedule: quarterly
agronholm-sqlacodegen-1b321d1/CHANGES.rst 0000664 0000000 0000000 00000033332 15215155472 0020117 0 ustar 00root root 0000000 0000000 Version history
===============
**4.0.4**
- Added autoincrement to primary key columns to prevent missing field errors.
(`#473 `_; PR by @jtmonroe)
- Preserve dialect-specific ``ARRAY`` types (e.g. ``postgresql.ARRAY``) instead
of adapting them to the generic ``sqlalchemy.ARRAY``. The generic type does
not implement operators like ``.contains()``, so adapting silently broke
PostgreSQL array queries on generated models.
(`#441 `_)
**4.0.3**
- Improved rendering of ``Identity`` server defaults by explicitly rendering
non-default parameters; ``Decimal`` values (as returned by some databases) are
now cast to ``int`` (PR by @NotCarlosSerrano)
**4.0.2**
- Fixed rendering of inherited keyword arguments for dialect-specific types that use
``**kwargs`` in their initializers (such as MySQL ``CHAR`` with ``collation``) while
preserving existing ``*args`` rendering behavior (PR by @hyoj0942)
- Fixed missing metadata argument when rendering plain tables with the SQLModel
- Added support for self-referential tables in the SQLModel generator (PR by @sheinbergon)
- Fixed empty dialect kwargs (e.g. ``postgresql_include=[]``) being included in
rendered indexes, tables, and columns (PR by @sheinbergon)
**4.0.1**
- Fix enum column definitions to explicitly include schema and name if reflected
via SQLAlchemy's Metadata (pr by @sheinbergon)
**4.0.0**
- **BACKWARD INCOMPATIBLE** API changes (for those who customize code generation by
subclassing the existing generators):
* Added new optional keyword argument, ``explicit_foreign_keys`` to
``DeclarativeGenerator``, to force foreign keys to be rendered as
``ClassName.attribute_name`` string references
* Removed the ``render_relationship_args()`` method from the SQLModel generator
* Added two new methods for customizing relationship rendering in
``DeclarativeGenerator``:
* ``render_relationship_annotation()``: returns the appropriate type annotation
(without the ``Mapped`` wrapper) for the relationship
* ``render_relationship_arguments()``: returns a dictionary of keyword arguments to
``sqlalchemy.orm.relationship()``
**4.0.0rc3**
- **BACKWARD INCOMPATIBLE** Relationship names changed when multiple FKs or junction tables
connect to the same target table. Regenerating models will break existing code.
- Added support for generating Python enum classes for ``ARRAY(Enum(...))`` columns
(e.g., PostgreSQL ``ARRAY(ENUM)``). Supports named/unnamed enums, shared enums across
columns, and multi-dimensional arrays. Respects ``--options nonativeenums``.
(PR by @sheinbergon)
- Improved relationship naming: one-to-many uses FK column names (e.g.,
``simple_items_parent_container``), many-to-many uses junction table names (e.g.,
``students_enrollments``). Use ``--options nofknames`` to revert to old behavior. (PR by @sheinbergon)
- Fixed ``Index`` kwargs (e.g. ``mysql_length``) being ignored during code generation
(PR by @luliangce)
- Fixed the SQLModel generator not adding the ``foreign_keys`` parameters when
generating multiple relationships between the same two tables
**4.0.0rc2**
- Add ``values_callable`` lambda to generated native enums column definitions.
This allows for proper enum value insertion when working with ORM models (PR by @sheinbergon)
**4.0.0rc1**
- **BACKWARD INCOMPATIBLE** ``TablesGenerator.render_column_type()`` was changed to
receive the ``Column`` object instead of the column type object as its sole argument
- Added Python enum generation for native database ENUM types (e.g., PostgreSQL / MySQL ENUM).
Retained synthetic Python enum generation from CHECK constraints with
IN clauses (e.g., ``column IN ('val1', 'val2', ...)``). Use ``--options nonativeenums`` to
disable enum generation for native database enums. Use ``--options nosyntheticenums`` to
disable enum generation for synthetic database enums (VARCHAR columns with check constraints).
(PR by @sheinbergon)
**3.2.0**
- Dropped support for Python 3.9
- Fix Postgres ``DOMAIN`` adaptation regression introduced in SQLAlchemy 2.0.42 (PR by @sheinbergon)
- Support disabling special naming logic for single column many-to-one and one-to-one relationships
(PR by @Henkhogan, revised by @sheinbergon)
- Add ``include_dialect_options`` option to render ``Table`` and ``Column``
dialect-specific kwargs and ``info`` in generated code. (PR by @jaogoy)
- Add ``keep_dialect_types`` option to preserve dialect-specific column types instead of
adapting to generic SQLAlchemy types. (PR by @jaogoy)
**3.1.1**
- Fallback ``NotImplemented`` errors encountered when accessing ``python_type`` for
non-native types to ``typing.Any``
(PR by @sheinbergon, based on work by @danplischke)
**3.1.0**
- Type annotations for ARRAY column attributes now include the Python type of
the array elements
- Added support for specifying engine arguments via ``--engine-arg``
(PR by @LajosCseppento)
- Fixed incorrect package name used in ``importlib.metadata.version`` for
``sqlalchemy-citext``, resolving ``PackageNotFoundError`` (PR by @oaimtiaz)
- Prevent double pluralization (PR by @dkratzert)
- Fixes DOMAIN extending JSON/JSONB data types (PR by @sheinbergon)
- Temporarily restrict SQLAlchemy version to 2.0.41 (PR by @sheinbergon)
- Fixes ``add_import`` behavior when adding imports from sqlalchemy and overall better
alignment of import behavior(s) across generators
- Fixes ``nullable`` column behavior for non-null columns for both
``sqlmodels`` and ``declarative`` generators (PR by @sheinbergon)
**3.0.0**
- Dropped support for Python 3.8
- Changed nullable relationships to include ``Optional`` in their type annotations
- Fixed SQLModel code generation
- Fixed two rendering issues in ``ENUM`` columns when a non-default schema is used: an
unwarranted positional argument and missing the ``schema`` argument
- Fixed ``AttributeError`` when metadata contains user defined column types
- Fixed ``AssertionError`` when metadata contains a column type that is a type decorator
with an all-uppercase name
- Fixed MySQL ``DOUBLE`` column types being rendered with the wrong arguments
**3.0.0rc5**
- Fixed pgvector support not working
**3.0.0rc4**
- Dropped support for Python 3.7
- Dropped support for SQLAlchemy 1.x
- Added support for the ``pgvector`` extension (with help from KellyRousselHoomano)
**3.0.0rc3**
- Added support for SQLAlchemy 2 (PR by rbuffat with help from mhauru)
- Renamed ``--option`` to ``--options`` and made its values delimited by commas
- Restored CIText and GeoAlchemy2 support (PR by stavvy-rotte)
**3.0.0rc2**
- Added support for generating SQLModel classes (PR by Andrii Khirilov)
- Fixed code generation when a single-column index is unique or does not match the
dialect's naming convention (PR by Leonardus Chen)
- Fixed another problem where sequence schemas were not properly separated from the
sequence name
- Fixed invalid generated primary/secondaryjoin expressions in self-referential
many-to-many relationships by using lambdas instead of strings
- Fixed ``AttributeError`` when the declarative generator encounters a table name
already in singular form when ``--option use_inflect`` is enabled
- Increased minimum SQLAlchemy version to 1.4.36 to address issues with ``ForeignKey``
and indexes, and to eliminate the PostgreSQL UUID column type annotation hack
**3.0.0rc1**
- Migrated all packaging/testing configuration to ``pyproject.toml``
- Fixed unwarranted ``ForeignKey`` declarations appearing in column attributes when
there are named, single column foreign key constraints (PR by Leonardus Chen)
. Fixed ``KeyError`` when rendering an index without any columns
- Fixed improper handling of schema prefixes in sequence names in server defaults
- Fixed identically named tables from different schemas resulting in invalid generated
code
- Fixed imports caused by ``server_default`` conflicting with class attribute names
- Worked around PostgreSQL UUID columns getting ``Any`` as the type annotation
**3.0.0b3**
- Dropped support for Python < 3.7
- Dropped support for SQLAlchemy 1.3
- Added a ``__main__`` module which can be used as an alternate entry point to the CLI
- Added detection for sequence use in column defaults on PostgreSQL
- Fixed ``sqlalchemy.exc.InvalidRequestError`` when encountering a column named
"metadata" (regression from 2.0)
- Fixed missing ``MetaData`` import with ``DeclarativeGenerator`` when only plain tables
are generated
- Fixed invalid data classes being generated due to some relationships having been
rendered without a default value
- Improved translation of column names into column attributes where the column name has
whitespace at the beginning or end
- Modified constraint and index rendering to add them explicitly instead of using
shortcuts like ``unique=True``, ``index=True`` or ``primary=True`` when the constraint
or index has a name that does not match the default naming convention
**3.0.0b2**
- Fixed ``IDENTITY`` columns not rendering properly when they are part of the primary
key
**3.0.0b1**
**NOTE**: Both the API and the command line interface have been refactored in a
backwards incompatible fashion. Notably several command line options have been moved to
specific generators and are no longer visible from ``sqlacodegen --help``. Their
replacement are documented in the README.
- Dropped support for Python < 3.6
- Added support for Python 3.10
- Added support for SQLAlchemy 1.4
- Added support for bidirectional relationships (use ``--option nobidi``) to disable
- Added support for multiple schemas via ``--schemas``
- Added support for ``IDENTITY`` columns
- Disabled inflection during table/relationship name generation by default
(use ``--option use_inflect`` to re-enable)
- Refactored the old ``CodeGenerator`` class into separate generator classes, selectable
via ``--generator``
- Refactored several command line options into generator specific options:
- ``--noindexes`` → ``--option noindexes``
- ``--noconstraints`` → ``--option noconstraints``
- ``--nocomments`` → ``--option nocomments``
- ``--nojoined`` → ``--option nojoined`` (``declarative`` and ``dataclass`` generators
only)
- ``--noinflect`` → (now the default; use ``--option use_inflect`` instead)
(``declarative`` and ``dataclass`` generators only)
- Fixed missing import for ``JSONB`` ``astext_type`` argument
- Fixed generated column or relationship names colliding with imports or each other
- Fixed ``CompileError`` when encountering server defaults that contain colons (``:``)
**2.3.0**
- Added support for rendering computed columns
- Fixed ``--nocomments`` not taking effect (fix proposed by AzuresYang)
- Fixed handling of MySQL ``SET`` column types (and possibly others as well)
**2.2.0**
- Added support for rendering table comments (PR by David Hirschfeld)
- Fixed bad identifier names being generated for plain tables (PR by softwarepk)
**2.1.0**
- Dropped support for Python 3.4
- Dropped support for SQLAlchemy 0.8
- Added support for Python 3.7 and 3.8
- Added support for SQLAlchemy 1.3
- Added support for column comments (requires SQLAlchemy 1.2+; based on PR by koalas8)
- Fixed crash on unknown column types (``NullType``)
**2.0.1**
- Don't adapt dialect specific column types if they need special constructor arguments
(thanks Nicholas Martin for the PR)
**2.0.0**
- Refactored code for better reuse
- Dropped support for Python 2.6, 3.2 and 3.3
- Dropped support for SQLAlchemy < 0.8
- Worked around a bug regarding Enum on SQLAlchemy 1.2+ (``name`` was missing)
- Added support for Geoalchemy2
- Fixed invalid class names being generated (fixes #60; PR by Dan O'Huiginn)
- Fixed array item types not being adapted or imported
(fixes #46; thanks to Martin Glauer and Shawn Koschik for help)
- Fixed attribute name of columns named ``metadata`` in mapped classes (fixes #62)
- Fixed rendered column types being changed from the original (fixes #11)
- Fixed server defaults which contain double quotes (fixes #7, #17, #28, #33, #36)
- Fixed ``secondary=`` not taking into account the association table's schema name
(fixes #30)
- Sort models by foreign key dependencies instead of schema and name (fixes #15, #16)
**1.1.6**
- Fixed compatibility with SQLAlchemy 1.0
- Added an option to only generate tables
**1.1.5**
- Fixed potential assignment of columns or relationships into invalid attribute names
(fixes #10)
- Fixed unique=True missing from unique Index declarations
- Fixed several issues with server defaults
- Fixed potential assignment of columns or relationships into invalid attribute names
- Allowed pascal case for tables already using it
- Switched from Mercurial to Git
**1.1.4**
- Fixed compatibility with SQLAlchemy 0.9.0
**1.1.3**
- Fixed compatibility with SQLAlchemy 0.8.3+
- Migrated tests from nose to pytest
**1.1.2**
- Fixed non-default schema name not being present in __table_args__ (fixes #2)
- Fixed self referential foreign key causing column type to not be rendered
- Fixed missing "deferrable" and "initially" keyword arguments in ForeignKey constructs
- Fixed foreign key and check constraint handling with alternate schemas (fixes #3)
**1.1.1**
- Fixed TypeError when inflect could not determine the singular name of a table for a
many-to-1 relationship
- Fixed _IntegerType, _StringType etc. being rendered instead of proper types on MySQL
**1.1.0**
- Added automatic detection of joined-table inheritance
- Fixed missing class name prefix in primary/secondary joins in relationships
- Instead of wildcard imports, generate explicit imports dynamically (fixes #1)
- Use the inflect library to produce better guesses for table to class name conversion
- Automatically detect Boolean columns based on CheckConstraints
- Skip redundant CheckConstraints for Enum and Boolean columns
**1.0.0**
- Initial release
agronholm-sqlacodegen-1b321d1/CONTRIBUTING.rst 0000664 0000000 0000000 00000004604 15215155472 0020756 0 ustar 00root root 0000000 0000000 Contributing to sqlacodegen
===========================
If you wish to contribute a fix or feature to sqlacodegen, please follow the following
guidelines.
When you make a pull request against the main sqlacodegen codebase, Github runs the
sqlacodegen test suite against your modified code. Before making a pull request, you
should ensure that the modified code passes tests locally. To that end, the use of tox_
is recommended. The default tox run first runs ``pre-commit`` and then the actual test
suite. To run the checks on all environments in parallel, invoke tox with ``tox -p``.
To build the documentation, run ``tox -e docs`` which will generate a directory named
``build`` in which you may view the formatted HTML documentation.
sqlacodegen uses pre-commit_ to perform several code style/quality checks. It is
recommended to activate pre-commit_ on your local clone of the repository (using
``pre-commit install``) to ensure that your changes will pass the same checks on GitHub.
.. _tox: https://tox.readthedocs.io/en/latest/install.html
.. _pre-commit: https://pre-commit.com/#installation
Making a pull request on Github
-------------------------------
To get your changes merged to the main codebase, you need a Github account.
#. Fork the repository (if you don't have your own fork of it yet) by navigating to the
`main sqlacodegen repository`_ and clicking on "Fork" near the top right corner.
#. Clone the forked repository to your local machine with
``git clone git@github.com/yourusername/sqlacodegen``.
#. Create a branch for your pull request, like ``git checkout -b myfixname``
#. Make the desired changes to the code base.
#. Commit your changes locally. If your changes close an existing issue, add the text
``Fixes #XXX.`` or ``Closes #XXX.`` to the commit message (where XXX is the issue
number).
#. Push the changeset(s) to your forked repository (``git push``)
#. Navigate to Pull requests page on the original repository (not your fork) and click
"New pull request"
#. Click on the text "compare across forks".
#. Select your own fork as the head repository and then select the correct branch name.
#. Click on "Create pull request".
If you have trouble, consult the `pull request making guide`_ on opensource.com.
.. _main sqlacodegen repository: https://github.com/agronholm/sqlacodegen
.. _pull request making guide: https://opensource.com/article/19/7/create-pull-request-github
agronholm-sqlacodegen-1b321d1/LICENSE 0000664 0000000 0000000 00000002152 15215155472 0017316 0 ustar 00root root 0000000 0000000 This is the MIT license: http://www.opensource.org/licenses/mit-license.php
Copyright (c) Alex Grönholm
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or
substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
agronholm-sqlacodegen-1b321d1/README.rst 0000664 0000000 0000000 00000023533 15215155472 0020006 0 ustar 00root root 0000000 0000000 .. image:: https://github.com/agronholm/sqlacodegen/actions/workflows/test.yml/badge.svg
:target: https://github.com/agronholm/sqlacodegen/actions/workflows/test.yml
:alt: Build Status
.. image:: https://coveralls.io/repos/github/agronholm/sqlacodegen/badge.svg?branch=master
:target: https://coveralls.io/github/agronholm/sqlacodegen?branch=master
:alt: Code Coverage
.. image:: https://tidelift.com/badges/package/pypi/sqlacodegen
:target: https://tidelift.com/subscription/pkg/pypi-sqlacodegen
:alt: Tidelift
This is a tool that reads the structure of an existing database and generates the
appropriate SQLAlchemy model code, using the declarative style if possible.
This tool was written as a replacement for `sqlautocode`_, which was suffering from
several issues (including, but not limited to, incompatibility with Python 3 and the
latest SQLAlchemy version).
.. _sqlautocode: http://code.google.com/p/sqlautocode/
Features
========
* Supports SQLAlchemy 2.x
* Produces declarative code that almost looks like it was hand written
* Produces `PEP 8`_ compliant code
* Accurately determines relationships, including many-to-many, one-to-one
* Automatically detects joined table inheritance
* Excellent test coverage
.. _PEP 8: http://www.python.org/dev/peps/pep-0008/
Installation
============
To install, do::
pip install sqlacodegen
To include support for the PostgreSQL ``CITEXT`` extension type (which should be
considered as tested only under a few environments) specify the ``citext`` extra::
pip install sqlacodegen[citext]
To include support for the PostgreSQL ``GEOMETRY``, ``GEOGRAPHY``, and ``RASTER`` types
(which should be considered as tested only under a few environments) specify the
``geoalchemy2`` extra:
To include support for the PostgreSQL ``PGVECTOR`` extension type, specify the
``pgvector`` extra::
pip install sqlacodegen[pgvector]
.. code-block:: bash
pip install sqlacodegen[geoalchemy2]
Quickstart
==========
At the minimum, you have to give sqlacodegen a database URL. The URL is passed directly
to SQLAlchemy's `create_engine()`_ method so please refer to
`SQLAlchemy's documentation`_ for instructions on how to construct a proper URL.
Examples::
sqlacodegen postgresql:///some_local_db
sqlacodegen --generator tables mysql+pymysql://user:password@localhost/dbname
sqlacodegen --generator dataclasses sqlite:///database.db
# --engine-arg values are parsed with ast.literal_eval
sqlacodegen oracle+oracledb://user:pass@127.0.0.1:1521/XE --engine-arg thick_mode=True
sqlacodegen oracle+oracledb://user:pass@127.0.0.1:1521/XE --engine-arg thick_mode=True --engine-arg connect_args='{"user": "user", "dsn": "..."}'
To see the list of generic options::
sqlacodegen --help
.. _create_engine(): http://docs.sqlalchemy.org/en/latest/core/engines.html#sqlalchemy.create_engine
.. _SQLAlchemy's documentation: http://docs.sqlalchemy.org/en/latest/core/engines.html
Available generators
====================
The selection of a generator determines the
The following built-in generators are available:
* ``tables`` (only generates ``Table`` objects, for those who don't want to use the ORM)
* ``declarative`` (the default; generates classes inheriting from ``declarative_base()``
* ``dataclasses`` (generates dataclass-based models; v1.4+ only)
* ``sqlmodels`` (generates model classes for SQLModel_)
.. _SQLModel: https://sqlmodel.tiangolo.com/
Generator-specific options
==========================
The following options can be turned on by passing them using ``--options`` (multiple
values must be delimited by commas, e.g. ``--options noconstraints,nobidi``):
* ``tables``
* ``noconstraints``: ignore constraints (foreign key, unique etc.)
* ``nocomments``: ignore table/column comments
* ``noindexes``: ignore indexes
* ``nonativeenums``: don't generate Python enum classes for native database ENUM types (e.g., PostgreSQL ENUM); use plain string mapping instead
* ``nosyntheticenums``: don't generate Python enum classes from CHECK constraints with IN clauses (e.g., ``column IN ('value1', 'value2', ...)``); preserves CHECK constraints as-is
* ``noidsuffix``: prevent the special naming logic for single column many-to-one
and one-to-one relationships (see `Relationship naming logic`_ for details)
* ``include_dialect_options``: render a table' dialect options, such as ``starrocks_partition`` for StarRocks' specific options.
* ``keep_dialect_types``: preserve dialect-specific column types instead of adapting to generic SQLAlchemy types.
* ``declarative``
* all the options from ``tables``
* ``use_inflect``: use the ``inflect`` library when naming classes and relationships
(turning plural names into singular; see below for details)
* ``nojoined``: don't try to detect joined-class inheritance (see below for details)
* ``nobidi``: generate relationships in a unidirectional fashion, so only the
many-to-one or first side of many-to-many relationships gets a relationship
attribute, as on v2.X
* ``nofknames``: disable improved relationship naming when multiple FKs or
junction tables connect to the same target. By default, uses FK column names
for one-to-many (e.g., ``simple_items_parent_container``) and junction table
names for many-to-many (e.g., ``students_enrollments``). Reverts to
underscore suffixes (``simple_items_``, ``student_``).
* ``dataclasses``
* all the options from ``declarative``
* ``sqlmodels``
* all the options from ``declarative``
Model class generators
----------------------
The code generators that generate classes try to generate model classes whenever
possible. There are two circumstances in which a ``Table`` is generated instead:
* the table has no primary key constraint (which is required by SQLAlchemy for every
model class)
* the table is an association table between two other tables (see below for the
specifics)
Model class naming logic
++++++++++++++++++++++++
By default, table names are converted to valid PEP 8 compliant class names by replacing
all characters unsuitable for Python identifiers with ``_``. Then, each valid parts
(separated by underscores) are title cased and then joined together, eliminating the
underscores. So, ``example_name`` becomes ``ExampleName``.
If the ``use_inflect`` option is used, the table name (which is assumed to be in
English) is converted to singular form using the "inflect" library. For example,
``sales_invoices`` becomes ``SalesInvoice``. Since table names are not always in
English, and the inflection process is far from perfect, inflection is disabled by
default.
Relationship detection logic
++++++++++++++++++++++++++++
Relationships are detected based on existing foreign key constraints as follows:
* **many-to-one**: a foreign key constraint exists on the table
* **one-to-one**: same as **many-to-one**, but a unique constraint exists on the
column(s) involved
* **many-to-many**: (not implemented on the ``sqlmodel`` generator) an association table
is found to exist between two tables
A table is considered an association table if it satisfies all of the following
conditions:
#. has exactly two foreign key constraints
#. all its columns are involved in said constraints
Relationship naming logic
+++++++++++++++++++++++++
Relationships are typically named based on the table name of the opposite class.
For example, if a class has a relationship to another class with the table named
``companies``, the relationship would be named ``companies`` (unless the ``use_inflect``
option was enabled, in which case it would be named ``company`` in the case of a
many-to-one or one-to-one relationship).
A special case for single column many-to-one and one-to-one relationships, however, is
if the column is named like ``employer_id``. Then the relationship is named ``employer``
due to that ``_id`` suffix.
For self referential relationships, the reverse side of the relationship will be named
with the ``_reverse`` suffix appended to it.
When multiple foreign keys or junction tables connect to the same target table,
relationships use qualifiers for disambiguation. One-to-many relationships use FK
column names (e.g., ``simple_items_parent_container``, ``simple_items_top_container``).
Many-to-many relationships use junction table names (e.g., ``students_enrollments``,
``students_waitlist``), except for self-referential cases which use FK column names
(e.g., ``parent``, ``child``). The ``nofknames`` option reverts to underscore suffixes
(``simple_items_``, ``student_``).
Customizing code generation logic
=================================
If the built-in generators with all their options don't quite do what you want, you can
customize the logic by subclassing one of the existing code generator classes. Override
whichever methods you need, and then add an `entry point`_ in the
``sqlacodegen.generators`` namespace that points to your new class. Once the entry point
is in place (you typically have to install the project with ``pip install``), you can
use ``--generator `` to invoke your custom code generator.
For examples, you can look at sqlacodegen's own entry points in its `pyproject.toml`_.
.. _entry point: https://setuptools.readthedocs.io/en/latest/userguide/entry_point.html
.. _pyproject.toml: https://github.com/agronholm/sqlacodegen/blob/master/pyproject.toml
Getting help
============
If you have problems or other questions, you should start a discussion on the
`sqlacodegen discussion forum`_. As an alternative, you could also try your luck on the
sqlalchemy_ room on Gitter.
.. _sqlacodegen discussion forum: https://github.com/agronholm/sqlacodegen/discussions/categories/q-a
.. _sqlalchemy: https://app.gitter.im/#/room/#sqlalchemy_community:gitter.im
Security contact information
============================
To report a security vulnerability, please use the `Tidelift security contact`_.
Tidelift will coordinate the fix and disclosure.
.. _Tidelift security contact: https://tidelift.com/security
agronholm-sqlacodegen-1b321d1/pyproject.toml 0000664 0000000 0000000 00000005356 15215155472 0021236 0 ustar 00root root 0000000 0000000 [build-system]
requires = [
"setuptools >= 64",
"setuptools_scm[toml] >= 6.4"
]
build-backend = "setuptools.build_meta"
[project]
name = "sqlacodegen"
description = "Automatic model code generator for SQLAlchemy"
readme = "README.rst"
authors = [{name = "Alex Grönholm", email = "alex.gronholm@nextday.fi"}]
maintainers = [{name = "Idan Sheinberg", email = "ishinberg0@gmail.com"}]
keywords = ["sqlalchemy"]
license = "MIT"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Environment :: Console",
"Topic :: Database",
"Topic :: Software Development :: Code Generators",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
requires-python = ">=3.10"
dependencies = [
"SQLAlchemy >= 2.0.29",
"inflect >= 4.0.0",
]
dynamic = ["version"]
[project.urls]
"Bug Tracker" = "https://github.com/agronholm/sqlacodegen/issues"
"Source Code" = "https://github.com/agronholm/sqlacodegen"
[project.optional-dependencies]
sqlmodel = ["sqlmodel >= 0.0.22"]
citext = ["sqlalchemy-citext >= 1.7.0"]
geoalchemy2 = ["geoalchemy2 >= 0.17.0"]
pgvector = ["pgvector >= 0.2.4"]
[project.entry-points."sqlacodegen.generators"]
tables = "sqlacodegen.generators:TablesGenerator"
declarative = "sqlacodegen.generators:DeclarativeGenerator"
dataclasses = "sqlacodegen.generators:DataclassGenerator"
sqlmodels = "sqlacodegen.generators:SQLModelGenerator"
[project.scripts]
sqlacodegen = "sqlacodegen.cli:main"
[dependency-groups]
test = [
"pytest >= 7.4",
"coverage >= 7",
"psycopg[binary]",
"mysql-connector-python",
]
[tool.setuptools_scm]
version_scheme = "post-release"
local_scheme = "dirty-tag"
[tool.ruff]
src = ["src"]
[tool.ruff.lint]
extend-select = [
"I", # isort
"ISC", # flake8-implicit-str-concat
"PGH", # pygrep-hooks
"RUF100", # unused noqa (yesqa)
"UP", # pyupgrade
"W", # pycodestyle warnings
]
[tool.mypy]
strict = true
disable_error_code = "no-untyped-call"
[tool.pytest.ini_options]
addopts = "-rsfE --tb=short"
testpaths = ["tests"]
[coverage.run]
source = ["sqlacodegen"]
relative_files = true
[coverage.report]
show_missing = true
[tool.tox]
env_list = ["py310", "py311", "py312", "py313", "py314"]
skip_missing_interpreters = true
[tool.tox.env_run_base]
package = "editable"
commands = [["python", "-m", "pytest", { replace = "posargs", extend = true }]]
dependency_groups = ["test"]
extras = ["sqlmodel", "citext", "geoalchemy2", "pgvector"]
agronholm-sqlacodegen-1b321d1/src/ 0000775 0000000 0000000 00000000000 15215155472 0017100 5 ustar 00root root 0000000 0000000 agronholm-sqlacodegen-1b321d1/src/sqlacodegen/ 0000775 0000000 0000000 00000000000 15215155472 0021365 5 ustar 00root root 0000000 0000000 agronholm-sqlacodegen-1b321d1/src/sqlacodegen/__init__.py 0000664 0000000 0000000 00000000000 15215155472 0023464 0 ustar 00root root 0000000 0000000 agronholm-sqlacodegen-1b321d1/src/sqlacodegen/__main__.py 0000664 0000000 0000000 00000000036 15215155472 0023456 0 ustar 00root root 0000000 0000000 from .cli import main
main()
agronholm-sqlacodegen-1b321d1/src/sqlacodegen/cli.py 0000664 0000000 0000000 00000010376 15215155472 0022515 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import argparse
import ast
import sys
from contextlib import ExitStack
from importlib.metadata import entry_points, version
from typing import Any, TextIO
from sqlalchemy.engine import create_engine
from sqlalchemy.schema import MetaData
try:
import citext
except ImportError:
citext = None
try:
import geoalchemy2
except ImportError:
geoalchemy2 = None
try:
import pgvector.sqlalchemy
except ImportError:
pgvector = None
def _parse_engine_arg(arg_str: str) -> tuple[str, Any]:
if "=" not in arg_str:
raise argparse.ArgumentTypeError("engine-arg must be in key=value format")
key, value = arg_str.split("=", 1)
try:
value = ast.literal_eval(value)
except Exception:
pass # Leave as string if literal_eval fails
return key, value
def _parse_engine_args(arg_list: list[str]) -> dict[str, Any]:
result = {}
for arg in arg_list or []:
key, value = _parse_engine_arg(arg)
result[key] = value
return result
def main() -> None:
generators = {ep.name: ep for ep in entry_points(group="sqlacodegen.generators")}
parser = argparse.ArgumentParser(
description="Generates SQLAlchemy model code from an existing database."
)
parser.add_argument("url", nargs="?", help="SQLAlchemy url to the database")
parser.add_argument(
"--options", help="options (comma-delimited) passed to the generator class"
)
parser.add_argument(
"--version", action="store_true", help="print the version number and exit"
)
parser.add_argument(
"--schemas", help="load tables from the given schemas (comma-delimited)"
)
parser.add_argument(
"--generator",
choices=generators,
default="declarative",
help="generator class to use",
)
parser.add_argument(
"--tables", help="tables to process (comma-delimited, default: all)"
)
parser.add_argument(
"--noviews",
action="store_true",
help="ignore views (always true for sqlmodels generator)",
)
parser.add_argument(
"--engine-arg",
action="append",
help=(
"engine arguments in key=value format, e.g., "
'--engine-arg=connect_args=\'{"user": "scott"}\' '
"--engine-arg thick_mode=true or "
'--engine-arg thick_mode=\'{"lib_dir": "/path"}\' '
"(values are parsed with ast.literal_eval)"
),
)
parser.add_argument("--outfile", help="file to write output to (default: stdout)")
args = parser.parse_args()
if args.version:
print(version("sqlacodegen"))
return
if not args.url:
print("You must supply a url\n", file=sys.stderr)
parser.print_help()
return
if citext:
print(f"Using sqlalchemy-citext {version('sqlalchemy-citext')}")
if geoalchemy2:
print(f"Using geoalchemy2 {version('geoalchemy2')}")
if pgvector:
print(f"Using pgvector {version('pgvector')}")
# Use reflection to fill in the metadata
engine_args = _parse_engine_args(args.engine_arg)
engine = create_engine(args.url, **engine_args)
metadata = MetaData()
tables = args.tables.split(",") if args.tables else None
schemas = args.schemas.split(",") if args.schemas else [None]
options = set(args.options.split(",")) if args.options else set()
# Instantiate the generator
generator_class = generators[args.generator].load()
generator = generator_class(metadata, engine, options)
if not generator.views_supported:
name = generator_class.__name__
print(
f"VIEW models will not be generated when using the '{name}' generator",
file=sys.stderr,
)
for schema in schemas:
metadata.reflect(
engine, schema, (generator.views_supported and not args.noviews), tables
)
# Open the target file (if given)
with ExitStack() as stack:
outfile: TextIO
if args.outfile:
outfile = open(args.outfile, "w", encoding="utf-8")
stack.enter_context(outfile)
else:
outfile = sys.stdout
# Write the generated model code to the specified file or standard output
outfile.write(generator.generate())
agronholm-sqlacodegen-1b321d1/src/sqlacodegen/generators.py 0000664 0000000 0000000 00000234550 15215155472 0024121 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import inspect
import re
import sys
from abc import ABCMeta, abstractmethod
from collections import defaultdict
from collections.abc import Collection, Iterable, Mapping, Sequence
from dataclasses import dataclass
from decimal import Decimal
from importlib import import_module
from inspect import Parameter
from itertools import count
from keyword import iskeyword
from pprint import pformat
from textwrap import indent
from typing import Any, ClassVar, Literal, cast
import inflect
import sqlalchemy
from sqlalchemy import (
ARRAY,
Boolean,
CheckConstraint,
Column,
Computed,
Constraint,
DefaultClause,
Enum,
ForeignKey,
ForeignKeyConstraint,
Identity,
Index,
MetaData,
PrimaryKeyConstraint,
String,
Table,
Text,
TypeDecorator,
UniqueConstraint,
)
from sqlalchemy.dialects.postgresql import DOMAIN, JSON, JSONB
from sqlalchemy.engine import Connection, Engine
from sqlalchemy.exc import CompileError
from sqlalchemy.sql.elements import TextClause
from sqlalchemy.sql.type_api import UserDefinedType
from sqlalchemy.types import TypeEngine
from .models import (
ColumnAttribute,
JoinType,
Model,
ModelClass,
RelationshipAttribute,
RelationshipType,
)
from .utils import (
decode_postgresql_sequence,
get_column_names,
get_common_fk_constraints,
get_compiled_expression,
get_constraint_sort_key,
get_stdlib_module_names,
qualified_table_name,
render_callable,
uses_default_name,
)
_re_boolean_check_constraint = re.compile(r"(?:.*?\.)?(.*?) IN \(0, 1\)")
_re_column_name = re.compile(r'(?:(["`]?).*\1\.)?(["`]?)(.*)\2')
_re_enum_check_constraint = re.compile(r"(?:.*?\.)?(.*?) IN \((.+)\)")
_re_enum_item = re.compile(r"'(.*?)(? bool:
pass
@abstractmethod
def generate(self) -> str:
"""
Generate the code for the given metadata.
.. note:: May modify the metadata.
"""
@dataclass(eq=False)
class TablesGenerator(CodeGenerator):
valid_options: ClassVar[set[str]] = {
"noindexes",
"noconstraints",
"nocomments",
"nonativeenums",
"nosyntheticenums",
"include_dialect_options",
"keep_dialect_types",
}
stdlib_module_names: ClassVar[set[str]] = get_stdlib_module_names()
def __init__(
self,
metadata: MetaData,
bind: Connection | Engine,
options: Sequence[str],
*,
indentation: str = " ",
):
super().__init__(metadata, bind, options)
self.indentation: str = indentation
self.imports: dict[str, set[str]] = defaultdict(set)
self.module_imports: set[str] = set()
# Render SchemaItem.info and dialect kwargs (Table/Column) into output
self.include_dialect_options_and_info: bool = (
"include_dialect_options" in self.options
)
# Keep dialect-specific types instead of adapting to generic SQLAlchemy types
self.keep_dialect_types: bool = "keep_dialect_types" in self.options
# Track Python enum classes: maps (table_name, column_name) -> enum_class_name
self.enum_classes: dict[tuple[str, str], str] = {}
# Track enum values: maps enum_class_name -> list of values
self.enum_values: dict[str, list[str]] = {}
@property
def views_supported(self) -> bool:
return True
def generate_base(self) -> None:
self.base = Base(
literal_imports=[LiteralImport("sqlalchemy", "MetaData")],
declarations=["metadata = MetaData()"],
metadata_ref="metadata",
)
def generate(self) -> str:
self.generate_base()
sections: list[str] = []
# Remove unwanted elements from the metadata
for table in list(self.metadata.tables.values()):
if self.should_ignore_table(table):
self.metadata.remove(table)
continue
if "noindexes" in self.options:
table.indexes.clear()
if "noconstraints" in self.options:
table.constraints.clear()
if "nocomments" in self.options:
table.comment = None
for column in table.columns:
if "nocomments" in self.options:
column.comment = None
# Use information from column constraints to figure out the intended column
# types
for table in self.metadata.tables.values():
self.fix_column_types(table)
# Generate the models
models: list[Model] = self.generate_models()
# Render module level variables
if variables := self.render_module_variables(models):
sections.append(variables + "\n")
# Render enum classes
if enum_classes := self.render_enum_classes():
sections.append(enum_classes + "\n")
# Render models
if rendered_models := self.render_models(models):
sections.append(rendered_models)
# Render collected imports
groups = self.group_imports()
if imports := "\n\n".join(
"\n".join(line for line in group) for group in groups
):
sections.insert(0, imports)
return "\n\n".join(sections) + "\n"
def collect_imports(self, models: Iterable[Model]) -> None:
for literal_import in self.base.literal_imports:
self.add_literal_import(literal_import.pkgname, literal_import.name)
for model in models:
self.collect_imports_for_model(model)
def collect_imports_for_model(self, model: Model) -> None:
if model.__class__ is Model:
self.add_import(Table)
for column in model.table.c:
self.collect_imports_for_column(column)
for constraint in model.table.constraints:
self.collect_imports_for_constraint(constraint)
for index in model.table.indexes:
self.collect_imports_for_constraint(index)
def collect_imports_for_column(self, column: Column[Any]) -> None:
self.add_import(column.type)
if isinstance(column.type, ARRAY):
self.add_import(column.type.item_type.__class__)
elif isinstance(column.type, (JSONB, JSON)):
if (
not isinstance(column.type.astext_type, Text)
or column.type.astext_type.length is not None
):
self.add_import(column.type.astext_type)
elif isinstance(column.type, DOMAIN):
self.add_import(column.type.data_type.__class__)
if column.default:
self.add_import(column.default)
if column.server_default:
if isinstance(column.server_default, (Computed, Identity)):
self.add_import(column.server_default)
elif isinstance(column.server_default, DefaultClause):
self.add_literal_import("sqlalchemy", "text")
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):
self.add_literal_import("sqlalchemy", "Index")
elif isinstance(constraint, PrimaryKeyConstraint):
if not uses_default_name(constraint):
self.add_literal_import("sqlalchemy", "PrimaryKeyConstraint")
elif isinstance(constraint, UniqueConstraint):
if len(constraint.columns) > 1 or not uses_default_name(constraint):
self.add_literal_import("sqlalchemy", "UniqueConstraint")
elif isinstance(constraint, ForeignKeyConstraint):
if len(constraint.columns) > 1 or not uses_default_name(constraint):
self.add_literal_import("sqlalchemy", "ForeignKeyConstraint")
else:
self.add_import(ForeignKey)
else:
self.add_import(constraint)
def add_import(self, obj: Any) -> None:
# Don't store builtin imports
if getattr(obj, "__module__", "builtins") == "builtins":
return
type_ = type(obj) if not isinstance(obj, type) else obj
pkgname = type_.__module__
# The column types have already been adapted towards generic types if possible,
# so if this is still a vendor specific type (e.g., MySQL INTEGER) be sure to
# use that rather than the generic sqlalchemy type as it might have different
# constructor parameters.
if pkgname.startswith("sqlalchemy.dialects."):
dialect_pkgname = ".".join(pkgname.split(".")[0:3])
dialect_pkg = import_module(dialect_pkgname)
if type_.__name__ in dialect_pkg.__all__:
pkgname = dialect_pkgname
elif type_ is getattr(sqlalchemy, type_.__name__, None):
pkgname = "sqlalchemy"
else:
pkgname = type_.__module__
self.add_literal_import(pkgname, type_.__name__)
def add_literal_import(self, pkgname: str, name: str) -> None:
names = self.imports.setdefault(pkgname, set())
names.add(name)
def remove_literal_import(self, pkgname: str, name: str) -> None:
names = self.imports.setdefault(pkgname, set())
if name in names:
names.remove(name)
def add_module_import(self, pgkname: str) -> None:
self.module_imports.add(pgkname)
def group_imports(self) -> list[list[str]]:
future_imports: list[str] = []
stdlib_imports: list[str] = []
thirdparty_imports: list[str] = []
def get_collection(package: str) -> list[str]:
collection = thirdparty_imports
if package == "__future__":
collection = future_imports
elif package in self.stdlib_module_names:
collection = stdlib_imports
elif package in sys.modules:
if "site-packages" not in (sys.modules[package].__file__ or ""):
collection = stdlib_imports
return collection
for package in sorted(self.imports):
imports = ", ".join(sorted(self.imports[package]))
collection = get_collection(package)
collection.append(f"from {package} import {imports}")
for module in sorted(self.module_imports):
collection = get_collection(module)
collection.append(f"import {module}")
return [
group
for group in (future_imports, stdlib_imports, thirdparty_imports)
if group
]
def generate_models(self) -> list[Model]:
models = [Model(table) for table in self.metadata.sorted_tables]
# Collect the imports
self.collect_imports(models)
# Generate names for models
global_names = {
name for namespace in self.imports.values() for name in namespace
}
for model in models:
self.generate_model_name(model, global_names)
global_names.add(model.name)
return models
def generate_model_name(self, model: Model, global_names: set[str]) -> None:
preferred_name = f"t_{model.table.name}"
model.name = self.find_free_name(preferred_name, global_names)
def render_module_variables(self, models: list[Model]) -> str:
declarations = self.base.declarations
if any(not isinstance(model, ModelClass) for model in models):
if self.base.table_metadata_declaration is not None:
declarations.append(self.base.table_metadata_declaration)
return "\n".join(declarations)
def render_models(self, models: list[Model]) -> str:
rendered: list[str] = []
for model in models:
rendered_table = self.render_table(model.table)
rendered.append(f"{model.name} = {rendered_table}")
return "\n\n".join(rendered)
def render_table(self, table: Table) -> str:
args: list[str] = [f"{table.name!r}, {self.base.metadata_ref}"]
kwargs: dict[str, object] = {}
for column in table.columns:
# Cast is required because of a bug in the SQLAlchemy stubs regarding
# Table.columns
args.append(self.render_column(column, True, is_table=True))
for constraint in sorted(table.constraints, key=get_constraint_sort_key):
if uses_default_name(constraint):
if isinstance(constraint, PrimaryKeyConstraint):
continue
elif isinstance(constraint, (ForeignKeyConstraint, UniqueConstraint)):
if len(constraint.columns) == 1:
continue
args.append(self.render_constraint(constraint))
for index in sorted(table.indexes, key=lambda i: cast(str, i.name)):
# One-column indexes should be rendered as index=True on columns
if len(index.columns) > 1 or not uses_default_name(index):
args.append(self.render_index(index))
if table.schema:
kwargs["schema"] = repr(table.schema)
table_comment = getattr(table, "comment", None)
if table_comment:
kwargs["comment"] = repr(table.comment)
# add info + dialect kwargs for callable context (opt-in)
if self.include_dialect_options_and_info:
self._add_dialect_kwargs_and_info(table, kwargs, values_for_dict=False)
return render_callable("Table", *args, kwargs=kwargs, indentation=" ")
def render_index(self, index: Index) -> str:
extra_args = [repr(col.name) for col in index.columns]
kwargs = {
key: repr(value) if isinstance(value, str) else value
for key, value in sorted(index.kwargs.items(), key=lambda item: item[0])
if value not in ([], {})
}
if index.unique:
kwargs["unique"] = True
return render_callable("Index", repr(index.name), *extra_args, kwargs=kwargs)
# TODO find better solution for is_table
def render_column(
self, column: Column[Any], show_name: bool, is_table: bool = False
) -> str:
args = []
kwargs: dict[str, Any] = {}
kwarg = []
is_part_of_composite_pk = (
column.primary_key and len(column.table.primary_key) > 1
)
dedicated_fks = [
c
for c in column.foreign_keys
if c.constraint
and len(c.constraint.columns) == 1
and uses_default_name(c.constraint)
]
is_unique = any(
isinstance(c, UniqueConstraint)
and set(c.columns) == {column}
and uses_default_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)
for i in column.table.indexes
)
is_primary = (
any(
isinstance(c, PrimaryKeyConstraint)
and column.name in c.columns
and uses_default_name(c)
for c in column.table.constraints
)
or column.primary_key
)
is_autoincrement = (
isinstance(column.autoincrement, bool) and column.autoincrement
)
has_index = any(
set(i.columns) == {column} and uses_default_name(i)
for i in column.table.indexes
)
if show_name:
args.append(repr(column.name))
# Render the column type if there are no foreign keys on it or any of them
# points back to itself
if not dedicated_fks or any(fk.column is column for fk in dedicated_fks):
args.append(self.render_column_type(column))
for fk in dedicated_fks:
args.append(self.render_constraint(fk))
if column.default:
args.append(repr(column.default))
if column.key != column.name:
kwargs["key"] = column.key
if is_primary:
kwargs["primary_key"] = True
if is_autoincrement and is_primary:
kwargs["autoincrement"] = True
if not column.nullable and not column.primary_key:
kwargs["nullable"] = False
if column.nullable and is_part_of_composite_pk:
kwargs["nullable"] = True
if is_unique:
column.unique = True
kwargs["unique"] = True
if has_index:
column.index = True
kwarg.append("index")
kwargs["index"] = True
if isinstance(column.server_default, DefaultClause):
kwargs["server_default"] = render_callable(
"text", repr(cast(TextClause, column.server_default.arg).text)
)
elif isinstance(column.server_default, Computed):
expression = str(column.server_default.sqltext)
computed_kwargs = {}
if column.server_default.persisted is not None:
computed_kwargs["persisted"] = column.server_default.persisted
args.append(
render_callable("Computed", repr(expression), kwargs=computed_kwargs)
)
elif isinstance(column.server_default, Identity):
identity = column.server_default
identity_kwargs: dict[str, Any] = {}
for name, param in inspect.signature(Identity).parameters.items():
if name == "self" or param.kind in (
Parameter.VAR_POSITIONAL,
Parameter.VAR_KEYWORD,
):
continue
value = getattr(identity, name, None)
if value is None:
continue
if isinstance(value, Decimal):
value = int(value)
if param.default is not Parameter.empty and value == param.default:
continue
identity_kwargs[name] = value
args.append(render_callable("Identity", kwargs=identity_kwargs))
elif column.server_default:
kwargs["server_default"] = repr(column.server_default)
comment = getattr(column, "comment", None)
if comment:
kwargs["comment"] = repr(comment)
# add column info + dialect kwargs for callable context (opt-in)
if self.include_dialect_options_and_info:
self._add_dialect_kwargs_and_info(column, kwargs, values_for_dict=False)
return self.render_column_callable(is_table, *args, **kwargs)
def render_column_callable(self, is_table: bool, *args: Any, **kwargs: Any) -> str:
if is_table:
self.add_import(Column)
return render_callable("Column", *args, kwargs=kwargs)
else:
return render_callable("mapped_column", *args, kwargs=kwargs)
def _render_column_type_value(self, value: Any) -> str:
if isinstance(value, (JSONB, JSON)):
# Remove astext_type if it's the default
if isinstance(value.astext_type, Text) and value.astext_type.length is None:
value.astext_type = None # type: ignore[assignment]
else:
self.add_import(Text)
if isinstance(value, TextClause):
self.add_literal_import("sqlalchemy", "text")
return render_callable("text", repr(value.text))
return repr(value)
def _collect_inherited_init_kwargs(
self,
column_type: Any,
init_sig: inspect.Signature,
seen_param_names: set[str],
missing: object,
) -> dict[str, str]:
has_var_keyword = any(
param.kind is Parameter.VAR_KEYWORD
for param in init_sig.parameters.values()
)
has_var_positional = any(
param.kind is Parameter.VAR_POSITIONAL
for param in init_sig.parameters.values()
)
if not has_var_keyword or has_var_positional:
return {}
inherited_kwargs: dict[str, str] = {}
for supercls in column_type.__class__.__mro__[1:]:
if supercls is object:
break
try:
super_sig = inspect.signature(supercls.__init__)
except (TypeError, ValueError):
continue
for super_param in list(super_sig.parameters.values())[1:]:
if super_param.name.startswith("_"):
continue
if super_param.kind in (
Parameter.POSITIONAL_ONLY,
Parameter.VAR_POSITIONAL,
Parameter.VAR_KEYWORD,
):
continue
if super_param.name in seen_param_names:
continue
seen_param_names.add(super_param.name)
value = getattr(column_type, super_param.name, missing)
if value is missing:
continue
default = super_param.default
if default is not Parameter.empty and value == default:
continue
inherited_kwargs[super_param.name] = self._render_column_type_value(
value
)
return inherited_kwargs
def render_column_type(self, column: Column[Any]) -> str:
column_type = column.type
# Check if this is an enum column with a Python enum class
if isinstance(column_type, Enum) and column is not None:
if enum_class_name := self.enum_classes.get(
(column.table.name, column.name)
):
# Import SQLAlchemy Enum (will be handled in collect_imports)
self.add_import(Enum)
extra_kwargs = ""
if column_type.name is not None:
extra_kwargs += f", name={column_type.name!r}"
if column_type.schema is not None:
extra_kwargs += f", schema={column_type.schema!r}"
return f"Enum({enum_class_name}, values_callable=lambda cls: [member.value for member in cls]{extra_kwargs})"
args = []
kwargs: dict[str, Any] = {}
# Check if this is an ARRAY column with an Enum item type mapped to a Python enum class
if isinstance(column_type, ARRAY) and isinstance(column_type.item_type, Enum):
if enum_class_name := self.enum_classes.get(
(column.table.name, column.name)
):
self.add_import(ARRAY)
self.add_import(Enum)
extra_kwargs = ""
if column_type.item_type.name is not None:
extra_kwargs += f", name={column_type.item_type.name!r}"
if column_type.item_type.schema is not None:
extra_kwargs += f", schema={column_type.item_type.schema!r}"
rendered_enum = f"Enum({enum_class_name}, values_callable=lambda cls: [member.value for member in cls]{extra_kwargs})"
if column_type.dimensions is not None:
kwargs["dimensions"] = repr(column_type.dimensions)
return render_callable("ARRAY", rendered_enum, kwargs=kwargs)
sig = inspect.signature(column_type.__class__.__init__)
defaults = {param.name: param.default for param in sig.parameters.values()}
missing = object()
use_kwargs = False
seen_param_names: set[str] = set()
for param in list(sig.parameters.values())[1:]:
# Remove annoyances like _warn_on_bytestring
if param.name.startswith("_"):
continue
elif param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
use_kwargs = True
continue
seen_param_names.add(param.name)
value = getattr(column_type, param.name, missing)
default = defaults.get(param.name, missing)
if value is missing or value == default:
use_kwargs = True
continue
rendered_value = self._render_column_type_value(value)
if use_kwargs:
kwargs[param.name] = rendered_value
else:
args.append(rendered_value)
kwargs.update(
self._collect_inherited_init_kwargs(
column_type, sig, seen_param_names, missing
)
)
vararg = next(
(
param.name
for param in sig.parameters.values()
if param.kind is Parameter.VAR_POSITIONAL
),
None,
)
if vararg and hasattr(column_type, vararg):
varargs_repr = [repr(arg) for arg in getattr(column_type, vararg)]
args.extend(varargs_repr)
# These arguments cannot be autodetected from the Enum initializer
if isinstance(column_type, Enum):
for colname in "name", "schema":
if (value := getattr(column_type, colname)) is not None:
kwargs[colname] = repr(value)
if isinstance(column_type, (JSONB, JSON)):
# Remove astext_type if it's the default
if (
isinstance(column_type.astext_type, Text)
and column_type.astext_type.length is None
):
del kwargs["astext_type"]
if args or kwargs:
return render_callable(column_type.__class__.__name__, *args, kwargs=kwargs)
else:
return column_type.__class__.__name__
def render_constraint(self, constraint: Constraint | ForeignKey) -> str:
def add_fk_options(*opts: Any) -> None:
args.extend(repr(opt) for opt in opts)
for attr in "ondelete", "onupdate", "deferrable", "initially", "match":
value = getattr(constraint, attr, None)
if value:
kwargs[attr] = repr(value)
args: list[str] = []
kwargs: dict[str, Any] = {}
if isinstance(constraint, ForeignKey):
remote_column = (
f"{constraint.column.table.fullname}.{constraint.column.name}"
)
add_fk_options(remote_column)
elif isinstance(constraint, ForeignKeyConstraint):
local_columns = get_column_names(constraint)
remote_columns = [
f"{fk.column.table.fullname}.{fk.column.name}"
for fk in constraint.elements
]
add_fk_options(local_columns, remote_columns)
elif isinstance(constraint, CheckConstraint):
args.append(repr(get_compiled_expression(constraint.sqltext, self.bind)))
elif isinstance(constraint, (UniqueConstraint, PrimaryKeyConstraint)):
args.extend(repr(col.name) for col in constraint.columns)
else:
raise TypeError(
f"Cannot render constraint of type {constraint.__class__.__name__}"
)
if isinstance(constraint, Constraint) and not uses_default_name(constraint):
kwargs["name"] = repr(constraint.name)
return render_callable(constraint.__class__.__name__, *args, kwargs=kwargs)
def _add_dialect_kwargs_and_info(
self, obj: Any, target_kwargs: dict[str, object], *, values_for_dict: bool
) -> None:
"""
Merge SchemaItem-like object's .info and .dialect_kwargs into target_kwargs.
- values_for_dict=True: keep raw values so pretty-printer emits repr() (for __table_args__ dict)
- values_for_dict=False: set values to repr() strings (for callable kwargs)
"""
info_dict = getattr(obj, "info", None)
if info_dict:
target_kwargs["info"] = info_dict if values_for_dict else repr(info_dict)
dialect_keys: list[str]
try:
dialect_keys = sorted(getattr(obj, "dialect_kwargs"))
except Exception:
return
dialect_kwargs = getattr(obj, "dialect_kwargs", {})
for key in dialect_keys:
try:
value = dialect_kwargs[key]
except Exception:
continue
if isinstance(value, list | dict) and not value:
continue
# Render values:
# - callable context (values_for_dict=False): produce a string expression.
# primitives use repr(value); custom objects stringify then repr().
# - dict context (values_for_dict=True): pass raw primitives / str;
# custom objects become str(value) so pformat quotes them.
if values_for_dict:
if isinstance(value, type(None) | bool | int | float):
target_kwargs[key] = value
elif isinstance(value, str | dict | list):
target_kwargs[key] = value
else:
target_kwargs[key] = str(value)
else:
if isinstance(
value, type(None) | bool | int | float | str | dict | list
):
target_kwargs[key] = repr(value)
else:
target_kwargs[key] = repr(str(value))
def should_ignore_table(self, table: Table) -> bool:
# Support for Alembic and sqlalchemy-migrate -- never expose the schema version
# tables
return table.name in ("alembic_version", "migrate_version")
def find_free_name(
self, name: str, global_names: set[str], local_names: Collection[str] = ()
) -> str:
"""
Generate an attribute name that does not clash with other local or global names.
"""
name = name.strip()
assert name, "Identifier cannot be empty"
name = _re_invalid_identifier.sub("_", name)
if name[0].isdigit():
name = "_" + name
elif iskeyword(name) or name == "metadata":
name += "_"
original = name
for i in count():
if name not in global_names and name not in local_names:
break
name = original + (str(i) if i else "_")
return name
def _enum_name_to_class_name(self, enum_name: str) -> str:
"""Convert a database enum name to a Python class name (PascalCase)."""
return "".join(part.capitalize() for part in enum_name.split("_") if part)
def _create_enum_class(
self, table_name: str, column_name: str, values: list[str]
) -> str:
"""
Create a Python enum class name and register it.
Returns the enum class name to use in generated code.
"""
# Generate enum class name from table and column names
# Convert to PascalCase: user_status -> UserStatus
base_name = "".join(
part.capitalize()
for part in table_name.split("_") + column_name.split("_")
if part
)
# Ensure uniqueness
enum_class_name = base_name
for counter in count(1):
if enum_class_name not in self.enum_values:
break
# Check if it's the same enum (same values)
if self.enum_values[enum_class_name] == values:
# Reuse existing enum class
return enum_class_name
enum_class_name = f"{base_name}{counter}"
# Register the new enum class
self.enum_values[enum_class_name] = values
return enum_class_name
def render_enum_classes(self) -> str:
"""Render Python enum class definitions."""
if not self.enum_values:
return ""
self.add_module_import("enum")
enum_defs = []
for enum_class_name, values in sorted(self.enum_values.items()):
# Create enum members with valid Python identifiers
members = []
for value in values:
# Unescape SQL escape sequences (e.g., \' -> ')
# The value from the CHECK constraint has SQL escaping
unescaped_value = value.replace("\\'", "'").replace("\\\\", "\\")
# Create a valid identifier from the enum value
member_name = _re_invalid_identifier.sub("_", unescaped_value).upper()
if not member_name:
member_name = "EMPTY"
elif member_name[0].isdigit():
member_name = "_" + member_name
elif iskeyword(member_name):
member_name += "_"
#
# # Re-escape for Python string literal
# python_escaped = unescaped_value.replace("\\", "\\\\").replace(
# "'", "\\'"
# )
members.append(f" {member_name} = {unescaped_value!r}")
enum_def = f"class {enum_class_name}(str, enum.Enum):\n" + "\n".join(
members
)
enum_defs.append(enum_def)
return "\n\n\n".join(enum_defs)
def fix_column_types(self, table: Table) -> None:
"""Adjust the reflected column types."""
def fix_enum_column(col_name: str, enum_type: Enum) -> None:
if (table.name, col_name) in self.enum_classes:
return
if enum_type.name:
existing_class = None
for (_, _), cls in self.enum_classes.items():
if cls == self._enum_name_to_class_name(enum_type.name):
existing_class = cls
break
if existing_class:
enum_class_name = existing_class
else:
enum_class_name = self._enum_name_to_class_name(enum_type.name)
if enum_class_name not in self.enum_values:
self.enum_values[enum_class_name] = list(enum_type.enums)
else:
enum_class_name = self._create_enum_class(
table.name, col_name, list(enum_type.enums)
)
self.enum_classes[(table.name, col_name)] = enum_class_name
# Detect check constraints for boolean and enum columns
for constraint in table.constraints.copy():
if isinstance(constraint, CheckConstraint):
sqltext = get_compiled_expression(constraint.sqltext, self.bind)
# Turn any integer-like column with a CheckConstraint like
# "column IN (0, 1)" into a Boolean
if match := _re_boolean_check_constraint.match(sqltext):
if colname_match := _re_column_name.match(match.group(1)):
colname = colname_match.group(3)
table.constraints.remove(constraint)
table.c[colname].type = Boolean()
continue
# Turn VARCHAR columns with CHECK constraints like "column IN ('a', 'b')"
# into synthetic Enum types with Python enum classes
if (
"nosyntheticenums" not in self.options
and (match := _re_enum_check_constraint.match(sqltext))
and (colname_match := _re_column_name.match(match.group(1)))
):
colname = colname_match.group(3)
items = match.group(2)
if isinstance(table.c[colname].type, String) and not isinstance(
table.c[colname].type, Enum
):
options = _re_enum_item.findall(items)
# Create Python enum class
enum_class_name = self._create_enum_class(
table.name, colname, options
)
self.enum_classes[(table.name, colname)] = enum_class_name
# Convert to Enum type but KEEP the constraint
table.c[colname].type = Enum(*options, native_enum=False)
continue
for column in table.c:
# Handle native database Enum types (e.g., PostgreSQL ENUM)
if (
"nonativeenums" not in self.options
and isinstance(column.type, Enum)
and column.type.enums
):
fix_enum_column(column.name, column.type)
# Handle ARRAY columns with Enum item types (e.g., PostgreSQL ARRAY(ENUM))
elif (
"nonativeenums" not in self.options
and isinstance(column.type, ARRAY)
and isinstance(column.type.item_type, Enum)
and column.type.item_type.enums
):
fix_enum_column(column.name, column.type.item_type)
if not self.keep_dialect_types:
try:
column.type = self.get_adapted_type(column.type)
except CompileError:
continue
# PostgreSQL specific fix: detect sequences from server_default
if column.server_default and self.bind.dialect.name == "postgresql":
if isinstance(column.server_default, DefaultClause) and isinstance(
column.server_default.arg, TextClause
):
schema, seqname = decode_postgresql_sequence(
column.server_default.arg
)
if seqname:
# Add an explicit sequence
if seqname != f"{column.table.name}_{column.name}_seq":
column.default = sqlalchemy.Sequence(seqname, schema=schema)
column.server_default = None
def get_adapted_type(self, coltype: Any) -> Any:
# Keep dialect-specific ARRAY subclasses; the generic sqlalchemy.ARRAY
# is missing operators like .contains() (GH-441).
if isinstance(coltype, ARRAY) and type(coltype) is not ARRAY:
coltype.item_type = self.get_adapted_type(coltype.item_type)
return coltype
compiled_type = coltype.compile(self.bind.engine.dialect)
for supercls in coltype.__class__.__mro__:
if not supercls.__name__.startswith("_") and hasattr(
supercls, "__visit_name__"
):
# Don't try to adapt UserDefinedType as it's not a proper column type
if supercls is UserDefinedType or issubclass(supercls, TypeDecorator):
return coltype
# Hack to fix adaptation of the Enum class which is broken since
# SQLAlchemy 1.2
kw = {}
if supercls is Enum:
kw["name"] = coltype.name
if coltype.schema:
kw["schema"] = coltype.schema
# Hack to fix Postgres DOMAIN type adaptation, broken as of SQLAlchemy 2.0.42
# For additional information - https://github.com/agronholm/sqlacodegen/issues/416#issuecomment-3417480599
if supercls is DOMAIN:
if coltype.default:
kw["default"] = coltype.default
if coltype.constraint_name is not None:
kw["constraint_name"] = coltype.constraint_name
if coltype.not_null:
kw["not_null"] = coltype.not_null
if coltype.check is not None:
kw["check"] = coltype.check
if coltype.create_type:
kw["create_type"] = coltype.create_type
try:
new_coltype = coltype.adapt(supercls)
except TypeError:
# If the adaptation fails, don't try again
break
for key, value in kw.items():
setattr(new_coltype, key, value)
if isinstance(coltype, ARRAY):
new_coltype.item_type = self.get_adapted_type(new_coltype.item_type)
try:
# If the adapted column type does not render the same as the
# original, don't substitute it
if new_coltype.compile(self.bind.engine.dialect) != compiled_type:
break
except CompileError:
# If the adapted column type can't be compiled, don't substitute it
break
# Stop on the first valid non-uppercase column type class
coltype = new_coltype
if supercls.__name__ != supercls.__name__.upper():
break
return coltype
class DeclarativeGenerator(TablesGenerator):
valid_options: ClassVar[set[str]] = TablesGenerator.valid_options | {
"use_inflect",
"nojoined",
"nobidi",
"noidsuffix",
"nofknames",
}
def __init__(
self,
metadata: MetaData,
bind: Connection | Engine,
options: Sequence[str],
*,
indentation: str = " ",
base_class_name: str = "Base",
explicit_foreign_keys: bool = False,
):
super().__init__(metadata, bind, options, indentation=indentation)
self.base_class_name: str = base_class_name
self.inflect_engine = inflect.engine()
self.explicit_foreign_keys = explicit_foreign_keys
def generate_base(self) -> None:
self.base = Base(
literal_imports=[LiteralImport("sqlalchemy.orm", "DeclarativeBase")],
declarations=[
f"class {self.base_class_name}(DeclarativeBase):",
f"{self.indentation}pass",
],
metadata_ref=f"{self.base_class_name}.metadata",
)
def collect_imports(self, models: Iterable[Model]) -> None:
super().collect_imports(models)
if any(isinstance(model, ModelClass) for model in models):
self.add_literal_import("sqlalchemy.orm", "Mapped")
self.add_literal_import("sqlalchemy.orm", "mapped_column")
def collect_imports_for_model(self, model: Model) -> None:
super().collect_imports_for_model(model)
if isinstance(model, ModelClass):
if model.relationships:
self.add_literal_import("sqlalchemy.orm", "relationship")
def generate_models(self) -> list[Model]:
models_by_table_name: dict[str, Model] = {}
# Pick association tables from the metadata into their own set, don't process
# them normally
links: defaultdict[str, list[Model]] = defaultdict(lambda: [])
for table in self.metadata.sorted_tables:
qualified_name = qualified_table_name(table)
# Link tables have exactly two foreign key constraints and all columns are
# involved in them
fk_constraints = sorted(
table.foreign_key_constraints, key=get_constraint_sort_key
)
if len(fk_constraints) == 2 and all(
col.foreign_keys for col in table.columns
):
model = models_by_table_name[qualified_name] = Model(table)
tablename = fk_constraints[0].elements[0].column.table.name
links[tablename].append(model)
continue
# Only form model classes for tables that have a primary key and are not
# association tables
if not table.primary_key:
models_by_table_name[qualified_name] = Model(table)
else:
model = ModelClass(table)
models_by_table_name[qualified_name] = model
# Fill in the columns
for column in table.c:
column_attr = ColumnAttribute(model, column)
model.columns.append(column_attr)
# Add relationships
for model in models_by_table_name.values():
if isinstance(model, ModelClass):
self.generate_relationships(
model, models_by_table_name, links[model.table.name]
)
# Nest inherited classes in their superclasses to ensure proper ordering
if "nojoined" not in self.options:
for model in list(models_by_table_name.values()):
if not isinstance(model, ModelClass):
continue
pk_column_names = {col.name for col in model.table.primary_key.columns}
for constraint in model.table.foreign_key_constraints:
if set(get_column_names(constraint)) == pk_column_names:
target = models_by_table_name[
qualified_table_name(constraint.elements[0].column.table)
]
if isinstance(target, ModelClass):
model.parent_class = target
target.children.append(model)
# Change base if we only have tables
if not any(
isinstance(model, ModelClass) for model in models_by_table_name.values()
):
super().generate_base()
# Collect the imports
self.collect_imports(models_by_table_name.values())
# Rename models and their attributes that conflict with imports or other
# attributes
global_names = {
name for namespace in self.imports.values() for name in namespace
}
for model in models_by_table_name.values():
self.generate_model_name(model, global_names)
global_names.add(model.name)
return list(models_by_table_name.values())
def generate_relationships(
self,
source: ModelClass,
models_by_table_name: dict[str, Model],
association_tables: list[Model],
) -> list[RelationshipAttribute]:
relationships: list[RelationshipAttribute] = []
reverse_relationship: RelationshipAttribute | None
# Add many-to-one (and one-to-many) relationships
pk_column_names = {col.name for col in source.table.primary_key.columns}
for constraint in sorted(
source.table.foreign_key_constraints, key=get_constraint_sort_key
):
target = models_by_table_name[
qualified_table_name(constraint.elements[0].column.table)
]
if isinstance(target, ModelClass):
if "nojoined" not in self.options:
if set(get_column_names(constraint)) == pk_column_names:
parent = models_by_table_name[
qualified_table_name(constraint.elements[0].column.table)
]
if isinstance(parent, ModelClass):
source.parent_class = parent
parent.children.append(source)
continue
# Add uselist=False to One-to-One relationships
column_names = get_column_names(constraint)
if any(
isinstance(c, (PrimaryKeyConstraint, UniqueConstraint))
and {col.name for col in c.columns} == set(column_names)
for c in constraint.table.constraints
):
r_type = RelationshipType.ONE_TO_ONE
else:
r_type = RelationshipType.MANY_TO_ONE
relationship = RelationshipAttribute(r_type, source, target, constraint)
source.relationships.append(relationship)
# For self referential relationships, remote_side needs to be set
if source is target:
relationship.remote_side = [
source.get_column_attribute(col.name)
for col in constraint.referred_table.primary_key
]
# If the two tables share more than one foreign key constraint,
# SQLAlchemy needs an explicit primaryjoin to figure out which column(s)
# it needs
common_fk_constraints = get_common_fk_constraints(
source.table, target.table
)
if len(common_fk_constraints) > 1:
relationship.foreign_keys = [
source.get_column_attribute(key)
for key in constraint.column_keys
]
# Generate the opposite end of the relationship in the target class
if "nobidi" not in self.options:
if r_type is RelationshipType.MANY_TO_ONE:
r_type = RelationshipType.ONE_TO_MANY
reverse_relationship = RelationshipAttribute(
r_type,
target,
source,
constraint,
foreign_keys=relationship.foreign_keys,
backref=relationship,
)
relationship.backref = reverse_relationship
target.relationships.append(reverse_relationship)
# For self referential relationships, remote_side needs to be set
if source is target:
reverse_relationship.remote_side = [
source.get_column_attribute(colname)
for colname in constraint.column_keys
]
# Add many-to-many relationships
for association_table in association_tables:
fk_constraints = sorted(
association_table.table.foreign_key_constraints,
key=get_constraint_sort_key,
)
target = models_by_table_name[
qualified_table_name(fk_constraints[1].elements[0].column.table)
]
if isinstance(target, ModelClass):
relationship = RelationshipAttribute(
RelationshipType.MANY_TO_MANY,
source,
target,
fk_constraints[1],
association_table,
)
source.relationships.append(relationship)
# Generate the opposite end of the relationship in the target class
reverse_relationship = None
if "nobidi" not in self.options:
reverse_relationship = RelationshipAttribute(
RelationshipType.MANY_TO_MANY,
target,
source,
fk_constraints[0],
association_table,
relationship,
)
relationship.backref = reverse_relationship
target.relationships.append(reverse_relationship)
# Add a primary/secondary join for self-referential many-to-many
# relationships
if source is target:
both_relationships = [relationship]
reverse_flags = [False, True]
if reverse_relationship:
both_relationships.append(reverse_relationship)
for relationship, reverse in zip(both_relationships, reverse_flags):
if (
not relationship.association_table
or not relationship.constraint
):
continue
constraints = sorted(
relationship.constraint.table.foreign_key_constraints,
key=get_constraint_sort_key,
reverse=reverse,
)
pri_pairs = zip(
get_column_names(constraints[0]), constraints[0].elements
)
sec_pairs = zip(
get_column_names(constraints[1]), constraints[1].elements
)
relationship.primaryjoin = [
(
relationship.source,
elem.column.name,
relationship.association_table,
col,
)
for col, elem in pri_pairs
]
relationship.secondaryjoin = [
(
relationship.target,
elem.column.name,
relationship.association_table,
col,
)
for col, elem in sec_pairs
]
return relationships
def generate_model_name(self, model: Model, global_names: set[str]) -> None:
if isinstance(model, ModelClass):
preferred_name = _re_invalid_identifier.sub("_", model.table.name)
preferred_name = "".join(
part[:1].upper() + part[1:] for part in preferred_name.split("_")
)
if "use_inflect" in self.options:
singular_name = self.inflect_engine.singular_noun(preferred_name)
if singular_name:
preferred_name = singular_name
model.name = self.find_free_name(preferred_name, global_names)
# Fill in the names for column attributes
local_names: set[str] = set()
for column_attr in model.columns:
self.generate_column_attr_name(column_attr, global_names, local_names)
local_names.add(column_attr.name)
# Fill in the names for relationship attributes
for relationship in model.relationships:
self.generate_relationship_name(relationship, global_names, local_names)
local_names.add(relationship.name)
else:
super().generate_model_name(model, global_names)
def generate_column_attr_name(
self,
column_attr: ColumnAttribute,
global_names: set[str],
local_names: set[str],
) -> None:
column_attr.name = self.find_free_name(
column_attr.column.name, global_names, local_names
)
def generate_relationship_name(
self,
relationship: RelationshipAttribute,
global_names: set[str],
local_names: set[str],
) -> None:
def strip_id_suffix(name: str) -> str:
# Strip _id only if at the end or followed by underscore (e.g., "course_id" -> "course", "course_id_1" -> "course_1")
# But don't strip from "parent_id1" (where id is followed by a digit without underscore)
return re.sub(r"_id(?=_|$)", "", name)
def get_m2m_qualified_name(default_name: str) -> str:
"""Generate qualified name for many-to-many relationship when multiple junction tables exist."""
# Check if there are multiple M2M relationships to the same target
target_m2m_relationships = [
r
for r in relationship.source.relationships
if r.target is relationship.target
and r.type == RelationshipType.MANY_TO_MANY
]
# Only use junction-based naming when there are multiple M2M to same target
if len(target_m2m_relationships) > 1:
if relationship.source is relationship.target:
# Self-referential: use FK column name from junction table
# (e.g., "parent_id" -> "parent", "child_id" -> "child")
if relationship.constraint:
column_names = [c.name for c in relationship.constraint.columns]
if len(column_names) == 1:
fk_qualifier = strip_id_suffix(column_names[0])
else:
fk_qualifier = "_".join(
strip_id_suffix(col_name) for col_name in column_names
)
return fk_qualifier
elif relationship.association_table:
# Normal: use junction table name as qualifier
junction_name = relationship.association_table.table.name
fk_qualifier = strip_id_suffix(junction_name)
return f"{relationship.target.table.name}_{fk_qualifier}"
else:
# Single M2M: use simple name from junction table FK column
# (e.g., "right_id" -> "right" instead of "right_table")
if relationship.constraint and "noidsuffix" not in self.options:
column_names = [c.name for c in relationship.constraint.columns]
if len(column_names) == 1:
stripped_name = strip_id_suffix(column_names[0])
if stripped_name != column_names[0]:
return stripped_name
return default_name
def get_fk_qualified_name(constraint: ForeignKeyConstraint) -> str:
"""Generate qualified name for one-to-many/one-to-one relationship using FK column names."""
column_names = [c.name for c in constraint.columns]
if len(column_names) == 1:
# Single column FK: strip _id suffix if present
fk_qualifier = strip_id_suffix(column_names[0])
else:
# Multi-column FK: concatenate all column names (strip _id from each)
fk_qualifier = "_".join(
strip_id_suffix(col_name) for col_name in column_names
)
# For self-referential relationships, don't prepend the table name
if relationship.source is relationship.target:
return fk_qualifier
else:
return f"{relationship.target.table.name}_{fk_qualifier}"
def resolve_preferred_name() -> str:
resolved_name = relationship.target.table.name
# For reverse relationships with multiple FKs to the same table, use the FK
# column name to create a more descriptive relationship name
# For M2M relationships with multiple junction tables, use the junction table name
use_fk_based_naming = "nofknames" not in self.options and (
(
relationship.constraint
and relationship.type
in (RelationshipType.ONE_TO_MANY, RelationshipType.ONE_TO_ONE)
and relationship.foreign_keys
)
or (
relationship.type == RelationshipType.MANY_TO_MANY
and relationship.association_table
)
)
if use_fk_based_naming:
if relationship.type == RelationshipType.MANY_TO_MANY:
resolved_name = get_m2m_qualified_name(resolved_name)
elif relationship.constraint:
resolved_name = get_fk_qualified_name(relationship.constraint)
# If there's a constraint with a single column that contains "_id", use the
# stripped version as the relationship name
elif relationship.constraint and "noidsuffix" not in self.options:
is_source = relationship.source.table is relationship.constraint.table
if is_source or relationship.type not in (
RelationshipType.ONE_TO_ONE,
RelationshipType.ONE_TO_MANY,
):
column_names = [c.name for c in relationship.constraint.columns]
if len(column_names) == 1:
stripped_name = strip_id_suffix(column_names[0])
# Only use the stripped name if it actually changed (had _id in it)
if stripped_name != column_names[0]:
resolved_name = stripped_name
else:
# For composite FKs, check if there are multiple FKs to the same target
target_relationships = [
r
for r in relationship.source.relationships
if r.target is relationship.target
and r.type == relationship.type
]
if len(target_relationships) > 1:
# Multiple FKs to same table - use concatenated column names
resolved_name = "_".join(
strip_id_suffix(col_name) for col_name in column_names
)
if "use_inflect" in self.options:
inflected_name: str | Literal[False]
if relationship.type in (
RelationshipType.ONE_TO_MANY,
RelationshipType.MANY_TO_MANY,
):
if not self.inflect_engine.singular_noun(resolved_name):
resolved_name = self.inflect_engine.plural_noun(resolved_name)
else:
inflected_name = self.inflect_engine.singular_noun(resolved_name)
if inflected_name:
resolved_name = inflected_name
return resolved_name
if (
relationship.type
in (RelationshipType.ONE_TO_MANY, RelationshipType.ONE_TO_ONE)
and relationship.source is relationship.target
and relationship.backref
and relationship.backref.name
):
preferred_name = relationship.backref.name + "_reverse"
else:
preferred_name = resolve_preferred_name()
relationship.name = self.find_free_name(
preferred_name, global_names, local_names
)
def render_models(self, models: list[Model]) -> str:
rendered: list[str] = []
for model in models:
if isinstance(model, ModelClass):
rendered.append(self.render_class(model))
else:
rendered.append(f"{model.name} = {self.render_table(model.table)}")
return "\n\n\n".join(rendered)
def render_class(self, model: ModelClass) -> str:
sections: list[str] = []
# Render class variables / special declarations
class_vars: str = self.render_class_variables(model)
if class_vars:
sections.append(class_vars)
# Render column attributes
rendered_column_attributes: list[str] = []
for nullable in (False, True):
for column_attr in model.columns:
if column_attr.column.nullable is nullable:
rendered_column_attributes.append(
self.render_column_attribute(column_attr)
)
if rendered_column_attributes:
sections.append("\n".join(rendered_column_attributes))
# Render relationship attributes
rendered_relationship_attributes: list[str] = [
self.render_relationship(relationship)
for relationship in model.relationships
]
if rendered_relationship_attributes:
sections.append("\n".join(rendered_relationship_attributes))
declaration = self.render_class_declaration(model)
rendered_sections = "\n\n".join(
indent(section, self.indentation) for section in sections
)
return f"{declaration}\n{rendered_sections}"
def render_class_declaration(self, model: ModelClass) -> str:
parent_class_name = (
model.parent_class.name if model.parent_class else self.base_class_name
)
return f"class {model.name}({parent_class_name}):"
def render_class_variables(self, model: ModelClass) -> str:
variables = [f"__tablename__ = {model.table.name!r}"]
# Render constraints and indexes as __table_args__
table_args = self.render_table_args(model.table)
if table_args:
variables.append(f"__table_args__ = {table_args}")
return "\n".join(variables)
def render_table_args(self, table: Table) -> str:
args: list[str] = []
kwargs: dict[str, object] = {}
# Render constraints
for constraint in sorted(table.constraints, key=get_constraint_sort_key):
if uses_default_name(constraint):
if isinstance(constraint, PrimaryKeyConstraint):
continue
if (
isinstance(constraint, (ForeignKeyConstraint, UniqueConstraint))
and len(constraint.columns) == 1
):
continue
args.append(self.render_constraint(constraint))
# Render indexes
for index in sorted(table.indexes, key=lambda i: cast(str, i.name)):
if len(index.columns) > 1 or not uses_default_name(index):
args.append(self.render_index(index))
if table.schema:
kwargs["schema"] = table.schema
if table.comment:
kwargs["comment"] = table.comment
# add info + dialect kwargs for dict context (__table_args__) (opt-in)
if self.include_dialect_options_and_info:
self._add_dialect_kwargs_and_info(table, kwargs, values_for_dict=True)
if kwargs:
formatted_kwargs = pformat(kwargs)
if not args:
return formatted_kwargs
else:
args.append(formatted_kwargs)
if args:
rendered_args = f",\n{self.indentation}".join(args)
if len(args) == 1:
rendered_args += ","
return f"(\n{self.indentation}{rendered_args}\n)"
else:
return ""
def render_column_python_type(self, column: Column[Any]) -> str:
def get_type_qualifiers() -> tuple[str, TypeEngine[Any], str]:
column_type = column.type
pre: list[str] = []
post_size = 0
if column.nullable:
self.add_literal_import("typing", "Optional")
pre.append("Optional[")
post_size += 1
if isinstance(column_type, ARRAY):
dim = getattr(column_type, "dimensions", None) or 1
pre.extend("list[" for _ in range(dim))
post_size += dim
column_type = column_type.item_type
return "".join(pre), column_type, "]" * post_size
def render_python_type(column_type: TypeEngine[Any]) -> str:
# Check if this is an enum column with a Python enum class
if isinstance(column_type, Enum):
table_name = column.table.name
column_name = column.name
if (table_name, column_name) in self.enum_classes:
enum_class_name = self.enum_classes[(table_name, column_name)]
return enum_class_name
if isinstance(column_type, DOMAIN):
column_type = column_type.data_type
try:
python_type = column_type.python_type
python_type_module = python_type.__module__
python_type_name = python_type.__name__
except NotImplementedError:
self.add_literal_import("typing", "Any")
return "Any"
if python_type_module == "builtins":
return python_type_name
self.add_module_import(python_type_module)
return f"{python_type_module}.{python_type_name}"
pre, col_type, post = get_type_qualifiers()
column_python_type = f"{pre}{render_python_type(col_type)}{post}"
return column_python_type
def render_column_attribute(self, column_attr: ColumnAttribute) -> str:
column = column_attr.column
rendered_column = self.render_column(column, column_attr.name != column.name)
rendered_column_python_type = self.render_column_python_type(column)
return f"{column_attr.name}: Mapped[{rendered_column_python_type}] = {rendered_column}"
def render_relationship(self, relationship: RelationshipAttribute) -> str:
kwargs = self.render_relationship_arguments(relationship)
annotation = self.render_relationship_annotation(relationship)
rendered_relationship = render_callable(
"relationship", repr(relationship.target.name), kwargs=kwargs
)
return f"{relationship.name}: Mapped[{annotation}] = {rendered_relationship}"
def render_relationship_annotation(
self, relationship: RelationshipAttribute
) -> str:
match relationship.type:
case RelationshipType.ONE_TO_MANY:
return f"list[{relationship.target.name!r}]"
case RelationshipType.ONE_TO_ONE | RelationshipType.MANY_TO_ONE:
if relationship.constraint and any(
col.nullable for col in relationship.constraint.columns
):
self.add_literal_import("typing", "Optional")
return f"Optional[{relationship.target.name!r}]"
else:
return f"'{relationship.target.name}'"
case RelationshipType.MANY_TO_MANY:
return f"list[{relationship.target.name!r}]"
def render_relationship_arguments(
self, relationship: RelationshipAttribute
) -> Mapping[str, Any]:
def render_column_attrs(column_attrs: list[ColumnAttribute]) -> str:
rendered = []
render_as_string = False
for attr in column_attrs:
if not self.explicit_foreign_keys and attr.model is relationship.source:
rendered.append(attr.name)
else:
rendered.append(f"{attr.model.name}.{attr.name}")
render_as_string = True
joined = "[" + ", ".join(rendered) + "]"
return repr(joined) if render_as_string else joined
def render_foreign_keys(column_attrs: list[ColumnAttribute]) -> str:
rendered = []
render_as_string = False
# Assume that column_attrs are all in relationship.source or none
for attr in column_attrs:
if not self.explicit_foreign_keys and attr.model is relationship.source:
rendered.append(attr.name)
else:
rendered.append(f"{attr.model.name}.{attr.name}")
render_as_string = True
if render_as_string:
return "'[" + ", ".join(rendered) + "]'"
else:
return "[" + ", ".join(rendered) + "]"
def render_join(terms: list[JoinType]) -> str:
rendered_joins = []
for source, source_col, target, target_col in terms:
rendered = f"lambda: {source.name}.{source_col} == {target.name}."
if target.__class__ is Model:
rendered += "c."
rendered += str(target_col)
rendered_joins.append(rendered)
if len(rendered_joins) > 1:
rendered = ", ".join(rendered_joins)
return f"and_({rendered})"
else:
return rendered_joins[0]
# Render keyword arguments
kwargs: dict[str, Any] = {}
if relationship.type is RelationshipType.ONE_TO_ONE and relationship.constraint:
if relationship.constraint.referred_table is relationship.source.table:
kwargs["uselist"] = False
# Add the "secondary" keyword for many-to-many relationships
if relationship.association_table:
table_ref = relationship.association_table.table.name
if relationship.association_table.schema:
table_ref = f"{relationship.association_table.schema}.{table_ref}"
kwargs["secondary"] = repr(table_ref)
if relationship.remote_side:
kwargs["remote_side"] = render_column_attrs(relationship.remote_side)
if relationship.foreign_keys:
kwargs["foreign_keys"] = render_foreign_keys(relationship.foreign_keys)
if relationship.primaryjoin:
kwargs["primaryjoin"] = render_join(relationship.primaryjoin)
if relationship.secondaryjoin:
kwargs["secondaryjoin"] = render_join(relationship.secondaryjoin)
if relationship.backref:
kwargs["back_populates"] = repr(relationship.backref.name)
return kwargs
class DataclassGenerator(DeclarativeGenerator):
def __init__(
self,
metadata: MetaData,
bind: Connection | Engine,
options: Sequence[str],
*,
indentation: str = " ",
base_class_name: str = "Base",
quote_annotations: bool = False,
metadata_key: str = "sa",
):
super().__init__(
metadata,
bind,
options,
indentation=indentation,
base_class_name=base_class_name,
)
self.metadata_key: str = metadata_key
self.quote_annotations: bool = quote_annotations
def generate_base(self) -> None:
self.base = Base(
literal_imports=[
LiteralImport("sqlalchemy.orm", "DeclarativeBase"),
LiteralImport("sqlalchemy.orm", "MappedAsDataclass"),
],
declarations=[
(f"class {self.base_class_name}(MappedAsDataclass, DeclarativeBase):"),
f"{self.indentation}pass",
],
metadata_ref=f"{self.base_class_name}.metadata",
)
class SQLModelGenerator(DeclarativeGenerator):
def __init__(
self,
metadata: MetaData,
bind: Connection | Engine,
options: Sequence[str],
*,
indentation: str = " ",
base_class_name: str = "SQLModel",
):
super().__init__(
metadata,
bind,
options,
indentation=indentation,
base_class_name=base_class_name,
explicit_foreign_keys=True,
)
@property
def views_supported(self) -> bool:
return False
def render_column_callable(self, is_table: bool, *args: Any, **kwargs: Any) -> str:
self.add_import(Column)
return render_callable("Column", *args, kwargs=kwargs)
def render_table(self, table: Table) -> str:
# Hack to fix #465 without breaking backwards compatibility
self.base.metadata_ref = "SQLModel.metadata"
return super().render_table(table)
def generate_base(self) -> None:
self.base = Base(
literal_imports=[],
declarations=[],
metadata_ref="SQLModel.metadata",
)
def collect_imports(self, models: Iterable[Model]) -> None:
super(DeclarativeGenerator, self).collect_imports(models)
if any(isinstance(model, ModelClass) for model in models):
self.add_literal_import("sqlmodel", "Field")
if models:
self.remove_literal_import("sqlalchemy", "MetaData")
self.add_literal_import("sqlmodel", "SQLModel")
def collect_imports_for_model(self, model: Model) -> None:
super(DeclarativeGenerator, self).collect_imports_for_model(model)
if isinstance(model, ModelClass):
for column_attr in model.columns:
if column_attr.column.nullable:
self.add_literal_import("typing", "Optional")
break
if model.relationships:
self.add_literal_import("sqlmodel", "Relationship")
def render_module_variables(self, models: list[Model]) -> str:
declarations: list[str] = []
if any(not isinstance(model, ModelClass) for model in models):
if self.base.table_metadata_declaration is not None:
declarations.append(self.base.table_metadata_declaration)
return "\n".join(declarations)
def render_class_declaration(self, model: ModelClass) -> str:
if model.parent_class:
parent = model.parent_class.name
else:
parent = self.base_class_name
superclass_part = f"({parent}, table=True)"
return f"class {model.name}{superclass_part}:"
def render_class_variables(self, model: ModelClass) -> str:
variables = []
if model.table.name != model.name.lower():
variables.append(f"__tablename__ = {model.table.name!r}")
# Render constraints and indexes as __table_args__
table_args = self.render_table_args(model.table)
if table_args:
variables.append(f"__table_args__ = {table_args}")
return "\n".join(variables)
def render_column_attribute(self, column_attr: ColumnAttribute) -> str:
column = column_attr.column
rendered_column = self.render_column(column, True)
rendered_column_python_type = self.render_column_python_type(column)
kwargs: dict[str, Any] = {}
if column.nullable:
kwargs["default"] = None
kwargs["sa_column"] = f"{rendered_column}"
rendered_field = render_callable("Field", kwargs=kwargs)
return f"{column_attr.name}: {rendered_column_python_type} = {rendered_field}"
def render_relationship(self, relationship: RelationshipAttribute) -> str:
kwargs = self.render_relationship_arguments(relationship)
annotation = self.render_relationship_annotation(relationship)
native_kwargs: dict[str, Any] = {}
non_native_kwargs: dict[str, Any] = {}
for key, value in kwargs.items():
# The following keyword arguments are natively supported in Relationship
if key in ("back_populates", "cascade_delete", "passive_deletes"):
native_kwargs[key] = value
else:
non_native_kwargs[key] = value
if non_native_kwargs:
native_kwargs["sa_relationship_kwargs"] = (
"{"
+ ", ".join(
f"{key!r}: {value}" for key, value in non_native_kwargs.items()
)
+ "}"
)
rendered_field = render_callable("Relationship", kwargs=native_kwargs)
return f"{relationship.name}: {annotation} = {rendered_field}"
agronholm-sqlacodegen-1b321d1/src/sqlacodegen/models.py 0000664 0000000 0000000 00000004310 15215155472 0023220 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Any
from sqlalchemy.sql.schema import Column, ForeignKeyConstraint, Table
@dataclass
class Model:
table: Table
name: str = field(init=False, default="")
@property
def schema(self) -> str | None:
return self.table.schema
@dataclass
class ModelClass(Model):
columns: list[ColumnAttribute] = field(default_factory=list)
relationships: list[RelationshipAttribute] = field(default_factory=list)
parent_class: ModelClass | None = None
children: list[ModelClass] = field(default_factory=list)
def get_column_attribute(self, column_name: str) -> ColumnAttribute:
for column in self.columns:
if column.column.name == column_name:
return column
raise LookupError(f"Cannot find column attribute for {column_name!r}")
class RelationshipType(Enum):
ONE_TO_ONE = auto()
ONE_TO_MANY = auto()
MANY_TO_ONE = auto()
MANY_TO_MANY = auto()
@dataclass
class ColumnAttribute:
model: ModelClass
column: Column[Any]
name: str = field(init=False, default="")
def __repr__(self) -> str:
return f"{self.__class__.__name__}(name={self.name!r}, type={self.column.type})"
def __str__(self) -> str:
return self.name
JoinType = tuple[Model, ColumnAttribute | str, Model, ColumnAttribute | str]
@dataclass
class RelationshipAttribute:
type: RelationshipType
source: ModelClass
target: ModelClass
constraint: ForeignKeyConstraint | None = None
association_table: Model | None = None
backref: RelationshipAttribute | None = None
remote_side: list[ColumnAttribute] = field(default_factory=list)
foreign_keys: list[ColumnAttribute] = field(default_factory=list)
primaryjoin: list[JoinType] = field(default_factory=list)
secondaryjoin: list[JoinType] = field(default_factory=list)
name: str = field(init=False, default="")
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}(name={self.name!r}, type={self.type}, "
f"target={self.target.name})"
)
def __str__(self) -> str:
return self.name
agronholm-sqlacodegen-1b321d1/src/sqlacodegen/py.typed 0000664 0000000 0000000 00000000000 15215155472 0023052 0 ustar 00root root 0000000 0000000 agronholm-sqlacodegen-1b321d1/src/sqlacodegen/utils.py 0000664 0000000 0000000 00000015735 15215155472 0023112 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import re
import sys
from collections.abc import Mapping
from typing import Any, Literal, cast
from sqlalchemy import PrimaryKeyConstraint, UniqueConstraint
from sqlalchemy.engine import Connection, Engine
from sqlalchemy.sql import ClauseElement
from sqlalchemy.sql.elements import TextClause
from sqlalchemy.sql.schema import (
CheckConstraint,
ColumnCollectionConstraint,
Constraint,
ForeignKeyConstraint,
Index,
Table,
)
_re_postgresql_nextval_sequence = re.compile(r"nextval\('(.+)'::regclass\)")
_re_postgresql_sequence_delimiter = re.compile(r'(.*?)([."]|$)')
def get_column_names(constraint: ColumnCollectionConstraint) -> list[str]:
return list(constraint.columns.keys())
def get_constraint_sort_key(constraint: Constraint) -> str:
if isinstance(constraint, CheckConstraint):
return f"C{constraint.sqltext}"
elif isinstance(constraint, ColumnCollectionConstraint):
return constraint.__class__.__name__[0] + repr(get_column_names(constraint))
else:
return str(constraint)
def get_compiled_expression(statement: ClauseElement, bind: Engine | Connection) -> str:
"""Return the statement in a form where any placeholders have been filled in."""
return str(statement.compile(bind, compile_kwargs={"literal_binds": True}))
def get_common_fk_constraints(
table1: Table, table2: Table
) -> set[ForeignKeyConstraint]:
"""
Return a set of foreign key constraints the two tables have against each other.
"""
c1 = {
c
for c in table1.constraints
if isinstance(c, ForeignKeyConstraint) and c.elements[0].column.table == table2
}
c2 = {
c
for c in table2.constraints
if isinstance(c, ForeignKeyConstraint) and c.elements[0].column.table == table1
}
return c1.union(c2)
def uses_default_name(constraint: Constraint | Index) -> bool:
if not constraint.name or constraint.table is None:
return True
table = constraint.table
values: dict[str, Any] = {
"table_name": table.name,
"constraint_name": constraint.name,
}
if isinstance(constraint, (Index, ColumnCollectionConstraint)):
values.update(
{
"column_0N_name": "".join(col.name for col in constraint.columns),
"column_0_N_name": "_".join(col.name for col in constraint.columns),
"column_0N_label": "".join(
col.label(col.name).name for col in constraint.columns
),
"column_0_N_label": "_".join(
col.label(col.name).name for col in constraint.columns
),
"column_0N_key": "".join(
col.key for col in constraint.columns if col.key
),
"column_0_N_key": "_".join(
col.key for col in constraint.columns if col.key
),
}
)
if constraint.columns:
columns = constraint.columns.values()
values.update(
{
"column_0_name": columns[0].name,
"column_0_label": columns[0].label(columns[0].name).name,
"column_0_key": columns[0].key,
}
)
key: Literal["fk", "pk", "ix", "ck", "uq"]
if isinstance(constraint, Index):
key = "ix"
elif isinstance(constraint, CheckConstraint):
key = "ck"
elif isinstance(constraint, UniqueConstraint):
key = "uq"
elif isinstance(constraint, PrimaryKeyConstraint):
key = "pk"
elif isinstance(constraint, ForeignKeyConstraint):
key = "fk"
values.update(
{
"referred_table_name": constraint.referred_table,
"referred_column_0_name": constraint.elements[0].column.name,
"referred_column_0N_name": "".join(
fk.column.name for fk in constraint.elements
),
"referred_column_0_N_name": "_".join(
fk.column.name for fk in constraint.elements
),
"referred_column_0_label": constraint.elements[0]
.column.label(constraint.elements[0].column.name)
.name,
"referred_fk.column_0N_label": "".join(
fk.column.label(fk.column.name).name for fk in constraint.elements
),
"referred_fk.column_0_N_label": "_".join(
fk.column.label(fk.column.name).name for fk in constraint.elements
),
"referred_fk.column_0_key": constraint.elements[0].column.key,
"referred_fk.column_0N_key": "".join(
fk.column.key for fk in constraint.elements if fk.column.key
),
"referred_fk.column_0_N_key": "_".join(
fk.column.key for fk in constraint.elements if fk.column.key
),
}
)
else:
raise TypeError(f"Unknown constraint type: {constraint.__class__.__qualname__}")
try:
convention = cast(
Mapping[str, str],
table.metadata.naming_convention,
)[key]
return constraint.name == (convention % values)
except KeyError:
return False
def render_callable(
name: str,
*args: object,
kwargs: Mapping[str, object] | None = None,
indentation: str = "",
) -> str:
"""
Render a function call.
:param name: name of the callable
:param args: positional arguments
:param kwargs: keyword arguments
:param indentation: if given, each argument will be rendered on its own line with
this value used as the indentation
"""
if kwargs:
args += tuple(f"{key}={value}" for key, value in kwargs.items())
if indentation:
prefix = f"\n{indentation}"
suffix = "\n"
delimiter = f",\n{indentation}"
else:
prefix = suffix = ""
delimiter = ", "
rendered_args = delimiter.join(str(arg) for arg in args)
return f"{name}({prefix}{rendered_args}{suffix})"
def qualified_table_name(table: Table) -> str:
if table.schema:
return f"{table.schema}.{table.name}"
else:
return str(table.name)
def decode_postgresql_sequence(clause: TextClause) -> tuple[str | None, str | None]:
match = _re_postgresql_nextval_sequence.match(clause.text)
if not match:
return None, None
schema: str | None = None
sequence: str = ""
in_quotes = False
for match in _re_postgresql_sequence_delimiter.finditer(match.group(1)):
sequence += match.group(1)
if match.group(2) == '"':
in_quotes = not in_quotes
elif match.group(2) == ".":
if in_quotes:
sequence += "."
else:
schema, sequence = sequence, ""
return schema, sequence
def get_stdlib_module_names() -> set[str]:
return set(sys.builtin_module_names) | set(sys.stdlib_module_names)
agronholm-sqlacodegen-1b321d1/tests/ 0000775 0000000 0000000 00000000000 15215155472 0017453 5 ustar 00root root 0000000 0000000 agronholm-sqlacodegen-1b321d1/tests/__init__.py 0000664 0000000 0000000 00000000000 15215155472 0021552 0 ustar 00root root 0000000 0000000 agronholm-sqlacodegen-1b321d1/tests/conftest.py 0000664 0000000 0000000 00000001622 15215155472 0021653 0 ustar 00root root 0000000 0000000 from textwrap import dedent
import pytest
from pytest import FixtureRequest
from sqlalchemy.engine import Engine, create_engine
from sqlalchemy.orm import clear_mappers, configure_mappers
from sqlalchemy.schema import MetaData
@pytest.fixture
def engine(request: FixtureRequest) -> Engine:
dialect = getattr(request, "param", None)
if dialect == "postgresql":
return create_engine("postgresql+psycopg:///testdb")
elif dialect == "mysql":
return create_engine("mysql+mysqlconnector://testdb")
else:
return create_engine("sqlite:///:memory:")
@pytest.fixture
def metadata() -> MetaData:
return MetaData()
def validate_code(generated_code: str, expected_code: str) -> None:
expected_code = dedent(expected_code)
assert generated_code == expected_code
try:
exec(generated_code, {})
configure_mappers()
finally:
clear_mappers()
agronholm-sqlacodegen-1b321d1/tests/test_cli.py 0000664 0000000 0000000 00000012310 15215155472 0021630 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import sqlite3
import subprocess
import sys
from importlib.metadata import version
from pathlib import Path
import pytest
future_imports = "from __future__ import annotations\n\n"
@pytest.fixture
def db_path(tmp_path: Path) -> Path:
path = tmp_path / "test.db"
with sqlite3.connect(str(path)) as conn:
cursor = conn.cursor()
cursor.execute(
"CREATE TABLE foo (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL)"
)
return path
def test_cli_tables(db_path: Path, tmp_path: Path) -> None:
output_path = tmp_path / "outfile"
subprocess.run(
[
"sqlacodegen",
f"sqlite:///{db_path}",
"--generator",
"tables",
"--outfile",
str(output_path),
],
check=True,
)
assert (
output_path.read_text()
== """\
from sqlalchemy import Column, Integer, MetaData, Table, Text
metadata = MetaData()
t_foo = Table(
'foo', metadata,
Column('id', Integer, primary_key=True),
Column('name', Text, nullable=False)
)
"""
)
def test_cli_declarative(db_path: Path, tmp_path: Path) -> None:
output_path = tmp_path / "outfile"
subprocess.run(
[
"sqlacodegen",
f"sqlite:///{db_path}",
"--generator",
"declarative",
"--outfile",
str(output_path),
],
check=True,
)
assert (
output_path.read_text()
== """\
from sqlalchemy import Integer, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Foo(Base):
__tablename__ = 'foo'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(Text, nullable=False)
"""
)
def test_cli_dataclass(db_path: Path, tmp_path: Path) -> None:
output_path = tmp_path / "outfile"
subprocess.run(
[
"sqlacodegen",
f"sqlite:///{db_path}",
"--generator",
"dataclasses",
"--outfile",
str(output_path),
],
check=True,
)
assert (
output_path.read_text()
== """\
from sqlalchemy import Integer, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, mapped_column
class Base(MappedAsDataclass, DeclarativeBase):
pass
class Foo(Base):
__tablename__ = 'foo'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(Text, nullable=False)
"""
)
def test_cli_sqlmodels(db_path: Path, tmp_path: Path) -> None:
output_path = tmp_path / "outfile"
subprocess.run(
[
"sqlacodegen",
f"sqlite:///{db_path}",
"--generator",
"sqlmodels",
"--outfile",
str(output_path),
],
check=True,
)
assert (
output_path.read_text()
== """\
from sqlalchemy import Column, Integer, Text
from sqlmodel import Field, SQLModel
class Foo(SQLModel, table=True):
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
name: str = Field(sa_column=Column('name', Text, nullable=False))
"""
)
def test_cli_engine_arg(db_path: Path, tmp_path: Path) -> None:
output_path = tmp_path / "outfile"
subprocess.run(
[
"sqlacodegen",
f"sqlite:///{db_path}",
"--generator",
"tables",
"--engine-arg",
'connect_args={"timeout": 10}',
"--outfile",
str(output_path),
],
check=True,
)
assert (
output_path.read_text()
== """\
from sqlalchemy import Column, Integer, MetaData, Table, Text
metadata = MetaData()
t_foo = Table(
'foo', metadata,
Column('id', Integer, primary_key=True),
Column('name', Text, nullable=False)
)
"""
)
def test_cli_invalid_engine_arg(db_path: Path, tmp_path: Path) -> None:
output_path = tmp_path / "outfile"
# Expect exception:
# TypeError: 'this_arg_does_not_exist' is an invalid keyword argument for Connection()
with pytest.raises(subprocess.CalledProcessError) as exc_info:
subprocess.run(
[
"sqlacodegen",
f"sqlite:///{db_path}",
"--generator",
"tables",
"--engine-arg",
'connect_args={"this_arg_does_not_exist": 10}',
"--outfile",
str(output_path),
],
check=True,
capture_output=True,
)
if sys.version_info < (3, 13):
assert (
"'this_arg_does_not_exist' is an invalid keyword argument"
in exc_info.value.stderr.decode()
)
else:
assert (
"got an unexpected keyword argument 'this_arg_does_not_exist'"
in exc_info.value.stderr.decode()
)
def test_main() -> None:
expected_version = version("sqlacodegen")
completed = subprocess.run(
[sys.executable, "-m", "sqlacodegen", "--version"],
stdout=subprocess.PIPE,
check=True,
)
assert completed.stdout.decode().strip() == expected_version
agronholm-sqlacodegen-1b321d1/tests/test_generator_dataclass.py 0000664 0000000 0000000 00000017070 15215155472 0025076 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import pytest
from _pytest.fixtures import FixtureRequest
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.engine import Engine
from sqlalchemy.schema import Column, ForeignKeyConstraint, MetaData, Table
from sqlalchemy.sql.expression import text
from sqlalchemy.types import INTEGER, VARCHAR
from sqlacodegen.generators import CodeGenerator, DataclassGenerator
from .conftest import validate_code
@pytest.fixture
def generator(
request: FixtureRequest, metadata: MetaData, engine: Engine
) -> CodeGenerator:
options = getattr(request, "param", [])
return DataclassGenerator(metadata, engine, options)
def test_basic_class(generator: CodeGenerator) -> None:
Table(
"simple",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("name", VARCHAR(20)),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, \
mapped_column
class Base(MappedAsDataclass, DeclarativeBase):
pass
class Simple(Base):
__tablename__ = 'simple'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[Optional[str]] = mapped_column(String(20))
""",
)
def test_mandatory_field_last(generator: CodeGenerator) -> None:
Table(
"simple",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("name", VARCHAR(20), server_default=text("foo")),
Column("age", INTEGER, nullable=False),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Integer, String, text
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, \
mapped_column
class Base(MappedAsDataclass, DeclarativeBase):
pass
class Simple(Base):
__tablename__ = 'simple'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
age: Mapped[int] = mapped_column(Integer, nullable=False)
name: Mapped[Optional[str]] = mapped_column(String(20), \
server_default=text('foo'))
""",
)
def test_onetomany_optional(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("container_id", INTEGER),
ForeignKeyConstraint(["container_id"], ["simple_containers.id"]),
)
Table(
"simple_containers",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, \
mapped_column, relationship
class Base(MappedAsDataclass, DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
back_populates='container')
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
container_id: Mapped[Optional[int]] = \
mapped_column(ForeignKey('simple_containers.id'))
container: Mapped[Optional['SimpleContainers']] = relationship('SimpleContainers', \
back_populates='simple_items')
""",
)
def test_manytomany(generator: CodeGenerator) -> None:
Table("simple_items", generator.metadata, Column("id", INTEGER, primary_key=True))
Table(
"simple_containers",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
Table(
"container_items",
generator.metadata,
Column("item_id", INTEGER),
Column("container_id", INTEGER),
ForeignKeyConstraint(["item_id"], ["simple_items.id"]),
ForeignKeyConstraint(["container_id"], ["simple_containers.id"]),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, ForeignKey, Integer, Table
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, \
mapped_column, relationship
class Base(MappedAsDataclass, DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
item: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
secondary='container_items', back_populates='container')
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
container: Mapped[list['SimpleContainers']] = \
relationship('SimpleContainers', secondary='container_items', back_populates='item')
t_container_items = Table(
'container_items', Base.metadata,
Column('item_id', ForeignKey('simple_items.id')),
Column('container_id', ForeignKey('simple_containers.id'))
)
""",
)
def test_named_foreign_key_constraints(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("container_id", INTEGER),
ForeignKeyConstraint(
["container_id"], ["simple_containers.id"], name="foreignkeytest"
),
)
Table(
"simple_containers",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKeyConstraint, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, \
mapped_column, relationship
class Base(MappedAsDataclass, DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
back_populates='container')
class SimpleItems(Base):
__tablename__ = 'simple_items'
__table_args__ = (
ForeignKeyConstraint(['container_id'], ['simple_containers.id'], \
name='foreignkeytest'),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
container_id: Mapped[Optional[int]] = mapped_column(Integer)
container: Mapped[Optional['SimpleContainers']] = relationship('SimpleContainers', \
back_populates='simple_items')
""",
)
def test_uuid_type_annotation(generator: CodeGenerator) -> None:
Table(
"simple",
generator.metadata,
Column("id", UUID, primary_key=True),
)
validate_code(
generator.generate(),
"""\
import uuid
from sqlalchemy import UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, \
mapped_column
class Base(MappedAsDataclass, DeclarativeBase):
pass
class Simple(Base):
__tablename__ = 'simple'
id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True)
""",
)
agronholm-sqlacodegen-1b321d1/tests/test_generator_declarative.py 0000664 0000000 0000000 00000265013 15215155472 0025424 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import sys
import pytest
from _pytest.fixtures import FixtureRequest
from geoalchemy2 import Geography, Geometry
from sqlalchemy import BIGINT, PrimaryKeyConstraint
from sqlalchemy import Enum as SAEnum
from sqlalchemy.dialects import postgresql
from sqlalchemy.dialects.postgresql import JSON, JSONB
from sqlalchemy.engine import Engine
from sqlalchemy.schema import (
CheckConstraint,
Column,
ForeignKey,
ForeignKeyConstraint,
Index,
MetaData,
Table,
UniqueConstraint,
)
from sqlalchemy.sql.expression import text
from sqlalchemy.types import ARRAY, INTEGER, VARCHAR, Text
from sqlacodegen.generators import CodeGenerator, DeclarativeGenerator
from .conftest import validate_code
@pytest.fixture
def generator(
request: FixtureRequest, metadata: MetaData, engine: Engine
) -> CodeGenerator:
options = getattr(request, "param", [])
return DeclarativeGenerator(metadata, engine, options)
def test_indexes(generator: CodeGenerator) -> None:
simple_items = Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("number", INTEGER),
Column("text", VARCHAR),
)
simple_items.indexes.add(Index("idx_number", simple_items.c.number))
simple_items.indexes.add(
Index("idx_text_number", simple_items.c.text, simple_items.c.number)
)
simple_items.indexes.add(Index("idx_text", simple_items.c.text, unique=True))
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Index, Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class SimpleItems(Base):
__tablename__ = 'simple_items'
__table_args__ = (
Index('idx_number', 'number'),
Index('idx_text', 'text', unique=True),
Index('idx_text_number', 'text', 'number')
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
number: Mapped[Optional[int]] = mapped_column(Integer)
text: Mapped[Optional[str]] = mapped_column(String)
""",
)
def test_index_with_kwargs(generator: CodeGenerator) -> None:
simple_items = Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("name", VARCHAR),
)
simple_items.indexes.add(
Index("idx_name", simple_items.c.name, postgresql_using="gist", mysql_length=10)
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Index, Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class SimpleItems(Base):
__tablename__ = 'simple_items'
__table_args__ = (
Index('idx_name', 'name', mysql_length=10, postgresql_using='gist'),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[Optional[str]] = mapped_column(String)
""",
)
def test_constraints(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("number", INTEGER),
CheckConstraint("number > 0"),
UniqueConstraint("id", "number"),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import CheckConstraint, Integer, UniqueConstraint
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class SimpleItems(Base):
__tablename__ = 'simple_items'
__table_args__ = (
CheckConstraint('number > 0'),
UniqueConstraint('id', 'number')
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
number: Mapped[Optional[int]] = mapped_column(Integer)
""",
)
@pytest.mark.parametrize("generator", [["include_dialect_options"]], indirect=True)
def test_include_dialect_options_and_info_table_and_column(
generator: CodeGenerator,
) -> None:
from .test_generator_tables import _PartitionInfo
Table(
"t_opts",
generator.metadata,
Column("id", INTEGER, primary_key=True, starrocks_is_agg_key=True),
Column("name", VARCHAR, starrocks_agg_type="REPLACE"),
starrocks_aggregate_key="id",
starrocks_partition_by=_PartitionInfo("RANGE(id)"),
starrocks_security="DEFINER",
starrocks_PROPERTIES={"replication_num": "3", "storage_medium": "SSD"},
info={
"table_kind": "MATERIALIZED VIEW",
"definition": "SELECT id, name FROM t_opts_base_table",
},
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class TOpts(Base):
__tablename__ = 't_opts'
__table_args__ = {'info': {'definition': 'SELECT id, name FROM t_opts_base_table',
'table_kind': 'MATERIALIZED VIEW'},
'starrocks_PROPERTIES': {'replication_num': '3', 'storage_medium': 'SSD'},
'starrocks_aggregate_key': 'id',
'starrocks_partition_by': 'RANGE(id)',
'starrocks_security': 'DEFINER'}
id: Mapped[int] = mapped_column(Integer, primary_key=True, starrocks_is_agg_key=True)
name: Mapped[Optional[str]] = mapped_column(String, starrocks_agg_type='REPLACE')
""",
)
@pytest.mark.parametrize("generator", [["include_dialect_options"]], indirect=True)
def test_include_dialect_options_and_info_with_hyphen(generator: CodeGenerator) -> None:
Table(
"t_opts2",
generator.metadata,
Column("id", INTEGER, primary_key=True),
mysql_engine="InnoDB",
info={"table_kind": "View"},
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class TOpts2(Base):
__tablename__ = 't_opts2'
__table_args__ = {'info': {'table_kind': 'View'}, 'mysql_engine': 'InnoDB'}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
""",
)
def test_include_dialect_options_not_enabled_skips(generator: CodeGenerator) -> None:
from .test_generator_tables import _PartitionInfo
Table(
"t_plain",
generator.metadata,
Column(
"id",
INTEGER,
primary_key=True,
info={"abc": True},
starrocks_is_agg_key=True,
),
starrocks_engine="OLAP",
starrocks_partition_by=_PartitionInfo("RANGE(id)"),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class TPlain(Base):
__tablename__ = 't_plain'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
""",
)
def test_keep_dialect_types_adapts_mysql_integer_default(
generator: CodeGenerator,
) -> None:
from sqlalchemy.dialects.mysql import INTEGER as MYSQL_INTEGER
Table(
"num",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("val", MYSQL_INTEGER(), nullable=False),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Num(Base):
__tablename__ = 'num'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
val: Mapped[int] = mapped_column(Integer, nullable=False)
""",
)
@pytest.mark.parametrize("generator", [["keep_dialect_types"]], indirect=True)
def test_keep_dialect_types_keeps_mysql_integer(generator: CodeGenerator) -> None:
from sqlalchemy.dialects.mysql import INTEGER as MYSQL_INTEGER
Table(
"num2",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("val", MYSQL_INTEGER(), nullable=False),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import INTEGER
from sqlalchemy.dialects.mysql import INTEGER
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Num2(Base):
__tablename__ = 'num2'
id: Mapped[int] = mapped_column(INTEGER, primary_key=True)
val: Mapped[int] = mapped_column(INTEGER, nullable=False)
""",
)
@pytest.mark.parametrize("engine", ["mysql"], indirect=["engine"])
@pytest.mark.parametrize("generator", [["keep_dialect_types"]], indirect=True)
def test_keep_dialect_types_keeps_mysql_char_collation(
generator: CodeGenerator,
) -> None:
from sqlalchemy.dialects.mysql import CHAR as MYSQL_CHAR
from sqlalchemy.dialects.mysql import INTEGER as MYSQL_INTEGER
Table(
"result_logs",
generator.metadata,
Column("id", MYSQL_INTEGER, primary_key=True),
Column("result_code", MYSQL_CHAR(1, collation="utf8mb3_bin"), nullable=False),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy.dialects.mysql import CHAR, INTEGER
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class ResultLogs(Base):
__tablename__ = 'result_logs'
id: Mapped[int] = mapped_column(INTEGER, primary_key=True)
result_code: Mapped[str] = mapped_column(CHAR(1, collation='utf8mb3_bin'), nullable=False)
""",
)
def test_onetomany(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("container_id", INTEGER),
ForeignKeyConstraint(["container_id"], ["simple_containers.id"]),
)
Table(
"simple_containers",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
back_populates='container')
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
container_id: Mapped[Optional[int]] = \
mapped_column(ForeignKey('simple_containers.id'))
container: Mapped[Optional['SimpleContainers']] = relationship('SimpleContainers', \
back_populates='simple_items')
""",
)
def test_onetomany_selfref(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("parent_item_id", INTEGER),
ForeignKeyConstraint(["parent_item_id"], ["simple_items.id"]),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
parent_item_id: Mapped[Optional[int]] = \
mapped_column(ForeignKey('simple_items.id'))
parent_item: Mapped[Optional['SimpleItems']] = relationship('SimpleItems', \
remote_side=[id], back_populates='parent_item_reverse')
parent_item_reverse: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
remote_side=[parent_item_id], back_populates='parent_item')
""",
)
def test_onetomany_selfref_multi(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("parent_item_id", INTEGER),
Column("top_item_id", INTEGER),
ForeignKeyConstraint(["parent_item_id"], ["simple_items.id"]),
ForeignKeyConstraint(["top_item_id"], ["simple_items.id"]),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
parent_item_id: Mapped[Optional[int]] = \
mapped_column(ForeignKey('simple_items.id'))
top_item_id: Mapped[Optional[int]] = mapped_column(ForeignKey('simple_items.id'))
parent_item: Mapped[Optional['SimpleItems']] = relationship('SimpleItems', \
remote_side=[id], foreign_keys=[parent_item_id], back_populates='parent_item_reverse')
parent_item_reverse: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
remote_side=[parent_item_id], foreign_keys=[parent_item_id], \
back_populates='parent_item')
top_item: Mapped[Optional['SimpleItems']] = relationship('SimpleItems', remote_side=[id], \
foreign_keys=[top_item_id], back_populates='top_item_reverse')
top_item_reverse: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
remote_side=[top_item_id], foreign_keys=[top_item_id], back_populates='top_item')
""",
)
def test_onetomany_composite(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("container_id1", INTEGER),
Column("container_id2", INTEGER),
ForeignKeyConstraint(
["container_id1", "container_id2"],
["simple_containers.id1", "simple_containers.id2"],
ondelete="CASCADE",
onupdate="CASCADE",
),
)
Table(
"simple_containers",
generator.metadata,
Column("id1", INTEGER, primary_key=True),
Column("id2", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKeyConstraint, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id1: Mapped[int] = mapped_column(Integer, primary_key=True)
id2: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
back_populates='simple_containers')
class SimpleItems(Base):
__tablename__ = 'simple_items'
__table_args__ = (
ForeignKeyConstraint(['container_id1', 'container_id2'], \
['simple_containers.id1', 'simple_containers.id2'], ondelete='CASCADE', \
onupdate='CASCADE'),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
container_id1: Mapped[Optional[int]] = mapped_column(Integer)
container_id2: Mapped[Optional[int]] = mapped_column(Integer)
simple_containers: Mapped[Optional['SimpleContainers']] = relationship('SimpleContainers', \
back_populates='simple_items')
""",
)
def test_onetomany_multiref(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("parent_container_id", INTEGER),
Column("top_container_id", INTEGER, nullable=False),
ForeignKeyConstraint(["parent_container_id"], ["simple_containers.id"]),
ForeignKeyConstraint(["top_container_id"], ["simple_containers.id"]),
)
Table(
"simple_containers",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items_parent_container: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
foreign_keys='[SimpleItems.parent_container_id]', back_populates='parent_container')
simple_items_top_container: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
foreign_keys='[SimpleItems.top_container_id]', back_populates='top_container')
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
top_container_id: Mapped[int] = \
mapped_column(ForeignKey('simple_containers.id'), nullable=False)
parent_container_id: Mapped[Optional[int]] = \
mapped_column(ForeignKey('simple_containers.id'))
parent_container: Mapped[Optional['SimpleContainers']] = relationship('SimpleContainers', \
foreign_keys=[parent_container_id], back_populates='simple_items_parent_container')
top_container: Mapped['SimpleContainers'] = relationship('SimpleContainers', \
foreign_keys=[top_container_id], back_populates='simple_items_top_container')
""",
)
@pytest.mark.parametrize("generator", [["nofknames"]], indirect=True)
def test_onetomany_multiref_with_nofknames(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("parent_container_id", INTEGER),
Column("top_container_id", INTEGER, nullable=False),
ForeignKeyConstraint(["parent_container_id"], ["simple_containers.id"]),
ForeignKeyConstraint(["top_container_id"], ["simple_containers.id"]),
)
Table(
"simple_containers",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
foreign_keys='[SimpleItems.parent_container_id]', back_populates='parent_container')
simple_items_: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
foreign_keys='[SimpleItems.top_container_id]', back_populates='top_container')
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
top_container_id: Mapped[int] = \
mapped_column(ForeignKey('simple_containers.id'), nullable=False)
parent_container_id: Mapped[Optional[int]] = \
mapped_column(ForeignKey('simple_containers.id'))
parent_container: Mapped[Optional['SimpleContainers']] = relationship('SimpleContainers', \
foreign_keys=[parent_container_id], back_populates='simple_items')
top_container: Mapped['SimpleContainers'] = relationship('SimpleContainers', \
foreign_keys=[top_container_id], back_populates='simple_items_')
""",
)
def test_onetomany_multiref_no_id_suffix(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("parent_container", INTEGER),
Column("top_container", INTEGER, nullable=False),
ForeignKeyConstraint(["parent_container"], ["simple_containers.id"]),
ForeignKeyConstraint(["top_container"], ["simple_containers.id"]),
)
Table(
"simple_containers",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items_parent_container: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
foreign_keys='[SimpleItems.parent_container]', back_populates='simple_containers')
simple_items_top_container: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
foreign_keys='[SimpleItems.top_container]', back_populates='simple_containers_')
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
top_container: Mapped[int] = mapped_column(ForeignKey('simple_containers.id'), nullable=False)
parent_container: Mapped[Optional[int]] = mapped_column(ForeignKey('simple_containers.id'))
simple_containers: Mapped[Optional['SimpleContainers']] = relationship('SimpleContainers', \
foreign_keys=[parent_container], back_populates='simple_items_parent_container')
simple_containers_: Mapped['SimpleContainers'] = relationship('SimpleContainers', \
foreign_keys=[top_container], back_populates='simple_items_top_container')
""",
)
def test_onetomany_multiref_composite(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("parent_id1", INTEGER),
Column("parent_id2", INTEGER),
Column("top_id1", INTEGER),
Column("top_id2", INTEGER),
ForeignKeyConstraint(
["parent_id1", "parent_id2"],
["simple_containers.id1", "simple_containers.id2"],
),
ForeignKeyConstraint(
["top_id1", "top_id2"], ["simple_containers.id1", "simple_containers.id2"]
),
)
Table(
"simple_containers",
generator.metadata,
Column("id1", INTEGER, primary_key=True),
Column("id2", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKeyConstraint, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id1: Mapped[int] = mapped_column(Integer, primary_key=True)
id2: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items_parent_id1_parent_id2: Mapped[list['SimpleItems']] = \
relationship('SimpleItems', foreign_keys='[SimpleItems.parent_id1, SimpleItems.parent_id2]', \
back_populates='parent_id1_parent_id2')
simple_items_top_id1_top_id2: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
foreign_keys='[SimpleItems.top_id1, SimpleItems.top_id2]', \
back_populates='top_id1_top_id2')
class SimpleItems(Base):
__tablename__ = 'simple_items'
__table_args__ = (
ForeignKeyConstraint(['parent_id1', 'parent_id2'], \
['simple_containers.id1', 'simple_containers.id2']),
ForeignKeyConstraint(['top_id1', 'top_id2'], \
['simple_containers.id1', 'simple_containers.id2'])
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
parent_id1: Mapped[Optional[int]] = mapped_column(Integer)
parent_id2: Mapped[Optional[int]] = mapped_column(Integer)
top_id1: Mapped[Optional[int]] = mapped_column(Integer)
top_id2: Mapped[Optional[int]] = mapped_column(Integer)
parent_id1_parent_id2: Mapped[Optional['SimpleContainers']] = \
relationship('SimpleContainers', foreign_keys=[parent_id1, parent_id2], \
back_populates='simple_items_parent_id1_parent_id2')
top_id1_top_id2: Mapped[Optional['SimpleContainers']] = \
relationship('SimpleContainers', foreign_keys=[top_id1, top_id2], \
back_populates='simple_items_top_id1_top_id2')
""",
)
@pytest.mark.parametrize("generator", [["use_inflect"]], indirect=True)
def test_onetomany_multiref_with_inflect(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("parent_container_id", INTEGER),
Column("top_container_id", INTEGER, nullable=False),
ForeignKeyConstraint(["parent_container_id"], ["simple_containers.id"]),
ForeignKeyConstraint(["top_container_id"], ["simple_containers.id"]),
)
Table(
"simple_containers",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleContainer(Base):
__tablename__ = 'simple_containers'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items_parent_containers: Mapped[list['SimpleItem']] = relationship('SimpleItem', \
foreign_keys='[SimpleItem.parent_container_id]', back_populates='parent_container')
simple_items_top_containers: Mapped[list['SimpleItem']] = relationship('SimpleItem', \
foreign_keys='[SimpleItem.top_container_id]', back_populates='top_container')
class SimpleItem(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
top_container_id: Mapped[int] = \
mapped_column(ForeignKey('simple_containers.id'), nullable=False)
parent_container_id: Mapped[Optional[int]] = \
mapped_column(ForeignKey('simple_containers.id'))
parent_container: Mapped[Optional['SimpleContainer']] = relationship('SimpleContainer', \
foreign_keys=[parent_container_id], back_populates='simple_items_parent_containers')
top_container: Mapped['SimpleContainer'] = relationship('SimpleContainer', \
foreign_keys=[top_container_id], back_populates='simple_items_top_containers')
""",
)
def test_onetoone(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("other_item_id", INTEGER),
ForeignKeyConstraint(["other_item_id"], ["other_items.id"]),
UniqueConstraint("other_item_id"),
)
Table(
"other_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class OtherItems(Base):
__tablename__ = 'other_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items: Mapped[Optional['SimpleItems']] = relationship('SimpleItems', uselist=False, \
back_populates='other_item')
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
other_item_id: Mapped[Optional[int]] = \
mapped_column(ForeignKey('other_items.id'), unique=True)
other_item: Mapped[Optional['OtherItems']] = relationship('OtherItems', \
back_populates='simple_items')
""",
)
def test_onetomany_noinflect(generator: CodeGenerator) -> None:
Table(
"oglkrogk",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("fehwiuhfiwID", INTEGER),
ForeignKeyConstraint(["fehwiuhfiwID"], ["fehwiuhfiw.id"]),
)
Table("fehwiuhfiw", generator.metadata, Column("id", INTEGER, primary_key=True))
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class Fehwiuhfiw(Base):
__tablename__ = 'fehwiuhfiw'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
oglkrogk: Mapped[list['Oglkrogk']] = relationship('Oglkrogk', \
back_populates='fehwiuhfiw')
class Oglkrogk(Base):
__tablename__ = 'oglkrogk'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
fehwiuhfiwID: Mapped[Optional[int]] = mapped_column(ForeignKey('fehwiuhfiw.id'))
fehwiuhfiw: Mapped[Optional['Fehwiuhfiw']] = \
relationship('Fehwiuhfiw', back_populates='oglkrogk')
""",
)
def test_onetomany_conflicting_column(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("container_id", INTEGER),
ForeignKeyConstraint(["container_id"], ["simple_containers.id"]),
)
Table(
"simple_containers",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("relationship", Text),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
relationship_: Mapped[Optional[str]] = mapped_column('relationship', Text)
simple_items: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
back_populates='container')
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
container_id: Mapped[Optional[int]] = \
mapped_column(ForeignKey('simple_containers.id'))
container: Mapped[Optional['SimpleContainers']] = relationship('SimpleContainers', \
back_populates='simple_items')
""",
)
def test_onetomany_conflicting_relationship(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("relationship_id", INTEGER),
ForeignKeyConstraint(["relationship_id"], ["relationship.id"]),
)
Table("relationship", generator.metadata, Column("id", INTEGER, primary_key=True))
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class Relationship(Base):
__tablename__ = 'relationship'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
back_populates='relationship_')
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
relationship_id: Mapped[Optional[int]] = \
mapped_column(ForeignKey('relationship.id'))
relationship_: Mapped[Optional['Relationship']] = relationship('Relationship', \
back_populates='simple_items')
""",
)
@pytest.mark.parametrize("generator", [["nobidi"]], indirect=True)
def test_manytoone_nobidi(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("container_id", INTEGER),
ForeignKeyConstraint(["container_id"], ["simple_containers.id"]),
)
Table(
"simple_containers",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
container_id: Mapped[Optional[int]] = \
mapped_column(ForeignKey('simple_containers.id'))
container: Mapped[Optional['SimpleContainers']] = relationship('SimpleContainers')
""",
)
def test_manytomany_multi(generator: CodeGenerator) -> None:
Table(
"students",
generator.metadata,
Column("student_id", INTEGER, primary_key=True),
Column("name", VARCHAR),
)
Table(
"courses",
generator.metadata,
Column("course_id", INTEGER, primary_key=True),
Column("title", VARCHAR),
)
# First many-to-many relationship (enrollments)
Table(
"enrollments",
generator.metadata,
Column("student_id", INTEGER, ForeignKey("students.student_id")),
Column("course_id", INTEGER, ForeignKey("courses.course_id")),
)
# Second many-to-many relationship (waitlist)
Table(
"waitlist",
generator.metadata,
Column("student_id", INTEGER, ForeignKey("students.student_id")),
Column("course_id", INTEGER, ForeignKey("courses.course_id")),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Column, ForeignKey, Integer, String, Table
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class Courses(Base):
__tablename__ = 'courses'
course_id: Mapped[int] = mapped_column(Integer, primary_key=True)
title: Mapped[Optional[str]] = mapped_column(String)
students_enrollments: Mapped[list['Students']] = relationship('Students', \
secondary='enrollments', back_populates='courses_enrollments')
students_waitlist: Mapped[list['Students']] = relationship('Students', \
secondary='waitlist', back_populates='courses_waitlist')
class Students(Base):
__tablename__ = 'students'
student_id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[Optional[str]] = mapped_column(String)
courses_enrollments: Mapped[list['Courses']] = relationship('Courses', \
secondary='enrollments', back_populates='students_enrollments')
courses_waitlist: Mapped[list['Courses']] = relationship('Courses', \
secondary='waitlist', back_populates='students_waitlist')
t_enrollments = Table(
'enrollments', Base.metadata,
Column('student_id', ForeignKey('students.student_id')),
Column('course_id', ForeignKey('courses.course_id'))
)
t_waitlist = Table(
'waitlist', Base.metadata,
Column('student_id', ForeignKey('students.student_id')),
Column('course_id', ForeignKey('courses.course_id'))
)
""",
)
@pytest.mark.parametrize("generator", [["nofknames"]], indirect=True)
def test_manytomany_multi_with_nofknames(generator: CodeGenerator) -> None:
Table(
"students",
generator.metadata,
Column("student_id", INTEGER, primary_key=True),
Column("name", VARCHAR),
)
Table(
"courses",
generator.metadata,
Column("course_id", INTEGER, primary_key=True),
Column("title", VARCHAR),
)
# First many-to-many relationship (enrollments)
Table(
"enrollments",
generator.metadata,
Column("student_id", INTEGER, ForeignKey("students.student_id")),
Column("course_id", INTEGER, ForeignKey("courses.course_id")),
)
# Second many-to-many relationship (waitlist)
Table(
"waitlist",
generator.metadata,
Column("student_id", INTEGER, ForeignKey("students.student_id")),
Column("course_id", INTEGER, ForeignKey("courses.course_id")),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Column, ForeignKey, Integer, String, Table
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class Courses(Base):
__tablename__ = 'courses'
course_id: Mapped[int] = mapped_column(Integer, primary_key=True)
title: Mapped[Optional[str]] = mapped_column(String)
student: Mapped[list['Students']] = relationship('Students', secondary='enrollments', \
back_populates='course')
student_: Mapped[list['Students']] = relationship('Students', secondary='waitlist', \
back_populates='course_')
class Students(Base):
__tablename__ = 'students'
student_id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[Optional[str]] = mapped_column(String)
course: Mapped[list['Courses']] = relationship('Courses', secondary='enrollments', \
back_populates='student')
course_: Mapped[list['Courses']] = relationship('Courses', secondary='waitlist', \
back_populates='student_')
t_enrollments = Table(
'enrollments', Base.metadata,
Column('student_id', ForeignKey('students.student_id')),
Column('course_id', ForeignKey('courses.course_id'))
)
t_waitlist = Table(
'waitlist', Base.metadata,
Column('student_id', ForeignKey('students.student_id')),
Column('course_id', ForeignKey('courses.course_id'))
)
""",
)
def test_manytomany(generator: CodeGenerator) -> None:
Table("left_table", generator.metadata, Column("id", INTEGER, primary_key=True))
Table(
"right_table",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
Table(
"association_table",
generator.metadata,
Column("left_id", INTEGER),
Column("right_id", INTEGER),
ForeignKeyConstraint(["left_id"], ["left_table.id"]),
ForeignKeyConstraint(["right_id"], ["right_table.id"]),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, ForeignKey, Integer, Table
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class LeftTable(Base):
__tablename__ = 'left_table'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
right: Mapped[list['RightTable']] = relationship('RightTable', \
secondary='association_table', back_populates='left')
class RightTable(Base):
__tablename__ = 'right_table'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
left: Mapped[list['LeftTable']] = relationship('LeftTable', \
secondary='association_table', back_populates='right')
t_association_table = Table(
'association_table', Base.metadata,
Column('left_id', ForeignKey('left_table.id')),
Column('right_id', ForeignKey('right_table.id'))
)
""",
)
@pytest.mark.parametrize("generator", [["nobidi"]], indirect=True)
def test_manytomany_nobidi(generator: CodeGenerator) -> None:
Table("simple_items", generator.metadata, Column("id", INTEGER, primary_key=True))
Table(
"simple_containers",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
Table(
"container_items",
generator.metadata,
Column("item_id", INTEGER),
Column("container_id", INTEGER),
ForeignKeyConstraint(["item_id"], ["simple_items.id"]),
ForeignKeyConstraint(["container_id"], ["simple_containers.id"]),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, ForeignKey, Integer, Table
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
item: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
secondary='container_items')
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
t_container_items = Table(
'container_items', Base.metadata,
Column('item_id', ForeignKey('simple_items.id')),
Column('container_id', ForeignKey('simple_containers.id'))
)
""",
)
def test_manytomany_selfref(generator: CodeGenerator) -> None:
Table("simple_items", generator.metadata, Column("id", INTEGER, primary_key=True))
Table(
"child_items",
generator.metadata,
Column("parent_id", INTEGER),
Column("child_id", INTEGER),
ForeignKeyConstraint(["parent_id"], ["simple_items.id"]),
ForeignKeyConstraint(["child_id"], ["simple_items.id"]),
schema="otherschema",
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, ForeignKey, Integer, Table
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
parent: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
secondary='otherschema.child_items', primaryjoin=lambda: SimpleItems.id \
== t_child_items.c.child_id, \
secondaryjoin=lambda: SimpleItems.id == \
t_child_items.c.parent_id, back_populates='child')
child: Mapped[list['SimpleItems']] = \
relationship('SimpleItems', secondary='otherschema.child_items', \
primaryjoin=lambda: SimpleItems.id == t_child_items.c.parent_id, \
secondaryjoin=lambda: SimpleItems.id == t_child_items.c.child_id, \
back_populates='parent')
t_child_items = Table(
'child_items', Base.metadata,
Column('parent_id', ForeignKey('simple_items.id')),
Column('child_id', ForeignKey('simple_items.id')),
schema='otherschema'
)
""",
)
def test_manytomany_composite(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id1", INTEGER, primary_key=True),
Column("id2", INTEGER, primary_key=True),
)
Table(
"simple_containers",
generator.metadata,
Column("id1", INTEGER, primary_key=True),
Column("id2", INTEGER, primary_key=True),
)
Table(
"container_items",
generator.metadata,
Column("item_id1", INTEGER),
Column("item_id2", INTEGER),
Column("container_id1", INTEGER),
Column("container_id2", INTEGER),
ForeignKeyConstraint(
["item_id1", "item_id2"], ["simple_items.id1", "simple_items.id2"]
),
ForeignKeyConstraint(
["container_id1", "container_id2"],
["simple_containers.id1", "simple_containers.id2"],
),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, ForeignKeyConstraint, Integer, Table
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id1: Mapped[int] = mapped_column(Integer, primary_key=True)
id2: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
secondary='container_items', back_populates='simple_containers')
class SimpleItems(Base):
__tablename__ = 'simple_items'
id1: Mapped[int] = mapped_column(Integer, primary_key=True)
id2: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_containers: Mapped[list['SimpleContainers']] = \
relationship('SimpleContainers', secondary='container_items', \
back_populates='simple_items')
t_container_items = Table(
'container_items', Base.metadata,
Column('item_id1', Integer),
Column('item_id2', Integer),
Column('container_id1', Integer),
Column('container_id2', Integer),
ForeignKeyConstraint(['container_id1', 'container_id2'], \
['simple_containers.id1', 'simple_containers.id2']),
ForeignKeyConstraint(['item_id1', 'item_id2'], \
['simple_items.id1', 'simple_items.id2'])
)
""",
)
def test_composite_nullable_pk(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id1", INTEGER, primary_key=True),
Column("id2", INTEGER, primary_key=True, nullable=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class SimpleItems(Base):
__tablename__ = 'simple_items'
id1: Mapped[int] = mapped_column(Integer, primary_key=True)
id2: Mapped[Optional[int]] = mapped_column(Integer, primary_key=True, nullable=True)
""",
)
def test_composite_autoincrement_pk(generator: CodeGenerator) -> None:
Table(
"simple_autoincrement_items",
generator.metadata,
Column("id1", INTEGER, primary_key=True, autoincrement=True),
Column("id2", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class SimpleAutoincrementItems(Base):
__tablename__ = 'simple_autoincrement_items'
id1: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
id2: Mapped[int] = mapped_column(Integer, primary_key=True)
""",
)
def test_joined_inheritance(generator: CodeGenerator) -> None:
Table(
"simple_sub_items",
generator.metadata,
Column("simple_items_id", INTEGER, primary_key=True),
Column("data3", INTEGER),
ForeignKeyConstraint(["simple_items_id"], ["simple_items.super_item_id"]),
)
Table(
"simple_super_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("data1", INTEGER),
)
Table(
"simple_items",
generator.metadata,
Column("super_item_id", INTEGER, primary_key=True),
Column("data2", INTEGER),
ForeignKeyConstraint(["super_item_id"], ["simple_super_items.id"]),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class SimpleSuperItems(Base):
__tablename__ = 'simple_super_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
data1: Mapped[Optional[int]] = mapped_column(Integer)
class SimpleItems(SimpleSuperItems):
__tablename__ = 'simple_items'
super_item_id: Mapped[int] = mapped_column(ForeignKey('simple_super_items.id'), \
primary_key=True)
data2: Mapped[Optional[int]] = mapped_column(Integer)
class SimpleSubItems(SimpleItems):
__tablename__ = 'simple_sub_items'
simple_items_id: Mapped[int] = \
mapped_column(ForeignKey('simple_items.super_item_id'), primary_key=True)
data3: Mapped[Optional[int]] = mapped_column(Integer)
""",
)
def test_joined_inheritance_same_table_name(generator: CodeGenerator) -> None:
Table(
"simple",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
Table(
"simple",
generator.metadata,
Column("id", INTEGER, ForeignKey("simple.id"), primary_key=True),
schema="altschema",
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Simple(Base):
__tablename__ = 'simple'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
class Simple_(Simple):
__tablename__ = 'simple'
__table_args__ = {'schema': 'altschema'}
id: Mapped[int] = mapped_column(ForeignKey('simple.id'), primary_key=True)
""",
)
@pytest.mark.parametrize("generator", [["use_inflect"]], indirect=True)
def test_use_inflect(generator: CodeGenerator) -> None:
Table("simple_items", generator.metadata, Column("id", INTEGER, primary_key=True))
Table("singular", generator.metadata, Column("id", INTEGER, primary_key=True))
validate_code(
generator.generate(),
"""\
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class SimpleItem(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
class Singular(Base):
__tablename__ = 'singular'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
""",
)
@pytest.mark.parametrize("generator", [["use_inflect"]], indirect=True)
@pytest.mark.parametrize(
argnames=("table_name", "class_name", "relationship_name"),
argvalues=[
("manufacturers", "manufacturer", "manufacturer"),
("statuses", "status", "status"),
("studies", "study", "study"),
("moose", "moose", "moose"),
],
ids=[
"test_inflect_manufacturer",
"test_inflect_status",
"test_inflect_study",
"test_inflect_moose",
],
)
def test_use_inflect_plural(
generator: CodeGenerator,
table_name: str,
class_name: str,
relationship_name: str,
) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(f"{relationship_name}_id", INTEGER),
ForeignKeyConstraint([f"{relationship_name}_id"], [f"{table_name}.id"]),
UniqueConstraint(f"{relationship_name}_id"),
)
Table(table_name, generator.metadata, Column("id", INTEGER, primary_key=True))
validate_code(
generator.generate(),
f"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class {class_name.capitalize()}(Base):
__tablename__ = '{table_name}'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_item: Mapped[Optional['SimpleItem']] = relationship('SimpleItem', uselist=False, \
back_populates='{relationship_name}')
class SimpleItem(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
{relationship_name}_id: Mapped[Optional[int]] = \
mapped_column(ForeignKey('{table_name}.id'), unique=True)
{relationship_name}: Mapped[Optional['{class_name.capitalize()}']] = \
relationship('{class_name.capitalize()}', back_populates='simple_item')
""",
)
@pytest.mark.parametrize("generator", [["use_inflect"]], indirect=True)
def test_use_inflect_plural_double_pluralize(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("users_id", INTEGER),
Column("groups_id", INTEGER),
ForeignKeyConstraint(
["groups_id"], ["groups.groups_id"], name="fk_users_groups_id"
),
PrimaryKeyConstraint("users_id", name="users_pkey"),
)
Table(
"groups",
generator.metadata,
Column("groups_id", INTEGER),
Column("group_name", Text(50), nullable=False),
PrimaryKeyConstraint("groups_id", name="groups_pkey"),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKeyConstraint, Integer, PrimaryKeyConstraint, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class Group(Base):
__tablename__ = 'groups'
__table_args__ = (
PrimaryKeyConstraint('groups_id', name='groups_pkey'),
)
groups_id: Mapped[int] = mapped_column(Integer, primary_key=True)
group_name: Mapped[str] = mapped_column(Text(50), nullable=False)
users: Mapped[list['User']] = relationship('User', back_populates='group')
class User(Base):
__tablename__ = 'users'
__table_args__ = (
ForeignKeyConstraint(['groups_id'], ['groups.groups_id'], name='fk_users_groups_id'),
PrimaryKeyConstraint('users_id', name='users_pkey')
)
users_id: Mapped[int] = mapped_column(Integer, primary_key=True)
groups_id: Mapped[Optional[int]] = mapped_column(Integer)
group: Mapped[Optional['Group']] = relationship('Group', back_populates='users')
""",
)
def test_table_kwargs(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
schema="testschema",
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class SimpleItems(Base):
__tablename__ = 'simple_items'
__table_args__ = {'schema': 'testschema'}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
""",
)
def test_table_args_kwargs(generator: CodeGenerator) -> None:
simple_items = Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("name", VARCHAR),
schema="testschema",
)
simple_items.indexes.add(Index("testidx", simple_items.c.id, simple_items.c.name))
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Index, Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class SimpleItems(Base):
__tablename__ = 'simple_items'
__table_args__ = (
Index('testidx', 'id', 'name'),
{'schema': 'testschema'}
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[Optional[str]] = mapped_column(String)
""",
)
def test_foreign_key_schema(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("other_item_id", INTEGER),
ForeignKeyConstraint(["other_item_id"], ["otherschema.other_items.id"]),
)
Table(
"other_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
schema="otherschema",
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKey, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class OtherItems(Base):
__tablename__ = 'other_items'
__table_args__ = {'schema': 'otherschema'}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
back_populates='other_item')
class SimpleItems(Base):
__tablename__ = 'simple_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
other_item_id: Mapped[Optional[int]] = \
mapped_column(ForeignKey('otherschema.other_items.id'))
other_item: Mapped[Optional['OtherItems']] = relationship('OtherItems', \
back_populates='simple_items')
""",
)
def test_invalid_attribute_names(generator: CodeGenerator) -> None:
Table(
"simple-items",
generator.metadata,
Column("id-test", INTEGER, primary_key=True),
Column("4test", INTEGER),
Column("_4test", INTEGER),
Column("def", INTEGER),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class SimpleItems(Base):
__tablename__ = 'simple-items'
id_test: Mapped[int] = mapped_column('id-test', Integer, primary_key=True)
_4test: Mapped[Optional[int]] = mapped_column('4test', Integer)
_4test_: Mapped[Optional[int]] = mapped_column('_4test', Integer)
def_: Mapped[Optional[int]] = mapped_column('def', Integer)
""",
)
def test_pascal(generator: CodeGenerator) -> None:
Table(
"CustomerAPIPreference",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class CustomerAPIPreference(Base):
__tablename__ = 'CustomerAPIPreference'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
""",
)
def test_underscore(generator: CodeGenerator) -> None:
Table(
"customer_api_preference",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class CustomerApiPreference(Base):
__tablename__ = 'customer_api_preference'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
""",
)
def test_pascal_underscore(generator: CodeGenerator) -> None:
Table(
"customer_API_Preference",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class CustomerAPIPreference(Base):
__tablename__ = 'customer_API_Preference'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
""",
)
def test_pascal_multiple_underscore(generator: CodeGenerator) -> None:
Table(
"customer_API__Preference",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class CustomerAPIPreference(Base):
__tablename__ = 'customer_API__Preference'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
""",
)
@pytest.mark.parametrize(
"generator, nocomments",
[([], False), (["nocomments"], True)],
indirect=["generator"],
)
def test_column_comment(generator: CodeGenerator, nocomments: bool) -> None:
Table(
"simple",
generator.metadata,
Column("id", INTEGER, primary_key=True, comment="this is a 'comment'"),
)
comment_part = "" if nocomments else ", comment=\"this is a 'comment'\""
validate_code(
generator.generate(),
f"""\
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Simple(Base):
__tablename__ = 'simple'
id: Mapped[int] = mapped_column(Integer, primary_key=True{comment_part})
""",
)
def test_table_comment(generator: CodeGenerator) -> None:
Table(
"simple",
generator.metadata,
Column("id", INTEGER, primary_key=True),
comment="this is a 'comment'",
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Simple(Base):
__tablename__ = 'simple'
__table_args__ = {'comment': "this is a 'comment'"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
""",
)
def test_metadata_column(generator: CodeGenerator) -> None:
Table(
"simple",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("metadata", VARCHAR),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Simple(Base):
__tablename__ = 'simple'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
metadata_: Mapped[Optional[str]] = mapped_column('metadata', String)
""",
)
def test_invalid_variable_name_from_column(generator: CodeGenerator) -> None:
Table(
"simple",
generator.metadata,
Column(" id ", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Simple(Base):
__tablename__ = 'simple'
id: Mapped[int] = mapped_column(' id ', Integer, primary_key=True)
""",
)
def test_only_tables(generator: CodeGenerator) -> None:
Table("simple", generator.metadata, Column("id", INTEGER))
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table
metadata = MetaData()
t_simple = Table(
'simple', metadata,
Column('id', Integer)
)
""",
)
def test_named_constraints(generator: CodeGenerator) -> None:
Table(
"simple",
generator.metadata,
Column("id", INTEGER),
Column("text", VARCHAR),
CheckConstraint("id > 0", name="checktest"),
PrimaryKeyConstraint("id", name="primarytest"),
UniqueConstraint("text", name="uniquetest"),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import CheckConstraint, Integer, PrimaryKeyConstraint, \
String, UniqueConstraint
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Simple(Base):
__tablename__ = 'simple'
__table_args__ = (
CheckConstraint('id > 0', name='checktest'),
PrimaryKeyConstraint('id', name='primarytest'),
UniqueConstraint('text', name='uniquetest')
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
text: Mapped[Optional[str]] = mapped_column(String)
""",
)
def test_named_foreign_key_constraints(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("container_id", INTEGER),
ForeignKeyConstraint(
["container_id"], ["simple_containers.id"], name="foreignkeytest"
),
)
Table(
"simple_containers",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKeyConstraint, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
back_populates='container')
class SimpleItems(Base):
__tablename__ = 'simple_items'
__table_args__ = (
ForeignKeyConstraint(['container_id'], ['simple_containers.id'], \
name='foreignkeytest'),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
container_id: Mapped[Optional[int]] = mapped_column(Integer)
container: Mapped[Optional['SimpleContainers']] = relationship('SimpleContainers', \
back_populates='simple_items')
""",
)
@pytest.mark.parametrize("generator", [["noidsuffix"]], indirect=True)
def test_named_foreign_key_constraints_with_noidsuffix(
generator: CodeGenerator,
) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("container_id", INTEGER),
ForeignKeyConstraint(
["container_id"], ["simple_containers.id"], name="foreignkeytest"
),
)
Table(
"simple_containers",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ForeignKeyConstraint, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class SimpleContainers(Base):
__tablename__ = 'simple_containers'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
simple_items: Mapped[list['SimpleItems']] = relationship('SimpleItems', \
back_populates='simple_containers')
class SimpleItems(Base):
__tablename__ = 'simple_items'
__table_args__ = (
ForeignKeyConstraint(['container_id'], ['simple_containers.id'], \
name='foreignkeytest'),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
container_id: Mapped[Optional[int]] = mapped_column(Integer)
simple_containers: Mapped[Optional['SimpleContainers']] = relationship('SimpleContainers', \
back_populates='simple_items')
""",
)
# @pytest.mark.xfail(strict=True)
def test_colname_import_conflict(generator: CodeGenerator) -> None:
Table(
"simple",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("text", VARCHAR),
Column("textwithdefault", VARCHAR, server_default=text("'test'")),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Integer, String, text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Simple(Base):
__tablename__ = 'simple'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
text_: Mapped[Optional[str]] = mapped_column('text', String)
textwithdefault: Mapped[Optional[str]] = mapped_column(String, \
server_default=text("'test'"))
""",
)
def test_table_with_arrays(generator: CodeGenerator) -> None:
Table(
"with_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("int_items_not_optional", ARRAY(INTEGER()), nullable=False),
Column("str_matrix", ARRAY(VARCHAR(), dimensions=2)),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import ARRAY, INTEGER, Integer, VARCHAR
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class WithItems(Base):
__tablename__ = 'with_items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
int_items_not_optional: Mapped[list[int]] = mapped_column(ARRAY(INTEGER()), nullable=False)
str_matrix: Mapped[Optional[list[list[str]]]] = mapped_column(ARRAY(VARCHAR(), dimensions=2))
""",
)
@pytest.mark.parametrize("engine", ["postgresql"], indirect=["engine"])
def test_domain_json(generator: CodeGenerator) -> None:
Table(
"test_domain_json",
generator.metadata,
Column("id", BIGINT, primary_key=True),
Column(
"foo",
postgresql.DOMAIN(
"domain_json",
JSON,
not_null=False,
),
nullable=True,
),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import BigInteger
from sqlalchemy.dialects.postgresql import DOMAIN, JSON
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class TestDomainJson(Base):
__tablename__ = 'test_domain_json'
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
foo: Mapped[Optional[dict]] = mapped_column(DOMAIN('domain_json', JSON(), not_null=False))
""",
)
@pytest.mark.parametrize(
"domain_type",
[JSONB, JSON],
)
def test_domain_non_default_json(
generator: CodeGenerator,
domain_type: type[JSON] | type[JSONB],
) -> None:
Table(
"test_domain_json",
generator.metadata,
Column("id", BIGINT, primary_key=True),
Column(
"foo",
postgresql.DOMAIN(
"domain_json",
domain_type(astext_type=Text(128)),
not_null=False,
),
nullable=True,
),
)
validate_code(
generator.generate(),
f"""\
from typing import Optional
from sqlalchemy import BigInteger, Text
from sqlalchemy.dialects.postgresql import DOMAIN, {domain_type.__name__}
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class TestDomainJson(Base):
__tablename__ = 'test_domain_json'
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
foo: Mapped[Optional[dict]] = mapped_column(DOMAIN('domain_json', {domain_type.__name__}(astext_type=Text(length=128)), not_null=False))
""",
)
@pytest.mark.skipif(
sys.version_info < (3, 10),
reason="This test assumes GeoAlchemy2 0.18.x and above, which does not support python 3.9",
)
@pytest.mark.parametrize("engine", ["postgresql"], indirect=["engine"])
def test_geoalchemy2_types(generator: CodeGenerator) -> None:
Table(
"spatial_table",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("geom", Geometry("POINT", srid=4326, dimension=2), nullable=False),
Column("geog", Geography("POLYGON", dimension=2)),
)
validate_code(
generator.generate(),
"""\
from typing import Any, Optional
from geoalchemy2.types import Geography, Geometry
from sqlalchemy import Index, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class SpatialTable(Base):
__tablename__ = 'spatial_table'
__table_args__ = (
Index('idx_spatial_table_geog', 'geog', postgresql_using='gist'),
Index('idx_spatial_table_geom', 'geom', postgresql_using='gist')
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
geom: Mapped[Any] = mapped_column(Geometry('POINT', 4326, 2, from_text='ST_GeomFromEWKT', name='geometry', nullable=False), nullable=False)
geog: Mapped[Optional[Any]] = mapped_column(Geography('POLYGON', dimension=2, from_text='ST_GeogFromText', name='geography'))
""",
)
def test_enum_nonativeenums_option(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"status",
SAEnum("active", "inactive", "pending", name="status_enum"),
nullable=False,
),
)
# Recreate generator with nonativeenums option
generator = DeclarativeGenerator(
generator.metadata, generator.bind, ["nonativeenums"]
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Users(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
status: Mapped[str] = mapped_column(Enum('active', 'inactive', 'pending', name='status_enum'), nullable=False)
""",
)
def test_enum_shared_values(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"status",
SAEnum("active", "inactive", "pending", name="status_enum"),
nullable=False,
),
)
Table(
"accounts",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"status",
SAEnum("active", "inactive", "pending", name="status_enum"),
nullable=False,
),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class StatusEnum(str, enum.Enum):
ACTIVE = 'active'
INACTIVE = 'inactive'
PENDING = 'pending'
class Accounts(Base):
__tablename__ = 'accounts'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
status: Mapped[StatusEnum] = mapped_column(Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls], name='status_enum'), nullable=False)
class Users(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
status: Mapped[StatusEnum] = mapped_column(Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls], name='status_enum'), nullable=False)
""",
)
def test_enum_unnamed(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"status",
SAEnum("active", "inactive", "pending"),
nullable=False,
),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class UsersStatus(str, enum.Enum):
ACTIVE = 'active'
INACTIVE = 'inactive'
PENDING = 'pending'
class Users(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
status: Mapped[UsersStatus] = mapped_column(Enum(UsersStatus, values_callable=lambda cls: [member.value for member in cls]), nullable=False)
""",
)
def test_enum_unnamed_reuse_same_values(generator: CodeGenerator) -> None:
# table "a_b", column "c" -> A + B + C = ABC
# table "a", column "b_c" -> A + B + C = ABC
# Both generate same name with same values, so reuse
Table(
"a_b",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"c",
SAEnum("active", "inactive"),
nullable=False,
),
)
Table(
"a",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"b_c",
SAEnum("active", "inactive"),
nullable=False,
),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class ABC(str, enum.Enum):
ACTIVE = 'active'
INACTIVE = 'inactive'
class A(Base):
__tablename__ = 'a'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
b_c: Mapped[ABC] = mapped_column(Enum(ABC, values_callable=lambda cls: [member.value for member in cls]), nullable=False)
class AB(Base):
__tablename__ = 'a_b'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
c: Mapped[ABC] = mapped_column(Enum(ABC, values_callable=lambda cls: [member.value for member in cls]), nullable=False)
""",
)
def test_enum_unnamed_name_collision_different_values(generator: CodeGenerator) -> None:
# table "a_b", column "c" -> A + B + C = ABC
# table "a", column "b_c" -> A + B + C = ABC
# Same name but different values, so append counter
Table(
"a_b",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"c",
SAEnum("active", "inactive"),
nullable=False,
),
)
Table(
"a",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"b_c",
SAEnum("pending", "complete"),
nullable=False,
),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class ABC(str, enum.Enum):
ACTIVE = 'active'
INACTIVE = 'inactive'
class ABC1(str, enum.Enum):
PENDING = 'pending'
COMPLETE = 'complete'
class A(Base):
__tablename__ = 'a'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
b_c: Mapped[ABC1] = mapped_column(Enum(ABC1, values_callable=lambda cls: [member.value for member in cls]), nullable=False)
class AB(Base):
__tablename__ = 'a_b'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
c: Mapped[ABC] = mapped_column(Enum(ABC, values_callable=lambda cls: [member.value for member in cls]), nullable=False)
""",
)
def test_synthetic_enum_generation(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("status", VARCHAR(20), nullable=False),
CheckConstraint("users.status IN ('active', 'inactive', 'pending')"),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import CheckConstraint, Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class UsersStatus(str, enum.Enum):
ACTIVE = 'active'
INACTIVE = 'inactive'
PENDING = 'pending'
class Users(Base):
__tablename__ = 'users'
__table_args__ = (
CheckConstraint("users.status IN ('active', 'inactive', 'pending')"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
status: Mapped[UsersStatus] = mapped_column(Enum(UsersStatus, values_callable=lambda cls: [member.value for member in cls]), nullable=False)
""",
)
def test_synthetic_enum_nosyntheticenums_option(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("status", VARCHAR(20), nullable=False),
CheckConstraint("users.status IN ('active', 'inactive', 'pending')"),
)
# Recreate generator with nosyntheticenums option
generator = DeclarativeGenerator(
generator.metadata, generator.bind, ["nosyntheticenums"]
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import CheckConstraint, Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Users(Base):
__tablename__ = 'users'
__table_args__ = (
CheckConstraint("users.status IN ('active', 'inactive', 'pending')"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
status: Mapped[str] = mapped_column(String(20), nullable=False)
""",
)
def test_synthetic_enum_shared_values(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("status", VARCHAR(20), nullable=False),
CheckConstraint("users.status IN ('active', 'inactive', 'pending')"),
)
Table(
"accounts",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("status", VARCHAR(20), nullable=False),
CheckConstraint("accounts.status IN ('active', 'inactive', 'pending')"),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import CheckConstraint, Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class AccountsStatus(str, enum.Enum):
ACTIVE = 'active'
INACTIVE = 'inactive'
PENDING = 'pending'
class UsersStatus(str, enum.Enum):
ACTIVE = 'active'
INACTIVE = 'inactive'
PENDING = 'pending'
class Accounts(Base):
__tablename__ = 'accounts'
__table_args__ = (
CheckConstraint("accounts.status IN ('active', 'inactive', 'pending')"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
status: Mapped[AccountsStatus] = mapped_column(Enum(AccountsStatus, values_callable=lambda cls: [member.value for member in cls]), nullable=False)
class Users(Base):
__tablename__ = 'users'
__table_args__ = (
CheckConstraint("users.status IN ('active', 'inactive', 'pending')"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
status: Mapped[UsersStatus] = mapped_column(Enum(UsersStatus, values_callable=lambda cls: [member.value for member in cls]), nullable=False)
""",
)
def test_array_enum_named(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"roles",
ARRAY(SAEnum("admin", "user", "moderator", name="role_enum")),
nullable=False,
),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import ARRAY, Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class RoleEnum(str, enum.Enum):
ADMIN = 'admin'
USER = 'user'
MODERATOR = 'moderator'
class Users(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
roles: Mapped[list[RoleEnum]] = mapped_column(ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls], name='role_enum')), nullable=False)
""",
)
def test_array_enum_unnamed(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"roles",
ARRAY(SAEnum("admin", "user")),
nullable=False,
),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import ARRAY, Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class UsersRoles(str, enum.Enum):
ADMIN = 'admin'
USER = 'user'
class Users(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
roles: Mapped[list[UsersRoles]] = mapped_column(ARRAY(Enum(UsersRoles, values_callable=lambda cls: [member.value for member in cls])), nullable=False)
""",
)
def test_array_enum_nullable(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"roles",
ARRAY(SAEnum("admin", "user", name="role_enum")),
),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
import enum
from sqlalchemy import ARRAY, Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class RoleEnum(str, enum.Enum):
ADMIN = 'admin'
USER = 'user'
class Users(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
roles: Mapped[Optional[list[RoleEnum]]] = mapped_column(ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls], name='role_enum')))
""",
)
def test_array_enum_with_dimensions(generator: CodeGenerator) -> None:
Table(
"items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"tag_matrix",
ARRAY(SAEnum("a", "b", name="tag_enum"), dimensions=2),
nullable=False,
),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import ARRAY, Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class TagEnum(str, enum.Enum):
A = 'a'
B = 'b'
class Items(Base):
__tablename__ = 'items'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
tag_matrix: Mapped[list[list[TagEnum]]] = mapped_column(ARRAY(Enum(TagEnum, values_callable=lambda cls: [member.value for member in cls], name='tag_enum'), dimensions=2), nullable=False)
""",
)
def test_array_enum_nonativeenums_option(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"roles",
ARRAY(SAEnum("admin", "user", name="role_enum")),
nullable=False,
),
)
generator = DeclarativeGenerator(
generator.metadata, generator.bind, ["nonativeenums"]
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import ARRAY, Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Users(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
roles: Mapped[list[str]] = mapped_column(ARRAY(Enum('admin', 'user', name='role_enum')), nullable=False)
""",
)
def test_array_enum_shared_with_regular_enum(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"primary_role",
SAEnum("admin", "user", name="role_enum"),
nullable=False,
),
Column(
"all_roles",
ARRAY(SAEnum("admin", "user", name="role_enum")),
nullable=False,
),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import ARRAY, Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class RoleEnum(str, enum.Enum):
ADMIN = 'admin'
USER = 'user'
class Users(Base):
__tablename__ = 'users'
id: Mapped[int] = mapped_column(Integer, primary_key=True)
primary_role: Mapped[RoleEnum] = mapped_column(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls], name='role_enum'), nullable=False)
all_roles: Mapped[list[RoleEnum]] = mapped_column(ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls], name='role_enum')), nullable=False)
""",
)
def test_enum_named_with_schema(generator: CodeGenerator) -> None:
Table(
"my_table",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"status",
SAEnum("active", "inactive", name="status_enum", schema="custom_schema"),
nullable=False,
),
schema="custom_schema",
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class StatusEnum(str, enum.Enum):
ACTIVE = 'active'
INACTIVE = 'inactive'
class MyTable(Base):
__tablename__ = 'my_table'
__table_args__ = {'schema': 'custom_schema'}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
status: Mapped[StatusEnum] = mapped_column(Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls], name='status_enum', schema='custom_schema'), nullable=False)
""",
)
def test_array_enum_named_with_schema(generator: CodeGenerator) -> None:
Table(
"my_table",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"tags",
ARRAY(SAEnum("a", "b", name="tag_enum", schema="custom_schema")),
nullable=False,
),
schema="custom_schema",
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import ARRAY, Enum, Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class TagEnum(str, enum.Enum):
A = 'a'
B = 'b'
class MyTable(Base):
__tablename__ = 'my_table'
__table_args__ = {'schema': 'custom_schema'}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
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)
""",
)
def test_index_with_empty_kwargs(generator: CodeGenerator) -> None:
simple_items = Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("name", VARCHAR),
)
simple_items.indexes.add(
Index(
"idx_name",
simple_items.c.name,
postgresql_using="gist",
postgresql_include=[],
)
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Index, Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class SimpleItems(Base):
__tablename__ = 'simple_items'
__table_args__ = (
Index('idx_name', 'name', postgresql_using='gist'),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[Optional[str]] = mapped_column(String)
""",
)
@pytest.mark.parametrize("generator", [["include_dialect_options"]], indirect=True)
def test_include_dialect_options_empty_values_skipped(
generator: CodeGenerator,
) -> None:
Table(
"t_opts3",
generator.metadata,
Column("id", INTEGER, primary_key=True),
mysql_engine="InnoDB",
mysql_partition_by=[],
mysql_PROPERTIES={},
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Integer
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class TOpts3(Base):
__tablename__ = 't_opts3'
__table_args__ = {'mysql_engine': 'InnoDB'}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
""",
)
agronholm-sqlacodegen-1b321d1/tests/test_generator_sqlmodel.py 0000664 0000000 0000000 00000037500 15215155472 0024757 0 ustar 00root root 0000000 0000000 from __future__ import annotations
import pytest
from _pytest.fixtures import FixtureRequest
from sqlalchemy import Enum as SAEnum
from sqlalchemy import Uuid
from sqlalchemy.engine import Engine
from sqlalchemy.schema import (
CheckConstraint,
Column,
ForeignKeyConstraint,
Index,
MetaData,
Table,
UniqueConstraint,
)
from sqlalchemy.types import ARRAY, INTEGER, VARCHAR
from sqlacodegen.generators import CodeGenerator, SQLModelGenerator
from .conftest import validate_code
@pytest.fixture
def generator(
request: FixtureRequest, metadata: MetaData, engine: Engine
) -> CodeGenerator:
options = getattr(request, "param", [])
return SQLModelGenerator(metadata, engine, options)
def test_indexes(generator: CodeGenerator) -> None:
simple_items = Table(
"item",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("number", INTEGER, nullable=False),
Column("text", VARCHAR),
)
simple_items.indexes.add(Index("idx_number", simple_items.c.number))
simple_items.indexes.add(
Index("idx_text_number", simple_items.c.text, simple_items.c.number)
)
simple_items.indexes.add(Index("idx_text", simple_items.c.text, unique=True))
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Column, Index, Integer, String
from sqlmodel import Field, SQLModel
class Item(SQLModel, table=True):
__table_args__ = (
Index('idx_number', 'number'),
Index('idx_text', 'text', unique=True),
Index('idx_text_number', 'text', 'number')
)
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
number: int = Field(sa_column=Column(\
'number', Integer, nullable=False))
text: Optional[str] = Field(default=None, sa_column=Column(\
'text', String))
""",
)
def test_constraints(generator: CodeGenerator) -> None:
Table(
"simple_constraints",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("number", INTEGER),
CheckConstraint("number > 0"),
UniqueConstraint("id", "number"),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import CheckConstraint, Column, Integer, UniqueConstraint
from sqlmodel import Field, SQLModel
class SimpleConstraints(SQLModel, table=True):
__tablename__ = 'simple_constraints'
__table_args__ = (
CheckConstraint('number > 0'),
UniqueConstraint('id', 'number')
)
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
number: Optional[int] = Field(default=None, sa_column=Column(\
'number', Integer))
""",
)
def test_onetomany(generator: CodeGenerator) -> None:
Table(
"simple_goods",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("container_id", INTEGER),
ForeignKeyConstraint(["container_id"], ["simple_containers.id"]),
)
Table(
"simple_containers",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, Relationship, SQLModel
class SimpleContainers(SQLModel, table=True):
__tablename__ = 'simple_containers'
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
simple_goods: list['SimpleGoods'] = Relationship(\
back_populates='container')
class SimpleGoods(SQLModel, table=True):
__tablename__ = 'simple_goods'
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
container_id: Optional[int] = Field(default=None, sa_column=Column(\
'container_id', ForeignKey('simple_containers.id')))
container: Optional['SimpleContainers'] = Relationship(\
back_populates='simple_goods')
""",
)
def test_onetomany_multiref(generator: CodeGenerator) -> None:
Table(
"simple_items_multiref",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("parent_container_id", INTEGER),
Column("top_container_id", INTEGER, nullable=False),
ForeignKeyConstraint(
["parent_container_id"], ["simple_containers_multiref.id"]
),
ForeignKeyConstraint(["top_container_id"], ["simple_containers_multiref.id"]),
)
Table(
"simple_containers_multiref",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, Relationship, SQLModel
class SimpleContainersMultiref(SQLModel, table=True):
__tablename__ = 'simple_containers_multiref'
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
simple_items_multiref_parent_container: list['SimpleItemsMultiref'] = \
Relationship(back_populates='parent_container', sa_relationship_kwargs={\
'foreign_keys': '[SimpleItemsMultiref.parent_container_id]'})
simple_items_multiref_top_container: list['SimpleItemsMultiref'] = \
Relationship(back_populates='top_container', sa_relationship_kwargs={'foreign_keys': \
'[SimpleItemsMultiref.top_container_id]'})
class SimpleItemsMultiref(SQLModel, table=True):
__tablename__ = 'simple_items_multiref'
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
top_container_id: int = \
Field(sa_column=Column('top_container_id', \
ForeignKey('simple_containers_multiref.id'), nullable=False))
parent_container_id: Optional[int] = \
Field(default=None, sa_column=Column('parent_container_id', \
ForeignKey('simple_containers_multiref.id')))
parent_container: Optional['SimpleContainersMultiref'] = Relationship(\
back_populates='simple_items_multiref_parent_container', sa_relationship_kwargs={\
'foreign_keys': '[SimpleItemsMultiref.parent_container_id]'})
top_container: 'SimpleContainersMultiref' = Relationship(\
back_populates='simple_items_multiref_top_container', sa_relationship_kwargs={\
'foreign_keys': '[SimpleItemsMultiref.top_container_id]'})
""",
)
def test_onetoone(generator: CodeGenerator) -> None:
Table(
"simple_onetoone",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("other_item_id", INTEGER),
ForeignKeyConstraint(["other_item_id"], ["other_items.id"]),
UniqueConstraint("other_item_id"),
)
Table("other_items", generator.metadata, Column("id", INTEGER, primary_key=True))
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, Relationship, SQLModel
class OtherItems(SQLModel, table=True):
__tablename__ = 'other_items'
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
simple_onetoone: Optional['SimpleOnetoone'] = Relationship(\
back_populates='other_item', sa_relationship_kwargs={'uselist': False})
class SimpleOnetoone(SQLModel, table=True):
__tablename__ = 'simple_onetoone'
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
other_item_id: Optional[int] = Field(default=None, sa_column=Column(\
'other_item_id', ForeignKey('other_items.id'), unique=True))
other_item: Optional['OtherItems'] = Relationship(\
back_populates='simple_onetoone')
""",
)
def test_uuid(generator: CodeGenerator) -> None:
Table(
"simple_uuid",
generator.metadata,
Column("id", Uuid, primary_key=True),
)
validate_code(
generator.generate(),
"""\
import uuid
from sqlalchemy import Column, Uuid
from sqlmodel import Field, SQLModel
class SimpleUuid(SQLModel, table=True):
__tablename__ = 'simple_uuid'
id: uuid.UUID = Field(sa_column=Column('id', Uuid, primary_key=True))
""",
)
def test_check_constraint_not_converted_to_enum(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("status", VARCHAR(20), nullable=False),
CheckConstraint("users.status IN ('active', 'inactive', 'pending')"),
)
# Recreate generator with nosyntheticenums option to preserve constraints
generator = SQLModelGenerator(
generator.metadata, generator.bind, ["nosyntheticenums"]
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import CheckConstraint, Column, Integer, String
from sqlmodel import Field, SQLModel
class Users(SQLModel, table=True):
__table_args__ = (
CheckConstraint("users.status IN ('active', 'inactive', 'pending')"),
)
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
status: str = Field(sa_column=Column('status', String(20), nullable=False))
""",
)
def test_synthetic_enum_generation(generator: CodeGenerator) -> None:
Table(
"accounts",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("status", VARCHAR(20), nullable=False),
CheckConstraint("accounts.status IN ('active', 'inactive', 'pending')"),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import CheckConstraint, Column, Enum, Integer
from sqlmodel import Field, SQLModel
class AccountsStatus(str, enum.Enum):
ACTIVE = 'active'
INACTIVE = 'inactive'
PENDING = 'pending'
class Accounts(SQLModel, table=True):
__table_args__ = (
CheckConstraint("accounts.status IN ('active', 'inactive', 'pending')"),
)
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
status: AccountsStatus = Field(sa_column=Column('status', Enum(AccountsStatus, values_callable=lambda cls: [member.value for member in cls]), nullable=False))
""",
)
def test_array_enum_named_with_schema(generator: CodeGenerator) -> None:
Table(
"my_table",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"tags",
ARRAY(SAEnum("a", "b", name="tag_enum", schema="custom_schema")),
nullable=False,
),
schema="custom_schema",
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import ARRAY, Column, Enum, Integer
from sqlmodel import Field, SQLModel
class TagEnum(str, enum.Enum):
A = 'a'
B = 'b'
class MyTable(SQLModel, table=True):
__tablename__ = 'my_table'
__table_args__ = {'schema': 'custom_schema'}
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
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))
""",
)
def test_fallback_table(generator: CodeGenerator) -> None:
Table(
"simple_fallback",
generator.metadata,
Column("field", VARCHAR(20), nullable=False),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, String, Table
from sqlmodel import SQLModel
t_simple_fallback = Table(
'simple_fallback', SQLModel.metadata,
Column('field', String(20), nullable=False)
)
""",
)
def test_onetomany_selfref(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("parent_item_id", INTEGER),
ForeignKeyConstraint(["parent_item_id"], ["simple_items.id"]),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, Relationship, SQLModel
class SimpleItems(SQLModel, table=True):
__tablename__ = 'simple_items'
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
parent_item_id: Optional[int] = Field(default=None, sa_column=Column(\
'parent_item_id', ForeignKey('simple_items.id')))
parent_item: Optional['SimpleItems'] = Relationship(\
back_populates='parent_item_reverse', sa_relationship_kwargs={\
'remote_side': '[SimpleItems.id]'})
parent_item_reverse: list['SimpleItems'] = Relationship(\
back_populates='parent_item', sa_relationship_kwargs={\
'remote_side': '[SimpleItems.parent_item_id]'})
""",
)
def test_onetomany_selfref_multi(generator: CodeGenerator) -> None:
Table(
"simple_items_selfref",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("parent_item_id", INTEGER),
Column("top_item_id", INTEGER),
ForeignKeyConstraint(["parent_item_id"], ["simple_items_selfref.id"]),
ForeignKeyConstraint(["top_item_id"], ["simple_items_selfref.id"]),
)
validate_code(
generator.generate(),
"""\
from typing import Optional
from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, Relationship, SQLModel
class SimpleItemsSelfref(SQLModel, table=True):
__tablename__ = 'simple_items_selfref'
id: int = Field(sa_column=Column('id', Integer, primary_key=True))
parent_item_id: Optional[int] = Field(default=None, sa_column=Column(\
'parent_item_id', ForeignKey('simple_items_selfref.id')))
top_item_id: Optional[int] = Field(default=None, sa_column=Column(\
'top_item_id', ForeignKey('simple_items_selfref.id')))
parent_item: Optional['SimpleItemsSelfref'] = Relationship(\
back_populates='parent_item_reverse', sa_relationship_kwargs={\
'remote_side': '[SimpleItemsSelfref.id]', \
'foreign_keys': '[SimpleItemsSelfref.parent_item_id]'})
parent_item_reverse: list['SimpleItemsSelfref'] = Relationship(\
back_populates='parent_item', sa_relationship_kwargs={\
'remote_side': '[SimpleItemsSelfref.parent_item_id]', \
'foreign_keys': '[SimpleItemsSelfref.parent_item_id]'})
top_item: Optional['SimpleItemsSelfref'] = Relationship(\
back_populates='top_item_reverse', sa_relationship_kwargs={\
'remote_side': '[SimpleItemsSelfref.id]', \
'foreign_keys': '[SimpleItemsSelfref.top_item_id]'})
top_item_reverse: list['SimpleItemsSelfref'] = Relationship(\
back_populates='top_item', sa_relationship_kwargs={\
'remote_side': '[SimpleItemsSelfref.top_item_id]', \
'foreign_keys': '[SimpleItemsSelfref.top_item_id]'})
""",
)
agronholm-sqlacodegen-1b321d1/tests/test_generator_tables.py 0000664 0000000 0000000 00000115565 15215155472 0024421 0 ustar 00root root 0000000 0000000 from __future__ import annotations
from decimal import Decimal
from textwrap import dedent
import pytest
from _pytest.fixtures import FixtureRequest
from sqlalchemy import Enum as SAEnum
from sqlalchemy import TypeDecorator
from sqlalchemy.dialects import mysql, postgresql, registry
from sqlalchemy.dialects.mysql.pymysql import MySQLDialect_pymysql
from sqlalchemy.engine import Engine
from sqlalchemy.schema import (
CheckConstraint,
Column,
Computed,
ForeignKey,
Identity,
Index,
MetaData,
Table,
UniqueConstraint,
)
from sqlalchemy.sql.expression import text
from sqlalchemy.sql.sqltypes import DateTime, NullType
from sqlalchemy.types import ARRAY, INTEGER, NUMERIC, SMALLINT, VARCHAR, Text
from sqlacodegen.generators import CodeGenerator, TablesGenerator
from .conftest import validate_code
# This needs to be uppercased to trigger #315
class TIMESTAMP_DECORATOR(TypeDecorator[DateTime]):
impl = DateTime
@pytest.fixture
def generator(
request: FixtureRequest, metadata: MetaData, engine: Engine
) -> CodeGenerator:
options = getattr(request, "param", [])
return TablesGenerator(metadata, engine, options)
@pytest.mark.parametrize("engine", ["postgresql"], indirect=["engine"])
def test_fancy_coltypes(generator: CodeGenerator) -> None:
from pgvector.sqlalchemy.vector import VECTOR
Table(
"simple_items",
generator.metadata,
Column("enum", postgresql.ENUM("A", "B", name="blah", schema="someschema")),
Column("bool", postgresql.BOOLEAN),
Column("vector", VECTOR(3)),
Column("number", NUMERIC(10, asdecimal=False)),
Column("timestamp", TIMESTAMP_DECORATOR()),
schema="someschema",
)
validate_code(
generator.generate(),
"""\
from tests.test_generator_tables import TIMESTAMP_DECORATOR
import enum
from pgvector.sqlalchemy.vector import VECTOR
from sqlalchemy import Boolean, Column, Enum, MetaData, Numeric, Table
metadata = MetaData()
class Blah(str, enum.Enum):
A = 'A'
B = 'B'
t_simple_items = Table(
'simple_items', metadata,
Column('enum', Enum(Blah, values_callable=lambda cls: [member.value for member in cls], name='blah', schema='someschema')),
Column('bool', Boolean),
Column('vector', VECTOR(3)),
Column('number', Numeric(10, asdecimal=False)),
Column('timestamp', TIMESTAMP_DECORATOR),
schema='someschema'
)
""",
)
def test_boolean_detection(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("bool1", INTEGER),
Column("bool2", SMALLINT),
Column("bool3", mysql.TINYINT),
CheckConstraint("simple_items.bool1 IN (0, 1)"),
CheckConstraint("simple_items.bool2 IN (0, 1)"),
CheckConstraint("simple_items.bool3 IN (0, 1)"),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Boolean, Column, MetaData, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('bool1', Boolean),
Column('bool2', Boolean),
Column('bool3', Boolean)
)
""",
)
@pytest.mark.parametrize("engine", ["postgresql"], indirect=["engine"])
def test_arrays(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("dp_array", postgresql.ARRAY(postgresql.DOUBLE_PRECISION(precision=53))),
Column("int_array", postgresql.ARRAY(INTEGER)),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Double, Integer, MetaData, Table
from sqlalchemy.dialects.postgresql import ARRAY
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('dp_array', ARRAY(Double(precision=53))),
Column('int_array', ARRAY(Integer()))
)
""",
)
@pytest.mark.parametrize("engine", ["postgresql"], indirect=["engine"])
def test_array_preserves_dialect_for_runtime_operators(
generator: CodeGenerator,
) -> None:
"""Regression test for GH-441."""
Table(
"simple_items",
generator.metadata,
Column("id", postgresql.TEXT, primary_key=True),
Column("tags", postgresql.ARRAY(postgresql.TEXT)),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, MetaData, Table, Text
from sqlalchemy.dialects.postgresql import ARRAY
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Text, primary_key=True),
Column('tags', ARRAY(Text()))
)
""",
)
@pytest.mark.parametrize("engine", ["postgresql"], indirect=["engine"])
def test_jsonb(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("jsonb", postgresql.JSONB(astext_type=Text(50))),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, MetaData, Table, Text
from sqlalchemy.dialects.postgresql import JSONB
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('jsonb', JSONB(astext_type=Text(length=50)))
)
""",
)
@pytest.mark.parametrize("engine", ["postgresql"], indirect=["engine"])
def test_jsonb_default(generator: CodeGenerator) -> None:
Table("simple_items", generator.metadata, Column("jsonb", postgresql.JSONB))
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, MetaData, Table
from sqlalchemy.dialects.postgresql import JSONB
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('jsonb', JSONB)
)
""",
)
@pytest.mark.parametrize("engine", ["postgresql"], indirect=["engine"])
def test_json_default(generator: CodeGenerator) -> None:
Table("simple_items", generator.metadata, Column("json", postgresql.JSON))
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, JSON, MetaData, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('json', JSON)
)
""",
)
def test_check_constraint_preserved(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("enum", VARCHAR(255)),
CheckConstraint(r"simple_items.enum IN ('A', '\'B', 'C')"),
)
# Recreate generator with nosyntheticenums option to preserve constraints
generator = TablesGenerator(
generator.metadata, generator.bind, ["nosyntheticenums"]
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import CheckConstraint, Column, MetaData, String, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('enum', String(255)),
CheckConstraint("simple_items.enum IN ('A', '\\\\'B', 'C')")
)
""",
)
def test_synthetic_enum_generation(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("status", VARCHAR(20)),
CheckConstraint("simple_items.status IN ('active', 'inactive', 'pending')"),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import CheckConstraint, Column, Enum, Integer, MetaData, Table
metadata = MetaData()
class SimpleItemsStatus(str, enum.Enum):
ACTIVE = 'active'
INACTIVE = 'inactive'
PENDING = 'pending'
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer, primary_key=True),
Column('status', Enum(SimpleItemsStatus, values_callable=lambda cls: [member.value for member in cls])),
CheckConstraint("simple_items.status IN ('active', 'inactive', 'pending')")
)
""",
)
def test_enum_shared_values(generator: CodeGenerator) -> None:
from sqlalchemy import Enum as SAEnum
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("status", SAEnum("active", "inactive", "pending", name="status_enum")),
)
Table(
"accounts",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("status", SAEnum("active", "inactive", "pending", name="status_enum")),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import Column, Enum, Integer, MetaData, Table
metadata = MetaData()
class StatusEnum(str, enum.Enum):
ACTIVE = 'active'
INACTIVE = 'inactive'
PENDING = 'pending'
t_accounts = Table(
'accounts', metadata,
Column('id', Integer, primary_key=True),
Column('status', Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls], name='status_enum'))
)
t_users = Table(
'users', metadata,
Column('id', Integer, primary_key=True),
Column('status', Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls], name='status_enum'))
)
""",
)
def test_array_enum_named(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("roles", ARRAY(SAEnum("admin", "user", "moderator", name="role_enum"))),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import ARRAY, Column, Enum, Integer, MetaData, Table
metadata = MetaData()
class RoleEnum(str, enum.Enum):
ADMIN = 'admin'
USER = 'user'
MODERATOR = 'moderator'
t_users = Table(
'users', metadata,
Column('id', Integer, primary_key=True),
Column('roles', ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls], name='role_enum')))
)
""",
)
def test_array_enum_shared(generator: CodeGenerator) -> None:
Table(
"users",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("roles", ARRAY(SAEnum("admin", "user", name="role_enum"))),
)
Table(
"groups",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("allowed_roles", ARRAY(SAEnum("admin", "user", name="role_enum"))),
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import ARRAY, Column, Enum, Integer, MetaData, Table
metadata = MetaData()
class RoleEnum(str, enum.Enum):
ADMIN = 'admin'
USER = 'user'
t_groups = Table(
'groups', metadata,
Column('id', Integer, primary_key=True),
Column('allowed_roles', ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls], name='role_enum')))
)
t_users = Table(
'users', metadata,
Column('id', Integer, primary_key=True),
Column('roles', ARRAY(Enum(RoleEnum, values_callable=lambda cls: [member.value for member in cls], name='role_enum')))
)
""",
)
def test_enum_named_with_schema(generator: CodeGenerator) -> None:
Table(
"my_table",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"status",
SAEnum("active", "inactive", name="status_enum", schema="custom_schema"),
),
schema="custom_schema",
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import Column, Enum, Integer, MetaData, Table
metadata = MetaData()
class StatusEnum(str, enum.Enum):
ACTIVE = 'active'
INACTIVE = 'inactive'
t_my_table = Table(
'my_table', metadata,
Column('id', Integer, primary_key=True),
Column('status', Enum(StatusEnum, values_callable=lambda cls: [member.value for member in cls], name='status_enum', schema='custom_schema')),
schema='custom_schema'
)
""",
)
def test_array_enum_named_with_schema(generator: CodeGenerator) -> None:
Table(
"my_table",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column(
"tags",
ARRAY(SAEnum("a", "b", name="tag_enum", schema="custom_schema")),
),
schema="custom_schema",
)
validate_code(
generator.generate(),
"""\
import enum
from sqlalchemy import ARRAY, Column, Enum, Integer, MetaData, Table
metadata = MetaData()
class TagEnum(str, enum.Enum):
A = 'a'
B = 'b'
t_my_table = Table(
'my_table', metadata,
Column('id', Integer, primary_key=True),
Column('tags', ARRAY(Enum(TagEnum, values_callable=lambda cls: [member.value for member in cls], name='tag_enum', schema='custom_schema'))),
schema='custom_schema'
)
""",
)
@pytest.mark.parametrize("engine", ["postgresql"], indirect=["engine"])
def test_domain_text(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column(
"postal_code",
postgresql.DOMAIN(
"us_postal_code",
Text,
constraint_name="valid_us_postal_code",
not_null=False,
check=text("VALUE ~ '^\\d{5}$' OR VALUE ~ '^\\d{5}-\\d{4}$'"),
),
nullable=False,
),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, MetaData, Table, Text, text
from sqlalchemy.dialects.postgresql import DOMAIN
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('postal_code', DOMAIN('us_postal_code', Text(), \
constraint_name='valid_us_postal_code', not_null=False, \
check=text("VALUE ~ '^\\\\d{5}$' OR VALUE ~ '^\\\\d{5}-\\\\d{4}$'")), nullable=False)
)
""",
)
@pytest.mark.parametrize("engine", ["postgresql"], indirect=["engine"])
def test_domain_int(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column(
"n",
postgresql.DOMAIN(
"positive_int",
INTEGER,
constraint_name="positive",
not_null=False,
check=text("VALUE > 0"),
),
nullable=False,
),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, INTEGER, MetaData, Table, text
from sqlalchemy.dialects.postgresql import DOMAIN
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('n', DOMAIN('positive_int', INTEGER(), \
constraint_name='positive', not_null=False, \
check=text('VALUE > 0')), nullable=False)
)
""",
)
@pytest.mark.parametrize("engine", ["postgresql"], indirect=["engine"])
def test_column_adaptation(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", postgresql.BIGINT),
Column("length", postgresql.DOUBLE_PRECISION),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import BigInteger, Column, Double, MetaData, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', BigInteger),
Column('length', Double)
)
""",
)
@pytest.mark.parametrize("engine", ["mysql"], indirect=["engine"])
def test_mysql_column_types(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", mysql.INTEGER),
Column("name", mysql.VARCHAR(255)),
Column("double", mysql.DOUBLE(1, 2)),
Column("set", mysql.SET("one", "two")),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, String, Table
from sqlalchemy.dialects.mysql import DOUBLE, SET
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer),
Column('name', String(255)),
Column('double', DOUBLE(1, 2)),
Column('set', SET('one', 'two'))
)
""",
)
@pytest.mark.parametrize("engine", ["mysql"], indirect=["engine"])
@pytest.mark.parametrize("generator", [["keep_dialect_types"]], indirect=True)
def test_mysql_char_collation_keep_dialect_types(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", mysql.INTEGER, primary_key=True),
Column("result_code", mysql.CHAR(1, collation="utf8mb3_bin"), nullable=False),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, MetaData, Table
from sqlalchemy.dialects.mysql import CHAR, INTEGER
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', INTEGER, primary_key=True),
Column('result_code', CHAR(1, collation='utf8mb3_bin'), nullable=False)
)
""",
)
def test_constraints(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER),
Column("number", INTEGER),
CheckConstraint("number > 0"),
UniqueConstraint("id", "number"),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import CheckConstraint, Column, Integer, MetaData, Table, \
UniqueConstraint
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer),
Column('number', Integer),
CheckConstraint('number > 0'),
UniqueConstraint('id', 'number')
)
""",
)
def test_indexes(generator: CodeGenerator) -> None:
simple_items = Table(
"simple_items",
generator.metadata,
Column("id", INTEGER),
Column("number", INTEGER),
Column("text", VARCHAR),
Index("ix_empty"),
)
simple_items.indexes.add(Index("ix_number", simple_items.c.number))
simple_items.indexes.add(
Index(
"ix_text_number",
simple_items.c.text,
simple_items.c.number,
unique=True,
)
)
simple_items.indexes.add(Index("ix_text", simple_items.c.text, unique=True))
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Index, Integer, MetaData, String, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer),
Column('number', Integer, index=True),
Column('text', String, unique=True, index=True),
Index('ix_empty'),
Index('ix_text_number', 'text', 'number', unique=True)
)
""",
)
def test_table_comment(generator: CodeGenerator) -> None:
Table(
"simple",
generator.metadata,
Column("id", INTEGER, primary_key=True),
comment="this is a 'comment'",
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table
metadata = MetaData()
t_simple = Table(
'simple', metadata,
Column('id', Integer, primary_key=True),
comment="this is a 'comment'"
)
""",
)
def test_table_name_identifiers(generator: CodeGenerator) -> None:
Table(
"simple-items table",
generator.metadata,
Column("id", INTEGER, primary_key=True),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table
metadata = MetaData()
t_simple_items_table = Table(
'simple-items table', metadata,
Column('id', Integer, primary_key=True)
)
""",
)
@pytest.mark.parametrize("generator", [["noindexes"]], indirect=True)
def test_option_noindexes(generator: CodeGenerator) -> None:
simple_items = Table(
"simple_items",
generator.metadata,
Column("number", INTEGER),
CheckConstraint("number > 2"),
)
simple_items.indexes.add(Index("idx_number", simple_items.c.number))
validate_code(
generator.generate(),
"""\
from sqlalchemy import CheckConstraint, Column, Integer, MetaData, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('number', Integer),
CheckConstraint('number > 2')
)
""",
)
@pytest.mark.parametrize("generator", [["noconstraints"]], indirect=True)
def test_option_noconstraints(generator: CodeGenerator) -> None:
simple_items = Table(
"simple_items",
generator.metadata,
Column("number", INTEGER),
CheckConstraint("number > 2"),
)
simple_items.indexes.add(Index("ix_number", simple_items.c.number))
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('number', Integer, index=True)
)
""",
)
@pytest.mark.parametrize("generator", [["nocomments"]], indirect=True)
def test_option_nocomments(generator: CodeGenerator) -> None:
Table(
"simple",
generator.metadata,
Column("id", INTEGER, primary_key=True, comment="pk column comment"),
comment="this is a 'comment'",
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table
metadata = MetaData()
t_simple = Table(
'simple', metadata,
Column('id', Integer, primary_key=True)
)
""",
)
@pytest.mark.parametrize(
"persisted, extra_args",
[(None, ""), (False, ", persisted=False"), (True, ", persisted=True")],
)
def test_computed_column(
generator: CodeGenerator, persisted: bool | None, extra_args: str
) -> None:
Table(
"computed",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("computed", INTEGER, Computed("1 + 2", persisted=persisted)),
)
validate_code(
generator.generate(),
f"""\
from sqlalchemy import Column, Computed, Integer, MetaData, Table
metadata = MetaData()
t_computed = Table(
'computed', metadata,
Column('id', Integer, primary_key=True),
Column('computed', Integer, Computed('1 + 2'{extra_args}))
)
""",
)
def test_schema(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("name", VARCHAR),
schema="testschema",
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, MetaData, String, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('name', String),
schema='testschema'
)
""",
)
def test_foreign_key_options(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column(
"name",
VARCHAR,
ForeignKey(
"simple_items.name",
ondelete="CASCADE",
onupdate="CASCADE",
deferrable=True,
initially="DEFERRED",
),
),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, ForeignKey, MetaData, String, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('name', String, ForeignKey('simple_items.name', \
ondelete='CASCADE', onupdate='CASCADE', deferrable=True, initially='DEFERRED'))
)
""",
)
def test_pk_default(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column(
"id",
INTEGER,
primary_key=True,
server_default=text("uuid_generate_v4()"),
),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table, text
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer, primary_key=True, \
server_default=text('uuid_generate_v4()'))
)
""",
)
@pytest.mark.parametrize("engine", ["mysql"], indirect=["engine"])
def test_mysql_timestamp(generator: CodeGenerator) -> None:
Table(
"simple",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("timestamp", mysql.TIMESTAMP),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, TIMESTAMP, Table
metadata = MetaData()
t_simple = Table(
'simple', metadata,
Column('id', Integer, primary_key=True),
Column('timestamp', TIMESTAMP)
)
""",
)
@pytest.mark.parametrize("engine", ["mysql"], indirect=["engine"])
def test_mysql_integer_display_width(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("number", mysql.INTEGER(11)),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table
from sqlalchemy.dialects.mysql import INTEGER
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer, primary_key=True),
Column('number', INTEGER(11))
)
""",
)
@pytest.mark.parametrize("engine", ["mysql"], indirect=["engine"])
def test_mysql_tinytext(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("my_tinytext", mysql.TINYTEXT),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table
from sqlalchemy.dialects.mysql import TINYTEXT
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer, primary_key=True),
Column('my_tinytext', TINYTEXT)
)
""",
)
@pytest.mark.parametrize("engine", ["mysql"], indirect=["engine"])
def test_mysql_mediumtext(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("my_mediumtext", mysql.MEDIUMTEXT),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table
from sqlalchemy.dialects.mysql import MEDIUMTEXT
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer, primary_key=True),
Column('my_mediumtext', MEDIUMTEXT)
)
""",
)
@pytest.mark.parametrize("engine", ["mysql"], indirect=["engine"])
def test_mysql_longtext(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, primary_key=True),
Column("my_longtext", mysql.LONGTEXT),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table
from sqlalchemy.dialects.mysql import LONGTEXT
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer, primary_key=True),
Column('my_longtext', LONGTEXT)
)
""",
)
def test_schema_boolean(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("bool1", INTEGER),
CheckConstraint("testschema.simple_items.bool1 IN (0, 1)"),
schema="testschema",
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Boolean, Column, MetaData, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('bool1', Boolean),
schema='testschema'
)
""",
)
def test_server_default_multiline(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column(
"id",
INTEGER,
primary_key=True,
server_default=text(
dedent(
"""\
/*Comment*/
/*Next line*/
something()"""
)
),
),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table, text
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer, primary_key=True, server_default=\
text('/*Comment*/\\n/*Next line*/\\nsomething()'))
)
""",
)
def test_server_default_colon(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("problem", VARCHAR, server_default=text("':001'")),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, MetaData, String, Table, text
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('problem', String, server_default=text("':001'"))
)
""",
)
def test_null_type(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("problem", NullType),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, MetaData, Table
from sqlalchemy.sql.sqltypes import NullType
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('problem', NullType)
)
""",
)
def test_identity_column(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column(
"id",
INTEGER,
primary_key=True,
server_default=Identity(start=1, increment=2),
),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Identity, Integer, MetaData, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer, Identity(start=1, increment=2), primary_key=True)
)
""",
)
def test_identity_column_decimal_values(generator: CodeGenerator) -> None:
# MSSQL reflects Identity column parameters (start, increment) as Decimal
# values instead of integers. This test ensures those are serialized correctly.
identity = Identity(start=1, increment=2)
# Simulate database reflection returning Decimal values (as MSSQL does)
identity.start = Decimal("1") # type: ignore[assignment]
identity.increment = Decimal("2") # type: ignore[assignment]
Table(
"simple_items",
generator.metadata,
Column(
"id",
INTEGER,
primary_key=True,
server_default=identity,
),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Identity, Integer, MetaData, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer, Identity(start=1, increment=2), primary_key=True)
)
""",
)
def test_multiline_column_comment(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER, comment="This\nis a multi-line\ncomment"),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer, comment='This\\nis a multi-line\\ncomment')
)
""",
)
def test_multiline_table_comment(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column("id", INTEGER),
comment="This\nis a multi-line\ncomment",
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer),
comment='This\\nis a multi-line\\ncomment'
)
""",
)
@pytest.mark.parametrize("engine", ["postgresql"], indirect=["engine"])
def test_postgresql_sequence_standard_name(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column(
"id",
INTEGER,
primary_key=True,
server_default=text("nextval('simple_items_id_seq'::regclass)"),
),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer, primary_key=True)
)
""",
)
@pytest.mark.parametrize("engine", ["postgresql"], indirect=["engine"])
def test_postgresql_sequence_nonstandard_name(generator: CodeGenerator) -> None:
Table(
"simple_items",
generator.metadata,
Column(
"id",
INTEGER,
primary_key=True,
server_default=text("nextval('test_seq'::regclass)"),
),
)
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Sequence, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer, Sequence('test_seq'), primary_key=True)
)
""",
)
@pytest.mark.parametrize(
"schemaname, seqname",
[
pytest.param("myschema", "test_seq"),
pytest.param("myschema", '"test_seq"'),
pytest.param('"my.schema"', "test_seq"),
pytest.param('"my.schema"', '"test_seq"'),
],
)
@pytest.mark.parametrize("engine", ["postgresql"], indirect=["engine"])
def test_postgresql_sequence_with_schema(
generator: CodeGenerator, schemaname: str, seqname: str
) -> None:
expected_schema = schemaname.strip('"')
Table(
"simple_items",
generator.metadata,
Column(
"id",
INTEGER,
primary_key=True,
server_default=text(f"nextval('{schemaname}.{seqname}'::regclass)"),
),
schema=expected_schema,
)
validate_code(
generator.generate(),
f"""\
from sqlalchemy import Column, Integer, MetaData, Sequence, Table
metadata = MetaData()
t_simple_items = Table(
'simple_items', metadata,
Column('id', Integer, Sequence('test_seq', \
schema='{expected_schema}'), primary_key=True),
schema='{expected_schema}'
)
""",
)
class MockStarRocksDialect(MySQLDialect_pymysql):
name = "starrocks"
construct_arguments = [
(
Column,
{
"is_agg_key": None,
"agg_type": None,
"IS_AGG_KEY": None,
"AGG_TYPE": None,
},
),
(
Table,
{
"primary_key": None,
"aggregate_key": None,
"unique_key": None,
"duplicate_key": None,
"engine": "OLAP",
"partition_by": None,
"order_by": None,
"security": None,
"properties": {},
"ENGINE": "OLAP",
"PARTITION_BY": None,
"ORDER_BY": None,
"SECURITY": None,
"PROPERTIES": {},
},
),
]
# Register StarRocksDialect
registry.register("starrocks", __name__, "MockStarRocksDialect")
class _PartitionInfo:
def __init__(self, partition_by: str) -> None:
self.partition_by = partition_by
def __str__(self) -> str:
return self.partition_by
def __repr__(self) -> str:
return repr(self.partition_by)
@pytest.mark.parametrize("generator", [["include_dialect_options"]], indirect=True)
def test_include_dialect_options_starrocks_tables(generator: CodeGenerator) -> None:
Table(
"t_starrocks",
generator.metadata,
Column("id", INTEGER, primary_key=True, starrocks_is_agg_key=True),
starrocks_ENGINE="OLAP",
starrocks_PARTITION_BY=_PartitionInfo("RANGE(id)"),
starrocks_ORDER_BY="id, name",
starrocks_PROPERTIES={"replication_num": "3", "storage_medium": "SSD"},
).info = {"table_kind": "TABLE"}
validate_code(
generator.generate(),
"""\
from sqlalchemy import Column, Integer, MetaData, Table
metadata = MetaData()
t_t_starrocks = Table(
't_starrocks', metadata,
Column('id', Integer, primary_key=True, starrocks_is_agg_key=True),
info={'table_kind': 'TABLE'},
starrocks_ENGINE='OLAP',
starrocks_ORDER_BY='id, name',
starrocks_PARTITION_BY='RANGE(id)',
starrocks_PROPERTIES={'replication_num': '3', 'storage_medium': 'SSD'}
)
""",
)