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

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

1199 statements  

1# dialects/oracle/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 

9 

10r""" 

11.. dialect:: oracle 

12 :name: Oracle Database 

13 :normal_support: 11+ 

14 :best_effort: 9+ 

15 

16 

17Auto Increment Behavior 

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

19 

20SQLAlchemy Table objects which include integer primary keys are usually assumed 

21to have "autoincrementing" behavior, meaning they can generate their own 

22primary key values upon INSERT. For use within Oracle Database, two options are 

23available, which are the use of IDENTITY columns (Oracle Database 12 and above 

24only) or the association of a SEQUENCE with the column. 

25 

26Specifying GENERATED AS IDENTITY (Oracle Database 12 and above) 

27~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

28 

29Starting from version 12, Oracle Database can make use of identity columns 

30using the :class:`_sql.Identity` to specify the autoincrementing behavior:: 

31 

32 t = Table( 

33 "mytable", 

34 metadata, 

35 Column("id", Integer, Identity(start=3), primary_key=True), 

36 Column(...), 

37 ..., 

38 ) 

39 

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

41 

42.. sourcecode:: sql 

43 

44 CREATE TABLE mytable ( 

45 id INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 3), 

46 ..., 

47 PRIMARY KEY (id) 

48 ) 

49 

50The :class:`_schema.Identity` object support many options to control the 

51"autoincrementing" behavior of the column, like the starting value, the 

52incrementing value, etc. In addition to the standard options, Oracle Database 

53supports setting :paramref:`_schema.Identity.always` to ``None`` to use the 

54default generated mode, rendering GENERATED AS IDENTITY in the DDL. Oracle 

55Database also supports two custom options specified using dialect kwargs: 

56 

57* ``oracle_on_null``: when set to ``True`` renders ``ON NULL`` in conjunction 

58 with a 'BY DEFAULT' identity column. 

59* ``oracle_order``: when ``True``, renders the ORDER keyword, indicating the 

60 identity is definitively ordered. May be necessary to provide deterministic 

61 ordering using Oracle Real Application Clusters (RAC). 

62 

63Using a SEQUENCE (all Oracle Database versions) 

64~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

65 

66Older version of Oracle Database had no "autoincrement" feature: SQLAlchemy 

67relies upon sequences to produce these values. With the older Oracle Database 

68versions, *a sequence must always be explicitly specified to enable 

69autoincrement*. This is divergent with the majority of documentation examples 

70which assume the usage of an autoincrement-capable database. To specify 

71sequences, use the sqlalchemy.schema.Sequence object which is passed to a 

72Column construct:: 

73 

74 t = Table( 

75 "mytable", 

76 metadata, 

77 Column("id", Integer, Sequence("id_seq", start=1), primary_key=True), 

78 Column(...), 

79 ..., 

80 ) 

81 

82This step is also required when using table reflection, i.e. autoload_with=engine:: 

83 

84 t = Table( 

85 "mytable", 

86 metadata, 

87 Column("id", Integer, Sequence("id_seq", start=1), primary_key=True), 

88 autoload_with=engine, 

89 ) 

90 

91In addition to the standard options, Oracle Database supports the following 

92custom option specified using dialect kwargs: 

93 

94* ``oracle_order``: when ``True``, renders the ORDER keyword, indicating the 

95 sequence is definitively ordered. May be necessary to provide deterministic 

96 ordering using Oracle RAC. 

97 

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

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

100 column. 

101 

102.. _oracle_isolation_level: 

103 

104Transaction Isolation Level / Autocommit 

105---------------------------------------- 

106 

107Oracle Database supports "READ COMMITTED" and "SERIALIZABLE" modes of 

108isolation. The AUTOCOMMIT isolation level is also supported by the 

109python-oracledb and cx_Oracle dialects. 

110 

111To set using per-connection execution options:: 

112 

113 connection = engine.connect() 

114 connection = connection.execution_options(isolation_level="AUTOCOMMIT") 

115 

116For ``READ COMMITTED`` and ``SERIALIZABLE``, the Oracle Database dialects sets 

117the level at the session level using ``ALTER SESSION``, which is reverted back 

118to its default setting when the connection is returned to the connection pool. 

119 

120Valid values for ``isolation_level`` include: 

121 

122* ``READ COMMITTED`` 

123* ``AUTOCOMMIT`` 

124* ``SERIALIZABLE`` 

125 

126.. note:: The implementation for the 

127 :meth:`_engine.Connection.get_isolation_level` method as implemented by the 

128 Oracle Database dialects necessarily force the start of a transaction using the 

129 Oracle Database DBMS_TRANSACTION.LOCAL_TRANSACTION_ID function; otherwise no 

130 level is normally readable. 

131 

132 Additionally, the :meth:`_engine.Connection.get_isolation_level` method will 

133 raise an exception if the ``v$transaction`` view is not available due to 

134 permissions or other reasons, which is a common occurrence in Oracle Database 

135 installations. 

136 

137 The python-oracledb and cx_Oracle dialects attempt to call the 

138 :meth:`_engine.Connection.get_isolation_level` method when the dialect makes 

139 its first connection to the database in order to acquire the 

140 "default"isolation level. This default level is necessary so that the level 

141 can be reset on a connection after it has been temporarily modified using 

142 :meth:`_engine.Connection.execution_options` method. In the common event 

143 that the :meth:`_engine.Connection.get_isolation_level` method raises an 

144 exception due to ``v$transaction`` not being readable as well as any other 

145 database-related failure, the level is assumed to be "READ COMMITTED". No 

146 warning is emitted for this initial first-connect condition as it is 

147 expected to be a common restriction on Oracle databases. 

148 

149.. seealso:: 

150 

151 :ref:`dbapi_autocommit` 

152 

153Identifier Casing 

154----------------- 

155 

156In Oracle Database, the data dictionary represents all case insensitive 

157identifier names using UPPERCASE text. This is in contradiction to the 

158expectations of SQLAlchemy, which assume a case insensitive name is represented 

159as lowercase text. 

160 

161As an example of case insensitive identifier names, consider the following table: 

162 

163.. sourcecode:: sql 

164 

165 CREATE TABLE MyTable (Identifier INTEGER PRIMARY KEY) 

166 

167If you were to ask Oracle Database for information about this table, the 

168table name would be reported as ``MYTABLE`` and the column name would 

169be reported as ``IDENTIFIER``. Compare to most other databases such as 

170PostgreSQL and MySQL which would report these names as ``mytable`` and 

171``identifier``. The names are **not quoted, therefore are case insensitive**. 

172The special casing of ``MyTable`` and ``Identifier`` would only be maintained 

173if they were quoted in the table definition: 

174 

175.. sourcecode:: sql 

176 

177 CREATE TABLE "MyTable" ("Identifier" INTEGER PRIMARY KEY) 

178 

179When constructing a SQLAlchemy :class:`.Table` object, **an all lowercase name 

180is considered to be case insensitive**. So the following table assumes 

181case insensitive names:: 

182 

183 Table("mytable", metadata, Column("identifier", Integer, primary_key=True)) 

184 

185Whereas when mixed case or UPPERCASE names are used, case sensitivity is 

186assumed:: 

187 

188 Table("MyTable", metadata, Column("Identifier", Integer, primary_key=True)) 

189 

190A similar situation occurs at the database driver level when emitting a 

191textual SQL SELECT statement and looking at column names in the DBAPI 

192``cursor.description`` attribute. A database like PostgreSQL will normalize 

193case insensitive names to be lowercase:: 

194 

195 >>> pg_engine = create_engine("postgresql://scott:tiger@localhost/test") 

196 >>> pg_connection = pg_engine.connect() 

197 >>> result = pg_connection.exec_driver_sql("SELECT 1 AS SomeName") 

198 >>> result.cursor.description 

199 (Column(name='somename', type_code=23),) 

200 

201Whereas Oracle normalizes them to UPPERCASE:: 

202 

203 >>> oracle_engine = create_engine("oracle+oracledb://scott:tiger@oracle18c/xe") 

204 >>> oracle_connection = oracle_engine.connect() 

205 >>> result = oracle_connection.exec_driver_sql( 

206 ... "SELECT 1 AS SomeName FROM DUAL" 

207 ... ) 

208 >>> result.cursor.description 

209 [('SOMENAME', <DbType DB_TYPE_NUMBER>, 127, None, 0, -127, True)] 

210 

211In order to achieve cross-database parity for the two cases of a. table 

212reflection and b. textual-only SQL statement round trips, SQLAlchemy performs a step 

213called **name normalization** when using the Oracle dialect. This process may 

214also apply to other third party dialects that have similar UPPERCASE handling 

215of case insensitive names. 

216 

217When using name normalization, SQLAlchemy attempts to detect if a name is 

218case insensitive by checking if all characters are UPPERCASE letters only; 

219if so, then it assumes this is a case insensitive name and is delivered as 

220a lowercase name. 

221 

222For table reflection, a tablename that is seen represented as all UPPERCASE 

223in Oracle Database's catalog tables will be assumed to have a case insensitive 

224name. This is what allows the ``Table`` definition to use lower case names 

225and be equally compatible from a reflection point of view on Oracle Database 

226and all other databases such as PostgreSQL and MySQL:: 

227 

228 # matches a table created with CREATE TABLE mytable 

229 Table("mytable", metadata, autoload_with=some_engine) 

230 

231Above, the all lowercase name ``"mytable"`` is case insensitive; it will match 

232a table reported by PostgreSQL as ``"mytable"`` and a table reported by 

233Oracle as ``"MYTABLE"``. If name normalization were not present, it would 

234not be possible for the above :class:`.Table` definition to be introspectable 

235in a cross-database way, since we are dealing with a case insensitive name 

236that is not reported by each database in the same way. 

237 

238Case sensitivity can be forced on in this case, such as if we wanted to represent 

239the quoted tablename ``"MYTABLE"`` with that exact casing, most simply by using 

240that casing directly, which will be seen as a case sensitive name:: 

241 

242 # matches a table created with CREATE TABLE "MYTABLE" 

243 Table("MYTABLE", metadata, autoload_with=some_engine) 

244 

245For the unusual case of a quoted all-lowercase name, the :class:`.quoted_name` 

246construct may be used:: 

247 

248 from sqlalchemy import quoted_name 

249 

250 # matches a table created with CREATE TABLE "mytable" 

251 Table( 

252 quoted_name("mytable", quote=True), metadata, autoload_with=some_engine 

253 ) 

254 

255Name normalization also takes place when handling result sets from **purely 

256textual SQL strings**, that have no other :class:`.Table` or :class:`.Column` 

257metadata associated with them. This includes SQL strings executed using 

258:meth:`.Connection.exec_driver_sql` and SQL strings executed using the 

259:func:`.text` construct which do not include :class:`.Column` metadata. 

260 

261Returning to the Oracle Database SELECT statement, we see that even though 

262``cursor.description`` reports the column name as ``SOMENAME``, SQLAlchemy 

263name normalizes this to ``somename``:: 

264 

265 >>> oracle_engine = create_engine("oracle+oracledb://scott:tiger@oracle18c/xe") 

266 >>> oracle_connection = oracle_engine.connect() 

267 >>> result = oracle_connection.exec_driver_sql( 

268 ... "SELECT 1 AS SomeName FROM DUAL" 

269 ... ) 

270 >>> result.cursor.description 

271 [('SOMENAME', <DbType DB_TYPE_NUMBER>, 127, None, 0, -127, True)] 

272 >>> result.keys() 

273 RMKeyView(['somename']) 

274 

275The single scenario where the above behavior produces inaccurate results 

276is when using an all-uppercase, quoted name. SQLAlchemy has no way to determine 

277that a particular name in ``cursor.description`` was quoted, and is therefore 

278case sensitive, or was not quoted, and should be name normalized:: 

279 

280 >>> result = oracle_connection.exec_driver_sql( 

281 ... 'SELECT 1 AS "SOMENAME" FROM DUAL' 

282 ... ) 

283 >>> result.cursor.description 

284 [('SOMENAME', <DbType DB_TYPE_NUMBER>, 127, None, 0, -127, True)] 

285 >>> result.keys() 

286 RMKeyView(['somename']) 

287 

288For this exact scenario, SQLAlchemy offers the :paramref:`.Connection.execution_options.driver_column_names` 

289execution options, which turns off name normalize for result sets:: 

290 

291 >>> result = oracle_connection.exec_driver_sql( 

292 ... 'SELECT 1 AS "SOMENAME" FROM DUAL', 

293 ... execution_options={"driver_column_names": True}, 

294 ... ) 

295 >>> result.keys() 

296 RMKeyView(['SOMENAME']) 

297 

298.. versionadded:: 2.1 Added the :paramref:`.Connection.execution_options.driver_column_names` 

299 execution option 

300 

301 

302.. _oracle_max_identifier_lengths: 

303 

304Maximum Identifier Lengths 

305-------------------------- 

306 

307SQLAlchemy is sensitive to the maximum identifier length supported by Oracle 

308Database. This affects generated SQL label names as well as the generation of 

309constraint names, particularly in the case where the constraint naming 

310convention feature described at :ref:`constraint_naming_conventions` is being 

311used. 

312 

313Oracle Database 12.2 increased the default maximum identifier length from 30 to 

314128. As of SQLAlchemy 1.4, the default maximum identifier length for the Oracle 

315dialects is 128 characters. Upon first connection, the maximum length actually 

316supported by the database is obtained. In all cases, setting the 

317:paramref:`_sa.create_engine.max_identifier_length` parameter will bypass this 

318change and the value given will be used as is:: 

319 

320 engine = create_engine( 

321 "oracle+oracledb://scott:tiger@localhost:1521?service_name=freepdb1", 

322 max_identifier_length=30, 

323 ) 

324 

325If :paramref:`_sa.create_engine.max_identifier_length` is not set, the oracledb 

326dialect internally uses the ``max_identifier_length`` attribute available on 

327driver connections since python-oracledb version 2.5. When using an older 

328driver version, or using the cx_Oracle dialect, SQLAlchemy will instead attempt 

329to use the query ``SELECT value FROM v$parameter WHERE name = 'compatible'`` 

330upon first connect in order to determine the effective compatibility version of 

331the database. The "compatibility" version is a version number that is 

332independent of the actual database version. It is used to assist database 

333migration. It is configured by an Oracle Database initialization parameter. The 

334compatibility version then determines the maximum allowed identifier length for 

335the database. If the V$ view is not available, the database version information 

336is used instead. 

337 

338The maximum identifier length comes into play both when generating anonymized 

339SQL labels in SELECT statements, but more crucially when generating constraint 

340names from a naming convention. It is this area that has created the need for 

341SQLAlchemy to change this default conservatively. For example, the following 

342naming convention produces two very different constraint names based on the 

343identifier length:: 

344 

345 from sqlalchemy import Column 

346 from sqlalchemy import Index 

347 from sqlalchemy import Integer 

348 from sqlalchemy import MetaData 

349 from sqlalchemy import Table 

350 from sqlalchemy.dialects import oracle 

351 from sqlalchemy.schema import CreateIndex 

352 

353 m = MetaData(naming_convention={"ix": "ix_%(column_0N_name)s"}) 

354 

355 t = Table( 

356 "t", 

357 m, 

358 Column("some_column_name_1", Integer), 

359 Column("some_column_name_2", Integer), 

360 Column("some_column_name_3", Integer), 

361 ) 

362 

363 ix = Index( 

364 None, 

365 t.c.some_column_name_1, 

366 t.c.some_column_name_2, 

367 t.c.some_column_name_3, 

368 ) 

369 

370 oracle_dialect = oracle.dialect(max_identifier_length=30) 

371 print(CreateIndex(ix).compile(dialect=oracle_dialect)) 

372 

373With an identifier length of 30, the above CREATE INDEX looks like: 

374 

375.. sourcecode:: sql 

376 

377 CREATE INDEX ix_some_column_name_1s_70cd ON t 

378 (some_column_name_1, some_column_name_2, some_column_name_3) 

379 

380However with length of 128, it becomes:: 

381 

382.. sourcecode:: sql 

383 

384 CREATE INDEX ix_some_column_name_1some_column_name_2some_column_name_3 ON t 

385 (some_column_name_1, some_column_name_2, some_column_name_3) 

386 

387Applications which have run versions of SQLAlchemy prior to 1.4 on Oracle 

388Database version 12.2 or greater are therefore subject to the scenario of a 

389database migration that wishes to "DROP CONSTRAINT" on a name that was 

390previously generated with the shorter length. This migration will fail when 

391the identifier length is changed without the name of the index or constraint 

392first being adjusted. Such applications are strongly advised to make use of 

393:paramref:`_sa.create_engine.max_identifier_length` in order to maintain 

394control of the generation of truncated names, and to fully review and test all 

395database migrations in a staging environment when changing this value to ensure 

396that the impact of this change has been mitigated. 

397 

398.. versionchanged:: 1.4 the default max_identifier_length for Oracle Database 

399 is 128 characters, which is adjusted down to 30 upon first connect if the 

400 Oracle Database, or its compatibility setting, are lower than version 12.2. 

401 

402 

403LIMIT/OFFSET/FETCH Support 

404-------------------------- 

405 

406Methods like :meth:`_sql.Select.limit` and :meth:`_sql.Select.offset` make use 

407of ``FETCH FIRST N ROW / OFFSET N ROWS`` syntax assuming Oracle Database 12c or 

408above, and assuming the SELECT statement is not embedded within a compound 

409statement like UNION. This syntax is also available directly by using the 

410:meth:`_sql.Select.fetch` method. 

411 

412.. versionchanged:: 2.0 the Oracle Database dialects now use ``FETCH FIRST N 

413 ROW / OFFSET N ROWS`` for all :meth:`_sql.Select.limit` and 

414 :meth:`_sql.Select.offset` usage including within the ORM and legacy 

415 :class:`_orm.Query`. To force the legacy behavior using window functions, 

416 specify the ``enable_offset_fetch=False`` dialect parameter to 

417 :func:`_sa.create_engine`. 

418 

419The use of ``FETCH FIRST / OFFSET`` may be disabled on any Oracle Database 

420version by passing ``enable_offset_fetch=False`` to :func:`_sa.create_engine`, 

421which will force the use of "legacy" mode that makes use of window functions. 

422This mode is also selected automatically when using a version of Oracle 

423Database prior to 12c. 

424 

425When using legacy mode, or when a :class:`.Select` statement with limit/offset 

426is embedded in a compound statement, an emulated approach for LIMIT / OFFSET 

427based on window functions is used, which involves creation of a subquery using 

428``ROW_NUMBER`` that is prone to performance issues as well as SQL construction 

429issues for complex statements. However, this approach is supported by all 

430Oracle Database versions. See notes below. 

431 

432Notes on LIMIT / OFFSET emulation (when fetch() method cannot be used) 

433~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

434 

435If using :meth:`_sql.Select.limit` and :meth:`_sql.Select.offset`, or with the 

436ORM the :meth:`_orm.Query.limit` and :meth:`_orm.Query.offset` methods on an 

437Oracle Database version prior to 12c, the following notes apply: 

438 

439* SQLAlchemy currently makes use of ROWNUM to achieve 

440 LIMIT/OFFSET; the exact methodology is taken from 

441 https://blogs.oracle.com/oraclemagazine/on-rownum-and-limiting-results . 

442 

443* the "FIRST_ROWS()" optimization keyword is not used by default. To enable 

444 the usage of this optimization directive, specify ``optimize_limits=True`` 

445 to :func:`_sa.create_engine`. 

446 

447 .. versionchanged:: 1.4 

448 

449 The Oracle Database dialect renders limit/offset integer values using a 

450 "post compile" scheme which renders the integer directly before passing 

451 the statement to the cursor for execution. The ``use_binds_for_limits`` 

452 flag no longer has an effect. 

453 

454 .. seealso:: 

455 

456 :ref:`change_4808`. 

457 

458.. _oracle_returning: 

459 

460RETURNING Support 

461----------------- 

462 

463Oracle Database supports RETURNING fully for INSERT, UPDATE and DELETE 

464statements that are invoked with a single collection of bound parameters (that 

465is, a ``cursor.execute()`` style statement; SQLAlchemy does not generally 

466support RETURNING with :term:`executemany` statements). Multiple rows may be 

467returned as well. 

468 

469.. versionchanged:: 2.0 the Oracle Database backend has full support for 

470 RETURNING on parity with other backends. 

471 

472 

473ON UPDATE CASCADE 

474----------------- 

475 

476Oracle Database doesn't have native ON UPDATE CASCADE functionality. A trigger 

477based solution is available at 

478https://web.archive.org/web/20090317041251/https://asktom.oracle.com/tkyte/update_cascade/index.html 

479 

480When using the SQLAlchemy ORM, the ORM has limited ability to manually issue 

481cascading updates - specify ForeignKey objects using the 

482"deferrable=True, initially='deferred'" keyword arguments, 

483and specify "passive_updates=False" on each relationship(). 

484 

485Oracle Database 8 Compatibility 

486------------------------------- 

487 

488.. warning:: The status of Oracle Database 8 compatibility is not known for 

489 SQLAlchemy 2.0. 

490 

491When Oracle Database 8 is detected, the dialect internally configures itself to 

492the following behaviors: 

493 

494* the use_ansi flag is set to False. This has the effect of converting all 

495 JOIN phrases into the WHERE clause, and in the case of LEFT OUTER JOIN 

496 makes use of Oracle's (+) operator. 

497 

498* the NVARCHAR2 and NCLOB datatypes are no longer generated as DDL when 

499 the :class:`~sqlalchemy.types.Unicode` is used - VARCHAR2 and CLOB are issued 

500 instead. This because these types don't seem to work correctly on Oracle 8 

501 even though they are available. The :class:`~sqlalchemy.types.NVARCHAR` and 

502 :class:`~sqlalchemy.dialects.oracle.NCLOB` types will always generate 

503 NVARCHAR2 and NCLOB. 

504 

505 

506Synonym/DBLINK Reflection 

507------------------------- 

508 

509When using reflection with Table objects, the dialect can optionally search 

510for tables indicated by synonyms, either in local or remote schemas or 

511accessed over DBLINK, by passing the flag ``oracle_resolve_synonyms=True`` as 

512a keyword argument to the :class:`_schema.Table` construct:: 

513 

514 some_table = Table( 

515 "some_table", autoload_with=some_engine, oracle_resolve_synonyms=True 

516 ) 

517 

518When this flag is set, the given name (such as ``some_table`` above) will be 

519searched not just in the ``ALL_TABLES`` view, but also within the 

520``ALL_SYNONYMS`` view to see if this name is actually a synonym to another 

521name. If the synonym is located and refers to a DBLINK, the Oracle Database 

522dialects know how to locate the table's information using DBLINK syntax(e.g. 

523``@dblink``). 

524 

525``oracle_resolve_synonyms`` is accepted wherever reflection arguments are 

526accepted, including methods such as :meth:`_schema.MetaData.reflect` and 

527:meth:`_reflection.Inspector.get_columns`. 

528 

529If synonyms are not in use, this flag should be left disabled. 

530 

531.. _oracle_constraint_reflection: 

532 

533Constraint Reflection 

534--------------------- 

535 

536The Oracle Database dialects can return information about foreign key, unique, 

537and CHECK constraints, as well as indexes on tables. 

538 

539Raw information regarding these constraints can be acquired using 

540:meth:`_reflection.Inspector.get_foreign_keys`, 

541:meth:`_reflection.Inspector.get_unique_constraints`, 

542:meth:`_reflection.Inspector.get_check_constraints`, and 

543:meth:`_reflection.Inspector.get_indexes`. 

544 

545When using reflection at the :class:`_schema.Table` level, the 

546:class:`_schema.Table` 

547will also include these constraints. 

548 

549Note the following caveats: 

550 

551* When using the :meth:`_reflection.Inspector.get_check_constraints` method, 

552 Oracle Database builds a special "IS NOT NULL" constraint for columns that 

553 specify "NOT NULL". This constraint is **not** returned by default; to 

554 include the "IS NOT NULL" constraints, pass the flag ``include_all=True``:: 

555 

556 from sqlalchemy import create_engine, inspect 

557 

558 engine = create_engine( 

559 "oracle+oracledb://scott:tiger@localhost:1521?service_name=freepdb1" 

560 ) 

561 inspector = inspect(engine) 

562 all_check_constraints = inspector.get_check_constraints( 

563 "some_table", include_all=True 

564 ) 

565 

566* in most cases, when reflecting a :class:`_schema.Table`, a UNIQUE constraint 

567 will **not** be available as a :class:`.UniqueConstraint` object, as Oracle 

568 Database mirrors unique constraints with a UNIQUE index in most cases (the 

569 exception seems to be when two or more unique constraints represent the same 

570 columns); the :class:`_schema.Table` will instead represent these using 

571 :class:`.Index` with the ``unique=True`` flag set. 

572 

573* Oracle Database creates an implicit index for the primary key of a table; 

574 this index is **excluded** from all index results. 

575 

576* the list of columns reflected for an index will not include column names 

577 that start with SYS_NC. 

578 

579Table names with SYSTEM/SYSAUX tablespaces 

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

581 

582The :meth:`_reflection.Inspector.get_table_names` and 

583:meth:`_reflection.Inspector.get_temp_table_names` 

584methods each return a list of table names for the current engine. These methods 

585are also part of the reflection which occurs within an operation such as 

586:meth:`_schema.MetaData.reflect`. By default, 

587these operations exclude the ``SYSTEM`` 

588and ``SYSAUX`` tablespaces from the operation. In order to change this, the 

589default list of tablespaces excluded can be changed at the engine level using 

590the ``exclude_tablespaces`` parameter:: 

591 

592 # exclude SYSAUX and SOME_TABLESPACE, but not SYSTEM 

593 e = create_engine( 

594 "oracle+oracledb://scott:tiger@localhost:1521/?service_name=freepdb1", 

595 exclude_tablespaces=["SYSAUX", "SOME_TABLESPACE"], 

596 ) 

597 

598.. _oracle_float_support: 

599 

600FLOAT / DOUBLE Support and Behaviors 

601------------------------------------ 

602 

603The SQLAlchemy :class:`.Float` and :class:`.Double` datatypes are generic 

604datatypes that resolve to the "least surprising" datatype for a given backend. 

605For Oracle Database, this means they resolve to the ``FLOAT`` and ``DOUBLE`` 

606types:: 

607 

608 >>> from sqlalchemy import cast, literal, Float 

609 >>> from sqlalchemy.dialects import oracle 

610 >>> float_datatype = Float() 

611 >>> print(cast(literal(5.0), float_datatype).compile(dialect=oracle.dialect())) 

612 CAST(:param_1 AS FLOAT) 

613 

614Oracle's ``FLOAT`` / ``DOUBLE`` datatypes are aliases for ``NUMBER``. Oracle 

615Database stores ``NUMBER`` values with full precision, not floating point 

616precision, which means that ``FLOAT`` / ``DOUBLE`` do not actually behave like 

617native FP values. Oracle Database instead offers special datatypes 

618``BINARY_FLOAT`` and ``BINARY_DOUBLE`` to deliver real 4- and 8- byte FP 

619values. 

620 

621SQLAlchemy supports these datatypes directly using :class:`.BINARY_FLOAT` and 

622:class:`.BINARY_DOUBLE`. To use the :class:`.Float` or :class:`.Double` 

623datatypes in a database agnostic way, while allowing Oracle backends to utilize 

624one of these types, use the :meth:`.TypeEngine.with_variant` method to set up a 

625variant:: 

626 

627 >>> from sqlalchemy import cast, literal, Float 

628 >>> from sqlalchemy.dialects import oracle 

629 >>> float_datatype = Float().with_variant(oracle.BINARY_FLOAT(), "oracle") 

630 >>> print(cast(literal(5.0), float_datatype).compile(dialect=oracle.dialect())) 

631 CAST(:param_1 AS BINARY_FLOAT) 

632 

633E.g. to use this datatype in a :class:`.Table` definition:: 

634 

635 my_table = Table( 

636 "my_table", 

637 metadata, 

638 Column( 

639 "fp_data", Float().with_variant(oracle.BINARY_FLOAT(), "oracle") 

640 ), 

641 ) 

642 

643.. _oracle_boolean_support: 

644 

645Boolean Support 

646--------------- 

647 

648.. versionadded:: 2.1 

649 

650Oracle Database 23ai introduced native support for the ``BOOLEAN`` datatype. 

651The Oracle dialect automatically detects the database version and uses the 

652native ``BOOLEAN`` type when available, or falls back to emulation using 

653``SMALLINT`` on older Oracle versions. 

654 

655The standard :class:`_types.Boolean` type can be used in table definitions:: 

656 

657 from sqlalchemy import Boolean, Column, Integer, Table, MetaData 

658 

659 metadata = MetaData() 

660 

661 my_table = Table( 

662 "my_table", 

663 metadata, 

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

665 Column("flag", Boolean), 

666 ) 

667 

668On Oracle 23ai and later, this will generate DDL using the native ``BOOLEAN`` type: 

669 

670.. code-block:: sql 

671 

672 CREATE TABLE my_table ( 

673 id INTEGER NOT NULL, 

674 flag BOOLEAN, 

675 PRIMARY KEY (id) 

676 ) 

677 

678On earlier Oracle versions, it will use ``SMALLINT`` for storage with appropriate 

679constraints and conversions. 

680 

681The :class:`_types.Boolean` type is also available as ``BOOLEAN`` from the Oracle 

682dialect for consistency with other type names:: 

683 

684 from sqlalchemy.dialects.oracle import BOOLEAN 

685 

686DateTime Compatibility 

687---------------------- 

688 

689Oracle Database has no datatype known as ``DATETIME``, it instead has only 

690``DATE``, which can actually store a date and time value. For this reason, the 

691Oracle Database dialects provide a type :class:`_oracle.DATE` which is a 

692subclass of :class:`.DateTime`. This type has no special behavior, and is only 

693present as a "marker" for this type; additionally, when a database column is 

694reflected and the type is reported as ``DATE``, the time-supporting 

695:class:`_oracle.DATE` type is used. 

696 

697.. _oracle_table_options: 

698 

699Oracle Database Table Options 

700----------------------------- 

701 

702The CREATE TABLE phrase supports the following options with Oracle Database 

703dialects in conjunction with the :class:`_schema.Table` construct: 

704 

705 

706* ``ON COMMIT``:: 

707 

708 Table( 

709 "some_table", 

710 metadata, 

711 ..., 

712 prefixes=["GLOBAL TEMPORARY"], 

713 oracle_on_commit="PRESERVE ROWS", 

714 ) 

715 

716* 

717 ``COMPRESS``:: 

718 

719 Table( 

720 "mytable", metadata, Column("data", String(32)), oracle_compress=True 

721 ) 

722 

723 Table("mytable", metadata, Column("data", String(32)), oracle_compress=6) 

724 

725 The ``oracle_compress`` parameter accepts either an integer compression 

726 level, or ``True`` to use the default compression level. 

727 

728* 

729 ``TABLESPACE``:: 

730 

731 Table("mytable", metadata, ..., oracle_tablespace="EXAMPLE_TABLESPACE") 

732 

733 The ``oracle_tablespace`` parameter specifies the tablespace in which the 

734 table is to be created. This is useful when you want to create a table in a 

735 tablespace other than the default tablespace of the user. 

736 

737 .. versionadded:: 2.0.37 

738 

739.. _oracle_index_options: 

740 

741Oracle Database Specific Index Options 

742-------------------------------------- 

743 

744Bitmap Indexes 

745~~~~~~~~~~~~~~ 

746 

747You can specify the ``oracle_bitmap`` parameter to create a bitmap index 

748instead of a B-tree index:: 

749 

750 Index("my_index", my_table.c.data, oracle_bitmap=True) 

751 

752Bitmap indexes cannot be unique and cannot be compressed. SQLAlchemy will not 

753check for such limitations, only the database will. 

754 

755Index compression 

756~~~~~~~~~~~~~~~~~ 

757 

758Oracle Database has a more efficient storage mode for indexes containing lots 

759of repeated values. Use the ``oracle_compress`` parameter to turn on key 

760compression:: 

761 

762 Index("my_index", my_table.c.data, oracle_compress=True) 

763 

764 Index( 

765 "my_index", 

766 my_table.c.data1, 

767 my_table.c.data2, 

768 unique=True, 

769 oracle_compress=1, 

770 ) 

771 

772The ``oracle_compress`` parameter accepts either an integer specifying the 

773number of prefix columns to compress, or ``True`` to use the default (all 

774columns for non-unique indexes, all but the last column for unique indexes). 

775 

776.. _oracle_vector_datatype: 

777 

778VECTOR Datatype 

779--------------- 

780 

781Oracle Database 23ai introduced a new VECTOR datatype for artificial intelligence 

782and machine learning search operations. The VECTOR datatype is a homogeneous array 

783of 8-bit signed integers, 8-bit unsigned integers (binary), 32-bit floating-point 

784numbers, or 64-bit floating-point numbers. 

785 

786A vector's storage type can be either DENSE or SPARSE. A dense vector contains 

787meaningful values in most or all of its dimensions. In contrast, a sparse vector 

788has non-zero values in only a few dimensions, with the majority being zero. 

789 

790Sparse vectors are represented by the total number of vector dimensions, an array 

791of indices, and an array of values where each value’s location in the vector is 

792indicated by the corresponding indices array position. All other vector values are 

793treated as zero. 

794 

795The storage formats that can be used with sparse vectors are float32, float64, and 

796int8. Note that the binary storage format cannot be used with sparse vectors. 

797 

798Sparse vectors are supported when you are using Oracle Database 23.7 or later. 

799 

800.. seealso:: 

801 

802 `Using VECTOR Data 

803 <https://python-oracledb.readthedocs.io/en/latest/user_guide/vector_data_type.html>`_ - in the documentation 

804 for the :ref:`oracledb` driver. 

805 

806.. versionadded:: 2.0.41 - Added VECTOR datatype 

807 

808.. versionadded:: 2.0.43 - Added DENSE/SPARSE support 

809 

810CREATE TABLE support for VECTOR 

811~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

812 

813With the :class:`.VECTOR` datatype, you can specify the number of dimensions, 

814the storage format, and the storage type for the data. Valid values for the 

815storage format are enum members of :class:`.VectorStorageFormat`. Valid values 

816for the storage type are enum members of :class:`.VectorStorageType`. If 

817storage type is not specified, a DENSE vector is created by default. 

818 

819To create a table that includes a :class:`.VECTOR` column:: 

820 

821 from sqlalchemy.dialects.oracle import ( 

822 VECTOR, 

823 VectorStorageFormat, 

824 VectorStorageType, 

825 ) 

826 

827 t = Table( 

828 "t1", 

829 metadata, 

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

831 Column( 

832 "embedding", 

833 VECTOR( 

834 dim=3, 

835 storage_format=VectorStorageFormat.FLOAT32, 

836 storage_type=VectorStorageType.SPARSE, 

837 ), 

838 ), 

839 Column(...), 

840 ..., 

841 ) 

842 

843Vectors can also be defined with an arbitrary number of dimensions and formats. 

844This allows you to specify vectors of different dimensions with the various 

845storage formats mentioned below. 

846 

847**Examples** 

848 

849* In this case, the storage format is flexible, allowing any vector type data to be 

850 inserted, such as INT8 or BINARY etc:: 

851 

852 vector_col: Mapped[array.array] = mapped_column(VECTOR(dim=3)) 

853 

854* The dimension is flexible in this case, meaning that any dimension vector can 

855 be used:: 

856 

857 vector_col: Mapped[array.array] = mapped_column( 

858 VECTOR(storage_format=VectorStorageType.INT8) 

859 ) 

860 

861* Both the dimensions and the storage format are flexible. It creates a DENSE vector:: 

862 

863 vector_col: Mapped[array.array] = mapped_column(VECTOR) 

864 

865* To create a SPARSE vector with both dimensions and the storage format as flexible, 

866 use the :attr:`.VectorStorageType.SPARSE` storage type:: 

867 

868 vector_col: Mapped[array.array] = mapped_column( 

869 VECTOR(storage_type=VectorStorageType.SPARSE) 

870 ) 

871 

872Python Datatypes for VECTOR 

873~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

874 

875VECTOR data can be inserted using Python list or Python ``array.array()`` objects. 

876Python arrays of type FLOAT (32-bit), DOUBLE (64-bit), INT (8-bit signed integers), 

877or BINARY (8-bit unsigned integers) are used as bind values when inserting 

878VECTOR columns:: 

879 

880 from sqlalchemy import insert, select 

881 

882 with engine.begin() as conn: 

883 conn.execute( 

884 insert(t1), 

885 {"id": 1, "embedding": [1, 2, 3]}, 

886 ) 

887 

888Data can be inserted into a sparse vector using the :class:`_oracle.SparseVector` 

889class, creating an object consisting of the number of dimensions, an array of indices, and a 

890corresponding array of values:: 

891 

892 from sqlalchemy import insert, select 

893 from sqlalchemy.dialects.oracle import SparseVector 

894 

895 sparse_val = SparseVector(10, [1, 2], array.array("d", [23.45, 221.22])) 

896 

897 with engine.begin() as conn: 

898 conn.execute( 

899 insert(t1), 

900 {"id": 1, "embedding": sparse_val}, 

901 ) 

902 

903VECTOR Indexes 

904~~~~~~~~~~~~~~ 

905 

906The VECTOR feature supports an Oracle-specific parameter ``oracle_vector`` 

907on the :class:`.Index` construct, which allows the construction of VECTOR 

908indexes. 

909 

910SPARSE vectors cannot be used in the creation of vector indexes. 

911 

912To utilize VECTOR indexing, set the ``oracle_vector`` parameter to True to use 

913the default values provided by Oracle. HNSW is the default indexing method:: 

914 

915 from sqlalchemy import Index 

916 

917 Index( 

918 "vector_index", 

919 t1.c.embedding, 

920 oracle_vector=True, 

921 ) 

922 

923The full range of parameters for vector indexes are available by using the 

924:class:`.VectorIndexConfig` dataclass in place of a boolean; this dataclass 

925allows full configuration of the index:: 

926 

927 Index( 

928 "hnsw_vector_index", 

929 t1.c.embedding, 

930 oracle_vector=VectorIndexConfig( 

931 index_type=VectorIndexType.HNSW, 

932 distance=VectorDistanceType.COSINE, 

933 accuracy=90, 

934 hnsw_neighbors=5, 

935 hnsw_efconstruction=20, 

936 parallel=10, 

937 ), 

938 ) 

939 

940 Index( 

941 "ivf_vector_index", 

942 t1.c.embedding, 

943 oracle_vector=VectorIndexConfig( 

944 index_type=VectorIndexType.IVF, 

945 distance=VectorDistanceType.DOT, 

946 accuracy=90, 

947 ivf_neighbor_partitions=5, 

948 ), 

949 ) 

950 

951For complete explanation of these parameters, see the Oracle documentation linked 

952below. 

953 

954.. seealso:: 

955 

956 `CREATE VECTOR INDEX <https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=GUID-B396C369-54BB-4098-A0DD-7C54B3A0D66F>`_ - in the Oracle documentation 

957 

958 

959 

960Similarity Searching 

961~~~~~~~~~~~~~~~~~~~~ 

962 

963When using the :class:`_oracle.VECTOR` datatype with a :class:`.Column` or similar 

964ORM mapped construct, additional comparison functions are available, including: 

965 

966* ``l2_distance`` 

967* ``cosine_distance`` 

968* ``inner_product`` 

969 

970Example Usage:: 

971 

972 result_vector = connection.scalars( 

973 select(t1).order_by(t1.embedding.l2_distance([2, 3, 4])).limit(3) 

974 ) 

975 

976 for user in vector: 

977 print(user.id, user.embedding) 

978 

979FETCH APPROXIMATE support 

980~~~~~~~~~~~~~~~~~~~~~~~~~ 

981 

982Approximate vector search can only be performed when all syntax and semantic 

983rules are satisfied, the corresponding vector index is available, and the 

984query optimizer determines to perform it. If any of these conditions are 

985unmet, then an approximate search is not performed. In this case the query 

986returns exact results. 

987 

988To enable approximate searching during similarity searches on VECTORS, the 

989``oracle_fetch_approximate`` parameter may be used with the :meth:`.Select.fetch` 

990clause to add ``FETCH APPROX`` to the SELECT statement:: 

991 

992 select(users_table).fetch(5, oracle_fetch_approximate=True) 

993 

994""" # noqa 

