Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 22852f4

Browse files
author
Sean Quah
committed
Make sqlite database migrations transactional again, part two (#14910)
#14910 fixed the regression introduced by #13873 where sqlite database migrataions would no longer run inside a transaction. However, it committed the transaction before Synapse updated its bookkeeping of which migrations have been run, which means that migrations may be run again after they have completed successfully. Leave the transaction open at the end of `executescript`, to restore the old, correct behaviour. Fixes #14909. Signed-off-by: Sean Quah <[email protected]>
1 parent 8a7d2de commit 22852f4

File tree

4 files changed

+102
-4
lines changed

4 files changed

+102
-4
lines changed

changelog.d/14923.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a regression introduced in Synapse 1.69.0 which can result in database corruption when database migrations are interrupted on sqlite.

synapse/storage/engines/_base.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,9 @@ def executescript(cursor: CursorType, script: str) -> None:
133133
134134
This is not provided by DBAPI2, and so needs engine-specific support.
135135
136-
Some database engines may automatically COMMIT the ongoing transaction both
137-
before and after executing the script.
136+
Some database engines may automatically COMMIT the ongoing transaction before
137+
executing the script in its own transaction. The script transaction is left
138+
open and it is the responsibility of the caller to commit it.
138139
"""
139140
...
140141

synapse/storage/engines/sqlite.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,14 @@ def executescript(cursor: sqlite3.Cursor, script: str) -> None:
135135
> than one statement with it, it will raise a Warning. Use executescript() if
136136
> you want to execute multiple SQL statements with one call.
137137
138-
The script is wrapped in transaction control statemnets, since the docs for
138+
The script is prefixed with a `BEGIN TRANSACTION`, since the docs for
139139
`executescript` warn:
140140
141141
> If there is a pending transaction, an implicit COMMIT statement is executed
142142
> first. No other implicit transaction control is performed; any transaction
143143
> control must be added to sql_script.
144144
"""
145-
cursor.executescript(f"BEGIN TRANSACTION;\n{script}\nCOMMIT;")
145+
cursor.executescript(f"BEGIN TRANSACTION; {script}")
146146

147147

148148
# Following functions taken from: https://github.com/coleifer/peewee

tests/storage/test_database.py

+96
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from synapse.server import HomeServer
2323
from synapse.storage.database import (
2424
DatabasePool,
25+
LoggingDatabaseConnection,
2526
LoggingTransaction,
2627
make_tuple_comparison_clause,
2728
)
@@ -37,6 +38,101 @@ def test_native_tuple_comparison(self) -> None:
3738
self.assertEqual(args, [1, 2])
3839

3940

41+
class ExecuteScriptTestCase(unittest.HomeserverTestCase):
42+
"""Tests for `BaseDatabaseEngine.executescript` implementations."""
43+
44+
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
45+
self.store = hs.get_datastores().main
46+
self.db_pool: DatabasePool = self.store.db_pool
47+
self.get_success(
48+
self.db_pool.runInteraction(
49+
"create",
50+
lambda txn: txn.execute("CREATE TABLE foo (name TEXT PRIMARY KEY)"),
51+
)
52+
)
53+
54+
def test_transaction(self) -> None:
55+
"""Test that all statements are run in a single transaction."""
56+
57+
def run(conn: LoggingDatabaseConnection) -> None:
58+
cur = conn.cursor(txn_name="test_transaction")
59+
self.db_pool.engine.executescript(
60+
cur,
61+
";".join(
62+
[
63+
"INSERT INTO foo (name) VALUES ('transaction test')",
64+
# This next statement will fail. When `executescript` is not
65+
# transactional, the previous row will be observed later.
66+
"INSERT INTO foo (name) VALUES ('transaction test')",
67+
]
68+
),
69+
)
70+
71+
self.get_failure(
72+
self.db_pool.runWithConnection(run),
73+
self.db_pool.engine.module.IntegrityError,
74+
)
75+
76+
self.assertIsNone(
77+
self.get_success(
78+
self.db_pool.simple_select_one_onecol(
79+
"foo",
80+
keyvalues={"name": "transaction test"},
81+
retcol="name",
82+
allow_none=True,
83+
)
84+
),
85+
"executescript is not running statements inside a transaction",
86+
)
87+
88+
def test_commit(self) -> None:
89+
"""Test that the script transaction remains open and can be committed."""
90+
91+
def run(conn: LoggingDatabaseConnection) -> None:
92+
cur = conn.cursor(txn_name="test_commit")
93+
self.db_pool.engine.executescript(
94+
cur, "INSERT INTO foo (name) VALUES ('commit test')"
95+
)
96+
cur.execute("COMMIT")
97+
98+
self.get_success(self.db_pool.runWithConnection(run))
99+
100+
self.assertIsNotNone(
101+
self.get_success(
102+
self.db_pool.simple_select_one_onecol(
103+
"foo",
104+
keyvalues={"name": "commit test"},
105+
retcol="name",
106+
allow_none=True,
107+
)
108+
),
109+
)
110+
111+
def test_rollback(self) -> None:
112+
"""Test that the script transaction remains open and can be rolled back."""
113+
114+
def run(conn: LoggingDatabaseConnection) -> None:
115+
cur = conn.cursor(txn_name="test_rollback")
116+
self.db_pool.engine.executescript(
117+
cur, "INSERT INTO foo (name) VALUES ('rollback test')"
118+
)
119+
cur.execute("ROLLBACK")
120+
121+
self.get_success(self.db_pool.runWithConnection(run))
122+
123+
self.assertIsNone(
124+
self.get_success(
125+
self.db_pool.simple_select_one_onecol(
126+
"foo",
127+
keyvalues={"name": "rollback test"},
128+
retcol="name",
129+
allow_none=True,
130+
)
131+
),
132+
"executescript is not leaving the script transaction open",
133+
)
134+
135+
40136
class CallbacksTestCase(unittest.HomeserverTestCase):
41137
"""Tests for transaction callbacks."""
42138

0 commit comments

Comments
 (0)