line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
=head1 NAME |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
SQLite::VirtualTable::Pivot -- use SQLite's virtual tables to represent pivot tables. |
4
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
=head1 SYNOPSIS |
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
$ export SQLITE_CURRENT_DB=/tmp/foo.db |
8
|
|
|
|
|
|
|
sqlite3 $SQLITE_CURRENT_DB |
9
|
|
|
|
|
|
|
sqlite> .load perlvtab.so |
10
|
|
|
|
|
|
|
sqlite> create table object_attributes (id integer, name varchar, value integer); |
11
|
|
|
|
|
|
|
sqlite> insert into object_attributes values ( 1, "length", 20 ); |
12
|
|
|
|
|
|
|
sqlite> insert into object_attributes values ( 1, "color", "red" ); |
13
|
|
|
|
|
|
|
sqlite> create virtual table object_pivot using perl |
14
|
|
|
|
|
|
|
("SQLite::VirtualTable::Pivot", "object_attributes" ); |
15
|
|
|
|
|
|
|
sqlite> select * from object_pivot; |
16
|
|
|
|
|
|
|
id|color|length |
17
|
|
|
|
|
|
|
1|red|20 |
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
=head1 DESCRIPTION |
20
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
A pivot table is a table in which the distinct row values of a column |
22
|
|
|
|
|
|
|
in one table are used as the names of the columns in another table. |
23
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
Here's an example: |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
Given this table : |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
Student Subject Grade |
29
|
|
|
|
|
|
|
------- ------- ----- |
30
|
|
|
|
|
|
|
Joe Reading A |
31
|
|
|
|
|
|
|
Joe Writing B |
32
|
|
|
|
|
|
|
Joe Arithmetic C |
33
|
|
|
|
|
|
|
Mary Reading B- |
34
|
|
|
|
|
|
|
Mary Writing A+ |
35
|
|
|
|
|
|
|
Mary Arithmetic C+ |
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
A pivot table created using the columns "Student" and "Subject" |
38
|
|
|
|
|
|
|
and the value "Grade" would yield : |
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
Student Arithmetic Reading Writing |
41
|
|
|
|
|
|
|
------- ---------- ------- ---------- |
42
|
|
|
|
|
|
|
Joe C A B |
43
|
|
|
|
|
|
|
Mary C+ B- A+ |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
To create a table, use the following syntax : |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
create virtual table object_pivot using perl |
48
|
|
|
|
|
|
|
("SQLite::VirtualTable::Pivot", "base_table" ); |
49
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
To specify the three columns, use : |
51
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
create virtual table object_pivot using perl |
53
|
|
|
|
|
|
|
("SQLite::VirtualTable::Pivot", "base_table", |
54
|
|
|
|
|
|
|
"pivot_row", "pivot_column", "pivot_value" ); |
55
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
where pivot_row, pivot_column and pivot_value are three columns |
57
|
|
|
|
|
|
|
in the base_table. The distinct values of pivot_column will be |
58
|
|
|
|
|
|
|
the names of the new columns in the pivot table. (The values may |
59
|
|
|
|
|
|
|
be sanitized to create valid column names.) |
60
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
If any of the three columns are foreign keys, these may be |
62
|
|
|
|
|
|
|
collapsed in the pivot table, as described below. |
63
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
The list of distinct columns is calculated the first |
65
|
|
|
|
|
|
|
time a pivot table is used (or created) in a database session. |
66
|
|
|
|
|
|
|
So, if the list changes, you may need to re-connect. |
67
|
|
|
|
|
|
|
|
68
|
|
|
|
|
|
|
=head1 Entity-Atribute-Value models |
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
The Entity-Attribute-Value model is a representation of data in |
71
|
|
|
|
|
|
|
a table containing three columns representing an entity, an attribute, |
72
|
|
|
|
|
|
|
and a value. For instance : |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
Entity Attribute Value |
75
|
|
|
|
|
|
|
------ --------- ----- |
76
|
|
|
|
|
|
|
1 color red |
77
|
|
|
|
|
|
|
1 length 20 |
78
|
|
|
|
|
|
|
2 color blue |
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
To reduce redundancy or to constrain the possible attributes/values, |
81
|
|
|
|
|
|
|
some or all of the three columns may be foreign keys. Consider for |
82
|
|
|
|
|
|
|
instance, the following : |
83
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
create table entities ( |
85
|
|
|
|
|
|
|
id integer primary key, |
86
|
|
|
|
|
|
|
entity varchar, |
87
|
|
|
|
|
|
|
unique (entity) ); |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
create table attributes ( |
90
|
|
|
|
|
|
|
id integer primary key, |
91
|
|
|
|
|
|
|
attribute varchar, |
92
|
|
|
|
|
|
|
unique (attribute) ); |
93
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
create table value_s ( |
95
|
|
|
|
|
|
|
id integer primary key, |
96
|
|
|
|
|
|
|
value integer, -- nb: "integer" is only the column affinity |
97
|
|
|
|
|
|
|
unique (value) ); |
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
create table eav ( |
100
|
|
|
|
|
|
|
entity integer references entities(id), |
101
|
|
|
|
|
|
|
attribute integer references attributes(id), |
102
|
|
|
|
|
|
|
value integer references value_s(id), |
103
|
|
|
|
|
|
|
primary key (entity,attribute) |
104
|
|
|
|
|
|
|
); |
105
|
|
|
|
|
|
|
|
106
|
|
|
|
|
|
|
Then the foreign keys may be "flattened" into the pivot table |
107
|
|
|
|
|
|
|
by using this SQL : |
108
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
create virtual table |
110
|
|
|
|
|
|
|
eav_pivot using perl ("SQLite::VirtualTable::Pivot", |
111
|
|
|
|
|
|
|
"eav", |
112
|
|
|
|
|
|
|
"entity->entity(id).entity", |
113
|
|
|
|
|
|
|
"attribute->attributes(id).attribute", |
114
|
|
|
|
|
|
|
"value->value_s(id).value" |
115
|
|
|
|
|
|
|
); |
116
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
Then the columns in eav_pivot would be the entries in |
118
|
|
|
|
|
|
|
attributes.attribute corresponding to the distinct |
119
|
|
|
|
|
|
|
values in eav.attribute. |
120
|
|
|
|
|
|
|
|
121
|
|
|
|
|
|
|
Moreover, queries against the pivot table will do the right |
122
|
|
|
|
|
|
|
thing, in the sense that restrictions will use the values in the |
123
|
|
|
|
|
|
|
value_s table, not in the eav table. |
124
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
=head1 EXAMPLE |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
create table students (student, subject, grade, primary key (student,subject)); |
128
|
|
|
|
|
|
|
insert into students values ("Joe", "Reading", "A"); |
129
|
|
|
|
|
|
|
insert into students values ("Joe", "Writing", "B"); |
130
|
|
|
|
|
|
|
insert into students values ("Joe", "Arithmetic", "C"); |
131
|
|
|
|
|
|
|
insert into students values ("Mary", "Reading", "B-"); |
132
|
|
|
|
|
|
|
insert into students values ("Mary", "Writing", "A+"); |
133
|
|
|
|
|
|
|
insert into students values ("Mary", "Arithmetic", "C+"); |
134
|
|
|
|
|
|
|
|
135
|
|
|
|
|
|
|
select load_extension("perlvtab.so"); |
136
|
|
|
|
|
|
|
create virtual table roster using perl ("SQLite::VirtualTable::Pivot", "students", "student", "subject", "grade"); |
137
|
|
|
|
|
|
|
select * from roster; |
138
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
Student Reading Writing Arithmetic |
140
|
|
|
|
|
|
|
------- ------- ------- ---------- |
141
|
|
|
|
|
|
|
Joe A B C |
142
|
|
|
|
|
|
|
Mary B- A+ C+ |
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
select student from roster where writing = "A+"; |
145
|
|
|
|
|
|
|
Mary |
146
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
=head1 FUNCTIONS (called by sqlite, see SQLite::VirtualTable) |
148
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
=cut |
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
package SQLite::VirtualTable::Pivot; |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
# from CPAN |
154
|
1
|
|
|
1
|
|
23949
|
use DBI; |
|
1
|
|
|
|
|
18735
|
|
|
1
|
|
|
|
|
75
|
|
155
|
1
|
|
|
1
|
|
1055
|
use DBIx::Simple; |
|
1
|
|
|
|
|
7437
|
|
|
1
|
|
|
|
|
33
|
|
156
|
1
|
|
|
1
|
|
1009
|
use Data::Dumper; |
|
1
|
|
|
|
|
7651
|
|
|
1
|
|
|
|
|
99
|
|
157
|
1
|
|
|
1
|
|
10
|
use Scalar::Util qw/looks_like_number/; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
92
|
|
158
|
1
|
|
|
1
|
|
524
|
use SQLite::VirtualTable::Util qw/unescape/; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
# base modules |
161
|
|
|
|
|
|
|
use base 'SQLite::VirtualTable'; |
162
|
|
|
|
|
|
|
use base 'Class::Accessor::Contextual'; |
163
|
|
|
|
|
|
|
|
164
|
|
|
|
|
|
|
# local module |
165
|
|
|
|
|
|
|
use SQLite::VirtualTable::Pivot::Cursor; |
166
|
|
|
|
|
|
|
use strict; |
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
our $VERSION = 0.02; |
169
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
# Create r/w accessors for everything that we store in the class hash |
171
|
|
|
|
|
|
|
__PACKAGE__->mk_accessors(qw| table |); # base_table name and distinct values |
172
|
|
|
|
|
|
|
__PACKAGE__->mk_accessors(qw| columns |); # distinct values in base_table.$pivot_row |
173
|
|
|
|
|
|
|
__PACKAGE__->mk_accessors(qw| vcolumns |); # valid column names based on the above |
174
|
|
|
|
|
|
|
__PACKAGE__->mk_accessors(qw| indexes counts |); # populated by BEST_INDEX, used by FILTER |
175
|
|
|
|
|
|
|
__PACKAGE__->mk_accessors(qw| pivot_row pivot_row_ref |); # entity (in EAV) + fk info |
176
|
|
|
|
|
|
|
__PACKAGE__->mk_accessors(qw| pivot_column pivot_column_ref |); # attribute + fk info |
177
|
|
|
|
|
|
|
__PACKAGE__->mk_accessors(qw| pivot_value pivot_value_ref |); # value + fk info |
178
|
|
|
|
|
|
|
__PACKAGE__->mk_accessors(qw| pivot_row_type |); # column affinity for entity |
179
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
# We need to use an env variable until DBD::SQLite + SQLite::VirtualTable |
181
|
|
|
|
|
|
|
# work together to pass one to CREATE() |
182
|
|
|
|
|
|
|
our $dbfile = $ENV{SQLITE_CURRENT_DB} or die "please set SQLITE_CURRENT_DB"; |
183
|
|
|
|
|
|
|
our $db; # handle: DBIx::Simple object |
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
# debug setup |
186
|
|
|
|
|
|
|
#$ENV{TRACE} = 1; |
187
|
|
|
|
|
|
|
#$ENV{DEBUG} = 1; |
188
|
|
|
|
|
|
|
sub debug($) { return unless $ENV{DEBUG}; print STDERR "# $_[0]\n"; } |
189
|
|
|
|
|
|
|
sub trace($) { return unless $ENV{TRACE}; print STDERR "# $_[0]\n"; } |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
# Initialize the database handle. Send force => 1 to force a reconnect |
192
|
|
|
|
|
|
|
sub _init_db { |
193
|
|
|
|
|
|
|
my %args = @_; |
194
|
|
|
|
|
|
|
our $db; |
195
|
|
|
|
|
|
|
return if defined($db) && !$args{force}; |
196
|
|
|
|
|
|
|
debug "connect to $dbfile"; |
197
|
|
|
|
|
|
|
$db = DBIx::Simple->connect( "dbi:SQLite:dbname=$dbfile", "", "" ) |
198
|
|
|
|
|
|
|
or die DBIx::Simple->error; |
199
|
|
|
|
|
|
|
$db->dbh->do("PRAGMA temp_store = 2"); # use in-memory temp tables |
200
|
|
|
|
|
|
|
} |
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
# Parse the string indicating a foreign key relationship in the base_table. |
203
|
|
|
|
|
|
|
# Given "entity->entity_ref(id).value", |
204
|
|
|
|
|
|
|
# return ("entity" , { table=>"entity_ref", child_key => "id", child_label => "value"} ). |
205
|
|
|
|
|
|
|
sub _parse_refspec { |
206
|
|
|
|
|
|
|
my $str = shift; |
207
|
|
|
|
|
|
|
$str =~ /^(.*)->(.*)\((.*)\)\.(.*)$/ |
208
|
|
|
|
|
|
|
and return ( $1, { table => $2, child_key => $3, child_label => $4 } ); |
209
|
|
|
|
|
|
|
return $str; |
210
|
|
|
|
|
|
|
} |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
=head1 CREATE (constructor) |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
Arguments : |
215
|
|
|
|
|
|
|
module : "perl", |
216
|
|
|
|
|
|
|
caller : "main" |
217
|
|
|
|
|
|
|
virtual_table : the name of the table being created |
218
|
|
|
|
|
|
|
base_table : the table being pivoted |
219
|
|
|
|
|
|
|
@pivot_columns (optional) : entity, attribute, value |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
Returns : |
222
|
|
|
|
|
|
|
A new SQLite::VirtualTable::Pivot object. |
223
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
Description : |
225
|
|
|
|
|
|
|
Create a new SQLite::VirtualTable::Pivot object. The base_table |
226
|
|
|
|
|
|
|
is the table to be pivoted. If this table contains only three |
227
|
|
|
|
|
|
|
columns, then they will be used in order as the pivot_row, |
228
|
|
|
|
|
|
|
pivot_column, and pivot_value columns (aka entity, attribute, value). |
229
|
|
|
|
|
|
|
Alternatively, these columns may be specified in the create |
230
|
|
|
|
|
|
|
statement by passing them as parameters. If one of the values |
231
|
|
|
|
|
|
|
is a foreign key and the pivot table should instead use a column |
232
|
|
|
|
|
|
|
in the child table, that may be specified using the following |
233
|
|
|
|
|
|
|
notation : |
234
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
base_table_column->child_table(child_key).child_column_to_use |
236
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
If a column name contains a space, then the portion after the |
238
|
|
|
|
|
|
|
space should be the column affinity. |
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
Examples : |
241
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
CREATE VIRTUAL TABLE pivot_table USING perl |
243
|
|
|
|
|
|
|
("SQLite::VirtualTable::Pivot","base_table" ); |
244
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
CREATE VIRTUAL TABLE pivot_table USING perl |
246
|
|
|
|
|
|
|
("SQLite::VirtualTable::Pivot","base_table", |
247
|
|
|
|
|
|
|
"entity","attribute","value"); |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
CREATE VIRTUAL TABLE pivot_table USING perl |
250
|
|
|
|
|
|
|
("SQLite::VirtualTable::Pivot","base_table", |
251
|
|
|
|
|
|
|
"entity integer","attribute varchar","value integer"); |
252
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
CREATE VIRTUAL TABLE pivot_table USING perl |
254
|
|
|
|
|
|
|
("SQLite::VirtualTable::Pivot","base_table", |
255
|
|
|
|
|
|
|
"entty", |
256
|
|
|
|
|
|
|
"attribute->attribute_lookup(id).attr", |
257
|
|
|
|
|
|
|
"value->value_lookup(id).value" ); |
258
|
|
|
|
|
|
|
=cut |
259
|
|
|
|
|
|
|
|
260
|
|
|
|
|
|
|
sub CREATE { |
261
|
|
|
|
|
|
|
my ( $class, $module, $caller, $virtual_table, $base_table, @pivot_columns ) = @_; |
262
|
|
|
|
|
|
|
trace "(CREATE, got @_)"; |
263
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
# connect |
265
|
|
|
|
|
|
|
_init_db(); |
266
|
|
|
|
|
|
|
|
267
|
|
|
|
|
|
|
# Get the base_table and its metadata. Parse the sql used to create it. |
268
|
|
|
|
|
|
|
$base_table = unescape($base_table); |
269
|
|
|
|
|
|
|
my ($createsql) = |
270
|
|
|
|
|
|
|
$db->select( 'sqlite_master', ['sql'], { name => $base_table } )->list |
271
|
|
|
|
|
|
|
or die "Could not find table '$base_table' " . $db->error; |
272
|
|
|
|
|
|
|
$createsql =~ s/^[^\(]*\(//; # remove leading |
273
|
|
|
|
|
|
|
$createsql =~ s/\)[^\)]*$//; # and trailing "CREATE" declaration, to get columns |
274
|
|
|
|
|
|
|
my @columns_and_contraints = split /,/, $createsql; |
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
# Set up the pivot_row (entity), pivot_column (attribute) and |
277
|
|
|
|
|
|
|
# pivot_value (value) columns, including foreign key specifications. |
278
|
|
|
|
|
|
|
my ($pivot_row, $pivot_row_type, $pivot_column, $pivot_value ); |
279
|
|
|
|
|
|
|
my ($pivot_row_ref, $pivot_column_ref, $pivot_value_ref); |
280
|
|
|
|
|
|
|
if (@pivot_columns == 3) { |
281
|
|
|
|
|
|
|
($pivot_row, $pivot_column, $pivot_value ) = map unescape($_), @pivot_columns; |
282
|
|
|
|
|
|
|
if ($pivot_row =~ / /) { |
283
|
|
|
|
|
|
|
($pivot_row,$pivot_row_type) = split / /, $pivot_row; |
284
|
|
|
|
|
|
|
} |
285
|
|
|
|
|
|
|
($pivot_row ,$pivot_row_ref) = _parse_refspec($pivot_row); |
286
|
|
|
|
|
|
|
($pivot_column,$pivot_column_ref) = _parse_refspec($pivot_column); |
287
|
|
|
|
|
|
|
($pivot_value ,$pivot_value_ref) = _parse_refspec($pivot_value); |
288
|
|
|
|
|
|
|
} else { |
289
|
|
|
|
|
|
|
($pivot_row, $pivot_column, $pivot_value ) = @columns_and_contraints; |
290
|
|
|
|
|
|
|
($pivot_row_type) = $pivot_row =~ /^\s*\S* (.*)$/; |
291
|
|
|
|
|
|
|
} |
292
|
|
|
|
|
|
|
for my $col ($pivot_row, $pivot_column, $pivot_value ) { |
293
|
|
|
|
|
|
|
$col =~ s/^\s*//; |
294
|
|
|
|
|
|
|
$col =~ s/ .*$//; |
295
|
|
|
|
|
|
|
next if grep /$col/i, @columns_and_contraints; |
296
|
|
|
|
|
|
|
warn "could not find $col in columns for $base_table\n"; |
297
|
|
|
|
|
|
|
} |
298
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
# Now compute the distinct values of pivot_row (attribute). |
300
|
|
|
|
|
|
|
debug "pivot_column (attribute) is $pivot_column"; |
301
|
|
|
|
|
|
|
my @columns = ( |
302
|
|
|
|
|
|
|
$pivot_row, |
303
|
|
|
|
|
|
|
$db->query( sprintf( |
304
|
|
|
|
|
|
|
"SELECT DISTINCT(%s) FROM %s", |
305
|
|
|
|
|
|
|
$pivot_column, $base_table))->flat |
306
|
|
|
|
|
|
|
); |
307
|
|
|
|
|
|
|
debug "distinct values for $pivot_column in $base_table are @columns"; |
308
|
|
|
|
|
|
|
|
309
|
|
|
|
|
|
|
my @vcolumns = @columns; # virtual table column names |
310
|
|
|
|
|
|
|
|
311
|
|
|
|
|
|
|
# Maybe apply foreign key transform to make vcolumns. |
312
|
|
|
|
|
|
|
if ($pivot_column_ref) { |
313
|
|
|
|
|
|
|
@vcolumns = ($vcolumns[0]); |
314
|
|
|
|
|
|
|
for my $c (@columns) { |
315
|
|
|
|
|
|
|
my ($next) = $db->select( |
316
|
|
|
|
|
|
|
$pivot_column_ref->{table}, |
317
|
|
|
|
|
|
|
$pivot_column_ref->{child_label}, |
318
|
|
|
|
|
|
|
{ $pivot_column_ref->{child_key} => $c } |
319
|
|
|
|
|
|
|
)->flat or next; |
320
|
|
|
|
|
|
|
push @vcolumns, $next; |
321
|
|
|
|
|
|
|
} |
322
|
|
|
|
|
|
|
} |
323
|
|
|
|
|
|
|
# Ensure that they are valid sqlite column names |
324
|
|
|
|
|
|
|
for (@vcolumns) { |
325
|
|
|
|
|
|
|
tr/a-zA-Z0-9_//dc; |
326
|
|
|
|
|
|
|
$_ = "$pivot_column\_$_" unless $_=~/^[a-zA-Z]/; |
327
|
|
|
|
|
|
|
} |
328
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
$pivot_row_type ||= "varchar"; # default entity type |
330
|
|
|
|
|
|
|
bless { |
331
|
|
|
|
|
|
|
name => $virtual_table, # the virtual pivot table name |
332
|
|
|
|
|
|
|
table => $base_table, # the base table name |
333
|
|
|
|
|
|
|
columns => \@columns, # the base table distinct(pivot_column) values |
334
|
|
|
|
|
|
|
vcolumns => \@vcolumns, # the names of the virtual pivot table columns |
335
|
|
|
|
|
|
|
pivot_row => $pivot_row, # the name of the "pivot row" column in the base table |
336
|
|
|
|
|
|
|
pivot_row_type => $pivot_row_type, # the column affinity for the pivot row |
337
|
|
|
|
|
|
|
pivot_row_ref => $pivot_row_ref, # hash (see _parse_refspec) |
338
|
|
|
|
|
|
|
pivot_column => $pivot_column, # the name of the "pivot column" column in the base table |
339
|
|
|
|
|
|
|
pivot_column_ref => $pivot_column_ref, # hash (see _parse_refspec) |
340
|
|
|
|
|
|
|
pivot_value => $pivot_value, # the name of the "pivot value" column in the base table |
341
|
|
|
|
|
|
|
pivot_value_ref => $pivot_value_ref, # hash (see _parse_refspec) |
342
|
|
|
|
|
|
|
}, $class; |
343
|
|
|
|
|
|
|
} |
344
|
|
|
|
|
|
|
*CONNECT = \&CREATE; |
345
|
|
|
|
|
|
|
|
346
|
|
|
|
|
|
|
=over |
347
|
|
|
|
|
|
|
|
348
|
|
|
|
|
|
|
=item DECLARE_SQL |
349
|
|
|
|
|
|
|
|
350
|
|
|
|
|
|
|
Arguments: none |
351
|
|
|
|
|
|
|
Returns: a CREATE TABLE statement that specifies the columns of |
352
|
|
|
|
|
|
|
the virtual table. |
353
|
|
|
|
|
|
|
|
354
|
|
|
|
|
|
|
=cut |
355
|
|
|
|
|
|
|
|
356
|
|
|
|
|
|
|
sub DECLARE_SQL { |
357
|
|
|
|
|
|
|
trace "DECLARE_SQL"; |
358
|
|
|
|
|
|
|
my $self = shift; |
359
|
|
|
|
|
|
|
return sprintf "CREATE TABLE %s (%s)", $self->table, join ',', $self->vcolumns; |
360
|
|
|
|
|
|
|
} |
361
|
|
|
|
|
|
|
|
362
|
|
|
|
|
|
|
# Map from incoming operators to sql operators |
363
|
|
|
|
|
|
|
our %OpMap = ( 'eq' => '=', 'lt' => '<', 'gt' => '>', |
364
|
|
|
|
|
|
|
'ge' => '>=', 'le' => '<=', 'match' => 'like',); |
365
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
# Create a new temporary table and return its name. |
367
|
|
|
|
|
|
|
sub _new_temp_table { |
368
|
|
|
|
|
|
|
my ($count) = $db->select('sqlite_temp_master','count(1)')->list; |
369
|
|
|
|
|
|
|
debug "made temp table number ".($count + 1 ); |
370
|
|
|
|
|
|
|
return sprintf("temp_%d_%d",$count + 1,$$); |
371
|
|
|
|
|
|
|
} |
372
|
|
|
|
|
|
|
|
373
|
|
|
|
|
|
|
# Generate and run a query using information created during BEST_INDEX |
374
|
|
|
|
|
|
|
# calls. This is called during a FILTER call. |
375
|
|
|
|
|
|
|
# |
376
|
|
|
|
|
|
|
# Arguments : |
377
|
|
|
|
|
|
|
# cursor : an SQLite::VirtualTable::Pivot::Cursor object |
378
|
|
|
|
|
|
|
# constraints : an array ref of hashrefs whose keys are : |
379
|
|
|
|
|
|
|
# column_name - the name of the column |
380
|
|
|
|
|
|
|
# operator - one of the keys of %OpMap above |
381
|
|
|
|
|
|
|
# bind : an arrayref of bind values, one per constraint. |
382
|
|
|
|
|
|
|
# |
383
|
|
|
|
|
|
|
sub _do_query { |
384
|
|
|
|
|
|
|
my ($self, $cursor, $constraints, $args) = @_; |
385
|
|
|
|
|
|
|
my @values = @$args; # bind values for constraints |
386
|
|
|
|
|
|
|
my $ref = $self->pivot_value_ref; |
387
|
|
|
|
|
|
|
# Set up join clauses and table in case the value is a foreign key. |
388
|
|
|
|
|
|
|
my $join_clause = sprintf( |
389
|
|
|
|
|
|
|
" INNER JOIN %s ON %s.%s=%s.%s ", |
390
|
|
|
|
|
|
|
#e.g. " INNER JOIN value_s ON value_s.id=eav.value "; |
391
|
|
|
|
|
|
|
$ref->{table}, $ref->{table}, $ref->{child_key}, |
392
|
|
|
|
|
|
|
$self->table, $ref->{child_label} |
393
|
|
|
|
|
|
|
) if $self->pivot_row_ref; |
394
|
|
|
|
|
|
|
my $value_table = $ref->{table} || $self->table; |
395
|
|
|
|
|
|
|
my $value_column = $ref->{child_label} || $self->pivot_column; |
396
|
|
|
|
|
|
|
for my $constraint (@$constraints) { |
397
|
|
|
|
|
|
|
my $value = shift @values; |
398
|
|
|
|
|
|
|
my $temp_table = _new_temp_table(); |
399
|
|
|
|
|
|
|
push @{ $cursor->temp_tables }, $temp_table; |
400
|
|
|
|
|
|
|
debug "creating temporary table $temp_table "; |
401
|
|
|
|
|
|
|
my $key = $self->pivot_row_type =~ /int/i ? " INTEGER PRIMARY KEY" : ""; |
402
|
|
|
|
|
|
|
$db->query( sprintf("CREATE TEMPORARY TABLE %s (%s $key)", |
403
|
|
|
|
|
|
|
$temp_table, $self->pivot_row) |
404
|
|
|
|
|
|
|
) or die $db->error; |
405
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
my ($query,@bind); |
407
|
|
|
|
|
|
|
if ($constraint->{column_name} eq $self->pivot_row) { |
408
|
|
|
|
|
|
|
$query = sprintf( "INSERT INTO %s SELECT DISTINCT(%s) FROM %s WHERE %s %s ?", |
409
|
|
|
|
|
|
|
$temp_table, $self->pivot_row, $self->table, $self->pivot_row, $OpMap{$constraint->{operator}} ); |
410
|
|
|
|
|
|
|
@bind = ($value); |
411
|
|
|
|
|
|
|
} else { |
412
|
|
|
|
|
|
|
$query = sprintf( "INSERT INTO %s SELECT %s FROM %s %s WHERE %s = ? AND %s.%s %s ?", |
413
|
|
|
|
|
|
|
$temp_table, |
414
|
|
|
|
|
|
|
$self->pivot_row, |
415
|
|
|
|
|
|
|
$self->table, $join_clause, |
416
|
|
|
|
|
|
|
$self->pivot_column, |
417
|
|
|
|
|
|
|
$value_table, $self->pivot_value, $OpMap{$constraint->{operator}}); |
418
|
|
|
|
|
|
|
@bind = ( $constraint->{column_name}, $value); |
419
|
|
|
|
|
|
|
} |
420
|
|
|
|
|
|
|
debug "ready to run $query with @bind"; |
421
|
|
|
|
|
|
|
$db->query($query, @bind ) or die $db->error; |
422
|
|
|
|
|
|
|
|
423
|
|
|
|
|
|
|
debug ("temp table $temp_table is for $constraint->{column_name} $constraint->{operator} $value"); |
424
|
|
|
|
|
|
|
#info ("temp table $temp_table has : ".join ",", $db->select($temp_table,"*")->list); |
425
|
|
|
|
|
|
|
} |
426
|
|
|
|
|
|
|
debug "created ".scalar @{ $cursor->temp_tables }." temp table(s)"; |
427
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
# Now we have created the temp tables, join them together to make the final query. |
429
|
|
|
|
|
|
|
|
430
|
|
|
|
|
|
|
my $value_table_or_a = $self->pivot_value_ref ? $self->pivot_value_ref->{table} : 'a'; |
431
|
|
|
|
|
|
|
my $sql = sprintf( "SELECT a.%s, %s, %s.%s AS %s FROM %s a", |
432
|
|
|
|
|
|
|
$self->pivot_row, # == entity |
433
|
|
|
|
|
|
|
$self->pivot_column, # == attribute |
434
|
|
|
|
|
|
|
$value_table_or_a, |
435
|
|
|
|
|
|
|
( $self->pivot_value_ref |
436
|
|
|
|
|
|
|
? $self->pivot_value_ref->{child_label} |
437
|
|
|
|
|
|
|
: $self->pivot_value ), |
438
|
|
|
|
|
|
|
$self->pivot_value, |
439
|
|
|
|
|
|
|
$self->table); |
440
|
|
|
|
|
|
|
|
441
|
|
|
|
|
|
|
$sql .= sprintf(" INNER JOIN %s ON a.%s = %s.id ", |
442
|
|
|
|
|
|
|
$value_table_or_a, $self->pivot_value, $value_table_or_a ) if $self->pivot_value_ref; |
443
|
|
|
|
|
|
|
|
444
|
|
|
|
|
|
|
for my $temp_table ($cursor->temp_tables) { |
445
|
|
|
|
|
|
|
$sql .= sprintf( " INNER JOIN %s ON %s.%s=a.%s ", |
446
|
|
|
|
|
|
|
$temp_table, $temp_table, |
447
|
|
|
|
|
|
|
$self->pivot_row, $self->pivot_row |
448
|
|
|
|
|
|
|
); |
449
|
|
|
|
|
|
|
} |
450
|
|
|
|
|
|
|
$sql .= sprintf(" ORDER BY a.%s", $self->pivot_row); |
451
|
|
|
|
|
|
|
|
452
|
|
|
|
|
|
|
# TODO move into cursor.pm |
453
|
|
|
|
|
|
|
my (@current_row); |
454
|
|
|
|
|
|
|
$cursor->reset; |
455
|
|
|
|
|
|
|
$cursor->{sth} = $db->dbh->prepare( $sql) or die "error in $sql : $DBI::errstr"; |
456
|
|
|
|
|
|
|
$cursor->sth->execute or die $DBI::errstr; |
457
|
|
|
|
|
|
|
$cursor->set( "last" => !( @current_row = $cursor->sth->fetchrow_array ) ); |
458
|
|
|
|
|
|
|
$cursor->set( current_row => \@current_row ); |
459
|
|
|
|
|
|
|
debug "ran query, first row is : @current_row"; |
460
|
|
|
|
|
|
|
} |
461
|
|
|
|
|
|
|
|
462
|
|
|
|
|
|
|
=item OPEN |
463
|
|
|
|
|
|
|
|
464
|
|
|
|
|
|
|
Create and return a new cursor. |
465
|
|
|
|
|
|
|
This returns a new SQLite::VirtualTable::Pivot::Cursor object. |
466
|
|
|
|
|
|
|
|
467
|
|
|
|
|
|
|
This is called before BEST_INDEX or FILTER, just to create the |
468
|
|
|
|
|
|
|
new empty object. |
469
|
|
|
|
|
|
|
|
470
|
|
|
|
|
|
|
=cut |
471
|
|
|
|
|
|
|
|
472
|
|
|
|
|
|
|
sub OPEN { |
473
|
|
|
|
|
|
|
my $self = shift; |
474
|
|
|
|
|
|
|
trace "(OPEN $self->{name})"; |
475
|
|
|
|
|
|
|
return SQLite::VirtualTable::Pivot::Cursor->new({virtual_table => $self})->reset; |
476
|
|
|
|
|
|
|
} |
477
|
|
|
|
|
|
|
|
478
|
|
|
|
|
|
|
=item BEST_INDEX |
479
|
|
|
|
|
|
|
|
480
|
|
|
|
|
|
|
Given a set of constraints and an order, return the name |
481
|
|
|
|
|
|
|
(and number) of the best index that should be used to |
482
|
|
|
|
|
|
|
run this query, and the cost of using this index. |
483
|
|
|
|
|
|
|
|
484
|
|
|
|
|
|
|
See SQLite::VirtualTable for a more complete description of |
485
|
|
|
|
|
|
|
the incoming and outgoing parameters. |
486
|
|
|
|
|
|
|
|
487
|
|
|
|
|
|
|
=cut |
488
|
|
|
|
|
|
|
|
489
|
|
|
|
|
|
|
sub BEST_INDEX { |
490
|
|
|
|
|
|
|
my ($self,$constraints,$order_bys) = @_; |
491
|
|
|
|
|
|
|
trace "(BEST_INDEX)"; |
492
|
|
|
|
|
|
|
# $order_bys is an arrayref of hashrefs with keys "column" and "direction". |
493
|
|
|
|
|
|
|
$self->{indexes} ||= []; |
494
|
|
|
|
|
|
|
$self->{counts} ||= {}; |
495
|
|
|
|
|
|
|
my $index_number = @{ $self->indexes }; |
496
|
|
|
|
|
|
|
my $index_name = "index_".$index_number; |
497
|
|
|
|
|
|
|
( $self->counts->{__table__} ) = $db->select( $self->table, 'count(1)', )->list; |
498
|
|
|
|
|
|
|
my $cost = $self->counts->{__table__}; |
499
|
|
|
|
|
|
|
my $i = 0; |
500
|
|
|
|
|
|
|
my @index_constraints; |
501
|
|
|
|
|
|
|
# We are going to build an "index" (in name only) for this set of |
502
|
|
|
|
|
|
|
# constraints. The cost will be the total number of matching attributes |
503
|
|
|
|
|
|
|
# in the table for each of the constraints. |
504
|
|
|
|
|
|
|
my %seen_column; |
505
|
|
|
|
|
|
|
for my $constraint (@$constraints) { |
506
|
|
|
|
|
|
|
# Keys of $constraint are : operator, usable, column. |
507
|
|
|
|
|
|
|
# We must fill in : arg_index, omit. |
508
|
|
|
|
|
|
|
next unless $constraint->{usable}; |
509
|
|
|
|
|
|
|
$cost ||= 0; |
510
|
|
|
|
|
|
|
my $column_name = $self->{columns}[$constraint->{column}]; |
511
|
|
|
|
|
|
|
debug "evaluating cost of using column $column_name, operator $constraint->{operator}"; |
512
|
|
|
|
|
|
|
$constraint->{arg_index} = $i++; # index of this constraint as it comes through in @args to FILTER |
513
|
|
|
|
|
|
|
$constraint->{omit} = 1; |
514
|
|
|
|
|
|
|
push @index_constraints, { |
515
|
|
|
|
|
|
|
operator => $constraint->{operator}, |
516
|
|
|
|
|
|
|
column_name => $column_name |
517
|
|
|
|
|
|
|
}; |
518
|
|
|
|
|
|
|
unless (defined($self->counts->{$column_name})) { |
519
|
|
|
|
|
|
|
# TODO cache these (when creating the table?) |
520
|
|
|
|
|
|
|
( $self->counts->{$column_name} ) = |
521
|
|
|
|
|
|
|
$db->select( $self->table, 'count(1)', |
522
|
|
|
|
|
|
|
{ $self->pivot_column => $column_name } )->list; |
523
|
|
|
|
|
|
|
} |
524
|
|
|
|
|
|
|
my $this_cost = $self->counts->{$column_name}; |
525
|
|
|
|
|
|
|
#debug "this cost is $this_cost"; |
526
|
|
|
|
|
|
|
$cost -= $this_cost unless $seen_column{$column_name}++; |
527
|
|
|
|
|
|
|
} |
528
|
|
|
|
|
|
|
push @{ $self->indexes }, { constraints => \@index_constraints, name => $index_name, cost => $cost }; |
529
|
|
|
|
|
|
|
unless (defined($cost)) { |
530
|
|
|
|
|
|
|
($cost) = $db->select($self->{table},'count(1)')->flat; |
531
|
|
|
|
|
|
|
debug "cost is num of rows which is $cost"; |
532
|
|
|
|
|
|
|
} |
533
|
|
|
|
|
|
|
my $order_by_consumed = 0; |
534
|
|
|
|
|
|
|
if ( @$order_bys == 1 ) |
535
|
|
|
|
|
|
|
{ # only consumed if we are ordering by the pivot_row in ascending order |
536
|
|
|
|
|
|
|
if ( $self->columns->[ $order_bys->[0]{column} ] eq $self->pivot_row |
537
|
|
|
|
|
|
|
&& $order_bys->[0]{direction} == 1 ) { |
538
|
|
|
|
|
|
|
$order_by_consumed = 1; |
539
|
|
|
|
|
|
|
} |
540
|
|
|
|
|
|
|
} |
541
|
|
|
|
|
|
|
debug "returning: index $index_number ($index_name) has cost $cost (orderconsumed: $order_by_consumed)"; |
542
|
|
|
|
|
|
|
return ( $index_number, $index_name, $order_by_consumed, $cost ); |
543
|
|
|
|
|
|
|
} |
544
|
|
|
|
|
|
|
|
545
|
|
|
|
|
|
|
=item FILTER |
546
|
|
|
|
|
|
|
|
547
|
|
|
|
|
|
|
Given a cursor and an index number (created dynamically in BEST_FILTER) |
548
|
|
|
|
|
|
|
and the @args to pass to the index, run the query on the base table, |
549
|
|
|
|
|
|
|
joining as necessary to filter the results. |
550
|
|
|
|
|
|
|
|
551
|
|
|
|
|
|
|
=cut |
552
|
|
|
|
|
|
|
|
553
|
|
|
|
|
|
|
sub FILTER { |
554
|
|
|
|
|
|
|
# called after OPEN, before NEXT |
555
|
|
|
|
|
|
|
my ($self, $cursor, $idxnum, $idxstr, @args) = @_; |
556
|
|
|
|
|
|
|
trace "(FILTER $cursor)"; |
557
|
|
|
|
|
|
|
debug "filter -- index chosen was $idxnum ($idxstr) "; |
558
|
|
|
|
|
|
|
my $constraints = $self->indexes->[$idxnum]{constraints}; |
559
|
|
|
|
|
|
|
debug "FILTER Is calling _do_query for $cursor"; |
560
|
|
|
|
|
|
|
$cursor->reset; |
561
|
|
|
|
|
|
|
$self->_do_query( $cursor, $constraints, \@args ); |
562
|
|
|
|
|
|
|
$self->NEXT($cursor); |
563
|
|
|
|
|
|
|
} |
564
|
|
|
|
|
|
|
|
565
|
|
|
|
|
|
|
=item EOF |
566
|
|
|
|
|
|
|
|
567
|
|
|
|
|
|
|
Are there any more rows left? |
568
|
|
|
|
|
|
|
|
569
|
|
|
|
|
|
|
=cut |
570
|
|
|
|
|
|
|
|
571
|
|
|
|
|
|
|
sub EOF { |
572
|
|
|
|
|
|
|
my ($self, $cursor ) = @_; |
573
|
|
|
|
|
|
|
$cursor->done; |
574
|
|
|
|
|
|
|
}; |
575
|
|
|
|
|
|
|
|
576
|
|
|
|
|
|
|
sub _row_values_are_equal { |
577
|
|
|
|
|
|
|
my $self = shift; |
578
|
|
|
|
|
|
|
my ($val1,$val2) = @_; |
579
|
|
|
|
|
|
|
return $val1==$val2 if $self->pivot_row_type =~ /integer/i; |
580
|
|
|
|
|
|
|
return $val1 eq $val2; |
581
|
|
|
|
|
|
|
} |
582
|
|
|
|
|
|
|
|
583
|
|
|
|
|
|
|
=item NEXT |
584
|
|
|
|
|
|
|
|
585
|
|
|
|
|
|
|
Advance the cursor one row. |
586
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
=cut |
588
|
|
|
|
|
|
|
|
589
|
|
|
|
|
|
|
sub NEXT { |
590
|
|
|
|
|
|
|
my ($self,$cursor) = @_; |
591
|
|
|
|
|
|
|
trace "(NEXT $cursor)"; |
592
|
|
|
|
|
|
|
$cursor->get_next_row; |
593
|
|
|
|
|
|
|
} |
594
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
=item COLUMN |
596
|
|
|
|
|
|
|
|
597
|
|
|
|
|
|
|
Get a piece of data from a given column (and the current row). |
598
|
|
|
|
|
|
|
|
599
|
|
|
|
|
|
|
=cut |
600
|
|
|
|
|
|
|
|
601
|
|
|
|
|
|
|
sub COLUMN { |
602
|
|
|
|
|
|
|
my ($self, $cursor, $n) = @_; |
603
|
|
|
|
|
|
|
my $value = $cursor->column_value( $self->{columns}[$n] ); |
604
|
|
|
|
|
|
|
return looks_like_number($value) ? 0 + $value : $value; |
605
|
|
|
|
|
|
|
} |
606
|
|
|
|
|
|
|
|
607
|
|
|
|
|
|
|
=item ROWID |
608
|
|
|
|
|
|
|
|
609
|
|
|
|
|
|
|
Generate a unique id for this row. |
610
|
|
|
|
|
|
|
|
611
|
|
|
|
|
|
|
=cut |
612
|
|
|
|
|
|
|
|
613
|
|
|
|
|
|
|
sub ROWID { |
614
|
|
|
|
|
|
|
my ($self, $cursor) = @_; |
615
|
|
|
|
|
|
|
return $cursor->row_id; |
616
|
|
|
|
|
|
|
} |
617
|
|
|
|
|
|
|
|
618
|
|
|
|
|
|
|
=item CLOSE |
619
|
|
|
|
|
|
|
|
620
|
|
|
|
|
|
|
Close the cursor. |
621
|
|
|
|
|
|
|
|
622
|
|
|
|
|
|
|
=cut |
623
|
|
|
|
|
|
|
|
624
|
|
|
|
|
|
|
sub CLOSE { |
625
|
|
|
|
|
|
|
my ($self,$cursor) = @_; |
626
|
|
|
|
|
|
|
trace "(CLOSE $cursor)"; |
627
|
|
|
|
|
|
|
for ($cursor->temp_tables) { |
628
|
|
|
|
|
|
|
$db->query("drop table $_") or warn "error dropping $_: ".$db->error; |
629
|
|
|
|
|
|
|
} |
630
|
|
|
|
|
|
|
} |
631
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
=item DROP |
633
|
|
|
|
|
|
|
|
634
|
|
|
|
|
|
|
Drop the virtual table. |
635
|
|
|
|
|
|
|
|
636
|
|
|
|
|
|
|
=cut |
637
|
|
|
|
|
|
|
|
638
|
|
|
|
|
|
|
sub DROP { |
639
|
|
|
|
|
|
|
|
640
|
|
|
|
|
|
|
} |
641
|
|
|
|
|
|
|
|
642
|
|
|
|
|
|
|
=item DISCONNECT |
643
|
|
|
|
|
|
|
|
644
|
|
|
|
|
|
|
Disconnect from the database. |
645
|
|
|
|
|
|
|
|
646
|
|
|
|
|
|
|
=cut |
647
|
|
|
|
|
|
|
|
648
|
|
|
|
|
|
|
sub DISCONNECT {} |
649
|
|
|
|
|
|
|
|
650
|
|
|
|
|
|
|
*DESTROY = \&DISCONNECT; |
651
|
|
|
|
|
|
|
|
652
|
|
|
|
|
|
|
=back |
653
|
|
|
|
|
|
|
|
654
|
|
|
|
|
|
|
=head1 TODO |
655
|
|
|
|
|
|
|
|
656
|
|
|
|
|
|
|
- re-use the existing database handle (requires changes |
657
|
|
|
|
|
|
|
to SQLite::VirtualTable and DBD::SQLite) |
658
|
|
|
|
|
|
|
- allow modification of the data in the virtual table |
659
|
|
|
|
|
|
|
- allow value column to not have integer affinity |
660
|
|
|
|
|
|
|
- more optimization |
661
|
|
|
|
|
|
|
|
662
|
|
|
|
|
|
|
=head1 SEE ALSO |
663
|
|
|
|
|
|
|
|
664
|
|
|
|
|
|
|
L |
665
|
|
|
|
|
|
|
|
666
|
|
|
|
|
|
|
L |
667
|
|
|
|
|
|
|
|
668
|
|
|
|
|
|
|
=cut |
669
|
|
|
|
|
|
|
|
670
|
|
|
|
|
|
|
1; |
671
|
|
|
|
|
|
|
|
672
|
|
|
|
|
|
|
|