995 

996from __future__ import annotations 

997 

998from collections import defaultdict 

999from dataclasses import fields 

1000from functools import lru_cache 

1001from functools import wraps 

1002import re 

1003from typing import Any 

1004from typing import Callable 

1005from typing import TYPE_CHECKING 

1006 

1007from . import dictionary 

1008from .json import JSON 

1009from .json import JSONIndexType 

1010from .json import JSONPathType 

1011from .types import _OracleBoolean 

1012from .types import _OracleDate 

1013from .types import BFILE 

1014from .types import BINARY_DOUBLE 

1015from .types import BINARY_FLOAT 

1016from .types import BOOLEAN 

1017from .types import DATE 

1018from .types import FLOAT 

1019from .types import INTERVAL 

1020from .types import LONG 

1021from .types import NCLOB 

1022from .types import NUMBER 

1023from .types import NVARCHAR2 # noqa 

1024from .types import OracleRaw # noqa 

1025from .types import RAW 

1026from .types import ROWID # noqa 

1027from .types import TIMESTAMP 

1028from .types import VARCHAR2 # noqa 

1029from .vector import VECTOR 

1030from .vector import VectorIndexConfig 

1031from .vector import VectorIndexType 

1032from ... import Computed 

1033from ... import exc 

1034from ... import schema as sa_schema 

