@@ -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
829963In 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