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