1035from ... import sql 

1036from ... import util 

1037from ...engine import default 

1038from ...engine import ObjectKind 

1039from ...engine import ObjectScope 

1040from ...engine import reflection 

1041from ...engine.reflection import ReflectionDefaults 

1042from ...sql import and_ 

1043from ...sql import bindparam 

1044from ...sql import compiler 

1045from ...sql import elements 

1046from ...sql import expression 

1047from ...sql import func 

1048from ...sql import null 

1049from ...sql import or_ 

1050from ...sql import select 

1051from ...sql import selectable as sa_selectable 

1052from ...sql import sqltypes 

1053from ...sql import util as sql_util 

1054from ...sql import visitors 

1055from ...sql.base import NO_ARG 

1056from ...sql.compiler import AggregateOrderByStyle 

1057from ...sql.visitors import InternalTraversal 

1058from ...types import BLOB 

1059from ...types import CHAR 

1060from ...types import CLOB 

1061from ...types import DOUBLE_PRECISION 

1062from ...types import INTEGER 

1063from ...types import NCHAR 

1064from ...types import NVARCHAR 

1065from ...types import REAL 

1066from ...types import VARCHAR 

1067 

1068if TYPE_CHECKING: 

1069 from ...sql.sqltypes import _JSON_VALUE 

1070 

1071RESERVED_WORDS = set( 

1072 "SHARE RAW DROP BETWEEN FROM DESC OPTION PRIOR LONG THEN " 

1073 "DEFAULT ALTER IS INTO MINUS INTEGER NUMBER GRANT IDENTIFIED " 

1074 "ALL TO ORDER ON FLOAT DATE HAVING CLUSTER NOWAIT RESOURCE " 

1075 "ANY TABLE INDEX FOR UPDATE WHERE CHECK SMALLINT WITH DELETE " 

1076 "BY ASC REVOKE LIKE SIZE RENAME NOCOMPRESS NULL GROUP VALUES " 

1077 "AS IN VIEW EXCLUSIVE COMPRESS SYNONYM SELECT INSERT EXISTS " 

1078 "NOT TRIGGER ELSE CREATE INTERSECT PCTFREE DISTINCT USER " 

1079 "CONNECT SET MODE OF UNIQUE VARCHAR2 VARCHAR LOCK OR CHAR " 

1080 "DECIMAL UNION PUBLIC AND START UID COMMENT CURRENT LEVEL".split() 

1081) 

1082 

1083NO_ARG_FNS = set( 

1084 "UID CURRENT_DATE SYSDATE USER CURRENT_TIME CURRENT_TIMESTAMP".split() 

1085) 

1086 

1087 

1088colspecs = { 

1089 sqltypes.Boolean: _OracleBoolean, 

1090 sqltypes.Interval: INTERVAL, 

1091 sqltypes.DateTime: DATE, 

1092 sqltypes.Date: _OracleDate, 

1093 sqltypes.JSON: JSON, 

1094 sqltypes.JSON.JSONIndexType: JSONIndexType, 

1095 sqltypes.JSON.JSONPathType: JSONPathType, 

1096} 

1097 

1098ischema_names = { 

1099 "VARCHAR2": VARCHAR, 

1100 "NVARCHAR2": NVARCHAR, 

1101 "CHAR": CHAR, 

1102 "NCHAR": NCHAR, 

1103 "DATE": DATE, 

1104 "NUMBER": NUMBER, 

1105 "BLOB": BLOB, 

1106 "BFILE": BFILE, 

1107 "CLOB": CLOB, 

1108 "NCLOB": NCLOB, 

1109 "TIMESTAMP": TIMESTAMP, 

1110 "TIMESTAMP WITH TIME ZONE": TIMESTAMP, 

1111 "TIMESTAMP WITH LOCAL TIME ZONE": TIMESTAMP, 

1112 "INTERVAL DAY TO SECOND": INTERVAL, 

1113 "RAW": RAW, 

1114 "FLOAT": FLOAT, 

1115 "DOUBLE PRECISION": DOUBLE_PRECISION, 

1116 "REAL": REAL, 

1117 "LONG": LONG, 

1118 "BINARY_DOUBLE": BINARY_DOUBLE, 

1119 "BINARY_FLOAT": BINARY_FLOAT, 

1120 "ROWID": ROWID, 

1121 "BOOLEAN": BOOLEAN, 

1122 "VECTOR": VECTOR, 

1123 "JSON": JSON, 

1124} 

1125 

1126 

1127class OracleTypeCompiler(compiler.GenericTypeCompiler): 

1128 # Note: 

1129 # Oracle DATE == DATETIME 

1130 # Oracle does not allow milliseconds in DATE 

1131 # Oracle does not support TIME columns 

1132 

1133 def visit_datetime(self, type_, **kw): 

1134 return self.visit_DATE(type_, **kw) 

1135 

1136 def visit_float(self, type_, **kw): 

1137 return self.visit_FLOAT(type_, **kw) 

1138 

1139 def visit_double(self, type_, **kw): 

1140 return self.visit_DOUBLE_PRECISION(type_, **kw) 

1141 

1142 def visit_unicode(self, type_, **kw): 

1143 if self.dialect._use_nchar_for_unicode: 

1144 return self.visit_NVARCHAR2(type_, **kw) 

1145 else: 

1146 return self.visit_VARCHAR2(type_, **kw) 

1147 

1148 def visit_INTERVAL(self, type_, **kw): 

1149 return "INTERVAL DAY%s TO SECOND%s" % ( 

1150 type_.day_precision is not None 

1151 and "(%d)" % type_.day_precision 

1152 or "", 

1153 type_.second_precision is not None 

1154 and "(%d)" % type_.second_precision 

1155 or "", 

1156 ) 

1157 

1158 def visit_LONG(self, type_, **kw): 

1159 return "LONG" 

1160 

1161 def visit_TIMESTAMP(self, type_, **kw): 

1162 if getattr(type_, "local_timezone", False): 

1163 return "TIMESTAMP WITH LOCAL TIME ZONE" 

1164 elif type_.timezone: 

1165 return "TIMESTAMP WITH TIME ZONE" 

1166 else: 

1167 return "TIMESTAMP" 

1168 

1169 def visit_DOUBLE_PRECISION(self, type_, **kw): 

1170 return self._generate_numeric(type_, "DOUBLE PRECISION", **kw) 

1171 

1172 def visit_BINARY_DOUBLE(self, type_, **kw): 

1173 return self._generate_numeric(type_, "BINARY_DOUBLE", **kw) 

1174 

1175 def visit_BINARY_FLOAT(self, type_, **kw): 

1176 return self._generate_numeric(type_, "BINARY_FLOAT", **kw) 

1177 

1178 def visit_FLOAT(self, type_, **kw): 

1179 kw["_requires_binary_precision"] = True 

1180 return self._generate_numeric(type_, "FLOAT", **kw) 

1181 

1182 def visit_NUMBER(self, type_, **kw): 

1183 return self._generate_numeric(type_, "NUMBER", **kw) 

1184 

1185 def _generate_numeric( 

1186 self, 

1187 type_, 

1188 name, 

1189 precision=None, 

1190 scale=None, 

1191 _requires_binary_precision=False, 

1192 **kw, 

1193 ): 

1194 if precision is None: 

1195 precision = getattr(type_, "precision", None) 

1196 

1197 if _requires_binary_precision: 

1198 binary_precision = getattr(type_, "binary_precision", None) 

1199 

1200 if precision and binary_precision is None: 

1201 # https://www.oracletutorial.com/oracle-basics/oracle-float/ 

1202 estimated_binary_precision = int(precision / 0.30103) 

1203 raise exc.ArgumentError( 

1204 "Oracle Database FLOAT types use 'binary precision', " 

1205 "which does not convert cleanly from decimal " 

1206 "'precision'. Please specify " 

1207 "this type with a separate Oracle Database variant, such " 

1208 f"as {type_.__class__.__name__}(precision={precision})." 

1209 f"with_variant(oracle.FLOAT" 

1210 f"(binary_precision=" 

1211 f"{estimated_binary_precision}), 'oracle'), so that the " 

1212 "Oracle Database specific 'binary_precision' may be " 

1213 "specified accurately." 

1214 ) 

1215 else: 

1216 precision = binary_precision 

1217 

1218 if scale is None: 

1219 scale = getattr(type_, "scale", None) 

1220 

1221 if precision is None: 

1222 return name 

1223 elif scale is None: 

1224 n = "%(name)s(%(precision)s)" 

1225 return n % {"name": name, "precision": precision} 

1226 else: 

1227 n = "%(name)s(%(precision)s, %(scale)s)" 

1228 return n % {"name": name, "precision": precision, "scale": scale} 

1229 

1230 def visit_string(self, type_, **kw): 

1231 return self.visit_VARCHAR2(type_, **kw) 

1232 

1233 def visit_VARCHAR2(self, type_, **kw): 

1234 return self._visit_varchar(type_, "", "2") 

1235 

1236 def visit_NVARCHAR2(self, type_, **kw): 

1237 return self._visit_varchar(type_, "N", "2") 

1238 

1239 visit_NVARCHAR = visit_NVARCHAR2 

1240 

1241 def visit_VARCHAR(self, type_, **kw): 

1242 return self._visit_varchar(type_, "", "") 

1243 

1244 def _visit_varchar(self, type_, n, num): 

1245 if not type_.length: 

1246 return "%(n)sVARCHAR%(two)s" % {"two": num, "n": n} 

1247 elif not n and self.dialect._supports_char_length: 

1248 varchar = "VARCHAR%(two)s(%(length)s CHAR)" 

1249 return varchar % {"length": type_.length, "two": num} 

1250 else: 

1251 varchar = "%(n)sVARCHAR%(two)s(%(length)s)" 

1252 return varchar % {"length": type_.length, "two": num, "n": n} 

1253 

1254 def visit_text(self, type_, **kw): 

1255 return self.visit_CLOB(type_, **kw) 

1256 

1257 def visit_unicode_text(self, type_, **kw): 

1258 if self.dialect._use_nchar_for_unicode: 

1259 return self.visit_NCLOB(type_, **kw) 

1260 else: 

1261 return self.visit_CLOB(type_, **kw) 

1262 

1263 def visit_large_binary(self, type_, **kw): 

1264 return self.visit_BLOB(type_, **kw) 

1265 

1266 def visit_big_integer(self, type_, **kw): 

1267 return self.visit_NUMBER(type_, precision=19, **kw) 

1268 

1269 def visit_boolean(self, type_, **kw): 

1270 if self.dialect.supports_native_boolean: 

1271 return self.visit_BOOLEAN(type_, **kw) 

1272 else: 

1273 return self.visit_SMALLINT(type_, **kw) 

1274 

1275 def visit_RAW(self, type_, **kw): 

1276 if type_.length: 

1277 return "RAW(%(length)s)" % {"length": type_.length} 

1278 else: 

1279 return "RAW" 

1280 

1281 def visit_ROWID(self, type_, **kw): 

1282 return "ROWID" 

1283 

1284 def visit_VECTOR(self, type_, **kw): 

1285 dim = type_.dim if type_.dim is not None else "*" 

1286 storage_format = ( 

1287 type_.storage_format.value 

1288 if type_.storage_format is not None 

1289 else "*" 

1290 ) 

1291 storage_type = ( 

1292 type_.storage_type.value if type_.storage_type is not None else "*" 

1293 ) 

1294 return f"VECTOR({dim},{storage_format},{storage_type})" 

1295 

1296 def visit_JSON(self, type_: JSON, **kw: Any) -> str: 

1297 use_blob = ( 

1298 not self.dialect._supports_oracle_json 

1299 if getattr(type_, "use_blob", NO_ARG) is NO_ARG 

1300 else type_.use_blob 

1301 ) 

1302 

1303 if use_blob: 

1304 return "BLOB" 

1305 else: 

1306 return "JSON" 

1307 

1308 

1309class OracleCompiler(compiler.SQLCompiler): 

1310 """Oracle compiler modifies the lexical structure of Select 

1311 statements to work under non-ANSI configured Oracle databases, if 

1312 the use_ansi flag is False. 

1313 """ 

1314 

1315 compound_keywords = util.update_copy( 

1316 compiler.SQLCompiler.compound_keywords, 

1317 {expression.CompoundSelect.EXCEPT: "MINUS"}, 

1318 ) 

1319 

1320 def __init__(self, *args, **kwargs): 

1321 self.__wheres = {} 

1322 super().__init__(*args, **kwargs) 

1323 

1324 def visit_mod_binary(self, binary, operator, **kw): 

1325 return "mod(%s, %s)" % ( 

1326 self.process(binary.left, **kw), 

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

1328 ) 

1329 

1330 def visit_now_func(self, fn, **kw): 

1331 return "CURRENT_TIMESTAMP" 

1332 

1333 def visit_char_length_func(self, fn, **kw): 

1334 return "LENGTH" + self.function_argspec(fn, **kw) 

1335 

1336 def visit_pow_func(self, fn, **kw): 

1337 return f"POWER{self.function_argspec(fn)}" 

1338 

1339 def visit_match_op_binary(self, binary, operator, **kw): 

1340 return "CONTAINS (%s, %s)" % ( 

1341 self.process(binary.left), 

1342 self.process(binary.right), 

1343 ) 

1344 

1345 def visit_true(self, expr, **kw): 

1346 return "1" 

1347 

1348 def visit_false(self, expr, **kw): 

1349 return "0" 

1350 

1351 def visit_cast(self, cast, **kwargs): 

1352 # Oracle requires VARCHAR2 to have a length in CAST expressions 

1353 # Adapt String types to VARCHAR2 with appropriate length 

1354 type_ = cast.typeclause.type 

1355 if isinstance(type_, sqltypes.String) and not isinstance( 

1356 type_, (sqltypes.Text, sqltypes.CLOB) 

1357 ): 

1358 adapted = VARCHAR2._adapt_string_for_cast(type_) 

1359 type_clause = self.dialect.type_compiler_instance.process(adapted) 

1360 else: 

1361 type_clause = cast.typeclause._compiler_dispatch(self, **kwargs) 

1362 

1363 return "CAST(%s AS %s)" % ( 

1364 cast.clause._compiler_dispatch(self, **kwargs), 

1365 type_clause, 

1366 ) 

1367 

1368 def get_cte_preamble(self, recursive): 

1369 return "WITH" 

1370 

1371 def get_select_hint_text(self, byfroms): 

1372 return " ".join("/*+ %s */" % text for table, text in byfroms.items()) 

1373 

1374 def function_argspec(self, fn, **kw): 

