root/branches/release-31/lib/MT/ObjectDriver/SQL.pm @ 1502

Revision 1502, 10.7 kB (checked in by fumiakiy, 21 months ago)

First attempt to support freetext index in MT::App::Search. BugId:68481

  • You need to configure "SearchScript mt-ftsearch.cgi" in mt-config.
  • You need to manually create fulltext index mt_entry_ft_index on mt_entry (entry_text, entry_text_more, entry_keywords, entry_title) in the MySQL database (columns are the default search targets).
  • Property svn:keywords set to Id Revision
Line 
1# Movable Type (r) Open Source (C) 2001-2008 Six Apart, Ltd.
2# This program is distributed under the terms of the
3# GNU General Public License, version 2.
4#
5# $Id$
6
7package MT::ObjectDriver::SQL;
8
9#--------------------------------------#
10# Dependencies
11
12use strict;
13use warnings;
14
15use base qw( Data::ObjectDriver::SQL );
16
17#--------------------------------------#
18# Class Accessors
19
20my @ACCESSORS = qw( transform range range_incl lob_columns date_columns not null not_null like distinct from_stmt binary );
21__PACKAGE__->mk_accessors(@ACCESSORS);
22
23#--------------------------------------#
24# Class Methods
25
26sub new {
27    my $class = shift;
28    my %param = @_;
29
30    my %data;
31    @data{@ACCESSORS} = delete @param{@ACCESSORS};
32
33    my $stmt = $class->SUPER::new(%param);
34
35    for my $field (@ACCESSORS) {
36        next if $field eq 'distinct';
37        next if $field eq 'from_stmt';
38        $stmt->$field(defined $data{$field} ? { %{ $data{$field} } } : {});
39    }
40    $stmt->distinct($data{distinct} || 0);
41    if(defined $data{from_stmt}) {
42        $stmt->from_stmt($data{from_stmt});
43    }
44
45    $stmt;
46}
47
48sub ts2db {
49    return unless $_[0];
50    if($_[0] =~ m{ \A \d{4} - }xms) {
51        return $_[0];
52    }
53    my $ret = sprintf '%04d-%02d-%02d %02d:%02d:%02d', unpack 'A4A2A2A2A2A2', $_[0];
54    return $ret;
55}
56
57sub distinct_stmt {
58    my $class = shift;
59    my ($stmt) = @_;
60    $stmt;
61}
62
63# This method will be used in Postgres and MSSQLServer
64sub _subselect_distinct {
65    my $class = shift;
66    my ($stmt) = @_;
67    ## If we're doing a SELECT DISTINCT, postgres would have us include
68    ## the order field, which means the DISTINCT isn't what we want--so
69    ## let's do a subselect.
70    my $subselect = $class->new;
71    $subselect->from_stmt($stmt);
72    $subselect->select([ @{ $stmt->select } ]);
73    #for my $col (@{ $subselect->select }) {
74    #    $col = $driver->dbd->fix_subselect_column($col); ## FIXME
75    #}
76    $subselect->select_map({ %{ $stmt->select_map } });
77    for my $col (keys %{ $subselect->select_map }) {
78        my $new_col = $col;
79        #$new_col = $driver->dbd->fix_subselect_column($new_col); ## FIXME
80        $subselect->select_map->{$new_col} = delete $subselect->select_map->{$col};
81    }
82    $subselect->bind      ([ @{ $stmt->bind } ]);
83    $subselect->distinct  (1);
84
85    $stmt->distinct(0);
86    $subselect;
87}
88
89
90#--------------------------------------#
91# Instance Methods
92
93sub as_sql {
94    my $stmt = shift;
95    my $sql = '';
96
97    my $old_sel;
98    if (@{ $stmt->select }) {
99        $old_sel = $stmt->select;
100
101        $sql = 'SELECT ';
102        if($stmt->distinct) {
103            $sql .= 'DISTINCT ';
104        }
105        $sql .= join(', ', @{ $stmt->select }) . "\n";
106        $stmt->select([]);
107    }
108
109    if ($stmt->from_stmt) {
110        $sql .= 'FROM ('
111            . $stmt->from_stmt->as_sql(@_)
112            . ") t\n";  # t is the subquery alias
113    } else {
114        $sql .= $stmt->SUPER::as_sql(@_);
115
116        ## Check if we generated an unbounded query for mt_session, since we're seeing those in production.
117        ## TODO: remove this. Or generalize it into query auditing.
118        ## my @from_tbls = @{ $stmt->from };
119        ## if (1 == scalar @from_tbls && $from_tbls[0] eq 'mt_session') {
120        ##     if (!$stmt->where || !@{ $stmt->where } || $sql !~ m{ where }xmsi) {
121        ##         MT->log({
122        ##             message => Carp::longmess("Generated unbounded query on mt_session [$sql]"),
123        ##             level => MT::Log::DEBUG()
124        ##         });
125        ##     }
126        ## }
127    }
128
129    $stmt->select($old_sel) if $old_sel;
130    return $sql;
131}
132
133sub _mk_term {
134    my $stmt = shift;
135    my ($col, $val) = @_;
136
137    $col =~ s/ \A [\w\.]+? \. //x;
138
139    ## Any last-minute property -> field name manipulation
140    if (my $m = $stmt->column_mutator) {
141        $col = $m->($col);
142    }
143
144    if (ref $val eq 'HASH') {
145        if (!exists $val->{op}) {
146            # hash-style value, containing hints on operation
147            if (exists $val->{like}) {
148                $val = { op => 'LIKE', value => $val->{like} };
149            }
150            if (exists $val->{not_like}) {
151                $val = { op => 'NOT LIKE', value => $val->{not_like} };
152            }
153            elsif (exists $val->{not_null}) {
154                $val = \'is not null';
155            }
156            elsif (exists $val->{not}) {
157                my $v = $val->{not};
158                if ('ARRAY' eq ref($v)) {
159                    my $v = 'NOT IN (' . join(',', @$v) . ')';
160                    $val = \$v;
161                } elsif (ref $v) {
162                    die "Unsupported value in 'not' column";
163                } else {
164                    $val = { value => $v,
165                             op    => '!=' };
166                }
167            }
168            elsif (exists $val->{between}) {
169                my $low = @{$val->{between}}[0];
170                my $high = @{$val->{between}}[1];
171                if($stmt->date_columns->{$col}) {
172                    $low = ts2db($low);
173                    $high = ts2db($high);
174                }
175                $val = [ '-and', { op => '>=', value => $low },
176                    { op => '<=', value => $high } ];
177            }
178            elsif (exists $val->{'>='}) {
179                $val = { op => '>=', value => $val->{'>='} };
180            }
181            elsif (exists $val->{'>'}) {
182                $val = { op => '>', value => $val->{'>'} };
183            }
184            elsif (exists $val->{'<='}) {
185                $val = { op => '<=', value => $val->{'<='} };
186            }
187            elsif (exists $val->{'<'}) {
188                $val = { op => '<', value => $val->{'<'} };
189            }
190            elsif (exists $val->{'!='}) {
191                $val = { op => '!=', value => $val->{'!='} };
192            }
193        }
194
195        ## Translate dates from app to database format.
196        if(($stmt->date_columns->{$col}) && (ref($val) eq 'HASH')) {
197            my $v = $val->{value};
198            if (ref($v) eq 'ARRAY') {
199                $v->[$_] = ts2db($v->[$_]) for @$v;
200            }
201            else {
202                $val->{value} = ts2db($v);
203            }
204        }
205    }
206    else {
207        ## Rearrange the value into an inclusive range.
208        my $range_incl = $stmt->range_incl;
209        my $range      = $stmt->range;
210
211        ## We may recurse, so let us empty range inclusions in our scope.
212        local $range_incl->{$col} = $range_incl->{$col};
213        local $range->{$col}      = $range->{$col};
214        if ($range_incl->{$col} || $range->{$col}) {
215            my ($lt, $gt) = $range_incl->{$col} ? ('<=', '>=') : ('<', '>');
216            my @vals;
217
218            my ($first_val, $last_val) = @$val;
219            if ($stmt->date_columns->{$col}) {
220                $first_val = ts2db($first_val) if defined $first_val;
221                $last_val = ts2db($last_val) if defined $last_val;
222            }
223
224            ## Ignore first value if it's undef (right-bounded range, eg [undef, 20050101000000] )
225            if (defined $first_val) {
226                push @vals, { op => $gt, value => $first_val };
227            }
228            ## Ignore last value if it's defined (left-bounded range, eg [20050101000000] )
229            if (defined $last_val) {
230                push @vals, { op => $lt, value => $last_val  };
231            }
232            if (2 == scalar @vals) {
233                $val = [ '-and', @vals ];
234            }
235            else {
236                ($val) = @vals;
237            }
238
239            ## Because the new value is an arrayref, we're about to get
240            ## called recursively with each of those hashrefs inside it.
241            ## So ignore that we're using an inclusive range within this
242            ## call's scope.
243            undef ($range_incl->{$col} ? $range_incl->{$col} : $range->{$col});
244        }
245
246        ## Translate dates from app to database format.
247        if ($stmt->date_columns->{$col}) {
248            if (ref($val) eq 'HASH') {
249                my $v = $val->{value};
250                if (ref($v) eq 'ARRAY') {
251                    $v->[$_] = ts2db($v->[$_]) for @$v;
252                }
253                else {
254                    $val->{value} = ts2db($v);
255                }
256            } elsif (!ref($val)) {
257                $val = ts2db($val);
258            }
259        }
260
261        if ($stmt->not->{$col}) {
262            if ('ARRAY' eq ref($val)) {
263                my $v = 'NOT IN (' . join(',', @$val) . ')';
264                $val = \$v;
265            }
266            elsif (ref $val) {
267                die "Unsupported value in 'not' column";
268            }
269            else {
270                $val = { value => $val,
271                         op    => '!=', };
272            }
273        }
274
275        if ($stmt->null->{$col}) {
276            $val = \'is null';
277        }
278
279        if ($stmt->not_null->{$col}) {
280            $val = \'is not null';
281        }
282
283        if ($stmt->like->{$col}) {
284            if (ref($val) eq 'HASH') {
285                $val->{op} = 'LIKE';
286            } elsif (!ref($val)) {
287                $val = { op    => 'LIKE',
288                         value => $val,   };
289            }
290        }
291    }
292
293    ## Transformation modifies the column name, so it should be last.
294    if(my $transformed_column = $stmt->transform->{$col}) {
295        $col = $transformed_column;
296    }
297
298    ## Prevent D::OD from re-mutating, since we've done it here
299    local $stmt->{column_mutator} = undef;
300
301    $stmt->SUPER::_mk_term($col, $val);
302}
303
304sub make_subselect {
305    my $stmt = shift;
306    my $class = ref $stmt;
307
308    my $subselect = $class->new();
309    for my $field (qw( bind distinct )) {
310        $subselect->$field($stmt->$field());
311    }
312
313    my @new_selects = map { s{ \A \w+\. }{}xms } @{ $stmt->select };
314    $subselect->select(\@new_selects);
315
316    my %new_select_map;
317    my $sel_map = $stmt->select_map;
318    for my $select_field (keys %$sel_map) {
319        my $new_select_field = $select_field;
320        $new_select_field =~ s{ \A \w+\. }{}xms;
321        $new_select_map{$new_select_field} = $sel_map->{$select_field};
322    }
323
324    $subselect->from_stmt($stmt);
325    return $subselect;
326}
327
328sub field_decorator {
329    my $stmt = shift;
330    my ($class) = @_;
331    return sub {
332        my($term) = @_;
333        my $field_prefix = $class->datasource;
334        for my $col (@{ $class->column_names }) {
335            $term =~ s/\b$col\b/${field_prefix}_$col/g;
336        }
337        return $term;
338    };
339}
340
341sub as_limit {
342    my $stmt = shift;
343    my $n = $stmt->limit;
344    # Support offset without limit
345    my $o = $stmt->offset || 0;
346    $n = 2147483647 if !$n && $o;
347    return '' unless $n;
348    die "Non-numerics in limit/offset clause ($n, $o)" if ($n =~ /\D/) || ($o =~ /\D/);
349    return sprintf "LIMIT %d%s\n", $n,
350           ($o ? " OFFSET " . int($o) : "");
351}
352
353sub add_freetext_where { 0 }
354
3551;
356__END__
357
358=head1 NAME
359
360MT::ObjectDriver::SQL
361
362=head1 METHODS
363
364TODO
365
366=head1 AUTHOR & COPYRIGHT
367
368Please see L<MT/AUTHOR & COPYRIGHT>.
369
370=cut
Note: See TracBrowser for help on using the browser.