File Coverage

blib/lib/DBIx/Util/Schema.pm
Criterion Covered Total %
statement 121 164 73.7
branch 51 102 50.0
condition 1 9 11.1
subroutine 15 15 100.0
pod 9 9 100.0
total 197 299 65.8


line stmt bran cond sub pod time code
1             package DBIx::Util::Schema;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2020-05-06'; # DATE
5             our $DIST = 'DBIx-Util-Schema'; # DIST
6             our $VERSION = '0.001'; # VERSION
7              
8 1     1   96840 use 5.010001;
  1         15  
9 1     1   20 use strict 'subs', 'vars';
  1         3  
  1         45  
10 1     1   6 use warnings;
  1         2  
  1         27  
11 1     1   1827 use Log::ger;
  1         56  
  1         5  
12              
13 1     1   262 use List::Util qw(first);
  1         2  
  1         123  
14              
15 1     1   8 use Exporter 'import';
  1         2  
  1         2554  
16             our @EXPORT_OK = qw(
17             table_exists
18             has_table
19             has_all_tables
20             has_any_table
21              
22             column_exists
23             has_column
24             has_all_columns
25             has_any_column
26              
27             list_tables
28             list_columns
29             list_indexes
30             );
31              
32             # TODO:
33             # primary_key_columns
34             # has_primary_key
35             # has_index_on
36             # has_unique_index_on
37             # has_a_unique_index
38              
39             our %SPEC;
40              
41             $SPEC{':package'} = {
42             v => 1.1,
43             summary => 'Utility routines related to database schema',
44             };
45              
46             our %arg0_dbh = (
47             dbh => {
48             schema => ['obj*'],
49             summary => 'DBI database handle',
50             req => 1,
51             pos => 0,
52             },
53             );
54              
55             our %arg1_table = (
56             table => {
57             schema => ['str*'],
58             summary => 'Table name',
59             req => 1,
60             pos => 1,
61             },
62             );
63              
64             our %arg1rest_tables = (
65             tables => {
66             schema => ['array*', of=>'str*'],
67             summary => 'Table names',
68             req => 1,
69             pos => 1,
70             slurpy => 1,
71             },
72             );
73              
74             our %arg2_column = (
75             column => {
76             schema => ['str*'],
77             summary => 'Table column name',
78             req => 1,
79             pos => 2,
80             },
81             );
82              
83             our %arg2rest_columns = (
84             columns => {
85             schema => ['array*', of=>'str*'],
86             summary => 'Table column names',
87             req => 1,
88             pos => 2,
89             slurpy => 1,
90             },
91             );
92              
93             $SPEC{has_table} = {
94             v => 1.1,
95             summary => 'Check whether database has a certain table',
96             args => {
97             %arg0_dbh,
98             %arg1_table,
99             },
100             args_as => "array",
101             result_naked => 1,
102             };
103             sub has_table {
104 15     15 1 61812 my ($dbh, $table) = @_;
105 15         25 my $sth;
106 15 50       40 if ($table =~ /(.+)\.(.+)/) {
107 0         0 $sth = $dbh->table_info(undef, $1, $2, undef);
108             } else {
109 15         94 $sth = $dbh->table_info(undef, undef, $table, undef);
110             }
111              
112 15 100       10591 $sth->fetchrow_hashref ? 1:0;
113             }
114              
115             # alias for has_table
116             $SPEC{table_exists} = { %{$SPEC{has_table}}, summary=>'Alias for has_table()' };
117             *table_exists = \&has_table;
118              
119             $SPEC{has_all_tables} = {
120             v => 1.1,
121             summary => 'Check whether database has all specified tables',
122             args => {
123             %arg0_dbh,
124             %arg1rest_tables,
125             },
126             args_as => "array",
127             result_naked => 1,
128             };
129             sub has_all_tables {
130 3     3 1 2818 my ($dbh, @tables) = @_;
131 3         6 my $sth;
132 3         7 for my $table (@tables) {
133 6 50       18 if ($table =~ /(.+)\.(.+)/) {
134 0         0 $sth = $dbh->table_info(undef, $1, $2, undef);
135             } else {
136 6         32 $sth = $dbh->table_info(undef, undef, $table, undef);
137             }
138 6 100       2135 return 0 unless $sth->fetchrow_hashref;
139             }
140 2         61 1;
141             }
142              
143             $SPEC{has_any_table} = {
144             v => 1.1,
145             summary => 'Check whether database has at least one of specified tables',
146             args => {
147             %arg0_dbh,
148             %arg1rest_tables,
149             },
150             args_as => "array",
151             result_naked => 1,
152             };
153             sub has_any_table {
154 4     4 1 2550 my ($dbh, @tables) = @_;
155 4         8 my $sth;
156 4         8 for my $table (@tables) {
157 5 50       17 if ($table =~ /(.+)\.(.+)/) {
158 0         0 $sth = $dbh->table_info(undef, $1, $2, undef);
159             } else {
160 5         30 $sth = $dbh->table_info(undef, undef, $table, undef);
161             }
162 5 100       1912 return 1 if $sth->fetchrow_hashref;
163             }
164 1 50       17 @tables ? 0 : 1;
165             }
166              
167             $SPEC{has_column} = {
168             v => 1.1,
169             summary => 'Check whether a table has a specified column',
170             args => {
171             %arg0_dbh,
172             %arg1_table,
173             %arg2_column,
174             },
175             args_as => "array",
176             result_naked => 1,
177             };
178             sub has_column {
179 5     5 1 2436 my ($dbh, $table, $column) = @_;
180 5 50       13 return 0 unless has_table($dbh, $table);
181 5         136 my @columns = list_columns($dbh, $table);
182 5 100       13 (grep {$_->{COLUMN_NAME} eq $column} @columns) ? 1:0;
  17         81  
183             }
184              
185             # alias for has_column
186             $SPEC{column_exists} = { %{$SPEC{has_column}}, summary=>'Alias for has_column()' };
187             *column_exists = \&has_column;
188              
189             $SPEC{has_all_columns} = {
190             v => 1.1,
191             summary => 'Check whether a table has all specified columns',
192             args => {
193             %arg0_dbh,
194             %arg1_table,
195             %arg2rest_columns,
196             },
197             args_as => "array",
198             result_naked => 1,
199             };
200             sub has_all_columns {
201 3     3 1 2562 my ($dbh, $table, @columns) = @_;
202 3 50       9 return 0 unless has_table($dbh, $table);
203 3         83 my @all_columns = list_columns($dbh, $table);
204 3         11 for my $column (@columns) {
205 8 100       14 unless (grep {$_->{COLUMN_NAME} eq $column} @all_columns) { return 0 }
  40         79  
  1         12  
206             }
207 2         26 1;
208             }
209              
210             $SPEC{has_any_column} = {
211             v => 1.1,
212             summary => 'Check whether a table has at least one of specified columns',
213             args => {
214             %arg0_dbh,
215             %arg1_table,
216             %arg2rest_columns,
217             },
218             args_as => "array",
219             result_naked => 1,
220             };
221             sub has_any_column {
222 4     4 1 2509 my ($dbh, $table, @columns) = @_;
223 4 50       10 return 0 unless has_table($dbh, $table);
224 4         115 my @all_columns = list_columns($dbh, $table);
225 4         15 for my $column (@columns) {
226 4 100       9 if (grep {$_->{COLUMN_NAME} eq $column} @all_columns) { return 1 }
  20         48  
  3         39  
227             }
228 1 50       17 @columns ? 0:1;
229             }
230              
231             $SPEC{list_tables} = {
232             v => 1.1,
233             summary => 'List table names in a database',
234             args => {
235             %arg0_dbh,
236             },
237             args_as => "array",
238             result_naked => 1,
239             };
240             sub list_tables {
241 2     2 1 2820 my ($dbh) = @_;
242              
243 2         29 my $driver = $dbh->{Driver}{Name};
244              
245 2         6 my @res;
246 2         13 my $sth = $dbh->table_info(undef, undef, undef, undef);
247 2         694 while (my $row = $sth->fetchrow_hashref) {
248 12         29 my $name = $row->{TABLE_NAME};
249 12         22 my $schem = $row->{TABLE_SCHEM};
250 12         17 my $type = $row->{TABLE_TYPE};
251              
252 12 50       25 if ($driver eq 'mysql') {
253             # mysql driver returns database name as schema, so that's useless
254 0         0 $schem = '';
255             }
256              
257 12 50       22 next if $type eq 'VIEW';
258 12 100       71 next if $type eq 'INDEX';
259 8 50       19 next if $schem =~ /^(information_schema)$/;
260              
261 8 50       21 if ($driver eq 'Pg') {
    50          
262 0 0       0 next if $schem =~ /^(pg_catalog)$/;
263             } elsif ($driver eq 'SQLite') {
264 8 100       46 next if $schem =~ /^(temp)$/;
265 6 100       46 next if $name =~ /^(sqlite_master|sqlite_temp_master)$/;
266             }
267              
268 4 50       87 push @res, join(
269             "",
270             $schem,
271             length($schem) ? "." : "",
272             $name,
273             );
274             }
275 2         36 sort @res;
276             }
277              
278             $SPEC{list_indexes} = {
279             v => 1.1,
280             summary => 'List indexes for a table in a database',
281             description => <<'_',
282              
283             SQLite notes: information is retrieved from DBI's table_info(). Autoindex for
284             primary key is not listed using table_info(), but this function adds it by
285             looking at `sqlite_master` table.
286              
287             MySQL notes: information is retrieved from statistics_info(). Note that a
288             multi-column index is reported as separate rows by statistics_info(), one for
289             each indexed column. But this function merges them and returns the list of
290             columns in `columns`.
291              
292             _
293             args => {
294             %arg0_dbh,
295             %arg1_table,
296             },
297             args_as => "array",
298             result_naked => 1,
299             };
300             sub list_indexes {
301 1     1 1 2766 my ($dbh, $wanted_table) = @_;
302              
303 1         13 my $driver = $dbh->{Driver}{Name};
304              
305 1         5 my @res;
306              
307 1 50       6 if ($driver eq 'SQLite') {
    0          
308              
309 1         2 my @wanted_tables;
310 1 50       4 if (defined $wanted_table) {
311 0         0 @wanted_tables = ($wanted_table);
312             } else {
313 1         3 @wanted_tables = list_tables($dbh);
314             }
315 1 50       6 for (@wanted_tables) { $_ = $1 if /.+\.(.+)/ }
  2         15  
316              
317 1         7 my $sth = $dbh->prepare("SELECT * FROM sqlite_master");
318 1         132 $sth->execute;
319 1         25 while (my $row = $sth->fetchrow_hashref) {
320 4 100       54 next unless $row->{type} eq 'index';
321 2 50       6 next unless grep { $_ eq $row->{tbl_name} } @wanted_tables;
  4         15  
322 2 100       20 next unless $row->{name} =~ /\Asqlite_autoindex_.+_(\d+)\z/;
323 1         4 my $col_num = $1;
324 1         4 my @cols = list_columns($dbh, $row->{tbl_name});
325             push @res, {
326             name => "PRIMARY",
327             table => $row->{tbl_name},
328 1         30 columns => [$cols[$col_num-1]{COLUMN_NAME}],
329             is_unique => 1,
330             is_pk => 1,
331             };
332             }
333              
334 1         8 $sth = $dbh->table_info(undef, undef, undef, undef);
335             ROW:
336 1         323 while (my $row = $sth->fetchrow_hashref) {
337 6 100       82 next unless $row->{TABLE_TYPE} eq 'INDEX';
338              
339 2         4 my $table = $row->{TABLE_NAME};
340 2         5 my $schem = $row->{TABLE_SCHEM};
341              
342             # match table name
343 2 50       6 if (defined $wanted_table) {
344 0 0       0 if ($wanted_table =~ /(.+)\.(.+)/) {
345 0 0 0     0 next unless $schem eq $1 && $table eq $2;
346             } else {
347 0 0       0 next unless $table eq $wanted_table;
348             }
349             }
350              
351 2 100       19 next unless my $sql = $row->{sqlite_sql};
352             #use DD; dd $row;
353 1 50       9 $sql =~ s/\A\s*CREATE\s+(UNIQUE\s+)?INDEX\s+//is or do {
354 0         0 log_trace "Not a CREATE INDEX statement, skipped: $row->{sqlite_sql}";
355 0         0 next ROW;
356             };
357              
358 1 50       6 $row->{is_unique} = $1 ? 1:0; # not-standard, backward compat
359 1 50       5 $row->{NON_UNIQUE} = $1 ? 0:1;
360              
361 1 50       7 $sql =~ s/\A(\S+)\s+//s
362             or die "Can't extract index name from sqlite_sql: $sql";
363 1         4 $row->{name} = $1; # non-standard, backward compat
364 1         4 $row->{INDEX_NAME} = $1;
365              
366 1 50       7 $sql =~ s/\AON\s*(\S+)\s*\(\s*(.+)\s*\)//s
367             or die "Can't extract indexed table+columns from sqlite_sql: $sql";
368 1   33     6 $row->{table} = $table // $1; # non-standard, backward-compat
369 1         36 $row->{columns} = [split /\s*,\s*/, $2]; # non-standard
370              
371 1         22 push @res, $row;
372             } # while row
373              
374             } elsif ($driver eq 'mysql') {
375              
376 0         0 my $sth = $dbh->statistics_info(undef, undef, undef, undef, undef);
377 0         0 $sth->execute;
378 0         0 my @res0;
379 0         0 while (my $row = $sth->fetchrow_hashref) {
380 0 0       0 if (defined $wanted_table) {
381 0 0       0 if ($wanted_table =~ /(.+)\.(.+)/) {
382 0 0 0     0 next unless $row->{TABLE_SCHEM} eq $1 && $row->{TABLE_NAME} eq $2;
383             } else {
384 0 0       0 next unless $row->{TABLE_NAME} eq $wanted_table;
385             }
386             }
387 0         0 $row->{table} = $row->{TABLE_NAME}; # non-standard, backward-compat
388 0         0 $row->{name} = $row->{INDEX_NAME}; # non-standard, backward-compat
389 0 0       0 $row->{is_unique} = $row->{NON_UNIQUE} ? 0:1; # non-standard, backward-compat
390 0 0       0 $row->{is_pk} = $row->{INDEX_NAME} eq 'PRIMARY' ? 1:0; # non-standard, backward-compat
391              
392 0         0 push @res0, $row;
393             }
394              
395             # merge separated per-indexed-column result into a single all-columns
396             # result
397 0         0 my @index_names;
398 0 0       0 for my $row (@res0) { push @index_names, $row->{INDEX_NAME} unless grep { $row->{INDEX_NAME} eq $_ } @index_names }
  0         0  
  0         0  
399 0         0 for my $index_name (@index_names) {
400 0         0 my @hashes = grep { $_->{INDEX_NAME} eq $index_name } @res0;
  0         0  
401 0 0       0 if (@hashes == 1) {
402 0         0 push @res, $hashes[0];
403             } else {
404 0         0 my %merged_hash;
405 0         0 $merged_hash{columns} = [];
406 0         0 for my $hash (@hashes) {
407 0         0 $merged_hash{columns}[ $hash->{ORDINAL_POSITION}-1 ] = $hash->{COLUMN_NAME};
408 0         0 for (keys %$hash) { $merged_hash{$_} = $hash->{$_} }
  0         0  
409             }
410 0         0 delete $merged_hash{ORDINAL_POSITION};
411 0         0 push @res, \%merged_hash;
412             }
413             }
414              
415             } else {
416              
417 0         0 die "Driver $driver is not yet supported for list_indexes";
418             }
419              
420 1         27 @res;
421             }
422              
423             $SPEC{list_columns} = {
424             v => 1.1,
425             summary => 'List columns of a table',
426             args => {
427             %arg0_dbh,
428             %arg1_table,
429             },
430             args_as => "array",
431             result_naked => 1,
432             };
433             sub list_columns {
434 14     14 1 2870 my ($dbh, $table) = @_;
435              
436 14         21 my @res;
437 14         25 my ($schema, $utable);
438 14 50       46 if ($table =~ /\./) {
439 0         0 ($schema, $utable) = split /\./, $table;
440             } else {
441 14         24 $schema = undef;
442 14         25 $utable = $table;
443             }
444 14         83 my $sth = $dbh->column_info(undef, $schema, $utable, undef);
445 14         15597 while (my $row = $sth->fetchrow_hashref) {
446 58         1315 push @res, $row;
447             }
448 14         703 sort @res;
449             }
450              
451             1;
452             # ABSTRACT: Utility routines related to database schema
453              
454             __END__