sqlspec 0.7.1__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

Files changed (54) hide show
  1. sqlspec/__init__.py +15 -0
  2. sqlspec/_serialization.py +16 -2
  3. sqlspec/_typing.py +40 -7
  4. sqlspec/adapters/adbc/__init__.py +7 -0
  5. sqlspec/adapters/adbc/config.py +183 -17
  6. sqlspec/adapters/adbc/driver.py +392 -0
  7. sqlspec/adapters/aiosqlite/__init__.py +5 -1
  8. sqlspec/adapters/aiosqlite/config.py +24 -6
  9. sqlspec/adapters/aiosqlite/driver.py +264 -0
  10. sqlspec/adapters/asyncmy/__init__.py +7 -2
  11. sqlspec/adapters/asyncmy/config.py +71 -11
  12. sqlspec/adapters/asyncmy/driver.py +246 -0
  13. sqlspec/adapters/asyncpg/__init__.py +9 -0
  14. sqlspec/adapters/asyncpg/config.py +102 -25
  15. sqlspec/adapters/asyncpg/driver.py +444 -0
  16. sqlspec/adapters/duckdb/__init__.py +5 -1
  17. sqlspec/adapters/duckdb/config.py +194 -12
  18. sqlspec/adapters/duckdb/driver.py +225 -0
  19. sqlspec/adapters/oracledb/__init__.py +7 -4
  20. sqlspec/adapters/oracledb/config/__init__.py +4 -4
  21. sqlspec/adapters/oracledb/config/_asyncio.py +96 -12
  22. sqlspec/adapters/oracledb/config/_common.py +1 -1
  23. sqlspec/adapters/oracledb/config/_sync.py +96 -12
  24. sqlspec/adapters/oracledb/driver.py +571 -0
  25. sqlspec/adapters/psqlpy/__init__.py +0 -0
  26. sqlspec/adapters/psqlpy/config.py +258 -0
  27. sqlspec/adapters/psqlpy/driver.py +335 -0
  28. sqlspec/adapters/psycopg/__init__.py +16 -0
  29. sqlspec/adapters/psycopg/config/__init__.py +6 -6
  30. sqlspec/adapters/psycopg/config/_async.py +107 -15
  31. sqlspec/adapters/psycopg/config/_common.py +2 -2
  32. sqlspec/adapters/psycopg/config/_sync.py +107 -15
  33. sqlspec/adapters/psycopg/driver.py +578 -0
  34. sqlspec/adapters/sqlite/__init__.py +7 -0
  35. sqlspec/adapters/sqlite/config.py +24 -6
  36. sqlspec/adapters/sqlite/driver.py +305 -0
  37. sqlspec/base.py +565 -63
  38. sqlspec/exceptions.py +30 -0
  39. sqlspec/extensions/litestar/__init__.py +19 -0
  40. sqlspec/extensions/litestar/_utils.py +56 -0
  41. sqlspec/extensions/litestar/config.py +87 -0
  42. sqlspec/extensions/litestar/handlers.py +213 -0
  43. sqlspec/extensions/litestar/plugin.py +105 -11
  44. sqlspec/statement.py +373 -0
  45. sqlspec/typing.py +81 -17
  46. sqlspec/utils/__init__.py +3 -0
  47. sqlspec/utils/fixtures.py +4 -5
  48. sqlspec/utils/sync_tools.py +335 -0
  49. {sqlspec-0.7.1.dist-info → sqlspec-0.9.0.dist-info}/METADATA +4 -1
  50. sqlspec-0.9.0.dist-info/RECORD +61 -0
  51. sqlspec-0.7.1.dist-info/RECORD +0 -46
  52. {sqlspec-0.7.1.dist-info → sqlspec-0.9.0.dist-info}/WHEEL +0 -0
  53. {sqlspec-0.7.1.dist-info → sqlspec-0.9.0.dist-info}/licenses/LICENSE +0 -0
  54. {sqlspec-0.7.1.dist-info → sqlspec-0.9.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,13 +1,13 @@
1
1
  from contextlib import asynccontextmanager
2
- from dataclasses import dataclass
2
+ from dataclasses import dataclass, field
3
3
  from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
4
4
 
5
5
  from asyncpg import Record
6
6
  from asyncpg import create_pool as asyncpg_create_pool
7
- from asyncpg.pool import Pool, PoolConnectionProxy
8
- from typing_extensions import TypeAlias
7
+ from asyncpg.pool import PoolConnectionProxy
9
8
 
10
9
  from sqlspec._serialization import decode_json, encode_json
10
+ from sqlspec.adapters.asyncpg.driver import AsyncpgConnection, AsyncpgDriver
11
11
  from sqlspec.base import AsyncDatabaseConfig, GenericPoolConfig
12
12
  from sqlspec.exceptions import ImproperConfigurationError
13
13
  from sqlspec.typing import Empty, EmptyType, dataclass_to_dict
@@ -17,21 +17,20 @@ if TYPE_CHECKING:
17
17
  from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine
18
18
 
19
19
  from asyncpg.connection import Connection
20
+ from asyncpg.pool import Pool
20
21
 
21
22
 
22
23
  __all__ = (
23
- "AsyncPgConfig",
24
- "AsyncPgPoolConfig",
24
+ "AsyncpgConfig",
25
+ "AsyncpgPoolConfig",
25
26
  )
26
27
 
27
28
 
28
29
  T = TypeVar("T")
29
30
 
30
- PgConnection: TypeAlias = "Union[Connection, PoolConnectionProxy]" # pyright: ignore[reportMissingTypeArgument]
31
-
32
31
 
33
32
  @dataclass
34
- class AsyncPgPoolConfig(GenericPoolConfig):
33
+ class AsyncpgPoolConfig(GenericPoolConfig):
35
34
  """Configuration for Asyncpg's :class:`Pool <asyncpg.pool.Pool>`.
36
35
 
37
36
  For details see: https://magicstack.github.io/asyncpg/current/api/index.html#connection-pools
@@ -52,7 +51,7 @@ class AsyncPgPoolConfig(GenericPoolConfig):
52
51
  min_size: "Union[int, EmptyType]" = Empty
53
52
  """The number of connections to keep open inside the connection pool."""
54
53
  max_size: "Union[int, EmptyType]" = Empty
55
- """The number of connections to allow in connection pool overflow”, that is connections that can be opened above
54
+ """The number of connections to allow in connection pool "overflow", that is connections that can be opened above
56
55
  and beyond the pool_size setting, which defaults to 10."""
57
56
 
58
57
  max_queries: "Union[int, EmptyType]" = Empty
@@ -71,23 +70,55 @@ class AsyncPgPoolConfig(GenericPoolConfig):
71
70
 
72
71
 
73
72
  @dataclass
74
- class AsyncPgConfig(AsyncDatabaseConfig[PgConnection, Pool]): # pyright: ignore[reportMissingTypeArgument]
73
+ class AsyncpgConfig(AsyncDatabaseConfig["AsyncpgConnection", "Pool", "AsyncpgDriver"]): # pyright: ignore[reportMissingTypeArgument]
75
74
  """Asyncpg Configuration."""
76
75
 
77
- pool_config: "Optional[AsyncPgPoolConfig]" = None
76
+ pool_config: "Optional[AsyncpgPoolConfig]" = field(default=None)
78
77
  """Asyncpg Pool configuration"""
79
- json_deserializer: "Callable[[str], Any]" = decode_json
78
+ json_deserializer: "Callable[[str], Any]" = field(hash=False, default=decode_json)
80
79
  """For dialects that support the :class:`JSON <sqlalchemy.types.JSON>` datatype, this is a Python callable that will
81
80
  convert a JSON string to a Python object. By default, this is set to SQLSpec's
82
81
  :attr:`decode_json() <sqlspec._serialization.decode_json>` function."""
83
- json_serializer: "Callable[[Any], str]" = encode_json
82
+ json_serializer: "Callable[[Any], str]" = field(hash=False, default=encode_json)
84
83
  """For dialects that support the JSON datatype, this is a Python callable that will render a given object as JSON.
85
84
  By default, SQLSpec's :attr:`encode_json() <sqlspec._serialization.encode_json>` is used."""
86
- pool_instance: "Optional[Pool[Any]]" = None
87
- """Optional pool to use.
85
+ connection_type: "type[AsyncpgConnection]" = field(
86
+ hash=False, init=False, default_factory=lambda: PoolConnectionProxy
87
+ )
88
+ """Type of the connection object"""
89
+ driver_type: "type[AsyncpgDriver]" = field(hash=False, init=False, default_factory=lambda: AsyncpgDriver) # type: ignore[type-abstract,unused-ignore]
90
+ """Type of the driver object"""
91
+ pool_instance: "Optional[Pool[Any]]" = field(hash=False, default=None)
92
+ """The connection pool instance. If set, this will be used instead of creating a new pool."""
88
93
 
89
- If set, the plugin will use the provided pool rather than instantiate one.
90
- """
94
+ @property
95
+ def connection_config_dict(self) -> "dict[str, Any]":
96
+ """Return the connection configuration as a dict.
97
+
98
+ Returns:
99
+ A string keyed dict of config kwargs for the asyncpg.connect function.
100
+
101
+ Raises:
102
+ ImproperConfigurationError: If the connection configuration is not provided.
103
+ """
104
+ if self.pool_config:
105
+ connect_dict: dict[str, Any] = {}
106
+
107
+ # Add dsn if available
108
+ if hasattr(self.pool_config, "dsn"):
109
+ connect_dict["dsn"] = self.pool_config.dsn
110
+
111
+ # Add any connect_kwargs if available
112
+ if (
113
+ hasattr(self.pool_config, "connect_kwargs")
114
+ and self.pool_config.connect_kwargs is not Empty
115
+ and isinstance(self.pool_config.connect_kwargs, dict)
116
+ ):
117
+ connect_dict.update(dict(self.pool_config.connect_kwargs.items()))
118
+
119
+ return connect_dict
120
+ msg = "You must provide a 'pool_config' for this adapter."
121
+ raise ImproperConfigurationError(msg)
91
122
 
92
123
  @property
93
124
  def pool_config_dict(self) -> "dict[str, Any]":
@@ -96,9 +127,17 @@ class AsyncPgConfig(AsyncDatabaseConfig[PgConnection, Pool]): # pyright: ignore
96
127
  Returns:
97
128
  A string keyed dict of config kwargs for the Asyncpg :func:`create_pool <asyncpg.pool.create_pool>`
98
129
  function.
130
+
131
+ Raises:
132
+ ImproperConfigurationError: If no pool_config is provided but a pool_instance is set.
99
133
  """
100
134
  if self.pool_config:
101
- return dataclass_to_dict(self.pool_config, exclude_empty=True, convert_nested=False)
135
+ return dataclass_to_dict(
136
+ self.pool_config,
137
+ exclude_empty=True,
138
+ exclude={"pool_instance", "driver_type", "connection_type"},
139
+ convert_nested=False,
140
+ )
102
141
  msg = "'pool_config' methods can not be used when a 'pool_instance' is provided."
103
142
  raise ImproperConfigurationError(msg)
104
143
 
@@ -107,6 +146,10 @@ class AsyncPgConfig(AsyncDatabaseConfig[PgConnection, Pool]): # pyright: ignore
107
146
 
108
147
  Returns:
109
148
  Getter that returns the pool instance used by the plugin.
149
+
150
+ Raises:
151
+ ImproperConfigurationError: If neither pool_config nor pool_instance are provided,
152
+ or if the pool could not be configured.
110
153
  """
111
154
  if self.pool_instance is not None:
112
155
  return self.pool_instance
@@ -117,11 +160,9 @@ class AsyncPgConfig(AsyncDatabaseConfig[PgConnection, Pool]): # pyright: ignore
117
160
 
118
161
  pool_config = self.pool_config_dict
119
162
  self.pool_instance = await asyncpg_create_pool(**pool_config)
120
- if self.pool_instance is None:
121
- msg = "Could not configure the 'pool_instance'. Please check your configuration."
122
- raise ImproperConfigurationError(
123
- msg,
124
- )
163
+ if self.pool_instance is None: # pyright: ignore[reportUnnecessaryComparison]
164
+ msg = "Could not configure the 'pool_instance'. Please check your configuration." # type: ignore[unreachable]
165
+ raise ImproperConfigurationError(msg)
125
166
  return self.pool_instance
126
167
 
127
168
  def provide_pool(self, *args: "Any", **kwargs: "Any") -> "Awaitable[Pool]": # pyright: ignore[reportMissingTypeArgument,reportUnknownParameterType]
@@ -132,13 +173,49 @@ class AsyncPgConfig(AsyncDatabaseConfig[PgConnection, Pool]): # pyright: ignore
132
173
  """
133
174
  return self.create_pool() # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
134
175
 
176
+ async def create_connection(self) -> "AsyncpgConnection":
177
+ """Create and return a new asyncpg connection from the pool.
178
+
179
+ Returns:
180
+ A Connection instance.
181
+
182
+ Raises:
183
+ ImproperConfigurationError: If the connection could not be created.
184
+ """
185
+ try:
186
+ pool = await self.provide_pool()
187
+ return await pool.acquire()
188
+ except Exception as e:
189
+ msg = f"Could not configure the asyncpg connection. Error: {e!s}"
190
+ raise ImproperConfigurationError(msg) from e
191
+
135
192
  @asynccontextmanager
136
- async def provide_connection(self, *args: "Any", **kwargs: "Any") -> "AsyncGenerator[PoolConnectionProxy, None]": # pyright: ignore[reportMissingTypeArgument,reportUnknownParameterType]
193
+ async def provide_connection(
194
+ self, *args: "Any", **kwargs: "Any"
195
+ ) -> "AsyncGenerator[PoolConnectionProxy[Any], None]": # pyright: ignore[reportMissingTypeArgument,reportUnknownParameterType]
137
196
  """Create a connection instance.
138
197
 
139
- Returns:
198
+ Yields:
140
199
  A connection instance.
141
200
  """
142
201
  db_pool = await self.provide_pool(*args, **kwargs) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
143
202
  async with db_pool.acquire() as connection: # pyright: ignore[reportUnknownVariableType]
144
203
  yield connection
204
+
205
+ async def close_pool(self) -> None:
206
+ """Close the pool."""
207
+ if self.pool_instance is not None:
208
+ await self.pool_instance.close()
209
+ self.pool_instance = None
210
+
211
+ @asynccontextmanager
212
+ async def provide_session(self, *args: Any, **kwargs: Any) -> "AsyncGenerator[AsyncpgDriver, None]":
213
+ """Create and provide a database session.
214
+
215
+ Yields:
216
+ A Aiosqlite driver instance.
217
+
218
+
219
+ """
220
+ async with self.provide_connection(*args, **kwargs) as connection:
221
+ yield self.driver_type(connection)
@@ -0,0 +1,444 @@
1
+ import logging
2
+ import re
3
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
4
+
5
+ from asyncpg import Connection
6
+ from typing_extensions import TypeAlias
7
+
8
+ from sqlspec.base import AsyncDriverAdapterProtocol, T
9
+ from sqlspec.exceptions import SQLParsingError
10
+ from sqlspec.statement import PARAM_REGEX, SQLStatement
11
+
12
+ if TYPE_CHECKING:
13
+ from asyncpg.connection import Connection
14
+ from asyncpg.pool import PoolConnectionProxy
15
+
16
+ from sqlspec.typing import ModelDTOT, StatementParameterType
17
+
18
+ __all__ = ("AsyncpgConnection", "AsyncpgDriver")
19
+
20
+ logger = logging.getLogger("sqlspec")
21
+
22
+ # Regex to find '?' placeholders, skipping those inside quotes or SQL comments
23
+ # Simplified version, assumes standard SQL quoting/comments
24
+ QMARK_REGEX = re.compile(
25
+ r"""(?P<dquote>"[^"]*") | # Double-quoted strings
26
+ (?P<squote>\'[^\']*\') | # Single-quoted strings
27
+ (?P<comment>--[^\n]*|/\*.*?\*/) | # SQL comments (single/multi-line)
28
+ (?P<qmark>\?) # The question mark placeholder
29
+ """,
30
+ re.VERBOSE | re.DOTALL,
31
+ )
32
+
33
+ AsyncpgConnection: TypeAlias = "Union[Connection[Any], PoolConnectionProxy[Any]]" # pyright: ignore[reportMissingTypeArgument]
34
+
35
+
36
+ class AsyncpgDriver(AsyncDriverAdapterProtocol["AsyncpgConnection"]):
37
+ """AsyncPG Postgres Driver Adapter."""
38
+
39
+ connection: "AsyncpgConnection"
40
+ dialect: str = "postgres"
41
+
42
+ def __init__(self, connection: "AsyncpgConnection") -> None:
43
+ self.connection = connection
44
+
45
+ def _process_sql_params( # noqa: C901, PLR0912, PLR0915
46
+ self,
47
+ sql: str,
48
+ parameters: "Optional[StatementParameterType]" = None,
49
+ /,
50
+ **kwargs: Any,
51
+ ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
52
+ # Use SQLStatement for parameter validation and merging first
53
+ # It also handles potential dialect-specific logic if implemented there.
54
+ stmt = SQLStatement(sql=sql, parameters=parameters, dialect=self.dialect, kwargs=kwargs or None)
55
+ sql, parameters = stmt.process()
56
+
57
+ # Case 1: Parameters are effectively a dictionary (either passed as dict or via kwargs merged by SQLStatement)
58
+ if isinstance(parameters, dict):
59
+ processed_sql_parts: list[str] = []
60
+ ordered_params = []
61
+ last_end = 0
62
+ param_index = 1
63
+ found_params_regex: list[str] = []
64
+
65
+ # Manually parse the PROCESSED SQL for :name -> $n conversion
66
+ for match in PARAM_REGEX.finditer(sql):
67
+ # Skip matches inside quotes or comments
68
+ if match.group("dquote") or match.group("squote") or match.group("comment"):
69
+ continue
70
+
71
+ if match.group("var_name"): # Finds :var_name
72
+ var_name = match.group("var_name")
73
+ found_params_regex.append(var_name)
74
+ start = match.start("var_name") - 1 # Include the ':'
75
+ end = match.end("var_name")
76
+
77
+ # SQLStatement should have already validated parameter existence,
78
+ # but we double-check here during ordering.
79
+ if var_name not in parameters:
80
+ # This should ideally not happen if SQLStatement validation is robust.
81
+ msg = (
82
+ f"Named parameter ':{var_name}' found in SQL but missing from processed parameters. "
83
+ f"Processed SQL: {sql}"
84
+ )
85
+ raise SQLParsingError(msg)
86
+
87
+ processed_sql_parts.extend((sql[last_end:start], f"${param_index}"))
88
+ ordered_params.append(parameters[var_name])
89
+ last_end = end
90
+ param_index += 1
91
+
92
+ processed_sql_parts.append(sql[last_end:])
93
+ final_sql = "".join(processed_sql_parts)
94
+
95
+ # --- Validation ---
96
+ # Check if named placeholders were found if dict params were provided
97
+ # SQLStatement might handle this validation, but a warning here can be useful.
98
+ if not found_params_regex and parameters:
99
+ logger.warning(
100
+ "Dict params provided (%s), but no :name placeholders found. SQL: %s",
101
+ list(parameters.keys()),
102
+ sql,
103
+ )
104
+ # If no placeholders, return original SQL from SQLStatement and empty tuple for asyncpg
105
+ return sql, ()
106
+
107
+ # Additional checks (potentially redundant if SQLStatement covers them):
108
+ # 1. Ensure all found placeholders have corresponding params (covered by check inside loop)
109
+ # 2. Ensure all provided params correspond to a placeholder
110
+ provided_keys = set(parameters.keys())
111
+ found_keys = set(found_params_regex)
112
+ unused_keys = provided_keys - found_keys
113
+ if unused_keys:
114
+ # SQLStatement might handle this, but log a warning just in case.
115
+ logger.warning(
116
+ "Parameters provided but not used in SQL: %s. SQL: %s",
117
+ unused_keys,
118
+ sql,
119
+ )
120
+
121
+ return final_sql, tuple(ordered_params) # asyncpg expects a sequence
122
+
123
+ # Case 2: Parameters are effectively a sequence/scalar (merged by SQLStatement)
124
+ if isinstance(parameters, (list, tuple)):
125
+ # Parameters are a sequence, need to convert ? -> $n
126
+ sequence_processed_parts: list[str] = []
127
+ param_index = 1
128
+ last_end = 0
129
+ qmark_found = False
130
+
131
+ # Manually parse the PROCESSED SQL to find '?' outside comments/quotes and convert to $n
132
+ for match in QMARK_REGEX.finditer(sql):
133
+ if match.group("dquote") or match.group("squote") or match.group("comment"):
134
+ continue # Skip quotes and comments
135
+
136
+ if match.group("qmark"):
137
+ qmark_found = True
138
+ start = match.start("qmark")
139
+ end = match.end("qmark")
140
+ sequence_processed_parts.extend((sql[last_end:start], f"${param_index}"))
141
+ last_end = end
142
+ param_index += 1
143
+
144
+ sequence_processed_parts.append(sql[last_end:])
145
+ final_sql = "".join(sequence_processed_parts)
146
+
147
+ # --- Validation ---
148
+ # Check if '?' was found if parameters were provided
149
+ if parameters and not qmark_found:
150
+ # SQLStatement might allow this, log a warning.
151
+ logger.warning(
152
+ "Sequence/scalar parameters provided, but no '?' placeholders found. SQL: %s",
153
+ sql,
154
+ )
155
+ # Return PROCESSED SQL from SQLStatement as no conversion happened here
156
+ return sql, parameters
157
+
158
+ # Check parameter count match (using count from manual parsing vs count from stmt)
159
+ expected_params = param_index - 1
160
+ actual_params = len(parameters)
161
+ if expected_params != actual_params:
162
+ msg = (
163
+ f"Parameter count mismatch: Processed SQL expected {expected_params} parameters ('$n'), "
164
+ f"but {actual_params} were provided by SQLStatement. "
165
+ f"Final Processed SQL: {final_sql}"
166
+ )
167
+ raise SQLParsingError(msg)
168
+
169
+ return final_sql, parameters
170
+
171
+ # Case 3: Parameters are None (as determined by SQLStatement)
172
+ # processed_params is None
173
+ # Check if the SQL contains any placeholders unexpectedly
174
+ # Check for :name style
175
+ named_placeholders_found = False
176
+ for match in PARAM_REGEX.finditer(sql):
177
+ if not (match.group("dquote") or match.group("squote") or match.group("comment")) and match.group(
178
+ "var_name"
179
+ ):
180
+ named_placeholders_found = True
181
+ break
182
+ if named_placeholders_found:
183
+ msg = f"Processed SQL contains named parameters (:name) but no parameters were provided. SQL: {sql}"
184
+ raise SQLParsingError(msg)
185
+
186
+ # Check for ? style
187
+ qmark_placeholders_found = False
188
+ for match in QMARK_REGEX.finditer(sql):
189
+ if not (match.group("dquote") or match.group("squote") or match.group("comment")) and match.group("qmark"):
190
+ qmark_placeholders_found = True
191
+ break
192
+ if qmark_placeholders_found:
193
+ msg = f"Processed SQL contains positional parameters (?) but no parameters were provided. SQL: {sql}"
194
+ raise SQLParsingError(msg)
195
+
196
+ # No parameters provided and none found in SQL, return original SQL from SQLStatement and empty tuple
197
+ return sql, () # asyncpg expects a sequence, even if empty
198
+
199
+ async def select(
200
+ self,
201
+ sql: str,
202
+ parameters: Optional["StatementParameterType"] = None,
203
+ /,
204
+ *,
205
+ connection: Optional["AsyncpgConnection"] = None,
206
+ schema_type: "Optional[type[ModelDTOT]]" = None,
207
+ **kwargs: Any,
208
+ ) -> "list[Union[ModelDTOT, dict[str, Any]]]":
209
+ """Fetch data from the database.
210
+
211
+ Args:
212
+ sql: SQL statement.
213
+ parameters: Query parameters.
214
+ connection: Optional connection to use.
215
+ schema_type: Optional schema class for the result.
216
+ **kwargs: Additional keyword arguments.
217
+
218
+ Returns:
219
+ List of row data as either model instances or dictionaries.
220
+ """
221
+ connection = self._connection(connection)
222
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
223
+ parameters = parameters if parameters is not None else {}
224
+
225
+ results = await connection.fetch(sql, *parameters) # pyright: ignore
226
+ if not results:
227
+ return []
228
+ if schema_type is None:
229
+ return [dict(row.items()) for row in results] # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
230
+ return [cast("ModelDTOT", schema_type(**dict(row.items()))) for row in results] # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
231
+
232
+ async def select_one(
233
+ self,
234
+ sql: str,
235
+ parameters: Optional["StatementParameterType"] = None,
236
+ /,
237
+ *,
238
+ connection: Optional["AsyncpgConnection"] = None,
239
+ schema_type: "Optional[type[ModelDTOT]]" = None,
240
+ **kwargs: Any,
241
+ ) -> "Union[ModelDTOT, dict[str, Any]]":
242
+ """Fetch one row from the database.
243
+
244
+ Args:
245
+ sql: SQL statement.
246
+ parameters: Query parameters.
247
+ connection: Optional connection to use.
248
+ schema_type: Optional schema class for the result.
249
+ **kwargs: Additional keyword arguments.
250
+
251
+ Returns:
252
+ The first row of the query results.
253
+ """
254
+ connection = self._connection(connection)
255
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
256
+ parameters = parameters if parameters is not None else {}
257
+ result = await connection.fetchrow(sql, *parameters) # pyright: ignore
258
+ result = self.check_not_found(result)
259
+
260
+ if schema_type is None:
261
+ # Always return as dictionary
262
+ return dict(result.items()) # type: ignore[attr-defined]
263
+ return cast("ModelDTOT", schema_type(**dict(result.items()))) # type: ignore[attr-defined]
264
+
265
+ async def select_one_or_none(
266
+ self,
267
+ sql: str,
268
+ parameters: Optional["StatementParameterType"] = None,
269
+ /,
270
+ *,
271
+ connection: Optional["AsyncpgConnection"] = None,
272
+ schema_type: "Optional[type[ModelDTOT]]" = None,
273
+ **kwargs: Any,
274
+ ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
275
+ """Fetch one row from the database.
276
+
277
+ Args:
278
+ sql: SQL statement.
279
+ parameters: Query parameters.
280
+ connection: Optional connection to use.
281
+ schema_type: Optional schema class for the result.
282
+ **kwargs: Additional keyword arguments.
283
+
284
+ Returns:
285
+ The first row of the query results.
286
+ """
287
+ connection = self._connection(connection)
288
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
289
+ parameters = parameters if parameters is not None else {}
290
+ result = await connection.fetchrow(sql, *parameters) # pyright: ignore
291
+ if result is None:
292
+ return None
293
+ if schema_type is None:
294
+ # Always return as dictionary
295
+ return dict(result.items())
296
+ return cast("ModelDTOT", schema_type(**dict(result.items())))
297
+
298
+ async def select_value(
299
+ self,
300
+ sql: str,
301
+ parameters: "Optional[StatementParameterType]" = None,
302
+ /,
303
+ *,
304
+ connection: "Optional[AsyncpgConnection]" = None,
305
+ schema_type: "Optional[type[T]]" = None,
306
+ **kwargs: Any,
307
+ ) -> "Union[T, Any]":
308
+ """Fetch a single value from the database.
309
+
310
+ Args:
311
+ sql: SQL statement.
312
+ parameters: Query parameters.
313
+ connection: Optional connection to use.
314
+ schema_type: Optional schema class for the result.
315
+ **kwargs: Additional keyword arguments.
316
+
317
+ Returns:
318
+ The first value from the first row of results, or None if no results.
319
+ """
320
+ connection = self._connection(connection)
321
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
322
+ parameters = parameters if parameters is not None else {}
323
+ result = await connection.fetchval(sql, *parameters) # pyright: ignore
324
+ result = self.check_not_found(result)
325
+ if schema_type is None:
326
+ return result
327
+ return schema_type(result) # type: ignore[call-arg]
328
+
329
+ async def select_value_or_none(
330
+ self,
331
+ sql: str,
332
+ parameters: "Optional[StatementParameterType]" = None,
333
+ /,
334
+ *,
335
+ connection: "Optional[AsyncpgConnection]" = None,
336
+ schema_type: "Optional[type[T]]" = None,
337
+ **kwargs: Any,
338
+ ) -> "Optional[Union[T, Any]]":
339
+ """Fetch a single value from the database.
340
+
341
+ Returns:
342
+ The first value from the first row of results, or None if no results.
343
+ """
344
+ connection = self._connection(connection)
345
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
346
+ parameters = parameters if parameters is not None else {}
347
+ result = await connection.fetchval(sql, *parameters) # pyright: ignore
348
+ if result is None:
349
+ return None
350
+ if schema_type is None:
351
+ return result
352
+ return schema_type(result) # type: ignore[call-arg]
353
+
354
+ async def insert_update_delete(
355
+ self,
356
+ sql: str,
357
+ parameters: Optional["StatementParameterType"] = None,
358
+ /,
359
+ *,
360
+ connection: Optional["AsyncpgConnection"] = None,
361
+ **kwargs: Any,
362
+ ) -> int:
363
+ """Insert, update, or delete data from the database.
364
+
365
+ Args:
366
+ sql: SQL statement.
367
+ parameters: Query parameters.
368
+ connection: Optional connection to use.
369
+ **kwargs: Additional keyword arguments.
370
+
371
+ Returns:
372
+ Row count affected by the operation.
373
+ """
374
+ connection = self._connection(connection)
375
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
376
+ parameters = parameters if parameters is not None else {}
377
+ status = await connection.execute(sql, *parameters) # pyright: ignore
378
+ # AsyncPG returns a string like "INSERT 0 1" where the last number is the affected rows
379
+ try:
380
+ return int(status.split()[-1]) # pyright: ignore[reportUnknownMemberType]
381
+ except (ValueError, IndexError, AttributeError):
382
+ return -1 # Fallback if we can't parse the status
383
+
384
+ async def insert_update_delete_returning(
385
+ self,
386
+ sql: str,
387
+ parameters: Optional["StatementParameterType"] = None,
388
+ /,
389
+ *,
390
+ connection: Optional["AsyncpgConnection"] = None,
391
+ schema_type: "Optional[type[ModelDTOT]]" = None,
392
+ **kwargs: Any,
393
+ ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
394
+ """Insert, update, or delete data from the database and return the affected row.
395
+
396
+ Args:
397
+ sql: SQL statement.
398
+ parameters: Query parameters.
399
+ connection: Optional connection to use.
400
+ schema_type: Optional schema class for the result.
401
+ **kwargs: Additional keyword arguments.
402
+
403
+ Returns:
404
+ The affected row data as either a model instance or dictionary.
405
+ """
406
+ connection = self._connection(connection)
407
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
408
+ parameters = parameters if parameters is not None else {}
409
+ result = await connection.fetchrow(sql, *parameters) # pyright: ignore
410
+ if result is None:
411
+ return None
412
+ if schema_type is None:
413
+ # Always return as dictionary
414
+ return dict(result.items()) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
415
+ return cast("ModelDTOT", schema_type(**dict(result.items()))) # pyright: ignore[reportUnknownArgumentType, reportUnknownMemberType, reportUnknownVariableType]
416
+
417
+ async def execute_script(
418
+ self,
419
+ sql: str,
420
+ parameters: Optional["StatementParameterType"] = None,
421
+ /,
422
+ *,
423
+ connection: Optional["AsyncpgConnection"] = None,
424
+ **kwargs: Any,
425
+ ) -> str:
426
+ """Execute a script.
427
+
428
+ Args:
429
+ sql: SQL statement.
430
+ parameters: Query parameters.
431
+ connection: Optional connection to use.
432
+ **kwargs: Additional keyword arguments.
433
+
434
+ Returns:
435
+ Status message for the operation.
436
+ """
437
+ connection = self._connection(connection)
438
+ sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
439
+ parameters = parameters if parameters is not None else {}
440
+ return await connection.execute(sql, *parameters) # pyright: ignore
441
+
442
+ def _connection(self, connection: Optional["AsyncpgConnection"] = None) -> "AsyncpgConnection":
443
+ """Return the connection to use. If None, use the default connection."""
444
+ return connection if connection is not None else self.connection
@@ -1,3 +1,7 @@
1
1
  from sqlspec.adapters.duckdb.config import DuckDBConfig
2
+ from sqlspec.adapters.duckdb.driver import DuckDBDriver
2
3
 
3
- __all__ = ("DuckDBConfig",)
4
+ __all__ = (
5
+ "DuckDBConfig",
6
+ "DuckDBDriver",
7
+ )