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
2 changes: 1 addition & 1 deletion src/rhosocial/activerecord/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- Field type support
"""

__version__ = "1.0.0.dev6"
__version__ = "1.0.0.dev7"

# ### **Version Format**
# The version string MUST follow the [PEP 440](https://packaging.python.org/en/latest/specifications/version-specifiers/) standard.
Expand Down
2 changes: 1 addition & 1 deletion src/rhosocial/activerecord/backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,4 @@
'IsolationLevel',
]

__version__ = '0.3.0'
__version__ = '0.4.0'
185 changes: 185 additions & 0 deletions src/rhosocial/activerecord/backend/dialect.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,140 @@
"""
pass

class AggregateHandler(ABC):
"""Base class for handling database-specific aggregate functionality.

This handler provides a consistent interface to check for database
compatibility with various aggregate features and to format
SQL expressions appropriately for each database dialect.
"""

def __init__(self, version: tuple):
"""Initialize the aggregate handler.

Args:
version: Database version tuple (major, minor, patch)
"""
self._version = version

@property
def version(self) -> tuple:
"""Get database version."""
return self._version

Check warning on line 181 in src/rhosocial/activerecord/backend/dialect.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/dialect.py#L181

Added line #L181 was not covered by tests

@property
@abstractmethod
def supports_window_functions(self) -> bool:
"""Check if database supports window functions."""
pass

Check warning on line 187 in src/rhosocial/activerecord/backend/dialect.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/dialect.py#L187

Added line #L187 was not covered by tests

@property
@abstractmethod
def supports_advanced_grouping(self) -> bool:
"""Check if database supports advanced grouping (CUBE, ROLLUP, GROUPING SETS)."""
pass

Check warning on line 193 in src/rhosocial/activerecord/backend/dialect.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/dialect.py#L193

Added line #L193 was not covered by tests

@abstractmethod
def format_window_function(self,
expr: str,
partition_by: Optional[List[str]] = None,
order_by: Optional[List[str]] = None,
frame_type: Optional[str] = None,
frame_start: Optional[str] = None,
frame_end: Optional[str] = None,
exclude_option: Optional[str] = None) -> str:
"""Format window function SQL for specific database dialect.

Args:
expr: Base expression for window function
partition_by: PARTITION BY columns
order_by: ORDER BY columns
frame_type: Window frame type (ROWS/RANGE/GROUPS)
frame_start: Frame start specification
frame_end: Frame end specification
exclude_option: Frame exclusion option

Returns:
str: Formatted window function SQL

Raises:
WindowFunctionNotSupportedError: If window functions not supported
"""
pass

Check warning on line 221 in src/rhosocial/activerecord/backend/dialect.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/dialect.py#L221

Added line #L221 was not covered by tests

@abstractmethod
def format_grouping_sets(self,
type_name: str,
columns: List[Union[str, List[str]]]) -> str:
"""Format grouping sets SQL for specific database dialect.

Args:
type_name: Grouping type (CUBE, ROLLUP, GROUPING SETS)
columns: Columns to group by

Returns:
str: Formatted grouping sets SQL

Raises:
GroupingSetNotSupportedError: If grouping sets not supported
"""
pass

Check warning on line 239 in src/rhosocial/activerecord/backend/dialect.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/dialect.py#L239

Added line #L239 was not covered by tests

class JsonOperationHandler:
"""Interface for database-specific JSON operation support.

This class defines methods that should be implemented by each database dialect
to handle JSON operations according to their specific syntax.
"""

@property
@abstractmethod
def supports_json_operations(self) -> bool:
"""Check if database supports JSON operations."""
pass

Check warning on line 252 in src/rhosocial/activerecord/backend/dialect.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/dialect.py#L252

Added line #L252 was not covered by tests

@property
@abstractmethod
def supports_json_arrows(self) -> bool:
"""Check if database supports -> and ->> arrow operators for JSON access."""
pass

Check warning on line 258 in src/rhosocial/activerecord/backend/dialect.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/dialect.py#L258

Added line #L258 was not covered by tests

@abstractmethod
def format_json_operation(self,
column: Union[str, Any],
path: Optional[str] = None,
operation: str = "extract",
value: Any = None,
alias: Optional[str] = None) -> str:
"""Format JSON operation SQL for specific database dialect.

Args:
column: JSON column name or expression
path: JSON path string
operation: Operation type (extract, text, contains, exists, etc.)
value: Value for operations that need it (contains, insert, etc.)
alias: Optional alias for the result

Returns:
str: Formatted JSON operation SQL

Raises:
JsonOperationNotSupportedError: If JSON operations not supported
"""
pass

