331 lines
11 KiB
Python
331 lines
11 KiB
Python
##############################################################################
|
|
#
|
|
# Copyright (c) 2009 Zope Foundation and Contributors.
|
|
# All Rights Reserved.
|
|
#
|
|
# This software is subject to the provisions of the Zope Public License,
|
|
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
|
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
|
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
|
# FOR A PARTICULAR PURPOSE.
|
|
#
|
|
##############################################################################
|
|
"""
|
|
Database schema installers
|
|
"""
|
|
import abc
|
|
import six
|
|
|
|
import logging
|
|
|
|
from ZODB.POSException import StorageError
|
|
|
|
log = logging.getLogger("relstorage")
|
|
|
|
|
|
@six.add_metaclass(abc.ABCMeta)
|
|
class AbstractSchemaInstaller(object):
|
|
|
|
# Keep this list in the same order as the schema scripts
|
|
all_tables = (
|
|
'commit_lock',
|
|
'pack_lock',
|
|
'transaction',
|
|
'new_oid',
|
|
'object_state',
|
|
'blob_chunk',
|
|
'current_object',
|
|
'object_ref',
|
|
'object_refs_added',
|
|
'pack_object',
|
|
'pack_state',
|
|
'pack_state_tid',
|
|
'temp_store',
|
|
'temp_blob_chunk',
|
|
'temp_pack_visit',
|
|
'temp_undo',
|
|
)
|
|
|
|
database_type = None # provided by a subclass
|
|
|
|
def __init__(self, connmanager, runner, keep_history):
|
|
self.connmanager = connmanager
|
|
self.runner = runner
|
|
self.keep_history = keep_history
|
|
|
|
@abc.abstractmethod
|
|
def list_tables(self, cursor):
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def list_sequences(self, cursor):
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def get_database_name(self, cursor):
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def _create_commit_lock(self, cursor):
|
|
"""
|
|
Create the global lock held during commit.
|
|
|
|
(MySQL and PostgreSQL do this differently.)
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def _create_pack_lock(self, cursor):
|
|
"""
|
|
Create the global lock held during pack.
|
|
|
|
(MySQL and PostgreSQL do this differently.)
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def _create_transaction(self, cursor):
|
|
"""
|
|
The transaction table lists all the transactions in the database.
|
|
|
|
This table is only used for history-preserving databases.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
|
|
@abc.abstractmethod
|
|
def _create_new_oid(self, cursor):
|
|
"""
|
|
Create the incrementing sequence for new OIDs.
|
|
|
|
This should be the same for history free and preserving
|
|
schemas.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def _create_object_state(self, cursor):
|
|
"""
|
|
Create the table holding all object states for all transactions.
|
|
|
|
If the schema is history-free, only store the current state.
|
|
History-preserving schemas may have a NULL `object_state` to represent
|
|
uncreation.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def _create_blob_chunk(self, cursor):
|
|
"""
|
|
Create the table holding all blob states for all transactions.
|
|
|
|
If the schema is history-free, only store the current state.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def _create_current_object(self, cursor):
|
|
"""
|
|
Table that stores pointers to the current object state.
|
|
|
|
This table is only used for history-preserving databases.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def _create_object_ref(self, cursor):
|
|
"""
|
|
A list of referenced OIDs from each object_state. This
|
|
table is populated as needed during packing. To prevent unnecessary
|
|
table locking, it does not use foreign keys, which is safe because
|
|
rows in object_state are never modified once committed, and rows are
|
|
removed from object_state only by packing.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def _create_object_refs_added(self, cursor):
|
|
"""
|
|
The object_refs_added table tracks whether object_refs has been
|
|
populated for all states in a given transaction. An entry is added
|
|
only when the work is finished. To prevent unnecessary table locking,
|
|
it does not use foreign keys, which is safe because object states are
|
|
never added to a transaction once committed, and rows are removed
|
|
from the transaction table only by packing.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def _create_pack_object(self, cursor):
|
|
"""
|
|
pack_object contains temporary state during garbage collection: The
|
|
list of all objects, a flag signifying whether the object should be
|
|
kept, and a flag signifying whether the object's references have been
|
|
visited. The keep_tid field specifies the current revision of the
|
|
object.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def _create_pack_state(self, cursor):
|
|
"""
|
|
Temporary state during packing: the list of object states
|
|
# to pack.
|
|
|
|
This is only used in history-preserving databases.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@abc.abstractmethod
|
|
def _create_pack_state_tid(self, cursor):
|
|
"""
|
|
Temporary state during packing: the list of
|
|
transactions that have at least one object state to pack.
|
|
|
|
This is only used in history-preserving databases.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
|
|
# Most databases handle temp tables on a session-by-session
|
|
# basis.
|
|
def _create_temp_store(self, _cursor):
|
|
"""States that will soon be stored."""
|
|
return
|
|
|
|
def _create_temp_blob_chunk(self, _cursor):
|
|
"""
|
|
Temporary state during packing: a list of objects
|
|
whose references need to be examined.
|
|
"""
|
|
return
|
|
|
|
def _create_temp_pack_visit(self, _cursor):
|
|
return
|
|
|
|
def _create_temp_undo(self, _cursor):
|
|
"""
|
|
Temporary state during undo: a list of objects
|
|
to be undone and the tid of the undone state.
|
|
"""
|
|
return
|
|
|
|
def _init_after_create(self, cursor):
|
|
"""
|
|
Create a special '0' transaction to represent object creation. The
|
|
'0' transaction is often referenced by object_state.prev_tid, but
|
|
never by object_state.tid.
|
|
|
|
Only in history-preserving databases.
|
|
"""
|
|
if self.keep_history:
|
|
stmt = """
|
|
INSERT INTO transaction (tid, username, description)
|
|
VALUES (0, 'system', 'special transaction for object creation');
|
|
"""
|
|
self.runner.run_script(cursor, stmt)
|
|
|
|
@abc.abstractmethod
|
|
def _reset_oid(self, cursor):
|
|
raise NotImplementedError()
|
|
|
|
def create(self, cursor):
|
|
"""Create the database tables."""
|
|
for table in self.all_tables:
|
|
meth = getattr(self, '_create_' + table)
|
|
meth(cursor)
|
|
|
|
self._init_after_create(cursor)
|
|
|
|
tables = self.list_tables(cursor)
|
|
self.check_compatibility(cursor, tables)
|
|
|
|
def prepare(self):
|
|
"""Create the database schema if it does not already exist."""
|
|
# XXX: We can generalize this to handle triggers, procs, etc,
|
|
# to make subclasses have easier time.
|
|
def callback(_conn, cursor):
|
|
tables = self.list_tables(cursor)
|
|
if 'object_state' not in tables:
|
|
self.create(cursor)
|
|
else:
|
|
self.check_compatibility(cursor, tables)
|
|
self.update_schema(cursor, tables)
|
|
self.connmanager.open_and_call(callback)
|
|
|
|
def check_compatibility(self, cursor, tables): # pylint:disable=unused-argument
|
|
if self.keep_history:
|
|
if 'transaction' not in tables and 'current_object' not in tables:
|
|
raise StorageError(
|
|
"Schema mismatch: a history-preserving adapter "
|
|
"can not connect to a history-free database. "
|
|
"If you need to convert, use the zodbconvert utility."
|
|
)
|
|
else:
|
|
if 'transaction' in tables and 'current_object' in tables:
|
|
raise StorageError(
|
|
"Schema mismatch: a history-free adapter "
|
|
"can not connect to a history-preserving database. "
|
|
"If you need to convert, use the zodbconvert utility."
|
|
)
|
|
if 'blob_chunk' not in tables:
|
|
raise StorageError(
|
|
"Schema mismatch; please create the blob_chunk tables."
|
|
"See migration instructions for RelStorage 1.5."
|
|
)
|
|
|
|
def update_schema(self, cursor, tables):
|
|
pass
|
|
|
|
_zap_all_tbl_stmt = 'DELETE FROM %s'
|
|
|
|
def zap_all(self, reset_oid=True, slow=False):
|
|
"""
|
|
Clear all data out of the database.
|
|
|
|
:keyword bool slow: If True (*not* the default) then database
|
|
specific optimizations will be skipped and rows will simply be
|
|
DELETEd. This is helpful when other connections might be open and
|
|
holding some kind of locks.
|
|
"""
|
|
stmt = self._zap_all_tbl_stmt if not slow else AbstractSchemaInstaller._zap_all_tbl_stmt
|
|
|
|
def callback(_conn, cursor):
|
|
existent = set(self.list_tables(cursor))
|
|
todo = reversed(self.all_tables)
|
|
log.debug("Checking tables: %r", todo)
|
|
for table in todo:
|
|
log.debug("Considering table %s", table)
|
|
if table.startswith('temp_'):
|
|
continue
|
|
if table in existent:
|
|
log.debug("Deleting from table %s...", table)
|
|
cursor.execute(stmt % table)
|
|
log.debug("Done deleting from tables.")
|
|
|
|
log.debug("Running init script.")
|
|
self._init_after_create(cursor)
|
|
log.debug("Done running init script.")
|
|
|
|
if reset_oid:
|
|
log.debug("Running OID reset script.")
|
|
self._reset_oid(cursor)
|
|
log.debug("Done running OID reset script.")
|
|
|
|
self.connmanager.open_and_call(callback)
|
|
|
|
|
|
def drop_all(self):
|
|
"""Drop all tables and sequences."""
|
|
def callback(_conn, cursor):
|
|
existent = set(self.list_tables(cursor))
|
|
todo = list(self.all_tables)
|
|
todo.reverse()
|
|
for table in todo:
|
|
if table in existent:
|
|
cursor.execute("DROP TABLE %s" % table)
|
|
for sequence in self.list_sequences(cursor):
|
|
cursor.execute("DROP SEQUENCE %s" % sequence)
|
|
self.connmanager.open_and_call(callback)
|