dtl


Exception Safety in DTL

by Mike Gradman and Corwin Joy

(Adapted from Exception Safety in STLPort by Dave Abrahams)

Basic Library Guarantees

DTL makes the guarantee that no resources are leaked in the face of exceptions.

This means:

Additionally:

Basic Client Requirements

The library guarantees above are conditional on some requirements that library clients must fulfill.

The "Strong Guarantee"

In many programs, some objects will be destroyed automatically during exception-unwinding. For these, the basic guarantee that resources won't be leaked is good enough. If a program hopes to survive an exception and continue running, though, it probably also uses long-lived containers which are expected to survive past exception-recovery in a known state. For example, a program could maintain a list of objects representing tasks it is working on. If adding a task to that list fails, the program may still need to rely on the list. If the list must survive an exception intact, we need the strong guarantee:

You can get the strong guarantee by "brute force" for any container operation as follows, provided the container's swap() member function can't fail (this is true for most real-world containers):

container_type container_copy( original_container );
container_copy.mutating_operation(...);
original_container.swap( container_copy );

Fortunately, many mutating operations give the strong guarantee with no additional requirements on the client. To get the strong guarantee for others, you can either use the above technique or conform to some additional requirements. This guarantee is also known as the aCi guarantee, i.e. the operation is atomic - all or nothing, consistent, and effects are isolated. Details on this kind of ACID analysis can be found at "Guru of the Week #61".

 

Operations that give a "no-throw" guarantee

All swap() member functions and class destructors.

 

Operations that give the "strong guarantee" if the basic requirements have been met.

All, unless otherwise listed.

Special note for IndexedDBView<View> member functions. The strong guarantee here excludes call to fetch() -- most members call fetch() to initialize the object if it is not already built from the database which technically means the operation has a possibly non-atomic effect on the object. When we say these operations are atomic we exclude effects from the initial fetch().

 

Operations that give no guarantee.

 

Theory of Exception Handling for Input and Output Iterators

(Extracted from discussions in comp.c++.std. Many thanks to Dave Abrahams for his comments &
criticisms. Any errors here are my own - and doubtless come from ignoring his advice :-) Corwin).

Some definitions:
InputIterator and OutputIterator - by this I will mean the generic InputIterator and OutputIterator concepts in STL. Specific examples in DTL are DBView::select, insert, update and delete.


i/o Iterator - shorthand for "InputIterator or OutputIterator"

"Abraham's Strong Guarantee" or "aCi", by this I mean the ACID transactional guarantee as defined in "Guru of the Week #61",
http://www.peerdirect.com/resources/gotw061a.html, i.e. the operation is
a = atomic, if the operation fails the object will be in its initial state, otherwise in its final state
C = consistent, if op succeeds or fails the system will be in a 'Consistent' state with no integrity constraints violated, no memory leaked etc.
i = isolated, if operation fails, no side effects including any changes to the database.

Claim 1:  Currently the STL makes aCi guarantees for some of its operations. These aCi guarantees are only with respect to the STL object acted upon and make no promise about state of the iterators that they act upon.  My claim is that if we require iterators to be aCi over a range, then STL operations over that range can be "jointly aCi" in that both the effect on the STL object and the effect on the iterator can be made simultaneously aCi.

Claim 2: Often the "jointly aCi" guarantee over a range is not what you want for an algorithm.  Instead, if we assume the each individual operation to read or write to our i/o Iterator is aCi, then we may want a "maximal" property which says that we will try to execute the maximum possible number of successful atomic transactions against the given iterator.  In this case, without 'range atomicity' on the underlying iterator I think the best we can hope for is consistency at the end of the algorithm.

Definition: An iterator is aCi over a range if it displays:
a: atomicity - the iterator must support transaction semantics.  In other words the iterator must be able to mark the beginning on a transaction on a range, and have commands to commit and rollback changes posted to the range since the beginning of the transaction.
C: we say an iterator is consistent if all operations on that iterator are consistent over the range
i: we say that an iterator is isolated over a range if any changes made by that iterator are not visible outside of the iterator until committed.
Proof of Claim 1: if: if an iterator x, is aCi and an algorithm f, is aCi then we may obtain f(x) aCi via the following pseudo-code

try {
	f(x); // operation(s) to apply changes over a range
	commit x;
}
catch(...) { 
	// oops, we could not change the entire range, rollback any changes
	rollback x;
}

