Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ bin
.direnv
.devenv*
devenv.local.nix

.idea/
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,49 @@ class Status(str, enum.Enum):
OPEN = "op!en"
CLOSED = "clo@sed"
```

### Bulk Inserts with `:copyfrom`

Use the `:copyfrom` command to generate batch insert methods that leverage SQLAlchemy’s executemany behavior via `Connection.execute()` with a list of parameter mappings.

SQL (example):

```sql
-- name: CreateUsersBatch :copyfrom
INSERT INTO users (email, name) VALUES ($1, $2);
```

Generated methods:

```py
def create_users_batch(self, arg_list: List[Any]) -> int
async def create_users_batch(self, arg_list: List[Any]) -> int
```

Call with a list of dicts using positional parameter keys `p1..pN` (the generator converts `$1`/`@name` to `:pN`):

```py
rows = [
{"p1": "a@example.com", "p2": "Alice"},
{"p1": "b@example.com", "p2": "Bob"},
]
count = queries.create_users_batch(rows) # returns affected rowcount (int)
```

When a typed params struct is emitted (e.g., many parameters or config thresholds), the method accepts `List[<QueryName>Params]`. The generator converts items to dicts internally:

```py
@dataclasses.dataclass()
class CreateUsersWithDetailsParams:
email: str
name: str
bio: Optional[str]
age: Optional[int]
active: Optional[bool]

