if {[catch { set ::VERBOSE 0 proc usage {} { puts stderr "Usage: $::argv0 ?SWITCHES? DATABASE/SCHEMA" puts stderr " Switches are:" puts stderr " -select SQL (recommend indexes for SQL statement)" puts stderr " -verbose (increase verbosity of output)" puts stderr " -test (run internal tests and then exit)" puts stderr "" exit } # Return the quoted version of identfier $id. Quotes are only added if # they are required by SQLite. # # This command currently assumes that quotes are required if the # identifier contains any ASCII-range characters that are not # alpha-numeric or underscores. # proc quote {id} { if {[requires_quote $id]} { set x [string map {\" \"\"} $id] return "\"$x\"" } return $id } proc requires_quote {id} { foreach c [split $id {}] { if {[string is alnum $c]==0 && $c!="_"} { return 1 } } return 0 } # The argument passed to this command is a Tcl list of identifiers. The # value returned is the same list, except with each item quoted and the # elements comma-separated. # proc list_to_sql {L} { set ret [list] foreach l $L { lappend ret [quote $l] } join $ret ", " } proc readfile {zFile} { set fd [open $zFile] set data [read $fd] close $fd return $data } proc process_cmdline_args {ctxvar argv} { upvar $ctxvar G set nArg [llength $argv] set G(database) [lindex $argv end] for {set i 0} {$i < [llength $argv]-1} {incr i} { set k [lindex $argv $i] switch -- $k { -select { incr i if {$i>=[llength $argv]-1} usage set zSelect [lindex $argv $i] if {[file readable $zSelect]} { lappend G(lSelect) [readfile $zSelect] } else { lappend G(lSelect) $zSelect } } -verbose { set ::VERBOSE 1 } -test { sqlidx_internal_tests } default { usage } } } if {$G(database)=="-test"} { sqlidx_internal_tests } } proc open_database {ctxvar} { upvar $ctxvar G sqlite3 db "" # Check if the "database" file is really an SQLite database. If so, copy # it into the temp db just opened. Otherwise, assume that it is an SQL # schema and execute it directly. set fd [open $G(database)] set hdr [read $fd 16] if {$hdr == "SQLite format 3\000"} { close $fd sqlite3 db2 $G(database) sqlite3_backup B db main db2 main B step 2000000000 set rc [B finish] db2 close if {$rc != "SQLITE_OK"} { error "Failed to load database $G(database)" } } else { append hdr [read $fd] db eval $hdr close $fd } } proc analyze_selects {ctxvar} { upvar $ctxvar G set G(trace) "" # Collect a line of xTrace output for each loop in the set of SELECT # statements. proc xTrace {zMsg} { upvar G G lappend G(trace) $zMsg } db trace xTrace foreach s $G(lSelect) { set stmt [sqlite3_prepare_v2 db $s -1 dummy] set rc [sqlite3_finalize $stmt] if {$rc!="SQLITE_OK"} { error "Failed to compile SQL: [sqlite3_errmsg db]" } } db trace "" if {$::VERBOSE} { foreach t $G(trace) { puts "trace: $t" } } # puts $G(trace) } # The argument is a list of the form: # # key1 {value1.1 value1.2} key2 {value2.1 value 2.2...} # # Values lists may be of any length greater than zero. This function returns # a list of lists created by pivoting on each values list. i.e. a list # consisting of the elements: # # {{key1 value1.1} {key2 value2.1}} # {{key1 value1.2} {key2 value2.1}} # {{key1 value1.1} {key2 value2.2}} # {{key1 value1.2} {key2 value2.2}} # proc expand_eq_list {L} { set ll [list {}] for {set i 0} {$i < [llength $L]} {incr i 2} { set key [lindex $L $i] set new [list] foreach piv [lindex $L $i+1] { foreach l $ll { lappend new [concat $l [list [list $key $piv]]] } } set ll $new } return $ll } #-------------------------------------------------------------------------- # Formulate a CREATE INDEX statement that creates an index on table $tname. # proc eqset_to_index {ctxvar aCollVar tname eqset {range {}}} { upvar $ctxvar G upvar $aCollVar aColl set rangeset [list] foreach e [lsort $eqset] { lappend rangeset [lindex $e 0] [lindex $e 1] ASC } set rangeset [concat $rangeset $range] set lCols [list] set idxname $tname foreach {c collate dir} $rangeset { append idxname "_$c" set coldef [quote $c] if {[string compare -nocase $collate $aColl($c)]!=0} { append idxname [string tolower $collate] append coldef " COLLATE [quote $collate]" } if {$dir=="DESC"} { append coldef " DESC" append idxname "desc" } lappend lCols $coldef } set create_index "CREATE INDEX [quote $idxname] ON [quote $tname](" append create_index [join $lCols ", "] append create_index ");" set G(trial.$idxname) $create_index } proc expand_or_cons {L} { set lRet [list [list]] foreach elem $L { set type [lindex $elem 0] if {$type=="eq" || $type=="range"} { set lNew [list] for {set i 0} {$i < [llength $lRet]} {incr i} { lappend lNew [concat [lindex $lRet $i] [list $elem]] } set lRet $lNew } elseif {$type=="or"} { set lNew [list] foreach branch [lrange $elem 1 end] { foreach b [expand_or_cons $branch] { for {set i 0} {$i < [llength $lRet]} {incr i} { lappend lNew [concat [lindex $lRet $i] $b] } } } set lRet $lNew } } return $lRet } #-------------------------------------------------------------------------- # Argument $tname is the name of a table in the main database opened by # database handle [db]. $arrayvar is the name of an array variable in the # caller's context. This command populates the array with an entry mapping # from column name to default collation sequence for each column of table # $tname. For example, if a table is declared: # # CREATE TABLE t1(a COLLATE nocase, b, c COLLATE binary) # # the mapping is populated with: # # map(a) -> "nocase" # map(b) -> "binary" # map(c) -> "binary" # proc sqlidx_get_coll_map {tname arrayvar} { upvar $arrayvar aColl set colnames [list] set qname [quote $tname] db eval "PRAGMA table_info = $qname" x { lappend colnames $x(name) } db eval "CREATE INDEX schemalint_test ON ${qname}([list_to_sql $colnames])" db eval "PRAGMA index_xinfo = schemalint_test" x { set aColl($x(name)) $x(coll) } db eval "DROP INDEX schemalint_test" } proc find_trial_indexes {ctxvar} { upvar $ctxvar G foreach t $G(trace) { set tname [lindex $t 0] catch { array unset mask } # Invoke "PRAGMA table_info" on the table. Use the results to create # an array mapping from column name to collation sequence. Store the # array in local variable aColl. # sqlidx_get_coll_map $tname aColl set orderby [list] if {[lindex $t end 0]=="orderby"} { set orderby [lrange [lindex $t end] 1 end] } foreach lCons [expand_or_cons [lrange $t 2 end]] { # Populate the array mask() so that it contains an entry for each # combination of prerequisite scans that may lead to distinct sets # of constraints being usable. # catch { array unset mask } set mask(0) 1 foreach a $lCons { set type [lindex $a 0] if {$type=="eq" || $type=="range"} { set m [lindex $a 3] foreach k [array names mask] { set mask([expr ($k & $m)]) 1 } set mask($m) 1 } } # Loop once for each distinct prerequisite scan mask identified in # the previous block. # foreach k [array names mask] { # Identify the constraints available for prerequisite mask $k. For # each == constraint, set an entry in the eq() array as follows: # # set eq(