Newer Older
694 lines | 18.598kb
packaging one directory
yuki-kimoto authored on 2009-11-16
1
package DBIx::Custom::SQL::Template;
2
use Object::Simple;
3

            
4
use Carp 'croak';
5

            
6
# Accessor is created by Object::Simple. Please read Object::Simple document
7

            
8
### Class-Object accessors
9

            
10
# Tag start
11
sub tag_start   : ClassObjectAttr {
12
    initialize => {default => '{', clone => 'scalar'}
13
}
14

            
15
# Tag end
16
sub tag_end     : ClassObjectAttr {
17
    initialize => {default => '}', clone => 'scalar'}
18
}
19

            
20
# Tag syntax
21
sub tag_syntax  : ClassObjectAttr {
22
    initialize => {default => <<'EOS', clone => 'scalar'}}
23
[tag]                     [expand]
24
{? name}                  ?
25
{= name}                  name = ?
26
{<> name}                 name <> ?
27

            
28
{< name}                  name < ?
29
{> name}                  name > ?
30
{>= name}                 name >= ?
31
{<= name}                 name <= ?
32

            
33
{like name}               name like ?
34
{in name number}          name in [?, ?, ..]
35

            
36
{insert key1 key2} (key1, key2) values (?, ?)
37
{update key1 key2}    set key1 = ?, key2 = ?
38
EOS
39

            
40
# Tag processors
41
sub tag_processors : ClassObjectAttr {
42
    type => 'hash',
43
    deref => 1,
44
    initialize => {
45
        clone => 'hash', 
46
        default => sub {{
47
            '?'             => \&DBIx::Custom::SQL::Template::TagProcessor::expand_basic_tag,
48
            '='             => \&DBIx::Custom::SQL::Template::TagProcessor::expand_basic_tag,
49
            '<>'            => \&DBIx::Custom::SQL::Template::TagProcessor::expand_basic_tag,
50
            '>'             => \&DBIx::Custom::SQL::Template::TagProcessor::expand_basic_tag,
51
            '<'             => \&DBIx::Custom::SQL::Template::TagProcessor::expand_basic_tag,
52
            '>='            => \&DBIx::Custom::SQL::Template::TagProcessor::expand_basic_tag,
53
            '<='            => \&DBIx::Custom::SQL::Template::TagProcessor::expand_basic_tag,
54
            'like'          => \&DBIx::Custom::SQL::Template::TagProcessor::expand_basic_tag,
55
            'in'            => \&DBIx::Custom::SQL::Template::TagProcessor::expand_in_tag,
56
            'insert'        => \&DBIx::Custom::SQL::Template::TagProcessor::expand_insert_tag,
57
            'update'    => \&DBIx::Custom::SQL::Template::TagProcessor::expand_update_tag
58
        }}
59
    }
60
}
61

            
62
# Add Tag processor
63
sub add_tag_processor {
64
    my $invocant = shift;
65
    my $tag_processors = ref $_[0] eq 'HASH' ? $_[0] : {@_};
66
    $invocant->tag_processors(%{$invocant->tag_processors}, %{$tag_processors});
67
    return $invocant;
68
}
69

            
70
# Clone
71
sub clone {
72
    my $self = shift;
73
    my $new = $self->new;
74
    
75
    $new->tag_start($self->tag_start);
76
    $new->tag_end($self->tag_end);
77
    $new->tag_syntax($self->tag_syntax);
78
    $new->tag_processors({%{$self->tag_processors || {}}});
79
    
80
    return $new;
81
}
82

            
83

            
84
### Object Methods
85

            
86
# Create Query
87
sub create_query {
88
    my ($self, $template)  = @_;
89
    
90
    # Parse template
91
    my $tree = $self->_parse_template($template);
92
    
93
    # Build query
94
    my $query = $self->_build_query($tree);
95
    
96
    return $query;
97
}
98

            
99
# Parse template
100
sub _parse_template {
101
    my ($self, $template) = @_;
102
    $template ||= '';
103
    
104
    my $tree = [];
105
    
106
    # Tags
107
    my $tag_start = quotemeta $self->tag_start;
108
    my $tag_end   = quotemeta $self->tag_end;
109
    
110
    # Tokenize
111
    my $state = 'text';
112
    
113
    # Save original template
114
    my $original_template = $template;
115
    
116
    # Parse template
117
    while ($template =~ s/([^$tag_start]*?)$tag_start([^$tag_end].*?)$tag_end//sm) {
118
        my $text = $1;
119
        my $tag  = $2;
120
        
121
        # Parse tree
122
        push @$tree, {type => 'text', tag_args => [$text]} if $text;
123
        
124
        if ($tag) {
125
            # Get tag name and arguments
126
            my ($tag_name, @tag_args) = split /\s+/, $tag;
127
            
128
            # Tag processor is exist?
129
            unless ($self->tag_processors->{$tag_name}) {
130
                my $tag_syntax = $self->tag_syntax;
131
                croak("Tag '{$tag}' in SQL template is not exist.\n\n" .
132
                      "<SQL template tag syntax>\n" .
133
                      "$tag_syntax\n" .
134
                      "<Your SQL template>\n" .
135
                      "$original_template\n\n");
136
            }
137
            
138
            # Check tag arguments
139
            foreach my $tag_arg (@tag_args) {
140
                # Cannot cantain placehosder '?'
141
                croak("Tag '{t }' arguments cannot contain '?'")
142
                  if $tag_arg =~ /\?/;
143
            }
144
            
145
            # Add tag to parsing tree
146
            push @$tree, {type => 'tag', tag_name => $tag_name, tag_args => [@tag_args]};
147
        }
148
    }
149
    
150
    # Add text to parsing tree 
151
    push @$tree, {type => 'text', tag_args => [$template]} if $template;
152
    
153
    return $tree;
154
}
155

            
156
# Build SQL from parsing tree
157
sub _build_query {
158
    my ($self, $tree) = @_;
159
    
160
    # SQL
161
    my $sql = '';
162
    
163
    # All parameter key infomation
164
    my $all_key_infos = [];
165
    
166
    # Build SQL 
167
    foreach my $node (@$tree) {
168
        
169
        # Get type, tag name, and arguments
170
        my $type     = $node->{type};
171
        my $tag_name = $node->{tag_name};
172
        my $tag_args = $node->{tag_args};
173
        
174
        # Text
175
        if ($type eq 'text') {
176
            # Join text
177
            $sql .= $tag_args->[0];
178
        }
179
        
180
        # Tag
181
        elsif ($type eq 'tag') {
182
            
183
            # Get tag processor
184
            my $tag_processor = $self->tag_processors->{$tag_name};
185
            
186
            # Tag processor is code ref?
187
            croak("Tag processor '$tag_name' must be code reference")
188
              unless ref $tag_processor eq 'CODE';
189
            
190
            # Expand tag using tag processor
191
            my ($expand, $key_infos)
192
              = $tag_processor->($tag_name, $tag_args);
193
            
194
            # Check tag processor return value
195
            croak("Tag processor '$tag_name' must return (\$expand, \$key_infos)")
196
              if !defined $expand || ref $key_infos ne 'ARRAY';
197
            
198
            # Check placeholder count
199
            croak("Placeholder count in SQL created by tag processor '$tag_name' " .
200
                  "must be same as key informations count")
201
              unless $self->_placeholder_count($expand) eq @$key_infos;
202
            
203
            # Add key information
204
            push @$all_key_infos, @$key_infos;
205
            
206
            # Join expand tag to SQL
207
            $sql .= $expand;
208
        }
209
    }
210
    
211
    # Add semicolon
212
    $sql .= ';' unless $sql =~ /;$/;
213
    
214
    # Query
215
    my $query = {sql => $sql, key_infos => $all_key_infos};
216
    
217
    return $query;
218
}
219

            
220
# Get placeholder count
221
sub _placeholder_count {
222
    my ($self, $expand) = @_;
223
    $expand ||= '';
224
    
225
    my $count = 0;
226
    my $pos   = -1;
227
    while (($pos = index($expand, '?', $pos + 1)) != -1) {
228
        $count++;
229
    }
230
    return $count;
231
}
232

            
233
Object::Simple->build_class;
234

            
235

            
236
package DBIx::Custom::SQL::Template::TagProcessor;
237
use strict;
238
use warnings;
239
use Carp 'croak';
240

            
241
# Expand tag '?', '=', '<>', '>', '<', '>=', '<=', 'like'
242
sub expand_basic_tag {
243
    my ($tag_name, $tag_args) = @_;
244
    my $original_key = $tag_args->[0];
245
    
246
    # Key is not exist
247
    croak("You must be pass key as argument to tag '{$tag_name }'")
248
      if !$original_key;
249
    
250
    # Expanded tag
251
    my $expand = $tag_name eq '?'
252
               ? '?'
253
               : "$original_key $tag_name ?";
254
    
255
    # Get table and clumn name
256
    my ($table, $column) = get_table_and_column($original_key);
257
    
258
    # Parameter key infomation
259
    my $key_info = {};
260
    
261
    # Original key
262
    $key_info->{original_key} = $original_key;
263
    
264
    # Table
265
    $key_info->{table}  = $table;
266
    
267
    # Column name
268
    $key_info->{column} = $column;
269
    
270
    # Access keys
271
    my $access_keys = [];
272
    push @$access_keys, [$original_key];
273
    push @$access_keys, [$table, $column] if $table && $column;
274
    $key_info->{access_keys} = $access_keys;
275
    
276
    # Add parameter key information
277
    my $key_infos = [];
278
    push @$key_infos, $key_info;
279
    
280
    return ($expand, $key_infos);
281
}
282

            
283
# Expand tag 'in'
284
sub expand_in_tag {
285
    my ($tag_name, $tag_args) = @_;
286
    my ($original_key, $placeholder_count) = @$tag_args;
287
    
288
    # Key must be specified
289
    croak("You must be pass key as first argument of tag '{$tag_name }'\n" . 
290
          "Usage: {$tag_name \$key \$placeholder_count}")
291
      unless $original_key;
292
      
293
    
294
    # Place holder count must be specified
295
    croak("You must be pass placeholder count as second argument of tag '{$tag_name }'\n" . 
296
          "Usage: {$tag_name \$key \$placeholder_count}")
297
      if !$placeholder_count || $placeholder_count =~ /\D/;
298

            
299
    # Expand tag
300
    my $expand = "$original_key $tag_name (";
301
    for (my $i = 0; $i < $placeholder_count; $i++) {
302
        $expand .= '?, ';
303
    }
304
    
305
    $expand =~ s/, $//;
306
    $expand .= ')';
307
    
308
    # Get table and clumn name
309
    my ($table, $column) = get_table_and_column($original_key);
310
    
311
    # Create parameter key infomations
312
    my $key_infos = [];
313
    for (my $i = 0; $i < $placeholder_count; $i++) {
314
        # Parameter key infomation
315
        my $key_info = {};
316
        
317
        # Original key
318
        $key_info->{original_key} = $original_key;
319
        
320
        # Table
321
        $key_info->{table}   = $table;
322
        
323
        # Column name
324
        $key_info->{column}  = $column;
325
        
326
        # Access keys
327
        my $access_keys = [];
328
        push @$access_keys, [$original_key, [$i]];
329
        push @$access_keys, [$table, $column, [$i]] if $table && $column;
330
        $key_info->{access_keys} = $access_keys;
331
        
332
        # Add parameter key infos
333
        push @$key_infos, $key_info;
334
    }
335
    
336
    return ($expand, $key_infos);
337
}
338

            
339
# Get table and column
340
sub get_table_and_column {
341
    my $key = shift;
342
    $key ||= '';
343
    
344
    return ('', $key) unless $key =~ /\./;
345
    
346
    my ($table, $column) = split /\./, $key;
347
    
348
    return ($table, $column);
349
}
350

            
351
# Expand tag 'insert'
352
sub expand_insert_tag {
353
    my ($tag_name, $tag_args) = @_;
354
    my $original_keys = $tag_args;
355
    
356
    # Insert key (k1, k2, k3, ..)
357
    my $insert_keys = '(';
358
    
359
    # placeholder (?, ?, ?, ..)
360
    my $place_holders = '(';
361
    
362
    foreach my $original_key (@$original_keys) {
363
        # Get table and column
364
        my ($table, $column) = get_table_and_column($original_key);
365
        
366
        # Join insert column
367
        $insert_keys   .= "$column, ";
368
        
369
        # Join place holder
370
        $place_holders .= "?, ";
371
    }
372
    
373
    # Delete last ', '
374
    $insert_keys =~ s/, $//;
375
    
376
    # Close 
377
    $insert_keys .= ')';
378
    $place_holders =~ s/, $//;
379
    $place_holders .= ')';
380
    
381
    # Expand tag
382
    my $expand = "$insert_keys values $place_holders";
383
    
384
    # Create parameter key infomations
385
    my $key_infos = [];
386
    foreach my $original_key (@$original_keys) {
387
        # Get table and clumn name
388
        my ($table, $column) = get_table_and_column($original_key);
389
        
390
        # Parameter key infomation
391
        my $key_info = {};
392
        
393
        # Original key
394
        $key_info->{original_key} = $original_key;
395
        
396
        # Table
397
        $key_info->{table}   = $table;
398
        
399
        # Column name
400
        $key_info->{column}  = $column;
401
        
402
        # Access keys
403
        my $access_keys = [];
404
        push @$access_keys, ['#insert', $original_key];
405
        push @$access_keys, ['#insert', $table, $column] if $table && $column;
406
        push @$access_keys, [$original_key];
407
        push @$access_keys, [$table, $column] if $table && $column;
408
        $key_info->{access_keys} = $access_keys;
409
        
410
        # Add parameter key infos
411
        push @$key_infos, $key_info;
412
    }
413
    
414
    return ($expand, $key_infos);
415
}
416

            
417
# Expand tag 'update'
418
sub expand_update_tag {
419
    my ($tag_name, $tag_args) = @_;
420
    my $original_keys = $tag_args;
421
    
422
    # Expanded tag
423
    my $expand = 'set ';
424
    
425
    # 
426
    foreach my $original_key (@$original_keys) {
427
        # Get table and clumn name
428
        my ($table, $column) = get_table_and_column($original_key);
429

            
430
        # Join key and placeholder
431
        $expand .= "$column = ?, ";
432
    }
433
    
434
    # Delete last ', '
435
    $expand =~ s/, $//;
436
    
437
    # Create parameter key infomations
438
    my $key_infos = [];
439
    foreach my $original_key (@$original_keys) {
440
        # Get table and clumn name
441
        my ($table, $column) = get_table_and_column($original_key);
442
        
443
        # Parameter key infomation
444
        my $key_info = {};
445
        
446
        # Original key
447
        $key_info->{original_key} = $original_key;
448
        
449
        # Table
450
        $key_info->{table}  = $table;
451
        
452
        # Column name
453
        $key_info->{column} = $column;
454
        
455
        # Access keys
456
        my $access_keys = [];
457
        push @$access_keys, ['#update', $original_key];
458
        push @$access_keys, ['#update', $table, $column] if $table && $column;
459
        push @$access_keys, [$original_key];
460
        push @$access_keys, [$table, $column] if $table && $column;
461
        $key_info->{access_keys} = $access_keys;
462
        
463
        # Add parameter key infos
464
        push @$key_infos, $key_info;
465
    }
466
    
467
    return ($expand, $key_infos);
468
}
469

            
470
1;
471

            
472
=head1 NAME
473

            
474
DBIx::Custom::SQL::Template - Custamizable SQL Template for DBIx::Custom
475

            
476
=head1 VERSION
477

            
478
Version 0.0101
479

            
480
=cut
481

            
482
=head1 SYNOPSIS
483
    
