# tdbc.tcl --
#
#	Definitions of base classes from which TDBC drivers' connections,
#	statements and result sets may inherit.
#
# Copyright (c) 2008 by Kevin B. Kenny
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
#
# RCS: @(#) $Id$
#
#------------------------------------------------------------------------------

package require TclOO

namespace eval ::tdbc {
    namespace export connection statement resultset
    variable generalError [list TDBC GENERAL_ERROR HY000 {}]
}

#------------------------------------------------------------------------------
#
# tdbc::ParseConvenienceArgs --
#
#	Parse the convenience arguments to a TDBC 'execute',
#	'executewithdictionary', or 'foreach' call.
#
# Parameters:
#	argv - Arguments to the call
#	optsVar -- Name of a variable in caller's scope that will receive
#		   a dictionary of the supplied options
#
# Results:
#	Returns any args remaining after parsing the options.
#
# Side effects:
#	Sets the 'opts' dictionary to the options.
#
#------------------------------------------------------------------------------

proc tdbc::ParseConvenienceArgs {argv optsVar} {

    variable generalError
    upvar 1 $optsVar opts

    set opts [dict create -as dicts]
    set i 0

    # Munch keyword options off the front of the command arguments

    foreach {key value} $argv {
	if {[string index $key 0] eq {-}} {
	    switch -regexp -- $key {
		-as? {
		    if {$value ne {dicts} && $value ne {lists}} {
			set errorcode $generalError
			lappend errorcode badVarType $value
			return -code error \
			    -errorcode $errorcode \
			    "bad variable type \"$value\":\
                             must be lists or dicts"
		    }
		    dict set opts -as $value
		}
		-c(?:o(?:l(?:u(?:m(?:n(?:s(?:v(?:a(?:r(?:i(?:a(?:b(?:le?)?)?)?)?)?)?)?)?)?)?)?)?) {
		    dict set opts -columnsvariable $value
		}
		-- {
		    incr i
		    break
		}
		default {
		    set errorcode $generalError
		    lappend errorcode badOption $key
		    return -code error \
			-errorcode $errorcode \
			"bad option \"$key\":\
                             must be -as or -columnsvariable"
		}
	    }
	} else {
	    break
	}
	incr i 2
    }

    return [lrange $argv[set argv {}] $i end]

}



#------------------------------------------------------------------------------
#
# tdbc::connection --
#
#	Class that represents a generic connection to a database.
#
#-----------------------------------------------------------------------------

