NAME
ObjStore - Perl Extension For ObjectStore OODBMS
SYNOPSIS
use ObjStore;
use ObjStore::Config;
my $db = ObjStore::open(TMP_DBDIR . "/silly.db", 0, 0666);
try_update {
my $wb = $db->root('whiteboard', sub {new ObjStore::AV($db, 1001)});
for (my $x=0; $x < 1000; $x++) {
$wb->[$x] = {
repetition => $x,
msgs => ["I will not talk in ObjectStore/perl class.",
"I will study the documentation before asking questions."]
};
}
};
print "Very impressive. I see you are already an expert.\n";
DESCRIPTION
The new SQL and the sunset of relational databases.
ObjectStore is the leading object-oriented database. It is engineered by Object Design, Inc. ( http://www.odi.com ) (NASDAQ: ODIS). The database uses the virtual memory mechanism to make persistent data available in the most efficient manner possible.
In case you didn't know, Object Design's Persistent Storage Engine has been licensed by Sun, Microsoft, Netscape, and Symantic for inclusion in their Java development environments.
Prior to this joining of forces,
ObjectStore was too radical a design decision for many applications.
Perl5 did not have a simple way of storing complex data persistently.
Now there is an easy way to build databases, especially if you care about preserving your ideals of data encapsulation. (See below!)
API
Fortunately, you probably wont need to use most of the API. It is exhibited below mainly to make it seem like this product has a difficult and steep learning curve. Skip to the next section.
Mostly, the API mirrors the C++ API. Refer to the ObjectStore documentation for exact symantics. If you need a function that isn't available in perl, send mail to the OS/Perl mailing list (see the README).
ObjStore
$name = ObjStore::release_name()
$major = ObjStore::release_major()
$minor = ObjStore::release_minor()
$maintenance = ObjStore::release_maintenance()
$yes = ObjStore::network_servers_available();
ObjStore::set_auto_open_mode(mode, fp, [sz]);
$num = ObjStore::return_all_pages();
$size = ObjStore::get_page_size();
@Servers = ObjStore::get_all_servers();
$in_abort = ObjStore::abort_in_progress();
$db = ObjStore::open($pathname, $read_only, $mode);
$num = ObjStore::get_n_databases();
::Server
$name = $s->get_host_name();
$is_broken = $s->connection_is_broken();
$s->disconnect();
$s->reconnect();
@Databases = $s->get_databases();
::Database
$db->close();
$db->destroy();
$db->get_default_segment_size();
$db->get_sector_size();
$db->size();
$db->size_in_sectors();
$ctime = $db->time_created();
$is_open = $db->is_open();
$is_mvcc = $db->is_open_mvcc();
$read_only = $db->is_open_read_only();
$can_write = $db->is_writable();
$db->set_fetch_policy(policy[, blocksize]);
Policy can be one of
segment
,page
, orstream
.$db->set_lock_whole_segment(policy);
Policy can be one of
as_used
,read
, orwrite
.$Seg = $db->create_segment();
$Seg = $db->get_segment($segment_number);
@Segments = $db->get_all_segments();
@Roots = $db->get_all_roots();
$root = $db->create_root($root_name);
$root = $db->find_root($root_name);
$value = $db->root($root_name, sub{ $new_value });
This is the recommended API for roots. If the given root is not found, creates a new one. Sets the root's value if $new_value is defined. Returns the root's current value.
$db->destroy_root($root_name);
Destroys the root with the given name if it exists.
::Root
$root->get_name();
$root->get_value();
$root->set_value($new_value);
$root->destroy();
::Transaction
ObjectStore transactions and exceptions are seemlessly integrated into perl. ObjectStore exceptions cause a die
in perl just as perl exceptions cause a transaction abort.
try_update {
$top = $db->root('top');
$top->{abc} = 3;
die "Oops! abc should not change!"; # aborts the transaction
};
There are three types of transactions: try_read
, try_update
, and try_abort_only
. In a read transaction, you are not allowed to modify persistent data.
try_read {
my $var = $db->root('top');
$var->{abc} = 7; # write to $var triggers die(...)
};
Also available is a wrapper begin
that lets you dynamically pick your transaction type:
my $txn = 'abort_only';
begin $txn, { ... };
$T = ObjStore::Transaction::get_current();
$type = $T->get_type();
$pop = $T->get_parent();
$T->prepare_to_commit();
$yes = $T->is_prepare_to_commit_invoked();
$yes = $T->is_prepare_to_commit_completed();
ObjStore::set_transaction_priority($very_low);
ObjStore::set_max_retries($oops);
ObjStore::rethrow_exceptions
my $oops = ObjStore::get_max_retries();
my $yes = ObjStore::is_lock_contention();
my $type = ObjStore::get_lock_status($ref);
my $tm = ObjStore::get_readlock_timeout();
my $tm = ObjStore::get_writelock_timeout();
ObjStore::set_readlock_timeout($tm);
ObjStore::set_writelock_timeout($tm);
::Segment
$Seg->destroy();
$size = $Seg->size();
$yes = $Seg->is_empty();
$yes = $Seg->is_deleted();
$num = $Seg->get_number();
$comment = $Seg->get_comment();
$Seg->set_comment($comment);
$Seg->lock_into_cache();
$Seg->unlock_from_cache();
$Seg->set_fetch_policy($policy[, $size]);
Policy can be one of
segment
,page
, orstream
.$Seg->set_lock_whole_segment($policy);
Policy can be one of
as_used
,read
, orwrite
.
PERSISTENT OBJECTS
Databases are comprised of segments. Segments dynamically resize from very small to very big. You should split your data into lots segments when it makes sense. Segments improve locality and can be a unit of locking or caching.
When you create a database object you must specify the segment in which it is to be allocated. All containers are created using the form 'new ObjStore::$type($store, $cardinality)'
and other creation methods follow a similar pattern. You may pass any persistent object in place of $store and the new container will be created in the same segment as the $store object.
Arrays
The following code snippet creates a persistent array reference with an expected cardinality of ten elements.
my $a7 = new ObjStore::AV($store, 10);
None of the usually array operations are supported except fetch and store. (Actually push, pop, shift and unshift might be available but undocumented.) At least the following works:
$a7->[1] = [1,2,3,[4,5],6];
Complete array support will be available as soon as Larry and friends fix the TIEARRAY interface. (See perltie(3) or http://www.perl.com more info.)
Hashes
The following code snippet creates a persistent hash reference with an expected cardinality of ten elements.
my $h7 = new ObjStore::HV($store, 10);
An array representation is used for low cardinalities. Arrays do not scale well, but they do afford a compact representation. ObjectStore's os_Dictionary
is used for large cardinalities.
Data structures can be built with the normal perl syntax:
$h7->{foo} = { 'fwaz'=> { 1=>'blort', 'snorf'=>3 }, b=>'ouph' };
Or the equally effective, but unbearibly tedious:
my $h1 = $dict->{foo} ||= new ObjStore::HV($dict);
my $h2 = $h1->{fwaz} ||= new ObjStore::HV($h1);
$h2->{1}='blort';
$h2->{snorf}=3;
$h1->{b}='ouph';
Perl saves us again! Relief.
Sets
If you have installed older releases, you might know that sets were supported. They still work, but they are re-implemented in terms of hashes.
References
You can generate a reference to any persistent object with the method new_ref($segment)
. One reason to use references is to refer to data in other databases. Refs do not affect remote refcnts and therefore are a relatively safe way to create cross database pointers.
$f = $r->focus; # returns the focus of the ref
$r->open($how); # attempts to open the focus' database
$yes = $r->deleted; # is the focus deleted?
Be aware that references can return garbage if the focus' database is not open. You will need to call open
explicitly.
Cursors
Containers have a method, new_cursor($segment)
, that creates a persistent cursor for the given container. Think of a cursor as a heavy-weight reference. The following methods are available, in addition to the methods for references.
$cs->seek_pole($pole); # seek to the first or last element
($k,$v) = $cs->at; # returns the current element
($k,$v) = $cs->next; # returns the next element
Array cursors return (index,value) pairs. Hash cursors return (key,value) pairs. Sets do something reasonable, but are highly depreciated. All cursors return the empty list () when no more elements are available.
You should not assume the order of iteration will follow any particular pattern (but it probably will).
If you change membership of a collection while you're iterating through it, anything could happen, so don't.
Depending on the collection representation, cursors may have additional useful behavior. Currently, there is no way to test for this.
Cursors do not change the primary reference count; they use weak references. That means that you can know if a container is deleted even while cursors are focused on it. Use the
deleted
method to check. And just like references, cursors that refer to collections in other databases don't use refcnts at all.
In the future, cursors may be extended to support the following methods:
$en = $cs->prev;
$cs = $key; # seek to $key
$dist = $cs - $cs2; # return the distance between two cursors
There may also be a way to test for availability of advanced features. (E.g. 'can_chicken_walk')
And Access Paths (Oh My!)
An array of references or cursors is essentially an access path. Two simple classes are provided for manipulating access paths:
ObjStore::Ref # access path composed of refs
ObjStore::Cursor # cursor based access path
See the source code for details.
INTROSPECTION
ospeek
While there is no formalized schema for a perl database, the ospeek
utility generates a sample of data content and structure. The following output was snapped from a database created with the SYNOPSIS. This is the full, complete output. ospeek
outputs a summary, not the entire database.
Wait! No Schema?! How Can This Scale?
How can a relational database scale?! When you write down a central schema, you are violating the principle of encapsulation. This is dumb. None of the usual database management operations require a central schema. Why create artificial dependencies between your classes when you can avoid it?
Lazy Evolution
Even schema evolution can be done piecemeal. Give all your objects an evolve
method that insures that their representation is up-to-date.
Tag your objects with version numbers.
Or intelligently figure out how to evolve objects by examining their current structure.
If you are using fake hashes, there is already support for lazy schema evolution. The main thing is to keep an archive of prior object formats to regression test your new evolve
methods. If you can do extracts to a mini-database, that would do the trick. Then just run your new code through a copy of your historical database.
ospeek Example Output
ObjStore::Root whiteboard = ObjStore::AV [
ObjStore::HV {
msgs => ObjStore::AV [
'I will not talk in ObjectStore/perl class.',
'I will study the documentation before asking questions.',
],
repetition => 0,
},
ObjStore::HV {
msgs => ObjStore::AV [
'I will not talk in ObjectStore/perl class.',
'I will study the documentation before asking questions.',
],
repetition => 1,
},
ObjStore::HV {
msgs => ObjStore::AV [
'I will not talk in ObjectStore/perl class.',
'I will study the documentation before asking questions.',
],
repetition => 2,
},
...
],
Examined 1022 persistent slots.
posh
You can also walk around a database from the inside. Study the output I snapped from this posh
session:
posh 1.17 (Perl 5.00403 ObjectStore Release 5.0.1.0)
[set for READ]
/opt/os/tmp% ls
copier.db perltest.db.copy silly.db
perltest.db posh.db test.db
/opt/os/tmp% cd silly.db
$at = ObjStore::HV {
whiteboard => ObjStore::UNIVERSAL::Ref ...
},
% cd $at->{whiteboard}
$at = ObjStore::AV=ARRAY(0xe0580000)% ls
$at = ObjStore::AV [
ObjStore::HV ...
ObjStore::HV ...
ObjStore::HV ...
...
],
$at = ObjStore::AV=ARRAY(0xe0580000)% ls $at->[0]->{msgs}
[0] = ObjStore::AV [
'I will not talk in ObjectStore/Perl class.',
'I will study the documentation before asking questions.',
],
$at = ObjStore::AV=HASH(0xe0580000)% update
[set for UPDATE]
$at = ObjStore::AV=ARRAY(0xe0580000)% cd $at->[0]->{msgs}
$at = ObjStore::AV=ARRAY(0xe058201c)% $at->[0] = 'This is ridiculous.';
$fake1 = 'This is ridiculous.',
$at = ObjStore::AV=ARRAY(0xe058201c)% ls
$at = ObjStore::AV [
'This is ridiculous.',
'I will study the documentation before asking questions.',
],
$at = ObjStore::AV=ARRAY(0xe058201c)% cd ..
$at = ObjStore::AV=ARRAY(0xe0580000)% ls
$at = ObjStore::AV [
ObjStore::HV ...
ObjStore::HV ...
ObjStore::HV ...
...
],
$at = ObjStore::AV=ARRAY(0xe0580000)% for (1..100) { $at->[$_] = $at->[0]; }
$fake2 = '',
$at = ObjStore::AV=ARRAY(0xe0580000)% ls(map {$at->[$_]->{msgs}} 68..70)
[0] = ObjStore::AV [
'This is ridiculous.',
'I will study the documentation before asking questions.',
],
[1] = ObjStore::AV [
'This is ridiculous.',
'I will study the documentation before asking questions.',
],
[2] = ObjStore::AV [
'This is ridiculous.',
'I will study the documentation before asking questions.',
],
WHY IS PERL A BETTER FIT FOR DATABASES THAN SQL, C++, OR JAVA?
When you write a structure declaration in C++ or Java you are assigning both field-names, field-types, and field-order.
struct CXX {
char *name;
char *title;
double size;
};
Programs almost always require a recompile to change these declarations. This is fine for small to medium size applications but is not suitable for large databases. It is too inflexible. An SQL-style language is needed.
When you create a table in SQL you are assigning only field-names and field-types.
create table CXX
(name varchar(80),
title varchar(80),
size double)
This is a more flexible data declaration, but SQL gives you far less expressive power than C++ or Java. Applications end up being written in C++ or Java while their data is stored in SQL. Managing the syncronization between the two languages creates a lot of extra complexity. So much so that there are many software companies that exist solely to help address this headache.
Perl is better because it spans all the requirements in a single language. For example, this is similar to an SQL table:
my $h1 = { name => undef, title => undef, size => undef };
Only the field-names are specified. This declaration is actually even more flexible than SQL because the field-types are dynamic.
But not only is perl more flexible, it's also fast. Malcolm Beattie is working on a perl compiler which is currently in beta. Here is his brief description of a new hybrid hash-array that is supported:
An array ref $a can be dereferenced as if it were a hash
ref. $a->{foo} looks up the key "foo" in %{$a->[0]}. The value is the
index in the true underlying array @$a. As an addition, if the array
ref is in a lexical variable tagged with a classname ("my CXX $obj" to
match your example above) then constant key dereferences of the form
$obj->{foo} are mapped to $obj->[123] at compile time by looking up
the index in %CXX::FIELDS.
For example:
my $schema_hashref = { 'field1' => 1, 'field2' => 2 };
my $arr = [$schema_hashref, 'fwaz', 'snorf'];
print "$arr->{field1} : $arr->{field2}\n"; # "fwaz : snorf"
Summary (LONG)
SQL
All perl databases use the same flexible schema that can be examined and updated with generic tools. This is the key advantage of SQL, now available in perl.
Perl / ObjectStore is definitely faster than SQL too. Not to mention that perl is a general purpose programming language and SQL is at best a query language.
C++
Special purpose data types can be coded in C++ and dynamically linked into perl. Since C++ will always be faster than Java, this gives perl an edge in the long run. Perl is to C/C++ as C/C++ is to assembly language.
JAVA
Java has the buzz, but:
Just like C++, the lack of a universal generic schema limits use to a single application at a time. Without some sort of
tie
mechanism, I don't see how this can be remedied.All Java databases must serialize data to store it. Until Java supports persistent allocation directly, database operations will always be slower than C++.
Perl will soon integrate with Java enough to use SwingSet - AWT.
I'd like to see some comparisions of code length when solving the same problems in Java and in perl... :-)
Summary (SHORT)
Perl can store data
optimized for flexibility and/or for speed
in transient memory and persistent memory
without violating the principle of encapsulation or obstructing general ease of use.
ETA
NOW TO 3 MONTHS
Perl-Java integration; perl compiler
3-6 MONTHS
Dynamically loaded application schemas; perl kernel-level threads; proper tied arrays & repaired tie interface
RDBMS EMULATION
Unstructured perl databases are, well, unstructured. I think the RDBMS table paradigm is actually a good way to structure data. And since you can store complex nested structures per row, add the same row to multiple tables, or nest tables, it's not half bad.
See ObjStore::Database::HV, ObjStore::Table, and ObjStore::AVHV.
Not documented yet.
THE ADVANCED CHAPTER
Bless
The ObjStore module installs its own version of bless
which assures that blessings are persistent. For example:
package Scottie;
use ObjStore;
@ISA = qw(ObjStore::HV);
sub new {
my ($class, $store) = @_;
my $o = $class->SUPER::new($store, $class);
$o->{attribute} = 5;
$o;
}
package main;
my Scottie $dog = new Scottie($db);
Class Autoloading
ObjStore tries to require
each class as you access persistent instances the first time. This means that you can write generic data processing programs that automatically load the appropriate libraries to manipulate data as it's accessed.
To disable the class autoloading behavior:
ObjStore::disable_class_auto_loading();
This mechanism is orthogonal to the AUTOLOAD
mechanism for autoloading functions.
Transactions Redux
EVAL
Transactions are always executed within an implicit
eval
. If you do not want to abort your program when an ObjectStore exception occurs, you should indicate that you want to have control over your own reflexive behavior:ObjStore::rethrow_exceptions(0);
After a transaction, you will need to check the value of
$@
to see if anything went wrong and determine how to proceed.try_update { ... }; die if $@; # Don't forget! Check for errors!
DEADLOCK
Top level transactions are automatically retried in the case of a deadlock. If you need to handle deadlocks specially, you can use ObjStore::set_max_retries(0).
Stargate
The stargate determines which collection representations are used to store implicitly created hashes and arrays. It is called recursively on data structures in order to copy them into persistent memory. You can access the stargate directly with ObjStore::translate
.
my $persistent_copy = ObjStore::translate([1,2,3,{fat=>'dog'}]);
If you want to design your own stargate, make sure to dismember transient structures as they are processed to insure that cyclic structures are collected in transient memory. (See ObjStore.pm
for an example.)
Weak References
At the moment when only weak references refer to an object, the method NOREFS
is invoked. You should either break any remaining weak references or store a real ref to the given object. You can also do nothing and wait for all remaining references to be broken naturally. In special situations, you may need to resort to:
$o->set_weak_refcnt_to_zero();
But this is sloppy and you should avoid it unless you're a slob.
Peek
You can customize the output of ospeek. Not documented yet. See ObjStore::Peeker.
$o->peek($peeker, $name); #add a method like this...
Fake Hashes
You can declare an object in two lines:
use base 'ObjStore::AVHV';
use Class::Fields qw(f1 f2 f3);
Not documented yet.
$o->evolve() if !$o->is_evolved;
Performance Check List
The word tuning implies too high a brain-level requirement. Getting performance out of ObjectStore is not rocket science.
COMPACTNESS
Is your data stored as compactly as possible?
You get 90% of your performance because you can fit your whole working data set into RAM. If you are doing a good job, your unindexed database should be less than twice the size of it's uncompressed ASCII dump; i.e., less than 2 times expansion. (See the section on data representation.)
SEGMENTS
Is your data partitioned into as many segments as possible? (See the introduction to containers.)
DO AS MUCH AS POSSIBLE PER TRANSACTION
Transactions, especially update transactions, involve a good deal of set/cleanup. The more you do per transaction the better.
WHERE IS THE REAL BOTTLENECK?
Use the 'time' command or DProf to analyze where your program is spending most of it's time. osp_copy is bottlenecked by perl and the network, not the database. Try using the perl compiler. (See http://www.perl.com ) Try upgrading to your network to ATM or run your program on the same machine as the ObjectStore server.
LOCKING AND CACHING
Object Design claims that your caching and locking settings also impact performance. I haven't been able to verify this. (See os_segment::set_lock_whole_segment and os_database::set_fetch_policy.)
Cross Database POINTERS
This feature is depreciated, but you can allow cross database pointers with:
$db->_allow_external_pointers;
But you should avoid this if at all possible. Pointers affect refcnts, even in other databases. Your refcnts will be wrong if you simply osrm
a random database. This will cause some of your data to become undeletable. Currently, there is no way to safely delete undeletable data.
Instead, you should use references or cursors to refer to data in other databases. References use the os_reference_protected class, which is designed to solve this problem. References and cursors do not use refcnts when pointing to remote database so you are free to osrm with less trepidation and planning.
TECHNICAL IMPLEMENTATION
You don't have to understand anything about the technical implementation. Just know that:
ObjectStore is outrageously powerful, sophisticated, and over-engineered.
The perl interface is optimized for simplicity and easy of use. (If it's not fun, why bother?)
The performance of raw ObjectStore is so good that even with a gunky perl layer, benchmarks will show that relational databases can be safely left on the bookshelf where they belong.
Differences Between The Perl And C++ APIs
Most stuff should be roughly the same. However, some static methods sit directly under ObjStore::
instead of under their own classes.
The interface for lookup and open is simplified.
All persistent objects have
database_of
andsegment_of
methods.Transactions are simplified.
Data Representation
Memory usage is much more important in a database than in transient memory. When databases can be as large or larger than ten million megabytes, a few percent difference in compactness can mean a lot.
All values take a minimum of 8 bytes (OSSV). These 8 bytes are used to store the type of the value, a pointer, and a 16-bit integer.
value stored allocation in addition to the OSSV
------------------------------ -------------------------------------
undef none
pointer none
16-bit signed integers none
32-bit signed integers 4 byte block (OSPV_iv)
double 8 byte block (OSPV_nv)
string length of string (char*)
object (ref or container) size of object (subclasses of OSSVPV)
splash collections ...
Hard Limits
Reference counts are only 32 bits unsigned.
Weak reference counts are only 16 bits unsigned.
Strings are limited to a length of 32767 bytes.
Go Extension Crazy
ObjStore::UNIVERSAL
is the base class for all persistent objects. You cannot directly access persistent scalars from perl. They are always immediately copied into transient scalars. So the ObjStore::UNIVERSAL
base class is only for objects (or collections).
ObjStore::UNIVERSAL::Ref
is the base class for references.
ObjStore::UNIVERSAL::Container
is the base class for all containers.
ObjStore::UNIVERSAL::Cursor
is the base class for cursors.
ObjStore::AV
is the base class for tied arrays.
ObjStore::AVHV
is the base class for fake hashes.
ObjStore::HV
is the base class for tied hashes.
ObjStore::File
will be the base class for large binary data.
When an ObjectStore exception occurs, $ObjStore::EXCEPTION
is called with an explaination. You can replace the default handler with your own.
Each subclass of ObjStore::UNIVERSAL::Container
has a %REP
hash. Persistent object implementations add their create functions to the hash. Each packages' new
method decides on the best representation, calls the creation function, and returns the persistent object.
You can add your own C++ representations for each of AV and HV. If you want to know the specifics, look at the code for the built-in representations (GENERIC.*
).
You can add new families of objects that inherit from ObjStore::UNIVERSAL
. Suppose you want highly optimized, persistent bit vectors? Or matrics? These would not be difficult to add. Especially once Object Design figures out how to support multiple application schemas within the same executable. They claim that this tonal facility will be available in the next release.
ossv_bridge typemap
The following explaination may be helpful to developers trying to understand the typemap. If you don't know what a typemap is, just skip to the next section.
The struct ossv_bridge
is used to bridge between perl and C++ objects. It contains transient cursors and transient pointers to persistent data. Immediately after a transaction finishes, invalidate
is invoked on all outstanding bridges. This is necessary in order to update the reference counts properly. This was also the most difficult part to get right. But hey, how many databases do reference counting? Or even how many databases can store pointers?
DIRECTION
APIs
Support for notification, database access control, and any other interesting ObjectStore APIs.
LEANER COLLECTION REPRESENTATIONS
The ObjectStore collections are weighted down with embedded index and query support. Worse, ObjectStore cursors cannot use references, only pointers. I'd like to find a suite of lean representations for large cardinality collections to compliment the Splash collections.
MORE BUILT-IN DATA TYPES
File objects implemented using osmmtype and subclassed from IO::Handle. Support for one of Object Design's Text Object Managers? Support for bit vectors and matrics?
EXPORTS
bless
, try_read
, try_update
, try_abort_only
by default. Most other static methods can also be exported.
BUGS
HIGH VOLITILITY
Anything not documented is subject to change without notice. (Backward compatibility will be preserved when possible.)
CURSED OBJECTS
The strings used to record the blessed nature of persistent objects are allocated under a private root in the default segment of a database (See
'ospeek -all'
). If you accidentally mess up or change any of these strings, your objects will be cursed. You have a backup, right?MOP
This is not a general purpose ObjectStore editor with complete MOP support. Actually, I don't think this is a bug.
AUTHOR
Copyright (c) 1997 Joshua Nathaniel Pritikin. All rights reserved.
This package is free software; you can redistribute it and/or modify it under the same terms as perl itself. This software is provided "as is" without express or implied warranty. Perl / ObjectStore is available via any CPAN mirror site. See http://www.perl.com/CPAN/modules/by-module/ObjStore
Portions of the collection code snapped from splash, Jim Morris's delightful C++ library ftp://ftp.wolfman.com/users/morris/public/splash .
Also, a poignant thanks to all the wonderful teachers with which I've had the opportunity of studying.
SEE ALSO
Examples in the t/ directory, perl5, ObjectStore, and never again SQL!