484
    my $sql_tmpl = DBIx::Custom::SQL::Template->new;
485
    
486
    my $tmpl   = "select from table {= k1} && {<> k2} || {like k3}";
487
    my $param = {k1 => 1, k2 => 2, k3 => 3};
488
    
489
    my $query = $sql_template->create_query($tmpl);
490
    
491
    
492
    # Using query from DBIx::Custom
493
    use DBIx::Custom;
494
    my $dbi = DBI->new(
495
       data_source => $data_source,
496
       user        => $user,
497
       password    => $password, 
498
       dbi_options => {PrintError => 0, RaiseError => 1}
499
    );
500
    
501
    $query = $dbi->create_query($tmpl); # This is SQL::Template create_query
502
    $dbi->query($query, $param);
503

            
504
=head1 CLASS-OBJECT ACCESSORS
505

            
506
Class-Object accessor is used from both object and class
507

            
508
    $class->$accessor # call from class
509
    $self->$accessor  # call form object
510

            
511
=head2 tag_processors
512

            
513
    # Set and get
514
    $self           = $sql_tmpl->tag_processors($tag_processors);
515
    $tag_processors = $sql_tmpl->tag_processors;
516
    
517
    # Sample
518
    $sql_tmpl->tag_processors(
519
        '?' => \&expand_question,
520
        '=' => \&expand_equel
521
    );
