[Top][All Lists]
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[gnue] r7308 - trunk/gnue-appserver/src
From: |
johannes |
Subject: |
[gnue] r7308 - trunk/gnue-appserver/src |
Date: |
Thu, 7 Apr 2005 04:31:58 -0500 (CDT) |
Author: johannes
Date: 2005-04-07 04:31:56 -0500 (Thu, 07 Apr 2005)
New Revision: 7308
Modified:
trunk/gnue-appserver/src/data.py
trunk/gnue-appserver/src/geasInstance.py
trunk/gnue-appserver/src/geasSession.py
Log:
Improved caching
Modified: trunk/gnue-appserver/src/data.py
===================================================================
--- trunk/gnue-appserver/src/data.py 2005-04-06 23:02:54 UTC (rev 7307)
+++ trunk/gnue-appserver/src/data.py 2005-04-07 09:31:56 UTC (rev 7308)
@@ -26,16 +26,19 @@
import string
import copy
import weakref
+import time
from gnue.common.apps import errors
from gnue.common.datasources import GDataSource, GConditions, GConnections
from gnue.common.utils.uuid import UUID
+bestone = None
+
class StateChangeError (errors.SystemError):
- def __init__ (self, table, row):
- msg = u_("Changing state from 'commitable' to 'initialized' not allowed "
+ def __init__ (self, table, row, oldState):
+ msg = u_("Changing state from '%(oldstate)s' to 'initialized' not allowed "
"in table '%(table)s' row %(row)s") \
- % {'table': table, 'row': row}
+ % {'table': table, 'row': row, 'oldstate': repr (oldState)}
errors.SystemError.__init__ (self, msg)
class InvalidCacheError (errors.SystemError):
@@ -53,6 +56,13 @@
msg = u_("Data contains circular references")
errors.ApplicationError.__init__ (self, msg)
+class DuplicateRecordError (errors.SystemError):
+ def __init__ (self, table, row):
+ msg = u_("Duplicate instance '%(row)s' in class '%(table)s'") \
+ % {'table': table, 'row': row}
+ errors.SystemError.__init__ (self, msg)
+
+
# =============================================================================
# Cache class
# =============================================================================
@@ -77,302 +87,453 @@
# ---------------------------------------------------------------------------
def __init__ (self):
+
self.__old = {} # Original data
self.__new = {} # Changed (dirty) data
self.__state = {} # State of the data
+ # Atomic cache dictionaries
+ self.__undo = {}
+ self.__backup = {}
+
+ # Transaction wide cache dictionaries
+ self.inserted = {}
+ self.deleted = {}
+
+
# ---------------------------------------------------------------------------
# Store data in the cache
# ---------------------------------------------------------------------------
def write (self, table, row, field, value, dirty):
"""
- Write data to the cache. If "dirty" is false (0), the cache takes the given
- value as the original value for the field. If "dirty" is true (1), the
- value is taken as the modified value for the field, and the original value
- is remembered (if it was set before).
+ Write data to the clean or the dirty cache. If data is written to the dirty
+ cache and no atomic operation is started already, this function will start
+ a new atomic operation.
- It is possible to set a dirty value without having set an original value
- before.
+ A write to the clean cache will be dropped silently if the given field is
+ already available in a 'clean version'. This provides transaction integrity
+ for backends without transaction support. If a clean write succeeds, the
+ state of the given record will be set to 'clean'
+
+ A write to the dirty cache will change the record-state to 'commitable', if
+ no state information was available before or the state was 'clean'.
+
+ @param table: name of the table
+ @param row: id of the row
+ @param field: name of the field to write data for
+ @param value: the value of the field
+ @param dirty: if True, write operation modifies the dirty cache, otherwise
+ it will change the clean cache.
"""
+
checktype (table, UnicodeType)
checktype (row, UnicodeType)
checktype (field, UnicodeType)
- if dirty:
- tables = self.__new
- else:
- tables = self.__old
+ key = "%s-%s" % (table, row)
- rows = tables.setdefault (table, {})
- fields = rows.setdefault (row, {})
+ # If we add 'clean' data there is no need to maintain an undo sequence
+ if not dirty:
+ crow = self.__old.setdefault (table, {}).setdefault (row, {})
+ # But be aware not to overwrite an already cached clean value. This is
+ # necessary to keep transaction integrity for backends without
+ # transaction support.
+ if not crow.has_key (field):
+ crow [field] = value
+ state = self.__state.setdefault (key, 'clean')
- fields [field] = value
+ else:
+ # If there is no backup of the record right now, we have to create one
+ # before applying any changes.
+ if not self.__backup.has_key (key):
+ rec = self.__new.get (table) and self.__new [table].get (row)
+ self.__backup [key] = (self.__state.get (key), rec and rec.items ())
- # Update state information of the row
- self.touch (table, row, dirty)
+ self.__new.setdefault (table, {}).setdefault (row, {}) [field] = value
+ # After modifying a record update it's state information. If a record is
+ # already initialized or it was a clean record, this modification made it
+ # 'commitable'.
+ state = self.__state.setdefault (key, 'clean')
+ if state in ['initialized', 'clean']:
+ self.__state [key] = 'commitable'
# ---------------------------------------------------------------------------
- # Return whether a certain value is stored in the clean/dirty cache
+ # Read data from the cache
# ---------------------------------------------------------------------------
- def __has (self, table, row, field, dirty):
+ def read (self, table, row, field, dirty = True):
+ """
+ Read data from the cache. If a dirty value is requested, this function
+ returns the current version of the field, no matter if it's dirty or not.
- if dirty:
+ @param table: name of the table
+ @param row: id of the row
+ @param field: name of the field
+ @param dirty: if True, return the current version of the field, otherwise
+ return only the clean value of the field.
+
+ @raises KeyError: if no value for the given field is available at all
+
+ @return: The current or clean value of the given field
+ """
+
+ checktype (table, UnicodeType)
+ checktype (row, UnicodeType)
+ checktype (field, UnicodeType)
+
+ # Make sure to use the proper dictionary; use the dirty tables only if a
+ # dirty value is available, otherwise always use the clean tables.
+ if dirty and self.__has (table, row, field, True):
tables = self.__new
else:
tables = self.__old
- # We concatenate all has_key () checks so it can return as early as
- # possible. The function is about 3 times faster than using normal if's
- return tables.has_key (table) and tables [table].has_key (row) and \
- tables [table][row].has_key (field)
+ return tables [table] [row] [field]
# ---------------------------------------------------------------------------
- # Return whether a certain value is stored in the cache or not
+ # Return whether a certain value is stored in the clean/dirty cache
# ---------------------------------------------------------------------------
- def has (self, table, row, field, dirty = None):
+ def __has (self, table, row, field, dirty):
"""
- Return true (1) if the given item is stored in the cache (either in a clean
- or in a dirty version). Return false (0) if it isn't.
+ Check wether a given table/row has a clean or dirty version of a given
+ field.
+
+ @param table: the table to be checked
+ @param row: id of the row to be checked
+ @param field: the name of the field to be checked
+ @param dirty: True if the dirty version should be checked, False otherwise
+
+ @return: True if a value for field is available, False otherwise
"""
- checktype (table, UnicodeType)
- checktype (row, UnicodeType)
- checktype (field, UnicodeType)
- if dirty is None:
- # We could use self.__has () here, but avoiding an additional function
- # call here is a big deal regarding performance, as this function is
- # called very often
- return (self.__old.has_key (table) and self.__old [table].has_key (row) \
- and self.__old [table][row].has_key (field)) or \
- (self.__new.has_key (table) and self.__new [table].has_key (row) \
- and self.__new [table][row].has_key (field))
+ try:
+ (self.__old, self.__new) [dirty] [table] [row] [field]
+
+ except KeyError:
+ return False
+
else:
- if dirty:
- tables = self.__new
- else:
- tables = self.__old
- return tables.has_key (table) and tables [table].has_key (row) and \
- tables [table][row].has_key (field)
+ return True
# ---------------------------------------------------------------------------
- # Read data from the cache
+ # Return whether a certain value is stored in the cache or not
# ---------------------------------------------------------------------------
- def read (self, table, row, field, dirty = True):
+ def has (self, table, row, field, dirty = None):
"""
- Read data from the cache. Depending on the dirty-flag set this function
- returns the current version, no matter if it's dirty or not. If the
- dirty-flag is False only the field's old value will be returned.
+ Check wether a given table/row has a clean or dirty version of a given
+ field.
- If the given item isn't available, an exception is raised.
+ @param table: the table to be checked
+ @param row: id of the row to be checked
+ @param field: the name of the field to be checked
+ @param dirty: Could be True, False or None with the following implication:
+ True : search for a 'dirty' version
+ False: search for a 'clean' version
+ None : search for a 'clean' or a 'dirty' version
+
+ @return: True if a value for field is available, False otherwise
"""
+
checktype (table, UnicodeType)
checktype (row, UnicodeType)
checktype (field, UnicodeType)
- if dirty and self.__has (table, row, field, 1):
- tables = self.__new # Data is in dirty cache
+ try:
+ # If we do not search for a given kind (clean/dirty) try both
+ if dirty is None:
+ try:
+ self.__old [table] [row] [field]
+
+ except KeyError:
+ self.__new [table] [row] [field]
+
+ elif dirty:
+ self.__new [table] [row] [field]
+
+ else:
+ self.__old [table] [row] [field]
+
+ except KeyError:
+ return False
+
else:
- tables = self.__old # Data isn't in dirty cache, so search
- # in clean cache
- rows = tables [table]
- fields = rows [row]
- return fields [field]
+ return True
# ---------------------------------------------------------------------------
- # Get the status of a record
+ # Update the state information of a given row
# ---------------------------------------------------------------------------
- def status (self, table, row):
+ def initialized (self, table, row):
"""
- Returns the status of the given row. Returns one of the following results:
+ Finish initialization of a given record (table/row). The next call of the
+ write () method will change the state to 'commitable'.
- 'initializing': newly created record with initialization not yet finished
+ @param table: name of table
+ @param row: name of the row
- 'initialized': newly created and initialized records with no modifications
+ @raises StateChangeError: if the former state is not 'initializing'.
+ """
- 'inserted': newly created record with modifications
-
- 'changed': existing record with modifications
-
- 'deleted': deleted record
-
- For this function to work, an original value for the 'gnue_id' field must
- be available for any record except for newly created ones, and setting
- 'gnue_id' to None means deleting the record.
- """
checktype (table, UnicodeType)
checktype (row, UnicodeType)
- if not self.__has (table, row, u'gnue_id', 0):
- return '' # row is not in cache at all
+ key = "%s-%s" % (table, row)
- old_id = self.__old [table] [row] [u'gnue_id']
+ # Calling initialized () is allowed only if the former state was
+ # 'initializing'. Otherwise it must be a bug !
+ state = self.__state.get (key)
+ if state != 'initializing':
+ raise StateChangeError, (table, row, state)
- if self.__has (table, row, u'gnue_id', 1):
- new_id = self.__new [table] [row] [u'gnue_id']
- else:
- new_id = old_id
+ self.__state [key] = 'initialized'
- if old_id is None:
- if new_id is None:
- return '' # row was inserted and deleted
- else:
- rowState = self.__getState (table, row)
- if rowState == 'commitable':
- return 'inserted'
- else:
- return rowState
- else:
- if new_id is None:
- return 'deleted'
- else:
- if self.__new.has_key (table):
- rows = self.__new [table]
- if rows.has_key (row):
- return 'changed'
- return '' # row has no dirty fields
# ---------------------------------------------------------------------------
- # List all tables with dirty records
+ # Insert a new record
# ---------------------------------------------------------------------------
- def dirtyTables (self):
+ def insertRecord (self, table, row):
"""
- Returns a dictionary of tables with dirty data (inserted, changed or
- deleted rows), where the key is the table name and the value is a
- dictionary of all dirty rows in the table, where the key is the row id and
- the value is a dictionary of all dirty fields in that row, where the key is
- the field name and the value is the current value of the field. Got it?
+ Insert a new record to the cache. This adds the field 'gnue_id' to the
+ dirty cache and sets the record-state to 'initializing'. Make sure to call
+ 'initialized ()' in order to make a record 'commitable' by further writes.
+
+ @param table: name of the table
+ @param row: id of the new record
+
+ @raises DuplicateRecordError: If a record (table, row) should be inserted
+ for which we have state information alredy!
"""
- return self.__new
+ key = "%s-%s" % (table, row)
+ # There must not be state information for the new record yet!
+ if key in self.__state:
+ raise DuplicateRecordError, (table, row)
+
+ # A new record always starts with a state of 'initializing', which means
+ # one has to call the initialized () method in order to let further changes
+ # bring the record into a 'commitable' state.
+ self.__state [key] = 'initializing'
+
+ # Create a backup of (None, None) for the record and initialize state. This
+ # also checks the type of table and row arguments.
+ self.write (table, row, u'gnue_id', row, True)
+
+ # Mark this record as 'inserted' and add it to the undo-sequence
+ self.inserted [key] = (table, row)
+ self.__undo [key] = 'insert'
+
+
# ---------------------------------------------------------------------------
- # Clear the whole cache
+ # Delete a record from the cache
# ---------------------------------------------------------------------------
- def clear (self, oldOnly = False):
+ def deleteRecord (self, table, row):
"""
- Forget all data in the cache, original values as well as dirty values.
+ Delete a given record from the cache. The state of the given record changes
+ to 'deleted' and the record will be added to the dirty tables.
+
+ @param table: name of the table
+ @param row: id of the row
"""
- if oldOnly:
- # on a commit we need to remove all 'commited' stuff from cache, in order
- # to get new data from other transactions in.
- for table in self.__old.keys ():
- for row in self.__old [table].keys ():
- # state of an 'old' row is empty if it has been deleted or if it's
- # really clean.
- if self.status (table, row) == '':
- del self.__old [table][row]
+ key = "%s-%s" % (table, row)
+ if not self.__backup.has_key (key):
+ rec = self.__new.get (table) and self.__new [table].get (row)
+ self.__backup [key] = (self.__state.get (key), rec and rec.items ())
- # if a table has no more rows, remove it too
- if not self.__old [table].keys ():
- del self.__old [table]
+ # If the record is added within the same transaction, drop it from the
+ # insertion queue, but make sure to keep a note regarding it's state within
+ # the atomic operation.
+ if self.inserted.has_key (key):
+ # If the record has been created within the same atomic operation, it's
+ # safe to remove it from the insertion queue. This is because a cancel
+ # does not restore that inserted record. If the record has been created
+ # by another atomic operation, we have to re-add it to the inserted list
+ # on canceling this deletion.
+ if not self.__undo.has_key (key):
+ self.__undo [key] = 'remove'
+
+ del self.inserted [key]
+ # The record is not added within the same transaction, so just add it to
+ # the deletion queue
else:
- self.__old = {}
- self.__new = {}
- self.__state = {}
+ self.deleted [key] = (table, row)
+ self.__undo [key] = 'delete'
+ # Finally remove the current 'gnue_id' and set the record state to
+ # 'deleted'. The former will add this record to the dirty tables, if it was
+ # clean before.
+ self.write (table, row, u'gnue_id', None, True)
+ self.__state [key] = 'deleted'
+
# ---------------------------------------------------------------------------
- # Update the state information of a given row
+ # Confirm changes
# ---------------------------------------------------------------------------
- def initialized (self, table, row):
+ def confirm (self):
"""
- This function sets a row of a table to 'initialized'. This means the next
- write () to this table makes this row 'commitable'
+ Close the current atomic operation by confirming all it's changes.
"""
- checktype (table, UnicodeType)
- checktype (row, UnicodeType)
+ self.__undo.clear ()
+ self.__backup.clear ()
- cState = self.__getState (table, row)
- if cState is not None and cState == 'commitable':
- raise StateChangeError, (table, row)
- self.__setState (table, row, 'initialized')
-
-
# ---------------------------------------------------------------------------
- # Create state information for a given table/row
+ # Cancel atomic operation
# ---------------------------------------------------------------------------
- def __setState (self, table, row, state):
+ def cancel (self):
"""
+ Cancel the current atomic operation by revoking all it's changes.
"""
- checktype (table, UnicodeType)
- checktype (row, UnicodeType)
+ while self.__backup:
+ (key, (state, items)) = self.__backup.popitem ()
- if not self.__state.has_key (table):
- self.__state [table] = {}
+ # Restore state information
+ if state is not None:
+ self.__state [key] = state
- rows = self.__state [table]
- rows [row] = state
+ elif self.__state.has_key (key):
+ del self.__state [key]
+ # Restore current (dirty) record data
+ table, row = key.split ('-')
+ if items is not None:
+ rec = self.__new.setdefault (table, {}).setdefault (row, {})
+ # Remove all fields from the row-dictionary and add all previously
+ # stored (field, value) tuples
+ rec.clear ()
+ for (field, value) in items:
+ rec [field] = value
+ elif items is None:
+ # No changed data was available before, so remove it from the cache
+ self.__removeFromDict (self.__new, table, row)
+
+ # If we have an entry in the current atomic cache, do further processing
+ if self.__undo.has_key (key):
+ action = self.__undo.pop (key)
+
+ try:
+ # Remove the record from the insertion queue
+ if action == 'insert':
+ del self.inserted [key]
+
+ # Re-Add the previously deleted record to the insertion queue
+ elif action == 'remove':
+ self.inserted [key] = (table, row)
+
+ # Remove the record from the deletion queue
+ elif action == 'delete':
+ del self.deleted [key]
+
+ except KeyError:
+ pass
+
+ assert not self.__undo, "atomic operation cache *not* empty"
+ assert not self.__backup, "atomic record cache *not* empty"
+
+
# ---------------------------------------------------------------------------
- # Return the current state of a row
+ # Get the state of a record
# ---------------------------------------------------------------------------
- def __getState (self, table, row):
+ def state (self, table, row):
"""
- This function returns the current state of a row
- @param table: name of the table to get state information for
- @param row: gnue-id of the row to get state information for
+ Returns the state of the given record. Returns one of the following
results:
- @return: state information for the requested row
+ 'initializing': newly created record with initialization not yet finished
+ 'initialized': newly created and initialized records with no
modifications
+ 'inserted': newly created record with modifications
+ 'changed': existing record with modifications
+ 'deleted': deleted record
+ 'clean': record is available and has no modifications
+
+ @return: current state of the given (table, row) tuple in the cache
"""
checktype (table, UnicodeType)
checktype (row, UnicodeType)
- if self.__state.has_key (table) and self.__state [table].has_key (row):
- return self.__state [table] [row]
+ key = "%s-%s" % (table, row)
+ state = self.__state.get (key, 'clean')
+ if self.inserted.has_key (key):
+ return (state, 'inserted') [state == 'commitable']
+
+ elif self.deleted.has_key (key):
+ return 'deleted'
+
else:
- return None
+ return (state, 'changed') [state == 'commitable']
# ---------------------------------------------------------------------------
- # Touch a record in the cache
+ # List all tables with dirty records
# ---------------------------------------------------------------------------
- def touch (self, table, row, dirty = True):
+ def dirtyTables (self):
"""
- This function touches a record in the cache. If has no state information it
- will be stet to 'initializing' and if the dirty flag is set and state is
- already 'initialized' the record get's 'commitable'
+ Returns a dictionary of tables with dirty data (inserted, changed or
+ deleted rows), where the key is the table name and the value is a
+ dictionary of all dirty rows in the table, where the key is the row id and
+ the value is a dictionary of all dirty fields in that row, where the key is
+ the field name and the value is the current value of the field. Got it?
+ """
- @param table: name of the table to be touched
- @param row: gnue_id of the row to be touched
- @param dirty: boolean flag to state wether to make the record dirty or not
+ return self.__new
+
+
+ # ---------------------------------------------------------------------------
+ # Clear the whole cache
+ # ---------------------------------------------------------------------------
+
+ def clear (self, oldOnly = False):
"""
+ Clear cached data, either all data or only the clean portion.
- # Update state information of the row
- state = self.__getState (table, row)
+ @param oldOnly: if True, only the clean portion of the cache will be
+ cleaned. This will be used to implement dirty reads.
+ """
- if state is None:
- state = 'initializing'
- self.__setState (table, row, state)
+ if oldOnly:
+ # on a commit we need to remove all 'commited' stuff from cache, in order
+ # to get new data from other transactions in.
+ for table in self.__old.keys ():
+ for row in self.__old [table].keys ():
+ # remove clean or deleted rows
+ if self.state (table, row) in ['clean', 'deleted']:
+ del self.__old [table][row]
+ key = "%s-%s" % (table, row)
+ if self.__state.has_key (key):
+ del self.__state [key]
- if dirty and state == 'initialized':
- self.__setState (table, row, 'commitable')
+ # if a table has no more rows, remove it too
+ if not self.__old [table]:
+ del self.__old [table]
+ else:
+ self.__old.clear ()
+ self.__new.clear ()
+ self.__state.clear ()
+
+
# ---------------------------------------------------------------------------
# Make the given row in a table to be treated as 'clean'
# ---------------------------------------------------------------------------
@@ -386,16 +547,23 @@
@param row: gnue_id of the row to be moved
"""
+ key = "%s-%s" % (table, row)
+
if self.__new.has_key (table) and self.__new [table].has_key (row):
- if not self.__old.has_key (table):
- self.__old [table] = {}
+ self.__old.setdefault (table, {}) [row] = self.__new [table] [row]
- self.__old [table] [row] = self.__new [table] [row]
-
self.__removeFromDict (self.__new, table, row)
- self.__removeFromDict (self.__state, table, row)
+ if self.__state.has_key (key):
+ del self.__state [key]
+ if key in self.inserted:
+ del self.inserted [key]
+
+ elif key in self.deleted:
+ del self.deleted [key]
+
+
# ---------------------------------------------------------------------------
# Remove a row of a table completely from the cache
# ---------------------------------------------------------------------------
@@ -406,14 +574,21 @@
no matter wether it's dirty or not.
@param table: name of the table
- @param row: gnue_id of the row to be removed from the cache
+ @param row: id of the row to be removed from the cache
"""
self.__removeFromDict (self.__new, table, row)
self.__removeFromDict (self.__old, table, row)
- self.__removeFromDict (self.__state, table, row)
+ key = "%s-%s" % (table, row)
+ if key in self.__state:
+ del self.__state [key]
+
+ if key in self.deleted:
+ del self.deleted [key]
+
+
# ---------------------------------------------------------------------------
# Remove a row of a table from a given cache-dictionary
# ---------------------------------------------------------------------------
@@ -426,34 +601,48 @@
@param dictionary: cache-dictionary: dict [table][row][field]
@param table: name of the table to remove a row from
- @param row: gnue_id of the row to be removed
+ @param row: id of the row to be removed
"""
- if dictionary.has_key (table) and dictionary [table].has_key (row):
- del dictionary [table] [row]
+ try:
+ if row in dictionary [table]:
+ del dictionary [table] [row]
- if dictionary.has_key (table) and not len (dictionary [table].keys ()):
- del dictionary [table]
+ if not dictionary [table]:
+ del dictionary [table]
+ except KeyError:
+ pass
+
# ---------------------------------------------------------------------------
- # Have a look if the cache is really in a clean state
+ # Get a sequence of fields not yet kept in any cache
# ---------------------------------------------------------------------------
- def _assertClean (self):
+ def uncachedFields (self, table, row, fields):
"""
- This function iterates over all 'new' records in the cache and verifies if
- they're in a clean state.
+ Return a sequence of fields which are not already cached in the clean or
+ dirty cache.
+
+ @param table: name of the table
+ @param row: id of the row
+ @param fields: sequence of fields to be checked
+
+ @return: subset from fields, which do not have a clean or dirty value
+ cached at the moment
"""
- for table in self.__new.keys ():
- for row in self.__new [table].keys ():
- if not self.status (table, row) in ["initialized"]:
- raise InvalidCacheError, (table, row, self.status (table, row))
+ result = []
+ append = result.append
-
+ for item in fields:
+ if not self.has (table, row, item):
+ append (item)
+ return result
+
+
# =============================================================================
# Helper methods
# =============================================================================
@@ -564,12 +753,7 @@
self.__connections = connections
self.__database = database
self.__cache = _cache ()
- self.__inserted = []
- self.__deleted = []
- self.__confirmedCache = None
- self.__confirmedInserts = []
- self.__confirmedDeletes = []
self.__constraints = {}
self.__uuidType = gConfig ('uuidtype').lower ()
@@ -634,6 +818,11 @@
checktype (fields, ListType)
for fields_element in fields: checktype (fields_element, UnicodeType)
+ checktype (order, [NoneType, ListType])
+ if order:
+ for order_element in order:
+ checktype (order_element, DictType)
+
return recordset (self.__cache, self.__connections, self.__database,
content, GConditions.buildCondition (conditions), order)
@@ -648,20 +837,19 @@
Table must be a unicode string.
"""
+
checktype (table, UnicodeType)
if self.__uuidType == 'time':
- id = UUID.generateTimeBased ()
+ row = UUID.generateTimeBased ()
else:
- id = UUID.generateRandom ()
+ row = UUID.generateRandom ()
- r = record (self.__cache, self.__connections, self.__database, table, id)
- self.__cache.write (table, id, u'gnue_id', None, 0) # old id is None
- self.__cache.write (table, id, u'gnue_id', id, 1) # new id
-
- self.__inserted.append ((table, id))
+ self.__cache.insertRecord (table, row)
+ r = record (self.__cache, self.__connections, self.__database, table, row)
return r
+
# ---------------------------------------------------------------------------
# Delete a record
# ---------------------------------------------------------------------------
@@ -674,14 +862,13 @@
Table and row must be unicode strings.
"""
+
checktype (table, UnicodeType)
checktype (row, UnicodeType)
- if not self.__cache.has (table, row, u'gnue_id'): # not yet in cache
- self.__cache.write (table, row, u'gnue_id', row, 0)
- self.__cache.write (table, row, u'gnue_id', None, 1)
- self.__deleted.append ((table, row))
+ self.__cache.deleteRecord (table, row)
+
# ---------------------------------------------------------------------------
# Find a record
# ---------------------------------------------------------------------------
@@ -703,19 +890,22 @@
for fields_element in fields:
checktype (fields_element, UnicodeType)
- uncachedFields = []
- for field in fields:
- if not self.__cache.has (table, row, field):
- uncachedFields.append(field)
+ # Let's have a look wether we need a requery or not. Newly inserted records
+ # cannot have uncached fields since they are new :)
+ state = self.__cache.state (table, row)
+ if state in ['initializing', 'initialized', 'inserted']:
+ uncachedFields = []
+ else:
+ uncachedFields = self.__cache.uncachedFields (table, row, fields)
- if uncachedFields == [] or \
- self.__cache.status (table, row) in ['initializing', 'initialized',
- 'inserted']:
- # already cached, no need to load from database
+ # If there are no uncached fields there is no need to reload from the
+ # backend.
+ if not uncachedFields:
r = record (self.__cache, self.__connections, self.__database, table,
row)
- r._cache (u'gnue_id', row)
+ self.__cache.write (table, row, u'gnue_id', row, dirty = False)
+
+ # otherwise requery the current record for all uncached fields
else:
- # not yet cached, need to load from database
new = self.__backend ().requery (table, {u'gnue_id': row},
uncachedFields)
r = record (self.__cache, self.__connections, self.__database, table,
row)
r._fill (None, uncachedFields, new)
@@ -757,17 +947,12 @@
recNo = 0
# first perform all inserts
- if self.__inserted:
+ if self.__cache.inserted:
for (table, row) in self.__orderInserts ():
fields = tables [table] [row]
recNo += 1
backend.insert (table, fields, recNo)
- self.__inserted.remove ((table, row))
-
- if (table, row) in self.__confirmedInserts:
- self.__confirmedInserts.remove ((table, row))
-
self.__cache.makeClean (table, row)
@@ -775,26 +960,19 @@
for (table, rows) in tables.items ():
for (row, fields) in rows.items ():
recNo += 1
- status = self.__cache.status (table, row)
-
- if status == 'changed':
+ if self.__cache.state (table, row) == 'changed':
backend.update (table, {'gnue_id': row}, fields, recNo)
self.__cache.makeClean (table, row)
+
# perform all deletes
- if len (self.__deleted):
+ if len (self.__cache.deleted):
for (table, row) in self.__orderDeletes ():
recNo += 1
backend.delete (table, {'gnue_id': row}, recNo)
self.__cache.remove (table, row)
- self.__deleted = []
- self.__confirmedDeletes = []
-
- # Assert
- self.__cache._assertClean ()
-
# Commit the whole transaction
self.__connections.commitAll ()
@@ -815,8 +993,8 @@
@return: sequence of (table, row) tuples in a sane order for insertion
"""
- records = [(table, row) for (table, row) in self.__inserted \
- if self.__cache.status (table, row) == 'inserted']
+ records = [(table, row) for (table, row) in self.__cache.inserted.values
()\
+ if self.__cache.state (table, row) == 'inserted']
result = self.__orderByDependency (records)
gDebug (1, "Ordered inserts: %s" % result)
@@ -835,7 +1013,7 @@
@return: sequence of (table, row) tuples in a sane order for deletion
"""
- order = self.__orderByDependency (self.__deleted)
+ order = self.__orderByDependency (self.__cache.deleted.values ())
# since we do deletes we need a reversed order
order.reverse ()
@@ -924,11 +1102,6 @@
Undo all uncommitted changes.
"""
- self.__inserted = []
- self.__deleted = []
- self.__confirmedInserts = []
- self.__confirmedDeletes = []
-
# Send the rollback to the database. Although we have (most probably) not
# written anything yet, we have to tell the database that a new transaction
# starts now, so that commits from other sessions become valid now for us
@@ -938,8 +1111,8 @@
# The transaction has ended. Changes from other transactions could become
# valid in this moment, so we have to clear the whole cache.
self.__cache.clear ()
- self.__confirmedCache = None
+
# ---------------------------------------------------------------------------
# Close the connection
# ---------------------------------------------------------------------------
@@ -962,12 +1135,7 @@
cancelChanges () function restores to this state.
"""
- # Doing a deepcopy here is a 'no-go' for performance. If we're creating
- # larger transaction (with a lot of records) this call will slow down
- # everything with every record added.
- self.__confirmedCache = copy.deepcopy (self.__cache)
- self.__confirmedInserts = self.__inserted [:]
- self.__confirmedDeletes = self.__deleted [:]
+ self.__cache.confirm ()
@@ -980,15 +1148,9 @@
This function revokes all changes up to the last call of the
confirmChanges () function.
"""
- if self.__confirmedCache is not None:
- self.__cache = copy.deepcopy (self.__confirmedCache)
- else:
- self.__cache.clear ()
+ self.__cache.cancel ()
- self.__inserted = self.__confirmedInserts [:]
- self.__deleted = self.__confirmedDeletes [:]
-
# =============================================================================
# Recordset class
# =============================================================================
@@ -1035,7 +1197,7 @@
"""
return self.__resultSet.getRecordCount () + \
- len (self.__add) + self.__added - len (self.__remove.keys ())
+ len (self.__add) + self.__added - len (self.__remove)
# ---------------------------------------------------------------------------
@@ -1057,7 +1219,7 @@
return rec
else:
- if not len (self.__order):
+ if not self.__order:
row = self.__add [0]
self.__add.remove (row)
self.__added += 1
@@ -1221,7 +1383,7 @@
# Iterate over all dirty rows of the master table and stick them into the
# apropriate filter
for (row, fields) in tables [master].items ():
- state = self.__cache.status (master, row)
+ state = self.__cache.state (master, row)
if state == 'inserted':
# an inserted (new) row must match the current condition
@@ -1248,7 +1410,7 @@
# If we have a sort order defined, we need to sort all records listed for
# addition
- if len (order) and len (self.__add):
+ if order and len (self.__add):
self.__sortAddition (order)
@@ -1485,6 +1647,8 @@
"""
result = []
+ append = result.append
+
for element in order:
field = element ['name']
direction = element.get ('descending') or False
@@ -1494,7 +1658,7 @@
if ignorecase and hasattr (value, 'lower'):
value = value.lower ()
- result.append ((value, direction))
+ append ((value, direction))
return result
@@ -1515,7 +1679,17 @@
# ---------------------------------------------------------------------------
def __init__ (self, cache, connections, database, table, row):
+ """
+ Create a new wrapper for a given record in the cache
+ @param cache: the underlying cache instance
+ @param connections: GConnections instance which can return a handle to the
+ current backend connection
+ @param database: name of the backend connection to be used
+ @param table: name of the table this record instance is a row of
+ @param row: id of the row represented by this record instance
+ """
+
self.__cache = cache
self.__connections = connections
self.__database = database
@@ -1529,6 +1703,10 @@
# ---------------------------------------------------------------------------
def getRow (self):
+ """
+ Return the current row-id of this record
+ @return: row id of this record instance
+ """
return self.__row
@@ -1538,24 +1716,36 @@
# ---------------------------------------------------------------------------
def _cache (self, field, value):
+ """
+ Write a field to the clean cache, but do not replace already cached values
- if not self.__cache.has (self.__table, self.__row, field):
- self.__cache.write (self.__table, self.__row, field, value, 0)
+ @param field: name of the field
+ @param value: value of the field
+ """
+ self.__cache.write (self.__table, self.__row, field, value, False)
+
+
# ---------------------------------------------------------------------------
# Fill the cache for this record with data from a (gnue-common) RecordSet
# ---------------------------------------------------------------------------
def _fill (self, alias, fields, RecordSet):
+ """
+ Write the given fields from the given RecordSet object to the clean cache,
+ but do not replace already cached values.
+ @param alias: alias used for fields in the RecordSet, i.e. 't0', 't1', ...
+ @param fields: sequence of fieldnames to cache from the RecordSet
+ @param RecordSet: dictionary like object to retrieve the data from
+ """
+
for field in fields:
- # Never ever override the cache with data from the backend
- if alias:
- fn = alias + '.' + field
- else:
- fn = field
- self._cache (field, RecordSet [fn])
+ f = alias and (alias + '.' + field) or field
+ self.__cache.write (self.__table, self.__row, field, RecordSet [f],
False)
+
+
# ---------------------------------------------------------------------------
# Get the value for a field
# ---------------------------------------------------------------------------
@@ -1568,23 +1758,31 @@
The field name must be given as a unicode string. The result will be
returned as the native Python datatype, in case of a string field it will
be Unicode.
+
+ @param field: name of the field to get a value for
+ @param original: if True return an original value (either from the clean
+ cache or freshly fetched from the backend). If False return a dirty or
+ clean value (in that order)
+
+ @return: current or original value of the given field
"""
+
checktype (field, UnicodeType)
- if original:
- if self.__cache.has (self.__table, self.__row, field, False):
- return self.__cache.read (self.__table, self.__row, field, False)
- else:
- if self.__cache.has (self.__table, self.__row, field):
- # If we find the field in the cache, use it
- return self.__cache.read (self.__table, self.__row, field)
+ # If an original value is requested, set the dirty-flag to False, otherwise
+ # leave it unspecified (=None), which means use a dirty or clean value in
+ # that order.
+ dirty = (None, False) [original]
+ if self.__cache.has (self.__table, self.__row, field, dirty):
+ return self.__cache.read (self.__table, self.__row, field, dirty is None)
- # if state is one of 'in*' the record is new and cannot exist in the
- # backend.
- if self.status () in ['initializing', 'initialized', 'inserted']:
+ # if the record is newly inserted, it cannot have values in the backend
+ # until now. So there's no need to fetch anything.
+ if self.state () in ['initializing', 'initialized', 'inserted']:
return None
- # Not found in cache, so get it from the db
+ # If we have no backend reference until now, create a weak reference to the
+ # connection instance and make sure we're logged in.
if self.__backend is None:
cons = self.__connections
self.__backend = weakref.ref (cons.getConnection (self.__database))
@@ -1594,12 +1792,11 @@
[field])
value = new.get (field)
if new:
- self.__cache.write (self.__table, self.__row, field, value, 0)
+ self.__cache.write (self.__table, self.__row, field, value, False)
return value
-
# ---------------------------------------------------------------------------
# Put the value for a field
# ---------------------------------------------------------------------------
@@ -1610,10 +1807,14 @@
The field name must be given as a unicode string, value must be the native
Python datatype of the field, in case of a string field it must be Unicode.
+
+ @param field: name of the field (as unicode string)
+ @param value: value to be set for the given field (native Python datatype)
"""
+
checktype (field, UnicodeType)
- self.__cache.write (self.__table, self.__row, field, value, 1)
+ self.__cache.write (self.__table, self.__row, field, value, True)
# ---------------------------------------------------------------------------
@@ -1624,6 +1825,9 @@
"""
This function marks this record as 'initialized' so following changes will
make it commitable.
+
+ @raises StateChangeError: if this function is called for a record having
+ another state than 'initializing'.
"""
self.__cache.initialized (self.__table, self.__row)
@@ -1633,25 +1837,23 @@
# Get the state of the current record
# ---------------------------------------------------------------------------
- def status (self):
+ def state (self):
"""
- This function is a pass-through to the cached record's state
- """
+ Returns the state of the given record. Returns one of the following
results:
- return self.__cache.status (self.__table, self.__row)
+ 'initializing': newly created record with initialization not yet finished
+ 'initialized': newly created and initialized records with no
modifications
+ 'inserted': newly created record with modifications
+ 'changed': existing record with modifications
+ 'deleted': deleted record
+ 'clean': record is available and has no modifications
+ @return: current state of the record
+ """
- # ---------------------------------------------------------------------------
- # Touch a record in the cache
- # ---------------------------------------------------------------------------
+ return self.__cache.state (self.__table, self.__row)
- def touch (self, dirty = True):
- """
- This function touches the underlying record in the cache.
- """
- self.__cache.touch (self.__table, self.__row, dirty)
-
# ---------------------------------------------------------------------------
# Give an official string represenation of the record instance
# ---------------------------------------------------------------------------
Modified: trunk/gnue-appserver/src/geasInstance.py
===================================================================
--- trunk/gnue-appserver/src/geasInstance.py 2005-04-06 23:02:54 UTC (rev
7307)
+++ trunk/gnue-appserver/src/geasInstance.py 2005-04-07 09:31:56 UTC (rev
7308)
@@ -281,7 +281,7 @@
# Do not call OnChange triggers while in OnInit code and when setting
# time/user stamp fields
- if regular and self.status () != 'initializing':
+ if regular and self.state () != 'initializing':
for trigger in self.__classdef.OnChange:
self.call (trigger, {}, {'oldValue': self.__getValue (propertyname),
'newValue': __value,
@@ -431,43 +431,23 @@
# Get the state of the instances record in the cache
# ---------------------------------------------------------------------------
- def status (self):
+ def state (self):
"""
This function returns the current state of the instance.
@return: state of the instance:
-
- 'initializing': new instance, OnInit still running
-
- 'initialized': new instance, OnInit finished, but no other modifications
-
- 'inserted': new instance, already modified
-
- 'changed': existing instance with modifications
-
- 'deleted': deleted instance
+ 'initializing': new instance, OnInit still running
+ 'initialized': new instance, OnInit finished, but no other modifications
+ 'inserted': new instance, already modified
+ 'changed': existing instance with modifications
+ 'deleted': deleted instance
+ 'clean': existing instance without any modifications
"""
- return self.__record.status ()
+ return self.__record.state ()
# ---------------------------------------------------------------------------
- # Touch the record in the cache
- # ---------------------------------------------------------------------------
-
- def touch (self, dirty = True):
- """
- This function touches the encapsulated record in the cache. If the record
- has been initialized already it becomes 'commitable' this way.
-
- @param dirty: if set to TRUE an 'initialized' record becomes 'commitable',
- otherwise it might change to 'initializing' only.
- """
-
- self.__record.touch (dirty)
-
-
- # ---------------------------------------------------------------------------
# Get the fully qualified classname of an instance
# ---------------------------------------------------------------------------
Modified: trunk/gnue-appserver/src/geasSession.py
===================================================================
--- trunk/gnue-appserver/src/geasSession.py 2005-04-06 23:02:54 UTC (rev
7307)
+++ trunk/gnue-appserver/src/geasSession.py 2005-04-07 09:31:56 UTC (rev
7308)
@@ -567,10 +567,6 @@
for object_id in obj_id_list:
if object_id:
instance = self.__findInstance (classdef, object_id, [])
- # make sure an 'initialized' record get's commitable, even if no
- # further access to the instance occurs
- instance.touch ()
-
new_object_id = object_id
else:
@@ -582,7 +578,7 @@
i += 1
result.append (new_object_id)
- if instance.status () in ['changed', 'inserted']:
+ if instance.state () in ['changed', 'inserted']:
instance.updateStamp ()
self.__dirtyInstances [new_object_id] = instance
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [gnue] r7308 - trunk/gnue-appserver/src,
johannes <=