Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/53.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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.
32 changes: 32 additions & 0 deletions src/rhosocial/activerecord/backend/impl/postgres/dialect.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@
PostgresTypeMixin,
PostgresConstraintMixin,
PostgresPolicyMixin,
PostgresRlsConfigMixin,
PostgresAlterTableSettingsMixin,
PostgresClusterMixin,
PostgresDomainMixin,
PostgresCollationDDLMixin,
PostgresForeignTableMixin,
PostgresRoutineMixin,
PostgresPublicationMixin,
# Type mixins
EnumTypeMixin,
TypesDataTypeMixin,
Expand Down Expand Up @@ -254,6 +262,14 @@
PostgresTypeSupport,
PostgresConstraintSupport,
PostgresPolicySupport,
PostgresRlsConfigSupport,
PostgresAlterTableSettingsSupport,
PostgresClusterSupport,
PostgresDomainSupport,
PostgresCollationDDLSupport,
PostgresForeignTableDDLSupport,
PostgresRoutineDDLSupport,
PostgresPublicationSupport,
# Type feature protocols
PostgresMultirangeSupport,
PostgresEnumTypeSupport,
Expand Down Expand Up @@ -400,6 +416,14 @@ class PostgresDialect(
PostgresTypeMixin,
PostgresConstraintMixin,
PostgresPolicyMixin,
PostgresRlsConfigMixin,
PostgresAlterTableSettingsMixin,
PostgresClusterMixin,
PostgresDomainMixin,
PostgresCollationDDLMixin,
PostgresForeignTableMixin,
PostgresRoutineMixin,
PostgresPublicationMixin,
# Type mixins
EnumTypeMixin,
TypesDataTypeMixin,
Expand Down Expand Up @@ -526,6 +550,14 @@ class PostgresDialect(
PostgresTypeSupport,
PostgresConstraintSupport,
PostgresPolicySupport,
PostgresRlsConfigSupport,
PostgresAlterTableSettingsSupport,
PostgresClusterSupport,
PostgresDomainSupport,
PostgresCollationDDLSupport,
PostgresForeignTableDDLSupport,
PostgresRoutineDDLSupport,
PostgresPublicationSupport,
# Type feature protocols
PostgresMultirangeSupport,
PostgresEnumTypeSupport,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@

# Verify table structure
from rhosocial.activerecord.backend.expression import QueryExpression, TableExpression
from rhosocial.activerecord.backend.expression.core import WildcardExpression, Column
from rhosocial.activerecord.backend.expression.core import Column
from rhosocial.activerecord.backend.expression.query_parts import OrderByClause

verify_expr = QueryExpression(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
},
)
sql, params = create_mv.to_sql()
print(f"Create MV with column aliases and storage options:")
print("Create MV with column aliases and storage options:")
print(f" SQL: {sql}")
backend.execute(sql, params)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
ColumnConstraint, ColumnConstraintType,
)
from rhosocial.activerecord.backend.expression.types import (
VarCharType, ArrayType, IntegerType, TextType,
VarCharType, ArrayType, TextType,
)
from rhosocial.activerecord.backend.impl.postgres.expression.types import (
PostgresSerialType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
assert col_b.data_type.__class__.__name__ == col_a.data_type.__class__.__name__, (
f"type mismatch in {name}.{col_b.name}"
)
print(f"Table data roundtrip: all columns and types match")
print("Table data roundtrip: all columns and types match")
print(f"Added tables: {diff.added_tables}")
print(f"Removed tables: {diff.removed_tables}")
print(f"Modified tables: {diff.modified_tables}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
- type.py - Enum/Range type expressions
- extension.py - Extension DDL expressions
- policy.py - Row-Level Security POLICY expressions (CREATE/ALTER/DROP)
- rls_config.py - Row-Level Security table configuration (ENABLE/DISABLE/FORCE)
- table_settings.py - ALTER TABLE SET LOGGED/UNLOGGED/ACCESS METHOD
- cluster.py - CLUSTER expressions
- domain.py - CREATE/ALTER/DROP DOMAIN expressions
- collation.py - CREATE/DROP COLLATION object expressions
- foreign_table.py - CREATE/DROP FOREIGN TABLE expressions
- routine.py - CREATE/DROP FUNCTION / AGGREGATE expressions
- publication.py - CREATE/DROP PUBLICATION / SUBSCRIPTION expressions

Missing expressions — why?
============================
Expand Down Expand Up @@ -93,6 +101,42 @@
PostgresAlterPolicyExpression,
PostgresDropPolicyExpression,
)
from .rls_config import (
RlsConfigurationMode,
PostgresAlterTableRlsExpression,
PostgresForceRlsExpression,
)
from .table_settings import (
LoggingMode,
PostgresAlterTableSettingsExpression,
)
from .cluster import PostgresClusterExpression
from .domain import (
AlterDomainActionType,
PostgresCreateDomainExpression,
PostgresAlterDomainExpression,
PostgresDropDomainExpression,
)
from .collation import (
PostgresCreateCollationExpression,
PostgresDropCollationExpression,
)
from .foreign_table import (
PostgresCreateForeignTableExpression,
PostgresDropForeignTableExpression,
)
from .routine import (
PostgresCreateFunctionExpression,
PostgresDropFunctionExpression,
PostgresCreateAggregateExpression,
PostgresDropAggregateExpression,
)
from .publication import (
PostgresCreatePublicationExpression,
PostgresDropPublicationExpression,
PostgresCreateSubscriptionExpression,
PostgresDropSubscriptionExpression,
)

__all__ = [
# vacuum
Expand Down Expand Up @@ -136,4 +180,34 @@
"PostgresCreatePolicyExpression",
"PostgresAlterPolicyExpression",
"PostgresDropPolicyExpression",
# rls_config
"RlsConfigurationMode",
"PostgresAlterTableRlsExpression",
"PostgresForceRlsExpression",
# table_settings
"LoggingMode",
"PostgresAlterTableSettingsExpression",
# cluster
"PostgresClusterExpression",
# domain
"AlterDomainActionType",
"PostgresCreateDomainExpression",
"PostgresAlterDomainExpression",
"PostgresDropDomainExpression",
# collation
"PostgresCreateCollationExpression",
"PostgresDropCollationExpression",
# foreign_table
"PostgresCreateForeignTableExpression",
"PostgresDropForeignTableExpression",
# routine
"PostgresCreateFunctionExpression",
"PostgresDropFunctionExpression",
"PostgresCreateAggregateExpression",
"PostgresDropAggregateExpression",
# publication
"PostgresCreatePublicationExpression",
"PostgresDropPublicationExpression",
"PostgresCreateSubscriptionExpression",
"PostgresDropSubscriptionExpression",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/cluster.py
"""
PostgreSQL DDL expressions: CLUSTER.

PostgreSQL Documentation:
- CLUSTER: https://www.postgresql.org/docs/current/sql-cluster.html

Version Requirements:
- CLUSTER table [ USING index ]: PostgreSQL 9.6+ (all supported versions)
- VERBOSE option: PostgreSQL 9.6+ (all supported versions)
"""

from typing import Optional, Tuple, TYPE_CHECKING

from rhosocial.activerecord.backend.expression.bases import BaseExpression

if TYPE_CHECKING:
from rhosocial.activerecord.backend.dialect import SQLDialectBase


__all__ = ["PostgresClusterExpression"]


class PostgresClusterExpression(BaseExpression):
"""PostgreSQL CLUSTER statement expression.

Physically reorders a table (or all tables) to match the order of the
index used for clustering. Commonly run inside a transaction after a
bulk load.

Attributes:
table_name: Name of the table to cluster. If None, clusters all
tables that have previously been clustered (``CLUSTER`` alone).
schema: Optional schema for the table.
using_index: Optional index name for ``CLUSTER table USING index``.
verbose: When True, report the table being clustered (``VERBOSE``).

Example:
>>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect
>>> dialect = PostgresDialect((14, 0, 0))
>>> expr = PostgresClusterExpression(
... dialect, table_name="orders", using_index="orders_pkey"
... )
>>> sql, params = expr.to_sql() # doctest: +SKIP

"""

def __init__(
self,
dialect: "SQLDialectBase",
table_name: Optional[str] = None,
schema: Optional[str] = None,
using_index: Optional[str] = None,
verbose: bool = False,
):
super().__init__(dialect)
self.table_name = table_name
self.schema = schema
self.using_index = using_index
self.verbose = verbose

def to_sql(self) -> "Tuple[str, tuple]":
"""Generate the CLUSTER statement.

Returns:
Tuple of (SQL string, empty params tuple).

"""
return self.dialect.format_cluster_statement(self)
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/collation.py
"""
PostgreSQL DDL expressions: COLLATION objects.

PostgreSQL Documentation:
- CREATE COLLATION: https://www.postgresql.org/docs/current/sql-createcollation.html
- ALTER COLLATION: https://www.postgresql.org/docs/current/sql-altercollation.html
- DROP COLLATION: https://www.postgresql.org/docs/current/sql-dropcollation.html

Version Requirements:
- CREATE/DROP COLLATION: PostgreSQL 9.6+ (all supported versions)
"""

from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING

from rhosocial.activerecord.backend.expression.bases import BaseExpression

if TYPE_CHECKING:
from rhosocial.activerecord.backend.dialect import SQLDialectBase


__all__ = ["PostgresCreateCollationExpression", "PostgresDropCollationExpression"]


class PostgresCreateCollationExpression(BaseExpression):
"""PostgreSQL CREATE COLLATION statement expression.

Defines a new collation object, typically from an OS locale.

Attributes:
name: Name of the collation.
schema: Optional schema for the collation.
locale: Optional ``LOCALE`` (provider-agnostic shorthand).
lc_collate: Optional ``LC_COLLATE``.
lc_ctype: Optional ``LC_CTYPE``.
provider: Optional ``PROVIDER`` (e.g. ``libc`` or ``icu``).
version: Optional ``VERSION``.
if_not_exists: When True, add ``IF NOT EXISTS``.
dialect_options: Reserved.

"""

def __init__(
self,
dialect: "SQLDialectBase",
name: str,
schema: Optional[str] = None,
locale: Optional[str] = None,
lc_collate: Optional[str] = None,
lc_ctype: Optional[str] = None,
provider: Optional[str] = None,
version: Optional[str] = None,
if_not_exists: bool = False,
dialect_options: Optional[Dict[str, Any]] = None,
):
super().__init__(dialect)
self.name = name
self.schema = schema
self.locale = locale
self.lc_collate = lc_collate
self.lc_ctype = lc_ctype
self.provider = provider
self.version = version
self.if_not_exists = if_not_exists
self.dialect_options = dialect_options or {}

def to_sql(self) -> "Tuple[str, tuple]":
"""Generate the CREATE COLLATION statement.

Returns:
Tuple of (SQL string, empty params tuple).

"""
return self.dialect.format_create_collation_ddl_statement(self)


class PostgresDropCollationExpression(BaseExpression):
"""PostgreSQL DROP COLLATION statement expression.

Attributes:
name: Name of the collation to drop.
schema: Optional schema for the collation.
if_exists: When True, add ``IF EXISTS``.
cascade: When True, add ``CASCADE``.
restrict: When True, add ``RESTRICT``. Mutually exclusive with
``cascade``.

"""

def __init__(
self,
dialect: "SQLDialectBase",
name: str,
schema: Optional[str] = None,
if_exists: bool = False,
cascade: bool = False,
restrict: bool = False,
):
super().__init__(dialect)
self.name = name
self.schema = schema
self.if_exists = if_exists
self.cascade = cascade
self.restrict = restrict

def to_sql(self) -> "Tuple[str, tuple]":
"""Return the DROP COLLATION statement.

Returns:
Tuple of (SQL string, empty params tuple).

"""
return self.dialect.format_drop_collation_ddl_statement(self)
Loading
Loading