riddle 1.5.7 → 1.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/HISTORY +4 -0
- data/lib/riddle/query.rb +15 -10
- data/lib/riddle/query/insert.rb +2 -2
- data/lib/riddle/query/select.rb +21 -12
- data/riddle.gemspec +1 -1
- data/spec/functional/escaping_spec.rb +49 -0
- data/spec/riddle/query/insert_spec.rb +4 -4
- data/spec/riddle/query/select_spec.rb +31 -16
- data/spec/riddle/query_spec.rb +2 -6
- metadata +6 -4
data/HISTORY
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
1.5.8 - August 26th 2013
|
2
|
+
- Reworked escaping to be consistent and always query-safe (Demian Ferreiro).
|
3
|
+
- Escape column names in SphinxQL WHERE, INSERT, ORDER BY and GROUP BY clauses and statements (Jason Rust).
|
4
|
+
|
1
5
|
1.5.7 - July 9th 2013
|
2
6
|
- Respect Riddle::OutOfBoundsError instances, instead of wrapping them in ResponseError.
|
3
7
|
- Handle boolean values for snippets options.
|
data/lib/riddle/query.rb
CHANGED
@@ -58,22 +58,21 @@ module Riddle::Query
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def self.snippets(data, index, query, options = nil)
|
61
|
-
data = data
|
62
|
-
query = query.gsub("'") { |x| "\\'" }
|
61
|
+
data, index, query = quote(data), quote(index), quote(query)
|
63
62
|
|
64
63
|
options = ', ' + options.keys.collect { |key|
|
65
64
|
value = translate_value options[key]
|
66
|
-
value =
|
65
|
+
value = quote value if value.is_a?(String)
|
67
66
|
|
68
67
|
"#{value} AS #{key}"
|
69
68
|
}.join(', ') unless options.nil?
|
70
69
|
|
71
|
-
"CALL SNIPPETS(
|
70
|
+
"CALL SNIPPETS(#{data}, #{index}, #{query}#{options})"
|
72
71
|
end
|
73
72
|
|
74
73
|
def self.create_function(name, type, file)
|
75
74
|
type = type.to_s.upcase
|
76
|
-
"CREATE FUNCTION #{name} RETURNS #{type} SONAME
|
75
|
+
"CREATE FUNCTION #{name} RETURNS #{type} SONAME #{quote file}"
|
77
76
|
end
|
78
77
|
|
79
78
|
def self.drop_function(name)
|
@@ -100,11 +99,17 @@ module Riddle::Query
|
|
100
99
|
end
|
101
100
|
|
102
101
|
def self.escape(string)
|
103
|
-
string.gsub("
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
}
|
102
|
+
string.gsub(/[\(\)\|\-!@~\/"\/\^\$\\]/) { |match| "\\#{match}" }
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.quote(string)
|
106
|
+
"'#{sql_escape string}'"
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.sql_escape(string)
|
110
|
+
return Mysql2::Client.escape(string) if defined?(Mysql2)
|
111
|
+
|
112
|
+
string.gsub(/['"\\]/) { |character| "\\#{character}" }
|
108
113
|
end
|
109
114
|
end
|
110
115
|
|
data/lib/riddle/query/insert.rb
CHANGED
@@ -14,7 +14,7 @@ class Riddle::Query::Insert
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def to_sql
|
17
|
-
"#{command} INTO #{@index} (
|
17
|
+
"#{command} INTO #{@index} (`#{columns_to_s}`) VALUES (#{values_to_s})"
|
18
18
|
end
|
19
19
|
|
20
20
|
private
|
@@ -24,7 +24,7 @@ class Riddle::Query::Insert
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def columns_to_s
|
27
|
-
columns.join('
|
27
|
+
columns.join('`, `')
|
28
28
|
end
|
29
29
|
|
30
30
|
def values_to_s
|
data/lib/riddle/query/select.rb
CHANGED
@@ -26,7 +26,7 @@ class Riddle::Query::Select
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def matching(match)
|
29
|
-
@matching = match
|
29
|
+
@matching = match
|
30
30
|
self
|
31
31
|
end
|
32
32
|
|
@@ -83,11 +83,11 @@ class Riddle::Query::Select
|
|
83
83
|
def to_sql
|
84
84
|
sql = "SELECT #{ @values.join(', ') } FROM #{ @indices.join(', ') }"
|
85
85
|
sql << " WHERE #{ combined_wheres }" if wheres?
|
86
|
-
sql << " GROUP BY #{@group_by}" if !@group_by.nil?
|
86
|
+
sql << " GROUP BY #{escape_column(@group_by)}" if !@group_by.nil?
|
87
87
|
unless @order_within_group_by.nil?
|
88
|
-
sql << " WITHIN GROUP ORDER BY #{@order_within_group_by}"
|
88
|
+
sql << " WITHIN GROUP ORDER BY #{escape_column(@order_within_group_by)}"
|
89
89
|
end
|
90
|
-
sql << " ORDER BY #{@order_by}" if !@order_by.nil?
|
90
|
+
sql << " ORDER BY #{escape_column(@order_by)}" if !@order_by.nil?
|
91
91
|
sql << " #{limit_clause}" unless @limit.nil? && @offset.nil?
|
92
92
|
sql << " #{options_clause}" unless @options.empty?
|
93
93
|
|
@@ -104,9 +104,9 @@ class Riddle::Query::Select
|
|
104
104
|
if @matching.nil?
|
105
105
|
wheres_to_s
|
106
106
|
elsif @wheres.empty? && @where_nots.empty? && @where_alls.empty? && @where_not_alls.empty?
|
107
|
-
"MATCH(
|
107
|
+
"MATCH(#{Riddle::Query.quote @matching})"
|
108
108
|
else
|
109
|
-
"MATCH(
|
109
|
+
"MATCH(#{Riddle::Query.quote @matching}) AND #{wheres_to_s}"
|
110
110
|
end
|
111
111
|
end
|
112
112
|
|
@@ -134,22 +134,22 @@ class Riddle::Query::Select
|
|
134
134
|
def filter_comparison_and_value(attribute, value)
|
135
135
|
case value
|
136
136
|
when Array
|
137
|
-
"#{attribute} IN (#{value.collect { |val| filter_value(val) }.join(', ')})"
|
137
|
+
"#{escape_column(attribute)} IN (#{value.collect { |val| filter_value(val) }.join(', ')})"
|
138
138
|
when Range
|
139
|
-
"#{attribute} BETWEEN #{filter_value(value.first)} AND #{filter_value(value.last)}"
|
139
|
+
"#{escape_column(attribute)} BETWEEN #{filter_value(value.first)} AND #{filter_value(value.last)}"
|
140
140
|
else
|
141
|
-
"#{attribute} = #{filter_value(value)}"
|
141
|
+
"#{escape_column(attribute)} = #{filter_value(value)}"
|
142
142
|
end
|
143
143
|
end
|
144
144
|
|
145
145
|
def exclusive_filter_comparison_and_value(attribute, value)
|
146
146
|
case value
|
147
147
|
when Array
|
148
|
-
"#{attribute} NOT IN (#{value.collect { |val| filter_value(val) }.join(', ')})"
|
148
|
+
"#{escape_column(attribute)} NOT IN (#{value.collect { |val| filter_value(val) }.join(', ')})"
|
149
149
|
when Range
|
150
|
-
"#{attribute} < #{filter_value(value.first)} OR #{attribute} > #{filter_value(value.last)}"
|
150
|
+
"#{escape_column(attribute)} < #{filter_value(value.first)} OR #{attribute} > #{filter_value(value.last)}"
|
151
151
|
else
|
152
|
-
"#{attribute} <> #{filter_value(value)}"
|
152
|
+
"#{escape_column(attribute)} <> #{filter_value(value)}"
|
153
153
|
end
|
154
154
|
end
|
155
155
|
|
@@ -188,4 +188,13 @@ class Riddle::Query::Select
|
|
188
188
|
value
|
189
189
|
end
|
190
190
|
end
|
191
|
+
|
192
|
+
def escape_column(column)
|
193
|
+
if column.to_s =~ /\A[`@]/
|
194
|
+
column
|
195
|
+
else
|
196
|
+
column_name, *extra = column.to_s.split(' ')
|
197
|
+
extra.unshift("`#{column_name}`").compact.join(' ')
|
198
|
+
end
|
199
|
+
end
|
191
200
|
end
|
data/riddle.gemspec
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'SphinxQL escaping', :live => true do
|
4
|
+
let(:connection) { Mysql2::Client.new :host => '127.0.0.1', :port => 9306 }
|
5
|
+
|
6
|
+
def sphinxql_matching(string)
|
7
|
+
select = Riddle::Query::Select.new
|
8
|
+
select.from 'people'
|
9
|
+
select.matching string
|
10
|
+
select.to_sql
|
11
|
+
end
|
12
|
+
|
13
|
+
['@', "'", '"', '\\"', "\\'"].each do |string|
|
14
|
+
it "escapes #{string}" do
|
15
|
+
lambda {
|
16
|
+
connection.query sphinxql_matching(Riddle::Query.escape(string))
|
17
|
+
}.should_not raise_error(Mysql2::Error)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'on snippets' do
|
22
|
+
def snippets_for(text, words = '', options = nil)
|
23
|
+
snippets_query = Riddle::Query.snippets(text, 'people', words, options)
|
24
|
+
connection.query(snippets_query).first['snippet']
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'preserves original text with special SphinxQL escape characters' do
|
28
|
+
text = 'email: john@example.com (yay!)'
|
29
|
+
snippets_for(text).should == text
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'preserves original text with special MySQL escape characters' do
|
33
|
+
text = "'Dear' Susie\nAlways use {\\LaTeX}"
|
34
|
+
snippets_for(text).should == text
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'escapes match delimiters with special SphinxQL escape characters' do
|
38
|
+
snippets = snippets_for('hello world', 'world',
|
39
|
+
:before_match => '()|-!', :after_match => '@~"/^$')
|
40
|
+
snippets.should == 'hello ()|-!world@~"/^$'
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'escapes match delimiters with special MySQL escape characters' do
|
44
|
+
snippets = snippets_for('hello world', 'world',
|
45
|
+
:before_match => "'\"", :after_match => "\n\t\\")
|
46
|
+
snippets.should == "hello '\"world\n\t\\"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end unless RUBY_PLATFORM == 'java' || Riddle.loaded_version.to_i < 2
|
@@ -3,23 +3,23 @@ require 'spec_helper'
|
|
3
3
|
describe Riddle::Query::Insert do
|
4
4
|
it 'handles inserts' do
|
5
5
|
query = Riddle::Query::Insert.new('foo_core', [:id, :deleted], [4, false])
|
6
|
-
query.to_sql.should == 'INSERT INTO foo_core (id
|
6
|
+
query.to_sql.should == 'INSERT INTO foo_core (`id`, `deleted`) VALUES (4, 0)'
|
7
7
|
end
|
8
8
|
|
9
9
|
it 'handles replaces' do
|
10
10
|
query = Riddle::Query::Insert.new('foo_core', [:id, :deleted], [4, false])
|
11
11
|
query.replace!
|
12
|
-
query.to_sql.should == 'REPLACE INTO foo_core (id
|
12
|
+
query.to_sql.should == 'REPLACE INTO foo_core (`id`, `deleted`) VALUES (4, 0)'
|
13
13
|
end
|
14
14
|
|
15
15
|
it 'encloses strings in single quotes' do
|
16
16
|
query = Riddle::Query::Insert.new('foo_core', [:id, :name], [4, 'bar'])
|
17
|
-
query.to_sql.should == "INSERT INTO foo_core (id
|
17
|
+
query.to_sql.should == "INSERT INTO foo_core (`id`, `name`) VALUES (4, 'bar')"
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'handles inserts with more than one set of values' do
|
21
21
|
query = Riddle::Query::Insert.new 'foo_core', [:id, :name], [[4, 'bar'], [5, 'baz']]
|
22
22
|
query.to_sql.
|
23
|
-
should == "INSERT INTO foo_core (id
|
23
|
+
should == "INSERT INTO foo_core (`id`, `name`) VALUES (4, 'bar'), (5, 'baz')"
|
24
24
|
end
|
25
25
|
end
|
@@ -34,84 +34,99 @@ describe Riddle::Query::Select do
|
|
34
34
|
|
35
35
|
it 'handles filters with integers' do
|
36
36
|
query.from('foo_core').matching('foo').where(:bar_id => 10).to_sql.
|
37
|
-
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bar_id = 10"
|
37
|
+
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bar_id` = 10"
|
38
38
|
end
|
39
39
|
|
40
40
|
it "handles exclusive filters with integers" do
|
41
41
|
query.from('foo_core').matching('foo').where_not(:bar_id => 10).to_sql.
|
42
|
-
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bar_id <> 10"
|
42
|
+
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bar_id` <> 10"
|
43
43
|
end
|
44
44
|
|
45
45
|
it "handles filters with true" do
|
46
46
|
query.from('foo_core').matching('foo').where(:bar => true).to_sql.
|
47
|
-
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bar = 1"
|
47
|
+
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bar` = 1"
|
48
48
|
end
|
49
49
|
|
50
50
|
it "handles exclusive filters with true" do
|
51
51
|
query.from('foo_core').matching('foo').where_not(:bar => true).to_sql.
|
52
|
-
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bar <> 1"
|
52
|
+
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bar` <> 1"
|
53
53
|
end
|
54
54
|
|
55
55
|
it "handles filters with false" do
|
56
56
|
query.from('foo_core').matching('foo').where(:bar => false).to_sql.
|
57
|
-
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bar = 0"
|
57
|
+
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bar` = 0"
|
58
58
|
end
|
59
59
|
|
60
60
|
it "handles exclusive filters with false" do
|
61
61
|
query.from('foo_core').matching('foo').where_not(:bar => false).to_sql.
|
62
|
-
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bar <> 0"
|
62
|
+
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bar` <> 0"
|
63
63
|
end
|
64
64
|
|
65
65
|
it "handles filters with arrays" do
|
66
66
|
query.from('foo_core').matching('foo').where(:bars => [1, 2]).to_sql.
|
67
|
-
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bars IN (1, 2)"
|
67
|
+
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bars` IN (1, 2)"
|
68
68
|
end
|
69
69
|
|
70
70
|
it "handles exclusive filters with arrays" do
|
71
71
|
query.from('foo_core').matching('foo').where_not(:bars => [1, 2]).to_sql.
|
72
|
-
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bars NOT IN (1, 2)"
|
72
|
+
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bars` NOT IN (1, 2)"
|
73
73
|
end
|
74
74
|
|
75
75
|
it "handles filters with timestamps" do
|
76
76
|
time = Time.now
|
77
77
|
query.from('foo_core').matching('foo').where(:created_at => time).to_sql.
|
78
|
-
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND created_at = #{time.to_i}"
|
78
|
+
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `created_at` = #{time.to_i}"
|
79
79
|
end
|
80
80
|
|
81
81
|
it "handles exclusive filters with timestamps" do
|
82
82
|
time = Time.now
|
83
83
|
query.from('foo_core').matching('foo').where_not(:created_at => time).
|
84
|
-
to_sql.should == "SELECT * FROM foo_core WHERE MATCH('foo') AND created_at <> #{time.to_i}"
|
84
|
+
to_sql.should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `created_at` <> #{time.to_i}"
|
85
85
|
end
|
86
86
|
|
87
87
|
it "handles filters with ranges" do
|
88
88
|
query.from('foo_core').matching('foo').where(:bar => 1..5).to_sql.
|
89
|
-
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND bar BETWEEN 1 AND 5"
|
89
|
+
should == "SELECT * FROM foo_core WHERE MATCH('foo') AND `bar` BETWEEN 1 AND 5"
|
90
90
|
end
|
91
91
|
|
92
92
|
it "handles filters expecting matches on all values" do
|
93
93
|
query.from('foo_core').where_all(:bars => [1, 2]).to_sql.
|
94
|
-
should == "SELECT * FROM foo_core WHERE bars = 1 AND bars = 2"
|
94
|
+
should == "SELECT * FROM foo_core WHERE `bars` = 1 AND `bars` = 2"
|
95
95
|
end
|
96
96
|
|
97
97
|
it "handles exclusive filters expecting matches on none of the values" do
|
98
98
|
query.from('foo_core').where_not_all(:bars => [1, 2]).to_sql.
|
99
|
-
should == "SELECT * FROM foo_core WHERE (bars <> 1 OR bars <> 2)"
|
99
|
+
should == "SELECT * FROM foo_core WHERE (`bars` <> 1 OR `bars` <> 2)"
|
100
100
|
end
|
101
101
|
|
102
102
|
it 'handles grouping' do
|
103
103
|
query.from('foo_core').group_by('bar_id').to_sql.
|
104
|
-
should == "SELECT * FROM foo_core GROUP BY bar_id"
|
104
|
+
should == "SELECT * FROM foo_core GROUP BY `bar_id`"
|
105
105
|
end
|
106
106
|
|
107
107
|
it 'handles ordering' do
|
108
108
|
query.from('foo_core').order_by('bar_id ASC').to_sql.
|
109
|
-
should == 'SELECT * FROM foo_core ORDER BY bar_id ASC'
|
109
|
+
should == 'SELECT * FROM foo_core ORDER BY `bar_id` ASC'
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'handles ordering when an already escaped column is passed in' do
|
113
|
+
query.from('foo_core').order_by('`bar_id` ASC').to_sql.
|
114
|
+
should == 'SELECT * FROM foo_core ORDER BY `bar_id` ASC'
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'handles ordering when just a symbol is passed in' do
|
118
|
+
query.from('foo_core').order_by(:bar_id).to_sql.
|
119
|
+
should == 'SELECT * FROM foo_core ORDER BY `bar_id`'
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'handles ordering when a computed sphinx variable is passed in' do
|
123
|
+
query.from('foo_core').order_by('@weight DESC').to_sql.
|
124
|
+
should == 'SELECT * FROM foo_core ORDER BY @weight DESC'
|
110
125
|
end
|
111
126
|
|
112
127
|
it 'handles group ordering' do
|
113
128
|
query.from('foo_core').order_within_group_by('bar_id ASC').to_sql.
|
114
|
-
should == 'SELECT * FROM foo_core WITHIN GROUP ORDER BY bar_id ASC'
|
129
|
+
should == 'SELECT * FROM foo_core WITHIN GROUP ORDER BY `bar_id` ASC'
|
115
130
|
end
|
116
131
|
|
117
132
|
it 'handles a limit' do
|
data/spec/riddle/query_spec.rb
CHANGED
@@ -78,14 +78,10 @@ describe Riddle::Query do
|
|
78
78
|
end
|
79
79
|
|
80
80
|
describe '.escape' do
|
81
|
-
%w(( ) | - ! @ ~
|
81
|
+
%w(( ) | - ! @ ~ / ^ $ ").each do |reserved|
|
82
82
|
it "escapes #{reserved}" do
|
83
|
-
Riddle::Query.escape(reserved).should == "
|
83
|
+
Riddle::Query.escape(reserved).should == "\\#{reserved}"
|
84
84
|
end
|
85
85
|
end
|
86
|
-
|
87
|
-
it "escapes \\" do
|
88
|
-
Riddle::Query.escape("\\").should == "\\\\"
|
89
|
-
end
|
90
86
|
end
|
91
87
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: riddle
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
8
|
- 5
|
9
|
-
-
|
10
|
-
version: 1.5.
|
9
|
+
- 8
|
10
|
+
version: 1.5.8
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Pat Allan
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2013-
|
18
|
+
date: 2013-08-26 00:00:00 +10:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -244,6 +244,7 @@ files:
|
|
244
244
|
- spec/fixtures/sql/data.tsv
|
245
245
|
- spec/fixtures/sql/structure.sql
|
246
246
|
- spec/functional/connection_spec.rb
|
247
|
+
- spec/functional/escaping_spec.rb
|
247
248
|
- spec/functional/excerpt_spec.rb
|
248
249
|
- spec/functional/keywords_spec.rb
|
249
250
|
- spec/functional/persistance_spec.rb
|
@@ -437,6 +438,7 @@ test_files:
|
|
437
438
|
- spec/fixtures/sql/data.tsv
|
438
439
|
- spec/fixtures/sql/structure.sql
|
439
440
|
- spec/functional/connection_spec.rb
|
441
|
+
- spec/functional/escaping_spec.rb
|
440
442
|
- spec/functional/excerpt_spec.rb
|
441
443
|
- spec/functional/keywords_spec.rb
|
442
444
|
- spec/functional/persistance_spec.rb
|