]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
PostgreSQL WITH options: proper bool/None rendering; support CreateView
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 13 Jul 2026 21:29:50 +0000 (17:29 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 15 Jul 2026 12:52:45 +0000 (08:52 -0400)
Added ``postgresql_with`` support to :class:`.CreateView` for specifying
PostgreSQL view options such as ``security_invoker``, ``security_barrier``,
and ``check_option``, rendered as a ``WITH (...)`` clause between the view
name and the ``AS`` keyword.

Additionally, the ``postgresql_with`` parameter accepted by
:class:`.Table` and :class:`.Index` now correctly renders Python boolean
values as ``true``/``false`` (lowercase), and ``None`` values as the
parameter name alone without an ``= value`` portion.

A shared ``_prepare_withclause_opts`` helper in ``PGDDLCompiler``
is used by all three constructs for consistent formatting.

Fixes: #11122
Fixes: #13432
Closes: #11296
pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/11296
pull-request-sha: e1f79a2f7bcf4a07da65040c8cfba0561d515ffc
Change-Id: I9c9b29e01b087215ca6caf1b8e0db5a5b0753b31

doc/build/changelog/unreleased_21/11122.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/sql/compiler.py
test/dialect/postgresql/test_compiler.py

diff --git a/doc/build/changelog/unreleased_21/11122.rst b/doc/build/changelog/unreleased_21/11122.rst
new file mode 100644 (file)
index 0000000..044c47f
--- /dev/null
@@ -0,0 +1,12 @@
+.. change::
+    :tags: usecase, postgresql
+    :tickets: 11122, 13432
+
+    Added ``postgresql_with`` support to :class:`.CreateView` for specifying
+    PostgreSQL view options such as ``security_invoker``, ``security_barrier``,
+    and ``check_option``, rendered as a ``WITH (...)`` clause between the view
+    name and the ``AS`` keyword. Additionally, the ``postgresql_with`` parameter
+    accepted by :class:`_schema.Table` and :class:`_schema.Index` now correctly
+    renders Python boolean values as ``true``/``false`` (lowercase), and
+    ``None`` values as the parameter name alone without an ``= value`` portion.
+    Pull request courtesy alphavector.
index 5a815bfa7ba646fc2fd760e6067b361fdb9e3a5d..e6070608f3e53f8e3dfe895e984c18a0998e242a 100644 (file)
@@ -1324,7 +1324,7 @@ list within the ``WITH`` clause. For example::
         Column("id", Integer, primary_key=True),
         postgresql_with={
             "fillfactor": 70,
-            "autovacuum_enabled": "false",
+            "autovacuum_enabled": False,
             "toast.vacuum_truncate": True,
         },
     )
@@ -1336,9 +1336,11 @@ This will generate DDL similar to:
     CREATE TABLE mytable (
         id INTEGER NOT NULL,
         PRIMARY KEY (id)
-    ) WITH (fillfactor = 70, autovacuum_enabled = false, toast.vacuum_truncate = True)
+    ) WITH (fillfactor = 70, autovacuum_enabled = false, toast.vacuum_truncate = true)
 
-The values in the dictionary can be integers, strings, or boolean values.
+The values in the dictionary can be integers, strings, boolean values, or
+``None``. Boolean values are rendered as lowercase ``true``/``false``.
+A ``None`` value renders the parameter name only, without an ``=`` value.
 Parameter names can include dots to specify parameters for associated objects
 like toast tables (e.g., ``toast.vacuum_truncate``).
 
@@ -1372,6 +1374,40 @@ in modern PostgreSQL versions).
 
     Table("some_table", metadata, ..., postgresql_with_oids=False)
 
+.. _postgresql_view_options:
+
+PostgreSQL View Options
+-----------------------
+
+The following sections indicate options which are supported by the PostgreSQL
+dialect in conjunction with :class:`.CreateView`.
+
+``WITH``
+^^^^^^^^
+
+Specifies view-level options for :class:`.CreateView` using the ``WITH``
+clause::
+
+    from sqlalchemy import CreateView, select
+
+    CreateView(
+        select(my_table),
+        "my_view",
+        postgresql_with={"security_invoker": True},
+    )
+
+This generates:
+
+.. sourcecode:: sql
+
+    CREATE VIEW my_view WITH (security_invoker = true) AS SELECT ...
+
+The same formatting rules that apply to :ref:`postgresql_table_options_with`
+apply here: booleans render as lowercase ``true``/``false``, ``None``
+renders as the parameter name only, and other values render as-is.
+
+.. versionadded:: 2.1
+
 .. _postgresql_constraint_options:
 
 PostgreSQL Constraint Options