Example of Claim 1.
Suppose we define a custom class called foo which can throw on construction and try to initialize an array of foo via a call to
foo *p = new foo[5];
What happens if the third element constructed throws? The compiler will then internally destruct the first and second elements built before the throw. So, conceptually, what the compiler's operator new does is this:

operator new[] { // new an array of objects
  try {
   iterator i = array.begin();
   *i = newly constructed object;
   i++;
  }
  catch(...) { 
   // oops, we couldn't construct all objects, attempt to roll-back by invoking destructors
   while (--i != array.rend() ) 
      destruct(*i);
  }
}
   


Example of Claim 2.

Now consider what happens when we invoke operator delete[] over a range

foo *p = new foo[5]; delete[] p;

What happens if the third element destructed throws? According to the standard, the results are undefined. One major reason for this is that the compiler does not have an easy way to "roll-back" range destruction operations as it cannot easily decrement the iterator and re-construct elements similar to what is done for the new[] operator. In other words, the "delete iterator" used by operator delete[] cannot give "range atomicity" as defined above. This is one reason why the conventional exception safety wisdom is to suppress errors in destructors so that we can get a "maximal guarantee" for operator delete[]. I.e. every item that can be successfully destructed in the array gets destroyed.

Claim 2:  There are going to be times when your i/o Iterator either cannot provide an atomic range guarantee, or you don't want your algorithm to act in an atomic fashion over a range. Instead, if we assume the each individual operation to read or write to our i/o Iterator is aCi, then we may want a "maximal" property which says that the maximum possible number of successful atomic transactions will be executed against the given iterator.

Definition: We say that an i/o Iterator is element-wise aCi if reading or writing an individual element is aCi.  This means that for an OutputIterator, called out, *out = data, must be aCi.  For an InputIterator, called in, data=*in, must be aCi.

Definition: We say that an iterator has gone "bad" if it is no longer able to read from or write to its underlying data source.  Concrete examples would be
(1) An ostream_iterator to a file where the underlying ostream can no longer write to the file.
(2) A database input iterator, where the connection to the database is lost and it can no longer read records from the database table it is pointing at.
(3) A directory iterator, such as the one by Dietmar Kuehl at
http://www.boost.org/libs/dir_it/dir_it.html, where the connection to the underlying drive is lost and further filenames cannot be retrieved.

Definition: We say that an iterator read/write operation has "failed" if a particular data element cannot be read from or written to an iterator but the iterator has not gone "bad".  Usually such a read "failure" will be caused by that particular element not matching a type or business rule that is required for each individual data object.
Examples:
(1) A database iterator tries to write the following rows to a table whose only field is a date field: '01-Jan-1980', 'lala',  '01-Jan-1981'.  Here the second element would 'fail' because it is not a valid date that can be written to the table.
(2) An istream_iterator<int> tries to read the following comma delimited elements from a file: 1, 2, lala, 4.  The element 'lala' cannot be converted to an <int> and hence 'fails'.

Some iterators have a natural notion of atomic elements (rows in a table, filenames in a directory).  For others, if an element 'fails' the bad data can affect other elements e.g. an istream_iterator tries to read 1, 2, 345, 6 where 4 is a corrupted byte that has replaced the comma delimiter between 3 and 5 leading to the incorrectly read number '345'.  This lack of element atomicity is a property of the iterator being worked with and is independent of the 'fail' concept since an element can be 'successfully' read but still hold a bad value. To get the 'maximal' property we can do the following:

1. Impose a client requirement that the user defined 'data' type cannot throw on construction or assignment.

2. For InputIterators use the following logic for ++ (and for construction when reading the first element); On ++, try to read the next 'data' element.  If fail(), push the resulting 'data' element into an error buffer and try to get the next element e.g.

while (iterator != end-of-stream) {
      read information into internal 'data' object;
      if (not fail() and not bad() )
          break;
      if (bad()) {
          iterator = end-of-stream;
          try {
		error_buffer.push_back(error information string)
	  } catch(..){
		set_buffer_overflow_flag
	  }
      }
      else {
          try {
		error_buffer.push_back(data + error information error information string); 
	  } catch(..) {
		set_buffer_overflow_flag
	  }
      }
}

(Here we assume the dereference operator *in, simply returns the contents of the internal buffer -- yes, we could code the read to happen on *in, but then we may end up having to return an empty object if the data-stream goes 'bad' and we do not want to throw.  Here, setting end-of-stream for the bad() case lets us terminate the read in a no-throw() kind of way.).