oo::class create ::tdbc::connection {

    # statementSeq is the sequence number of the last statement created.
    # statementClass is the name of the class that implements the
    #	'statement' API.
    # primaryKeysStatement is the statement that queries primary keys
    # foreignKeysStatement is the statement that queries foreign keys

    variable statementSeq primaryKeysStatement foreignKeysStatement

    # The base class constructor accepts no arguments.  It sets up the
    # machinery to do the bookkeeping to keep track of what statements
    # are associated with the connection.  The derived class constructor
    # is expected to set the variable, 'statementClass' to the name
    # of the class that represents statements, so that the 'prepare'
    # method can invoke it.

    constructor {} {
	set statementSeq 0
	namespace eval Stmt {}
    }

    # The 'close' method is simply an alternative syntax for destroying
    # the connection.

    method close {} {
	my destroy
    }

    # The 'prepare' method creates a new statement against the connection,
    # giving its constructor the current statement and the SQL code to
    # prepare.  It uses the 'statementClass' variable set by the constructor
    # to get the class to instantiate.

    method prepare {sqlcode} {
	return [my statementCreate Stmt::[incr statementSeq] [self] $sqlcode]
    }

    # The 'statementCreate' method delegates to the constructor
    # of the class specified by the 'statementClass' variable. It's
    # intended for drivers designed before tdbc 1.0b10. Current ones
    # should forward this method to the constructor directly.

    method statementCreate {name instance sqlcode} {
	my variable statementClass
	return [$statementClass create $name $instance $sqlcode]
    }

    # Derived classes are expected to implement the 'prepareCall' method,
    # and have it call 'prepare' as needed (or do something else and
    # install the resulting statement)

    # The 'statements' method lists the statements active against this
    # connection.

    method statements {} {
	info commands Stmt::*
    }

    # The 'resultsets' method lists the result sets active against this
    # connection.

    method resultsets {} {
	set retval {}
	foreach statement [my statements] {
	    foreach resultset [$statement resultsets] {
		lappend retval $resultset
	    }
	}
	return $retval
    }

    # The 'transaction' method executes a block of Tcl code as an
    # ACID transaction against the database.

    method transaction {script} {
	my begintransaction
	set status [catch {uplevel 1 $script} result options]
	if {$status in {0 2 3 4}} {
	    set status2 [catch {my commit} result2 options2]
	    if {$status2 == 1} {
		set status 1
		set result $result2
		set options $options2
	    }
	}
	switch -exact -- $status {
	    0 {
		# do nothing
	    }
	    2 - 3 - 4 {
		set options [dict merge {-level 1} $options[set options {}]]
		dict incr options -level
	    }
	    default {
		my rollback
	    }
	}
	return -options $options $result
    }

    # The 'allrows' method prepares a statement, then executes it with
    # a given set of substituents, returning a list of all the rows
    # that the statement returns. Optionally, it stores the names of
    # the columns in '-columnsvariable'.
    # Usage:
    #     $db allrows ?-as lists|dicts? ?-columnsvariable varName? ?--?
    #	      sql ?dictionary?

    method allrows args {

	variable ::tdbc::generalError

	# Grab keyword-value parameters

	set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts]

	# Check postitional parameters

	set cmd [list [self] prepare]
	if {[llength $args] == 1} {
	    set sqlcode [lindex $args 0]
	} elseif {[llength $args] == 2} {
	    lassign $args sqlcode dict
	} else {
	    set errorcode $generalError
	    lappend errorcode wrongNumArgs
	    return -code error -errorcode $errorcode \
		"wrong # args: should be [lrange [info level 0] 0 1]\
                 ?-option value?... ?--? sqlcode ?dictionary?"
	}
	lappend cmd $sqlcode

	# Prepare the statement

	set stmt [uplevel 1 $cmd]

	# Delegate to the statement to accumulate the results

	set cmd [list $stmt allrows {*}$opts --]
	if {[info exists dict]} {
	    lappend cmd $dict
	}
	set status [catch {
	    uplevel 1 $cmd
	} result options]

	# Destroy the statement

	catch {
	    $stmt close
	}

	return -options $options $result
    }

    # The 'foreach' method prepares a statement, then executes it with
    # a supplied set of substituents.  For each row of the result,
    # it sets a variable to the row and invokes a script in the caller's
    # scope.
    #
    # Usage:
    #     $db foreach ?-as lists|dicts? ?-columnsVariable varName? ?--?
    #         varName sql ?dictionary? script

    method foreach args {

	variable ::tdbc::generalError

	# Grab keyword-value parameters

	set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts]

	# Check postitional parameters

	set cmd [list [self] prepare]
	if {[llength $args] == 3} {
	    lassign $args varname sqlcode script
	} elseif {[llength $args] == 4} {
	    lassign $args varname sqlcode dict script
	} else {
	    set errorcode $generalError
	    lappend errorcode wrongNumArgs
	    return -code error -errorcode $errorcode \
		"wrong # args: should be [lrange [info level 0] 0 1]\
                 ?-option value?... ?--? varname sqlcode ?dictionary? script"
	}
	lappend cmd $sqlcode

	# Prepare the statement

	set stmt [uplevel 1 $cmd]

	# Delegate to the statement to iterate over the results

	set cmd [list $stmt foreach {*}$opts -- $varname]
	if {[info exists dict]} {
	    lappend cmd $dict
	}
	lappend cmd $script
	set status [catch {
	    uplevel 1 $cmd
	} result options]

	# Destroy the statement

	catch {
	    $stmt close
	}

	# Adjust return level in the case that the script [return]s

	if {$status == 2} {
	    set options [dict merge {-level 1} $options[set options {}]]
	    dict incr options -level
	}
	return -options $options $result
    }

    # The 'BuildPrimaryKeysStatement' method builds a SQL statement to
    # retrieve the primary keys from a database. (It executes once the
    # first time the 'primaryKeys' method is executed, and retains the
    # prepared statement for reuse.)

    method BuildPrimaryKeysStatement {} {

	# On some databases, CONSTRAINT_CATALOG is always NULL and
	# JOINing to it fails. Check for this case and include that
	# JOIN only if catalog names are supplied.

	set catalogClause {}
	if {[lindex [set count [my allrows -as lists {
	    SELECT COUNT(*)
            FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
            WHERE CONSTRAINT_CATALOG IS NOT NULL}]] 0 0] != 0} {
	    set catalogClause \
		{AND xtable.CONSTRAINT_CATALOG = xcolumn.CONSTRAINT_CATALOG}
	}
	set primaryKeysStatement [my prepare "
	     SELECT xtable.TABLE_SCHEMA AS \"tableSchema\",
                 xtable.TABLE_NAME AS \"tableName\",
                 xtable.CONSTRAINT_CATALOG AS \"constraintCatalog\",
                 xtable.CONSTRAINT_SCHEMA AS \"constraintSchema\",
                 xtable.CONSTRAINT_NAME AS \"constraintName\",
                 xcolumn.COLUMN_NAME AS \"columnName\",
                 xcolumn.ORDINAL_POSITION AS \"ordinalPosition\"
             FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS xtable
             INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE xcolumn
                     ON xtable.CONSTRAINT_SCHEMA = xcolumn.CONSTRAINT_SCHEMA
                    AND xtable.TABLE_NAME = xcolumn.TABLE_NAME
                    AND xtable.CONSTRAINT_NAME = xcolumn.CONSTRAINT_NAME
	            $catalogClause
             WHERE xtable.TABLE_NAME = :tableName
               AND xtable.CONSTRAINT_TYPE = 'PRIMARY KEY'
  	"]
    }

    # The default implementation of the 'primarykeys' method uses the
    # SQL INFORMATION_SCHEMA to retrieve primary key information. Databases
    # that might not have INFORMATION_SCHEMA must overload this method.

    method primarykeys {tableName} {
	if {![info exists primaryKeysStatement]} {
	    my BuildPrimaryKeysStatement
	}
	tailcall $primaryKeysStatement allrows [list tableName $tableName]
    }

    # The 'BuildForeignKeysStatements' method builds a SQL statement to
    # retrieve the foreign keys from a database. (It executes once the
    # first time the 'foreignKeys' method is executed, and retains the
    # prepared statements for reuse.)

    method BuildForeignKeysStatement {} {

	# On some databases, CONSTRAINT_CATALOG is always NULL and
	# JOINing to it fails. Check for this case and include that
	# JOIN only if catalog names are supplied.

	set catalogClause1 {}
	set catalogClause2 {}
	if {[lindex [set count [my allrows -as lists {
	    SELECT COUNT(*)
            FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
            WHERE CONSTRAINT_CATALOG IS NOT NULL}]] 0 0] != 0} {
	    set catalogClause1 \
		{AND fkc.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG}
	    set catalogClause2 \
		{AND pkc.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG}
	}

	foreach {exists1 clause1} {
	    0 {}
	    1 { AND pkc.TABLE_NAME = :primary}
	} {
	    foreach {exists2 clause2} {
		0 {}
		1 { AND fkc.TABLE_NAME = :foreign}
	    } {
		set stmt [my prepare "
	     SELECT rc.CONSTRAINT_CATALOG AS \"foreignConstraintCatalog\",
                    rc.CONSTRAINT_SCHEMA AS \"foreignConstraintSchema\",
                    rc.CONSTRAINT_NAME AS \"foreignConstraintName\",
                    rc.UNIQUE_CONSTRAINT_CATALOG
                        AS \"primaryConstraintCatalog\",
                    rc.UNIQUE_CONSTRAINT_SCHEMA AS \"primaryConstraintSchema\",
                    rc.UNIQUE_CONSTRAINT_NAME AS \"primaryConstraintName\",
                    rc.UPDATE_RULE AS \"updateAction\",
		    rc.DELETE_RULE AS \"deleteAction\",
                    pkc.TABLE_CATALOG AS \"primaryCatalog\",
                    pkc.TABLE_SCHEMA AS \"primarySchema\",
                    pkc.TABLE_NAME AS \"primaryTable\",
                    pkc.COLUMN_NAME AS \"primaryColumn\",
                    fkc.TABLE_CATALOG AS \"foreignCatalog\",
                    fkc.TABLE_SCHEMA AS \"foreignSchema\",
                    fkc.TABLE_NAME AS \"foreignTable\",
                    fkc.COLUMN_NAME AS \"foreignColumn\",
                    pkc.ORDINAL_POSITION AS \"ordinalPosition\"
             FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
             INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE fkc
                     ON fkc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
                    AND fkc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
                    $catalogClause1
             INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE pkc
                     ON pkc.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME
                     AND pkc.CONSTRAINT_SCHEMA = rc.UNIQUE_CONSTRAINT_SCHEMA
                     $catalogClause2
                     AND pkc.ORDINAL_POSITION = fkc.ORDINAL_POSITION
             WHERE 1=1
                 $clause1
                 $clause2
             ORDER BY \"foreignConstraintCatalog\", \"foreignConstraintSchema\", \"foreignConstraintName\", \"ordinalPosition\"
"]
		dict set foreignKeysStatement $exists1 $exists2 $stmt
	    }
	}
    }

    # The default implementation of the 'foreignkeys' method uses the
    # SQL INFORMATION_SCHEMA to retrieve primary key information. Databases
    # that might not have INFORMATION_SCHEMA must overload this method.

    method foreignkeys {args} {

	variable ::tdbc::generalError

	# Check arguments

	set argdict {}
	if {[llength $args] % 2 != 0} {
	    set errorcode $generalError
	    lappend errorcode wrongNumArgs
	    return -code error -errorcode $errorcode \
		"wrong # args: should be [lrange [info level 0] 0 1]\
                 ?-option value?..."
	}
	foreach {key value} $args {
	    if {$key ni {-primary -foreign}} {
		set errorcode $generalError
		lappend errorcode badOption
		return -code error -errorcode $errorcode \
		    "bad option \"$key\", must be -primary or -foreign"
	    }
	    set key [string range $key 1 end]
	    if {[dict exists $argdict $key]} {
		set errorcode $generalError
		lappend errorcode dupOption
		return -code error -errorcode $errorcode \
		    "duplicate option \"$key\" supplied"
	    }
	    dict set argdict $key $value
	}

	# Build the statements that query foreign keys. There are four
	# of them, one for each combination of whether -primary
	# and -foreign is specified.

	if {![info exists foreignKeysStatement]} {
	    my BuildForeignKeysStatement
	}
	set stmt [dict get $foreignKeysStatement \
		      [dict exists $argdict primary] \
		      [dict exists $argdict foreign]]
	tailcall $stmt allrows $argdict
    }

    # Derived classes are expected to implement the 'begintransaction',
    # 'commit', and 'rollback' methods.

    # Derived classes are expected to implement 'tables' and 'columns' method.

}

#------------------------------------------------------------------------------
#
# Class: tdbc::statement
#
#	Class that represents a SQL statement in a generic database
#
#------------------------------------------------------------------------------

oo::class create tdbc::statement {

    # resultSetSeq is the sequence number of the last result set created.
    # resultSetClass is the name of the class that implements the 'resultset'
    #	API.

    variable resultSetClass resultSetSeq

    # The base class constructor accepts no arguments.  It initializes
    # the machinery for tracking the ownership of result sets. The derived
    # constructor is expected to invoke the base constructor, and to
    # set a variable 'resultSetClass' to the fully-qualified name of the
    # class that represents result sets.

    constructor {} {
	set resultSetSeq 0
	namespace eval ResultSet {}
    }

    # The 'execute' method on a statement runs the statement with
    # a particular set of substituted variables.  It actually works
    # by creating the result set object and letting that objects
    # constructor do the work of running the statement.  The creation
    # is wrapped in an [uplevel] call because the substitution proces
    # may need to access variables in the caller's scope.

    # WORKAROUND: Take out the '0 &&' from the next line when
    # Bug 2649975 is fixed
    if {0 && [package vsatisfies [package provide Tcl] 8.6]} {
	method execute args {
	    tailcall my resultSetCreate \
		[namespace current]::ResultSet::[incr resultSetSeq]  \
		[self] {*}$args
	}
    } else {
	method execute args {
	    return \
		[uplevel 1 \
		     [list \
			  [self] resultSetCreate \
			  [namespace current]::ResultSet::[incr resultSetSeq] \
			  [self] {*}$args]]
	}
    }

    # The 'ResultSetCreate' method is expected to be a forward to the
    # appropriate result set constructor. If it's missing, the driver must
    # have been designed for tdbc 1.0b9 and earlier, and the 'resultSetClass'
    # variable holds the class name.

    method resultSetCreate {name instance args} {
	return [uplevel 1 [list $resultSetClass create \
			       $name $instance {*}$args]]
    }

    # The 'resultsets' method returns a list of result sets produced by
    # the current statement

    method resultsets {} {
	info commands ResultSet::*
    }

    # The 'allrows' method executes a statement with a given set of
    # substituents, and returns a list of all the rows that the statement
    # returns.  Optionally, it stores the names of columns in
    # '-columnsvariable'.
    #
    # Usage:
    #	$statement allrows ?-as lists|dicts? ?-columnsvariable varName? ?--?
    #		?dictionary?


    method allrows args {

	variable ::tdbc::generalError

	# Grab keyword-value parameters

	set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts]

	# Check postitional parameters

	set cmd [list [self] execute]
	if {[llength $args] == 0} {
	    # do nothing
	} elseif {[llength $args] == 1} {
	    lappend cmd [lindex $args 0]
	} else {
	    set errorcode $generalError
	    lappend errorcode wrongNumArgs
	    return -code error -errorcode $errorcode \
		"wrong # args: should be [lrange [info level 0] 0 1]\
                 ?-option value?... ?--? ?dictionary?"
	}

	# Get the result set

	set resultSet [uplevel 1 $cmd]

	# Delegate to the result set's [allrows] method to accumulate
	# the rows of the result.

	set cmd [list $resultSet allrows {*}$opts]
	set status [catch {
	    uplevel 1 $cmd
	} result options]

	# Destroy the result set

	catch {
	    rename $resultSet {}
	}

	# Adjust return level in the case that the script [return]s

	if {$status == 2} {
	    set options [dict merge {-level 1} $options[set options {}]]
	    dict incr options -level
	}
	return -options $options $result
    }

    # The 'foreach' method executes a statement with a given set of
    # substituents.  It runs the supplied script, substituting the supplied
    # named variable. Optionally, it stores the names of columns in
    # '-columnsvariable'.
    #
    # Usage:
    #	$statement foreach ?-as lists|dicts? ?-columnsvariable varName? ?--?
    #		variableName ?dictionary? script

    method foreach args {

	variable ::tdbc::generalError

	# Grab keyword-value parameters

	set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts]

	# Check positional parameters

	set cmd [list [self] execute]
	if {[llength $args] == 2} {
	    lassign $args varname script
	} elseif {[llength $args] == 3} {
	    lassign $args varname dict script
	    lappend cmd $dict
	} else {
	    set errorcode $generalError
	    lappend errorcode wrongNumArgs
	    return -code error -errorcode $errorcode \
		"wrong # args: should be [lrange [info level 0] 0 1]\
                 ?-option value?... ?--? varName ?dictionary? script"
	}

	# Get the result set

	set resultSet [uplevel 1 $cmd]

	# Delegate to the result set's [foreach] method to evaluate
	# the script for each row of the result.

	set cmd [list $resultSet foreach {*}$opts -- $varname $script]
	set status [catch {
	    uplevel 1 $cmd
	} result options]

	# Destroy the result set

	catch {
	    rename $resultSet {}
	}

	# Adjust return level in the case that the script [return]s

	if {$status == 2} {
	    set options [dict merge {-level 1} $options[set options {}]]
	    dict incr options -level
	}
	return -options $options $result
    }

    # The 'close' method is syntactic sugar for invoking the destructor

    method close {} {
	my destroy
    }

    # Derived classes are expected to implement their own constructors,
    # plus the following methods:

    # paramtype paramName ?direction? type ?scale ?precision??
    #     Declares the type of a parameter in the statement

}

