sqlspec 0.14.1__py3-none-any.whl → 0.15.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 (158) hide show
  1. sqlspec/__init__.py +50 -25
  2. sqlspec/__main__.py +1 -1
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +256 -120
  6. sqlspec/_typing.py +278 -142
  7. sqlspec/adapters/adbc/__init__.py +4 -3
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/config.py +115 -260
  10. sqlspec/adapters/adbc/driver.py +462 -367
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +199 -129
  14. sqlspec/adapters/aiosqlite/driver.py +230 -269
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -168
  18. sqlspec/adapters/asyncmy/driver.py +260 -225
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +82 -181
  22. sqlspec/adapters/asyncpg/driver.py +285 -383
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -258
  26. sqlspec/adapters/bigquery/driver.py +474 -646
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +415 -351
  30. sqlspec/adapters/duckdb/driver.py +343 -413
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -379
  34. sqlspec/adapters/oracledb/driver.py +507 -560
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -254
  38. sqlspec/adapters/psqlpy/driver.py +505 -234
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -403
  42. sqlspec/adapters/psycopg/driver.py +706 -872
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +202 -118
  46. sqlspec/adapters/sqlite/driver.py +264 -303
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder → builder}/_base.py +120 -55
  50. sqlspec/{statement/builder → builder}/_column.py +17 -6
  51. sqlspec/{statement/builder → builder}/_ddl.py +46 -79
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +5 -10
  53. sqlspec/{statement/builder → builder}/_delete.py +6 -25
  54. sqlspec/{statement/builder → builder}/_insert.py +6 -64
  55. sqlspec/builder/_merge.py +56 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +3 -10
  57. sqlspec/{statement/builder → builder}/_select.py +11 -56
  58. sqlspec/{statement/builder → builder}/_update.py +12 -18
  59. sqlspec/{statement/builder → builder}/mixins/__init__.py +10 -14
  60. sqlspec/{statement/builder → builder}/mixins/_cte_and_set_ops.py +48 -59
  61. sqlspec/{statement/builder → builder}/mixins/_insert_operations.py +22 -16
  62. sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
  63. sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +3 -5
  64. sqlspec/{statement/builder → builder}/mixins/_order_limit_operations.py +3 -3
  65. sqlspec/{statement/builder → builder}/mixins/_pivot_operations.py +4 -8
  66. sqlspec/{statement/builder → builder}/mixins/_select_operations.py +21 -36
  67. sqlspec/{statement/builder → builder}/mixins/_update_operations.py +3 -14
  68. sqlspec/{statement/builder → builder}/mixins/_where_clause.py +52 -79
  69. sqlspec/cli.py +4 -5
  70. sqlspec/config.py +180 -133
  71. sqlspec/core/__init__.py +63 -0
  72. sqlspec/core/cache.py +873 -0
  73. sqlspec/core/compiler.py +396 -0
  74. sqlspec/core/filters.py +828 -0
  75. sqlspec/core/hashing.py +310 -0
  76. sqlspec/core/parameters.py +1209 -0
  77. sqlspec/core/result.py +664 -0
  78. sqlspec/{statement → core}/splitter.py +321 -191
  79. sqlspec/core/statement.py +651 -0
  80. sqlspec/driver/__init__.py +7 -10
  81. sqlspec/driver/_async.py +387 -176
  82. sqlspec/driver/_common.py +527 -289
  83. sqlspec/driver/_sync.py +390 -172
  84. sqlspec/driver/mixins/__init__.py +2 -19
  85. sqlspec/driver/mixins/_result_tools.py +168 -0
  86. sqlspec/driver/mixins/_sql_translator.py +6 -3
  87. sqlspec/exceptions.py +5 -252
  88. sqlspec/extensions/aiosql/adapter.py +93 -96
  89. sqlspec/extensions/litestar/config.py +0 -1
  90. sqlspec/extensions/litestar/handlers.py +15 -26
  91. sqlspec/extensions/litestar/plugin.py +16 -14
  92. sqlspec/extensions/litestar/providers.py +17 -52
  93. sqlspec/loader.py +424 -105
  94. sqlspec/migrations/__init__.py +12 -0
  95. sqlspec/migrations/base.py +92 -68
  96. sqlspec/migrations/commands.py +24 -106
  97. sqlspec/migrations/loaders.py +402 -0
  98. sqlspec/migrations/runner.py +49 -51
  99. sqlspec/migrations/tracker.py +31 -44
  100. sqlspec/migrations/utils.py +64 -24
  101. sqlspec/protocols.py +7 -183
  102. sqlspec/storage/__init__.py +1 -1
  103. sqlspec/storage/backends/base.py +37 -40
  104. sqlspec/storage/backends/fsspec.py +136 -112
  105. sqlspec/storage/backends/obstore.py +138 -160
  106. sqlspec/storage/capabilities.py +5 -4
  107. sqlspec/storage/registry.py +57 -106
  108. sqlspec/typing.py +136 -115
  109. sqlspec/utils/__init__.py +2 -3
  110. sqlspec/utils/correlation.py +0 -3
  111. sqlspec/utils/deprecation.py +6 -6
  112. sqlspec/utils/fixtures.py +6 -6
  113. sqlspec/utils/logging.py +0 -2
  114. sqlspec/utils/module_loader.py +7 -12
  115. sqlspec/utils/singleton.py +0 -1
  116. sqlspec/utils/sync_tools.py +16 -37
  117. sqlspec/utils/text.py +12 -51
  118. sqlspec/utils/type_guards.py +443 -232
  119. {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/METADATA +7 -2
  120. sqlspec-0.15.0.dist-info/RECORD +134 -0
  121. sqlspec/adapters/adbc/transformers.py +0 -108
  122. sqlspec/driver/connection.py +0 -207
  123. sqlspec/driver/mixins/_cache.py +0 -114
  124. sqlspec/driver/mixins/_csv_writer.py +0 -91
  125. sqlspec/driver/mixins/_pipeline.py +0 -508
  126. sqlspec/driver/mixins/_query_tools.py +0 -796
  127. sqlspec/driver/mixins/_result_utils.py +0 -138
  128. sqlspec/driver/mixins/_storage.py +0 -912
  129. sqlspec/driver/mixins/_type_coercion.py +0 -128
  130. sqlspec/driver/parameters.py +0 -138
  131. sqlspec/statement/__init__.py +0 -21
  132. sqlspec/statement/builder/_merge.py +0 -95
  133. sqlspec/statement/cache.py +0 -50
  134. sqlspec/statement/filters.py +0 -625
  135. sqlspec/statement/parameters.py +0 -956
  136. sqlspec/statement/pipelines/__init__.py +0 -210
  137. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  138. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  139. sqlspec/statement/pipelines/context.py +0 -109
  140. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  141. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  142. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  143. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  144. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  145. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  146. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  147. sqlspec/statement/pipelines/validators/_performance.py +0 -714
  148. sqlspec/statement/pipelines/validators/_security.py +0 -967
  149. sqlspec/statement/result.py +0 -435
  150. sqlspec/statement/sql.py +0 -1774
  151. sqlspec/utils/cached_property.py +0 -25
  152. sqlspec/utils/statement_hashing.py +0 -203
  153. sqlspec-0.14.1.dist-info/RECORD +0 -145
  154. /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
  155. {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/WHEEL +0 -0
  156. {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/entry_points.txt +0 -0
  157. {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/licenses/LICENSE +0 -0
  158. {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/licenses/NOTICE +0 -0
@@ -2,17 +2,14 @@
2
2
 
3
3
  This module provides type-safe runtime checks that help the type checker
4
4
  understand type narrowing, replacing defensive hasattr() and duck typing patterns.
5
-
6
- NOTE: Some sqlspec imports are nested inside functions to prevent circular
7
- imports where necessary. This module is imported by core sqlspec modules,
8
- so imports that would create cycles are deferred.
9
5
  """
10
6
 
11
- from collections.abc import Iterable, Sequence
7
+ from collections.abc import Sequence
12
8
  from collections.abc import Set as AbstractSet
13
9
  from typing import TYPE_CHECKING, Any, Optional, Union, cast
14
10
 
15
11
  from sqlspec.typing import (
12
+ ATTRS_INSTALLED,
16
13
  LITESTAR_INSTALLED,
17
14
  MSGSPEC_INSTALLED,
18
15
  PYDANTIC_INSTALLED,
@@ -20,6 +17,8 @@ from sqlspec.typing import (
20
17
  DataclassProtocol,
21
18
  DTOData,
22
19
  Struct,
20
+ attrs_asdict,
21
+ attrs_has,
23
22
  )
24
23
 
25
24
  if TYPE_CHECKING:
@@ -28,12 +27,10 @@ if TYPE_CHECKING:
28
27
  from sqlglot import exp
29
28
  from typing_extensions import TypeGuard
30
29
 
30
+ from sqlspec._typing import AttrsInstanceStub, BaseModelStub, DTODataStub, StructStub
31
+ from sqlspec.builder import Select
32
+ from sqlspec.core.filters import LimitOffsetFilter, StatementFilter
31
33
  from sqlspec.protocols import (
32
- AsyncCloseableConnectionProtocol,
33
- AsyncCopyCapableConnectionProtocol,
34
- AsyncPipelineCapableDriverProtocol,
35
- AsyncTransactionCapableConnectionProtocol,
36
- AsyncTransactionStateConnectionProtocol,
37
34
  BytesConvertibleProtocol,
38
35
  DictProtocol,
39
36
  FilterAppenderProtocol,
@@ -42,22 +39,14 @@ if TYPE_CHECKING:
42
39
  HasLimitProtocol,
43
40
  HasOffsetProtocol,
44
41
  HasOrderByProtocol,
45
- HasRiskLevelProtocol,
46
42
  HasSQLMethodProtocol,
47
43
  HasWhereProtocol,
48
44
  IndexableRow,
49
45
  ObjectStoreItemProtocol,
50
46
  ParameterValueProtocol,
51
47
  SQLBuilderProtocol,
52
- SyncCloseableConnectionProtocol,
53
- SyncCopyCapableConnectionProtocol,
54
- SyncPipelineCapableDriverProtocol,
55
- SyncTransactionCapableConnectionProtocol,
56
- SyncTransactionStateConnectionProtocol,
57
48
  WithMethodProtocol,
58
49
  )
59
- from sqlspec.statement.builder import Select
60
- from sqlspec.statement.filters import LimitOffsetFilter, StatementFilter
61
50
  from sqlspec.typing import SupportedSchemaModel
62
51
 
63
52
  __all__ = (
@@ -65,25 +54,35 @@ __all__ = (
65
54
  "can_convert_to_schema",
66
55
  "can_extract_parameters",
67
56
  "dataclass_to_dict",
57
+ "expression_has_limit",
68
58
  "extract_dataclass_fields",
69
59
  "extract_dataclass_items",
60
+ "get_initial_expression",
61
+ "get_literal_parent",
62
+ "get_node_expressions",
63
+ "get_node_this",
64
+ "get_param_style_and_name",
65
+ "get_value_attribute",
66
+ "has_attr",
70
67
  "has_bytes_conversion",
71
68
  "has_dict_attribute",
72
69
  "has_expression_attr",
73
70
  "has_expressions",
71
+ "has_expressions_attribute",
74
72
  "has_parameter_builder",
75
73
  "has_parameter_value",
74
+ "has_parent_attribute",
76
75
  "has_query_builder_parameters",
77
- "has_risk_level",
78
76
  "has_sql_method",
79
77
  "has_sqlglot_expression",
78
+ "has_this_attribute",
80
79
  "has_to_statement",
81
80
  "has_with_method",
82
- "is_async_closeable_connection",
83
- "is_async_copy_capable",
84
- "is_async_pipeline_capable_driver",
85
- "is_async_transaction_capable",
86
- "is_async_transaction_state_capable",
81
+ "is_attrs_instance",
82
+ "is_attrs_instance_with_field",
83
+ "is_attrs_instance_without_field",
84
+ "is_attrs_schema",
85
+ "is_copy_statement",
87
86
  "is_dataclass",
88
87
  "is_dataclass_instance",
89
88
  "is_dataclass_with_field",
@@ -100,6 +99,7 @@ __all__ = (
100
99
  "is_msgspec_struct",
101
100
  "is_msgspec_struct_with_field",
102
101
  "is_msgspec_struct_without_field",
102
+ "is_number_literal",
103
103
  "is_object_store_item",
104
104
  "is_pydantic_model",
105
105
  "is_pydantic_model_with_field",
@@ -112,11 +112,8 @@ __all__ = (
112
112
  "is_schema_without_field",
113
113
  "is_select_builder",
114
114
  "is_statement_filter",
115
- "is_sync_closeable_connection",
116
- "is_sync_copy_capable",
117
- "is_sync_pipeline_capable_driver",
118
- "is_sync_transaction_capable",
119
- "is_sync_transaction_state_capable",
115
+ "is_string_literal",
116
+ "is_typed_parameter",
120
117
  "schema_dump",
121
118
  "supports_limit",
122
119
  "supports_offset",
@@ -134,7 +131,7 @@ def is_statement_filter(obj: Any) -> "TypeGuard[StatementFilter]":
134
131
  Returns:
135
132
  True if the object is a StatementFilter, False otherwise
136
133
  """
137
- from sqlspec.statement.filters import StatementFilter as FilterProtocol
134
+ from sqlspec.core.filters import StatementFilter as FilterProtocol
138
135
 
139
136
  return isinstance(obj, FilterProtocol)
140
137
 
@@ -148,7 +145,7 @@ def is_limit_offset_filter(obj: Any) -> "TypeGuard[LimitOffsetFilter]":
148
145
  Returns:
149
146
  True if the object is a LimitOffsetFilter, False otherwise
150
147
  """
151
- from sqlspec.statement.filters import LimitOffsetFilter
148
+ from sqlspec.core.filters import LimitOffsetFilter
152
149
 
153
150
  return isinstance(obj, LimitOffsetFilter)
154
151
 
@@ -162,7 +159,7 @@ def is_select_builder(obj: Any) -> "TypeGuard[Select]":
162
159
  Returns:
163
160
  True if the object is a Select, False otherwise
164
161
  """
165
- from sqlspec.statement.builder import Select
162
+ from sqlspec.builder import Select
166
163
 
167
164
  return isinstance(obj, Select)
168
165
 
@@ -193,16 +190,16 @@ def is_indexable_row(row: Any) -> "TypeGuard[IndexableRow]":
193
190
  return isinstance(row, IndexableRow)
194
191
 
195
192
 
196
- def is_iterable_parameters(params: Any) -> "TypeGuard[Sequence[Any]]":
193
+ def is_iterable_parameters(parameters: Any) -> "TypeGuard[Sequence[Any]]":
197
194
  """Check if parameters are iterable (but not string or dict).
198
195
 
199
196
  Args:
200
- params: The parameters to check
197
+ parameters: The parameters to check
201
198
 
202
199
  Returns:
203
200
  True if the parameters are iterable, False otherwise
204
201
  """
205
- return isinstance(params, Sequence) and not isinstance(params, (str, bytes, dict))
202
+ return isinstance(parameters, Sequence) and not isinstance(parameters, (str, bytes, dict))
206
203
 
207
204
 
208
205
  def has_with_method(obj: Any) -> "TypeGuard[WithMethodProtocol]":
@@ -237,9 +234,6 @@ def can_convert_to_schema(obj: Any) -> "TypeGuard[Any]":
237
234
  return isinstance(obj, ToSchemaMixin)
238
235
 
239
236
 
240
- # Type guards migrated from typing.py
241
-
242
-
243
237
  def is_dataclass_instance(obj: Any) -> "TypeGuard[DataclassProtocol]":
244
238
  """Check if an object is a dataclass instance.
245
239
 
@@ -249,8 +243,14 @@ def is_dataclass_instance(obj: Any) -> "TypeGuard[DataclassProtocol]":
249
243
  Returns:
250
244
  True if the object is a dataclass instance.
251
245
  """
252
- # and that its type is a dataclass.
253
- return not isinstance(obj, type) and hasattr(type(obj), "__dataclass_fields__")
246
+ if isinstance(obj, type):
247
+ return False
248
+ try:
249
+ _ = type(obj).__dataclass_fields__
250
+ except AttributeError:
251
+ return False
252
+ else:
253
+ return True
254
254
 
255
255
 
256
256
  def is_dataclass(obj: Any) -> "TypeGuard[DataclassProtocol]":
@@ -262,8 +262,13 @@ def is_dataclass(obj: Any) -> "TypeGuard[DataclassProtocol]":
262
262
  Returns:
263
263
  bool
264
264
  """
265
- if isinstance(obj, type) and hasattr(obj, "__dataclass_fields__"):
266
- return True
265
+ if isinstance(obj, type):
266
+ try:
267
+ _ = obj.__dataclass_fields__ # type: ignore[attr-defined]
268
+ except AttributeError:
269
+ return False
270
+ else:
271
+ return True
267
272
  return is_dataclass_instance(obj)
268
273
 
269
274
 
@@ -277,7 +282,14 @@ def is_dataclass_with_field(obj: Any, field_name: str) -> "TypeGuard[object]":
277
282
  Returns:
278
283
  bool
279
284
  """
280
- return is_dataclass(obj) and hasattr(obj, field_name)
285
+ if not is_dataclass(obj):
286
+ return False
287
+ try:
288
+ _ = getattr(obj, field_name)
289
+ except AttributeError:
290
+ return False
291
+ else:
292
+ return True
281
293
 
282
294
 
283
295
  def is_dataclass_without_field(obj: Any, field_name: str) -> "TypeGuard[object]":
@@ -290,11 +302,18 @@ def is_dataclass_without_field(obj: Any, field_name: str) -> "TypeGuard[object]"
290
302
  Returns:
291
303
  bool
292
304
  """
293
- return is_dataclass(obj) and not hasattr(obj, field_name)
305
+ if not is_dataclass(obj):
306
+ return False
307
+ try:
308
+ _ = getattr(obj, field_name)
309
+ except AttributeError:
310
+ return True
311
+ else:
312
+ return False
294
313
 
295
314
 
296
- def is_pydantic_model(obj: Any) -> "TypeGuard[BaseModel]":
297
- """Check if a value is a pydantic model.
315
+ def is_pydantic_model(obj: Any) -> "TypeGuard[Any]":
316
+ """Check if a value is a pydantic model class or instance.
298
317
 
299
318
  Args:
300
319
  obj: Value to check.
@@ -302,10 +321,17 @@ def is_pydantic_model(obj: Any) -> "TypeGuard[BaseModel]":
302
321
  Returns:
303
322
  bool
304
323
  """
305
- return PYDANTIC_INSTALLED and isinstance(obj, BaseModel)
324
+ if not PYDANTIC_INSTALLED:
325
+ return False
326
+ if isinstance(obj, type):
327
+ try:
328
+ return issubclass(obj, BaseModel)
329
+ except TypeError:
330
+ return False
331
+ return isinstance(obj, BaseModel)
306
332
 
307
333
 
308
- def is_pydantic_model_with_field(obj: Any, field_name: str) -> "TypeGuard[BaseModel]":
334
+ def is_pydantic_model_with_field(obj: Any, field_name: str) -> "TypeGuard[BaseModelStub]":
309
335
  """Check if a pydantic model has a specific field.
310
336
 
311
337
  Args:
@@ -315,10 +341,17 @@ def is_pydantic_model_with_field(obj: Any, field_name: str) -> "TypeGuard[BaseMo
315
341
  Returns:
316
342
  bool
317
343
  """
318
- return is_pydantic_model(obj) and hasattr(obj, field_name)
344
+ if not is_pydantic_model(obj):
345
+ return False
346
+ try:
347
+ _ = getattr(obj, field_name)
348
+ except AttributeError:
349
+ return False
350
+ else:
351
+ return True
319
352
 
320
353
 
321
- def is_pydantic_model_without_field(obj: Any, field_name: str) -> "TypeGuard[BaseModel]":
354
+ def is_pydantic_model_without_field(obj: Any, field_name: str) -> "TypeGuard[BaseModelStub]":
322
355
  """Check if a pydantic model does not have a specific field.
323
356
 
324
357
  Args:
@@ -328,11 +361,18 @@ def is_pydantic_model_without_field(obj: Any, field_name: str) -> "TypeGuard[Bas
328
361
  Returns:
329
362
  bool
330
363
  """
331
- return is_pydantic_model(obj) and not hasattr(obj, field_name)
364
+ if not is_pydantic_model(obj):
365
+ return False
366
+ try:
367
+ _ = getattr(obj, field_name)
368
+ except AttributeError:
369
+ return True
370
+ else:
371
+ return False
332
372
 
333
373
 
334
- def is_msgspec_struct(obj: Any) -> "TypeGuard[Struct]":
335
- """Check if a value is a msgspec struct.
374
+ def is_msgspec_struct(obj: Any) -> "TypeGuard[StructStub]":
375
+ """Check if a value is a msgspec struct class or instance.
336
376
 
337
377
  Args:
338
378
  obj: Value to check.
@@ -340,10 +380,17 @@ def is_msgspec_struct(obj: Any) -> "TypeGuard[Struct]":
340
380
  Returns:
341
381
  bool
342
382
  """
343
- return MSGSPEC_INSTALLED and isinstance(obj, Struct)
383
+ if not MSGSPEC_INSTALLED:
384
+ return False
385
+ if isinstance(obj, type):
386
+ try:
387
+ return issubclass(obj, Struct)
388
+ except TypeError:
389
+ return False
390
+ return isinstance(obj, Struct)
344
391
 
345
392
 
346
- def is_msgspec_struct_with_field(obj: Any, field_name: str) -> "TypeGuard[Struct]":
393
+ def is_msgspec_struct_with_field(obj: Any, field_name: str) -> "TypeGuard[StructStub]":
347
394
  """Check if a msgspec struct has a specific field.
348
395
 
349
396
  Args:
@@ -353,10 +400,17 @@ def is_msgspec_struct_with_field(obj: Any, field_name: str) -> "TypeGuard[Struct
353
400
  Returns:
354
401
  bool
355
402
  """
356
- return is_msgspec_struct(obj) and hasattr(obj, field_name)
403
+ if not is_msgspec_struct(obj):
404
+ return False
405
+ try:
406
+ _ = getattr(obj, field_name)
357
407
 
408
+ except AttributeError:
409
+ return False
410
+ return True
358
411
 
359
- def is_msgspec_struct_without_field(obj: Any, field_name: str) -> "TypeGuard[Struct]":
412
+
413
+ def is_msgspec_struct_without_field(obj: Any, field_name: str) -> "TypeGuard[StructStub]":
360
414
  """Check if a msgspec struct does not have a specific field.
361
415
 
362
416
  Args:
@@ -366,7 +420,63 @@ def is_msgspec_struct_without_field(obj: Any, field_name: str) -> "TypeGuard[Str
366
420
  Returns:
367
421
  bool
368
422
  """
369
- return is_msgspec_struct(obj) and not hasattr(obj, field_name)
423
+ if not is_msgspec_struct(obj):
424
+ return False
425
+ try:
426
+ _ = getattr(obj, field_name)
427
+ except AttributeError:
428
+ return True
429
+ return False
430
+
431
+
432
+ def is_attrs_instance(obj: Any) -> "TypeGuard[AttrsInstanceStub]":
433
+ """Check if a value is an attrs class instance.
434
+
435
+ Args:
436
+ obj: Value to check.
437
+
438
+ Returns:
439
+ bool
440
+ """
441
+ return ATTRS_INSTALLED and attrs_has(obj.__class__)
442
+
443
+
444
+ def is_attrs_schema(cls: Any) -> "TypeGuard[type[AttrsInstanceStub]]":
445
+ """Check if a class type is an attrs schema.
446
+
447
+ Args:
448
+ cls: Class to check.
449
+
450
+ Returns:
451
+ bool
452
+ """
453
+ return ATTRS_INSTALLED and attrs_has(cls)
454
+
455
+
456
+ def is_attrs_instance_with_field(obj: Any, field_name: str) -> "TypeGuard[AttrsInstanceStub]":
457
+ """Check if an attrs instance has a specific field.
458
+
459
+ Args:
460
+ obj: Value to check.
461
+ field_name: Field name to check for.
462
+
463
+ Returns:
464
+ bool
465
+ """
466
+ return is_attrs_instance(obj) and hasattr(obj, field_name)
467
+
468
+
469
+ def is_attrs_instance_without_field(obj: Any, field_name: str) -> "TypeGuard[AttrsInstanceStub]":
470
+ """Check if an attrs instance does not have a specific field.
471
+
472
+ Args:
473
+ obj: Value to check.
474
+ field_name: Field name to check for.
475
+
476
+ Returns:
477
+ bool
478
+ """
479
+ return is_attrs_instance(obj) and not hasattr(obj, field_name)
370
480
 
371
481
 
372
482
  def is_dict(obj: Any) -> "TypeGuard[dict[str, Any]]":
@@ -408,7 +518,7 @@ def is_dict_without_field(obj: Any, field_name: str) -> "TypeGuard[dict[str, Any
408
518
 
409
519
 
410
520
  def is_schema(obj: Any) -> "TypeGuard[SupportedSchemaModel]":
411
- """Check if a value is a msgspec Struct or Pydantic model.
521
+ """Check if a value is a msgspec Struct, Pydantic model, attrs instance, or schema class.
412
522
 
413
523
  Args:
414
524
  obj: Value to check.
@@ -416,7 +526,13 @@ def is_schema(obj: Any) -> "TypeGuard[SupportedSchemaModel]":
416
526
  Returns:
417
527
  bool
418
528
  """
419
- return is_msgspec_struct(obj) or is_pydantic_model(obj)
529
+ return (
530
+ is_msgspec_struct(obj)
531
+ or is_pydantic_model(obj)
532
+ or is_attrs_instance(obj)
533
+ or is_attrs_schema(obj)
534
+ or is_dataclass(obj)
535
+ )
420
536
 
421
537
 
422
538
  def is_schema_or_dict(obj: Any) -> "TypeGuard[Union[SupportedSchemaModel, dict[str, Any]]]":
@@ -485,7 +601,7 @@ def is_schema_or_dict_without_field(
485
601
  return not is_schema_or_dict_with_field(obj, field_name)
486
602
 
487
603
 
488
- def is_dto_data(v: Any) -> "TypeGuard[DTOData[Any]]":
604
+ def is_dto_data(v: Any) -> "TypeGuard[DTODataStub[Any]]":
489
605
  """Check if a value is a Litestar DTOData object.
490
606
 
491
607
  Args:
@@ -525,149 +641,6 @@ def has_dict_attribute(obj: Any) -> "TypeGuard[DictProtocol]":
525
641
  return isinstance(obj, DictProtocol)
526
642
 
527
643
 
528
- def is_sync_transaction_capable(obj: Any) -> "TypeGuard[SyncTransactionCapableConnectionProtocol]":
529
- """Check if a connection supports sync transactions.
530
-
531
- Args:
532
- obj: Connection object to check.
533
-
534
- Returns:
535
- True if the connection has commit and rollback methods.
536
- """
537
- from sqlspec.protocols import SyncTransactionCapableConnectionProtocol
538
-
539
- return isinstance(obj, SyncTransactionCapableConnectionProtocol)
540
-
541
-
542
- def is_async_transaction_capable(obj: Any) -> "TypeGuard[AsyncTransactionCapableConnectionProtocol]":
543
- """Check if a connection supports async transactions.
544
-
545
- Args:
546
- obj: Connection object to check.
547
-
548
- Returns:
549
- True if the connection has async commit and rollback methods.
550
- """
551
- from sqlspec.protocols import AsyncTransactionCapableConnectionProtocol
552
-
553
- return isinstance(obj, AsyncTransactionCapableConnectionProtocol)
554
-
555
-
556
- def is_sync_transaction_state_capable(obj: Any) -> "TypeGuard[SyncTransactionStateConnectionProtocol]":
557
- """Check if a connection can report sync transaction state.
558
-
559
- Args:
560
- obj: Connection object to check.
561
-
562
- Returns:
563
- True if the connection has in_transaction and begin methods.
564
- """
565
- from sqlspec.protocols import SyncTransactionStateConnectionProtocol
566
-
567
- return isinstance(obj, SyncTransactionStateConnectionProtocol)
568
-
569
-
570
- def is_async_transaction_state_capable(obj: Any) -> "TypeGuard[AsyncTransactionStateConnectionProtocol]":
571
- """Check if a connection can report async transaction state.
572
-
573
- Args:
574
- obj: Connection object to check.
575
-
576
- Returns:
577
- True if the connection has in_transaction and async begin methods.
578
- """
579
- from sqlspec.protocols import AsyncTransactionStateConnectionProtocol
580
-
581
- return isinstance(obj, AsyncTransactionStateConnectionProtocol)
582
-
583
-
584
- def is_sync_closeable_connection(obj: Any) -> "TypeGuard[SyncCloseableConnectionProtocol]":
585
- """Check if a connection can be closed synchronously.
586
-
587
- Args:
588
- obj: Connection object to check.
589
-
590
- Returns:
591
- True if the connection has a close method.
592
- """
593
- from sqlspec.protocols import SyncCloseableConnectionProtocol
594
-
595
- return isinstance(obj, SyncCloseableConnectionProtocol)
596
-
597
-
598
- def is_async_closeable_connection(obj: Any) -> "TypeGuard[AsyncCloseableConnectionProtocol]":
599
- """Check if a connection can be closed asynchronously.
600
-
601
- Args:
602
- obj: Connection object to check.
603
-
604
- Returns:
605
- True if the connection has an async close method.
606
- """
607
- from sqlspec.protocols import AsyncCloseableConnectionProtocol
608
-
609
- return isinstance(obj, AsyncCloseableConnectionProtocol)
610
-
611
-
612
- def is_sync_copy_capable(obj: Any) -> "TypeGuard[SyncCopyCapableConnectionProtocol]":
613
- """Check if a connection supports sync COPY operations.
614
-
615
- Args:
616
- obj: Connection object to check.
617
-
618
- Returns:
619
- True if the connection has copy_from and copy_to methods.
620
- """
621
- from sqlspec.protocols import SyncCopyCapableConnectionProtocol
622
-
623
- return isinstance(obj, SyncCopyCapableConnectionProtocol)
624
-
625
-
626
- def is_async_copy_capable(obj: Any) -> "TypeGuard[AsyncCopyCapableConnectionProtocol]":
627
- """Check if a connection supports async COPY operations.
628
-
629
- Args:
630
- obj: Connection object to check.
631
-
632
- Returns:
633
- True if the connection has async copy_from and copy_to methods.
634
- """
635
- from sqlspec.protocols import AsyncCopyCapableConnectionProtocol
636
-
637
- return isinstance(obj, AsyncCopyCapableConnectionProtocol)
638
-
639
-
640
- def is_sync_pipeline_capable_driver(obj: Any) -> "TypeGuard[SyncPipelineCapableDriverProtocol]":
641
- """Check if a driver supports sync native pipeline execution.
642
-
643
- Args:
644
- obj: Driver object to check.
645
-
646
- Returns:
647
- True if the driver has _execute_pipeline_native method.
648
- """
649
- from sqlspec.protocols import SyncPipelineCapableDriverProtocol
650
-
651
- return isinstance(obj, SyncPipelineCapableDriverProtocol)
652
-
653
-
654
- def is_async_pipeline_capable_driver(obj: Any) -> "TypeGuard[AsyncPipelineCapableDriverProtocol]":
655
- """Check if a driver supports async native pipeline execution.
656
-
657
- Args:
658
- obj: Driver object to check.
659
-
660
- Returns:
661
- True if the driver has async _execute_pipeline_native method.
662
- """
663
- from sqlspec.protocols import AsyncPipelineCapableDriverProtocol
664
-
665
- return isinstance(obj, AsyncPipelineCapableDriverProtocol)
666
-
667
-
668
- # Dataclass utility functions
669
-
670
-
671
644
  def extract_dataclass_fields(
672
645
  obj: "DataclassProtocol",
673
646
  exclude_none: bool = False,
@@ -701,15 +674,15 @@ def extract_dataclass_fields(
701
674
  msg = f"Fields {common} are both included and excluded."
702
675
  raise ValueError(msg)
703
676
 
704
- dataclass_fields: Iterable[Field[Any]] = fields(obj)
677
+ dataclass_fields: list[Field[Any]] = list(fields(obj))
705
678
  if exclude_none:
706
- dataclass_fields = (field for field in dataclass_fields if getattr(obj, field.name) is not None)
679
+ dataclass_fields = [field for field in dataclass_fields if getattr(obj, field.name) is not None]
707
680
  if exclude_empty:
708
- dataclass_fields = (field for field in dataclass_fields if getattr(obj, field.name) is not Empty)
681
+ dataclass_fields = [field for field in dataclass_fields if getattr(obj, field.name) is not Empty]
709
682
  if include:
710
- dataclass_fields = (field for field in dataclass_fields if field.name in include)
683
+ dataclass_fields = [field for field in dataclass_fields if field.name in include]
711
684
  if exclude:
712
- dataclass_fields = (field for field in dataclass_fields if field.name not in exclude)
685
+ dataclass_fields = [field for field in dataclass_fields if field.name not in exclude]
713
686
 
714
687
  return tuple(dataclass_fields)
715
688
 
@@ -721,9 +694,7 @@ def extract_dataclass_items(
721
694
  include: "Optional[AbstractSet[str]]" = None,
722
695
  exclude: "Optional[AbstractSet[str]]" = None,
723
696
  ) -> "tuple[tuple[str, Any], ...]":
724
- """Extract dataclass name, value pairs.
725
-
726
- Unlike the 'asdict' method exports by the stdlib, this function does not pickle values.
697
+ """Extract name-value pairs from a dataclass instance.
727
698
 
728
699
  Args:
729
700
  obj: A dataclass instance.
@@ -746,11 +717,7 @@ def dataclass_to_dict(
746
717
  convert_nested: bool = True,
747
718
  exclude: "Optional[AbstractSet[str]]" = None,
748
719
  ) -> "dict[str, Any]":
749
- """Convert a dataclass to a dictionary.
750
-
751
- This method has important differences to the standard library version:
752
- - it does not deepcopy values
753
- - it does not recurse into collections
720
+ """Convert a dataclass instance to a dictionary.
754
721
 
755
722
  Args:
756
723
  obj: A dataclass instance.
@@ -772,13 +739,11 @@ def dataclass_to_dict(
772
739
  return cast("dict[str, Any]", ret)
773
740
 
774
741
 
775
- def schema_dump(
776
- data: "Union[dict[str, Any], DataclassProtocol, Struct, BaseModel]", exclude_unset: bool = True
777
- ) -> "dict[str, Any]":
742
+ def schema_dump(data: Any, exclude_unset: bool = True) -> "dict[str, Any]":
778
743
  """Dump a data object to a dictionary.
779
744
 
780
745
  Args:
781
- data: :type:`dict[str, Any]` | :class:`DataclassProtocol` | :class:`msgspec.Struct` | :class:`pydantic.BaseModel`
746
+ data: :type:`dict[str, Any]` | :class:`DataclassProtocol` | :class:`msgspec.Struct` | :class:`pydantic.BaseModel` | :class:`AttrsInstance`
782
747
  exclude_unset: :type:`bool` Whether to exclude unset values.
783
748
 
784
749
  Returns:
@@ -791,20 +756,19 @@ def schema_dump(
791
756
  if is_dataclass(data):
792
757
  return dataclass_to_dict(data, exclude_empty=exclude_unset)
793
758
  if is_pydantic_model(data):
794
- return data.model_dump(exclude_unset=exclude_unset)
759
+ return data.model_dump(exclude_unset=exclude_unset) # type: ignore[no-any-return]
795
760
  if is_msgspec_struct(data):
796
761
  if exclude_unset:
797
762
  return {f: val for f in data.__struct_fields__ if (val := getattr(data, f, None)) != UNSET}
798
763
  return {f: getattr(data, f, None) for f in data.__struct_fields__}
764
+ if is_attrs_instance(data):
765
+ return attrs_asdict(data)
799
766
 
800
767
  if has_dict_attribute(data):
801
768
  return data.__dict__
802
769
  return cast("dict[str, Any]", data)
803
770
 
804
771
 
805
- # New type guards for hasattr() pattern replacement
806
-
807
-
808
772
  def can_extract_parameters(obj: Any) -> "TypeGuard[FilterParameterProtocol]":
809
773
  """Check if an object can extract parameters."""
810
774
  from sqlspec.protocols import FilterParameterProtocol
@@ -826,13 +790,6 @@ def has_parameter_value(obj: Any) -> "TypeGuard[ParameterValueProtocol]":
826
790
  return isinstance(obj, ParameterValueProtocol)
827
791
 
828
792
 
829
- def has_risk_level(obj: Any) -> "TypeGuard[HasRiskLevelProtocol]":
830
- """Check if an object has a risk_level attribute."""
831
- from sqlspec.protocols import HasRiskLevelProtocol
832
-
833
- return isinstance(obj, HasRiskLevelProtocol)
834
-
835
-
836
793
  def supports_where(obj: Any) -> "TypeGuard[HasWhereProtocol]":
837
794
  """Check if an SQL expression supports WHERE clauses."""
838
795
  from sqlspec.protocols import HasWhereProtocol
@@ -922,3 +879,257 @@ def has_to_statement(obj: Any) -> "TypeGuard[Any]":
922
879
  from sqlspec.protocols import HasToStatementProtocol
923
880
 
924
881
  return isinstance(obj, HasToStatementProtocol)
882
+
883
+
884
+ def has_attr(obj: Any, attr: str) -> bool:
885
+ """Safe replacement for hasattr() that works with mypyc.
886
+
887
+ Args:
888
+ obj: Object to check
889
+ attr: Attribute name to look for
890
+
891
+ Returns:
892
+ True if attribute exists, False otherwise
893
+ """
894
+ try:
895
+ getattr(obj, attr)
896
+ except AttributeError:
897
+ return False
898
+ return True
899
+
900
+
901
+ def get_node_this(node: "exp.Expression", default: Optional[Any] = None) -> Any:
902
+ """Safely get the 'this' attribute from a SQLGlot node.
903
+
904
+ Args:
905
+ node: The SQLGlot expression node
906
+ default: Default value if 'this' attribute doesn't exist
907
+
908
+ Returns:
909
+ The value of node.this or the default value
910
+ """
911
+ try:
912
+ return node.this
913
+ except AttributeError:
914
+ return default
915
+
916
+
917
+ def has_this_attribute(node: "exp.Expression") -> bool:
918
+ """Check if a node has the 'this' attribute without using hasattr().
919
+
920
+ Args:
921
+ node: The SQLGlot expression node
922
+
923
+ Returns:
924
+ True if the node has a 'this' attribute, False otherwise
925
+ """
926
+ try:
927
+ _ = node.this
928
+ except AttributeError:
929
+ return False
930
+ return True
931
+
932
+
933
+ def get_node_expressions(node: "exp.Expression", default: Optional[Any] = None) -> Any:
934
+ """Safely get the 'expressions' attribute from a SQLGlot node.
935
+
936
+ Args:
937
+ node: The SQLGlot expression node
938
+ default: Default value if 'expressions' attribute doesn't exist
939
+
940
+ Returns:
941
+ The value of node.expressions or the default value
942
+ """
943
+ try:
944
+ return node.expressions
945
+ except AttributeError:
946
+ return default
947
+
948
+
949
+ def has_expressions_attribute(node: "exp.Expression") -> bool:
950
+ """Check if a node has the 'expressions' attribute without using hasattr().
951
+
952
+ Args:
953
+ node: The SQLGlot expression node
954
+
955
+ Returns:
956
+ True if the node has an 'expressions' attribute, False otherwise
957
+ """
958
+ try:
959
+ _ = node.expressions
960
+ except AttributeError:
961
+ return False
962
+ return True
963
+
964
+
965
+ def get_literal_parent(literal: "exp.Expression", default: Optional[Any] = None) -> Any:
966
+ """Safely get the 'parent' attribute from a SQLGlot literal.
967
+
968
+ Args:
969
+ literal: The SQLGlot expression
970
+ default: Default value if 'parent' attribute doesn't exist
971
+
972
+ Returns:
973
+ The value of literal.parent or the default value
974
+ """
975
+ try:
976
+ return literal.parent
977
+ except AttributeError:
978
+ return default
979
+
980
+
981
+ def has_parent_attribute(literal: "exp.Expression") -> bool:
982
+ """Check if a literal has the 'parent' attribute without using hasattr().
983
+
984
+ Args:
985
+ literal: The SQLGlot expression
986
+
987
+ Returns:
988
+ True if the literal has a 'parent' attribute, False otherwise
989
+ """
990
+ try:
991
+ _ = literal.parent
992
+ except AttributeError:
993
+ return False
994
+ return True
995
+
996
+
997
+ def is_string_literal(literal: "exp.Literal") -> bool:
998
+ """Check if a literal is a string literal without using hasattr().
999
+
1000
+ Args:
1001
+ literal: The SQLGlot Literal expression
1002
+
1003
+ Returns:
1004
+ True if the literal is a string, False otherwise
1005
+ """
1006
+ try:
1007
+ return bool(literal.is_string)
1008
+ except AttributeError:
1009
+ try:
1010
+ return isinstance(literal.this, str)
1011
+ except AttributeError:
1012
+ return False
1013
+
1014
+
1015
+ def is_number_literal(literal: "exp.Literal") -> bool:
1016
+ """Check if a literal is a number literal without using hasattr().
1017
+
1018
+ Args:
1019
+ literal: The SQLGlot Literal expression
1020
+
1021
+ Returns:
1022
+ True if the literal is a number, False otherwise
1023
+ """
1024
+ try:
1025
+ return bool(literal.is_number)
1026
+ except AttributeError:
1027
+ try:
1028
+ if literal.this is not None:
1029
+ float(str(literal.this))
1030
+ return True
1031
+ except (AttributeError, ValueError, TypeError):
1032
+ pass
1033
+ return False
1034
+
1035
+
1036
+ def get_initial_expression(context: Any) -> "Optional[exp.Expression]":
1037
+ """Safely get initial_expression from context.
1038
+
1039
+ Args:
1040
+ context: SQL processing context
1041
+
1042
+ Returns:
1043
+ The initial expression or None if not available
1044
+ """
1045
+ try:
1046
+ return context.initial_expression # type: ignore[no-any-return]
1047
+ except AttributeError:
1048
+ return None
1049
+
1050
+
1051
+ def expression_has_limit(expr: "Optional[exp.Expression]") -> bool:
1052
+ """Check if an expression has a limit clause.
1053
+
1054
+ Args:
1055
+ expr: SQLGlot expression to check
1056
+
1057
+ Returns:
1058
+ True if expression has limit in args, False otherwise
1059
+ """
1060
+ if expr is None:
1061
+ return False
1062
+ try:
1063
+ return "limit" in expr.args
1064
+ except AttributeError:
1065
+ return False
1066
+
1067
+
1068
+ def get_value_attribute(obj: Any) -> Any:
1069
+ """Safely get the 'value' attribute from an object.
1070
+
1071
+ Args:
1072
+ obj: Object to get value from
1073
+
1074
+ Returns:
1075
+ The value attribute or the object itself if no value attribute
1076
+ """
1077
+ try:
1078
+ return obj.value
1079
+ except AttributeError:
1080
+ return obj
1081
+
1082
+
1083
+ def get_param_style_and_name(param: Any) -> "tuple[Optional[str], Optional[str]]":
1084
+ """Safely get style and name attributes from a parameter.
1085
+
1086
+ Args:
1087
+ param: Parameter object
1088
+
1089
+ Returns:
1090
+ Tuple of (style, name) or (None, None) if attributes don't exist
1091
+ """
1092
+ try:
1093
+ style = param.style
1094
+ name = param.name
1095
+ except AttributeError:
1096
+ return None, None
1097
+ return style, name
1098
+
1099
+
1100
+ def is_copy_statement(expression: Any) -> "TypeGuard[exp.Expression]":
1101
+ """Check if the SQL expression is a PostgreSQL COPY statement.
1102
+
1103
+ Args:
1104
+ expression: The SQL expression to check
1105
+
1106
+ Returns:
1107
+ True if this is a COPY statement, False otherwise
1108
+ """
1109
+ from sqlglot import exp
1110
+
1111
+ if expression is None:
1112
+ return False
1113
+
1114
+ if has_attr(exp, "Copy") and isinstance(expression, getattr(exp, "Copy", type(None))):
1115
+ return True
1116
+
1117
+ if isinstance(expression, (exp.Command, exp.Anonymous)):
1118
+ sql_text = str(expression).strip().upper()
1119
+ return sql_text.startswith("COPY ")
1120
+
1121
+ return False
1122
+
1123
+
1124
+ def is_typed_parameter(obj: Any) -> "TypeGuard[Any]":
1125
+ """Check if an object is a typed parameter.
1126
+
1127
+ Args:
1128
+ obj: The object to check
1129
+
1130
+ Returns:
1131
+ True if the object is a TypedParameter, False otherwise
1132
+ """
1133
+ from sqlspec.core.parameters import TypedParameter
1134
+
1135
+ return isinstance(obj, TypedParameter)