@@ -2710,6 +2746,26 @@ class PGDDLCompiler(compiler.DDLCompiler):
         domain = drop.element
         return f"DROP DOMAIN {self.preparer.format_type(domain)}"
 
+    def _prepare_withclause_opts(self, withclause):
+        with_opts = []
+        for param, value in withclause.items():
+            if value is None:
+                with_opts.append(param)
+            elif isinstance(value, bool):
+                with_opts.append(
+                    "%s = %s" % (param, "true" if value else "false")
+                )
+            else:
+                with_opts.append("%s = %s" % (param, value))
+        return ", ".join(with_opts)
+
+    def create_table_select_suffixes(self, element, type_, **kw):
+        if type_ == "view":
+            withclause = element.dialect_options["postgresql"]["with"]
+            if withclause:
+                return "WITH (%s)" % self._prepare_withclause_opts(withclause)
+        return ""
+
     def visit_create_index(self, create, **kw):
         preparer = self.preparer
         index = create.element
@@ -2775,14 +2831,7 @@ class PGDDLCompiler(compiler.DDLCompiler):
 
         withclause = index.dialect_options["postgresql"]["with"]
         if withclause:
-            text += " WITH (%s)" % (
-                ", ".join(
-                    [
-                        "%s = %s" % storage_parameter
-                        for storage_parameter in withclause.items()
-                    ]
-                )
-            )
+            text += " WITH (%s)" % self._prepare_withclause_opts(withclause)
 
         tablespace_name = index.dialect_options["postgresql"]["tablespace"]
         if tablespace_name:
@@ -2880,8 +2929,9 @@ class PGDDLCompiler(compiler.DDLCompiler):
             table_opts.append("\n USING %s" % pg_opts["using"])
 
         if pg_opts["with"]:
-            storage_params = (f"{k} = {v}" for k, v in pg_opts["with"].items())
-            table_opts.append(f" WITH ({', '.join(storage_params)})")
+            table_opts.append(
+                "\n WITH (%s)" % self._prepare_withclause_opts(pg_opts["with"])
+            )
 
         if pg_opts["with_oids"] is True:
             table_opts.append("\n WITH OIDS")
@@ -3556,6 +3606,12 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
                 "nulls_not_distinct": None,
             },
         ),
+        (
+            schema.CreateView,
+            {
+                "with": None,
+            },
+        ),
     ]
 
     reflection_options = ("postgresql_ignore_search_path",)
index 42b64ec726391b238bddfac8e4c804897197d06d..1c0e9a38de521975e0d6c75c2a1a87e25ba47355 100644 (file)
@@ -7116,6 +7116,14 @@ class DDLCompiler(Compiled):
     def visit_create_table_as(self, element: CreateTableAs, **kw: Any) -> str:
         return self._generate_table_select(element, "create_table_as", **kw)
 
+    def create_table_select_suffixes(
+        self,
+        element: _TableViaSelect,
+        type_: str,
+        **kw: Any,
+    ) -> str:
+        return ""
+
     def _generate_table_select(
         self,
         element: _TableViaSelect,
@@ -7136,7 +7144,7 @@ class DDLCompiler(Compiled):
             else element.if_not_exists
         )
 
-        parts = [
+        parts: List[Optional[str]] = [
             "CREATE",
             "OR REPLACE" if getattr(element, "or_replace", False) else None,
             "TEMPORARY" if element.temporary else None,
@@ -7147,9 +7155,11 @@ class DDLCompiler(Compiled):
             ),
             "IF NOT EXISTS" if use_if_not_exists else None,
             prep.format_table(element.table),
-            "AS",
-            select_sql,
         ]
