Merge lp:~percona-toolkit-dev/percona-toolkit/detect-key-len-with-range-scan into lp:percona-toolkit/2.1
- detect-key-len-with-range-scan
- Merge into 2.1
Proposed by
Daniel Nichter
Status: | Merged |
---|---|
Merged at revision: | 284 |
Proposed branch: | lp:~percona-toolkit-dev/percona-toolkit/detect-key-len-with-range-scan |
Merge into: | lp:percona-toolkit/2.1 |
Diff against target: |
799 lines (+666/-31) 5 files modified
bin/pt-online-schema-change (+164/-12) bin/pt-table-checksum (+164/-12) lib/IndexLength.pm (+175/-0) t/lib/IndexLength.pm (+135/-0) t/pt-table-checksum/chunk_index.t (+28/-7) |
To merge this branch: | bzr merge lp:~percona-toolkit-dev/percona-toolkit/detect-key-len-with-range-scan |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Daniel Nichter | Approve | ||
Review via email:
|
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Daniel Nichter (daniel-nichter) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'bin/pt-online-schema-change' | |||
2 | --- bin/pt-online-schema-change 2012-06-11 12:15:48 +0000 | |||
3 | +++ bin/pt-online-schema-change 2012-06-11 16:59:23 +0000 | |||
4 | @@ -5128,6 +5128,158 @@ | |||
5 | 5128 | # ########################################################################### | 5128 | # ########################################################################### |
6 | 5129 | 5129 | ||
7 | 5130 | # ########################################################################### | 5130 | # ########################################################################### |
8 | 5131 | # IndexLength package | ||
9 | 5132 | # This package is a copy without comments from the original. The original | ||
10 | 5133 | # with comments and its test file can be found in the Bazaar repository at, | ||
11 | 5134 | # lib/IndexLength.pm | ||
12 | 5135 | # t/lib/IndexLength.t | ||
13 | 5136 | # See https://launchpad.net/percona-toolkit for more information. | ||
14 | 5137 | # ########################################################################### | ||
15 | 5138 | { | ||
16 | 5139 | |||
17 | 5140 | package IndexLength; | ||
18 | 5141 | |||
19 | 5142 | use strict; | ||
20 | 5143 | use warnings FATAL => 'all'; | ||
21 | 5144 | use English qw(-no_match_vars); | ||
22 | 5145 | use constant PTDEBUG => $ENV{PTDEBUG} || 0; | ||
23 | 5146 | |||
24 | 5147 | use Data::Dumper; | ||
25 | 5148 | $Data::Dumper::Indent = 1; | ||
26 | 5149 | $Data::Dumper::Sortkeys = 1; | ||
27 | 5150 | $Data::Dumper::Quotekeys = 0; | ||
28 | 5151 | |||
29 | 5152 | sub new { | ||
30 | 5153 | my ( $class, %args ) = @_; | ||
31 | 5154 | my @required_args = qw(Quoter); | ||
32 | 5155 | foreach my $arg ( @required_args ) { | ||
33 | 5156 | die "I need a $arg argument" unless $args{$arg}; | ||
34 | 5157 | } | ||
35 | 5158 | |||
36 | 5159 | my $self = { | ||
37 | 5160 | Quoter => $args{Quoter}, | ||
38 | 5161 | }; | ||
39 | 5162 | |||
40 | 5163 | return bless $self, $class; | ||
41 | 5164 | } | ||
42 | 5165 | |||
43 | 5166 | sub index_length { | ||
44 | 5167 | my ($self, %args) = @_; | ||
45 | 5168 | my @required_args = qw(Cxn tbl index); | ||
46 | 5169 | foreach my $arg ( @required_args ) { | ||
47 | 5170 | die "I need a $arg argument" unless $args{$arg}; | ||
48 | 5171 | } | ||
49 | 5172 | my ($cxn) = @args{@required_args}; | ||
50 | 5173 | |||
51 | 5174 | die "The tbl argument does not have a tbl_struct" | ||
52 | 5175 | unless exists $args{tbl}->{tbl_struct}; | ||
53 | 5176 | die "Index $args{index} does not exist in table $args{tbl}->{name}" | ||
54 | 5177 | unless $args{tbl}->{tbl_struct}->{keys}->{$args{index}}; | ||
55 | 5178 | |||
56 | 5179 | my $index_struct = $args{tbl}->{tbl_struct}->{keys}->{$args{index}}; | ||
57 | 5180 | my $index_cols = $index_struct->{cols}; | ||
58 | 5181 | my $n_index_cols = $args{n_index_cols}; | ||
59 | 5182 | if ( !$n_index_cols || $n_index_cols > @$index_cols ) { | ||
60 | 5183 | $n_index_cols = scalar @$index_cols; | ||
61 | 5184 | } | ||
62 | 5185 | |||
63 | 5186 | my $vals = $self->_get_first_values( | ||
64 | 5187 | %args, | ||
65 | 5188 | n_index_cols => $n_index_cols, | ||
66 | 5189 | ); | ||
67 | 5190 | |||
68 | 5191 | my $sql = $self->_make_range_query( | ||
69 | 5192 | %args, | ||
70 | 5193 | n_index_cols => $n_index_cols, | ||
71 | 5194 | vals => $vals, | ||
72 | 5195 | ); | ||
73 | 5196 | my $sth = $cxn->dbh()->prepare($sql); | ||
74 | 5197 | PTDEBUG && _d($sth->{Statement}, 'params:', @$vals); | ||
75 | 5198 | $sth->execute(@$vals); | ||
76 | 5199 | my $row = $sth->fetchrow_hashref(); | ||
77 | 5200 | $sth->finish(); | ||
78 | 5201 | PTDEBUG && _d('Range scan:', Dumper($row)); | ||
79 | 5202 | return $row->{key_len}, $row->{key}; | ||
80 | 5203 | } | ||
81 | 5204 | |||
82 | 5205 | sub _get_first_values { | ||
83 | 5206 | my ($self, %args) = @_; | ||
84 | 5207 | my @required_args = qw(Cxn tbl index n_index_cols); | ||
85 | 5208 | foreach my $arg ( @required_args ) { | ||
86 | 5209 | die "I need a $arg argument" unless $args{$arg}; | ||
87 | 5210 | } | ||
88 | 5211 | my ($cxn, $tbl, $index, $n_index_cols) = @args{@required_args}; | ||
89 | 5212 | |||
90 | 5213 | my $q = $self->{Quoter}; | ||
91 | 5214 | |||
92 | 5215 | my $index_struct = $tbl->{tbl_struct}->{keys}->{$index}; | ||
93 | 5216 | my $index_cols = $index_struct->{cols}; | ||
94 | 5217 | my $index_columns = join (', ', | ||
95 | 5218 | map { $q->quote($_) } @{$index_cols}[0..($n_index_cols - 1)]); | ||
96 | 5219 | |||
97 | 5220 | my @where; | ||
98 | 5221 | foreach my $col ( @{$index_cols}[0..($n_index_cols - 1)] ) { | ||
99 | 5222 | push @where, $q->quote($col) . " IS NOT NULL" | ||
100 | 5223 | } | ||
101 | 5224 | |||
102 | 5225 | my $sql = "SELECT /*!40001 SQL_NO_CACHE */ $index_columns " | ||
103 | 5226 | . "FROM $tbl->{name} FORCE INDEX (" . $q->quote($index) . ") " | ||
104 | 5227 | . "WHERE " . join(' AND ', @where) | ||
105 | 5228 | . " ORDER BY $index_columns " | ||
106 | 5229 | . "LIMIT 1 /*key_len*/"; # only need 1 row | ||
107 | 5230 | PTDEBUG && _d($sql); | ||
108 | 5231 | my $vals = $cxn->dbh()->selectrow_arrayref($sql); | ||
109 | 5232 | return $vals; | ||
110 | 5233 | } | ||
111 | 5234 | |||
112 | 5235 | sub _make_range_query { | ||
113 | 5236 | my ($self, %args) = @_; | ||
114 | 5237 | my @required_args = qw(tbl index n_index_cols vals); | ||
115 | 5238 | foreach my $arg ( @required_args ) { | ||
116 | 5239 | die "I need a $arg argument" unless $args{$arg}; | ||
117 | 5240 | } | ||
118 | 5241 | my ($tbl, $index, $n_index_cols, $vals) = @args{@required_args}; | ||
119 | 5242 | |||
120 | 5243 | my $q = $self->{Quoter}; | ||
121 | 5244 | |||
122 | 5245 | my $index_struct = $tbl->{tbl_struct}->{keys}->{$index}; | ||
123 | 5246 | my $index_cols = $index_struct->{cols}; | ||
124 | 5247 | |||
125 | 5248 | my @where; | ||
126 | 5249 | if ( $n_index_cols > 1 ) { | ||
127 | 5250 | foreach my $n ( 0..($n_index_cols - 2) ) { | ||
128 | 5251 | my $col = $index_cols->[$n]; | ||
129 | 5252 | my $val = $vals->[$n]; | ||
130 | 5253 | push @where, $q->quote($col) . " = ?"; | ||
131 | 5254 | } | ||
132 | 5255 | } | ||
133 | 5256 | |||
134 | 5257 | my $col = $index_cols->[$n_index_cols - 1]; | ||
135 | 5258 | my $val = $vals->[-1]; # should only be as many vals as cols | ||
136 | 5259 | push @where, $q->quote($col) . " >= ?"; | ||
137 | 5260 | |||
138 | 5261 | my $sql = "EXPLAIN SELECT /*!40001 SQL_NO_CACHE */ * " | ||
139 | 5262 | . "FROM $tbl->{name} FORCE INDEX (" . $q->quote($index) . ") " | ||
140 | 5263 | . "WHERE " . join(' AND ', @where) | ||
141 | 5264 | . " /*key_len*/"; | ||
142 | 5265 | return $sql; | ||
143 | 5266 | } | ||
144 | 5267 | |||
145 | 5268 | sub _d { | ||
146 | 5269 | my ($package, undef, $line) = caller 0; | ||
147 | 5270 | @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } | ||
148 | 5271 | map { defined $_ ? $_ : 'undef' } | ||
149 | 5272 | @_; | ||
150 | 5273 | print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; | ||
151 | 5274 | } | ||
152 | 5275 | |||
153 | 5276 | 1; | ||
154 | 5277 | } | ||
155 | 5278 | # ########################################################################### | ||
156 | 5279 | # End IndexLength package | ||
157 | 5280 | # ########################################################################### | ||
158 | 5281 | |||
159 | 5282 | # ########################################################################### | ||
160 | 5131 | # This is a combination of modules and programs in one -- a runnable module. | 5283 | # This is a combination of modules and programs in one -- a runnable module. |
161 | 5132 | # http://www.perl.com/pub/a/2006/07/13/lightning-articles.html?page=last | 5284 | # http://www.perl.com/pub/a/2006/07/13/lightning-articles.html?page=last |
162 | 5133 | # Or, look it up in the Camel book on pages 642 and 643 in the 3rd edition. | 5285 | # Or, look it up in the Camel book on pages 642 and 643 in the 3rd edition. |
163 | @@ -5908,30 +6060,30 @@ | |||
164 | 5908 | } | 6060 | } |
165 | 5909 | else { # chunking the table | 6061 | else { # chunking the table |
166 | 5910 | if ( $o->get('check-plan') ) { | 6062 | if ( $o->get('check-plan') ) { |
171 | 5911 | my $expl = explain_statement( | 6063 | my $idx_len = new IndexLength(Quoter => $q); |
172 | 5912 | sth => $statements->{explain_first_lower_boundary}, | 6064 | my ($key_len, $key) = $idx_len->index_length( |
173 | 5913 | tbl => $tbl, | 6065 | Cxn => $args{Cxn}, |
174 | 5914 | vals => [], | 6066 | tbl => $tbl, |
175 | 6067 | index => $nibble_iter->nibble_index(), | ||
176 | 6068 | n_index_cols => $o->get('chunk-index-columns'), | ||
177 | 5915 | ); | 6069 | ); |
181 | 5916 | if ( !$expl->{key} | 6070 | if ( !$key || lc($key) ne lc($nibble_iter->nibble_index()) ) { |
179 | 5917 | || lc($expl->{key}) ne lc($nibble_iter->nibble_index()) ) | ||
180 | 5918 | { | ||
182 | 5919 | die "Cannot determine the key_len of the chunk index " | 6071 | die "Cannot determine the key_len of the chunk index " |
183 | 5920 | . "because MySQL chose " | 6072 | . "because MySQL chose " |
185 | 5921 | . ($expl->{key} ? "the $expl->{key}" : "no") . " index " | 6073 | . ($key ? "the $key" : "no") . " index " |
186 | 5922 | . "instead of the " . $nibble_iter->nibble_index() | 6074 | . "instead of the " . $nibble_iter->nibble_index() |
187 | 5923 | . " index for the first lower boundary statement. " | 6075 | . " index for the first lower boundary statement. " |
188 | 5924 | . "See --[no]check-plan in the documentation for more " | 6076 | . "See --[no]check-plan in the documentation for more " |
189 | 5925 | . "information."; | 6077 | . "information."; |
190 | 5926 | } | 6078 | } |
194 | 5927 | elsif ( !$expl->{key_len} ) { | 6079 | elsif ( !$key_len ) { |
195 | 5928 | die "The key_len of the $expl->{key} index is " | 6080 | die "The key_len of the $key index is " |
196 | 5929 | . (defined $expl->{key_len} ? "zero" : "NULL") | 6081 | . (defined $key_len ? "zero" : "NULL") |
197 | 5930 | . ", but this should not be possible. " | 6082 | . ", but this should not be possible. " |
198 | 5931 | . "See --[no]check-plan in the documentation for more " | 6083 | . "See --[no]check-plan in the documentation for more " |
199 | 5932 | . "information."; | 6084 | . "information."; |
200 | 5933 | } | 6085 | } |
202 | 5934 | $tbl->{key_len} = $expl->{key_len}; | 6086 | $tbl->{key_len} = $key_len; |
203 | 5935 | } | 6087 | } |
204 | 5936 | } | 6088 | } |
205 | 5937 | 6089 | ||
206 | 5938 | 6090 | ||
207 | === modified file 'bin/pt-table-checksum' | |||
208 | --- bin/pt-table-checksum 2012-06-11 12:15:48 +0000 | |||
209 | +++ bin/pt-table-checksum 2012-06-11 16:59:23 +0000 | |||
210 | @@ -6081,6 +6081,158 @@ | |||
211 | 6081 | # ########################################################################### | 6081 | # ########################################################################### |
212 | 6082 | 6082 | ||
213 | 6083 | # ########################################################################### | 6083 | # ########################################################################### |
214 | 6084 | # IndexLength package | ||
215 | 6085 | # This package is a copy without comments from the original. The original | ||
216 | 6086 | # with comments and its test file can be found in the Bazaar repository at, | ||
217 | 6087 | # lib/IndexLength.pm | ||
218 | 6088 | # t/lib/IndexLength.t | ||
219 | 6089 | # See https://launchpad.net/percona-toolkit for more information. | ||
220 | 6090 | # ########################################################################### | ||
221 | 6091 | { | ||
222 | 6092 | |||
223 | 6093 | package IndexLength; | ||
224 | 6094 | |||
225 | 6095 | use strict; | ||
226 | 6096 | use warnings FATAL => 'all'; | ||
227 | 6097 | use English qw(-no_match_vars); | ||
228 | 6098 | use constant PTDEBUG => $ENV{PTDEBUG} || 0; | ||
229 | 6099 | |||
230 | 6100 | use Data::Dumper; | ||
231 | 6101 | $Data::Dumper::Indent = 1; | ||
232 | 6102 | $Data::Dumper::Sortkeys = 1; | ||
233 | 6103 | $Data::Dumper::Quotekeys = 0; | ||
234 | 6104 | |||
235 | 6105 | sub new { | ||
236 | 6106 | my ( $class, %args ) = @_; | ||
237 | 6107 | my @required_args = qw(Quoter); | ||
238 | 6108 | foreach my $arg ( @required_args ) { | ||
239 | 6109 | die "I need a $arg argument" unless $args{$arg}; | ||
240 | 6110 | } | ||
241 | 6111 | |||
242 | 6112 | my $self = { | ||
243 | 6113 | Quoter => $args{Quoter}, | ||
244 | 6114 | }; | ||
245 | 6115 | |||
246 | 6116 | return bless $self, $class; | ||
247 | 6117 | } | ||
248 | 6118 | |||
249 | 6119 | sub index_length { | ||
250 | 6120 | my ($self, %args) = @_; | ||
251 | 6121 | my @required_args = qw(Cxn tbl index); | ||
252 | 6122 | foreach my $arg ( @required_args ) { | ||
253 | 6123 | die "I need a $arg argument" unless $args{$arg}; | ||
254 | 6124 | } | ||
255 | 6125 | my ($cxn) = @args{@required_args}; | ||
256 | 6126 | |||
257 | 6127 | die "The tbl argument does not have a tbl_struct" | ||
258 | 6128 | unless exists $args{tbl}->{tbl_struct}; | ||
259 | 6129 | die "Index $args{index} does not exist in table $args{tbl}->{name}" | ||
260 | 6130 | unless $args{tbl}->{tbl_struct}->{keys}->{$args{index}}; | ||
261 | 6131 | |||
262 | 6132 | my $index_struct = $args{tbl}->{tbl_struct}->{keys}->{$args{index}}; | ||
263 | 6133 | my $index_cols = $index_struct->{cols}; | ||
264 | 6134 | my $n_index_cols = $args{n_index_cols}; | ||
265 | 6135 | if ( !$n_index_cols || $n_index_cols > @$index_cols ) { | ||
266 | 6136 | $n_index_cols = scalar @$index_cols; | ||
267 | 6137 | } | ||
268 | 6138 | |||
269 | 6139 | my $vals = $self->_get_first_values( | ||
270 | 6140 | %args, | ||
271 | 6141 | n_index_cols => $n_index_cols, | ||
272 | 6142 | ); | ||
273 | 6143 | |||
274 | 6144 | my $sql = $self->_make_range_query( | ||
275 | 6145 | %args, | ||
276 | 6146 | n_index_cols => $n_index_cols, | ||
277 | 6147 | vals => $vals, | ||
278 | 6148 | ); | ||
279 | 6149 | my $sth = $cxn->dbh()->prepare($sql); | ||
280 | 6150 | PTDEBUG && _d($sth->{Statement}, 'params:', @$vals); | ||
281 | 6151 | $sth->execute(@$vals); | ||
282 | 6152 | my $row = $sth->fetchrow_hashref(); | ||
283 | 6153 | $sth->finish(); | ||
284 | 6154 | PTDEBUG && _d('Range scan:', Dumper($row)); | ||
285 | 6155 | return $row->{key_len}, $row->{key}; | ||
286 | 6156 | } | ||
287 | 6157 | |||
288 | 6158 | sub _get_first_values { | ||
289 | 6159 | my ($self, %args) = @_; | ||
290 | 6160 | my @required_args = qw(Cxn tbl index n_index_cols); | ||
291 | 6161 | foreach my $arg ( @required_args ) { | ||
292 | 6162 | die "I need a $arg argument" unless $args{$arg}; | ||
293 | 6163 | } | ||
294 | 6164 | my ($cxn, $tbl, $index, $n_index_cols) = @args{@required_args}; | ||
295 | 6165 | |||
296 | 6166 | my $q = $self->{Quoter}; | ||
297 | 6167 | |||
298 | 6168 | my $index_struct = $tbl->{tbl_struct}->{keys}->{$index}; | ||
299 | 6169 | my $index_cols = $index_struct->{cols}; | ||
300 | 6170 | my $index_columns = join (', ', | ||
301 | 6171 | map { $q->quote($_) } @{$index_cols}[0..($n_index_cols - 1)]); | ||
302 | 6172 | |||
303 | 6173 | my @where; | ||
304 | 6174 | foreach my $col ( @{$index_cols}[0..($n_index_cols - 1)] ) { | ||
305 | 6175 | push @where, $q->quote($col) . " IS NOT NULL" | ||
306 | 6176 | } | ||
307 | 6177 | |||
308 | 6178 | my $sql = "SELECT /*!40001 SQL_NO_CACHE */ $index_columns " | ||
309 | 6179 | . "FROM $tbl->{name} FORCE INDEX (" . $q->quote($index) . ") " | ||
310 | 6180 | . "WHERE " . join(' AND ', @where) | ||
311 | 6181 | . " ORDER BY $index_columns " | ||
312 | 6182 | . "LIMIT 1 /*key_len*/"; # only need 1 row | ||
313 | 6183 | PTDEBUG && _d($sql); | ||
314 | 6184 | my $vals = $cxn->dbh()->selectrow_arrayref($sql); | ||
315 | 6185 | return $vals; | ||
316 | 6186 | } | ||
317 | 6187 | |||
318 | 6188 | sub _make_range_query { | ||
319 | 6189 | my ($self, %args) = @_; | ||
320 | 6190 | my @required_args = qw(tbl index n_index_cols vals); | ||
321 | 6191 | foreach my $arg ( @required_args ) { | ||
322 | 6192 | die "I need a $arg argument" unless $args{$arg}; | ||
323 | 6193 | } | ||
324 | 6194 | my ($tbl, $index, $n_index_cols, $vals) = @args{@required_args}; | ||
325 | 6195 | |||
326 | 6196 | my $q = $self->{Quoter}; | ||
327 | 6197 | |||
328 | 6198 | my $index_struct = $tbl->{tbl_struct}->{keys}->{$index}; | ||
329 | 6199 | my $index_cols = $index_struct->{cols}; | ||
330 | 6200 | |||
331 | 6201 | my @where; | ||
332 | 6202 | if ( $n_index_cols > 1 ) { | ||
333 | 6203 | foreach my $n ( 0..($n_index_cols - 2) ) { | ||
334 | 6204 | my $col = $index_cols->[$n]; | ||
335 | 6205 | my $val = $vals->[$n]; | ||
336 | 6206 | push @where, $q->quote($col) . " = ?"; | ||
337 | 6207 | } | ||
338 | 6208 | } | ||
339 | 6209 | |||
340 | 6210 | my $col = $index_cols->[$n_index_cols - 1]; | ||
341 | 6211 | my $val = $vals->[-1]; # should only be as many vals as cols | ||
342 | 6212 | push @where, $q->quote($col) . " >= ?"; | ||
343 | 6213 | |||
344 | 6214 | my $sql = "EXPLAIN SELECT /*!40001 SQL_NO_CACHE */ * " | ||
345 | 6215 | . "FROM $tbl->{name} FORCE INDEX (" . $q->quote($index) . ") " | ||
346 | 6216 | . "WHERE " . join(' AND ', @where) | ||
347 | 6217 | . " /*key_len*/"; | ||
348 | 6218 | return $sql; | ||
349 | 6219 | } | ||
350 | 6220 | |||
351 | 6221 | sub _d { | ||
352 | 6222 | my ($package, undef, $line) = caller 0; | ||
353 | 6223 | @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } | ||
354 | 6224 | map { defined $_ ? $_ : 'undef' } | ||
355 | 6225 | @_; | ||
356 | 6226 | print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; | ||
357 | 6227 | } | ||
358 | 6228 | |||
359 | 6229 | 1; | ||
360 | 6230 | } | ||
361 | 6231 | # ########################################################################### | ||
362 | 6232 | # End IndexLength package | ||
363 | 6233 | # ########################################################################### | ||
364 | 6234 | |||
365 | 6235 | # ########################################################################### | ||
366 | 6084 | # This is a combination of modules and programs in one -- a runnable module. | 6236 | # This is a combination of modules and programs in one -- a runnable module. |
367 | 6085 | # http://www.perl.com/pub/a/2006/07/13/lightning-articles.html?page=last | 6237 | # http://www.perl.com/pub/a/2006/07/13/lightning-articles.html?page=last |
368 | 6086 | # Or, look it up in the Camel book on pages 642 and 643 in the 3rd edition. | 6238 | # Or, look it up in the Camel book on pages 642 and 643 in the 3rd edition. |
369 | @@ -6748,30 +6900,30 @@ | |||
370 | 6748 | } | 6900 | } |
371 | 6749 | else { # chunking the table | 6901 | else { # chunking the table |
372 | 6750 | if ( $o->get('check-plan') ) { | 6902 | if ( $o->get('check-plan') ) { |
377 | 6751 | my $expl = explain_statement( | 6903 | my $idx_len = new IndexLength(Quoter => $q); |
378 | 6752 | sth => $statements->{explain_first_lower_boundary}, | 6904 | my ($key_len, $key) = $idx_len->index_length( |
379 | 6753 | tbl => $tbl, | 6905 | Cxn => $args{Cxn}, |
380 | 6754 | vals => [], | 6906 | tbl => $tbl, |
381 | 6907 | index => $nibble_iter->nibble_index(), | ||
382 | 6908 | n_index_cols => $o->get('chunk-index-columns'), | ||
383 | 6755 | ); | 6909 | ); |
387 | 6756 | if ( !$expl->{key} | 6910 | if ( !$key || lc($key) ne lc($nibble_iter->nibble_index()) ) { |
385 | 6757 | || lc($expl->{key}) ne lc($nibble_iter->nibble_index()) ) | ||
386 | 6758 | { | ||
388 | 6759 | die "Cannot determine the key_len of the chunk index " | 6911 | die "Cannot determine the key_len of the chunk index " |
389 | 6760 | . "because MySQL chose " | 6912 | . "because MySQL chose " |
391 | 6761 | . ($expl->{key} ? "the $expl->{key}" : "no") . " index " | 6913 | . ($key ? "the $key" : "no") . " index " |
392 | 6762 | . "instead of the " . $nibble_iter->nibble_index() | 6914 | . "instead of the " . $nibble_iter->nibble_index() |
393 | 6763 | . " index for the first lower boundary statement. " | 6915 | . " index for the first lower boundary statement. " |
394 | 6764 | . "See --[no]check-plan in the documentation for more " | 6916 | . "See --[no]check-plan in the documentation for more " |
395 | 6765 | . "information."; | 6917 | . "information."; |
396 | 6766 | } | 6918 | } |
400 | 6767 | elsif ( !$expl->{key_len} ) { | 6919 | elsif ( !$key_len ) { |
401 | 6768 | die "The key_len of the $expl->{key} index is " | 6920 | die "The key_len of the $key index is " |
402 | 6769 | . (defined $expl->{key_len} ? "zero" : "NULL") | 6921 | . (defined $key_len ? "zero" : "NULL") |
403 | 6770 | . ", but this should not be possible. " | 6922 | . ", but this should not be possible. " |
404 | 6771 | . "See --[no]check-plan in the documentation for more " | 6923 | . "See --[no]check-plan in the documentation for more " |
405 | 6772 | . "information."; | 6924 | . "information."; |
406 | 6773 | } | 6925 | } |
408 | 6774 | $tbl->{key_len} = $expl->{key_len}; | 6926 | $tbl->{key_len} = $key_len; |
409 | 6775 | } | 6927 | } |
410 | 6776 | } | 6928 | } |
411 | 6777 | 6929 | ||
412 | 6778 | 6930 | ||
413 | === added file 'lib/IndexLength.pm' | |||
414 | --- lib/IndexLength.pm 1970-01-01 00:00:00 +0000 | |||
415 | +++ lib/IndexLength.pm 2012-06-11 16:59:23 +0000 | |||
416 | @@ -0,0 +1,175 @@ | |||
417 | 1 | # This program is copyright 2012 Percona Inc. | ||
418 | 2 | # Feedback and improvements are welcome. | ||
419 | 3 | # | ||
420 | 4 | # THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED | ||
421 | 5 | # WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF | ||
422 | 6 | # MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. | ||
423 | 7 | # | ||
424 | 8 | # This program is free software; you can redistribute it and/or modify it under | ||
425 | 9 | # the terms of the GNU General Public License as published by the Free Software | ||
426 | 10 | # Foundation, version 2; OR the Perl Artistic License. On UNIX and similar | ||
427 | 11 | # systems, you can issue `man perlgpl' or `man perlartistic' to read these | ||
428 | 12 | # licenses. | ||
429 | 13 | # | ||
430 | 14 | # You should have received a copy of the GNU General Public License along with | ||
431 | 15 | # this program; if not, write to the Free Software Foundation, Inc., 59 Temple | ||
432 | 16 | # Place, Suite 330, Boston, MA 02111-1307 USA. | ||
433 | 17 | # ########################################################################### | ||
434 | 18 | # IndexLength package | ||
435 | 19 | # ########################################################################### | ||
436 | 20 | { | ||
437 | 21 | # Package: IndexLength | ||
438 | 22 | # IndexLength get the key_len of a index. | ||
439 | 23 | |||
440 | 24 | package IndexLength; | ||
441 | 25 | |||
442 | 26 | use strict; | ||
443 | 27 | use warnings FATAL => 'all'; | ||
444 | 28 | use English qw(-no_match_vars); | ||
445 | 29 | use constant PTDEBUG => $ENV{PTDEBUG} || 0; | ||
446 | 30 | |||
447 | 31 | use Data::Dumper; | ||
448 | 32 | $Data::Dumper::Indent = 1; | ||
449 | 33 | $Data::Dumper::Sortkeys = 1; | ||
450 | 34 | $Data::Dumper::Quotekeys = 0; | ||
451 | 35 | |||
452 | 36 | sub new { | ||
453 | 37 | my ( $class, %args ) = @_; | ||
454 | 38 | my @required_args = qw(Quoter); | ||
455 | 39 | foreach my $arg ( @required_args ) { | ||
456 | 40 | die "I need a $arg argument" unless $args{$arg}; | ||
457 | 41 | } | ||
458 | 42 | |||
459 | 43 | my $self = { | ||
460 | 44 | Quoter => $args{Quoter}, | ||
461 | 45 | }; | ||
462 | 46 | |||
463 | 47 | return bless $self, $class; | ||
464 | 48 | } | ||
465 | 49 | |||
466 | 50 | # Returns the length of the index in bytes using only | ||
467 | 51 | # the first N left-most columns of the index. | ||
468 | 52 | sub index_length { | ||
469 | 53 | my ($self, %args) = @_; | ||
470 | 54 | my @required_args = qw(Cxn tbl index); | ||
471 | 55 | foreach my $arg ( @required_args ) { | ||
472 | 56 | die "I need a $arg argument" unless $args{$arg}; | ||
473 | 57 | } | ||
474 | 58 | my ($cxn) = @args{@required_args}; | ||
475 | 59 | |||
476 | 60 | die "The tbl argument does not have a tbl_struct" | ||
477 | 61 | unless exists $args{tbl}->{tbl_struct}; | ||
478 | 62 | die "Index $args{index} does not exist in table $args{tbl}->{name}" | ||
479 | 63 | unless $args{tbl}->{tbl_struct}->{keys}->{$args{index}}; | ||
480 | 64 | |||
481 | 65 | my $index_struct = $args{tbl}->{tbl_struct}->{keys}->{$args{index}}; | ||
482 | 66 | my $index_cols = $index_struct->{cols}; | ||
483 | 67 | my $n_index_cols = $args{n_index_cols}; | ||
484 | 68 | if ( !$n_index_cols || $n_index_cols > @$index_cols ) { | ||
485 | 69 | $n_index_cols = scalar @$index_cols; | ||
486 | 70 | } | ||
487 | 71 | |||
488 | 72 | # Get the first row with non-NULL values. | ||
489 | 73 | my $vals = $self->_get_first_values( | ||
490 | 74 | %args, | ||
491 | 75 | n_index_cols => $n_index_cols, | ||
492 | 76 | ); | ||
493 | 77 | |||
494 | 78 | # Make an EXPLAIN query to scan the range and execute it. | ||
495 | 79 | my $sql = $self->_make_range_query( | ||
496 | 80 | %args, | ||
497 | 81 | n_index_cols => $n_index_cols, | ||
498 | 82 | vals => $vals, | ||
499 | 83 | ); | ||
500 | 84 | my $sth = $cxn->dbh()->prepare($sql); | ||
501 | 85 | PTDEBUG && _d($sth->{Statement}, 'params:', @$vals); | ||
502 | 86 | $sth->execute(@$vals); | ||
503 | 87 | my $row = $sth->fetchrow_hashref(); | ||
504 | 88 | $sth->finish(); | ||
505 | 89 | PTDEBUG && _d('Range scan:', Dumper($row)); | ||
506 | 90 | return $row->{key_len}, $row->{key}; | ||
507 | 91 | } | ||
508 | 92 | |||
509 | 93 | sub _get_first_values { | ||
510 | 94 | my ($self, %args) = @_; | ||
511 | 95 | my @required_args = qw(Cxn tbl index n_index_cols); | ||
512 | 96 | foreach my $arg ( @required_args ) { | ||
513 | 97 | die "I need a $arg argument" unless $args{$arg}; | ||
514 | 98 | } | ||
515 | 99 | my ($cxn, $tbl, $index, $n_index_cols) = @args{@required_args}; | ||
516 | 100 | |||
517 | 101 | my $q = $self->{Quoter}; | ||
518 | 102 | |||
519 | 103 | # Select just the index columns. | ||
520 | 104 | my $index_struct = $tbl->{tbl_struct}->{keys}->{$index}; | ||
521 | 105 | my $index_cols = $index_struct->{cols}; | ||
522 | 106 | my $index_columns = join (', ', | ||
523 | 107 | map { $q->quote($_) } @{$index_cols}[0..($n_index_cols - 1)]); | ||
524 | 108 | |||
525 | 109 | # Where no index column is null, because we can't > NULL. | ||
526 | 110 | my @where; | ||
527 | 111 | foreach my $col ( @{$index_cols}[0..($n_index_cols - 1)] ) { | ||
528 | 112 | push @where, $q->quote($col) . " IS NOT NULL" | ||
529 | 113 | } | ||
530 | 114 | |||
531 | 115 | my $sql = "SELECT /*!40001 SQL_NO_CACHE */ $index_columns " | ||
532 | 116 | . "FROM $tbl->{name} FORCE INDEX (" . $q->quote($index) . ") " | ||
533 | 117 | . "WHERE " . join(' AND ', @where) | ||
534 | 118 | . " ORDER BY $index_columns " | ||
535 | 119 | . "LIMIT 1 /*key_len*/"; # only need 1 row | ||
536 | 120 | PTDEBUG && _d($sql); | ||
537 | 121 | my $vals = $cxn->dbh()->selectrow_arrayref($sql); | ||
538 | 122 | return $vals; | ||
539 | 123 | } | ||
540 | 124 | |||
541 | 125 | sub _make_range_query { | ||
542 | 126 | my ($self, %args) = @_; | ||
543 | 127 | my @required_args = qw(tbl index n_index_cols vals); | ||
544 | 128 | foreach my $arg ( @required_args ) { | ||
545 | 129 | die "I need a $arg argument" unless $args{$arg}; | ||
546 | 130 | } | ||
547 | 131 | my ($tbl, $index, $n_index_cols, $vals) = @args{@required_args}; | ||
548 | 132 | |||
549 | 133 | my $q = $self->{Quoter}; | ||
550 | 134 | |||
551 | 135 | my $index_struct = $tbl->{tbl_struct}->{keys}->{$index}; | ||
552 | 136 | my $index_cols = $index_struct->{cols}; | ||
553 | 137 | |||
554 | 138 | # All but the last index col = val. | ||
555 | 139 | my @where; | ||
556 | 140 | if ( $n_index_cols > 1 ) { | ||
557 | 141 | # -1 for zero-index array as usual, then -1 again because | ||
558 | 142 | # we don't want the last column; that's added below. | ||
559 | 143 | foreach my $n ( 0..($n_index_cols - 2) ) { | ||
560 | 144 | my $col = $index_cols->[$n]; | ||
561 | 145 | my $val = $vals->[$n]; | ||
562 | 146 | push @where, $q->quote($col) . " = ?"; | ||
563 | 147 | } | ||
564 | 148 | } | ||
565 | 149 | |||
566 | 150 | # The last index col > val. This causes the range scan using just | ||
567 | 151 | # the N left-most index columns. | ||
568 | 152 | my $col = $index_cols->[$n_index_cols - 1]; | ||
569 | 153 | my $val = $vals->[-1]; # should only be as many vals as cols | ||
570 | 154 | push @where, $q->quote($col) . " >= ?"; | ||
571 | 155 | |||
572 | 156 | my $sql = "EXPLAIN SELECT /*!40001 SQL_NO_CACHE */ * " | ||
573 | 157 | . "FROM $tbl->{name} FORCE INDEX (" . $q->quote($index) . ") " | ||
574 | 158 | . "WHERE " . join(' AND ', @where) | ||
575 | 159 | . " /*key_len*/"; | ||
576 | 160 | return $sql; | ||
577 | 161 | } | ||
578 | 162 | |||
579 | 163 | sub _d { | ||
580 | 164 | my ($package, undef, $line) = caller 0; | ||
581 | 165 | @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; } | ||
582 | 166 | map { defined $_ ? $_ : 'undef' } | ||
583 | 167 | @_; | ||
584 | 168 | print STDERR "# $package:$line $PID ", join(' ', @_), "\n"; | ||
585 | 169 | } | ||
586 | 170 | |||
587 | 171 | 1; | ||
588 | 172 | } | ||
589 | 173 | # ########################################################################### | ||
590 | 174 | # End IndexLength package | ||
591 | 175 | # ########################################################################### | ||
592 | 0 | 176 | ||
593 | === added file 't/lib/IndexLength.pm' | |||
594 | --- t/lib/IndexLength.pm 1970-01-01 00:00:00 +0000 | |||
595 | +++ t/lib/IndexLength.pm 2012-06-11 16:59:23 +0000 | |||
596 | @@ -0,0 +1,135 @@ | |||
597 | 1 | #!/usr/bin/perl | ||
598 | 2 | |||
599 | 3 | BEGIN { | ||
600 | 4 | die "The PERCONA_TOOLKIT_BRANCH environment variable is not set.\n" | ||
601 | 5 | unless $ENV{PERCONA_TOOLKIT_BRANCH} && -d $ENV{PERCONA_TOOLKIT_BRANCH}; | ||
602 | 6 | unshift @INC, "$ENV{PERCONA_TOOLKIT_BRANCH}/lib"; | ||
603 | 7 | }; | ||
604 | 8 | |||
605 | 9 | use strict; | ||
606 | 10 | use warnings FATAL => 'all'; | ||
607 | 11 | use English qw(-no_match_vars); | ||
608 | 12 | use Test::More; | ||
609 | 13 | |||
610 | 14 | use PerconaTest; | ||
611 | 15 | use DSNParser; | ||
612 | 16 | use Sandbox; | ||
613 | 17 | |||
614 | 18 | use Cxn; | ||
615 | 19 | use Quoter; | ||
616 | 20 | use TableParser; | ||
617 | 21 | use OptionParser; | ||
618 | 22 | use IndexLength; | ||
619 | 23 | |||
620 | 24 | use constant PTDEBUG => $ENV{PTDEBUG} || 0; | ||
621 | 25 | use constant PTDEVDEBUG => $ENV{PTDEBUG} || 0; | ||
622 | 26 | |||
623 | 27 | use Data::Dumper; | ||
624 | 28 | $Data::Dumper::Indent = 1; | ||
625 | 29 | $Data::Dumper::Sortkeys = 1; | ||
626 | 30 | $Data::Dumper::Quotekeys = 0; | ||
627 | 31 | |||
628 | 32 | my $dp = new DSNParser(opts=>$dsn_opts); | ||
629 | 33 | my $sb = new Sandbox(basedir => '/tmp', DSNParser => $dp); | ||
630 | 34 | my $dbh = $sb->get_dbh_for('master'); | ||
631 | 35 | |||
632 | 36 | if ( !$dbh ) { | ||
633 | 37 | plan skip_all => 'Cannot connect to sandbox master'; | ||
634 | 38 | } | ||
635 | 39 | else { | ||
636 | 40 | plan tests => 7; | ||
637 | 41 | } | ||
638 | 42 | |||
639 | 43 | my $output; | ||
640 | 44 | my $q = new Quoter(); | ||
641 | 45 | my $tp = new TableParser(Quoter => $q); | ||
642 | 46 | my $il = new IndexLength(Quoter => $q); | ||
643 | 47 | my $o = new OptionParser(description => 'IndexLength'); | ||
644 | 48 | $o->get_specs("$trunk/bin/pt-table-checksum"); | ||
645 | 49 | my $cxn = new Cxn( | ||
646 | 50 | dbh => $dbh, | ||
647 | 51 | dsn => { h=>'127.1', P=>'12345', n=>'h=127.1,P=12345' }, | ||
648 | 52 | DSNParser => $dp, | ||
649 | 53 | OptionParser => $o, | ||
650 | 54 | ); | ||
651 | 55 | |||
652 | 56 | sub test_index_len { | ||
653 | 57 | my (%args) = @_; | ||
654 | 58 | my @required_args = qw(name tbl index len); | ||
655 | 59 | foreach my $arg ( @required_args ) { | ||
656 | 60 | die "I need a $arg argument" unless $args{$arg}; | ||
657 | 61 | } | ||
658 | 62 | |||
659 | 63 | my ($len, $key) = $il->index_length( | ||
660 | 64 | Cxn => $cxn, | ||
661 | 65 | tbl => $args{tbl}, | ||
662 | 66 | index => $args{index}, | ||
663 | 67 | n_index_cols => $args{n_index_cols}, | ||
664 | 68 | ); | ||
665 | 69 | |||
666 | 70 | is( | ||
667 | 71 | $len, | ||
668 | 72 | $args{len}, | ||
669 | 73 | "$args{name}" | ||
670 | 74 | ); | ||
671 | 75 | } | ||
672 | 76 | |||
673 | 77 | # ############################################################################# | ||
674 | 78 | # bad_plan, PK with 4 cols | ||
675 | 79 | # ############################################################################# | ||
676 | 80 | $sb->load_file('master', "t/pt-table-checksum/samples/bad-plan-bug-1010232.sql"); | ||
677 | 81 | my $tbl_struct = $tp->parse( | ||
678 | 82 | $tp->get_create_table($dbh, 'bad_plan', 't')); | ||
679 | 83 | my $tbl = { | ||
680 | 84 | name => $q->quote('bad_plan', 't'), | ||
681 | 85 | tbl_struct => $tbl_struct, | ||
682 | 86 | }; | ||
683 | 87 | |||
684 | 88 | for my $n ( 1..4 ) { | ||
685 | 89 | my $len = $n * 2 + ($n >= 2 ? 1 : 0); | ||
686 | 90 | test_index_len( | ||
687 | 91 | name => "bad_plan.t $n cols = $len bytes", | ||
688 | 92 | tbl => $tbl, | ||
689 | 93 | index => "PRIMARY", | ||
690 | 94 | n_index_cols => $n, | ||
691 | 95 | len => $len, | ||
692 | 96 | ); | ||
693 | 97 | } | ||
694 | 98 | |||
695 | 99 | # ############################################################################# | ||
696 | 100 | # Some sakila tables | ||
697 | 101 | # ############################################################################# | ||
698 | 102 | $tbl_struct = $tp->parse( | ||
699 | 103 | $tp->get_create_table($dbh, 'sakila', 'film_actor')); | ||
700 | 104 | $tbl = { | ||
701 | 105 | name => $q->quote('sakila', 'film_actor'), | ||
702 | 106 | tbl_struct => $tbl_struct, | ||
703 | 107 | }; | ||
704 | 108 | |||
705 | 109 | test_index_len( | ||
706 | 110 | name => "sakila.film_actor 1 col = 2 bytes", | ||
707 | 111 | tbl => $tbl, | ||
708 | 112 | index => "PRIMARY", | ||
709 | 113 | n_index_cols => 1, | ||
710 | 114 | len => 2, | ||
711 | 115 | ); | ||
712 | 116 | |||
713 | 117 | # ############################################################################# | ||
714 | 118 | # Use full index if no n_index_cols | ||
715 | 119 | # ############################################################################# | ||
716 | 120 | |||
717 | 121 | # Use sakila.film_actor stuff from previous tests. | ||
718 | 122 | |||
719 | 123 | test_index_len( | ||
720 | 124 | name => "sakila.film_actor all cols = 4 bytes", | ||
721 | 125 | tbl => $tbl, | ||
722 | 126 | index => "PRIMARY", | ||
723 | 127 | len => 4, | ||
724 | 128 | ); | ||
725 | 129 | |||
726 | 130 | # ############################################################################# | ||
727 | 131 | # Done. | ||
728 | 132 | # ############################################################################# | ||
729 | 133 | $sb->wipe_clean($dbh); | ||
730 | 134 | ok($sb->ok(), "Sandbox servers") or BAIL_OUT(__FILE__ . " broke the sandbox"); | ||
731 | 135 | exit; | ||
732 | 0 | 136 | ||
733 | === modified file 't/pt-table-checksum/chunk_index.t' | |||
734 | --- t/pt-table-checksum/chunk_index.t 2012-06-11 12:07:18 +0000 | |||
735 | +++ t/pt-table-checksum/chunk_index.t 2012-06-11 16:59:23 +0000 | |||
736 | @@ -25,7 +25,7 @@ | |||
737 | 25 | plan skip_all => 'Cannot connect to sandbox master'; | 25 | plan skip_all => 'Cannot connect to sandbox master'; |
738 | 26 | } | 26 | } |
739 | 27 | else { | 27 | else { |
741 | 28 | plan tests => 16; | 28 | plan tests => 17; |
742 | 29 | } | 29 | } |
743 | 30 | 30 | ||
744 | 31 | # The sandbox servers run with lock_wait_timeout=3 and it's not dynamic | 31 | # The sandbox servers run with lock_wait_timeout=3 and it's not dynamic |
745 | @@ -175,7 +175,7 @@ | |||
746 | 175 | $exit_status, | 175 | $exit_status, |
747 | 176 | 0, | 176 | 0, |
748 | 177 | "Bad key_len chunks are not errors" | 177 | "Bad key_len chunks are not errors" |
750 | 178 | ); | 178 | ) or diag($output); |
751 | 179 | 179 | ||
752 | 180 | cmp_ok( | 180 | cmp_ok( |
753 | 181 | PerconaTest::count_checksum_results($output, 'skipped'), | 181 | PerconaTest::count_checksum_results($output, 'skipped'), |
754 | @@ -205,19 +205,40 @@ | |||
755 | 205 | sub { | 205 | sub { |
756 | 206 | $exit_status = pt_table_checksum::main( | 206 | $exit_status = pt_table_checksum::main( |
757 | 207 | $master_dsn, '--max-load', '', | 207 | $master_dsn, '--max-load', '', |
761 | 208 | qw(--lock-wait-timeout 3 --chunk-size 5000 -t sakila.rental), | 208 | qw(--lock-wait-timeout 3 --chunk-size 1000 -t sakila.film_actor), |
762 | 209 | qw(--chunk-index rental_date --chunk-index-columns 5), | 209 | qw(--chunk-index PRIMARY --chunk-index-columns 9), |
763 | 210 | qw(--explain --explain)); | 210 | ); |
764 | 211 | }, | 211 | }, |
765 | 212 | stderr => 1, | 212 | stderr => 1, |
766 | 213 | ); | 213 | ); |
767 | 214 | 214 | ||
768 | 215 | is( | 215 | is( |
771 | 216 | $exit_status, | 216 | PerconaTest::count_checksum_results($output, 'rows'), |
772 | 217 | 0, | 217 | 5462, |
773 | 218 | "--chunk-index-columns > number of index columns" | 218 | "--chunk-index-columns > number of index columns" |
774 | 219 | ) or diag($output); | ||
775 | 220 | |||
776 | 221 | $output = output( | ||
777 | 222 | sub { | ||
778 | 223 | $exit_status = pt_table_checksum::main( | ||
779 | 224 | $master_dsn, '--max-load', '', | ||
780 | 225 | qw(--lock-wait-timeout 3 --chunk-size 1000 -t sakila.film_actor), | ||
781 | 226 | qw(--chunk-index-columns 1 --chunk-size-limit 3), | ||
782 | 227 | ); | ||
783 | 228 | }, | ||
784 | 229 | stderr => 1, | ||
785 | 219 | ); | 230 | ); |
786 | 220 | 231 | ||
787 | 232 | # Since we're not using the full index, it's basically a non-unique index, | ||
788 | 233 | # so there are dupes. The table really has 5462 rows, so we must get | ||
789 | 234 | # at least that many, and probably a few more. | ||
790 | 235 | cmp_ok( | ||
791 | 236 | PerconaTest::count_checksum_results($output, 'rows'), | ||
792 | 237 | '>=', | ||
793 | 238 | 5462, | ||
794 | 239 | "Initial key_len reflects --chunk-index-columns" | ||
795 | 240 | ) or diag($output); | ||
796 | 241 | |||
797 | 221 | # ############################################################################# | 242 | # ############################################################################# |
798 | 222 | # Done. | 243 | # Done. |
799 | 223 | # ############################################################################# | 244 | # ############################################################################# |