mtsql 1.11.14__py3-none-any.whl → 1.11.16__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.
@@ -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)