Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/base.py: 28%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1552 statements  

1# dialects/postgresql/base.py 

2# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors 

3# <see AUTHORS file> 

4# 

5# This module is part of SQLAlchemy and is released under 

6# the MIT License: https://www.opensource.org/licenses/mit-license.php 

7# mypy: ignore-errors 

8 

9r""" 

10.. dialect:: postgresql 

11 :name: PostgreSQL 

12 :normal_support: 9.6+ 

13 :best_effort: 9+ 

14 

15.. _postgresql_sequences: 

16 

17Sequences/SERIAL/IDENTITY 

18------------------------- 

19 

20PostgreSQL supports sequences, and SQLAlchemy uses these as the default means 

21of creating new primary key values for integer-based primary key columns. When 

22creating tables, SQLAlchemy will issue the ``SERIAL`` datatype for 

23integer-based primary key columns, which generates a sequence and server side 

24default corresponding to the column. 

25 

26To specify a specific named sequence to be used for primary key generation, 

27use the :func:`~sqlalchemy.schema.Sequence` construct:: 

28 

29 Table( 

30 "sometable", 

31 metadata, 

32 Column( 

33 "id", Integer, Sequence("some_id_seq", start=1), primary_key=True 

34 ), 

35 ) 

36 

37When SQLAlchemy issues a single INSERT statement, to fulfill the contract of 

38having the "last insert identifier" available, a RETURNING clause is added to 

39the INSERT statement which specifies the primary key columns should be 

40returned after the statement completes. The RETURNING functionality only takes 

41place if PostgreSQL 8.2 or later is in use. As a fallback approach, the 

42sequence, whether specified explicitly or implicitly via ``SERIAL``, is 

43executed independently beforehand, the returned value to be used in the 

44subsequent insert. Note that when an 

45:func:`~sqlalchemy.sql.expression.insert()` construct is executed using 

46"executemany" semantics, the "last inserted identifier" functionality does not 

47apply; no RETURNING clause is emitted nor is the sequence pre-executed in this 

48case. 

49 

50 

51PostgreSQL 10 and above IDENTITY columns 

52^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

53 

54PostgreSQL 10 and above have a new IDENTITY feature that supersedes the use 

55of SERIAL. The :class:`_schema.Identity` construct in a 

56:class:`_schema.Column` can be used to control its behavior:: 

57 

58 from sqlalchemy import Table, Column, MetaData, Integer, Computed 

59 

60 metadata = MetaData() 

61 

62 data = Table( 

63 "data", 

64 metadata, 

65 Column( 

66 "id", Integer, Identity(start=42, cycle=True), primary_key=True 

67 ), 

68 Column("data", String), 

69 ) 

70 

71The CREATE TABLE for the above :class:`_schema.Table` object would be: 

72 

73.. sourcecode:: sql 

74 

75 CREATE TABLE data ( 

76 id INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 42 CYCLE), 

77 data VARCHAR, 

78 PRIMARY KEY (id) 

79 ) 

80 

81.. versionchanged:: 1.4 Added :class:`_schema.Identity` construct 

82 in a :class:`_schema.Column` to specify the option of an autoincrementing 

83 column. 

84 

85.. note:: 

86 

87 Previous versions of SQLAlchemy did not have built-in support for rendering 

88 of IDENTITY, and could use the following compilation hook to replace 

89 occurrences of SERIAL with IDENTITY:: 

90 

91 from sqlalchemy.schema import CreateColumn 

92 from sqlalchemy.ext.compiler import compiles 

93 

94 

95 @compiles(CreateColumn, "postgresql") 

96 def use_identity(element, compiler, **kw): 

97 text = compiler.visit_create_column(element, **kw) 

98 text = text.replace("SERIAL", "INT GENERATED BY DEFAULT AS IDENTITY") 

99 return text 

100 

101 Using the above, a table such as:: 

102 

103 t = Table( 

104 "t", m, Column("id", Integer, primary_key=True), Column("data", String) 

105 ) 

106 

107 Will generate on the backing database as: 

108 

109 .. sourcecode:: sql 

110 

111 CREATE TABLE t ( 

112 id INT GENERATED BY DEFAULT AS IDENTITY, 

113 data VARCHAR, 

114 PRIMARY KEY (id) 

115 ) 

116 

117.. _postgresql_monotonic_functions: 

118 

119PostgreSQL 18 and above UUID with uuidv7 as a server default 

120^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

121 

122PostgreSQL 18's ``uuidv7`` SQL function is available as any other 

123SQL function using the :data:`_sql.func` namespace:: 

124 

125 >>> from sqlalchemy import select, func 

126 >>> print(select(func.uuidv7())) 

127 SELECT uuidv7() AS uuidv7_1 

128 

129When using ``func.uuidv7()`` as a default on a :class:`.Column` using either 

130Core or ORM, an extra directive ``monotonic=True`` may be passed which 

131indicates this function produces monotonically increasing values; this in turn 

132allows Core and ORM to use a more efficient batched form of INSERT for large 

133insert operations:: 

134 

135 import uuid 

136 

137 

138 class MyClass(Base): 

139 __tablename__ = "my_table" 

140 

141 id: Mapped[uuid.UUID] = mapped_column( 

142 server_default=func.uuidv7(monotonic=True) 

143 ) 

144 

145With the above mapping, the ORM will be able to efficiently batch rows when 

146running bulk insert operations using the :ref:`engine_insertmanyvalues` 

147feature. 

148 

149.. versionadded:: 2.1 

150 Added ``monotonic=True`` to allow functions like PostgreSQL's 

151 ``uuidv7()`` to work with batched "insertmanyvalues" 

152 

153.. seealso:: 

154 

155 :ref:`engine_insertmanyvalues_monotonic_functions` 

156 

157 

158.. _postgresql_ss_cursors: 

159 

160Server Side Cursors 

161------------------- 

162 

163Server-side cursor support is available for the psycopg2, asyncpg 

164dialects and may also be available in others. 

165 

166Server side cursors are enabled on a per-statement basis by using the 

167:paramref:`.Connection.execution_options.stream_results` connection execution 

168option:: 

169 

170 with engine.connect() as conn: 

171 result = conn.execution_options(stream_results=True).execute( 

172 text("select * from table") 

173 ) 

174 

175Note that some kinds of SQL statements may not be supported with 

176server side cursors; generally, only SQL statements that return rows should be 

177used with this option. 

178 

179.. deprecated:: 1.4 The dialect-level server_side_cursors flag is deprecated 

180 and will be removed in a future release. Please use the 

181 :paramref:`_engine.Connection.stream_results` execution option for 

182 unbuffered cursor support. 

183 

184.. seealso:: 

185 

186 :ref:`engine_stream_results` 

187 

188.. _postgresql_isolation_level: 

189 

190Transaction Isolation Level 

191--------------------------- 

192 

193Most SQLAlchemy dialects support setting of transaction isolation level 

194using the :paramref:`_sa.create_engine.isolation_level` parameter 

195at the :func:`_sa.create_engine` level, and at the :class:`_engine.Connection` 

196level via the :paramref:`.Connection.execution_options.isolation_level` 

197parameter. 

198 

199For PostgreSQL dialects, this feature works either by making use of the 

200DBAPI-specific features, such as psycopg2's isolation level flags which will 

201embed the isolation level setting inline with the ``"BEGIN"`` statement, or for 

202DBAPIs with no direct support by emitting ``SET SESSION CHARACTERISTICS AS 

203TRANSACTION ISOLATION LEVEL <level>`` ahead of the ``"BEGIN"`` statement 

204emitted by the DBAPI. For the special AUTOCOMMIT isolation level, 

205DBAPI-specific techniques are used which is typically an ``.autocommit`` 

206flag on the DBAPI connection object. 

207 

208To set isolation level using :func:`_sa.create_engine`:: 

209 

210 engine = create_engine( 

211 "postgresql+pg8000://scott:tiger@localhost/test", 

212 isolation_level="REPEATABLE READ", 

213 ) 

214 

215To set using per-connection execution options:: 

216 

217 with engine.connect() as conn: 

218 conn = conn.execution_options(isolation_level="REPEATABLE READ") 

219 with conn.begin(): 

220 ... # work with transaction 

221 

222There are also more options for isolation level configurations, such as 

223"sub-engine" objects linked to a main :class:`_engine.Engine` which each apply 

224different isolation level settings. See the discussion at 

225:ref:`dbapi_autocommit` for background. 

226 

227Valid values for ``isolation_level`` on most PostgreSQL dialects include: 

228 

229* ``READ COMMITTED`` 

230* ``READ UNCOMMITTED`` 

231* ``REPEATABLE READ`` 

232* ``SERIALIZABLE`` 

233* ``AUTOCOMMIT`` 

234 

235.. seealso:: 

236 

237 :ref:`dbapi_autocommit` 

238 

239 :ref:`postgresql_readonly_deferrable` 

240 

241 :ref:`psycopg2_isolation_level` 

242 

243 :ref:`pg8000_isolation_level` 

244 

245.. _postgresql_readonly_deferrable: 

246 

247Setting READ ONLY / DEFERRABLE 

248------------------------------ 

249 

250Most PostgreSQL dialects support setting the "READ ONLY" and "DEFERRABLE" 

251characteristics of the transaction, which is in addition to the isolation level 

252setting. These two attributes can be established either in conjunction with or 

253independently of the isolation level by passing the ``postgresql_readonly`` and 

254``postgresql_deferrable`` flags with 

255:meth:`_engine.Connection.execution_options`. The example below illustrates 

256passing the ``"SERIALIZABLE"`` isolation level at the same time as setting 

257"READ ONLY" and "DEFERRABLE":: 

258 

259 with engine.connect() as conn: 

260 conn = conn.execution_options( 

261 isolation_level="SERIALIZABLE", 

262 postgresql_readonly=True, 

263 postgresql_deferrable=True, 

264 ) 

265 with conn.begin(): 

266 ... # work with transaction 

267 

268Note that some DBAPIs such as asyncpg only support "readonly" with 

269SERIALIZABLE isolation. 

270 

271.. versionadded:: 1.4 

272 Added support for the ``postgresql_readonly`` 

273 and ``postgresql_deferrable`` execution options. 

274 

275.. _postgresql_reset_on_return: 

276 

277Temporary Table / Resource Reset for Connection Pooling 

278------------------------------------------------------- 

279 

280The :class:`.QueuePool` connection pool implementation used 

281by the SQLAlchemy :class:`.Engine` object includes 

282:ref:`reset on return <pool_reset_on_return>` behavior that will invoke 

283the DBAPI ``.rollback()`` method when connections are returned to the pool. 

284While this rollback will clear out the immediate state used by the previous 

285transaction, it does not cover a wider range of session-level state, including 

286temporary tables as well as other server state such as prepared statement 

287handles and statement caches. The PostgreSQL database includes a variety 

288of commands which may be used to reset this state, including 

289``DISCARD``, ``RESET``, ``DEALLOCATE``, and ``UNLISTEN``. 

290 

291 

292To install 

293one or more of these commands as the means of performing reset-on-return, 

294the :meth:`.PoolEvents.reset` event hook may be used, as demonstrated 

295in the example below. The implementation 

296will end transactions in progress as well as discard temporary tables 

297using the ``CLOSE``, ``RESET`` and ``DISCARD`` commands; see the PostgreSQL 

298documentation for background on what each of these statements do. 

299 

300The :paramref:`_sa.create_engine.pool_reset_on_return` parameter 

301is set to ``None`` so that the custom scheme can replace the default behavior 

302completely. The custom hook implementation calls ``.rollback()`` in any case, 

303as it's usually important that the DBAPI's own tracking of commit/rollback 

304will remain consistent with the state of the transaction:: 

305 

306 

307 from sqlalchemy import create_engine 

308 from sqlalchemy import event 

309 

310 postgresql_engine = create_engine( 

311 "postgresql+psycopg2://scott:tiger@hostname/dbname", 

312 # disable default reset-on-return scheme 

313 pool_reset_on_return=None, 

314 ) 

315 

316 

317 @event.listens_for(postgresql_engine, "reset") 

318 def _reset_postgresql(dbapi_connection, connection_record, reset_state): 

319 if not reset_state.terminate_only: 

320 dbapi_connection.execute("CLOSE ALL") 

321 dbapi_connection.execute("RESET ALL") 

322 dbapi_connection.execute("DISCARD TEMP") 

323 

324 # so that the DBAPI itself knows that the connection has been 

325 # reset 

326 dbapi_connection.rollback() 

327 

328.. versionchanged:: 2.0.0b3 Added additional state arguments to 

329 the :meth:`.PoolEvents.reset` event and additionally ensured the event 

330 is invoked for all "reset" occurrences, so that it's appropriate 

331 as a place for custom "reset" handlers. Previous schemes which 

332 use the :meth:`.PoolEvents.checkin` handler remain usable as well. 

333 

334.. seealso:: 

335 

336 :ref:`pool_reset_on_return` - in the :ref:`pooling_toplevel` documentation 

337 

338.. _postgresql_alternate_search_path: 

339 

340Setting Alternate Search Paths on Connect 

341------------------------------------------ 

342 

343The PostgreSQL ``search_path`` variable refers to the list of schema names 

344that will be implicitly referenced when a particular table or other 

345object is referenced in a SQL statement. As detailed in the next section 

346:ref:`postgresql_schema_reflection`, SQLAlchemy is generally organized around 

347the concept of keeping this variable at its default value of ``public``, 

348however, in order to have it set to any arbitrary name or names when connections 

349are used automatically, the "SET SESSION search_path" command may be invoked 

350for all connections in a pool using the following event handler, as discussed 

351at :ref:`schema_set_default_connections`:: 

352 

353 from sqlalchemy import event 

354 from sqlalchemy import create_engine 

355 

356 engine = create_engine("postgresql+psycopg2://scott:tiger@host/dbname") 

357 

358 

359 @event.listens_for(engine, "connect", insert=True) 

360 def set_search_path(dbapi_connection, connection_record): 

361 existing_autocommit = dbapi_connection.autocommit 

362 dbapi_connection.autocommit = True 

363 cursor = dbapi_connection.cursor() 

364 cursor.execute("SET SESSION search_path='%s'" % schema_name) 

365 cursor.close() 

366 dbapi_connection.autocommit = existing_autocommit 

367 

368The reason the recipe is complicated by use of the ``.autocommit`` DBAPI 

369attribute is so that when the ``SET SESSION search_path`` directive is invoked, 

370it is invoked outside of the scope of any transaction and therefore will not 

371be reverted when the DBAPI connection has a rollback. 

372 

373.. seealso:: 

374 

375 :ref:`schema_set_default_connections` - in the :ref:`metadata_toplevel` documentation 

376 

377.. _postgresql_schema_reflection: 

378 

379Remote-Schema Table Introspection and PostgreSQL search_path 

380------------------------------------------------------------ 

381 

382.. admonition:: Section Best Practices Summarized 

383 

384 keep the ``search_path`` variable set to its default of ``public``, without 

385 any other schema names. Ensure the username used to connect **does not** 

386 match remote schemas, or ensure the ``"$user"`` token is **removed** from 

387 ``search_path``. For other schema names, name these explicitly 

388 within :class:`_schema.Table` definitions. Alternatively, the 

389 ``postgresql_ignore_search_path`` option will cause all reflected 

390 :class:`_schema.Table` objects to have a :attr:`_schema.Table.schema` 

391 attribute set up. 

392 

393The PostgreSQL dialect can reflect tables from any schema, as outlined in 

394:ref:`metadata_reflection_schemas`. 

395 

396In all cases, the first thing SQLAlchemy does when reflecting tables is 

397to **determine the default schema for the current database connection**. 

398It does this using the PostgreSQL ``current_schema()`` 

399function, illustated below using a PostgreSQL client session (i.e. using 

400the ``psql`` tool): 

401 

402.. sourcecode:: sql 

403 

404 test=> select current_schema(); 

405 current_schema 

406 ---------------- 

407 public 

408 (1 row) 

409 

410Above we see that on a plain install of PostgreSQL, the default schema name 

411is the name ``public``. 

412 

413However, if your database username **matches the name of a schema**, PostgreSQL's 

414default is to then **use that name as the default schema**. Below, we log in 

415using the username ``scott``. When we create a schema named ``scott``, **it 

416implicitly changes the default schema**: 

417 

418.. sourcecode:: sql 

419 

420 test=> select current_schema(); 

421 current_schema 

422 ---------------- 

423 public 

424 (1 row) 

425 

426 test=> create schema scott; 

427 CREATE SCHEMA 

428 test=> select current_schema(); 

429 current_schema 

430 ---------------- 

431 scott 

432 (1 row) 

433 

434The behavior of ``current_schema()`` is derived from the 

435`PostgreSQL search path 

436<https://www.postgresql.org/docs/current/static/ddl-schemas.html#DDL-SCHEMAS-PATH>`_ 

437variable ``search_path``, which in modern PostgreSQL versions defaults to this: 

438 

439.. sourcecode:: sql 

440 

441 test=> show search_path; 

442 search_path 

443 ----------------- 

444 "$user", public 

445 (1 row) 

446 

447Where above, the ``"$user"`` variable will inject the current username as the 

448default schema, if one exists. Otherwise, ``public`` is used. 

449 

450When a :class:`_schema.Table` object is reflected, if it is present in the 

451schema indicated by the ``current_schema()`` function, **the schema name assigned 

452to the ".schema" attribute of the Table is the Python "None" value**. Otherwise, the 

453".schema" attribute will be assigned the string name of that schema. 

454 

455With regards to tables which these :class:`_schema.Table` 

456objects refer to via foreign key constraint, a decision must be made as to how 

457the ``.schema`` is represented in those remote tables, in the case where that 

458remote schema name is also a member of the current ``search_path``. 

459 

460By default, the PostgreSQL dialect mimics the behavior encouraged by 

461PostgreSQL's own ``pg_get_constraintdef()`` builtin procedure. This function 

462returns a sample definition for a particular foreign key constraint, 

463omitting the referenced schema name from that definition when the name is 

464also in the PostgreSQL schema search path. The interaction below 

465illustrates this behavior: 

466 

467.. sourcecode:: sql 

468 

469 test=> CREATE TABLE test_schema.referred(id INTEGER PRIMARY KEY); 

470 CREATE TABLE 

471 test=> CREATE TABLE referring( 

472 test(> id INTEGER PRIMARY KEY, 

473 test(> referred_id INTEGER REFERENCES test_schema.referred(id)); 

474 CREATE TABLE 

475 test=> SET search_path TO public, test_schema; 

476 test=> SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM 

477 test-> pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n 

478 test-> ON n.oid = c.relnamespace 

479 test-> JOIN pg_catalog.pg_constraint r ON c.oid = r.conrelid 

480 test-> WHERE c.relname='referring' AND r.contype = 'f' 

481 test-> ; 

482 pg_get_constraintdef 

483 --------------------------------------------------- 

484 FOREIGN KEY (referred_id) REFERENCES referred(id) 

485 (1 row) 

486 

487Above, we created a table ``referred`` as a member of the remote schema 

488``test_schema``, however when we added ``test_schema`` to the 

489PG ``search_path`` and then asked ``pg_get_constraintdef()`` for the 

490``FOREIGN KEY`` syntax, ``test_schema`` was not included in the output of 

491the function. 

492 

493On the other hand, if we set the search path back to the typical default 

494of ``public``: 

495 

496.. sourcecode:: sql 

497 

498 test=> SET search_path TO public; 

499 SET 

500 

501The same query against ``pg_get_constraintdef()`` now returns the fully 

502schema-qualified name for us: 

503 

504.. sourcecode:: sql 

505 

506 test=> SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM 

507 test-> pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n 

508 test-> ON n.oid = c.relnamespace 

509 test-> JOIN pg_catalog.pg_constraint r ON c.oid = r.conrelid 

510 test-> WHERE c.relname='referring' AND r.contype = 'f'; 

511 pg_get_constraintdef 

512 --------------------------------------------------------------- 

513 FOREIGN KEY (referred_id) REFERENCES test_schema.referred(id) 

514 (1 row) 

515 

516SQLAlchemy will by default use the return value of ``pg_get_constraintdef()`` 

517in order to determine the remote schema name. That is, if our ``search_path`` 

518were set to include ``test_schema``, and we invoked a table 

519reflection process as follows:: 

520 

521 >>> from sqlalchemy import Table, MetaData, create_engine, text 

522 >>> engine = create_engine("postgresql+psycopg2://scott:tiger@localhost/test") 

523 >>> with engine.connect() as conn: 

524 ... conn.execute(text("SET search_path TO test_schema, public")) 

525 ... metadata_obj = MetaData() 

526 ... referring = Table("referring", metadata_obj, autoload_with=conn) 

527 <sqlalchemy.engine.result.CursorResult object at 0x101612ed0> 

528 

529The above process would deliver to the :attr:`_schema.MetaData.tables` 

530collection 

531``referred`` table named **without** the schema:: 

532 

533 >>> metadata_obj.tables["referred"].schema is None 

534 True 

535 

536To alter the behavior of reflection such that the referred schema is 

537maintained regardless of the ``search_path`` setting, use the 

538``postgresql_ignore_search_path`` option, which can be specified as a 

539dialect-specific argument to both :class:`_schema.Table` as well as 

540:meth:`_schema.MetaData.reflect`:: 

541 

542 >>> with engine.connect() as conn: 

543 ... conn.execute(text("SET search_path TO test_schema, public")) 

544 ... metadata_obj = MetaData() 

545 ... referring = Table( 

546 ... "referring", 

547 ... metadata_obj, 

548 ... autoload_with=conn, 

549 ... postgresql_ignore_search_path=True, 

550 ... ) 

551 <sqlalchemy.engine.result.CursorResult object at 0x1016126d0> 

552 

553We will now have ``test_schema.referred`` stored as schema-qualified:: 

554 

555 >>> metadata_obj.tables["test_schema.referred"].schema 

556 'test_schema' 

557 

558.. sidebar:: Best Practices for PostgreSQL Schema reflection 

559 

560 The description of PostgreSQL schema reflection behavior is complex, and 

561 is the product of many years of dealing with widely varied use cases and 

562 user preferences. But in fact, there's no need to understand any of it if 

563 you just stick to the simplest use pattern: leave the ``search_path`` set 

564 to its default of ``public`` only, never refer to the name ``public`` as 

565 an explicit schema name otherwise, and refer to all other schema names 

566 explicitly when building up a :class:`_schema.Table` object. The options 

567 described here are only for those users who can't, or prefer not to, stay 

568 within these guidelines. 

569 

570.. seealso:: 

571 

572 :ref:`reflection_schema_qualified_interaction` - discussion of the issue 

573 from a backend-agnostic perspective 

574 

575 `The Schema Search Path 

576 <https://www.postgresql.org/docs/current/static/ddl-schemas.html#DDL-SCHEMAS-PATH>`_ 

577 - on the PostgreSQL website. 

578 

579INSERT/UPDATE...RETURNING 

580------------------------- 

581 

582The dialect supports PG 8.2's ``INSERT..RETURNING``, ``UPDATE..RETURNING`` and 

583``DELETE..RETURNING`` syntaxes. ``INSERT..RETURNING`` is used by default 

584for single-row INSERT statements in order to fetch newly generated 

585primary key identifiers. To specify an explicit ``RETURNING`` clause, 

586use the :meth:`._UpdateBase.returning` method on a per-statement basis:: 

587 

588 # INSERT..RETURNING 

589 result = ( 

590 table.insert().returning(table.c.col1, table.c.col2).values(name="foo") 

591 ) 

592 print(result.fetchall()) 

593 

594 # UPDATE..RETURNING 

595 result = ( 

596 table.update() 

597 .returning(table.c.col1, table.c.col2) 

598 .where(table.c.name == "foo") 

599 .values(name="bar") 

600 ) 

601 print(result.fetchall()) 

602 

603 # DELETE..RETURNING 

604 result = ( 

605 table.delete() 

606 .returning(table.c.col1, table.c.col2) 

607 .where(table.c.name == "foo") 

608 ) 

609 print(result.fetchall()) 

610 

611.. _postgresql_insert_on_conflict: 

612 

613INSERT...ON CONFLICT (Upsert) 

614------------------------------ 

615 

616Starting with version 9.5, PostgreSQL allows "upserts" (update or insert) of 

617rows into a table via the ``ON CONFLICT`` clause of the ``INSERT`` statement. A 

618candidate row will only be inserted if that row does not violate any unique 

619constraints. In the case of a unique constraint violation, a secondary action 

620can occur which can be either "DO UPDATE", indicating that the data in the 

621target row should be updated, or "DO NOTHING", which indicates to silently skip 

622this row. 

623 

624Conflicts are determined using existing unique constraints and indexes. These 

625constraints may be identified either using their name as stated in DDL, 

626or they may be inferred by stating the columns and conditions that comprise 

627the indexes. 

628 

629SQLAlchemy provides ``ON CONFLICT`` support via the PostgreSQL-specific 

630:func:`_postgresql.insert()` function, which provides 

631the generative methods :meth:`_postgresql.Insert.on_conflict_do_update` 

632and :meth:`~.postgresql.Insert.on_conflict_do_nothing`: 

633 

634.. sourcecode:: pycon+sql 

635 

636 >>> from sqlalchemy.dialects.postgresql import insert 

637 >>> insert_stmt = insert(my_table).values( 

638 ... id="some_existing_id", data="inserted value" 

639 ... ) 

640 >>> do_nothing_stmt = insert_stmt.on_conflict_do_nothing(index_elements=["id"]) 

641 >>> print(do_nothing_stmt) 

642 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s) 

643 ON CONFLICT (id) DO NOTHING 

644 {stop} 

645 

646 >>> do_update_stmt = insert_stmt.on_conflict_do_update( 

647 ... constraint="pk_my_table", set_=dict(data="updated value") 

648 ... ) 

649 >>> print(do_update_stmt) 

650 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s) 

651 ON CONFLICT ON CONSTRAINT pk_my_table DO UPDATE SET data = %(param_1)s 

652 

653.. seealso:: 

654 

655 `INSERT .. ON CONFLICT 

656 <https://www.postgresql.org/docs/current/static/sql-insert.html#SQL-ON-CONFLICT>`_ 

657 - in the PostgreSQL documentation. 

658 

659Specifying the Target 

660^^^^^^^^^^^^^^^^^^^^^ 

661 

662Both methods supply the "target" of the conflict using either the 

663named constraint or by column inference: 

664 

665* The :paramref:`_postgresql.Insert.on_conflict_do_update.index_elements` argument 

666 specifies a sequence containing string column names, :class:`_schema.Column` 

667 objects, and/or SQL expression elements, which would identify a unique 

668 index: 

669 

670 .. sourcecode:: pycon+sql 

671 

672 >>> do_update_stmt = insert_stmt.on_conflict_do_update( 

673 ... index_elements=["id"], set_=dict(data="updated value") 

674 ... ) 

675 >>> print(do_update_stmt) 

676 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s) 

677 ON CONFLICT (id) DO UPDATE SET data = %(param_1)s 

678 {stop} 

679 

680 >>> do_update_stmt = insert_stmt.on_conflict_do_update( 

681 ... index_elements=[my_table.c.id], set_=dict(data="updated value") 

682 ... ) 

683 >>> print(do_update_stmt) 

684 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s) 

685 ON CONFLICT (id) DO UPDATE SET data = %(param_1)s 

686 

687* When using :paramref:`_postgresql.Insert.on_conflict_do_update.index_elements` to 

688 infer an index, a partial index can be inferred by also specifying the 

689 use the :paramref:`_postgresql.Insert.on_conflict_do_update.index_where` parameter: 

690 

691 .. sourcecode:: pycon+sql 

692 

693 >>> stmt = insert(my_table).values(user_email="a@b.com", data="inserted data") 

694 >>> stmt = stmt.on_conflict_do_update( 

695 ... index_elements=[my_table.c.user_email], 

696 ... index_where=my_table.c.user_email.like("%@gmail.com"), 

697 ... set_=dict(data=stmt.excluded.data), 

698 ... ) 

699 >>> print(stmt) 

700 {printsql}INSERT INTO my_table (data, user_email) 

701 VALUES (%(data)s, %(user_email)s) ON CONFLICT (user_email) 

702 WHERE user_email LIKE %(user_email_1)s DO UPDATE SET data = excluded.data 

703 

704* The :paramref:`_postgresql.Insert.on_conflict_do_update.constraint` argument is 

705 used to specify an index directly rather than inferring it. This can be 

706 the name of a UNIQUE constraint, a PRIMARY KEY constraint, or an INDEX: 

707 

708 .. sourcecode:: pycon+sql 

709 

710 >>> do_update_stmt = insert_stmt.on_conflict_do_update( 

711 ... constraint="my_table_idx_1", set_=dict(data="updated value") 

712 ... ) 

713 >>> print(do_update_stmt) 

714 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s) 

715 ON CONFLICT ON CONSTRAINT my_table_idx_1 DO UPDATE SET data = %(param_1)s 

716 {stop} 

717 

718 >>> do_update_stmt = insert_stmt.on_conflict_do_update( 

719 ... constraint="my_table_pk", set_=dict(data="updated value") 

720 ... ) 

721 >>> print(do_update_stmt) 

722 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s) 

723 ON CONFLICT ON CONSTRAINT my_table_pk DO UPDATE SET data = %(param_1)s 

724 {stop} 

725 

726* The :paramref:`_postgresql.Insert.on_conflict_do_update.constraint` argument may 

727 also refer to a SQLAlchemy construct representing a constraint, 

728 e.g. :class:`.UniqueConstraint`, :class:`.PrimaryKeyConstraint`, 

729 :class:`.Index`, or :class:`.ExcludeConstraint`. In this use, 

730 if the constraint has a name, it is used directly. Otherwise, if the 

731 constraint is unnamed, then inference will be used, where the expressions 

732 and optional WHERE clause of the constraint will be spelled out in the 

733 construct. This use is especially convenient 

734 to refer to the named or unnamed primary key of a :class:`_schema.Table` 

735 using the 

736 :attr:`_schema.Table.primary_key` attribute: 

737 

738 .. sourcecode:: pycon+sql 

739 

740 >>> do_update_stmt = insert_stmt.on_conflict_do_update( 

741 ... constraint=my_table.primary_key, set_=dict(data="updated value") 

742 ... ) 

743 >>> print(do_update_stmt) 

744 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s) 

745 ON CONFLICT (id) DO UPDATE SET data = %(param_1)s 

746 

747The SET Clause 

748^^^^^^^^^^^^^^^ 

749 

750``ON CONFLICT...DO UPDATE`` is used to perform an update of the already 

751existing row, using any combination of new values as well as values 

752from the proposed insertion. These values are specified using the 

753:paramref:`_postgresql.Insert.on_conflict_do_update.set_` parameter. This 

754parameter accepts a dictionary which consists of direct values 

755for UPDATE: 

756 

757.. sourcecode:: pycon+sql 

758 

759 >>> stmt = insert(my_table).values(id="some_id", data="inserted value") 

760 >>> do_update_stmt = stmt.on_conflict_do_update( 

761 ... index_elements=["id"], set_=dict(data="updated value") 

762 ... ) 

763 >>> print(do_update_stmt) 

764 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s) 

765 ON CONFLICT (id) DO UPDATE SET data = %(param_1)s 

766 

767.. warning:: 

768 

769 The :meth:`_expression.Insert.on_conflict_do_update` 

770 method does **not** take into 

771 account Python-side default UPDATE values or generation functions, e.g. 

772 those specified using :paramref:`_schema.Column.onupdate`. 

773 These values will not be exercised for an ON CONFLICT style of UPDATE, 

774 unless they are manually specified in the 

775 :paramref:`_postgresql.Insert.on_conflict_do_update.set_` dictionary. 

776 

777Updating using the Excluded INSERT Values 

778^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

779 

780In order to refer to the proposed insertion row, the special alias 

781:attr:`~.postgresql.Insert.excluded` is available as an attribute on 

782the :class:`_postgresql.Insert` object; this object is a 

783:class:`_expression.ColumnCollection` 

784which alias contains all columns of the target 

785table: 

786 

787.. sourcecode:: pycon+sql 

788 

789 >>> stmt = insert(my_table).values( 

790 ... id="some_id", data="inserted value", author="jlh" 

791 ... ) 

792 >>> do_update_stmt = stmt.on_conflict_do_update( 

793 ... index_elements=["id"], 

794 ... set_=dict(data="updated value", author=stmt.excluded.author), 

795 ... ) 

796 >>> print(do_update_stmt) 

797 {printsql}INSERT INTO my_table (id, data, author) 

798 VALUES (%(id)s, %(data)s, %(author)s) 

799 ON CONFLICT (id) DO UPDATE SET data = %(param_1)s, author = excluded.author 

800 

801Additional WHERE Criteria 

802^^^^^^^^^^^^^^^^^^^^^^^^^ 

803 

804The :meth:`_expression.Insert.on_conflict_do_update` method also accepts 

805a WHERE clause using the :paramref:`_postgresql.Insert.on_conflict_do_update.where` 

806parameter, which will limit those rows which receive an UPDATE: 

807 

808.. sourcecode:: pycon+sql 

809 

810 >>> stmt = insert(my_table).values( 

811 ... id="some_id", data="inserted value", author="jlh" 

812 ... ) 

813 >>> on_update_stmt = stmt.on_conflict_do_update( 

814 ... index_elements=["id"], 

815 ... set_=dict(data="updated value", author=stmt.excluded.author), 

816 ... where=(my_table.c.status == 2), 

817 ... ) 

818 >>> print(on_update_stmt) 

819 {printsql}INSERT INTO my_table (id, data, author) 

820 VALUES (%(id)s, %(data)s, %(author)s) 

821 ON CONFLICT (id) DO UPDATE SET data = %(param_1)s, author = excluded.author 

822 WHERE my_table.status = %(status_1)s 

823 

824Skipping Rows with DO NOTHING 

825^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

826 

827``ON CONFLICT`` may be used to skip inserting a row entirely 

828if any conflict with a unique or exclusion constraint occurs; below 

829this is illustrated using the 

830:meth:`~.postgresql.Insert.on_conflict_do_nothing` method: 

831 

832.. sourcecode:: pycon+sql 

833 

834 >>> stmt = insert(my_table).values(id="some_id", data="inserted value") 

835 >>> stmt = stmt.on_conflict_do_nothing(index_elements=["id"]) 

836 >>> print(stmt) 

837 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s) 

838 ON CONFLICT (id) DO NOTHING 

839 

840If ``DO NOTHING`` is used without specifying any columns or constraint, 

841it has the effect of skipping the INSERT for any unique or exclusion 

842constraint violation which occurs: 

843 

844.. sourcecode:: pycon+sql 

845 

846 >>> stmt = insert(my_table).values(id="some_id", data="inserted value") 

847 >>> stmt = stmt.on_conflict_do_nothing() 

848 >>> print(stmt) 

849 {printsql}INSERT INTO my_table (id, data) VALUES (%(id)s, %(data)s) 

850 ON CONFLICT DO NOTHING 

851 

852.. _postgresql_match: 

853 

854Full Text Search 

855---------------- 

856 

857PostgreSQL's full text search system is available through the use of the 

858:data:`.func` namespace, combined with the use of custom operators 

859via the :meth:`.Operators.bool_op` method. For simple cases with some 

860degree of cross-backend compatibility, the :meth:`.Operators.match` operator 

861may also be used. 

862 

863.. _postgresql_simple_match: 

864 

865Simple plain text matching with ``match()`` 

866^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

867 

868The :meth:`.Operators.match` operator provides for cross-compatible simple 

869text matching. For the PostgreSQL backend, it's hardcoded to generate 

870an expression using the ``@@`` operator in conjunction with the 

871``plainto_tsquery()`` PostgreSQL function. 

872 

873On the PostgreSQL dialect, an expression like the following:: 

874 

875 select(sometable.c.text.match("search string")) 

876 

877would emit to the database: 

878 

879.. sourcecode:: sql 

880 

881 SELECT text @@ plainto_tsquery('search string') FROM table 

882 

883Above, passing a plain string to :meth:`.Operators.match` will automatically 

884make use of ``plainto_tsquery()`` to specify the type of tsquery. This 

885establishes basic database cross-compatibility for :meth:`.Operators.match` 

886with other backends. 

887 

888.. versionchanged:: 2.0 The default tsquery generation function used by the 

889 PostgreSQL dialect with :meth:`.Operators.match` is ``plainto_tsquery()``. 

890 

891 To render exactly what was rendered in 1.4, use the following form:: 

892 

893 from sqlalchemy import func 

894 

895 select(sometable.c.text.bool_op("@@")(func.to_tsquery("search string"))) 

896 

897 Which would emit: 

898 

899 .. sourcecode:: sql 

900 

901 SELECT text @@ to_tsquery('search string') FROM table 

902 

903Using PostgreSQL full text functions and operators directly 

904^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

905 

906Text search operations beyond the simple use of :meth:`.Operators.match` 

907may make use of the :data:`.func` namespace to generate PostgreSQL full-text 

908functions, in combination with :meth:`.Operators.bool_op` to generate 

909any boolean operator. 

910 

911For example, the query:: 

912 

913 select(func.to_tsquery("cat").bool_op("@>")(func.to_tsquery("cat & rat"))) 

914 

915would generate: 

916 

917.. sourcecode:: sql 

918 

919 SELECT to_tsquery('cat') @> to_tsquery('cat & rat') 

920 

921 

922The :class:`_postgresql.TSVECTOR` type can provide for explicit CAST:: 

923 

924 from sqlalchemy.dialects.postgresql import TSVECTOR 

925 from sqlalchemy import select, cast 

926 

927 select(cast("some text", TSVECTOR)) 

928 

929produces a statement equivalent to: 

930 

931.. sourcecode:: sql 

932 

933 SELECT CAST('some text' AS TSVECTOR) AS anon_1 

934 

935The ``func`` namespace is augmented by the PostgreSQL dialect to set up 

936correct argument and return types for most full text search functions. 

937These functions are used automatically by the :attr:`_sql.func` namespace 

938assuming the ``sqlalchemy.dialects.postgresql`` package has been imported, 

939or :func:`_sa.create_engine` has been invoked using a ``postgresql`` 

940dialect. These functions are documented at: 

941 

942* :class:`_postgresql.to_tsvector` 

943* :class:`_postgresql.to_tsquery` 

944* :class:`_postgresql.plainto_tsquery` 

945* :class:`_postgresql.phraseto_tsquery` 

946* :class:`_postgresql.websearch_to_tsquery` 

947* :class:`_postgresql.ts_headline` 

948 

949Specifying the "regconfig" with ``match()`` or custom operators 

950^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

951 

952PostgreSQL's ``plainto_tsquery()`` function accepts an optional 

953"regconfig" argument that is used to instruct PostgreSQL to use a 

954particular pre-computed GIN or GiST index in order to perform the search. 

955When using :meth:`.Operators.match`, this additional parameter may be 

956specified using the ``postgresql_regconfig`` parameter, such as:: 

957 

958 select(mytable.c.id).where( 

959 mytable.c.title.match("somestring", postgresql_regconfig="english") 

960 ) 

961 

962Which would emit: 

963 

964.. sourcecode:: sql 

965 

966 SELECT mytable.id FROM mytable 

967 WHERE mytable.title @@ plainto_tsquery('english', 'somestring') 

968 

969When using other PostgreSQL search functions with :data:`.func`, the 

970"regconfig" parameter may be passed directly as the initial argument:: 

971 

972 select(mytable.c.id).where( 

973 func.to_tsvector("english", mytable.c.title).bool_op("@@")( 

974 func.to_tsquery("english", "somestring") 

975 ) 

976 ) 

977 

978produces a statement equivalent to: 

979 

980.. sourcecode:: sql 

981 

982 SELECT mytable.id FROM mytable 

983 WHERE to_tsvector('english', mytable.title) @@ 

984 to_tsquery('english', 'somestring') 

985 

986It is recommended that you use the ``EXPLAIN ANALYZE...`` tool from 

987PostgreSQL to ensure that you are generating queries with SQLAlchemy that 

988take full advantage of any indexes you may have created for full text search. 

989 

990.. seealso:: 

991 

992 `Full Text Search <https://www.postgresql.org/docs/current/textsearch-controls.html>`_ - in the PostgreSQL documentation 

993 

994 

995FROM ONLY ... 

996------------- 

997 

998The dialect supports PostgreSQL's ONLY keyword for targeting only a particular 

999table in an inheritance hierarchy. This can be used to produce the 

1000``SELECT ... FROM ONLY``, ``UPDATE ONLY ...``, and ``DELETE FROM ONLY ...`` 

1001syntaxes. It uses SQLAlchemy's hints mechanism:: 

1002 

1003 # SELECT ... FROM ONLY ... 

1004 result = table.select().with_hint(table, "ONLY", "postgresql") 

1005 print(result.fetchall()) 

1006 

1007 # UPDATE ONLY ... 

1008 table.update(values=dict(foo="bar")).with_hint( 

1009 "ONLY", dialect_name="postgresql" 

1010 ) 

1011 

1012 # DELETE FROM ONLY ... 

1013 table.delete().with_hint("ONLY", dialect_name="postgresql") 

1014 

1015.. _postgresql_indexes: 

1016 

1017PostgreSQL-Specific Index Options 

1018--------------------------------- 

1019 

1020Several extensions to the :class:`.Index` construct are available, specific 

1021to the PostgreSQL dialect. 

1022 

1023.. _postgresql_covering_indexes: 

1024 

1025Covering Indexes 

1026^^^^^^^^^^^^^^^^ 

1027 

1028A covering index includes additional columns that are not part of the index key 

1029but are stored in the index, allowing PostgreSQL to satisfy queries using only 

1030the index without accessing the table (an "index-only scan"). This is 

1031indicated on the index using the ``INCLUDE`` clause. The 

1032``postgresql_include`` option for :class:`.Index` (as well as 

1033:class:`.UniqueConstraint`) renders ``INCLUDE(colname)`` for the given string 

1034names:: 

1035 

1036 Index("my_index", table.c.x, postgresql_include=["y"]) 

1037 

1038would render the index as ``CREATE INDEX my_index ON table (x) INCLUDE (y)`` 

1039 

1040Note that this feature requires PostgreSQL 11 or later. 

1041 

1042.. seealso:: 

1043 

1044 :ref:`postgresql_constraint_options_include` - the same feature implemented 

1045 for :class:`.UniqueConstraint` 

1046 

1047.. versionadded:: 1.4 

1048 Support for covering indexes with :class:`.Index`. 

1049 

1050.. _postgresql_partial_indexes: 

1051 

1052Partial Indexes 

1053^^^^^^^^^^^^^^^ 

1054 

1055Partial indexes add criterion to the index definition so that the index is 

1056applied to a subset of rows. These can be specified on :class:`.Index` 

1057using the ``postgresql_where`` keyword argument:: 

1058 

1059 Index("my_index", my_table.c.id, postgresql_where=my_table.c.value > 10) 

1060 

1061.. _postgresql_operator_classes: 

1062 

1063Operator Classes 

1064^^^^^^^^^^^^^^^^ 

1065 

1066PostgreSQL allows the specification of an *operator class* for each column of 

1067an index (see 

1068https://www.postgresql.org/docs/current/interactive/indexes-opclass.html). 

1069The :class:`.Index` construct allows these to be specified via the 

1070``postgresql_ops`` keyword argument:: 

1071 

1072 Index( 

1073 "my_index", 

1074 my_table.c.id, 

1075 my_table.c.data, 

1076 postgresql_ops={"data": "text_pattern_ops", "id": "int4_ops"}, 

1077 ) 

1078 

1079Note that the keys in the ``postgresql_ops`` dictionaries are the 

1080"key" name of the :class:`_schema.Column`, i.e. the name used to access it from 

1081the ``.c`` collection of :class:`_schema.Table`, which can be configured to be 

1082different than the actual name of the column as expressed in the database. 

1083 

1084If ``postgresql_ops`` is to be used against a complex SQL expression such 

1085as a function call, then to apply to the column it must be given a label 

1086that is identified in the dictionary by name, e.g.:: 

1087 

1088 Index( 

1089 "my_index", 

1090 my_table.c.id, 

1091 func.lower(my_table.c.data).label("data_lower"), 

1092 postgresql_ops={"data_lower": "text_pattern_ops", "id": "int4_ops"}, 

1093 ) 

1094 

1095Operator classes are also supported by the 

1096:class:`_postgresql.ExcludeConstraint` construct using the 

1097:paramref:`_postgresql.ExcludeConstraint.ops` parameter. See that parameter for 

1098details. 

1099 

1100Index Types 

1101^^^^^^^^^^^ 

1102 

1103PostgreSQL provides several index types: B-Tree, Hash, GiST, and GIN, as well 

1104as the ability for users to create their own (see 

1105https://www.postgresql.org/docs/current/static/indexes-types.html). These can be 

1106specified on :class:`.Index` using the ``postgresql_using`` keyword argument:: 

1107 

1108 Index("my_index", my_table.c.data, postgresql_using="gin") 

1109 

1110The value passed to the keyword argument will be simply passed through to the 

1111underlying CREATE INDEX command, so it *must* be a valid index type for your 

1112version of PostgreSQL. 

1113 

1114.. _postgresql_index_storage: 

1115 

1116Index Storage Parameters 

1117^^^^^^^^^^^^^^^^^^^^^^^^ 

1118 

1119PostgreSQL allows storage parameters to be set on indexes. The storage 

1120parameters available depend on the index method used by the index. Storage 

1121parameters can be specified on :class:`.Index` using the ``postgresql_with`` 

1122keyword argument:: 

1123 

1124 Index("my_index", my_table.c.data, postgresql_with={"fillfactor": 50}) 

1125 

1126PostgreSQL allows to define the tablespace in which to create the index. 

1127The tablespace can be specified on :class:`.Index` using the 

1128``postgresql_tablespace`` keyword argument:: 

1129 

1130 Index("my_index", my_table.c.data, postgresql_tablespace="my_tablespace") 

1131 

1132Note that the same option is available on :class:`_schema.Table` as well. 

1133 

1134.. _postgresql_index_concurrently: 

1135 

1136Indexes with CONCURRENTLY 

1137^^^^^^^^^^^^^^^^^^^^^^^^^ 

1138 

1139The PostgreSQL index option CONCURRENTLY is supported by passing the 

1140flag ``postgresql_concurrently`` to the :class:`.Index` construct:: 

1141 

1142 tbl = Table("testtbl", m, Column("data", Integer)) 

1143 

1144 idx1 = Index("test_idx1", tbl.c.data, postgresql_concurrently=True) 

1145 

1146The above index construct will render DDL for CREATE INDEX, assuming 

1147PostgreSQL 8.2 or higher is detected or for a connection-less dialect, as: 

1148 

1149.. sourcecode:: sql 

1150 

1151 CREATE INDEX CONCURRENTLY test_idx1 ON testtbl (data) 

1152 

1153For DROP INDEX, assuming PostgreSQL 9.2 or higher is detected or for 

1154a connection-less dialect, it will emit: 

1155 

1156.. sourcecode:: sql 

1157 

1158 DROP INDEX CONCURRENTLY test_idx1 

1159 

1160When using CONCURRENTLY, the PostgreSQL database requires that the statement 

1161be invoked outside of a transaction block. The Python DBAPI enforces that 

1162even for a single statement, a transaction is present, so to use this 

1163construct, the DBAPI's "autocommit" mode must be used:: 

1164 

1165 metadata = MetaData() 

1166 table = Table("foo", metadata, Column("id", String)) 

1167 index = Index("foo_idx", table.c.id, postgresql_concurrently=True) 

1168 

1169 with engine.connect() as conn: 

1170 with conn.execution_options(isolation_level="AUTOCOMMIT"): 

1171 table.create(conn) 

1172 

1173.. seealso:: 

1174 

1175 :ref:`postgresql_isolation_level` 

1176 

1177.. _postgresql_index_reflection: 

1178 

1179PostgreSQL Index Reflection 

1180--------------------------- 

1181 

1182Notes on Index reflection 

1183 

1184 

1185Implicit UNIQUE index for UNIQUE CONSTRAINT 

1186^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

1187 

1188The PostgreSQL database creates a UNIQUE INDEX implicitly whenever the 

1189UNIQUE CONSTRAINT construct is used. When inspecting a table using 

1190:class:`_reflection.Inspector`, the :meth:`_reflection.Inspector.get_indexes` 

1191and the :meth:`_reflection.Inspector.get_unique_constraints` 

1192will report on these 

1193two constructs distinctly; in the case of the index, the key 

1194``duplicates_constraint`` will be present in the index entry if it is 

1195detected as mirroring a constraint. When performing reflection using 

1196``Table(..., autoload_with=engine)``, the UNIQUE INDEX is **not** returned 

1197in :attr:`_schema.Table.indexes` when it is detected as mirroring a 

1198:class:`.UniqueConstraint` in the :attr:`_schema.Table.constraints` collection. 

1199 

1200.. _postgresql_invalid_index_reflection: 

1201 

1202Invalid indexes 

1203^^^^^^^^^^^^^^^ 

1204 

1205Indexes marked "invalid" by PostgreSQL (i.e. an index that exists in the 

1206catalog, but is ignored by the query planner because it may be incomplete, 

1207while still being maintained on writes) remain present in reflection. The 

1208:meth:`_reflection.Inspector.get_indexes` and 

1209:meth:`_reflection.Inspector.get_multi_indexes` methods include 

1210``postgresql_invalid=True`` in the index's ``dialect_options`` dictionary when 

1211``pg_index.indisvalid`` is false. Valid indexes omit this flag. 

1212 

1213When reflecting a :class:`_schema.Table`, the flag, if present, is stored 

1214separately from DDL options on each reflected :class:`.Index`:: 

1215 

1216 table = Table("my_table", MetaData(), autoload_with=engine) 

1217 for index in table.indexes: 

1218 if index.reflect_only_elements["postgresql"].get("invalid"): 

1219 print(index.name) 

1220 

1221The :attr:`.Index.reflect_only_elements` mapping is read-only. These values are 

1222not included in :attr:`.Index.dialect_kwargs` and do not affect DDL. 

1223 

1224An invalid index may be left by a failed ``CREATE INDEX CONCURRENTLY``, may 

1225still be building, or may be a partitioned index awaiting attachment of its 

1226partition indexes. This flag reports the state at reflection time; it does not 

1227request an index rebuild. 

1228 

1229.. versionadded:: 2.1 

1230 

1231Special Reflection Options 

1232-------------------------- 

1233 

1234The :class:`_reflection.Inspector` 

1235used for the PostgreSQL backend is an instance 

1236of :class:`.PGInspector`, which offers additional methods:: 

1237 

1238 from sqlalchemy import create_engine, inspect 

1239 

1240 engine = create_engine("postgresql+psycopg2://localhost/test") 

1241 insp = inspect(engine) # will be a PGInspector 

1242 

1243 print(insp.get_enums()) 

1244 

1245.. autoclass:: PGInspector 

1246 :members: 

1247 

1248.. _postgresql_table_options: 

1249 

1250PostgreSQL Table Options 

1251------------------------ 

1252 

1253Several options for CREATE TABLE are supported directly by the PostgreSQL 

1254dialect in conjunction with the :class:`_schema.Table` construct, detailed 

1255in the following sections. 

1256 

1257.. seealso:: 

1258 

1259 `PostgreSQL CREATE TABLE options 

1260 <https://www.postgresql.org/docs/current/static/sql-createtable.html>`_ - 

1261 in the PostgreSQL documentation. 

1262 

1263``INHERITS`` 

1264^^^^^^^^^^^^ 

1265 

1266Specifies one or more parent tables from which this table inherits columns and 

1267constraints, enabling table inheritance hierarchies in PostgreSQL. 

1268 

1269:: 

1270 

1271 Table("some_table", metadata, ..., postgresql_inherits="some_supertable") 

1272 

1273 Table("some_table", metadata, ..., postgresql_inherits=("t1", "t2", ...)) 

1274 

1275For schema-qualified parent table names, use :class:`.quoted_name` with 

1276``quote=False`` to prevent the dotted name from being quoted as a single 

1277identifier:: 

1278 

1279 from sqlalchemy.sql import quoted_name 

1280 

1281 Table( 

1282 "some_table", 

1283 metadata, 

1284 ..., 

1285 postgresql_inherits=quoted_name( 

1286 "my_schema.some_supertable", quote=False 

1287 ), 

1288 ) 

1289 

1290SQLAlchemy does not automatically copy the columns from the inherited tables 

1291mentioned in the ``postgresql_inherits`` argument into the new 

1292:class:`_schema.Table`. To populate the new table columns reflection may be 

1293used, or a function similar to the following one:: 

1294 

1295 def get_parent_columns(tbl: sa.Table) -> list[sa.Column]: 

1296 return [ 

1297 sa.Column( 

1298 c.name, 

1299 c.type, 

1300 key=c.key, 

1301 nullable=c.nullable, 

1302 # Set system=true to omit from the CREATE TABLE statement 

1303 system=True, 

1304 ) 

1305 for c in tbl.columns 

1306 ] 

1307 

1308 

1309 documents = Table( 

1310 "some_table", 

1311 metadata, 

1312 *get_parent_columns(some_supertable), 

1313 # ... # add other columns here normally if needed 

1314 postgresql_inherits="some_supertable", 

1315 ) 

1316 

1317``ON COMMIT`` 

1318^^^^^^^^^^^^^ 

1319 

1320Controls the behavior of temporary tables at transaction commit, with options 

1321to preserve rows, delete rows, or drop the table. 

1322 

1323:: 

1324 

1325 Table("some_table", metadata, ..., postgresql_on_commit="PRESERVE ROWS") 

1326 

1327``PARTITION BY`` 

1328^^^^^^^^^^^^^^^^ 

1329 

1330Declares the table as a partitioned table using the specified partitioning 

1331strategy (RANGE, LIST, or HASH) on the given column(s). 

1332 

1333:: 

1334 

1335 Table( 

1336 "some_table", 

1337 metadata, 

1338 ..., 

1339 postgresql_partition_by="LIST (part_column)", 

1340 ) 

1341 

1342``TABLESPACE`` 

1343^^^^^^^^^^^^^^ 

1344 

1345Specifies the tablespace where the table will be stored, allowing control over 

1346the physical location of table data on disk. 

1347 

1348:: 

1349 

1350 Table("some_table", metadata, ..., postgresql_tablespace="some_tablespace") 

1351 

1352The above option is also available on the :class:`.Index` construct. 

1353 

1354``USING`` 

1355^^^^^^^^^ 

1356 

1357Specifies the table access method to use for storing table data, such as 

1358``heap`` (the default) or other custom access methods. 

1359 

1360:: 

1361 

1362 Table("some_table", metadata, ..., postgresql_using="heap") 

1363 

1364.. versionadded:: 2.0.26 

1365 

1366.. _postgresql_table_options_with: 

1367 

1368``WITH`` 

1369^^^^^^^^ 

1370 

1371:: 

1372 

1373 Table("some_table", metadata, ..., postgresql_with={"fillfactor": 100}) 

1374 

1375The ``postgresql_with`` parameter accepts a dictionary of storage parameters 

1376that will be applied to the table using the ``WITH`` clause in the 

1377``CREATE TABLE`` statement. Storage parameters control various aspects of 

1378table behavior such as the fill factor for pages, autovacuum settings, and 

1379toast table parameters. 

1380 

1381When the table is created, the parameters are rendered as a comma-separated 

1382list within the ``WITH`` clause. For example:: 

1383 

1384 Table( 

1385 "mytable", 

1386 metadata, 

1387 Column("id", Integer, primary_key=True), 

1388 postgresql_with={ 

1389 "fillfactor": 70, 

1390 "autovacuum_enabled": False, 

1391 "toast.vacuum_truncate": True, 

1392 }, 

1393 ) 

1394 

1395This will generate DDL similar to: 

1396 

1397.. sourcecode:: sql 

1398 

1399 CREATE TABLE mytable ( 

1400 id INTEGER NOT NULL, 

1401 PRIMARY KEY (id) 

1402 ) WITH (fillfactor = 70, autovacuum_enabled = false, toast.vacuum_truncate = true) 

1403 

1404The values in the dictionary can be integers, strings, boolean values, or 

1405``None``. Boolean values are rendered as lowercase ``true``/``false``. 

1406A ``None`` value renders the parameter name only, without an ``=`` value. 

1407Parameter names can include dots to specify parameters for associated objects 

1408like toast tables (e.g., ``toast.vacuum_truncate``). 

1409 

1410.. seealso:: 

1411 

1412 `PostgreSQL Storage Parameters 

1413 <https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS>`_ - 

1414 documentation on available storage parameters. 

1415 

1416.. versionadded:: 2.1 

1417 

1418``WITH OIDS`` 

1419^^^^^^^^^^^^^ 

1420 

1421Enables the legacy OID (object identifier) system column for the table, which 

1422assigns a unique identifier to each row. 

1423 

1424:: 

1425 

1426 Table("some_table", metadata, ..., postgresql_with_oids=True) 

1427 

1428.. note:: Support for tables "with OIDs" was removed in postgresql 12. 

1429 

1430``WITHOUT OIDS`` 

1431^^^^^^^^^^^^^^^^ 

1432 

1433Explicitly disables the OID system column for the table (the default behavior 

1434in modern PostgreSQL versions). 

1435 

1436:: 

1437 

1438 Table("some_table", metadata, ..., postgresql_with_oids=False) 

1439 

1440.. _postgresql_view_options: 

1441 

1442PostgreSQL View Options 

1443----------------------- 

1444 

1445The following sections indicate options which are supported by the PostgreSQL 

1446dialect in conjunction with :class:`.CreateView`. 

1447 

1448``WITH`` 

1449^^^^^^^^ 

1450 

1451Specifies view-level options for :class:`.CreateView` using the ``WITH`` 

1452clause:: 

1453 

1454 from sqlalchemy import CreateView, select 

1455 

1456 CreateView( 

1457 select(my_table), 

1458 "my_view", 

1459 postgresql_with={"security_invoker": True}, 

1460 ) 

1461 

1462This generates: 

1463 

1464.. sourcecode:: sql 

1465 

1466 CREATE VIEW my_view WITH (security_invoker = true) AS SELECT ... 

1467 

1468The same formatting rules that apply to :ref:`postgresql_table_options_with` 

1469apply here: booleans render as lowercase ``true``/``false``, ``None`` 

1470renders as the parameter name only, and other values render as-is. 

1471 

1472.. versionadded:: 2.1 

1473 

1474.. _postgresql_constraint_options: 

1475 

1476PostgreSQL Constraint Options 

1477----------------------------- 

1478 

1479The following sections indicate options which are supported by the PostgreSQL 

1480dialect in conjunction with selected constraint constructs. 

1481 

1482 

1483``NOT VALID`` 

1484^^^^^^^^^^^^^ 

1485 

1486Allows a constraint to be added without validating existing rows, improving 

1487performance when adding constraints to large tables. This option applies 

1488towards CHECK and FOREIGN KEY constraints when the constraint is being added 

1489to an existing table via ALTER TABLE, and has the effect that existing rows 

1490are not scanned during the ALTER operation against the constraint being added. 

1491 

1492When using a SQL migration tool such as `Alembic <https://alembic.sqlalchemy.org>`_ 

1493that renders ALTER TABLE constructs, the ``postgresql_not_valid`` argument 

1494may be specified as an additional keyword argument within the operation 

1495that creates the constraint, as in the following Alembic example:: 

1496 

1497 def update(): 

1498 op.create_foreign_key( 

1499 "fk_user_address", 

1500 "address", 

1501 "user", 

1502 ["user_id"], 

1503 ["id"], 

1504 postgresql_not_valid=True, 

1505 ) 

1506 

1507The keyword is ultimately accepted directly by the 

1508:class:`_schema.CheckConstraint`, :class:`_schema.ForeignKeyConstraint` 

1509and :class:`_schema.ForeignKey` constructs; when using a tool like 

1510Alembic, dialect-specific keyword arguments are passed through to 

1511these constructs from the migration operation directives:: 

1512 

1513 CheckConstraint("some_field IS NOT NULL", postgresql_not_valid=True) 

1514 

1515 ForeignKeyConstraint( 

1516 ["some_id"], ["some_table.some_id"], postgresql_not_valid=True 

1517 ) 

1518 

1519.. versionadded:: 1.4.32 

1520 

1521.. seealso:: 

1522 

1523 `PostgreSQL ALTER TABLE options 

1524 <https://www.postgresql.org/docs/current/static/sql-altertable.html>`_ - 

1525 in the PostgreSQL documentation. 

1526 

1527.. _postgresql_constraint_options_include: 

1528 

1529``INCLUDE`` 

1530^^^^^^^^^^^ 

1531 

1532This keyword is applicable to both a ``UNIQUE`` constraint as well as an 

1533``INDEX``. The ``postgresql_include`` option available for 

1534:class:`.UniqueConstraint` as well as :class:`.Index` creates a covering index 

1535by including additional columns in the underlying index without making them 

1536part of the key constraint. This option adds one or more columns as a "payload" 

1537to the index created automatically by PostgreSQL for the constraint. For 

1538example, the following table definition:: 

1539 

1540 Table( 

1541 "mytable", 

1542 metadata, 

1543 Column("id", Integer, nullable=False), 

1544 Column("value", Integer, nullable=False), 

1545 UniqueConstraint("id", postgresql_include=["value"]), 

1546 ) 

1547 

1548would produce the DDL statement 

1549 

1550.. sourcecode:: sql 

1551 

1552 CREATE TABLE mytable ( 

1553 id INTEGER NOT NULL, 

1554 value INTEGER NOT NULL, 

1555 UNIQUE (id) INCLUDE (value) 

1556 ) 

1557 

1558Note that this feature requires PostgreSQL 11 or later. 

1559 

1560.. versionadded:: 2.0.41 

1561 Added support for ``postgresql_include`` to :class:`.UniqueConstraint`, 

1562 to complement the existing feature in :class:`.Index`. 

1563 

1564.. seealso:: 

1565 

1566 :ref:`postgresql_covering_indexes` - background on ``postgresql_include`` 

1567 for the :class:`.Index` construct. 

1568 

1569 

1570Column list with foreign key ``ON DELETE SET`` actions 

1571^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

1572 

1573Allows selective column updates when a foreign key action is triggered, limiting 

1574which columns are set to NULL or DEFAULT upon deletion of a referenced row. 

1575This applies to :class:`.ForeignKey` and :class:`.ForeignKeyConstraint`, the 

1576:paramref:`.ForeignKey.ondelete` parameter will accept on the PostgreSQL 

1577backend only a string list of column names inside parenthesis, following the 

1578``SET NULL`` or ``SET DEFAULT`` phrases, which will limit the set of columns 

1579that are subject to the action:: 

1580 

1581 fktable = Table( 

1582 "fktable", 

1583 metadata, 

1584 Column("tid", Integer), 

1585 Column("id", Integer), 

1586 Column("fk_id_del_set_null", Integer), 

1587 ForeignKeyConstraint( 

1588 columns=["tid", "fk_id_del_set_null"], 

1589 refcolumns=[pktable.c.tid, pktable.c.id], 

1590 ondelete="SET NULL (fk_id_del_set_null)", 

1591 ), 

1592 ) 

1593 

1594.. versionadded:: 2.0.40 

1595 

1596.. _postgresql_computed_column_notes: 

1597 

1598Computed Columns (GENERATED ALWAYS AS) 

1599--------------------------------------- 

1600 

1601SQLAlchemy's support for the "GENERATED ALWAYS AS" SQL instruction, which 

1602establishes a dynamic, automatically populated value for a column, is available 

1603using the :ref:`computed_ddl` feature of SQLAlchemy DDL. E.g.:: 

1604 

1605 from sqlalchemy import Table, Column, MetaData, Integer, Computed 

1606 

1607 metadata_obj = MetaData() 

1608 

1609 square = Table( 

1610 "square", 

1611 metadata_obj, 

1612 Column("id", Integer, primary_key=True), 

1613 Column("side", Integer), 

1614 Column("area", Integer, Computed("side * side")), 

1615 Column("perimeter", Integer, Computed("4 * side", persisted=True)), 

1616 ) 

1617 

1618There are two general varieties of the "computed" column, ``VIRTUAL`` and ``STORED``. 

1619A ``STORED`` computed column computes and persists its value at INSERT/UPDATE time, 

1620while a ``VIRTUAL`` computed column computes its value on access without persisting it. 

1621This preference is indicated using the :paramref:`.Computed.persisted` parameter, 

1622which defaults to ``None`` to use the database default behavior. 

1623 

1624For PostgreSQL, prior to version 18 only the ``STORED`` variant was supported, 

1625requiring the ``STORED`` keyword to be emitted explicitly. PostgreSQL 18 added 

1626support for ``VIRTUAL`` columns and made ``VIRTUAL`` the default behavior. 

1627 

1628To accommodate this change, SQLAlchemy's behavior when 

1629:paramref:`.Computed.persisted` is not specified depends on the PostgreSQL 

1630version: on PostgreSQL 18 and later, no keyword is rendered, allowing the 

1631database to use its default of ``VIRTUAL``; on PostgreSQL 17 and earlier, 

1632``STORED`` is rendered and a warning is emitted. To ensure consistent 

1633``STORED`` behavior across all PostgreSQL versions, explicitly set 

1634``persisted=True``. 

1635 

1636.. versionchanged:: 2.1 

1637 

1638 PostgreSQL 18+ now defaults to ``VIRTUAL`` when :paramref:`.Computed.persisted` 

1639 is not specified. A warning is emitted for older versions of PostgreSQL 

1640 when this parameter is not indicated. 

1641 

1642 

1643 

1644 

1645``NULLS NOT DISTINCT`` 

1646^^^^^^^^^^^^^^^^^^^^^^ 

1647 

1648By default, two ``null`` values are not considered equal for unique constraints 

1649and indexes. Therefore, seemingly duplicate rows may be stored if one of the 

1650values in the constraint is ``null``. This default behavior is implementation 

1651defined, so other SQL dialects may behave differently than PostgreSQL. 

1652 

1653The ``NULLS NOT DISTINCT`` clause can be used to change this behavior, treating 

1654null values as equal and preventing unintended duplicate rows. The opposite 

1655``NULLS DISTINCT`` clause can also be used to make PostgreSQL's default behavior 

1656explict. 

1657 

1658The ``postgresql_nulls_not_distinct`` parameter can be set to ``True`` to 

1659add the ``NULLS NOT DISTINCT`` clause, or ``False`` to add ``NULLS DISTINCT``. 

1660Not setting it, or passing ``None``, will not add a clause and keep the default 

1661behavior. 

1662 

1663This feature requires PostgreSQL 15 or later. 

1664 

1665.. versionadded:: 2.0.16 

1666 

1667 

1668.. _postgresql_table_valued_overview: 

1669 

1670Table values, Table and Column valued functions, Row and Tuple objects 

1671----------------------------------------------------------------------- 

1672 

1673PostgreSQL makes great use of modern SQL forms such as table-valued functions, 

1674tables and rows as values. These constructs are commonly used as part 

1675of PostgreSQL's support for complex datatypes such as JSON, ARRAY, and other 

1676datatypes. SQLAlchemy's SQL expression language has native support for 

1677most table-valued and row-valued forms. 

1678 

1679.. _postgresql_table_valued: 

1680 

1681Table-Valued Functions 

1682^^^^^^^^^^^^^^^^^^^^^^^ 

1683 

1684Many PostgreSQL built-in functions are intended to be used in the FROM clause 

1685of a SELECT statement, and are capable of returning table rows or sets of table 

1686rows. A large portion of PostgreSQL's JSON functions for example such as 

1687``json_array_elements()``, ``json_object_keys()``, ``json_each_text()``, 

1688``json_each()``, ``json_to_record()``, ``json_populate_recordset()`` use such 

1689forms. These classes of SQL function calling forms in SQLAlchemy are available 

1690using the :meth:`_functions.FunctionElement.table_valued` method in conjunction 

1691with :class:`_functions.Function` objects generated from the :data:`_sql.func` 

1692namespace. 

1693 

1694Examples from PostgreSQL's reference documentation follow below: 

1695 

1696* ``json_each()``: 

1697 

1698 .. sourcecode:: pycon+sql 

1699 

1700 >>> from sqlalchemy import select, func 

1701 >>> stmt = select( 

1702 ... func.json_each('{"a":"foo", "b":"bar"}').table_valued("key", "value") 

1703 ... ) 

1704 >>> print(stmt) 

1705 {printsql}SELECT anon_1.key, anon_1.value 

1706 FROM json_each(:json_each_1) AS anon_1 

1707 

1708* ``json_populate_record()``: 

1709 

1710 .. sourcecode:: pycon+sql 

1711 

1712 >>> from sqlalchemy import select, func, literal_column 

1713 >>> stmt = select( 

1714 ... func.json_populate_record( 

1715 ... literal_column("null::myrowtype"), '{"a":1,"b":2}' 

1716 ... ).table_valued("a", "b", name="x") 

1717 ... ) 

1718 >>> print(stmt) 

1719 {printsql}SELECT x.a, x.b 

1720 FROM json_populate_record(null::myrowtype, :json_populate_record_1) AS x 

1721 

1722* ``json_to_record()`` - this form uses a PostgreSQL specific form of derived 

1723 columns in the alias, where we may make use of :func:`_sql.column` elements with 

1724 types to produce them. The :meth:`_functions.FunctionElement.table_valued` 

1725 method produces a :class:`_sql.TableValuedAlias` construct, and the method 

1726 :meth:`_sql.TableValuedAlias.render_derived` method sets up the derived 

1727 columns specification: 

1728 

1729 .. sourcecode:: pycon+sql 

1730 

1731 >>> from sqlalchemy import select, func, column, Integer, Text 

1732 >>> stmt = select( 

1733 ... func.json_to_record('{"a":1,"b":[1,2,3],"c":"bar"}') 

1734 ... .table_valued( 

1735 ... column("a", Integer), 

1736 ... column("b", Text), 

1737 ... column("d", Text), 

1738 ... ) 

1739 ... .render_derived(name="x", with_types=True) 

1740 ... ) 

1741 >>> print(stmt) 

1742 {printsql}SELECT x.a, x.b, x.d 

1743 FROM json_to_record(:json_to_record_1) AS x(a INTEGER, b TEXT, d TEXT) 

1744 

1745* ``WITH ORDINALITY`` - part of the SQL standard, ``WITH ORDINALITY`` adds an 

1746 ordinal counter to the output of a function and is accepted by a limited set 

1747 of PostgreSQL functions including ``unnest()`` and ``generate_series()``. The 

1748 :meth:`_functions.FunctionElement.table_valued` method accepts a keyword 

1749 parameter ``with_ordinality`` for this purpose, which accepts the string name 

1750 that will be applied to the "ordinality" column: 

1751 

1752 .. sourcecode:: pycon+sql 

1753 

1754 >>> from sqlalchemy import select, func 

1755 >>> stmt = select( 

1756 ... func.generate_series(4, 1, -1) 

1757 ... .table_valued("value", with_ordinality="ordinality") 

1758 ... .render_derived() 

1759 ... ) 

1760 >>> print(stmt) 

1761 {printsql}SELECT anon_1.value, anon_1.ordinality 

1762 FROM generate_series(:generate_series_1, :generate_series_2, :generate_series_3) 

1763 WITH ORDINALITY AS anon_1(value, ordinality) 

1764 

1765.. versionadded:: 1.4.0b2 

1766 

1767.. seealso:: 

1768 

1769 :ref:`tutorial_functions_table_valued` - in the :ref:`unified_tutorial` 

1770 

1771.. _postgresql_column_valued: 

1772 

1773Column Valued Functions 

1774^^^^^^^^^^^^^^^^^^^^^^^ 

1775 

1776Similar to the table valued function, a column valued function is present 

1777in the FROM clause, but delivers itself to the columns clause as a single 

1778scalar value. PostgreSQL functions such as ``json_array_elements()``, 

1779``unnest()`` and ``generate_series()`` may use this form. Column valued functions are available using the 

1780:meth:`_functions.FunctionElement.column_valued` method of :class:`_functions.FunctionElement`: 

1781 

1782* ``json_array_elements()``: 

1783 

1784 .. sourcecode:: pycon+sql 

1785 

1786 >>> from sqlalchemy import select, func 

1787 >>> stmt = select( 

1788 ... func.json_array_elements('["one", "two"]').column_valued("x") 

1789 ... ) 

1790 >>> print(stmt) 

1791 {printsql}SELECT x 

1792 FROM json_array_elements(:json_array_elements_1) AS x 

1793 

1794* ``unnest()`` - in order to generate a PostgreSQL ARRAY literal, the 

1795 :func:`_postgresql.array` construct may be used: 

1796 

1797 .. sourcecode:: pycon+sql 

1798 

1799 >>> from sqlalchemy.dialects.postgresql import array 

1800 >>> from sqlalchemy import select, func 

1801 >>> stmt = select(func.unnest(array([1, 2])).column_valued()) 

1802 >>> print(stmt) 

1803 {printsql}SELECT anon_1 

1804 FROM unnest(ARRAY[%(param_1)s, %(param_2)s]) AS anon_1 

1805 

1806 The function can of course be used against an existing table-bound column 

1807 that's of type :class:`_types.ARRAY`: 

1808 

1809 .. sourcecode:: pycon+sql 

1810 

1811 >>> from sqlalchemy import table, column, ARRAY, Integer 

1812 >>> from sqlalchemy import select, func 

1813 >>> t = table("t", column("value", ARRAY(Integer))) 

1814 >>> stmt = select(func.unnest(t.c.value).column_valued("unnested_value")) 

1815 >>> print(stmt) 

1816 {printsql}SELECT unnested_value 

1817 FROM unnest(t.value) AS unnested_value 

1818 

1819.. seealso:: 

1820 

1821 :ref:`tutorial_functions_column_valued` - in the :ref:`unified_tutorial` 

1822 

1823 

1824Row Types 

1825^^^^^^^^^ 

1826 

1827Built-in support for rendering a ``ROW`` may be approximated using 

1828``func.ROW`` with the :attr:`_sa.func` namespace, or by using the 

1829:func:`_sql.tuple_` construct: 

1830 

1831.. sourcecode:: pycon+sql 

1832 

1833 >>> from sqlalchemy import table, column, func, tuple_ 

1834 >>> t = table("t", column("id"), column("fk")) 

1835 >>> stmt = ( 

1836 ... t.select() 

1837 ... .where(tuple_(t.c.id, t.c.fk) > (1, 2)) 

1838 ... .where(func.ROW(t.c.id, t.c.fk) < func.ROW(3, 7)) 

1839 ... ) 

1840 >>> print(stmt) 

1841 {printsql}SELECT t.id, t.fk 

1842 FROM t 

1843 WHERE (t.id, t.fk) > (:param_1, :param_2) AND ROW(t.id, t.fk) < ROW(:ROW_1, :ROW_2) 

1844 

1845.. seealso:: 

1846 

1847 `PostgreSQL Row Constructors 

1848 <https://www.postgresql.org/docs/current/sql-expressions.html#SQL-SYNTAX-ROW-CONSTRUCTORS>`_ 

1849 

1850 `PostgreSQL Row Constructor Comparison 

1851 <https://www.postgresql.org/docs/current/functions-comparisons.html#ROW-WISE-COMPARISON>`_ 

1852 

1853Table Types passed to Functions 

1854^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

1855 

1856PostgreSQL supports passing a table as an argument to a function, which is 

1857known as a "record" type. SQLAlchemy :class:`_sql.FromClause` objects 

1858such as :class:`_schema.Table` support this special form using the 

1859:meth:`_sql.FromClause.table_valued` method, which is comparable to the 

1860:meth:`_functions.FunctionElement.table_valued` method except that the collection 

1861of columns is already established by that of the :class:`_sql.FromClause` 

1862itself: 

1863 

1864.. sourcecode:: pycon+sql 

1865 

1866 >>> from sqlalchemy import table, column, func, select 

1867 >>> a = table("a", column("id"), column("x"), column("y")) 

1868 >>> stmt = select(func.row_to_json(a.table_valued())) 

1869 >>> print(stmt) 

1870 {printsql}SELECT row_to_json(a) AS row_to_json_1 

1871 FROM a 

1872 

1873.. versionadded:: 1.4.0b2 

1874 

1875 

1876.. _postgresql_collation: 

1877 

1878Schema-Qualified Collations 

1879---------------------------- 

1880 

1881PostgreSQL supports collations that are qualified by a schema name, such as 

1882``CREATE COLLATION my_schema.my_collation (...)``. To refer to such a 

1883collation, use the :paramref:`.String.collation_schema` parameter (or 

1884:paramref:`_postgresql.DOMAIN.collation_schema` for a :class:`_postgresql.DOMAIN`) 

1885in conjunction with :paramref:`.String.collation`, rather than attempting to 

1886embed the schema name inside the ``collation`` string itself:: 

1887 

1888 Column( 

1889 "data", 

1890 String(collation="my_collation", collation_schema="my_schema"), 

1891 ) 

1892 

1893The above renders DDL similar to: 

1894 

1895.. sourcecode:: sql 

1896 

1897 data VARCHAR COLLATE "my_schema"."my_collation" 

1898 

1899.. versionadded:: 2.1 

1900 

1901.. seealso:: 

1902 

1903 :paramref:`.String.collation` 

1904 

1905 :paramref:`.String.collation_schema` 

1906 

1907""" # noqa: E501 

