summaryrefslogtreecommitdiff
path: root/scripts/mysql_tableinfo.sh
blob: f5083a776c679b4bbdcb3b2f238dfb5772d80a86 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
#!@PERL@ -w

use strict;
use Getopt::Long;
use DBI;

=head1 NAME

mysql_tableinfo - creates and populates information tables with 
the output of SHOW DATABASES, SHOW TABLES (or SHOW TABLE STATUS), 
SHOW COLUMNS and SHOW INDEX.

This is version 1.1.

=head1 SYNOPSIS
 
  mysql_tableinfo [OPTIONS] database_to_write [database_like_wild] [table_like_wild]

  Do not backquote (``) database_to_write, 
  and do not quote ('') database_like_wild or table_like_wild

  Examples:

  mysql_tableinfo info

  mysql_tableinfo info this_db

  mysql_tableinfo info %a% b%

  mysql_tableinfo info --clear-only

  mysql_tableinfo info --col --idx --table-status

=cut

# Documentation continued at end of file


sub usage {
    die @_,"\nExecute 'perldoc $0' for documentation\n";
}

my %opt = (
    'user'	=> scalar getpwuid($>),
    'host'      => "localhost",
    'prefix'    => "", #to avoid 'use of uninitialized value...'
);
Getopt::Long::Configure(qw(no_ignore_case)); # disambuguate -p and -P
GetOptions( \%opt,
    "help",
    "user|u=s",
    "password|p=s",
    "host|h=s",
    "port|P=s",
    "socket|S=s",
    "tbl-status",
    "col",
    "idx",
    "clear",
    "clear-only",
    "prefix=s",
    "quiet|q",
) or usage("Invalid option");

if ($opt{'help'}) {usage();}

my ($db_to_write,$db_like_wild,$tbl_like_wild);
if (@ARGV==0)
{
    usage("Not enough arguments");
}
$db_to_write="`$ARGV[0]`"; shift @ARGV;
$db_like_wild=($ARGV[0])?$ARGV[0]:"%"; shift @ARGV;
$tbl_like_wild=($ARGV[0])?$ARGV[0]:"%"; shift @ARGV;
if (@ARGV>0) { usage("Too many arguments"); }

$0 = $1 if $0 =~ m:/([^/]+)$:;

my $info_db="`".$opt{'prefix'}."db`";
my $info_tbl="`".$opt{'prefix'}."tbl".
    (($opt{'tbl-status'})?"_status":"")."`";
my $info_col="`".$opt{'prefix'}."col`";
my $info_idx="`".$opt{'prefix'}."idx`";


# --- connect to the database ---

my $dsn = ";host=$opt{'host'}";
$dsn .= ";port=$opt{'port'}" if $opt{'port'};
$dsn .= ";mysql_socket=$opt{'socket'}" if $opt{'socket'};

my $dbh = DBI->connect("dbi:mysql:$dsn;mysql_read_default_group=perl",
                        $opt{'user'}, $opt{'password'},
{
    RaiseError => 1,
    PrintError => 0,
    AutoCommit => 1,
});

$db_like_wild=$dbh->quote($db_like_wild);
$tbl_like_wild=$dbh->quote($tbl_like_wild);

#Ask

if (!$opt{'quiet'})
{
    print "\n!! This program is doing to do:\n\n";
    print "**DROP** TABLE ...\n" if ($opt{'clear'} or $opt{'clear-only'});
    print "**DELETE** FROM ... WHERE `Database` LIKE $db_like_wild AND `Table` LIKE $tbl_like_wild
**INSERT** INTO ...

on the following tables :\n";

    foreach  (($info_db, $info_tbl),
	      (($opt{'col'})?$info_col:()), 
	      (($opt{'idx'})?$info_idx:()))
    {
	print("  $db_to_write.$_\n");
    }
    print "\nContinue (you can skip this confirmation step with --quiet) ? (y|n) [n]";
    if (<STDIN> !~ /^\s*y\s*$/i) 
    {
	print "Nothing done!\n";exit;
    }
}

