File Coverage

blib/lib/Catmandu/Store/Solr/CQL.pm
Criterion Covered Total %
statement 9 80 11.2
branch 0 50 0.0
condition 0 12 0.0
subroutine 3 7 42.8
pod 2 4 50.0
total 14 153 9.1


line stmt bran cond sub pod time code
1             package Catmandu::Store::Solr::CQL; #TODO see Catmandu::Store::ElasticSearch::CQL
2              
3 1     1   6 use strict;
  1         3  
  1         46  
4 1     1   7 use warnings;
  1         2  
  1         35  
5 1     1   672 use CQL::Parser;
  1         22034  
  1         916  
6              
7             my $any_field = qr'^(srw|cql)\.(serverChoice|anywhere)$'i;
8             my $match_all = qr'^(srw|cql)\.allRecords$'i;
9             my $distance_modifier = qr'\s*\/\s*distance\s*<\s*(\d+)'i;
10             my $reserved = qr'[\+\-\&\|\!\(\)\{\}\[\]\^\"\~\*\?\:\\]';
11              
12             my $parser;
13              
14             sub parse {
15 0     0 1   my ($self, $query) = @_;
16 0   0       $parser ||= CQL::Parser->new;
17 0           $self->visit($parser->parse($query));
18             }
19              
20             sub escape_term {
21 0     0 0   my $term = $_[0];
22 0           $term =~ s/($reserved)/\\$1/g;
23 0           $term;
24             }
25              
26             sub quote_term {
27 0     0 0   my $term = $_[0];
28 0 0         $term = qq("$term") if $term =~ /\s/;
29 0           $term;
30             }
31              
32             sub visit {
33 0     0 1   my ($self, $node) = @_;
34              
35 0 0         if ($node->isa('CQL::TermNode')) {
36 0           my $term = escape_term($node->getTerm);
37              
38 0 0         if ($term =~ $match_all) {
39 0           return "*:*";
40             }
41              
42 0           my $qualifier = $node->getQualifier;
43 0           my $relation = $node->getRelation;
44 0           my @modifiers = $relation->getModifiers;
45 0           my $base = lc $relation->getBase;
46              
47 0 0         if ($qualifier =~ $any_field) {
48 0           $qualifier = "";
49             } else {
50 0           $qualifier = "$qualifier:";
51             }
52              
53 0 0 0       if ($base eq '=' or $base eq 'scr') {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
54 0           $term = quote_term($term);
55 0           for my $m (@modifiers) {
56 0 0         if ($m->[1] eq 'fuzzy') {
57 0           return "$qualifier$term~";
58             }
59             }
60 0           return $qualifier.$term;
61             } elsif ($base eq '<') {
62 0           $term = quote_term($term);
63 0           return $qualifier."{* TO $term}";
64             } elsif ($base eq '>') {
65 0           $term = quote_term($term);
66 0           return $qualifier."{$term TO *}";
67             } elsif ($base eq '<=') {
68 0           $term = quote_term($term);
69 0           return $qualifier."[* TO $term]";
70             } elsif ($base eq '>=') {
71 0           $term = quote_term($term);
72 0           return $qualifier."[$term TO *]";
73             } elsif ($base eq '<>') {
74 0           $term = quote_term($term);
75 0           return "-$qualifier$term";
76             } elsif ($base eq 'exact') {
77 0           return $qualifier.quote_term($term);
78             } elsif ($base eq 'all') {
79 0           my @terms = split /\s+/, $term;
80 0 0         if (@terms == 1) {
81 0           return $qualifier.$term;
82             }
83 0           $term = join ' ', map { "+$_" } @terms;
  0            
84 0 0         if ($qualifier) {
85 0           return "$qualifier($term)";
86             }
87 0           return $term;
88             } elsif ($base eq 'any') {
89 0           $term = join ' OR ', map { $qualifier.$_ } split /\s+/, $term;
  0            
90 0           return "($term)";
91             } elsif ($base eq 'within') {
92 0           my @range = split /\s+/, $term;
93 0 0         if (@range == 1) {
94 0           return $qualifier.$term;
95             } else {
96 0           return $qualifier."[$range[0] TO $range[1]]";
97             }
98             } else {
99 0           return $qualifier.quote_term($term);
100             }
101             }
102              
103 0 0         if ($node->isa('CQL::ProxNode')) {
104 0           my $distance = 1;
105 0           my $qualifier = $node->left->getQualifier;
106 0           my $term = escape_term(join(' ', $node->left->getTerm, $node->right->getTerm));
107              
108 0 0         if (my ($n) = $node->op =~ $distance_modifier) {
109 0 0         $distance = $n if $n > 1;
110             }
111              
112 0 0         if ($qualifier =~ $any_field) {
113 0           return qq("$term"~$distance);
114             } else {
115 0           return qq($qualifier:"$term"~$distance);
116             }
117             }
118              
119 0 0         if ($node->isa('CQL::BooleanNode')) {
120 0           my $lft = $node->left;
121 0           my $rgt = $node->right;
122 0           my $lft_q = $self->visit($lft);
123 0           my $rgt_q = $self->visit($rgt);
124 0 0 0       $lft_q = "($lft_q)" unless $lft->isa('CQL::TermNode') || $lft->isa('CQL::ProxNode');
125 0 0 0       $rgt_q = "($rgt_q)" unless $rgt->isa('CQL::TermNode') || $rgt->isa('CQL::ProxNode');
126              
127 0           return join ' ', $lft_q, uc $node->op, $rgt_q;
128             }
129             }
130              
131             1;
132              
133             =head1 NAME
134              
135             Catmandu::Store::Solr::CQL - Converts a CQL query string to a Solr query string
136              
137             =head1 SYNOPSIS
138              
139             $solr_query_string = Catmandu::Store::Solr::CQL->parse($cql_query_string);
140              
141             =head1 DESCRIPTION
142              
143             This package currently parses most of CQL 1.1:
144              
145             and
146             or
147             not
148             prox
149             prox/distance<$n
150             srw.allRecords
151             srw.serverChoice
152             srw.anywhere
153             cql.allRecords
154             cql.serverChoice
155             cql.anywhere
156             =
157             scr
158             =/fuzzy
159             scr/fuzzy
160             <
161             >
162             <=
163             >=
164             <>
165             exact
166             all
167             any
168             within
169              
170             =head1 METHODS
171              
172             =head2 parse
173              
174             Parses the given CQL query string with L and converts it to a Solr query string.
175              
176             =head2 visit
177              
178             Converts the given L to a Solr query string.
179              
180             =head1 TODO
181              
182             support cql 1.2, more modifiers (esp. masked), sortBy, encloses
183              
184             =head1 SEE ALSO
185              
186             L.
187              
188             =cut