Skip to content

Commit 57696f9

Browse files
committed
feat(rowcount): support to rowcount, lastworid and total_changes
1 parent 43546b8 commit 57696f9

File tree

2 files changed

+231
-20
lines changed

2 files changed

+231
-20
lines changed

src/sqlitecloud/dbapi2.py

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from sqlitecloud.resultset import (
3333
SQLITECLOUD_RESULT_TYPE,
3434
SQLITECLOUD_VALUE_TYPE,
35+
SQLiteCloudOperationResult,
3536
SQLiteCloudResult,
3637
)
3738

@@ -199,6 +200,8 @@ def __init__(
199200

200201
self.detect_types = detect_types
201202

203+
self.total_changes = 0
204+
202205
@property
203206
def sqlcloud_connection(self) -> SQLiteCloudConnect:
204207
"""
@@ -347,6 +350,7 @@ def __init__(self, connection: Connection) -> None:
347350
self._connection = connection
348351
self._iter_row: int = 0
349352
self._resultset: SQLiteCloudResult = None
353+
self._result_operation: SQLiteCloudOperationResult = None
350354

351355
self.row_factory: Optional[Callable[["Cursor", Tuple], object]] = None
352356

@@ -402,21 +406,26 @@ def description(
402406
@property
403407
def rowcount(self) -> int:
404408
"""
405-
The number of rows that the last .execute*() produced (for DQL statements like SELECT)
406-
407-
The number of rows affected by DML statements like UPDATE or INSERT is not supported.
408-
409-
Returns:
410-
int: The number of rows in the result set or -1 if no result set is available.
409+
The number of rows that the last .execute*() returned for DQL statements like SELECT or
410+
the number rows affected by DML statements like UPDATE, INSERT and DELETE.
411411
"""
412-
return self._resultset.nrows if self._is_result_rowset() else -1
412+
if self._is_result_rowset():
413+
return self._resultset.nrows
414+
if self._is_result_operation():
415+
return self._result_operation.changes
416+
return -1
413417

414418
@property
415419
def lastrowid(self) -> Optional[int]:
416420
"""
417-
Not implemented yet in the library.
421+
Last rowid for DML operations (INSERT, UPDATE, DELETE).
422+
In case of `executemany()` it returns the last rowid of the last operation.
418423
"""
419-
return None
424+
return (
425+
self._result_operation.rowid
426+
if self._result_operation and self._result_operation.rowid > 0
427+
else None
428+
)
420429

421430
def close(self) -> None:
422431
"""
@@ -430,7 +439,7 @@ def close(self) -> None:
430439
def execute(
431440
self,
432441
sql: str,
433-
parameters: Union[Tuple[any], Dict[Union[str, int], any]] = (),
442+
parameters: Union[Tuple[Any], Dict[Union[str, int], Any]] = (),
434443
) -> "Cursor":
435444
"""
436445
Prepare and execute a SQL statement (either a query or command) to the SQLite Cloud database.
@@ -459,19 +468,26 @@ def execute(
459468

460469
parameters = self._adapt_parameters(parameters)
461470

462-
prepared_statement = self._driver.prepare_statement(sql, parameters)
463-
result = self._driver.execute(
464-
prepared_statement, self.connection.sqlcloud_connection
471+
# TODO: convert parameters from :name to `?` style
472+
result = self._driver.execute_statement(
473+
sql, parameters, self.connection.sqlcloud_connection
465474
)
466475

467-
self._resultset = result
476+
self._resultset = None
477+
self._result_operation = None
478+
479+
if isinstance(result, SQLiteCloudResult):
480+
self._resultset = result
481+
if isinstance(result, SQLiteCloudOperationResult):
482+
self._result_operation = result
483+
self._connection.total_changes = result.total_changes
468484

469485
return self
470486

471487
def executemany(
472488
self,
473489
sql: str,
474-
seq_of_parameters: Iterable[Union[Tuple[any], Dict[Union[str, int], any]]],
490+
seq_of_parameters: Iterable[Union[Tuple[Any], Dict[Union[str, int], Any]]],
475491
) -> "Cursor":
476492
"""
477493
Executes a SQL statement multiple times, each with a different set of parameters.
@@ -489,13 +505,16 @@ def executemany(
489505
self._ensure_connection()
490506

491507
commands = ""
508+
params = []
492509
for parameters in seq_of_parameters:
493-
parameters = self._adapt_parameters(parameters)
510+
params += list(parameters)
494511

495-
prepared_statement = self._driver.prepare_statement(sql, parameters)
496-
commands += prepared_statement + ";"
512+
if not sql.endswith(";"):
513+
sql += ";"
497514

498-
self.execute(commands)
515+
commands += sql
516+
517+
self.execute(commands, params)
499518

500519
return self
501520

@@ -578,6 +597,9 @@ def _is_result_rowset(self) -> bool:
578597
and self._resultset.tag == SQLITECLOUD_RESULT_TYPE.RESULT_ROWSET
579598
)
580599

600+
def _is_result_operation(self) -> bool:
601+
return self._result_operation is not None
602+
581603
def _ensure_connection(self):
582604
"""
583605
Ensure the cursor is usable or has been closed.
@@ -697,7 +719,8 @@ def __iter__(self) -> "Cursor":
697719

698720
def __next__(self) -> Optional[Tuple[Any]]:
699721
if (
700-
not self._resultset.is_result
722+
self._resultset
723+
and not self._resultset.is_result
701724
and self._resultset.data
702725
and self._iter_row < self._resultset.nrows
703726
):

src/tests/integration/test_dbapi2.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,191 @@ def test_row_object_for_factory_string_representation(
259259
row = cursor.fetchone()
260260

261261
assert str(row) == "Bar: foo\nDoe: john"
262+
263+
def test_last_rowid_and_rowcount_with_select(self, sqlitecloud_dbapi2_connection):
264+
connection = sqlitecloud_dbapi2_connection
265+
266+
cursor = connection.execute("SELECT * FROM genres LIMIT 3")
267+
268+
assert cursor.fetchone() is not None
269+
assert cursor.lastrowid is None
270+
assert cursor.rowcount == 3
271+
272+
def test_last_rowid_and_rowcount_with_execute_update(
273+
self, sqlitecloud_dbapi2_connection
274+
):
275+
connection = sqlitecloud_dbapi2_connection
276+
277+
new_name = "Jazz" + str(uuid.uuid4())
278+
genreId = 2
279+
280+
cursor = connection.execute(
281+
"UPDATE genres SET Name = ? WHERE GenreId = ?",
282+
(new_name, genreId),
283+
)
284+
285+
assert cursor.fetchone() is None
286+
assert cursor.lastrowid is None
287+
assert cursor.rowcount == 1
288+
289+
def test_last_rowid_and_rowcount_with_execute_insert(
290+
self, sqlitecloud_dbapi2_connection
291+
):
292+
connection = sqlitecloud_dbapi2_connection
293+
294+
new_name = "Jazz" + str(uuid.uuid4())
295+
296+
cursor = connection.execute(
297+
"INSERT INTO genres (Name) VALUES (?)",
298+
(new_name,),
299+
)
300+
301+
last_result = connection.execute(
302+
"SELECT GenreId FROM genres WHERE Name = ?", (new_name,)
303+
)
304+
305+
assert cursor.fetchone() is None
306+
assert cursor.lastrowid == last_result.fetchone()[0]
307+
assert cursor.rowcount == 1
308+
309+
def test_last_rowid_and_rowcount_with_execute_delete(
310+
self, sqlitecloud_dbapi2_connection
311+
):
312+
connection = sqlitecloud_dbapi2_connection
313+
314+
new_name = "Jazz" + str(uuid.uuid4())
315+
316+
cursor_select = connection.execute(
317+
"INSERT INTO genres (Name) VALUES (?)",
318+
(new_name,),
319+
)
320+
321+
cursor = connection.execute("DELETE FROM genres WHERE Name = ?", (new_name,))
322+
323+
assert cursor.fetchone() is None
324+
assert cursor.lastrowid == cursor_select.lastrowid
325+
assert cursor.rowcount == 1
326+
327+
def test_last_rowid_and_rowcount_with_multiple_updates(
328+
self, sqlitecloud_dbapi2_connection
329+
):
330+
connection = sqlitecloud_dbapi2_connection
331+
332+
new_name = "Jazz" + str(uuid.uuid4())
333+
334+
cursor = connection.execute(
335+
"UPDATE genres SET Name = ? WHERE GenreId = ? or GenreId = ?",
336+
(new_name, 2, 3),
337+
)
338+
339+
assert cursor.fetchone() is None
340+
assert cursor.lastrowid is None
341+
assert cursor.rowcount == 2
342+
343+
def test_last_rowid_and_rowcount_with_multiple_deletes(
344+
self, sqlitecloud_dbapi2_connection
345+
):
346+
connection = sqlitecloud_dbapi2_connection
347+
348+
new_name1 = "Jazz" + str(uuid.uuid4())
349+
new_name2 = "Jazz" + str(uuid.uuid4())
350+
351+
cursor = connection.executemany(
352+
"INSERT INTO genres (Name) VALUES (?)",
353+
[(new_name1,), (new_name2,)],
354+
)
355+
356+
cursor = connection.execute(
357+
"DELETE FROM genres WHERE Name = ? or Name = ?", (new_name1, new_name2)
358+
)
359+
360+
assert cursor.fetchone() is None
361+
assert cursor.lastrowid > 0
362+
assert cursor.rowcount == 2
363+
364+
def test_last_rowid_and_rowcount_with_executemany_updates(
365+
self, sqlitecloud_dbapi2_connection
366+
):
367+
connection = sqlitecloud_dbapi2_connection
368+
369+
new_name1 = "Jazz" + str(uuid.uuid4())
370+
new_name2 = "Jazz" + str(uuid.uuid4())
371+
genreId = 2
372+
373+
cursor = connection.executemany(
374+
"UPDATE genres SET Name = ? WHERE GenreId = ?;",
375+
[(new_name1, genreId), (new_name2, genreId)],
376+
)
377+
378+
assert cursor.fetchone() is None
379+
assert cursor.lastrowid is None
380+
assert cursor.rowcount == 1
381+
382+
def test_last_rowid_and_rowcount_with_executemany_inserts(
383+
self, sqlitecloud_dbapi2_connection
384+
):
385+
connection = sqlitecloud_dbapi2_connection
386+
387+
new_name1 = "Jazz" + str(uuid.uuid4())
388+
new_name2 = "Jazz" + str(uuid.uuid4())
389+
390+
cursor = connection.executemany(
391+
"INSERT INTO genres (Name) VALUES (?)",
392+
[(new_name1,), (new_name2,)],
393+
)
394+
395+
last_result = connection.execute(
396+
"SELECT GenreId FROM genres WHERE Name = ?", (new_name2,)
397+
)
398+
399+
assert cursor.fetchone() is None
400+
assert cursor.lastrowid == last_result.fetchone()[0]
401+
assert cursor.rowcount == 1
402+
403+
def test_last_rowid_and_rowcount_with_executemany_deletes(
404+
self, sqlitecloud_dbapi2_connection
405+
):
406+
connection = sqlitecloud_dbapi2_connection
407+
408+
new_name1 = "Jazz" + str(uuid.uuid4())
409+
new_name2 = "Jazz" + str(uuid.uuid4())
410+
411+
cursor_select = connection.executemany(
412+
"INSERT INTO genres (Name) VALUES (?)",
413+
[(new_name1,), (new_name2,)],
414+
)
415+
416+
cursor = connection.executemany(
417+
"DELETE FROM genres WHERE Name = ?", [(new_name1,), (new_name2,)]
418+
)
419+
420+
assert cursor.fetchone() is None
421+
assert cursor.lastrowid == cursor_select.lastrowid
422+
assert cursor.rowcount == 1
423+
424+
def test_connection_total_changes(self, sqlitecloud_dbapi2_connection):
425+
connection = sqlitecloud_dbapi2_connection
426+
427+
new_name1 = "Jazz" + str(uuid.uuid4())
428+
new_name2 = "Jazz" + str(uuid.uuid4())
429+
new_name3 = "Jazz" + str(uuid.uuid4())
430+
431+
connection.executemany(
432+
"INSERT INTO genres (Name) VALUES (?)",
433+
[(new_name1,), (new_name2,)],
434+
)
435+
assert connection.total_changes == 2
436+
437+
connection.execute("SELECT * FROM genres")
438+
assert connection.total_changes == 2
439+
440+
connection.execute(
441+
"UPDATE genres SET Name = ? WHERE Name = ?", (new_name3, new_name1)
442+
)
443+
assert connection.total_changes == 3
444+
445+
connection.execute(
446+
"DELETE FROM genres WHERE Name in (?, ?, ?)",
447+
(new_name1, new_name2, new_name3),
448+
)
449+
assert connection.total_changes == 5

0 commit comments

Comments
 (0)