sqlspec 0.26.0__py3-none-any.whl → 0.28.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 (212) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +155 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +880 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +74 -2
  9. sqlspec/adapters/adbc/driver.py +226 -58
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +44 -50
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +536 -0
  16. sqlspec/adapters/aiosqlite/config.py +86 -16
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
  18. sqlspec/adapters/aiosqlite/driver.py +127 -38
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +1 -1
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +503 -0
  26. sqlspec/adapters/asyncmy/config.py +59 -17
  27. sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
  28. sqlspec/adapters/asyncmy/driver.py +293 -62
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +460 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +48 -2
  38. sqlspec/adapters/asyncpg/driver.py +153 -23
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +585 -0
  44. sqlspec/adapters/bigquery/config.py +36 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +489 -144
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +55 -23
  50. sqlspec/adapters/duckdb/_types.py +2 -2
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +563 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +225 -44
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +5 -5
  59. sqlspec/adapters/duckdb/type_converter.py +51 -21
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1628 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +475 -86
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +765 -0
  69. sqlspec/adapters/oracledb/migrations.py +316 -25
  70. sqlspec/adapters/oracledb/type_converter.py +91 -16
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +483 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
  77. sqlspec/adapters/psqlpy/driver.py +108 -41
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +40 -11
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +962 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +91 -3
  87. sqlspec/adapters/psycopg/driver.py +200 -78
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +582 -0
  95. sqlspec/adapters/sqlite/config.py +85 -16
  96. sqlspec/adapters/sqlite/data_dictionary.py +34 -2
  97. sqlspec/adapters/sqlite/driver.py +120 -52
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +5 -5
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +91 -58
  104. sqlspec/builder/_column.py +5 -5
  105. sqlspec/builder/_ddl.py +98 -89
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +41 -44
  109. sqlspec/builder/_insert.py +5 -82
  110. sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +9 -11
  113. sqlspec/builder/_select.py +1313 -25
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +76 -69
  116. sqlspec/config.py +331 -62
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +18 -18
  119. sqlspec/core/compiler.py +6 -8
  120. sqlspec/core/filters.py +55 -47
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +234 -47
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +32 -31
  126. sqlspec/core/type_conversion.py +3 -2
  127. sqlspec/driver/__init__.py +1 -3
  128. sqlspec/driver/_async.py +183 -160
  129. sqlspec/driver/_common.py +197 -109
  130. sqlspec/driver/_sync.py +189 -161
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +70 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +69 -61
  142. sqlspec/extensions/fastapi/__init__.py +21 -0
  143. sqlspec/extensions/fastapi/extension.py +331 -0
  144. sqlspec/extensions/fastapi/providers.py +543 -0
  145. sqlspec/extensions/flask/__init__.py +36 -0
  146. sqlspec/extensions/flask/_state.py +71 -0
  147. sqlspec/extensions/flask/_utils.py +40 -0
  148. sqlspec/extensions/flask/extension.py +389 -0
  149. sqlspec/extensions/litestar/__init__.py +21 -4
  150. sqlspec/extensions/litestar/cli.py +54 -10
  151. sqlspec/extensions/litestar/config.py +56 -266
  152. sqlspec/extensions/litestar/handlers.py +46 -17
  153. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  154. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  155. sqlspec/extensions/litestar/plugin.py +349 -224
  156. sqlspec/extensions/litestar/providers.py +25 -25
  157. sqlspec/extensions/litestar/store.py +265 -0
  158. sqlspec/extensions/starlette/__init__.py +10 -0
  159. sqlspec/extensions/starlette/_state.py +25 -0
  160. sqlspec/extensions/starlette/_utils.py +52 -0
  161. sqlspec/extensions/starlette/extension.py +254 -0
  162. sqlspec/extensions/starlette/middleware.py +154 -0
  163. sqlspec/loader.py +30 -49
  164. sqlspec/migrations/base.py +200 -76
  165. sqlspec/migrations/commands.py +591 -62
  166. sqlspec/migrations/context.py +6 -9
  167. sqlspec/migrations/fix.py +199 -0
  168. sqlspec/migrations/loaders.py +47 -19
  169. sqlspec/migrations/runner.py +241 -75
  170. sqlspec/migrations/tracker.py +237 -21
  171. sqlspec/migrations/utils.py +51 -3
  172. sqlspec/migrations/validation.py +177 -0
  173. sqlspec/protocols.py +106 -36
  174. sqlspec/storage/_utils.py +85 -0
  175. sqlspec/storage/backends/fsspec.py +133 -107
  176. sqlspec/storage/backends/local.py +78 -51
  177. sqlspec/storage/backends/obstore.py +276 -168
  178. sqlspec/storage/registry.py +75 -39
  179. sqlspec/typing.py +30 -84
  180. sqlspec/utils/__init__.py +25 -4
  181. sqlspec/utils/arrow_helpers.py +81 -0
  182. sqlspec/utils/config_resolver.py +6 -6
  183. sqlspec/utils/correlation.py +4 -5
  184. sqlspec/utils/data_transformation.py +3 -2
  185. sqlspec/utils/deprecation.py +9 -8
  186. sqlspec/utils/fixtures.py +4 -4
  187. sqlspec/utils/logging.py +46 -6
  188. sqlspec/utils/module_loader.py +205 -5
  189. sqlspec/utils/portal.py +311 -0
  190. sqlspec/utils/schema.py +288 -0
  191. sqlspec/utils/serializers.py +113 -4
  192. sqlspec/utils/sync_tools.py +36 -22
  193. sqlspec/utils/text.py +1 -2
  194. sqlspec/utils/type_guards.py +136 -20
  195. sqlspec/utils/version.py +433 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
  197. sqlspec-0.28.0.dist-info/RECORD +221 -0
  198. sqlspec/builder/mixins/__init__.py +0 -55
  199. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  200. sqlspec/builder/mixins/_delete_operations.py +0 -50
  201. sqlspec/builder/mixins/_insert_operations.py +0 -282
  202. sqlspec/builder/mixins/_merge_operations.py +0 -698
  203. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  204. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  205. sqlspec/builder/mixins/_select_operations.py +0 -930
  206. sqlspec/builder/mixins/_update_operations.py +0 -199
  207. sqlspec/builder/mixins/_where_clause.py +0 -1298
  208. sqlspec-0.26.0.dist-info/RECORD +0 -157
  209. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  210. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
  211. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
  212. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,7 +7,9 @@ understand type narrowing, replacing defensive hasattr() and duck typing pattern
