Skip to content

Commit 9b72f71

Browse files
committed
feat(parse-columns): support to PARSE_COLUMNS
Column names are parsed to extract the explicit decltype.
1 parent 8b9c53f commit 9b72f71

File tree

4 files changed

+320
-61
lines changed

4 files changed

+320
-61
lines changed

src/sqlitecloud/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
PARSE_DECLTYPES,
88
Connection,
99
Cursor,
10+
adapters,
1011
connect,
12+
converters,
1113
register_adapter,
1214
register_converter,
1315
)
@@ -21,6 +23,8 @@
2123
"register_converter",
2224
"PARSE_DECLTYPES",
2325
"PARSE_COLNAMES",
26+
"adapters",
27+
"converters",
2428
]
2529

2630
VERSION = "0.0.79"

src/sqlitecloud/dbapi2.py

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# https://peps.python.org/pep-0249/
55
#
66
import logging
7+
import re
78
from datetime import date, datetime
89
from typing import (
910
Any,
@@ -24,6 +25,7 @@
2425
SQLiteCloudConfig,
2526
SQLiteCloudConnect,
2627
SQLiteCloudException,
28+
SQLiteDataTypes,
2729
)
2830
from sqlitecloud.driver import Driver
2931
from sqlitecloud.resultset import (
@@ -51,9 +53,9 @@
5153
PARSE_COLNAMES = 2
5254

5355
# Adapters registry to convert Python types to SQLite types
54-
_adapters = {}
56+
adapters: Dict[Type[Any], Callable[[Any], SQLiteDataTypes]] = {}
5557
# Converters registry to convert SQLite types to Python types
56-
_converters = {}
58+
converters: Dict[str, Callable[[bytes], Any]] = {}
5759

5860

5961
@overload
@@ -148,8 +150,8 @@ def register_adapter(
148150
callable (Callable): The callable that converts the type into a supported
149151
SQLite supported type.
150152
"""
151-
global _adapters
152-
_adapters[pytype] = adapter_callable
153+
registry = _get_adapters_registry()
154+
registry[pytype] = adapter_callable
153155

154156

155157
def register_converter(type_name: str, converter: Callable[[bytes], Any]) -> None:
@@ -161,8 +163,16 @@ def register_converter(type_name: str, converter: Callable[[bytes], Any]) -> Non
161163
The match with the name of the type in the query is case-insensitive.
162164
converter (Callable): The callable that converts the bytestring into the custom Python type.
163165
"""
164-
global _converters
165-
_converters[type_name.lower()] = converter
166+
registry = _get_converters_registry()
167+
registry[type_name.lower()] = converter
168+
169+
170+
def _get_adapters_registry() -> dict:
171+
return adapters
172+
173+
174+
def _get_converters_registry() -> dict:
175+
return converters
166176

167177

168178
class Connection:
@@ -307,8 +317,8 @@ def _apply_adapter(self, value: Any) -> SQLiteTypes:
307317
Returns:
308318
SQLiteTypes: The SQLite supported type or the given value when no adapter is found.
309319
"""
310-
if type(value) in _adapters:
311-
return _adapters[type(value)](value)
320+
if type(value) in adapters:
321+
return adapters[type(value)](value)
312322

313323
if hasattr(value, "__conform__"):
314324
# we don't support sqlite3.PrepareProtocol
@@ -365,7 +375,7 @@ def description(
365375
for i in range(self._resultset.ncols):
366376
description += (
367377
(
368-
self._resultset.colname[i],
378+
self._parse_colname(self._resultset.colname[i])[0],
369379
None,
370380
None,
371381
None,
@@ -541,6 +551,28 @@ def setinputsizes(self, sizes) -> None:
541551
def setoutputsize(self, size, column=None) -> None:
542552
pass
543553

554+
def _parse_colname(self, colname: str) -> Tuple[str, str]:
555+
"""
556+
Parse the column name to extract the column name and the
557+
declared type if present when it follows the syntax `colname [decltype]`.
558+
559+
Args:
560+
colname (str): The column name with optional declared type.
561+
Eg: "mycol [mytype]"
562+
563+
Returns:
564+
Tuple[str, str]: The column name and the declared type.
565+
Eg: ("mycol", "mytype")
566+
"""
567+
# search for `[mytype]` in `mycol [mytype]`
568+
pattern = r"\[(.*?)\]"
569+
570+
matches = re.findall(pattern, colname)
571+
if not matches or len(matches) == 0:
572+
return colname, None
573+
574+
return colname.replace(f"[{matches[0]}]", "").strip(), matches[0]
575+
544576
def _call_row_factory(self, row: Tuple) -> object:
545577
if self.row_factory is None:
546578
return row
@@ -572,11 +604,26 @@ def _adapt_parameters(self, parameters: Union[Dict, Tuple]) -> Union[Dict, Tuple
572604

573605
return tuple(self._connection._apply_adapter(p) for p in parameters)
574606

575-
def _convert_value(self, value: Any, decltype: Optional[str]) -> Any:
576-
# todo: parse columns first
607+
def _convert_value(
608+
self, value: Any, colname: Optional[str], decltype: Optional[str]
609+
) -> Any:
610+
if (
611+
colname
612+
and (self.connection.detect_types & PARSE_COLNAMES) == PARSE_COLNAMES
613+
):
614+
try:
615+
return self._parse_colnames(value, colname)
616+
except MissingDecltypeException:
617+
pass
577618

578-
if (self.connection.detect_types & PARSE_DECLTYPES) == PARSE_DECLTYPES:
579-
return self._parse_decltypes(value, decltype)
619+
if (
620+
decltype
621+
and (self.connection.detect_types & PARSE_DECLTYPES) == PARSE_DECLTYPES
622+
):
623+
try:
624+
return self._parse_decltypes(value, decltype)
625+
except MissingDecltypeException:
626+
pass
580627

581628
if decltype == SQLITECLOUD_VALUE_TYPE.TEXT.value or (
582629
decltype is None and isinstance(value, str)
@@ -585,16 +632,27 @@ def _convert_value(self, value: Any, decltype: Optional[str]) -> Any:
585632

586633
return value
587634

588-
def _parse_decltypes(self, value: Any, decltype: str) -> Any:
635+
def _parse_colnames(self, value: Any, colname: str) -> Optional[Any]:
636+
"""Convert the value using the explicit type in the column name."""
637+
_, decltype = self._parse_colname(colname)
638+
639+
if decltype:
640+
return self._parse_decltypes(value, decltype)
641+
642+
raise MissingDecltypeException(f"No decltype declared for: {decltype}")
643+
644+
def _parse_decltypes(self, value: Any, decltype: str) -> Optional[Any]:
645+
"""Convert the value by calling the registered converter for the given decltype."""
589646
decltype = decltype.lower()
590-
if decltype in _converters:
647+
registry = _get_converters_registry()
648+
if decltype in registry:
591649
# sqlite3 always passes value as bytes
592650
value = (
593651
str(value).encode("utf-8") if not isinstance(value, bytes) else value
594652
)
595-
return _converters[decltype](value)
653+
return registry[decltype](value)
596654

597-
return value
655+
raise MissingDecltypeException(f"No decltype registered for: {decltype}")
598656

599657
def _apply_text_factory(self, value: Any) -> Any:
600658
"""Use Connection.text_factory to convert value with TEXT column or
@@ -614,9 +672,10 @@ def _get_value(self, row: int, col: int) -> Optional[Any]:
614672
return None
615673

616674
value = self._resultset.get_value(row, col)
675+
colname = self._resultset.get_name(col)
617676
decltype = self._resultset.get_decltype(col)
618677

619-
return self._convert_value(value, decltype)
678+
return self._convert_value(value, colname, decltype)
620679

621680
def __iter__(self) -> "Cursor":
622681
return self
@@ -638,6 +697,12 @@ def __next__(self) -> Optional[Tuple[Any]]:
638697
raise StopIteration
639698

640699

700+
class MissingDecltypeException(Exception):
701+
def __init__(self, message: str) -> None:
702+
super().__init__(message)
703+
self.message = message
704+
705+
641706
def register_adapters_and_converters():
642707
"""
643708
sqlite3 default adapters and converters.

src/tests/conftest.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import sqlite3
3+
from copy import deepcopy
34

45
import pytest
56
from dotenv import load_dotenv
@@ -14,6 +15,29 @@ def load_env_vars():
1415
load_dotenv(".env")
1516

1617

18+
@pytest.fixture(autouse=True)
19+
def reset_module_state():
20+
original_sqlc_adapters = deepcopy(sqlitecloud.adapters)
21+
original_sqlc_converters = deepcopy(sqlitecloud.converters)
22+
23+
original_sql_adapters = deepcopy(sqlite3.adapters)
24+
original_sql_converters = deepcopy(sqlite3.converters)
25+
26+
yield
27+
28+
sqlitecloud.adapters.clear()
29+
sqlitecloud.converters.clear()
30+
31+
sqlite3.adapters.clear()
32+
sqlite3.converters.clear()
33+
34+
sqlitecloud.adapters.update(original_sqlc_adapters)
35+
sqlitecloud.converters.update(original_sqlc_converters)
36+
37+
sqlite3.adapters.update(original_sql_adapters)
38+
sqlite3.converters.update(original_sql_converters)
39+
40+
1741
@pytest.fixture()
1842
def sqlitecloud_connection():
1943
account = SQLiteCloudAccount()
@@ -42,7 +66,16 @@ def sqlitecloud_dbapi2_connection():
4266
# the test.
4367
# Fixtures are both cached and cannot be called
4468
# directly whithin the test.
45-
yield next(get_sqlitecloud_dbapi2_connection())
69+
connection_generator = get_sqlitecloud_dbapi2_connection()
70+
71+
connection = next(connection_generator)
72+
73+
yield connection
74+
75+
try:
76+
next(connection_generator)
77+
except StopIteration:
78+
pass
4679

4780

4881
def get_sqlitecloud_dbapi2_connection(detect_types: int = 0):
@@ -62,6 +95,20 @@ def get_sqlitecloud_dbapi2_connection(detect_types: int = 0):
6295
connection.close()
6396

6497

98+
@pytest.fixture()
99+
def sqlite3_connection():
100+
connection_generator = get_sqlite3_connection()
101+
102+
connection = next(connection_generator)
103+
104+
yield connection
105+
106+
try:
107+
next(connection_generator)
108+
except StopIteration:
109+
pass
110+
111+
65112
def get_sqlite3_connection(detect_types: int = 0):
66113
# set isolation_level=None to enable autocommit
67114
# and to be aligned with the behavior of SQLite Cloud
@@ -71,4 +118,5 @@ def get_sqlite3_connection(detect_types: int = 0):
71118
detect_types=detect_types,
72119
)
73120
yield connection
121+
74122
connection.close()

0 commit comments

Comments
 (0)