Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
717b988
chore: bump version to 1.0.0.dev16
vistart Jun 16, 2026
bd72d65
ci: update activerecord dependency to release/v1.0.0.dev29
vistart Jun 19, 2026
63ed8d1
fix: install testsuite from git release/v1.0.0.dev16 instead of PyPI …
vistart Jun 19, 2026
fb975d5
feat(schema): implement schema-diff support and DataType refactoring …
vistart Jun 24, 2026
e691dc4
feat(cli): add named-migration subcommand for PostgreSQL backend (#47)
vistart Jun 30, 2026
e45fc79
feat(postgres): fulltext_search capability, benchmark markers, compos…
vistart Jul 5, 2026
aca738b
test(postgres): rewrite fulltext tests to reflect tsvector search cap…
vistart Jul 6, 2026
ab3be40
fix(backend): defensively consume cursor results in get_server_version()
vistart Jul 6, 2026
8c48c78
feat: PGQ enhancement - fulltext search, cursor pollution fix, and PG…
vistart Aug 2, 2026
5c9fae4
test(postgres): sync testsuite mirroring - add 9 missing test files +…
vistart Aug 2, 2026
5b83d05
feat(backend): render ALTER TABLE IF [NOT] EXISTS qualifiers for Post…
vistart Aug 7, 2026
226867d
feat(backend): add PostgreSQL Row-Level Security POLICY DDL expressio…
vistart Aug 7, 2026
acf620e
feat(backend): add PostgreSQL DDL coverage-completion expressions and…
vistart Aug 8, 2026
6d43947
chore(postgres): support TESTSUITE_ACTIVE_SCENARIOS filtering and rai…
vistart Aug 14, 2026
fcedfc3
feat(backend): override create_schema_differ() to supply PostgresSche…
vistart Aug 14, 2026
3087163
chore(changelog): remove redundant scenario/schema-differ fragments
vistart Aug 14, 2026
1eaedb6
docs: remove GEMINI.md
vistart Aug 14, 2026
bb42ab7
ci(test): install core 1.0.0.dev29 from PyPI
vistart Aug 15, 2026
e4b5ea4
ci(test): install testsuite 1.0.0.dev16 from PyPI
vistart Aug 15, 2026
4c46b66
docs: update CHANGELOG for v1.0.0.dev16
vistart Aug 15, 2026
f5ea77a
ci(test): bump postgres 19 job from 19beta2 to 19beta3
vistart Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
34 changes: 17 additions & 17 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ jobs:

- name: Install activerecord and testsuite
run: |
pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev28#egg=rhosocial-activerecord
pip install rhosocial-activerecord-testsuite==1.0.0.dev15
pip install rhosocial-activerecord==1.0.0.dev29
pip install rhosocial-activerecord-testsuite==1.0.0.dev16

- name: Create postgres scenario config
run: |
Expand Down Expand Up @@ -123,13 +123,13 @@ jobs:
--rootdir=. \
--confcutdir=. \
"$TESTSUITE_DIR/benchmark/crud/test_delete_async.py" \
"$TESTSUITE_DIR/benchmark/crud/test_delete_sync.py" \
"$TESTSUITE_DIR/benchmark/crud/test_delete.py" \
"$TESTSUITE_DIR/benchmark/crud/test_insert_async.py" \
"$TESTSUITE_DIR/benchmark/crud/test_insert_sync.py" \
"$TESTSUITE_DIR/benchmark/crud/test_insert.py" \
"$TESTSUITE_DIR/benchmark/crud/test_query_async.py" \
"$TESTSUITE_DIR/benchmark/crud/test_query_sync.py" \
"$TESTSUITE_DIR/benchmark/crud/test_query.py" \
"$TESTSUITE_DIR/benchmark/crud/test_update_async.py" \
"$TESTSUITE_DIR/benchmark/crud/test_update_sync.py" \
"$TESTSUITE_DIR/benchmark/crud/test_update.py" \
"$TESTSUITE_DIR/benchmark/query" \
"$TESTSUITE_DIR/benchmark/transaction" \
"$TESTSUITE_DIR/benchmark/mixin" \
Expand Down Expand Up @@ -220,7 +220,7 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"

test-experimental-python-315:
# Temporarily disabled until Python 3.15 beta/toolchain compatibility stabilizes.
# Temporarily disabled until Python 3.15 toolchain compatibility stabilizes.
if: ${{ false }}
name: Python ${{ matrix.python-version }} / postgres ${{ matrix.postgres-version }} (experimental)
runs-on: ubuntu-latest
Expand All @@ -231,8 +231,8 @@ jobs:
fail-fast: false
matrix:
include:
- { python-version: '3.15.0-beta.2', postgres-version: '18', freethreaded: false }
- { python-version: '3.15.0-beta.2', postgres-version: '18', freethreaded: true }
- { python-version: '3.15.0-rc.1', postgres-version: '18', freethreaded: false }
- { python-version: '3.15.0-rc.1', postgres-version: '18', freethreaded: true }

services:
postgres:
Expand Down Expand Up @@ -279,8 +279,8 @@ jobs:

- name: Install activerecord and testsuite
run: |
pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev28#egg=rhosocial-activerecord
pip install rhosocial-activerecord-testsuite==1.0.0.dev15
pip install rhosocial-activerecord==1.0.0.dev29
pip install rhosocial-activerecord-testsuite==1.0.0.dev16

- name: Create postgres scenario config
run: |
Expand Down Expand Up @@ -391,8 +391,8 @@ jobs:

- name: Install activerecord and testsuite
run: |
pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev28#egg=rhosocial-activerecord
pip install rhosocial-activerecord-testsuite==1.0.0.dev15
pip install rhosocial-activerecord==1.0.0.dev29
pip install rhosocial-activerecord-testsuite==1.0.0.dev16

- name: Create postgres scenario config
run: |
Expand Down Expand Up @@ -455,13 +455,13 @@ jobs:
fail_ci_if_error: false

test-postgres-19:
name: Python 3.14 / postgres 19beta1 (PGQ)
name: Python 3.14 / postgres 19beta3 (PGQ)
runs-on: ubuntu-latest
continue-on-error: true

services:
postgres:
image: postgres:19beta1
image: postgres:19beta3
env:
POSTGRES_DB: test_db
POSTGRES_USER: root
Expand Down Expand Up @@ -490,8 +490,8 @@ jobs:

- name: Install activerecord and testsuite
run: |
pip install git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev28#egg=rhosocial-activerecord
pip install rhosocial-activerecord-testsuite==1.0.0.dev15
pip install rhosocial-activerecord==1.0.0.dev29
pip install rhosocial-activerecord-testsuite==1.0.0.dev16

- name: Create postgres scenario config
run: |
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## [v1.0.0.dev16] - 2026-08-15

### Added

- Added PostgreSQL-specific DataType subclasses and schema-diff support with `PostgresSchemaDiffer` for accurate schema introspection and column comparison. ([#46](https://github.com/rhosocial/python-activerecord-postgres/issues/46))
- Added named-migration CLI subcommand with --async support and PostgreSQL-specific example migrations for the PostgreSQL backend. ([#47](https://github.com/rhosocial/python-activerecord-postgres/issues/47))
- Add PGQ enhancements: fulltext search capability, ON CONFLICT switches, JSON arrow operators, cursor result-set pollution fix, and PostgreSQL 19 compatibility gate for quantified path and comma-separated patterns ([#49](https://github.com/rhosocial/python-activerecord-postgres/issues/49))
- Added `PostgresAlterColumnModifierMixin` so PostgreSQL renders the vendor `IF [NOT] EXISTS` qualifiers on ALTER TABLE actions: `ADD COLUMN IF NOT EXISTS`, `DROP COLUMN IF EXISTS`, and `DROP CONSTRAINT IF EXISTS` (all supported since PostgreSQL 9.6). The qualifiers are opt-in via `if_not_exists` / `if_exists` on `AddColumn` / `DropColumn` / `DropTableConstraint`; `None` (default) renders the plain SQL-standard form. Also added the `supports_drop_constraint_if_exists` capability to `PostgresConstraintSupport` / `PostgresConstraintMixin`. ([#51](https://github.com/rhosocial/python-activerecord-postgres/issues/51))
- Added PostgreSQL Row-Level Security POLICY DDL expressions and dialect support. New `PostgresCreatePolicyExpression`, `PostgresAlterPolicyExpression`, `PostgresDropPolicyExpression` (plus `PolicyType`, `PolicyCommand`, `AlterPolicyMode` enums) in `expression/ddl/policy.py` allow RLS policy administration via the Expression-Dialect pipeline; the new `PostgresPolicyMixin` provides version-gated `format_*` methods (PG 9.5+ for basic CREATE/ALTER/DROP, 10+ for `AS RESTRICTIVE` and explicit `AS PERMISSIVE`, 15+ for the `CURRENT_ROLE` keyword in `TO`) and capability switches; `PostgresPolicySupport` is the corresponding `@runtime_checkable` Protocol. Per the layering decision POLICY is a PostgreSQL extension (not SQL standard) and is therefore implemented in the postgres backend package only — the core library `backend/expression/statements/` is untouched. ([#52](https://github.com/rhosocial/python-activerecord-postgres/issues/52))
- Added PostgreSQL coverage-completion DDL expressions and dialect support. New expression/mixin/protocol triads in the postgres backend package cover: `ALTER TABLE ... ENABLE/DISABLE [ALWAYS] ROW LEVEL SECURITY` and `FORCE/NO FORCE ROW LEVEL SECURITY` (`PostgresAlterTableRlsExpression`, `PostgresForceRlsExpression`; PG 9.5+); `ALTER TABLE ... SET LOGGED/UNLOGGED` (PG 9.6+) and `SET ACCESS METHOD` (PG 15+); `CLUSTER` (PG 9.6+, index re-cluster only); `CREATE/ALTER/DROP DOMAIN` (`PostgresAlterDomainExpression` supports SET DEFAULT / RENAME); `CREATE/DROP COLLATION` objects; `CREATE/DROP FOREIGN TABLE`; `CREATE/DROP FUNCTION` and `CREATE/DROP AGGREGATE` (PG 9.6+); and `CREATE/DROP PUBLICATION` / `CREATE/DROP SUBSCRIPTION` (PG 10+). Additionally `PostgresAlterColumnModifierMixin.format_alter_column_action` now renders PostgreSQL's `ALTER COLUMN ... SET DATA TYPE ... USING (expr)` conversion expression (opt-in via `dialect_options={"using": ...}`; rejected on non-type operations). Each feature has a version-gated capability switch and a matching `@runtime_checkable` Protocol (`PostgresRlsConfigSupport`, `PostgresAlterTableSettingsSupport`, `PostgresClusterSupport`, `PostgresDomainSupport`, `PostgresCollationDDLSupport`, `PostgresForeignTableDDLSupport`, `PostgresRoutineDDLSupport`, `PostgresPublicationSupport`). All are PostgreSQL extensions implemented only in the postgres backend package — the core library is untouched. ([#53](https://github.com/rhosocial/python-activerecord-postgres/issues/53))


## [v1.0.0.dev15] - 2026-06-15

### Added
Expand Down
1 change: 0 additions & 1 deletion GEMINI.md

This file was deleted.

10 changes: 8 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "rhosocial-activerecord-postgres"
version = "1.0.0.dev15"
version = "1.0.0.dev16"
description = "postgres backend implementation for rhosocial-activerecord, providing a robust and optimized postgres database support."
readme = "README.md"
license = { file = "LICENSE" }
Expand Down Expand Up @@ -37,7 +37,7 @@ classifiers = [
]
dependencies = [
"psycopg[binary]",
"rhosocial-activerecord"
"rhosocial-activerecord>=1.0.0.dev0,<2.0.0"
]

[project.optional-dependencies]
Expand Down Expand Up @@ -90,6 +90,12 @@ markers = [
"requires_functions: marks tests requiring specific database functions",
"requires_inner_join: marks tests requiring INNER JOIN support",
"benchmark_fastapi: marks FastAPI benchmark tests",
"benchmark: marks benchmark tests",
"benchmark_backend: marks backend benchmark tests",
"benchmark_async: marks async benchmark tests",
"benchmark_sync: marks sync benchmark tests",
"benchmark_write: marks write benchmark tests",
"benchmark_read: marks read benchmark tests",
]

[tool.coverage.run]
Expand Down
97 changes: 97 additions & 0 deletions src/rhosocial/activerecord/backend/impl/postgres/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,55 @@
from .dialect import PostgresDialect
from .transaction import PostgresTransactionManager, AsyncPostgresTransactionManager
from .types import PostgresEnumType
from .expression.types import (
PostgresBigSerialType,
PostgresBitType,
PostgresBoxType,
PostgresByteaType,
PostgresCIDType,
PostgresCircleType,
PostgresDateMultirangeType,
PostgresDateRangeType,
PostgresGeographyType,
PostgresGeometryType,
PostgresHstoreType,
PostgresInetType,
PostgresInt4MultirangeType,
PostgresInt4RangeType,
PostgresInt8MultirangeType,
PostgresInt8RangeType,
PostgresJsonPathType,
PostgresLineSegmentType,
PostgresLineType,
PostgresMacAddr8Type,
PostgresMacAddrType,
PostgresMoneyType,
PostgresNumMultirangeType,
PostgresNumRangeType,
PostgresOIDType,
PostgresPathType,
PostgresPgLSNType,
PostgresPointType,
PostgresPolygonType,
PostgresRegClassType,
PostgresRegTypeType,
PostgresSerialType,
PostgresSmallSerialType,
PostgresTIDType,
PostgresTSQueryType,
PostgresTSVectorType,
PostgresTsMultirangeType,
PostgresTsRangeType,
PostgresTsTzMultirangeType,
PostgresTsTzRangeType,
PostgresUUIDType,
PostgresVarBitType,
PostgresVectorType,
PostgresXID8Type,
PostgresXIDType,
PostgresXMLType,
PostgresCidrType,
)
from .explain import PostgresExplainResult, PostgresExplainPlanLine
from .expression.ddl import (
PostgresCreateEnumTypeExpression,
Expand Down Expand Up @@ -505,4 +554,52 @@
# hstore functions - Subscript
"hstore_subscript_get",
"hstore_subscript_set",
# PostgreSQL DataType subclasses for DDL
"PostgresBigSerialType",
"PostgresBitType",
"PostgresBoxType",
"PostgresByteaType",
"PostgresCIDType",
"PostgresCircleType",
"PostgresDateMultirangeType",
"PostgresDateRangeType",
"PostgresGeographyType",
"PostgresGeometryType",
"PostgresHstoreType",
"PostgresInetType",
"PostgresInt4MultirangeType",
"PostgresInt4RangeType",
"PostgresInt8MultirangeType",
"PostgresInt8RangeType",
"PostgresJsonPathType",
"PostgresLineSegmentType",
"PostgresLineType",
"PostgresMacAddr8Type",
"PostgresMacAddrType",
"PostgresMoneyType",
"PostgresNumMultirangeType",
"PostgresNumRangeType",
"PostgresOIDType",
"PostgresPathType",
"PostgresPgLSNType",
"PostgresPointType",
"PostgresPolygonType",
"PostgresRegClassType",
"PostgresRegTypeType",
"PostgresSerialType",
"PostgresSmallSerialType",
"PostgresTIDType",
"PostgresTSQueryType",
"PostgresTSVectorType",
"PostgresTsMultirangeType",
"PostgresTsRangeType",
"PostgresTsTzMultirangeType",
"PostgresTsTzRangeType",
"PostgresUUIDType",
"PostgresVarBitType",
"PostgresVectorType",
"PostgresXID8Type",
"PostgresXIDType",
"PostgresXMLType",
"PostgresCidrType",
]
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

COMMAND_NAMES = [
'info', 'query', 'introspect', 'status',
'named-expression', 'named-procedure', 'named-procedure-graph', 'named-connection',
'named-expression', 'named-procedure', 'named-procedure-graph',
'named-migration', 'named-connection',
]


Expand All @@ -18,6 +19,7 @@ def register_commands(subparsers):
from .named_expression import create_parser as nq_parser
from .named_procedure import create_parser as np_parser
from .named_procedure_graph import create_parser as npg_parser
from .named_migration import create_parser as nm_parser
from .named_connection import create_parser as nc_parser
info_parser(subparsers)
query_parser(subparsers)
Expand All @@ -26,6 +28,7 @@ def register_commands(subparsers):
nq_parser(subparsers)
np_parser(subparsers)
npg_parser(subparsers)
nm_parser(subparsers)
nc_parser(subparsers)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py
"""named-migration subcommand — PostgreSQL adapter for the shared CLI helper."""

from __future__ import annotations

from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend

from .connection import create_connection_parent_parser, resolve_connection_config_from_args
from .output import create_provider


def create_parser(subparsers):
"""Create the named-migration subcommand parser.

Provides connection and output arguments via a parent parser.
"""
from rhosocial.activerecord.backend.migration.cli import create_named_migration_parser

local_parent = create_connection_parent_parser()
nm_epilog = """Examples:
# Apply a migration
%(prog)s myapp.migrations.v001.CreateUsersTable --host localhost --database mydb --direction up

# Rollback
%(prog)s myapp.migrations.v001.CreateUsersTable --host localhost --database mydb --direction down

# Apply with record store
%(prog)s myapp.migrations.v001.CreateUsersTable --database mydb --record-store ./migrations.json

# Dry-run
%(prog)s myapp.migrations.v001.CreateUsersTable --database mydb --direction up --dry-run

# Apply asynchronously
%(prog)s myapp.migrations.v001.CreateUsersTable --database mydb --direction up --async

# Pass parameters to the migration
%(prog)s myapp.migrations.v001.CreateUsersTable --param table_name=custom_table

# List available migrations
%(prog)s myapp.migrations --list

# Describe a migration
%(prog)s myapp.migrations.v001.CreateUsersTable --describe

# Batch: apply all pending migrations in a module
%(prog)s myapp.migrations --all --record-store ./migrations.json

# Batch: dry-run all pending migrations
%(prog)s myapp.migrations --all --record-store ./migrations.json --dry-run

# Batch: rollback all applied migrations
%(prog)s myapp.migrations --all --record-store ./migrations.json --direction down

# Batch: wrap all migrations in a single transaction
%(prog)s myapp.migrations --all --record-store ./migrations.json --single-transaction

End-to-end demo scripts with UP/DOWN round-trip verification:
https://github.com/rhosocial/python-activerecord-postgres/tree/main/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations
PYTHONPATH=src bash demo_basic.sh
PYTHONPATH=src bash demo_chain.sh
PYTHONPATH=src bash demo_all.sh
"""
return create_named_migration_parser(subparsers, local_parent, epilog=nm_epilog)


def handle(args):
"""Handle the named-migration subcommand."""
from rhosocial.activerecord.backend.migration.cli import handle_named_migration as handle_nm

provider = create_provider(args.output, ascii_borders=args.rich_ascii)

backend = None

def backend_factory():
nonlocal backend
config = resolve_connection_config_from_args(args)
backend = PostgresBackend(connection_config=config)
backend.connect()
backend.introspect_and_adapt()
return backend

def disconnect():
if backend and getattr(backend, "_connection", None):
backend.disconnect()

is_async = getattr(args, "is_async", False)
if is_async:
async_backend = None

def backend_async_factory():
nonlocal async_backend
config = resolve_connection_config_from_args(args)
async_backend = AsyncPostgresBackend(connection_config=config)
return async_backend

async def disconnect_async():
if async_backend and getattr(async_backend, "_connection", None):
await async_backend.disconnect()

handle_nm(
args,
provider,
backend_factory=backend_factory,
disconnect=disconnect,
backend_async_factory=backend_async_factory,
disconnect_async=disconnect_async,
)
return

handle_nm(
args,
provider,
backend_factory=backend_factory,
disconnect=disconnect,
)
Loading
Loading