sqlmodel-ext 0.5.0__tar.gz → 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/.github/workflows/ci.yml +2 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/CHANGELOG.md +90 -8
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/PKG-INFO +7 -6
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/README.md +6 -5
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/README_zh.md +6 -5
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/ai-rules/AGENTS.md +4 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/explanation/cached-table.md +3 -3
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/explanation/metaclass.md +7 -7
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/explanation/polymorphic-internals.md +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/explanation/transactional-cache-transparency.md +5 -6
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/cache-queries.md +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/extra-mixins.md +3 -2
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/integrate-with-fastapi.md +4 -11
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/keyset-pagination.md +3 -3
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/paginate-a-list-endpoint.md +15 -20
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/reference/crud-methods.md +4 -4
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/reference/decorators.md +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/reference/field-types.md +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/reference/index.md +2 -2
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/reference/mixins.md +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/reference/pagination-types.md +26 -5
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/tutorials/02-building-a-blog-api.md +3 -3
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/explanation/cached-table.md +3 -3
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/explanation/metaclass.md +7 -7
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/explanation/polymorphic-internals.md +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/explanation/transactional-cache-transparency.md +5 -6
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/cache-queries.md +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/extra-mixins.md +3 -2
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/integrate-with-fastapi.md +4 -11
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/keyset-pagination.md +3 -3
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/paginate-a-list-endpoint.md +15 -20
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/reference/crud-methods.md +4 -4
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/reference/decorators.md +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/reference/field-types.md +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/reference/index.md +2 -2
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/reference/mixins.md +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/reference/pagination-types.md +26 -5
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/tutorials/02-building-a-blog-api.md +3 -3
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/pyproject.toml +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/pyrightconfig.json +5 -5
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/__init__.py +4 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/_compat.py +4 -2
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/_exceptions.py +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/_sa_type.py +20 -8
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/_type_unwrap.py +10 -2
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/base.py +245 -86
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/check_derived.py +8 -8
- sqlmodel_ext-0.5.1/src/sqlmodel_ext/dependencies.py +178 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/field_types/__init__.py +27 -39
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/field_types/_internal/path.py +22 -10
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +27 -13
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +4 -5
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +40 -24
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/field_types/mixins/module_name_mixin.py +2 -2
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/field_types/url.py +6 -6
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/mixins/cached_table.py +239 -266
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/mixins/migration_cache_invalidation.py +6 -6
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/mixins/polymorphic.py +36 -23
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/mixins/relation_preload.py +16 -16
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/mixins/resource_quota.py +9 -6
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/mixins/table.py +44 -28
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/mixins/trgm_searchable.py +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/pagination.py +72 -39
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/relation_load_checker.py +21 -15
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/session.py +60 -33
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/unset.py +1 -3
- sqlmodel_ext-0.5.1/tests/test_cached_commit_paths.py +265 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_enhanced_session.py +6 -3
- sqlmodel_ext-0.5.1/tests/test_final_051_rereview.py +52 -0
- sqlmodel_ext-0.5.1/tests/test_final_051_review.py +262 -0
- sqlmodel_ext-0.5.1/tests/test_final_051_review2.py +72 -0
- sqlmodel_ext-0.5.1/tests/test_inherited_column_attributes.py +666 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_pagination.py +23 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_pg_dialect_types.py +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_polymorphic_jti.py +1 -1
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_polymorphic_sti_query.py +1 -1
- sqlmodel_ext-0.5.1/tests/test_query_dependency.py +326 -0
- sqlmodel_ext-0.5.1/tests/test_query_dependency_metadata.py +154 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_smoke_imports.py +2 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/.gitignore +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/LICENSE +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/ai-rules/CLAUDE.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/ai-rules/README.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/.vitepress/config.mts +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/.vitepress/en.mts +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/.vitepress/theme/index.ts +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/.vitepress/theme/style/custom.css +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/.vitepress/zh.mts +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/explanation/cascade-delete-semantics.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/explanation/crud-pipeline.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/explanation/index.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/explanation/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/explanation/prerequisites.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/explanation/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/explanation/relation-preload.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/explanation/single-source-of-truth.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/explanation/unset-three-state.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/guide/cached-table.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/guide/crud.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/guide/field-types.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/guide/index.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/guide/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/guide/pagination.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/guide/polymorphic.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/guide/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/guide/relation-preload.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/aggregate-queries.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/check-partial-dtos.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/configure-cascade-delete.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/define-jti-models.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/define-sti-models.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/enforce-locking-and-isolation.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/handle-concurrent-updates.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/handle-referenced-deletes.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/index.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/migrate-to-0-5.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/prevent-missing-greenlet.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/release-connection-during-long-io.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/type-check-with-basedpyright.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/use-with-ai-assistants.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/how-to/write-patch-endpoints.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/index.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/internals/cached-table.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/internals/crud.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/internals/index.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/internals/metaclass.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/internals/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/internals/polymorphic.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/internals/prerequisites.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/internals/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/internals/relation-preload.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/reference/base-classes.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/tutorials/01-getting-started.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/tutorials/03-adding-redis-cache.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/tutorials/index.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/explanation/cascade-delete-semantics.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/explanation/crud-pipeline.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/explanation/index.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/explanation/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/explanation/prerequisites.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/explanation/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/explanation/relation-preload.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/explanation/single-source-of-truth.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/explanation/unset-three-state.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/guide/cached-table.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/guide/crud.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/guide/field-types.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/guide/index.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/guide/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/guide/pagination.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/guide/polymorphic.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/guide/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/guide/relation-preload.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/aggregate-queries.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/check-partial-dtos.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/configure-cascade-delete.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/define-jti-models.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/define-sti-models.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/enforce-locking-and-isolation.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/handle-concurrent-updates.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/handle-referenced-deletes.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/index.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/migrate-to-0-5.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/prevent-missing-greenlet.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/release-connection-during-long-io.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/type-check-with-basedpyright.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/use-with-ai-assistants.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/how-to/write-patch-endpoints.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/index.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/internals/cached-table.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/internals/crud.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/internals/index.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/internals/metaclass.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/internals/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/internals/polymorphic.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/internals/prerequisites.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/internals/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/internals/relation-preload.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/reference/base-classes.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/tutorials/01-getting-started.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/tutorials/03-adding-redis-cache.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/tutorials/index.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/01_unset_patch.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/02_single_source_types.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/03_keyset_pagination.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/04_locking_and_isolation.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/05_aggregates.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/06_referenced_delete.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/07_transactional_cache.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/08_optimistic_lock.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/09_relation_preload_bulk.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/10_typed_select.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/11_type_errors_caught_by_basedpyright.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/README.md +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/check_derived_demo/pyrightconfig.json +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/check_derived_demo/shop/__init__.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/check_derived_demo/shop/handlers.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/check_derived_demo/shop/models.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/examples/consumer_integration/run.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/package.json +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/pnpm-lock.yaml +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/_utils.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/constants.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/derived_decls.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/field_types/_internal/__init__.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/field_types/_ssrf.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/field_types/dialects/__init__.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/field_types/dialects/postgresql/__init__.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/field_types/ip_address.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/field_types/mixins/__init__.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/mixins/__init__.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/mixins/_uuid.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/mixins/constants.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/mixins/exceptions.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/mixins/info_response.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/mixins/mixin_table_scan.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/mixins/optimistic_lock.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/py.typed +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/src/sqlmodel_ext/select.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/__init__.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/_models.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/conftest.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_annotated_default_factory_inheritance.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_array_explicit_field_sa_type.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_basic_crud.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_cached_table.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_check_derived.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_crud_extended.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_decimal_types.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_deferred_index.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_derived_decls.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_examples.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_field_types_aliases_extra.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_field_types_jsonb_limits.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_field_types_misc.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_field_types_str_num.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_field_types_url_ssrf.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_final_check_derived_review2.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_final_check_derived_temp_root.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_final_nested_duplicate_names.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_final_omitted_sentinel_alias_modes.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_final_partial_alias_generator.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_final_partial_annotated_field_warnings.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_final_partial_discriminator.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_final_partial_frozen_field.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_final_partial_rhs_field_metadata.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_final_review4_followup.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_final_review5.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_metaclass_extra.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_mixins_extra.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_optimistic_lock.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_partial.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_relation_load_checker.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_relation_load_checker_load_resolution.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_relation_load_checker_sessions.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_relation_preload.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_relationship_pep604.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_review_mixed_version_cache.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_review_raw_insert_cache.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_review_raw_insert_edges.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_review_raw_insert_late_model.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_review_raw_insert_sti.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_select.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_sti_default_isolation.py +0 -0
- {sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/tests/test_unset.py +0 -0
|
@@ -52,7 +52,8 @@ jobs:
|
|
|
52
52
|
# -------------------------------------------------------------------
|
|
53
53
|
# Job 1b: static type check. basedpyright reads pyrightconfig.json from
|
|
54
54
|
# the repository root (src only, pythonVersion 3.12 = lowest supported).
|
|
55
|
-
# The gate is 0 errors
|
|
55
|
+
# The gate is 0 errors and 0 warnings: pyrightconfig.json sets failOnWarnings,
|
|
56
|
+
# so any warning makes basedpyright exit non-zero and fails the job.
|
|
56
57
|
# The alembic extra is installed so the optional-import module
|
|
57
58
|
# `mixins/migration_cache_invalidation.py` resolves its imports.
|
|
58
59
|
# "0 errors" is also what an empty scan prints, so the job additionally
|
|
@@ -7,6 +7,96 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
7
7
|
> there is no stability or backward-compatibility guarantee between versions, and
|
|
8
8
|
> you use it at your own risk. Pin the exact version you depend on.
|
|
9
9
|
|
|
10
|
+
## [0.5.1]
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`query_dependency(Model)`**: FastAPI dependency for query-parameter DTOs
|
|
15
|
+
(`TableViewRequest`, `PaginationRequest`, `PageWindowRequest`, `TimeFilterRequest`,
|
|
16
|
+
`TrgmSearchRequest`, your subclasses):
|
|
17
|
+
`Annotated[TableViewRequest, Depends(query_dependency(TableViewRequest))]`. It declares
|
|
18
|
+
one query parameter per field carrying the field's type, constraints, default, `title`,
|
|
19
|
+
description, `examples`, `deprecated`, `json_schema_extra` and `discriminator` (each
|
|
20
|
+
parameter's OpenAPI schema is the field's JSON schema; a callable `json_schema_extra`
|
|
21
|
+
or a `Discriminator` object is a `TypeError`), builds the model itself and
|
|
22
|
+
re-raises a failed construction as `RequestValidationError` with locations prefixed by
|
|
23
|
+
`'query'`, so FastAPI answers 422. Requires the `fastapi` extra when called. See
|
|
24
|
+
[Paginate a list endpoint](docs/en/how-to/paginate-a-list-endpoint.md).
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- A bare mutation of a cached model that was flushed before the enhanced `commit()` -- inside
|
|
29
|
+
a savepoint (`begin_nested()` + `flush()`), by a manual `session.flush()`, or by an
|
|
30
|
+
autoflush -- was never invalidated: the cache kept serving the pre-commit row after the
|
|
31
|
+
commit. Pending invalidations were collected by scanning `new` / `dirty` / `deleted`
|
|
32
|
+
right before the commit, and a flushed object is no longer in those sets. Every flush
|
|
33
|
+
now registers the cached instances it writes (`after_flush`); an outermost rollback
|
|
34
|
+
drops them, a savepoint rollback keeps them (at most one extra invalidation). Also
|
|
35
|
+
present in 0.5.0.
|
|
36
|
+
- A table class inheriting a field declared with a type alias and a right-hand
|
|
37
|
+
`Field(...)` (`language: Str64 = Field(index=True)` in a non-table base) lost that
|
|
38
|
+
`Field`'s attributes: the column had no index / unique constraint / primary key /
|
|
39
|
+
foreign key / `nullable` / `sa_type` / `sa_column_kwargs` / `ondelete`, and the
|
|
40
|
+
Pydantic field could lose attributes set only there (e.g. `title`). The metaclass
|
|
41
|
+
rebuilt the inherited field from the alias's `Field` alone. The inherited field is now
|
|
42
|
+
the base class's resolved field (`Base.model_fields[name]`, in which Pydantic has
|
|
43
|
+
already merged the alias's `Field` and the right-hand one): its Pydantic attributes
|
|
44
|
+
(`default`, `alias`, `validation_alias`, `title`, `description`, ...) are taken as-is,
|
|
45
|
+
`None` included, so a base `Field(alias=None)` still clears the alias's `alias` in the
|
|
46
|
+
table class. Only the SQLModel column attributes are completed: the alias's and the
|
|
47
|
+
right-hand `Field`'s column settings are folded into one (the right-hand side wins on
|
|
48
|
+
conflicts; `False` never switches off `True`), and constraints of an alias nested in
|
|
49
|
+
a union (`Alias | None = Field(...)`), which Pydantic does not merge at field level,
|
|
50
|
+
are added. Plain-typed inherited fields (`int = Field(index=True)`) were not affected.
|
|
51
|
+
Also present in 0.5.0.
|
|
52
|
+
- A cross-field validation failure of `TableViewRequest` (and the other query DTOs) used
|
|
53
|
+
as a FastAPI dependency (`after_id` with a non-zero `offset`, `after_id` with a mutable
|
|
54
|
+
`order`, an inverted time range) was a 500. Declared through `query_dependency()` it is
|
|
55
|
+
a 422 located at the offending query parameter; the documented `ValidationError`
|
|
56
|
+
exception-handler workaround is no longer needed.
|
|
57
|
+
- Every commit of a cached model through the enhanced `AsyncSession` logged a spurious
|
|
58
|
+
`WARNING` "fallback compensation triggered: ..." and invalidated the cache a second
|
|
59
|
+
time. The `after_commit` event now hands the committed pendings over to the enhanced
|
|
60
|
+
`commit()`, which invalidates each of them once. Commits that bypass the enhanced
|
|
61
|
+
`commit()` (a plain SQLAlchemy / SQLModel session, `run_sync(lambda s: s.commit())`)
|
|
62
|
+
keep the fire-and-forget compensation and its `WARNING`; it now also takes over when
|
|
63
|
+
the enhanced `commit()` fails or is cancelled after the database committed.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- A table class field declared with a type alias **and** a right-hand `Field(...)`
|
|
68
|
+
(`name: Alias = Field(...)`) now gets its Pydantic attributes exactly as Pydantic
|
|
69
|
+
resolves that declaration -- the same field plain SQLModel builds, and the same field
|
|
70
|
+
a table class gets when it inherits the declaration from a base. The right-hand
|
|
71
|
+
`Field` overrides the alias's Pydantic attributes, `None` included; sqlmodel's
|
|
72
|
+
`Field()` passes every one of them explicitly, so with a right-hand `Field` the
|
|
73
|
+
alias's `alias` / `validation_alias` / `serialization_alias` / `title` /
|
|
74
|
+
`description` no longer apply (`Field(alias=None)` clears the alias's `alias`;
|
|
75
|
+
previously the metaclass skipped `None` and kept the alias's values). The alias's
|
|
76
|
+
`default` still applies unless the right-hand `Field` sets one -- except for an alias
|
|
77
|
+
nested in a union (`Alias | None = Field(...)`), whose `Field` Pydantic does not
|
|
78
|
+
merge: such a field is required unless the right-hand `Field` sets a default. Of the
|
|
79
|
+
bundled aliases only the `Optional*Decimal*` ones carry a Pydantic attribute
|
|
80
|
+
(`default=None`), so `OptionalNonNegativeDecimal38_18 | None = Field(index=True)` is
|
|
81
|
+
now required; drop the redundant `| None` or add `default=None`. Column attributes are
|
|
82
|
+
unchanged: the alias's and the right-hand `Field`'s column settings are still folded
|
|
83
|
+
together.
|
|
84
|
+
- The cross-field rules of `PaginationRequest` and `TimeFilterRequest` are field validators:
|
|
85
|
+
a violation is still a `ValidationError` with the same message, but located at the
|
|
86
|
+
field the rule constrains (`after_id`, `created_before_datetime`,
|
|
87
|
+
`updated_before_datetime`) instead of the model root, and independent violations are
|
|
88
|
+
reported together. `TimeFilterRequest` no longer overrides `model_post_init`.
|
|
89
|
+
- `PolymorphicBaseMixin._is_joined_table_inheritance()` is renamed
|
|
90
|
+
`is_joined_table_inheritance()` (it was documented and called across modules; the
|
|
91
|
+
underscore was not a real boundary). Update direct callers.
|
|
92
|
+
- `get(table_view=...)` is annotated `TableViewArgument` (`TimeFilterRequest |
|
|
93
|
+
PageWindowRequest`), matching what it already accepted at runtime (e.g. a bare
|
|
94
|
+
`PageWindowRequest`). The `join=(Model, onclause)` ON clause is annotated with the
|
|
95
|
+
new `OnClauseArgument` alias (SQLAlchemy's public `OnClauseRole`) instead of
|
|
96
|
+
SQLAlchemy's private `_OnClauseArgument`.
|
|
97
|
+
- The type-check gate of the repository is now 0 errors **and 0 warnings**
|
|
98
|
+
(`failOnWarnings: true`).
|
|
99
|
+
|
|
10
100
|
## [0.5.0]
|
|
11
101
|
|
|
12
102
|
Upgrade guide: [Migrate from 0.4.x to 0.5.0](docs/en/how-to/migrate-to-0-5.md)
|
|
@@ -164,14 +254,6 @@ Upgrade guide: [Migrate from 0.4.x to 0.5.0](docs/en/how-to/migrate-to-0-5.md)
|
|
|
164
254
|
database has already committed): the affected cache entries keep serving
|
|
165
255
|
pre-commit data until their TTL expires. Use `no_cache=True` for reads that must
|
|
166
256
|
not rely on the cache.
|
|
167
|
-
- Every commit of a cached model through the enhanced `AsyncSession` logs a spurious
|
|
168
|
-
`WARNING` "fallback compensation triggered: ..." (pre-existing in 0.4.x). The only
|
|
169
|
-
effect is one redundant cache invalidation; correctness is unaffected.
|
|
170
|
-
- When `TableViewRequest` is used as a `Depends()` dependency, a cross-field validation
|
|
171
|
-
failure (e.g. `after_id` with a non-zero `offset`) surfaces as a 500 instead of a 422,
|
|
172
|
-
because FastAPI constructs the object outside its per-parameter validation. Register
|
|
173
|
-
the `ValidationError` → 422 handler shown in
|
|
174
|
-
[Paginate a list endpoint](docs/en/how-to/paginate-a-list-endpoint.md).
|
|
175
257
|
- On Python 3.12, `JSON100K | None` as a `table=True` field raises
|
|
176
258
|
`has no matching SQLAlchemy type`; declare the column explicitly with
|
|
177
259
|
`Field(default=None, sa_type=JSONB)` (pre-existing).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sqlmodel-ext
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphic inheritance, optimistic locking, relation preloading, and reusable field types.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Foxerine/sqlmodel-ext
|
|
6
6
|
Project-URL: Repository, https://github.com/Foxerine/sqlmodel-ext
|
|
@@ -399,7 +399,7 @@ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
|
399
399
|
from sqlmodel import Field
|
|
400
400
|
from sqlmodel_ext import (
|
|
401
401
|
AsyncSession, SQLModelBase, UUIDTableBaseMixin, Str64, Text10K,
|
|
402
|
-
ListResponse, TableViewRequest, UUIDIdDatetimeInfoMixin,
|
|
402
|
+
ListResponse, TableViewRequest, UUIDIdDatetimeInfoMixin, query_dependency,
|
|
403
403
|
)
|
|
404
404
|
|
|
405
405
|
# ── Dependency-injection layer: declare once (e.g. in a shared
|
|
@@ -415,9 +415,10 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|
|
415
415
|
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
|
416
416
|
"""Request-scoped AsyncSession. The single way endpoints touch the DB."""
|
|
417
417
|
|
|
418
|
-
#
|
|
419
|
-
#
|
|
420
|
-
|
|
418
|
+
# query_dependency() binds every TableViewRequest field as a query param
|
|
419
|
+
# (no hand-written offset/limit/order plumbing) and turns cross-field errors
|
|
420
|
+
# (e.g. after_id + offset) into a 422 instead of a 500.
|
|
421
|
+
TableViewRequestDep = Annotated[TableViewRequest, Depends(query_dependency(TableViewRequest))]
|
|
421
422
|
|
|
422
423
|
async def get_current_user(session: SessionDep) -> "User":
|
|
423
424
|
... # decode the bearer token, load the user — your auth, unchanged
|
|
@@ -1177,7 +1178,7 @@ mapping = Tool.get_identity_to_class_map()
|
|
|
1177
1178
|
# {'websearchtool': WebSearchTool, 'calculatortool': CalculatorTool}
|
|
1178
1179
|
|
|
1179
1180
|
# Check inheritance type
|
|
1180
|
-
Tool.
|
|
1181
|
+
Tool.is_joined_table_inheritance() # True for JTI, False for STI
|
|
1181
1182
|
```
|
|
1182
1183
|
|
|
1183
1184
|
---
|
|
@@ -343,7 +343,7 @@ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
|
343
343
|
from sqlmodel import Field
|
|
344
344
|
from sqlmodel_ext import (
|
|
345
345
|
AsyncSession, SQLModelBase, UUIDTableBaseMixin, Str64, Text10K,
|
|
346
|
-
ListResponse, TableViewRequest, UUIDIdDatetimeInfoMixin,
|
|
346
|
+
ListResponse, TableViewRequest, UUIDIdDatetimeInfoMixin, query_dependency,
|
|
347
347
|
)
|
|
348
348
|
|
|
349
349
|
# ── Dependency-injection layer: declare once (e.g. in a shared
|
|
@@ -359,9 +359,10 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|
|
359
359
|
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
|
360
360
|
"""Request-scoped AsyncSession. The single way endpoints touch the DB."""
|
|
361
361
|
|
|
362
|
-
#
|
|
363
|
-
#
|
|
364
|
-
|
|
362
|
+
# query_dependency() binds every TableViewRequest field as a query param
|
|
363
|
+
# (no hand-written offset/limit/order plumbing) and turns cross-field errors
|
|
364
|
+
# (e.g. after_id + offset) into a 422 instead of a 500.
|
|
365
|
+
TableViewRequestDep = Annotated[TableViewRequest, Depends(query_dependency(TableViewRequest))]
|
|
365
366
|
|
|
366
367
|
async def get_current_user(session: SessionDep) -> "User":
|
|
367
368
|
... # decode the bearer token, load the user — your auth, unchanged
|
|
@@ -1121,7 +1122,7 @@ mapping = Tool.get_identity_to_class_map()
|
|
|
1121
1122
|
# {'websearchtool': WebSearchTool, 'calculatortool': CalculatorTool}
|
|
1122
1123
|
|
|
1123
1124
|
# Check inheritance type
|
|
1124
|
-
Tool.
|
|
1125
|
+
Tool.is_joined_table_inheritance() # True for JTI, False for STI
|
|
1125
1126
|
```
|
|
1126
1127
|
|
|
1127
1128
|
---
|
|
@@ -343,7 +343,7 @@ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
|
343
343
|
from sqlmodel import Field
|
|
344
344
|
from sqlmodel_ext import (
|
|
345
345
|
AsyncSession, SQLModelBase, UUIDTableBaseMixin, Str64, Text10K,
|
|
346
|
-
ListResponse, TableViewRequest, UUIDIdDatetimeInfoMixin,
|
|
346
|
+
ListResponse, TableViewRequest, UUIDIdDatetimeInfoMixin, query_dependency,
|
|
347
347
|
)
|
|
348
348
|
|
|
349
349
|
# ── 依赖注入层:声明一次(例如放在共享的 deps 模块),
|
|
@@ -359,9 +359,10 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|
|
359
359
|
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
|
360
360
|
"""请求级 AsyncSession。端点访问数据库的唯一途径。"""
|
|
361
361
|
|
|
362
|
-
#
|
|
363
|
-
#
|
|
364
|
-
|
|
362
|
+
# query_dependency() 把 TableViewRequest 的每个字段绑定为查询参数(无需手写
|
|
363
|
+
# offset/limit/order 的管道代码),并把跨字段错误(如 after_id + offset)变成
|
|
364
|
+
# 422 而不是 500。
|
|
365
|
+
TableViewRequestDep = Annotated[TableViewRequest, Depends(query_dependency(TableViewRequest))]
|
|
365
366
|
|
|
366
367
|
async def get_current_user(session: SessionDep) -> "User":
|
|
367
368
|
... # 解析 bearer token、加载用户——沿用你自己的认证逻辑
|
|
@@ -1121,7 +1122,7 @@ mapping = Tool.get_identity_to_class_map()
|
|
|
1121
1122
|
# {'websearchtool': WebSearchTool, 'calculatortool': CalculatorTool}
|
|
1122
1123
|
|
|
1123
1124
|
# 检查继承类型
|
|
1124
|
-
Tool.
|
|
1125
|
+
Tool.is_joined_table_inheritance() # JTI 为 True,STI 为 False
|
|
1125
1126
|
```
|
|
1126
1127
|
|
|
1127
1128
|
---
|
|
@@ -199,6 +199,10 @@ data = patch.model_dump(exclude_unset=True)
|
|
|
199
199
|
- List endpoints take a `TableViewRequest` (`offset` / `limit` / `order` /
|
|
200
200
|
`desc` / `after_id` + time bounds). `after_id` cannot be combined with
|
|
201
201
|
`offset`, custom `order_by` or `join`.
|
|
202
|
+
- Inject query-parameter DTOs with
|
|
203
|
+
`Annotated[TableViewRequest, Depends(query_dependency(TableViewRequest))]`,
|
|
204
|
+
never a bare `Depends()` (a failed cross-field rule would be a 500 instead
|
|
205
|
+
of a 422).
|
|
202
206
|
|
|
203
207
|
## 7. Relationships: always preload
|
|
204
208
|
|
|
@@ -118,7 +118,7 @@ With `commit=False` nothing is invalidated (the data is not committed yet); the
|
|
|
118
118
|
| `delete(instances)` | per-instance `DEL id:...` + `INCR ver:{cls}` |
|
|
119
119
|
| `delete(condition)` | model-level `SCAN+DEL id:{cls}:*` + `INCR ver:{cls}` |
|
|
120
120
|
| `add()` | `INCR ver:{cls}`; instances with an explicitly specified id also have their ID cache invalidated (guards against id reuse) |
|
|
121
|
-
| bare `session.add()` / attribute mutation / `session.delete()` + `commit()` |
|
|
121
|
+
| bare `session.add()` / attribute mutation / `session.delete()` + `commit()` | registered by the flush that sends them (a savepoint flush, a manual `flush()`, an autoflush or the commit's own flush): row level (the flushed `id`) + query cache |
|
|
122
122
|
|
|
123
123
|
### Cascade deletes
|
|
124
124
|
|
|
@@ -140,8 +140,8 @@ The query cache `INCR`s the version keys of itself and all ancestors in one pipe
|
|
|
140
140
|
|
|
141
141
|
## The two invalidation paths
|
|
142
142
|
|
|
143
|
-
1. **Synchronous path** (enhanced `AsyncSession.commit()`):
|
|
144
|
-
2. **Compensation path** (`after_commit` event): the event handler is synchronous and cannot `await`, so it schedules a fire-and-forget task that invalidates
|
|
143
|
+
1. **Synchronous path** (enhanced `AsyncSession.commit()`): it marks the session before the real commit; the `after_commit` event sees the mark and hands the complete set of registered items (including those registered during the commit's own flush, e.g. cascade children) over to `commit()`, which `await`s their invalidation before returning. Each item is invalidated once; no warning is logged.
|
|
144
|
+
2. **Compensation path** (`after_commit` event without the mark): the event handler is synchronous and cannot `await`, so it schedules a fire-and-forget task that invalidates the items and logs a `WARNING` "fallback compensation triggered: ...". It catches commits that did not go through the enhanced `commit()` (a plain SQLAlchemy / SQLModel session, `run_sync(lambda s: s.commit())`), and an enhanced `commit()` that failed or was cancelled after the database committed; there is a very short stale window, and TTL provides eventual consistency.
|
|
145
145
|
|
|
146
146
|
Sentinel objects:
|
|
147
147
|
|
|
@@ -126,7 +126,7 @@ real_table_args, custom_table_args = [], []
|
|
|
126
126
|
for arg in raw_table_args:
|
|
127
127
|
(custom_table_args if isinstance(arg, CustomTableArg) else real_table_args).append(arg)
|
|
128
128
|
attrs['__table_args__'] = tuple(real_table_args)
|
|
129
|
-
# appended to the module-level queue
|
|
129
|
+
# appended to the module-level queue classes_with_custom_table_args after super().__new__
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
**Why**: SQLAlchemy's `Table.__init__` consumes every element of `__table_args__` immediately — an `Index` referencing a not-yet-existing column raises on the spot. `CustomTableArg` is a generic "defer this" marker: the metaclass only intercepts and enqueues, knowing nothing about the semantics; the current consumer is `mixins.polymorphic.DeferredIndex` (deferred indexes on STI subclass columns). `table_name=` / `abstract=` become `__tablename__` / `__abstract__`.
|
|
@@ -134,7 +134,7 @@ attrs['__table_args__'] = tuple(real_table_args)
|
|
|
134
134
|
### Step 4: Resolve annotations and record "which fields this class declares itself"
|
|
135
135
|
|
|
136
136
|
```python
|
|
137
|
-
annotations, annotation_strings, eval_globals, eval_locals =
|
|
137
|
+
annotations, annotation_strings, eval_globals, eval_locals = resolve_annotations(attrs)
|
|
138
138
|
_own_annotation_names = frozenset(annotations)
|
|
139
139
|
```
|
|
140
140
|
|
|
@@ -142,7 +142,7 @@ This snapshot must be taken **before** any annotation injection: later steps inj
|
|
|
142
142
|
|
|
143
143
|
### Step 4.5: Recover SQLModel attributes from `Annotated[T, Field(...)]`
|
|
144
144
|
|
|
145
|
-
When Pydantic v2 processes `Annotated` metadata it replaces `sqlmodel.main.FieldInfo` with `pydantic.fields.FieldInfo`, which knows nothing about SQLModel attributes such as `foreign_key` or `sa_type`. For **table classes** (including `Annotated` fields inherited from parents), `_recover_annotated_sqlmodel_fields()` converts them back into the `= Field(...)` form; non-table classes are left as-is so child table classes can inherit them. When several `FieldInfo`s are merged, an explicit `default=None` is kept as a real value (not "unset") — otherwise the field would silently become required.
|
|
145
|
+
When Pydantic v2 processes `Annotated` metadata it replaces `sqlmodel.main.FieldInfo` with `pydantic.fields.FieldInfo`, which knows nothing about SQLModel attributes such as `foreign_key` or `sa_type`. For **table classes** (including `Annotated` fields inherited from parents), `_recover_annotated_sqlmodel_fields()` converts them back into the `= Field(...)` form; non-table classes are left as-is so child table classes can inherit them. When several `FieldInfo`s are merged, an explicit `default=None` is kept as a real value (not "unset") — otherwise the field would silently become required. A field with a right-hand `Field` (`name: Alias = Field(...)`) is rebuilt from Pydantic's resolution of that declaration — `FieldInfo.from_annotated_attribute()` when the table class declares it, the base's resolved field `Base.model_fields[name]` when the table class **inherits** it (without re-declaring it). Both give the same field: its Pydantic attributes are taken as-is (an explicit `alias=None` stays `None`, as in plain SQLModel), and only the column attributes are completed — the `FieldInfoMetadata` carriers of the alias's and the right-hand `Field` are folded into one, since SQLModel reads only the first. Metadata that stays in the class annotation (an `AfterValidator` next to the alias's `Field`, for example) is not copied into the field, so it is applied once.
|
|
146
146
|
|
|
147
147
|
### Step 4.5.b: `oplock_version` is a reserved name
|
|
148
148
|
|
|
@@ -178,7 +178,7 @@ The generated annotations exist at runtime; static type checkers still see the b
|
|
|
178
178
|
|
|
179
179
|
```python
|
|
180
180
|
for field_name, field_type in annotations.items():
|
|
181
|
-
sa_type =
|
|
181
|
+
sa_type = extract_sa_type_from_annotation(field_type)
|
|
182
182
|
if sa_type is not None:
|
|
183
183
|
field_value = attrs.get(field_name, Undefined)
|
|
184
184
|
if field_value is Undefined:
|
|
@@ -195,10 +195,10 @@ for field_name, field_type in annotations.items():
|
|
|
195
195
|
|
|
196
196
|
`_durably_set_sa_type()` writes `sa_type` into a `FieldInfoMetadata` entry of `FieldInfo.metadata` — the channel SQLModel's own `Field(sa_type=...)` uses and that survives Pydantic's `model_fields` rebuild; a plain `setattr` would be lost before the column is built. An explicitly set `sa_type` is never overwritten.
|
|
197
197
|
|
|
198
|
-
#### `
|
|
198
|
+
#### `extract_sa_type_from_annotation()` — three extraction methods
|
|
199
199
|
|
|
200
200
|
```python
|
|
201
|
-
def
|
|
201
|
+
def extract_sa_type_from_annotation(annotation):
|
|
202
202
|
# Method 1: the type itself has a __sqlmodel_sa_type__ attribute
|
|
203
203
|
# Method 2: an Annotated metadata item has __sqlmodel_sa_type__, or the schema returned by
|
|
204
204
|
# its __get_pydantic_core_schema__ carries 'sa_type' in its metadata
|
|
@@ -206,7 +206,7 @@ def _extract_sa_type_from_annotation(annotation):
|
|
|
206
206
|
...
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
Take `Array[str]`: `__class_getitem__` returns `Annotated[list[str],
|
|
209
|
+
Take `Array[str]`: `__class_getitem__` returns `Annotated[list[str], ArrayTypeHandler(str)]`, and the `ArrayTypeHandler` schema carries `metadata={'sa_type': ARRAY(String)}`; the `JSON100K` schema carries `metadata={'sa_type': JSONB}`. **The type declares its own column type**, and the metaclass delivers it to the column builder.
|
|
210
210
|
|
|
211
211
|
### Steps 5–7: Save SQLModel `FieldInfo`s, call the parent, restore
|
|
212
212
|
|
|
@@ -45,7 +45,7 @@ def __init_subclass__(cls, polymorphic_on=None, polymorphic_abstract=None, **kwa
|
|
|
45
45
|
|
|
46
46
|
```python
|
|
47
47
|
@classmethod
|
|
48
|
-
def
|
|
48
|
+
def is_joined_table_inheritance(cls) -> bool:
|
|
49
49
|
"""Subclass table name differs from parent → JTI"""
|
|
50
50
|
|
|
51
51
|
@classmethod
|
{sqlmodel_ext-0.5.0 → sqlmodel_ext-0.5.1}/docs/en/explanation/transactional-cache-transparency.md
RENAMED
|
@@ -94,15 +94,14 @@ Register first, then execute, and the warning won't fire (the table already has
|
|
|
94
94
|
|
|
95
95
|
CRUD methods (`save` / `update` / `delete` / `add`) **don't invalidate** themselves; they only register pending invalidations into `session.info`. The enhanced `AsyncSession.commit()` orchestrates:
|
|
96
96
|
|
|
97
|
-
1.
|
|
98
|
-
2.
|
|
99
|
-
3.
|
|
100
|
-
4.
|
|
101
|
-
5. Run post-commit callbacks
|
|
97
|
+
1. Every flush registers the cached models it writes (the `after_flush` event), covering bare `add` / attribute mutation / `delete` that bypass the CRUD methods -- whether they were flushed earlier (inside a savepoint, by a manual `flush()`, by an autoflush) or by this commit
|
|
98
|
+
2. Actually commit. The `after_commit` event pops the complete set of registered items — including those registered by this commit's own flush, such as cascade-deleted children — and hands it over to `commit()`
|
|
99
|
+
3. Synchronously invalidate the handed-over items, each exactly once
|
|
100
|
+
4. Run post-commit callbacks
|
|
102
101
|
|
|
103
102
|
With `commit=False` nothing is invalidated (the data isn't committed yet); the registered items wait for your final `session.commit()` — "several `commit=False` operations + one commit" is naturally correct. Rollback discards the registered items.
|
|
104
103
|
|
|
105
|
-
|
|
104
|
+
For a commit that doesn't go through the enhanced `commit()` (e.g. a plain sqlmodel session, or `run_sync(lambda s: s.commit())`), the `after_commit` event instead schedules a fire-and-forget compensation task that invalidates the popped items and logs a `WARNING` "fallback compensation triggered: ..." — there is a brief stale window, TTL provides eventual consistency. The same fallback takes over if the enhanced `commit()` fails or is cancelled after the database has committed. A commit through the enhanced session never logs that warning.
|
|
106
105
|
|
|
107
106
|
### Savepoints
|
|
108
107
|
|
|
@@ -79,7 +79,7 @@ char = await char.save(session)
|
|
|
79
79
|
| `delete(instance)` | same |
|
|
80
80
|
| `delete(condition=...)` | model-wide ID cleanup + version `+1` |
|
|
81
81
|
| `add()` | version `+1` (instances with an explicitly specified id also clear their ID cache) |
|
|
82
|
-
| Bare `session.add()` / attribute changes / `session.delete()` followed by `session.commit()` | Also invalidated automatically (the
|
|
82
|
+
| Bare `session.add()` / attribute changes / `session.delete()` followed by `session.commit()` | Also invalidated automatically (the flush that writes them registers them, including a savepoint flush, a manual `flush()` or an autoflush earlier in the transaction) |
|
|
83
83
|
|
|
84
84
|
## 4. Correct inside transactions too
|
|
85
85
|
|
|
@@ -138,13 +138,14 @@ class Tag(SQLModelBase, UUIDTableBaseMixin, TrgmSearchableMixin, table=True):
|
|
|
138
138
|
```python
|
|
139
139
|
from typing import Annotated
|
|
140
140
|
from fastapi import Depends
|
|
141
|
+
from sqlmodel_ext import query_dependency
|
|
141
142
|
from sqlmodel_ext.mixins import TrgmSearchRequest
|
|
142
143
|
|
|
143
144
|
@router.get("", response_model=ListResponse[TagResponse])
|
|
144
145
|
async def list_tags(
|
|
145
146
|
session: SessionDep,
|
|
146
|
-
table_view: Annotated[TableViewRequest, Depends()],
|
|
147
|
-
search: Annotated[TrgmSearchRequest, Depends()],
|
|
147
|
+
table_view: Annotated[TableViewRequest, Depends(query_dependency(TableViewRequest))],
|
|
148
|
+
search: Annotated[TrgmSearchRequest, Depends(query_dependency(TrgmSearchRequest))],
|
|
148
149
|
) -> ListResponse[Tag]:
|
|
149
150
|
condition = search.apply_condition(Tag, col(Tag.name) != 'hidden') # ANDed into your existing scope condition
|
|
150
151
|
return await Tag.get_with_count(session, condition, table_view=table_view)
|
|
@@ -64,10 +64,11 @@ if patch.subtitle is not Unset:
|
|
|
64
64
|
from typing import Annotated
|
|
65
65
|
from uuid import UUID
|
|
66
66
|
from fastapi import APIRouter, Depends
|
|
67
|
-
from sqlmodel_ext import ListResponse, TableViewRequest
|
|
67
|
+
from sqlmodel_ext import ListResponse, TableViewRequest, query_dependency
|
|
68
68
|
|
|
69
69
|
router = APIRouter(prefix="/articles", tags=["articles"])
|
|
70
|
-
|
|
70
|
+
# query_dependency(): a failed cross-field rule (after_id + offset, ...) is a 422, not a 500 -- see "Paginate a list endpoint"
|
|
71
|
+
TableViewDep = Annotated[TableViewRequest, Depends(query_dependency(TableViewRequest))]
|
|
71
72
|
|
|
72
73
|
@router.post("", response_model=ArticleResponse)
|
|
73
74
|
async def create_article(
|
|
@@ -117,20 +118,12 @@ The `with_for_update=True` in the `delete` endpoint closes the TOCTOU window bet
|
|
|
117
118
|
|
|
118
119
|
```python
|
|
119
120
|
from fastapi import FastAPI, Request
|
|
120
|
-
from fastapi.encoders import jsonable_encoder
|
|
121
121
|
from fastapi.responses import JSONResponse
|
|
122
|
-
from pydantic import ValidationError
|
|
123
122
|
from sqlmodel_ext import OptimisticLockError
|
|
124
123
|
from sqlmodel_ext.mixins import KeysetCursorError, ResourceReferencedError
|
|
125
124
|
|
|
126
125
|
app = FastAPI()
|
|
127
126
|
|
|
128
|
-
@app.exception_handler(ValidationError) # cross-field validation of query-parameter DTOs (after_id + offset, etc.)
|
|
129
|
-
async def dto_validation_error_handler(request: Request, exc: ValidationError) -> JSONResponse:
|
|
130
|
-
return JSONResponse(status_code=422, content={
|
|
131
|
-
"detail": jsonable_encoder(exc.errors(include_url=False, include_context=False)),
|
|
132
|
-
})
|
|
133
|
-
|
|
134
127
|
@app.exception_handler(KeysetCursorError) # cursor invalid / unsupported, status_code = 422
|
|
135
128
|
async def keyset_error_handler(request: Request, exc: KeysetCursorError) -> JSONResponse:
|
|
136
129
|
return JSONResponse(status_code=exc.status_code, content={"detail": str(exc)})
|
|
@@ -144,7 +137,7 @@ async def oplock_handler(request: Request, exc: OptimisticLockError) -> JSONResp
|
|
|
144
137
|
return JSONResponse(status_code=409, content={"detail": "Record was modified by someone else. Please refresh and retry."})
|
|
145
138
|
```
|
|
146
139
|
|
|
147
|
-
|
|
140
|
+
Cross-field validation of query-parameter DTOs needs no handler: `query_dependency()` already turns it into FastAPI's `RequestValidationError` (422, locations starting with `query`). With a bare `Depends()` the same error would be a 500 — the `ValidationError` raised when FastAPI itself calls `TableViewRequest(...)` is outside its request-validation path.
|
|
148
141
|
|
|
149
142
|
## Key conventions
|
|
150
143
|
|
|
@@ -37,7 +37,7 @@ next_page = await Article.get(
|
|
|
37
37
|
|
|
38
38
|
Repeat until the returned list is empty. `get_with_count()` accepts `after_id` as well; the `count` it returns is the size of the whole filtered set and is **not affected by the cursor**.
|
|
39
39
|
|
|
40
|
-
In FastAPI the endpoint code doesn't change: with `TableViewRequest` as a dependency, the client simply passes `?after_id=<id of the last row of the previous page>&limit=20` (
|
|
40
|
+
In FastAPI the endpoint code doesn't change: with `TableViewRequest` as a dependency, the client simply passes `?after_id=<id of the last row of the previous page>&limit=20` (declare the dependency with `query_dependency(TableViewRequest)` so the rejected combinations below are a 422, see [Paginate a list endpoint](./paginate-a-list-endpoint#why-query-dependency)).
|
|
41
41
|
|
|
42
42
|
## How it guarantees no duplicates and no gaps
|
|
43
43
|
|
|
@@ -49,8 +49,8 @@ In FastAPI the endpoint code doesn't change: with `TableViewRequest` as a depend
|
|
|
49
49
|
|
|
50
50
|
| Combination | When rejected | Error |
|
|
51
51
|
|------|---------|------|
|
|
52
|
-
| `after_id` + non-zero `offset` | When constructing `TableViewRequest` | `ValidationError`
|
|
53
|
-
| `after_id` + `order='updated_at'` (or any mutable domain sort column) | At construction | `ValidationError` |
|
|
52
|
+
| `after_id` + non-zero `offset` | When constructing `TableViewRequest` | `ValidationError` at `after_id` (through `query_dependency()`: 422 at `["query", "after_id"]`) |
|
|
53
|
+
| `after_id` + `order='updated_at'` (or any mutable domain sort column) | At construction | `ValidationError` at `after_id` (422 through `query_dependency()`) |
|
|
54
54
|
| `after_id` + explicit `order_by=` | At `get()` | `KeysetCursorUnsupportedError` |
|
|
55
55
|
| `after_id` + `join=` | At `get()` | `KeysetCursorUnsupportedError` |
|
|
56
56
|
| Anchor does not exist, or is outside this query's visible scope (`condition` + `filter` + STI filter) | At `get()` | `KeysetCursorInvalidError` |
|
|
@@ -13,33 +13,28 @@
|
|
|
13
13
|
```python
|
|
14
14
|
from typing import Annotated
|
|
15
15
|
from fastapi import Depends
|
|
16
|
-
from sqlmodel_ext import TableViewRequest
|
|
16
|
+
from sqlmodel_ext import TableViewRequest, query_dependency
|
|
17
17
|
|
|
18
|
-
TableViewDep = Annotated[TableViewRequest, Depends()]
|
|
18
|
+
TableViewDep = Annotated[TableViewRequest, Depends(query_dependency(TableViewRequest))]
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
`TableViewRequest` carries pagination (`offset` / `limit` / `desc` / `order`), a keyset cursor (`after_id`) and time filtering (`created_after_datetime` / `created_before_datetime` / `updated_after_datetime` / `updated_before_datetime`).
|
|
21
|
+
`TableViewRequest` carries pagination (`offset` / `limit` / `desc` / `order`), a keyset cursor (`after_id`) and time filtering (`created_after_datetime` / `created_before_datetime` / `updated_after_datetime` / `updated_before_datetime`). `query_dependency()` declares one query parameter per model field (same type, constraints, default, title, description, examples, deprecation and `json_schema_extra` as the model field, so each parameter's OpenAPI schema is the field's JSON schema) and constructs the `TableViewRequest` itself.
|
|
22
22
|
|
|
23
|
-
###
|
|
23
|
+
### Why `query_dependency()`
|
|
24
24
|
|
|
25
|
-
Single-field errors (`limit=0`, a time without a timezone) are validated per parameter by FastAPI and are a 422 directly. But **cross-field** rules — `after_id` and `offset` are mutually exclusive, `after_id` can't be combined with `order=updated_at`, time ranges must be in order — only fire when FastAPI calls `TableViewRequest(...)`
|
|
25
|
+
Single-field errors (`limit=0`, a time without a timezone) are validated per parameter by FastAPI and are a 422 directly. But **cross-field** rules — `after_id` and `offset` are mutually exclusive, `after_id` can't be combined with `order=updated_at`, time ranges must be in order — only fire when the `TableViewRequest` is constructed. With a bare `Depends()`, FastAPI calls `TableViewRequest(...)` itself; the Pydantic `ValidationError` raised there is not a `RequestValidationError`, FastAPI does not catch it, and the request fails with a **500**.
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@app.exception_handler(ValidationError)
|
|
34
|
-
async def dto_validation_error_handler(request: Request, exc: ValidationError) -> JSONResponse:
|
|
35
|
-
return JSONResponse(
|
|
36
|
-
status_code=422,
|
|
37
|
-
content={"detail": jsonable_encoder(exc.errors(include_url=False, include_context=False))},
|
|
38
|
-
)
|
|
27
|
+
`query_dependency()` re-raises that `ValidationError` as a `RequestValidationError` whose error locations start with `query`, so FastAPI's default handler answers **422** in the same shape as any other query-parameter error — no exception handler to register:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{"detail": [{"type": "value_error", "loc": ["query", "after_id"],
|
|
31
|
+
"msg": "Value error, after_id and offset cannot be combined: ...", "input": "..."}]}
|
|
39
32
|
```
|
|
40
33
|
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
A cross-field error is located at the field its rule constrains: both `after_id` rules report `after_id`, the time-range rules report the `*_before_datetime` field. The same form works for `PageWindowRequest`, `PaginationRequest`, `TimeFilterRequest`, `TrgmSearchRequest` and your own subclasses (`query_dependency(MyTableViewRequest)`). The endpoint can still declare further query parameters next to it; undeclared query parameters (such as a cache-busting `?_=123`) are ignored.
|
|
35
|
+
|
|
36
|
+
::: warning Don't switch to `Annotated[TableViewRequest, Query()]`
|
|
37
|
+
A Pydantic query model only works when it is the endpoint's **only** query parameter: add one more ordinary query parameter to the endpoint (e.g. `status: str | None = None`) and FastAPI no longer hands the whole query string to the model, so requests fail outright with 422 `Field required`. On top of that, `SQLModelBase`'s `extra='forbid'` turns any undeclared query parameter (such as `?_=123`) into a 422.
|
|
43
38
|
:::
|
|
44
39
|
|
|
45
40
|
## 2. Call `get_with_count()` in the endpoint
|
|
@@ -98,7 +93,7 @@ Sorting always appends `id` in the same direction as a tiebreaker column, so row
|
|
|
98
93
|
- **Time parameters must carry a timezone** (`...Z` or `+08:00`). All time bounds are `AwareDatetime`; a value without a timezone is a 422 outright — otherwise it would be silently interpreted in the database's timezone.
|
|
99
94
|
- **Time intervals are half-open** `[after, before)`. `created_after_datetime=2026-01-01T00:00:00Z` + `created_before_datetime=2026-02-01T00:00:00Z` means "all of January (UTC)".
|
|
100
95
|
- **`order` only accepts values in the `Literal`**; any other string makes FastAPI return `422`.
|
|
101
|
-
- **`after_id` can't be used with a non-zero `offset`**, nor with `order=updated_at` — both are rejected when `TableViewRequest` is constructed (a 422
|
|
96
|
+
- **`after_id` can't be used with a non-zero `offset`**, nor with `order=updated_at` — both are rejected when `TableViewRequest` is constructed (a 422 at `["query", "after_id"]` through `query_dependency()`).
|
|
102
97
|
|
|
103
98
|
## Related reference
|
|
104
99
|
|
|
@@ -154,14 +154,14 @@ async def get(
|
|
|
154
154
|
offset: int | None = None,
|
|
155
155
|
limit: int | None = None,
|
|
156
156
|
fetch_mode: Literal["one", "first", "all"] = "first",
|
|
157
|
-
join: type[TableBaseMixin] | tuple[type[TableBaseMixin],
|
|
157
|
+
join: type[TableBaseMixin] | tuple[type[TableBaseMixin], OnClauseArgument] | None = None,
|
|
158
158
|
options: list[ExecutableOption] | None = None,
|
|
159
159
|
load: QueryableAttribute[Any] | list[QueryableAttribute[Any]] | None = None,
|
|
160
160
|
order_by: list[ColumnElement[Any]] | None = None,
|
|
161
161
|
filter: ColumnElement[bool] | bool | None = None,
|
|
162
162
|
with_for_update: bool = False,
|
|
163
163
|
skip_locked: bool = False,
|
|
164
|
-
table_view:
|
|
164
|
+
table_view: TableViewArgument | None = None, # TimeFilterRequest | PageWindowRequest
|
|
165
165
|
jti_subclasses: list[type[PolymorphicBaseMixin]] | Literal['all'] | None = None,
|
|
166
166
|
populate_existing: bool = False,
|
|
167
167
|
authoritative: bool = False,
|
|
@@ -195,7 +195,7 @@ On `CachedTableBaseMixin` models, `get()` has one extra parameter, `no_cache: bo
|
|
|
195
195
|
| `filter` | `ColumnElement[bool]` | Additional WHERE condition |
|
|
196
196
|
| `with_for_update` | `bool` | `SELECT ... FOR UPDATE` row lock. Instance `id()`s are written to `session.info[SESSION_FOR_UPDATE_KEY]`; **forces** `populate_existing` (cannot be turned off), guaranteeing you get the latest database values rather than a stale object from the identity map |
|
|
197
197
|
| `skip_locked` | `bool` | `FOR UPDATE SKIP LOCKED`: skip rows locked by other transactions instead of waiting. Only effective when `with_for_update=True`. "0 rows" may also mean "every candidate is locked by someone else", so **do not** use it for existence checks |
|
|
198
|
-
| `table_view` | `TableViewRequest` | Bundle of pagination + ordering + time filtering + keyset cursor parameters. `order` always appends `id` in the same direction as a tiebreaker; `after_id` applies the keyset cursor |
|
|
198
|
+
| `table_view` | `TableViewRequest` (or any `TimeFilterRequest` / `PageWindowRequest`; each part is applied when present) | Bundle of pagination + ordering + time filtering + keyset cursor parameters. `order` always appends `id` in the same direction as a tiebreaker; `after_id` applies the keyset cursor |
|
|
199
199
|
| `jti_subclasses` | `list[type] \| 'all'` | Subclass loading for JTI polymorphic relationships (requires `load`) |
|
|
200
200
|
| `populate_existing` | `bool` | Lock-free forced overwrite of identity-map objects with database data |
|
|
201
201
|
| `authoritative` | `bool` | The single switch for **authorization reads** (the result decides whether something is allowed, so it must be authoritative): at this layer equivalent to `populate_existing=True`; cached models additionally bypass Redis. Monotonically merged with `populate_existing` (`or`) |
|
|
@@ -346,7 +346,7 @@ async def get_with_count(
|
|
|
346
346
|
session: AsyncSession,
|
|
347
347
|
condition: ColumnElement[bool] | bool | None = None,
|
|
348
348
|
*,
|
|
349
|
-
join: type[TableBaseMixin] | tuple[type[TableBaseMixin],
|
|
349
|
+
join: type[TableBaseMixin] | tuple[type[TableBaseMixin], OnClauseArgument] | None = None,
|
|
350
350
|
options: list[ExecutableOption] | None = None,
|
|
351
351
|
load: QueryableAttribute[Any] | list[QueryableAttribute[Any]] | None = None,
|
|
352
352
|
order_by: list[ColumnElement[Any]] | None = None,
|
|
@@ -190,7 +190,7 @@ from sqlmodel_ext.session import SessionFactory
|
|
|
190
190
|
|
|
191
191
|
| Member | Description |
|
|
192
192
|
|------|------|
|
|
193
|
-
| `commit(*, fail_soft_when_observed=False)` |
|
|
193
|
+
| `commit(*, fail_soft_when_observed=False)` | Changes to cached models (including bare `session.add()` / attribute changes / `session.delete()`) are registered by the flush that writes them -- earlier in the transaction or during this commit; after commit, invalidates **synchronously**, then runs the post-commit callbacks in order. `fail_soft_when_observed=True`: a failure of the commit itself is raised as usual; every step **after** the commit (invalidation, each callback) is individually fault-tolerant (catches `BaseException` including cancellation, logs, continues). Default: errors raised by the invalidation step and cancellation propagate; a callback's `Exception` is logged and skipped. **Redis failures during invalidation are logged, not raised, in both modes** — the database has committed, so the affected cache entries serve pre-commit data until their TTL expires; use `no_cache=True` for reads that must not rely on the cache |
|
|
194
194
|
| `commit_count` (property) | Number of successful commits on this session. The single source of truth for "was it committed": record a baseline before a critical write, and `commit_count > baseline` afterwards means the database has committed (a sufficient but not necessary signal) |
|
|
195
195
|
| `add_post_commit_callback(callback)` | Registers an `async` zero-argument callback, run **only after the next real commit**; discarded by `rollback()` / `reset()` / `close()`; registering inside a savepoint → `RuntimeError`; a failing callback doesn't block later callbacks nor change the commit result |
|
|
196
196
|
| `rollback(*, best_effort_budget_seconds=None)` | Rolls back + discards pending callbacks. Passing a budget enters **bounded abandon** mode: rolls back under `asyncio.timeout`, and on failure or timeout does a best-effort `invalidate()` of the connection, **without raising** (`CancelledError` still propagates). Not a hard cap (`invalidate` may still wait inside the driver) |
|
|
@@ -358,7 +358,7 @@ from sqlmodel_ext.field_types.dialects.postgresql import JSON100K, JSONList100K,
|
|
|
358
358
|
|
|
359
359
|
Contract: **object in, object out**.
|
|
360
360
|
|
|
361
|
-
- **Inbound**: accepts a JSON object / array (preferred) or a JSON **string** (compatibility form). Both forms must be encodable (nesting within the limits of both serializers, orjson and Pydantic — Pydantic's is about 98 levels) with a canonical encoding of at most 100K **characters** (counted in characters, not UTF-8 bytes, so CJK text is not rejected 3x too early).
|
|
361
|
+
- **Inbound**: accepts a JSON object / array (preferred) or a JSON **string** (compatibility form). Both forms must be encodable (nesting within the limits of both serializers, orjson and Pydantic — Pydantic's is platform-dependent, about 98 levels on Windows builds and higher on Linux) with a canonical encoding of at most 100K **characters** (counted in characters, not UTF-8 bytes, so CJK text is not rejected 3x too early).
|
|
362
362
|
- **Outbound**: `model_dump()`, `model_dump(mode='json')` and `model_dump_json()` all output the object / array itself (since 0.5.0; it used to be a JSON string). The serialization schema declares a pure object / array; the validation schema honestly declares `anyOf[object, string]`.
|
|
363
363
|
- **Table models are checked too**: `table=True` models skip Pydantic validation, so `SQLModelBase.model_post_init` calls `ensure_json_within_limits` on these fields (call `super()` when overriding `model_post_init`). Code paths that bypass models can call `ensure_json_within_limits(value)` directly.
|
|
364
364
|
- The 100K limit does not appear in the JSON Schema (JSON Schema cannot express an encoded length for objects); document it in the field docstring.
|
|
@@ -15,7 +15,7 @@ from sqlmodel_ext import (
|
|
|
15
15
|
PolymorphicBaseMixin, AutoPolymorphicIdentityMixin,
|
|
16
16
|
create_subclass_id_mixin,
|
|
17
17
|
RelationPreloadMixin, requires_relations, requires_for_update,
|
|
18
|
-
ListResponse, TableViewRequest, PaginationRequest, TimeFilterRequest,
|
|
18
|
+
ListResponse, TableViewRequest, PaginationRequest, TimeFilterRequest, query_dependency,
|
|
19
19
|
Str64, Port, HttpUrl, SafeHttpUrl, IPAddress, ...
|
|
20
20
|
)
|
|
21
21
|
```
|
|
@@ -29,7 +29,7 @@ from sqlmodel_ext import (
|
|
|
29
29
|
| [Field types](./field-types) | `Str16`–`Text1M`, `Port`, `Percentage`, `PositiveInt`, `HttpUrl`, `SafeHttpUrl`, `IPAddress`, `Array[T]`, `JSON100K`, `NumpyVector` |
|
|
30
30
|
| [Mixins](./mixins) | `CachedTableBaseMixin`, `OptimisticLockMixin`, `PolymorphicBaseMixin`, `AutoPolymorphicIdentityMixin`, `RelationPreloadMixin`, info response mixins |
|
|
31
31
|
| [Decorators & helpers](./decorators) | `@requires_relations`, `@requires_for_update`, `rel()`, `cond()`, enhanced `session.reset()` |
|
|
32
|
-
| [Pagination types](./pagination-types) | `ListResponse[T]`, `TableViewRequest`, `PaginationRequest`, `TimeFilterRequest` |
|
|
32
|
+
| [Pagination types](./pagination-types) | `ListResponse[T]`, `TableViewRequest`, `PaginationRequest`, `TimeFilterRequest`, `query_dependency()` |
|
|
33
33
|
|
|
34
34
|
## Constants
|
|
35
35
|
|
|
@@ -171,7 +171,7 @@ from sqlmodel_ext import PolymorphicBaseMixin
|
|
|
171
171
|
|
|
172
172
|
```python
|
|
173
173
|
@classmethod
|
|
174
|
-
def
|
|
174
|
+
def is_joined_table_inheritance(cls) -> bool
|
|
175
175
|
|
|
176
176
|
@classmethod
|
|
177
177
|
def get_concrete_subclasses(cls) -> list[type[PolymorphicBaseMixin]]
|