1908 

1909from __future__ import annotations 

1910 

1911from collections import defaultdict 

1912from functools import lru_cache 

1913import re 

1914from typing import Any 

1915from typing import cast 

1916from typing import Dict 

1917from typing import List 

1918from typing import Optional 

1919from typing import Tuple 

1920from typing import TYPE_CHECKING 

1921from typing import TypedDict 

1922from typing import Union 

1923 

1924from . import arraylib as _array 

1925from . import json as _json 

1926from . import pg_catalog 

1927from . import ranges as _ranges 

1928from .ext import _regconfig_fn 

1929from .ext import aggregate_order_by 

1930from .hstore import HSTORE 

1931from .named_types import CreateDomainType as CreateDomainType # noqa: F401 

1932from .named_types import CreateEnumType as CreateEnumType # noqa: F401 

1933from .named_types import DOMAIN as DOMAIN # noqa: F401 

1934from .named_types import DropDomainType as DropDomainType # noqa: F401 

1935from .named_types import DropEnumType as DropEnumType # noqa: F401 

1936from .named_types import ENUM as ENUM # noqa: F401 

1937from .named_types import NamedType as NamedType # noqa: F401 

1938from .types import _DECIMAL_TYPES # noqa: F401 

1939from .types import _FLOAT_TYPES # noqa: F401 

