@@ -76,7 +76,7 @@ Each adapter in `sqlspec/adapters/` follows this structure:
7676- ` _types.py ` - Adapter-specific type definitions
7777- Optional: ` _*_handlers.py ` - Type handlers for optional features (numpy, pgvector)
7878
79- Supported adapters: adbc, aiosqlite, asyncmy, asyncpg, bigquery, duckdb, oracledb, psqlpy, psycopg, sqlite
79+ Supported adapters: adbc, aiosqlite, asyncmy, asyncpg, bigquery, duckdb, oracledb, psqlpy, psycopg, spanner, sqlite
8080
8181### Key Design Patterns
8282
@@ -295,6 +295,77 @@ _register_with_sqlglot()
295295- MySQL: ` DISTANCE(embedding, STRING_TO_VECTOR('[0.1,0.2]'), 'EUCLIDEAN') ` (function)
296296- Oracle: ` VECTOR_DISTANCE(embedding, TO_VECTOR('[0.1,0.2]'), EUCLIDEAN) ` (function)
297297
298+ ### Custom SQLglot Dialect Pattern
299+
300+ For databases with unique SQL syntax not supported by existing sqlglot dialects:
301+
302+ ``` python
303+ # In sqlspec/adapters/{adapter}/dialect/_dialect.py
304+ from sqlglot import exp
305+ from sqlglot.dialects.bigquery import BigQuery
306+ from sqlglot.tokens import TokenType
307+
308+ class CustomDialect (BigQuery ):
309+ """ Inherit from closest matching dialect."""
310+
311+ class Tokenizer (BigQuery .Tokenizer ):
312+ """ Add custom keywords."""
313+ KEYWORDS = {
314+ ** BigQuery.Tokenizer.KEYWORDS ,
315+ " INTERLEAVE" : TokenType.INTERLEAVE ,
316+ }
317+
318+ class Parser (BigQuery .Parser ):
319+ """ Override parser for custom syntax."""
320+ def _parse_table_parts (self , schema = False , is_db_reference = False , wildcard = False ):
321+ table = super ()._parse_table_parts(schema = schema, is_db_reference = is_db_reference, wildcard = wildcard)
322+
323+ # Parse custom clause
324+ if self ._match_text_seq(" INTERLEAVE" , " IN" , " PARENT" ):
325+ parent = self ._parse_table(schema = True , is_db_reference = True )
326+ table.set(" interleave_parent" , parent)
327+
328+ return table
329+
330+ class Generator (BigQuery .Generator ):
331+ """ Override generator for custom SQL output."""
332+ def table_sql (self , expression , sep = " " ):
333+ sql = super ().table_sql(expression, sep = sep)
334+
335+ # Generate custom clause
336+ parent = expression.args.get(" interleave_parent" )
337+ if parent:
338+ sql = f " { sql} \n INTERLEAVE IN PARENT { self .sql(parent)} "
339+
340+ return sql
341+
342+ # Register dialect in adapter __init__.py
343+ from sqlglot.dialects.dialect import Dialect
344+ Dialect.classes[" custom" ] = CustomDialect
345+ ```
346+
347+ ** Create custom dialect when** :
348+ - Database has unique DDL/DML syntax not in existing dialects
349+ - Need to parse and validate database-specific keywords
350+ - Need to generate database-specific SQL from AST
351+ - An existing dialect provides 80%+ compatibility to inherit from
352+
353+ ** Do NOT create custom dialect if** :
354+ - Only parameter style differences (use parameter profiles)
355+ - Only type conversion differences (use type converters)
356+ - Only connection management differences (use config/driver)
357+
358+ ** Key principles** :
359+ - ** Inherit from closest dialect** : Spanner inherits BigQuery (both GoogleSQL)
360+ - ** Minimal overrides** : Only override methods that need customization
361+ - ** Store metadata in AST** : Use ` expression.set(key, value) ` for custom data
362+ - ** Handle missing tokens** : Check ` getattr(TokenType, "KEYWORD", None) ` before using
363+ - ** Test thoroughly** : Unit tests for parsing/generation, integration tests with real DB
364+
365+ ** Reference implementation** : ` sqlspec/adapters/spanner/dialect/ ` (GoogleSQL and PostgreSQL modes)
366+
367+ ** Documentation** : See ` /docs/guides/architecture/custom-sqlglot-dialects.md ` for full guide
368+
298369### Error Handling
299370
300371- Custom exceptions inherit from ` SQLSpecError ` in ` sqlspec/exceptions.py `
0 commit comments