Metakit 2.3.4 - last release candidate
March, 2001

Welcome to the latest release of the Metakit embedded database library.
This is a near-final release, it is functionally 100% done, but a few
minor problems/bugs still remain.  Release MK 2.3.4 is essentialy ready
for production use, with one extremely important disclaimer:

   >>> Do *not* rely on new commit-aside/-extend models, these
   >>> new features are not good enough for general use yet.
   >>> Worse still, they will remain experimental in 2.3 final!

This document describes the main changes since 2.01 in some detail:

 - a new version numbering scheme (again)
 - some essential file format changes
 - new commit modes: commit-aside and commit-extend
 - the memo datatype has been removed from the public API
 - made a start with making custom viewers modifiable
 - new "mapped views": hashed, blocked, and ordered
 - recursive view definitions
 - the Tcl language binding now includes a command-object API
 - better error checking to avoid disk full errors from being missed
 - documentation and current status

Most of these changes are intended to make Metakit more scalable and to
improve performance in various ways.  This process is not complete, as
several new bottlenecks have been intoduced in the new code.  The main
goal of the 2.3 release is to make sure the file format is correct and
functioning properly.  The next minor releases will then address various
bottlenecks to take maximum advantage of the new file format.

See the last section for notes about the status and stability of MK.


NEW VERSION NUMBERING SCHEME

I am adopting a more rigid 3-level numbering scheme, as follows:

 - stable release are X.0, X.2, X.4, i.e. even-numbered
 - development releases are X.1, X.3, X.5, i.e. odd-numbered
 - alpha releases are all called X.Y.1, they are distinguished only by
   the date embedded in the snapshot tarball, e.g "mk2-20000629.tar.gz"
 - beta releases are all called X.Y.2, again only dates in snapshots
 - the "final release candidate" will be called X.Y.3, with one extra
   chance to fix any remaining mistakes in a release called X.Y.4
 - the first final release will be called X.Y.5, with every new update
   and fix incrementing that third digit

To avoid confusion between 2.01 and a version number of 2.1, this new
release has been dubbed 2.3 - since it is still development version.


NEW FILE FORMAT

This release has major changes in the way data is stored on file, or to
be more accurate: the data itself is hardly different, but administration
of the data is now radically different (it used to be a tree-walk on each
open and commit, the new format reads/commits far more lazily).

The most immediate effect is that file opens are now O(1), and no longer
take time proprotional to the number of subviews and columns.  There has
been some "cutting corners" in this release, but the file format is now
able to support several degrees of laziness, which will gradually be
implemented to make opens instant, and frequent commits cheap.

The biggest drawback of this change, is that there are now two different
file formats.  The new 2.3 release will convert old formats on-the-fly
during open, and will save the new format on commit.  This means that
moving to the new release is relatively painless, but once changes are
committed with 2.3, you can not go back to 2.01 or earlier.

The "random" versus "serial" file format differences of pre-2.3 Metakit is
now gone.  When serializing data out with "SaveTo", MK will now figure out
how to save the file in an optimally compact form, suitable for loading on
demand.  That means that the best way to reorganize a MK datafile to get
rid of internal free space, is now to serialize it out to file.  As extra
benefit, a serialized file has an accurate file size in the header, so
that the exact size is known up front when serializing back in.  This can
be useful when data is streamed across a communications channel.  Finally,
storage objects are now derived from views, and both can be initialized
from an input file/stream.

One special file format change affects the top level: MK datafiles are now
conformant with *IFF file formats (i.e. 8-byte header with type and size).
Serialized files, and files using the ordinary ("full") commits consist of
exactly one section.  However, files using commit-extend (described below) 
will contain multiple sections.

The new file format now uses a tail marker.  Because of that, a datafile
can be appended to another file.  MK will always start by looking for the
tail marker to determine where the file starts (resorting to old-style
search-from-start to deal with pre-2.3 files).  Another effect, is that
any existing file can now be opened by MK.  If there is no valid MK data,
new contents will be appended.  There is a new c4_Strategy::EndOfData
member which returns -1 if the file contained to valid MK data.  One goal
of all this logic, was to allow adding MK data at the end of executables.

