├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── db.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── buf.gen.yaml ├── buf.work.yaml ├── devenv.lock ├── devenv.nix ├── devenv.yaml ├── examples ├── requirements.txt ├── sqlc.yaml └── src │ ├── authors │ ├── __init__.py │ ├── models.py │ ├── query.py │ ├── query.sql │ └── schema.sql │ ├── booktest │ ├── __init__.py │ ├── models.py │ ├── query.py │ ├── query.sql │ └── schema.sql │ ├── dbtest │ ├── __init__.py │ └── migrations.py │ ├── jets │ ├── __init__.py │ ├── models.py │ ├── query-building.py │ ├── query-building.sql │ └── schema.sql │ ├── ondeck │ ├── __init__.py │ ├── city.py │ ├── models.py │ ├── query │ │ ├── city.sql │ │ └── venue.sql │ ├── schema │ │ ├── 0001_city.sql │ │ ├── 0002_venue.sql │ │ └── 0003_add_column.sql │ └── venue.py │ └── tests │ ├── __init__.py │ ├── conftest.py │ ├── test_authors.py │ ├── test_booktest.py │ └── test_ondeck.py ├── go.mod ├── go.sum ├── internal ├── ast │ └── ast.pb.go ├── config.go ├── endtoend │ ├── endtoend_test.go │ └── testdata │ │ ├── emit_pydantic_models │ │ ├── db │ │ │ ├── __init__.py │ │ │ ├── models.py │ │ │ └── query.py │ │ ├── query.sql │ │ ├── schema.sql │ │ └── sqlc.yaml │ │ ├── emit_str_enum │ │ ├── db │ │ │ ├── models.py │ │ │ └── query.py │ │ ├── query.sql │ │ ├── schema.sql │ │ └── sqlc.yaml │ │ ├── exec_result │ │ ├── python │ │ │ ├── models.py │ │ │ └── query.py │ │ ├── query.sql │ │ ├── schema.sql │ │ └── sqlc.yaml │ │ ├── exec_rows │ │ ├── python │ │ │ ├── models.py │ │ │ └── query.py │ │ ├── query.sql │ │ ├── schema.sql │ │ └── sqlc.yaml │ │ ├── inflection_exclude_table_names │ │ ├── python │ │ │ ├── models.py │ │ │ └── query.py │ │ ├── query.sql │ │ ├── schema.sql │ │ └── sqlc.yaml │ │ ├── query_parameter_limit_two │ │ ├── python │ │ │ ├── models.py │ │ │ └── query.py │ │ ├── query.sql │ │ ├── schema.sql │ │ └── sqlc.yaml │ │ ├── query_parameter_limit_undefined │ │ ├── python │ │ │ ├── models.py │ │ │ └── query.py │ │ ├── query.sql │ │ ├── schema.sql │ │ └── sqlc.yaml │ │ ├── query_parameter_limit_zero │ │ ├── python │ │ │ ├── models.py │ │ │ └── query.py │ │ ├── query.sql │ │ ├── schema.sql │ │ └── sqlc.yaml │ │ └── query_parameter_no_limit │ │ ├── query.sql │ │ ├── schema.sql │ │ ├── sqlc.yaml │ │ └── stderr.txt ├── gen.go ├── imports.go ├── inflection │ └── singular.go ├── poet │ ├── builders.go │ └── poet.go ├── postgresql_type.go └── printer │ ├── printer.go │ └── printer_test.go ├── plugin └── main.go └── protos ├── ast └── ast.proto └── buf.yaml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: go 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | test: 9 | name: test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-go@v5 14 | with: 15 | go-version: '1.23.5' 16 | - uses: sqlc-dev/setup-sqlc@v4 17 | with: 18 | sqlc-version: '1.28.0' 19 | - run: make 20 | - run: make test 21 | - run: sqlc diff 22 | working-directory: examples 23 | -------------------------------------------------------------------------------- /.github/workflows/db.yml: -------------------------------------------------------------------------------- 1 | name: db 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | 9 | build: 10 | name: test 11 | runs-on: ubuntu-latest 12 | 13 | services: 14 | postgres: 15 | image: postgres:11 16 | env: 17 | POSTGRES_USER: postgres 18 | POSTGRES_PASSWORD: postgres 19 | POSTGRES_DB: postgres 20 | ports: 21 | - 5432:5432 22 | # needed because the postgres container does not provide a healthcheck 23 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: actions/setup-python@v5 28 | with: 29 | python-version: 3.9 30 | - name: Install python dependencies 31 | working-directory: ./examples 32 | run: | 33 | python -m pip install --upgrade pip 34 | python -m pip install -r requirements.txt 35 | - name: Test python code 36 | working-directory: ./examples 37 | env: 38 | PG_USER: postgres 39 | PG_HOST: localhost 40 | PG_DATABASE: postgres 41 | PG_PASSWORD: postgres 42 | PG_PORT: ${{ job.services.postgres.ports['5432'] }} 43 | run: | 44 | pytest src/tests 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | 3 | # Devenv 4 | .envrc 5 | .direnv 6 | .devenv* 7 | devenv.local.nix 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Riza, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build test 2 | 3 | build: 4 | go build ./... 5 | 6 | test: bin/sqlc-gen-python.wasm 7 | go test ./... 8 | 9 | all: bin/sqlc-gen-python bin/sqlc-gen-python.wasm 10 | 11 | bin/sqlc-gen-python: bin go.mod go.sum $(wildcard **/*.go) 12 | cd plugin && go build -o ../bin/sqlc-gen-python ./main.go 13 | 14 | bin/sqlc-gen-python.wasm: bin/sqlc-gen-python 15 | cd plugin && GOOS=wasip1 GOARCH=wasm go build -o ../bin/sqlc-gen-python.wasm main.go 16 | 17 | bin: 18 | mkdir -p bin 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | ```yaml 4 | version: "2" 5 | plugins: 6 | - name: py 7 | wasm: 8 | url: https://downloads.sqlc.dev/plugin/sqlc-gen-python_1.3.0.wasm 9 | sha256: fbedae96b5ecae2380a70fb5b925fd4bff58a6cfb1f3140375d098fbab7b3a3c 10 | sql: 11 | - schema: "schema.sql" 12 | queries: "query.sql" 13 | engine: postgresql 14 | codegen: 15 | - out: src/authors 16 | plugin: py 17 | options: 18 | package: authors 19 | emit_sync_querier: true 20 | emit_async_querier: true 21 | ``` 22 | 23 | ### Emit Pydantic Models instead of `dataclasses` 24 | 25 | Option: `emit_pydantic_models` 26 | 27 | By default, `sqlc-gen-python` will emit `dataclasses` for the models. If you prefer to use [`pydantic`](https://docs.pydantic.dev/latest/) models, you can enable this option. 28 | 29 | with `emit_pydantic_models` 30 | 31 | ```py 32 | from pydantic import BaseModel 33 | 34 | class Author(pydantic.BaseModel): 35 | id: int 36 | name: str 37 | ``` 38 | 39 | without `emit_pydantic_models` 40 | 41 | ```py 42 | import dataclasses 43 | 44 | @dataclasses.dataclass() 45 | class Author: 46 | id: int 47 | name: str 48 | ``` 49 | 50 | ### Use `enum.StrEnum` for Enums 51 | 52 | Option: `emit_str_enum` 53 | 54 | `enum.StrEnum` was introduce in Python 3.11. 55 | 56 | `enum.StrEnum` is a subclass of `str` that is also a subclass of `Enum`. This allows for the use of `Enum` values as strings, compared to strings, or compared to other `enum.StrEnum` types. 57 | 58 | This is convenient for type checking and validation, as well as for serialization and deserialization. 59 | 60 | By default, `sqlc-gen-python` will emit `(str, enum.Enum)` for the enum classes. If you prefer to use `enum.StrEnum`, you can enable this option. 61 | 62 | with `emit_str_enum` 63 | 64 | ```py 65 | class Status(enum.StrEnum): 66 | """Venues can be either open or closed""" 67 | OPEN = "op!en" 68 | CLOSED = "clo@sed" 69 | ``` 70 | 71 | without `emit_str_enum` (current behavior) 72 | 73 | ```py 74 | class Status(str, enum.Enum): 75 | """Venues can be either open or closed""" 76 | OPEN = "op!en" 77 | CLOSED = "clo@sed" 78 | ``` 79 | -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | managed: 3 | enabled: true 4 | plugins: 5 | - plugin: buf.build/protocolbuffers/go:v1.30.0 6 | out: internal 7 | opt: paths=source_relative 8 | -------------------------------------------------------------------------------- /buf.work.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | directories: 3 | - protos 4 | -------------------------------------------------------------------------------- /devenv.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "devenv": { 4 | "locked": { 5 | "dir": "src/modules", 6 | "lastModified": 1698243190, 7 | "narHash": "sha256-n+SbyNQRhUcaZoU00d+7wi17HJpw/kAUrXOL4zRcqE8=", 8 | "owner": "cachix", 9 | "repo": "devenv", 10 | "rev": "86f476f7edb86159fd20764489ab4e4df6edb4b6", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "dir": "src/modules", 15 | "owner": "cachix", 16 | "repo": "devenv", 17 | "type": "github" 18 | } 19 | }, 20 | "flake-compat": { 21 | "flake": false, 22 | "locked": { 23 | "lastModified": 1673956053, 24 | "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", 25 | "owner": "edolstra", 26 | "repo": "flake-compat", 27 | "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "edolstra", 32 | "repo": "flake-compat", 33 | "type": "github" 34 | } 35 | }, 36 | "flake-utils": { 37 | "inputs": { 38 | "systems": "systems" 39 | }, 40 | "locked": { 41 | "lastModified": 1685518550, 42 | "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", 43 | "owner": "numtide", 44 | "repo": "flake-utils", 45 | "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "owner": "numtide", 50 | "repo": "flake-utils", 51 | "type": "github" 52 | } 53 | }, 54 | "gitignore": { 55 | "inputs": { 56 | "nixpkgs": [ 57 | "pre-commit-hooks", 58 | "nixpkgs" 59 | ] 60 | }, 61 | "locked": { 62 | "lastModified": 1660459072, 63 | "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", 64 | "owner": "hercules-ci", 65 | "repo": "gitignore.nix", 66 | "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "hercules-ci", 71 | "repo": "gitignore.nix", 72 | "type": "github" 73 | } 74 | }, 75 | "nixpkgs": { 76 | "locked": { 77 | "lastModified": 1698553279, 78 | "narHash": "sha256-T/9P8yBSLcqo/v+FTOBK+0rjzjPMctVymZydbvR/Fak=", 79 | "owner": "NixOS", 80 | "repo": "nixpkgs", 81 | "rev": "90e85bc7c1a6fc0760a94ace129d3a1c61c3d035", 82 | "type": "github" 83 | }, 84 | "original": { 85 | "owner": "NixOS", 86 | "ref": "nixpkgs-unstable", 87 | "repo": "nixpkgs", 88 | "type": "github" 89 | } 90 | }, 91 | "nixpkgs-stable": { 92 | "locked": { 93 | "lastModified": 1685801374, 94 | "narHash": "sha256-otaSUoFEMM+LjBI1XL/xGB5ao6IwnZOXc47qhIgJe8U=", 95 | "owner": "NixOS", 96 | "repo": "nixpkgs", 97 | "rev": "c37ca420157f4abc31e26f436c1145f8951ff373", 98 | "type": "github" 99 | }, 100 | "original": { 101 | "owner": "NixOS", 102 | "ref": "nixos-23.05", 103 | "repo": "nixpkgs", 104 | "type": "github" 105 | } 106 | }, 107 | "pre-commit-hooks": { 108 | "inputs": { 109 | "flake-compat": "flake-compat", 110 | "flake-utils": "flake-utils", 111 | "gitignore": "gitignore", 112 | "nixpkgs": [ 113 | "nixpkgs" 114 | ], 115 | "nixpkgs-stable": "nixpkgs-stable" 116 | }, 117 | "locked": { 118 | "lastModified": 1698227354, 119 | "narHash": "sha256-Fi5H9jbaQLmLw9qBi/mkR33CoFjNbobo5xWdX4tKz1Q=", 120 | "owner": "cachix", 121 | "repo": "pre-commit-hooks.nix", 122 | "rev": "bd38df3d508dfcdff52cd243d297f218ed2257bf", 123 | "type": "github" 124 | }, 125 | "original": { 126 | "owner": "cachix", 127 | "repo": "pre-commit-hooks.nix", 128 | "type": "github" 129 | } 130 | }, 131 | "root": { 132 | "inputs": { 133 | "devenv": "devenv", 134 | "nixpkgs": "nixpkgs", 135 | "pre-commit-hooks": "pre-commit-hooks" 136 | } 137 | }, 138 | "systems": { 139 | "locked": { 140 | "lastModified": 1681028828, 141 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 142 | "owner": "nix-systems", 143 | "repo": "default", 144 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 145 | "type": "github" 146 | }, 147 | "original": { 148 | "owner": "nix-systems", 149 | "repo": "default", 150 | "type": "github" 151 | } 152 | } 153 | }, 154 | "root": "root", 155 | "version": 7 156 | } 157 | -------------------------------------------------------------------------------- /devenv.nix: -------------------------------------------------------------------------------- 1 | { pkgs, ... }: 2 | 3 | { 4 | # https://devenv.sh/packages/ 5 | packages = [ 6 | pkgs.go 7 | pkgs.git 8 | pkgs.govulncheck 9 | pkgs.gopls 10 | pkgs.golint 11 | pkgs.python311 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /devenv.yaml: -------------------------------------------------------------------------------- 1 | inputs: 2 | nixpkgs: 3 | url: github:NixOS/nixpkgs/nixpkgs-unstable 4 | -------------------------------------------------------------------------------- /examples/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest~=6.2.2 2 | pytest-asyncio~=0.14.0 3 | psycopg2-binary~=2.8.6 4 | asyncpg~=0.21.0 5 | sqlalchemy==1.4.0 6 | -------------------------------------------------------------------------------- /examples/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | plugins: 3 | - name: py 4 | wasm: 5 | url: https://downloads.sqlc.dev/plugin/sqlc-gen-python_1.2.0.wasm 6 | sha256: a6c5d174c407007c3717eea36ff0882744346e6ba991f92f71d6ab2895204c0e 7 | sql: 8 | - schema: "src/authors/schema.sql" 9 | queries: "src/authors/query.sql" 10 | engine: postgresql 11 | codegen: 12 | - out: src/authors 13 | plugin: py 14 | options: 15 | package: authors 16 | emit_sync_querier: true 17 | emit_async_querier: true 18 | query_parameter_limit: 5 19 | - schema: "src/booktest/schema.sql" 20 | queries: "src/booktest/query.sql" 21 | engine: postgresql 22 | codegen: 23 | - out: src/booktest 24 | plugin: py 25 | options: 26 | package: booktest 27 | emit_async_querier: true 28 | query_parameter_limit: 5 29 | - schema: "src/jets/schema.sql" 30 | queries: "src/jets/query-building.sql" 31 | engine: postgresql 32 | codegen: 33 | - out: src/jets 34 | plugin: py 35 | options: 36 | package: jets 37 | emit_async_querier: true 38 | query_parameter_limit: 5 39 | - schema: "src/ondeck/schema" 40 | queries: "src/ondeck/query" 41 | engine: postgresql 42 | codegen: 43 | - out: src/ondeck 44 | plugin: py 45 | options: 46 | package: ondeck 47 | emit_async_querier: true 48 | query_parameter_limit: 5 49 | -------------------------------------------------------------------------------- /examples/src/authors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlc-dev/sqlc-gen-python/53fa0b2e3d10c4201f7a5a344d00a560330da3bb/examples/src/authors/__init__.py -------------------------------------------------------------------------------- /examples/src/authors/models.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | import dataclasses 5 | from typing import Optional 6 | 7 | 8 | @dataclasses.dataclass() 9 | class Author: 10 | id: int 11 | name: str 12 | bio: Optional[str] 13 | -------------------------------------------------------------------------------- /examples/src/authors/query.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | # source: query.sql 5 | from typing import AsyncIterator, Iterator, Optional 6 | 7 | import sqlalchemy 8 | import sqlalchemy.ext.asyncio 9 | 10 | from authors import models 11 | 12 | 13 | CREATE_AUTHOR = """-- name: create_author \\:one 14 | INSERT INTO authors ( 15 | name, bio 16 | ) VALUES ( 17 | :p1, :p2 18 | ) 19 | RETURNING id, name, bio 20 | """ 21 | 22 | 23 | DELETE_AUTHOR = """-- name: delete_author \\:exec 24 | DELETE FROM authors 25 | WHERE id = :p1 26 | """ 27 | 28 | 29 | GET_AUTHOR = """-- name: get_author \\:one 30 | SELECT id, name, bio FROM authors 31 | WHERE id = :p1 LIMIT 1 32 | """ 33 | 34 | 35 | LIST_AUTHORS = """-- name: list_authors \\:many 36 | SELECT id, name, bio FROM authors 37 | ORDER BY name 38 | """ 39 | 40 | 41 | class Querier: 42 | def __init__(self, conn: sqlalchemy.engine.Connection): 43 | self._conn = conn 44 | 45 | def create_author(self, *, name: str, bio: Optional[str]) -> Optional[models.Author]: 46 | row = self._conn.execute(sqlalchemy.text(CREATE_AUTHOR), {"p1": name, "p2": bio}).first() 47 | if row is None: 48 | return None 49 | return models.Author( 50 | id=row[0], 51 | name=row[1], 52 | bio=row[2], 53 | ) 54 | 55 | def delete_author(self, *, id: int) -> None: 56 | self._conn.execute(sqlalchemy.text(DELETE_AUTHOR), {"p1": id}) 57 | 58 | def get_author(self, *, id: int) -> Optional[models.Author]: 59 | row = self._conn.execute(sqlalchemy.text(GET_AUTHOR), {"p1": id}).first() 60 | if row is None: 61 | return None 62 | return models.Author( 63 | id=row[0], 64 | name=row[1], 65 | bio=row[2], 66 | ) 67 | 68 | def list_authors(self) -> Iterator[models.Author]: 69 | result = self._conn.execute(sqlalchemy.text(LIST_AUTHORS)) 70 | for row in result: 71 | yield models.Author( 72 | id=row[0], 73 | name=row[1], 74 | bio=row[2], 75 | ) 76 | 77 | 78 | class AsyncQuerier: 79 | def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): 80 | self._conn = conn 81 | 82 | async def create_author(self, *, name: str, bio: Optional[str]) -> Optional[models.Author]: 83 | row = (await self._conn.execute(sqlalchemy.text(CREATE_AUTHOR), {"p1": name, "p2": bio})).first() 84 | if row is None: 85 | return None 86 | return models.Author( 87 | id=row[0], 88 | name=row[1], 89 | bio=row[2], 90 | ) 91 | 92 | async def delete_author(self, *, id: int) -> None: 93 | await self._conn.execute(sqlalchemy.text(DELETE_AUTHOR), {"p1": id}) 94 | 95 | async def get_author(self, *, id: int) -> Optional[models.Author]: 96 | row = (await self._conn.execute(sqlalchemy.text(GET_AUTHOR), {"p1": id})).first() 97 | if row is None: 98 | return None 99 | return models.Author( 100 | id=row[0], 101 | name=row[1], 102 | bio=row[2], 103 | ) 104 | 105 | async def list_authors(self) -> AsyncIterator[models.Author]: 106 | result = await self._conn.stream(sqlalchemy.text(LIST_AUTHORS)) 107 | async for row in result: 108 | yield models.Author( 109 | id=row[0], 110 | name=row[1], 111 | bio=row[2], 112 | ) 113 | -------------------------------------------------------------------------------- /examples/src/authors/query.sql: -------------------------------------------------------------------------------- 1 | -- name: GetAuthor :one 2 | SELECT * FROM authors 3 | WHERE id = $1 LIMIT 1; 4 | 5 | -- name: ListAuthors :many 6 | SELECT * FROM authors 7 | ORDER BY name; 8 | 9 | -- name: CreateAuthor :one 10 | INSERT INTO authors ( 11 | name, bio 12 | ) VALUES ( 13 | $1, $2 14 | ) 15 | RETURNING *; 16 | 17 | -- name: DeleteAuthor :exec 18 | DELETE FROM authors 19 | WHERE id = $1; 20 | -------------------------------------------------------------------------------- /examples/src/authors/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE authors ( 2 | id BIGSERIAL PRIMARY KEY, 3 | name text NOT NULL, 4 | bio text 5 | ); 6 | -------------------------------------------------------------------------------- /examples/src/booktest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlc-dev/sqlc-gen-python/53fa0b2e3d10c4201f7a5a344d00a560330da3bb/examples/src/booktest/__init__.py -------------------------------------------------------------------------------- /examples/src/booktest/models.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | import dataclasses 5 | import datetime 6 | import enum 7 | from typing import List 8 | 9 | 10 | class BookType(str, enum.Enum): 11 | FICTION = "FICTION" 12 | NONFICTION = "NONFICTION" 13 | 14 | 15 | @dataclasses.dataclass() 16 | class Author: 17 | author_id: int 18 | name: str 19 | 20 | 21 | @dataclasses.dataclass() 22 | class Book: 23 | book_id: int 24 | author_id: int 25 | isbn: str 26 | book_type: BookType 27 | title: str 28 | year: int 29 | available: datetime.datetime 30 | tags: List[str] 31 | -------------------------------------------------------------------------------- /examples/src/booktest/query.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | # source: query.sql 5 | import dataclasses 6 | import datetime 7 | from typing import AsyncIterator, List, Optional 8 | 9 | import sqlalchemy 10 | import sqlalchemy.ext.asyncio 11 | 12 | from booktest import models 13 | 14 | 15 | BOOKS_BY_TAGS = """-- name: books_by_tags \\:many 16 | SELECT 17 | book_id, 18 | title, 19 | name, 20 | isbn, 21 | tags 22 | FROM books 23 | LEFT JOIN authors ON books.author_id = authors.author_id 24 | WHERE tags && :p1\\:\\:varchar[] 25 | """ 26 | 27 | 28 | @dataclasses.dataclass() 29 | class BooksByTagsRow: 30 | book_id: int 31 | title: str 32 | name: Optional[str] 33 | isbn: str 34 | tags: List[str] 35 | 36 | 37 | BOOKS_BY_TITLE_YEAR = """-- name: books_by_title_year \\:many 38 | SELECT book_id, author_id, isbn, book_type, title, year, available, tags FROM books 39 | WHERE title = :p1 AND year = :p2 40 | """ 41 | 42 | 43 | CREATE_AUTHOR = """-- name: create_author \\:one 44 | INSERT INTO authors (name) VALUES (:p1) 45 | RETURNING author_id, name 46 | """ 47 | 48 | 49 | CREATE_BOOK = """-- name: create_book \\:one 50 | INSERT INTO books ( 51 | author_id, 52 | isbn, 53 | book_type, 54 | title, 55 | year, 56 | available, 57 | tags 58 | ) VALUES ( 59 | :p1, 60 | :p2, 61 | :p3, 62 | :p4, 63 | :p5, 64 | :p6, 65 | :p7 66 | ) 67 | RETURNING book_id, author_id, isbn, book_type, title, year, available, tags 68 | """ 69 | 70 | 71 | @dataclasses.dataclass() 72 | class CreateBookParams: 73 | author_id: int 74 | isbn: str 75 | book_type: models.BookType 76 | title: str 77 | year: int 78 | available: datetime.datetime 79 | tags: List[str] 80 | 81 | 82 | DELETE_BOOK = """-- name: delete_book \\:exec 83 | DELETE FROM books 84 | WHERE book_id = :p1 85 | """ 86 | 87 | 88 | GET_AUTHOR = """-- name: get_author \\:one 89 | SELECT author_id, name FROM authors 90 | WHERE author_id = :p1 91 | """ 92 | 93 | 94 | GET_BOOK = """-- name: get_book \\:one 95 | SELECT book_id, author_id, isbn, book_type, title, year, available, tags FROM books 96 | WHERE book_id = :p1 97 | """ 98 | 99 | 100 | UPDATE_BOOK = """-- name: update_book \\:exec 101 | UPDATE books 102 | SET title = :p1, tags = :p2 103 | WHERE book_id = :p3 104 | """ 105 | 106 | 107 | UPDATE_BOOK_ISBN = """-- name: update_book_isbn \\:exec 108 | UPDATE books 109 | SET title = :p1, tags = :p2, isbn = :p4 110 | WHERE book_id = :p3 111 | """ 112 | 113 | 114 | class AsyncQuerier: 115 | def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): 116 | self._conn = conn 117 | 118 | async def books_by_tags(self, *, dollar_1: List[str]) -> AsyncIterator[BooksByTagsRow]: 119 | result = await self._conn.stream(sqlalchemy.text(BOOKS_BY_TAGS), {"p1": dollar_1}) 120 | async for row in result: 121 | yield BooksByTagsRow( 122 | book_id=row[0], 123 | title=row[1], 124 | name=row[2], 125 | isbn=row[3], 126 | tags=row[4], 127 | ) 128 | 129 | async def books_by_title_year(self, *, title: str, year: int) -> AsyncIterator[models.Book]: 130 | result = await self._conn.stream(sqlalchemy.text(BOOKS_BY_TITLE_YEAR), {"p1": title, "p2": year}) 131 | async for row in result: 132 | yield models.Book( 133 | book_id=row[0], 134 | author_id=row[1], 135 | isbn=row[2], 136 | book_type=row[3], 137 | title=row[4], 138 | year=row[5], 139 | available=row[6], 140 | tags=row[7], 141 | ) 142 | 143 | async def create_author(self, *, name: str) -> Optional[models.Author]: 144 | row = (await self._conn.execute(sqlalchemy.text(CREATE_AUTHOR), {"p1": name})).first() 145 | if row is None: 146 | return None 147 | return models.Author( 148 | author_id=row[0], 149 | name=row[1], 150 | ) 151 | 152 | async def create_book(self, arg: CreateBookParams) -> Optional[models.Book]: 153 | row = (await self._conn.execute(sqlalchemy.text(CREATE_BOOK), { 154 | "p1": arg.author_id, 155 | "p2": arg.isbn, 156 | "p3": arg.book_type, 157 | "p4": arg.title, 158 | "p5": arg.year, 159 | "p6": arg.available, 160 | "p7": arg.tags, 161 | })).first() 162 | if row is None: 163 | return None 164 | return models.Book( 165 | book_id=row[0], 166 | author_id=row[1], 167 | isbn=row[2], 168 | book_type=row[3], 169 | title=row[4], 170 | year=row[5], 171 | available=row[6], 172 | tags=row[7], 173 | ) 174 | 175 | async def delete_book(self, *, book_id: int) -> None: 176 | await self._conn.execute(sqlalchemy.text(DELETE_BOOK), {"p1": book_id}) 177 | 178 | async def get_author(self, *, author_id: int) -> Optional[models.Author]: 179 | row = (await self._conn.execute(sqlalchemy.text(GET_AUTHOR), {"p1": author_id})).first() 180 | if row is None: 181 | return None 182 | return models.Author( 183 | author_id=row[0], 184 | name=row[1], 185 | ) 186 | 187 | async def get_book(self, *, book_id: int) -> Optional[models.Book]: 188 | row = (await self._conn.execute(sqlalchemy.text(GET_BOOK), {"p1": book_id})).first() 189 | if row is None: 190 | return None 191 | return models.Book( 192 | book_id=row[0], 193 | author_id=row[1], 194 | isbn=row[2], 195 | book_type=row[3], 196 | title=row[4], 197 | year=row[5], 198 | available=row[6], 199 | tags=row[7], 200 | ) 201 | 202 | async def update_book(self, *, title: str, tags: List[str], book_id: int) -> None: 203 | await self._conn.execute(sqlalchemy.text(UPDATE_BOOK), {"p1": title, "p2": tags, "p3": book_id}) 204 | 205 | async def update_book_isbn(self, *, title: str, tags: List[str], book_id: int, isbn: str) -> None: 206 | await self._conn.execute(sqlalchemy.text(UPDATE_BOOK_ISBN), { 207 | "p1": title, 208 | "p2": tags, 209 | "p3": book_id, 210 | "p4": isbn, 211 | }) 212 | -------------------------------------------------------------------------------- /examples/src/booktest/query.sql: -------------------------------------------------------------------------------- 1 | -- name: GetAuthor :one 2 | SELECT * FROM authors 3 | WHERE author_id = $1; 4 | 5 | -- name: GetBook :one 6 | SELECT * FROM books 7 | WHERE book_id = $1; 8 | 9 | -- name: DeleteBook :exec 10 | DELETE FROM books 11 | WHERE book_id = $1; 12 | 13 | -- name: BooksByTitleYear :many 14 | SELECT * FROM books 15 | WHERE title = $1 AND year = $2; 16 | 17 | -- name: BooksByTags :many 18 | SELECT 19 | book_id, 20 | title, 21 | name, 22 | isbn, 23 | tags 24 | FROM books 25 | LEFT JOIN authors ON books.author_id = authors.author_id 26 | WHERE tags && $1::varchar[]; 27 | 28 | -- name: CreateAuthor :one 29 | INSERT INTO authors (name) VALUES ($1) 30 | RETURNING *; 31 | 32 | -- name: CreateBook :one 33 | INSERT INTO books ( 34 | author_id, 35 | isbn, 36 | book_type, 37 | title, 38 | year, 39 | available, 40 | tags 41 | ) VALUES ( 42 | $1, 43 | $2, 44 | $3, 45 | $4, 46 | $5, 47 | $6, 48 | $7 49 | ) 50 | RETURNING *; 51 | 52 | -- name: UpdateBook :exec 53 | UPDATE books 54 | SET title = $1, tags = $2 55 | WHERE book_id = $3; 56 | 57 | -- name: UpdateBookISBN :exec 58 | UPDATE books 59 | SET title = $1, tags = $2, isbn = $4 60 | WHERE book_id = $3; 61 | -------------------------------------------------------------------------------- /examples/src/booktest/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE authors ( 2 | author_id SERIAL PRIMARY KEY, 3 | name text NOT NULL DEFAULT '' 4 | ); 5 | 6 | CREATE INDEX authors_name_idx ON authors(name); 7 | 8 | CREATE TYPE book_type AS ENUM ( 9 | 'FICTION', 10 | 'NONFICTION' 11 | ); 12 | 13 | CREATE TABLE books ( 14 | book_id SERIAL PRIMARY KEY, 15 | author_id integer NOT NULL REFERENCES authors(author_id), 16 | isbn text NOT NULL DEFAULT '' UNIQUE, 17 | book_type book_type NOT NULL DEFAULT 'FICTION', 18 | title text NOT NULL DEFAULT '', 19 | year integer NOT NULL DEFAULT 2000, 20 | available timestamp with time zone NOT NULL DEFAULT 'NOW()', 21 | tags varchar[] NOT NULL DEFAULT '{}' 22 | ); 23 | 24 | CREATE INDEX books_title_idx ON books(title, year); 25 | 26 | CREATE FUNCTION say_hello(text) RETURNS text AS $$ 27 | BEGIN 28 | RETURN CONCAT('hello ', $1); 29 | END; 30 | $$ LANGUAGE plpgsql; 31 | 32 | CREATE INDEX books_title_lower_idx ON books(title); 33 | -------------------------------------------------------------------------------- /examples/src/dbtest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlc-dev/sqlc-gen-python/53fa0b2e3d10c4201f7a5a344d00a560330da3bb/examples/src/dbtest/__init__.py -------------------------------------------------------------------------------- /examples/src/dbtest/migrations.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | import sqlalchemy 5 | import sqlalchemy.ext.asyncio 6 | 7 | 8 | def apply_migrations(conn: sqlalchemy.engine.Connection, paths: List[str]): 9 | files = _find_sql_files(paths) 10 | 11 | for file in files: 12 | with open(file, "r") as fd: 13 | blob = fd.read() 14 | stmts = blob.split(";") 15 | for stmt in stmts: 16 | if stmt.strip(): 17 | conn.execute(sqlalchemy.text(stmt)) 18 | 19 | 20 | async def apply_migrations_async(conn: sqlalchemy.ext.asyncio.AsyncConnection, paths: List[str]): 21 | files = _find_sql_files(paths) 22 | 23 | for file in files: 24 | with open(file, "r") as fd: 25 | blob = fd.read() 26 | raw_conn = await conn.get_raw_connection() 27 | # The asyncpg sqlalchemy adapter uses a prepared statement cache which can't handle the migration statements 28 | await raw_conn._connection.execute(blob) 29 | 30 | 31 | def _find_sql_files(paths: List[str]) -> List[str]: 32 | files = [] 33 | for path in paths: 34 | if not os.path.exists(path): 35 | raise FileNotFoundError(f"{path} does not exist") 36 | if os.path.isdir(path): 37 | for file in os.listdir(path): 38 | if file.endswith(".sql"): 39 | files.append(os.path.join(path, file)) 40 | else: 41 | files.append(path) 42 | files.sort() 43 | return files 44 | -------------------------------------------------------------------------------- /examples/src/jets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlc-dev/sqlc-gen-python/53fa0b2e3d10c4201f7a5a344d00a560330da3bb/examples/src/jets/__init__.py -------------------------------------------------------------------------------- /examples/src/jets/models.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | import dataclasses 5 | 6 | 7 | @dataclasses.dataclass() 8 | class Jet: 9 | id: int 10 | pilot_id: int 11 | age: int 12 | name: str 13 | color: str 14 | 15 | 16 | @dataclasses.dataclass() 17 | class Language: 18 | id: int 19 | language: str 20 | 21 | 22 | @dataclasses.dataclass() 23 | class Pilot: 24 | id: int 25 | name: str 26 | 27 | 28 | @dataclasses.dataclass() 29 | class PilotLanguage: 30 | pilot_id: int 31 | language_id: int 32 | -------------------------------------------------------------------------------- /examples/src/jets/query-building.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | # source: query-building.sql 5 | from typing import AsyncIterator, Optional 6 | 7 | import sqlalchemy 8 | import sqlalchemy.ext.asyncio 9 | 10 | from jets import models 11 | 12 | 13 | COUNT_PILOTS = """-- name: count_pilots \\:one 14 | SELECT COUNT(*) FROM pilots 15 | """ 16 | 17 | 18 | DELETE_PILOT = """-- name: delete_pilot \\:exec 19 | DELETE FROM pilots WHERE id = :p1 20 | """ 21 | 22 | 23 | LIST_PILOTS = """-- name: list_pilots \\:many 24 | SELECT id, name FROM pilots LIMIT 5 25 | """ 26 | 27 | 28 | class AsyncQuerier: 29 | def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): 30 | self._conn = conn 31 | 32 | async def count_pilots(self) -> Optional[int]: 33 | row = (await self._conn.execute(sqlalchemy.text(COUNT_PILOTS))).first() 34 | if row is None: 35 | return None 36 | return row[0] 37 | 38 | async def delete_pilot(self, *, id: int) -> None: 39 | await self._conn.execute(sqlalchemy.text(DELETE_PILOT), {"p1": id}) 40 | 41 | async def list_pilots(self) -> AsyncIterator[models.Pilot]: 42 | result = await self._conn.stream(sqlalchemy.text(LIST_PILOTS)) 43 | async for row in result: 44 | yield models.Pilot( 45 | id=row[0], 46 | name=row[1], 47 | ) 48 | -------------------------------------------------------------------------------- /examples/src/jets/query-building.sql: -------------------------------------------------------------------------------- 1 | -- name: CountPilots :one 2 | SELECT COUNT(*) FROM pilots; 3 | 4 | -- name: ListPilots :many 5 | SELECT * FROM pilots LIMIT 5; 6 | 7 | -- name: DeletePilot :exec 8 | DELETE FROM pilots WHERE id = $1; 9 | -------------------------------------------------------------------------------- /examples/src/jets/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE pilots ( 2 | id integer NOT NULL, 3 | name text NOT NULL 4 | ); 5 | 6 | ALTER TABLE pilots ADD CONSTRAINT pilot_pkey PRIMARY KEY (id); 7 | 8 | CREATE TABLE jets ( 9 | id integer NOT NULL, 10 | pilot_id integer NOT NULL, 11 | age integer NOT NULL, 12 | name text NOT NULL, 13 | color text NOT NULL 14 | ); 15 | 16 | ALTER TABLE jets ADD CONSTRAINT jet_pkey PRIMARY KEY (id); 17 | ALTER TABLE jets ADD CONSTRAINT jet_pilots_fkey FOREIGN KEY (pilot_id) REFERENCES pilots(id); 18 | 19 | CREATE TABLE languages ( 20 | id integer NOT NULL, 21 | language text NOT NULL 22 | ); 23 | 24 | ALTER TABLE languages ADD CONSTRAINT language_pkey PRIMARY KEY (id); 25 | 26 | -- Join table 27 | CREATE TABLE pilot_languages ( 28 | pilot_id integer NOT NULL, 29 | language_id integer NOT NULL 30 | ); 31 | 32 | -- Composite primary key 33 | ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_pkey PRIMARY KEY (pilot_id, language_id); 34 | ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_pilots_fkey FOREIGN KEY (pilot_id) REFERENCES pilots(id); 35 | ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_languages_fkey FOREIGN KEY (language_id) REFERENCES languages(id); 36 | -------------------------------------------------------------------------------- /examples/src/ondeck/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlc-dev/sqlc-gen-python/53fa0b2e3d10c4201f7a5a344d00a560330da3bb/examples/src/ondeck/__init__.py -------------------------------------------------------------------------------- /examples/src/ondeck/city.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | # source: city.sql 5 | from typing import AsyncIterator, Optional 6 | 7 | import sqlalchemy 8 | import sqlalchemy.ext.asyncio 9 | 10 | from ondeck import models 11 | 12 | 13 | CREATE_CITY = """-- name: create_city \\:one 14 | INSERT INTO city ( 15 | name, 16 | slug 17 | ) VALUES ( 18 | :p1, 19 | :p2 20 | ) RETURNING slug, name 21 | """ 22 | 23 | 24 | GET_CITY = """-- name: get_city \\:one 25 | SELECT slug, name 26 | FROM city 27 | WHERE slug = :p1 28 | """ 29 | 30 | 31 | LIST_CITIES = """-- name: list_cities \\:many 32 | SELECT slug, name 33 | FROM city 34 | ORDER BY name 35 | """ 36 | 37 | 38 | UPDATE_CITY_NAME = """-- name: update_city_name \\:exec 39 | UPDATE city 40 | SET name = :p2 41 | WHERE slug = :p1 42 | """ 43 | 44 | 45 | class AsyncQuerier: 46 | def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): 47 | self._conn = conn 48 | 49 | async def create_city(self, *, name: str, slug: str) -> Optional[models.City]: 50 | row = (await self._conn.execute(sqlalchemy.text(CREATE_CITY), {"p1": name, "p2": slug})).first() 51 | if row is None: 52 | return None 53 | return models.City( 54 | slug=row[0], 55 | name=row[1], 56 | ) 57 | 58 | async def get_city(self, *, slug: str) -> Optional[models.City]: 59 | row = (await self._conn.execute(sqlalchemy.text(GET_CITY), {"p1": slug})).first() 60 | if row is None: 61 | return None 62 | return models.City( 63 | slug=row[0], 64 | name=row[1], 65 | ) 66 | 67 | async def list_cities(self) -> AsyncIterator[models.City]: 68 | result = await self._conn.stream(sqlalchemy.text(LIST_CITIES)) 69 | async for row in result: 70 | yield models.City( 71 | slug=row[0], 72 | name=row[1], 73 | ) 74 | 75 | async def update_city_name(self, *, slug: str, name: str) -> None: 76 | await self._conn.execute(sqlalchemy.text(UPDATE_CITY_NAME), {"p1": slug, "p2": name}) 77 | -------------------------------------------------------------------------------- /examples/src/ondeck/models.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | import dataclasses 5 | import datetime 6 | import enum 7 | from typing import List, Optional 8 | 9 | 10 | class Status(str, enum.Enum): 11 | """Venues can be either open or closed""" 12 | OPEN = "op!en" 13 | CLOSED = "clo@sed" 14 | 15 | 16 | @dataclasses.dataclass() 17 | class City: 18 | slug: str 19 | name: str 20 | 21 | 22 | @dataclasses.dataclass() 23 | class Venue: 24 | """Venues are places where muisc happens""" 25 | id: int 26 | status: Status 27 | statuses: Optional[List[Status]] 28 | # This value appears in public URLs 29 | slug: str 30 | name: str 31 | city: str 32 | spotify_playlist: str 33 | songkick_id: Optional[str] 34 | tags: Optional[List[str]] 35 | created_at: datetime.datetime 36 | -------------------------------------------------------------------------------- /examples/src/ondeck/query/city.sql: -------------------------------------------------------------------------------- 1 | -- name: ListCities :many 2 | SELECT * 3 | FROM city 4 | ORDER BY name; 5 | 6 | -- name: GetCity :one 7 | SELECT * 8 | FROM city 9 | WHERE slug = $1; 10 | 11 | -- name: CreateCity :one 12 | -- Create a new city. The slug must be unique. 13 | -- This is the second line of the comment 14 | -- This is the third line 15 | INSERT INTO city ( 16 | name, 17 | slug 18 | ) VALUES ( 19 | $1, 20 | $2 21 | ) RETURNING *; 22 | 23 | -- name: UpdateCityName :exec 24 | UPDATE city 25 | SET name = $2 26 | WHERE slug = $1; 27 | -------------------------------------------------------------------------------- /examples/src/ondeck/query/venue.sql: -------------------------------------------------------------------------------- 1 | -- name: ListVenues :many 2 | SELECT * 3 | FROM venue 4 | WHERE city = $1 5 | ORDER BY name; 6 | 7 | -- name: DeleteVenue :exec 8 | DELETE FROM venue 9 | WHERE slug = $1 AND slug = $1; 10 | 11 | -- name: GetVenue :one 12 | SELECT * 13 | FROM venue 14 | WHERE slug = $1 AND city = $2; 15 | 16 | -- name: CreateVenue :one 17 | INSERT INTO venue ( 18 | slug, 19 | name, 20 | city, 21 | created_at, 22 | spotify_playlist, 23 | status, 24 | statuses, 25 | tags 26 | ) VALUES ( 27 | $1, 28 | $2, 29 | $3, 30 | NOW(), 31 | $4, 32 | $5, 33 | $6, 34 | $7 35 | ) RETURNING id; 36 | 37 | -- name: UpdateVenueName :one 38 | UPDATE venue 39 | SET name = $2 40 | WHERE slug = $1 41 | RETURNING id; 42 | 43 | -- name: VenueCountByCity :many 44 | SELECT 45 | city, 46 | count(*) 47 | FROM venue 48 | GROUP BY 1 49 | ORDER BY 1; 50 | -------------------------------------------------------------------------------- /examples/src/ondeck/schema/0001_city.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE city ( 2 | slug text PRIMARY KEY, 3 | name text NOT NULL 4 | ) 5 | -------------------------------------------------------------------------------- /examples/src/ondeck/schema/0002_venue.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE status AS ENUM ('op!en', 'clo@sed'); 2 | COMMENT ON TYPE status IS 'Venues can be either open or closed'; 3 | 4 | CREATE TABLE venues ( 5 | id SERIAL primary key, 6 | dropped text, 7 | status status not null, 8 | statuses status[], 9 | slug text not null, 10 | name varchar(255) not null, 11 | city text not null references city(slug), 12 | spotify_playlist varchar not null, 13 | songkick_id text, 14 | tags text[] 15 | ); 16 | COMMENT ON TABLE venues IS 'Venues are places where muisc happens'; 17 | COMMENT ON COLUMN venues.slug IS 'This value appears in public URLs'; 18 | 19 | -------------------------------------------------------------------------------- /examples/src/ondeck/schema/0003_add_column.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE venues RENAME TO venue; 2 | ALTER TABLE venue ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT NOW(); 3 | ALTER TABLE venue DROP COLUMN dropped; 4 | -------------------------------------------------------------------------------- /examples/src/ondeck/venue.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | # source: venue.sql 5 | import dataclasses 6 | from typing import AsyncIterator, List, Optional 7 | 8 | import sqlalchemy 9 | import sqlalchemy.ext.asyncio 10 | 11 | from ondeck import models 12 | 13 | 14 | CREATE_VENUE = """-- name: create_venue \\:one 15 | INSERT INTO venue ( 16 | slug, 17 | name, 18 | city, 19 | created_at, 20 | spotify_playlist, 21 | status, 22 | statuses, 23 | tags 24 | ) VALUES ( 25 | :p1, 26 | :p2, 27 | :p3, 28 | NOW(), 29 | :p4, 30 | :p5, 31 | :p6, 32 | :p7 33 | ) RETURNING id 34 | """ 35 | 36 | 37 | @dataclasses.dataclass() 38 | class CreateVenueParams: 39 | slug: str 40 | name: str 41 | city: str 42 | spotify_playlist: str 43 | status: models.Status 44 | statuses: Optional[List[models.Status]] 45 | tags: Optional[List[str]] 46 | 47 | 48 | DELETE_VENUE = """-- name: delete_venue \\:exec 49 | DELETE FROM venue 50 | WHERE slug = :p1 AND slug = :p1 51 | """ 52 | 53 | 54 | GET_VENUE = """-- name: get_venue \\:one 55 | SELECT id, status, statuses, slug, name, city, spotify_playlist, songkick_id, tags, created_at 56 | FROM venue 57 | WHERE slug = :p1 AND city = :p2 58 | """ 59 | 60 | 61 | LIST_VENUES = """-- name: list_venues \\:many 62 | SELECT id, status, statuses, slug, name, city, spotify_playlist, songkick_id, tags, created_at 63 | FROM venue 64 | WHERE city = :p1 65 | ORDER BY name 66 | """ 67 | 68 | 69 | UPDATE_VENUE_NAME = """-- name: update_venue_name \\:one 70 | UPDATE venue 71 | SET name = :p2 72 | WHERE slug = :p1 73 | RETURNING id 74 | """ 75 | 76 | 77 | VENUE_COUNT_BY_CITY = """-- name: venue_count_by_city \\:many 78 | SELECT 79 | city, 80 | count(*) 81 | FROM venue 82 | GROUP BY 1 83 | ORDER BY 1 84 | """ 85 | 86 | 87 | @dataclasses.dataclass() 88 | class VenueCountByCityRow: 89 | city: str 90 | count: int 91 | 92 | 93 | class AsyncQuerier: 94 | def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): 95 | self._conn = conn 96 | 97 | async def create_venue(self, arg: CreateVenueParams) -> Optional[int]: 98 | row = (await self._conn.execute(sqlalchemy.text(CREATE_VENUE), { 99 | "p1": arg.slug, 100 | "p2": arg.name, 101 | "p3": arg.city, 102 | "p4": arg.spotify_playlist, 103 | "p5": arg.status, 104 | "p6": arg.statuses, 105 | "p7": arg.tags, 106 | })).first() 107 | if row is None: 108 | return None 109 | return row[0] 110 | 111 | async def delete_venue(self, *, slug: str) -> None: 112 | await self._conn.execute(sqlalchemy.text(DELETE_VENUE), {"p1": slug}) 113 | 114 | async def get_venue(self, *, slug: str, city: str) -> Optional[models.Venue]: 115 | row = (await self._conn.execute(sqlalchemy.text(GET_VENUE), {"p1": slug, "p2": city})).first() 116 | if row is None: 117 | return None 118 | return models.Venue( 119 | id=row[0], 120 | status=row[1], 121 | statuses=row[2], 122 | slug=row[3], 123 | name=row[4], 124 | city=row[5], 125 | spotify_playlist=row[6], 126 | songkick_id=row[7], 127 | tags=row[8], 128 | created_at=row[9], 129 | ) 130 | 131 | async def list_venues(self, *, city: str) -> AsyncIterator[models.Venue]: 132 | result = await self._conn.stream(sqlalchemy.text(LIST_VENUES), {"p1": city}) 133 | async for row in result: 134 | yield models.Venue( 135 | id=row[0], 136 | status=row[1], 137 | statuses=row[2], 138 | slug=row[3], 139 | name=row[4], 140 | city=row[5], 141 | spotify_playlist=row[6], 142 | songkick_id=row[7], 143 | tags=row[8], 144 | created_at=row[9], 145 | ) 146 | 147 | async def update_venue_name(self, *, slug: str, name: str) -> Optional[int]: 148 | row = (await self._conn.execute(sqlalchemy.text(UPDATE_VENUE_NAME), {"p1": slug, "p2": name})).first() 149 | if row is None: 150 | return None 151 | return row[0] 152 | 153 | async def venue_count_by_city(self) -> AsyncIterator[VenueCountByCityRow]: 154 | result = await self._conn.stream(sqlalchemy.text(VENUE_COUNT_BY_CITY)) 155 | async for row in result: 156 | yield VenueCountByCityRow( 157 | city=row[0], 158 | count=row[1], 159 | ) 160 | -------------------------------------------------------------------------------- /examples/src/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlc-dev/sqlc-gen-python/53fa0b2e3d10c4201f7a5a344d00a560330da3bb/examples/src/tests/__init__.py -------------------------------------------------------------------------------- /examples/src/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import random 4 | 5 | import pytest 6 | import sqlalchemy 7 | import sqlalchemy.ext.asyncio 8 | 9 | 10 | @pytest.fixture(scope="session") 11 | def postgres_uri() -> str: 12 | pg_host = os.environ.get("PG_HOST", "postgres") 13 | pg_port = os.environ.get("PG_PORT", 5432) 14 | pg_user = os.environ.get("PG_USER", "postgres") 15 | pg_password = os.environ.get("PG_PASSWORD", "mysecretpassword") 16 | pg_db = os.environ.get("PG_DATABASE", "dinotest") 17 | 18 | return f"postgresql://{pg_user}:{pg_password}@{pg_host}:{pg_port}/{pg_db}" 19 | 20 | 21 | @pytest.fixture(scope="session") 22 | def sqlalchemy_connection(postgres_uri) -> sqlalchemy.engine.Connection: 23 | engine = sqlalchemy.create_engine(postgres_uri, future=True) 24 | with engine.connect() as conn: 25 | yield conn 26 | 27 | 28 | @pytest.fixture(scope="function") 29 | def db(sqlalchemy_connection: sqlalchemy.engine.Connection) -> sqlalchemy.engine.Connection: 30 | conn = sqlalchemy_connection 31 | schema_name = f"sqltest_{random.randint(0, 1000)}" 32 | conn.execute(sqlalchemy.text(f"CREATE SCHEMA {schema_name}")) 33 | conn.execute(sqlalchemy.text(f"SET search_path TO {schema_name}")) 34 | conn.commit() 35 | yield conn 36 | conn.rollback() 37 | conn.execute(sqlalchemy.text(f"DROP SCHEMA {schema_name} CASCADE")) 38 | conn.execute(sqlalchemy.text("SET search_path TO public")) 39 | 40 | 41 | @pytest.fixture(scope="session") 42 | async def async_sqlalchemy_connection(postgres_uri) -> sqlalchemy.ext.asyncio.AsyncConnection: 43 | postgres_uri = postgres_uri.replace("postgresql", "postgresql+asyncpg") 44 | engine = sqlalchemy.ext.asyncio.create_async_engine(postgres_uri) 45 | async with engine.connect() as conn: 46 | yield conn 47 | 48 | 49 | @pytest.fixture(scope="function") 50 | async def async_db(async_sqlalchemy_connection: sqlalchemy.ext.asyncio.AsyncConnection) -> sqlalchemy.ext.asyncio.AsyncConnection: 51 | conn = async_sqlalchemy_connection 52 | schema_name = f"sqltest_{random.randint(0, 1000)}" 53 | await conn.execute(sqlalchemy.text(f"CREATE SCHEMA {schema_name}")) 54 | await conn.execute(sqlalchemy.text(f"SET search_path TO {schema_name}")) 55 | await conn.commit() 56 | yield conn 57 | await conn.rollback() 58 | await conn.execute(sqlalchemy.text(f"DROP SCHEMA {schema_name} CASCADE")) 59 | await conn.execute(sqlalchemy.text("SET search_path TO public")) 60 | 61 | 62 | @pytest.fixture(scope="session") 63 | def event_loop(): 64 | """Change event_loop fixture to session level.""" 65 | loop = asyncio.get_event_loop_policy().new_event_loop() 66 | yield loop 67 | loop.close() 68 | -------------------------------------------------------------------------------- /examples/src/tests/test_authors.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | import sqlalchemy.ext.asyncio 5 | 6 | from authors import query 7 | from dbtest.migrations import apply_migrations, apply_migrations_async 8 | 9 | 10 | def test_authors(db: sqlalchemy.engine.Connection): 11 | apply_migrations(db, [os.path.dirname(__file__) + "/../authors/schema.sql"]) 12 | 13 | querier = query.Querier(db) 14 | 15 | authors = list(querier.list_authors()) 16 | assert authors == [] 17 | 18 | author_name = "Brian Kernighan" 19 | author_bio = "Co-author of The C Programming Language and The Go Programming Language" 20 | new_author = querier.create_author(name=author_name, bio=author_bio) 21 | assert new_author.id > 0 22 | assert new_author.name == author_name 23 | assert new_author.bio == author_bio 24 | 25 | db_author = querier.get_author(id=new_author.id) 26 | assert db_author == new_author 27 | 28 | author_list = list(querier.list_authors()) 29 | assert len(author_list) == 1 30 | assert author_list[0] == new_author 31 | 32 | 33 | @pytest.mark.asyncio 34 | async def test_authors_async(async_db: sqlalchemy.ext.asyncio.AsyncConnection): 35 | await apply_migrations_async(async_db, [os.path.dirname(__file__) + "/../authors/schema.sql"]) 36 | 37 | querier = query.AsyncQuerier(async_db) 38 | 39 | async for _ in querier.list_authors(): 40 | assert False, "No authors should exist" 41 | 42 | author_name = "Brian Kernighan" 43 | author_bio = "Co-author of The C Programming Language and The Go Programming Language" 44 | new_author = await querier.create_author(name=author_name, bio=author_bio) 45 | assert new_author.id > 0 46 | assert new_author.name == author_name 47 | assert new_author.bio == author_bio 48 | 49 | db_author = await querier.get_author(id=new_author.id) 50 | assert db_author == new_author 51 | 52 | author_list = [] 53 | async for author in querier.list_authors(): 54 | author_list.append(author) 55 | assert len(author_list) == 1 56 | assert author_list[0] == new_author 57 | -------------------------------------------------------------------------------- /examples/src/tests/test_booktest.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | 4 | import pytest 5 | import sqlalchemy.ext.asyncio 6 | 7 | from booktest import query, models 8 | from dbtest.migrations import apply_migrations_async 9 | 10 | 11 | @pytest.mark.asyncio 12 | async def test_books(async_db: sqlalchemy.ext.asyncio.AsyncConnection): 13 | await apply_migrations_async(async_db, [os.path.dirname(__file__) + "/../booktest/schema.sql"]) 14 | 15 | querier = query.AsyncQuerier(async_db) 16 | 17 | author = await querier.create_author(name="Unknown Master") 18 | assert author is not None 19 | 20 | now = datetime.datetime.now() 21 | await querier.create_book(query.CreateBookParams( 22 | author_id=author.author_id, 23 | isbn="1", 24 | title="my book title", 25 | book_type=models.BookType.FICTION, 26 | year=2016, 27 | available=now, 28 | tags=[], 29 | )) 30 | 31 | b1 = await querier.create_book(query.CreateBookParams( 32 | author_id=author.author_id, 33 | isbn="2", 34 | title="the second book", 35 | book_type=models.BookType.FICTION, 36 | year=2016, 37 | available=now, 38 | tags=["cool", "unique"], 39 | )) 40 | 41 | await querier.update_book(book_id=b1.book_id, title="changed second title", tags=["cool", "disastor"]) 42 | 43 | b3 = await querier.create_book(query.CreateBookParams( 44 | author_id=author.author_id, 45 | isbn="3", 46 | title="the third book", 47 | book_type=models.BookType.FICTION, 48 | year=2001, 49 | available=now, 50 | tags=["cool"], 51 | )) 52 | 53 | b4 = await querier.create_book(query.CreateBookParams( 54 | author_id=author.author_id, 55 | isbn="4", 56 | title="4th place finisher", 57 | book_type=models.BookType.NONFICTION, 58 | year=2011, 59 | available=now, 60 | tags=["other"], 61 | )) 62 | 63 | await querier.update_book_isbn(book_id=b4.book_id, isbn="NEW ISBN", title="never ever gonna finish, a quatrain", tags=["someother"]) 64 | 65 | books0 = querier.books_by_title_year(title="my book title", year=2016) 66 | expected_titles = {"my book title"} 67 | async for book in books0: 68 | expected_titles.remove(book.title) # raises a key error if the title does not exist 69 | assert len(book.tags) == 0 70 | 71 | author = await querier.get_author(author_id=book.author_id) 72 | assert author.name == "Unknown Master" 73 | assert len(expected_titles) == 0 74 | 75 | books = querier.books_by_tags(dollar_1=["cool", "other", "someother"]) 76 | expected_titles = {"changed second title", "the third book", "never ever gonna finish, a quatrain"} 77 | async for book in books: 78 | expected_titles.remove(book.title) 79 | assert len(expected_titles) == 0 80 | 81 | b5 = await querier.get_book(book_id=b3.book_id) 82 | assert b5 is not None 83 | await querier.delete_book(book_id=b5.book_id) 84 | b6 = await querier.get_book(book_id=b5.book_id) 85 | assert b6 is None 86 | -------------------------------------------------------------------------------- /examples/src/tests/test_ondeck.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | import sqlalchemy.ext.asyncio 5 | 6 | from ondeck import models 7 | from ondeck import city as city_queries 8 | from ondeck import venue as venue_queries 9 | from dbtest.migrations import apply_migrations_async 10 | 11 | 12 | @pytest.mark.asyncio 13 | async def test_ondeck(async_db: sqlalchemy.ext.asyncio.AsyncConnection): 14 | await apply_migrations_async(async_db, [os.path.dirname(__file__) + "/../ondeck/schema"]) 15 | 16 | city_querier = city_queries.AsyncQuerier(async_db) 17 | venue_querier = venue_queries.AsyncQuerier(async_db) 18 | 19 | city = await city_querier.create_city(slug="san-francisco", name="San Francisco") 20 | assert city is not None 21 | 22 | venue_id = await venue_querier.create_venue(venue_queries.CreateVenueParams( 23 | slug="the-fillmore", 24 | name="The Fillmore", 25 | city=city.slug, 26 | spotify_playlist="spotify:uri", 27 | status=models.Status.OPEN, 28 | statuses=[models.Status.OPEN, models.Status.CLOSED], 29 | tags=["rock", "punk"], 30 | )) 31 | assert venue_id is not None 32 | 33 | venue = await venue_querier.get_venue(slug="the-fillmore", city=city.slug) 34 | assert venue is not None 35 | assert venue.id == venue_id 36 | 37 | assert city == await city_querier.get_city(slug=city.slug) 38 | assert [venue_queries.VenueCountByCityRow(city=city.slug, count=1)] == await _to_list(venue_querier.venue_count_by_city()) 39 | assert [city] == await _to_list(city_querier.list_cities()) 40 | assert [venue] == await _to_list(venue_querier.list_venues(city=city.slug)) 41 | 42 | await city_querier.update_city_name(slug=city.slug, name="SF") 43 | _id = await venue_querier.update_venue_name(slug=venue.slug, name="Fillmore") 44 | assert _id == venue_id 45 | 46 | await venue_querier.delete_venue(slug=venue.slug) 47 | 48 | 49 | async def _to_list(it): 50 | out = [] 51 | async for i in it: 52 | out.append(i) 53 | return out 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sqlc-dev/sqlc-gen-python 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/google/go-cmp v0.6.0 7 | github.com/jinzhu/inflection v1.0.0 8 | github.com/sqlc-dev/plugin-sdk-go v1.23.0 9 | google.golang.org/protobuf v1.34.2 10 | ) 11 | 12 | require ( 13 | github.com/golang/protobuf v1.5.3 // indirect 14 | golang.org/x/net v0.14.0 // indirect 15 | golang.org/x/sys v0.11.0 // indirect 16 | golang.org/x/text v0.12.0 // indirect 17 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect 18 | google.golang.org/grpc v1.59.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 2 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 3 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 4 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 5 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 6 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 7 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 8 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 9 | github.com/sqlc-dev/plugin-sdk-go v1.23.0 h1:iSeJhnXPlbDXlbzUEebw/DxsGzE9rdDJArl8Hvt0RMM= 10 | github.com/sqlc-dev/plugin-sdk-go v1.23.0/go.mod h1:I1r4THOfyETD+LI2gogN2LX8wCjwUZrgy/NU4In3llA= 11 | golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= 12 | golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 13 | golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= 14 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 15 | golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= 16 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 17 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 18 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= 19 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= 20 | google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= 21 | google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= 22 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 23 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 24 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 25 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 26 | -------------------------------------------------------------------------------- /internal/config.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | type Config struct { 4 | EmitExactTableNames bool `json:"emit_exact_table_names"` 5 | EmitSyncQuerier bool `json:"emit_sync_querier"` 6 | EmitAsyncQuerier bool `json:"emit_async_querier"` 7 | Package string `json:"package"` 8 | Out string `json:"out"` 9 | EmitPydanticModels bool `json:"emit_pydantic_models"` 10 | EmitStrEnum bool `json:"emit_str_enum"` 11 | QueryParameterLimit *int32 `json:"query_parameter_limit"` 12 | InflectionExcludeTableNames []string `json:"inflection_exclude_table_names"` 13 | } 14 | -------------------------------------------------------------------------------- /internal/endtoend/endtoend_test.go: -------------------------------------------------------------------------------- 1 | package endtoend 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "regexp" 11 | "testing" 12 | 13 | "github.com/google/go-cmp/cmp" 14 | ) 15 | 16 | func FindTests(t *testing.T, root string) []string { 17 | t.Helper() 18 | var dirs []string 19 | err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 20 | if err != nil { 21 | return err 22 | } 23 | if info.Name() == "sqlc.yaml" { 24 | dirs = append(dirs, filepath.Dir(path)) 25 | return filepath.SkipDir 26 | } 27 | return nil 28 | }) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | return dirs 33 | } 34 | 35 | func LookPath(t *testing.T, cmds ...string) string { 36 | t.Helper() 37 | for _, cmd := range cmds { 38 | path, err := exec.LookPath(cmd) 39 | if err == nil { 40 | return path 41 | } 42 | } 43 | t.Fatalf("could not find command(s) in $PATH: %s", cmds) 44 | return "" 45 | } 46 | 47 | func ExpectedOutput(t *testing.T, dir string) []byte { 48 | t.Helper() 49 | path := filepath.Join(dir, "stderr.txt") 50 | if _, err := os.Stat(path); err != nil { 51 | if os.IsNotExist(err) { 52 | return []byte{} 53 | } else { 54 | t.Fatal(err) 55 | } 56 | } 57 | output, err := os.ReadFile(path) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | return output 62 | } 63 | 64 | var pattern = regexp.MustCompile(`sha256: ".*"`) 65 | 66 | func TestGenerate(t *testing.T) { 67 | // The SHA256 is required, so we calculate it and then update all of the 68 | // sqlc.yaml files. 69 | // TODO: Remove this once sqlc v1.24.0 has been released 70 | wasmpath := filepath.Join("..", "..", "bin", "sqlc-gen-python.wasm") 71 | if _, err := os.Stat(wasmpath); err != nil { 72 | t.Fatalf("sqlc-gen-python.wasm not found: %s", err) 73 | } 74 | wmod, err := os.ReadFile(wasmpath) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | sum := sha256.Sum256(wmod) 79 | sha256 := fmt.Sprintf("%x", sum) 80 | 81 | sqlc := LookPath(t, "sqlc-dev", "sqlc") 82 | 83 | for _, dir := range FindTests(t, "testdata") { 84 | dir := dir 85 | t.Run(dir, func(t *testing.T) { 86 | // Check if sqlc.yaml has the correct SHA256 for the plugin. If not, update the file 87 | // TODO: Remove this once sqlc v1.24.0 has been released 88 | yaml, err := os.ReadFile(filepath.Join(dir, "sqlc.yaml")) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | if !bytes.Contains(yaml, []byte(sha256)) { 93 | yaml = pattern.ReplaceAllLiteral(yaml, []byte(`sha256: "`+sha256+`"`)) 94 | if err := os.WriteFile(filepath.Join(dir, "sqlc.yaml"), yaml, 0644); err != nil { 95 | t.Fatal(err) 96 | } 97 | } 98 | 99 | want := ExpectedOutput(t, dir) 100 | cmd := exec.Command(sqlc, "diff") 101 | cmd.Dir = dir 102 | got, err := cmd.CombinedOutput() 103 | if diff := cmp.Diff(string(want), string(got)); diff != "" { 104 | t.Errorf("sqlc diff mismatch (-want +got):\n%s", diff) 105 | } 106 | if len(want) == 0 && err != nil { 107 | t.Error(err) 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/emit_pydantic_models/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlc-dev/sqlc-gen-python/53fa0b2e3d10c4201f7a5a344d00a560330da3bb/internal/endtoend/testdata/emit_pydantic_models/db/__init__.py -------------------------------------------------------------------------------- /internal/endtoend/testdata/emit_pydantic_models/db/models.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | import pydantic 5 | from typing import Optional 6 | 7 | 8 | class Author(pydantic.BaseModel): 9 | id: int 10 | name: str 11 | bio: Optional[str] 12 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/emit_pydantic_models/db/query.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | # source: query.sql 5 | from typing import AsyncIterator, Iterator, Optional 6 | 7 | import sqlalchemy 8 | import sqlalchemy.ext.asyncio 9 | 10 | from db import models 11 | 12 | 13 | CREATE_AUTHOR = """-- name: create_author \\:one 14 | INSERT INTO authors ( 15 | name, bio 16 | ) VALUES ( 17 | :p1, :p2 18 | ) 19 | RETURNING id, name, bio 20 | """ 21 | 22 | 23 | DELETE_AUTHOR = """-- name: delete_author \\:exec 24 | DELETE FROM authors 25 | WHERE id = :p1 26 | """ 27 | 28 | 29 | GET_AUTHOR = """-- name: get_author \\:one 30 | SELECT id, name, bio FROM authors 31 | WHERE id = :p1 LIMIT 1 32 | """ 33 | 34 | 35 | LIST_AUTHORS = """-- name: list_authors \\:many 36 | SELECT id, name, bio FROM authors 37 | ORDER BY name 38 | """ 39 | 40 | 41 | class Querier: 42 | def __init__(self, conn: sqlalchemy.engine.Connection): 43 | self._conn = conn 44 | 45 | def create_author(self, *, name: str, bio: Optional[str]) -> Optional[models.Author]: 46 | row = self._conn.execute(sqlalchemy.text(CREATE_AUTHOR), {"p1": name, "p2": bio}).first() 47 | if row is None: 48 | return None 49 | return models.Author( 50 | id=row[0], 51 | name=row[1], 52 | bio=row[2], 53 | ) 54 | 55 | def delete_author(self, *, id: int) -> None: 56 | self._conn.execute(sqlalchemy.text(DELETE_AUTHOR), {"p1": id}) 57 | 58 | def get_author(self, *, id: int) -> Optional[models.Author]: 59 | row = self._conn.execute(sqlalchemy.text(GET_AUTHOR), {"p1": id}).first() 60 | if row is None: 61 | return None 62 | return models.Author( 63 | id=row[0], 64 | name=row[1], 65 | bio=row[2], 66 | ) 67 | 68 | def list_authors(self) -> Iterator[models.Author]: 69 | result = self._conn.execute(sqlalchemy.text(LIST_AUTHORS)) 70 | for row in result: 71 | yield models.Author( 72 | id=row[0], 73 | name=row[1], 74 | bio=row[2], 75 | ) 76 | 77 | 78 | class AsyncQuerier: 79 | def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): 80 | self._conn = conn 81 | 82 | async def create_author(self, *, name: str, bio: Optional[str]) -> Optional[models.Author]: 83 | row = (await self._conn.execute(sqlalchemy.text(CREATE_AUTHOR), {"p1": name, "p2": bio})).first() 84 | if row is None: 85 | return None 86 | return models.Author( 87 | id=row[0], 88 | name=row[1], 89 | bio=row[2], 90 | ) 91 | 92 | async def delete_author(self, *, id: int) -> None: 93 | await self._conn.execute(sqlalchemy.text(DELETE_AUTHOR), {"p1": id}) 94 | 95 | async def get_author(self, *, id: int) -> Optional[models.Author]: 96 | row = (await self._conn.execute(sqlalchemy.text(GET_AUTHOR), {"p1": id})).first() 97 | if row is None: 98 | return None 99 | return models.Author( 100 | id=row[0], 101 | name=row[1], 102 | bio=row[2], 103 | ) 104 | 105 | async def list_authors(self) -> AsyncIterator[models.Author]: 106 | result = await self._conn.stream(sqlalchemy.text(LIST_AUTHORS)) 107 | async for row in result: 108 | yield models.Author( 109 | id=row[0], 110 | name=row[1], 111 | bio=row[2], 112 | ) 113 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/emit_pydantic_models/query.sql: -------------------------------------------------------------------------------- 1 | -- name: GetAuthor :one 2 | SELECT * FROM authors 3 | WHERE id = $1 LIMIT 1; 4 | 5 | -- name: ListAuthors :many 6 | SELECT * FROM authors 7 | ORDER BY name; 8 | 9 | -- name: CreateAuthor :one 10 | INSERT INTO authors ( 11 | name, bio 12 | ) VALUES ( 13 | $1, $2 14 | ) 15 | RETURNING *; 16 | 17 | -- name: DeleteAuthor :exec 18 | DELETE FROM authors 19 | WHERE id = $1; 20 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/emit_pydantic_models/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE authors ( 2 | id BIGSERIAL PRIMARY KEY, 3 | name text NOT NULL, 4 | bio text 5 | ); 6 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/emit_pydantic_models/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | plugins: 3 | - name: py 4 | wasm: 5 | url: file://../../../../bin/sqlc-gen-python.wasm 6 | sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" 7 | sql: 8 | - schema: schema.sql 9 | queries: query.sql 10 | engine: postgresql 11 | codegen: 12 | - plugin: py 13 | out: db 14 | options: 15 | package: db 16 | emit_sync_querier: true 17 | emit_async_querier: true 18 | emit_pydantic_models: true -------------------------------------------------------------------------------- /internal/endtoend/testdata/emit_str_enum/db/models.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | import dataclasses 5 | import enum 6 | from typing import Optional 7 | 8 | 9 | class BookStatus(enum.StrEnum): 10 | AVAILABLE = "available" 11 | CHECKED_OUT = "checked_out" 12 | OVERDUE = "overdue" 13 | 14 | 15 | @dataclasses.dataclass() 16 | class Book: 17 | id: int 18 | title: str 19 | status: Optional[BookStatus] 20 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/emit_str_enum/db/query.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | # source: query.sql 5 | from typing import AsyncIterator, Iterator, Optional 6 | 7 | import sqlalchemy 8 | import sqlalchemy.ext.asyncio 9 | 10 | from db import models 11 | 12 | 13 | CREATE_BOOK = """-- name: create_book \\:one 14 | INSERT INTO books ( 15 | title, status 16 | ) VALUES ( 17 | :p1, :p2 18 | ) RETURNING id, title, status 19 | """ 20 | 21 | 22 | DELETE_BOOK = """-- name: delete_book \\:exec 23 | DELETE FROM books 24 | WHERE id = :p1 25 | """ 26 | 27 | 28 | GET_BOOK = """-- name: get_book \\:one 29 | SELECT id, title, status FROM books 30 | WHERE id = :p1 LIMIT 1 31 | """ 32 | 33 | 34 | LIST_BOOKS = """-- name: list_books \\:many 35 | SELECT id, title, status FROM books 36 | ORDER BY title 37 | """ 38 | 39 | 40 | class Querier: 41 | def __init__(self, conn: sqlalchemy.engine.Connection): 42 | self._conn = conn 43 | 44 | def create_book(self, *, title: str, status: Optional[models.BookStatus]) -> Optional[models.Book]: 45 | row = self._conn.execute(sqlalchemy.text(CREATE_BOOK), {"p1": title, "p2": status}).first() 46 | if row is None: 47 | return None 48 | return models.Book( 49 | id=row[0], 50 | title=row[1], 51 | status=row[2], 52 | ) 53 | 54 | def delete_book(self, *, id: int) -> None: 55 | self._conn.execute(sqlalchemy.text(DELETE_BOOK), {"p1": id}) 56 | 57 | def get_book(self, *, id: int) -> Optional[models.Book]: 58 | row = self._conn.execute(sqlalchemy.text(GET_BOOK), {"p1": id}).first() 59 | if row is None: 60 | return None 61 | return models.Book( 62 | id=row[0], 63 | title=row[1], 64 | status=row[2], 65 | ) 66 | 67 | def list_books(self) -> Iterator[models.Book]: 68 | result = self._conn.execute(sqlalchemy.text(LIST_BOOKS)) 69 | for row in result: 70 | yield models.Book( 71 | id=row[0], 72 | title=row[1], 73 | status=row[2], 74 | ) 75 | 76 | 77 | class AsyncQuerier: 78 | def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): 79 | self._conn = conn 80 | 81 | async def create_book(self, *, title: str, status: Optional[models.BookStatus]) -> Optional[models.Book]: 82 | row = (await self._conn.execute(sqlalchemy.text(CREATE_BOOK), {"p1": title, "p2": status})).first() 83 | if row is None: 84 | return None 85 | return models.Book( 86 | id=row[0], 87 | title=row[1], 88 | status=row[2], 89 | ) 90 | 91 | async def delete_book(self, *, id: int) -> None: 92 | await self._conn.execute(sqlalchemy.text(DELETE_BOOK), {"p1": id}) 93 | 94 | async def get_book(self, *, id: int) -> Optional[models.Book]: 95 | row = (await self._conn.execute(sqlalchemy.text(GET_BOOK), {"p1": id})).first() 96 | if row is None: 97 | return None 98 | return models.Book( 99 | id=row[0], 100 | title=row[1], 101 | status=row[2], 102 | ) 103 | 104 | async def list_books(self) -> AsyncIterator[models.Book]: 105 | result = await self._conn.stream(sqlalchemy.text(LIST_BOOKS)) 106 | async for row in result: 107 | yield models.Book( 108 | id=row[0], 109 | title=row[1], 110 | status=row[2], 111 | ) 112 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/emit_str_enum/query.sql: -------------------------------------------------------------------------------- 1 | -- name: GetBook :one 2 | SELECT * FROM books 3 | WHERE id = $1 LIMIT 1; 4 | 5 | -- name: ListBooks :many 6 | SELECT * FROM books 7 | ORDER BY title; 8 | 9 | -- name: CreateBook :one 10 | INSERT INTO books ( 11 | title, status 12 | ) VALUES ( 13 | $1, $2 14 | ) RETURNING *; 15 | 16 | -- name: DeleteBook :exec 17 | DELETE FROM books 18 | WHERE id = $1; 19 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/emit_str_enum/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TYPE book_status AS ENUM ('available', 'checked_out', 'overdue'); 2 | 3 | 4 | CREATE TABLE books ( 5 | id BIGSERIAL PRIMARY KEY, 6 | title text NOT NULL, 7 | status book_status DEFAULT 'available' 8 | ); 9 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/emit_str_enum/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | plugins: 3 | - name: py 4 | wasm: 5 | url: file://../../../../bin/sqlc-gen-python.wasm 6 | sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" 7 | sql: 8 | - schema: schema.sql 9 | queries: query.sql 10 | engine: postgresql 11 | codegen: 12 | - plugin: py 13 | out: db 14 | options: 15 | package: db 16 | emit_sync_querier: true 17 | emit_async_querier: true 18 | emit_str_enum: true 19 | 20 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/exec_result/python/models.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | import dataclasses 5 | 6 | 7 | @dataclasses.dataclass() 8 | class Bar: 9 | id: int 10 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/exec_result/python/query.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | # source: query.sql 5 | import sqlalchemy 6 | import sqlalchemy.ext.asyncio 7 | 8 | from querytest import models 9 | 10 | 11 | DELETE_BAR_BY_ID = """-- name: delete_bar_by_id \\:execresult 12 | DELETE FROM bar WHERE id = :p1 13 | """ 14 | 15 | 16 | class Querier: 17 | def __init__(self, conn: sqlalchemy.engine.Connection): 18 | self._conn = conn 19 | 20 | def delete_bar_by_id(self, *, id: int) -> sqlalchemy.engine.Result: 21 | return self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) 22 | 23 | 24 | class AsyncQuerier: 25 | def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): 26 | self._conn = conn 27 | 28 | async def delete_bar_by_id(self, *, id: int) -> sqlalchemy.engine.Result: 29 | return await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) 30 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/exec_result/query.sql: -------------------------------------------------------------------------------- 1 | -- name: DeleteBarByID :execresult 2 | DELETE FROM bar WHERE id = $1; 3 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/exec_result/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE bar (id serial not null); 2 | 3 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/exec_result/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | plugins: 3 | - name: py 4 | wasm: 5 | url: file://../../../../bin/sqlc-gen-python.wasm 6 | sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" 7 | sql: 8 | - schema: schema.sql 9 | queries: query.sql 10 | engine: postgresql 11 | codegen: 12 | - plugin: py 13 | out: python 14 | options: 15 | package: querytest 16 | emit_sync_querier: true 17 | emit_async_querier: true 18 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/exec_rows/python/models.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | import dataclasses 5 | 6 | 7 | @dataclasses.dataclass() 8 | class Bar: 9 | id: int 10 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/exec_rows/python/query.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | # source: query.sql 5 | import sqlalchemy 6 | import sqlalchemy.ext.asyncio 7 | 8 | from querytest import models 9 | 10 | 11 | DELETE_BAR_BY_ID = """-- name: delete_bar_by_id \\:execrows 12 | DELETE FROM bar WHERE id = :p1 13 | """ 14 | 15 | 16 | class Querier: 17 | def __init__(self, conn: sqlalchemy.engine.Connection): 18 | self._conn = conn 19 | 20 | def delete_bar_by_id(self, *, id: int) -> int: 21 | result = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) 22 | return result.rowcount 23 | 24 | 25 | class AsyncQuerier: 26 | def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): 27 | self._conn = conn 28 | 29 | async def delete_bar_by_id(self, *, id: int) -> int: 30 | result = await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) 31 | return result.rowcount 32 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/exec_rows/query.sql: -------------------------------------------------------------------------------- 1 | -- name: DeleteBarByID :execrows 2 | DELETE FROM bar WHERE id = $1; 3 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/exec_rows/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE bar (id serial not null); 2 | 3 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/exec_rows/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | plugins: 3 | - name: py 4 | wasm: 5 | url: file://../../../../bin/sqlc-gen-python.wasm 6 | sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" 7 | sql: 8 | - schema: schema.sql 9 | queries: query.sql 10 | engine: postgresql 11 | codegen: 12 | - plugin: py 13 | out: python 14 | options: 15 | package: querytest 16 | emit_sync_querier: true 17 | emit_async_querier: true 18 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/inflection_exclude_table_names/python/models.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | import dataclasses 5 | 6 | 7 | @dataclasses.dataclass() 8 | class Bar: 9 | id: int 10 | name: str 11 | 12 | 13 | @dataclasses.dataclass() 14 | class Exclusions: 15 | id: int 16 | name: str 17 | 18 | 19 | @dataclasses.dataclass() 20 | class MyData: 21 | id: int 22 | name: str 23 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/inflection_exclude_table_names/python/query.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | # source: query.sql 5 | from typing import Optional 6 | 7 | import sqlalchemy 8 | import sqlalchemy.ext.asyncio 9 | 10 | from querytest import models 11 | 12 | 13 | DELETE_BAR_BY_ID = """-- name: delete_bar_by_id \\:one 14 | DELETE FROM bars WHERE id = :p1 RETURNING id, name 15 | """ 16 | 17 | 18 | DELETE_EXCLUSION_BY_ID = """-- name: delete_exclusion_by_id \\:one 19 | DELETE FROM exclusions WHERE id = :p1 RETURNING id, name 20 | """ 21 | 22 | 23 | DELETE_MY_DATA_BY_ID = """-- name: delete_my_data_by_id \\:one 24 | DELETE FROM my_data WHERE id = :p1 RETURNING id, name 25 | """ 26 | 27 | 28 | class Querier: 29 | def __init__(self, conn: sqlalchemy.engine.Connection): 30 | self._conn = conn 31 | 32 | def delete_bar_by_id(self, *, id: int) -> Optional[models.Bar]: 33 | row = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}).first() 34 | if row is None: 35 | return None 36 | return models.Bar( 37 | id=row[0], 38 | name=row[1], 39 | ) 40 | 41 | def delete_exclusion_by_id(self, *, id: int) -> Optional[models.Exclusions]: 42 | row = self._conn.execute(sqlalchemy.text(DELETE_EXCLUSION_BY_ID), {"p1": id}).first() 43 | if row is None: 44 | return None 45 | return models.Exclusions( 46 | id=row[0], 47 | name=row[1], 48 | ) 49 | 50 | def delete_my_data_by_id(self, *, id: int) -> Optional[models.MyData]: 51 | row = self._conn.execute(sqlalchemy.text(DELETE_MY_DATA_BY_ID), {"p1": id}).first() 52 | if row is None: 53 | return None 54 | return models.MyData( 55 | id=row[0], 56 | name=row[1], 57 | ) 58 | 59 | 60 | class AsyncQuerier: 61 | def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): 62 | self._conn = conn 63 | 64 | async def delete_bar_by_id(self, *, id: int) -> Optional[models.Bar]: 65 | row = (await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id})).first() 66 | if row is None: 67 | return None 68 | return models.Bar( 69 | id=row[0], 70 | name=row[1], 71 | ) 72 | 73 | async def delete_exclusion_by_id(self, *, id: int) -> Optional[models.Exclusions]: 74 | row = (await self._conn.execute(sqlalchemy.text(DELETE_EXCLUSION_BY_ID), {"p1": id})).first() 75 | if row is None: 76 | return None 77 | return models.Exclusions( 78 | id=row[0], 79 | name=row[1], 80 | ) 81 | 82 | async def delete_my_data_by_id(self, *, id: int) -> Optional[models.MyData]: 83 | row = (await self._conn.execute(sqlalchemy.text(DELETE_MY_DATA_BY_ID), {"p1": id})).first() 84 | if row is None: 85 | return None 86 | return models.MyData( 87 | id=row[0], 88 | name=row[1], 89 | ) 90 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/inflection_exclude_table_names/query.sql: -------------------------------------------------------------------------------- 1 | -- name: DeleteBarByID :one 2 | DELETE FROM bars WHERE id = $1 RETURNING id, name; 3 | 4 | -- name: DeleteMyDataByID :one 5 | DELETE FROM my_data WHERE id = $1 RETURNING id, name; 6 | 7 | -- name: DeleteExclusionByID :one 8 | DELETE FROM exclusions WHERE id = $1 RETURNING id, name; 9 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/inflection_exclude_table_names/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE bars (id serial not null, name text not null, primary key (id)); 2 | CREATE TABLE my_data (id serial not null, name text not null, primary key (id)); 3 | CREATE TABLE exclusions (id serial not null, name text not null, primary key (id)); 4 | 5 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/inflection_exclude_table_names/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | plugins: 3 | - name: py 4 | wasm: 5 | url: file://../../../../bin/sqlc-gen-python.wasm 6 | sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" 7 | sql: 8 | - schema: schema.sql 9 | queries: query.sql 10 | engine: postgresql 11 | codegen: 12 | - plugin: py 13 | out: python 14 | options: 15 | package: querytest 16 | emit_sync_querier: true 17 | emit_async_querier: true 18 | inflection_exclude_table_names: 19 | - my_data 20 | - exclusions 21 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_two/python/models.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | import dataclasses 5 | 6 | 7 | @dataclasses.dataclass() 8 | class Bar: 9 | id: int 10 | name: str 11 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_two/python/query.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | # source: query.sql 5 | import sqlalchemy 6 | import sqlalchemy.ext.asyncio 7 | 8 | from querytest import models 9 | 10 | 11 | DELETE_BAR_BY_ID = """-- name: delete_bar_by_id \\:execrows 12 | DELETE FROM bar WHERE id = :p1 13 | """ 14 | 15 | 16 | DELETE_BAR_BY_ID_AND_NAME = """-- name: delete_bar_by_id_and_name \\:execrows 17 | DELETE FROM bar WHERE id = :p1 AND name = :p2 18 | """ 19 | 20 | 21 | class Querier: 22 | def __init__(self, conn: sqlalchemy.engine.Connection): 23 | self._conn = conn 24 | 25 | def delete_bar_by_id(self, *, id: int) -> int: 26 | result = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) 27 | return result.rowcount 28 | 29 | def delete_bar_by_id_and_name(self, *, id: int, name: str) -> int: 30 | result = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID_AND_NAME), {"p1": id, "p2": name}) 31 | return result.rowcount 32 | 33 | 34 | class AsyncQuerier: 35 | def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): 36 | self._conn = conn 37 | 38 | async def delete_bar_by_id(self, *, id: int) -> int: 39 | result = await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) 40 | return result.rowcount 41 | 42 | async def delete_bar_by_id_and_name(self, *, id: int, name: str) -> int: 43 | result = await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID_AND_NAME), {"p1": id, "p2": name}) 44 | return result.rowcount 45 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_two/query.sql: -------------------------------------------------------------------------------- 1 | -- name: DeleteBarByID :execrows 2 | DELETE FROM bar WHERE id = $1; 3 | 4 | -- name: DeleteBarByIDAndName :execrows 5 | DELETE FROM bar WHERE id = $1 AND name = $2; 6 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_two/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE bar (id serial not null, name text not null, primary key (id)); 2 | 3 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_two/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | plugins: 3 | - name: py 4 | wasm: 5 | url: file://../../../../bin/sqlc-gen-python.wasm 6 | sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" 7 | sql: 8 | - schema: schema.sql 9 | queries: query.sql 10 | engine: postgresql 11 | codegen: 12 | - plugin: py 13 | out: python 14 | options: 15 | package: querytest 16 | emit_sync_querier: true 17 | emit_async_querier: true 18 | query_parameter_limit: 2 -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_undefined/python/models.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | import dataclasses 5 | 6 | 7 | @dataclasses.dataclass() 8 | class Bar: 9 | id: int 10 | name1: str 11 | name2: str 12 | name3: str 13 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_undefined/python/query.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | # source: query.sql 5 | import sqlalchemy 6 | import sqlalchemy.ext.asyncio 7 | 8 | from querytest import models 9 | 10 | 11 | DELETE_BAR_BY_ID = """-- name: delete_bar_by_id \\:execrows 12 | DELETE FROM bar WHERE id = :p1 13 | """ 14 | 15 | 16 | DELETE_BAR_BY_ID_AND_NAME = """-- name: delete_bar_by_id_and_name \\:execrows 17 | DELETE FROM bar 18 | WHERE id = :p1 19 | AND name1 = :p2 20 | AND name2 = :p3 21 | AND name3 = :p4 22 | """ 23 | 24 | 25 | class Querier: 26 | def __init__(self, conn: sqlalchemy.engine.Connection): 27 | self._conn = conn 28 | 29 | def delete_bar_by_id(self, *, id: int) -> int: 30 | result = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) 31 | return result.rowcount 32 | 33 | def delete_bar_by_id_and_name(self, *, id: int, name1: str, name2: str, name3: str) -> int: 34 | result = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID_AND_NAME), { 35 | "p1": id, 36 | "p2": name1, 37 | "p3": name2, 38 | "p4": name3, 39 | }) 40 | return result.rowcount 41 | 42 | 43 | class AsyncQuerier: 44 | def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): 45 | self._conn = conn 46 | 47 | async def delete_bar_by_id(self, *, id: int) -> int: 48 | result = await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) 49 | return result.rowcount 50 | 51 | async def delete_bar_by_id_and_name(self, *, id: int, name1: str, name2: str, name3: str) -> int: 52 | result = await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID_AND_NAME), { 53 | "p1": id, 54 | "p2": name1, 55 | "p3": name2, 56 | "p4": name3, 57 | }) 58 | return result.rowcount 59 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_undefined/query.sql: -------------------------------------------------------------------------------- 1 | -- name: DeleteBarByID :execrows 2 | DELETE FROM bar WHERE id = $1; 3 | 4 | -- name: DeleteBarByIDAndName :execrows 5 | DELETE FROM bar 6 | WHERE id = $1 7 | AND name1 = $2 8 | AND name2 = $3 9 | AND name3 = $4 10 | ; 11 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_undefined/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE bar ( 2 | id serial not null, 3 | name1 text not null, 4 | name2 text not null, 5 | name3 text not null, 6 | primary key (id)); 7 | 8 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_undefined/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | plugins: 3 | - name: py 4 | wasm: 5 | url: file://../../../../bin/sqlc-gen-python.wasm 6 | sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" 7 | sql: 8 | - schema: schema.sql 9 | queries: query.sql 10 | engine: postgresql 11 | codegen: 12 | - plugin: py 13 | out: python 14 | options: 15 | package: querytest 16 | emit_sync_querier: true 17 | emit_async_querier: true -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_zero/python/models.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | import dataclasses 5 | 6 | 7 | @dataclasses.dataclass() 8 | class Bar: 9 | id: int 10 | name: str 11 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_zero/python/query.py: -------------------------------------------------------------------------------- 1 | # Code generated by sqlc. DO NOT EDIT. 2 | # versions: 3 | # sqlc v1.28.0 4 | # source: query.sql 5 | import dataclasses 6 | 7 | import sqlalchemy 8 | import sqlalchemy.ext.asyncio 9 | 10 | from querytest import models 11 | 12 | 13 | DELETE_BAR_BY_ID = """-- name: delete_bar_by_id \\:execrows 14 | DELETE FROM bar WHERE id = :p1 15 | """ 16 | 17 | 18 | @dataclasses.dataclass() 19 | class DeleteBarByIDParams: 20 | id: int 21 | 22 | 23 | DELETE_BAR_BY_ID_AND_NAME = """-- name: delete_bar_by_id_and_name \\:execrows 24 | DELETE FROM bar WHERE id = :p1 AND name = :p2 25 | """ 26 | 27 | 28 | @dataclasses.dataclass() 29 | class DeleteBarByIDAndNameParams: 30 | id: int 31 | name: str 32 | 33 | 34 | class Querier: 35 | def __init__(self, conn: sqlalchemy.engine.Connection): 36 | self._conn = conn 37 | 38 | def delete_bar_by_id(self, arg: DeleteBarByIDParams) -> int: 39 | result = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": arg.id}) 40 | return result.rowcount 41 | 42 | def delete_bar_by_id_and_name(self, arg: DeleteBarByIDAndNameParams) -> int: 43 | result = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID_AND_NAME), {"p1": arg.id, "p2": arg.name}) 44 | return result.rowcount 45 | 46 | 47 | class AsyncQuerier: 48 | def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): 49 | self._conn = conn 50 | 51 | async def delete_bar_by_id(self, arg: DeleteBarByIDParams) -> int: 52 | result = await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": arg.id}) 53 | return result.rowcount 54 | 55 | async def delete_bar_by_id_and_name(self, arg: DeleteBarByIDAndNameParams) -> int: 56 | result = await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID_AND_NAME), {"p1": arg.id, "p2": arg.name}) 57 | return result.rowcount 58 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_zero/query.sql: -------------------------------------------------------------------------------- 1 | -- name: DeleteBarByID :execrows 2 | DELETE FROM bar WHERE id = $1; 3 | 4 | -- name: DeleteBarByIDAndName :execrows 5 | DELETE FROM bar WHERE id = $1 AND name = $2; 6 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_zero/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE bar (id serial not null, name text not null, primary key (id)); 2 | 3 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_limit_zero/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | plugins: 3 | - name: py 4 | wasm: 5 | url: file://../../../../bin/sqlc-gen-python.wasm 6 | sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" 7 | sql: 8 | - schema: schema.sql 9 | queries: query.sql 10 | engine: postgresql 11 | codegen: 12 | - plugin: py 13 | out: python 14 | options: 15 | package: querytest 16 | emit_sync_querier: true 17 | emit_async_querier: true 18 | query_parameter_limit: 0 19 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_no_limit/query.sql: -------------------------------------------------------------------------------- 1 | -- name: DeleteBarByID :execrows 2 | DELETE FROM bar WHERE id = $1; 3 | 4 | -- name: DeleteBarByIDAndName :execrows 5 | DELETE FROM bar WHERE id = $1 AND name = $2; 6 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_no_limit/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE bar (id serial not null, name text not null, primary key (id)); -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_no_limit/sqlc.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | plugins: 3 | - name: py 4 | wasm: 5 | url: file://../../../../bin/sqlc-gen-python.wasm 6 | sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" 7 | sql: 8 | - schema: schema.sql 9 | queries: query.sql 10 | engine: postgresql 11 | codegen: 12 | - plugin: py 13 | out: python 14 | options: 15 | package: querytest 16 | emit_sync_querier: true 17 | emit_async_querier: true 18 | query_parameter_limit: -1 19 | -------------------------------------------------------------------------------- /internal/endtoend/testdata/query_parameter_no_limit/stderr.txt: -------------------------------------------------------------------------------- 1 | # package py 2 | error generating code: error generating output: invalid query parameter limit 3 | -------------------------------------------------------------------------------- /internal/gen.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | import ( 4 | "context" 5 | json "encoding/json" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "regexp" 10 | "sort" 11 | "strings" 12 | 13 | "github.com/sqlc-dev/plugin-sdk-go/metadata" 14 | "github.com/sqlc-dev/plugin-sdk-go/plugin" 15 | "github.com/sqlc-dev/plugin-sdk-go/sdk" 16 | 17 | pyast "github.com/sqlc-dev/sqlc-gen-python/internal/ast" 18 | "github.com/sqlc-dev/sqlc-gen-python/internal/inflection" 19 | "github.com/sqlc-dev/sqlc-gen-python/internal/poet" 20 | pyprint "github.com/sqlc-dev/sqlc-gen-python/internal/printer" 21 | ) 22 | 23 | type Constant struct { 24 | Name string 25 | Type string 26 | Value string 27 | } 28 | 29 | type Enum struct { 30 | Name string 31 | Comment string 32 | Constants []Constant 33 | } 34 | 35 | type pyType struct { 36 | InnerType string 37 | IsArray bool 38 | IsNull bool 39 | } 40 | 41 | func (t pyType) Annotation() *pyast.Node { 42 | ann := poet.Name(t.InnerType) 43 | if t.IsArray { 44 | ann = subscriptNode("List", ann) 45 | } 46 | if t.IsNull { 47 | ann = subscriptNode("Optional", ann) 48 | } 49 | return ann 50 | } 51 | 52 | type Field struct { 53 | Name string 54 | Type pyType 55 | Comment string 56 | } 57 | 58 | type Struct struct { 59 | Table plugin.Identifier 60 | Name string 61 | Fields []Field 62 | Comment string 63 | } 64 | 65 | type QueryValue struct { 66 | Emit bool 67 | Name string 68 | Struct *Struct 69 | Typ pyType 70 | } 71 | 72 | func (v QueryValue) Annotation() *pyast.Node { 73 | if v.Typ != (pyType{}) { 74 | return v.Typ.Annotation() 75 | } 76 | if v.Struct != nil { 77 | if v.Emit { 78 | return poet.Name(v.Struct.Name) 79 | } else { 80 | return typeRefNode("models", v.Struct.Name) 81 | } 82 | } 83 | panic("no type for QueryValue: " + v.Name) 84 | } 85 | 86 | func (v QueryValue) EmitStruct() bool { 87 | return v.Emit 88 | } 89 | 90 | func (v QueryValue) IsStruct() bool { 91 | return v.Struct != nil 92 | } 93 | 94 | func (v QueryValue) isEmpty() bool { 95 | return v.Typ == (pyType{}) && v.Name == "" && v.Struct == nil 96 | } 97 | 98 | func (v QueryValue) RowNode(rowVar string) *pyast.Node { 99 | if !v.IsStruct() { 100 | return subscriptNode( 101 | rowVar, 102 | constantInt(0), 103 | ) 104 | } 105 | call := &pyast.Call{ 106 | Func: v.Annotation(), 107 | } 108 | for i, f := range v.Struct.Fields { 109 | call.Keywords = append(call.Keywords, &pyast.Keyword{ 110 | Arg: f.Name, 111 | Value: subscriptNode( 112 | rowVar, 113 | constantInt(i), 114 | ), 115 | }) 116 | } 117 | return &pyast.Node{ 118 | Node: &pyast.Node_Call{ 119 | Call: call, 120 | }, 121 | } 122 | } 123 | 124 | // A struct used to generate methods and fields on the Queries struct 125 | type Query struct { 126 | Cmd string 127 | Comments []string 128 | MethodName string 129 | FieldName string 130 | ConstantName string 131 | SQL string 132 | SourceName string 133 | Ret QueryValue 134 | Args []QueryValue 135 | } 136 | 137 | func (q Query) AddArgs(args *pyast.Arguments) { 138 | // A single struct arg does not need to be passed as a keyword argument 139 | if len(q.Args) == 1 && q.Args[0].IsStruct() { 140 | args.Args = append(args.Args, &pyast.Arg{ 141 | Arg: q.Args[0].Name, 142 | Annotation: q.Args[0].Annotation(), 143 | }) 144 | return 145 | } 146 | for _, a := range q.Args { 147 | args.KwOnlyArgs = append(args.KwOnlyArgs, &pyast.Arg{ 148 | Arg: a.Name, 149 | Annotation: a.Annotation(), 150 | }) 151 | } 152 | } 153 | 154 | func (q Query) ArgDictNode() *pyast.Node { 155 | dict := &pyast.Dict{} 156 | i := 1 157 | for _, a := range q.Args { 158 | if a.isEmpty() { 159 | continue 160 | } 161 | if a.IsStruct() { 162 | for _, f := range a.Struct.Fields { 163 | dict.Keys = append(dict.Keys, poet.Constant(fmt.Sprintf("p%v", i))) 164 | dict.Values = append(dict.Values, typeRefNode(a.Name, f.Name)) 165 | i++ 166 | } 167 | } else { 168 | dict.Keys = append(dict.Keys, poet.Constant(fmt.Sprintf("p%v", i))) 169 | dict.Values = append(dict.Values, poet.Name(a.Name)) 170 | i++ 171 | } 172 | } 173 | if len(dict.Keys) == 0 { 174 | return nil 175 | } 176 | return &pyast.Node{ 177 | Node: &pyast.Node_Dict{ 178 | Dict: dict, 179 | }, 180 | } 181 | } 182 | 183 | func makePyType(req *plugin.GenerateRequest, col *plugin.Column) pyType { 184 | typ := pyInnerType(req, col) 185 | return pyType{ 186 | InnerType: typ, 187 | IsArray: col.IsArray, 188 | IsNull: !col.NotNull, 189 | } 190 | } 191 | 192 | func pyInnerType(req *plugin.GenerateRequest, col *plugin.Column) string { 193 | switch req.Settings.Engine { 194 | case "postgresql": 195 | return postgresType(req, col) 196 | default: 197 | log.Println("unsupported engine type") 198 | return "Any" 199 | } 200 | } 201 | 202 | func modelName(name string, settings *plugin.Settings) string { 203 | out := "" 204 | for _, p := range strings.Split(name, "_") { 205 | out += strings.Title(p) 206 | } 207 | return out 208 | } 209 | 210 | var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") 211 | var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") 212 | 213 | func methodName(name string) string { 214 | snake := matchFirstCap.ReplaceAllString(name, "${1}_${2}") 215 | snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") 216 | return strings.ToLower(snake) 217 | } 218 | 219 | var pyIdentPattern = regexp.MustCompile("[^a-zA-Z0-9_]+") 220 | 221 | func pyEnumValueName(value string) string { 222 | id := strings.Replace(value, "-", "_", -1) 223 | id = strings.Replace(id, ":", "_", -1) 224 | id = strings.Replace(id, "/", "_", -1) 225 | id = pyIdentPattern.ReplaceAllString(id, "") 226 | return strings.ToUpper(id) 227 | } 228 | 229 | func buildEnums(req *plugin.GenerateRequest) []Enum { 230 | var enums []Enum 231 | for _, schema := range req.Catalog.Schemas { 232 | if schema.Name == "pg_catalog" || schema.Name == "information_schema" { 233 | continue 234 | } 235 | for _, enum := range schema.Enums { 236 | var enumName string 237 | if schema.Name == req.Catalog.DefaultSchema { 238 | enumName = enum.Name 239 | } else { 240 | enumName = schema.Name + "_" + enum.Name 241 | } 242 | e := Enum{ 243 | Name: modelName(enumName, req.Settings), 244 | Comment: enum.Comment, 245 | } 246 | for _, v := range enum.Vals { 247 | e.Constants = append(e.Constants, Constant{ 248 | Name: pyEnumValueName(v), 249 | Value: v, 250 | Type: e.Name, 251 | }) 252 | } 253 | enums = append(enums, e) 254 | } 255 | } 256 | if len(enums) > 0 { 257 | sort.Slice(enums, func(i, j int) bool { return enums[i].Name < enums[j].Name }) 258 | } 259 | return enums 260 | } 261 | 262 | func buildModels(conf Config, req *plugin.GenerateRequest) []Struct { 263 | var structs []Struct 264 | for _, schema := range req.Catalog.Schemas { 265 | if schema.Name == "pg_catalog" || schema.Name == "information_schema" { 266 | continue 267 | } 268 | for _, table := range schema.Tables { 269 | var tableName string 270 | if schema.Name == req.Catalog.DefaultSchema { 271 | tableName = table.Rel.Name 272 | } else { 273 | tableName = schema.Name + "_" + table.Rel.Name 274 | } 275 | structName := tableName 276 | if !conf.EmitExactTableNames { 277 | structName = inflection.Singular(inflection.SingularParams{ 278 | Name: structName, 279 | Exclusions: conf.InflectionExcludeTableNames, 280 | }) 281 | } 282 | s := Struct{ 283 | Table: plugin.Identifier{Schema: schema.Name, Name: table.Rel.Name}, 284 | Name: modelName(structName, req.Settings), 285 | Comment: table.Comment, 286 | } 287 | for _, column := range table.Columns { 288 | typ := makePyType(req, column) // TODO: This used to call compiler.ConvertColumn? 289 | typ.InnerType = strings.TrimPrefix(typ.InnerType, "models.") 290 | s.Fields = append(s.Fields, Field{ 291 | Name: column.Name, 292 | Type: typ, 293 | Comment: column.Comment, 294 | }) 295 | } 296 | structs = append(structs, s) 297 | } 298 | } 299 | if len(structs) > 0 { 300 | sort.Slice(structs, func(i, j int) bool { return structs[i].Name < structs[j].Name }) 301 | } 302 | return structs 303 | } 304 | 305 | func columnName(c *plugin.Column, pos int) string { 306 | if c.Name != "" { 307 | return c.Name 308 | } 309 | return fmt.Sprintf("column_%d", pos+1) 310 | } 311 | 312 | func paramName(p *plugin.Parameter) string { 313 | if p.Column.Name != "" { 314 | return p.Column.Name 315 | } 316 | return fmt.Sprintf("dollar_%d", p.Number) 317 | } 318 | 319 | type pyColumn struct { 320 | id int32 321 | *plugin.Column 322 | } 323 | 324 | func columnsToStruct(req *plugin.GenerateRequest, name string, columns []pyColumn) *Struct { 325 | gs := Struct{ 326 | Name: name, 327 | } 328 | seen := map[string]int32{} 329 | suffixes := map[int32]int32{} 330 | for i, c := range columns { 331 | colName := columnName(c.Column, i) 332 | fieldName := colName 333 | // Track suffixes by the ID of the column, so that columns referring to 334 | // the same numbered parameter can be reused. 335 | var suffix int32 336 | if o, ok := suffixes[c.id]; ok { 337 | suffix = o 338 | } else if v := seen[colName]; v > 0 { 339 | suffix = v + 1 340 | } 341 | suffixes[c.id] = suffix 342 | if suffix > 0 { 343 | fieldName = fmt.Sprintf("%s_%d", fieldName, suffix) 344 | } 345 | gs.Fields = append(gs.Fields, Field{ 346 | Name: fieldName, 347 | Type: makePyType(req, c.Column), 348 | }) 349 | seen[colName]++ 350 | } 351 | return &gs 352 | } 353 | 354 | var postgresPlaceholderRegexp = regexp.MustCompile(`\B\$(\d+)\b`) 355 | 356 | // Sqlalchemy uses ":name" for placeholders, so "$N" is converted to ":pN" 357 | // This also means ":" has special meaning to sqlalchemy, so it must be escaped. 358 | func sqlalchemySQL(s, engine string) string { 359 | s = strings.ReplaceAll(s, ":", `\\:`) 360 | if engine == "postgresql" { 361 | return postgresPlaceholderRegexp.ReplaceAllString(s, ":p$1") 362 | } 363 | return s 364 | } 365 | 366 | func buildQueries(conf Config, req *plugin.GenerateRequest, structs []Struct) ([]Query, error) { 367 | qs := make([]Query, 0, len(req.Queries)) 368 | for _, query := range req.Queries { 369 | if query.Name == "" { 370 | continue 371 | } 372 | if query.Cmd == "" { 373 | continue 374 | } 375 | if query.Cmd == metadata.CmdCopyFrom { 376 | return nil, errors.New("Support for CopyFrom in Python is not implemented") 377 | } 378 | 379 | methodName := methodName(query.Name) 380 | 381 | gq := Query{ 382 | Cmd: query.Cmd, 383 | Comments: query.Comments, 384 | MethodName: methodName, 385 | FieldName: sdk.LowerTitle(query.Name) + "Stmt", 386 | ConstantName: strings.ToUpper(methodName), 387 | SQL: sqlalchemySQL(query.Text, req.Settings.Engine), 388 | SourceName: query.Filename, 389 | } 390 | 391 | qpl := 4 392 | if conf.QueryParameterLimit != nil { 393 | qpl = int(*conf.QueryParameterLimit) 394 | } 395 | if qpl < 0 { 396 | return nil, errors.New("invalid query parameter limit") 397 | } 398 | if len(query.Params) > qpl || qpl == 0 { 399 | var cols []pyColumn 400 | for _, p := range query.Params { 401 | cols = append(cols, pyColumn{ 402 | id: p.Number, 403 | Column: p.Column, 404 | }) 405 | } 406 | gq.Args = []QueryValue{{ 407 | Emit: true, 408 | Name: "arg", 409 | Struct: columnsToStruct(req, query.Name+"Params", cols), 410 | }} 411 | } else { 412 | args := make([]QueryValue, 0, len(query.Params)) 413 | for _, p := range query.Params { 414 | args = append(args, QueryValue{ 415 | Name: paramName(p), 416 | Typ: makePyType(req, p.Column), 417 | }) 418 | } 419 | gq.Args = args 420 | } 421 | 422 | if len(query.Columns) == 1 { 423 | c := query.Columns[0] 424 | gq.Ret = QueryValue{ 425 | Name: columnName(c, 0), 426 | Typ: makePyType(req, c), 427 | } 428 | } else if len(query.Columns) > 1 { 429 | var gs *Struct 430 | var emit bool 431 | 432 | for _, s := range structs { 433 | if len(s.Fields) != len(query.Columns) { 434 | continue 435 | } 436 | same := true 437 | 438 | for i, f := range s.Fields { 439 | c := query.Columns[i] 440 | // HACK: models do not have "models." on their types, so trim that so we can find matches 441 | trimmedPyType := makePyType(req, c) 442 | trimmedPyType.InnerType = strings.TrimPrefix(trimmedPyType.InnerType, "models.") 443 | sameName := f.Name == columnName(c, i) 444 | sameType := f.Type == trimmedPyType 445 | sameTable := sdk.SameTableName(c.Table, &s.Table, req.Catalog.DefaultSchema) 446 | if !sameName || !sameType || !sameTable { 447 | same = false 448 | } 449 | } 450 | if same { 451 | gs = &s 452 | break 453 | } 454 | } 455 | 456 | if gs == nil { 457 | var columns []pyColumn 458 | for i, c := range query.Columns { 459 | columns = append(columns, pyColumn{ 460 | id: int32(i), 461 | Column: c, 462 | }) 463 | } 464 | gs = columnsToStruct(req, query.Name+"Row", columns) 465 | emit = true 466 | } 467 | gq.Ret = QueryValue{ 468 | Emit: emit, 469 | Name: "i", 470 | Struct: gs, 471 | } 472 | } 473 | 474 | qs = append(qs, gq) 475 | } 476 | sort.Slice(qs, func(i, j int) bool { return qs[i].MethodName < qs[j].MethodName }) 477 | return qs, nil 478 | } 479 | 480 | func moduleNode(version, source string) *pyast.Module { 481 | mod := &pyast.Module{ 482 | Body: []*pyast.Node{ 483 | poet.Comment( 484 | "Code generated by sqlc. DO NOT EDIT.", 485 | ), 486 | poet.Comment( 487 | "versions:", 488 | ), 489 | poet.Comment( 490 | " sqlc " + version, 491 | ), 492 | }, 493 | } 494 | if source != "" { 495 | mod.Body = append(mod.Body, 496 | poet.Comment( 497 | "source: "+source, 498 | ), 499 | ) 500 | } 501 | return mod 502 | } 503 | 504 | func importNode(name string) *pyast.Node { 505 | return &pyast.Node{ 506 | Node: &pyast.Node_Import{ 507 | Import: &pyast.Import{ 508 | Names: []*pyast.Node{ 509 | { 510 | Node: &pyast.Node_Alias{ 511 | Alias: &pyast.Alias{ 512 | Name: name, 513 | }, 514 | }, 515 | }, 516 | }, 517 | }, 518 | }, 519 | } 520 | } 521 | 522 | func classDefNode(name string, bases ...*pyast.Node) *pyast.Node { 523 | return &pyast.Node{ 524 | Node: &pyast.Node_ClassDef{ 525 | ClassDef: &pyast.ClassDef{ 526 | Name: name, 527 | Bases: bases, 528 | }, 529 | }, 530 | } 531 | } 532 | 533 | func assignNode(target string, value *pyast.Node) *pyast.Node { 534 | return &pyast.Node{ 535 | Node: &pyast.Node_Assign{ 536 | Assign: &pyast.Assign{ 537 | Targets: []*pyast.Node{ 538 | poet.Name(target), 539 | }, 540 | Value: value, 541 | }, 542 | }, 543 | } 544 | } 545 | 546 | func constantInt(value int) *pyast.Node { 547 | return &pyast.Node{ 548 | Node: &pyast.Node_Constant{ 549 | Constant: &pyast.Constant{ 550 | Value: &pyast.Constant_Int{ 551 | Int: int32(value), 552 | }, 553 | }, 554 | }, 555 | } 556 | } 557 | 558 | func subscriptNode(value string, slice *pyast.Node) *pyast.Node { 559 | return &pyast.Node{ 560 | Node: &pyast.Node_Subscript{ 561 | Subscript: &pyast.Subscript{ 562 | Value: &pyast.Name{Id: value}, 563 | Slice: slice, 564 | }, 565 | }, 566 | } 567 | } 568 | 569 | func dataclassNode(name string) *pyast.ClassDef { 570 | return &pyast.ClassDef{ 571 | Name: name, 572 | DecoratorList: []*pyast.Node{ 573 | { 574 | Node: &pyast.Node_Call{ 575 | Call: &pyast.Call{ 576 | Func: poet.Attribute(poet.Name("dataclasses"), "dataclass"), 577 | }, 578 | }, 579 | }, 580 | }, 581 | } 582 | } 583 | 584 | func pydanticNode(name string) *pyast.ClassDef { 585 | return &pyast.ClassDef{ 586 | Name: name, 587 | Bases: []*pyast.Node{ 588 | { 589 | Node: &pyast.Node_Attribute{ 590 | Attribute: &pyast.Attribute{ 591 | Value: &pyast.Node{ 592 | Node: &pyast.Node_Name{ 593 | Name: &pyast.Name{Id: "pydantic"}, 594 | }, 595 | }, 596 | Attr: "BaseModel", 597 | }, 598 | }, 599 | }, 600 | }, 601 | } 602 | } 603 | 604 | func fieldNode(f Field) *pyast.Node { 605 | return &pyast.Node{ 606 | Node: &pyast.Node_AnnAssign{ 607 | AnnAssign: &pyast.AnnAssign{ 608 | Target: &pyast.Name{Id: f.Name}, 609 | Annotation: f.Type.Annotation(), 610 | Comment: f.Comment, 611 | }, 612 | }, 613 | } 614 | } 615 | 616 | func typeRefNode(base string, parts ...string) *pyast.Node { 617 | n := poet.Name(base) 618 | for _, p := range parts { 619 | n = poet.Attribute(n, p) 620 | } 621 | return n 622 | } 623 | 624 | func connMethodNode(method, name string, arg *pyast.Node) *pyast.Node { 625 | args := []*pyast.Node{ 626 | { 627 | Node: &pyast.Node_Call{ 628 | Call: &pyast.Call{ 629 | Func: typeRefNode("sqlalchemy", "text"), 630 | Args: []*pyast.Node{ 631 | poet.Name(name), 632 | }, 633 | }, 634 | }, 635 | }, 636 | } 637 | if arg != nil { 638 | args = append(args, arg) 639 | } 640 | return &pyast.Node{ 641 | Node: &pyast.Node_Call{ 642 | Call: &pyast.Call{ 643 | Func: typeRefNode("self", "_conn", method), 644 | Args: args, 645 | }, 646 | }, 647 | } 648 | } 649 | 650 | func buildImportGroup(specs map[string]importSpec) *pyast.Node { 651 | var body []*pyast.Node 652 | for _, spec := range buildImportBlock2(specs) { 653 | if len(spec.Names) > 0 && spec.Names[0] != "" { 654 | imp := &pyast.ImportFrom{ 655 | Module: spec.Module, 656 | } 657 | for _, name := range spec.Names { 658 | imp.Names = append(imp.Names, poet.Alias(name)) 659 | } 660 | body = append(body, &pyast.Node{ 661 | Node: &pyast.Node_ImportFrom{ 662 | ImportFrom: imp, 663 | }, 664 | }) 665 | } else { 666 | body = append(body, importNode(spec.Module)) 667 | } 668 | } 669 | return &pyast.Node{ 670 | Node: &pyast.Node_ImportGroup{ 671 | ImportGroup: &pyast.ImportGroup{ 672 | Imports: body, 673 | }, 674 | }, 675 | } 676 | } 677 | 678 | func buildModelsTree(ctx *pyTmplCtx, i *importer) *pyast.Node { 679 | mod := moduleNode(ctx.SqlcVersion, "") 680 | std, pkg := i.modelImportSpecs() 681 | mod.Body = append(mod.Body, buildImportGroup(std), buildImportGroup(pkg)) 682 | 683 | for _, e := range ctx.Enums { 684 | bases := []*pyast.Node{ 685 | poet.Name("str"), 686 | poet.Attribute(poet.Name("enum"), "Enum"), 687 | } 688 | if i.C.EmitStrEnum { 689 | // override the bases to emit enum.StrEnum (only support in Python >=3.11) 690 | bases = []*pyast.Node{ 691 | poet.Attribute(poet.Name("enum"), "StrEnum"), 692 | } 693 | } 694 | def := &pyast.ClassDef{ 695 | Name: e.Name, 696 | Bases: bases, 697 | } 698 | if e.Comment != "" { 699 | def.Body = append(def.Body, &pyast.Node{ 700 | Node: &pyast.Node_Expr{ 701 | Expr: &pyast.Expr{ 702 | Value: poet.Constant(e.Comment), 703 | }, 704 | }, 705 | }) 706 | } 707 | for _, c := range e.Constants { 708 | def.Body = append(def.Body, assignNode(c.Name, poet.Constant(c.Value))) 709 | } 710 | mod.Body = append(mod.Body, &pyast.Node{ 711 | Node: &pyast.Node_ClassDef{ 712 | ClassDef: def, 713 | }, 714 | }) 715 | } 716 | 717 | for _, m := range ctx.Models { 718 | var def *pyast.ClassDef 719 | if ctx.C.EmitPydanticModels { 720 | def = pydanticNode(m.Name) 721 | } else { 722 | def = dataclassNode(m.Name) 723 | } 724 | if m.Comment != "" { 725 | def.Body = append(def.Body, &pyast.Node{ 726 | Node: &pyast.Node_Expr{ 727 | Expr: &pyast.Expr{ 728 | Value: poet.Constant(m.Comment), 729 | }, 730 | }, 731 | }) 732 | } 733 | for _, f := range m.Fields { 734 | def.Body = append(def.Body, fieldNode(f)) 735 | } 736 | mod.Body = append(mod.Body, &pyast.Node{ 737 | Node: &pyast.Node_ClassDef{ 738 | ClassDef: def, 739 | }, 740 | }) 741 | } 742 | 743 | return &pyast.Node{Node: &pyast.Node_Module{Module: mod}} 744 | } 745 | 746 | func querierClassDef() *pyast.ClassDef { 747 | return &pyast.ClassDef{ 748 | Name: "Querier", 749 | Body: []*pyast.Node{ 750 | { 751 | Node: &pyast.Node_FunctionDef{ 752 | FunctionDef: &pyast.FunctionDef{ 753 | Name: "__init__", 754 | Args: &pyast.Arguments{ 755 | Args: []*pyast.Arg{ 756 | { 757 | Arg: "self", 758 | }, 759 | { 760 | Arg: "conn", 761 | Annotation: typeRefNode("sqlalchemy", "engine", "Connection"), 762 | }, 763 | }, 764 | }, 765 | Body: []*pyast.Node{ 766 | { 767 | Node: &pyast.Node_Assign{ 768 | Assign: &pyast.Assign{ 769 | Targets: []*pyast.Node{ 770 | poet.Attribute(poet.Name("self"), "_conn"), 771 | }, 772 | Value: poet.Name("conn"), 773 | }, 774 | }, 775 | }, 776 | }, 777 | }, 778 | }, 779 | }, 780 | }, 781 | } 782 | } 783 | 784 | func asyncQuerierClassDef() *pyast.ClassDef { 785 | return &pyast.ClassDef{ 786 | Name: "AsyncQuerier", 787 | Body: []*pyast.Node{ 788 | { 789 | Node: &pyast.Node_FunctionDef{ 790 | FunctionDef: &pyast.FunctionDef{ 791 | Name: "__init__", 792 | Args: &pyast.Arguments{ 793 | Args: []*pyast.Arg{ 794 | { 795 | Arg: "self", 796 | }, 797 | { 798 | Arg: "conn", 799 | Annotation: typeRefNode("sqlalchemy", "ext", "asyncio", "AsyncConnection"), 800 | }, 801 | }, 802 | }, 803 | Body: []*pyast.Node{ 804 | { 805 | Node: &pyast.Node_Assign{ 806 | Assign: &pyast.Assign{ 807 | Targets: []*pyast.Node{ 808 | poet.Attribute(poet.Name("self"), "_conn"), 809 | }, 810 | Value: poet.Name("conn"), 811 | }, 812 | }, 813 | }, 814 | }, 815 | }, 816 | }, 817 | }, 818 | }, 819 | } 820 | } 821 | 822 | func buildQueryTree(ctx *pyTmplCtx, i *importer, source string) *pyast.Node { 823 | mod := moduleNode(ctx.SqlcVersion, source) 824 | std, pkg := i.queryImportSpecs(source) 825 | mod.Body = append(mod.Body, buildImportGroup(std), buildImportGroup(pkg)) 826 | mod.Body = append(mod.Body, &pyast.Node{ 827 | Node: &pyast.Node_ImportGroup{ 828 | ImportGroup: &pyast.ImportGroup{ 829 | Imports: []*pyast.Node{ 830 | { 831 | Node: &pyast.Node_ImportFrom{ 832 | ImportFrom: &pyast.ImportFrom{ 833 | Module: ctx.C.Package, 834 | Names: []*pyast.Node{ 835 | poet.Alias("models"), 836 | }, 837 | }, 838 | }, 839 | }, 840 | }, 841 | }, 842 | }, 843 | }) 844 | 845 | for _, q := range ctx.Queries { 846 | if !ctx.OutputQuery(q.SourceName) { 847 | continue 848 | } 849 | queryText := fmt.Sprintf("-- name: %s \\\\%s\n%s\n", q.MethodName, q.Cmd, q.SQL) 850 | mod.Body = append(mod.Body, assignNode(q.ConstantName, poet.Constant(queryText))) 851 | for _, arg := range q.Args { 852 | if arg.EmitStruct() { 853 | var def *pyast.ClassDef 854 | if ctx.C.EmitPydanticModels { 855 | def = pydanticNode(arg.Struct.Name) 856 | } else { 857 | def = dataclassNode(arg.Struct.Name) 858 | } 859 | for _, f := range arg.Struct.Fields { 860 | def.Body = append(def.Body, fieldNode(f)) 861 | } 862 | mod.Body = append(mod.Body, poet.Node(def)) 863 | } 864 | } 865 | if q.Ret.EmitStruct() { 866 | var def *pyast.ClassDef 867 | if ctx.C.EmitPydanticModels { 868 | def = pydanticNode(q.Ret.Struct.Name) 869 | } else { 870 | def = dataclassNode(q.Ret.Struct.Name) 871 | } 872 | for _, f := range q.Ret.Struct.Fields { 873 | def.Body = append(def.Body, fieldNode(f)) 874 | } 875 | mod.Body = append(mod.Body, poet.Node(def)) 876 | } 877 | } 878 | 879 | if ctx.C.EmitSyncQuerier { 880 | cls := querierClassDef() 881 | for _, q := range ctx.Queries { 882 | if !ctx.OutputQuery(q.SourceName) { 883 | continue 884 | } 885 | f := &pyast.FunctionDef{ 886 | Name: q.MethodName, 887 | Args: &pyast.Arguments{ 888 | Args: []*pyast.Arg{ 889 | { 890 | Arg: "self", 891 | }, 892 | }, 893 | }, 894 | } 895 | 896 | q.AddArgs(f.Args) 897 | exec := connMethodNode("execute", q.ConstantName, q.ArgDictNode()) 898 | 899 | switch q.Cmd { 900 | case ":one": 901 | f.Body = append(f.Body, 902 | assignNode("row", poet.Node( 903 | &pyast.Call{ 904 | Func: poet.Attribute(exec, "first"), 905 | }, 906 | )), 907 | poet.Node( 908 | &pyast.If{ 909 | Test: poet.Node( 910 | &pyast.Compare{ 911 | Left: poet.Name("row"), 912 | Ops: []*pyast.Node{ 913 | poet.Is(), 914 | }, 915 | Comparators: []*pyast.Node{ 916 | poet.Constant(nil), 917 | }, 918 | }, 919 | ), 920 | Body: []*pyast.Node{ 921 | poet.Return( 922 | poet.Constant(nil), 923 | ), 924 | }, 925 | }, 926 | ), 927 | poet.Return(q.Ret.RowNode("row")), 928 | ) 929 | f.Returns = subscriptNode("Optional", q.Ret.Annotation()) 930 | case ":many": 931 | f.Body = append(f.Body, 932 | assignNode("result", exec), 933 | poet.Node( 934 | &pyast.For{ 935 | Target: poet.Name("row"), 936 | Iter: poet.Name("result"), 937 | Body: []*pyast.Node{ 938 | poet.Expr( 939 | poet.Yield( 940 | q.Ret.RowNode("row"), 941 | ), 942 | ), 943 | }, 944 | }, 945 | ), 946 | ) 947 | f.Returns = subscriptNode("Iterator", q.Ret.Annotation()) 948 | case ":exec": 949 | f.Body = append(f.Body, exec) 950 | f.Returns = poet.Constant(nil) 951 | case ":execrows": 952 | f.Body = append(f.Body, 953 | assignNode("result", exec), 954 | poet.Return(poet.Attribute(poet.Name("result"), "rowcount")), 955 | ) 956 | f.Returns = poet.Name("int") 957 | case ":execresult": 958 | f.Body = append(f.Body, 959 | poet.Return(exec), 960 | ) 961 | f.Returns = typeRefNode("sqlalchemy", "engine", "Result") 962 | default: 963 | panic("unknown cmd " + q.Cmd) 964 | } 965 | 966 | cls.Body = append(cls.Body, poet.Node(f)) 967 | } 968 | mod.Body = append(mod.Body, poet.Node(cls)) 969 | } 970 | 971 | if ctx.C.EmitAsyncQuerier { 972 | cls := asyncQuerierClassDef() 973 | for _, q := range ctx.Queries { 974 | if !ctx.OutputQuery(q.SourceName) { 975 | continue 976 | } 977 | f := &pyast.AsyncFunctionDef{ 978 | Name: q.MethodName, 979 | Args: &pyast.Arguments{ 980 | Args: []*pyast.Arg{ 981 | { 982 | Arg: "self", 983 | }, 984 | }, 985 | }, 986 | } 987 | 988 | q.AddArgs(f.Args) 989 | exec := connMethodNode("execute", q.ConstantName, q.ArgDictNode()) 990 | 991 | switch q.Cmd { 992 | case ":one": 993 | f.Body = append(f.Body, 994 | assignNode("row", poet.Node( 995 | &pyast.Call{ 996 | Func: poet.Attribute(poet.Await(exec), "first"), 997 | }, 998 | )), 999 | poet.Node( 1000 | &pyast.If{ 1001 | Test: poet.Node( 1002 | &pyast.Compare{ 1003 | Left: poet.Name("row"), 1004 | Ops: []*pyast.Node{ 1005 | poet.Is(), 1006 | }, 1007 | Comparators: []*pyast.Node{ 1008 | poet.Constant(nil), 1009 | }, 1010 | }, 1011 | ), 1012 | Body: []*pyast.Node{ 1013 | poet.Return( 1014 | poet.Constant(nil), 1015 | ), 1016 | }, 1017 | }, 1018 | ), 1019 | poet.Return(q.Ret.RowNode("row")), 1020 | ) 1021 | f.Returns = subscriptNode("Optional", q.Ret.Annotation()) 1022 | case ":many": 1023 | stream := connMethodNode("stream", q.ConstantName, q.ArgDictNode()) 1024 | f.Body = append(f.Body, 1025 | assignNode("result", poet.Await(stream)), 1026 | poet.Node( 1027 | &pyast.AsyncFor{ 1028 | Target: poet.Name("row"), 1029 | Iter: poet.Name("result"), 1030 | Body: []*pyast.Node{ 1031 | poet.Expr( 1032 | poet.Yield( 1033 | q.Ret.RowNode("row"), 1034 | ), 1035 | ), 1036 | }, 1037 | }, 1038 | ), 1039 | ) 1040 | f.Returns = subscriptNode("AsyncIterator", q.Ret.Annotation()) 1041 | case ":exec": 1042 | f.Body = append(f.Body, poet.Await(exec)) 1043 | f.Returns = poet.Constant(nil) 1044 | case ":execrows": 1045 | f.Body = append(f.Body, 1046 | assignNode("result", poet.Await(exec)), 1047 | poet.Return(poet.Attribute(poet.Name("result"), "rowcount")), 1048 | ) 1049 | f.Returns = poet.Name("int") 1050 | case ":execresult": 1051 | f.Body = append(f.Body, 1052 | poet.Return(poet.Await(exec)), 1053 | ) 1054 | f.Returns = typeRefNode("sqlalchemy", "engine", "Result") 1055 | default: 1056 | panic("unknown cmd " + q.Cmd) 1057 | } 1058 | 1059 | cls.Body = append(cls.Body, poet.Node(f)) 1060 | } 1061 | mod.Body = append(mod.Body, poet.Node(cls)) 1062 | } 1063 | 1064 | return poet.Node(mod) 1065 | } 1066 | 1067 | type pyTmplCtx struct { 1068 | SqlcVersion string 1069 | Models []Struct 1070 | Queries []Query 1071 | Enums []Enum 1072 | SourceName string 1073 | C Config 1074 | } 1075 | 1076 | func (t *pyTmplCtx) OutputQuery(sourceName string) bool { 1077 | return t.SourceName == sourceName 1078 | } 1079 | 1080 | func HashComment(s string) string { 1081 | return "# " + strings.ReplaceAll(s, "\n", "\n# ") 1082 | } 1083 | 1084 | func Generate(_ context.Context, req *plugin.GenerateRequest) (*plugin.GenerateResponse, error) { 1085 | var conf Config 1086 | if len(req.PluginOptions) > 0 { 1087 | if err := json.Unmarshal(req.PluginOptions, &conf); err != nil { 1088 | return nil, err 1089 | } 1090 | } 1091 | 1092 | enums := buildEnums(req) 1093 | models := buildModels(conf, req) 1094 | queries, err := buildQueries(conf, req, models) 1095 | if err != nil { 1096 | return nil, err 1097 | } 1098 | 1099 | i := &importer{ 1100 | Models: models, 1101 | Queries: queries, 1102 | Enums: enums, 1103 | C: conf, 1104 | } 1105 | 1106 | tctx := pyTmplCtx{ 1107 | Models: models, 1108 | Queries: queries, 1109 | Enums: enums, 1110 | SqlcVersion: req.SqlcVersion, 1111 | C: conf, 1112 | } 1113 | 1114 | output := map[string]string{} 1115 | result := pyprint.Print(buildModelsTree(&tctx, i), pyprint.Options{}) 1116 | tctx.SourceName = "models.py" 1117 | output["models.py"] = string(result.Python) 1118 | 1119 | files := map[string]struct{}{} 1120 | for _, q := range queries { 1121 | files[q.SourceName] = struct{}{} 1122 | } 1123 | 1124 | for source := range files { 1125 | tctx.SourceName = source 1126 | result := pyprint.Print(buildQueryTree(&tctx, i, source), pyprint.Options{}) 1127 | name := source 1128 | if !strings.HasSuffix(name, ".py") { 1129 | name = strings.TrimSuffix(name, ".sql") 1130 | name += ".py" 1131 | } 1132 | output[name] = string(result.Python) 1133 | } 1134 | 1135 | resp := plugin.GenerateResponse{} 1136 | 1137 | for filename, code := range output { 1138 | resp.Files = append(resp.Files, &plugin.File{ 1139 | Name: filename, 1140 | Contents: []byte(code), 1141 | }) 1142 | } 1143 | 1144 | return &resp, nil 1145 | } 1146 | -------------------------------------------------------------------------------- /internal/imports.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | type importSpec struct { 10 | Module string 11 | Name string 12 | Alias string 13 | } 14 | 15 | func (i importSpec) String() string { 16 | if i.Alias != "" { 17 | if i.Name == "" { 18 | return fmt.Sprintf("import %s as %s", i.Module, i.Alias) 19 | } 20 | return fmt.Sprintf("from %s import %s as %s", i.Module, i.Name, i.Alias) 21 | } 22 | if i.Name == "" { 23 | return "import " + i.Module 24 | } 25 | return fmt.Sprintf("from %s import %s", i.Module, i.Name) 26 | } 27 | 28 | type importer struct { 29 | Models []Struct 30 | Queries []Query 31 | Enums []Enum 32 | C Config 33 | } 34 | 35 | func structUses(name string, s Struct) bool { 36 | for _, f := range s.Fields { 37 | if name == "typing.List" && f.Type.IsArray { 38 | return true 39 | } 40 | if name == "typing.Optional" && f.Type.IsNull { 41 | return true 42 | } 43 | if f.Type.InnerType == name { 44 | return true 45 | } 46 | } 47 | return false 48 | } 49 | 50 | func queryValueUses(name string, qv QueryValue) bool { 51 | if !qv.isEmpty() { 52 | if name == "typing.List" && qv.Typ.IsArray { 53 | return true 54 | } 55 | if name == "typing.Optional" && qv.Typ.IsNull { 56 | return true 57 | } 58 | if qv.IsStruct() && qv.EmitStruct() { 59 | if structUses(name, *qv.Struct) { 60 | return true 61 | } 62 | } else { 63 | if qv.Typ.InnerType == name { 64 | return true 65 | } 66 | } 67 | } 68 | return false 69 | } 70 | 71 | func (i *importer) Imports(fileName string) []string { 72 | if fileName == "models.py" { 73 | return i.modelImports() 74 | } 75 | return i.queryImports(fileName) 76 | } 77 | 78 | func (i *importer) modelImportSpecs() (map[string]importSpec, map[string]importSpec) { 79 | modelUses := func(name string) bool { 80 | for _, model := range i.Models { 81 | if structUses(name, model) { 82 | return true 83 | } 84 | } 85 | return false 86 | } 87 | 88 | std := stdImports(modelUses) 89 | if i.C.EmitPydanticModels { 90 | std["pydantic"] = importSpec{Module: "pydantic"} 91 | } else { 92 | std["dataclasses"] = importSpec{Module: "dataclasses"} 93 | } 94 | if len(i.Enums) > 0 { 95 | std["enum"] = importSpec{Module: "enum"} 96 | } 97 | 98 | pkg := make(map[string]importSpec) 99 | 100 | return std, pkg 101 | } 102 | 103 | func (i *importer) modelImports() []string { 104 | std, pkg := i.modelImportSpecs() 105 | importLines := []string{ 106 | buildImportBlock(std), 107 | "", 108 | buildImportBlock(pkg), 109 | } 110 | return importLines 111 | } 112 | 113 | func (i *importer) queryImportSpecs(fileName string) (map[string]importSpec, map[string]importSpec) { 114 | queryUses := func(name string) bool { 115 | for _, q := range i.Queries { 116 | if q.SourceName != fileName { 117 | continue 118 | } 119 | if queryValueUses(name, q.Ret) { 120 | return true 121 | } 122 | for _, arg := range q.Args { 123 | if queryValueUses(name, arg) { 124 | return true 125 | } 126 | } 127 | } 128 | return false 129 | } 130 | 131 | std := stdImports(queryUses) 132 | 133 | pkg := make(map[string]importSpec) 134 | pkg["sqlalchemy"] = importSpec{Module: "sqlalchemy"} 135 | if i.C.EmitAsyncQuerier { 136 | pkg["sqlalchemy.ext.asyncio"] = importSpec{Module: "sqlalchemy.ext.asyncio"} 137 | } 138 | 139 | queryValueModelImports := func(qv QueryValue) { 140 | if qv.IsStruct() && qv.EmitStruct() { 141 | if i.C.EmitPydanticModels { 142 | std["pydantic"] = importSpec{Module: "pydantic"} 143 | } else { 144 | std["dataclasses"] = importSpec{Module: "dataclasses"} 145 | } 146 | } 147 | } 148 | 149 | for _, q := range i.Queries { 150 | if q.SourceName != fileName { 151 | continue 152 | } 153 | if q.Cmd == ":one" { 154 | std["typing.Optional"] = importSpec{Module: "typing", Name: "Optional"} 155 | } 156 | if q.Cmd == ":many" { 157 | if i.C.EmitSyncQuerier { 158 | std["typing.Iterator"] = importSpec{Module: "typing", Name: "Iterator"} 159 | } 160 | if i.C.EmitAsyncQuerier { 161 | std["typing.AsyncIterator"] = importSpec{Module: "typing", Name: "AsyncIterator"} 162 | } 163 | } 164 | queryValueModelImports(q.Ret) 165 | for _, qv := range q.Args { 166 | queryValueModelImports(qv) 167 | } 168 | } 169 | 170 | return std, pkg 171 | } 172 | 173 | func (i *importer) queryImports(fileName string) []string { 174 | std, pkg := i.queryImportSpecs(fileName) 175 | 176 | modelImportStr := fmt.Sprintf("from %s import models", i.C.Package) 177 | 178 | importLines := []string{ 179 | buildImportBlock(std), 180 | "", 181 | buildImportBlock(pkg), 182 | "", 183 | modelImportStr, 184 | } 185 | return importLines 186 | } 187 | 188 | type importFromSpec struct { 189 | Module string 190 | Names []string 191 | Alias string 192 | } 193 | 194 | func buildImportBlock2(pkgs map[string]importSpec) []importFromSpec { 195 | pkgImports := make([]importFromSpec, 0) 196 | fromImports := make(map[string][]string) 197 | for _, is := range pkgs { 198 | if is.Name == "" || is.Alias != "" { 199 | pkgImports = append(pkgImports, importFromSpec{ 200 | Module: is.Module, 201 | Names: []string{is.Name}, 202 | Alias: is.Alias, 203 | }) 204 | } else { 205 | names, ok := fromImports[is.Module] 206 | if !ok { 207 | names = make([]string, 0, 1) 208 | } 209 | names = append(names, is.Name) 210 | fromImports[is.Module] = names 211 | } 212 | } 213 | for modName, names := range fromImports { 214 | sort.Strings(names) 215 | pkgImports = append(pkgImports, importFromSpec{ 216 | Module: modName, 217 | Names: names, 218 | }) 219 | } 220 | sort.Slice(pkgImports, func(i, j int) bool { 221 | return pkgImports[i].Module < pkgImports[j].Module || pkgImports[i].Names[0] < pkgImports[j].Names[0] 222 | }) 223 | return pkgImports 224 | } 225 | 226 | func buildImportBlock(pkgs map[string]importSpec) string { 227 | pkgImports := make([]importSpec, 0) 228 | fromImports := make(map[string][]string) 229 | for _, is := range pkgs { 230 | if is.Name == "" || is.Alias != "" { 231 | pkgImports = append(pkgImports, is) 232 | } else { 233 | names, ok := fromImports[is.Module] 234 | if !ok { 235 | names = make([]string, 0, 1) 236 | } 237 | names = append(names, is.Name) 238 | fromImports[is.Module] = names 239 | } 240 | } 241 | 242 | importStrings := make([]string, 0, len(pkgImports)+len(fromImports)) 243 | for _, is := range pkgImports { 244 | importStrings = append(importStrings, is.String()) 245 | } 246 | for modName, names := range fromImports { 247 | sort.Strings(names) 248 | nameString := strings.Join(names, ", ") 249 | importStrings = append(importStrings, fmt.Sprintf("from %s import %s", modName, nameString)) 250 | } 251 | sort.Strings(importStrings) 252 | return strings.Join(importStrings, "\n") 253 | } 254 | 255 | func stdImports(uses func(name string) bool) map[string]importSpec { 256 | std := make(map[string]importSpec) 257 | if uses("decimal.Decimal") { 258 | std["decimal"] = importSpec{Module: "decimal"} 259 | } 260 | if uses("datetime.date") || uses("datetime.time") || uses("datetime.datetime") || uses("datetime.timedelta") { 261 | std["datetime"] = importSpec{Module: "datetime"} 262 | } 263 | if uses("uuid.UUID") { 264 | std["uuid"] = importSpec{Module: "uuid"} 265 | } 266 | if uses("typing.List") { 267 | std["typing.List"] = importSpec{Module: "typing", Name: "List"} 268 | } 269 | if uses("typing.Optional") { 270 | std["typing.Optional"] = importSpec{Module: "typing", Name: "Optional"} 271 | } 272 | if uses("Any") { 273 | std["typing.Any"] = importSpec{Module: "typing", Name: "Any"} 274 | } 275 | return std 276 | } 277 | -------------------------------------------------------------------------------- /internal/inflection/singular.go: -------------------------------------------------------------------------------- 1 | package inflection 2 | 3 | import ( 4 | "strings" 5 | 6 | upstream "github.com/jinzhu/inflection" 7 | ) 8 | 9 | type SingularParams struct { 10 | Name string 11 | Exclusions []string 12 | } 13 | 14 | func Singular(s SingularParams) string { 15 | for _, exclusion := range s.Exclusions { 16 | if strings.EqualFold(s.Name, exclusion) { 17 | return s.Name 18 | } 19 | } 20 | 21 | // Manual fix for incorrect handling of "campus" 22 | // 23 | // https://github.com/kyleconroy/sqlc/issues/430 24 | // https://github.com/jinzhu/inflection/issues/13 25 | if strings.ToLower(s.Name) == "campus" { 26 | return s.Name 27 | } 28 | // Manual fix for incorrect handling of "meta" 29 | // 30 | // https://github.com/kyleconroy/sqlc/issues/1217 31 | // https://github.com/jinzhu/inflection/issues/21 32 | if strings.ToLower(s.Name) == "meta" { 33 | return s.Name 34 | } 35 | return upstream.Singular(s.Name) 36 | } 37 | -------------------------------------------------------------------------------- /internal/poet/builders.go: -------------------------------------------------------------------------------- 1 | package poet 2 | 3 | import "github.com/sqlc-dev/sqlc-gen-python/internal/ast" 4 | 5 | func Alias(name string) *ast.Node { 6 | return &ast.Node{ 7 | Node: &ast.Node_Alias{ 8 | Alias: &ast.Alias{ 9 | Name: name, 10 | }, 11 | }, 12 | } 13 | } 14 | 15 | func Await(value *ast.Node) *ast.Node { 16 | return &ast.Node{ 17 | Node: &ast.Node_Await{ 18 | Await: &ast.Await{ 19 | Value: value, 20 | }, 21 | }, 22 | } 23 | } 24 | 25 | func Attribute(value *ast.Node, attr string) *ast.Node { 26 | return &ast.Node{ 27 | Node: &ast.Node_Attribute{ 28 | Attribute: &ast.Attribute{ 29 | Value: value, 30 | Attr: attr, 31 | }, 32 | }, 33 | } 34 | } 35 | 36 | func Comment(text string) *ast.Node { 37 | return &ast.Node{ 38 | Node: &ast.Node_Comment{ 39 | Comment: &ast.Comment{ 40 | Text: text, 41 | }, 42 | }, 43 | } 44 | } 45 | 46 | func Expr(value *ast.Node) *ast.Node { 47 | return &ast.Node{ 48 | Node: &ast.Node_Expr{ 49 | Expr: &ast.Expr{ 50 | Value: value, 51 | }, 52 | }, 53 | } 54 | } 55 | 56 | func Is() *ast.Node { 57 | return &ast.Node{ 58 | Node: &ast.Node_Is{ 59 | Is: &ast.Is{}, 60 | }, 61 | } 62 | } 63 | 64 | func Name(id string) *ast.Node { 65 | return &ast.Node{ 66 | Node: &ast.Node_Name{ 67 | Name: &ast.Name{Id: id}, 68 | }, 69 | } 70 | } 71 | 72 | func Return(value *ast.Node) *ast.Node { 73 | return &ast.Node{ 74 | Node: &ast.Node_Return{ 75 | Return: &ast.Return{ 76 | Value: value, 77 | }, 78 | }, 79 | } 80 | } 81 | 82 | func Yield(value *ast.Node) *ast.Node { 83 | return &ast.Node{ 84 | Node: &ast.Node_Yield{ 85 | Yield: &ast.Yield{ 86 | Value: value, 87 | }, 88 | }, 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /internal/poet/poet.go: -------------------------------------------------------------------------------- 1 | package poet 2 | 3 | import ( 4 | "github.com/sqlc-dev/sqlc-gen-python/internal/ast" 5 | ) 6 | 7 | type proto interface { 8 | ProtoMessage() 9 | } 10 | 11 | func Nodes(nodes ...proto) []*ast.Node { 12 | list := make([]*ast.Node, len(nodes)) 13 | for i, _ := range nodes { 14 | list[i] = Node(nodes[i]) 15 | } 16 | return list 17 | } 18 | 19 | func Node(node proto) *ast.Node { 20 | switch n := node.(type) { 21 | 22 | case *ast.Alias: 23 | return &ast.Node{ 24 | Node: &ast.Node_Alias{ 25 | Alias: n, 26 | }, 27 | } 28 | 29 | case *ast.Await: 30 | return &ast.Node{ 31 | Node: &ast.Node_Await{ 32 | Await: n, 33 | }, 34 | } 35 | 36 | case *ast.AnnAssign: 37 | return &ast.Node{ 38 | Node: &ast.Node_AnnAssign{ 39 | AnnAssign: n, 40 | }, 41 | } 42 | 43 | case *ast.Assign: 44 | return &ast.Node{ 45 | Node: &ast.Node_Assign{ 46 | Assign: n, 47 | }, 48 | } 49 | 50 | case *ast.AsyncFor: 51 | return &ast.Node{ 52 | Node: &ast.Node_AsyncFor{ 53 | AsyncFor: n, 54 | }, 55 | } 56 | 57 | case *ast.AsyncFunctionDef: 58 | return &ast.Node{ 59 | Node: &ast.Node_AsyncFunctionDef{ 60 | AsyncFunctionDef: n, 61 | }, 62 | } 63 | 64 | case *ast.Attribute: 65 | return &ast.Node{ 66 | Node: &ast.Node_Attribute{ 67 | Attribute: n, 68 | }, 69 | } 70 | 71 | case *ast.Call: 72 | return &ast.Node{ 73 | Node: &ast.Node_Call{ 74 | Call: n, 75 | }, 76 | } 77 | 78 | case *ast.ClassDef: 79 | return &ast.Node{ 80 | Node: &ast.Node_ClassDef{ 81 | ClassDef: n, 82 | }, 83 | } 84 | 85 | case *ast.Comment: 86 | return &ast.Node{ 87 | Node: &ast.Node_Comment{ 88 | Comment: n, 89 | }, 90 | } 91 | 92 | case *ast.Compare: 93 | return &ast.Node{ 94 | Node: &ast.Node_Compare{ 95 | Compare: n, 96 | }, 97 | } 98 | 99 | // case *ast.Constant: 100 | 101 | // case *ast.Dict: 102 | 103 | case *ast.Expr: 104 | return &ast.Node{ 105 | Node: &ast.Node_Expr{ 106 | Expr: n, 107 | }, 108 | } 109 | 110 | case *ast.For: 111 | return &ast.Node{ 112 | Node: &ast.Node_For{ 113 | For: n, 114 | }, 115 | } 116 | 117 | case *ast.FunctionDef: 118 | return &ast.Node{ 119 | Node: &ast.Node_FunctionDef{ 120 | FunctionDef: n, 121 | }, 122 | } 123 | 124 | case *ast.If: 125 | return &ast.Node{ 126 | Node: &ast.Node_If{ 127 | If: n, 128 | }, 129 | } 130 | 131 | // case *ast.Node_Import: 132 | // w.printImport(n.Import, indent) 133 | 134 | // case *ast.Node_ImportFrom: 135 | // w.printImportFrom(n.ImportFrom, indent) 136 | 137 | // case *ast.Node_Is: 138 | // w.print("is") 139 | 140 | // case *ast.Node_Keyword: 141 | // w.printKeyword(n.Keyword, indent) 142 | 143 | case *ast.Module: 144 | return &ast.Node{ 145 | Node: &ast.Node_Module{ 146 | Module: n, 147 | }, 148 | } 149 | 150 | // w.printModule(n.Module, indent) 151 | 152 | // case *ast.Node_Name: 153 | // w.print(n.Name.Id) 154 | 155 | // case *ast.Node_Pass: 156 | // w.print("pass") 157 | 158 | // case *ast.Node_Return: 159 | // w.printReturn(n.Return, indent) 160 | 161 | // case *ast.Node_Subscript: 162 | // w.printSubscript(n.Subscript, indent) 163 | 164 | case *ast.Yield: 165 | return &ast.Node{ 166 | Node: &ast.Node_Yield{ 167 | Yield: n, 168 | }, 169 | } 170 | 171 | default: 172 | panic(n) 173 | } 174 | 175 | } 176 | 177 | func Constant(value interface{}) *ast.Node { 178 | switch n := value.(type) { 179 | case string: 180 | return &ast.Node{ 181 | Node: &ast.Node_Constant{ 182 | Constant: &ast.Constant{ 183 | Value: &ast.Constant_Str{ 184 | Str: n, 185 | }, 186 | }, 187 | }, 188 | } 189 | 190 | case int: 191 | return &ast.Node{ 192 | Node: &ast.Node_Constant{ 193 | Constant: &ast.Constant{ 194 | Value: &ast.Constant_Int{ 195 | Int: int32(n), 196 | }, 197 | }, 198 | }, 199 | } 200 | 201 | case nil: 202 | return &ast.Node{ 203 | Node: &ast.Node_Constant{ 204 | Constant: &ast.Constant{ 205 | Value: &ast.Constant_None{}, 206 | }, 207 | }, 208 | } 209 | 210 | default: 211 | panic("unknown type") 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /internal/postgresql_type.go: -------------------------------------------------------------------------------- 1 | package python 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/sqlc-dev/plugin-sdk-go/plugin" 7 | "github.com/sqlc-dev/plugin-sdk-go/sdk" 8 | ) 9 | 10 | func postgresType(req *plugin.GenerateRequest, col *plugin.Column) string { 11 | columnType := sdk.DataType(col.Type) 12 | 13 | switch columnType { 14 | case "serial", "serial4", "pg_catalog.serial4", "bigserial", "serial8", "pg_catalog.serial8", "smallserial", "serial2", "pg_catalog.serial2", "integer", "int", "int4", "pg_catalog.int4", "bigint", "int8", "pg_catalog.int8", "smallint", "int2", "pg_catalog.int2": 15 | return "int" 16 | case "float", "double precision", "float8", "pg_catalog.float8", "real", "float4", "pg_catalog.float4": 17 | return "float" 18 | case "numeric", "pg_catalog.numeric", "money": 19 | return "decimal.Decimal" 20 | case "boolean", "bool", "pg_catalog.bool": 21 | return "bool" 22 | case "json", "jsonb": 23 | return "Any" 24 | case "bytea", "blob", "pg_catalog.bytea": 25 | return "memoryview" 26 | case "date": 27 | return "datetime.date" 28 | case "pg_catalog.time", "pg_catalog.timetz": 29 | return "datetime.time" 30 | case "pg_catalog.timestamp", "pg_catalog.timestamptz", "timestamptz": 31 | return "datetime.datetime" 32 | case "interval", "pg_catalog.interval": 33 | return "datetime.timedelta" 34 | case "text", "pg_catalog.varchar", "pg_catalog.bpchar", "string", "citext": 35 | return "str" 36 | case "uuid": 37 | return "uuid.UUID" 38 | case "inet", "cidr", "macaddr", "macaddr8": 39 | // psycopg2 does have support for ipaddress objects, but it is not enabled by default 40 | // 41 | // https://www.psycopg.org/docs/extras.html#adapt-network 42 | return "str" 43 | case "ltree", "lquery", "ltxtquery": 44 | return "str" 45 | default: 46 | for _, schema := range req.Catalog.Schemas { 47 | if schema.Name == "pg_catalog" || schema.Name == "information_schema" { 48 | continue 49 | } 50 | for _, enum := range schema.Enums { 51 | if columnType == enum.Name { 52 | if schema.Name == req.Catalog.DefaultSchema { 53 | return "models." + modelName(enum.Name, req.Settings) 54 | } 55 | return "models." + modelName(schema.Name+"_"+enum.Name, req.Settings) 56 | } 57 | } 58 | } 59 | log.Printf("unknown PostgreSQL type: %s\n", columnType) 60 | return "Any" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internal/printer/printer.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "github.com/sqlc-dev/sqlc-gen-python/internal/ast" 8 | ) 9 | 10 | type writer struct { 11 | options Options 12 | src []byte 13 | } 14 | 15 | type Options struct { 16 | } 17 | 18 | type PrintResult struct { 19 | Python []byte 20 | } 21 | 22 | func Print(node *ast.Node, options Options) PrintResult { 23 | w := writer{options: options} 24 | w.printNode(node, 0) 25 | return PrintResult{ 26 | Python: w.src, 27 | } 28 | } 29 | 30 | func (w *writer) print(text string) { 31 | w.src = append(w.src, text...) 32 | } 33 | 34 | func (w *writer) printIndent(indent int32) { 35 | for i, n := 0, int(indent); i < n; i++ { 36 | w.src = append(w.src, " "...) 37 | } 38 | } 39 | 40 | func (w *writer) printNode(node *ast.Node, indent int32) { 41 | switch n := node.Node.(type) { 42 | 43 | case *ast.Node_Alias: 44 | w.print(n.Alias.Name) 45 | 46 | case *ast.Node_AnnAssign: 47 | w.printAnnAssign(n.AnnAssign, indent) 48 | 49 | case *ast.Node_Assign: 50 | w.printAssign(n.Assign, indent) 51 | 52 | case *ast.Node_AsyncFor: 53 | w.printAsyncFor(n.AsyncFor, indent) 54 | 55 | case *ast.Node_AsyncFunctionDef: 56 | w.printAsyncFunctionDef(n.AsyncFunctionDef, indent) 57 | 58 | case *ast.Node_Attribute: 59 | w.printAttribute(n.Attribute, indent) 60 | 61 | case *ast.Node_Await: 62 | w.printAwait(n.Await, indent) 63 | 64 | case *ast.Node_Call: 65 | w.printCall(n.Call, indent) 66 | 67 | case *ast.Node_ClassDef: 68 | w.printClassDef(n.ClassDef, indent) 69 | 70 | case *ast.Node_Comment: 71 | w.printComment(n.Comment, indent) 72 | 73 | case *ast.Node_Compare: 74 | w.printCompare(n.Compare, indent) 75 | 76 | case *ast.Node_Constant: 77 | w.printConstant(n.Constant, indent) 78 | 79 | case *ast.Node_Dict: 80 | w.printDict(n.Dict, indent) 81 | 82 | case *ast.Node_Expr: 83 | w.printNode(n.Expr.Value, indent) 84 | 85 | case *ast.Node_For: 86 | w.printFor(n.For, indent) 87 | 88 | case *ast.Node_FunctionDef: 89 | w.printFunctionDef(n.FunctionDef, indent) 90 | 91 | case *ast.Node_If: 92 | w.printIf(n.If, indent) 93 | 94 | case *ast.Node_Import: 95 | w.printImport(n.Import, indent) 96 | 97 | case *ast.Node_ImportFrom: 98 | w.printImportFrom(n.ImportFrom, indent) 99 | 100 | case *ast.Node_ImportGroup: 101 | w.printImportGroup(n.ImportGroup, indent) 102 | 103 | case *ast.Node_Is: 104 | w.print("is") 105 | 106 | case *ast.Node_Keyword: 107 | w.printKeyword(n.Keyword, indent) 108 | 109 | case *ast.Node_Module: 110 | w.printModule(n.Module, indent) 111 | 112 | case *ast.Node_Name: 113 | w.print(n.Name.Id) 114 | 115 | case *ast.Node_Pass: 116 | w.print("pass") 117 | 118 | case *ast.Node_Return: 119 | w.printReturn(n.Return, indent) 120 | 121 | case *ast.Node_Subscript: 122 | w.printSubscript(n.Subscript, indent) 123 | 124 | case *ast.Node_Yield: 125 | w.printYield(n.Yield, indent) 126 | 127 | default: 128 | panic(n) 129 | 130 | } 131 | } 132 | 133 | func (w *writer) printAnnAssign(aa *ast.AnnAssign, indent int32) { 134 | if aa.Comment != "" { 135 | w.print("# ") 136 | w.print(aa.Comment) 137 | w.print("\n") 138 | w.printIndent(indent) 139 | } 140 | w.printName(aa.Target, indent) 141 | w.print(": ") 142 | w.printNode(aa.Annotation, indent) 143 | } 144 | 145 | func (w *writer) printArg(a *ast.Arg, indent int32) { 146 | w.print(a.Arg) 147 | if a.Annotation != nil { 148 | w.print(": ") 149 | w.printNode(a.Annotation, indent) 150 | } 151 | } 152 | 153 | func (w *writer) printAssign(a *ast.Assign, indent int32) { 154 | for i, name := range a.Targets { 155 | w.printNode(name, indent) 156 | if i != len(a.Targets)-1 { 157 | w.print(", ") 158 | } 159 | } 160 | w.print(" = ") 161 | w.printNode(a.Value, indent) 162 | } 163 | 164 | func (w *writer) printAsyncFor(n *ast.AsyncFor, indent int32) { 165 | w.print("async ") 166 | w.printFor(&ast.For{ 167 | Target: n.Target, 168 | Iter: n.Iter, 169 | Body: n.Body, 170 | }, indent) 171 | } 172 | 173 | func (w *writer) printAsyncFunctionDef(afd *ast.AsyncFunctionDef, indent int32) { 174 | w.print("async ") 175 | w.printFunctionDef(&ast.FunctionDef{ 176 | Name: afd.Name, 177 | Args: afd.Args, 178 | Body: afd.Body, 179 | Returns: afd.Returns, 180 | }, indent) 181 | } 182 | 183 | func (w *writer) printAttribute(a *ast.Attribute, indent int32) { 184 | if _, ok := a.Value.Node.(*ast.Node_Await); ok { 185 | w.print("(") 186 | w.printNode(a.Value, indent) 187 | w.print(")") 188 | } else { 189 | w.printNode(a.Value, indent) 190 | } 191 | w.print(".") 192 | w.print(a.Attr) 193 | } 194 | 195 | func (w *writer) printAwait(n *ast.Await, indent int32) { 196 | w.print("await ") 197 | w.printNode(n.Value, indent) 198 | } 199 | 200 | func (w *writer) printCall(c *ast.Call, indent int32) { 201 | w.printNode(c.Func, indent) 202 | w.print("(") 203 | for i, a := range c.Args { 204 | w.printNode(a, indent) 205 | if i != len(c.Args)-1 { 206 | w.print(", ") 207 | } 208 | } 209 | for _, kw := range c.Keywords { 210 | w.print("\n") 211 | w.printIndent(indent + 1) 212 | w.printKeyword(kw, indent+1) 213 | w.print(",") 214 | } 215 | if len(c.Keywords) > 0 { 216 | w.print("\n") 217 | w.printIndent(indent) 218 | } 219 | w.print(")") 220 | } 221 | 222 | func (w *writer) printClassDef(cd *ast.ClassDef, indent int32) { 223 | for _, node := range cd.DecoratorList { 224 | w.print("@") 225 | w.printNode(node, indent) 226 | w.print("\n") 227 | } 228 | w.print("class ") 229 | w.print(cd.Name) 230 | if len(cd.Bases) > 0 { 231 | w.print("(") 232 | for i, node := range cd.Bases { 233 | w.printNode(node, indent) 234 | if i != len(cd.Bases)-1 { 235 | w.print(", ") 236 | } 237 | } 238 | w.print(")") 239 | } 240 | w.print(":\n") 241 | for i, node := range cd.Body { 242 | if i != 0 { 243 | if _, ok := node.Node.(*ast.Node_FunctionDef); ok { 244 | w.print("\n") 245 | } 246 | if _, ok := node.Node.(*ast.Node_AsyncFunctionDef); ok { 247 | w.print("\n") 248 | } 249 | } 250 | w.printIndent(indent + 1) 251 | // A docstring is a string literal that occurs as the first 252 | // statement in a module, function, class, or method 253 | // definition. Such a docstring becomes the __doc__ special 254 | // attribute of that object. 255 | if i == 0 { 256 | if e, ok := node.Node.(*ast.Node_Expr); ok { 257 | if c, ok := e.Expr.Value.Node.(*ast.Node_Constant); ok { 258 | w.print(`""`) 259 | w.printConstant(c.Constant, indent) 260 | w.print(`""`) 261 | w.print("\n") 262 | continue 263 | } 264 | } 265 | } 266 | w.printNode(node, indent+1) 267 | w.print("\n") 268 | } 269 | } 270 | 271 | func (w *writer) printConstant(c *ast.Constant, indent int32) { 272 | switch n := c.Value.(type) { 273 | case *ast.Constant_Int: 274 | w.print(strconv.Itoa(int(n.Int))) 275 | 276 | case *ast.Constant_None: 277 | w.print("None") 278 | 279 | case *ast.Constant_Str: 280 | str := `"` 281 | if strings.Contains(n.Str, "\n") { 282 | str = `"""` 283 | } 284 | w.print(str) 285 | w.print(n.Str) 286 | w.print(str) 287 | 288 | default: 289 | panic(n) 290 | } 291 | } 292 | 293 | func (w *writer) printComment(c *ast.Comment, indent int32) { 294 | w.print("# ") 295 | w.print(c.Text) 296 | w.print("\n") 297 | } 298 | 299 | func (w *writer) printCompare(c *ast.Compare, indent int32) { 300 | w.printNode(c.Left, indent) 301 | w.print(" ") 302 | for _, node := range c.Ops { 303 | w.printNode(node, indent) 304 | w.print(" ") 305 | } 306 | for _, node := range c.Comparators { 307 | w.printNode(node, indent) 308 | } 309 | } 310 | 311 | func (w *writer) printDict(d *ast.Dict, indent int32) { 312 | if len(d.Keys) != len(d.Values) { 313 | panic(`dict keys and values are not the same length`) 314 | } 315 | w.print("{") 316 | split := len(d.Keys) > 3 317 | keyIndent := indent 318 | if split { 319 | keyIndent += 1 320 | } 321 | for i, _ := range d.Keys { 322 | if split { 323 | w.print("\n") 324 | w.printIndent(keyIndent) 325 | } 326 | w.printNode(d.Keys[i], keyIndent) 327 | w.print(": ") 328 | w.printNode(d.Values[i], keyIndent) 329 | if i != len(d.Keys)-1 || split { 330 | if split { 331 | w.print(",") 332 | } else { 333 | w.print(", ") 334 | } 335 | } 336 | } 337 | if split { 338 | w.print("\n") 339 | w.printIndent(indent) 340 | } 341 | w.print("}") 342 | } 343 | 344 | func (w *writer) printFor(n *ast.For, indent int32) { 345 | w.print("for ") 346 | w.printNode(n.Target, indent) 347 | w.print(" in ") 348 | w.printNode(n.Iter, indent) 349 | w.print(":\n") 350 | for i, node := range n.Body { 351 | w.printIndent(indent + 1) 352 | w.printNode(node, indent+1) 353 | if i != len(n.Body)-1 { 354 | w.print("\n") 355 | } 356 | } 357 | } 358 | 359 | func (w *writer) printIf(i *ast.If, indent int32) { 360 | w.print("if ") 361 | w.printNode(i.Test, indent) 362 | w.print(":\n") 363 | for j, node := range i.Body { 364 | w.printIndent(indent + 1) 365 | w.printNode(node, indent+1) 366 | if j != len(i.Body)-1 { 367 | w.print("\n") 368 | } 369 | } 370 | } 371 | 372 | func (w *writer) printFunctionDef(fd *ast.FunctionDef, indent int32) { 373 | w.print("def ") 374 | w.print(fd.Name) 375 | w.print("(") 376 | if fd.Args != nil { 377 | for i, arg := range fd.Args.Args { 378 | w.printArg(arg, indent) 379 | if i != len(fd.Args.Args)-1 { 380 | w.print(", ") 381 | } 382 | } 383 | if len(fd.Args.KwOnlyArgs) > 0 { 384 | w.print(", *, ") 385 | for i, arg := range fd.Args.KwOnlyArgs { 386 | w.printArg(arg, indent) 387 | if i != len(fd.Args.KwOnlyArgs)-1 { 388 | w.print(", ") 389 | } 390 | } 391 | } 392 | } 393 | w.print(")") 394 | if fd.Returns != nil { 395 | w.print(" -> ") 396 | w.printNode(fd.Returns, indent) 397 | } 398 | w.print(":\n") 399 | for i, node := range fd.Body { 400 | w.printIndent(indent + 1) 401 | w.printNode(node, indent+1) 402 | if i != len(fd.Body)-1 { 403 | w.print("\n") 404 | } 405 | } 406 | } 407 | 408 | func (w *writer) printImport(imp *ast.Import, indent int32) { 409 | w.print("import ") 410 | for i, node := range imp.Names { 411 | w.printNode(node, indent) 412 | if i != len(imp.Names)-1 { 413 | w.print(", ") 414 | } 415 | } 416 | w.print("\n") 417 | } 418 | 419 | func (w *writer) printImportFrom(imp *ast.ImportFrom, indent int32) { 420 | w.print("from ") 421 | w.print(imp.Module) 422 | w.print(" import ") 423 | for i, node := range imp.Names { 424 | w.printNode(node, indent) 425 | if i != len(imp.Names)-1 { 426 | w.print(", ") 427 | } 428 | } 429 | w.print("\n") 430 | } 431 | 432 | func (w *writer) printImportGroup(n *ast.ImportGroup, indent int32) { 433 | if len(n.Imports) == 0 { 434 | return 435 | } 436 | for _, node := range n.Imports { 437 | w.printNode(node, indent) 438 | } 439 | w.print("\n") 440 | } 441 | 442 | func (w *writer) printIs(i *ast.Is, indent int32) { 443 | w.print("is") 444 | } 445 | func (w *writer) printKeyword(k *ast.Keyword, indent int32) { 446 | w.print(k.Arg) 447 | w.print("=") 448 | w.printNode(k.Value, indent) 449 | } 450 | 451 | func (w *writer) printModule(mod *ast.Module, indent int32) { 452 | for i, node := range mod.Body { 453 | prevIsImport := false 454 | if i > 0 { 455 | _, isImport := mod.Body[i-1].Node.(*ast.Node_ImportGroup) 456 | prevIsImport = isImport 457 | } 458 | _, isClassDef := node.Node.(*ast.Node_ClassDef) 459 | _, isAssign := node.Node.(*ast.Node_Assign) 460 | if isClassDef || isAssign { 461 | if prevIsImport { 462 | w.print("\n") 463 | } else { 464 | w.print("\n\n") 465 | } 466 | } 467 | w.printNode(node, indent) 468 | if isAssign { 469 | w.print("\n") 470 | } 471 | } 472 | } 473 | 474 | func (w *writer) printName(n *ast.Name, indent int32) { 475 | w.print(n.Id) 476 | } 477 | 478 | func (w *writer) printReturn(r *ast.Return, indent int32) { 479 | w.print("return ") 480 | w.printNode(r.Value, indent) 481 | } 482 | 483 | func (w *writer) printSubscript(ss *ast.Subscript, indent int32) { 484 | w.printName(ss.Value, indent) 485 | w.print("[") 486 | w.printNode(ss.Slice, indent) 487 | w.print("]") 488 | 489 | } 490 | 491 | func (w *writer) printYield(n *ast.Yield, indent int32) { 492 | w.print("yield ") 493 | w.printNode(n.Value, indent) 494 | } 495 | -------------------------------------------------------------------------------- /internal/printer/printer_test.go: -------------------------------------------------------------------------------- 1 | package printer 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | 9 | "github.com/sqlc-dev/sqlc-gen-python/internal/ast" 10 | ) 11 | 12 | type testcase struct { 13 | Node *ast.Node 14 | Expected string 15 | } 16 | 17 | func TestPrinter(t *testing.T) { 18 | for name, tc := range map[string]testcase{ 19 | "assign": { 20 | Node: &ast.Node{ 21 | Node: &ast.Node_Assign{ 22 | Assign: &ast.Assign{ 23 | Targets: []*ast.Node{ 24 | { 25 | Node: &ast.Node_Name{ 26 | Name: &ast.Name{Id: "FICTION"}, 27 | }, 28 | }, 29 | }, 30 | Value: &ast.Node{ 31 | Node: &ast.Node_Constant{ 32 | Constant: &ast.Constant{ 33 | Value: &ast.Constant_Str{ 34 | Str: "FICTION", 35 | }, 36 | }, 37 | }, 38 | }, 39 | }, 40 | }, 41 | }, 42 | Expected: `FICTION = "FICTION"`, 43 | }, 44 | "class-base": { 45 | Node: &ast.Node{ 46 | Node: &ast.Node_ClassDef{ 47 | ClassDef: &ast.ClassDef{ 48 | Name: "Foo", 49 | Bases: []*ast.Node{ 50 | { 51 | Node: &ast.Node_Name{ 52 | Name: &ast.Name{Id: "str"}, 53 | }, 54 | }, 55 | { 56 | Node: &ast.Node_Attribute{ 57 | Attribute: &ast.Attribute{ 58 | Value: &ast.Node{ 59 | Node: &ast.Node_Name{ 60 | Name: &ast.Name{Id: "enum"}, 61 | }, 62 | }, 63 | Attr: "Enum", 64 | }, 65 | }, 66 | }, 67 | }, 68 | }, 69 | }, 70 | }, 71 | Expected: `class Foo(str, enum.Enum):`, 72 | }, 73 | "dataclass": { 74 | Node: &ast.Node{ 75 | Node: &ast.Node_ClassDef{ 76 | ClassDef: &ast.ClassDef{ 77 | Name: "Foo", 78 | DecoratorList: []*ast.Node{ 79 | { 80 | Node: &ast.Node_Name{ 81 | Name: &ast.Name{ 82 | Id: "dataclass", 83 | }, 84 | }, 85 | }, 86 | }, 87 | Body: []*ast.Node{ 88 | { 89 | Node: &ast.Node_AnnAssign{ 90 | AnnAssign: &ast.AnnAssign{ 91 | Target: &ast.Name{Id: "bar"}, 92 | Annotation: &ast.Node{ 93 | Node: &ast.Node_Name{ 94 | Name: &ast.Name{Id: "int"}, 95 | }, 96 | }, 97 | }, 98 | }, 99 | }, 100 | { 101 | Node: &ast.Node_AnnAssign{ 102 | AnnAssign: &ast.AnnAssign{ 103 | Target: &ast.Name{Id: "bat"}, 104 | Annotation: &ast.Node{ 105 | Node: &ast.Node_Subscript{ 106 | Subscript: &ast.Subscript{ 107 | Value: &ast.Name{Id: "Optional"}, 108 | Slice: &ast.Node{ 109 | Node: &ast.Node_Name{ 110 | Name: &ast.Name{Id: "int"}, 111 | }, 112 | }, 113 | }, 114 | }, 115 | }, 116 | }, 117 | }, 118 | }, 119 | }, 120 | }, 121 | }, 122 | }, 123 | Expected: ` 124 | @dataclass 125 | class Foo: 126 | bar: int 127 | bat: Optional[int] 128 | `, 129 | }, 130 | "call": { 131 | Node: &ast.Node{ 132 | Node: &ast.Node_Call{ 133 | Call: &ast.Call{ 134 | Func: &ast.Node{ 135 | Node: &ast.Node_Alias{ 136 | Alias: &ast.Alias{ 137 | Name: "foo", 138 | }, 139 | }, 140 | }, 141 | }, 142 | }, 143 | }, 144 | Expected: `foo()`, 145 | }, 146 | 147 | "import": { 148 | Node: &ast.Node{ 149 | Node: &ast.Node_Import{ 150 | Import: &ast.Import{ 151 | Names: []*ast.Node{ 152 | { 153 | Node: &ast.Node_Alias{ 154 | Alias: &ast.Alias{ 155 | Name: "foo", 156 | }, 157 | }, 158 | }, 159 | }, 160 | }, 161 | }, 162 | }, 163 | Expected: `import foo`, 164 | }, 165 | "import-from": { 166 | Node: &ast.Node{ 167 | Node: &ast.Node_ImportFrom{ 168 | ImportFrom: &ast.ImportFrom{ 169 | Module: "pkg", 170 | Names: []*ast.Node{ 171 | { 172 | Node: &ast.Node_Alias{ 173 | Alias: &ast.Alias{ 174 | Name: "foo", 175 | }, 176 | }, 177 | }, 178 | { 179 | Node: &ast.Node_Alias{ 180 | Alias: &ast.Alias{ 181 | Name: "bar", 182 | }, 183 | }, 184 | }, 185 | }, 186 | }, 187 | }, 188 | }, 189 | Expected: `from pkg import foo, bar`, 190 | }, 191 | } { 192 | tc := tc 193 | t.Run(name, func(t *testing.T) { 194 | result := Print(tc.Node, Options{}) 195 | if diff := cmp.Diff(strings.TrimSpace(tc.Expected), strings.TrimSpace(string(result.Python))); diff != "" { 196 | t.Errorf("print mismatch (-want +got):\n%s", diff) 197 | } 198 | }) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /plugin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/sqlc-dev/plugin-sdk-go/codegen" 5 | 6 | python "github.com/sqlc-dev/sqlc-gen-python/internal" 7 | ) 8 | 9 | func main() { 10 | codegen.Run(python.Generate) 11 | } 12 | -------------------------------------------------------------------------------- /protos/ast/ast.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package ast; 4 | 5 | option go_package = "github.com/sqlc-dev/sqlc-gen-python/internal/ast"; 6 | 7 | message Node { 8 | oneof node { 9 | ClassDef class_def = 1 [json_name="ClassDef"]; 10 | Import import = 2 [json_name="Import"]; 11 | ImportFrom import_from = 3 [json_name="ImportFrom"]; 12 | Module module = 4 [json_name="Module"]; 13 | Alias alias = 5 [json_name="Alias"]; 14 | AnnAssign ann_assign = 6 [json_name="AnnAssign"]; 15 | Name name = 7 [json_name="Name"]; 16 | Subscript subscript = 8 [json_name="Subscript"]; 17 | Attribute attribute = 9 [json_name="Attribute"]; 18 | Constant constant = 10 [json_name="Constant"]; 19 | Assign assign = 11 [json_name="Assign"]; 20 | Comment comment = 12 [json_name="Comment"]; 21 | Expr expr = 13 [json_name="Expr"]; 22 | Call call = 14 [json_name="Call"]; 23 | FunctionDef function_def = 15 [json_name="FunctionDef"]; 24 | Arg arg = 16 [json_name="Arg"]; 25 | Arguments arguments = 17 [json_name="Arguments"]; 26 | AsyncFunctionDef async_function_def = 18 [json_name="AsyncFunctionDef"]; 27 | Pass pass = 19 [json_name="Pass"]; 28 | Dict dict = 20 [json_name="Dict"]; 29 | If if = 21 [json_name="If"]; 30 | Compare compare = 22 [json_name="Compare"]; 31 | Return return = 23 [json_name="Return"]; 32 | Is is = 24 [json_name="Is"]; 33 | Keyword keyword = 25 [json_name="Keyword"]; 34 | Yield yield = 26 [json_name="Yield"]; 35 | For for = 27 [json_name="For"]; 36 | Await await = 28 [json_name="Await"]; 37 | AsyncFor async_for = 29 [json_name="AsyncFor"]; 38 | ImportGroup import_group = 30 [json_name="ImportGroup"]; 39 | } 40 | } 41 | 42 | message Alias 43 | { 44 | string name = 1 [json_name="name"]; 45 | } 46 | 47 | message Await 48 | { 49 | Node value = 1 [json_name="value"]; 50 | } 51 | 52 | message Attribute 53 | { 54 | Node value = 1 [json_name="value"]; 55 | string attr = 2 [json_name="attr"]; 56 | } 57 | 58 | message AnnAssign 59 | { 60 | Name target = 1 [json_name="target"]; 61 | Node annotation = 2 [json_name="annotation"]; 62 | int32 simple = 3 [json_name="simple"]; 63 | string Comment = 4 [json_name="comment"]; 64 | } 65 | 66 | message Arg 67 | { 68 | string arg = 1 [json_name="arg"]; 69 | Node annotation = 2 [json_name="annotation"]; 70 | } 71 | 72 | message Arguments 73 | { 74 | repeated Arg args = 1 [json_name="args"]; 75 | repeated Arg kw_only_args = 2 [json_name="kwonlyargs"]; 76 | } 77 | 78 | message AsyncFor 79 | { 80 | Node target = 1 [json_name="target"]; 81 | Node iter = 2 [json_name="iter"]; 82 | repeated Node body = 3 [json_name="body"]; 83 | } 84 | 85 | message AsyncFunctionDef 86 | { 87 | string name = 1 [json_name="name"]; 88 | Arguments Args = 2 [json_name="args"]; 89 | repeated Node body = 3 [json_name="body"]; 90 | Node returns = 4 [json_name="returns"]; 91 | } 92 | 93 | message Assign 94 | { 95 | repeated Node targets = 1 [json_name="targets"]; 96 | Node value = 2 [json_name="value"]; 97 | string Comment = 3 [json_name="comment"]; 98 | } 99 | 100 | message Call 101 | { 102 | Node func = 1 [json_name="func"]; 103 | repeated Node args = 2 [json_name="args"]; 104 | repeated Keyword keywords = 3 [json_name="keywords"]; 105 | } 106 | 107 | message ClassDef 108 | { 109 | string name = 1 [json_name="name"]; 110 | repeated Node bases = 2 [json_name="bases"]; 111 | repeated Node keywords = 3 [json_name="keywords"]; 112 | repeated Node body = 4 [json_name="body"]; 113 | repeated Node decorator_list = 5 [json_name="decorator_list"]; 114 | } 115 | 116 | // The Python ast module does not parse comments. It's not clear if this is the 117 | // best way to support them in the AST 118 | message Comment 119 | { 120 | string text = 1 [json_name="text"]; 121 | } 122 | 123 | message Compare 124 | { 125 | Node left = 1 [json_name="left"]; 126 | repeated Node ops = 2 [json_name="ops"]; 127 | repeated Node comparators = 3 [json_name="comparators"]; 128 | } 129 | 130 | message Constant 131 | { 132 | oneof value { 133 | string str = 1 [json_name="string"]; 134 | int32 int = 2 [json_name="int"]; 135 | bool none = 3 [json_name="none"]; 136 | } 137 | } 138 | 139 | message Dict 140 | { 141 | repeated Node keys = 1 [json_name="keys"]; 142 | repeated Node values = 2 [json_name="values"]; 143 | } 144 | 145 | message Expr 146 | { 147 | Node value = 1 [json_name="value"]; 148 | } 149 | 150 | message For 151 | { 152 | Node target = 1 [json_name="target"]; 153 | Node iter = 2 [json_name="iter"]; 154 | repeated Node body = 3 [json_name="body"]; 155 | } 156 | 157 | message FunctionDef 158 | { 159 | string name = 1 [json_name="name"]; 160 | Arguments Args = 2 [json_name="args"]; 161 | repeated Node body = 3 [json_name="body"]; 162 | Node returns = 4 [json_name="returns"]; 163 | } 164 | 165 | message If 166 | { 167 | Node test = 1 [json_name="test"]; 168 | repeated Node body = 2 [json_name="body"]; 169 | repeated Node or_else = 3 [json_name="orelse"]; 170 | } 171 | 172 | message Import 173 | { 174 | repeated Node names = 1 [json_name="names"]; 175 | } 176 | 177 | message ImportFrom 178 | { 179 | string module = 1 [json_name="module"]; 180 | repeated Node names = 2 [json_name="names"]; 181 | int32 level = 3 [json_name="level"]; 182 | } 183 | 184 | // Imports are always put at the top of the file, just after any module 185 | // comments and docstrings, and before module globals and constants. 186 | // 187 | // Imports should be grouped in the following order: 188 | // 189 | // Standard library imports. 190 | // Related third party imports. 191 | // Local application/library specific imports. 192 | // 193 | // You should put a blank line between each group of imports. 194 | // 195 | // https://www.python.org/dev/peps/pep-0008/#imports 196 | message ImportGroup 197 | { 198 | repeated Node imports = 1 [json_name="imports"]; 199 | } 200 | 201 | message Is 202 | { 203 | } 204 | 205 | message Keyword 206 | { 207 | string arg = 1 [json_name="arg"]; 208 | Node value = 2 [json_name="value"]; 209 | } 210 | 211 | message Module 212 | { 213 | repeated Node body = 1 [json_name="body"]; 214 | } 215 | 216 | message Name 217 | { 218 | string id = 1 [json_name="id"]; 219 | } 220 | 221 | message Pass 222 | { 223 | } 224 | 225 | message Return 226 | { 227 | Node value = 1 [json_name="value"]; 228 | } 229 | 230 | 231 | message Subscript 232 | { 233 | Name value = 1 [json_name="value"]; 234 | Node slice = 2 [json_name="slice"]; 235 | } 236 | 237 | message Yield 238 | { 239 | Node value = 1 [json_name="value"]; 240 | } 241 | 242 | 243 | -------------------------------------------------------------------------------- /protos/buf.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | name: github.com/sqlc-dev/sqlc-gen-python 3 | breaking: 4 | use: 5 | - FILE 6 | lint: 7 | use: 8 | - DEFAULT 9 | except: 10 | - PACKAGE_VERSION_SUFFIX 11 | --------------------------------------------------------------------------------