1940from .types import _INT_TYPES # noqa: F401 

1941from .types import BIT as BIT 

1942from .types import BYTEA as BYTEA 

1943from .types import CIDR as CIDR 

1944from .types import CITEXT as CITEXT 

1945from .types import INET as INET 

1946from .types import INTERVAL as INTERVAL 

1947from .types import MACADDR as MACADDR 

1948from .types import MACADDR8 as MACADDR8 

1949from .types import MONEY as MONEY 

1950from .types import OID as OID 

1951from .types import PGBit as PGBit # noqa: F401 

1952from .types import PGCidr as PGCidr # noqa: F401 

1953from .types import PGInet as PGInet # noqa: F401 

1954from .types import PGInterval as PGInterval # noqa: F401 

1955from .types import PGMacAddr as PGMacAddr # noqa: F401 

1956from .types import PGMacAddr8 as PGMacAddr8 # noqa: F401 

1957from .types import PGUuid as PGUuid 

1958from .types import REGCLASS as REGCLASS 

1959from .types import REGCONFIG as REGCONFIG # noqa: F401 

1960from .types import TIME as TIME 

1961from .types import TIMESTAMP as TIMESTAMP 

1962from .types import TSVECTOR as TSVECTOR 

1963from ... import exc 

1964from ... import schema 

1965from ... import select 

1966from ... import sql 

1967from ... import util 

1968from ...engine import characteristics 

1969from ...engine import default 

1970from ...engine import interfaces 

1971from ...engine import ObjectKind 

1972from ...engine import ObjectScope 

1973from ...engine import reflection 

1974from ...engine import URL 

1975from ...engine.reflection import ReflectionDefaults 

1976from ...sql import bindparam 

1977from ...sql import coercions 

1978from ...sql import compiler 

1979from ...sql import elements 

1980from ...sql import expression 

1981from ...sql import functions 

1982from ...sql import roles 

1983from ...sql import sqltypes 

1984from ...sql import util as sql_util 

1985from ...sql.base import DialectKWArgConst 

1986from ...sql.compiler import InsertmanyvaluesSentinelOpts 

1987from ...sql.visitors import InternalTraversal 

1988from ...types import BIGINT 

1989from ...types import BOOLEAN 

1990from ...types import CHAR 

1991from ...types import DATE 

1992from ...types import DOUBLE_PRECISION 

1993from ...types import FLOAT 

1994from ...types import INTEGER 

1995from ...types import NUMERIC 

1996from ...types import REAL 

1997from ...types import SMALLINT 

1998from ...types import TEXT 

1999from ...types import UUID as UUID 

2000from ...types import VARCHAR 

2001 

2002IDX_USING = re.compile(r"^(?:btree|hash|gist|gin|[\w_]+)$", re.I) 

2003 

2004RESERVED_WORDS = { 

2005 "all", 

2006 "analyse", 

2007 "analyze", 

2008 "and", 

2009 "any", 

2010 "array", 

2011 "as", 

2012 "asc", 

2013 "asymmetric", 

2014 "both", 

2015 "case", 

2016 "cast", 

2017 "check", 

2018 "collate", 

2019 "column", 

2020 "constraint", 

2021 "create", 

2022 "current_catalog", 

2023 "current_date", 

2024 "current_role", 

2025 "current_time", 

2026 "current_timestamp", 

2027 "current_user", 

2028 "default", 

2029 "deferrable", 

2030 "desc", 

2031 "distinct", 

2032 "do", 

2033 "else", 

2034 "end", 

2035 "except", 

2036 "false", 

2037 "fetch", 

2038 "for", 

2039 "foreign", 

2040 "from", 

2041 "grant", 

2042 "group", 

2043 "having", 

2044 "in", 

2045 "initially", 

2046 "intersect", 

2047 "into", 

2048 "leading", 

2049 "limit", 

2050 "localtime", 

2051 "localtimestamp", 

2052 "new", 

2053 "not", 

2054 "null", 

2055 "of", 

2056 "off", 

2057 "offset", 

2058 "old", 

2059 "on", 

2060 "only", 

2061 "or", 

2062 "order", 

2063 "placing", 

2064 "primary", 

2065 "references", 

2066 "returning", 

2067 "select", 

2068 "session_user", 

2069 "some", 

2070 "symmetric", 

2071 "table", 

2072 "then", 

2073 "to", 

2074 "trailing", 

2075 "true", 

2076 "union", 

2077 "unique", 

2078 "user", 

2079 "using", 

2080 "variadic", 

2081 "when", 

2082 "where", 

2083 "window", 

2084 "with", 

2085 "authorization", 

2086 "between", 

2087 "binary", 

2088 "cross", 

2089 "current_schema", 

2090 "freeze", 

2091 "full", 

2092 "ilike", 

2093 "inner", 

2094 "is", 

2095 "isnull", 

2096 "join", 

2097 "left", 

2098 "like", 

2099 "natural", 

2100 "notnull", 

2101 "outer", 

2102 "over", 

2103 "overlaps", 

2104 "right", 

2105 "similar", 

2106 "verbose", 

2107} 

2108 

2109 

2110colspecs = { 

2111 sqltypes.ARRAY: _array.ARRAY, 

2112 sqltypes.Interval: INTERVAL, 

2113 sqltypes.Enum: ENUM, 

2114 sqltypes.JSON.JSONPathType: _json.JSONPATH, 

2115 sqltypes.JSON: _json.JSON, 

2116 sqltypes.Uuid: PGUuid, 

2117} 

2118 

2119 

2120ischema_names = { 

2121 "_array": _array.ARRAY, 

2122 "hstore": HSTORE, 

2123 "json": _json.JSON, 

2124 "jsonb": _json.JSONB, 

2125 "int4range": _ranges.INT4RANGE, 

2126 "int8range": _ranges.INT8RANGE, 

2127 "numrange": _ranges.NUMRANGE, 

2128 "daterange": _ranges.DATERANGE, 

2129 "tsrange": _ranges.TSRANGE, 

2130 "tstzrange": _ranges.TSTZRANGE, 

2131 "int4multirange": _ranges.INT4MULTIRANGE, 

2132 "int8multirange": _ranges.INT8MULTIRANGE, 

2133 "nummultirange": _ranges.NUMMULTIRANGE, 

2134 "datemultirange": _ranges.DATEMULTIRANGE, 

2135 "tsmultirange": _ranges.TSMULTIRANGE, 

2136 "tstzmultirange": _ranges.TSTZMULTIRANGE, 

2137 "integer": INTEGER, 

2138 "bigint": BIGINT, 

2139 "smallint": SMALLINT, 

2140 "character varying": VARCHAR, 

2141 "character": CHAR, 

2142 '"char"': sqltypes.String, 

2143 "name": sqltypes.String, 

2144 "text": TEXT, 

2145 "numeric": NUMERIC, 

2146 "float": FLOAT, 

2147 "real": REAL, 

2148 "inet": INET, 

2149 "cidr": CIDR, 

2150 "citext": CITEXT, 

2151 "uuid": UUID, 

2152 "bit": BIT, 

2153 "bit varying": BIT, 

2154 "macaddr": MACADDR, 

2155 "macaddr8": MACADDR8, 

2156 "money": MONEY, 

2157 "oid": OID, 

2158 "regclass": REGCLASS, 

2159 "double precision": DOUBLE_PRECISION, 

2160 "timestamp": TIMESTAMP, 

2161 "timestamp with time zone": TIMESTAMP, 

2162 "timestamp without time zone": TIMESTAMP, 

2163 "time with time zone": TIME, 

2164 "time without time zone": TIME, 

2165 "date": DATE, 

2166 "time": TIME, 

2167 "bytea": BYTEA, 

2168 "boolean": BOOLEAN, 

2169 "interval": INTERVAL, 

2170 "tsvector": TSVECTOR, 

2171} 

2172 

2173 

2174class PGCompiler(compiler.SQLCompiler): 

2175 def visit_to_tsvector_func(self, element, **kw): 

2176 return self._assert_pg_ts_ext(element, **kw) 

2177 

2178 def visit_to_tsquery_func(self, element, **kw): 

2179 return self._assert_pg_ts_ext(element, **kw) 

2180 

2181 def visit_plainto_tsquery_func(self, element, **kw): 

2182 return self._assert_pg_ts_ext(element, **kw) 

2183 

2184 def visit_phraseto_tsquery_func(self, element, **kw): 

2185 return self._assert_pg_ts_ext(element, **kw) 

2186 

2187 def visit_websearch_to_tsquery_func(self, element, **kw): 

2188 return self._assert_pg_ts_ext(element, **kw) 

2189 

2190 def visit_ts_headline_func(self, element, **kw): 

2191 return self._assert_pg_ts_ext(element, **kw) 

2192 

2193 def _assert_pg_ts_ext(self, element, **kw): 

2194 if not isinstance(element, _regconfig_fn): 

2195 # other options here include trying to rewrite the function 

2196 # with the correct types. however, that means we have to 

2197 # "un-SQL-ize" the first argument, which can't work in a 

2198 # generalized way. Also, parent compiler class has already added 

