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();