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
54 changes: 19 additions & 35 deletions .claude/backend_development.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ rhosocial-activerecord-{backend}/
│ └── impl/
│ └── {backend}/
│ ├── __init__.py
│ ├── backend.py # Main backend implementation
│ ├── backend/ # sync + async backend classes
│ ├── adapters.py # Backend-specific type adapters
│ ├── config.py # Connection configuration
│ ├── dialect.py # SQL dialect handling
Expand Down Expand Up @@ -114,7 +114,7 @@ Expression.to_sql() -> Dialect.format_*() -> SQL string and parameters
- `operators.py`: SQL operations (binary, unary, arithmetic expressions)
- `predicates.py`: SQL predicate expressions (WHERE clause conditions)
- `query_parts.py`: SQL query clauses (WHERE, GROUP BY, HAVING, ORDER BY, etc.)
- `statements.py`: DML/DQL/DDL statements (SELECT, INSERT, UPDATE, DELETE, etc.)
- `statements/`: DML/DQL/DDL statements (a package, not a module) (SELECT, INSERT, UPDATE, DELETE, etc.)
- `functions.py`: Standalone factory functions for creating SQL expressions
- `aggregates.py`: SQL aggregation expressions and functions
- `advanced_functions.py`: Advanced SQL functions (CASE, CAST, EXISTS, window functions)
Expand Down Expand Up @@ -149,8 +149,9 @@ Every backend must implement the `StorageBackend` abstract base class. This is t
```python
# backend.py
from typing import Any, Dict, List, Optional, Tuple, Type
from ...base import StorageBackend, ConnectionConfig
from ...type_adapter import SQLTypeAdapter
from rhosocial.activerecord.backend.base import StorageBackend
from rhosocial.activerecord.backend.config import ConnectionConfig
from rhosocial.activerecord.backend.type_adapter import SQLTypeAdapter

class MyBackend(StorageBackend):
"""Implementation of MyDatabase backend using native driver."""
Expand All @@ -175,7 +176,8 @@ class MyBackend(StorageBackend):
"""Check if connection is valid."""
pass

def execute(self, sql: str, params: Optional[Tuple] = None, returning: Optional[Union[bool, List[str], ReturningOptions]] = None, column_adapters: Optional[Dict[str, Tuple[SQLTypeAdapter, Type]]] = None) -> QueryResult:
def execute(self, sql: str, params: Optional[Tuple] = None, *,
options: Optional[ExecutionOptions] = None) -> QueryResult:
"""Executes a SQL query."""
pass

Expand All @@ -197,10 +199,6 @@ class MyBackend(StorageBackend):
"""
pass

def _initialize_capabilities(self) -> DatabaseCapabilities:
"""Declare all features supported by this backend."""
pass

def _handle_error(self, error: Exception) -> None:
"""Catch driver-specific exceptions and raise standard errors."""
pass
Expand Down Expand Up @@ -241,10 +239,10 @@ Define an immutable configuration class for your backend.
# config.py
from dataclasses import dataclass, field
from typing import Optional, Dict, Any
from rhosocial.activerecord.backend.config import BaseConfig
from rhosocial.activerecord.backend.config import ConnectionConfig

@dataclass(frozen=True)
class MyDatabaseConfig(BaseConfig):
@dataclass
class MyDatabaseConfig(ConnectionConfig):
host: str = "localhost"
port: int = 5432
database: str
Expand Down Expand Up @@ -366,34 +364,20 @@ Every dialect inherits from:

```python
# dialect.py
from rhosocial.activerecord.backend.dialect import (
SQLDialect, ReturningClauseHandler, TypeMapping
)
from typing import Dict, List, Optional
from rhosocial.activerecord.backend.dialect.base import SQLDialectBase
from typing import Optional, Tuple

class MyDatabaseDialect(SQLDialect):
class MyDatabaseDialect(SQLDialectBase):
def __init__(self):
super().__init__()
self.returning_handler = MyDatabaseReturningHandler()

def quote_identifier(self, identifier: str) -> str: return f'"{identifier}"'
def get_placeholder(self, name: str = None) -> str: return "?"

def format_limit_offset(self, sql: str, limit: Optional[int], offset: Optional[int]) -> str:
if limit is not None: sql += f" LIMIT {limit}"
if offset is not None: sql += f" OFFSET {offset}"
return sql

def get_type_mappings(self) -> Dict[str, TypeMapping]:
return {"INTEGER": TypeMapping("INTEGER"), "TEXT": TypeMapping("TEXT"), ...}
def get_parameter_placeholder(self, position: int = 0) -> str:
return "?"

class MyDatabaseReturningHandler(ReturningClauseHandler):
@property
def is_supported(self) -> bool: return True

