sqlspec 0.56.0__py3-none-any.whl → 0.56.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.
Files changed (34) hide show
  1. sqlspec/adapters/adbc/core.py +148 -65
  2. sqlspec/adapters/adbc/driver.py +11 -3
  3. sqlspec/adapters/bigquery/driver.py +21 -2
  4. sqlspec/adapters/duckdb/core.py +2 -2
  5. sqlspec/adapters/oracledb/_json_handlers.py +10 -7
  6. sqlspec/adapters/oracledb/_param_types.py +3 -2
  7. sqlspec/adapters/oracledb/_uuid_handlers.py +2 -1
  8. sqlspec/adapters/oracledb/adk/store.py +2 -2
  9. sqlspec/adapters/oracledb/config.py +10 -28
  10. sqlspec/adapters/oracledb/core.py +390 -57
  11. sqlspec/adapters/oracledb/data_dictionary.py +21 -0
  12. sqlspec/adapters/oracledb/driver.py +191 -5
  13. sqlspec/adapters/psqlpy/core.py +2 -1
  14. sqlspec/adapters/spanner/type_converter.py +15 -4
  15. sqlspec/builder/_explain.py +73 -43
  16. sqlspec/core/compiler.py +69 -1
  17. sqlspec/core/explain.py +2 -0
  18. sqlspec/core/statement.py +8 -13
  19. sqlspec/core/type_converter.py +3 -1
  20. sqlspec/data_dictionary/_types.py +2 -2
  21. sqlspec/driver/_async.py +2 -2
  22. sqlspec/driver/_common.py +1 -1
  23. sqlspec/driver/_sync.py +2 -2
  24. sqlspec/migrations/base.py +10 -10
  25. sqlspec/migrations/commands.py +1 -1
  26. sqlspec/utils/arrow_helpers.py +50 -3
  27. sqlspec/utils/schema.py +3 -2
  28. sqlspec/utils/type_converters.py +2 -1
  29. sqlspec/utils/uuids.py +50 -3
  30. {sqlspec-0.56.0.dist-info → sqlspec-0.56.2.dist-info}/METADATA +3 -5
  31. {sqlspec-0.56.0.dist-info → sqlspec-0.56.2.dist-info}/RECORD +34 -34
  32. {sqlspec-0.56.0.dist-info → sqlspec-0.56.2.dist-info}/WHEEL +0 -0
  33. {sqlspec-0.56.0.dist-info → sqlspec-0.56.2.dist-info}/entry_points.txt +0 -0
  34. {sqlspec-0.56.0.dist-info → sqlspec-0.56.2.dist-info}/licenses/LICENSE +0 -0
@@ -46,6 +46,7 @@ from sqlspec.utils.module_loader import import_string
46
46
  from sqlspec.utils.serializers import to_json
47
47
  from sqlspec.utils.type_converters import build_uuid_coercions
48
48
  from sqlspec.utils.type_guards import has_rowcount, has_sqlstate
49
+ from sqlspec.utils.uuids import uuid_from_string
49
50
 
50
51
  if TYPE_CHECKING:
51
52
  from collections.abc import Callable, Mapping
@@ -635,6 +636,8 @@ _BASE_TYPE_COERCION_MAP: Final[dict[type, Any]] = {
635
636
  tuple: _convert_array_for_postgres_adbc,
636
637
  list: _convert_array_for_postgres_adbc,
637
638
  dict: _identity,
639
+ UUID: _identity,
640
+ **build_uuid_coercions(native=True),
638
641
  }
639
642
 
640
643
 
