Skip to content
Merged
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
40 changes: 37 additions & 3 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,43 @@ Unreleased

**Fixed:**

* Psycopg reads COPY files in chunks, not all at once. ADK stores use
RETURNING to cut round trips. Psqlpy closes a connection if setup fails.
* Builder results keep CTE trees independent, and column pruning no longer
exposes its cached expression to mutation. SQL generation avoids redundant
copies of temporary trees while preserving caller and cache ownership.

* SQL Server migration drivers retain the previous default schema if restoring
it fails, so cleanup can be retried. The migration guide clarifies that this
setting belongs to the database user rather than one connection.

* Asyncpg stack telemetry reports sequential prepared execution rather than
native pipelining. Each statement still returns its own result.

* Builder upserts emit ``MERGE`` for the ``db2`` dialect.
* Fixture files keep JSON strings such as ``"true"`` and ``"[1]"`` as strings.
This also works for SQLite JSON columns. Write objects and arrays directly
instead of encoding them as strings. Column filtering respects case.
See :doc:`usage/testing`.

* DDL builders and migration trackers keep quoted table names intact.
Names with spaces and mixed-case Oracle names retain their quotes.

* MySQL pools release connections when setup fails. They discard connections
that fail to roll back. MySQL Connector keeps native async pooling on
Connector 9.4 and later, plus direct connections on older versions.
Asyncmy retains native ``LOAD DATA LOCAL INFILE`` support.

* Oracle keeps Thick-mode options for sync pools. Async pools reject Thick
mode before they open. Pool shutdown preserves native checks for
borrowed connections. Custom handlers still convert LOBs, and JSON handlers
preserve the user's callbacks.

* Db2 pools clean up after failed or cancelled setup. Batch results keep an
unknown row count when the driver cannot report one.
String searches keep their start position and requested occurrence.

* Spanner schema queries no longer require a table name. SQL output keeps JOIN
hints and plain comments. Sequence statements keep qualified names and
``IF NOT EXISTS`` guards. Cached row converters refresh when the configured
JSON deserializer changes.

* ADBC ADK stores reuse cached PostgreSQL placeholder conversion and preserve
question marks in quoted identifiers, literals, and comments.
Expand All @@ -60,6 +90,10 @@ Unreleased
parsing SQL again. ADBC keeps bound values in its ADK store queries.
DuckDB Arrow loads keep sparse dictionary fields and quote table names.

* Psycopg reads COPY files in chunks, not all at once. ADK stores use
RETURNING to cut round trips. Psqlpy closes a connection if setup fails.

* Builder upserts emit ``MERGE`` for the ``db2`` dialect.
* The arrow-odbc adapter detects the SQL dialect from the ODBC driver name
only, so database, host, or user names no longer select the wrong dialect.

Expand Down
5 changes: 4 additions & 1 deletion docs/usage/migrations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,10 @@ raises ``MigrationError`` before any DDL is issued.
``sys.schemas``. The previous default schema is restored after each
migration (a failed transactional migration rolls the switch back).
SQL Server refuses to alter the default schema of the ``dbo`` database
user (what ``sa`` maps to), so connect with a dedicated login. Because
user (what ``sa`` maps to). Use a dedicated migration login and database
user. ``ALTER USER`` changes the database user's default schema across
connections; it is not a session-local setting. Do not run migrations
alongside other work or migrations that share that database user. Because
the restore is committed, a failed non-transactional migration on a
connection with autocommit disabled also commits the statements that
succeeded before the failure.
Expand Down
29 changes: 21 additions & 8 deletions docs/usage/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,8 @@ functionality for asynchronous drivers:
- **Exact names:** table and column names are quoted in every statement, so they
must match the database spelling exactly, including case. Reserved words such
as ``order`` work as table or column names. On PostgreSQL, unqualified tables
resolve through the session's search path. The query builder renders Oracle
names unquoted.
resolve through the session's search path. On Oracle, explicitly quoted table
names keep their spelling; ordinary column names follow uppercase folding.
- **Column types:** before inserting, the loader reads the table's columns from
the driver's data dictionary on PostgreSQL-family, MySQL, DuckDB, and SQLite
drivers and converts these JSON values:
Expand All @@ -317,9 +317,15 @@ functionality for asynchronous drivers:
- strings and numbers in ``numeric``/``decimal`` columns to ``Decimal``;
- strings in ``uuid`` columns to ``UUID``;
- base64 strings in ``bytea``, ``blob``, ``tinyblob``, ``mediumblob``,
``longblob``, ``binary``, and ``varbinary`` columns to bytes (the only
conversion on SQLite);
- values of PostgreSQL and MySQL ``json``/``jsonb`` columns to JSON text.
``longblob``, ``binary``, and ``varbinary`` columns to bytes;
- values of PostgreSQL, MySQL, DuckDB, and SQLite ``json``/``jsonb`` columns
to JSON text. SQLite converts only binary and JSON columns.