def format_clause(self, columns: Optional[List[str]] = None) -> str:
cols = "*" if not columns else ', '.join(self.dialect.quote_identifier(c) for c in columns)
return f"RETURNING {cols}"
# Identifier quoting is inherited from SQLDialectBase.format_identifier;
# LIMIT/OFFSET come from the DQL mixins; type mapping is not a dialect
# method but a set of SQLTypeAdapter registrations on the backend.
```

### 4. The Type Adaptation System
Expand Down Expand Up @@ -565,7 +549,7 @@ This checklist summarizes all the required and recommended steps for creating a
- [ ] Implement `get_default_adapter_suggestions` to define the conversion strategy.
- [ ] Implement `Transaction Management`.
- [ ] Implement `Error Handling` to map driver exceptions.
- [ ] Implement `Feature Detection` to declare capabilities.
- [ ] Implement the `supports_*` capability switches your dialect needs.
- [ ] If providing an `AsyncStorageBackend`, ensure it has **Functional Equivalence** with the sync version.

### Required Tests
Expand Down
27 changes: 13 additions & 14 deletions .claude/code_style.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ from pydantic import BaseModel, Field

# Local application imports
from rhosocial.activerecord.model import ActiveRecord
from rhosocial.activerecord.backend import StorageBackend
from rhosocial.activerecord.backend.base import StorageBackend

# Relative imports (within same package)
from .base import BaseActiveRecord
Expand All @@ -65,7 +65,7 @@ from ..interface import IActiveRecord

### Line Length and Wrapping

- Maximum line length: 100 characters
- Maximum line length: 120 characters (ruff `line-length = 120`; black formats to 100, but 100 is not the limit)
- Wrap long lines using parentheses:

```python
Expand Down Expand Up @@ -291,7 +291,7 @@ from typing import annotations

import third_party

from rhosocial.activerecord import local
from rhosocial.activerecord.model import ActiveRecord
from . import relative

# Constants
Expand Down Expand Up @@ -414,12 +414,12 @@ raise DatabaseError(
"""Test module for specific feature."""

import pytest
from rhosocial.activerecord import Model
from rhosocial.activerecord.model import ActiveRecord

# Fixtures at top
@pytest.fixture
def sample_model():
return Model(name="test")
return ActiveRecord(name="test")

# Test classes for organization
class TestCRUDOperations:
Expand Down Expand Up @@ -603,13 +603,12 @@ def transaction(backend: StorageBackend):
### Mixin Classes

```python
class TimestampMixin:
"""Add timestamp fields to models."""
created_at: datetime = Field(default_factory=datetime.now)
updated_at: Optional[datetime] = None

def before_save(self):
self.updated_at = datetime.now()
# TimestampMixin declares NO fields: it hooks
# ModelEvent.BEFORE_INSERT / BEFORE_UPDATE and requires the model to
# declare the fields itself, named per __created_at_field__ /
# __updated_at_field__. Use DefaultTimestampMixin to get them declared
# for you. There is no before_save() hook -- lifecycle is registered
# with instance.on(ModelEvent.X, handler).
```

## Expression-Dialect System Coding Standards
Expand Down Expand Up @@ -687,7 +686,7 @@ class MyExpression(mixins.ArithmeticMixin, mixins.ComparisonMixin, bases.SQLValu

def to_sql(self) -> 'bases.SQLQueryAndParams':
# Delegate formatting to dialect
formatted_value = self.dialect.format_string_literal(self.value)
formatted_value = self.dialect.format_literal(self.value)
return formatted_value, (self.value,)
```

Expand All @@ -705,7 +704,7 @@ def format_identifier(self, identifier: str) -> str:
- [ ] Path comment at file start matches actual location
- [ ] All functions/classes have docstrings
- [ ] Type hints on all function signatures
- [ ] No line exceeds 100 characters
- [ ] No line exceeds 120 characters
- [ ] Imports sorted and grouped correctly
- [ ] No commented-out code
- [ ] Exception messages are informative
Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/gen-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ CREATE TABLE users (
2. Show the SQL migration to create the table
3. Remind the user to configure the backend:
```python
from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend
from rhosocial.activerecord.backend.impl.sqlite.backend import SQLiteBackend
User.configure(SQLiteBackend("sqlite:///app.db"))
```

Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/validate-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ user = User(name="John") # Will raise "No backend configured"
user.save()

# CORRECT
from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend
from rhosocial.activerecord.backend.impl.sqlite.backend import SQLiteBackend
User.configure(SQLiteBackend("sqlite:///app.db"))
user = User(name="John")
user.save()
Expand Down
8 changes: 4 additions & 4 deletions .claude/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ dependencies = [

```bash
cd /mnt/i/GitHubRepositories/rhosocial/python-activerecord-sqlserver
source .venv/bin/activate
export PYTHONPATH=src
source .venv3.14-ubuntu26.04/bin/activate
export PYTHONPATH=src:tests
pytest tests/ -v
```

Expand All @@ -36,5 +36,5 @@ pytest tests/ -v

## Reference

- [Core testing guide](../python-activerecord/.claude/testing.md)
- [SQL Server backend development](../python-activerecord/.claude/backend_development.md)
- [Core testing guide](../../python-activerecord/.claude/testing.md)
- [SQL Server backend development](../../../python-activerecord/.claude/backend_development.md)
Loading
Loading