Several features have been prepared for in the new file format, including
heterogenous views, clustering, compression, encryption - as well as the
ability to store revisions.  None of this is anywhere near implementation,
so please don't expect much in these areas for quite some time.  The main
reason to mention this at all, is to indicate that the new file format has
plenty of room for future expansion.


NEW COMMIT MODES

There are two new commit modes: "commit-aside" and "commit-extend", with
very different characteristics.  They cannot yet be combined, but this is
one of the goals, since that will support some interesting scenario's.

Commit-aside stores all changes made to one datafile in another one.  The
primary datafile is not altered, it may be opened read-only.  The changes
are saved in a special format and managed completely by MK.  Opening the
original file later will of course lead to the original pre-commit state.
By attaching that other file (using c4_Storage::SetAside), the latest
changes become visible again.  This logic can be stacked, though the API
for all this is not very intuitive.  Commit-aside applies to every change
made to the file, including structure changes.  The second data file is
in effect a "difference file", it has no use without the original file.

In commit-aside mode, the c4_View::Commit call takes an extra argument,
specifying whether to do a "fast" or a "full" commit (fast is default).
Fast means: save the changes in the aside file, full means: apply all
changes to the primary datafile and clear the aside file contents.  The
rollback also comes in two versions: fast rollback rolls back to the
state before the last commit, while full rollback forgets about commit
aside altogether and reverts to the original datafile (without deleting
the aside file contents - it can be re-attached later).  Note that any
changes made to the primary datafile will invalidate the aside file.

The purpose of commit-aside is two-fold: to speed up commits, and as a
way to save changes when a key database needs to remain unchanged (this
is useful when distributing over CD-ROM, or to manage original releases,
for example).  Commit aside is not yet optimally efficient, but it'll get
better - the key issue is that the amount of data saved is proportional
to the amount of change, and *not* to the size/number of views & columns,

Commit-extend is somewhat different: it is a modified version of a normal
commit, which saves all changes at the end of the datafile, in such a way
that current readers are not affected.  Readers will not see any changes
until they do a rollback (which is a misnomer in this context).  At that
point, the reader will resynchronize to the latest *committed* changes.

Commit-extend supports multiple readers and a single writer, and due to
the way it is implemented it does so without any locking or contention.
The trade-off is that datafiles will grow on each commit, and need to be
cleaned up periodically to avoid filling the disk.  This can often be
scheduled to some "idle" period, at which point a normal (exclusive)
open and commit can be performed, possible followed by serialization to
generate an optimally-packed datafile again.  Note that commit-extend is
not much more efficient than a normal commit, it still writes out entire
changed columns (there is a speedup because free-space is not reclaimed).

In this release, commit-aside and commit-extend cannot be combined: this
would greatly reduce the amount of data saved on commit, while supporting
a form of high-performance concurrency.  This "hybrid commit" is planned
for the final 2.3 release, as is a more sophisticated degree of delta-
storage in commit-aside.


NO MORE MEMOS TO WORRY ABOUT

The memo ("M") datatype has always been a confusing addition to MK, since
it complements the bytes ("B") datatype yet adds very little value.  It
has always been difficult to decide between B and M in the design phase,
and since there were no conversion tools - that choice has to be made up
front.  This release does away with memos.  

At least, that's how it looks on the outside.  On the inside, MK has now
been extended to dynamically switch between column-wise (B) and item-wise
(M) storage, depending on the amount of data and the size of the view.
The result is likely to be better than either approach before, because
the choise is based on actual dynamics, and on an item-by-item basis.
The heuristics which determine how data is stored internally are still
being tweaked.  But the good news is that binary data storage is now
always specified in a uniform manner, i.e. through c4_BytesProp.  In a
way, this is analogous to how MK has always offered "adaptive integers",
i.e. the fully transparent switch between 1/2/4/8/16/32-bit integers.

Another change is that strings are now internally based on the same more
adaptive style, they now scale much better than before and also no longer
suffer from a startup delay (caused by a pre-2.3 null-byte scan on open).


MODIFIABLE CUSTOM VIEWERS

