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
mt/sql/redshift/__init__.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
from pkg_resources import DistributionNotFound, get_distribution, parse_version
|
|
2
|
+
from sqlalchemy.dialects import registry # noqa
|
|
3
|
+
|
|
1
4
|
from .main import *
|
|
2
5
|
|
|
3
6
|
__api__ = [
|
|
@@ -15,3 +18,26 @@ __api__ = [
|
|
|
15
18
|
"drop_column",
|
|
16
19
|
"conform",
|
|
17
20
|
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
for package in ["psycopg2", "psycopg2-binary", "psycopg2cffi"]:
|
|
24
|
+
try:
|
|
25
|
+
if get_distribution(package).parsed_version < parse_version("2.5"):
|
|
26
|
+
raise ImportError("Minimum required version for psycopg2 is 2.5")
|
|
27
|
+
break
|
|
28
|
+
except DistributionNotFound:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
registry.register("rs", "mt.sql.redshift.dialect", "RedshiftDialect_psycopg2")
|
|
32
|
+
registry.register("rs.psycopg2", "mt.sql.redshift.dialect", "RedshiftDialect_psycopg2")
|
|
33
|
+
registry.register(
|
|
34
|
+
"rs+psycopg2cffi",
|
|
35
|
+
"mt.sql.redshift.dialect",
|
|
36
|
+
"RedshiftDialect_psycopg2cffi",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
registry.register(
|
|
40
|
+
"rs+redshift_connector",
|
|
41
|
+
"mt.sql.redshift.dialect",
|
|
42
|
+
"RedshiftDialect_redshift_connector",
|
|
43
|
+
)
|
mt/sql/redshift/ddl.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import sqlalchemy as sa
|
|
2
|
+
from sqlalchemy.ext import compiler as sa_compiler
|
|
3
|
+
from sqlalchemy.schema import DDLElement
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _check_if_key_exists(key):
|
|
7
|
+
return isinstance(key, sa.Column) or key
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_table_attributes(preparer,
|
|
11
|
+
diststyle=None,
|
|
12
|
+
distkey=None,
|
|
13
|
+
sortkey=None,
|
|
14
|
+
interleaved_sortkey=None):
|
|
15
|
+
"""
|
|
16
|
+
Parse the table attributes into an acceptable string for Redshift,
|
|
17
|
+
checking for valid combinations of distribution options.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
preparer: RedshiftIdentifierPreparer, required
|
|
22
|
+
The preparer associated with the compiler, usually accessed through
|
|
23
|
+
compiler.preparer. You can use a RedshiftDDLCompiler instance to
|
|
24
|
+
access it.
|
|
25
|
+
diststyle: str, optional
|
|
26
|
+
The diststle to use for the table attributes. This must be one of:
|
|
27
|
+
("ALL", "EVEN", "KEY"). If unset, Redshift passes AUTO. If KEY is used
|
|
28
|
+
a distkey argument must be provided. Inversely, if a diststyle other
|
|
29
|
+
than KEY is provided, a distkey argument cannot be provided.
|
|
30
|
+
distkey: str or sqlalchemy.Column, optional
|
|
31
|
+
The distribution key to use the for the table attributes. This can be
|
|
32
|
+
provided without any distsyle specified or with KEY diststyle
|
|
33
|
+
specified.
|
|
34
|
+
sortkey: str or sqlalchemy.Column (or iterable thereof), optional
|
|
35
|
+
The (compound) sort key(s) to use for the table attributes. Mutually
|
|
36
|
+
exclusive option from interleaved_sortkey.
|
|
37
|
+
interleaved_sortkey: str or sqlalchemy.Column (or iterable), optional
|
|
38
|
+
The (interleaved) sort key(s) to use for the table attributes. Mutually
|
|
39
|
+
exclusive option from sortkey.
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
string
|
|
44
|
+
the table_attributes to append to a DDLElement, normally when creating
|
|
45
|
+
a table or materialized view.
|
|
46
|
+
|
|
47
|
+
Raises
|
|
48
|
+
------
|
|
49
|
+
sqlalchemy.exc.ArgumentError
|
|
50
|
+
when an invalid diststyle is set,
|
|
51
|
+
when incompatable (diststyle, distkey) pairs are used,
|
|
52
|
+
when both sortkey and interleaved_sortkey is specified.
|
|
53
|
+
"""
|
|
54
|
+
text = ""
|
|
55
|
+
|
|
56
|
+
has_distkey = _check_if_key_exists(distkey)
|
|
57
|
+
if diststyle:
|
|
58
|
+
diststyle = diststyle.upper()
|
|
59
|
+
if diststyle not in ('EVEN', 'KEY', 'ALL'):
|
|
60
|
+
raise sa.exc.ArgumentError(
|
|
61
|
+
u"diststyle {0} is invalid".format(diststyle)
|
|
62
|
+
)
|
|
63
|
+
if diststyle != 'KEY' and has_distkey:
|
|
64
|
+
raise sa.exc.ArgumentError(
|
|
65
|
+
u"DISTSTYLE EVEN/ALL is not compatible with a DISTKEY."
|
|
66
|
+
)
|
|
67
|
+
if diststyle == 'KEY' and not has_distkey:
|
|
68
|
+
raise sa.exc.ArgumentError(
|
|
69
|
+
u"DISTKEY specification is required for DISTSTYLE KEY"
|
|
70
|
+
)
|
|
71
|
+
text += " DISTSTYLE " + diststyle
|
|
72
|
+
|
|
73
|
+
if has_distkey:
|
|
74
|
+
if isinstance(distkey, sa.Column):
|
|
75
|
+
distkey = distkey.name
|
|
76
|
+
text += " DISTKEY ({0})".format(preparer.quote(distkey))
|
|
77
|
+
|
|
78
|
+
has_sortkey = _check_if_key_exists(sortkey)
|
|
79
|
+
has_interleaved = _check_if_key_exists(interleaved_sortkey)
|
|
80
|
+
if has_sortkey and has_interleaved:
|
|
81
|
+
raise sa.exc.ArgumentError(
|
|
82
|
+
"Parameters sortkey and interleaved_sortkey are "
|
|
83
|
+
"mutually exclusive; you may not specify both."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if has_sortkey or has_interleaved:
|
|
87
|
+
keys = sortkey if has_sortkey else interleaved_sortkey
|
|
88
|
+
if isinstance(keys, (str, sa.Column)):
|
|
89
|
+
keys = [keys]
|
|
90
|
+
keys = [key.name if isinstance(key, sa.Column) else key
|
|
91
|
+
for key in keys]
|
|
92
|
+
if has_interleaved:
|
|
93
|
+
text += " INTERLEAVED"
|
|
94
|
+
sortkey_string = ", ".join(preparer.quote(key)
|
|
95
|
+
for key in keys)
|
|
96
|
+
text += " SORTKEY ({0})".format(sortkey_string)
|
|
97
|
+
return text
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CreateMaterializedView(DDLElement):
|
|
101
|
+
"""
|
|
102
|
+
DDLElement to create a materialized view in Redshift where the distribution
|
|
103
|
+
options can be set.
|
|
104
|
+
SEE:
|
|
105
|
+
docs.aws.amazon.com/redshift/latest/dg/materialized-view-create-sql-command
|
|
106
|
+
|
|
107
|
+
This works for any selectable. Consider the trivial example of this table:
|
|
108
|
+
|
|
109
|
+
>>> import sqlalchemy as sa
|
|
110
|
+
>>> from sqlalchemy_redshift.dialect import CreateMaterializedView
|
|
111
|
+
>>> engine = sa.create_engine('redshift+psycopg2://example')
|
|
112
|
+
>>> metadata = sa.MetaData()
|
|
113
|
+
>>> user = sa.Table(
|
|
114
|
+
... 'user',
|
|
115
|
+
... metadata,
|
|
116
|
+
... sa.Column('id', sa.Integer, primary_key=True),
|
|
117
|
+
... sa.Column('name', sa.String)
|
|
118
|
+
... )
|
|
119
|
+
>>> selectable = sa.select([user.c.id, user.c.name], from_obj=user)
|
|
120
|
+
>>> view = CreateMaterializedView(
|
|
121
|
+
... 'materialized_view_of_users',
|
|
122
|
+
... selectable,
|
|
123
|
+
... distkey='id',
|
|
124
|
+
... sortkey='name'
|
|
125
|
+
... )
|
|
126
|
+
>>> print(view.compile(engine))
|
|
127
|
+
<BLANKLINE>
|
|
128
|
+
CREATE MATERIALIZED VIEW materialized_view_of_users
|
|
129
|
+
DISTKEY (id) SORTKEY (name)
|
|
130
|
+
AS SELECT "user".id, "user".name
|
|
131
|
+
FROM "user"
|
|
132
|
+
<BLANKLINE>
|
|
133
|
+
<BLANKLINE>
|
|
134
|
+
|
|
135
|
+
The materialized view can take full advantage of Redshift's distributed
|
|
136
|
+
architecture via distribution styles and sort keys.
|
|
137
|
+
|
|
138
|
+
The CreateMaterializedView is a DDLElement, so it can be executed via any
|
|
139
|
+
execute() command, be it from an Engine, Session, or Connection.
|
|
140
|
+
"""
|
|
141
|
+
def __init__(self, name, selectable, backup=True, diststyle=None,
|
|
142
|
+
distkey=None, sortkey=None, interleaved_sortkey=None):
|
|
143
|
+
"""
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
name: str, required
|
|
147
|
+
the name of the materialized view to be created.
|
|
148
|
+
selectable: str, required
|
|
149
|
+
the sqlalchemy selectable to be the base query for the view.
|
|
150
|
+
diststyle: str, optional
|
|
151
|
+
The diststle to use for the table attributes. This must be one of:
|
|
152
|
+
("ALL", "EVEN", "KEY"). If unset, Redshift passes AUTO. If KEY is
|
|
153
|
+
used, a distkey argument must be provided. Inversely, if
|
|
154
|
+
a diststyle other than KEY is provided, a distkey argument cannot
|
|
155
|
+
be provided.
|
|
156
|
+
distkey: str or sqlalchemy.Column, optional
|
|
157
|
+
The distribution key to use the for the table attributes. This can
|
|
158
|
+
be provided without any distsyle specified or with KEY diststyle
|
|
159
|
+
specified.
|
|
160
|
+
sortkey: str or sqlalchemy.Column (or iterable thereof), optional
|
|
161
|
+
The (compound) sort key(s) to use for the table attributes.
|
|
162
|
+
Mutually exclusive option from interleaved_sortkey.
|
|
163
|
+
interleaved_sortkey: str or sqlalchemy.Column (or iterable), optional
|
|
164
|
+
The (interleaved) sort key(s) to use for the table attributes.
|
|
165
|
+
Mutually exclusive option from sortkey.
|
|
166
|
+
"""
|
|
167
|
+
self.name = name
|
|
168
|
+
self.selectable = selectable
|
|
169
|
+
self.backup = backup
|
|
170
|
+
self.diststyle = diststyle
|
|
171
|
+
self.distkey = distkey
|
|
172
|
+
self.sortkey = sortkey
|
|
173
|
+
self.interleaved_sortkey = interleaved_sortkey
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@sa_compiler.compiles(CreateMaterializedView)
|
|
177
|
+
def compile_create_materialized_view(element, compiler, **kw):
|
|
178
|
+
"""
|
|
179
|
+
Returns the sql query that creates the materialized view
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
text = """\
|
|
183
|
+
CREATE MATERIALIZED VIEW {name}
|
|
184
|
+
{backup}
|
|
185
|
+
{table_attributes}
|
|
186
|
+
AS {selectable}\
|
|
187
|
+
"""
|
|
188
|
+
table_attributes = get_table_attributes(
|
|
189
|
+
compiler.preparer,
|
|
190
|
+
diststyle=element.diststyle,
|
|
191
|
+
distkey=element.distkey,
|
|
192
|
+
sortkey=element.sortkey,
|
|
193
|
+
interleaved_sortkey=element.interleaved_sortkey
|
|
194
|
+
)
|
|
195
|
+
# Defaults to yes, so omit default cas3
|
|
196
|
+
backup = "" if element.backup else "BACKUP NO "
|
|
197
|
+
selectable = compiler.sql_compiler.process(element.selectable,
|
|
198
|
+
literal_binds=True)
|
|
199
|
+
text = text.format(
|
|
200
|
+
name=element.name,
|
|
201
|
+
backup=backup,
|
|
202
|
+
table_attributes=table_attributes,
|
|
203
|
+
selectable=selectable
|
|
204
|
+
)
|
|
205
|
+
# Clean it up to have no leading spaces
|
|
206
|
+
text = "\n".join([line.strip() for line in text.split("\n")
|
|
207
|
+
if line.strip()])
|
|
208
|
+
return text
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class DropMaterializedView(DDLElement):
|
|
212
|
+
"""
|
|
213
|
+
Drop the materialized view from the database.
|
|
214
|
+
SEE:
|
|
215
|
+
docs.aws.amazon.com/redshift/latest/dg/materialized-view-drop-sql-command
|
|
216
|
+
|
|
217
|
+
This undoes the create command, as expected:
|
|
218
|
+
|
|
219
|
+
>>> import sqlalchemy as sa
|
|
220
|
+
>>> from sqlalchemy_redshift.dialect import DropMaterializedView
|
|
221
|
+
>>> engine = sa.create_engine('redshift+psycopg2://example')
|
|
222
|
+
>>> drop = DropMaterializedView(
|
|
223
|
+
... 'materialized_view_of_users',
|
|
224
|
+
... if_exists=True
|
|
225
|
+
... )
|
|
226
|
+
>>> print(drop.compile(engine))
|
|
227
|
+
<BLANKLINE>
|
|
228
|
+
DROP MATERIALIZED VIEW IF EXISTS materialized_view_of_users
|
|
229
|
+
<BLANKLINE>
|
|
230
|
+
<BLANKLINE>
|
|
231
|
+
|
|
232
|
+
This can be included in any execute() statement.
|
|
233
|
+
"""
|
|
234
|
+
def __init__(self, name, if_exists=False, cascade=False):
|
|
235
|
+
"""
|
|
236
|
+
Build the DropMaterializedView DDLElement.
|
|
237
|
+
|
|
238
|
+
Parameters
|
|
239
|
+
----------
|
|
240
|
+
name: str
|
|
241
|
+
name of the materialized view to drop
|
|
242
|
+
if_exists: bool, optional
|
|
243
|
+
if True, the IF EXISTS clause is added. This will make the query
|
|
244
|
+
successful even if the view does not exist, i.e. it lets you drop
|
|
245
|
+
a non-existant view. Defaults to False.
|
|
246
|
+
cascade: bool, optional
|
|
247
|
+
if True, the CASCADE clause is added. This will drop all
|
|
248
|
+
views/objects in the DB that depend on this materialized view.
|
|
249
|
+
Defaults to False.
|
|
250
|
+
"""
|
|
251
|
+
self.name = name
|
|
252
|
+
self.if_exists = if_exists
|
|
253
|
+
self.cascade = cascade
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@sa_compiler.compiles(DropMaterializedView)
|
|
257
|
+
def compile_drop_materialized_view(element, compiler, **kw):
|
|
258
|
+
"""
|
|
259
|
+
Formats and returns the drop statement for materialized views.
|
|
260
|
+
"""
|
|
261
|
+
text = "DROP MATERIALIZED VIEW {if_exists}{name}{cascade}"
|
|
262
|
+
if_exists = "IF EXISTS " if element.if_exists else ""
|
|
263
|
+
cascade = " CASCADE" if element.cascade else ""
|
|
264
|
+
return text.format(if_exists=if_exists, name=element.name, cascade=cascade)
|