diff --git a/Gemfile b/Gemfile
index 2283ebe..42f51f3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -23,6 +23,6 @@ group :development do
gem 'rubocop', '~> 0.54', require: false
gem 'rubocop-rspec', '~> 1.23', require: false
gem 'simplecov', '~> 0.16', require: false
- gem "rexical", '~> 1.0', require: false
- gem "racc", '~> 1.4', require: false
+ gem 'rexical', '~> 1.0', require: false
+ gem 'racc', '~> 1.4', require: false
end
diff --git a/coverage/.last_run.json b/coverage/.last_run.json
index ec12727..8afec4c 100644
--- a/coverage/.last_run.json
+++ b/coverage/.last_run.json
@@ -1,5 +1,5 @@
{
"result": {
- "covered_percent": 66.92
+ "covered_percent": 78.1
}
}
diff --git a/coverage/.resultset.json b/coverage/.resultset.json
index ef5ab35..cb0bf64 100644
--- a/coverage/.resultset.json
+++ b/coverage/.resultset.json
@@ -9,69 +9,73 @@
1,
1,
null,
- 0,
- 0,
+ 1,
+ 1,
null,
- 0,
+ 1,
+ 13,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 13,
+ 13,
+ 13,
+ 13,
+ 13,
+ 13,
+ 13,
+ 13,
+ 13,
+ 13,
+ null,
+ null,
+ 1,
+ 13,
+ 13,
+ 24,
+ 23,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ 24,
+ null,
+ 24,
+ null,
+ null,
+ 1,
0,
null,
null,
+ 1,
+ 16,
null,
null,
+ 1,
+ 13,
null,
- 0,
+ null,
+ 1,
0,
null,
null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- 0,
- null,
- null,
- 0,
- 0,
- 0,
- null,
- null,
- null,
- 0,
- 0,
- 0,
- null,
- null,
- 0,
- 0,
- null,
- null,
- 0,
- 0,
- null,
- null,
- 0,
- 0,
- null,
- null,
- 0,
- 0,
- null,
- null,
- 0,
+ 1,
0,
0,
0,
@@ -181,57 +185,121 @@
null
],
"/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/text_to_sql_query.rb": [
+ 1,
null,
+ 1,
+ 1,
+ 44,
+ 44,
+ 44,
+ 44,
+ 114,
+ 114,
+ 114,
null,
+ 44,
+ 13,
null,
null,
null,
+ 1,
+ 44,
+ 44,
+ 44,
null,
null,
+ 1,
null,
+ 1,
+ 128,
+ 128,
+ 128,
null,
+ 23,
+ 23,
null,
+ 22,
null,
+ 15,
null,
+ 10,
null,
+ 10,
+ 0,
null,
null,
+ 10,
+ 10,
null,
+ 10,
null,
null,
null,
+ 58,
+ 58,
+ 58,
+ 2,
null,
+ 56,
null,
null,
null,
null,
+ 1,
+ 37,
+ 0,
null,
null,
+ 37,
+ 37,
null,
+ 37,
+ 0,
null,
null,
+ 37,
+ 0,
null,
null,
+ 37,
+ 37,
null,
+ 37,
+ 37,
null,
+ 37,
null,
null,
+ 1,
+ 81,
+ 81,
+ 81,
+ 81,
+ 81,
null,
+ null
+ ],
+ "/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/parser.rb": [
null,
null,
null,
null,
null,
null,
+ 1,
null,
+ 1,
null,
+ 1,
null,
+ 1,
null,
null,
null,
null,
null,
null,
+ 1,
null,
null,
null,
@@ -240,6 +308,7 @@
null,
null,
null,
+ 1,
null,
null,
null,
@@ -248,22 +317,29 @@
null,
null,
null,
+ 1,
null,
null,
null,
+ 1,
null,
null,
null,
+ 1,
null,
null,
null,
+ 1,
null,
null,
null,
+ 1,
null,
null,
+ 1,
null,
null,
+ 1,
null,
null,
null,
@@ -273,9 +349,255 @@
null,
null,
null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ 1,
+ null,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ 1,
+ null,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ 0,
+ null,
+ null,
+ null
+ ],
+ "/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/lexer.rb": [
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 1,
+ 1,
+ 1,
+ null,
+ 1,
+ null,
+ 1,
+ 1,
+ 1,
+ null,
+ 1,
+ 132,
+ 132,
+ 132,
+ null,
+ null,
+ 1,
+ 702,
+ null,
+ null,
+ 1,
+ 94,
+ 94,
+ null,
+ 1,
+ null,
+ 1,
+ 0,
+ 0,
+ 0,
+ null,
+ null,
+ null,
+ 1,
+ 0,
+ 0,
+ null,
+ null,
+ null,
+ 1,
+ 834,
+ null,
+ null,
+ 703,
+ 703,
+ null,
+ null,
+ 1,
+ 939,
+ 939,
+ 939,
+ null,
+ null,
+ 939,
+ null,
+ null,
+ 702,
+ 106,
+ null,
+ 649,
+ 106,
+ null,
+ 596,
+ 84,
+ null,
+ 554,
+ 84,
+ null,
+ 512,
+ 46,
+ null,
+ 489,
+ 132,
+ null,
+ 423,
+ 610,
+ null,
+ 118,
+ 236,
+ null,
+ null,
+ null,
+ 0,
+ 0,
+ 939,
+ null,
+ null,
+ 0,
+ null,
+ 237,
+ null,
+ null,
+ 1,
+ 38,
+ 38,
+ 38,
+ 174,
+ null,
+ 38,
+ null,
null
]
},
- "timestamp": 1580827025
+ "timestamp": 1583928139
}
}
diff --git a/coverage/index.html b/coverage/index.html
index cb2a0c4..5828793 100644
--- a/coverage/index.html
+++ b/coverage/index.html
@@ -14,27 +14,27 @@
-
Generated
2020-02-04T15:37:05+01:00
+
Generated
2020-03-11T13:02:19+01:00
All Files
- (24.18%
+ (78.1%
covered at
-
- 0.22
+
+ 69.62
hits/line)
- 3 files in total.
- 91 relevant lines.
- 22 lines covered and
- 69 lines missed
+ 5 files in total.
+ 242 relevant lines.
+ 189 lines covered and
+ 53 lines missed
@@ -50,24 +50,44 @@
+
+ | lib/lexer.rb |
+ 86.67 % |
+ 107 |
+ 60 |
+ 52 |
+ 8 |
+ 237.5 |
+
+
+
+ | lib/parser.rb |
+ 96.97 % |
+ 207 |
+ 33 |
+ 32 |
+ 1 |
+ 1.0 |
+
+
| lib/pg_searchable_regex.rb |
- 10.53 % |
- 76 |
- 38 |
- 4 |
- 34 |
- 0.1 |
+ 87.5 % |
+ 80 |
+ 40 |
+ 35 |
+ 5 |
+ 7.7 |
| lib/text_to_sql_query.rb |
- 100.0 % |
+ 92.86 % |
93 |
- 0 |
- 0 |
- 0 |
- 0.0 |
+ 56 |
+ 52 |
+ 4 |
+ 40.0 |
@@ -96,14 +116,1936 @@
+
+
+
+
+
+
+ -
+
+
+
#--
+
+
+ -
+
+
+
# DO NOT MODIFY!!!!
+
+
+ -
+
+
+
# This file is automatically generated by rex 1.0.7
+
+
+ -
+
+
+
# from lexical definition file "./lib/specification.rex".
+
+
+ -
+
+
+
#++
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
require 'racc/parser'
+
+
+ -
+ 1
+
+
class Query < Racc::Parser
+
+
+ -
+ 1
+
+
require 'strscan'
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
class ScanError < StandardError ; end
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
attr_reader :lineno
+
+
+ -
+ 1
+
+
attr_reader :filename
+
+
+ -
+ 1
+
+
attr_accessor :state
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
def scan_setup(str)
+
+
+ -
+ 132
+
+
@ss = StringScanner.new(str)
+
+
+ -
+ 132
+
+
@lineno = 1
+
+
+ -
+ 132
+
+
@state = nil
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
def action
+
+
+ -
+ 702
+
+
yield
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
def scan_str(str)
+
+
+ -
+ 94
+
+
scan_setup(str)
+
+
+ -
+ 94
+
+
do_parse
+
+
+ -
+
+
+
end
+
+
+ -
+ 1
+
+
alias :scan :scan_str
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
def load_file( filename )
+
+
+ -
+
+
+
@filename = filename
+
+
+ -
+
+
+
File.open(filename, "r") do |f|
+
+
+ -
+
+
+
scan_setup(f.read)
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
def scan_file( filename )
+
+
+ -
+
+
+
load_file(filename)
+
+
+ -
+
+
+
do_parse
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
def next_token
+
+
+ -
+ 834
+
+
return if @ss.eos?
+
+
+ -
+
+
+
+
+
+ -
+
+
+
# skips empty actions
+
+
+ -
+ 703
+
+
until token = _next_token or @ss.eos?; end
+
+
+ -
+ 703
+
+
token
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
def _next_token
+
+
+ -
+ 939
+
+
text = @ss.peek(1)
+
+
+ -
+ 939
+
+
@lineno += 1 if text == "\n"
+
+
+ -
+ 939
+
+
token = case @state
+
+
+ -
+
+
+
when nil
+
+
+ -
+
+
+
case
+
+
+ -
+ 939
+
+
when (text = @ss.scan(/ +/))
+
+
+ -
+
+
+
;
+
+
+ -
+
+
+
+
+
+ -
+ 702
+
+
when (text = @ss.scan(/\(/))
+
+
+ -
+ 106
+
+
action { return [:L_BRACKET, text] }
+
+
+ -
+
+
+
+
+
+ -
+ 649
+
+
when (text = @ss.scan(/\)/))
+
+
+ -
+ 106
+
+
action { return [:R_BRACKET, text] }
+
+
+ -
+
+
+
+
+
+ -
+ 596
+
+
when (text = @ss.scan(/(?i)\bor\b/))
+
+
+ -
+ 84
+
+
action { return [:OPERATOR_OR, text] }
+
+
+ -
+
+
+
+
+
+ -
+ 554
+
+
when (text = @ss.scan(/(?i)\band\b/))
+
+
+ -
+ 84
+
+
action { return [:OPERATOR_AND, text] }
+
+
+ -
+
+
+
+
+
+ -
+ 512
+
+
when (text = @ss.scan(/(?i)\bnot\b/))
+
+
+ -
+ 46
+
+
action { return [:OPERATOR_NOT, text] }
+
+
+ -
+
+
+
+
+
+ -
+ 489
+
+
when (text = @ss.scan(/"([^"]*)"/))
+
+
+ -
+ 132
+
+
action { return [:TERM_WITH_QUOTES, text] }
+
+
+ -
+
+
+
+
+
+ -
+ 423
+
+
when (text = @ss.scan(/[a-zA-Z0-9\-_]+/))
+
+
+ -
+ 610
+
+
action { return [:TERM_WITHOUT_QUOTES, text] }
+
+
+ -
+
+
+
+
+
+ -
+ 118
+
+
when (text = @ss.scan(/\:/))
+
+
+ -
+ 236
+
+
action { return [:COLON, text] }
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
else
+
+
+ -
+
+
+
text = @ss.string[@ss.pos .. -1]
+
+
+ -
+
+
+
raise ScanError, "can not match: '" + text + "'"
+
+
+ -
+ 939
+
+
end # if
+
+
+ -
+
+
+
+
+
+ -
+
+
+
else
+
+
+ -
+
+
+
raise ScanError, "undefined state: '" + state.to_s + "'"
+
+
+ -
+
+
+
end # case state
+
+
+ -
+ 237
+
+
token
+
+
+ -
+
+
+
end # def _next_token
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
def tokenize(code)
+
+
+ -
+ 38
+
+
scan_setup(code)
+
+
+ -
+ 38
+
+
tokens = []
+
+
+ -
+ 38
+
+
while token = next_token
+
+
+ -
+ 174
+
+
tokens << token
+
+
+ -
+
+
+
end
+
+
+ -
+ 38
+
+
tokens
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
end # class
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
#
+
+
+ -
+
+
+
# DO NOT MODIFY!!!!
+
+
+ -
+
+
+
# This file is automatically generated by Racc 1.4.16
+
+
+ -
+
+
+
# from Racc grammar file "".
+
+
+ -
+
+
+
#
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
require 'racc/parser.rb'
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
require_relative 'lexer'
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
class Query < Racc::Parser
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
module_eval(<<'...end grammar.y/module_eval...', 'grammar.y', 26)
+
+
+ -
+
+
+
def parse(input)
+
+
+ -
+
+
+
scan_str(input)
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
...end grammar.y/module_eval...
+
+
+ -
+
+
+
##### State transition tables begin ###
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
racc_action_table = [
+
+
+ -
+
+
+
5, 10, 9, 3, 4, 5, 6, 19, 3, 4,
+
+
+ -
+
+
+
7, 6, 5, 10, 9, 3, 4, 5, 6, 11,
+
+
+ -
+
+
+
3, 4, 5, 6, 14, 3, 4, nil, 6, 5,
+
+
+ -
+
+
+
10, 9, 3, 4, 5, 6, nil, 3, 4, 5,
+
+
+ -
+
+
+
6, nil, 3, 4, nil, 6, 5, 10, nil, 3,
+
+
+ -
+
+
+
4, 5, 6, nil, 3, 4, nil, 6, 3, 4,
+
+
+ -
+
+
+
nil, 6, 17, 18 ]
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
racc_action_check = [
+
+
+ -
+
+
+
13, 13, 13, 13, 13, 0, 13, 13, 0, 0,
+
+
+ -
+
+
+
1, 0, 2, 2, 2, 2, 2, 5, 2, 3,
+
+
+ -
+
+
+
5, 5, 6, 5, 7, 6, 6, nil, 6, 8,
+
+
+ -
+
+
+
8, 8, 8, 8, 9, 8, nil, 9, 9, 10,
+
+
+ -
+
+
+
9, nil, 10, 10, nil, 10, 15, 15, nil, 15,
+
+
+ -
+
+
+
15, 16, 15, nil, 16, 16, nil, 16, 12, 12,
+
+
+ -
+
+
+
nil, 12, 11, 11 ]
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
racc_action_pointer = [
+
+
+ -
+
+
+
3, 10, 10, 12, nil, 15, 20, 24, 27, 32,
+
+
+ -
+
+
+
37, 57, 53, -2, nil, 44, 49, nil, nil, nil ]
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
racc_action_default = [
+
+
+ -
+
+
+
-2, -12, -1, -3, -4, -12, -12, -12, -11, -12,
+
+
+ -
+
+
+
-12, -12, -9, -12, 20, -7, -8, -5, -6, -10 ]
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
racc_goto_table = [
+
+
+ -
+
+
+
2, 1, nil, nil, nil, 12, 13, nil, nil, 15,
+
+
+ -
+
+
+
16 ]
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
racc_goto_check = [
+
+
+ -
+
+
+
2, 1, nil, nil, nil, 2, 2, nil, nil, 2,
+
+
+ -
+
+
+
2 ]
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
racc_goto_pointer = [
+
+
+ -
+
+
+
nil, 1, 0 ]
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
racc_goto_default = [
+
+
+ -
+
+
+
nil, nil, 8 ]
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
racc_reduce_table = [
+
+
+ -
+
+
+
0, 0, :racc_error,
+
+
+ -
+
+
+
1, 11, :_reduce_none,
+
+
+ -
+
+
+
0, 11, :_reduce_2,
+
+
+ -
+
+
+
1, 12, :_reduce_3,
+
+
+ -
+
+
+
1, 12, :_reduce_4,
+
+
+ -
+
+
+
3, 12, :_reduce_5,
+
+
+ -
+
+
+
3, 12, :_reduce_6,
+
+
+ -
+
+
+
3, 12, :_reduce_7,
+
+
+ -
+
+
+
3, 12, :_reduce_8,
+
+
+ -
+
+
+
2, 12, :_reduce_9,
+
+
+ -
+
+
+
3, 12, :_reduce_10,
+
+
+ -
+
+
+
2, 12, :_reduce_11 ]
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
racc_reduce_n = 12
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
racc_shift_n = 20
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
racc_token_table = {
+
+
+ -
+
+
+
false => 0,
+
+
+ -
+
+
+
:error => 1,
+
+
+ -
+
+
+
:OPERATOR_NOT => 2,
+
+
+ -
+
+
+
:OPERATOR_AND => 3,
+
+
+ -
+
+
+
:OPERATOR_OR => 4,
+
+
+ -
+
+
+
:TERM_WITHOUT_QUOTES => 5,
+
+
+ -
+
+
+
:TERM_WITH_QUOTES => 6,
+
+
+ -
+
+
+
:COLON => 7,
+
+
+ -
+
+
+
:L_BRACKET => 8,
+
+
+ -
+
+
+
:R_BRACKET => 9 }
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
racc_nt_base = 10
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
racc_use_result_var = true
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
Racc_arg = [
+
+
+ -
+
+
+
racc_action_table,
+
+
+ -
+
+
+
racc_action_check,
+
+
+ -
+
+
+
racc_action_default,
+
+
+ -
+
+
+
racc_action_pointer,
+
+
+ -
+
+
+
racc_goto_table,
+
+
+ -
+
+
+
racc_goto_check,
+
+
+ -
+
+
+
racc_goto_default,
+
+
+ -
+
+
+
racc_goto_pointer,
+
+
+ -
+
+
+
racc_nt_base,
+
+
+ -
+
+
+
racc_reduce_table,
+
+
+ -
+
+
+
racc_token_table,
+
+
+ -
+
+
+
racc_shift_n,
+
+
+ -
+
+
+
racc_reduce_n,
+
+
+ -
+
+
+
racc_use_result_var ]
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
Racc_token_to_s_table = [
+
+
+ -
+
+
+
"$end",
+
+
+ -
+
+
+
"error",
+
+
+ -
+
+
+
"OPERATOR_NOT",
+
+
+ -
+
+
+
"OPERATOR_AND",
+
+
+ -
+
+
+
"OPERATOR_OR",
+
+
+ -
+
+
+
"TERM_WITHOUT_QUOTES",
+
+
+ -
+
+
+
"TERM_WITH_QUOTES",
+
+
+ -
+
+
+
"COLON",
+
+
+ -
+
+
+
"L_BRACKET",
+
+
+ -
+
+
+
"R_BRACKET",
+
+
+ -
+
+
+
"$start",
+
+
+ -
+
+
+
"target",
+
+
+ -
+
+
+
"expression" ]
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
Racc_debug_parser = false
+
+
+ -
+
+
+
+
+
+ -
+
+
+
##### State transition tables end #####
+
+
+ -
+
+
+
+
+
+ -
+
+
+
# reduce 0 omitted
+
+
+ -
+
+
+
+
+
+ -
+
+
+
# reduce 1 omitted
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
module_eval(<<'.,.,', 'grammar.y', 8)
+
+
+ -
+
+
+
def _reduce_2(val, _values, result)
+
+
+ -
+
+
+
result = 0
+
+
+ -
+
+
+
result
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
.,.,
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
module_eval(<<'.,.,', 'grammar.y', 10)
+
+
+ -
+
+
+
def _reduce_3(val, _values, result)
+
+
+ -
+
+
+
result = {:DEFAULT_COLUMN => val[0]}
+
+
+ -
+
+
+
result
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
.,.,
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
module_eval(<<'.,.,', 'grammar.y', 11)
+
+
+ -
+
+
+
def _reduce_4(val, _values, result)
+
+
+ -
+
+
+
result = {:DEFAULT_COLUMN => val[0]}
+
+
+ -
+
+
+
result
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
.,.,
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
module_eval(<<'.,.,', 'grammar.y', 12)
+
+
+ -
+
+
+
def _reduce_5(val, _values, result)
+
+
+ -
+
+
+
result = {val[0] => val[2]}
+
+
+ -
+
+
+
result
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
.,.,
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
module_eval(<<'.,.,', 'grammar.y', 13)
+
+
+ -
+
+
+
def _reduce_6(val, _values, result)
+
+
+ -
+
+
+
result = {val[0] => val[2]}
+
+
+ -
+
+
+
result
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
.,.,
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
module_eval(<<'.,.,', 'grammar.y', 14)
+
+
+ -
+
+
+
def _reduce_7(val, _values, result)
+
+
+ -
+
+
+
result = {:OPERATOR_OR => [val[0], val[2]]}
+
+
+ -
+
+
+
result
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
.,.,
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
module_eval(<<'.,.,', 'grammar.y', 15)
+
+
+ -
+
+
+
def _reduce_8(val, _values, result)
+
+
+ -
+
+
+
result = {:OPERATOR_AND => [val[0], val[2]]}
+
+
+ -
+
+
+
result
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
.,.,
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
module_eval(<<'.,.,', 'grammar.y', 16)
+
+
+ -
+
+
+
def _reduce_9(val, _values, result)
+
+
+ -
+
+
+
result = {:OPERATOR_NOT => val[1]}
+
+
+ -
+
+
+
result
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
.,.,
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
module_eval(<<'.,.,', 'grammar.y', 17)
+
+
+ -
+
+
+
def _reduce_10(val, _values, result)
+
+
+ -
+
+
+
result = val[1]
+
+
+ -
+
+
+
result
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
.,.,
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
module_eval(<<'.,.,', 'grammar.y', 18)
+
+
+ -
+
+
+
def _reduce_11(val, _values, result)
+
+
+ -
+
+
+
result = {:OPERATOR_OR => [val[0], val[1]]}
+
+
+ -
+
+
+
result
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
.,.,
+
+
+ -
+
+
+
+
+
+ -
+ 1
+
+
def _reduce_none(val, _values, result)
+
+
+ -
+
+
+
val[0]
+
+
+ -
+
+
+
end
+
+
+ -
+
+
+
+
+
+ -
+
+
+
end # class Query
+
+
+
+
+
+
+
@@ -152,14 +2094,14 @@
-
-
+
+ 1
module PgSearchable
-
-
+
+ 1
extend ActiveSupport::Concern
@@ -170,14 +2112,14 @@
-
-
+
+ 1
included do
-
-
+
+ 13
def update_pg_search_cache
@@ -212,14 +2154,14 @@
-
-
+
+ 1
class_methods do
-
-
+
+ 1
def pg_search(
@@ -290,62 +2232,62 @@
)
-
-
+
+ 13
@ts_search_fields = fields
-
-
+
+ 13
@ts_search_fields_mappings = fields_mappings
-
-
+
+ 13
@ts_cache_field = cache
-
-
+
+ 13
@ts_language = language
-
-
+
+ 13
@ts_scope_method = scope
-
-
+
+ 13
@ts_skip_cache_update = skip_callback
-
-
+
+ 13
@ts_wildcard = wildcard
-
-
+
+ 13
@ts_joins = joins
-
+
+ 13
-
- @default_field = (default_field.to_sym || fields.first)
+ @default_field = default_field.to_s.empty? ? fields.first : default_field.to_sym
-
-
+
+ 13
ts_add_scope
@@ -362,82 +2304,82 @@
-
-
+
+ 1
def ts_add_scope
-
-
+
+ 13
class_eval do
-
+
+ 13
-
- scope ts_scope_method, ->(value) { ts_search(value) }
+ scope ts_scope_method, ->(value) do
-
+
+ 24
-
- end
+ resulting_ids = ts_search(value).map(&:id)
-
+
+ 23
-
- end
+ where(id: resulting_ids)
-
+ end
-
+
- def ts_search(value)
+ end
-
-
-
- return if @ts_search_fields.blank? || value.blank?
-
-
-
-
-
- TextToSqlQuery.new(value, @ts_search_fields, @default_field, @ts_search_fields_mappings).where_clause( includes(@ts_joins).references(:all))
-
-
-
+
end
-
+
-
+
+ 1
-
- def should_update_cache_field?
+ def ts_search(value)
-
+
+ 24
+
+ return if @ts_search_fields.blank? || value.blank?
+
+
+
- !@ts_skip_cache_update && @ts_cache_field.present?
+ includes(@ts_joins).references(:all).where(
+
+
+
+ 24
+
+ TextToSqlQuery.new(value, @ts_search_fields, @default_field, @ts_search_fields_mappings).where_clause).distinct
@@ -452,16 +2394,16 @@
-
+
+ 1
-
- def ts_cache_field
+ def should_update_cache_field?
- @ts_cache_field
+ !@ts_skip_cache_update && @ts_cache_field.present?
@@ -476,16 +2418,16 @@
-
+
+ 1
-
- def ts_scope_method
+ def ts_cache_field
-
+
+ 16
-
- @ts_scope_method
+ @ts_cache_field
@@ -500,16 +2442,16 @@
-
+
+ 1
-
- def ts_cache_method
+ def ts_scope_method
-
+
+ 13
-
- @ts_cache_method
+ @ts_scope_method
@@ -524,43 +2466,67 @@
-
+
+ 1
-
- def ts_fields_to_vector(extra_data = [])
+ def ts_cache_method
- field_to_vector = ->(field) { "to_tsvector('#{@ts_language}', coalesce(#{field}::text, ''))" }
+ @ts_cache_method
-
-
-
- data_to_vector = ->(data) { "to_tsvector('#{@ts_language}', '#{data}')" }
-
-
-
-
-
- (@ts_search_fields.map(&field_to_vector) + extra_data.map(&data_to_vector)).join(' || ')
-
-
-
+
end
-
+
+
+
+
+
+
+
+ 1
+
+ def ts_fields_to_vector(extra_data = [])
+
+
+
+
+
+ field_to_vector = ->(field) { "to_tsvector('#{@ts_language}', coalesce(#{field}::text, ''))" }
+
+
+
+
+
+ data_to_vector = ->(data) { "to_tsvector('#{@ts_language}', '#{data}')" }
+
+
+
+
+
+ (@ts_search_fields.map(&field_to_vector) + extra_data.map(&data_to_vector)).join(' || ')
+
+
+
+
+
+ end
+
+
+
end
-
+
end
@@ -574,21 +2540,21 @@
- -
+
-
+ 1
-
-
require './parser'
+ require_relative 'parser'
-
@@ -597,56 +2563,56 @@
- -
-
+
-
+ 1
class TextToSqlQuery
- -
-
+
-
+ 1
def initialize(text, fields, default_field, fields_mappings = {})
- -
-
+
-
+ 44
@text = text.to_s.strip
- -
-
+
-
+ 44
@fields = fields.map(&:to_sym)
- -
-
+
-
+ 44
@default_field = default_field.to_sym
- -
-
+
-
+ 44
@fields_mappings = fields_mappings.merge(@fields.reduce({}) do |mappings, field|
- -
-
+
-
+ 114
_table_name, field_name = field.to_s.split('.')
- -
-
+
-
+ 114
mappings[field_name.to_sym] = field
- -
-
+
-
+ 114
mappings
@@ -657,14 +2623,14 @@
end)
- -
-
+
-
+ 44
fields_mappings.each do |field, value|
- -
-
+
-
+ 13
@fields_mappings[field] = value if @fields_mappings[field]
@@ -687,26 +2653,26 @@
- -
-
+
-
+ 1
def where_clause
- -
-
+
-
+ 44
@parser = Query.new
- -
-
+
-
+ 44
@parsed_tree = @parser.parse(@text)
- -
-
+
-
+ 44
generate_sql @parsed_tree
@@ -723,8 +2689,8 @@
- -
-
+
-
+ 1
private
@@ -735,26 +2701,26 @@
- -
-
+
-
+ 1
def generate_sql(tree)
- -
-
+
-
+ 128
first_key = tree.keys.first
- -
-
+
-
+ 128
node_value = tree[first_key]
- -
-
+
-
+ 128
case first_key
@@ -765,16 +2731,16 @@
when :DEFAULT_COLUMN
- -
-
+
-
+ 23
escaped_node_value = handle_special_chars node_value
- -
+
-
+ 23
-
-
["#{@default_field.to_s} ILIKE ?", "%#{escaped_node_value}%"]
+ ["CAST(#{@default_field.to_s} AS TEXT) ILIKE ?", "%#{escaped_node_value}%"]
-
@@ -783,8 +2749,8 @@
when :OPERATOR_OR
- -
-
+
-
+ 22
generate_expression_for_logical_operator(:OR, node_value)
@@ -795,8 +2761,8 @@
when :OPERATOR_AND
- -
-
+
-
+ 15
generate_expression_for_logical_operator(:AND, node_value)
@@ -807,8 +2773,8 @@
when :OPERATOR_NOT
- -
-
+
-
+ 10
not_array = generate_sql node_value
@@ -819,13 +2785,13 @@
- -
-
+
-
+ 10
if not_array.length < 2
- -
+
-
raise "There should be more than 1 element for expression following NOT operator"
@@ -843,26 +2809,26 @@
- -
+
-
+ 10
-
-
not_expression = not_array.first
+ not_expression = not_array.shift
- -
+
-
+ 10
-
-
not_params = not_array[1..]
+ not_params = not_array
-
-
+
- -
-
+
-
+ 10
["NOT #{not_expression}"] + not_params
@@ -885,26 +2851,26 @@
# key is column name
- -
-
+
-
+ 58
escaped_node_value = handle_special_chars node_value
- -
-
+
-
+ 58
mapping = @fields_mappings[first_key.to_sym]
- -
-
+
-
+ 58
if mapping.nil?
- -
-
+
-
+ 2
raise "Unknown field '#{first_key.to_s}'"
@@ -915,10 +2881,10 @@
else
- -
+
-
+ 56
-
-
["#{mapping.to_s} ILIKE ?", "%#{escaped_node_value}%"]
+ ["CAST(#{mapping.to_s} AS TEXT) ILIKE ?", "%#{escaped_node_value}%"]
-
@@ -945,19 +2911,19 @@
- -
-
+
-
+ 1
def generate_expression_for_logical_operator(operator, operator_array)
- -
-
+
-
+ 37
if operator_array.length != 2
- -
+
-
raise "There should be two array elements for #{operator.to_s} operator"
@@ -975,14 +2941,14 @@
- -
-
+
-
+ 37
first_operand = generate_sql operator_array.first
- -
-
+
-
+ 37
second_operand = generate_sql operator_array.last
@@ -993,13 +2959,13 @@
- -
-
+
-
+ 37
if first_operand.length < 2
- -
+
-
raise 'There should be more than 1 element in first operand array'
@@ -1017,13 +2983,13 @@
- -
-
+
-
+ 37
if second_operand.length < 2
- -
+
-
raise 'There should be more than 1 element in second operand array'
@@ -1041,16 +3007,16 @@
- -
+
-
+ 37
-
-
first_operand_expression = first_operand.first
+ first_operand_expression = first_operand.shift
- -
+
-
+ 37
-
-
first_operand_params = first_operand[1..]
+ first_operand_params = first_operand
-
@@ -1059,16 +3025,16 @@
- -
+
-
+ 37
-
-
second_operand_expression = second_operand.first
+ second_operand_expression = second_operand.shift
- -
+
-
+ 37
-
-
second_operand_params = second_operand[1..]
+ second_operand_params = second_operand
-
@@ -1077,8 +3043,8 @@
- -
-
+
-
+ 37
["(#{first_operand_expression} #{operator.to_s} #{second_operand_expression})"] + first_operand_params + second_operand_params
@@ -1095,38 +3061,38 @@
- -
-
+
-
+ 1
def handle_special_chars(text)
- -
-
+
-
+ 81
result = text.gsub(/\"/, '')
- -
-
+
-
+ 81
result.gsub!(/\_/, '\_')
- -
-
+
-
+ 81
result.tr!('\\', '\\')
- -
-
+
-
+ 81
result.gsub!(/%/, '\%')
- -
-
+
-
+ 81
result
diff --git a/lib/lexer.rb b/lib/lexer.rb
index 80cb51f..aa87d79 100644
--- a/lib/lexer.rb
+++ b/lib/lexer.rb
@@ -66,13 +66,13 @@ class Query < Racc::Parser
when (text = @ss.scan(/\)/))
action { return [:R_BRACKET, text] }
- when (text = @ss.scan(/(?i)or/))
+ when (text = @ss.scan(/(?i)\bor\b/))
action { return [:OPERATOR_OR, text] }
- when (text = @ss.scan(/(?i)and/))
+ when (text = @ss.scan(/(?i)\band\b/))
action { return [:OPERATOR_AND, text] }
- when (text = @ss.scan(/(?i)not/))
+ when (text = @ss.scan(/(?i)\bnot\b/))
action { return [:OPERATOR_NOT, text] }
when (text = @ss.scan(/"([^"]*)"/))
diff --git a/lib/pg_searchable_regex.rb b/lib/pg_searchable_regex.rb
index 4387373..8c7374b 100644
--- a/lib/pg_searchable_regex.rb
+++ b/lib/pg_searchable_regex.rb
@@ -36,20 +36,23 @@ module PgSearchable
@ts_skip_cache_update = skip_callback
@ts_wildcard = wildcard
@ts_joins = joins
- @default_field = (default_field.to_sym || fields.first)
+ @default_field = default_field.to_s.empty? ? fields.first : default_field.to_sym
ts_add_scope
end
def ts_add_scope
class_eval do
- scope ts_scope_method, ->(value) { ts_search(value) }
+ scope ts_scope_method, ->(value) do
+ resulting_ids = ts_search(value).map(&:id)
+ where(id: resulting_ids)
+ end
end
end
def ts_search(value)
return if @ts_search_fields.blank? || value.blank?
includes(@ts_joins).references(:all).where(
- TextToSqlQuery.new(value, @ts_search_fields, @default_field, @ts_search_fields_mappings).where_clause)
+ TextToSqlQuery.new(value, @ts_search_fields, @default_field, @ts_search_fields_mappings).where_clause).distinct
end
def should_update_cache_field?
diff --git a/lib/specification.rex b/lib/specification.rex
index c92a37e..2fc8eaa 100644
--- a/lib/specification.rex
+++ b/lib/specification.rex
@@ -3,9 +3,9 @@ macro
L_BRACKET \(
R_BRACKET \)
SPACE \ + # Space char
- OPERATOR_OR (?i)or
- OPERATOR_AND (?i)and
- OPERATOR_NOT (?i)not
+ OPERATOR_OR (?i)\bor\b
+ OPERATOR_AND (?i)\band\b
+ OPERATOR_NOT (?i)\bnot\b
TERM_WITH_QUOTES "([^"]*)"
TERM_WITHOUT_QUOTES [a-zA-Z0-9\-_]+
COLON \:
diff --git a/lib/text_to_regex_query.rb b/lib/text_to_regex_query.rb
deleted file mode 100644
index 7b7c569..0000000
--- a/lib/text_to_regex_query.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# frozen_string_literal: true
-
-# transforms "english like" text queries into a where clause with regex
-# https://www.postgresql.org/docs/9.5/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
-
-class TextToRegexQuery
- def initialize(text, fields, default_field, fields_mappings = {})
- @text = text.to_s.strip
- @fields = fields.map(&:to_sym)
- @default_field = default_field.to_sym
- @fields_mappings = fields_mappings.merge(@fields.reduce({}) do |mappings, field|
- table_name, field_name = field.to_s.split(".")
- mappings[field_name.to_sym] = field
- mappings
- end)
- end
-
- def where_clause(query)
- @cleared_text = @text.dup
- @column_chunks = []
- remove_duplicated_spaces
- extract_columns
- escape_special_characters
- generate_where_clause(query)
- end
-
-private
-
- def remove_duplicated_spaces
- @cleared_text.gsub!(/\s+/, ' ')
- end
-
- def escape_special_characters
- @cleared_text.gsub!(/\_/, '\_')
- @cleared_text.tr!('\\', '\\')
- @cleared_text.gsub!(/%/, '\%')
- end
-
- def extract_columns
- column_search_term_pairs = @cleared_text.scan(/([A-Za-z0-9_]+:[\w\_-]+)/)
-
- @column_chunks = (column_search_term_pairs.flatten.map do |pair|
- column, term = pair.split(':')
- next unless @fields_mappings.include?(column.to_sym)
- @cleared_text.gsub!(pair, '')
- { @fields_mappings[column.to_sym] => term }
- end).compact
- unless @cleared_text.strip.empty?
- @column_chunks = [{ @default_field.to_s => @cleared_text.strip }] + @column_chunks
- end
- @column_chunks
- end
-
- def generate_where_clause(query)
- where_clause = ''
- columns = @column_chunks.map { |c| c.keys.first }
- values = @column_chunks.map { |c| c.values.first }
-
- columns.each do |column|
- quoted_column = '"' + column.to_s.gsub(".",'"."') + '"'
- where_clause += "#{quoted_column} ILIKE ? OR "
- end
- where_clause += " 1<>1 "
- regexed_values = values.map { |v| "%#{v}%" }
- query.where([where_clause] + regexed_values)
- end
-end
diff --git a/lib/text_to_sql_query.rb b/lib/text_to_sql_query.rb
index d473f48..c464349 100644
--- a/lib/text_to_sql_query.rb
+++ b/lib/text_to_sql_query.rb
@@ -29,7 +29,7 @@ class TextToSqlQuery
case first_key
when :DEFAULT_COLUMN
escaped_node_value = handle_special_chars node_value
- ["#{@default_field.to_s} ILIKE ?", "%#{escaped_node_value}%"]
+ ["CAST(#{@default_field.to_s} AS TEXT) ILIKE ?", "%#{escaped_node_value}%"]
when :OPERATOR_OR
generate_expression_for_logical_operator(:OR, node_value)
when :OPERATOR_AND
@@ -53,7 +53,7 @@ class TextToSqlQuery
if mapping.nil?
raise "Unknown field '#{first_key.to_s}'"
else
- ["#{mapping.to_s} ILIKE ?", "%#{escaped_node_value}%"]
+ ["CAST(#{mapping.to_s} AS TEXT) ILIKE ?", "%#{escaped_node_value}%"]
end
end
end
diff --git a/spec/lib/pg_searchable_new_spec.rb b/spec/lib/pg_searchable_new_spec.rb
new file mode 100644
index 0000000..2bf7fd6
--- /dev/null
+++ b/spec/lib/pg_searchable_new_spec.rb
@@ -0,0 +1,181 @@
+# frozen_string_literal: true
+require_relative '../../lib/pg_searchable_regex'
+
+describe PgSearchable do
+ describe 'pg_search' do
+
+ describe 'properties' do
+ describe 'scope' do
+ it 'defaults to "scope_search"' do
+ expect(VectorModel).to respond_to(:scope_search)
+ end
+
+ it 'can use a different scope name' do
+ expect(VectorModelWithCustomSearchScope).to respond_to(:fulltext)
+ end
+
+ it 'doesnt pollutes the default method name if customized' do
+ expect(VectorModelWithCustomSearchScope).not_to respond_to(:scope_search)
+ end
+ end
+
+ describe 'language' do
+ it 'defaults to english lexemes' do
+ record = VectorModel.create name: 'something', value: 'amazing'
+ expect(VectorModel.scope_search('value:amaz')).to include(record)
+ end
+
+ it 'can be changed to simple to avoid lexeme truncation' do
+ record = SimpleVectorModel.create name: 'something', value: 'amazing'
+ expect(SimpleVectorModel.scope_search('value:amazings')).not_to include(record)
+ end
+ end
+ end
+
+ describe 'single model' do
+ it 'fails if column name is unknown' do
+ record1 = VectorModel.create name: 'hamo', value: '100'
+
+ expect{(VectorModel.scope_search("device_id:#{record1.id}"))}.to raise_error(RuntimeError, "Unknown field 'device_id'")
+ end
+
+ it 'searches only default column if no column name is used' do
+ record1 = VectorModel.create name: 'hamo', value: '-45'
+ record2 = VectorModel.create name: 'meho', value: '120'
+
+ expect(VectorModel.scope_search('45')).to be_empty
+ expect(VectorModel.scope_search("#{record2.id}")).to contain_exactly(record2)
+ end
+
+ it 'searches column declared in search term' do
+ record1 = VectorModel.create name: 'hamo', value: '-45'
+ record2 = VectorModel.create name: 'meho', value: '120'
+
+ expect(VectorModel.scope_search('name:45')).to be_empty
+ expect(VectorModel.scope_search('value:120')).to contain_exactly(record2)
+ end
+
+ it 'searches column declared in search term with only partial match' do
+ record1 = VectorModel.create name: 'hamo', value: '-45'
+ record2 = VectorModel.create name: 'meho', value: '120'
+
+ expect(VectorModel.scope_search('value:-4')).to contain_exactly(record1)
+ expect(VectorModel.scope_search('value:12')).to contain_exactly(record2)
+ end
+
+ it 'handles multiple search terms without logical operator between' do
+ record1 = VectorModel.create name: 'hamo', value: '-45'
+ record2 = VectorModel.create name: 'meho', value: '120'
+ record3 = VectorModel.create name: 'atif', value: '80'
+
+ expect(VectorModel.scope_search('value:-4 name:meho')).to contain_exactly(record1, record2)
+ end
+
+ it 'handles multiple search terms with AND operator between' do
+ record1 = VectorModel.create name: 'hamo', value: '-45'
+ record2 = VectorModel.create name: 'meho', value: '120'
+ record3 = VectorModel.create name: 'atif', value: '80'
+
+ expect(VectorModel.scope_search('value:-4 AND name:meho')).to be_empty
+ end
+
+ it 'handles search term with NOT operator' do
+ record1 = VectorModel.create name: 'hamo', value: '-45'
+ record2 = VectorModel.create name: 'meho', value: '120'
+ record3 = VectorModel.create name: 'atif', value: '80'
+
+ expect(VectorModel.scope_search('NOT value:0')).to contain_exactly(record1)
+ end
+
+ it 'handles multiple search terms with mixed logical operators between without brackets' do
+ record1 = VectorModel.create name: 'hamo', value: '-45'
+ record2 = VectorModel.create name: 'meho', value: '120'
+ record3 = VectorModel.create name: 'atif', value: '80'
+
+ expect(VectorModel.scope_search('name:hamo AND value:120 OR value:80')).to contain_exactly(record3)
+ end
+
+ it 'handles multiple search terms with mixed logical operators between with brackets' do
+ record1 = VectorModel.create name: 'hamo', value: '-45'
+ record2 = VectorModel.create name: 'meho', value: '120'
+ record3 = VectorModel.create name: 'atif', value: '80'
+
+ expect(VectorModel.scope_search('name:hamo AND (value:120 OR value:80)')).to be_empty
+ end
+ end
+
+ describe 'field mappings and joins' do
+ it 'does not fail if column is unknown but mapping with that name exists' do
+ record1 = VectorModelWithMappings.create name: 'hamo', value: '-45'
+
+ expect(VectorModelWithMappings.scope_search("device_id:#{record1.id}")).to contain_exactly(record1)
+ end
+
+ it 'can search in referenced column' do
+ record = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
+ Tag.create(taggable: record, value: 'red')
+
+ expect(DynamicModelWithTagValues.scope_search('tag:red')).to contain_exactly(record)
+ end
+
+ it 'can search in referenced column with multiple search terms' do
+ record1 = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
+ record2 = DynamicModelWithTagValues.create name: 'new record', value: 'not so amazing'
+ Tag.create(taggable: record1, value: 'red')
+ Tag.create(taggable: record2, value: 'green')
+
+ expect(DynamicModelWithTagValues.scope_search('tag:red tag:green')).to contain_exactly(record1, record2)
+ end
+
+ it 'can search in referenced column and in model columns with multiple search terms' do
+ record1 = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
+ record2 = DynamicModelWithTagValues.create name: 'new record', value: 'not so amazing'
+ Tag.create(taggable: record1, value: 'red')
+ Tag.create(taggable: record2, value: 'green')
+
+ expect(DynamicModelWithTagValues.scope_search('tag:red value:"not"')).to contain_exactly(record1, record2)
+ end
+
+ it 'can find models without tags' do
+ record1 = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
+ record2 = DynamicModelWithTagValues.create name: 'new record', value: 'not so amazing'
+ Tag.create(taggable: record1, value: 'green')
+
+ expect(DynamicModelWithTagValues.scope_search('tag:green or value:"not"')).to contain_exactly(record1, record2)
+ end
+
+
+
+ it 'can search in referenced column and in model columns with multiple search terms connected with logical operators' do
+ record1 = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
+ record2 = DynamicModelWithTagValues.create name: 'new record', value: 'not so amazing'
+ record3 = DynamicModelWithTagValues.create name: 'last one', value: 'no value'
+ record4 = DynamicModelWithTagValues.create name: 'really last one', value: 'no value'
+ Tag.create(taggable: record1, value: 'red')
+ Tag.create(taggable: record1, value: 'green')
+ Tag.create(taggable: record2, value: 'black')
+ Tag.create(taggable: record3, value: '-12')
+ Tag.create(taggable: record4, value: '-')
+
+ expect(DynamicModelWithTagValues.scope_search('tag:red or tag:black')).to contain_exactly(record1, record2)
+ expect(DynamicModelWithTagValues.scope_search('tag:red and tag:black')).to be_empty
+ expect(DynamicModelWithTagValues.scope_search('tag:red or tag:green')).to contain_exactly(record1)
+ expect(DynamicModelWithTagValues.scope_search('not tag:-12 and not value:amazing')).to contain_exactly(record4)
+ end
+
+ it 'can search in referenced column and in model columns with multiple search terms connected with logical operators and with brackets' do
+ record1 = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
+ record2 = DynamicModelWithTagValues.create name: 'new record', value: 'not so amazing'
+ record3 = DynamicModelWithTagValues.create name: 'last one', value: 'no value'
+ record4 = DynamicModelWithTagValues.create name: 'really last one', value: 'no value'
+ Tag.create(taggable: record1, value: 'red')
+ Tag.create(taggable: record1, value: 'green')
+ Tag.create(taggable: record2, value: 'black')
+ Tag.create(taggable: record3, value: '-12')
+ Tag.create(taggable: record4, value: '-')
+
+ expect(DynamicModelWithTagValues.scope_search('(tag:- and not tag:12) or (value:"amazing" and not value:"not")')).to contain_exactly(record1, record4)
+ end
+ end
+ end
+end
diff --git a/spec/lib/pg_searchable_spec.rb b/spec/lib/pg_searchable_spec.rb
deleted file mode 100644
index 5a80c00..0000000
--- a/spec/lib/pg_searchable_spec.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-# frozen_string_literal: true
-
-describe PgSearchable do
- include_examples 'pg_search', VectorModel
- include_examples 'pg_search', VectorWithCustomPrimaryKeyModel
- include_examples 'pg_search', VectorWithCustomCallback
- include_examples 'pg_search', SimpleVectorModel
- include_examples 'pg_search', VectorWithoutWildcardModel
- include_examples 'pg_search', VectorModelWithCustomSearchScope, 'fulltext'
- include_examples 'pg_search', VectorModelWithTagValues
- include_examples 'pg_search', DynamicModel
- include_examples 'pg_search', DynamicModelWithTagValues
- include_examples 'pg_search', DynamicModelWithCategory
- include_examples 'pg_search', DynamicModelWithSectionsTrhough
-
- describe 'pg_search' do
- describe 'joins' do
- it 'can dynamically query compound relation' do
- record = DynamicModelWithCategory.create(name: 'something', value: 'amazing')
- category = Category.create(name: 'searchable')
- Tag.create(value: 'impressive', category: category, taggable: record)
- expect(DynamicModelWithCategory.scope_search('searchable')).to include(record)
- end
-
- it 'can use has_many :through relation' do
- record = DynamicModelWithSectionsTrhough.create(name: 'something', value: 'amazing')
- tag = Tag.create(value: 'impressive', taggable: record)
- Section.create(name: 'searchable', tag: tag)
- expect(DynamicModelWithSectionsTrhough.scope_search('searchable')).to include(record)
- end
- end
-
- describe 'properties' do
- describe 'skip_callback' do
- context 'when enabled' do
- let(:record) { VectorModel.create(name: 'something', value: 'amazing') }
-
- it 'can find the record after it updates' do
- record.update(name: 'cookie')
- expect(VectorModel.scope_search('cookie')).to include(record)
- end
- end
-
- context 'when disabled' do
- let(:record) { VectorModelWithoutCallback.create(name: 'something', value: 'amazing') }
-
- it 'cannot find the record after it updates' do
- record.update(name: 'cookie')
- expect(VectorModelWithoutCallback.scope_search('cookie')).not_to include(record)
- end
-
- it 'can find the record after manually calling .update_pg_search_cache' do
- record.update(name: 'cookie')
- record.update_pg_search_cache
- expect(VectorModelWithoutCallback.scope_search('cookie')).to include(record)
- end
- end
- end
-
- describe 'scope' do
- it 'defaults to "scope_search"' do
- expect(VectorModel).to respond_to(:scope_search)
- end
-
- it 'can use a different scope name' do
- expect(VectorModelWithCustomSearchScope).to respond_to(:fulltext)
- end
-
- it 'doesnt pollutes the default method name if customized' do
- expect(VectorModelWithCustomSearchScope).not_to respond_to(:scope_search)
- end
- end
-
- describe 'language' do
- it 'defaults to english lexemes' do
- record = VectorModel.create name: 'something', value: 'amazing'
- expect(VectorModel.scope_search('amaz')).to include(record)
- end
-
- it 'can be changed to simple to avoid lexeme truncation' do
- record = SimpleVectorModel.create name: 'something', value: 'amazing'
- expect(SimpleVectorModel.scope_search('amazings')).not_to include(record)
- end
- end
-
- describe 'wildcard' do
- it 'by default uses it' do
- record = VectorModel.create name: '12345', value: 'amazing'
- expect(VectorModel.scope_search('123')).to include(record)
- end
-
- it 'can be set to false' do
- record = VectorWithoutWildcardModel.create name: '12345', value: 'amazing'
- expect(VectorWithoutWildcardModel.scope_search('123')).not_to include(record)
- end
- end
-
- describe 'tags' do
- it 'allow indexing fields of other associations' do
- record = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
- Tag.create(taggable: record, value: 'red')
- expect(DynamicModelWithTagValues.scope_search('red')).to include(record)
- end
- end
-
- describe 'external_cache_data' do
- it 'can index external data using a method' do
- record = VectorModelWithTagValues.create name: 'something', value: 'amazing'
- Tag.create(taggable: record, value: 'red')
- expect(VectorModelWithTagValues.scope_search('red')).to include(record)
- end
- end
- end
- end
-end
diff --git a/spec/lib/query_lexer_spec.rb b/spec/lib/query_lexer_spec.rb
index f531899..ff68dfd 100644
--- a/spec/lib/query_lexer_spec.rb
+++ b/spec/lib/query_lexer_spec.rb
@@ -255,5 +255,278 @@ class QueryLexerTester
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
expect(@result[2][1]).to eq '"tag with or and not inside"'
end
+
+ it 'tests query without column name containing AND word inside without quotes' do
+ @result = @evaluator.tokenize('land')
+
+ expect(@result.length).to eq 1
+
+ expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[0][1]).to eq 'land'
+ end
+
+ it 'tests query without column name containing AND word inside with quotes' do
+ @result = @evaluator.tokenize('"land"')
+
+ expect(@result.length).to eq 1
+
+ expect(@result[0][0]).to eq :TERM_WITH_QUOTES
+ expect(@result[0][1]).to eq '"land"'
+ end
+
+ it 'tests query without column name and with word starting with AND without quotes' do
+ @result = @evaluator.tokenize('andromeda')
+
+ expect(@result.length).to eq 1
+
+ expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[0][1]).to eq 'andromeda'
+ end
+
+ it 'tests query without column name and with word starting with AND with quotes' do
+ @result = @evaluator.tokenize('"andromeda"')
+
+ expect(@result.length).to eq 1
+
+ expect(@result[0][0]).to eq :TERM_WITH_QUOTES
+ expect(@result[0][1]).to eq '"andromeda"'
+ end
+
+ it 'tests query with column name containing AND word inside' do
+ @result = @evaluator.tokenize('land:some-search-term')
+
+ expect(@result.length).to eq 3
+
+ expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[0][1]).to eq 'land'
+ expect(@result[1][0]).to eq :COLON
+ expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[2][1]).to eq 'some-search-term'
+ end
+
+ it 'tests query with column name starting with AND' do
+ @result = @evaluator.tokenize('andromeda:some-search-term')
+
+ expect(@result.length).to eq 3
+
+ expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[0][1]).to eq 'andromeda'
+ expect(@result[1][0]).to eq :COLON
+ expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[2][1]).to eq 'some-search-term'
+ end
+
+ it 'tests query with search term without quotes containing AND word inside' do
+ @result = @evaluator.tokenize('name:land')
+
+ expect(@result.length).to eq 3
+
+ expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[0][1]).to eq 'name'
+ expect(@result[1][0]).to eq :COLON
+ expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[2][1]).to eq 'land'
+ end
+
+ it 'tests query with search term with quotes containing AND word inside' do
+ @result = @evaluator.tokenize('name:"land"')
+
+ expect(@result.length).to eq 3
+
+ expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[0][1]).to eq 'name'
+ expect(@result[1][0]).to eq :COLON
+ expect(@result[2][0]).to eq :TERM_WITH_QUOTES
+ expect(@result[2][1]).to eq '"land"'
+ end
+
+ it 'tests query with search term without quotes and search term starting with AND without quotes' do
+ @result = @evaluator.tokenize('name:andromeda')
+
+ expect(@result.length).to eq 3
+
+ expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[0][1]).to eq 'name'
+ expect(@result[1][0]).to eq :COLON
+ expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[2][1]).to eq 'andromeda'
+ end
+
+ it 'tests query with search term without quotes and search term starting with AND with quotes' do
+ @result = @evaluator.tokenize('name:"andrew"')
+
+ expect(@result.length).to eq 3
+
+ expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[0][1]).to eq 'name'
+ expect(@result[1][0]).to eq :COLON
+ expect(@result[2][0]).to eq :TERM_WITH_QUOTES
+ expect(@result[2][1]).to eq '"andrew"'
+ end
+
+ it 'tests query with search term in brackets with AND inside word' do
+ @result = @evaluator.tokenize('(land)')
+
+ expect(@result.length).to eq 3
+
+ expect(@result[0][0]).to eq :L_BRACKET
+ expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[1][1]).to eq 'land'
+ expect(@result[2][0]).to eq :R_BRACKET
+ end
+
+ it 'tests query with search term in brackets starting with AND' do
+ @result = @evaluator.tokenize('(andromeda)')
+
+ expect(@result.length).to eq 3
+
+ expect(@result[0][0]).to eq :L_BRACKET
+ expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[1][1]).to eq 'andromeda'
+ expect(@result[2][0]).to eq :R_BRACKET
+ end
+
+ it 'tests query with search term in brackets with AND inside word with quotes' do
+ @result = @evaluator.tokenize('("land")')
+
+ expect(@result.length).to eq 3
+
+ expect(@result[0][0]).to eq :L_BRACKET
+ expect(@result[1][0]).to eq :TERM_WITH_QUOTES
+ expect(@result[1][1]).to eq '"land"'
+ expect(@result[2][0]).to eq :R_BRACKET
+ end
+
+ it 'tests query with search term in brackets starting with AND with quotes' do
+ @result = @evaluator.tokenize('("andromeda")')
+
+ expect(@result.length).to eq 3
+
+ expect(@result[0][0]).to eq :L_BRACKET
+ expect(@result[1][0]).to eq :TERM_WITH_QUOTES
+ expect(@result[1][1]).to eq '"andromeda"'
+ expect(@result[2][0]).to eq :R_BRACKET
+ end
+
+ it 'tests query with multiple search terms with mixed and-or-not after dash and underscore' do
+ @result = @evaluator.tokenize('123-and-456 -or-2 -not_not_1')
+
+ expect(@result.length).to eq 3
+
+ expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[0][1]).to eq '123-and-456'
+ expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[1][1]).to eq '-or-2'
+ expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[2][1]).to eq '-not_not_1'
+ end
+
+ it 'tests query with multiple search terms starting with AND' do
+ @result = @evaluator.tokenize('land andrew and andromeda or orlando')
+
+ expect(@result.length).to eq 6
+
+ expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[0][1]).to eq 'land'
+ expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[1][1]).to eq 'andrew'
+ expect(@result[2][0]).to eq :OPERATOR_AND
+ expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[3][1]).to eq 'andromeda'
+ expect(@result[4][0]).to eq :OPERATOR_OR
+ expect(@result[5][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[5][1]).to eq 'orlando'
+ end
+
+ it 'tests query with multiple search terms with dash and underscore chars' do
+ @result = @evaluator.tokenize('land 123-andrew or -andrew- and not _ornela_ or ornela')
+
+ expect(@result.length).to eq 9
+
+ expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[0][1]).to eq 'land'
+ expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[1][1]).to eq '123-andrew'
+ expect(@result[2][0]).to eq :OPERATOR_OR
+ expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[3][1]).to eq '-andrew-'
+ expect(@result[4][0]).to eq :OPERATOR_AND
+ expect(@result[5][0]).to eq :OPERATOR_NOT
+ expect(@result[6][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[6][1]).to eq '_ornela_'
+ expect(@result[7][0]).to eq :OPERATOR_OR
+ expect(@result[8][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[8][1]).to eq 'ornela'
+
+ end
+
+ it 'tests query with multiple search terms with dash and underscore chars under quotes' do
+ @result = @evaluator.tokenize('"land" "123-andrew" or "-andrew-"')
+
+ expect(@result.length).to eq 4
+
+ expect(@result[0][0]).to eq :TERM_WITH_QUOTES
+ expect(@result[0][1]).to eq '"land"'
+ expect(@result[1][0]).to eq :TERM_WITH_QUOTES
+ expect(@result[1][1]).to eq '"123-andrew"'
+ expect(@result[2][0]).to eq :OPERATOR_OR
+ expect(@result[3][0]).to eq :TERM_WITH_QUOTES
+ expect(@result[3][1]).to eq '"-andrew-"'
+ end
+
+ it 'tests query with multiple search terms with mixed and-or-not on start' do
+ @result = @evaluator.tokenize('sindy andora ornoty notary')
+
+ expect(@result.length).to eq 4
+
+ expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[0][1]).to eq 'sindy'
+ expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[1][1]).to eq 'andora'
+ expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[2][1]).to eq 'ornoty'
+ expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[3][1]).to eq 'notary'
+ end
+
+ it 'tests query with multiple search terms with mixed and-or-not on start and with logical operators between' do
+ @result = @evaluator.tokenize('sindy and andora-notary or not ornoty notary')
+
+ expect(@result.length).to eq 7
+
+ expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[0][1]).to eq 'sindy'
+ expect(@result[1][0]).to eq :OPERATOR_AND
+ expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[2][1]).to eq 'andora-notary'
+ expect(@result[3][0]).to eq :OPERATOR_OR
+ expect(@result[4][0]).to eq :OPERATOR_NOT
+ expect(@result[5][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[5][1]).to eq 'ornoty'
+ expect(@result[6][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[6][1]).to eq 'notary'
+ end
+
+ it 'tests query with multiple search terms with mixed and-or-not on start and with logical operators between' do
+ @result = @evaluator.tokenize('(sindy and andora-notary)or not(ornoty notary)')
+
+ expect(@result.length).to eq 11
+
+ expect(@result[0][0]).to eq :L_BRACKET
+ expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[1][1]).to eq 'sindy'
+ expect(@result[2][0]).to eq :OPERATOR_AND
+ expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[3][1]).to eq 'andora-notary'
+ expect(@result[4][0]).to eq :R_BRACKET
+ expect(@result[5][0]).to eq :OPERATOR_OR
+ expect(@result[6][0]).to eq :OPERATOR_NOT
+ expect(@result[7][0]).to eq :L_BRACKET
+ expect(@result[8][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[8][1]).to eq 'ornoty'
+ expect(@result[9][0]).to eq :TERM_WITHOUT_QUOTES
+ expect(@result[9][1]).to eq 'notary'
+ expect(@result[10][0]).to eq :R_BRACKET
+ end
end
end
diff --git a/spec/lib/query_parser_spec.rb b/spec/lib/query_parser_spec.rb
index 16439fc..5ef78cd 100644
--- a/spec/lib/query_parser_spec.rb
+++ b/spec/lib/query_parser_spec.rb
@@ -133,7 +133,7 @@ class QueryParserTester
expect(@result[:OPERATOR_AND]).to eq @expected_array_total
end
- it 'tests operator precedence' do
+ it 'tests operator precedence, or - and' do
@result1 = @evaluator.parse('tag:mta or name:JF and 12_4')
@result2 = @evaluator.parse('tag:mta or (name:JF and 12_4)')
@@ -155,10 +155,469 @@ class QueryParserTester
end
- # Tests to write :
- # * query with multiple column names and search terms without logical operators
- # * AND NOT, OR NOT tests
+ it 'tests operator precedence, and - or' do
+ @result1 = @evaluator.parse('tag:mta and name:JF or 12_4')
+ @result2 = @evaluator.parse('(tag:mta and name:JF) or 12_4')
+ expect(@result1).to eq @result2
+
+ expect(@result1.length).to eq 1
+
+ @expected_array_part_2 = [
+ {'tag' => 'mta'},
+ {'name' => 'JF'}
+ ]
+
+ @expected_array_total = [
+ {:OPERATOR_AND => @expected_array_part_2},
+ {:DEFAULT_COLUMN => '12_4'}
+ ]
+
+ expect(@result1[:OPERATOR_OR]).to eq @expected_array_total
+
+ end
+
+ it 'tests simple query with not operator' do
+ @result = @evaluator.parse('not -54')
+
+ expect(@result.length).to be 1
+ expect(@result[:OPERATOR_NOT][:DEFAULT_COLUMN]).to eq '-54'
+ end
+
+ it 'tests query with mixed NOT and AND logic operator' do
+ @result = @evaluator.parse('name:"some wild name" and not -123-456')
+
+ @expected_and_array = [
+ {'name' => '"some wild name"'},
+ {:OPERATOR_NOT => {:DEFAULT_COLUMN => '-123-456'}}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_AND]).to eq @expected_and_array
+ end
+
+ it 'tests query with mixed NOT, AND and OR logic operators' do
+ @result = @evaluator.parse('name:"some wild name" or not (tag:mta and -123-456)')
+
+ @expected_and_array_operands = [
+ {'tag' => 'mta'},
+ {:DEFAULT_COLUMN => '-123-456'}
+ ]
+
+ @expected_or_array_operands = [
+ {'name' => '"some wild name"'},
+ {:OPERATOR_NOT => {:OPERATOR_AND => @expected_and_array_operands}}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @expected_or_array_operands
+ end
+
+ it 'tests query with two default column search terms without quotes and without logical operators' do
+ @result = @evaluator.parse('id-5 -456')
+
+ @expected_array = [
+ {:DEFAULT_COLUMN => 'id-5'},
+ {:DEFAULT_COLUMN => '-456'}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @expected_array
+ end
+
+ it 'tests query with two default column search terms with quotes and without logical operators' do
+ @result = @evaluator.parse('"id-5 no Q" -456')
+
+ @expected_array = [
+ {:DEFAULT_COLUMN => '"id-5 no Q"'},
+ {:DEFAULT_COLUMN => '-456'}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @expected_array
+ end
+
+ it 'tests query with two default column search terms with quotes on both terms and without logical operators' do
+ @result = @evaluator.parse('"id-5 no Q" "wild id -456"')
+
+ @expected_array = [
+ {:DEFAULT_COLUMN => '"id-5 no Q"'},
+ {:DEFAULT_COLUMN => '"wild id -456"'}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @expected_array
+ end
+
+ it 'tests query with one default column and one non defualt column, search terms without quotes and without logical operators' do
+ @result = @evaluator.parse('tag:mta -456')
+
+ @expected_array = [
+ {'tag' => 'mta'},
+ {:DEFAULT_COLUMN => '-456'}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @expected_array
+ end
+
+ it 'tests query with one default column and one non defualt column, search terms with quotes and without logical operators' do
+ @result = @evaluator.parse('-1-23 name:"WILD name"')
+
+ @expected_array = [
+ {:DEFAULT_COLUMN => '-1-23'},
+ {'name' => '"WILD name"'}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @expected_array
+ end
+
+ it 'tests query with two columns, search terms with quotes and without logical operators' do
+ @result = @evaluator.parse('tag:-1-23 name:"WILD name"')
+
+ @expected_array = [
+ {'tag' => '-1-23'},
+ {'name' => '"WILD name"'}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @expected_array
+ end
+
+ it 'tests query with two columns, search terms with quotes on both and without logical operators' do
+ @result = @evaluator.parse('tag:"1 2 3" name:"WILD name"')
+
+ @expected_array = [
+ {'tag' => '"1 2 3"'},
+ {'name' => '"WILD name"'}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @expected_array
+ end
+
+ it 'tests query with three columns, search terms with quotes on both and without logical operators' do
+ @result = @evaluator.parse('tag:"1 2 3" name:"WILD name" name:"ANOTHER wild name"')
+
+ @expected_array = [
+ {'tag' => '"1 2 3"'},
+ {:OPERATOR_OR => [
+ {'name' => '"WILD name"'},
+ {'name' => '"ANOTHER wild name"'}
+ ]}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @expected_array
+ end
+
+ it 'tests complex query' do
+ @result = @evaluator.parse('(device-id:"with space" tag:mta no-quotes-id-123)'\
+ 'or "id with quotes-5" and ( ("id with q 10" or "id with q 20")'\
+ 'and ("id with Q 30" "id with Q 40") and not id-without-Q-50)')
+
+ # (device-id:"with space" tag:mta no-quotes-id-123) or "id with quotes-5" and ( ("id with q 10" or "id with q 20") and ("id with Q 30" "id with Q 40") and not id-without-Q-50)
+ # _____________________A___________________________ or _______B___________ and __________________________________________________C______________________________________________
+ # (____A1________________ ___A2__ ______A3________) or _______B___________ and ( ____________C1____________________ and ______________C2_______________ and ______C3___________)
+
+
+ @A = {:OPERATOR_OR => [
+ {'device-id' => '"with space"'},
+ {:OPERATOR_OR => [
+ {'tag' => 'mta'},
+ {:DEFAULT_COLUMN => 'no-quotes-id-123'}]
+ }
+ ]}
+
+ @B = {:DEFAULT_COLUMN => '"id with quotes-5"'}
+
+
+ @C1 = {:OPERATOR_OR => [
+ {:DEFAULT_COLUMN => '"id with q 10"'},
+ {:DEFAULT_COLUMN => '"id with q 20"'}
+ ]}
+
+
+ @C2 = {:OPERATOR_OR => [
+ {:DEFAULT_COLUMN => '"id with Q 30"'},
+ {:DEFAULT_COLUMN => '"id with Q 40"'}
+ ]}
+
+ @C3 = {:OPERATOR_NOT => {:DEFAULT_COLUMN => 'id-without-Q-50'}}
+
+ @C = {:OPERATOR_AND => [
+ {:OPERATOR_AND => [
+ @C1,
+ @C2
+ ]},
+ @C3
+ ]}
+
+ @expected_final_array_result = [
+ @A,
+ {:OPERATOR_AND => [
+ @B,
+ @C
+ ]}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @expected_final_array_result
+ end
+
+ it 'tests query without column name containing AND word inside without quotes' do
+ @result = @evaluator.parse('land')
+
+ expect(@result.length).to eq 1
+ expect(@result[:DEFAULT_COLUMN]).to eq 'land'
+ end
+
+ it 'tests query without column name containing AND word inside with quotes' do
+ @result = @evaluator.parse('"land"')
+
+ expect(@result.length).to eq 1
+ expect(@result[:DEFAULT_COLUMN]).to eq '"land"'
+ end
+
+ it 'tests query without column name and with word starting with AND without quotes' do
+ @result = @evaluator.parse('andromeda')
+
+ expect(@result.length).to eq 1
+ expect(@result[:DEFAULT_COLUMN]).to eq 'andromeda'
+ end
+
+ it 'tests query without column name and with word starting with AND with quotes' do
+ @result = @evaluator.parse('"andromeda"')
+
+ expect(@result.length).to eq 1
+ expect(@result[:DEFAULT_COLUMN]).to eq '"andromeda"'
+ end
+
+ it 'tests query with column name containing AND word inside' do
+ @result = @evaluator.parse('land:some-search-term')
+
+ expect(@result.length).to eq 1
+ expect(@result['land']).to eq 'some-search-term'
+ end
+
+ it 'tests query with column name starting with AND' do
+ @result = @evaluator.parse('andromeda:some-search-term')
+
+ expect(@result.length).to eq 1
+ expect(@result['andromeda']).to eq 'some-search-term'
+ end
+
+ it 'tests query with search term without quotes containing AND word inside' do
+ @result = @evaluator.parse('name:land')
+
+ expect(@result.length).to eq 1
+ expect(@result['name']).to eq 'land'
+ end
+
+ it 'tests query with search term with quotes containing AND word inside' do
+ @result = @evaluator.parse('name:"land"')
+
+ expect(@result.length).to eq 1
+ expect(@result['name']).to eq '"land"'
+ end
+
+ it 'tests query with search term without quotes and search term starting with AND without quotes' do
+ @result = @evaluator.parse('name:andromeda')
+
+ expect(@result.length).to eq 1
+ expect(@result['name']).to eq 'andromeda'
+ end
+
+ it 'tests query with search term without quotes and search term starting with AND with quotes' do
+ @result = @evaluator.parse('name:"andrew"')
+
+ expect(@result.length).to eq 1
+ expect(@result['name']).to eq '"andrew"'
+ end
+
+ it 'tests query with search term in brackets with AND inside word' do
+ @result = @evaluator.parse('(land)')
+
+ expect(@result.length).to eq 1
+ expect(@result[:DEFAULT_COLUMN]).to eq 'land'
+ end
+
+ it 'tests query with search term in brackets starting with AND' do
+ @result = @evaluator.parse('(andromeda)')
+
+ expect(@result.length).to eq 1
+ expect(@result[:DEFAULT_COLUMN]).to eq 'andromeda'
+ end
+
+ it 'tests query with multiple search terms with mixed and-or-not after dash and underscore' do
+ @result = @evaluator.parse('123-and-456 -or-2 -not_not_1')
+
+ @or_array_1 = [
+ {:DEFAULT_COLUMN => '-or-2'},
+ {:DEFAULT_COLUMN => '-not_not_1'}
+ ]
+
+ @last_or_array = [
+ {:DEFAULT_COLUMN => '123-and-456'},
+ {:OPERATOR_OR => @or_array_1}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @last_or_array
+ end
+
+ it 'tests query with search term in brackets with AND inside word with quotes' do
+ @result = @evaluator.parse('("land")')
+
+ expect(@result.length).to eq 1
+ expect(@result[:DEFAULT_COLUMN]).to eq '"land"'
+ end
+
+ it 'tests query with search term in brackets starting with AND with quotes' do
+ @result = @evaluator.parse('("andromeda")')
+
+ expect(@result.length).to eq 1
+ expect(@result[:DEFAULT_COLUMN]).to eq '"andromeda"'
+ end
+
+ it 'tests query with multiple search terms starting with AND' do
+ @result = @evaluator.parse('land andrew and andromeda or orlando')
+
+ @and_array = [
+ {:DEFAULT_COLUMN=>'andrew'},
+ {:DEFAULT_COLUMN=>'andromeda'}
+ ]
+
+ @last_or_array = [
+ {:OPERATOR_AND => @and_array},
+ {:DEFAULT_COLUMN => 'orlando'}
+ ]
+
+ @main_or_array = [
+ {:DEFAULT_COLUMN => 'land'},
+ {:OPERATOR_OR => @last_or_array}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @main_or_array
+ end
+
+ it 'tests query with multiple search terms with dash and underscore chars' do
+ @result = @evaluator.parse('land 123-andrew or -andrew- and not _ornela_ or ornela')
+
+ @and_array = [
+ {:DEFAULT_COLUMN=>'-andrew-'},
+ {:OPERATOR_NOT=>{:DEFAULT_COLUMN => '_ornela_'}}
+ ]
+
+ @intra_or_array = [
+ {:DEFAULT_COLUMN => '123-andrew'},
+ {:OPERATOR_AND => @and_array}
+ ]
+
+ @last_or_array = [
+ {:OPERATOR_OR => @intra_or_array},
+ {:DEFAULT_COLUMN => 'ornela'}
+ ]
+
+ @main_or_array = [
+ {:DEFAULT_COLUMN => 'land'},
+ {:OPERATOR_OR => @last_or_array}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @main_or_array
+
+ end
+
+ it 'tests query with multiple search terms with dash and underscore chars under quotes' do
+ @result = @evaluator.parse('"land" "123-andrew" or "-andrew-"')
+
+
+ @last_or_array = [
+ {:DEFAULT_COLUMN => '"123-andrew"'},
+ {:DEFAULT_COLUMN => '"-andrew-"'}
+ ]
+
+ @main_or_array = [
+ {:DEFAULT_COLUMN => '"land"'},
+ {:OPERATOR_OR => @last_or_array}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @main_or_array
+ end
+
+ it 'tests query with multiple search terms with mixed and-or-not on start' do
+ @result = @evaluator.parse('sindy andora ornoty notary')
+
+ @or_array_1 = [
+ {:DEFAULT_COLUMN=>'ornoty'},
+ {:DEFAULT_COLUMN=>'notary'}
+ ]
+
+ @or_array_2 = [
+ {:DEFAULT_COLUMN => 'andora'},
+ {:OPERATOR_OR => @or_array_1}
+ ]
+
+ @main_or_array = [
+ {:DEFAULT_COLUMN => 'sindy'},
+ {:OPERATOR_OR => @or_array_2}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @main_or_array
+ end
+
+ it 'tests query with multiple search terms with mixed and-or-not on start and with logical operators between' do
+ @result = @evaluator.parse('sindy and andora-notary or not ornoty notary')
+
+ @and_array = [
+ {:DEFAULT_COLUMN=>'sindy'},
+ {:DEFAULT_COLUMN=>'andora-notary'}
+ ]
+
+ @not_or_array = [
+ {:DEFAULT_COLUMN => 'ornoty'},
+ {:DEFAULT_COLUMN => 'notary'}
+ ]
+
+ @main_or_array = [
+ {:OPERATOR_AND => @and_array},
+ {:OPERATOR_NOT => {
+ :OPERATOR_OR => @not_or_array
+ }}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_OR]).to eq @main_or_array
+ end
+
+ it 'tests query with multiple search terms with mixed and-or-not on start and with logical operators between' do
+ @result = @evaluator.parse('(sindy or andora-notary)and not(ornoty notary)')
+
+ @or_array_1 = [
+ {:DEFAULT_COLUMN=>'sindy'},
+ {:DEFAULT_COLUMN=>'andora-notary'}
+ ]
+
+ @not_or_array = [
+ {:DEFAULT_COLUMN => 'ornoty'},
+ {:DEFAULT_COLUMN => 'notary'}
+ ]
+
+ @main_and_array = [
+ {:OPERATOR_OR => @or_array_1},
+ {:OPERATOR_NOT => {
+ :OPERATOR_OR => @not_or_array
+ }}
+ ]
+
+ expect(@result.length).to eq 1
+ expect(@result[:OPERATOR_AND]).to eq @main_and_array
+ end
end
end
diff --git a/spec/lib/text_to_regex_query_spec.rb b/spec/lib/text_to_regex_query_spec.rb
deleted file mode 100644
index 57cc291..0000000
--- a/spec/lib/text_to_regex_query_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-describe TextToRegexQuery do
- include_examples 'pg_search', SimpleVectorModel
- describe '.new' do
- # just default
- it { expect(described_class.new('some-default-value', [:"players.name"], :"players.name").where_clause(SimpleVectorModel)).to eq([' players.name like ? OR 1<>1', '%some-default-value%']) }
-
- # default and named
- it { expect(described_class.new('name:hamo id:1', [:"players.name"], :"players.name").where_clause(SimpleVectorModel)).to eq([' players.name like ? OR players.name like ? OR 1<>1', '%id:1%', '%hamo%']) }
-
- # escape characters
- it { expect(described_class.new('name:hamo id:1',[:"players.name"], :"players.name").where_clause(SimpleVectorModel)).to eq([' players.name like ? OR players.name like ? OR 1<>1', '%id:1%', '%hamo%']) }
-
- # default and explicit with underscore
- it { expect(described_class.new('device_id:-123', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq([" players.device_id like ? OR 1<>1", "%-123%"]) }
-
- # default and explicit with underscore with multiple columns
- it { expect(described_class.new('name:bla device_id:-123', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq([" players.name like ? OR players.device_id like ? OR 1<>1", "%bla%", "%-123%"]) }
-
- end
-end
diff --git a/spec/lib/text_to_sql_query_spec.rb b/spec/lib/text_to_sql_query_spec.rb
index c8f39c0..aed9c6b 100644
--- a/spec/lib/text_to_sql_query_spec.rb
+++ b/spec/lib/text_to_sql_query_spec.rb
@@ -4,58 +4,63 @@ require_relative '../../lib/text_to_sql_query'
describe TextToSqlQuery do
describe '.new' do
# tests simple search term without column name and without quotes
- it { expect(described_class.new('some-default-value', [:"players.name"], :"players.name").where_clause).to eq(['players.name ILIKE ?', '%some-default-value%']) }
+ it { expect(described_class.new('some-default-value', [:"players.name"], :"players.name").where_clause).to eq(['CAST(players.name AS TEXT) ILIKE ?', '%some-default-value%']) }
# tests simple search term with column name and without quotes
- it { expect(described_class.new('name:ab', [:"players.name"], :"players.name").where_clause).to eq(['players.name ILIKE ?', '%ab%']) }
+ it { expect(described_class.new('name:ab', [:"players.name"], :"players.name").where_clause).to eq(['CAST(players.name AS TEXT) ILIKE ?', '%ab%']) }
# tests simple search term with unknown column name and without quotes
it { expect{described_class.new('unknown:ab', [:"players.name"], :"players.name").where_clause}.to raise_error(RuntimeError, "Unknown field 'unknown'") }
# tests simple search term without column name and with quotes
- it { expect(described_class.new('"ab"', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["players.device_id ILIKE ?", "%ab%"]) }
+ it { expect(described_class.new('"ab"', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["CAST(players.device_id AS TEXT) ILIKE ?", "%ab%"]) }
# tests simple search term with column name and with quotes
- it { expect(described_class.new('tags:"ab"', [:"players.name", :"players.tags"], :"players.device_id").where_clause).to eq(["players.tags ILIKE ?", "%ab%"]) }
+ it { expect(described_class.new('tags:"ab"', [:"players.name", :"players.tags"], :"players.device_id").where_clause).to eq(["CAST(players.tags AS TEXT) ILIKE ?", "%ab%"]) }
# tests search without operators
- it { expect(described_class.new('123 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["(players.device_id ILIKE ? OR players.device_id ILIKE ?)", "%123%", "%456%"]) }
+ it { expect(described_class.new('123 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["(CAST(players.device_id AS TEXT) ILIKE ? OR CAST(players.device_id AS TEXT) ILIKE ?)", "%123%", "%456%"]) }
# tests search with OR operator
- it { expect(described_class.new('123 or 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["(players.device_id ILIKE ? OR players.device_id ILIKE ?)", "%123%", "%456%"]) }
+ it { expect(described_class.new('123 or 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["(CAST(players.device_id AS TEXT) ILIKE ? OR CAST(players.device_id AS TEXT) ILIKE ?)", "%123%", "%456%"]) }
# tests search with AND operator
- it { expect(described_class.new('123 and 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["(players.device_id ILIKE ? AND players.device_id ILIKE ?)", "%123%", "%456%"]) }
+ it { expect(described_class.new('123 and 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["(CAST(players.device_id AS TEXT) ILIKE ? AND CAST(players.device_id AS TEXT) ILIKE ?)", "%123%", "%456%"]) }
# tests search with NOT operator on default column
- it { expect(described_class.new('not 23', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["NOT players.device_id ILIKE ?", "%23%"]) }
+ it { expect(described_class.new('not 23', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["NOT CAST(players.device_id AS TEXT) ILIKE ?", "%23%"]) }
# tests search with NOT operator on non-default column
- it { expect(described_class.new('not value:23', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(["NOT players.value ILIKE ?", "%23%"]) }
+ it { expect(described_class.new('not value:23', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(["NOT CAST(players.value AS TEXT) ILIKE ?", "%23%"]) }
# tests search with mixed logical operators
- it { expect(described_class.new('name:ab and not value:hf-1', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['(players.name ILIKE ? AND NOT players.value ILIKE ?)', '%ab%', '%hf-1%']) }
+ it { expect(described_class.new('name:ab and not value:hf-1', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['(CAST(players.name AS TEXT) ILIKE ? AND NOT CAST(players.value AS TEXT) ILIKE ?)', '%ab%', '%hf-1%']) }
# tests search with mixed logical operators without NOT'
- it { expect(described_class.new('name:a and name:b or name:c', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['((players.name ILIKE ? AND players.name ILIKE ?) OR players.name ILIKE ?)', '%a%', '%b%', '%c%']) }
+ it { expect(described_class.new('name:a and name:b or name:c', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['((CAST(players.name AS TEXT) ILIKE ? AND CAST(players.name AS TEXT) ILIKE ?) OR CAST(players.name AS TEXT) ILIKE ?)', '%a%', '%b%', '%c%']) }
# tests search with brackets in expression
- it { expect(described_class.new('name:a and (name:b or name:c)', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['(players.name ILIKE ? AND (players.name ILIKE ? OR players.name ILIKE ?))', '%a%', '%b%', '%c%']) }
+ it { expect(described_class.new('name:a and (name:b or name:c)', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['(CAST(players.name AS TEXT) ILIKE ? AND (CAST(players.name AS TEXT) ILIKE ? OR CAST(players.name AS TEXT) ILIKE ?))', '%a%', '%b%', '%c%']) }
# tests search with brackets in expression and with NOT operator
- it { expect(described_class.new('name:a and not (name:b or name:c)', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['(players.name ILIKE ? AND NOT (players.name ILIKE ? OR players.name ILIKE ?))', '%a%', '%b%', '%c%']) }
+ it { expect(described_class.new('name:a and not (name:b or name:c)', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['(CAST(players.name AS TEXT) ILIKE ? AND NOT (CAST(players.name AS TEXT) ILIKE ? OR CAST(players.name AS TEXT) ILIKE ?))', '%a%', '%b%', '%c%']) }
# tests search with special characters in search term
- it { expect(described_class.new('name:"%a_\"', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['players.name ILIKE ?', '%\%a\_\\%']) }
+ it { expect(described_class.new('name:"%a_\"', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['CAST(players.name AS TEXT) ILIKE ?', '%\%a\_\\%']) }
# tests search with field mappings
- it { expect(described_class.new('tags:h1-r', [:'players.name', :'players.value', :'players.device_id'], :"players.device_id", { tags: "tags.value" }).where_clause).to eq(['tags.value ILIKE ?', '%h1-r%']) }
+ it { expect(described_class.new('tags:h1-r', [:'players.name', :'players.value', :'players.device_id'], :"players.device_id", { tags: "tags.value" }).where_clause).to eq(['CAST(tags.value AS TEXT) ILIKE ?', '%h1-r%']) }
# tests search with field mappings when fields array has same mapping
- it { expect(described_class.new('tags:h1-r', [:'players.name', :'players.tags', :'players.device_id'], :"players.device_id", { tags: "tags.value" }).where_clause).to eq(["tags.value ILIKE ?", "%h1-r%"]) }
+ it { expect(described_class.new('tags:h1-r', [:'players.name', :'players.tags', :'players.device_id'], :"players.device_id", { tags: "tags.value" }).where_clause).to eq(["CAST(tags.value AS TEXT) ILIKE ?", "%h1-r%"]) }
# tests complex query
- it { expect(described_class.new('(device_id:"with space" tags:mta no-quotes-id-123) or "id with quotes-5" and ( ("id with q 10" or "id with q 20") and ("id with Q 30" "id with Q 40") and not id-without-Q-50)', [:'players.name', :'players.value', :'players.device_id'], :"players.device_id", { tags: 'tags.name' }).where_clause).to eq(['((players.device_id ILIKE ? OR (tags.name ILIKE ? OR players.device_id ILIKE ?)) OR (players.device_id ILIKE ? AND (((players.device_id ILIKE ? OR players.device_id ILIKE ?) AND (players.device_id ILIKE ? OR players.device_id ILIKE ?)) AND NOT players.device_id ILIKE ?)))', '%with space%', '%mta%', '%no-quotes-id-123%', '%id with quotes-5%', '%id with q 10%', '%id with q 20%', '%id with Q 30%', '%id with Q 40%', '%id-without-Q-50%']) }
+ it { expect(described_class.new('(device_id:"with space" tags:mta no-quotes-id-123) or "id with quotes-5" and ( ("id with q 10" or "id with q 20") and ("id with Q 30" "id with Q 40") and not id-without-Q-50)', [:'players.name', :'players.value', :'players.device_id'], :"players.device_id", { tags: 'tags.name' }).where_clause).to eq(['((CAST(players.device_id AS TEXT) ILIKE ? OR (CAST(tags.name AS TEXT) ILIKE ? OR CAST(players.device_id AS TEXT) ILIKE ?)) OR (CAST(players.device_id AS TEXT) ILIKE ? AND (((CAST(players.device_id AS TEXT) ILIKE ? OR CAST(players.device_id AS TEXT) ILIKE ?) AND (CAST(players.device_id AS TEXT) ILIKE ? OR CAST(players.device_id AS TEXT) ILIKE ?)) AND NOT CAST(players.device_id AS TEXT) ILIKE ?)))', '%with space%', '%mta%', '%no-quotes-id-123%', '%id with quotes-5%', '%id with q 10%', '%id with q 20%', '%id with Q 30%', '%id with Q 40%', '%id-without-Q-50%']) }
+ # tests query with multiple search terms with mixed and-or-not after dash and underscore
+ it { expect(described_class.new('123-and-456 -or-2 -not_not_1', [:'players.title', :'players.tag', :'players.device_id'], :'players.device_id').where_clause).to eq(['(CAST(players.device_id AS TEXT) ILIKE ? OR (CAST(players.device_id AS TEXT) ILIKE ? OR CAST(players.device_id AS TEXT) ILIKE ?))', '%123-and-456%', '%-or-2%', '%-not\_not\_1%'])}
+
+ # tests query with multiple search terms with mixed and-or-not after dash and underscore
+ it { expect(described_class.new('andrew or ornela', [:'players.title', :'players.tag', :'players.device_id'], :'players.device_id').where_clause).to eq(['(CAST(players.device_id AS TEXT) ILIKE ? OR CAST(players.device_id AS TEXT) ILIKE ?)', '%andrew%', '%ornela%'])}
end
end
diff --git a/spec/lib/text_to_tsquery_spec.rb b/spec/lib/text_to_tsquery_spec.rb
deleted file mode 100644
index 330899b..0000000
--- a/spec/lib/text_to_tsquery_spec.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-# frozen_string_literal: true
-
-describe TextToTsquery do
- describe '.new' do
- # partial match
- it { expect(described_class.new('A').tsquery).to eq('A:*') }
- it { expect(described_class.new('A', wildcard: false).tsquery).to eq('A:') }
- it { expect(described_class.new(' A ').tsquery).to eq('A:*') }
-
- # AND operations
- it { expect(described_class.new('A B').tsquery).to eq('A:*&B:*') }
- it { expect(described_class.new('A B C').tsquery).to eq('A:*&B:*&C:*') }
- it { expect(described_class.new('A and B').tsquery).to eq('A:*&B:*') }
- it { expect(described_class.new('A AND B').tsquery).to eq('A:*&B:*') }
- it { expect(described_class.new('A & B').tsquery).to eq('A:*&B:*') }
- it { expect(described_class.new('A && B').tsquery).to eq('A:*&B:*') }
- it { expect(described_class.new('A & B && C and D AND E F').tsquery).to eq('A:*&B:*&C:*&D:*&E:*&F:*') }
-
- # OR operations
- it { expect(described_class.new('A or B').tsquery).to eq('A:*|B:*') }
- it { expect(described_class.new('A or B', wildcard: false).tsquery).to eq('A:|B:') }
- it { expect(described_class.new('A OR B').tsquery).to eq('A:*|B:*') }
- it { expect(described_class.new('A OR B', wildcard: false).tsquery).to eq('A:|B:') }
- it { expect(described_class.new('A | B').tsquery).to eq('A:*|B:*') }
- it { expect(described_class.new('A | B', wildcard: false).tsquery).to eq('A:|B:') }
- it { expect(described_class.new('A || B').tsquery).to eq('A:*|B:*') }
- it { expect(described_class.new('A || B', wildcard: false).tsquery).to eq('A:|B:') }
- it { expect(described_class.new('A or or B').tsquery).to eq('A:*|B:*') }
- it { expect(described_class.new('A or or B', wildcard: false).tsquery).to eq('A:|B:') }
- it { expect(described_class.new('A | B || C or D OR E').tsquery).to eq('A:*|B:*|C:*|D:*|E:*') }
- it { expect(described_class.new('A | B || C or D OR E', wildcard: false).tsquery).to eq('A:|B:|C:|D:|E:') }
-
- # () Precedence
- it { expect(described_class.new('(A)').tsquery).to eq('(A:*)') }
- it { expect(described_class.new('(A)', wildcard: false).tsquery).to eq('(A:)') }
- it { expect(described_class.new('(A B)').tsquery).to eq('(A:*&B:*)') }
- it { expect(described_class.new('(A B)', wildcard: false).tsquery).to eq('(A:&B:)') }
- it { expect(described_class.new('A (B !C)').tsquery).to eq('A:*&(B:*&!C)') }
- it { expect(described_class.new('A (B !C)', wildcard: false).tsquery).to eq('A:&(B:&!C)') }
- it { expect(described_class.new('(A AND B) OR C').tsquery).to eq('(A:*&B:*)|C:*') }
- it { expect(described_class.new('(A AND B) OR C', wildcard: false).tsquery).to eq('(A:&B:)|C:') }
- it { expect(described_class.new('A AND (B OR C)').tsquery).to eq('A:*&(B:*|C:*)') }
- it { expect(described_class.new('A AND (B OR C)', wildcard: false).tsquery).to eq('A:&(B:|C:)') }
- it { expect(described_class.new('(A & B) || C').tsquery).to eq('(A:*&B:*)|C:*') }
- it { expect(described_class.new('(A & B) || C', wildcard: false).tsquery).to eq('(A:&B:)|C:') }
- it { expect(described_class.new('A && (B | C)').tsquery).to eq('A:*&(B:*|C:*)') }
- it { expect(described_class.new('A && (B | C)', wildcard: false).tsquery).to eq('A:&(B:|C:)') }
- it { expect(described_class.new('A && !D (B | C | !E)').tsquery).to eq('A:*&!D&(B:*|C:*|!E)') }
- it { expect(described_class.new('A && !D (B | C | !E)', wildcard: false).tsquery).to eq('A:&!D&(B:|C:|!E)') }
-
- # Exact Matches
- it { expect(described_class.new('"A"').tsquery).to eq("'A'") }
- it { expect(described_class.new('"A B"').tsquery).to eq("'A B'") }
- it { expect(described_class.new('"A&B"').tsquery).to eq("'A&B'") }
- it { expect(described_class.new('"-A|B"').tsquery).to eq("'-A|B'") }
- it { expect(described_class.new('"A-B"').tsquery).to eq("'A-B'") }
- it { expect(described_class.new('"A" B').tsquery).to eq("'A'&B:*") }
- it { expect(described_class.new('"A" B', wildcard: false).tsquery).to eq("'A'&B:") }
- it { expect(described_class.new('"A B" C').tsquery).to eq("'A B'&C:*") }
- it { expect(described_class.new('"A B" C', wildcard: false).tsquery).to eq("'A B'&C:") }
- it { expect(described_class.new('("A B" or C) and D').tsquery).to eq("('A B'|C:*)&D:*") }
- it { expect(described_class.new('("A B" or C) and D', wildcard: false).tsquery).to eq("('A B'|C:)&D:") }
-
- describe 'validations' do
- it { expect { described_class.new('(') }.to raise_error(ArgumentError, /parenthesis/) }
- end
- end
-
- describe '.valid_search_parenthesis?' do
- it { expect(described_class.valid_search_parenthesis?('')).to eq true }
- it { expect(described_class.valid_search_parenthesis?('()')).to eq true }
- it { expect(described_class.valid_search_parenthesis?('()()')).to eq true }
- it { expect(described_class.valid_search_parenthesis?('(()())')).to eq true }
- it { expect(described_class.valid_search_parenthesis?('((())())')).to eq true }
- it { expect(described_class.valid_search_parenthesis?('(')).to eq false }
- it { expect(described_class.valid_search_parenthesis?(')(')).to eq false }
- it { expect(described_class.valid_search_parenthesis?('())')).to eq false }
- it { expect(described_class.valid_search_parenthesis?('((()())')).to eq false }
- end
-end
diff --git a/spec/schema.rb b/spec/schema.rb
index 7cb1a78..0ddb2b9 100644
--- a/spec/schema.rb
+++ b/spec/schema.rb
@@ -42,11 +42,4 @@ ActiveRecord::Schema.define do
t.string :name
t.timestamps null: false
end
-
- create_table :players, force: true do |t|
- t.references :tag
- t.string :name
- t.string :value
- t.string :device_id
- end
end
diff --git a/spec/support/models.rb b/spec/support/models.rb
index f18beb4..58d632c 100755
--- a/spec/support/models.rb
+++ b/spec/support/models.rb
@@ -2,7 +2,13 @@
class VectorModel < ActiveRecord::Base
include PgSearchable
- pg_search fields: %i[id name value], cache: :search_cache
+ pg_search fields: %i[vector_models.id vector_models.name vector_models.value], cache: :search_cache
+end
+
+class VectorModelWithMappings < ActiveRecord::Base
+ self.table_name = :vector_models
+ include PgSearchable
+ pg_search fields: %i[vector_models.id vector_models.name vector_models.value], fields_mappings: {device_id: "vector_models.id"}, cache: :search_cache
end
class VectorModelWithoutCallback < ActiveRecord::Base
@@ -32,13 +38,7 @@ end
class SimpleVectorModel < ActiveRecord::Base
self.table_name = :vector_models
include PgSearchable
- pg_search fields: %i[id name value], cache: :search_cache, language: :simple
-end
-
-class SimplePlayerModel < ActiveRecord::Base
- self.table_name = :players
- include PgSearchable
- pg_search fields: %i[device_id name value tags], cache: :search_cache, language: :simple
+ pg_search fields: %i[vector_models.id vector_models.name vector_models.value], cache: :search_cache, language: :simple
end
class VectorWithoutWildcardModel < ActiveRecord::Base
@@ -72,7 +72,7 @@ end
class DynamicModelWithTagValues < ActiveRecord::Base
self.table_name = :dynamic_models
include PgSearchable
- pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value tags.value], joins: [:tags]
+ pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value], fields_mappings: {tag: 'tags.value'}, joins: [:tags]
has_many :tags, as: :taggable
end
diff --git a/version.rb b/version.rb
index f0966b2..7a7c437 100644
--- a/version.rb
+++ b/version.rb
@@ -3,7 +3,7 @@
module Version
MAJOR = 1
MINOR = 0
- PATCH = 24
+ PATCH = 27
def self.to_s
[MAJOR, MINOR, PATCH].compact.join('.')