sqlspec 0.11.0__py3-none-any.whl → 0.12.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 (155) hide show
  1. sqlspec/__init__.py +16 -3
  2. sqlspec/_serialization.py +3 -10
  3. sqlspec/_sql.py +1147 -0
  4. sqlspec/_typing.py +343 -41
  5. sqlspec/adapters/adbc/__init__.py +2 -6
  6. sqlspec/adapters/adbc/config.py +474 -149
  7. sqlspec/adapters/adbc/driver.py +330 -644
  8. sqlspec/adapters/aiosqlite/__init__.py +2 -6
  9. sqlspec/adapters/aiosqlite/config.py +143 -57
  10. sqlspec/adapters/aiosqlite/driver.py +269 -462
  11. sqlspec/adapters/asyncmy/__init__.py +3 -8
  12. sqlspec/adapters/asyncmy/config.py +247 -202
  13. sqlspec/adapters/asyncmy/driver.py +217 -451
  14. sqlspec/adapters/asyncpg/__init__.py +4 -7
  15. sqlspec/adapters/asyncpg/config.py +329 -176
  16. sqlspec/adapters/asyncpg/driver.py +418 -498
  17. sqlspec/adapters/bigquery/__init__.py +2 -2
  18. sqlspec/adapters/bigquery/config.py +407 -0
  19. sqlspec/adapters/bigquery/driver.py +592 -634
  20. sqlspec/adapters/duckdb/__init__.py +4 -1
  21. sqlspec/adapters/duckdb/config.py +432 -321
  22. sqlspec/adapters/duckdb/driver.py +393 -436
  23. sqlspec/adapters/oracledb/__init__.py +3 -8
  24. sqlspec/adapters/oracledb/config.py +625 -0
  25. sqlspec/adapters/oracledb/driver.py +549 -942
  26. sqlspec/adapters/psqlpy/__init__.py +4 -7
  27. sqlspec/adapters/psqlpy/config.py +372 -203
  28. sqlspec/adapters/psqlpy/driver.py +197 -550
  29. sqlspec/adapters/psycopg/__init__.py +3 -8
  30. sqlspec/adapters/psycopg/config.py +741 -0
  31. sqlspec/adapters/psycopg/driver.py +732 -733
  32. sqlspec/adapters/sqlite/__init__.py +2 -6
  33. sqlspec/adapters/sqlite/config.py +146 -81
  34. sqlspec/adapters/sqlite/driver.py +243 -426
  35. sqlspec/base.py +220 -825
  36. sqlspec/config.py +354 -0
  37. sqlspec/driver/__init__.py +22 -0
  38. sqlspec/driver/_async.py +252 -0
  39. sqlspec/driver/_common.py +338 -0
  40. sqlspec/driver/_sync.py +261 -0
  41. sqlspec/driver/mixins/__init__.py +17 -0
  42. sqlspec/driver/mixins/_pipeline.py +523 -0
  43. sqlspec/driver/mixins/_result_utils.py +122 -0
  44. sqlspec/driver/mixins/_sql_translator.py +35 -0
  45. sqlspec/driver/mixins/_storage.py +993 -0
  46. sqlspec/driver/mixins/_type_coercion.py +131 -0
  47. sqlspec/exceptions.py +299 -7
  48. sqlspec/extensions/aiosql/__init__.py +10 -0
  49. sqlspec/extensions/aiosql/adapter.py +474 -0
  50. sqlspec/extensions/litestar/__init__.py +1 -6
  51. sqlspec/extensions/litestar/_utils.py +1 -5
  52. sqlspec/extensions/litestar/config.py +5 -6
  53. sqlspec/extensions/litestar/handlers.py +13 -12
  54. sqlspec/extensions/litestar/plugin.py +22 -24
  55. sqlspec/extensions/litestar/providers.py +37 -55
  56. sqlspec/loader.py +528 -0
  57. sqlspec/service/__init__.py +3 -0
  58. sqlspec/service/base.py +24 -0
  59. sqlspec/service/pagination.py +26 -0
  60. sqlspec/statement/__init__.py +21 -0
  61. sqlspec/statement/builder/__init__.py +54 -0
  62. sqlspec/statement/builder/_ddl_utils.py +119 -0
  63. sqlspec/statement/builder/_parsing_utils.py +135 -0
  64. sqlspec/statement/builder/base.py +328 -0
  65. sqlspec/statement/builder/ddl.py +1379 -0
  66. sqlspec/statement/builder/delete.py +80 -0
  67. sqlspec/statement/builder/insert.py +274 -0
  68. sqlspec/statement/builder/merge.py +95 -0
  69. sqlspec/statement/builder/mixins/__init__.py +65 -0
  70. sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
  71. sqlspec/statement/builder/mixins/_case_builder.py +91 -0
  72. sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
  73. sqlspec/statement/builder/mixins/_delete_from.py +34 -0
  74. sqlspec/statement/builder/mixins/_from.py +61 -0
  75. sqlspec/statement/builder/mixins/_group_by.py +119 -0
  76. sqlspec/statement/builder/mixins/_having.py +35 -0
  77. sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
  78. sqlspec/statement/builder/mixins/_insert_into.py +36 -0
  79. sqlspec/statement/builder/mixins/_insert_values.py +69 -0
  80. sqlspec/statement/builder/mixins/_join.py +110 -0
  81. sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
  82. sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
  83. sqlspec/statement/builder/mixins/_order_by.py +46 -0
  84. sqlspec/statement/builder/mixins/_pivot.py +82 -0
  85. sqlspec/statement/builder/mixins/_returning.py +37 -0
  86. sqlspec/statement/builder/mixins/_select_columns.py +60 -0
  87. sqlspec/statement/builder/mixins/_set_ops.py +122 -0
  88. sqlspec/statement/builder/mixins/_unpivot.py +80 -0
  89. sqlspec/statement/builder/mixins/_update_from.py +54 -0
  90. sqlspec/statement/builder/mixins/_update_set.py +91 -0
  91. sqlspec/statement/builder/mixins/_update_table.py +29 -0
  92. sqlspec/statement/builder/mixins/_where.py +374 -0
  93. sqlspec/statement/builder/mixins/_window_functions.py +86 -0
  94. sqlspec/statement/builder/protocols.py +20 -0
  95. sqlspec/statement/builder/select.py +206 -0
  96. sqlspec/statement/builder/update.py +178 -0
  97. sqlspec/statement/filters.py +571 -0
  98. sqlspec/statement/parameters.py +736 -0
  99. sqlspec/statement/pipelines/__init__.py +67 -0
  100. sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
  101. sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
  102. sqlspec/statement/pipelines/base.py +315 -0
  103. sqlspec/statement/pipelines/context.py +119 -0
  104. sqlspec/statement/pipelines/result_types.py +41 -0
  105. sqlspec/statement/pipelines/transformers/__init__.py +8 -0
  106. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
  107. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
  108. sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
  109. sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
  110. sqlspec/statement/pipelines/validators/__init__.py +23 -0
  111. sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
  112. sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
  113. sqlspec/statement/pipelines/validators/_performance.py +703 -0
  114. sqlspec/statement/pipelines/validators/_security.py +990 -0
  115. sqlspec/statement/pipelines/validators/base.py +67 -0
  116. sqlspec/statement/result.py +527 -0
  117. sqlspec/statement/splitter.py +701 -0
  118. sqlspec/statement/sql.py +1198 -0
  119. sqlspec/storage/__init__.py +15 -0
  120. sqlspec/storage/backends/__init__.py +0 -0
  121. sqlspec/storage/backends/base.py +166 -0
  122. sqlspec/storage/backends/fsspec.py +315 -0
  123. sqlspec/storage/backends/obstore.py +464 -0
  124. sqlspec/storage/protocol.py +170 -0
  125. sqlspec/storage/registry.py +315 -0
  126. sqlspec/typing.py +157 -36
  127. sqlspec/utils/correlation.py +155 -0
  128. sqlspec/utils/deprecation.py +3 -6
  129. sqlspec/utils/fixtures.py +6 -11
  130. sqlspec/utils/logging.py +135 -0
  131. sqlspec/utils/module_loader.py +45 -43
  132. sqlspec/utils/serializers.py +4 -0
  133. sqlspec/utils/singleton.py +6 -8
  134. sqlspec/utils/sync_tools.py +15 -27
  135. sqlspec/utils/text.py +58 -26
  136. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/METADATA +100 -26
  137. sqlspec-0.12.0.dist-info/RECORD +145 -0
  138. sqlspec/adapters/bigquery/config/__init__.py +0 -3
  139. sqlspec/adapters/bigquery/config/_common.py +0 -40
  140. sqlspec/adapters/bigquery/config/_sync.py +0 -87
  141. sqlspec/adapters/oracledb/config/__init__.py +0 -9
  142. sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
  143. sqlspec/adapters/oracledb/config/_common.py +0 -131
  144. sqlspec/adapters/oracledb/config/_sync.py +0 -186
  145. sqlspec/adapters/psycopg/config/__init__.py +0 -19
  146. sqlspec/adapters/psycopg/config/_async.py +0 -169
  147. sqlspec/adapters/psycopg/config/_common.py +0 -56
  148. sqlspec/adapters/psycopg/config/_sync.py +0 -168
  149. sqlspec/filters.py +0 -330
  150. sqlspec/mixins.py +0 -306
  151. sqlspec/statement.py +0 -378
  152. sqlspec-0.11.0.dist-info/RECORD +0 -69
  153. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/WHEEL +0 -0
  154. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/licenses/LICENSE +0 -0
  155. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,741 @@
