1
1
Fork 0
mirror of https://github.com/QB64Official/qb64.git synced 2024-09-18 22:04:53 +00:00
qb64/internal/c/mingw64/opt/lib/tdbc1.0.6/tdbc.tcl

922 lines
26 KiB
Tcl
Raw Normal View History

2019-01-01 11:40:38 +00:00
# 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.
}