Skip to content

Commit fa70db5

Browse files
authored
Fix administer schema_repair() (#8730)
`administer schema_repair()` had a bug where, if any repairs were done, it set the in-memory schema to be the "reloaded_schema" used to compare against for mismatches. This schema is equivalent to the original, except that all of the ids were different. This meant that any queries that were compiled using that schema would be broken, and would try to query nonexistent tables. This only affected the `administer` codepath; running `schema_repair` as part of patches during version upgrades still worked. If no uncached queries were compiled before the server was restarted, this might not be noticed, which I suspect is what happened with previous uses of `administer schema_repair()`. Previously schema_repair was not really tested, since it typically only does anything when a bug resulted in a broken schema. I've added infrastructure for intentionally breaking the schema (by UPDATEing the reflection schema), and a couple tests.
1 parent cce73c8 commit fa70db5

File tree

4 files changed

+129
-16
lines changed

4 files changed

+129
-16
lines changed

edb/server/bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,7 @@ def prepare_repair_patch(
704704
res = edbcompiler.repair_schema(compilerctx)
705705
if not res:
706706
return ""
707-
sql, _, _ = res
707+
sql, _ = res
708708

709709
return sql.decode('utf-8')
710710

edb/server/compiler/ddl.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,7 +1318,7 @@ def _track(key: str) -> None:
13181318

13191319
def repair_schema(
13201320
ctx: compiler.CompileContext,
1321-
) -> Optional[tuple[bytes, s_schema.Schema, Any]]:
1321+
) -> Optional[tuple[bytes, Any]]:
13221322
"""Repair inconsistencies in the schema caused by bug fixes
13231323
13241324
Works by comparing the actual current schema to the schema we get
@@ -1386,7 +1386,7 @@ def repair_schema(
13861386
debug.header('Repair Delta Script')
13871387
debug.dump_code(sql, lexer='sql')
13881388

1389-
return sql, reloaded_schema, config_ops
1389+
return sql, config_ops
13901390

13911391

13921392
def administer_repair_schema(
@@ -1404,9 +1404,7 @@ def administer_repair_schema(
14041404
res = repair_schema(ctx)
14051405
if not res:
14061406
return dbstate.MaintenanceQuery(sql=b"")
1407-
sql, new_schema, config_ops = res
1408-
1409-
current_tx.update_schema(new_schema)
1407+
sql, config_ops = res
14101408

14111409
return dbstate.DDLQuery(
14121410
sql=sql,

tests/test_edgeql_data_migration.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12117,6 +12117,10 @@ async def test_edgeql_migration_trigger_shift_01(self):
1211712117
class TestEdgeQLDataMigrationNonisolated(EdgeQLDataMigrationTestCase):
1211812118
TRANSACTION_ISOLATION = False
1211912119

12120+
PER_TEST_TEARDOWN = [
12121+
'reset schema to initial'
12122+
]
12123+
1212012124
async def test_edgeql_migration_eq_collections_25(self):
1212112125
await self.con.execute(r"""
1212212126
START MIGRATION TO {
@@ -12439,6 +12443,127 @@ async def test_edgeql_migration_extension_01(self):
1243912443
tb.bag([[3, 1, 4], [5, 6, 0]]),
1244012444
)
1244112445

12446+
async def _test_schema_repair(
12447+
self, *,
12448+
schema,
12449+
setup_script=None,
12450+
breakage_script,
12451+
check_breakage=True,
12452+
):
12453+
'''Helper for testing ADMINISTER schema_repair()
12454+
12455+
Takes a schema and an optional setup_script and runs them.
12456+
12457+
Then runs the breakage_script, which will be run with access
12458+
to the reflschema and which should break the schema in some
12459+
way.
12460+
12461+
We will then load back the broken schema, verify migrations
12462+
are broken, run schema_repair, and verify migrations work.
12463+
'''
12464+
12465+
await self.migrate(schema, module='default')
12466+
if setup_script:
12467+
await self.con.execute(setup_script)
12468+
12469+
# Use a transaction to make sure the configures get rolled back
12470+
async with self.con.transaction():
12471+
# Enable poking directly at the schema
12472+
await self.con.execute('''
12473+
configure session set __internal_query_reflschema := true;
12474+
configure session set __internal_no_apply_query_rewrites :=
12475+
true;
12476+
''')
12477+
12478+
# Break the schema
12479+
await self.con.execute(breakage_script)
12480+
12481+
# Do some random configure current database in order to force
12482+
# a reload of the schema from reflection.
12483+
await self.con.execute("""
12484+
configure current database reset __internal_sess_testvalue
12485+
""")
12486+
12487+
# Make sure we succesfully busted the schema.
12488+
async with self.assertRaisesRegexTx(
12489+
# Error could be the 'complete' assertion in this test
12490+
# suite or some failure in the server.
12491+
(AssertionError, edgedb.EdgeDBError),
12492+
'',
12493+
):
12494+
await self.migrate(schema, module='default')
12495+
12496+
await self.con.execute('''
12497+
administer schema_repair()
12498+
''')
12499+
12500+
# Make sure the schema is repaired
12501+
async with self._run_and_rollback():
12502+
await self.start_migration(schema, module='default')
12503+
await self.assert_describe_migration({
12504+
'complete': True
12505+
})
12506+
12507+
# Make sure objects still can be fetched
12508+
await self.con.query('''
12509+
select Object
12510+
''')
12511+
12512+
async def test_edgeql_migration_schema_repair_01(self):
12513+
await self._test_schema_repair(
12514+
schema='''
12515+
type T {
12516+
lol := count(Object);
12517+
foo: str;
12518+
}
12519+
type S extending T;
12520+
''',
12521+
setup_script='''
12522+
insert T { foo := "t" };
12523+
insert S { foo := "s" };
12524+
''',
12525+
breakage_script='''
12526+
update schema::Property
12527+
filter .name__internal = 'default::__|foo@default|S'
12528+
set { required := true };
12529+
''',
12530+
)
12531+
12532+
# Check that a query works and the data is still there
12533+
await self.assert_query_result(
12534+
'''
12535+
select T { foo };
12536+
''',
12537+
tb.bag([
12538+
{'foo': 't'},
12539+
{'foo': 's'},
12540+
])
12541+
)
12542+
12543+
async def test_edgeql_migration_schema_repair_02(self):
12544+
await self._test_schema_repair(
12545+
schema='''
12546+
abstract type Named {
12547+
index std::fts::index on (
12548+
std::fts::with_options(
12549+
.name, language := std::fts::Language.eng));
12550+
required name: std::str;
12551+
};
12552+
12553+
type Project0 extending Named;
12554+
''',
12555+
breakage_script='''
12556+
update schema::Index
12557+
filter .name__internal =
12558+
'default::std|fts|index@default|Project0' ++
12559+
'@bb75b87f16a631d2bd48c6c660d4b52a8b7274cb'
12560+
set { name__internal :=
12561+
'default::fts|index@default|Project0' ++
12562+
'@bb75b87f16a631d2bd48c6c660d4b52a8b7274cb'
12563+
}
12564+
''',
12565+
)
12566+
1244212567

1244312568
class EdgeQLAIMigrationTestCase(EdgeQLDataMigrationTestCase):
1244412569
# AI specific tests, just because creating the extension is so slow,

tests/test_edgeql_ddl.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18878,16 +18878,6 @@ async def test_edgeql_ddl_rebase_views_02(self):
1887818878
[{}],
1887918879
)
1888018880

18881-
async def test_edgeql_ddl_schema_repair(self):
18882-
await self.con.execute('''
18883-
create type Tgt {
18884-
create property lol := count(Object)
18885-
}
18886-
''')
18887-
await self.con.execute('''
18888-
administer schema_repair()
18889-
''')
18890-
1889118881
async def test_edgeql_ddl_alias_and_create_set_required(self):
1889218882
await self.con.execute(r"""
1889318883
create type T;

0 commit comments

Comments
 (0)