Write JSON column values directly in the fixture. Use an object for an object,
an array for an array, and a string for a string. Strings such as ``"true"``
and ``"[1]"`` remain strings. The loader does not decode their contents again.
Convert pre-encoded object or array strings in older fixtures to JSON objects
or arrays before loading them.

Every other value is passed to the driver as decoded from JSON. That includes
``BIT`` columns (including MySQL ``BIT``),
Expand All @@ -333,8 +339,13 @@ functionality for asynchronous drivers:
contain them still load. Other databases are not checked for them, so SQL Server
computed columns and Oracle virtual columns are exported and loaded like any
other column.
- **Upserts:** ``conflict_keys`` maps a table to the columns of a unique
constraint; every entry must name a table being loaded, spelled exactly. Rows
- **Sparse rows:** rows can have different keys. Missing keys become ``None``
within the table's combined set of columns. With ``ignore_unknown_columns=True``,
keys absent from the table's column metadata are omitted; names must match
exactly, including case.
- **Upserts:** ``conflict_keys`` maps a table to one column name or a sequence
of column names forming a unique constraint. Entries for tables outside the
current load are ignored, so one mapping can serve several subset loads. Rows
for that table update the non-key columns of existing rows instead of failing;
PostgreSQL ``GENERATED ALWAYS`` identity columns are never updated, and a table
with nothing left to update skips the conflicting row. PostgreSQL-family,
Expand All @@ -343,7 +354,9 @@ functionality for asynchronous drivers:
the table. ``VALUES()`` is deprecated since MySQL 8.0.20 but kept because
MariaDB does not support the row-alias form. Other dialects raise
``ValueError`` before any statement runs. Without conflict keys, a duplicate
row raises the database's integrity error.
row raises the database's integrity error. Pass ``exclude_update_columns``
as a sequence of column names or a per-table mapping to keep those columns
unchanged on conflict; their values are still used for new rows.
- **Identity columns:** on PostgreSQL, values for ``GENERATED ALWAYS`` identity
columns are inserted with ``OVERRIDING SYSTEM VALUE``. CockroachDB does not
accept explicit values for ``GENERATED ALWAYS`` columns; use
Expand Down
2 changes: 1 addition & 1 deletion sqlspec/adapters/adbc/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1162,7 +1162,7 @@ def wrap_parameter(node: exp.Expression) -> exp.Expression:

for expression in expressions:
expression.transform(wrap_parameter, copy=False)
rewritten = "; ".join(expression.sql(dialect=dialect) for expression in expressions)
rewritten = "; ".join(expression.sql(dialect=dialect, copy=False) for expression in expressions)
return rewritten, effective_scalars, effective_arrays


Expand Down
42 changes: 28 additions & 14 deletions sqlspec/adapters/aiomysql/_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,31 @@
import contextlib
from typing import TYPE_CHECKING, Any

import aiomysql
import pymysql.constants
from aiomysql import Pool as AiomysqlPool
from aiomysql import ProgrammingError as AiomysqlProgrammingError
import aiomysql as _aiomysql # pyright: ignore
from aiomysql import Connection # pyright: ignore
from aiomysql import Error as _AiomysqlError # pyright: ignore
from aiomysql import MySQLError as _AiomysqlMySQLError # pyright: ignore
from aiomysql import Pool as _AiomysqlPool # pyright: ignore
from aiomysql import ProgrammingError as AiomysqlProgrammingError # pyright: ignore
from aiomysql import SSCursor as AiomysqlSSCursor
from aiomysql.cursors import RE_INSERT_VALUES as AIOMYSQL_INSERT_VALUES_PATTERN
from aiomysql.cursors import Cursor as AiomysqlRawCursor
from aiomysql.cursors import DictCursor as AiomysqlDictCursor
from pymysql.err import Error as AiomysqlPymysqlError
from pymysql.err import MySQLError as AiomysqlPymysqlMySQLError
from aiomysql.cursors import Cursor as _AiomysqlCursor # pyright: ignore
from aiomysql.cursors import DictCursor as _AiomysqlDictCursor # pyright: ignore
from pymysql.constants import FIELD_TYPE as _PYMYSQL_FIELD_TYPE # pyright: ignore

