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/39.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fill all PostgreSQL index, trigger, table, and extension protocol feature coverage gaps
36 changes: 33 additions & 3 deletions src/rhosocial/activerecord/backend/impl/postgres/dialect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1029,12 +1029,42 @@ def supports_unique_index(self) -> bool:

def supports_index_if_not_exists(self) -> bool:
"""Whether CREATE INDEX IF NOT EXISTS is supported."""
return True # PostgreSQL 9.5+ supports IF NOT EXISTS
return self.version >= (9, 5, 0) # PostgreSQL 9.5+ supports IF NOT EXISTS

def supports_index_if_exists(self) -> bool:
"""Whether DROP INDEX IF EXISTS is supported."""
return True

def format_add_index_action(self, action) -> Tuple[str, tuple]:
"""PostgreSQL does not support ALTER TABLE ADD INDEX.

Raises UnsupportedFeatureError — use CREATE INDEX instead.
"""
from rhosocial.activerecord.backend.dialect.exceptions import (
UnsupportedFeatureError,
)

raise UnsupportedFeatureError(
self.name,
"ALTER TABLE ADD INDEX",
suggestion="Use CREATE INDEX to create an index on the table.",
)

def format_drop_index_action(self, action) -> Tuple[str, tuple]:
"""PostgreSQL does not support ALTER TABLE DROP INDEX.

Raises UnsupportedFeatureError — use DROP INDEX statement instead.
"""
from rhosocial.activerecord.backend.dialect.exceptions import (
UnsupportedFeatureError,
)

raise UnsupportedFeatureError(
self.name,
"ALTER TABLE DROP INDEX",
suggestion="Use DROP INDEX statement to remove an index.",
)

# endregion

# region Sequence Support
Expand All @@ -1051,7 +1081,7 @@ def supports_drop_sequence(self) -> bool:
# region Table Support
def supports_if_not_exists_table(self) -> bool:
"""Whether CREATE TABLE IF NOT EXISTS is supported."""
return True
return self.version >= (9, 5, 0)

def supports_if_exists_table(self) -> bool:
"""Whether DROP TABLE IF EXISTS is supported."""
Expand All @@ -1067,7 +1097,7 @@ def supports_table_inheritance(self) -> bool:

def supports_table_partitioning(self) -> bool:
"""Whether table partitioning is supported."""
return True # PostgreSQL supports partitioning
return self.version >= (10, 0, 0)

def supports_table_tablespace(self) -> bool:
"""Whether tablespace specification is supported."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
PostgresCreatePartitionExpression,
PostgresDetachPartitionExpression,
PostgresAttachPartitionExpression,
PostgresCreateIndexExpression,
PostgresDropIndexExpression,
PostgresAlterIndexExpression,
PostgresAlterIndexActionType,
PostgresReindexExpression,
PostgresCreateStatisticsExpression,
PostgresDropStatisticsExpression,
Expand Down Expand Up @@ -56,6 +60,10 @@
"PostgresCreatePartitionExpression",
"PostgresDetachPartitionExpression",
"PostgresAttachPartitionExpression",
"PostgresCreateIndexExpression",
"PostgresDropIndexExpression",
"PostgresAlterIndexExpression",
"PostgresAlterIndexActionType",
"PostgresReindexExpression",
"PostgresCreateStatisticsExpression",
"PostgresDropStatisticsExpression",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@
PostgresDetachPartitionExpression,
PostgresAttachPartitionExpression,
)
from .index import PostgresReindexExpression
from .index import (
PostgresCreateIndexExpression,
PostgresDropIndexExpression,
PostgresAlterIndexExpression,
PostgresAlterIndexActionType,
PostgresReindexExpression,
)
from .statistics import (
PostgresCreateStatisticsExpression,
PostgresDropStatisticsExpression,
Expand All @@ -45,6 +51,10 @@
"PostgresDetachPartitionExpression",
"PostgresAttachPartitionExpression",
# index
"PostgresCreateIndexExpression",
"PostgresDropIndexExpression",
"PostgresAlterIndexExpression",
"PostgresAlterIndexActionType",
"PostgresReindexExpression",
# statistics
"PostgresCreateStatisticsExpression",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,224 @@
# src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index.py
"""
PostgreSQL DDL expressions: Index operations (REINDEX).
PostgreSQL DDL expressions: Index operations (CREATE/DROP/ALTER INDEX, REINDEX).

