| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
############################################################################## |
|
2
|
|
|
|
|
|
|
# DBD::TreeData Module # |
|
3
|
|
|
|
|
|
|
# E-mail: Brendan Byrd <Perl@resonatorsoft.org> # |
|
4
|
|
|
|
|
|
|
############################################################################## |
|
5
|
|
|
|
|
|
|
|
|
6
|
|
|
|
|
|
|
############################################################################## |
|
7
|
|
|
|
|
|
|
# DBD::TreeData |
|
8
|
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
package DBD::TreeData; |
|
10
|
|
|
|
|
|
|
|
|
11
|
1
|
|
|
1
|
|
49951
|
use sanity; |
|
|
1
|
|
|
|
|
560486
|
|
|
|
1
|
|
|
|
|
15
|
|
|
12
|
|
|
|
|
|
|
|
|
13
|
1
|
|
|
1
|
|
582448
|
use parent qw(DBD::AnyData); |
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
8
|
|
|
14
|
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
# ABSTRACT: DBI driver for any abstract hash/array tree |
|
16
|
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
our $VERSION = '0.90'; # VERSION |
|
18
|
|
|
|
|
|
|
our $drh = undef; # holds driver handle once initialized |
|
19
|
|
|
|
|
|
|
our $err = 0; # DBI::err |
|
20
|
|
|
|
|
|
|
our $errstr = ""; # DBI::errstr |
|
21
|
|
|
|
|
|
|
our $sqlstate = ""; # DBI::state |
|
22
|
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
our $methods_already_installed = 0; |
|
24
|
|
|
|
|
|
|
|
|
25
|
|
|
|
|
|
|
sub driver { |
|
26
|
|
|
|
|
|
|
return $drh if $drh; # already created - return same one |
|
27
|
|
|
|
|
|
|
my ($class, $attr) = @_; |
|
28
|
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
$drh = $class->DBI::DBD::SqlEngine::driver({ # DBD::AnyData doesn't pass over our $attr stuff, so let DBI::DBD::SqlEngine handle it |
|
30
|
|
|
|
|
|
|
'Name' => 'TreeData', |
|
31
|
|
|
|
|
|
|
'Version' => $VERSION, |
|
32
|
|
|
|
|
|
|
'Err' => \$DBD::TreeData::err, |
|
33
|
|
|
|
|
|
|
'Errstr' => \$DBD::TreeData::errstr, |
|
34
|
|
|
|
|
|
|
'State' => \$DBD::TreeData::state, |
|
35
|
|
|
|
|
|
|
'Attribution' => 'DBD::TreeData by Brendan Byrd', |
|
36
|
|
|
|
|
|
|
}) || return undef; |
|
37
|
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
unless ( $methods_already_installed++ ) { |
|
39
|
|
|
|
|
|
|
DBD::TreeData::dr->install_method('tree_process_hash_tree'); |
|
40
|
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
# because of the DBD::AnyData driver by-pass, we have to do its dirty work |
|
42
|
|
|
|
|
|
|
DBD::TreeData::db->install_method('ad_import'); |
|
43
|
|
|
|
|
|
|
DBD::TreeData::db->install_method('ad_catalog'); |
|
44
|
|
|
|
|
|
|
DBD::TreeData::db->install_method('ad_convert'); |
|
45
|
|
|
|
|
|
|
DBD::TreeData::db->install_method('ad_export'); |
|
46
|
|
|
|
|
|
|
DBD::TreeData::db->install_method('ad_clear'); |
|
47
|
|
|
|
|
|
|
DBD::TreeData::db->install_method('ad_dump'); |
|
48
|
|
|
|
|
|
|
} |
|
49
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
return $drh; |
|
51
|
|
|
|
|
|
|
} |
|
52
|
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
sub CLONE { |
|
54
|
|
|
|
|
|
|
undef $drh; |
|
55
|
|
|
|
|
|
|
} |
|
56
|
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
1; |
|
58
|
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
############################################################################## |
|
61
|
|
|
|
|
|
|
# DBD::TreeData::dr |
|
62
|
|
|
|
|
|
|
|
|
63
|
|
|
|
|
|
|
package # hide from PAUSE |
|
64
|
|
|
|
|
|
|
DBD::TreeData::dr; # ====== DRIVER ====== |
|
65
|
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
use sanity 0.94; |
|
67
|
|
|
|
|
|
|
use DBI 1.619; # first version with tree_ prefix |
|
68
|
|
|
|
|
|
|
use DBD::AnyData 0.110; |
|
69
|
|
|
|
|
|
|
use parent qw(-norequire DBD::AnyData::dr); # no such file as ::dr.pm |
|
70
|
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
use List::AllUtils qw(none any uniq firstidx indexes); |
|
72
|
|
|
|
|
|
|
use Scalar::Util qw(reftype looks_like_number); |
|
73
|
|
|
|
|
|
|
use Lingua::EN::Inflect::Phrase qw(to_PL to_S); |
|
74
|
|
|
|
|
|
|
use Data::Dumper; |
|
75
|
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
use subs qw(foundin notin col2word print_debug); |
|
77
|
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
our @dbh; |
|
79
|
|
|
|
|
|
|
our $debug = 0; |
|
80
|
|
|
|
|
|
|
our $VERSION = $DBD::TreeData::VERSION; |
|
81
|
|
|
|
|
|
|
our $imp_data_size = 0; |
|
82
|
|
|
|
|
|
|
our ($tables, $columns, $ids, $types, $can_null); |
|
83
|
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
sub connect { |
|
85
|
|
|
|
|
|
|
my ($drh, $dr_dsn, $user, $auth, $attr) = @_; |
|
86
|
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
if ($dr_dsn =~ /\;|\=/) { # is DSN notation |
|
88
|
|
|
|
|
|
|
foreach my $var (split /\;/, $dr_dsn) { |
|
89
|
|
|
|
|
|
|
my ($attr_name, $attr_value) = split(/\=/, $var, 2); |
|
90
|
|
|
|
|
|
|
return $drh->set_err($DBI::stderr, "Can't parse DSN part '$var'", '08001') unless (defined $attr_value); |
|
91
|
|
|
|
|
|
|
|
|
92
|
|
|
|
|
|
|
$attr_name = lc($attr_name); |
|
93
|
|
|
|
|
|
|
$attr_name = 'tree_'.$attr_name unless ($attr_name =~ /^tree_/o); |
|
94
|
|
|
|
|
|
|
$attr->{$attr_name} = $attr_value; |
|
95
|
|
|
|
|
|
|
} |
|
96
|
|
|
|
|
|
|
} |
|
97
|
|
|
|
|
|
|
else { |
|
98
|
|
|
|
|
|
|
$attr->{tree_table_name} ||= $dr_dsn; |
|
99
|
|
|
|
|
|
|
} |
|
100
|
|
|
|
|
|
|
$attr->{tree_table_name} ||= 'tree_data'; |
|
101
|
|
|
|
|
|
|
$debug = $attr->{tree_debug} || $attr->{TraceLevel} || $drh->{TraceLevel}; |
|
102
|
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
# Run through the tree conversion |
|
104
|
|
|
|
|
|
|
$attr->{tree_data} or return $drh->set_err($DBI::stderr, "Data! Data! Data! I cannot make bricks without clay!", '08004'); |
|
105
|
|
|
|
|
|
|
$drh->tree_process_hash_tree($attr->{tree_table_name}, $attr->{tree_data}, 0); |
|
106
|
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
# remove global data and keep the local tree in $tref |
|
108
|
|
|
|
|
|
|
my $tref = $tables; |
|
109
|
|
|
|
|
|
|
$attr->{tree_columns} = { |
|
110
|
|
|
|
|
|
|
names => { map { $_ => $tref->{$_}{columns} } keys %$tref }, |
|
111
|
|
|
|
|
|
|
types => $types, |
|
112
|
|
|
|
|
|
|
nulls => $can_null, |
|
113
|
|
|
|
|
|
|
}; |
|
114
|
|
|
|
|
|
|
$attr->{tree_cardinality} = $ids->{table}; |
|
115
|
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
### TODO: Clean this up ### |
|
117
|
|
|
|
|
|
|
undef $tables; |
|
118
|
|
|
|
|
|
|
undef $columns; |
|
119
|
|
|
|
|
|
|
undef $ids; |
|
120
|
|
|
|
|
|
|
undef $types; |
|
121
|
|
|
|
|
|
|
undef $can_null; |
|
122
|
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
# Add into our $dbh object, using AnyData's methods |
|
124
|
|
|
|
|
|
|
my ($outer_dbh, $dbh) = DBI::_new_dbh($drh, { |
|
125
|
|
|
|
|
|
|
Name => $attr->{tree_table_name}, |
|
126
|
|
|
|
|
|
|
}, $attr); |
|
127
|
|
|
|
|
|
|
$dbh->func( 0, "init_default_attributes" ); # make sure we get all of the right sql_* vars in place |
|
128
|
|
|
|
|
|
|
$dbh->func("init_done"); |
|
129
|
|
|
|
|
|
|
$dbh->STORE('Active', 1); |
|
130
|
|
|
|
|
|
|
|
|
131
|
|
|
|
|
|
|
### TODO: Need error checking for tree_rename_tables ### |
|
132
|
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
foreach my $table (keys %$tref) { |
|
134
|
|
|
|
|
|
|
my $table_name = exists $attr->{'tree_rename_tables'} ? |
|
135
|
|
|
|
|
|
|
($attr->{'tree_rename_tables'}{$table} || $table) : $table; |
|
136
|
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
$dbh->func($table_name, 'ARRAY', [@{$tref->{$table}{data}}], { |
|
138
|
|
|
|
|
|
|
col_names => join(',', @{$tref->{$table}{columns}}), |
|
139
|
|
|
|
|
|
|
}, 'ad_import'); |
|
140
|
|
|
|
|
|
|
} |
|
141
|
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
# Using the DBD::AnyData $dbh for the rest of the work |
|
143
|
|
|
|
|
|
|
push @dbh, $dbh; |
|
144
|
|
|
|
|
|
|
return $outer_dbh; |
|
145
|
|
|
|
|
|
|
} |
|
146
|
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
sub data_sources { |
|
148
|
|
|
|
|
|
|
# Typically no need for parameters, as the defaults work just fine... |
|
149
|
|
|
|
|
|
|
return ('dbi:TreeData:'); |
|
150
|
|
|
|
|
|
|
} |
|
151
|
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
sub disconnect_all { |
|
153
|
|
|
|
|
|
|
while (my $dbh = shift @dbh) { |
|
154
|
|
|
|
|
|
|
ref $dbh && $dbh->disconnect; |
|
155
|
|
|
|
|
|
|
} |
|
156
|
|
|
|
|
|
|
return 1; |
|
157
|
|
|
|
|
|
|
} |
|
158
|
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
sub tree_process_hash_tree ($$$;$) { |
|
160
|
|
|
|
|
|
|
my ($drh, $col, $tree, $depth) = @_; |
|
161
|
|
|
|
|
|
|
my ($col_id, $serialized_tree); |
|
162
|
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
if ($depth > 100) { |
|
164
|
|
|
|
|
|
|
$drh->set_err(0, "Too deep down the rabbit hole; crawling back..."); |
|
165
|
|
|
|
|
|
|
return $col => undef; |
|
166
|
|
|
|
|
|
|
} |
|
167
|
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
print_debug($depth, "$depth - $col => ".(reftype($tree) || substr($tree, 0, 30))); |
|
169
|
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
state $id_names = ['group', 'matrix', 'cube', 'hypercube']; # if you go past here, you've gone too far... |
|
171
|
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
given (reftype $tree) { |
|
173
|
|
|
|
|
|
|
# Common code for both HASHs and ARRAYs |
|
174
|
|
|
|
|
|
|
when (/HASH|ARRAY/) { |
|
175
|
|
|
|
|
|
|
$col = to_S(col2word($col)); |
|
176
|
|
|
|
|
|
|
$col =~ s/ /_/g; |
|
177
|
|
|
|
|
|
|
$col_id = $col.'_id'; |
|
178
|
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
# compare serialized trees for the same IDs |
|
180
|
|
|
|
|
|
|
if ($depth) { # no point if this is the first node |
|
181
|
|
|
|
|
|
|
$serialized_tree = Data::Dumper->new([$tree], ['*XXXX'])-> |
|
182
|
|
|
|
|
|
|
# (options for consistency, for exact matches) |
|
183
|
|
|
|
|
|
|
Reset->Sortkeys(1)-> |
|
184
|
|
|
|
|
|
|
# (options designed to use the smallest possible footprint, as these can get rather large) |
|
185
|
|
|
|
|
|
|
Indent(0)->Quotekeys(0)->Pair('=')->Dump; |
|
186
|
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
# cycle through possible ID names |
|
188
|
|
|
|
|
|
|
my @list = ('', (reftype $tree eq 'ARRAY') ? @$id_names : ()); |
|
189
|
|
|
|
|
|
|
foreach my $suffix (@list) { |
|
190
|
|
|
|
|
|
|
my $id_name = $col.($suffix ? '_'.$suffix : '').'_id'; |
|
191
|
|
|
|
|
|
|
my $tree = $serialized_tree; |
|
192
|
|
|
|
|
|
|
$tree =~ s/^(\W{1,2})XXXX/$1$id_name/; |
|
193
|
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
# already exists, makes this easier |
|
195
|
|
|
|
|
|
|
my $id = $ids->{trees}{$tree}; |
|
196
|
|
|
|
|
|
|
if ($id) { |
|
197
|
|
|
|
|
|
|
print_debug($depth+1, "TREE <=== Dumper match ID ".join(' => ', split(/\|/, $id))); |
|
198
|
|
|
|
|
|
|
return split(/\|/, $id); |
|
199
|
|
|
|
|
|
|
} |
|
200
|
|
|
|
|
|
|
} |
|
201
|
|
|
|
|
|
|
} |
|
202
|
|
|
|
|
|
|
continue; |
|
203
|
|
|
|
|
|
|
} |
|
204
|
|
|
|
|
|
|
# HASHs control the main tables, providing column names and data for the rows |
|
205
|
|
|
|
|
|
|
# Table = $col (plural) |
|
206
|
|
|
|
|
|
|
# ID = $col.'_id' |
|
207
|
|
|
|
|
|
|
when ('HASH') { |
|
208
|
|
|
|
|
|
|
# parse out a table name (with plural form) |
|
209
|
|
|
|
|
|
|
my $table_name = $depth ? to_PL($col) : $col; |
|
210
|
|
|
|
|
|
|
$table_name =~ s/ /_/g; |
|
211
|
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
# now run through the columns and data (with recursive loop goodness) |
|
213
|
|
|
|
|
|
|
my %data = map { |
|
214
|
|
|
|
|
|
|
my ($dat, $id) = ($$tree{$_}, $_); |
|
215
|
|
|
|
|
|
|
# clean up the column names |
|
216
|
|
|
|
|
|
|
$id = col2word($id); |
|
217
|
|
|
|
|
|
|
$id =~ s/ /_/g; |
|
218
|
|
|
|
|
|
|
$drh->tree_process_hash_tree($id => $dat, $depth + 1); |
|
219
|
|
|
|
|
|
|
} keys %$tree; |
|
220
|
|
|
|
|
|
|
### FIXME: don't forget about undef in keys ### |
|
221
|
|
|
|
|
|
|
|
|
222
|
|
|
|
|
|
|
# check the new column names to see if we've seen this table before |
|
223
|
|
|
|
|
|
|
my @cols = ($col_id, sort keys %data); # new ID column first |
|
224
|
|
|
|
|
|
|
my $col_key = join('|', @cols); |
|
225
|
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
if ($columns->{$col_key}) { # known table |
|
227
|
|
|
|
|
|
|
$table_name = $columns->{$col_key}; |
|
228
|
|
|
|
|
|
|
|
|
229
|
|
|
|
|
|
|
print_debug($depth+1, "HASH ===> Found known table '$table_name'"); |
|
230
|
|
|
|
|
|
|
} |
|
231
|
|
|
|
|
|
|
elsif ($tables->{$table_name}) { # errr, known table, but not with this column structure |
|
232
|
|
|
|
|
|
|
my $t; |
|
233
|
|
|
|
|
|
|
foreach my $j ('', 2 .. 200) { # loop through a bunch of potential table names |
|
234
|
|
|
|
|
|
|
my $tname = $table_name.$j; |
|
235
|
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
if ($t = $tables->{$tname}) { |
|
237
|
|
|
|
|
|
|
my @tcols = @{$t->{columns}}; |
|
238
|
|
|
|
|
|
|
my @ucols = uniq(@cols, @tcols); |
|
239
|
|
|
|
|
|
|
# have to be the same ID columns && need to have at least one field in common |
|
240
|
|
|
|
|
|
|
# (remove keys while we're at it) |
|
241
|
|
|
|
|
|
|
if (shift(@tcols) eq shift(@cols) && uniq(@cols, @tcols) < (@tcols + @cols)) { |
|
242
|
|
|
|
|
|
|
my @extra_cols = notin(\@tcols, \@cols); |
|
243
|
|
|
|
|
|
|
|
|
244
|
|
|
|
|
|
|
# new table has extra columns to add |
|
245
|
|
|
|
|
|
|
if (@extra_cols) { |
|
246
|
|
|
|
|
|
|
# add new column names and resort |
|
247
|
|
|
|
|
|
|
my @old_cols = @{$t->{columns}}; |
|
248
|
|
|
|
|
|
|
my @new_cols = ($col_id, sort(@tcols, @extra_cols)); |
|
249
|
|
|
|
|
|
|
my @diff_idx = grep { $old_cols[$_] ne $new_cols[$_] } (0 .. (@new_cols - 1)); |
|
250
|
|
|
|
|
|
|
$t->{columns} = \@new_cols; |
|
251
|
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
unless ($diff_idx[0] > @{$t->{columns}}-1) { |
|
253
|
|
|
|
|
|
|
# well, the new columns aren't on the end, so old data needs to be shuffled |
|
254
|
|
|
|
|
|
|
for (my $l = 0; $l < @{$t->{data}}; $l++) { |
|
255
|
|
|
|
|
|
|
my @data = @{$t->{data}[$l]}; |
|
256
|
|
|
|
|
|
|
my %data = map { $old_cols[$_] => $data[$_] } (0 .. (@data - 1)); # change to hash |
|
257
|
|
|
|
|
|
|
@data = map { $data{$_} } @new_cols; # change to array |
|
258
|
|
|
|
|
|
|
$t->{data}[$l] = \@data; |
|
259
|
|
|
|
|
|
|
} |
|
260
|
|
|
|
|
|
|
} |
|
261
|
|
|
|
|
|
|
|
|
262
|
|
|
|
|
|
|
# remove the old column key and replace with a new one |
|
263
|
|
|
|
|
|
|
delete $columns->{ join('|', @old_cols) }; |
|
264
|
|
|
|
|
|
|
} |
|
265
|
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
# if the new table is missing certain columns, they will insert undefs as needed naturally below |
|
267
|
|
|
|
|
|
|
# however, nullability checks might be in order |
|
268
|
|
|
|
|
|
|
my @missing_cols = notin(\@cols, \@tcols); |
|
269
|
|
|
|
|
|
|
$can_null->{$_} = 1 for (@missing_cols, @extra_cols); |
|
270
|
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
print_debug($depth+1, "HASH ===> Found known table with different columns '$table_name'"); |
|
272
|
|
|
|
|
|
|
last; |
|
273
|
|
|
|
|
|
|
} |
|
274
|
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
# wrong table to use; try next name |
|
276
|
|
|
|
|
|
|
next; |
|
277
|
|
|
|
|
|
|
} |
|
278
|
|
|
|
|
|
|
else { # just treat this as as new table, then |
|
279
|
|
|
|
|
|
|
$drh->set_err(0, "Found a table with a dupe name, but totally different columns; calling it '$tname'...") if ($j); |
|
280
|
|
|
|
|
|
|
$table_name = $tname; |
|
281
|
|
|
|
|
|
|
$tables->{$table_name} = $t = { |
|
282
|
|
|
|
|
|
|
columns => [@cols], |
|
283
|
|
|
|
|
|
|
data => [], |
|
284
|
|
|
|
|
|
|
}; |
|
285
|
|
|
|
|
|
|
|
|
286
|
|
|
|
|
|
|
print_debug($depth+1, "HASH ===> Creating new table '$table_name' because of conflicting columns"); |
|
287
|
|
|
|
|
|
|
last; |
|
288
|
|
|
|
|
|
|
} |
|
289
|
|
|
|
|
|
|
} |
|
290
|
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
$col_key = join('|', @{$t->{columns}}); |
|
292
|
|
|
|
|
|
|
$columns->{$col_key} = $table_name; |
|
293
|
|
|
|
|
|
|
} |
|
294
|
|
|
|
|
|
|
else { # new table |
|
295
|
|
|
|
|
|
|
$tables->{$table_name} = { |
|
296
|
|
|
|
|
|
|
columns => [@cols], |
|
297
|
|
|
|
|
|
|
data => [], |
|
298
|
|
|
|
|
|
|
}; |
|
299
|
|
|
|
|
|
|
$columns->{$col_key} = $table_name; |
|
300
|
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
print_debug($depth+1, "HASH ===> Creating new table '$table_name'"); |
|
302
|
|
|
|
|
|
|
} |
|
303
|
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
# Add new row |
|
305
|
|
|
|
|
|
|
my $t = $tables->{$table_name}; |
|
306
|
|
|
|
|
|
|
my $id = ++($ids->{table}{$table_name}); |
|
307
|
|
|
|
|
|
|
$serialized_tree =~ s/^(\W{1,2})XXXX/$1$col_id/; |
|
308
|
|
|
|
|
|
|
$ids->{trees}{$serialized_tree} = $col_id.'|'.$id; |
|
309
|
|
|
|
|
|
|
push(@{$t->{data}}, [ $id, map { $data{$_} } grep { $_ ne $col_id } @{$t->{columns}} ]); |
|
310
|
|
|
|
|
|
|
|
|
311
|
|
|
|
|
|
|
# Since we're done with this table, send back the col_id and id# |
|
312
|
|
|
|
|
|
|
print_debug($depth+1, "HASH <=== $col_id => $id"); |
|
313
|
|
|
|
|
|
|
$types->{$col_id} = 'ID'; |
|
314
|
|
|
|
|
|
|
return $col_id => $id; |
|
315
|
|
|
|
|
|
|
} |
|
316
|
|
|
|
|
|
|
# ARRAYs provide ID grouping tables, capturing the individual rows in a group |
|
317
|
|
|
|
|
|
|
# These are going to be two-column tables with two different IDs |
|
318
|
|
|
|
|
|
|
# Table = $col.'_groups' (plural) |
|
319
|
|
|
|
|
|
|
# ID = $col.(group|matrix|cube|etc.).'_id' |
|
320
|
|
|
|
|
|
|
when ('ARRAY') { |
|
321
|
|
|
|
|
|
|
# Pass the data on down first (ARRAY of ARRAYS to prevent de-duplication of keys) |
|
322
|
|
|
|
|
|
|
my @data = map { |
|
323
|
|
|
|
|
|
|
my $dat = $_; |
|
324
|
|
|
|
|
|
|
[ $drh->tree_process_hash_tree($col => $dat, $depth + 1) ] |
|
325
|
|
|
|
|
|
|
} @$tree; |
|
326
|
|
|
|
|
|
|
|
|
327
|
|
|
|
|
|
|
# Okay, we could end up with several different scenarios: |
|
328
|
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
# A. All items have the same column name (as a ID) |
|
330
|
|
|
|
|
|
|
# B. All items appear to be some form of data |
|
331
|
|
|
|
|
|
|
# C. A mixture of IDs and data (scary!) |
|
332
|
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
# Process both groups individually (and hope for the best) |
|
334
|
|
|
|
|
|
|
my @id_cols = grep { $data[$_]->[0] =~ /_id$/; } (0 .. (@data - 1)); |
|
335
|
|
|
|
|
|
|
my @data_cols = grep { $data[$_]->[0] !~ /_id$/; } (0 .. (@data - 1)); |
|
336
|
|
|
|
|
|
|
@id_cols = () unless ($depth); # skip any group ID tables if this is the very first node |
|
337
|
|
|
|
|
|
|
|
|
338
|
|
|
|
|
|
|
$drh->set_err(0, "Inconsistant sets of data within an array near '$col'; going to process it as best as possible...") if (@id_cols && @data_cols); |
|
339
|
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
# Items of IDs |
|
341
|
|
|
|
|
|
|
### TODO: Clean this up; the logic is a bit of a mess... ### |
|
342
|
|
|
|
|
|
|
my (@max_id, @group_id); |
|
343
|
|
|
|
|
|
|
foreach my $ii (@id_cols, @data_cols) { |
|
344
|
|
|
|
|
|
|
# In all cases, there will be two tables to populate: a group/id table, and a id/data (or id/id) table |
|
345
|
|
|
|
|
|
|
my ($icol, $item) = @{$data[$ii]}; |
|
346
|
|
|
|
|
|
|
my $is_id = ($icol =~ /_id$/i); |
|
347
|
|
|
|
|
|
|
|
|
348
|
|
|
|
|
|
|
# IDs are singular; table names are plural |
|
349
|
|
|
|
|
|
|
my $strip = to_S(col2word($icol)); |
|
350
|
|
|
|
|
|
|
$icol = $strip; |
|
351
|
|
|
|
|
|
|
$icol =~ s/ /_/g; |
|
352
|
|
|
|
|
|
|
$icol .= '_id' if ($is_id); |
|
353
|
|
|
|
|
|
|
|
|
354
|
|
|
|
|
|
|
# Process group ID names |
|
355
|
|
|
|
|
|
|
# ncol = N+1, icol = N (as in _group_id => _id, or _matrix_id => _group_id) |
|
356
|
|
|
|
|
|
|
my $ncol = $icol; |
|
357
|
|
|
|
|
|
|
$ncol =~ s/_id$//i; |
|
358
|
|
|
|
|
|
|
my $i = firstidx { $ncol =~ s/(?<=_)$_$//; } @$id_names; # that's underscore + $_ + EOL |
|
359
|
|
|
|
|
|
|
# $i = -1 if not found, which then ++$i = 0 and id_names = _group |
|
360
|
|
|
|
|
|
|
|
|
361
|
|
|
|
|
|
|
if (++$i > 3) { # start whining here |
|
362
|
|
|
|
|
|
|
$ncol .= '_hypercube_'.$id_names->[$i -= 4]; |
|
363
|
|
|
|
|
|
|
|
|
364
|
|
|
|
|
|
|
$drh->set_err(0, "Seriously?! We're using ridiculous names like '$ncol"."_id' at this point..."); |
|
365
|
|
|
|
|
|
|
} |
|
366
|
|
|
|
|
|
|
else { $ncol .= '_'.$id_names->[$i]; } |
|
367
|
|
|
|
|
|
|
$i++; # prevent -1 on @_id arrays |
|
368
|
|
|
|
|
|
|
|
|
369
|
|
|
|
|
|
|
# Parse out a group table name (with plural form) |
|
370
|
|
|
|
|
|
|
my $grp_table_name = to_S(col2word($ncol)); |
|
371
|
|
|
|
|
|
|
$grp_table_name = to_PL($grp_table_name); # like blah_groups |
|
372
|
|
|
|
|
|
|
$grp_table_name =~ s/ /_/g; |
|
373
|
|
|
|
|
|
|
|
|
374
|
|
|
|
|
|
|
$icol .= '_id' unless ($icol =~ /_id$/); |
|
375
|
|
|
|
|
|
|
$ncol .= '_id' unless ($ncol =~ /_id$/); |
|
376
|
|
|
|
|
|
|
$max_id[$i] = $ncol; |
|
377
|
|
|
|
|
|
|
print_debug($depth+1, "ARRAY ===> max_id = $i/$ncol"); |
|
378
|
|
|
|
|
|
|
|
|
379
|
|
|
|
|
|
|
# Create new group table (if it doesn't already exist) |
|
380
|
|
|
|
|
|
|
my $t; |
|
381
|
|
|
|
|
|
|
if ($depth) { # skip any group ID tables if this is the very first node |
|
382
|
|
|
|
|
|
|
unless ($tables->{$grp_table_name}) { |
|
383
|
|
|
|
|
|
|
### FIXME: Assuming that table doesn't exist with the same columns ### |
|
384
|
|
|
|
|
|
|
print_debug($depth+1, "ARRAY ===> Creating new group table '$grp_table_name'"); |
|
385
|
|
|
|
|
|
|
|
|
386
|
|
|
|
|
|
|
$tables->{$grp_table_name} = { |
|
387
|
|
|
|
|
|
|
columns => [ $ncol, $icol ], |
|
388
|
|
|
|
|
|
|
data => [], |
|
389
|
|
|
|
|
|
|
}; |
|
390
|
|
|
|
|
|
|
} |
|
391
|
|
|
|
|
|
|
$t = $tables->{$grp_table_name}; |
|
392
|
|
|
|
|
|
|
my $col_key = join('|', @{$t->{columns}}); |
|
393
|
|
|
|
|
|
|
$columns->{$col_key} = $grp_table_name; |
|
394
|
|
|
|
|
|
|
} |
|
395
|
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
# Add new row |
|
397
|
|
|
|
|
|
|
$group_id[$i] = ++($ids->{table}{$grp_table_name}) unless ($group_id[$i]); # only increment once (per group type) |
|
398
|
|
|
|
|
|
|
if ($is_id) { # ID column: $item = ID, and this goes in a group table (id/data table already processed) |
|
399
|
|
|
|
|
|
|
print_debug($depth+1, "ARRAY ===> $grp_table_name => [ $group_id[$i], $item ] (new ID row for an group table)"); |
|
400
|
|
|
|
|
|
|
push(@{$t->{data}}, [ $group_id[$i], $item ]); |
|
401
|
|
|
|
|
|
|
} |
|
402
|
|
|
|
|
|
|
else { # data column: $item = data, and we process both tables |
|
403
|
|
|
|
|
|
|
my $itbl_name = to_PL($strip); # like blahs |
|
404
|
|
|
|
|
|
|
$itbl_name =~ s/ /_/g; |
|
405
|
|
|
|
|
|
|
my $data_col = $strip; |
|
406
|
|
|
|
|
|
|
$data_col =~ s/ /_/g; |
|
407
|
|
|
|
|
|
|
|
|
408
|
|
|
|
|
|
|
# Create new id table (if it doesn't already exist) |
|
409
|
|
|
|
|
|
|
unless ($tables->{$itbl_name}) { |
|
410
|
|
|
|
|
|
|
print_debug($depth+1, "ARRAY ===> Creating new ID table '$itbl_name'"); |
|
411
|
|
|
|
|
|
|
|
|
412
|
|
|
|
|
|
|
$tables->{$itbl_name} = { |
|
413
|
|
|
|
|
|
|
columns => [ $icol, $data_col ], |
|
414
|
|
|
|
|
|
|
data => [], |
|
415
|
|
|
|
|
|
|
}; |
|
416
|
|
|
|
|
|
|
} |
|
417
|
|
|
|
|
|
|
my $n = $tables->{$itbl_name}; |
|
418
|
|
|
|
|
|
|
my $col_key = join('|', @{$n->{columns}}); |
|
419
|
|
|
|
|
|
|
$columns->{$col_key} = $itbl_name; |
|
420
|
|
|
|
|
|
|
$types->{$icol} = 'ID'; |
|
421
|
|
|
|
|
|
|
|
|
422
|
|
|
|
|
|
|
$max_id[$i-1] = $icol; |
|
423
|
|
|
|
|
|
|
print_debug($depth+1, "ARRAY ===> max_id = ".int($i-1)."/$icol"); |
|
424
|
|
|
|
|
|
|
### FIXME: Assuming that table doesn't exist with the same columns ### |
|
425
|
|
|
|
|
|
|
|
|
426
|
|
|
|
|
|
|
# First, check serial tree with single value |
|
427
|
|
|
|
|
|
|
my $stree = Data::Dumper->new([$item], ['*'.$icol])->Reset->Indent(0)->Dump; |
|
428
|
|
|
|
|
|
|
if ($ids->{trees}{$stree} && $depth) { |
|
429
|
|
|
|
|
|
|
# Add new group row (with proper col_id) |
|
430
|
|
|
|
|
|
|
my $id = (split(/\|/, $ids->{trees}{$stree}))[1]; |
|
431
|
|
|
|
|
|
|
print_debug($depth+1, "ARRAY ===> $grp_table_name => [ $group_id[$i], $id ] (serial tree found)"); |
|
432
|
|
|
|
|
|
|
push(@{$t->{data}}, [ $group_id[$i], $id ] ); |
|
433
|
|
|
|
|
|
|
|
|
434
|
|
|
|
|
|
|
# (no need to add into main table; already exists) |
|
435
|
|
|
|
|
|
|
} |
|
436
|
|
|
|
|
|
|
else { |
|
437
|
|
|
|
|
|
|
# Add new group row (with proper col_id) |
|
438
|
|
|
|
|
|
|
my $id = ++($ids->{table}{$itbl_name}); |
|
439
|
|
|
|
|
|
|
if ($depth) { |
|
440
|
|
|
|
|
|
|
print_debug($depth+1, "ARRAY ===> $grp_table_name => [ $group_id[$i], $id ] (new group row)"); |
|
441
|
|
|
|
|
|
|
push(@{$t->{data}}, [ $group_id[$i], $id ]); |
|
442
|
|
|
|
|
|
|
} |
|
443
|
|
|
|
|
|
|
|
|
444
|
|
|
|
|
|
|
# Add new id row |
|
445
|
|
|
|
|
|
|
$ids->{trees}{$stree} = $icol.'_id|'.$id; |
|
446
|
|
|
|
|
|
|
print_debug($depth+2, "ARRAY ===> $itbl_name => [ $id, $item ] (new ID/data row)"); |
|
447
|
|
|
|
|
|
|
push(@{$n->{data}}, [ $id, $item ]); |
|
448
|
|
|
|
|
|
|
} |
|
449
|
|
|
|
|
|
|
} |
|
450
|
|
|
|
|
|
|
} |
|
451
|
|
|
|
|
|
|
|
|
452
|
|
|
|
|
|
|
# Pass back an ID |
|
453
|
|
|
|
|
|
|
my ($gid_col, $gid) = (pop(@max_id) || $col, pop(@group_id)); # undef @max_id might happen with an empty array |
|
454
|
|
|
|
|
|
|
|
|
455
|
|
|
|
|
|
|
print_debug($depth+1, "ARRAY <=== $gid_col => $gid"); |
|
456
|
|
|
|
|
|
|
$serialized_tree =~ s/^(\W{1,2})XXXX/$1$gid_col/; |
|
457
|
|
|
|
|
|
|
$ids->{trees}{$serialized_tree} = $gid_col.'|'.$gid; |
|
458
|
|
|
|
|
|
|
$types->{$gid_col} = 'ID'; |
|
459
|
|
|
|
|
|
|
return $gid_col => $gid; |
|
460
|
|
|
|
|
|
|
} |
|
461
|
|
|
|
|
|
|
# An actual scalar; return back the proper column name and data |
|
462
|
|
|
|
|
|
|
when ('' || undef) { |
|
463
|
|
|
|
|
|
|
return type_detect($col, $tree); |
|
464
|
|
|
|
|
|
|
} |
|
465
|
|
|
|
|
|
|
# De-reference |
|
466
|
|
|
|
|
|
|
when (/SCALAR|VSTRING/) { |
|
467
|
|
|
|
|
|
|
return type_detect($col, $$tree); |
|
468
|
|
|
|
|
|
|
} |
|
469
|
|
|
|
|
|
|
# Warn and de-reference |
|
470
|
|
|
|
|
|
|
when (/Regexp|LVALUE/i) { |
|
471
|
|
|
|
|
|
|
$drh->set_err(0, "Found a ".(reftype $tree)."; just going to treat this like a SCALAR..."); |
|
472
|
|
|
|
|
|
|
return type_detect($col, $$tree); |
|
473
|
|
|
|
|
|
|
} |
|
474
|
|
|
|
|
|
|
# Warn and de-reference (for further examination) |
|
475
|
|
|
|
|
|
|
when ('REF') { |
|
476
|
|
|
|
|
|
|
$drh->set_err(0, "Found a REF; going to dive in the rabbit hole..."); |
|
477
|
|
|
|
|
|
|
return $drh->tree_process_hash_tree($col => $$tree, $depth + 1); |
|
478
|
|
|
|
|
|
|
} |
|
479
|
|
|
|
|
|
|
# Warn and de-reference (for further examination) |
|
480
|
|
|
|
|
|
|
when ('GLOB') { |
|
481
|
|
|
|
|
|
|
foreach my $t (qw(Regexp VSTRING IO FORMAT LVALUE GLOB REF CODE HASH ARRAY SCALAR)) { # scalar last, since a ref is still a scalar |
|
482
|
|
|
|
|
|
|
if (defined *$$tree{$t}) { |
|
483
|
|
|
|
|
|
|
$drh->set_err(0, "Found a GLOB (which turn out to be a $t); going to dive in the rabbit hole..."); |
|
484
|
|
|
|
|
|
|
return $drh->tree_process_hash_tree($col => *$$tree{$t}, $depth + 1); |
|
485
|
|
|
|
|
|
|
} |
|
486
|
|
|
|
|
|
|
} |
|
487
|
|
|
|
|
|
|
$drh->set_err(0, "Found a GLOB, but it didn't point to anything..."); |
|
488
|
|
|
|
|
|
|
return $col => undef; |
|
489
|
|
|
|
|
|
|
} |
|
490
|
|
|
|
|
|
|
# Warn and throw away |
|
491
|
|
|
|
|
|
|
when ('CODE') { |
|
492
|
|
|
|
|
|
|
### TODO: Warn immediately, eval block with timer to use as output, then continue ### |
|
493
|
|
|
|
|
|
|
### Definitely need a switch, though ### |
|
494
|
|
|
|
|
|
|
$drh->set_err(0, "Found a CODE block; not going to even touch this one..."); |
|
495
|
|
|
|
|
|
|
return $col => undef; |
|
496
|
|
|
|
|
|
|
} |
|
497
|
|
|
|
|
|
|
default { |
|
498
|
|
|
|
|
|
|
$drh->set_err(0, "Found a ".(reftype $tree)."; WTF is this? Can't use this at all..."); |
|
499
|
|
|
|
|
|
|
return $col => undef; |
|
500
|
|
|
|
|
|
|
} |
|
501
|
|
|
|
|
|
|
} |
|
502
|
|
|
|
|
|
|
|
|
503
|
|
|
|
|
|
|
die "WTF?! Perl broke my given/when! Alert the Pumpking!!!"; |
|
504
|
|
|
|
|
|
|
} |
|
505
|
|
|
|
|
|
|
|
|
506
|
|
|
|
|
|
|
# Find items in @B that are in @A |
|
507
|
|
|
|
|
|
|
sub foundin (\@\@) { |
|
508
|
|
|
|
|
|
|
my ($A, $B) = @_; |
|
509
|
|
|
|
|
|
|
return grep { my $i = $_; any { $i eq $_ } @$A; } @$B; |
|
510
|
|
|
|
|
|
|
} |
|
511
|
|
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
# Find items in @B that are not in @A |
|
513
|
|
|
|
|
|
|
sub notin (\@\@) { |
|
514
|
|
|
|
|
|
|
my ($A, $B) = @_; |
|
515
|
|
|
|
|
|
|
return grep { my $i = $_; none { $i eq $_ } @$A; } @$B; |
|
516
|
|
|
|
|
|
|
} |
|
517
|
|
|
|
|
|
|
|
|
518
|
|
|
|
|
|
|
sub col2word ($) { |
|
519
|
|
|
|
|
|
|
my $word = $_[0]; |
|
520
|
|
|
|
|
|
|
$word = lc($word); |
|
521
|
|
|
|
|
|
|
$word =~ s/[\W_]+/ /g; |
|
522
|
|
|
|
|
|
|
$word =~ s/^\s+|\s+(?:id)?$//g; |
|
523
|
|
|
|
|
|
|
return $word; |
|
524
|
|
|
|
|
|
|
} |
|
525
|
|
|
|
|
|
|
|
|
526
|
|
|
|
|
|
|
sub type_detect ($;$) { |
|
527
|
|
|
|
|
|
|
my ($col, $val) = @_; |
|
528
|
|
|
|
|
|
|
my $is_num = looks_like_number($val); |
|
529
|
|
|
|
|
|
|
|
|
530
|
|
|
|
|
|
|
$col = to_S(col2word($col)); # if we're at this point, it's a single item |
|
531
|
|
|
|
|
|
|
$col =~ s/ /_/g; |
|
532
|
|
|
|
|
|
|
unless (defined $val) { |
|
533
|
|
|
|
|
|
|
$can_null->{$_} = 1; |
|
534
|
|
|
|
|
|
|
return $col => undef; |
|
535
|
|
|
|
|
|
|
} |
|
536
|
|
|
|
|
|
|
|
|
537
|
|
|
|
|
|
|
$types->{$col} = 'STRING' if (!$is_num && $types->{$col}); # any non-number data invalidates the NUMBER type |
|
538
|
|
|
|
|
|
|
$types->{$col} ||= $is_num ? 'NUMBER' : 'STRING'; |
|
539
|
|
|
|
|
|
|
return $col => $val; |
|
540
|
|
|
|
|
|
|
} |
|
541
|
|
|
|
|
|
|
|
|
542
|
|
|
|
|
|
|
sub print_debug ($$) { |
|
543
|
|
|
|
|
|
|
my ($depth, $msg) = @_; |
|
544
|
|
|
|
|
|
|
return unless ($debug); |
|
545
|
|
|
|
|
|
|
|
|
546
|
|
|
|
|
|
|
print (" " x $depth); |
|
547
|
|
|
|
|
|
|
say $msg; |
|
548
|
|
|
|
|
|
|
} |
|
549
|
|
|
|
|
|
|
|
|
550
|
|
|
|
|
|
|
1; |
|
551
|
|
|
|
|
|
|
|
|
552
|
|
|
|
|
|
|
############################################################################## |
|
553
|
|
|
|
|
|
|
# DBD::TreeData::db |
|
554
|
|
|
|
|
|
|
|
|
555
|
|
|
|
|
|
|
package # hide from PAUSE |
|
556
|
|
|
|
|
|
|
DBD::TreeData::db; # ====== DATABASE ====== |
|
557
|
|
|
|
|
|
|
|
|
558
|
|
|
|
|
|
|
our $imp_data_size = 0; |
|
559
|
|
|
|
|
|
|
use DBD::AnyData; |
|
560
|
|
|
|
|
|
|
use parent qw(-norequire DBD::AnyData::db); # no such file as ::db.pm |
|
561
|
|
|
|
|
|
|
|
|
562
|
|
|
|
|
|
|
use Config; |
|
563
|
|
|
|
|
|
|
use List::AllUtils qw(first); |
|
564
|
|
|
|
|
|
|
|
|
565
|
|
|
|
|
|
|
# Overriding the package here to add some *_info methods |
|
566
|
|
|
|
|
|
|
|
|
567
|
|
|
|
|
|
|
### TODO: get_info ### |
|
568
|
|
|
|
|
|
|
|
|
569
|
|
|
|
|
|
|
sub table_info { |
|
570
|
|
|
|
|
|
|
my ($dbh, $catalog, $schema, $table) = @_; |
|
571
|
|
|
|
|
|
|
my $names = [qw( TABLE_QUALIFIER TABLE_OWNER TABLE_NAME TABLE_TYPE REMARKS )]; |
|
572
|
|
|
|
|
|
|
|
|
573
|
|
|
|
|
|
|
$table = '^'.$table.'$' if length $table; |
|
574
|
|
|
|
|
|
|
|
|
575
|
|
|
|
|
|
|
return sponge_sth_loader($dbh, 'TABLE_INFO', $names, [ |
|
576
|
|
|
|
|
|
|
grep { !$table || $_->[2] =~ /$table/i } $dbh->func("get_avail_tables") |
|
577
|
|
|
|
|
|
|
] ); |
|
578
|
|
|
|
|
|
|
} |
|
579
|
|
|
|
|
|
|
|
|
580
|
|
|
|
|
|
|
sub column_info { |
|
581
|
|
|
|
|
|
|
my ($dbh, $catalog, $schema, $table, $column) = @_; |
|
582
|
|
|
|
|
|
|
my $type = 'COLUMN_INFO'; |
|
583
|
|
|
|
|
|
|
my $names = [qw( |
|
584
|
|
|
|
|
|
|
TABLE_CAT TABLE_SCHEM TABLE_NAME COLUMN_NAME DATA_TYPE TYPE_NAME COLUMN_SIZE BUFFER_LENGTH DECIMAL_DIGITS |
|
585
|
|
|
|
|
|
|
NUM_PREC_RADIX NULLABLE REMARKS COLUMN_DEF SQL_DATA_TYPE SQL_DATETIME_SUB CHAR_OCTET_LENGTH ORDINAL_POSITION IS_NULLABLE |
|
586
|
|
|
|
|
|
|
CHAR_SET_CAT CHAR_SET_SCHEM CHAR_SET_NAME COLLATION_CAT COLLATION_SCHEM COLLATION_NAME UDT_CAT UDT_SCHEM UDT_NAME |
|
587
|
|
|
|
|
|
|
DOMAIN_CAT DOMAIN_SCHEM DOMAIN_NAME SCOPE_CAT SCOPE_SCHEM SCOPE_NAME MAX_CARDINALITY DTD_IDENTIFIER IS_SELF_REF |
|
588
|
|
|
|
|
|
|
)]; |
|
589
|
|
|
|
|
|
|
|
|
590
|
|
|
|
|
|
|
$table = '^'.$table .'$' if length $table; |
|
591
|
|
|
|
|
|
|
$column = '^'.$column.'$' if length $column; |
|
592
|
|
|
|
|
|
|
|
|
593
|
|
|
|
|
|
|
my @tables = $dbh->func("get_avail_tables"); |
|
594
|
|
|
|
|
|
|
my @col_rows = (); |
|
595
|
|
|
|
|
|
|
my $tc = $dbh->{tree_columns}; |
|
596
|
|
|
|
|
|
|
|
|
597
|
|
|
|
|
|
|
# De-mangle types |
|
598
|
|
|
|
|
|
|
my $types = $dbh->type_info_all; |
|
599
|
|
|
|
|
|
|
shift(@$types); # helper "column key" row |
|
600
|
|
|
|
|
|
|
my %types = map { $_->[0] => $_ } @$types; |
|
601
|
|
|
|
|
|
|
|
|
602
|
|
|
|
|
|
|
foreach my $tbl (sort { $a->[2] cmp $b->[2] } @tables) { # ->[2] = table name |
|
603
|
|
|
|
|
|
|
next unless ($tbl); |
|
604
|
|
|
|
|
|
|
next unless (!$table || $tbl->[2] =~ /$table/i); |
|
605
|
|
|
|
|
|
|
|
|
606
|
|
|
|
|
|
|
my $id = 0; |
|
607
|
|
|
|
|
|
|
foreach my $col ( @{$tc->{names}{$tbl->[2]}} ) { |
|
608
|
|
|
|
|
|
|
next unless (!$column || $col =~ /$column/i); |
|
609
|
|
|
|
|
|
|
my $ti = $types{ $id ? uc($tc->{types}{$col}) : 'PID' }; |
|
610
|
|
|
|
|
|
|
my $can_null = $id && $tc->{nulls}{$col} || 0; |
|
611
|
|
|
|
|
|
|
|
|
612
|
|
|
|
|
|
|
my $col_row = [ |
|
613
|
|
|
|
|
|
|
# 0=TABLE_CAT TABLE_SCHEM TABLE_NAME COLUMN_NAME DATA_TYPE TYPE_NAME COLUMN_SIZE BUFFER_LENGTH DECIMAL_DIGITS |
|
614
|
|
|
|
|
|
|
undef, undef, $tbl->[2], $col, $ti->[0], $ti->[1], $ti->[2], undef, $ti->[17] ? int($ti->[14] * log($ti->[17])/log(10)) : undef, # log(r^l) = l * log(r) |
|
615
|
|
|
|
|
|
|
# 9=NUM_PREC_RADIX NULLABLE REMARKS COLUMN_DEF SQL_DATA_TYPE SQL_DATETIME_SUB CHAR_OCTET_LENGTH ORDINAL_POSITION IS_NULLABLE |
|
616
|
|
|
|
|
|
|
$ti->[17], $can_null, undef, undef, $ti->[15], $ti->[16], $ti->[17] ? undef : $ti->[2], $id, $can_null ? 'YES' : 'NO', |
|
617
|
|
|
|
|
|
|
# 18=CHAR_SET_CAT CHAR_SET_SCHEM CHAR_SET_NAME COLLATION_CAT COLLATION_SCHEM COLLATION_NAME UDT_CAT UDT_SCHEM UDT_NAME |
|
618
|
|
|
|
|
|
|
undef, undef, undef, undef, undef, undef, undef, undef, undef, |
|
619
|
|
|
|
|
|
|
# DOMAIN_CAT DOMAIN_SCHEM DOMAIN_NAME SCOPE_CAT SCOPE_SCHEM SCOPE_NAME MAX_CARDINALITY DTD_IDENTIFIER IS_SELF_REF |
|
620
|
|
|
|
|
|
|
undef, undef, undef, undef, undef, undef, undef, undef, undef, |
|
621
|
|
|
|
|
|
|
]; |
|
622
|
|
|
|
|
|
|
|
|
623
|
|
|
|
|
|
|
push @col_rows, $col_row; |
|
624
|
|
|
|
|
|
|
$id++; |
|
625
|
|
|
|
|
|
|
} |
|
626
|
|
|
|
|
|
|
} |
|
627
|
|
|
|
|
|
|
|
|
628
|
|
|
|
|
|
|
return sponge_sth_loader($dbh, $type, $names, \@col_rows); |
|
629
|
|
|
|
|
|
|
} |
|
630
|
|
|
|
|
|
|
|
|
631
|
|
|
|
|
|
|
sub primary_key_info { |
|
632
|
|
|
|
|
|
|
my ($dbh, $catalog, $schema, $table) = @_; |
|
633
|
|
|
|
|
|
|
my $type = 'PRIMARY_KEY_INFO'; |
|
634
|
|
|
|
|
|
|
|
|
635
|
|
|
|
|
|
|
my $cols = $dbh->{tree_columns}{names}{$table} || return $dbh->set_err($DBI::stderr, "No such table name: $table", '42704'); |
|
636
|
|
|
|
|
|
|
my $pkey = $cols->[0]; |
|
637
|
|
|
|
|
|
|
|
|
638
|
|
|
|
|
|
|
return sponge_sth_loader($dbh, $type, |
|
639
|
|
|
|
|
|
|
[qw(TABLE_CAT TABLE_SCHEM TABLE_NAME COLUMN_NAME KEY_SEQ PK_NAME)], |
|
640
|
|
|
|
|
|
|
[ [ undef, undef, $table, $pkey, 1, $pkey.'_pkey' ] ] |
|
641
|
|
|
|
|
|
|
); |
|
642
|
|
|
|
|
|
|
} |
|
643
|
|
|
|
|
|
|
|
|
644
|
|
|
|
|
|
|
sub foreign_key_info { |
|
645
|
|
|
|
|
|
|
my ($dbh, $pk_catalog, $pk_schema, $pk_table, $fk_catalog, $fk_schema, $fk_table) = @_; |
|
646
|
|
|
|
|
|
|
my $type = 'FOREIGN_KEY_INFO'; |
|
647
|
|
|
|
|
|
|
my $names = [qw( |
|
648
|
|
|
|
|
|
|
PKTABLE_CAT PKTABLE_SCHEM PKTABLE_NAME PKCOLUMN_NAME FKTABLE_CAT FKTABLE_SCHEM FKTABLE_NAME FKCOLUMN_NAME |
|
649
|
|
|
|
|
|
|
KEY_SEQ UPDATE_RULE DELETE_RULE FK_NAME PK_NAME DEFERRABILITY UNIQUE_OR_PRIMARY |
|
650
|
|
|
|
|
|
|
)]; |
|
651
|
|
|
|
|
|
|
|
|
652
|
|
|
|
|
|
|
my $colnames = $dbh->{tree_columns}{names}; |
|
653
|
|
|
|
|
|
|
my $pkey = $pk_table && $colnames->{$pk_table} ? $colnames->{$pk_table}[0] : undef; |
|
654
|
|
|
|
|
|
|
my $fkey = $fk_table && $colnames->{$fk_table} ? $colnames->{$fk_table}[0] : undef; |
|
655
|
|
|
|
|
|
|
my ($pk_list, $fk_list) = ([$pk_table], [$fk_table]); |
|
656
|
|
|
|
|
|
|
my @dbi_data; |
|
657
|
|
|
|
|
|
|
|
|
658
|
|
|
|
|
|
|
# If both PKT and FKT are given, the function returns the foreign key, if any, |
|
659
|
|
|
|
|
|
|
# in table FKT that refers to the primary (unique) key of table PKT. |
|
660
|
|
|
|
|
|
|
if ($pkey && $fkey) { |
|
661
|
|
|
|
|
|
|
$fkey = first { $_ eq $pkey } $colnames->{$fk_table}; # pkey or bust |
|
662
|
|
|
|
|
|
|
} |
|
663
|
|
|
|
|
|
|
|
|
664
|
|
|
|
|
|
|
# If only PKT is given, then the result set contains the primary key of that table |
|
665
|
|
|
|
|
|
|
# and all foreign keys that refer to it. |
|
666
|
|
|
|
|
|
|
elsif ($pkey) { $fk_list = [ grep { $colnames->{$_} ~~ /^$pkey$/ } keys %$colnames ]; } |
|
667
|
|
|
|
|
|
|
|
|
668
|
|
|
|
|
|
|
# If only FKT is given, then the result set contains all foreign keys in that table |
|
669
|
|
|
|
|
|
|
# and the primary keys to which they refer. |
|
670
|
|
|
|
|
|
|
elsif ($fkey) { |
|
671
|
|
|
|
|
|
|
my @cols = @{$colnames->{$fk_table}}; |
|
672
|
|
|
|
|
|
|
shift @cols; # remove primary key |
|
673
|
|
|
|
|
|
|
|
|
674
|
|
|
|
|
|
|
$pk_list = []; |
|
675
|
|
|
|
|
|
|
foreach my $col (@cols) { |
|
676
|
|
|
|
|
|
|
my $tbl = (first { $colnames->{$_}[0] eq $col } keys %$colnames) || next; |
|
677
|
|
|
|
|
|
|
push @$pk_list, $tbl; |
|
678
|
|
|
|
|
|
|
} |
|
679
|
|
|
|
|
|
|
} |
|
680
|
|
|
|
|
|
|
else { return sponge_sth_loader($dbh, $type, $names, []); } |
|
681
|
|
|
|
|
|
|
|
|
682
|
|
|
|
|
|
|
# main loop |
|
683
|
|
|
|
|
|
|
foreach my $pt (@$pk_list) { |
|
684
|
|
|
|
|
|
|
foreach my $ft (@$fk_list) { |
|
685
|
|
|
|
|
|
|
my $key = $colnames->{$pt}[0]; # key links are named the same |
|
686
|
|
|
|
|
|
|
push @dbi_data, [ |
|
687
|
|
|
|
|
|
|
# 0=PKTABLE_CAT PKTABLE_SCHEM PKTABLE_NAME PKCOLUMN_NAME FKTABLE_CAT FKTABLE_SCHEM FKTABLE_NAME FKCOLUMN_NAME |
|
688
|
|
|
|
|
|
|
undef, undef, $pt, $key, undef, undef, $ft, $key, |
|
689
|
|
|
|
|
|
|
# 8=KEY_SEQ UPDATE_RULE DELETE_RULE FK_NAME PK_NAME DEFERRABILITY UNIQUE_OR_PRIMARY |
|
690
|
|
|
|
|
|
|
1, 3, 3, join('_', $ft, $key, 'fkey'), $key.'_pkey', 7, 'PRIMARY', |
|
691
|
|
|
|
|
|
|
]; |
|
692
|
|
|
|
|
|
|
} |
|
693
|
|
|
|
|
|
|
} |
|
694
|
|
|
|
|
|
|
|
|
695
|
|
|
|
|
|
|
return sponge_sth_loader($dbh, $type, $names, \@dbi_data); |
|
696
|
|
|
|
|
|
|
} |
|
697
|
|
|
|
|
|
|
|
|
698
|
|
|
|
|
|
|
sub statistics_info { |
|
699
|
|
|
|
|
|
|
my ($dbh, $catalog, $schema, $table, $unique_only, $quick) = @_; |
|
700
|
|
|
|
|
|
|
my $type = 'STATISTICS_INFO'; |
|
701
|
|
|
|
|
|
|
|
|
702
|
|
|
|
|
|
|
my $cols = $dbh->{tree_columns}{names}{$table} || return $dbh->set_err($DBI::stderr, "No such table name: $table", '42704'); |
|
703
|
|
|
|
|
|
|
my $pkey = $cols->[0]; |
|
704
|
|
|
|
|
|
|
my $rows = $dbh->{tree_cardinality}{$table}; |
|
705
|
|
|
|
|
|
|
|
|
706
|
|
|
|
|
|
|
return sponge_sth_loader($dbh, $type, |
|
707
|
|
|
|
|
|
|
[qw( |
|
708
|
|
|
|
|
|
|
TABLE_CAT TABLE_SCHEM TABLE_NAME NON_UNIQUE INDEX_QUALIFIER INDEX_NAME TYPE ORDINAL_POSITION |
|
709
|
|
|
|
|
|
|
COLUMN_NAME ASC_OR_DESC CARDINALITY PAGES FILTER_CONDITION |
|
710
|
|
|
|
|
|
|
)], |
|
711
|
|
|
|
|
|
|
[ |
|
712
|
|
|
|
|
|
|
[ |
|
713
|
|
|
|
|
|
|
undef, undef, $table, 0, undef, undef, 'table', undef, |
|
714
|
|
|
|
|
|
|
undef, undef, $rows, undef, undef |
|
715
|
|
|
|
|
|
|
], |
|
716
|
|
|
|
|
|
|
[ |
|
717
|
|
|
|
|
|
|
undef, undef, $table, 0, undef, $pkey.'_pkey', 'content', 1, |
|
718
|
|
|
|
|
|
|
$pkey, 'A', $rows, undef, undef |
|
719
|
|
|
|
|
|
|
], |
|
720
|
|
|
|
|
|
|
], |
|
721
|
|
|
|
|
|
|
); |
|
722
|
|
|
|
|
|
|
} |
|
723
|
|
|
|
|
|
|
|
|
724
|
|
|
|
|
|
|
sub sponge_sth_loader { |
|
725
|
|
|
|
|
|
|
my ($dbh, $tbl_name, $names, $rows) = @_; |
|
726
|
|
|
|
|
|
|
|
|
727
|
|
|
|
|
|
|
# (mostly a straight copy from DBI::DBD::SqlEngine) |
|
728
|
|
|
|
|
|
|
my $dbh2 = $dbh->func("sql_sponge_driver"); |
|
729
|
|
|
|
|
|
|
my $sth = $dbh2->prepare( |
|
730
|
|
|
|
|
|
|
$tbl_name, |
|
731
|
|
|
|
|
|
|
{ |
|
732
|
|
|
|
|
|
|
rows => $rows || [], |
|
733
|
|
|
|
|
|
|
NAME => $names, |
|
734
|
|
|
|
|
|
|
} |
|
735
|
|
|
|
|
|
|
); |
|
736
|
|
|
|
|
|
|
$sth or $dbh->set_err( $DBI::stderr, $dbh2->errstr, $dbh2->state ); |
|
737
|
|
|
|
|
|
|
return $sth; |
|
738
|
|
|
|
|
|
|
} |
|
739
|
|
|
|
|
|
|
|
|
740
|
|
|
|
|
|
|
sub type_info_all { |
|
741
|
|
|
|
|
|
|
# We are basically just translating Perl variable types to SQL, |
|
742
|
|
|
|
|
|
|
# though once everything has been flattened, it's basically just |
|
743
|
|
|
|
|
|
|
# string and number. |
|
744
|
|
|
|
|
|
|
|
|
745
|
|
|
|
|
|
|
# Perl's number size varies between 32/64-bit versions |
|
746
|
|
|
|
|
|
|
my $nbits = $Config{ptrsize} * 16 - 11; |
|
747
|
|
|
|
|
|
|
|
|
748
|
|
|
|
|
|
|
return [ |
|
749
|
|
|
|
|
|
|
{ |
|
750
|
|
|
|
|
|
|
TYPE_NAME => 0, |
|
751
|
|
|
|
|
|
|
DATA_TYPE => 1, |
|
752
|
|
|
|
|
|
|
COLUMN_SIZE => 2, # was PRECISION originally |
|
753
|
|
|
|
|
|
|
LITERAL_PREFIX => 3, |
|
754
|
|
|
|
|
|
|
LITERAL_SUFFIX => 4, |
|
755
|
|
|
|
|
|
|
CREATE_PARAMS => 5, |
|
756
|
|
|
|
|
|
|
NULLABLE => 6, |
|
757
|
|
|
|
|
|
|
CASE_SENSITIVE => 7, |
|
758
|
|
|
|
|
|
|
SEARCHABLE => 8, |
|
759
|
|
|
|
|
|
|
UNSIGNED_ATTRIBUTE => 9, |
|
760
|
|
|
|
|
|
|
FIXED_PREC_SCALE => 10, # was MONEY originally |
|
761
|
|
|
|
|
|
|
AUTO_UNIQUE_VALUE => 11, # was AUTO_INCREMENT originally |
|
762
|
|
|
|
|
|
|
LOCAL_TYPE_NAME => 12, |
|
763
|
|
|
|
|
|
|
MINIMUM_SCALE => 13, |
|
764
|
|
|
|
|
|
|
MAXIMUM_SCALE => 14, |
|
765
|
|
|
|
|
|
|
SQL_DATA_TYPE => 15, |
|
766
|
|
|
|
|
|
|
SQL_DATETIME_SUB => 16, |
|
767
|
|
|
|
|
|
|
NUM_PREC_RADIX => 17, |
|
768
|
|
|
|
|
|
|
INTERVAL_PRECISION => 18, |
|
769
|
|
|
|
|
|
|
}, |
|
770
|
|
|
|
|
|
|
# Name DataType Max Literals Params Null Case Search Unsign Fixed Auto LocalTypeName M/M Scale SQLDataType DateTime_Sub Radix ItvPrec |
|
771
|
|
|
|
|
|
|
[ "PID", DBI::SQL_INTEGER(), 32, undef, undef, undef, 0, 0, 3, 1, 1, 0, "PID", 0, 0, DBI::SQL_INTEGER(), undef, 2, undef], |
|
772
|
|
|
|
|
|
|
[ "ID", DBI::SQL_INTEGER(), 32, undef, undef, undef, 1, 0, 3, 1, 1, 0, "ID", 0, 0, DBI::SQL_INTEGER(), undef, 2, undef], |
|
773
|
|
|
|
|
|
|
[ "NUMBER", DBI::SQL_NUMERIC(), $nbits, undef, undef, undef, 1, 0, 3, 0, 0, 0, "Number", 0,$nbits, DBI::SQL_NUMERIC(), undef, 2, undef], |
|
774
|
|
|
|
|
|
|
[ "STRING", DBI::SQL_VARCHAR(), 2**31, "'", "'", undef, 1, 1, 3, undef, undef, undef, "String", undef, undef, DBI::SQL_VARCHAR(), undef, undef, undef], |
|
775
|
|
|
|
|
|
|
]; |
|
776
|
|
|
|
|
|
|
} |
|
777
|
|
|
|
|
|
|
|
|
778
|
|
|
|
|
|
|
1; |
|
779
|
|
|
|
|
|
|
|
|
780
|
|
|
|
|
|
|
############################################################################## |
|
781
|
|
|
|
|
|
|
# DBD::TreeData::st |
|
782
|
|
|
|
|
|
|
|
|
783
|
|
|
|
|
|
|
package # hide from PAUSE |
|
784
|
|
|
|
|
|
|
DBD::TreeData::st; # ====== STATEMENT ====== |
|
785
|
|
|
|
|
|
|
|
|
786
|
|
|
|
|
|
|
our $imp_data_size = 0; |
|
787
|
|
|
|
|
|
|
use DBD::AnyData; |
|
788
|
|
|
|
|
|
|
use parent qw(-norequire DBD::AnyData::st); # no such file as ::st.pm |
|
789
|
|
|
|
|
|
|
|
|
790
|
|
|
|
|
|
|
1; |
|
791
|
|
|
|
|
|
|
|
|
792
|
|
|
|
|
|
|
############################################################################## |
|
793
|
|
|
|
|
|
|
# DBD::TreeData::Statement |
|
794
|
|
|
|
|
|
|
|
|
795
|
|
|
|
|
|
|
package # hide from PAUSE |
|
796
|
|
|
|
|
|
|
DBD::TreeData::Statement; # ====== SqlEngine::Statement ====== |
|
797
|
|
|
|
|
|
|
|
|
798
|
|
|
|
|
|
|
our $imp_data_size = 0; |
|
799
|
|
|
|
|
|
|
use DBD::AnyData; |
|
800
|
|
|
|
|
|
|
use parent qw(-norequire DBD::AnyData::Statement); # no such file as ::Statement.pm |
|
801
|
|
|
|
|
|
|
|
|
802
|
|
|
|
|
|
|
1; |
|
803
|
|
|
|
|
|
|
|
|
804
|
|
|
|
|
|
|
############################################################################## |
|
805
|
|
|
|
|
|
|
# DBD::TreeData::Table |
|
806
|
|
|
|
|
|
|
|
|
807
|
|
|
|
|
|
|
package # hide from PAUSE |
|
808
|
|
|
|
|
|
|
DBD::TreeData::Table; # ====== SqlEngine::Table ====== |
|
809
|
|
|
|
|
|
|
|
|
810
|
|
|
|
|
|
|
our $imp_data_size = 0; |
|
811
|
|
|
|
|
|
|
use DBD::AnyData; |
|
812
|
|
|
|
|
|
|
use parent qw(-norequire DBD::AnyData::Table); # no such file as ::Table.pm |
|
813
|
|
|
|
|
|
|
|
|
814
|
|
|
|
|
|
|
1; |
|
815
|
|
|
|
|
|
|
|
|
816
|
|
|
|
|
|
|
__END__ |
|
817
|
|
|
|
|
|
|
=pod |
|
818
|
|
|
|
|
|
|
|
|
819
|
|
|
|
|
|
|
=head1 NAME |
|
820
|
|
|
|
|
|
|
|
|
821
|
|
|
|
|
|
|
DBD::TreeData - DBI driver for any abstract hash/array tree |
|
822
|
|
|
|
|
|
|
|
|
823
|
|
|
|
|
|
|
=head1 SYNOPSIS |
|
824
|
|
|
|
|
|
|
|
|
825
|
|
|
|
|
|
|
use DBI; |
|
826
|
|
|
|
|
|
|
use JSON::Any; |
|
827
|
|
|
|
|
|
|
use LWP::Simple; |
|
828
|
|
|
|
|
|
|
|
|
829
|
|
|
|
|
|
|
# Example JSON object |
|
830
|
|
|
|
|
|
|
my $json = get 'http://maps.googleapis.com/maps/api/geocode/json?address=1600+Pennsylvania+Ave+NW,+20500®ion=us&language=en&sensor=false'; |
|
831
|
|
|
|
|
|
|
my $obj = JSON::Any->jsonToObj($json); |
|
832
|
|
|
|
|
|
|
|
|
833
|
|
|
|
|
|
|
my $dbh = DBI->connect('dbi:TreeData:', '', '', { |
|
834
|
|
|
|
|
|
|
tree_table_name => 'geocode', |
|
835
|
|
|
|
|
|
|
tree_data => $obj, |
|
836
|
|
|
|
|
|
|
}); |
|
837
|
|
|
|
|
|
|
|
|
838
|
|
|
|
|
|
|
# Informational dump |
|
839
|
|
|
|
|
|
|
use Data::Dump; |
|
840
|
|
|
|
|
|
|
dd ($dbh->table_info->fetchall_arrayref); |
|
841
|
|
|
|
|
|
|
dd (map { [ @{$_}[2 .. 6] ] } @{ |
|
842
|
|
|
|
|
|
|
$dbh->column_info('','','','')->fetchall_arrayref |
|
843
|
|
|
|
|
|
|
}); |
|
844
|
|
|
|
|
|
|
|
|
845
|
|
|
|
|
|
|
# DBIC dump |
|
846
|
|
|
|
|
|
|
use DBIx::Class::Schema::Loader 'make_schema_at'; |
|
847
|
|
|
|
|
|
|
make_schema_at( |
|
848
|
|
|
|
|
|
|
'My::Schema', { |
|
849
|
|
|
|
|
|
|
debug => 1, |
|
850
|
|
|
|
|
|
|
dump_directory => './lib', |
|
851
|
|
|
|
|
|
|
}, |
|
852
|
|
|
|
|
|
|
[ 'dbi:TreeData:geocode', '', '', { tree_data => $obj } ], |
|
853
|
|
|
|
|
|
|
); |
|
854
|
|
|
|
|
|
|
|
|
855
|
|
|
|
|
|
|
=head1 DESCRIPTION |
|
856
|
|
|
|
|
|
|
|
|
857
|
|
|
|
|
|
|
DBD::TreeData provides a DBI driver to translate any sort of tree-based data set (encapsulated in a Perl object) into a flat set of tables, |
|
858
|
|
|
|
|
|
|
complete with real SQL functionality. This module utilizes L<DBD::AnyData> to create the new tables, which uses L<SQL::Statement> to support |
|
859
|
|
|
|
|
|
|
the SQL parsing. (Any caveats with those modules likely applies here.) |
|
860
|
|
|
|
|
|
|
|
|
861
|
|
|
|
|
|
|
This module can be handy to translate JSON, XML, YAML, and many other tree formats to be used in class sets like L<DBIx::Class>. Unlike |
|
862
|
|
|
|
|
|
|
L<DBD::AnyData>, the format of the data doesn't have to be pre-flattened, and will be spread out into multiple tables. |
|
863
|
|
|
|
|
|
|
|
|
864
|
|
|
|
|
|
|
Also, this driver fully supports all of the C<<< *_info >>> methods, making it ideal to shove into modules like L<DBIx::Class::Schema::Loader>. |
|
865
|
|
|
|
|
|
|
(The C<<< table_info >>> and C<<< column_info >>> filters use REs with beginE<sol>end bounds pre-set.) |
|
866
|
|
|
|
|
|
|
|
|
867
|
|
|
|
|
|
|
=encoding utf8 |
|
868
|
|
|
|
|
|
|
|
|
869
|
|
|
|
|
|
|
=head1 CONNECT ATTRIBUTES |
|
870
|
|
|
|
|
|
|
|
|
871
|
|
|
|
|
|
|
=head2 tree_data |
|
872
|
|
|
|
|
|
|
|
|
873
|
|
|
|
|
|
|
The actual tree object. Of course, this attribute is required. |
|
874
|
|
|
|
|
|
|
|
|
875
|
|
|
|
|
|
|
=head2 tree_table_name |
|
876
|
|
|
|
|
|
|
|
|
877
|
|
|
|
|
|
|
The name of the starting table. Not required, but recommended. If not specified, defaults to 'tree_data', or the value of the driver |
|
878
|
|
|
|
|
|
|
DSN string (after the C<<< dbi:TreeData: >>> part). |
|
879
|
|
|
|
|
|
|
|
|
880
|
|
|
|
|
|
|
=head2 tree_debug |
|
881
|
|
|
|
|
|
|
|
|
882
|
|
|
|
|
|
|
Boolean. Print debug information while translating the tree. |
|
883
|
|
|
|
|
|
|
|
|
884
|
|
|
|
|
|
|
=head2 tree_rename_tables |
|
885
|
|
|
|
|
|
|
|
|
886
|
|
|
|
|
|
|
Hashref of table names. If you don't like the name of an auto-created table, you can rename them while the database is being built. Within |
|
887
|
|
|
|
|
|
|
the hashref, the keysE<sol>values are the oldE<sol>new names, respectively. |
|
888
|
|
|
|
|
|
|
|
|
889
|
|
|
|
|
|
|
=head1 TRANSLATION BEHAVIOR |
|
890
|
|
|
|
|
|
|
|
|
891
|
|
|
|
|
|
|
The tree translation into flat tables is done using a recursive descent algorithm. It starts with a check of the current node's reference |
|
892
|
|
|
|
|
|
|
type, which dictates how it interprets the children. The goal is to create a fully L<4NF|http://en.wikipedia.org/wiki/Fourth_normal_form> |
|
893
|
|
|
|
|
|
|
database from the tree. |
|
894
|
|
|
|
|
|
|
|
|
895
|
|
|
|
|
|
|
Arrays are interpreted as a list of rows, and typically get rolled up into "group" tables. Hashes are interpreted as a list of column names |
|
896
|
|
|
|
|
|
|
and values. Non-references are considered values. Scalar refs and VStrings are de-referenced first. Other types of refs are processed as |
|
897
|
|
|
|
|
|
|
best as possible, but the driver will complain. (Code ref blocks are currently NOT executed and discarded.) |
|
898
|
|
|
|
|
|
|
|
|
899
|
|
|
|
|
|
|
Nested arrays will create nested group tables with different suffixes, like C<<< matrix >>>, C<<< cube >>>, and C<<< hypercube >>>. If it has to go beyond |
|
900
|
|
|
|
|
|
|
that (and you really shouldn't have structures like that), it'll start complaining (sarcastically). |
|
901
|
|
|
|
|
|
|
|
|
902
|
|
|
|
|
|
|
In almost all cases, the table name is derived from a previous key. Table names also use L<Lingua::EN::Inflect::Phrase> to create |
|
903
|
|
|
|
|
|
|
pluralized names. Primary IDs will have singular names with a C<<< _id >>> suffix. |
|
904
|
|
|
|
|
|
|
|
|
905
|
|
|
|
|
|
|
For example, this tree: |
|
906
|
|
|
|
|
|
|
|
|
907
|
|
|
|
|
|
|
address_components => [ |
|
908
|
|
|
|
|
|
|
{ |
|
909
|
|
|
|
|
|
|
long_name => 1600, |
|
910
|
|
|
|
|
|
|
short_name => 1600, |
|
911
|
|
|
|
|
|
|
types => [ "street_number" ] |
|
912
|
|
|
|
|
|
|
}, |
|
913
|
|
|
|
|
|
|
{ |
|
914
|
|
|
|
|
|
|
long_name => "President's Park", |
|
915
|
|
|
|
|
|
|
short_name => "President's Park", |
|
916
|
|
|
|
|
|
|
types => [ "establishment" ] |
|
917
|
|
|
|
|
|
|
}, |
|
918
|
|
|
|
|
|
|
{ |
|
919
|
|
|
|
|
|
|
long_name => "Pennsylvania Avenue Northwest", |
|
920
|
|
|
|
|
|
|
short_name => "Pennsylvania Ave NW", |
|
921
|
|
|
|
|
|
|
types => [ "route" ] |
|
922
|
|
|
|
|
|
|
}, |
|
923
|
|
|
|
|
|
|
{ |
|
924
|
|
|
|
|
|
|
long_name => "Washington", |
|
925
|
|
|
|
|
|
|
short_name => "Washington", |
|
926
|
|
|
|
|
|
|
types => [ "locality", "political" ] |
|
927
|
|
|
|
|
|
|
}, |
|
928
|
|
|
|
|
|
|
... etc ..., |
|
929
|
|
|
|
|
|
|
], |
|
930
|
|
|
|
|
|
|
|
|
931
|
|
|
|
|
|
|
Would create the following tables: |
|
932
|
|
|
|
|
|
|
|
|
933
|
|
|
|
|
|
|
<main_table> |
|
934
|
|
|
|
|
|
|
address_component_groups |
|
935
|
|
|
|
|
|
|
address_components |
|
936
|
|
|
|
|
|
|
type_groups |
|
937
|
|
|
|
|
|
|
types |
|
938
|
|
|
|
|
|
|
|
|
939
|
|
|
|
|
|
|
In this case, C<<< address_components >>> has most of the columns and data, but it also has a tie to an ID of C<<< address_component_groups >>>. |
|
940
|
|
|
|
|
|
|
|
|
941
|
|
|
|
|
|
|
Since C<<< types >>> points to an array, it will have its own dedicated table. That table would have data like: |
|
942
|
|
|
|
|
|
|
|
|
943
|
|
|
|
|
|
|
type_id â type |
|
944
|
|
|
|
|
|
|
âââââââââªââââââââââââââââ |
|
945
|
|
|
|
|
|
|
1 â street_number |
|
946
|
|
|
|
|
|
|
2 â establishment |
|
947
|
|
|
|
|
|
|
3 â route |
|
948
|
|
|
|
|
|
|
4 â locality |
|
949
|
|
|
|
|
|
|
5 â political |
|
950
|
|
|
|
|
|
|
... â ... |
|
951
|
|
|
|
|
|
|
|
|
952
|
|
|
|
|
|
|
Most of the C<<< type_groups >>> table would be a 1:1 match. However, the last component entry has more than one value in the C<<< types >>> array, so the |
|
953
|
|
|
|
|
|
|
C<<< type_group_id >>> associated to that component would have multiple entries (4 & 5). Duplicate values are also tracked, so that IDs are reused. |
|
954
|
|
|
|
|
|
|
|
|
955
|
|
|
|
|
|
|
=head1 CAVEATS |
|
956
|
|
|
|
|
|
|
|
|
957
|
|
|
|
|
|
|
=head2 DBI E<sol> DBD::AnyData Conflict |
|
958
|
|
|
|
|
|
|
|
|
959
|
|
|
|
|
|
|
As of the time of this writing, the latest version of L<DBI> (1.623) and the latest version of L<DBD::AnyData> (0.110) do not work together. |
|
960
|
|
|
|
|
|
|
Since TreeData relies on L<DBD::AnyData> for table creation, you will need to downgrade to L<DBI> 1.622 to use this driver, until a new |
|
961
|
|
|
|
|
|
|
version of L<DBD::AnyData> comes out. |
|
962
|
|
|
|
|
|
|
|
|
963
|
|
|
|
|
|
|
=head1 AVAILABILITY |
|
964
|
|
|
|
|
|
|
|
|
965
|
|
|
|
|
|
|
The project homepage is L<https://github.com/SineSwiper/DBD-TreeData/wiki>. |
|
966
|
|
|
|
|
|
|
|
|
967
|
|
|
|
|
|
|
The latest version of this module is available from the Comprehensive Perl |
|
968
|
|
|
|
|
|
|
Archive Network (CPAN). Visit L<http://www.perl.com/CPAN/> to find a CPAN |
|
969
|
|
|
|
|
|
|
site near you, or see L<https://metacpan.org/module/DBD::TreeData/>. |
|
970
|
|
|
|
|
|
|
|
|
971
|
|
|
|
|
|
|
=for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan |
|
972
|
|
|
|
|
|
|
|
|
973
|
|
|
|
|
|
|
=head1 SUPPORT |
|
974
|
|
|
|
|
|
|
|
|
975
|
|
|
|
|
|
|
=head2 Internet Relay Chat |
|
976
|
|
|
|
|
|
|
|
|
977
|
|
|
|
|
|
|
You can get live help by using IRC ( Internet Relay Chat ). If you don't know what IRC is, |
|
978
|
|
|
|
|
|
|
please read this excellent guide: L<http://en.wikipedia.org/wiki/Internet_Relay_Chat>. Please |
|
979
|
|
|
|
|
|
|
be courteous and patient when talking to us, as we might be busy or sleeping! You can join |
|
980
|
|
|
|
|
|
|
those networks/channels and get help: |
|
981
|
|
|
|
|
|
|
|
|
982
|
|
|
|
|
|
|
=over 4 |
|
983
|
|
|
|
|
|
|
|
|
984
|
|
|
|
|
|
|
=item * |
|
985
|
|
|
|
|
|
|
|
|
986
|
|
|
|
|
|
|
irc.perl.org |
|
987
|
|
|
|
|
|
|
|
|
988
|
|
|
|
|
|
|
You can connect to the server at 'irc.perl.org' and join this channel: #dbi then talk to this person for help: SineSwiper. |
|
989
|
|
|
|
|
|
|
|
|
990
|
|
|
|
|
|
|
=back |
|
991
|
|
|
|
|
|
|
|
|
992
|
|
|
|
|
|
|
=head2 Bugs / Feature Requests |
|
993
|
|
|
|
|
|
|
|
|
994
|
|
|
|
|
|
|
Please report any bugs or feature requests via L<L<https://github.com/SineSwiper/DBD-TreeData/issues>|GitHub>. |
|
995
|
|
|
|
|
|
|
|
|
996
|
|
|
|
|
|
|
=head1 AUTHOR |
|
997
|
|
|
|
|
|
|
|
|
998
|
|
|
|
|
|
|
Brendan Byrd <BBYRD@CPAN.org> |
|
999
|
|
|
|
|
|
|
|
|
1000
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
|
1001
|
|
|
|
|
|
|
|
|
1002
|
|
|
|
|
|
|
This software is Copyright (c) 2013 by Brendan Byrd. |
|
1003
|
|
|
|
|
|
|
|
|
1004
|
|
|
|
|
|
|
This is free software, licensed under: |
|
1005
|
|
|
|
|
|
|
|
|
1006
|
|
|
|
|
|
|
The Artistic License 2.0 (GPL Compatible) |
|
1007
|
|
|
|
|
|
|
|
|
1008
|
|
|
|
|
|
|
=cut |
|
1009
|
|
|
|
|
|
|
|