Skip to content

Commit

Permalink
chore: imlement get_missing_table (#497)
Browse files Browse the repository at this point in the history
  • Loading branch information
betodealmeida authored Dec 2, 2024
1 parent 6cd7b8d commit 73643e2
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 9 deletions.
20 changes: 18 additions & 2 deletions src/shillelagh/backends/apsw/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@
_logger = logging.getLogger(__name__)


def get_missing_table(message: str) -> Optional[str]:
"""
Return the missing table from a message.
This is used to extract the table name from an APSW error message.
"""
if match := NO_SUCH_TABLE.search(message):
return match.groupdict()["uri"]

return None


def check_closed(method: CURSOR_METHOD) -> CURSOR_METHOD:
"""Decorator that checks if a connection or cursor is closed."""

Expand Down Expand Up @@ -242,6 +254,7 @@ def execute(
# this is where the magic happens: instead of forcing users to register
# their virtual tables explicitly, we do it for them when they first try
# to access them and it fails because the table doesn't exist yet
created_tables = set()
while True:
try:
self._cursor.execute(operation, parameters)
Expand All @@ -250,10 +263,13 @@ def execute(
break
except apsw.SQLError as ex:
message = ex.args[0]
if match := NO_SUCH_TABLE.search(message):
if uri := get_missing_table(message):
if uri in created_tables:
raise ProgrammingError(message) from ex

# create the virtual table
uri = match.groupdict()["uri"]
self._create_table(uri)
created_tables.add(uri)
continue

raise ProgrammingError(message) from ex
Expand Down
39 changes: 32 additions & 7 deletions tests/backends/apsw/db_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# pylint: disable=protected-access, c-extension-no-member, too-few-public-methods

import datetime
from typing import Any
from typing import Any, Optional
from unittest import mock

import apsw
Expand All @@ -14,10 +14,10 @@

from shillelagh.adapters.registry import AdapterLoader, UnsafeAdaptersError
from shillelagh.backends.apsw.db import (
NO_SUCH_TABLE,
Connection,
connect,
convert_binding,
get_missing_table,
)
from shillelagh.exceptions import NotSupportedError, ProgrammingError
from shillelagh.fields import Float, String, StringInteger
Expand Down Expand Up @@ -211,6 +211,31 @@ def test_execute_with_native_parameters(registry: AdapterLoader) -> None:
assert cursor.rowcount == 0


def test_execute_table_not_created(
mocker: MockerFixture,
registry: AdapterLoader,
) -> None:
"""
Test error when table can not be created.
When that happens, we should raise the exception to prevent an infinite loop.
"""
registry.add("dummy", FakeAdapter)

connection = connect(":memory:", ["dummy"], isolation_level="IMMEDIATE")
cursor = connection.cursor()

# make `_create_table` a no-op
mocker.patch.object(cursor, "_create_table")

with pytest.raises(ProgrammingError) as excinfo:
cursor.execute(
'SELECT * FROM "dummy://" WHERE name = ?',
(datetime.datetime.now(),),
)
assert "no such table: dummy://" in str(excinfo.value)


def test_check_closed() -> None:
"""
Test trying to use cursor/connection after closing them.
Expand Down Expand Up @@ -549,16 +574,16 @@ def test_best_index(mocker: MockerFixture) -> None:


@pytest.mark.parametrize(
"error,uri",
"message,uri",
[
("apsw.SQLError: no such table: dummy://", "dummy://"),
("SQLError: no such table: dummy://", "dummy://"),
("no such table: dummy://", "dummy://"),
("another error", None),
],
)
def test_no_such_table_search(error: str, uri: str) -> None:
def test_get_missing_table(message: str, uri: Optional[str]) -> None:
"""
Test ``NO_SUCH_TABLE`` search.
Test the "no such table" error message processing.
"""
match = NO_SUCH_TABLE.search(error)
assert match and match.groupdict() == {"uri": uri}
assert get_missing_table(message) == uri

0 comments on commit 73643e2

Please sign in to comment.