Compare commits
36 Commits
generate-q
...
handle-joi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c4f3f3b58 | ||
|
|
c7b173cb79 | ||
|
|
6c9878ecea | ||
|
|
15fc342633 | ||
|
|
f8352dcaa1 | ||
|
|
e0fae50584 | ||
|
|
fe96f682db | ||
|
|
8f7b94f8cd | ||
|
|
0de540c5bd | ||
|
|
455845e36b | ||
|
|
f4bec85161 | ||
|
|
4e382d7c1a | ||
|
|
18f6875b54 | ||
|
|
a54b2586ec | ||
|
|
fc5fdd3db2 | ||
|
|
b62638a647 | ||
|
|
838b60ed92 | ||
|
|
afbfdb87cd | ||
|
|
3de6bd22d8 | ||
|
|
ff7cd0a21d | ||
|
|
893bbd3d97 | ||
|
|
c7ae81a533 | ||
|
|
94767d2b24 | ||
|
|
d307811f5f | ||
|
|
ebc21919bf | ||
|
|
f696a7cd20 | ||
|
|
fe7dcbcd6f | ||
|
|
21b736420b | ||
|
|
d96817e3f8 | ||
|
|
b2cdcfbd61 | ||
|
|
66c01702c8 | ||
|
|
b9bfa004c6 | ||
|
|
e5925c8209 | ||
|
|
c7ab6465e4 | ||
|
|
37ad466439 | ||
|
|
530951ff45 |
5
.document
Normal file
5
.document
Normal file
@@ -0,0 +1,5 @@
|
||||
lib/**/*.rb
|
||||
bin/*
|
||||
-
|
||||
features/**/*.feature
|
||||
LICENSE.txt
|
||||
7
.env.test
Normal file
7
.env.test
Normal file
@@ -0,0 +1,7 @@
|
||||
# Postgres
|
||||
DATABASE_NAME=pg_searchable_test
|
||||
DATABASE_HOST=
|
||||
DATABASE_PORT=5432
|
||||
DATABASE_POOL=10
|
||||
DATABASE_USERNAME=postgres
|
||||
DATABASE_PASSWORD=
|
||||
53
.gitignore
vendored
Normal file
53
.gitignore
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
# rcov generated
|
||||
coverage
|
||||
coverage.data
|
||||
|
||||
# rdoc generated
|
||||
rdoc
|
||||
|
||||
# yard generated
|
||||
doc
|
||||
.yardoc
|
||||
|
||||
# bundler
|
||||
.bundle
|
||||
.env.local
|
||||
.env.test.local
|
||||
*.gem
|
||||
*.gemspec
|
||||
|
||||
# juwelier generated
|
||||
pkg
|
||||
|
||||
# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
|
||||
#
|
||||
# * Create a file at ~/.gitignore
|
||||
# * Include files you want ignored
|
||||
# * Run: git config --global core.excludesfile ~/.gitignore
|
||||
#
|
||||
# After doing this, these files will be ignored in all your git projects,
|
||||
# saving you from having to 'pollute' every project you touch with them
|
||||
#
|
||||
# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
|
||||
#
|
||||
# For MacOS:
|
||||
#
|
||||
#.DS_Store
|
||||
|
||||
# For TextMate
|
||||
#*.tmproj
|
||||
#tmtags
|
||||
|
||||
# For emacs:
|
||||
#*~
|
||||
#\#*
|
||||
#.\#*
|
||||
|
||||
# For vim:
|
||||
#*.swp
|
||||
|
||||
# For redcar:
|
||||
#.redcar
|
||||
|
||||
# For rubinius:
|
||||
#*.rbc
|
||||
31
.rubocop.yml
Normal file
31
.rubocop.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
require: rubocop-rspec
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.1
|
||||
|
||||
Layout/AccessModifierIndentation:
|
||||
EnforcedStyle: outdent
|
||||
|
||||
Layout/EmptyLinesAroundAccessModifier:
|
||||
Enabled: false
|
||||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
||||
Metrics/LineLength:
|
||||
Max: 120
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 15
|
||||
|
||||
Metrics/ParameterLists:
|
||||
Enabled: false
|
||||
|
||||
Metrics/BlockLength:
|
||||
Enabled: false
|
||||
|
||||
Style/StringLiterals:
|
||||
EnforcedStyle: single_quotes
|
||||
|
||||
RSpec/NestedGroups:
|
||||
Max: 5
|
||||
1
.ruby-version
Normal file
1
.ruby-version
Normal file
@@ -0,0 +1 @@
|
||||
2.1.5
|
||||
2
Gemfile
2
Gemfile
@@ -23,4 +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
|
||||
end
|
||||
|
||||
@@ -76,11 +76,13 @@ GEM
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.9.0)
|
||||
public_suffix (3.1.1)
|
||||
racc (1.4.16)
|
||||
rack (1.6.11)
|
||||
rainbow (3.0.0)
|
||||
rake (12.3.2)
|
||||
rdoc (3.12.2)
|
||||
json (~> 1.4)
|
||||
rexical (1.0.7)
|
||||
rspec (3.8.0)
|
||||
rspec-core (~> 3.8.0)
|
||||
rspec-expectations (~> 3.8.0)
|
||||
@@ -132,7 +134,9 @@ DEPENDENCIES
|
||||
juwelier (~> 2.1)
|
||||
pg (~> 0.15)
|
||||
pry (~> 0.12)
|
||||
racc (~> 1.4)
|
||||
rdoc (~> 3.12)
|
||||
rexical (~> 1.0)
|
||||
rspec (~> 3.8)
|
||||
rubocop (~> 0.54)
|
||||
rubocop-rspec (~> 1.23)
|
||||
|
||||
@@ -119,3 +119,11 @@ To run the test suite create `.env.test.local` file containing the same entries
|
||||
## CONTRIBUTING
|
||||
|
||||
Make sure the test coverage remains at 100%, there are no rubocop complaints (`bundle exec rubocop`) and make a Pull Request.
|
||||
|
||||
|
||||
## Modifying parser and lexer
|
||||
|
||||
* `rake lexer` - generates `lexer.rb` file based on `specification.rex` file
|
||||
* `rake parser` - generates `parser.rb` file based on `grammar.y` file
|
||||
* `rake generate` - generates both `lexer.rb` and `parser.rb` files
|
||||
|
||||
|
||||
13
Rakefile
13
Rakefile
@@ -44,3 +44,16 @@ Rake::RDocTask.new do |rdoc|
|
||||
rdoc.rdoc_files.include('README*')
|
||||
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||
end
|
||||
|
||||
desc 'Generate Lexer'
|
||||
task :lexer do
|
||||
`rex ./lib/specification.rex -o ./lib/lexer.rb`
|
||||
end
|
||||
|
||||
desc 'Generate Parser'
|
||||
task :parser do
|
||||
`racc ./lib/grammar.y -o ./lib/parser.rb`
|
||||
end
|
||||
|
||||
desc 'Generate Lexer and Parser'
|
||||
task generate: %i[lexer parser]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": {
|
||||
"covered_percent": 66.92
|
||||
"covered_percent": 78.1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
12,
|
||||
13,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@@ -32,27 +32,31 @@
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
12,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
12,
|
||||
43,
|
||||
13,
|
||||
13,
|
||||
24,
|
||||
23,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
31,
|
||||
30,
|
||||
24,
|
||||
null,
|
||||
24,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
@@ -60,11 +64,11 @@
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
16,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
12,
|
||||
13,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
@@ -180,75 +184,420 @@
|
||||
null,
|
||||
null
|
||||
],
|
||||
"/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/text_to_regex_query.rb": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
1,
|
||||
35,
|
||||
35,
|
||||
35,
|
||||
35,
|
||||
37,
|
||||
37,
|
||||
7,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
null,
|
||||
null,
|
||||
"/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/text_to_sql_query.rb": [
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
5,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
5,
|
||||
5,
|
||||
5,
|
||||
44,
|
||||
44,
|
||||
44,
|
||||
44,
|
||||
114,
|
||||
114,
|
||||
114,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
5,
|
||||
null,
|
||||
5,
|
||||
7,
|
||||
7,
|
||||
5,
|
||||
5,
|
||||
null,
|
||||
5,
|
||||
3,
|
||||
null,
|
||||
5,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
5,
|
||||
13,
|
||||
44,
|
||||
13,
|
||||
null,
|
||||
5,
|
||||
8,
|
||||
null,
|
||||
5,
|
||||
null,
|
||||
5,
|
||||
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,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
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,
|
||||
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,
|
||||
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": 1579269179
|
||||
"timestamp": 1583928139
|
||||
}
|
||||
}
|
||||
|
||||
2740
coverage/index.html
2740
coverage/index.html
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,9 @@ class Query
|
||||
| TERM_WITHOUT_QUOTES COLON TERM_WITH_QUOTES { result = {val[0] => val[2]} }
|
||||
| expression OPERATOR_OR expression { result = {:OPERATOR_OR => [val[0], val[2]]} }
|
||||
| expression OPERATOR_AND expression { result = {:OPERATOR_AND => [val[0], val[2]]} }
|
||||
| OPERATOR_NOT expression { result = {:OPERATOR_NOT => val[1]} }
|
||||
| L_BRACKET expression R_BRACKET { result = val[1] }
|
||||
| expression expression { result = {:OPERATOR_OR => [val[0], val[1]]} }
|
||||
end
|
||||
|
||||
---- header
|
||||
|
||||
107
lib/lexer.rb
Normal file
107
lib/lexer.rb
Normal file
@@ -0,0 +1,107 @@
|
||||
#--
|
||||
# DO NOT MODIFY!!!!
|
||||
# This file is automatically generated by rex 1.0.7
|
||||
# from lexical definition file "./lib/specification.rex".
|
||||
#++
|
||||
|
||||
require 'racc/parser'
|
||||
class Query < Racc::Parser
|
||||
require 'strscan'
|
||||
|
||||
class ScanError < StandardError ; end
|
||||
|
||||
attr_reader :lineno
|
||||
attr_reader :filename
|
||||
attr_accessor :state
|
||||
|
||||
def scan_setup(str)
|
||||
@ss = StringScanner.new(str)
|
||||
@lineno = 1
|
||||
@state = nil
|
||||
end
|
||||
|
||||
def action
|
||||
yield
|
||||
end
|
||||
|
||||
def scan_str(str)
|
||||
scan_setup(str)
|
||||
do_parse
|
||||
end
|
||||
alias :scan :scan_str
|
||||
|
||||
def load_file( filename )
|
||||
@filename = filename
|
||||
File.open(filename, "r") do |f|
|
||||
scan_setup(f.read)
|
||||
end
|
||||
end
|
||||
|
||||
def scan_file( filename )
|
||||
load_file(filename)
|
||||
do_parse
|
||||
end
|
||||
|
||||
|
||||
def next_token
|
||||
return if @ss.eos?
|
||||
|
||||
# skips empty actions
|
||||
until token = _next_token or @ss.eos?; end
|
||||
token
|
||||
end
|
||||
|
||||
def _next_token
|
||||
text = @ss.peek(1)
|
||||
@lineno += 1 if text == "\n"
|
||||
token = case @state
|
||||
when nil
|
||||
case
|
||||
when (text = @ss.scan(/ +/))
|
||||
;
|
||||
|
||||
when (text = @ss.scan(/\(/))
|
||||
action { return [:L_BRACKET, text] }
|
||||
|
||||
when (text = @ss.scan(/\)/))
|
||||
action { return [:R_BRACKET, text] }
|
||||
|
||||
when (text = @ss.scan(/(?i)\bor\b/))
|
||||
action { return [:OPERATOR_OR, text] }
|
||||
|
||||
when (text = @ss.scan(/(?i)\band\b/))
|
||||
action { return [:OPERATOR_AND, text] }
|
||||
|
||||
when (text = @ss.scan(/(?i)\bnot\b/))
|
||||
action { return [:OPERATOR_NOT, text] }
|
||||
|
||||
when (text = @ss.scan(/"([^"]*)"/))
|
||||
action { return [:TERM_WITH_QUOTES, text] }
|
||||
|
||||
when (text = @ss.scan(/[a-zA-Z0-9\-_]+/))
|
||||
action { return [:TERM_WITHOUT_QUOTES, text] }
|
||||
|
||||
when (text = @ss.scan(/\:/))
|
||||
action { return [:COLON, text] }
|
||||
|
||||
|
||||
else
|
||||
text = @ss.string[@ss.pos .. -1]
|
||||
raise ScanError, "can not match: '" + text + "'"
|
||||
end # if
|
||||
|
||||
else
|
||||
raise ScanError, "undefined state: '" + state.to_s + "'"
|
||||
end # case state
|
||||
token
|
||||
end # def _next_token
|
||||
|
||||
def tokenize(code)
|
||||
scan_setup(code)
|
||||
tokens = []
|
||||
while token = next_token
|
||||
tokens << token
|
||||
end
|
||||
tokens
|
||||
end
|
||||
end # class
|
||||
4
lib/parser-parser-part/.gitignore
vendored
4
lib/parser-parser-part/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
.idea
|
||||
|
||||
lexer.rb
|
||||
parser.rb
|
||||
@@ -1,19 +0,0 @@
|
||||
# parser
|
||||
|
||||
### Prerequisites
|
||||
|
||||
* Rexical (rex)
|
||||
* Racc
|
||||
|
||||
### Available commands
|
||||
|
||||
* `rake lexer` - generates `lexer.rb` file based on `specification.rex` file
|
||||
* `rake parser` - generates `parser.rb` file based on `grammar.y` file
|
||||
* `rake generate` - generates `lexer.rb` and `parser.rb` files
|
||||
|
||||
### Testing
|
||||
|
||||
To run only `lexer` tests, execute : `rspec spec/query_lexer_spec.rb`
|
||||
To run only `parser` tests, execute : `rspec spec/query_parser_spec.rb`
|
||||
|
||||
To run all tests, execute : `rake spec`
|
||||
@@ -1,20 +0,0 @@
|
||||
require 'rspec/core/rake_task'
|
||||
|
||||
RSpec::Core::RakeTask.new do |c|
|
||||
options = ['--color']
|
||||
options += %w[--format documentation]
|
||||
c.rspec_opts = options
|
||||
end
|
||||
|
||||
desc 'Generate Lexer'
|
||||
task :lexer do
|
||||
`rex specification.rex -o lexer.rb`
|
||||
end
|
||||
|
||||
desc 'Generate Parser'
|
||||
task :parser do
|
||||
`racc grammar.y -o parser.rb`
|
||||
end
|
||||
|
||||
desc 'Generate Lexer and Parser'
|
||||
task generate: %i[lexer parser]
|
||||
@@ -1,26 +0,0 @@
|
||||
class Query
|
||||
prechigh
|
||||
left OPERATOR_NOT
|
||||
left OPERATOR_AND
|
||||
left OPERATOR_OR
|
||||
preclow
|
||||
rule
|
||||
target: expression
|
||||
| /* none */ { result = 0 }
|
||||
|
||||
expression: TERM_WITHOUT_QUOTES { result = {:DEFAULT_COLUMN => val[0]} }
|
||||
| TERM_WITH_QUOTES { result = {:DEFAULT_COLUMN => val[0]} }
|
||||
| TERM_WITHOUT_QUOTES COLON TERM_WITHOUT_QUOTES { result = {val[0] => val[2]} }
|
||||
| TERM_WITHOUT_QUOTES COLON TERM_WITH_QUOTES { result = {val[0] => val[2]} }
|
||||
| expression OPERATOR_OR expression { result = {:OPERATOR_OR => [val[0], val[2]]} }
|
||||
| expression OPERATOR_AND expression { result = {:OPERATOR_AND => [val[0], val[2]]} }
|
||||
| L_BRACKET expression R_BRACKET { result = val[1] }
|
||||
end
|
||||
|
||||
---- header
|
||||
require_relative 'lexer'
|
||||
|
||||
---- inner
|
||||
def parse(input)
|
||||
scan_str(input)
|
||||
end
|
||||
@@ -1,259 +0,0 @@
|
||||
require './lexer'
|
||||
|
||||
class QueryLexerTester
|
||||
describe 'Testing the Lexer' do
|
||||
before do
|
||||
@evaluator = Query.new
|
||||
end
|
||||
|
||||
it 'tests bracket expression' do
|
||||
@result = @evaluator.tokenize('()')
|
||||
expect(@result.length).to eq 2
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests bracket expression with spaces' do
|
||||
@result = @evaluator.tokenize(' ( ) ')
|
||||
expect(@result.length).to eq 2
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests expression with OR operator' do
|
||||
@result = @evaluator.tokenize('() or () OR ()')
|
||||
expect(@result.length).to eq 8
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
expect(@result[2][0]).to eq :OPERATOR_OR
|
||||
expect(@result[3][0]).to eq :L_BRACKET
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
expect(@result[5][0]).to eq :OPERATOR_OR
|
||||
expect(@result[6][0]).to eq :L_BRACKET
|
||||
expect(@result[7][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests expression with AND operator' do
|
||||
@result = @evaluator.tokenize('() AND () and ()')
|
||||
expect(@result.length).to eq 8
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
expect(@result[2][0]).to eq :OPERATOR_AND
|
||||
expect(@result[3][0]).to eq :L_BRACKET
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
expect(@result[5][0]).to eq :OPERATOR_AND
|
||||
expect(@result[6][0]).to eq :L_BRACKET
|
||||
expect(@result[7][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests expression with NOT OR and NOT AND operator' do
|
||||
@result = @evaluator.tokenize('() NOT or () not AND ()')
|
||||
expect(@result.length).to eq 10
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :R_BRACKET
|
||||
expect(@result[2][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[3][0]).to eq :OPERATOR_OR
|
||||
expect(@result[4][0]).to eq :L_BRACKET
|
||||
expect(@result[5][0]).to eq :R_BRACKET
|
||||
expect(@result[6][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[7][0]).to eq :OPERATOR_AND
|
||||
expect(@result[8][0]).to eq :L_BRACKET
|
||||
expect(@result[9][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests search term under quotes' do
|
||||
@result = @evaluator.tokenize('"123-456"')
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[0][1]).to eq '"123-456"'
|
||||
end
|
||||
|
||||
it 'tests term without quotes' do
|
||||
@result = @evaluator.tokenize('device_id')
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'device_id'
|
||||
end
|
||||
|
||||
it 'tests integer term without quotes' do
|
||||
@result = @evaluator.tokenize('123')
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq '123'
|
||||
end
|
||||
|
||||
it 'tests multiple terms without quotes' do
|
||||
@result = @evaluator.tokenize('device_id tag 123-456 name123')
|
||||
|
||||
expect(@result.length).to eq 4
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'device_id'
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'tag'
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq '123-456'
|
||||
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[3][1]).to eq 'name123'
|
||||
end
|
||||
|
||||
it 'tests simple query with column name and search term without quotes' do
|
||||
@result = @evaluator.tokenize('name:JF')
|
||||
|
||||
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 'JF'
|
||||
end
|
||||
|
||||
it 'tests simple query with two columns with name and search terms without quotes' do
|
||||
@result = @evaluator.tokenize('name:JF tag:mta')
|
||||
|
||||
expect(@result.length).to eq 6
|
||||
|
||||
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 'JF'
|
||||
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[3][1]).to eq 'tag'
|
||||
expect(@result[4][0]).to eq :COLON
|
||||
expect(@result[5][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[5][1]).to eq 'mta'
|
||||
|
||||
end
|
||||
|
||||
it 'tests simple query with column name and search term with quotes' do
|
||||
@result = @evaluator.tokenize('name:"name with space"')
|
||||
|
||||
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 '"name with space"'
|
||||
end
|
||||
|
||||
it 'tests search term with quotes containing non alphanumerical characters' do
|
||||
@result = @evaluator.tokenize('"|*|/\()#-!=<>&$"')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[0][1]).to eq '"|*|/\()#-!=<>&$"'
|
||||
end
|
||||
|
||||
it 'tests simple query in brackets' do
|
||||
@result = @evaluator.tokenize('(name:"name with space")')
|
||||
|
||||
expect(@result.length).to eq 5
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'name'
|
||||
expect(@result[2][0]).to eq :COLON
|
||||
expect(@result[3][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[3][1]).to eq '"name with space"'
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests multiple query wtih brackets' do
|
||||
@result = @evaluator.tokenize('(name:"name with space") or (tag:mta)')
|
||||
|
||||
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 'name'
|
||||
expect(@result[2][0]).to eq :COLON
|
||||
expect(@result[3][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[3][1]).to eq '"name with space"'
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
expect(@result[5][0]).to eq :OPERATOR_OR
|
||||
expect(@result[6][0]).to eq :L_BRACKET
|
||||
expect(@result[7][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[7][1]).to eq 'tag'
|
||||
expect(@result[8][0]).to eq :COLON
|
||||
expect(@result[9][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[9][1]).to eq 'mta'
|
||||
expect(@result[10][0]).to eq :R_BRACKET
|
||||
|
||||
end
|
||||
|
||||
it 'tests complex query' do
|
||||
@result = @evaluator.tokenize('(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)')
|
||||
|
||||
expect(@result.length).to eq 27
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'device-id'
|
||||
expect(@result[2][0]).to eq :COLON
|
||||
expect(@result[3][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[3][1]).to eq '"with space"'
|
||||
expect(@result[4][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[4][1]).to eq 'tag'
|
||||
expect(@result[5][0]).to eq :COLON
|
||||
expect(@result[6][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[6][1]).to eq 'mta'
|
||||
expect(@result[7][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[7][1]).to eq 'no-quotes-id-123'
|
||||
expect(@result[8][0]).to eq :R_BRACKET
|
||||
|
||||
expect(@result[9][0]).to eq :OPERATOR_OR
|
||||
expect(@result[10][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[10][1]).to eq '"id with quotes-5"'
|
||||
expect(@result[11][0]).to eq :OPERATOR_AND
|
||||
|
||||
expect(@result[12][0]).to eq :L_BRACKET
|
||||
expect(@result[13][0]).to eq :L_BRACKET
|
||||
expect(@result[14][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[14][1]).to eq '"id with q 10"'
|
||||
expect(@result[15][0]).to eq :OPERATOR_OR
|
||||
expect(@result[16][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[16][1]).to eq '"id with q 20"'
|
||||
expect(@result[17][0]).to eq :R_BRACKET
|
||||
|
||||
expect(@result[18][0]).to eq :OPERATOR_AND
|
||||
expect(@result[19][0]).to eq :L_BRACKET
|
||||
expect(@result[20][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[20][1]).to eq '"id with Q 30"'
|
||||
expect(@result[21][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[21][1]).to eq '"id with Q 40"'
|
||||
expect(@result[22][0]).to eq :R_BRACKET
|
||||
|
||||
expect(@result[23][0]).to eq :OPERATOR_AND
|
||||
expect(@result[24][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[25][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[25][1]).to eq 'id-without-Q-50'
|
||||
expect(@result[26][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests query with -or-, -and- and -not- words inside quoted expression' do
|
||||
@result = @evaluator.tokenize('tag:"tag with or and not inside"')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'tag'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[2][1]).to eq '"tag with or and not inside"'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,164 +0,0 @@
|
||||
require './parser'
|
||||
|
||||
class QueryParserTester
|
||||
describe 'Testing the Parser' do
|
||||
before do
|
||||
@evaluator = Query.new
|
||||
end
|
||||
|
||||
it 'tests query with only one search term without quotes and without column name' do
|
||||
@result = @evaluator.parse('-123')
|
||||
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '-123'
|
||||
end
|
||||
|
||||
it 'tests query with only one search term with quotes and without column name' do
|
||||
@result = @evaluator.parse('"OR 128"')
|
||||
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '"OR 128"'
|
||||
end
|
||||
|
||||
it 'tests query with one column and search term without quotes' do
|
||||
@result = @evaluator.parse('tag:mta')
|
||||
|
||||
expect(@result['tag']).to eq 'mta'
|
||||
end
|
||||
|
||||
it 'tests query with one column and search term with quotes' do
|
||||
@result = @evaluator.parse('tag:"tag 120"')
|
||||
|
||||
expect(@result['tag']).to eq '"tag 120"'
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with OR and search terms without quotes' do
|
||||
@result = @evaluator.parse('tag:mta OR tag:12')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '12' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with OR and search terms with quotes' do
|
||||
@result = @evaluator.parse('tag:mta OR tag:"tag 12"')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '"tag 12"' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with AND and search terms without quotes' do
|
||||
@result = @evaluator.parse('tag:mta AND tag:12')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '12' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two columns connected with AND and search terms with quotes' do
|
||||
@result = @evaluator.parse('tag:mta and tag:"tag 12"')
|
||||
|
||||
@expected_array = [
|
||||
{ 'tag' => 'mta' },
|
||||
{ 'tag' => '"tag 12"' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests simple query with brackets' do
|
||||
@result = @evaluator.parse('(123)')
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '123'
|
||||
end
|
||||
|
||||
it 'tests simple query with brackets and with a column name' do
|
||||
@result = @evaluator.parse('(name:JF)')
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result['name']).to eq 'JF'
|
||||
end
|
||||
|
||||
it 'tests query with OR operator in brackets' do
|
||||
@result = @evaluator.parse('(name:JF or tag:mta)')
|
||||
|
||||
@expected_array = [
|
||||
{ 'name' => 'JF' },
|
||||
{ 'tag' => 'mta' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two simple brackets expressions' do
|
||||
@result = @evaluator.parse('(name:JF) and (-456)')
|
||||
|
||||
@expected_array = [
|
||||
{ 'name' => 'JF' },
|
||||
{ :DEFAULT_COLUMN => '-456' }
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two brackets expressions' do
|
||||
@result = @evaluator.parse('(name:JF or tag:"tag 0") and (-456)')
|
||||
|
||||
@expected_array_part_1 = [
|
||||
{ 'name' => 'JF' },
|
||||
{ 'tag' => '"tag 0"' }
|
||||
]
|
||||
|
||||
@expected_array_total = [
|
||||
{:OPERATOR_OR => @expected_array_part_1},
|
||||
{:DEFAULT_COLUMN => '-456'}
|
||||
]
|
||||
|
||||
expect(@result.count).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array_total
|
||||
end
|
||||
|
||||
it 'tests operator precedence' do
|
||||
@result1 = @evaluator.parse('tag:mta or name:JF and 12_4')
|
||||
@result2 = @evaluator.parse('tag:mta or (name:JF and 12_4)')
|
||||
|
||||
expect(@result1).to eq @result2
|
||||
|
||||
expect(@result1.length).to eq 1
|
||||
|
||||
@expected_array_part_2 = [
|
||||
{'name' => 'JF'},
|
||||
{:DEFAULT_COLUMN => '12_4'}
|
||||
]
|
||||
|
||||
@expected_array_total = [
|
||||
{'tag' => 'mta'},
|
||||
{:OPERATOR_AND => @expected_array_part_2}
|
||||
]
|
||||
|
||||
expect(@result1[:OPERATOR_OR]).to eq @expected_array_total
|
||||
|
||||
end
|
||||
|
||||
# Tests to write :
|
||||
# * query with multiple column names and search terms without logical operators
|
||||
# * AND NOT, OR NOT tests
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,35 +0,0 @@
|
||||
class Query
|
||||
macro
|
||||
L_BRACKET \(
|
||||
R_BRACKET \)
|
||||
SPACE \ + # Space char
|
||||
OPERATOR_OR (?i)or
|
||||
OPERATOR_AND (?i)and
|
||||
OPERATOR_NOT (?i)not
|
||||
TERM_WITH_QUOTES "([^"]*)"
|
||||
TERM_WITHOUT_QUOTES [a-zA-Z0-9-_]+
|
||||
COLON \:
|
||||
|
||||
|
||||
|
||||
rule
|
||||
{SPACE} # No action
|
||||
{L_BRACKET} { return [:L_BRACKET, text] }
|
||||
{R_BRACKET} { return [:R_BRACKET, text] }
|
||||
{OPERATOR_OR} { return [:OPERATOR_OR, text] }
|
||||
{OPERATOR_AND} { return [:OPERATOR_AND, text] }
|
||||
{OPERATOR_NOT} { return [:OPERATOR_NOT, text] }
|
||||
{TERM_WITH_QUOTES} { return [:TERM_WITH_QUOTES, text] }
|
||||
{TERM_WITHOUT_QUOTES} { return [:TERM_WITHOUT_QUOTES, text] }
|
||||
{COLON} { return [:COLON, text] }
|
||||
|
||||
inner
|
||||
def tokenize(code)
|
||||
scan_setup(code)
|
||||
tokens = []
|
||||
while token = next_token
|
||||
tokens << token
|
||||
end
|
||||
tokens
|
||||
end
|
||||
end
|
||||
207
lib/parser.rb
Normal file
207
lib/parser.rb
Normal file
@@ -0,0 +1,207 @@
|
||||
#
|
||||
# DO NOT MODIFY!!!!
|
||||
# This file is automatically generated by Racc 1.4.16
|
||||
# from Racc grammar file "".
|
||||
#
|
||||
|
||||
require 'racc/parser.rb'
|
||||
|
||||
require_relative 'lexer'
|
||||
|
||||
class Query < Racc::Parser
|
||||
|
||||
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 ###
|
||||
|
||||
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 ]
|
||||
|
||||
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 ]
|
||||
|
||||
racc_action_pointer = [
|
||||
3, 10, 10, 12, nil, 15, 20, 24, 27, 32,
|
||||
37, 57, 53, -2, nil, 44, 49, nil, nil, nil ]
|
||||
|
||||
racc_action_default = [
|
||||
-2, -12, -1, -3, -4, -12, -12, -12, -11, -12,
|
||||
-12, -12, -9, -12, 20, -7, -8, -5, -6, -10 ]
|
||||
|
||||
racc_goto_table = [
|
||||
2, 1, nil, nil, nil, 12, 13, nil, nil, 15,
|
||||
16 ]
|
||||
|
||||
racc_goto_check = [
|
||||
2, 1, nil, nil, nil, 2, 2, nil, nil, 2,
|
||||
2 ]
|
||||
|
||||
racc_goto_pointer = [
|
||||
nil, 1, 0 ]
|
||||
|
||||
racc_goto_default = [
|
||||
nil, nil, 8 ]
|
||||
|
||||
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 ]
|
||||
|
||||
racc_reduce_n = 12
|
||||
|
||||
racc_shift_n = 20
|
||||
|
||||
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 }
|
||||
|
||||
racc_nt_base = 10
|
||||
|
||||
racc_use_result_var = true
|
||||
|
||||
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 ]
|
||||
|
||||
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" ]
|
||||
|
||||
Racc_debug_parser = false
|
||||
|
||||
##### State transition tables end #####
|
||||
|
||||
# reduce 0 omitted
|
||||
|
||||
# reduce 1 omitted
|
||||
|
||||
module_eval(<<'.,.,', 'grammar.y', 8)
|
||||
def _reduce_2(val, _values, result)
|
||||
result = 0
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
module_eval(<<'.,.,', 'grammar.y', 10)
|
||||
def _reduce_3(val, _values, result)
|
||||
result = {:DEFAULT_COLUMN => val[0]}
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
module_eval(<<'.,.,', 'grammar.y', 11)
|
||||
def _reduce_4(val, _values, result)
|
||||
result = {:DEFAULT_COLUMN => val[0]}
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
module_eval(<<'.,.,', 'grammar.y', 12)
|
||||
def _reduce_5(val, _values, result)
|
||||
result = {val[0] => val[2]}
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
module_eval(<<'.,.,', 'grammar.y', 13)
|
||||
def _reduce_6(val, _values, result)
|
||||
result = {val[0] => val[2]}
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
module_eval(<<'.,.,', 'grammar.y', 14)
|
||||
def _reduce_7(val, _values, result)
|
||||
result = {:OPERATOR_OR => [val[0], val[2]]}
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
module_eval(<<'.,.,', 'grammar.y', 15)
|
||||
def _reduce_8(val, _values, result)
|
||||
result = {:OPERATOR_AND => [val[0], val[2]]}
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
module_eval(<<'.,.,', 'grammar.y', 16)
|
||||
def _reduce_9(val, _values, result)
|
||||
result = {:OPERATOR_NOT => val[1]}
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
module_eval(<<'.,.,', 'grammar.y', 17)
|
||||
def _reduce_10(val, _values, result)
|
||||
result = val[1]
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
module_eval(<<'.,.,', 'grammar.y', 18)
|
||||
def _reduce_11(val, _values, result)
|
||||
result = {:OPERATOR_OR => [val[0], val[1]]}
|
||||
result
|
||||
end
|
||||
.,.,
|
||||
|
||||
def _reduce_none(val, _values, result)
|
||||
val[0]
|
||||
end
|
||||
|
||||
end # class Query
|
||||
@@ -3,7 +3,7 @@
|
||||
require 'active_support'
|
||||
require 'squeel'
|
||||
require_relative './text_to_tsquery'
|
||||
require_relative './text_to_regex_query'
|
||||
require_relative './text_to_sql_query'
|
||||
|
||||
module PgSearchable
|
||||
extend ActiveSupport::Concern
|
||||
@@ -26,7 +26,7 @@ module PgSearchable
|
||||
wildcard: true,
|
||||
external_cache_data: nil,
|
||||
joins: [],
|
||||
default_field: ""
|
||||
default_fields: []
|
||||
)
|
||||
@ts_search_fields = fields
|
||||
@ts_search_fields_mappings = fields_mappings
|
||||
@@ -36,19 +36,35 @@ module PgSearchable
|
||||
@ts_skip_cache_update = skip_callback
|
||||
@ts_wildcard = wildcard
|
||||
@ts_joins = joins
|
||||
@default_field = (default_field.to_sym || fields.first)
|
||||
@default_fields = if default_fields.is_a? Array
|
||||
default_fields.empty? ? [fields.first] : default_fields
|
||||
else
|
||||
default_fields.to_s.empty? ? [fields.first] : [default_fields.to_sym]
|
||||
end
|
||||
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).rows.map { |row| row[0] }
|
||||
where(id: resulting_ids)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ts_search(value)
|
||||
return if @ts_search_fields.blank? || value.blank?
|
||||
TextToRegexQuery.new(value, @ts_search_fields, @default_field, @ts_search_fields_mappings).where_clause( includes(@ts_joins).references(:all))
|
||||
sql_query_object = TextToSqlQuery.new(
|
||||
value,
|
||||
@ts_search_fields,
|
||||
@default_fields,
|
||||
@ts_search_fields_mappings,
|
||||
@ts_joins
|
||||
)
|
||||
sql_query = select(:id).distinct.joins(sql_query_object.join_clause).group(:id).having(sql_query_object.where_clause)
|
||||
modified_sql_query = sql_query.to_sql.gsub('INNER', 'LEFT OUTER')
|
||||
ActiveRecord::Base.connection.exec_query(modified_sql_query)
|
||||
end
|
||||
|
||||
def should_update_cache_field?
|
||||
|
||||
@@ -3,11 +3,11 @@ 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-_]+
|
||||
TERM_WITHOUT_QUOTES [a-zA-Z0-9\-_]+
|
||||
COLON \:
|
||||
|
||||
|
||||
@@ -32,4 +32,4 @@ inner
|
||||
end
|
||||
tokens
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
114
lib/text_to_sql_query.rb
Normal file
114
lib/text_to_sql_query.rb
Normal file
@@ -0,0 +1,114 @@
|
||||
require_relative 'parser'
|
||||
|
||||
class TextToSqlQuery
|
||||
def initialize(text, fields, default_fields, fields_mappings = {}, joins = [])
|
||||
@text = text.to_s.strip
|
||||
@fields = fields.map(&:to_sym)
|
||||
|
||||
# Keep compatibility with previous version where default_field(s) was string/symbol
|
||||
@default_fields = if default_fields.is_a? Array
|
||||
default_fields.map(&:to_sym)
|
||||
else
|
||||
[default_fields.to_sym]
|
||||
end
|
||||
|
||||
@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)
|
||||
fields_mappings.each do |field, value|
|
||||
@fields_mappings[field] = value if @fields_mappings[field]
|
||||
end
|
||||
@joins = joins
|
||||
end
|
||||
|
||||
def where_clause
|
||||
@parser = Query.new
|
||||
@parsed_tree = @parser.parse(@text)
|
||||
generate_sql @parsed_tree
|
||||
end
|
||||
|
||||
def join_clause
|
||||
return nil if @joins.empty?
|
||||
return *@joins
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_sql(tree)
|
||||
first_key = tree.keys.first
|
||||
node_value = tree[first_key]
|
||||
case first_key
|
||||
when :DEFAULT_COLUMN
|
||||
escaped_node_value = handle_special_chars node_value
|
||||
query_part = '('
|
||||
params_part = []
|
||||
@default_fields.each do |default_field|
|
||||
query_part += "CAST(#{default_field.to_s} AS TEXT) ILIKE ? OR "
|
||||
params_part << "%#{escaped_node_value}%"
|
||||
end
|
||||
query_part = query_part[0...-4] + ')'
|
||||
[query_part, *params_part]
|
||||
when :OPERATOR_OR
|
||||
generate_expression_for_logical_operator(:OR, node_value)
|
||||
when :OPERATOR_AND
|
||||
generate_expression_for_logical_operator(:AND, node_value)
|
||||
when :OPERATOR_NOT
|
||||
not_array = generate_sql node_value
|
||||
|
||||
if not_array.length < 2
|
||||
raise "There should be more than 1 element for expression following NOT operator"
|
||||
end
|
||||
|
||||
not_expression = not_array.shift
|
||||
not_params = not_array
|
||||
|
||||
["NOT #{not_expression}"] + not_params
|
||||
|
||||
else
|
||||
# key is column name
|
||||
escaped_node_value = handle_special_chars node_value
|
||||
mapping = @fields_mappings[first_key.to_sym]
|
||||
if mapping.nil?
|
||||
raise "Unknown field '#{first_key.to_s}'"
|
||||
else
|
||||
["STRING_AGG(CAST(#{mapping.to_s} AS TEXT), '') ILIKE ?", "%#{escaped_node_value}%"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def generate_expression_for_logical_operator(operator, operator_array)
|
||||
if operator_array.length != 2
|
||||
raise "There should be two array elements for #{operator.to_s} operator"
|
||||
end
|
||||
|
||||
first_operand = generate_sql operator_array.first
|
||||
second_operand = generate_sql operator_array.last
|
||||
|
||||
if first_operand.length < 2
|
||||
raise 'There should be more than 1 element in first operand array'
|
||||
end
|
||||
|
||||
if second_operand.length < 2
|
||||
raise 'There should be more than 1 element in second operand array'
|
||||
end
|
||||
|
||||
first_operand_expression = first_operand.shift
|
||||
first_operand_params = first_operand
|
||||
|
||||
second_operand_expression = second_operand.shift
|
||||
second_operand_params = second_operand
|
||||
|
||||
["(#{first_operand_expression} #{operator.to_s} #{second_operand_expression})"] + first_operand_params + second_operand_params
|
||||
end
|
||||
|
||||
def handle_special_chars(text)
|
||||
result = text.gsub(/\"/, '')
|
||||
result.gsub!(/\_/, '\_')
|
||||
result.tr!('\\', '\\')
|
||||
result.gsub!(/%/, '\%')
|
||||
result.downcase!
|
||||
result
|
||||
end
|
||||
end
|
||||
Binary file not shown.
369
spec/lib/pg_searchable_new_spec.rb
Normal file
369
spec/lib/pg_searchable_new_spec.rb
Normal file
@@ -0,0 +1,369 @@
|
||||
# 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 'can search multiple default columns if no column name is used with single search term' do
|
||||
records = VectorModelWithTwoDefaultColumns.create [{ name: 'hamo', value: '5' }, { name: 'meho', value: '20 hamo' }, { name: 'munja-5', value: '300' }]
|
||||
|
||||
expect(VectorModelWithTwoDefaultColumns.scope_search('name:hamo')).to contain_exactly(records[0])
|
||||
expect(VectorModelWithTwoDefaultColumns.scope_search('hamo')).to contain_exactly(records[0], records[1])
|
||||
expect(VectorModelWithTwoDefaultColumns.scope_search('5')).to contain_exactly(records[0], records[2])
|
||||
end
|
||||
|
||||
it 'can search multiple default columns if no column name is used in query containing multiple search terms' do
|
||||
records = VectorModelWithTwoDefaultColumns.create [{ name: 'hamo', value: '9' }, { name: 'meho', value: '5' }, { name: 'oko-9', value: '100' }]
|
||||
|
||||
expect(VectorModelWithTwoDefaultColumns.scope_search('(9 and not name:hamo) or meho')).to contain_exactly(records[2], records[1])
|
||||
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 for search term containing INNER word' do
|
||||
records = DynamicModelWithTagValues.create [{ name: 'inner', value: 'amazing' },
|
||||
{ name: 'new record', value: 'INNER' }]
|
||||
|
||||
Tag.create [{ taggable: records[0], value: 'red', custom_attribute: 'rose' },
|
||||
{ taggable: records[1], value: 'INNER', custom_attribute: 'garden' }]
|
||||
|
||||
expect(DynamicModelWithTagValues.scope_search('name:inner')).to contain_exactly(records[0])
|
||||
expect(DynamicModelWithTagValues.scope_search('value:INNER')).to contain_exactly(records[1])
|
||||
expect(DynamicModelWithTagValues.scope_search('tag:inner')).to contain_exactly(records[1])
|
||||
expect(DynamicModelWithTagValues.scope_search('tag:INNER')).to contain_exactly(records[1])
|
||||
end
|
||||
|
||||
describe 'searching in model with has_many association' do
|
||||
before do
|
||||
records = DynamicModelWithTagValues.create [{ name: 'something', value: 'amazing' },
|
||||
{ name: 'new record', value: 'not so amazing' },
|
||||
{ name: 'last one', value: 'no value' },
|
||||
{ name: 'really last one', value: 'no value' }]
|
||||
|
||||
Tag.create [{ taggable: records[0], value: 'red', custom_attribute: 'rose' },
|
||||
{ taggable: records[0], value: 'green', custom_attribute: 'garden' },
|
||||
{ taggable: records[1], value: 'black', custom_attribute: 'sky' },
|
||||
{ taggable: records[2], value: '-1/12', custom_attribute: 'gold nugget' },
|
||||
{ taggable: records[3], value: '-', custom_attribute: 'unknown' },
|
||||
{ taggable: records[3], value: 'red-green', custom_attribute: 'unicorn' }]
|
||||
end
|
||||
|
||||
it 'can search with multiple search terms connected with OR operator' do
|
||||
records = DynamicModelWithTagValues.all
|
||||
|
||||
expect(DynamicModelWithTagValues.scope_search('tag:red or tag:black')).to contain_exactly(records[0], records[1], records[3])
|
||||
expect(DynamicModelWithTagValues.scope_search('tag:red or tag:green')).to contain_exactly(records[0], records[3])
|
||||
end
|
||||
|
||||
it 'can search with multiple search terms connected with AND operator' do
|
||||
records = DynamicModelWithTagValues.all
|
||||
|
||||
expect(DynamicModelWithTagValues.scope_search('tag:red and tag:black')).to be_empty
|
||||
expect(DynamicModelWithTagValues.scope_search('tag:red and tag:green')).to contain_exactly(records[0], records[3])
|
||||
end
|
||||
|
||||
it 'can search with multiple search terms and containing NOT' do
|
||||
records = DynamicModelWithTagValues.all
|
||||
|
||||
expect(DynamicModelWithTagValues.scope_search('not tag:"-1/12" and not value:amazing')).to contain_exactly(records[3])
|
||||
end
|
||||
|
||||
it 'can search in referenced column and in model columns with multiple search terms connected with logical operators and with brackets' do
|
||||
records = DynamicModelWithTagValues.all
|
||||
|
||||
expect(DynamicModelWithTagValues.scope_search('(tag:- and not tag:12) or (value:"amazing" and not value:"not")')).to contain_exactly(records[0], records[3])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'searching through multiple referenced columns in model with has_many association' do
|
||||
before do
|
||||
records = DynamicModelWithTagValuesAndCustomAttribute.create [{ name: 'something', value: 'amazing' },
|
||||
{ name: 'new record', value: 'not so amazing' },
|
||||
{ name: 'last one', value: 'no value' },
|
||||
{ name: 'really last one', value: 'no value' }]
|
||||
|
||||
Tag.create [{ taggable: records[0], value: 'red', custom_attribute: 'rose' },
|
||||
{ taggable: records[0], value: 'green', custom_attribute: 'garden' },
|
||||
{ taggable: records[1], value: 'black', custom_attribute: 'sky' },
|
||||
{ taggable: records[2], value: '-1/12', custom_attribute: 'gold nugget' },
|
||||
{ taggable: records[3], value: '-', custom_attribute: 'unknown' },
|
||||
{ taggable: records[3], value: 'red-green', custom_attribute: 'unicorn' }]
|
||||
end
|
||||
|
||||
it 'can search with multiple search terms connected with OR operator' do
|
||||
records = DynamicModelWithTagValuesAndCustomAttribute.all
|
||||
|
||||
expect(DynamicModelWithTagValuesAndCustomAttribute.scope_search('tag:red or custom:sky')).to contain_exactly(records[0], records[1], records[3])
|
||||
end
|
||||
|
||||
it 'can search with multiple search terms connected with AND operator' do
|
||||
records = DynamicModelWithTagValuesAndCustomAttribute.all
|
||||
|
||||
expect(DynamicModelWithTagValuesAndCustomAttribute.scope_search('custom:unicorn and tag:black')).to be_empty
|
||||
expect(DynamicModelWithTagValuesAndCustomAttribute.scope_search('tag:"-1/12" and custom:gold')).to contain_exactly(records[2])
|
||||
end
|
||||
|
||||
it 'can search with multiple search terms and containing NOT' do
|
||||
expect(DynamicModelWithTagValuesAndCustomAttribute.scope_search('not tag:"-1/12" and not value:amazing and not custom:unknown')).to be_empty
|
||||
end
|
||||
|
||||
it 'can search with multiple search terms connected with logical operators and with brackets' do
|
||||
records = DynamicModelWithTagValuesAndCustomAttribute.all
|
||||
|
||||
expect(DynamicModelWithTagValuesAndCustomAttribute.scope_search('(tag:- and not tag:12) or (value:"amazing" and not value:"not") or (custom:unknown or custom:rose)')).to contain_exactly(records[0], records[3])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'searching in model with multiple has_many associations' do
|
||||
before do
|
||||
records = DynamicModelWithTagAndCategories.create [{ name: 'something', value: 'amazing' },
|
||||
{ name: 'new record', value: 'not so amazing' },
|
||||
{ name: 'last one', value: 'no value' },
|
||||
{ name: 'really last one', value: 'no value' }]
|
||||
|
||||
Tag.create [{ taggable: records[0], value: 'red', custom_attribute: 'rose' },
|
||||
{ taggable: records[0], value: 'green', custom_attribute: 'garden' },
|
||||
{ taggable: records[1], value: 'black', custom_attribute: 'sky' },
|
||||
{ taggable: records[2], value: '-1/12', custom_attribute: 'gold nugget' },
|
||||
{ taggable: records[3], value: '-', custom_attribute: 'unknown' },
|
||||
{ taggable: records[3], value: 'red-green', custom_attribute: 'unicorn' }]
|
||||
|
||||
Category.create [{ categoriable: records[0], name: 'home' },
|
||||
{ categoriable: records[0], name: 'home' },
|
||||
{ categoriable: records[1], name: 'world' },
|
||||
{ categoriable: records[2], name: 'math' },
|
||||
{ categoriable: records[3], name: 'unknown' },
|
||||
{ categoriable: records[3], name: 'myth' }]
|
||||
|
||||
end
|
||||
|
||||
it 'can search with multiple search terms connected with OR operator' do
|
||||
records = DynamicModelWithTagAndCategories.all
|
||||
|
||||
expect(DynamicModelWithTagAndCategories.scope_search('tag:red or category:math')).to contain_exactly(records[0], records[2], records[3])
|
||||
end
|
||||
|
||||
it 'can search with multiple search terms connected with AND operator' do
|
||||
records = DynamicModelWithTagAndCategories.all
|
||||
|
||||
expect(DynamicModelWithTagAndCategories.scope_search('category:home and tag:-')).to be_empty
|
||||
expect(DynamicModelWithTagAndCategories.scope_search('tag:"-1/12" and category:math')).to contain_exactly(records[2])
|
||||
end
|
||||
|
||||
it 'can search with multiple search terms and containing NOT' do
|
||||
expect(DynamicModelWithTagAndCategories.scope_search('not tag:"-1/12" and not value:amazing and not category:unknown')).to be_empty
|
||||
end
|
||||
|
||||
it 'can search with multiple search terms connected with logical operators and with brackets' do
|
||||
records = DynamicModelWithTagAndCategories.all
|
||||
|
||||
expect(DynamicModelWithTagAndCategories.scope_search('(tag:- and not tag:12) or (value:"amazing" and not value:"not") or (category:unknown or category:math)')).to contain_exactly(records[0], records[2], records[3])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'joins with through association' do
|
||||
let(:players) { Player.all }
|
||||
|
||||
before do
|
||||
players = Player.create([{ name: 'first', value: 'downtown' },
|
||||
{ name: 'redliner', value: 'uptown' },
|
||||
{ name: 'flying', value: 'eagle' },
|
||||
{ name: 'blue', value: 'marine' }])
|
||||
|
||||
|
||||
ptags = Ptag.create [{ value: 'e-ink' },
|
||||
{ value: 'black&white' },
|
||||
{ value: 'rich' },
|
||||
{ value: 'grayscale' },
|
||||
{ value: 'LED' }]
|
||||
|
||||
players[0].ptags << ptags[0..1]
|
||||
players[1].ptags << ptags[0..2]
|
||||
players[2].ptags << ptags[2..3]
|
||||
players[3].ptags << ptags[4]
|
||||
end
|
||||
|
||||
it 'can search in default column of model with through association' do
|
||||
expect(Player.scope_search('l and not y and not blue')).to contain_exactly(players[1])
|
||||
end
|
||||
|
||||
it 'can search in referenced column associated with through relation - simple search' do
|
||||
expect(Player.scope_search('tag:e-ink')).to contain_exactly(players[0], players[1])
|
||||
end
|
||||
|
||||
it 'can search in referenced column associated with through relaction - simple search with OR operator' do
|
||||
expect(Player.scope_search('tag:e-ink OR tag:rich')).to contain_exactly(players[0], players[1], players[2])
|
||||
end
|
||||
|
||||
it 'can search in referenced column associated with through relaction - simple search with AND operator' do
|
||||
expect(Player.scope_search('tag:e-ink AND tag:rich')).to contain_exactly(players[1])
|
||||
end
|
||||
|
||||
it 'can search in referenced column associated with through relaction - simple search with NOT operator' do
|
||||
expect(Player.scope_search('NOT tag:e-ink')).to contain_exactly(players[2], players[3])
|
||||
end
|
||||
|
||||
it 'can search in referenced column associated with through relaction - mixed search terms with OR' do
|
||||
expect(Player.scope_search('tag:e-ink OR blue')).to contain_exactly(players[0], players[1], players[3])
|
||||
end
|
||||
|
||||
it 'can search in referenced column associated with through relaction - mixed search terms with AND' do
|
||||
expect(Player.scope_search('tag:e-ink AND first')).to contain_exactly(players[0])
|
||||
end
|
||||
|
||||
it 'can search in referenced column associated with through relaction - mixed search terms with OR NOT' do
|
||||
expect(Player.scope_search('tag:led OR NOT first')).to contain_exactly(players[1], players[2], players[3])
|
||||
end
|
||||
|
||||
it 'can search in referenced column associated with through relaction - mixed search terms with AND NOT' do
|
||||
expect(Player.scope_search('tag:e-ink AND NOT first')).to contain_exactly(players[1])
|
||||
end
|
||||
|
||||
it 'can search in referenced column and in model columns - complex search' do
|
||||
expect(Player.scope_search('(tag:e-ink AND tag:rich) OR (blue or value:eagle)')).to contain_exactly(players[1], players[2], players[3])
|
||||
expect(Player.scope_search('tag:e-ink AND tag:rich AND NOT tag:black')).to be_empty
|
||||
expect(Player.scope_search('name:l AND (value:uptown or value:marine) AND (tag:e-ink and tag:rich)')).to contain_exactly(players[1])
|
||||
expect(Player.scope_search('name:l AND (value:uptown or value:marine) AND (tag:e-ink and tag:led)')).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
require './lexer'
|
||||
require_relative '../../lib/lexer'
|
||||
|
||||
class QueryLexerTester
|
||||
describe 'Testing the Lexer' do
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
require './parser'
|
||||
require_relative '../../lib/parser'
|
||||
|
||||
class QueryParserTester
|
||||
describe 'Testing the Parser' do
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
78
spec/lib/text_to_sql_query_spec.rb
Normal file
78
spec/lib/text_to_sql_query_spec.rb
Normal file
@@ -0,0 +1,78 @@
|
||||
# frozen_string_literal: true
|
||||
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(['(CAST(players.name AS TEXT) ILIKE ?)', '%some-default-value%']) }
|
||||
|
||||
# tests simple search with two default columns without quotes
|
||||
it { expect(described_class.new('search-term', [:'players.name', :'players.tags', 'players.id'], [:'players.name', :'players.tags']).where_clause).to eq(['(CAST(players.name AS TEXT) ILIKE ? OR CAST(players.tags AS TEXT) ILIKE ?)', '%search-term%', '%search-term%']) }
|
||||
|
||||
# tests simple search with two default columns with quotes
|
||||
it { expect(described_class.new('"search-term"', [:'players.name', :'players.tags', 'players.id'], [:'players.name', :'players.tags']).where_clause).to eq(['(CAST(players.name AS TEXT) ILIKE ? OR CAST(players.tags AS TEXT) ILIKE ?)', '%search-term%', '%search-term%']) }
|
||||
|
||||
# tests simple search with three default columns without quotes
|
||||
it { expect(described_class.new('search-term', [:'players.name', :'players.tags', 'players.id'], [:'players.name', :'players.tags', :'players.id']).where_clause).to eq(['(CAST(players.name AS TEXT) ILIKE ? OR CAST(players.tags AS TEXT) ILIKE ? OR CAST(players.id AS TEXT) ILIKE ?)', '%search-term%', '%search-term%', '%search-term%']) }
|
||||
|
||||
# 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(["STRING_AGG(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(["(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(["STRING_AGG(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(["((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(["((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(["((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 (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 STRING_AGG(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(["(STRING_AGG(CAST(players.name AS TEXT), '') ILIKE ? AND NOT STRING_AGG(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(["((STRING_AGG(CAST(players.name AS TEXT), '') ILIKE ? AND STRING_AGG(CAST(players.name AS TEXT), '') ILIKE ?) OR STRING_AGG(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(["(STRING_AGG(CAST(players.name AS TEXT), '') ILIKE ? AND (STRING_AGG(CAST(players.name AS TEXT), '') ILIKE ? OR STRING_AGG(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(["(STRING_AGG(CAST(players.name AS TEXT), '') ILIKE ? AND NOT (STRING_AGG(CAST(players.name AS TEXT), '') ILIKE ? OR STRING_AGG(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(["STRING_AGG(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(["STRING_AGG(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(["STRING_AGG(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(["((STRING_AGG(CAST(players.device_id AS TEXT), '') ILIKE ? OR (STRING_AGG(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 mixed query with and without column names and with multiple default columns
|
||||
it { expect(described_class.new('("green hunter" and device_id:100) or ("blue bird" and not device_id:200)', [:'players.name', :'players.value', :'players.device_id'], [:"players.name", :"players.tags"]).where_clause).to eq(["(((CAST(players.name AS TEXT) ILIKE ? OR CAST(players.tags AS TEXT) ILIKE ?) AND STRING_AGG(CAST(players.device_id AS TEXT), '') ILIKE ?) OR ((CAST(players.name AS TEXT) ILIKE ? OR CAST(players.tags AS TEXT) ILIKE ?) AND NOT STRING_AGG(CAST(players.device_id AS TEXT), '') ILIKE ?))", '%green hunter%', '%green hunter%', '%100%', '%blue bird%', '%blue bird%', '%200%']) }
|
||||
|
||||
# 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
|
||||
@@ -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
|
||||
@@ -27,6 +27,7 @@ ActiveRecord::Schema.define do
|
||||
|
||||
create_table :tags, force: true do |t|
|
||||
t.string :value
|
||||
t.string :custom_attribute
|
||||
t.references :category, index: true
|
||||
t.references :taggable, polymorphic: true, index: true
|
||||
t.timestamps null: false
|
||||
@@ -34,6 +35,7 @@ ActiveRecord::Schema.define do
|
||||
|
||||
create_table :categories, force: true do |t|
|
||||
t.string :name
|
||||
t.references :categoriable, polymorphic: true, index: true
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
@@ -42,4 +44,19 @@ ActiveRecord::Schema.define do
|
||||
t.string :name
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
create_table :players, force: true do |t|
|
||||
t.string :name
|
||||
t.string :value
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
create_table :ptags, force: true do |t|
|
||||
t.string :value
|
||||
end
|
||||
|
||||
create_table :taggings, force: true do |t|
|
||||
t.belongs_to :player
|
||||
t.belongs_to :ptag
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,19 @@
|
||||
|
||||
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 VectorModelWithTwoDefaultColumns < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[vector_models.id vector_models.name vector_models.value], default_fields: %i[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,7 +44,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
|
||||
pg_search fields: %i[vector_models.id vector_models.name vector_models.value], cache: :search_cache, language: :simple
|
||||
end
|
||||
|
||||
class VectorWithoutWildcardModel < ActiveRecord::Base
|
||||
@@ -66,7 +78,22 @@ 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
|
||||
|
||||
class DynamicModelWithTagAndCategories < ActiveRecord::Base
|
||||
self.table_name = :dynamic_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value], fields_mappings: {tag: 'tags.value', category: 'categories.name'}, joins: [:tags, :categories]
|
||||
has_many :tags, as: :taggable
|
||||
has_many :categories, as: :categoriable
|
||||
end
|
||||
|
||||
class DynamicModelWithTagValuesAndCustomAttribute < ActiveRecord::Base
|
||||
self.table_name = :dynamic_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value], fields_mappings: {tag: 'tags.value', custom: 'tags.custom_attribute'}, joins: [:tags]
|
||||
has_many :tags, as: :taggable
|
||||
end
|
||||
|
||||
@@ -100,4 +127,23 @@ end
|
||||
|
||||
class Category < ActiveRecord::Base
|
||||
has_many :tags
|
||||
belongs_to :categoriable, polymorphic: true
|
||||
end
|
||||
|
||||
class Player < ActiveRecord::Base
|
||||
include PgSearchable
|
||||
pg_search fields: %i[players.id players.name players.value], default_fields: [:name], fields_mappings: { tag: 'ptags.value' }, joins: [:ptags]
|
||||
has_many :taggings
|
||||
has_many :ptags, through: :taggings
|
||||
end
|
||||
|
||||
class Ptag < ActiveRecord::Base
|
||||
self.table_name = :ptags
|
||||
has_many :taggings
|
||||
has_many :players, through: :taggings
|
||||
end
|
||||
|
||||
class Tagging < ActiveRecord::Base
|
||||
belongs_to :player
|
||||
belongs_to :ptag
|
||||
end
|
||||
@@ -3,7 +3,7 @@
|
||||
module Version
|
||||
MAJOR = 1
|
||||
MINOR = 0
|
||||
PATCH = 13
|
||||
PATCH = 27
|
||||
|
||||
def self.to_s
|
||||
[MAJOR, MINOR, PATCH].compact.join('.')
|
||||
|
||||
Reference in New Issue
Block a user