3587 lines
118 KiB
Python
3587 lines
118 KiB
Python
# postgresql/base.py
|
|
# Copyright (C) 2005-2019 the SQLAlchemy authors and contributors
|
|
# <see AUTHORS file>
|
|
#
|
|
# This module is part of SQLAlchemy and is released under
|
|
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
|
|
|
r"""
|
|
.. dialect:: postgresql
|
|
:name: PostgreSQL
|
|
|
|
.. _postgresql_sequences:
|
|
|
|
Sequences/SERIAL/IDENTITY
|
|
-------------------------
|
|
|
|
PostgreSQL supports sequences, and SQLAlchemy uses these as the default means
|
|
of creating new primary key values for integer-based primary key columns. When
|
|
creating tables, SQLAlchemy will issue the ``SERIAL`` datatype for
|
|
integer-based primary key columns, which generates a sequence and server side
|
|
default corresponding to the column.
|
|
|
|
To specify a specific named sequence to be used for primary key generation,
|
|
use the :func:`~sqlalchemy.schema.Sequence` construct::
|
|
|
|
Table('sometable', metadata,
|
|
Column('id', Integer, Sequence('some_id_seq'), primary_key=True)
|
|
)
|
|
|
|
When SQLAlchemy issues a single INSERT statement, to fulfill the contract of
|
|
having the "last insert identifier" available, a RETURNING clause is added to
|
|
the INSERT statement which specifies the primary key columns should be
|
|
returned after the statement completes. The RETURNING functionality only takes
|
|
place if PostgreSQL 8.2 or later is in use. As a fallback approach, the
|
|
sequence, whether specified explicitly or implicitly via ``SERIAL``, is
|
|
executed independently beforehand, the returned value to be used in the
|
|
subsequent insert. Note that when an
|
|
:func:`~sqlalchemy.sql.expression.insert()` construct is executed using
|
|
"executemany" semantics, the "last inserted identifier" functionality does not
|
|
apply; no RETURNING clause is emitted nor is the sequence pre-executed in this
|
|
case.
|
|
|
|
To force the usage of RETURNING by default off, specify the flag
|
|
``implicit_returning=False`` to :func:`.create_engine`.
|
|
|
|
PostgreSQL 10 IDENTITY columns
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
PostgreSQL 10 has a new IDENTITY feature that supersedes the use of SERIAL.
|
|
Built-in support for rendering of IDENTITY is not available yet, however the
|
|
following compilation hook may be used to replace occurrences of SERIAL with
|
|
IDENTITY::
|
|
|
|
from sqlalchemy.schema import CreateColumn
|
|
from sqlalchemy.ext.compiler import compiles
|
|
|
|
|
|
@compiles(CreateColumn, 'postgresql')
|
|
def use_identity(element, compiler, **kw):
|
|
text = compiler.visit_create_column(element, **kw)
|
|
text = text.replace("SERIAL", "INT GENERATED BY DEFAULT AS IDENTITY")
|
|
return text
|
|
|
|
Using the above, a table such as::
|
|
|
|
t = Table(
|
|
't', m,
|
|
Column('id', Integer, primary_key=True),
|
|
Column('data', String)
|
|
)
|
|
|
|
Will generate on the backing database as::
|
|
|
|
CREATE TABLE t (
|
|
id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
|
data VARCHAR,
|
|
PRIMARY KEY (id)
|
|
)
|
|
|
|
.. _postgresql_isolation_level:
|
|
|
|
Transaction Isolation Level
|
|
---------------------------
|
|
|
|
All PostgreSQL dialects support setting of transaction isolation level
|
|
both via a dialect-specific parameter
|
|
:paramref:`.create_engine.isolation_level` accepted by :func:`.create_engine`,
|
|
as well as the :paramref:`.Connection.execution_options.isolation_level`
|
|
argument as passed to :meth:`.Connection.execution_options`.
|
|
When using a non-psycopg2 dialect, this feature works by issuing the command
|
|
``SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL <level>`` for
|
|
each new connection. For the special AUTOCOMMIT isolation level,
|
|
DBAPI-specific techniques are used.
|
|
|
|
To set isolation level using :func:`.create_engine`::
|
|
|
|
engine = create_engine(
|
|
"postgresql+pg8000://scott:tiger@localhost/test",
|
|
isolation_level="READ UNCOMMITTED"
|
|
)
|
|
|
|
To set using per-connection execution options::
|
|
|
|
connection = engine.connect()
|
|
connection = connection.execution_options(
|
|
isolation_level="READ COMMITTED"
|
|
)
|
|
|
|
Valid values for ``isolation_level`` include:
|
|
|
|
* ``READ COMMITTED``
|
|
* ``READ UNCOMMITTED``
|
|
* ``REPEATABLE READ``
|
|
* ``SERIALIZABLE``
|
|
* ``AUTOCOMMIT`` - on psycopg2 / pg8000 only
|
|
|
|
.. seealso::
|
|
|
|
:ref:`psycopg2_isolation_level`
|
|
|
|
:ref:`pg8000_isolation_level`
|
|
|
|
.. _postgresql_schema_reflection:
|
|
|
|
Remote-Schema Table Introspection and PostgreSQL search_path
|
|
------------------------------------------------------------
|
|
|
|
**TL;DR;**: keep the ``search_path`` variable set to its default of ``public``,
|
|
name schemas **other** than ``public`` explicitly within ``Table`` definitions.
|
|
|
|
The PostgreSQL dialect can reflect tables from any schema. The
|
|
:paramref:`.Table.schema` argument, or alternatively the
|
|
:paramref:`.MetaData.reflect.schema` argument determines which schema will
|
|
be searched for the table or tables. The reflected :class:`.Table` objects
|
|
will in all cases retain this ``.schema`` attribute as was specified.
|
|
However, with regards to tables which these :class:`.Table` objects refer to
|
|
via foreign key constraint, a decision must be made as to how the ``.schema``
|
|
is represented in those remote tables, in the case where that remote
|
|
schema name is also a member of the current
|
|
`PostgreSQL search path
|
|
<http://www.postgresql.org/docs/current/static/ddl-schemas.html#DDL-SCHEMAS-PATH>`_.
|
|
|
|
By default, the PostgreSQL dialect mimics the behavior encouraged by
|
|
PostgreSQL's own ``pg_get_constraintdef()`` builtin procedure. This function
|
|
returns a sample definition for a particular foreign key constraint,
|
|
omitting the referenced schema name from that definition when the name is
|
|
also in the PostgreSQL schema search path. The interaction below
|
|
illustrates this behavior::
|
|
|
|
test=> CREATE TABLE test_schema.referred(id INTEGER PRIMARY KEY);
|
|
CREATE TABLE
|
|
test=> CREATE TABLE referring(
|
|
test(> id INTEGER PRIMARY KEY,
|
|
test(> referred_id INTEGER REFERENCES test_schema.referred(id));
|
|
CREATE TABLE
|
|
test=> SET search_path TO public, test_schema;
|
|
test=> SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM
|
|
test-> pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n
|
|
test-> ON n.oid = c.relnamespace
|
|
test-> JOIN pg_catalog.pg_constraint r ON c.oid = r.conrelid
|
|
test-> WHERE c.relname='referring' AND r.contype = 'f'
|
|
test-> ;
|
|
pg_get_constraintdef
|
|
---------------------------------------------------
|
|
FOREIGN KEY (referred_id) REFERENCES referred(id)
|
|
(1 row)
|
|
|
|
Above, we created a table ``referred`` as a member of the remote schema
|
|
``test_schema``, however when we added ``test_schema`` to the
|
|
PG ``search_path`` and then asked ``pg_get_constraintdef()`` for the
|
|
``FOREIGN KEY`` syntax, ``test_schema`` was not included in the output of
|
|
the function.
|
|
|
|
On the other hand, if we set the search path back to the typical default
|
|
of ``public``::
|
|
|
|
test=> SET search_path TO public;
|
|
SET
|
|
|
|
The same query against ``pg_get_constraintdef()`` now returns the fully
|
|
schema-qualified name for us::
|
|
|
|
test=> SELECT pg_catalog.pg_get_constraintdef(r.oid, true) FROM
|
|
test-> pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n
|
|
test-> ON n.oid = c.relnamespace
|
|
test-> JOIN pg_catalog.pg_constraint r ON c.oid = r.conrelid
|
|
test-> WHERE c.relname='referring' AND r.contype = 'f';
|
|
pg_get_constraintdef
|
|
---------------------------------------------------------------
|
|
FOREIGN KEY (referred_id) REFERENCES test_schema.referred(id)
|
|
(1 row)
|
|
|
|
SQLAlchemy will by default use the return value of ``pg_get_constraintdef()``
|
|
in order to determine the remote schema name. That is, if our ``search_path``
|
|
were set to include ``test_schema``, and we invoked a table
|
|
reflection process as follows::
|
|
|
|
>>> from sqlalchemy import Table, MetaData, create_engine
|
|
>>> engine = create_engine("postgresql://scott:tiger@localhost/test")
|
|
>>> with engine.connect() as conn:
|
|
... conn.execute("SET search_path TO test_schema, public")
|
|
... meta = MetaData()
|
|
... referring = Table('referring', meta,
|
|
... autoload=True, autoload_with=conn)
|
|
...
|
|
<sqlalchemy.engine.result.ResultProxy object at 0x101612ed0>
|
|
|
|
The above process would deliver to the :attr:`.MetaData.tables` collection
|
|
``referred`` table named **without** the schema::
|
|
|
|
>>> meta.tables['referred'].schema is None
|
|
True
|
|
|
|
To alter the behavior of reflection such that the referred schema is
|
|
maintained regardless of the ``search_path`` setting, use the
|
|
``postgresql_ignore_search_path`` option, which can be specified as a
|
|
dialect-specific argument to both :class:`.Table` as well as
|
|
:meth:`.MetaData.reflect`::
|
|
|
|
>>> with engine.connect() as conn:
|
|
... conn.execute("SET search_path TO test_schema, public")
|
|
... meta = MetaData()
|
|
... referring = Table('referring', meta, autoload=True,
|
|
... autoload_with=conn,
|
|
... postgresql_ignore_search_path=True)
|
|
...
|
|
<sqlalchemy.engine.result.ResultProxy object at 0x1016126d0>
|
|
|
|
We will now have ``test_schema.referred`` stored as schema-qualified::
|
|
|
|
>>> meta.tables['test_schema.referred'].schema
|
|
'test_schema'
|
|
|
|
.. sidebar:: Best Practices for PostgreSQL Schema reflection
|
|
|
|
The description of PostgreSQL schema reflection behavior is complex, and
|
|
is the product of many years of dealing with widely varied use cases and
|
|
user preferences. But in fact, there's no need to understand any of it if
|
|
you just stick to the simplest use pattern: leave the ``search_path`` set
|
|
to its default of ``public`` only, never refer to the name ``public`` as
|
|
an explicit schema name otherwise, and refer to all other schema names
|
|
explicitly when building up a :class:`.Table` object. The options
|
|
described here are only for those users who can't, or prefer not to, stay
|
|
within these guidelines.
|
|
|
|
Note that **in all cases**, the "default" schema is always reflected as
|
|
``None``. The "default" schema on PostgreSQL is that which is returned by the
|
|
PostgreSQL ``current_schema()`` function. On a typical PostgreSQL
|
|
installation, this is the name ``public``. So a table that refers to another
|
|
which is in the ``public`` (i.e. default) schema will always have the
|
|
``.schema`` attribute set to ``None``.
|
|
|
|
.. versionadded:: 0.9.2 Added the ``postgresql_ignore_search_path``
|
|
dialect-level option accepted by :class:`.Table` and
|
|
:meth:`.MetaData.reflect`.
|
|
|
|
|
|
.. seealso::
|
|
|
|
`The Schema Search Path
|
|
<http://www.postgresql.org/docs/9.0/static/ddl-schemas.html#DDL-SCHEMAS-PATH>`_
|
|
- on the PostgreSQL website.
|
|
|
|
INSERT/UPDATE...RETURNING
|
|
-------------------------
|
|
|
|
The dialect supports PG 8.2's ``INSERT..RETURNING``, ``UPDATE..RETURNING`` and
|
|
``DELETE..RETURNING`` syntaxes. ``INSERT..RETURNING`` is used by default
|
|
for single-row INSERT statements in order to fetch newly generated
|
|
primary key identifiers. To specify an explicit ``RETURNING`` clause,
|
|
use the :meth:`._UpdateBase.returning` method on a per-statement basis::
|
|
|
|
# INSERT..RETURNING
|
|
result = table.insert().returning(table.c.col1, table.c.col2).\
|
|
values(name='foo')
|
|
print result.fetchall()
|
|
|
|
# UPDATE..RETURNING
|
|
result = table.update().returning(table.c.col1, table.c.col2).\
|
|
where(table.c.name=='foo').values(name='bar')
|
|
print result.fetchall()
|
|
|
|
# DELETE..RETURNING
|
|
result = table.delete().returning(table.c.col1, table.c.col2).\
|
|
where(table.c.name=='foo')
|
|
print result.fetchall()
|
|
|
|
.. _postgresql_insert_on_conflict:
|
|
|
|
INSERT...ON CONFLICT (Upsert)
|
|
------------------------------
|
|
|
|
Starting with version 9.5, PostgreSQL allows "upserts" (update or insert) of
|
|
rows into a table via the ``ON CONFLICT`` clause of the ``INSERT`` statement. A
|
|
candidate row will only be inserted if that row does not violate any unique
|
|
constraints. In the case of a unique constraint violation, a secondary action
|
|
can occur which can be either "DO UPDATE", indicating that the data in the
|
|
target row should be updated, or "DO NOTHING", which indicates to silently skip
|
|
this row.
|
|
|
|
Conflicts are determined using existing unique constraints and indexes. These
|
|
constraints may be identified either using their name as stated in DDL,
|
|
or they may be *inferred* by stating the columns and conditions that comprise
|
|
the indexes.
|
|
|
|
SQLAlchemy provides ``ON CONFLICT`` support via the PostgreSQL-specific
|
|
:func:`.postgresql.dml.insert()` function, which provides
|
|
the generative methods :meth:`~.postgresql.dml.Insert.on_conflict_do_update`
|
|
and :meth:`~.postgresql.dml.Insert.on_conflict_do_nothing`::
|
|
|
|
from sqlalchemy.dialects.postgresql import insert
|
|
|
|
insert_stmt = insert(my_table).values(
|
|
id='some_existing_id',
|
|
data='inserted value')
|
|
|
|
do_nothing_stmt = insert_stmt.on_conflict_do_nothing(
|
|
index_elements=['id']
|
|
)
|
|
|
|
conn.execute(do_nothing_stmt)
|
|
|
|
do_update_stmt = insert_stmt.on_conflict_do_update(
|
|
constraint='pk_my_table',
|
|
set_=dict(data='updated value')
|
|
)
|
|
|
|
conn.execute(do_update_stmt)
|
|
|
|
Both methods supply the "target" of the conflict using either the
|
|
named constraint or by column inference:
|
|
|
|
* The :paramref:`.Insert.on_conflict_do_update.index_elements` argument
|
|
specifies a sequence containing string column names, :class:`.Column`
|
|
objects, and/or SQL expression elements, which would identify a unique
|
|
index::
|
|
|
|
do_update_stmt = insert_stmt.on_conflict_do_update(
|
|
index_elements=['id'],
|
|
set_=dict(data='updated value')
|
|
)
|
|
|
|
do_update_stmt = insert_stmt.on_conflict_do_update(
|
|
index_elements=[my_table.c.id],
|
|
set_=dict(data='updated value')
|
|
)
|
|
|
|
* When using :paramref:`.Insert.on_conflict_do_update.index_elements` to
|
|
infer an index, a partial index can be inferred by also specifying the
|
|
use the :paramref:`.Insert.on_conflict_do_update.index_where` parameter::
|
|
|
|
from sqlalchemy.dialects.postgresql import insert
|
|
|
|
stmt = insert(my_table).values(user_email='a@b.com', data='inserted data')
|
|
stmt = stmt.on_conflict_do_update(
|
|
index_elements=[my_table.c.user_email],
|
|
index_where=my_table.c.user_email.like('%@gmail.com'),
|
|
set_=dict(data=stmt.excluded.data)
|
|
)
|
|
conn.execute(stmt)
|
|
|
|
* The :paramref:`.Insert.on_conflict_do_update.constraint` argument is
|
|
used to specify an index directly rather than inferring it. This can be
|
|
the name of a UNIQUE constraint, a PRIMARY KEY constraint, or an INDEX::
|
|
|
|
do_update_stmt = insert_stmt.on_conflict_do_update(
|
|
constraint='my_table_idx_1',
|
|
set_=dict(data='updated value')
|
|
)
|
|
|
|
do_update_stmt = insert_stmt.on_conflict_do_update(
|
|
constraint='my_table_pk',
|
|
set_=dict(data='updated value')
|
|
)
|
|
|
|
* The :paramref:`.Insert.on_conflict_do_update.constraint` argument may
|
|
also refer to a SQLAlchemy construct representing a constraint,
|
|
e.g. :class:`.UniqueConstraint`, :class:`.PrimaryKeyConstraint`,
|
|
:class:`.Index`, or :class:`.ExcludeConstraint`. In this use,
|
|
if the constraint has a name, it is used directly. Otherwise, if the
|
|
constraint is unnamed, then inference will be used, where the expressions
|
|
and optional WHERE clause of the constraint will be spelled out in the
|
|
construct. This use is especially convenient
|
|
to refer to the named or unnamed primary key of a :class:`.Table` using the
|
|
:attr:`.Table.primary_key` attribute::
|
|
|
|
do_update_stmt = insert_stmt.on_conflict_do_update(
|
|
constraint=my_table.primary_key,
|
|
set_=dict(data='updated value')
|
|
)
|
|
|
|
``ON CONFLICT...DO UPDATE`` is used to perform an update of the already
|
|
existing row, using any combination of new values as well as values
|
|
from the proposed insertion. These values are specified using the
|
|
:paramref:`.Insert.on_conflict_do_update.set_` parameter. This
|
|
parameter accepts a dictionary which consists of direct values
|
|
for UPDATE::
|
|
|
|
from sqlalchemy.dialects.postgresql import insert
|
|
|
|
stmt = insert(my_table).values(id='some_id', data='inserted value')
|
|
do_update_stmt = stmt.on_conflict_do_update(
|
|
index_elements=['id'],
|
|
set_=dict(data='updated value')
|
|
)
|
|
conn.execute(do_update_stmt)
|
|
|
|
.. warning::
|
|
|
|
The :meth:`.Insert.on_conflict_do_update` method does **not** take into
|
|
account Python-side default UPDATE values or generation functions, e.g.
|
|
those specified using :paramref:`.Column.onupdate`.
|
|
These values will not be exercised for an ON CONFLICT style of UPDATE,
|
|
unless they are manually specified in the
|
|
:paramref:`.Insert.on_conflict_do_update.set_` dictionary.
|
|
|
|
In order to refer to the proposed insertion row, the special alias
|
|
:attr:`~.postgresql.dml.Insert.excluded` is available as an attribute on
|
|
the :class:`.postgresql.dml.Insert` object; this object is a
|
|
:class:`.ColumnCollection` which alias contains all columns of the target
|
|
table::
|
|
|
|
from sqlalchemy.dialects.postgresql import insert
|
|
|
|
stmt = insert(my_table).values(
|
|
id='some_id',
|
|
data='inserted value',
|
|
author='jlh')
|
|
do_update_stmt = stmt.on_conflict_do_update(
|
|
index_elements=['id'],
|
|
set_=dict(data='updated value', author=stmt.excluded.author)
|
|
)
|
|
conn.execute(do_update_stmt)
|
|
|
|
The :meth:`.Insert.on_conflict_do_update` method also accepts
|
|
a WHERE clause using the :paramref:`.Insert.on_conflict_do_update.where`
|
|
parameter, which will limit those rows which receive an UPDATE::
|
|
|
|
from sqlalchemy.dialects.postgresql import insert
|
|
|
|
stmt = insert(my_table).values(
|
|
id='some_id',
|
|
data='inserted value',
|
|
author='jlh')
|
|
on_update_stmt = stmt.on_conflict_do_update(
|
|
index_elements=['id'],
|
|
set_=dict(data='updated value', author=stmt.excluded.author)
|
|
where=(my_table.c.status == 2)
|
|
)
|
|
conn.execute(on_update_stmt)
|
|
|
|
``ON CONFLICT`` may also be used to skip inserting a row entirely
|
|
if any conflict with a unique or exclusion constraint occurs; below
|
|
this is illustrated using the
|
|
:meth:`~.postgresql.dml.Insert.on_conflict_do_nothing` method::
|
|
|
|
from sqlalchemy.dialects.postgresql import insert
|
|
|
|
stmt = insert(my_table).values(id='some_id', data='inserted value')
|
|
stmt = stmt.on_conflict_do_nothing(index_elements=['id'])
|
|
conn.execute(stmt)
|
|
|
|
If ``DO NOTHING`` is used without specifying any columns or constraint,
|
|
it has the effect of skipping the INSERT for any unique or exclusion
|
|
constraint violation which occurs::
|
|
|
|
from sqlalchemy.dialects.postgresql import insert
|
|
|
|
stmt = insert(my_table).values(id='some_id', data='inserted value')
|
|
stmt = stmt.on_conflict_do_nothing()
|
|
conn.execute(stmt)
|
|
|
|
.. versionadded:: 1.1 Added support for PostgreSQL ON CONFLICT clauses
|
|
|
|
.. seealso::
|
|
|
|
`INSERT .. ON CONFLICT
|
|
<http://www.postgresql.org/docs/current/static/sql-insert.html#SQL-ON-CONFLICT>`_
|
|
- in the PostgreSQL documentation.
|
|
|
|
.. _postgresql_match:
|
|
|
|
Full Text Search
|
|
----------------
|
|
|
|
SQLAlchemy makes available the PostgreSQL ``@@`` operator via the
|
|
:meth:`.ColumnElement.match` method on any textual column expression.
|
|
On a PostgreSQL dialect, an expression like the following::
|
|
|
|
select([sometable.c.text.match("search string")])
|
|
|
|
will emit to the database::
|
|
|
|
SELECT text @@ to_tsquery('search string') FROM table
|
|
|
|
The PostgreSQL text search functions such as ``to_tsquery()``
|
|
and ``to_tsvector()`` are available
|
|
explicitly using the standard :data:`.func` construct. For example::
|
|
|
|
select([
|
|
func.to_tsvector('fat cats ate rats').match('cat & rat')
|
|
])
|
|
|
|
Emits the equivalent of::
|
|
|
|
SELECT to_tsvector('fat cats ate rats') @@ to_tsquery('cat & rat')
|
|
|
|
The :class:`.postgresql.TSVECTOR` type can provide for explicit CAST::
|
|
|
|
from sqlalchemy.dialects.postgresql import TSVECTOR
|
|
from sqlalchemy import select, cast
|
|
select([cast("some text", TSVECTOR)])
|
|
|
|
produces a statement equivalent to::
|
|
|
|
SELECT CAST('some text' AS TSVECTOR) AS anon_1
|
|
|
|
Full Text Searches in PostgreSQL are influenced by a combination of: the
|
|
PostgreSQL setting of ``default_text_search_config``, the ``regconfig`` used
|
|
to build the GIN/GiST indexes, and the ``regconfig`` optionally passed in
|
|
during a query.
|
|
|
|
When performing a Full Text Search against a column that has a GIN or
|
|
GiST index that is already pre-computed (which is common on full text
|
|
searches) one may need to explicitly pass in a particular PostgreSQL
|
|
``regconfig`` value to ensure the query-planner utilizes the index and does
|
|
not re-compute the column on demand.
|
|
|
|
In order to provide for this explicit query planning, or to use different
|
|
search strategies, the ``match`` method accepts a ``postgresql_regconfig``
|
|
keyword argument::
|
|
|
|
select([mytable.c.id]).where(
|
|
mytable.c.title.match('somestring', postgresql_regconfig='english')
|
|
)
|
|
|
|
Emits the equivalent of::
|
|
|
|
SELECT mytable.id FROM mytable
|
|
WHERE mytable.title @@ to_tsquery('english', 'somestring')
|
|
|
|
One can also specifically pass in a `'regconfig'` value to the
|
|
``to_tsvector()`` command as the initial argument::
|
|
|
|
select([mytable.c.id]).where(
|
|
func.to_tsvector('english', mytable.c.title )\
|
|
.match('somestring', postgresql_regconfig='english')
|
|
)
|
|
|
|
produces a statement equivalent to::
|
|
|
|
SELECT mytable.id FROM mytable
|
|
WHERE to_tsvector('english', mytable.title) @@
|
|
to_tsquery('english', 'somestring')
|
|
|
|
It is recommended that you use the ``EXPLAIN ANALYZE...`` tool from
|
|
PostgreSQL to ensure that you are generating queries with SQLAlchemy that
|
|
take full advantage of any indexes you may have created for full text search.
|
|
|
|
FROM ONLY ...
|
|
------------------------
|
|
|
|
The dialect supports PostgreSQL's ONLY keyword for targeting only a particular
|
|
table in an inheritance hierarchy. This can be used to produce the
|
|
``SELECT ... FROM ONLY``, ``UPDATE ONLY ...``, and ``DELETE FROM ONLY ...``
|
|
syntaxes. It uses SQLAlchemy's hints mechanism::
|
|
|
|
# SELECT ... FROM ONLY ...
|
|
result = table.select().with_hint(table, 'ONLY', 'postgresql')
|
|
print result.fetchall()
|
|
|
|
# UPDATE ONLY ...
|
|
table.update(values=dict(foo='bar')).with_hint('ONLY',
|
|
dialect_name='postgresql')
|
|
|
|
# DELETE FROM ONLY ...
|
|
table.delete().with_hint('ONLY', dialect_name='postgresql')
|
|
|
|
|
|
.. _postgresql_indexes:
|
|
|
|
PostgreSQL-Specific Index Options
|
|
---------------------------------
|
|
|
|
Several extensions to the :class:`.Index` construct are available, specific
|
|
to the PostgreSQL dialect.
|
|
|
|
.. _postgresql_partial_indexes:
|
|
|
|
Partial Indexes
|
|
^^^^^^^^^^^^^^^
|
|
|
|
Partial indexes add criterion to the index definition so that the index is
|
|
applied to a subset of rows. These can be specified on :class:`.Index`
|
|
using the ``postgresql_where`` keyword argument::
|
|
|
|
Index('my_index', my_table.c.id, postgresql_where=my_table.c.value > 10)
|
|
|
|
Operator Classes
|
|
^^^^^^^^^^^^^^^^
|
|
|
|
PostgreSQL allows the specification of an *operator class* for each column of
|
|
an index (see
|
|
http://www.postgresql.org/docs/8.3/interactive/indexes-opclass.html).
|
|
The :class:`.Index` construct allows these to be specified via the
|
|
``postgresql_ops`` keyword argument::
|
|
|
|
Index(
|
|
'my_index', my_table.c.id, my_table.c.data,
|
|
postgresql_ops={
|
|
'data': 'text_pattern_ops',
|
|
'id': 'int4_ops'
|
|
})
|
|
|
|
Note that the keys in the ``postgresql_ops`` dictionary are the "key" name of
|
|
the :class:`.Column`, i.e. the name used to access it from the ``.c``
|
|
collection of :class:`.Table`, which can be configured to be different than
|
|
the actual name of the column as expressed in the database.
|
|
|
|
If ``postgresql_ops`` is to be used against a complex SQL expression such
|
|
as a function call, then to apply to the column it must be given a label
|
|
that is identified in the dictionary by name, e.g.::
|
|
|
|
Index(
|
|
'my_index', my_table.c.id,
|
|
func.lower(my_table.c.data).label('data_lower'),
|
|
postgresql_ops={
|
|
'data_lower': 'text_pattern_ops',
|
|
'id': 'int4_ops'
|
|
})
|
|
|
|
|
|
Index Types
|
|
^^^^^^^^^^^
|
|
|
|
PostgreSQL provides several index types: B-Tree, Hash, GiST, and GIN, as well
|
|
as the ability for users to create their own (see
|
|
http://www.postgresql.org/docs/8.3/static/indexes-types.html). These can be
|
|
specified on :class:`.Index` using the ``postgresql_using`` keyword argument::
|
|
|
|
Index('my_index', my_table.c.data, postgresql_using='gin')
|
|
|
|
The value passed to the keyword argument will be simply passed through to the
|
|
underlying CREATE INDEX command, so it *must* be a valid index type for your
|
|
version of PostgreSQL.
|
|
|
|
.. _postgresql_index_storage:
|
|
|
|
Index Storage Parameters
|
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
PostgreSQL allows storage parameters to be set on indexes. The storage
|
|
parameters available depend on the index method used by the index. Storage
|
|
parameters can be specified on :class:`.Index` using the ``postgresql_with``
|
|
keyword argument::
|
|
|
|
Index('my_index', my_table.c.data, postgresql_with={"fillfactor": 50})
|
|
|
|
.. versionadded:: 1.0.6
|
|
|
|
PostgreSQL allows to define the tablespace in which to create the index.
|
|
The tablespace can be specified on :class:`.Index` using the
|
|
``postgresql_tablespace`` keyword argument::
|
|
|
|
Index('my_index', my_table.c.data, postgresql_tablespace='my_tablespace')
|
|
|
|
.. versionadded:: 1.1
|
|
|
|
Note that the same option is available on :class:`.Table` as well.
|
|
|
|
.. _postgresql_index_concurrently:
|
|
|
|
Indexes with CONCURRENTLY
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
The PostgreSQL index option CONCURRENTLY is supported by passing the
|
|
flag ``postgresql_concurrently`` to the :class:`.Index` construct::
|
|
|
|
tbl = Table('testtbl', m, Column('data', Integer))
|
|
|
|
idx1 = Index('test_idx1', tbl.c.data, postgresql_concurrently=True)
|
|
|
|
The above index construct will render DDL for CREATE INDEX, assuming
|
|
PostgreSQL 8.2 or higher is detected or for a connection-less dialect, as::
|
|
|
|
CREATE INDEX CONCURRENTLY test_idx1 ON testtbl (data)
|
|
|
|
For DROP INDEX, assuming PostgreSQL 9.2 or higher is detected or for
|
|
a connection-less dialect, it will emit::
|
|
|
|
DROP INDEX CONCURRENTLY test_idx1
|
|
|
|
.. versionadded:: 1.1 support for CONCURRENTLY on DROP INDEX. The
|
|
CONCURRENTLY keyword is now only emitted if a high enough version
|
|
of PostgreSQL is detected on the connection (or for a connection-less
|
|
dialect).
|
|
|
|
When using CONCURRENTLY, the PostgreSQL database requires that the statement
|
|
be invoked outside of a transaction block. The Python DBAPI enforces that
|
|
even for a single statement, a transaction is present, so to use this
|
|
construct, the DBAPI's "autocommit" mode must be used::
|
|
|
|
metadata = MetaData()
|
|
table = Table(
|
|
"foo", metadata,
|
|
Column("id", String))
|
|
index = Index(
|
|
"foo_idx", table.c.id, postgresql_concurrently=True)
|
|
|
|
with engine.connect() as conn:
|
|
with conn.execution_options(isolation_level='AUTOCOMMIT'):
|
|
table.create(conn)
|
|
|
|
.. seealso::
|
|
|
|
:ref:`postgresql_isolation_level`
|
|
|
|
.. _postgresql_index_reflection:
|
|
|
|
PostgreSQL Index Reflection
|
|
---------------------------
|
|
|
|
The PostgreSQL database creates a UNIQUE INDEX implicitly whenever the
|
|
UNIQUE CONSTRAINT construct is used. When inspecting a table using
|
|
:class:`.Inspector`, the :meth:`.Inspector.get_indexes`
|
|
and the :meth:`.Inspector.get_unique_constraints` will report on these
|
|
two constructs distinctly; in the case of the index, the key
|
|
``duplicates_constraint`` will be present in the index entry if it is
|
|
detected as mirroring a constraint. When performing reflection using
|
|
``Table(..., autoload=True)``, the UNIQUE INDEX is **not** returned
|
|
in :attr:`.Table.indexes` when it is detected as mirroring a
|
|
:class:`.UniqueConstraint` in the :attr:`.Table.constraints` collection.
|
|
|
|
.. versionchanged:: 1.0.0 - :class:`.Table` reflection now includes
|
|
:class:`.UniqueConstraint` objects present in the :attr:`.Table.constraints`
|
|
collection; the PostgreSQL backend will no longer include a "mirrored"
|
|
:class:`.Index` construct in :attr:`.Table.indexes` if it is detected
|
|
as corresponding to a unique constraint.
|
|
|
|
Special Reflection Options
|
|
--------------------------
|
|
|
|
The :class:`.Inspector` used for the PostgreSQL backend is an instance
|
|
of :class:`.PGInspector`, which offers additional methods::
|
|
|
|
from sqlalchemy import create_engine, inspect
|
|
|
|
engine = create_engine("postgresql+psycopg2://localhost/test")
|
|
insp = inspect(engine) # will be a PGInspector
|
|
|
|
print(insp.get_enums())
|
|
|
|
.. autoclass:: PGInspector
|
|
:members:
|
|
|
|
.. _postgresql_table_options:
|
|
|
|
PostgreSQL Table Options
|
|
------------------------
|
|
|
|
Several options for CREATE TABLE are supported directly by the PostgreSQL
|
|
dialect in conjunction with the :class:`.Table` construct:
|
|
|
|
* ``TABLESPACE``::
|
|
|
|
Table("some_table", metadata, ..., postgresql_tablespace='some_tablespace')
|
|
|
|
The above option is also available on the :class:`.Index` construct.
|
|
|
|
* ``ON COMMIT``::
|
|
|
|
Table("some_table", metadata, ..., postgresql_on_commit='PRESERVE ROWS')
|
|
|
|
* ``WITH OIDS``::
|
|
|
|
Table("some_table", metadata, ..., postgresql_with_oids=True)
|
|
|
|
* ``WITHOUT OIDS``::
|
|
|
|
Table("some_table", metadata, ..., postgresql_with_oids=False)
|
|
|
|
* ``INHERITS``::
|
|
|
|
Table("some_table", metadata, ..., postgresql_inherits="some_supertable")
|
|
|
|
Table("some_table", metadata, ..., postgresql_inherits=("t1", "t2", ...))
|
|
|
|
.. versionadded:: 1.0.0
|
|
|
|
* ``PARTITION BY``::
|
|
|
|
Table("some_table", metadata, ...,
|
|
postgresql_partition_by='LIST (part_column)')
|
|
|
|
.. versionadded:: 1.2.6
|
|
|
|
.. seealso::
|
|
|
|
`PostgreSQL CREATE TABLE options
|
|
<http://www.postgresql.org/docs/current/static/sql-createtable.html>`_
|
|
|
|
ARRAY Types
|
|
-----------
|
|
|
|
The PostgreSQL dialect supports arrays, both as multidimensional column types
|
|
as well as array literals:
|
|
|
|
* :class:`.postgresql.ARRAY` - ARRAY datatype
|
|
|
|
* :class:`.postgresql.array` - array literal
|
|
|
|
* :func:`.postgresql.array_agg` - ARRAY_AGG SQL function
|
|
|
|
* :class:`.postgresql.aggregate_order_by` - helper for PG's ORDER BY aggregate
|
|
function syntax.
|
|
|
|
JSON Types
|
|
----------
|
|
|
|
The PostgreSQL dialect supports both JSON and JSONB datatypes, including
|
|
psycopg2's native support and support for all of PostgreSQL's special
|
|
operators:
|
|
|
|
* :class:`.postgresql.JSON`
|
|
|
|
* :class:`.postgresql.JSONB`
|
|
|
|
HSTORE Type
|
|
-----------
|
|
|
|
The PostgreSQL HSTORE type as well as hstore literals are supported:
|
|
|
|
* :class:`.postgresql.HSTORE` - HSTORE datatype
|
|
|
|
* :class:`.postgresql.hstore` - hstore literal
|
|
|
|
ENUM Types
|
|
----------
|
|
|
|
PostgreSQL has an independently creatable TYPE structure which is used
|
|
to implement an enumerated type. This approach introduces significant
|
|
complexity on the SQLAlchemy side in terms of when this type should be
|
|
CREATED and DROPPED. The type object is also an independently reflectable
|
|
entity. The following sections should be consulted:
|
|
|
|
* :class:`.postgresql.ENUM` - DDL and typing support for ENUM.
|
|
|
|
* :meth:`.PGInspector.get_enums` - retrieve a listing of current ENUM types
|
|
|
|
* :meth:`.postgresql.ENUM.create` , :meth:`.postgresql.ENUM.drop` - individual
|
|
CREATE and DROP commands for ENUM.
|
|
|
|
.. _postgresql_array_of_enum:
|
|
|
|
Using ENUM with ARRAY
|
|
^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
The combination of ENUM and ARRAY is not directly supported by backend
|
|
DBAPIs at this time. In order to send and receive an ARRAY of ENUM,
|
|
use the following workaround type, which decorates the
|
|
:class:`.postgresql.ARRAY` datatype.
|
|
|
|
.. sourcecode:: python
|
|
|
|
from sqlalchemy import TypeDecorator
|
|
from sqlalchemy.dialects.postgresql import ARRAY
|
|
|
|
class ArrayOfEnum(TypeDecorator):
|
|
impl = ARRAY
|
|
|
|
def bind_expression(self, bindvalue):
|
|
return sa.cast(bindvalue, self)
|
|
|
|
def result_processor(self, dialect, coltype):
|
|
super_rp = super(ArrayOfEnum, self).result_processor(
|
|
dialect, coltype)
|
|
|
|
def handle_raw_string(value):
|
|
inner = re.match(r"^{(.*)}$", value).group(1)
|
|
return inner.split(",") if inner else []
|
|
|
|
def process(value):
|
|
if value is None:
|
|
return None
|
|
return super_rp(handle_raw_string(value))
|
|
return process
|
|
|
|
E.g.::
|
|
|
|
Table(
|
|
'mydata', metadata,
|
|
Column('id', Integer, primary_key=True),
|
|
Column('data', ArrayOfEnum(ENUM('a', 'b, 'c', name='myenum')))
|
|
|
|
)
|
|
|
|
This type is not included as a built-in type as it would be incompatible
|
|
with a DBAPI that suddenly decides to support ARRAY of ENUM directly in
|
|
a new version.
|
|
|
|
.. _postgresql_array_of_json:
|
|
|
|
Using JSON/JSONB with ARRAY
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
Similar to using ENUM, for an ARRAY of JSON/JSONB we need to render the
|
|
appropriate CAST, however current psycopg2 drivers seem to handle the result
|
|
for ARRAY of JSON automatically, so the type is simpler::
|
|
|
|
|
|
class CastingArray(ARRAY):
|
|
def bind_expression(self, bindvalue):
|
|
return sa.cast(bindvalue, self)
|
|
|
|
E.g.::
|
|
|
|
Table(
|
|
'mydata', metadata,
|
|
Column('id', Integer, primary_key=True),
|
|
Column('data', CastingArray(JSONB))
|
|
)
|
|
|
|
|
|
"""
|
|
from collections import defaultdict
|
|
import datetime as dt
|
|
import re
|
|
|
|
from ... import exc
|
|
from ... import schema
|
|
from ... import sql
|
|
from ... import util
|
|
from ...engine import default
|
|
from ...engine import reflection
|
|
from ...sql import compiler
|
|
from ...sql import elements
|
|
from ...sql import expression
|
|
from ...sql import sqltypes
|
|
from ...sql import util as sql_util
|
|
from ...types import BIGINT
|
|
from ...types import BOOLEAN
|
|
from ...types import CHAR
|
|
from ...types import DATE
|
|
from ...types import FLOAT
|
|
from ...types import INTEGER
|
|
from ...types import NUMERIC
|
|
from ...types import REAL
|
|
from ...types import SMALLINT
|
|
from ...types import TEXT
|
|
from ...types import VARCHAR
|
|
|
|
|
|
try:
|
|
from uuid import UUID as _python_UUID # noqa
|
|
except ImportError:
|
|
_python_UUID = None
|
|
|
|
|
|
IDX_USING = re.compile(r"^(?:btree|hash|gist|gin|[\w_]+)$", re.I)
|
|
|
|
AUTOCOMMIT_REGEXP = re.compile(
|
|
r"\s*(?:UPDATE|INSERT|CREATE|DELETE|DROP|ALTER|GRANT|REVOKE|"
|
|
"IMPORT FOREIGN SCHEMA|REFRESH MATERIALIZED VIEW|TRUNCATE)",
|
|
re.I | re.UNICODE,
|
|
)
|
|
|
|
RESERVED_WORDS = set(
|
|
[
|
|
"all",
|
|
"analyse",
|
|
"analyze",
|
|
"and",
|
|
"any",
|
|
"array",
|
|
"as",
|
|
"asc",
|
|
"asymmetric",
|
|
"both",
|
|
"case",
|
|
"cast",
|
|
"check",
|
|
"collate",
|
|
"column",
|
|
"constraint",
|
|
"create",
|
|
"current_catalog",
|
|
"current_date",
|
|
"current_role",
|
|
"current_time",
|
|
"current_timestamp",
|
|
"current_user",
|
|
"default",
|
|
"deferrable",
|
|
"desc",
|
|
"distinct",
|
|
"do",
|
|
"else",
|
|
"end",
|
|
"except",
|
|
"false",
|
|
"fetch",
|
|
"for",
|
|
"foreign",
|
|
"from",
|
|
"grant",
|
|
"group",
|
|
"having",
|
|
"in",
|
|
"initially",
|
|
"intersect",
|
|
"into",
|
|
"leading",
|
|
"limit",
|
|
"localtime",
|
|
"localtimestamp",
|
|
"new",
|
|
"not",
|
|
"null",
|
|
"of",
|
|
"off",
|
|
"offset",
|
|
"old",
|
|
"on",
|
|
"only",
|
|
"or",
|
|
"order",
|
|
"placing",
|
|
"primary",
|
|
"references",
|
|
"returning",
|
|
"select",
|
|
"session_user",
|
|
"some",
|
|
"symmetric",
|
|
"table",
|
|
"then",
|
|
"to",
|
|
"trailing",
|
|
"true",
|
|
"union",
|
|
"unique",
|
|
"user",
|
|
"using",
|
|
"variadic",
|
|
"when",
|
|
"where",
|
|
"window",
|
|
"with",
|
|
"authorization",
|
|
"between",
|
|
"binary",
|
|
"cross",
|
|
"current_schema",
|
|
"freeze",
|
|
"full",
|
|
"ilike",
|
|
"inner",
|
|
"is",
|
|
"isnull",
|
|
"join",
|
|
"left",
|
|
"like",
|
|
"natural",
|
|
"notnull",
|
|
"outer",
|
|
"over",
|
|
"overlaps",
|
|
"right",
|
|
"similar",
|
|
"verbose",
|
|
]
|
|
)
|
|
|
|
_DECIMAL_TYPES = (1231, 1700)
|
|
_FLOAT_TYPES = (700, 701, 1021, 1022)
|
|
_INT_TYPES = (20, 21, 23, 26, 1005, 1007, 1016)
|
|
|
|
|
|
class BYTEA(sqltypes.LargeBinary):
|
|
__visit_name__ = "BYTEA"
|
|
|
|
|
|
class DOUBLE_PRECISION(sqltypes.Float):
|
|
__visit_name__ = "DOUBLE_PRECISION"
|
|
|
|
|
|
class INET(sqltypes.TypeEngine):
|
|
__visit_name__ = "INET"
|
|
|
|
|
|
PGInet = INET
|
|
|
|
|
|
class CIDR(sqltypes.TypeEngine):
|
|
__visit_name__ = "CIDR"
|
|
|
|
|
|
PGCidr = CIDR
|
|
|
|
|
|
class MACADDR(sqltypes.TypeEngine):
|
|
__visit_name__ = "MACADDR"
|
|
|
|
|
|
PGMacAddr = MACADDR
|
|
|
|
|
|
class MONEY(sqltypes.TypeEngine):
|
|
|
|
"""Provide the PostgreSQL MONEY type.
|
|
|
|
.. versionadded:: 1.2
|
|
|
|
"""
|
|
|
|
__visit_name__ = "MONEY"
|
|
|
|
|
|
class OID(sqltypes.TypeEngine):
|
|
|
|
"""Provide the PostgreSQL OID type.
|
|
|
|
.. versionadded:: 0.9.5
|
|
|
|
"""
|
|
|
|
__visit_name__ = "OID"
|
|
|
|
|
|
class REGCLASS(sqltypes.TypeEngine):
|
|
|
|
"""Provide the PostgreSQL REGCLASS type.
|
|
|
|
.. versionadded:: 1.2.7
|
|
|
|
"""
|
|
|
|
__visit_name__ = "REGCLASS"
|
|
|
|
|
|
class TIMESTAMP(sqltypes.TIMESTAMP):
|
|
def __init__(self, timezone=False, precision=None):
|
|
super(TIMESTAMP, self).__init__(timezone=timezone)
|
|
self.precision = precision
|
|
|
|
|
|
class TIME(sqltypes.TIME):
|
|
def __init__(self, timezone=False, precision=None):
|
|
super(TIME, self).__init__(timezone=timezone)
|
|
self.precision = precision
|
|
|
|
|
|
class INTERVAL(sqltypes.NativeForEmulated, sqltypes._AbstractInterval):
|
|
|
|
"""PostgreSQL INTERVAL type.
|
|
|
|
The INTERVAL type may not be supported on all DBAPIs.
|
|
It is known to work on psycopg2 and not pg8000 or zxjdbc.
|
|
|
|
"""
|
|
|
|
__visit_name__ = "INTERVAL"
|
|
native = True
|
|
|
|
def __init__(self, precision=None, fields=None):
|
|
"""Construct an INTERVAL.
|
|
|
|
:param precision: optional integer precision value
|
|
:param fields: string fields specifier. allows storage of fields
|
|
to be limited, such as ``"YEAR"``, ``"MONTH"``, ``"DAY TO HOUR"``,
|
|
etc.
|
|
|
|
.. versionadded:: 1.2
|
|
|
|
"""
|
|
self.precision = precision
|
|
self.fields = fields
|
|
|
|
@classmethod
|
|
def adapt_emulated_to_native(cls, interval, **kw):
|
|
return INTERVAL(precision=interval.second_precision)
|
|
|
|
@property
|
|
def _type_affinity(self):
|
|
return sqltypes.Interval
|
|
|
|
@property
|
|
def python_type(self):
|
|
return dt.timedelta
|
|
|
|
|
|
PGInterval = INTERVAL
|
|
|
|
|
|
class BIT(sqltypes.TypeEngine):
|
|
__visit_name__ = "BIT"
|
|
|
|
def __init__(self, length=None, varying=False):
|
|
if not varying:
|
|
# BIT without VARYING defaults to length 1
|
|
self.length = length or 1
|
|
else:
|
|
# but BIT VARYING can be unlimited-length, so no default
|
|
self.length = length
|
|
self.varying = varying
|
|
|
|
|
|
PGBit = BIT
|
|
|
|
|
|
class UUID(sqltypes.TypeEngine):
|
|
|
|
"""PostgreSQL UUID type.
|
|
|
|
Represents the UUID column type, interpreting
|
|
data either as natively returned by the DBAPI
|
|
or as Python uuid objects.
|
|
|
|
The UUID type may not be supported on all DBAPIs.
|
|
It is known to work on psycopg2 and not pg8000.
|
|
|
|
"""
|
|
|
|
__visit_name__ = "UUID"
|
|
|
|
def __init__(self, as_uuid=False):
|
|
"""Construct a UUID type.
|
|
|
|
|
|
:param as_uuid=False: if True, values will be interpreted
|
|
as Python uuid objects, converting to/from string via the
|
|
DBAPI.
|
|
|
|
"""
|
|
if as_uuid and _python_UUID is None:
|
|
raise NotImplementedError(
|
|
"This version of Python does not support "
|
|
"the native UUID type."
|
|
)
|
|
self.as_uuid = as_uuid
|
|
|
|
def bind_processor(self, dialect):
|
|
if self.as_uuid:
|
|
|
|
def process(value):
|
|
if value is not None:
|
|
value = util.text_type(value)
|
|
return value
|
|
|
|
return process
|
|
else:
|
|
return None
|
|
|
|
def result_processor(self, dialect, coltype):
|
|
if self.as_uuid:
|
|
|
|
def process(value):
|
|
if value is not None:
|
|
value = _python_UUID(value)
|
|
return value
|
|
|
|
return process
|
|
else:
|
|
return None
|
|
|
|
|
|
PGUuid = UUID
|
|
|
|
|
|
class TSVECTOR(sqltypes.TypeEngine):
|
|
|
|
"""The :class:`.postgresql.TSVECTOR` type implements the PostgreSQL
|
|
text search type TSVECTOR.
|
|
|
|
It can be used to do full text queries on natural language
|
|
documents.
|
|
|
|
.. versionadded:: 0.9.0
|
|
|
|
.. seealso::
|
|
|
|
:ref:`postgresql_match`
|
|
|
|
"""
|
|
|
|
__visit_name__ = "TSVECTOR"
|
|
|
|
|
|
class ENUM(sqltypes.NativeForEmulated, sqltypes.Enum):
|
|
|
|
"""PostgreSQL ENUM type.
|
|
|
|
This is a subclass of :class:`.types.Enum` which includes
|
|
support for PG's ``CREATE TYPE`` and ``DROP TYPE``.
|
|
|
|
When the builtin type :class:`.types.Enum` is used and the
|
|
:paramref:`.Enum.native_enum` flag is left at its default of
|
|
True, the PostgreSQL backend will use a :class:`.postgresql.ENUM`
|
|
type as the implementation, so the special create/drop rules
|
|
will be used.
|
|
|
|
The create/drop behavior of ENUM is necessarily intricate, due to the
|
|
awkward relationship the ENUM type has in relationship to the
|
|
parent table, in that it may be "owned" by just a single table, or
|
|
may be shared among many tables.
|
|
|
|
When using :class:`.types.Enum` or :class:`.postgresql.ENUM`
|
|
in an "inline" fashion, the ``CREATE TYPE`` and ``DROP TYPE`` is emitted
|
|
corresponding to when the :meth:`.Table.create` and :meth:`.Table.drop`
|
|
methods are called::
|
|
|
|
table = Table('sometable', metadata,
|
|
Column('some_enum', ENUM('a', 'b', 'c', name='myenum'))
|
|
)
|
|
|
|
table.create(engine) # will emit CREATE ENUM and CREATE TABLE
|
|
table.drop(engine) # will emit DROP TABLE and DROP ENUM
|
|
|
|
To use a common enumerated type between multiple tables, the best
|
|
practice is to declare the :class:`.types.Enum` or
|
|
:class:`.postgresql.ENUM` independently, and associate it with the
|
|
:class:`.MetaData` object itself::
|
|
|
|
my_enum = ENUM('a', 'b', 'c', name='myenum', metadata=metadata)
|
|
|
|
t1 = Table('sometable_one', metadata,
|
|
Column('some_enum', myenum)
|
|
)
|
|
|
|
t2 = Table('sometable_two', metadata,
|
|
Column('some_enum', myenum)
|
|
)
|
|
|
|
When this pattern is used, care must still be taken at the level
|
|
of individual table creates. Emitting CREATE TABLE without also
|
|
specifying ``checkfirst=True`` will still cause issues::
|
|
|
|
t1.create(engine) # will fail: no such type 'myenum'
|
|
|
|
If we specify ``checkfirst=True``, the individual table-level create
|
|
operation will check for the ``ENUM`` and create if not exists::
|
|
|
|
# will check if enum exists, and emit CREATE TYPE if not
|
|
t1.create(engine, checkfirst=True)
|
|
|
|
When using a metadata-level ENUM type, the type will always be created
|
|
and dropped if either the metadata-wide create/drop is called::
|
|
|
|
metadata.create_all(engine) # will emit CREATE TYPE
|
|
metadata.drop_all(engine) # will emit DROP TYPE
|
|
|
|
The type can also be created and dropped directly::
|
|
|
|
my_enum.create(engine)
|
|
my_enum.drop(engine)
|
|
|
|
.. versionchanged:: 1.0.0 The PostgreSQL :class:`.postgresql.ENUM` type
|
|
now behaves more strictly with regards to CREATE/DROP. A metadata-level
|
|
ENUM type will only be created and dropped at the metadata level,
|
|
not the table level, with the exception of
|
|
``table.create(checkfirst=True)``.
|
|
The ``table.drop()`` call will now emit a DROP TYPE for a table-level
|
|
enumerated type.
|
|
|
|
"""
|
|
|
|
native_enum = True
|
|
|
|
def __init__(self, *enums, **kw):
|
|
"""Construct an :class:`~.postgresql.ENUM`.
|
|
|
|
Arguments are the same as that of
|
|
:class:`.types.Enum`, but also including
|
|
the following parameters.
|
|
|
|
:param create_type: Defaults to True.
|
|
Indicates that ``CREATE TYPE`` should be
|
|
emitted, after optionally checking for the
|
|
presence of the type, when the parent
|
|
table is being created; and additionally
|
|
that ``DROP TYPE`` is called when the table
|
|
is dropped. When ``False``, no check
|
|
will be performed and no ``CREATE TYPE``
|
|
or ``DROP TYPE`` is emitted, unless
|
|
:meth:`~.postgresql.ENUM.create`
|
|
or :meth:`~.postgresql.ENUM.drop`
|
|
are called directly.
|
|
Setting to ``False`` is helpful
|
|
when invoking a creation scheme to a SQL file
|
|
without access to the actual database -
|
|
the :meth:`~.postgresql.ENUM.create` and
|
|
:meth:`~.postgresql.ENUM.drop` methods can
|
|
be used to emit SQL to a target bind.
|
|
|
|
"""
|
|
self.create_type = kw.pop("create_type", True)
|
|
super(ENUM, self).__init__(*enums, **kw)
|
|
|
|
@classmethod
|
|
def adapt_emulated_to_native(cls, impl, **kw):
|
|
"""Produce a PostgreSQL native :class:`.postgresql.ENUM` from plain
|
|
:class:`.Enum`.
|
|
|
|
"""
|
|
kw.setdefault("validate_strings", impl.validate_strings)
|
|
kw.setdefault("name", impl.name)
|
|
kw.setdefault("schema", impl.schema)
|
|
kw.setdefault("inherit_schema", impl.inherit_schema)
|
|
kw.setdefault("metadata", impl.metadata)
|
|
kw.setdefault("_create_events", False)
|
|
kw.setdefault("values_callable", impl.values_callable)
|
|
return cls(**kw)
|
|
|
|
def create(self, bind=None, checkfirst=True):
|
|
"""Emit ``CREATE TYPE`` for this
|
|
:class:`~.postgresql.ENUM`.
|
|
|
|
If the underlying dialect does not support
|
|
PostgreSQL CREATE TYPE, no action is taken.
|
|
|
|
:param bind: a connectable :class:`.Engine`,
|
|
:class:`.Connection`, or similar object to emit
|
|
SQL.
|
|
:param checkfirst: if ``True``, a query against
|
|
the PG catalog will be first performed to see
|
|
if the type does not exist already before
|
|
creating.
|
|
|
|
"""
|
|
if not bind.dialect.supports_native_enum:
|
|
return
|
|
|
|
if not checkfirst or not bind.dialect.has_type(
|
|
bind, self.name, schema=self.schema
|
|
):
|
|
bind.execute(CreateEnumType(self))
|
|
|
|
def drop(self, bind=None, checkfirst=True):
|
|
"""Emit ``DROP TYPE`` for this
|
|
:class:`~.postgresql.ENUM`.
|
|
|
|
If the underlying dialect does not support
|
|
PostgreSQL DROP TYPE, no action is taken.
|
|
|
|
:param bind: a connectable :class:`.Engine`,
|
|
:class:`.Connection`, or similar object to emit
|
|
SQL.
|
|
:param checkfirst: if ``True``, a query against
|
|
the PG catalog will be first performed to see
|
|
if the type actually exists before dropping.
|
|
|
|
"""
|
|
if not bind.dialect.supports_native_enum:
|
|
return
|
|
|
|
if not checkfirst or bind.dialect.has_type(
|
|
bind, self.name, schema=self.schema
|
|
):
|
|
bind.execute(DropEnumType(self))
|
|
|
|
def _check_for_name_in_memos(self, checkfirst, kw):
|
|
"""Look in the 'ddl runner' for 'memos', then
|
|
note our name in that collection.
|
|
|
|
This to ensure a particular named enum is operated
|
|
upon only once within any kind of create/drop
|
|
sequence without relying upon "checkfirst".
|
|
|
|
"""
|
|
if not self.create_type:
|
|
return True
|
|
if "_ddl_runner" in kw:
|
|
ddl_runner = kw["_ddl_runner"]
|
|
if "_pg_enums" in ddl_runner.memo:
|
|
pg_enums = ddl_runner.memo["_pg_enums"]
|
|
else:
|
|
pg_enums = ddl_runner.memo["_pg_enums"] = set()
|
|
present = (self.schema, self.name) in pg_enums
|
|
pg_enums.add((self.schema, self.name))
|
|
return present
|
|
else:
|
|
return False
|
|
|
|
def _on_table_create(self, target, bind, checkfirst=False, **kw):
|
|
if (
|
|
checkfirst
|
|
or (
|
|
not self.metadata
|
|
and not kw.get("_is_metadata_operation", False)
|
|
)
|
|
and not self._check_for_name_in_memos(checkfirst, kw)
|
|
):
|
|
self.create(bind=bind, checkfirst=checkfirst)
|
|
|
|
def _on_table_drop(self, target, bind, checkfirst=False, **kw):
|
|
if (
|
|
not self.metadata
|
|
and not kw.get("_is_metadata_operation", False)
|
|
and not self._check_for_name_in_memos(checkfirst, kw)
|
|
):
|
|
self.drop(bind=bind, checkfirst=checkfirst)
|
|
|
|
def _on_metadata_create(self, target, bind, checkfirst=False, **kw):
|
|
if not self._check_for_name_in_memos(checkfirst, kw):
|
|
self.create(bind=bind, checkfirst=checkfirst)
|
|
|
|
def _on_metadata_drop(self, target, bind, checkfirst=False, **kw):
|
|
if not self._check_for_name_in_memos(checkfirst, kw):
|
|
self.drop(bind=bind, checkfirst=checkfirst)
|
|
|
|
|
|
colspecs = {sqltypes.Interval: INTERVAL, sqltypes.Enum: ENUM}
|
|
|
|
ischema_names = {
|
|
"integer": INTEGER,
|
|
"bigint": BIGINT,
|
|
"smallint": SMALLINT,
|
|
"character varying": VARCHAR,
|
|
"character": CHAR,
|
|
'"char"': sqltypes.String,
|
|
"name": sqltypes.String,
|
|
"text": TEXT,
|
|
"numeric": NUMERIC,
|
|
"float": FLOAT,
|
|
"real": REAL,
|
|
"inet": INET,
|
|
"cidr": CIDR,
|
|
"uuid": UUID,
|
|
"bit": BIT,
|
|
"bit varying": BIT,
|
|
"macaddr": MACADDR,
|
|
"money": MONEY,
|
|
"oid": OID,
|
|
"regclass": REGCLASS,
|
|
"double precision": DOUBLE_PRECISION,
|
|
"timestamp": TIMESTAMP,
|
|
"timestamp with time zone": TIMESTAMP,
|
|
"timestamp without time zone": TIMESTAMP,
|
|
"time with time zone": TIME,
|
|
"time without time zone": TIME,
|
|
"date": DATE,
|
|
"time": TIME,
|
|
"bytea": BYTEA,
|
|
"boolean": BOOLEAN,
|
|
"interval": INTERVAL,
|
|
"tsvector": TSVECTOR,
|
|
}
|
|
|
|
|
|
class PGCompiler(compiler.SQLCompiler):
|
|
def visit_array(self, element, **kw):
|
|
return "ARRAY[%s]" % self.visit_clauselist(element, **kw)
|
|
|
|
def visit_slice(self, element, **kw):
|
|
return "%s:%s" % (
|
|
self.process(element.start, **kw),
|
|
self.process(element.stop, **kw),
|
|
)
|
|
|
|
def visit_json_getitem_op_binary(
|
|
self, binary, operator, _cast_applied=False, **kw
|
|
):
|
|
if (
|
|
not _cast_applied
|
|
and binary.type._type_affinity is not sqltypes.JSON
|
|
):
|
|
kw["_cast_applied"] = True
|
|
return self.process(sql.cast(binary, binary.type), **kw)
|
|
|
|
kw["eager_grouping"] = True
|
|
|
|
return self._generate_generic_binary(
|
|
binary, " -> " if not _cast_applied else " ->> ", **kw
|
|
)
|
|
|
|
def visit_json_path_getitem_op_binary(
|
|
self, binary, operator, _cast_applied=False, **kw
|
|
):
|
|
if (
|
|
not _cast_applied
|
|
and binary.type._type_affinity is not sqltypes.JSON
|
|
):
|
|
kw["_cast_applied"] = True
|
|
return self.process(sql.cast(binary, binary.type), **kw)
|
|
|
|
kw["eager_grouping"] = True
|
|
return self._generate_generic_binary(
|
|
binary, " #> " if not _cast_applied else " #>> ", **kw
|
|
)
|
|
|
|
def visit_getitem_binary(self, binary, operator, **kw):
|
|
return "%s[%s]" % (
|
|
self.process(binary.left, **kw),
|
|
self.process(binary.right, **kw),
|
|
)
|
|
|
|
def visit_aggregate_order_by(self, element, **kw):
|
|
return "%s ORDER BY %s" % (
|
|
self.process(element.target, **kw),
|
|
self.process(element.order_by, **kw),
|
|
)
|
|
|
|
def visit_match_op_binary(self, binary, operator, **kw):
|
|
if "postgresql_regconfig" in binary.modifiers:
|
|
regconfig = self.render_literal_value(
|
|
binary.modifiers["postgresql_regconfig"], sqltypes.STRINGTYPE
|
|
)
|
|
if regconfig:
|
|
return "%s @@ to_tsquery(%s, %s)" % (
|
|
self.process(binary.left, **kw),
|
|
regconfig,
|
|
self.process(binary.right, **kw),
|
|
)
|
|
return "%s @@ to_tsquery(%s)" % (
|
|
self.process(binary.left, **kw),
|
|
self.process(binary.right, **kw),
|
|
)
|
|
|
|
def visit_ilike_op_binary(self, binary, operator, **kw):
|
|
escape = binary.modifiers.get("escape", None)
|
|
|
|
return "%s ILIKE %s" % (
|
|
self.process(binary.left, **kw),
|
|
self.process(binary.right, **kw),
|
|
) + (
|
|
" ESCAPE " + self.render_literal_value(escape, sqltypes.STRINGTYPE)
|
|
if escape
|
|
else ""
|
|
)
|
|
|
|
def visit_notilike_op_binary(self, binary, operator, **kw):
|
|
escape = binary.modifiers.get("escape", None)
|
|
return "%s NOT ILIKE %s" % (
|
|
self.process(binary.left, **kw),
|
|
self.process(binary.right, **kw),
|
|
) + (
|
|
" ESCAPE " + self.render_literal_value(escape, sqltypes.STRINGTYPE)
|
|
if escape
|
|
else ""
|
|
)
|
|
|
|
def visit_empty_set_expr(self, element_types):
|
|
# cast the empty set to the type we are comparing against. if
|
|
# we are comparing against the null type, pick an arbitrary
|
|
# datatype for the empty set
|
|
return "SELECT %s WHERE 1!=1" % (
|
|
", ".join(
|
|
"CAST(NULL AS %s)"
|
|
% self.dialect.type_compiler.process(
|
|
INTEGER() if type_._isnull else type_
|
|
)
|
|
for type_ in element_types or [INTEGER()]
|
|
),
|
|
)
|
|
|
|
def render_literal_value(self, value, type_):
|
|
value = super(PGCompiler, self).render_literal_value(value, type_)
|
|
|
|
if self.dialect._backslash_escapes:
|
|
value = value.replace("\\", "\\\\")
|
|
return value
|
|
|
|
def visit_sequence(self, seq, **kw):
|
|
return "nextval('%s')" % self.preparer.format_sequence(seq)
|
|
|
|
def limit_clause(self, select, **kw):
|
|
text = ""
|
|
if select._limit_clause is not None:
|
|
text += " \n LIMIT " + self.process(select._limit_clause, **kw)
|
|
if select._offset_clause is not None:
|
|
if select._limit_clause is None:
|
|
text += " \n LIMIT ALL"
|
|
text += " OFFSET " + self.process(select._offset_clause, **kw)
|
|
return text
|
|
|
|
def format_from_hint_text(self, sqltext, table, hint, iscrud):
|
|
if hint.upper() != "ONLY":
|
|
raise exc.CompileError("Unrecognized hint: %r" % hint)
|
|
return "ONLY " + sqltext
|
|
|
|
def get_select_precolumns(self, select, **kw):
|
|
if select._distinct is not False:
|
|
if select._distinct is True:
|
|
return "DISTINCT "
|
|
elif isinstance(select._distinct, (list, tuple)):
|
|
return (
|
|
"DISTINCT ON ("
|
|
+ ", ".join(
|
|
[self.process(col, **kw) for col in select._distinct]
|
|
)
|
|
+ ") "
|
|
)
|
|
else:
|
|
return (
|
|
"DISTINCT ON ("
|
|
+ self.process(select._distinct, **kw)
|
|
+ ") "
|
|
)
|
|
else:
|
|
return ""
|
|
|
|
def for_update_clause(self, select, **kw):
|
|
|
|
if select._for_update_arg.read:
|
|
if select._for_update_arg.key_share:
|
|
tmp = " FOR KEY SHARE"
|
|
else:
|
|
tmp = " FOR SHARE"
|
|
elif select._for_update_arg.key_share:
|
|
tmp = " FOR NO KEY UPDATE"
|
|
else:
|
|
tmp = " FOR UPDATE"
|
|
|
|
if select._for_update_arg.of:
|
|
|
|
tables = util.OrderedSet()
|
|
for c in select._for_update_arg.of:
|
|
tables.update(sql_util.surface_selectables_only(c))
|
|
|
|
tmp += " OF " + ", ".join(
|
|
self.process(table, ashint=True, use_schema=False, **kw)
|
|
for table in tables
|
|
)
|
|
|
|
if select._for_update_arg.nowait:
|
|
tmp += " NOWAIT"
|
|
if select._for_update_arg.skip_locked:
|
|
tmp += " SKIP LOCKED"
|
|
|
|
return tmp
|
|
|
|
def returning_clause(self, stmt, returning_cols):
|
|
|
|
columns = [
|
|
self._label_select_column(None, c, True, False, {})
|
|
for c in expression._select_iterables(returning_cols)
|
|
]
|
|
|
|
return "RETURNING " + ", ".join(columns)
|
|
|
|
def visit_substring_func(self, func, **kw):
|
|
s = self.process(func.clauses.clauses[0], **kw)
|
|
start = self.process(func.clauses.clauses[1], **kw)
|
|
if len(func.clauses.clauses) > 2:
|
|
length = self.process(func.clauses.clauses[2], **kw)
|
|
return "SUBSTRING(%s FROM %s FOR %s)" % (s, start, length)
|
|
else:
|
|
return "SUBSTRING(%s FROM %s)" % (s, start)
|
|
|
|
def _on_conflict_target(self, clause, **kw):
|
|
|
|
if clause.constraint_target is not None:
|
|
target_text = "ON CONSTRAINT %s" % clause.constraint_target
|
|
elif clause.inferred_target_elements is not None:
|
|
target_text = "(%s)" % ", ".join(
|
|
(
|
|
self.preparer.quote(c)
|
|
if isinstance(c, util.string_types)
|
|
else self.process(c, include_table=False, use_schema=False)
|
|
)
|
|
for c in clause.inferred_target_elements
|
|
)
|
|
if clause.inferred_target_whereclause is not None:
|
|
target_text += " WHERE %s" % self.process(
|
|
clause.inferred_target_whereclause,
|
|
include_table=False,
|
|
use_schema=False,
|
|
)
|
|
else:
|
|
target_text = ""
|
|
|
|
return target_text
|
|
|
|
def visit_on_conflict_do_nothing(self, on_conflict, **kw):
|
|
|
|
target_text = self._on_conflict_target(on_conflict, **kw)
|
|
|
|
if target_text:
|
|
return "ON CONFLICT %s DO NOTHING" % target_text
|
|
else:
|
|
return "ON CONFLICT DO NOTHING"
|
|
|
|
def visit_on_conflict_do_update(self, on_conflict, **kw):
|
|
|
|
clause = on_conflict
|
|
|
|
target_text = self._on_conflict_target(on_conflict, **kw)
|
|
|
|
action_set_ops = []
|
|
|
|
set_parameters = dict(clause.update_values_to_set)
|
|
# create a list of column assignment clauses as tuples
|
|
|
|
insert_statement = self.stack[-1]["selectable"]
|
|
cols = insert_statement.table.c
|
|
for c in cols:
|
|
col_key = c.key
|
|
if col_key in set_parameters:
|
|
value = set_parameters.pop(col_key)
|
|
if elements._is_literal(value):
|
|
value = elements.BindParameter(None, value, type_=c.type)
|
|
|
|
else:
|
|
if (
|
|
isinstance(value, elements.BindParameter)
|
|
and value.type._isnull
|
|
):
|
|
value = value._clone()
|
|
value.type = c.type
|
|
value_text = self.process(value.self_group(), use_schema=False)
|
|
|
|
key_text = self.preparer.quote(col_key)
|
|
action_set_ops.append("%s = %s" % (key_text, value_text))
|
|
|
|
# check for names that don't match columns
|
|
if set_parameters:
|
|
util.warn(
|
|
"Additional column names not matching "
|
|
"any column keys in table '%s': %s"
|
|
% (
|
|
self.statement.table.name,
|
|
(", ".join("'%s'" % c for c in set_parameters)),
|
|
)
|
|
)
|
|
for k, v in set_parameters.items():
|
|
key_text = (
|
|
self.preparer.quote(k)
|
|
if isinstance(k, util.string_types)
|
|
else self.process(k, use_schema=False)
|
|
)
|
|
value_text = self.process(
|
|
elements._literal_as_binds(v), use_schema=False
|
|
)
|
|
action_set_ops.append("%s = %s" % (key_text, value_text))
|
|
|
|
action_text = ", ".join(action_set_ops)
|
|
if clause.update_whereclause is not None:
|
|
action_text += " WHERE %s" % self.process(
|
|
clause.update_whereclause, include_table=True, use_schema=False
|
|
)
|
|
|
|
return "ON CONFLICT %s DO UPDATE SET %s" % (target_text, action_text)
|
|
|
|
def update_from_clause(
|
|
self, update_stmt, from_table, extra_froms, from_hints, **kw
|
|
):
|
|
return "FROM " + ", ".join(
|
|
t._compiler_dispatch(self, asfrom=True, fromhints=from_hints, **kw)
|
|
for t in extra_froms
|
|
)
|
|
|
|
def delete_extra_from_clause(
|
|
self, delete_stmt, from_table, extra_froms, from_hints, **kw
|
|
):
|
|
"""Render the DELETE .. USING clause specific to PostgreSQL."""
|
|
return "USING " + ", ".join(
|
|
t._compiler_dispatch(self, asfrom=True, fromhints=from_hints, **kw)
|
|
for t in extra_froms
|
|
)
|
|
|
|
|
|
class PGDDLCompiler(compiler.DDLCompiler):
|
|
def get_column_specification(self, column, **kwargs):
|
|
|
|
colspec = self.preparer.format_column(column)
|
|
impl_type = column.type.dialect_impl(self.dialect)
|
|
if isinstance(impl_type, sqltypes.TypeDecorator):
|
|
impl_type = impl_type.impl
|
|
|
|
if (
|
|
column.primary_key
|
|
and column is column.table._autoincrement_column
|
|
and (
|
|
self.dialect.supports_smallserial
|
|
or not isinstance(impl_type, sqltypes.SmallInteger)
|
|
)
|
|
and (
|
|
column.default is None
|
|
or (
|
|
isinstance(column.default, schema.Sequence)
|
|
and column.default.optional
|
|
)
|
|
)
|
|
):
|
|
if isinstance(impl_type, sqltypes.BigInteger):
|
|
colspec += " BIGSERIAL"
|
|
elif isinstance(impl_type, sqltypes.SmallInteger):
|
|
colspec += " SMALLSERIAL"
|
|
else:
|
|
colspec += " SERIAL"
|
|
else:
|
|
colspec += " " + self.dialect.type_compiler.process(
|
|
column.type, type_expression=column
|
|
)
|
|
default = self.get_column_default_string(column)
|
|
if default is not None:
|
|
colspec += " DEFAULT " + default
|
|
|
|
if column.computed is not None:
|
|
colspec += " " + self.process(column.computed)
|
|
|
|
if not column.nullable:
|
|
colspec += " NOT NULL"
|
|
return colspec
|
|
|
|
def visit_drop_table_comment(self, drop):
|
|
return "COMMENT ON TABLE %s IS NULL" % self.preparer.format_table(
|
|
drop.element
|
|
)
|
|
|
|
def visit_create_enum_type(self, create):
|
|
type_ = create.element
|
|
|
|
return "CREATE TYPE %s AS ENUM (%s)" % (
|
|
self.preparer.format_type(type_),
|
|
", ".join(
|
|
self.sql_compiler.process(sql.literal(e), literal_binds=True)
|
|
for e in type_.enums
|
|
),
|
|
)
|
|
|
|
def visit_drop_enum_type(self, drop):
|
|
type_ = drop.element
|
|
|
|
return "DROP TYPE %s" % (self.preparer.format_type(type_))
|
|
|
|
def visit_create_index(self, create):
|
|
preparer = self.preparer
|
|
index = create.element
|
|
self._verify_index_table(index)
|
|
text = "CREATE "
|
|
if index.unique:
|
|
text += "UNIQUE "
|
|
text += "INDEX "
|
|
|
|
if self.dialect._supports_create_index_concurrently:
|
|
concurrently = index.dialect_options["postgresql"]["concurrently"]
|
|
if concurrently:
|
|
text += "CONCURRENTLY "
|
|
|
|
text += "%s ON %s " % (
|
|
self._prepared_index_name(index, include_schema=False),
|
|
preparer.format_table(index.table),
|
|
)
|
|
|
|
using = index.dialect_options["postgresql"]["using"]
|
|
if using:
|
|
text += (
|
|
"USING %s "
|
|
% self.preparer.validate_sql_phrase(using, IDX_USING).lower()
|
|
)
|
|
|
|
ops = index.dialect_options["postgresql"]["ops"]
|
|
text += "(%s)" % (
|
|
", ".join(
|
|
[
|
|
self.sql_compiler.process(
|
|
expr.self_group()
|
|
if not isinstance(expr, expression.ColumnClause)
|
|
else expr,
|
|
include_table=False,
|
|
literal_binds=True,
|
|
)
|
|
+ (
|
|
(" " + ops[expr.key])
|
|
if hasattr(expr, "key") and expr.key in ops
|
|
else ""
|
|
)
|
|
for expr in index.expressions
|
|
]
|
|
)
|
|
)
|
|
|
|
withclause = index.dialect_options["postgresql"]["with"]
|
|
|
|
if withclause:
|
|
text += " WITH (%s)" % (
|
|
", ".join(
|
|
[
|
|
"%s = %s" % storage_parameter
|
|
for storage_parameter in withclause.items()
|
|
]
|
|
)
|
|
)
|
|
|
|
tablespace_name = index.dialect_options["postgresql"]["tablespace"]
|
|
|
|
if tablespace_name:
|
|
text += " TABLESPACE %s" % preparer.quote(tablespace_name)
|
|
|
|
whereclause = index.dialect_options["postgresql"]["where"]
|
|
|
|
if whereclause is not None:
|
|
where_compiled = self.sql_compiler.process(
|
|
whereclause, include_table=False, literal_binds=True
|
|
)
|
|
text += " WHERE " + where_compiled
|
|
return text
|
|
|
|
def visit_drop_index(self, drop):
|
|
index = drop.element
|
|
|
|
text = "\nDROP INDEX "
|
|
|
|
if self.dialect._supports_drop_index_concurrently:
|
|
concurrently = index.dialect_options["postgresql"]["concurrently"]
|
|
if concurrently:
|
|
text += "CONCURRENTLY "
|
|
|
|
text += self._prepared_index_name(index, include_schema=True)
|
|
return text
|
|
|
|
def visit_exclude_constraint(self, constraint, **kw):
|
|
text = ""
|
|
if constraint.name is not None:
|
|
text += "CONSTRAINT %s " % self.preparer.format_constraint(
|
|
constraint
|
|
)
|
|
elements = []
|
|
for expr, name, op in constraint._render_exprs:
|
|
kw["include_table"] = False
|
|
elements.append(
|
|
"%s WITH %s" % (self.sql_compiler.process(expr, **kw), op)
|
|
)
|
|
text += "EXCLUDE USING %s (%s)" % (
|
|
self.preparer.validate_sql_phrase(
|
|
constraint.using, IDX_USING
|
|
).lower(),
|
|
", ".join(elements),
|
|
)
|
|
if constraint.where is not None:
|
|
text += " WHERE (%s)" % self.sql_compiler.process(
|
|
constraint.where, literal_binds=True
|
|
)
|
|
text += self.define_constraint_deferrability(constraint)
|
|
return text
|
|
|
|
def post_create_table(self, table):
|
|
table_opts = []
|
|
pg_opts = table.dialect_options["postgresql"]
|
|
|
|
inherits = pg_opts.get("inherits")
|
|
if inherits is not None:
|
|
if not isinstance(inherits, (list, tuple)):
|
|
inherits = (inherits,)
|
|
table_opts.append(
|
|
"\n INHERITS ( "
|
|
+ ", ".join(self.preparer.quote(name) for name in inherits)
|
|
+ " )"
|
|
)
|
|
|
|
if pg_opts["partition_by"]:
|
|
table_opts.append("\n PARTITION BY %s" % pg_opts["partition_by"])
|
|
|
|
if pg_opts["with_oids"] is True:
|
|
table_opts.append("\n WITH OIDS")
|
|
elif pg_opts["with_oids"] is False:
|
|
table_opts.append("\n WITHOUT OIDS")
|
|
|
|
if pg_opts["on_commit"]:
|
|
on_commit_options = pg_opts["on_commit"].replace("_", " ").upper()
|
|
table_opts.append("\n ON COMMIT %s" % on_commit_options)
|
|
|
|
if pg_opts["tablespace"]:
|
|
tablespace_name = pg_opts["tablespace"]
|
|
table_opts.append(
|
|
"\n TABLESPACE %s" % self.preparer.quote(tablespace_name)
|
|
)
|
|
|
|
return "".join(table_opts)
|
|
|
|
def visit_computed_column(self, generated):
|
|
if generated.persisted is False:
|
|
raise exc.CompileError(
|
|
"PostrgreSQL computed columns do not support 'virtual' "
|
|
"persistence; set the 'persisted' flag to None or True for "
|
|
"PostgreSQL support."
|
|
)
|
|
|
|
return "GENERATED ALWAYS AS (%s) STORED" % self.sql_compiler.process(
|
|
generated.sqltext, include_table=False, literal_binds=True
|
|
)
|
|
|
|
|
|
class PGTypeCompiler(compiler.GenericTypeCompiler):
|
|
def visit_TSVECTOR(self, type_, **kw):
|
|
return "TSVECTOR"
|
|
|
|
def visit_INET(self, type_, **kw):
|
|
return "INET"
|
|
|
|
def visit_CIDR(self, type_, **kw):
|
|
return "CIDR"
|
|
|
|
def visit_MACADDR(self, type_, **kw):
|
|
return "MACADDR"
|
|
|
|
def visit_MONEY(self, type_, **kw):
|
|
return "MONEY"
|
|
|
|
def visit_OID(self, type_, **kw):
|
|
return "OID"
|
|
|
|
def visit_REGCLASS(self, type_, **kw):
|
|
return "REGCLASS"
|
|
|
|
def visit_FLOAT(self, type_, **kw):
|
|
if not type_.precision:
|
|
return "FLOAT"
|
|
else:
|
|
return "FLOAT(%(precision)s)" % {"precision": type_.precision}
|
|
|
|
def visit_DOUBLE_PRECISION(self, type_, **kw):
|
|
return "DOUBLE PRECISION"
|
|
|
|
def visit_BIGINT(self, type_, **kw):
|
|
return "BIGINT"
|
|
|
|
def visit_HSTORE(self, type_, **kw):
|
|
return "HSTORE"
|
|
|
|
def visit_JSON(self, type_, **kw):
|
|
return "JSON"
|
|
|
|
def visit_JSONB(self, type_, **kw):
|
|
return "JSONB"
|
|
|
|
def visit_INT4RANGE(self, type_, **kw):
|
|
return "INT4RANGE"
|
|
|
|
def visit_INT8RANGE(self, type_, **kw):
|
|
return "INT8RANGE"
|
|
|
|
def visit_NUMRANGE(self, type_, **kw):
|
|
return "NUMRANGE"
|
|
|
|
def visit_DATERANGE(self, type_, **kw):
|
|
return "DATERANGE"
|
|
|
|
def visit_TSRANGE(self, type_, **kw):
|
|
return "TSRANGE"
|
|
|
|
def visit_TSTZRANGE(self, type_, **kw):
|
|
return "TSTZRANGE"
|
|
|
|
def visit_datetime(self, type_, **kw):
|
|
return self.visit_TIMESTAMP(type_, **kw)
|
|
|
|
def visit_enum(self, type_, **kw):
|
|
if not type_.native_enum or not self.dialect.supports_native_enum:
|
|
return super(PGTypeCompiler, self).visit_enum(type_, **kw)
|
|
else:
|
|
return self.visit_ENUM(type_, **kw)
|
|
|
|
def visit_ENUM(self, type_, **kw):
|
|
return self.dialect.identifier_preparer.format_type(type_)
|
|
|
|
def visit_TIMESTAMP(self, type_, **kw):
|
|
return "TIMESTAMP%s %s" % (
|
|
"(%d)" % type_.precision
|
|
if getattr(type_, "precision", None) is not None
|
|
else "",
|
|
(type_.timezone and "WITH" or "WITHOUT") + " TIME ZONE",
|
|
)
|
|
|
|
def visit_TIME(self, type_, **kw):
|
|
return "TIME%s %s" % (
|
|
"(%d)" % type_.precision
|
|
if getattr(type_, "precision", None) is not None
|
|
else "",
|
|
(type_.timezone and "WITH" or "WITHOUT") + " TIME ZONE",
|
|
)
|
|
|
|
def visit_INTERVAL(self, type_, **kw):
|
|
text = "INTERVAL"
|
|
if type_.fields is not None:
|
|
text += " " + type_.fields
|
|
if type_.precision is not None:
|
|
text += " (%d)" % type_.precision
|
|
return text
|
|
|
|
def visit_BIT(self, type_, **kw):
|
|
if type_.varying:
|
|
compiled = "BIT VARYING"
|
|
if type_.length is not None:
|
|
compiled += "(%d)" % type_.length
|
|
else:
|
|
compiled = "BIT(%d)" % type_.length
|
|
return compiled
|
|
|
|
def visit_UUID(self, type_, **kw):
|
|
return "UUID"
|
|
|
|
def visit_large_binary(self, type_, **kw):
|
|
return self.visit_BYTEA(type_, **kw)
|
|
|
|
def visit_BYTEA(self, type_, **kw):
|
|
return "BYTEA"
|
|
|
|
def visit_ARRAY(self, type_, **kw):
|
|
|
|
# TODO: pass **kw?
|
|
inner = self.process(type_.item_type)
|
|
return re.sub(
|
|
r"((?: COLLATE.*)?)$",
|
|
(
|
|
r"%s\1"
|
|
% (
|
|
"[]"
|
|
* (type_.dimensions if type_.dimensions is not None else 1)
|
|
)
|
|
),
|
|
inner,
|
|
count=1,
|
|
)
|
|
|
|
|
|
class PGIdentifierPreparer(compiler.IdentifierPreparer):
|
|
|
|
reserved_words = RESERVED_WORDS
|
|
|
|
def _unquote_identifier(self, value):
|
|
if value[0] == self.initial_quote:
|
|
value = value[1:-1].replace(
|
|
self.escape_to_quote, self.escape_quote
|
|
)
|
|
return value
|
|
|
|
def format_type(self, type_, use_schema=True):
|
|
if not type_.name:
|
|
raise exc.CompileError("PostgreSQL ENUM type requires a name.")
|
|
|
|
name = self.quote(type_.name)
|
|
effective_schema = self.schema_for_object(type_)
|
|
|
|
if (
|
|
not self.omit_schema
|
|
and use_schema
|
|
and effective_schema is not None
|
|
):
|
|
name = self.quote_schema(effective_schema) + "." + name
|
|
return name
|
|
|
|
|
|
class PGInspector(reflection.Inspector):
|
|
def __init__(self, conn):
|
|
reflection.Inspector.__init__(self, conn)
|
|
|
|
def get_table_oid(self, table_name, schema=None):
|
|
"""Return the OID for the given table name."""
|
|
|
|
return self.dialect.get_table_oid(
|
|
self.bind, table_name, schema, info_cache=self.info_cache
|
|
)
|
|
|
|
def get_enums(self, schema=None):
|
|
"""Return a list of ENUM objects.
|
|
|
|
Each member is a dictionary containing these fields:
|
|
|
|
* name - name of the enum
|
|
* schema - the schema name for the enum.
|
|
* visible - boolean, whether or not this enum is visible
|
|
in the default search path.
|
|
* labels - a list of string labels that apply to the enum.
|
|
|
|
:param schema: schema name. If None, the default schema
|
|
(typically 'public') is used. May also be set to '*' to
|
|
indicate load enums for all schemas.
|
|
|
|
.. versionadded:: 1.0.0
|
|
|
|
"""
|
|
schema = schema or self.default_schema_name
|
|
return self.dialect._load_enums(self.bind, schema)
|
|
|
|
def get_foreign_table_names(self, schema=None):
|
|
"""Return a list of FOREIGN TABLE names.
|
|
|
|
Behavior is similar to that of :meth:`.Inspector.get_table_names`,
|
|
except that the list is limited to those tables that report a
|
|
``relkind`` value of ``f``.
|
|
|
|
.. versionadded:: 1.0.0
|
|
|
|
"""
|
|
schema = schema or self.default_schema_name
|
|
return self.dialect._get_foreign_table_names(self.bind, schema)
|
|
|
|
def get_view_names(self, schema=None, include=("plain", "materialized")):
|
|
"""Return all view names in `schema`.
|
|
|
|
:param schema: Optional, retrieve names from a non-default schema.
|
|
For special quoting, use :class:`.quoted_name`.
|
|
|
|
:param include: specify which types of views to return. Passed
|
|
as a string value (for a single type) or a tuple (for any number
|
|
of types). Defaults to ``('plain', 'materialized')``.
|
|
|
|
.. versionadded:: 1.1
|
|
|
|
"""
|
|
|
|
return self.dialect.get_view_names(
|
|
self.bind, schema, info_cache=self.info_cache, include=include
|
|
)
|
|
|
|
|
|
class CreateEnumType(schema._CreateDropBase):
|
|
__visit_name__ = "create_enum_type"
|
|
|
|
|
|
class DropEnumType(schema._CreateDropBase):
|
|
__visit_name__ = "drop_enum_type"
|
|
|
|
|
|
class PGExecutionContext(default.DefaultExecutionContext):
|
|
def fire_sequence(self, seq, type_):
|
|
return self._execute_scalar(
|
|
(
|
|
"select nextval('%s')"
|
|
% self.dialect.identifier_preparer.format_sequence(seq)
|
|
),
|
|
type_,
|
|
)
|
|
|
|
def get_insert_default(self, column):
|
|
if column.primary_key and column is column.table._autoincrement_column:
|
|
if column.server_default and column.server_default.has_argument:
|
|
|
|
# pre-execute passive defaults on primary key columns
|
|
return self._execute_scalar(
|
|
"select %s" % column.server_default.arg, column.type
|
|
)
|
|
|
|
elif column.default is None or (
|
|
column.default.is_sequence and column.default.optional
|
|
):
|
|
|
|
# execute the sequence associated with a SERIAL primary
|
|
# key column. for non-primary-key SERIAL, the ID just
|
|
# generates server side.
|
|
|
|
try:
|
|
seq_name = column._postgresql_seq_name
|
|
except AttributeError:
|
|
tab = column.table.name
|
|
col = column.name
|
|
tab = tab[0 : 29 + max(0, (29 - len(col)))]
|
|
col = col[0 : 29 + max(0, (29 - len(tab)))]
|
|
name = "%s_%s_seq" % (tab, col)
|
|
column._postgresql_seq_name = seq_name = name
|
|
|
|
if column.table is not None:
|
|
effective_schema = self.connection.schema_for_object(
|
|
column.table
|
|
)
|
|
else:
|
|
effective_schema = None
|
|
|
|
if effective_schema is not None:
|
|
exc = 'select nextval(\'"%s"."%s"\')' % (
|
|
effective_schema,
|
|
seq_name,
|
|
)
|
|
else:
|
|
exc = "select nextval('\"%s\"')" % (seq_name,)
|
|
|
|
return self._execute_scalar(exc, column.type)
|
|
|
|
return super(PGExecutionContext, self).get_insert_default(column)
|
|
|
|
def should_autocommit_text(self, statement):
|
|
return AUTOCOMMIT_REGEXP.match(statement)
|
|
|
|
|
|
class PGDialect(default.DefaultDialect):
|
|
name = "postgresql"
|
|
supports_alter = True
|
|
max_identifier_length = 63
|
|
supports_sane_rowcount = True
|
|
|
|
supports_native_enum = True
|
|
supports_native_boolean = True
|
|
supports_smallserial = True
|
|
|
|
supports_sequences = True
|
|
sequences_optional = True
|
|
preexecute_autoincrement_sequences = True
|
|
postfetch_lastrowid = False
|
|
|
|
supports_comments = True
|
|
supports_default_values = True
|
|
supports_empty_insert = False
|
|
supports_multivalues_insert = True
|
|
default_paramstyle = "pyformat"
|
|
ischema_names = ischema_names
|
|
colspecs = colspecs
|
|
|
|
statement_compiler = PGCompiler
|
|
ddl_compiler = PGDDLCompiler
|
|
type_compiler = PGTypeCompiler
|
|
preparer = PGIdentifierPreparer
|
|
execution_ctx_cls = PGExecutionContext
|
|
inspector = PGInspector
|
|
isolation_level = None
|
|
|
|
construct_arguments = [
|
|
(
|
|
schema.Index,
|
|
{
|
|
"using": False,
|
|
"where": None,
|
|
"ops": {},
|
|
"concurrently": False,
|
|
"with": {},
|
|
"tablespace": None,
|
|
},
|
|
),
|
|
(
|
|
schema.Table,
|
|
{
|
|
"ignore_search_path": False,
|
|
"tablespace": None,
|
|
"partition_by": None,
|
|
"with_oids": None,
|
|
"on_commit": None,
|
|
"inherits": None,
|
|
},
|
|
),
|
|
]
|
|
|
|
reflection_options = ("postgresql_ignore_search_path",)
|
|
|
|
_backslash_escapes = True
|
|
_supports_create_index_concurrently = True
|
|
_supports_drop_index_concurrently = True
|
|
|
|
def __init__(
|
|
self,
|
|
isolation_level=None,
|
|
json_serializer=None,
|
|
json_deserializer=None,
|
|
**kwargs
|
|
):
|
|
default.DefaultDialect.__init__(self, **kwargs)
|
|
self.isolation_level = isolation_level
|
|
self._json_deserializer = json_deserializer
|
|
self._json_serializer = json_serializer
|
|
|
|
def initialize(self, connection):
|
|
super(PGDialect, self).initialize(connection)
|
|
self.implicit_returning = self.server_version_info > (
|
|
8,
|
|
2,
|
|
) and self.__dict__.get("implicit_returning", True)
|
|
self.supports_native_enum = self.server_version_info >= (8, 3)
|
|
if not self.supports_native_enum:
|
|
self.colspecs = self.colspecs.copy()
|
|
# pop base Enum type
|
|
self.colspecs.pop(sqltypes.Enum, None)
|
|
# psycopg2, others may have placed ENUM here as well
|
|
self.colspecs.pop(ENUM, None)
|
|
|
|
# http://www.postgresql.org/docs/9.3/static/release-9-2.html#AEN116689
|
|
self.supports_smallserial = self.server_version_info >= (9, 2)
|
|
|
|
self._backslash_escapes = (
|
|
self.server_version_info < (8, 2)
|
|
or connection.scalar("show standard_conforming_strings") == "off"
|
|
)
|
|
|
|
self._supports_create_index_concurrently = (
|
|
self.server_version_info >= (8, 2)
|
|
)
|
|
self._supports_drop_index_concurrently = self.server_version_info >= (
|
|
9,
|
|
2,
|
|
)
|
|
|
|
def on_connect(self):
|
|
if self.isolation_level is not None:
|
|
|
|
def connect(conn):
|
|
self.set_isolation_level(conn, self.isolation_level)
|
|
|
|
return connect
|
|
else:
|
|
return None
|
|
|
|
_isolation_lookup = set(
|
|
[
|
|
"SERIALIZABLE",
|
|
"READ UNCOMMITTED",
|
|
"READ COMMITTED",
|
|
"REPEATABLE READ",
|
|
]
|
|
)
|
|
|
|
def set_isolation_level(self, connection, level):
|
|
level = level.replace("_", " ")
|
|
if level not in self._isolation_lookup:
|
|
raise exc.ArgumentError(
|
|
"Invalid value '%s' for isolation_level. "
|
|
"Valid isolation levels for %s are %s"
|
|
% (level, self.name, ", ".join(self._isolation_lookup))
|
|
)
|
|
cursor = connection.cursor()
|
|
cursor.execute(
|
|
"SET SESSION CHARACTERISTICS AS TRANSACTION "
|
|
"ISOLATION LEVEL %s" % level
|
|
)
|
|
cursor.execute("COMMIT")
|
|
cursor.close()
|
|
|
|
def get_isolation_level(self, connection):
|
|
cursor = connection.cursor()
|
|
cursor.execute("show transaction isolation level")
|
|
val = cursor.fetchone()[0]
|
|
cursor.close()
|
|
return val.upper()
|
|
|
|
def do_begin_twophase(self, connection, xid):
|
|
self.do_begin(connection.connection)
|
|
|
|
def do_prepare_twophase(self, connection, xid):
|
|
connection.execute("PREPARE TRANSACTION '%s'" % xid)
|
|
|
|
def do_rollback_twophase(
|
|
self, connection, xid, is_prepared=True, recover=False
|
|
):
|
|
if is_prepared:
|
|
if recover:
|
|
# FIXME: ugly hack to get out of transaction
|
|
# context when committing recoverable transactions
|
|
# Must find out a way how to make the dbapi not
|
|
# open a transaction.
|
|
connection.execute("ROLLBACK")
|
|
connection.execute("ROLLBACK PREPARED '%s'" % xid)
|
|
connection.execute("BEGIN")
|
|
self.do_rollback(connection.connection)
|
|
else:
|
|
self.do_rollback(connection.connection)
|
|
|
|
def do_commit_twophase(
|
|
self, connection, xid, is_prepared=True, recover=False
|
|
):
|
|
if is_prepared:
|
|
if recover:
|
|
connection.execute("ROLLBACK")
|
|
connection.execute("COMMIT PREPARED '%s'" % xid)
|
|
connection.execute("BEGIN")
|
|
self.do_rollback(connection.connection)
|
|
else:
|
|
self.do_commit(connection.connection)
|
|
|
|
def do_recover_twophase(self, connection):
|
|
resultset = connection.execute(
|
|
sql.text("SELECT gid FROM pg_prepared_xacts")
|
|
)
|
|
return [row[0] for row in resultset]
|
|
|
|
def _get_default_schema_name(self, connection):
|
|
return connection.scalar("select current_schema()")
|
|
|
|
def has_schema(self, connection, schema):
|
|
query = (
|
|
"select nspname from pg_namespace " "where lower(nspname)=:schema"
|
|
)
|
|
cursor = connection.execute(
|
|
sql.text(query).bindparams(
|
|
sql.bindparam(
|
|
"schema",
|
|
util.text_type(schema.lower()),
|
|
type_=sqltypes.Unicode,
|
|
)
|
|
)
|
|
)
|
|
|
|
return bool(cursor.first())
|
|
|
|
def has_table(self, connection, table_name, schema=None):
|
|
# seems like case gets folded in pg_class...
|
|
if schema is None:
|
|
cursor = connection.execute(
|
|
sql.text(
|
|
"select relname from pg_class c join pg_namespace n on "
|
|
"n.oid=c.relnamespace where "
|
|
"pg_catalog.pg_table_is_visible(c.oid) "
|
|
"and relname=:name"
|
|
).bindparams(
|
|
sql.bindparam(
|
|
"name",
|
|
util.text_type(table_name),
|
|
type_=sqltypes.Unicode,
|
|
)
|
|
)
|
|
)
|
|
else:
|
|
cursor = connection.execute(
|
|
sql.text(
|
|
"select relname from pg_class c join pg_namespace n on "
|
|
"n.oid=c.relnamespace where n.nspname=:schema and "
|
|
"relname=:name"
|
|
).bindparams(
|
|
sql.bindparam(
|
|
"name",
|
|
util.text_type(table_name),
|
|
type_=sqltypes.Unicode,
|
|
),
|
|
sql.bindparam(
|
|
"schema",
|
|
util.text_type(schema),
|
|
type_=sqltypes.Unicode,
|
|
),
|
|
)
|
|
)
|
|
return bool(cursor.first())
|
|
|
|
def has_sequence(self, connection, sequence_name, schema=None):
|
|
if schema is None:
|
|
cursor = connection.execute(
|
|
sql.text(
|
|
"SELECT relname FROM pg_class c join pg_namespace n on "
|
|
"n.oid=c.relnamespace where relkind='S' and "
|
|
"n.nspname=current_schema() "
|
|
"and relname=:name"
|
|
).bindparams(
|
|
sql.bindparam(
|
|
"name",
|
|
util.text_type(sequence_name),
|
|
type_=sqltypes.Unicode,
|
|
)
|
|
)
|
|
)
|
|
else:
|
|
cursor = connection.execute(
|
|
sql.text(
|
|
"SELECT relname FROM pg_class c join pg_namespace n on "
|
|
"n.oid=c.relnamespace where relkind='S' and "
|
|
"n.nspname=:schema and relname=:name"
|
|
).bindparams(
|
|
sql.bindparam(
|
|
"name",
|
|
util.text_type(sequence_name),
|
|
type_=sqltypes.Unicode,
|
|
),
|
|
sql.bindparam(
|
|
"schema",
|
|
util.text_type(schema),
|
|
type_=sqltypes.Unicode,
|
|
),
|
|
)
|
|
)
|
|
|
|
return bool(cursor.first())
|
|
|
|
def has_type(self, connection, type_name, schema=None):
|
|
if schema is not None:
|
|
query = """
|
|
SELECT EXISTS (
|
|
SELECT * FROM pg_catalog.pg_type t, pg_catalog.pg_namespace n
|
|
WHERE t.typnamespace = n.oid
|
|
AND t.typname = :typname
|
|
AND n.nspname = :nspname
|
|
)
|
|
"""
|
|
query = sql.text(query)
|
|
else:
|
|
query = """
|
|
SELECT EXISTS (
|
|
SELECT * FROM pg_catalog.pg_type t
|
|
WHERE t.typname = :typname
|
|
AND pg_type_is_visible(t.oid)
|
|
)
|
|
"""
|
|
query = sql.text(query)
|
|
query = query.bindparams(
|
|
sql.bindparam(
|
|
"typname", util.text_type(type_name), type_=sqltypes.Unicode
|
|
)
|
|
)
|
|
if schema is not None:
|
|
query = query.bindparams(
|
|
sql.bindparam(
|
|
"nspname", util.text_type(schema), type_=sqltypes.Unicode
|
|
)
|
|
)
|
|
cursor = connection.execute(query)
|
|
return bool(cursor.scalar())
|
|
|
|
def _get_server_version_info(self, connection):
|
|
v = connection.execute("select version()").scalar()
|
|
m = re.match(
|
|
r".*(?:PostgreSQL|EnterpriseDB) "
|
|
r"(\d+)\.?(\d+)?(?:\.(\d+))?(?:\.\d+)?(?:devel|beta)?",
|
|
v,
|
|
)
|
|
if not m:
|
|
raise AssertionError(
|
|
"Could not determine version from string '%s'" % v
|
|
)
|
|
return tuple([int(x) for x in m.group(1, 2, 3) if x is not None])
|
|
|
|
@reflection.cache
|
|
def get_table_oid(self, connection, table_name, schema=None, **kw):
|
|
"""Fetch the oid for schema.table_name.
|
|
|
|
Several reflection methods require the table oid. The idea for using
|
|
this method is that it can be fetched one time and cached for
|
|
subsequent calls.
|
|
|
|
"""
|
|
table_oid = None
|
|
if schema is not None:
|
|
schema_where_clause = "n.nspname = :schema"
|
|
else:
|
|
schema_where_clause = "pg_catalog.pg_table_is_visible(c.oid)"
|
|
query = (
|
|
"""
|
|
SELECT c.oid
|
|
FROM pg_catalog.pg_class c
|
|
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
WHERE (%s)
|
|
AND c.relname = :table_name AND c.relkind in
|
|
('r', 'v', 'm', 'f', 'p')
|
|
"""
|
|
% schema_where_clause
|
|
)
|
|
# Since we're binding to unicode, table_name and schema_name must be
|
|
# unicode.
|
|
table_name = util.text_type(table_name)
|
|
if schema is not None:
|
|
schema = util.text_type(schema)
|
|
s = sql.text(query).bindparams(table_name=sqltypes.Unicode)
|
|
s = s.columns(oid=sqltypes.Integer)
|
|
if schema:
|
|
s = s.bindparams(sql.bindparam("schema", type_=sqltypes.Unicode))
|
|
c = connection.execute(s, table_name=table_name, schema=schema)
|
|
table_oid = c.scalar()
|
|
if table_oid is None:
|
|
raise exc.NoSuchTableError(table_name)
|
|
return table_oid
|
|
|
|
@reflection.cache
|
|
def get_schema_names(self, connection, **kw):
|
|
result = connection.execute(
|
|
sql.text(
|
|
"SELECT nspname FROM pg_namespace "
|
|
"WHERE nspname NOT LIKE 'pg_%' "
|
|
"ORDER BY nspname"
|
|
).columns(nspname=sqltypes.Unicode)
|
|
)
|
|
return [name for name, in result]
|
|
|
|
@reflection.cache
|
|
def get_table_names(self, connection, schema=None, **kw):
|
|
result = connection.execute(
|
|
sql.text(
|
|
"SELECT c.relname FROM pg_class c "
|
|
"JOIN pg_namespace n ON n.oid = c.relnamespace "
|
|
"WHERE n.nspname = :schema AND c.relkind in ('r', 'p')"
|
|
).columns(relname=sqltypes.Unicode),
|
|
schema=schema if schema is not None else self.default_schema_name,
|
|
)
|
|
return [name for name, in result]
|
|
|
|
@reflection.cache
|
|
def _get_foreign_table_names(self, connection, schema=None, **kw):
|
|
result = connection.execute(
|
|
sql.text(
|
|
"SELECT c.relname FROM pg_class c "
|
|
"JOIN pg_namespace n ON n.oid = c.relnamespace "
|
|
"WHERE n.nspname = :schema AND c.relkind = 'f'"
|
|
).columns(relname=sqltypes.Unicode),
|
|
schema=schema if schema is not None else self.default_schema_name,
|
|
)
|
|
return [name for name, in result]
|
|
|
|
@reflection.cache
|
|
def get_view_names(
|
|
self, connection, schema=None, include=("plain", "materialized"), **kw
|
|
):
|
|
|
|
include_kind = {"plain": "v", "materialized": "m"}
|
|
try:
|
|
kinds = [include_kind[i] for i in util.to_list(include)]
|
|
except KeyError:
|
|
raise ValueError(
|
|
"include %r unknown, needs to be a sequence containing "
|
|
"one or both of 'plain' and 'materialized'" % (include,)
|
|
)
|
|
if not kinds:
|
|
raise ValueError(
|
|
"empty include, needs to be a sequence containing "
|
|
"one or both of 'plain' and 'materialized'"
|
|
)
|
|
|
|
result = connection.execute(
|
|
sql.text(
|
|
"SELECT c.relname FROM pg_class c "
|
|
"JOIN pg_namespace n ON n.oid = c.relnamespace "
|
|
"WHERE n.nspname = :schema AND c.relkind IN (%s)"
|
|
% (", ".join("'%s'" % elem for elem in kinds))
|
|
).columns(relname=sqltypes.Unicode),
|
|
schema=schema if schema is not None else self.default_schema_name,
|
|
)
|
|
return [name for name, in result]
|
|
|
|
@reflection.cache
|
|
def get_view_definition(self, connection, view_name, schema=None, **kw):
|
|
view_def = connection.scalar(
|
|
sql.text(
|
|
"SELECT pg_get_viewdef(c.oid) view_def FROM pg_class c "
|
|
"JOIN pg_namespace n ON n.oid = c.relnamespace "
|
|
"WHERE n.nspname = :schema AND c.relname = :view_name "
|
|
"AND c.relkind IN ('v', 'm')"
|
|
).columns(view_def=sqltypes.Unicode),
|
|
schema=schema if schema is not None else self.default_schema_name,
|
|
view_name=view_name,
|
|
)
|
|
return view_def
|
|
|
|
@reflection.cache
|
|
def get_columns(self, connection, table_name, schema=None, **kw):
|
|
|
|
table_oid = self.get_table_oid(
|
|
connection, table_name, schema, info_cache=kw.get("info_cache")
|
|
)
|
|
SQL_COLS = """
|
|
SELECT a.attname,
|
|
pg_catalog.format_type(a.atttypid, a.atttypmod),
|
|
(SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid)
|
|
FROM pg_catalog.pg_attrdef d
|
|
WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum
|
|
AND a.atthasdef)
|
|
AS DEFAULT,
|
|
a.attnotnull, a.attnum, a.attrelid as table_oid,
|
|
pgd.description as comment
|
|
FROM pg_catalog.pg_attribute a
|
|
LEFT JOIN pg_catalog.pg_description pgd ON (
|
|
pgd.objoid = a.attrelid AND pgd.objsubid = a.attnum)
|
|
WHERE a.attrelid = :table_oid
|
|
AND a.attnum > 0 AND NOT a.attisdropped
|
|
ORDER BY a.attnum
|
|
"""
|
|
s = (
|
|
sql.text(SQL_COLS)
|
|
.bindparams(sql.bindparam("table_oid", type_=sqltypes.Integer))
|
|
.columns(attname=sqltypes.Unicode, default=sqltypes.Unicode)
|
|
)
|
|
c = connection.execute(s, table_oid=table_oid)
|
|
rows = c.fetchall()
|
|
|
|
# dictionary with (name, ) if default search path or (schema, name)
|
|
# as keys
|
|
domains = self._load_domains(connection)
|
|
|
|
# dictionary with (name, ) if default search path or (schema, name)
|
|
# as keys
|
|
enums = dict(
|
|
((rec["name"],), rec)
|
|
if rec["visible"]
|
|
else ((rec["schema"], rec["name"]), rec)
|
|
for rec in self._load_enums(connection, schema="*")
|
|
)
|
|
|
|
# format columns
|
|
columns = []
|
|
|
|
for (
|
|
name,
|
|
format_type,
|
|
default_,
|
|
notnull,
|
|
attnum,
|
|
table_oid,
|
|
comment,
|
|
) in rows:
|
|
column_info = self._get_column_info(
|
|
name,
|
|
format_type,
|
|
default_,
|
|
notnull,
|
|
domains,
|
|
enums,
|
|
schema,
|
|
comment,
|
|
)
|
|
columns.append(column_info)
|
|
return columns
|
|
|
|
def _get_column_info(
|
|
self,
|
|
name,
|
|
format_type,
|
|
default,
|
|
notnull,
|
|
domains,
|
|
enums,
|
|
schema,
|
|
comment,
|
|
):
|
|
def _handle_array_type(attype):
|
|
return (
|
|
# strip '[]' from integer[], etc.
|
|
re.sub(r"\[\]$", "", attype),
|
|
attype.endswith("[]"),
|
|
)
|
|
|
|
# strip (*) from character varying(5), timestamp(5)
|
|
# with time zone, geometry(POLYGON), etc.
|
|
attype = re.sub(r"\(.*\)", "", format_type)
|
|
|
|
# strip '[]' from integer[], etc. and check if an array
|
|
attype, is_array = _handle_array_type(attype)
|
|
|
|
# strip quotes from case sensitive enum or domain names
|
|
enum_or_domain_key = tuple(util.quoted_token_parser(attype))
|
|
|
|
nullable = not notnull
|
|
|
|
charlen = re.search(r"\(([\d,]+)\)", format_type)
|
|
if charlen:
|
|
charlen = charlen.group(1)
|
|
args = re.search(r"\((.*)\)", format_type)
|
|
if args and args.group(1):
|
|
args = tuple(re.split(r"\s*,\s*", args.group(1)))
|
|
else:
|
|
args = ()
|
|
kwargs = {}
|
|
|
|
if attype == "numeric":
|
|
if charlen:
|
|
prec, scale = charlen.split(",")
|
|
args = (int(prec), int(scale))
|
|
else:
|
|
args = ()
|
|
elif attype == "double precision":
|
|
args = (53,)
|
|
elif attype == "integer":
|
|
args = ()
|
|
elif attype in ("timestamp with time zone", "time with time zone"):
|
|
kwargs["timezone"] = True
|
|
if charlen:
|
|
kwargs["precision"] = int(charlen)
|
|
args = ()
|
|
elif attype in (
|
|
"timestamp without time zone",
|
|
"time without time zone",
|
|
"time",
|
|
):
|
|
kwargs["timezone"] = False
|
|
if charlen:
|
|
kwargs["precision"] = int(charlen)
|
|
args = ()
|
|
elif attype == "bit varying":
|
|
kwargs["varying"] = True
|
|
if charlen:
|
|
args = (int(charlen),)
|
|
else:
|
|
args = ()
|
|
elif attype.startswith("interval"):
|
|
field_match = re.match(r"interval (.+)", attype, re.I)
|
|
if charlen:
|
|
kwargs["precision"] = int(charlen)
|
|
if field_match:
|
|
kwargs["fields"] = field_match.group(1)
|
|
attype = "interval"
|
|
args = ()
|
|
elif charlen:
|
|
args = (int(charlen),)
|
|
|
|
while True:
|
|
# looping here to suit nested domains
|
|
if attype in self.ischema_names:
|
|
coltype = self.ischema_names[attype]
|
|
break
|
|
elif enum_or_domain_key in enums:
|
|
enum = enums[enum_or_domain_key]
|
|
coltype = ENUM
|
|
kwargs["name"] = enum["name"]
|
|
if not enum["visible"]:
|
|
kwargs["schema"] = enum["schema"]
|
|
args = tuple(enum["labels"])
|
|
break
|
|
elif enum_or_domain_key in domains:
|
|
domain = domains[enum_or_domain_key]
|
|
attype = domain["attype"]
|
|
attype, is_array = _handle_array_type(attype)
|
|
# strip quotes from case sensitive enum or domain names
|
|
enum_or_domain_key = tuple(util.quoted_token_parser(attype))
|
|
# A table can't override whether the domain is nullable.
|
|
nullable = domain["nullable"]
|
|
if domain["default"] and not default:
|
|
# It can, however, override the default
|
|
# value, but can't set it to null.
|
|
default = domain["default"]
|
|
continue
|
|
else:
|
|
coltype = None
|
|
break
|
|
|
|
if coltype:
|
|
coltype = coltype(*args, **kwargs)
|
|
if is_array:
|
|
coltype = self.ischema_names["_array"](coltype)
|
|
else:
|
|
util.warn(
|
|
"Did not recognize type '%s' of column '%s'" % (attype, name)
|
|
)
|
|
coltype = sqltypes.NULLTYPE
|
|
# adjust the default value
|
|
autoincrement = False
|
|
if default is not None:
|
|
match = re.search(r"""(nextval\(')([^']+)('.*$)""", default)
|
|
if match is not None:
|
|
if issubclass(coltype._type_affinity, sqltypes.Integer):
|
|
autoincrement = True
|
|
# the default is related to a Sequence
|
|
sch = schema
|
|
if "." not in match.group(2) and sch is not None:
|
|
# unconditionally quote the schema name. this could
|
|
# later be enhanced to obey quoting rules /
|
|
# "quote schema"
|
|
default = (
|
|
match.group(1)
|
|
+ ('"%s"' % sch)
|
|
+ "."
|
|
+ match.group(2)
|
|
+ match.group(3)
|
|
)
|
|
|
|
column_info = dict(
|
|
name=name,
|
|
type=coltype,
|
|
nullable=nullable,
|
|
default=default,
|
|
autoincrement=autoincrement,
|
|
comment=comment,
|
|
)
|
|
return column_info
|
|
|
|
@reflection.cache
|
|
def get_pk_constraint(self, connection, table_name, schema=None, **kw):
|
|
table_oid = self.get_table_oid(
|
|
connection, table_name, schema, info_cache=kw.get("info_cache")
|
|
)
|
|
|
|
if self.server_version_info < (8, 4):
|
|
PK_SQL = """
|
|
SELECT a.attname
|
|
FROM
|
|
pg_class t
|
|
join pg_index ix on t.oid = ix.indrelid
|
|
join pg_attribute a
|
|
on t.oid=a.attrelid AND %s
|
|
WHERE
|
|
t.oid = :table_oid and ix.indisprimary = 't'
|
|
ORDER BY a.attnum
|
|
""" % self._pg_index_any(
|
|
"a.attnum", "ix.indkey"
|
|
)
|
|
|
|
else:
|
|
# unnest() and generate_subscripts() both introduced in
|
|
# version 8.4
|
|
PK_SQL = """
|
|
SELECT a.attname
|
|
FROM pg_attribute a JOIN (
|
|
SELECT unnest(ix.indkey) attnum,
|
|
generate_subscripts(ix.indkey, 1) ord
|
|
FROM pg_index ix
|
|
WHERE ix.indrelid = :table_oid AND ix.indisprimary
|
|
) k ON a.attnum=k.attnum
|
|
WHERE a.attrelid = :table_oid
|
|
ORDER BY k.ord
|
|
"""
|
|
t = sql.text(PK_SQL).columns(attname=sqltypes.Unicode)
|
|
c = connection.execute(t, table_oid=table_oid)
|
|
cols = [r[0] for r in c.fetchall()]
|
|
|
|
PK_CONS_SQL = """
|
|
SELECT conname
|
|
FROM pg_catalog.pg_constraint r
|
|
WHERE r.conrelid = :table_oid AND r.contype = 'p'
|
|
ORDER BY 1
|
|
"""
|
|
t = sql.text(PK_CONS_SQL).columns(conname=sqltypes.Unicode)
|
|
c = connection.execute(t, table_oid=table_oid)
|
|
name = c.scalar()
|
|
|
|
return {"constrained_columns": cols, "name": name}
|
|
|
|
@reflection.cache
|
|
def get_foreign_keys(
|
|
self,
|
|
connection,
|
|
table_name,
|
|
schema=None,
|
|
postgresql_ignore_search_path=False,
|
|
**kw
|
|
):
|
|
preparer = self.identifier_preparer
|
|
table_oid = self.get_table_oid(
|
|
connection, table_name, schema, info_cache=kw.get("info_cache")
|
|
)
|
|
|
|
FK_SQL = """
|
|
SELECT r.conname,
|
|
pg_catalog.pg_get_constraintdef(r.oid, true) as condef,
|
|
n.nspname as conschema
|
|
FROM pg_catalog.pg_constraint r,
|
|
pg_namespace n,
|
|
pg_class c
|
|
|
|
WHERE r.conrelid = :table AND
|
|
r.contype = 'f' AND
|
|
c.oid = confrelid AND
|
|
n.oid = c.relnamespace
|
|
ORDER BY 1
|
|
"""
|
|
# http://www.postgresql.org/docs/9.0/static/sql-createtable.html
|
|
FK_REGEX = re.compile(
|
|
r"FOREIGN KEY \((.*?)\) REFERENCES (?:(.*?)\.)?(.*?)\((.*?)\)"
|
|
r"[\s]?(MATCH (FULL|PARTIAL|SIMPLE)+)?"
|
|
r"[\s]?(ON UPDATE "
|
|
r"(CASCADE|RESTRICT|NO ACTION|SET NULL|SET DEFAULT)+)?"
|
|
r"[\s]?(ON DELETE "
|
|
r"(CASCADE|RESTRICT|NO ACTION|SET NULL|SET DEFAULT)+)?"
|
|
r"[\s]?(DEFERRABLE|NOT DEFERRABLE)?"
|
|
r"[\s]?(INITIALLY (DEFERRED|IMMEDIATE)+)?"
|
|
)
|
|
|
|
t = sql.text(FK_SQL).columns(
|
|
conname=sqltypes.Unicode, condef=sqltypes.Unicode
|
|
)
|
|
c = connection.execute(t, table=table_oid)
|
|
fkeys = []
|
|
for conname, condef, conschema in c.fetchall():
|
|
m = re.search(FK_REGEX, condef).groups()
|
|
|
|
(
|
|
constrained_columns,
|
|
referred_schema,
|
|
referred_table,
|
|
referred_columns,
|
|
_,
|
|
match,
|
|
_,
|
|
onupdate,
|
|
_,
|
|
ondelete,
|
|
deferrable,
|
|
_,
|
|
initially,
|
|
) = m
|
|
|
|
if deferrable is not None:
|
|
deferrable = True if deferrable == "DEFERRABLE" else False
|
|
constrained_columns = [
|
|
preparer._unquote_identifier(x)
|
|
for x in re.split(r"\s*,\s*", constrained_columns)
|
|
]
|
|
|
|
if postgresql_ignore_search_path:
|
|
# when ignoring search path, we use the actual schema
|
|
# provided it isn't the "default" schema
|
|
if conschema != self.default_schema_name:
|
|
referred_schema = conschema
|
|
else:
|
|
referred_schema = schema
|
|
elif referred_schema:
|
|
# referred_schema is the schema that we regexp'ed from
|
|
# pg_get_constraintdef(). If the schema is in the search
|
|
# path, pg_get_constraintdef() will give us None.
|
|
referred_schema = preparer._unquote_identifier(referred_schema)
|
|
elif schema is not None and schema == conschema:
|
|
# If the actual schema matches the schema of the table
|
|
# we're reflecting, then we will use that.
|
|
referred_schema = schema
|
|
|
|
referred_table = preparer._unquote_identifier(referred_table)
|
|
referred_columns = [
|
|
preparer._unquote_identifier(x)
|
|
for x in re.split(r"\s*,\s", referred_columns)
|
|
]
|
|
fkey_d = {
|
|
"name": conname,
|
|
"constrained_columns": constrained_columns,
|
|
"referred_schema": referred_schema,
|
|
"referred_table": referred_table,
|
|
"referred_columns": referred_columns,
|
|
"options": {
|
|
"onupdate": onupdate,
|
|
"ondelete": ondelete,
|
|
"deferrable": deferrable,
|
|
"initially": initially,
|
|
"match": match,
|
|
},
|
|
}
|
|
fkeys.append(fkey_d)
|
|
return fkeys
|
|
|
|
def _pg_index_any(self, col, compare_to):
|
|
if self.server_version_info < (8, 1):
|
|
# http://www.postgresql.org/message-id/10279.1124395722@sss.pgh.pa.us
|
|
# "In CVS tip you could replace this with "attnum = ANY (indkey)".
|
|
# Unfortunately, most array support doesn't work on int2vector in
|
|
# pre-8.1 releases, so I think you're kinda stuck with the above
|
|
# for now.
|
|
# regards, tom lane"
|
|
return "(%s)" % " OR ".join(
|
|
"%s[%d] = %s" % (compare_to, ind, col) for ind in range(0, 10)
|
|
)
|
|
else:
|
|
return "%s = ANY(%s)" % (col, compare_to)
|
|
|
|
@reflection.cache
|
|
def get_indexes(self, connection, table_name, schema, **kw):
|
|
table_oid = self.get_table_oid(
|
|
connection, table_name, schema, info_cache=kw.get("info_cache")
|
|
)
|
|
|
|
# cast indkey as varchar since it's an int2vector,
|
|
# returned as a list by some drivers such as pypostgresql
|
|
|
|
if self.server_version_info < (8, 5):
|
|
IDX_SQL = """
|
|
SELECT
|
|
i.relname as relname,
|
|
ix.indisunique, ix.indexprs, ix.indpred,
|
|
a.attname, a.attnum, NULL, ix.indkey%s,
|
|
%s, %s, am.amname
|
|
FROM
|
|
pg_class t
|
|
join pg_index ix on t.oid = ix.indrelid
|
|
join pg_class i on i.oid = ix.indexrelid
|
|
left outer join
|
|
pg_attribute a
|
|
on t.oid = a.attrelid and %s
|
|
left outer join
|
|
pg_am am
|
|
on i.relam = am.oid
|
|
WHERE
|
|
t.relkind IN ('r', 'v', 'f', 'm')
|
|
and t.oid = :table_oid
|
|
and ix.indisprimary = 'f'
|
|
ORDER BY
|
|
t.relname,
|
|
i.relname
|
|
""" % (
|
|
# version 8.3 here was based on observing the
|
|
# cast does not work in PG 8.2.4, does work in 8.3.0.
|
|
# nothing in PG changelogs regarding this.
|
|
"::varchar" if self.server_version_info >= (8, 3) else "",
|
|
"ix.indoption::varchar"
|
|
if self.server_version_info >= (8, 3)
|
|
else "NULL",
|
|
"i.reloptions"
|
|
if self.server_version_info >= (8, 2)
|
|
else "NULL",
|
|
self._pg_index_any("a.attnum", "ix.indkey"),
|
|
)
|
|
else:
|
|
IDX_SQL = """
|
|
SELECT
|
|
i.relname as relname,
|
|
ix.indisunique, ix.indexprs, ix.indpred,
|
|
a.attname, a.attnum, c.conrelid, ix.indkey::varchar,
|
|
ix.indoption::varchar, i.reloptions, am.amname
|
|
FROM
|
|
pg_class t
|
|
join pg_index ix on t.oid = ix.indrelid
|
|
join pg_class i on i.oid = ix.indexrelid
|
|
left outer join
|
|
pg_attribute a
|
|
on t.oid = a.attrelid and a.attnum = ANY(ix.indkey)
|
|
left outer join
|
|
pg_constraint c
|
|
on (ix.indrelid = c.conrelid and
|
|
ix.indexrelid = c.conindid and
|
|
c.contype in ('p', 'u', 'x'))
|
|
left outer join
|
|
pg_am am
|
|
on i.relam = am.oid
|
|
WHERE
|
|
t.relkind IN ('r', 'v', 'f', 'm', 'p')
|
|
and t.oid = :table_oid
|
|
and ix.indisprimary = 'f'
|
|
ORDER BY
|
|
t.relname,
|
|
i.relname
|
|
"""
|
|
|
|
t = sql.text(IDX_SQL).columns(
|
|
relname=sqltypes.Unicode, attname=sqltypes.Unicode
|
|
)
|
|
c = connection.execute(t, table_oid=table_oid)
|
|
|
|
indexes = defaultdict(lambda: defaultdict(dict))
|
|
|
|
sv_idx_name = None
|
|
for row in c.fetchall():
|
|
(
|
|
idx_name,
|
|
unique,
|
|
expr,
|
|
prd,
|
|
col,
|
|
col_num,
|
|
conrelid,
|
|
idx_key,
|
|
idx_option,
|
|
options,
|
|
amname,
|
|
) = row
|
|
|
|
if expr:
|
|
if idx_name != sv_idx_name:
|
|
util.warn(
|
|
"Skipped unsupported reflection of "
|
|
"expression-based index %s" % idx_name
|
|
)
|
|
sv_idx_name = idx_name
|
|
continue
|
|
|
|
if prd and not idx_name == sv_idx_name:
|
|
util.warn(
|
|
"Predicate of partial index %s ignored during reflection"
|
|
% idx_name
|
|
)
|
|
sv_idx_name = idx_name
|
|
|
|
has_idx = idx_name in indexes
|
|
index = indexes[idx_name]
|
|
if col is not None:
|
|
index["cols"][col_num] = col
|
|
if not has_idx:
|
|
index["key"] = [int(k.strip()) for k in idx_key.split()]
|
|
|
|
# (new in pg 8.3)
|
|
# "pg_index.indoption" is list of ints, one per column/expr.
|
|
# int acts as bitmask: 0x01=DESC, 0x02=NULLSFIRST
|
|
sorting = {}
|
|
for col_idx, col_flags in enumerate(
|
|
(idx_option or "").split()
|
|
):
|
|
col_flags = int(col_flags.strip())
|
|
col_sorting = ()
|
|
# try to set flags only if they differ from PG defaults...
|
|
if col_flags & 0x01:
|
|
col_sorting += ("desc",)
|
|
if not (col_flags & 0x02):
|
|
col_sorting += ("nullslast",)
|
|
else:
|
|
if col_flags & 0x02:
|
|
col_sorting += ("nullsfirst",)
|
|
if col_sorting:
|
|
sorting[col_idx] = col_sorting
|
|
if sorting:
|
|
index["sorting"] = sorting
|
|
|
|
index["unique"] = unique
|
|
if conrelid is not None:
|
|
index["duplicates_constraint"] = idx_name
|
|
if options:
|
|
index["options"] = dict(
|
|
[option.split("=") for option in options]
|
|
)
|
|
|
|
# it *might* be nice to include that this is 'btree' in the
|
|
# reflection info. But we don't want an Index object
|
|
# to have a ``postgresql_using`` in it that is just the
|
|
# default, so for the moment leaving this out.
|
|
if amname and amname != "btree":
|
|
index["amname"] = amname
|
|
|
|
result = []
|
|
for name, idx in indexes.items():
|
|
entry = {
|
|
"name": name,
|
|
"unique": idx["unique"],
|
|
"column_names": [idx["cols"][i] for i in idx["key"]],
|
|
}
|
|
if "duplicates_constraint" in idx:
|
|
entry["duplicates_constraint"] = idx["duplicates_constraint"]
|
|
if "sorting" in idx:
|
|
entry["column_sorting"] = dict(
|
|
(idx["cols"][idx["key"][i]], value)
|
|
for i, value in idx["sorting"].items()
|
|
)
|
|
if "options" in idx:
|
|
entry.setdefault("dialect_options", {})[
|
|
"postgresql_with"
|
|
] = idx["options"]
|
|
if "amname" in idx:
|
|
entry.setdefault("dialect_options", {})[
|
|
"postgresql_using"
|
|
] = idx["amname"]
|
|
result.append(entry)
|
|
return result
|
|
|
|
@reflection.cache
|
|
def get_unique_constraints(
|
|
self, connection, table_name, schema=None, **kw
|
|
):
|
|
table_oid = self.get_table_oid(
|
|
connection, table_name, schema, info_cache=kw.get("info_cache")
|
|
)
|
|
|
|
UNIQUE_SQL = """
|
|
SELECT
|
|
cons.conname as name,
|
|
cons.conkey as key,
|
|
a.attnum as col_num,
|
|
a.attname as col_name
|
|
FROM
|
|
pg_catalog.pg_constraint cons
|
|
join pg_attribute a
|
|
on cons.conrelid = a.attrelid AND
|
|
a.attnum = ANY(cons.conkey)
|
|
WHERE
|
|
cons.conrelid = :table_oid AND
|
|
cons.contype = 'u'
|
|
"""
|
|
|
|
t = sql.text(UNIQUE_SQL).columns(col_name=sqltypes.Unicode)
|
|
c = connection.execute(t, table_oid=table_oid)
|
|
|
|
uniques = defaultdict(lambda: defaultdict(dict))
|
|
for row in c.fetchall():
|
|
uc = uniques[row.name]
|
|
uc["key"] = row.key
|
|
uc["cols"][row.col_num] = row.col_name
|
|
|
|
return [
|
|
{"name": name, "column_names": [uc["cols"][i] for i in uc["key"]]}
|
|
for name, uc in uniques.items()
|
|
]
|
|
|
|
@reflection.cache
|
|
def get_table_comment(self, connection, table_name, schema=None, **kw):
|
|
table_oid = self.get_table_oid(
|
|
connection, table_name, schema, info_cache=kw.get("info_cache")
|
|
)
|
|
|
|
COMMENT_SQL = """
|
|
SELECT
|
|
pgd.description as table_comment
|
|
FROM
|
|
pg_catalog.pg_description pgd
|
|
WHERE
|
|
pgd.objsubid = 0 AND
|
|
pgd.objoid = :table_oid
|
|
"""
|
|
|
|
c = connection.execute(sql.text(COMMENT_SQL), table_oid=table_oid)
|
|
return {"text": c.scalar()}
|
|
|
|
@reflection.cache
|
|
def get_check_constraints(self, connection, table_name, schema=None, **kw):
|
|
table_oid = self.get_table_oid(
|
|
connection, table_name, schema, info_cache=kw.get("info_cache")
|
|
)
|
|
|
|
CHECK_SQL = """
|
|
SELECT
|
|
cons.conname as name,
|
|
pg_get_constraintdef(cons.oid) as src
|
|
FROM
|
|
pg_catalog.pg_constraint cons
|
|
WHERE
|
|
cons.conrelid = :table_oid AND
|
|
cons.contype = 'c'
|
|
"""
|
|
|
|
c = connection.execute(sql.text(CHECK_SQL), table_oid=table_oid)
|
|
|
|
ret = []
|
|
for name, src in c:
|
|
# samples:
|
|
# "CHECK (((a > 1) AND (a < 5)))"
|
|
# "CHECK (((a = 1) OR ((a > 2) AND (a < 5))))"
|
|
# "CHECK (((a > 1) AND (a < 5))) NOT VALID"
|
|
m = re.match(r"^CHECK *\(\((.+)\)\)( NOT VALID)?$", src)
|
|
if not m:
|
|
util.warn("Could not parse CHECK constraint text: %r" % src)
|
|
sqltext = ""
|
|
else:
|
|
sqltext = m.group(1)
|
|
entry = {"name": name, "sqltext": sqltext}
|
|
if m and m.group(2):
|
|
entry["dialect_options"] = {"not_valid": True}
|
|
|
|
ret.append(entry)
|
|
return ret
|
|
|
|
def _load_enums(self, connection, schema=None):
|
|
schema = schema or self.default_schema_name
|
|
if not self.supports_native_enum:
|
|
return {}
|
|
|
|
# Load data types for enums:
|
|
SQL_ENUMS = """
|
|
SELECT t.typname as "name",
|
|
-- no enum defaults in 8.4 at least
|
|
-- t.typdefault as "default",
|
|
pg_catalog.pg_type_is_visible(t.oid) as "visible",
|
|
n.nspname as "schema",
|
|
e.enumlabel as "label"
|
|
FROM pg_catalog.pg_type t
|
|
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
LEFT JOIN pg_catalog.pg_enum e ON t.oid = e.enumtypid
|
|
WHERE t.typtype = 'e'
|
|
"""
|
|
|
|
if schema != "*":
|
|
SQL_ENUMS += "AND n.nspname = :schema "
|
|
|
|
# e.oid gives us label order within an enum
|
|
SQL_ENUMS += 'ORDER BY "schema", "name", e.oid'
|
|
|
|
s = sql.text(SQL_ENUMS).columns(
|
|
attname=sqltypes.Unicode, label=sqltypes.Unicode
|
|
)
|
|
|
|
if schema != "*":
|
|
s = s.bindparams(schema=schema)
|
|
|
|
c = connection.execute(s)
|
|
|
|
enums = []
|
|
enum_by_name = {}
|
|
for enum in c.fetchall():
|
|
key = (enum["schema"], enum["name"])
|
|
if key in enum_by_name:
|
|
enum_by_name[key]["labels"].append(enum["label"])
|
|
else:
|
|
enum_by_name[key] = enum_rec = {
|
|
"name": enum["name"],
|
|
"schema": enum["schema"],
|
|
"visible": enum["visible"],
|
|
"labels": [],
|
|
}
|
|
if enum["label"] is not None:
|
|
enum_rec["labels"].append(enum["label"])
|
|
enums.append(enum_rec)
|
|
return enums
|
|
|
|
def _load_domains(self, connection):
|
|
# Load data types for domains:
|
|
SQL_DOMAINS = """
|
|
SELECT t.typname as "name",
|
|
pg_catalog.format_type(t.typbasetype, t.typtypmod) as "attype",
|
|
not t.typnotnull as "nullable",
|
|
t.typdefault as "default",
|
|
pg_catalog.pg_type_is_visible(t.oid) as "visible",
|
|
n.nspname as "schema"
|
|
FROM pg_catalog.pg_type t
|
|
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
WHERE t.typtype = 'd'
|
|
"""
|
|
|
|
s = sql.text(SQL_DOMAINS).columns(attname=sqltypes.Unicode)
|
|
c = connection.execute(s)
|
|
|
|
domains = {}
|
|
for domain in c.fetchall():
|
|
# strip (30) from character varying(30)
|
|
attype = re.search(r"([^\(]+)", domain["attype"]).group(1)
|
|
# 'visible' just means whether or not the domain is in a
|
|
# schema that's on the search path -- or not overridden by
|
|
# a schema with higher precedence. If it's not visible,
|
|
# it will be prefixed with the schema-name when it's used.
|
|
if domain["visible"]:
|
|
key = (domain["name"],)
|
|
else:
|
|
key = (domain["schema"], domain["name"])
|
|
|
|
domains[key] = {
|
|
"attype": attype,
|
|
"nullable": domain["nullable"],
|
|
"default": domain["default"],
|
|
}
|
|
|
|
return domains
|