DBIx-Custom / lib / DBIx / Custom / QueryBuilder.pm /
Newer Older
315 lines | 8.278kb
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(
changed arguments of tag pro...
yuki-kimoto authored on 2010-08-05
14
    '?'      => \&DBIx::Custom::QueryBuilder::TagProcessors::placeholder,
15
    '='      => \&DBIx::Custom::QueryBuilder::TagProcessors::equal,
16
    '<>'     => \&DBIx::Custom::QueryBuilder::TagProcessors::not_equal,
17
    '>'      => \&DBIx::Custom::QueryBuilder::TagProcessors::greater_than,
18
    '<'      => \&DBIx::Custom::QueryBuilder::TagProcessors::lower_than,
19
    '>='     => \&DBIx::Custom::QueryBuilder::TagProcessors::greater_than_equal,
20
    '<='     => \&DBIx::Custom::QueryBuilder::TagProcessors::lower_than_equal,
21
    'like'   => \&DBIx::Custom::QueryBuilder::TagProcessors::like,
22
    'in'     => \&DBIx::Custom::QueryBuilder::TagProcessors::in,
23
    'insert' => \&DBIx::Custom::QueryBuilder::TagProcessors::insert,
24
    'update' => \&DBIx::Custom::QueryBuilder::TagProcessors::update
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
25
);
26

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

            
30
__PACKAGE__->attr('tag_syntax' => <<'EOS');
31
[tag]                     [expand]
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
32
{? NAME}                  ?
33
{= NAME}                  NAME = ?
34
{<> NAME}                 NAME <> ?
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
35

            
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
36
{< NAME}                  NAME < ?
37
{> NAME}                  NAME > ?
38
{>= NAME}                 NAME >= ?
39
{<= NAME}                 NAME <= ?
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
40

            
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
41
{like NAME}               NAME like ?
42
{in NAME number}          NAME in [?, ?, ..]
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
43

            
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
44
{insert NAME1 NAME2}      (NAME1, NAME2) values (?, ?)
45
{update NAME1 NAME2}      set NAME1 = ?, NAME2 = ?
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
46
EOS
47

            
48
sub register_tag_processor {
49
    my $self = shift;
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
50
    
51
    # Merge
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
52
    my $tag_processors = ref $_[0] eq 'HASH' ? $_[0] : {@_};
53
    $self->tag_processors({%{$self->tag_processors}, %{$tag_processors}});
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
54
    
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
55
    return $self;
56
}
57

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

            
70
sub _parse {
71
    my ($self, $source) = @_;
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;
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
102
                croak qq{Tag "{$tag}" is not registerd.\n\n} .
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
103
                      "<SQL builder syntax>\n" .
104
                      "$tag_syntax\n" .
105
                      "<Your source>\n" .
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
106
                      "$original\n\n";
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
107
            }
108
            
109
            # Check tag arguments
110
            foreach my $tag_arg (@tag_args) {
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
111
            
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
112
                # Cannot cantain placehosder '?'
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
113
                croak qq{Tag cannot contain "?"}
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
114
                  if $tag_arg =~ /\?/;
115
            }
116
            
117
            # Add tag to parsing tree
118
            push @$tree, {type => 'tag', tag_name => $tag_name, tag_args => [@tag_args]};
119
        }
120
    }
121
    
122
    # Add text to parsing tree 
123
    push @$tree, {type => 'text', tag_args => [$source]} if $source;
124
    
125
    return $tree;
126
}
127

            
128
sub _build_query {
129
    my ($self, $tree) = @_;
130
    
131
    # SQL
132
    my $sql = '';
133
    
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
134
    # All Columns
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
135
    my $all_columns = [];
136
    
137
    # Build SQL 
138
    foreach my $node (@$tree) {
139
        
140
        # Get type, tag name, and arguments
141
        my $type     = $node->{type};
142
        my $tag_name = $node->{tag_name};
143
        my $tag_args = $node->{tag_args};
144
        
145
        # Text
146
        if ($type eq 'text') {
147
            # Join text
148
            $sql .= $tag_args->[0];
149
        }
150
        
151
        # Tag
152
        elsif ($type eq 'tag') {
153
            
154
            # Get tag processor
155
            my $tag_processor = $self->tag_processors->{$tag_name};
156
            
157
            # Tag processor is code ref?
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
158
            croak qq{Tag processor "$tag_name" must be code reference}
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
159
              unless ref $tag_processor eq 'CODE';
160
            
161
            # Expand tag using tag processor
changed arguments of tag pro...
yuki-kimoto authored on 2010-08-05
162
            my ($expand, $columns) = @{$tag_processor->(@$tag_args)};
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
163
            
164
            # Check tag processor return value
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
165
            croak qq{Tag processor "$tag_name" must return [\$expand, \$columns]}
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
166
              if !defined $expand || ref $columns ne 'ARRAY';
167
            
168
            # Check placeholder count
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
169
            croak qq{Count of Placeholder must be same as count of columns in "$tag_name"}
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
170
              unless $self->_placeholder_count($expand) eq @$columns;
171
            
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
172
            # Add columns
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
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
    
changed argument of tag proc...
yuki-kimoto authored on 2010-08-03
192
    # Count
193
    $expand ||= '';
renamed default_query_filter...
yuki-kimoto authored on 2010-08-03
194
    my $count = 0;
195
    my $pos   = -1;
196
    while (($pos = index($expand, '?', $pos + 1)) != -1) {
197
        $count++;
198
    }
199
    return $count;
200
}
201

            
202
1;
203

            
204
=head1 NAME
205

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

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

            
217
=head1 ATTRIBUTES
218

            
219
=head2 C<tag_processors>
220

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

            
224
Tag processors.
225

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

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

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

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

            
247
Tag syntax.
248

            
249
=head1 METHODS
250

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

            
254
=head2 C<new>
255

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

            
260
Create a instance.
261

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

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

            
268
B<Example:>
269

            
270
Source:
271

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

            
275
Query:
276

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

            
280
=head2 C<register_tag_processor>
281

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

            
284
Register tag processor.
285

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

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

            
300
=head1 Tags
301

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