1375 if len(fn.clauses) > 0 or fn.name.upper() not in NO_ARG_FNS: 

1376 return compiler.SQLCompiler.function_argspec(self, fn, **kw) 

1377 else: 

1378 return "" 

1379 

1380 def visit_function(self, func, **kw): 

1381 text = super().visit_function(func, **kw) 

1382 if kw.get("asfrom", False) and func.name.lower() != "table": 

1383 text = "TABLE (%s)" % text 

1384 return text 

1385 

1386 def visit_table_valued_column(self, element, **kw): 

1387 text = super().visit_table_valued_column(element, **kw) 

1388 text = text + ".COLUMN_VALUE" 

1389 return text 

1390 

1391 def default_from(self): 

1392 """Called when a ``SELECT`` statement has no froms, 

1393 and no ``FROM`` clause is to be appended. 

1394 

1395 The Oracle compiler tacks a "FROM DUAL" to the statement. 

1396 """ 

1397 

1398 return " FROM DUAL" 

1399 

1400 def visit_join(self, join, from_linter=None, **kwargs): 

1401 if self.dialect.use_ansi: 

1402 return compiler.SQLCompiler.visit_join( 

1403 self, join, from_linter=from_linter, **kwargs 

1404 ) 

1405 else: 

1406 if from_linter: 

1407 from_linter.edges.add((join.left, join.right)) 

1408 

1409 kwargs["asfrom"] = True 

1410 if isinstance(join.right, expression.FromGrouping): 

1411 right = join.right.element 

1412 else: 

1413 right = join.right 

1414 return ( 

1415 self.process(join.left, from_linter=from_linter, **kwargs) 

1416 + ", " 

1417 + self.process(right, from_linter=from_linter, **kwargs) 

1418 ) 

1419 

1420 def _get_nonansi_join_whereclause(self, froms): 

1421 clauses = [] 

1422 

1423 def visit_join(join): 

1424 if join.isouter: 

1425 # https://docs.oracle.com/database/121/SQLRF/queries006.htm#SQLRF52354 

1426 # "apply the outer join operator (+) to all columns of B in 

1427 # the join condition in the WHERE clause" - that is, 

1428 # unconditionally regardless of operator or the other side 

1429 def visit_binary(binary): 

1430 if isinstance( 

1431 binary.left, expression.ColumnClause 

1432 ) and join.right.is_derived_from(binary.left.table): 

1433 binary.left = _OuterJoinColumn(binary.left) 

1434 elif isinstance( 

1435 binary.right, expression.ColumnClause 

1436 ) and join.right.is_derived_from(binary.right.table): 

1437 binary.right = _OuterJoinColumn(binary.right) 

1438 

1439 clauses.append( 

1440 visitors.cloned_traverse( 

1441 join.onclause, {}, {"binary": visit_binary} 

1442 ) 

1443 ) 

1444 else: 

1445 clauses.append(join.onclause) 

1446 

1447 for j in join.left, join.right: 

1448 if isinstance(j, expression.Join): 

1449 visit_join(j) 

1450 elif isinstance(j, expression.FromGrouping): 

1451 visit_join(j.element) 

1452 

1453 for f in froms: 

1454 if isinstance(f, expression.Join): 

1455 visit_join(f) 

1456 

1457 if not clauses: 

1458 return None 

1459 else: 

1460 return sql.and_(*clauses) 

1461 

1462 def visit_outer_join_column(self, vc, **kw): 

1463 return self.process(vc.column, **kw) + "(+)" 

1464 

1465 def visit_sequence(self, seq, **kw): 

1466 return self.preparer.format_sequence(seq) + ".nextval" 

1467 

1468 def get_render_as_alias_suffix(self, alias_name_text): 

1469 """Oracle doesn't like ``FROM table AS alias``""" 

1470 

1471 return " " + alias_name_text 

1472 

1473 def returning_clause( 

1474 self, stmt, returning_cols, *, populate_result_map, **kw 

1475 ): 

1476 columns = [] 

1477 binds = [] 

1478 

1479 for i, column in enumerate( 

1480 expression._select_iterables(returning_cols) 

1481 ): 

1482 if ( 

1483 self.isupdate 

1484 and isinstance(column, sa_schema.Column) 

1485 and isinstance(column.server_default, Computed) 

1486 and not self.dialect._supports_update_returning_computed_cols 

1487 ): 

1488 util.warn( 

1489 "Computed columns don't work with Oracle Database UPDATE " 

1490 "statements that use RETURNING; the value of the column " 

1491 "*before* the UPDATE takes place is returned. It is " 

1492 "advised to not use RETURNING with an Oracle Database " 

1493 "computed column. Consider setting implicit_returning " 

1494 "to False on the Table object in order to avoid implicit " 

1495 "RETURNING clauses from being generated for this Table." 

1496 ) 

1497 if column.type._has_column_expression: 

1498 col_expr = column.type.column_expression(column) 

1499 else: 

1500 col_expr = column 

1501 

1502 outparam = sql.outparam("ret_%d" % i, type_=column.type) 

1503 self.binds[outparam.key] = outparam 

1504 binds.append( 

1505 self.bindparam_string(self._truncate_bindparam(outparam)) 

1506 ) 

1507 

1508 # has_out_parameters would in a normal case be set to True 

1509 # as a result of the compiler visiting an outparam() object. 

1510 # in this case, the above outparam() objects are not being 

1511 # visited. Ensure the statement itself didn't have other 

1512 # outparam() objects independently. 

1513 # technically, this could be supported, but as it would be 

1514 # a very strange use case without a clear rationale, disallow it 

1515 if self.has_out_parameters: 

1516 raise exc.InvalidRequestError( 

1517 "Using explicit outparam() objects with " 

1518 "UpdateBase.returning() in the same Core DML statement " 

1519 "is not supported in the Oracle Database dialects." 

1520 ) 

1521 

1522 self._oracle_returning = True 

1523 

1524 columns.append(self.process(col_expr, within_columns_clause=False)) 

1525 if populate_result_map: 

1526 self._add_to_result_map( 

1527 getattr(col_expr, "name", col_expr._anon_name_label), 

1528 getattr(col_expr, "name", col_expr._anon_name_label), 

1529 ( 

1530 column, 

1531 getattr(column, "name", None), 

1532 getattr(column, "key", None), 

1533 ), 

1534 column.type, 

1535 ) 

1536 

1537 return "RETURNING " + ", ".join(columns) + " INTO " + ", ".join(binds) 

1538 

1539 def _row_limit_clause(self, select, **kw): 

1540 """Oracle Database 12c supports OFFSET/FETCH operators 

1541 Use it instead subquery with row_number 

1542 

1543 """ 

1544 

1545 if ( 

1546 select._fetch_clause is not None 

1547 or not self.dialect._supports_offset_fetch 

1548 ): 

1549 return super()._row_limit_clause( 

1550 select, use_literal_execute_for_simple_int=True, **kw 

1551 ) 

1552 else: 

1553 return self.fetch_clause( 

1554 select, 

1555 fetch_clause=self._get_limit_or_fetch(select), 

1556 use_literal_execute_for_simple_int=True, 

1557 **kw, 

1558 ) 

1559 

1560 def _get_limit_or_fetch(self, select): 

1561 if select._fetch_clause is None: 

1562 return select._limit_clause 

1563 else: 

1564 return select._fetch_clause 

1565 

1566 def fetch_clause( 

1567 self, 

1568 select, 

1569 fetch_clause=None, 

1570 require_offset=False, 

1571 use_literal_execute_for_simple_int=False, 

1572 **kw, 

1573 ): 

1574 text = super().fetch_clause( 

1575 select, 

1576 fetch_clause=fetch_clause, 

1577 require_offset=require_offset, 

1578 use_literal_execute_for_simple_int=( 

1579 use_literal_execute_for_simple_int 

1580 ), 

1581 **kw, 

1582 ) 

1583 

1584 if select.dialect_options["oracle"]["fetch_approximate"]: 

1585 text = re.sub("FETCH FIRST", "FETCH APPROX FIRST", text) 

1586 

1587 return text 

1588 

1589 def translate_select_structure(self, select_stmt, **kwargs): 

1590 select = select_stmt 

1591 

1592 if not getattr(select, "_oracle_visit", None): 

1593 if not self.dialect.use_ansi: 

1594 froms = self._display_froms_for_select( 

1595 select, kwargs.get("asfrom", False) 

1596 ) 

1597 whereclause = self._get_nonansi_join_whereclause(froms) 

1598 if whereclause is not None: 

1599 select = select.where(whereclause) 

1600 select._oracle_visit = True 

1601 

1602 # if fetch is used this is not needed 

1603 if ( 

1604 select._has_row_limiting_clause 

1605 and not self.dialect._supports_offset_fetch 

1606 and select._fetch_clause is None 

1607 ): 

1608 limit_clause = select._limit_clause 

1609 offset_clause = select._offset_clause 

1610 

1611 if select._simple_int_clause(limit_clause): 

1612 limit_clause = limit_clause.render_literal_execute() 

1613 

1614 if select._simple_int_clause(offset_clause): 

1615 offset_clause = offset_clause.render_literal_execute() 

1616 

1617 # currently using form at: 

1618 # https://blogs.oracle.com/oraclemagazine/\ 

1619 # on-rownum-and-limiting-results 

1620 

1621 orig_select = select 

1622 select = select._generate() 

1623 select._oracle_visit = True 

1624 

1625 # add expressions to accommodate FOR UPDATE OF 

1626 for_update = select._for_update_arg 

1627 if for_update is not None and for_update.of: 

1628 for_update = for_update._clone() 

1629 for_update._copy_internals() 

1630 

1631 for elem in for_update.of: 

1632 if not select.selected_columns.contains_column(elem): 

1633 select = select.add_columns(elem) 

1634 

1635 # Wrap the middle select and add the hint 

1636 inner_subquery = select.alias() 

1637 limitselect = sql.select( 

1638 *[ 

1639 c 

1640 for c in inner_subquery.c 

1641 if orig_select.selected_columns.corresponding_column(c) 

1642 is not None 

1643 ] 

1644 ) 

1645 

1646 if ( 

1647 limit_clause is not None 

1648 and self.dialect.optimize_limits 

1649 and select._simple_int_clause(limit_clause) 

1650 ): 

1651 limitselect = limitselect.prefix_with( 

1652 expression.text( 

1653 "/*+ FIRST_ROWS(%s) */" 

1654 % self.process(limit_clause, **kwargs) 

1655 ) 

1656 ) 

1657 

1658 limitselect._oracle_visit = True 

1659 limitselect._is_wrapper = True 

1660 

1661 # add expressions to accommodate FOR UPDATE OF 

1662 if for_update is not None and for_update.of: 

1663 adapter = sql_util.ClauseAdapter(inner_subquery) 

1664 for_update.of = [ 

1665 adapter.traverse(elem) for elem in for_update.of 

1666 ] 

1667 

1668 # If needed, add the limiting clause 

1669 if limit_clause is not None: 

1670 if select._simple_int_clause(limit_clause) and ( 

1671 offset_clause is None 

1672 or select._simple_int_clause(offset_clause) 

1673 ): 

1674 max_row = limit_clause 

1675 

1676 if offset_clause is not None: 

1677 max_row = max_row + offset_clause 

1678 

1679 else: 

1680 max_row = limit_clause 

1681 

1682 if offset_clause is not None: 

1683 max_row = max_row + offset_clause 

1684 limitselect = limitselect.where( 

1685 sql.literal_column("ROWNUM") <= max_row 

1686 ) 

1687 

1688 # If needed, add the ora_rn, and wrap again with offset. 

1689 if offset_clause is None: 

1690 limitselect._for_update_arg = for_update 

1691 select = limitselect 

1692 else: 

1693 limitselect = limitselect.add_columns( 

1694 sql.literal_column("ROWNUM").label("ora_rn") 

1695 ) 

1696 limitselect._oracle_visit = True 

1697 limitselect._is_wrapper = True 

1698 

1699 if for_update is not None and for_update.of: 

1700 limitselect_cols = limitselect.selected_columns 

1701 for elem in for_update.of: 

1702 if ( 

1703 limitselect_cols.corresponding_column(elem) 

1704 is None 

1705 ): 

1706 limitselect = limitselect.add_columns(elem) 

1707 

1708 limit_subquery = limitselect.alias() 

1709 origselect_cols = orig_select.selected_columns 

1710 offsetselect = sql.select( 

1711 *[ 

1712 c 

1713 for c in limit_subquery.c 

1714 if origselect_cols.corresponding_column(c) 

1715 is not None 

1716 ] 

1717 ) 

1718 

1719 offsetselect._oracle_visit = True 

1720 offsetselect._is_wrapper = True 

1721 

1722 if for_update is not None and for_update.of: 

1723 adapter = sql_util.ClauseAdapter(limit_subquery) 

1724 for_update.of = [ 

1725 adapter.traverse(elem) for elem in for_update.of 

1726 ] 

1727 

1728 offsetselect = offsetselect.where( 

1729 sql.literal_column("ora_rn") > offset_clause 

1730 ) 

1731 

1732 offsetselect._for_update_arg = for_update 

1733 select = offsetselect 

1734 

1735 return select 

1736 

1737 def limit_clause(self, select, **kw): 

1738 return "" 

1739 

1740 def visit_empty_set_expr(self, type_, **kw): 

1741 return "SELECT 1 FROM DUAL WHERE 1!=1" 

1742 

1743 def for_update_clause(self, select, **kw): 

1744 if self.is_subquery(): 

1745 return "" 

1746 

1747 tmp = " FOR UPDATE" 

1748 

1749 if select._for_update_arg.of: 

1750 tmp += " OF " + ", ".join( 

1751 self.process(elem, **kw) for elem in select._for_update_arg.of 

1752 ) 

1753 

1754 if select._for_update_arg.nowait: 

1755 tmp += " NOWAIT" 

1756 if select._for_update_arg.skip_locked: 

1757 tmp += " SKIP LOCKED" 

1758 

1759 return tmp 

1760 

1761 def visit_is_distinct_from_binary(self, binary, operator, **kw): 

1762 return "DECODE(%s, %s, 0, 1) = 1" % ( 

1763 self.process(binary.left), 

1764 self.process(binary.right), 

1765 ) 

1766 

1767 def visit_is_not_distinct_from_binary(self, binary, operator, **kw): 

1768 return "DECODE(%s, %s, 0, 1) = 0" % ( 

1769 self.process(binary.left), 

1770 self.process(binary.right), 

1771 ) 

1772 

1773 def visit_regexp_match_op_binary(self, binary, operator, **kw): 

1774 string = self.process(binary.left, **kw) 

1775 pattern = self.process(binary.right, **kw) 

1776 flags = binary.modifiers["flags"] 

1777 if flags is None: 

1778 return "REGEXP_LIKE(%s, %s)" % (string, pattern) 

1779 else: 

1780 return "REGEXP_LIKE(%s, %s, %s)" % ( 

1781 string, 

1782 pattern, 

1783 self.render_literal_value(flags, sqltypes.STRINGTYPE), 

1784 ) 

1785 

1786 def visit_not_regexp_match_op_binary(self, binary, operator, **kw): 

1787 return "NOT %s" % self.visit_regexp_match_op_binary( 

1788 binary, operator, **kw 

1789 ) 

1790 

1791 def visit_regexp_replace_op_binary(self, binary, operator, **kw): 

1792 string = self.process(binary.left, **kw) 

1793 pattern_replace = self.process(binary.right, **kw) 

1794 flags = binary.modifiers["flags"] 

1795 if flags is None: 

1796 return "REGEXP_REPLACE(%s, %s)" % ( 

1797 string, 

1798 pattern_replace, 

1799 ) 

1800 else: 

1801 return "REGEXP_REPLACE(%s, %s, %s)" % ( 

1802 string, 

1803 pattern_replace, 

1804 self.render_literal_value(flags, sqltypes.STRINGTYPE), 

1805 ) 

1806 

1807 def visit_aggregate_strings_func(self, fn, **kw): 

1808 return super().visit_aggregate_strings_func( 

1809 fn, use_function_name="LISTAGG", **kw 

1810 ) 

1811 

1812 def _visit_bitwise(self, binary, fn_name, custom_right=None, **kw): 

1813 left = self.process(binary.left, **kw) 

1814 right = self.process( 

1815 custom_right if custom_right is not None else binary.right, **kw 

1816 ) 

1817 return f"{fn_name}({left}, {right})" 

1818 

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

1820 return self._visit_bitwise(binary, "BITXOR", **kw) 

1821 

1822 def visit_bitwise_or_op_binary(self, binary, operator, **kw): 

1823 return self._visit_bitwise(binary, "BITOR", **kw) 

1824 

1825 def visit_bitwise_and_op_binary(self, binary, operator, **kw): 

1826 return self._visit_bitwise(binary, "BITAND", **kw) 

1827 

1828 def visit_bitwise_rshift_op_binary(self, binary, operator, **kw): 

1829 raise exc.CompileError("Cannot compile bitwise_rshift in oracle") 

1830 

1831 def visit_bitwise_lshift_op_binary(self, binary, operator, **kw): 

1832 raise exc.CompileError("Cannot compile bitwise_lshift in oracle") 

1833 

1834 def visit_bitwise_not_op_unary_operator(self, element, operator, **kw): 

1835 raise exc.CompileError("Cannot compile bitwise_not in oracle") 

1836 

1837 def _render_json_extract_from_binary(self, binary, operator, **kw): 

1838 literal_kw = kw.copy() 

1839 literal_kw["literal_binds"] = True 

1840 

1841 left = self.process(binary.left, **kw) 

1842 right = self.process(binary.right, **literal_kw) 

1843 

1844 if binary.type._type_affinity is sqltypes.Boolean: 

1845 # RETURNING clause doesn't handle true/false to 1/0 

1846 # mapping, so use CASE expression for boolean 

1847 return ( 

1848 f"CASE JSON_VALUE({left}, {right})" 

1849 f" WHEN 'true' THEN 1" 

1850 f" WHEN 'false' THEN 0" 

1851 f" ELSE CAST(JSON_VALUE({left}, {right})" 

1852 f" AS NUMBER(1)) END" 

1853 ) 

1854 elif binary.type._type_affinity is sqltypes.Integer: 

1855 json_value_returning = "INTEGER" 

1856 elif binary.type._type_affinity in ( 

1857 sqltypes.Numeric, 

1858 sqltypes.Float, 

1859 ): 

1860 if isinstance(binary.type, sqltypes.Float): 

1861 json_value_returning = "FLOAT" 

1862 else: 

1863 json_value_returning = ( 

1864 f"NUMBER({binary.type.precision}, {binary.type.scale})" 

1865 ) 

1866 elif binary.type._type_affinity is sqltypes.String: 

1867 json_value_returning = "VARCHAR2(4000)" 

1868 else: 

1869 # binary.type._type_affinity is sqltypes.JSON 

1870 # or other 