if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
from types import TracebackType
from typing import Protocol, TypeAlias

from pymysql.err import Error as _PymysqlError
from pymysql.err import MySQLError as _PymysqlMySQLError

from sqlspec.adapters.aiomysql.driver import AiomysqlDriver
from sqlspec.core import StatementConfig

class AiomysqlConnectionProtocol(Protocol):
async def cursor(self, cursor: "type[AiomysqlRawCursor] | None" = None) -> AiomysqlRawCursor: ...
async def cursor(self, cursor: "type[AiomysqlRawCursor] | None" = None) -> "AiomysqlRawCursor": ...

async def commit(self) -> object: ...

Expand All @@ -38,21 +42,31 @@ def close(self) -> object: ...
def get_transaction_status(self) -> bool: ...

class AiomysqlModuleProtocol(Protocol):
async def create_pool(self, **kwargs: Any) -> AiomysqlPool: ...
async def create_pool(self, **kwargs: Any) -> "AiomysqlPool": ...

async def connect(self, **kwargs: Any) -> "AiomysqlConnection": ...

class AiomysqlFieldTypeProtocol(Protocol):
JSON: int

AiomysqlConnection: TypeAlias = AiomysqlConnectionProtocol
AiomysqlFieldType: TypeAlias = AiomysqlFieldTypeProtocol
AiomysqlModule: TypeAlias = AiomysqlModuleProtocol
AiomysqlRawCursor: TypeAlias = _AiomysqlCursor
AiomysqlDictCursor: TypeAlias = _AiomysqlDictCursor
AiomysqlFieldType: TypeAlias = AiomysqlFieldTypeProtocol
AiomysqlPool: TypeAlias = _AiomysqlPool
AiomysqlPymysqlError: TypeAlias = _PymysqlError
AiomysqlPymysqlMySQLError: TypeAlias = _PymysqlMySQLError

if not TYPE_CHECKING:
AiomysqlConnection = aiomysql.Connection
AiomysqlFieldType = pymysql.constants.FIELD_TYPE
AiomysqlModule = aiomysql
AiomysqlConnection = Connection
AiomysqlModule = _aiomysql
AiomysqlRawCursor = _AiomysqlCursor
AiomysqlDictCursor = _AiomysqlDictCursor
AiomysqlFieldType = _PYMYSQL_FIELD_TYPE
AiomysqlPool = _AiomysqlPool
AiomysqlPymysqlError = _AiomysqlError
AiomysqlPymysqlMySQLError = _AiomysqlMySQLError

__all__ = (
"AIOMYSQL_INSERT_VALUES_PATTERN",
Expand Down
9 changes: 3 additions & 6 deletions sqlspec/adapters/aiomysql/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""aiomysql database configuration."""

import asyncio
import contextlib
from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypedDict, cast
from weakref import WeakSet
Expand Down Expand Up @@ -182,7 +181,6 @@ def build_connection_config(
config.setdefault("host", "localhost")
config.setdefault("port", 3306)
config.setdefault("charset", "utf8mb4")
config.setdefault("pool_recycle", 300)
return _normalize_local_infile(config)


Expand All @@ -204,7 +202,7 @@ async def acquire_connection(self) -> "AiomysqlConnection":
try:
ensure_conn = self._config._ensure_connection
await ensure_conn(connection)
except Exception:
except BaseException:
self._contexts.pop(id(connection), None)
with contextlib.suppress(Exception):
await ctx.__aexit__(None, None, None)
Expand Down Expand Up @@ -242,7 +240,7 @@ async def __aenter__(self) -> AiomysqlConnection:
try:
ensure_conn = self._config._ensure_connection
await ensure_conn(connection)
except Exception:
except BaseException:
self._connection = None
self._ctx = None
with contextlib.suppress(Exception):
Expand Down Expand Up @@ -374,8 +372,7 @@ async def _close_pool(self) -> None:
"""Close the actual async connection pool."""
if self.connection_instance:
self.connection_instance.close()
with contextlib.suppress(Exception):
await asyncio.wait_for(self.connection_instance.wait_closed(), timeout=5.0)
await self.connection_instance.wait_closed()
self.connection_instance = None

async def create_connection(self) -> AiomysqlConnection:
Expand Down
Loading
Loading