+        suffixes = self.create_table_select_suffixes(element, type_, **kw)
+        if suffixes:
+            parts.append(suffixes)
+        parts += ["AS", select_sql]
         return " ".join(p for p in parts if p)
 
     def visit_create_column(self, create, first_pk=False, **kw):
index 2570bbe9018908ec6d2581340ab9657b6ef5f55c..b2da47cd08a3a007f681f63f70f9ba77eed41431 100644 (file)
@@ -12,6 +12,7 @@ from sqlalchemy import cast
 from sqlalchemy import CheckConstraint
 from sqlalchemy import Column
 from sqlalchemy import Computed
+from sqlalchemy import CreateView
 from sqlalchemy import Date
 from sqlalchemy import delete
 from sqlalchemy import Enum
@@ -633,7 +634,28 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
         self.assert_compile(
             schema.CreateTable(tbl3),
             "CREATE TABLE atable3 () "
-            "WITH (user_catalog_table = False, parallel_workers = 15)",
+            "WITH (user_catalog_table = false, parallel_workers = 15)",
+        )
+
+        tbl4 = Table(
+            "atable4",
+            m,
+            Column("id", Integer),
+            postgresql_with={
+                "autovacuum_enabled": True,
+                "autovacuum_analyze_scale_factor": 0.2,
+                "vacuum_index_cleanup": "auto",
+                "vacuum_truncate": None,
+            },
+        )
+
+        self.assert_compile(
+            schema.CreateTable(tbl4),
+            "CREATE TABLE atable4 (id INTEGER) "
+            "WITH (autovacuum_enabled = true, "
+            "autovacuum_analyze_scale_factor = 0.2, "
+            "vacuum_index_cleanup = auto, "
+            "vacuum_truncate)",
         )
 
     def test_create_table_with_oncommit_option(self):
@@ -676,6 +698,35 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
             "ON COMMIT PRESERVE ROWS TABLESPACE sometablespace",
         )
 
+    def test_create_view_with_options(self):
+        src = table("src", column("id"), column("name"))
+
+        stmt = CreateView(
+            select(src.c.id, src.c.name),
+            "my_view",
+            postgresql_with={"security_invoker": True},
+        )
+        self.assert_compile(
+            stmt,
+            "CREATE VIEW my_view WITH (security_invoker = true) "
+            "AS SELECT src.id, src.name FROM src",
+        )
+
+        stmt2 = CreateView(
+            select(src.c.id),
+            "other_view",
+            postgresql_with={
+                "security_barrier": True,
+                "check_option": "local",
+            },
+        )
+        self.assert_compile(
+            stmt2,
+            "CREATE VIEW other_view "
+            "WITH (security_barrier = true, check_option = local) "
+            "AS SELECT src.id FROM src",
+        )
+
     def test_create_partial_index(self):
         m = MetaData()
         tbl = Table("testtbl", m, Column("data", Integer))
@@ -1022,6 +1073,26 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
             postgresql_with={"buffering": "off"},
         )
 
+        idx4 = Index(
+            "test_idx4",
+            tbl.c.data,
+            postgresql_using="gin",
+            postgresql_with={
+                "fastupdate": False,
+                "gin_pending_list_limit": 4096,
+            },
+        )
+
+        idx5 = Index(
+            "test_idx5",
+            tbl.c.data,
+            postgresql_using="brin",
+            postgresql_with={
+                "pages_per_range": 1,
+                "autosummarize": None,
+            },
+        )
+
         self.assert_compile(
             schema.CreateIndex(idx1),
             "CREATE INDEX test_idx1 ON testtbl (data)",
@@ -1036,6 +1107,18 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
             "USING gist (data) "
             "WITH (buffering = off)",
         )
+        self.assert_compile(
+            schema.CreateIndex(idx4),
+            "CREATE INDEX test_idx4 ON testtbl "
+            "USING gin (data) "
+            "WITH (fastupdate = false, gin_pending_list_limit = 4096)",
+        )
+        self.assert_compile(
+            schema.CreateIndex(idx5),
+            "CREATE INDEX test_idx5 ON testtbl "
+            "USING brin (data) "
+            "WITH (pages_per_range = 1, autosummarize)",
+        )
 
     def test_create_index_with_using_unusual_conditions(self):
         m = MetaData()