1871 return f"JSON_QUERY({left}, {right})" 

1872 

1873 return ( 

1874 f"JSON_VALUE({left}, {right}" 

1875 f" RETURNING {json_value_returning} ERROR ON ERROR)" 

1876 ) 

1877 

1878 def visit_json_getitem_op_binary( 

1879 self, binary: elements.BinaryExpression[Any], operator: Any, **kw: Any 

1880 ) -> str: 

1881 return self._render_json_extract_from_binary(binary, operator, **kw) 

1882 

1883 def visit_json_path_getitem_op_binary( 

1884 self, binary: elements.BinaryExpression[Any], operator: Any, **kw: Any 

1885 ) -> str: 

1886 return self._render_json_extract_from_binary(binary, operator, **kw) 

1887 

1888 

1889class OracleDDLCompiler(compiler.DDLCompiler): 

1890 

1891 def _build_vector_index_config( 

1892 self, vector_index_config: VectorIndexConfig 

1893 ) -> str: 

1894 parts = [] 

1895 sql_param_name = { 

1896 "hnsw_neighbors": "neighbors", 

1897 "hnsw_efconstruction": "efconstruction", 

1898 "ivf_neighbor_partitions": "neighbor partitions", 

1899 "ivf_sample_per_partition": "sample_per_partition", 

1900 "ivf_min_vectors_per_partition": "min_vectors_per_partition", 

1901 } 

1902 if vector_index_config.index_type == VectorIndexType.HNSW: 

1903 parts.append("ORGANIZATION INMEMORY NEIGHBOR GRAPH") 

1904 elif vector_index_config.index_type == VectorIndexType.IVF: 

1905 parts.append("ORGANIZATION NEIGHBOR PARTITIONS") 

1906 if vector_index_config.distance is not None: 

1907 parts.append(f"DISTANCE {vector_index_config.distance.value}") 

1908 

1909 if vector_index_config.accuracy is not None: 

1910 parts.append( 

1911 f"WITH TARGET ACCURACY {vector_index_config.accuracy}" 

1912 ) 

1913 

1914 parameters_str = [f"type {vector_index_config.index_type.name}"] 

1915 prefix = vector_index_config.index_type.name.lower() + "_" 

1916 

1917 for field in fields(vector_index_config): 

1918 if field.name.startswith(prefix): 

1919 key = sql_param_name.get(field.name) 

1920 value = getattr(vector_index_config, field.name) 

1921 if value is not None: 

1922 parameters_str.append(f"{key} {value}") 

1923 

1924 parameters_str = ", ".join(parameters_str) 

1925 parts.append(f"PARAMETERS ({parameters_str})") 

1926 

1927 if vector_index_config.parallel is not None: 

1928 parts.append(f"PARALLEL {vector_index_config.parallel}") 

1929 

1930 return " ".join(parts) 

1931 

1932 def define_constraint_cascades(self, constraint): 

1933 text = "" 

1934 if constraint.ondelete is not None: 

1935 text += " ON DELETE %s" % constraint.ondelete 

1936 

1937 # oracle has no ON UPDATE CASCADE - 

1938 # its only available via triggers 

1939 # https://web.archive.org/web/20090317041251/https://asktom.oracle.com/tkyte/update_cascade/index.html 

1940 if constraint.onupdate is not None: 

1941 util.warn( 

1942 "Oracle Database does not contain native UPDATE CASCADE " 

1943 "functionality - onupdates will not be rendered for foreign " 

1944 "keys. Consider using deferrable=True, initially='deferred' " 

1945 "or triggers." 

1946 ) 

1947 

1948 return text 

1949 

1950 def visit_drop_table_comment(self, drop, **kw): 

1951 return "COMMENT ON TABLE %s IS ''" % self.preparer.format_table( 

1952 drop.element 

1953 ) 

1954 

1955 def visit_create_index(self, create, **kw): 

1956 index = create.element 

1957 self._verify_index_table(index) 

1958 preparer = self.preparer 

1959 text = "CREATE " 

1960 if index.unique: 

1961 text += "UNIQUE " 

1962 if index.dialect_options["oracle"]["bitmap"]: 

1963 text += "BITMAP " 

1964 vector_options = index.dialect_options["oracle"]["vector"] 

1965 if vector_options: 

1966 text += "VECTOR " 

1967 text += "INDEX %s ON %s (%s)" % ( 

1968 self._prepared_index_name(index, include_schema=True), 

1969 preparer.format_table(index.table, use_schema=True), 

1970 ", ".join( 

1971 self.sql_compiler.process( 

1972 expr, include_table=False, literal_binds=True 

1973 ) 

1974 for expr in index.expressions 

1975 ), 

1976 ) 

1977 if index.dialect_options["oracle"]["compress"] is not False: 

1978 if index.dialect_options["oracle"]["compress"] is True: 

1979 text += " COMPRESS" 

1980 else: 

1981 text += " COMPRESS %d" % ( 

1982 index.dialect_options["oracle"]["compress"] 

1983 ) 

1984 if vector_options: 

1985 if vector_options is True: 

1986 vector_options = VectorIndexConfig() 

1987 

1988 text += " " + self._build_vector_index_config(vector_options) 

1989 return text 

1990 

1991 def post_create_table(self, table): 

1992 table_opts = [] 

1993 opts = table.dialect_options["oracle"] 

1994 

1995 if opts["on_commit"]: 

1996 on_commit_options = opts["on_commit"].replace("_", " ").upper() 

1997 table_opts.append("\n ON COMMIT %s" % on_commit_options) 

1998 

1999 if opts["compress"]: 

2000 if opts["compress"] is True: 

2001 table_opts.append("\n COMPRESS") 

2002 else: 

2003 table_opts.append("\n COMPRESS FOR %s" % (opts["compress"])) 

2004 if opts["tablespace"]: 

2005 table_opts.append( 

2006 "\n TABLESPACE %s" % self.preparer.quote(opts["tablespace"]) 

2007 ) 

2008 return "".join(table_opts) 

2009 

2010 def get_identity_options(self, identity_options): 

2011 text = super().get_identity_options(identity_options) 

2012 text = text.replace("NO MINVALUE", "NOMINVALUE") 

2013 text = text.replace("NO MAXVALUE", "NOMAXVALUE") 

2014 text = text.replace("NO CYCLE", "NOCYCLE") 

2015 options = identity_options.dialect_options["oracle"] 

2016 if options.get("order") is not None: 

2017 text += " ORDER" if options["order"] else " NOORDER" 

2018 return text.strip() 

2019 

2020 def visit_computed_column(self, generated, **kw): 

2021 text = "GENERATED ALWAYS AS (%s)" % self.sql_compiler.process( 

2022 generated.sqltext, include_table=False, literal_binds=True 

2023 ) 

2024 if generated.persisted is True: 

2025 raise exc.CompileError( 

2026 "Oracle Database computed columns do not support 'stored' " 

2027 "persistence; set the 'persisted' flag to None or False for " 

2028 "Oracle Database support." 

2029 ) 

2030 elif generated.persisted is False: 

2031 text += " VIRTUAL" 

2032 return text 

2033 

2034 def visit_identity_column(self, identity, **kw): 

2035 if identity.always is None: 

2036 kind = "" 

2037 else: 

2038 kind = "ALWAYS" if identity.always else "BY DEFAULT" 

2039 text = "GENERATED %s" % kind 

2040 if identity.dialect_options["oracle"].get("on_null"): 

2041 text += " ON NULL" 

2042 text += " AS IDENTITY" 

2043 options = self.get_identity_options(identity) 

2044 if options: 

2045 text += " (%s)" % options 

2046 return text 

2047 

2048 

2049class OracleIdentifierPreparer(compiler.IdentifierPreparer): 

2050 reserved_words = {x.lower() for x in RESERVED_WORDS} 

2051 illegal_initial_characters = {str(dig) for dig in range(0, 10)}.union( 

2052 ["_", "$"] 

2053 ) 

2054 

2055 def _bindparam_requires_quotes(self, value): 

2056 """Return True if the given identifier requires quoting.""" 

2057 lc_value = value.lower() 

2058 return ( 

2059 lc_value in self.reserved_words 

2060 or value[0] in self.illegal_initial_characters 

2061 or not self.legal_characters.match(str(value)) 

2062 ) 

2063 

2064 def format_savepoint(self, savepoint): 

2065 name = savepoint.ident.lstrip("_") 

2066 return super().format_savepoint(savepoint, name) 

2067 

2068 

2069class OracleExecutionContext(default.DefaultExecutionContext): 

2070 def fire_sequence(self, seq, type_): 

2071 return self._execute_scalar( 

2072 "SELECT " 

2073 + self.identifier_preparer.format_sequence(seq) 

2074 + ".nextval FROM DUAL", 

2075 type_, 

2076 ) 

2077 

2078 def pre_exec(self): 

2079 if self.statement and "_oracle_dblink" in self.execution_options: 

2080 self.statement = self.statement.replace( 

2081 dictionary.DB_LINK_PLACEHOLDER, 

2082 self.execution_options["_oracle_dblink"], 

2083 ) 

2084 

2085 

2086class OracleDialect(default._BackendsMultiReflection, default.DefaultDialect): 

2087 name = "oracle" 

2088 supports_statement_cache = True 

2089 supports_alter = True 

2090 max_identifier_length = 128 

2091 

2092 _supports_offset_fetch = True 

2093 

2094 insert_returning = True 

2095 update_returning = True 

2096 delete_returning = True 

2097 

2098 div_is_floordiv = False 

2099 

2100 supports_simple_order_by_label = False 

2101 cte_follows_insert = True 

2102 returns_native_bytes = True 

2103 

2104 supports_native_boolean = True 

2105 supports_sequences = True 

2106 sequences_optional = False 

2107 postfetch_lastrowid = False 

2108 

2109 default_paramstyle = "named" 

2110 colspecs = colspecs 

2111 ischema_names = ischema_names 

2112 requires_name_normalize = True 

2113 

2114 supports_comments = True 

2115 

2116 supports_default_values = False 

2117 supports_default_metavalue = True 

2118 supports_empty_insert = False 

2119 supports_identity_columns = True 

2120 

2121 _supports_oracle_json = True 

2122 

2123 aggregate_order_by_style = AggregateOrderByStyle.WITHIN_GROUP 

2124 

2125 statement_compiler = OracleCompiler 

2126 ddl_compiler = OracleDDLCompiler 

2127 type_compiler_cls = OracleTypeCompiler 

2128 preparer = OracleIdentifierPreparer 

2129 execution_ctx_cls = OracleExecutionContext 

2130 

2131 reflection_options = ("oracle_resolve_synonyms",) 

2132 

2133 _use_nchar_for_unicode = False 

2134 

2135 construct_arguments = [ 

2136 ( 

2137 sa_schema.Table, 

2138 { 

2139 "resolve_synonyms": False, 

2140 "on_commit": None, 

2141 "compress": False, 

2142 "tablespace": None, 

2143 }, 

2144 ), 

2145 ( 

2146 sa_schema.Index, 

2147 { 

2148 "bitmap": False, 

2149 "compress": False, 

2150 "vector": False, 

2151 }, 

2152 ), 

2153 (sa_schema.Sequence, {"order": None}), 

2154 (sa_schema.Identity, {"order": None, "on_null": None}), 

2155 (sa_selectable.Select, {"fetch_approximate": False}), 

2156 (sa_selectable.CompoundSelect, {"fetch_approximate": False}), 

2157 ] 

2158 

2159 @util.deprecated_params( 

2160 use_binds_for_limits=( 

2161 "1.4", 

2162 "The ``use_binds_for_limits`` Oracle Database dialect parameter " 

2163 "is deprecated. The dialect now renders LIMIT / OFFSET integers " 

2164 "inline in all cases using a post-compilation hook, so that the " 

2165 "value is still represented by a 'bound parameter' on the Core " 

2166 "Expression side.", 

2167 ) 

2168 ) 

2169 def __init__( 

2170 self, 

2171 use_ansi=True, 

2172 optimize_limits=False, 

2173 use_binds_for_limits=None, 

2174 use_nchar_for_unicode=False, 

2175 exclude_tablespaces=("SYSTEM", "SYSAUX"), 

2176 enable_offset_fetch=True, 

2177 json_serializer: Callable[[_JSON_VALUE], str] | None = None, 

2178 json_deserializer: Callable[[str], _JSON_VALUE] | None = None, 

2179 **kwargs, 

2180 ): 

2181 default.DefaultDialect.__init__(self, **kwargs) 

2182 self._use_nchar_for_unicode = use_nchar_for_unicode 

2183 self.use_ansi = use_ansi 

2184 self.optimize_limits = optimize_limits 

2185 self.exclude_tablespaces = exclude_tablespaces 

2186 self.enable_offset_fetch = self._supports_offset_fetch = ( 

2187 enable_offset_fetch 

2188 ) 

2189 self._json_serializer = json_serializer 

2190 self._json_deserializer = json_deserializer 

2191 

2192 def initialize(self, connection): 

2193 super().initialize(connection) 

2194 

2195 # Oracle 8i has RETURNING: 

2196 # https://docs.oracle.com/cd/A87860_01/doc/index.htm 

2197 

2198 # so does Oracle8: 

2199 # https://docs.oracle.com/cd/A64702_01/doc/index.htm 

2200 

2201 if self._is_oracle_8: 

2202 self.colspecs = self.colspecs.copy() 

2203 self.colspecs.pop(sqltypes.Interval) 

2204 self.use_ansi = False 

2205 

2206 self._supports_oracle_json = self.server_version_info >= (21,) 

2207 self.supports_native_boolean = self.server_version_info >= (23,) 

2208 self.supports_identity_columns = self.server_version_info >= (12,) 

2209 self._supports_offset_fetch = ( 

2210 self.enable_offset_fetch and self.server_version_info >= (12,) 

2211 ) 

2212 

2213 def _get_effective_compat_server_version_info(self, connection): 

2214 # dialect does not need compat levels below 12.2, so don't query 

2215 # in those cases 

2216 

2217 if self.server_version_info < (12, 2): 

2218 return self.server_version_info 

2219 try: 

2220 compat = connection.exec_driver_sql( 

2221 "SELECT value FROM v$parameter WHERE name = 'compatible'" 

2222 ).scalar() 

2223 except exc.DBAPIError: 

2224 compat = None 

2225 

2226 if compat: 

2227 try: 

2228 return tuple(int(x) for x in compat.split(".")) 

2229 except: 

2230 return self.server_version_info 

2231 else: 

2232 return self.server_version_info 

2233 

2234 @property 

2235 def _is_oracle_8(self): 

2236 return self.server_version_info and self.server_version_info < (9,) 

2237 

2238 @property 

2239 def _supports_table_compression(self): 

2240 return self.server_version_info and self.server_version_info >= (10, 1) 

2241 

2242 @property 

2243 def _supports_table_compress_for(self): 

2244 return self.server_version_info and self.server_version_info >= (11,) 

2245 

2246 @property 

2247 def _supports_char_length(self): 

2248 return not self._is_oracle_8 

2249 

2250 @property 

2251 def _supports_update_returning_computed_cols(self): 

2252 # on version 18 this error is no longet present while it happens on 11 

2253 # it may work also on versions before the 18 

2254 return self.server_version_info and self.server_version_info >= (18,) 

2255 

2256 @property 

2257 def _supports_except_all(self): 

2258 return self.server_version_info and self.server_version_info >= (21,) 

2259 

2260 def do_release_savepoint(self, connection, name): 

2261 # Oracle does not support RELEASE SAVEPOINT 

2262 pass 

2263 

2264 def _check_max_identifier_length(self, connection): 

2265 if self._get_effective_compat_server_version_info(connection) < ( 

2266 12, 

2267 2, 

2268 ): 

2269 return 30 

2270 else: 

2271 # use the default 

2272 return None 

2273 

2274 def get_isolation_level_values(self, dbapi_connection): 

2275 return ["READ COMMITTED", "SERIALIZABLE"] 

2276 

2277 def get_default_isolation_level(self, dbapi_conn): 

2278 try: 

2279 return self.get_isolation_level(dbapi_conn) 

2280 except NotImplementedError: 

2281 raise 

2282 except: 

2283 return "READ COMMITTED" 

2284 

2285 def _execute_reflection( 

2286 self, connection, query, dblink, returns_long, params=None 

2287 ): 

2288 if dblink and not dblink.startswith("@"): 

2289 dblink = f"@{dblink}" 

2290 execution_options = { 

2291 # handle db links 

2292 "_oracle_dblink": dblink or "", 

2293 # override any schema translate map 

2294 "schema_translate_map": None, 

2295 } 

2296 

2297 if dblink and returns_long: 

2298 # Oracle seems to error with 

2299 # "ORA-00997: illegal use of LONG datatype" when returning 

2300 # LONG columns via a dblink in a query with bind params 

2301 # This type seems to be very hard to cast into something else 

2302 # so it seems easier to just use bind param in this case 

2303 def visit_bindparam(bindparam): 

2304 bindparam.literal_execute = True 

2305 

2306 query = visitors.cloned_traverse( 

2307 query, {}, {"bindparam": visit_bindparam} 

2308 ) 

2309 return connection.execute( 

2310 query, params, execution_options=execution_options 

2311 ) 

2312 

2313 @util.memoized_property 

2314 def _has_multi_table_query(self): 

2315 # materialized views are returned by all_tables 

2316 tables = ( 

2317 select( 

2318 dictionary.all_tables.c.table_name, 

2319 dictionary.all_tables.c.owner, 

2320 ) 

2321 .union_all( 

2322 select( 

2323 dictionary.all_views.c.view_name.label("table_name"), 

2324 dictionary.all_views.c.owner, 

2325 ) 

2326 ) 

2327 .subquery("tables_and_views") 

2328 ) 

2329 

2330 query = select(tables.c.table_name).where( 

2331 tables.c.table_name.in_(bindparam("table_names")), 

2332 tables.c.owner == bindparam("owner"), 

2333 ) 

2334 return query 

2335 

2336 def has_multi_table( 

2337 self, connection, table_names, schema=None, dblink=None, **kw 

2338 ): 

2339 """Supported kw arguments are: ``dblink`` to reflect via a db link.""" 

2340 

2341 owner = schema or self.default_schema_name 

2342 db_tns = [self.denormalize_name(tn) for tn in table_names] 

2343 params = { 

2344 "table_names": db_tns, 

2345 "owner": self.denormalize_schema_name(owner), 

2346 } 

2347 cursor = self._execute_reflection( 

2348 connection, 

2349 self._has_multi_table_query, 

2350 dblink, 

2351 returns_long=False, 

2352 params=params, 

2353 ) 

2354 existing = set(cursor.scalars().all()) 

2355 retval = { 

2356 (schema, table): db_tn in existing 

2357 for table, db_tn in zip(table_names, db_tns) 

2358 } 

2359 return retval.items() 