Check warning on line 282 in src/rhosocial/activerecord/backend/dialect.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/dialect.py#L282

Added line #L282 was not covered by tests

@abstractmethod
def supports_json_function(self, function_name: str) -> bool:
"""Check if specific JSON function is supported by the database.

Args:
function_name: Name of JSON function to check (e.g., "json_extract", "json_array")

Returns:
bool: True if function is supported
"""
pass

Check warning on line 294 in src/rhosocial/activerecord/backend/dialect.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/dialect.py#L294

Added line #L294 was not covered by tests

class SQLExpressionBase(ABC):
"""Base class for SQL expressions

Expand Down Expand Up @@ -241,6 +375,8 @@
_type_mapper: TypeMapper
_value_mapper: ValueMapper
_returning_handler: ReturningClauseHandler
_aggregate_handler: AggregateHandler
_json_operation_handler: JsonOperationHandler
_version: tuple

def __init__(self, version: tuple) -> None:
Expand Down Expand Up @@ -271,6 +407,16 @@
"""Get returning clause handler"""
return self._returning_handler

@property
def aggregate_handler(self) -> AggregateHandler:
"""Get aggregate functionality handler"""
return self._aggregate_handler

Check warning on line 413 in src/rhosocial/activerecord/backend/dialect.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/dialect.py#L413

Added line #L413 was not covered by tests

@property
def json_operation_handler(self) -> JsonOperationHandler:
"""Get JSON operation handler"""
return self._json_operation_handler

@abstractmethod
def format_expression(self, expr: SQLExpressionBase) -> str:
"""Format expression
Expand Down Expand Up @@ -372,6 +518,45 @@
"""Create SQL expression"""
pass

def format_json_expression(self, **params) -> str:
"""Format JSON expression according to dialect rules.

Delegates to the json_operation_handler for database-specific JSON formatting.
Uses keyword arguments to avoid direct coupling with the expression classes.

Args:
**params: JSON expression parameters including:
column: Column name or expression
path: JSON path string
operation: Operation type (extract, contains, exists, etc.)
value: Value for operations that need it (optional)
alias: Optional result alias (optional)

Returns:
str: Database-specific JSON expression

Raises:
ValueError: If required parameters are missing
JsonOperationNotSupportedError: If JSON operations not supported
"""
# Check the required parameters
if 'column' not in params or 'path' not in params:
raise ValueError("Missing required parameters for JSON expression: 'column' and 'path' are required")

Check warning on line 544 in src/rhosocial/activerecord/backend/dialect.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/dialect.py#L544

Added line #L544 was not covered by tests

# Set the default value
operation = params.get('operation', 'extract')
value = params.get('value', None)
alias = params.get('alias', None)

Check warning on line 549 in src/rhosocial/activerecord/backend/dialect.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/dialect.py#L547-L549

Added lines #L547 - L549 were not covered by tests

# Delegates to the json_operation_handler for database-specific JSON formatting
return self.json_operation_handler.format_json_operation(

Check warning on line 552 in src/rhosocial/activerecord/backend/dialect.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/dialect.py#L552

Added line #L552 was not covered by tests
column=params['column'],
path=params['path'],
operation=operation,
value=value,
alias=alias
)