2199 # the incorrect return type to the result map. So let's just 

2200 # make sure the function we want is used up front. 

2201 

2202 raise exc.CompileError( 

2203 f'Can\'t compile "{element.name}()" full text search ' 

2204 f"function construct that does not originate from the " 

2205 f'"sqlalchemy.dialects.postgresql" package. ' 

2206 f'Please ensure "import sqlalchemy.dialects.postgresql" is ' 

2207 f"called before constructing " 

2208 f'"sqlalchemy.func.{element.name}()" to ensure registration ' 

2209 f"of the correct argument and return types." 

2210 ) 

2211 

2212 return f"{element.name}{self.function_argspec(element, **kw)}" 

2213 

2214 def render_bind_cast(self, type_, dbapi_type, sqltext): 

2215 if dbapi_type._type_affinity is sqltypes.String and dbapi_type.length: 

2216 # use VARCHAR with no length for VARCHAR cast. 

2217 # see #9511 

2218 dbapi_type = sqltypes.STRINGTYPE 

2219 return f"""{sqltext}::{ 

2220 self.dialect.type_compiler_instance.process( 

2221 dbapi_type, identifier_preparer=self.preparer 

2222 ) 

2223 }""" 

2224 

2225 def visit_array(self, element, **kw): 

2226 if not element.clauses and not element.type.item_type._isnull: 

2227 return "ARRAY[]::%s" % element.type.compile(self.dialect) 

2228 return "ARRAY[%s]" % self.visit_clauselist(element, **kw) 

2229 

2230 def visit_slice(self, element, **kw): 

2231 return "%s:%s" % ( 

2232 self.process(element.start, **kw), 

2233 self.process(element.stop, **kw), 

2234 ) 

2235 

2236 def visit_bitwise_xor_op_binary(self, binary, operator, **kw): 

2237 return self._generate_generic_binary(binary, " # ", **kw) 

2238 

2239 def visit_json_getitem_op_binary( 

2240 self, binary, operator, _cast_applied=False, **kw 

2241 ): 

2242 if ( 

2243 not _cast_applied 

2244 and binary.type._type_affinity is not sqltypes.JSON 

2245 ): 

2246 kw["_cast_applied"] = True 

2247 return self.process(sql.cast(binary, binary.type), **kw) 

2248 

2249 kw["eager_grouping"] = True 

2250 

2251 if ( 

2252 not _cast_applied 

2253 and isinstance(binary.left.type, _json.JSONB) 

2254 and self.dialect._supports_jsonb_subscripting 

2255 ): 

2256 left = binary.left 

2257 if isinstance(left, (functions.FunctionElement, elements.Cast)): 

2258 left = elements.Grouping(left) 

2259 

2260 # for pg14+JSONB use subscript notation: col['key'] instead 

2261 # of col -> 'key' 

2262 return "%s[%s]" % ( 

2263 self.process(left, **kw), 

2264 self.process(binary.right, **kw), 

2265 ) 

2266 else: 

2267 # Fall back to arrow notation for older versions or when cast 

2268 # is applied 

2269 return self._generate_generic_binary( 

2270 binary, " -> " if not _cast_applied else " ->> ", **kw 

2271 ) 

2272 

2273 def visit_json_path_getitem_op_binary( 

2274 self, binary, operator, _cast_applied=False, **kw 

2275 ): 

2276 if ( 

2277 not _cast_applied 

2278 and binary.type._type_affinity is not sqltypes.JSON 

2279 ): 

2280 kw["_cast_applied"] = True 

2281 return self.process(sql.cast(binary, binary.type), **kw) 

2282 

2283 kw["eager_grouping"] = True 

2284 return self._generate_generic_binary( 

2285 binary, " #> " if not _cast_applied else " #>> ", **kw 

2286 ) 

2287 

2288 def visit_hstore_getitem_op_binary(self, binary, operator, **kw): 

2289 kw["eager_grouping"] = True 

2290 

2291 if self.dialect._supports_jsonb_subscripting: 

2292 # use subscript notation: col['key'] instead of col -> 'key' 

2293 # For function calls, wrap in parentheses: (func())[key] 

2294 left_str = self.process(binary.left, **kw) 

2295 if isinstance(binary.left, sql.functions.FunctionElement): 

2296 left_str = f"({left_str})" 

2297 return "%s[%s]" % ( 

2298 left_str, 

2299 self.process(binary.right, **kw), 

2300 ) 

2301 else: 

2302 # Fall back to arrow notation for older versions 

2303 return self._generate_generic_binary(binary, " -> ", **kw) 

2304 

2305 def visit_getitem_binary(self, binary, operator, **kw): 

2306 return "%s[%s]" % ( 

2307 self.process(binary.left, **kw), 

2308 self.process(binary.right, **kw), 

2309 ) 

2310 

2311 def visit_aggregate_order_by(self, element, **kw): 

2312 return "%s ORDER BY %s" % ( 

2313 self.process(element.target, **kw), 

2314 self.process(element.order_by, **kw), 

2315 ) 

2316 

2317 def visit_match_op_binary(self, binary, operator, **kw): 

2318 if "postgresql_regconfig" in binary.modifiers: 

2319 regconfig = self.render_literal_value( 

2320 binary.modifiers["postgresql_regconfig"], sqltypes.STRINGTYPE 

2321 ) 

2322 if regconfig: 

2323 return "%s @@ plainto_tsquery(%s, %s)" % ( 

2324 self.process(binary.left, **kw), 

2325 regconfig, 

2326 self.process(binary.right, **kw), 

2327 ) 

2328 return "%s @@ plainto_tsquery(%s)" % ( 

2329 self.process(binary.left, **kw), 

2330 self.process(binary.right, **kw), 

2331 ) 

2332 

2333 def visit_ilike_case_insensitive_operand(self, element, **kw): 

2334 return element.element._compiler_dispatch(self, **kw) 

2335 

2336 def visit_ilike_op_binary(self, binary, operator, **kw): 

2337 escape = binary.modifiers.get("escape", None) 

2338 

2339 return "%s ILIKE %s" % ( 

2340 self.process(binary.left, **kw), 

2341 self.process(binary.right, **kw), 

2342 ) + ( 

2343 " ESCAPE " + self.render_literal_value(escape, sqltypes.STRINGTYPE) 

2344 if escape is not None 

2345 else "" 

2346 ) 

2347 

2348 def visit_not_ilike_op_binary(self, binary, operator, **kw): 

2349 escape = binary.modifiers.get("escape", None) 

2350 return "%s NOT ILIKE %s" % ( 

2351 self.process(binary.left, **kw), 

2352 self.process(binary.right, **kw), 

2353 ) + ( 

2354 " ESCAPE " + self.render_literal_value(escape, sqltypes.STRINGTYPE) 

2355 if escape is not None 

2356 else "" 

2357 ) 

2358 

2359 def _regexp_match(self, base_op, binary, operator, kw): 

2360 flags = binary.modifiers["flags"] 

2361 if flags is None: 

2362 return self._generate_generic_binary( 

2363 binary, " %s " % base_op, **kw 

2364 ) 

2365 if flags == "i": 

2366 return self._generate_generic_binary( 

2367 binary, " %s* " % base_op, **kw 

2368 ) 

2369 return "%s %s CONCAT('(?', %s, ')', %s)" % ( 

2370 self.process(binary.left, **kw), 

2371 base_op, 

2372 self.render_literal_value(flags, sqltypes.STRINGTYPE), 

2373 self.process(binary.right, **kw), 

2374 ) 

2375 

2376 def visit_regexp_match_op_binary(self, binary, operator, **kw): 

2377 return self._regexp_match("~", binary, operator, kw) 

2378 

2379 def visit_not_regexp_match_op_binary(self, binary, operator, **kw): 

2380 return self._regexp_match("!~", binary, operator, kw) 

2381 

2382 def visit_regexp_replace_op_binary(self, binary, operator, **kw): 

2383 string = self.process(binary.left, **kw) 

2384 pattern_replace = self.process(binary.right, **kw) 

2385 flags = binary.modifiers["flags"] 

2386 if flags is None: 

2387 return "REGEXP_REPLACE(%s, %s)" % ( 

2388 string, 

2389 pattern_replace, 

2390 ) 

2391 else: 

2392 return "REGEXP_REPLACE(%s, %s, %s)" % ( 

2393 string, 

2394 pattern_replace, 

2395 self.render_literal_value(flags, sqltypes.STRINGTYPE), 

2396 ) 

2397 

2398 def visit_empty_set_expr(self, element_types, **kw): 

2399 # cast the empty set to the type we are comparing against. if 

2400 # we are comparing against the null type, pick an arbitrary 

2401 # datatype for the empty set 

2402 return "SELECT %s WHERE 1!=1" % ( 

2403 ", ".join( 

2404 "CAST(NULL AS %s)" 

2405 % self.dialect.type_compiler_instance.process( 

2406 INTEGER() if type_._isnull else type_ 

2407 ) 

2408 for type_ in element_types or [INTEGER()] 

2409 ), 

2410 ) 

2411 

2412 def render_literal_value(self, value, type_): 

2413 value = super().render_literal_value(value, type_) 

2414 

2415 if self.dialect._backslash_escapes: 

2416 value = value.replace("\\", "\\\\") 

2417 return value 

2418 

2419 def visit_aggregate_strings_func(self, fn, **kw): 

2420 return super().visit_aggregate_strings_func( 

2421 fn, use_function_name="string_agg", **kw 

2422 ) 

2423 

2424 def visit_pow_func(self, fn, **kw): 

2425 return f"power{self.function_argspec(fn)}" 

2426 

2427 def visit_sequence(self, seq, **kw): 

2428 return "nextval(%s)" % self.preparer._format_sequence_string_literal( 

2429 seq 

2430 ) 

2431 

2432 def limit_clause(self, select, **kw): 

2433 text = "" 

2434 if select._limit_clause is not None: 

2435 text += " \n LIMIT " + self.process(select._limit_clause, **kw) 

2436 if select._offset_clause is not None: 

2437 if select._limit_clause is None: 

2438 text += "\n LIMIT ALL" 

2439 text += " OFFSET " + self.process(select._offset_clause, **kw) 

2440 return text 

2441 

2442 def format_from_hint_text(self, sqltext, table, hint, iscrud): 

2443 if hint.upper() != "ONLY": 

2444 raise exc.CompileError("Unrecognized hint: %r" % hint) 

2445 return "ONLY " + sqltext 

2446 

2447 def get_select_precolumns(self, select, **kw): 

2448 # Do not call super().get_select_precolumns because 

2449 # it will warn/raise when distinct on is present 

2450 if select._distinct or select._distinct_on: 

2451 if select._distinct_on: 

2452 return ( 

2453 "DISTINCT ON (" 

2454 + ", ".join( 

2455 [ 

2456 self.process(col, **kw) 

2457 for col in select._distinct_on 

2458 ] 

2459 ) 

2460 + ") " 

2461 ) 

2462 else: 

2463 return "DISTINCT " 

2464 else: 

2465 return "" 

2466 

2467 def visit_postgresql_distinct_on(self, element, **kw): 

2468 if self.stack[-1]["selectable"]._distinct_on: 

2469 raise exc.CompileError( 

2470 "Cannot mix ``select.ext(distinct_on(...))`` and " 

2471 "``select.distinct(...)``" 

2472 ) 

2473 

2474 if element._distinct_on: 

2475 cols = ", ".join( 

2476 self.process(col, **kw) for col in element._distinct_on 

2477 ) 

2478 return f"ON ({cols})" 

2479 else: 

2480 return None 

2481 

2482 def for_update_clause(self, select, **kw): 

2483 if select._for_update_arg.read: 

2484 if select._for_update_arg.key_share: 

2485 tmp = " FOR KEY SHARE" 

2486 else: 

2487 tmp = " FOR SHARE" 

2488 elif select._for_update_arg.key_share: 

2489 tmp = " FOR NO KEY UPDATE" 

2490 else: 

2491 tmp = " FOR UPDATE" 

2492 

2493 if select._for_update_arg.of: 

2494 tables = util.OrderedSet() 

2495 for c in select._for_update_arg.of: 

2496 tables.update(sql_util.surface_selectables_only(c)) 

2497 

2498 of_kw = dict(kw) 

2499 of_kw.update(ashint=True, use_schema=False) 

2500 tmp += " OF " + ", ".join( 

2501 self.process(table, **of_kw) for table in tables 

2502 ) 

2503 

2504 if select._for_update_arg.nowait: 

2505 tmp += " NOWAIT" 

2506 if select._for_update_arg.skip_locked: 

2507 tmp += " SKIP LOCKED" 

2508 

2509 return tmp 

2510 

2511 def visit_substring_func(self, func, **kw): 

2512 s = self.process(func.clauses.clauses[0], **kw) 

2513 start = self.process(func.clauses.clauses[1], **kw) 

2514 if len(func.clauses.clauses) > 2: 

2515 length = self.process(func.clauses.clauses[2], **kw) 

2516 return "SUBSTRING(%s FROM %s FOR %s)" % (s, start, length) 

2517 else: 

2518 return "SUBSTRING(%s FROM %s)" % (s, start) 

2519 

2520 def _on_conflict_target(self, clause, **kw): 

2521 if clause.constraint_target is not None: 

2522 # target may be a name of an Index, UniqueConstraint or 

2523 # ExcludeConstraint. While there is a separate 

2524 # "max_identifier_length" for indexes, PostgreSQL uses the same 

2525 # length for all objects so we can use 

2526 # truncate_and_render_constraint_name 

2527 target_text = ( 

2528 "ON CONSTRAINT %s" 

2529 % self.preparer.truncate_and_render_constraint_name( 

2530 clause.constraint_target 

2531 ) 

2532 ) 

2533 elif clause.inferred_target_elements is not None: 

2534 target_text = "(%s)" % ", ".join( 

2535 ( 

2536 self.preparer.quote(c) 

2537 if isinstance(c, str) 

2538 else self.process(c, include_table=False, use_schema=False) 

2539 ) 

2540 for c in clause.inferred_target_elements 

2541 ) 

2542 if clause.inferred_target_whereclause is not None: 

2543 whereclause_kw = dict(kw) 

2544 whereclause_kw.update(include_table=False, use_schema=False) 

2545 target_text += " WHERE %s" % self.process( 

2546 clause.inferred_target_whereclause, 

2547 **whereclause_kw, 

2548 ) 

2549 else: 

2550 target_text = "" 

2551 

2552 return target_text 

2553 

2554 def visit_on_conflict_do_nothing(self, on_conflict, **kw): 

2555 target_text = self._on_conflict_target(on_conflict, **kw) 

2556 

2557 if target_text: 

2558 return "ON CONFLICT %s DO NOTHING" % target_text 

2559 else: 

2560 return "ON CONFLICT DO NOTHING" 

2561 

2562 def visit_on_conflict_do_update(self, on_conflict, **kw): 

2563 clause = on_conflict 

2564 

2565 target_text = self._on_conflict_target(on_conflict, **kw) 

2566 

2567 action_set_ops = [] 

2568 

2569 set_parameters = dict(clause.update_values_to_set) 

2570 # create a list of column assignment clauses as tuples 

2571 

2572 insert_statement = self.stack[-1]["selectable"] 

2573 cols = insert_statement.table.c 

2574 set_kw = dict(kw) 

2575 set_kw.update(use_schema=False) 

2576 for c in cols: 

2577 col_key = c.key 

2578 

2579 if col_key in set_parameters: 

2580 value = set_parameters.pop(col_key) 

2581 elif c in set_parameters: 

2582 value = set_parameters.pop(c) 

2583 else: 

2584 continue 

2585 

2586 assert not coercions._is_literal(value) 

2587 if ( 

2588 isinstance(value, elements.BindParameter) 

2589 and value.type._isnull 

2590 ): 

2591 value = value._with_binary_element_type(c.type) 

2592 

2593 value_text = self.process( 

2594 value.self_group(), is_upsert_set=True, **set_kw 

2595 ) 

2596 

2597 key_text = self.preparer.quote(c.name) 

2598 action_set_ops.append("%s = %s" % (key_text, value_text)) 

2599 

2600 # check for names that don't match columns 

2601 if set_parameters: 

2602 util.warn( 

2603 "Additional column names not matching " 

2604 "any column keys in table '%s': %s" 

2605 % ( 

2606 self.current_executable.table.name, 

2607 (", ".join("'%s'" % c for c in set_parameters)), 

2608 ) 

2609 ) 

2610 for k, v in set_parameters.items(): 

2611 key_text = ( 

2612 self.preparer.quote(k) 

2613 if isinstance(k, str) 

2614 else self.process(k, use_schema=False) 

2615 ) 

2616 value_text = self.process( 

2617 coercions.expect(roles.ExpressionElementRole, v), 

2618 is_upsert_set=True, 

2619 **set_kw, 

2620 ) 

2621 action_set_ops.append("%s = %s" % (key_text, value_text)) 

2622 

2623 action_text = ", ".join(action_set_ops) 

2624 if clause.update_whereclause is not None: 

2625 where_kw = dict(kw) 

2626 where_kw.update(include_table=True, use_schema=False) 

2627 action_text += " WHERE %s" % self.process( 

2628 clause.update_whereclause, **where_kw 

2629 ) 

2630 

2631 return "ON CONFLICT %s DO UPDATE SET %s" % (target_text, action_text) 

2632 

2633 def update_from_clause( 

2634 self, update_stmt, from_table, extra_froms, from_hints, **kw 

2635 ): 

2636 kw["asfrom"] = True 

2637 return "FROM " + ", ".join( 

2638 t._compiler_dispatch(self, fromhints=from_hints, **kw) 

2639 for t in extra_froms 

2640 ) 

2641 

2642 def delete_extra_from_clause( 

2643 self, delete_stmt, from_table, extra_froms, from_hints, **kw 

2644 ): 

2645 """Render the DELETE .. USING clause specific to PostgreSQL.""" 

2646 kw["asfrom"] = True 

2647 return "USING " + ", ".join( 

2648 t._compiler_dispatch(self, fromhints=from_hints, **kw) 

2649 for t in extra_froms 

2650 ) 

2651 

2652 def fetch_clause(self, select, **kw): 

2653 # pg requires parens for non literal clauses. It's also required for 

2654 # bind parameters if a ::type casts is used by the driver (asyncpg), 

2655 # so it's easiest to just always add it 

2656 text = "" 

2657 if select._offset_clause is not None: 

2658 text += "\n OFFSET (%s) ROWS" % self.process( 

2659 select._offset_clause, **kw 

2660 ) 

2661 if select._fetch_clause is not None: 

2662 text += "\n FETCH FIRST (%s)%s ROWS %s" % ( 

2663 self.process(select._fetch_clause, **kw), 

2664 " PERCENT" if select._fetch_clause_options["percent"] else "", 

2665 ( 

2666 "WITH TIES" 

2667 if select._fetch_clause_options["with_ties"] 

2668 else "ONLY" 

2669 ), 

2670 ) 

2671 return text 

2672 

2673 

2674class PGDDLCompiler(compiler.DDLCompiler): 

2675 def get_column_specification(self, column, **kwargs): 

2676 colspec = self.preparer.format_column(column) 

2677 impl_type = column.type.dialect_impl(self.dialect) 

2678 if isinstance(impl_type, sqltypes.TypeDecorator): 

2679 impl_type = impl_type.impl 

2680 

2681 has_identity = ( 

2682 column.identity is not None 

2683 and self.dialect.supports_identity_columns 

2684 ) 

2685 

2686 if ( 

2687 column.primary_key 

2688 and column is column.table._autoincrement_column 

2689 and ( 

2690 self.dialect.supports_smallserial 

2691 or not isinstance(impl_type, sqltypes.SmallInteger) 

2692 ) 

2693 and not has_identity 

2694 and ( 

2695 column.default is None 

2696 or ( 

2697 isinstance(column.default, schema.Sequence) 

2698 and column.default.optional 

2699 ) 

2700 ) 

2701 ): 

2702 if isinstance(impl_type, sqltypes.BigInteger): 

2703 colspec += " BIGSERIAL" 

2704 elif isinstance(impl_type, sqltypes.SmallInteger): 

2705 colspec += " SMALLSERIAL" 

2706 else: 

2707 colspec += " SERIAL" 

2708 else: 

2709 colspec += " " + self.dialect.type_compiler_instance.process( 

2710 column.type, 

2711 type_expression=column, 

2712 identifier_preparer=self.preparer, 

2713 ) 

2714 default = self.get_column_default_string(column) 

2715 if default is not None: 

2716 colspec += " DEFAULT " + default 

2717 

2718 if column.computed is not None: 

2719 colspec += " " + self.process(column.computed) 

2720 if has_identity: 

2721 colspec += " " + self.process(column.identity) 

2722 

2723 if not column.nullable and not has_identity: 

2724 colspec += " NOT NULL" 

2725 elif column.nullable and has_identity: 

2726 colspec += " NULL" 

2727 return colspec 

2728 

2729 def _define_constraint_validity(self, constraint): 

2730 not_valid = constraint.dialect_options["postgresql"]["not_valid"] 

2731 return " NOT VALID" if not_valid else "" 

2732 

2733 def _define_include(self, obj): 

2734 includeclause = obj.dialect_options["postgresql"]["include"] 

2735 if not includeclause: 

2736 return "" 

2737 inclusions = [ 

2738 obj.table.c[col] if isinstance(col, str) else col 

2739 for col in includeclause 

2740 ] 

2741 return " INCLUDE (%s)" % ", ".join( 

2742 [self.preparer.quote(c.name) for c in inclusions] 

2743 ) 

2744 

2745 def visit_check_constraint(self, constraint, **kw): 

2746 if constraint._type_bound: 

2747 typ = list(constraint.columns)[0].type 

2748 if ( 

2749 isinstance(typ, sqltypes.ARRAY) 

2750 and isinstance(typ.item_type, sqltypes.Enum) 

2751 and not typ.item_type.native_enum 

2752 ): 

2753 raise exc.CompileError( 

2754 "PostgreSQL dialect cannot produce the CHECK constraint " 

2755 "for ARRAY of non-native ENUM; please specify " 

2756 "create_constraint=False on this Enum datatype." 

2757 ) 

2758 

2759 text = super().visit_check_constraint(constraint) 

2760 text += self._define_constraint_validity(constraint) 

2761 return text 

2762 

2763 def visit_foreign_key_constraint(self, constraint, **kw): 

2764 text = super().visit_foreign_key_constraint(constraint) 

2765 text += self._define_constraint_validity(constraint) 

2766 return text 

2767 

2768 def visit_primary_key_constraint(self, constraint, **kw): 

2769 text = self.define_constraint_preamble(constraint, **kw) 

2770 text += self.define_primary_key_body(constraint, **kw) 

2771 text += self._define_include(constraint) 

2772 text += self.define_constraint_deferrability(constraint) 

2773 return text 

2774 

2775 def visit_unique_constraint(self, constraint, **kw): 

2776 if len(constraint) == 0: 

2777 return "" 

2778 text = self.define_constraint_preamble(constraint, **kw) 

2779 text += self.define_unique_body(constraint, **kw) 

2780 text += self._define_include(constraint) 

2781 text += self.define_constraint_deferrability(constraint) 

2782 return text 

2783 

2784 @util.memoized_property 

2785 def _fk_ondelete_pattern(self): 

2786 return re.compile( 

2787 r"^(?:RESTRICT|CASCADE|SET (?:NULL|DEFAULT)(?:\s*\(.+\))?" 

2788 r"|NO ACTION)$", 

2789 re.I, 

2790 ) 

2791 

2792 def define_constraint_ondelete_cascade(self, constraint): 

2793 return " ON DELETE %s" % self.preparer.validate_sql_phrase( 

2794 constraint.ondelete, self._fk_ondelete_pattern 

2795 ) 

2796 

2797 def visit_create_enum_type(self, create, **kw): 

2798 type_ = create.element 

2799 

2800 return "CREATE TYPE %s AS ENUM (%s)" % ( 

2801 self.preparer.format_type(type_), 

2802 ", ".join( 

2803 self.sql_compiler.process(sql.literal(e), literal_binds=True) 

2804 for e in type_.enums 

2805 ), 

2806 ) 

2807 

2808 def visit_drop_enum_type(self, drop, **kw): 

2809 type_ = drop.element 

2810 

2811 return "DROP TYPE %s" % (self.preparer.format_type(type_)) 

2812 

2813 def visit_create_domain_type(self, create, **kw): 

2814 domain: DOMAIN = create.element 

2815 

2816 options = [] 

2817 if domain.collation is not None: 

2818 collation = self.preparer.format_collation( 

2819 domain.collation, domain.collation_schema 

2820 ) 

2821 options.append(f"COLLATE {collation}") 

2822 if domain.default is not None: 

2823 default = self.render_default_string(domain.default) 

2824 options.append(f"DEFAULT {default}") 

2825 if domain.constraint_name is not None: 

2826 name = self.preparer.truncate_and_render_constraint_name( 

2827 domain.constraint_name 

2828 ) 

2829 options.append(f"CONSTRAINT {name}") 

2830 if domain.not_null: 

2831 options.append("NOT NULL") 

2832 if domain.check is not None: 

2833 check = self.sql_compiler.process( 

2834 domain.check, include_table=False, literal_binds=True 

2835 ) 

2836 options.append(f"CHECK ({check})") 

2837 

2838 return ( 

2839 f"CREATE DOMAIN {self.preparer.format_type(domain)} AS " 

2840 f"{self.type_compiler.process(domain.data_type)} " 

2841 f"{' '.join(options)}" 

2842 ) 

2843 

2844 def visit_drop_domain_type(self, drop, **kw): 

2845 domain = drop.element 

2846 return f"DROP DOMAIN {self.preparer.format_type(domain)}" 

2847 

2848 def _prepare_withclause_opts(self, withclause): 

2849 with_opts = [] 

2850 for param, value in withclause.items(): 

2851 if value is None: 

2852 with_opts.append(param) 

2853 elif isinstance(value, bool): 

2854 with_opts.append( 

2855 "%s = %s" % (param, "true" if value else "false") 

2856 ) 

2857 else: 

2858 with_opts.append("%s = %s" % (param, value)) 

2859 return ", ".join(with_opts) 

2860 

2861 def create_table_select_suffixes(self, element, type_, **kw): 

2862 if type_ == "view": 

2863 withclause = element.dialect_options["postgresql"]["with"] 

2864 if withclause: 

2865 return "WITH (%s)" % self._prepare_withclause_opts(withclause) 

2866 return "" 

2867 

2868 def visit_create_index(self, create, **kw): 

2869 preparer = self.preparer 

2870 index = create.element 

2871 self._verify_index_table(index) 

2872 text = "CREATE " 

2873 if index.unique: 

2874 text += "UNIQUE " 

2875 

2876 text += "INDEX " 

2877 

2878 if self.dialect._supports_create_index_concurrently: 

2879 concurrently = index.dialect_options["postgresql"]["concurrently"] 

2880 if concurrently: 

2881 text += "CONCURRENTLY " 

2882 

2883 if create.if_not_exists: 

2884 text += "IF NOT EXISTS " 

2885 

2886 text += "%s ON %s " % ( 

2887 self._prepared_index_name(index, include_schema=False), 

2888 preparer.format_table(index.table), 

2889 ) 

2890 

2891 using = index.dialect_options["postgresql"]["using"] 

2892 if using: 

2893 text += ( 

2894 "USING %s " 

2895 % self.preparer.validate_sql_phrase(using, IDX_USING).lower() 

2896 ) 

2897 

2898 ops = index.dialect_options["postgresql"]["ops"] 

2899 text += "(%s)" % ( 

2900 ", ".join( 

2901 [ 

2902 self.sql_compiler.process( 

2903 ( 

2904 expr.self_group() 

2905 if not isinstance(expr, expression.ColumnClause) 

2906 else expr 

2907 ), 

2908 include_table=False, 

2909 literal_binds=True, 

2910 ) 

2911 + ( 

2912 (" " + ops[expr.key]) 

2913 if hasattr(expr, "key") and expr.key in ops 

2914 else "" 

2915 ) 

2916 for expr in index.expressions 

2917 ] 

2918 ) 

2919 ) 

2920 

2921 text += self._define_include(index) 

2922 

2923 nulls_not_distinct = index.dialect_options["postgresql"][ 

2924 "nulls_not_distinct" 

2925 ] 

2926 if nulls_not_distinct is True: 

2927 text += " NULLS NOT DISTINCT" 

2928 elif nulls_not_distinct is False: 

2929 text += " NULLS DISTINCT" 

2930 

2931 withclause = index.dialect_options["postgresql"]["with"] 

2932 if withclause: 

2933 text += " WITH (%s)" % self._prepare_withclause_opts(withclause) 

2934 

2935 tablespace_name = index.dialect_options["postgresql"]["tablespace"] 

2936 if tablespace_name: 

2937 text += " TABLESPACE %s" % preparer.quote(tablespace_name) 

2938 

2939 whereclause = index.dialect_options["postgresql"]["where"] 

2940 if whereclause is not None: 

2941 whereclause = coercions.expect( 

2942 roles.DDLExpressionRole, whereclause 

2943 ) 

2944 

2945 where_compiled = self.sql_compiler.process( 

2946 whereclause, include_table=False, literal_binds=True 

2947 ) 

2948 text += " WHERE " + where_compiled 