2360 

2361 @reflection.cache 

2362 def has_sequence( 

2363 self, connection, sequence_name, schema=None, dblink=None, **kw 

2364 ): 

2365 """Supported kw arguments are: ``dblink`` to reflect via a db link.""" 

2366 if not schema: 

2367 schema = self.default_schema_name 

2368 

2369 query = select(dictionary.all_sequences.c.sequence_name).where( 

2370 dictionary.all_sequences.c.sequence_name 

2371 == self.denormalize_schema_name(sequence_name), 

2372 dictionary.all_sequences.c.sequence_owner 

2373 == self.denormalize_schema_name(schema), 

2374 ) 

2375 

2376 cursor = self._execute_reflection( 

2377 connection, query, dblink, returns_long=False 

2378 ) 

2379 return bool(cursor.scalar()) 

2380 

2381 def _get_default_schema_name(self, connection): 

2382 return self.normalize_name( 

2383 connection.exec_driver_sql( 

2384 "select sys_context( 'userenv', 'current_schema' ) from dual" 

2385 ).scalar() 

2386 ) 

2387 

2388 def denormalize_schema_name(self, name): 

2389 # look for quoted_name 

2390 force = getattr(name, "quote", None) 

2391 if force is None and name == "public": 

2392 # look for case insensitive, no quoting specified, "public" 

2393 return "PUBLIC" 

2394 return super().denormalize_name(name) 

2395 

2396 @reflection.flexi_cache( 

2397 ("schema", InternalTraversal.dp_string), 

2398 ("filter_names", InternalTraversal.dp_string_list), 

2399 ("dblink", InternalTraversal.dp_string), 

2400 ) 

2401 def _get_synonyms(self, connection, schema, filter_names, dblink, **kw): 

2402 owner = self.denormalize_schema_name( 

2403 schema or self.default_schema_name 

2404 ) 

2405 

2406 has_filter_names, params = self._prepare_filter_names(filter_names) 

2407 query = select( 

2408 dictionary.all_synonyms.c.synonym_name, 

2409 dictionary.all_synonyms.c.table_name, 

2410 dictionary.all_synonyms.c.table_owner, 

2411 dictionary.all_synonyms.c.db_link, 

2412 ).where(dictionary.all_synonyms.c.owner == owner) 

2413 if has_filter_names: 

2414 query = query.where( 

2415 dictionary.all_synonyms.c.synonym_name.in_( 

2416 params["filter_names"] 

2417 ) 

2418 ) 

2419 result = self._execute_reflection( 

2420 connection, query, dblink, returns_long=False 

2421 ).mappings() 

2422 return result.all() 

2423 

2424 @lru_cache() 

2425 def _all_objects_query( 

2426 self, owner, scope, kind, has_filter_names, has_mat_views 

2427 ): 

2428 query = ( 

2429 select(dictionary.all_objects.c.object_name) 

2430 .select_from(dictionary.all_objects) 

2431 .where(dictionary.all_objects.c.owner == owner) 

2432 ) 

2433 

2434 # NOTE: materialized views are listed in all_objects twice; 

2435 # once as MATERIALIZE VIEW and once as TABLE 

2436 if kind is ObjectKind.ANY: 

2437 # materilaized view are listed also as tables so there is no 

2438 # need to add them to the in_. 

2439 query = query.where( 

2440 dictionary.all_objects.c.object_type.in_(("TABLE", "VIEW")) 

2441 ) 

2442 else: 

2443 object_type = [] 

2444 if ObjectKind.VIEW in kind: 

2445 object_type.append("VIEW") 

2446 if ( 

2447 ObjectKind.MATERIALIZED_VIEW in kind 

2448 and ObjectKind.TABLE not in kind 

2449 ): 

2450 # materilaized view are listed also as tables so there is no 

2451 # need to add them to the in_ if also selecting tables. 

2452 object_type.append("MATERIALIZED VIEW") 

2453 if ObjectKind.TABLE in kind: 

2454 object_type.append("TABLE") 

2455 if has_mat_views and ObjectKind.MATERIALIZED_VIEW not in kind: 

2456 # materialized view are listed also as tables, 

2457 # so they need to be filtered out 

2458 # EXCEPT ALL / MINUS profiles as faster than using 

2459 # NOT EXISTS or NOT IN with a subquery, but it's in 

2460 # general faster to get the mat view names and exclude 

2461 # them only when needed 

2462 query = query.where( 

2463 dictionary.all_objects.c.object_name.not_in( 

2464 bindparam("mat_views") 

2465 ) 

2466 ) 

2467 query = query.where( 

2468 dictionary.all_objects.c.object_type.in_(object_type) 

2469 ) 

2470 

2471 # handles scope 

2472 if scope is ObjectScope.DEFAULT: 

2473 query = query.where(dictionary.all_objects.c.temporary == "N") 

2474 elif scope is ObjectScope.TEMPORARY: 

2475 query = query.where(dictionary.all_objects.c.temporary == "Y") 

2476 

2477 if has_filter_names: 

2478 query = query.where( 

2479 dictionary.all_objects.c.object_name.in_( 

2480 bindparam("filter_names") 

2481 ) 

2482 ) 

2483 return query 

2484 

2485 @reflection.flexi_cache( 

2486 ("schema", InternalTraversal.dp_string), 

2487 ("scope", InternalTraversal.dp_plain_obj), 

2488 ("kind", InternalTraversal.dp_plain_obj), 

2489 ("filter_names", InternalTraversal.dp_string_list), 

2490 ("dblink", InternalTraversal.dp_string), 

2491 ) 

2492 def _get_all_objects( 

2493 self, connection, schema, scope, kind, filter_names, dblink, **kw 

2494 ): 

2495 owner = self.denormalize_schema_name( 

2496 schema or self.default_schema_name 

2497 ) 

2498 

2499 has_filter_names, params = self._prepare_filter_names(filter_names) 

2500 has_mat_views = False 

2501 if ( 

2502 ObjectKind.TABLE in kind 

2503 and ObjectKind.MATERIALIZED_VIEW not in kind 

2504 ): 

2505 # see note in _all_objects_query 

2506 mat_views = self.get_materialized_view_names( 

2507 connection, schema, dblink, _normalize=False, **kw 

2508 ) 

2509 if mat_views: 

2510 params["mat_views"] = mat_views 

2511 has_mat_views = True 

2512 

2513 query = self._all_objects_query( 

2514 owner, scope, kind, has_filter_names, has_mat_views 

2515 ) 

2516 

2517 result = self._execute_reflection( 

2518 connection, query, dblink, returns_long=False, params=params 

2519 ).scalars() 

2520 

2521 return result.all() 

2522 

2523 def _handle_synonyms_decorator(fn): 

2524 @wraps(fn) 

2525 def wrapper(self, *args, **kwargs): 

2526 return self._handle_synonyms(fn, *args, **kwargs) 

2527 

2528 return wrapper 

2529 

2530 def _handle_synonyms(self, fn, connection, *args, **kwargs): 

2531 if not kwargs.get("oracle_resolve_synonyms", False): 

2532 return fn(self, connection, *args, **kwargs) 

2533 

2534 original_kw = kwargs.copy() 

2535 schema = kwargs.pop("schema", None) 

2536 result = self._get_synonyms( 

2537 connection, 

2538 schema=schema, 

2539 filter_names=kwargs.pop("filter_names", None), 

2540 dblink=kwargs.pop("dblink", None), 

2541 info_cache=kwargs.get("info_cache", None), 

2542 ) 

2543 

2544 dblinks_owners = defaultdict(dict) 

2545 for row in result: 

2546 key = row["db_link"], row["table_owner"] 

2547 tn = self.normalize_name(row["table_name"]) 

2548 dblinks_owners[key][tn] = row["synonym_name"] 

2549 

2550 if not dblinks_owners: 

2551 # No synonym, do the plain thing 

2552 return fn(self, connection, *args, **original_kw) 

2553 

2554 data = {} 

2555 for (dblink, table_owner), mapping in dblinks_owners.items(): 

2556 call_kw = { 

2557 **original_kw, 

2558 "schema": table_owner, 

2559 "dblink": self.normalize_name(dblink), 

2560 "filter_names": mapping.keys(), 

2561 } 

2562 call_result = fn(self, connection, *args, **call_kw) 

2563 for (_, tn), value in call_result: 

2564 synonym_name = self.normalize_name(mapping[tn]) 

2565 data[(schema, synonym_name)] = value 

2566 return data.items() 

2567 

2568 @reflection.cache 

2569 def get_schema_names(self, connection, dblink=None, **kw): 

2570 """Supported kw arguments are: ``dblink`` to reflect via a db link.""" 

2571 query = select(dictionary.all_users.c.username).order_by( 

2572 dictionary.all_users.c.username 

2573 ) 

2574 result = self._execute_reflection( 

2575 connection, query, dblink, returns_long=False 

2576 ).scalars() 

2577 return [self.normalize_name(row) for row in result] 

2578 

2579 @reflection.cache 

2580 def get_table_names(self, connection, schema=None, dblink=None, **kw): 

2581 """Supported kw arguments are: ``dblink`` to reflect via a db link.""" 

2582 # note that table_names() isn't loading DBLINKed or synonym'ed tables 

2583 if schema is None: 

2584 schema = self.default_schema_name 

2585 

2586 den_schema = self.denormalize_schema_name(schema) 

2587 if kw.get("oracle_resolve_synonyms", False): 

2588 tables = ( 

2589 select( 

2590 dictionary.all_tables.c.table_name, 

2591 dictionary.all_tables.c.owner, 

2592 dictionary.all_tables.c.iot_name, 

2593 dictionary.all_tables.c.duration, 

2594 dictionary.all_tables.c.tablespace_name, 

2595 ) 

2596 .union_all( 

2597 select( 

2598 dictionary.all_synonyms.c.synonym_name.label( 

2599 "table_name" 

2600 ), 

2601 dictionary.all_synonyms.c.owner, 

2602 dictionary.all_tables.c.iot_name, 

2603 dictionary.all_tables.c.duration, 

2604 dictionary.all_tables.c.tablespace_name, 

2605 ) 

2606 .select_from(dictionary.all_tables) 

2607 .join( 

2608 dictionary.all_synonyms, 

2609 and_( 

2610 dictionary.all_tables.c.table_name 

2611 == dictionary.all_synonyms.c.table_name, 

2612 dictionary.all_tables.c.owner 

2613 == func.coalesce( 

2614 dictionary.all_synonyms.c.table_owner, 

2615 dictionary.all_synonyms.c.owner, 

2616 ), 

2617 ), 

2618 ) 

2619 ) 

2620 .subquery("available_tables") 

2621 ) 

2622 else: 

2623 tables = dictionary.all_tables 

2624 

2625 query = select(tables.c.table_name) 

2626 if self.exclude_tablespaces: 

2627 query = query.where( 

2628 func.coalesce( 

2629 tables.c.tablespace_name, "no tablespace" 

2630 ).not_in(self.exclude_tablespaces) 

2631 ) 

2632 query = query.where( 

2633 tables.c.owner == den_schema, 

2634 tables.c.iot_name.is_(null()), 

2635 tables.c.duration.is_(null()), 

2636 ) 

2637 

2638 # remove materialized views 

2639 mat_query = select( 

2640 dictionary.all_mviews.c.mview_name.label("table_name") 

2641 ).where(dictionary.all_mviews.c.owner == den_schema) 

2642 

2643 query = ( 

2644 query.except_all(mat_query) 

2645 if self._supports_except_all 

2646 else query.except_(mat_query) 

2647 ) 

2648 

2649 result = self._execute_reflection( 

2650 connection, query, dblink, returns_long=False 

2651 ).scalars() 

2652 return [self.normalize_name(row) for row in result] 

2653 

2654 @reflection.cache 

2655 def get_temp_table_names(self, connection, dblink=None, **kw): 

2656 """Supported kw arguments are: ``dblink`` to reflect via a db link.""" 

2657 schema = self.denormalize_schema_name(self.default_schema_name) 

2658 

2659 query = select(dictionary.all_tables.c.table_name) 

2660 if self.exclude_tablespaces: 

2661 query = query.where( 

2662 func.coalesce( 

2663 dictionary.all_tables.c.tablespace_name, "no tablespace" 

2664 ).not_in(self.exclude_tablespaces) 

2665 ) 

2666 query = query.where( 

2667 dictionary.all_tables.c.owner == schema, 

2668 dictionary.all_tables.c.iot_name.is_(null()), 

2669 dictionary.all_tables.c.duration.is_not(null()), 

2670 ) 

2671 

2672 result = self._execute_reflection( 

2673 connection, query, dblink, returns_long=False 

2674 ).scalars() 

2675 return [self.normalize_name(row) for row in result] 

2676 

2677 @reflection.cache 

2678 def get_materialized_view_names( 

2679 self, connection, schema=None, dblink=None, _normalize=True, **kw 

2680 ): 

2681 """Supported kw arguments are: ``dblink`` to reflect via a db link.""" 

2682 if not schema: 

2683 schema = self.default_schema_name 

2684 

2685 query = select(dictionary.all_mviews.c.mview_name).where( 

2686 dictionary.all_mviews.c.owner 

2687 == self.denormalize_schema_name(schema) 

2688 ) 

2689 result = self._execute_reflection( 

2690 connection, query, dblink, returns_long=False 

2691 ).scalars() 

2692 if _normalize: 

2693 return [self.normalize_name(row) for row in result] 

2694 else: 

2695 return result.all() 

2696 

2697 @reflection.cache 

2698 def get_view_names(self, connection, schema=None, dblink=None, **kw): 

2699 """Supported kw arguments are: ``dblink`` to reflect via a db link.""" 

2700 if not schema: 

2701 schema = self.default_schema_name 

2702 

2703 query = select(dictionary.all_views.c.view_name).where( 

2704 dictionary.all_views.c.owner 

2705 == self.denormalize_schema_name(schema) 

2706 ) 

2707 result = self._execute_reflection( 

2708 connection, query, dblink, returns_long=False 

2709 ).scalars() 

2710 return [self.normalize_name(row) for row in result] 

2711 

2712 @reflection.cache 

2713 def get_sequence_names(self, connection, schema=None, dblink=None, **kw): 

2714 """Supported kw arguments are: ``dblink`` to reflect via a db link.""" 

2715 if not schema: 

2716 schema = self.default_schema_name 

2717 query = select(dictionary.all_sequences.c.sequence_name).where( 

2718 dictionary.all_sequences.c.sequence_owner 

2719 == self.denormalize_schema_name(schema) 

2720 ) 

2721 

2722 result = self._execute_reflection( 

2723 connection, query, dblink, returns_long=False 

2724 ).scalars() 

2725 return [self.normalize_name(row) for row in result] 

2726 

2727 def _value_or_raise(self, data, table, schema): 

2728 table = self.normalize_name(str(table)) 

2729 try: 

2730 return dict(data)[(schema, table)] 

2731 except KeyError: 

2732 raise exc.NoSuchTableError( 

2733 f"{schema}.{table}" if schema else table 

2734 ) from None 

2735 

2736 def _prepare_filter_names(self, filter_names): 

2737 if filter_names: 

2738 fn = [self.denormalize_name(name) for name in filter_names] 

2739 return True, {"filter_names": fn} 

2740 else: 

2741 return False, {} 

2742 

2743 @lru_cache() 

2744 def _table_options_query( 

2745 self, owner, scope, kind, has_filter_names, has_mat_views 

2746 ): 

2747 query = select( 

2748 dictionary.all_tables.c.table_name, 

2749 ( 

2750 dictionary.all_tables.c.compression 

2751 if self._supports_table_compression 

2752 else sql.null().label("compression") 

2753 ), 

2754 ( 

2755 dictionary.all_tables.c.compress_for 

2756 if self._supports_table_compress_for 

2757 else sql.null().label("compress_for") 

2758 ), 

2759 dictionary.all_tables.c.tablespace_name, 

2760 ).where(dictionary.all_tables.c.owner == owner) 

2761 if has_filter_names: 

2762 query = query.where( 

2763 dictionary.all_tables.c.table_name.in_( 

2764 bindparam("filter_names") 

2765 ) 

2766 ) 

2767 if scope is ObjectScope.DEFAULT: 

2768 query = query.where(dictionary.all_tables.c.duration.is_(null())) 

2769 elif scope is ObjectScope.TEMPORARY: 

2770 query = query.where( 

2771 dictionary.all_tables.c.duration.is_not(null()) 

2772 ) 

2773 

2774 if ( 

2775 has_mat_views 

2776 and ObjectKind.TABLE in kind 

2777 and ObjectKind.MATERIALIZED_VIEW not in kind 

2778 ): 

2779 # can't use EXCEPT ALL / MINUS here because we don't have an 

2780 # excludable row vs. the query above 

2781 # outerjoin + where null works better on oracle 21 but 11 does 

2782 # not like it at all. this is the next best thing 

2783 

2784 query = query.where( 

2785 dictionary.all_tables.c.table_name.not_in( 

2786 bindparam("mat_views") 

2787 ) 

2788 ) 

2789 elif ( 

2790 ObjectKind.TABLE not in kind 

2791 and ObjectKind.MATERIALIZED_VIEW in kind 

2792 ): 

2793 query = query.where( 

2794 dictionary.all_tables.c.table_name.in_(bindparam("mat_views")) 

2795 ) 

2796 return query 

2797 

2798 @_handle_synonyms_decorator 

2799 def get_multi_table_options( 

2800 self, 

2801 connection, 

2802 *, 

2803 schema, 

2804 filter_names, 

2805 scope, 

2806 kind, 

2807 dblink=None, 

2808 **kw, 

2809 ): 

2810 """Supported kw arguments are: ``dblink`` to reflect via a db link; 

2811 ``oracle_resolve_synonyms`` to resolve names to synonyms 

2812 """ 

2813 owner = self.denormalize_schema_name( 

2814 schema or self.default_schema_name 

2815 ) 

2816 

2817 has_filter_names, params = self._prepare_filter_names(filter_names) 

2818 has_mat_views = False 

2819 

2820 if ( 

2821 ObjectKind.TABLE in kind 

2822 and ObjectKind.MATERIALIZED_VIEW not in kind 

2823 ): 

2824 # see note in _table_options_query 

2825 mat_views = self.get_materialized_view_names( 

2826 connection, schema, dblink, _normalize=False, **kw 

2827 ) 

2828 if mat_views: 

2829 params["mat_views"] = mat_views 

2830 has_mat_views = True 

2831 elif ( 

2832 ObjectKind.TABLE not in kind 

2833 and ObjectKind.MATERIALIZED_VIEW in kind 

2834 ): 

2835 mat_views = self.get_materialized_view_names( 

2836 connection, schema, dblink, _normalize=False, **kw 

2837 ) 

2838 params["mat_views"] = mat_views 

2839 

2840 options = {} 

