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
1 change: 1 addition & 0 deletions changelog.d/24.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Contract the rebuilt column layer. The column-expression and protocol contracts are validated against each backend's own `ColumnTypeSupport` tables: an explicit declaration is backend-independent, inference is compared with the backend's `suggested_column_types()` answer, and a backend refusal (`None`) skips rather than fails. New guarantees pin the completeness of the common entries (a canonical `COMMON_TYPES` list now lives in `column_helpers.py`), class-or-`None` answers, constructibility, the absence of a value-family label, and the `UseColumnType` escape hatch past a refusal. Core-level `Model.c` operation gates exercise the surface through the field proxy: each value family's operations are reachable and produce expressions, and the ones a family does not offer refuse — `AttributeError` for a missing attribute, `TypeError` where the interpreter itself refuses a family that never declared arithmetic.
1 change: 1 addition & 0 deletions changelog.d/24.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sync the contracts with the core's DataType accessors — `column_type()` becomes `column_data_type()` and `columns_type()` becomes `columns_data_type()`, and a new declaration-accessor contract pins the `UseColumnType` marker's identity and `.column_class`, the `None` answer for a field that declares nothing, and the `TypeError` for two declarations on one field. Unify the unknown-field ruling across the accessor family: a field the model does not have is a mistake in the call rather than an answer, so every field accessor raises `KeyError` for it and an explicitly named unknown field fails the batch accessor the same way, while `None` (or an empty collection) remains the answer for a real field that declares nothing. Follow the core's `DateTimeColumn` → `TimestampColumn` rename, migrate the shared fixtures off the bare-`Column` doctrine (expressions are built from objects — a `FunctionCall` where a parameter wraps one, a `TableExpression` for parameters annotated as one, `ArrayType`'s required `element_type` supplied), and update the `typed_column` README.
1 change: 1 addition & 0 deletions changelog.d/24.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Keep the pool's per-test slot release off the POSIX locking module. `release_all` imported `fcntl` at function entry, so a serial run on Windows failed every test with `ModuleNotFoundError` even though no slot lock had ever been taken — slot locks are only ever taken by `_acquire_slot`, reachable exclusively while pooling is active. The teardown hook now returns before the import when the worker holds no slot, with regression tests covering both the serial-run path and the POSIX-only locked path.
16 changes: 14 additions & 2 deletions src/rhosocial/activerecord/testsuite/core/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,12 +276,24 @@ def release_all() -> None:

Called when a test finishes so the freed slots are immediately available to
the next test on any worker.
"""
import fcntl

Returns immediately when the worker holds no slot. Slot locks are only ever
taken by :func:`_acquire_slot`, which is reachable exclusively through the
``pooled_database_*`` helpers and therefore only while :func:`pooling_active`
holds. A serial run (no ``-n``) never opens a lock file, so it must not touch
the platform-specific locking module either: ``fcntl`` exists only on POSIX,
and importing it unconditionally made ``pytest_sessionfinish`` fail on Windows
with ``ModuleNotFoundError: No module named 'fcntl'`` even though no lock was
ever held.
"""
with _GUARD:
held = list(_HELD.items())
_HELD.clear()
if not held:
return

import fcntl

for scenario, index in held:
fh = _OPEN_LOCKS.pop((scenario, index), None)
if fh is None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from rhosocial.activerecord.base import DerivedField, UseColumn
from rhosocial.activerecord.model import ActiveRecord
from rhosocial.activerecord.backend.expression import Column, Literal
from rhosocial.activerecord.backend.expression import Literal, NumericColumn


