Skip to content

Commit 6816617

Browse files
committed
[view] Support CREATE OR REPLACE
1 parent 4b05d05 commit 6816617

File tree

2 files changed

+72
-8
lines changed

2 files changed

+72
-8
lines changed

sqlalchemy_utils/view.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@
77

88

99
class CreateView(DDLElement):
10-
def __init__(self, name, selectable, materialized=False):
10+
def __init__(self, name, selectable, materialized=False, replace=False):
11+
if materialized and replace:
12+
raise ValueError("Cannot use CREATE OR REPLACE with materialized views")
1113
self.name = name
1214
self.selectable = selectable
1315
self.materialized = materialized
16+
self.replace = replace
1417

1518

1619
@compiler.compiles(CreateView)
1720
def compile_create_materialized_view(element, compiler, **kw):
18-
return 'CREATE {}VIEW {} AS {}'.format(
21+
return 'CREATE {}{}VIEW {} AS {}'.format(
22+
'OR REPLACE ' if element.replace else '',
1923
'MATERIALIZED ' if element.materialized else '',
2024
compiler.dialect.identifier_preparer.quote(element.name),
2125
compiler.sql_compiler.process(element.selectable, literal_binds=True),
@@ -124,7 +128,8 @@ def create_view(
124128
name,
125129
selectable,
126130
metadata,
127-
cascade_on_drop=True
131+
cascade_on_drop=True,
132+
replace=False,
128133
):
129134
""" Create a view on a given metadata
130135
@@ -164,7 +169,11 @@ def create_view(
164169
metadata=None
165170
)
166171

167-
sa.event.listen(metadata, 'after_create', CreateView(name, selectable))
172+
sa.event.listen(
173+
metadata,
174+
'after_create',
175+
CreateView(name, selectable, replace=replace),
176+
)
168177

169178
@sa.event.listens_for(metadata, 'after_create')
170179
def create_indexes(target, connection, **kw):

tests/test_views.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,15 @@ def life_cycle(
121121
engine,
122122
metadata,
123123
column,
124-
cascade_on_drop
124+
cascade_on_drop,
125+
replace=False,
125126
):
126127
__table__ = create_view(
127128
name='trivial_view',
128129
selectable=sa.select(*_select_args(column)),
129130
metadata=metadata,
130-
cascade_on_drop=cascade_on_drop
131+
cascade_on_drop=cascade_on_drop,
132+
replace=replace,
131133
)
132134
__table__.create(engine)
133135
__table__.drop(engine)
@@ -164,13 +166,66 @@ def test_life_cycle_no_cascade(
164166
self.life_cycle(engine, Base.metadata, User.id, cascade_on_drop=False)
165167

166168

169+
class SupportsReplace(TrivialViewTestCases):
170+
def test_life_cycle_replace(
171+
self,
172+
connection,
173+
engine,
174+
Base,
175+
User
176+
):
177+
self.life_cycle(
178+
engine,
179+
Base.metadata,
180+
User.id,
181+
cascade_on_drop=False,
182+
replace=True,
183+
)
184+
185+
def test_life_cycle_replace_existing(
186+
self,
187+
connection,
188+
engine,
189+
Base,
190+
User
191+
):
192+
__table__ = create_view(
193+
name='trivial_view',
194+
selectable=sa.select(*_select_args(User.id)),
195+
metadata=Base.metadata,
196+
)
197+
__table__.create(engine)
198+
self.life_cycle(
199+
engine,
200+
Base.metadata,
201+
User.id,
202+
cascade_on_drop=False,
203+
replace=True,
204+
)
205+
206+
def test_replace_materialized(
207+
self,
208+
connection,
209+
engine,
210+
Base,
211+
User
212+
):
213+
with pytest.raises(ValueError):
214+
create_materialized_view(
215+
name='trivial_view',
216+
selectable=sa.select(*_select_args(User.id)),
217+
metadata=Base.metadata,
218+
replace=True,
219+
)
220+
221+
167222
@pytest.mark.usefixtures('postgresql_dsn')
168-
class TestPostgresTrivialView(SupportsCascade, SupportsNoCascade):
223+
class TestPostgresTrivialView(SupportsCascade, SupportsNoCascade, SupportsReplace):
169224
pass
170225

171226

172227
@pytest.mark.usefixtures('mysql_dsn')
173-
class TestMySqlTrivialView(SupportsCascade, SupportsNoCascade):
228+
class TestMySqlTrivialView(SupportsCascade, SupportsNoCascade, SupportsReplace):
174229
pass
175230

176231

0 commit comments

Comments
 (0)