Skip to content

Commit d420971

Browse files
authored
feat(oracle): Add support for UUID binary conversion and handlers for Oracle RAW(16) (#232)
Introduce automatic conversion between Python UUID objects and Oracle RAW(16) binary format. Implement input and output type handlers for seamless integration, along with comprehensive unit tests to ensure functionality. Enable configuration options for UUID handling in the Oracle driver.
1 parent 446d60a commit d420971

File tree

6 files changed

+1309
-7
lines changed

6 files changed

+1309
-7
lines changed

AGENTS.md

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,140 @@ def register_handlers(connection: "Connection") -> None:
824824
logger.debug("Registered type handlers for [feature]")
825825
```
826826

827+
### Handler Chaining Pattern (Multiple Type Handlers)
828+
829+
When multiple type handlers need to coexist (e.g., NumPy vectors + UUID binary), use handler chaining to avoid conflicts. Oracle's python-oracledb allows only ONE inputtypehandler and ONE outputtypehandler per connection.
830+
831+
**Problem**: Directly assigning a new handler overwrites any existing handler.
832+
833+
**Solution**: Check for existing handlers and chain them together:
834+
835+
```python
836+
def register_handlers(connection: "Connection") -> None:
837+
"""Register type handlers with chaining support.
838+
839+
Chains to existing type handlers to avoid conflicts with other features.
840+
841+
Args:
842+
connection: Database connection.
843+
"""
844+
existing_input = getattr(connection, "inputtypehandler", None)
845+
existing_output = getattr(connection, "outputtypehandler", None)
846+
847+
def combined_input_handler(cursor: "Cursor", value: Any, arraysize: int) -> Any:
848+
# Try new handler first
849+
result = _input_type_handler(cursor, value, arraysize)
850+
if result is not None:
851+
return result
852+
# Chain to existing handler
853+
if existing_input is not None:
854+
return existing_input(cursor, value, arraysize)
855+
return None
856+
857+
def combined_output_handler(cursor: "Cursor", metadata: Any) -> Any:
858+
# Try new handler first
859+
result = _output_type_handler(cursor, metadata)
860+
if result is not None:
861+
return result
862+
# Chain to existing handler
863+
if existing_output is not None:
864+
return existing_output(cursor, metadata)
865+
return None
866+
867+
connection.inputtypehandler = combined_input_handler
868+
connection.outputtypehandler = combined_output_handler
869+
logger.debug("Registered type handlers with chaining support")
870+
```
871+
872+
**Registration Order Matters**:
873+
874+
```python
875+
async def _init_connection(self, connection):
876+
"""Initialize connection with multiple type handlers."""
877+
# Register handlers in order of priority
878+
if self.driver_features.get("enable_numpy_vectors", False):
879+
from ._numpy_handlers import register_handlers
880+
register_handlers(connection) # First handler
881+
882+
if self.driver_features.get("enable_uuid_binary", False):
883+
from ._uuid_handlers import register_handlers
884+
register_handlers(connection) # Chains to NumPy handler
885+
```
886+
887+
**Key Principles**:
888+
889+
1. **Use getattr() to check for existing handlers** - This is acceptable duck-typing (not defensive programming)
890+
2. **Chain handlers in combined functions** - New handler checks first, then delegates to existing
891+
3. **Return None if no match** - Signals to continue to next handler or default behavior
892+
4. **Order matters** - Last registered handler gets first chance to process
893+
5. **Log chaining** - Include "with chaining support" in debug message
894+
895+
**Example Usage**:
896+
897+
```python
898+
# Both features work together via chaining
899+
config = OracleAsyncConfig(
900+
pool_config={"dsn": "oracle://..."},
901+
driver_features={
902+
"enable_numpy_vectors": True, # NumPy vectors
903+
"enable_uuid_binary": True # UUID binary (chains to NumPy)
904+
}
905+
)
906+
907+
# Insert both types in same transaction
908+
await session.execute(
909+
"INSERT INTO ml_data (id, model_id, embedding) VALUES (:1, :2, :3)",
910+
(1, uuid.uuid4(), np.random.rand(768).astype(np.float32))
911+
)
912+
```
913+
914+
### Oracle Metadata Tuple Unpacking Pattern
915+
916+
Oracle's cursor.description returns a 7-element tuple for each column. Always unpack explicitly to access internal_size:
917+
918+
```python
919+
def _output_type_handler(cursor: "Cursor", metadata: Any) -> Any:
920+
"""Oracle output type handler.
921+
922+
Args:
923+
cursor: Oracle cursor.
924+
metadata: Column metadata tuple (name, type_code, display_size,
925+
internal_size, precision, scale, null_ok).
926+
"""
927+
import oracledb
928+
929+
# Unpack tuple explicitly - metadata[3] is internal_size
930+
_name, type_code, _display_size, internal_size, _precision, _scale, _null_ok = metadata
931+
932+
if type_code is oracledb.DB_TYPE_RAW and internal_size == 16:
933+
return cursor.var(type_code, arraysize=cursor.arraysize, outconverter=converter_out)
934+
return None
935+
```
936+
937+
**Why explicit unpacking**:
938+
939+
- **Correctness**: Oracle metadata is a tuple, not an object with attributes
940+
- **No .size attribute**: Attempting `metadata.size` raises AttributeError
941+
- **Clear intent**: Unpacking documents the 7-element structure
942+
- **Prevents errors**: Catches unexpected metadata format changes
943+
944+
**Common mistake**:
945+
946+
```python
947+
# WRONG - metadata has no .size attribute
948+
if type_code is oracledb.DB_TYPE_RAW and metadata.size == 16:
949+
...
950+
```
951+
952+
**Correct approach**:
953+
954+
```python
955+
# RIGHT - unpack tuple to access internal_size
956+
_name, type_code, _display_size, internal_size, _precision, _scale, _null_ok = metadata
957+
if type_code is oracledb.DB_TYPE_RAW and internal_size == 16:
958+
...
959+
```
960+
827961
### Configuring driver_features with Auto-Detection
828962

829963
In adapter's `config.py`, implement auto-detection:
@@ -1895,7 +2029,7 @@ Current state of all adapters (as of type-cleanup branch):
18952029

18962030
| Adapter | TypedDict | Auto-Detect | enable_ Prefix | Defaults | Grade | Notes |
18972031
|------------|-----------|-------------|----------------|----------|------------|------------------------------------------|
1898-
| Oracle | ✅ | ✅ | ✅ | ✅ | Gold | Perfect implementation, reference model |
2032+
| Oracle | ✅ | ✅ | ✅ | ✅ | Gold | NumPy vectors + UUID binary w/ chaining |
18992033
| AsyncPG | ✅ | ✅ | ✅ | ✅ | Excellent | Comprehensive TypedDict docs added |
19002034
| Psycopg | ✅ | ✅ | ✅ | ✅ | Excellent | Comprehensive TypedDict docs added |
19012035
| Psqlpy | ✅ | ✅ | ✅ | ✅ | Excellent | Simple but correct |

0 commit comments

Comments
 (0)