plain.postgres 0.113.2__tar.gz → 0.114.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.113.2 → plain_postgres-0.114.0}/PKG-INFO +55 -23
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/CHANGELOG.md +22 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/README.md +52 -20
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/__init__.py +22 -40
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/agents/.claude/rules/plain-postgres.md +3 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/base.py +3 -7
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/converge.py +2 -3
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/core.py +4 -4
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/migrations.py +2 -4
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/sync.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/connection.py +8 -12
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/constraints.py +10 -10
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/convergence/corrections.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/databases.py +3 -5
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/ddl.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/deletion.py +1 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/dialect.py +3 -4
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/exceptions.py +0 -12
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/expressions.py +23 -32
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/base.py +8 -5
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/binary.py +1 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/duration.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/encrypted.py +3 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/json.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/network.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/primary_key.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/related.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/related_descriptors.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/reverse_descriptors.py +5 -3
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/temporal.py +5 -3
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/text.py +4 -3
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/timezones.py +2 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/forms.py +13 -18
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/__init__.py +63 -70
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/datetime.py +5 -5
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/math.py +1 -4
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/mixins.py +2 -3
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/text.py +11 -8
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/indexes.py +9 -9
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/context.py +1 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/runner.py +24 -14
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/lookups.py +7 -5
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/meta.py +12 -10
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/__init__.py +9 -14
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/autodetector.py +66 -71
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/exceptions.py +0 -12
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/executor.py +1 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/graph.py +3 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/loader.py +4 -5
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/migration.py +13 -13
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/operations/base.py +3 -3
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/operations/fields.py +2 -3
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/operations/models.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/operations/special.py +2 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/questioner.py +1 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/serializer.py +5 -5
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/state.py +10 -12
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/utils.py +10 -11
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/writer.py +5 -5
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/otel.py +2 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/preflight/database.py +1 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/query.py +14 -18
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/query_utils.py +7 -10
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/registry.py +14 -14
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/schema.py +8 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/schema_lock.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/sources.py +1 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/sql/__init__.py +11 -12
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/sql/compiler.py +10 -12
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/sql/query.py +13 -13
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/test/database.py +1 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/test/pytest.py +1 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/transaction.py +2 -4
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/utils.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/pyproject.toml +2 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0001_initial.py +5 -4
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0002_test_field_removed.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0004_defaultquerysetmodel_mixintestmodel_and_more.py +6 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0005_feature_carfeature_car_features.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0006_secretstore.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0007_treenode_unconstrainedchild.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0008_setsentinelparent_diamondparenta_midparent_and_more.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0009_circb_circa_circb_partner.py +6 -5
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0010_hideableitem.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0011_defaultsexample.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0012_iterationexample.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0013_indexexample_constraintexample_nullabilityexample.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0014_widget_rename_feature_tag_remove_carfeature_car_and_more.py +6 -5
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0015_dbdefaultsexample.py +6 -5
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0016_formsexample.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0017_random_string_token.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/0018_storageparametersexample.py +5 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/constraints.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/defaults.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/delete.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/encrypted.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/forms.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/indexes.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/iteration.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/mixins.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/nullability.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/querysets.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/relationships.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/storage_parameters.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/trees.py +2 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/urls.py +2 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/urls.py +2 -3
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/conftest.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_autodetector_not_null_errors.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_autodetector_type_change.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_connection_isolation.py +0 -3
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_connection_lifecycle.py +3 -4
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_connection_pool.py +1 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_connection_self_heal.py +7 -10
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_constraint_violation_error.py +4 -5
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence_constraints.py +20 -21
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence_defaults.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence_fk.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence_indexes.py +7 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence_nullability.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence_storage_parameters.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_convergence_timeouts.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_db_expression_defaults.py +12 -6
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_diagnose.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_executor_connection_hook.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_fk_characterization.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_health.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_literal_default_persistence.py +1 -5
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_management_connection.py +2 -4
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_migration_executor.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_no_callable_defaults.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_otel_metrics.py +3 -4
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_rollback_exc_attribution.py +6 -10
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_schema_lock.py +15 -14
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_schema_normalize_type.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_schema_timeouts.py +4 -13
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_create_update.py +9 -10
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_databases.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_delete_behaviors.py +11 -16
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_encrypted_fields.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_exceptions.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_field_defaults.py +8 -7
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_integrity_error_mapping.py +6 -9
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_m2m.py +2 -4
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_manager_assignment.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_modelform_roundtrip.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_only_empty_defaults.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_order_by_expressions.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_queryset_slicing.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_random_string_field.py +0 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_read_only_transactions.py +20 -20
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_related.py +1 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/.gitignore +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/CLAUDE.md +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/LICENSE +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/README.md +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/adapters.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/agents/.claude/skills/plain-postgres-doctor/SKILL.md +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/aggregates.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/__init__.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/decorators.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/diagnose.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/cli/schema.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/config.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/constants.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/convergence/__init__.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/convergence/analysis.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/convergence/planning.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/database_url.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/db.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/default_settings.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/entrypoints.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/enums.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/__init__.py +2 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/boolean.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/mixins.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/numeric.py +1 -1
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/related_lookups.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/related_managers.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/reverse_related.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/fields/uuid.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/comparison.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/random.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/uuid.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/functions/window.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/__init__.py +4 -4
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/__init__.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/checks_cumulative.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/checks_snapshot.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/checks_structural.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/helpers.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/ownership.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/health/types.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/introspection/schema.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/middleware.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/operations/__init__.py +7 -7
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/optimizer.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/migrations/recorder.py +2 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/options.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/preflight/__init__.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/preflight/indexes.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/preflight/models.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/sql/constants.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/sql/datastructures.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/sql/where.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/test/__init__.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/types.py +2 -2
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/plain/postgres/types.pyi +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/forms.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/__init__.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/models/unregistered.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/examples/views.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/app/settings.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/conftest_convergence.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_apply_replan.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_databases_not_on_runtime_path.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_introspection.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_preflight_duplicate_indexes.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_preflight_fk_composite_hint.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/internal/test_preflight_fk_coverage.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_database_url.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_deferred_loading.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_functions_uuid.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_iterator.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_manual_pk.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_mixins.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_queryset_repr.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_raw_query.py +0 -0
- {plain_postgres-0.113.2 → plain_postgres-0.114.0}/tests/public/test_related_instance_filter.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: plain.postgres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.114.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
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Python: >=3.13
|
|
9
|
-
Requires-Dist: plain<1.0.0,>=0.
|
|
9
|
+
Requires-Dist: plain<1.0.0,>=0.157.0
|
|
10
10
|
Requires-Dist: psycopg-pool>=3.2
|
|
11
11
|
Requires-Dist: psycopg>=3.2
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
@@ -179,9 +179,8 @@ user = User.query.get(email="test@example.com")
|
|
|
179
179
|
|
|
180
180
|
# Complex queries with Q objects
|
|
181
181
|
from plain.postgres import Q
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
182
|
+
|
|
183
|
+
users = User.query.filter(Q(is_admin=True) | Q(email__endswith="@example.com"))
|
|
185
184
|
|
|
186
185
|
# Ordering
|
|
187
186
|
users = User.query.order_by("-created_at")
|
|
@@ -200,6 +199,7 @@ You can customize [`QuerySet`](./query.py#QuerySet) classes to provide specializ
|
|
|
200
199
|
from typing import Self
|
|
201
200
|
from plain.postgres import types
|
|
202
201
|
|
|
202
|
+
|
|
203
203
|
class PublishedQuerySet(postgres.QuerySet["Article"]):
|
|
204
204
|
def published_only(self) -> Self:
|
|
205
205
|
return self.filter(status="published")
|
|
@@ -207,6 +207,7 @@ class PublishedQuerySet(postgres.QuerySet["Article"]):
|
|
|
207
207
|
def draft_only(self) -> Self:
|
|
208
208
|
return self.filter(status="draft")
|
|
209
209
|
|
|
210
|
+
|
|
210
211
|
@postgres.register_model
|
|
211
212
|
class Article(postgres.Model):
|
|
212
213
|
title: str = types.TextField(max_length=200)
|
|
@@ -214,6 +215,7 @@ class Article(postgres.Model):
|
|
|
214
215
|
|
|
215
216
|
query = PublishedQuerySet()
|
|
216
217
|
|
|
218
|
+
|
|
217
219
|
# Usage - all methods available on Article.query
|
|
218
220
|
all_articles = Article.query.all()
|
|
219
221
|
published_articles = Article.query.published_only()
|
|
@@ -239,6 +241,7 @@ from __future__ import annotations
|
|
|
239
241
|
from plain import postgres
|
|
240
242
|
from plain.postgres import types
|
|
241
243
|
|
|
244
|
+
|
|
242
245
|
@postgres.register_model
|
|
243
246
|
class User(postgres.Model):
|
|
244
247
|
email: str = types.EmailField()
|
|
@@ -256,11 +259,14 @@ For complex queries that can't be expressed with the ORM, you can use raw SQL.
|
|
|
256
259
|
Use `Model.query.raw()` to execute raw SQL and get model instances back:
|
|
257
260
|
|
|
258
261
|
```python
|
|
259
|
-
users = User.query.raw(
|
|
262
|
+
users = User.query.raw(
|
|
263
|
+
"""
|
|
260
264
|
SELECT * FROM users
|
|
261
265
|
WHERE created_at > %s
|
|
262
266
|
ORDER BY created_at DESC
|
|
263
|
-
""",
|
|
267
|
+
""",
|
|
268
|
+
[some_date],
|
|
269
|
+
)
|
|
264
270
|
|
|
265
271
|
for user in users:
|
|
266
272
|
print(user.email) # Full model instance with all fields
|
|
@@ -333,6 +339,7 @@ for category in Category.query.all():
|
|
|
333
339
|
|
|
334
340
|
# Good — single query with annotation
|
|
335
341
|
from plain.postgres.aggregates import Count
|
|
342
|
+
|
|
336
343
|
for category in Category.query.annotate(num_products=Count("products")).all():
|
|
337
344
|
print(category.num_products)
|
|
338
345
|
```
|
|
@@ -346,6 +353,7 @@ Templates should only render data, never trigger queries. Prepare everything in
|
|
|
346
353
|
def get_template_context(self):
|
|
347
354
|
return {"posts": Post.query.all()} # related lookups happen in template
|
|
348
355
|
|
|
356
|
+
|
|
349
357
|
# Good — eagerly load everything
|
|
350
358
|
def get_template_context(self):
|
|
351
359
|
return {"posts": Post.query.select_related("author").prefetch_related("tags").all()}
|
|
@@ -369,10 +377,12 @@ emails = list(User.query.values_list("email", flat=True))
|
|
|
369
377
|
|
|
370
378
|
```python
|
|
371
379
|
# Bad
|
|
372
|
-
if User.query.filter(is_active=True).count() > 0:
|
|
380
|
+
if User.query.filter(is_active=True).count() > 0:
|
|
381
|
+
...
|
|
373
382
|
|
|
374
383
|
# Good
|
|
375
|
-
if User.query.filter(is_active=True).exists():
|
|
384
|
+
if User.query.filter(is_active=True).exists():
|
|
385
|
+
...
|
|
376
386
|
```
|
|
377
387
|
|
|
378
388
|
#### Use `.count()` instead of `len(queryset)`
|
|
@@ -479,8 +489,8 @@ Run a block of code in a read-only transaction using `read_only()`. Any write (I
|
|
|
479
489
|
from plain.postgres.db import read_only
|
|
480
490
|
|
|
481
491
|
with read_only():
|
|
482
|
-
users = User.query.all()
|
|
483
|
-
User.query.create(name="x")
|
|
492
|
+
users = User.query.all() # reads work
|
|
493
|
+
User.query.create(name="x") # raises psycopg.errors.ReadOnlySqlTransaction
|
|
484
494
|
```
|
|
485
495
|
|
|
486
496
|
`read_only()` opens a single `BEGIN READ ONLY` transaction for the block. Nested `atomic()` blocks inside become savepoints of the outer read-only transaction and inherit read-only.
|
|
@@ -493,10 +503,10 @@ Because the whole block is one transaction, catching a database error inside `re
|
|
|
493
503
|
with read_only():
|
|
494
504
|
try:
|
|
495
505
|
with atomic():
|
|
496
|
-
User.query.create(name="x")
|
|
506
|
+
User.query.create(name="x") # raises, savepoint rolls back
|
|
497
507
|
except psycopg.errors.ReadOnlySqlTransaction:
|
|
498
508
|
pass
|
|
499
|
-
User.query.count()
|
|
509
|
+
User.query.count() # still works — outer txn is healthy
|
|
500
510
|
```
|
|
501
511
|
|
|
502
512
|
## Schema management
|
|
@@ -718,7 +728,9 @@ class User(postgres.Model):
|
|
|
718
728
|
],
|
|
719
729
|
constraints=[
|
|
720
730
|
postgres.UniqueConstraint(fields=["email"], name="users_email_uniq"),
|
|
721
|
-
postgres.CheckConstraint(
|
|
731
|
+
postgres.CheckConstraint(
|
|
732
|
+
check=postgres.Q(age__gte=0), name="users_age_positive"
|
|
733
|
+
),
|
|
722
734
|
],
|
|
723
735
|
)
|
|
724
736
|
```
|
|
@@ -819,6 +831,7 @@ from datetime import datetime
|
|
|
819
831
|
from plain import postgres
|
|
820
832
|
from plain.postgres import types
|
|
821
833
|
|
|
834
|
+
|
|
822
835
|
class Product(postgres.Model):
|
|
823
836
|
# Text fields
|
|
824
837
|
name: str = types.TextField(max_length=200)
|
|
@@ -923,6 +936,7 @@ This is **not** for passwords or tokens you issue — those should be hashed (on
|
|
|
923
936
|
from plain import postgres
|
|
924
937
|
from plain.postgres import types
|
|
925
938
|
|
|
939
|
+
|
|
926
940
|
@postgres.register_model
|
|
927
941
|
class Integration(postgres.Model):
|
|
928
942
|
name: str = types.TextField(max_length=100)
|
|
@@ -959,6 +973,7 @@ Use [`ForeignKeyField`](./fields/related.py#ForeignKeyField) for many-to-one and
|
|
|
959
973
|
from plain import postgres
|
|
960
974
|
from plain.postgres import types
|
|
961
975
|
|
|
976
|
+
|
|
962
977
|
@postgres.register_model
|
|
963
978
|
class Book(postgres.Model):
|
|
964
979
|
title: str = types.TextField(max_length=200)
|
|
@@ -972,9 +987,9 @@ Accessing a foreign key gives you the related object without a query — only it
|
|
|
972
987
|
|
|
973
988
|
```python
|
|
974
989
|
book = Book.query.get(id=1)
|
|
975
|
-
book.author
|
|
976
|
-
book.author.id
|
|
977
|
-
book.author.name
|
|
990
|
+
book.author # no query — a partial Author instance
|
|
991
|
+
book.author.id # no query — the foreign key value
|
|
992
|
+
book.author.name # one query — loads the rest of the row
|
|
978
993
|
```
|
|
979
994
|
|
|
980
995
|
The first access to any non-key field loads the whole row in a single query. There is no separate `author_id` attribute — `book.author.id` is the foreign key value, and it is type-checked because `book.author` is an `Author`. In loops, use `select_related()` to load related rows up front and avoid a query per row.
|
|
@@ -989,17 +1004,20 @@ When you define a `ForeignKey` or `ManyToManyField`, Plain automatically creates
|
|
|
989
1004
|
from plain import postgres
|
|
990
1005
|
from plain.postgres import types
|
|
991
1006
|
|
|
1007
|
+
|
|
992
1008
|
@postgres.register_model
|
|
993
1009
|
class Author(postgres.Model):
|
|
994
1010
|
name: str = types.TextField(max_length=200)
|
|
995
1011
|
# Explicit reverse accessor for all books by this author
|
|
996
1012
|
books = types.ReverseForeignKey(to="Book", field="author")
|
|
997
1013
|
|
|
1014
|
+
|
|
998
1015
|
@postgres.register_model
|
|
999
1016
|
class Book(postgres.Model):
|
|
1000
1017
|
title: str = types.TextField(max_length=200)
|
|
1001
1018
|
author: Author = types.ForeignKeyField(Author, on_delete=postgres.CASCADE)
|
|
1002
1019
|
|
|
1020
|
+
|
|
1003
1021
|
# Usage
|
|
1004
1022
|
author = Author.query.get(name="Jane Doe")
|
|
1005
1023
|
for book in author.books.all():
|
|
@@ -1018,11 +1036,13 @@ class Feature(postgres.Model):
|
|
|
1018
1036
|
# Explicit reverse accessor for all cars with this feature
|
|
1019
1037
|
cars = types.ReverseManyToMany(to="Car", field="features")
|
|
1020
1038
|
|
|
1039
|
+
|
|
1021
1040
|
@postgres.register_model
|
|
1022
1041
|
class Car(postgres.Model):
|
|
1023
1042
|
model: str = types.TextField(max_length=100)
|
|
1024
1043
|
features = types.ManyToManyField(Feature)
|
|
1025
1044
|
|
|
1045
|
+
|
|
1026
1046
|
# Usage
|
|
1027
1047
|
feature = Feature.query.get(name="Sunroof")
|
|
1028
1048
|
for car in feature.cars.all():
|
|
@@ -1042,10 +1062,14 @@ To get type checking for custom QuerySet methods on reverse relations, specify t
|
|
|
1042
1062
|
|
|
1043
1063
|
```python
|
|
1044
1064
|
# Basic usage
|
|
1045
|
-
books: types.ReverseForeignKey[Book] = types.ReverseForeignKey(
|
|
1065
|
+
books: types.ReverseForeignKey[Book] = types.ReverseForeignKey(
|
|
1066
|
+
to="Book", field="author"
|
|
1067
|
+
)
|
|
1046
1068
|
|
|
1047
1069
|
# With custom QuerySet for proper method recognition
|
|
1048
|
-
books: types.ReverseForeignKey[Book, BookQuerySet] = types.ReverseForeignKey(
|
|
1070
|
+
books: types.ReverseForeignKey[Book, BookQuerySet] = types.ReverseForeignKey(
|
|
1071
|
+
to="Book", field="author"
|
|
1072
|
+
)
|
|
1049
1073
|
|
|
1050
1074
|
# Now type checkers recognize custom methods like .published()
|
|
1051
1075
|
author.books.query.published()
|
|
@@ -1189,6 +1213,7 @@ class Order(postgres.Model):
|
|
|
1189
1213
|
status: str = types.TextField(max_length=20)
|
|
1190
1214
|
created_at: datetime = types.DateTimeField()
|
|
1191
1215
|
|
|
1216
|
+
|
|
1192
1217
|
# Good — indexed for common queries
|
|
1193
1218
|
class Order(postgres.Model):
|
|
1194
1219
|
status: str = types.TextField(max_length=20)
|
|
@@ -1210,6 +1235,7 @@ def create(self):
|
|
|
1210
1235
|
raise ValueError("duplicate")
|
|
1211
1236
|
return super().create()
|
|
1212
1237
|
|
|
1238
|
+
|
|
1213
1239
|
# Good — database-enforced
|
|
1214
1240
|
model_options = postgres.Options(
|
|
1215
1241
|
constraints=[postgres.UniqueConstraint(fields=["email"])],
|
|
@@ -1222,7 +1248,9 @@ CASCADE for owned children, RESTRICT for referenced data, SET_NULL for optional
|
|
|
1222
1248
|
|
|
1223
1249
|
```python
|
|
1224
1250
|
# Bad — blindly using CASCADE everywhere
|
|
1225
|
-
company: Company = types.ForeignKeyField(
|
|
1251
|
+
company: Company = types.ForeignKeyField(
|
|
1252
|
+
"Company", on_delete=postgres.CASCADE
|
|
1253
|
+
) # deleting company deletes invoices!
|
|
1226
1254
|
|
|
1227
1255
|
# Good — block the delete while invoices reference the company
|
|
1228
1256
|
company: Company = types.ForeignKeyField("Company", on_delete=postgres.RESTRICT)
|
|
@@ -1248,10 +1276,12 @@ Models integrate with [plain.forms](../../../plain-forms/plain/forms/README.md):
|
|
|
1248
1276
|
from plain import forms
|
|
1249
1277
|
from .models import User
|
|
1250
1278
|
|
|
1279
|
+
|
|
1251
1280
|
class UserForm(forms.ModelForm):
|
|
1252
1281
|
class Meta:
|
|
1253
1282
|
model = User
|
|
1254
|
-
fields =
|
|
1283
|
+
fields = ("email", "is_admin")
|
|
1284
|
+
|
|
1255
1285
|
|
|
1256
1286
|
# Usage
|
|
1257
1287
|
form = UserForm(request=request)
|
|
@@ -1512,7 +1542,9 @@ Use `UniqueConstraint` in your model's `model_options`:
|
|
|
1512
1542
|
```python
|
|
1513
1543
|
model_options = postgres.Options(
|
|
1514
1544
|
constraints=[
|
|
1515
|
-
postgres.UniqueConstraint(
|
|
1545
|
+
postgres.UniqueConstraint(
|
|
1546
|
+
fields=["email", "organization"], name="unique_email_per_org"
|
|
1547
|
+
),
|
|
1516
1548
|
],
|
|
1517
1549
|
)
|
|
1518
1550
|
```
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# plain-postgres changelog
|
|
2
2
|
|
|
3
|
+
## [0.114.0](https://github.com/dropseed/plain/releases/plain-postgres@0.114.0) (2026-08-12)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- Wrong-type arguments now raise `TypeError` instead of `ValueError` across the schema and query APIs: `UniqueConstraint`/`Index` options (`condition`, `deferrable`, `include`, `opclasses`, `fields`), `Extract`/`Trunc` field types, non-bool `isnull` lookups, `RunPython` without a callable, serializer registration, assigning a wrong-model instance (or bool) to a foreign key, and reverse descriptors on non-FK/M2M fields ([f52e18f532](https://github.com/dropseed/plain/commit/f52e18f532))
|
|
8
|
+
- `Migration.operations`/`dependencies`/`replaces` are typed as `Sequence`, and `plain migrations create` now writes them as tuples in generated files. Existing list-based migration files keep working ([f52e18f532](https://github.com/dropseed/plain/commit/f52e18f532))
|
|
9
|
+
- Migration filename timestamps use the app's `TIME_ZONE` (via `timezone.localtime()`) instead of the machine's local clock, so a team generates consistently named migrations ([f52e18f532](https://github.com/dropseed/plain/commit/f52e18f532))
|
|
10
|
+
- Model field `default_validators` is now a tuple and `empty_values` a tuple; `ignored_tables` on the connection is a tuple ([f52e18f532](https://github.com/dropseed/plain/commit/f52e18f532))
|
|
11
|
+
- `ModelsRegistry.get_models()` caching was reworked to a per-instance memo (still cleared by `clear_cache()`), replacing a `functools.cache` keyed on the registry instance ([f52e18f532](https://github.com/dropseed/plain/commit/f52e18f532))
|
|
12
|
+
- `on_commit()` handler errors are logged with `logger.exception()` — same message and traceback, standard idiom ([f52e18f532](https://github.com/dropseed/plain/commit/f52e18f532))
|
|
13
|
+
|
|
14
|
+
### Upgrade instructions
|
|
15
|
+
|
|
16
|
+
- If you catch `ValueError` around any of the argument-validation cases above, catch `TypeError` instead
|
|
17
|
+
- Newly generated migrations use tuples for `dependencies`/`operations`; existing migration files don't need to change
|
|
18
|
+
- Requires `plain>=0.157.0`
|
|
19
|
+
|
|
3
20
|
## [0.113.2](https://github.com/dropseed/plain/releases/plain-postgres@0.113.2) (2026-08-10)
|
|
4
21
|
|
|
5
22
|
### What's changed
|
|
@@ -408,7 +425,11 @@
|
|
|
408
425
|
|
|
409
426
|
```python
|
|
410
427
|
# Before
|
|
411
|
-
from plain.postgres.connections import
|
|
428
|
+
from plain.postgres.connections import (
|
|
429
|
+
get_connection,
|
|
430
|
+
read_only,
|
|
431
|
+
use_management_connection,
|
|
432
|
+
)
|
|
412
433
|
|
|
413
434
|
# After
|
|
414
435
|
from plain.postgres.db import get_connection, read_only, use_management_connection
|
|
@@ -166,9 +166,8 @@ user = User.query.get(email="test@example.com")
|
|
|
166
166
|
|
|
167
167
|
# Complex queries with Q objects
|
|
168
168
|
from plain.postgres import Q
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
169
|
+
|
|
170
|
+
users = User.query.filter(Q(is_admin=True) | Q(email__endswith="@example.com"))
|
|
172
171
|
|
|
173
172
|
# Ordering
|
|
174
173
|
users = User.query.order_by("-created_at")
|
|
@@ -187,6 +186,7 @@ You can customize [`QuerySet`](./query.py#QuerySet) classes to provide specializ
|
|
|
187
186
|
from typing import Self
|
|
188
187
|
from plain.postgres import types
|
|
189
188
|
|
|
189
|
+
|
|
190
190
|
class PublishedQuerySet(postgres.QuerySet["Article"]):
|
|
191
191
|
def published_only(self) -> Self:
|
|
192
192
|
return self.filter(status="published")
|
|
@@ -194,6 +194,7 @@ class PublishedQuerySet(postgres.QuerySet["Article"]):
|
|
|
194
194
|
def draft_only(self) -> Self:
|
|
195
195
|
return self.filter(status="draft")
|
|
196
196
|
|
|
197
|
+
|
|
197
198
|
@postgres.register_model
|
|
198
199
|
class Article(postgres.Model):
|
|
199
200
|
title: str = types.TextField(max_length=200)
|
|
@@ -201,6 +202,7 @@ class Article(postgres.Model):
|
|
|
201
202
|
|
|
202
203
|
query = PublishedQuerySet()
|
|
203
204
|
|
|
205
|
+
|
|
204
206
|
# Usage - all methods available on Article.query
|
|
205
207
|
all_articles = Article.query.all()
|
|
206
208
|
published_articles = Article.query.published_only()
|
|
@@ -226,6 +228,7 @@ from __future__ import annotations
|
|
|
226
228
|
from plain import postgres
|
|
227
229
|
from plain.postgres import types
|
|
228
230
|
|
|
231
|
+
|
|
229
232
|
@postgres.register_model
|
|
230
233
|
class User(postgres.Model):
|
|
231
234
|
email: str = types.EmailField()
|
|
@@ -243,11 +246,14 @@ For complex queries that can't be expressed with the ORM, you can use raw SQL.
|
|
|
243
246
|
Use `Model.query.raw()` to execute raw SQL and get model instances back:
|
|
244
247
|
|
|
245
248
|
```python
|
|
246
|
-
users = User.query.raw(
|
|
249
|
+
users = User.query.raw(
|
|
250
|
+
"""
|
|
247
251
|
SELECT * FROM users
|
|
248
252
|
WHERE created_at > %s
|
|
249
253
|
ORDER BY created_at DESC
|
|
250
|
-
""",
|
|
254
|
+
""",
|
|
255
|
+
[some_date],
|
|
256
|
+
)
|
|
251
257
|
|
|
252
258
|
for user in users:
|
|
253
259
|
print(user.email) # Full model instance with all fields
|
|
@@ -320,6 +326,7 @@ for category in Category.query.all():
|
|
|
320
326
|
|
|
321
327
|
# Good — single query with annotation
|
|
322
328
|
from plain.postgres.aggregates import Count
|
|
329
|
+
|
|
323
330
|
for category in Category.query.annotate(num_products=Count("products")).all():
|
|
324
331
|
print(category.num_products)
|
|
325
332
|
```
|
|
@@ -333,6 +340,7 @@ Templates should only render data, never trigger queries. Prepare everything in
|
|
|
333
340
|
def get_template_context(self):
|
|
334
341
|
return {"posts": Post.query.all()} # related lookups happen in template
|
|
335
342
|
|
|
343
|
+
|
|
336
344
|
# Good — eagerly load everything
|
|
337
345
|
def get_template_context(self):
|
|
338
346
|
return {"posts": Post.query.select_related("author").prefetch_related("tags").all()}
|
|
@@ -356,10 +364,12 @@ emails = list(User.query.values_list("email", flat=True))
|
|
|
356
364
|
|
|
357
365
|
```python
|
|
358
366
|
# Bad
|
|
359
|
-
if User.query.filter(is_active=True).count() > 0:
|
|
367
|
+
if User.query.filter(is_active=True).count() > 0:
|
|
368
|
+
...
|
|
360
369
|
|
|
361
370
|
# Good
|
|
362
|
-
if User.query.filter(is_active=True).exists():
|
|
371
|
+
if User.query.filter(is_active=True).exists():
|
|
372
|
+
...
|
|
363
373
|
```
|
|
364
374
|
|
|
365
375
|
#### Use `.count()` instead of `len(queryset)`
|
|
@@ -466,8 +476,8 @@ Run a block of code in a read-only transaction using `read_only()`. Any write (I
|
|
|
466
476
|
from plain.postgres.db import read_only
|
|
467
477
|
|
|
468
478
|
with read_only():
|
|
469
|
-
users = User.query.all()
|
|
470
|
-
User.query.create(name="x")
|
|
479
|
+
users = User.query.all() # reads work
|
|
480
|
+
User.query.create(name="x") # raises psycopg.errors.ReadOnlySqlTransaction
|
|
471
481
|
```
|
|
472
482
|
|
|
473
483
|
`read_only()` opens a single `BEGIN READ ONLY` transaction for the block. Nested `atomic()` blocks inside become savepoints of the outer read-only transaction and inherit read-only.
|
|
@@ -480,10 +490,10 @@ Because the whole block is one transaction, catching a database error inside `re
|
|
|
480
490
|
with read_only():
|
|
481
491
|
try:
|
|
482
492
|
with atomic():
|
|
483
|
-
User.query.create(name="x")
|
|
493
|
+
User.query.create(name="x") # raises, savepoint rolls back
|
|
484
494
|
except psycopg.errors.ReadOnlySqlTransaction:
|
|
485
495
|
pass
|
|
486
|
-
User.query.count()
|
|
496
|
+
User.query.count() # still works — outer txn is healthy
|
|
487
497
|
```
|
|
488
498
|
|
|
489
499
|
## Schema management
|
|
@@ -705,7 +715,9 @@ class User(postgres.Model):
|
|
|
705
715
|
],
|
|
706
716
|
constraints=[
|
|
707
717
|
postgres.UniqueConstraint(fields=["email"], name="users_email_uniq"),
|
|
708
|
-
postgres.CheckConstraint(
|
|
718
|
+
postgres.CheckConstraint(
|
|
719
|
+
check=postgres.Q(age__gte=0), name="users_age_positive"
|
|
720
|
+
),
|
|
709
721
|
],
|
|
710
722
|
)
|
|
711
723
|
```
|
|
@@ -806,6 +818,7 @@ from datetime import datetime
|
|
|
806
818
|
from plain import postgres
|
|
807
819
|
from plain.postgres import types
|
|
808
820
|
|
|
821
|
+
|
|
809
822
|
class Product(postgres.Model):
|
|
810
823
|
# Text fields
|
|
811
824
|
name: str = types.TextField(max_length=200)
|
|
@@ -910,6 +923,7 @@ This is **not** for passwords or tokens you issue — those should be hashed (on
|
|
|
910
923
|
from plain import postgres
|
|
911
924
|
from plain.postgres import types
|
|
912
925
|
|
|
926
|
+
|
|
913
927
|
@postgres.register_model
|
|
914
928
|
class Integration(postgres.Model):
|
|
915
929
|
name: str = types.TextField(max_length=100)
|
|
@@ -946,6 +960,7 @@ Use [`ForeignKeyField`](./fields/related.py#ForeignKeyField) for many-to-one and
|
|
|
946
960
|
from plain import postgres
|
|
947
961
|
from plain.postgres import types
|
|
948
962
|
|
|
963
|
+
|
|
949
964
|
@postgres.register_model
|
|
950
965
|
class Book(postgres.Model):
|
|
951
966
|
title: str = types.TextField(max_length=200)
|
|
@@ -959,9 +974,9 @@ Accessing a foreign key gives you the related object without a query — only it
|
|
|
959
974
|
|
|
960
975
|
```python
|
|
961
976
|
book = Book.query.get(id=1)
|
|
962
|
-
book.author
|
|
963
|
-
book.author.id
|
|
964
|
-
book.author.name
|
|
977
|
+
book.author # no query — a partial Author instance
|
|
978
|
+
book.author.id # no query — the foreign key value
|
|
979
|
+
book.author.name # one query — loads the rest of the row
|
|
965
980
|
```
|
|
966
981
|
|
|
967
982
|
The first access to any non-key field loads the whole row in a single query. There is no separate `author_id` attribute — `book.author.id` is the foreign key value, and it is type-checked because `book.author` is an `Author`. In loops, use `select_related()` to load related rows up front and avoid a query per row.
|
|
@@ -976,17 +991,20 @@ When you define a `ForeignKey` or `ManyToManyField`, Plain automatically creates
|
|
|
976
991
|
from plain import postgres
|
|
977
992
|
from plain.postgres import types
|
|
978
993
|
|
|
994
|
+
|
|
979
995
|
@postgres.register_model
|
|
980
996
|
class Author(postgres.Model):
|
|
981
997
|
name: str = types.TextField(max_length=200)
|
|
982
998
|
# Explicit reverse accessor for all books by this author
|
|
983
999
|
books = types.ReverseForeignKey(to="Book", field="author")
|
|
984
1000
|
|
|
1001
|
+
|
|
985
1002
|
@postgres.register_model
|
|
986
1003
|
class Book(postgres.Model):
|
|
987
1004
|
title: str = types.TextField(max_length=200)
|
|
988
1005
|
author: Author = types.ForeignKeyField(Author, on_delete=postgres.CASCADE)
|
|
989
1006
|
|
|
1007
|
+
|
|
990
1008
|
# Usage
|
|
991
1009
|
author = Author.query.get(name="Jane Doe")
|
|
992
1010
|
for book in author.books.all():
|
|
@@ -1005,11 +1023,13 @@ class Feature(postgres.Model):
|
|
|
1005
1023
|
# Explicit reverse accessor for all cars with this feature
|
|
1006
1024
|
cars = types.ReverseManyToMany(to="Car", field="features")
|
|
1007
1025
|
|
|
1026
|
+
|
|
1008
1027
|
@postgres.register_model
|
|
1009
1028
|
class Car(postgres.Model):
|
|
1010
1029
|
model: str = types.TextField(max_length=100)
|
|
1011
1030
|
features = types.ManyToManyField(Feature)
|
|
1012
1031
|
|
|
1032
|
+
|
|
1013
1033
|
# Usage
|
|
1014
1034
|
feature = Feature.query.get(name="Sunroof")
|
|
1015
1035
|
for car in feature.cars.all():
|
|
@@ -1029,10 +1049,14 @@ To get type checking for custom QuerySet methods on reverse relations, specify t
|
|
|
1029
1049
|
|
|
1030
1050
|
```python
|
|
1031
1051
|
# Basic usage
|
|
1032
|
-
books: types.ReverseForeignKey[Book] = types.ReverseForeignKey(
|
|
1052
|
+
books: types.ReverseForeignKey[Book] = types.ReverseForeignKey(
|
|
1053
|
+
to="Book", field="author"
|
|
1054
|
+
)
|
|
1033
1055
|
|
|
1034
1056
|
# With custom QuerySet for proper method recognition
|
|
1035
|
-
books: types.ReverseForeignKey[Book, BookQuerySet] = types.ReverseForeignKey(
|
|
1057
|
+
books: types.ReverseForeignKey[Book, BookQuerySet] = types.ReverseForeignKey(
|
|
1058
|
+
to="Book", field="author"
|
|
1059
|
+
)
|
|
1036
1060
|
|
|
1037
1061
|
# Now type checkers recognize custom methods like .published()
|
|
1038
1062
|
author.books.query.published()
|
|
@@ -1176,6 +1200,7 @@ class Order(postgres.Model):
|
|
|
1176
1200
|
status: str = types.TextField(max_length=20)
|
|
1177
1201
|
created_at: datetime = types.DateTimeField()
|
|
1178
1202
|
|
|
1203
|
+
|
|
1179
1204
|
# Good — indexed for common queries
|
|
1180
1205
|
class Order(postgres.Model):
|
|
1181
1206
|
status: str = types.TextField(max_length=20)
|
|
@@ -1197,6 +1222,7 @@ def create(self):
|
|
|
1197
1222
|
raise ValueError("duplicate")
|
|
1198
1223
|
return super().create()
|
|
1199
1224
|
|
|
1225
|
+
|
|
1200
1226
|
# Good — database-enforced
|
|
1201
1227
|
model_options = postgres.Options(
|
|
1202
1228
|
constraints=[postgres.UniqueConstraint(fields=["email"])],
|
|
@@ -1209,7 +1235,9 @@ CASCADE for owned children, RESTRICT for referenced data, SET_NULL for optional
|
|
|
1209
1235
|
|
|
1210
1236
|
```python
|
|
1211
1237
|
# Bad — blindly using CASCADE everywhere
|
|
1212
|
-
company: Company = types.ForeignKeyField(
|
|
1238
|
+
company: Company = types.ForeignKeyField(
|
|
1239
|
+
"Company", on_delete=postgres.CASCADE
|
|
1240
|
+
) # deleting company deletes invoices!
|
|
1213
1241
|
|
|
1214
1242
|
# Good — block the delete while invoices reference the company
|
|
1215
1243
|
company: Company = types.ForeignKeyField("Company", on_delete=postgres.RESTRICT)
|
|
@@ -1235,10 +1263,12 @@ Models integrate with [plain.forms](../../../plain-forms/plain/forms/README.md):
|
|
|
1235
1263
|
from plain import forms
|
|
1236
1264
|
from .models import User
|
|
1237
1265
|
|
|
1266
|
+
|
|
1238
1267
|
class UserForm(forms.ModelForm):
|
|
1239
1268
|
class Meta:
|
|
1240
1269
|
model = User
|
|
1241
|
-
fields =
|
|
1270
|
+
fields = ("email", "is_admin")
|
|
1271
|
+
|
|
1242
1272
|
|
|
1243
1273
|
# Usage
|
|
1244
1274
|
form = UserForm(request=request)
|
|
@@ -1499,7 +1529,9 @@ Use `UniqueConstraint` in your model's `model_options`:
|
|
|
1499
1529
|
```python
|
|
1500
1530
|
model_options = postgres.Options(
|
|
1501
1531
|
constraints=[
|
|
1502
|
-
postgres.UniqueConstraint(
|
|
1532
|
+
postgres.UniqueConstraint(
|
|
1533
|
+
fields=["email", "organization"], name="unique_email_per_org"
|
|
1534
|
+
),
|
|
1503
1535
|
],
|
|
1504
1536
|
)
|
|
1505
1537
|
```
|
|
@@ -56,65 +56,47 @@ from . import types
|
|
|
56
56
|
# Advanced query-time features (aggregates, expressions, etc.) should be
|
|
57
57
|
# imported from their specific modules (e.g., plain.postgres.aggregates).
|
|
58
58
|
__all__ = [
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
"TextChoices",
|
|
64
|
-
# From fields
|
|
59
|
+
"CASCADE",
|
|
60
|
+
"NO_ACTION",
|
|
61
|
+
"RESTRICT",
|
|
62
|
+
"SET_NULL",
|
|
65
63
|
"BigIntegerField",
|
|
66
64
|
"BinaryField",
|
|
67
65
|
"BooleanField",
|
|
66
|
+
"CheckConstraint",
|
|
67
|
+
"DatabaseConnectionMiddleware",
|
|
68
68
|
"DateField",
|
|
69
69
|
"DateTimeField",
|
|
70
70
|
"DecimalField",
|
|
71
71
|
"DurationField",
|
|
72
72
|
"EmailField",
|
|
73
|
+
"F",
|
|
73
74
|
"FloatField",
|
|
75
|
+
"ForeignKeyField",
|
|
74
76
|
"GenericIPAddressField",
|
|
77
|
+
"Index",
|
|
75
78
|
"IntegerField",
|
|
79
|
+
"JSONField",
|
|
80
|
+
"ManyToManyField",
|
|
81
|
+
"Model",
|
|
82
|
+
"Options",
|
|
76
83
|
"PrimaryKeyField",
|
|
84
|
+
"Q",
|
|
85
|
+
"QuerySet",
|
|
77
86
|
"RandomStringField",
|
|
87
|
+
"ReverseForeignKey",
|
|
88
|
+
"ReverseManyToMany",
|
|
78
89
|
"SmallIntegerField",
|
|
90
|
+
"TextChoices",
|
|
79
91
|
"TextField",
|
|
80
92
|
"TimeField",
|
|
93
|
+
"TimeZoneField",
|
|
81
94
|
"URLField",
|
|
82
95
|
"UUIDField",
|
|
83
|
-
|
|
84
|
-
"JSONField",
|
|
85
|
-
# From fields.timezones
|
|
86
|
-
"TimeZoneField",
|
|
87
|
-
# From indexes
|
|
88
|
-
"Index",
|
|
89
|
-
# From deletion
|
|
90
|
-
"CASCADE",
|
|
91
|
-
"NO_ACTION",
|
|
92
|
-
"RESTRICT",
|
|
93
|
-
"SET_NULL",
|
|
94
|
-
# From options
|
|
95
|
-
"Options",
|
|
96
|
-
# From query
|
|
97
|
-
"QuerySet",
|
|
98
|
-
# From query_utils
|
|
99
|
-
"Q",
|
|
100
|
-
# From expressions
|
|
101
|
-
"F",
|
|
102
|
-
# From base
|
|
103
|
-
"Model",
|
|
104
|
-
# From fields.related
|
|
105
|
-
"ForeignKeyField",
|
|
106
|
-
"ManyToManyField",
|
|
107
|
-
# From fields.reverse_descriptors
|
|
108
|
-
"ReverseForeignKey",
|
|
109
|
-
"ReverseManyToMany",
|
|
110
|
-
# From db
|
|
96
|
+
"UniqueConstraint",
|
|
111
97
|
"get_connection",
|
|
112
|
-
"use_management_connection",
|
|
113
|
-
# From middleware
|
|
114
|
-
"DatabaseConnectionMiddleware",
|
|
115
|
-
# From registry
|
|
116
|
-
"register_model",
|
|
117
98
|
"models_registry",
|
|
118
|
-
|
|
99
|
+
"register_model",
|
|
119
100
|
"types",
|
|
101
|
+
"use_management_connection",
|
|
120
102
|
]
|
|
@@ -23,7 +23,9 @@ can't infer the target type from the string — annotate explicitly so
|
|
|
23
23
|
instance access keeps its type:
|
|
24
24
|
|
|
25
25
|
```python
|
|
26
|
-
parent: TreeNode | None = types.ForeignKeyField(
|
|
26
|
+
parent: TreeNode | None = types.ForeignKeyField(
|
|
27
|
+
"self", on_delete=postgres.CASCADE, allow_null=True
|
|
28
|
+
)
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
For `JSONField` and `EncryptedJSONField`, the stub returns `Any` (the
|