sqlite3-1.1.7.tm 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723
  1. # tdbcsqlite3.tcl --
  2. #
  3. # SQLite3 database driver for TDBC
  4. #
  5. # Copyright (c) 2008 by Kevin B. Kenny.
  6. # See the file "license.terms" for information on usage and redistribution
  7. # of this file, and for a DISCLAIMER OF ALL WARRANTIES.
  8. #
  9. # RCS: @(#) $Id: tdbcodbc.tcl,v 1.47 2008/02/27 02:08:27 kennykb Exp $
  10. #
  11. #------------------------------------------------------------------------------
  12. package require tdbc
  13. package require sqlite3
  14. package provide tdbc::sqlite3 1.1.7
  15. namespace eval tdbc::sqlite3 {
  16. namespace export connection
  17. }
  18. #------------------------------------------------------------------------------
  19. #
  20. # tdbc::sqlite3::connection --
  21. #
  22. # Class representing a SQLite3 database connection
  23. #
  24. #------------------------------------------------------------------------------
  25. ::oo::class create ::tdbc::sqlite3::connection {
  26. superclass ::tdbc::connection
  27. variable timeout
  28. variable keepcase
  29. # The constructor accepts a database name and opens the database.
  30. constructor {databaseName args} {
  31. set timeout 0
  32. set keepcase 0
  33. if {[llength $args] % 2 != 0} {
  34. set cmd [lrange [info level 0] 0 end-[llength $args]]
  35. return -code error \
  36. -errorcode {TDBC GENERAL_ERROR HY000 SQLITE3 WRONGNUMARGS} \
  37. "wrong # args, should be \"$cmd ?-option value?...\""
  38. }
  39. next
  40. sqlite3 [namespace current]::db $databaseName
  41. if {[llength $args] > 0} {
  42. my configure {*}$args
  43. }
  44. db nullvalue \ufffd
  45. }
  46. # The 'statementCreate' method forwards to the constructor of the
  47. # statement class
  48. forward statementCreate ::tdbc::sqlite3::statement create
  49. # The 'configure' method queries and sets options to the database
  50. method configure args {
  51. if {[llength $args] == 0} {
  52. # Query all configuration options
  53. set result {-encoding utf-8}
  54. lappend result -isolation
  55. if {[db onecolumn {PRAGMA read_uncommitted}]} {
  56. lappend result readuncommitted
  57. } else {
  58. lappend result serializable
  59. }
  60. lappend result -keepcase $keepcase
  61. lappend result -readonly 0
  62. lappend result -timeout $timeout
  63. return $result
  64. } elseif {[llength $args] == 1} {
  65. # Query a single option
  66. set option [lindex $args 0]
  67. if {[catch {::tcl::prefix match -message "option" {
  68. -encoding -isolation -keepcase -readonly -timeout
  69. } $option} opt]} {
  70. return -code error \
  71. -errorcode [list TDBC GENERAL_ERROR HY000 SQLITE3 \
  72. BADOPTION $option] $opt
  73. }
  74. switch -exact -- $opt {
  75. -encoding {
  76. return utf-8
  77. }
  78. -isolation {
  79. if {[db onecolumn {PRAGMA read_uncommitted}]} {
  80. return readuncommitted
  81. } else {
  82. return serializable
  83. }
  84. }
  85. -keepcase {
  86. return $keepcase
  87. }
  88. -readonly {
  89. return 0
  90. }
  91. -timeout {
  92. return $timeout
  93. }
  94. }
  95. } elseif {[llength $args] % 2 != 0} {
  96. # Syntax error
  97. set cmd [lrange [info level 0] 0 end-[llength $args]]
  98. return -code error \
  99. -errorcode [list TDBC GENERAL_ERROR HY000 \
  100. SQLITE3 WRONGNUMARGS] \
  101. "wrong # args, should be \" $cmd ?-option value?...\""
  102. }
  103. # Set one or more options
  104. foreach {option value} $args {
  105. if {[catch {::tcl::prefix match -message "option" {
  106. -encoding -isolation -keepcase -readonly -timeout
  107. } $option} opt]} {
  108. return -code error \
  109. -errorcode [list TDBC GENERAL_ERROR HY000 SQLITE3 \
  110. BADOPTION $option] $opt
  111. }
  112. switch -exact -- $opt {
  113. -encoding {
  114. if {$value ne {utf-8}} {
  115. return -code error \
  116. -errorcode [list TDBC FEATURE_NOT_SUPPORTED 0A000 \
  117. SQLITE3 ENCODING] \
  118. "-encoding not supported. SQLite3 is always\
  119. Unicode."
  120. }
  121. }
  122. -isolation {
  123. if {[catch {::tcl::prefix match -message "isolation level" {
  124. readuncommitted readcommitted repeatableread
  125. serializable readonly
  126. } $value} val]} {
  127. return -code error \
  128. -errorcode [list TDBC GENERAL_ERROR HY000 \
  129. SQLITE3 BADISOLATION $value] \
  130. $val
  131. }
  132. switch -exact -- $val {
  133. readuncommitted {
  134. db eval {PRAGMA read_uncommitted = 1}
  135. }
  136. readcommitted -
  137. repeatableread -
  138. serializable -
  139. readonly {
  140. db eval {PRAGMA read_uncommitted = 0}
  141. }
  142. }
  143. }
  144. -keepcase {
  145. if {![string is boolean -strict $value]} {
  146. return -code error \
  147. -errorcode [list TDBC DATA_EXCEPTION 22018 \
  148. SQLITE3 $value] \
  149. "expected boolean but got \"$value\""
  150. }
  151. # Normalize boolean value to 0/1
  152. set keepcase [expr {bool($value)}]
  153. }
  154. -readonly {
  155. if {$value} {
  156. return -code error \
  157. -errorcode [list TDBC FEATURE_NOT_SUPPORTED 0A000 \
  158. SQLITE3 READONLY] \
  159. "SQLite3's Tcl API does not support read-only\
  160. access"
  161. }
  162. }
  163. -timeout {
  164. if {![string is integer -strict $value]} {
  165. return -code error \
  166. -errorcode [list TDBC DATA_EXCEPTION 22018 \
  167. SQLITE3 $value] \
  168. "expected integer but got \"$value\""
  169. }
  170. db timeout $value
  171. set timeout $value
  172. }
  173. }
  174. }
  175. return
  176. }
  177. # The 'tables' method introspects on the tables in the database.
  178. method tables {{pattern %}} {
  179. set retval {}
  180. my foreach row {
  181. SELECT * from sqlite_master
  182. WHERE type IN ('table', 'view')
  183. AND name LIKE :pattern
  184. } {
  185. if {!$keepcase} {
  186. dict set row name [string tolower [dict get $row name]]
  187. }
  188. dict set retval [dict get $row name] $row
  189. }
  190. return $retval
  191. }
  192. # The 'columns' method introspects on columns of a table.
  193. method columns {table {pattern %}} {
  194. regsub -all ' $table '' table
  195. set retval {}
  196. set pattern [string map [list \
  197. * {[*]} \
  198. ? {[?]} \
  199. \[ \\\[ \
  200. \] \\\[ \
  201. _ ? \
  202. % *] [string tolower $pattern]]
  203. my foreach origrow "PRAGMA table_info('$table')" {
  204. set row {}
  205. dict for {key value} $origrow {
  206. dict set row [string tolower $key] $value
  207. }
  208. if {!$keepcase} {
  209. dict set row name [string tolower [dict get $row name]]
  210. }
  211. if {![string match $pattern \
  212. [string tolower [dict get $row name]]]} {
  213. continue
  214. }
  215. switch -regexp -matchvar info [dict get $row type] {
  216. {^(.+)\(\s*([[:digit:]]+)\s*,\s*([[:digit:]]+)\s*\)\s*$} {
  217. dict set row type \
  218. [string trim [string tolower [lindex $info 1]]]
  219. dict set row precision [lindex $info 2]
  220. dict set row scale [lindex $info 3]
  221. }
  222. {^(.+)\(\s*([[:digit:]]+)\s*\)\s*$} {
  223. dict set row type [string tolower [lindex $info 1]]
  224. dict set row precision [lindex $info 2]
  225. dict set row scale 0
  226. }
  227. default {
  228. dict set row type [string tolower [dict get $row type]]
  229. dict set row precision 0
  230. dict set row scale 0
  231. }
  232. }
  233. dict set row nullable [expr {![dict get $row notnull]}]
  234. dict set retval [dict get $row name] $row
  235. }
  236. return $retval
  237. }
  238. # The 'primarykeys' method enumerates the primary keys on a table.
  239. method primarykeys {table} {
  240. set result {}
  241. my foreach row "PRAGMA table_info($table)" {
  242. if {[dict get $row pk]} {
  243. lappend result [dict create ordinalPosition \
  244. [expr {[dict get $row cid]+1}] \
  245. columnName \
  246. [dict get $row name]]
  247. }
  248. }
  249. return $result
  250. }
  251. # The 'foreignkeys' method enumerates the foreign keys that are
  252. # declared in a table or that refer to a given table.
  253. method foreignkeys {args} {
  254. variable ::tdbc::generalError
  255. # Check arguments
  256. set argdict {}
  257. if {[llength $args] % 2 != 0} {
  258. set errorcode $generalError
  259. lappend errorcode wrongNumArgs
  260. return -code error -errorcode $errorcode \
  261. "wrong # args: should be [lrange [info level 0] 0 1]\
  262. ?-option value?..."
  263. }
  264. foreach {key value} $args {
  265. if {$key ni {-primary -foreign}} {
  266. set errorcode $generalError
  267. lappend errorcode badOption
  268. return -code error -errorcode $errorcode \
  269. "bad option \"$key\", must be -primary or -foreign"
  270. }
  271. set key [string range $key 1 end]
  272. if {[dict exists $argdict $key]} {
  273. set errorcode $generalError
  274. lappend errorcode dupOption
  275. return -code error -errorcode $errorcode \
  276. "duplicate option \"$key\" supplied"
  277. }
  278. dict set argdict $key $value
  279. }
  280. # If we know the table with the foreign key, search just its
  281. # foreign keys. Otherwise, iterate over all the tables in the
  282. # database.
  283. if {[dict exists $argdict foreign]} {
  284. return [my ForeignKeysForTable [dict get $argdict foreign] \
  285. $argdict]
  286. } else {
  287. set result {}
  288. foreach foreignTable [dict keys [my tables]] {
  289. lappend result {*}[my ForeignKeysForTable \
  290. $foreignTable $argdict]
  291. }
  292. return $result
  293. }
  294. }
  295. # The private ForeignKeysForTable method enumerates the foreign keys
  296. # in a specific table.
  297. #
  298. # Parameters:
  299. #
  300. # foreignTable - Name of the table containing foreign keys.
  301. # argdict - Dictionary that may or may not contain a key,
  302. # 'primary', whose value is the name of a table that
  303. # must hold the primary key corresponding to the foreign
  304. # key. If the 'primary' key is absent, all tables are
  305. # candidates.
  306. # Results:
  307. #
  308. # Returns the list of foreign keys that meed the specified
  309. # conditions, as a list of dictionaries, each containing the
  310. # keys, foreignConstraintName, foreignTable, foreignColumn,
  311. # primaryTable, primaryColumn, and ordinalPosition. Note that the
  312. # foreign constraint name is constructed arbitrarily, since SQLite3
  313. # does not report this information.
  314. method ForeignKeysForTable {foreignTable argdict} {
  315. set result {}
  316. set n 0
  317. # Go through the foreign keys in the given table, looking for
  318. # ones that refer to the primary table (if one is given), or
  319. # for any primary keys if none is given.
  320. my foreach row "PRAGMA foreign_key_list($foreignTable)" {
  321. if {(![dict exists $argdict primary])
  322. || ([string tolower [dict get $row table]]
  323. eq [string tolower [dict get $argdict primary]])} {
  324. # Construct a dictionary for each key, translating
  325. # SQLite names to TDBC ones and converting sequence
  326. # numbers to 1-based indexing.
  327. set rrow [dict create foreignTable $foreignTable \
  328. foreignConstraintName \
  329. ?$foreignTable?[dict get $row id]]
  330. if {[dict exists $row seq]} {
  331. dict set rrow ordinalPosition \
  332. [expr {1 + [dict get $row seq]}]
  333. }
  334. foreach {to from} {
  335. foreignColumn from
  336. primaryTable table
  337. primaryColumn to
  338. deleteAction on_delete
  339. updateAction on_update
  340. } {
  341. if {[dict exists $row $from]} {
  342. dict set rrow $to [dict get $row $from]
  343. }
  344. }
  345. # Add the newly-constucted dictionary to the result list
  346. lappend result $rrow
  347. }
  348. }
  349. return $result
  350. }
  351. # The 'preparecall' method prepares a call to a stored procedure.
  352. # SQLite3 does not have stored procedures, since it's an in-process
  353. # server.
  354. method preparecall {call} {
  355. return -code error \
  356. -errorcode [list TDBC FEATURE_NOT_SUPPORTED 0A000 \
  357. SQLITE3 PREPARECALL] \
  358. {SQLite3 does not support stored procedures}
  359. }
  360. # The 'begintransaction' method launches a database transaction
  361. method begintransaction {} {
  362. db eval {BEGIN TRANSACTION}
  363. }
  364. # The 'commit' method commits a database transaction
  365. method commit {} {
  366. db eval {COMMIT}
  367. }
  368. # The 'rollback' method abandons a database transaction
  369. method rollback {} {
  370. db eval {ROLLBACK}
  371. }
  372. # The 'transaction' method executes a script as a single transaction.
  373. # We override the 'transaction' method of the base class, since SQLite3
  374. # has a faster implementation of the same thing. (The base class's generic
  375. # method should also work.)
  376. # (Don't overload the base class method, because 'break', 'continue'
  377. # and 'return' in the transaction body don't work!)
  378. #method transaction {script} {
  379. # uplevel 1 [list {*}[namespace code db] transaction $script]
  380. #}
  381. method prepare {sqlCode} {
  382. set result [next $sqlCode]
  383. return $result
  384. }
  385. method getDBhandle {} {
  386. return [namespace which db]
  387. }
  388. }
  389. #------------------------------------------------------------------------------
  390. #
  391. # tdbc::sqlite3::statement --
  392. #
  393. # Class representing a statement to execute against a SQLite3 database
  394. #
  395. #------------------------------------------------------------------------------
  396. ::oo::class create ::tdbc::sqlite3::statement {
  397. superclass ::tdbc::statement
  398. variable Params db sql
  399. # The constructor accepts the handle to the connection and the SQL
  400. # code for the statement to prepare. All that it does is to parse the
  401. # statement and store it. The parse is used to support the
  402. # 'params' and 'paramtype' methods.
  403. constructor {connection sqlcode} {
  404. next
  405. set Params {}
  406. set db [$connection getDBhandle]
  407. set sql $sqlcode
  408. foreach token [::tdbc::tokenize $sqlcode] {
  409. if {[string index $token 0] in {$ : @}} {
  410. dict set Params [string range $token 1 end] \
  411. {type Tcl_Obj precision 0 scale 0 nullable 1 direction in}
  412. }
  413. }
  414. }
  415. # The 'resultSetCreate' method relays to the result set constructor
  416. forward resultSetCreate ::tdbc::sqlite3::resultset create
  417. # The 'params' method returns descriptions of the parameters accepted
  418. # by the statement
  419. method params {} {
  420. return $Params
  421. }
  422. # The 'paramtype' method need do nothing; Sqlite3 uses manifest typing.
  423. method paramtype args {;}
  424. method getDBhandle {} {
  425. return $db
  426. }
  427. method getSql {} {
  428. return $sql
  429. }
  430. }
  431. #-------------------------------------------------------------------------------
  432. #
  433. # tdbc::sqlite3::resultset --
  434. #
  435. # Class that represents a SQLlite result set in Tcl
  436. #
  437. #-------------------------------------------------------------------------------
  438. ::oo::class create ::tdbc::sqlite3::resultset {
  439. superclass ::tdbc::resultset
  440. # The variables of this class all have peculiar names. The reason is
  441. # that the RunQuery method needs to execute with an activation record
  442. # that has no local variables whose names could conflict with names
  443. # in the SQL query. We start the variable names with hyphens because
  444. # they can't be bind variables.
  445. variable -set {*}{
  446. -columns -db -needcolumns -resultArray
  447. -results -sql -Cursor -RowCount -END
  448. }
  449. constructor {statement args} {
  450. next
  451. set -db [$statement getDBhandle]
  452. set -sql [$statement getSql]
  453. set -columns {}
  454. set -results {}
  455. ${-db} trace [namespace code {my RecordStatement}]
  456. if {[llength $args] == 0} {
  457. # Variable substitutions are evaluated in caller's context
  458. uplevel 1 [list ${-db} eval ${-sql} \
  459. [namespace which -variable -resultArray] \
  460. [namespace code {my RecordResult}]]
  461. } elseif {[llength $args] == 1} {
  462. # Variable substitutions are in the dictionary at [lindex $args 0].
  463. set -paramDict [lindex $args 0]
  464. # At this point, the activation record must contain no variables
  465. # that might be bound within the query. All variables at this point
  466. # begin with hyphens so that they are syntactically incorrect
  467. # as bound variables in SQL.
  468. unset args
  469. unset statement
  470. dict with -paramDict {
  471. ${-db} eval ${-sql} -resultArray {
  472. my RecordResult
  473. }
  474. }
  475. } else {
  476. ${-db} trace {}
  477. # Too many args
  478. return -code error \
  479. -errorcode [list TDBC GENERAL_ERROR HY000 \
  480. SQLITE3 WRONGNUMARGS] \
  481. "wrong # args: should be\
  482. [lrange [info level 0] 0 1] statement ?dictionary?"
  483. }
  484. ${-db} trace {}
  485. set -Cursor 0
  486. if {${-Cursor} < [llength ${-results}]
  487. && [lindex ${-results} ${-Cursor}] eq {statement}} {
  488. incr -Cursor 2
  489. }
  490. if {${-Cursor} < [llength ${-results}]
  491. && [lindex ${-results} ${-Cursor}] eq {columns}} {
  492. incr -Cursor
  493. set -columns [lindex ${-results} ${-Cursor}]
  494. incr -Cursor
  495. }
  496. set -RowCount [${-db} changes]
  497. }
  498. # Record the start of a SQL statement
  499. method RecordStatement {stmt} {
  500. set -needcolumns 1
  501. lappend -results statement {}
  502. }
  503. # Record one row of results from a query by appending it as a dictionary
  504. # to the 'results' list. As a side effect, set 'columns' to a list
  505. # comprising the names of the columns of the result.
  506. method RecordResult {} {
  507. set columns ${-resultArray(*)}
  508. if {[info exists -needcolumns]} {
  509. lappend -results columns $columns
  510. unset -needcolumns
  511. }
  512. set dict {}
  513. foreach key $columns {
  514. if {[set -resultArray($key)] ne "\ufffd"} {
  515. dict set dict $key [set -resultArray($key)]
  516. }
  517. }
  518. lappend -results row $dict
  519. }
  520. # Advance to the next result set
  521. method nextresults {} {
  522. set have 0
  523. while {${-Cursor} < [llength ${-results}]} {
  524. if {[lindex ${-results} ${-Cursor}] eq {statement}} {
  525. set have 1
  526. incr -Cursor 2
  527. break
  528. }
  529. incr -Cursor 2
  530. }
  531. if {!$have} {
  532. set -END {}
  533. }
  534. if {${-Cursor} >= [llength ${-results}]} {
  535. set -columns {}
  536. } elseif {[lindex ${-results} ${-Cursor}] eq {columns}} {
  537. incr -Cursor
  538. set -columns [lindex ${-results} ${-Cursor}]
  539. incr -Cursor
  540. } else {
  541. set -columns {}
  542. }
  543. return $have
  544. }
  545. method getDBhandle {} {
  546. return ${-db}
  547. }
  548. # Return a list of the columns
  549. method columns {} {
  550. if {[info exists -END]} {
  551. return -code error \
  552. -errorcode {TDBC GENERAL_ERROR HY010 SQLITE3 FUNCTIONSEQ} \
  553. "Function sequence error: result set is exhausted."
  554. }
  555. return ${-columns}
  556. }
  557. # Return the next row of the result set as a list
  558. method nextlist var {
  559. upvar 1 $var row
  560. if {[info exists -END]} {
  561. return -code error \
  562. -errorcode {TDBC GENERAL_ERROR HY010 SQLITE3 FUNCTIONSEQ} \
  563. "Function sequence error: result set is exhausted."
  564. }
  565. if {${-Cursor} >= [llength ${-results}]
  566. || [lindex ${-results} ${-Cursor}] ne {row}} {
  567. return 0
  568. } else {
  569. set row {}
  570. incr -Cursor
  571. set d [lindex ${-results} ${-Cursor}]
  572. incr -Cursor
  573. foreach key ${-columns} {
  574. if {[dict exists $d $key]} {
  575. lappend row [dict get $d $key]
  576. } else {
  577. lappend row {}
  578. }
  579. }
  580. }
  581. return 1
  582. }
  583. # Return the next row of the result set as a dict
  584. method nextdict var {
  585. upvar 1 $var row
  586. if {[info exists -END]} {
  587. return -code error \
  588. -errorcode {TDBC GENERAL_ERROR HY010 SQLITE3 FUNCTIONSEQ} \
  589. "Function sequence error: result set is exhausted."
  590. }
  591. if {${-Cursor} >= [llength ${-results}]
  592. || [lindex ${-results} ${-Cursor}] ne {row}} {
  593. return 0
  594. } else {
  595. incr -Cursor
  596. set row [lindex ${-results} ${-Cursor}]
  597. incr -Cursor
  598. }
  599. return 1
  600. }
  601. # Return the number of rows affected by a statement
  602. method rowcount {} {
  603. if {[info exists -END]} {
  604. return -code error \
  605. -errorcode {TDBC GENERAL_ERROR HY010 SQLITE3 FUNCTIONSEQ} \
  606. "Function sequence error: result set is exhausted."
  607. }
  608. return ${-RowCount}
  609. }
  610. }