if ($opt{'clear'} or $opt{'clear-only'}) 
{
#do not drop the $db_to_write database !
    foreach  (($info_db, $info_tbl),
	      (($opt{'col'})?$info_col:()), 
	      (($opt{'idx'})?$info_idx:()))
    {
	$dbh->do("DROP TABLE IF EXISTS $db_to_write.$_");
    }
    if ($opt{'clear-only'}) 
    {
	print "Wrote to database $db_to_write .\n" unless ($opt{'quiet'});
	exit; 
    }
}


my %sth;
my %extra_col_desc;
my %row;
my %done_create_table;

#create the $db_to_write database
$dbh->do("CREATE DATABASE IF NOT EXISTS $db_to_write");
$dbh->do("USE $db_to_write");

#get databases
$sth{'db'}=$dbh->prepare("SHOW DATABASES LIKE $db_like_wild");
$sth{'db'}->execute;

#create $info_db which will receive info about databases.
#Ensure that the first column to be called "Database" (as SHOW DATABASES LIKE
#returns a varying
#column name (of the form "Database (%...)") which is not suitable)
$extra_col_desc{'db'}=do_create_table("db",$info_db,undef,"`Database`");
#we'll remember the type of the `Database` column (as returned by
#SHOW DATABASES), which we will need when creating the next tables. 

#clear out-of-date info from this table 
$dbh->do("DELETE FROM $info_db WHERE `Database` LIKE $db_like_wild"); 


