Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create tables with an explicit INTEGER PRIMARY KEY. #87

Merged
merged 20 commits into from
Mar 14, 2019
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions axiom/_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,38 @@
CREATE_OBJECT = 'INSERT INTO *DATABASE*.axiom_objects (type_id) VALUES (?)'
CREATE_TYPE = 'INSERT INTO *DATABASE*.axiom_types (typename, module, version) VALUES (?, ?, ?)'

GET_TABLE_INFO = 'PRAGMA *DATABASE*.table_info(?)'

BASE_SCHEMA = ["""

# The storeID for an object must be unique over the lifetime of the store.
mithrandi marked this conversation as resolved.
Show resolved Hide resolved
# Since the storeID is allocated by inserting into axiom_objects, we use
# AUTOINCREMENT so that oids/rowids and thus storeIDs are never reused.

# The column is named "oid" instead of "storeID" for backwards compatibility
# with the implicit oid/rowid column in old Stores.
CREATE_OBJECTS = """
CREATE TABLE *DATABASE*.axiom_objects (
oid INTEGER PRIMARY KEY AUTOINCREMENT,
mithrandi marked this conversation as resolved.
Show resolved Hide resolved
type_id INTEGER NOT NULL
CONSTRAINT fk_type_id REFERENCES axiom_types(oid)
)
""",

"""

CREATE_OBJECTS_IDX = """
CREATE INDEX *DATABASE*.axiom_objects_type_idx
ON axiom_objects(type_id);
""",

"""

CREATE_TYPES = """
CREATE TABLE *DATABASE*.axiom_types (
oid INTEGER PRIMARY KEY AUTOINCREMENT,
typename VARCHAR,
module VARCHAR,
version INTEGER
)
""",

"""

CREATE_ATTRIBUTES = """
CREATE TABLE *DATABASE*.axiom_attributes (
type_id INTEGER,
row_offset INTEGER,
Expand All @@ -35,7 +45,11 @@
attribute VARCHAR,
docstring TEXT
)
"""]
"""

BASE_SCHEMA = [
CREATE_OBJECTS, CREATE_OBJECTS_IDX, CREATE_TYPES, CREATE_ATTRIBUTES]


TYPEOF_QUERY = """
SELECT *DATABASE*.axiom_types.typename, *DATABASE*.axiom_types.module, *DATABASE*.axiom_types.version
Expand All @@ -58,6 +72,8 @@

ALL_TYPES = 'SELECT oid, module, typename, version FROM *DATABASE*.axiom_types'

LATEST_TYPES = 'SELECT typename, MAX(version) FROM *DATABASE*.axiom_types GROUP BY typename'

GET_GREATER_VERSIONS_OF_TYPE = ('SELECT version FROM *DATABASE*.axiom_types '
'WHERE typename = ? AND version > ?')

Expand Down
16 changes: 10 additions & 6 deletions axiom/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -879,12 +879,16 @@ def deleteFromStore(self, deleteObject=True):

def _baseSelectSQL(cls, st):
if cls not in st.typeToSelectSQLCache:
st.typeToSelectSQLCache[cls] = ' '.join(['SELECT * FROM',
st.getTableName(cls),
'WHERE',
st.getShortColumnName(cls.storeID),
'= ?'
])
attrs = list(cls.getSchema())
st.typeToSelectSQLCache[cls] = ' '.join(
['SELECT',
', '.join(
[st.getShortColumnName(a[1]) for a in attrs]),
'FROM',
st.getTableName(cls),
'WHERE',
st.getShortColumnName(cls.storeID),
'= ?'])
return st.typeToSelectSQLCache[cls]

_baseSelectSQL = classmethod(_baseSelectSQL)
Expand Down
3 changes: 3 additions & 0 deletions axiom/plugins/axiom_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from axiom.listversions import ListVersions
from axiom import version
from axiom.iaxiom import IVersion
from axiom.upgrade import upgradeExplicitOid

directlyProvides(version, IPlugin, IVersion)