2949 

2950 return text 

2951 

2952 def define_unique_constraint_distinct(self, constraint, **kw): 

2953 nulls_not_distinct = constraint.dialect_options["postgresql"][ 

2954 "nulls_not_distinct" 

2955 ] 

2956 if nulls_not_distinct is True: 

2957 nulls_not_distinct_param = "NULLS NOT DISTINCT " 

2958 elif nulls_not_distinct is False: 

2959 nulls_not_distinct_param = "NULLS DISTINCT " 

2960 else: 

2961 nulls_not_distinct_param = "" 

2962 return nulls_not_distinct_param 

2963 

2964 def visit_drop_index(self, drop, **kw): 

2965 index = drop.element 

2966 

2967 text = "\nDROP INDEX " 

2968 

2969 if self.dialect._supports_drop_index_concurrently: 

2970 concurrently = index.dialect_options["postgresql"]["concurrently"] 

2971 if concurrently: 

2972 text += "CONCURRENTLY " 

2973 

2974 if drop.if_exists: 

2975 text += "IF EXISTS " 

2976 

2977 text += self._prepared_index_name(index, include_schema=True) 

2978 return text 

2979 

2980 def visit_exclude_constraint(self, constraint, **kw): 

2981 text = "" 

2982 if constraint.name is not None: 

2983 text += "CONSTRAINT %s " % self.preparer.format_constraint( 

2984 constraint 

2985 ) 

2986 elements = [] 

2987 kw["include_table"] = False 

2988 kw["literal_binds"] = True 

2989 for expr, name, op in constraint._render_exprs: 

2990 exclude_element = self.sql_compiler.process(expr, **kw) + ( 

2991 (" " + constraint.ops[expr.key]) 

2992 if hasattr(expr, "key") and expr.key in constraint.ops 

2993 else "" 

2994 ) 

2995 

2996 elements.append("%s WITH %s" % (exclude_element, op)) 

2997 text += "EXCLUDE USING %s (%s)" % ( 

2998 self.preparer.validate_sql_phrase( 

2999 constraint.using, IDX_USING 

3000 ).lower(), 

3001 ", ".join(elements), 

3002 ) 

3003 if constraint.where is not None: 

3004 text += " WHERE (%s)" % self.sql_compiler.process( 

3005 constraint.where, literal_binds=True 

3006 ) 

3007 text += self.define_constraint_deferrability(constraint) 

3008 return text 

3009 

3010 def post_create_table(self, table): 

3011 table_opts = [] 

3012 pg_opts = table.dialect_options["postgresql"] 

3013 

3014 inherits = pg_opts.get("inherits") 

3015 if inherits is not None: 

3016 if not isinstance(inherits, (list, tuple)): 

3017 inherits = (inherits,) 

3018 table_opts.append( 

3019 "\n INHERITS (" 

3020 + ", ".join(self.preparer.quote(name) for name in inherits) 

3021 + ")" 

3022 ) 

3023 

3024 if pg_opts["partition_by"]: 

3025 table_opts.append("\n PARTITION BY %s" % pg_opts["partition_by"]) 

3026 

3027 if pg_opts["using"]: 

3028 table_opts.append("\n USING %s" % pg_opts["using"]) 

3029 

3030 if pg_opts["with"]: 

3031 table_opts.append( 

3032 "\n WITH (%s)" % self._prepare_withclause_opts(pg_opts["with"]) 

3033 ) 

3034 

3035 if pg_opts["with_oids"] is True: 

3036 table_opts.append("\n WITH OIDS") 

3037 elif pg_opts["with_oids"] is False: 

3038 table_opts.append("\n WITHOUT OIDS") 

3039 

3040 if pg_opts["on_commit"]: 

3041 on_commit_options = pg_opts["on_commit"].replace("_", " ").upper() 

3042 table_opts.append("\n ON COMMIT %s" % on_commit_options) 

3043 

3044 if pg_opts["tablespace"]: 

3045 tablespace_name = pg_opts["tablespace"] 

3046 table_opts.append( 

3047 "\n TABLESPACE %s" % self.preparer.quote(tablespace_name) 

3048 ) 

3049 

3050 return "".join(table_opts) 

3051 

3052 def visit_computed_column(self, generated, **kw): 

3053 if self.dialect.supports_virtual_generated_columns: 

3054 return super().visit_computed_column(generated, **kw) 

3055 if generated.persisted is False: 

3056 raise exc.CompileError( 

3057 "PostrgreSQL computed columns do not support 'virtual' " 

3058 "persistence; set the 'persisted' flag to None or True for " 

3059 "PostgreSQL support." 

3060 ) 

3061 elif generated.persisted is None: 

3062 util.warn( 

3063 f"Computed column {generated.column} is being created as " 

3064 "'STORED' since the current PostgreSQL version does not " 

3065 "support VIRTUAL columns. On PostgreSQL 18+, when " 

3066 "'persisted' is not " 

3067 "specified, no keyword will be rendered and VIRTUAL will be " 

3068 "used by default. Set 'persisted=True' to ensure STORED " 

3069 "behavior across all PostgreSQL versions." 

3070 ) 

3071 

3072 return "GENERATED ALWAYS AS (%s) STORED" % self.sql_compiler.process( 

3073 generated.sqltext, include_table=False, literal_binds=True 

3074 ) 

3075 

3076 def visit_create_sequence(self, create, **kw): 

3077 prefix = None 

3078 if create.element.data_type is not None: 

3079 prefix = " AS %s" % self.type_compiler.process( 

3080 create.element.data_type 

3081 ) 

3082 

3083 return super().visit_create_sequence(create, prefix=prefix, **kw) 

3084 

3085 def _can_comment_on_constraint(self, ddl_instance): 

3086 constraint = ddl_instance.element 

3087 if constraint.name is None: 

3088 raise exc.CompileError( 

3089 f"Can't emit COMMENT ON for constraint {constraint!r}: " 

3090 "it has no name" 

3091 ) 

3092 if constraint.table is None: 

3093 raise exc.CompileError( 

3094 f"Can't emit COMMENT ON for constraint {constraint!r}: " 

3095 "it has no associated table" 

3096 ) 

3097 

3098 def visit_set_constraint_comment(self, create, **kw): 

3099 self._can_comment_on_constraint(create) 

3100 return "COMMENT ON CONSTRAINT %s ON %s IS %s" % ( 

3101 self.preparer.format_constraint(create.element), 

3102 self.preparer.format_table(create.element.table), 

3103 self.sql_compiler.render_literal_value( 

3104 create.element.comment, sqltypes.String() 

3105 ), 

3106 ) 

3107 

3108 def visit_drop_constraint_comment(self, drop, **kw): 

3109 self._can_comment_on_constraint(drop) 

3110 return "COMMENT ON CONSTRAINT %s ON %s IS NULL" % ( 

3111 self.preparer.format_constraint(drop.element), 

3112 self.preparer.format_table(drop.element.table), 

3113 ) 

3114 

3115 

3116class PGTypeCompiler(compiler.GenericTypeCompiler): 

3117 def visit_TSVECTOR(self, type_, **kw): 

3118 return "TSVECTOR" 

3119 

3120 def visit_TSQUERY(self, type_, **kw): 

3121 return "TSQUERY" 

3122 

3123 def visit_INET(self, type_, **kw): 

3124 return "INET" 

3125 

3126 def visit_CIDR(self, type_, **kw): 

3127 return "CIDR" 

3128 

3129 def visit_CITEXT(self, type_, **kw): 

3130 return "CITEXT" 

3131 

3132 def visit_MACADDR(self, type_, **kw): 

3133 return "MACADDR" 

3134 

3135 def visit_MACADDR8(self, type_, **kw): 

3136 return "MACADDR8" 

3137 

3138 def visit_MONEY(self, type_, **kw): 

3139 return "MONEY" 

3140 

3141 def visit_OID(self, type_, **kw): 

3142 return "OID" 

3143 

3144 def visit_REGCONFIG(self, type_, **kw): 

3145 return "REGCONFIG" 

3146 

3147 def visit_REGCLASS(self, type_, **kw): 

3148 return "REGCLASS" 

3149 

3150 def visit_FLOAT(self, type_, **kw): 

3151 if not type_.precision: 

3152 return "FLOAT" 

3153 else: 

3154 return "FLOAT(%(precision)s)" % {"precision": type_.precision} 

3155 

3156 def visit_double(self, type_, **kw): 

3157 return self.visit_DOUBLE_PRECISION(type, **kw) 

3158 

3159 def visit_BIGINT(self, type_, **kw): 

3160 return "BIGINT" 

3161 

3162 def visit_HSTORE(self, type_, **kw): 

3163 return "HSTORE" 

3164 

3165 def visit_JSON(self, type_, **kw): 

3166 return "JSON" 

3167 

3168 def visit_JSONB(self, type_, **kw): 

3169 return "JSONB" 

3170 

3171 def visit_INT4MULTIRANGE(self, type_, **kw): 

3172 return "INT4MULTIRANGE" 

3173 

3174 def visit_INT8MULTIRANGE(self, type_, **kw): 

3175 return "INT8MULTIRANGE" 

3176 

3177 def visit_NUMMULTIRANGE(self, type_, **kw): 

3178 return "NUMMULTIRANGE" 

3179 

3180 def visit_DATEMULTIRANGE(self, type_, **kw): 

3181 return "DATEMULTIRANGE" 

3182 

3183 def visit_TSMULTIRANGE(self, type_, **kw): 

3184 return "TSMULTIRANGE" 

3185 

3186 def visit_TSTZMULTIRANGE(self, type_, **kw): 

3187 return "TSTZMULTIRANGE" 

3188 

3189 def visit_INT4RANGE(self, type_, **kw): 

3190 return "INT4RANGE" 

3191 

3192 def visit_INT8RANGE(self, type_, **kw): 

3193 return "INT8RANGE" 

3194 

3195 def visit_NUMRANGE(self, type_, **kw): 

3196 return "NUMRANGE" 

3197 

3198 def visit_DATERANGE(self, type_, **kw): 

3199 return "DATERANGE" 

3200 

3201 def visit_TSRANGE(self, type_, **kw): 

3202 return "TSRANGE" 

3203 

3204 def visit_TSTZRANGE(self, type_, **kw): 

3205 return "TSTZRANGE" 

3206 

3207 def visit_json_int_index(self, type_, **kw): 

3208 return "INT" 

3209 

3210 def visit_json_str_index(self, type_, **kw): 

3211 return "TEXT" 

3212 

3213 def visit_datetime(self, type_, **kw): 

3214 return self.visit_TIMESTAMP(type_, **kw) 

3215 

3216 def visit_enum(self, type_, **kw): 

3217 if not type_.native_enum or not self.dialect.supports_native_enum: 

3218 return super().visit_enum(type_, **kw) 

3219 else: 

3220 return self.visit_ENUM(type_, **kw) 

3221 

3222 def visit_ENUM(self, type_, identifier_preparer=None, **kw): 

3223 if identifier_preparer is None: 

3224 identifier_preparer = self.dialect.identifier_preparer 

3225 return identifier_preparer.format_type(type_) 

3226 

3227 def visit_DOMAIN(self, type_, identifier_preparer=None, **kw): 

3228 if identifier_preparer is None: 

3229 identifier_preparer = self.dialect.identifier_preparer 

3230 return identifier_preparer.format_type(type_) 

3231 

3232 def visit_TIMESTAMP(self, type_, **kw): 

3233 return "TIMESTAMP%s %s" % ( 

3234 ( 

3235 "(%d)" % type_.precision 

3236 if getattr(type_, "precision", None) is not None 

3237 else "" 

3238 ), 

3239 (type_.timezone and "WITH" or "WITHOUT") + " TIME ZONE", 

3240 ) 

3241 

3242 def visit_TIME(self, type_, **kw): 

3243 return "TIME%s %s" % ( 

3244 ( 

3245 "(%d)" % type_.precision 

3246 if getattr(type_, "precision", None) is not None 

3247 else "" 

3248 ), 

3249 (type_.timezone and "WITH" or "WITHOUT") + " TIME ZONE", 

3250 ) 

3251 

3252 def visit_INTERVAL(self, type_, **kw): 

3253 text = "INTERVAL" 

3254 if type_.fields is not None: 

3255 text += " " + type_.fields 

3256 if type_.precision is not None: 

3257 text += " (%d)" % type_.precision 

3258 return text 

3259 

3260 def visit_BIT(self, type_, **kw): 

3261 if type_.varying: 

3262 compiled = "BIT VARYING" 

3263 if type_.length is not None: 

3264 compiled += "(%d)" % type_.length 

3265 else: 

3266 compiled = "BIT(%d)" % type_.length 

3267 return compiled 

3268 

3269 def visit_uuid(self, type_, **kw): 

3270 if type_.native_uuid: 

3271 return self.visit_UUID(type_, **kw) 

3272 else: 

3273 return super().visit_uuid(type_, **kw) 

3274 

3275 def visit_UUID(self, type_, **kw): 

3276 return "UUID" 

3277 

3278 def visit_large_binary(self, type_, **kw): 

3279 return self.visit_BYTEA(type_, **kw) 

3280 

3281 def visit_BYTEA(self, type_, **kw): 

3282 return "BYTEA" 

3283 

3284 def visit_ARRAY(self, type_, **kw): 

3285 inner = self.process(type_.item_type, **kw) 

3286 return re.sub( 

3287 r"((?: COLLATE.*)?)$", 

3288 ( 

3289 r"%s\1" 

3290 % ( 

3291 "[]" 

3292 * (type_.dimensions if type_.dimensions is not None else 1) 

3293 ) 

3294 ), 

3295 inner, 

3296 count=1, 

3297 ) 

3298 

3299 def visit_json_path(self, type_, **kw): 

3300 return self.visit_JSONPATH(type_, **kw) 

3301 

3302 def visit_JSONPATH(self, type_, **kw): 

3303 return "JSONPATH" 

3304 

3305 

3306class _CompilerSequence: 

3307 """Minimal stand-in for :class:`.Sequence`. 

3308 

3309 Used for the implicit sequence behind a SERIAL column, where no 

3310 :class:`.Sequence` object exists but a name still has to be rendered 

3311 through :meth:`.IdentifierPreparer.format_sequence`. 

3312 

3313 """ 

3314 

3315 __slots__ = ("name", "schema") 

3316 

3317 # the schema handed to us has already been resolved against the 

3318 # schema translate map, so don't let format_sequence() translate it 

3319 # a second time 

3320 _use_schema_map = False 

3321 

3322 def __init__(self, name, schema=None): 

3323 self.name = name 

3324 self.schema = schema 

3325 

3326 

3327class PGIdentifierPreparer(compiler.IdentifierPreparer): 

3328 reserved_words = RESERVED_WORDS 

3329 

3330 def _format_sequence_string_literal(self, sequence): 

3331 """Render a sequence name as a SQL string literal. 

3332 

3333 ``nextval()`` and friends take the sequence as a string rather than 

3334 as an identifier, so the quoted identifier produced by 

3335 :meth:`.format_sequence` has to be escaped a second time for the 

3336 enclosing literal. 

3337 

3338 """ 

3339 return "'%s'" % self.format_sequence( 

3340 sequence, use_schema=True 

3341 ).replace("'", "''") 

3342 

3343 def _unquote_identifier(self, value): 

3344 if value[0] == self.initial_quote: 

3345 value = value[1:-1].replace( 

3346 self.escape_to_quote, self.escape_quote 

3347 ) 

3348 return value 

3349 

3350 def format_type(self, type_, use_schema=True): 

3351 if not type_.name: 

3352 raise exc.CompileError( 

3353 f"PostgreSQL {type_.__class__.__name__} type requires a name." 

3354 ) 

3355 

3356 name = self.quote(type_.name) 

3357 effective_schema = self.schema_for_object(type_) 

3358 

3359 # a built-in type with the same name will obscure this type, so raise 

3360 # for that case. this applies really to any visible type with the same 

3361 # name in any other visible schema that would not be appropriate for 

3362 # us to check against, so this is not a robust check, but 

3363 # at least do something for an obvious built-in name conflict 

3364 if ( 

3365 effective_schema is None 

3366 and type_.name in self.dialect.ischema_names 

3367 ): 

3368 raise exc.CompileError( 

3369 f"{type_!r} has name " 

3370 f"'{type_.name}' that matches an existing type, and " 

3371 "requires an explicit schema name in order to be rendered " 

3372 "in DDL." 

3373 ) 

3374 

3375 if ( 

3376 not self.omit_schema 

3377 and use_schema 

3378 and effective_schema is not None 

3379 ): 

3380 name = f"{self.quote_schema(effective_schema)}.{name}" 

3381 return name 

3382 

3383 

3384class ReflectedNamedType(TypedDict): 

3385 """Represents a reflected named type.""" 

3386 

3387 name: str 

3388 """Name of the type.""" 

3389 schema: str 

3390 """The schema of the type.""" 

3391 visible: bool 

3392 """Indicates if this type is in the current search path.""" 

3393 

3394 

3395class ReflectedDomainConstraint(TypedDict): 

3396 """Represents a reflect check constraint of a domain.""" 

3397 

3398 name: str 

3399 """Name of the constraint.""" 

3400 check: str 

3401 """The check constraint text.""" 

3402 

3403 

3404class ReflectedDomain(ReflectedNamedType): 

3405 """Represents a reflected enum.""" 

3406 

3407 type: str 

3408 """The string name of the underlying data type of the domain.""" 

3409 nullable: bool 

3410 """Indicates if the domain allows null or not.""" 

3411 default: Optional[str] 

3412 """The string representation of the default value of this domain 

3413 or ``None`` if none present. 

3414 """ 

3415 constraints: List[ReflectedDomainConstraint] 

3416 """The constraints defined in the domain, if any. 

3417 The constraint are in order of evaluation by postgresql. 

3418 """ 

3419 collation: Optional[str] 

3420 """The collation for the domain.""" 

3421 collation_schema: Optional[str] 

3422 """The schema of the collation for the domain, if the collation is not 

3423 visible on the current search_path.""" 

3424 

3425 

3426class ReflectedEnum(ReflectedNamedType): 

3427 """Represents a reflected enum.""" 

3428 

3429 labels: List[str] 

3430 """The labels that compose the enum.""" 

3431 

3432 

3433class PGInspector(reflection.Inspector): 

3434 dialect: PGDialect 

3435 

3436 def get_table_oid( 

3437 self, table_name: str, schema: Optional[str] = None 

3438 ) -> int: 

3439 """Return the OID for the given table name. 

3440 

3441 :param table_name: string name of the table. For special quoting, 

3442 use :class:`.quoted_name`. 

3443 

3444 :param schema: string schema name; if omitted, uses the default schema 

3445 of the database connection. For special quoting, 

3446 use :class:`.quoted_name`. 

3447 

3448 """ 

3449 

3450 with self._operation_context() as conn: 

3451 return self.dialect.get_table_oid( 

3452 conn, table_name, schema, info_cache=self.info_cache 

3453 ) 

3454 

3455 def get_domains( 

3456 self, schema: Optional[str] = None 

3457 ) -> List[ReflectedDomain]: 

3458 """Return a list of DOMAIN objects. 

3459 

3460 Each member is a dictionary containing these fields: 

3461 

3462 * name - name of the domain 

3463 * schema - the schema name for the domain. 

3464 * visible - boolean, whether or not this domain is visible 

3465 in the default search path. 

3466 * type - the type defined by this domain. 

3467 * nullable - Indicates if this domain can be ``NULL``. 

3468 * default - The default value of the domain or ``None`` if the 

3469 domain has no default. 

3470 * constraints - A list of dict with the constraint defined by this 

3471 domain. Each element contains two keys: ``name`` of the 

3472 constraint and ``check`` with the constraint text. 

3473 

3474 :param schema: schema name. If None, the default schema 

3475 (typically 'public') is used. May also be set to ``'*'`` to 

3476 indicate load domains for all schemas. 

3477 

3478 .. versionadded:: 2.0 

3479 

3480 """ 

3481 with self._operation_context() as conn: 

3482 return self.dialect._load_domains( 

3483 conn, schema, info_cache=self.info_cache 

3484 ) 

3485 

3486 def get_enums(self, schema: Optional[str] = None) -> List[ReflectedEnum]: 

3487 """Return a list of ENUM objects. 

3488 

3489 Each member is a dictionary containing these fields: 

3490 

3491 * name - name of the enum 

3492 * schema - the schema name for the enum. 

3493 * visible - boolean, whether or not this enum is visible 

3494 in the default search path. 

3495 * labels - a list of string labels that apply to the enum. 

3496 

3497 :param schema: schema name. If None, the default schema 

3498 (typically 'public') is used. May also be set to ``'*'`` to 

3499 indicate load enums for all schemas. 

3500 

3501 """ 

3502 with self._operation_context() as conn: 

3503 return self.dialect._load_enums( 

3504 conn, schema, info_cache=self.info_cache 

3505 ) 

3506 

3507 def get_foreign_table_names( 

3508 self, schema: Optional[str] = None 

3509 ) -> List[str]: 

3510 """Return a list of FOREIGN TABLE names. 

3511 

3512 Behavior is similar to that of 

3513 :meth:`_reflection.Inspector.get_table_names`, 

3514 except that the list is limited to those tables that report a 

3515 ``relkind`` value of ``f``. 

3516 

3517 """ 

3518 with self._operation_context() as conn: 

3519 return self.dialect._get_foreign_table_names( 

3520 conn, schema, info_cache=self.info_cache 

3521 ) 

3522 

3523 def has_type( 

3524 self, type_name: str, schema: Optional[str] = None, **kw: Any 

3525 ) -> bool: 

3526 """Return if the database has the specified type in the provided 

3527 schema. 

3528 

3529 :param type_name: the type to check. 

3530 :param schema: schema name. If None, the default schema 

3531 (typically 'public') is used. May also be set to ``'*'`` to 

3532 check in all schemas. 

3533 

3534 .. versionadded:: 2.0 

3535 

3536 """ 

3537 with self._operation_context() as conn: 

3538 return self.dialect.has_type( 

3539 conn, type_name, schema, info_cache=self.info_cache 

3540 ) 

3541 

3542 

3543class PGExecutionContext(default.DefaultExecutionContext): 

3544 def fire_sequence(self, seq, type_): 

3545 return self._execute_scalar( 

3546 ( 

3547 "select nextval(%s)" 

3548 % self.identifier_preparer._format_sequence_string_literal(seq) 

3549 ), 

3550 type_, 

3551 ) 

3552 

3553 def get_insert_default(self, column): 

3554 if column.primary_key and column is column.table._autoincrement_column: 

3555 if column.server_default and column.server_default.has_argument: 

3556 # pre-execute passive defaults on primary key columns 

3557 return self._execute_scalar( 

3558 "select %s" % column.server_default.arg, column.type 

3559 ) 

3560 

3561 elif column.default is None or ( 

3562 column.default.is_sequence and column.default.optional 

3563 ): 

3564 # execute the sequence associated with a SERIAL primary 

3565 # key column. for non-primary-key SERIAL, the ID just 

3566 # generates server side. 

3567 

3568 try: 

3569 seq_name = column._postgresql_seq_name 

3570 except AttributeError: 

3571 tab = column.table.name 

3572 col = column.name 

3573 tab = tab[0 : 29 + max(0, (29 - len(col)))] 

3574 col = col[0 : 29 + max(0, (29 - len(tab)))] 

3575 name = "%s_%s_seq" % (tab, col) 

3576 column._postgresql_seq_name = seq_name = name 

3577 

3578 if column.table is not None: 

3579 effective_schema = self.connection.schema_for_object( 

3580 column.table 

3581 ) 

3582 else: 

3583 effective_schema = None 

3584 

3585 exc = ( 

3586 "select nextval(%s)" 

3587 % self.identifier_preparer._format_sequence_string_literal( 

3588 _CompilerSequence(seq_name, effective_schema) 

3589 ) 

3590 ) 

3591 

3592 return self._execute_scalar(exc, column.type) 

3593 

3594 return super().get_insert_default(column) 

3595 

3596 

3597class PGReadOnlyConnectionCharacteristic( 

3598 characteristics.ConnectionCharacteristic 

3599): 

3600 transactional = True 

3601 

3602 def reset_characteristic(self, dialect, dbapi_conn): 

3603 dialect.set_readonly(dbapi_conn, False) 

3604 

3605 def set_characteristic(self, dialect, dbapi_conn, value): 

3606 dialect.set_readonly(dbapi_conn, value) 

3607 

3608 def get_characteristic(self, dialect, dbapi_conn): 

3609 return dialect.get_readonly(dbapi_conn) 

3610 

3611 

3612class PGDeferrableConnectionCharacteristic( 

3613 characteristics.ConnectionCharacteristic 

3614): 

3615 transactional = True 

3616 

3617 def reset_characteristic(self, dialect, dbapi_conn): 

3618 dialect.set_deferrable(dbapi_conn, False) 

3619 

3620 def set_characteristic(self, dialect, dbapi_conn, value): 

3621 dialect.set_deferrable(dbapi_conn, value) 

3622 

3623 def get_characteristic(self, dialect, dbapi_conn): 

3624 return dialect.get_deferrable(dbapi_conn) 

3625 

3626 

3627class PGDialect(default._BackendsMultiReflection, default.DefaultDialect): 

3628 name = "postgresql" 

3629 supports_statement_cache = True 

3630 supports_alter = True 

3631 max_identifier_length = 63 

3632 supports_sane_rowcount = True 

3633 

3634 bind_typing = interfaces.BindTyping.RENDER_CASTS 

3635 

3636 supports_native_enum = True 

3637 supports_native_boolean = True 

3638 supports_native_uuid = True 

3639 supports_smallserial = True 

3640 supports_virtual_generated_columns = True 

3641 

3642 supports_sequences = True 

3643 sequences_optional = True 

3644 preexecute_autoincrement_sequences = True 

3645 postfetch_lastrowid = False 

3646 use_insertmanyvalues = True 

3647 

3648 returns_native_bytes = True 

3649 

3650 insertmanyvalues_implicit_sentinel = ( 

3651 InsertmanyvaluesSentinelOpts.ANY_AUTOINCREMENT 

3652 | InsertmanyvaluesSentinelOpts.USE_INSERT_FROM_SELECT 

3653 | InsertmanyvaluesSentinelOpts.RENDER_SELECT_COL_CASTS 

3654 ) 

3655 

3656 supports_comments = True 

3657 supports_constraint_comments = True 

3658 supports_default_values = True 

3659 

3660 supports_default_metavalue = True 

3661 

3662 supports_empty_insert = False 

3663 supports_multivalues_insert = True 

3664 

3665 supports_identity_columns = True 

3666 

3667 default_paramstyle = "pyformat" 

3668 ischema_names = ischema_names 

3669 colspecs = colspecs 

3670 

3671 statement_compiler = PGCompiler 

3672 ddl_compiler = PGDDLCompiler 

3673 type_compiler_cls = PGTypeCompiler 

3674 preparer = PGIdentifierPreparer 

3675 execution_ctx_cls = PGExecutionContext 

3676 inspector = PGInspector 

3677 

3678 update_returning = True 

3679 delete_returning = True 

3680 insert_returning = True 

3681 update_returning_multifrom = True 

3682 delete_returning_multifrom = True 

3683 

3684 connection_characteristics = ( 

3685 default.DefaultDialect.connection_characteristics 

3686 ) 

3687 connection_characteristics = connection_characteristics.union( 

3688 { 

3689 "postgresql_readonly": PGReadOnlyConnectionCharacteristic(), 

3690 "postgresql_deferrable": PGDeferrableConnectionCharacteristic(), 

3691 } 

3692 ) 

3693 

3694 construct_arguments = [ 

3695 ( 

3696 schema.Index, 

3697 { 

3698 "using": False, 

3699 "include": None, 

3700 "where": None, 

3701 "ops": {}, 

3702 "concurrently": False, 

3703 "with": {}, 

3704 "tablespace": None, 

3705 "nulls_not_distinct": None, 

3706 "invalid": DialectKWArgConst.REFLECTED_ONLY, 

3707 }, 

3708 ), 

3709 ( 

3710 schema.Table, 

3711 { 

3712 "ignore_search_path": False, 

3713 "tablespace": None, 

3714 "partition_by": None, 

3715 "with_oids": None, 

3716 "with": None, 

3717 "on_commit": None, 

3718 "inherits": None, 

3719 "using": None, 

3720 }, 

3721 ), 

3722 ( 

3723 schema.CheckConstraint, 

3724 { 

3725 "not_valid": False, 

3726 }, 

3727 ), 

3728 ( 

3729 schema.ForeignKeyConstraint, 

3730 { 

3731 "not_valid": False, 

3732 }, 

3733 ), 

3734 ( 

3735 schema.PrimaryKeyConstraint, 

3736 {"include": None}, 

3737 ), 

3738 ( 

3739 schema.UniqueConstraint, 

3740 { 

3741 "include": None, 

3742 "nulls_not_distinct": None, 

3743 }, 

3744 ), 

3745 ( 

3746 schema.CreateView, 

3747 { 

3748 "with": None, 

3749 }, 

3750 ), 

3751 ] 

3752 

3753 reflection_options = ("postgresql_ignore_search_path",) 

3754 

3755 _backslash_escapes = False 

3756 _supports_create_index_concurrently = True 

3757 _supports_drop_index_concurrently = True 

