1+ from typing import Any , Sequence , Type , Callable
2+ from itertools import islice
3+ from uuid import UUID
4+ import sqlite3
5+
6+ from .base_adapter import BaseAdapter
7+ from .models import ColumnInfo
8+
9+
10+ class SQLiteAdapter (BaseAdapter ):
11+
12+ COERCERS : dict [Type , Callable [..., Any ]] = {
13+ UUID : str
14+ }
15+
16+ def __init__ (self , database : str ) -> None :
17+ self ._connection = sqlite3 .connect (database )
18+ # SQLite disables FK enforcement per-connection by default;
19+ # turn it on so seeded data behaves like Postgres/MySQL.
20+ self ._connection .execute ("PRAGMA foreign_keys = ON" )
21+
22+ def ping (self ) -> None :
23+ self ._connection .execute ("SELECT 1" )
24+
25+ def get_tables (self ) -> list [str ]:
26+ cursor = self ._connection .execute (
27+ "SELECT name FROM sqlite_master "
28+ "WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
29+ )
30+ return [row [0 ] for row in cursor .fetchall ()]
31+
32+ def get_columns (self , table : str ) -> list [ColumnInfo ]:
33+ super ().get_columns (table )
34+ columns : list [ColumnInfo ] = []
35+ quoted_table = self ._quote_identifier (table )
36+ cursor = self ._connection .execute (f"PRAGMA table_info({ quoted_table } )" )
37+ for _ , name , col_type , notnull , default , pk in cursor .fetchall ():
38+ columns .append (ColumnInfo (
39+ name = name ,
40+ type = col_type or "" ,
41+ is_nullable = not bool (notnull ),
42+ default = default ,
43+ extra = "PRIMARY KEY" if pk else None
44+ ))
45+ return columns
46+
47+ def fetch (self , table : str , columns : Sequence [str ]) -> list [tuple [Any , ...]]:
48+ super ().fetch (table , columns )
49+ columns_query = ", " .join (self ._quote_identifier (col ) for col in columns )
50+ quoted_table = self ._quote_identifier (table )
51+ cursor = self ._connection .execute (f"SELECT { columns_query } FROM { quoted_table } " )
52+ return cursor .fetchall ()
53+
54+ def insert (self , table : str , rows : Sequence [dict [str , Any ]], batch_size : int = 200 ) -> None :
55+ super ().insert (table , rows )
56+ columns = list (rows [0 ].keys ())
57+ columns_query = ", " .join (self ._quote_identifier (c ) for c in columns )
58+ placeholders = ", " .join (f":{ c } " for c in columns )
59+ quoted_table = self ._quote_identifier (table )
60+ query = f"INSERT INTO { quoted_table } ({ columns_query } ) VALUES ({ placeholders } )"
61+
62+ iterator = iter (rows )
63+ try :
64+ while batch := list (islice (iterator , batch_size )):
65+ coerced_batch = list (map (self ._coerce_row , batch ))
66+ self ._connection .executemany (query , coerced_batch )
67+ self ._connection .commit ()
68+ except Exception as err :
69+ self ._connection .rollback ()
70+ raise err
71+
72+ def close (self ) -> None :
73+ self ._connection .close ()
74+
75+ def _coerce_row (self , row : dict [str , Any ]) -> dict [str , Any ]:
76+ return {key : self ._coerce (value ) for key , value in row .items ()}
77+
78+ def _coerce (self , value : Any ) -> Any :
79+ coercer = self .COERCERS .get (type (value ))
80+ return coercer (value ) if coercer else value
81+
82+ @staticmethod
83+ def _quote_identifier (identifier : str ) -> str :
84+ # Standard SQL identifier escaping: wrap in double quotes,
85+ # double any embedded double quotes.
86+ return '"' + identifier .replace ('"' , '""' ) + '"'
0 commit comments