mtsql 1.11.14__py3-none-any.whl → 1.11.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mt/sql/redshift/__init__.py +26 -0
- mt/sql/redshift/ddl.py +264 -0
- mt/sql/redshift/dialect.py +1545 -0
- mt/sql/redshift/redshift-ca-bundle.crt +145 -0
- mt/sql/version.py +1 -1
- {mtsql-1.11.14.dist-info → mtsql-1.11.17.dist-info}/METADATA +3 -3
- mtsql-1.11.17.dist-info/RECORD +16 -0
- mtsql-1.11.14.dist-info/RECORD +0 -13
- {mtsql-1.11.14.dist-info → mtsql-1.11.17.dist-info}/LICENSE +0 -0
- {mtsql-1.11.14.dist-info → mtsql-1.11.17.dist-info}/WHEEL +0 -0
- {mtsql-1.11.14.dist-info → mtsql-1.11.17.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1545 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from collections import defaultdict, namedtuple
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
|
|
7
|
+
import pkg_resources
|
|
8
|
+
import sqlalchemy as sa
|
|
9
|
+
from packaging.version import Version
|
|
10
|
+
from sqlalchemy import inspect
|
|
11
|
+
from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION
|
|
12
|
+
from sqlalchemy.dialects.postgresql.base import (PGCompiler, PGDDLCompiler,
|
|
13
|
+
PGDialect, PGExecutionContext,
|
|
14
|
+
PGIdentifierPreparer,
|
|
15
|
+
PGTypeCompiler)
|
|
16
|
+
from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
|
|
17
|
+
from sqlalchemy.dialects.postgresql.psycopg2cffi import PGDialect_psycopg2cffi
|
|
18
|
+
from sqlalchemy.engine import reflection
|
|
19
|
+
from sqlalchemy.engine.default import DefaultDialect
|
|
20
|
+
from sqlalchemy.ext.compiler import compiles
|
|
21
|
+
from sqlalchemy.sql.expression import (BinaryExpression, BooleanClauseList,
|
|
22
|
+
Delete)
|
|
23
|
+
from sqlalchemy.sql.type_api import TypeEngine
|
|
24
|
+
from sqlalchemy.types import (BIGINT, BOOLEAN, CHAR, DATE, DECIMAL, INTEGER,
|
|
25
|
+
REAL, SMALLINT, TIMESTAMP, VARCHAR, NullType)
|
|
26
|
+
|
|
27
|
+
from .commands import (AlterTableAppendCommand, Compression, CopyCommand,
|
|
28
|
+
CreateLibraryCommand, Encoding, Format,
|
|
29
|
+
RefreshMaterializedView, UnloadFromSelect)
|
|
30
|
+
from .ddl import (CreateMaterializedView, DropMaterializedView,
|
|
31
|
+
get_table_attributes)
|
|
32
|
+
|
|
33
|
+
sa_version = Version(sa.__version__)
|
|
34
|
+
logger = getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
import alembic
|
|
38
|
+
except ImportError:
|
|
39
|
+
pass
|
|
40
|
+
else:
|
|
41
|
+
from alembic.ddl import postgresql
|
|
42
|
+
from alembic.ddl.base import RenameTable
|
|
43
|
+
compiles(RenameTable, 'redshift')(postgresql.visit_rename_table)
|
|
44
|
+
|
|
45
|
+
if Version(alembic.__version__) >= Version('1.0.6'):
|
|
46
|
+
from alembic.ddl.base import ColumnComment
|
|
47
|
+
compiles(ColumnComment, 'redshift')(postgresql.visit_column_comment)
|
|
48
|
+
|
|
49
|
+
class RedshiftImpl(postgresql.PostgresqlImpl):
|
|
50
|
+
__dialect__ = 'redshift'
|
|
51
|
+
|
|
52
|
+
# "Each dialect provides the full set of typenames supported by that backend
|
|
53
|
+
# with its __all__ collection
|
|
54
|
+
# https://docs.sqlalchemy.org/en/13/core/type_basics.html#vendor-specific-types
|
|
55
|
+
__all__ = (
|
|
56
|
+
'SMALLINT',
|
|
57
|
+
'INTEGER',
|
|
58
|
+
'BIGINT',
|
|
59
|
+
'DECIMAL',
|
|
60
|
+
'REAL',
|
|
61
|
+
'BOOLEAN',
|
|
62
|
+
'CHAR',
|
|
63
|
+
'DATE',
|
|
64
|
+
'TIMESTAMP',
|
|
65
|
+
'VARCHAR',
|
|
66
|
+
'DOUBLE_PRECISION',
|
|
67
|
+
'GEOMETRY',
|
|
68
|
+
'SUPER',
|
|
69
|
+
'TIMESTAMPTZ',
|
|
70
|
+
'TIMETZ',
|
|
71
|
+
'HLLSKETCH',
|
|
72
|
+
|
|
73
|
+
'RedshiftDialect', 'RedshiftDialect_psycopg2',
|
|
74
|
+
'RedshiftDialect_psycopg2cffi', 'RedshiftDialect_redshift_connector',
|
|
75
|
+
|
|
76
|
+
'CopyCommand', 'UnloadFromSelect', 'Compression',
|
|
77
|
+
'Encoding', 'Format', 'CreateLibraryCommand', 'AlterTableAppendCommand',
|
|
78
|
+
'RefreshMaterializedView',
|
|
79
|
+
|
|
80
|
+
'CreateMaterializedView', 'DropMaterializedView'
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Regex for parsing and identity constraint out of adsrc, e.g.:
|
|
85
|
+
# "identity"(445178, 0, '1,1'::text)
|
|
86
|
+
IDENTITY_RE = re.compile(r"""
|
|
87
|
+
"identity" \(
|
|
88
|
+
(?P<current>-?\d+)
|
|
89
|
+
,\s
|
|
90
|
+
(?P<base>-?\d+)
|
|
91
|
+
,\s
|
|
92
|
+
'(?P<seed>-?\d+),(?P<step>-?\d+)'
|
|
93
|
+
.*
|
|
94
|
+
\)
|
|
95
|
+
""", re.VERBOSE)
|
|
96
|
+
|
|
97
|
+
# Regex for SQL identifiers (valid table and column names)
|
|
98
|
+
SQL_IDENTIFIER_RE = re.compile(r"""
|
|
99
|
+
[_a-zA-Z][\w$]* # SQL standard identifier
|
|
100
|
+
| # or
|
|
101
|
+
(?:"[^"]+")+ # SQL delimited (quoted) identifier
|
|
102
|
+
""", re.VERBOSE)
|
|
103
|
+
|
|
104
|
+
# Regex for foreign key constraints, e.g.:
|
|
105
|
+
# FOREIGN KEY(col1) REFERENCES othertable (col2)
|
|
106
|
+
# See https://docs.aws.amazon.com/redshift/latest/dg/r_names.html
|
|
107
|
+
# for a definition of valid SQL identifiers.
|
|
108
|
+
FOREIGN_KEY_RE = re.compile(r"""
|
|
109
|
+
^FOREIGN\ KEY \s* \( # FOREIGN KEY, arbitrary whitespace, literal '('
|
|
110
|
+
(?P<columns> # Start a group to capture the referring columns
|
|
111
|
+
(?: # Start a non-capturing group
|
|
112
|
+
\s* # Arbitrary whitespace
|
|
113
|
+
([_a-zA-Z][\w$]* | ("[^"]+")+) # SQL identifier
|
|
114
|
+
\s* # Arbitrary whitespace
|
|
115
|
+
,? # There will be a colon if this isn't the last one
|
|
116
|
+
)+ # Close the non-capturing group; require at least one
|
|
117
|
+
) # Close the 'columns' group
|
|
118
|
+
\s* \) # Arbitrary whitespace and literal ')'
|
|
119
|
+
\s* REFERENCES \s*
|
|
120
|
+
((?P<referred_schema>([_a-zA-Z][\w$]* | ("[^"]*")+))\.)? # SQL identifier
|
|
121
|
+
(?P<referred_table>[_a-zA-Z][\w$]* | ("[^"]*")+) # SQL identifier
|
|
122
|
+
\s* \( # FOREIGN KEY, arbitrary whitespace, literal '('
|
|
123
|
+
(?P<referred_columns> # Start a group to capture the referring columns
|
|
124
|
+
(?: # Start a non-capturing group
|
|
125
|
+
\s* # Arbitrary whitespace
|
|
126
|
+
([_a-zA-Z][\w$]* | ("[^"]+")+) # SQL identifier
|
|
127
|
+
\s* # Arbitrary whitespace
|
|
128
|
+
,? # There will be a colon if this isn't the last one
|
|
129
|
+
)+ # Close the non-capturing group; require at least one
|
|
130
|
+
) # Close the 'columns' group
|
|
131
|
+
\s* \) # Arbitrary whitespace and literal ')'
|
|
132
|
+
""", re.VERBOSE)
|
|
133
|
+
|
|
134
|
+
# Regex for primary key constraints, e.g.:
|
|
135
|
+
# PRIMARY KEY (col1, col2)
|
|
136
|
+
PRIMARY_KEY_RE = re.compile(r"""
|
|
137
|
+
^PRIMARY \s* KEY \s* \( # FOREIGN KEY, arbitrary whitespace, literal '('
|
|
138
|
+
(?P<columns> # Start a group to capture column names
|
|
139
|
+
(?:
|
|
140
|
+
\s* # Arbitrary whitespace
|
|
141
|
+
# SQL identifier or delimited identifier
|
|
142
|
+
( [_a-zA-Z][\w$]* | ("[^"]*")+ )
|
|
143
|
+
\s* # Arbitrary whitespace
|
|
144
|
+
,? # There will be a colon if this isn't the last one
|
|
145
|
+
)+ # Close the non-capturing group; require at least one
|
|
146
|
+
)
|
|
147
|
+
\s* \) \s* # Arbitrary whitespace and literal ')'
|
|
148
|
+
""", re.VERBOSE)
|
|
149
|
+
|
|
150
|
+
# Reserved words as extracted from Redshift docs.
|
|
151
|
+
# See pull_reserved_words.sh at the top level of this repository
|
|
152
|
+
# for the code used to generate this set.
|
|
153
|
+
RESERVED_WORDS = set([
|
|
154
|
+
"aes128", "aes256", "all", "allowoverwrite", "analyse", "analyze",
|
|
155
|
+
"and", "any", "array", "as", "asc", "authorization", "az64",
|
|
156
|
+
"backup", "between", "binary", "blanksasnull", "both", "bytedict",
|
|
157
|
+
"bzip2", "case", "cast", "check", "collate", "column", "constraint",
|
|
158
|
+
"create", "credentials", "cross", "current_date", "current_time",
|
|
159
|
+
"current_timestamp", "current_user", "current_user_id", "default",
|
|
160
|
+
"deferrable", "deflate", "defrag", "delta", "delta32k", "desc",
|
|
161
|
+
"disable", "distinct", "do", "else", "emptyasnull", "enable",
|
|
162
|
+
"encode", "encrypt", "encryption", "end", "except", "explicit",
|
|
163
|
+
"false", "for", "foreign", "freeze", "from", "full", "globaldict256",
|
|
164
|
+
"globaldict64k", "grant", "group", "gzip", "having", "identity",
|
|
165
|
+
"ignore", "ilike", "in", "initially", "inner", "intersect", "into",
|
|
166
|
+
"is", "isnull", "join", "language", "leading", "left", "like",
|
|
167
|
+
"limit", "localtime", "localtimestamp", "lun", "luns", "lzo", "lzop",
|
|
168
|
+
"minus", "mostly16", "mostly32", "mostly8", "natural", "new", "not",
|
|
169
|
+
"notnull", "null", "nulls", "off", "offline", "offset", "oid", "old",
|
|
170
|
+
"on", "only", "open", "or", "order", "outer", "overlaps", "parallel",
|
|
171
|
+
"partition", "percent", "permissions", "pivot", "placing", "primary",
|
|
172
|
+
"raw", "readratio", "recover", "references", "respect", "rejectlog",
|
|
173
|
+
"resort", "restore", "right", "select", "session_user", "similar",
|
|
174
|
+
"snapshot", "some", "sysdate", "system", "table", "tag", "tdes",
|
|
175
|
+
"text255", "text32k", "then", "timestamp", "to", "top", "trailing",
|
|
176
|
+
"true", "truncatecolumns", "union", "unique", "unnest", "unpivot",
|
|
177
|
+
"user", "using", "verbose", "wallet", "when", "where", "with",
|
|
178
|
+
"without",
|
|
179
|
+
])
|
|
180
|
+
|
|
181
|
+
REFLECTION_SQL = """\
|
|
182
|
+
SELECT
|
|
183
|
+
n.nspname as "schema",
|
|
184
|
+
c.relname as "table_name",
|
|
185
|
+
att.attname as "name",
|
|
186
|
+
format_encoding(att.attencodingtype::integer) as "encode",
|
|
187
|
+
format_type(att.atttypid, att.atttypmod) as "type",
|
|
188
|
+
att.attisdistkey as "distkey",
|
|
189
|
+
att.attsortkeyord as "sortkey",
|
|
190
|
+
att.attnotnull as "notnull",
|
|
191
|
+
pg_catalog.col_description(att.attrelid, att.attnum)
|
|
192
|
+
as "comment",
|
|
193
|
+
adsrc,
|
|
194
|
+
attnum,
|
|
195
|
+
pg_catalog.format_type(att.atttypid, att.atttypmod),
|
|
196
|
+
pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) AS DEFAULT,
|
|
197
|
+
n.oid as "schema_oid",
|
|
198
|
+
c.oid as "table_oid"
|
|
199
|
+
FROM pg_catalog.pg_class c
|
|
200
|
+
LEFT JOIN pg_catalog.pg_namespace n
|
|
201
|
+
ON n.oid = c.relnamespace
|
|
202
|
+
JOIN pg_catalog.pg_attribute att
|
|
203
|
+
ON att.attrelid = c.oid
|
|
204
|
+
LEFT JOIN pg_catalog.pg_attrdef ad
|
|
205
|
+
ON (att.attrelid, att.attnum) = (ad.adrelid, ad.adnum)
|
|
206
|
+
WHERE n.nspname !~ '^pg_'
|
|
207
|
+
AND att.attnum > 0
|
|
208
|
+
AND NOT att.attisdropped
|
|
209
|
+
{schema_clause} {table_clause}
|
|
210
|
+
UNION
|
|
211
|
+
SELECT
|
|
212
|
+
view_schema as "schema",
|
|
213
|
+
view_name as "table_name",
|
|
214
|
+
col_name as "name",
|
|
215
|
+
null as "encode",
|
|
216
|
+
col_type as "type",
|
|
217
|
+
null as "distkey",
|
|
218
|
+
0 as "sortkey",
|
|
219
|
+
null as "notnull",
|
|
220
|
+
null as "comment",
|
|
221
|
+
null as "adsrc",
|
|
222
|
+
null as "attnum",
|
|
223
|
+
col_type as "format_type",
|
|
224
|
+
null as "default",
|
|
225
|
+
null as "schema_oid",
|
|
226
|
+
null as "table_oid"
|
|
227
|
+
FROM pg_get_late_binding_view_cols() cols(
|
|
228
|
+
view_schema name,
|
|
229
|
+
view_name name,
|
|
230
|
+
col_name name,
|
|
231
|
+
col_type varchar,
|
|
232
|
+
col_num int)
|
|
233
|
+
WHERE 1 {schema_clause} {table_clause}
|
|
234
|
+
UNION
|
|
235
|
+
SELECT c.schemaname AS "schema",
|
|
236
|
+
c.tablename AS "table_name",
|
|
237
|
+
c.columnname AS "name",
|
|
238
|
+
null AS "encode",
|
|
239
|
+
-- Spectrum represents data types differently.
|
|
240
|
+
-- Standardize, so we can infer types.
|
|
241
|
+
CASE
|
|
242
|
+
WHEN c.external_type = 'int' THEN 'integer'
|
|
243
|
+
WHEN c.external_type = 'float' THEN 'real'
|
|
244
|
+
WHEN c.external_type = 'double' THEN 'double precision'
|
|
245
|
+
WHEN c.external_type = 'timestamp'
|
|
246
|
+
THEN 'timestamp without time zone'
|
|
247
|
+
WHEN c.external_type ilike 'varchar%'
|
|
248
|
+
THEN replace(c.external_type, 'varchar', 'character varying')
|
|
249
|
+
WHEN c.external_type ilike 'decimal%'
|
|
250
|
+
THEN replace(c.external_type, 'decimal', 'numeric')
|
|
251
|
+
ELSE
|
|
252
|
+
replace(
|
|
253
|
+
replace(
|
|
254
|
+
replace(c.external_type, 'decimal', 'numeric'),
|
|
255
|
+
'char', 'character'),
|
|
256
|
+
'varchar', 'character varying')
|
|
257
|
+
END
|
|
258
|
+
AS "type",
|
|
259
|
+
false AS "distkey",
|
|
260
|
+
0 AS "sortkey",
|
|
261
|
+
null AS "notnull",
|
|
262
|
+
null as "comment",
|
|
263
|
+
null AS "adsrc",
|
|
264
|
+
c.columnnum AS "attnum",
|
|
265
|
+
CASE
|
|
266
|
+
WHEN c.external_type = 'int' THEN 'integer'
|
|
267
|
+
WHEN c.external_type = 'float' THEN 'real'
|
|
268
|
+
WHEN c.external_type = 'double' THEN 'double precision'
|
|
269
|
+
WHEN c.external_type = 'timestamp'
|
|
270
|
+
THEN 'timestamp without time zone'
|
|
271
|
+
WHEN c.external_type ilike 'varchar%'
|
|
272
|
+
THEN replace(c.external_type, 'varchar', 'character varying')
|
|
273
|
+
WHEN c.external_type ilike 'decimal%'
|
|
274
|
+
THEN replace(c.external_type, 'decimal', 'numeric')
|
|
275
|
+
ELSE
|
|
276
|
+
replace(
|
|
277
|
+
replace(
|
|
278
|
+
replace(c.external_type, 'decimal', 'numeric'),
|
|
279
|
+
'char', 'character'),
|
|
280
|
+
'varchar', 'character varying')
|
|
281
|
+
END
|
|
282
|
+
AS "format_type",
|
|
283
|
+
null AS "default",
|
|
284
|
+
s.esoid AS "schema_oid",
|
|
285
|
+
null AS "table_oid"
|
|
286
|
+
FROM svv_external_columns c
|
|
287
|
+
JOIN svv_external_schemas s ON s.schemaname = c.schemaname
|
|
288
|
+
WHERE 1 {schema_clause} {table_clause}
|
|
289
|
+
ORDER BY "schema", "table_name", "attnum";
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class RedshiftTypeEngine(TypeEngine):
|
|
294
|
+
|
|
295
|
+
def _default_dialect(self, default=None):
|
|
296
|
+
"""
|
|
297
|
+
Returns the default dialect used for TypeEngine compilation yielding
|
|
298
|
+
String result.
|
|
299
|
+
|
|
300
|
+
:meth:`~sqlalchemy.sql.type_api.TypeEngine.compile`
|
|
301
|
+
"""
|
|
302
|
+
return RedshiftDialectMixin()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class TIMESTAMPTZ(RedshiftTypeEngine, sa.dialects.postgresql.TIMESTAMP):
|
|
306
|
+
"""
|
|
307
|
+
Redshift defines a TIMTESTAMPTZ column type as an alias
|
|
308
|
+
of TIMESTAMP WITH TIME ZONE.
|
|
309
|
+
https://docs.aws.amazon.com/redshift/latest/dg/c_Supported_data_types.html
|
|
310
|
+
|
|
311
|
+
Adding an explicit type to the RedshiftDialect allows us follow the
|
|
312
|
+
SqlAlchemy conventions for "vendor-specific types."
|
|
313
|
+
|
|
314
|
+
https://docs.sqlalchemy.org/en/13/core/type_basics.html#vendor-specific-types
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
__visit_name__ = 'TIMESTAMPTZ'
|
|
318
|
+
|
|
319
|
+
def __init__(self, timezone=True, precision=None):
|
|
320
|
+
# timezone param must be present as it's provided in base class so the
|
|
321
|
+
# object can be instantiated with kwargs. see
|
|
322
|
+
# :meth:`~sqlalchemy.dialects.postgresql.base.PGDialect._get_column_info`
|
|
323
|
+
super(TIMESTAMPTZ, self).__init__(timezone=True, precision=precision)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class TIMETZ(RedshiftTypeEngine, sa.dialects.postgresql.TIME):
|
|
327
|
+
"""
|
|
328
|
+
Redshift defines a TIMTETZ column type as an alias
|
|
329
|
+
of TIME WITH TIME ZONE.
|
|
330
|
+
https://docs.aws.amazon.com/redshift/latest/dg/c_Supported_data_types.html
|
|
331
|
+
|
|
332
|
+
Adding an explicit type to the RedshiftDialect allows us follow the
|
|
333
|
+
SqlAlchemy conventions for "vendor-specific types."
|
|
334
|
+
|
|
335
|
+
https://docs.sqlalchemy.org/en/13/core/type_basics.html#vendor-specific-types
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
__visit_name__ = 'TIMETZ'
|
|
339
|
+
|
|
340
|
+
def __init__(self, timezone=True, precision=None):
|
|
341
|
+
# timezone param must be present as it's provided in base class so the
|
|
342
|
+
# object can be instantiated with kwargs. see
|
|
343
|
+
# :meth:`~sqlalchemy.dialects.postgresql.base.PGDialect._get_column_info`
|
|
344
|
+
super(TIMETZ, self).__init__(timezone=True, precision=precision)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class GEOMETRY(RedshiftTypeEngine, sa.dialects.postgresql.TEXT):
|
|
348
|
+
"""
|
|
349
|
+
Redshift defines a GEOMETRY column type
|
|
350
|
+
https://docs.aws.amazon.com/redshift/latest/dg/c_Supported_data_types.html
|
|
351
|
+
|
|
352
|
+
Adding an explicit type to the RedshiftDialect allows us follow the
|
|
353
|
+
SqlAlchemy conventions for "vendor-specific types."
|
|
354
|
+
|
|
355
|
+
https://docs.sqlalchemy.org/en/13/core/type_basics.html#vendor-specific-types
|
|
356
|
+
"""
|
|
357
|
+
__visit_name__ = 'GEOMETRY'
|
|
358
|
+
|
|
359
|
+
def __init__(self):
|
|
360
|
+
super(GEOMETRY, self).__init__()
|
|
361
|
+
|
|
362
|
+
def get_dbapi_type(self, dbapi):
|
|
363
|
+
return dbapi.GEOMETRY
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class SUPER(RedshiftTypeEngine, sa.dialects.postgresql.TEXT):
|
|
367
|
+
"""
|
|
368
|
+
Redshift defines a SUPER column type
|
|
369
|
+
https://docs.aws.amazon.com/redshift/latest/dg/c_Supported_data_types.html
|
|
370
|
+
|
|
371
|
+
Adding an explicit type to the RedshiftDialect allows us follow the
|
|
372
|
+
SqlAlchemy conventions for "vendor-specific types."
|
|
373
|
+
|
|
374
|
+
https://docs.sqlalchemy.org/en/13/core/type_basics.html#vendor-specific-types
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
__visit_name__ = 'SUPER'
|
|
378
|
+
|
|
379
|
+
def __init__(self):
|
|
380
|
+
super(SUPER, self).__init__()
|
|
381
|
+
|
|
382
|
+
def get_dbapi_type(self, dbapi):
|
|
383
|
+
return dbapi.SUPER
|
|
384
|
+
|
|
385
|
+
def bind_expression(self, bindvalue):
|
|
386
|
+
return sa.func.json_parse(bindvalue)
|
|
387
|
+
|
|
388
|
+
def process_bind_param(self, value, dialect):
|
|
389
|
+
if not isinstance(value, str):
|
|
390
|
+
return json.dumps(value)
|
|
391
|
+
return value
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class HLLSKETCH(RedshiftTypeEngine, sa.dialects.postgresql.TEXT):
|
|
395
|
+
"""
|
|
396
|
+
Redshift defines a HLLSKETCH column type
|
|
397
|
+
https://docs.aws.amazon.com/redshift/latest/dg/c_Supported_data_types.html
|
|
398
|
+
|
|
399
|
+
Adding an explicit type to the RedshiftDialect allows us follow the
|
|
400
|
+
SqlAlchemy conventions for "vendor-specific types."
|
|
401
|
+
|
|
402
|
+
https://docs.sqlalchemy.org/en/13/core/type_basics.html#vendor-specific-types
|
|
403
|
+
"""
|
|
404
|
+
__visit_name__ = 'HLLSKETCH'
|
|
405
|
+
|
|
406
|
+
def __init__(self):
|
|
407
|
+
super(HLLSKETCH, self).__init__()
|
|
408
|
+
|
|
409
|
+
def get_dbapi_type(self, dbapi):
|
|
410
|
+
return dbapi.HLLSKETCH
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# Mapping for database schema inspection of Amazon Redshift datatypes
|
|
414
|
+
REDSHIFT_ISCHEMA_NAMES = {
|
|
415
|
+
"geometry": GEOMETRY,
|
|
416
|
+
"super": SUPER,
|
|
417
|
+
"time with time zone": TIMETZ,
|
|
418
|
+
"timestamp with time zone": TIMESTAMPTZ,
|
|
419
|
+
"hllsketch": HLLSKETCH,
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class RelationKey(namedtuple('RelationKey', ('name', 'schema'))):
|
|
424
|
+
"""
|
|
425
|
+
Structured tuple of table/view name and schema name.
|
|
426
|
+
"""
|
|
427
|
+
__slots__ = ()
|
|
428
|
+
|
|
429
|
+
def __new__(cls, name, schema=None, connection=None):
|
|
430
|
+
"""
|
|
431
|
+
Construct a new RelationKey with an explicit schema name.
|
|
432
|
+
"""
|
|
433
|
+
if schema is None and connection is None:
|
|
434
|
+
raise ValueError("Must specify either schema or connection")
|
|
435
|
+
if schema is None:
|
|
436
|
+
schema = inspect(connection).default_schema_name
|
|
437
|
+
return super(RelationKey, cls).__new__(cls, name, schema)
|
|
438
|
+
|
|
439
|
+
def __str__(self):
|
|
440
|
+
if self.schema is None:
|
|
441
|
+
return self.name
|
|
442
|
+
else:
|
|
443
|
+
return self.schema + "." + self.name
|
|
444
|
+
|
|
445
|
+
@staticmethod
|
|
446
|
+
def _unquote(part):
|
|
447
|
+
if (
|
|
448
|
+
part is not None and part.startswith('"') and
|
|
449
|
+
part.endswith('"')
|
|
450
|
+
):
|
|
451
|
+
return part[1:-1]
|
|
452
|
+
return part
|
|
453
|
+
|
|
454
|
+
def unquoted(self):
|
|
455
|
+
"""
|
|
456
|
+
Return *key* with one level of double quotes removed.
|
|
457
|
+
|
|
458
|
+
Redshift stores some identifiers without quotes in internal tables,
|
|
459
|
+
even though the name must be quoted elsewhere.
|
|
460
|
+
In particular, this happens for tables named as a keyword.
|
|
461
|
+
"""
|
|
462
|
+
return RelationKey(
|
|
463
|
+
RelationKey._unquote(self.name),
|
|
464
|
+
RelationKey._unquote(self.schema)
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class RedshiftCompiler(PGCompiler):
|
|
469
|
+
|
|
470
|
+
def visit_now_func(self, fn, **kw):
|
|
471
|
+
return "SYSDATE"
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class RedshiftDDLCompiler(PGDDLCompiler):
|
|
475
|
+
"""
|
|
476
|
+
Handles Redshift-specific ``CREATE TABLE`` syntax.
|
|
477
|
+
|
|
478
|
+
Users can specify the `diststyle`, `distkey`, `sortkey` and `encode`
|
|
479
|
+
properties per table and per column.
|
|
480
|
+
|
|
481
|
+
Table level properties can be set using the dialect specific syntax. For
|
|
482
|
+
example, to specify a distribution key and style you apply the following:
|
|
483
|
+
|
|
484
|
+
>>> import sqlalchemy as sa
|
|
485
|
+
>>> from sqlalchemy.schema import CreateTable
|
|
486
|
+
>>> engine = sa.create_engine('redshift+psycopg2://example')
|
|
487
|
+
>>> metadata = sa.MetaData()
|
|
488
|
+
>>> user = sa.Table(
|
|
489
|
+
... 'user',
|
|
490
|
+
... metadata,
|
|
491
|
+
... sa.Column('id', sa.Integer, primary_key=True),
|
|
492
|
+
... sa.Column('name', sa.String),
|
|
493
|
+
... redshift_diststyle='KEY',
|
|
494
|
+
... redshift_distkey='id',
|
|
495
|
+
... redshift_interleaved_sortkey=['id', 'name'],
|
|
496
|
+
... )
|
|
497
|
+
>>> print(CreateTable(user).compile(engine))
|
|
498
|
+
<BLANKLINE>
|
|
499
|
+
CREATE TABLE "user" (
|
|
500
|
+
id INTEGER NOT NULL,
|
|
501
|
+
name VARCHAR,
|
|
502
|
+
PRIMARY KEY (id)
|
|
503
|
+
) DISTSTYLE KEY DISTKEY (id) INTERLEAVED SORTKEY (id, name)
|
|
504
|
+
<BLANKLINE>
|
|
505
|
+
<BLANKLINE>
|
|
506
|
+
|
|
507
|
+
A single sort key can be applied without a wrapping list:
|
|
508
|
+
|
|
509
|
+
>>> customer = sa.Table(
|
|
510
|
+
... 'customer',
|
|
511
|
+
... metadata,
|
|
512
|
+
... sa.Column('id', sa.Integer, primary_key=True),
|
|
513
|
+
... sa.Column('name', sa.String),
|
|
514
|
+
... redshift_sortkey='id',
|
|
515
|
+
... )
|
|
516
|
+
>>> print(CreateTable(customer).compile(engine))
|
|
517
|
+
<BLANKLINE>
|
|
518
|
+
CREATE TABLE customer (
|
|
519
|
+
id INTEGER NOT NULL,
|
|
520
|
+
name VARCHAR,
|
|
521
|
+
PRIMARY KEY (id)
|
|
522
|
+
) SORTKEY (id)
|
|
523
|
+
<BLANKLINE>
|
|
524
|
+
<BLANKLINE>
|
|
525
|
+
|
|
526
|
+
Column-level special syntax can also be applied using Redshift dialect
|
|
527
|
+
specific keyword arguments.
|
|
528
|
+
For example, we can specify the ENCODE for a column:
|
|
529
|
+
|
|
530
|
+
>>> product = sa.Table(
|
|
531
|
+
... 'product',
|
|
532
|
+
... metadata,
|
|
533
|
+
... sa.Column('id', sa.Integer, primary_key=True),
|
|
534
|
+
... sa.Column('name', sa.String, redshift_encode='lzo')
|
|
535
|
+
... )
|
|
536
|
+
>>> print(CreateTable(product).compile(engine))
|
|
537
|
+
<BLANKLINE>
|
|
538
|
+
CREATE TABLE product (
|
|
539
|
+
id INTEGER NOT NULL,
|
|
540
|
+
name VARCHAR ENCODE lzo,
|
|
541
|
+
PRIMARY KEY (id)
|
|
542
|
+
)
|
|
543
|
+
<BLANKLINE>
|
|
544
|
+
<BLANKLINE>
|
|
545
|
+
|
|
546
|
+
The TIMESTAMPTZ and TIMETZ column types are also supported in the DDL.
|
|
547
|
+
|
|
548
|
+
For SQLAlchemy versions < 1.3.0, passing Redshift dialect options
|
|
549
|
+
as keyword arguments is not supported on the column level.
|
|
550
|
+
Instead, a column info dictionary can be used:
|
|
551
|
+
|
|
552
|
+
>>> product_pre_1_3_0 = sa.Table(
|
|
553
|
+
... 'product_pre_1_3_0',
|
|
554
|
+
... metadata,
|
|
555
|
+
... sa.Column('id', sa.Integer, primary_key=True),
|
|
556
|
+
... sa.Column('name', sa.String, info={'encode': 'lzo'})
|
|
557
|
+
... )
|
|
558
|
+
|
|
559
|
+
We can also specify the distkey and sortkey options:
|
|
560
|
+
|
|
561
|
+
>>> sku = sa.Table(
|
|
562
|
+
... 'sku',
|
|
563
|
+
... metadata,
|
|
564
|
+
... sa.Column('id', sa.Integer, primary_key=True),
|
|
565
|
+
... sa.Column(
|
|
566
|
+
... 'name',
|
|
567
|
+
... sa.String,
|
|
568
|
+
... redshift_distkey=True,
|
|
569
|
+
... redshift_sortkey=True
|
|
570
|
+
... )
|
|
571
|
+
... )
|
|
572
|
+
>>> print(CreateTable(sku).compile(engine))
|
|
573
|
+
<BLANKLINE>
|
|
574
|
+
CREATE TABLE sku (
|
|
575
|
+
id INTEGER NOT NULL,
|
|
576
|
+
name VARCHAR DISTKEY SORTKEY,
|
|
577
|
+
PRIMARY KEY (id)
|
|
578
|
+
)
|
|
579
|
+
<BLANKLINE>
|
|
580
|
+
<BLANKLINE>
|
|
581
|
+
"""
|
|
582
|
+
|
|
583
|
+
def post_create_table(self, table):
|
|
584
|
+
kwargs = ["diststyle", "distkey", "sortkey", "interleaved_sortkey"]
|
|
585
|
+
info = table.dialect_options['redshift']
|
|
586
|
+
info = {key: info.get(key) for key in kwargs}
|
|
587
|
+
return get_table_attributes(self.preparer, **info)
|
|
588
|
+
|
|
589
|
+
def get_column_specification(self, column, **kwargs):
|
|
590
|
+
colspec = self.preparer.format_column(column)
|
|
591
|
+
|
|
592
|
+
colspec += " " + self.dialect.type_compiler.process(column.type)
|
|
593
|
+
|
|
594
|
+
default = self.get_column_default_string(column)
|
|
595
|
+
if default is not None:
|
|
596
|
+
# Identity constraints show up as *default* when reflected.
|
|
597
|
+
m = IDENTITY_RE.match(default)
|
|
598
|
+
if m:
|
|
599
|
+
colspec += " IDENTITY({seed},{step})".format(**m.groupdict())
|
|
600
|
+
else:
|
|
601
|
+
colspec += " DEFAULT " + default
|
|
602
|
+
|
|
603
|
+
colspec += self._fetch_redshift_column_attributes(column)
|
|
604
|
+
|
|
605
|
+
if not column.nullable:
|
|
606
|
+
colspec += " NOT NULL"
|
|
607
|
+
return colspec
|
|
608
|
+
|
|
609
|
+
def _fetch_redshift_column_attributes(self, column):
|
|
610
|
+
text = ""
|
|
611
|
+
if sa_version >= Version('1.3.0'):
|
|
612
|
+
info = column.dialect_options['redshift']
|
|
613
|
+
else:
|
|
614
|
+
if not hasattr(column, 'info'):
|
|
615
|
+
return text
|
|
616
|
+
info = column.info
|
|
617
|
+
|
|
618
|
+
identity = info.get('identity')
|
|
619
|
+
if identity:
|
|
620
|
+
text += " IDENTITY({0},{1})".format(identity[0], identity[1])
|
|
621
|
+
|
|
622
|
+
encode = info.get('encode')
|
|
623
|
+
if encode:
|
|
624
|
+
text += " ENCODE " + encode
|
|
625
|
+
|
|
626
|
+
distkey = info.get('distkey')
|
|
627
|
+
if distkey:
|
|
628
|
+
text += " DISTKEY"
|
|
629
|
+
|
|
630
|
+
sortkey = info.get('sortkey')
|
|
631
|
+
if sortkey:
|
|
632
|
+
text += " SORTKEY"
|
|
633
|
+
return text
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
class RedshiftTypeCompiler(PGTypeCompiler):
|
|
637
|
+
|
|
638
|
+
def visit_GEOMETRY(self, type_, **kw):
|
|
639
|
+
return "GEOMETRY"
|
|
640
|
+
|
|
641
|
+
def visit_SUPER(self, type_, **kw):
|
|
642
|
+
return "SUPER"
|
|
643
|
+
|
|
644
|
+
def visit_TIMESTAMPTZ(self, type_, **kw):
|
|
645
|
+
return "TIMESTAMPTZ"
|
|
646
|
+
|
|
647
|
+
def visit_TIMETZ(self, type_, **kw):
|
|
648
|
+
return "TIMETZ"
|
|
649
|
+
|
|
650
|
+
def visit_HLLSKETCH(self, type_, **kw):
|
|
651
|
+
return "HLLSKETCH"
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
class RedshiftIdentifierPreparer(PGIdentifierPreparer):
|
|
655
|
+
reserved_words = RESERVED_WORDS
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
class RedshiftDialectMixin(DefaultDialect):
|
|
659
|
+
"""
|
|
660
|
+
Define Redshift-specific behavior.
|
|
661
|
+
|
|
662
|
+
Most public methods are overrides of the underlying interfaces defined in
|
|
663
|
+
:class:`~sqlalchemy.engine.interfaces.Dialect` and
|
|
664
|
+
:class:`~sqlalchemy.engine.Inspector`.
|
|
665
|
+
"""
|
|
666
|
+
|
|
667
|
+
name = 'redshift'
|
|
668
|
+
max_identifier_length = 127
|
|
669
|
+
|
|
670
|
+
statement_compiler = RedshiftCompiler
|
|
671
|
+
ddl_compiler = RedshiftDDLCompiler
|
|
672
|
+
preparer = RedshiftIdentifierPreparer
|
|
673
|
+
type_compiler = RedshiftTypeCompiler
|
|
674
|
+
construct_arguments = [
|
|
675
|
+
(sa.schema.Index, {
|
|
676
|
+
"using": False,
|
|
677
|
+
"where": None,
|
|
678
|
+
"ops": {}
|
|
679
|
+
}),
|
|
680
|
+
(sa.schema.Table, {
|
|
681
|
+
"ignore_search_path": False,
|
|
682
|
+
"diststyle": None,
|
|
683
|
+
"distkey": None,
|
|
684
|
+
"sortkey": None,
|
|
685
|
+
"interleaved_sortkey": None,
|
|
686
|
+
}),
|
|
687
|
+
(sa.schema.Column, {
|
|
688
|
+
"encode": None,
|
|
689
|
+
"distkey": None,
|
|
690
|
+
"sortkey": None,
|
|
691
|
+
"identity": None,
|
|
692
|
+
}),
|
|
693
|
+
]
|
|
694
|
+
|
|
695
|
+
def __init__(self, *args, **kw):
|
|
696
|
+
super(RedshiftDialectMixin, self).__init__(*args, **kw)
|
|
697
|
+
# Cache domains, as these will be static;
|
|
698
|
+
# Redshift does not support user-created domains.
|
|
699
|
+
self._domains = None
|
|
700
|
+
|
|
701
|
+
@property
|
|
702
|
+
def ischema_names(self):
|
|
703
|
+
"""
|
|
704
|
+
Returns information about datatypes supported by Amazon Redshift.
|
|
705
|
+
|
|
706
|
+
Used in
|
|
707
|
+
:meth:`~sqlalchemy.engine.dialects.postgresql.base.PGDialect._get_column_info`.
|
|
708
|
+
"""
|
|
709
|
+
return {
|
|
710
|
+
**super(RedshiftDialectMixin, self).ischema_names,
|
|
711
|
+
**REDSHIFT_ISCHEMA_NAMES
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
@reflection.cache
|
|
715
|
+
def get_columns(self, connection, table_name, schema=None, **kw):
|
|
716
|
+
"""
|
|
717
|
+
Return information about columns in `table_name`.
|
|
718
|
+
|
|
719
|
+
Overrides interface
|
|
720
|
+
:meth:`~sqlalchemy.engine.interfaces.Dialect.get_columns`.
|
|
721
|
+
"""
|
|
722
|
+
cols = self._get_redshift_columns(connection, table_name, schema, **kw)
|
|
723
|
+
if not self._domains:
|
|
724
|
+
self._domains = self._load_domains(connection)
|
|
725
|
+
domains = self._domains
|
|
726
|
+
columns = []
|
|
727
|
+
for col in cols:
|
|
728
|
+
column_info = self._get_column_info(
|
|
729
|
+
name=col.name, format_type=col.format_type,
|
|
730
|
+
default=col.default, notnull=col.notnull, domains=domains,
|
|
731
|
+
enums=[], schema=col.schema, encode=col.encode,
|
|
732
|
+
comment=col.comment)
|
|
733
|
+
columns.append(column_info)
|
|
734
|
+
return columns
|
|
735
|
+
|
|
736
|
+
@reflection.cache
|
|
737
|
+
def has_table(self, connection, table_name, schema=None, **kw):
|
|
738
|
+
if not schema:
|
|
739
|
+
schema = inspect(connection).default_schema_name
|
|
740
|
+
|
|
741
|
+
info_cache = kw.get('info_cache')
|
|
742
|
+
table = self._get_all_relation_info(connection,
|
|
743
|
+
schema=schema,
|
|
744
|
+
table_name=table_name,
|
|
745
|
+
info_cache=info_cache)
|
|
746
|
+
|
|
747
|
+
return True if table else False
|
|
748
|
+
|
|
749
|
+
@reflection.cache
|
|
750
|
+
def get_check_constraints(self, connection, table_name, schema=None, **kw):
|
|
751
|
+
table_oid = self.get_table_oid(
|
|
752
|
+
connection, table_name, schema, info_cache=kw.get("info_cache")
|
|
753
|
+
)
|
|
754
|
+
table_oid = 'NULL' if not table_oid else table_oid
|
|
755
|
+
|
|
756
|
+
result = connection.execute(sa.text("""
|
|
757
|
+
SELECT
|
|
758
|
+
cons.conname as name,
|
|
759
|
+
pg_get_constraintdef(cons.oid) as src
|
|
760
|
+
FROM
|
|
761
|
+
pg_catalog.pg_constraint cons
|
|
762
|
+
WHERE
|
|
763
|
+
cons.conrelid = {} AND
|
|
764
|
+
cons.contype = 'c'
|
|
765
|
+
""".format(table_oid)))
|
|
766
|
+
ret = []
|
|
767
|
+
for name, src in result:
|
|
768
|
+
# samples:
|
|
769
|
+
# "CHECK (((a > 1) AND (a < 5)))"
|
|
770
|
+
# "CHECK (((a = 1) OR ((a > 2) AND (a < 5))))"
|
|
771
|
+
# "CHECK (((a > 1) AND (a < 5))) NOT VALID"
|
|
772
|
+
# "CHECK (some_boolean_function(a))"
|
|
773
|
+
# "CHECK (((a\n < 1)\n OR\n (a\n >= 5))\n)"
|
|
774
|
+
|
|
775
|
+
m = re.match(
|
|
776
|
+
r"^CHECK *\((.+)\)( NOT VALID)?$", src, flags=re.DOTALL
|
|
777
|
+
)
|
|
778
|
+
if not m:
|
|
779
|
+
logger.warning(f"Could not parse CHECK constraint text: {src}")
|
|
780
|
+
sqltext = ""
|
|
781
|
+
else:
|
|
782
|
+
sqltext = re.compile(
|
|
783
|
+
r"^[\s\n]*\((.+)\)[\s\n]*$", flags=re.DOTALL
|
|
784
|
+
).sub(r"\1", m.group(1))
|
|
785
|
+
entry = {"name": name, "sqltext": sqltext}
|
|
786
|
+
if m and m.group(2):
|
|
787
|
+
entry["dialect_options"] = {"not_valid": True}
|
|
788
|
+
|
|
789
|
+
ret.append(entry)
|
|
790
|
+
return ret
|
|
791
|
+
|
|
792
|
+
@reflection.cache
|
|
793
|
+
def get_table_oid(self, connection, table_name, schema=None, **kw):
|
|
794
|
+
"""Fetch the oid for schema.table_name.
|
|
795
|
+
Return null if not found (external table does not have table oid)"""
|
|
796
|
+
schema_field = '"{schema}".'.format(schema=schema) if schema else ""
|
|
797
|
+
|
|
798
|
+
result = connection.execute(
|
|
799
|
+
sa.text(
|
|
800
|
+
"""
|
|
801
|
+
select '{schema_field}"{table_name}"'::regclass::oid;
|
|
802
|
+
""".format(
|
|
803
|
+
schema_field=schema_field,
|
|
804
|
+
table_name=table_name
|
|
805
|
+
)
|
|
806
|
+
)
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
return result.scalar()
|
|
810
|
+
|
|
811
|
+
@reflection.cache
|
|
812
|
+
def get_pk_constraint(self, connection, table_name, schema=None, **kw):
|
|
813
|
+
"""
|
|
814
|
+
Return information about the primary key constraint on `table_name`.
|
|
815
|
+
|
|
816
|
+
Overrides interface
|
|
817
|
+
:meth:`~sqlalchemy.engine.interfaces.Dialect.get_pk_constraint`.
|
|
818
|
+
"""
|
|
819
|
+
constraints = self._get_redshift_constraints(connection, table_name,
|
|
820
|
+
schema, **kw)
|
|
821
|
+
pk_constraints = [c for c in constraints if c.contype == 'p']
|
|
822
|
+
if not pk_constraints:
|
|
823
|
+
return {'constrained_columns': [], 'name': ''}
|
|
824
|
+
pk_constraint = pk_constraints[0]
|
|
825
|
+
m = PRIMARY_KEY_RE.match(pk_constraint.condef)
|
|
826
|
+
colstring = m.group('columns')
|
|
827
|
+
constrained_columns = SQL_IDENTIFIER_RE.findall(colstring)
|
|
828
|
+
return {
|
|
829
|
+
'constrained_columns': constrained_columns,
|
|
830
|
+
'name': pk_constraint.conname,
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
@reflection.cache
|
|
834
|
+
def get_foreign_keys(self, connection, table_name, schema=None, **kw):
|
|
835
|
+
"""
|
|
836
|
+
Return information about foreign keys in `table_name`.
|
|
837
|
+
|
|
838
|
+
Overrides interface
|
|
839
|
+
:meth:`~sqlalchemy.engine.interfaces.Dialect.get_pk_constraint`.
|
|
840
|
+
"""
|
|
841
|
+
constraints = self._get_redshift_constraints(connection, table_name,
|
|
842
|
+
schema, **kw)
|
|
843
|
+
fk_constraints = [c for c in constraints if c.contype == 'f']
|
|
844
|
+
uniques = defaultdict(lambda: defaultdict(dict))
|
|
845
|
+
for con in fk_constraints:
|
|
846
|
+
uniques[con.conname]["key"] = con.conkey
|
|
847
|
+
uniques[con.conname]["condef"] = con.condef
|
|
848
|
+
fkeys = []
|
|
849
|
+
for conname, attrs in uniques.items():
|
|
850
|
+
m = FOREIGN_KEY_RE.match(attrs['condef'])
|
|
851
|
+
colstring = m.group('referred_columns')
|
|
852
|
+
referred_columns = SQL_IDENTIFIER_RE.findall(colstring)
|
|
853
|
+
referred_table = m.group('referred_table')
|
|
854
|
+
referred_schema = m.group('referred_schema')
|
|
855
|
+
colstring = m.group('columns')
|
|
856
|
+
constrained_columns = SQL_IDENTIFIER_RE.findall(colstring)
|
|
857
|
+
fkey_d = {
|
|
858
|
+
'name': conname,
|
|
859
|
+
'constrained_columns': constrained_columns,
|
|
860
|
+
'referred_schema': referred_schema,
|
|
861
|
+
'referred_table': referred_table,
|
|
862
|
+
'referred_columns': referred_columns,
|
|
863
|
+
}
|
|
864
|
+
fkeys.append(fkey_d)
|
|
865
|
+
return fkeys
|
|
866
|
+
|
|
867
|
+
@reflection.cache
|
|
868
|
+
def get_table_names(self, connection, schema=None, **kw):
|
|
869
|
+
"""
|
|
870
|
+
Return a list of table names for `schema`.
|
|
871
|
+
|
|
872
|
+
Overrides interface
|
|
873
|
+
:meth:`~sqlalchemy.engine.interfaces.Dialect.get_table_names`.
|
|
874
|
+
"""
|
|
875
|
+
return self._get_table_or_view_names('r', connection, schema, **kw)
|
|
876
|
+
|
|
877
|
+
@reflection.cache
|
|
878
|
+
def get_view_names(self, connection, schema=None, **kw):
|
|
879
|
+
"""
|
|
880
|
+
Return a list of view names for `schema`.
|
|
881
|
+
|
|
882
|
+
Overrides interface
|
|
883
|
+
:meth:`~sqlalchemy.engine.interfaces.Dialect.get_view_names`.
|
|
884
|
+
"""
|
|
885
|
+
return self._get_table_or_view_names('v', connection, schema, **kw)
|
|
886
|
+
|
|
887
|
+
@reflection.cache
|
|
888
|
+
def get_view_definition(self, connection, view_name, schema=None, **kw):
|
|
889
|
+
"""Return view definition.
|
|
890
|
+
Given a :class:`.Connection`, a string `view_name`,
|
|
891
|
+
and an optional string `schema`, return the view definition.
|
|
892
|
+
|
|
893
|
+
Overrides interface
|
|
894
|
+
:meth:`~sqlalchemy.engine.interfaces.Dialect.get_view_definition`.
|
|
895
|
+
"""
|
|
896
|
+
view = self._get_redshift_relation(connection, view_name, schema, **kw)
|
|
897
|
+
return sa.text(view.view_definition)
|
|
898
|
+
|
|
899
|
+
def get_indexes(self, connection, table_name, schema, **kw):
|
|
900
|
+
"""
|
|
901
|
+
Return information about indexes in `table_name`.
|
|
902
|
+
|
|
903
|
+
Because Redshift does not support traditional indexes,
|
|
904
|
+
this always returns an empty list.
|
|
905
|
+
|
|
906
|
+
Overrides interface
|
|
907
|
+
:meth:`~sqlalchemy.engine.interfaces.Dialect.get_indexes`.
|
|
908
|
+
"""
|
|
909
|
+
return []
|
|
910
|
+
|
|
911
|
+
@reflection.cache
|
|
912
|
+
def get_unique_constraints(self, connection, table_name,
|
|
913
|
+
schema=None, **kw):
|
|
914
|
+
"""
|
|
915
|
+
Return information about unique constraints in `table_name`.
|
|
916
|
+
|
|
917
|
+
Overrides interface
|
|
918
|
+
:meth:`~sqlalchemy.engine.interfaces.Dialect.get_unique_constraints`.
|
|
919
|
+
"""
|
|
920
|
+
constraints = self._get_redshift_constraints(connection,
|
|
921
|
+
table_name, schema, **kw)
|
|
922
|
+
constraints = [c for c in constraints if c.contype == 'u']
|
|
923
|
+
uniques = defaultdict(lambda: defaultdict(dict))
|
|
924
|
+
for con in constraints:
|
|
925
|
+
uniques[con.conname]["key"] = con.conkey
|
|
926
|
+
uniques[con.conname]["cols"][con.attnum] = con.attname
|
|
927
|
+
|
|
928
|
+
return [
|
|
929
|
+
{'name': name,
|
|
930
|
+
'column_names': [uc["cols"][i] for i in uc["key"]]}
|
|
931
|
+
for name, uc in uniques.items()
|
|
932
|
+
]
|
|
933
|
+
|
|
934
|
+
@reflection.cache
|
|
935
|
+
def get_table_options(self, connection, table_name, schema, **kw):
|
|
936
|
+
"""
|
|
937
|
+
Return a dictionary of options specified when the table of the
|
|
938
|
+
given name was created.
|
|
939
|
+
|
|
940
|
+
Overrides interface
|
|
941
|
+
:meth:`~sqlalchemy.engine.Inspector.get_table_options`.
|
|
942
|
+
"""
|
|
943
|
+
def keyfunc(column):
|
|
944
|
+
num = int(column.sortkey)
|
|
945
|
+
# If sortkey is interleaved, column numbers alternate
|
|
946
|
+
# negative values, so take abs.
|
|
947
|
+
return abs(num)
|
|
948
|
+
table = self._get_redshift_relation(connection, table_name,
|
|
949
|
+
schema, **kw)
|
|
950
|
+
columns = self._get_redshift_columns(connection, table_name,
|
|
951
|
+
schema, **kw)
|
|
952
|
+
sortkey_cols = sorted([col for col in columns if col.sortkey],
|
|
953
|
+
key=keyfunc)
|
|
954
|
+
interleaved = any([int(col.sortkey) < 0 for col in sortkey_cols])
|
|
955
|
+
sortkey = tuple(col.name for col in sortkey_cols)
|
|
956
|
+
interleaved_sortkey = None
|
|
957
|
+
if interleaved:
|
|
958
|
+
interleaved_sortkey = sortkey
|
|
959
|
+
sortkey = None
|
|
960
|
+
distkeys = [col.name for col in columns if col.distkey]
|
|
961
|
+
distkey = distkeys[0] if distkeys else None
|
|
962
|
+
return {
|
|
963
|
+
'redshift_diststyle': table.diststyle,
|
|
964
|
+
'redshift_distkey': distkey,
|
|
965
|
+
'redshift_sortkey': sortkey,
|
|
966
|
+
'redshift_interleaved_sortkey': interleaved_sortkey,
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
def _get_table_or_view_names(self, relkind, connection, schema=None, **kw):
|
|
970
|
+
default_schema = inspect(connection).default_schema_name
|
|
971
|
+
if not schema:
|
|
972
|
+
schema = default_schema
|
|
973
|
+
info_cache = kw.get('info_cache')
|
|
974
|
+
all_relations = self._get_all_relation_info(connection,
|
|
975
|
+
schema=schema,
|
|
976
|
+
info_cache=info_cache)
|
|
977
|
+
relation_names = []
|
|
978
|
+
for key, relation in all_relations.items():
|
|
979
|
+
if key.schema == schema and relation.relkind == relkind:
|
|
980
|
+
relation_names.append(key.name)
|
|
981
|
+
return relation_names
|
|
982
|
+
|
|
983
|
+
def _get_column_info(self, *args, **kwargs):
|
|
984
|
+
kw = kwargs.copy()
|
|
985
|
+
encode = kw.pop('encode', None)
|
|
986
|
+
if sa_version >= Version('1.3.16'):
|
|
987
|
+
# SQLAlchemy 1.3.16 introduced generated columns,
|
|
988
|
+
# not supported in redshift
|
|
989
|
+
kw['generated'] = ''
|
|
990
|
+
|
|
991
|
+
if sa_version < Version('1.4.0') and 'identity' in kw:
|
|
992
|
+
del kw['identity']
|
|
993
|
+
elif sa_version >= Version('1.4.0') and 'identity' not in kw:
|
|
994
|
+
kw['identity'] = None
|
|
995
|
+
|
|
996
|
+
column_info = super(RedshiftDialectMixin, self)._get_column_info(
|
|
997
|
+
*args,
|
|
998
|
+
**kw
|
|
999
|
+
)
|
|
1000
|
+
if isinstance(column_info['type'], VARCHAR):
|
|
1001
|
+
if column_info['type'].length is None:
|
|
1002
|
+
column_info['type'] = NullType()
|
|
1003
|
+
if 'info' not in column_info:
|
|
1004
|
+
column_info['info'] = {}
|
|
1005
|
+
if encode and encode != 'none':
|
|
1006
|
+
column_info['info']['encode'] = encode
|
|
1007
|
+
return column_info
|
|
1008
|
+
|
|
1009
|
+
def _get_redshift_relation(self, connection, table_name,
|
|
1010
|
+
schema=None, **kw):
|
|
1011
|
+
info_cache = kw.get('info_cache')
|
|
1012
|
+
all_relations = self._get_all_relation_info(connection,
|
|
1013
|
+
schema=schema,
|
|
1014
|
+
table_name=table_name,
|
|
1015
|
+
info_cache=info_cache)
|
|
1016
|
+
key = RelationKey(table_name, schema, connection)
|
|
1017
|
+
if key not in all_relations.keys():
|
|
1018
|
+
key = key.unquoted()
|
|
1019
|
+
try:
|
|
1020
|
+
return all_relations[key]
|
|
1021
|
+
except KeyError:
|
|
1022
|
+
raise sa.exc.NoSuchTableError(key)
|
|
1023
|
+
|
|
1024
|
+
def _get_redshift_columns(self, connection, table_name, schema=None, **kw):
|
|
1025
|
+
info_cache = kw.get('info_cache')
|
|
1026
|
+
all_schema_columns = self._get_schema_column_info(
|
|
1027
|
+
connection,
|
|
1028
|
+
schema=schema,
|
|
1029
|
+
table_name=table_name,
|
|
1030
|
+
info_cache=info_cache
|
|
1031
|
+
)
|
|
1032
|
+
key = RelationKey(table_name, schema, connection)
|
|
1033
|
+
if key not in all_schema_columns.keys():
|
|
1034
|
+
key = key.unquoted()
|
|
1035
|
+
return all_schema_columns[key]
|
|
1036
|
+
|
|
1037
|
+
def _get_redshift_constraints(self, connection, table_name,
|
|
1038
|
+
schema=None, **kw):
|
|
1039
|
+
info_cache = kw.get('info_cache')
|
|
1040
|
+
all_constraints = self._get_all_constraint_info(connection,
|
|
1041
|
+
schema=schema,
|
|
1042
|
+
table_name=table_name,
|
|
1043
|
+
info_cache=info_cache)
|
|
1044
|
+
key = RelationKey(table_name, schema, connection)
|
|
1045
|
+
if key not in all_constraints.keys():
|
|
1046
|
+
key = key.unquoted()
|
|
1047
|
+
return all_constraints[key]
|
|
1048
|
+
|
|
1049
|
+
@reflection.cache
|
|
1050
|
+
def _get_all_relation_info(self, connection, **kw):
|
|
1051
|
+
schema = kw.get('schema', None)
|
|
1052
|
+
schema_clause = (
|
|
1053
|
+
"AND schema = '{schema}'".format(schema=schema) if schema else ""
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
table_name = kw.get('table_name', None)
|
|
1057
|
+
table_clause = (
|
|
1058
|
+
"AND relname = '{table}'".format(
|
|
1059
|
+
table=table_name
|
|
1060
|
+
) if table_name else ""
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
result = connection.execute(sa.text("""
|
|
1064
|
+
SELECT
|
|
1065
|
+
c.relkind,
|
|
1066
|
+
n.oid as "schema_oid",
|
|
1067
|
+
n.nspname as "schema",
|
|
1068
|
+
c.oid as "rel_oid",
|
|
1069
|
+
c.relname,
|
|
1070
|
+
CASE c.reldiststyle
|
|
1071
|
+
WHEN 0 THEN 'EVEN' WHEN 1 THEN 'KEY' WHEN 8 THEN 'ALL' END
|
|
1072
|
+
AS "diststyle",
|
|
1073
|
+
c.relowner AS "owner_id",
|
|
1074
|
+
u.usename AS "owner_name",
|
|
1075
|
+
TRIM(TRAILING ';' FROM pg_catalog.pg_get_viewdef(c.oid, true))
|
|
1076
|
+
AS "view_definition",
|
|
1077
|
+
pg_catalog.array_to_string(c.relacl, '\n') AS "privileges"
|
|
1078
|
+
FROM pg_catalog.pg_class c
|
|
1079
|
+
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
1080
|
+
JOIN pg_catalog.pg_user u ON u.usesysid = c.relowner
|
|
1081
|
+
WHERE c.relkind IN ('r', 'v', 'm', 'S', 'f')
|
|
1082
|
+
AND n.nspname !~ '^pg_' {schema_clause} {table_clause}
|
|
1083
|
+
UNION
|
|
1084
|
+
SELECT
|
|
1085
|
+
'r' AS "relkind",
|
|
1086
|
+
s.esoid AS "schema_oid",
|
|
1087
|
+
s.schemaname AS "schema",
|
|
1088
|
+
null AS "rel_oid",
|
|
1089
|
+
t.tablename AS "relname",
|
|
1090
|
+
null AS "diststyle",
|
|
1091
|
+
s.esowner AS "owner_id",
|
|
1092
|
+
u.usename AS "owner_name",
|
|
1093
|
+
null AS "view_definition",
|
|
1094
|
+
null AS "privileges"
|
|
1095
|
+
FROM
|
|
1096
|
+
svv_external_tables t
|
|
1097
|
+
JOIN svv_external_schemas s ON s.schemaname = t.schemaname
|
|
1098
|
+
JOIN pg_catalog.pg_user u ON u.usesysid = s.esowner
|
|
1099
|
+
where 1 {schema_clause} {table_clause}
|
|
1100
|
+
ORDER BY "relkind", "schema_oid", "schema";
|
|
1101
|
+
""".format(schema_clause=schema_clause, table_clause=table_clause)))
|
|
1102
|
+
relations = {}
|
|
1103
|
+
for rel in result:
|
|
1104
|
+
key = RelationKey(rel.relname, rel.schema, connection)
|
|
1105
|
+
relations[key] = rel
|
|
1106
|
+
return relations
|
|
1107
|
+
|
|
1108
|
+
# We fetch column info an entire schema at a time to improve performance
|
|
1109
|
+
# when reflecting schema for multiple tables at once.
|
|
1110
|
+
@reflection.cache
|
|
1111
|
+
def _get_schema_column_info(self, connection, **kw):
|
|
1112
|
+
schema = kw.get('schema', None)
|
|
1113
|
+
schema_clause = (
|
|
1114
|
+
"AND schema = '{schema}'".format(schema=schema) if schema else ""
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
table_name = kw.get('table_name', None)
|
|
1118
|
+
table_clause = (
|
|
1119
|
+
"AND table_name = '{table}'".format(
|
|
1120
|
+
table=table_name
|
|
1121
|
+
) if table_name else ""
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
all_columns = defaultdict(list)
|
|
1125
|
+
result = connection.execute(sa.text(REFLECTION_SQL.format(
|
|
1126
|
+
schema_clause=schema_clause,
|
|
1127
|
+
table_clause=table_clause
|
|
1128
|
+
)))
|
|
1129
|
+
|
|
1130
|
+
for col in result:
|
|
1131
|
+
key = RelationKey(col.table_name, col.schema, connection)
|
|
1132
|
+
all_columns[key].append(col)
|
|
1133
|
+
|
|
1134
|
+
return dict(all_columns)
|
|
1135
|
+
|
|
1136
|
+
@reflection.cache
|
|
1137
|
+
def _get_all_constraint_info(self, connection, **kw):
|
|
1138
|
+
schema = kw.get('schema', None)
|
|
1139
|
+
schema_clause = (
|
|
1140
|
+
"AND schema = '{schema}'".format(schema=schema) if schema else ""
|
|
1141
|
+
)
|
|
1142
|
+
|
|
1143
|
+
table_name = kw.get('table_name', None)
|
|
1144
|
+
table_clause = (
|
|
1145
|
+
"AND table_name = '{table}'".format(
|
|
1146
|
+
table=table_name
|
|
1147
|
+
) if table_name else ""
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
result = connection.execute(sa.text("""
|
|
1151
|
+
SELECT
|
|
1152
|
+
n.nspname as "schema",
|
|
1153
|
+
c.relname as "table_name",
|
|
1154
|
+
t.contype,
|
|
1155
|
+
t.conname,
|
|
1156
|
+
t.conkey,
|
|
1157
|
+
a.attnum,
|
|
1158
|
+
a.attname,
|
|
1159
|
+
pg_catalog.pg_get_constraintdef(t.oid, true)::varchar(512) as condef,
|
|
1160
|
+
n.oid as "schema_oid",
|
|
1161
|
+
c.oid as "rel_oid"
|
|
1162
|
+
FROM pg_catalog.pg_class c
|
|
1163
|
+
LEFT JOIN pg_catalog.pg_namespace n
|
|
1164
|
+
ON n.oid = c.relnamespace
|
|
1165
|
+
JOIN pg_catalog.pg_constraint t
|
|
1166
|
+
ON t.conrelid = c.oid
|
|
1167
|
+
JOIN pg_catalog.pg_attribute a
|
|
1168
|
+
ON t.conrelid = a.attrelid AND a.attnum = ANY(t.conkey)
|
|
1169
|
+
WHERE n.nspname !~ '^pg_' {schema_clause} {table_clause}
|
|
1170
|
+
UNION
|
|
1171
|
+
SELECT
|
|
1172
|
+
s.schemaname AS "schema",
|
|
1173
|
+
c.tablename AS "table_name",
|
|
1174
|
+
'p' as "contype",
|
|
1175
|
+
c.tablename || '_pkey' as "conname",
|
|
1176
|
+
array[1::SMALLINT] as "conkey",
|
|
1177
|
+
1 as "attnum",
|
|
1178
|
+
c.columnname as "attname",
|
|
1179
|
+
'PRIMARY KEY (' || c.columnname || ')'::VARCHAR(512) as "condef",
|
|
1180
|
+
s.esoid AS "schema_oid",
|
|
1181
|
+
null AS "rel_oid"
|
|
1182
|
+
FROM
|
|
1183
|
+
svv_external_columns c
|
|
1184
|
+
JOIN svv_external_schemas s ON s.schemaname = c.schemaname
|
|
1185
|
+
where 1 {schema_clause} {table_clause}
|
|
1186
|
+
ORDER BY "schema", "table_name"
|
|
1187
|
+
""".format(schema_clause=schema_clause, table_clause=table_clause)))
|
|
1188
|
+
all_constraints = defaultdict(list)
|
|
1189
|
+
for con in result:
|
|
1190
|
+
key = RelationKey(con.table_name, con.schema, connection)
|
|
1191
|
+
all_constraints[key].append(con)
|
|
1192
|
+
return all_constraints
|
|
1193
|
+
|
|
1194
|
+
def _set_backslash_escapes(self, connection):
|
|
1195
|
+
self._backslash_escapes = False
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
class Psycopg2RedshiftDialectMixin(RedshiftDialectMixin):
|
|
1199
|
+
"""
|
|
1200
|
+
Define behavior specific to ``psycopg2``.
|
|
1201
|
+
|
|
1202
|
+
Most public methods are overrides of the underlying interfaces defined in
|
|
1203
|
+
:class:`~sqlalchemy.engine.interfaces.Dialect` and
|
|
1204
|
+
:class:`~sqlalchemy.engine.Inspector`.
|
|
1205
|
+
"""
|
|
1206
|
+
def create_connect_args(self, *args, **kwargs):
|
|
1207
|
+
"""
|
|
1208
|
+
Build DB-API compatible connection arguments.
|
|
1209
|
+
|
|
1210
|
+
Overrides interface
|
|
1211
|
+
:meth:`~sqlalchemy.engine.interfaces.Dialect.create_connect_args`.
|
|
1212
|
+
"""
|
|
1213
|
+
default_args = {
|
|
1214
|
+
'sslmode': 'verify-full',
|
|
1215
|
+
'sslrootcert': pkg_resources.resource_filename(
|
|
1216
|
+
__name__,
|
|
1217
|
+
'redshift-ca-bundle.crt'
|
|
1218
|
+
),
|
|
1219
|
+
}
|
|
1220
|
+
cargs, cparams = (
|
|
1221
|
+
super(Psycopg2RedshiftDialectMixin, self).create_connect_args(
|
|
1222
|
+
*args, **kwargs
|
|
1223
|
+
)
|
|
1224
|
+
)
|
|
1225
|
+
default_args.update(cparams)
|
|
1226
|
+
return cargs, default_args
|
|
1227
|
+
|
|
1228
|
+
@classmethod
|
|
1229
|
+
def dbapi(cls):
|
|
1230
|
+
try:
|
|
1231
|
+
return importlib.import_module(cls.driver)
|
|
1232
|
+
except ImportError:
|
|
1233
|
+
raise ImportError(
|
|
1234
|
+
'No module named {}'.format(cls.driver)
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
class RedshiftDialect_psycopg2(
|
|
1239
|
+
Psycopg2RedshiftDialectMixin, PGDialect_psycopg2
|
|
1240
|
+
):
|
|
1241
|
+
supports_statement_cache = False
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
# Add RedshiftDialect synonym for backwards compatibility.
|
|
1245
|
+
RedshiftDialect = RedshiftDialect_psycopg2
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
class RedshiftDialect_psycopg2cffi(
|
|
1249
|
+
Psycopg2RedshiftDialectMixin, PGDialect_psycopg2cffi
|
|
1250
|
+
):
|
|
1251
|
+
supports_statement_cache = False
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
class RedshiftDialect_redshift_connector(RedshiftDialectMixin, PGDialect):
|
|
1255
|
+
|
|
1256
|
+
class RedshiftCompiler_redshift_connector(RedshiftCompiler, PGCompiler):
|
|
1257
|
+
def limit_clause(self, select, **kw):
|
|
1258
|
+
text = ""
|
|
1259
|
+
if select._limit_clause is not None:
|
|
1260
|
+
# an integer value for limit is retrieved
|
|
1261
|
+
text += " \n LIMIT " + str(select._limit)
|
|
1262
|
+
if select._offset_clause is not None:
|
|
1263
|
+
if select._limit_clause is None:
|
|
1264
|
+
text += "\n LIMIT ALL"
|
|
1265
|
+
# an integer value for offset is retrieved
|
|
1266
|
+
text += " OFFSET " + str(select._offset)
|
|
1267
|
+
return text
|
|
1268
|
+
|
|
1269
|
+
def visit_mod_binary(self, binary, operator, **kw):
|
|
1270
|
+
return (
|
|
1271
|
+
self.process(binary.left, **kw)
|
|
1272
|
+
+ " %% "
|
|
1273
|
+
+ self.process(binary.right, **kw)
|
|
1274
|
+
)
|
|
1275
|
+
|
|
1276
|
+
def post_process_text(self, text):
|
|
1277
|
+
from sqlalchemy import util
|
|
1278
|
+
if "%%" in text:
|
|
1279
|
+
util.warn(
|
|
1280
|
+
"The SQLAlchemy postgresql dialect "
|
|
1281
|
+
"now automatically escapes '%' in text() "
|
|
1282
|
+
"expressions to '%%'."
|
|
1283
|
+
)
|
|
1284
|
+
return text.replace("%", "%%")
|
|
1285
|
+
|
|
1286
|
+
class RedshiftExecutionContext_redshift_connector(PGExecutionContext):
|
|
1287
|
+
def pre_exec(self):
|
|
1288
|
+
if not self.compiled:
|
|
1289
|
+
return
|
|
1290
|
+
|
|
1291
|
+
driver = 'redshift_connector'
|
|
1292
|
+
|
|
1293
|
+
supports_unicode_statements = True
|
|
1294
|
+
|
|
1295
|
+
supports_unicode_binds = True
|
|
1296
|
+
|
|
1297
|
+
default_paramstyle = "format"
|
|
1298
|
+
supports_sane_multi_rowcount = True
|
|
1299
|
+
statement_compiler = RedshiftCompiler_redshift_connector
|
|
1300
|
+
execution_ctx_cls = RedshiftExecutionContext_redshift_connector
|
|
1301
|
+
|
|
1302
|
+
supports_statement_cache = False
|
|
1303
|
+
use_setinputsizes = False # not implemented in redshift_connector
|
|
1304
|
+
|
|
1305
|
+
def __init__(self, client_encoding=None, **kwargs):
|
|
1306
|
+
super(
|
|
1307
|
+
RedshiftDialect_redshift_connector, self
|
|
1308
|
+
).__init__(client_encoding=client_encoding, **kwargs)
|
|
1309
|
+
self.client_encoding = client_encoding
|
|
1310
|
+
|
|
1311
|
+
@classmethod
|
|
1312
|
+
def dbapi(cls):
|
|
1313
|
+
try:
|
|
1314
|
+
driver_module = importlib.import_module(cls.driver)
|
|
1315
|
+
|
|
1316
|
+
# Starting v2.0.908 driver converts description column names to str
|
|
1317
|
+
if Version(driver_module.__version__) < Version('2.0.908'):
|
|
1318
|
+
cls.description_encoding = "use_encoding"
|
|
1319
|
+
else:
|
|
1320
|
+
cls.description_encoding = None
|
|
1321
|
+
|
|
1322
|
+
return driver_module
|
|
1323
|
+
except ImportError:
|
|
1324
|
+
raise ImportError(
|
|
1325
|
+
'No module named redshift_connector. Please install '
|
|
1326
|
+
'redshift_connector to use this sqlalchemy dialect.'
|
|
1327
|
+
)
|
|
1328
|
+
|
|
1329
|
+
def set_client_encoding(self, connection, client_encoding):
|
|
1330
|
+
"""
|
|
1331
|
+
Sets the client-side encoding using the provided connection object.
|
|
1332
|
+
"""
|
|
1333
|
+
# adjust for ConnectionFairy possibly being present
|
|
1334
|
+
if hasattr(connection, "connection"):
|
|
1335
|
+
connection = connection.connection
|
|
1336
|
+
|
|
1337
|
+
cursor = connection.cursor()
|
|
1338
|
+
cursor.execute("SET CLIENT_ENCODING TO '" + client_encoding + "'")
|
|
1339
|
+
cursor.execute("COMMIT")
|
|
1340
|
+
cursor.close()
|
|
1341
|
+
|
|
1342
|
+
def set_isolation_level(self, connection, level):
|
|
1343
|
+
"""
|
|
1344
|
+
Sets the isolation level for the current transaction.
|
|
1345
|
+
|
|
1346
|
+
Additionally, autocommit can be enabled on the underlying
|
|
1347
|
+
db-api connection object via argument level='AUTOCOMMIT'.
|
|
1348
|
+
|
|
1349
|
+
See Amazon Redshift documentation for information on supported
|
|
1350
|
+
isolation levels.
|
|
1351
|
+
https://docs.aws.amazon.com/redshift/latest/dg/r_BEGIN.html
|
|
1352
|
+
"""
|
|
1353
|
+
level = level.replace("_", " ")
|
|
1354
|
+
|
|
1355
|
+
# adjust for ConnectionFairy possibly being present
|
|
1356
|
+
if hasattr(connection, "connection"):
|
|
1357
|
+
connection = connection.connection
|
|
1358
|
+
|
|
1359
|
+
if level == "AUTOCOMMIT":
|
|
1360
|
+
connection.autocommit = True
|
|
1361
|
+
else:
|
|
1362
|
+
connection.autocommit = False
|
|
1363
|
+
super(
|
|
1364
|
+
RedshiftDialect_redshift_connector, self
|
|
1365
|
+
).set_isolation_level(connection, level)
|
|
1366
|
+
|
|
1367
|
+
def on_connect(self):
|
|
1368
|
+
fns = []
|
|
1369
|
+
|
|
1370
|
+
def on_connect(conn):
|
|
1371
|
+
from sqlalchemy import util
|
|
1372
|
+
from sqlalchemy.sql.elements import quoted_name
|
|
1373
|
+
conn.py_types[quoted_name] = conn.py_types[util.text_type]
|
|
1374
|
+
|
|
1375
|
+
fns.append(on_connect)
|
|
1376
|
+
|
|
1377
|
+
if self.client_encoding is not None:
|
|
1378
|
+
|
|
1379
|
+
def on_connect(conn):
|
|
1380
|
+
self.set_client_encoding(conn, self.client_encoding)
|
|
1381
|
+
|
|
1382
|
+
fns.append(on_connect)
|
|
1383
|
+
|
|
1384
|
+
if self.isolation_level is not None:
|
|
1385
|
+
|
|
1386
|
+
def on_connect(conn):
|
|
1387
|
+
self.set_isolation_level(conn, self.isolation_level)
|
|
1388
|
+
|
|
1389
|
+
fns.append(on_connect)
|
|
1390
|
+
|
|
1391
|
+
if len(fns) > 0:
|
|
1392
|
+
|
|
1393
|
+
def on_connect(conn):
|
|
1394
|
+
for fn in fns:
|
|
1395
|
+
fn(conn)
|
|
1396
|
+
|
|
1397
|
+
return on_connect
|
|
1398
|
+
else:
|
|
1399
|
+
return None
|
|
1400
|
+
|
|
1401
|
+
def create_connect_args(self, *args, **kwargs):
|
|
1402
|
+
"""
|
|
1403
|
+
Build DB-API compatible connection arguments.
|
|
1404
|
+
|
|
1405
|
+
Overrides interface
|
|
1406
|
+
:meth:`~sqlalchemy.engine.interfaces.Dialect.create_connect_args`.
|
|
1407
|
+
"""
|
|
1408
|
+
default_args = {
|
|
1409
|
+
'sslmode': 'verify-full',
|
|
1410
|
+
'ssl': True,
|
|
1411
|
+
'application_name': 'sqlalchemy-redshift'
|
|
1412
|
+
}
|
|
1413
|
+
cargs, cparams = super(RedshiftDialectMixin, self).create_connect_args(
|
|
1414
|
+
*args, **kwargs
|
|
1415
|
+
)
|
|
1416
|
+
# set client_encoding so it is picked up by on_connect(), as
|
|
1417
|
+
# redshift_connector does not have client_encoding connection parameter
|
|
1418
|
+
self.client_encoding = cparams.pop(
|
|
1419
|
+
'client_encoding', self.client_encoding
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1422
|
+
if 'port' in cparams:
|
|
1423
|
+
cparams['port'] = int(cparams['port'])
|
|
1424
|
+
|
|
1425
|
+
if 'username' in cparams:
|
|
1426
|
+
cparams['user'] = cparams['username']
|
|
1427
|
+
del cparams['username']
|
|
1428
|
+
|
|
1429
|
+
default_args.update(cparams)
|
|
1430
|
+
return cargs, default_args
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
def gen_columns_from_children(root):
|
|
1434
|
+
"""
|
|
1435
|
+
Generates columns that are being used in child elements of the delete query
|
|
1436
|
+
this will be used to determine tables for the using clause.
|
|
1437
|
+
:param root: the delete query
|
|
1438
|
+
:return: a generator of columns
|
|
1439
|
+
"""
|
|
1440
|
+
if isinstance(root, (Delete, BinaryExpression, BooleanClauseList)):
|
|
1441
|
+
for child in root.get_children():
|
|
1442
|
+
yc = gen_columns_from_children(child)
|
|
1443
|
+
for it in yc:
|
|
1444
|
+
yield it
|
|
1445
|
+
elif isinstance(root, sa.Column):
|
|
1446
|
+
yield root
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
@compiles(Delete, 'redshift')
|
|
1450
|
+
def visit_delete_stmt(element, compiler, **kwargs):
|
|
1451
|
+
"""
|
|
1452
|
+
Adds redshift-dialect specific compilation rule for the
|
|
1453
|
+
delete statement.
|
|
1454
|
+
|
|
1455
|
+
Redshift DELETE syntax can be found here:
|
|
1456
|
+
https://docs.aws.amazon.com/redshift/latest/dg/r_DELETE.html
|
|
1457
|
+
|
|
1458
|
+
.. :code-block: sql
|
|
1459
|
+
|
|
1460
|
+
DELETE [ FROM ] table_name
|
|
1461
|
+
[ { USING } table_name, ...]
|
|
1462
|
+
[ WHERE condition ]
|
|
1463
|
+
|
|
1464
|
+
By default, SqlAlchemy compiles DELETE statements with the
|
|
1465
|
+
syntax:
|
|
1466
|
+
|
|
1467
|
+
.. :code-block: sql
|
|
1468
|
+
|
|
1469
|
+
DELETE [ FROM ] table_name
|
|
1470
|
+
[ WHERE condition ]
|
|
1471
|
+
|
|
1472
|
+
problem illustration:
|
|
1473
|
+
|
|
1474
|
+
>>> from sqlalchemy import Table, Column, Integer, MetaData, delete
|
|
1475
|
+
>>> from sqlalchemy_redshift.dialect import RedshiftDialect_psycopg2
|
|
1476
|
+
>>> meta = MetaData()
|
|
1477
|
+
>>> table1 = Table(
|
|
1478
|
+
... 'table_1',
|
|
1479
|
+
... meta,
|
|
1480
|
+
... Column('pk', Integer, primary_key=True)
|
|
1481
|
+
... )
|
|
1482
|
+
...
|
|
1483
|
+
>>> table2 = Table(
|
|
1484
|
+
... 'table_2',
|
|
1485
|
+
... meta,
|
|
1486
|
+
... Column('pk', Integer, primary_key=True)
|
|
1487
|
+
... )
|
|
1488
|
+
...
|
|
1489
|
+
>>> del_stmt = delete(table1).where(table1.c.pk==table2.c.pk)
|
|
1490
|
+
>>> str(del_stmt.compile(dialect=RedshiftDialect_psycopg2()))
|
|
1491
|
+
'DELETE FROM table_1 USING table_2 WHERE table_1.pk = table_2.pk'
|
|
1492
|
+
>>> str(del_stmt)
|
|
1493
|
+
'DELETE FROM table_1 , table_2 WHERE table_1.pk = table_2.pk'
|
|
1494
|
+
>>> del_stmt2 = delete(table1)
|
|
1495
|
+
>>> str(del_stmt2)
|
|
1496
|
+
'DELETE FROM table_1'
|
|
1497
|
+
>>> del_stmt3 = delete(table1).where(table1.c.pk > 1000)
|
|
1498
|
+
>>> str(del_stmt3)
|
|
1499
|
+
'DELETE FROM table_1 WHERE table_1.pk > :pk_1'
|
|
1500
|
+
>>> str(del_stmt3.compile(dialect=RedshiftDialect_psycopg2()))
|
|
1501
|
+
'DELETE FROM table_1 WHERE table_1.pk > %(pk_1)s'
|
|
1502
|
+
"""
|
|
1503
|
+
|
|
1504
|
+
# Set empty strings for the default where clause and using clause
|
|
1505
|
+
whereclause = ''
|
|
1506
|
+
usingclause = ''
|
|
1507
|
+
|
|
1508
|
+
# determine if the delete query needs a ``USING`` injected
|
|
1509
|
+
# by inspecting the whereclause's children & their children...
|
|
1510
|
+
# first, the where clause text is buit, if applicable
|
|
1511
|
+
# then, the using clause text is built, if applicable
|
|
1512
|
+
# note:
|
|
1513
|
+
# the tables in the using clause are sorted in the order in
|
|
1514
|
+
# which they first appear in the where clause.
|
|
1515
|
+
delete_stmt_table = compiler.process(element.table, asfrom=True, **kwargs)
|
|
1516
|
+
|
|
1517
|
+
if sa_version >= Version('1.4.0'):
|
|
1518
|
+
if element.whereclause is not None:
|
|
1519
|
+
clause = compiler.process(element.whereclause, **kwargs)
|
|
1520
|
+
if clause:
|
|
1521
|
+
whereclause = ' WHERE {clause}'.format(clause=clause)
|
|
1522
|
+
else:
|
|
1523
|
+
whereclause_tuple = element.get_children()
|
|
1524
|
+
if whereclause_tuple:
|
|
1525
|
+
whereclause = ' WHERE {clause}'.format(
|
|
1526
|
+
clause=compiler.process(*whereclause_tuple, **kwargs)
|
|
1527
|
+
)
|
|
1528
|
+
|
|
1529
|
+
if whereclause:
|
|
1530
|
+
usingclause_tables = []
|
|
1531
|
+
whereclause_columns = gen_columns_from_children(element)
|
|
1532
|
+
for col in whereclause_columns:
|
|
1533
|
+
table = compiler.process(col.table, asfrom=True, **kwargs)
|
|
1534
|
+
if table != delete_stmt_table and \
|
|
1535
|
+
table not in usingclause_tables:
|
|
1536
|
+
usingclause_tables.append(table)
|
|
1537
|
+
if usingclause_tables:
|
|
1538
|
+
usingclause = ' USING {clause}'.format(
|
|
1539
|
+
clause=', '.join(usingclause_tables)
|
|
1540
|
+
)
|
|
1541
|
+
|
|
1542
|
+
return 'DELETE FROM {table}{using}{where}'.format(
|
|
1543
|
+
table=delete_stmt_table,
|
|
1544
|
+
using=usingclause,
|
|
1545
|
+
where=whereclause)
|