DBIx-Custom / lib / DBIx / Custom / QueryBuilder.pm /
Newer Older
314 lines | 8.469kb
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
1
package DBIx::Custom::QueryBuilder;
2

            
3
use strict;
4
use warnings;
5

            
6
use base 'Object::Simple';
7

            
8
use Carp 'croak';
9
use DBIx::Custom::Query;
10
use DBIx::Custom::QueryBuilder::TagProcessor;
11

            
12
__PACKAGE__->dual_attr('tag_processors', default => sub { {} }, inherit => 'hash_copy');
13
__PACKAGE__->register_tag_processor(
14
    '?'      => \&DBIx::Custom::QueryBuilder::TagProcessors::expand_placeholder_tag,
15
    '='      => \&DBIx::Custom::QueryBuilder::TagProcessors::expand_basic_tag,
16
    '<>'     => \&DBIx::Custom::QueryBuilder::TagProcessors::expand_basic_tag,
17
    '>'      => \&DBIx::Custom::QueryBuilder::TagProcessors::expand_basic_tag,
18
    '<'      => \&DBIx::Custom::QueryBuilder::TagProcessors::expand_basic_tag,
19
    '>='     => \&DBIx::Custom::QueryBuilder::TagProcessors::expand_basic_tag,
20
    '<='     => \&DBIx::Custom::QueryBuilder::TagProcessors::expand_basic_tag,
21
    'like'   => \&DBIx::Custom::QueryBuilder::TagProcessors::expand_basic_tag,
22
    'in'     => \&DBIx::Custom::QueryBuilder::TagProcessors::expand_in_tag,
23
    'insert' => \&DBIx::Custom::QueryBuilder::TagProcessors::expand_insert_tag,
24
    'update' => \&DBIx::Custom::QueryBuilder::TagProcessors::expand_update_tag
25
);
26

            
27
__PACKAGE__->attr(tag_start => '{');
28
__PACKAGE__->attr(tag_end   => '}');
29

            
30
__PACKAGE__->attr('tag_syntax' => <<'EOS');
31
[tag]                     [expand]
32
{? name}                  ?
33
{= name}                  name = ?
34
{<> name}                 name <> ?
35

            
36
{< name}                  name < ?
37
{> name}                  name > ?
38
{>= name}                 name >= ?
39
{<= name}                 name <= ?
40

            
41
{like name}               name like ?
42
{in name number}          name in [?, ?, ..]
43

            
44
{insert key1 key2} (key1, key2) values (?, ?)
45
{update key1 key2}    set key1 = ?, key2 = ?
46
EOS
47

            
48
sub register_tag_processor {
49
    my $self = shift;
50
    my $tag_processors = ref $_[0] eq 'HASH' ? $_[0] : {@_};
51
    $self->tag_processors({%{$self->tag_processors}, %{$tag_processors}});
52
    return $self;
53
}
54

            
55
sub build_query {
56
    my ($self, $source)  = @_;
57
    
58
    # Parse
59
    my $tree = $self->_parse($source);
60
    
61
    # Build query
62
    my $query = $self->_build_query($tree);
63
    
64
    return $query;
65
}
66

            
67
sub _parse {
68
    my ($self, $source) = @_;
69
    
70
    if (ref $source eq 'ARRAY') {
71
        $source = $source->[1];
72
    }
73
    $source ||= '';
74
    
75
    my $tree = [];
76
    
77
    # Tags
78
    my $tag_start = quotemeta $self->tag_start;
79
    my $tag_end   = quotemeta $self->tag_end;
80
    
81
    # Tokenize
82
    my $state = 'text';
83
    
84
    # Save original
85
    my $original = $source;
86
    
87
    # Parse
88
    while ($source =~ s/([^$tag_start]*?)$tag_start([^$tag_end].*?)$tag_end//sm) {
89
        my $text = $1;
90
        my $tag  = $2;
91
        
92
        # Parse tree
93
        push @$tree, {type => 'text', tag_args => [$text]} if $text;
94
        
95
        if ($tag) {
96
            # Get tag name and arguments
97
            my ($tag_name, @tag_args) = split /\s+/, $tag;
98
            
99
            # Tag processor is exist?
100
            unless ($self->tag_processors->{$tag_name}) {
101
                my $tag_syntax = $self->tag_syntax;
102
                croak("Tag '{$tag}' is not registerd.\n\n" .
103
                      "<SQL builder syntax>\n" .
104
                      "$tag_syntax\n" .
105
                      "<Your source>\n" .
106
                      "$original\n\n");
107
            }
108
            
109
            # Check tag arguments
110
            foreach my $tag_arg (@tag_args) {
111
                # Cannot cantain placehosder '?'
112
                croak("Tag '{t }' arguments cannot contain '?'")
113
                  if $tag_arg =~ /\?/;
114
            }
115
            
116
            # Add tag to parsing tree
117
            push @$tree, {type => 'tag', tag_name => $tag_name, tag_args => [@tag_args]};
118
        }
119
    }
120
    
121
    # Add text to parsing tree 
122
    push @$tree, {type => 'text', tag_args => [$source]} if $source;
123
    
124
    return $tree;
125
}
126

            
127
sub _build_query {
128
    my ($self, $tree) = @_;
129
    
130
    # SQL
131
    my $sql = '';
132
    
133
    # All parameter key infomation
134
    my $all_columns = [];
135
    
136
    # Build SQL 
137
    foreach my $node (@$tree) {
138
        
139
        # Get type, tag name, and arguments
140
        my $type     = $node->{type};
141
        my $tag_name = $node->{tag_name};
142
        my $tag_args = $node->{tag_args};
143
        
144
        # Text
145
        if ($type eq 'text') {
146
            # Join text
147
            $sql .= $tag_args->[0];
148
        }
149
        
150
        # Tag
151
        elsif ($type eq 'tag') {
152
            
153
            # Get tag processor
154
            my $tag_processor = $self->tag_processors->{$tag_name};
155
            
156
            # Tag processor is code ref?
157
            croak("Tag processor '$tag_name' must be code reference")
158
              unless ref $tag_processor eq 'CODE';
159
            
160
            # Expand tag using tag processor
161
            my ($expand, $columns) = $tag_processor->($tag_name, $tag_args);
162
            
163
            # Check tag processor return value
164
            croak("Tag processor '$tag_name' must return (\$expand, \$columns)")
165
              if !defined $expand || ref $columns ne 'ARRAY';
166
            
167
            # Check placeholder count
168
            croak("Placeholder count in SQL created by tag processor '$tag_name' " .
169
                  "must be same as key informations count")
170
              unless $self->_placeholder_count($expand) eq @$columns;
171
            
172
            # Add key information
173
            push @$all_columns, @$columns;
174
            
175
            # Join expand tag to SQL
176
            $sql .= $expand;
177
        }
178
    }
179
    
180
    # Add semicolon
181
    $sql .= ';' unless $sql =~ /;$/;
182
    
183
    # Query
184
    my $query = DBIx::Custom::Query->new(sql => $sql, columns => $all_columns);
185
    
186
    return $query;
187
}
188

            
189
sub _placeholder_count {
190
    my ($self, $expand) = @_;
191
    $expand ||= '';
192
    
193
    my $count = 0;
194
    my $pos   = -1;
195
    while (($pos = index($expand, '?', $pos + 1)) != -1) {
196
        $count++;
197
    }
198
    return $count;
199
}
200

            
201
1;
202

            
203
=head1 NAME
204

            
205
DBIx::Custom::QueryBuilder - Query builder
206

            
207
=head1 SYNOPSIS
208
    
