plain.postgres 0.104.0__tar.gz → 0.106.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.106.0}/PKG-INFO +41 -13
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/CHANGELOG.md +32 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/README.md +40 -12
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +24 -3
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/base.py +200 -376
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/constraints.py +65 -2
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/base.py +15 -4
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/binary.py +3 -1
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/boolean.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/duration.py +3 -1
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/encrypted.py +3 -1
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/network.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/numeric.py +7 -5
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/related_managers.py +7 -2
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/temporal.py +9 -3
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/text.py +4 -4
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/timezones.py +3 -1
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/uuid.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/forms.py +16 -22
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/meta.py +18 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/query.py +9 -30
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/sql/compiler.py +1 -3
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/sql/query.py +1 -4
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/transaction.py +2 -2
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/types.py +7 -7
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/types.pyi +142 -58
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/pyproject.toml +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0002_test_field_removed.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/models/constraints.py +2 -2
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/models/defaults.py +8 -11
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/models/delete.py +17 -29
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/models/encrypted.py +4 -4
- plain_postgres-0.106.0/tests/app/examples/models/forms.py +31 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/models/indexes.py +2 -2
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/models/iteration.py +2 -2
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/models/mixins.py +3 -5
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/models/nullability.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/models/querysets.py +3 -3
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/models/relationships.py +4 -4
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/models/storage_parameters.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/models/trees.py +1 -1
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_autodetector_not_null_errors.py +15 -15
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_autodetector_type_change.py +6 -6
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_constraint_violation_error.py +143 -17
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_db_expression_defaults.py +24 -44
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_fk_characterization.py +6 -6
- plain_postgres-0.106.0/tests/public/test_create_update.py +256 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_delete_behaviors.py +14 -8
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_encrypted_fields.py +1 -1
- plain_postgres-0.106.0/tests/public/test_integrity_error_mapping.py +64 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_m2m.py +9 -6
- plain_postgres-0.106.0/tests/public/test_manual_pk.py +48 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_related.py +9 -9
- plain_postgres-0.104.0/tests/app/examples/models/forms.py +0 -35
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/.gitignore +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/CLAUDE.md +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/LICENSE +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/README.md +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/adapters.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/cli/converge.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/cli/core.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/cli/decorators.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/cli/migrations.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/cli/schema.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/cli/sync.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/connection.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/convergence/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/convergence/analysis.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/convergence/fixes.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/convergence/planning.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/db.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/ddl.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/deletion.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/dialect.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/exceptions.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/expressions.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/json.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/primary_key.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/related.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/related_descriptors.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/reverse_descriptors.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/fields/reverse_related.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/functions/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/functions/datetime.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/functions/math.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/functions/mixins.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/functions/random.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/functions/text.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/functions/uuid.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/indexes.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/introspection/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/context.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/helpers.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/ownership.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/runner.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/introspection/health/types.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/introspection/schema.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/lookups.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/middleware.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/autodetector.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/exceptions.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/executor.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/graph.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/loader.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/migration.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/operations/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/operations/base.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/operations/fields.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/operations/models.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/operations/special.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/questioner.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/recorder.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/serializer.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/state.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/utils.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/migrations/writer.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/options.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/otel.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/preflight.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/query_utils.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/registry.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/schema.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/sources.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/sql/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/test/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/test/database.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/test/pytest.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/plain/postgres/utils.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/forms.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0006_secretstore.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0010_hideableitem.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0011_defaultsexample.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0012_iterationexample.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.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.106.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0016_formsexample.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0017_random_string_token.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/0018_storageparametersexample.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/models/__init__.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/models/unregistered.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/urls.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/examples/views.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/app/urls.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/conftest.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/conftest_convergence.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_connection_isolation.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_connection_lifecycle.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_connection_pool.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_convergence.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_convergence_constraints.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_convergence_defaults.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_convergence_fk.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_convergence_indexes.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_convergence_nullability.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_convergence_storage_parameters.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_convergence_timeouts.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_diagnose.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_executor_connection_hook.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_health.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_introspection.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_literal_default_persistence.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_management_connection.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_migration_executor.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_no_callable_defaults.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_otel_metrics.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_schema_normalize_type.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/internal/test_schema_timeouts.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_database_url.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_deferred_loading.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_exceptions.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_field_defaults.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_functions_uuid.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_iterator.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_manager_assignment.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_mixins.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_modelform_roundtrip.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_queryset_repr.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_random_string_field.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_raw_query.py +0 -0
- {plain_postgres-0.104.0 → plain_postgres-0.106.0}/tests/public/test_read_only_transactions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plain.postgres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.106.0
|
|
4
4
|
Summary: Model your data and store it in a database.
|
|
5
5
|
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -74,7 +74,7 @@ user = User.query.create(
|
|
|
74
74
|
|
|
75
75
|
# Update a user
|
|
76
76
|
user.email = "new@example.com"
|
|
77
|
-
user.
|
|
77
|
+
user.update()
|
|
78
78
|
|
|
79
79
|
# Delete a user
|
|
80
80
|
user.delete()
|
|
@@ -388,12 +388,12 @@ total = User.query.count()
|
|
|
388
388
|
|
|
389
389
|
#### Use `bulk_create` / `bulk_update` for batch operations
|
|
390
390
|
|
|
391
|
-
Avoid calling `.
|
|
391
|
+
Avoid calling `.create()` in a loop — each call is a separate query.
|
|
392
392
|
|
|
393
393
|
```python
|
|
394
394
|
# Bad — N INSERT statements
|
|
395
395
|
for name in names:
|
|
396
|
-
Tag(name=name).
|
|
396
|
+
Tag(name=name).create()
|
|
397
397
|
|
|
398
398
|
# Good — single INSERT
|
|
399
399
|
Tag.query.bulk_create([Tag(name=name) for name in names])
|
|
@@ -405,7 +405,7 @@ Tag.query.bulk_create([Tag(name=name) for name in names])
|
|
|
405
405
|
# Bad — N UPDATE statements
|
|
406
406
|
for user in User.query.filter(is_active=False):
|
|
407
407
|
user.is_archived = True
|
|
408
|
-
user.
|
|
408
|
+
user.update()
|
|
409
409
|
|
|
410
410
|
# Good — single UPDATE statement
|
|
411
411
|
User.query.filter(is_active=False).update(is_archived=True)
|
|
@@ -452,16 +452,16 @@ from plain.postgres import transaction
|
|
|
452
452
|
|
|
453
453
|
with transaction.atomic():
|
|
454
454
|
user = User(email="test@example.com")
|
|
455
|
-
user.
|
|
456
|
-
Profile(user=user).
|
|
457
|
-
# Both
|
|
455
|
+
user.create()
|
|
456
|
+
Profile(user=user).create()
|
|
457
|
+
# Both writes commit together, or both roll back on error
|
|
458
458
|
```
|
|
459
459
|
|
|
460
460
|
Nesting `atomic()` creates savepoints:
|
|
461
461
|
|
|
462
462
|
```python
|
|
463
463
|
with transaction.atomic():
|
|
464
|
-
user.
|
|
464
|
+
user.create()
|
|
465
465
|
try:
|
|
466
466
|
with transaction.atomic():
|
|
467
467
|
risky_operation() # If this fails...
|
|
@@ -1033,7 +1033,7 @@ author.books.query.published()
|
|
|
1033
1033
|
|
|
1034
1034
|
### Validation
|
|
1035
1035
|
|
|
1036
|
-
`
|
|
1036
|
+
`create()` and `update()` run `full_clean()` by default — field validators and the model's `clean()` method — raising `ValidationError` on violation. Pass `clean_and_validate=False` to skip it (e.g. for trusted bulk loads). Constraints are _not_ pre-checked here; the database enforces them (see below).
|
|
1037
1037
|
|
|
1038
1038
|
```python
|
|
1039
1039
|
@postgres.register_model
|
|
@@ -1054,6 +1054,33 @@ class User(postgres.Model):
|
|
|
1054
1054
|
|
|
1055
1055
|
Field-level validation happens automatically based on field types and constraints.
|
|
1056
1056
|
|
|
1057
|
+
**The database is authoritative for constraints.** `create()`/`update()` don't pre-check your declared unique/check constraints — they attempt the write, and if Postgres rejects it, translate the `IntegrityError` into a `ValidationError` (routed to the field for single-column uniques, `NON_FIELD_ERRORS` otherwise). You get the same field-level error you'd expect, the write costs no per-constraint `SELECT`, and a raced concurrent insert can't slip through as a 500. (FK violations, `NOT NULL`, and a hand-set primary-key collision have no declared constraint to map to and re-raise as the original `IntegrityError`. `create()` always inserts, so passing a stray `id` that already exists is rejected by Postgres as the original `IntegrityError`.)
|
|
1058
|
+
|
|
1059
|
+
Because the rejected write reaches the database, it aborts the surrounding transaction. If you catch the `ValidationError` and want to keep using the transaction, wrap the write in `transaction.atomic()` so it rolls back to a savepoint:
|
|
1060
|
+
|
|
1061
|
+
```python
|
|
1062
|
+
try:
|
|
1063
|
+
with transaction.atomic():
|
|
1064
|
+
User.query.create(email=taken_email)
|
|
1065
|
+
except ValidationError:
|
|
1066
|
+
... # report it — the transaction is still usable
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
Forms are the exception: a `ModelForm` pre-checks constraints explicitly (a `validate_constraints()` call in its `_post_clean`) so it can surface every violation at once, then saves with validation already done. A direct `create()`/`update()` reports the first violation Postgres hits.
|
|
1070
|
+
|
|
1071
|
+
This applies to instance writes only. Set-based writes — `QuerySet.update()` and `bulk_create()` — raise the raw `psycopg.IntegrityError`, since there's no instance to attribute the error to. If you retry on a unique conflict, catch both:
|
|
1072
|
+
|
|
1073
|
+
```python
|
|
1074
|
+
try:
|
|
1075
|
+
obj.create()
|
|
1076
|
+
except (psycopg.IntegrityError, ValidationError):
|
|
1077
|
+
... # lost a race — reload and retry, or report it
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
For a plain insert-or-update with no per-row logic, `bulk_create(..., update_conflicts=True, unique_fields=[...])` is an atomic upsert with no race to catch.
|
|
1081
|
+
|
|
1082
|
+
Two caveats. The mapping covers **immediate** constraints — the default. An explicitly deferred constraint (`UniqueConstraint(deferrable=Deferrable.DEFERRED)`) is checked at commit, _after_ the write returns, so its violation still surfaces as a raw `psycopg.IntegrityError`. And when a row violates several constraints at once, a form's pre-check (or an explicit `validate_constraints()`) reports them all, while a direct `create()`/`update()` gets only the first one the database hits.
|
|
1083
|
+
|
|
1057
1084
|
### Indexes and constraints
|
|
1058
1085
|
|
|
1059
1086
|
You can optimize queries and ensure data integrity with indexes and constraints. These are managed automatically by [convergence](#convergence) — just declare them on the model and run `postgres sync`.
|
|
@@ -1076,7 +1103,7 @@ class User(postgres.Model):
|
|
|
1076
1103
|
)
|
|
1077
1104
|
```
|
|
1078
1105
|
|
|
1079
|
-
Constraints are
|
|
1106
|
+
Constraints are checked by `validate_constraints()` — run by a `ModelForm` (and any explicit `validate_constraints()` call), but **not** by `full_clean()` (which validates shape only) or a direct `create()`/`update()`, where the database enforces them instead (see [Validation](#validation)). Pass `violation_error` to customize the resulting `ValidationError`. It accepts anything `ValidationError(...)` accepts — a string, a `{field: message}` dict, or a fully-formed `ValidationError`:
|
|
1080
1107
|
|
|
1081
1108
|
```python
|
|
1082
1109
|
# Simple message — lands on NON_FIELD_ERRORS
|
|
@@ -1155,10 +1182,11 @@ class Order(postgres.Model):
|
|
|
1155
1182
|
Enforce uniqueness and data integrity at the database level.
|
|
1156
1183
|
|
|
1157
1184
|
```python
|
|
1158
|
-
# Bad — only validated in Python
|
|
1159
|
-
def
|
|
1185
|
+
# Bad — only validated in Python (racy: two requests can both pass the check)
|
|
1186
|
+
def create(self):
|
|
1160
1187
|
if MyModel.query.filter(email=self.email).exists():
|
|
1161
1188
|
raise ValueError("duplicate")
|
|
1189
|
+
return super().create()
|
|
1162
1190
|
|
|
1163
1191
|
# Good — database-enforced
|
|
1164
1192
|
model_options = postgres.Options(
|
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.106.0](https://github.com/dropseed/plain/releases/plain-postgres@0.106.0) (2026-06-03)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- **`Model.save()` is replaced by explicit `create()` (always INSERT) and `update()` (always UPDATE).** A call site now says which it does. `create()` inserts a new row and returns `self`; `update()` writes an existing row and returns `self`, raising if no row matched (there is no silent INSERT fallback). `update(fields=[...])` limits the columns written (replacing `save(update_fields=[...])`). `save()`, `save_base()`, and the `force_insert`/`force_update` arguments are removed. ([f75deb3ba2](https://github.com/dropseed/plain/commit/f75deb3ba2))
|
|
8
|
+
- **Constructing a model with `id=` now raises.** A hand-set `id` at construction almost always means "load the existing row" — use `Model.query.get(id=...)`. A freshly constructed instance is unambiguously new. ([9c0acf7bac](https://github.com/dropseed/plain/commit/9c0acf7bac), [661c7806df](https://github.com/dropseed/plain/commit/661c7806df))
|
|
9
|
+
- **Declared unique/check constraint violations now raise `ValidationError`, not raw `psycopg.IntegrityError`** — mapped at the write boundary, so even a violation that races past validation surfaces as the same field-level error a pre-check would raise. The database is now the authority; the pre-check `SELECT` is no longer required for correctness. Set-based writes (`bulk_create`, `QuerySet.update`) still raise raw `psycopg` errors. ([40c97521c5](https://github.com/dropseed/plain/commit/40c97521c5), [bdbdd524e5](https://github.com/dropseed/plain/commit/bdbdd524e5))
|
|
10
|
+
- **`full_clean()` is now shape-only** (`clean_fields()` + `clean()`); the integrity pre-check moved to `validate_constraints()`, called explicitly. `full_clean()`'s `validate_unique`/`validate_constraints` parameters and the `Model.validate_unique()` / `_get_unique_checks` / `_perform_unique_checks` trio are removed; forms call `full_clean()` then `validate_constraints()`. ([ba4b386fb7](https://github.com/dropseed/plain/commit/ba4b386fb7), [65d56945fb](https://github.com/dropseed/plain/commit/65d56945fb))
|
|
11
|
+
- `delete()` clears the instance's `id` and resets it to "new," so `create()` re-inserts it cleanly; the instance's other field values survive the delete. ([f75deb3ba2](https://github.com/dropseed/plain/commit/f75deb3ba2))
|
|
12
|
+
|
|
13
|
+
### Upgrade instructions
|
|
14
|
+
|
|
15
|
+
- Replace `obj.save()` with `obj.create()` for a new row or `obj.update()` for an existing one; `obj.save(update_fields=[...])` becomes `obj.update(fields=[...])`. The `/plain-upgrade` skill rewrites these for you — the create-vs-update choice is context-dependent, so it reasons per call site.
|
|
16
|
+
- Remove `force_insert=`/`force_update=` — `create()` and `update()` are the explicit forms.
|
|
17
|
+
- Don't pass `id=` to a model constructor; load existing rows with `Model.query.get(id=...)`.
|
|
18
|
+
- Catch `ValidationError` (or `(psycopg.IntegrityError, ValidationError)` when retrying) for unique/check conflicts on instance writes. Inside an open `transaction.atomic()` a violation aborts the transaction — wrap the write in its own `atomic()` to catch and continue.
|
|
19
|
+
- Direct `full_clean()` callers that relied on constraint pre-checking should call `validate_constraints()` explicitly.
|
|
20
|
+
|
|
21
|
+
## [0.105.0](https://github.com/dropseed/plain/releases/plain-postgres@0.105.0) (2026-05-25)
|
|
22
|
+
|
|
23
|
+
### What's changed
|
|
24
|
+
|
|
25
|
+
- **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))
|
|
26
|
+
- **`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))
|
|
27
|
+
- **`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))
|
|
28
|
+
- `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))
|
|
29
|
+
|
|
30
|
+
### Upgrade instructions
|
|
31
|
+
|
|
32
|
+
- **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.
|
|
33
|
+
- **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)`.
|
|
34
|
+
|
|
3
35
|
## [0.104.0](https://github.com/dropseed/plain/releases/plain-postgres@0.104.0) (2026-05-22)
|
|
4
36
|
|
|
5
37
|
### What's changed
|
|
@@ -61,7 +61,7 @@ user = User.query.create(
|
|
|
61
61
|
|
|
62
62
|
# Update a user
|
|
63
63
|
user.email = "new@example.com"
|
|
64
|
-
user.
|
|
64
|
+
user.update()
|
|
65
65
|
|
|
66
66
|
# Delete a user
|
|
67
67
|
user.delete()
|
|
@@ -375,12 +375,12 @@ total = User.query.count()
|
|
|
375
375
|
|
|
376
376
|
#### Use `bulk_create` / `bulk_update` for batch operations
|
|
377
377
|
|
|
378
|
-
Avoid calling `.
|
|
378
|
+
Avoid calling `.create()` in a loop — each call is a separate query.
|
|
379
379
|
|
|
380
380
|
```python
|
|
381
381
|
# Bad — N INSERT statements
|
|
382
382
|
for name in names:
|
|
383
|
-
Tag(name=name).
|
|
383
|
+
Tag(name=name).create()
|
|
384
384
|
|
|
385
385
|
# Good — single INSERT
|
|
386
386
|
Tag.query.bulk_create([Tag(name=name) for name in names])
|
|
@@ -392,7 +392,7 @@ Tag.query.bulk_create([Tag(name=name) for name in names])
|
|
|
392
392
|
# Bad — N UPDATE statements
|
|
393
393
|
for user in User.query.filter(is_active=False):
|
|
394
394
|
user.is_archived = True
|
|
395
|
-
user.
|
|
395
|
+
user.update()
|
|
396
396
|
|
|
397
397
|
# Good — single UPDATE statement
|
|
398
398
|
User.query.filter(is_active=False).update(is_archived=True)
|
|
@@ -439,16 +439,16 @@ from plain.postgres import transaction
|
|
|
439
439
|
|
|
440
440
|
with transaction.atomic():
|
|
441
441
|
user = User(email="test@example.com")
|
|
442
|
-
user.
|
|
443
|
-
Profile(user=user).
|
|
444
|
-
# Both
|
|
442
|
+
user.create()
|
|
443
|
+
Profile(user=user).create()
|
|
444
|
+
# Both writes commit together, or both roll back on error
|
|
445
445
|
```
|
|
446
446
|
|
|
447
447
|
Nesting `atomic()` creates savepoints:
|
|
448
448
|
|
|
449
449
|
```python
|
|
450
450
|
with transaction.atomic():
|
|
451
|
-
user.
|
|
451
|
+
user.create()
|
|
452
452
|
try:
|
|
453
453
|
with transaction.atomic():
|
|
454
454
|
risky_operation() # If this fails...
|
|
@@ -1020,7 +1020,7 @@ author.books.query.published()
|
|
|
1020
1020
|
|
|
1021
1021
|
### Validation
|
|
1022
1022
|
|
|
1023
|
-
`
|
|
1023
|
+
`create()` and `update()` run `full_clean()` by default — field validators and the model's `clean()` method — raising `ValidationError` on violation. Pass `clean_and_validate=False` to skip it (e.g. for trusted bulk loads). Constraints are _not_ pre-checked here; the database enforces them (see below).
|
|
1024
1024
|
|
|
1025
1025
|
```python
|
|
1026
1026
|
@postgres.register_model
|
|
@@ -1041,6 +1041,33 @@ class User(postgres.Model):
|
|
|
1041
1041
|
|
|
1042
1042
|
Field-level validation happens automatically based on field types and constraints.
|
|
1043
1043
|
|
|
1044
|
+
**The database is authoritative for constraints.** `create()`/`update()` don't pre-check your declared unique/check constraints — they attempt the write, and if Postgres rejects it, translate the `IntegrityError` into a `ValidationError` (routed to the field for single-column uniques, `NON_FIELD_ERRORS` otherwise). You get the same field-level error you'd expect, the write costs no per-constraint `SELECT`, and a raced concurrent insert can't slip through as a 500. (FK violations, `NOT NULL`, and a hand-set primary-key collision have no declared constraint to map to and re-raise as the original `IntegrityError`. `create()` always inserts, so passing a stray `id` that already exists is rejected by Postgres as the original `IntegrityError`.)
|
|
1045
|
+
|
|
1046
|
+
Because the rejected write reaches the database, it aborts the surrounding transaction. If you catch the `ValidationError` and want to keep using the transaction, wrap the write in `transaction.atomic()` so it rolls back to a savepoint:
|
|
1047
|
+
|
|
1048
|
+
```python
|
|
1049
|
+
try:
|
|
1050
|
+
with transaction.atomic():
|
|
1051
|
+
User.query.create(email=taken_email)
|
|
1052
|
+
except ValidationError:
|
|
1053
|
+
... # report it — the transaction is still usable
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
Forms are the exception: a `ModelForm` pre-checks constraints explicitly (a `validate_constraints()` call in its `_post_clean`) so it can surface every violation at once, then saves with validation already done. A direct `create()`/`update()` reports the first violation Postgres hits.
|
|
1057
|
+
|
|
1058
|
+
This applies to instance writes only. Set-based writes — `QuerySet.update()` and `bulk_create()` — raise the raw `psycopg.IntegrityError`, since there's no instance to attribute the error to. If you retry on a unique conflict, catch both:
|
|
1059
|
+
|
|
1060
|
+
```python
|
|
1061
|
+
try:
|
|
1062
|
+
obj.create()
|
|
1063
|
+
except (psycopg.IntegrityError, ValidationError):
|
|
1064
|
+
... # lost a race — reload and retry, or report it
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
For a plain insert-or-update with no per-row logic, `bulk_create(..., update_conflicts=True, unique_fields=[...])` is an atomic upsert with no race to catch.
|
|
1068
|
+
|
|
1069
|
+
Two caveats. The mapping covers **immediate** constraints — the default. An explicitly deferred constraint (`UniqueConstraint(deferrable=Deferrable.DEFERRED)`) is checked at commit, _after_ the write returns, so its violation still surfaces as a raw `psycopg.IntegrityError`. And when a row violates several constraints at once, a form's pre-check (or an explicit `validate_constraints()`) reports them all, while a direct `create()`/`update()` gets only the first one the database hits.
|
|
1070
|
+
|
|
1044
1071
|
### Indexes and constraints
|
|
1045
1072
|
|
|
1046
1073
|
You can optimize queries and ensure data integrity with indexes and constraints. These are managed automatically by [convergence](#convergence) — just declare them on the model and run `postgres sync`.
|
|
@@ -1063,7 +1090,7 @@ class User(postgres.Model):
|
|
|
1063
1090
|
)
|
|
1064
1091
|
```
|
|
1065
1092
|
|
|
1066
|
-
Constraints are
|
|
1093
|
+
Constraints are checked by `validate_constraints()` — run by a `ModelForm` (and any explicit `validate_constraints()` call), but **not** by `full_clean()` (which validates shape only) or a direct `create()`/`update()`, where the database enforces them instead (see [Validation](#validation)). Pass `violation_error` to customize the resulting `ValidationError`. It accepts anything `ValidationError(...)` accepts — a string, a `{field: message}` dict, or a fully-formed `ValidationError`:
|
|
1067
1094
|
|
|
1068
1095
|
```python
|
|
1069
1096
|
# Simple message — lands on NON_FIELD_ERRORS
|
|
@@ -1142,10 +1169,11 @@ class Order(postgres.Model):
|
|
|
1142
1169
|
Enforce uniqueness and data integrity at the database level.
|
|
1143
1170
|
|
|
1144
1171
|
```python
|
|
1145
|
-
# Bad — only validated in Python
|
|
1146
|
-
def
|
|
1172
|
+
# Bad — only validated in Python (racy: two requests can both pass the check)
|
|
1173
|
+
def create(self):
|
|
1147
1174
|
if MyModel.query.filter(email=self.email).exists():
|
|
1148
1175
|
raise ValueError("duplicate")
|
|
1176
|
+
return super().create()
|
|
1149
1177
|
|
|
1150
1178
|
# Good — database-enforced
|
|
1151
1179
|
model_options = postgres.Options(
|
|
@@ -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`.
|
|
@@ -56,6 +75,8 @@ Use `Model.query` to build querysets (e.g., `User.query.filter(is_active=True)`)
|
|
|
56
75
|
- Use `bulk_create`/`bulk_update` for batch ops, `.update()`/`.delete()` for mass ops
|
|
57
76
|
- Use `.values_list()` when you only need specific columns
|
|
58
77
|
- Wrap multi-step writes in `transaction.atomic()`
|
|
78
|
+
- Instance writes are `obj.create()` (always INSERT) and `obj.update()` (always UPDATE; `update(fields=[...])` limits the columns) — there is no `save()`, `force_insert`, or `force_update`. Constructing an instance then `create()`-ing it inserts; a hand-set `id` that collides raises `IntegrityError`.
|
|
79
|
+
- `create()`/`update()` raise `ValidationError` (not raw `psycopg.IntegrityError`) on a declared unique/check constraint violation, even a raced one — the DB enforces it, so inside an open `transaction.atomic()` the violation aborts the transaction (wrap the write in its own `atomic()` to catch and keep using the transaction). Set-based writes (`QuerySet.update()`/`bulk_create()`) raise raw `psycopg.IntegrityError`. Retrying on conflict? `except (psycopg.IntegrityError, ValidationError)`, or `bulk_create(..., update_conflicts=True)`
|
|
59
80
|
- Always paginate list queries — unbounded querysets get slower as data grows
|
|
60
81
|
|
|
61
82
|
Run `uv run plain docs postgres` for full patterns with code examples.
|