3758 _supports_jsonb_subscripting = True 

3759 _pg_am_btree_oid = -1 

3760 

3761 def __init__( 

3762 self, 

3763 native_inet_types=None, 

3764 json_serializer=None, 

3765 json_deserializer=None, 

3766 **kwargs, 

3767 ): 

3768 default.DefaultDialect.__init__(self, **kwargs) 

3769 

3770 self._native_inet_types = native_inet_types 

3771 self._json_deserializer = json_deserializer 

3772 self._json_serializer = json_serializer 

3773 

3774 def initialize(self, connection): 

3775 super().initialize(connection) 

3776 

3777 # https://www.postgresql.org/docs/9.3/static/release-9-2.html#AEN116689 

3778 self.supports_smallserial = self.server_version_info >= (9, 2) 

3779 

3780 self._set_backslash_escapes(connection) 

3781 

3782 self._supports_drop_index_concurrently = self.server_version_info >= ( 

3783 9, 

3784 2, 

3785 ) 

3786 self.supports_identity_columns = self.server_version_info >= (10,) 

3787 

3788 self._supports_jsonb_subscripting = self.server_version_info >= (14,) 

3789 

3790 self.supports_virtual_generated_columns = self.server_version_info >= ( 

3791 18, 

3792 ) 

3793 

3794 def get_isolation_level_values(self, dbapi_conn): 

3795 # note the generic dialect doesn't have AUTOCOMMIT, however 

3796 # all postgresql dialects should include AUTOCOMMIT. 

3797 return ( 

3798 "SERIALIZABLE", 

3799 "READ UNCOMMITTED", 

3800 "READ COMMITTED", 

3801 "REPEATABLE READ", 

3802 ) 

3803 

3804 def set_isolation_level(self, dbapi_connection, level): 

3805 cursor = dbapi_connection.cursor() 

3806 cursor.execute( 

3807 "SET SESSION CHARACTERISTICS AS TRANSACTION " 

3808 f"ISOLATION LEVEL {level}" 

3809 ) 

3810 cursor.execute("COMMIT") 

3811 cursor.close() 

3812 

3813 def get_isolation_level(self, dbapi_connection): 

3814 cursor = dbapi_connection.cursor() 

3815 cursor.execute("show transaction isolation level") 

3816 val = cursor.fetchone()[0] 

3817 cursor.close() 

3818 return val.upper() 

3819 

3820 def set_readonly(self, connection, value): 

3821 raise NotImplementedError() 

3822 

3823 def get_readonly(self, connection): 

3824 raise NotImplementedError() 

3825 

3826 def set_deferrable(self, connection, value): 

3827 raise NotImplementedError() 

3828 

3829 def get_deferrable(self, connection): 

3830 raise NotImplementedError() 

3831 

3832 def _split_multihost_from_url(self, url: URL) -> Union[ 

3833 Tuple[None, None], 

3834 Tuple[Tuple[Optional[str], ...], Tuple[Optional[int], ...]], 

3835 ]: 

3836 hosts: Optional[Tuple[Optional[str], ...]] = None 

3837 ports_str: Union[str, Tuple[Optional[str], ...], None] = None 

3838 

3839 integrated_multihost = False 

3840 

3841 if "host" in url.query: 

3842 if isinstance(url.query["host"], (list, tuple)): 

3843 integrated_multihost = True 

3844 hosts, ports_str = zip( 

3845 *[ 

3846 token.split(":") if ":" in token else (token, None) 

3847 for token in url.query["host"] 

3848 ] 

3849 ) 

3850 

3851 elif isinstance(url.query["host"], str): 

3852 hosts = tuple(url.query["host"].split(",")) 

3853 

3854 if ( 

3855 "port" not in url.query 

3856 and len(hosts) == 1 

3857 and ":" in hosts[0] 

3858 ): 

3859 # internet host is alphanumeric plus dots or hyphens. 

3860 # this is essentially rfc1123, which refers to rfc952. 

3861 # https://stackoverflow.com/questions/3523028/ 

3862 # valid-characters-of-a-hostname 

3863 host_port_match = re.match( 

3864 r"^([a-zA-Z0-9\-\.]*)(?:\:(\d*))?$", hosts[0] 

3865 ) 

3866 if host_port_match: 

3867 integrated_multihost = True 

3868 h, p = host_port_match.group(1, 2) 

3869 if TYPE_CHECKING: 

3870 assert isinstance(h, str) 

3871 assert isinstance(p, str) 

3872 hosts = (h,) 

3873 ports_str = cast( 

3874 "Tuple[Optional[str], ...]", (p,) if p else (None,) 

3875 ) 

3876 

3877 if "port" in url.query: 

3878 if integrated_multihost: 

3879 raise exc.ArgumentError( 

3880 "Can't mix 'multihost' formats together; use " 

3881 '"host=h1,h2,h3&port=p1,p2,p3" or ' 

3882 '"host=h1:p1&host=h2:p2&host=h3:p3" separately' 

3883 ) 

3884 if isinstance(url.query["port"], (list, tuple)): 

3885 ports_str = url.query["port"] 

3886 elif isinstance(url.query["port"], str): 

3887 ports_str = tuple(url.query["port"].split(",")) 

3888 

3889 ports: Optional[Tuple[Optional[int], ...]] = None 

3890 

3891 if ports_str: 

3892 try: 

3893 ports = tuple(int(x) if x else None for x in ports_str) 

3894 except ValueError: 

3895 raise exc.ArgumentError( 

3896 f"Received non-integer port arguments: {ports_str}" 

3897 ) from None 

3898 

3899 if ports and ( 

3900 (not hosts and len(ports) > 1) 

3901 or ( 

3902 hosts 

3903 and ports 

3904 and len(hosts) != len(ports) 

3905 and (len(hosts) > 1 or len(ports) > 1) 

3906 ) 

3907 ): 

3908 raise exc.ArgumentError("number of hosts and ports don't match") 

3909 

3910 if hosts is not None: 

3911 if ports is None: 

3912 ports = tuple(None for _ in hosts) 

3913 

3914 return hosts, ports # type: ignore 

3915 

3916 def do_begin_twophase(self, connection, xid): 

3917 self.do_begin(connection.connection) 

3918 

3919 def do_prepare_twophase(self, connection, xid): 

3920 connection.execute( 

3921 sql.text("PREPARE TRANSACTION :xid").bindparams( 

3922 sql.bindparam("xid", xid, literal_execute=True) 

3923 ) 

3924 ) 

3925 

3926 def do_rollback_twophase( 

3927 self, connection, xid, is_prepared=True, recover=False 

3928 ): 

3929 if is_prepared: 

3930 if recover: 

3931 # FIXME: ugly hack to get out of transaction 

3932 # context when committing recoverable transactions 

3933 # Must find out a way how to make the dbapi not 

3934 # open a transaction. 

3935 connection.exec_driver_sql("ROLLBACK") 

3936 connection.execute( 

3937 sql.text("ROLLBACK PREPARED :xid").bindparams( 

3938 sql.bindparam("xid", xid, literal_execute=True) 

3939 ) 

3940 ) 

3941 connection.exec_driver_sql("BEGIN") 

3942 self.do_rollback(connection.connection) 

3943 else: 

3944 self.do_rollback(connection.connection) 

3945 

3946 def do_commit_twophase( 

3947 self, connection, xid, is_prepared=True, recover=False 

3948 ): 

3949 if is_prepared: 

3950 if recover: 

3951 connection.exec_driver_sql("ROLLBACK") 

3952 connection.execute( 

3953 sql.text("COMMIT PREPARED :xid").bindparams( 

3954 sql.bindparam("xid", xid, literal_execute=True) 

3955 ) 

3956 ) 

3957 connection.exec_driver_sql("BEGIN") 

3958 self.do_rollback(connection.connection) 

3959 else: 

3960 self.do_commit(connection.connection) 

3961 

3962 def do_recover_twophase(self, connection): 

3963 return connection.scalars( 

3964 sql.text("SELECT gid FROM pg_prepared_xacts") 

3965 ).all() 

3966 

3967 def _get_default_schema_name(self, connection): 

3968 return connection.exec_driver_sql("select current_schema()").scalar() 

3969 

3970 @reflection.cache 

3971 def has_schema(self, connection, schema, **kw): 

3972 query = select(pg_catalog.pg_namespace.c.nspname).where( 

3973 pg_catalog.pg_namespace.c.nspname == schema 

3974 ) 

3975 return bool(connection.scalar(query)) 

3976 

3977 def _pg_class_filter_scope_schema( 

3978 self, query, schema, scope, pg_class_table=None 

3979 ): 

3980 if pg_class_table is None: 

3981 pg_class_table = pg_catalog.pg_class 

3982 query = query.join( 

3983 pg_catalog.pg_namespace, 

3984 pg_catalog.pg_namespace.c.oid == pg_class_table.c.relnamespace, 

3985 ) 

3986 

3987 if scope is ObjectScope.DEFAULT: 

3988 query = query.where(pg_class_table.c.relpersistence != "t") 

3989 elif scope is ObjectScope.TEMPORARY: 

3990 query = query.where(pg_class_table.c.relpersistence == "t") 

3991 

3992 if schema is None: 

3993 query = query.where( 

3994 pg_catalog.pg_table_is_visible(pg_class_table.c.oid), 

3995 # ignore pg_catalog schema 

3996 pg_catalog.pg_namespace.c.nspname != "pg_catalog", 

3997 ) 

3998 else: 

3999 query = query.where(pg_catalog.pg_namespace.c.nspname == schema) 

4000 return query 

4001 

4002 def _pg_class_relkind_condition(self, relkinds, pg_class_table=None): 

4003 if pg_class_table is None: 

4004 pg_class_table = pg_catalog.pg_class 

4005 # uses the any form instead of in otherwise postgresql complaings 

4006 # that 'IN could not convert type character to "char"' 

4007 return pg_class_table.c.relkind == sql.any_(_array.array(relkinds)) 

4008 

4009 @lru_cache() 

4010 def _has_multi_table_query(self, schema): 

4011 query = select(pg_catalog.pg_class.c.relname).where( 

4012 pg_catalog.pg_class.c.relname.in_( 

4013 bindparam("table_names", expanding=True) 

4014 ), 

4015 self._pg_class_relkind_condition( 

4016 pg_catalog.RELKINDS_ALL_TABLE_LIKE 

4017 ), 

4018 ) 

4019 return self._pg_class_filter_scope_schema( 

4020 query, schema, scope=ObjectScope.ANY 

4021 ) 

4022 

4023 @reflection.cache 

4024 def has_table(self, connection, table_name, schema=None, **kw): 

4025 # NOTE: it's not worth calling into the multi table since the query 

4026 # is compatible also with this single case. 

4027 self._ensure_has_table_connection(connection) 

4028 query = self._has_multi_table_query(schema) 

4029 return bool(connection.scalar(query, {"table_names": [table_name]})) 

4030 

4031 def has_multi_table(self, connection, table_names, schema=None, **kw): 

4032 query = self._has_multi_table_query(schema) 

4033 params = {"table_names": table_names} 

4034 existing = set(connection.scalars(query, params).all()) 

4035 retval = {(schema, table): table in existing for table in table_names} 

4036 return retval.items() 

4037 

4038 @reflection.cache 

4039 def has_sequence(self, connection, sequence_name, schema=None, **kw): 

4040 query = select(pg_catalog.pg_class.c.relname).where( 

4041 pg_catalog.pg_class.c.relkind == "S", 

4042 pg_catalog.pg_class.c.relname == sequence_name, 

4043 ) 

4044 query = self._pg_class_filter_scope_schema( 

4045 query, schema, scope=ObjectScope.ANY 

4046 ) 

4047 return bool(connection.scalar(query)) 

4048 

4049 @reflection.cache 

4050 def has_type(self, connection, type_name, schema=None, **kw): 

4051 query = ( 

4052 select(pg_catalog.pg_type.c.typname) 

4053 .join( 

4054 pg_catalog.pg_namespace, 

4055 pg_catalog.pg_namespace.c.oid 

4056 == pg_catalog.pg_type.c.typnamespace, 

4057 ) 

4058 .where(pg_catalog.pg_type.c.typname == type_name) 

4059 ) 

4060 if schema is None: 

4061 query = query.where( 

4062 pg_catalog.pg_type_is_visible(pg_catalog.pg_type.c.oid), 

4063 # ignore pg_catalog schema 

4064 pg_catalog.pg_namespace.c.nspname != "pg_catalog", 

4065 ) 

4066 elif schema != "*": 

4067 query = query.where(pg_catalog.pg_namespace.c.nspname == schema) 

4068 

4069 return bool(connection.scalar(query)) 

4070 

4071 def _get_server_version_info(self, connection): 

4072 v = connection.exec_driver_sql("select pg_catalog.version()").scalar() 

4073 m = re.match( 

4074 r".*(?:PostgreSQL|EnterpriseDB) " 

4075 r"(\d+)\.?(\d+)?(?:\.(\d+))?(?:\.\d+)?(?:devel|beta)?", 

4076 v, 

4077 ) 

4078 if not m: 

4079 raise AssertionError( 

4080 "Could not determine version from string '%s'" % v 

4081 ) 

4082 return tuple([int(x) for x in m.group(1, 2, 3) if x is not None]) 

4083 

4084 @reflection.cache 

4085 def get_table_oid(self, connection, table_name, schema=None, **kw): 

4086 """Fetch the oid for schema.table_name.""" 

4087 query = select(pg_catalog.pg_class.c.oid).where( 

4088 pg_catalog.pg_class.c.relname == table_name, 

4089 self._pg_class_relkind_condition( 

4090 pg_catalog.RELKINDS_ALL_TABLE_LIKE 

4091 ), 

4092 ) 

4093 query = self._pg_class_filter_scope_schema( 

4094 query, schema, scope=ObjectScope.ANY 

4095 ) 

4096 table_oid = connection.scalar(query) 

4097 if table_oid is None: 

4098 raise exc.NoSuchTableError( 

4099 f"{schema}.{table_name}" if schema else table_name 

4100 ) 

4101 return table_oid 

4102 

4103 @reflection.cache 

4104 def get_schema_names(self, connection, **kw): 

4105 query = ( 

4106 select(pg_catalog.pg_namespace.c.nspname) 

4107 .where( 

4108 ~pg_catalog.pg_namespace.c.nspname.startswith( 

4109 "pg_", autoescape=True 

4110 ) 

4111 ) 

4112 .order_by(pg_catalog.pg_namespace.c.nspname) 

4113 ) 

4114 return connection.scalars(query).all() 

4115 

4116 def _get_relnames_for_relkinds(self, connection, schema, relkinds, scope): 

4117 query = select(pg_catalog.pg_class.c.relname).where( 

4118 self._pg_class_relkind_condition(relkinds) 

4119 ) 

4120 query = self._pg_class_filter_scope_schema(query, schema, scope=scope) 

4121 return connection.scalars(query).all() 

4122 

4123 @reflection.cache 

4124 def get_table_names(self, connection, schema=None, **kw): 

4125 return self._get_relnames_for_relkinds( 

4126 connection, 

4127 schema, 

4128 pg_catalog.RELKINDS_TABLE_NO_FOREIGN, 

4129 scope=ObjectScope.DEFAULT, 

4130 ) 

4131 

4132 @reflection.cache 

4133 def get_temp_table_names(self, connection, **kw): 

4134 return self._get_relnames_for_relkinds( 

4135 connection, 

4136 schema=None, 

4137 relkinds=pg_catalog.RELKINDS_TABLE_NO_FOREIGN, 

4138 scope=ObjectScope.TEMPORARY, 

4139 ) 

4140 

4141 @reflection.cache 

4142 def _get_foreign_table_names(self, connection, schema=None, **kw): 

4143 return self._get_relnames_for_relkinds( 

4144 connection, schema, relkinds=("f",), scope=ObjectScope.ANY 

4145 ) 

4146 

4147 @reflection.cache 

4148 def get_view_names(self, connection, schema=None, **kw): 

4149 return self._get_relnames_for_relkinds( 

4150 connection, 

4151 schema, 

4152 pg_catalog.RELKINDS_VIEW, 

4153 scope=ObjectScope.DEFAULT, 

4154 ) 

4155 

4156 @reflection.cache 

4157 def get_materialized_view_names(self, connection, schema=None, **kw): 

4158 return self._get_relnames_for_relkinds( 

4159 connection, 

4160 schema, 

4161 pg_catalog.RELKINDS_MAT_VIEW, 

4162 scope=ObjectScope.DEFAULT, 

4163 ) 

4164 

4165 @reflection.cache 

4166 def get_temp_view_names(self, connection, schema=None, **kw): 

4167 return self._get_relnames_for_relkinds( 

4168 connection, 

4169 schema, 

4170 # NOTE: do not include temp materialzied views (that do not 

4171 # seem to be a thing at least up to version 14) 

4172 pg_catalog.RELKINDS_VIEW, 

4173 scope=ObjectScope.TEMPORARY, 

4174 ) 

4175 

4176 @reflection.cache 

4177 def get_sequence_names(self, connection, schema=None, **kw): 

4178 return self._get_relnames_for_relkinds( 

4179 connection, schema, relkinds=("S",), scope=ObjectScope.ANY 

4180 ) 

4181 

4182 @reflection.cache 

4183 def get_view_definition(self, connection, view_name, schema=None, **kw): 

4184 query = ( 

4185 select(pg_catalog.pg_get_viewdef(pg_catalog.pg_class.c.oid)) 

4186 .select_from(pg_catalog.pg_class) 

4187 .where( 

4188 pg_catalog.pg_class.c.relname == view_name, 

4189 self._pg_class_relkind_condition( 

4190 pg_catalog.RELKINDS_VIEW + pg_catalog.RELKINDS_MAT_VIEW 

4191 ), 

4192 ) 

4193 ) 

4194 query = self._pg_class_filter_scope_schema( 

4195 query, schema, scope=ObjectScope.ANY 

4196 ) 

4197 res = connection.scalar(query) 

4198 if res is None: 

4199 raise exc.NoSuchTableError( 

4200 f"{schema}.{view_name}" if schema else view_name 

4201 ) 

4202 else: 

4203 return res 

4204 

4205 def _prepare_filter_names(self, filter_names): 

4206 if filter_names: 

4207 return True, {"filter_names": filter_names} 

4208 else: 

4209 return False, {} 

4210 

4211 def _kind_to_relkinds(self, kind: ObjectKind) -> Tuple[str, ...]: 

4212 if kind is ObjectKind.ANY: 

4213 return pg_catalog.RELKINDS_ALL_TABLE_LIKE 

4214 relkinds = () 

4215 if ObjectKind.TABLE in kind: 

4216 relkinds += pg_catalog.RELKINDS_TABLE 

4217 if ObjectKind.VIEW in kind: 

4218 relkinds += pg_catalog.RELKINDS_VIEW 

4219 if ObjectKind.MATERIALIZED_VIEW in kind: 

4220 relkinds += pg_catalog.RELKINDS_MAT_VIEW 

4221 return relkinds 

4222 

4223 @lru_cache() 

4224 def _table_options_query(self, schema, has_filter_names, scope, kind): 

4225 inherits_sq = ( 

4226 select( 

4227 pg_catalog.pg_inherits.c.inhrelid, 

4228 sql.func.array_agg( 

4229 aggregate_order_by( 

4230 pg_catalog.pg_class.c.relname, 

4231 pg_catalog.pg_inherits.c.inhseqno, 

4232 ) 

4233 ).label("parent_table_names"), 

4234 ) 

4235 .select_from(pg_catalog.pg_inherits) 

4236 .join( 

4237 pg_catalog.pg_class, 

4238 pg_catalog.pg_inherits.c.inhparent 

4239 == pg_catalog.pg_class.c.oid, 

4240 ) 

4241 .group_by(pg_catalog.pg_inherits.c.inhrelid) 

4242 .subquery("inherits") 

4243 ) 

4244 

4245 if self.server_version_info < (12,): 

4246 # this is not in the pg_catalog.pg_class since it was 

4247 # removed in PostgreSQL version 12 

4248 has_oids = sql.column("relhasoids", BOOLEAN) 

4249 else: 

4250 has_oids = sql.null().label("relhasoids") 

4251 

4252 relkinds = self._kind_to_relkinds(kind) 

4253 query = ( 

4254 select( 

4255 pg_catalog.pg_class.c.oid, 

4256 pg_catalog.pg_class.c.relname, 

4257 pg_catalog.pg_class.c.reloptions, 

4258 has_oids, 

4259 sql.case( 

4260 ( 

4261 sql.and_( 

4262 pg_catalog.pg_am.c.amname.is_not(None), 

4263 pg_catalog.pg_am.c.amname 

4264 != sql.func.current_setting( 

4265 "default_table_access_method" 

4266 ), 

4267 ), 

4268 pg_catalog.pg_am.c.amname, 

4269 ), 

4270 else_=sql.null(), 

4271 ).label("access_method_name"), 

4272 pg_catalog.pg_tablespace.c.spcname.label("tablespace_name"), 

4273 inherits_sq.c.parent_table_names, 

4274 ) 

4275 .select_from(pg_catalog.pg_class) 

4276 .outerjoin( 

4277 # NOTE: on postgresql < 12, this could be avoided 

4278 # since relam is always 0 so nothing is joined. 

4279 pg_catalog.pg_am, 

4280 pg_catalog.pg_class.c.relam == pg_catalog.pg_am.c.oid, 

4281 ) 

4282 .outerjoin( 

4283 inherits_sq, 

4284 pg_catalog.pg_class.c.oid == inherits_sq.c.inhrelid, 

4285 ) 

4286 .outerjoin( 

4287 pg_catalog.pg_tablespace, 

4288 pg_catalog.pg_tablespace.c.oid 

4289 == pg_catalog.pg_class.c.reltablespace, 

4290 ) 

4291 .where(self._pg_class_relkind_condition(relkinds)) 

4292 ) 

4293 query = self._pg_class_filter_scope_schema(query, schema, scope=scope) 

4294 if has_filter_names: 

4295 query = query.where( 

4296 pg_catalog.pg_class.c.relname.in_(bindparam("filter_names")) 

4297 ) 

4298 return query 

4299 

4300 def get_multi_table_options( 

4301 self, connection, schema, filter_names, scope, kind, **kw 

4302 ): 

4303 has_filter_names, params = self._prepare_filter_names(filter_names) 

4304 query = self._table_options_query( 

4305 schema, has_filter_names, scope, kind 

4306 ) 

4307 rows = connection.execute(query, params).mappings() 

4308 table_options = {} 

4309 

4310 for row in rows: 

4311 current: dict[str, Any] = {} 

4312 if row["access_method_name"] is not None: 

4313 current["postgresql_using"] = row["access_method_name"] 

4314 

4315 if row["parent_table_names"]: 

4316 current["postgresql_inherits"] = tuple( 

4317 row["parent_table_names"] 

4318 ) 

4319 

4320 if row["reloptions"]: 

4321 current["postgresql_with"] = dict( 

4322 option.split("=", 1) for option in row["reloptions"] 

4323 ) 

4324 

4325 if row["relhasoids"]: 

4326 current["postgresql_with_oids"] = True 

4327 

4328 if row["tablespace_name"] is not None: 

4329 current["postgresql_tablespace"] = row["tablespace_name"] 

4330 

4331 table_options[(schema, row["relname"])] = current 

4332 

4333 return table_options.items() 

4334 

4335 @lru_cache() 

4336 def _columns_query(self, schema, has_filter_names, scope, kind): 

4337 # NOTE: the query with the default and identity options scalar 

4338 # subquery is faster than trying to use outer joins for them 

4339 generated = ( 

4340 pg_catalog.pg_attribute.c.attgenerated.label("generated") 

4341 if self.server_version_info >= (12,) 

4342 else sql.null().label("generated") 

4343 ) 

4344 if self.server_version_info >= (10,): 

4345 # join lateral performs worse (~2x slower) than a scalar_subquery 

4346 # also the subquery can be run only if the column is an identity 

4347 identity = sql.case( 

4348 ( # attidentity != '' is required or it will reflect also 

4349 # serial columns as identity. 

4350 pg_catalog.pg_attribute.c.attidentity != "", 

4351 select( 

4352 sql.func.json_build_object( 

4353 "always", 

4354 pg_catalog.pg_attribute.c.attidentity == "a", 

4355 "start", 

4356 pg_catalog.pg_sequence.c.seqstart, 

4357 "increment", 

4358 pg_catalog.pg_sequence.c.seqincrement, 

4359 "minvalue", 

4360 pg_catalog.pg_sequence.c.seqmin, 

4361 "maxvalue", 

4362 pg_catalog.pg_sequence.c.seqmax, 

4363 "cache", 

4364 pg_catalog.pg_sequence.c.seqcache, 

4365 "cycle", 

4366 pg_catalog.pg_sequence.c.seqcycle, 

4367 type_=sqltypes.JSON(), 

4368 ) 

4369 ) 

4370 .select_from(pg_catalog.pg_sequence) 

4371 .where( 

4372 # not needed but pg seems to like it 

4373 pg_catalog.pg_attribute.c.attidentity != "", 

4374 pg_catalog.pg_sequence.c.seqrelid 

4375 == sql.cast( 

4376 sql.cast( 

4377 pg_catalog.pg_get_serial_sequence( 

4378 sql.cast( 

4379 sql.cast( 

4380 pg_catalog.pg_attribute.c.attrelid, 

4381 REGCLASS, 

4382 ), 

4383 TEXT, 

4384 ), 

4385 pg_catalog.pg_attribute.c.attname, 

4386 ), 

4387 REGCLASS, 

4388 ), 

4389 OID, 

4390 ), 

4391 ) 

4392 .correlate(pg_catalog.pg_attribute) 

4393 .scalar_subquery(), 

4394 ), 

4395 else_=sql.null(), 

4396 ).label("identity_options") 

4397 else: 

4398 identity = sql.null().label("identity_options") 

4399 

4400 # join lateral performs the same as scalar_subquery here, also 

4401 # the subquery can be run only if the column has a default 

4402 default = sql.case( 

4403 ( 

4404 pg_catalog.pg_attribute.c.atthasdef, 

4405 select( 

4406 pg_catalog.pg_get_expr( 

4407 pg_catalog.pg_attrdef.c.adbin, 

4408 pg_catalog.pg_attrdef.c.adrelid, 

4409 ) 

4410 ) 

4411 .select_from(pg_catalog.pg_attrdef) 

4412 .where( 

4413 # not needed but pg seems to like it 

4414 pg_catalog.pg_attribute.c.atthasdef, 

4415 pg_catalog.pg_attrdef.c.adrelid 

4416 == pg_catalog.pg_attribute.c.attrelid, 

4417 pg_catalog.pg_attrdef.c.adnum 

4418 == pg_catalog.pg_attribute.c.attnum, 

4419 ) 

4420 .correlate(pg_catalog.pg_attribute) 

4421 .scalar_subquery(), 

4422 ), 

4423 else_=sql.null(), 

4424 ).label("default") 

4425 

4426 # get the name and schema of the collate when it's different from 

4427 # the default one; the schema is only included when the collation 

4428 # is not visible on the current search_path 

4429 collate = sql.case( 

4430 ( 

4431 sql.and_( 

4432 pg_catalog.pg_attribute.c.attcollation != 0, 

4433 select(pg_catalog.pg_type.c.typcollation) 

4434 .where( 

4435 pg_catalog.pg_type.c.oid 

4436 == pg_catalog.pg_attribute.c.atttypid, 

4437 ) 

4438 .correlate(pg_catalog.pg_attribute) 

4439 .scalar_subquery() 

4440 != pg_catalog.pg_attribute.c.attcollation, 

4441 ), 

4442 select( 

4443 sql.func.json_build_object( 

4444 "name", 

4445 pg_catalog.pg_collation.c.collname, 

4446 "schema", 

4447 sql.case( 

4448 ( 

4449 pg_catalog.pg_collation_is_visible( 

4450 pg_catalog.pg_collation.c.oid 

4451 ), 

4452 sql.null(), 

4453 ), 

4454 else_=pg_catalog.pg_namespace.c.nspname, 

4455 ), 

4456 type_=sqltypes.JSON(), 

4457 ) 

4458 ) 

4459 .select_from(pg_catalog.pg_collation) 

4460 .join( 

4461 pg_catalog.pg_namespace, 

4462 pg_catalog.pg_namespace.c.oid 

4463 == pg_catalog.pg_collation.c.collnamespace, 

4464 ) 

4465 .where( 

4466 pg_catalog.pg_collation.c.oid 

4467 == pg_catalog.pg_attribute.c.attcollation 

4468 ) 

4469 .correlate(pg_catalog.pg_attribute) 

4470 .scalar_subquery(), 

4471 ), 

4472 else_=sql.null(), 

4473 ).label("collation") 

4474 

4475 relkinds = self._kind_to_relkinds(kind) 