522

            
523
You can use add_tag_processor to add tag processor
524

            
525
=head2 tag_start
526

            
527
    # Set and get
528
    $self      = $sql_tmpl->tag_start($tag_start);
529
    $tag_start = $sql_tmpl->tag_start;
530
    
531
    # Sample
532
    $sql_tmpl->tag_start('{');
533

            
534
Default is '{'
535

            
536
=head2 tag_end
537

            
538
    # Set and get
539
    $self    = $sql_tmpl->tag_start($tag_end);
540
    $tag_end = $sql_tmpl->tag_start;
541
    
542
    # Sample
543
    $sql_tmpl->tag_start('}');
544

            
545
Default is '}'
546
    
547
=head2 tag_syntax
548
    
549
    # Set and get
550
    $self       = $sql_tmpl->tag_syntax($tag_syntax);
551
    $tag_syntax = $sql_tmpl->tag_syntax;
552
    
553
    # Sample
554
    $sql_tmpl->tag_syntax(
555
        "[Tag]            [Expand]\n" .
556
        "{? name}         ?\n" .
557
        "{= name}         name = ?\n" .
558
        "{<> name}        name <> ?\n"
559
    );
560

            
561
=head1 METHODS
562

            
563
=head2 create_query
564
    
565
    # Create SQL form SQL template
