diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2779fd5..365479d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,22 @@ -name: make +name: go on: push: branches: - main pull_request: jobs: - build: + test: name: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: - go-version: '1.21.0' + go-version: '1.21' + - uses: sqlc-dev/setup-sqlc@v4 + with: + sqlc-version: '1.23.0' - run: make + - run: make test + - run: sqlc diff + working-directory: examples \ No newline at end of file diff --git a/.github/workflows/db.yml b/.github/workflows/db.yml new file mode 100644 index 0000000..8b10271 --- /dev/null +++ b/.github/workflows/db.yml @@ -0,0 +1,44 @@ +name: db +on: + push: + branches: + - main + pull_request: +jobs: + + build: + name: test + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:11 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + # needed because the postgres container does not provide a healthcheck + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: 3.9 + - name: Install python dependencies + working-directory: ./examples + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + - name: Test python code + working-directory: ./examples + env: + PG_USER: postgres + PG_HOST: localhost + PG_DATABASE: postgres + PG_PASSWORD: postgres + PG_PORT: ${{ job.services.postgres.ports['5432'] }} + run: | + pytest src/tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5e82d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bin \ No newline at end of file diff --git a/Makefile b/Makefile index 9a262fc..d8ac60b 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,18 @@ -all: sqlc-gen-python sqlc-gen-python.wasm +.PHONY: build test -sqlc-gen-python: - cd plugin && go build -o ~/bin/sqlc-gen-python ./main.go +build: + go build ./... -sqlc-gen-python.wasm: - cd plugin && GOOS=wasip1 GOARCH=wasm go build -o sqlc-gen-python.wasm main.go - openssl sha256 plugin/sqlc-gen-python.wasm +test: bin/sqlc-gen-python.wasm + go test ./... + +all: bin/sqlc-gen-python bin/sqlc-gen-python.wasm + +bin/sqlc-gen-python: bin go.mod go.sum $(wildcard **/*.go) + cd plugin && go build -o ../bin/sqlc-gen-python ./main.go + +bin/sqlc-gen-python.wasm: bin/sqlc-gen-python + cd plugin && GOOS=wasip1 GOARCH=wasm go build -o ../bin/sqlc-gen-python.wasm main.go + +bin: + mkdir -p bin diff --git a/examples/requirements.txt b/examples/requirements.txt new file mode 100644 index 0000000..26f645a --- /dev/null +++ b/examples/requirements.txt @@ -0,0 +1,5 @@ +pytest~=6.2.2 +pytest-asyncio~=0.14.0 +psycopg2-binary~=2.8.6 +asyncpg~=0.21.0 +sqlalchemy==1.4.0 diff --git a/examples/sqlc.yaml b/examples/sqlc.yaml new file mode 100644 index 0000000..36a9b9e --- /dev/null +++ b/examples/sqlc.yaml @@ -0,0 +1,48 @@ +version: '2' +plugins: +- name: py + wasm: + url: https://downloads.sqlc.dev/plugin/sqlc-gen-python_1.1.0.wasm + sha256: ef58f143a8c116781091441770c7166caaf361dd645f62b8f05f462e9f95c3b2 +sql: +- schema: "src/authors/schema.sql" + queries: "src/authors/query.sql" + engine: postgresql + codegen: + - out: src/authors + plugin: py + options: + package: authors + emit_sync_querier: true + emit_async_querier: true + query_parameter_limit: 5 +- schema: "src/booktest/schema.sql" + queries: "src/booktest/query.sql" + engine: postgresql + codegen: + - out: src/booktest + plugin: py + options: + package: booktest + emit_async_querier: true + query_parameter_limit: 5 +- schema: "src/jets/schema.sql" + queries: "src/jets/query-building.sql" + engine: postgresql + codegen: + - out: src/jets + plugin: py + options: + package: jets + emit_async_querier: true + query_parameter_limit: 5 +- schema: "src/ondeck/schema" + queries: "src/ondeck/query" + engine: postgresql + codegen: + - out: src/ondeck + plugin: py + options: + package: ondeck + emit_async_querier: true + query_parameter_limit: 5 diff --git a/examples/src/authors/__init__.py b/examples/src/authors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/src/authors/models.py b/examples/src/authors/models.py new file mode 100644 index 0000000..a8d25d0 --- /dev/null +++ b/examples/src/authors/models.py @@ -0,0 +1,12 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +import dataclasses +from typing import Optional + + +@dataclasses.dataclass() +class Author: + id: int + name: str + bio: Optional[str] diff --git a/examples/src/authors/query.py b/examples/src/authors/query.py new file mode 100644 index 0000000..58eccb9 --- /dev/null +++ b/examples/src/authors/query.py @@ -0,0 +1,112 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +# source: query.sql +from typing import AsyncIterator, Iterator, Optional + +import sqlalchemy +import sqlalchemy.ext.asyncio + +from authors import models + + +CREATE_AUTHOR = """-- name: create_author \\:one +INSERT INTO authors ( + name, bio +) VALUES ( + :p1, :p2 +) +RETURNING id, name, bio +""" + + +DELETE_AUTHOR = """-- name: delete_author \\:exec +DELETE FROM authors +WHERE id = :p1 +""" + + +GET_AUTHOR = """-- name: get_author \\:one +SELECT id, name, bio FROM authors +WHERE id = :p1 LIMIT 1 +""" + + +LIST_AUTHORS = """-- name: list_authors \\:many +SELECT id, name, bio FROM authors +ORDER BY name +""" + + +class Querier: + def __init__(self, conn: sqlalchemy.engine.Connection): + self._conn = conn + + def create_author(self, *, name: str, bio: Optional[str]) -> Optional[models.Author]: + row = self._conn.execute(sqlalchemy.text(CREATE_AUTHOR), {"p1": name, "p2": bio}).first() + if row is None: + return None + return models.Author( + id=row[0], + name=row[1], + bio=row[2], + ) + + def delete_author(self, *, id: int) -> None: + self._conn.execute(sqlalchemy.text(DELETE_AUTHOR), {"p1": id}) + + def get_author(self, *, id: int) -> Optional[models.Author]: + row = self._conn.execute(sqlalchemy.text(GET_AUTHOR), {"p1": id}).first() + if row is None: + return None + return models.Author( + id=row[0], + name=row[1], + bio=row[2], + ) + + def list_authors(self) -> Iterator[models.Author]: + result = self._conn.execute(sqlalchemy.text(LIST_AUTHORS)) + for row in result: + yield models.Author( + id=row[0], + name=row[1], + bio=row[2], + ) + + +class AsyncQuerier: + def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): + self._conn = conn + + async def create_author(self, *, name: str, bio: Optional[str]) -> Optional[models.Author]: + row = (await self._conn.execute(sqlalchemy.text(CREATE_AUTHOR), {"p1": name, "p2": bio})).first() + if row is None: + return None + return models.Author( + id=row[0], + name=row[1], + bio=row[2], + ) + + async def delete_author(self, *, id: int) -> None: + await self._conn.execute(sqlalchemy.text(DELETE_AUTHOR), {"p1": id}) + + async def get_author(self, *, id: int) -> Optional[models.Author]: + row = (await self._conn.execute(sqlalchemy.text(GET_AUTHOR), {"p1": id})).first() + if row is None: + return None + return models.Author( + id=row[0], + name=row[1], + bio=row[2], + ) + + async def list_authors(self) -> AsyncIterator[models.Author]: + result = await self._conn.stream(sqlalchemy.text(LIST_AUTHORS)) + async for row in result: + yield models.Author( + id=row[0], + name=row[1], + bio=row[2], + ) diff --git a/examples/src/authors/query.sql b/examples/src/authors/query.sql new file mode 100644 index 0000000..75e38b2 --- /dev/null +++ b/examples/src/authors/query.sql @@ -0,0 +1,19 @@ +-- name: GetAuthor :one +SELECT * FROM authors +WHERE id = $1 LIMIT 1; + +-- name: ListAuthors :many +SELECT * FROM authors +ORDER BY name; + +-- name: CreateAuthor :one +INSERT INTO authors ( + name, bio +) VALUES ( + $1, $2 +) +RETURNING *; + +-- name: DeleteAuthor :exec +DELETE FROM authors +WHERE id = $1; diff --git a/examples/src/authors/schema.sql b/examples/src/authors/schema.sql new file mode 100644 index 0000000..b4fad78 --- /dev/null +++ b/examples/src/authors/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL, + bio text +); diff --git a/examples/src/booktest/__init__.py b/examples/src/booktest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/src/booktest/models.py b/examples/src/booktest/models.py new file mode 100644 index 0000000..712f166 --- /dev/null +++ b/examples/src/booktest/models.py @@ -0,0 +1,30 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +import dataclasses +import datetime +import enum +from typing import List + + +class BookType(str, enum.Enum): + FICTION = "FICTION" + NONFICTION = "NONFICTION" + + +@dataclasses.dataclass() +class Author: + author_id: int + name: str + + +@dataclasses.dataclass() +class Book: + book_id: int + author_id: int + isbn: str + book_type: BookType + title: str + year: int + available: datetime.datetime + tags: List[str] diff --git a/examples/src/booktest/query.py b/examples/src/booktest/query.py new file mode 100644 index 0000000..94b8565 --- /dev/null +++ b/examples/src/booktest/query.py @@ -0,0 +1,211 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +# source: query.sql +import dataclasses +import datetime +from typing import AsyncIterator, List, Optional + +import sqlalchemy +import sqlalchemy.ext.asyncio + +from booktest import models + + +BOOKS_BY_TAGS = """-- name: books_by_tags \\:many +SELECT + book_id, + title, + name, + isbn, + tags +FROM books +LEFT JOIN authors ON books.author_id = authors.author_id +WHERE tags && :p1\\:\\:varchar[] +""" + + +@dataclasses.dataclass() +class BooksByTagsRow: + book_id: int + title: str + name: Optional[str] + isbn: str + tags: List[str] + + +BOOKS_BY_TITLE_YEAR = """-- name: books_by_title_year \\:many +SELECT book_id, author_id, isbn, book_type, title, year, available, tags FROM books +WHERE title = :p1 AND year = :p2 +""" + + +CREATE_AUTHOR = """-- name: create_author \\:one +INSERT INTO authors (name) VALUES (:p1) +RETURNING author_id, name +""" + + +CREATE_BOOK = """-- name: create_book \\:one +INSERT INTO books ( + author_id, + isbn, + book_type, + title, + year, + available, + tags +) VALUES ( + :p1, + :p2, + :p3, + :p4, + :p5, + :p6, + :p7 +) +RETURNING book_id, author_id, isbn, book_type, title, year, available, tags +""" + + +@dataclasses.dataclass() +class CreateBookParams: + author_id: int + isbn: str + book_type: models.BookType + title: str + year: int + available: datetime.datetime + tags: List[str] + + +DELETE_BOOK = """-- name: delete_book \\:exec +DELETE FROM books +WHERE book_id = :p1 +""" + + +GET_AUTHOR = """-- name: get_author \\:one +SELECT author_id, name FROM authors +WHERE author_id = :p1 +""" + + +GET_BOOK = """-- name: get_book \\:one +SELECT book_id, author_id, isbn, book_type, title, year, available, tags FROM books +WHERE book_id = :p1 +""" + + +UPDATE_BOOK = """-- name: update_book \\:exec +UPDATE books +SET title = :p1, tags = :p2 +WHERE book_id = :p3 +""" + + +UPDATE_BOOK_ISBN = """-- name: update_book_isbn \\:exec +UPDATE books +SET title = :p1, tags = :p2, isbn = :p4 +WHERE book_id = :p3 +""" + + +class AsyncQuerier: + def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): + self._conn = conn + + async def books_by_tags(self, *, dollar_1: List[str]) -> AsyncIterator[BooksByTagsRow]: + result = await self._conn.stream(sqlalchemy.text(BOOKS_BY_TAGS), {"p1": dollar_1}) + async for row in result: + yield BooksByTagsRow( + book_id=row[0], + title=row[1], + name=row[2], + isbn=row[3], + tags=row[4], + ) + + async def books_by_title_year(self, *, title: str, year: int) -> AsyncIterator[models.Book]: + result = await self._conn.stream(sqlalchemy.text(BOOKS_BY_TITLE_YEAR), {"p1": title, "p2": year}) + async for row in result: + yield models.Book( + book_id=row[0], + author_id=row[1], + isbn=row[2], + book_type=row[3], + title=row[4], + year=row[5], + available=row[6], + tags=row[7], + ) + + async def create_author(self, *, name: str) -> Optional[models.Author]: + row = (await self._conn.execute(sqlalchemy.text(CREATE_AUTHOR), {"p1": name})).first() + if row is None: + return None + return models.Author( + author_id=row[0], + name=row[1], + ) + + async def create_book(self, arg: CreateBookParams) -> Optional[models.Book]: + row = (await self._conn.execute(sqlalchemy.text(CREATE_BOOK), { + "p1": arg.author_id, + "p2": arg.isbn, + "p3": arg.book_type, + "p4": arg.title, + "p5": arg.year, + "p6": arg.available, + "p7": arg.tags, + })).first() + if row is None: + return None + return models.Book( + book_id=row[0], + author_id=row[1], + isbn=row[2], + book_type=row[3], + title=row[4], + year=row[5], + available=row[6], + tags=row[7], + ) + + async def delete_book(self, *, book_id: int) -> None: + await self._conn.execute(sqlalchemy.text(DELETE_BOOK), {"p1": book_id}) + + async def get_author(self, *, author_id: int) -> Optional[models.Author]: + row = (await self._conn.execute(sqlalchemy.text(GET_AUTHOR), {"p1": author_id})).first() + if row is None: + return None + return models.Author( + author_id=row[0], + name=row[1], + ) + + async def get_book(self, *, book_id: int) -> Optional[models.Book]: + row = (await self._conn.execute(sqlalchemy.text(GET_BOOK), {"p1": book_id})).first() + if row is None: + return None + return models.Book( + book_id=row[0], + author_id=row[1], + isbn=row[2], + book_type=row[3], + title=row[4], + year=row[5], + available=row[6], + tags=row[7], + ) + + async def update_book(self, *, title: str, tags: List[str], book_id: int) -> None: + await self._conn.execute(sqlalchemy.text(UPDATE_BOOK), {"p1": title, "p2": tags, "p3": book_id}) + + async def update_book_isbn(self, *, title: str, tags: List[str], book_id: int, isbn: str) -> None: + await self._conn.execute(sqlalchemy.text(UPDATE_BOOK_ISBN), { + "p1": title, + "p2": tags, + "p3": book_id, + "p4": isbn, + }) diff --git a/examples/src/booktest/query.sql b/examples/src/booktest/query.sql new file mode 100644 index 0000000..194897a --- /dev/null +++ b/examples/src/booktest/query.sql @@ -0,0 +1,60 @@ +-- name: GetAuthor :one +SELECT * FROM authors +WHERE author_id = $1; + +-- name: GetBook :one +SELECT * FROM books +WHERE book_id = $1; + +-- name: DeleteBook :exec +DELETE FROM books +WHERE book_id = $1; + +-- name: BooksByTitleYear :many +SELECT * FROM books +WHERE title = $1 AND year = $2; + +-- name: BooksByTags :many +SELECT + book_id, + title, + name, + isbn, + tags +FROM books +LEFT JOIN authors ON books.author_id = authors.author_id +WHERE tags && $1::varchar[]; + +-- name: CreateAuthor :one +INSERT INTO authors (name) VALUES ($1) +RETURNING *; + +-- name: CreateBook :one +INSERT INTO books ( + author_id, + isbn, + book_type, + title, + year, + available, + tags +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7 +) +RETURNING *; + +-- name: UpdateBook :exec +UPDATE books +SET title = $1, tags = $2 +WHERE book_id = $3; + +-- name: UpdateBookISBN :exec +UPDATE books +SET title = $1, tags = $2, isbn = $4 +WHERE book_id = $3; diff --git a/examples/src/booktest/schema.sql b/examples/src/booktest/schema.sql new file mode 100644 index 0000000..2beecab --- /dev/null +++ b/examples/src/booktest/schema.sql @@ -0,0 +1,32 @@ +CREATE TABLE authors ( + author_id SERIAL PRIMARY KEY, + name text NOT NULL DEFAULT '' +); + +CREATE INDEX authors_name_idx ON authors(name); + +CREATE TYPE book_type AS ENUM ( + 'FICTION', + 'NONFICTION' +); + +CREATE TABLE books ( + book_id SERIAL PRIMARY KEY, + author_id integer NOT NULL REFERENCES authors(author_id), + isbn text NOT NULL DEFAULT '' UNIQUE, + book_type book_type NOT NULL DEFAULT 'FICTION', + title text NOT NULL DEFAULT '', + year integer NOT NULL DEFAULT 2000, + available timestamp with time zone NOT NULL DEFAULT 'NOW()', + tags varchar[] NOT NULL DEFAULT '{}' +); + +CREATE INDEX books_title_idx ON books(title, year); + +CREATE FUNCTION say_hello(text) RETURNS text AS $$ +BEGIN + RETURN CONCAT('hello ', $1); +END; +$$ LANGUAGE plpgsql; + +CREATE INDEX books_title_lower_idx ON books(title); diff --git a/examples/src/dbtest/__init__.py b/examples/src/dbtest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/src/dbtest/migrations.py b/examples/src/dbtest/migrations.py new file mode 100644 index 0000000..6ace6bc --- /dev/null +++ b/examples/src/dbtest/migrations.py @@ -0,0 +1,43 @@ +import os +from typing import List + +import sqlalchemy +import sqlalchemy.ext.asyncio + + +def apply_migrations(conn: sqlalchemy.engine.Connection, paths: List[str]): + files = _find_sql_files(paths) + + for file in files: + with open(file, "r") as fd: + blob = fd.read() + stmts = blob.split(";") + for stmt in stmts: + if stmt.strip(): + conn.execute(sqlalchemy.text(stmt)) + + +async def apply_migrations_async(conn: sqlalchemy.ext.asyncio.AsyncConnection, paths: List[str]): + files = _find_sql_files(paths) + + for file in files: + with open(file, "r") as fd: + blob = fd.read() + raw_conn = await conn.get_raw_connection() + # The asyncpg sqlalchemy adapter uses a prepared statement cache which can't handle the migration statements + await raw_conn._connection.execute(blob) + + +def _find_sql_files(paths: List[str]) -> List[str]: + files = [] + for path in paths: + if not os.path.exists(path): + raise FileNotFoundError(f"{path} does not exist") + if os.path.isdir(path): + for file in os.listdir(path): + if file.endswith(".sql"): + files.append(os.path.join(path, file)) + else: + files.append(path) + files.sort() + return files diff --git a/examples/src/jets/__init__.py b/examples/src/jets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/src/jets/models.py b/examples/src/jets/models.py new file mode 100644 index 0000000..7d3063a --- /dev/null +++ b/examples/src/jets/models.py @@ -0,0 +1,31 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +import dataclasses + + +@dataclasses.dataclass() +class Jet: + id: int + pilot_id: int + age: int + name: str + color: str + + +@dataclasses.dataclass() +class Language: + id: int + language: str + + +@dataclasses.dataclass() +class Pilot: + id: int + name: str + + +@dataclasses.dataclass() +class PilotLanguage: + pilot_id: int + language_id: int diff --git a/examples/src/jets/query-building.py b/examples/src/jets/query-building.py new file mode 100644 index 0000000..6fe42df --- /dev/null +++ b/examples/src/jets/query-building.py @@ -0,0 +1,47 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +# source: query-building.sql +from typing import AsyncIterator, Optional + +import sqlalchemy +import sqlalchemy.ext.asyncio + +from jets import models + + +COUNT_PILOTS = """-- name: count_pilots \\:one +SELECT COUNT(*) FROM pilots +""" + + +DELETE_PILOT = """-- name: delete_pilot \\:exec +DELETE FROM pilots WHERE id = :p1 +""" + + +LIST_PILOTS = """-- name: list_pilots \\:many +SELECT id, name FROM pilots LIMIT 5 +""" + + +class AsyncQuerier: + def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): + self._conn = conn + + async def count_pilots(self) -> Optional[int]: + row = (await self._conn.execute(sqlalchemy.text(COUNT_PILOTS))).first() + if row is None: + return None + return row[0] + + async def delete_pilot(self, *, id: int) -> None: + await self._conn.execute(sqlalchemy.text(DELETE_PILOT), {"p1": id}) + + async def list_pilots(self) -> AsyncIterator[models.Pilot]: + result = await self._conn.stream(sqlalchemy.text(LIST_PILOTS)) + async for row in result: + yield models.Pilot( + id=row[0], + name=row[1], + ) diff --git a/examples/src/jets/query-building.sql b/examples/src/jets/query-building.sql new file mode 100644 index 0000000..ede8952 --- /dev/null +++ b/examples/src/jets/query-building.sql @@ -0,0 +1,8 @@ +-- name: CountPilots :one +SELECT COUNT(*) FROM pilots; + +-- name: ListPilots :many +SELECT * FROM pilots LIMIT 5; + +-- name: DeletePilot :exec +DELETE FROM pilots WHERE id = $1; diff --git a/examples/src/jets/schema.sql b/examples/src/jets/schema.sql new file mode 100644 index 0000000..2cc4aca --- /dev/null +++ b/examples/src/jets/schema.sql @@ -0,0 +1,35 @@ +CREATE TABLE pilots ( + id integer NOT NULL, + name text NOT NULL +); + +ALTER TABLE pilots ADD CONSTRAINT pilot_pkey PRIMARY KEY (id); + +CREATE TABLE jets ( + id integer NOT NULL, + pilot_id integer NOT NULL, + age integer NOT NULL, + name text NOT NULL, + color text NOT NULL +); + +ALTER TABLE jets ADD CONSTRAINT jet_pkey PRIMARY KEY (id); +ALTER TABLE jets ADD CONSTRAINT jet_pilots_fkey FOREIGN KEY (pilot_id) REFERENCES pilots(id); + +CREATE TABLE languages ( + id integer NOT NULL, + language text NOT NULL +); + +ALTER TABLE languages ADD CONSTRAINT language_pkey PRIMARY KEY (id); + +-- Join table +CREATE TABLE pilot_languages ( + pilot_id integer NOT NULL, + language_id integer NOT NULL +); + +-- Composite primary key +ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_pkey PRIMARY KEY (pilot_id, language_id); +ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_pilots_fkey FOREIGN KEY (pilot_id) REFERENCES pilots(id); +ALTER TABLE pilot_languages ADD CONSTRAINT pilot_language_languages_fkey FOREIGN KEY (language_id) REFERENCES languages(id); diff --git a/examples/src/ondeck/__init__.py b/examples/src/ondeck/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/src/ondeck/city.py b/examples/src/ondeck/city.py new file mode 100644 index 0000000..e32873e --- /dev/null +++ b/examples/src/ondeck/city.py @@ -0,0 +1,76 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +# source: city.sql +from typing import AsyncIterator, Optional + +import sqlalchemy +import sqlalchemy.ext.asyncio + +from ondeck import models + + +CREATE_CITY = """-- name: create_city \\:one +INSERT INTO city ( + name, + slug +) VALUES ( + :p1, + :p2 +) RETURNING slug, name +""" + + +GET_CITY = """-- name: get_city \\:one +SELECT slug, name +FROM city +WHERE slug = :p1 +""" + + +LIST_CITIES = """-- name: list_cities \\:many +SELECT slug, name +FROM city +ORDER BY name +""" + + +UPDATE_CITY_NAME = """-- name: update_city_name \\:exec +UPDATE city +SET name = :p2 +WHERE slug = :p1 +""" + + +class AsyncQuerier: + def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): + self._conn = conn + + async def create_city(self, *, name: str, slug: str) -> Optional[models.City]: + row = (await self._conn.execute(sqlalchemy.text(CREATE_CITY), {"p1": name, "p2": slug})).first() + if row is None: + return None + return models.City( + slug=row[0], + name=row[1], + ) + + async def get_city(self, *, slug: str) -> Optional[models.City]: + row = (await self._conn.execute(sqlalchemy.text(GET_CITY), {"p1": slug})).first() + if row is None: + return None + return models.City( + slug=row[0], + name=row[1], + ) + + async def list_cities(self) -> AsyncIterator[models.City]: + result = await self._conn.stream(sqlalchemy.text(LIST_CITIES)) + async for row in result: + yield models.City( + slug=row[0], + name=row[1], + ) + + async def update_city_name(self, *, slug: str, name: str) -> None: + await self._conn.execute(sqlalchemy.text(UPDATE_CITY_NAME), {"p1": slug, "p2": name}) diff --git a/examples/src/ondeck/models.py b/examples/src/ondeck/models.py new file mode 100644 index 0000000..71c98c1 --- /dev/null +++ b/examples/src/ondeck/models.py @@ -0,0 +1,35 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +import dataclasses +import datetime +import enum +from typing import List, Optional + + +class Status(str, enum.Enum): + """Venues can be either open or closed""" + OPEN = "op!en" + CLOSED = "clo@sed" + + +@dataclasses.dataclass() +class City: + slug: str + name: str + + +@dataclasses.dataclass() +class Venue: + """Venues are places where muisc happens""" + id: int + status: Status + statuses: Optional[List[Status]] + # This value appears in public URLs + slug: str + name: str + city: str + spotify_playlist: str + songkick_id: Optional[str] + tags: Optional[List[str]] + created_at: datetime.datetime diff --git a/examples/src/ondeck/query/city.sql b/examples/src/ondeck/query/city.sql new file mode 100644 index 0000000..f34dc99 --- /dev/null +++ b/examples/src/ondeck/query/city.sql @@ -0,0 +1,26 @@ +-- name: ListCities :many +SELECT * +FROM city +ORDER BY name; + +-- name: GetCity :one +SELECT * +FROM city +WHERE slug = $1; + +-- name: CreateCity :one +-- Create a new city. The slug must be unique. +-- This is the second line of the comment +-- This is the third line +INSERT INTO city ( + name, + slug +) VALUES ( + $1, + $2 +) RETURNING *; + +-- name: UpdateCityName :exec +UPDATE city +SET name = $2 +WHERE slug = $1; diff --git a/examples/src/ondeck/query/venue.sql b/examples/src/ondeck/query/venue.sql new file mode 100644 index 0000000..8c6bd02 --- /dev/null +++ b/examples/src/ondeck/query/venue.sql @@ -0,0 +1,49 @@ +-- name: ListVenues :many +SELECT * +FROM venue +WHERE city = $1 +ORDER BY name; + +-- name: DeleteVenue :exec +DELETE FROM venue +WHERE slug = $1 AND slug = $1; + +-- name: GetVenue :one +SELECT * +FROM venue +WHERE slug = $1 AND city = $2; + +-- name: CreateVenue :one +INSERT INTO venue ( + slug, + name, + city, + created_at, + spotify_playlist, + status, + statuses, + tags +) VALUES ( + $1, + $2, + $3, + NOW(), + $4, + $5, + $6, + $7 +) RETURNING id; + +-- name: UpdateVenueName :one +UPDATE venue +SET name = $2 +WHERE slug = $1 +RETURNING id; + +-- name: VenueCountByCity :many +SELECT + city, + count(*) +FROM venue +GROUP BY 1 +ORDER BY 1; diff --git a/examples/src/ondeck/schema/0001_city.sql b/examples/src/ondeck/schema/0001_city.sql new file mode 100644 index 0000000..af38f16 --- /dev/null +++ b/examples/src/ondeck/schema/0001_city.sql @@ -0,0 +1,4 @@ +CREATE TABLE city ( + slug text PRIMARY KEY, + name text NOT NULL +) diff --git a/examples/src/ondeck/schema/0002_venue.sql b/examples/src/ondeck/schema/0002_venue.sql new file mode 100644 index 0000000..940de7a --- /dev/null +++ b/examples/src/ondeck/schema/0002_venue.sql @@ -0,0 +1,18 @@ +CREATE TYPE status AS ENUM ('op!en', 'clo@sed'); +COMMENT ON TYPE status IS 'Venues can be either open or closed'; + +CREATE TABLE venues ( + id SERIAL primary key, + dropped text, + status status not null, + statuses status[], + slug text not null, + name varchar(255) not null, + city text not null references city(slug), + spotify_playlist varchar not null, + songkick_id text, + tags text[] +); +COMMENT ON TABLE venues IS 'Venues are places where muisc happens'; +COMMENT ON COLUMN venues.slug IS 'This value appears in public URLs'; + diff --git a/examples/src/ondeck/schema/0003_add_column.sql b/examples/src/ondeck/schema/0003_add_column.sql new file mode 100644 index 0000000..9b334bc --- /dev/null +++ b/examples/src/ondeck/schema/0003_add_column.sql @@ -0,0 +1,3 @@ +ALTER TABLE venues RENAME TO venue; +ALTER TABLE venue ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT NOW(); +ALTER TABLE venue DROP COLUMN dropped; diff --git a/examples/src/ondeck/venue.py b/examples/src/ondeck/venue.py new file mode 100644 index 0000000..88e4a56 --- /dev/null +++ b/examples/src/ondeck/venue.py @@ -0,0 +1,159 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +# source: venue.sql +import dataclasses +from typing import AsyncIterator, List, Optional + +import sqlalchemy +import sqlalchemy.ext.asyncio + +from ondeck import models + + +CREATE_VENUE = """-- name: create_venue \\:one +INSERT INTO venue ( + slug, + name, + city, + created_at, + spotify_playlist, + status, + statuses, + tags +) VALUES ( + :p1, + :p2, + :p3, + NOW(), + :p4, + :p5, + :p6, + :p7 +) RETURNING id +""" + + +@dataclasses.dataclass() +class CreateVenueParams: + slug: str + name: str + city: str + spotify_playlist: str + status: models.Status + statuses: Optional[List[models.Status]] + tags: Optional[List[str]] + + +DELETE_VENUE = """-- name: delete_venue \\:exec +DELETE FROM venue +WHERE slug = :p1 AND slug = :p1 +""" + + +GET_VENUE = """-- name: get_venue \\:one +SELECT id, status, statuses, slug, name, city, spotify_playlist, songkick_id, tags, created_at +FROM venue +WHERE slug = :p1 AND city = :p2 +""" + + +LIST_VENUES = """-- name: list_venues \\:many +SELECT id, status, statuses, slug, name, city, spotify_playlist, songkick_id, tags, created_at +FROM venue +WHERE city = :p1 +ORDER BY name +""" + + +UPDATE_VENUE_NAME = """-- name: update_venue_name \\:one +UPDATE venue +SET name = :p2 +WHERE slug = :p1 +RETURNING id +""" + + +VENUE_COUNT_BY_CITY = """-- name: venue_count_by_city \\:many +SELECT + city, + count(*) +FROM venue +GROUP BY 1 +ORDER BY 1 +""" + + +@dataclasses.dataclass() +class VenueCountByCityRow: + city: str + count: int + + +class AsyncQuerier: + def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): + self._conn = conn + + async def create_venue(self, arg: CreateVenueParams) -> Optional[int]: + row = (await self._conn.execute(sqlalchemy.text(CREATE_VENUE), { + "p1": arg.slug, + "p2": arg.name, + "p3": arg.city, + "p4": arg.spotify_playlist, + "p5": arg.status, + "p6": arg.statuses, + "p7": arg.tags, + })).first() + if row is None: + return None + return row[0] + + async def delete_venue(self, *, slug: str) -> None: + await self._conn.execute(sqlalchemy.text(DELETE_VENUE), {"p1": slug}) + + async def get_venue(self, *, slug: str, city: str) -> Optional[models.Venue]: + row = (await self._conn.execute(sqlalchemy.text(GET_VENUE), {"p1": slug, "p2": city})).first() + if row is None: + return None + return models.Venue( + id=row[0], + status=row[1], + statuses=row[2], + slug=row[3], + name=row[4], + city=row[5], + spotify_playlist=row[6], + songkick_id=row[7], + tags=row[8], + created_at=row[9], + ) + + async def list_venues(self, *, city: str) -> AsyncIterator[models.Venue]: + result = await self._conn.stream(sqlalchemy.text(LIST_VENUES), {"p1": city}) + async for row in result: + yield models.Venue( + id=row[0], + status=row[1], + statuses=row[2], + slug=row[3], + name=row[4], + city=row[5], + spotify_playlist=row[6], + songkick_id=row[7], + tags=row[8], + created_at=row[9], + ) + + async def update_venue_name(self, *, slug: str, name: str) -> Optional[int]: + row = (await self._conn.execute(sqlalchemy.text(UPDATE_VENUE_NAME), {"p1": slug, "p2": name})).first() + if row is None: + return None + return row[0] + + async def venue_count_by_city(self) -> AsyncIterator[VenueCountByCityRow]: + result = await self._conn.stream(sqlalchemy.text(VENUE_COUNT_BY_CITY)) + async for row in result: + yield VenueCountByCityRow( + city=row[0], + count=row[1], + ) diff --git a/examples/src/tests/__init__.py b/examples/src/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/src/tests/conftest.py b/examples/src/tests/conftest.py new file mode 100644 index 0000000..f807209 --- /dev/null +++ b/examples/src/tests/conftest.py @@ -0,0 +1,67 @@ +import asyncio +import os +import random + +import pytest +import sqlalchemy +import sqlalchemy.ext.asyncio + + +@pytest.fixture(scope="session") +def postgres_uri() -> str: + pg_host = os.environ.get("PG_HOST", "postgres") + pg_port = os.environ.get("PG_PORT", 5432) + pg_user = os.environ.get("PG_USER", "postgres") + pg_password = os.environ.get("PG_PASSWORD", "mysecretpassword") + pg_db = os.environ.get("PG_DATABASE", "dinotest") + + return f"postgresql://{pg_user}:{pg_password}@{pg_host}:{pg_port}/{pg_db}" + + +@pytest.fixture(scope="session") +def sqlalchemy_connection(postgres_uri) -> sqlalchemy.engine.Connection: + engine = sqlalchemy.create_engine(postgres_uri, future=True) + with engine.connect() as conn: + yield conn + + +@pytest.fixture(scope="function") +def db(sqlalchemy_connection: sqlalchemy.engine.Connection) -> sqlalchemy.engine.Connection: + conn = sqlalchemy_connection + schema_name = f"sqltest_{random.randint(0, 1000)}" + conn.execute(sqlalchemy.text(f"CREATE SCHEMA {schema_name}")) + conn.execute(sqlalchemy.text(f"SET search_path TO {schema_name}")) + conn.commit() + yield conn + conn.rollback() + conn.execute(sqlalchemy.text(f"DROP SCHEMA {schema_name} CASCADE")) + conn.execute(sqlalchemy.text("SET search_path TO public")) + + +@pytest.fixture(scope="session") +async def async_sqlalchemy_connection(postgres_uri) -> sqlalchemy.ext.asyncio.AsyncConnection: + postgres_uri = postgres_uri.replace("postgresql", "postgresql+asyncpg") + engine = sqlalchemy.ext.asyncio.create_async_engine(postgres_uri) + async with engine.connect() as conn: + yield conn + + +@pytest.fixture(scope="function") +async def async_db(async_sqlalchemy_connection: sqlalchemy.ext.asyncio.AsyncConnection) -> sqlalchemy.ext.asyncio.AsyncConnection: + conn = async_sqlalchemy_connection + schema_name = f"sqltest_{random.randint(0, 1000)}" + await conn.execute(sqlalchemy.text(f"CREATE SCHEMA {schema_name}")) + await conn.execute(sqlalchemy.text(f"SET search_path TO {schema_name}")) + await conn.commit() + yield conn + await conn.rollback() + await conn.execute(sqlalchemy.text(f"DROP SCHEMA {schema_name} CASCADE")) + await conn.execute(sqlalchemy.text("SET search_path TO public")) + + +@pytest.fixture(scope="session") +def event_loop(): + """Change event_loop fixture to session level.""" + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() diff --git a/examples/src/tests/test_authors.py b/examples/src/tests/test_authors.py new file mode 100644 index 0000000..c3031cd --- /dev/null +++ b/examples/src/tests/test_authors.py @@ -0,0 +1,56 @@ +import os + +import pytest +import sqlalchemy.ext.asyncio + +from authors import query +from dbtest.migrations import apply_migrations, apply_migrations_async + + +def test_authors(db: sqlalchemy.engine.Connection): + apply_migrations(db, [os.path.dirname(__file__) + "/../authors/schema.sql"]) + + querier = query.Querier(db) + + authors = list(querier.list_authors()) + assert authors == [] + + author_name = "Brian Kernighan" + author_bio = "Co-author of The C Programming Language and The Go Programming Language" + new_author = querier.create_author(name=author_name, bio=author_bio) + assert new_author.id > 0 + assert new_author.name == author_name + assert new_author.bio == author_bio + + db_author = querier.get_author(id=new_author.id) + assert db_author == new_author + + author_list = list(querier.list_authors()) + assert len(author_list) == 1 + assert author_list[0] == new_author + + +@pytest.mark.asyncio +async def test_authors_async(async_db: sqlalchemy.ext.asyncio.AsyncConnection): + await apply_migrations_async(async_db, [os.path.dirname(__file__) + "/../authors/schema.sql"]) + + querier = query.AsyncQuerier(async_db) + + async for _ in querier.list_authors(): + assert False, "No authors should exist" + + author_name = "Brian Kernighan" + author_bio = "Co-author of The C Programming Language and The Go Programming Language" + new_author = await querier.create_author(name=author_name, bio=author_bio) + assert new_author.id > 0 + assert new_author.name == author_name + assert new_author.bio == author_bio + + db_author = await querier.get_author(id=new_author.id) + assert db_author == new_author + + author_list = [] + async for author in querier.list_authors(): + author_list.append(author) + assert len(author_list) == 1 + assert author_list[0] == new_author diff --git a/examples/src/tests/test_booktest.py b/examples/src/tests/test_booktest.py new file mode 100644 index 0000000..19959ae --- /dev/null +++ b/examples/src/tests/test_booktest.py @@ -0,0 +1,85 @@ +import datetime +import os + +import pytest +import sqlalchemy.ext.asyncio + +from booktest import query, models +from dbtest.migrations import apply_migrations_async + + +@pytest.mark.asyncio +async def test_books(async_db: sqlalchemy.ext.asyncio.AsyncConnection): + await apply_migrations_async(async_db, [os.path.dirname(__file__) + "/../booktest/schema.sql"]) + + querier = query.AsyncQuerier(async_db) + + author = await querier.create_author(name="Unknown Master") + assert author is not None + + now = datetime.datetime.now() + await querier.create_book(query.CreateBookParams( + author_id=author.author_id, + isbn="1", + title="my book title", + book_type=models.BookType.FICTION, + year=2016, + available=now, + tags=[], + )) + + b1 = await querier.create_book(query.CreateBookParams( + author_id=author.author_id, + isbn="2", + title="the second book", + book_type=models.BookType.FICTION, + year=2016, + available=now, + tags=["cool", "unique"], + )) + + await querier.update_book(book_id=b1.book_id, title="changed second title", tags=["cool", "disastor"]) + + b3 = await querier.create_book(query.CreateBookParams( + author_id=author.author_id, + isbn="3", + title="the third book", + book_type=models.BookType.FICTION, + year=2001, + available=now, + tags=["cool"], + )) + + b4 = await querier.create_book(query.CreateBookParams( + author_id=author.author_id, + isbn="4", + title="4th place finisher", + book_type=models.BookType.NONFICTION, + year=2011, + available=now, + tags=["other"], + )) + + await querier.update_book_isbn(book_id=b4.book_id, isbn="NEW ISBN", title="never ever gonna finish, a quatrain", tags=["someother"]) + + books0 = querier.books_by_title_year(title="my book title", year=2016) + expected_titles = {"my book title"} + async for book in books0: + expected_titles.remove(book.title) # raises a key error if the title does not exist + assert len(book.tags) == 0 + + author = await querier.get_author(author_id=book.author_id) + assert author.name == "Unknown Master" + assert len(expected_titles) == 0 + + books = querier.books_by_tags(dollar_1=["cool", "other", "someother"]) + expected_titles = {"changed second title", "the third book", "never ever gonna finish, a quatrain"} + async for book in books: + expected_titles.remove(book.title) + assert len(expected_titles) == 0 + + b5 = await querier.get_book(book_id=b3.book_id) + assert b5 is not None + await querier.delete_book(book_id=b5.book_id) + b6 = await querier.get_book(book_id=b5.book_id) + assert b6 is None diff --git a/examples/src/tests/test_ondeck.py b/examples/src/tests/test_ondeck.py new file mode 100644 index 0000000..20185a8 --- /dev/null +++ b/examples/src/tests/test_ondeck.py @@ -0,0 +1,53 @@ +import os + +import pytest +import sqlalchemy.ext.asyncio + +from ondeck import models +from ondeck import city as city_queries +from ondeck import venue as venue_queries +from dbtest.migrations import apply_migrations_async + + +@pytest.mark.asyncio +async def test_ondeck(async_db: sqlalchemy.ext.asyncio.AsyncConnection): + await apply_migrations_async(async_db, [os.path.dirname(__file__) + "/../ondeck/schema"]) + + city_querier = city_queries.AsyncQuerier(async_db) + venue_querier = venue_queries.AsyncQuerier(async_db) + + city = await city_querier.create_city(slug="san-francisco", name="San Francisco") + assert city is not None + + venue_id = await venue_querier.create_venue(venue_queries.CreateVenueParams( + slug="the-fillmore", + name="The Fillmore", + city=city.slug, + spotify_playlist="spotify:uri", + status=models.Status.OPEN, + statuses=[models.Status.OPEN, models.Status.CLOSED], + tags=["rock", "punk"], + )) + assert venue_id is not None + + venue = await venue_querier.get_venue(slug="the-fillmore", city=city.slug) + assert venue is not None + assert venue.id == venue_id + + assert city == await city_querier.get_city(slug=city.slug) + assert [venue_queries.VenueCountByCityRow(city=city.slug, count=1)] == await _to_list(venue_querier.venue_count_by_city()) + assert [city] == await _to_list(city_querier.list_cities()) + assert [venue] == await _to_list(venue_querier.list_venues(city=city.slug)) + + await city_querier.update_city_name(slug=city.slug, name="SF") + _id = await venue_querier.update_venue_name(slug=venue.slug, name="Fillmore") + assert _id == venue_id + + await venue_querier.delete_venue(slug=venue.slug) + + +async def _to_list(it): + out = [] + async for i in it: + out.append(i) + return out diff --git a/internal/endtoend/endtoend_test.go b/internal/endtoend/endtoend_test.go new file mode 100644 index 0000000..bd66c27 --- /dev/null +++ b/internal/endtoend/endtoend_test.go @@ -0,0 +1,111 @@ +package endtoend + +import ( + "bytes" + "crypto/sha256" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func FindTests(t *testing.T, root string) []string { + t.Helper() + var dirs []string + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.Name() == "sqlc.yaml" { + dirs = append(dirs, filepath.Dir(path)) + return filepath.SkipDir + } + return nil + }) + if err != nil { + t.Fatal(err) + } + return dirs +} + +func LookPath(t *testing.T, cmds ...string) string { + t.Helper() + for _, cmd := range cmds { + path, err := exec.LookPath(cmd) + if err == nil { + return path + } + } + t.Fatalf("could not find command(s) in $PATH: %s", cmds) + return "" +} + +func ExpectedOutput(t *testing.T, dir string) []byte { + t.Helper() + path := filepath.Join(dir, "stderr.txt") + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return []byte{} + } else { + t.Fatal(err) + } + } + output, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + return output +} + +var pattern = regexp.MustCompile(`sha256: ".*"`) + +func TestGenerate(t *testing.T) { + // The SHA256 is required, so we calculate it and then update all of the + // sqlc.yaml files. + // TODO: Remove this once sqlc v1.24.0 has been released + wasmpath := filepath.Join("..", "..", "bin", "sqlc-gen-python.wasm") + if _, err := os.Stat(wasmpath); err != nil { + t.Fatalf("sqlc-gen-python.wasm not found: %s", err) + } + wmod, err := os.ReadFile(wasmpath) + if err != nil { + t.Fatal(err) + } + sum := sha256.Sum256(wmod) + sha256 := fmt.Sprintf("%x", sum) + + sqlc := LookPath(t, "sqlc-dev", "sqlc") + + for _, dir := range FindTests(t, "testdata") { + dir := dir + t.Run(dir, func(t *testing.T) { + // Check if sqlc.yaml has the correct SHA256 for the plugin. If not, update the file + // TODO: Remove this once sqlc v1.24.0 has been released + yaml, err := os.ReadFile(filepath.Join(dir, "sqlc.yaml")) + if err != nil { + t.Fatal(err) + } + if !bytes.Contains(yaml, []byte(sha256)) { + yaml = pattern.ReplaceAllLiteral(yaml, []byte(`sha256: "`+sha256+`"`)) + if err := os.WriteFile(filepath.Join(dir, "sqlc.yaml"), yaml, 0644); err != nil { + t.Fatal(err) + } + } + + want := ExpectedOutput(t, dir) + cmd := exec.Command(sqlc, "diff") + cmd.Dir = dir + got, err := cmd.CombinedOutput() + if diff := cmp.Diff(string(want), string(got)); diff != "" { + t.Errorf("sqlc diff mismatch (-want +got):\n%s", diff) + } + if len(want) == 0 && err != nil { + t.Error(err) + } + }) + } +} diff --git a/internal/endtoend/testdata/emit_pydantic_models/db/__init__.py b/internal/endtoend/testdata/emit_pydantic_models/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/internal/endtoend/testdata/emit_pydantic_models/db/models.py b/internal/endtoend/testdata/emit_pydantic_models/db/models.py new file mode 100644 index 0000000..7ac0da3 --- /dev/null +++ b/internal/endtoend/testdata/emit_pydantic_models/db/models.py @@ -0,0 +1,11 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +import pydantic +from typing import Optional + + +class Author(pydantic.BaseModel): + id: int + name: str + bio: Optional[str] diff --git a/internal/endtoend/testdata/emit_pydantic_models/db/query.py b/internal/endtoend/testdata/emit_pydantic_models/db/query.py new file mode 100644 index 0000000..dc04a26 --- /dev/null +++ b/internal/endtoend/testdata/emit_pydantic_models/db/query.py @@ -0,0 +1,112 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +# source: query.sql +from typing import AsyncIterator, Iterator, Optional + +import sqlalchemy +import sqlalchemy.ext.asyncio + +from db import models + + +CREATE_AUTHOR = """-- name: create_author \\:one +INSERT INTO authors ( + name, bio +) VALUES ( + :p1, :p2 +) +RETURNING id, name, bio +""" + + +DELETE_AUTHOR = """-- name: delete_author \\:exec +DELETE FROM authors +WHERE id = :p1 +""" + + +GET_AUTHOR = """-- name: get_author \\:one +SELECT id, name, bio FROM authors +WHERE id = :p1 LIMIT 1 +""" + + +LIST_AUTHORS = """-- name: list_authors \\:many +SELECT id, name, bio FROM authors +ORDER BY name +""" + + +class Querier: + def __init__(self, conn: sqlalchemy.engine.Connection): + self._conn = conn + + def create_author(self, *, name: str, bio: Optional[str]) -> Optional[models.Author]: + row = self._conn.execute(sqlalchemy.text(CREATE_AUTHOR), {"p1": name, "p2": bio}).first() + if row is None: + return None + return models.Author( + id=row[0], + name=row[1], + bio=row[2], + ) + + def delete_author(self, *, id: int) -> None: + self._conn.execute(sqlalchemy.text(DELETE_AUTHOR), {"p1": id}) + + def get_author(self, *, id: int) -> Optional[models.Author]: + row = self._conn.execute(sqlalchemy.text(GET_AUTHOR), {"p1": id}).first() + if row is None: + return None + return models.Author( + id=row[0], + name=row[1], + bio=row[2], + ) + + def list_authors(self) -> Iterator[models.Author]: + result = self._conn.execute(sqlalchemy.text(LIST_AUTHORS)) + for row in result: + yield models.Author( + id=row[0], + name=row[1], + bio=row[2], + ) + + +class AsyncQuerier: + def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): + self._conn = conn + + async def create_author(self, *, name: str, bio: Optional[str]) -> Optional[models.Author]: + row = (await self._conn.execute(sqlalchemy.text(CREATE_AUTHOR), {"p1": name, "p2": bio})).first() + if row is None: + return None + return models.Author( + id=row[0], + name=row[1], + bio=row[2], + ) + + async def delete_author(self, *, id: int) -> None: + await self._conn.execute(sqlalchemy.text(DELETE_AUTHOR), {"p1": id}) + + async def get_author(self, *, id: int) -> Optional[models.Author]: + row = (await self._conn.execute(sqlalchemy.text(GET_AUTHOR), {"p1": id})).first() + if row is None: + return None + return models.Author( + id=row[0], + name=row[1], + bio=row[2], + ) + + async def list_authors(self) -> AsyncIterator[models.Author]: + result = await self._conn.stream(sqlalchemy.text(LIST_AUTHORS)) + async for row in result: + yield models.Author( + id=row[0], + name=row[1], + bio=row[2], + ) diff --git a/internal/endtoend/testdata/emit_pydantic_models/query.sql b/internal/endtoend/testdata/emit_pydantic_models/query.sql new file mode 100644 index 0000000..75e38b2 --- /dev/null +++ b/internal/endtoend/testdata/emit_pydantic_models/query.sql @@ -0,0 +1,19 @@ +-- name: GetAuthor :one +SELECT * FROM authors +WHERE id = $1 LIMIT 1; + +-- name: ListAuthors :many +SELECT * FROM authors +ORDER BY name; + +-- name: CreateAuthor :one +INSERT INTO authors ( + name, bio +) VALUES ( + $1, $2 +) +RETURNING *; + +-- name: DeleteAuthor :exec +DELETE FROM authors +WHERE id = $1; diff --git a/internal/endtoend/testdata/emit_pydantic_models/schema.sql b/internal/endtoend/testdata/emit_pydantic_models/schema.sql new file mode 100644 index 0000000..b4fad78 --- /dev/null +++ b/internal/endtoend/testdata/emit_pydantic_models/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL, + bio text +); diff --git a/internal/endtoend/testdata/emit_pydantic_models/sqlc.yaml b/internal/endtoend/testdata/emit_pydantic_models/sqlc.yaml new file mode 100644 index 0000000..f3557b7 --- /dev/null +++ b/internal/endtoend/testdata/emit_pydantic_models/sqlc.yaml @@ -0,0 +1,18 @@ +version: '2' +plugins: +- name: py + wasm: + url: file://../../../../bin/sqlc-gen-python.wasm + sha256: "c8c759e80b7d66c728cf9d37674f8f5c9cc33774afa789dbceee0e3446773458" +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_pydantic_models: true \ No newline at end of file diff --git a/internal/endtoend/testdata/exec_result/python/models.py b/internal/endtoend/testdata/exec_result/python/models.py new file mode 100644 index 0000000..d2293ed --- /dev/null +++ b/internal/endtoend/testdata/exec_result/python/models.py @@ -0,0 +1,9 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +import dataclasses + + +@dataclasses.dataclass() +class Bar: + id: int diff --git a/internal/endtoend/testdata/exec_result/python/query.py b/internal/endtoend/testdata/exec_result/python/query.py new file mode 100644 index 0000000..ceccd51 --- /dev/null +++ b/internal/endtoend/testdata/exec_result/python/query.py @@ -0,0 +1,29 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +# source: query.sql +import sqlalchemy +import sqlalchemy.ext.asyncio + +from querytest import models + + +DELETE_BAR_BY_ID = """-- name: delete_bar_by_id \\:execresult +DELETE FROM bar WHERE id = :p1 +""" + + +class Querier: + def __init__(self, conn: sqlalchemy.engine.Connection): + self._conn = conn + + def delete_bar_by_id(self, *, id: int) -> sqlalchemy.engine.Result: + return self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) + + +class AsyncQuerier: + def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): + self._conn = conn + + async def delete_bar_by_id(self, *, id: int) -> sqlalchemy.engine.Result: + return await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) diff --git a/internal/endtoend/testdata/exec_result/query.sql b/internal/endtoend/testdata/exec_result/query.sql new file mode 100644 index 0000000..7391778 --- /dev/null +++ b/internal/endtoend/testdata/exec_result/query.sql @@ -0,0 +1,2 @@ +-- name: DeleteBarByID :execresult +DELETE FROM bar WHERE id = $1; diff --git a/internal/endtoend/testdata/exec_result/schema.sql b/internal/endtoend/testdata/exec_result/schema.sql new file mode 100644 index 0000000..638370a --- /dev/null +++ b/internal/endtoend/testdata/exec_result/schema.sql @@ -0,0 +1,2 @@ +CREATE TABLE bar (id serial not null); + diff --git a/internal/endtoend/testdata/exec_result/sqlc.yaml b/internal/endtoend/testdata/exec_result/sqlc.yaml new file mode 100644 index 0000000..107dd93 --- /dev/null +++ b/internal/endtoend/testdata/exec_result/sqlc.yaml @@ -0,0 +1,17 @@ +version: '2' +plugins: +- name: py + wasm: + url: file://../../../../bin/sqlc-gen-python.wasm + sha256: "c8c759e80b7d66c728cf9d37674f8f5c9cc33774afa789dbceee0e3446773458" +sql: +- schema: schema.sql + queries: query.sql + engine: postgresql + codegen: + - plugin: py + out: python + options: + package: querytest + emit_sync_querier: true + emit_async_querier: true diff --git a/internal/endtoend/testdata/exec_rows/python/models.py b/internal/endtoend/testdata/exec_rows/python/models.py new file mode 100644 index 0000000..d2293ed --- /dev/null +++ b/internal/endtoend/testdata/exec_rows/python/models.py @@ -0,0 +1,9 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +import dataclasses + + +@dataclasses.dataclass() +class Bar: + id: int diff --git a/internal/endtoend/testdata/exec_rows/python/query.py b/internal/endtoend/testdata/exec_rows/python/query.py new file mode 100644 index 0000000..904f428 --- /dev/null +++ b/internal/endtoend/testdata/exec_rows/python/query.py @@ -0,0 +1,31 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +# source: query.sql +import sqlalchemy +import sqlalchemy.ext.asyncio + +from querytest import models + + +DELETE_BAR_BY_ID = """-- name: delete_bar_by_id \\:execrows +DELETE FROM bar WHERE id = :p1 +""" + + +class Querier: + def __init__(self, conn: sqlalchemy.engine.Connection): + self._conn = conn + + def delete_bar_by_id(self, *, id: int) -> int: + result = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) + return result.rowcount + + +class AsyncQuerier: + def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): + self._conn = conn + + async def delete_bar_by_id(self, *, id: int) -> int: + result = await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) + return result.rowcount diff --git a/internal/endtoend/testdata/exec_rows/query.sql b/internal/endtoend/testdata/exec_rows/query.sql new file mode 100644 index 0000000..94d2d09 --- /dev/null +++ b/internal/endtoend/testdata/exec_rows/query.sql @@ -0,0 +1,2 @@ +-- name: DeleteBarByID :execrows +DELETE FROM bar WHERE id = $1; diff --git a/internal/endtoend/testdata/exec_rows/schema.sql b/internal/endtoend/testdata/exec_rows/schema.sql new file mode 100644 index 0000000..638370a --- /dev/null +++ b/internal/endtoend/testdata/exec_rows/schema.sql @@ -0,0 +1,2 @@ +CREATE TABLE bar (id serial not null); + diff --git a/internal/endtoend/testdata/exec_rows/sqlc.yaml b/internal/endtoend/testdata/exec_rows/sqlc.yaml new file mode 100644 index 0000000..107dd93 --- /dev/null +++ b/internal/endtoend/testdata/exec_rows/sqlc.yaml @@ -0,0 +1,17 @@ +version: '2' +plugins: +- name: py + wasm: + url: file://../../../../bin/sqlc-gen-python.wasm + sha256: "c8c759e80b7d66c728cf9d37674f8f5c9cc33774afa789dbceee0e3446773458" +sql: +- schema: schema.sql + queries: query.sql + engine: postgresql + codegen: + - plugin: py + out: python + options: + package: querytest + emit_sync_querier: true + emit_async_querier: true diff --git a/internal/endtoend/testdata/inflection_exclude_table_names/python/models.py b/internal/endtoend/testdata/inflection_exclude_table_names/python/models.py new file mode 100644 index 0000000..b01f524 --- /dev/null +++ b/internal/endtoend/testdata/inflection_exclude_table_names/python/models.py @@ -0,0 +1,22 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +import dataclasses + + +@dataclasses.dataclass() +class Bar: + id: int + name: str + + +@dataclasses.dataclass() +class Exclusions: + id: int + name: str + + +@dataclasses.dataclass() +class MyData: + id: int + name: str diff --git a/internal/endtoend/testdata/inflection_exclude_table_names/python/query.py b/internal/endtoend/testdata/inflection_exclude_table_names/python/query.py new file mode 100644 index 0000000..100bef3 --- /dev/null +++ b/internal/endtoend/testdata/inflection_exclude_table_names/python/query.py @@ -0,0 +1,89 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +# source: query.sql +from typing import Optional + +import sqlalchemy +import sqlalchemy.ext.asyncio + +from querytest import models + + +DELETE_BAR_BY_ID = """-- name: delete_bar_by_id \\:one +DELETE FROM bars WHERE id = :p1 RETURNING id, name +""" + + +DELETE_EXCLUSION_BY_ID = """-- name: delete_exclusion_by_id \\:one +DELETE FROM exclusions WHERE id = :p1 RETURNING id, name +""" + + +DELETE_MY_DATA_BY_ID = """-- name: delete_my_data_by_id \\:one +DELETE FROM my_data WHERE id = :p1 RETURNING id, name +""" + + +class Querier: + def __init__(self, conn: sqlalchemy.engine.Connection): + self._conn = conn + + def delete_bar_by_id(self, *, id: int) -> Optional[models.Bar]: + row = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}).first() + if row is None: + return None + return models.Bar( + id=row[0], + name=row[1], + ) + + def delete_exclusion_by_id(self, *, id: int) -> Optional[models.Exclusions]: + row = self._conn.execute(sqlalchemy.text(DELETE_EXCLUSION_BY_ID), {"p1": id}).first() + if row is None: + return None + return models.Exclusions( + id=row[0], + name=row[1], + ) + + def delete_my_data_by_id(self, *, id: int) -> Optional[models.MyData]: + row = self._conn.execute(sqlalchemy.text(DELETE_MY_DATA_BY_ID), {"p1": id}).first() + if row is None: + return None + return models.MyData( + id=row[0], + name=row[1], + ) + + +class AsyncQuerier: + def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): + self._conn = conn + + async def delete_bar_by_id(self, *, id: int) -> Optional[models.Bar]: + row = (await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id})).first() + if row is None: + return None + return models.Bar( + id=row[0], + name=row[1], + ) + + async def delete_exclusion_by_id(self, *, id: int) -> Optional[models.Exclusions]: + row = (await self._conn.execute(sqlalchemy.text(DELETE_EXCLUSION_BY_ID), {"p1": id})).first() + if row is None: + return None + return models.Exclusions( + id=row[0], + name=row[1], + ) + + async def delete_my_data_by_id(self, *, id: int) -> Optional[models.MyData]: + row = (await self._conn.execute(sqlalchemy.text(DELETE_MY_DATA_BY_ID), {"p1": id})).first() + if row is None: + return None + return models.MyData( + id=row[0], + name=row[1], + ) diff --git a/internal/endtoend/testdata/inflection_exclude_table_names/query.sql b/internal/endtoend/testdata/inflection_exclude_table_names/query.sql new file mode 100644 index 0000000..f279a10 --- /dev/null +++ b/internal/endtoend/testdata/inflection_exclude_table_names/query.sql @@ -0,0 +1,8 @@ +-- name: DeleteBarByID :one +DELETE FROM bars WHERE id = $1 RETURNING id, name; + +-- name: DeleteMyDataByID :one +DELETE FROM my_data WHERE id = $1 RETURNING id, name; + +-- name: DeleteExclusionByID :one +DELETE FROM exclusions WHERE id = $1 RETURNING id, name; diff --git a/internal/endtoend/testdata/inflection_exclude_table_names/schema.sql b/internal/endtoend/testdata/inflection_exclude_table_names/schema.sql new file mode 100644 index 0000000..ea5b797 --- /dev/null +++ b/internal/endtoend/testdata/inflection_exclude_table_names/schema.sql @@ -0,0 +1,4 @@ +CREATE TABLE bars (id serial not null, name text not null, primary key (id)); +CREATE TABLE my_data (id serial not null, name text not null, primary key (id)); +CREATE TABLE exclusions (id serial not null, name text not null, primary key (id)); + diff --git a/internal/endtoend/testdata/inflection_exclude_table_names/sqlc.yaml b/internal/endtoend/testdata/inflection_exclude_table_names/sqlc.yaml new file mode 100644 index 0000000..45ff311 --- /dev/null +++ b/internal/endtoend/testdata/inflection_exclude_table_names/sqlc.yaml @@ -0,0 +1,20 @@ +version: '2' +plugins: +- name: py + wasm: + url: file://../../../../bin/sqlc-gen-python.wasm + sha256: "c8c759e80b7d66c728cf9d37674f8f5c9cc33774afa789dbceee0e3446773458" +sql: +- schema: schema.sql + queries: query.sql + engine: postgresql + codegen: + - plugin: py + out: python + options: + package: querytest + emit_sync_querier: true + emit_async_querier: true + inflection_exclude_table_names: + - my_data + - exclusions diff --git a/internal/endtoend/testdata/query_parameter_limit_two/python/models.py b/internal/endtoend/testdata/query_parameter_limit_two/python/models.py new file mode 100644 index 0000000..9bc595f --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_two/python/models.py @@ -0,0 +1,10 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +import dataclasses + + +@dataclasses.dataclass() +class Bar: + id: int + name: str diff --git a/internal/endtoend/testdata/query_parameter_limit_two/python/query.py b/internal/endtoend/testdata/query_parameter_limit_two/python/query.py new file mode 100644 index 0000000..3ca9cba --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_two/python/query.py @@ -0,0 +1,44 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +# source: query.sql +import sqlalchemy +import sqlalchemy.ext.asyncio + +from querytest import models + + +DELETE_BAR_BY_ID = """-- name: delete_bar_by_id \\:execrows +DELETE FROM bar WHERE id = :p1 +""" + + +DELETE_BAR_BY_ID_AND_NAME = """-- name: delete_bar_by_id_and_name \\:execrows +DELETE FROM bar WHERE id = :p1 AND name = :p2 +""" + + +class Querier: + def __init__(self, conn: sqlalchemy.engine.Connection): + self._conn = conn + + def delete_bar_by_id(self, *, id: int) -> int: + result = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) + return result.rowcount + + def delete_bar_by_id_and_name(self, *, id: int, name: str) -> int: + result = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID_AND_NAME), {"p1": id, "p2": name}) + return result.rowcount + + +class AsyncQuerier: + def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): + self._conn = conn + + async def delete_bar_by_id(self, *, id: int) -> int: + result = await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) + return result.rowcount + + async def delete_bar_by_id_and_name(self, *, id: int, name: str) -> int: + result = await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID_AND_NAME), {"p1": id, "p2": name}) + return result.rowcount diff --git a/internal/endtoend/testdata/query_parameter_limit_two/query.sql b/internal/endtoend/testdata/query_parameter_limit_two/query.sql new file mode 100644 index 0000000..b96c66b --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_two/query.sql @@ -0,0 +1,5 @@ +-- name: DeleteBarByID :execrows +DELETE FROM bar WHERE id = $1; + +-- name: DeleteBarByIDAndName :execrows +DELETE FROM bar WHERE id = $1 AND name = $2; diff --git a/internal/endtoend/testdata/query_parameter_limit_two/schema.sql b/internal/endtoend/testdata/query_parameter_limit_two/schema.sql new file mode 100644 index 0000000..a27c312 --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_two/schema.sql @@ -0,0 +1,2 @@ +CREATE TABLE bar (id serial not null, name text not null, primary key (id)); + diff --git a/internal/endtoend/testdata/query_parameter_limit_two/sqlc.yaml b/internal/endtoend/testdata/query_parameter_limit_two/sqlc.yaml new file mode 100644 index 0000000..3f4cfcc --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_two/sqlc.yaml @@ -0,0 +1,18 @@ +version: '2' +plugins: +- name: py + wasm: + url: file://../../../../bin/sqlc-gen-python.wasm + sha256: "c8c759e80b7d66c728cf9d37674f8f5c9cc33774afa789dbceee0e3446773458" +sql: +- schema: schema.sql + queries: query.sql + engine: postgresql + codegen: + - plugin: py + out: python + options: + package: querytest + emit_sync_querier: true + emit_async_querier: true + query_parameter_limit: 2 \ No newline at end of file diff --git a/internal/endtoend/testdata/query_parameter_limit_undefined/python/models.py b/internal/endtoend/testdata/query_parameter_limit_undefined/python/models.py new file mode 100644 index 0000000..5e2f655 --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_undefined/python/models.py @@ -0,0 +1,12 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +import dataclasses + + +@dataclasses.dataclass() +class Bar: + id: int + name1: str + name2: str + name3: str diff --git a/internal/endtoend/testdata/query_parameter_limit_undefined/python/query.py b/internal/endtoend/testdata/query_parameter_limit_undefined/python/query.py new file mode 100644 index 0000000..4dccfa9 --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_undefined/python/query.py @@ -0,0 +1,58 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +# source: query.sql +import sqlalchemy +import sqlalchemy.ext.asyncio + +from querytest import models + + +DELETE_BAR_BY_ID = """-- name: delete_bar_by_id \\:execrows +DELETE FROM bar WHERE id = :p1 +""" + + +DELETE_BAR_BY_ID_AND_NAME = """-- name: delete_bar_by_id_and_name \\:execrows +DELETE FROM bar +WHERE id = :p1 +AND name1 = :p2 +AND name2 = :p3 +AND name3 = :p4 +""" + + +class Querier: + def __init__(self, conn: sqlalchemy.engine.Connection): + self._conn = conn + + def delete_bar_by_id(self, *, id: int) -> int: + result = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) + return result.rowcount + + def delete_bar_by_id_and_name(self, *, id: int, name1: str, name2: str, name3: str) -> int: + result = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID_AND_NAME), { + "p1": id, + "p2": name1, + "p3": name2, + "p4": name3, + }) + return result.rowcount + + +class AsyncQuerier: + def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): + self._conn = conn + + async def delete_bar_by_id(self, *, id: int) -> int: + result = await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": id}) + return result.rowcount + + async def delete_bar_by_id_and_name(self, *, id: int, name1: str, name2: str, name3: str) -> int: + result = await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID_AND_NAME), { + "p1": id, + "p2": name1, + "p3": name2, + "p4": name3, + }) + return result.rowcount diff --git a/internal/endtoend/testdata/query_parameter_limit_undefined/query.sql b/internal/endtoend/testdata/query_parameter_limit_undefined/query.sql new file mode 100644 index 0000000..aea8511 --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_undefined/query.sql @@ -0,0 +1,10 @@ +-- name: DeleteBarByID :execrows +DELETE FROM bar WHERE id = $1; + +-- name: DeleteBarByIDAndName :execrows +DELETE FROM bar +WHERE id = $1 +AND name1 = $2 +AND name2 = $3 +AND name3 = $4 +; diff --git a/internal/endtoend/testdata/query_parameter_limit_undefined/schema.sql b/internal/endtoend/testdata/query_parameter_limit_undefined/schema.sql new file mode 100644 index 0000000..1d9131f --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_undefined/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE bar ( + id serial not null, + name1 text not null, + name2 text not null, + name3 text not null, + primary key (id)); + diff --git a/internal/endtoend/testdata/query_parameter_limit_undefined/sqlc.yaml b/internal/endtoend/testdata/query_parameter_limit_undefined/sqlc.yaml new file mode 100644 index 0000000..95357f7 --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_undefined/sqlc.yaml @@ -0,0 +1,17 @@ +version: '2' +plugins: +- name: py + wasm: + url: file://../../../../bin/sqlc-gen-python.wasm + sha256: "c8c759e80b7d66c728cf9d37674f8f5c9cc33774afa789dbceee0e3446773458" +sql: +- schema: schema.sql + queries: query.sql + engine: postgresql + codegen: + - plugin: py + out: python + options: + package: querytest + emit_sync_querier: true + emit_async_querier: true \ No newline at end of file diff --git a/internal/endtoend/testdata/query_parameter_limit_zero/python/models.py b/internal/endtoend/testdata/query_parameter_limit_zero/python/models.py new file mode 100644 index 0000000..9bc595f --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_zero/python/models.py @@ -0,0 +1,10 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +import dataclasses + + +@dataclasses.dataclass() +class Bar: + id: int + name: str diff --git a/internal/endtoend/testdata/query_parameter_limit_zero/python/query.py b/internal/endtoend/testdata/query_parameter_limit_zero/python/query.py new file mode 100644 index 0000000..2a42517 --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_zero/python/query.py @@ -0,0 +1,57 @@ +# Code generated by sqlc. DO NOT EDIT. +# versions: +# sqlc v1.23.0 +# source: query.sql +import dataclasses + +import sqlalchemy +import sqlalchemy.ext.asyncio + +from querytest import models + + +DELETE_BAR_BY_ID = """-- name: delete_bar_by_id \\:execrows +DELETE FROM bar WHERE id = :p1 +""" + + +@dataclasses.dataclass() +class DeleteBarByIDParams: + id: int + + +DELETE_BAR_BY_ID_AND_NAME = """-- name: delete_bar_by_id_and_name \\:execrows +DELETE FROM bar WHERE id = :p1 AND name = :p2 +""" + + +@dataclasses.dataclass() +class DeleteBarByIDAndNameParams: + id: int + name: str + + +class Querier: + def __init__(self, conn: sqlalchemy.engine.Connection): + self._conn = conn + + def delete_bar_by_id(self, arg: DeleteBarByIDParams) -> int: + result = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": arg.id}) + return result.rowcount + + def delete_bar_by_id_and_name(self, arg: DeleteBarByIDAndNameParams) -> int: + result = self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID_AND_NAME), {"p1": arg.id, "p2": arg.name}) + return result.rowcount + + +class AsyncQuerier: + def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): + self._conn = conn + + async def delete_bar_by_id(self, arg: DeleteBarByIDParams) -> int: + result = await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID), {"p1": arg.id}) + return result.rowcount + + async def delete_bar_by_id_and_name(self, arg: DeleteBarByIDAndNameParams) -> int: + result = await self._conn.execute(sqlalchemy.text(DELETE_BAR_BY_ID_AND_NAME), {"p1": arg.id, "p2": arg.name}) + return result.rowcount diff --git a/internal/endtoend/testdata/query_parameter_limit_zero/query.sql b/internal/endtoend/testdata/query_parameter_limit_zero/query.sql new file mode 100644 index 0000000..b96c66b --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_zero/query.sql @@ -0,0 +1,5 @@ +-- name: DeleteBarByID :execrows +DELETE FROM bar WHERE id = $1; + +-- name: DeleteBarByIDAndName :execrows +DELETE FROM bar WHERE id = $1 AND name = $2; diff --git a/internal/endtoend/testdata/query_parameter_limit_zero/schema.sql b/internal/endtoend/testdata/query_parameter_limit_zero/schema.sql new file mode 100644 index 0000000..a27c312 --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_zero/schema.sql @@ -0,0 +1,2 @@ +CREATE TABLE bar (id serial not null, name text not null, primary key (id)); + diff --git a/internal/endtoend/testdata/query_parameter_limit_zero/sqlc.yaml b/internal/endtoend/testdata/query_parameter_limit_zero/sqlc.yaml new file mode 100644 index 0000000..dc6b25e --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_limit_zero/sqlc.yaml @@ -0,0 +1,18 @@ +version: '2' +plugins: +- name: py + wasm: + url: file://../../../../bin/sqlc-gen-python.wasm + sha256: "c8c759e80b7d66c728cf9d37674f8f5c9cc33774afa789dbceee0e3446773458" +sql: +- schema: schema.sql + queries: query.sql + engine: postgresql + codegen: + - plugin: py + out: python + options: + package: querytest + emit_sync_querier: true + emit_async_querier: true + query_parameter_limit: 0 diff --git a/internal/endtoend/testdata/query_parameter_no_limit/query.sql b/internal/endtoend/testdata/query_parameter_no_limit/query.sql new file mode 100644 index 0000000..b96c66b --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_no_limit/query.sql @@ -0,0 +1,5 @@ +-- name: DeleteBarByID :execrows +DELETE FROM bar WHERE id = $1; + +-- name: DeleteBarByIDAndName :execrows +DELETE FROM bar WHERE id = $1 AND name = $2; diff --git a/internal/endtoend/testdata/query_parameter_no_limit/schema.sql b/internal/endtoend/testdata/query_parameter_no_limit/schema.sql new file mode 100644 index 0000000..fd3ca97 --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_no_limit/schema.sql @@ -0,0 +1 @@ +CREATE TABLE bar (id serial not null, name text not null, primary key (id)); \ No newline at end of file diff --git a/internal/endtoend/testdata/query_parameter_no_limit/sqlc.yaml b/internal/endtoend/testdata/query_parameter_no_limit/sqlc.yaml new file mode 100644 index 0000000..76b5412 --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_no_limit/sqlc.yaml @@ -0,0 +1,18 @@ +version: '2' +plugins: +- name: py + wasm: + url: file://../../../../bin/sqlc-gen-python.wasm + sha256: "c8c759e80b7d66c728cf9d37674f8f5c9cc33774afa789dbceee0e3446773458" +sql: +- schema: schema.sql + queries: query.sql + engine: postgresql + codegen: + - plugin: py + out: python + options: + package: querytest + emit_sync_querier: true + emit_async_querier: true + query_parameter_limit: -1 diff --git a/internal/endtoend/testdata/query_parameter_no_limit/stderr.txt b/internal/endtoend/testdata/query_parameter_no_limit/stderr.txt new file mode 100644 index 0000000..efed0cc --- /dev/null +++ b/internal/endtoend/testdata/query_parameter_no_limit/stderr.txt @@ -0,0 +1,2 @@ +# package py +error generating code: error generating output: invalid query parameter limit