count = queries.create_users_with_details([
CreateUsersWithDetailsParams("a@example.com", "Alice", None, None, True),
])
```

Implementation note: sync and async use `conn.execute(sqlalchemy.text(SQL), list_of_dicts)` and `await async_conn.execute(...)` respectively; SQLAlchemy performs efficient batch inserts under the hood.
24 changes: 24 additions & 0 deletions internal/endtoend/testdata/copyfrom/python/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Code generated by sqlc. DO NOT EDIT.
# versions:
# sqlc v1.30.0
import dataclasses
import datetime
from typing import Optional


@dataclasses.dataclass()
class Author:
id: int
name: str
bio: str


@dataclasses.dataclass()
class User:
id: int
email: str
name: str
bio: Optional[str]
age: Optional[int]
active: Optional[bool]
created_at: datetime.datetime
198 changes: 198 additions & 0 deletions internal/endtoend/testdata/copyfrom/python/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# Code generated by sqlc. DO NOT EDIT.
# versions:
# sqlc v1.30.0
# source: query.sql
import dataclasses
from typing import Any, List, Optional

import sqlalchemy
import sqlalchemy.ext.asyncio

from copyfrom import models


CREATE_AUTHOR = """-- name: create_author \\:one
INSERT INTO authors (name, bio) VALUES (:p1, :p2) RETURNING id, name, bio
"""


CREATE_AUTHORS = """-- name: create_authors \\:copyfrom
INSERT INTO authors (name, bio) VALUES (:p1, :p2)
"""


CREATE_AUTHORS_NAMED = """-- name: create_authors_named \\:copyfrom
INSERT INTO authors (name, bio) VALUES (:p1, :p2)
"""


CREATE_USER = """-- name: create_user \\:one
INSERT INTO users (email, name) VALUES (:p1, :p2) RETURNING id, email, name, bio, age, active, created_at
"""


CREATE_USERS_BATCH = """-- name: create_users_batch \\:copyfrom
INSERT INTO users (email, name) VALUES (:p1, :p2)
"""


CREATE_USERS_SHUFFLED = """-- name: create_users_shuffled \\:copyfrom
INSERT INTO users (email, name, bio, age, active) VALUES (:p2, :p1, :p3, :p4, :p5)
"""


@dataclasses.dataclass()
class CreateUsersShuffledParams:
name: str
email: str
bio: Optional[str]
age: Optional[int]
active: Optional[bool]


CREATE_USERS_WITH_DETAILS = """-- name: create_users_with_details \\:copyfrom
INSERT INTO users (email, name, bio, age, active) VALUES (:p1, :p2, :p3, :p4, :p5)
"""


@dataclasses.dataclass()
class CreateUsersWithDetailsParams:
email: str
name: str
bio: Optional[str]
age: Optional[int]
active: Optional[bool]


class Querier:
def __init__(self, conn: sqlalchemy.engine.Connection):
self._conn = conn

def create_author(self, *, name: str, bio: 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 create_authors(self, arg_list: List[Any]) -> int:
result = self._conn.execute(sqlalchemy.text(CREATE_AUTHORS), arg_list)
return result.rowcount

def create_authors_named(self, arg_list: List[Any]) -> int:
result = self._conn.execute(sqlalchemy.text(CREATE_AUTHORS_NAMED), arg_list)
return result.rowcount

def create_user(self, *, email: str, name: str) -> Optional[models.User]:
row = self._conn.execute(sqlalchemy.text(CREATE_USER), {"p1": email, "p2": name}).first()
if row is None:
return None
return models.User(
id=row[0],
email=row[1],
name=row[2],
bio=row[3],
age=row[4],
active=row[5],
created_at=row[6],
)

def create_users_batch(self, arg_list: List[Any]) -> int:
result = self._conn.execute(sqlalchemy.text(CREATE_USERS_BATCH), arg_list)
return result.rowcount

def create_users_shuffled(self, arg_list: List[CreateUsersShuffledParams]) -> int:
data = list()
for item in arg_list:
data.append({
"p1": item.name,
"p2": item.email,
"p3": item.bio,
"p4": item.age,
"p5": item.active,
})
result = self._conn.execute(sqlalchemy.text(CREATE_USERS_SHUFFLED), data)
return result.rowcount

def create_users_with_details(self, arg_list: List[CreateUsersWithDetailsParams]) -> int:
data = list()
for item in arg_list:
data.append({
"p1": item.email,
"p2": item.name,
"p3": item.bio,
"p4": item.age,
"p5": item.active,
})
result = self._conn.execute(sqlalchemy.text(CREATE_USERS_WITH_DETAILS), data)
return result.rowcount


class AsyncQuerier:
def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection):
self._conn = conn

async def create_author(self, *, name: str, bio: 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 create_authors(self, arg_list: List[Any]) -> int:
result = await self._conn.execute(sqlalchemy.text(CREATE_AUTHORS), arg_list)
return result.rowcount

async def create_authors_named(self, arg_list: List[Any]) -> int:
result = await self._conn.execute(sqlalchemy.text(CREATE_AUTHORS_NAMED), arg_list)
return result.rowcount

async def create_user(self, *, email: str, name: str) -> Optional[models.User]:
row = (await self._conn.execute(sqlalchemy.text(CREATE_USER), {"p1": email, "p2": name})).first()
if row is None:
return None
return models.User(
id=row[0],
email=row[1],
name=row[2],
bio=row[3],
age=row[4],
active=row[5],
created_at=row[6],
)

async def create_users_batch(self, arg_list: List[Any]) -> int:
result = await self._conn.execute(sqlalchemy.text(CREATE_USERS_BATCH), arg_list)
return result.rowcount

async def create_users_shuffled(self, arg_list: List[CreateUsersShuffledParams]) -> int:
data = list()
for item in arg_list:
data.append({
"p1": item.name,
"p2": item.email,
"p3": item.bio,
"p4": item.age,
"p5": item.active,
})
result = await self._conn.execute(sqlalchemy.text(CREATE_USERS_SHUFFLED), data)
return result.rowcount

async def create_users_with_details(self, arg_list: List[CreateUsersWithDetailsParams]) -> int:
data = list()
for item in arg_list:
data.append({
"p1": item.email,
"p2": item.name,
"p3": item.bio,
"p4": item.age,
"p5": item.active,
})
result = await self._conn.execute(sqlalchemy.text(CREATE_USERS_WITH_DETAILS), data)
return result.rowcount
20 changes: 20 additions & 0 deletions internal/endtoend/testdata/copyfrom/query.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- name: CreateAuthors :copyfrom
INSERT INTO authors (name, bio) VALUES ($1, $2);

-- name: CreateAuthor :one
INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING *;

-- name: CreateAuthorsNamed :copyfrom
INSERT INTO authors (name, bio) VALUES (@name, @bio);

-- name: CreateUser :one
INSERT INTO users (email, name) VALUES (@email, @name) RETURNING *;

-- name: CreateUsersBatch :copyfrom
INSERT INTO users (email, name) VALUES (@email, @name);

-- name: CreateUsersWithDetails :copyfrom
INSERT INTO users (email, name, bio, age, active) VALUES ($1, $2, $3, $4, $5);

-- name: CreateUsersShuffled :copyfrom
INSERT INTO users (email, name, bio, age, active) VALUES ($2, $1, $3, $4, $5);
15 changes: 15 additions & 0 deletions internal/endtoend/testdata/copyfrom/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
name text NOT NULL,
bio text NOT NULL
);

CREATE TABLE users (
id SERIAL PRIMARY KEY,
email text NOT NULL,
name text NOT NULL,
bio text,
age int,
active boolean DEFAULT true,
created_at timestamp NOT NULL DEFAULT NOW()
);
17 changes: 17 additions & 0 deletions internal/endtoend/testdata/copyfrom/sqlc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: "2"
plugins:
- name: py
wasm:
url: file://../../../../bin/sqlc-gen-python.wasm
sha256: "d587b6af0bfe07dbb2c316bffb5f7f5d0073a60788c5d533b84373067c9c1916"
sql:
- schema: "schema.sql"
queries: "query.sql"
engine: postgresql
codegen:
- out: python
plugin: py
options:
package: copyfrom
emit_sync_querier: true
emit_async_querier: true
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Code generated by sqlc. DO NOT EDIT.
# versions:
# sqlc v1.28.0
# sqlc v1.30.0
import pydantic
from typing import Optional

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Code generated by sqlc. DO NOT EDIT.
# versions:
# sqlc v1.28.0
# sqlc v1.30.0
# source: query.sql
from typing import AsyncIterator, Iterator, Optional

Expand Down
2 changes: 1 addition & 1 deletion internal/endtoend/testdata/emit_pydantic_models/sqlc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins:
- name: py
wasm:
url: file://../../../../bin/sqlc-gen-python.wasm
sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca"
sha256: "d587b6af0bfe07dbb2c316bffb5f7f5d0073a60788c5d533b84373067c9c1916"
sql:
- schema: schema.sql
queries: query.sql
Expand Down
2 changes: 1 addition & 1 deletion internal/endtoend/testdata/emit_str_enum/db/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Code generated by sqlc. DO NOT EDIT.
# versions:
# sqlc v1.28.0
# sqlc v1.30.0
import dataclasses
import enum
from typing import Optional
Expand Down
2 changes: 1 addition & 1 deletion internal/endtoend/testdata/emit_str_enum/db/query.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Code generated by sqlc. DO NOT EDIT.
# versions:
# sqlc v1.28.0
# sqlc v1.30.0
# source: query.sql
from typing import AsyncIterator, Iterator, Optional

Expand Down
2 changes: 1 addition & 1 deletion internal/endtoend/testdata/emit_str_enum/sqlc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins:
- name: py
wasm:
url: file://../../../../bin/sqlc-gen-python.wasm
sha256: "d6846ffad948181e611e883cedd2d2be66e091edc1273a0abc6c9da18399e0ca"
sha256: "d587b6af0bfe07dbb2c316bffb5f7f5d0073a60788c5d533b84373067c9c1916"
sql:
- schema: schema.sql
queries: query.sql
Expand Down
2 changes: 1 addition & 1 deletion internal/endtoend/testdata/exec_result/python/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Code generated by sqlc. DO NOT EDIT.
# versions:
# sqlc v1.28.0
# sqlc v1.30.0
import dataclasses


Expand Down
Loading