class TestDerivedFieldDeclaration:
Expand All @@ -27,7 +27,7 @@ def test_descriptor_class_access(self, product_class):
def test_derived_field_with_static_expression(self, product_class):
"""Test DerivedField constructed with a pre-built expression object (non-callable path)."""
dialect = product_class.backend().dialect
static_expr = Column(dialect, "price") * Literal(dialect, 2)
static_expr = NumericColumn(dialect, "price") * Literal(dialect, 2)
df = DerivedField(static_expr)
resolved = df.resolve(dialect)
assert resolved is static_expr, \
Expand Down Expand Up @@ -182,7 +182,7 @@ def test_derived_dict_with_lambda(self, product_class):
"""A lambda-based derived dict should evaluate against the dialect."""
self._insert(product_class, "D1", 100.0, 4)
results = product_class.find_all(
derived={"my_discount": lambda d: Column(d, "price") * Literal(d, 0.8)}
derived={"my_discount": lambda d: NumericColumn(d, "price") * Literal(d, 0.8)}
)
assert results[0].__dict__["my_discount"] == pytest.approx(80.0), \
"Expected my_discount to be 80.0"
Expand All @@ -202,7 +202,7 @@ def test_derived_dict_with_expression_object(self, product_class):
"""A derived dict should accept a pre-built expression object directly."""
self._insert(product_class, "D3", 80.0, 5)
dialect = product_class.backend().dialect
expr = Column(dialect, "price") * Literal(dialect, 0.5)
expr = NumericColumn(dialect, "price") * Literal(dialect, 0.5)
results = product_class.find_all(
derived={"half_price": expr}
)
Expand Down Expand Up @@ -239,7 +239,7 @@ def test_extra_derived_basic(self, product_class):
"""extra_derived should add new derived fields without disturbing declared ones."""
self._insert(product_class, "F", 100.0, 10)
results = product_class.find_all(
extra_derived={"triple_price": lambda d: Column(d, "price") * Literal(d, 3)}
extra_derived={"triple_price": lambda d: NumericColumn(d, "price") * Literal(d, 3)}
)
assert len(results) == 1, "Expected 1 record to be returned"
assert results[0].__dict__["triple_price"] == pytest.approx(300.0), \
Expand All @@ -250,15 +250,15 @@ def test_extra_derived_conflict_raises(self, product_class):
self._insert(product_class, "G", 50.0, 2)
with pytest.raises(ValueError, match="conflicts with a declared derived field"):
product_class.find_all(
extra_derived={"discounted_price": lambda d: Column(d, "price") * Literal(d, 0.5)}
extra_derived={"discounted_price": lambda d: NumericColumn(d, "price") * Literal(d, 0.5)}
)

def test_derived_and_extra_derived_together(self, product_class):
"""derived and extra_derived should compose in a single query."""
self._insert(product_class, "H", 40.0, 5)
results = product_class.find_all(
derived=True,
extra_derived={"double_qty": lambda d: Column(d, "quantity") * Literal(d, 2)}
extra_derived={"double_qty": lambda d: NumericColumn(d, "quantity") * Literal(d, 2)}
)
assert results[0].discounted_price == pytest.approx(36.0), \
"Expected discounted_price to be 36.0"
Expand Down Expand Up @@ -397,7 +397,7 @@ class ConflictModel(ActiveRecord):
id: Optional[int] = None
discount_rate: Annotated[float, UseColumn("disc")]
discounted: ClassVar[Annotated[float, DerivedField(
lambda d: Column(d, "price") * Literal(d, 0.9),
lambda d: NumericColumn(d, "price") * Literal(d, 0.9),
), UseColumn("disc")]]

def test_use_column_no_conflict_different_names(self):
Expand All @@ -407,7 +407,7 @@ class NoConflict(ActiveRecord):
id: Optional[int] = None
discount_rate: Annotated[float, UseColumn("rate")]
discounted: ClassVar[Annotated[float, DerivedField(
lambda d: Column(d, "price") * Literal(d, 0.9),
lambda d: NumericColumn(d, "price") * Literal(d, 0.9),
), UseColumn("disc")]]

assert NoConflict.__derived_fields__["discounted"].column_name == "disc", \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from rhosocial.activerecord.base import DerivedField, UseColumn
from rhosocial.activerecord.model import ActiveRecord
from rhosocial.activerecord.backend.expression import Column, Literal
from rhosocial.activerecord.backend.expression import Literal, NumericColumn