566
    $query = $sql_tmpl->create_query($tmpl);
567
    
568
    # Sample
569
    $query = $sql_tmpl->create_sql(
570
         "select * from table where {= title} && {like author} || {<= price}")
571
    
572
    # Result
573
    $qeury->{sql} : "select * from table where title = ? && author like ? price <= ?;"
574
    $query->{key_infos} : [['title'], ['author'], ['price']]
575
    
576
    # Sample2 (with table name)
577
    ($sql, @bind_values) = $sql_tmpl->create_sql(
578
            "select * from table where {= table.title} && {like table.author}",
579
            {table => {title => 'Perl', author => '%Taro%'}}
580
        )
581
    
582
    # Result2
583
    $query->{sql} : "select * from table where table.title = ? && table.title like ?;"
584
    $query->{key_infos} :[ [['table.title'],['table', 'title']],
585
                           [['table.author'],['table', 'author']] ]
586

            
587
This method create query using by DBIx::Custom.
588
query is two infomation
589

            
590
    1.sql       : SQL
591
    2.key_infos : Parameter access key information
592

            
593
=head2 add_tag_processor
594

            
595
Add tag processor
596
  
597
    # Add
598
    $self = $sql_tmpl->add_tag_processor($tag_processor);
599
    
600
    # Sample