2841 default = ReflectionDefaults.table_options 

2842 

2843 if ObjectKind.TABLE in kind or ObjectKind.MATERIALIZED_VIEW in kind: 

2844 query = self._table_options_query( 

2845 owner, scope, kind, has_filter_names, has_mat_views 

2846 ) 

2847 result = self._execute_reflection( 

2848 connection, query, dblink, returns_long=False, params=params 

2849 ) 

2850 

2851 for table, compression, compress_for, tablespace in result: 

2852 data = default() 

2853 if compression == "ENABLED": 

2854 data["oracle_compress"] = compress_for 

2855 if tablespace: 

2856 data["oracle_tablespace"] = tablespace 

2857 options[(schema, self.normalize_name(table))] = data 

2858 if ObjectKind.VIEW in kind and ObjectScope.DEFAULT in scope: 

2859 # add the views (no temporary views) 

2860 for view in self.get_view_names(connection, schema, dblink, **kw): 

2861 if not filter_names or view in filter_names: 

2862 options[(schema, view)] = default() 

2863 

2864 return options.items() 

2865 

2866 def _run_batches( 

2867 self, connection, query, dblink, returns_long, mappings, all_objects 

2868 ): 

2869 each_batch = 500 

2870 batches = list(all_objects) 

2871 while batches: 

2872 batch = batches[0:each_batch] 

2873 batches[0:each_batch] = [] 

2874 

2875 result = self._execute_reflection( 

2876 connection, 

2877 query, 

2878 dblink, 

2879 returns_long=returns_long, 

2880 params={"all_objects": batch}, 

2881 ) 

2882 if mappings: 

2883 yield from result.mappings() 

2884 else: 

2885 yield from result 

2886 

2887 @lru_cache() 

2888 def _column_query(self, owner): 

2889 all_cols = dictionary.all_tab_cols 

2890 all_comments = dictionary.all_col_comments 

2891 all_ids = dictionary.all_tab_identity_cols 

2892 

2893 if self.server_version_info >= (12,): 

2894 add_cols = ( 

2895 all_cols.c.default_on_null, 

2896 sql.case( 

2897 (all_ids.c.table_name.is_(None), sql.null()), 

2898 else_=all_ids.c.generation_type 

2899 + "," 

2900 + all_ids.c.identity_options, 

2901 ).label("identity_options"), 

2902 ) 

2903 join_identity_cols = True 

2904 else: 

2905 add_cols = ( 

2906 sql.null().label("default_on_null"), 

2907 sql.null().label("identity_options"), 

2908 ) 

2909 join_identity_cols = False 

2910 

2911 # NOTE: on oracle cannot create tables/views without columns and 

2912 # a table cannot have all column hidden: 

2913 # ORA-54039: table must have at least one column that is not invisible 

2914 # all_tab_cols returns data for tables/views/mat-views. 

2915 # all_tab_cols does not return recycled tables 

2916 

2917 query = ( 

2918 select( 

2919 all_cols.c.table_name, 

2920 all_cols.c.column_name, 

2921 all_cols.c.data_type, 

2922 all_cols.c.char_length, 

2923 all_cols.c.data_length, 

2924 all_cols.c.data_precision, 

2925 all_cols.c.data_scale, 

2926 all_cols.c.nullable, 

2927 all_cols.c.data_default, 

2928 all_comments.c.comments, 

2929 all_cols.c.virtual_column, 

2930 *add_cols, 

2931 ).select_from(all_cols) 

2932 # NOTE: all_col_comments has a row for each column even if no 

2933 # comment is present, so a join could be performed, but there 

2934 # seems to be no difference compared to an outer join 

2935 .outerjoin( 

2936 all_comments, 

2937 and_( 

2938 all_cols.c.table_name == all_comments.c.table_name, 

2939 all_cols.c.column_name == all_comments.c.column_name, 

2940 all_cols.c.owner == all_comments.c.owner, 

2941 ), 

2942 ) 

2943 ) 

2944 if join_identity_cols: 

2945 query = query.outerjoin( 

2946 all_ids, 

2947 and_( 

2948 all_cols.c.table_name == all_ids.c.table_name, 

2949 all_cols.c.column_name == all_ids.c.column_name, 

2950 all_cols.c.owner == all_ids.c.owner, 

2951 ), 

2952 ) 

2953 

2954 query = query.where( 

2955 all_cols.c.table_name.in_(bindparam("all_objects")), 

2956 all_cols.c.hidden_column == "NO", 

2957 all_cols.c.owner == owner, 

2958 ).order_by(all_cols.c.table_name, all_cols.c.column_id) 

2959 return query 

2960 

2961 @_handle_synonyms_decorator 

2962 def get_multi_columns( 

2963 self, 

2964 connection, 

2965 *, 

2966 schema, 

2967 filter_names, 

2968 scope, 

2969 kind, 

2970 dblink=None, 

2971 **kw, 

2972 ): 

2973 """Supported kw arguments are: ``dblink`` to reflect via a db link; 

2974 ``oracle_resolve_synonyms`` to resolve names to synonyms 

2975 """ 

2976 owner = self.denormalize_schema_name( 

2977 schema or self.default_schema_name 

2978 ) 

2979 query = self._column_query(owner) 

2980 

2981 if ( 

2982 filter_names 

2983 and kind is ObjectKind.ANY 

2984 and scope is ObjectScope.ANY 

2985 ): 

2986 all_objects = [self.denormalize_name(n) for n in filter_names] 

2987 else: 

2988 all_objects = self._get_all_objects( 

2989 connection, schema, scope, kind, filter_names, dblink, **kw 

2990 ) 

2991 

2992 columns = defaultdict(list) 

2993 

2994 # all_tab_cols.data_default is LONG 

2995 result = self._run_batches( 

2996 connection, 

2997 query, 

2998 dblink, 

2999 returns_long=True, 

3000 mappings=True, 

3001 all_objects=all_objects, 

3002 ) 

3003 

3004 def maybe_int(value): 

3005 if isinstance(value, float) and value.is_integer(): 

3006 return int(value) 

3007 else: 

3008 return value 

3009 

3010 remove_size = re.compile(r"\(\d+\)") 

3011 

3012 for row_dict in result: 

3013 table_name = self.normalize_name(row_dict["table_name"]) 

3014 orig_colname = row_dict["column_name"] 

3015 colname = self.normalize_name(orig_colname) 

3016 coltype = row_dict["data_type"] 

3017 precision = maybe_int(row_dict["data_precision"]) 

3018 

3019 if coltype == "NUMBER": 

3020 scale = maybe_int(row_dict["data_scale"]) 

3021 if precision is None and scale == 0: 

3022 coltype = INTEGER() 

3023 else: 

3024 coltype = NUMBER(precision, scale) 

3025 elif coltype == "FLOAT": 

3026 # https://docs.oracle.com/cd/B14117_01/server.101/b10758/sqlqr06.htm 

3027 if precision == 126: 

3028 # The DOUBLE PRECISION datatype is a floating-point 

3029 # number with binary precision 126. 

3030 coltype = DOUBLE_PRECISION() 

3031 elif precision == 63: 

3032 # The REAL datatype is a floating-point number with a 

3033 # binary precision of 63, or 18 decimal. 

3034 coltype = REAL() 

3035 else: 

3036 # non standard precision 

3037 coltype = FLOAT(binary_precision=precision) 

3038 

3039 elif coltype in ("VARCHAR2", "NVARCHAR2", "CHAR", "NCHAR"): 

3040 char_length = maybe_int(row_dict["char_length"]) 

3041 coltype = self.ischema_names.get(coltype)(char_length) 

3042 elif coltype == "RAW": 

3043 data_length = maybe_int(row_dict["data_length"]) 

3044 coltype = RAW(data_length) 

3045 elif "WITH TIME ZONE" in coltype: 

3046 coltype = TIMESTAMP(timezone=True) 

3047 elif "WITH LOCAL TIME ZONE" in coltype: 

3048 coltype = TIMESTAMP(local_timezone=True) 

3049 else: 

3050 coltype = re.sub(remove_size, "", coltype) 

3051 try: 

3052 coltype = self.ischema_names[coltype] 

3053 except KeyError: 

3054 util.warn( 

3055 "Did not recognize type '%s' of column '%s'" 

3056 % (coltype, colname) 

3057 ) 

3058 coltype = sqltypes.NULLTYPE 

3059 

3060 default = row_dict["data_default"] 

3061 if row_dict["virtual_column"] == "YES": 

3062 computed = dict(sqltext=default) 

3063 default = None 

3064 else: 

3065 computed = None 

3066 

3067 identity_options = row_dict["identity_options"] 

3068 if identity_options is not None: 

3069 identity = self._parse_identity_options( 

3070 identity_options, row_dict["default_on_null"] 

3071 ) 

3072 default = None 

3073 else: 

3074 identity = None 

3075 

3076 cdict = { 

3077 "name": colname, 

3078 "type": coltype, 

3079 "nullable": row_dict["nullable"] == "Y", 

3080 "default": default, 

3081 "comment": row_dict["comments"], 

3082 } 

3083 if orig_colname.lower() == orig_colname: 

3084 cdict["quote"] = True 

3085 if computed is not None: 

3086 cdict["computed"] = computed 

3087 if identity is not None: 

3088 cdict["identity"] = identity 

3089 

3090 columns[(schema, table_name)].append(cdict) 

3091 

3092 # NOTE: default not needed since all tables have columns 

3093 # default = ReflectionDefaults.columns 

3094 # return ( 

3095 # (key, value if value else default()) 

3096 # for key, value in columns.items() 

3097 # ) 

3098 return columns.items() 

3099 

3100 def _parse_identity_options(self, identity_options, default_on_null): 

3101 # identity_options is a string that starts with 'ALWAYS,' or 

3102 # 'BY DEFAULT,' and continues with 

3103 # START WITH: 1, INCREMENT BY: 1, MAX_VALUE: 123, MIN_VALUE: 1, 

3104 # CYCLE_FLAG: N, CACHE_SIZE: 1, ORDER_FLAG: N, SCALE_FLAG: N, 

3105 # EXTEND_FLAG: N, SESSION_FLAG: N, KEEP_VALUE: N 

3106 parts = [p.strip() for p in identity_options.split(",")] 

3107 identity = { 

3108 "always": parts[0] == "ALWAYS", 

3109 "oracle_on_null": default_on_null == "YES", 

3110 } 

3111 

3112 for part in parts[1:]: 

3113 option, value = part.split(":") 

3114 value = value.strip() 

3115 

3116 if "START WITH" in option: 

3117 identity["start"] = int(value) 

3118 elif "INCREMENT BY" in option: 

3119 identity["increment"] = int(value) 

3120 elif "MAX_VALUE" in option: 

3121 identity["maxvalue"] = int(value) 

3122 elif "MIN_VALUE" in option: 

3123 identity["minvalue"] = int(value) 

3124 elif "CYCLE_FLAG" in option: 

3125 identity["cycle"] = value == "Y" 

3126 elif "CACHE_SIZE" in option: 

3127 identity["cache"] = int(value) 

3128 elif "ORDER_FLAG" in option: 

3129 identity["oracle_order"] = value == "Y" 

3130 return identity 

3131 

3132 @lru_cache() 

3133 def _comment_query(self, owner, scope, kind, has_filter_names): 

3134 # NOTE: all_tab_comments / all_mview_comments have a row for all 

3135 # object even if they don't have comments 

3136 queries = [] 

3137 if ObjectKind.TABLE in kind or ObjectKind.VIEW in kind: 

3138 # all_tab_comments returns also plain views 

3139 tbl_view = select( 

3140 dictionary.all_tab_comments.c.table_name, 

3141 dictionary.all_tab_comments.c.comments, 

3142 ).where( 

3143 dictionary.all_tab_comments.c.owner == owner, 

3144 dictionary.all_tab_comments.c.table_name.not_like("BIN$%"), 

3145 ) 

3146 if ObjectKind.VIEW not in kind: 

3147 tbl_view = tbl_view.where( 

3148 dictionary.all_tab_comments.c.table_type == "TABLE" 

3149 ) 

3150 elif ObjectKind.TABLE not in kind: 

3151 tbl_view = tbl_view.where( 

3152 dictionary.all_tab_comments.c.table_type == "VIEW" 

3153 ) 

3154 queries.append(tbl_view) 

3155 if ObjectKind.MATERIALIZED_VIEW in kind: 

3156 mat_view = select( 

3157 dictionary.all_mview_comments.c.mview_name.label("table_name"), 

3158 dictionary.all_mview_comments.c.comments, 

3159 ).where( 

3160 dictionary.all_mview_comments.c.owner == owner, 

3161 dictionary.all_mview_comments.c.mview_name.not_like("BIN$%"), 

3162 ) 

3163 queries.append(mat_view) 

3164 if len(queries) == 1: 

3165 query = queries[0] 

3166 else: 

3167 union = sql.union_all(*queries).subquery("tables_and_views") 

3168 query = select(union.c.table_name, union.c.comments) 

3169 

3170 name_col = query.selected_columns.table_name 

3171 

3172 if scope in (ObjectScope.DEFAULT, ObjectScope.TEMPORARY): 

3173 temp = "Y" if scope is ObjectScope.TEMPORARY else "N" 

3174 # need distinct since materialized view are listed also 

3175 # as tables in all_objects 

3176 query = query.distinct().join( 

3177 dictionary.all_objects, 

3178 and_( 

3179 dictionary.all_objects.c.owner == owner, 

3180 dictionary.all_objects.c.object_name == name_col, 

3181 dictionary.all_objects.c.temporary == temp, 

3182 ), 

3183 ) 

3184 if has_filter_names: 

3185 query = query.where(name_col.in_(bindparam("filter_names"))) 

3186 return query 

3187 

3188 @_handle_synonyms_decorator 

3189 def get_multi_table_comment( 

3190 self, 

3191 connection, 

3192 *, 

3193 schema, 

3194 filter_names, 

3195 scope, 

3196 kind, 

3197 dblink=None, 

3198 **kw, 

3199 ): 

3200 """Supported kw arguments are: ``dblink`` to reflect via a db link; 

3201 ``oracle_resolve_synonyms`` to resolve names to synonyms 

3202 """ 

3203 owner = self.denormalize_schema_name( 

3204 schema or self.default_schema_name 

3205 ) 

3206 has_filter_names, params = self._prepare_filter_names(filter_names) 

3207 query = self._comment_query(owner, scope, kind, has_filter_names) 

3208 

3209 result = self._execute_reflection( 

3210 connection, query, dblink, returns_long=False, params=params 

3211 ) 

3212 default = ReflectionDefaults.table_comment 

3213 # materialized views by default seem to have a comment like 

3214 # "snapshot table for snapshot owner.mat_view_name" 

3215 ignore_mat_view = "snapshot table for snapshot " 

3216 return ( 

3217 ( 

3218 (schema, self.normalize_name(table)), 

3219 ( 

3220 {"text": comment} 

3221 if comment is not None 

3222 and not comment.startswith(ignore_mat_view) 

3223 else default() 

3224 ), 

3225 ) 

3226 for table, comment in result 

3227 ) 

3228 

3229 @lru_cache() 

3230 def _index_query(self, owner): 

3231 return ( 

3232 select( 

3233 dictionary.all_ind_columns.c.table_name, 

3234 dictionary.all_ind_columns.c.index_name, 

3235 dictionary.all_ind_columns.c.column_name, 

3236 dictionary.all_indexes.c.index_type, 

3237 dictionary.all_indexes.c.uniqueness, 

3238 dictionary.all_indexes.c.compression, 

3239 dictionary.all_indexes.c.prefix_length, 

3240 dictionary.all_ind_columns.c.descend, 

3241 dictionary.all_ind_expressions.c.column_expression, 

3242 ) 

3243 .select_from(dictionary.all_ind_columns) 

3244 .join( 

3245 dictionary.all_indexes, 

3246 sql.and_( 

3247 dictionary.all_ind_columns.c.index_name 

3248 == dictionary.all_indexes.c.index_name, 

3249 dictionary.all_ind_columns.c.index_owner 

3250 == dictionary.all_indexes.c.owner, 

3251 ), 

3252 ) 

3253 .outerjoin( 

3254 # NOTE: this adds about 20% to the query time. Using a 

3255 # case expression with a scalar subquery only when needed 

3256 # with the assumption that most indexes are not expression 

3257 # would be faster but oracle does not like that with 

3258 # LONG datatype. It errors with: 

3259 # ORA-00997: illegal use of LONG datatype 

3260 dictionary.all_ind_expressions, 

3261 sql.and_( 

3262 dictionary.all_ind_expressions.c.index_name 

3263 == dictionary.all_ind_columns.c.index_name, 

3264 dictionary.all_ind_expressions.c.index_owner 

3265 == dictionary.all_ind_columns.c.index_owner, 

3266 dictionary.all_ind_expressions.c.column_position 

3267 == dictionary.all_ind_columns.c.column_position, 

3268 ), 

3269 ) 

3270 .where( 

3271 dictionary.all_indexes.c.table_owner == owner, 

3272 dictionary.all_indexes.c.table_name.in_( 

3273 bindparam("all_objects") 

3274 ), 

3275 ) 

3276 .order_by( 

3277 dictionary.all_ind_columns.c.index_name, 

3278 dictionary.all_ind_columns.c.column_position, 

3279 ) 

3280 ) 

3281 

3282 @reflection.flexi_cache( 

3283 ("schema", InternalTraversal.dp_string), 

3284 ("dblink", InternalTraversal.dp_string), 

3285 ("all_objects", InternalTraversal.dp_string_list), 

3286 ) 

3287 def _get_indexes_rows(self, connection, schema, dblink, all_objects, **kw): 

3288 owner = self.denormalize_schema_name( 

3289 schema or self.default_schema_name 

3290 ) 

3291 

3292 query = self._index_query(owner) 

3293 

3294 pks = { 

3295 row_dict["constraint_name"] 

3296 for row_dict in self._get_all_constraint_rows( 

3297 connection, schema, dblink, all_objects, **kw 

3298 ) 

3299 if row_dict["constraint_type"] == "P" 

3300 } 

3301 

3302 # all_ind_expressions.column_expression is LONG 

3303 result = self._run_batches( 

3304 connection, 

3305 query, 

3306 dblink, 

3307 returns_long=True, 

3308 mappings=True, 

3309 all_objects=all_objects, 

3310 ) 

3311 

3312 return [ 

3313 row_dict 

3314 for row_dict in result 

3315 if row_dict["index_name"] not in pks 

3316 ] 

3317 

3318 @_handle_synonyms_decorator 

3319 def get_multi_indexes( 

3320 self, 

3321 connection, 

3322 *, 

3323 schema, 

3324 filter_names, 

3325 scope, 

3326 kind, 

3327 dblink=None, 

3328 **kw, 

3329 ): 

