Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ jobs:
run: |
pip install -r requirements.txt
pip install -r requirements-dev.txt
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" # NOSONAR
pip install git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17#egg=rhosocial-activerecord-testsuite
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/error-taxonomy" # NOSONAR
pip install git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/error-taxonomy#egg=rhosocial-activerecord-testsuite # NOSONAR

- name: Install package
run: |
Expand Down Expand Up @@ -84,8 +84,8 @@ jobs:
run: |
pip install -r requirements.txt
pip install -r requirements-dev.txt
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" # NOSONAR
pip install git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17#egg=rhosocial-activerecord-testsuite
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/error-taxonomy" # NOSONAR
pip install git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/error-taxonomy#egg=rhosocial-activerecord-testsuite # NOSONAR

- name: Install package
run: |
Expand Down Expand Up @@ -117,8 +117,8 @@ jobs:
run: |
pip install -r requirements.txt
pip install -r requirements-dev.txt
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" # NOSONAR
pip install git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17#egg=rhosocial-activerecord-testsuite
pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/error-taxonomy" # NOSONAR
pip install git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/error-taxonomy#egg=rhosocial-activerecord-testsuite # NOSONAR

- name: Install package
run: |
Expand Down
1 change: 1 addition & 0 deletions changelog.d/12.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Classified Snowflake driver errors into the semantic error taxonomy (data, record, policy, concurrency, infrastructure) and aligned `execute` / `execute_many` with the execute-phase error contract: framework errors pass through, auto-commit failures raise `TransactionError`, and only transient connection failures are retried. Added contract tests covering the mapping and phases.
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@
from rhosocial.activerecord.backend.base import AsyncStorageBackend
from rhosocial.activerecord.backend.errors import (
ConnectionError,
DatabaseError,
IntegrityError,
OperationalError,
QueryError,
)
from rhosocial.activerecord.backend.introspection.backend_mixin import IntrospectorBackendMixin
from ..config import SnowflakeConnectionConfig
Expand All @@ -55,6 +51,16 @@ class AsyncSnowflakeBackend(
driver for async compatibility. This follows the project's sync/async parity
principle, providing the same API surface with async/await syntax.

Execution notes:
``execute``/``execute_many`` come from the core execution pipeline and
are not overridden. Snowflake has no row-level locking and no
deadlock/lock-wait semantics, so execute failures never surface as
``LockError`` or ``DeadlockError``; they keep the driver's own
operational/programming category. The backend defines no
``_handle_auto_commit``, so execute relies on the driver's
``autocommit`` setting (``True`` by default) rather than issuing an
extra commit.

Caveats:
- All I/O operations run in a thread pool executor, not truly async.
- High-concurrency scenarios should inject a bounded executor to
Expand Down Expand Up @@ -186,24 +192,25 @@ async def ping(self, reconnect: bool = True) -> bool:
return False

async def _handle_error(self, error: Exception) -> None:
"""Handle and classify a Snowflake error."""
category = self._classify_error(error)
error_msg = str(error)
if category == 'connection':
self.log(logging.DEBUG, f"Connection error: {error_msg}")
raise ConnectionError(error_msg) from error
elif category == 'integrity':
self.log(logging.DEBUG, f"Integrity error: {error_msg}")
raise IntegrityError(error_msg) from error
elif category == 'query':
self.log(logging.DEBUG, f"Query error: {error_msg}")
raise QueryError(error_msg) from error
elif category == 'operational':
self.log(logging.DEBUG, f"Operational error: {error_msg}")
raise OperationalError(error_msg) from error
else:
self.log(logging.DEBUG, f"Database error: {error_msg}")
raise DatabaseError(error_msg) from error
"""Handle and classify a Snowflake error asynchronously.

Framework ``DatabaseError`` instances are re-raised unchanged so
callers can still catch ``ConnectionError``, ``TypeConversionError`` or
``TransactionError`` by their real type; they are never reclassified
into a driver category. All other exceptions are translated into the
shared error taxonomy and carry driver diagnostics (errno, SQLSTATE,
operation, backend). Only transient infrastructure failures (network
timeouts, service unavailability, lost connections) become retryable.

Args:
error: The exception to handle.

Raises:
The same instance when ``error`` is already a framework
``DatabaseError``; otherwise the appropriate rhosocial error based
on classification.
"""
self._raise_mapped_error(error)

async def get_server_version(self) -> Tuple[int, ...]:
"""Get the Snowflake server version."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@
from rhosocial.activerecord.backend.base import StorageBackend
from rhosocial.activerecord.backend.errors import (
ConnectionError,
DatabaseError,
IntegrityError,
OperationalError,
QueryError,
)
from rhosocial.activerecord.backend.introspection.backend_mixin import IntrospectorBackendMixin
from ..config import SnowflakeConnectionConfig
Expand All @@ -31,6 +27,16 @@ class SnowflakeBackend(
"""Snowflake-specific backend implementation (synchronous).

Uses snowflake-connector-python for database connectivity.

Execution notes:
``execute``/``execute_many`` come from the core execution pipeline and
are not overridden. Snowflake has no row-level locking and no
deadlock/lock-wait semantics, so execute failures never surface as
``LockError`` or ``DeadlockError``; they keep the driver's own
operational/programming category. The backend defines no
``_handle_auto_commit``, so execute relies on the driver's
``autocommit`` setting (``True`` by default) rather than issuing an
extra commit.
"""

def __init__(self, **kwargs):
Expand Down Expand Up @@ -148,29 +154,23 @@ def ping(self, reconnect: bool = True) -> bool:
def _handle_error(self, error: Exception) -> None:
"""Handle and classify a Snowflake error.

Framework ``DatabaseError`` instances are re-raised unchanged so
callers can still catch ``ConnectionError``, ``TypeConversionError`` or
``TransactionError`` by their real type; they are never reclassified
into a driver category. All other exceptions are translated into the
shared error taxonomy and carry driver diagnostics (errno, SQLSTATE,
operation, backend). Only transient infrastructure failures (network
timeouts, service unavailability, lost connections) become retryable.

Args:
error: The exception to handle.

Raises:
Appropriate rhosocial error based on classification.
The same instance when ``error`` is already a framework
``DatabaseError``; otherwise the appropriate rhosocial error based
on classification.
"""
category = self._classify_error(error)
error_msg = str(error)
if category == 'connection':
self.log(logging.DEBUG, f"Connection error: {error_msg}")
raise ConnectionError(error_msg) from error
elif category == 'integrity':
self.log(logging.DEBUG, f"Integrity error: {error_msg}")
raise IntegrityError(error_msg) from error
elif category == 'query':
self.log(logging.DEBUG, f"Query error: {error_msg}")
raise QueryError(error_msg) from error
elif category == 'operational':
self.log(logging.DEBUG, f"Operational error: {error_msg}")
raise OperationalError(error_msg) from error
else:
self.log(logging.DEBUG, f"Database error: {error_msg}")
raise DatabaseError(error_msg) from error
self._raise_mapped_error(error)

def get_server_version(self) -> Tuple[int, ...]:
"""Get the Snowflake server version.
Expand Down
Loading
Loading