class Store(Empowered):
    """
    I am a database that Axiom Items can be stored in.

    Store an item in me by setting its 'store' attribute to be me.

    I can be created one of two ways::

        Store()                      # Create an in-memory database

        Store("/path/to/file.axiom") # create an on-disk database in the
                                     # directory /path/to/file.axiom

    @ivar typeToTableNameCache: a dictionary mapping Item subclass type objects
    to the fully-qualified sqlite table name where items of that type are
    stored.  This cache is generated from the saved schema metadata when this
    store is opened and updated when schema changes from other store objects
    (such as in other processes) are detected.

    @cvar __legacy__: an L{Item} may refer to a L{Store} via a L{reference},
    and this attribute tells the item reference system that the store itself is
    not an old version of an item; i.e. it does not need to have its upgraders
    invoked.

    @cvar storeID: an L{Item} may refer to a L{Store} via a L{reference}, and
    this attribute tells the item reference system that the L{Store} has a
    special ID that to use (which is never allocated to any item).
    """

    aggregateInterfaces = {
        IService: storeServiceSpecialCase,
        IServiceCollection: storeServiceSpecialCase,
        iaxiom.IBatchService: _storeBatchServiceSpecialCase,
        iaxiom.IScheduler: _schedulerServiceSpecialCase}

    transaction = None          # set of objects changed in the current transaction
    touched = None              # set of objects changed since the last checkpoint

    databaseName = 'main'       # can differ if database is attached to another
                                # database.

    dbdir = None # FilePath to the Axiom database directory, or None for
                 # in-memory Stores.
    filesdir = None # FilePath to the filesystem-storage subdirectory of the
                    # database directory, or None for in-memory Stores.

    store = property(lambda self: self) # I have a 'store' attribute because I
                                        # am 'stored' within myself; this is
                                        # also for references to use.


    # Counter indicating things are going on which disallows changes to the
    # database.  Callbacks dispatched to application code while this is
    # non-zero will reject database changes with a ChangeRejected exception.
    _rejectChanges = 0

    # The following method and attributes are the ad-hoc interface required as
    # targets of attributes.reference attributes.  (In other words, the store
    # is a little bit like a fake item.)  These should probably eventually be
    # on an interface somewhere, and be better named.

    def _currentlyValidAsReferentFor(self, store):
        """
        Check to see if this store is currently valid as a target of a
        reference from an item in the given L{Store}.  This is true iff the
        given L{Store} is this L{Store}.

        @param store: the store that the referring item is present in.

        @type store: L{Store}
        """
        if store is self:
            return True
        else:
            return False

    __legacy__ = False

    storeID = STORE_SELF_ID


    def __init__(self, dbdir=None, filesdir=None, debug=False, parent=None,
                 idInParent=None, journalMode=None):
        """
        Create a store.

        @param dbdir: A L{FilePath} to (or name of) an existing Axiom directory, or
        directory that does not exist yet which will be created as this Store
        is instantiated.  If unspecified, this database will be kept in memory.

        @param filesdir: A L{FilePath} to (or name of) a directory to keep files in for in-memory
        stores. An exception will be raised if both this attribute and C{dbdir}
        are specified.

        @param debug: set to True if this Store should print out every SQL
        statement it sends to SQLite.

        @param parent: (internal) If this is opened using an
        L{axiom.substore.Substore}, a reference to its parent.

        @param idInParent: (internal) If this is opened using an
        L{axiom.substore.Substore}, the storeID of the item within its parent
        which opened it.

        @raises: C{ValueError} if both C{dbdir} and C{filesdir} are specified.
        """
        if parent is not None or idInParent is not None:
            assert parent is not None
            assert idInParent is not None
        self.parent = parent
        self.idInParent = idInParent
        self.debug = debug
        self.autocommit = True
        self.journalMode = journalMode
        self.queryTimes = []
        self.execTimes = []

        self._inMemoryPowerups = {}

        self._attachedChildren = {} # database name => child store object

        self.statementCache = {} # non-normalized => normalized qmark SQL
                                 # statements

        self.activeTables = {}  # tables which have had items added/removed
                                # this run

        self.objectCache = _fincache.FinalizingCache()

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

        self.typeToInsertSQLCache = {}
        self.typeToSelectSQLCache = {}
        self.typeToDeleteSQLCache = {}

        self.typeToTableNameCache = {}
        self.attrNameToColumnNameCache = {}  # map fully-qualified attribute
                                             # names (native str?) to database
                                             # column names

        self._upgradeManager = upgrade._StoreUpgrade(self)

        self._axiom_service = None


        if self.parent is None:
            self._upgradeService = SchedulingService()
        else:
            # Substores should hook into their parent, since they shouldn't
            # expect to have their own substore service started.
            self._upgradeService = self.parent._upgradeService


        # OK!  Everything that can be set up without touching the filesystem
        # has been done.  Let's get ready to open the actual database...

        _initialOpenFailure = None
        if dbdir is None:
            self._initdb(IN_MEMORY_DATABASE)
            self._initSchema()
            self._memorySubstores = []
            if filesdir is not None:
                if not isinstance(filesdir, filepath.FilePath):
                    filesdir = filepath.FilePath(filesdir)
                self.filesdir = filesdir
                if not self.filesdir.isdir():
                    self.filesdir.makedirs()
                    self.filesdir.child("temp").createDirectory()
        else:
            if filesdir is not None:
                raise ValueError("Only one of dbdir and filesdir"
                                 " may be specified")
            if not isinstance(dbdir, filepath.FilePath):
                dbdir = filepath.FilePath(dbdir)
                # required subdirs: files, temp, run
                # datafile: db.sqlite
            self.dbdir = dbdir
            self.filesdir = self.dbdir.child('files')

            if not dbdir.isdir():
                tempdbdir = dbdir.temporarySibling()
                tempdbdir.makedirs() # maaaaaaaybe this is a bad idea, we
                                     # probably shouldn't be doing this
                                     # automatically.
                for child in ('files', 'temp', 'run'):
                    tempdbdir.child(child).createDirectory()
                self._initdb(tempdbdir.child('db.sqlite').path)
                self._initSchema()
                self.close(_report=False)
                try:
                    tempdbdir.moveTo(dbdir)
                except:
                    _initialOpenFailure = Failure()

            try:
                self._initdb(dbdir.child('db.sqlite').path)
            except:
                if _initialOpenFailure is not None:
                    log.msg("Failed to initialize axiom database."
                            "  Possible cause of error: ")
                    log.err(_initialOpenFailure)
                raise

        self.transact(self._startup)

        # _startup may have found some things which we must now upgrade.
        if self._upgradeManager.upgradesPending:
            # Automatically upgrade when possible.
            self._upgradeComplete = self._upgradeService.coop.cooperate(
                self._upgradeManager.upgradeEverything())
            d = self._upgradeComplete.whenDone()
            def logUpgradeFailure(aFailure):
                if aFailure.check(errors.ItemUpgradeError):
                    log.err(aFailure.value.originalFailure, 'Item upgrade error')
                log.err(aFailure, "upgrading %r failed" % (self,))
            d.addErrback(logUpgradeFailure)
        else:
            self._upgradeComplete = None

        log.msg(
            interface=iaxiom.IStatEvent,
            store_opened=self.dbdir is not None and self.dbdir.path or '')

    _childCounter = 0

    def _attachChild(self, child):
        "attach a child database, returning an identifier for it"
        self._childCounter += 1
        databaseName = 'child_db_%d' % (self._childCounter,)
        self._attachedChildren[databaseName] = child
        # ATTACH DATABASE statements can't use bind paramaters, blech.
        self.executeSQL("ATTACH DATABASE '%s' AS %s" % (
                child.dbdir.child('db.sqlite').path,
                databaseName,))
        return databaseName

    attachedToParent = False

    def attachToParent(self):
        assert self.parent is not None, 'must have a parent to attach'
        assert self.transaction is None, "can't attach within a transaction"

        self.close()

        self.attachedToParent = True
        self.databaseName = self.parent._attachChild(self)
        self.connection = self.parent.connection
        self.cursor = self.parent.cursor

