Skip to content
Closed
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/50.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
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`.
2 changes: 2 additions & 0 deletions src/rhosocial/activerecord/backend/impl/postgres/dialect.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
PostgresLockingMixin,
# Introspection capability mixin
PostgresIntrospectionCapabilityMixin,
PostgresAlterColumnModifierMixin,
)

from .collation import validate_postgres_collation_name
Expand Down Expand Up @@ -348,6 +349,7 @@ class PostgresDialect(
# PostgreSQL-specific mixins
PostgresExtensionMixin,
PostgresMaterializedViewMixin,
PostgresAlterColumnModifierMixin, # Before TableMixin/ConstraintMixin to override format_*_action
PostgresTableMixin, # Before TableMixin to override supports_table_like_syntax
TableMixin,
ConstraintMixin,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .ddl.comment import PostgresCommentMixin
from .ddl.type import PostgresTypeMixin
from .ddl.constraint import PostgresConstraintMixin
from .ddl.column import PostgresAlterColumnModifierMixin

# DML mixins
from .dml.vacuum import PostgresVacuumMixin
Expand Down Expand Up @@ -116,6 +117,7 @@
"PostgresCommentMixin",
"PostgresTypeMixin",
"PostgresConstraintMixin",
"PostgresAlterColumnModifierMixin",
# DML mixins
"PostgresVacuumMixin",
"PostgresStoredProcedureMixin",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .comment import PostgresCommentMixin
from .type import PostgresTypeMixin
from .constraint import PostgresConstraintMixin
from .column import PostgresAlterColumnModifierMixin

__all__ = [
"PostgresPartitionMixin",
Expand All @@ -15,4 +16,5 @@
"PostgresCommentMixin",
"PostgresTypeMixin",
"PostgresConstraintMixin",
"PostgresAlterColumnModifierMixin",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/column.py
"""PostgreSQL ALTER TABLE IF [NOT] EXISTS qualifier formatting."""

from typing import Tuple


class PostgresAlterColumnModifierMixin:
"""PostgreSQL IF [NOT] EXISTS qualifiers for ALTER TABLE actions.

PostgreSQL has supported these qualifiers since 9.6:
- ADD [COLUMN] [IF NOT EXISTS]
- DROP [COLUMN] [IF EXISTS]
- DROP CONSTRAINT [IF EXISTS]

The qualifiers are vendor extensions (not in ISO/IEC 9075-2 §11.10);
applications opt in by passing ``if_not_exists`` / ``if_exists`` on
the corresponding ``AddColumn`` / ``DropColumn`` /
``DropTableConstraint`` actions. ``None`` (default) means "no
qualifier", matching the SQL standard form.
"""

def supports_add_column_if_not_exists(self) -> bool:
"""``ADD COLUMN IF NOT EXISTS`` is supported since PostgreSQL 9.6."""
return True

def supports_drop_column_if_exists(self) -> bool:
"""``DROP COLUMN IF EXISTS`` is supported since PostgreSQL 9.6."""
return True

def supports_drop_constraint_if_exists(self) -> bool:
"""``DROP CONSTRAINT IF EXISTS`` is supported since PostgreSQL 9.6."""
return True

def format_add_column_action(self, action) -> Tuple[str, tuple]:
column_sql, column_params = self.format_column_definition(action.column)
if getattr(action, "if_not_exists", None) is True:
return f"ADD COLUMN IF NOT EXISTS {column_sql}", column_params
return f"ADD COLUMN {column_sql}", column_params

def format_drop_column_action(self, action) -> Tuple[str, tuple]:
if getattr(action, "if_exists", None) is True:
return f"DROP COLUMN IF EXISTS {self.format_identifier(action.column_name)}", ()
return f"DROP COLUMN {self.format_identifier(action.column_name)}", ()

def format_drop_table_constraint_action(self, action) -> Tuple[str, tuple]:
result = "DROP CONSTRAINT"
if getattr(action, "if_exists", None) is True:
result = f"{result} IF EXISTS"
result = f"{result} {self.format_identifier(action.constraint_name)}"
if getattr(action, "cascade", None):
result = f"{result} CASCADE"
return result, ()
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,11 @@ def supports_exclude_constraint(self) -> bool:
Supported by all PostgreSQL versions.
"""
return True

def supports_drop_constraint_if_exists(self) -> bool:
"""``DROP CONSTRAINT IF EXISTS`` (vendor extension).

PostgreSQL has supported the ``IF EXISTS`` qualifier on
``DROP CONSTRAINT`` since 9.6.
"""
return True
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,11 @@ def supports_exclude_constraint(self) -> bool:
Requires appropriate operator class (e.g., btree_gist for gist).
"""
...

def supports_drop_constraint_if_exists(self) -> bool:
"""Whether ``DROP CONSTRAINT IF EXISTS`` is supported.

Vendor extension (not in ISO/IEC 9075-2 §11.10). PostgreSQL has
supported it since 9.6.
"""
...
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_alter_table_if_exists.py
"""Tests for ALTER TABLE IF [NOT] EXISTS qualifier rendering on PostgreSQL.

PostgreSQL supports the vendor extensions since 9.6:
- ADD COLUMN IF NOT EXISTS
- DROP COLUMN IF EXISTS
- DROP CONSTRAINT IF EXISTS

The qualifiers are opt-in via ``if_not_exists`` / ``if_exists`` on the
``AddColumn`` / ``DropColumn`` / ``DropTableConstraint`` actions; ``None``
(the default) renders the plain SQL-standard form.
"""

import pytest

from rhosocial.activerecord.backend.expression.statements import ColumnDefinition
from rhosocial.activerecord.backend.expression.statements.ddl_alter import (
AddColumn,
AlterTableExpression,
DropColumn,
DropTableConstraint,
)
from rhosocial.activerecord.backend.expression.types import TextType
from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect


@pytest.fixture
def dialect():
return PostgresDialect(version=(15, 0, 0))


class TestPostgresAlterTableModifierCapabilities:
def test_supports_switches(self, dialect):
assert dialect.supports_add_column_if_not_exists() is True
assert dialect.supports_drop_column_if_exists() is True
assert dialect.supports_drop_constraint_if_exists() is True


class TestPostgresAddColumnIfNotExists:
def test_if_not_exists_renders_qualifier(self, dialect):
action = AddColumn(
dialect,
ColumnDefinition("content", TextType()),
if_not_exists=True,
)
sql, params = action.to_sql()
assert 'ADD COLUMN IF NOT EXISTS "content" TEXT' == sql
assert params == ()

def test_none_renders_plain_form(self, dialect):
action = AddColumn(dialect, ColumnDefinition("content", TextType()))
sql, params = action.to_sql()
assert 'ADD COLUMN "content" TEXT' == sql
assert "IF NOT EXISTS" not in sql
assert params == ()

def test_inside_alter_table(self, dialect):
action = AddColumn(
dialect,
ColumnDefinition("content", TextType()),
if_not_exists=True,
)
expr = AlterTableExpression(
dialect, table_name="users", actions=[action]
)
sql, params = expr.to_sql()
assert 'ALTER TABLE "users"' in sql
assert 'ADD COLUMN IF NOT EXISTS "content" TEXT' in sql
assert params == ()


class TestPostgresDropColumnIfExists:
def test_if_exists_renders_qualifier(self, dialect):
action = DropColumn(dialect, column_name="x", if_exists=True)
sql, params = action.to_sql()
assert 'DROP COLUMN IF EXISTS "x"' == sql
assert params == ()

def test_none_renders_plain_form(self, dialect):
action = DropColumn(dialect, column_name="x")
sql, params = action.to_sql()
assert 'DROP COLUMN "x"' == sql
assert "IF EXISTS" not in sql
assert params == ()


class TestPostgresDropConstraintIfExists:
def test_if_exists_renders_qualifier(self, dialect):
action = DropTableConstraint(
dialect, constraint_name="fkey", if_exists=True
)
sql, params = action.to_sql()
assert 'DROP CONSTRAINT IF EXISTS "fkey"' == sql
assert params == ()

def test_if_exists_with_cascade(self, dialect):
action = DropTableConstraint(
dialect, constraint_name="fkey", if_exists=True, cascade=True
)
sql, params = action.to_sql()
assert 'DROP CONSTRAINT IF EXISTS "fkey" CASCADE' == sql
assert params == ()

def test_none_renders_plain_form(self, dialect):
action = DropTableConstraint(dialect, constraint_name="fkey")
sql, params = action.to_sql()
assert 'DROP CONSTRAINT "fkey"' == sql
assert "IF EXISTS" not in sql
assert params == ()
Loading