class TestAsyncDerivedFieldDeclaration:
Expand All @@ -27,7 +27,7 @@ async def test_descriptor_class_access(self, async_product_class):
async def test_derived_field_with_static_expression(self, async_product_class):
"""Test DerivedField constructed with a pre-built expression object (non-callable path)."""
dialect = async_product_class.backend().dialect
static_expr = Column(dialect, "price") * Literal(dialect, 2)
static_expr = NumericColumn(dialect, "price") * Literal(dialect, 2)
df = DerivedField(static_expr)
resolved = df.resolve(dialect)
assert resolved is static_expr, \
Expand Down Expand Up @@ -182,7 +182,7 @@ async def test_derived_dict_with_lambda(self, async_product_class):
"""A lambda-based derived dict should evaluate against the dialect."""
await self._insert(async_product_class, "D1", 100.0, 4)
results = await async_product_class.find_all(
derived={"my_discount": lambda d: Column(d, "price") * Literal(d, 0.8)}
derived={"my_discount": lambda d: NumericColumn(d, "price") * Literal(d, 0.8)}
)
assert results[0].__dict__["my_discount"] == pytest.approx(80.0), \
"Expected my_discount to be 80.0"
Expand All @@ -202,7 +202,7 @@ async def test_derived_dict_with_expression_object(self, async_product_class):
"""A derived dict should accept a pre-built expression object directly."""
await self._insert(async_product_class, "D3", 80.0, 5)
dialect = async_product_class.backend().dialect
expr = Column(dialect, "price") * Literal(dialect, 0.5)
expr = NumericColumn(dialect, "price") * Literal(dialect, 0.5)
results = await async_product_class.find_all(
derived={"half_price": expr}
)
Expand Down Expand Up @@ -239,7 +239,7 @@ async def test_extra_derived_basic(self, async_product_class):
"""extra_derived should add new derived fields without disturbing declared ones."""
await self._insert(async_product_class, "F", 100.0, 10)
results = await async_product_class.find_all(
extra_derived={"triple_price": lambda d: Column(d, "price") * Literal(d, 3)}
extra_derived={"triple_price": lambda d: NumericColumn(d, "price") * Literal(d, 3)}
)
assert len(results) == 1, "Expected 1 record to be returned"
assert results[0].__dict__["triple_price"] == pytest.approx(300.0), \
Expand All @@ -250,15 +250,15 @@ async def test_extra_derived_conflict_raises(self, async_product_class):
await self._insert(async_product_class, "G", 50.0, 2)
with pytest.raises(ValueError, match="conflicts with a declared derived field"):
await async_product_class.find_all(
extra_derived={"discounted_price": lambda d: Column(d, "price") * Literal(d, 0.5)}
extra_derived={"discounted_price": lambda d: NumericColumn(d, "price") * Literal(d, 0.5)}
)

async def test_derived_and_extra_derived_together(self, async_product_class):
"""derived and extra_derived should compose in a single query."""
await self._insert(async_product_class, "H", 40.0, 5)
results = await async_product_class.find_all(
derived=True,
extra_derived={"double_qty": lambda d: Column(d, "quantity") * Literal(d, 2)}
extra_derived={"double_qty": lambda d: NumericColumn(d, "quantity") * Literal(d, 2)}
)
assert results[0].discounted_price == pytest.approx(36.0), \
"Expected discounted_price to be 36.0"
Expand Down Expand Up @@ -397,7 +397,7 @@ class ConflictModel(ActiveRecord):
id: Optional[int] = None
discount_rate: Annotated[float, UseColumn("disc")]
discounted: ClassVar[Annotated[float, DerivedField(
lambda d: Column(d, "price") * Literal(d, 0.9),
lambda d: NumericColumn(d, "price") * Literal(d, 0.9),
), UseColumn("disc")]]

async def test_use_column_no_conflict_different_names(self):
Expand All @@ -407,7 +407,7 @@ class NoConflict(ActiveRecord):
id: Optional[int] = None
discount_rate: Annotated[float, UseColumn("rate")]
discounted: ClassVar[Annotated[float, DerivedField(
lambda d: Column(d, "price") * Literal(d, 0.9),
lambda d: NumericColumn(d, "price") * Literal(d, 0.9),
), UseColumn("disc")]]

assert NoConflict.__derived_fields__["discounted"].column_name == "disc", \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ class AsyncMappedOrderItem(CompositePKMixin, AsyncActiveRecord):
# =============================================================================

from rhosocial.activerecord.base import DerivedField
from rhosocial.activerecord.backend.expression import Column, Literal
from rhosocial.activerecord.backend.expression import NumericColumn, Literal


class Product(ActiveRecord):
Expand All @@ -779,11 +779,11 @@ class Product(ActiveRecord):
quantity: int

discounted_price: ClassVar[Annotated[float, DerivedField(
lambda d: Column(d, "price") * Literal(d, 0.9),
lambda d: NumericColumn(d, "price") * Literal(d, 0.9),
)]]

total_value: ClassVar[Annotated[float, DerivedField(
lambda d: Column(d, "price") * Column(d, "quantity"),
lambda d: NumericColumn(d, "price") * NumericColumn(d, "quantity"),
)]]


Expand All @@ -796,11 +796,11 @@ class AsyncProduct(AsyncActiveRecord):
quantity: int

discounted_price: ClassVar[Annotated[float, DerivedField(
lambda d: Column(d, "price") * Literal(d, 0.9),
lambda d: NumericColumn(d, "price") * Literal(d, 0.9),
)]]

