123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715 |
- # tdbcsqlite3.tcl --
- #
- # SQLite3 database driver for TDBC
- #
- # 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: tdbcodbc.tcl,v 1.47 2008/02/27 02:08:27 kennykb Exp $
- #
- #------------------------------------------------------------------------------
- package require tdbc
- package require sqlite3
- package provide tdbc::sqlite3 1.1.2
- namespace eval tdbc::sqlite3 {
- namespace export connection
- }
- #------------------------------------------------------------------------------
- #
- # tdbc::sqlite3::connection --
- #
- # Class representing a SQLite3 database connection
- #
- #------------------------------------------------------------------------------
- ::oo::class create ::tdbc::sqlite3::connection {
- superclass ::tdbc::connection
- variable timeout
- # The constructor accepts a database name and opens the database.
- constructor {databaseName args} {
- set timeout 0
- if {[llength $args] % 2 != 0} {
- set cmd [lrange [info level 0] 0 end-[llength $args]]
- return -code error \
- -errorcode {TDBC GENERAL_ERROR HY000 SQLITE3 WRONGNUMARGS} \
- "wrong # args, should be \"$cmd ?-option value?...\""
- }
- next
- sqlite3 [namespace current]::db $databaseName
- if {[llength $args] > 0} {
- my configure {*}$args
- }
- db nullvalue \ufffd
- }
- # The 'statementCreate' method forwards to the constructor of the
- # statement class
- forward statementCreate ::tdbc::sqlite3::statement create
- # The 'configure' method queries and sets options to the database
- method configure args {
- if {[llength $args] == 0} {
- # Query all configuration options
- set result {-encoding utf-8}
- lappend result -isolation
- if {[db onecolumn {PRAGMA read_uncommitted}]} {
- lappend result readuncommitted
- } else {
- lappend result serializable
- }
- lappend result -readonly 0
- lappend result -timeout $timeout
- return $result
- } elseif {[llength $args] == 1} {
- # Query a single option
- set option [lindex $args 0]
- switch -exact -- $option {
- -e - -en - -enc - -enco - -encod - -encodi - -encodin -
- -encoding {
- return utf-8
- }
- -i - -is - -iso - -isol - -isola - -isolat - -isolati -
- -isolatio - -isolation {
- if {[db onecolumn {PRAGMA read_uncommitted}]} {
- return readuncommitted
- } else {
- return serializable
- }
- }
- -r - -re - -rea - -read - -reado - -readon - -readonl -
- -readonly {
- return 0
- }
- -t - -ti - -tim - -time - -timeo - -timeou - -timeout {
- return $timeout
- }
- default {
- return -code error \
- -errorcode [list TDBC GENERAL_ERROR HY000 SQLITE3 \
- BADOPTION $option] \
- "bad option \"$option\": must be\
- -encoding, -isolation, -readonly or -timeout"
- }
- }
- } elseif {[llength $args] % 2 != 0} {
- # Syntax error
- set cmd [lrange [info level 0] 0 end-[llength $args]]
- return -code error \
- -errorcode [list TDBC GENERAL_ERROR HY000 \
- SQLITE3 WRONGNUMARGS] \
- "wrong # args, should be \" $cmd ?-option value?...\""
- }
- # Set one or more options
- foreach {option value} $args {
- switch -exact -- $option {
- -e - -en - -enc - -enco - -encod - -encodi - -encodin -
- -encoding {
- if {$value ne {utf-8}} {
- return -code error \
- -errorcode [list TDBC FEATURE_NOT_SUPPORTED 0A000 \
- SQLITE3 ENCODING] \
- "-encoding not supported. SQLite3 is always \
- Unicode."
- }
- }
- -i - -is - -iso - -isol - -isola - -isolat - -isolati -
- -isolatio - -isolation {
- switch -exact -- $value {
- readu - readun - readunc - readunco - readuncom -
- readuncomm - readuncommi - readuncommit -
- readuncommitt - readuncommitte - readuncommitted {
- db eval {PRAGMA read_uncommitted = 1}
- }
- readc - readco - readcom - readcomm - readcommi -
- readcommit - readcommitt - readcommitte -
- readcommitted -
- rep - repe - repea - repeat - repeata - repeatab -
- repeatabl - repeatable - repeatabler - repeatablere -
- repeatablerea - repeatablread -
- s - se - ser - seri - seria - serial - seriali -
- serializ - serializa - serializab - serializabl -
- serializable -
- reado - readon - readonl - readonly {
- db eval {PRAGMA read_uncommitted = 0}
- }
- default {
- return -code error \
- -errorcode [list TDBC GENERAL_ERROR HY000 \
- SQLITE3 BADISOLATION $value] \
- "bad isolation level \"$value\":\
- should be readuncommitted, readcommitted,\
- repeatableread, serializable, or readonly"
- }
- }
- }
- -r - -re - -rea - -read - -reado - -readon - -readonl -
- -readonly {
- if {$value} {
- return -code error \
- -errorcode [list TDBC FEATURE_NOT_SUPPORTED 0A000 \
- SQLITE3 READONLY] \
- "SQLite3's Tcl API does not support read-only\
- access"
- }
- }
- -t - -ti - -tim - -time - -timeo - -timeou - -timeout {
- if {![string is integer $value]} {
- return -code error \
- -errorcode [list TDBC DATA_EXCEPTION 22018 \
- SQLITE3 $value] \
- "expected integer but got \"$value\""
- }
- db timeout $value
- set timeout $value
- }
- default {
- return -code error \
- -errorcode [list TDBC GENERAL_ERROR HY000 \
- SQLITE3 BADOPTION $value] \
- "bad option \"$option\": must be\
- -encoding, -isolation, -readonly or -timeout"
- }
- }
- }
- return
- }
- # The 'tables' method introspects on the tables in the database.
- method tables {{pattern %}} {
- set retval {}
- my foreach row {
- SELECT * from sqlite_master
- WHERE type IN ('table', 'view')
- AND name LIKE :pattern
- } {
- dict set row name [string tolower [dict get $row name]]
- dict set retval [dict get $row name] $row
- }
- return $retval
- }
- # The 'columns' method introspects on columns of a table.
- method columns {table {pattern %}} {
- regsub -all ' $table '' table
- set retval {}
- set pattern [string map [list \
- * {[*]} \
- ? {[?]} \
- \[ \\\[ \
- \] \\\[ \
- _ ? \
- % *] [string tolower $pattern]]
- my foreach origrow "PRAGMA table_info('$table')" {
- set row {}
- dict for {key value} $origrow {
- dict set row [string tolower $key] $value
- }
- dict set row name [string tolower [dict get $row name]]
- if {![string match $pattern [dict get $row name]]} {
- continue
- }
- switch -regexp -matchvar info [dict get $row type] {
- {^(.+)\(\s*([[:digit:]]+)\s*,\s*([[:digit:]]+)\s*\)\s*$} {
- dict set row type [string tolower [lindex $info 1]]
- dict set row precision [lindex $info 2]
- dict set row scale [lindex $info 3]
- }
- {^(.+)\(\s*([[:digit:]]+)\s*\)\s*$} {
- dict set row type [string tolower [lindex $info 1]]
- dict set row precision [lindex $info 2]
- dict set row scale 0
- }
- default {
- dict set row type [string tolower [dict get $row type]]
- dict set row precision 0
- dict set row scale 0
- }
- }
- dict set row nullable [expr {![dict get $row notnull]}]
- dict set retval [dict get $row name] $row
- }
- return $retval
- }
- # The 'primarykeys' method enumerates the primary keys on a table.
- method primarykeys {table} {
- set result {}
- my foreach row "PRAGMA table_info($table)" {
- if {[dict get $row pk]} {
- lappend result [dict create ordinalPosition \
- [expr {[dict get $row cid]+1}] \
- columnName \
- [dict get $row name]]
- }
- }
- return $result
- }
- # The 'foreignkeys' method enumerates the foreign keys that are
- # declared in a table or that refer to a given table.
- 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
- }
- # If we know the table with the foreign key, search just its
- # foreign keys. Otherwise, iterate over all the tables in the
- # database.
- if {[dict exists $argdict foreign]} {
- return [my ForeignKeysForTable [dict get $argdict foreign] \
- $argdict]
- } else {
- set result {}
- foreach foreignTable [dict keys [my tables]] {
- lappend result {*}[my ForeignKeysForTable \
- $foreignTable $argdict]
- }
- return $result
- }
- }
- # The private ForeignKeysForTable method enumerates the foreign keys
- # in a specific table.
- #
- # Parameters:
- #
- # foreignTable - Name of the table containing foreign keys.
- # argdict - Dictionary that may or may not contain a key,
- # 'primary', whose value is the name of a table that
- # must hold the primary key corresponding to the foreign
- # key. If the 'primary' key is absent, all tables are
- # candidates.
- # Results:
- #
- # Returns the list of foreign keys that meed the specified
- # conditions, as a list of dictionaries, each containing the
- # keys, foreignConstraintName, foreignTable, foreignColumn,
- # primaryTable, primaryColumn, and ordinalPosition. Note that the
- # foreign constraint name is constructed arbitrarily, since SQLite3
- # does not report this information.
- method ForeignKeysForTable {foreignTable argdict} {
- set result {}
- set n 0
- # Go through the foreign keys in the given table, looking for
- # ones that refer to the primary table (if one is given), or
- # for any primary keys if none is given.
- my foreach row "PRAGMA foreign_key_list($foreignTable)" {
- if {(![dict exists $argdict primary])
- || ([string tolower [dict get $row table]]
- eq [dict get $argdict primary])} {
- # Construct a dictionary for each key, translating
- # SQLite names to TDBC ones and converting sequence
- # numbers to 1-based indexing.
- set rrow [dict create foreignTable $foreignTable \
- foreignConstraintName \
- ?$foreignTable?[dict get $row id]]
- if {[dict exists $row seq]} {
- dict set rrow ordinalPosition \
- [expr {1 + [dict get $row seq]}]
- }
- foreach {to from} {
- foreignColumn from
- primaryTable table
- primaryColumn to
- deleteAction on_delete
- updateAction on_update
- } {
- if {[dict exists $row $from]} {
- dict set rrow $to [dict get $row $from]
- }
- }
- # Add the newly-constucted dictionary to the result list
- lappend result $rrow
- }
- }
- return $result
- }
- # The 'preparecall' method prepares a call to a stored procedure.
- # SQLite3 does not have stored procedures, since it's an in-process
- # server.
- method preparecall {call} {
- return -code error \
- -errorcode [list TDBC FEATURE_NOT_SUPPORTED 0A000 \
- SQLITE3 PREPARECALL] \
- {SQLite3 does not support stored procedures}
- }
- # The 'begintransaction' method launches a database transaction
- method begintransaction {} {
- db eval {BEGIN TRANSACTION}
- }
- # The 'commit' method commits a database transaction
- method commit {} {
- db eval {COMMIT}
- }
- # The 'rollback' method abandons a database transaction
- method rollback {} {
- db eval {ROLLBACK}
- }
- # The 'transaction' method executes a script as a single transaction.
- # We override the 'transaction' method of the base class, since SQLite3
- # has a faster implementation of the same thing. (The base class's generic
- # method should also work.)
- # (Don't overload the base class method, because 'break', 'continue'
- # and 'return' in the transaction body don't work!)
- #method transaction {script} {
- # uplevel 1 [list {*}[namespace code db] transaction $script]
- #}
- method prepare {sqlCode} {
- set result [next $sqlCode]
- return $result
- }
- method getDBhandle {} {
- return [namespace which db]
- }
- }
- #------------------------------------------------------------------------------
- #
- # tdbc::sqlite3::statement --
- #
- # Class representing a statement to execute against a SQLite3 database
- #
- #------------------------------------------------------------------------------
- ::oo::class create ::tdbc::sqlite3::statement {
- superclass ::tdbc::statement
- variable Params db sql
- # The constructor accepts the handle to the connection and the SQL
- # code for the statement to prepare. All that it does is to parse the
- # statement and store it. The parse is used to support the
- # 'params' and 'paramtype' methods.
- constructor {connection sqlcode} {
- next
- set Params {}
- set db [$connection getDBhandle]
- set sql $sqlcode
- foreach token [::tdbc::tokenize $sqlcode] {
- if {[string index $token 0] in {$ : @}} {
- dict set Params [string range $token 1 end] \
- {type Tcl_Obj precision 0 scale 0 nullable 1 direction in}
- }
- }
- }
- # The 'resultSetCreate' method relays to the result set constructor
- forward resultSetCreate ::tdbc::sqlite3::resultset create
- # The 'params' method returns descriptions of the parameters accepted
- # by the statement
- method params {} {
- return $Params
- }
- # The 'paramtype' method need do nothing; Sqlite3 uses manifest typing.
- method paramtype args {;}
- method getDBhandle {} {
- return $db
- }
- method getSql {} {
- return $sql
- }
- }
- #-------------------------------------------------------------------------------
- #
- # tdbc::sqlite3::resultset --
- #
- # Class that represents a SQLlite result set in Tcl
- #
- #-------------------------------------------------------------------------------
- ::oo::class create ::tdbc::sqlite3::resultset {
- superclass ::tdbc::resultset
- # The variables of this class all have peculiar names. The reason is
- # that the RunQuery method needs to execute with an activation record
- # that has no local variables whose names could conflict with names
- # in the SQL query. We start the variable names with hyphens because
- # they can't be bind variables.
- variable -set {*}{
- -columns -db -needcolumns -resultArray
- -results -sql -Cursor -RowCount -END
- }
- constructor {statement args} {
- next
- set -db [$statement getDBhandle]
- set -sql [$statement getSql]
- set -columns {}
- set -results {}
- ${-db} trace [namespace code {my RecordStatement}]
- if {[llength $args] == 0} {
- # Variable substitutions are evaluated in caller's context
- uplevel 1 [list ${-db} eval ${-sql} \
- [namespace which -variable -resultArray] \
- [namespace code {my RecordResult}]]
- } elseif {[llength $args] == 1} {
- # Variable substitutions are in the dictionary at [lindex $args 0].
- set -paramDict [lindex $args 0]
- # At this point, the activation record must contain no variables
- # that might be bound within the query. All variables at this point
- # begin with hyphens so that they are syntactically incorrect
- # as bound variables in SQL.
- unset args
- unset statement
- dict with -paramDict {
- ${-db} eval ${-sql} -resultArray {
- my RecordResult
- }
- }
- } else {
- ${-db} trace {}
- # Too many args
- return -code error \
- -errorcode [list TDBC GENERAL_ERROR HY000 \
- SQLITE3 WRONGNUMARGS] \
- "wrong # args: should be\
- [lrange [info level 0] 0 1] statement ?dictionary?"
- }
- ${-db} trace {}
- set -Cursor 0
- if {${-Cursor} < [llength ${-results}]
- && [lindex ${-results} ${-Cursor}] eq {statement}} {
- incr -Cursor 2
- }
- if {${-Cursor} < [llength ${-results}]
- && [lindex ${-results} ${-Cursor}] eq {columns}} {
- incr -Cursor
- set -columns [lindex ${-results} ${-Cursor}]
- incr -Cursor
- }
- set -RowCount [${-db} changes]
- }
- # Record the start of a SQL statement
- method RecordStatement {stmt} {
- set -needcolumns 1
- lappend -results statement {}
- }
- # Record one row of results from a query by appending it as a dictionary
- # to the 'results' list. As a side effect, set 'columns' to a list
- # comprising the names of the columns of the result.
- method RecordResult {} {
- set columns ${-resultArray(*)}
- if {[info exists -needcolumns]} {
- lappend -results columns $columns
- unset -needcolumns
- }
- set dict {}
- foreach key $columns {
- if {[set -resultArray($key)] ne "\ufffd"} {
- dict set dict $key [set -resultArray($key)]
- }
- }
- lappend -results row $dict
- }
- # Advance to the next result set
- method nextresults {} {
- set have 0
- while {${-Cursor} < [llength ${-results}]} {
- if {[lindex ${-results} ${-Cursor}] eq {statement}} {
- set have 1
- incr -Cursor 2
- break
- }
- incr -Cursor 2
- }
- if {!$have} {
- set -END {}
- }
- if {${-Cursor} >= [llength ${-results}]} {
- set -columns {}
- } elseif {[lindex ${-results} ${-Cursor}] eq {columns}} {
- incr -Cursor
- set -columns [lindex ${-results} ${-Cursor}]
- incr -Cursor
- } else {
- set -columns {}
- }
- return $have
- }
- method getDBhandle {} {
- return ${-db}
- }
- # Return a list of the columns
- method columns {} {
- if {[info exists -END]} {
- return -code error \
- -errorcode {TDBC GENERAL_ERROR HY010 SQLITE3 FUNCTIONSEQ} \
- "Function sequence error: result set is exhausted."
- }
- return ${-columns}
- }
- # Return the next row of the result set as a list
- method nextlist var {
- upvar 1 $var row
- if {[info exists -END]} {
- return -code error \
- -errorcode {TDBC GENERAL_ERROR HY010 SQLITE3 FUNCTIONSEQ} \
- "Function sequence error: result set is exhausted."
- }
- if {${-Cursor} >= [llength ${-results}]
- || [lindex ${-results} ${-Cursor}] ne {row}} {
- return 0
- } else {
- set row {}
- incr -Cursor
- set d [lindex ${-results} ${-Cursor}]
- incr -Cursor
- foreach key ${-columns} {
- if {[dict exists $d $key]} {
- lappend row [dict get $d $key]
- } else {
- lappend row {}
- }
- }
- }
- return 1
- }
- # Return the next row of the result set as a dict
- method nextdict var {
- upvar 1 $var row
- if {[info exists -END]} {
- return -code error \
- -errorcode {TDBC GENERAL_ERROR HY010 SQLITE3 FUNCTIONSEQ} \
- "Function sequence error: result set is exhausted."
- }
- if {${-Cursor} >= [llength ${-results}]
- || [lindex ${-results} ${-Cursor}] ne {row}} {
- return 0
- } else {
- incr -Cursor
- set row [lindex ${-results} ${-Cursor}]
- incr -Cursor
- }
- return 1
- }
- # Return the number of rows affected by a statement
- method rowcount {} {
- if {[info exists -END]} {
- return -code error \
- -errorcode {TDBC GENERAL_ERROR HY010 SQLITE3 FUNCTIONSEQ} \
- "Function sequence error: result set is exhausted."
- }
- return ${-RowCount}
- }
- }
|