Skip to content

release(v0.1.0-rc.3): composite primary keys, DI kwargs auto-wiring, keyset pagination, and route sorting - #144

Merged
alialfostovar merged 36 commits into
masterfrom
release/v0.1.0-rc.3
Oct 21, 2026
Merged

alialfostovar merged 36 commits into
masterfrom
release/v0.1.0-rc.3

Conversation

@alialfostovar

Copy link
Copy Markdown
Contributor

Description

This release candidate (v0.1.0-rc.3) introduces critical architectural hardening, PostgreSQL partition pruning support, thread-safety guarantees, and robust routing mechanics to finalize framework stability ahead of the upcoming v1.0.0 release.

Summary

  • Constructor Auto-Wiring Resilience & Kwargs Mapping: Refactored IoCContainer._auto_wire to assemble dependencies using keyword mappings (**kwargs) instead of positional arguments, eliminating argument misplacement when defaults or KEYWORD_ONLY parameters are present. Added is_registered() across all lifecycle stores to safely skip unregistered parameters with declared defaults (e.g. timeout: int = 30), allowing Python defaults to apply cleanly.
  • Recursive Type Unwrapping & Cache Backward Compatibility: Implemented _unwrap_type to resolve underlying dependency contracts across Annotated, typing.Union, and Python 3.10+ types.UnionType (e.g., IService | None = None). Introduced _DependencySignature (a specialized list subclass exposing kwargs_map) to preserve backward compatibility for legacy list-based reflection assertions.
  • Strict Scope Boundary Enforcement: Hardened resolution lifecycle boundaries by raising an explicit DIException when attempting to resolve scoped dependencies or AsyncSession outside an active scope context (_current_scope_id is None). Aligned get_settings() in config.py to check is_registered prior to resolution, ensuring cold-start singleton fallback registration.
  • Native Composite Primary Keys & PostgreSQL Partition Pruning: Extended AbstractRepository and BaseRepository with pks, pk_names, and is_composite_pk reflection. Added _build_ids_condition producing tuple_(*self.pks).in_(...) expressions to enable native PostgreSQL partition pruning on partitioned tables. Implemented guarded fail-fast properties (pk, pk_name, cursor_field) that raise descriptive AttributeErrors when accessed on composite entities without explicit overrides. Updated single and batch CRUD operations (get_by_ids, update, update_multi, delete, delete_multi, restore, restore_multi) to support composite tuples, dicts, and model instances.
  • Dynamic Keyset Cursor Pagination & Search Alignment: Extended CursorParams with runtime cursor_field and order overrides. Implemented dynamic compound keyset condition evaluation ((col < val) OR (col == val AND pk < last_pk) for DESC), reset conflicting query order clauses (order_by(None)), and propagated SearchRequest.sort[0] from BaseRouter through repository search queries.
  • SQLAlchemy Date Type Coercion for asyncpg: Resolved driver-level DatatypeMismatchError exceptions against PostgreSQL by explicitly inspecting (col_type is date or isinstance(col.type, Date)), stripping ISO time fragments via val.split('T')[0], and parsing values into native datetime.date objects.
  • Mutual Exclusivity in Search Pagination: Added a Fail-Fast model validator in SearchRequest raising a validation error when both a keyset token (cursor) and offset page index (page > 1) are supplied simultaneously, returning a structured HTTP 422 envelope.
  • Route Specificity Sorting & Shadowing Prevention: Bound auto-sorting hooks (_bind_router_sorting_hooks) wrapping router.add_api_route and router.add_route in BaseRouter, ensuring subsequent route registrations on subclasses are automatically re-sorted to prevent dynamic parameterized paths (e.g. /{id:uuid}) from shadowing static endpoints (e.g. /summary). Added the register_custom_routes() subclass lifecycle hook and exposed a public sort_routes() utility.
  • Dedicated GET /_schema Introspection Endpoint: Replaced the ?schema=true query parameter interceptor in ZCoreAPIRoute with a dedicated RouteKey.SCHEMA endpoint (GET /_schema) supporting optional ?action=create|update|view|lookup query filters and optional user authentication.
  • Thread-Safe Background Cache Eviction: Guarded _ensure_eviction_task and close_cache() in zcore.cache.base with threading.Lock using the double-checked locking pattern, eliminating race conditions that previously spawned redundant background eviction tasks under concurrent multi-threaded execution.
  • Test Fixture State Preservation: Hardened DependencyOverride in testing/fixtures.py to record pre-existing dependency overrides during setUp() and restore them upon tearDown(), preventing cross-test dependency override pollution.
  • Scaffolding & Version Centralization: Corrected repository class name formatting in CLI scaffolding templates ({ModelName}Repository), statically exported __version__ = "0.1.0-rc.3", and restructured README.md with technical, neutral architecture and benchmark documentation.

Impact

