def test_not_upgraded_current_schema_version_with_outstanding_deltas(self): """ Test that workers don't start if the DB is on the current schema version, but there are still outstanding delta migrations to run. """ db_pool = self.hs.get_datastore().db_pool db_conn = LoggingDatabaseConnection( db_pool._db_pool.connect(), db_pool.engine, "tests", ) # Set the schema version of the database to the current version cur = db_conn.cursor() cur.execute("UPDATE schema_version SET version = ?", (SCHEMA_VERSION, )) db_conn.commit() # Path `os.listdir` here to make synapse think that there is a migration # file ready to be run. # Note that we can't patch this function for the whole method, else Synapse # will try to find the file when building the database initially. with mock.patch("os.listdir", mock.Mock(side_effect=fake_listdir)): with self.assertRaises(PrepareDatabaseException): # Synapse should think that there is an outstanding migration file due to # patching 'os.listdir' in the function decorator. # # We expect Synapse to raise an exception to indicate the master process # needs to apply this migration file. prepare_database(db_conn, db_pool.engine, self.hs.config)
def test_not_upgraded_old_schema_version(self): """Test that workers don't start if the DB has an older schema version""" db_pool = self.hs.get_datastore().db_pool db_conn = LoggingDatabaseConnection( db_pool._db_pool.connect(), db_pool.engine, "tests", ) cur = db_conn.cursor() cur.execute("UPDATE schema_version SET version = ?", (SCHEMA_VERSION - 1,)) db_conn.commit() with self.assertRaises(PrepareDatabaseException): prepare_database(db_conn, db_pool.engine, self.hs.config)
def test_rolling_back(self): """Test that workers can start if the DB is a newer schema version""" db_pool = self.hs.get_datastore().db_pool db_conn = LoggingDatabaseConnection( db_pool._db_pool.connect(), db_pool.engine, "tests", ) cur = db_conn.cursor() cur.execute("UPDATE schema_version SET version = ?", (SCHEMA_VERSION + 1,)) db_conn.commit() prepare_database(db_conn, db_pool.engine, self.hs.config)
def prepare_database( db_conn: LoggingDatabaseConnection, database_engine: BaseDatabaseEngine, config: Optional[HomeServerConfig], databases: Collection[str] = ("main", "state"), ): """Prepares a physical database for usage. Will either create all necessary tables or upgrade from an older schema version. If `config` is None then prepare_database will assert that no upgrade is necessary, *or* will create a fresh database if the database is empty. Args: db_conn: database_engine: config : application config, or None if we are connecting to an existing database which we expect to be configured already databases: The name of the databases that will be used with this physical database. Defaults to all databases. """ try: cur = db_conn.cursor(txn_name="prepare_database") # sqlite does not automatically start transactions for DDL / SELECT statements, # so we start one before running anything. This ensures that any upgrades # are either applied completely, or not at all. # # (psycopg2 automatically starts a transaction as soon as we run any statements # at all, so this is redundant but harmless there.) cur.execute("BEGIN TRANSACTION") logger.info("%r: Checking existing schema version", databases) version_info = _get_or_create_schema_state(cur, database_engine) if version_info: user_version, delta_files, upgraded = version_info logger.info( "%r: Existing schema is %i (+%i deltas)", databases, user_version, len(delta_files), ) # config should only be None when we are preparing an in-memory SQLite db, # which should be empty. if config is None: raise ValueError( "config==None in prepare_database, but database is not empty" ) # if it's a worker app, refuse to upgrade the database, to avoid multiple # workers doing it at once. if config.worker_app is not None and user_version != SCHEMA_VERSION: raise UpgradeDatabaseException( OUTDATED_SCHEMA_ON_WORKER_ERROR % (SCHEMA_VERSION, user_version)) _upgrade_existing_database( cur, user_version, delta_files, upgraded, database_engine, config, databases=databases, ) else: logger.info("%r: Initialising new database", databases) # if it's a worker app, refuse to upgrade the database, to avoid multiple # workers doing it at once. if config and config.worker_app is not None: raise UpgradeDatabaseException(EMPTY_DATABASE_ON_WORKER_ERROR) _setup_new_database(cur, database_engine, databases=databases) # check if any of our configured dynamic modules want a database if config is not None: _apply_module_schemas(cur, database_engine, config) cur.close() db_conn.commit() except Exception: db_conn.rollback() raise