4476 query = ( 

4477 select( 

4478 pg_catalog.pg_attribute.c.attname.label("name"), 

4479 pg_catalog.format_type( 

4480 pg_catalog.pg_attribute.c.atttypid, 

4481 pg_catalog.pg_attribute.c.atttypmod, 

4482 ).label("format_type"), 

4483 default, 

4484 pg_catalog.pg_attribute.c.attnotnull.label("not_null"), 

4485 pg_catalog.pg_class.c.relname.label("table_name"), 

4486 pg_catalog.pg_description.c.description.label("comment"), 

4487 generated, 

4488 identity, 

4489 collate, 

4490 ) 

4491 .select_from(pg_catalog.pg_class) 

4492 # NOTE: postgresql support table with no user column, meaning 

4493 # there is no row with pg_attribute.attnum > 0. use a left outer 

4494 # join to avoid filtering these tables. 

4495 .outerjoin( 

4496 pg_catalog.pg_attribute, 

4497 sql.and_( 

4498 pg_catalog.pg_class.c.oid 

4499 == pg_catalog.pg_attribute.c.attrelid, 

4500 pg_catalog.pg_attribute.c.attnum > 0, 

4501 ~pg_catalog.pg_attribute.c.attisdropped, 

4502 ), 

4503 ) 

4504 .outerjoin( 

4505 pg_catalog.pg_description, 

4506 sql.and_( 

4507 pg_catalog.pg_description.c.objoid 

4508 == pg_catalog.pg_attribute.c.attrelid, 

4509 pg_catalog.pg_description.c.objsubid 

4510 == pg_catalog.pg_attribute.c.attnum, 

4511 ), 

4512 ) 

4513 .where(self._pg_class_relkind_condition(relkinds)) 

4514 .order_by( 

4515 pg_catalog.pg_class.c.relname, pg_catalog.pg_attribute.c.attnum 

4516 ) 

4517 ) 

4518 query = self._pg_class_filter_scope_schema(query, schema, scope=scope) 

4519 if has_filter_names: 

4520 query = query.where( 

4521 pg_catalog.pg_class.c.relname.in_(bindparam("filter_names")) 

4522 ) 

4523 return query 

4524 

4525 def get_multi_columns( 

4526 self, connection, schema, filter_names, scope, kind, **kw 

4527 ): 

4528 has_filter_names, params = self._prepare_filter_names(filter_names) 

4529 query = self._columns_query(schema, has_filter_names, scope, kind) 

4530 rows = connection.execute(query, params).mappings() 

4531 

4532 named_type_loader = _NamedTypeLoader(self, connection, kw) 

4533 columns = self._get_columns_info(rows, named_type_loader, schema) 

4534 

4535 return columns.items() 

4536 

4537 _format_type_args_pattern = re.compile(r"\((.*)\)") 

4538 _format_type_args_delim = re.compile(r"\s*,\s*") 

4539 _format_array_spec_pattern = re.compile(r"((?:\[\])*)$") 

4540 

4541 def _reflect_type( 

4542 self, 

4543 format_type: Optional[str], 

4544 named_type_loader: _NamedTypeLoader, 

4545 type_description: str, 

4546 collation: Optional[str], 

4547 collation_schema: Optional[str] = None, 

4548 ) -> sqltypes.TypeEngine[Any]: 

4549 """ 

4550 Attempts to reconstruct a column type defined in ischema_names based 

4551 on the information available in the format_type. 

4552 

4553 If the `format_type` cannot be associated with a known `ischema_names`, 

4554 it is treated as a reference to a known PostgreSQL named `ENUM` or 

4555 `DOMAIN` type. 

4556 """ 

4557 type_description = type_description or "unknown type" 

4558 if format_type is None: 

4559 util.warn( 

4560 "PostgreSQL format_type() returned NULL for %s" 

4561 % type_description 

4562 ) 

4563 return sqltypes.NULLTYPE 

4564 

4565 attype_args_match = self._format_type_args_pattern.search(format_type) 

4566 if attype_args_match and attype_args_match.group(1): 

4567 attype_args = self._format_type_args_delim.split( 

4568 attype_args_match.group(1) 

4569 ) 

4570 else: 

4571 attype_args = () 

4572 

4573 match_array_dim = self._format_array_spec_pattern.search(format_type) 

4574 # Each "[]" in array specs corresponds to an array dimension 

4575 array_dim = len(match_array_dim.group(1) or "") // 2 

4576 

4577 # Remove all parameters and array specs from format_type to obtain an 

4578 # ischema_name candidate 

4579 attype = self._format_type_args_pattern.sub("", format_type) 

4580 attype = self._format_array_spec_pattern.sub("", attype) 

4581 

4582 schema_type = self.ischema_names.get(attype.lower(), None) 

4583 args, kwargs = (), {} 

4584 

4585 if attype == "numeric": 

4586 if len(attype_args) == 2: 

4587 precision, scale = map(int, attype_args) 

4588 args = (precision, scale) 

4589 

4590 elif attype == "double precision": 

4591 args = (53,) 

4592 

4593 elif attype == "integer": 

4594 args = () 

4595 

4596 elif attype in ("timestamp with time zone", "time with time zone"): 

4597 kwargs["timezone"] = True 

4598 if len(attype_args) == 1: 

4599 kwargs["precision"] = int(attype_args[0]) 

4600 

4601 elif attype in ( 

4602 "timestamp without time zone", 

4603 "time without time zone", 

4604 "time", 

4605 ): 

4606 kwargs["timezone"] = False 

4607 if len(attype_args) == 1: 

4608 kwargs["precision"] = int(attype_args[0]) 

4609 

4610 elif attype == "bit varying": 

4611 kwargs["varying"] = True 

4612 if len(attype_args) == 1: 

4613 charlen = int(attype_args[0]) 

4614 args = (charlen,) 

4615 

4616 # a domain or enum can start with interval, so be mindful of that. 

4617 elif attype == "interval" or attype.startswith("interval "): 

4618 schema_type = INTERVAL 

4619 

4620 field_match = re.match(r"interval (.+)", attype) 

4621 if field_match: 

4622 kwargs["fields"] = field_match.group(1) 

4623 

4624 if len(attype_args) == 1: 

4625 kwargs["precision"] = int(attype_args[0]) 

4626 

4627 else: 

4628 enum_or_domain_key = tuple(util.quoted_token_parser(attype)) 

4629 

4630 if ( 

4631 schema_type is None 

4632 and enum_or_domain_key in named_type_loader.enums 

4633 ): 

4634 schema_type = ENUM 

4635 enum = named_type_loader.enums[enum_or_domain_key] 

4636 

4637 kwargs["name"] = enum["name"] 

4638 

4639 if not enum["visible"]: 

4640 kwargs["schema"] = enum["schema"] 

4641 args = tuple(enum["labels"]) 

4642 elif ( 

4643 schema_type is None 

4644 and enum_or_domain_key in named_type_loader.domains 

4645 ): 

4646 schema_type = DOMAIN 

4647 domain = named_type_loader.domains[enum_or_domain_key] 

4648 

4649 data_type = self._reflect_type( 

4650 domain["type"], 

4651 named_type_loader, 

4652 type_description="DOMAIN '%s'" % domain["name"], 

4653 collation=domain["collation"], 

4654 collation_schema=domain["collation_schema"], 

4655 ) 

4656 args = (domain["name"], data_type) 

4657 

4658 kwargs["collation"] = domain["collation"] 

4659 kwargs["collation_schema"] = domain["collation_schema"] 

4660 kwargs["default"] = domain["default"] 

4661 kwargs["not_null"] = not domain["nullable"] 

4662 kwargs["create_type"] = False 

4663 

4664 if domain["constraints"]: 

4665 # We only support a single constraint 

4666 check_constraint = domain["constraints"][0] 

4667 

4668 kwargs["constraint_name"] = check_constraint["name"] 

4669 kwargs["check"] = check_constraint["check"] 

4670 

4671 if not domain["visible"]: 

4672 kwargs["schema"] = domain["schema"] 

4673 

4674 else: 

4675 try: 

4676 charlen = int(attype_args[0]) 

4677 args = (charlen, *attype_args[1:]) 

4678 except (ValueError, IndexError): 

4679 args = attype_args 

4680 

4681 if not schema_type: 

4682 util.warn( 

4683 "Did not recognize type '%s' of %s" 

4684 % (attype, type_description) 

4685 ) 

4686 return sqltypes.NULLTYPE 

4687 

4688 if collation is not None: 

4689 kwargs["collation"] = collation 

4690 if collation_schema is not None: 

4691 kwargs["collation_schema"] = collation_schema 

4692 

4693 data_type = schema_type(*args, **kwargs) 

4694 if array_dim >= 1: 

4695 # postgres does not preserve dimensionality or size of array types. 

4696 data_type = _array.ARRAY(data_type) 

4697 

4698 return data_type 

4699 

4700 def _get_columns_info(self, rows, named_type_loader, schema): 

4701 columns = defaultdict(list) 

4702 for row_dict in rows: 

4703 # ensure that each table has an entry, even if it has no columns 

4704 if row_dict["name"] is None: 

4705 columns[(schema, row_dict["table_name"])] = ( 

4706 ReflectionDefaults.columns() 

4707 ) 

4708 continue 

4709 table_cols = columns[(schema, row_dict["table_name"])] 

4710 

4711 collation_info = row_dict["collation"] 

4712 if collation_info is not None: 

4713 collation = collation_info["name"] 

4714 collation_schema = collation_info["schema"] 

4715 else: 

4716 collation = collation_schema = None 

4717 

4718 coltype = self._reflect_type( 

4719 row_dict["format_type"], 

4720 named_type_loader, 

4721 type_description="column '%s'" % row_dict["name"], 

4722 collation=collation, 

4723 collation_schema=collation_schema, 

4724 ) 

4725 

4726 default = row_dict["default"] 

4727 name = row_dict["name"] 

4728 generated = row_dict["generated"] 

4729 nullable = not row_dict["not_null"] 

4730 

4731 if isinstance(coltype, DOMAIN): 

4732 if not default: 

4733 # domain can override the default value but 

4734 # can't set it to None 

4735 if coltype.default is not None: 

4736 default = coltype.default 

4737 

4738 nullable = nullable and not coltype.not_null 

4739 

4740 identity = row_dict["identity_options"] 

4741 

4742 # If a zero byte or blank string depending on driver (is also 

4743 # absent for older PG versions), then not a generated column. 

4744 # Otherwise, s = stored, v = virtual. 

4745 if generated not in (None, "", b"\x00"): 

4746 computed = dict( 

4747 sqltext=default, persisted=generated in ("s", b"s") 

4748 ) 

4749 default = None 

4750 else: 

4751 computed = None 

4752 

4753 # adjust the default value 

4754 autoincrement = False 

4755 if default is not None: 

4756 match = re.search(r"""(nextval\(')([^']+)('.*$)""", default) 

4757 if match is not None: 

4758 if issubclass(coltype._type_affinity, sqltypes.Integer): 

4759 autoincrement = True 

4760 # the default is related to a Sequence 

4761 if "." not in match.group(2) and schema is not None: 

4762 # unconditionally quote the schema name. this could 

4763 # later be enhanced to obey quoting rules / 

4764 # "quote schema" 

4765 default = ( 

4766 match.group(1) 

4767 + ('"%s"' % schema) 

4768 + "." 

4769 + match.group(2) 

4770 + match.group(3) 

4771 ) 

4772 

4773 column_info = { 

4774 "name": name, 

4775 "type": coltype, 

4776 "nullable": nullable, 

4777 "default": default, 

4778 "autoincrement": autoincrement or identity is not None, 

4779 "comment": row_dict["comment"], 

4780 } 

4781 if computed is not None: 

4782 column_info["computed"] = computed 

4783 if identity is not None: 

4784 column_info["identity"] = identity 

4785 

4786 table_cols.append(column_info) 

4787 

4788 return columns 

4789 

4790 @lru_cache() 

4791 def _table_oids_query(self, schema, has_filter_names, scope, kind): 

4792 relkinds = self._kind_to_relkinds(kind) 

4793 oid_q = select( 

4794 pg_catalog.pg_class.c.oid, pg_catalog.pg_class.c.relname 

4795 ).where(self._pg_class_relkind_condition(relkinds)) 

4796 oid_q = self._pg_class_filter_scope_schema(oid_q, schema, scope=scope) 

4797 

4798 if has_filter_names: 

4799 oid_q = oid_q.where( 

4800 pg_catalog.pg_class.c.relname.in_(bindparam("filter_names")) 

4801 ) 

4802 return oid_q 

4803 

4804 @reflection.flexi_cache( 

4805 ("schema", InternalTraversal.dp_string), 

4806 ("filter_names", InternalTraversal.dp_string_list), 

4807 ("kind", InternalTraversal.dp_plain_obj), 

4808 ("scope", InternalTraversal.dp_plain_obj), 

4809 ) 

4810 def _get_table_oids( 

4811 self, connection, schema, filter_names, scope, kind, **kw 

4812 ): 

4813 has_filter_names, params = self._prepare_filter_names(filter_names) 

4814 oid_q = self._table_oids_query(schema, has_filter_names, scope, kind) 

4815 result = connection.execute(oid_q, params) 

4816 return result.all() 

4817 

4818 @util.memoized_property 

4819 def _constraint_query(self): 

4820 if self.server_version_info >= (11, 0): 

4821 indnkeyatts = pg_catalog.pg_index.c.indnkeyatts 

4822 else: 

4823 indnkeyatts = pg_catalog.pg_index.c.indnatts.label("indnkeyatts") 

4824 

4825 if self.server_version_info >= (15,): 

4826 indnullsnotdistinct = pg_catalog.pg_index.c.indnullsnotdistinct 

4827 else: 

4828 indnullsnotdistinct = sql.false().label("indnullsnotdistinct") 

4829 

4830 con_sq = ( 

4831 select( 

4832 pg_catalog.pg_constraint.c.conrelid, 

4833 pg_catalog.pg_constraint.c.conname, 

4834 sql.func.unnest(pg_catalog.pg_index.c.indkey).label("attnum"), 

4835 sql.func.generate_subscripts( 

4836 pg_catalog.pg_index.c.indkey, 1 

4837 ).label("ord"), 

4838 indnkeyatts, 

4839 indnullsnotdistinct, 

4840 pg_catalog.pg_description.c.description, 

4841 ) 

4842 .join( 

4843 pg_catalog.pg_index, 

4844 pg_catalog.pg_constraint.c.conindid 

4845 == pg_catalog.pg_index.c.indexrelid, 

4846 ) 

4847 .outerjoin( 

4848 pg_catalog.pg_description, 

4849 pg_catalog.pg_description.c.objoid 

4850 == pg_catalog.pg_constraint.c.oid, 

4851 ) 

4852 .where( 

4853 pg_catalog.pg_constraint.c.contype == bindparam("contype"), 

4854 pg_catalog.pg_constraint.c.conrelid.in_(bindparam("oids")), 

4855 # NOTE: filtering also on pg_index.indrelid for oids does 

4856 # not seem to have a performance effect, but it may be an 

4857 # option if perf problems are reported 

4858 ) 

4859 .subquery("con") 

4860 ) 

4861 

4862 attr_sq = ( 

4863 select( 

4864 con_sq.c.conrelid, 

4865 con_sq.c.conname, 

4866 con_sq.c.description, 

4867 con_sq.c.ord, 

4868 con_sq.c.indnkeyatts, 

4869 con_sq.c.indnullsnotdistinct, 

4870 pg_catalog.pg_attribute.c.attname, 

4871 ) 

4872 .select_from(pg_catalog.pg_attribute) 

4873 .join( 

4874 con_sq, 

4875 sql.and_( 

4876 pg_catalog.pg_attribute.c.attnum == con_sq.c.attnum, 

4877 pg_catalog.pg_attribute.c.attrelid == con_sq.c.conrelid, 

4878 ), 

4879 ) 

4880 .where( 

4881 # NOTE: restate the condition here, since pg15 otherwise 

4882 # seems to get confused on pscopg2 sometimes, doing 

4883 # a sequential scan of pg_attribute. 

4884 # The condition in the con_sq subquery is not actually needed 

4885 # in pg15, but it may be needed in older versions. Keeping it 

4886 # does not seems to have any impact in any case. 

4887 con_sq.c.conrelid.in_(bindparam("oids")) 

4888 ) 

4889 .subquery("attr") 

4890 ) 

4891 

4892 return ( 

4893 select( 

4894 attr_sq.c.conrelid, 

4895 sql.func.array_agg( 

4896 # NOTE: cast since some postgresql derivatives may 

4897 # not support array_agg on the name type 

4898 aggregate_order_by( 

4899 attr_sq.c.attname.cast(TEXT), attr_sq.c.ord 

4900 ) 

4901 ).label("cols"), 

4902 attr_sq.c.conname, 

4903 sql.func.min(attr_sq.c.description).label("description"), 

4904 sql.func.min(attr_sq.c.indnkeyatts).label("indnkeyatts"), 

4905 sql.func.bool_and(attr_sq.c.indnullsnotdistinct).label( 

4906 "indnullsnotdistinct" 

4907 ), 

4908 ) 

4909 .group_by(attr_sq.c.conrelid, attr_sq.c.conname) 

4910 .order_by(attr_sq.c.conrelid, attr_sq.c.conname) 

4911 ) 

4912 

4913 def _reflect_constraint( 

4914 self, connection, contype, schema, filter_names, scope, kind, **kw 

4915 ): 

4916 # used to reflect primary and unique constraint 

4917 table_oids = self._get_table_oids( 

4918 connection, schema, filter_names, scope, kind, **kw 

4919 ) 

4920 batches = list(table_oids) 

4921 is_unique = contype == "u" 

4922 

4923 while batches: 

4924 batch = batches[0:3000] 

4925 batches[0:3000] = [] 

4926 

4927 result = connection.execute( 

4928 self._constraint_query, 

4929 {"oids": [r[0] for r in batch], "contype": contype}, 

4930 ).mappings() 

4931 

4932 result_by_oid = defaultdict(list) 

4933 for row_dict in result: 

4934 result_by_oid[row_dict["conrelid"]].append(row_dict) 

4935 

4936 for oid, tablename in batch: 

4937 for_oid = result_by_oid.get(oid, ()) 

4938 if for_oid: 

4939 for row in for_oid: 

4940 # See note in get_multi_indexes 

4941 all_cols = row["cols"] 

4942 indnkeyatts = row["indnkeyatts"] 

4943 if len(all_cols) > indnkeyatts: 

4944 inc_cols = all_cols[indnkeyatts:] 

4945 cst_cols = all_cols[:indnkeyatts] 

4946 else: 

4947 inc_cols = [] 

4948 cst_cols = all_cols 

4949 

4950 opts = {} 

4951 if self.server_version_info >= (11,): 

4952 opts["postgresql_include"] = inc_cols 

4953 if is_unique: 

4954 opts["postgresql_nulls_not_distinct"] = row[ 

4955 "indnullsnotdistinct" 

4956 ] 

4957 yield ( 

4958 tablename, 

4959 cst_cols, 

4960 row["conname"], 

4961 row["description"], 

4962 opts, 

4963 ) 

4964 else: 

4965 yield tablename, None, None, None, None 

4966 

4967 def get_multi_pk_constraint( 

4968 self, connection, schema, filter_names, scope, kind, **kw 

4969 ): 

4970 result = self._reflect_constraint( 

4971 connection, "p", schema, filter_names, scope, kind, **kw 

4972 ) 

4973 

4974 # only a single pk can be present for each table. Return an entry 

4975 # even if a table has no primary key 

4976 default = ReflectionDefaults.pk_constraint 

4977 

4978 def pk_constraint(pk_name, cols, comment, opts): 

4979 info = { 

4980 "constrained_columns": cols, 

4981 "name": pk_name, 

4982 "comment": comment, 

4983 } 

4984 if opts: 

4985 info["dialect_options"] = opts 

4986 return info 

4987 

4988 return ( 

4989 ( 

4990 (schema, table_name), 

4991 ( 

4992 pk_constraint(pk_name, cols, comment, opts) 

4993 if pk_name is not None 

4994 else default() 

4995 ), 

4996 ) 

4997 for table_name, cols, pk_name, comment, opts in result 

4998 ) 

4999 

5000 @lru_cache() 

5001 def _foreing_key_query(self, schema, has_filter_names, scope, kind): 

5002 pg_class_ref = pg_catalog.pg_class.alias("cls_ref") 

5003 pg_namespace_ref = pg_catalog.pg_namespace.alias("nsp_ref") 

5004 relkinds = self._kind_to_relkinds(kind) 

5005 query = ( 

5006 select( 

5007 pg_catalog.pg_class.c.relname, 

5008 pg_catalog.pg_constraint.c.conname, 

5009 # NOTE: avoid calling pg_get_constraintdef when not needed 

5010 # to speed up the query 

5011 sql.case( 

5012 ( 

5013 pg_catalog.pg_constraint.c.oid.is_not(None), 

5014 pg_catalog.pg_get_constraintdef( 

5015 pg_catalog.pg_constraint.c.oid, True 

5016 ), 

5017 ), 

5018 else_=None, 

5019 ), 

5020 pg_namespace_ref.c.nspname, 

5021 pg_catalog.pg_description.c.description, 

5022 ) 

5023 .select_from(pg_catalog.pg_class) 

5024 .outerjoin( 

5025 pg_catalog.pg_constraint, 

5026 sql.and_( 

5027 pg_catalog.pg_class.c.oid 

5028 == pg_catalog.pg_constraint.c.conrelid, 

5029 pg_catalog.pg_constraint.c.contype == "f", 

5030 ), 

5031 ) 

5032 .outerjoin( 

5033 pg_class_ref, 

5034 pg_class_ref.c.oid == pg_catalog.pg_constraint.c.confrelid, 

5035 ) 

5036 .outerjoin( 

5037 pg_namespace_ref, 

5038 pg_class_ref.c.relnamespace == pg_namespace_ref.c.oid, 

5039 ) 

5040 .outerjoin( 

5041 pg_catalog.pg_description, 

5042 pg_catalog.pg_description.c.objoid 

5043 == pg_catalog.pg_constraint.c.oid, 

5044 ) 

5045 .order_by( 

5046 pg_catalog.pg_class.c.relname, 

5047 pg_catalog.pg_constraint.c.conname, 

5048 ) 

5049 .where(self._pg_class_relkind_condition(relkinds)) 

5050 ) 

5051 query = self._pg_class_filter_scope_schema(query, schema, scope) 

5052 if has_filter_names: 

5053 query = query.where( 

5054 pg_catalog.pg_class.c.relname.in_(bindparam("filter_names")) 

5055 ) 

5056 return query 

5057 

5058 @util.memoized_property 

5059 def _fk_regex_pattern(self): 

5060 # optionally quoted token 

5061 qtoken = r'(?:"(?:[^"]|"")+"|[\w]+?)' 

5062 

5063 # https://www.postgresql.org/docs/current/static/sql-createtable.html 

5064 return re.compile( 

5065 r"FOREIGN KEY \((.*?)\) " 

5066 rf"REFERENCES (?:({qtoken})\.)?({qtoken})\(((?:{qtoken}(?: *, *)?)+)\)" # noqa: E501 

5067 r"[\s]?(MATCH (FULL|PARTIAL|SIMPLE)+)?" 

5068 r"[\s]?(?:ON (UPDATE|DELETE) " 

5069 r"(CASCADE|RESTRICT|NO ACTION|" 

5070 r"SET (?:NULL|DEFAULT)(?:\s\(.+\))?)+)?" 

5071 r"[\s]?(?:ON (UPDATE|DELETE) " 

5072 r"(CASCADE|RESTRICT|NO ACTION|" 

5073 r"SET (?:NULL|DEFAULT)(?:\s\(.+\))?)+)?" 

5074 r"[\s]?(DEFERRABLE|NOT DEFERRABLE)?" 

5075 r"[\s]?(INITIALLY (DEFERRED|IMMEDIATE)+)?" 

5076 ) 

5077 

5078 def _parse_fk(self, condef): 

5079 FK_REGEX = self._fk_regex_pattern 

5080 m = re.search(FK_REGEX, condef).groups() 

5081 

5082 ( 

5083 constrained_columns, 

5084 referred_schema, 

5085 referred_table, 

5086 referred_columns, 

5087 _, 

5088 match, 

5089 upddelkey1, 

5090 upddelval1, 

5091 upddelkey2, 

5092 upddelval2, 

5093 deferrable, 

5094 _, 

5095 initially, 

5096 ) = m 

5097 

5098 onupdate = ( 

5099 upddelval1 

5100 if upddelkey1 == "UPDATE" 

5101 else upddelval2 if upddelkey2 == "UPDATE" else None 

5102 ) 

5103 ondelete = ( 

5104 upddelval1 

5105 if upddelkey1 == "DELETE" 

5106 else upddelval2 if upddelkey2 == "DELETE" else None 

5107 ) 

5108 

5109 return ( 

5110 constrained_columns, 

5111 referred_schema, 

5112 referred_table, 

5113 referred_columns, 

5114 match, 

5115 onupdate, 

5116 ondelete, 

5117 deferrable, 

5118 initially, 

5119 ) 

5120 

5121 def get_multi_foreign_keys( 

5122 self, 

5123 connection, 

5124 schema, 

5125 filter_names, 

5126 scope, 

5127 kind, 

5128 postgresql_ignore_search_path=False, 

5129 **kw, 

5130 ): 

5131 preparer = self.identifier_preparer 

5132 

5133 has_filter_names, params = self._prepare_filter_names(filter_names) 

5134 query = self._foreing_key_query(schema, has_filter_names, scope, kind) 

5135 result = connection.execute(query, params) 

5136 

5137 fkeys = defaultdict(list) 

5138 default = ReflectionDefaults.foreign_keys 

5139 for table_name, conname, condef, conschema, comment in result: 

5140 # ensure that each table has an entry, even if it has 

5141 # no foreign keys 

5142 if conname is None: 

5143 fkeys[(schema, table_name)] = default() 

5144 continue 

5145 table_fks = fkeys[(schema, table_name)] 

5146 

5147 ( 

5148 constrained_columns, 

5149 referred_schema, 

5150 referred_table, 

5151 referred_columns, 

5152 match, 

5153 onupdate, 

5154 ondelete, 

5155 deferrable, 

5156 initially, 

5157 ) = self._parse_fk(condef) 

5158 

5159 if deferrable is not None: 

5160 deferrable = True if deferrable == "DEFERRABLE" else False 

5161 constrained_columns = [ 

5162 preparer._unquote_identifier(x) 

5163 for x in re.split(r"\s*,\s*", constrained_columns) 

5164 ] 

5165 

5166 if postgresql_ignore_search_path: 

5167 # when ignoring search path, we use the actual schema 

5168 # provided it isn't the "default" schema 

5169 if conschema != self.default_schema_name: 

5170 referred_schema = conschema 

5171 else: 

5172 referred_schema = schema 

5173 elif referred_schema: 

5174 # referred_schema is the schema that we regexp'ed from 

5175 # pg_get_constraintdef(). If the schema is in the search 

5176 # path, pg_get_constraintdef() will give us None. 

5177 referred_schema = preparer._unquote_identifier(referred_schema) 

5178 elif schema is not None and schema == conschema: 

5179 # If the actual schema matches the schema of the table 

5180 # we're reflecting, then we will use that. 

5181 referred_schema = schema 

5182 

5183 referred_table = preparer._unquote_identifier(referred_table) 

5184 referred_columns = [ 

5185 preparer._unquote_identifier(x) 

5186 for x in re.split(r"\s*,\s", referred_columns) 

5187 ] 

5188 options = { 

5189 k: v 

5190 for k, v in [ 

5191 ("onupdate", onupdate), 

5192 ("ondelete", ondelete), 

5193 ("initially", initially), 

5194 ("deferrable", deferrable), 

5195 ("match", match), 

5196 ] 

5197 if v is not None and v != "NO ACTION" 

5198 } 

5199 fkey_d = { 

5200 "name": conname, 

5201 "constrained_columns": constrained_columns, 

5202 "referred_schema": referred_schema, 

5203 "referred_table": referred_table, 

5204 "referred_columns": referred_columns, 

5205 "options": options, 

5206 "comment": comment, 

5207 } 

5208 table_fks.append(fkey_d) 

5209 return fkeys.items() 

5210 

5211 @util.memoized_property 

5212 def _index_query(self): 

5213 # NOTE: pg_index is used as from two times to improve performance, 

5214 # since extraing all the index information from `idx_sq` to avoid 

5215 # the second pg_index use leads to a worse performing query in 

5216 # particular when querying for a single table (as of pg 17) 

5217 # NOTE: repeating oids clause improve query performance 

5218 

5219 # subquery to get the columns 

5220 idx_sq = ( 

5221 select( 

5222 pg_catalog.pg_index.c.indexrelid, 

5223 pg_catalog.pg_index.c.indrelid, 

5224 sql.func.unnest(pg_catalog.pg_index.c.indkey).label("attnum"), 

5225 sql.func.unnest(pg_catalog.pg_index.c.indclass).label( 

5226 "att_opclass" 

5227 ), 

5228 sql.func.generate_subscripts( 

5229 pg_catalog.pg_index.c.indkey, 1 

5230 ).label("ord"), 

5231 ) 

5232 .where( 

5233 ~pg_catalog.pg_index.c.indisprimary, 

5234 pg_catalog.pg_index.c.indrelid.in_(bindparam("oids")), 

5235 ) 

5236 .subquery("idx") 

5237 ) 

5238 