PostgreSQL Documentation:
- CREATE INDEX: https://www.postgresql.org/docs/current/sql-createindex.html
- DROP INDEX: https://www.postgresql.org/docs/current/sql-dropindex.html
- ALTER INDEX: https://www.postgresql.org/docs/current/sql-alterindex.html
- REINDEX: https://www.postgresql.org/docs/current/sql-reindex.html

Version Requirements:
- REINDEX: PostgreSQL 8.0+
- DROP INDEX CONCURRENTLY: PostgreSQL 18+
- CONCURRENTLY option: PostgreSQL 12+
- TABLESPACE option: PostgreSQL 14+
- NULLS NOT DISTINCT: PostgreSQL 15+
- CONCURRENTLY + NULLS NOT DISTINCT: PostgreSQL 16+
"""

from typing import Any, Dict, Optional, Tuple, TYPE_CHECKING
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING

from rhosocial.activerecord.backend.expression.bases import BaseExpression
from rhosocial.activerecord.backend.expression.statements.ddl_index import (
CreateIndexExpression,
DropIndexExpression,
)

if TYPE_CHECKING:
if TYPE_CHECKING: # pragma: no cover
from rhosocial.activerecord.backend.dialect import SQLDialectBase
from rhosocial.activerecord.backend.expression import SQLPredicate


__all__ = ["PostgresReindexExpression"]
__all__ = [
"PostgresCreateIndexExpression",
"PostgresDropIndexExpression",
"PostgresAlterIndexExpression",
"PostgresAlterIndexActionType",
"PostgresReindexExpression",
]


class PostgresCreateIndexExpression(CreateIndexExpression):
"""PostgreSQL-specific CREATE INDEX expression.

Extends the standard CreateIndexExpression with PostgreSQL-specific
features such as NULLS NOT DISTINCT.

The option is stored in dialect_options under the key ``"nulls_not_distinct"``
and is consumed by ``PostgresIndexMixin.format_create_index_statement``.

Example:
>>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect
>>> dialect = PostgresDialect((15, 0, 0))
>>> idx = PostgresCreateIndexExpression(
... dialect=dialect,
... index_name="idx_uniq_abc",
... table_name="t",
... columns=["a", "b", "c"],
... unique=True,
... nulls_not_distinct_unique=True,
... )
>>> sql, params = idx.to_sql()
>>> sql
'CREATE UNIQUE INDEX "idx_uniq_abc" ON "t" ("a", "b", "c") NULLS NOT DISTINCT'
"""

def __init__(
self,
dialect: "SQLDialectBase",
index_name: str,
table_name: str,
columns: List[Union[str, "BaseExpression"]],
unique: bool = False,
if_not_exists: bool = False,
index_type: Optional[str] = None,
where: Optional["SQLPredicate"] = None,
include: Optional[List[str]] = None,
tablespace: Optional[str] = None,
concurrent: bool = False,
nulls_not_distinct_unique: bool = False,
*,
dialect_options: Optional[Dict[str, Any]] = None,
):
merged_options = dict(dialect_options or {})
if nulls_not_distinct_unique:
merged_options["nulls_not_distinct"] = True
super().__init__(
dialect=dialect,
index_name=index_name,
table_name=table_name,
columns=columns,
unique=unique,
if_not_exists=if_not_exists,
index_type=index_type,
where=where,
include=include,
tablespace=tablespace,
concurrent=concurrent,
dialect_options=merged_options,
)

def to_sql(self) -> Tuple[str, tuple]:
return self.dialect.format_create_index_statement(self)


class PostgresDropIndexExpression(DropIndexExpression):
"""PostgreSQL-specific DROP INDEX expression.