@@ -873,29 +876,27 @@ def prepare_postgres_uuid_bindings(
873
876
  if not is_postgres_dialect(dialect):
874
877
  return compiled_sql, prepared_parameters
875
878
  if is_many:
876
- uuid_ordinals = _detect_batch_uuid_ordinals(prepared_parameters)
879
+ scalar_ordinals, array_ordinals = _detect_batch_uuid_ordinals(prepared_parameters)
877
880
  elif isinstance(prepared_parameters, (list, tuple)):
878
- uuid_ordinals = tuple(
879
- index for index, value in enumerate(prepared_parameters, 1) if isinstance(value, _UUID_TYPES)
880
- )
881
+ scalar_ordinals, array_ordinals = _detect_uuid_ordinals(prepared_parameters)
881
882
  else:
882
- uuid_ordinals = ()
883
- if not uuid_ordinals:
883
+ scalar_ordinals, array_ordinals = (), ()
884
+ if not scalar_ordinals and not array_ordinals:
884
885
  return compiled_sql, prepared_parameters
885
886
  sqlglot_dialect = "postgres" if dialect == "postgresql" else dialect
886
- rewritten_sql, effective_ordinals = _rewrite_postgres_uuid_placeholders(
887
- compiled_sql, tuple(sorted(uuid_ordinals)), sqlglot_dialect
887
+ rewritten_sql, effective_scalars, effective_arrays = _rewrite_postgres_uuid_placeholders(
888
+ compiled_sql, tuple(sorted(scalar_ordinals)), tuple(sorted(array_ordinals)), sqlglot_dialect
888
889
  )
889
- if not effective_ordinals:
890
+ if not effective_scalars and not effective_arrays:
890
891
  return compiled_sql, prepared_parameters
891
892
  if is_many:
892
893
  converted_rows = [
893
- _convert_uuid_row(row, effective_ordinals, row_number)
894
+ _convert_uuid_row(row, effective_scalars, effective_arrays, row_number)
894
895
  for row_number, row in enumerate(prepared_parameters, 1)
895
896
  ]
896
897
  converted_parameters = tuple(converted_rows) if isinstance(prepared_parameters, tuple) else converted_rows
897
898
  else:
898
- converted_parameters = _convert_uuid_row(prepared_parameters, effective_ordinals, 1)
899
+ converted_parameters = _convert_uuid_row(prepared_parameters, effective_scalars, effective_arrays, 1)
899
900
  return rewritten_sql, converted_parameters
900
901
 
901
902
 
@@ -920,16 +921,42 @@ def _direct_parameter_cast(parameter: exp.Parameter) -> "exp.Cast | None":
920
921
  return None
921
922
 
922
923
 
924
+ def _is_uuid_data_type(target: Any) -> bool:
925
+ """Report whether a cast target names the scalar ``UUID`` type."""
926
+ if not isinstance(target, exp.DataType):
927
+ return False
928
+ if target.this == exp.DataType.Type.UUID:
929
+ return True
930
+ target_kind = target.args.get("kind")
931
+ if isinstance(target_kind, exp.Dot):
932
+ target_kind = target_kind.expression
933
+ return (
934
+ target.this == exp.DataType.Type.USERDEFINED
935
+ and isinstance(target_kind, exp.Identifier)
936
+ and target_kind.name.lower() == "uuid"
937
+ )
938
+
939
+
940
+ def _is_uuid_array_data_type(target: Any) -> bool:
941
+ """Report whether a cast target names a one-dimensional ``UUID[]`` type."""
942
+ if not isinstance(target, exp.DataType) or target.this != exp.DataType.Type.ARRAY:
943
+ return False
944
+ element_types = target.expressions
945
+ return len(element_types) == 1 and _is_uuid_data_type(element_types[0])
946
+
947
+
923
948
  @lru_cache(maxsize=256)
924
949
  def _rewrite_postgres_uuid_placeholders(
925
- sql: str, uuid_ordinals: "tuple[int, ...]", dialect: str
926
- ) -> "tuple[str, tuple[int, ...]]":
927
- """Wrap requested UUID parameter placeholders in explicit UUID casts.
928
-
929
- Placeholders that already carry a different explicit cast stay authoritative
930
- and are excluded from the returned effective ordinals. Results are memoized
931
- per ``(sql, uuid_ordinals, dialect)`` so repeated executions of the same
932
- statement shape reuse the rewrite without parsing again.
950
+ sql: str, scalar_ordinals: "tuple[int, ...]", array_ordinals: "tuple[int, ...]", dialect: str
951
+ ) -> "tuple[str, tuple[int, ...], tuple[int, ...]]":
952
+ """Wrap requested UUID parameter placeholders in explicit UUID or UUID[] casts.
953
+
954
+ Placeholders already carrying the cast their ordinal requires keep that cast
955
+ and stay effective. Placeholders carrying any other explicit cast stay
956
+ authoritative and are excluded from the returned effective ordinals. Results
957
+ are memoized per ``(sql, scalar_ordinals, array_ordinals, dialect)`` so
958
+ repeated executions of the same statement shape reuse the rewrite without
959
+ parsing again.
933
960
  """
934
961
  try:
935
962
  expressions = cast(
@@ -943,7 +970,8 @@ def _rewrite_postgres_uuid_placeholders(
943
970
  msg = "Failed to parse PostgreSQL ADBC SQL for UUID parameter binding: SQLGlot returned no statements."
944
971
  raise SQLSpecError(msg)
945
972
 
946
- requested = set(uuid_ordinals)
973
+ array_requested = set(array_ordinals)
974
+ requested = set(scalar_ordinals) | array_requested
947
975
  authoritative: set[int] = set()
948
976
  for expression in expressions:
949
977
  for parameter in expression.find_all(exp.Parameter):
@@ -951,23 +979,18 @@ def _rewrite_postgres_uuid_placeholders(
951
979
  if ordinal is None or ordinal not in requested:
952
980
  continue
953
981
  cast_expression = _direct_parameter_cast(parameter)
954
- if cast_expression is not None:
955
- target = cast_expression.args.get("to")
956
- target_kind = target.args.get("kind") if isinstance(target, exp.DataType) else None
957
- if isinstance(target_kind, exp.Dot):
958
- target_kind = target_kind.expression
959
- is_uuid_cast = isinstance(target, exp.DataType) and (
960
- target.this == exp.DataType.Type.UUID
961
- or (
962
- target.this == exp.DataType.Type.USERDEFINED
963
- and isinstance(target_kind, exp.Identifier)
964
- and target_kind.name.lower() == "uuid"
965
- )
966
- )
967
- if not is_uuid_cast:
968
- authoritative.add(ordinal)
969
- effective = tuple(ordinal for ordinal in uuid_ordinals if ordinal not in authoritative)
970
- effective_set = set(effective)
982
+ if cast_expression is None:
983
+ continue
984
+ target = cast_expression.args.get("to")
985
+ matches_required_cast = (
986
+ _is_uuid_array_data_type(target) if ordinal in array_requested else _is_uuid_data_type(target)
987
+ )
988
+ if not matches_required_cast:
989
+ authoritative.add(ordinal)
990
+ effective_scalars = tuple(ordinal for ordinal in scalar_ordinals if ordinal not in authoritative)
991
+ effective_arrays = tuple(ordinal for ordinal in array_ordinals if ordinal not in authoritative)
992
+ effective_array_set = set(effective_arrays)
993
+ effective_set = set(effective_scalars) | effective_array_set
971
994
 
972
995
  def wrap_parameter(node: exp.Expression) -> exp.Expression:
973
996
  if not isinstance(node, exp.Parameter):
@@ -977,52 +1000,112 @@ def _rewrite_postgres_uuid_placeholders(
977
1000
  return node
978
1001
  if _direct_parameter_cast(node) is not None:
979
1002
  return node
980
- return exp.Cast(this=node.copy(), to=exp.DataType.build("UUID"))
1003
+ target_type = "UUID[]" if ordinal in effective_array_set else "UUID"
1004
+ return exp.Cast(this=node.copy(), to=exp.DataType.build(target_type, dialect=dialect))
981
1005
 
982
1006
  for expression in expressions:
983
1007
  expression.transform(wrap_parameter, copy=False)
984
- return "; ".join(expression.sql(dialect=dialect) for expression in expressions), effective
1008
+ rewritten = "; ".join(expression.sql(dialect=dialect) for expression in expressions)
1009
+ return rewritten, effective_scalars, effective_arrays
1010
+
1011
+
1012
+ def _sequence_holds_uuid(values: "list[Any] | tuple[Any, ...]") -> bool:
1013
+ """Report whether a sequence's first non-null element is a UUID."""
1014
+ for value in values:
1015
+ if value is not None:
1016
+ return isinstance(value, _UUID_TYPES)
1017
+ return False
985
1018
 
986
1019
 
987
- def _detect_batch_uuid_ordinals(parameters: Any) -> "tuple[int, ...]":
1020
+ def _detect_uuid_ordinals(parameters: "list[Any] | tuple[Any, ...]") -> "tuple[tuple[int, ...], tuple[int, ...]]":
1021
+ """Split a parameter sequence into scalar-UUID and UUID-array ordinals."""
1022
+ scalar_ordinals: list[int] = []
1023
+ array_ordinals: list[int] = []
1024
+ for ordinal, value in enumerate(parameters, 1):
1025
+ if isinstance(value, _UUID_TYPES):
1026
+ scalar_ordinals.append(ordinal)
1027
+ elif isinstance(value, (list, tuple)) and _sequence_holds_uuid(value):
1028
+ array_ordinals.append(ordinal)
1029
+ return tuple(scalar_ordinals), tuple(array_ordinals)
1030
+
1031
+
1032
+ def _detect_batch_uuid_ordinals(parameters: Any) -> "tuple[tuple[int, ...], tuple[int, ...]]":
988
1033
  if not isinstance(parameters, (list, tuple)) or not parameters:
989
- return ()
1034
+ return (), ()
990
1035
  first_row = parameters[0]
991
1036
  if not isinstance(first_row, (list, tuple)):
992
- return ()
1037
+ return (), ()
993
1038
  expected_size = len(first_row)
994
1039
  for row in parameters:
995
1040
  if not isinstance(row, (list, tuple)) or len(row) != expected_size:
996
- return ()
997
- return tuple(
998
- ordinal
999
- for ordinal in range(1, expected_size + 1)
1000
- if any(isinstance(row[ordinal - 1], _UUID_TYPES) for row in parameters)
1041
+ return (), ()
1042
+ scalar_ordinals: list[int] = []
1043
+ array_ordinals: list[int] = []
1044
+ for ordinal in range(1, expected_size + 1):
1045
+ values = [row[ordinal - 1] for row in parameters]
1046
+ if any(isinstance(value, _UUID_TYPES) for value in values):
1047
+ scalar_ordinals.append(ordinal)
1048
+ elif any(isinstance(value, (list, tuple)) and _sequence_holds_uuid(value) for value in values):
1049
+ array_ordinals.append(ordinal)
1050
+ return tuple(scalar_ordinals), tuple(array_ordinals)
1051
+
1052
+
1053
+ def _uuid_binding_error(ordinal: int, value: Any, row_number: int, element_index: "int | None" = None) -> str:
1054
+ """Build the incompatible-value message for a UUID parameter ordinal."""
1055
+ location = "" if element_index is None else f" at element {element_index}"
1056
+ return (
1057
+ f"ADBC PostgreSQL UUID parameter ordinal {ordinal} has incompatible "
1058
+ f"{type(value).__name__} value{location} in batch row {row_number}; expected a UUID object, "
1059
+ "parseable UUID string, or None."
1060
+ )
1061
+
1062
+
1063
+ def _convert_uuid_value(value: Any, ordinal: int, row_number: int, element_index: "int | None" = None) -> Any:
1064
+ """Normalize one UUID-bound value to its canonical string form."""
1065
+ if value is None:
1066
+ return None
1067
+ if isinstance(value, _UUID_TYPES):
1068
+ return str(value)
1069
+ if not isinstance(value, str):
1070
+ raise SQLSpecError(_uuid_binding_error(ordinal, value, row_number, element_index))
1071
+ try:
1072
+ return str(uuid_from_string(value))
1073
+ except (TypeError, ValueError) as exc:
1074
+ raise SQLSpecError(_uuid_binding_error(ordinal, value, row_number, element_index)) from exc
1075
+
1076
+
1077
+ def _null_array_element_error(ordinal: int, row_number: int, element_index: int) -> str:
1078
+ """Build the null-array-element message for a UUID array parameter ordinal."""
1079
+ return (
1080
+ f"ADBC PostgreSQL UUID parameter ordinal {ordinal} has a null value at element {element_index} "
1081
+ f"in batch row {row_number}; the PostgreSQL ADBC driver encodes null array elements as empty "
1082
+ "strings, which PostgreSQL rejects for UUID[]. Remove the null element or bind the array as text."
1001
1083
  )
1002
1084
 
1003
1085
 
1004
- def _convert_uuid_row(row: Any, ordinals: "tuple[int, ...]", row_number: int) -> Any:
1086
+ def _convert_uuid_sequence(values: Any, ordinal: int, row_number: int) -> "list[Any]":
1087
+ """Normalize every element of a UUID array parameter."""
1088
+ converted: list[Any] = []
1089
+ for element_index, value in enumerate(values, 1):
1090
+ if value is None:
1091
+ raise SQLSpecError(_null_array_element_error(ordinal, row_number, element_index))
1092
+ converted.append(_convert_uuid_value(value, ordinal, row_number, element_index))
1093
+ return converted
1094
+
1095
+
1096
+ def _convert_uuid_row(
1097
+ row: Any, scalar_ordinals: "tuple[int, ...]", array_ordinals: "tuple[int, ...]", row_number: int
1098
+ ) -> Any:
1005
1099
  converted = list(row)
1006
- for ordinal in ordinals:
1100
+ for ordinal in scalar_ordinals:
1101
+ converted[ordinal - 1] = _convert_uuid_value(converted[ordinal - 1], ordinal, row_number)
1102
+ for ordinal in array_ordinals:
1007
1103
  value = converted[ordinal - 1]
1008
1104
  if value is None:
1009
1105
  continue
1010
- if not isinstance(value, _UUID_TYPES) and not isinstance(value, str):
1011
- msg = (
1012
- f"ADBC PostgreSQL UUID parameter ordinal {ordinal} has incompatible "
1013
- f"{type(value).__name__} value in batch row {row_number}; expected a UUID object, "
1014
- "parseable UUID string, or None."
1015
- )
1016
- raise SQLSpecError(msg)
1017
- try:
1018
- converted[ordinal - 1] = str(UUID(str(value)))
1019
- except (AttributeError, TypeError, ValueError) as exc:
1020
- msg = (
1021
- f"ADBC PostgreSQL UUID parameter ordinal {ordinal} has incompatible "
1022
- f"{type(value).__name__} value in batch row {row_number}; expected a UUID object, "
1023
- "parseable UUID string, or None."
1024
- )
1025
- raise SQLSpecError(msg) from exc
1106
+ if not isinstance(value, (list, tuple)):
1107
+ raise SQLSpecError(_uuid_binding_error(ordinal, value, row_number))
1108
+ converted[ordinal - 1] = _convert_uuid_sequence(value, ordinal, row_number)
1026
1109
  return tuple(converted) if isinstance(row, tuple) else converted
1027
1110
 
1028
1111
 
@@ -40,7 +40,7 @@ from sqlspec.core import (
40
40
  )
41
41
  from sqlspec.driver import BaseSyncExceptionHandler, SyncDriverAdapterBase, SyncRowStream
42
42
  from sqlspec.exceptions import DatabaseConnectionError, SQLSpecError
43
- from sqlspec.utils.arrow_helpers import arrow_reader_with_deferred_close
43
+ from sqlspec.utils.arrow_helpers import arrow_reader_with_deferred_close, arrow_table_to_pylist
44
44
  from sqlspec.utils.logging import get_logger
45
45
  from sqlspec.utils.module_loader import ensure_pyarrow
46
46
  from sqlspec.utils.serializers import to_json
@@ -129,7 +129,12 @@ class AdbcSelectStreamSource:
129
129
  batch = next(reader)
130
130
  except StopIteration:
131
131
  return []
132
- rows = cast("list[dict[str, Any]]", batch.to_pylist())
132
+ rows = arrow_table_to_pylist(
133
+ batch,
134
+ decode_arrow_extension_types=bool(
135
+ self._driver.driver_features.get("enable_arrow_extension_types", True)
136
+ ),
137
+ )
133
138
  if rows:
134
139
  return rows
135
140
 
@@ -224,7 +229,10 @@ class AdbcDriver(SyncDriverAdapterBase):
224
229
 
225
230
  if is_select_like:
226
231
  arrow_table = cursor.fetch_arrow_table()
227
- data = arrow_table.to_pylist()
232
+ data = arrow_table_to_pylist(
233
+ arrow_table,
234
+ decode_arrow_extension_types=bool(self.driver_features.get("enable_arrow_extension_types", True)),
235
+ )
228
236
  column_names = list(arrow_table.column_names)
229
237
  return self.create_execution_result(
230
238
  cursor,
@@ -772,14 +772,33 @@ def _bigquery_arrow_reader_from_iterable(batches: "Iterable[ArrowRecordBatch]")
772
772
  def _records_to_json_rows(
773
773
  records: "Sequence[Mapping[str, Any]] | Sequence[Sequence[Any]]", columns: "list[str] | None"
774
774
  ) -> "list[dict[str, Any]]":
775
- materialized = list(records)
775
+ materialized = records if isinstance(records, list) else list(records)
776
776
  if not materialized:
777
777
  msg = "load_from_records requires at least one record."
778
778
  raise ImproperConfigurationError(msg)
779
779
 
780
780
  first = materialized[0]
781
781
  if isinstance(first, Mapping):
782
- resolved = columns if columns is not None else list(first.keys())
782
+ if columns is None:
783
+ resolved = list(first.keys())
784
+ expected = set(resolved)
785
+ can_reuse = True
786
+ for record in materialized:
787
+ if not isinstance(record, Mapping):
788
+ msg = "load_from_records mapping records must all be mappings."
789
+ raise ImproperConfigurationError(msg)
790
+ record_keys = list(record.keys())
791
+ if set(record_keys) != expected:
792
+ msg = "load_from_records mapping records must all share the same keys."
793
+ raise ImproperConfigurationError(msg)
794
+ if type(record) is not dict or record_keys != resolved:
795
+ can_reuse = False
796
+ validated_records = cast("list[Mapping[str, Any]]", materialized)
797
+ if can_reuse:
798
+ return cast("list[dict[str, Any]]", validated_records)
799
+ return [{column: record[column] for column in resolved} for record in validated_records]
800
+
801
+ resolved = columns
783
802
  expected = set(resolved)
784
803
  rows: list[dict[str, Any]] = []
785
804
  for record in materialized:
@@ -4,7 +4,6 @@ import contextlib
4
4
  from datetime import date, datetime
5
5
  from decimal import Decimal
6
6
  from typing import TYPE_CHECKING, Any, Final, cast
7
- from uuid import UUID
8
7
 
9
8
  from sqlspec.core import DriverParameterProfile, ParameterStyle, StatementConfig, build_statement_config_from_profile
10
9
  from sqlspec.exceptions import (
@@ -24,6 +23,7 @@ from sqlspec.exceptions import (
24
23
  from sqlspec.utils.serializers import to_json
25
24
  from sqlspec.utils.type_converters import build_decimal_converter, build_uuid_coercions, time_iso_convert
26
25
  from sqlspec.utils.type_guards import has_rowcount
26
+ from sqlspec.utils.uuids import uuid_from_string
27
27
 
28
28
  if TYPE_CHECKING:
29
29
  from collections.abc import Callable, Mapping
@@ -395,7 +395,7 @@ def _restore_uuid_columns(rows: "list[dict[str, Any]]", description: "list[Any]
395
395
  for column in uuid_columns:
396
396
  value = row.get(column)
397
397
  if isinstance(value, str):
398
- row[column] = UUID(value)
398
+ row[column] = uuid_from_string(value)
399
399
 
400
400
 
401
401
  driver_profile = build_profile()
@@ -6,8 +6,9 @@ and Oracle's JSON storage types via connection type handlers.
6
6
  Routing matrix (input):
7
7
 
8
8
  * Oracle 21c+ native ``JSON``: bind via ``DB_TYPE_JSON`` (binary OSON).
9
- * Oracle 12c-20c with ``BLOB CHECK (... IS JSON)``: bind via ``DB_TYPE_BLOB`` with
10
- textual UTF-8 JSON bytes.
9
+ * Oracle 12c-20c with ``BLOB CHECK (... IS JSON)``: the driver pre-serializes
10
+ JSON-shaped values and creates a ``DB_TYPE_BLOB`` locator containing UTF-8
11
+ JSON before input-handler dispatch.
11
12
  * Oracle 11g and earlier: bind via ``DB_TYPE_CLOB`` with serialized JSON string.
12
13
  * The rung is chosen by ``resolve_oracle_json_storage``, the single JSON-threshold
13
14
  source shared with ``OracleVersionInfo`` and the extension stores.
@@ -33,11 +34,12 @@ handler returns ``None`` for values it does not own.
33
34
  """
34
35
 
35
36
  from functools import partial
36
- from typing import TYPE_CHECKING, Any, cast
37
+ from typing import TYPE_CHECKING, Any
37
38
 
38
39
  from oracledb import DB_TYPE_CHAR, DB_TYPE_NCHAR, DB_TYPE_NVARCHAR, DB_TYPE_VARCHAR
39
40
 
40
41
  from sqlspec.adapters.oracledb._typing import DB_TYPE_BLOB, DB_TYPE_CLOB, DB_TYPE_JSON, DB_TYPE_LONG, DB_TYPE_LONG_RAW
42
+ from sqlspec.adapters.oracledb.data_dictionary import resolve_oracle_connection_major
41
43
  from sqlspec.data_dictionary.dialects.oracle import (
42
44
  ORACLE_JSON_STORAGE_BLOB_JSON,
43
45
  ORACLE_JSON_STORAGE_NATIVE,
@@ -51,6 +53,7 @@ if TYPE_CHECKING:
51
53
  __all__ = (
52
54
  "chain_input_handler",
53
55
  "chain_output_handler",
56
+ "is_json_payload",
54
57
  "json_converter_in_blob",
55
58
  "json_converter_in_clob",
56
59
  "json_converter_out_blob",
@@ -131,7 +134,7 @@ def chain_input_handler(inner: Any, fallback: "Any | None") -> Any:
131
134
  instance: python-oracledb selects its calling convention via
132
135
  ``inspect.signature(handler)``, which succeeds for a partial of a (compiled)
133
136
  function but raises ``ValueError`` for a compiled ``__call__`` object -- the
134
- latter forces the legacy 6-argument call and breaks fetches.
137
+ latter forces the older 6-argument call and breaks fetches.
135
138
  """
136
139
  return partial(_chained_input_handler, inner, fallback)
137
140
 
@@ -144,7 +147,7 @@ def chain_output_handler(inner: Any, fallback: "Any | None") -> Any:
144
147
  return partial(_chained_output_handler, inner, fallback)
145
148
 
146
149
 
147
- def _is_json_payload(value: Any) -> bool:
150
+ def is_json_payload(value: Any) -> bool:
148
151
  """Return True if the value should be claimed by the JSON input handler.
149
152
 
150
153
  ``dict`` and ``tuple``/``list`` of dicts are claimed. Sequences whose first
@@ -167,10 +170,10 @@ def _is_json_payload(value: Any) -> bool:
167
170
 
168
171
  def _input_type_handler(cursor: "Cursor | AsyncCursor", value: Any, arraysize: int) -> Any:
169
172
  """Oracle input type handler for JSON-shaped Python values."""
170
- if not _is_json_payload(value):
173
+ if not is_json_payload(value):
171
174
  return None
172
175
 
173
- server_major = cast("int | None", getattr(cursor.connection, "_sqlspec_oracle_major", None))
176
+ server_major = resolve_oracle_connection_major(cursor.connection)
174
177
 
175
178
  if server_major is None:
176
179
  return cursor.var(DB_TYPE_JSON, arraysize=arraysize)
@@ -8,7 +8,8 @@ Wrap a parameter value to express explicit intent:
8
8
 
9
9
  * :class:`OracleClob` — bind as ``DB_TYPE_CLOB`` regardless of length.
10
10
  * :class:`OracleBlob` — bind as ``DB_TYPE_BLOB`` regardless of length.
11
- * :class:`OracleJson` — bind as native JSON; defers to the C1 input handler.
11
+ * :class:`OracleJson` — express JSON intent; the input handler selects the
12
+ storage type supported by the connected Oracle version.
12
13
 
13
14
  The wrappers themselves perform no validation — type discipline lives at the
14
15
  routing site so error messages can include database-context detail.
@@ -38,7 +39,7 @@ class OracleBlob:
38
39
 
39
40
 
40
41
  class OracleJson:
41
- """Mark a value to be bound as native ``DB_TYPE_JSON`` regardless of detected version."""
42
+ """Mark a value for version-aware Oracle JSON binding."""
42
43
 
43
44
  __slots__ = ("value",)
44
45
 
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any
10
10
  from sqlspec.adapters.oracledb._json_handlers import chain_input_handler, chain_output_handler
11
11
  from sqlspec.adapters.oracledb._typing import DB_TYPE_RAW
12
12
  from sqlspec.utils.logging import get_logger
13
+ from sqlspec.utils.uuids import uuid_from_bytes
13
14
 
14
15
  if TYPE_CHECKING:
15
16
  from oracledb import AsyncConnection, AsyncCursor, Connection, Cursor
@@ -59,7 +60,7 @@ def uuid_converter_out(value: bytes | None) -> "uuid.UUID | bytes | None":
59
60
  return value
60
61
 
61
62
  try:
62
- return uuid.UUID(bytes=value)
63
+ return uuid_from_bytes(value)
63
64
  except (ValueError, TypeError):
64
65
  logger.debug("RAW(16) value is not valid UUID format, returning as bytes", extra={"value_length": len(value)})
65
66
  return value
@@ -296,7 +296,7 @@ class OracleAsyncADKStore(BaseAsyncADKStore["OracleAsyncConfig"]):
296
296
  config: OracleAsyncConfig with extension_config["adk"] settings.
297
297
 
298
298
  Notes:
299
- - JSON storage type detected based on Oracle version (21c+, 12c+, legacy)
299
+ - JSON storage type detected by capability (native JSON, BLOB IS JSON, or plain LOB)
300
300
  - event_data stored as JSON (21c+) or BLOB (older versions)
301
301
  - TIMESTAMP WITH TIME ZONE for timezone-aware timestamps
302
302
  - Named parameters using :param_name
@@ -1265,7 +1265,7 @@ class OracleSyncADKStore(BaseSyncADKStore["OracleSyncConfig"]):
1265
1265
  config: OracleSyncConfig with extension_config["adk"] settings.
1266
1266
 
1267
1267
  Notes:
1268
- - JSON storage type detected based on Oracle version (21c+, 12c+, legacy)
1268
+ - JSON storage type detected by capability (native JSON, BLOB IS JSON, or plain LOB)
1269
1269
  - event_data stored as JSON (21c+) or BLOB (older versions)
1270
1270
  - TIMESTAMP WITH TIME ZONE for timezone-aware timestamps
1271
1271
  - Named parameters using :param_name
@@ -23,7 +23,7 @@ from sqlspec.adapters.oracledb._typing import (
23
23
  from sqlspec.adapters.oracledb._uuid_handlers import register_uuid_handlers
24
24
  from sqlspec.adapters.oracledb._vector_handlers import register_numpy_handlers # pyright: ignore[reportPrivateUsage]
25
25
  from sqlspec.adapters.oracledb.core import apply_driver_features, default_statement_config
26
- from sqlspec.adapters.oracledb.data_dictionary import OracleVersionCache
26
+ from sqlspec.adapters.oracledb.data_dictionary import OracleVersionCache, resolve_oracle_connection_major
27
27
  from sqlspec.adapters.oracledb.driver import (
28
28
  OracleAsyncDriver,
29
29
  OracleAsyncExceptionHandler,
@@ -32,7 +32,6 @@ from sqlspec.adapters.oracledb.driver import (
32
32
  )
33
33
  from sqlspec.adapters.oracledb.migrations import OracleAsyncMigrationTracker, OracleSyncMigrationTracker
34
34
  from sqlspec.config import AsyncDatabaseConfig, ExtensionConfigs, SyncDatabaseConfig
35
- from sqlspec.data_dictionary.dialects.oracle import parse_oracle_version_components
36
35
  from sqlspec.driver._async import AsyncPoolConnectionContext, AsyncPoolSessionFactory
37
36
  from sqlspec.driver._sync import SyncPoolConnectionContext, SyncPoolSessionFactory
38
37
  from sqlspec.extensions.events import EventRuntimeHints
@@ -413,9 +412,9 @@ class OracleSyncConfig(SyncDatabaseConfig[OracleSyncConnection, "OracleSyncConne
413
412
  vector case) internally. UUID registration remains gated for
414
413
  backwards compatibility with existing user configurations.
415
414
 
416
- Caches ``connection._sqlspec_oracle_major`` so the JSON input handler
417
- can pick the right binding path (``DB_TYPE_JSON`` on 21c+, OSON-encoded
418
- ``DB_TYPE_BLOB`` on 19c-20c, JSON-string ``DB_TYPE_CLOB`` on 12c-18c)
415
+ Caches ``connection._sqlspec_oracle_major`` so JSON parameter coercion
416
+ can pick the right binding path (``DB_TYPE_JSON`` on 21c+, a textual
417
+ ``BLOB IS JSON`` locator on 12c-20c, and ``DB_TYPE_CLOB`` before 12c)
419
418
  without re-querying server metadata on every bind. Caches
420
419
  ``connection._sqlspec_vector_return_format`` so the vector output
421
420
  handler can dispatch to ``numpy`` / ``list`` / ``array`` without
@@ -434,7 +433,9 @@ class OracleSyncConfig(SyncDatabaseConfig[OracleSyncConnection, "OracleSyncConne
434
433
 
435
434
  # Stash detected major version on the connection so the JSON input handler
436
435
  # can pick the right binding path without per-bind metadata queries.
437
- setattr(connection, "_sqlspec_oracle_major", _resolve_connection_major(self._oracle_version_cache, connection))
436
+ setattr(
437
+ connection, "_sqlspec_oracle_major", resolve_oracle_connection_major(connection, self._oracle_version_cache)
438
+ )
438
439
  # Stash the vector-read format so the VECTOR output handler can
439
440
  # dispatch without re-reading driver-feature defaults on every fetch.
440
441
  setattr(connection, "_sqlspec_vector_return_format", self.driver_features.get("vector_return_format"))
@@ -628,7 +629,9 @@ class OracleAsyncConfig(AsyncDatabaseConfig[OracleAsyncConnection, "OracleAsyncC
628
629
 
629
630
  # Stash detected major version on the connection so the JSON input handler
630
631
  # can pick the right binding path without per-bind metadata queries.
631
- setattr(connection, "_sqlspec_oracle_major", _resolve_connection_major(self._oracle_version_cache, connection))
632
+ setattr(
633
+ connection, "_sqlspec_oracle_major", resolve_oracle_connection_major(connection, self._oracle_version_cache)
634
+ )
632
635
  # Stash the vector-read format so the VECTOR output handler can
633
636
  # dispatch without re-reading driver-feature defaults on every fetch.
634
637
  setattr(connection, "_sqlspec_vector_return_format", self.driver_features.get("vector_return_format"))
@@ -655,24 +658,3 @@ class OracleAsyncConfig(AsyncDatabaseConfig[OracleAsyncConnection, "OracleAsyncC
655
658
  await self.connection_instance.close()
656
659
  self.connection_instance = None
657
660
  self._oracle_version_cache.reset()
658
-
659
-
660
- def _resolve_connection_major(cache: "OracleVersionCache", connection: Any) -> "int | None":
661
- """Resolve the Oracle server major for connection-setup type handlers.
662
-
663
- Prefers the pool-scoped version cache once a driver has resolved it through
664
- the data-dictionary path. Before that, the major is parsed from
665
- ``connection.version`` — a connection attribute populated at connect time, so
666
- no query is issued — using the same version parser the data dictionary uses.
667
- Returns ``None`` when unavailable; callers treat ``None`` as "assume 21c+".
668
- """
669
- if cache.resolved and cache.version is not None:
670
- return cache.version.major
671
- try:
672
- version_str = connection.version
673
- except AttributeError:
674
- return None
675
- if not version_str:
676
- return None
677
- components = parse_oracle_version_components(str(version_str))
678
- return components[0] if components is not None else None