Custom viewers were introduced in version 1.8 and have turned out to be
extremely effective and flexible.  Almost all the relational operators of
MK are implemented as classes derived from c4_CustomViewer.  As of this
release, custom viewers now support the ability to handle modifications.

Many of the existing viewers, such as join and intersection, are still
read-only (and some changes could never be handled properly), but a few
others were realtively easy to extend.  View operators such as Concat,
RemapWith, Pair, now support modifications on the resulting view.  These
modifications are passed back to the underlying views.

There are several types of modifications: setting individual properties
in individual rows, inserting/removing rows, and changing structure.
Propagating individual property changes is usually easiest to implement.
Inserting/deleting rows is far more complex, and not even semantically
obvious in many cases (where do you insert row N, if the view is the
result of concatenating an N-row view A and an M-row view B?).  For the
moment, row insertions/deletions are only implemented in mapped views
(see below).

The last category of changes is the most complex of all, and has not
been addressed in this release.  The best way to stay out of trouble is
to only restructure views on open (i.e. at app initialization time),
and to then only deal with inserting/modifying/deleting rows.  Subviews
are no different, and can be modified in the same way as the main views.


MAPPED VIEWS

Modifiable custom viewers which support row insertion and deletion have
been dubbed "mapped views", because they usually represent a mapping of
some kind over one or more underlying views.  Changes are propagated to
the underlying views in the way that is deemed most intuitive.

The 2.3 release contains three new mapped view implementations: hash,
blocked, and ordered.

The hash view takes a main "data" view as input, plus a "map" view.  The
map view must be defined with a fixed structure ("_H:I,_R:I"), and is
managed fully by the hash view.  The data view can be any structure, it
is the collection of data on which hashing is to be applied.  When hash
views are defined, they must be told how many of the first properties
form a key (note that the key fields must always be first).  What the
hash view does is create a new layer on top of the original data view,
which intercepts all modification and find requests.  Modifications
are applied to the data, but also cause the map view to be adjusted as
needed.  Find requests which happen to specify a value for the key are
singled out and cause a fast O(1) hash lookup to take place.

There are several consequences of this approach: first of all, it is up
to the application to set up a hash view after open (for now at least).
Also, once you want hashing, you should *never* change the underlying
data or map views - the only way to make sure they contain the proper
info for hashing to work is to always change *through* the hash view.
Note that the data view is the same as without hashing, all details
needed for instant hash lookup are maintained in the separate map view.
Unlike most conventional hash tables, insertion by position is still
allowed, though insertion at the end is fastest (by far).  Also unlike
most hash implementations, it is possible to change the key of a row,
because MK will rehash whenever that happens.  Caveat: the details of
changing key fields are not yet correct in this release.

Find requests automatically detect hash views and optimize accordingly.
The "python/find.py" script demonstrates find access optimizations.

The blocked view is a view which stores its rows in "blocks", i.e. one
extra level of subviews.  To make a view of the form "a:I,b:S,c:D"
blocked, you have to define it as "_B[a:I,b:S,c:D]" instead, then call
the Blocked member to produce the view you can work with.  The blocked
view then takes over all insertions and deletions and rebalances both
root and leaf blocks in such a way that none of the subviews ever
contains either very few nor very many rows ("few" and "many" depend
on the total number of rows, everything is adaptive).  Due to the extra
level of indirection, blocked views are slightly slower, but the gain
is that they can scale to an unlimited number of rows and still offer
good performance (probably O(log N) instead of the usual O(N)).  These
blocked views will behave like ordinary views in every respect, i.e.
you can iterate and access rows by position, regardless of the blocking
and occasional rebalancing that takes place underneath.

The ordered view is a view which assumes the underlying view is sorted
on its first N properties (N defaults to 1), and which maintains that
order by ignoring isnert positions and supplying its own instead.  As
with hash views, keys are unique.  Inserting a row which has the same
key as another one will cause the other row to be replaced instead.

Ordered views intercept changes, but also find requests.  If a find
specifies a value for the key field(s), then binary search is used to
find the rows far more quickly.

All of the above views can be combined, e.g. by layering a hash view
on top of a blocked data view, you get a persistent hash structure
which scales up indefinitely, yet offers O(1) hash access performance.