3. For OutputIterators use the similar logic for ++. On ++, try to write the next 'data' element.  If fail() or bad(), push the resulting 'data' element into an error buffer similar to what is shown for InputIterator.

4. Set up the i/o Iterators so that they do not throw on any of their operations. Then, if we invoke an algorithm f(InputIterator, OutputIterator) we will have the property that every element that could be read from the InputIterator was either fetched completely or logged to an error buffer (up to error buffer overflow).  Similarly, every element that could be written to the OutputIterator was either written completely or logged to an error buffer.  Here, one could imagine a *standard* function for accessing such an error buffer so that errors could be dealt with in a generic way. Still, this is not a very strong guarantee since f(In, Out) could abort for reasons unrelated to the iterators it is working on.  In any case , I think the best we can hope for is consistency at the end of f(In, Out), which should be enforced by the item-wise aCi property of our iterator, regardless of whether we throw or not.

 

DTL Input and Output Iterators -- The Practice

DTL Iterators vs. Standard Streams - The Good, The Bad, and The Fail

When iterators encounter an error when either reading or writing to the database or in initializing or maintaining their state, they act like streams. Errors could occur either through the manipulation of a single element (the element fails to pass the test in an InsVal or SelVal method) or the iterator itself could end up in an unusable state (say, if the connection with the database is lost). This behavior is very much like the C++ standard library streams, which have similar characteristics in their possible states:

So as with standard libary streams, iterators provide flags for each of these three states and their values are OR'ed together to get the appropriate effect: dtl_iostate::goodbit, dtl_iostate::failbit, and dtl_iostate::badbit. The iterators inherit from dtl_ios_base, which contains setstate(), rdbuf(), clear(), good(), fail(), and bad() functions that work just like their counterparts in std::ios_base except that setstate() and clear() do not throw if badbit or failbit are set. In the case of DTL, all of these state operations are nothrow.

Now Introducing Our Hero, IOHandler<DataObj, ParamObj>

As described in the theory section above, there are fundamentally two main ways to deal with errors that occur while operating on an iterator. The first method is to simply throw in the event of an error, which will usually abort any algorithm operating on that range / iterator. In the throw case, we will usually want to rollback any operations performed on the range. The first version of our hero, AlwaysThrowsHandler<DataObj, ParamObj>, always tells the code that threw to rethrow the exception by returning dtl_ios_base::THROW_EXCEPTION. The assumption underlying this version of the error handler is that all errors should be passed back to the end-user who can then decide whether or not to roll back any changes made to the range. Examples of range commit and rollback are shown later. The second way of dealing with errors is to simply log & suppress them with the assumption that they will be dealt with after the calling algorithm is done with the iterator. This second method is supported by a version of our hero called LoggingHandler<DataObj, ParamObj> which records errors to a vector<string> for later use and suppresses exceptions by returning dtl_ios_base::SUPPRESS_ERROR. To see how an error handler is defined we turn our X-ray vision on some example code:

// Let's Use our X-Ray Vision to Look at the Innards of our Hero


template<class DataObj, class ParamObj = DefaultParamObj<DataObj> > class OurHeroicHandler
{
private:
	// ... some state data, but assume handler is default constructible
public:

	dtl_ios_base::MeansOfRecovery
		operator()(RootException &ex, dtl_ios_base &base,
		   DataObj &data, ParamObj &params)
	{
		// example of what you might do in a handler
		if (bad())
		{
			LogErrorToFile(ex);
			return dtl_ios_base::THROW_EXCEPTION;
		}
		else if (fail())
		{
			// tries to make the DataObj valid and then reapplies previous operation
			// to base on the good object ... may still fail
			bool failed = WorkMagicOnDataObjAndTryAgain(...);

			if (failed)
			{
				LogErrorToFile(ex);
				return dtl_ios_base::THROW_EXCEPTION;
			}
			else
				return dtl_ios_base::SUPPRESS_ERROR; // success ... our superhero
								     // has saved the day!
		}
	}
};

The above handler instructs the code invoking the handler to still throw the exception if the iterator is in a bad state. In the case that the iterator has only failed and is salvageable, our hero weaves some magic on the DataObj and then reapplies the operation that failed on the iterator. If the retry of the operation succeeded, our hero has saved the day and the handler tells the invoking code that all is well. Otherwise, it is a dark day for the iterator and the invoking code will be told to throw. To tell what personality we would like our IOHandler to take for a given DB_iterator it, we simply invoke it.set_io_handler(), passing in an instance of our handler as the argument. Similarly, it.get_io_handler() will return the current handler for the iterator. Note that you must pass a NULL pointer to this method, e. g.:

      it.get_io_handler((OurHeroicHandler<DataObj, ParamObj> *) NULL);