601
    $sql_tmpl->add_tag_processor(
602
        '?' => sub {
603
            my ($tag_name, $tag_args) = @_;
604
            
605
            my $key1 = $tag_args->[0];
606
            my $key2 = $tag_args->[1];
607
            
608
            my $key_infos = [];
609
            
610
            # Expand tag and create key informations
611
            
612
            # Return expand tags and key informations
613
            return ($expand, $key_infos);
614
        }
615
    );
616

            
617
Tag processor recieve 2 argument
618

            
619
    1. Tag name            (?, =, <>, or etc)
620
    2. Tag arguments       (arg1 and arg2 in {tag_name arg1 arg2})
621

            
622
Tag processor return 2 value
623

            
624
    1. Expanded Tag (For exsample, '{= title}' is expanded to 'title = ?')
625
    2. Key infomations
626
    
627
You must be return expanded tag and key infomations.
628

            
629
Key information is a little complex. so I will explan this in future.
630

            
631
If you want to know more, Please see DBIx::Custom::SQL::Template source code.
632

            
633
=head2 clone
634

            
635
    # Clone DBIx::Custom::SQL::Template object
636
    $clone = $self->clone;
637
    
638
=head1 Available Tags
639
    
640
    # Available Tags
641
    [tag]            [expand]
642
    {? name}         ?
643
    {= name}         name = ?
644
    {<> name}        name <> ?
645
    
646
    {< name}         name < ?
647
    {> name}         name > ?
648
    {>= name}        name >= ?
649
    {<= name}        name <= ?
650
    
651
    {like name}      name like ?
652
    {in name}        name in [?, ?, ..]
653
    
654
    {insert}  (key1, key2, key3) values (?, ?, ?)
655
    {update}     set key1 = ?, key2 = ?, key3 = ?
656
    
657
    # Sample1
658
    $query = $sql_tmpl->create_sql(
659
        "insert into table {insert key1 key2}"
660
    );
661
    # Result1
662
    $sql : "insert into table (key1, key2) values (?, ?)"
663
    
664
    
665
    # Sample2
666
    $query = $sql_tmpl->create_sql(
667
        "update table {update key1 key2} where {= key3}"
668
    );
669
    
670
    # Result2
671
    $query->{sql} : "update table set key1 = ?, key2 = ? where key3 = ?;"
672
    
673
=head1 AUTHOR
674

            
675
Yuki Kimoto, C<< <kimoto.yuki at gmail.com> >>
676

            
677
Github 
678
L<http://github.com/yuki-kimoto>
679
L<http://github.com/yuki-kimoto/DBIx-Custom-SQL-Template>
680

            
681
Please let know me bag if you find
682
Please request me if you want to do something
683

            
684
=head1 COPYRIGHT & LICENSE
685

            
686
Copyright 2009 Yuki Kimoto, all rights reserved.
687

            
688
This program is free software; you can redistribute it and/or modify it
689
under the same terms as Perl itself.
690

            
691

            
692
=cut
693

            
694
1; # End of DBIx::Custom::SQL::Template