209
    my $builder = DBIx::Custom::QueryBuilder->new;
210
    
211
    my $source = "select from table {= k1} && {<> k2} || {like k3}";
212
    my $param = {k1 => 1, k2 => 2, k3 => 3};
213
    
214
    my $query = $sql_builder->build_query($source);
215

            
216
=head1 ATTRIBUTES
217

            
218
=head2 C<tag_processors>
219

            
220
    my $tag_processors = $builder->tag_processors;
221
    $builder           = $builder->tag_processors(\%tag_processors);
222

            
223
Tag processors.
224

            
225
=head2 C<tag_start>
226
    
227
    my $tag_start = $builder->tag_start;
228
    $builder      = $builder->tag_start('{');
229

            
230
String of tag start.
231
Default to '{'
232

            
233
=head2 C<tag_end>
234
    
235
    my $tag_end = $builder->tag_start;
236
    $builder    = $builder->tag_start('}');
237

            
238
String of tag end.
239
Default to '}'
240
    
241
=head2 C<tag_syntax>
242
    
243
    my $tag_syntax = $builder->tag_syntax;
244
    $builder       = $builder->tag_syntax($tag_syntax);
245

            
246
Tag syntax.
247

            
248
=head1 METHODS
249

            
250
This class is L<Object::Simple> subclass.
251
You can use all methods of L<Object::Simple>
252

            
253
=head2 C<new>
254

            
255
    my $builder = DBIx::Custom::SQLBuilder->new;
256
    my $builder = DBIx::Custom::SQLBuilder->new(%attrs);
257
    my $builder = DBIx::Custom::SQLBuilder->new(\%attrs);
258

            
259
Create a instance.
260

            
261
=head2 C<build_query>
262
    
263
    my $query = $builder->build_query($source);
264

            
265
Build L<DBIx::Custom::Query> object.
266

            
267
B<Example:>
268

            
269
Source:
270

            
271
    my $query = $builder->build_query(
272
      "select * from table where {= title} && {like author} || {<= price}")
273

            
274
Query:
275

            
276
    $qeury->sql : "select * from table where title = ? && author like ? price <= ?;"
277
    $query->columns : ['title', 'author', 'price']
278

            
279
=head2 C<register_tag_processor>
280

            
281
    $builder = $builder->register_tag_processor($tag_processor);
282

            
283
Register tag processor.
284

            
285
    $builder->register_tag_processor(
286
        '?' => sub {
287
            my $args = shift;
288
            
289
            # Do something
290
            
291
            # Expanded tag and column names
292
            return ($expand, $columns);
293
        }
294
    );
295

            
296
Tag processor receive arguments in tags
297
and must return expanded tag and column names.
298

            
299
=head1 Tags
300

            
301
    {? NAME}    ->   ?
302
    {= NAME}    ->   NAME = ?
303
    {<> NAME}   ->   NAME <> ?
304
    
305
    {< NAME}    ->   NAME < ?
306
    {> NAME}    ->   NAME > ?
307
    {>= NAME}   ->   NAME >= ?
308
    {<= NAME}   ->   NAME <= ?
309
    
310
    {like NAME}       ->   NAME like ?
311
    {in NAME COUNT}   ->   NAME in [?, ?, ..]
312
    
313
    {insert NAME1 NAME2 NAME3}   ->   (NAME1, NAME2, NAME3) values (?, ?, ?)
314
    {update NAME1 NAME2 NAME3}   ->   set NAME1 = ?, NAME2 = ?, NAME3 = ?