## Full example

    #!/usr/bin/env qore

    # database test script
    # databases users must be able to create and destroy tables and procedures, etc
    # in order to execute all tests

    %require-our
    %enable-all-warnings

    our ($o, $errors, $test_count);

    const opts =
    	( "help"    : "h,help",
    	  "host"    : "H,host=s",
    	  "pass"    : "p,pass=s",
    	  "db"      : "d,db=s",
    	  "user"    : "u,user=s",
    	  "type"    : "t,type=s",
    	  "enc"     : "e,encoding=s",
    	  "verbose" : "v,verbose:i+",
    	  "leave"   : "l,leave"
     );

    sub usage()
    {
    	printf("usage: %s [options]
     -h,--help          this help text
     -u,--user=ARG      set username
     -p,--pass=ARG      set password
     -d,--db=ARG        set database name
     -e,--encoding=ARG  set database character set encoding (i.e. \"utf8\")
     -H,--host=ARG      set hostname (for MySQL and PostgreSQL connections)
     -t,--type          set database driver (default mysql)
     -v,--verbose       more v's = more information
     -l,--leave         leave test tables in schema at end\n",
    	   basename($ENV."_"));
    	exit();
    }

    const object_map =
     ( "oracle" :
       ( "tables" : ora_tables ),
       "mysql"  :
       ( "tables" : mysql_tables ),
       "pgsql"  :
       ( "tables" : pgsql_tables ),
       "sybase" :
       ( "tables" : syb_tables,
    	 "procs"  : sybase_procs ),
       "freetds"  :
       ( "tables" : freetds_sybase_tables,
    	 "procs"  : sybase_procs ) );

    const ora_tables = (
    	"family" : "create table family (
       family_id int not null,
       name varchar2(80) not null
    )",
    	"people" : "create table people (
       person_id int not null,
       family_id int not null,
       name varchar2(250) not null,
       dob date not null
    )",
    	"attributes" : "create table attributes (
       person_id int not null,
       attribute varchar2(80) not null,
       value varchar2(160) not null
    )" );

    const mysql_tables = (
    	"family" : "create table family (
       family_id int not null,
       name varchar(80) not null
    ) type = innodb",
    	"people" : "create table people (
       person_id int not null,
       family_id int not null,
       name varchar(250) not null,
       dob date not null
    ) type = innodb",
    	"attributes" : "create table attributes (
       person_id int not null,
       attribute varchar(80) not null,
       value varchar(160) not null
    ) type = innodb" );

    const pgsql_tables = (
    	"family" : "create table family (
       family_id int not null,
       name varchar(80) not null )",
    	"people" : "create table people (
       person_id int not null,
       family_id int not null,
       name varchar(250) not null,
       dob date not null )",
    	"attributes" : "create table attributes (
       person_id int not null,
       attribute varchar(80) not null,
       value varchar(160) not null)",
    	"data_test" : "create table data_test (
    		int2_f smallint not null,
    		int4_f integer not null,
    		int8_f int8 not null,
    		bool_f boolean not null,

    		float4_f real not null,
    		float8_f double precision not null,

    		number_f numeric(16,3) not null,
    		money_f money not null,

    		text_f text not null,
    		varchar_f varchar(40) not null,
    		char_f char(40) not null,
    		name_f name not null,

    		date_f date not null,
    		abstime_f abstime not null,
    		reltime_f reltime not null,
    		interval_f interval not null,
    		time_f time not null,
    		timetz_f time with time zone not null,
    		timestamp_f timestamp not null,
    		timestamptz_f timestamp with time zone not null,
    		tinterval_f tinterval not null,

    		bytea_f bytea not null
    		--bit_f bit(11) not null,
    		--varbit_f bit varying(11) not null
    )" );

    const syb_tables = (
    	"family" : "create table family (
       family_id int not null,
       name varchar(80) not null
    )",
    	"people" : "create table people (
       person_id int not null,
       family_id int not null,
       name varchar(250) not null,
       dob date not null
    )",
    	"attributes" : "create table attributes (
       person_id int not null,
       attribute varchar(80) not null,
       value varchar(160) not null
    )",
    	"data_test" : "create table data_test (
    	null_f char(1) null,

    	varchar_f varchar(40) not null,
    	char_f char(40) not null,
    	unichar_f unichar(40) not null,
    	univarchar_f univarchar(40) not null,
    	text_f text not null,
    	unitext_f unitext not null, -- note that unitext is stored as 'image'

    		bit_f bit not null,
    	tinyint_f tinyint not null,
    	smallint_f smallint not null,
    	int_f int not null,
    		int_f2 int not null,

    	decimal_f decimal(10,4) not null,

    	float_f float not null,     -- 8-bytes
    	real_f real not null,       -- 4-bytes
    	money_f money not null,
    	smallmoney_f smallmoney not null,

    	date_f date not null,
    	time_f time not null,
    	datetime_f datetime not null,
    	smalldatetime_f smalldatetime not null,

    	binary_f binary(4) not null,
    	varbinary_f varbinary(4) not null,
    	image_f image not null
    )" );

    const sybase_procs = (
    	"find_family" :
    "create procedure find_family @name varchar(80)
    as
    select * from family where name = @name
    commit -- to maintain transaction count
    ",
    	"get_values" :
    "create procedure get_values @string varchar(80) output, @int int output
    as
    select @string = 'hello there'
    select @int = 150
    commit -- to maintain transaction count
    ",
    	"get_values_and_select" :
    "create procedure get_values_and_select @string varchar(80) output, @int int output
    as
    select @string = 'hello there'
    select @int = 150
    select * from family where family_id = 1
    commit -- to maintain transaction count
    ",
    	"get_values_and_multiple_select" :
    "create procedure get_values_and_multiple_select @string varchar(80) output, @int int output
    as
    select @string = 'hello there'
    select @int = 150
    select * from family where family_id = 1
    select * from people where person_id = 1
    commit -- to maintain transaction count
    ",
    	"just_select" :
    "create procedure just_select
    as
    select * from family where family_id = 1
    commit -- to maintain transaction count
    ",
    	"multiple_select" :
    "create procedure multiple_select
    as
    select * from family where family_id = 1
    select * from people where person_id = 1
    commit -- to maintain transaction count
    "
     );

    const freetds_sybase_tables = (
    	"family" : "create table family (
       family_id int not null,
       name varchar(80) not null
    )",
    	"people" : "create table people (
       person_id int not null,
       family_id int not null,
       name varchar(250) not null,
       dob date not null
    )",
    	"attributes" : "create table attributes (
       person_id int not null,
       attribute varchar(80) not null,
       value varchar(160) not null
    )",
    	"data_test" : "create table data_test (
    	null_f char(1) null,

    	varchar_f varchar(40) not null,
    	char_f char(40) not null,
    	text_f text not null,
    	unitext_f unitext not null, -- note that unitext is stored as 'image'

    		bit_f bit not null,
    	tinyint_f tinyint not null,
    	smallint_f smallint not null,
    	int_f int not null,
    		int_f2 int not null,

    	decimal_f decimal(10,4) not null,

    	float_f float not null,     -- 8-bytes
    	real_f real not null,       -- 4-bytes
    	money_f money not null,
    	smallmoney_f smallmoney not null,

    	date_f date not null,
    	time_f time not null,
    	datetime_f datetime not null,
    	smalldatetime_f smalldatetime not null,

    	binary_f binary(4) not null,
    	varbinary_f varbinary(4) not null,
    	image_f image not null
    )" );

    const freetds_mssql_tables = (
    	"family" : "create table family (
       family_id int not null,
       name varchar(80) not null
    )",
    	"people" : "create table people (
       person_id int not null,
       family_id int not null,
       name varchar(250) not null,
       dob datetime not null
    )",
    	"attributes" : "create table attributes (
       person_id int not null,
       attribute varchar(80) not null,
       value varchar(160) not null
    )",
    	"data_test" : "create table data_test (
    	null_f char(1) null,

    	varchar_f varchar(40) not null,
    	char_f char(40) not null,
    	text_f text not null,

    		bit_f bit not null,
    	tinyint_f tinyint not null,
    	smallint_f smallint not null,
    	int_f int not null,
    		int_f2 int not null,

    	decimal_f decimal(10,4) not null,

    	float_f float not null,     -- 8-bytes
    	real_f real not null,       -- 4-bytes
    	money_f money not null,
    	smallmoney_f smallmoney not null,

    	datetime_f datetime not null,
    	smalldatetime_f smalldatetime not null,

    	binary_f binary(4) not null,
    	varbinary_f varbinary(4) not null,
    	image_f image not null
    )" );

    sub parse_command_line()
    {
    	my $g = new GetOpt(opts);
    	$o = $g.parse(\$ARGV);
    	if ($o.help)
    	usage();

    	if (!strlen($o.db))
    	{
    	stderr.printf("set the login parameters with -u,-p,-d, etc (-h for help)\n");
    	exit(1);
    	}
    	if (elements $ARGV)
    	{
    	stderr.printf("excess arguments on command-line (%n): -h for help\n", $ARGV);
    	exit(1);
    	}
    	if (!strlen($o.type))
    	$o.type = "mysql";
    }

    sub create_datamodel($db)
    {
    	drop_test_datamodel($db);

    	my $driver = $db.getDriverName();
    	# create tables
    	my $tables = object_map.$driver.tables;
    	if ($driver == "freetds")
    	if ($db.is_sybase)
    		$tables = freetds_sybase_tables;
    		else
    		$tables = freetds_mssql_tables;

    	foreach my $table in (keys $tables)
    	{
    	tprintf(2, "creating table %n\n", $table);
    	$db.exec($tables.$table);
    	}

    	# create procedures if any
    	foreach my $proc in (keys object_map.$driver.procs)
    	{
    	tprintf(2, "creating procedure %n\n", $proc);
    	$db.exec(object_map.$driver.procs.$proc);
    	}

    	# create functions if any
    	foreach my $func in (keys object_map.$driver.funcs)
    	{
    	tprintf(2, "creating function %n\n", $func);
    	$db.exec(object_map.$driver.funcs.$func);
    	}

    	$db.exec("insert into family values ( 1, 'Smith' )");
    	$db.exec("insert into family values ( 2, 'Jones' )");

    	# we insert the dates here using binding by value so we don't have
    	# to worry about each database's specific date format
    	$db.exec("insert into people values ( 1, 1, 'Arnie', %v)", 1983-05-13);
    	$db.exec("insert into people values ( 2, 1, 'Sylvia', %v)", 1994-11-10);
    	$db.exec("insert into people values ( 3, 1, 'Carol', %v)", 2003-07-23);
    	$db.exec("insert into people values ( 4, 1, 'Bernard', %v)", 1979-02-27);
    	$db.exec("insert into people values ( 5, 1, 'Isaac', %v)", 2000-04-04);
    	$db.exec("insert into people values ( 6, 2, 'Alan', %v)", 1992-06-04);
    	$db.exec("insert into people values ( 7, 2, 'John', %v)", 1995-03-23);

    	$db.exec("insert into attributes values ( 1, 'hair', 'blond' )");
    	$db.exec("insert into attributes values ( 1, 'eyes', 'hazel' )");
    	$db.exec("insert into attributes values ( 2, 'hair', 'blond' )");
    	$db.exec("insert into attributes values ( 2, 'eyes', 'blue' )");
    	$db.exec("insert into attributes values ( 3, 'hair', 'brown' )");
    	$db.exec("insert into attributes values ( 3, 'eyes', 'grey')");
    	$db.exec("insert into attributes values ( 4, 'hair', 'brown' )");
    	$db.exec("insert into attributes values ( 4, 'eyes', 'brown' )");
    	$db.exec("insert into attributes values ( 5, 'hair', 'red' )");
    	$db.exec("insert into attributes values ( 5, 'eyes', 'green' )");
    	$db.exec("insert into attributes values ( 6, 'hair', 'black' )");
    	$db.exec("insert into attributes values ( 6, 'eyes', 'blue' )");
    	$db.exec("insert into attributes values ( 7, 'hair', 'brown' )");
    	$db.exec("insert into attributes values ( 7, 'eyes', 'brown' )");
    	$db.commit();
    }

    sub drop_test_datamodel($db)
    {
    	my $driver = $db.getDriverName();
    	# drop the tables and ignore exceptions
    	# the commits are needed for databases like postgresql, where errors will prohibit and further
    	# actions from being taken on the Datasource
    	foreach my $table in (keys object_map.$driver.tables)
    	try {
    		$db.exec("drop table " + $table);
    		$db.commit();
    		tprintf(2, "dropped table %n\n", $table);
    	}
    		catch ()
    	{
    		$db.commit();
    	}

    	# drop procedures and ignore exceptions
    	foreach my $proc in (keys object_map.$driver.procs)
    	{
    	my $cmd = object_map.$driver.drop_proc_cmd;
    	if (!exists $cmd)
    		$cmd = "drop procedure";
    	try {
    		$db.exec($cmd + " " + $proc);
    		$db.commit();
    		tprintf(2, "dropped procedure %n\n", $proc);
    	}
    	catch ()
    	{
    		$db.commit();
    	}
    	}

    	# drop functions and ignore exceptions
    	foreach my $func in (keys object_map.$driver.funcs)
    	{
    	my $cmd = object_map.$driver.drop_func_cmd;
    	if (!exists $cmd)
    		$cmd = "drop function";
    	try {
    		$db.exec($cmd + " " + $func);
    		$db.commit();
    		tprintf(2, "dropped function %n\n", $func);
    	}
    	catch ()
    	{
    		$db.commit();
    	}
    	}
    }

    sub getDS()
    {
    	my $ds = new Datasource($o.type, $o.user, $o.pass, $o.db, $o.enc);
    	if (strlen($o.host))
    	$ds.setHostName($o.host);
    	return $ds;
    }

    sub tprintf($v, $msg)
    {
    	if ($v <= $o.verbose)
    	vprintf($msg, $argv);
    }

    sub test_value($v1, $v2, $msg)
    {
    	++$test_count;
    	if ($v1 == $v2)
    	tprintf(1, "OK: %s test\n", $msg);
    	else
    	{
    		tprintf(0, "ERROR: %s test failed! (%n != %n)\n", $msg, $v1, $v2);
    		$errors++;
    	}
    }

    const family_hash = (
      "Jones" : (
    	  "people" : (
    	  "John" : (
    		  "dob" : 1995-03-23,
    		  "eyes" : "brown",
    		  "hair" : "brown" ),
    	  "Alan" : (
    		  "dob" : 1992-06-04,
    		  "eyes" : "blue",
    		  "hair" : "black" ) ) ),
    	"Smith" : (
    	"people" : (
    		"Arnie" : (
    		"dob" : 1983-05-13,
    		"eyes" : "hazel",
    		"hair" : "blond" ),
    		"Carol" : (
    		"dob" : 2003-07-23,
    		"eyes" : "grey",
    		"hair" : "brown" ),
    		"Isaac" : (
    		"dob" : 2000-04-04,
    		"eyes" : "green",
    		"hair" : "red" ),
    		"Bernard" : (
    		"dob" : 1979-02-27,
    		"eyes" : "brown",
    		"hair" : "brown" ),
    		"Sylvia" : (
    		"dob" : 1994-11-10,
    		"eyes" : "blue",
    		"hair" : "blond" ) ) ) );

    sub context_test($db)
    {
    	# first we select all the data from the tables and then use
    	# context statements to order the output hierarchically

    	# context statements are most useful when a set of queries can be executed once
    	# and the results processed many times by creating "views" with context statements

    	my $people = $db.select("select * from people");
    	my $attributes = $db.select("select * from attributes");

    	my $today = format_date("YYYYMMDD", now());

    	# in this test, we create a big hash structure out of the queries executed above
    	# and compare it at the end to the expected result

    	# display each family sorted by family name
    	my $fl;
    	context family ($db.select("select * from family")) sortBy (%name)
    	{
    	my $pl;
    	tprintf(2, "Family %d: %s\n", %family_id, %name);

    	# display people, sorted by eye color, descending
    	context people ($people)
    		sortDescendingBy (find %value in $attributes
    				  where (%attribute == "eyes"
    					 && %person_id == %people:person_id))
    		where (%family_id == %family:family_id)
    	{
    		my $al;
    		tprintf(2, "  %s, born %s\n", %name, format_date("Month DD, YYYY", %dob));
    		context ($attributes) sortBy (%attribute) where (%person_id == %people:person_id)
    		{
    		$al.%attribute = %value;
    		tprintf(2, "    has %s %s\n", %value, %attribute);
    		}
    		# leave out the ID fields and name from hash under name; subtracting a
    		# string from a hash removes that key from the result
    		# this is "doing it the hard way", there is only one key left,
    		# "dob", then attributes are added directly into the person hash
    		$pl.%name = %% - "family_id" - "person_id" - "name" + $al;
    	}
    	# leave out family_id and name fields (leaving an empty hash)
    	$fl.%name = %% - "family_id" - "name" + ( "people" : $pl );
    	}

    	# test context ordering
    	test_value(keys $fl, ("Jones", "Smith"), "first context");
    	test_value(keys $fl.Smith.people, ("Arnie", "Carol", "Isaac", "Bernard", "Sylvia"), "second context");
    	# test entire context value
    	test_value($fl, family_hash, "third context");
    }

    sub test_timeout($db, $c)
    {
    	$db.setTransactionLockTimeout(1ms);
    	try {
    	# this should cause a TRANSACTION-LOCK-TIMEOUT exception to be thrown
    	$db.exec("insert into family values (3, 'Test')\n");
    	test_value(True, False, "transaction timeout");
    	$db.exec("delete from family where name = 'Test'");
    	}
    	catch ($ex)
    	{
    	test_value(True, True, "transaction timeout");
    	}
    	# signal parent thread to continue
    	$c.dec();
    }

    sub transaction_test($db)
    {
    	my $ndb = getDS();
    	my $r;
    	tprintf(2, "db.autocommit=%N, ndb.autocommit=%N\n", $db.getAutoCommit(), $ndb.getAutoCommit());

    	# first, we insert a new row into "family" but do not commit it
    	my $rows = $db.exec("insert into family values (3, 'Test')\n");
    	if ($rows !== 1)
    	printf("FAILED INSERT, rows=%N\n", $rows);

    	# now we verify that the new row is not visible to the other datasource
    	# unless it's a sybase/ms sql server datasource, in which case this would deadlock :-(
    	if ($o.type != "sybase" && $o.type != "freetds")
    	{
    	$r = $ndb.selectRow("select name from family where family_id = 3").name;
    	test_value($r, NOTHING, "first transaction");
    	}

    	# now we verify that the new row is visible to the inserting datasource
    	$r = $db.selectRow("select name from family where family_id = 3").name;
    	test_value($r, "Test", "second transaction");

    	# test datasource timeout
    	# this Counter variable will allow the parent thread to sleep
    	# until the child thread times out
    	my $c = new Counter(1);
    	background test_timeout($db, $c);

    	# wait for child thread to time out
    	$c.waitForZero();

    	# now, we commit the transaction
    	$db.commit();

    	# now we verify that the new row is visible in the other datasource
    	$r = $ndb.selectRow("select name from family where family_id = 3").name;
    	test_value($r, "Test", "third transaction");

    	# now we delete the row we inserted (so we can repeat the test)
    	$r = $ndb.exec("delete from family where family_id = 3");
    	test_value($r, 1, "delete row count");
    	$ndb.commit();
    }

    sub oracle_test()
    {
    }

    # here we use a little workaround for modules that provide functions,
    # namespace additions (constants, classes, etc) needed by test functions
    # at parse time.  To avoid parse errors (as database modules are loaded
    # in this script at run-time when the Datasource class is instantiated)
    # we use a Program object that we parse and run on demand to return the
    # value required
    sub get_val($code)
    {
    	my $p = new Program();

    	my $str = sprintf("return %s;", $code);
    	$p.parse($str, "code");
    	return $p.run();
    }

    sub pgsql_test($db)
    {
    	my $args = ( "int2_f"          : 258,
    		 "int4_f"          : 233932,
    		 "int8_f"          : 239392939458,
    		 "bool_f"          : True,
    		 "float4_f"        : 21.3444,
    		 "float8_f"        : 49394.23423491,
    		 "number_f"        : get_val("pgsql_bind(PG_TYPE_NUMERIC, '7235634215.3250')"),
    		 "money_f"         : get_val("pgsql_bind(PG_TYPE_CASH, \"400.56\")"),
    		 "text_f"          : 'some text  ',
    		 "varchar_f"       : 'varchar ',
    		 "char_f"          : 'char text',
    		 "name_f"          : 'name',
    		 "date_f"          : 2004-01-05,
    		 "abstime_f"       : 2005-12-03T10:00:01,
    		 "reltime_f"       : 5M + 71D + 19h + 245m + 51s,
    		 "interval_f"      : 6M + 3D + 2h + 45m + 15s,
    		 "time_f"          : 11:35:00,
    		 "timetz_f"        : get_val("pgsql_bind(PG_TYPE_TIMETZ, \"11:38:21 CST\")"),
    		 "timestamp_f"     : 2005-04-01T11:35:26,
    		 "timestamptz_f"   : 2005-04-01T11:35:26.259,
    		 "tinterval_f"     : get_val("pgsql_bind(PG_TYPE_TINTERVAL, '[\"May 10, 1947 23:59:12\" \"Jan 14, 1973 03:14:21\"]')"),
    		 "bytea_f"         : <bead>
    		 #bit_f             :
    		 #varbit_f          :
    	);

    	$db.vexec("insert into data_test values (%v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v)", hash_values($args));

    	my $q = $db.selectRow("select * from data_test");
    	if ($o.verbose > 1)
    	foreach my $k in (keys $q)
    		tprintf(2, " %-16s= %-10s %N\n", $k, type($q.$k), $q.$k);

    	# fix values where we know the return type is different
    	$args.money_f = 400.56;
    	$args.timetz_f = 11:38:21;
    	$args.tinterval_f = '["1947-05-10 21:59:12" "1973-01-14 02:14:21"]';
    	$args.number_f = "7235634215.3250";
    	$args.reltime_f = 19177551s;
    	$args.interval_f = 6M + 3D + 9915s;

    	# rounding errors can happen in float4
    	$q.float4_f = round($q.float4_f);
    	$args.float4_f = round($args.float4_f);

    	# remove values where we know they won't match
    	# abstime and timestamptz are converted to GMT by the server
    	delete $q.abstime_f;
    	delete $q.timestamptz_f;

    	# compare each value
    	foreach my $k in (keys $q)
    	test_value($q.$k, $args.$k, sprintf("%s bind and retrieve", $k));

    	$db.commit();
    }

    sub mysql_test()
    {
    }

    const family_q = ( "family_id" : 1,
    		   "name" : "Smith" );
    const person_q = ( "person_id" : 1,
    		   "family_id" : 1,
    		   "name" : "Arnie",
    		   "dob" : 1983-05-13 );
    const params = ( "string" : "hello there",
    		 "int" : 150 );

    sub sybase_test($db)
    {
    	# simple stored proc test, bind by name
    	my $x = $db.exec("exec find_family %v", "Smith");
    	test_value($x, ("name": list("Smith"), "family_id" : list(1)), "simple stored proc");

    	# stored proc execute with output params
    	$x = $db.exec("declare @string varchar(40), @int int
    exec get_values :string output, :int output");
    	test_value($x, params + ("rowcount":1), "get_values");

    	# we use Datasource::selectRows() in the following queries because we
    	# get hash results instead of a hash of lists as with exec in the queries
    	# normally we should not use selectRows to execute a stored procedure,
    	# as the Datasource::selectRows() method will not grab the transaction lock,
    	# but we already called Datasource::exec() above, so we have it already.
    	# the other alternative would be to call Datasource::beginTransaction() before
    	# Datasource::selectRows()

    	# simple stored proc test, bind by name, returns hash
    	$x = $db.selectRows("exec find_family %v", "Smith");
    	test_value($x, family_q, "simple stored proc");

    	# stored proc execute with output params and select results
    	$x = $db.selectRows("declare @string varchar(40), @int int
    exec get_values_and_select :string output, :int output");
    	test_value($x, ("query":family_q,"params":params), "get_values_and_select");

    	# stored proc execute with output params and multiple select results
    	$x = $db.selectRows("declare @string varchar(40), @int int
    exec get_values_and_multiple_select :string output, :int output");
    	test_value($x, ("query":("query0":family_q,"query1":person_q),"params":params), "get_values_and_multiple_select");

    	# stored proc execute with just select results
    	$x = $db.selectRows("exec just_select");
    	test_value($x, family_q, "just_select");

    	# stored proc execute with multiple select results
    	$x = $db.selectRows("exec multiple_select");
    	test_value($x, ("query0":family_q,"query1":person_q), "multiple_select");

    	my $args = ( "null_f"          : NULL,
    		 "varchar_f"       : "varchar",
    		 "char_f"          : "char",
    		 "unichar_f"       : "unichar",
    		 "univarchar_f"    : "univarchar",
    		 "text_f"          : "test",
    		 "unitext_f"       : "test",
    		 "bit_f"           : True,
    		 "tinyint_f"       : 55,
    		 "smallint_f"      : 4285,
    		 "int_f"           : 405402,
    		 "int_f2"          : 214123498,
    		 "decimal_f"       : 500.1231,
    		 "float_f"         : 23443.234324234,
    		 "real_f"          : 213.123,
    		 "money_f"         : 3434234250.2034,
    		 "smallmoney_f"    : 211100.1012,
    		 "date_f"          : 2007-05-01,
    			 "time_f"          : 10:30:01,
    		 "datetime_f"      : 3459-01-01T11:15:02.250,
    		 "smalldatetime_f" : 2007-12-01T12:01:00,
    		 "binary_f"        : <0badbeef>,
    		 "varbinary_f"     : <feedface>,
    		 "image_f"         : <cafebead> );

    	# insert data
    	my $rows = $db.vexec("insert into data_test values (%v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %d, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v, %v)", hash_values($args));

    	my $q = $db.selectRow("select * from data_test");
    	if ($o.verbose > 1)
    	foreach my $k in (keys $q)
    		tprintf(2, " %-16s= %-10s %N\n", $k, type($q.$k), $q.$k);

    	# remove values where we know they won't match
    	# unitext_f is returned as IMAGE by the server
    	delete $q.unitext_f;
    	delete $args.unitext_f;
    	# rounding errors can happen in real
    	$q.real_f = round($q.real_f);
    	$args.real_f = round($args.real_f);

    	# compare each value
    	foreach my $k in (keys $q)
    	test_value($q.$k, $args.$k, sprintf("%s bind and retrieve", $k));

    	$db.commit();
    }

    sub freetds_test($db)
    {
    	# simple stored proc test, bind by name
    	my $x = $db.exec("exec find_family %v", "Smith");
    	test_value($x, ("name": list("Smith"), "family_id" : list(1)), "simple stored proc");

    	# we cannot retrieve parameters from newer SQL Servers with the approach we use;
    	# Microsoft changed the handling of the protocol and require us to use RPC calls,
    	# this will be implemented in the next version of qore where the "freetds" driver will
    	# be able to add custom methods to the Datasource class.  For now, we skip these tests

    	if ($db.is_sybase)
    	{
    	$x = $db.exec("declare @string varchar(40), @int int
    exec get_values :string output, :int output");
    	test_value($x, params, "get_values");
    	}

    	# we use Datasource::selectRows() in the following queries because we
    	# get hash results instead of a hash of lists as with exec in the queries
    	# normally we should not use selectRows to execute a stored procedure,
    	# as the Datasource::selectRows() method will not grab the transaction lock,
    	# but we already called Datasource::exec() above, so we have it already.
    	# the other alternative would be to call Datasource::beginTransaction() before
    	# Datasource::selectRows()

    	# simple stored proc test, bind by name, returns hash
    	$x = $db.selectRows("exec find_family %v", "Smith");
    	test_value($x, family_q, "simple stored proc");

    	# stored proc execute with output params and select results
    	if ($db.is_sybase)
    	{
    	$x = $db.selectRows("declare @string varchar(40), @int int
    exec get_values_and_select :string output, :int output");
    	test_value($x, ("query":family_q,"params":params), "get_values_and_select");

    	# stored proc execute with output params and multiple select results
    	$x = $db.selectRows("declare @string varchar(40), @int int
    exec get_values_and_multiple_select :string output, :int output");
    	test_value($x, ("query":("query0":family_q,"query1":person_q),"params":params), "get_values_and_multiple_select");
    	}

    	# stored proc execute with just select results
    	$x = $db.selectRows("exec just_select");
    	test_value($x, family_q, "just_select");

    	# stored proc execute with multiple select results
    	$x = $db.selectRows("exec multiple_select");
    	test_value($x, ("query0":family_q,"query1":person_q), "multiple_select");

    	# the freetds driver does not work with the following sybase column types:
    	# unichar, univarchar

    	my $args = ( "null_f"          : NULL,
    		 "varchar_f"       : "test",
    		 "char_f"          : "test",
    		 "text_f"          : "test",
    		 "unitext_f"       : "test",
    		 "bit_f"           : True,
    		 "tinyint_f"       : 55,
    		 "smallint_f"      : 4285,
    		 "int_f"           : 405402,
    		 "int_f2"          : 214123498,
    		 "decimal_f"       : 500.1231,
    		 "float_f"         : 23443.234324234,
    		 "real_f"          : 213.123,
    		 "money_f"         : 3434234250.2034,
    		 "smallmoney_f"    : 211100.1012,
    		 "date_f"          : 2007-05-01,
    			 "time_f"          : 10:30:01,
    		 "datetime_f"      : 3459-01-01T11:15:02.250,
    		 "smalldatetime_f" : 2007-12-01T12:01:00,
    		 "binary_f"        : <0badbeef>,
    		 "varbinary_f"     : <feedface>,
    		 "image_f"         : <cafebead> );

    	# remove fields not supported by sql server
    	if (!$db.is_sybase)
    	{
    	delete $args.unitext_f;
    	delete $args.date_f;
    	delete $args.time_f;
    	}

    	my $sql = "insert into data_test values (";
    	for (my $i; $i < elements $args; ++$i)
    	$sql += "%v, ";
    	$sql = substr($sql, 0, -2) + ")";

    	# insert data, using the values from the hash above
    	my $rows = $db.vexec($sql, hash_values($args));

    	my $q = $db.selectRow("select * from data_test");
    	if ($o.verbose > 1)
    	foreach my $k in (keys $q)
    		tprintf(2, " %-16s= %-10s %N\n", $k, type($q.$k), $q.$k);

    	# remove values where we know they won't match
    	# unitext_f is returned as IMAGE by the server
    	delete $q.unitext_f;
    	delete $args.unitext_f;
    	# rounding errors can happen in real
    	$q.real_f = round($q.real_f);
    	$args.real_f = round($args.real_f);

    	# compare each value
    	foreach my $k in (keys $q)
    	test_value($q.$k, $args.$k, sprintf("%s bind and retrieve", $k));

    	$db.commit();
    }

    sub main()
    {
    	my $test_map =
    	( "sybase" : \sybase_test(),
    	  "freetds"  : \freetds_test(),
    	  "mysql"  : \mysql_test(),
    	  "pgsql"  : \pgsql_test(),
    	  "oracle" : \oracle_test());

    	parse_command_line();
    	my $db = getDS();

    	my $driver = $db.getDriverName();
    	printf("testing %s driver\n", $driver);
    	my $sv = $db.getServerVersion();
    	if ($o.verbose > 1)
    	tprintf(2, "client version=%n\nserver version=%n\n", $db.getClientVersion(), $sv);

    	# determine if the server is a sybase or sql server dataserver
    	if ($driver == "freetds")
    	if ($sv !~ /microsoft/i)
    		$db.is_sybase = True;

    	create_datamodel($db);

    	context_test($db);
    	transaction_test($db);
    	my $test = $test_map.($db.getDriverName());
    	if (exists $test)
    	$test($db);

    	if (!$o.leave)
    	drop_test_datamodel($db);
    	printf("%d/%d tests OK\n", $test_count - $errors, $test_count);
    }

    main();