Skip to content

Commit d4b74f2

Browse files
committed
Tests for SQLite3 feature parity and pandas support.
1 parent ba53bde commit d4b74f2

File tree

9 files changed

+408
-15
lines changed

9 files changed

+408
-15
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ main.dSYM/
1414
.idea
1515
SqliteCloud.egg-info
1616
src/sqlitecloud/libsqcloud.so
17+
18+
playground.ipynb

requirements-dev.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,7 @@ isort==5.10.1
1414
autoflake==1.4
1515
pre-commit==2.17.0
1616
bandit==1.7.1
17+
# We can use the most recent compatible version because
18+
# this package is only used for testing compatibility
19+
# with pandas dataframe
20+
pandas>=1.1.5

src/sqlitecloud/dbapi2.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
SQCLOUD_RESULT_TYPE,
2323
SQCloudConfig,
2424
SQCloudConnect,
25+
SQCloudException,
2526
SqliteCloudAccount,
2627
SQLiteCloudDataTypes,
2728
)
@@ -198,12 +199,12 @@ def close(self):
198199

199200
def commit(self):
200201
"""
201-
This method is a no-op as SQLite Cloud is currently not supporting transactions.
202+
Not implementied yet.
202203
"""
203204

204205
def rollback(self):
205206
"""
206-
This method is a no-op as SQLite Cloud is currently not supporting transactions.
207+
Not implemented yet.
207208
"""
208209