Expand Down Expand Up @@ -57,8 +58,10 @@ def upgradeStore(self, store):
Recursively upgrade C{store}.
"""
self.upgradeEverything(store)
upgradeExplicitOid(store)

for substore in store.query(SubStore):
print 'Upgrading: {!r}'.format(substore)
self.upgradeStore(substore.open())

def perform(self, store, count):
Expand Down
93 changes: 50 additions & 43 deletions axiom/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -1162,11 +1162,6 @@ def __init__(self, dbdir=None, filesdir=None, debug=False, parent=None, idInPare

self.objectCache = _fincache.FinalizingCache()

self.tableQueries = {} # map typename: query string w/ storeID
# parameter. a typename is a persistent
# database handle for what we'll call a 'FQPN',
# i.e. arg to namedAny.

self.typenameAndVersionToID = {} # map database-persistent typename and
# version to an oid in the types table

Expand Down Expand Up @@ -1989,8 +1984,13 @@ def _indexNameOf(self, tableClass, attrname):
'_'.join(attrname))


def _tableNameOnlyFor(self, typename, version):
return 'item_{}_v{:d}'.format(typename, version)


def _tableNameFor(self, typename, version):
return "%s.item_%s_v%d" % (self.databaseName, typename, version)
return '.'.join([self.databaseName,
self._tableNameOnlyFor(typename, version)])


def getTableName(self, tableClass):
Expand Down Expand Up @@ -2033,6 +2033,8 @@ def getShortColumnName(self, attribute):
that this method is used during table creation.
"""
if isinstance(attribute, _StoreIDComparer):
# The column is named "oid" instead of "storeID" for backwards
# compatibility with the implicit oid/rowid column in old Stores.
return 'oid'
return '[' + attribute.attrname + ']'

Expand Down Expand Up @@ -2074,23 +2076,14 @@ def getTypeID(self, tableClass):
return self.transact(self._maybeCreateTable, tableClass, key)


def _maybeCreateTable(self, tableClass, key):
def _justCreateTable(self, tableClass):
"""
A type ID has been requested for an Item subclass whose table was not
present when this Store was opened. Attempt to create the table, and
if that fails because another Store object (perhaps in another process)
has created the table, re-read the schema. When that's done, return
the typeID.
Execute the table creation DDL for an Item subclass.

This method is internal to the implementation of getTypeID. It must be
run in a transaction.
Indexes are *not* created.

@type tableClass: type
@param tableClass: an Item subclass
@param key: a 2-tuple of the tableClass's typeName and schemaVersion

@return: a typeID for the table; a new one if no table exists, or the
existing one if the table was created by another Store object
referencing this database.
"""
sqlstr = []
sqlarg = []
Expand All @@ -2101,20 +2094,38 @@ def _maybeCreateTable(self, tableClass, key):

sqlstr.append("CREATE TABLE %s (" % tableName)

# The column is named "oid" instead of "storeID" for backwards
# compatibility with the implicit oid/rowid column in old Stores.
sqlarg.append("oid INTEGER PRIMARY KEY")
mithrandi marked this conversation as resolved.
Show resolved Hide resolved
for nam, atr in tableClass.getSchema():
# it's a stored attribute
sqlarg.append("\n%s %s" %
(atr.getShortColumnName(self), atr.sqltype))

if len(sqlarg) == 0:
# XXX should be raised way earlier, in the class definition or something
raise NoEmptyItems("%r did not define any attributes" % (tableClass,))

sqlstr.append(', '.join(sqlarg))
sqlstr.append(')')
self.createSQL(''.join(sqlstr))


def _maybeCreateTable(self, tableClass, key):
"""
A type ID has been requested for an Item subclass whose table was not
present when this Store was opened. Attempt to create the table, and
if that fails because another Store object (perhaps in another process)
has created the table, re-read the schema. When that's done, return
the typeID.

This method is internal to the implementation of getTypeID. It must be
run in a transaction.

@param tableClass: an Item subclass
@param key: a 2-tuple of the tableClass's typeName and schemaVersion

@return: a typeID for the table; a new one if no table exists, or the
existing one if the table was created by another Store object
referencing this database.
"""
try:
self.createSQL(''.join(sqlstr))
self._justCreateTable(tableClass)
except errors.TableAlreadyExists:
# Although we don't have a memory of this table from the last time
# we called "_startup()", another process has updated the schema
Expand Down Expand Up @@ -2201,14 +2212,6 @@ def _createIndexesFor(self, tableClass, extantIndexes):
self.createSQL(csql)