Calling the similar members of the DBView<DataObj, ParamObj> template will set and get the default view for newly created iterators that refer to that view. Now you know everything needed to summon our superhero and to let him do his work to save the day!

Now here's another example that shows the use of a logging handler class to report the exceptions that occur.

// Example Code Using LoggingHandler on a DBView

// test of failed SelValidate() when reading data
void TestBadSelValidate()
{
 	vector<Example> results;

	// construct view
	// DBView<Example> is actually DBView<Example, 
	// DefaultParamObj<Example> > thanks to the default 
	// argument to the DBView template

	// use our bad BCA which references a nonexistent column name in DB_EXAMPLE
	DBView<Example>
		view("DB_EXAMPLE", BCAExampleObj(),
		"WHERE INT_VALUE BETWEEN (?) AND (?) AND "
		"STRING_VALUE = (?) OR EXAMPLE_DATE < (?) ORDER BY EXAMPLE_LONG",
		BPAExampleObj(), BadSelValidate());

	view.set_io_handler(LoggingHandler<Example>());

	// loop through query results and add them to our vector
	// in this loop, read_it.GetLastCount() records read from DB

	DBView<Example>::select_iterator read_it = view.begin();

	// set parameter values for the WHERE clause in our SQL query
	read_it.Params().lowIntValue = 2;
	read_it.Params().highIntValue = 8;
	read_it.Params().strValue = "Example";
	
	TIMESTAMP_STRUCT paramDate = {2000, 1, 1, 0, 0, 0, 0};
	read_it.Params().dateValue = paramDate;

	for ( ; read_it != view.end(); read_it++)
	{
		try
		{
		  // note that the read_iterator::GetLastCount()  is incremented in operator++()
		  // remember that the record is fetched and thus the count incremented
		  // before operator*() is applied to the read_iterator

		  cout << "Reading element #" << read_it.GetLastCount() << endl;
		  
		  cout << "read_it->exampleInt = " << read_it->exampleInt << endl;
		  cout << "read_it->exampleStr = " << read_it->exampleStr << endl;
		  
		  results.push_back(*read_it);
		}
		catch (RootException &ex)
		{
		  cout << "Caught Exception!!!!" << endl;
		  cout << ex.what() << endl;
		}
	}

	LoggingHandler<Example> handler = 
		read_it.get_io_handler((LoggingHandler<Example> *) NULL);

	typedef LoggingHandler<Example>::LoggedTriple LoggedTriple;

	vector<LoggedTriple> errors = handler.GetLog();

	for (vector<LoggedTriple>::iterator log_it = errors.begin(); log_it != errors.end();
			log_it++)
	{
		LoggedTriple error = *log_it;

		cout << "Error msg = " << error.errmsg << endl;
		cout << "Example = " << error.dataObj << endl;
	}

}

Range commit and rollback

You can emulate transactions over ranges of operations using DBConnection::CommitAll() and DBConnection::RollbackAll().

// Range Transaction over a DBConnection: Insertion into a DBView 

const TIMESTAMP_STRUCT chrysalis = {2002, 4, 3, 0, 0, 0, 0};
const TIMESTAMP_STRUCT mikero = {2001, 11, 2, 0, 0, 0, 0};
const TIMESTAMP_STRUCT victory = {2001, 3, 10, 0, 0, 0, 0};

