1# This TCL script is the main driver script for the sqlite3_checker utility
2# program.
3#
4
5# Special case:
6#
7#      sqlite3_checker --test FILENAME ARGS
8#
9# uses FILENAME in place of this script.
10#
11if {[lindex $argv 0]=="--test" && [llength $argv]>1} {
12  set ::argv0 [lindex $argv 1]
13  set argv [lrange $argv 2 end]
14  source $argv0
15  exit 0
16}
17
18# Emulate a TCL shell
19#
20proc tclsh {} {
21  set line {}
22  while {![eof stdin]} {
23    if {$line!=""} {
24      puts -nonewline "> "
25    } else {
26      puts -nonewline "% "
27    }
28    flush stdout
29    append line [gets stdin]
30    if {[info complete $line]} {
31      if {[catch {uplevel #0 $line} result]} {
32        puts stderr "Error: $result"
33      } elseif {$result!=""} {
34        puts $result
35      }
36      set line {}
37    } else {
38      append line \n
39    }
40  }
41}
42
43# Do an incremental integrity check of a single index
44#
45proc check_index {idxname batchsize bTrace} {
46  set i 0
47  set more 1
48  set nerr 0
49  set pct 00.0
50  set max [db one {SELECT nEntry FROM sqlite_btreeinfo('main')
51                    WHERE name=$idxname}]
52  puts -nonewline "$idxname: $i of $max rows ($pct%)\r"
53  flush stdout
54  if {$bTrace} {
55    set sql {SELECT errmsg, current_key AS key,
56                    CASE WHEN rowid=1 THEN scanner_sql END AS traceOut
57               FROM incremental_index_check($idxname)
58              WHERE after_key=$key
59              LIMIT $batchsize}
60  } else {
61    set sql {SELECT errmsg, current_key AS key, NULL AS traceOut
62               FROM incremental_index_check($idxname)
63              WHERE after_key=$key
64              LIMIT $batchsize}
65  }
66  while {$more} {
67    set more 0
68    db eval $sql {
69      set more 1
70      if {$errmsg!=""} {
71        incr nerr
72        puts "$idxname: key($key): $errmsg"
73      } elseif {$traceOut!=""} {
74        puts "$idxname: $traceOut"
75      }
76      incr i
77
78    }
79    set x [format {%.1f} [expr {($i*100.0)/$max}]]
80    if {$x!=$pct} {
81      puts -nonewline "$idxname: $i of $max rows ($pct%)\r"
82      flush stdout
83      set pct $x
84    }
85  }
86  puts "$idxname: $nerr errors out of $i entries"
87}
88
89# Print a usage message on standard error, then quit.
90#
91proc usage {} {
92  set argv0 [file rootname [file tail [info nameofexecutable]]]
93  puts stderr "Usage: $argv0 OPTIONS database-filename"
94  puts stderr {
95Do sanity checking on a live SQLite3 database file specified by the
96"database-filename" argument.
97
98Options:
99
100   --batchsize N     Number of rows to check per transaction
101
102   --freelist        Perform a freelist check
103
104   --index NAME      Run a check of the index NAME
105
106   --summary         Print summary information about the database
107
108   --table NAME      Run a check of all indexes for table NAME
109
110   --tclsh           Run the built-in TCL interpreter (for debugging)
111
112   --trace           (Debugging only:) Output trace information on the scan
113
114   --version         Show the version number of SQLite
115}
116  exit 1
117}
118
119set file_to_analyze {}
120append argv {}
121set bFreelistCheck 0
122set bSummary 0
123set zIndex {}
124set zTable {}
125set batchsize 1000
126set bAll 1
127set bTrace 0
128set argc [llength $argv]
129for {set i 0} {$i<$argc} {incr i} {
130  set arg [lindex $argv $i]
131  if {[regexp {^-+tclsh$} $arg]} {
132    tclsh
133    exit 0
134  }
135  if {[regexp {^-+version$} $arg]} {
136    sqlite3 mem :memory:
137    puts [mem one {SELECT sqlite_version()||' '||sqlite_source_id()}]
138    mem close
139    exit 0
140  }
141  if {[regexp {^-+freelist$} $arg]} {
142    set bFreelistCheck 1
143    set bAll 0
144    continue
145  }
146  if {[regexp {^-+summary$} $arg]} {
147    set bSummary 1
148    set bAll 0
149    continue
150  }
151  if {[regexp {^-+trace$} $arg]} {
152    set bTrace 1
153    continue
154  }
155  if {[regexp {^-+batchsize$} $arg]} {
156    incr i
157    if {$i>=$argc} {
158      puts stderr "missing argument on $arg"
159      exit 1
160    }
161    set batchsize [lindex $argv $i]
162    continue
163  }
164  if {[regexp {^-+index$} $arg]} {
165    incr i
166    if {$i>=$argc} {
167      puts stderr "missing argument on $arg"
168      exit 1
169    }
170    set zIndex [lindex $argv $i]
171    set bAll 0
172    continue
173  }
174  if {[regexp {^-+table$} $arg]} {
175    incr i
176    if {$i>=$argc} {
177      puts stderr "missing argument on $arg"
178      exit 1
179    }
180    set zTable [lindex $argv $i]
181    set bAll 0
182    continue
183  }
184  if {[regexp {^-} $arg]} {
185    puts stderr "Unknown option: $arg"
186    usage
187  }
188  if {$file_to_analyze!=""} {
189    usage
190  } else {
191    set file_to_analyze $arg
192  }
193}
194if {$file_to_analyze==""} usage
195
196# If a TCL script is specified on the command-line, then run that
197# script.
198#
199if {[file extension $file_to_analyze]==".tcl"} {
200  source $file_to_analyze
201  exit 0
202}
203
204set root_filename $file_to_analyze
205regexp {^file:(//)?([^?]*)} $file_to_analyze all x1 root_filename
206if {![file exists $root_filename]} {
207  puts stderr "No such file: $root_filename"
208  exit 1
209}
210if {![file readable $root_filename]} {
211  puts stderr "File is not readable: $root_filename"
212  exit 1
213}
214
215if {[catch {sqlite3 db $file_to_analyze} res]} {
216  puts stderr "Cannot open datababase $root_filename: $res"
217  exit 1
218}
219
220if {$bFreelistCheck || $bAll} {
221  puts -nonewline "freelist-check: "
222  flush stdout
223  db eval BEGIN
224  puts [db one {SELECT checkfreelist('main')}]
225  db eval END
226}
227if {$bSummary} {
228  set scale 0
229  set pgsz [db one {PRAGMA page_size}]
230  db eval {SELECT nPage*$pgsz AS sz, name, tbl_name
231             FROM sqlite_btreeinfo
232            WHERE type='index'
233            ORDER BY 1 DESC, name} {
234    if {$scale==0} {
235      if {$sz>10000000} {
236        set scale 1000000.0
237        set unit MB
238      } else {
239        set scale 1000.0
240        set unit KB
241      }
242    }
243    puts [format {%7.1f %s index %s of table %s} \
244            [expr {$sz/$scale}] $unit $name $tbl_name]
245  }
246}
247if {$zIndex!=""} {
248  check_index $zIndex $batchsize $bTrace
249}
250if {$zTable!=""} {
251  foreach idx [db eval {SELECT name FROM sqlite_master
252                         WHERE type='index' AND rootpage>0
253                           AND tbl_name=$zTable}] {
254    check_index $idx $batchsize $bTrace
255  }
256}
257if {$bAll} {
258  set allidx [db eval {SELECT name FROM sqlite_btreeinfo('main')
259                        WHERE type='index' AND rootpage>0
260                        ORDER BY nEntry}]
261  foreach idx $allidx {
262    check_index $idx $batchsize $bTrace
263  }
264}
265