plain.postgres 0.104.0__tar.gz → 0.105.0__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.
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/PKG-INFO +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/CHANGELOG.md +14 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +22 -3
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/base.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/base.py +15 -4
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/binary.py +3 -1
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/boolean.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/duration.py +3 -1
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/encrypted.py +3 -1
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/network.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/numeric.py +7 -5
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/temporal.py +9 -3
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/text.py +4 -4
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/timezones.py +3 -1
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/uuid.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/types.py +7 -7
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/types.pyi +142 -58
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/pyproject.toml +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/models/constraints.py +2 -2
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/models/defaults.py +8 -11
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/models/delete.py +17 -29
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/models/encrypted.py +4 -4
- plain_postgres-0.105.0/tests/app/examples/models/forms.py +31 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/models/indexes.py +2 -2
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/models/iteration.py +2 -2
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/models/mixins.py +3 -5
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/models/nullability.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/models/querysets.py +3 -3
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/models/relationships.py +4 -4
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/models/storage_parameters.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/models/trees.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_autodetector_not_null_errors.py +15 -15
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_autodetector_type_change.py +6 -6
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_fk_characterization.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_delete_behaviors.py +1 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_related.py +2 -2
- plain_postgres-0.104.0/tests/app/examples/models/forms.py +0 -35
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/.gitignore +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/CLAUDE.md +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/LICENSE +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/README.md +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/README.md +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/adapters.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/cli/converge.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/cli/core.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/cli/decorators.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/cli/migrations.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/cli/schema.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/cli/sync.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/connection.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/constraints.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/convergence/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/convergence/analysis.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/convergence/fixes.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/convergence/planning.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/db.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/ddl.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/dialect.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/json.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/primary_key.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/related.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/related_managers.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/fields/reverse_related.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/forms.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/functions/random.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/functions/uuid.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/indexes.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/introspection/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/introspection/health/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/introspection/health/context.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/introspection/health/helpers.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/introspection/health/ownership.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/introspection/health/runner.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/introspection/health/types.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/introspection/schema.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/meta.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/middleware.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/autodetector.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/operations/base.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/state.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/options.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/preflight.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/query.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/query_utils.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/schema.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/sources.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/sql/compiler.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/sql/query.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/test/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/test/database.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/test/pytest.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/transaction.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/plain/postgres/utils.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/forms.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/models/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/models/unregistered.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/urls.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/examples/views.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/app/urls.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/conftest.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/conftest_convergence.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_connection_isolation.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_connection_lifecycle.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_connection_pool.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_constraint_violation_error.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_convergence.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_convergence_constraints.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_convergence_defaults.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_convergence_fk.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_convergence_indexes.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_convergence_nullability.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_convergence_storage_parameters.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_convergence_timeouts.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_db_expression_defaults.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_diagnose.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_executor_connection_hook.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_health.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_introspection.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_literal_default_persistence.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_management_connection.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_migration_executor.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_no_callable_defaults.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_otel_metrics.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_schema_normalize_type.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/internal/test_schema_timeouts.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_database_url.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_deferred_loading.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_encrypted_fields.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_exceptions.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_field_defaults.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_functions_uuid.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_iterator.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_m2m.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_manager_assignment.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_mixins.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_modelform_roundtrip.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_queryset_repr.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_random_string_field.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_raw_query.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.105.0}/tests/public/test_read_only_transactions.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.105.0](https://github.com/dropseed/plain/releases/plain-postgres@0.105.0) (2026-05-25)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- **Field constructors in `plain.postgres.types` are now typed as parameterized descriptors.** Stubs return `XField[T]` (e.g. `TextField[str]`, `IntegerField[int | None]` when `allow_null=True`) instead of the primitive value. Combined with `Field[T]`'s overloaded `__get__`, class-level access (`User.email`) sees the descriptor and instance access (`user.email`) sees the value — no annotation needed on model declarations. ([229ecdbbfa](https://github.com/dropseed/plain/commit/229ecdbbfa))
|
|
8
|
+
- **`Field.__set__` is now typed as `value: T`** instead of `Any`, so a type checker flags `row.name = 123` on a `TextField` at the call site. Runtime conversion via `to_python()` is unchanged. ([229ecdbbfa](https://github.com/dropseed/plain/commit/229ecdbbfa))
|
|
9
|
+
- **`ForeignKeyField` overloads split by `to=` argument shape.** Passing a model class (`to=Author`) returns `_ForeignKeyDescriptor[T, V]` and accepts bare-PK assignment (`book.author = 5`) via `__set__`. Passing a string forward reference (`to="Author"`) returns the bare value type and requires an LHS annotation so the type can bind. ([229ecdbbfa](https://github.com/dropseed/plain/commit/229ecdbbfa))
|
|
10
|
+
- `JSONField` and `EncryptedJSONField` keep explicit annotations on model declarations — their runtime classes aren't generic, so the stubs return `Any`. ([229ecdbbfa](https://github.com/dropseed/plain/commit/229ecdbbfa))
|
|
11
|
+
|
|
12
|
+
### Upgrade instructions
|
|
13
|
+
|
|
14
|
+
- **Drop primitive type annotations on model field declarations.** `name: str = types.TextField()` will now fail type checking because the right-hand side is typed `TextField[str]`, not `str`. Change to `name = types.TextField()` — type checkers still infer `str` at instance access through the descriptor's `__get__`. Runtime is unchanged either way.
|
|
15
|
+
- **Keep explicit annotations for `JSONField` / `EncryptedJSONField`** — these still return `Any` from the stubs, so the declared type is what flows through. Example: `parameters: dict[str, Any] | None = types.JSONField(required=False, allow_null=True)`.
|
|
16
|
+
|
|
3
17
|
## [0.104.0](https://github.com/dropseed/plain/releases/plain-postgres@0.104.0) (2026-05-22)
|
|
4
18
|
|
|
5
19
|
### What's changed
|
|
@@ -7,13 +7,32 @@ paths:
|
|
|
7
7
|
|
|
8
8
|
## Field Imports
|
|
9
9
|
|
|
10
|
-
Import fields via `from plain.postgres import types
|
|
10
|
+
Import fields via `from plain.postgres import types`. Don't add primitive
|
|
11
|
+
annotations — the field stubs return typed descriptors that resolve to the
|
|
12
|
+
right value type on instance access:
|
|
11
13
|
|
|
12
14
|
```python
|
|
13
15
|
from plain.postgres import types
|
|
14
16
|
|
|
15
|
-
name
|
|
16
|
-
car
|
|
17
|
+
name = types.TextField(max_length=100)
|
|
18
|
+
car = types.ForeignKeyField(Car, on_delete=postgres.CASCADE)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For string forward references (`"self"`, `"OtherModel"`), the type checker
|
|
22
|
+
can't infer the target type from the string — annotate explicitly so
|
|
23
|
+
instance access keeps its type:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
parent: TreeNode | None = types.ForeignKeyField("self", on_delete=postgres.CASCADE, allow_null=True)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For `JSONField` and `EncryptedJSONField`, the stub returns `Any` (the
|
|
30
|
+
runtime class isn't generic over its value shape), so annotate explicitly
|
|
31
|
+
to preserve typing:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
parameters: dict[str, Any] | None = types.JSONField(required=False, allow_null=True)
|
|
35
|
+
config: dict | None = types.EncryptedJSONField(required=False, allow_null=True)
|
|
17
36
|
```
|
|
18
37
|
|
|
19
38
|
Do NOT import field classes directly from `plain.postgres` or `plain.postgres.fields`.
|
|
@@ -437,12 +437,18 @@ class Field[T](RegisterLookupMixin):
|
|
|
437
437
|
|
|
438
438
|
return cast(T, data.get(field_name))
|
|
439
439
|
|
|
440
|
-
def __set__(self, instance: Model, value:
|
|
440
|
+
def __set__(self, instance: Model, value: T) -> None:
|
|
441
441
|
"""
|
|
442
442
|
Descriptor __set__ for attribute assignment.
|
|
443
443
|
|
|
444
444
|
Validates and converts the value using to_python(), then stores it
|
|
445
445
|
in instance.__dict__[name].
|
|
446
|
+
|
|
447
|
+
The parameter is typed `T` (the field's value type) so a type checker
|
|
448
|
+
rejects assigning incompatible types — `row.name = 123` on a
|
|
449
|
+
TextField is caught at the call site. The runtime is more permissive
|
|
450
|
+
(to_python converts strings → ints etc.), but encouraging explicit
|
|
451
|
+
conversion at the boundary is the better default.
|
|
446
452
|
"""
|
|
447
453
|
# Safety check: ensure field has been properly initialized
|
|
448
454
|
if not hasattr(self, "column"):
|
|
@@ -454,12 +460,17 @@ class Field[T](RegisterLookupMixin):
|
|
|
454
460
|
|
|
455
461
|
# Convert/validate the value. The DATABASE_DEFAULT sentinel is stored
|
|
456
462
|
# as-is so the INSERT compiler can emit `DEFAULT` in the VALUES clause.
|
|
457
|
-
|
|
458
|
-
|
|
463
|
+
# Use a separate local so the parameter's narrow `T` type isn't
|
|
464
|
+
# widened by to_python's `T | None` return.
|
|
465
|
+
stored: Any
|
|
466
|
+
if value is None or value is DATABASE_DEFAULT:
|
|
467
|
+
stored = value
|
|
468
|
+
else:
|
|
469
|
+
stored = self.to_python(value)
|
|
459
470
|
|
|
460
471
|
# Store in instance dict
|
|
461
472
|
assert self.name is not None
|
|
462
|
-
instance.__dict__[self.name] =
|
|
473
|
+
instance.__dict__[self.name] = stored
|
|
463
474
|
|
|
464
475
|
def __delete__(self, instance: Model) -> None:
|
|
465
476
|
"""
|
|
@@ -15,7 +15,9 @@ if TYPE_CHECKING:
|
|
|
15
15
|
from plain.postgres.sql.compiler import SQLCompiler
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class BinaryField
|
|
18
|
+
class BinaryField[
|
|
19
|
+
T: (bytes | memoryview, bytes | memoryview | None) = bytes | memoryview
|
|
20
|
+
](ColumnField[T]):
|
|
19
21
|
db_type_sql = "bytea"
|
|
20
22
|
empty_values = [None, b""]
|
|
21
23
|
_default_empty_value = b""
|
|
@@ -13,7 +13,9 @@ if TYPE_CHECKING:
|
|
|
13
13
|
from plain.postgres.connection import DatabaseConnection
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class DurationField
|
|
16
|
+
class DurationField[
|
|
17
|
+
T: (datetime.timedelta, datetime.timedelta | None) = datetime.timedelta
|
|
18
|
+
](DefaultableField[T]):
|
|
17
19
|
"""Store timedelta objects using PostgreSQL's interval type."""
|
|
18
20
|
|
|
19
21
|
db_type_sql = "interval"
|
|
@@ -178,7 +178,9 @@ class EncryptedFieldMixin:
|
|
|
178
178
|
return errors
|
|
179
179
|
|
|
180
180
|
|
|
181
|
-
class EncryptedTextField(
|
|
181
|
+
class EncryptedTextField[T: (str, str | None) = str](
|
|
182
|
+
EncryptedFieldMixin, ColumnField[T]
|
|
183
|
+
):
|
|
182
184
|
"""A text field that encrypts its value before storing in the database.
|
|
183
185
|
|
|
184
186
|
Values are encrypted using Fernet (AES-128-CBC + HMAC-SHA256) with a key
|
|
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
|
|
15
15
|
from plain.postgres.connection import DatabaseConnection
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class GenericIPAddressField(DefaultableField[
|
|
18
|
+
class GenericIPAddressField[T: (str, str | None) = str](DefaultableField[T]):
|
|
19
19
|
db_type_sql = "inet"
|
|
20
20
|
empty_strings_allowed = False
|
|
21
21
|
|
|
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
|
|
16
16
|
from plain.postgres.connection import DatabaseConnection
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
class FloatField(DefaultableField[
|
|
19
|
+
class FloatField[T: (float, float | None) = float](DefaultableField[T]):
|
|
20
20
|
db_type_sql = "double precision"
|
|
21
21
|
empty_strings_allowed = False
|
|
22
22
|
|
|
@@ -44,7 +44,7 @@ class FloatField(DefaultableField[float]):
|
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
class IntegerField(DefaultableField[
|
|
47
|
+
class IntegerField[T: (int, int | None) = int](DefaultableField[T]):
|
|
48
48
|
db_type_sql = "integer"
|
|
49
49
|
integer_range: tuple[int, int] = (-2147483648, 2147483647)
|
|
50
50
|
psycopg_type: type = numeric.Int4
|
|
@@ -118,19 +118,21 @@ class IntegerField(DefaultableField[int]):
|
|
|
118
118
|
)
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
class BigIntegerField(IntegerField):
|
|
121
|
+
class BigIntegerField[T: (int, int | None) = int](IntegerField[T]):
|
|
122
122
|
db_type_sql = "bigint"
|
|
123
123
|
integer_range = (-9223372036854775808, 9223372036854775807)
|
|
124
124
|
psycopg_type = numeric.Int8
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
class SmallIntegerField(IntegerField):
|
|
127
|
+
class SmallIntegerField[T: (int, int | None) = int](IntegerField[T]):
|
|
128
128
|
db_type_sql = "smallint"
|
|
129
129
|
integer_range = (-32768, 32767)
|
|
130
130
|
psycopg_type = numeric.Int2
|
|
131
131
|
|
|
132
132
|
|
|
133
|
-
class DecimalField(
|
|
133
|
+
class DecimalField[T: (decimal.Decimal, decimal.Decimal | None) = decimal.Decimal](
|
|
134
|
+
DefaultableField[T]
|
|
135
|
+
):
|
|
134
136
|
db_type_sql = "numeric(%(max_digits)s,%(decimal_places)s)"
|
|
135
137
|
empty_strings_allowed = False
|
|
136
138
|
|
|
@@ -69,7 +69,9 @@ def _check_if_value_fixed(
|
|
|
69
69
|
return []
|
|
70
70
|
|
|
71
71
|
|
|
72
|
-
class DateField(
|
|
72
|
+
class DateField[T: (datetime.date, datetime.date | None) = datetime.date](
|
|
73
|
+
DefaultableField[T]
|
|
74
|
+
):
|
|
73
75
|
db_type_sql = "date"
|
|
74
76
|
empty_strings_allowed = False
|
|
75
77
|
|
|
@@ -149,7 +151,9 @@ class DateField(DefaultableField[datetime.date]):
|
|
|
149
151
|
return value
|
|
150
152
|
|
|
151
153
|
|
|
152
|
-
class DateTimeField
|
|
154
|
+
class DateTimeField[
|
|
155
|
+
T: (datetime.datetime, datetime.datetime | None) = datetime.datetime
|
|
156
|
+
](ColumnField[T]):
|
|
153
157
|
db_type_sql = "timestamp with time zone"
|
|
154
158
|
empty_strings_allowed = False
|
|
155
159
|
|
|
@@ -302,7 +306,9 @@ class DateTimeField(ColumnField[datetime.datetime]):
|
|
|
302
306
|
return value
|
|
303
307
|
|
|
304
308
|
|
|
305
|
-
class TimeField(
|
|
309
|
+
class TimeField[T: (datetime.time, datetime.time | None) = datetime.time](
|
|
310
|
+
DefaultableField[T]
|
|
311
|
+
):
|
|
306
312
|
db_type_sql = "time without time zone"
|
|
307
313
|
empty_strings_allowed = False
|
|
308
314
|
|
|
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
|
|
13
13
|
from plain.postgres.functions.random import RandomString
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class TextField(ChoicesField[
|
|
16
|
+
class TextField[T: (str, str | None) = str](ChoicesField[T]):
|
|
17
17
|
db_type_sql = "text"
|
|
18
18
|
|
|
19
19
|
def __init__(
|
|
@@ -87,15 +87,15 @@ class TextField(ChoicesField[str]):
|
|
|
87
87
|
return self.to_python(value)
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
class EmailField(TextField):
|
|
90
|
+
class EmailField[T: (str, str | None) = str](TextField[T]):
|
|
91
91
|
default_validators = [validators.validate_email]
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
class URLField(TextField):
|
|
94
|
+
class URLField[T: (str, str | None) = str](TextField[T]):
|
|
95
95
|
default_validators = [validators.URLValidator()]
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
class RandomStringField(ColumnField[
|
|
98
|
+
class RandomStringField[T: (str, str | None) = str](ColumnField[T]):
|
|
99
99
|
"""Text column whose value is a Postgres-generated random hex string.
|
|
100
100
|
|
|
101
101
|
The column carries a ``DEFAULT`` that evaluates per row, so raw SQL and
|
|
@@ -47,7 +47,9 @@ def _get_canonical_timezones() -> frozenset[str]:
|
|
|
47
47
|
)
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
class TimeZoneField
|
|
50
|
+
class TimeZoneField[
|
|
51
|
+
T: (zoneinfo.ZoneInfo, zoneinfo.ZoneInfo | None) = zoneinfo.ZoneInfo
|
|
52
|
+
](ChoicesField[T]):
|
|
51
53
|
"""
|
|
52
54
|
A model field that stores timezone names as strings but provides ZoneInfo objects.
|
|
53
55
|
|
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
Typed field imports for better IDE and type checker support.
|
|
3
3
|
|
|
4
4
|
This module provides the same field classes as plain.postgres.fields,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
with a companion .pyi stub file that types each constructor as the
|
|
6
|
+
typed descriptor (`XField[T]`). Combined with the descriptor's overloaded
|
|
7
|
+
`__get__`, type checkers see the field reference at the class level and
|
|
8
|
+
the primitive value at instance access — no annotation needed.
|
|
7
9
|
|
|
8
10
|
Usage:
|
|
9
11
|
from plain.postgres import types
|
|
10
12
|
|
|
11
13
|
@postgres.register_model
|
|
12
14
|
class User(postgres.Model):
|
|
13
|
-
email
|
|
14
|
-
age
|
|
15
|
-
is_active
|
|
16
|
-
|
|
17
|
-
This is optional - you can continue using untyped field definitions.
|
|
15
|
+
email = types.EmailField()
|
|
16
|
+
age = types.IntegerField()
|
|
17
|
+
is_active = types.BooleanField(default=True)
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
# Re-export scalar field types
|