mirror of
https://github.com/maxkratz/edgedb.git
synced 2024-09-16 18:59:05 +00:00
e00878e755
Queries like: ``` with noobs := { ((insert Person { name := "foo" }), "bar"), ((insert Person { name := "spam" }), "eggs"), }, select noobs; ``` would return an empty set, since (for annoying reasons that could possibly be avoided in this simple case but not in the general case), we need to do an `ensure_source_rvar` on `noobs`, but `get_nearest_dml_stmt` is highly limited and can only find one DML statement. This means we can't produce the proper overlay that includes both Persons. Upgrade `get_nearest_dml_stmt` to be able to collect all of the relevant DML sources. Fixes #6057. That bug was just a special case of #3030; this fix thus represents a combination of forward progress on #3030 as well as some lateral movement: as discussed in that issue, some queries are now *differently* wrong (see test_edgeql_update_union_overlay_02, which previously would have returned the old value twice instead of the new value twice). I am going to close #3030 in favor of an updated #6222 once this is merged.
667 lines
19 KiB
Python
667 lines
19 KiB
Python
#
|
|
# This source file is part of the EdgeDB open source project.
|
|
#
|
|
# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
|
|
|
|
import itertools
|
|
|
|
import edgedb
|
|
|
|
from edb.testbase import server as tb
|
|
|
|
|
|
class TestDelete(tb.QueryTestCase):
|
|
SETUP = """
|
|
START MIGRATION TO {
|
|
module default {
|
|
type LinkingType {
|
|
multi link objs -> AbstractDeleteTest;
|
|
};
|
|
|
|
abstract type AbstractDeleteTest {
|
|
property name -> str;
|
|
};
|
|
|
|
type DeleteTest extending AbstractDeleteTest;
|
|
type DeleteTest2 extending AbstractDeleteTest;
|
|
};
|
|
};
|
|
POPULATE MIGRATION;
|
|
COMMIT MIGRATION;
|
|
"""
|
|
|
|
async def test_edgeql_delete_bad_01(self):
|
|
with self.assertRaisesRegex(
|
|
edgedb.QueryError,
|
|
r'cannot delete non-ObjectType object'):
|
|
|
|
await self.con.execute('''\
|
|
DELETE 42;
|
|
''')
|
|
|
|
async def test_edgeql_delete_bad_02(self):
|
|
async with self.assertRaisesRegexTx(
|
|
edgedb.QueryError,
|
|
r'free objects cannot be deleted',
|
|
):
|
|
await self.con.execute('''\
|
|
WITH foo := {bar := 1}
|
|
DELETE foo
|
|
''')
|
|
|
|
async with self.assertRaisesRegexTx(
|
|
edgedb.QueryError,
|
|
r'free objects cannot be deleted',
|
|
):
|
|
await self.con.execute('''\
|
|
DELETE std::FreeObject
|
|
''')
|
|
|
|
async def test_edgeql_delete_bad_03(self):
|
|
async with self.assertRaisesRegexTx(
|
|
edgedb.QueryError,
|
|
r'delete standard library type',
|
|
):
|
|
await self.con.execute('''\
|
|
DELETE schema::Object;
|
|
''')
|
|
|
|
async with self.assertRaisesRegexTx(
|
|
edgedb.QueryError,
|
|
r'delete standard library type',
|
|
):
|
|
await self.con.execute('''\
|
|
DELETE {default::LinkingType, schema::Object};
|
|
''')
|
|
|
|
async def test_edgeql_delete_simple_01(self):
|
|
# ensure a clean slate, not part of functionality testing
|
|
await self.con.execute(r"""
|
|
DELETE DeleteTest;
|
|
""")
|
|
|
|
await self.con.execute(r"""
|
|
INSERT DeleteTest {
|
|
name := 'delete-test'
|
|
};
|
|
""")
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT DeleteTest;
|
|
""",
|
|
[{}],
|
|
)
|
|
|
|
await self.con.execute(r"""
|
|
DELETE DeleteTest;
|
|
""")
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT DeleteTest;
|
|
""",
|
|
[],
|
|
)
|
|
|
|
async def test_edgeql_delete_simple_02(self):
|
|
id1 = str((await self.con.query_single(r"""
|
|
SELECT(INSERT DeleteTest {
|
|
name := 'delete-test1'
|
|
}) LIMIT 1;
|
|
""")).id)
|
|
|
|
id2 = str((await self.con.query_single(r"""
|
|
SELECT(INSERT DeleteTest {
|
|
name := 'delete-test2'
|
|
}) LIMIT 1;
|
|
""")).id)
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
DELETE (SELECT DeleteTest
|
|
FILTER DeleteTest.name = 'bad name');
|
|
""",
|
|
[],
|
|
)
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT DeleteTest ORDER BY DeleteTest.name;
|
|
""",
|
|
[{'id': id1}, {'id': id2}],
|
|
)
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT (DELETE (SELECT DeleteTest
|
|
FILTER DeleteTest.name = 'delete-test1'));
|
|
""",
|
|
[{'id': id1}],
|
|
)
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT DeleteTest ORDER BY DeleteTest.name;
|
|
""",
|
|
[{'id': id2}],
|
|
)
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT (DELETE (SELECT DeleteTest
|
|
FILTER DeleteTest.name = 'delete-test2'));
|
|
""",
|
|
[{'id': id2}],
|
|
)
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT DeleteTest ORDER BY DeleteTest.name;
|
|
""",
|
|
[],
|
|
)
|
|
|
|
async def test_edgeql_delete_returning_01(self):
|
|
id1 = str((await self.con.query_single(r"""
|
|
SELECT (INSERT DeleteTest {
|
|
name := 'delete-test1'
|
|
}) LIMIT 1;
|
|
""")).id)
|
|
|
|
await self.con.execute(r"""
|
|
INSERT DeleteTest {
|
|
name := 'delete-test2'
|
|
};
|
|
INSERT DeleteTest {
|
|
name := 'delete-test3'
|
|
};
|
|
""")
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT (DELETE DeleteTest
|
|
FILTER DeleteTest.name = 'delete-test1');
|
|
""",
|
|
[{'id': id1}],
|
|
)
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
WITH
|
|
D := (DELETE DeleteTest
|
|
FILTER DeleteTest.name = 'delete-test2')
|
|
SELECT D {name};
|
|
""",
|
|
[{'name': 'delete-test2'}],
|
|
)
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT
|
|
(DELETE DeleteTest
|
|
FILTER DeleteTest.name = 'delete-test3'
|
|
).name ++ '--DELETED';
|
|
""",
|
|
['delete-test3--DELETED'],
|
|
)
|
|
|
|
async def test_edgeql_delete_returning_02(self):
|
|
await self.con.execute(r"""
|
|
INSERT DeleteTest {
|
|
name := 'delete-test1'
|
|
};
|
|
INSERT DeleteTest {
|
|
name := 'delete-test2'
|
|
};
|
|
INSERT DeleteTest {
|
|
name := 'delete-test3'
|
|
};
|
|
""")
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
WITH D := (DELETE DeleteTest)
|
|
SELECT count(D);
|
|
""",
|
|
[3],
|
|
)
|
|
|
|
async def test_edgeql_delete_returning_03(self):
|
|
await self.con.execute(r"""
|
|
INSERT DeleteTest {
|
|
name := 'dt1.1'
|
|
};
|
|
INSERT DeleteTest {
|
|
name := 'dt1.2'
|
|
};
|
|
INSERT DeleteTest {
|
|
name := 'dt1.3'
|
|
};
|
|
# create a different object
|
|
INSERT DeleteTest2 {
|
|
name := 'dt2.1'
|
|
};
|
|
|
|
INSERT DeleteTest2 {
|
|
name := 'delete test2.2'
|
|
};
|
|
""")
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
WITH
|
|
D := (DELETE DeleteTest)
|
|
SELECT DeleteTest2 {
|
|
name,
|
|
foo := 'bar'
|
|
} FILTER DeleteTest2.name LIKE D.name[:2] ++ '%';
|
|
""",
|
|
[{
|
|
'name': 'dt2.1',
|
|
'foo': 'bar',
|
|
}],
|
|
)
|
|
|
|
deleted = await self.con._fetchall(
|
|
r"""
|
|
DELETE DeleteTest2;
|
|
""",
|
|
__typeids__=True,
|
|
__typenames__=True
|
|
)
|
|
|
|
self.assertTrue(hasattr(deleted[0], '__tid__'))
|
|
self.assertEqual(deleted[0].__tname__, 'default::DeleteTest2')
|
|
|
|
async def test_edgeql_delete_returning_04(self):
|
|
await self.con.execute(r"""
|
|
INSERT DeleteTest {
|
|
name := 'dt1.1'
|
|
};
|
|
INSERT DeleteTest {
|
|
name := 'dt1.2'
|
|
};
|
|
INSERT DeleteTest {
|
|
name := 'dt1.3'
|
|
};
|
|
# create a different object
|
|
INSERT DeleteTest2 {
|
|
name := 'dt2.1'
|
|
};
|
|
""")
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
WITH
|
|
# make sure that aliased deletion works as an expression
|
|
#
|
|
Q := (DELETE DeleteTest)
|
|
SELECT DeleteTest2 {
|
|
name,
|
|
count := count(Q),
|
|
} FILTER DeleteTest2.name = 'dt2.1';
|
|
""",
|
|
[{
|
|
'name': 'dt2.1',
|
|
'count': 3,
|
|
}],
|
|
)
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT (DELETE DeleteTest2) {name};
|
|
""",
|
|
[{
|
|
'name': 'dt2.1',
|
|
}],
|
|
)
|
|
|
|
async def test_edgeql_delete_returning_05(self):
|
|
await self.con.execute(r"""
|
|
INSERT DeleteTest {
|
|
name := 'dt1.1'
|
|
};
|
|
INSERT DeleteTest {
|
|
name := 'dt1.2'
|
|
};
|
|
INSERT DeleteTest {
|
|
name := 'dt1.3'
|
|
};
|
|
# create a different object
|
|
INSERT DeleteTest2 {
|
|
name := 'dt2.1'
|
|
};
|
|
""")
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
WITH
|
|
D := (DELETE DeleteTest)
|
|
# the returning clause is actually trying to simulate
|
|
# returning "stats" of deleted objects
|
|
#
|
|
SELECT DeleteTest2 {
|
|
name,
|
|
count := count(D),
|
|
} FILTER DeleteTest2.name = 'dt2.1';
|
|
""",
|
|
[{
|
|
'name': 'dt2.1',
|
|
'count': 3
|
|
}],
|
|
)
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT (DELETE DeleteTest2) {name};
|
|
""",
|
|
[{
|
|
'name': 'dt2.1',
|
|
}],
|
|
)
|
|
|
|
async def test_edgeql_delete_sugar_01(self):
|
|
await self.con.execute(r"""
|
|
FOR x IN {'1', '2', '3', '4', '5', '6'}
|
|
UNION (INSERT DeleteTest {
|
|
name := 'sugar delete ' ++ x
|
|
});
|
|
""")
|
|
|
|
await self.con.execute(r"""
|
|
DELETE
|
|
DeleteTest
|
|
FILTER
|
|
.name[-1] != '2'
|
|
ORDER BY .name
|
|
OFFSET 2 LIMIT 2;
|
|
# should delete 4 and 5
|
|
""")
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT DeleteTest.name;
|
|
""",
|
|
{
|
|
'sugar delete 1',
|
|
'sugar delete 2',
|
|
'sugar delete 3',
|
|
'sugar delete 6',
|
|
},
|
|
)
|
|
|
|
async def test_edgeql_delete_union(self):
|
|
await self.con.execute(r"""
|
|
FOR x IN {'1', '2', '3', '4', '5', '6'}
|
|
UNION (INSERT DeleteTest {
|
|
name := 'delete union ' ++ x
|
|
});
|
|
|
|
FOR x IN {'7', '8', '9'}
|
|
UNION (INSERT DeleteTest2 {
|
|
name := 'delete union ' ++ x
|
|
});
|
|
|
|
INSERT DeleteTest { name := 'not delete union 1' };
|
|
|
|
INSERT DeleteTest2 { name := 'not delete union 2' };
|
|
""")
|
|
|
|
await self.con.execute(r"""
|
|
WITH
|
|
ToDelete := (
|
|
(SELECT DeleteTest FILTER .name ILIKE 'delete union%')
|
|
UNION
|
|
(SELECT DeleteTest2 FILTER .name ILIKE 'delete union%')
|
|
)
|
|
DELETE ToDelete;
|
|
""")
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT
|
|
DeleteTest
|
|
FILTER
|
|
.name ILIKE 'delete union%';
|
|
|
|
""",
|
|
[],
|
|
)
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT
|
|
DeleteTest {name}
|
|
FILTER
|
|
.name ILIKE 'not delete union%';
|
|
|
|
""",
|
|
[{
|
|
'name': 'not delete union 1'
|
|
}],
|
|
)
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT
|
|
DeleteTest2
|
|
FILTER
|
|
.name ILIKE 'delete union%';
|
|
|
|
""",
|
|
[],
|
|
)
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
SELECT
|
|
DeleteTest2 {name}
|
|
FILTER
|
|
.name ILIKE 'not delete union%';
|
|
|
|
""",
|
|
[{
|
|
'name': 'not delete union 2'
|
|
}],
|
|
)
|
|
|
|
async def test_edgeql_delete_abstract_01(self):
|
|
await self.con.execute(r"""
|
|
|
|
INSERT DeleteTest { name := 'child of abstract 1' };
|
|
INSERT DeleteTest2 { name := 'child of abstract 2' };
|
|
""")
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
WITH
|
|
D := (
|
|
DELETE
|
|
AbstractDeleteTest
|
|
FILTER
|
|
.name ILIKE 'child of abstract%'
|
|
)
|
|
SELECT D { name } ORDER BY .name;
|
|
""",
|
|
[{
|
|
'name': 'child of abstract 1'
|
|
}, {
|
|
'name': 'child of abstract 2'
|
|
}],
|
|
)
|
|
|
|
async def test_edgeql_delete_assert_exists(self):
|
|
await self.con.execute(r"""
|
|
INSERT DeleteTest2 { name := 'x' };
|
|
""")
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
select assert_exists((delete DeleteTest2 filter .name = 'x'));
|
|
""",
|
|
[{}],
|
|
)
|
|
|
|
async def test_edgeql_delete_then_union(self):
|
|
await self.con.execute(r"""
|
|
INSERT DeleteTest2 { name := 'x' };
|
|
INSERT DeleteTest2 { name := 'y' };
|
|
""")
|
|
|
|
await self.assert_query_result(
|
|
r"""
|
|
with
|
|
delete1 := assert_exists((delete DeleteTest2 filter .name = 'x')),
|
|
delete2 := assert_exists((delete DeleteTest2 filter .name = 'y')),
|
|
select {delete1, delete2};
|
|
""",
|
|
[{}, {}],
|
|
)
|
|
|
|
async def test_edgeql_delete_multi_simultaneous_01(self):
|
|
await self.con.execute(r"""
|
|
with
|
|
a := (insert DeleteTest { name := '1' }),
|
|
b := (insert DeleteTest { name := '2' }),
|
|
c := (insert LinkingType { objs := {a, b} })
|
|
select c;
|
|
""")
|
|
|
|
dels = {
|
|
'a': '(DELETE DeleteTest)',
|
|
'b': '(DELETE LinkingType)',
|
|
}
|
|
|
|
# We want to try all the different permutations of deletion
|
|
# binding order and order that the variables are referenced in
|
|
# the body. (Somewhat upsettingly, the order that the delete CTEs
|
|
# are included into the SQL union affected the behavior.)
|
|
# All the queries look like some variant on:
|
|
#
|
|
# with
|
|
# a := (DELETE DeleteTest),
|
|
# b := (DELETE LinkingType),
|
|
# select {a, b};
|
|
|
|
for binds, uses in itertools.product(
|
|
list(itertools.permutations(dels.keys())),
|
|
list(itertools.permutations(dels.keys())),
|
|
):
|
|
bind_q = '\n'.join(
|
|
' ' * 18 + f'{k} := {dels[k]},' for k in binds
|
|
).lstrip()
|
|
q = f'''
|
|
with
|
|
{bind_q}
|
|
select {{{', '.join(uses)}}};
|
|
'''
|
|
|
|
async with self._run_and_rollback():
|
|
with self.annotate(binds=binds, uses=uses):
|
|
await self.con.execute(q)
|
|
|
|
async def test_edgeql_delete_multi_simultaneous_02(self):
|
|
populate = r"""
|
|
with
|
|
a := (insert DeleteTest { name := '1' }),
|
|
b := (insert DeleteTest2 { name := '2' }),
|
|
c := (insert LinkingType { objs := {a, b} })
|
|
select c;
|
|
"""
|
|
|
|
await self.con.execute(populate)
|
|
await self.con.execute(r"""
|
|
with
|
|
a := (DELETE AbstractDeleteTest),
|
|
b := (DELETE LinkingType),
|
|
select {a, b};
|
|
""")
|
|
|
|
await self.con.execute(populate)
|
|
await self.con.execute(r"""
|
|
with
|
|
a := (DELETE AbstractDeleteTest),
|
|
b := (DELETE LinkingType),
|
|
select {b, a};
|
|
""")
|
|
|
|
async def test_edgeql_delete_where_order_dml(self):
|
|
async with self.assertRaisesRegexTx(
|
|
edgedb.QueryError,
|
|
"INSERT statements cannot be used in a FILTER clause"):
|
|
await self.con.query('''
|
|
delete DeleteTest
|
|
filter
|
|
(INSERT DeleteTest {
|
|
name := 't1',
|
|
})
|
|
''')
|
|
|
|
async with self.assertRaisesRegexTx(
|
|
edgedb.QueryError,
|
|
"UPDATE statements cannot be used in a FILTER clause"):
|
|
await self.con.query('''
|
|
delete DeleteTest
|
|
filter
|
|
(UPDATE DeleteTest set {
|
|
name := 't1',
|
|
})
|
|
''')
|
|
|
|
async with self.assertRaisesRegexTx(
|
|
edgedb.QueryError,
|
|
"DELETE statements cannot be used in a FILTER clause"):
|
|
await self.con.query('''
|
|
delete DeleteTest
|
|
filter
|
|
(DELETE DeleteTest filter .name = 't1')
|
|
''')
|
|
|
|
async with self.assertRaisesRegexTx(
|
|
edgedb.QueryError,
|
|
"INSERT statements cannot be used in an ORDER BY clause"):
|
|
await self.con.query('''
|
|
delete DeleteTest
|
|
order by
|
|
(INSERT DeleteTest {
|
|
name := 't1',
|
|
})
|
|
limit 1
|
|
''')
|
|
|
|
async with self.assertRaisesRegexTx(
|
|
edgedb.QueryError,
|
|
"UPDATE statements cannot be used in an ORDER BY clause"):
|
|
await self.con.query('''
|
|
delete DeleteTest
|
|
order by
|
|
(UPDATE DeleteTest set {
|
|
name := 't1',
|
|
})
|
|
limit 1
|
|
''')
|
|
|
|
async with self.assertRaisesRegexTx(
|
|
edgedb.QueryError,
|
|
"DELETE statements cannot be used in an ORDER BY clause"):
|
|
await self.con.query('''
|
|
delete DeleteTest
|
|
order by
|
|
(DELETE DeleteTest filter .name = 't1')
|
|
limit 1
|
|
''')
|