while ($row{'db'}=$sth{'db'}->fetchrow_arrayref) #go through all databases
{

#insert the database name
    $dbh->do("INSERT INTO $info_db VALUES("
	     .join(',' ,  ( map $dbh->quote($_), @{$row{'db'}} ) ).")" );

#for each database, get tables

    $sth{'tbl'}=$dbh->prepare("SHOW TABLE"
			    .( ($opt{'tbl-status'}) ? 
			       " STATUS"
			       : "S" )
			    ." from `$row{'db'}->[0]` LIKE $tbl_like_wild");
    $sth{'tbl'}->execute;
    unless ($done_create_table{$info_tbl})

#tables must be created only once, and out-of-date info must be
#cleared once
    {
	$done_create_table{$info_tbl}=1;
	$extra_col_desc{'tbl'}=
	    do_create_table("tbl",$info_tbl,
#add an extra column (database name) at the left
#and ensure that the table name will be called "Table"
#(this is unncessesary with
#SHOW TABLE STATUS, but necessary with SHOW TABLES (which returns a column
#named "Tables_in_..."))
			    "`Database` ".$extra_col_desc{'db'},"`Table`"); 
	$dbh->do("DELETE FROM $info_tbl WHERE `Database` LIKE $db_like_wild		                         AND `Table` LIKE $tbl_like_wild");
    }

    while ($row{'tbl'}=$sth{'tbl'}->fetchrow_arrayref)
    {
	$dbh->do("INSERT INTO $info_tbl VALUES("
		 .$dbh->quote($row{'db'}->[0]).","
		 .join(',' ,  ( map $dbh->quote($_), @{$row{'tbl'}} ) ).")");

#for each table, get columns...

	if ($opt{'col'})
	{
	    $sth{'col'}=$dbh->prepare("SHOW COLUMNS FROM `$row{'tbl'}->[0]` FROM `$row{'db'}->[0]`"); 
	    $sth{'col'}->execute;
	    unless ($done_create_table{$info_col})
	    {
		$done_create_table{$info_col}=1;
		do_create_table("col",$info_col,
				"`Database` ".$extra_col_desc{'db'}.","
				."`Table` ".$extra_col_desc{'tbl'}.","
				."`Seq_in_table` BIGINT(3)"); 
#We need to add a sequence number (1 for the first column of the table,
#2 for the second etc) so that users are able to retrieve columns in order
#if they want. This is not needed for INDEX 
#(where there is already Seq_in_index)
		$dbh->do("DELETE FROM $info_col WHERE `Database` 
                            LIKE $db_like_wild
			    AND `Table` LIKE $tbl_like_wild");
	    }
	    my $col_number=0;
	    while ($row{'col'}=$sth{'col'}->fetchrow_arrayref)
	    {
		$dbh->do("INSERT INTO $info_col VALUES("
			 .$dbh->quote($row{'db'}->[0]).","
			 .$dbh->quote($row{'tbl'}->[0]).","
			 .++$col_number.","
			 .join(',' ,  ( map $dbh->quote($_), @{$row{'col'}} ) ).")");
	    }
	}

#and get index.

	if ($opt{'idx'})
	{
	    $sth{'idx'}=$dbh->prepare("SHOW INDEX FROM `$row{'tbl'}->[0]` FROM `$row{'db'}->[0]`"); 
	    $sth{'idx'}->execute;
	    unless ($done_create_table{$info_idx})
	    {
		$done_create_table{$info_idx}=1;
		do_create_table("idx",$info_idx,
				"`Database` ".$extra_col_desc{'db'});
		$dbh->do("DELETE FROM $info_idx WHERE `Database`
			 LIKE $db_like_wild
			 AND `Table` LIKE $tbl_like_wild");
	    }
	    while ($row{'idx'}=$sth{'idx'}->fetchrow_arrayref)
	    {
		$dbh->do("INSERT INTO $info_idx VALUES("
			 .$dbh->quote($row{'db'}->[0]).","
			 .join(',' ,  ( map $dbh->quote($_), @{$row{'idx'}} ) ).")");
	    }
	}
    }
}

print "Wrote to database $db_to_write .\n" unless ($opt{'quiet'});
exit;


sub do_create_table
{
    my ($sth_key,$target_tbl,$extra_col_desc,$first_col_name)=@_; 
    my $create_table_query=$extra_col_desc;
    my ($i,$first_col_desc,$col_desc);

    for ($i=0;$i<$sth{$sth_key}->{NUM_OF_FIELDS};$i++)
    {
	if ($create_table_query) { $create_table_query.=", "; }	
	$col_desc=$sth{$sth_key}->{mysql_type_name}->[$i];
	if ($col_desc =~ /char|int/i)
	{
	    $col_desc.="($sth{$sth_key}->{PRECISION}->[$i])";
	}
	elsif ($col_desc =~ /decimal|numeric/i) #(never seen that)
	{
	    $col_desc.=
		"($sth{$sth_key}->{PRECISION}->[$i],$sth{$sth_key}->{SCALE}->[$i])";
	}
	elsif ($col_desc !~ /date/i) #date and datetime are OK,
	                         #no precision or scale for them
	{
	    warn "unexpected column type '$col_desc' 
(neither 'char','int','decimal|numeric')
when creating $target_tbl, hope table creation will go OK\n";
	}
	if ($i==0) {$first_col_desc=$col_desc};
	$create_table_query.=
	    ( ($i==0 and $first_col_name) ? 
	      "$first_col_name " :"`$sth{$sth_key}->{NAME}->[$i]` " )
	    .$col_desc;
    }
if ($create_table_query)
{
    $dbh->do("CREATE TABLE IF NOT EXISTS $target_tbl ($create_table_query)");
}
return $first_col_desc;
}

__END__


=head1 DESCRIPTION

mysql_tableinfo asks a MySQL server information about its
databases, tables, table columns and index, and stores this
in tables called `db`, `tbl` (or `tbl_status`), `col`, `idx` 
(with an optional prefix specified with --prefix).
After that, you can query these information tables, for example
to build your admin scripts with SQL queries, like

SELECT CONCAT("CHECK TABLE ",`database`,".",`table`," EXTENDED;") 
FROM info.tbl WHERE ... ;

as people usually do with some other RDBMS
(note: to increase the speed of your queries on the info tables,
you may add some index on them).

The database_like_wild and table_like_wild instructs the program
to gather information only about databases and tables
whose names match these patterns. If the info
tables already exist, their rows matching the patterns are simply
deleted and replaced by the new ones. That is,
old rows not matching the patterns are not touched.
If the database_like_wild and table_like_wild arguments
are not specified on the command-line they default to "%".

The program :

- does CREATE DATABASE IF NOT EXISTS database_to_write
where database_to_write is the database name specified on the command-line.

- does CREATE TABLE IF NOT EXISTS database_to_write.`db`

- fills database_to_write.`db` with the output of
SHOW DATABASES LIKE database_like_wild

- does CREATE TABLE IF NOT EXISTS database_to_write.`tbl`
(respectively database_to_write.`tbl_status`
if the --tbl-status option is on)

- for every found database,
fills database_to_write.`tbl` (respectively database_to_write.`tbl_status`)
with the output of 
SHOW TABLES FROM found_db LIKE table_like_wild
(respectively SHOW TABLE STATUS FROM found_db LIKE table_like_wild)

- if the --col option is on,
    * does CREATE TABLE IF NOT EXISTS database_to_write.`col`
    * for every found table,
      fills database_to_write.`col` with the output of 
      SHOW COLUMNS FROM found_tbl FROM found_db

- if the --idx option is on,
    * does CREATE TABLE IF NOT EXISTS database_to_write.`idx`
    * for every found table,
      fills database_to_write.`idx` with the output of 
      SHOW INDEX FROM found_tbl FROM found_db

Some options may modify this general scheme (see below).

As mentioned, the contents of the info tables are the output of
SHOW commands. In fact the contents are slightly more complete :

- the `tbl` (or `tbl_status`) info table 
  has an extra column which contains the database name,

- the `col` info table
  has an extra column which contains the table name,
  and an extra column which contains, for each described column,
  the number of this column in the table owning it (this extra column
  is called `Seq_in_table`). `Seq_in_table` makes it possible for you
  to retrieve your columns in sorted order, when you are querying
  the `col` table. 

- the `index` info table
  has an extra column which contains the database name.

Caution: info tables contain certain columns (e.g.
Database, Table, Null...) whose names, as they are MySQL reserved words,
need to be backquoted (`...`) when used in SQL statements.

Caution: as information fetching and info tables filling happen at the
same time, info tables may contain inaccurate information about
themselves.

=head1 OPTIONS

=over 4

=item --clear

Does DROP TABLE on the info tables (only those that the program is
going to fill, for example if you do not use --col it won't drop
the `col` table) and processes normally. Does not drop database_to_write.

=item --clear-only

Same as --clear but exits after the DROPs.

=item --col

Adds columns information (into table `col`).

=item --idx

Adds index information (into table `idx`).

=item --prefix prefix

The info tables are named from the concatenation of prefix and,
respectively, db, tbl (or tbl_status), col, idx. Do not quote ('')
or backquote (``) prefix.

=item -q, --quiet

Does not warn you about what the script is going to do (DROP TABLE etc)
and does not ask for a confirmation before starting.

=item --tbl-status

Instead of using SHOW TABLES, uses SHOW TABLE STATUS
(much more complete information, but slower). 

=item --help

Display helpscreen and exit

=item -u, --user=#         

user for database login if not current user. Give a user
who has sufficient privileges (CREATE, ...).

=item -p, --password=#     

password to use when connecting to server

=item -h, --host=#     

host to connect to

=item -P, --port=#         

port to use when connecting to server

=item -S, --socket=#         

UNIX domain socket to use when connecting to server

=head1 WARRANTY

This software is free and comes without warranty of any kind. You
should never trust backup software without studying the code yourself.
Study the code inside this script and only rely on it if I<you> believe
that it does the right thing for you.

Patches adding bug fixes, documentation and new features are welcome.

=head1 TO DO

Use extended inserts to be faster (for servers with many databases
or tables). But to do that, must care about net-buffer-length.

=head1 AUTHOR

2002-06-18 Guilhem Bichot (guilhem.bichot@mines-paris.org)

And all the authors of mysqlhotcopy, which served as a model for 
the structure of the program.