total_value: ClassVar[Annotated[float, DerivedField(
lambda d: Column(d, "price") * Column(d, "quantity"),
lambda d: NumericColumn(d, "price") * NumericColumn(d, "quantity"),
)]]


Expand All @@ -814,11 +814,11 @@ class ProductFormA(ActiveRecord):
quantity: int

discounted_price: ClassVar[DerivedField] = DerivedField(
lambda d: Column(d, "price") * Literal(d, 0.9),
lambda d: NumericColumn(d, "price") * Literal(d, 0.9),
)

total_value: ClassVar[DerivedField] = DerivedField(
lambda d: Column(d, "price") * Column(d, "quantity"),
lambda d: NumericColumn(d, "price") * NumericColumn(d, "quantity"),
)


Expand All @@ -831,11 +831,11 @@ class AsyncProductFormA(AsyncActiveRecord):
quantity: int

discounted_price: ClassVar[DerivedField] = DerivedField(
lambda d: Column(d, "price") * Literal(d, 0.9),
lambda d: NumericColumn(d, "price") * Literal(d, 0.9),
)

total_value: ClassVar[DerivedField] = DerivedField(
lambda d: Column(d, "price") * Column(d, "quantity"),
lambda d: NumericColumn(d, "price") * NumericColumn(d, "quantity"),
)


Expand Down Expand Up @@ -900,11 +900,11 @@ class ProductWithColumnAndAdapter(ActiveRecord):
quantity: int

discounted_price: ClassVar[Annotated[float, DerivedField(
lambda d: Column(d, "price") * Literal(d, 0.9),
lambda d: NumericColumn(d, "price") * Literal(d, 0.9),
), UseColumn("disc")]]

total_int: ClassVar[Annotated[int, DerivedField(
lambda d: Column(d, "price") * Column(d, "quantity"),
lambda d: NumericColumn(d, "price") * NumericColumn(d, "quantity"),
), UseAdapter(PriceToIntAdapter(), int)]]


Expand All @@ -917,10 +917,10 @@ class AsyncProductWithColumnAndAdapter(AsyncActiveRecord):
quantity: int

discounted_price: ClassVar[Annotated[float, DerivedField(
lambda d: Column(d, "price") * Literal(d, 0.9),
lambda d: NumericColumn(d, "price") * Literal(d, 0.9),
), UseColumn("disc")]]

total_int: ClassVar[Annotated[int, DerivedField(
lambda d: Column(d, "price") * Column(d, "quantity"),
lambda d: NumericColumn(d, "price") * NumericColumn(d, "quantity"),
), UseAdapter(PriceToIntAdapter(), int)]]

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
from typing import ClassVar, Optional
from uuid import UUID

from rhosocial.activerecord.backend.expression.core import Column
from rhosocial.activerecord.backend.expression.column_types import (
NumericColumn,
StringColumn,
)
from rhosocial.activerecord.backend.expression.statements.ddl_table import (
ColumnConstraintType,
ForeignKeyConstraint,
Expand Down Expand Up @@ -65,7 +68,7 @@ class SpecOrder(UUIDMixin, ActiveRecord):
str,
UseConstraint(
ColumnConstraintType.CHECK,
check_condition=lambda dialect: Column(dialect, "status").in_(
check_condition=lambda dialect: StringColumn(dialect, "status").in_(
["open", "paid", "shipped"]
),
name="ck_spec_orders_status",
Expand Down Expand Up @@ -96,7 +99,7 @@ class AsyncSpecOrder(UUIDMixin, AsyncActiveRecord):
str,
UseConstraint(
ColumnConstraintType.CHECK,
check_condition=lambda dialect: Column(dialect, "status").in_(
check_condition=lambda dialect: StringColumn(dialect, "status").in_(
["open", "paid", "shipped"]
),
name="ck_spec_orders_status",
Expand Down Expand Up @@ -132,7 +135,7 @@ class CapabilityPost(ActiveRecord):
UseGeneratedColumn(
lambda dialect: GeneratedColumnExpression(
dialect,
expression=Column(dialect, "views") * 2,
expression=NumericColumn(dialect, "views") * 2,
storage_type=GeneratedColumnType.STORED,
)
),
Expand Down Expand Up @@ -161,7 +164,7 @@ class AsyncCapabilityPost(AsyncActiveRecord):
UseGeneratedColumn(
lambda dialect: GeneratedColumnExpression(
dialect,
expression=Column(dialect, "views") * 2,
expression=NumericColumn(dialect, "views") * 2,
storage_type=GeneratedColumnType.STORED,
)
),
Expand Down
Loading
Loading