From ac6bf258ffb954e01f8a17de338b6e34f735ecf5 Mon Sep 17 00:00:00 2001 From: Devin Stein Date: Mon, 16 Dec 2024 10:16:13 -0600 Subject: [PATCH 1/6] feat: add emit_str_enum config option --- internal/config.go | 1 + internal/gen.go | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/internal/config.go b/internal/config.go index 009cb04..1a8a565 100644 --- a/internal/config.go +++ b/internal/config.go @@ -7,6 +7,7 @@ type Config struct { Package string `json:"package"` Out string `json:"out"` EmitPydanticModels bool `json:"emit_pydantic_models"` + EmitStrEnum bool `json:"emit_str_enum"` QueryParameterLimit *int32 `json:"query_parameter_limit"` InflectionExcludeTableNames []string `json:"inflection_exclude_table_names"` } diff --git a/internal/gen.go b/internal/gen.go index f81c53b..6e50fae 100644 --- a/internal/gen.go +++ b/internal/gen.go @@ -681,12 +681,19 @@ func buildModelsTree(ctx *pyTmplCtx, i *importer) *pyast.Node { mod.Body = append(mod.Body, buildImportGroup(std), buildImportGroup(pkg)) for _, e := range ctx.Enums { + bases := []*pyast.Node{ + poet.Name("str"), + poet.Attribute(poet.Name("enum"), "Enum"), + } + if i.C.EmitStrEnum { + // override the bases to emit enum.StrEnum (only support in Python >=3.11) + bases = []*pyast.Node{ + poet.Attribute(poet.Name("enum"), "StrEnum"), + } + } def := &pyast.ClassDef{ - Name: e.Name, - Bases: []*pyast.Node{ - poet.Name("str"), - poet.Attribute(poet.Name("enum"), "Enum"), - }, + Name: e.Name, + Bases: bases, } if e.Comment != "" { def.Body = append(def.Body, &pyast.Node{ From 1f7fd85b4363bf77915fc014f31f9c60fc3131e8 Mon Sep 17 00:00:00 2001 From: Devin Stein Date: Fri, 3 Jan 2025 16:10:03 -0800 Subject: [PATCH 2/6] docs: add docs for emit_str_enum --- README.md | 48 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d7e0d75..5420823 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,39 @@ ## Usage ```yaml -version: '2' +version: "2" plugins: -- name: py - wasm: - url: https://downloads.sqlc.dev/plugin/sqlc-gen-python_1.2.0.wasm - sha256: a6c5d174c407007c3717eea36ff0882744346e6ba991f92f71d6ab2895204c0e + - name: py + wasm: + url: https://downloads.sqlc.dev/plugin/sqlc-gen-python_1.2.0.wasm + sha256: a6c5d174c407007c3717eea36ff0882744346e6ba991f92f71d6ab2895204c0e sql: -- schema: "schema.sql" - queries: "query.sql" - engine: postgresql - codegen: - - out: src/authors - plugin: py - options: - package: authors - emit_sync_querier: true - emit_async_querier: true + - schema: "schema.sql" + queries: "query.sql" + engine: postgresql + codegen: + - out: src/authors + plugin: py + options: + package: authors + emit_sync_querier: true + emit_async_querier: true ``` + +### Emit Pydantic Models instead of `dataclasses` + +Option: `emit_pydantic_models` + +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. + +### Use `enum.StrEnum` for Enums + +Option: `emit_str_enum` + +`enum.StrEnum` was introduce in Python 3.11. + +`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. + +This is convenient for type checking and validation, as well as for serialization and deserialization. + +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. From 8c3af5750738df96d134c685d9b2f942e16ef5a8 Mon Sep 17 00:00:00 2001 From: Devin Stein Date: Fri, 3 Jan 2025 16:10:47 -0800 Subject: [PATCH 3/6] tests(emit_str_enum): add end to end test for emit str enum --- .../testdata/emit_str_enum/db/models.py | 19 +++ .../testdata/emit_str_enum/db/query.py | 111 ++++++++++++++++++ .../endtoend/testdata/emit_str_enum/query.sql | 18 +++ .../testdata/emit_str_enum/schema.sql | 8 ++ .../endtoend/testdata/emit_str_enum/sqlc.yaml | 19 +++ 5 files changed, 175 insertions(+) create mode 100644 internal/endtoend/testdata/emit_str_enum/db/models.py create mode 100644 internal/endtoend/testdata/emit_str_enum/db/query.py create mode 100644 internal/endtoend/testdata/emit_str_enum/query.sql create mode 100644 internal/endtoend/testdata/emit_str_enum/schema.sql create mode 100644 internal/endtoend/testdata/emit_str_enum/sqlc.yaml diff --git a/internal/endtoend/testdata/emit_str_enum/db/models.py b/internal/endtoend/testdata/emit_str_enum/db/models.py new file mode 100644 index 0000000..148a257 --- /dev/null +++ b/internal/endtoend/testdata/emit_str_enum/db/models.py @@ -0,0 +1,19 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.27.0 +import dataclasses +import enum +from typing import Optional + + +class BookStatus(enum.StrEnum): + AVAILABLE = "available" + CHECKED_OUT = "checked_out" + OVERDUE = "overdue" + + +@dataclasses.dataclass() +class Book: + id: int + title: str + status: Optional[BookStatus] diff --git a/internal/endtoend/testdata/emit_str_enum/db/query.py b/internal/endtoend/testdata/emit_str_enum/db/query.py new file mode 100644 index 0000000..c6a846d --- /dev/null +++ b/internal/endtoend/testdata/emit_str_enum/db/query.py @@ -0,0 +1,111 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.27.0 +# source: query.sql +from typing import AsyncIterator, Iterator, Optional + +import sqlalchemy +import sqlalchemy.ext.asyncio + +from db import models + + +CREATE_BOOK = """-- name: create_book \\:one +INSERT INTO books ( + title, status +) VALUES ( + :p1, :p2 +) RETURNING id, title, status +""" + + +DELETE_BOOK = """-- name: delete_book \\:exec +DELETE FROM books +WHERE id = :p1 +""" + + +GET_BOOK = """-- name: get_book \\:one +SELECT id, title, status FROM books +WHERE id = :p1 LIMIT 1 +""" + + +LIST_BOOKS = """-- name: list_books \\:many +SELECT id, title, status FROM books +ORDER BY title +""" + + +class Querier: + def __init__(self, conn: sqlalchemy.engine.Connection): + self._conn = conn + + def create_book(self, *, title: str, status: Optional[models.BookStatus]) -> Optional[models.Book]: + row = self._conn.execute(sqlalchemy.text(CREATE_BOOK), {"p1": title, "p2": status}).first() + if row is None: + return None + return models.Book( + id=row[0], + title=row[1], + status=row[2], + ) + + def delete_book(self, *, id: int) -> None: + self._conn.execute(sqlalchemy.text(DELETE_BOOK), {"p1": id}) + + def get_book(self, *, id: int) -> Optional[models.Book]: + row = self._conn.execute(sqlalchemy.text(GET_BOOK), {"p1": id}).first() + if row is None: + return None + return models.Book( + id=row[0], + title=row[1], + status=row[2], + ) + + def list_books(self) -> Iterator[models.Book]: + result = self._conn.execute(sqlalchemy.text(LIST_BOOKS)) + for row in result: + yield models.Book( + id=row[0], + title=row[1], + status=row[2], + ) + + +class AsyncQuerier: + def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): + self._conn = conn + + async def create_book(self, *, title: str, status: Optional[models.BookStatus]) -> Optional[models.Book]: + row = (await self._conn.execute(sqlalchemy.text(CREATE_BOOK), {"p1": title, "p2": status})).first() + if row is None: + return None + return models.Book( + id=row[0], + title=row[1], + status=row[2], + ) + + async def delete_book(self, *, id: int) -> None: + await self._conn.execute(sqlalchemy.text(DELETE_BOOK), {"p1": id}) + + async def get_book(self, *, id: int) -> Optional[models.Book]: + row = (await self._conn.execute(sqlalchemy.text(GET_BOOK), {"p1": id})).first() + if row is None: + return None + return models.Book( + id=row[0], + title=row[1], + status=row[2], + ) + + async def list_books(self) -> AsyncIterator[models.Book]: + result = await self._conn.stream(sqlalchemy.text(LIST_BOOKS)) + async for row in result: + yield models.Book( + id=row[0], + title=row[1], + status=row[2], + ) diff --git a/internal/endtoend/testdata/emit_str_enum/query.sql b/internal/endtoend/testdata/emit_str_enum/query.sql new file mode 100644 index 0000000..61142f7 --- /dev/null +++ b/internal/endtoend/testdata/emit_str_enum/query.sql @@ -0,0 +1,18 @@ +-- name: GetBook :one +SELECT * FROM books +WHERE id = $1 LIMIT 1; + +-- name: ListBooks :many +SELECT * FROM books +ORDER BY title; + +-- name: CreateBook :one +INSERT INTO books ( + title, status +) VALUES ( + $1, $2 +) RETURNING *; + +-- name: DeleteBook :exec +DELETE FROM books +WHERE id = $1; diff --git a/internal/endtoend/testdata/emit_str_enum/schema.sql b/internal/endtoend/testdata/emit_str_enum/schema.sql new file mode 100644 index 0000000..6842b27 --- /dev/null +++ b/internal/endtoend/testdata/emit_str_enum/schema.sql @@ -0,0 +1,8 @@ +CREATE TYPE book_status AS ENUM ('available', 'checked_out', 'overdue'); + + +CREATE TABLE books ( + id BIGSERIAL PRIMARY KEY, + title text NOT NULL, + status book_status DEFAULT 'available' +); diff --git a/internal/endtoend/testdata/emit_str_enum/sqlc.yaml b/internal/endtoend/testdata/emit_str_enum/sqlc.yaml new file mode 100644 index 0000000..04e3feb --- /dev/null +++ b/internal/endtoend/testdata/emit_str_enum/sqlc.yaml @@ -0,0 +1,19 @@ +version: "2" +plugins: + - name: py + wasm: + url: file://../../../../bin/sqlc-gen-python.wasm + sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" +sql: + - schema: schema.sql + queries: query.sql + engine: postgresql + codegen: + - plugin: py + out: db + options: + package: db + emit_sync_querier: true + emit_async_querier: true + emit_str_enum: true + From eef4c982a476762306a6d60394b5c0ca47f2efc9 Mon Sep 17 00:00:00 2001 From: Devin Stein Date: Fri, 3 Jan 2025 16:11:07 -0800 Subject: [PATCH 4/6] chore(tests): update wasm sha --- internal/endtoend/testdata/emit_pydantic_models/sqlc.yaml | 2 +- internal/endtoend/testdata/exec_result/sqlc.yaml | 2 +- internal/endtoend/testdata/exec_rows/sqlc.yaml | 2 +- .../endtoend/testdata/inflection_exclude_table_names/sqlc.yaml | 2 +- internal/endtoend/testdata/query_parameter_limit_two/sqlc.yaml | 2 +- .../endtoend/testdata/query_parameter_limit_undefined/sqlc.yaml | 2 +- internal/endtoend/testdata/query_parameter_limit_zero/sqlc.yaml | 2 +- internal/endtoend/testdata/query_parameter_no_limit/sqlc.yaml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/endtoend/testdata/emit_pydantic_models/sqlc.yaml b/internal/endtoend/testdata/emit_pydantic_models/sqlc.yaml index 180ce29..beae200 100644 --- a/internal/endtoend/testdata/emit_pydantic_models/sqlc.yaml +++ b/internal/endtoend/testdata/emit_pydantic_models/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: py wasm: url: file://../../../../bin/sqlc-gen-python.wasm - sha256: "a6c5d174c407007c3717eea36ff0882744346e6ba991f92f71d6ab2895204c0e" + sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" sql: - schema: schema.sql queries: query.sql diff --git a/internal/endtoend/testdata/exec_result/sqlc.yaml b/internal/endtoend/testdata/exec_result/sqlc.yaml index 2adbd31..ddffc83 100644 --- a/internal/endtoend/testdata/exec_result/sqlc.yaml +++ b/internal/endtoend/testdata/exec_result/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: py wasm: url: file://../../../../bin/sqlc-gen-python.wasm - sha256: "a6c5d174c407007c3717eea36ff0882744346e6ba991f92f71d6ab2895204c0e" + sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" sql: - schema: schema.sql queries: query.sql diff --git a/internal/endtoend/testdata/exec_rows/sqlc.yaml b/internal/endtoend/testdata/exec_rows/sqlc.yaml index 2adbd31..ddffc83 100644 --- a/internal/endtoend/testdata/exec_rows/sqlc.yaml +++ b/internal/endtoend/testdata/exec_rows/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: py wasm: url: file://../../../../bin/sqlc-gen-python.wasm - sha256: "a6c5d174c407007c3717eea36ff0882744346e6ba991f92f71d6ab2895204c0e" + sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" sql: - schema: schema.sql queries: query.sql diff --git a/internal/endtoend/testdata/inflection_exclude_table_names/sqlc.yaml b/internal/endtoend/testdata/inflection_exclude_table_names/sqlc.yaml index aba5400..efbb150 100644 --- a/internal/endtoend/testdata/inflection_exclude_table_names/sqlc.yaml +++ b/internal/endtoend/testdata/inflection_exclude_table_names/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: py wasm: url: file://../../../../bin/sqlc-gen-python.wasm - sha256: "a6c5d174c407007c3717eea36ff0882744346e6ba991f92f71d6ab2895204c0e" + sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" sql: - schema: schema.sql queries: query.sql diff --git a/internal/endtoend/testdata/query_parameter_limit_two/sqlc.yaml b/internal/endtoend/testdata/query_parameter_limit_two/sqlc.yaml index e389988..336bca7 100644 --- a/internal/endtoend/testdata/query_parameter_limit_two/sqlc.yaml +++ b/internal/endtoend/testdata/query_parameter_limit_two/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: py wasm: url: file://../../../../bin/sqlc-gen-python.wasm - sha256: "a6c5d174c407007c3717eea36ff0882744346e6ba991f92f71d6ab2895204c0e" + sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" sql: - schema: schema.sql queries: query.sql diff --git a/internal/endtoend/testdata/query_parameter_limit_undefined/sqlc.yaml b/internal/endtoend/testdata/query_parameter_limit_undefined/sqlc.yaml index 66d7a14..c20cd57 100644 --- a/internal/endtoend/testdata/query_parameter_limit_undefined/sqlc.yaml +++ b/internal/endtoend/testdata/query_parameter_limit_undefined/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: py wasm: url: file://../../../../bin/sqlc-gen-python.wasm - sha256: "a6c5d174c407007c3717eea36ff0882744346e6ba991f92f71d6ab2895204c0e" + sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" sql: - schema: schema.sql queries: query.sql diff --git a/internal/endtoend/testdata/query_parameter_limit_zero/sqlc.yaml b/internal/endtoend/testdata/query_parameter_limit_zero/sqlc.yaml index 274f730..6e2cdeb 100644 --- a/internal/endtoend/testdata/query_parameter_limit_zero/sqlc.yaml +++ b/internal/endtoend/testdata/query_parameter_limit_zero/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: py wasm: url: file://../../../../bin/sqlc-gen-python.wasm - sha256: "a6c5d174c407007c3717eea36ff0882744346e6ba991f92f71d6ab2895204c0e" + sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" sql: - schema: schema.sql queries: query.sql diff --git a/internal/endtoend/testdata/query_parameter_no_limit/sqlc.yaml b/internal/endtoend/testdata/query_parameter_no_limit/sqlc.yaml index b563730..c432e4f 100644 --- a/internal/endtoend/testdata/query_parameter_no_limit/sqlc.yaml +++ b/internal/endtoend/testdata/query_parameter_no_limit/sqlc.yaml @@ -3,7 +3,7 @@ plugins: - name: py wasm: url: file://../../../../bin/sqlc-gen-python.wasm - sha256: "a6c5d174c407007c3717eea36ff0882744346e6ba991f92f71d6ab2895204c0e" + sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca" sql: - schema: schema.sql queries: query.sql From 397fe5bdc2a15bc6e80208fc5e95a1d7ae49a170 Mon Sep 17 00:00:00 2001 From: Devin Stein Date: Fri, 3 Jan 2025 16:13:24 -0800 Subject: [PATCH 5/6] docs: add examples of with w/o --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index 5420823..6d09868 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,27 @@ Option: `emit_pydantic_models` 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. +with `emit_pydantic_models` + +```py +from pydantic import BaseModel + +class Author(pydantic.BaseModel): + id: int + name: str +``` + +without `emit_pydantic_models` + +```py +from dataclasses import dataclasses + +@dataclasses +class Author: + id: int + name: str +``` + ### Use `enum.StrEnum` for Enums Option: `emit_str_enum` @@ -37,3 +58,21 @@ Option: `emit_str_enum` This is convenient for type checking and validation, as well as for serialization and deserialization. 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. + +with `emit_str_enum` + +```py +class Status(enum.StrEnum): + """Venues can be either open or closed""" + OPEN = "op!en" + CLOSED = "clo@sed" +``` + +without `emit_str_enum` (current behavior) + +```py +class Status(str, enum.Enum): + """Venues can be either open or closed""" + OPEN = "op!en" + CLOSED = "clo@sed" +``` From 1684bf5f7986e32d03d43422d3af5b0b1156f072 Mon Sep 17 00:00:00 2001 From: Devin Stein Date: Fri, 3 Jan 2025 16:14:28 -0800 Subject: [PATCH 6/6] docs: update to use correct dataclass syntax --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6d09868..5d53009 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ class Author(pydantic.BaseModel): without `emit_pydantic_models` ```py -from dataclasses import dataclasses +import dataclasses -@dataclasses +@dataclasses.dataclass() class Author: id: int name: str