pax_global_header00006660000000000000000000000064144717406240014523gustar00rootroot0000000000000052 comment=a7f52089572f46ac66995fd533426d736cdeacc5 sqlacodegen-3.0.0rc3/000077500000000000000000000000001447174062400144205ustar00rootroot00000000000000sqlacodegen-3.0.0rc3/.github/000077500000000000000000000000001447174062400157605ustar00rootroot00000000000000sqlacodegen-3.0.0rc3/.github/ISSUE_TEMPLATE/000077500000000000000000000000001447174062400201435ustar00rootroot00000000000000sqlacodegen-3.0.0rc3/.github/ISSUE_TEMPLATE/bug_report.yaml000066400000000000000000000034571447174062400232100ustar00rootroot00000000000000name: 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. sqlacodegen-3.0.0rc3/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000000341447174062400221300ustar00rootroot00000000000000blank_issues_enabled: false sqlacodegen-3.0.0rc3/.github/ISSUE_TEMPLATE/features_request.yaml000066400000000000000000000020341447174062400244140ustar00rootroot00000000000000name: 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 sqlacodegen-3.0.0rc3/.github/workflows/000077500000000000000000000000001447174062400200155ustar00rootroot00000000000000sqlacodegen-3.0.0rc3/.github/workflows/publish.yml000066400000000000000000000012151447174062400222050ustar00rootroot00000000000000name: 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: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: 3.x - name: Install dependencies run: pip install build - name: Create packages run: python -m build - name: Upload packages uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.pypi_password }} sqlacodegen-3.0.0rc3/.github/workflows/test.yml000066400000000000000000000027241447174062400215240ustar00rootroot00000000000000name: test suite on: push: branches: [master] pull_request: jobs: test: strategy: fail-fast: false matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] sqlalchemy-version: ["1.4", "2.0"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} allow-prereleases: true cache: pip cache-dependency-path: pyproject.toml - name: Install dependencies SQLAlchemy 1.4 if: matrix.sqlalchemy-version == 1.4 run: pip install -e .[test,sqlmodel] coveralls SQLAlchemy==1.4.* - name: Install dependencies SQLAlchemy 2.0 if: matrix.sqlalchemy-version == 2.0 run: pip install -e .[test] coveralls SQLAlchemy==2.0.* - name: Test with pytest run: coverage run -m pytest - name: Upload Coverage run: coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_FLAG_NAME: ${{ matrix.test-name }} COVERALLS_PARALLEL: true SQLALCHEMY_WARN_20: "true" coveralls: name: Finish Coveralls needs: [test] runs-on: ubuntu-latest container: python:3-slim steps: - name: Finished run: | pip install coveralls coveralls --service=github --finish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} sqlacodegen-3.0.0rc3/.gitignore000066400000000000000000000002011447174062400164010ustar00rootroot00000000000000*.egg-info *.pyc .project .pydevproject .coverage .settings .tox .idea .vscode .cache .pytest_cache .mypy_cache dist build venv* sqlacodegen-3.0.0rc3/.pre-commit-config.yaml000066400000000000000000000021101447174062400206730ustar00rootroot00000000000000# 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: v4.4.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.0.285 hooks: - id: ruff args: [--fix, --show-fixes] - repo: https://github.com/psf/black rev: 23.7.0 hooks: - id: black - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.5.1 hooks: - id: mypy additional_dependencies: - pytest - "sqlalchemy[mypy] < 2.0" - 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 sqlacodegen-3.0.0rc3/CHANGES.rst000066400000000000000000000166431447174062400162340ustar00rootroot00000000000000Version history =============== **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 sqlacodegen-3.0.0rc3/CONTRIBUTING.rst000066400000000000000000000046041447174062400170650ustar00rootroot00000000000000Contributing 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 sqlacodegen-3.0.0rc3/LICENSE000066400000000000000000000021521447174062400154250ustar00rootroot00000000000000This 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. sqlacodegen-3.0.0rc3/README.rst000066400000000000000000000166471447174062400161250ustar00rootroot00000000000000.. 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 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 1.4.x and 2 * 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: .. 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 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 * ``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 * ``dataclasses`` * all the options from ``declarative`` * ``sqlmodel`` * 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. 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 sqlacodegen-3.0.0rc3/pyproject.toml000066400000000000000000000052301447174062400173340ustar00rootroot00000000000000[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"}] keywords = ["sqlalchemy"] license = {text = "MIT"} classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Environment :: Console", "Topic :: Database", "Topic :: Software Development :: Code Generators", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ] requires-python = ">=3.7" dependencies = [ "SQLAlchemy >= 1.4.36", "inflect >= 4.0.0", "importlib_metadata; python_version < '3.10'", "greenlet >= 3.0.0a1; python_version >= '3.12'", ] dynamic = ["version"] [project.urls] "Bug Tracker" = "https://github.com/agronholm/sqlacodegen/issues" "Source Code" = "https://github.com/agronholm/sqlacodegen" [project.optional-dependencies] test = [ "pytest", "pytest-cov", "psycopg2-binary", "mysql-connector-python", ] sqlmodel = [ "sqlmodel", ] citext = ["sqlalchemy-citext >= 1.7.0"] geoalchemy2 = ["geoalchemy2 >= 0.11.1"] [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" [tool.setuptools_scm] version_scheme = "post-release" local_scheme = "dirty-tag" [tool.ruff] select = [ "E", "F", "W", # default Flake8 "I", # isort "ISC", # flake8-implicit-str-concat "PGH", # pygrep-hooks "RUF100", # unused noqa (yesqa) "UP", # pyupgrade ] src = ["src"] [tool.mypy] strict = true plugins = ["sqlalchemy.ext.mypy.plugin"] [tool.pytest.ini_options] addopts = "-rsx --tb=short" testpaths = ["tests"] [coverage.run] source = ["sqlacodegen"] relative_files = true [coverage.report] show_missing = true [tool.tox] legacy_tox_ini = """ [tox] envlist = py37, py38, py39, py310, py311, py312 skip_missing_interpreters = true isolated_build = true [testenv] extras = test setenv = SQLALCHEMY_WARN_20 = true commands = python -m pytest {posargs} """ sqlacodegen-3.0.0rc3/src/000077500000000000000000000000001447174062400152075ustar00rootroot00000000000000sqlacodegen-3.0.0rc3/src/sqlacodegen/000077500000000000000000000000001447174062400174745ustar00rootroot00000000000000sqlacodegen-3.0.0rc3/src/sqlacodegen/__init__.py000066400000000000000000000000001447174062400215730ustar00rootroot00000000000000sqlacodegen-3.0.0rc3/src/sqlacodegen/__main__.py000066400000000000000000000000361447174062400215650ustar00rootroot00000000000000from .cli import main main() sqlacodegen-3.0.0rc3/src/sqlacodegen/cli.py000066400000000000000000000055341447174062400206240ustar00rootroot00000000000000from __future__ import annotations import argparse import sys from contextlib import ExitStack from typing import 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 if sys.version_info < (3, 10): from importlib_metadata import entry_points, version else: from importlib.metadata import entry_points, version 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") 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 {citext.__version__}") if geoalchemy2: print(f"Using geoalchemy2 {geoalchemy2.__version__}") # Use reflection to fill in the metadata engine = create_engine(args.url) metadata = MetaData() tables = args.tables.split(",") if args.tables else None schemas = args.schemas.split(",") if args.schemas else [None] options = set(args.option.split(",")) if args.options else set() for schema in schemas: metadata.reflect(engine, schema, not args.noviews, tables) # Instantiate the generator generator_class = generators[args.generator].load() generator = generator_class(metadata, engine, options) # 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()) sqlacodegen-3.0.0rc3/src/sqlacodegen/generators.py000066400000000000000000001762741447174062400222400ustar00rootroot00000000000000from __future__ import annotations import inspect import re import sys from abc import ABCMeta, abstractmethod from collections import defaultdict from collections.abc import Collection, Iterable, Sequence from dataclasses import dataclass 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 import inflect import sqlalchemy from sqlalchemy import ( ARRAY, Boolean, CheckConstraint, Column, Computed, Constraint, DefaultClause, Enum, Float, ForeignKey, ForeignKeyConstraint, Identity, Index, MetaData, PrimaryKeyConstraint, String, Table, Text, UniqueConstraint, ) from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.engine import Connection, Engine from sqlalchemy.exc import CompileError from sqlalchemy.sql.elements import TextClause 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, qualified_table_name, render_callable, uses_default_name, ) if sys.version_info < (3, 10): from importlib_metadata import version else: from importlib.metadata import version _sqla_version = tuple(int(x) for x in version("sqlalchemy").split(".")[:2]) _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"'(.*?)(? 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"} builtin_module_names: ClassVar[set[str]] = set(sys.builtin_module_names) | { "dataclasses" } 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() 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 variables = self.render_module_variables(models) if variables: sections.append(variables + "\n") # Render models rendered_models = self.render_models(models) if rendered_models: sections.append(rendered_models) # Render collected imports groups = self.group_imports() imports = "\n\n".join("\n".join(line for line in group) for group in groups) if imports: 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): if ( not isinstance(column.type.astext_type, Text) or column.type.astext_type.length is not None ): self.add_import(column.type.astext_type) 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_.__name__ in dir(sqlalchemy): 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] = [] for package in sorted(self.imports): imports = ", ".join(sorted(self.imports[package])) collection = thirdparty_imports if package == "__future__": collection = future_imports elif package in self.builtin_module_names: collection = stdlib_imports elif package in sys.modules: if "site-packages" not in (sys.modules[package].__file__ or ""): collection = stdlib_imports collection.append(f"from {package} import {imports}") for module in sorted(self.module_imports): thirdparty_imports.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: 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) 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 = {} 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_sole_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 ) 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.type)) 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 ( not column.nullable and not is_sole_pk and (_sqla_version < (2, 0) or is_table) ): kwargs["nullable"] = False 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(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): args.append(repr(column.server_default)) elif column.server_default: kwargs["server_default"] = repr(column.server_default) comment = getattr(column, "comment", None) if comment: kwargs["comment"] = repr(comment) if _sqla_version < (2, 0) or 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(self, coltype: object) -> str: args = [] kwargs: dict[str, Any] = {} sig = inspect.signature(coltype.__class__.__init__) defaults = {param.name: param.default for param in sig.parameters.values()} missing = object() use_kwargs = False 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): continue value = getattr(coltype, param.name, missing) default = defaults.get(param.name, missing) if value is missing or value == default: use_kwargs = True elif use_kwargs: kwargs[param.name] = repr(value) else: args.append(repr(value)) vararg = next( ( param.name for param in sig.parameters.values() if param.kind is Parameter.VAR_POSITIONAL ), None, ) if vararg and hasattr(coltype, vararg): varargs_repr = [repr(arg) for arg in getattr(coltype, vararg)] args.extend(varargs_repr) if isinstance(coltype, Enum) and coltype.name is not None: kwargs["name"] = repr(coltype.name) if isinstance(coltype, JSONB): # Remove astext_type if it's the default if ( isinstance(coltype.astext_type, Text) and coltype.astext_type.length is None ): del kwargs["astext_type"] if args or kwargs: return render_callable(coltype.__class__.__name__, *args, kwargs=kwargs) else: return coltype.__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 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 fix_column_types(self, table: Table) -> None: """Adjust the reflected column types.""" # 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 match = _re_boolean_check_constraint.match(sqltext) if match: colname_match = _re_column_name.match(match.group(1)) if colname_match: colname = colname_match.group(3) table.constraints.remove(constraint) table.c[colname].type = Boolean() continue # Turn any string-type column with a CheckConstraint like # "column IN (...)" into an Enum match = _re_enum_check_constraint.match(sqltext) if match: colname_match = _re_column_name.match(match.group(1)) if colname_match: colname = colname_match.group(3) items = match.group(2) if isinstance(table.c[colname].type, String): table.constraints.remove(constraint) if not isinstance(table.c[colname].type, Enum): options = _re_enum_item.findall(items) table.c[colname].type = Enum( *options, native_enum=False ) continue for column in table.c: try: column.type = self.get_adapted_type(column.type) except CompileError: pass # 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: compiled_type = coltype.compile(self.bind.engine.dialect) for supercls in coltype.__class__.__mro__: if not supercls.__name__.startswith("_") and hasattr( supercls, "__visit_name__" ): # Hack to fix adaptation of the Enum class which is broken since # SQLAlchemy 1.2 kw = {} if supercls is Enum: kw["name"] = coltype.name 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: # Make an exception to the rule for Float and arrays of Float, # since at least on PostgreSQL, Float can accurately represent # both REAL and DOUBLE_PRECISION if not isinstance(new_coltype, Float) and not ( isinstance(new_coltype, ARRAY) and isinstance(new_coltype.item_type, Float) ): 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", } def __init__( self, metadata: MetaData, bind: Connection | Engine, options: Sequence[str], *, indentation: str = " ", base_class_name: str = "Base", ): super().__init__(metadata, bind, options, indentation=indentation) self.base_class_name: str = base_class_name self.inflect_engine = inflect.engine() def generate_base(self) -> None: if _sqla_version < (1, 4): table_decoration = f"metadata = {self.base_class_name}.metadata" self.base = Base( literal_imports=[ LiteralImport("sqlalchemy.ext.declarative", "declarative_base") ], declarations=[f"{self.base_class_name} = declarative_base()"], metadata_ref=self.base_class_name, table_metadata_declaration=table_decoration, ) elif (1, 4) <= _sqla_version < (2, 0): table_decoration = f"metadata = {self.base_class_name}.metadata" self.base = Base( literal_imports=[LiteralImport("sqlalchemy.orm", "declarative_base")], declarations=[f"{self.base_class_name} = declarative_base()"], metadata_ref="metadata", table_metadata_declaration=table_decoration, ) else: 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): if _sqla_version >= (2, 0): 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: # Self referential reverse relationships preferred_name: str 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 = relationship.target.table.name # If there's a constraint with a single column that ends with "_id", use the # preceding part as the relationship name if relationship.constraint: 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 and column_names[0].endswith("_id"): preferred_name = column_names[0][:-3] if "use_inflect" in self.options: if relationship.type in ( RelationshipType.ONE_TO_MANY, RelationshipType.MANY_TO_MANY, ): inflected_name = self.inflect_engine.plural_noun(preferred_name) if inflected_name: preferred_name = inflected_name else: inflected_name = self.inflect_engine.singular_noun(preferred_name) if inflected_name: preferred_name = inflected_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, str] = {} # 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: 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 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_attribute(self, column_attr: ColumnAttribute) -> str: column = column_attr.column rendered_column = self.render_column(column, column_attr.name != column.name) if _sqla_version < (2, 0): return f"{column_attr.name} = {rendered_column}" else: try: python_type = column.type.python_type python_type_name = python_type.__name__ if python_type.__module__ == "builtins": column_python_type = python_type_name else: python_type_module = python_type.__module__ column_python_type = f"{python_type_module}.{python_type_name}" self.add_module_import(python_type_module) except NotImplementedError: self.add_literal_import("typing", "Any") column_python_type = "Any" if column.nullable: self.add_literal_import("typing", "Optional") column_python_type = f"Optional[{column_python_type}]" return ( f"{column_attr.name}: Mapped[{column_python_type}] = {rendered_column}" ) def render_relationship(self, relationship: RelationshipAttribute) -> str: def render_column_attrs(column_attrs: list[ColumnAttribute]) -> str: rendered = [] for attr in column_attrs: if attr.model is relationship.source: rendered.append(attr.name) else: rendered.append(repr(f"{attr.model.name}.{attr.name}")) return "[" + ", ".join(rendered) + "]" 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 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) rendered_relationship = render_callable( "relationship", repr(relationship.target.name), kwargs=kwargs ) if _sqla_version < (2, 0): return f"{relationship.name} = {rendered_relationship}" else: relationship_type: str if relationship.type == RelationshipType.ONE_TO_MANY: self.add_literal_import("typing", "List") relationship_type = f"List['{relationship.target.name}']" elif relationship.type in ( RelationshipType.ONE_TO_ONE, RelationshipType.MANY_TO_ONE, ): relationship_type = f"'{relationship.target.name}'" elif relationship.type == RelationshipType.MANY_TO_MANY: self.add_literal_import("typing", "List") relationship_type = f"List['{relationship.target.name}']" else: self.add_literal_import("typing", "Any") relationship_type = "Any" return ( f"{relationship.name}: Mapped[{relationship_type}] " f"= {rendered_relationship}" ) 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: if _sqla_version < (2, 0): self.base = Base( literal_imports=[LiteralImport("sqlalchemy.orm", "registry")], declarations=["mapper_registry = registry()"], metadata_ref="metadata", decorator="@mapper_registry.mapped", ) else: 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", ) def collect_imports(self, models: Iterable[Model]) -> None: super().collect_imports(models) if _sqla_version < (2, 0): if not self.quote_annotations: self.add_literal_import("__future__", "annotations") if any(isinstance(model, ModelClass) for model in models): self.remove_literal_import("sqlalchemy.orm", "declarative_base") self.add_literal_import("dataclasses", "dataclass") self.add_literal_import("dataclasses", "field") self.add_literal_import("sqlalchemy.orm", "registry") def collect_imports_for_model(self, model: Model) -> None: super().collect_imports_for_model(model) if _sqla_version < (2, 0): if isinstance(model, ModelClass): for column_attr in model.columns: if column_attr.column.nullable: self.add_literal_import("typing", "Optional") break for relationship_attr in model.relationships: if relationship_attr.type in ( RelationshipType.ONE_TO_MANY, RelationshipType.MANY_TO_MANY, ): self.add_literal_import("typing", "List") def collect_imports_for_column(self, column: Column[Any]) -> None: super().collect_imports_for_column(column) if _sqla_version < (2, 0): try: python_type = column.type.python_type except NotImplementedError: pass else: self.add_import(python_type) def render_module_variables(self, models: list[Model]) -> str: if _sqla_version >= (2, 0): return super().render_module_variables(models) else: if not any(isinstance(model, ModelClass) for model in models): return super().render_module_variables(models) declarations: list[str] = ["mapper_registry = registry()"] if any(not isinstance(model, ModelClass) for model in models): declarations.append("metadata = mapper_registry.metadata") if not self.quote_annotations: self.add_literal_import("__future__", "annotations") return "\n".join(declarations) def render_class_declaration(self, model: ModelClass) -> str: if _sqla_version >= (2, 0): return super().render_class_declaration(model) else: superclass_part = ( f"({model.parent_class.name})" if model.parent_class else "" ) return ( f"@mapper_registry.mapped\n@dataclass" f"\nclass {model.name}{superclass_part}:" ) def render_class_variables(self, model: ModelClass) -> str: if _sqla_version >= (2, 0): return super().render_class_variables(model) else: variables = [ super().render_class_variables(model), f"__sa_dataclass_metadata_key__ = {self.metadata_key!r}", ] return "\n".join(variables) def render_column_attribute(self, column_attr: ColumnAttribute) -> str: if _sqla_version >= (2, 0): return super().render_column_attribute(column_attr) else: column = column_attr.column try: python_type = column.type.python_type except NotImplementedError: python_type_name = "Any" else: python_type_name = python_type.__name__ kwargs: dict[str, Any] = {} if column.autoincrement and column.name in column.table.primary_key: kwargs["init"] = False elif column.nullable: self.add_literal_import("typing", "Optional") kwargs["default"] = None python_type_name = f"Optional[{python_type_name}]" rendered_column = self.render_column( column, column_attr.name != column.name ) kwargs["metadata"] = f"{{{self.metadata_key!r}: {rendered_column}}}" rendered_field = render_callable("field", kwargs=kwargs) return f"{column_attr.name}: {python_type_name} = {rendered_field}" def render_relationship(self, relationship: RelationshipAttribute) -> str: if _sqla_version >= (2, 0): return super().render_relationship(relationship) else: rendered = super().render_relationship(relationship).partition(" = ")[2] kwargs: dict[str, Any] = {} annotation = relationship.target.name if self.quote_annotations: annotation = repr(relationship.target.name) if relationship.type in ( RelationshipType.ONE_TO_MANY, RelationshipType.MANY_TO_MANY, ): self.add_literal_import("typing", "List") annotation = f"List[{annotation}]" kwargs["default_factory"] = "list" else: self.add_literal_import("typing", "Optional") kwargs["default"] = "None" annotation = f"Optional[{annotation}]" kwargs["metadata"] = f"{{{self.metadata_key!r}: {rendered}}}" rendered_field = render_callable("field", kwargs=kwargs) return f"{relationship.name}: {annotation} = {rendered_field}" 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, ) def generate_base(self) -> None: self.base = Base( literal_imports=[], declarations=[], metadata_ref="", ) def collect_imports(self, models: Iterable[Model]) -> None: super(DeclarativeGenerator, self).collect_imports(models) if any(isinstance(model, ModelClass) for model in models): self.remove_literal_import("sqlalchemy", "MetaData") self.add_literal_import("sqlmodel", "SQLModel") self.add_literal_import("sqlmodel", "Field") 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") for relationship_attr in model.relationships: if relationship_attr.type in ( RelationshipType.ONE_TO_MANY, RelationshipType.MANY_TO_MANY, ): self.add_literal_import("typing", "List") def collect_imports_for_column(self, column: Column[Any]) -> None: super().collect_imports_for_column(column) try: python_type = column.type.python_type except NotImplementedError: self.add_literal_import("typing", "Any") else: self.add_import(python_type) 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 try: python_type = column.type.python_type except NotImplementedError: python_type_name = "Any" else: python_type_name = python_type.__name__ kwargs: dict[str, Any] = {} if ( column.autoincrement and column.name in column.table.primary_key ) or column.nullable: self.add_literal_import("typing", "Optional") kwargs["default"] = None python_type_name = f"Optional[{python_type_name}]" rendered_column = self.render_column(column, True) kwargs["sa_column"] = f"{rendered_column}" rendered_field = render_callable("Field", kwargs=kwargs) return f"{column_attr.name}: {python_type_name} = {rendered_field}" def render_relationship(self, relationship: RelationshipAttribute) -> str: rendered = super().render_relationship(relationship).partition(" = ")[2] args = self.render_relationship_args(rendered) kwargs: dict[str, Any] = {} annotation = repr(relationship.target.name) if relationship.type in ( RelationshipType.ONE_TO_MANY, RelationshipType.MANY_TO_MANY, ): self.add_literal_import("typing", "List") annotation = f"List[{annotation}]" else: self.add_literal_import("typing", "Optional") annotation = f"Optional[{annotation}]" rendered_field = render_callable("Relationship", *args, kwargs=kwargs) return f"{relationship.name}: {annotation} = {rendered_field}" def render_relationship_args(self, arguments: str) -> list[str]: argument_list = arguments.split(",") # delete ')' and ' ' from args argument_list[-1] = argument_list[-1][:-1] argument_list = [argument[1:] for argument in argument_list] rendered_args: list[str] = [] for arg in argument_list: if "back_populates" in arg: rendered_args.append(arg) if "uselist=False" in arg: rendered_args.append("sa_relationship_kwargs={'uselist': False}") return rendered_args sqlacodegen-3.0.0rc3/src/sqlacodegen/models.py000066400000000000000000000043421447174062400213340ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass, field from enum import Enum, auto from typing import Any, Tuple, Union 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, Union[ColumnAttribute, str], Model, Union[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 sqlacodegen-3.0.0rc3/src/sqlacodegen/py.typed000066400000000000000000000000001447174062400211610ustar00rootroot00000000000000sqlacodegen-3.0.0rc3/src/sqlacodegen/utils.py000066400000000000000000000153501447174062400212120ustar00rootroot00000000000000from __future__ import annotations import re from collections.abc import Mapping from typing import Any 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, } ) 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: 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 sqlacodegen-3.0.0rc3/tests/000077500000000000000000000000001447174062400155625ustar00rootroot00000000000000sqlacodegen-3.0.0rc3/tests/__init__.py000066400000000000000000000000001447174062400176610ustar00rootroot00000000000000sqlacodegen-3.0.0rc3/tests/conftest.py000066400000000000000000000022611447174062400177620ustar00rootroot00000000000000from 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 from sqlacodegen.generators import _sqla_version requires_sqlalchemy_1_4 = pytest.mark.skipif( _sqla_version >= (2, 0), reason="Test requires SQLAlchemy 1.4.x " ) requires_sqlalchemy_2_0 = pytest.mark.skipif( _sqla_version < (2, 0), reason="Test requires SQLAlchemy 2.0.x or newer" ) @pytest.fixture def engine(request: FixtureRequest) -> Engine: dialect = getattr(request, "param", None) if dialect == "postgresql": return create_engine("postgresql:///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() sqlacodegen-3.0.0rc3/tests/test_cli.py000066400000000000000000000115021447174062400177410ustar00rootroot00000000000000from __future__ import annotations import sqlite3 import subprocess import sys from pathlib import Path import pytest from sqlacodegen.generators import _sqla_version from .conftest import requires_sqlalchemy_1_4 if sys.version_info < (3, 8): from importlib_metadata import version else: from importlib.metadata import version future_imports = "from __future__ import annotations\n\n" if _sqla_version < (1, 4): declarative_package = "sqlalchemy.ext.declarative" else: declarative_package = "sqlalchemy.orm" @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, ) if _sqla_version < (2, 0): assert ( output_path.read_text() == f"""\ from sqlalchemy import Column, Integer, Text from {declarative_package} import declarative_base Base = declarative_base() class Foo(Base): __tablename__ = 'foo' id = Column(Integer, primary_key=True) name = Column(Text, nullable=False) """ ) else: 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) """ ) 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, ) if _sqla_version < (2, 0): assert ( output_path.read_text() == f"""\ {future_imports}from dataclasses import dataclass, field from sqlalchemy import Column, Integer, Text from sqlalchemy.orm import registry mapper_registry = registry() @mapper_registry.mapped @dataclass class Foo: __tablename__ = 'foo' __sa_dataclass_metadata_key__ = 'sa' id: int = field(init=False, metadata={{'sa': Column(Integer, primary_key=True)}}) name: str = field(metadata={{'sa': Column(Text, nullable=False)}}) """ ) else: 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) """ ) @requires_sqlalchemy_1_4 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 typing import Optional from sqlalchemy import Column, Integer, Text from sqlmodel import Field, SQLModel class Foo(SQLModel, table=True): id: Optional[int] = Field(default=None, sa_column=Column('id', Integer, \ primary_key=True)) name: str = Field(sa_column=Column('name', Text, nullable=False)) """ ) 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 sqlacodegen-3.0.0rc3/tests/test_generator_dataclass.py000066400000000000000000000222011447174062400231750ustar00rootroot00000000000000from __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 requires_sqlalchemy_1_4, validate_code pytestmark = requires_sqlalchemy_1_4 @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 __future__ import annotations from dataclasses import dataclass, field from typing import Optional from sqlalchemy import Column, Integer, String from sqlalchemy.orm import registry mapper_registry = registry() @mapper_registry.mapped @dataclass class Simple: __tablename__ = 'simple' __sa_dataclass_metadata_key__ = 'sa' id: int = field(init=False, metadata={'sa': Column(Integer, \ primary_key=True)}) name: Optional[str] = field(default=None, metadata={'sa': \ Column(String(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 __future__ import annotations from dataclasses import dataclass, field from typing import Optional from sqlalchemy import Column, Integer, String, text from sqlalchemy.orm import registry mapper_registry = registry() @mapper_registry.mapped @dataclass class Simple: __tablename__ = 'simple' __sa_dataclass_metadata_key__ = 'sa' id: int = field(init=False, metadata={'sa': Column(Integer, \ primary_key=True)}) age: int = field(metadata={'sa': Column(Integer, nullable=False)}) name: Optional[str] = field(default=None, metadata={'sa': \ 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 __future__ import annotations from dataclasses import dataclass, field from typing import List, Optional from sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.orm import registry, relationship mapper_registry = registry() @mapper_registry.mapped @dataclass class SimpleContainers: __tablename__ = 'simple_containers' __sa_dataclass_metadata_key__ = 'sa' id: int = field(init=False, metadata={'sa': Column(Integer, \ primary_key=True)}) simple_items: List[SimpleItems] = field(default_factory=list, \ metadata={'sa': relationship('SimpleItems', back_populates='container')}) @mapper_registry.mapped @dataclass class SimpleItems: __tablename__ = 'simple_items' __sa_dataclass_metadata_key__ = 'sa' id: int = field(init=False, metadata={'sa': Column(Integer, \ primary_key=True)}) container_id: Optional[int] = field(default=None, \ metadata={'sa': Column(ForeignKey('simple_containers.id'))}) container: Optional[SimpleContainers] = field(default=None, \ metadata={'sa': 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 __future__ import annotations from dataclasses import dataclass, field from typing import List from sqlalchemy import Column, ForeignKey, Integer, Table from sqlalchemy.orm import registry, relationship mapper_registry = registry() metadata = mapper_registry.metadata @mapper_registry.mapped @dataclass class SimpleContainers: __tablename__ = 'simple_containers' __sa_dataclass_metadata_key__ = 'sa' id: int = field(init=False, metadata={'sa': Column(Integer, \ primary_key=True)}) item: List[SimpleItems] = field(default_factory=list, metadata=\ {'sa': relationship('SimpleItems', secondary='container_items', \ back_populates='container')}) @mapper_registry.mapped @dataclass class SimpleItems: __tablename__ = 'simple_items' __sa_dataclass_metadata_key__ = 'sa' id: int = field(init=False, metadata={'sa': Column(Integer, \ primary_key=True)}) container: List[SimpleContainers] = \ field(default_factory=list, metadata={'sa': relationship('SimpleContainers', \ secondary='container_items', back_populates='item')}) t_container_items = Table( 'container_items', 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 __future__ import annotations from dataclasses import dataclass, field from typing import List, Optional from sqlalchemy import Column, ForeignKeyConstraint, Integer from sqlalchemy.orm import registry, relationship mapper_registry = registry() @mapper_registry.mapped @dataclass class SimpleContainers: __tablename__ = 'simple_containers' __sa_dataclass_metadata_key__ = 'sa' id: int = field(init=False, metadata={'sa': Column(Integer, \ primary_key=True)}) simple_items: List[SimpleItems] = field(default_factory=list, \ metadata={'sa': relationship('SimpleItems', back_populates='container')}) @mapper_registry.mapped @dataclass class SimpleItems: __tablename__ = 'simple_items' __table_args__ = ( ForeignKeyConstraint(['container_id'], ['simple_containers.id'], \ name='foreignkeytest'), ) __sa_dataclass_metadata_key__ = 'sa' id: int = field(init=False, metadata={'sa': Column(Integer, \ primary_key=True)}) container_id: Optional[int] = field(default=None, metadata={'sa': \ Column(Integer)}) container: Optional[SimpleContainers] = field(default=None, \ metadata={'sa': 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(), """\ from __future__ import annotations from dataclasses import dataclass, field from sqlalchemy import Column from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import registry mapper_registry = registry() @mapper_registry.mapped @dataclass class Simple: __tablename__ = 'simple' __sa_dataclass_metadata_key__ = 'sa' id: str = field(init=False, metadata={'sa': \ Column(UUID, primary_key=True)}) """, ) sqlacodegen-3.0.0rc3/tests/test_generator_dataclass2.py000066400000000000000000000171771447174062400232770ustar00rootroot00000000000000from __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 requires_sqlalchemy_2_0, validate_code pytestmark = requires_sqlalchemy_2_0 @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) 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 List, 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['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 typing import List 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 List, 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['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(), """\ from sqlalchemy import UUID from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, \ mapped_column import uuid class Base(MappedAsDataclass, DeclarativeBase): pass class Simple(Base): __tablename__ = 'simple' id: Mapped[uuid.UUID] = mapped_column(UUID, primary_key=True) """, ) sqlacodegen-3.0.0rc3/tests/test_generator_declarative.py000066400000000000000000001154451447174062400235360ustar00rootroot00000000000000from __future__ import annotations import pytest from _pytest.fixtures import FixtureRequest from sqlalchemy import PrimaryKeyConstraint 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 INTEGER, VARCHAR, Text from sqlacodegen.generators import CodeGenerator, DeclarativeGenerator from .conftest import requires_sqlalchemy_1_4, validate_code pytestmark = requires_sqlalchemy_1_4 @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 sqlalchemy import Column, Index, Integer, String from sqlalchemy.orm import declarative_base Base = declarative_base() 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 = Column(Integer, primary_key=True) number = Column(Integer) text = 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 sqlalchemy import CheckConstraint, Column, Integer, UniqueConstraint from sqlalchemy.orm import declarative_base Base = declarative_base() class SimpleItems(Base): __tablename__ = 'simple_items' __table_args__ = ( CheckConstraint('number > 0'), UniqueConstraint('id', 'number') ) id = Column(Integer, primary_key=True) number = Column(Integer) """, ) 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 sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() class SimpleContainers(Base): __tablename__ = 'simple_containers' id = Column(Integer, primary_key=True) simple_items = relationship('SimpleItems', back_populates='container') class SimpleItems(Base): __tablename__ = 'simple_items' id = Column(Integer, primary_key=True) container_id = Column(ForeignKey('simple_containers.id')) container = 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 sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() class SimpleItems(Base): __tablename__ = 'simple_items' id = Column(Integer, primary_key=True) parent_item_id = Column(ForeignKey('simple_items.id')) parent_item = relationship('SimpleItems', remote_side=[id], \ back_populates='parent_item_reverse') parent_item_reverse = 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 sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() class SimpleItems(Base): __tablename__ = 'simple_items' id = Column(Integer, primary_key=True) parent_item_id = Column(ForeignKey('simple_items.id')) top_item_id = Column(ForeignKey('simple_items.id')) parent_item = relationship('SimpleItems', remote_side=[id], \ foreign_keys=[parent_item_id], back_populates='parent_item_reverse') parent_item_reverse = relationship('SimpleItems', \ remote_side=[parent_item_id], foreign_keys=[parent_item_id], \ back_populates='parent_item') top_item = relationship('SimpleItems', remote_side=[id], \ foreign_keys=[top_item_id], back_populates='top_item_reverse') top_item_reverse = 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 sqlalchemy import Column, ForeignKeyConstraint, Integer from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() class SimpleContainers(Base): __tablename__ = 'simple_containers' id1 = Column(Integer, primary_key=True, nullable=False) id2 = Column(Integer, primary_key=True, nullable=False) simple_items = 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 = Column(Integer, primary_key=True) container_id1 = Column(Integer) container_id2 = Column(Integer) simple_containers = 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), 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 sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() class SimpleContainers(Base): __tablename__ = 'simple_containers' id = Column(Integer, primary_key=True) simple_items = relationship('SimpleItems', \ foreign_keys='[SimpleItems.parent_container_id]', back_populates='parent_container') simple_items_ = relationship('SimpleItems', \ foreign_keys='[SimpleItems.top_container_id]', back_populates='top_container') class SimpleItems(Base): __tablename__ = 'simple_items' id = Column(Integer, primary_key=True) parent_container_id = Column(ForeignKey('simple_containers.id')) top_container_id = Column(ForeignKey('simple_containers.id')) parent_container = relationship('SimpleContainers', \ foreign_keys=[parent_container_id], back_populates='simple_items') top_container = relationship('SimpleContainers', \ foreign_keys=[top_container_id], back_populates='simple_items_') """, ) 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 sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() class OtherItems(Base): __tablename__ = 'other_items' id = Column(Integer, primary_key=True) simple_items = relationship('SimpleItems', uselist=False, \ back_populates='other_item') class SimpleItems(Base): __tablename__ = 'simple_items' id = Column(Integer, primary_key=True) other_item_id = Column(ForeignKey('other_items.id'), unique=True) other_item = 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 sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() class Fehwiuhfiw(Base): __tablename__ = 'fehwiuhfiw' id = Column(Integer, primary_key=True) oglkrogk = relationship('Oglkrogk', back_populates='fehwiuhfiw') class Oglkrogk(Base): __tablename__ = 'oglkrogk' id = Column(Integer, primary_key=True) fehwiuhfiwID = Column(ForeignKey('fehwiuhfiw.id')) 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 sqlalchemy import Column, ForeignKey, Integer, Text from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() class SimpleContainers(Base): __tablename__ = 'simple_containers' id = Column(Integer, primary_key=True) relationship_ = Column('relationship', Text) simple_items = relationship('SimpleItems', back_populates='container') class SimpleItems(Base): __tablename__ = 'simple_items' id = Column(Integer, primary_key=True) container_id = Column(ForeignKey('simple_containers.id')) container = 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 sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() class Relationship(Base): __tablename__ = 'relationship' id = Column(Integer, primary_key=True) simple_items = relationship('SimpleItems', back_populates='relationship_') class SimpleItems(Base): __tablename__ = 'simple_items' id = Column(Integer, primary_key=True) relationship_id = Column(ForeignKey('relationship.id')) 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 sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() class SimpleContainers(Base): __tablename__ = 'simple_containers' id = Column(Integer, primary_key=True) class SimpleItems(Base): __tablename__ = 'simple_items' id = Column(Integer, primary_key=True) container_id = Column(ForeignKey('simple_containers.id')) container = relationship('SimpleContainers') """, ) 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 declarative_base, relationship Base = declarative_base() metadata = Base.metadata class SimpleContainers(Base): __tablename__ = 'simple_containers' id = Column(Integer, primary_key=True) item = relationship('SimpleItems', secondary='container_items', \ back_populates='container') class SimpleItems(Base): __tablename__ = 'simple_items' id = Column(Integer, primary_key=True) container = relationship('SimpleContainers', secondary='container_items', \ back_populates='item') t_container_items = Table( 'container_items', metadata, Column('item_id', ForeignKey('simple_items.id')), Column('container_id', ForeignKey('simple_containers.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 declarative_base, relationship Base = declarative_base() metadata = Base.metadata class SimpleContainers(Base): __tablename__ = 'simple_containers' id = Column(Integer, primary_key=True) item = relationship('SimpleItems', secondary='container_items') class SimpleItems(Base): __tablename__ = 'simple_items' id = Column(Integer, primary_key=True) t_container_items = Table( 'container_items', 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 declarative_base, relationship Base = declarative_base() metadata = Base.metadata class SimpleItems(Base): __tablename__ = 'simple_items' id = Column(Integer, primary_key=True) parent = 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 = 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', 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 declarative_base, relationship Base = declarative_base() metadata = Base.metadata class SimpleContainers(Base): __tablename__ = 'simple_containers' id1 = Column(Integer, primary_key=True, nullable=False) id2 = Column(Integer, primary_key=True, nullable=False) simple_items = relationship('SimpleItems', secondary='container_items', \ back_populates='simple_containers') class SimpleItems(Base): __tablename__ = 'simple_items' id1 = Column(Integer, primary_key=True, nullable=False) id2 = Column(Integer, primary_key=True, nullable=False) simple_containers = relationship('SimpleContainers', \ secondary='container_items', back_populates='simple_items') t_container_items = Table( 'container_items', 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_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 sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.orm import declarative_base Base = declarative_base() class SimpleSuperItems(Base): __tablename__ = 'simple_super_items' id = Column(Integer, primary_key=True) data1 = Column(Integer) class SimpleItems(SimpleSuperItems): __tablename__ = 'simple_items' super_item_id = Column(ForeignKey('simple_super_items.id'), \ primary_key=True) data2 = Column(Integer) class SimpleSubItems(SimpleItems): __tablename__ = 'simple_sub_items' simple_items_id = Column(ForeignKey('simple_items.super_item_id'), \ primary_key=True) data3 = 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 Column, ForeignKey, Integer from sqlalchemy.orm import declarative_base Base = declarative_base() class Simple(Base): __tablename__ = 'simple' id = Column(Integer, primary_key=True) class Simple_(Simple): __tablename__ = 'simple' __table_args__ = {'schema': 'altschema'} id = 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 Column, Integer from sqlalchemy.orm import declarative_base Base = declarative_base() class SimpleItem(Base): __tablename__ = 'simple_items' id = Column(Integer, primary_key=True) class Singular(Base): __tablename__ = 'singular' id = 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 sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() class {class_name.capitalize()}(Base): __tablename__ = '{table_name}' id = Column(Integer, primary_key=True) simple_item = relationship('SimpleItem', uselist=False, \ back_populates='{relationship_name}') class SimpleItem(Base): __tablename__ = 'simple_items' id = Column(Integer, primary_key=True) {relationship_name}_id = Column(ForeignKey('{table_name}.id'), unique=True) {relationship_name} = relationship('{class_name.capitalize()}', \ back_populates='simple_item') """, ) 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 Column, Integer from sqlalchemy.orm import declarative_base Base = declarative_base() class SimpleItems(Base): __tablename__ = 'simple_items' __table_args__ = {'schema': 'testschema'} id = 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 sqlalchemy import Column, Index, Integer, String from sqlalchemy.orm import declarative_base Base = declarative_base() class SimpleItems(Base): __tablename__ = 'simple_items' __table_args__ = ( Index('testidx', 'id', 'name'), {'schema': 'testschema'} ) id = Column(Integer, primary_key=True) name = 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 sqlalchemy import Column, ForeignKey, Integer from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() class OtherItems(Base): __tablename__ = 'other_items' __table_args__ = {'schema': 'otherschema'} id = Column(Integer, primary_key=True) simple_items = relationship('SimpleItems', back_populates='other_item') class SimpleItems(Base): __tablename__ = 'simple_items' id = Column(Integer, primary_key=True) other_item_id = Column(ForeignKey('otherschema.other_items.id')) other_item = 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 sqlalchemy import Column, Integer from sqlalchemy.orm import declarative_base Base = declarative_base() class SimpleItems(Base): __tablename__ = 'simple-items' id_test = Column('id-test', Integer, primary_key=True) _4test = Column('4test', Integer) _4test_ = Column('_4test', Integer) def_ = 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 Column, Integer from sqlalchemy.orm import declarative_base Base = declarative_base() class CustomerAPIPreference(Base): __tablename__ = 'CustomerAPIPreference' id = 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 Column, Integer from sqlalchemy.orm import declarative_base Base = declarative_base() class CustomerApiPreference(Base): __tablename__ = 'customer_api_preference' id = 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 Column, Integer from sqlalchemy.orm import declarative_base Base = declarative_base() class CustomerAPIPreference(Base): __tablename__ = 'customer_API_Preference' id = 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 Column, Integer from sqlalchemy.orm import declarative_base Base = declarative_base() class CustomerAPIPreference(Base): __tablename__ = 'customer_API__Preference' id = 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 Column, Integer from sqlalchemy.orm import declarative_base Base = declarative_base() class Simple(Base): __tablename__ = 'simple' id = 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 Column, Integer from sqlalchemy.orm import declarative_base Base = declarative_base() class Simple(Base): __tablename__ = 'simple' __table_args__ = {'comment': "this is a 'comment'"} id = 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 sqlalchemy import Column, Integer, String from sqlalchemy.orm import declarative_base Base = declarative_base() class Simple(Base): __tablename__ = 'simple' id = Column(Integer, primary_key=True) metadata_ = 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 Column, Integer from sqlalchemy.orm import declarative_base Base = declarative_base() class Simple(Base): __tablename__ = 'simple' id = 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 sqlalchemy import CheckConstraint, Column, Integer, \ PrimaryKeyConstraint, String, UniqueConstraint from sqlalchemy.orm import declarative_base Base = declarative_base() class Simple(Base): __tablename__ = 'simple' __table_args__ = ( CheckConstraint('id > 0', name='checktest'), PrimaryKeyConstraint('id', name='primarytest'), UniqueConstraint('text', name='uniquetest') ) id = Column(Integer, primary_key=True) text = 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 sqlalchemy import Column, ForeignKeyConstraint, Integer from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() class SimpleContainers(Base): __tablename__ = 'simple_containers' id = Column(Integer, primary_key=True) simple_items = relationship('SimpleItems', back_populates='container') class SimpleItems(Base): __tablename__ = 'simple_items' __table_args__ = ( ForeignKeyConstraint(['container_id'], ['simple_containers.id'], \ name='foreignkeytest'), ) id = Column(Integer, primary_key=True) container_id = Column(Integer) container = relationship('SimpleContainers', \ back_populates='simple_items') """, ) 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 sqlalchemy import Column, Integer, String, text from sqlalchemy.orm import declarative_base Base = declarative_base() class Simple(Base): __tablename__ = 'simple' id = Column(Integer, primary_key=True) text_ = Column('text', String) textwithdefault = Column(String, server_default=text("'test'")) """, ) sqlacodegen-3.0.0rc3/tests/test_generator_declarative2.py000066400000000000000000001203521447174062400236110ustar00rootroot00000000000000from __future__ import annotations import pytest from _pytest.fixtures import FixtureRequest from sqlalchemy import PrimaryKeyConstraint 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 INTEGER, VARCHAR, Text from sqlacodegen.generators import CodeGenerator, DeclarativeGenerator from .conftest import requires_sqlalchemy_2_0, validate_code pytestmark = requires_sqlalchemy_2_0 @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_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) """, ) 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 List, 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['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 List, 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['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 List, 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['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['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 List, 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['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), 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 List, 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) parent_container_id: Mapped[Optional[int]] = \ mapped_column(ForeignKey('simple_containers.id')) top_container_id: Mapped[Optional[int]] = \ mapped_column(ForeignKey('simple_containers.id')) parent_container: Mapped['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_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['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['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 List, 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['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 List, 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['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 List, 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['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['SimpleContainers'] = relationship('SimpleContainers') """, ) 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 typing import List 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 typing import List 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 typing import List 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 typing import List 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_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['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['{class_name.capitalize()}'] = \ relationship('{class_name.capitalize()}', back_populates='simple_item') """, ) 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 List, 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['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 List, 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['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'")) """, ) sqlacodegen-3.0.0rc3/tests/test_generator_sqlmodel.py000066400000000000000000000136101447174062400230620ustar00rootroot00000000000000from __future__ import annotations import pytest from _pytest.fixtures import FixtureRequest from sqlalchemy.engine import Engine from sqlalchemy.schema import ( CheckConstraint, Column, ForeignKeyConstraint, Index, MetaData, Table, UniqueConstraint, ) from sqlalchemy.types import INTEGER, VARCHAR from sqlacodegen.generators import CodeGenerator, SQLModelGenerator from .conftest import validate_code pytest.importorskip("sqlmodel", reason="Requires the sqlmodel package") @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), 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: Optional[int] = Field(default=None, sa_column=Column(\ 'id', Integer, primary_key=True)) number: Optional[int] = Field(default=None, sa_column=Column(\ 'number', Integer)) 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: Optional[int] = Field(default=None, 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 List, Optional from sqlalchemy import Column, ForeignKey, Integer from sqlmodel import Field, Relationship, SQLModel class SimpleContainers(SQLModel, table=True): __tablename__ = 'simple_containers' id: Optional[int] = Field(default=None, 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: Optional[int] = Field(default=None, 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_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: Optional[int] = Field(default=None, sa_column=Column(\ 'id', Integer, primary_key=True)) simple_onetoone: Optional['SimpleOnetoone'] = Relationship(\ sa_relationship_kwargs={'uselist': False}, back_populates='other_item') class SimpleOnetoone(SQLModel, table=True): __tablename__ = 'simple_onetoone' id: Optional[int] = Field(default=None, 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') """, ) sqlacodegen-3.0.0rc3/tests/test_generator_tables.py000066400000000000000000000575321447174062400225270ustar00rootroot00000000000000from __future__ import annotations from textwrap import dedent import pytest from _pytest.fixtures import FixtureRequest from sqlalchemy.dialects import mysql, postgresql 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 NullType from sqlalchemy.types import INTEGER, NUMERIC, SMALLINT, VARCHAR, Text from sqlacodegen.generators import CodeGenerator, TablesGenerator from .conftest import requires_sqlalchemy_1_4, validate_code pytestmark = requires_sqlalchemy_1_4 @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: Table( "simple_items", generator.metadata, Column("enum", postgresql.ENUM("A", "B", name="blah")), Column("bool", postgresql.BOOLEAN), Column("number", NUMERIC(10, asdecimal=False)), ) validate_code( generator.generate(), """\ from sqlalchemy import Boolean, Column, Enum, MetaData, Numeric, Table metadata = MetaData() t_simple_items = Table( 'simple_items', metadata, Column('enum', Enum('A', 'B', name='blah')), Column('bool', Boolean), Column('number', Numeric(10, asdecimal=False)) ) """, ) 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 ARRAY, Column, Float, Integer, MetaData, Table metadata = MetaData() t_simple_items = Table( 'simple_items', metadata, Column('dp_array', ARRAY(Float(precision=53))), Column('int_array', ARRAY(Integer())) ) """, ) @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) ) """, ) def test_enum_detection(generator: CodeGenerator) -> None: Table( "simple_items", generator.metadata, Column("enum", VARCHAR(255)), CheckConstraint(r"simple_items.enum IN ('A', '\'B', 'C')"), ) validate_code( generator.generate(), """\ from sqlalchemy import Column, Enum, MetaData, Table metadata = MetaData() t_simple_items = Table( 'simple_items', metadata, Column('enum', Enum('A', "\\\\'B", 'C')) ) """, ) @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, Float, MetaData, Table metadata = MetaData() t_simple_items = Table( 'simple_items', metadata, Column('id', BigInteger), Column('length', Float) ) """, ) @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("set", mysql.SET("one", "two")), ) validate_code( generator.generate(), """\ from sqlalchemy import Column, Integer, MetaData, String, Table from sqlalchemy.dialects.mysql import SET metadata = MetaData() t_simple_items = Table( 'simple_items', metadata, Column('id', Integer), Column('name', String(255)), Column('set', SET('one', 'two')) ) """, ) 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_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}' ) """, ) sqlacodegen-3.0.0rc3/tests/test_generator_tables2.py000066400000000000000000000575361447174062400226150ustar00rootroot00000000000000from __future__ import annotations from textwrap import dedent import pytest from _pytest.fixtures import FixtureRequest from sqlalchemy.dialects import mysql, postgresql 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 NullType from sqlalchemy.types import INTEGER, NUMERIC, SMALLINT, VARCHAR, Text from sqlacodegen.generators import CodeGenerator, TablesGenerator from .conftest import requires_sqlalchemy_2_0, validate_code pytestmark = requires_sqlalchemy_2_0 @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: Table( "simple_items", generator.metadata, Column("enum", postgresql.ENUM("A", "B", name="blah")), Column("bool", postgresql.BOOLEAN), Column("number", NUMERIC(10, asdecimal=False)), ) validate_code( generator.generate(), """\ from sqlalchemy import Boolean, Column, Enum, MetaData, Numeric, Table metadata = MetaData() t_simple_items = Table( 'simple_items', metadata, Column('enum', Enum('A', 'B', name='blah')), Column('bool', Boolean), Column('number', Numeric(10, asdecimal=False)) ) """, ) 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 ARRAY, Column, Double, Integer, MetaData, Table 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_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) ) """, ) def test_enum_detection(generator: CodeGenerator) -> None: Table( "simple_items", generator.metadata, Column("enum", VARCHAR(255)), CheckConstraint(r"simple_items.enum IN ('A', '\'B', 'C')"), ) validate_code( generator.generate(), """\ from sqlalchemy import Column, Enum, MetaData, Table metadata = MetaData() t_simple_items = Table( 'simple_items', metadata, Column('enum', Enum('A', "\\\\'B", 'C')) ) """, ) @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("set", mysql.SET("one", "two")), ) validate_code( generator.generate(), """\ from sqlalchemy import Column, Integer, MetaData, String, Table from sqlalchemy.dialects.mysql import SET metadata = MetaData() t_simple_items = Table( 'simple_items', metadata, Column('id', Integer), Column('name', String(255)), Column('set', SET('one', 'two')) ) """, ) 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_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}' ) """, )