An ordered view on top a blocked view creates a blocked structure
which keeps its rows sorted.  The current release will take advantage
of binary search if possible - but real btree-like searches will be
implemented before 2.3 final (this is far more efficient than "just"
binary search, due to its much higher locality of reference).

And finally, you can layer an ordered view on a hash view on one or
two blocked views (for data and/or map).  This creates a structure
which maintains sort order (and can be traversed in that order), yet
with instant hash access on single key values.  The "tcl/mapped.tcl"
script shows an example of various ways in which this can be done.


RECURSIVE VIEW DEFINIONS

You can now define views with recursive subviews.  The way this is
done is to specify a subview of the form "subview[^]".  An example:
	roots[name:S,children[^]]
This define a "roots" view containing 0 or more rows.  Each has the
form given above, which is also identical to:
	name:S,children[name:S,children[^]]
In other words, the "children" subview has the same format as its
parent, and this continues to arbitrary depths.  The above example is
in fact nothing but a definition of a tree containing named nodes.
Restructuring works, it will recursively restructure the entire tree.


COMMAND OBJECTS IN MK4TCL

The Mk4tcl extension has undergone a massive face-lift.  The original
API is still there, but there is now also an OO-style command interface
(in the same way as Tk works).  This is based on work by Matt Newman.
Because of this, a far more powerful binding to the MK C++ core is now
available from Tcl (as it has been in Python for a while).  These new
calls include relational operators, such as join, select, sort, etc.
For now, this is "thinly" documented in the "tcl/mk4tcl.h" header.
Another useful source to check, is the "tcl/test/mk5object.tcl" script.

The Tequila server and the I/O handler package (iohan) have undergone
some changes (extensions, really).


BETTER ERROR CHECKING

The write and commit logic have been improved to better detect whether
anything went wrong during the commit, to prevent the final end marker
from being updated.  This has the effect of ignoring all changes.  The
most important change was to check and remember errors in fflush, and
to make the c4_Stream::Write member return a success flag i.s.o. void.


DOCUMENTATION

Documentation.  Hm, what documentation?  I have converted the C++ docs
to work with Doxygen, which extracts everything from comments in the
source code.  The C++ documentation is available as a scripted document.
To get it, you need to download the file:
	http://www.equi4.com/previews/mk4api.bin
and one of the following (sorry, only Linux and Windows for now):
	http://www.equi4.com/previews/tclkit-linux.bin
	http://www.equi4.com/previews/tclkit.exe
Uncompress and make all files executable, then drop mk4api on tclkit,
or whatever your explorer, navigator, finder, or shell wants you to do.
If this is not workable, I will create a tarball of the html/gif files.

For Python and Tcl, check the pages doc/python.html and doc/tcl.html
in the source distribution (they are also part of the above "mk4api").
There is a new examples/ directory with several Python and Tcl examples.

The documentation is far from complete ("non-existent" or "wrong" are
also applicable terms in some cases, I'm afraid).  I have started work
on improving this.  Comments on what needs to be done first are most
gratefully accepted - there is a huge amount of work left to do.


CURRENT STATUS

This 2.3.4 build passes all but one of the original regression tests
(the one which does not pass is related to serialized data loading).

There are several performance bottlenecks, despite the fact that the new
format really should work better than the pre-2.3 file format.  For now,
the first goal is to make sure that the file format is 100% frozen and
useable as is - to avoid even more complex future conversion nightmares.
I will start aiming for top speed *after* everything works properly.

The commit logic can be slow for complex datasets, I am looking into it.

There are some problems when changing keys in hash & ordered views, the
best workaround is to not change key properties - delete and re-insert
the row instead if you need this capability.

This MK 2.3.4 final release candidate is very solid.  Recent versions
of the beta preceding it have been in use in a wide range of active
projects without substantial problems.  A byte-order conversion bug has
recently been found and fixed, as well as double alignment on Solaris.

At this point, I recommend using 2.3.4 as replacement for 2.01, except
when tweaks for top performance have been done in 2.01 (2.3.4 is not yet
as highly optimized, and has a slightly different performance behavior).


-- Jean-Claude Wippler <jcw@equi4.com>
