File Coverage

blib/lib/Metabase/Index/SQL.pm
Criterion Covered Total %
statement 246 255 96.4
branch 41 56 73.2
condition 11 17 64.7
subroutine 47 50 94.0
pod 0 17 0.0
total 345 395 87.3


line stmt bran cond sub pod time code
1 6     6   4326 use 5.006;
  6         20  
2 6     6   32 use strict;
  6         9  
  6         150  
3 6     6   34 use warnings;
  6         7  
  6         360  
4              
5             package Metabase::Index::SQL;
6             # ABSTRACT: Metabase index backend role for common SQL actions
7              
8             our $VERSION = '1.001';
9              
10 6     6   37 use Moose::Role;
  6         9  
  6         59  
11              
12 6     6   27781 use Class::Load qw/load_class try_load_class/;
  6         13  
  6         475  
13 6     6   3595 use Data::Stream::Bulk::Array;
  6         630183  
  6         253  
14 6     6   51 use Data::Stream::Bulk::Nil;
  6         10  
  6         159  
15 6     6   3461 use DBIx::RunSQL;
  6         9147  
  6         189  
16 6     6   3110 use DBIx::Simple;
  6         26596  
  6         174  
17 6     6   627 use File::Temp ();
  6         12590  
  6         120  
18 6     6   3332 use List::AllUtils qw/uniq/;
  6         48714  
  6         511  
19 6     6   4378 use SQL::Abstract;
  6         52193  
  6         358  
20 6     6   57 use SQL::Translator::Schema;
  6         11  
  6         198  
21 6     6   27 use SQL::Translator::Schema::Constants;
  6         10  
  6         388  
22 6     6   3217 use SQL::Translator::Diff;
  6         83638  
  6         255  
23 6     6   46 use SQL::Translator::Utils qw/normalize_name/;
  6         9  
  6         405  
24 6     6   2470 use SQL::Translator;
  6         144815  
  6         201  
25 6     6   68 use Try::Tiny;
  6         12  
  6         431  
26 6     6   3528 use Metabase::Fact;
  6         84449  
  6         19429  
27              
28             with 'Metabase::Backend::SQL';
29             with 'Metabase::Index' => { -version => 1.000 };
30              
31             has typemap => (
32             is => 'ro',
33             isa => 'HashRef',
34             lazy_build => 1,
35             );
36              
37             requires '_build_typemap';
38              
39             #--------------------------------------------------------------------------#
40             # attributes built by the role
41             #--------------------------------------------------------------------------#
42              
43             has _core_table => (
44             is => 'ro',
45             isa => 'Str',
46             default => sub { "core_meta" },
47             );
48              
49             has _requested_content_type => (
50             is => 'rw',
51             isa => 'Str',
52             clearer => '_clear_requested_content_type',
53             );
54              
55             has _requested_resource_type => (
56             is => 'rw',
57             isa => 'Str',
58             clearer => '_clear_requested_resource_type',
59             );
60              
61             has _query_fields => (
62             traits => ['Array'],
63             is => 'ro',
64             isa => 'ArrayRef[Str]',
65             lazy_build => 1,
66             handles => {
67             _push_query_fields => 'push',
68             _grep_query_fields => 'grep',
69             _all_query_fields => 'elements',
70             },
71             );
72              
73             has _content_tables => (
74             traits => ['Array'],
75             is => 'ro',
76             isa => 'ArrayRef[Str]',
77             lazy_build => 1,
78             handles => {
79             _push_content_tables => 'push',
80             _grep_content_tables => 'grep',
81             _all_content_tables => 'elements',
82             },
83             );
84              
85             has _resource_tables => (
86             traits => ['Array'],
87             is => 'ro',
88             isa => 'ArrayRef[Str]',
89             lazy_build => 1,
90             handles => {
91             _push_resource_tables => 'push',
92             _grep_resource_tables => 'grep',
93             _all_resource_tables => 'elements',
94             },
95             );
96              
97 69     69   2749 sub _build__content_tables { return [] }
98              
99 70     70   3079 sub _build__resource_tables { return [] }
100              
101 483     483   18439 sub _build__query_fields { return [] }
102              
103             sub _all_tables {
104 0     0   0 my $self = shift;
105             return
106 0         0 $self->_core_table,
107             $self->_all_content_tables,
108             $self->_all_resource_tables;
109             }
110              
111             #--------------------------------------------------------------------------#
112             # methods
113             #--------------------------------------------------------------------------#
114              
115             sub initialize {
116 71     71 0 14919 my ($self, $classes, $resources) = @_;
117 71         685 @$resources = uniq ( @$resources, "Metabase::Resource::metabase::user" );
118 71         2545 my $schema = $self->schema;
119             # Core table
120 71         2385 my $table = $self->_table_from_meta( $self->_core_table, Metabase::Fact->core_metadata_types );
121 71         704 my $pk = $table->get_field('guid');
122 71         3211 while ( my ($k,$v) = each %{$self->_guid_field_params} ) {
  213         18161  
123 142 50       2779 $pk->$k($v)
124             if $pk->can($k);
125             }
126             $table->add_constraint(
127 71         2449 name => $self->_core_table . "_pk",
128             fields => ['guid'],
129             type => PRIMARY_KEY,
130             );
131 71         117091 $schema->add_table($table);
132             # Fact tables
133             my @expanded =
134 0         0 map { $_->fact_classes }
135 71         24443 grep { $_->isa("Metabase::Report") }
  70         1664  
136             @$classes;
137 71         212 for my $c ( @$classes, @expanded ) {
138 70 50       668 next unless try_load_class($c);
139 70         8938 my $name = normalize_name( lc($c->type) );
140 70         1941 my $types = $c->content_metadata_types;
141 70 100 66     1011 next unless $types && keys %$types;
142 69         3548 $self->_push_content_tables($name);
143 69         271 my $table = $self->_table_from_meta( $name, $types );
144             $table->add_field(
145             name => '_guid',
146             is_nullable => 0,
147 69         167 %{$self->_guid_field_params}
  69         2410  
148             );
149 69         87549 $table->add_constraint(
150             name => "${name}_pk",
151             fields => ['_guid'],
152             type => PRIMARY_KEY,
153             );
154 69         122164 $schema->add_table($table);
155             }
156             # Resource tables
157 71         22711 for my $r ( @$resources ) {
158 139 50       24210 next unless try_load_class($r);
159 139         13208 my $name = $r;
160 139         722 $name =~ s/^Metabase::Resource:://;
161 139         596 $name =~ s/::/_/g;
162 139         680 $name = normalize_name( lc $name );
163 139         3276 my $types = $r->metadata_types;
164 139 50       3038 next unless keys %$types;
165 139         6972 $self->_push_resource_tables($name);
166 139         489 my $table = $self->_table_from_meta( $name, $types );
167             $table->add_field(
168             name => '_guid',
169             is_nullable => 0,
170             is_primary_key => 1,
171 139         387 %{$self->_guid_field_params}
  139         5063  
172             );
173 139         203690 $table->add_constraint(
174             name => "${name}_pk",
175             fields => ['_guid'],
176             type => PRIMARY_KEY,
177             );
178 139         224960 $schema->add_table($table);
179             }
180              
181 71         23396 $self->_deploy_schema;
182              
183 71         625 return;
184             }
185              
186             sub _table_from_meta {
187 279     279   1462 my ($self, $name, $typehash) = @_;
188 279         7712 my $table = SQL::Translator::Schema::Table->new( name => $name );
189 279         82315 for my $k ( sort keys %$typehash ) {
190             # warn "Adding $k to $name\n";
191             $table->add_field(
192             name => normalize_name($k),
193 1188   50     1050538 data_type => $self->typemap->{$typehash->{$k} || "//str"},
194             );
195             }
196 279         383183 return $table;
197             }
198              
199             sub _content_table {
200 169     169   460 my ($self, $name) = @_;
201 169         841 return normalize_name( lc $name );
202             }
203              
204             sub _resource_table {
205 50     50   91 my ($self, $name) = @_;
206 50         199 $name =~ s/^Metabase-Resource-//;
207 50         238 return normalize_name( lc $name );
208             }
209              
210             sub _get_search_sql {
211 482     482   942 my ( $self, $select, $spec ) = @_;
212              
213             # clear type constraints before analyzing query
214 482         21319 $self->_clear_requested_content_type;
215 482         18139 $self->_clear_requested_resource_type;
216 482         16229 $self->_clear_query_fields;
217              
218 482         1635 my ($where, $limit) = $self->get_native_query($spec);
219              
220 482         655 my ($saw_content_field, $saw_resource_field);
221 482         19948 for my $f ( $self->_all_query_fields ) {
222 499 100       2381 $saw_content_field++ if $f =~ qr{^content\.};
223 499 100       2128 $saw_resource_field++ if $f =~ qr{^resource\.};
224 499 100       3062 return unless $f =~ qr{^(?:core|content|resource)\.};
225             }
226              
227 465 50 66     3804 if ( $saw_content_field && ! $self->_requested_content_type ) {
228 0         0 Carp::confess("query requested content metadata without content type constraint");
229             }
230 465 50 66     2467 if ( $saw_resource_field && ! $self->_requested_resource_type ) {
231 0         0 Carp::confess("query requested resource metadata without resource type constraint");
232             }
233              
234             # based on requests, conduct joins
235 465         1030 my @from = qq{from "core_meta" core};
236 465 50       14990 return unless $self->_check_query_fields($self->_core_table, 'core');
237              
238 465 100       15396 if ( my $content_type = $self->_requested_content_type ) {
239 153         507 my $content_table = $self->_content_table($content_type);
240 153 50       1833 return unless $self->_check_query_fields($content_table, 'content');
241 153         499 push @from, qq{join "$content_table" content on core.guid = content._guid};
242             }
243 465 100       15328 if ( my $resource_type = $self->_requested_resource_type ) {
244 34         90 my $resource_table = $self->_resource_table($resource_type);
245 34 50       345 return unless $self->_check_query_fields($resource_table, 'resource');
246 34         103 push @from, qq{join "$resource_table" resource on core.guid = resource._guid};
247             }
248              
249 465         2097 my $sql = join(" ", $select, @from, $where);
250 465         1420 return ($sql, $limit);
251             }
252              
253             sub _check_query_fields {
254 652     652   1078 my ($self, $table, $type) = @_; # type 'core', 'resource' or 'content'
255 652         20132 my $table_obj = $self->schema->get_table("$table");
256 652         31730 for my $f ( $self->_all_query_fields ) {
257 856 100       67460 next unless $f =~ /^$type\.(.+)$/;
258 482         977 my $name = $1;
259 482 50       1808 return unless $table_obj->get_field($name);
260             }
261 652         78944 return 1;
262             }
263              
264             sub add {
265 16     16 0 7616 my ( $self, $fact ) = @_;
266              
267 16 50       108 Carp::confess("can't index a Fact without a GUID") unless $fact->guid;
268              
269             try {
270 16     16   1281 $self->dbis->begin_work();
271 16         550 my $core_meta = $fact->core_metadata;
272 16         696 $core_meta->{resource} = "$core_meta->{resource}"; #stringify obj
273             # use Data::Dumper;
274             # warn "Adding " . Dumper $core_meta;
275 16         258 $core_meta->{guid} = $self->_munge_guid($core_meta->{guid});
276 16         550 $self->dbis->insert( 'core_meta', $core_meta );
277 16         14828 my $content_meta = $fact->content_metadata;
278             # not all facts have content metadata
279 16 50       1593 if ( keys %$content_meta ) {
280 16         59 $content_meta->{_guid} = $self->_munge_guid($fact->guid);
281             # use Data::Dumper;
282             # warn "Adding " . Dumper $content_meta;
283 16         92 my $content_table = $self->_content_table( $fact->type );
284 16         793 $self->dbis->insert( $content_table, $content_meta );
285             }
286             # XXX eventually, add resource metadata -- dagolden, 2011-08-24
287 16         6849 my $resource_meta = $fact->resource_metadata;
288             # not all facts have resource metadata
289 16 50       1490 if ( keys %$resource_meta ) {
290 16         62 $resource_meta->{_guid} = $self->_munge_guid($fact->guid);
291             # use Data::Dumper;
292             # warn "Adding " . Dumper $resource_meta;
293 16         69 my $resource_table = $self->_resource_table( $resource_meta->{type} );
294 16         762 $self->dbis->insert( $resource_table, $resource_meta );
295             }
296 16         9074 $self->dbis->commit;
297             }
298             catch {
299 0     0   0 $self->dbis->rollback;
300 0         0 Carp::confess("Error inserting record: $_");
301 16         299 };
302              
303             }
304              
305             sub count {
306 255     255 0 11233 my ( $self, %spec) = @_;
307              
308 255         676 my ($sql, $limit) = $self->_get_search_sql("select count(*)", \%spec);
309              
310 255 50       611 return 0 unless $sql;
311             # warn "COUNT: $sql\n";
312              
313 255         7233 my ($count) = $self->dbis->query($sql)->list;
314              
315 255         39500 return $count;
316             }
317              
318             sub query {
319 19     19 0 19461 my ( $self, %spec) = @_;
320              
321 19         67 my ($sql, $limit) = $self->_get_search_sql("select core.guid", \%spec);
322              
323 19 100       86 return Data::Stream::Bulk::Nil->new
324             unless $sql;
325              
326             # warn "QUERY: $sql\n";
327 18         556 my $result = $self->dbis->query($sql);
328              
329             return Data::Stream::Bulk::Array->new(
330 18         4425 array => [ map { $self->_unmunge_guid( $_->[0] ) } $result->arrays ]
  20         477  
331             );
332             }
333              
334             # XXX evil hackery to allow shards to give us ordering info
335             sub _shard_query {
336 208     208   626 my ( $self, %spec) = @_;
337 208         288 my $spec = \%spec;
338              
339 208         264 my $select;
340 208 100 66     1274 if ( defined $spec->{-order} and ref $spec->{-order} eq 'ARRAY') {
341 48         73 my @clauses;
342 48         59 my @order = @{$spec->{-order}};
  48         188  
343 48         148 while ( @order ) {
344 48         136 my ($dir, $field) = splice( @order, 0, 2);
345 48         220 $field = $self->_quote_field( $field );
346 48         216 $dir =~ s/^-//;
347 48         106 $dir = uc $dir;
348 48         194 push @clauses, $field;
349             }
350 48         123 $select = "select " . join(", ", "core.guid", @clauses);
351             }
352             else {
353 160         246 $select = "select core.guid";
354             }
355              
356 208         561 my ($sql, $limit) = $self->_get_search_sql($select, \%spec);
357              
358 208 100       992 return Data::Stream::Bulk::Nil->new
359             unless $sql;
360              
361             # warn "QUERY: $sql\n";
362 192         6440 my $result = $self->dbis->query($sql);
363              
364 192         45757 return Data::Stream::Bulk::Array->new(
365             array => [ $result->arrays ]
366             );
367             }
368              
369             sub delete {
370 4     4 0 2277 my ( $self, $guid ) = @_;
371              
372 4 50       17 Carp::confess("can't delete without a GUID") unless $guid;
373              
374 4         26 $guid = $self->_munge_guid($guid);
375             try {
376 4     4   309 $self->dbis->begin_work();
377 4         242 $self->dbis->delete( 'core_meta', { 'guid' => $guid } );
378             # XXX need to track _content_tables
379 4         2445 for my $table ( uniq $self->_all_content_tables ) {
380 4         112 $self->dbis->delete( $table, { '_guid' => $guid } );
381             }
382 4         1527 for my $table ( uniq $self->_all_resource_tables ) {
383 8         1470 $self->dbis->delete( $table, { '_guid' => $guid } );
384             }
385             # XXX eventually, add resource metadata -- dagolden, 2011-08-24
386 4         1275 $self->dbis->commit;
387             }
388             catch {
389 0     0   0 $self->dbis->rollback;
390 0         0 Carp::confess("Error deleting record: $_");
391 4         52 };
392             # delete
393             }
394              
395             #--------------------------------------------------------------------------#
396             # required by Metabase::Query
397             #--------------------------------------------------------------------------#
398              
399             requires '_quote_field';
400             requires '_quote_val';
401              
402             # We need to track fields used in a query
403             before _quote_field => sub {
404             my ($self, $field) = @_;
405             $self->_push_query_fields($field);
406             };
407              
408             # We need to track type constraints to determine which tables to join
409             before op_eq => sub {
410             my ($self, $field, $value) = @_;
411             if ($field eq 'core.type') {
412             $self->_requested_content_type( $value );
413             }
414             if ($field eq 'resource.type') {
415             $self->_requested_resource_type( $value );
416             }
417             };
418              
419             sub translate_query {
420 496     496 0 40588 my ( $self, $spec ) = @_;
421              
422 496         597 my (@parts, $limit);
423              
424             # where
425 496 100       1821 if ( defined $spec->{-where} ) {
426 309         1207 push @parts, "where " . $self->dispatch_query_op( $spec->{-where} );
427             }
428              
429             # order
430 496 100 66     2186 if ( defined $spec->{-order} and ref $spec->{-order} eq 'ARRAY') {
431 53         48 my @clauses;
432 53         70 my @order = @{$spec->{-order}};
  53         159  
433 53         137 while ( @order ) {
434 53         123 my ($dir, $field) = splice( @order, 0, 2);
435 53         163 $field = $self->_quote_field( $field );
436 53         205 $dir =~ s/^-//;
437 53         152 $dir = uc $dir;
438 53         188 push @clauses, "$field $dir";
439             }
440 53         139 push @parts, qq{order by } . join(", ", @clauses);
441             }
442              
443             # limit
444 496 100       1458 if ( $limit = $spec->{-limit} ) {
445 18         38 push @parts, qq{limit $limit};
446             }
447              
448 496         1946 return join( q{ }, @parts ), $limit;
449             }
450              
451             around [qw/op_eq op_ne op_gt op_lt op_ge op_le op_like/ ] => sub {
452             my $orig = shift;
453             my $self = shift;
454             my ($field, $val) = @_;
455             if ( $field eq "core.guid" ) {
456             # warn "*** Fixing $field ($val)";
457             $val = $self->_munge_guid($val);
458             }
459             return $self->$orig($field, $val);
460             };
461              
462             around [qw/op_between/ ] => sub {
463             my $orig = shift;
464             my $self = shift;
465             my ($field, $low, $high) = @_;
466             if ( $field eq "core.guid" ) {
467             $low = $self->_munge_guid($low);
468             $high = $self->_munge_guid($high);
469             }
470             return $self->$orig($field, $low, $high);
471             };
472              
473             sub op_eq {
474 385     385 0 588 my ($self, $field, $val) = @_;
475 385         1092 return $self->_quote_field($field) . " = " . $self->_quote_val($val);
476             }
477              
478             sub op_ne {
479 52     52 0 93 my ($self, $field, $val) = @_;
480 52         146 return $self->_quote_field($field) . " != " . $self->_quote_val($val);
481             }
482              
483             sub op_gt {
484 21     21 0 43 my ($self, $field, $val) = @_;
485 21         67 return $self->_quote_field($field) . " > " . $self->_quote_val($val);
486             }
487              
488             sub op_lt {
489 3     3 0 9 my ($self, $field, $val) = @_;
490 3         9 return $self->_quote_field($field) . " < " . $self->_quote_val($val);
491             }
492              
493             sub op_ge {
494 1     1 0 3 my ($self, $field, $val) = @_;
495 1         6 return $self->_quote_field($field) . " >= " . $self->_quote_val($val);
496             }
497              
498             sub op_le {
499 1     1 0 4 my ($self, $field, $val) = @_;
500 1         5 return $self->_quote_field($field) . " <= " . $self->_quote_val($val);
501             }
502              
503             sub op_between {
504 1     1 0 4 my ($self, $field, $low, $high) = @_;
505 1         5 return $self->_quote_field($field) . " between "
506             . $self->_quote_val($low) . " and " . $self->_quote_val($high);
507             }
508              
509             sub op_like {
510 1     1 0 4 my ($self, $field, $val) = @_;
511             # XXX really should quote/check $val
512 1         7 return $self->_quote_field($field) . " like " . $self->_quote_val($val);
513             }
514              
515             sub op_not {
516 1     1 0 92 my ($self, $pred) = @_;
517 1         4 my $clause = $self->dispatch_query_op($pred);
518 1         6 return "NOT ($clause)";
519             }
520              
521             sub op_or {
522 1     1 0 115 my ($self, @args) = @_;
523 1         5 my @predicates = map { $self->dispatch_query_op($_) } @args;
  2         8  
524 1         4 return join(" or ", map { "($_)" } @predicates);
  2         10  
525             }
526              
527             sub op_and {
528 155     155 0 14160 my ($self, @args) = @_;
529 155         323 my @predicates = map { $self->dispatch_query_op($_) } @args;
  310         895  
530 155         292 return join(" and ", map { "($_)" } @predicates);
  310         1228  
531             }
532              
533             1;
534              
535             __END__
536              
537             =pod
538              
539             =encoding UTF-8
540              
541             =head1 NAME
542              
543             Metabase::Index::SQL - Metabase index backend role for common SQL actions
544              
545             =head1 VERSION
546              
547             version 1.001
548              
549             =head1 SYNOPSIS
550              
551             package Metabase::Index::SQLite;
552              
553             use Moose;
554              
555             with 'Metabase::Index::SQL';
556              
557             # implement required fields
558             ...;
559              
560             1;
561              
562             =head1 DESCRIPTION
563              
564             This is a role that consumes the L<Metabase::Backend::SQL> role and implements
565             implements the L<Metabase::Index> and L<Metabase::Query> roles generically
566             using SQL semantics. RDBMS vendor specific methods must be implemented by a
567             Moose class consuming this role.
568              
569             The following methods must be implemented:
570              
571             _build_dsn # a DSN string for DBI
572             _build_db_user # a username for DBI
573             _build_db_pass # a password for DBI
574             _build_db_type # a SQL::Translator type for the DB vendor
575             _build_typemap # hashref of metadata types to schema data types
576             _quote_field # vendor-specific identifier quoting
577             _quote_val # vendor-specific value quoting
578              
579             =for Pod::Coverage::TrustPod add query delete count initialize
580             translate_query op_eq op_ne op_gt op_lt op_ge op_le op_between op_like
581             op_not op_or op_and PRIMARY_KEY
582              
583             =head1 AUTHORS
584              
585             =over 4
586              
587             =item *
588              
589             David Golden <dagolden@cpan.org>
590              
591             =item *
592              
593             Leon Brocard <acme@astray.org>
594              
595             =back
596              
597             =head1 COPYRIGHT AND LICENSE
598              
599             This software is Copyright (c) 2011 by David Golden.
600              
601             This is free software, licensed under:
602              
603             The Apache License, Version 2.0, January 2004
604              
605             =cut