209210
def cursor(self):
@@ -290,15 +291,21 @@ def rowcount(self) -> int:
290291
"""
291292
return self._resultset.nrows if self._is_result_rowset() else -1
292293

293-
def close(self) -> None:
294+
@property
295+
def lastrowid(self) -> Optional[int]:
296+
"""
297+
Not implemented yet in the library.
294298
"""
295-
Closes the database connection use to create the cursor.
299+
return None
296300

297-
Note:
298-
DB-API 2.0 interface does not manage the Sqlite Cloud PubSub feature.
299-
Therefore, only the main socket is closed.
301+
def close(self) -> None:
302+
"""
303+
Just mark the cursors to be no more usable in SQLite Cloud database.
304+
In sqlite the `close()` is used to free up resources: https://devpress.csdn.net/python/62fe355b7e668234661931d8.html
300305
"""
301-
self._driver.disconnect(self.connection.sqlcloud_connection, True)
306+
self._ensure_connection()
307+
308+
self._connection = None
302309

303310
def execute(
304311
self,
@@ -330,6 +337,8 @@ def execute(
330337
Returns:
331338
Cursor: The cursor object.
332339
"""
340+
self._ensure_connection()
341+
333342
prepared_statement = self._driver.prepare_statement(sql, parameters)
334343
result = self._driver.execute(
335344
prepared_statement, self.connection.sqlcloud_connection
@@ -361,6 +370,8 @@ def executemany(
361370
Returns:
362371
Cursor: The cursor object.
363372
"""
373+
self._ensure_connection()
374+
364375
commands = ""
365376
for parameters in seq_of_parameters:
366377
prepared_statement = self._driver.prepare_statement(sql, parameters)
@@ -379,6 +390,8 @@ def fetchone(self) -> Optional[Any]:
379390
The next row of the query result set as a tuple,
380391
or None if no more rows are available.
381392
"""
393+
self._ensure_connection()
394+
382395
if not self._is_result_rowset():
383396
return None
384397

@@ -395,6 +408,8 @@ def fetchmany(self, size=None) -> List[Any]:
395408
Returns:
396409
List[Tuple]: A list of rows, where each row is represented as a tuple.
397410
"""
411+
self._ensure_connection()
412+
398413
if not self._is_result_rowset():
399414
return []
400415

@@ -417,6 +432,8 @@ def fetchall(self) -> List[Any]:
417432
Returns:
418433
A list of rows, where each row is represented as a tuple.
419434
"""
435+
self._ensure_connection()
436+
420437
if not self._is_result_rowset():
421438
return []
422439

@@ -439,10 +456,22 @@ def _is_result_rowset(self) -> bool:
439456
self._resultset and self._resultset.tag == SQCLOUD_RESULT_TYPE.RESULT_ROWSET
440457
)
441458

459+
def _ensure_connection(self):
460+
"""
461+
Ensure the cursor is usable or has been closed.
462+
463+
Raises:
464+
SQCloudException: If the cursor is closed.
465+
"""
466+
if not self._connection:
467+
raise SQCloudException("The cursor is closed.")
468+
442469
def __iter__(self) -> "Cursor":
443470
return self
444471

445472
def __next__(self) -> Optional[Tuple[Any]]:
473+
self._ensure_connection()
474+
446475
if (
447476
not self._resultset.is_result
448477
and self._resultset.data
@@ -457,6 +486,3 @@ def __next__(self) -> Optional[Tuple[Any]]:
457486
return self._call_row_factory(out)
458487

459488
raise StopIteration
460-
461-
def __len__(self) -> int:
462-
return self.rowcount

src/sqlitecloud/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class SQCLOUD_ROWSET(Enum):
3939

4040
class SQCLOUD_VALUE_TYPE(Enum):
4141
INTEGER = "INTEGER"
42-
FLOAT = "FLOAT"
42+
FLOAT = "REAL"
4343
TEXT = "TEXT"
4444
BLOB = "BLOB"
4545
NULL = "NULL"

src/tests/assets/chinook.sqlite

866 KB
Binary file not shown.

src/tests/assets/prices.csv

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
DATE,TICKER,CLOSE
2+
20161125,GE,31.440001
3+
20161123,GE,31.34
4+
20161122,GE,31.18
5+
20161121,GE,30.870001
6+
20161118,GE,30.67
7+
20161117,GE,30.790001
8+
20161116,GE,30.74
9+
20161115,GE,30.75
10+
20161114,GE,30.51
11+
20161111,GE,30.709999
12+
20161110,GE,30.41
13+
20161109,GE,29.629999
14+
20161108,GE,29.42
15+
20161107,GE,29.309999
16+
20161104,GE,28.440001
17+
20161103,GE,28.280001
18+
20161102,GE,28.49
19+
20161101,GE,28.879999
20+
20161031,GE,29.1
21+
20161028,GE,29.219999
22+
20161027,GE,28.629999
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import os
2+
3+
import pandas as pd
4+
from pandas.testing import assert_frame_equal
5+
6+
7+
# Integration tests for sqlitecloud and pandas dataframe
8+
class TestPanads:
9+
def test_insert_from_dataframe(self, sqlitecloud_dbapi2_connection):
10+
conn = sqlitecloud_dbapi2_connection
11+
12+
dfprices = pd.read_csv(
13+
os.path.join(os.path.dirname(__file__), "../assets/prices.csv")
14+
)
15+
16+
dfmapping = pd.DataFrame(
17+
{
18+
"AXP": ["American Express Company"],
19+
"GE": ["General Electric Company"],
20+
"GS": ["Goldman Sachs Group Inc"],
21+
"UTX": ["United Technologies Corporation"],
22+
}
23+
)
24+
25+
conn.executemany(
26+
"DROP TABLE IF EXISTS ?", [("CLOSING_PRICES",), ("TICKER_MAPPING",)]
27+
)
28+
29+
# arg if_exists="replace" raises the error
30+
dfprices.to_sql("CLOSING_PRICES", conn, index=False)
31+
dfmapping.to_sql("TICKER_MAPPING", conn, index=False)
32+
33+
df_actual_tables = pd.read_sql(
34+
"SELECT name FROM sqlite_master WHERE type='table'", conn
35+
)
36+
df_actual_prices = pd.read_sql("SELECT * FROM CLOSING_PRICES", conn)
37+
df_actual_mapping = pd.read_sql("SELECT * FROM TICKER_MAPPING", conn)
38+
39+
assert "CLOSING_PRICES" in df_actual_tables["name"].to_list()
40+
assert "TICKER_MAPPING" in df_actual_tables["name"].to_list()
41+
assert_frame_equal(
42+
df_actual_prices,
43+
dfprices,
44+
check_exact=False,
45+
atol=1e-6,
46+
check_dtype=False,
47+
)
48+
assert_frame_equal(
49+
df_actual_mapping,
50+
dfmapping,
51+
check_exact=False,
52+
atol=1e-6,
53+
check_dtype=False,
54+
)
55+
56+
def test_select_into_dataframe(self, sqlitecloud_dbapi2_connection):
57+
conn = sqlitecloud_dbapi2_connection
58+
59+
query = "SELECT * FROM albums"
60+
df = pd.read_sql_query(query, conn)
61+
cursor = conn.execute(query)
62+
63+
assert df.columns.to_list() == [
64+
description[0] for description in cursor.description
65+
]
66+
# compare as tuples
67+
assert list(df.itertuples(index=False, name=None)) == cursor.fetchall()

0 commit comments

Comments
 (0)