1
+ """Psycopg database configuration with direct field-based configuration."""
2
+
3
+ import contextlib
4
+ import logging
5
+ from contextlib import asynccontextmanager
6
+ from dataclasses import replace
7
+ from typing import TYPE_CHECKING, Any, ClassVar, Optional, cast
8
+
9
+ from psycopg.rows import dict_row
10
+ from psycopg_pool import AsyncConnectionPool, ConnectionPool
11
+
12
+ from sqlspec.adapters.psycopg.driver import (
13
+ PsycopgAsyncConnection,
14
+ PsycopgAsyncDriver,
15
+ PsycopgSyncConnection,
16
+ PsycopgSyncDriver,
17
+ )
18
+ from sqlspec.config import AsyncDatabaseConfig, SyncDatabaseConfig
19
+ from sqlspec.statement.sql import SQLConfig
20
+ from sqlspec.typing import DictRow, Empty
21
+
22
+ if TYPE_CHECKING:
23
+ from collections.abc import AsyncGenerator, Callable, Generator
24
+
25
+ from psycopg import Connection
26
+ from sqlglot.dialects.dialect import DialectType
27
+
28
+ logger = logging.getLogger("sqlspec.adapters.psycopg")
29
+
30
+ CONNECTION_FIELDS = frozenset(
31
+ {
32
+ "conninfo",
33
+ "host",
34
+ "port",
35
+ "user",
36
+ "password",
37
+ "dbname",
38
+ "connect_timeout",
39
+ "options",
40
+ "application_name",
41
+ "sslmode",
42
+ "sslcert",
43
+ "sslkey",
44
+ "sslrootcert",
45
+ "autocommit",
46
+ }
47
+ )
48
+
49
+ POOL_FIELDS = CONNECTION_FIELDS.union(
50
+ {
51
+ "min_size",
52
+ "max_size",
53
+ "name",
54
+ "timeout",
55
+ "max_waiting",
56
+ "max_lifetime",
57
+ "max_idle",
58
+ "reconnect_timeout",
59
+ "num_workers",
60
+ "configure",
61
+ "kwargs",
62
+ }
63
+ )
64
+
65
+ __all__ = ("CONNECTION_FIELDS", "POOL_FIELDS", "PsycopgAsyncConfig", "PsycopgSyncConfig")
66
+
67
+
68
+ class PsycopgSyncConfig(SyncDatabaseConfig[PsycopgSyncConnection, ConnectionPool, PsycopgSyncDriver]):
69
+ """Configuration for Psycopg synchronous database connections with direct field-based configuration."""
70
+
71
+ __slots__ = (
72
+ "_dialect",
73
+ "application_name",
74
+ "autocommit",
75
+ "configure",
76
+ "connect_timeout",
77
+ "conninfo",
78
+ "dbname",
79
+ "default_row_type",
80
+ "extras",
81
+ "host",
82
+ "kwargs",
83
+ "max_idle",
84
+ "max_lifetime",
85
+ "max_size",
86
+ "max_waiting",
87
+ "min_size",
88
+ "name",
89
+ "num_workers",
90
+ "options",
91
+ "password",
92
+ "pool_instance",
93
+ "port",
94
+ "reconnect_timeout",
95
+ "sslcert",
96
+ "sslkey",
97
+ "sslmode",
98
+ "sslrootcert",
99
+ "statement_config",
100
+ "timeout",
101
+ "user",
102
+ )
103
+
104
+ is_async: ClassVar[bool] = False
105
+ supports_connection_pooling: ClassVar[bool] = True
106
+
107
+ # Driver class reference for dialect resolution
108
+ driver_type: type[PsycopgSyncDriver] = PsycopgSyncDriver
109
+ connection_type: type[PsycopgSyncConnection] = PsycopgSyncConnection
110
+ # Parameter style support information
111
+ supported_parameter_styles: ClassVar[tuple[str, ...]] = ("pyformat_positional", "pyformat_named")
112
+ """Psycopg supports %s (positional) and %(name)s (named) parameter styles."""
113
+
114
+ preferred_parameter_style: ClassVar[str] = "pyformat_positional"
115
+ """Psycopg's native parameter style is %s (pyformat positional)."""
116
+
117
+ def __init__(
118
+ self,
119
+ statement_config: "Optional[SQLConfig]" = None,
120
+ default_row_type: "type[DictRow]" = DictRow,
121
+ # Connection parameters
122
+ conninfo: Optional[str] = None,
123
+ host: Optional[str] = None,
124
+ port: Optional[int] = None,
125
+ user: Optional[str] = None,
126
+ password: Optional[str] = None,
127
+ dbname: Optional[str] = None,
128
+ connect_timeout: Optional[float] = None,
129
+ options: Optional[str] = None,
130
+ application_name: Optional[str] = None,
131
+ sslmode: Optional[str] = None,
132
+ sslcert: Optional[str] = None,
133
+ sslkey: Optional[str] = None,
134
+ sslrootcert: Optional[str] = None,
135
+ autocommit: Optional[bool] = None,
136
+ # Pool parameters
137
+ min_size: Optional[int] = None,
138
+ max_size: Optional[int] = None,
139
+ name: Optional[str] = None,
140
+ timeout: Optional[float] = None,
141
+ max_waiting: Optional[int] = None,
142
+ max_lifetime: Optional[float] = None,
143
+ max_idle: Optional[float] = None,
144
+ reconnect_timeout: Optional[float] = None,
145
+ num_workers: Optional[int] = None,
146
+ configure: Optional["Callable[[Connection[Any]], None]"] = None,
147
+ kwargs: Optional[dict[str, Any]] = None,
148
+ # User-defined extras
149
+ extras: Optional[dict[str, Any]] = None,
150
+ **additional_kwargs: Any,
151
+ ) -> None:
152
+ """Initialize Psycopg synchronous configuration.
153
+
154
+ Args:
155
+ statement_config: Default SQL statement configuration
156
+ default_row_type: Default row type for results
157
+ conninfo: Connection string in libpq format
158
+ host: Database server host
159
+ port: Database server port
160
+ user: Database user
161
+ password: Database password
162
+ dbname: Database name
163
+ connect_timeout: Connection timeout in seconds
164
+ options: Command-line options to send to the server
165
+ application_name: Application name for logging and statistics
166
+ sslmode: SSL mode (disable, prefer, require, etc.)
167
+ sslcert: SSL client certificate file
168
+ sslkey: SSL client private key file
169
+ sslrootcert: SSL root certificate file
170
+ autocommit: Enable autocommit mode
171
+ min_size: Minimum number of connections in the pool
172
+ max_size: Maximum number of connections in the pool
173
+ name: Name of the connection pool
174
+ timeout: Timeout for acquiring connections
175
+ max_waiting: Maximum number of waiting clients
176
+ max_lifetime: Maximum connection lifetime
177
+ max_idle: Maximum idle time for connections
178
+ reconnect_timeout: Time between reconnection attempts
179
+ num_workers: Number of background workers
180
+ configure: Callback to configure new connections
181
+ kwargs: Additional connection parameters
182
+ extras: Additional connection parameters not explicitly defined
183
+ **additional_kwargs: Additional parameters (stored in extras)
184
+ """
185
+ # Store connection parameters as instance attributes
186
+ self.conninfo = conninfo
187
+ self.host = host
188
+ self.port = port
189
+ self.user = user
190
+ self.password = password
191
+ self.dbname = dbname
192
+ self.connect_timeout = connect_timeout
193
+ self.options = options
194
+ self.application_name = application_name
195
+ self.sslmode = sslmode
196
+ self.sslcert = sslcert
197
+ self.sslkey = sslkey
198
+ self.sslrootcert = sslrootcert
199
+ self.autocommit = autocommit
200
+
201
+ # Store pool parameters as instance attributes
202
+ self.min_size = min_size
203
+ self.max_size = max_size
204
+ self.name = name
205
+ self.timeout = timeout
206
+ self.max_waiting = max_waiting
207
+ self.max_lifetime = max_lifetime
208
+ self.max_idle = max_idle
209
+ self.reconnect_timeout = reconnect_timeout
210
+ self.num_workers = num_workers
211
+ self.configure = configure
212
+ self.kwargs = kwargs or {}
213
+
214
+ # Handle extras and additional kwargs
215
+ self.extras = extras or {}
216
+ self.extras.update(additional_kwargs)
217
+
218
+ # Store other config
219
+ self.statement_config = statement_config or SQLConfig()
220
+ self.default_row_type = default_row_type
221
+ self._dialect: DialectType = None
222
+
223
+ super().__init__()
224
+
225
+ @property
226
+ def connection_config_dict(self) -> dict[str, Any]:
227
+ """Return the connection configuration as a dict for psycopg operations.
228
+
229
+ Returns only connection-specific parameters.
230
+ """
231
+ # Gather non-None parameters from connection fields only
232
+ config = {
233
+ field: getattr(self, field)
234
+ for field in CONNECTION_FIELDS
235
+ if getattr(self, field, None) is not None and getattr(self, field) is not Empty
236
+ }
237
+
238
+ # Merge extras and kwargs
239
+ config.update(self.extras)
240
+ if self.kwargs:
241
+ config.update(self.kwargs)
242
+
243
+ # Set DictRow as the row factory
244
+ config["row_factory"] = dict_row
245
+
246
+ return config
247
+
248
+ @property
249
+ def pool_config_dict(self) -> dict[str, Any]:
250
+ """Return the pool configuration as a dict for psycopg pool operations.
251
+
252
+ Returns all configuration parameters including connection and pool-specific parameters.
253
+ """
254
+ # Gather non-None parameters from all fields (connection + pool)
255
+ config = {
256
+ field: getattr(self, field)
257
+ for field in POOL_FIELDS
258
+ if getattr(self, field, None) is not None and getattr(self, field) is not Empty
259
+ }
260
+
261
+ # Merge extras and kwargs
262
+ config.update(self.extras)
263
+ if self.kwargs:
264
+ config.update(self.kwargs)
265
+
266
+ # Set DictRow as the row factory
267
+ config["row_factory"] = dict_row
268
+
269
+ return config
270
+
271
+ def _create_pool(self) -> "ConnectionPool":
272
+ """Create the actual connection pool."""
273
+ logger.info("Creating Psycopg connection pool", extra={"adapter": "psycopg"})
274
+
275
+ try:
276
+ # Get all config (creates a new dict)
277
+ all_config = self.pool_config_dict.copy()
278
+
279
+ # Separate pool-specific parameters that ConnectionPool accepts directly
280
+ pool_params = {
281
+ "min_size": all_config.pop("min_size", 4),
282
+ "max_size": all_config.pop("max_size", None),
283
+ "name": all_config.pop("name", None),
284
+ "timeout": all_config.pop("timeout", 30.0),
285
+ "max_waiting": all_config.pop("max_waiting", 0),
286
+ "max_lifetime": all_config.pop("max_lifetime", 3600.0),
287
+ "max_idle": all_config.pop("max_idle", 600.0),
288
+ "reconnect_timeout": all_config.pop("reconnect_timeout", 300.0),
289
+ "num_workers": all_config.pop("num_workers", 3),
290
+ }
291
+
292
+ # Create a configure callback to set row_factory
293
+ def configure_connection(conn: "PsycopgSyncConnection") -> None:
294
+ # Set DictRow as the row factory
295
+ conn.row_factory = dict_row
296
+
297
+ pool_params["configure"] = all_config.pop("configure", configure_connection)
298
+
299
+ # Remove None values from pool_params
300
+ pool_params = {k: v for k, v in pool_params.items() if v is not None}
301
+
302
+ # Handle conninfo vs individual connection parameters
303
+ conninfo = all_config.pop("conninfo", None)
304
+ if conninfo:
305
+ # If conninfo is provided, use it directly
306
+ # Don't pass kwargs when using conninfo string
307
+ pool = ConnectionPool(conninfo, **pool_params)
308
+ else:
309
+ # Otherwise, pass connection parameters via kwargs
310
+ # Remove any non-connection parameters
311
+ # row_factory is already popped out earlier
312
+ all_config.pop("row_factory", None)
313
+ # Remove pool-specific settings that may have been left
314
+ all_config.pop("kwargs", None)
315
+ pool = ConnectionPool("", kwargs=all_config, **pool_params)
316
+
317
+ logger.info("Psycopg connection pool created successfully", extra={"adapter": "psycopg"})
318
+ except Exception as e:
319
+ logger.exception("Failed to create Psycopg connection pool", extra={"adapter": "psycopg", "error": str(e)})
320
+ raise
321
+ return pool
322
+
323
+ def _close_pool(self) -> None:
324
+ """Close the actual connection pool."""
325
+ if not self.pool_instance:
326
+ return
327
+
328
+ logger.info("Closing Psycopg connection pool", extra={"adapter": "psycopg"})
329
+
330
+ try:
331
+ self.pool_instance.close()
332
+ logger.info("Psycopg connection pool closed successfully", extra={"adapter": "psycopg"})
333
+ except Exception as e:
334
+ logger.exception("Failed to close Psycopg connection pool", extra={"adapter": "psycopg", "error": str(e)})
335
+ raise
336
+
337
+ def create_connection(self) -> "PsycopgSyncConnection":
338
+ """Create a single connection (not from pool).
339
+
340
+ Returns:
341
+ A psycopg Connection instance configured with DictRow.
342
+ """
343
+ if self.pool_instance is None:
344
+ self.pool_instance = self.create_pool()
345
+ return cast("PsycopgSyncConnection", self.pool_instance.getconn()) # pyright: ignore
346
+
347
+ @contextlib.contextmanager
348
+ def provide_connection(self, *args: Any, **kwargs: Any) -> "Generator[PsycopgSyncConnection, None, None]":
349
+ """Provide a connection context manager.
350
+
351
+ Args:
352
+ *args: Additional arguments.
353
+ **kwargs: Additional keyword arguments.
354
+
355
+ Yields:
356
+ A psycopg Connection instance.
357
+ """
358
+ if self.pool_instance:
359
+ with self.pool_instance.connection() as conn:
360
+ yield conn # type: ignore[misc]
361
+ else:
362
+ conn = self.create_connection() # type: ignore[assignment]
363
+ try:
364
+ yield conn # type: ignore[misc]
365
+ finally:
366
+ conn.close()
367
+
368
+ @contextlib.contextmanager
369
+ def provide_session(self, *args: Any, **kwargs: Any) -> "Generator[PsycopgSyncDriver, None, None]":
370
+ """Provide a driver session context manager.
371
+
372
+ Args:
373
+ *args: Additional arguments.
374
+ **kwargs: Additional keyword arguments.
375
+
376
+ Yields:
377
+ A PsycopgSyncDriver instance.
378
+ """
379
+ with self.provide_connection(*args, **kwargs) as conn:
380
+ # Create statement config with parameter style info if not already set
381
+ statement_config = self.statement_config
382
+ if statement_config.allowed_parameter_styles is None:
383
+ statement_config = replace(
384
+ statement_config,
385
+ allowed_parameter_styles=self.supported_parameter_styles,
386
+ target_parameter_style=self.preferred_parameter_style,
387
+ )
388
+
389
+ driver = self.driver_type(connection=conn, config=statement_config)
390
+ yield driver
391
+
392
+ def provide_pool(self, *args: Any, **kwargs: Any) -> "ConnectionPool":
393
+ """Provide pool instance.
394
+
395
+ Returns:
396
+ The connection pool.
397
+ """
398
+ if not self.pool_instance:
399
+ self.pool_instance = self.create_pool()
400
+ return self.pool_instance
401
+
402
+
403
+ class PsycopgAsyncConfig(AsyncDatabaseConfig[PsycopgAsyncConnection, AsyncConnectionPool, PsycopgAsyncDriver]):
404
+ """Configuration for Psycopg asynchronous database connections with direct field-based configuration."""
405
+
406
+ __slots__ = (
407
+ "_dialect",
408
+ "application_name",
409
+ "autocommit",
410
+ "configure",
411
+ "connect_timeout",
412
+ "conninfo",
413
+ "dbname",
414
+ "default_row_type",
415
+ "extras",
416
+ "host",
417
+ "kwargs",
418
+ "max_idle",
419
+ "max_lifetime",
420
+ "max_size",
421
+ "max_waiting",
422
+ "min_size",
423
+ "name",
424
+ "num_workers",
425
+ "options",
426
+ "password",
427
+ "pool_instance",
428
+ "port",
429
+ "reconnect_timeout",
430
+ "sslcert",
431
+ "sslkey",
432
+ "sslmode",
433
+ "sslrootcert",
434
+ "statement_config",
435
+ "timeout",
436
+ "user",
437
+ )
438
+
439
+ is_async: ClassVar[bool] = True
440
+ supports_connection_pooling: ClassVar[bool] = True
441
+
442
+ # Driver class reference for dialect resolution
443
+ driver_type: type[PsycopgAsyncDriver] = PsycopgAsyncDriver
444
+ connection_type: type[PsycopgAsyncConnection] = PsycopgAsyncConnection
445
+
446
+ # Parameter style support information
447
+ supported_parameter_styles: ClassVar[tuple[str, ...]] = ("pyformat_positional", "pyformat_named")
448
+ """Psycopg supports %s (pyformat_positional) and %(name)s (pyformat_named) parameter styles."""
449
+
450
+ preferred_parameter_style: ClassVar[str] = "pyformat_positional"
451
+ """Psycopg's preferred parameter style is %s (pyformat_positional)."""
452
+
453
+ def __init__(
454
+ self,
455
+ statement_config: "Optional[SQLConfig]" = None,
456
+ default_row_type: "type[DictRow]" = DictRow,
457
+ # Connection parameters
458
+ conninfo: Optional[str] = None,
459
+ host: Optional[str] = None,
460
+ port: Optional[int] = None,
461
+ user: Optional[str] = None,
462
+ password: Optional[str] = None,
463
+ dbname: Optional[str] = None,
464
+ connect_timeout: Optional[float] = None,
465
+ options: Optional[str] = None,
466
+ application_name: Optional[str] = None,
467
+ sslmode: Optional[str] = None,
468
+ sslcert: Optional[str] = None,
469
+ sslkey: Optional[str] = None,
470
+ sslrootcert: Optional[str] = None,
471
+ autocommit: Optional[bool] = None,
472
+ # Pool parameters
473
+ min_size: Optional[int] = None,
474
+ max_size: Optional[int] = None,
475
+ name: Optional[str] = None,
476
+ timeout: Optional[float] = None,
477
+ max_waiting: Optional[int] = None,
478
+ max_lifetime: Optional[float] = None,
479
+ max_idle: Optional[float] = None,
480
+ reconnect_timeout: Optional[float] = None,
481
+ num_workers: Optional[int] = None,
482
+ configure: Optional["Callable[[Connection[Any]], None]"] = None,
483
+ kwargs: Optional[dict[str, Any]] = None,
484
+ # User-defined extras
485
+ extras: Optional[dict[str, Any]] = None,
486
+ **additional_kwargs: Any,
487
+ ) -> None:
488
+ """Initialize Psycopg asynchronous configuration.
489
+
490
+ Args:
491
+ statement_config: Default SQL statement configuration
492
+ default_row_type: Default row type for results
493
+ conninfo: Connection string in libpq format
494
+ host: Database server host
495
+ port: Database server port
496
+ user: Database user
497
+ password: Database password
498
+ dbname: Database name
499
+ connect_timeout: Connection timeout in seconds
500
+ options: Command-line options to send to the server
501
+ application_name: Application name for logging and statistics
502
+ sslmode: SSL mode (disable, prefer, require, etc.)
503
+ sslcert: SSL client certificate file
504
+ sslkey: SSL client private key file
505
+ sslrootcert: SSL root certificate file
506
+ autocommit: Enable autocommit mode
507
+ min_size: Minimum number of connections in the pool
508
+ max_size: Maximum number of connections in the pool
509
+ name: Name of the connection pool
510
+ timeout: Timeout for acquiring connections
511
+ max_waiting: Maximum number of waiting clients
512
+ max_lifetime: Maximum connection lifetime
513
+ max_idle: Maximum idle time for connections
514
+ reconnect_timeout: Time between reconnection attempts
515
+ num_workers: Number of background workers
516
+ configure: Callback to configure new connections
517
+ kwargs: Additional connection parameters
518
+ extras: Additional connection parameters not explicitly defined
519
+ **additional_kwargs: Additional parameters (stored in extras)
520
+ """
521
+ # Store connection parameters as instance attributes
522
+ self.conninfo = conninfo
523
+ self.host = host
524
+ self.port = port
525
+ self.user = user
526
+ self.password = password
527
+ self.dbname = dbname
528
+ self.connect_timeout = connect_timeout
529
+ self.options = options
530
+ self.application_name = application_name
531
+ self.sslmode = sslmode
532
+ self.sslcert = sslcert
533
+ self.sslkey = sslkey
534
+ self.sslrootcert = sslrootcert
535
+ self.autocommit = autocommit
536
+
537
+ # Store pool parameters as instance attributes
538
+ self.min_size = min_size
539
+ self.max_size = max_size
540
+ self.name = name
541
+ self.timeout = timeout
542
+ self.max_waiting = max_waiting
543
+ self.max_lifetime = max_lifetime
544
+ self.max_idle = max_idle
545
+ self.reconnect_timeout = reconnect_timeout
546
+ self.num_workers = num_workers
547
+ self.configure = configure
548
+ self.kwargs = kwargs or {}
549
+
550
+ # Handle extras and additional kwargs
551
+ self.extras = extras or {}
552
+ self.extras.update(additional_kwargs)
553
+
554
+ # Store other config
555
+ self.statement_config = statement_config or SQLConfig()
556
+ self.default_row_type = default_row_type
557
+ self._dialect: DialectType = None
558
+
559
+ super().__init__()
560
+
561
+ @property
562
+ def connection_config_dict(self) -> dict[str, Any]:
563
+ """Return the connection configuration as a dict for psycopg operations.
564
+
565
+ Returns only connection-specific parameters.
566
+ """
567
+ # Gather non-None parameters from connection fields only
568
+ config = {
569
+ field: getattr(self, field)
570
+ for field in CONNECTION_FIELDS
571
+ if getattr(self, field, None) is not None and getattr(self, field) is not Empty
572
+ }
573
+
574
+ # Merge extras and kwargs
575
+ config.update(self.extras)
576
+ if self.kwargs:
577
+ config.update(self.kwargs)
578
+
579
+ # Set DictRow as the row factory
580
+ config["row_factory"] = dict_row
581
+
582
+ return config
583
+
584
+ @property
585
+ def pool_config_dict(self) -> dict[str, Any]:
586
+ """Return the pool configuration as a dict for psycopg pool operations.
587
+
588
+ Returns all configuration parameters including connection and pool-specific parameters.
589
+ """
590
+ # Gather non-None parameters from all fields (connection + pool)
591
+ config = {
592
+ field: getattr(self, field)
593
+ for field in POOL_FIELDS
594
+ if getattr(self, field, None) is not None and getattr(self, field) is not Empty
595
+ }
596
+
597
+ # Merge extras and kwargs
598
+ config.update(self.extras)
599
+ if self.kwargs:
600
+ config.update(self.kwargs)
601
+
602
+ # Set DictRow as the row factory
603
+ config["row_factory"] = dict_row
604
+
605
+ return config
606
+
607
+ async def _create_pool(self) -> "AsyncConnectionPool":
608
+ """Create the actual async connection pool."""
609
+ logger.info("Creating async Psycopg connection pool", extra={"adapter": "psycopg"})
610
+
611
+ try:
612
+ # Get all config (creates a new dict)
613
+ all_config = self.pool_config_dict.copy()
614
+
615
+ # Separate pool-specific parameters that AsyncConnectionPool accepts directly
616
+ pool_params = {
617
+ "min_size": all_config.pop("min_size", 4),
618
+ "max_size": all_config.pop("max_size", None),
619
+ "name": all_config.pop("name", None),
620
+ "timeout": all_config.pop("timeout", 30.0),
621
+ "max_waiting": all_config.pop("max_waiting", 0),
622
+ "max_lifetime": all_config.pop("max_lifetime", 3600.0),
623
+ "max_idle": all_config.pop("max_idle", 600.0),
624
+ "reconnect_timeout": all_config.pop("reconnect_timeout", 300.0),
625
+ "num_workers": all_config.pop("num_workers", 3),
626
+ }
627
+
628
+ # Create a configure callback to set row_factory
629
+ async def configure_connection(conn: "PsycopgAsyncConnection") -> None:
630
+ # Set DictRow as the row factory
631
+ conn.row_factory = dict_row
632
+
633
+ pool_params["configure"] = all_config.pop("configure", configure_connection)
634
+
635
+ # Remove None values from pool_params
636
+ pool_params = {k: v for k, v in pool_params.items() if v is not None}
637
+
638
+ # Handle conninfo vs individual connection parameters
639
+ conninfo = all_config.pop("conninfo", None)
640
+ if conninfo:
641
+ # If conninfo is provided, use it directly
642
+ # Don't pass kwargs when using conninfo string
643
+ pool = AsyncConnectionPool(conninfo, **pool_params)
644
+ else:
645
+ # Otherwise, pass connection parameters via kwargs
646
+ # Remove any non-connection parameters
647
+ # row_factory is already popped out earlier
648
+ all_config.pop("row_factory", None)
649
+ # Remove pool-specific settings that may have been left
650
+ all_config.pop("kwargs", None)
651
+ pool = AsyncConnectionPool("", kwargs=all_config, **pool_params)
652
+
653
+ await pool.open()
654
+ logger.info("Async Psycopg connection pool created successfully", extra={"adapter": "psycopg"})
655
+ except Exception as e:
656
+ logger.exception(
657
+ "Failed to create async Psycopg connection pool", extra={"adapter": "psycopg", "error": str(e)}
658
+ )
659
+ raise
660
+ return pool
661
+
662
+ async def _close_pool(self) -> None:
663
+ """Close the actual async connection pool."""
664
+ if not self.pool_instance:
665
+ return
666
+
667
+ logger.info("Closing async Psycopg connection pool", extra={"adapter": "psycopg"})
668
+
669
+ try:
670
+ await self.pool_instance.close()
671
+ logger.info("Async Psycopg connection pool closed successfully", extra={"adapter": "psycopg"})
672
+ except Exception as e:
673
+ logger.exception(
674
+ "Failed to close async Psycopg connection pool", extra={"adapter": "psycopg", "error": str(e)}
675
+ )
676
+ raise
677
+
678
+ async def create_connection(self) -> "PsycopgAsyncConnection": # pyright: ignore
679
+ """Create a single async connection (not from pool).
680
+
681
+ Returns:
682
+ A psycopg AsyncConnection instance configured with DictRow.
683
+ """
684
+ if self.pool_instance is None:
685
+ self.pool_instance = await self.create_pool()
686
+ return cast("PsycopgAsyncConnection", await self.pool_instance.getconn()) # pyright: ignore
687
+
688
+ @asynccontextmanager
689
+ async def provide_connection(self, *args: Any, **kwargs: Any) -> "AsyncGenerator[PsycopgAsyncConnection, None]": # pyright: ignore
690
+ """Provide an async connection context manager.
691
+
692
+ Args:
693
+ *args: Additional arguments.
694
+ **kwargs: Additional keyword arguments.
695
+
696
+ Yields:
697
+ A psycopg AsyncConnection instance.
698
+ """
699
+ if self.pool_instance:
700
+ async with self.pool_instance.connection() as conn:
701
+ yield conn # type: ignore[misc]
702
+ else:
703
+ conn = await self.create_connection() # type: ignore[assignment]
704
+ try:
705
+ yield conn # type: ignore[misc]
706
+ finally:
707
+ await conn.close()
708
+
709
+ @asynccontextmanager
710
+ async def provide_session(self, *args: Any, **kwargs: Any) -> "AsyncGenerator[PsycopgAsyncDriver, None]":
711
+ """Provide an async driver session context manager.
712
+
713
+ Args:
714
+ *args: Additional arguments.
715
+ **kwargs: Additional keyword arguments.
716
+
717
+ Yields:
718
+ A PsycopgAsyncDriver instance.
719
+ """
720
+ async with self.provide_connection(*args, **kwargs) as conn:
721
+ # Create statement config with parameter style info if not already set
722
+ statement_config = self.statement_config
723
+ if statement_config.allowed_parameter_styles is None:
724
+ statement_config = replace(
725
+ statement_config,
726
+ allowed_parameter_styles=self.supported_parameter_styles,
727
+ target_parameter_style=self.preferred_parameter_style,
728
+ )
729
+
730
+ driver = self.driver_type(connection=conn, config=statement_config)
731
+ yield driver
732
+
733
+ async def provide_pool(self, *args: Any, **kwargs: Any) -> "AsyncConnectionPool":
734
+ """Provide async pool instance.
735
+
736
+ Returns:
737
+ The async connection pool.
738
+ """
739
+ if not self.pool_instance:
740
+ self.pool_instance = await self.create_pool()
741
+ return self.pool_instance