#     def detachFromParent(self):
#         pass


    def _initSchema(self):
        # No point in even attempting to transactionalize this:
        # every single statement is a CREATE TABLE or a CREATE
        # INDEX and those commit transactions silently anyway.
        for stmt in _schema.BASE_SCHEMA:
            self.executeSchemaSQL(stmt)


    def _startup(self):
        """
        Called during __init__.  Check consistency of schema in database with
        classes in memory.  Load all Python modules for stored items, and load
        version information for upgrader service to run later.
        """
        typesToCheck = []

        for oid, module, typename, version in self.querySchemaSQL(_schema.ALL_TYPES):
            if self.debug:
                print()
                print('SCHEMA:', oid, module, typename, version)
            if typename not in _typeNameToMostRecentClass:
                try:
                    namedAny(module)
                except ValueError as err:
                    raise ImportError('cannot find module ' + module, str(err))
            self.typenameAndVersionToID[typename, version] = oid

        # Can't call this until typenameAndVersionToID is populated, since this
        # depends on building a reverse map of that.
        persistedSchema = self._loadTypeSchema()

        # Now that we have persistedSchema, loop over everything again and
        # prepare old types.
        for (typename, version), typeID in six.iteritems(self.typenameAndVersionToID):
            cls = _typeNameToMostRecentClass.get(typename)

            if cls is not None:
                if version != cls.schemaVersion:
                    typesToCheck.append(
                        self._prepareOldVersionOf(
                            typename, version, persistedSchema))
                else:
                    typesToCheck.append(cls)

        for cls in typesToCheck:
            self._checkTypeSchemaConsistency(cls, persistedSchema)

        # Schema is consistent!  Now, if I forgot to create any indexes last
        # time I saw this table, do it now...
        extantIndexes = self._loadExistingIndexes()
        for cls in typesToCheck:
            self._createIndexesFor(cls, extantIndexes)

        self._upgradeManager.checkUpgradePaths()


    def _loadExistingIndexes(self):
        """
        Return a C{set} of the SQL indexes which already exist in the
        underlying database.  It is important to load all of this information
        at once (as opposed to using many CREATE INDEX IF NOT EXISTS statements
        or many CREATE INDEX statements and handling the errors) to minimize
        the cost of opening a store.  Loading all the indexes at once is much
        faster than doing pretty much anything that involves doing something
        once per required index.
        """
        # Totally SQLite-specific: look up what indexes exist already in
        # sqlite_master so we can skip trying to create them (which can be
        # really slow).
        return set(
            name
            for (name,) in self.querySchemaSQL(
                "SELECT name FROM *DATABASE*.sqlite_master "
                "WHERE type = 'index'"))


    def _initdb(self, dbfname):
        self.connection = Connection.fromDatabaseName(dbfname)
        self.cursor = self.connection.cursor()
        if self.journalMode is not None:
            self.querySchemaSQL(
                'PRAGMA *DATABASE*.journal_mode = {}'.format(self.journalMode))


    def __repr__(self):
        dbdir = self.dbdir
        if self.dbdir is None:
            dbdir = '(in memory)'

        return "<Store {dbdir}@{id:#x}".format(dbdir=dbdir, id=id(self))


    def findOrCreate(self, userItemClass, __ifnew=None, **attrs):
        """
        Usage::

            s.findOrCreate(userItemClass [, function] [, x=1, y=2, ...])

        Example::

            class YourItemType(Item):
                a = integer()
                b = text()
                c = integer()

            def f(x):
                print x, \"-- it's new!\"
            s.findOrCreate(YourItemType, f, a=1, b=u'2')

        Search for an item with columns in the database that match the passed
        set of keyword arguments, returning the first match if one is found,
        creating one with the given attributes if not.  Takes an optional
        positional argument function to call on the new item if it is new.
        """
        andargs = []
        for k, v in six.iteritems(attrs):
            col = getattr(userItemClass, k)
            andargs.append(col == v)

        if len(andargs) == 0:
            cond = []
        elif len(andargs) == 1:
            cond = [andargs[0]]
        else:
            cond = [attributes.AND(*andargs)]

        for result in self.query(userItemClass, *cond):
            return result
        newItem = userItemClass(store=self, **attrs)
        if __ifnew is not None:
            __ifnew(newItem)
        return newItem

    def newFilePath(self, *path):
        p = self.filesdir
        for subdir in path:
            p = p.child(subdir)
        return p

    def newTemporaryFilePath(self, *path):
        p = self.dbdir.child('temp')
        for subdir in path:
            p = p.child(subdir)
        return p

    def newFile(self, *path):
        """
        Open a new file somewhere in this Store's file area.

        @param path: a sequence of path segments.

        @return: an L{AtomicFile}.
        """
        assert len(path) > 0, "newFile requires a nonzero number of segments"
        if self.dbdir is None:
            if self.filesdir is None:
                raise RuntimeError("This in-memory store has no file directory")
            else:
                tmpbase = self.filesdir
        else:
            tmpbase = self.dbdir
        tmpname = tmpbase.child('temp').child(str(next(tempCounter)) + ".tmp")
        return AtomicFile(tmpname.path, self.newFilePath(*path))

    def newDirectory(self, *path):
        p = self.filesdir
        for subdir in path:
            p = p.child(subdir)
        return p


    def _loadTypeSchema(self):
        """
        Load all of the stored schema information for all types known by this
        store.  It's important to load everything all at once (rather than
        loading the schema for each type separately as it is needed) to keep
        store opening fast.  A single query with many results is much faster
        than many queries with a few results each.

        @return: A dict with two-tuples of item type name and schema version as
            keys and lists of five-tuples of attribute schema information for
            that type.  The elements of the five-tuple are::

              - a string giving the name of the Python attribute
              - a string giving the SQL type
              - a boolean indicating whether the attribute is indexed
              - the Python attribute type object (eg, axiom.attributes.integer)
              - a string giving documentation for the attribute
        """

        # Oops, need an index going the other way.  This only happens once per
        # store open, and it's based on data queried from the store, so there
        # doesn't seem to be any broader way to cache and re-use the result.
        # However, if we keyed the resulting dict on the database typeID rather
        # than (typeName, schemaVersion), we wouldn't need the information this
        # dict gives us.  That would mean changing the callers of this function
        # to use typeID instead of that tuple, which may be possible.  Probably
        # only represents a very tiny possible speedup.
        typeIDToNameAndVersion = {}
        for key, value in six.iteritems(self.typenameAndVersionToID):
            typeIDToNameAndVersion[value] = key

        # Indexing attribute, ordering by it, and getting rid of row_offset
        # from the schema and the sorted() here doesn't seem to be any faster
        # than doing this.
        persistedSchema = sorted(self.querySchemaSQL(_schema.PERSISTED_SCHEMA))

        # This is trivially (but measurably!) faster than getattr(attributes,
        # pythontype).
        getAttribute = attributes.__dict__.__getitem__

        result = {}
        for (attribute, typeID, sqltype, indexed, pythontype,
             docstring) in persistedSchema:
            key = typeIDToNameAndVersion[typeID]
            if key not in result:
                result[key] = []
            result[key].append((
                    attribute, sqltype, indexed,
                    getAttribute(pythontype), docstring))
        return result


    def _checkTypeSchemaConsistency(self, actualType, onDiskSchema):
        """
        Called for all known types at database startup: make sure that what we
        know (in memory) about this type agrees with what is stored about this
        type in the database.

        @param actualType: A L{MetaItem} instance which is associated with a
            table in this store.  The schema it defines in memory will be
            checked against the schema known in the database to ensure they
            agree.

        @param onDiskSchema: A mapping from L{MetaItem} instances (such as
            C{actualType}) to the schema known in the database and associated
            with C{actualType}.

        @raise RuntimeError: if the schema defined by C{actualType} does not
            match the database-present schema given in C{onDiskSchema} or if
            C{onDiskSchema} contains a newer version of the schema associated
            with C{actualType} than C{actualType} represents.
        """
        # make sure that both the runtime and the database both know about this
        # type; if they don't both know, we can't check that their views are
        # consistent
        try:
            inMemorySchema = _inMemorySchemaCache[actualType]
        except KeyError:
            inMemorySchema = _inMemorySchemaCache[actualType] = [
                (storedAttribute.attrname, storedAttribute.sqltype)
                for (name, storedAttribute) in actualType.getSchema()]

        key = (actualType.typeName, actualType.schemaVersion)
        persistedSchema = [(storedAttribute[0], storedAttribute[1])
                           for storedAttribute in onDiskSchema[key]]
        if inMemorySchema != persistedSchema:
            raise RuntimeError(
                "Schema mismatch on already-loaded %r <%r> object version %d:\n%s" %
                (actualType, actualType.typeName, actualType.schemaVersion,
                 _diffSchema(persistedSchema, inMemorySchema)))

        if actualType.__legacy__:
            return

        if (key[0], key[1] + 1) in onDiskSchema:
            raise RuntimeError(
                "Memory version of %r is %d; database has newer" % (
                    actualType.typeName, key[1]))


    # finally find old versions of the data and prepare to upgrade it.
    def _prepareOldVersionOf(self, typename, version, persistedSchema):
        """
        Note that this database contains old versions of a particular type.
        Create the appropriate dummy item subclass and queue the type to be
        upgraded.

        @param typename: The I{typeName} associated with the schema for which
            to create a dummy item class.

        @param version: The I{schemaVersion} of the old version of the schema
            for which to create a dummy item class.

        @param persistedSchema: A mapping giving information about all schemas
            stored in the database, used to create the attributes of the dummy
            item class.
        """
        appropriateSchema = persistedSchema[typename, version]
        # create actual attribute objects
        dummyAttributes = {}
        for (attribute, sqlType, indexed, pythontype,
             docstring) in appropriateSchema:
            atr = pythontype(indexed=indexed, doc=docstring)
            dummyAttributes[attribute] = atr
        dummyBases = []
        oldType = declareLegacyItem(
            typename, version, dummyAttributes, dummyBases)
        self._upgradeManager.queueTypeUpgrade(oldType)
        return oldType


    def whenFullyUpgraded(self):
        """
        Return a Deferred which fires when this Store has been fully upgraded.
        """
        if self._upgradeComplete is not None:
            return self._upgradeComplete.whenDone()
        else:
            return defer.succeed(None)

    def getOldVersionOf(self, typename, version):
        return _legacyTypes[typename, version]



        # grab the schema for that version
        # look up upgraders which push it forward

    def findUnique(self, tableClass, comparison=None, default=_noItem):
        """
        Find an Item in the database which should be unique.  If it is found,
        return it.  If it is not found, return 'default' if it was passed,
        otherwise raise L{errors.ItemNotFound}.  If more than one item is
        found, raise L{errors.DuplicateUniqueItem}.

        @param comparison: implementor of L{iaxiom.IComparison}.

        @param default: value to use if the item is not found.
        """
        results = list(self.query(tableClass, comparison, limit=2))
        lr = len(results)

        if lr == 0:
            if default is _noItem:
                raise errors.ItemNotFound(comparison)
            else:
                return default
        elif lr == 2:
            raise errors.DuplicateUniqueItem(comparison, results)
        elif lr == 1:
            return results[0]
        else:
            raise AssertionError("limit=2 database query returned 3+ results: ",
                                 comparison, results)


    def findFirst(self, tableClass, comparison=None,
                  offset=None, sort=None, default=None):
        """
        Usage::

            s.findFirst(tableClass [, query arguments except 'limit'])

        Example::

            class YourItemType(Item):
                a = integer()
                b = text()
                c = integer()
            ...
            it = s.findFirst(YourItemType,
                             AND(YourItemType.a == 1,
                                 YourItemType.b == u'2'),
                                 sort=YourItemType.c.descending)

        Search for an item with columns in the database that match the passed
        comparison, offset and sort, returning the first match if one is found,
        or the passed default (None if none is passed) if one is not found.
        """

        limit = 1
        for item in self.query(tableClass, comparison, limit, offset, sort):
            return item
        return default

    def query(self, tableClass, comparison=None,
              limit=None, offset=None, sort=None):
        """
        Return a generator of instances of C{tableClass},
        or tuples of instances if C{tableClass} is a
        tuple of classes.

        Examples::

            fastCars = s.query(Vehicle,
                axiom.attributes.AND(
                    Vehicle.wheels == 4,
                    Vehicle.maxKPH > 200),
                limit=100,
                sort=Vehicle.maxKPH.descending)

            quotesByClient = s.query( (Client, Quote),
                axiom.attributes.AND(
                    Client.active == True,
                    Quote.client == Client.storeID,
                    Quote.created >= someDate),
                limit=10,
                sort=(Client.name.ascending,
                      Quote.created.descending))

        @param tableClass: a subclass of Item to look for instances of,
        or a tuple of subclasses.

        @param comparison: a provider of L{IComparison}, or None, to match
        all items available in the store. If tableClass is a tuple, then
        the comparison must refer to all Item subclasses in that tuple,
        and specify the relationships between them.

        @param limit: an int to limit the total length of the results, or None
        for all available results.

        @param offset: an int to specify a starting point within the available
        results, or None to start at 0.

        @param sort: an L{ISort}, something that comes from an SQLAttribute's
        'ascending' or 'descending' attribute.

        @return: an L{ItemQuery} object, which is an iterable of Items or
        tuples of Items, according to tableClass.
        """
        if isinstance(tableClass, tuple):
            queryClass = MultipleItemQuery
        else:
            queryClass = ItemQuery

        return queryClass(self, tableClass, comparison, limit, offset, sort)

    def sum(self, summableAttribute, *a, **k):
        args = (self, summableAttribute.type) + a
        return AttributeQuery(attribute=summableAttribute,
                              *args, **k).sum()
    def count(self, *a, **k):
        return self.query(*a, **k).count()

    def batchInsert(self, itemType, itemAttributes, dataRows):
        """
        Create multiple items in the store without loading
        corresponding Python objects into memory.

        the items' C{stored} callback will not be called.

        Example::

            myData = [(37, u"Fred",  u"Wichita"),
                      (28, u"Jim",   u"Fresno"),
                      (43, u"Betty", u"Dubuque")]
            myStore.batchInsert(FooItem,
                                [FooItem.age, FooItem.name, FooItem.city],
                                myData)

        @param itemType: an Item subclass to create instances of.

        @param itemAttributes: an iterable of attributes on the Item subclass.

        @param dataRows: an iterable of iterables, each the same
        length as C{itemAttributes} and containing data corresponding
        to each attribute in it.

        @return: None.
        """
        class FakeItem:
            pass
        _NEEDS_DEFAULT = object() # token for lookup failure
        fakeOSelf = FakeItem()
        fakeOSelf.store = self
        sql = itemType._baseInsertSQL(self)
        indices = {}
        schema = [attr for (name, attr) in itemType.getSchema()]
        for i, attr in enumerate(itemAttributes):
            indices[attr.attrname] = i
        for row in dataRows:
            oid = self.store.executeSchemaSQL(
                _schema.CREATE_OBJECT, [self.store.getTypeID(itemType)])
            insertArgs = [oid]
            for attr in schema:
                i = indices.get(attr.attrname, _NEEDS_DEFAULT)
                if i is _NEEDS_DEFAULT:
                    pyval = attr.default
                else:
                    pyval = row[i]
                dbval = attr._convertPyval(fakeOSelf, pyval)
                insertArgs.append(dbval)
            self.executeSQL(sql, insertArgs)

    def _loadedItem(self, itemClass, storeID, attrs):
        try:
            result = self.objectCache.get(storeID)
            # XXX do checks on consistency between attrs and DB object, maybe?
        except KeyError:
            result = itemClass.existingInStore(self, storeID, attrs)
            if not result.__legacy__:
                self.objectCache.cache(storeID, result)
        return result


    def changed(self, item):
        """
        An item in this store was changed.  Add it to the current transaction's
        list of changed items, if a transaction is currently underway, or raise
        an exception if this L{Store} is currently in a state which does not
        allow changes.
        """
        if self._rejectChanges:
            raise errors.ChangeRejected()
        if self.transaction is not None:
            self.transaction.add(item)
            self.touched.add(item)


    def checkpoint(self):
        self._rejectChanges += 1
        try:
            for item in self.touched:
                # XXX: it should be possible here, using various clever hacks, to
                # automatically optimize functionally identical statements into
                # executemany.
                item.checkpoint()
            self.touched.clear()
        finally:
            self._rejectChanges -= 1

    executedThisTransaction = None
    tablesCreatedThisTransaction = None

    def transact(self, f, *a, **k):
        """
        Execute C{f(*a, **k)} in the context of a database transaction.

        Any changes made to this L{Store} by C{f} will be committed when C{f}
        returns.  If C{f} raises an exception, those changes will be reverted
        instead.

        If a transaction is already in progress (in this thread - ie, if a
        frame executing L{Store.transact} is already on the call stack), this
        will B{not} start a nested transaction.  Changes will not be committed
        until the existing transaction completes, and an exception raised by
        C{f} will not revert changes made by C{f}.  You probably don't want to
        ever call this if another transaction is in progress.

        @return: Whatever C{f(*a, **kw)} returns.
        @raise: Whatever C{f(*a, **kw)} raises, or a database exception.
        """
        if self.transaction is not None:
            return f(*a, **k)
        if self.attachedToParent:
            return self.parent.transact(f, *a, **k)
        try:
            self._begin()
            try:
                result = f(*a, **k)
                self.checkpoint()
            except:
                exc = Failure()
                try:
                    self.revert()
                except:
                    log.err(exc)
                    raise
                raise
            else:
                self._commit()
            return result
        finally:
            self._cleanupTxnState()

    # The following three methods are necessary...
    # - in PySQLite: because PySQLite has some buggy transaction handling which
    #   makes it impossible to issue explicit BEGIN statements - which we
    #   _need_ to do to provide guarantees for read/write transactions.

    def _begin(self):
        if self.debug:
            print('<'*10, 'BEGIN', '>'*10)
        self.cursor.execute("BEGIN IMMEDIATE TRANSACTION")
        self._setupTxnState()

    def _setupTxnState(self):
        self.executedThisTransaction = []
        self.tablesCreatedThisTransaction = []
        if self.attachedToParent:
            self.transaction = self.parent.transaction
            self.touched = self.parent.touched
        else:
            self.transaction = set()
            self.touched = set()
        self.autocommit = False
        for sub in self._attachedChildren.values():
            sub._setupTxnState()

    def _commit(self):
        if self.debug:
            print('*'*10, 'COMMIT', '*'*10)
        # self.connection.commit()
        self.cursor.execute("COMMIT")
        log.msg(interface=iaxiom.IStatEvent, stat_commits=1)
        self._postCommitHook()


    def _postCommitHook(self):
        self._rejectChanges += 1
        try:
            for committed in self.transaction:
                committed.committed()
        finally:
            self._rejectChanges -= 1


    def _rollback(self):
        if self.debug:
            print('>'*10, 'ROLLBACK', '<'*10)
        # self.connection.rollback()
        self.cursor.execute("ROLLBACK")
        log.msg(interface=iaxiom.IStatEvent, stat_rollbacks=1)


    def revert(self):
        self._rollback()
        self._inMemoryRollback()


    def _inMemoryRollback(self):
        self._rejectChanges += 1
        try:
            for item in self.transaction:
                item.revert()
        finally:
            self._rejectChanges -= 1
        self.transaction.clear()
        for tableClass in self.tablesCreatedThisTransaction:
            del self.typenameAndVersionToID[tableClass.typeName,
                                            tableClass.schemaVersion]
            # Clear all cache related to this table
            for cache in (self.typeToInsertSQLCache,
                          self.typeToDeleteSQLCache,
                          self.typeToSelectSQLCache,
                          self.typeToTableNameCache) :
                if tableClass in cache:
                    del cache[tableClass]
            qualifiedStoreIDName = tableClass.storeID.fullyQualifiedName()
            if qualifiedStoreIDName in self.attrNameToColumnNameCache:
                del self.attrNameToColumnNameCache[qualifiedStoreIDName]
            for name, attr in tableClass.getSchema():
                attrFQN = attr.fullyQualifiedName()
                if attrFQN in self.attrNameToColumnNameCache:
                    del self.attrNameToColumnNameCache[attrFQN]

        for sub in self._attachedChildren.values():
            sub._inMemoryRollback()


    def _cleanupTxnState(self):
        self.autocommit = True
        self.transaction = None
        self.touched = None
        self.executedThisTransaction = None
        self.tablesCreatedThisTransaction = []
        for sub in list(self._attachedChildren.values()):
            sub._cleanupTxnState()

    def close(self, _report=True):
        self.cursor.close()
        self.connection.close()
        self.cursor = self.connection = None
        if self.debug and _report:
            if not self.queryTimes:
                print('no queries')
            else:
                print('query:', self.avgms(self.queryTimes))
            if not self.execTimes:
                print('no execs')
            else:
                print('exec:', self.avgms(self.execTimes))

    def avgms(self, l):
        return 'count: %d avg: %dus' % (len(l),
                                        int( (sum(l)/len(l)) * 1000000.),)

    def _indexNameOf(self, tableClass, attrname):
        """
        Return the unqualified (ie, no database name) name of the given
        attribute of the given table.

        @type tableClass: L{MetaItem}
        @param tableClass: The Python class associated with a table in the
            database.

        @param attrname: A sequence of the names of the columns of the
            indicated table which will be included in the named index.

        @return: A C{str} giving the name of the index which will index the
            given attributes of the given table.
        """
        return "axiomidx_%s_v%d_%s" % (tableClass.typeName,
                                       tableClass.schemaVersion,
                                       '_'.join(attrname))


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


    def _tableNameFor(self, typename, version):
        return '.'.join([self.databaseName,
                         self._tableNameOnlyFor(typename, version)])


    def getTableName(self, tableClass):
        """
        Retrieve the fully qualified name of the table holding items
        of a particular class in this store.  If the table does not
        exist in the database, it will be created as a side-effect.

        @param tableClass: an Item subclass

        @raises axiom.errors.ItemClassesOnly: if an object other than a
            subclass of Item is passed.

        @return: a string
        """
        if not (isinstance(tableClass, type) and issubclass(tableClass, item.Item)):
            raise errors.ItemClassesOnly("Only subclasses of Item have table names.")

        if tableClass not in self.typeToTableNameCache:
            self.typeToTableNameCache[tableClass] = self._tableNameFor(tableClass.typeName, tableClass.schemaVersion)
            # make sure the table exists
            self.getTypeID(tableClass)
        return self.typeToTableNameCache[tableClass]


    def getShortColumnName(self, attribute):
        """
        Retreive the column name for a particular attribute in this
        store.  The attribute must be bound to an Item subclass (its
        type must be valid). If the underlying table does not exist in
        the database, it will be created as a side-effect.

        @param tableClass: an Item subclass

        @return: a string

        XXX: The current implementation does not really match the
        description, which is actually more restrictive. But it will
        be true soon, so I guess it is ok for now.  The reason is
        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 + ']'


    def getColumnName(self, attribute):
        """
        Retreive the fully qualified column name for a particular
        attribute in this store.  The attribute must be bound to an
        Item subclass (its type must be valid). If the underlying
        table does not exist in the database, it will be created as a
        side-effect.

        @param tableClass: an Item subclass

        @return: a string
        """
        name = attribute.fullyQualifiedName()
        if name not in self.attrNameToColumnNameCache:
            self.attrNameToColumnNameCache[name] = '.'.join(
                (self.getTableName(attribute.type),
                 self.getShortColumnName(attribute)))
        return self.attrNameToColumnNameCache[name]


    def getTypeID(self, tableClass):
        """
        Retrieve the typeID associated with a particular table in the
        in-database schema for this Store.  A typeID is an opaque integer
        representing the Item subclass, and the associated table in this
        Store's SQLite database.

        @param tableClass: a subclass of Item

        @return: an integer
        """
        key = (tableClass.typeName,
               tableClass.schemaVersion)
        if key in self.typenameAndVersionToID:
            return self.typenameAndVersionToID[key]
        return self.transact(self._maybeCreateTable, tableClass, key)


    def _justCreateTable(self, tableClass):
        """
        Execute the table creation DDL for an Item subclass.

        Indexes are *not* created.

        @type tableClass: type
        @param tableClass: an Item subclass
        """
        sqlstr = []
        sqlarg = []

        # needs to be calculated including version
        tableName = self._tableNameFor(tableClass.typeName,
                                       tableClass.schemaVersion)

        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")
        for nam, atr in tableClass.getSchema():
            sqlarg.append("\n%s %s" %
                          (atr.getShortColumnName(self), atr.sqltype))

        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._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
            # since then.
            self._startup()
            return self.typenameAndVersionToID[key]


        typeID = self.executeSchemaSQL(_schema.CREATE_TYPE,
                                       [tableClass.typeName,
                                        tableClass.__module__,
                                        tableClass.schemaVersion])

        self.typenameAndVersionToID[key] = typeID

        if self.tablesCreatedThisTransaction is not None:
            self.tablesCreatedThisTransaction.append(tableClass)

        # If the new type is a legacy type (not the current version), we need
        # to queue it for upgrade to ensure that if we are in the middle of an
        # upgrade, legacy items of this version get upgraded.
        cls = _typeNameToMostRecentClass.get(tableClass.typeName)
        if cls is not None and tableClass.schemaVersion != cls.schemaVersion:
            self._upgradeManager.queueTypeUpgrade(tableClass)

        # We can pass () for extantIndexes here because since the table didn't
        # exist for tableClass, none of its indexes could have either.
        # Whatever checks _createIndexesFor will make would give the same
        # result against the actual set of existing indexes as they will
        # against ().
        self._createIndexesFor(tableClass, ())

        for n, (name, storedAttribute) in enumerate(tableClass.getSchema()):
            self.executeSchemaSQL(
                _schema.ADD_SCHEMA_ATTRIBUTE,
                [typeID, n, storedAttribute.indexed, storedAttribute.sqltype,
                 storedAttribute.allowNone, storedAttribute.attrname,
                 storedAttribute.doc, storedAttribute.__class__.__name__])
            # XXX probably need something better for pythontype eventually,
            # when we figure out a good way to do user-defined attributes or we
            # start parameterizing references.

        return typeID


    def _createIndexesFor(self, tableClass, extantIndexes):
        """
        Create any indexes which don't exist and are required by the schema
        defined by C{tableClass}.

        @param tableClass: A L{MetaItem} instance which may define a schema
            which includes indexes.

        @param extantIndexes: A container (anything which can be the right-hand
            argument to the C{in} operator) which contains the unqualified
            names of all indexes which already exist in the underlying database
            and do not need to be created.
        """
        try:
            indexes = _requiredTableIndexes[tableClass]
        except KeyError:
            indexes = set()
            for nam, atr in tableClass.getSchema():
                if atr.indexed:
                    indexes.add(((atr.getShortColumnName(self),), (atr.attrname,)))
                for compound in atr.compoundIndexes:
                    indexes.add((tuple(inatr.getShortColumnName(self) for inatr in compound),
                                 tuple(inatr.attrname for inatr in compound)))
            _requiredTableIndexes[tableClass] = indexes

        # _ZOMFG_ SQL is such a piece of _shit_: you can't fully qualify the
        # table name in CREATE INDEX statements because the _INDEX_ is fully
        # qualified!

        indexColumnPrefix = '.'.join(self.getTableName(tableClass).split(".")[1:])

        for (indexColumns, indexAttrs) in indexes:
            nameOfIndex = self._indexNameOf(tableClass, indexAttrs)
            if nameOfIndex in extantIndexes:
                continue
            csql = 'CREATE INDEX %s.%s ON %s(%s)' % (
                self.databaseName, nameOfIndex, indexColumnPrefix,
                ', '.join(indexColumns))
            self.createSQL(csql)


    def getItemByID(self, storeID, default=_noItem, autoUpgrade=True):
        """
        Retrieve an item by its storeID, and return it.

        Note: most of the failure modes of this method are catastrophic and
        should not be handled by application code.  The only one that
        application programmers should be concerned with is KeyError.  They are
        listed for educational purposes.

        @param storeID: an L{int} which refers to the store.

        @param default: if passed, return this value rather than raising in the
        case where no Item is found.

        @raise TypeError: if storeID is not an integer.

        @raise UnknownItemType: if the storeID refers to an item row in the
        database, but the corresponding type information is not available to
        Python.

        @raise RuntimeError: if the found item's class version is higher than
        the current application is aware of.  (In other words, if you have
        upgraded a database to a new schema and then attempt to open it with a
        previous version of the code.)

        @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.
        """

        if not isinstance(storeID, six.integer_types):
            raise TypeError("storeID *must* be an int or long, not %r" % (
                    type(storeID).__name__,))
        if storeID == STORE_SELF_ID:
            return self
        try:
            return self.objectCache.get(storeID)
        except KeyError:
            pass
        log.msg(interface=iaxiom.IStatEvent, stat_cache_misses=1, key=storeID)
        results = self.querySchemaSQL(_schema.TYPEOF_QUERY, [storeID])
        assert (len(results) in [1, 0]),\
            "Database panic: more than one result for TYPEOF!"
        if results:
            typename, module, version = results[0]
            useMostRecent = False
            moreRecentAvailable = False

            # The schema may have changed since the last time I saw the
            # database.  Let's look to see if this is suspiciously broken...

            if _typeIsTotallyUnknown(typename, version):
                # Another process may have created it - let's re-up the schema
                # and see what we get.
                self._startup()

                # OK, all the modules have been loaded now, everything
                # verified.
                if _typeIsTotallyUnknown(typename, version):

                    # If there is STILL no inkling of it anywhere, we are
                    # almost certainly boned.  Let's tell the user in a
                    # structured way, at least.
                    raise errors.UnknownItemType(
                        "cannot load unknown schema/version pair: %r %r - id: %r" %
                        (typename, version, storeID))

            if typename in _typeNameToMostRecentClass:
                moreRecentAvailable = True
                mostRecent = _typeNameToMostRecentClass[typename]

                if mostRecent.schemaVersion < version:
                    raise RuntimeError("%s:%d - was found in the database and most recent %s is %d" %
                                       (typename, version, typename, mostRecent.schemaVersion))
                if mostRecent.schemaVersion == version:
                    useMostRecent = True
            if useMostRecent:
                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
                # cache here.  (It must, so that app code can safely call
                # upgradeVersion and get a consistent object out of it.)
                x = self.transact(self._upgradeManager.upgradeItem, x)
            elif not x.__legacy__:
                # We loaded the most recent version of an object
                self.objectCache.cache(storeID, x)
            return x
        if default is _noItem:
            raise errors.ItemNotFound(storeID)
        return default


    def querySchemaSQL(self, sql, args=()):
        sql = sql.replace("*DATABASE*", self.databaseName)
        return self.querySQL(sql, args)


    def querySQL(self, sql, args=()):
        """For use with SELECT (or SELECT-like PRAGMA) statements.
        """
        if self.debug:
            result = timeinto(self.queryTimes, self._queryandfetch, sql, args)
        else:
            result = self._queryandfetch(sql, args)
        return result


    def _queryandfetch(self, sql, args):
        if self.debug:
            print('**', sql, '--', ', '.join(map(str, args)))
        self.cursor.execute(sql, args)
        before = time.time()
        result = list(self.cursor)
        after = time.time()
        if after - before > 2.0:
            log.msg('Extremely long list(cursor): %s' % (after - before,))
            log.msg(sql)
            # import traceback; traceback.print_stack()
        if self.debug:
            print('  lastrow:', self.cursor.lastRowID())
            print('  result:', result)
        return result


    def createSQL(self, sql, args=()):
        """
        For use with auto-committing statements such as CREATE TABLE or CREATE
        INDEX.
        """
        before = time.time()
        self._execSQL(sql, args)
        after = time.time()
        if after - before > 2.0:
            log.msg('Extremely long CREATE: %s' % (after - before,))
            log.msg(sql)
            # import traceback; traceback.print_stack()


    def _execSQL(self, sql, args):
        if self.debug:
            rows = timeinto(self.execTimes, self._queryandfetch, sql, args)
        else:
            rows = self._queryandfetch(sql, args)
        assert not rows
        return sql


    def executeSchemaSQL(self, sql, args=()):
        sql = sql.replace("*DATABASE*", self.databaseName)
        return self.executeSQL(sql, args)


    def executeSQL(self, sql, args=()):
        """
        For use with UPDATE or INSERT statements.
        """
        sql = self._execSQL(sql, args)
        result = self.cursor.lastRowID()
        if self.executedThisTransaction is not None:
            self.executedThisTransaction.append((result, sql, args))
        return result class Item(six.with_metaclass(MetaItem, Empowered, slotmachine._Strict)):
    # Python-Special Attributes
    __dirty__ = inmemory()
    __legacy__ = False

    __already_inherited__ = 0

    # Private attributes.
    __store = inmemory()        # underlying reference to the store.

    __everInserted = inmemory() # has this object ever been inserted into the
                                # database?

    __justCreated = inmemory()  # was this object just created, i.e. is there
                                # no committed database representation of it
                                # yet

    __deleting = inmemory()     # has this been marked for deletion at
                                # checkpoint

    __deletingObject = inmemory() # being marked for deletion at checkpoint,
                                  # are we also deleting the central object row
                                  # (True: as in an actual delete) or are we
                                  # simply deleting the data row (False: as in
                                  # part of an upgrade)

    storeID = _SpecialStoreIDAttribute(default=None)
    _storeIDComparer = None
    _axiom_service = inmemory()

    # A mapping from interfaces to in-memory powerups.
    _inMemoryPowerups = inmemory()

    def _currentlyValidAsReferentFor(self, store):
        """
        Is this object currently valid as a reference?  Objects which will be
        deleted in this transaction, or objects which are not in the same store
        are not valid.  See attributes.reference.__get__.
        """
        if store is None:
            # If your store is None, you can refer to whoever you want.  I'm in
            # a store but it doesn't matter that you're not.
            return True
        if self.store is not store:
            return False
        if self.__deletingObject:
            return False
        return True


    def _schemaPrepareInsert(self, store):
        """
        Prepare each attribute in my schema for insertion into a given store,
        either by upgrade or by creation.  This makes sure all references point
        to this store and all relative paths point to this store's files
        directory.
        """
        for name, atr in self.getSchema():
            atr.prepareInsert(self, store)


    def store():
        def get(self):
            return self.__store
        def set(self, store):
            if self.__store is not None:
                raise AttributeError(
                    "Store already set - can't move between stores")

            if store._rejectChanges:
                raise ChangeRejected()

            self._schemaPrepareInsert(store)
            self.__store = store
            oid = self.storeID = self.store.executeSchemaSQL(
                _schema.CREATE_OBJECT, [self.store.getTypeID(type(self))])
            if not self.__legacy__:
                store.objectCache.cache(oid, self)
            if store.autocommit:
                log.msg(interface=iaxiom.IStatEvent,
                        name='database', stat_autocommits=1)

                self.checkpoint()
            else:
                self.touch()
            self.activate()
            self.stored()
        return get, set, """

        A reference to a Store; when set for the first time, inserts this object
        into that store.  Cannot be set twice; once inserted, objects are
        'stuck' to a particular store and must be copied by creating a new
        Item.

        """

    store = property(*store())


    def __repr__(self):
        """
        Return a nice string representation of the Item which contains some
        information about each of its attributes.
        """
        attrs = ", ".join("{n}={v}".format(n=name, v=attr.reprFor(self))
                          for name, attr in sorted(self.getSchema()))
        template = "{s.__name__}({attrs}, storeID={s.storeID})@{id:#x}"
        return template.format(s=self, attrs=attrs, id=id(self))


    def __subinit__(self, **kw):
        """
        Initializer called regardless of whether this object was created by
        instantiation or loading from the database.
        """
        self._axiom_service = None
        self._inMemoryPowerups = {}
        self.__dirty__ = {}
        to__store = kw.pop('__store', None)
        to__everInserted = kw.pop('__everInserted', False)
        to__justUpgraded = kw.pop('__justUpgraded', False)
        self.__store = to__store
        self.__everInserted = to__everInserted
        self.__deletingObject = False
        self.__deleting = False
        tostore = kw.pop('store',None)

        if not self.__everInserted:
            for (name, attr) in self.getSchema():
                if name not in kw:
                    kw[name] = attr.computeDefault()

        for k, v in six.iteritems(kw):
            setattr(self, k, v)

        if tostore != None:
            if to__justUpgraded:

                # we can't just set the store, because that allocates an ID.
                # we do still need to do all the attribute prep, make sure
                # references refer to this store, paths are adjusted to point
                # to this store's static offset, etc.

                self._schemaPrepareInsert(tostore)
                self.__store = tostore

                # However, setting the store would normally cache this item as
                # well, so we need to cache it here - unless this is actually a
                # dummy class which isn't real!  In that case don't.
                if not self.__legacy__:
                    tostore.objectCache.cache(self.storeID, self)
                if tostore.autocommit:
                    self.checkpoint()
            else:
                self.store = tostore


    def __init__(self, **kw):
        """
        Create a new Item.  This is called on an item *only* when it is being created
        for the first time, not when it is loaded from the database.  The
        'activate()' hook is called every time an item is loaded from the
        database, as well as the first time that an item is inserted into the
        store.  This will be inside __init__ if you pass a 'store' keyword
        argument to an Item's constructor.

        This takes an arbitrary set of keyword arguments, which will be set as
        attributes on the created item.  Subclasses of Item must honor this
        signature.
        """
        if type(self) is Item:
            raise CantInstantiateItem()
        self.__justCreated = True
        self.__subinit__(**kw)


    def __finalizer__(self):
        return noop


    def existingInStore(cls, store, storeID, attrs):
        """Create and return a new instance from a row from the store."""
        self = cls.__new__(cls)

        self.__justCreated = False
        self.__subinit__(__store=store,
                         storeID=storeID,
                         __everInserted=True)

        schema = self.getSchema()
        assert len(schema) == len(attrs), "invalid number of attributes"
        for data, (name, attr) in zip(attrs, schema):
            attr.loaded(self, data)
        self.activate()
        return self
    existingInStore = classmethod(existingInStore)

    def activate(self):
        """The object was loaded from the store.
        """

    def getSchema(cls):
        """
        return all persistent class attributes
        """
        schema = []
        for name, atr in cls.__attributes__:
            atr = atr.__get__(None, cls)
            if isinstance(atr, SQLAttribute):
                schema.append((name, atr))
        cls.getSchema = staticmethod(lambda schema=schema: schema)
        return schema
    getSchema = classmethod(getSchema)


    def persistentValues(self):
        """
        Return a dictionary of all attributes which will be/have been/are being
        stored in the database.
        """
        return {k: getattr(self, k) for (k, attr) in self.getSchema()}


    def touch(self):
        # xxx what
        if self.store is None:
            return
        self.store.changed(self)

    def revert(self):
        if self.__justCreated:
            # The SQL revert has already been taken care of.
            if not self.__legacy__:
                self.store.objectCache.uncache(self.storeID, self)
            return
        self.__dirty__.clear()
        dbattrs = self.store.querySQL(
            self._baseSelectSQL(self.store),
            [self.storeID])[0]

        for data, (name, atr) in zip(dbattrs, self.getSchema()):
            atr.loaded(self, data)

        self.__deleting = False
        self.__deletingObject = False


    def deleted(self):
        """User-definable callback that is invoked when an object is well and truly
        gone from the database; the transaction which deleted it has been
        committed.
        """


    def stored(self):
        """
        User-definable callback that is invoked when an object is placed into a
        Store for the very first time.

        If an Item is created with a store, this will be invoked I{after}
        C{activate}.
        """


    def committed(self):
        """
        Called after the database is brought into a consistent state with this
        object.
        """
        if self.__deleting:
            self.deleted()
            if not self.__legacy__:
                self.store.objectCache.uncache(self.storeID, self)
                self.__store = None
        self.__justCreated = False


    def checkpoint(self):
        """
        Update the database to reflect in-memory changes made to this item; for
        example, to make it show up in store.query() calls where it is now
        valid, but was not the last time it was persisted to the database.

        This is called automatically when in 'autocommit mode' (i.e. not in a
        transaction) and at the end of each transaction for every object that
        has been changed.
        """

        if self.store is None:
            raise NotInStore("You can't checkpoint {!r}: not in a store".format(self))


        if self.__deleting:
            if not self.__everInserted:
                # don't issue duplicate SQL and crap; we were created, then
                # destroyed immediately.
                return
            self.store.executeSQL(self._baseDeleteSQL(self.store), [self.storeID])
            # re-using OIDs plays havoc with the cache, and with other things
            # as well.  We need to make sure that we leave a placeholder row at
            # the end of the table.
            if self.__deletingObject:
                # Mark this object as dead.
                self.store.executeSchemaSQL(_schema.CHANGE_TYPE,
                                            [-1, self.storeID])

                # Can't do this any more:
                # self.store.executeSchemaSQL(_schema.DELETE_OBJECT, [self.storeID])

                # TODO: need to measure the performance impact of this, then do
                # it to make sure things are in fact deleted:
                # self.store.executeSchemaSQL(_schema.APP_VACUUM)

            else:
                assert self.__legacy__

            # we're done...
            if self.store.autocommit:
                self.committed()
            return

        if self.__everInserted:
            # case 1: we've been inserted before, either previously in this
            # transaction or we were loaded from the db
            if not self.__dirty__:
                # we might have been checkpointed twice within the same
                # transaction; just don't do anything.
                return
            self.store.executeSQL(*self._updateSQL())
        else:
            # case 2: we are in the middle of creating the object, we've never
            # been inserted into the db before
            schemaAttrs = self.getSchema()

            insertArgs = [self.storeID]
            for (ignoredName, attrObj) in schemaAttrs:
                attrObjDuplicate, attributeValue = self.__dirty__[attrObj.attrname]
                # assert attrObjDuplicate is attrObj
                insertArgs.append(attributeValue)

            # XXX this isn't atomic, gross.
            self.store.executeSQL(self._baseInsertSQL(self.store), insertArgs)
            self.__everInserted = True
        # In case 1, we're dirty but we did an update, synchronizing the
        # database, in case 2, we haven't been created but we issue an insert.
        # In either case, the code in attributes.py sets the attribute *as well
        # as* populating __dirty__, so we clear out dirty and we keep the same
        # value, knowing it's the same as what's in the db.
        self.__dirty__.clear()
        if self.store.autocommit:
            self.committed()

    def upgradeVersion(self, typename, oldversion, newversion, **kw):
        # right now there is only ever one acceptable series of arguments here
        # but it is useful to pass them anyway to make sure the code is
        # functioning as expected
        assert typename == self.typeName, '{!r} != {!r}'.format(typename, self.typeName)
        assert oldversion == self.schemaVersion
        key = typename, newversion
        T = None
        if key in _legacyTypes:
            T = _legacyTypes[key]
        elif typename in _typeNameToMostRecentClass:
            mostRecent = _typeNameToMostRecentClass[typename]
            if mostRecent.schemaVersion == newversion:
                T = mostRecent
        if T is None:
            raise RuntimeError("don't know about type/version pair %s:%d" % (
                    typename, newversion))
        newTypeID = self.store.getTypeID(T) # call first to make sure the table
                                            # exists for doInsert below

        new = T(store=self.store,
                __justUpgraded=True,
                storeID=self.storeID,
                **kw)

        new.touch()
        new.activate()

        self.store.executeSchemaSQL(_schema.CHANGE_TYPE,
                                    [newTypeID, self.storeID])
        self.deleteFromStore(False)
        return new

    def deleteFromStore(self, deleteObject=True):
        # go grab dependent stuff
        if deleteObject:
            if not allowDeletion(self.store, self.__class__,
                                 lambda attr: attr == self):
                raise DeletionDisallowed(
                    'Cannot delete item; '
                    'has referents with whenDeleted == reference.DISALLOW')

            for dependent in dependentItems(self.store, self.__class__,
                                            lambda attr: attr == self):
                dependent.deleteFromStore()

        self.touch()

        self.__deleting = True
        self.__deletingObject = deleteObject

        if self.store.autocommit:
            self.checkpoint()


    # You may specify schemaVersion and typeName in subclasses
    schemaVersion = None
    typeName = None

    ###### SQL generation ######

    def _baseSelectSQL(cls, st):
        if cls not in st.typeToSelectSQLCache:
            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)

    def _baseInsertSQL(cls, st):
        if cls not in st.typeToInsertSQLCache:
            attrs = list(cls.getSchema())
            qs = ', '.join(['?']*(len(attrs)+1))
            st.typeToInsertSQLCache[cls] = (
                'INSERT INTO '+
                st.getTableName(cls) + ' (' + ', '.join(
                    [ st.getShortColumnName(cls.storeID) ] +
                    [ st.getShortColumnName(a[1]) for a in attrs]) +
                ') VALUES (' + qs + ')')
        return st.typeToInsertSQLCache[cls]

    _baseInsertSQL = classmethod(_baseInsertSQL)

    def _baseDeleteSQL(cls, st):
        if cls not in st.typeToDeleteSQLCache:
            st.typeToDeleteSQLCache[cls] = ' '.join(['DELETE FROM',
                                                     st.getTableName(cls),
                                                     'WHERE',
                                                     st.getShortColumnName(cls.storeID),
                                                     '= ? '
                                                     ])
        return st.typeToDeleteSQLCache[cls]

    _baseDeleteSQL = classmethod(_baseDeleteSQL)

    def _updateSQL(self):
        # XXX no point in caching for every possible combination of attribute
        # values - probably.  check out how prepared statements are used in
        # python sometime.
        dirty = list(self.__dirty__.items())
        if not dirty:
            raise RuntimeError("Non-dirty item trying to generate SQL.")
        dirty.sort()
        dirtyColumns = []
        dirtyValues = []
        for dirtyAttrName, (dirtyAttribute, dirtyValue) in dirty:
            dirtyColumns.append(self.store.getShortColumnName(dirtyAttribute))
            dirtyValues.append(dirtyValue)
        stmt = ' '.join([
            'UPDATE', self.store.getTableName(self.__class__), 'SET',
             ', '.join(['%s = ?'] * len(dirty)) %
              tuple(dirtyColumns),
            'WHERE ', self.store.getShortColumnName(type(self).storeID), ' = ?'])
        dirtyValues.append(self.storeID)
        return stmt, dirtyValues


    def getTableName(cls, store):
        """
        Retrieve a string naming the database table associated with this item
        class.
        """
        return store.getTableName(cls)
    getTableName = classmethod(getTableName)


    def getTableAlias(cls, store, currentAliases):
        return None
    getTableAlias = classmethod(getTableAlias) def normalize(qualName):
    """
    Turn a fully-qualified Python name into a string usable as part of a
    table name.
    """
    return qualName.lower().replace('.', '_') class Placeholder(object):
    """
    Wrap an existing L{Item} type to provide a different name for it.

    This can be used to join a table against itself which is useful for
    flattening normalized data.  For example, given a schema defined like
    this::

        class Tag(Item):
            taggedObject = reference()
            tagName = text()


        class SomethingElse(Item):
            ...


    It might be useful to construct a query for instances of SomethingElse
    which have been tagged both with C{"foo"} and C{"bar"}::

        t1 = Placeholder(Tag)
        t2 = Placeholder(Tag)
        store.query(SomethingElse, AND(t1.taggedObject == SomethingElse.storeID,
                                       t1.tagName == u"foo",
                                       t2.taggedObject == SomethingElse.storeID,
                                       t2.tagName == u"bar"))
    """
    _placeholderTableAlias = None

    def __init__(self, itemClass):
        global _placeholderCount

        self._placeholderItemClass = itemClass
        self._placeholderCount = _placeholderCount + 1
        _placeholderCount += 1

        self.existingInStore = self._placeholderItemClass.existingInStore


    def __lt__(self, other):
        """
        Provide a deterministic sort order between Placeholder instances.
        """
        if isinstance(other, Placeholder):
            return self._placeholderCount < other._placeholderCount
        return NotImplemented


    def __eq__(self, other):
        """
        Provide a deterministic sort order between Placeholder instances.
        """
        if isinstance(other, Placeholder):
            return self._placeholderCount == other._placeholderCount
        return NotImplemented


    def __cmp__(self, other):
        """
        Provide a deterministic sort order between Placeholder instances.
        Those instantiated first will compare as less than than instantiated
        later.
        """
        if isinstance(other, Placeholder):
            return cmp(self._placeholderCount, other._placeholderCount)
        return NotImplemented


    def __getattr__(self, name):
        if name == 'storeID' or name in dict(self._placeholderItemClass.getSchema()):
            return _PlaceholderColumn(self, getattr(self._placeholderItemClass, name))
        raise AttributeError(name)


    def getSchema(self):
        # In a MultipleItemQuery, the same table can appear more than
        # once in the "SELECT ..." part of the query, determined by
        # getSchema(). In this case, the correct placeholder names
        # need to be used.
        schema = []
        for (name, atr) in self._placeholderItemClass.getSchema():
            schema.append((
                    name,
                    _PlaceholderColumn(
                        self, getattr(self._placeholderItemClass, name))))
        return schema


    def getTableName(self, store):
        return self._placeholderItemClass.getTableName(store)


    def getTableAlias(self, store, currentAliases):
        if self._placeholderTableAlias is None:
            self._placeholderTableAlias = 'placeholder_' + str(len(currentAliases))
        return self._placeholderTableAlias class Comparable(_ContainableMixin, _ComparisonOperatorMuxer,
                 _MatchingOperationMuxer, _OrderingMixin):
    """
    Helper for a thing that can be compared like an SQLAttribute (or is in fact
    an SQLAttribute).  Requires that 'self' have 'type' (Item-subclass) and
    'columnName' (str) attributes, as well as an 'infilter' method in the
    spirit of SQLAttribute, documented below.
    """

    # XXX TODO: improve error reporting

    def compare(self, other, sqlop):
        return compare(self, other, sqlop) class SQLAttribute(inmemory, Comparable):
    """
    Abstract superclass of all attributes.

    _Not_ an attribute itself.

    @ivar indexed: A C{bool} indicating whether this attribute will be indexed
    in the database.

    @ivar default: The value used for this attribute, if no value is specified.
    """
    sqltype = None

    def __init__(self, doc='', indexed=False, default=None, allowNone=True, defaultFactory=None):
        inmemory.__init__(self, doc)
        self.indexed = indexed
        self.compoundIndexes = []
        self.allowNone = allowNone
        self.default = default
        self.defaultFactory = defaultFactory
        if default is not None and defaultFactory is not None:
            raise ValueError("You may specify only one of default "
                             "or defaultFactory, not both")

    def computeDefault(self):
        if self.defaultFactory is not None:
            return self.defaultFactory()
        return self.default


    def reprFor(self, oself):
        return repr(self.__get__(oself))


    def getShortColumnName(self, store):
        return store.getShortColumnName(self)


    def getColumnName(self, store):
        return store.getColumnName(self)


    def prepareInsert(self, oself, store):
        """
        Override this method to do something to an item to prepare for its
        insertion into a database.
        """

    def coercer(self, value):
        """
        must return a value equivalent to the data being passed in for it to be
        considered valid for a value of this attribute.  for example, 'int' or
        'str'.
        """

        raise NotImplementedError()


    def infilter(self, pyval, oself, store):
        """
        used to convert a Python value to something that lives in the database;
        so called because it is called when objects go in to the database.  It
        takes a Python value and returns an SQL value.
        """
        raise NotImplementedError()

    def outfilter(self, dbval, oself):
        """
        used to convert an SQL value to something that lives in memory; so
        called because it is called when objects come out of the database.  It
        takes an SQL value and returns a Python value.
        """
        return dbval

    # requiredSlots must be called before it's run

    prefix = "_axiom_memory_"
    dbprefix = "_axiom_store_"

    def requiredSlots(self, modname, classname, attrname):
        self.modname = modname
        self.classname = classname
        self.attrname = attrname
        self.underlying = self.prefix + attrname
        self.dbunderlying = self.dbprefix + attrname
        yield self.underlying
        yield self.dbunderlying


    def fullyQualifiedName(self):
        return '.'.join([self.modname,
                         self.classname,
                         self.attrname])

    def __repr__(self):
        return '<%s %s>' % ( self.__class__.__name__, self.fullyQualifiedName())

    def type():
        def get(self):
            if self._type is None:
                from twisted.python.reflect import namedAny
                self._type = namedAny(self.modname+'.'+self.classname)
            return self._type
        return get,
    _type = None
    type = property(*type())

    def __get__(self, oself, cls=None):
        if cls is not None and oself is None:
            if self._type is not None:
                assert self._type == cls
            else:
                self._type = cls
            return self

        pyval = getattr(oself, self.underlying, _NEEDS_FETCH)
        if pyval is _NEEDS_FETCH:
            dbval = getattr(oself, self.dbunderlying, _NEEDS_FETCH)
            if dbval is _NEEDS_FETCH:
                # here is what *is* happening here:

                # SQL attributes are always loaded when an Item is created by
                # loading from the database, either via a query, a getItemByID
                # or an attribute access.  If an attribute is left un-set, that
                # means that the item it is on was just created, and we fill in
                # the default value.

                # Here is what *should be*, but *is not* happening here:

                # this condition ought to indicate that a value may exist in
                # the database, but it is not currently available in memory.
                # It would then query the database immediately, loading all
                # SQL-resident attributes related to this item to minimize the
                # number of queries run (e.g. rather than one per attribute)

                # this is a more desireable condition because it means that you
                # can create items "for free", so doing, for example,
                # self.bar.storeID is a much cheaper operation than doing
                # self.bar.baz.  This particular idiom is frequently used in
                # queries and so speeding it up to avoid having to do a
                # database hit unless you actually need an item's attributes
                # would be worthwhile.

                return self.default
            pyval = self.outfilter(dbval, oself)
            # An upgrader may have changed the value of this attribute.  If so,
            # return the new value, not the old one.
            if dbval != getattr(oself, self.dbunderlying):
                return self.__get__(oself, cls)
            # cache python value
            setattr(oself, self.underlying, pyval)
        return pyval

    def loaded(self, oself, dbval):
        """
        This method is invoked when the item is loaded from the database, and
        when a transaction is reverted which restores this attribute's value.

        @param oself: an instance of an item which has this attribute.

        @param dbval: the underlying database value which was retrieved.
        """
        setattr(oself, self.dbunderlying, dbval)
        delattr(oself, self.underlying) # member_descriptors don't raise
                                        # attribute errors; what gives?  good
                                        # for us, I guess.

    def _convertPyval(self, oself, pyval):
        """
        Convert a Python value to a value suitable for inserting into the
        database.

        @param oself: The object on which this descriptor is an attribute.
        @param pyval: The value to be converted.
        @return: A value legal for this column in the database.
        """
        # convert to dbval later, I guess?
        if pyval is None and not self.allowNone:
            raise TypeError("attribute [%s.%s = %s()] must not be None" % (
                    self.classname, self.attrname, self.__class__.__name__))

        return self.infilter(pyval, oself, oself.store)

    def __set__(self, oself, pyval):
        st = oself.store

        dbval = self._convertPyval(oself, pyval)
        oself.__dirty__[self.attrname] = self, dbval
        oself.touch()
        setattr(oself, self.underlying, pyval)
        setattr(oself, self.dbunderlying, dbval)
        if st is not None and st.autocommit:
            st._rejectChanges += 1
            try:
                oself.checkpoint()
            finally:
                st._rejectChanges -= 1 class integer(SQLAttribute):
    sqltype = 'INTEGER'
    def infilter(self, pyval, oself, store):
        if pyval is None:
            return None
        requireType(self, pyval, inttyperepr, *six.integer_types)
        if not LARGEST_NEGATIVE <= pyval <= LARGEST_POSITIVE:
            raise ConstraintError(
                self, inttyperepr, pyval)
        return pyval class timestamp(integer):
    """
    An in-database representation of date and time.

    To make formatting as easy as possible, this is represented in Python as an
    instance of L{epsilon.extime.Time}; see its documentation for more details.
    """
    def infilter(self, pyval, oself, store):
        if pyval is None:
            return None
        return integer.infilter(self,
                                int(pyval.asPOSIXTimestamp() * MICRO), oself,
                                store)

    def outfilter(self, dbval, oself):
        if dbval is None:
            return None
        return Time.fromPOSIXTimestamp(dbval / MICRO) class textlist(text):
    delimiter = u'\u001f'

    # Once upon a time, textlist encoded the list in such a way that caused []
    # to be indistinguishable from [u'']. This value is now used as a
    # placeholder at the head of the list, to avoid this problem in a way that
    # is almost completely backwards-compatible with older databases.
    guard = u'\u0002'

    def outfilter(self, dbval, oself):
        unicodeString = super(textlist, self).outfilter(dbval, oself)
        if unicodeString is None:
            return None
        val = unicodeString.split(self.delimiter)
        if val[:1] == [self.guard]:
            del val[:1]
        return val

    def infilter(self, pyval, oself, store):
        if pyval is None:
            return None
        for innerVal in pyval:
            assert self.delimiter not in innerVal and self.guard not in innerVal
        result = self.delimiter.join([self.guard] + list(pyval))
        return super(textlist, self).infilter(result, oself, store) class ConstraintError(TypeError):
    """A type constraint was violated.
    """

    def __init__(self,
                 attributeObj,
                 requiredTypes,
                 providedValue):
        self.attributeObj = attributeObj
        self.requiredTypes = requiredTypes
        self.providedValue = providedValue
        TypeError.__init__(self,
                           "attribute [%s.%s = %s()] must be "
                           "(%s); not %r" %
                           (attributeObj.classname,
                            attributeObj.attrname,
                            attributeObj.__class__.__name__,
                            requiredTypes,
                            type(providedValue).__name__)) class ieee754_double(SQLAttribute):
    """
    From the SQLite documentation::

        Each value stored in an SQLite database (or manipulated by the
        database engine) has one of the following storage classes: (...)
        REAL. The value is a floating point value, stored as an 8-byte IEEE
        floating point number.

    This attribute type implements IEEE754 double-precision binary
    floating-point storage.  Some people call this 'float', and think it is
    somehow related to numbers.  This assumption can be misleading when working
    with certain types of data.

    This attribute name has an unweildy name on purpose.  You should be aware
    of the caveats related to binary floating point math before using this
    type.  It is particularly ill-advised to use it to store values
    representing large amounts of currency as rounding errors may be
    significant enough to introduce accounting discrepancies.

    Certain edge-cases are not handled properly.  For example, INF and NAN are
    considered by SQLite to be equal to everything, rather than the Python
    interpretation where INF is equal only to itself and greater than
    everything, and NAN is equal to nothing, not even itself.
    """

    sqltype = 'REAL'

    def infilter(self, pyval, oself, store):
        if pyval is None:
            return None
        requireType(self, pyval, 'float', float)
        return pyval

    def outfilter(self, dbval, oself):
        return dbval class point1decimal(AbstractFixedPointDecimal):
    decimalPlaces = 1 class money(point4decimal):
    """
    I am a 4-point precision fixed-point decimal number column type; suggested
    for representing a quantity of money.

    (This does not, however, include features such as currency.)
    """ class text(SQLAttribute):
    """
    Attribute representing a sequence of characters; this is represented in
    memory as a Python 'unicode'.
    """

    def __init__(self, caseSensitive=False, **kw):
        SQLAttribute.__init__(self, **kw)
        if caseSensitive:
            self.sqltype = 'TEXT'
        else:
            self.sqltype = 'TEXT COLLATE NOCASE'
        self.caseSensitive = caseSensitive

    def infilter(self, pyval, oself, store):
        if pyval is None:
            return None
        if not isinstance(pyval, six.string_types) or '\0' in pyval:
            raise ConstraintError(
                self, "unicode string without NULL bytes", pyval)
        return pyval

    def outfilter(self, dbval, oself):
        return dbval def axiomText(*a, **kw):
    """
    Strategy for generating Axiom-compatible text values.
    """
    return st.text(
        alphabet=st.characters(
            blacklist_categories={'Cs'},
            blacklist_characters={'\x00'}),
        *a, **kw) def axiomIntegers(minValue=LARGEST_NEGATIVE, maxValue=LARGEST_POSITIVE):
    """
    Strategy for generating Axiom-compatible integers.

    @type minValue: L{int}
    @param minValue: Minimum value to generate; default is the least value
        that can be stored in an L{axiom.attributes.integer} attribute.

    @type manValue: L{int}
    @param manValue: Maximum value to generate; default is the greatest value
        that can be stored in an L{axiom.attributes.integer} attribute.
    """
    return st.integers(min_value=minValue, max_value=maxValue) def fixedDecimals(precision, minValue=None, maxValue=None):
    """
    Strategy for generating L{decimal.Decimal} values of a fixed precision.

    @type precision: L{decimal.Decimal}
    @param precision: The precision to use; for example, C{Decimal('0.01')} for
        a L{axiom.attributes.point2decimal} attribute.

    @type minValue: L{decimal.Decimal}
    @param minValue: The minimum value to generate, or C{None} for the least
        possible.
    @type minValue: L{decimal.Decimal}
    @param minValue: The maximum value to generate, or C{None} for the greatest
        possible.
    """
    if minValue is None:
        minValue = LARGEST_NEGATIVE
    else:
        minValue = int(minValue / precision)
    if maxValue is None:
        maxValue = LARGEST_POSITIVE
    else:
        maxValue = int(maxValue / precision)
    return st.integers(min_value=minValue, max_value=maxValue).map(
        lambda v: v * precision) def textlists():
    """
    Strategy for generating lists storable with L{axiom.attributes.textlist}.
    """
    return st.lists(st.text(
        alphabet=st.characters(
            blacklist_categories={'Cs'},
            blacklist_characters={'\x00', '\x02', '\x1f'}))) def timestamps(*a, **kw):
    """
    Strategy for generating L{epsilon.extime.Time} objects.
    """
    return st.builds(Time.fromDatetime, st.datetimes(*a, **kw))
import random
import gc
from decimal import Decimal
from datetime import timedelta
from epsilon.extime import Time
from twisted.trial.unittest import TestCase
from twisted.python.reflect import qual
from hypothesis import given, strategies as st
from axiom.store import Store
from axiom.item import Item, normalize, Placeholder
from axiom.attributes import (
    Comparable, SQLAttribute, integer, timestamp, textlist, ConstraintError,
    ieee754_double, point1decimal, money, text)
from axiom.test.strategies import (
    axiomText, axiomIntegers, fixedDecimals, textlists, timestamps)
        """
        self.assertEqual(
            SQLAttributeDummyClass.dummyAttribute.attrname,
            'dummyAttribute')


    def test_fullyQualifiedName(self):
        """
        Test that the L{SQLAttribute.fullyQualifiedName} method correctly
        returns the fully qualified Python name of the attribute: that is, the
        fully qualified Python name of the type it is defined on (plus a dot)
        plus the name of the attribute.
        """
        self.assertEqual(
            SQLAttributeDummyClass.dummyAttribute.fullyQualifiedName(),
            'axiom.test.test_attributes.SQLAttributeDummyClass.dummyAttribute')


    def test_fullyQualifiedStoreID(self):
        """
        Test that the L{IColumn} implementation on the storeID emits the
        correct fullyQualifiedName as well.  This is necessary because storeID
        is unfortunately implemented differently than other columns, due to its
        presence on Item.
        """
        self.assertEqual(
            SQLAttributeDummyClass.storeID.fullyQualifiedName(),
            'axiom.test.test_attributes.SQLAttributeDummyClass.storeID')


    def test_fullyQualifiedPlaceholder(self):
        """
        Verify that the L{IColumn.fullyQualifiedName} implementation on
        placeholder attributes returns a usable string, but one which is
        recognizable as an invalid Python identifier.
        """
        ph = Placeholder(SQLAttributeDummyClass)
        self.assertEqual(
            'axiom.test.test_attributes.SQLAttributeDummyClass'
            '.dummyAttribute.<placeholder:%d>' % (ph._placeholderCount,),
            ph.dummyAttribute.fullyQualifiedName())


    def test_accessor(self):
        """
        Test that the __get__ of SQLAttribute does the obvious thing, and returns
        its value when given an instance.
        """
        dummy = FullImplementationDummyClass(dummyAttribute=1234)
        self.assertEqual(
            FullImplementationDummyClass.dummyAttribute.__get__(dummy), 1234)
        self.assertEqual(dummy.dummyAttribute, 1234)


    def test_storeIDAccessor(self):
        """
        Test that the __get__ of the IColumn implementation for storeID works
        the same as that for normal attributes.
        """
        s = Store()
        dummy = FullImplementationDummyClass(store=s)
        self.assertIdentical(s.getItemByID(dummy.storeID), dummy)

    def test_placeholderAccessor(self):
        """
        Test that the __get__ of SQLAttribute does the obvious thing, and returns
        its value when given an instance.
        """
        dummy = FullImplementationDummyClass(dummyAttribute=1234)
        self.assertEqual(
            Placeholder(FullImplementationDummyClass
                        ).dummyAttribute.__get__(dummy), 1234)
        self.assertEqual(dummy.dummyAttribute, 1234)


    def test_typeAttribute(self):
        """
        Test that the C{type} attribute of an L{SQLAttribute} references the
        class on which the attribute is defined.
        """
        self.assertIdentical(
            SQLAttributeDummyClass,
            SQLAttributeDummyClass.dummyAttribute.type)


    def test_getShortColumnName(self):
        """
        Test that L{Store.getShortColumnName} returns something pretty close to
        the name of the attribute.

        XXX Testing this really well would require being able to parse a good
        chunk of SQL.  I don't know how to do that yet. -exarkun
        """
        s = Store()
        self.assertIn(
            'dummyAttribute',
            s.getShortColumnName(SQLAttributeDummyClass.dummyAttribute))


    def test_getColumnName(self):
        """
        Test that L{Store.getColumnName} returns something made up of the
        attribute's type's typeName and the attribute's name.
        """
        s = Store()
        self.assertIn(
            'dummyAttribute',
            s.getColumnName(SQLAttributeDummyClass.dummyAttribute))
        self.assertIn(
            normalize(qual(SQLAttributeDummyClass)),
            s.getColumnName(SQLAttributeDummyClass.dummyAttribute))



class KitchenSink(Item):
    """
    An item with one of everything, more or less.
    """
    t = text()
    i = integer()

    ts = timestamp()