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