# GNU Enterprise Common Library - Base database driver - Result set
#
# Copyright 2000-2007 Free Software Foundation
#
# This file is part of GNU Enterprise.
#
# GNU Enterprise is free software; you can redistribute it
# and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation; either
# version 2, or (at your option) any later version.
#
# GNU Enterprise is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public
# License along with program; see the file COPYING. If not,
# write to the Free Software Foundation, Inc., 59 Temple Place
# - Suite 330, Boston, MA 02111-1307, USA.
#
# $Id: ResultSet.py 9222 2007-01-08 13:02:49Z johannes $

"""
Generic ResultSet class extended by all database driver plugins.
"""

__all__ = ['ResultSet']

from gnue.common.datasources import Exceptions

from RecordSet import RecordSet


# =============================================================================
# Basic ResultSet class
# =============================================================================

class ResultSet:
    """
    Representation of a database resultset (a set of records usually
    representing the result of a database query).

    A ResultSet instance encapsulates an ordered set of database records. It
    maintains a cursor that can be moved around. It also provides functions to
    insert new records and to post changes to the backend.

    This class must be subclassed by all database drivers, and a driver must at
    least implement one of the following functions:
        - _query_object_ (for normal queries)
        - _query_sql_ (for raw SQL queries)
    and the L{_count_}, L{_fetch_} and L{_close_} functions.
    """

    # -------------------------------------------------------------------------
    # Constructor
    # -------------------------------------------------------------------------

    def __init__(self,
            defaultData      = {},
            connection       = None,
            tablename        = None,
            rowidField       = None,
            primarykeyFields = [],
            primarykeySeq    = None,
            boundFields      = [],
            requery          = True,
            readonly         = False,
            details          = {},
            eventController  = None):
        """
        Create a new ResultSet instance.

        @param defaultData: Dictionary with default data to be used whenever a
            new record is inserted in this ResultSet.
        @param connection: GConnection object the ResultSet object can use to
            query data and post changes.
        @param tablename: Table name.
        @param rowidField: Field name of the field containing a unique row id
            generated by the backend, if available.
        @param primarykeyFields: List of field names that make up a unique key,
            if available.
        @param primarykeySeq: If this is set to the name of a backend sequence,
            the getSequence method of the Connection object is called to fill
            the primarykeyField before posting to the backend.  If
            primarykeySeq is given, the primarykeyFields may only contain a
            single field name.
        @param boundFields: List of fields to be included when posting changes
            to the backend. All fields not in this list are considered unbound
            fields and are not persistent.
        @param requery: If this is set to True, the ResultSet reqeries its
            values from the backend after posting, in case a backend trigger
            has changed something.  This happens in the L{requery} method which
            has to be called after L{post}.
        @param readonly: True if the ResultSet is read only. If set, an attempt
            to insert, modify or delete any record in this RecordSet raises an
            exception.
        @param details: Dictionary defining all details of this ResultSet,
            where the key is the L{GDataSource} object and the values are
            tuples containing a list of primary key fields and a list of the
            corresponding foreign key fields.
        @param eventController: EventController instance to notify of data
            events.
        """

        self.__defaultData      = defaultData
        self.__connection       = connection
        self.__tablename        = tablename
        self.__rowidField       = rowidField
        self.__primarykeyFields = primarykeyFields
        self.__primarykeySeq    = primarykeySeq
        self.__boundFields      = boundFields
        self.__requery          = requery
        self.__readonly         = readonly
        self.__details          = details
        self.__eventController  = eventController

        # Data for static datasources
        self.__static_data = []

        # Parameters of last query
        self.__lastquery_type = None
        self.__lastquery_cache = None
        self.__lastquery_kwargs = None

        # Generator to yield fieldname/value dictionaries
        self.__generator = None

        # List of all RecordSet objects cached in this ResultSet
        self.__cached_records = []

        # Index of the current record
        self.__current_index = -1

        # Number of records
        self.__record_count = 0

        # Pointer to the current record
        self.current = None


    # -------------------------------------------------------------------------
    # Sequence behaviour
    # -------------------------------------------------------------------------

    def __iter__(self):
        """
        Return an iterator yielding the records in the resultset as
        L{RecordSet.RecordSet} instances.
    
        The cursor is not moved while iterating through the resultset.
        """
        position = 0
        while True:
            if position >= len(self.__cached_records):
                if not self.__cache_next_record():
                    break
            yield self.__cached_records[position]
            position += 1

    # -------------------------------------------------------------------------

    def __nonzero__(self):
        return True

    # -------------------------------------------------------------------------

    def __len__(self):
        # Python len() function requires datatype "int"!
        return int(self.getRecordCount())

    # -------------------------------------------------------------------------

    def __getitem__(self, index):
        record = self.getRecord(index)
        if not record:
            raise IndexError
        else:
            return record


    # -------------------------------------------------------------------------
    # String representation
    # -------------------------------------------------------------------------

    def __repr__(self):
        """
        Shows a string representation of the RecordSet.
        """

        if self.__tablename:
            return "<ResultSet for %s at %d>" % (self.__tablename, id(self))
        else:
            return "<Unbound/Static ResultSet at %d>" % id(self)


    # -------------------------------------------------------------------------
    # Execute a query
    # -------------------------------------------------------------------------

    def query(self, type, cache, **kwargs):
        """
        Populate the resultset with data.

        @param type: Type of the query, can be 'object' or 'sql'.
        @param cache: Size of the cache to use for this query.
        @param kwargs: Depends on the type.
        @raise Exceptions.ObjectTypeNotAvailableError: if the requested type of
            query is not available for this connection.
        @raise Exception: if the query cannot be executed. The exact exception
            type depends on the backend.
        """

        checktype(type, str)
        checktype(cache, int)

        # Dispose current result set data
        self.__generator = None
        self.__cached_records = []
        self.__current_index = -1
        self.__record_count = 0
        self.current = None

        # Remember this query to be able to repeat it in _refresh()
        self.__lastquery_type = type
        self.__lastquery_cache = cache
        self.__lastquery_kwargs = kwargs

        queryfunc = '_query_' + type + '_'
        if not hasattr(self, queryfunc):
            raise Exceptions.ObjectTypeNotAvailableError, type

        getattr(self, queryfunc)(self.__connection, **kwargs)

        self.__generator = self._fetch_(cache)

        # (TODO: could be delayed to first call of getRecordCount)
        self.__record_count = self._count_()


    # -------------------------------------------------------------------------
    # Get the raw data for this ResultSet
    # -------------------------------------------------------------------------

    def raw(self):
        """
        Return an iterator yielding the raw values of this ResultSet.

        The iterator returned by this function will yield dictionaries, with
        field names being the keys and field values being the values.

        Note that this function cannot be used together with the normal
        firstRecord/nextRecord functions, because they base on the same source
        of data: any record fetched with firstRecord/nextRecord will not appear
        in this iterator, and vice versa.

        If you just need the raw data and do not intend to make any use of
        L{RecordSet.RecordSet}'s features, it is strongly recommended that you
        use this function, because it performes more than 10 times faster than
        firstRecord/nextRecord.
        """

        return self.__generator

    # -------------------------------------------------------------------------
    # Get the number of records in the recordset
    # -------------------------------------------------------------------------

    def getRecordCount(self):
        """
        Return the number of records currently in the recordset.
        """

        if self.__record_count > 0:
            return self.__record_count
        else:
            # Fallback in case record count unknown
            return len(self.__cached_records)


    # -------------------------------------------------------------------------
    # Get a specific record (0=based)
    # -------------------------------------------------------------------------

    def getRecord(self, record):
        """
        Return the record at the given position without moving the cursor.

        @param record: the zero-based position of the record to return.
        @return: the L{RecordSet.RecordSet} instance, or None if the given
            position is higher than the number of records in the ResultSet.
        @raise Exception: if the requested record is not yet in cache and
            fetching it from the backend fails. The exact exception class
            depends on the backend.
        """

        checktype(record, int)

        while (record + 1 > len(self.__cached_records)) \
                and self.__cache_next_record():
            pass

        if record + 1 > len(self.__cached_records):
            return None
        else:
            return self.__cached_records[record]


    # -------------------------------------------------------------------------
    # Get data as array
    # -------------------------------------------------------------------------

    def getArray(self, fields):
        """
        Return the values of the given fields for all records in the resultset
        as a 2-dimensional list.
    
        The record pointer is not moved.

        @param fields: Fieldnames of the fields to include in the array.
        @raise Exception: if a record is not yet in cache and fetching it from
            the backend fails. The exact exception class depends on the
            backend.
        """

        checktype(fields, list)

        # First, load all records into the cache
        while self.__cache_next_record():
            pass

        # Now build up the array
        result = []
        for record in self.__cached_records:
            line = []
            for field in fields:
                line.append(record[field])
            result.append(line)
        return result


    # -------------------------------------------------------------------------
    # Get data as multi dimensional dictionary
    # -------------------------------------------------------------------------

    def getDictArray(self, keyfields, fields):
        """
        Return the values of the given fields for all records as a
        multidimensional dictionary.
    
        The record pointer is not moved.

        @param keyfields: Fieldnames of the fields to use as dictionary keys.
        @param fields: Fieldnames of the fields to include in the value
            dictionaries.
        @return: Dictionary with the values of the first keyfield as key, and
            the values are dictionaries with the value of the second keyfield
            as key, and so on, until the last dictionary contains the
            fieldname/value pairs for the fields given in the second parameter.
        @raise Exception: if a record is not yet in cache and fetching it from
            the backend fails. The exact exception class depends on the
            backend.
        """

        checktype(keyfields, list)
        checktype(fields, list)

        # First, load all records into the cache
        while self.__cache_next_record():
            pass

        # Now build up the array
        result = {}
        for record in self.__cached_records:
            d = result
            for field in keyfields:
                d = d.setdefault(record[field], {})
            for field in fields:
                d[field] = record[field]
        return result


    # -------------------------------------------------------------------------
    # Record navigation
    # -------------------------------------------------------------------------

    def firstRecord(self):
        """
        Move the cursor to the first record.

        @return: the new current record as a L{RecordSet.RecordSet} instance,
            or None if the resultset is empty.
        @raise Exception: if the requested record is not yet in cache and
            fetching it from the backend fails. The exact exception class
            depends on the backend.
        """
        if self.__current_index < 0:
            if not self.__cache_next_record():
                return None
        self.__move(0)
        return self.current

    # -------------------------------------------------------------------------

    def prevRecord(self):
        """
        Move the cursor backwards by one record.

        If the cursor already points to the first record, it is not moved.

        @return: the new current record as a L{RecordSet.RecordSet} instance,
            or None if the cursor already pointed to the first record.
        """
        if self.__current_index < 1:
            return None
        else:
            self.__move(self.__current_index - 1)
            return self.current

    # -------------------------------------------------------------------------

    def nextRecord(self):
        """
        Move the cursor forward by one record.

        If the cursor already points to the last record, it is not moved.

        @return: the new current record as a L{RecordSet.RecordSet} instance,
            or None if the cursor already pointed to the last record.
        @raise Exception: if the requested record is not yet in cache and
            fetching it from the backend fails. The exact exception class
            depends on the backend.
        """
        if self.__current_index + 1 == len(self.__cached_records):
            if not self.__cache_next_record():
                return None
        self.__move(self.__current_index + 1)
        return self.current

    # -------------------------------------------------------------------------

    def lastRecord(self):
        """
        Move the cursor to the last record.

        @return: the new current record as a L{RecordSet.RecordSet} instance,
            or None if the resultset is empty.
        @raise Exception: if the requested record is not yet in cache and
            fetching it from the backend fails. The exact exception class
            depends on the backend.
        """
        while self.__cache_next_record():
            pass
        if len(self.__cached_records) == 0:
            return None
        else:
            self.__move(len(self.__cached_records) - 1)
            return self.current

    # -------------------------------------------------------------------------

    def setRecord(self, record):
        """
        Set the cursor to a specific record.

        If the number of the record to set the cursor to is greater than the
        number of records in the resultset, the cursor is not moved.

        @param record: zero-based number of the record to set the cursor to.
        @return: the new current record as a L{RecordSet.RecordSet} instance,
            or None if the record number to set the cursor to is greater than
            the number of records in the resultset.
        @raise Exception: if the requested record is not yet in cache and
            fetching it from the backend fails. The exact exception class
            depends on the backend.
        """
        checktype(record, int)

        while (record > len(self.__cached_records) - 1) \
                and self.__cache_next_record():
            pass
        if record >= len(self.__cached_records):
            return None
        else:
            self.__move(record)
            return self.current

    # -------------------------------------------------------------------------

    def findRecord(self, fieldValues):
        """
        Find a record by field values.

        This function searches through the already loaded records and moves the
        cursor to the first record to match the given fieldValues dictionary.
        If no match is found, then the record pointer is set to -1.

        @param fieldValues: fieldname/value dictionary to search for.
        @return: the first record that matches as a L{RecordSet.RecordSet}
            instance or None if no match was found.
        @raise Exception: if a record is not already in cache and fetching it
            from the backend fails. The exact exception class depends on the
            backend.
        """
        checktype(fieldValues, dict)

        i = 0
        while True:
            if i >= len(self.__cached_records):
                if not self.__cache_next_record():
                    # No match found
                    self.__move(-1)
                    return None
            record = self.__cached_records[i]
            found = True
            for (key, value) in fieldValues.items():
                if record[key] != value:
                    found = False
                    continue
            if found:
                self.__move(i)
                return self.current
            i += 1

    # -------------------------------------------------------------------------

    def __move(self, record):
        if record != self.__current_index or (self.__current_index >= 0 \
                and self.current != self.__cached_records[self.__current_index]):
            self.__current_index = record
            self.__sync(True)

    # -------------------------------------------------------------------------

    def isFirstRecord(self):
        """
        Return True if the cursor is at the first record.
        """
        return (self.__current_index == 0)

    # -------------------------------------------------------------------------

    def isLastRecord(self):
        """
        Return True if the cursor is at the last record.

        @raise Exception: if the next record is not yet in cache and fetching
            it from the backend fails. The exact exception class depends on the
            backend.
        """
        if self.__current_index < len(self.__cached_records) - 1 or \
                self.__cache_next_record():
            return False
        else:
            return True

    # -------------------------------------------------------------------------

    def getRecordNumber(self):
        """
        Return the zero-based position of the cursor within the recordset.
        """
        return self.__current_index


    # -------------------------------------------------------------------------
    # Insert a new record after the current one
    # -------------------------------------------------------------------------

    def insertRecord(self, defaultData = {}):
        """
        Insert a new, empty record after the current cursor position.

        The cursor is moved to the newly inserted record.

        @param defaultData: fieldname/value pairs to initialize the record
            with.  All fields not given in this dictionary are initialized with
            None.
        @return: the newly inserted record.
        @raise Exceptions.ReadOnlyInsertError: if the ResultSet is read only.
        """

        checktype(defaultData, dict)

        if self.__readonly:
            raise Exceptions.ReadOnlyInsertError

        assert gDebug(8, 'Inserting a blank record in %s' % self)

        self.__record_count += 1
        self.__current_index += 1
        record = self.__create_record(
                defaultData = defaultData,
                position    = self.__current_index)
        self.__sync(True)
        return record


    # -------------------------------------------------------------------------
    # Create a new record with a copy of the existing one
    # -------------------------------------------------------------------------

    def duplicateRecord(self, exclude = [], include = []):
        """
        Create a new record and initialize it with field values from the record
        at the current cursor position.

        The cursor is moved to the newly inserted record.

        @param exclude: list of fields not to copy.
        @param include: list of fields to copy. An empty list means to copy all
            fields except primary key fields and rowid fields, which are never
            copied anyway.
        @return: the newly inserted record.
        @raise Exceptions.ReadOnlyInsertError: if the ResultSet is read only.
        """

        checktype(exclude, list)
        checktype(include, list)

        current = self.current
        inserted = self.insertRecord()

        # If include= is specified, then that is our base list.
        # Otherwise, get the base list as the fields in the table
        if include:
            fields = list(include)
        else:
            fields = current.keys()

        # Exclude all the fields in exclude=
        for field in exclude:
            fields.remove(field)

        # Do not duplicate the primary key fields,
        # unless it was named in the include= parameter
        for field in self.__primarykeyFields:
            if field not in include and field in fields:
                fields.remove(field)

        # Never include the rowid
        field = self.__rowidField
        if field and field in fields:
            fields.remove(field)

        # Copy the fields over
        for field in fields:
            inserted[field] = current[field]

        return inserted


    # -------------------------------------------------------------------------
    # Find out if there is anything to post
    # -------------------------------------------------------------------------

    def isPending(self):
        """
        Return True if the resultset or a detail resultset has uncommitted
        changes.
        """
        for rec in self.__cached_records:
            if rec.isPending():
                return True
        return False


    # -------------------------------------------------------------------------
    # Post changes to the backend
    # -------------------------------------------------------------------------

    def post(self, fkData = {}):
        """
        Post all local changes to the backend.

        This method leaves the ResultSet in an incomplete state.  After every
        call to post, L{requery} must be called.  If the operation should be
        committed, the L{Connection.commit} method can be called between post
        and requery.

        This method does not change the status of any record, so in case of an
        exception, it can be just called again.

        @param fkData: fieldname/value dictionary for foreign key fields. Used
            internally for detail resultsets in a master/detail relationship.
        @raise Exception: if posting the changes to the backend fails for any
            reason. The exact exception classes depend on the backend.
        """

        # save current record position
        currentRecord = self.__current_index

        # post our changes
        try:
            # we move the cursor along while we post, so triggers see the
            # posting record as the current record
            self.__current_index = 0
            while self.__current_index < len(self.__cached_records):
                self.current = self.__cached_records[self.__current_index]
                if self.current.isPending() and not self.current.isVoid():

                    # activate all matching detail resultsets so that commit
                    # triggers see the correct details
                    self.current._activate()

                    # Set the foreign keys for inserted records in case the
                    # master changed its primary key in a commit trigger
                    if self.current.isInserted():
                        for (fieldname, value) in fkData.items():
                            self.current[fieldname] = value

                    # write changes to the backend
                    self.current._post()

                self.__current_index += 1

        except:
            # If any error happened on writing to the backend, move the UI to
            # the record that caused the error
            self.__sync(True)
            raise

        # Restore current record position
        self.__current_index = currentRecord


    # -------------------------------------------------------------------------
    # Sync resultset with backend, and sync listeners with resultset
    # -------------------------------------------------------------------------

    def requery(self, commit):
        """
        Synchronize everything after a call to L{post}.

        This method must be called after each call to the L{post} method. If
        the operation should be committed, the L{Connection.commit} method can
        be called between post and requery.

        Note that this method does not only requery changes from the database,
        but it also updates the record status for all added, modified or
        deleted records. So, requery must always be called after L{post}, even
        if the requery feature is not used.

        @param commit: indicate whether a commit was run since the last L{post}
            call.

        @raise Exception: if querying the records from the backend fails for
            any reason. The exact exception classes depend on the backend.
        """

        index = 0
        while index < len(self.__cached_records):
            record = self.__cached_records[index]
            if record._needsRequery(commit):
                if ((record.isEmpty() or record.isVoid() \
                                or record.isDeleted()) \
                        and self.__connection is not None):
                    self.__remove_record(index)
                else:
                    record._requery(commit)
                    index += 1
            else:
                index += 1

        self.__sync(False)


    # -------------------------------------------------------------------------
    # Merge another ResultSet into this one
    # -------------------------------------------------------------------------

    def _merge(self, otherResultSet):
        """
        Merge another (more current) ResultSet into this one.

        This function is used by the master RecordSet to update it's detail
        ResultSets after it has been committed to the backend (and some backend
        triggers might have changed the details).

        @param otherResultSet: the other ResultSet to merge
        """

        if self.__primarykeyFields:
            keyFields = self.__primarykeyFields
        elif self.__rowidField:
            keyFields = [self.__rowidField]
        else:
            return

        # Make sure that all records are cached
        while self.__cache_next_record():
            pass

        newData = otherResultSet.getDictArray(keyFields, self.__boundFields)

        index = 0
        for record in self.__cached_records[:]:
            if record.isEmpty():
                # keep empty record in old ResultSet
                index += 1
                continue
            d = newData
            for field in keyFields:
                if d.has_key(record[field]):
                    d = d[record[field]]
                else:
                    # Record is not in newData - it has been deleted meanwhile
                    d = None
                    break
            if d:
                # Found in newData - update RecordSet
                record._initialDataFromDict(d)
                # And set to empty dict to indicate it has been processed
                d.clear()
                index += 1
            else:
                # Not found in newData - delete it
                self.__remove_record(index)

        # Add the rest of newData - it has been inserted
        # Convert the multi dimensional dictionary into a list
        l = newData.values()
        for field in keyFields[1:]:
            l2 = []
            for d in l:
                l2 += d.values()
            l = l2
        # Now for all non-empty dicts, append a new record
        for row in l:
            if row:
                record = self.__create_record(initialData = row)
                self.__record_count += 1

        self.__sync(False)


    # -------------------------------------------------------------------------
    # Query the backend again with the same query as the original one
    # -------------------------------------------------------------------------

    def _refresh(self):
        """
        Repeat the last query, updating the result set with current data from
        the backend.

        This method issues a new query against the backend which is identical
        to the last query that was used to build this result set. Data from
        this new query then replaces the data currently held in the result set.
        If there were unsaved changes before, they are lost.  This method tries
        to set the current record pointer to the same record as it was before.
        """
        # Remember current record
        old_current = self.current

        # Fire the query with the remembered parameters
        self.query(self.__lastquery_type, self.__lastquery_cache,
                **self.__lastquery_kwargs)

        # Set curent record pointer to old current record. This impiles
        # fetching through the new result set until we find it.
        if self.__rowidField:
            keyfields = {self.__rowidField: old_current[self.__rowidField]}
        elif self.__primarykeyFields:
            keyfields = {}
            for field in self.__primarykeyFields:
                keyfields[field] = old_current[field]
        else:
            keyfields = {}
            for field in self.__boundFields:
                keyfields[field] = old_current[field]
        if keyfields:
            if self.findRecord(keyfields):
                # We have found the old current record, nothing more to do
                pass
            else:
                # Old current record is not there in new data
                # FIXME: Try to navigate to first record that would follow the
                # current record according to current sort order
                self.firstRecord()


    # -------------------------------------------------------------------------
    # Close the result set
    # -------------------------------------------------------------------------

    def close(self):
        """
        Close the database connection.
        """

        self._close_()


    # -------------------------------------------------------------------------
    # Sync self.current with self.__current_index and adjust detail resultsets
    # and the user interface
    # -------------------------------------------------------------------------

    def __sync(self, send_event):

        old_current = self.current
        if self.__current_index == -1:
            self.current = None
        else:
            self.current = self.__cached_records[self.__current_index]

        # If the current record has *really* changed (this method can be called
        # for non-changing records after requery or merge) to a new current
        # record, bring all detail records in sync.
        if self.current and self.current != old_current:
            # If the cursor moved out of an empty record, throw it away.
            if old_current and old_current.isEmpty():
                if old_current in self.__cached_records:
                    index = self.__cached_records.index(old_current)
                    self.__remove_record(index)
            self.current._activate()

        if send_event and self.__eventController is not None:
            self.__eventController.dispatchEvent('dsCursorMoved')


    # -------------------------------------------------------------------------
    # Load next record from backend into cache
    # -------------------------------------------------------------------------

    def __cache_next_record(self):

        if not self.__generator:
            return False

        try:
            row = self.__generator.next()
        except StopIteration:
            return False

        record = self.__create_record(initialData = row)

        return True


    # -------------------------------------------------------------------------
    # Create a new record in the cache
    # -------------------------------------------------------------------------

    def __create_record(self, initialData = {}, defaultData = {},
            position = None):

        __defaultData = self.__defaultData.copy()
        __defaultData.update(defaultData)

        record = RecordSet(
                initialData      = initialData,
                defaultData      = __defaultData,
                connection       = self.__connection,
                tablename        = self.__tablename,
                rowidField       = self.__rowidField,
                primarykeyFields = self.__primarykeyFields,
                primarykeySeq    = self.__primarykeySeq,
                boundFields      = self.__boundFields,
                requery          = self.__requery,
                readonly         = self.__readonly,
                details          = self.__details,
                eventController  = self.__eventController)

        if position is None:
            self.__cached_records.append(record)
        else:
            self.__cached_records.insert(position, record)

        return record


    # -------------------------------------------------------------------------
    # Remove a record from the cache
    # -------------------------------------------------------------------------

    def __remove_record(self, index):

        self.__cached_records.pop(index)
        self.__record_count -= 1

        # if a record preceding the cursor position was deleted, move cursor
        # position along
        if index <= self.__current_index:
            self.__current_index -= 1

        # ... but don't move below 0 unless there is *really* no record left
        if self.__current_index < 0 and self.__record_count:
            self.__current_index = 0


    # -------------------------------------------------------------------------
    # Virtual methods
    # -------------------------------------------------------------------------

    def _query_static_(self, connection, data):
        """
        Execute a query.

        Descendants would rather overwrite _query_object_ and/or _query_sql_.
        Parameters depend on the type of query (object/sql).
        """
        self.__static_data = data

    # -------------------------------------------------------------------------

    def _count_(self):
        """
        Return the number of records returned by the query.

        @return: Number of records that will be yielded by @L{_fetch_}.
        """
        return len(self.__static_data)

    # -------------------------------------------------------------------------

    def _fetch_(self, cachesize):
        """
        Yield records from the query.

        Descendants must overwrite this function to return a sequence of
        fieldname/value dictionaries.

        @param cachesize: Recommended cache size the driver should use to
            communicate with the backend.
        @return: A generator for fieldname/value dictionaries.
        """
        for row in self.__static_data:
            yield row

    # -------------------------------------------------------------------------

    def _close_(self):
        """
        Close the cursor.

        Descendants can overwrite this function to clean up things done in the
        _query_ functions.
        """
        pass