#------------------------------------------------------------------------------
#
# Class: tdbc::resultset
#
#	Class that represents a result set in a generic database.
#
#------------------------------------------------------------------------------

oo::class create tdbc::resultset {

    constructor {} { }

    # The 'allrows' method returns a list of all rows that a given
    # result set returns.

    method allrows args {

	variable ::tdbc::generalError

	# Parse args

	set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts]
	if {[llength $args] != 0} {
	    set errorcode $generalError
	    lappend errorcode wrongNumArgs
	    return -code error -errorcode $errorcode \
		"wrong # args: should be [lrange [info level 0] 0 1]\
                 ?-option value?... ?--? varName script"
	}

	# Do -columnsvariable if requested

	if {[dict exists $opts -columnsvariable]} {
	    upvar 1 [dict get $opts -columnsvariable] columns
	}

	# Assemble the results

	if {[dict get $opts -as] eq {lists}} {
	    set delegate nextlist
	} else {
	    set delegate nextdict
	}
	set results [list]
	while {1} {
	    set columns [my columns]
	    while {[my $delegate row]} {
		lappend results $row
	    }
	    if {![my nextresults]} break
	}
	return $results

    }

    # The 'foreach' method runs a script on each row from a result set.

    method foreach args {

	variable ::tdbc::generalError

	# Grab keyword-value parameters

	set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts]

	# Check positional parameters

	if {[llength $args] != 2} {
	    set errorcode $generalError
	    lappend errorcode wrongNumArgs
	    return -code error -errorcode $errorcode \
		"wrong # args: should be [lrange [info level 0] 0 1]\
                 ?-option value?... ?--? varName script"
	}

	# Do -columnsvariable if requested

	if {[dict exists $opts -columnsvariable]} {
	    upvar 1 [dict get $opts -columnsvariable] columns
	}

	# Iterate over the groups of results
	while {1} {

	    # Export column names to caller

	    set columns [my columns]

	    # Iterate over the rows of one group of results

	    upvar 1 [lindex $args 0] row
	    if {[dict get $opts -as] eq {lists}} {
		set delegate nextlist
	    } else {
		set delegate nextdict
	    }
	    while {[my $delegate row]} {
		set status [catch {
		    uplevel 1 [lindex $args 1]
		} result options]
		switch -exact -- $status {
		    0 - 4 {	# OK or CONTINUE
		    }
		    2 {		# RETURN
			set options \
			    [dict merge {-level 1} $options[set options {}]]
			dict incr options -level
			return -options $options $result
		    }
		    3 {		# BREAK
			set broken 1
			break
		    }
		    default {	# ERROR or unknown status
			return -options $options $result
		    }
		}
	    }

	    # Advance to the next group of results if there is one

	    if {[info exists broken] || ![my nextresults]} {
		break
	    }
	}

	return
    }


    # The 'nextrow' method retrieves a row in the form of either
    # a list or a dictionary.

    method nextrow {args} {

	variable ::tdbc::generalError

	set opts [dict create -as dicts]
	set i 0

	# Munch keyword options off the front of the command arguments

	foreach {key value} $args {
	    if {[string index $key 0] eq {-}} {
		switch -regexp -- $key {
		    -as? {
			dict set opts -as $value
		    }
		    -- {
			incr i
			break
		    }
		    default {
			set errorcode $generalError
			lappend errorcode badOption $key
			return -code error -errorcode $errorcode \
			    "bad option \"$key\":\
                             must be -as or -columnsvariable"
		    }
		}
	    } else {
		break
	    }
	    incr i 2
	}

	set args [lrange $args $i end]
	if {[llength $args] != 1} {
	    set errorcode $generalError
	    lappend errorcode wrongNumArgs
	    return -code error -errorcode $errorcode \
		"wrong # args: should be [lrange [info level 0] 0 1]\
                 ?-option value?... ?--? varName"
	}
	upvar 1 [lindex $args 0] row
	if {[dict get $opts -as] eq {lists}} {
	    set delegate nextlist
	} else {
	    set delegate nextdict
	}
	return [my $delegate row]
    }

    # Derived classes must override 'nextresults' if a single
    # statement execution can yield multiple sets of results

    method nextresults {} {
	return 0
    }

    # Derived classes must override 'outputparams' if statements can
    # have output parameters.

    method outputparams {} {
	return {}
    }

    # The 'close' method is syntactic sugar for destroying the result set.

    method close {} {
	my destroy
    }

    # Derived classes are expected to implement the following methods:

    # constructor and destructor.
    #        Constructor accepts a statement and an optional
    #        a dictionary of substituted parameters  and
    #        executes the statement against the database. If
    #	     the dictionary is not supplied, then the default
    #	     is to get params from variables in the caller's scope).
    # columns
    #     -- Returns a list of the names of the columns in the result.
    # nextdict variableName
    #     -- Stores the next row of the result set in the given variable
    #        in caller's scope, in the form of a dictionary that maps
    #	     column names to values.
    # nextlist variableName
    #     -- Stores the next row of the result set in the given variable
    #        in caller's scope, in the form of a list of cells.
    # rowcount
    #     -- Returns a count of rows affected by the statement, or -1
    #        if the count of rows has not been determined.

}