Skip to content

Commit 0713ca7

Browse files
committed
Serialize SQLite catalog initialization
1 parent cceff06 commit 0713ca7

2 files changed

Lines changed: 79 additions & 5 deletions

File tree

‎src/openstatspec/sql/wide.py‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,8 +1551,8 @@ def _compensate_catalog_initialization(
15511551

15521552

15531553
def _requires_compensating_catalog_cleanup(profile_name: str) -> bool:
1554-
"""Only PostgreSQL rolls back catalog DDL without stale compensation."""
1555-
return profile_name != "postgresql"
1554+
"""Only non-transactional MySQL-wire DDL needs stale compensation."""
1555+
return profile_name in MYSQL_WIRE_PROFILES
15561556

15571557

15581558
def _catalog_residual_inventory(
@@ -1631,7 +1631,17 @@ def dolt_state_snapshot(
16311631
def _catalog_initialization_serialization(
16321632
connection: Any, *, profile_name: str,
16331633
):
1634-
"""Serialize non-transactional catalog DDL on one database server."""
1634+
"""Serialize catalog DDL across connections on one database server."""
1635+
if profile_name == "sqlite":
1636+
connection.exec_driver_sql("BEGIN IMMEDIATE")
1637+
try:
1638+
yield
1639+
except Exception:
1640+
connection.rollback()
1641+
raise
1642+
else:
1643+
connection.commit()
1644+
return
16351645
if profile_name not in MYSQL_WIRE_PROFILES:
16361646
yield
16371647
return
@@ -1695,9 +1705,15 @@ def initialize_wide_catalog(
16951705
_require_dolt_working_set_binding(
16961706
pre_dolt_state, active, phase="catalog initialization preflight",
16971707
)
1698-
connection.rollback()
1708+
if profile.name != "sqlite":
1709+
connection.rollback()
16991710
try:
1700-
with connection.begin():
1711+
mutation = (
1712+
connection.begin_nested()
1713+
if profile.name == "sqlite"
1714+
else connection.begin()
1715+
)
1716+
with mutation:
17011717
create_normative_catalog(connection, normative)
17021718
metadata.create_all(connection, tables=list(legacy))
17031719
_migrate_catalog_columns(

‎tests/test_catalog_lifecycle.py‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from decimal import Decimal
22
import sqlite3
3+
import threading
34

45
import pytest
56
from sqlalchemy import MetaData, Table
@@ -497,6 +498,63 @@ def test_stale_initializer_compensation_preserves_concurrent_verified_catalog(
497498

498499
assert _table_names(path) == before
499500

501+
def test_sqlite_initializer_serializes_failure_before_concurrent_winner(
502+
tmp_path, monkeypatch,
503+
):
504+
path = tmp_path / "serialized-concurrent-init.sqlite"
505+
database = f"sqlite:///{path}"
506+
loser_inside_migration = threading.Event()
507+
winner_started = threading.Event()
508+
release_loser = threading.Event()
509+
original_migrate = wide._migrate_catalog_columns
510+
failures = {}
511+
512+
def migrate(connection, datasets, variables, multiple_response):
513+
if threading.current_thread().name == "losing-initializer":
514+
loser_inside_migration.set()
515+
assert winner_started.wait(5)
516+
assert release_loser.wait(5)
517+
raise RuntimeError("injected catalog migration failure")
518+
return original_migrate(connection, datasets, variables, multiple_response)
519+
520+
def compensation_must_not_run(*_args, **_kwargs):
521+
raise AssertionError("SQLite must roll back DDL without stale compensation")
522+
523+
def initialize(name):
524+
if name == "winner":
525+
winner_started.set()
526+
try:
527+
openstatspec.initialize_catalog(database_url=database)
528+
except Exception as error:
529+
failures[name] = error
530+
531+
monkeypatch.setattr(wide, "_migrate_catalog_columns", migrate)
532+
monkeypatch.setattr(
533+
wide, "_compensate_catalog_initialization", compensation_must_not_run,
534+
)
535+
536+
loser = threading.Thread(
537+
target=initialize, args=("loser",), name="losing-initializer",
538+
)
539+
winner = threading.Thread(
540+
target=initialize, args=("winner",), name="winning-initializer",
541+
)
542+
loser.start()
543+
assert loser_inside_migration.wait(5)
544+
winner.start()
545+
assert winner_started.wait(5)
546+
release_loser.set()
547+
loser.join(10)
548+
winner.join(10)
549+
550+
assert not loser.is_alive()
551+
assert not winner.is_alive()
552+
assert type(failures.get("loser")) is RuntimeError
553+
assert str(failures["loser"]) == "injected catalog migration failure"
554+
assert "winner" not in failures
555+
assert openstatspec.initialize_catalog(database_url=database)["catalog"] == "verified"
556+
557+
500558
def test_mysql_catalog_initialization_lock_spans_mutation_boundary():
501559
events = []
502560

0 commit comments

Comments
 (0)