1# dialects/postgresql/base.py
2# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7# mypy: ignore-errors
8
9r"""
10.. dialect:: postgresql
11 :name: PostgreSQL
12 :normal_support: 9.6+
13 :best_effort: 9+
14
15.. _postgresql_sequences:
16
17Sequences/SERIAL/IDENTITY
18-------------------------
19
20PostgreSQL supports sequences, and SQLAlchemy uses these as the default means
21of creating new primary key values for integer-based primary key columns. When
22creating tables, SQLAlchemy will issue the ``SERIAL`` datatype for
23integer-based primary key columns, which generates a sequence and server side
24default corresponding to the column.
25
26To specify a specific named sequence to be used for primary key generation,
27use the :func:`~sqlalchemy.schema.Sequence` construct::
28
29 Table(
30 "sometable",
31 metadata,
32 Column(
33 "id", Integer, Sequence("some_id_seq", start=1), primary_key=True
34 ),
35 )
36
37When SQLAlchemy issues a single INSERT statement, to fulfill the contract of
38having the "last insert identifier" available, a RETURNING clause is added to
39the INSERT statement which specifies the primary key columns should be
40returned after the statement completes. The RETURNING functionality only takes
41place if PostgreSQL 8.2 or later is in use. As a fallback approach, the
42sequence, whether specified explicitly or implicitly via ``SERIAL``, is
43executed independently beforehand, the returned value to be used in the
44subsequent insert. Note that when an
45:func:`~sqlalchemy.sql.expression.insert()` construct is executed using
46"executemany" semantics, the "last inserted identifier" functionality does not
47apply; no RETURNING clause is emitted nor is the sequence pre-executed in this
48case.
49
50
51PostgreSQL 10 and above IDENTITY columns
52^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
53
54PostgreSQL 10 and above have a new IDENTITY feature that supersedes the use
55of SERIAL. The :class:`_schema.Identity` construct in a
56:class:`_schema.Column` can be used to control its behavior::
57
58 from sqlalchemy import Table, Column, MetaData, Integer, Computed
59
60 metadata = MetaData()
61
62 data = Table(
63 "data",
64 metadata,
65 Column(
66 "id", Integer, Identity(start=42, cycle=True), primary_key=True
67 ),
68 Column("data", String),
69 )
70
71The CREATE TABLE for the above :class:`_schema.Table` object would be:
72
73.. sourcecode:: sql
74
75 CREATE TABLE data (
76 id INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 42 CYCLE),
77 data VARCHAR,
78 PRIMARY KEY (id)
79 )
80
81.. versionchanged:: 1.4 Added :class:`_schema.Identity` construct
82 in a :class:`_schema.Column` to specify the option of an autoincrementing
83 column.
84
85.. note::
86
87 Previous versions of SQLAlchemy did not have built-in support for rendering
88 of IDENTITY, and could use the following compilation hook to replace
89 occurrences of SERIAL with IDENTITY::
90
91 from sqlalchemy.schema import CreateColumn
92 from sqlalchemy.ext.compiler import compiles
93
94
95 @compiles(CreateColumn, "postgresql")
96 def use_identity(element, compiler, **kw):
97 text = compiler.visit_create_column(element, **kw)
98 text = text.replace("SERIAL", "INT GENERATED BY DEFAULT AS IDENTITY")
99 return text
100
101 Using the above, a table such as::
102
103 t = Table(
104 "t", m, Column("id", Integer, primary_key=True), Column("data", String)
105 )
106
107 Will generate on the backing database as:
108
109 .. sourcecode:: sql
110
111 CREATE TABLE t (
112 id INT GENERATED BY DEFAULT AS IDENTITY,
113 data VARCHAR,
114 PRIMARY KEY (id)
115 )
116
117.. _postgresql_monotonic_functions:
118
119PostgreSQL 18 and above UUID with uuidv7 as a server default
120^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
121
122PostgreSQL 18's ``uuidv7`` SQL function is available as any other
123SQL function using the :data:`_sql.func` namespace::
124
125 >>> from sqlalchemy import select, func
126 >>> print(select(func.uuidv7()))
127 SELECT uuidv7() AS uuidv7_1
128
129When using ``func.uuidv7()`` as a default on a :class:`.Column` using either
130Core or ORM, an extra directive ``monotonic=True`` may be passed which
131indicates this function produces monotonically increasing values; this in turn
132allows Core and ORM to use a more efficient batched form of INSERT for large
133insert operations::
134
135 import uuid
136
137
138 class MyClass(Base):
139 __tablename__ = "my_table"
140
141 id: Mapped[uuid.UUID] = mapped_column(
142 server_default=func.uuidv7(monotonic=True)
143 )
144
145With the above mapping, the ORM will be able to efficiently batch rows when
146running bulk insert operations using the :ref:`engine_insertmanyvalues`
147feature.
148
149.. versionadded:: 2.1
150 Added ``monotonic=True`` to allow functions like PostgreSQL's
151 ``uuidv7()`` to work with batched "insertmanyvalues"
152
153.. seealso::
154
155 :ref:`engine_insertmanyvalues_monotonic_functions`
156
157
158.. _postgresql_ss_cursors:
159
160Server Side Cursors
161-------------------
162
163Server-side cursor support is available for the psycopg2, asyncpg
164dialects and may also be available in others.
165
166Server side cursors are enabled on a per-statement basis by using the
167:paramref:`.Connection.execution_options.stream_results` connection execution
168option::
169
170 with engine.connect() as conn:
171 result = conn.execution_options(stream_results=True).execute(
172 text("select * from table")
173 )
174
175Note that some kinds of SQL statements may not be supported with
176server side cursors; generally, only SQL statements that return rows should be
177used with this option.
178
179.. deprecated:: 1.4 The dialect-level server_side_cursors flag is deprecated
180 and will be removed in a future release. Please use the
181 :paramref:`_engine.Connection.stream_results` execution option for
182 unbuffered cursor support.
183
184.. seealso::
185
186 :ref:`engine_stream_results`
187
188.. _postgresql_isolation_level:
189
190Transaction Isolation Level
191---------------------------
192
193Most SQLAlchemy dialects support setting of transaction isolation level
194using the :paramref:`_sa.create_engine.isolation_level` parameter
195at the :func:`_sa.create_engine` level, and at the :class:`_engine.Connection`
196level via the :paramref:`.Connection.execution_options.isolation_level`
197parameter.
198
199For PostgreSQL dialects, this feature works either by making use of the
200DBAPI-specific features, such as psycopg2's isolation level flags which will
201embed the isolation level setting inline with the ``"BEGIN"`` statement, or for
202DBAPIs with no direct support by emitting ``SET SESSION CHARACTERISTICS AS
203TRANSACTION ISOLATION LEVEL <level>`` ahead of the ``"BEGIN"`` statement
204emitted by the DBAPI. For the special AUTOCOMMIT isolation level,
205DBAPI-specific techniques are used which is typically an ``.autocommit``
206flag on the DBAPI connection object.
207
208To set isolation level using :func:`_sa.create_engine`::
209
210 engine = create_engine(
211 "postgresql+pg8000://scott:tiger@localhost/test",
212 isolation_level="REPEATABLE READ",
213 )
214
215To set using per-connection execution options::
216
217 with engine.connect() as conn:
218 conn = conn.execution_options(isolation_level="REPEATABLE READ")
219 with conn.begin():
220 ... # work with transaction
221
222There are also more options for isolation level configurations, such as
223"sub-engine" objects linked to a main :class:`_engine.Engine` which each apply
224different isolation level settings. See the discussion at
225:ref:`dbapi_autocommit` for background.
226
227Valid values for ``isolation_level`` on most PostgreSQL dialects include:
228
229* ``READ COMMITTED``
230* ``READ UNCOMMITTED``
231* ``REPEATABLE READ``
232* ``SERIALIZABLE``
233* ``AUTOCOMMIT``
234
235.. seealso::
236
237 :ref:`dbapi_autocommit`
238
239 :ref:`postgresql_readonly_deferrable`
240
241 :ref:`psycopg2_isolation_level`
242
243 :ref:`pg8000_isolation_level`
244
245.. _postgresql_readonly_deferrable:
246
247Setting READ ONLY / DEFERRABLE
248------------------------------
249
250Most PostgreSQL dialects support setting the "READ ONLY" and "DEFERRABLE"
251characteristics of the transaction, which is in addition to the isolation level
252setting. These two attributes can be established either in conjunction with or
253independently of the isolation level by passing the ``postgresql_readonly`` and
254``postgresql_deferrable`` flags with
255:meth:`_engine.Connection.execution_options`. The example below illustrates
256passing the ``"SERIALIZABLE"`` isolation level at the same time as setting
257"READ ONLY" and "DEFERRABLE"::
258
259 with engine.connect() as conn:
260 conn = conn.execution_options(
261 isolation_level="SERIALIZABLE",
262 postgresql_readonly=True,
263 postgresql_deferrable=True,
264 )
265 with conn.begin():
266 ... # work with transaction
267
268Note that some DBAPIs such as asyncpg only support "readonly" with
269SERIALIZABLE isolation.
270
271.. versionadded:: 1.4
272 Added support for the ``postgresql_readonly``
273 and ``postgresql_deferrable`` execution options.
274
275.. _postgresql_reset_on_return:
276
277Temporary Table / Resource Reset for Connection Pooling
278-------------------------------------------------------
279
280The :class:`.QueuePool` connection pool implementation used
281by the SQLAlchemy :class:`.Engine` object includes
282:ref:`reset on return <pool_reset_on_return>` behavior that will invoke
283the DBAPI ``.rollback()`` method when connections are returned to the pool.
284While this rollback will clear out the immediate state used by the previous
285transaction, it does not cover a wider range of session-level state, including
286temporary tables as well as other server state such as prepared statement
287handles and statement caches. The PostgreSQL database includes a variety
288of commands which may be used to reset this state, including
289``DISCARD``, ``RESET``, ``DEALLOCATE``, and ``UNLISTEN``.
290
291
292To install
293one or more of these commands as the means of performing reset-on-return,
294the :meth:`.PoolEvents.reset` event hook may be used, as demonstrated
295in the example below. The implementation
296will end transactions in progress as well as discard temporary tables
297using the ``CLOSE``, ``RESET`` and ``DISCARD`` commands; see the PostgreSQL
298documentation for background on what each of these statements do.
299
300The :paramref:`_sa.create_engine.pool_reset_on_return` parameter
301is set to ``None`` so that the custom scheme can replace the default behavior
302completely. The custom hook implementation calls ``.rollback()`` in any case,
303as it's usually important that the DBAPI's own tracking of commit/rollback
304will remain consistent with the state of the transaction::
305
306
307 from sqlalchemy import create_engine
308 from sqlalchemy import event
309
310 postgresql_engine = create_engine(
311 "postgresql+psycopg2://scott:tiger@hostname/dbname",
312 # disable default reset-on-return scheme
313 pool_reset_on_return=None,
314 )
315
316
317 @event.listens_for(postgresql_engine, "reset")
318 def _reset_postgresql(dbapi_connection, connection_record, reset_state):
319 if not reset_state.terminate_only:
320 dbapi_connection.execute("CLOSE ALL")
321 dbapi_connection.execute("RESET ALL")
322 dbapi_connection.execute("DISCARD TEMP")
323
324 # so that the DBAPI itself knows that the connection has been
325 # reset
326 dbapi_connection.rollback()
327
328.. versionchanged:: 2.0.0b3 Added additional state arguments to
329 the :meth:`.PoolEvents.reset` event and additionally ensured the event
330 is invoked for all "reset" occurrences, so that it's appropriate
331 as a place for custom "reset" handlers. Previous schemes which
332 use the :meth:`.PoolEvents.checkin` handler remain usable as well.
333
334.. seealso::
335
336 :ref:`pool_reset_on_return` - in the :ref:`pooling_toplevel` documentation
337
338.. _postgresql_alternate_search_path:
339
340Setting Alternate Search Paths on Connect
341------------------------------------------
342
343The PostgreSQL ``search_path`` variable refers to the list of schema names
344that will be implicitly referenced when a particular table or other
345object is referenced in a SQL statement. As detailed in the next section
346:ref:`postgresql_schema_reflection`, SQLAlchemy is generally organized around
347the concept of keeping this variable at its default value of ``public``,
348however, in order to have it set to any arbitrary name or names when connections
349are used automatically, the "SET SESSION search_path" command may be invoked
350for all connections in a pool using the following event handler, as discussed
351at :ref:`schema_set_default_connections`::
352
353 from sqlalchemy import event
354 from sqlalchemy import create_engine
355
356 engine = create_engine("postgresql+psycopg2://scott:tiger@host/dbname")
357
358
359 @event.listens_for(engine, "connect", insert=True)
360 def set_search_path(dbapi_connection, connection_record):
361 existing_autocommit = dbapi_connection.autocommit
362 dbapi_connection.autocommit = True
363 cursor = dbapi_connection.cursor()
364 cursor.execute("SET SESSION search_path='%s'" % schema_name)
365 cursor.close()
366 dbapi_connection.autocommit = existing_autocommit
367
368The reason the recipe is complicated by use of the ``.autocommit`` DBAPI
369attribute is so that when the ``SET SESSION search_path`` directive is invoked,
370it is invoked outside of the scope of any transaction and therefore will not
371be reverted when the DBAPI connection has a rollback.
372
373.. seealso::
374
375 :ref:`schema_set_default_connections` - in the :ref:`metadata_toplevel` documentation
376
377.. _postgresql_schema_reflection:
378
379Remote-Schema Table Introspection and PostgreSQL search_path
380------------------------------------------------------------
381
382.. admonition:: Section Best Practices Summarized
383
384 keep the ``search_path`` variable set to its default of ``public``, without
385 any other schema names. Ensure the username used to connect **does not**
386 match remote schemas, or ensure the ``"$user"`` token is **removed** from
387 ``search_path``. For other schema names, name these explicitly
388 within :class:`_schema.Table` definitions. Alternatively, the
389 ``postgresql_ignore_search_path`` option will cause all reflected
390 :class:`_schema.Table` objects to have a :attr:`_schema.Table.schema`
391 attribute set up.
392
393The PostgreSQL dialect can reflect tables from any schema, as outlined in
394:ref:`metadata_reflection_schemas`.
395
396In all cases, the first thing SQLAlchemy does when reflecting tables is
397to **determine the default schema for the current database connection**.
398It does this using the PostgreSQL ``current_schema()``
399function, illustated below using a PostgreSQL client session (i.e. using
400the ``psql`` tool):
401
402.. sourcecode:: sql
403
404 test=> select current_schema();
405 current_schema
406 ----------------
407 public
408 (1 row)
409
410Above we see that on a plain install of PostgreSQL, the default schema name
411is the name ``public``.
412
413However, if your database username **matches the name of a schema**, PostgreSQL's
414default is to then **use that name as the default schema**. Below, we log in
415using the username ``scott``. When we create a schema named ``scott``, **it
416implicitly changes the default schema**:
417
418.. sourcecode:: sql
419
420 test=> select current_schema();
421 current_schema
422 ----------------
423 public
424 (1 row)
425
426 test=> create schema scott;
427 CREATE SCHEMA
428 test=> select current_schema();
429 current_schema
430 ----------------
431 scott
432 (1 row)
433
434The behavior of ``current_schema()`` is derived from the
435`PostgreSQL search path
436<https://www.postgresql.org/docs/current/static/ddl-schemas.html#DDL-SCHEMAS-PATH>`_
437variable ``search_path``, which in modern PostgreSQL versions defaults to this:
438
439.. sourcecode:: sql
440
441 test=> show search_path;
442 search_path
443 -----------------
444 "$user", public
445 (1 row)
446
447Where above, the ``"$user"`` variable will inject the current username as the
448default schema, if one exists. Otherwise, ``public`` is used.
449
450When a :class:`_schema.Table` object is reflected, if it is present in the
451schema indicated by the ``current_schema()`` function, **the schema name assigned
452to the ".schema" attribute of the Table is the Python "None" value**. Otherwise, the
453".schema" attribute will be assigned the string name of that schema.
454
455With regards to tables which these :class:`_schema.Table`
456objects refer to via foreign key constraint, a decision must be made as to how
457the ``.schema`` is represented in those remote tables, in the case where that
458remote schema name is also a member of the current ``search_path``.
459
460By default, the PostgreSQL dialect mimics the behavior encouraged by
461PostgreSQL's own ``pg_get_constraintdef()`` builtin procedure. This function
462returns a sample definition for a particular foreign key constraint,
463omitting the referenced schema name from that definition when the name is
464also in the PostgreSQL schema search path. The interaction below
465illustrates this behavior:
466
467.. sourcecode:: sql
468
469 test=> CREATE TABLE test_schema.referred(id INTEGER PRIMARY KEY);
470 CREATE TABLE
471 test=> CREATE TABLE referring(
472 test(> id INTEGER PRIMARY KEY,
473 test(> referred_id INTEGER REFERENCES test_schema.referred(id));
474 CREATE TABLE
475 test=> SET search_path TO public, test_schema;
476 test=> SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM
477 test-> pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n
478 test-> ON n.oid = c.relnamespace
479 test-> JOIN pg_catalog.pg_constraint r ON c.oid = r.conrelid
480 test-> WHERE c.relname='referring' AND r.contype = 'f'
481 test-> ;
482 pg_get_constraintdef
483 ---------------------------------------------------
484 FOREIGN KEY (referred_id) REFERENCES referred(id)
485 (1 row)
486
487Above, we created a table ``referred`` as a member of the remote schema
488``test_schema``, however when we added ``test_schema`` to the
489PG ``search_path`` and then asked ``pg_get_constraintdef()`` for the
490``FOREIGN KEY`` syntax, ``test_schema`` was not included in the output of
491the function.
492
493On the other hand, if we set the search path back to the typical default
494of ``public``:
495
496.. sourcecode:: sql
497
498 test=> SET search_path TO public;
499 SET
500
501The same query against ``pg_get_constraintdef()`` now returns the fully
502schema-qualified name for us:
503
504.. sourcecode:: sql
505
506 test=> SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM
507 test-> pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n
508 test-> ON n.oid = c.relnamespace
509 test-> JOIN pg_catalog.pg_constraint r ON c.oid = r.conrelid
510 test-> WHERE c.relname='referring' AND r.contype = 'f';
511 pg_get_constraintdef
512 ---------------------------------------------------------------
513 FOREIGN KEY (referred_id) REFERENCES test_schema.referred(id)
514 (1 row)
515
516SQLAlchemy will by default use the return value of ``pg_get_constraintdef()``
517in order to determine the remote schema name. That is, if our ``search_path``
518were set to include ``test_schema``, and we invoked a table
519reflection process as follows::
520
521 >>> from sqlalchemy import Table, MetaData, create_engine, text
522 >>> engine = create_engine("postgresql+psycopg2://scott:tiger@localhost/test")
523 >>> with engine.connect() as conn:
524 ... conn.execute(text("SET search_path TO test_schema, public"))
525 ... metadata_obj = MetaData()
526 ... referring = Table("referring", metadata_obj, autoload_with=conn)
527 <sqlalchemy.engine.result.CursorResult object at 0x101612ed0>
528
529The above process would deliver to the :attr:`_schema.MetaData.tables`
530collection
531``referred`` table named **without** the schema::
532
533 >>> metadata_obj.tables["referred"].schema is None
534 True
535
536To alter the behavior of reflection such that the referred schema is
537maintained regardless of the ``search_path`` setting, use the
538``postgresql_ignore_search_path`` option, which can be specified as a
539dialect-specific argument to both :class:`_schema.Table` as well as
540:meth:`_schema.MetaData.reflect`::
541
542 >>> with engine.connect() as conn:
543 ... conn.execute(text("SET search_path TO test_schema, public"))
544 ... metadata_obj = MetaData()
545 ... referring = Table(
546 ... "referring",
547 ... metadata_obj,
548 ... autoload_with=conn,
549 ... postgresql_ignore_search_path=True,
550 ... )
551 <sqlalchemy.engine.result.CursorResult object at 0x1016126d0>
552
553We will now have ``test_schema.referred`` stored as schema-qualified::
554
555 >>> metadata_obj.tables["test_schema.referred"].schema
556 'test_schema'
557
558.. sidebar:: Best Practices for PostgreSQL Schema reflection
559
560 The description of PostgreSQL schema reflection behavior is complex, and
561 is the product of many years of dealing with widely varied use cases and
562 user preferences. But in fact, there's no need to understand any of it if
563 you just stick to the simplest use pattern: leave the ``search_path`` set
564 to its default of ``public`` only, never refer to the name ``public`` as
565 an explicit schema name otherwise, and refer to all other schema names
566 explicitly when building up a :class:`_schema.Table` object. The options
567 described here are only for those users who can't, or prefer not to, stay
568 within these guidelines.
569
570.. seealso::
571
572 :ref:`reflection_schema_qualified_interaction` - discussion of the issue
573 from a backend-agnostic perspective
574
575 `The Schema Search Path
576 <https://www.postgresql.org/docs/current/static/ddl-schemas.html#DDL-SCHEMAS-PATH>`_
577 - on the PostgreSQL website.
578
579INSERT/UPDATE...RETURNING
580-------------------------
581
582The dialect supports PG 8.2's ``INSERT..RETURNING``, ``UPDATE..RETURNING`` and
583``DELETE..RETURNING`` syntaxes. ``INSERT..RETURNING`` is used by default
584for single-row INSERT statements in order to fetch newly generated
585primary key identifiers. To specify an explicit ``RETURNING`` clause,
586use the :meth:`._UpdateBase.returning` method on a per-statement basis::
587
588 # INSERT..RETURNING
589 result = (
590 table.insert().returning(table.c.col1, table.c.col2).values(name="foo")
591 )
592 print(result.fetchall())
593
594 # UPDATE..RETURNING
595 result = (
596 table.update()
597 .returning(table.c.col1, table.c.col2)
598 .where(table.c.name == "foo")
599 .values(name="bar")
600 )
601 print(result.fetchall())
602
603 # DELETE..RETURNING
604 result = (
605 table.delete()
606 .returning(table.c.col1, table.c.col2)
607 .where(table.c.name == "foo")
608 )
609 print(result.fetchall())
610
611.. _postgresql_insert_on_conflict:
612
613INSERT...ON CONFLICT (Upsert)
614------------------------------
615
616Starting with version 9.5, PostgreSQL allows "upserts" (update or insert) of
617rows into a table via the ``ON CONFLICT`` clause of the ``INSERT`` statement. A
618candidate row will only be inserted if that row does not violate any unique
619constraints. In the case of a unique constraint violation, a secondary action
620can occur which can be either "DO UPDATE", indicating that the data in the
621target row should be updated, or "DO NOTHING", which indicates to silently skip
622this row.
623
624Conflicts are determined using existing unique constraints and indexes. These
625constraints may be identified either using their name as stated in DDL,
626or they may be inferred by stating the columns and conditions that comprise
627the indexes.
628
629SQLAlchemy provides ``ON CONFLICT`` support via the PostgreSQL-specific
630:func:`_postgresql.insert()` function, which provides
631the generative methods :meth:`_postgresql.Insert.on_conflict_do_update`
632and :meth:`~.postgresql.Insert.on_conflict_do_nothing`:
633
634.. sourcecode:: pycon+sql
635
636 >>> from sqlalchemy.dialects.postgresql import insert
637 >>> insert_stmt = insert(my_table).values(
638 ... id="some_existing_id", data="inserted value"
639 ... )
640 >>> do_nothing_stmt = insert_stmt.on_conflict_do_nothing(index_elements=["id"])
641 >>> print(do_nothing_stmt)
642 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s)
643 ON CONFLICT (id) DO NOTHING
644 {stop}
645
646 >>> do_update_stmt = insert_stmt.on_conflict_do_update(
647 ... constraint="pk_my_table", set_=dict(data="updated value")
648 ... )
649 >>> print(do_update_stmt)
650 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s)
651 ON CONFLICT ON CONSTRAINT pk_my_table DO UPDATE SET data = %(param_1)s
652
653.. seealso::
654
655 `INSERT .. ON CONFLICT
656 <https://www.postgresql.org/docs/current/static/sql-insert.html#SQL-ON-CONFLICT>`_
657 - in the PostgreSQL documentation.
658
659Specifying the Target
660^^^^^^^^^^^^^^^^^^^^^
661
662Both methods supply the "target" of the conflict using either the
663named constraint or by column inference:
664
665* The :paramref:`_postgresql.Insert.on_conflict_do_update.index_elements` argument
666 specifies a sequence containing string column names, :class:`_schema.Column`
667 objects, and/or SQL expression elements, which would identify a unique
668 index:
669
670 .. sourcecode:: pycon+sql
671
672 >>> do_update_stmt = insert_stmt.on_conflict_do_update(
673 ... index_elements=["id"], set_=dict(data="updated value")
674 ... )
675 >>> print(do_update_stmt)
676 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s)
677 ON CONFLICT (id) DO UPDATE SET data = %(param_1)s
678 {stop}
679
680 >>> do_update_stmt = insert_stmt.on_conflict_do_update(
681 ... index_elements=[my_table.c.id], set_=dict(data="updated value")
682 ... )
683 >>> print(do_update_stmt)
684 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s)
685 ON CONFLICT (id) DO UPDATE SET data = %(param_1)s
686
687* When using :paramref:`_postgresql.Insert.on_conflict_do_update.index_elements` to
688 infer an index, a partial index can be inferred by also specifying the
689 use the :paramref:`_postgresql.Insert.on_conflict_do_update.index_where` parameter:
690
691 .. sourcecode:: pycon+sql
692
693 >>> stmt = insert(my_table).values(user_email="a@b.com", data="inserted data")
694 >>> stmt = stmt.on_conflict_do_update(
695 ... index_elements=[my_table.c.user_email],
696 ... index_where=my_table.c.user_email.like("%@gmail.com"),
697 ... set_=dict(data=stmt.excluded.data),
698 ... )
699 >>> print(stmt)
700 {printsql}INSERT INTO my_table (data, user_email)
701 VALUES (%(data)s, %(user_email)s) ON CONFLICT (user_email)
702 WHERE user_email LIKE %(user_email_1)s DO UPDATE SET data = excluded.data
703
704* The :paramref:`_postgresql.Insert.on_conflict_do_update.constraint` argument is
705 used to specify an index directly rather than inferring it. This can be
706 the name of a UNIQUE constraint, a PRIMARY KEY constraint, or an INDEX:
707
708 .. sourcecode:: pycon+sql
709
710 >>> do_update_stmt = insert_stmt.on_conflict_do_update(
711 ... constraint="my_table_idx_1", set_=dict(data="updated value")
712 ... )
713 >>> print(do_update_stmt)
714 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s)
715 ON CONFLICT ON CONSTRAINT my_table_idx_1 DO UPDATE SET data = %(param_1)s
716 {stop}
717
718 >>> do_update_stmt = insert_stmt.on_conflict_do_update(
719 ... constraint="my_table_pk", set_=dict(data="updated value")
720 ... )
721 >>> print(do_update_stmt)
722 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s)
723 ON CONFLICT ON CONSTRAINT my_table_pk DO UPDATE SET data = %(param_1)s
724 {stop}
725
726* The :paramref:`_postgresql.Insert.on_conflict_do_update.constraint` argument may
727 also refer to a SQLAlchemy construct representing a constraint,
728 e.g. :class:`.UniqueConstraint`, :class:`.PrimaryKeyConstraint`,
729 :class:`.Index`, or :class:`.ExcludeConstraint`. In this use,
730 if the constraint has a name, it is used directly. Otherwise, if the
731 constraint is unnamed, then inference will be used, where the expressions
732 and optional WHERE clause of the constraint will be spelled out in the
733 construct. This use is especially convenient
734 to refer to the named or unnamed primary key of a :class:`_schema.Table`
735 using the
736 :attr:`_schema.Table.primary_key` attribute:
737
738 .. sourcecode:: pycon+sql
739
740 >>> do_update_stmt = insert_stmt.on_conflict_do_update(
741 ... constraint=my_table.primary_key, set_=dict(data="updated value")
742 ... )
743 >>> print(do_update_stmt)
744 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s)
745 ON CONFLICT (id) DO UPDATE SET data = %(param_1)s
746
747The SET Clause
748^^^^^^^^^^^^^^^
749
750``ON CONFLICT...DO UPDATE`` is used to perform an update of the already
751existing row, using any combination of new values as well as values
752from the proposed insertion. These values are specified using the
753:paramref:`_postgresql.Insert.on_conflict_do_update.set_` parameter. This
754parameter accepts a dictionary which consists of direct values
755for UPDATE:
756
757.. sourcecode:: pycon+sql
758
759 >>> stmt = insert(my_table).values(id="some_id", data="inserted value")
760 >>> do_update_stmt = stmt.on_conflict_do_update(
761 ... index_elements=["id"], set_=dict(data="updated value")
762 ... )
763 >>> print(do_update_stmt)
764 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s)
765 ON CONFLICT (id) DO UPDATE SET data = %(param_1)s
766
767.. warning::
768
769 The :meth:`_expression.Insert.on_conflict_do_update`
770 method does **not** take into
771 account Python-side default UPDATE values or generation functions, e.g.
772 those specified using :paramref:`_schema.Column.onupdate`.
773 These values will not be exercised for an ON CONFLICT style of UPDATE,
774 unless they are manually specified in the
775 :paramref:`_postgresql.Insert.on_conflict_do_update.set_` dictionary.
776
777Updating using the Excluded INSERT Values
778^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
779
780In order to refer to the proposed insertion row, the special alias
781:attr:`~.postgresql.Insert.excluded` is available as an attribute on
782the :class:`_postgresql.Insert` object; this object is a
783:class:`_expression.ColumnCollection`
784which alias contains all columns of the target
785table:
786
787.. sourcecode:: pycon+sql
788
789 >>> stmt = insert(my_table).values(
790 ... id="some_id", data="inserted value", author="jlh"
791 ... )
792 >>> do_update_stmt = stmt.on_conflict_do_update(
793 ... index_elements=["id"],
794 ... set_=dict(data="updated value", author=stmt.excluded.author),
795 ... )
796 >>> print(do_update_stmt)
797 {printsql}INSERT INTO my_table (id, data, author)
798 VALUES (%(id)s, %(data)s, %(author)s)
799 ON CONFLICT (id) DO UPDATE SET data = %(param_1)s, author = excluded.author
800
801Additional WHERE Criteria
802^^^^^^^^^^^^^^^^^^^^^^^^^
803
804The :meth:`_expression.Insert.on_conflict_do_update` method also accepts
805a WHERE clause using the :paramref:`_postgresql.Insert.on_conflict_do_update.where`
806parameter, which will limit those rows which receive an UPDATE:
807
808.. sourcecode:: pycon+sql
809
810 >>> stmt = insert(my_table).values(
811 ... id="some_id", data="inserted value", author="jlh"
812 ... )
813 >>> on_update_stmt = stmt.on_conflict_do_update(
814 ... index_elements=["id"],
815 ... set_=dict(data="updated value", author=stmt.excluded.author),
816 ... where=(my_table.c.status == 2),
817 ... )
818 >>> print(on_update_stmt)
819 {printsql}INSERT INTO my_table (id, data, author)
820 VALUES (%(id)s, %(data)s, %(author)s)
821 ON CONFLICT (id) DO UPDATE SET data = %(param_1)s, author = excluded.author
822 WHERE my_table.status = %(status_1)s
823
824Skipping Rows with DO NOTHING
825^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
826
827``ON CONFLICT`` may be used to skip inserting a row entirely
828if any conflict with a unique or exclusion constraint occurs; below
829this is illustrated using the
830:meth:`~.postgresql.Insert.on_conflict_do_nothing` method:
831
832.. sourcecode:: pycon+sql
833
834 >>> stmt = insert(my_table).values(id="some_id", data="inserted value")
835 >>> stmt = stmt.on_conflict_do_nothing(index_elements=["id"])
836 >>> print(stmt)
837 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s)
838 ON CONFLICT (id) DO NOTHING
839
840If ``DO NOTHING`` is used without specifying any columns or constraint,
841it has the effect of skipping the INSERT for any unique or exclusion
842constraint violation which occurs:
843
844.. sourcecode:: pycon+sql
845
846 >>> stmt = insert(my_table).values(id="some_id", data="inserted value")
847 >>> stmt = stmt.on_conflict_do_nothing()
848 >>> print(stmt)
849 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s)
850 ON CONFLICT DO NOTHING
851
852.. _postgresql_match:
853
854Full Text Search
855----------------
856
857PostgreSQL's full text search system is available through the use of the
858:data:`.func` namespace, combined with the use of custom operators
859via the :meth:`.Operators.bool_op` method. For simple cases with some
860degree of cross-backend compatibility, the :meth:`.Operators.match` operator
861may also be used.
862
863.. _postgresql_simple_match:
864
865Simple plain text matching with ``match()``
866^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
867
868The :meth:`.Operators.match` operator provides for cross-compatible simple
869text matching. For the PostgreSQL backend, it's hardcoded to generate
870an expression using the ``@@`` operator in conjunction with the
871``plainto_tsquery()`` PostgreSQL function.
872
873On the PostgreSQL dialect, an expression like the following::
874
875 select(sometable.c.text.match("search string"))
876
877would emit to the database:
878
879.. sourcecode:: sql
880
881 SELECT text @@ plainto_tsquery('search string') FROM table
882
883Above, passing a plain string to :meth:`.Operators.match` will automatically
884make use of ``plainto_tsquery()`` to specify the type of tsquery. This
885establishes basic database cross-compatibility for :meth:`.Operators.match`
886with other backends.
887
888.. versionchanged:: 2.0 The default tsquery generation function used by the
889 PostgreSQL dialect with :meth:`.Operators.match` is ``plainto_tsquery()``.
890
891 To render exactly what was rendered in 1.4, use the following form::
892
893 from sqlalchemy import func
894
895 select(sometable.c.text.bool_op("@@")(func.to_tsquery("search string")))
896
897 Which would emit:
898
899 .. sourcecode:: sql
900
901 SELECT text @@ to_tsquery('search string') FROM table
902
903Using PostgreSQL full text functions and operators directly
904^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
905
906Text search operations beyond the simple use of :meth:`.Operators.match`
907may make use of the :data:`.func` namespace to generate PostgreSQL full-text
908functions, in combination with :meth:`.Operators.bool_op` to generate
909any boolean operator.
910
911For example, the query::
912
913 select(func.to_tsquery("cat").bool_op("@>")(func.to_tsquery("cat & rat")))
914
915would generate:
916
917.. sourcecode:: sql
918
919 SELECT to_tsquery('cat') @> to_tsquery('cat & rat')
920
921
922The :class:`_postgresql.TSVECTOR` type can provide for explicit CAST::
923
924 from sqlalchemy.dialects.postgresql import TSVECTOR
925 from sqlalchemy import select, cast
926
927 select(cast("some text", TSVECTOR))
928
929produces a statement equivalent to:
930
931.. sourcecode:: sql
932
933 SELECT CAST('some text' AS TSVECTOR) AS anon_1
934
935The ``func`` namespace is augmented by the PostgreSQL dialect to set up
936correct argument and return types for most full text search functions.
937These functions are used automatically by the :attr:`_sql.func` namespace
938assuming the ``sqlalchemy.dialects.postgresql`` package has been imported,
939or :func:`_sa.create_engine` has been invoked using a ``postgresql``
940dialect. These functions are documented at:
941
942* :class:`_postgresql.to_tsvector`
943* :class:`_postgresql.to_tsquery`
944* :class:`_postgresql.plainto_tsquery`
945* :class:`_postgresql.phraseto_tsquery`
946* :class:`_postgresql.websearch_to_tsquery`
947* :class:`_postgresql.ts_headline`
948
949Specifying the "regconfig" with ``match()`` or custom operators
950^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
951
952PostgreSQL's ``plainto_tsquery()`` function accepts an optional
953"regconfig" argument that is used to instruct PostgreSQL to use a
954particular pre-computed GIN or GiST index in order to perform the search.
955When using :meth:`.Operators.match`, this additional parameter may be
956specified using the ``postgresql_regconfig`` parameter, such as::
957
958 select(mytable.c.id).where(
959 mytable.c.title.match("somestring", postgresql_regconfig="english")
960 )
961
962Which would emit:
963
964.. sourcecode:: sql
965
966 SELECT mytable.id FROM mytable
967 WHERE mytable.title @@ plainto_tsquery('english', 'somestring')
968
969When using other PostgreSQL search functions with :data:`.func`, the
970"regconfig" parameter may be passed directly as the initial argument::
971
972 select(mytable.c.id).where(
973 func.to_tsvector("english", mytable.c.title).bool_op("@@")(
974 func.to_tsquery("english", "somestring")
975 )
976 )
977
978produces a statement equivalent to:
979
980.. sourcecode:: sql
981
982 SELECT mytable.id FROM mytable
983 WHERE to_tsvector('english', mytable.title) @@
984 to_tsquery('english', 'somestring')
985
986It is recommended that you use the ``EXPLAIN ANALYZE...`` tool from
987PostgreSQL to ensure that you are generating queries with SQLAlchemy that
988take full advantage of any indexes you may have created for full text search.
989
990.. seealso::
991
992 `Full Text Search <https://www.postgresql.org/docs/current/textsearch-controls.html>`_ - in the PostgreSQL documentation
993
994
995FROM ONLY ...
996-------------
997
998The dialect supports PostgreSQL's ONLY keyword for targeting only a particular
999table in an inheritance hierarchy. This can be used to produce the
1000``SELECT ... FROM ONLY``, ``UPDATE ONLY ...``, and ``DELETE FROM ONLY ...``
1001syntaxes. It uses SQLAlchemy's hints mechanism::
1002
1003 # SELECT ... FROM ONLY ...
1004 result = table.select().with_hint(table, "ONLY", "postgresql")
1005 print(result.fetchall())
1006
1007 # UPDATE ONLY ...
1008 table.update(values=dict(foo="bar")).with_hint(
1009 "ONLY", dialect_name="postgresql"
1010 )
1011
1012 # DELETE FROM ONLY ...
1013 table.delete().with_hint("ONLY", dialect_name="postgresql")
1014
1015.. _postgresql_indexes:
1016
1017PostgreSQL-Specific Index Options
1018---------------------------------
1019
1020Several extensions to the :class:`.Index` construct are available, specific
1021to the PostgreSQL dialect.
1022
1023.. _postgresql_covering_indexes:
1024
1025Covering Indexes
1026^^^^^^^^^^^^^^^^
1027
1028A covering index includes additional columns that are not part of the index key
1029but are stored in the index, allowing PostgreSQL to satisfy queries using only
1030the index without accessing the table (an "index-only scan"). This is
1031indicated on the index using the ``INCLUDE`` clause. The
1032``postgresql_include`` option for :class:`.Index` (as well as
1033:class:`.UniqueConstraint`) renders ``INCLUDE(colname)`` for the given string
1034names::
1035
1036 Index("my_index", table.c.x, postgresql_include=["y"])
1037
1038would render the index as ``CREATE INDEX my_index ON table (x) INCLUDE (y)``
1039
1040Note that this feature requires PostgreSQL 11 or later.
1041
1042.. seealso::
1043
1044 :ref:`postgresql_constraint_options_include` - the same feature implemented
1045 for :class:`.UniqueConstraint`
1046
1047.. versionadded:: 1.4
1048 Support for covering indexes with :class:`.Index`.
1049
1050.. _postgresql_partial_indexes:
1051
1052Partial Indexes
1053^^^^^^^^^^^^^^^
1054
1055Partial indexes add criterion to the index definition so that the index is
1056applied to a subset of rows. These can be specified on :class:`.Index`
1057using the ``postgresql_where`` keyword argument::
1058
1059 Index("my_index", my_table.c.id, postgresql_where=my_table.c.value > 10)
1060
1061.. _postgresql_operator_classes:
1062
1063Operator Classes
1064^^^^^^^^^^^^^^^^
1065
1066PostgreSQL allows the specification of an *operator class* for each column of
1067an index (see
1068https://www.postgresql.org/docs/current/interactive/indexes-opclass.html).
1069The :class:`.Index` construct allows these to be specified via the
1070``postgresql_ops`` keyword argument::
1071
1072 Index(
1073 "my_index",
1074 my_table.c.id,
1075 my_table.c.data,
1076 postgresql_ops={"data": "text_pattern_ops", "id": "int4_ops"},
1077 )
1078
1079Note that the keys in the ``postgresql_ops`` dictionaries are the
1080"key" name of the :class:`_schema.Column`, i.e. the name used to access it from
1081the ``.c`` collection of :class:`_schema.Table`, which can be configured to be
1082different than the actual name of the column as expressed in the database.
1083
1084If ``postgresql_ops`` is to be used against a complex SQL expression such
1085as a function call, then to apply to the column it must be given a label
1086that is identified in the dictionary by name, e.g.::
1087
1088 Index(
1089 "my_index",
1090 my_table.c.id,
1091 func.lower(my_table.c.data).label("data_lower"),
1092 postgresql_ops={"data_lower": "text_pattern_ops", "id": "int4_ops"},
1093 )
1094
1095Operator classes are also supported by the
1096:class:`_postgresql.ExcludeConstraint` construct using the
1097:paramref:`_postgresql.ExcludeConstraint.ops` parameter. See that parameter for
1098details.
1099
1100Index Types
1101^^^^^^^^^^^
1102
1103PostgreSQL provides several index types: B-Tree, Hash, GiST, and GIN, as well
1104as the ability for users to create their own (see
1105https://www.postgresql.org/docs/current/static/indexes-types.html). These can be
1106specified on :class:`.Index` using the ``postgresql_using`` keyword argument::
1107
1108 Index("my_index", my_table.c.data, postgresql_using="gin")
1109
1110The value passed to the keyword argument will be simply passed through to the
1111underlying CREATE INDEX command, so it *must* be a valid index type for your
1112version of PostgreSQL.
1113
1114.. _postgresql_index_storage:
1115
1116Index Storage Parameters
1117^^^^^^^^^^^^^^^^^^^^^^^^
1118
1119PostgreSQL allows storage parameters to be set on indexes. The storage
1120parameters available depend on the index method used by the index. Storage
1121parameters can be specified on :class:`.Index` using the ``postgresql_with``
1122keyword argument::
1123
1124 Index("my_index", my_table.c.data, postgresql_with={"fillfactor": 50})
1125
1126PostgreSQL allows to define the tablespace in which to create the index.
1127The tablespace can be specified on :class:`.Index` using the
1128``postgresql_tablespace`` keyword argument::
1129
1130 Index("my_index", my_table.c.data, postgresql_tablespace="my_tablespace")
1131
1132Note that the same option is available on :class:`_schema.Table` as well.
1133
1134.. _postgresql_index_concurrently:
1135
1136Indexes with CONCURRENTLY
1137^^^^^^^^^^^^^^^^^^^^^^^^^
1138
1139The PostgreSQL index option CONCURRENTLY is supported by passing the
1140flag ``postgresql_concurrently`` to the :class:`.Index` construct::
1141
1142 tbl = Table("testtbl", m, Column("data", Integer))
1143
1144 idx1 = Index("test_idx1", tbl.c.data, postgresql_concurrently=True)
1145
1146The above index construct will render DDL for CREATE INDEX, assuming
1147PostgreSQL 8.2 or higher is detected or for a connection-less dialect, as:
1148
1149.. sourcecode:: sql
1150
1151 CREATE INDEX CONCURRENTLY test_idx1 ON testtbl (data)
1152
1153For DROP INDEX, assuming PostgreSQL 9.2 or higher is detected or for
1154a connection-less dialect, it will emit:
1155
1156.. sourcecode:: sql
1157
1158 DROP INDEX CONCURRENTLY test_idx1
1159
1160When using CONCURRENTLY, the PostgreSQL database requires that the statement
1161be invoked outside of a transaction block. The Python DBAPI enforces that
1162even for a single statement, a transaction is present, so to use this
1163construct, the DBAPI's "autocommit" mode must be used::
1164
1165 metadata = MetaData()
1166 table = Table("foo", metadata, Column("id", String))
1167 index = Index("foo_idx", table.c.id, postgresql_concurrently=True)
1168
1169 with engine.connect() as conn:
1170 with conn.execution_options(isolation_level="AUTOCOMMIT"):
1171 table.create(conn)
1172
1173.. seealso::
1174
1175 :ref:`postgresql_isolation_level`
1176
1177.. _postgresql_index_reflection:
1178
1179PostgreSQL Index Reflection
1180---------------------------
1181
1182Notes on Index reflection
1183
1184
1185Implicit UNIQUE index for UNIQUE CONSTRAINT
1186^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1187
1188The PostgreSQL database creates a UNIQUE INDEX implicitly whenever the
1189UNIQUE CONSTRAINT construct is used. When inspecting a table using
1190:class:`_reflection.Inspector`, the :meth:`_reflection.Inspector.get_indexes`
1191and the :meth:`_reflection.Inspector.get_unique_constraints`
1192will report on these
1193two constructs distinctly; in the case of the index, the key
1194``duplicates_constraint`` will be present in the index entry if it is
1195detected as mirroring a constraint. When performing reflection using
1196``Table(..., autoload_with=engine)``, the UNIQUE INDEX is **not** returned
1197in :attr:`_schema.Table.indexes` when it is detected as mirroring a
1198:class:`.UniqueConstraint` in the :attr:`_schema.Table.constraints` collection.
1199
1200.. _postgresql_invalid_index_reflection:
1201
1202Invalid indexes
1203^^^^^^^^^^^^^^^
1204
1205Indexes marked "invalid" by PostgreSQL (i.e. an index that exists in the
1206catalog, but is ignored by the query planner because it may be incomplete,
1207while still being maintained on writes) remain present in reflection. The
1208:meth:`_reflection.Inspector.get_indexes` and
1209:meth:`_reflection.Inspector.get_multi_indexes` methods include
1210``postgresql_invalid=True`` in the index's ``dialect_options`` dictionary when
1211``pg_index.indisvalid`` is false. Valid indexes omit this flag.
1212
1213When reflecting a :class:`_schema.Table`, the flag, if present, is stored
1214separately from DDL options on each reflected :class:`.Index`::
1215
1216 table = Table("my_table", MetaData(), autoload_with=engine)
1217 for index in table.indexes:
1218 if index.reflect_only_elements["postgresql"].get("invalid"):
1219 print(index.name)
1220
1221The :attr:`.Index.reflect_only_elements` mapping is read-only. These values are
1222not included in :attr:`.Index.dialect_kwargs` and do not affect DDL.
1223
1224An invalid index may be left by a failed ``CREATE INDEX CONCURRENTLY``, may
1225still be building, or may be a partitioned index awaiting attachment of its
1226partition indexes. This flag reports the state at reflection time; it does not
1227request an index rebuild.
1228
1229.. versionadded:: 2.1
1230
1231Special Reflection Options
1232--------------------------
1233
1234The :class:`_reflection.Inspector`
1235used for the PostgreSQL backend is an instance
1236of :class:`.PGInspector`, which offers additional methods::
1237
1238 from sqlalchemy import create_engine, inspect
1239
1240 engine = create_engine("postgresql+psycopg2://localhost/test")
1241 insp = inspect(engine) # will be a PGInspector
1242
1243 print(insp.get_enums())
1244
1245.. autoclass:: PGInspector
1246 :members:
1247
1248.. _postgresql_table_options:
1249
1250PostgreSQL Table Options
1251------------------------
1252
1253Several options for CREATE TABLE are supported directly by the PostgreSQL
1254dialect in conjunction with the :class:`_schema.Table` construct, detailed
1255in the following sections.
1256
1257.. seealso::
1258
1259 `PostgreSQL CREATE TABLE options
1260 <https://www.postgresql.org/docs/current/static/sql-createtable.html>`_ -
1261 in the PostgreSQL documentation.
1262
1263``INHERITS``
1264^^^^^^^^^^^^
1265
1266Specifies one or more parent tables from which this table inherits columns and
1267constraints, enabling table inheritance hierarchies in PostgreSQL.
1268
1269::
1270
1271 Table("some_table", metadata, ..., postgresql_inherits="some_supertable")
1272
1273 Table("some_table", metadata, ..., postgresql_inherits=("t1", "t2", ...))
1274
1275For schema-qualified parent table names, use :class:`.quoted_name` with
1276``quote=False`` to prevent the dotted name from being quoted as a single
1277identifier::
1278
1279 from sqlalchemy.sql import quoted_name
1280
1281 Table(
1282 "some_table",
1283 metadata,
1284 ...,
1285 postgresql_inherits=quoted_name(
1286 "my_schema.some_supertable", quote=False
1287 ),
1288 )
1289
1290SQLAlchemy does not automatically copy the columns from the inherited tables
1291mentioned in the ``postgresql_inherits`` argument into the new
1292:class:`_schema.Table`. To populate the new table columns reflection may be
1293used, or a function similar to the following one::
1294
1295 def get_parent_columns(tbl: sa.Table) -> list[sa.Column]:
1296 return [
1297 sa.Column(
1298 c.name,
1299 c.type,
1300 key=c.key,
1301 nullable=c.nullable,
1302 # Set system=true to omit from the CREATE TABLE statement
1303 system=True,
1304 )
1305 for c in tbl.columns
1306 ]
1307
1308
1309 documents = Table(
1310 "some_table",
1311 metadata,
1312 *get_parent_columns(some_supertable),
1313 # ... # add other columns here normally if needed
1314 postgresql_inherits="some_supertable",
1315 )
1316
1317``ON COMMIT``
1318^^^^^^^^^^^^^
1319
1320Controls the behavior of temporary tables at transaction commit, with options
1321to preserve rows, delete rows, or drop the table.
1322
1323::
1324
1325 Table("some_table", metadata, ..., postgresql_on_commit="PRESERVE ROWS")
1326
1327``PARTITION BY``
1328^^^^^^^^^^^^^^^^
1329
1330Declares the table as a partitioned table using the specified partitioning
1331strategy (RANGE, LIST, or HASH) on the given column(s).
1332
1333::
1334
1335 Table(
1336 "some_table",
1337 metadata,
1338 ...,
1339 postgresql_partition_by="LIST (part_column)",
1340 )
1341
1342``TABLESPACE``
1343^^^^^^^^^^^^^^
1344
1345Specifies the tablespace where the table will be stored, allowing control over
1346the physical location of table data on disk.
1347
1348::
1349
1350 Table("some_table", metadata, ..., postgresql_tablespace="some_tablespace")
1351
1352The above option is also available on the :class:`.Index` construct.
1353
1354``USING``
1355^^^^^^^^^
1356
1357Specifies the table access method to use for storing table data, such as
1358``heap`` (the default) or other custom access methods.
1359
1360::
1361
1362 Table("some_table", metadata, ..., postgresql_using="heap")
1363
1364.. versionadded:: 2.0.26
1365
1366.. _postgresql_table_options_with:
1367
1368``WITH``
1369^^^^^^^^
1370
1371::
1372
1373 Table("some_table", metadata, ..., postgresql_with={"fillfactor": 100})
1374
1375The ``postgresql_with`` parameter accepts a dictionary of storage parameters
1376that will be applied to the table using the ``WITH`` clause in the
1377``CREATE TABLE`` statement. Storage parameters control various aspects of
1378table behavior such as the fill factor for pages, autovacuum settings, and
1379toast table parameters.
1380
1381When the table is created, the parameters are rendered as a comma-separated
1382list within the ``WITH`` clause. For example::
1383
1384 Table(
1385 "mytable",
1386 metadata,
1387 Column("id", Integer, primary_key=True),
1388 postgresql_with={
1389 "fillfactor": 70,
1390 "autovacuum_enabled": False,
1391 "toast.vacuum_truncate": True,
1392 },
1393 )
1394
1395This will generate DDL similar to:
1396
1397.. sourcecode:: sql
1398
1399 CREATE TABLE mytable (
1400 id INTEGER NOT NULL,
1401 PRIMARY KEY (id)
1402 ) WITH (fillfactor = 70, autovacuum_enabled = false, toast.vacuum_truncate = true)
1403
1404The values in the dictionary can be integers, strings, boolean values, or
1405``None``. Boolean values are rendered as lowercase ``true``/``false``.
1406A ``None`` value renders the parameter name only, without an ``=`` value.
1407Parameter names can include dots to specify parameters for associated objects
1408like toast tables (e.g., ``toast.vacuum_truncate``).
1409
1410.. seealso::
1411
1412 `PostgreSQL Storage Parameters
1413 <https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS>`_ -
1414 documentation on available storage parameters.
1415
1416.. versionadded:: 2.1
1417
1418``WITH OIDS``
1419^^^^^^^^^^^^^
1420
1421Enables the legacy OID (object identifier) system column for the table, which
1422assigns a unique identifier to each row.
1423
1424::
1425
1426 Table("some_table", metadata, ..., postgresql_with_oids=True)
1427
1428.. note:: Support for tables "with OIDs" was removed in postgresql 12.
1429
1430``WITHOUT OIDS``
1431^^^^^^^^^^^^^^^^
1432
1433Explicitly disables the OID system column for the table (the default behavior
1434in modern PostgreSQL versions).
1435
1436::
1437
1438 Table("some_table", metadata, ..., postgresql_with_oids=False)
1439
1440.. _postgresql_view_options:
1441
1442PostgreSQL View Options
1443-----------------------
1444
1445The following sections indicate options which are supported by the PostgreSQL
1446dialect in conjunction with :class:`.CreateView`.
1447
1448``WITH``
1449^^^^^^^^
1450
1451Specifies view-level options for :class:`.CreateView` using the ``WITH``
1452clause::
1453
1454 from sqlalchemy import CreateView, select
1455
1456 CreateView(
1457 select(my_table),
1458 "my_view",
1459 postgresql_with={"security_invoker": True},
1460 )
1461
1462This generates:
1463
1464.. sourcecode:: sql
1465
1466 CREATE VIEW my_view WITH (security_invoker = true) AS SELECT ...
1467
1468The same formatting rules that apply to :ref:`postgresql_table_options_with`
1469apply here: booleans render as lowercase ``true``/``false``, ``None``
1470renders as the parameter name only, and other values render as-is.
1471
1472.. versionadded:: 2.1
1473
1474.. _postgresql_constraint_options:
1475
1476PostgreSQL Constraint Options
1477-----------------------------
1478
1479The following sections indicate options which are supported by the PostgreSQL
1480dialect in conjunction with selected constraint constructs.
1481
1482
1483``NOT VALID``
1484^^^^^^^^^^^^^
1485
1486Allows a constraint to be added without validating existing rows, improving
1487performance when adding constraints to large tables. This option applies
1488towards CHECK and FOREIGN KEY constraints when the constraint is being added
1489to an existing table via ALTER TABLE, and has the effect that existing rows
1490are not scanned during the ALTER operation against the constraint being added.
1491
1492When using a SQL migration tool such as `Alembic <https://alembic.sqlalchemy.org>`_
1493that renders ALTER TABLE constructs, the ``postgresql_not_valid`` argument
1494may be specified as an additional keyword argument within the operation
1495that creates the constraint, as in the following Alembic example::
1496
1497 def update():
1498 op.create_foreign_key(
1499 "fk_user_address",
1500 "address",
1501 "user",
1502 ["user_id"],
1503 ["id"],
1504 postgresql_not_valid=True,
1505 )
1506
1507The keyword is ultimately accepted directly by the
1508:class:`_schema.CheckConstraint`, :class:`_schema.ForeignKeyConstraint`
1509and :class:`_schema.ForeignKey` constructs; when using a tool like
1510Alembic, dialect-specific keyword arguments are passed through to
1511these constructs from the migration operation directives::
1512
1513 CheckConstraint("some_field IS NOT NULL", postgresql_not_valid=True)
1514
1515 ForeignKeyConstraint(
1516 ["some_id"], ["some_table.some_id"], postgresql_not_valid=True
1517 )
1518
1519.. versionadded:: 1.4.32
1520
1521.. seealso::
1522
1523 `PostgreSQL ALTER TABLE options
1524 <https://www.postgresql.org/docs/current/static/sql-altertable.html>`_ -
1525 in the PostgreSQL documentation.
1526
1527.. _postgresql_constraint_options_include:
1528
1529``INCLUDE``
1530^^^^^^^^^^^
1531
1532This keyword is applicable to both a ``UNIQUE`` constraint as well as an
1533``INDEX``. The ``postgresql_include`` option available for
1534:class:`.UniqueConstraint` as well as :class:`.Index` creates a covering index
1535by including additional columns in the underlying index without making them
1536part of the key constraint. This option adds one or more columns as a "payload"
1537to the index created automatically by PostgreSQL for the constraint. For
1538example, the following table definition::
1539
1540 Table(
1541 "mytable",
1542 metadata,
1543 Column("id", Integer, nullable=False),
1544 Column("value", Integer, nullable=False),
1545 UniqueConstraint("id", postgresql_include=["value"]),
1546 )
1547
1548would produce the DDL statement
1549
1550.. sourcecode:: sql
1551
1552 CREATE TABLE mytable (
1553 id INTEGER NOT NULL,
1554 value INTEGER NOT NULL,
1555 UNIQUE (id) INCLUDE (value)
1556 )
1557
1558Note that this feature requires PostgreSQL 11 or later.
1559
1560.. versionadded:: 2.0.41
1561 Added support for ``postgresql_include`` to :class:`.UniqueConstraint`,
1562 to complement the existing feature in :class:`.Index`.
1563
1564.. seealso::
1565
1566 :ref:`postgresql_covering_indexes` - background on ``postgresql_include``
1567 for the :class:`.Index` construct.
1568
1569
1570Column list with foreign key ``ON DELETE SET`` actions
1571^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1572
1573Allows selective column updates when a foreign key action is triggered, limiting
1574which columns are set to NULL or DEFAULT upon deletion of a referenced row.
1575This applies to :class:`.ForeignKey` and :class:`.ForeignKeyConstraint`, the
1576:paramref:`.ForeignKey.ondelete` parameter will accept on the PostgreSQL
1577backend only a string list of column names inside parenthesis, following the
1578``SET NULL`` or ``SET DEFAULT`` phrases, which will limit the set of columns
1579that are subject to the action::
1580
1581 fktable = Table(
1582 "fktable",
1583 metadata,
1584 Column("tid", Integer),
1585 Column("id", Integer),
1586 Column("fk_id_del_set_null", Integer),
1587 ForeignKeyConstraint(
1588 columns=["tid", "fk_id_del_set_null"],
1589 refcolumns=[pktable.c.tid, pktable.c.id],
1590 ondelete="SET NULL (fk_id_del_set_null)",
1591 ),
1592 )
1593
1594.. versionadded:: 2.0.40
1595
1596.. _postgresql_computed_column_notes:
1597
1598Computed Columns (GENERATED ALWAYS AS)
1599---------------------------------------
1600
1601SQLAlchemy's support for the "GENERATED ALWAYS AS" SQL instruction, which
1602establishes a dynamic, automatically populated value for a column, is available
1603using the :ref:`computed_ddl` feature of SQLAlchemy DDL. E.g.::
1604
1605 from sqlalchemy import Table, Column, MetaData, Integer, Computed
1606
1607 metadata_obj = MetaData()
1608
1609 square = Table(
1610 "square",
1611 metadata_obj,
1612 Column("id", Integer, primary_key=True),
1613 Column("side", Integer),
1614 Column("area", Integer, Computed("side * side")),
1615 Column("perimeter", Integer, Computed("4 * side", persisted=True)),
1616 )
1617
1618There are two general varieties of the "computed" column, ``VIRTUAL`` and ``STORED``.
1619A ``STORED`` computed column computes and persists its value at INSERT/UPDATE time,
1620while a ``VIRTUAL`` computed column computes its value on access without persisting it.
1621This preference is indicated using the :paramref:`.Computed.persisted` parameter,
1622which defaults to ``None`` to use the database default behavior.
1623
1624For PostgreSQL, prior to version 18 only the ``STORED`` variant was supported,
1625requiring the ``STORED`` keyword to be emitted explicitly. PostgreSQL 18 added
1626support for ``VIRTUAL`` columns and made ``VIRTUAL`` the default behavior.
1627
1628To accommodate this change, SQLAlchemy's behavior when
1629:paramref:`.Computed.persisted` is not specified depends on the PostgreSQL
1630version: on PostgreSQL 18 and later, no keyword is rendered, allowing the
1631database to use its default of ``VIRTUAL``; on PostgreSQL 17 and earlier,
1632``STORED`` is rendered and a warning is emitted. To ensure consistent
1633``STORED`` behavior across all PostgreSQL versions, explicitly set
1634``persisted=True``.
1635
1636.. versionchanged:: 2.1
1637
1638 PostgreSQL 18+ now defaults to ``VIRTUAL`` when :paramref:`.Computed.persisted`
1639 is not specified. A warning is emitted for older versions of PostgreSQL
1640 when this parameter is not indicated.
1641
1642
1643
1644
1645``NULLS NOT DISTINCT``
1646^^^^^^^^^^^^^^^^^^^^^^
1647
1648By default, two ``null`` values are not considered equal for unique constraints
1649and indexes. Therefore, seemingly duplicate rows may be stored if one of the
1650values in the constraint is ``null``. This default behavior is implementation
1651defined, so other SQL dialects may behave differently than PostgreSQL.
1652
1653The ``NULLS NOT DISTINCT`` clause can be used to change this behavior, treating
1654null values as equal and preventing unintended duplicate rows. The opposite
1655``NULLS DISTINCT`` clause can also be used to make PostgreSQL's default behavior
1656explict.
1657
1658The ``postgresql_nulls_not_distinct`` parameter can be set to ``True`` to
1659add the ``NULLS NOT DISTINCT`` clause, or ``False`` to add ``NULLS DISTINCT``.
1660Not setting it, or passing ``None``, will not add a clause and keep the default
1661behavior.
1662
1663This feature requires PostgreSQL 15 or later.
1664
1665.. versionadded:: 2.0.16
1666
1667
1668.. _postgresql_table_valued_overview:
1669
1670Table values, Table and Column valued functions, Row and Tuple objects
1671-----------------------------------------------------------------------
1672
1673PostgreSQL makes great use of modern SQL forms such as table-valued functions,
1674tables and rows as values. These constructs are commonly used as part
1675of PostgreSQL's support for complex datatypes such as JSON, ARRAY, and other
1676datatypes. SQLAlchemy's SQL expression language has native support for
1677most table-valued and row-valued forms.
1678
1679.. _postgresql_table_valued:
1680
1681Table-Valued Functions
1682^^^^^^^^^^^^^^^^^^^^^^^
1683
1684Many PostgreSQL built-in functions are intended to be used in the FROM clause
1685of a SELECT statement, and are capable of returning table rows or sets of table
1686rows. A large portion of PostgreSQL's JSON functions for example such as
1687``json_array_elements()``, ``json_object_keys()``, ``json_each_text()``,
1688``json_each()``, ``json_to_record()``, ``json_populate_recordset()`` use such
1689forms. These classes of SQL function calling forms in SQLAlchemy are available
1690using the :meth:`_functions.FunctionElement.table_valued` method in conjunction
1691with :class:`_functions.Function` objects generated from the :data:`_sql.func`
1692namespace.
1693
1694Examples from PostgreSQL's reference documentation follow below:
1695
1696* ``json_each()``:
1697
1698 .. sourcecode:: pycon+sql
1699
1700 >>> from sqlalchemy import select, func
1701 >>> stmt = select(
1702 ... func.json_each('{"a":"foo", "b":"bar"}').table_valued("key", "value")
1703 ... )
1704 >>> print(stmt)
1705 {printsql}SELECT anon_1.key, anon_1.value
1706 FROM json_each(:json_each_1) AS anon_1
1707
1708* ``json_populate_record()``:
1709
1710 .. sourcecode:: pycon+sql
1711
1712 >>> from sqlalchemy import select, func, literal_column
1713 >>> stmt = select(
1714 ... func.json_populate_record(
1715 ... literal_column("null::myrowtype"), '{"a":1,"b":2}'
1716 ... ).table_valued("a", "b", name="x")
1717 ... )
1718 >>> print(stmt)
1719 {printsql}SELECT x.a, x.b
1720 FROM json_populate_record(null::myrowtype, :json_populate_record_1) AS x
1721
1722* ``json_to_record()`` - this form uses a PostgreSQL specific form of derived
1723 columns in the alias, where we may make use of :func:`_sql.column` elements with
1724 types to produce them. The :meth:`_functions.FunctionElement.table_valued`
1725 method produces a :class:`_sql.TableValuedAlias` construct, and the method
1726 :meth:`_sql.TableValuedAlias.render_derived` method sets up the derived
1727 columns specification:
1728
1729 .. sourcecode:: pycon+sql
1730
1731 >>> from sqlalchemy import select, func, column, Integer, Text
1732 >>> stmt = select(
1733 ... func.json_to_record('{"a":1,"b":[1,2,3],"c":"bar"}')
1734 ... .table_valued(
1735 ... column("a", Integer),
1736 ... column("b", Text),
1737 ... column("d", Text),
1738 ... )
1739 ... .render_derived(name="x", with_types=True)
1740 ... )
1741 >>> print(stmt)
1742 {printsql}SELECT x.a, x.b, x.d
1743 FROM json_to_record(:json_to_record_1) AS x(a INTEGER, b TEXT, d TEXT)
1744
1745* ``WITH ORDINALITY`` - part of the SQL standard, ``WITH ORDINALITY`` adds an
1746 ordinal counter to the output of a function and is accepted by a limited set
1747 of PostgreSQL functions including ``unnest()`` and ``generate_series()``. The
1748 :meth:`_functions.FunctionElement.table_valued` method accepts a keyword
1749 parameter ``with_ordinality`` for this purpose, which accepts the string name
1750 that will be applied to the "ordinality" column:
1751
1752 .. sourcecode:: pycon+sql
1753
1754 >>> from sqlalchemy import select, func
1755 >>> stmt = select(
1756 ... func.generate_series(4, 1, -1)
1757 ... .table_valued("value", with_ordinality="ordinality")
1758 ... .render_derived()
1759 ... )
1760 >>> print(stmt)
1761 {printsql}SELECT anon_1.value, anon_1.ordinality
1762 FROM generate_series(:generate_series_1, :generate_series_2, :generate_series_3)
1763 WITH ORDINALITY AS anon_1(value, ordinality)
1764
1765.. versionadded:: 1.4.0b2
1766
1767.. seealso::
1768
1769 :ref:`tutorial_functions_table_valued` - in the :ref:`unified_tutorial`
1770
1771.. _postgresql_column_valued:
1772
1773Column Valued Functions
1774^^^^^^^^^^^^^^^^^^^^^^^
1775
1776Similar to the table valued function, a column valued function is present
1777in the FROM clause, but delivers itself to the columns clause as a single
1778scalar value. PostgreSQL functions such as ``json_array_elements()``,
1779``unnest()`` and ``generate_series()`` may use this form. Column valued functions are available using the
1780:meth:`_functions.FunctionElement.column_valued` method of :class:`_functions.FunctionElement`:
1781
1782* ``json_array_elements()``:
1783
1784 .. sourcecode:: pycon+sql
1785
1786 >>> from sqlalchemy import select, func
1787 >>> stmt = select(
1788 ... func.json_array_elements('["one", "two"]').column_valued("x")
1789 ... )
1790 >>> print(stmt)
1791 {printsql}SELECT x
1792 FROM json_array_elements(:json_array_elements_1) AS x
1793
1794* ``unnest()`` - in order to generate a PostgreSQL ARRAY literal, the
1795 :func:`_postgresql.array` construct may be used:
1796
1797 .. sourcecode:: pycon+sql
1798
1799 >>> from sqlalchemy.dialects.postgresql import array
1800 >>> from sqlalchemy import select, func
1801 >>> stmt = select(func.unnest(array([1, 2])).column_valued())
1802 >>> print(stmt)
1803 {printsql}SELECT anon_1
1804 FROM unnest(ARRAY[%(param_1)s, %(param_2)s]) AS anon_1
1805
1806 The function can of course be used against an existing table-bound column
1807 that's of type :class:`_types.ARRAY`:
1808
1809 .. sourcecode:: pycon+sql
1810
1811 >>> from sqlalchemy import table, column, ARRAY, Integer
1812 >>> from sqlalchemy import select, func
1813 >>> t = table("t", column("value", ARRAY(Integer)))
1814 >>> stmt = select(func.unnest(t.c.value).column_valued("unnested_value"))
1815 >>> print(stmt)
1816 {printsql}SELECT unnested_value
1817 FROM unnest(t.value) AS unnested_value
1818
1819.. seealso::
1820
1821 :ref:`tutorial_functions_column_valued` - in the :ref:`unified_tutorial`
1822
1823
1824Row Types
1825^^^^^^^^^
1826
1827Built-in support for rendering a ``ROW`` may be approximated using
1828``func.ROW`` with the :attr:`_sa.func` namespace, or by using the
1829:func:`_sql.tuple_` construct:
1830
1831.. sourcecode:: pycon+sql
1832
1833 >>> from sqlalchemy import table, column, func, tuple_
1834 >>> t = table("t", column("id"), column("fk"))
1835 >>> stmt = (
1836 ... t.select()
1837 ... .where(tuple_(t.c.id, t.c.fk) > (1, 2))
1838 ... .where(func.ROW(t.c.id, t.c.fk) < func.ROW(3, 7))
1839 ... )
1840 >>> print(stmt)
1841 {printsql}SELECT t.id, t.fk
1842 FROM t
1843 WHERE (t.id, t.fk) > (:param_1, :param_2) AND ROW(t.id, t.fk) < ROW(:ROW_1, :ROW_2)
1844
1845.. seealso::
1846
1847 `PostgreSQL Row Constructors
1848 <https://www.postgresql.org/docs/current/sql-expressions.html#SQL-SYNTAX-ROW-CONSTRUCTORS>`_
1849
1850 `PostgreSQL Row Constructor Comparison
1851 <https://www.postgresql.org/docs/current/functions-comparisons.html#ROW-WISE-COMPARISON>`_
1852
1853Table Types passed to Functions
1854^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1855
1856PostgreSQL supports passing a table as an argument to a function, which is
1857known as a "record" type. SQLAlchemy :class:`_sql.FromClause` objects
1858such as :class:`_schema.Table` support this special form using the
1859:meth:`_sql.FromClause.table_valued` method, which is comparable to the
1860:meth:`_functions.FunctionElement.table_valued` method except that the collection
1861of columns is already established by that of the :class:`_sql.FromClause`
1862itself:
1863
1864.. sourcecode:: pycon+sql
1865
1866 >>> from sqlalchemy import table, column, func, select
1867 >>> a = table("a", column("id"), column("x"), column("y"))
1868 >>> stmt = select(func.row_to_json(a.table_valued()))
1869 >>> print(stmt)
1870 {printsql}SELECT row_to_json(a) AS row_to_json_1
1871 FROM a
1872
1873.. versionadded:: 1.4.0b2
1874
1875
1876.. _postgresql_collation:
1877
1878Schema-Qualified Collations
1879----------------------------
1880
1881PostgreSQL supports collations that are qualified by a schema name, such as
1882``CREATE COLLATION my_schema.my_collation (...)``. To refer to such a
1883collation, use the :paramref:`.String.collation_schema` parameter (or
1884:paramref:`_postgresql.DOMAIN.collation_schema` for a :class:`_postgresql.DOMAIN`)
1885in conjunction with :paramref:`.String.collation`, rather than attempting to
1886embed the schema name inside the ``collation`` string itself::
1887
1888 Column(
1889 "data",
1890 String(collation="my_collation", collation_schema="my_schema"),
1891 )
1892
1893The above renders DDL similar to:
1894
1895.. sourcecode:: sql
1896
1897 data VARCHAR COLLATE "my_schema"."my_collation"
1898
1899.. versionadded:: 2.1
1900
1901.. seealso::
1902
1903 :paramref:`.String.collation`
1904
1905 :paramref:`.String.collation_schema`
1906
1907""" # noqa: E501
1908
1909from __future__ import annotations
1910
1911from collections import defaultdict
1912from functools import lru_cache
1913import re
1914from typing import Any
1915from typing import cast
1916from typing import Dict
1917from typing import List
1918from typing import Optional
1919from typing import Tuple
1920from typing import TYPE_CHECKING
1921from typing import TypedDict
1922from typing import Union
1923
1924from . import arraylib as _array
1925from . import json as _json
1926from . import pg_catalog
1927from . import ranges as _ranges
1928from .ext import _regconfig_fn
1929from .ext import aggregate_order_by
1930from .hstore import HSTORE
1931from .named_types import CreateDomainType as CreateDomainType # noqa: F401
1932from .named_types import CreateEnumType as CreateEnumType # noqa: F401
1933from .named_types import DOMAIN as DOMAIN # noqa: F401
1934from .named_types import DropDomainType as DropDomainType # noqa: F401
1935from .named_types import DropEnumType as DropEnumType # noqa: F401
1936from .named_types import ENUM as ENUM # noqa: F401
1937from .named_types import NamedType as NamedType # noqa: F401
1938from .types import _DECIMAL_TYPES # noqa: F401
1939from .types import _FLOAT_TYPES # noqa: F401
1940from .types import _INT_TYPES # noqa: F401
1941from .types import BIT as BIT
1942from .types import BYTEA as BYTEA
1943from .types import CIDR as CIDR
1944from .types import CITEXT as CITEXT
1945from .types import INET as INET
1946from .types import INTERVAL as INTERVAL
1947from .types import MACADDR as MACADDR
1948from .types import MACADDR8 as MACADDR8
1949from .types import MONEY as MONEY
1950from .types import OID as OID
1951from .types import PGBit as PGBit # noqa: F401
1952from .types import PGCidr as PGCidr # noqa: F401
1953from .types import PGInet as PGInet # noqa: F401
1954from .types import PGInterval as PGInterval # noqa: F401
1955from .types import PGMacAddr as PGMacAddr # noqa: F401
1956from .types import PGMacAddr8 as PGMacAddr8 # noqa: F401
1957from .types import PGUuid as PGUuid
1958from .types import REGCLASS as REGCLASS
1959from .types import REGCONFIG as REGCONFIG # noqa: F401
1960from .types import TIME as TIME
1961from .types import TIMESTAMP as TIMESTAMP
1962from .types import TSVECTOR as TSVECTOR
1963from ... import exc
1964from ... import schema
1965from ... import select
1966from ... import sql
1967from ... import util
1968from ...engine import characteristics
1969from ...engine import default
1970from ...engine import interfaces
1971from ...engine import ObjectKind
1972from ...engine import ObjectScope
1973from ...engine import reflection
1974from ...engine import URL
1975from ...engine.reflection import ReflectionDefaults
1976from ...sql import bindparam
1977from ...sql import coercions
1978from ...sql import compiler
1979from ...sql import elements
1980from ...sql import expression
1981from ...sql import functions
1982from ...sql import roles
1983from ...sql import sqltypes
1984from ...sql import util as sql_util
1985from ...sql.base import DialectKWArgConst
1986from ...sql.compiler import InsertmanyvaluesSentinelOpts
1987from ...sql.visitors import InternalTraversal
1988from ...types import BIGINT
1989from ...types import BOOLEAN
1990from ...types import CHAR
1991from ...types import DATE
1992from ...types import DOUBLE_PRECISION
1993from ...types import FLOAT
1994from ...types import INTEGER
1995from ...types import NUMERIC
1996from ...types import REAL
1997from ...types import SMALLINT
1998from ...types import TEXT
1999from ...types import UUID as UUID
2000from ...types import VARCHAR
2001
2002IDX_USING = re.compile(r"^(?:btree|hash|gist|gin|[\w_]+)$", re.I)
2003
2004RESERVED_WORDS = {
2005 "all",
2006 "analyse",
2007 "analyze",
2008 "and",
2009 "any",
2010 "array",
2011 "as",
2012 "asc",
2013 "asymmetric",
2014 "both",
2015 "case",
2016 "cast",
2017 "check",
2018 "collate",
2019 "column",
2020 "constraint",
2021 "create",
2022 "current_catalog",
2023 "current_date",
2024 "current_role",
2025 "current_time",
2026 "current_timestamp",
2027 "current_user",
2028 "default",
2029 "deferrable",
2030 "desc",
2031 "distinct",
2032 "do",
2033 "else",
2034 "end",
2035 "except",
2036 "false",
2037 "fetch",
2038 "for",
2039 "foreign",
2040 "from",
2041 "grant",
2042 "group",
2043 "having",
2044 "in",
2045 "initially",
2046 "intersect",
2047 "into",
2048 "leading",
2049 "limit",
2050 "localtime",
2051 "localtimestamp",
2052 "new",
2053 "not",
2054 "null",
2055 "of",
2056 "off",
2057 "offset",
2058 "old",
2059 "on",
2060 "only",
2061 "or",
2062 "order",
2063 "placing",
2064 "primary",
2065 "references",
2066 "returning",
2067 "select",
2068 "session_user",
2069 "some",
2070 "symmetric",
2071 "table",
2072 "then",
2073 "to",
2074 "trailing",
2075 "true",
2076 "union",
2077 "unique",
2078 "user",
2079 "using",
2080 "variadic",
2081 "when",
2082 "where",
2083 "window",
2084 "with",
2085 "authorization",
2086 "between",
2087 "binary",
2088 "cross",
2089 "current_schema",
2090 "freeze",
2091 "full",
2092 "ilike",
2093 "inner",
2094 "is",
2095 "isnull",
2096 "join",
2097 "left",
2098 "like",
2099 "natural",
2100 "notnull",
2101 "outer",
2102 "over",
2103 "overlaps",
2104 "right",
2105 "similar",
2106 "verbose",
2107}
2108
2109
2110colspecs = {
2111 sqltypes.ARRAY: _array.ARRAY,
2112 sqltypes.Interval: INTERVAL,
2113 sqltypes.Enum: ENUM,
2114 sqltypes.JSON.JSONPathType: _json.JSONPATH,
2115 sqltypes.JSON: _json.JSON,
2116 sqltypes.Uuid: PGUuid,
2117}
2118
2119
2120ischema_names = {
2121 "_array": _array.ARRAY,
2122 "hstore": HSTORE,
2123 "json": _json.JSON,
2124 "jsonb": _json.JSONB,
2125 "int4range": _ranges.INT4RANGE,
2126 "int8range": _ranges.INT8RANGE,
2127 "numrange": _ranges.NUMRANGE,
2128 "daterange": _ranges.DATERANGE,
2129 "tsrange": _ranges.TSRANGE,
2130 "tstzrange": _ranges.TSTZRANGE,
2131 "int4multirange": _ranges.INT4MULTIRANGE,
2132 "int8multirange": _ranges.INT8MULTIRANGE,
2133 "nummultirange": _ranges.NUMMULTIRANGE,
2134 "datemultirange": _ranges.DATEMULTIRANGE,
2135 "tsmultirange": _ranges.TSMULTIRANGE,
2136 "tstzmultirange": _ranges.TSTZMULTIRANGE,
2137 "integer": INTEGER,
2138 "bigint": BIGINT,
2139 "smallint": SMALLINT,
2140 "character varying": VARCHAR,
2141 "character": CHAR,
2142 '"char"': sqltypes.String,
2143 "name": sqltypes.String,
2144 "text": TEXT,
2145 "numeric": NUMERIC,
2146 "float": FLOAT,
2147 "real": REAL,
2148 "inet": INET,
2149 "cidr": CIDR,
2150 "citext": CITEXT,
2151 "uuid": UUID,
2152 "bit": BIT,
2153 "bit varying": BIT,
2154 "macaddr": MACADDR,
2155 "macaddr8": MACADDR8,
2156 "money": MONEY,
2157 "oid": OID,
2158 "regclass": REGCLASS,
2159 "double precision": DOUBLE_PRECISION,
2160 "timestamp": TIMESTAMP,
2161 "timestamp with time zone": TIMESTAMP,
2162 "timestamp without time zone": TIMESTAMP,
2163 "time with time zone": TIME,
2164 "time without time zone": TIME,
2165 "date": DATE,
2166 "time": TIME,
2167 "bytea": BYTEA,
2168 "boolean": BOOLEAN,
2169 "interval": INTERVAL,
2170 "tsvector": TSVECTOR,
2171}
2172
2173
2174class PGCompiler(compiler.SQLCompiler):
2175 def visit_to_tsvector_func(self, element, **kw):
2176 return self._assert_pg_ts_ext(element, **kw)
2177
2178 def visit_to_tsquery_func(self, element, **kw):
2179 return self._assert_pg_ts_ext(element, **kw)
2180
2181 def visit_plainto_tsquery_func(self, element, **kw):
2182 return self._assert_pg_ts_ext(element, **kw)
2183
2184 def visit_phraseto_tsquery_func(self, element, **kw):
2185 return self._assert_pg_ts_ext(element, **kw)
2186
2187 def visit_websearch_to_tsquery_func(self, element, **kw):
2188 return self._assert_pg_ts_ext(element, **kw)
2189
2190 def visit_ts_headline_func(self, element, **kw):
2191 return self._assert_pg_ts_ext(element, **kw)
2192
2193 def _assert_pg_ts_ext(self, element, **kw):
2194 if not isinstance(element, _regconfig_fn):
2195 # other options here include trying to rewrite the function
2196 # with the correct types. however, that means we have to
2197 # "un-SQL-ize" the first argument, which can't work in a
2198 # generalized way. Also, parent compiler class has already added
2199 # the incorrect return type to the result map. So let's just
2200 # make sure the function we want is used up front.
2201
2202 raise exc.CompileError(
2203 f'Can\'t compile "{element.name}()" full text search '
2204 f"function construct that does not originate from the "
2205 f'"sqlalchemy.dialects.postgresql" package. '
2206 f'Please ensure "import sqlalchemy.dialects.postgresql" is '
2207 f"called before constructing "
2208 f'"sqlalchemy.func.{element.name}()" to ensure registration '
2209 f"of the correct argument and return types."
2210 )
2211
2212 return f"{element.name}{self.function_argspec(element, **kw)}"
2213
2214 def render_bind_cast(self, type_, dbapi_type, sqltext):
2215 if dbapi_type._type_affinity is sqltypes.String and dbapi_type.length:
2216 # use VARCHAR with no length for VARCHAR cast.
2217 # see #9511
2218 dbapi_type = sqltypes.STRINGTYPE
2219 return f"""{sqltext}::{
2220 self.dialect.type_compiler_instance.process(
2221 dbapi_type, identifier_preparer=self.preparer
2222 )
2223 }"""
2224
2225 def visit_array(self, element, **kw):
2226 if not element.clauses and not element.type.item_type._isnull:
2227 return "ARRAY[]::%s" % element.type.compile(self.dialect)
2228 return "ARRAY[%s]" % self.visit_clauselist(element, **kw)
2229
2230 def visit_slice(self, element, **kw):
2231 return "%s:%s" % (
2232 self.process(element.start, **kw),
2233 self.process(element.stop, **kw),
2234 )
2235
2236 def visit_bitwise_xor_op_binary(self, binary, operator, **kw):
2237 return self._generate_generic_binary(binary, " # ", **kw)
2238
2239 def visit_json_getitem_op_binary(
2240 self, binary, operator, _cast_applied=False, **kw
2241 ):
2242 if (
2243 not _cast_applied
2244 and binary.type._type_affinity is not sqltypes.JSON
2245 ):
2246 kw["_cast_applied"] = True
2247 return self.process(sql.cast(binary, binary.type), **kw)
2248
2249 kw["eager_grouping"] = True
2250
2251 if (
2252 not _cast_applied
2253 and isinstance(binary.left.type, _json.JSONB)
2254 and self.dialect._supports_jsonb_subscripting
2255 ):
2256 left = binary.left
2257 if isinstance(left, (functions.FunctionElement, elements.Cast)):
2258 left = elements.Grouping(left)
2259
2260 # for pg14+JSONB use subscript notation: col['key'] instead
2261 # of col -> 'key'
2262 return "%s[%s]" % (
2263 self.process(left, **kw),
2264 self.process(binary.right, **kw),
2265 )
2266 else:
2267 # Fall back to arrow notation for older versions or when cast
2268 # is applied
2269 return self._generate_generic_binary(
2270 binary, " -> " if not _cast_applied else " ->> ", **kw
2271 )
2272
2273 def visit_json_path_getitem_op_binary(
2274 self, binary, operator, _cast_applied=False, **kw
2275 ):
2276 if (
2277 not _cast_applied
2278 and binary.type._type_affinity is not sqltypes.JSON
2279 ):
2280 kw["_cast_applied"] = True
2281 return self.process(sql.cast(binary, binary.type), **kw)
2282
2283 kw["eager_grouping"] = True
2284 return self._generate_generic_binary(
2285 binary, " #> " if not _cast_applied else " #>> ", **kw
2286 )
2287
2288 def visit_hstore_getitem_op_binary(self, binary, operator, **kw):
2289 kw["eager_grouping"] = True
2290
2291 if self.dialect._supports_jsonb_subscripting:
2292 # use subscript notation: col['key'] instead of col -> 'key'
2293 # For function calls, wrap in parentheses: (func())[key]
2294 left_str = self.process(binary.left, **kw)
2295 if isinstance(binary.left, sql.functions.FunctionElement):
2296 left_str = f"({left_str})"
2297 return "%s[%s]" % (
2298 left_str,
2299 self.process(binary.right, **kw),
2300 )
2301 else:
2302 # Fall back to arrow notation for older versions
2303 return self._generate_generic_binary(binary, " -> ", **kw)
2304
2305 def visit_getitem_binary(self, binary, operator, **kw):
2306 return "%s[%s]" % (
2307 self.process(binary.left, **kw),
2308 self.process(binary.right, **kw),
2309 )
2310
2311 def visit_aggregate_order_by(self, element, **kw):
2312 return "%s ORDER BY %s" % (
2313 self.process(element.target, **kw),
2314 self.process(element.order_by, **kw),
2315 )
2316
2317 def visit_match_op_binary(self, binary, operator, **kw):
2318 if "postgresql_regconfig" in binary.modifiers:
2319 regconfig = self.render_literal_value(
2320 binary.modifiers["postgresql_regconfig"], sqltypes.STRINGTYPE
2321 )
2322 if regconfig:
2323 return "%s @@ plainto_tsquery(%s, %s)" % (
2324 self.process(binary.left, **kw),
2325 regconfig,
2326 self.process(binary.right, **kw),
2327 )
2328 return "%s @@ plainto_tsquery(%s)" % (
2329 self.process(binary.left, **kw),
2330 self.process(binary.right, **kw),
2331 )
2332
2333 def visit_ilike_case_insensitive_operand(self, element, **kw):
2334 return element.element._compiler_dispatch(self, **kw)
2335
2336 def visit_ilike_op_binary(self, binary, operator, **kw):
2337 escape = binary.modifiers.get("escape", None)
2338
2339 return "%s ILIKE %s" % (
2340 self.process(binary.left, **kw),
2341 self.process(binary.right, **kw),
2342 ) + (
2343 " ESCAPE " + self.render_literal_value(escape, sqltypes.STRINGTYPE)
2344 if escape is not None
2345 else ""
2346 )
2347
2348 def visit_not_ilike_op_binary(self, binary, operator, **kw):
2349 escape = binary.modifiers.get("escape", None)
2350 return "%s NOT ILIKE %s" % (
2351 self.process(binary.left, **kw),
2352 self.process(binary.right, **kw),
2353 ) + (
2354 " ESCAPE " + self.render_literal_value(escape, sqltypes.STRINGTYPE)
2355 if escape is not None
2356 else ""
2357 )
2358
2359 def _regexp_match(self, base_op, binary, operator, kw):
2360 flags = binary.modifiers["flags"]
2361 if flags is None:
2362 return self._generate_generic_binary(
2363 binary, " %s " % base_op, **kw
2364 )
2365 if flags == "i":
2366 return self._generate_generic_binary(
2367 binary, " %s* " % base_op, **kw
2368 )
2369 return "%s %s CONCAT('(?', %s, ')', %s)" % (
2370 self.process(binary.left, **kw),
2371 base_op,
2372 self.render_literal_value(flags, sqltypes.STRINGTYPE),
2373 self.process(binary.right, **kw),
2374 )
2375
2376 def visit_regexp_match_op_binary(self, binary, operator, **kw):
2377 return self._regexp_match("~", binary, operator, kw)
2378
2379 def visit_not_regexp_match_op_binary(self, binary, operator, **kw):
2380 return self._regexp_match("!~", binary, operator, kw)
2381
2382 def visit_regexp_replace_op_binary(self, binary, operator, **kw):
2383 string = self.process(binary.left, **kw)
2384 pattern_replace = self.process(binary.right, **kw)
2385 flags = binary.modifiers["flags"]
2386 if flags is None:
2387 return "REGEXP_REPLACE(%s, %s)" % (
2388 string,
2389 pattern_replace,
2390 )
2391 else:
2392 return "REGEXP_REPLACE(%s, %s, %s)" % (
2393 string,
2394 pattern_replace,
2395 self.render_literal_value(flags, sqltypes.STRINGTYPE),
2396 )
2397
2398 def visit_empty_set_expr(self, element_types, **kw):
2399 # cast the empty set to the type we are comparing against. if
2400 # we are comparing against the null type, pick an arbitrary
2401 # datatype for the empty set
2402 return "SELECT %s WHERE 1!=1" % (
2403 ", ".join(
2404 "CAST(NULL AS %s)"
2405 % self.dialect.type_compiler_instance.process(
2406 INTEGER() if type_._isnull else type_
2407 )
2408 for type_ in element_types or [INTEGER()]
2409 ),
2410 )
2411
2412 def render_literal_value(self, value, type_):
2413 value = super().render_literal_value(value, type_)
2414
2415 if self.dialect._backslash_escapes:
2416 value = value.replace("\\", "\\\\")
2417 return value
2418
2419 def visit_aggregate_strings_func(self, fn, **kw):
2420 return super().visit_aggregate_strings_func(
2421 fn, use_function_name="string_agg", **kw
2422 )
2423
2424 def visit_pow_func(self, fn, **kw):
2425 return f"power{self.function_argspec(fn)}"
2426
2427 def visit_sequence(self, seq, **kw):
2428 return "nextval(%s)" % self.preparer._format_sequence_string_literal(
2429 seq
2430 )
2431
2432 def limit_clause(self, select, **kw):
2433 text = ""
2434 if select._limit_clause is not None:
2435 text += " \n LIMIT " + self.process(select._limit_clause, **kw)
2436 if select._offset_clause is not None:
2437 if select._limit_clause is None:
2438 text += "\n LIMIT ALL"
2439 text += " OFFSET " + self.process(select._offset_clause, **kw)
2440 return text
2441
2442 def format_from_hint_text(self, sqltext, table, hint, iscrud):
2443 if hint.upper() != "ONLY":
2444 raise exc.CompileError("Unrecognized hint: %r" % hint)
2445 return "ONLY " + sqltext
2446
2447 def get_select_precolumns(self, select, **kw):
2448 # Do not call super().get_select_precolumns because
2449 # it will warn/raise when distinct on is present
2450 if select._distinct or select._distinct_on:
2451 if select._distinct_on:
2452 return (
2453 "DISTINCT ON ("
2454 + ", ".join(
2455 [
2456 self.process(col, **kw)
2457 for col in select._distinct_on
2458 ]
2459 )
2460 + ") "
2461 )
2462 else:
2463 return "DISTINCT "
2464 else:
2465 return ""
2466
2467 def visit_postgresql_distinct_on(self, element, **kw):
2468 if self.stack[-1]["selectable"]._distinct_on:
2469 raise exc.CompileError(
2470 "Cannot mix ``select.ext(distinct_on(...))`` and "
2471 "``select.distinct(...)``"
2472 )
2473
2474 if element._distinct_on:
2475 cols = ", ".join(
2476 self.process(col, **kw) for col in element._distinct_on
2477 )
2478 return f"ON ({cols})"
2479 else:
2480 return None
2481
2482 def for_update_clause(self, select, **kw):
2483 if select._for_update_arg.read:
2484 if select._for_update_arg.key_share:
2485 tmp = " FOR KEY SHARE"
2486 else:
2487 tmp = " FOR SHARE"
2488 elif select._for_update_arg.key_share:
2489 tmp = " FOR NO KEY UPDATE"
2490 else:
2491 tmp = " FOR UPDATE"
2492
2493 if select._for_update_arg.of:
2494 tables = util.OrderedSet()
2495 for c in select._for_update_arg.of:
2496 tables.update(sql_util.surface_selectables_only(c))
2497
2498 of_kw = dict(kw)
2499 of_kw.update(ashint=True, use_schema=False)
2500 tmp += " OF " + ", ".join(
2501 self.process(table, **of_kw) for table in tables
2502 )
2503
2504 if select._for_update_arg.nowait:
2505 tmp += " NOWAIT"
2506 if select._for_update_arg.skip_locked:
2507 tmp += " SKIP LOCKED"
2508
2509 return tmp
2510
2511 def visit_substring_func(self, func, **kw):
2512 s = self.process(func.clauses.clauses[0], **kw)
2513 start = self.process(func.clauses.clauses[1], **kw)
2514 if len(func.clauses.clauses) > 2:
2515 length = self.process(func.clauses.clauses[2], **kw)
2516 return "SUBSTRING(%s FROM %s FOR %s)" % (s, start, length)
2517 else:
2518 return "SUBSTRING(%s FROM %s)" % (s, start)
2519
2520 def _on_conflict_target(self, clause, **kw):
2521 if clause.constraint_target is not None:
2522 # target may be a name of an Index, UniqueConstraint or
2523 # ExcludeConstraint. While there is a separate
2524 # "max_identifier_length" for indexes, PostgreSQL uses the same
2525 # length for all objects so we can use
2526 # truncate_and_render_constraint_name
2527 target_text = (
2528 "ON CONSTRAINT %s"
2529 % self.preparer.truncate_and_render_constraint_name(
2530 clause.constraint_target
2531 )
2532 )
2533 elif clause.inferred_target_elements is not None:
2534 target_text = "(%s)" % ", ".join(
2535 (
2536 self.preparer.quote(c)
2537 if isinstance(c, str)
2538 else self.process(c, include_table=False, use_schema=False)
2539 )
2540 for c in clause.inferred_target_elements
2541 )
2542 if clause.inferred_target_whereclause is not None:
2543 whereclause_kw = dict(kw)
2544 whereclause_kw.update(include_table=False, use_schema=False)
2545 target_text += " WHERE %s" % self.process(
2546 clause.inferred_target_whereclause,
2547 **whereclause_kw,
2548 )
2549 else:
2550 target_text = ""
2551
2552 return target_text
2553
2554 def visit_on_conflict_do_nothing(self, on_conflict, **kw):
2555 target_text = self._on_conflict_target(on_conflict, **kw)
2556
2557 if target_text:
2558 return "ON CONFLICT %s DO NOTHING" % target_text
2559 else:
2560 return "ON CONFLICT DO NOTHING"
2561
2562 def visit_on_conflict_do_update(self, on_conflict, **kw):
2563 clause = on_conflict
2564
2565 target_text = self._on_conflict_target(on_conflict, **kw)
2566
2567 action_set_ops = []
2568
2569 set_parameters = dict(clause.update_values_to_set)
2570 # create a list of column assignment clauses as tuples
2571
2572 insert_statement = self.stack[-1]["selectable"]
2573 cols = insert_statement.table.c
2574 set_kw = dict(kw)
2575 set_kw.update(use_schema=False)
2576 for c in cols:
2577 col_key = c.key
2578
2579 if col_key in set_parameters:
2580 value = set_parameters.pop(col_key)
2581 elif c in set_parameters:
2582 value = set_parameters.pop(c)
2583 else:
2584 continue
2585
2586 assert not coercions._is_literal(value)
2587 if (
2588 isinstance(value, elements.BindParameter)
2589 and value.type._isnull
2590 ):
2591 value = value._with_binary_element_type(c.type)
2592
2593 value_text = self.process(
2594 value.self_group(), is_upsert_set=True, **set_kw
2595 )
2596
2597 key_text = self.preparer.quote(c.name)
2598 action_set_ops.append("%s = %s" % (key_text, value_text))
2599
2600 # check for names that don't match columns
2601 if set_parameters:
2602 util.warn(
2603 "Additional column names not matching "
2604 "any column keys in table '%s': %s"
2605 % (
2606 self.current_executable.table.name,
2607 (", ".join("'%s'" % c for c in set_parameters)),
2608 )
2609 )
2610 for k, v in set_parameters.items():
2611 key_text = (
2612 self.preparer.quote(k)
2613 if isinstance(k, str)
2614 else self.process(k, use_schema=False)
2615 )
2616 value_text = self.process(
2617 coercions.expect(roles.ExpressionElementRole, v),
2618 is_upsert_set=True,
2619 **set_kw,
2620 )
2621 action_set_ops.append("%s = %s" % (key_text, value_text))
2622
2623 action_text = ", ".join(action_set_ops)
2624 if clause.update_whereclause is not None:
2625 where_kw = dict(kw)
2626 where_kw.update(include_table=True, use_schema=False)
2627 action_text += " WHERE %s" % self.process(
2628 clause.update_whereclause, **where_kw
2629 )
2630
2631 return "ON CONFLICT %s DO UPDATE SET %s" % (target_text, action_text)
2632
2633 def update_from_clause(
2634 self, update_stmt, from_table, extra_froms, from_hints, **kw
2635 ):
2636 kw["asfrom"] = True
2637 return "FROM " + ", ".join(
2638 t._compiler_dispatch(self, fromhints=from_hints, **kw)
2639 for t in extra_froms
2640 )
2641
2642 def delete_extra_from_clause(
2643 self, delete_stmt, from_table, extra_froms, from_hints, **kw
2644 ):
2645 """Render the DELETE .. USING clause specific to PostgreSQL."""
2646 kw["asfrom"] = True
2647 return "USING " + ", ".join(
2648 t._compiler_dispatch(self, fromhints=from_hints, **kw)
2649 for t in extra_froms
2650 )
2651
2652 def fetch_clause(self, select, **kw):
2653 # pg requires parens for non literal clauses. It's also required for
2654 # bind parameters if a ::type casts is used by the driver (asyncpg),
2655 # so it's easiest to just always add it
2656 text = ""
2657 if select._offset_clause is not None:
2658 text += "\n OFFSET (%s) ROWS" % self.process(
2659 select._offset_clause, **kw
2660 )
2661 if select._fetch_clause is not None:
2662 text += "\n FETCH FIRST (%s)%s ROWS %s" % (
2663 self.process(select._fetch_clause, **kw),
2664 " PERCENT" if select._fetch_clause_options["percent"] else "",
2665 (
2666 "WITH TIES"
2667 if select._fetch_clause_options["with_ties"]
2668 else "ONLY"
2669 ),
2670 )
2671 return text
2672
2673
2674class PGDDLCompiler(compiler.DDLCompiler):
2675 def get_column_specification(self, column, **kwargs):
2676 colspec = self.preparer.format_column(column)
2677 impl_type = column.type.dialect_impl(self.dialect)
2678 if isinstance(impl_type, sqltypes.TypeDecorator):
2679 impl_type = impl_type.impl
2680
2681 has_identity = (
2682 column.identity is not None
2683 and self.dialect.supports_identity_columns
2684 )
2685
2686 if (
2687 column.primary_key
2688 and column is column.table._autoincrement_column
2689 and (
2690 self.dialect.supports_smallserial
2691 or not isinstance(impl_type, sqltypes.SmallInteger)
2692 )
2693 and not has_identity
2694 and (
2695 column.default is None
2696 or (
2697 isinstance(column.default, schema.Sequence)
2698 and column.default.optional
2699 )
2700 )
2701 ):
2702 if isinstance(impl_type, sqltypes.BigInteger):
2703 colspec += " BIGSERIAL"
2704 elif isinstance(impl_type, sqltypes.SmallInteger):
2705 colspec += " SMALLSERIAL"
2706 else:
2707 colspec += " SERIAL"
2708 else:
2709 colspec += " " + self.dialect.type_compiler_instance.process(
2710 column.type,
2711 type_expression=column,
2712 identifier_preparer=self.preparer,
2713 )
2714 default = self.get_column_default_string(column)
2715 if default is not None:
2716 colspec += " DEFAULT " + default
2717
2718 if column.computed is not None:
2719 colspec += " " + self.process(column.computed)
2720 if has_identity:
2721 colspec += " " + self.process(column.identity)
2722
2723 if not column.nullable and not has_identity:
2724 colspec += " NOT NULL"
2725 elif column.nullable and has_identity:
2726 colspec += " NULL"
2727 return colspec
2728
2729 def _define_constraint_validity(self, constraint):
2730 not_valid = constraint.dialect_options["postgresql"]["not_valid"]
2731 return " NOT VALID" if not_valid else ""
2732
2733 def _define_include(self, obj):
2734 includeclause = obj.dialect_options["postgresql"]["include"]
2735 if not includeclause:
2736 return ""
2737 inclusions = [
2738 obj.table.c[col] if isinstance(col, str) else col
2739 for col in includeclause
2740 ]
2741 return " INCLUDE (%s)" % ", ".join(
2742 [self.preparer.quote(c.name) for c in inclusions]
2743 )
2744
2745 def visit_check_constraint(self, constraint, **kw):
2746 if constraint._type_bound:
2747 typ = list(constraint.columns)[0].type
2748 if (
2749 isinstance(typ, sqltypes.ARRAY)
2750 and isinstance(typ.item_type, sqltypes.Enum)
2751 and not typ.item_type.native_enum
2752 ):
2753 raise exc.CompileError(
2754 "PostgreSQL dialect cannot produce the CHECK constraint "
2755 "for ARRAY of non-native ENUM; please specify "
2756 "create_constraint=False on this Enum datatype."
2757 )
2758
2759 text = super().visit_check_constraint(constraint)
2760 text += self._define_constraint_validity(constraint)
2761 return text
2762
2763 def visit_foreign_key_constraint(self, constraint, **kw):
2764 text = super().visit_foreign_key_constraint(constraint)
2765 text += self._define_constraint_validity(constraint)
2766 return text
2767
2768 def visit_primary_key_constraint(self, constraint, **kw):
2769 text = self.define_constraint_preamble(constraint, **kw)
2770 text += self.define_primary_key_body(constraint, **kw)
2771 text += self._define_include(constraint)
2772 text += self.define_constraint_deferrability(constraint)
2773 return text
2774
2775 def visit_unique_constraint(self, constraint, **kw):
2776 if len(constraint) == 0:
2777 return ""
2778 text = self.define_constraint_preamble(constraint, **kw)
2779 text += self.define_unique_body(constraint, **kw)
2780 text += self._define_include(constraint)
2781 text += self.define_constraint_deferrability(constraint)
2782 return text
2783
2784 @util.memoized_property
2785 def _fk_ondelete_pattern(self):
2786 # a keyword phrase, optionally followed by the column list accepted
2787 # by SET NULL / SET DEFAULT. column names may be double-quoted, as
2788 # rendered by pg_get_constraintdef() when reflected
2789 ident = r'(?:[\w$]+|"(?:[^"]|"")+")'
2790 return re.compile(
2791 r"^\w+(?:\s+\w+)*"
2792 rf"(?:\s*\(\s*{ident}(?:\s*,\s*{ident})*\s*\))?$"
2793 )
2794
2795 def define_constraint_ondelete_cascade(self, constraint):
2796 return " ON DELETE %s" % self.preparer.validate_sql_phrase(
2797 constraint.ondelete, self._fk_ondelete_pattern
2798 )
2799
2800 def visit_create_enum_type(self, create, **kw):
2801 type_ = create.element
2802
2803 return "CREATE TYPE %s AS ENUM (%s)" % (
2804 self.preparer.format_type(type_),
2805 ", ".join(
2806 self.sql_compiler.process(sql.literal(e), literal_binds=True)
2807 for e in type_.enums
2808 ),
2809 )
2810
2811 def visit_drop_enum_type(self, drop, **kw):
2812 type_ = drop.element
2813
2814 return "DROP TYPE %s" % (self.preparer.format_type(type_))
2815
2816 def visit_create_domain_type(self, create, **kw):
2817 domain: DOMAIN = create.element
2818
2819 options = []
2820 if domain.collation is not None:
2821 collation = self.preparer.format_collation(
2822 domain.collation, domain.collation_schema
2823 )
2824 options.append(f"COLLATE {collation}")
2825 if domain.default is not None:
2826 default = self.render_default_string(domain.default)
2827 options.append(f"DEFAULT {default}")
2828 if domain.constraint_name is not None:
2829 name = self.preparer.truncate_and_render_constraint_name(
2830 domain.constraint_name
2831 )
2832 options.append(f"CONSTRAINT {name}")
2833 if domain.not_null:
2834 options.append("NOT NULL")
2835 if domain.check is not None:
2836 check = self.sql_compiler.process(
2837 domain.check, include_table=False, literal_binds=True
2838 )
2839 options.append(f"CHECK ({check})")
2840
2841 return (
2842 f"CREATE DOMAIN {self.preparer.format_type(domain)} AS "
2843 f"{self.type_compiler.process(domain.data_type)} "
2844 f"{' '.join(options)}"
2845 )
2846
2847 def visit_drop_domain_type(self, drop, **kw):
2848 domain = drop.element
2849 return f"DROP DOMAIN {self.preparer.format_type(domain)}"
2850
2851 def _prepare_withclause_opts(self, withclause):
2852 with_opts = []
2853 for param, value in withclause.items():
2854 if value is None:
2855 with_opts.append(param)
2856 elif isinstance(value, bool):
2857 with_opts.append(
2858 "%s = %s" % (param, "true" if value else "false")
2859 )
2860 else:
2861 with_opts.append("%s = %s" % (param, value))
2862 return ", ".join(with_opts)
2863
2864 def create_table_select_suffixes(self, element, type_, **kw):
2865 if type_ == "view":
2866 withclause = element.dialect_options["postgresql"]["with"]
2867 if withclause:
2868 return "WITH (%s)" % self._prepare_withclause_opts(withclause)
2869 return ""
2870
2871 def visit_create_index(self, create, **kw):
2872 preparer = self.preparer
2873 index = create.element
2874 self._verify_index_table(index)
2875 text = "CREATE "
2876 if index.unique:
2877 text += "UNIQUE "
2878
2879 text += "INDEX "
2880
2881 if self.dialect._supports_create_index_concurrently:
2882 concurrently = index.dialect_options["postgresql"]["concurrently"]
2883 if concurrently:
2884 text += "CONCURRENTLY "
2885
2886 if create.if_not_exists:
2887 text += "IF NOT EXISTS "
2888
2889 text += "%s ON %s " % (
2890 self._prepared_index_name(index, include_schema=False),
2891 preparer.format_table(index.table),
2892 )
2893
2894 using = index.dialect_options["postgresql"]["using"]
2895 if using:
2896 text += (
2897 "USING %s "
2898 % self.preparer.validate_sql_phrase(using, IDX_USING).lower()
2899 )
2900
2901 ops = index.dialect_options["postgresql"]["ops"]
2902 text += "(%s)" % (
2903 ", ".join(
2904 [
2905 self.sql_compiler.process(
2906 (
2907 expr.self_group()
2908 if not isinstance(expr, expression.ColumnClause)
2909 else expr
2910 ),
2911 include_table=False,
2912 literal_binds=True,
2913 )
2914 + (
2915 (" " + ops[expr.key])
2916 if hasattr(expr, "key") and expr.key in ops
2917 else ""
2918 )
2919 for expr in index.expressions
2920 ]
2921 )
2922 )
2923
2924 text += self._define_include(index)
2925
2926 nulls_not_distinct = index.dialect_options["postgresql"][
2927 "nulls_not_distinct"
2928 ]
2929 if nulls_not_distinct is True:
2930 text += " NULLS NOT DISTINCT"
2931 elif nulls_not_distinct is False:
2932 text += " NULLS DISTINCT"
2933
2934 withclause = index.dialect_options["postgresql"]["with"]
2935 if withclause:
2936 text += " WITH (%s)" % self._prepare_withclause_opts(withclause)
2937
2938 tablespace_name = index.dialect_options["postgresql"]["tablespace"]
2939 if tablespace_name:
2940 text += " TABLESPACE %s" % preparer.quote(tablespace_name)
2941
2942 whereclause = index.dialect_options["postgresql"]["where"]
2943 if whereclause is not None:
2944 whereclause = coercions.expect(
2945 roles.DDLExpressionRole, whereclause
2946 )
2947
2948 where_compiled = self.sql_compiler.process(
2949 whereclause, include_table=False, literal_binds=True
2950 )
2951 text += " WHERE " + where_compiled
2952
2953 return text
2954
2955 def define_unique_constraint_distinct(self, constraint, **kw):
2956 nulls_not_distinct = constraint.dialect_options["postgresql"][
2957 "nulls_not_distinct"
2958 ]
2959 if nulls_not_distinct is True:
2960 nulls_not_distinct_param = "NULLS NOT DISTINCT "
2961 elif nulls_not_distinct is False:
2962 nulls_not_distinct_param = "NULLS DISTINCT "
2963 else:
2964 nulls_not_distinct_param = ""
2965 return nulls_not_distinct_param
2966
2967 def visit_drop_index(self, drop, **kw):
2968 index = drop.element
2969
2970 text = "\nDROP INDEX "
2971
2972 if self.dialect._supports_drop_index_concurrently:
2973 concurrently = index.dialect_options["postgresql"]["concurrently"]
2974 if concurrently:
2975 text += "CONCURRENTLY "
2976
2977 if drop.if_exists:
2978 text += "IF EXISTS "
2979
2980 text += self._prepared_index_name(index, include_schema=True)
2981 return text
2982
2983 def visit_exclude_constraint(self, constraint, **kw):
2984 text = ""
2985 if constraint.name is not None:
2986 text += "CONSTRAINT %s " % self.preparer.format_constraint(
2987 constraint
2988 )
2989 elements = []
2990 kw["include_table"] = False
2991 kw["literal_binds"] = True
2992 for expr, name, op in constraint._render_exprs:
2993 exclude_element = self.sql_compiler.process(expr, **kw) + (
2994 (" " + constraint.ops[expr.key])
2995 if hasattr(expr, "key") and expr.key in constraint.ops
2996 else ""
2997 )
2998
2999 elements.append("%s WITH %s" % (exclude_element, op))
3000 text += "EXCLUDE USING %s (%s)" % (
3001 self.preparer.validate_sql_phrase(
3002 constraint.using, IDX_USING
3003 ).lower(),
3004 ", ".join(elements),
3005 )
3006 if constraint.where is not None:
3007 text += " WHERE (%s)" % self.sql_compiler.process(
3008 constraint.where, literal_binds=True
3009 )
3010 text += self.define_constraint_deferrability(constraint)
3011 return text
3012
3013 def post_create_table(self, table):
3014 table_opts = []
3015 pg_opts = table.dialect_options["postgresql"]
3016
3017 inherits = pg_opts.get("inherits")
3018 if inherits is not None:
3019 if not isinstance(inherits, (list, tuple)):
3020 inherits = (inherits,)
3021 table_opts.append(
3022 "\n INHERITS ("
3023 + ", ".join(self.preparer.quote(name) for name in inherits)
3024 + ")"
3025 )
3026
3027 if pg_opts["partition_by"]:
3028 table_opts.append("\n PARTITION BY %s" % pg_opts["partition_by"])
3029
3030 if pg_opts["using"]:
3031 table_opts.append("\n USING %s" % pg_opts["using"])
3032
3033 if pg_opts["with"]:
3034 table_opts.append(
3035 "\n WITH (%s)" % self._prepare_withclause_opts(pg_opts["with"])
3036 )
3037
3038 if pg_opts["with_oids"] is True:
3039 table_opts.append("\n WITH OIDS")
3040 elif pg_opts["with_oids"] is False:
3041 table_opts.append("\n WITHOUT OIDS")
3042
3043 if pg_opts["on_commit"]:
3044 on_commit_options = pg_opts["on_commit"].replace("_", " ").upper()
3045 table_opts.append("\n ON COMMIT %s" % on_commit_options)
3046
3047 if pg_opts["tablespace"]:
3048 tablespace_name = pg_opts["tablespace"]
3049 table_opts.append(
3050 "\n TABLESPACE %s" % self.preparer.quote(tablespace_name)
3051 )
3052
3053 return "".join(table_opts)
3054
3055 def visit_computed_column(self, generated, **kw):
3056 if self.dialect.supports_virtual_generated_columns:
3057 return super().visit_computed_column(generated, **kw)
3058 if generated.persisted is False:
3059 raise exc.CompileError(
3060 "PostrgreSQL computed columns do not support 'virtual' "
3061 "persistence; set the 'persisted' flag to None or True for "
3062 "PostgreSQL support."
3063 )
3064 elif generated.persisted is None:
3065 util.warn(
3066 f"Computed column {generated.column} is being created as "
3067 "'STORED' since the current PostgreSQL version does not "
3068 "support VIRTUAL columns. On PostgreSQL 18+, when "
3069 "'persisted' is not "
3070 "specified, no keyword will be rendered and VIRTUAL will be "
3071 "used by default. Set 'persisted=True' to ensure STORED "
3072 "behavior across all PostgreSQL versions."
3073 )
3074
3075 return "GENERATED ALWAYS AS (%s) STORED" % self.sql_compiler.process(
3076 generated.sqltext, include_table=False, literal_binds=True
3077 )
3078
3079 def visit_create_sequence(self, create, **kw):
3080 prefix = None
3081 if create.element.data_type is not None:
3082 prefix = " AS %s" % self.type_compiler.process(
3083 create.element.data_type
3084 )
3085
3086 return super().visit_create_sequence(create, prefix=prefix, **kw)
3087
3088 def _can_comment_on_constraint(self, ddl_instance):
3089 constraint = ddl_instance.element
3090 if constraint.name is None:
3091 raise exc.CompileError(
3092 f"Can't emit COMMENT ON for constraint {constraint!r}: "
3093 "it has no name"
3094 )
3095 if constraint.table is None:
3096 raise exc.CompileError(
3097 f"Can't emit COMMENT ON for constraint {constraint!r}: "
3098 "it has no associated table"
3099 )
3100
3101 def visit_set_constraint_comment(self, create, **kw):
3102 self._can_comment_on_constraint(create)
3103 return "COMMENT ON CONSTRAINT %s ON %s IS %s" % (
3104 self.preparer.format_constraint(create.element),
3105 self.preparer.format_table(create.element.table),
3106 self.sql_compiler.render_literal_value(
3107 create.element.comment, sqltypes.String()
3108 ),
3109 )
3110
3111 def visit_drop_constraint_comment(self, drop, **kw):
3112 self._can_comment_on_constraint(drop)
3113 return "COMMENT ON CONSTRAINT %s ON %s IS NULL" % (
3114 self.preparer.format_constraint(drop.element),
3115 self.preparer.format_table(drop.element.table),
3116 )
3117
3118
3119class PGTypeCompiler(compiler.GenericTypeCompiler):
3120 def visit_TSVECTOR(self, type_, **kw):
3121 return "TSVECTOR"
3122
3123 def visit_TSQUERY(self, type_, **kw):
3124 return "TSQUERY"
3125
3126 def visit_INET(self, type_, **kw):
3127 return "INET"
3128
3129 def visit_CIDR(self, type_, **kw):
3130 return "CIDR"
3131
3132 def visit_CITEXT(self, type_, **kw):
3133 return "CITEXT"
3134
3135 def visit_MACADDR(self, type_, **kw):
3136 return "MACADDR"
3137
3138 def visit_MACADDR8(self, type_, **kw):
3139 return "MACADDR8"
3140
3141 def visit_MONEY(self, type_, **kw):
3142 return "MONEY"
3143
3144 def visit_OID(self, type_, **kw):
3145 return "OID"
3146
3147 def visit_REGCONFIG(self, type_, **kw):
3148 return "REGCONFIG"
3149
3150 def visit_REGCLASS(self, type_, **kw):
3151 return "REGCLASS"
3152
3153 def visit_FLOAT(self, type_, **kw):
3154 if not type_.precision:
3155 return "FLOAT"
3156 else:
3157 return "FLOAT(%(precision)s)" % {"precision": type_.precision}
3158
3159 def visit_double(self, type_, **kw):
3160 return self.visit_DOUBLE_PRECISION(type, **kw)
3161
3162 def visit_BIGINT(self, type_, **kw):
3163 return "BIGINT"
3164
3165 def visit_HSTORE(self, type_, **kw):
3166 return "HSTORE"
3167
3168 def visit_JSON(self, type_, **kw):
3169 return "JSON"
3170
3171 def visit_JSONB(self, type_, **kw):
3172 return "JSONB"
3173
3174 def visit_INT4MULTIRANGE(self, type_, **kw):
3175 return "INT4MULTIRANGE"
3176
3177 def visit_INT8MULTIRANGE(self, type_, **kw):
3178 return "INT8MULTIRANGE"
3179
3180 def visit_NUMMULTIRANGE(self, type_, **kw):
3181 return "NUMMULTIRANGE"
3182
3183 def visit_DATEMULTIRANGE(self, type_, **kw):
3184 return "DATEMULTIRANGE"
3185
3186 def visit_TSMULTIRANGE(self, type_, **kw):
3187 return "TSMULTIRANGE"
3188
3189 def visit_TSTZMULTIRANGE(self, type_, **kw):
3190 return "TSTZMULTIRANGE"
3191
3192 def visit_INT4RANGE(self, type_, **kw):
3193 return "INT4RANGE"
3194
3195 def visit_INT8RANGE(self, type_, **kw):
3196 return "INT8RANGE"
3197
3198 def visit_NUMRANGE(self, type_, **kw):
3199 return "NUMRANGE"
3200
3201 def visit_DATERANGE(self, type_, **kw):
3202 return "DATERANGE"
3203
3204 def visit_TSRANGE(self, type_, **kw):
3205 return "TSRANGE"
3206
3207 def visit_TSTZRANGE(self, type_, **kw):
3208 return "TSTZRANGE"
3209
3210 def visit_json_int_index(self, type_, **kw):
3211 return "INT"
3212
3213 def visit_json_str_index(self, type_, **kw):
3214 return "TEXT"
3215
3216 def visit_datetime(self, type_, **kw):
3217 return self.visit_TIMESTAMP(type_, **kw)
3218
3219 def visit_enum(self, type_, **kw):
3220 if not type_.native_enum or not self.dialect.supports_native_enum:
3221 return super().visit_enum(type_, **kw)
3222 else:
3223 return self.visit_ENUM(type_, **kw)
3224
3225 def visit_ENUM(self, type_, identifier_preparer=None, **kw):
3226 if identifier_preparer is None:
3227 identifier_preparer = self.dialect.identifier_preparer
3228 return identifier_preparer.format_type(type_)
3229
3230 def visit_DOMAIN(self, type_, identifier_preparer=None, **kw):
3231 if identifier_preparer is None:
3232 identifier_preparer = self.dialect.identifier_preparer
3233 return identifier_preparer.format_type(type_)
3234
3235 def visit_TIMESTAMP(self, type_, **kw):
3236 return "TIMESTAMP%s %s" % (
3237 (
3238 "(%d)" % type_.precision
3239 if getattr(type_, "precision", None) is not None
3240 else ""
3241 ),
3242 (type_.timezone and "WITH" or "WITHOUT") + " TIME ZONE",
3243 )
3244
3245 def visit_TIME(self, type_, **kw):
3246 return "TIME%s %s" % (
3247 (
3248 "(%d)" % type_.precision
3249 if getattr(type_, "precision", None) is not None
3250 else ""
3251 ),
3252 (type_.timezone and "WITH" or "WITHOUT") + " TIME ZONE",
3253 )
3254
3255 def visit_INTERVAL(self, type_, **kw):
3256 text = "INTERVAL"
3257 if type_.fields is not None:
3258 text += " " + type_.fields
3259 if type_.precision is not None:
3260 text += " (%d)" % type_.precision
3261 return text
3262
3263 def visit_BIT(self, type_, **kw):
3264 if type_.varying:
3265 compiled = "BIT VARYING"
3266 if type_.length is not None:
3267 compiled += "(%d)" % type_.length
3268 else:
3269 compiled = "BIT(%d)" % type_.length
3270 return compiled
3271
3272 def visit_uuid(self, type_, **kw):
3273 if type_.native_uuid:
3274 return self.visit_UUID(type_, **kw)
3275 else:
3276 return super().visit_uuid(type_, **kw)
3277
3278 def visit_UUID(self, type_, **kw):
3279 return "UUID"
3280
3281 def visit_large_binary(self, type_, **kw):
3282 return self.visit_BYTEA(type_, **kw)
3283
3284 def visit_BYTEA(self, type_, **kw):
3285 return "BYTEA"
3286
3287 def visit_ARRAY(self, type_, **kw):
3288 inner = self.process(type_.item_type, **kw)
3289 return re.sub(
3290 r"((?: COLLATE.*)?)$",
3291 (
3292 r"%s\1"
3293 % (
3294 "[]"
3295 * (type_.dimensions if type_.dimensions is not None else 1)
3296 )
3297 ),
3298 inner,
3299 count=1,
3300 )
3301
3302 def visit_json_path(self, type_, **kw):
3303 return self.visit_JSONPATH(type_, **kw)
3304
3305 def visit_JSONPATH(self, type_, **kw):
3306 return "JSONPATH"
3307
3308
3309class _CompilerSequence:
3310 """Minimal stand-in for :class:`.Sequence`.
3311
3312 Used for the implicit sequence behind a SERIAL column, where no
3313 :class:`.Sequence` object exists but a name still has to be rendered
3314 through :meth:`.IdentifierPreparer.format_sequence`.
3315
3316 """
3317
3318 __slots__ = ("name", "schema")
3319
3320 # the schema handed to us has already been resolved against the
3321 # schema translate map, so don't let format_sequence() translate it
3322 # a second time
3323 _use_schema_map = False
3324
3325 def __init__(self, name, schema=None):
3326 self.name = name
3327 self.schema = schema
3328
3329
3330class PGIdentifierPreparer(compiler.IdentifierPreparer):
3331 reserved_words = RESERVED_WORDS
3332
3333 def _format_sequence_string_literal(self, sequence):
3334 """Render a sequence name as a SQL string literal.
3335
3336 ``nextval()`` and friends take the sequence as a string rather than
3337 as an identifier, so the quoted identifier produced by
3338 :meth:`.format_sequence` has to be escaped a second time for the
3339 enclosing literal.
3340
3341 """
3342 return "'%s'" % self.format_sequence(
3343 sequence, use_schema=True
3344 ).replace("'", "''")
3345
3346 def _unquote_identifier(self, value):
3347 if value[0] == self.initial_quote:
3348 value = value[1:-1].replace(
3349 self.escape_to_quote, self.escape_quote
3350 )
3351 return value
3352
3353 def format_type(self, type_, use_schema=True):
3354 if not type_.name:
3355 raise exc.CompileError(
3356 f"PostgreSQL {type_.__class__.__name__} type requires a name."
3357 )
3358
3359 name = self.quote(type_.name)
3360 effective_schema = self.schema_for_object(type_)
3361
3362 # a built-in type with the same name will obscure this type, so raise
3363 # for that case. this applies really to any visible type with the same
3364 # name in any other visible schema that would not be appropriate for
3365 # us to check against, so this is not a robust check, but
3366 # at least do something for an obvious built-in name conflict
3367 if (
3368 effective_schema is None
3369 and type_.name in self.dialect.ischema_names
3370 ):
3371 raise exc.CompileError(
3372 f"{type_!r} has name "
3373 f"'{type_.name}' that matches an existing type, and "
3374 "requires an explicit schema name in order to be rendered "
3375 "in DDL."
3376 )
3377
3378 if (
3379 not self.omit_schema
3380 and use_schema
3381 and effective_schema is not None
3382 ):
3383 name = f"{self.quote_schema(effective_schema)}.{name}"
3384 return name
3385
3386
3387class ReflectedNamedType(TypedDict):
3388 """Represents a reflected named type."""
3389
3390 name: str
3391 """Name of the type."""
3392 schema: str
3393 """The schema of the type."""
3394 visible: bool
3395 """Indicates if this type is in the current search path."""
3396
3397
3398class ReflectedDomainConstraint(TypedDict):
3399 """Represents a reflect check constraint of a domain."""
3400
3401 name: str
3402 """Name of the constraint."""
3403 check: str
3404 """The check constraint text."""
3405
3406
3407class ReflectedDomain(ReflectedNamedType):
3408 """Represents a reflected enum."""
3409
3410 type: str
3411 """The string name of the underlying data type of the domain."""
3412 nullable: bool
3413 """Indicates if the domain allows null or not."""
3414 default: Optional[str]
3415 """The string representation of the default value of this domain
3416 or ``None`` if none present.
3417 """
3418 constraints: List[ReflectedDomainConstraint]
3419 """The constraints defined in the domain, if any.
3420 The constraint are in order of evaluation by postgresql.
3421 """
3422 collation: Optional[str]
3423 """The collation for the domain."""
3424 collation_schema: Optional[str]
3425 """The schema of the collation for the domain, if the collation is not
3426 visible on the current search_path."""
3427
3428
3429class ReflectedEnum(ReflectedNamedType):
3430 """Represents a reflected enum."""
3431
3432 labels: List[str]
3433 """The labels that compose the enum."""
3434
3435
3436class PGInspector(reflection.Inspector):
3437 dialect: PGDialect
3438
3439 def get_table_oid(
3440 self, table_name: str, schema: Optional[str] = None
3441 ) -> int:
3442 """Return the OID for the given table name.
3443
3444 :param table_name: string name of the table. For special quoting,
3445 use :class:`.quoted_name`.
3446
3447 :param schema: string schema name; if omitted, uses the default schema
3448 of the database connection. For special quoting,
3449 use :class:`.quoted_name`.
3450
3451 """
3452
3453 with self._operation_context() as conn:
3454 return self.dialect.get_table_oid(
3455 conn, table_name, schema, info_cache=self.info_cache
3456 )
3457
3458 def get_domains(
3459 self, schema: Optional[str] = None
3460 ) -> List[ReflectedDomain]:
3461 """Return a list of DOMAIN objects.
3462
3463 Each member is a dictionary containing these fields:
3464
3465 * name - name of the domain
3466 * schema - the schema name for the domain.
3467 * visible - boolean, whether or not this domain is visible
3468 in the default search path.
3469 * type - the type defined by this domain.
3470 * nullable - Indicates if this domain can be ``NULL``.
3471 * default - The default value of the domain or ``None`` if the
3472 domain has no default.
3473 * constraints - A list of dict with the constraint defined by this
3474 domain. Each element contains two keys: ``name`` of the
3475 constraint and ``check`` with the constraint text.
3476
3477 :param schema: schema name. If None, the default schema
3478 (typically 'public') is used. May also be set to ``'*'`` to
3479 indicate load domains for all schemas.
3480
3481 .. versionadded:: 2.0
3482
3483 """
3484 with self._operation_context() as conn:
3485 return self.dialect._load_domains(
3486 conn, schema, info_cache=self.info_cache
3487 )
3488
3489 def get_enums(self, schema: Optional[str] = None) -> List[ReflectedEnum]:
3490 """Return a list of ENUM objects.
3491
3492 Each member is a dictionary containing these fields:
3493
3494 * name - name of the enum
3495 * schema - the schema name for the enum.
3496 * visible - boolean, whether or not this enum is visible
3497 in the default search path.
3498 * labels - a list of string labels that apply to the enum.
3499
3500 :param schema: schema name. If None, the default schema
3501 (typically 'public') is used. May also be set to ``'*'`` to
3502 indicate load enums for all schemas.
3503
3504 """
3505 with self._operation_context() as conn:
3506 return self.dialect._load_enums(
3507 conn, schema, info_cache=self.info_cache
3508 )
3509
3510 def get_foreign_table_names(
3511 self, schema: Optional[str] = None
3512 ) -> List[str]:
3513 """Return a list of FOREIGN TABLE names.
3514
3515 Behavior is similar to that of
3516 :meth:`_reflection.Inspector.get_table_names`,
3517 except that the list is limited to those tables that report a
3518 ``relkind`` value of ``f``.
3519
3520 """
3521 with self._operation_context() as conn:
3522 return self.dialect._get_foreign_table_names(
3523 conn, schema, info_cache=self.info_cache
3524 )
3525
3526 def has_type(
3527 self, type_name: str, schema: Optional[str] = None, **kw: Any
3528 ) -> bool:
3529 """Return if the database has the specified type in the provided
3530 schema.
3531
3532 :param type_name: the type to check.
3533 :param schema: schema name. If None, the default schema
3534 (typically 'public') is used. May also be set to ``'*'`` to
3535 check in all schemas.
3536
3537 .. versionadded:: 2.0
3538
3539 """
3540 with self._operation_context() as conn:
3541 return self.dialect.has_type(
3542 conn, type_name, schema, info_cache=self.info_cache
3543 )
3544
3545
3546class PGExecutionContext(default.DefaultExecutionContext):
3547 def fire_sequence(self, seq, type_):
3548 return self._execute_scalar(
3549 (
3550 "select nextval(%s)"
3551 % self.identifier_preparer._format_sequence_string_literal(seq)
3552 ),
3553 type_,
3554 )
3555
3556 def get_insert_default(self, column):
3557 if column.primary_key and column is column.table._autoincrement_column:
3558 if column.server_default and column.server_default.has_argument:
3559 # pre-execute passive defaults on primary key columns
3560 return self._execute_scalar(
3561 "select %s" % column.server_default.arg, column.type
3562 )
3563
3564 elif column.default is None or (
3565 column.default.is_sequence and column.default.optional
3566 ):
3567 # execute the sequence associated with a SERIAL primary
3568 # key column. for non-primary-key SERIAL, the ID just
3569 # generates server side.
3570
3571 try:
3572 seq_name = column._postgresql_seq_name
3573 except AttributeError:
3574 tab = column.table.name
3575 col = column.name
3576 tab = tab[0 : 29 + max(0, (29 - len(col)))]
3577 col = col[0 : 29 + max(0, (29 - len(tab)))]
3578 name = "%s_%s_seq" % (tab, col)
3579 column._postgresql_seq_name = seq_name = name
3580
3581 if column.table is not None:
3582 effective_schema = self.connection.schema_for_object(
3583 column.table
3584 )
3585 else:
3586 effective_schema = None
3587
3588 exc = (
3589 "select nextval(%s)"
3590 % self.identifier_preparer._format_sequence_string_literal(
3591 _CompilerSequence(seq_name, effective_schema)
3592 )
3593 )
3594
3595 return self._execute_scalar(exc, column.type)
3596
3597 return super().get_insert_default(column)
3598
3599
3600class PGReadOnlyConnectionCharacteristic(
3601 characteristics.ConnectionCharacteristic
3602):
3603 transactional = True
3604
3605 def reset_characteristic(self, dialect, dbapi_conn):
3606 dialect.set_readonly(dbapi_conn, False)
3607
3608 def set_characteristic(self, dialect, dbapi_conn, value):
3609 dialect.set_readonly(dbapi_conn, value)
3610
3611 def get_characteristic(self, dialect, dbapi_conn):
3612 return dialect.get_readonly(dbapi_conn)
3613
3614
3615class PGDeferrableConnectionCharacteristic(
3616 characteristics.ConnectionCharacteristic
3617):
3618 transactional = True
3619
3620 def reset_characteristic(self, dialect, dbapi_conn):
3621 dialect.set_deferrable(dbapi_conn, False)
3622
3623 def set_characteristic(self, dialect, dbapi_conn, value):
3624 dialect.set_deferrable(dbapi_conn, value)
3625
3626 def get_characteristic(self, dialect, dbapi_conn):
3627 return dialect.get_deferrable(dbapi_conn)
3628
3629
3630class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
3631 name = "postgresql"
3632 supports_statement_cache = True
3633 supports_alter = True
3634 max_identifier_length = 63
3635 supports_sane_rowcount = True
3636
3637 bind_typing = interfaces.BindTyping.RENDER_CASTS
3638
3639 supports_native_enum = True
3640 supports_native_boolean = True
3641 supports_native_uuid = True
3642 supports_smallserial = True
3643 supports_virtual_generated_columns = True
3644
3645 supports_sequences = True
3646 sequences_optional = True
3647 preexecute_autoincrement_sequences = True
3648 postfetch_lastrowid = False
3649 use_insertmanyvalues = True
3650
3651 returns_native_bytes = True
3652
3653 insertmanyvalues_implicit_sentinel = (
3654 InsertmanyvaluesSentinelOpts.ANY_AUTOINCREMENT
3655 | InsertmanyvaluesSentinelOpts.USE_INSERT_FROM_SELECT
3656 | InsertmanyvaluesSentinelOpts.RENDER_SELECT_COL_CASTS
3657 )
3658
3659 supports_comments = True
3660 supports_constraint_comments = True
3661 supports_default_values = True
3662
3663 supports_default_metavalue = True
3664
3665 supports_empty_insert = False
3666 supports_multivalues_insert = True
3667
3668 supports_identity_columns = True
3669
3670 default_paramstyle = "pyformat"
3671 ischema_names = ischema_names
3672 colspecs = colspecs
3673
3674 statement_compiler = PGCompiler
3675 ddl_compiler = PGDDLCompiler
3676 type_compiler_cls = PGTypeCompiler
3677 preparer = PGIdentifierPreparer
3678 execution_ctx_cls = PGExecutionContext
3679 inspector = PGInspector
3680
3681 update_returning = True
3682 delete_returning = True
3683 insert_returning = True
3684 update_returning_multifrom = True
3685 delete_returning_multifrom = True
3686
3687 connection_characteristics = (
3688 default.DefaultDialect.connection_characteristics
3689 )
3690 connection_characteristics = connection_characteristics.union(
3691 {
3692 "postgresql_readonly": PGReadOnlyConnectionCharacteristic(),
3693 "postgresql_deferrable": PGDeferrableConnectionCharacteristic(),
3694 }
3695 )
3696
3697 construct_arguments = [
3698 (
3699 schema.Index,
3700 {
3701 "using": False,
3702 "include": None,
3703 "where": None,
3704 "ops": {},
3705 "concurrently": False,
3706 "with": {},
3707 "tablespace": None,
3708 "nulls_not_distinct": None,
3709 "invalid": DialectKWArgConst.REFLECTED_ONLY,
3710 },
3711 ),
3712 (
3713 schema.Table,
3714 {
3715 "ignore_search_path": False,
3716 "tablespace": None,
3717 "partition_by": None,
3718 "with_oids": None,
3719 "with": None,
3720 "on_commit": None,
3721 "inherits": None,
3722 "using": None,
3723 },
3724 ),
3725 (
3726 schema.CheckConstraint,
3727 {
3728 "not_valid": False,
3729 },
3730 ),
3731 (
3732 schema.ForeignKeyConstraint,
3733 {
3734 "not_valid": False,
3735 },
3736 ),
3737 (
3738 schema.PrimaryKeyConstraint,
3739 {"include": None},
3740 ),
3741 (
3742 schema.UniqueConstraint,
3743 {
3744 "include": None,
3745 "nulls_not_distinct": None,
3746 },
3747 ),
3748 (
3749 schema.CreateView,
3750 {
3751 "with": None,
3752 },
3753 ),
3754 ]
3755
3756 reflection_options = ("postgresql_ignore_search_path",)
3757
3758 _backslash_escapes = False
3759 _supports_create_index_concurrently = True
3760 _supports_drop_index_concurrently = True
3761 _supports_jsonb_subscripting = True
3762 _pg_am_btree_oid = -1
3763
3764 def __init__(
3765 self,
3766 native_inet_types=None,
3767 json_serializer=None,
3768 json_deserializer=None,
3769 **kwargs,
3770 ):
3771 default.DefaultDialect.__init__(self, **kwargs)
3772
3773 self._native_inet_types = native_inet_types
3774 self._json_deserializer = json_deserializer
3775 self._json_serializer = json_serializer
3776
3777 def initialize(self, connection):
3778 super().initialize(connection)
3779
3780 # https://www.postgresql.org/docs/9.3/static/release-9-2.html#AEN116689
3781 self.supports_smallserial = self.server_version_info >= (9, 2)
3782
3783 self._set_backslash_escapes(connection)
3784
3785 self._supports_drop_index_concurrently = self.server_version_info >= (
3786 9,
3787 2,
3788 )
3789 self.supports_identity_columns = self.server_version_info >= (10,)
3790
3791 self._supports_jsonb_subscripting = self.server_version_info >= (14,)
3792
3793 self.supports_virtual_generated_columns = self.server_version_info >= (
3794 18,
3795 )
3796
3797 def get_isolation_level_values(self, dbapi_conn):
3798 # note the generic dialect doesn't have AUTOCOMMIT, however
3799 # all postgresql dialects should include AUTOCOMMIT.
3800 return (
3801 "SERIALIZABLE",
3802 "READ UNCOMMITTED",
3803 "READ COMMITTED",
3804 "REPEATABLE READ",
3805 )
3806
3807 def set_isolation_level(self, dbapi_connection, level):
3808 cursor = dbapi_connection.cursor()
3809 cursor.execute(
3810 "SET SESSION CHARACTERISTICS AS TRANSACTION "
3811 f"ISOLATION LEVEL {level}"
3812 )
3813 cursor.execute("COMMIT")
3814 cursor.close()
3815
3816 def get_isolation_level(self, dbapi_connection):
3817 cursor = dbapi_connection.cursor()
3818 cursor.execute("show transaction isolation level")
3819 val = cursor.fetchone()[0]
3820 cursor.close()
3821 return val.upper()
3822
3823 def set_readonly(self, connection, value):
3824 raise NotImplementedError()
3825
3826 def get_readonly(self, connection):
3827 raise NotImplementedError()
3828
3829 def set_deferrable(self, connection, value):
3830 raise NotImplementedError()
3831
3832 def get_deferrable(self, connection):
3833 raise NotImplementedError()
3834
3835 def _split_multihost_from_url(self, url: URL) -> Union[
3836 Tuple[None, None],
3837 Tuple[Tuple[Optional[str], ...], Tuple[Optional[int], ...]],
3838 ]:
3839 hosts: Optional[Tuple[Optional[str], ...]] = None
3840 ports_str: Union[str, Tuple[Optional[str], ...], None] = None
3841
3842 integrated_multihost = False
3843
3844 if "host" in url.query:
3845 if isinstance(url.query["host"], (list, tuple)):
3846 integrated_multihost = True
3847 hosts, ports_str = zip(
3848 *[
3849 token.split(":") if ":" in token else (token, None)
3850 for token in url.query["host"]
3851 ]
3852 )
3853
3854 elif isinstance(url.query["host"], str):
3855 hosts = tuple(url.query["host"].split(","))
3856
3857 if (
3858 "port" not in url.query
3859 and len(hosts) == 1
3860 and ":" in hosts[0]
3861 ):
3862 # internet host is alphanumeric plus dots or hyphens.
3863 # this is essentially rfc1123, which refers to rfc952.
3864 # https://stackoverflow.com/questions/3523028/
3865 # valid-characters-of-a-hostname
3866 host_port_match = re.match(
3867 r"^([a-zA-Z0-9\-\.]*)(?:\:(\d*))?$", hosts[0]
3868 )
3869 if host_port_match:
3870 integrated_multihost = True
3871 h, p = host_port_match.group(1, 2)
3872 if TYPE_CHECKING:
3873 assert isinstance(h, str)
3874 assert isinstance(p, str)
3875 hosts = (h,)
3876 ports_str = cast(
3877 "Tuple[Optional[str], ...]", (p,) if p else (None,)
3878 )
3879
3880 if "port" in url.query:
3881 if integrated_multihost:
3882 raise exc.ArgumentError(
3883 "Can't mix 'multihost' formats together; use "
3884 '"host=h1,h2,h3&port=p1,p2,p3" or '
3885 '"host=h1:p1&host=h2:p2&host=h3:p3" separately'
3886 )
3887 if isinstance(url.query["port"], (list, tuple)):
3888 ports_str = url.query["port"]
3889 elif isinstance(url.query["port"], str):
3890 ports_str = tuple(url.query["port"].split(","))
3891
3892 ports: Optional[Tuple[Optional[int], ...]] = None
3893
3894 if ports_str:
3895 try:
3896 ports = tuple(int(x) if x else None for x in ports_str)
3897 except ValueError:
3898 raise exc.ArgumentError(
3899 f"Received non-integer port arguments: {ports_str}"
3900 ) from None
3901
3902 if ports and (
3903 (not hosts and len(ports) > 1)
3904 or (
3905 hosts
3906 and ports
3907 and len(hosts) != len(ports)
3908 and (len(hosts) > 1 or len(ports) > 1)
3909 )
3910 ):
3911 raise exc.ArgumentError("number of hosts and ports don't match")
3912
3913 if hosts is not None:
3914 if ports is None:
3915 ports = tuple(None for _ in hosts)
3916
3917 return hosts, ports # type: ignore
3918
3919 def do_begin_twophase(self, connection, xid):
3920 self.do_begin(connection.connection)
3921
3922 def do_prepare_twophase(self, connection, xid):
3923 connection.execute(
3924 sql.text("PREPARE TRANSACTION :xid").bindparams(
3925 sql.bindparam("xid", xid, literal_execute=True)
3926 )
3927 )
3928
3929 def do_rollback_twophase(
3930 self, connection, xid, is_prepared=True, recover=False
3931 ):
3932 if is_prepared:
3933 if recover:
3934 # FIXME: ugly hack to get out of transaction
3935 # context when committing recoverable transactions
3936 # Must find out a way how to make the dbapi not
3937 # open a transaction.
3938 connection.exec_driver_sql("ROLLBACK")
3939 connection.execute(
3940 sql.text("ROLLBACK PREPARED :xid").bindparams(
3941 sql.bindparam("xid", xid, literal_execute=True)
3942 )
3943 )
3944 connection.exec_driver_sql("BEGIN")
3945 self.do_rollback(connection.connection)
3946 else:
3947 self.do_rollback(connection.connection)
3948
3949 def do_commit_twophase(
3950 self, connection, xid, is_prepared=True, recover=False
3951 ):
3952 if is_prepared:
3953 if recover:
3954 connection.exec_driver_sql("ROLLBACK")
3955 connection.execute(
3956 sql.text("COMMIT PREPARED :xid").bindparams(
3957 sql.bindparam("xid", xid, literal_execute=True)
3958 )
3959 )
3960 connection.exec_driver_sql("BEGIN")
3961 self.do_rollback(connection.connection)
3962 else:
3963 self.do_commit(connection.connection)
3964
3965 def do_recover_twophase(self, connection):
3966 return connection.scalars(
3967 sql.text("SELECT gid FROM pg_prepared_xacts")
3968 ).all()
3969
3970 def _get_default_schema_name(self, connection):
3971 return connection.exec_driver_sql("select current_schema()").scalar()
3972
3973 @reflection.cache
3974 def has_schema(self, connection, schema, **kw):
3975 query = select(pg_catalog.pg_namespace.c.nspname).where(
3976 pg_catalog.pg_namespace.c.nspname == schema
3977 )
3978 return bool(connection.scalar(query))
3979
3980 def _pg_class_filter_scope_schema(
3981 self, query, schema, scope, pg_class_table=None
3982 ):
3983 if pg_class_table is None:
3984 pg_class_table = pg_catalog.pg_class
3985 query = query.join(
3986 pg_catalog.pg_namespace,
3987 pg_catalog.pg_namespace.c.oid == pg_class_table.c.relnamespace,
3988 )
3989
3990 if scope is ObjectScope.DEFAULT:
3991 query = query.where(pg_class_table.c.relpersistence != "t")
3992 elif scope is ObjectScope.TEMPORARY:
3993 query = query.where(pg_class_table.c.relpersistence == "t")
3994
3995 if schema is None:
3996 query = query.where(
3997 pg_catalog.pg_table_is_visible(pg_class_table.c.oid),
3998 # ignore pg_catalog schema
3999 pg_catalog.pg_namespace.c.nspname != "pg_catalog",
4000 )
4001 else:
4002 query = query.where(pg_catalog.pg_namespace.c.nspname == schema)
4003 return query
4004
4005 def _pg_class_relkind_condition(self, relkinds, pg_class_table=None):
4006 if pg_class_table is None:
4007 pg_class_table = pg_catalog.pg_class
4008 # uses the any form instead of in otherwise postgresql complaings
4009 # that 'IN could not convert type character to "char"'
4010 return pg_class_table.c.relkind == sql.any_(_array.array(relkinds))
4011
4012 @lru_cache()
4013 def _has_multi_table_query(self, schema):
4014 query = select(pg_catalog.pg_class.c.relname).where(
4015 pg_catalog.pg_class.c.relname.in_(
4016 bindparam("table_names", expanding=True)
4017 ),
4018 self._pg_class_relkind_condition(
4019 pg_catalog.RELKINDS_ALL_TABLE_LIKE
4020 ),
4021 )
4022 return self._pg_class_filter_scope_schema(
4023 query, schema, scope=ObjectScope.ANY
4024 )
4025
4026 @reflection.cache
4027 def has_table(self, connection, table_name, schema=None, **kw):
4028 # NOTE: it's not worth calling into the multi table since the query
4029 # is compatible also with this single case.
4030 self._ensure_has_table_connection(connection)
4031 query = self._has_multi_table_query(schema)
4032 return bool(connection.scalar(query, {"table_names": [table_name]}))
4033
4034 def has_multi_table(self, connection, table_names, schema=None, **kw):
4035 query = self._has_multi_table_query(schema)
4036 params = {"table_names": table_names}
4037 existing = set(connection.scalars(query, params).all())
4038 retval = {(schema, table): table in existing for table in table_names}
4039 return retval.items()
4040
4041 @reflection.cache
4042 def has_sequence(self, connection, sequence_name, schema=None, **kw):
4043 query = select(pg_catalog.pg_class.c.relname).where(
4044 pg_catalog.pg_class.c.relkind == "S",
4045 pg_catalog.pg_class.c.relname == sequence_name,
4046 )
4047 query = self._pg_class_filter_scope_schema(
4048 query, schema, scope=ObjectScope.ANY
4049 )
4050 return bool(connection.scalar(query))
4051
4052 @reflection.cache
4053 def has_type(self, connection, type_name, schema=None, **kw):
4054 query = (
4055 select(pg_catalog.pg_type.c.typname)
4056 .join(
4057 pg_catalog.pg_namespace,
4058 pg_catalog.pg_namespace.c.oid
4059 == pg_catalog.pg_type.c.typnamespace,
4060 )
4061 .where(pg_catalog.pg_type.c.typname == type_name)
4062 )
4063 if schema is None:
4064 query = query.where(
4065 pg_catalog.pg_type_is_visible(pg_catalog.pg_type.c.oid),
4066 # ignore pg_catalog schema
4067 pg_catalog.pg_namespace.c.nspname != "pg_catalog",
4068 )
4069 elif schema != "*":
4070 query = query.where(pg_catalog.pg_namespace.c.nspname == schema)
4071
4072 return bool(connection.scalar(query))
4073
4074 def _get_server_version_info(self, connection):
4075 v = connection.exec_driver_sql("select pg_catalog.version()").scalar()
4076 m = re.match(
4077 r".*(?:PostgreSQL|EnterpriseDB) "
4078 r"(\d+)\.?(\d+)?(?:\.(\d+))?(?:\.\d+)?(?:devel|beta)?",
4079 v,
4080 )
4081 if not m:
4082 raise AssertionError(
4083 "Could not determine version from string '%s'" % v
4084 )
4085 return tuple([int(x) for x in m.group(1, 2, 3) if x is not None])
4086
4087 @reflection.cache
4088 def get_table_oid(self, connection, table_name, schema=None, **kw):
4089 """Fetch the oid for schema.table_name."""
4090 query = select(pg_catalog.pg_class.c.oid).where(
4091 pg_catalog.pg_class.c.relname == table_name,
4092 self._pg_class_relkind_condition(
4093 pg_catalog.RELKINDS_ALL_TABLE_LIKE
4094 ),
4095 )
4096 query = self._pg_class_filter_scope_schema(
4097 query, schema, scope=ObjectScope.ANY
4098 )
4099 table_oid = connection.scalar(query)
4100 if table_oid is None:
4101 raise exc.NoSuchTableError(
4102 f"{schema}.{table_name}" if schema else table_name
4103 )
4104 return table_oid
4105
4106 @reflection.cache
4107 def get_schema_names(self, connection, **kw):
4108 query = (
4109 select(pg_catalog.pg_namespace.c.nspname)
4110 .where(
4111 ~pg_catalog.pg_namespace.c.nspname.startswith(
4112 "pg_", autoescape=True
4113 )
4114 )
4115 .order_by(pg_catalog.pg_namespace.c.nspname)
4116 )
4117 return connection.scalars(query).all()
4118
4119 def _get_relnames_for_relkinds(self, connection, schema, relkinds, scope):
4120 query = select(pg_catalog.pg_class.c.relname).where(
4121 self._pg_class_relkind_condition(relkinds)
4122 )
4123 query = self._pg_class_filter_scope_schema(query, schema, scope=scope)
4124 return connection.scalars(query).all()
4125
4126 @reflection.cache
4127 def get_table_names(self, connection, schema=None, **kw):
4128 return self._get_relnames_for_relkinds(
4129 connection,
4130 schema,
4131 pg_catalog.RELKINDS_TABLE_NO_FOREIGN,
4132 scope=ObjectScope.DEFAULT,
4133 )
4134
4135 @reflection.cache
4136 def get_temp_table_names(self, connection, **kw):
4137 return self._get_relnames_for_relkinds(
4138 connection,
4139 schema=None,
4140 relkinds=pg_catalog.RELKINDS_TABLE_NO_FOREIGN,
4141 scope=ObjectScope.TEMPORARY,
4142 )
4143
4144 @reflection.cache
4145 def _get_foreign_table_names(self, connection, schema=None, **kw):
4146 return self._get_relnames_for_relkinds(
4147 connection, schema, relkinds=("f",), scope=ObjectScope.ANY
4148 )
4149
4150 @reflection.cache
4151 def get_view_names(self, connection, schema=None, **kw):
4152 return self._get_relnames_for_relkinds(
4153 connection,
4154 schema,
4155 pg_catalog.RELKINDS_VIEW,
4156 scope=ObjectScope.DEFAULT,
4157 )
4158
4159 @reflection.cache
4160 def get_materialized_view_names(self, connection, schema=None, **kw):
4161 return self._get_relnames_for_relkinds(
4162 connection,
4163 schema,
4164 pg_catalog.RELKINDS_MAT_VIEW,
4165 scope=ObjectScope.DEFAULT,
4166 )
4167
4168 @reflection.cache
4169 def get_temp_view_names(self, connection, schema=None, **kw):
4170 return self._get_relnames_for_relkinds(
4171 connection,
4172 schema,
4173 # NOTE: do not include temp materialzied views (that do not
4174 # seem to be a thing at least up to version 14)
4175 pg_catalog.RELKINDS_VIEW,
4176 scope=ObjectScope.TEMPORARY,
4177 )
4178
4179 @reflection.cache
4180 def get_sequence_names(self, connection, schema=None, **kw):
4181 return self._get_relnames_for_relkinds(
4182 connection, schema, relkinds=("S",), scope=ObjectScope.ANY
4183 )
4184
4185 @reflection.cache
4186 def get_view_definition(self, connection, view_name, schema=None, **kw):
4187 query = (
4188 select(pg_catalog.pg_get_viewdef(pg_catalog.pg_class.c.oid))
4189 .select_from(pg_catalog.pg_class)
4190 .where(
4191 pg_catalog.pg_class.c.relname == view_name,
4192 self._pg_class_relkind_condition(
4193 pg_catalog.RELKINDS_VIEW + pg_catalog.RELKINDS_MAT_VIEW
4194 ),
4195 )
4196 )
4197 query = self._pg_class_filter_scope_schema(
4198 query, schema, scope=ObjectScope.ANY
4199 )
4200 res = connection.scalar(query)
4201 if res is None:
4202 raise exc.NoSuchTableError(
4203 f"{schema}.{view_name}" if schema else view_name
4204 )
4205 else:
4206 return res
4207
4208 def _prepare_filter_names(self, filter_names):
4209 if filter_names:
4210 return True, {"filter_names": filter_names}
4211 else:
4212 return False, {}
4213
4214 def _kind_to_relkinds(self, kind: ObjectKind) -> Tuple[str, ...]:
4215 if kind is ObjectKind.ANY:
4216 return pg_catalog.RELKINDS_ALL_TABLE_LIKE
4217 relkinds = ()
4218 if ObjectKind.TABLE in kind:
4219 relkinds += pg_catalog.RELKINDS_TABLE
4220 if ObjectKind.VIEW in kind:
4221 relkinds += pg_catalog.RELKINDS_VIEW
4222 if ObjectKind.MATERIALIZED_VIEW in kind:
4223 relkinds += pg_catalog.RELKINDS_MAT_VIEW
4224 return relkinds
4225
4226 @lru_cache()
4227 def _table_options_query(self, schema, has_filter_names, scope, kind):
4228 inherits_sq = (
4229 select(
4230 pg_catalog.pg_inherits.c.inhrelid,
4231 sql.func.array_agg(
4232 aggregate_order_by(
4233 pg_catalog.pg_class.c.relname,
4234 pg_catalog.pg_inherits.c.inhseqno,
4235 )
4236 ).label("parent_table_names"),
4237 )
4238 .select_from(pg_catalog.pg_inherits)
4239 .join(
4240 pg_catalog.pg_class,
4241 pg_catalog.pg_inherits.c.inhparent
4242 == pg_catalog.pg_class.c.oid,
4243 )
4244 .group_by(pg_catalog.pg_inherits.c.inhrelid)
4245 .subquery("inherits")
4246 )
4247
4248 if self.server_version_info < (12,):
4249 # this is not in the pg_catalog.pg_class since it was
4250 # removed in PostgreSQL version 12
4251 has_oids = sql.column("relhasoids", BOOLEAN)
4252 else:
4253 has_oids = sql.null().label("relhasoids")
4254
4255 relkinds = self._kind_to_relkinds(kind)
4256 query = (
4257 select(
4258 pg_catalog.pg_class.c.oid,
4259 pg_catalog.pg_class.c.relname,
4260 pg_catalog.pg_class.c.reloptions,
4261 has_oids,
4262 sql.case(
4263 (
4264 sql.and_(
4265 pg_catalog.pg_am.c.amname.is_not(None),
4266 pg_catalog.pg_am.c.amname
4267 != sql.func.current_setting(
4268 "default_table_access_method"
4269 ),
4270 ),
4271 pg_catalog.pg_am.c.amname,
4272 ),
4273 else_=sql.null(),
4274 ).label("access_method_name"),
4275 pg_catalog.pg_tablespace.c.spcname.label("tablespace_name"),
4276 inherits_sq.c.parent_table_names,
4277 )
4278 .select_from(pg_catalog.pg_class)
4279 .outerjoin(
4280 # NOTE: on postgresql < 12, this could be avoided
4281 # since relam is always 0 so nothing is joined.
4282 pg_catalog.pg_am,
4283 pg_catalog.pg_class.c.relam == pg_catalog.pg_am.c.oid,
4284 )
4285 .outerjoin(
4286 inherits_sq,
4287 pg_catalog.pg_class.c.oid == inherits_sq.c.inhrelid,
4288 )
4289 .outerjoin(
4290 pg_catalog.pg_tablespace,
4291 pg_catalog.pg_tablespace.c.oid
4292 == pg_catalog.pg_class.c.reltablespace,
4293 )
4294 .where(self._pg_class_relkind_condition(relkinds))
4295 )
4296 query = self._pg_class_filter_scope_schema(query, schema, scope=scope)
4297 if has_filter_names:
4298 query = query.where(
4299 pg_catalog.pg_class.c.relname.in_(bindparam("filter_names"))
4300 )
4301 return query
4302
4303 def get_multi_table_options(
4304 self, connection, schema, filter_names, scope, kind, **kw
4305 ):
4306 has_filter_names, params = self._prepare_filter_names(filter_names)
4307 query = self._table_options_query(
4308 schema, has_filter_names, scope, kind
4309 )
4310 rows = connection.execute(query, params).mappings()
4311 table_options = {}
4312
4313 for row in rows:
4314 current: dict[str, Any] = {}
4315 if row["access_method_name"] is not None:
4316 current["postgresql_using"] = row["access_method_name"]
4317
4318 if row["parent_table_names"]:
4319 current["postgresql_inherits"] = tuple(
4320 row["parent_table_names"]
4321 )
4322
4323 if row["reloptions"]:
4324 current["postgresql_with"] = dict(
4325 option.split("=", 1) for option in row["reloptions"]
4326 )
4327
4328 if row["relhasoids"]:
4329 current["postgresql_with_oids"] = True
4330
4331 if row["tablespace_name"] is not None:
4332 current["postgresql_tablespace"] = row["tablespace_name"]
4333
4334 table_options[(schema, row["relname"])] = current
4335
4336 return table_options.items()
4337
4338 @lru_cache()
4339 def _columns_query(self, schema, has_filter_names, scope, kind):
4340 # NOTE: the query with the default and identity options scalar
4341 # subquery is faster than trying to use outer joins for them
4342 generated = (
4343 pg_catalog.pg_attribute.c.attgenerated.label("generated")
4344 if self.server_version_info >= (12,)
4345 else sql.null().label("generated")
4346 )
4347 if self.server_version_info >= (10,):
4348 # join lateral performs worse (~2x slower) than a scalar_subquery
4349 # also the subquery can be run only if the column is an identity
4350 identity = sql.case(
4351 ( # attidentity != '' is required or it will reflect also
4352 # serial columns as identity.
4353 pg_catalog.pg_attribute.c.attidentity != "",
4354 select(
4355 sql.func.json_build_object(
4356 "always",
4357 pg_catalog.pg_attribute.c.attidentity == "a",
4358 "start",
4359 pg_catalog.pg_sequence.c.seqstart,
4360 "increment",
4361 pg_catalog.pg_sequence.c.seqincrement,
4362 "minvalue",
4363 pg_catalog.pg_sequence.c.seqmin,
4364 "maxvalue",
4365 pg_catalog.pg_sequence.c.seqmax,
4366 "cache",
4367 pg_catalog.pg_sequence.c.seqcache,
4368 "cycle",
4369 pg_catalog.pg_sequence.c.seqcycle,
4370 type_=sqltypes.JSON(),
4371 )
4372 )
4373 .select_from(pg_catalog.pg_sequence)
4374 .where(
4375 # not needed but pg seems to like it
4376 pg_catalog.pg_attribute.c.attidentity != "",
4377 pg_catalog.pg_sequence.c.seqrelid
4378 == sql.cast(
4379 sql.cast(
4380 pg_catalog.pg_get_serial_sequence(
4381 sql.cast(
4382 sql.cast(
4383 pg_catalog.pg_attribute.c.attrelid,
4384 REGCLASS,
4385 ),
4386 TEXT,
4387 ),
4388 pg_catalog.pg_attribute.c.attname,
4389 ),
4390 REGCLASS,
4391 ),
4392 OID,
4393 ),
4394 )
4395 .correlate(pg_catalog.pg_attribute)
4396 .scalar_subquery(),
4397 ),
4398 else_=sql.null(),
4399 ).label("identity_options")
4400 else:
4401 identity = sql.null().label("identity_options")
4402
4403 # join lateral performs the same as scalar_subquery here, also
4404 # the subquery can be run only if the column has a default
4405 default = sql.case(
4406 (
4407 pg_catalog.pg_attribute.c.atthasdef,
4408 select(
4409 pg_catalog.pg_get_expr(
4410 pg_catalog.pg_attrdef.c.adbin,
4411 pg_catalog.pg_attrdef.c.adrelid,
4412 )
4413 )
4414 .select_from(pg_catalog.pg_attrdef)
4415 .where(
4416 # not needed but pg seems to like it
4417 pg_catalog.pg_attribute.c.atthasdef,
4418 pg_catalog.pg_attrdef.c.adrelid
4419 == pg_catalog.pg_attribute.c.attrelid,
4420 pg_catalog.pg_attrdef.c.adnum
4421 == pg_catalog.pg_attribute.c.attnum,
4422 )
4423 .correlate(pg_catalog.pg_attribute)
4424 .scalar_subquery(),
4425 ),
4426 else_=sql.null(),
4427 ).label("default")
4428
4429 # get the name and schema of the collate when it's different from
4430 # the default one; the schema is only included when the collation
4431 # is not visible on the current search_path
4432 collate = sql.case(
4433 (
4434 sql.and_(
4435 pg_catalog.pg_attribute.c.attcollation != 0,
4436 select(pg_catalog.pg_type.c.typcollation)
4437 .where(
4438 pg_catalog.pg_type.c.oid
4439 == pg_catalog.pg_attribute.c.atttypid,
4440 )
4441 .correlate(pg_catalog.pg_attribute)
4442 .scalar_subquery()
4443 != pg_catalog.pg_attribute.c.attcollation,
4444 ),
4445 select(
4446 sql.func.json_build_object(
4447 "name",
4448 pg_catalog.pg_collation.c.collname,
4449 "schema",
4450 sql.case(
4451 (
4452 pg_catalog.pg_collation_is_visible(
4453 pg_catalog.pg_collation.c.oid
4454 ),
4455 sql.null(),
4456 ),
4457 else_=pg_catalog.pg_namespace.c.nspname,
4458 ),
4459 type_=sqltypes.JSON(),
4460 )
4461 )
4462 .select_from(pg_catalog.pg_collation)
4463 .join(
4464 pg_catalog.pg_namespace,
4465 pg_catalog.pg_namespace.c.oid
4466 == pg_catalog.pg_collation.c.collnamespace,
4467 )
4468 .where(
4469 pg_catalog.pg_collation.c.oid
4470 == pg_catalog.pg_attribute.c.attcollation
4471 )
4472 .correlate(pg_catalog.pg_attribute)
4473 .scalar_subquery(),
4474 ),
4475 else_=sql.null(),
4476 ).label("collation")
4477
4478 relkinds = self._kind_to_relkinds(kind)
4479 query = (
4480 select(
4481 pg_catalog.pg_attribute.c.attname.label("name"),
4482 pg_catalog.format_type(
4483 pg_catalog.pg_attribute.c.atttypid,
4484 pg_catalog.pg_attribute.c.atttypmod,
4485 ).label("format_type"),
4486 default,
4487 pg_catalog.pg_attribute.c.attnotnull.label("not_null"),
4488 pg_catalog.pg_class.c.relname.label("table_name"),
4489 pg_catalog.pg_description.c.description.label("comment"),
4490 generated,
4491 identity,
4492 collate,
4493 )
4494 .select_from(pg_catalog.pg_class)
4495 # NOTE: postgresql support table with no user column, meaning
4496 # there is no row with pg_attribute.attnum > 0. use a left outer
4497 # join to avoid filtering these tables.
4498 .outerjoin(
4499 pg_catalog.pg_attribute,
4500 sql.and_(
4501 pg_catalog.pg_class.c.oid
4502 == pg_catalog.pg_attribute.c.attrelid,
4503 pg_catalog.pg_attribute.c.attnum > 0,
4504 ~pg_catalog.pg_attribute.c.attisdropped,
4505 ),
4506 )
4507 .outerjoin(
4508 pg_catalog.pg_description,
4509 sql.and_(
4510 pg_catalog.pg_description.c.objoid
4511 == pg_catalog.pg_attribute.c.attrelid,
4512 pg_catalog.pg_description.c.objsubid
4513 == pg_catalog.pg_attribute.c.attnum,
4514 ),
4515 )
4516 .where(self._pg_class_relkind_condition(relkinds))
4517 .order_by(
4518 pg_catalog.pg_class.c.relname, pg_catalog.pg_attribute.c.attnum
4519 )
4520 )
4521 query = self._pg_class_filter_scope_schema(query, schema, scope=scope)
4522 if has_filter_names:
4523 query = query.where(
4524 pg_catalog.pg_class.c.relname.in_(bindparam("filter_names"))
4525 )
4526 return query
4527
4528 def get_multi_columns(
4529 self, connection, schema, filter_names, scope, kind, **kw
4530 ):
4531 has_filter_names, params = self._prepare_filter_names(filter_names)
4532 query = self._columns_query(schema, has_filter_names, scope, kind)
4533 rows = connection.execute(query, params).mappings()
4534
4535 named_type_loader = _NamedTypeLoader(self, connection, kw)
4536 columns = self._get_columns_info(rows, named_type_loader, schema)
4537
4538 return columns.items()
4539
4540 _format_type_args_pattern = re.compile(r"\((.*)\)")
4541 _format_type_args_delim = re.compile(r"\s*,\s*")
4542 _format_array_spec_pattern = re.compile(r"((?:\[\])*)$")
4543
4544 def _reflect_type(
4545 self,
4546 format_type: Optional[str],
4547 named_type_loader: _NamedTypeLoader,
4548 type_description: str,
4549 collation: Optional[str],
4550 collation_schema: Optional[str] = None,
4551 ) -> sqltypes.TypeEngine[Any]:
4552 """
4553 Attempts to reconstruct a column type defined in ischema_names based
4554 on the information available in the format_type.
4555
4556 If the `format_type` cannot be associated with a known `ischema_names`,
4557 it is treated as a reference to a known PostgreSQL named `ENUM` or
4558 `DOMAIN` type.
4559 """
4560 type_description = type_description or "unknown type"
4561 if format_type is None:
4562 util.warn(
4563 "PostgreSQL format_type() returned NULL for %s"
4564 % type_description
4565 )
4566 return sqltypes.NULLTYPE
4567
4568 attype_args_match = self._format_type_args_pattern.search(format_type)
4569 if attype_args_match and attype_args_match.group(1):
4570 attype_args = self._format_type_args_delim.split(
4571 attype_args_match.group(1)
4572 )
4573 else:
4574 attype_args = ()
4575
4576 match_array_dim = self._format_array_spec_pattern.search(format_type)
4577 # Each "[]" in array specs corresponds to an array dimension
4578 array_dim = len(match_array_dim.group(1) or "") // 2
4579
4580 # Remove all parameters and array specs from format_type to obtain an
4581 # ischema_name candidate
4582 attype = self._format_type_args_pattern.sub("", format_type)
4583 attype = self._format_array_spec_pattern.sub("", attype)
4584
4585 schema_type = self.ischema_names.get(attype.lower(), None)
4586 args, kwargs = (), {}
4587
4588 if attype == "numeric":
4589 if len(attype_args) == 2:
4590 precision, scale = map(int, attype_args)
4591 args = (precision, scale)
4592
4593 elif attype == "double precision":
4594 args = (53,)
4595
4596 elif attype == "integer":
4597 args = ()
4598
4599 elif attype in ("timestamp with time zone", "time with time zone"):
4600 kwargs["timezone"] = True
4601 if len(attype_args) == 1:
4602 kwargs["precision"] = int(attype_args[0])
4603
4604 elif attype in (
4605 "timestamp without time zone",
4606 "time without time zone",
4607 "time",
4608 ):
4609 kwargs["timezone"] = False
4610 if len(attype_args) == 1:
4611 kwargs["precision"] = int(attype_args[0])
4612
4613 elif attype == "bit varying":
4614 kwargs["varying"] = True
4615 if len(attype_args) == 1:
4616 charlen = int(attype_args[0])
4617 args = (charlen,)
4618
4619 # a domain or enum can start with interval, so be mindful of that.
4620 elif attype == "interval" or attype.startswith("interval "):
4621 schema_type = INTERVAL
4622
4623 field_match = re.match(r"interval (.+)", attype)
4624 if field_match:
4625 kwargs["fields"] = field_match.group(1)
4626
4627 if len(attype_args) == 1:
4628 kwargs["precision"] = int(attype_args[0])
4629
4630 else:
4631 enum_or_domain_key = tuple(util.quoted_token_parser(attype))
4632
4633 if (
4634 schema_type is None
4635 and enum_or_domain_key in named_type_loader.enums
4636 ):
4637 schema_type = ENUM
4638 enum = named_type_loader.enums[enum_or_domain_key]
4639
4640 kwargs["name"] = enum["name"]
4641
4642 if not enum["visible"]:
4643 kwargs["schema"] = enum["schema"]
4644 args = tuple(enum["labels"])
4645 elif (
4646 schema_type is None
4647 and enum_or_domain_key in named_type_loader.domains
4648 ):
4649 schema_type = DOMAIN
4650 domain = named_type_loader.domains[enum_or_domain_key]
4651
4652 data_type = self._reflect_type(
4653 domain["type"],
4654 named_type_loader,
4655 type_description="DOMAIN '%s'" % domain["name"],
4656 collation=domain["collation"],
4657 collation_schema=domain["collation_schema"],
4658 )
4659 args = (domain["name"], data_type)
4660
4661 kwargs["collation"] = domain["collation"]
4662 kwargs["collation_schema"] = domain["collation_schema"]
4663 kwargs["default"] = domain["default"]
4664 kwargs["not_null"] = not domain["nullable"]
4665 kwargs["create_type"] = False
4666
4667 if domain["constraints"]:
4668 # We only support a single constraint
4669 check_constraint = domain["constraints"][0]
4670
4671 kwargs["constraint_name"] = check_constraint["name"]
4672 kwargs["check"] = check_constraint["check"]
4673
4674 if not domain["visible"]:
4675 kwargs["schema"] = domain["schema"]
4676
4677 else:
4678 try:
4679 charlen = int(attype_args[0])
4680 args = (charlen, *attype_args[1:])
4681 except (ValueError, IndexError):
4682 args = attype_args
4683
4684 if not schema_type:
4685 util.warn(
4686 "Did not recognize type '%s' of %s"
4687 % (attype, type_description)
4688 )
4689 return sqltypes.NULLTYPE
4690
4691 if collation is not None:
4692 kwargs["collation"] = collation
4693 if collation_schema is not None:
4694 kwargs["collation_schema"] = collation_schema
4695
4696 data_type = schema_type(*args, **kwargs)
4697 if array_dim >= 1:
4698 # postgres does not preserve dimensionality or size of array types.
4699 data_type = _array.ARRAY(data_type)
4700
4701 return data_type
4702
4703 def _get_columns_info(self, rows, named_type_loader, schema):
4704 columns = defaultdict(list)
4705 for row_dict in rows:
4706 # ensure that each table has an entry, even if it has no columns
4707 if row_dict["name"] is None:
4708 columns[(schema, row_dict["table_name"])] = (
4709 ReflectionDefaults.columns()
4710 )
4711 continue
4712 table_cols = columns[(schema, row_dict["table_name"])]
4713
4714 collation_info = row_dict["collation"]
4715 if collation_info is not None:
4716 collation = collation_info["name"]
4717 collation_schema = collation_info["schema"]
4718 else:
4719 collation = collation_schema = None
4720
4721 coltype = self._reflect_type(
4722 row_dict["format_type"],
4723 named_type_loader,
4724 type_description="column '%s'" % row_dict["name"],
4725 collation=collation,
4726 collation_schema=collation_schema,
4727 )
4728
4729 default = row_dict["default"]
4730 name = row_dict["name"]
4731 generated = row_dict["generated"]
4732 nullable = not row_dict["not_null"]
4733
4734 if isinstance(coltype, DOMAIN):
4735 if not default:
4736 # domain can override the default value but
4737 # can't set it to None
4738 if coltype.default is not None:
4739 default = coltype.default
4740
4741 nullable = nullable and not coltype.not_null
4742
4743 identity = row_dict["identity_options"]
4744
4745 # If a zero byte or blank string depending on driver (is also
4746 # absent for older PG versions), then not a generated column.
4747 # Otherwise, s = stored, v = virtual.
4748 if generated not in (None, "", b"\x00"):
4749 computed = dict(
4750 sqltext=default, persisted=generated in ("s", b"s")
4751 )
4752 default = None
4753 else:
4754 computed = None
4755
4756 # adjust the default value
4757 autoincrement = False
4758 if default is not None:
4759 match = re.search(r"""(nextval\(')([^']+)('.*$)""", default)
4760 if match is not None:
4761 if issubclass(coltype._type_affinity, sqltypes.Integer):
4762 autoincrement = True
4763 # the default is related to a Sequence
4764 if "." not in match.group(2) and schema is not None:
4765 # unconditionally quote the schema name. this could
4766 # later be enhanced to obey quoting rules /
4767 # "quote schema"
4768 default = (
4769 match.group(1)
4770 + ('"%s"' % schema)
4771 + "."
4772 + match.group(2)
4773 + match.group(3)
4774 )
4775
4776 column_info = {
4777 "name": name,
4778 "type": coltype,
4779 "nullable": nullable,
4780 "default": default,
4781 "autoincrement": autoincrement or identity is not None,
4782 "comment": row_dict["comment"],
4783 }
4784 if computed is not None:
4785 column_info["computed"] = computed
4786 if identity is not None:
4787 column_info["identity"] = identity
4788
4789 table_cols.append(column_info)
4790
4791 return columns
4792
4793 @lru_cache()
4794 def _table_oids_query(self, schema, has_filter_names, scope, kind):
4795 relkinds = self._kind_to_relkinds(kind)
4796 oid_q = select(
4797 pg_catalog.pg_class.c.oid, pg_catalog.pg_class.c.relname
4798 ).where(self._pg_class_relkind_condition(relkinds))
4799 oid_q = self._pg_class_filter_scope_schema(oid_q, schema, scope=scope)
4800
4801 if has_filter_names:
4802 oid_q = oid_q.where(
4803 pg_catalog.pg_class.c.relname.in_(bindparam("filter_names"))
4804 )
4805 return oid_q
4806
4807 @reflection.flexi_cache(
4808 ("schema", InternalTraversal.dp_string),
4809 ("filter_names", InternalTraversal.dp_string_list),
4810 ("kind", InternalTraversal.dp_plain_obj),
4811 ("scope", InternalTraversal.dp_plain_obj),
4812 )
4813 def _get_table_oids(
4814 self, connection, schema, filter_names, scope, kind, **kw
4815 ):
4816 has_filter_names, params = self._prepare_filter_names(filter_names)
4817 oid_q = self._table_oids_query(schema, has_filter_names, scope, kind)
4818 result = connection.execute(oid_q, params)
4819 return result.all()
4820
4821 @util.memoized_property
4822 def _constraint_query(self):
4823 if self.server_version_info >= (11, 0):
4824 indnkeyatts = pg_catalog.pg_index.c.indnkeyatts
4825 else:
4826 indnkeyatts = pg_catalog.pg_index.c.indnatts.label("indnkeyatts")
4827
4828 if self.server_version_info >= (15,):
4829 indnullsnotdistinct = pg_catalog.pg_index.c.indnullsnotdistinct
4830 else:
4831 indnullsnotdistinct = sql.false().label("indnullsnotdistinct")
4832
4833 con_sq = (
4834 select(
4835 pg_catalog.pg_constraint.c.conrelid,
4836 pg_catalog.pg_constraint.c.conname,
4837 sql.func.unnest(pg_catalog.pg_index.c.indkey).label("attnum"),
4838 sql.func.generate_subscripts(
4839 pg_catalog.pg_index.c.indkey, 1
4840 ).label("ord"),
4841 indnkeyatts,
4842 indnullsnotdistinct,
4843 pg_catalog.pg_description.c.description,
4844 )
4845 .join(
4846 pg_catalog.pg_index,
4847 pg_catalog.pg_constraint.c.conindid
4848 == pg_catalog.pg_index.c.indexrelid,
4849 )
4850 .outerjoin(
4851 pg_catalog.pg_description,
4852 sql.and_(
4853 pg_catalog.pg_description.c.objoid
4854 == pg_catalog.pg_constraint.c.oid,
4855 pg_catalog.pg_description.c.classoid
4856 == sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
4857 ),
4858 )
4859 .where(
4860 pg_catalog.pg_constraint.c.contype == bindparam("contype"),
4861 pg_catalog.pg_constraint.c.conrelid.in_(bindparam("oids")),
4862 # NOTE: filtering also on pg_index.indrelid for oids does
4863 # not seem to have a performance effect, but it may be an
4864 # option if perf problems are reported
4865 )
4866 .subquery("con")
4867 )
4868
4869 attr_sq = (
4870 select(
4871 con_sq.c.conrelid,
4872 con_sq.c.conname,
4873 con_sq.c.description,
4874 con_sq.c.ord,
4875 con_sq.c.indnkeyatts,
4876 con_sq.c.indnullsnotdistinct,
4877 pg_catalog.pg_attribute.c.attname,
4878 )
4879 .select_from(pg_catalog.pg_attribute)
4880 .join(
4881 con_sq,
4882 sql.and_(
4883 pg_catalog.pg_attribute.c.attnum == con_sq.c.attnum,
4884 pg_catalog.pg_attribute.c.attrelid == con_sq.c.conrelid,
4885 ),
4886 )
4887 .where(
4888 # NOTE: restate the condition here, since pg15 otherwise
4889 # seems to get confused on pscopg2 sometimes, doing
4890 # a sequential scan of pg_attribute.
4891 # The condition in the con_sq subquery is not actually needed
4892 # in pg15, but it may be needed in older versions. Keeping it
4893 # does not seems to have any impact in any case.
4894 con_sq.c.conrelid.in_(bindparam("oids"))
4895 )
4896 .subquery("attr")
4897 )
4898
4899 return (
4900 select(
4901 attr_sq.c.conrelid,
4902 sql.func.array_agg(
4903 # NOTE: cast since some postgresql derivatives may
4904 # not support array_agg on the name type
4905 aggregate_order_by(
4906 attr_sq.c.attname.cast(TEXT), attr_sq.c.ord
4907 )
4908 ).label("cols"),
4909 attr_sq.c.conname,
4910 sql.func.min(attr_sq.c.description).label("description"),
4911 sql.func.min(attr_sq.c.indnkeyatts).label("indnkeyatts"),
4912 sql.func.bool_and(attr_sq.c.indnullsnotdistinct).label(
4913 "indnullsnotdistinct"
4914 ),
4915 )
4916 .group_by(attr_sq.c.conrelid, attr_sq.c.conname)
4917 .order_by(attr_sq.c.conrelid, attr_sq.c.conname)
4918 )
4919
4920 def _reflect_constraint(
4921 self, connection, contype, schema, filter_names, scope, kind, **kw
4922 ):
4923 # used to reflect primary and unique constraint
4924 table_oids = self._get_table_oids(
4925 connection, schema, filter_names, scope, kind, **kw
4926 )
4927 batches = list(table_oids)
4928 is_unique = contype == "u"
4929
4930 while batches:
4931 batch = batches[0:3000]
4932 batches[0:3000] = []
4933
4934 result = connection.execute(
4935 self._constraint_query,
4936 {"oids": [r[0] for r in batch], "contype": contype},
4937 ).mappings()
4938
4939 result_by_oid = defaultdict(list)
4940 for row_dict in result:
4941 result_by_oid[row_dict["conrelid"]].append(row_dict)
4942
4943 for oid, tablename in batch:
4944 for_oid = result_by_oid.get(oid, ())
4945 if for_oid:
4946 for row in for_oid:
4947 # See note in get_multi_indexes
4948 all_cols = row["cols"]
4949 indnkeyatts = row["indnkeyatts"]
4950 if len(all_cols) > indnkeyatts:
4951 inc_cols = all_cols[indnkeyatts:]
4952 cst_cols = all_cols[:indnkeyatts]
4953 else:
4954 inc_cols = []
4955 cst_cols = all_cols
4956
4957 opts = {}
4958 if self.server_version_info >= (11,):
4959 opts["postgresql_include"] = inc_cols
4960 if is_unique:
4961 opts["postgresql_nulls_not_distinct"] = row[
4962 "indnullsnotdistinct"
4963 ]
4964 yield (
4965 tablename,
4966 cst_cols,
4967 row["conname"],
4968 row["description"],
4969 opts,
4970 )
4971 else:
4972 yield tablename, None, None, None, None
4973
4974 def get_multi_pk_constraint(
4975 self, connection, schema, filter_names, scope, kind, **kw
4976 ):
4977 result = self._reflect_constraint(
4978 connection, "p", schema, filter_names, scope, kind, **kw
4979 )
4980
4981 # only a single pk can be present for each table. Return an entry
4982 # even if a table has no primary key
4983 default = ReflectionDefaults.pk_constraint
4984
4985 def pk_constraint(pk_name, cols, comment, opts):
4986 info = {
4987 "constrained_columns": cols,
4988 "name": pk_name,
4989 "comment": comment,
4990 }
4991 if opts:
4992 info["dialect_options"] = opts
4993 return info
4994
4995 return (
4996 (
4997 (schema, table_name),
4998 (
4999 pk_constraint(pk_name, cols, comment, opts)
5000 if pk_name is not None
5001 else default()
5002 ),
5003 )
5004 for table_name, cols, pk_name, comment, opts in result
5005 )
5006
5007 @lru_cache()
5008 def _foreing_key_query(self, schema, has_filter_names, scope, kind):
5009 pg_class_ref = pg_catalog.pg_class.alias("cls_ref")
5010 pg_namespace_ref = pg_catalog.pg_namespace.alias("nsp_ref")
5011 relkinds = self._kind_to_relkinds(kind)
5012 query = (
5013 select(
5014 pg_catalog.pg_class.c.relname,
5015 pg_catalog.pg_constraint.c.conname,
5016 # NOTE: avoid calling pg_get_constraintdef when not needed
5017 # to speed up the query
5018 sql.case(
5019 (
5020 pg_catalog.pg_constraint.c.oid.is_not(None),
5021 pg_catalog.pg_get_constraintdef(
5022 pg_catalog.pg_constraint.c.oid, True
5023 ),
5024 ),
5025 else_=None,
5026 ),
5027 pg_namespace_ref.c.nspname,
5028 pg_catalog.pg_description.c.description,
5029 )
5030 .select_from(pg_catalog.pg_class)
5031 .outerjoin(
5032 pg_catalog.pg_constraint,
5033 sql.and_(
5034 pg_catalog.pg_class.c.oid
5035 == pg_catalog.pg_constraint.c.conrelid,
5036 pg_catalog.pg_constraint.c.contype == "f",
5037 ),
5038 )
5039 .outerjoin(
5040 pg_class_ref,
5041 pg_class_ref.c.oid == pg_catalog.pg_constraint.c.confrelid,
5042 )
5043 .outerjoin(
5044 pg_namespace_ref,
5045 pg_class_ref.c.relnamespace == pg_namespace_ref.c.oid,
5046 )
5047 .outerjoin(
5048 pg_catalog.pg_description,
5049 sql.and_(
5050 pg_catalog.pg_description.c.objoid
5051 == pg_catalog.pg_constraint.c.oid,
5052 pg_catalog.pg_description.c.classoid
5053 == sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
5054 ),
5055 )
5056 .order_by(
5057 pg_catalog.pg_class.c.relname,
5058 pg_catalog.pg_constraint.c.conname,
5059 )
5060 .where(self._pg_class_relkind_condition(relkinds))
5061 )
5062 query = self._pg_class_filter_scope_schema(query, schema, scope)
5063 if has_filter_names:
5064 query = query.where(
5065 pg_catalog.pg_class.c.relname.in_(bindparam("filter_names"))
5066 )
5067 return query
5068
5069 @util.memoized_property
5070 def _fk_regex_pattern(self):
5071 # optionally quoted token
5072 qtoken = r'(?:"(?:[^"]|"")+"|[\w]+?)'
5073
5074 # https://www.postgresql.org/docs/current/static/sql-createtable.html
5075 return re.compile(
5076 rf"FOREIGN KEY \((.*?)(?:, PERIOD ({qtoken}))?\) "
5077 rf"REFERENCES (?:({qtoken})\.)?({qtoken})"
5078 rf"\(((?:{qtoken}(?: *, *)?)+)(?:, PERIOD ({qtoken}))?\)"
5079 r"[\s]?(MATCH (FULL|PARTIAL|SIMPLE)+)?"
5080 r"[\s]?(?:ON (UPDATE|DELETE) "
5081 r"(CASCADE|RESTRICT|NO ACTION|"
5082 r"SET (?:NULL|DEFAULT)(?:\s\(.+\))?)+)?"
5083 r"[\s]?(?:ON (UPDATE|DELETE) "
5084 r"(CASCADE|RESTRICT|NO ACTION|"
5085 r"SET (?:NULL|DEFAULT)(?:\s\(.+\))?)+)?"
5086 r"[\s]?(DEFERRABLE|NOT DEFERRABLE)?"
5087 r"[\s]?(INITIALLY (DEFERRED|IMMEDIATE)+)?"
5088 )
5089
5090 def _parse_fk(self, condef):
5091 FK_REGEX = self._fk_regex_pattern
5092 m = re.search(FK_REGEX, condef).groups()
5093
5094 (
5095 constrained_columns,
5096 constrained_period_column,
5097 referred_schema,
5098 referred_table,
5099 referred_columns,
5100 referred_period_column,
5101 _,
5102 match,
5103 upddelkey1,
5104 upddelval1,
5105 upddelkey2,
5106 upddelval2,
5107 deferrable,
5108 _,
5109 initially,
5110 ) = m
5111
5112 # TODO: the PERIOD designation of a temporal foreign key
5113 # (PostgreSQL 18+) is not yet reflected
5114 if constrained_period_column:
5115 constrained_columns += f", {constrained_period_column}"
5116 if referred_period_column:
5117 referred_columns += f", {referred_period_column}"
5118
5119 onupdate = (
5120 upddelval1
5121 if upddelkey1 == "UPDATE"
5122 else upddelval2 if upddelkey2 == "UPDATE" else None
5123 )
5124 ondelete = (
5125 upddelval1
5126 if upddelkey1 == "DELETE"
5127 else upddelval2 if upddelkey2 == "DELETE" else None
5128 )
5129
5130 return (
5131 constrained_columns,
5132 referred_schema,
5133 referred_table,
5134 referred_columns,
5135 match,
5136 onupdate,
5137 ondelete,
5138 deferrable,
5139 initially,
5140 )
5141
5142 def get_multi_foreign_keys(
5143 self,
5144 connection,
5145 schema,
5146 filter_names,
5147 scope,
5148 kind,
5149 postgresql_ignore_search_path=False,
5150 **kw,
5151 ):
5152 preparer = self.identifier_preparer
5153
5154 has_filter_names, params = self._prepare_filter_names(filter_names)
5155 query = self._foreing_key_query(schema, has_filter_names, scope, kind)
5156 result = connection.execute(query, params)
5157
5158 fkeys = defaultdict(list)
5159 default = ReflectionDefaults.foreign_keys
5160 for table_name, conname, condef, conschema, comment in result:
5161 # ensure that each table has an entry, even if it has
5162 # no foreign keys
5163 if conname is None:
5164 fkeys[(schema, table_name)] = default()
5165 continue
5166 table_fks = fkeys[(schema, table_name)]
5167
5168 (
5169 constrained_columns,
5170 referred_schema,
5171 referred_table,
5172 referred_columns,
5173 match,
5174 onupdate,
5175 ondelete,
5176 deferrable,
5177 initially,
5178 ) = self._parse_fk(condef)
5179
5180 if deferrable is not None:
5181 deferrable = True if deferrable == "DEFERRABLE" else False
5182 constrained_columns = [
5183 preparer._unquote_identifier(x)
5184 for x in re.split(r"\s*,\s*", constrained_columns)
5185 ]
5186
5187 if postgresql_ignore_search_path:
5188 # when ignoring search path, we use the actual schema
5189 # provided it isn't the "default" schema
5190 if conschema != self.default_schema_name:
5191 referred_schema = conschema
5192 else:
5193 referred_schema = schema
5194 elif referred_schema:
5195 # referred_schema is the schema that we regexp'ed from
5196 # pg_get_constraintdef(). If the schema is in the search
5197 # path, pg_get_constraintdef() will give us None.
5198 referred_schema = preparer._unquote_identifier(referred_schema)
5199 elif schema is not None and schema == conschema:
5200 # If the actual schema matches the schema of the table
5201 # we're reflecting, then we will use that.
5202 referred_schema = schema
5203
5204 referred_table = preparer._unquote_identifier(referred_table)
5205 referred_columns = [
5206 preparer._unquote_identifier(x)
5207 for x in re.split(r"\s*,\s", referred_columns)
5208 ]
5209 options = {
5210 k: v
5211 for k, v in [
5212 ("onupdate", onupdate),
5213 ("ondelete", ondelete),
5214 ("initially", initially),
5215 ("deferrable", deferrable),
5216 ("match", match),
5217 ]
5218 if v is not None and v != "NO ACTION"
5219 }
5220 fkey_d = {
5221 "name": conname,
5222 "constrained_columns": constrained_columns,
5223 "referred_schema": referred_schema,
5224 "referred_table": referred_table,
5225 "referred_columns": referred_columns,
5226 "options": options,
5227 "comment": comment,
5228 }
5229 table_fks.append(fkey_d)
5230 return fkeys.items()
5231
5232 @util.memoized_property
5233 def _index_query(self):
5234 # NOTE: pg_index is used as from two times to improve performance,
5235 # since extraing all the index information from `idx_sq` to avoid
5236 # the second pg_index use leads to a worse performing query in
5237 # particular when querying for a single table (as of pg 17)
5238 # NOTE: repeating oids clause improve query performance
5239
5240 # subquery to get the columns
5241 idx_sq = (
5242 select(
5243 pg_catalog.pg_index.c.indexrelid,
5244 pg_catalog.pg_index.c.indrelid,
5245 sql.func.unnest(pg_catalog.pg_index.c.indkey).label("attnum"),
5246 sql.func.unnest(pg_catalog.pg_index.c.indclass).label(
5247 "att_opclass"
5248 ),
5249 sql.func.generate_subscripts(
5250 pg_catalog.pg_index.c.indkey, 1
5251 ).label("ord"),
5252 )
5253 .where(
5254 ~pg_catalog.pg_index.c.indisprimary,
5255 pg_catalog.pg_index.c.indrelid.in_(bindparam("oids")),
5256 )
5257 .subquery("idx")
5258 )
5259
5260 attr_sq = (
5261 select(
5262 idx_sq.c.indexrelid,
5263 idx_sq.c.indrelid,
5264 idx_sq.c.ord,
5265 # NOTE: always using pg_get_indexdef is too slow so just
5266 # invoke when the element is an expression
5267 sql.case(
5268 (
5269 idx_sq.c.attnum == 0,
5270 pg_catalog.pg_get_indexdef(
5271 idx_sq.c.indexrelid, idx_sq.c.ord + 1, True
5272 ),
5273 ),
5274 # NOTE: need to cast this since attname is of type "name"
5275 # that's limited to 63 bytes, while pg_get_indexdef
5276 # returns "text" so its output may get cut
5277 else_=pg_catalog.pg_attribute.c.attname.cast(TEXT),
5278 ).label("element"),
5279 (idx_sq.c.attnum == 0).label("is_expr"),
5280 # since it's converted to array cast it to bigint (oid are
5281 # "unsigned four-byte integer") to make it easier for
5282 # dialects to interpret
5283 idx_sq.c.att_opclass.cast(BIGINT),
5284 )
5285 .select_from(idx_sq)
5286 .outerjoin(
5287 # do not remove rows where idx_sq.c.attnum is 0
5288 pg_catalog.pg_attribute,
5289 sql.and_(
5290 pg_catalog.pg_attribute.c.attnum == idx_sq.c.attnum,
5291 pg_catalog.pg_attribute.c.attrelid == idx_sq.c.indrelid,
5292 ),
5293 )
5294 .where(idx_sq.c.indrelid.in_(bindparam("oids")))
5295 .subquery("idx_attr")
5296 )
5297
5298 cols_sq = (
5299 select(
5300 attr_sq.c.indexrelid,
5301 sql.func.min(attr_sq.c.indrelid),
5302 sql.func.array_agg(
5303 aggregate_order_by(attr_sq.c.element, attr_sq.c.ord)
5304 ).label("elements"),
5305 sql.func.array_agg(
5306 aggregate_order_by(attr_sq.c.is_expr, attr_sq.c.ord)
5307 ).label("elements_is_expr"),
5308 sql.func.array_agg(
5309 aggregate_order_by(attr_sq.c.att_opclass, attr_sq.c.ord)
5310 ).label("elements_opclass"),
5311 )
5312 .group_by(attr_sq.c.indexrelid)
5313 .subquery("idx_cols")
5314 )
5315
5316 if self.server_version_info >= (11, 0):
5317 indnkeyatts = pg_catalog.pg_index.c.indnkeyatts
5318 else:
5319 indnkeyatts = pg_catalog.pg_index.c.indnatts.label("indnkeyatts")
5320
5321 if self.server_version_info >= (15,):
5322 nulls_not_distinct = pg_catalog.pg_index.c.indnullsnotdistinct
5323 else:
5324 nulls_not_distinct = sql.false().label("indnullsnotdistinct")
5325
5326 return (
5327 select(
5328 pg_catalog.pg_index.c.indrelid,
5329 pg_catalog.pg_class.c.relname,
5330 pg_catalog.pg_index.c.indisunique,
5331 pg_catalog.pg_index.c.indisvalid,
5332 pg_catalog.pg_constraint.c.conrelid.is_not(None).label(
5333 "has_constraint"
5334 ),
5335 pg_catalog.pg_index.c.indoption,
5336 pg_catalog.pg_class.c.reloptions,
5337 # will get the value using the pg_am cached dict
5338 pg_catalog.pg_class.c.relam,
5339 # NOTE: pg_get_expr is very fast so this case has almost no
5340 # performance impact
5341 sql.case(
5342 (
5343 pg_catalog.pg_index.c.indpred.is_not(None),
5344 pg_catalog.pg_get_expr(
5345 pg_catalog.pg_index.c.indpred,
5346 pg_catalog.pg_index.c.indrelid,
5347 ),
5348 ),
5349 else_=None,
5350 ).label("filter_definition"),
5351 indnkeyatts,
5352 nulls_not_distinct,
5353 cols_sq.c.elements,
5354 cols_sq.c.elements_is_expr,
5355 # will get the value using the pg_opclass cached dict
5356 cols_sq.c.elements_opclass,
5357 )
5358 .select_from(pg_catalog.pg_index)
5359 .where(
5360 pg_catalog.pg_index.c.indrelid.in_(bindparam("oids")),
5361 ~pg_catalog.pg_index.c.indisprimary,
5362 )
5363 .join(
5364 pg_catalog.pg_class,
5365 pg_catalog.pg_index.c.indexrelid == pg_catalog.pg_class.c.oid,
5366 )
5367 .outerjoin(
5368 cols_sq,
5369 pg_catalog.pg_index.c.indexrelid == cols_sq.c.indexrelid,
5370 )
5371 .outerjoin(
5372 pg_catalog.pg_constraint,
5373 sql.and_(
5374 pg_catalog.pg_index.c.indrelid
5375 == pg_catalog.pg_constraint.c.conrelid,
5376 pg_catalog.pg_index.c.indexrelid
5377 == pg_catalog.pg_constraint.c.conindid,
5378 pg_catalog.pg_constraint.c.contype
5379 == sql.any_(_array.array(("p", "u", "x"))),
5380 ),
5381 )
5382 .order_by(
5383 pg_catalog.pg_index.c.indrelid, pg_catalog.pg_class.c.relname
5384 )
5385 )
5386
5387 def get_multi_indexes(
5388 self, connection, schema, filter_names, scope, kind, **kw
5389 ):
5390 table_oids = self._get_table_oids(
5391 connection, schema, filter_names, scope, kind, **kw
5392 )
5393
5394 pg_am_btree_oid = self._load_pg_am_btree_oid(connection)
5395 # lazy load only if needed, the assumption is that most indexes
5396 # will use btree so it may not be needed at all
5397 pg_am_dict = None
5398 pg_opclass_dict = self._load_pg_opclass_notdefault_dict(
5399 connection, **kw
5400 )
5401
5402 indexes = defaultdict(list)
5403 default = ReflectionDefaults.indexes
5404
5405 batches = list(table_oids)
5406
5407 while batches:
5408 batch = batches[0:3000]
5409 batches[0:3000] = []
5410
5411 result = connection.execute(
5412 self._index_query, {"oids": [r[0] for r in batch]}
5413 ).mappings()
5414
5415 result_by_oid = defaultdict(list)
5416 for row_dict in result:
5417 result_by_oid[row_dict["indrelid"]].append(row_dict)
5418
5419 for oid, table_name in batch:
5420 if oid not in result_by_oid:
5421 # ensure that each table has an entry, even if reflection
5422 # is skipped because not supported
5423 indexes[(schema, table_name)] = default()
5424 continue
5425
5426 for row in result_by_oid[oid]:
5427 index_name = row["relname"]
5428
5429 table_indexes = indexes[(schema, table_name)]
5430
5431 all_elements = row["elements"]
5432 all_elements_is_expr = row["elements_is_expr"]
5433 all_elements_opclass = row["elements_opclass"]
5434 indnkeyatts = row["indnkeyatts"]
5435 # "The number of key columns in the index, not counting any
5436 # included columns, which are merely stored and do not
5437 # participate in the index semantics"
5438 if len(all_elements) > indnkeyatts:
5439 # this is a "covering index" which has INCLUDE columns
5440 # as well as regular index columns
5441 inc_cols = all_elements[indnkeyatts:]
5442 idx_elements = all_elements[:indnkeyatts]
5443 idx_elements_is_expr = all_elements_is_expr[
5444 :indnkeyatts
5445 ]
5446 # postgresql does not support expression on included
5447 # columns as of v14: "ERROR: expressions are not
5448 # supported in included columns".
5449 assert all(
5450 not is_expr
5451 for is_expr in all_elements_is_expr[indnkeyatts:]
5452 )
5453 idx_elements_opclass = all_elements_opclass[
5454 :indnkeyatts
5455 ]
5456 else:
5457 idx_elements = all_elements
5458 idx_elements_is_expr = all_elements_is_expr
5459 inc_cols = []
5460 idx_elements_opclass = all_elements_opclass
5461
5462 index = {"name": index_name, "unique": row["indisunique"]}
5463 if any(idx_elements_is_expr):
5464 index["column_names"] = [
5465 None if is_expr else expr
5466 for expr, is_expr in zip(
5467 idx_elements, idx_elements_is_expr
5468 )
5469 ]
5470 index["expressions"] = idx_elements
5471 else:
5472 index["column_names"] = idx_elements
5473
5474 dialect_options = {}
5475
5476 postgresql_ops = {}
5477 for name, opclass in zip(
5478 idx_elements, idx_elements_opclass
5479 ):
5480 # is not in the dict if the opclass is the default one
5481 opclass_name = pg_opclass_dict.get(opclass)
5482 if opclass_name is not None:
5483 postgresql_ops[name] = opclass_name
5484
5485 if postgresql_ops:
5486 dialect_options["postgresql_ops"] = postgresql_ops
5487
5488 sorting = {}
5489 for col_index, col_flags in enumerate(row["indoption"]):
5490 col_sorting = ()
5491 # try to set flags only if they differ from PG
5492 # defaults...
5493 if col_flags & 0x01:
5494 col_sorting += ("desc",)
5495 if not (col_flags & 0x02):
5496 col_sorting += ("nulls_last",)
5497 else:
5498 if col_flags & 0x02:
5499 col_sorting += ("nulls_first",)
5500 if col_sorting:
5501 sorting[idx_elements[col_index]] = col_sorting
5502 if sorting:
5503 index["column_sorting"] = sorting
5504 if row["has_constraint"]:
5505 index["duplicates_constraint"] = index_name
5506
5507 if row["reloptions"]:
5508 dialect_options["postgresql_with"] = dict(
5509 [
5510 option.split("=", 1)
5511 for option in row["reloptions"]
5512 ]
5513 )
5514 # it *might* be nice to include that this is 'btree' in the
5515 # reflection info. But we don't want an Index object
5516 # to have a ``postgresql_using`` in it that is just the
5517 # default, so for the moment leaving this out.
5518 if row["relam"] != pg_am_btree_oid:
5519 if pg_am_dict is None:
5520 pg_am_dict = self._load_pg_am_dict(
5521 connection, **kw
5522 )
5523 dialect_options["postgresql_using"] = pg_am_dict[
5524 row["relam"]
5525 ]
5526 if row["filter_definition"]:
5527 dialect_options["postgresql_where"] = row[
5528 "filter_definition"
5529 ]
5530 if self.server_version_info >= (11,):
5531 dialect_options["postgresql_include"] = inc_cols
5532 if row["indnullsnotdistinct"]:
5533 # the default is False, so ignore it.
5534 dialect_options["postgresql_nulls_not_distinct"] = row[
5535 "indnullsnotdistinct"
5536 ]
5537
5538 if not row["indisvalid"]:
5539 dialect_options["postgresql_invalid"] = True
5540
5541 if dialect_options:
5542 index["dialect_options"] = dialect_options
5543
5544 table_indexes.append(index)
5545 return indexes.items()
5546
5547 def get_multi_unique_constraints(
5548 self,
5549 connection,
5550 schema,
5551 filter_names,
5552 scope,
5553 kind,
5554 **kw,
5555 ):
5556 result = self._reflect_constraint(
5557 connection, "u", schema, filter_names, scope, kind, **kw
5558 )
5559
5560 # each table can have multiple unique constraints
5561 uniques = defaultdict(list)
5562 default = ReflectionDefaults.unique_constraints
5563 for table_name, cols, con_name, comment, options in result:
5564 # ensure a list is created for each table. leave it empty if
5565 # the table has no unique constraint
5566 if con_name is None:
5567 uniques[(schema, table_name)] = default()
5568 continue
5569
5570 uc_dict = {
5571 "column_names": cols,
5572 "name": con_name,
5573 "comment": comment,
5574 }
5575 if options:
5576 uc_dict["dialect_options"] = options
5577
5578 uniques[(schema, table_name)].append(uc_dict)
5579 return uniques.items()
5580
5581 @lru_cache()
5582 def _comment_query(self, schema, has_filter_names, scope, kind):
5583 relkinds = self._kind_to_relkinds(kind)
5584 query = (
5585 select(
5586 pg_catalog.pg_class.c.relname,
5587 pg_catalog.pg_description.c.description,
5588 )
5589 .select_from(pg_catalog.pg_class)
5590 .outerjoin(
5591 pg_catalog.pg_description,
5592 sql.and_(
5593 pg_catalog.pg_class.c.oid
5594 == pg_catalog.pg_description.c.objoid,
5595 pg_catalog.pg_description.c.objsubid == 0,
5596 pg_catalog.pg_description.c.classoid
5597 == sql.func.cast("pg_catalog.pg_class", REGCLASS),
5598 ),
5599 )
5600 .where(self._pg_class_relkind_condition(relkinds))
5601 )
5602 query = self._pg_class_filter_scope_schema(query, schema, scope)
5603 if has_filter_names:
5604 query = query.where(
5605 pg_catalog.pg_class.c.relname.in_(bindparam("filter_names"))
5606 )
5607 return query
5608
5609 def get_multi_table_comment(
5610 self, connection, schema, filter_names, scope, kind, **kw
5611 ):
5612 has_filter_names, params = self._prepare_filter_names(filter_names)
5613 query = self._comment_query(schema, has_filter_names, scope, kind)
5614 result = connection.execute(query, params)
5615
5616 default = ReflectionDefaults.table_comment
5617 return (
5618 (
5619 (schema, table),
5620 {"text": comment} if comment is not None else default(),
5621 )
5622 for table, comment in result
5623 )
5624
5625 @lru_cache()
5626 def _check_constraint_query(self, schema, has_filter_names, scope, kind):
5627 relkinds = self._kind_to_relkinds(kind)
5628 query = (
5629 select(
5630 pg_catalog.pg_class.c.relname,
5631 pg_catalog.pg_constraint.c.conname,
5632 # NOTE: avoid calling pg_get_constraintdef when not needed
5633 # to speed up the query
5634 sql.case(
5635 (
5636 pg_catalog.pg_constraint.c.oid.is_not(None),
5637 pg_catalog.pg_get_constraintdef(
5638 pg_catalog.pg_constraint.c.oid, True
5639 ),
5640 ),
5641 else_=None,
5642 ),
5643 pg_catalog.pg_description.c.description,
5644 )
5645 .select_from(pg_catalog.pg_class)
5646 .outerjoin(
5647 pg_catalog.pg_constraint,
5648 sql.and_(
5649 pg_catalog.pg_class.c.oid
5650 == pg_catalog.pg_constraint.c.conrelid,
5651 pg_catalog.pg_constraint.c.contype == "c",
5652 ),
5653 )
5654 .outerjoin(
5655 pg_catalog.pg_description,
5656 sql.and_(
5657 pg_catalog.pg_description.c.objoid
5658 == pg_catalog.pg_constraint.c.oid,
5659 pg_catalog.pg_description.c.classoid
5660 == sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
5661 ),
5662 )
5663 .order_by(
5664 pg_catalog.pg_class.c.relname,
5665 pg_catalog.pg_constraint.c.conname,
5666 )
5667 .where(self._pg_class_relkind_condition(relkinds))
5668 )
5669 query = self._pg_class_filter_scope_schema(query, schema, scope)
5670 if has_filter_names:
5671 query = query.where(
5672 pg_catalog.pg_class.c.relname.in_(bindparam("filter_names"))
5673 )
5674 return query
5675
5676 def get_multi_check_constraints(
5677 self, connection, schema, filter_names, scope, kind, **kw
5678 ):
5679 has_filter_names, params = self._prepare_filter_names(filter_names)
5680 query = self._check_constraint_query(
5681 schema, has_filter_names, scope, kind
5682 )
5683 result = connection.execute(query, params)
5684
5685 check_constraints = defaultdict(list)
5686 default = ReflectionDefaults.check_constraints
5687 for table_name, check_name, src, comment in result:
5688 # only two cases for check_name and src: both null or both defined
5689 if check_name is None and src is None:
5690 check_constraints[(schema, table_name)] = default()
5691 continue
5692 # samples:
5693 # "CHECK (((a > 1) AND (a < 5)))"
5694 # "CHECK (((a = 1) OR ((a > 2) AND (a < 5))))"
5695 # "CHECK (((a > 1) AND (a < 5))) NOT VALID"
5696 # "CHECK (some_boolean_function(a))"
5697 # "CHECK (((a\n < 1)\n OR\n (a\n >= 5))\n)"
5698 # "CHECK (a NOT NULL) NO INHERIT"
5699 # "CHECK (a NOT NULL) NO INHERIT NOT VALID"
5700
5701 m = re.match(
5702 r"^CHECK *\((.+)\)( NO INHERIT)?( NOT VALID)?$",
5703 src,
5704 flags=re.DOTALL,
5705 )
5706 if not m:
5707 util.warn("Could not parse CHECK constraint text: %r" % src)
5708 sqltext = ""
5709 else:
5710 sqltext = util.strip_outer_parens(m.group(1))
5711 entry = {
5712 "name": check_name,
5713 "sqltext": sqltext,
5714 "comment": comment,
5715 }
5716 if m:
5717 do = {}
5718 if " NOT VALID" in m.groups():
5719 do["not_valid"] = True
5720 if " NO INHERIT" in m.groups():
5721 do["no_inherit"] = True
5722 if do:
5723 entry["dialect_options"] = do
5724
5725 check_constraints[(schema, table_name)].append(entry)
5726 return check_constraints.items()
5727
5728 def _pg_type_filter_schema(self, query, schema):
5729 if schema is None:
5730 query = query.where(
5731 pg_catalog.pg_type_is_visible(pg_catalog.pg_type.c.oid),
5732 # ignore pg_catalog schema
5733 pg_catalog.pg_namespace.c.nspname != "pg_catalog",
5734 )
5735 elif schema != "*":
5736 query = query.where(pg_catalog.pg_namespace.c.nspname == schema)
5737 return query
5738
5739 @lru_cache()
5740 def _enum_query(self, schema):
5741 lbl_agg_sq = (
5742 select(
5743 pg_catalog.pg_enum.c.enumtypid,
5744 sql.func.array_agg(
5745 aggregate_order_by(
5746 # NOTE: cast since some postgresql derivatives may
5747 # not support array_agg on the name type
5748 pg_catalog.pg_enum.c.enumlabel.cast(TEXT),
5749 pg_catalog.pg_enum.c.enumsortorder,
5750 )
5751 ).label("labels"),
5752 )
5753 .group_by(pg_catalog.pg_enum.c.enumtypid)
5754 .subquery("lbl_agg")
5755 )
5756
5757 query = (
5758 select(
5759 pg_catalog.pg_type.c.typname.label("name"),
5760 pg_catalog.pg_type_is_visible(pg_catalog.pg_type.c.oid).label(
5761 "visible"
5762 ),
5763 pg_catalog.pg_namespace.c.nspname.label("schema"),
5764 lbl_agg_sq.c.labels.label("labels"),
5765 )
5766 .join(
5767 pg_catalog.pg_namespace,
5768 pg_catalog.pg_namespace.c.oid
5769 == pg_catalog.pg_type.c.typnamespace,
5770 )
5771 .outerjoin(
5772 lbl_agg_sq, pg_catalog.pg_type.c.oid == lbl_agg_sq.c.enumtypid
5773 )
5774 .where(pg_catalog.pg_type.c.typtype == "e")
5775 .order_by(
5776 pg_catalog.pg_namespace.c.nspname, pg_catalog.pg_type.c.typname
5777 )
5778 )
5779
5780 return self._pg_type_filter_schema(query, schema)
5781
5782 @reflection.cache
5783 def _load_enums(self, connection, schema=None, **kw):
5784 if not self.supports_native_enum:
5785 return []
5786
5787 result = connection.execute(self._enum_query(schema))
5788
5789 enums = []
5790 for name, visible, schema, labels in result:
5791 enums.append(
5792 {
5793 "name": name,
5794 "schema": schema,
5795 "visible": visible,
5796 "labels": [] if labels is None else labels,
5797 }
5798 )
5799 return enums
5800
5801 @lru_cache()
5802 def _domain_query(self, schema):
5803 con_sq = (
5804 select(
5805 pg_catalog.pg_constraint.c.contypid,
5806 sql.func.array_agg(
5807 pg_catalog.pg_get_constraintdef(
5808 pg_catalog.pg_constraint.c.oid, True
5809 )
5810 ).label("condefs"),
5811 sql.func.array_agg(
5812 # NOTE: cast since some postgresql derivatives may
5813 # not support array_agg on the name type
5814 pg_catalog.pg_constraint.c.conname.cast(TEXT)
5815 ).label("connames"),
5816 )
5817 # The domain this constraint is on; zero if not a domain constraint
5818 .where(pg_catalog.pg_constraint.c.contypid != 0)
5819 .group_by(pg_catalog.pg_constraint.c.contypid)
5820 .subquery("domain_constraints")
5821 )
5822
5823 collation_namespace = pg_catalog.pg_namespace.alias(
5824 "collation_namespace"
5825 )
5826
5827 query = (
5828 select(
5829 pg_catalog.pg_type.c.typname.label("name"),
5830 pg_catalog.format_type(
5831 pg_catalog.pg_type.c.typbasetype,
5832 pg_catalog.pg_type.c.typtypmod,
5833 ).label("attype"),
5834 (~pg_catalog.pg_type.c.typnotnull).label("nullable"),
5835 pg_catalog.pg_type.c.typdefault.label("default"),
5836 pg_catalog.pg_type_is_visible(pg_catalog.pg_type.c.oid).label(
5837 "visible"
5838 ),
5839 pg_catalog.pg_namespace.c.nspname.label("schema"),
5840 con_sq.c.condefs,
5841 con_sq.c.connames,
5842 pg_catalog.pg_collation.c.collname,
5843 sql.case(
5844 (
5845 pg_catalog.pg_collation.c.oid.is_(None),
5846 sql.null(),
5847 ),
5848 (
5849 pg_catalog.pg_collation_is_visible(
5850 pg_catalog.pg_collation.c.oid
5851 ),
5852 sql.null(),
5853 ),
5854 else_=collation_namespace.c.nspname,
5855 ).label("collation_schema"),
5856 )
5857 .join(
5858 pg_catalog.pg_namespace,
5859 pg_catalog.pg_namespace.c.oid
5860 == pg_catalog.pg_type.c.typnamespace,
5861 )
5862 .outerjoin(
5863 pg_catalog.pg_collation,
5864 pg_catalog.pg_type.c.typcollation
5865 == pg_catalog.pg_collation.c.oid,
5866 )
5867 .outerjoin(
5868 collation_namespace,
5869 collation_namespace.c.oid
5870 == pg_catalog.pg_collation.c.collnamespace,
5871 )
5872 .outerjoin(
5873 con_sq,
5874 pg_catalog.pg_type.c.oid == con_sq.c.contypid,
5875 )
5876 .where(pg_catalog.pg_type.c.typtype == "d")
5877 .order_by(
5878 pg_catalog.pg_namespace.c.nspname, pg_catalog.pg_type.c.typname
5879 )
5880 )
5881 return self._pg_type_filter_schema(query, schema)
5882
5883 @reflection.cache
5884 def _load_domains(self, connection, schema=None, **kw):
5885 result = connection.execute(self._domain_query(schema))
5886
5887 domains: List[ReflectedDomain] = []
5888 for domain in result.mappings():
5889 # strip (30) from character varying(30)
5890 attype = re.search(r"([^\(]+)", domain["attype"]).group(1)
5891 constraints: List[ReflectedDomainConstraint] = []
5892 if domain["connames"]:
5893 # When a domain has multiple CHECK constraints, they will
5894 # be tested in alphabetical order by name.
5895 sorted_constraints = sorted(
5896 zip(domain["connames"], domain["condefs"]),
5897 key=lambda t: t[0],
5898 )
5899 for name, def_ in sorted_constraints:
5900 # constraint is in the form "CHECK (expression)"
5901 # or "NOT NULL". Ignore the "NOT NULL" and
5902 # remove "CHECK (" and the tailing ")".
5903 if def_.casefold().startswith("check"):
5904 check = def_[7:-1]
5905 constraints.append({"name": name, "check": check})
5906 domain_rec: ReflectedDomain = {
5907 "name": domain["name"],
5908 "schema": domain["schema"],
5909 "visible": domain["visible"],
5910 "type": attype,
5911 "nullable": domain["nullable"],
5912 "default": domain["default"],
5913 "constraints": constraints,
5914 "collation": domain["collname"],
5915 "collation_schema": domain["collation_schema"],
5916 }
5917 domains.append(domain_rec)
5918
5919 return domains
5920
5921 @util.memoized_property
5922 def _pg_am_query(self):
5923 return sql.select(pg_catalog.pg_am.c.oid, pg_catalog.pg_am.c.amname)
5924
5925 @reflection.cache
5926 def _load_pg_am_dict(self, connection, **kw) -> dict[int, str]:
5927 rows = connection.execute(self._pg_am_query)
5928 return dict(rows.all())
5929
5930 def _load_pg_am_btree_oid(self, connection):
5931 # this oid is assumed to be stable
5932 if self._pg_am_btree_oid == -1:
5933 self._pg_am_btree_oid = connection.scalar(
5934 self._pg_am_query.where(pg_catalog.pg_am.c.amname == "btree")
5935 )
5936 return self._pg_am_btree_oid
5937
5938 @util.memoized_property
5939 def _pg_opclass_notdefault_query(self):
5940 return sql.select(
5941 pg_catalog.pg_opclass.c.oid, pg_catalog.pg_opclass.c.opcname
5942 ).where(~pg_catalog.pg_opclass.c.opcdefault)
5943
5944 @reflection.cache
5945 def _load_pg_opclass_notdefault_dict(
5946 self, connection, **kw
5947 ) -> dict[int, str]:
5948 rows = connection.execute(self._pg_opclass_notdefault_query)
5949 return dict(rows.all())
5950
5951 def _set_backslash_escapes(self, connection):
5952 # this method is provided as an override hook for descendant
5953 # dialects (e.g. Redshift), so removing it may break them
5954 std_string = connection.exec_driver_sql(
5955 "show standard_conforming_strings"
5956 ).scalar()
5957 self._backslash_escapes = std_string == "off"
5958
5959
5960class _NamedTypeLoader:
5961 """Helper class used for deferred loading of named types (enums, domains)
5962 only when needed.
5963 """
5964
5965 def __init__(
5966 self, dialect: PGDialect, connection, kw: Dict[str, Any]
5967 ) -> None:
5968 self.dialect = dialect
5969 self.connection = connection
5970 self.kw = kw
5971
5972 @util.memoized_property
5973 def enums(self) -> Dict[Tuple[str] | Tuple[str, str], ReflectedEnum]:
5974 # dictionary with (name, ) if default search path or (schema, name)
5975 # as keys
5976 enums = dict(
5977 (
5978 ((rec["name"],), rec)
5979 if rec["visible"]
5980 else ((rec["schema"], rec["name"]), rec)
5981 )
5982 for rec in self.dialect._load_enums(
5983 self.connection,
5984 schema="*",
5985 info_cache=self.kw.get("info_cache"),
5986 )
5987 )
5988 return enums
5989
5990 @util.memoized_property
5991 def domains(self) -> Dict[Tuple[str] | Tuple[str, str], ReflectedDomain]:
5992 # dictionary with (name, ) if default search path or (schema, name)
5993 # as keys
5994 domains = {
5995 ((d["schema"], d["name"]) if not d["visible"] else (d["name"],)): d
5996 for d in self.dialect._load_domains(
5997 self.connection,
5998 schema="*",
5999 info_cache=self.kw.get("info_cache"),
6000 )
6001 }
6002 return domains