7
7
  from collections.abc import Sequence
8
8
  from collections.abc import Set as AbstractSet
9
9
  from functools import lru_cache
10
- from typing import TYPE_CHECKING, Any, Optional, Union, cast
10
+ from typing import TYPE_CHECKING, Any, cast
11
+
12
+ from typing_extensions import is_typeddict
11
13
 
12
14
  from sqlspec.typing import (
13
15
  ATTRS_INSTALLED,
@@ -24,9 +26,9 @@ from sqlspec.typing import (
24
26
 
25
27
  if TYPE_CHECKING:
26
28
  from dataclasses import Field
29
+ from typing import TypeGuard
27
30
 
28
31
  from sqlglot import exp
29
- from typing_extensions import TypeGuard
30
32
 
31
33
  from sqlspec._typing import AttrsInstanceStub, BaseModelStub, DTODataStub, StructStub
32
34
  from sqlspec.builder import Select
@@ -46,6 +48,7 @@ if TYPE_CHECKING:
46
48
  ObjectStoreItemProtocol,
47
49
  ParameterValueProtocol,
48
50
  SQLBuilderProtocol,
51
+ SupportsArrowResults,
49
52
  WithMethodProtocol,
50
53
  )
51
54
  from sqlspec.typing import SupportedSchemaModel
@@ -100,6 +103,7 @@ __all__ = (
100
103
  "is_indexable_row",
101
104
  "is_iterable_parameters",
102
105
  "is_limit_offset_filter",
106
+ "is_local_path",
103
107
  "is_msgspec_struct",
104
108
  "is_msgspec_struct_with_field",
105
109
  "is_msgspec_struct_without_field",
@@ -117,8 +121,11 @@ __all__ = (
117
121
  "is_select_builder",
118
122
  "is_statement_filter",
119
123
  "is_string_literal",
124
+ "is_typed_dict",
120
125
  "is_typed_parameter",
121
126
  "schema_dump",
127
+ "supports_arrow_native",
128
+ "supports_arrow_results",
122
129
  "supports_limit",
123
130
  "supports_offset",
124
131
  "supports_order_by",
@@ -126,6 +133,18 @@ __all__ = (
126
133
  )
127
134
 
128
135
 
136
+ def is_typed_dict(obj: Any) -> "TypeGuard[type]":
137
+ """Check if an object is a TypedDict class.
138
+
139
+ Args:
140
+ obj: The object to check
141
+
142
+ Returns:
143
+ True if the object is a TypedDict class, False otherwise
144
+ """
145
+ return is_typeddict(obj)
146
+
147
+
129
148
  def is_statement_filter(obj: Any) -> "TypeGuard[StatementFilter]":
130
149
  """Check if an object implements the StatementFilter protocol.
131
150
 
@@ -434,7 +453,7 @@ def is_msgspec_struct_without_field(obj: Any, field_name: str) -> "TypeGuard[Str
434
453
 
435
454
 
436
455
  @lru_cache(maxsize=500)
437
- def _detect_rename_pattern(field_name: str, encode_name: str) -> "Optional[str]":
456
+ def _detect_rename_pattern(field_name: str, encode_name: str) -> "str | None":
438
457
  """Detect the rename pattern by comparing field name transformations.
439
458
 
440
459
  Args:
@@ -458,7 +477,7 @@ def _detect_rename_pattern(field_name: str, encode_name: str) -> "Optional[str]"
458
477
  return None
459
478
 
460
479
 
461
- def get_msgspec_rename_config(schema_type: type) -> "Optional[str]":
480
+ def get_msgspec_rename_config(schema_type: type) -> "str | None":
462
481
  """Extract msgspec rename configuration from a struct type.
463
482
 
464
483
  Analyzes field name transformations to detect the rename pattern used by msgspec.
@@ -611,7 +630,7 @@ def is_schema(obj: Any) -> "TypeGuard[SupportedSchemaModel]":
611
630
  )
612
631
 
613
632
 
614
- def is_schema_or_dict(obj: Any) -> "TypeGuard[Union[SupportedSchemaModel, dict[str, Any]]]":
633
+ def is_schema_or_dict(obj: Any) -> "TypeGuard[SupportedSchemaModel | dict[str, Any]]":
615
634
  """Check if a value is a msgspec Struct, Pydantic model, or dict.
616
635
 
617
636
  Args:
@@ -649,7 +668,7 @@ def is_schema_without_field(obj: Any, field_name: str) -> "TypeGuard[SupportedSc
649
668
  return not is_schema_with_field(obj, field_name)
650
669
 
651
670
 
652
- def is_schema_or_dict_with_field(obj: Any, field_name: str) -> "TypeGuard[Union[SupportedSchemaModel, dict[str, Any]]]":
671
+ def is_schema_or_dict_with_field(obj: Any, field_name: str) -> "TypeGuard[SupportedSchemaModel | dict[str, Any]]":
653
672
  """Check if a value is a msgspec Struct, Pydantic model, or dict with a specific field.
654
673
 
655
674
  Args:
@@ -662,9 +681,7 @@ def is_schema_or_dict_with_field(obj: Any, field_name: str) -> "TypeGuard[Union[
662
681
  return is_schema_with_field(obj, field_name) or is_dict_with_field(obj, field_name)
663
682
 
664
683
 
665
- def is_schema_or_dict_without_field(
666
- obj: Any, field_name: str
667
- ) -> "TypeGuard[Union[SupportedSchemaModel, dict[str, Any]]]":
684
+ def is_schema_or_dict_without_field(obj: Any, field_name: str) -> "TypeGuard[SupportedSchemaModel | dict[str, Any]]":
668
685
  """Check if a value is a msgspec Struct, Pydantic model, or dict without a specific field.
669
686
 
670
687
  Args:
@@ -721,8 +738,8 @@ def extract_dataclass_fields(
721
738
  obj: "DataclassProtocol",
722
739
  exclude_none: bool = False,
723
740
  exclude_empty: bool = False,
724
- include: "Optional[AbstractSet[str]]" = None,
725
- exclude: "Optional[AbstractSet[str]]" = None,
741
+ include: "AbstractSet[str] | None" = None,
742
+ exclude: "AbstractSet[str] | None" = None,
726
743
  ) -> "tuple[Field[Any], ...]":
727
744
  """Extract dataclass fields.
728
745
 
@@ -767,8 +784,8 @@ def extract_dataclass_items(
767
784
  obj: "DataclassProtocol",
768
785
  exclude_none: bool = False,
769
786
  exclude_empty: bool = False,
770
- include: "Optional[AbstractSet[str]]" = None,
771
- exclude: "Optional[AbstractSet[str]]" = None,
787
+ include: "AbstractSet[str] | None" = None,
788
+ exclude: "AbstractSet[str] | None" = None,
772
789
  ) -> "tuple[tuple[str, Any], ...]":
773
790
  """Extract name-value pairs from a dataclass instance.
774
791
 
@@ -791,7 +808,7 @@ def dataclass_to_dict(
791
808
  exclude_none: bool = False,
792
809
  exclude_empty: bool = False,
793
810
  convert_nested: bool = True,
794
- exclude: "Optional[AbstractSet[str]]" = None,
811
+ exclude: "AbstractSet[str] | None" = None,
795
812
  ) -> "dict[str, Any]":
796
813
  """Convert a dataclass instance to a dictionary.
797
814
 
@@ -978,7 +995,7 @@ def has_attr(obj: Any, attr: str) -> bool:
978
995
  return True
979
996
 
980
997
 
981
- def get_node_this(node: "exp.Expression", default: Optional[Any] = None) -> Any:
998
+ def get_node_this(node: "exp.Expression", default: Any | None = None) -> Any:
982
999
  """Safely get the 'this' attribute from a SQLGlot node.
983
1000
 
984
1001
  Args:
@@ -1010,7 +1027,7 @@ def has_this_attribute(node: "exp.Expression") -> bool:
1010
1027
  return True
1011
1028
 
1012
1029
 
1013
- def get_node_expressions(node: "exp.Expression", default: Optional[Any] = None) -> Any:
1030
+ def get_node_expressions(node: "exp.Expression", default: Any | None = None) -> Any:
1014
1031
  """Safely get the 'expressions' attribute from a SQLGlot node.
1015
1032
 
1016
1033
  Args:
@@ -1042,7 +1059,7 @@ def has_expressions_attribute(node: "exp.Expression") -> bool:
1042
1059
  return True
1043
1060
 
1044
1061
 
1045
- def get_literal_parent(literal: "exp.Expression", default: Optional[Any] = None) -> Any:
1062
+ def get_literal_parent(literal: "exp.Expression", default: Any | None = None) -> Any:
1046
1063
  """Safely get the 'parent' attribute from a SQLGlot literal.
1047
1064
 
1048
1065
  Args:
@@ -1113,7 +1130,7 @@ def is_number_literal(literal: "exp.Literal") -> bool:
1113
1130
  return False
1114
1131
 
1115
1132
 
1116
- def get_initial_expression(context: Any) -> "Optional[exp.Expression]":
1133
+ def get_initial_expression(context: Any) -> "exp.Expression | None":
1117
1134
  """Safely get initial_expression from context.
1118
1135
 
1119
1136
  Args:
@@ -1128,7 +1145,7 @@ def get_initial_expression(context: Any) -> "Optional[exp.Expression]":
1128
1145
  return None
1129
1146
 
1130
1147
 
1131
- def expression_has_limit(expr: "Optional[exp.Expression]") -> bool:
1148
+ def expression_has_limit(expr: "exp.Expression | None") -> bool:
1132
1149
  """Check if an expression has a limit clause.
1133
1150
 
1134
1151
  Args:
@@ -1160,7 +1177,7 @@ def get_value_attribute(obj: Any) -> Any:
1160
1177
  return obj
1161
1178
 
1162
1179
 
1163
- def get_param_style_and_name(param: Any) -> "tuple[Optional[str], Optional[str]]":
1180
+ def get_param_style_and_name(param: Any) -> "tuple[str | None, str | None]":
1164
1181
  """Safely get style and name attributes from a parameter.
1165
1182
 
1166
1183
  Args:
@@ -1242,3 +1259,102 @@ def has_expression_and_parameters(obj: Any) -> bool:
1242
1259
  True if the object has both attributes, False otherwise
1243
1260
  """
1244
1261
  return hasattr(obj, "expression") and hasattr(obj, "parameters")
1262
+
1263
+
1264
+ WINDOWS_DRIVE_PATTERN_LENGTH = 3
1265
+
1266
+
1267
+ def is_local_path(uri: str) -> bool:
1268
+ r"""Check if URI represents a local filesystem path.
1269
+
1270
+ Detects local paths including:
1271
+ - file:// URIs
1272
+ - Absolute paths (Unix: /, Windows: C:\\)
1273
+ - Relative paths (., .., ~)
1274
+
1275
+ Args:
1276
+ uri: URI or path string to check.
1277
+
1278
+ Returns:
1279
+ True if uri is a local path, False for remote URIs.
1280
+
1281
+ Examples:
1282
+ >>> is_local_path("file:///data/file.txt")
1283
+ True
1284
+ >>> is_local_path("/absolute/path")
1285
+ True
1286
+ >>> is_local_path("s3://bucket/key")
1287
+ False
1288
+ """
1289
+ if not uri:
1290
+ return False
1291
+
1292
+ if "://" in uri and not uri.startswith("file://"):
1293
+ return False
1294
+
1295
+ if uri.startswith("file://"):
1296
+ return True
1297
+
1298
+ if uri.startswith("/"):
1299
+ return True
1300
+
1301
+ if uri.startswith((".", "~")):
1302
+ return True
1303
+
1304
+ if len(uri) >= WINDOWS_DRIVE_PATTERN_LENGTH and uri[1:3] == ":\\":
1305
+ return True
1306
+
1307
+ return "/" in uri or "\\" in uri
1308
+
1309
+
1310
+ def supports_arrow_native(backend: Any) -> bool:
1311
+ """Check if storage backend supports native Arrow operations.
1312
+
1313
+ Some storage backends (like certain obstore stores) have native
1314
+ Arrow read/write support, which is faster than going through bytes.
1315
+
1316
+ Args:
1317
+ backend: Storage backend instance to check.
1318
+
1319
+ Returns:
1320
+ True if backend has native read_arrow/write_arrow methods.
1321
+
1322
+ Examples:
1323
+ >>> from sqlspec.storage.backends.obstore import ObStoreBackend
1324
+ >>> backend = ObStoreBackend("file:///tmp")
1325
+ >>> supports_arrow_native(backend)
1326
+ False
1327
+ """
1328
+ from sqlspec.protocols import ObjectStoreProtocol
1329
+
1330
+ if not isinstance(backend, ObjectStoreProtocol):
1331
+ return False
1332
+
1333
+ try:
1334
+ store = backend.store # type: ignore[attr-defined]
1335
+ return callable(getattr(store, "read_arrow", None))
1336
+ except AttributeError:
1337
+ return False
1338
+
1339
+
1340
+ def supports_arrow_results(obj: Any) -> "TypeGuard[SupportsArrowResults]":
1341
+ """Check if object supports Arrow result format.
1342
+
1343
+ Use this type guard to check if a driver or adapter supports returning
1344
+ query results in Apache Arrow format via select_to_arrow() method.
1345
+
1346
+ Args:
1347
+ obj: Object to check for Arrow results support.
1348
+
1349
+ Returns:
1350
+ True if object implements SupportsArrowResults protocol.
1351
+
1352
+ Examples:
1353
+ >>> from sqlspec.adapters.duckdb import DuckDBDriver
1354
+ >>> driver = DuckDBDriver(...)
1355
+ >>> supports_arrow_results(driver)
1356
+ True
1357
+ """
1358
+ from sqlspec.protocols import SupportsArrowResults
1359
+
1360
+ return isinstance(obj, SupportsArrowResults)