// this example shows range insert transactions in action
void RangeInsertExample()
{
	DBConnection conn;
        conn.Connect("UID=example;PWD=example;DSN=example;");

	typedef DBView<Example> DBV;

	DBV view("DB_EXAMPLE", DefaultBCA<Example>(), 
	   "", DefaultBPA<DefaultParamObj<Example> >(), DefaultSelValidate<Example>(),
	   DefaultInsValidate<Example>(), conn);

	cout << "Examples in view before attempted range insert:" << endl;

	copy(view.begin(), view.end(), ostream_iterator<Example>(cout, "\n"));

	vector<Example> read_from_DB_before;

	copy(view.begin(), view.end(), back_inserter(read_from_DB_before));

	// examples that we want to insert into the DB ...
	// we want an all or nothing on these guys!
	vector<Example> all_or_nothing_examples;

	// third element will fail to be inserted, should force rollback
	all_or_nothing_examples.push_back(Example(79, "FUBAR", 2.2, 99, mikero));
	all_or_nothing_examples.push_back(Example(81, "All Messed Up", 21.09, 75, chrysalis));
	all_or_nothing_examples.push_back(Example(85, "Bad Boy", -21.22, 11, victory));
	all_or_nothing_examples.push_back(Example(99, "Good One", 77.99, 41, victory));
	
	// must write all the elements to succeed in the transaction
	// else we rollback
	try { 
      
	  DBV::insert_iterator write_it = view;

	  write_it.set_io_handler(AlwaysThrowsHandler());

	  for (vector<Example>::iterator ins_it = all_or_nothing_examples.begin(); 
	      ins_it != all_or_nothing_examples.end(); ins_it++, write_it++)
		  {	  
			 *write_it = *ins_it;
		  }

          conn.CommitAll(); // we assume commit and rollback must always succeed to avoid two-phase commit type logic
	}
        catch(RootException &ex) 
	{ 
	  cout << ex << endl;
          conn.RollbackAll(); 
	}

	cout << "Examples in view after attempted range insert:" << endl;

	copy(view.begin(), view.end(), ostream_iterator<Example>(cout, "\n"));

	vector<Example> read_from_DB_after;

	copy(view.begin(), view.end(), back_inserter(read_from_DB_after));

	cout << "Changes resulting from attempted range insert:" << endl;
   
        TableDiff(cout, read_from_DB_before, read_from_DB_after);
} 

DTL does not provide built in support of range transactions for IndexedDBViews. (We thought about it, but the performance penalty was too high).Instead, you can get the effect of commit and rollback by copying your IndexedDBView's "initial" values to a backup IndexedDBView. To rollback, simply swap the backup IndexedDBView into the IndexedDBView that you made changes to. You will also have to call DBConnection::RollbackAll() to propagate the rollback to the database. This technique is used in the examples below.

// Range Transaction over a DBConnection: Insertion into a IndexedDBView 

const TIMESTAMP_STRUCT chrysalis = {2002, 4, 3, 0, 0, 0, 0};
const TIMESTAMP_STRUCT mikero = {2001, 11, 2, 0, 0, 0, 0};
const TIMESTAMP_STRUCT victory = {2001, 3, 10, 0, 0, 0, 0};

// range transaction example for an indexed view
void RangeIndexInsertExample()
{
	DBConnection conn;
        conn.Connect("UID=example;PWD=example;DSN=example;");

	typedef DBView<Example> DBV;

	DBV view("DB_EXAMPLE", DefaultBCA<Example>(), 
	   "", DefaultBPA<DefaultParamObj<Example> >(), DefaultSelValidate<Example>(),
	   DefaultInsValidate<Example>(), conn);

	view.set_io_handler(AlwaysThrowsHandler<Example>());

	IndexedDBView<DBV> idxview(view, 
		"PrimaryIndex; STRING_VALUE; UNIQUE AlternateIndex; EXAMPLE_LONG, EXAMPLE_DATE",
		BOUND);

	cout << "Examples in view before attempted range insert:" << endl;

	copy(idxview.begin(), idxview.end(), ostream_iterator<Example>(cout, "\n"));

	vector<Example> read_from_DB_before;

	copy(idxview.begin(), idxview.end(), back_inserter(read_from_DB_before));

	// examples that we want to insert into the DB ...
	// we want an all or nothing on these guys!
	vector<Example> all_or_nothing_examples;

	// third element will fail to be inserted, should force rollback
	all_or_nothing_examples.push_back(Example(79, "FUBAR", 2.2, 99, mikero));
	all_or_nothing_examples.push_back(Example(81, "All Messed Up", 21.09, 75, chrysalis));
	all_or_nothing_examples.push_back(Example(85, "Bad Boy", -21.22, 11, victory));
	all_or_nothing_examples.push_back(Example(99, "Good One", 77.99, 41, victory));
	
	// must write all the elements to succeed in the transaction
	// else we rollback

	IndexedDBView<DBV> tmp(idxview); // make copy so we can rollback to idxview on failure

	try { 
	  for (vector<Example>::iterator ins_it = all_or_nothing_examples.begin(); 
	      ins_it != all_or_nothing_examples.end(); ins_it++)
		  {	  
			 idxview.insert(*ins_it); // work with tmp
		  }

          conn.CommitAll(); // we assume commit and rollback must always succeed to avoid two-phase commit type logic
	}
        catch(RootException &ex) 
	{ 
	  cout << ex << endl;
	  idxview.swap(tmp); // this will rollback to original results in memory
          conn.RollbackAll(); 
	}

	cout << "Examples in view after attempted range insert:" << endl;

	copy(idxview.begin(), idxview.end(), ostream_iterator<Example>(cout, "\n"));

	vector<Example> read_from_DB_after;

	copy(idxview.begin(), idxview.end(), back_inserter(read_from_DB_after));

	cout << "Changes resulting from attempted range insert:" >> endl;
   
        TableDiff(cout, read_from_DB_before, read_from_DB_after);
}

