@@ -96,6 +96,35 @@ SQLSpec is a type-safe SQL query mapper designed for minimal abstraction between
9696- ** Single-Pass Processing** : Parse once → transform once → validate once - SQL object is single source of truth
9797- ** Abstract Methods with Concrete Implementations** : Protocol defines abstract methods, base classes provide concrete sync/async implementations
9898
99+ ### Adapter Transaction Detection Pattern
100+
101+ Each adapter MUST override ` _connection_in_transaction() ` with direct attribute access instead of using the base class fallback which relies on ` getattr() ` chains.
102+
103+ ``` python
104+ # In each adapter's driver.py
105+ class MyAdapterDriver (SyncDriverBase ):
106+ def _connection_in_transaction (self ) -> bool :
107+ # AsyncPG: uses is_in_transaction() method
108+ return self .connection.is_in_transaction()
109+
110+ # SQLite/DuckDB: uses in_transaction property
111+ return self .connection.in_transaction
112+
113+ # Psycopg: uses status attribute
114+ return self .connection.status != psycopg.pq.TransactionStatus.IDLE
115+
116+ # BigQuery: No transaction support
117+ return False
118+ ```
119+
120+ ** Why this matters:**
121+
122+ - The base class uses ` getattr() ` chains which are slow and prevent mypyc optimization
123+ - Each adapter knows exactly which attribute to check
124+ - Direct attribute access is 10-50x faster in hot paths
125+
126+ ** Reference implementations:** All adapters in ` sqlspec/adapters/*/driver.py ` have this override.
127+
99128### Query Stack Implementation Guidelines
100129
101130- ** Builder Discipline**
@@ -290,6 +319,60 @@ def test_starlette_autocommit_mode() -> None:
290319- No inline comments - use docstrings
291320- Google-style docstrings with Args, Returns, Raises sections
292321
322+ ### Type Guards Pattern
323+
324+ When checking object capabilities, ALWAYS use type guards from ` sqlspec.utils.type_guards ` instead of ` hasattr() ` :
325+
326+ ``` python
327+ # NEVER do this - breaks mypyc and is slow
328+ if hasattr (obj, " expression" ) and hasattr (obj, " sql" ):
329+ sql = obj.sql
330+ expr = obj.expression
331+
332+ # ALWAYS do this - type-safe and fast
333+ from sqlspec.utils.type_guards import has_expression_and_sql
334+ if has_expression_and_sql(obj):
335+ sql = obj.sql # Type checker knows these exist
336+ expr = obj.expression
337+ ```
338+
339+ ** Available type guards:**
340+
341+ | Guard | Checks For | Use When |
342+ | -------| ------------| ----------|
343+ | ` is_readable(obj) ` | ` .read() ` method | Checking LOB/stream objects |
344+ | ` has_array_interface(obj) ` | ` .dtype ` and ` .shape ` | NumPy-like arrays |
345+ | ` has_cursor_metadata(obj) ` | ` .description ` | Cursor metadata access |
346+ | ` has_expression_and_sql(obj) ` | ` .expression ` and ` .sql ` | SQL statement objects |
347+ | ` has_expression_and_parameters(obj) ` | ` .expression ` and ` .parameters ` | Parameterized statements |
348+ | ` is_statement_filter(obj) ` | ` .append_to_statement() ` | Statement filter objects |
349+
350+ ** Creating new type guards:**
351+
352+ 1 . Add protocol to ` sqlspec/protocols.py ` :
353+
354+ ``` python
355+ class MyProtocol (Protocol ):
356+ """ Protocol for objects with specific capability."""
357+ my_attribute: str
358+ def my_method (self ) -> None : ...
359+ ```
360+
361+ 2 . Add type guard to ` sqlspec/utils/type_guards.py ` :
362+
363+ ``` python
364+ def has_my_capability (obj : Any) -> TypeGuard[MyProtocol]:
365+ """ Check if object has my_attribute and my_method."""
366+ return hasattr (obj, " my_attribute" ) and hasattr (obj, " my_method" )
367+ ```
368+
369+ ** Key principles:**
370+
371+ - Type guards centralize ` hasattr() ` calls in ONE place
372+ - Protocols provide type narrowing after the guard passes
373+ - Type checkers understand the guard's implications
374+ - Works with mypyc compilation
375+
293376### Testing
294377
295378- ** MANDATORY** : Function-based tests only (` def test_something(): ` )
@@ -887,26 +970,37 @@ Available backends:
887970PostgreSQL adapters (asyncpg, psycopg, psqlpy) default to ` listen_notify ` .
888971All other adapters default to ` table_queue ` .
889972
890- ** Stores** generate adapter-specific DDL for the queue table :
973+ ** Stores** generate adapter-specific DDL using a hook-based pattern :
891974
892975``` python
893976# In sqlspec/adapters/{adapter}/events/store.py
894977class AdapterEventQueueStore (BaseEventQueueStore[AdapterConfig]):
895978 __slots__ = ()
896979
980+ # REQUIRED: Return (payload_type, metadata_type, timestamp_type)
897981 def _column_types (self ) -> tuple[str , str , str ]:
898- # Return (payload_type, metadata_type, timestamp_type)
899982 return " JSONB" , " JSONB" , " TIMESTAMPTZ" # PostgreSQL
900983
901- def _build_create_table_sql ( self ) -> str :
902- # Override for database-specific DDL syntax
903- return super ()._build_create_table_sql()
984+ # OPTIONAL hooks for dialect variations :
985+ def _string_type ( self , length : int ) -> str :
986+ return f " VARCHAR( { length } ) " # Override for STRING(N), VARCHAR2(N), etc.
904987
905- def _wrap_create_statement (self , statement : str , object_type : str ) -> str :
906- # Wrap with IF NOT EXISTS, PL/SQL blocks, etc.
907- return statement.replace(" CREATE TABLE" , " CREATE TABLE IF NOT EXISTS" , 1 )
988+ def _integer_type (self ) -> str :
989+ return " INTEGER" # Override for INT64, NUMBER(10), etc.
990+
991+ def _timestamp_default (self ) -> str :
992+ return " CURRENT_TIMESTAMP" # Override for CURRENT_TIMESTAMP(6), SYSTIMESTAMP, etc.
993+
994+ def _primary_key_syntax (self ) -> str :
995+ return " " # Override for " PRIMARY KEY (event_id)" if PK must be inline
996+
997+ def _table_clause (self ) -> str :
998+ return " " # Override for " CLUSTER BY ..." or " INMEMORY ..."
908999```
9091000
1001+ Most adapters only override ` _column_types() ` . Complex dialects may override
1002+ ` _build_create_table_sql() ` directly (Oracle PL/SQL, BigQuery CLUSTER BY, Spanner no-DEFAULT).
1003+
9101004** Backend factory pattern** for native backends:
9111005
9121006``` python
0 commit comments