Extends the standard DropIndexExpression with PostgreSQL-specific
features such as CONCURRENTLY (PG 18+).

``concurrent`` is stored in ``dialect_options["concurrent"]`` and
consumed by ``PostgresIndexMixin.format_drop_index_statement``.

Example:
>>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect
>>> dialect = PostgresDialect((18, 0, 0))
>>> expr = PostgresDropIndexExpression(
... dialect=dialect,
... index_name="idx_old",
... concurrent=True,
... )
>>> sql, params = expr.to_sql()
>>> sql
'DROP INDEX CONCURRENTLY "idx_old"'
"""

def __init__(
self,
dialect: "SQLDialectBase",
index_name: str,
table_name: Optional[str] = None,
if_exists: bool = False,
concurrent: bool = False,
*,
dialect_options: Optional[Dict[str, Any]] = None,
):
merged_options = dict(dialect_options or {})
if concurrent:
merged_options["concurrent"] = True
super().__init__(
dialect=dialect,
index_name=index_name,
table_name=table_name,
if_exists=if_exists,
dialect_options=merged_options,
)

def to_sql(self) -> Tuple[str, tuple]:
return self.dialect.format_drop_index_statement(self)


class PostgresAlterIndexActionType(Enum):
"""Action types for ALTER INDEX statement."""

RENAME_TO = "RENAME TO"
SET_TABLESPACE = "SET TABLESPACE"
SET_STORAGE_PARAMETERS = "SET"
RESET_STORAGE_PARAMETERS = "RESET"
ALTER_COLUMN_STATISTICS = "ALTER COLUMN SET STATISTICS"
ALL_IN_TABLESPACE = "ALL IN TABLESPACE"


class PostgresAlterIndexExpression(BaseExpression):
"""PostgreSQL ALTER INDEX statement expression.

Supports all ALTER INDEX operations:
- Rename index
- Move index to a different tablespace
- Set/reset storage parameters
- Alter per-column statistics target
- Bulk tablespace move with ALL IN TABLESPACE

Example:
>>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect
>>> dialect = PostgresDialect()
>>> # Rename index
>>> expr = PostgresAlterIndexExpression(
... dialect, "idx_old",
... action_type=PostgresAlterIndexActionType.RENAME_TO,
... new_name="idx_new",
... )
>>> sql, params = expr.to_sql()
>>> sql
'ALTER INDEX "idx_old" RENAME TO "idx_new"'
"""

def __init__(
self,
dialect: "SQLDialectBase",
index_name: str,
action_type: PostgresAlterIndexActionType,
*,
if_exists: bool = False,
new_name: Optional[str] = None,
tablespace: Optional[str] = None,
storage_parameters: Optional[Dict[str, Any]] = None,
column_number: Optional[int] = None,
statistics_target: Optional[int] = None,
source_tablespace: Optional[str] = None,
target_tablespace: Optional[str] = None,
nowait: bool = False,
dialect_options: Optional[Dict[str, Any]] = None,
):
super().__init__(dialect)
self.index_name = index_name
self.action_type = action_type
self.if_exists = if_exists
self.new_name = new_name
self.tablespace = tablespace
self.storage_parameters = storage_parameters
self.column_number = column_number
self.statistics_target = statistics_target
self.source_tablespace = source_tablespace
self.target_tablespace = target_tablespace
self.nowait = nowait
self.dialect_options = dialect_options or {}

def to_sql(self) -> Tuple[str, tuple]:
return self.dialect.format_alter_index_statement(self)


class PostgresReindexExpression(BaseExpression):
Expand Down
Loading
Loading