// Range Transaction over a DBConnection: Updating in a IndexedDBView 

const TIMESTAMP_STRUCT chrysalis = {2002, 4, 3, 0, 0, 0, 0};
const TIMESTAMP_STRUCT mikero = {2001, 11, 2, 0, 0, 0, 0};
const TIMESTAMP_STRUCT victory = {2001, 3, 10, 0, 0, 0, 0};

// range update
void RangeIndexUpdateExample()
{
	DBConnection conn;
        conn.Connect("UID=example;PWD=example;DSN=example;");

	typedef DBView<Example> DBV;

	DBV view("DB_EXAMPLE", DefaultBCA<Example>(), 
	   "", DefaultBPA<DefaultParamObj<Example> >(), DefaultSelValidate<Example>(),
	   DefaultInsValidate<Example>(), conn);

	view.set_io_handler(AlwaysThrowsHandler<Example>());

	IndexedDBView<DBV> idxview(view, 
		"PrimaryIndex; STRING_VALUE; UNIQUE AlternateIndex; EXAMPLE_LONG, EXAMPLE_DATE",
		BOUND);

	cout << "Examples in view before attempted range insert:" << endl;

	copy(idxview.begin(), idxview.end(), ostream_iterator<Example>(cout, "\n"));

	vector<Example> read_from_DB_before;

	copy(idxview.begin(), idxview.end(), back_inserter(read_from_DB_before));

	// examples that we want to insert into the DB ...
	// we want an all or nothing on these guys!
	// string indicates key of element to replace, Example is object to replace the object
	// to replace with
	map<string, Example> all_or_nothing_examples;

	// third element will fail to be updated, should force rollback
	all_or_nothing_examples["Bedazzled"] = Example(79, "FUBAR", 2.2, 99, mikero);
	all_or_nothing_examples["Corwin"] = Example(81, "All Messed Up", 21.09, 75, chrysalis);
	all_or_nothing_examples["Jordan"] = Example(85, "Bad Boy", -21.22, 11, victory);
	all_or_nothing_examples["Mirror Image"] = Example(99, "Good One", 77.99, 41, victory);
	
	// must update all the elements to succeed in the transaction
	// else we rollback

	IndexedDBView<DBV> tmp(idxview); // make copy so we can rollback to idxview on failure

	// march through vector and replace elements appropriately
	try {
	  map<string, Example>::iterator map_it;
	  
	  for (map_it = all_or_nothing_examples.begin(); 
	       map_it != all_or_nothing_examples.end(); 
		   map_it++)
		  {	  
		     IndexedDBView<DBV>::iterator find_it = idxview.find((*map_it).first);

			 if (find_it != idxview.end())
				 idxview.replace(find_it, (*map_it).second);
		  }

          conn.CommitAll(); // we assume commit and rollback must always succeed to avoid two-phase commit type logic
	}
        catch(RootException &ex) 
	{ 
	  cout << ex << endl;
	  idxview.swap(tmp); // this will rollback to original results in memory
          conn.RollbackAll(); 
	}

	cout << "Examples in view after attempted range update:" << endl;

	copy(idxview.begin(), idxview.end(), ostream_iterator<Example<(cout, "\n"));

	vector<Example< read_from_DB_after;

	copy(idxview.begin(), idxview.end(), back_inserter(read_from_DB_after));

	cout << "Changes resulting from attempted range update:" << endl;
   
        TableDiff(cout, read_from_DB_before, read_from_DB_after);
}
And that's it! Congratulations you made it to the end of the document! (We thought no-one ever would get here!)

[DTL Home]

Copyright © 2002, Michael Gradman and Corwin Joy.

Permission to use, copy, modify, distribute and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appears in all copies and that both that copyright notice and this permission notice appear in supporting documentation. Corwin Joy and Michael Gradman make no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty.

This site written using the ORB. [The ORB]

1