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.
- sqlspec/adapters/adbc/core.py +148 -65
- sqlspec/adapters/adbc/driver.py +11 -3
- sqlspec/adapters/bigquery/driver.py +21 -2
- sqlspec/adapters/duckdb/core.py +2 -2
- sqlspec/adapters/oracledb/_json_handlers.py +10 -7
- sqlspec/adapters/oracledb/_param_types.py +3 -2
- sqlspec/adapters/oracledb/_uuid_handlers.py +2 -1
- sqlspec/adapters/oracledb/adk/store.py +2 -2
- sqlspec/adapters/oracledb/config.py +10 -28
- sqlspec/adapters/oracledb/core.py +390 -57
- sqlspec/adapters/oracledb/data_dictionary.py +21 -0
- sqlspec/adapters/oracledb/driver.py +191 -5
- sqlspec/adapters/psqlpy/core.py +2 -1
- sqlspec/adapters/spanner/type_converter.py +15 -4
- sqlspec/builder/_explain.py +73 -43
- sqlspec/core/compiler.py +69 -1
- sqlspec/core/explain.py +2 -0
- sqlspec/core/statement.py +8 -13
- sqlspec/core/type_converter.py +3 -1
- sqlspec/data_dictionary/_types.py +2 -2
- sqlspec/driver/_async.py +2 -2
- sqlspec/driver/_common.py +1 -1
- sqlspec/driver/_sync.py +2 -2
- sqlspec/migrations/base.py +10 -10
- sqlspec/migrations/commands.py +1 -1
- sqlspec/utils/arrow_helpers.py +50 -3
- sqlspec/utils/schema.py +3 -2
- sqlspec/utils/type_converters.py +2 -1
- sqlspec/utils/uuids.py +50 -3
- {sqlspec-0.56.0.dist-info → sqlspec-0.56.2.dist-info}/METADATA +3 -5
- {sqlspec-0.56.0.dist-info → sqlspec-0.56.2.dist-info}/RECORD +34 -34
- {sqlspec-0.56.0.dist-info → sqlspec-0.56.2.dist-info}/WHEEL +0 -0
- {sqlspec-0.56.0.dist-info → sqlspec-0.56.2.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.56.0.dist-info → sqlspec-0.56.2.dist-info}/licenses/LICENSE +0 -0
sqlspec/adapters/adbc/core.py
CHANGED
|
@@ -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
|
-
|
|
879
|
+
scalar_ordinals, array_ordinals = _detect_batch_uuid_ordinals(prepared_parameters)
|
|
877
880
|
elif isinstance(prepared_parameters, (list, tuple)):
|
|
878
|
-
|
|
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
|
-
|
|
883
|
-
if not
|
|
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,
|
|
887
|
-
compiled_sql, tuple(sorted(
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
926
|
-
) -> "tuple[str, tuple[int, ...]]":
|
|
927
|
-
"""Wrap requested UUID parameter placeholders in explicit UUID casts.
|
|
928
|
-
|
|
929
|
-
Placeholders
|
|
930
|
-
and
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
|
|
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
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
|
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
|
|
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,
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
|
sqlspec/adapters/adbc/driver.py
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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:
|
sqlspec/adapters/duckdb/core.py
CHANGED
|
@@ -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] =
|
|
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)``:
|
|
10
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
173
|
+
if not is_json_payload(value):
|
|
171
174
|
return None
|
|
172
175
|
|
|
173
|
-
server_major =
|
|
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` —
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
417
|
-
can pick the right binding path (``DB_TYPE_JSON`` on 21c+,
|
|
418
|
-
``
|
|
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(
|
|
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(
|
|
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
|