class SQLBuilder:
"""SQL Builder

Expand Down
13 changes: 12 additions & 1 deletion src/rhosocial/activerecord/backend/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,18 @@ class ReturningNotSupportedError(DatabaseError):
"""Raised when RETURNING clause is not supported by the database"""
pass


class IsolationLevelError(TransactionError):
"""Raised when attempting to change isolation level during active transaction."""
pass

class WindowFunctionNotSupportedError(DatabaseError):
"""Raised when window functions are not supported by the database."""
pass

class JsonOperationNotSupportedError(DatabaseError):
"""Raised when JSON operations are not supported by the database."""
pass

class GroupingSetNotSupportedError(DatabaseError):
"""Raised when advanced grouping operations (CUBE, ROLLUP, GROUPING SETS) are not supported."""
pass
80 changes: 67 additions & 13 deletions src/rhosocial/activerecord/backend/impl/sqlite/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
import sys
import time
from sqlite3 import ProgrammingError
from typing import Optional, Tuple, List, Any, Dict
from typing import Optional, Tuple, List, Any, Dict, Union

from .dialect import SQLiteDialect, SQLDialectBase
from .transaction import SQLiteTransactionManager
from ...base import StorageBackend, ColumnTypes
from ...errors import ConnectionError, IntegrityError, OperationalError, QueryError, DeadlockError, DatabaseError, \
ReturningNotSupportedError
ReturningNotSupportedError, JsonOperationNotSupportedError
from ...typing import QueryResult


Expand Down Expand Up @@ -152,20 +152,41 @@
if self._delete_on_close and self.config.database != ":memory:":
try:
import os
import time
self.log(logging.INFO, f"Deleting database files: {self.config.database}")

# Define retry delete function
def retry_delete(file_path, max_retries=5, retry_delay=0.1):
for attempt in range(max_retries):
try:
if os.path.exists(file_path):
os.remove(file_path)
return True
return True # File doesn't exist, consider deletion successful
except OSError as e:

Check warning on line 166 in src/rhosocial/activerecord/backend/impl/sqlite/backend.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/impl/sqlite/backend.py#L166

Added line #L166 was not covered by tests
if attempt < max_retries - 1: # If not the last attempt
self.log(logging.DEBUG, f"Failed to delete file {file_path}, retrying: {str(e)}")
time.sleep(retry_delay) # Wait for a while before retrying

Check warning on line 169 in src/rhosocial/activerecord/backend/impl/sqlite/backend.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/impl/sqlite/backend.py#L168-L169

Added lines #L168 - L169 were not covered by tests
else:
self.log(logging.WARNING, f"Failed to delete file {file_path}, maximum retry attempts reached: {str(e)}")
return False
return False

Check warning on line 173 in src/rhosocial/activerecord/backend/impl/sqlite/backend.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/impl/sqlite/backend.py#L171-L173

Added lines #L171 - L173 were not covered by tests

# Delete main database file
if os.path.exists(self.config.database):
os.remove(self.config.database)

# Delete WAL and SHM files if they exist
main_db_deleted = retry_delete(self.config.database)

# Delete WAL and SHM files
wal_file = f"{self.config.database}-wal"
shm_file = f"{self.config.database}-shm"
if os.path.exists(wal_file):
os.remove(wal_file)
if os.path.exists(shm_file):
os.remove(shm_file)
self.log(logging.INFO, "Database files deleted successfully")
except OSError as e:
wal_deleted = retry_delete(wal_file)
shm_deleted = retry_delete(shm_file)

# Record deletion results
if main_db_deleted and wal_deleted and shm_deleted:
self.log(logging.INFO, "Database files deleted successfully")
else:
self.log(logging.WARNING, "Some database files could not be deleted after multiple attempts")
except Exception as e:

Check warning on line 189 in src/rhosocial/activerecord/backend/impl/sqlite/backend.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/impl/sqlite/backend.py#L188-L189

Added lines #L188 - L189 were not covered by tests
self.log(logging.ERROR, f"Failed to delete database files: {str(e)}")
raise ConnectionError(f"Failed to delete database files: {str(e)}")
except sqlite3.Error as e:
Expand Down Expand Up @@ -559,4 +580,37 @@
self.log(logging.WARNING,
f"Failed to determine SQLite version, defaulting to 3.35.0: {str(e)}")

return SQLiteBackend._sqlite_version_cache
return SQLiteBackend._sqlite_version_cache

def format_json_operation(self, column: Union[str, Any], path: Optional[str] = None,
operation: str = "extract", value: Any = None,
alias: Optional[str] = None) -> str:
"""Format JSON operation according to database dialect.

Delegates to the dialect's json_operation_handler for database-specific formatting.

Args:
column: JSON column name or expression
path: JSON path
operation: Operation type (extract, contains, exists, etc.)
value: Value for operations that need it (contains, insert, etc.)
alias: Optional alias for the result

Returns:
str: Database-specific JSON operation SQL

Raises:
JsonOperationNotSupportedError: If JSON operations not supported
"""
if not hasattr(self.dialect, 'json_operation_handler'):
raise JsonOperationNotSupportedError(

Check warning on line 606 in src/rhosocial/activerecord/backend/impl/sqlite/backend.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/impl/sqlite/backend.py#L606

Added line #L606 was not covered by tests
f"JSON operations not supported by {self.dialect.__class__.__name__}"
)

return self.dialect.json_operation_handler.format_json_operation(

Check warning on line 610 in src/rhosocial/activerecord/backend/impl/sqlite/backend.py

View check run for this annotation

Codecov / codecov/patch

src/rhosocial/activerecord/backend/impl/sqlite/backend.py#L610

Added line #L610 was not covered by tests
column=column,
path=path,
operation=operation,
value=value,
alias=alias
)
Loading