5239 attr_sq = ( 

5240 select( 

5241 idx_sq.c.indexrelid, 

5242 idx_sq.c.indrelid, 

5243 idx_sq.c.ord, 

5244 # NOTE: always using pg_get_indexdef is too slow so just 

5245 # invoke when the element is an expression 

5246 sql.case( 

5247 ( 

5248 idx_sq.c.attnum == 0, 

5249 pg_catalog.pg_get_indexdef( 

5250 idx_sq.c.indexrelid, idx_sq.c.ord + 1, True 

5251 ), 

5252 ), 

5253 # NOTE: need to cast this since attname is of type "name" 

5254 # that's limited to 63 bytes, while pg_get_indexdef 

5255 # returns "text" so its output may get cut 

5256 else_=pg_catalog.pg_attribute.c.attname.cast(TEXT), 

5257 ).label("element"), 

5258 (idx_sq.c.attnum == 0).label("is_expr"), 

5259 # since it's converted to array cast it to bigint (oid are 

5260 # "unsigned four-byte integer") to make it easier for 

5261 # dialects to interpret 

5262 idx_sq.c.att_opclass.cast(BIGINT), 

5263 ) 

5264 .select_from(idx_sq) 

5265 .outerjoin( 

5266 # do not remove rows where idx_sq.c.attnum is 0 

5267 pg_catalog.pg_attribute, 

5268 sql.and_( 

5269 pg_catalog.pg_attribute.c.attnum == idx_sq.c.attnum, 

5270 pg_catalog.pg_attribute.c.attrelid == idx_sq.c.indrelid, 

5271 ), 

5272 ) 

5273 .where(idx_sq.c.indrelid.in_(bindparam("oids"))) 

5274 .subquery("idx_attr") 

5275 ) 

5276 

5277 cols_sq = ( 

5278 select( 

5279 attr_sq.c.indexrelid, 

5280 sql.func.min(attr_sq.c.indrelid), 

5281 sql.func.array_agg( 

5282 aggregate_order_by(attr_sq.c.element, attr_sq.c.ord) 

5283 ).label("elements"), 

5284 sql.func.array_agg( 

5285 aggregate_order_by(attr_sq.c.is_expr, attr_sq.c.ord) 

5286 ).label("elements_is_expr"), 

5287 sql.func.array_agg( 

5288 aggregate_order_by(attr_sq.c.att_opclass, attr_sq.c.ord) 

5289 ).label("elements_opclass"), 

5290 ) 

5291 .group_by(attr_sq.c.indexrelid) 

5292 .subquery("idx_cols") 

5293 ) 

5294 

5295 if self.server_version_info >= (11, 0): 

5296 indnkeyatts = pg_catalog.pg_index.c.indnkeyatts 

5297 else: 

5298 indnkeyatts = pg_catalog.pg_index.c.indnatts.label("indnkeyatts") 

5299 

5300 if self.server_version_info >= (15,): 

5301 nulls_not_distinct = pg_catalog.pg_index.c.indnullsnotdistinct 

5302 else: 

5303 nulls_not_distinct = sql.false().label("indnullsnotdistinct") 

5304 

5305 return ( 

5306 select( 

5307 pg_catalog.pg_index.c.indrelid, 

5308 pg_catalog.pg_class.c.relname, 

5309 pg_catalog.pg_index.c.indisunique, 

5310 pg_catalog.pg_index.c.indisvalid, 

5311 pg_catalog.pg_constraint.c.conrelid.is_not(None).label( 

5312 "has_constraint" 

5313 ), 

5314 pg_catalog.pg_index.c.indoption, 

5315 pg_catalog.pg_class.c.reloptions, 

5316 # will get the value using the pg_am cached dict 

5317 pg_catalog.pg_class.c.relam, 

5318 # NOTE: pg_get_expr is very fast so this case has almost no 

5319 # performance impact 

5320 sql.case( 

5321 ( 

5322 pg_catalog.pg_index.c.indpred.is_not(None), 

5323 pg_catalog.pg_get_expr( 

5324 pg_catalog.pg_index.c.indpred, 

5325 pg_catalog.pg_index.c.indrelid, 

5326 ), 

5327 ), 

5328 else_=None, 

5329 ).label("filter_definition"), 

5330 indnkeyatts, 

5331 nulls_not_distinct, 

5332 cols_sq.c.elements, 

5333 cols_sq.c.elements_is_expr, 

5334 # will get the value using the pg_opclass cached dict 

5335 cols_sq.c.elements_opclass, 

5336 ) 

5337 .select_from(pg_catalog.pg_index) 

5338 .where( 

5339 pg_catalog.pg_index.c.indrelid.in_(bindparam("oids")), 

5340 ~pg_catalog.pg_index.c.indisprimary, 

5341 ) 

5342 .join( 

5343 pg_catalog.pg_class, 

5344 pg_catalog.pg_index.c.indexrelid == pg_catalog.pg_class.c.oid, 

5345 ) 

5346 .outerjoin( 

5347 cols_sq, 

5348 pg_catalog.pg_index.c.indexrelid == cols_sq.c.indexrelid, 

5349 ) 

5350 .outerjoin( 

5351 pg_catalog.pg_constraint, 

5352 sql.and_( 

5353 pg_catalog.pg_index.c.indrelid 

5354 == pg_catalog.pg_constraint.c.conrelid, 

5355 pg_catalog.pg_index.c.indexrelid 

5356 == pg_catalog.pg_constraint.c.conindid, 

5357 pg_catalog.pg_constraint.c.contype 

5358 == sql.any_(_array.array(("p", "u", "x"))), 

5359 ), 

5360 ) 

5361 .order_by( 

5362 pg_catalog.pg_index.c.indrelid, pg_catalog.pg_class.c.relname 

5363 ) 

5364 ) 

5365 

5366 def get_multi_indexes( 

5367 self, connection, schema, filter_names, scope, kind, **kw 

5368 ): 

5369 table_oids = self._get_table_oids( 

5370 connection, schema, filter_names, scope, kind, **kw 

5371 ) 

5372 

5373 pg_am_btree_oid = self._load_pg_am_btree_oid(connection) 

5374 # lazy load only if needed, the assumption is that most indexes 

5375 # will use btree so it may not be needed at all 

5376 pg_am_dict = None 

5377 pg_opclass_dict = self._load_pg_opclass_notdefault_dict( 

5378 connection, **kw 

5379 ) 

5380 

5381 indexes = defaultdict(list) 

5382 default = ReflectionDefaults.indexes 

5383 

5384 batches = list(table_oids) 

5385 

5386 while batches: 

5387 batch = batches[0:3000] 

5388 batches[0:3000] = [] 

5389 

5390 result = connection.execute( 

5391 self._index_query, {"oids": [r[0] for r in batch]} 

5392 ).mappings() 

5393 

5394 result_by_oid = defaultdict(list) 

5395 for row_dict in result: 

5396 result_by_oid[row_dict["indrelid"]].append(row_dict) 

5397 

5398 for oid, table_name in batch: 

5399 if oid not in result_by_oid: 

5400 # ensure that each table has an entry, even if reflection 

5401 # is skipped because not supported 

5402 indexes[(schema, table_name)] = default() 

5403 continue 

5404 

5405 for row in result_by_oid[oid]: 

5406 index_name = row["relname"] 

5407 

5408 table_indexes = indexes[(schema, table_name)] 

5409 

5410 all_elements = row["elements"] 

5411 all_elements_is_expr = row["elements_is_expr"] 

5412 all_elements_opclass = row["elements_opclass"] 

5413 indnkeyatts = row["indnkeyatts"] 

5414 # "The number of key columns in the index, not counting any 

5415 # included columns, which are merely stored and do not 

5416 # participate in the index semantics" 

5417 if len(all_elements) > indnkeyatts: 

5418 # this is a "covering index" which has INCLUDE columns 

5419 # as well as regular index columns 

5420 inc_cols = all_elements[indnkeyatts:] 

5421 idx_elements = all_elements[:indnkeyatts] 

5422 idx_elements_is_expr = all_elements_is_expr[ 

5423 :indnkeyatts 

5424 ] 

5425 # postgresql does not support expression on included 

5426 # columns as of v14: "ERROR: expressions are not 

5427 # supported in included columns". 

5428 assert all( 

5429 not is_expr 

5430 for is_expr in all_elements_is_expr[indnkeyatts:] 

5431 ) 

5432 idx_elements_opclass = all_elements_opclass[ 

5433 :indnkeyatts 

5434 ] 

5435 else: 

5436 idx_elements = all_elements 

5437 idx_elements_is_expr = all_elements_is_expr 

5438 inc_cols = [] 

5439 idx_elements_opclass = all_elements_opclass 

5440 

5441 index = {"name": index_name, "unique": row["indisunique"]} 

5442 if any(idx_elements_is_expr): 

5443 index["column_names"] = [ 

5444 None if is_expr else expr 

5445 for expr, is_expr in zip( 

5446 idx_elements, idx_elements_is_expr 

5447 ) 

5448 ] 

5449 index["expressions"] = idx_elements 

5450 else: 

5451 index["column_names"] = idx_elements 

5452 

5453 dialect_options = {} 

5454 

5455 postgresql_ops = {} 

5456 for name, opclass in zip( 

5457 idx_elements, idx_elements_opclass 

5458 ): 

5459 # is not in the dict if the opclass is the default one 

5460 opclass_name = pg_opclass_dict.get(opclass) 

5461 if opclass_name is not None: 

5462 postgresql_ops[name] = opclass_name 

5463 

5464 if postgresql_ops: 

5465 dialect_options["postgresql_ops"] = postgresql_ops 

5466 

5467 sorting = {} 

5468 for col_index, col_flags in enumerate(row["indoption"]): 

5469 col_sorting = () 

5470 # try to set flags only if they differ from PG 

5471 # defaults... 

5472 if col_flags & 0x01: 

5473 col_sorting += ("desc",) 

5474 if not (col_flags & 0x02): 

5475 col_sorting += ("nulls_last",) 

5476 else: 

5477 if col_flags & 0x02: 

5478 col_sorting += ("nulls_first",) 

5479 if col_sorting: 

5480 sorting[idx_elements[col_index]] = col_sorting 

5481 if sorting: 

5482 index["column_sorting"] = sorting 

5483 if row["has_constraint"]: 

5484 index["duplicates_constraint"] = index_name 

5485 

5486 if row["reloptions"]: 

5487 dialect_options["postgresql_with"] = dict( 

5488 [ 

5489 option.split("=", 1) 

5490 for option in row["reloptions"] 

5491 ] 

5492 ) 

5493 # it *might* be nice to include that this is 'btree' in the 

5494 # reflection info. But we don't want an Index object 

5495 # to have a ``postgresql_using`` in it that is just the 

5496 # default, so for the moment leaving this out. 

5497 if row["relam"] != pg_am_btree_oid: 

5498 if pg_am_dict is None: 

5499 pg_am_dict = self._load_pg_am_dict( 

5500 connection, **kw 

5501 ) 

5502 dialect_options["postgresql_using"] = pg_am_dict[ 

5503 row["relam"] 

5504 ] 

5505 if row["filter_definition"]: 

5506 dialect_options["postgresql_where"] = row[ 

5507 "filter_definition" 

5508 ] 

5509 if self.server_version_info >= (11,): 

5510 dialect_options["postgresql_include"] = inc_cols 

5511 if row["indnullsnotdistinct"]: 

5512 # the default is False, so ignore it. 

5513 dialect_options["postgresql_nulls_not_distinct"] = row[ 

5514 "indnullsnotdistinct" 

5515 ] 

5516 

5517 if not row["indisvalid"]: 

5518 dialect_options["postgresql_invalid"] = True 

5519 

5520 if dialect_options: 

5521 index["dialect_options"] = dialect_options 

5522 

5523 table_indexes.append(index) 

5524 return indexes.items() 

5525 

5526 def get_multi_unique_constraints( 

5527 self, 

5528 connection, 

5529 schema, 

5530 filter_names, 

5531 scope, 

5532 kind, 

5533 **kw, 

5534 ): 

5535 result = self._reflect_constraint( 

5536 connection, "u", schema, filter_names, scope, kind, **kw 

5537 ) 

5538 

5539 # each table can have multiple unique constraints 

5540 uniques = defaultdict(list) 

5541 default = ReflectionDefaults.unique_constraints 

5542 for table_name, cols, con_name, comment, options in result: 

5543 # ensure a list is created for each table. leave it empty if 

5544 # the table has no unique constraint 

5545 if con_name is None: 

5546 uniques[(schema, table_name)] = default() 

5547 continue 

5548 

5549 uc_dict = { 

5550 "column_names": cols, 

5551 "name": con_name, 

5552 "comment": comment, 

5553 } 

5554 if options: 

5555 uc_dict["dialect_options"] = options 

5556 

5557 uniques[(schema, table_name)].append(uc_dict) 

5558 return uniques.items() 

5559 

5560 @lru_cache() 

5561 def _comment_query(self, schema, has_filter_names, scope, kind): 

5562 relkinds = self._kind_to_relkinds(kind) 

5563 query = ( 

5564 select( 

5565 pg_catalog.pg_class.c.relname, 

5566 pg_catalog.pg_description.c.description, 

5567 ) 

5568 .select_from(pg_catalog.pg_class) 

5569 .outerjoin( 

5570 pg_catalog.pg_description, 

5571 sql.and_( 

5572 pg_catalog.pg_class.c.oid 

5573 == pg_catalog.pg_description.c.objoid, 

5574 pg_catalog.pg_description.c.objsubid == 0, 

5575 pg_catalog.pg_description.c.classoid 

5576 == sql.func.cast("pg_catalog.pg_class", REGCLASS), 

5577 ), 

5578 ) 

5579 .where(self._pg_class_relkind_condition(relkinds)) 

5580 ) 

5581 query = self._pg_class_filter_scope_schema(query, schema, scope) 

5582 if has_filter_names: 

5583 query = query.where( 

5584 pg_catalog.pg_class.c.relname.in_(bindparam("filter_names")) 

5585 ) 

5586 return query 

5587 

5588 def get_multi_table_comment( 

5589 self, connection, schema, filter_names, scope, kind, **kw 

5590 ): 

5591 has_filter_names, params = self._prepare_filter_names(filter_names) 

5592 query = self._comment_query(schema, has_filter_names, scope, kind) 

5593 result = connection.execute(query, params) 

5594 

5595 default = ReflectionDefaults.table_comment 

5596 return ( 

5597 ( 

5598 (schema, table), 

5599 {"text": comment} if comment is not None else default(), 

5600 ) 

5601 for table, comment in result 

5602 ) 

5603 

5604 @lru_cache() 

5605 def _check_constraint_query(self, schema, has_filter_names, scope, kind): 

5606 relkinds = self._kind_to_relkinds(kind) 

5607 query = ( 

5608 select( 

5609 pg_catalog.pg_class.c.relname, 

5610 pg_catalog.pg_constraint.c.conname, 

5611 # NOTE: avoid calling pg_get_constraintdef when not needed 

5612 # to speed up the query 

5613 sql.case( 

5614 ( 

5615 pg_catalog.pg_constraint.c.oid.is_not(None), 

5616 pg_catalog.pg_get_constraintdef( 

5617 pg_catalog.pg_constraint.c.oid, True 

5618 ), 

5619 ), 

5620 else_=None, 

5621 ), 

5622 pg_catalog.pg_description.c.description, 

5623 ) 

5624 .select_from(pg_catalog.pg_class) 

5625 .outerjoin( 

5626 pg_catalog.pg_constraint, 

5627 sql.and_( 

5628 pg_catalog.pg_class.c.oid 

5629 == pg_catalog.pg_constraint.c.conrelid, 

5630 pg_catalog.pg_constraint.c.contype == "c", 

5631 ), 

5632 ) 

5633 .outerjoin( 

5634 pg_catalog.pg_description, 

5635 pg_catalog.pg_description.c.objoid 

5636 == pg_catalog.pg_constraint.c.oid, 

5637 ) 

5638 .order_by( 

5639 pg_catalog.pg_class.c.relname, 

5640 pg_catalog.pg_constraint.c.conname, 

5641 ) 

5642 .where(self._pg_class_relkind_condition(relkinds)) 

5643 ) 

5644 query = self._pg_class_filter_scope_schema(query, schema, scope) 

5645 if has_filter_names: 

5646 query = query.where( 

5647 pg_catalog.pg_class.c.relname.in_(bindparam("filter_names")) 

5648 ) 

5649 return query 

5650 

5651 def get_multi_check_constraints( 

5652 self, connection, schema, filter_names, scope, kind, **kw 

5653 ): 

5654 has_filter_names, params = self._prepare_filter_names(filter_names) 

5655 query = self._check_constraint_query( 

5656 schema, has_filter_names, scope, kind 

5657 ) 

5658 result = connection.execute(query, params) 

5659 

5660 check_constraints = defaultdict(list) 

5661 default = ReflectionDefaults.check_constraints 

5662 for table_name, check_name, src, comment in result: 

5663 # only two cases for check_name and src: both null or both defined 

5664 if check_name is None and src is None: 

5665 check_constraints[(schema, table_name)] = default() 

5666 continue 

5667 # samples: 

5668 # "CHECK (((a > 1) AND (a < 5)))" 

5669 # "CHECK (((a = 1) OR ((a > 2) AND (a < 5))))" 

5670 # "CHECK (((a > 1) AND (a < 5))) NOT VALID" 

5671 # "CHECK (some_boolean_function(a))" 

5672 # "CHECK (((a\n < 1)\n OR\n (a\n >= 5))\n)" 

5673 # "CHECK (a NOT NULL) NO INHERIT" 

5674 # "CHECK (a NOT NULL) NO INHERIT NOT VALID" 

5675 

5676 m = re.match( 

5677 r"^CHECK *\((.+)\)( NO INHERIT)?( NOT VALID)?$", 

5678 src, 

5679 flags=re.DOTALL, 

5680 ) 

5681 if not m: 

5682 util.warn("Could not parse CHECK constraint text: %r" % src) 

5683 sqltext = "" 

5684 else: 

5685 sqltext = util.strip_outer_parens(m.group(1)) 

5686 entry = { 

5687 "name": check_name, 

5688 "sqltext": sqltext, 

5689 "comment": comment, 

5690 } 

5691 if m: 

5692 do = {} 

5693 if " NOT VALID" in m.groups(): 

5694 do["not_valid"] = True 

5695 if " NO INHERIT" in m.groups(): 

5696 do["no_inherit"] = True 

5697 if do: 

5698 entry["dialect_options"] = do 

5699 

5700 check_constraints[(schema, table_name)].append(entry) 

5701 return check_constraints.items() 

5702 

5703 def _pg_type_filter_schema(self, query, schema): 

5704 if schema is None: 

5705 query = query.where( 

5706 pg_catalog.pg_type_is_visible(pg_catalog.pg_type.c.oid), 

5707 # ignore pg_catalog schema 

5708 pg_catalog.pg_namespace.c.nspname != "pg_catalog", 

5709 ) 

5710 elif schema != "*": 

5711 query = query.where(pg_catalog.pg_namespace.c.nspname == schema) 

5712 return query 

5713 

5714 @lru_cache() 

5715 def _enum_query(self, schema): 

5716 lbl_agg_sq = ( 

5717 select( 

5718 pg_catalog.pg_enum.c.enumtypid, 

5719 sql.func.array_agg( 

5720 aggregate_order_by( 

5721 # NOTE: cast since some postgresql derivatives may 

5722 # not support array_agg on the name type 

5723 pg_catalog.pg_enum.c.enumlabel.cast(TEXT), 

5724 pg_catalog.pg_enum.c.enumsortorder, 

5725 ) 

5726 ).label("labels"), 

5727 ) 

5728 .group_by(pg_catalog.pg_enum.c.enumtypid) 

5729 .subquery("lbl_agg") 

5730 ) 

5731 

5732 query = ( 

5733 select( 

5734 pg_catalog.pg_type.c.typname.label("name"), 

5735 pg_catalog.pg_type_is_visible(pg_catalog.pg_type.c.oid).label( 

5736 "visible" 

5737 ), 

5738 pg_catalog.pg_namespace.c.nspname.label("schema"), 

5739 lbl_agg_sq.c.labels.label("labels"), 

5740 ) 

5741 .join( 

5742 pg_catalog.pg_namespace, 

5743 pg_catalog.pg_namespace.c.oid 

5744 == pg_catalog.pg_type.c.typnamespace, 

5745 ) 

5746 .outerjoin( 

5747 lbl_agg_sq, pg_catalog.pg_type.c.oid == lbl_agg_sq.c.enumtypid 

5748 ) 

5749 .where(pg_catalog.pg_type.c.typtype == "e") 

5750 .order_by( 

5751 pg_catalog.pg_namespace.c.nspname, pg_catalog.pg_type.c.typname 

5752 ) 

5753 ) 

5754 

5755 return self._pg_type_filter_schema(query, schema) 

5756 

5757 @reflection.cache 

5758 def _load_enums(self, connection, schema=None, **kw): 

5759 if not self.supports_native_enum: 

5760 return [] 

5761 

5762 result = connection.execute(self._enum_query(schema)) 

5763 

5764 enums = [] 

5765 for name, visible, schema, labels in result: 

5766 enums.append( 

5767 { 

5768 "name": name, 

5769 "schema": schema, 

5770 "visible": visible, 

5771 "labels": [] if labels is None else labels, 

5772 } 

5773 ) 

5774 return enums 

5775 

5776 @lru_cache() 

5777 def _domain_query(self, schema): 

5778 con_sq = ( 

5779 select( 

5780 pg_catalog.pg_constraint.c.contypid, 

5781 sql.func.array_agg( 

5782 pg_catalog.pg_get_constraintdef( 

5783 pg_catalog.pg_constraint.c.oid, True 

5784 ) 

5785 ).label("condefs"), 

5786 sql.func.array_agg( 

5787 # NOTE: cast since some postgresql derivatives may 

5788 # not support array_agg on the name type 

5789 pg_catalog.pg_constraint.c.conname.cast(TEXT) 

5790 ).label("connames"), 

5791 ) 

5792 # The domain this constraint is on; zero if not a domain constraint 

5793 .where(pg_catalog.pg_constraint.c.contypid != 0) 

5794 .group_by(pg_catalog.pg_constraint.c.contypid) 

5795 .subquery("domain_constraints") 

5796 ) 

5797 

5798 collation_namespace = pg_catalog.pg_namespace.alias( 

5799 "collation_namespace" 

5800 ) 

5801 

5802 query = ( 

5803 select( 

5804 pg_catalog.pg_type.c.typname.label("name"), 

5805 pg_catalog.format_type( 

5806 pg_catalog.pg_type.c.typbasetype, 

5807 pg_catalog.pg_type.c.typtypmod, 

5808 ).label("attype"), 

5809 (~pg_catalog.pg_type.c.typnotnull).label("nullable"), 

5810 pg_catalog.pg_type.c.typdefault.label("default"), 

5811 pg_catalog.pg_type_is_visible(pg_catalog.pg_type.c.oid).label( 

5812 "visible" 

5813 ), 

5814 pg_catalog.pg_namespace.c.nspname.label("schema"), 

5815 con_sq.c.condefs, 

5816 con_sq.c.connames, 

5817 pg_catalog.pg_collation.c.collname, 

5818 sql.case( 

5819 ( 

5820 pg_catalog.pg_collation.c.oid.is_(None), 

5821 sql.null(), 

5822 ), 

5823 ( 

5824 pg_catalog.pg_collation_is_visible( 

5825 pg_catalog.pg_collation.c.oid 

5826 ), 

5827 sql.null(), 

5828 ), 

5829 else_=collation_namespace.c.nspname, 

5830 ).label("collation_schema"), 

5831 ) 

5832 .join( 

5833 pg_catalog.pg_namespace, 

5834 pg_catalog.pg_namespace.c.oid 

5835 == pg_catalog.pg_type.c.typnamespace, 

5836 ) 

5837 .outerjoin( 

5838 pg_catalog.pg_collation, 

5839 pg_catalog.pg_type.c.typcollation 

5840 == pg_catalog.pg_collation.c.oid, 

5841 ) 

5842 .outerjoin( 

5843 collation_namespace, 

5844 collation_namespace.c.oid 

5845 == pg_catalog.pg_collation.c.collnamespace, 

5846 ) 

5847 .outerjoin( 

5848 con_sq, 

5849 pg_catalog.pg_type.c.oid == con_sq.c.contypid, 

5850 ) 

5851 .where(pg_catalog.pg_type.c.typtype == "d") 

5852 .order_by( 

5853 pg_catalog.pg_namespace.c.nspname, pg_catalog.pg_type.c.typname 

5854 ) 

5855 ) 

5856 return self._pg_type_filter_schema(query, schema) 

5857 

5858 @reflection.cache 

5859 def _load_domains(self, connection, schema=None, **kw): 

5860 result = connection.execute(self._domain_query(schema)) 

5861 

5862 domains: List[ReflectedDomain] = [] 

5863 for domain in result.mappings(): 

5864 # strip (30) from character varying(30) 

5865 attype = re.search(r"([^\(]+)", domain["attype"]).group(1) 

5866 constraints: List[ReflectedDomainConstraint] = [] 

5867 if domain["connames"]: 

5868 # When a domain has multiple CHECK constraints, they will 

5869 # be tested in alphabetical order by name. 

5870 sorted_constraints = sorted( 

5871 zip(domain["connames"], domain["condefs"]), 

5872 key=lambda t: t[0], 

5873 ) 

5874 for name, def_ in sorted_constraints: 

5875 # constraint is in the form "CHECK (expression)" 

5876 # or "NOT NULL". Ignore the "NOT NULL" and 

5877 # remove "CHECK (" and the tailing ")". 

5878 if def_.casefold().startswith("check"): 

5879 check = def_[7:-1] 

5880 constraints.append({"name": name, "check": check}) 

5881 domain_rec: ReflectedDomain = { 

5882 "name": domain["name"], 

5883 "schema": domain["schema"], 

5884 "visible": domain["visible"], 

5885 "type": attype, 

5886 "nullable": domain["nullable"], 

5887 "default": domain["default"], 

5888 "constraints": constraints, 

5889 "collation": domain["collname"], 

5890 "collation_schema": domain["collation_schema"], 

5891 } 

5892 domains.append(domain_rec) 

5893 

5894 return domains 

5895 

5896 @util.memoized_property 

5897 def _pg_am_query(self): 

5898 return sql.select(pg_catalog.pg_am.c.oid, pg_catalog.pg_am.c.amname) 

5899 

5900 @reflection.cache 

5901 def _load_pg_am_dict(self, connection, **kw) -> dict[int, str]: 

5902 rows = connection.execute(self._pg_am_query) 

5903 return dict(rows.all()) 

5904 

5905 def _load_pg_am_btree_oid(self, connection): 

5906 # this oid is assumed to be stable 

5907 if self._pg_am_btree_oid == -1: 

5908 self._pg_am_btree_oid = connection.scalar( 

5909 self._pg_am_query.where(pg_catalog.pg_am.c.amname == "btree") 

5910 ) 

5911 return self._pg_am_btree_oid 

5912 

5913 @util.memoized_property 

5914 def _pg_opclass_notdefault_query(self): 

5915 return sql.select( 

5916 pg_catalog.pg_opclass.c.oid, pg_catalog.pg_opclass.c.opcname 

5917 ).where(~pg_catalog.pg_opclass.c.opcdefault) 

5918 

5919 @reflection.cache 

5920 def _load_pg_opclass_notdefault_dict( 

5921 self, connection, **kw 

5922 ) -> dict[int, str]: 

5923 rows = connection.execute(self._pg_opclass_notdefault_query) 

5924 return dict(rows.all()) 

5925 

5926 def _set_backslash_escapes(self, connection): 

5927 # this method is provided as an override hook for descendant 

5928 # dialects (e.g. Redshift), so removing it may break them 

5929 std_string = connection.exec_driver_sql( 

5930 "show standard_conforming_strings" 

5931 ).scalar() 

5932 self._backslash_escapes = std_string == "off" 

5933 

5934 

5935class _NamedTypeLoader: 

5936 """Helper class used for deferred loading of named types (enums, domains) 

5937 only when needed. 

5938 """ 

5939 

5940 def __init__( 

5941 self, dialect: PGDialect, connection, kw: Dict[str, Any] 

5942 ) -> None: 

5943 self.dialect = dialect 

5944 self.connection = connection 

5945 self.kw = kw 

5946 

5947 @util.memoized_property 

5948 def enums(self) -> Dict[Tuple[str] | Tuple[str, str], ReflectedEnum]: 

5949 # dictionary with (name, ) if default search path or (schema, name) 

5950 # as keys 

5951 enums = dict( 

5952 ( 

5953 ((rec["name"],), rec) 

5954 if rec["visible"] 

5955 else ((rec["schema"], rec["name"]), rec) 

5956 ) 

5957 for rec in self.dialect._load_enums( 

5958 self.connection, 

5959 schema="*", 

5960 info_cache=self.kw.get("info_cache"), 

5961 ) 

5962 ) 

5963 return enums 

5964 

5965 @util.memoized_property 

5966 def domains(self) -> Dict[Tuple[str] | Tuple[str, str], ReflectedDomain]: 

5967 # dictionary with (name, ) if default search path or (schema, name) 

5968 # as keys 

5969 domains = { 

5970 ((d["schema"], d["name"]) if not d["visible"] else (d["name"],)): d 

5971 for d in self.dialect._load_domains( 

5972 self.connection, 

5973 schema="*", 

5974 info_cache=self.kw.get("info_cache"), 

5975 ) 

5976 } 

5977 return domains