def getTableQuery(self, typename, version):
if (typename, version) not in self.tableQueries:
query = 'SELECT * FROM %s WHERE oid = ?' % (
self._tableNameFor(typename, version), )
self.tableQueries[typename, version] = query
return self.tableQueries[typename, version]


def getItemByID(self, storeID, default=_noItem, autoUpgrade=True):
"""
Retrieve an item by its storeID, and return it.
Expand All @@ -2234,7 +2237,7 @@ def getItemByID(self, storeID, default=_noItem, autoUpgrade=True):
upgraded a database to a new schema and then attempt to open it with a
previous version of the code.)

@raise KeyError: if no item corresponded to the given storeID.
@raise errors.ItemNotFound: if no item existed with the given storeID.

@return: an Item, or the given default, if it was passed and no row
corresponding to the given storeID can be located in the database.
Expand All @@ -2255,14 +2258,6 @@ def getItemByID(self, storeID, default=_noItem, autoUpgrade=True):
"Database panic: more than one result for TYPEOF!"
if results:
typename, module, version = results[0]
# for the moment we're going to assume no inheritance
attrs = self.querySQL(self.getTableQuery(typename, version),
[storeID])
if len(attrs) != 1:
if default is _noItem:
raise errors.ItemNotFound("No results for known-to-be-good object")
return default
attrs = attrs[0]
useMostRecent = False
moreRecentAvailable = False

Expand Down Expand Up @@ -2298,6 +2293,18 @@ def getItemByID(self, storeID, default=_noItem, autoUpgrade=True):
T = mostRecent
else:
T = self.getOldVersionOf(typename, version)

# for the moment we're going to assume no inheritance
attrs = self.querySQL(T._baseSelectSQL(self), [storeID])
if len(attrs) == 0:
if default is _noItem:
raise errors.ItemNotFound(
'No results for known-to-be-good object')
return default
elif len(attrs) > 1:
raise errors.DataIntegrityError(
'Too many results for {:d}'.format(storeID))
attrs = attrs[0]
x = T.existingInStore(self, storeID, attrs)
if moreRecentAvailable and (not useMostRecent) and autoUpgrade:
# upgradeVersion will do caching as necessary, we don't have to
Expand All @@ -2309,7 +2316,7 @@ def getItemByID(self, storeID, default=_noItem, autoUpgrade=True):
self.objectCache.cache(storeID, x)
return x
if default is _noItem:
raise KeyError(storeID)
raise errors.ItemNotFound(storeID)
mithrandi marked this conversation as resolved.
Show resolved Hide resolved
return default


Expand Down
Binary file added axiom/test/historic/storeID.axiom.tbz2
Binary file not shown.
39 changes: 39 additions & 0 deletions axiom/test/historic/stub_storeID.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# -*- test-case-name: axiom.test.historic.test_storeID -*-

from axiom.item import Item
from axiom.attributes import text
from axiom.test.historic.stubloader import saveStub


class Dummy(Item):
typeName = 'axiom_storeid_dummy'
schemaVersion = 1

attribute = text(doc='text', allowNone=False)



class Dummy2(Item):
typeName = 'axiom_storeid_dummy2'
schemaVersion = 1

attribute = text(doc='text', allowNone=False)



def createDatabase(s):
"""
Populate the given Store with some Dummy items.
"""
Dummy(store=s, attribute=u'one')
Dummy(store=s, attribute=u'two')
i = Dummy(store=s, attribute=u'three')
Dummy2(store=s, attribute=u'four')
# Work around https://github.com/twisted/axiom/issues/86
i.checkpoint()
i.deleteFromStore()



if __name__ == '__main__':
saveStub(createDatabase, 0x1240846306fcda3289550cdf9515b2c7111d2bac)
Loading