Eliminates driver-level query failures on PostgreSQL partitioned tables and date columns, prevents race conditions during background cache task initialization, resolves route shadowing on custom router extensions, and ensures safe constructor dependency injection when default parameters are declared.

Fixes #128, Fixes #129, Fixes #130, Fixes #131, Fixes #132, Fixes #133

🤝 Acknowledgements

Special thanks to @dwaddle for diagnosing and contributing solutions for the IoC container kwargs signature mapping (#131) and cache background task concurrency race conditions.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactoring (architectural or code cleanup with no functional changes)
  • Documentation update

⚠️ Breaking Changes & Migration Guide

  1. Schema Introspection Migration: The query parameter ?schema=true has been deprecated and removed. Applications must now query the dedicated GET /<resource>/_schema endpoint (with optional ?action=create|update|view|lookup).
  2. Conflicting Search Pagination (422 Error): SearchRequest now strictly rejects simultaneous cursor and page > 1 payloads with an HTTP 422 error instead of silently dropping one parameter.
  3. Composite Primary Keys Access: Accessing .pk or .pk_name on a repository whose model uses composite primary keys now raises an AttributeError. Use .pks or .pk_names instead.
  4. Scoped DI Boundaries: Resolving AsyncSession or scoped classes outside of an active HTTP request or background_scope now raises DIException.

How Has This Been Tested?

  • Unit/Integration tests (hatch test)
    • Ran full test suite (529 tests passed in 19.46s) across Python 3.11, 3.12, and 3.13 with 0 warnings and 0 regressions.
    • Tested IoCContainer skipping unregistered defaults, injecting registered defaults, resolving Union / Optional types, and raising DIException outside active scopes.
    • Verified composite primary key CRUD lifecycle, guarded properties (pk, pk_name), and batch operations with tuple/dict keys.
    • Tested multi-page keyset cursor pagination with ASC/DESC dynamic sorting and Date column type coercion.
    • Verified mutual exclusivity rejection (HTTP 422) on SearchRequest(cursor=..., page=2).
    • Tested route specificity ordering avoiding path shadowing for subclassed router endpoints in __init__ and via register_custom_routes.
    • Tested action-specific, full-resource, and invalid-action schema introspection on GET /_schema.
    • Verified concurrent invocation of _ensure_eviction_task with ThreadPoolExecutor (20 workers) ensuring single-instance task creation.
  • Static type check passing (mypy src/)
    • Strict static type analysis passed with zero errors across all packages.
  • Code style and formatting checks (ruff check src/, ruff format --check src/).

Checklist:

  • My code follows the style guidelines and architectural rules of this project
  • I have performed a self-review of my own code
  • My changes pass strict static type analysis (mypy src/ generates no warnings)
  • I have commented my code, particularly in hard-to-understand areas
  • I have updated the documentation in zcore-docs if applicable
  • My changes generate no new runtime warnings
  • I have added tests that prove my fix is effective or that my feature works

dwaddle and others added 30 commits October 17, 2026 16:09
…fault values (#134)

Fixes #131

### Problem
`IoCContainer._auto_wire` collected all type-annotated constructor
parameters into `dependencies` without checking whether `param.default
is not inspect.Parameter.empty`. When a class constructor declared
parameters with default values for primitive or unregistered types
(e.g., `timeout: int = 30` or `client: Any = None`), `_auto_wire`
attempted to resolve those types through the container, raising
`TypeError` or unexpected resolution failures.

### Solution
- Add `IoCContainer._is_registered` to check whether a dependency type
is explicitly registered in `_singletons`, `_scoped_definitions`,
`_factories`, or active `_scoped_instances`.
- In `IoCContainer._auto_wire`, skip variadic parameters (`*args`,
`**kwargs`) and parameters that define a default value (`param.default
is not inspect.Parameter.empty`) when their type annotation is not
explicitly registered in the container, allowing Python to fall back to
the parameter's default value (consistent with
`background_task._resolve_injections`).
- Add unit tests in `tests/test_di.py` covering primitive/unregistered
defaults, mixed required dependencies + defaults, and explicitly
registered dependencies with defaults.

### Backward Compatibility
100% backward compatible; all required constructor dependencies and
explicitly registered optional dependencies continue to be injected
identically.
…ring

- Added types import and _unwrap_type to recursively unwrap Annotated, Optional, and Union types
- Added is_registered() public helper across all lifecycle stores
- Dependency signature cache now maps parameter names to types instead of positional list
- Auto-wiring constructs instances via keyword arguments (**resolved_kwargs)
- Removed special BaseSettings singleton caching in auto-wire
- Removed explicit AsyncSession checks in _auto_wire (handled via scoped definition check)
- background_task injection now uses container._unwrap_type
- Added tests for registered/unregistered Optional union injection
…gnature

- Added _DependencySignature list subclass that preserves list-like inspection while exposing kwargs_map
- is_registered() now also treats AsyncSession and active scoped instances as registered
- Auto-wire handles both _DependencySignature (kwargs) and legacy list signatures (positional args)
- get_settings now checks container.is_registered before resolving, raising DIException otherwise
- Tests updated for Optional union registration behavior
…and scope boundary enforcement (#135)

## Description

This PR resolves an issue in `IoCContainer._auto_wire` where constructor
parameters were previously collected into dependencies purely based on
type annotations, without verifying whether a parameter defined a
default value (`param.default is not inspect.Parameter.empty`). This
caused the container to attempt resolving primitive types (such as
`int`, `str`) or unregistered optional dependencies, triggering
instantiation failures (`TypeError: int() takes at most 2 arguments (0
given)`).

Key architectural enhancements in this PR:
1. **Default Parameter Handling:** Skips unregistered constructor
parameters that define default values, allowing Python to safely fall
back to the declared default. If a dependency is explicitly registered
in the container, it takes precedence and is injected.
2. **Keyword-Based Auto-Wiring (`**kwargs`):** Switched constructor
auto-wiring from positional arguments (`*args`) to keyword mapping
(`**kwargs`). This completely eliminates parameter misplacement when
optional/default parameters precede registered ones or when keyword-only
arguments (`KEYWORD_ONLY`) are utilized.
3. **Cache Backward Compatibility (`_DependencySignature`):**
Implemented a specialized `list` subclass (`_DependencySignature`) to
retain 100% backward compatibility for test assertions expecting a list
(`assert cache[cls] == [...]` or `== []`), while internally maintaining
the parameter-to-dependency mapping (`kwargs_map`).
4. **Type Unwrapping (`_unwrap_type`):** Added recursive unwrapping for
`Annotated`, `typing.Union`, and Python 3.10+ `types.UnionType` (e.g.
`IService | None = None`) to correctly evaluate underlying interface
registrations.
5. **Scope Boundary Enforcement:** Enforces strict lifecycle boundaries
by raising `DIException` when attempting to resolve scoped dependencies
or `AsyncSession` outside an active scope context (`_current_scope_id is
None`).
6. **Settings Fallback Alignment:** Updated `get_settings()` in
`config.py` to check `container.is_registered()` prior to resolution,
ensuring singleton registration upon cold starts.

Fixes #131

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [x] Refactoring (architectural or code cleanup with no functional
changes)
- [ ] Documentation update

## How Has This Been Tested?

The fix was verified against the full test suite using `hatch test`:
- **Unit/Integration Tests:** Executed all 521 unit and integration
tests across the framework (`tests/test_di.py`, `tests/test_config.py`,
etc.).
- **Specific Test Cases:**
- `test_auto_wire_skips_unregistered_default_parameters`: Validates
primitive defaults (`timeout: int = 30`) remain untouched and cache
reflects `[]`.
- `test_auto_wire_resolves_required_and_skips_unregistered_defaults`:
Ensures required dependencies are wired while defaults are skipped.
- `test_auto_wire_injects_registered_dependency_even_with_default`:
Confirms container registrations take precedence over default parameter
values.
- `test_auto_wire_injects_registered_dependency_with_optional_union`:
Verifies `Union` / `Optional` type unwrapping.
- `test_resolve_scoped_outside_scope_raises_di_exception`: Asserts
`DIException` is raised when resolving scoped instances outside active
scopes.
- `test_settings_get_settings_fallback`: Confirms settings fallback
auto-registers as a singleton.

- [x] Unit/Integration tests (`hatch test`)
- [x] Static type check passing (`mypy src/`)

## Checklist:

- [x] My code follows the style guidelines and architectural rules of
this project
- [x] I have performed a self-review of my own code
- [x] My changes pass strict static type analysis (`mypy src/` generates
no warnings)
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have updated the documentation in `zcore-docs` if applicable
- [x] My changes generate no new runtime warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- Added cursor_field and order fields to CursorParams for per-request override
- CursorPagination accepts optional order with 'desc' fallback
- _encode_cursor now serializes date, UUID, and Decimal types to strings
- Added type coercion for date, UUID, int, and float values in cursor boundary
- Clears existing ORDER BY before applying cursor ordering
- Repository get_list and search now derive cursor_field/order from pagination or search sort
- BaseRouter propagates search sort field/order into CursorParams
…d tests

- Consolidated UUID/Decimal cursor serialization into single isinstance branch
- Added test_repo_search_with_cursor_pagination_and_sort for asc/desc sort via SearchRequest
- Added MockCursorSearchService and test_router_search_with_cursor_pagination_and_sort
- Verified cursor propagates search sort field/order through BaseRouter
…ic search sort (#136)

## Description

Resolves a conflict between `CursorPagination` and `SearchRequest`
sorting criteria. Previously, `SearchEngine.build_base_query` applied
the client-specified `ORDER BY` clause while `CursorPagination.paginate`
fell back strictly to filtering and sorting by the primary key (`id`).
This caused duplicated `ORDER BY` clauses, corrupted compound cursors,
and invalid `WHERE id < :val` boundary conditions.

### Key Changes:
- **`zcore.db.pagination`:**
- Extended `CursorParams` with optional runtime `cursor_field` and
`order` attributes.
- Added support for serializing `date`, `UUID`, and `Decimal` values in
`_encode_cursor`.
- Added type coercion for `date`, `uuid.UUID`, `int`, and `float` during
cursor coordinate parsing.
- Reset existing query ordering (`query = query.order_by(None)`) before
applying synchronized keyset `ORDER BY` clauses.
- Implemented dynamic compound sorting: `(col < val) OR (col == val AND
pk < last_pk)` for `DESC` and `(col > val) OR (col == val AND pk >
last_pk)` for `ASC`.
- **`zcore.db.repository`:**
- Updated `ReadRepositoryMixin.get_list` to respect runtime
`cursor_field` and `order` on `CursorParams`.
- Updated `SearchRepositoryMixin.search` to automatically extract the
primary sort field and order from `SearchRequest.sort` and configure
`CursorPagination` accordingly.
- **`zcore.web.base_router`:**
- Updated `search_endpoint` and `lookup_endpoint` to propagate
`search_in.sort[0]` field and order into `CursorParams`.
- **Tests:**
- Added `test_repo_search_with_cursor_pagination_and_sort` to verify
ASC/DESC multi-page keyset pagination with non-PK columns.
- Added `test_router_search_with_cursor_pagination_and_sort` to verify
end-to-end HTTP payload propagation through `BaseRouter`.

Fixes # (issue number)

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Refactoring (architectural or code cleanup with no functional
changes)
- [ ] Documentation update

## How Has This Been Tested?

- [x] Unit/Integration tests (`hatch test`)
-
`tests/test_repository.py::test_repo_search_with_cursor_pagination_and_sort[desc]`
passed.
-
`tests/test_repository.py::test_repo_search_with_cursor_pagination_and_sort[asc]`
passed.
-
`tests/test_web.py::test_router_search_with_cursor_pagination_and_sort`
passed.
- Verified no record collision or duplication occurs between successive
cursor pages.
- [x] Static type check passing (`mypy src/`)

## Checklist:

- [x] My code follows the style guidelines and architectural rules of
this project
- [x] I have performed a self-review of my own code
- [x] My changes pass strict static type analysis (`mypy src/` generates
no warnings)
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have updated the documentation in `zcore-docs` if applicable
- [x] My changes generate no new runtime warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- Added Date import from sqlalchemy.sql.sqltypes
- Date coercion now triggers on isinstance(col.type, Date), not only python_type
- Strips time component via val.split('T')[0] before date.fromisoformat
- Added test_repo_cursor_pagination_with_date_column_coercion with DateCursorModel
- Consolidated Date import into main sqlalchemy import in test_repository
- Moved datetime.date import to top of imports
- Reformatted pagination imports to multi-line in test_web
## Description

Resolves a driver-level type mismatch error
(`asyncpg.exceptions.DatatypeMismatchError: operator does not exist:
date < character varying`) when using `CursorPagination` over SQLAlchemy
`Date` columns against strictly-typed databases like PostgreSQL.

### Motivation & Context:
In SQLAlchemy, the `Date` type does not inherit from `DateTime`.
Previously, `CursorPagination.paginate` only verified
`isinstance(col.type, DateTime)`. As a result, cursor values for `Date`
fields remained unparsed string primitives (`str`), which drivers like
`asyncpg` bind as `varchar`/`text`, causing PostgreSQL query execution
failures.

### Key Changes:
- **`src/zcore/db/pagination.py`:**
  - Imported `Date` from `sqlalchemy.sql.sqltypes`.
- Added explicit type inspection for `(col_type is date or
isinstance(col.type, Date))`.
- Stripped optional time fragments via `val.split("T")[0]` before
converting to `datetime.date` using `date.fromisoformat(...)`.
- **`tests/test_repository.py`:**
- Added `DateCursorModel`, `DateCursorCreateSchema`, and
`DateCursorRepository`.
- Added regression test
`test_repo_cursor_pagination_with_date_column_coercion` asserting
multi-page keyset pagination with actual `datetime.date` boundaries.
- **`tests/test_web.py`:**
  - Cleaned up and formatted multi-line pagination imports.

Fixes #128

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Refactoring (architectural or code cleanup with no functional
changes)
- [ ] Documentation update

## How Has This Been Tested?

- [x] Unit/Integration tests (`hatch test`)
- Ran
`tests/test_repository.py::test_repo_cursor_pagination_with_date_column_coercion`
verifying that parsed cursor values are instances of `datetime.date` and
sequential pages maintain proper ordering without data collisions.
- Ran the full test suite (`test_repository.py`, `test_search.py`,
`test_web.py`) with 0 regressions.
- [x] Static type check passing (`mypy src/`)

## Checklist:

- [x] My code follows the style guidelines and architectural rules of
this project
- [x] I have performed a self-review of my own code
- [x] My changes pass strict static type analysis (`mypy src/` generates
no warnings)
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have updated the documentation in `zcore-docs` if applicable
- [x] My changes generate no new runtime warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- Replaced pk/pk_name attributes with pks/pk_names tuples and is_composite_pk flag
- Added pk and pk_name convenience properties that raise on composite models
- Added _build_pk_condition for model instance, dict, tuple/list, or scalar targets
- Added _build_ids_condition supporting scalar in_ and composite tuple_.in_
- Updated get_by_ids, update, update_multi, delete, delete_multi, restore, restore_multi to use composite-aware conditions
- BaseRepository __init__ now inspects all primary key columns
- Added CompositePartitionModel, CompositeCreateSchema, CompositeUpdateSchema, CompositeRepository test fixtures
- Added tests for guarded properties and full composite CRUD cycle
- Added strict=False to zip calls in composite PK condition and update_multi
- Collapsed multi-line test calls and parametrize decorators to single lines
- Added trailing newline at EOF in repository.py
- Replaced pk/pk_name attributes with pks/pk_names tuples and is_composite_pk flag
- Added pk and pk_name convenience properties that raise on composite models
- Added _build_pk_condition for model instance, dict, tuple/list, or scalar targets
- Added _build_ids_condition supporting scalar in_ and composite tuple_.in_
- Updated get_by_ids, update, update_multi, delete, delete_multi, restore, restore_multi to use composite-aware conditions
- BaseRepository __init__ now inspects all primary key columns
- Added CompositePartitionModel, CompositeCreateSchema, CompositeUpdateSchema, CompositeRepository test fixtures
- Added tests for guarded properties and full composite CRUD cycle
- Added strict=False to zip calls in composite PK condition and update_multi
- Collapsed multi-line test calls and parametrize decorators to single lines
- Added trailing newline at EOF in repository.py
…b.com:Baseryn/zcore into feat/repository-composite-primary-key-support
#138)

## Description

Adds native support for composite primary keys (e.g., `(id,
operation_date)`) in `BaseRepository`. Previously,
`BaseRepository.__init__` hardcoded single-column PK resolution
(`primary_key[0]`), ignoring subsequent PK columns. On composite models
and PostgreSQL partitioned tables (e.g., Range/List partitioning), this
broke query and mutation methods (`get`, `get_by_ids`, `update`,
`delete`, etc.) and disabled PostgreSQL partition pruning, triggering
full scans across all partitions.

### Key Changes:
- **`src/zcore/db/repository.py`:**
- Replaced hardcoded single-column resolution in `__init__` with
`self.pks = tuple(mapper.primary_key)` and `self.pk_names =
tuple(col.name for col in self.pks)`.
- Added automatic zero-configuration reflection flag
`self.is_composite_pk = len(self.pks) > 1`.
- Implemented guarded convenience properties (`@property def pk` and
`@property def pk_name`) adhering to the Fail-Fast design pattern:
returns the primary column for single-key models, and raises an explicit
`AttributeError` on composite models to prevent silent partial-key
queries.
- Implemented `_build_pk_condition(target)` to resolve equality filters
whether the target is an ORM model instance, a dictionary, a tuple/list,
or a scalar.
- Implemented `_build_ids_condition(ids)` generating
`tuple_(*self.pks).in_(...)` expressions for composite entities,
generating standard `(col1, col2) IN ((val1, val2), ...)` clauses that
allow PostgreSQL to perform partition pruning.
- Updated `exist`, `get`, `get_by_ids`, `update`, `update_multi`,
`delete`, `delete_multi`, `restore`, and `restore_multi` to operate via
composite-aware conditions.
- **`tests/test_repository.py`:**
- Added `CompositePartitionModel`, `CompositeCreateSchema`,
`CompositeUpdateSchema`, and `CompositeRepository`.
- Added `test_repo_composite_guarded_properties` testing Fail-Fast
behavior on composite models and backward compatibility on single-key
models.
- Added `test_repo_composite_primary_key_crud` verifying full CRUD and
batch operations across tuples, dicts, and model instances.
- **`tests/test_web.py`:**
  - Applied formatting cleanups.

Fixes #129

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Refactoring (architectural or code cleanup with no functional
changes)
- [ ] Documentation update

## How Has This Been Tested?

- [x] Unit/Integration tests (`hatch test`)
- `tests/test_repository.py::test_repo_composite_guarded_properties`
passed.
- `tests/test_repository.py::test_repo_composite_primary_key_crud`
passed (tested single-record and multi-record `get`, `update`,
`update_multi`, `delete`, `delete_multi`, and `exist`).
  - Full test suite passing with 0 regressions.
- [x] Static type check passing (`mypy src/`)

## Checklist:

- [x] My code follows the style guidelines and architectural rules of
this project
- [x] I have performed a self-review of my own code
- [x] My changes pass strict static type analysis (`mypy src/` generates
no warnings)
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have updated the documentation in `zcore-docs` if applicable
- [x] My changes generate no new runtime warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- Added _bind_router_sorting_hooks to wrap add_api_route and add_route, triggering _sort_routes after each registration
- Added public sort_routes() wrapper for manual route sorting
- Added register_custom_routes() hook called at end of _register_routes for subclass extensions
- Updated __init__ docstring to mention auto-sorting hook binding
…uters

- Removed duplicated DateCursorModel fixtures from test_repository
- Added test_subclassed_router_custom_route_in_init_avoids_shadowing verifying post-super() route additions are re-sorted
- Added test_subclassed_router_hook_register_custom_routes verifying the register_custom_routes hook
…shadowing (#139)

## Description

Resolves route shadowing issues where custom endpoints registered in
`BaseRouter` subclasses were shadowed by dynamic parameterized paths
(e.g., `/{id:uuid}` shadowing static paths like `/summary` or
`/reports`), causing Starlette to match the dynamic route first and fail
with `422 RequestValidationError`.

### Motivation & Context:
Previously, `_sort_routes()` was executed strictly once during
`_register_routes()` inside `BaseRouter.__init__()`. Any custom endpoint
registered by child routers after `super().__init__()` was appended to
the tail of `self.router.routes` without participating in specificity
sorting.

### Key Changes:
- **`src/zcore/web/base_router.py`:**
- Added `_bind_router_sorting_hooks()` which wraps
`self.router.add_api_route` and `self.router.add_route` after base
routes are registered, ensuring that whenever any subsequent route is
registered, routes are automatically re-sorted based on path
specificity.
- Added `register_custom_routes()` hook method called at the end of
`_register_routes()` to allow clean subclass extensions without
overriding `__init__`.
- Added public `sort_routes()` method exposing on-demand route sorting.
- **`tests/test_repository.py`:**
- Removed duplicate `DateCursorModel` fixture definitions, eliminating
the SQLAlchemy declarative `SAWarning`.
- **`tests/test_web.py`:**
- Added `test_subclassed_router_custom_route_in_init_avoids_shadowing`
verifying that routes added in `__init__` after `super().__init__()` are
sorted before `/{id:uuid}` and return 200 OK.
- Added `test_subclassed_router_hook_register_custom_routes` verifying
the `register_custom_routes` lifecycle hook.

Fixes #130

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Refactoring (architectural or code cleanup with no functional
changes)
- [ ] Documentation update

## How Has This Been Tested?

- [x] Unit/Integration tests (`hatch test`)
-
`tests/test_web.py::test_subclassed_router_custom_route_in_init_avoids_shadowing`
passed.
-
`tests/test_web.py::test_subclassed_router_hook_register_custom_routes`
passed.
- Full test suite passed cleanly with 0 warnings (`529 passed in
19.46s`).
- [x] Static type check passing (`mypy src/`)

## Checklist:

- [x] My code follows the style guidelines and architectural rules of
this project
- [x] I have performed a self-review of my own code
- [x] My changes pass strict static type analysis (`mypy src/` generates
no warnings)
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have updated the documentation in `zcore-docs` if applicable
- [x] My changes generate no new runtime warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- Added model_validator check in SearchRequest raising ValueError when cursor is non-empty and page > 1
- Added parametrized test_search_request_pagination_mutual_exclusivity covering empty, whitespace, and None cursors
- Added test_search_endpoint_rejects_conflicting_pagination_params verifying 422 response via registered handlers
#140)

## Description

Resolves an ambiguous API contract where `SearchRequest` allowed both
offset pagination (`page > 1`) and keyset pagination (`cursor: str`) to
be sent simultaneously. Previously, `BaseRouter.search_endpoint`
silently discarded one parameter depending on the configured pagination
class, causing hard-to-detect frontend state discrepancies.

### Motivation & Context:
Enforces the **Fail-Fast** design principle at the schema validation
boundary. When a client explicitly specifies both a cursor token and a
page index greater than 1, `SearchRequest` now raises a validation error
instead of silently dropping one parameter. Standard requests using
default `page=1` with a `cursor`, or offset requests without a `cursor`,
remain fully supported without breaking changes.

### Key Changes:
- **`src/zcore/db/search.py`:**
- Added a validation rule in `SearchRequest._validate_and_bound_size`
raising a `ValueError` when `self.cursor` is non-empty/non-whitespace
and `self.page > 1`.
- **`tests/test_search.py`:**
- Added parameterized unit test
`test_search_request_pagination_mutual_exclusivity` verifying
combinations of `None`, empty string, whitespace, and valid tokens
against `page=1` and `page > 1`.
- **`tests/test_web.py`:**
- Added HTTP endpoint integration test
`test_search_endpoint_rejects_conflicting_pagination_params` ensuring
FastAPI/ZCore exception handlers return a standard 422
`RequestValidationError` envelope.

Fixes #132

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Refactoring (architectural or code cleanup with no functional
changes)
- [ ] Documentation update

## How Has This Been Tested?

- [x] Unit/Integration tests (`hatch test`)
-
`tests/test_search.py::test_search_request_pagination_mutual_exclusivity`
passed for all parameter combinations.
-
`tests/test_web.py::test_search_endpoint_rejects_conflicting_pagination_params`
passed with HTTP 422 response assertions.
  - Full test suite passed cleanly with 0 warnings.
- [x] Static type check passing (`mypy src/`)

## Checklist:

- [x] My code follows the style guidelines and architectural rules of
this project
- [x] I have performed a self-review of my own code
- [x] My changes pass strict static type analysis (`mypy src/` generates
no warnings)
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have updated the documentation in `zcore-docs` if applicable
- [x] My changes generate no new runtime warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- Added module-level threading.Lock to guard _ensure_eviction_task and close_cache
- Double-checked locking ensures only one eviction task is created under concurrent calls
- close_cache now extracts and nullifies _eviction_task inside the lock before awaiting cancellation
- Added test_concurrent_ensure_eviction_task_single_instance with ThreadPoolExecutor
## Description

Resolves a race condition in `zcore.cache.base._ensure_eviction_task`
where concurrent cache operations across multiple thread contexts (e.g.,
AnyIO worker threads, background workers, or parallel test harness
executions) could evaluate `_eviction_task is None` simultaneously and
spawn redundant background eviction loops (`_start_eviction_loop`). This
caused duplicate memory sweeps and repeated shutdown logs (`Cache memory
eviction loop stopped`).

### Motivation & Context:
Guarantees strict single-instance lifecycle management for background
cache workers. By applying the **Double-Checked Locking** pattern with
`threading.Lock()`, task instantiation is thread-safe while preserving
high-performance throughput on subsequent calls without unneeded
synchronization overhead.

### Key Changes:
- **`src/zcore/cache/base.py`:**
  - Added module-level `_lock = threading.Lock()`.
- Implemented double-checked locking in `_ensure_eviction_task` ensuring
only one background asyncio task is spawned under concurrent
multi-threaded execution.
- Updated `close_cache()` to atomically extract and nullify
`_eviction_task` within the lock before awaiting its cancellation.
- **`tests/test_cache.py`:**
- Added `test_concurrent_ensure_eviction_task_single_instance` using
`ThreadPoolExecutor` (10 workers, 20 concurrent invocations) verifying
that only a single background task is created and maintained.

Fixes #133

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Refactoring (architectural or code cleanup with no functional
changes)
- [ ] Documentation update

## How Has This Been Tested?

- [x] Unit/Integration tests (`hatch test`)
-
`tests/test_cache.py::test_concurrent_ensure_eviction_task_single_instance`
passed.
  - Ran full test suite with 0 regressions and 0 warnings.
- [x] Static type check passing (`mypy src/`)

## Checklist:

- [x] My code follows the style guidelines and architectural rules of
this project
- [x] I have performed a self-review of my own code
- [x] My changes pass strict static type analysis (`mypy src/` generates
no warnings)
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have updated the documentation in `zcore-docs` if applicable
- [x] My changes generate no new runtime warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- Added __version__ resolution via importlib.metadata with rc.3 fallback
- Exported __version__ from zcore package
- CLI --version and banner now use __version__ dynamically
- main.py template now imports and returns __version__
- Fixed repository template class name to use {ModelNameRepository}
- Updated README version references to rc.3
…ursor field

- Made cursor_field a property/setter backed by _cursor_field with None default
- cursor_field getter raises AttributeError on composite models without explicit override
- _build_pk_condition now validates dict completeness, tuple/list length, and rejects scalars for composite models
- _build_ids_condition validates composite dict completeness, tuple length, and rejects scalar items
- update_multi validates composite dict/tuple keys with descriptive error messages
- Removed redundant cursor_field class annotation
- Removed importlib.metadata lookup; __version__ is now a static '0.1.0-rc.3' string
- Fixed repository template class name from {ModelNameRepository} to {ModelName}Repository
- Updated CLI version test assertions to rc.3
- Collapsed multi-line missing-key list comprehension in update_multi
…ite key validation (#142)

## Description

Prepares and packages the framework for the **`v0.1.0-rc.3`** release
candidate. Centralizes version definitions across CLI utilities and
scaffold templates, fixes repository scaffolding syntax, and hardens
`BaseRepository` with strict validation rules and guarded properties for
composite primary key models.

### Key Changes:
- **Version Centralization & Release Bump (`0.1.0-rc.3`):**
- Defined and exported static `__version__ = "0.1.0-rc.3"` in
`src/zcore/__init__.py`.
- Replaced hardcoded version strings in CLI banner (`ui.py`), CLI
`--version` output (`cli/__init__.py`), scaffolded `main.py` template
(`templates.py`), and `README.md`.
  - Updated CLI version test assertions in `tests/test_cli.py`.
- **Repository Scaffolding Template Fix:**
- Corrected repository class name generation in
`src/zcore/cli/templates.py` from `{ModelNameRepository}` to
`{ModelName}Repository`.
- **Repository Composite Key Hardening & Guarded Properties:**
- Converted `cursor_field` into a guarded property/setter backed by
`_cursor_field = None`. Accessing `cursor_field` on composite primary
key models without an explicit field override or request-level sort
criteria raises a descriptive `AttributeError` (Fail-Fast pattern).
- Enforced strict validation in `_build_pk_condition`: verifies
dictionary completeness (supporting both `col.key` and `col.name`),
validates exact tuple length matching (`strict=True`), and rejects
scalar inputs for composite entities with clear error messages.
- Hardened `_build_ids_condition`: validates composite dictionaries and
tuples, preventing invalid single-item tuple fallbacks and raising
`ValueError` on scalar items.
- Hardened `update_multi`: validates composite key dictionaries/tuples
and ensures complete primary key payloads are assembled for bulk DBAPI
operations.

Fixes # (issue number or release milestone)

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [x] Refactoring (architectural or code cleanup with no functional
changes)
- [x] Documentation update

## How Has This Been Tested?

- [x] Unit/Integration tests (`hatch test`)
- `tests/test_cli.py::test_cli_version_output_rc2` (updated for rc.3)
passed.
- `tests/test_repository.py` composite CRUD and guarded property tests
passed.
- Ran the full test suite (`529 passed in 19.46s`) with zero warnings
and zero regressions.
- [x] Static type check passing (`mypy src/`)

## Checklist:

- [x] My code follows the style guidelines and architectural rules of
this project
- [x] I have performed a self-review of my own code
- [x] My changes pass strict static type analysis (`mypy src/` generates
no warnings)
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have updated the documentation in `zcore-docs` if applicable
- [x] My changes generate no new runtime warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- Rewrote overview with concrete architectural challenges and less marketing language
- Rebranded ZShop as a reference implementation with factual capability descriptions
- Reworked comparison table with neutral phrasing and added composite PK details
- Added new details section on composite primary keys and keyset pagination
- Renamed 'Core Architectural Patterns' to 'Core Architectural Components' with Problem/Solution structure
- Simplified quick start endpoint list and CLI command reference
- Reframed community testing section to focus on technical feedback
- Updated footer attribution
alialfostovar and others added 5 commits October 19, 2026 19:55
…yOverride fixture

- Track whether stub had an override and store its original value in setUp
- tearDown restores the original override instead of unconditionally deleting
…ion endpoint

- Added RouteKey.SCHEMA and schema_path attribute (default '/_schema')
- Added schema_endpoint resolving create/update/view/lookup schemas with optional ?action= filter
- Schema endpoint uses optional user stub for authentication
- Removed expose_schemas, _get_openapi_extra, find_input_schema, find_output_schema, and inline schema generation from ZCoreAPIRoute
- All endpoints now declare explicit openapi_extra={'action': ...}
- get_route_action returns empty string for unmapped route keys
- Added tests for action-specific, full-resource, and invalid-action schema introspection
- Added test for RouteKey.SCHEMA exclusion and route sorting
- Updated README to document /_schema endpoint
…g.Lock (#133) (#143)

### Summary
This PR addresses #133 by guarding background memory eviction task
creation in `zcore.cache.base` with a `threading.Lock` double-checked
initialization pattern.

### Changes
1. **Thread-Safe Eviction Task Scheduling**: Added `_eviction_lock =
threading.Lock()` in `zcore.cache.base`. In `_ensure_eviction_task()`,
the loop check and `loop.create_task(_start_eviction_loop(...))`
assignment are guarded via `with _eviction_lock:` with double-checked
verification.
2. **Synchronized Shutdown in `close_cache()`**: Wrapped
`_eviction_task` retrieval and clearing inside `with _eviction_lock:` to
prevent concurrent threads from scheduling a new task while shutdown is
in flight.
3. **Regression Tests**: Added
`test_ensure_eviction_task_concurrent_thread_safety` in
`tests/test_cache.py` exercising concurrent invocation across 20 threads
to guarantee exactly one eviction loop task is spawned.

### Verification
- `uv run ruff format --check`: Passed cleanly.
- `uv run ruff check`: Passed cleanly.
- `uv run --all-extras pytest`: 516/516 passed.

Closes #133.
- Collapsed loop.create_task call to a single line
- Removed unused ThreadPoolExecutor import from test_cache
- Added trailing newlines at EOF
@alialfostovar
alialfostovar merged commit 69eea8a into master Oct 21, 2026
14 checks passed
@alialfostovar
alialfostovar deleted the release/v0.1.0-rc.3 branch October 21, 2026 11:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment