File Coverage

blib/lib/SQL/Entity.pm
Criterion Covered Total %
statement 167 171 97.6
branch 43 60 71.6
condition 16 28 57.1
subroutine 32 33 96.9
pod 20 20 100.0
total 278 312 89.1


line stmt bran cond sub pod time code
1             package SQL::Entity;
2              
3 5     5   74054 use warnings;
  5         1352  
  5         324  
4 5     5   33 use strict;
  5         10  
  5         203  
5 5     5   41 use vars qw(@EXPORT_OK %EXPORT_TAGS $VERSION);
  5         16  
  5         470  
6 5     5   23859 use Storable qw(dclone);
  5         24883  
  5         513  
7              
8             $VERSION = 0.05;
9              
10 5     5   2375 use Abstract::Meta::Class ':all';
  5         38338  
  5         1534  
11 5     5   123 use Carp 'confess';
  5         11  
  5         405  
12 5     5   4024 use SQL::Entity::Column ':all';
  5         30  
  5         2192  
13 5     5   4617 use SQL::Entity::Column::LOB ':all';
  5         15  
  5         998  
14 5     5   2541 use SQL::Entity::Condition ':all';
  5         14  
  5         2129  
15 5     5   3671 use SQL::Entity::Index ':all';
  5         160  
  5         725  
16 5     5   4500 use SQL::Entity::Relationship ':all';
  5         17  
  5         1350  
17              
18 5     5   46 use base qw(Exporter SQL::Entity::Table);
  5         10  
  5         9400  
19              
20 5     5   3609 use constant THE_ROWID => 'the_rowid';
  5         10  
  5         14251  
21              
22             @EXPORT_OK = qw(
23             sql_relationship
24             sql_column
25             sql_lob
26             sql_index
27             sql_cond
28             sql_and
29             sql_or
30             );
31              
32             %EXPORT_TAGS = (all => \@EXPORT_OK);
33              
34              
35             =head1 NAME
36              
37             SQL::Entity - Entity sql abstraction layer.
38              
39             =head1 CLASS HIERARCHY
40              
41             SQL::Entity::Table
42             |
43             +----SQL::Entity
44              
45             =head1 SYNOPSIS
46              
47             use SQL::Entity;
48              
49             my $entity = SQL::Entity->new(
50             id => 'emp',
51             name => 'emp',
52             unique_expression => 'rowid',
53             columns => {
54             emp_name => sql_column(name => 'ename'),
55             emp_no => sql_column(name => 'empno'),
56             },
57             );
58              
59             my($sql_text, $bind_variables) = $entity->query(
60             sql_cond('emp_no', '>', '20')
61             ->and(sql_cond('emp_name', 'NOT LIKE', 'HO%'))
62             )
63              
64             # select from database
65             .... do some stuff
66              
67             my ($sql_text, $bind_variables) = $entity->insert(
68             emp_no => '0',
69             emp_name => 'Smith',
70             );
71              
72             # insert row/s
73             ... do some stuff
74              
75             my ($sql_text, $bind_variables) = $entity->update(
76             { ename => 'Smith'},
77             { empno => '20'} #pk values
78             );
79              
80             # update row
81             ... do some stuff
82              
83             my ($sql_text, $bind_variables) = $entity->delete(
84             empno => '20'
85             );
86             # delete row/s
87             ... do some stuff
88              
89              
90             my $dept = SQL::Entity->new(
91             name => 'dept',
92             columns => [
93             sql_column(name => 'deptno'),
94             sql_column(name => 'dname')
95             ],
96             );
97              
98             my $emp = SQL::Entity->new(
99             name => 'emp',
100             primary_key => ['empno'],
101             unique_expression => 'rowid',
102             columns => [
103             sql_column(name => 'ename'),
104             sql_column(name => 'empno'),
105             sql_column(name => 'deptno')
106             ],
107             );
108              
109              
110             $emp->add_to_one_relationships(sql_relationship(
111             target_entity => $dept,
112             condition => sql_cond($dept->column('deptno'), '=', $entity->column('deptno'))
113             # or join_columns => ['deptno'],
114             ));
115              
116             $emp->add_subquery_columns($dept->column('dname'));
117              
118              
119             =head1 DESCRIPTION
120              
121             This class uses entity meta definition to generate different kinds of sql statmements.
122              
123             =head2 EXPORT
124              
125             sql_column
126             sql_lob
127             sql_index
128             sql_cond
129             sql_and
130             sql_or by 'all' tag
131              
132             =head2 ATTRIBUTES
133              
134             =over
135              
136             =item id
137              
138             =cut
139              
140             has '$.id';
141              
142              
143             =item query_from
144              
145             SQL fragment.
146              
147             =cut
148              
149             has '$.query_from';
150              
151              
152             =item query_from_helper
153              
154             Code referebce that may transform query_from
155              
156             =cut
157              
158             has '&.query_from_helper';
159              
160              
161             =item columns
162              
163             =cut
164              
165             has '%.subquery_columns' => (
166             item_accessor => 'subquery_column',
167             associated_class => 'SQL::Entity::Column',
168             index_by => 'id',
169             the_other_end => 'entity',
170             );
171              
172              
173             =item unique_expression
174              
175             Expression that's value will be used to identifying the unique row in Entity.
176             It may be any column or pseudo column like ROWID for Oracle,
177             or expression like PK_COLUMN1||PK_COLUMN2
178              
179             =cut
180              
181              
182             has '$.unique_expression';
183              
184              
185             =item unique_row_column
186              
187             Association to the column object that based on unique_expression.
188              
189             =cut
190              
191             has '$.unique_row_column';
192              
193              
194             =item to_one_relations
195              
196             Association many_to_one, or one_to_one tables.
197              
198             =cut
199              
200             has '%.to_one_relationships' => (associated_class => 'SQL::Entity::Relationship', item_accessor => 'to_one_relationship', index_by => 'name');
201              
202              
203             =item to_many_relations
204              
205             Association many_to_many, or one_to_many tables.
206             To many relation implicitly creates to one relation on the reflective entity.
207              
208             =cut
209              
210              
211             has '%.to_many_relationships' => (
212             associated_class => 'SQL::Entity::Relationship',
213             item_accessor => 'to_many_relationship',
214             index_by => 'name',
215             on_change => sub {
216             my ($self, $attribute, $scope, $value, $key) = @_;
217             if($scope eq 'mutator') {
218             foreach my $relation (values %$$value) {
219             $relation->associate_the_other_end($self);
220             }
221            
222             } else {
223             $$value->associate_the_other_end($self);
224             }
225             $self;
226             }
227             );
228              
229              
230             =item sql_template_parameters
231              
232             Allows use mini language variable,
233              
234             SELECT t.* FROM
235             (SELECT t.* FROM tab t WHERE t.col1 = [% var1 %]) t
236              
237             =cut
238              
239             has '%.sql_template_parameters' => (item_accessor => 'sql_template_parameter');
240              
241              
242             =item dml_generator
243              
244             Represents class that will be used to generate DML statements.
245             SQL::DMLGenerator by default.
246              
247             =cut
248              
249             {
250             my %loaded;
251             has '$.dml_generator' => (
252             default => 'SQL::DMLGenerator',
253             on_read => sub {
254             my ($self, $attribute, $scope, $value) = @_;
255             my $result = $attribute->get_value($self);
256             unless ($loaded{$result}) {
257             my $module = $result;
258             $module =~ s/::/\//g;
259             $module .= ".pm";
260             eval {
261             require $module;
262             $loaded{$result} = 1;
263             }
264             }
265             $result;
266             }
267             );
268             }
269              
270              
271             =back
272              
273             =head2 METHODS
274              
275             =over
276              
277             =item initialise
278              
279             =cut
280              
281             sub initialise {
282 11     11 1 2879 my ($self) = @_;
283 11         69 $self->SUPER::initialise();
284 11 50       271 unless ($self->id) {
285 11         118 my $schema = $self->schema;
286 11 50       155 $self->set_id(($schema ? $schema ."." :"") . $self->name);
287             }
288 11         211 $self->initialise_unique_row_column;
289             }
290              
291              
292             =item initialise_unique_row_column
293              
294             =cut
295              
296             sub initialise_unique_row_column {
297 11     11 1 18 my ($self) = @_;
298 11 100       40 unless ($self->unique_expression) {
299 4         50 my @pk = $self->primary_key;
300 4 50       49 confess "unique_expression or primary_key is required"
301             unless(@pk);
302 4 50       11 my $alias = @pk > 1 ? $self->alias : "";
303 4         20 $self->unique_expression( join "||", @pk);
304             }
305            
306 11 50       141 if ($self->unique_expression) {
307 11         100 my $unique_expression = $self->unique_expression;
308 11 50       119 $self->set_unique_row_column(
309             sql_column(
310             ($unique_expression =~ m/[^\w]/ ? 'expression' : 'name') => $self->unique_expression,
311             id => THE_ROWID() ,
312             table => $self,
313             updatable => 0,
314             insertable => 0,
315             )
316             );
317             }
318             }
319              
320              
321              
322             =item set_relationship_join_method
323              
324             Sets join methods
325              
326             =cut
327              
328             sub set_relationship_join_method {
329 13     13 1 29 my ($self, $column, $method, $join_methods) = @_;
330 13         36 my $table = $column->table;
331 13 100 100     166 if ($table && $table ne $self) {
332 4 100       13 return if $join_methods->{$table->id};
333 3         34 $join_methods->{$table->id} = $method;
334             }
335             }
336              
337              
338             =item query
339              
340             Returns sql statement and bind variables,
341             Takes optionally array ref of the requeted columns (undef returns all entity columns), condition object, bind_variables reference
342              
343             my ($sql, $bind_variables) = $entity->query(undef,
344             sql_cond('empno', '>', '20')->and(sql_cond('dname', 'NOT LIKE', 'HO%'))
345             );
346              
347             =cut
348              
349              
350             sub query {
351 13     13 1 2459 my ($self, @args) = @_;
352 13         92 my ($sql, $bind_variables) = $self->SUPER::query(@args);
353 13         56 $sql = $self->parse_template_parameters($sql);
354 13         48 ($sql, $bind_variables);
355             }
356              
357              
358             =item lock
359              
360             Returns sql that locks all rows that meets passed in condition
361             It uses SELECT ... FOR UPDATE pattern.
362             Takes optionally array ref of the requeted columns, condition object, bind_variables reference
363              
364             my ($sql, $bind_variables) = $entity->lock(undef,
365             sql_cond('empno', '>', '20')->and(sql_cond('dname', 'NOT LIKE', 'HO%'))
366             );
367              
368             =cut
369              
370             sub lock {
371 0     0 1 0 my ($self, @args) = @_;
372 0         0 my ($sql, $bind_variables) = $self->SUPER::query(@args);
373 0         0 $sql .= " FOR UPDATE";
374 0         0 ($sql, $bind_variables);
375             }
376              
377              
378             =item insert
379              
380             Returns insert sql statement and bind variables
381              
382             my ($sql, $bind_variables) = $entity->insert(
383             dname => 'hr',
384             deptno => '10',
385             ename => 'adi',
386             empno => '1',
387             );
388              
389             =cut
390              
391             sub insert {
392 2     2 1 1519 my ($self, %args) = @_;
393 2         15 my @columns = $self->insertable_columns;
394 2         23 my %field_values;
395 2         6 foreach my $column (@columns) {
396 6         37 my $name = $column->name;
397 6         59 $field_values{$name} = $args{$name};
398             }
399 2         9 my $dml_generator = $self->dml_generator;
400 2         38 $dml_generator->insert($self, \%field_values);
401             }
402              
403              
404             =item update
405              
406             Returns update sql statement and bind variables
407              
408             my ($sql, $bind_variables) = $entity->update(
409             {dname => 'hr',
410             deptno => '10',
411             ename => 'adi',
412             empno => '1',},
413             {the_rowid => 'AAAMgzAAEAAAAAgAAB'},
414             );
415              
416             =cut
417              
418             sub update {
419 2     2 1 2456 my ($self, $fields_values, $conditions) = @_;
420 2         15 my @columns = $self->updatable_columns;
421 2         24 my %field_values;
422            
423 2         6 foreach my $column (@columns) {
424 6         18 my $name = $column->name;
425 6 50       60 next unless exists($fields_values->{$name});
426 6         20 $field_values{$name} = $fields_values->{$name};
427             }
428              
429 2         9 my $dml_generator = $self->dml_generator;
430 2         13 $dml_generator->update($self, \%field_values, $conditions);
431              
432             }
433              
434              
435             =item delete
436              
437             Returns deletes sql statement and bind variables
438              
439             my ($sql, $bind_variables) = $entity->delete(empno => '1');
440              
441             =cut
442              
443             sub delete {
444 2     2 1 3319 my ($self, @args) = @_;
445 2         11 my $dml_generator = $self->dml_generator;
446 2         273 $dml_generator->delete($self, @args);
447             }
448              
449              
450             =item unique_condition_values
451              
452             Returns condition that uniquely identify the entity.
453             Takes the entity fields values, and validation flag.
454             If validation flag is true, then exception will be raise if there are not condition values.
455              
456             =cut
457              
458             sub unique_condition_values {
459 5     5 1 7101 my ($self, $fields_values, $validate) = @_;
460 5         19 my $column = $self->unique_row_column;
461 5         78 my %result;
462 5 100 33     44 if ($fields_values && $column && (defined $fields_values->{$column->id} || ($column->name && $fields_values->{$column->name}))) {
      66        
      33        
463 2   33     42 my $column_name = $column->name || $column->expression;
464 2   66     137 my $value = ($fields_values->{$column->id} || $fields_values->{$column_name});
465 2 50       355 $result{$column_name} = $value if $value;
466              
467             } else {
468 3         106 my @pk = $self->primary_key;
469 3         38 for my $column (@pk) {
470 3 100       13 next unless exists $fields_values->{$column};
471 1         3 my $value = $fields_values->{$column};
472 1 50       7 $result{$column} = $value if defined $value;
473             }
474             }
475 5 100       23 unless (%result) {
476 2         3 my @columns = values %{$self->columns};
  2         8  
477 2         24 for my $column (@columns) {
478 5 100       71 if($column->unique) {
479 2         20 my $column_name= $column->name;
480 2         17 my $value = $fields_values->{$column_name};
481 2 100       9 if (defined $value) {
482 1         3 $result{$column_name} = $value;
483 1         2 last;
484             }
485             }
486             }
487 2 100 50     21 confess "cant find unique value: on dataset: \n\t" . join ",\n\t", map { $_ . " => " . ($fields_values->{$_} || '')} keys %$fields_values
  1   66     345  
488             if !(%result) && $validate;
489             }
490            
491 4 50       27 wantarray ? (%result) : \%result;
492             }
493              
494              
495             =item selectable_columns
496              
497             Retuns list of columns that can be selected.
498             Takes requested columns as parameter.
499              
500             =cut
501              
502             sub selectable_columns {
503 13     13 1 25 my ($self, $requested_columns) = @_;
504 13         55 my $subquery_columns = $self->subquery_columns;
505 13         157 my @result = ($self->unique_row_column, (values %$subquery_columns), $self->SUPER::selectable_columns($requested_columns));
506 13 100       169 if (@$requested_columns) {
507 1         3 my %column_hash = map {$_->id, $_} @result;
  2         12  
508 1 50       10 return map {$column_hash{$_} ? ($column_hash{$_}) : ()} @$requested_columns;
  1         6  
509             }
510 12         48 @result;
511             }
512              
513              
514              
515             =item from_sql_clause
516              
517             Returns FROM .. sql fragment without join.
518              
519             =cut
520              
521             sub from_sql_clause {
522 16     16 1 25 my ($self, $join_methods) = @_;
523 16         63 my $query_from = $self->query_from;
524 16         162 my $query_from_helper = $self->query_from_helper;
525 16 100       126 $query_from = $query_from_helper->($self)
526             if $query_from_helper;
527 16         59 my $alias = $self->alias;
528 16         152 my $name = $self->name;
529 16 100       204 ($query_from
530             ? "( $query_from )" . $self->from_clause_alias
531             : $self->SUPER::from_clause_params($join_methods))
532             }
533              
534              
535             =item from_clause_params
536              
537             Returns FROM sql frgments with join clause.
538              
539             =cut
540              
541             sub from_clause_params {
542 15     15 1 22 my ($self, $join_methods) = @_;
543 15         54 $self->from_sql_clause($join_methods) . $self->join_clause($join_methods);
544             }
545              
546            
547             =item join_clause
548              
549             Returns "JOIN ... " sql fragment for all to one relationship
550              
551             =cut
552              
553             sub join_clause {
554 15     15 1 208 my ($self, $join_methods) = @_;
555 15         24 my $result = '';
556 15         62 foreach my $k (keys %$join_methods) {
557 3 100 66     10 my $relation = ($self->to_one_relationship($k) || $self->to_many_relationship($k)) or return '';
558 1         28 my $target_entity = $relation->target_entity;
559 1         10 my $join_method = $join_methods->{$k};
560 1 50       4 next if $join_method ne 'JOIN';
561 1         5 my $condition = $relation->condition;
562 1         16 my %query_columns = $target_entity->query_columns;
563 1         23 $result .= "\n${join_method} "
564             . $target_entity->from_clause_params($join_methods)
565             . " ON (" . $relation->join_condition_as_string($self) . ")";
566            
567             }
568 13         108 $result;
569             }
570              
571              
572             =item relationship_query
573              
574             Returns sql query + bind_variables to many relationship
575              
576             =cut
577              
578             sub relationship_query {
579 1     1 1 1542 my ($self, $relation_name, @args) = @_;
580 1 50       5 my $relationship = $self->relationship($relation_name)
581             or confess "cant find relationship ${relation_name}";
582 1         4 my $entity = $relationship->target_entity;
583 1         10 my ($sql, $bind_variables) = $entity->query();
584 1         3 my $condition = $self->condition_converter(@args);
585 1         3 $sql .= "\nWHERE EXISTS (SELECT 1 FROM "
586             . $self->from_sql_clause
587             . " WHERE " . $relationship->join_condition_as_string($self, $bind_variables, $condition) .")"
588             . $relationship->order_by_clause;
589 1         7 ($sql, $bind_variables);
590             }
591              
592              
593             =item normalise_field_names
594              
595             Replaces all keys that are passed in as alias to column name
596             for instance we have the folllowing SQL: SELECT ename as name, id, loc FROM emp
597             name will be replaced to ename.
598              
599             =cut
600              
601             sub normalise_field_names {
602 4     4 1 11 my ($self, @args) = @_;
603 4         12 my %columns = $self->query_columns;
604 4         61 my @result;
605 4         16 for(my $i = 0; $i < $#args; $i +=2) {
606 4         7 my $column = $args[$i];
607 4 50       23 push @result, (($columns{$column} ? $columns{$column}->name : $column), $args[$i + 1]);
608             }
609             @result
610 4         72 }
611              
612              
613             =item relationship
614              
615             Return relationship object, takes relationship name.
616              
617             =cut
618              
619             sub relationship {
620 1     1 1 2 my ($self, $relation_name) = @_;
621 1   50     4 my $result = $self->to_many_relationship($relation_name) || $self->to_one_relationship($relation_name) || '';
622 1 50       19 confess "cant find relationship $result" unless $result;
623 1         5 $result;
624             }
625              
626             =item query_columns
627              
628             All columns that belongs to this object.
629              
630             =cut
631              
632              
633             sub query_columns {
634 17     17 1 125 my ($self) = @_;
635 17         65 (THE_ROWID() => $self->unique_row_column, $self->subquery_columns, $self->SUPER::query_columns);
636             }
637              
638              
639             =item condition_converter
640              
641             Converts passed in argumets to condition object
642              
643             =cut
644              
645             sub condition_converter {
646 1     1 1 3 my ($self, @args) = @_;
647 1 50       10 (@args > 1)
648             ? SQL::Entity::Condition->struct_to_condition(@args)
649             : $args[0];
650             }
651              
652              
653             =item parse_template_parameters
654              
655             Parses template variables.
656              
657             =cut
658              
659             sub parse_template_parameters {
660 13     13 1 23 my ($self, $sql) = @_;
661 13 50       55 my $sql_template_parameters = $self->sql_template_parameters or return $sql;
662 13         167 for my $k (keys %$sql_template_parameters) {
663 1         2 my $value = $sql_template_parameters->{$k};
664 1         28 $sql =~ s/\[\%\s+$k\s+\%\]/$value/g;
665             }
666 13         34 $sql;
667             }
668              
669              
670             =item clone
671              
672             Clones this entity
673              
674             =cut
675              
676             sub clone {
677 5     5 1 44 my $self = shift;
678 5         749 dclone $self;
679             }
680              
681             1;
682              
683             __END__