3330 """Supported kw arguments are: ``dblink`` to reflect via a db link; 

3331 ``oracle_resolve_synonyms`` to resolve names to synonyms 

3332 """ 

3333 all_objects = self._get_all_objects( 

3334 connection, schema, scope, kind, filter_names, dblink, **kw 

3335 ) 

3336 

3337 uniqueness = {"NONUNIQUE": False, "UNIQUE": True} 

3338 enabled = {"DISABLED": False, "ENABLED": True} 

3339 is_bitmap = {"BITMAP", "FUNCTION-BASED BITMAP"} 

3340 

3341 indexes = defaultdict(dict) 

3342 

3343 for row_dict in self._get_indexes_rows( 

3344 connection, schema, dblink, all_objects, **kw 

3345 ): 

3346 index_name = self.normalize_name(row_dict["index_name"]) 

3347 table_name = self.normalize_name(row_dict["table_name"]) 

3348 table_indexes = indexes[(schema, table_name)] 

3349 

3350 if index_name not in table_indexes: 

3351 table_indexes[index_name] = index_dict = { 

3352 "name": index_name, 

3353 "column_names": [], 

3354 "dialect_options": {}, 

3355 "unique": uniqueness.get(row_dict["uniqueness"], False), 

3356 } 

3357 do = index_dict["dialect_options"] 

3358 if row_dict["index_type"] in is_bitmap: 

3359 do["oracle_bitmap"] = True 

3360 if enabled.get(row_dict["compression"], False): 

3361 do["oracle_compress"] = row_dict["prefix_length"] 

3362 

3363 else: 

3364 index_dict = table_indexes[index_name] 

3365 

3366 expr = row_dict["column_expression"] 

3367 if expr is not None: 

3368 index_dict["column_names"].append(None) 

3369 if "expressions" in index_dict: 

3370 index_dict["expressions"].append(expr) 

3371 else: 

3372 index_dict["expressions"] = index_dict["column_names"][:-1] 

3373 index_dict["expressions"].append(expr) 

3374 

3375 if row_dict["descend"].lower() != "asc": 

3376 assert row_dict["descend"].lower() == "desc" 

3377 cs = index_dict.setdefault("column_sorting", {}) 

3378 cs[expr] = ("desc",) 

3379 else: 

3380 assert row_dict["descend"].lower() == "asc" 

3381 cn = self.normalize_name(row_dict["column_name"]) 

3382 index_dict["column_names"].append(cn) 

3383 if "expressions" in index_dict: 

3384 index_dict["expressions"].append(cn) 

3385 

3386 default = ReflectionDefaults.indexes 

3387 

3388 return ( 

3389 (key, list(indexes[key].values()) if key in indexes else default()) 

3390 for key in ( 

3391 (schema, self.normalize_name(obj_name)) 

3392 for obj_name in all_objects 

3393 ) 

3394 ) 

3395 

3396 @lru_cache() 

3397 def _constraint_query(self, owner): 

3398 local = dictionary.all_cons_columns.alias("local") 

3399 remote = dictionary.all_cons_columns.alias("remote") 

3400 return ( 

3401 select( 

3402 dictionary.all_constraints.c.table_name, 

3403 dictionary.all_constraints.c.constraint_type, 

3404 dictionary.all_constraints.c.constraint_name, 

3405 local.c.column_name.label("local_column"), 

3406 remote.c.table_name.label("remote_table"), 

3407 remote.c.column_name.label("remote_column"), 

3408 remote.c.owner.label("remote_owner"), 

3409 dictionary.all_constraints.c.search_condition, 

3410 dictionary.all_constraints.c.delete_rule, 

3411 ) 

3412 .select_from(dictionary.all_constraints) 

3413 .join( 

3414 local, 

3415 and_( 

3416 local.c.owner == dictionary.all_constraints.c.owner, 

3417 dictionary.all_constraints.c.constraint_name 

3418 == local.c.constraint_name, 

3419 ), 

3420 ) 

3421 .outerjoin( 

3422 remote, 

3423 and_( 

3424 dictionary.all_constraints.c.r_owner == remote.c.owner, 

3425 dictionary.all_constraints.c.r_constraint_name 

3426 == remote.c.constraint_name, 

3427 or_( 

3428 remote.c.position.is_(sql.null()), 

3429 local.c.position == remote.c.position, 

3430 ), 

3431 ), 

3432 ) 

3433 .where( 

3434 dictionary.all_constraints.c.owner == owner, 

3435 dictionary.all_constraints.c.table_name.in_( 

3436 bindparam("all_objects") 

3437 ), 

3438 dictionary.all_constraints.c.constraint_type.in_( 

3439 ("R", "P", "U", "C") 

3440 ), 

3441 ) 

3442 .order_by( 

3443 dictionary.all_constraints.c.constraint_name, local.c.position 

3444 ) 

3445 ) 

3446 

3447 @reflection.flexi_cache( 

3448 ("schema", InternalTraversal.dp_string), 

3449 ("dblink", InternalTraversal.dp_string), 

3450 ("all_objects", InternalTraversal.dp_string_list), 

3451 ) 

3452 def _get_all_constraint_rows( 

3453 self, connection, schema, dblink, all_objects, **kw 

3454 ): 

3455 owner = self.denormalize_schema_name( 

3456 schema or self.default_schema_name 

3457 ) 

3458 query = self._constraint_query(owner) 

3459 

3460 # since the result is cached a list must be created 

3461 values = list( 

3462 self._run_batches( 

3463 connection, 

3464 query, 

3465 dblink, 

3466 returns_long=False, 

3467 mappings=True, 

3468 all_objects=all_objects, 

3469 ) 

3470 ) 

3471 return values 

3472 

3473 @_handle_synonyms_decorator 

3474 def get_multi_pk_constraint( 

3475 self, 

3476 connection, 

3477 *, 

3478 scope, 

3479 schema, 

3480 filter_names, 

3481 kind, 

3482 dblink=None, 

3483 **kw, 

3484 ): 

3485 """Supported kw arguments are: ``dblink`` to reflect via a db link; 

3486 ``oracle_resolve_synonyms`` to resolve names to synonyms 

3487 """ 

3488 all_objects = self._get_all_objects( 

3489 connection, schema, scope, kind, filter_names, dblink, **kw 

3490 ) 

3491 

3492 primary_keys = defaultdict(dict) 

3493 default = ReflectionDefaults.pk_constraint 

3494 

3495 for row_dict in self._get_all_constraint_rows( 

3496 connection, schema, dblink, all_objects, **kw 

3497 ): 

3498 if row_dict["constraint_type"] != "P": 

3499 continue 

3500 table_name = self.normalize_name(row_dict["table_name"]) 

3501 constraint_name = self.normalize_name(row_dict["constraint_name"]) 

3502 column_name = self.normalize_name(row_dict["local_column"]) 

3503 

3504 table_pk = primary_keys[(schema, table_name)] 

3505 if not table_pk: 

3506 table_pk["name"] = constraint_name 

3507 table_pk["constrained_columns"] = [column_name] 

3508 else: 

3509 table_pk["constrained_columns"].append(column_name) 

3510 

3511 return ( 

3512 (key, primary_keys[key] if key in primary_keys else default()) 

3513 for key in ( 

3514 (schema, self.normalize_name(obj_name)) 

3515 for obj_name in all_objects 

3516 ) 

3517 ) 

3518 

3519 @_handle_synonyms_decorator 

3520 def get_multi_foreign_keys( 

3521 self, 

3522 connection, 

3523 *, 

3524 scope, 

3525 schema, 

3526 filter_names, 

3527 kind, 

3528 dblink=None, 

3529 **kw, 

3530 ): 

3531 """Supported kw arguments are: ``dblink`` to reflect via a db link; 

3532 ``oracle_resolve_synonyms`` to resolve names to synonyms 

3533 """ 

3534 all_objects = self._get_all_objects( 

3535 connection, schema, scope, kind, filter_names, dblink, **kw 

3536 ) 

3537 

3538 resolve_synonyms = kw.get("oracle_resolve_synonyms", False) 

3539 

3540 owner = self.denormalize_schema_name( 

3541 schema or self.default_schema_name 

3542 ) 

3543 

3544 all_remote_owners = set() 

3545 fkeys = defaultdict(dict) 

3546 

3547 for row_dict in self._get_all_constraint_rows( 

3548 connection, schema, dblink, all_objects, **kw 

3549 ): 

3550 if row_dict["constraint_type"] != "R": 

3551 continue 

3552 

3553 table_name = self.normalize_name(row_dict["table_name"]) 

3554 constraint_name = self.normalize_name(row_dict["constraint_name"]) 

3555 table_fkey = fkeys[(schema, table_name)] 

3556 

3557 assert constraint_name is not None 

3558 

3559 local_column = self.normalize_name(row_dict["local_column"]) 

3560 remote_table = self.normalize_name(row_dict["remote_table"]) 

3561 remote_column = self.normalize_name(row_dict["remote_column"]) 

3562 remote_owner_orig = row_dict["remote_owner"] 

3563 remote_owner = self.normalize_name(remote_owner_orig) 

3564 if remote_owner_orig is not None: 

3565 all_remote_owners.add(remote_owner_orig) 

3566 

3567 if remote_table is None: 

3568 # ticket 363 

3569 if dblink and not dblink.startswith("@"): 

3570 dblink = f"@{dblink}" 

3571 util.warn( 

3572 "Got 'None' querying 'table_name' from " 

3573 f"all_cons_columns{dblink or ''} - does the user have " 

3574 "proper rights to the table?" 

3575 ) 

3576 continue 

3577 

3578 if constraint_name not in table_fkey: 

3579 table_fkey[constraint_name] = fkey = { 

3580 "name": constraint_name, 

3581 "constrained_columns": [], 

3582 "referred_schema": None, 

3583 "referred_table": remote_table, 

3584 "referred_columns": [], 

3585 "options": {}, 

3586 } 

3587 

3588 if resolve_synonyms: 

3589 # will be removed below 

3590 fkey["_ref_schema"] = remote_owner 

3591 

3592 if schema is not None or remote_owner_orig != owner: 

3593 fkey["referred_schema"] = remote_owner 

3594 

3595 delete_rule = row_dict["delete_rule"] 

3596 if delete_rule != "NO ACTION": 

3597 fkey["options"]["ondelete"] = delete_rule 

3598 

3599 else: 

3600 fkey = table_fkey[constraint_name] 

3601 

3602 fkey["constrained_columns"].append(local_column) 

3603 fkey["referred_columns"].append(remote_column) 

3604 

3605 if resolve_synonyms and all_remote_owners: 

3606 query = select( 

3607 dictionary.all_synonyms.c.owner, 

3608 dictionary.all_synonyms.c.table_name, 

3609 dictionary.all_synonyms.c.table_owner, 

3610 dictionary.all_synonyms.c.synonym_name, 

3611 ).where(dictionary.all_synonyms.c.owner.in_(all_remote_owners)) 

3612 

3613 result = self._execute_reflection( 

3614 connection, query, dblink, returns_long=False 

3615 ).mappings() 

3616 

3617 remote_owners_lut = {} 

3618 for row in result: 

3619 synonym_owner = self.normalize_name(row["owner"]) 

3620 table_name = self.normalize_name(row["table_name"]) 

3621 

3622 remote_owners_lut[(synonym_owner, table_name)] = ( 

3623 row["table_owner"], 

3624 row["synonym_name"], 

3625 ) 

3626 

3627 empty = (None, None) 

3628 for table_fkeys in fkeys.values(): 

3629 for table_fkey in table_fkeys.values(): 

3630 key = ( 

3631 table_fkey.pop("_ref_schema"), 

3632 table_fkey["referred_table"], 

3633 ) 

3634 remote_owner, syn_name = remote_owners_lut.get(key, empty) 

3635 if syn_name: 

3636 sn = self.normalize_name(syn_name) 

3637 table_fkey["referred_table"] = sn 

3638 if schema is not None or remote_owner != owner: 

3639 ro = self.normalize_name(remote_owner) 

3640 table_fkey["referred_schema"] = ro 

3641 else: 

3642 table_fkey["referred_schema"] = None 

3643 default = ReflectionDefaults.foreign_keys 

3644 

3645 return ( 

3646 (key, list(fkeys[key].values()) if key in fkeys else default()) 

3647 for key in ( 

3648 (schema, self.normalize_name(obj_name)) 

3649 for obj_name in all_objects 

3650 ) 

3651 ) 

3652 

3653 @_handle_synonyms_decorator 

3654 def get_multi_unique_constraints( 

3655 self, 

3656 connection, 

3657 *, 

3658 scope, 

3659 schema, 

3660 filter_names, 

3661 kind, 

3662 dblink=None, 

3663 **kw, 

3664 ): 

3665 """Supported kw arguments are: ``dblink`` to reflect via a db link; 

3666 ``oracle_resolve_synonyms`` to resolve names to synonyms 

3667 """ 

3668 all_objects = self._get_all_objects( 

3669 connection, schema, scope, kind, filter_names, dblink, **kw 

3670 ) 

3671 

3672 unique_cons = defaultdict(dict) 

3673 

3674 index_names = { 

3675 row_dict["index_name"] 

3676 for row_dict in self._get_indexes_rows( 

3677 connection, schema, dblink, all_objects, **kw 

3678 ) 

3679 } 

3680 

3681 for row_dict in self._get_all_constraint_rows( 

3682 connection, schema, dblink, all_objects, **kw 

3683 ): 

3684 if row_dict["constraint_type"] != "U": 

3685 continue 

3686 table_name = self.normalize_name(row_dict["table_name"]) 

3687 constraint_name_orig = row_dict["constraint_name"] 

3688 constraint_name = self.normalize_name(constraint_name_orig) 

3689 column_name = self.normalize_name(row_dict["local_column"]) 

3690 table_uc = unique_cons[(schema, table_name)] 

3691 

3692 assert constraint_name is not None 

3693 

3694 if constraint_name not in table_uc: 

3695 table_uc[constraint_name] = uc = { 

3696 "name": constraint_name, 

3697 "column_names": [], 

3698 "duplicates_index": ( 

3699 constraint_name 

3700 if constraint_name_orig in index_names 

3701 else None 

3702 ), 

3703 } 

3704 else: 

3705 uc = table_uc[constraint_name] 

3706 

3707 uc["column_names"].append(column_name) 

3708 

3709 default = ReflectionDefaults.unique_constraints 

3710 

3711 return ( 

3712 ( 

3713 key, 

3714 ( 

3715 list(unique_cons[key].values()) 

3716 if key in unique_cons 

3717 else default() 

3718 ), 

3719 ) 

3720 for key in ( 

3721 (schema, self.normalize_name(obj_name)) 

3722 for obj_name in all_objects 

3723 ) 

3724 ) 

3725 

3726 @reflection.cache 

3727 def get_view_definition( 

3728 self, 

3729 connection, 

3730 view_name, 

3731 schema=None, 

3732 dblink=None, 

3733 **kw, 

3734 ): 

3735 """Supported kw arguments are: ``dblink`` to reflect via a db link; 

3736 ``oracle_resolve_synonyms`` to resolve names to synonyms 

3737 """ 

3738 if kw.get("oracle_resolve_synonyms", False): 

3739 synonyms = self._get_synonyms( 

3740 connection, schema, filter_names=[view_name], dblink=dblink 

3741 ) 

3742 if synonyms: 

3743 assert len(synonyms) == 1 

3744 row_dict = synonyms[0] 

3745 dblink = self.normalize_name(row_dict["db_link"]) 

3746 schema = row_dict["table_owner"] 

3747 view_name = row_dict["table_name"] 

3748 

3749 name = self.denormalize_name(view_name) 

3750 owner = self.denormalize_schema_name( 

3751 schema or self.default_schema_name 

3752 ) 

3753 query = ( 

3754 select(dictionary.all_views.c.text) 

3755 .where( 

3756 dictionary.all_views.c.view_name == name, 

3757 dictionary.all_views.c.owner == owner, 

3758 ) 

3759 .union_all( 

3760 select(dictionary.all_mviews.c.query).where( 

3761 dictionary.all_mviews.c.mview_name == name, 

3762 dictionary.all_mviews.c.owner == owner, 

3763 ) 

3764 ) 

3765 ) 

3766 

3767 rp = self._execute_reflection( 

3768 connection, query, dblink, returns_long=False 

3769 ).scalar() 

3770 if rp is None: 

3771 raise exc.NoSuchTableError( 

3772 f"{schema}.{view_name}" if schema else view_name 

3773 ) 

3774 else: 

3775 return rp 

3776 

3777 @_handle_synonyms_decorator 

3778 def get_multi_check_constraints( 

3779 self, 

3780 connection, 

3781 *, 

3782 schema, 

3783 filter_names, 

3784 dblink=None, 

3785 scope, 

3786 kind, 

3787 include_all=False, 

3788 **kw, 

3789 ): 

3790 """Supported kw arguments are: ``dblink`` to reflect via a db link; 

3791 ``oracle_resolve_synonyms`` to resolve names to synonyms 

3792 """ 

3793 all_objects = self._get_all_objects( 

3794 connection, schema, scope, kind, filter_names, dblink, **kw 

3795 ) 

3796 

3797 not_null = re.compile(r"..+?. IS NOT NULL$") 

3798 

3799 check_constraints = defaultdict(list) 

3800 

3801 for row_dict in self._get_all_constraint_rows( 

3802 connection, schema, dblink, all_objects, **kw 

3803 ): 

3804 if row_dict["constraint_type"] != "C": 

3805 continue 

3806 table_name = self.normalize_name(row_dict["table_name"]) 

3807 constraint_name = self.normalize_name(row_dict["constraint_name"]) 

3808 search_condition = row_dict["search_condition"] 

3809 

3810 table_checks = check_constraints[(schema, table_name)] 

3811 if constraint_name is not None and ( 

3812 include_all or not not_null.match(search_condition) 

3813 ): 

3814 table_checks.append( 

3815 {"name": constraint_name, "sqltext": search_condition} 

3816 ) 

3817 

3818 default = ReflectionDefaults.check_constraints 

3819 

3820 return ( 

3821 ( 

3822 key, 

3823 ( 

3824 check_constraints[key] 

3825 if key in check_constraints 

3826 else default() 

3827 ), 

3828 ) 

3829 for key in ( 

3830 (schema, self.normalize_name(obj_name)) 

3831 for obj_name in all_objects 

3832 ) 

3833 ) 

3834 

3835 def _list_dblinks(self, connection, dblink=None): 

3836 query = select(dictionary.all_db_links.c.db_link) 

3837 links = self._execute_reflection( 

3838 connection, query, dblink, returns_long=False 

3839 ).scalars() 

3840 return [self.normalize_name(link) for link in links] 

3841 

3842 

3843class _OuterJoinColumn(sql.ClauseElement): 

3844 __visit_name__ = "outer_join_column" 

3845 

3846 def __init__(self, column): 

3847 self.column = column