sqlite3-1.1.2.tm 19 KB

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