Compare commits
1 Commits
join-bug-f
...
generate-q
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0eb85eaec2 |
@@ -1,7 +0,0 @@
|
|||||||
# Postgres
|
|
||||||
DATABASE_NAME=pg_searchable_test
|
|
||||||
DATABASE_HOST=
|
|
||||||
DATABASE_PORT=5432
|
|
||||||
DATABASE_POOL=10
|
|
||||||
DATABASE_USERNAME=postgres
|
|
||||||
DATABASE_PASSWORD=
|
|
||||||
54
.gitignore
vendored
54
.gitignore
vendored
@@ -1,53 +1 @@
|
|||||||
# rcov generated
|
.idea
|
||||||
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
31
.rubocop.yml
@@ -1,31 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
2.1.5
|
|
||||||
2
Gemfile
2
Gemfile
@@ -23,6 +23,4 @@ group :development do
|
|||||||
gem 'rubocop', '~> 0.54', require: false
|
gem 'rubocop', '~> 0.54', require: false
|
||||||
gem 'rubocop-rspec', '~> 1.23', require: false
|
gem 'rubocop-rspec', '~> 1.23', require: false
|
||||||
gem 'simplecov', '~> 0.16', require: false
|
gem 'simplecov', '~> 0.16', require: false
|
||||||
gem 'rexical', '~> 1.0', require: false
|
|
||||||
gem 'racc', '~> 1.4', require: false
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -76,13 +76,11 @@ GEM
|
|||||||
coderay (~> 1.1.0)
|
coderay (~> 1.1.0)
|
||||||
method_source (~> 0.9.0)
|
method_source (~> 0.9.0)
|
||||||
public_suffix (3.1.1)
|
public_suffix (3.1.1)
|
||||||
racc (1.4.16)
|
|
||||||
rack (1.6.11)
|
rack (1.6.11)
|
||||||
rainbow (3.0.0)
|
rainbow (3.0.0)
|
||||||
rake (12.3.2)
|
rake (12.3.2)
|
||||||
rdoc (3.12.2)
|
rdoc (3.12.2)
|
||||||
json (~> 1.4)
|
json (~> 1.4)
|
||||||
rexical (1.0.7)
|
|
||||||
rspec (3.8.0)
|
rspec (3.8.0)
|
||||||
rspec-core (~> 3.8.0)
|
rspec-core (~> 3.8.0)
|
||||||
rspec-expectations (~> 3.8.0)
|
rspec-expectations (~> 3.8.0)
|
||||||
@@ -134,9 +132,7 @@ DEPENDENCIES
|
|||||||
juwelier (~> 2.1)
|
juwelier (~> 2.1)
|
||||||
pg (~> 0.15)
|
pg (~> 0.15)
|
||||||
pry (~> 0.12)
|
pry (~> 0.12)
|
||||||
racc (~> 1.4)
|
|
||||||
rdoc (~> 3.12)
|
rdoc (~> 3.12)
|
||||||
rexical (~> 1.0)
|
|
||||||
rspec (~> 3.8)
|
rspec (~> 3.8)
|
||||||
rubocop (~> 0.54)
|
rubocop (~> 0.54)
|
||||||
rubocop-rspec (~> 1.23)
|
rubocop-rspec (~> 1.23)
|
||||||
|
|||||||
@@ -119,11 +119,3 @@ To run the test suite create `.env.test.local` file containing the same entries
|
|||||||
## CONTRIBUTING
|
## CONTRIBUTING
|
||||||
|
|
||||||
Make sure the test coverage remains at 100%, there are no rubocop complaints (`bundle exec rubocop`) and make a Pull Request.
|
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,16 +44,3 @@ Rake::RDocTask.new do |rdoc|
|
|||||||
rdoc.rdoc_files.include('README*')
|
rdoc.rdoc_files.include('README*')
|
||||||
rdoc.rdoc_files.include('lib/**/*.rb')
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||||
end
|
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": {
|
"result": {
|
||||||
"covered_percent": 78.1
|
"covered_percent": 66.92
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
1,
|
1,
|
||||||
null,
|
null,
|
||||||
1,
|
1,
|
||||||
13,
|
12,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@@ -32,31 +32,27 @@
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
13,
|
12,
|
||||||
13,
|
12,
|
||||||
13,
|
12,
|
||||||
13,
|
12,
|
||||||
13,
|
12,
|
||||||
13,
|
12,
|
||||||
13,
|
12,
|
||||||
13,
|
12,
|
||||||
13,
|
12,
|
||||||
13,
|
12,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
1,
|
1,
|
||||||
13,
|
12,
|
||||||
13,
|
43,
|
||||||
24,
|
|
||||||
23,
|
|
||||||
null,
|
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
1,
|
1,
|
||||||
24,
|
31,
|
||||||
null,
|
30,
|
||||||
24,
|
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
1,
|
1,
|
||||||
@@ -64,11 +60,11 @@
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
1,
|
1,
|
||||||
16,
|
0,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
1,
|
1,
|
||||||
13,
|
12,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
1,
|
1,
|
||||||
@@ -184,420 +180,75 @@
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
],
|
],
|
||||||
"/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/text_to_sql_query.rb": [
|
"/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/text_to_regex_query.rb": [
|
||||||
1,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
null,
|
null,
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
44,
|
35,
|
||||||
44,
|
35,
|
||||||
44,
|
35,
|
||||||
44,
|
35,
|
||||||
114,
|
37,
|
||||||
114,
|
37,
|
||||||
114,
|
7,
|
||||||
null,
|
null,
|
||||||
44,
|
null,
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
null,
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
null,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
null,
|
||||||
|
5,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
13,
|
||||||
13,
|
13,
|
||||||
null,
|
null,
|
||||||
|
5,
|
||||||
|
8,
|
||||||
null,
|
null,
|
||||||
|
5,
|
||||||
null,
|
null,
|
||||||
1,
|
5,
|
||||||
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,
|
||||||
null
|
null
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"timestamp": 1583928139
|
"timestamp": 1579269179
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2700
coverage/index.html
2700
coverage/index.html
File diff suppressed because it is too large
Load Diff
@@ -14,9 +14,7 @@ class Query
|
|||||||
| TERM_WITHOUT_QUOTES COLON TERM_WITH_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_OR expression { result = {:OPERATOR_OR => [val[0], val[2]]} }
|
||||||
| expression OPERATOR_AND expression { result = {:OPERATOR_AND => [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] }
|
| L_BRACKET expression R_BRACKET { result = val[1] }
|
||||||
| expression expression { result = {:OPERATOR_OR => [val[0], val[1]]} }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---- header
|
---- header
|
||||||
|
|||||||
107
lib/lexer.rb
107
lib/lexer.rb
@@ -1,107 +0,0 @@
|
|||||||
#--
|
|
||||||
# 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
Normal file
4
lib/parser-parser-part/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.idea
|
||||||
|
|
||||||
|
lexer.rb
|
||||||
|
parser.rb
|
||||||
19
lib/parser-parser-part/README.md
Normal file
19
lib/parser-parser-part/README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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`
|
||||||
20
lib/parser-parser-part/Rakefile
Normal file
20
lib/parser-parser-part/Rakefile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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]
|
||||||
26
lib/parser-parser-part/grammar.y
Normal file
26
lib/parser-parser-part/grammar.y
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
259
lib/parser-parser-part/spec/query_lexer_spec.rb
Normal file
259
lib/parser-parser-part/spec/query_lexer_spec.rb
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
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
|
||||||
164
lib/parser-parser-part/spec/query_parser_spec.rb
Normal file
164
lib/parser-parser-part/spec/query_parser_spec.rb
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
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
|
||||||
35
lib/parser-parser-part/specification.rex
Normal file
35
lib/parser-parser-part/specification.rex
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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
207
lib/parser.rb
@@ -1,207 +0,0 @@
|
|||||||
#
|
|
||||||
# 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
|
|
||||||
@@ -26,7 +26,7 @@ module PgSearchable
|
|||||||
wildcard: true,
|
wildcard: true,
|
||||||
external_cache_data: nil,
|
external_cache_data: nil,
|
||||||
joins: [],
|
joins: [],
|
||||||
default_fields: []
|
default_field: ""
|
||||||
)
|
)
|
||||||
@ts_search_fields = fields
|
@ts_search_fields = fields
|
||||||
@ts_search_fields_mappings = fields_mappings
|
@ts_search_fields_mappings = fields_mappings
|
||||||
@@ -36,35 +36,19 @@ module PgSearchable
|
|||||||
@ts_skip_cache_update = skip_callback
|
@ts_skip_cache_update = skip_callback
|
||||||
@ts_wildcard = wildcard
|
@ts_wildcard = wildcard
|
||||||
@ts_joins = joins
|
@ts_joins = joins
|
||||||
@default_fields = if default_fields.is_a? Array
|
@default_field = (default_field.to_sym || fields.first)
|
||||||
default_fields.empty? ? [fields.first] : default_fields
|
|
||||||
else
|
|
||||||
default_fields.to_s.empty? ? [fields.first] : [default_fields.to_sym]
|
|
||||||
end
|
|
||||||
ts_add_scope
|
ts_add_scope
|
||||||
end
|
end
|
||||||
|
|
||||||
def ts_add_scope
|
def ts_add_scope
|
||||||
class_eval do
|
class_eval do
|
||||||
scope ts_scope_method, ->(value) do
|
scope ts_scope_method, ->(value) { ts_search(value) }
|
||||||
resulting_ids = ts_search(value).map(&:id)
|
|
||||||
where(id: resulting_ids)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def ts_search(value)
|
def ts_search(value)
|
||||||
return if @ts_search_fields.blank? || value.blank?
|
return if @ts_search_fields.blank? || value.blank?
|
||||||
model = ancestors.first
|
TextToSqlQuery.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,
|
|
||||||
model
|
|
||||||
)
|
|
||||||
joins(sql_query_object.join_clause).where(sql_query_object.where_clause).distinct
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def should_update_cache_field?
|
def should_update_cache_field?
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ macro
|
|||||||
L_BRACKET \(
|
L_BRACKET \(
|
||||||
R_BRACKET \)
|
R_BRACKET \)
|
||||||
SPACE \ + # Space char
|
SPACE \ + # Space char
|
||||||
OPERATOR_OR (?i)\bor\b
|
OPERATOR_OR (?i)or
|
||||||
OPERATOR_AND (?i)\band\b
|
OPERATOR_AND (?i)and
|
||||||
OPERATOR_NOT (?i)\bnot\b
|
OPERATOR_NOT (?i)not
|
||||||
TERM_WITH_QUOTES "([^"]*)"
|
TERM_WITH_QUOTES "([^"]*)"
|
||||||
TERM_WITHOUT_QUOTES [a-zA-Z0-9\-_]+
|
TERM_WITHOUT_QUOTES [a-zA-Z0-9-_]+
|
||||||
COLON \:
|
COLON \:
|
||||||
|
|
||||||
|
|
||||||
@@ -32,4 +32,4 @@ inner
|
|||||||
end
|
end
|
||||||
tokens
|
tokens
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
67
lib/text_to_regex_query.rb
Normal file
67
lib/text_to_regex_query.rb
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# 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
|
||||||
@@ -1,17 +1,10 @@
|
|||||||
require_relative 'parser'
|
require './parser'
|
||||||
|
|
||||||
class TextToSqlQuery
|
class TextToSqlQuery
|
||||||
def initialize(text, fields, default_fields, fields_mappings = {}, joins = [], model = nil)
|
def initialize(text, fields, default_field, fields_mappings = {})
|
||||||
@text = text.to_s.strip
|
@text = text.to_s.strip
|
||||||
@fields = fields.map(&:to_sym)
|
@fields = fields.map(&:to_sym)
|
||||||
|
@default_field = default_field.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|
|
@fields_mappings = fields_mappings.merge(@fields.reduce({}) do |mappings, field|
|
||||||
_table_name, field_name = field.to_s.split('.')
|
_table_name, field_name = field.to_s.split('.')
|
||||||
mappings[field_name.to_sym] = field
|
mappings[field_name.to_sym] = field
|
||||||
@@ -20,8 +13,6 @@ class TextToSqlQuery
|
|||||||
fields_mappings.each do |field, value|
|
fields_mappings.each do |field, value|
|
||||||
@fields_mappings[field] = value if @fields_mappings[field]
|
@fields_mappings[field] = value if @fields_mappings[field]
|
||||||
end
|
end
|
||||||
@joins = joins
|
|
||||||
@model = model
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def where_clause
|
def where_clause
|
||||||
@@ -30,20 +21,6 @@ class TextToSqlQuery
|
|||||||
generate_sql @parsed_tree
|
generate_sql @parsed_tree
|
||||||
end
|
end
|
||||||
|
|
||||||
def join_clause
|
|
||||||
return if @joins.empty?
|
|
||||||
|
|
||||||
table_column_mappings
|
|
||||||
model_association_mappings
|
|
||||||
|
|
||||||
join_clause_part = ''
|
|
||||||
@joins.each do |join|
|
|
||||||
join_sql_part = generate_join_sql_part_for join
|
|
||||||
join_clause_part += join_sql_part
|
|
||||||
end
|
|
||||||
join_clause_part
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def generate_sql(tree)
|
def generate_sql(tree)
|
||||||
@@ -52,14 +29,7 @@ class TextToSqlQuery
|
|||||||
case first_key
|
case first_key
|
||||||
when :DEFAULT_COLUMN
|
when :DEFAULT_COLUMN
|
||||||
escaped_node_value = handle_special_chars node_value
|
escaped_node_value = handle_special_chars node_value
|
||||||
query_part = '('
|
["#{@default_field.to_s} ILIKE ?", "%#{escaped_node_value}%"]
|
||||||
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
|
when :OPERATOR_OR
|
||||||
generate_expression_for_logical_operator(:OR, node_value)
|
generate_expression_for_logical_operator(:OR, node_value)
|
||||||
when :OPERATOR_AND
|
when :OPERATOR_AND
|
||||||
@@ -71,9 +41,9 @@ class TextToSqlQuery
|
|||||||
raise "There should be more than 1 element for expression following NOT operator"
|
raise "There should be more than 1 element for expression following NOT operator"
|
||||||
end
|
end
|
||||||
|
|
||||||
not_expression = not_array.shift
|
not_expression = not_array.first
|
||||||
not_params = not_array
|
not_params = not_array[1..]
|
||||||
|
|
||||||
["NOT #{not_expression}"] + not_params
|
["NOT #{not_expression}"] + not_params
|
||||||
|
|
||||||
else
|
else
|
||||||
@@ -83,7 +53,7 @@ class TextToSqlQuery
|
|||||||
if mapping.nil?
|
if mapping.nil?
|
||||||
raise "Unknown field '#{first_key.to_s}'"
|
raise "Unknown field '#{first_key.to_s}'"
|
||||||
else
|
else
|
||||||
["CAST(#{mapping.to_s} AS TEXT) ILIKE ?", "%#{escaped_node_value}%"]
|
["#{mapping.to_s} ILIKE ?", "%#{escaped_node_value}%"]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -104,11 +74,11 @@ class TextToSqlQuery
|
|||||||
raise 'There should be more than 1 element in second operand array'
|
raise 'There should be more than 1 element in second operand array'
|
||||||
end
|
end
|
||||||
|
|
||||||
first_operand_expression = first_operand.shift
|
first_operand_expression = first_operand.first
|
||||||
first_operand_params = first_operand
|
first_operand_params = first_operand[1..]
|
||||||
|
|
||||||
second_operand_expression = second_operand.shift
|
second_operand_expression = second_operand.first
|
||||||
second_operand_params = second_operand
|
second_operand_params = second_operand[1..]
|
||||||
|
|
||||||
["(#{first_operand_expression} #{operator.to_s} #{second_operand_expression})"] + first_operand_params + second_operand_params
|
["(#{first_operand_expression} #{operator.to_s} #{second_operand_expression})"] + first_operand_params + second_operand_params
|
||||||
end
|
end
|
||||||
@@ -120,50 +90,4 @@ class TextToSqlQuery
|
|||||||
result.gsub!(/%/, '\%')
|
result.gsub!(/%/, '\%')
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
end
|
||||||
def table_column_mappings
|
|
||||||
@table_column_mappings = {}
|
|
||||||
@fields_mappings.each_value do |table_with_column|
|
|
||||||
split_names = table_with_column.to_s.split '.'
|
|
||||||
table_name = split_names.first
|
|
||||||
column_name = split_names.second
|
|
||||||
@table_column_mappings[table_name] = [] if @table_column_mappings[table_name].nil?
|
|
||||||
@table_column_mappings[table_name] << column_name
|
|
||||||
end
|
|
||||||
@table_column_mappings
|
|
||||||
end
|
|
||||||
|
|
||||||
def model_association_mappings
|
|
||||||
@model_associations = {}
|
|
||||||
@model.reflect_on_all_associations.each do |association|
|
|
||||||
name = association.name
|
|
||||||
|
|
||||||
@model_associations[name] = {
|
|
||||||
option_as: association.options[:as] || name,
|
|
||||||
type: association.type
|
|
||||||
}
|
|
||||||
end
|
|
||||||
@model_associations
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate_join_sql_part_for(join)
|
|
||||||
association_data = @model_associations[join]
|
|
||||||
join_table_name = join.to_s
|
|
||||||
raise "Join table #{join_table_name} has no association data" if association_data.nil?
|
|
||||||
|
|
||||||
select_sql_part = ''
|
|
||||||
columns_for_table = @table_column_mappings[join_table_name] || []
|
|
||||||
|
|
||||||
# TODO: Can be optimized - do not include columns that are not referenced in user query
|
|
||||||
columns_for_table.each do |column_name|
|
|
||||||
select_sql_part += "string_agg(#{column_name}, '') AS #{column_name}, "
|
|
||||||
end
|
|
||||||
|
|
||||||
option_as = association_data[:option_as]
|
|
||||||
type = association_data[:type]
|
|
||||||
model_name = @model.to_s
|
|
||||||
table_name = @model.table_name
|
|
||||||
|
|
||||||
"LEFT JOIN (SELECT #{option_as}_id, #{select_sql_part} #{type} FROM #{join_table_name} GROUP BY #{option_as}_id, #{type}) #{join_table_name} on #{join_table_name}.#{option_as}_id = #{table_name}.id AND #{join_table_name}.#{type} = '#{model_name}'"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
BIN
pkg/pg_searchable_regex-1.0.13.gem
Normal file
BIN
pkg/pg_searchable_regex-1.0.13.gem
Normal file
Binary file not shown.
@@ -1,290 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
115
spec/lib/pg_searchable_spec.rb
Normal file
115
spec/lib/pg_searchable_spec.rb
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# 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_relative '../../lib/lexer'
|
require './lexer'
|
||||||
|
|
||||||
class QueryLexerTester
|
class QueryLexerTester
|
||||||
describe 'Testing the Lexer' do
|
describe 'Testing the Lexer' do
|
||||||
@@ -255,278 +255,5 @@ class QueryLexerTester
|
|||||||
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
|
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
|
||||||
expect(@result[2][1]).to eq '"tag with or and not inside"'
|
expect(@result[2][1]).to eq '"tag with or and not inside"'
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
require_relative '../../lib/parser'
|
require './parser'
|
||||||
|
|
||||||
class QueryParserTester
|
class QueryParserTester
|
||||||
describe 'Testing the Parser' do
|
describe 'Testing the Parser' do
|
||||||
@@ -133,7 +133,7 @@ class QueryParserTester
|
|||||||
expect(@result[:OPERATOR_AND]).to eq @expected_array_total
|
expect(@result[:OPERATOR_AND]).to eq @expected_array_total
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'tests operator precedence, or - and' do
|
it 'tests operator precedence' do
|
||||||
@result1 = @evaluator.parse('tag:mta or name:JF and 12_4')
|
@result1 = @evaluator.parse('tag:mta or name:JF and 12_4')
|
||||||
@result2 = @evaluator.parse('tag:mta or (name:JF and 12_4)')
|
@result2 = @evaluator.parse('tag:mta or (name:JF and 12_4)')
|
||||||
|
|
||||||
@@ -155,469 +155,10 @@ class QueryParserTester
|
|||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'tests operator precedence, and - or' do
|
# Tests to write :
|
||||||
@result1 = @evaluator.parse('tag:mta and name:JF or 12_4')
|
# * query with multiple column names and search terms without logical operators
|
||||||
@result2 = @evaluator.parse('(tag:mta and name:JF) or 12_4')
|
# * AND NOT, OR NOT tests
|
||||||
|
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|||||||
22
spec/lib/text_to_regex_query_spec.rb
Normal file
22
spec/lib/text_to_regex_query_spec.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 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
|
||||||
@@ -1,78 +1,61 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
require_relative '../../lib/text_to_sql_query'
|
|
||||||
|
|
||||||
describe TextToSqlQuery do
|
describe TextToRegexQuery do
|
||||||
|
include_examples 'pg_search', SimpleVectorModel
|
||||||
describe '.new' do
|
describe '.new' do
|
||||||
# tests simple search term without column name and without quotes
|
# 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%']) }
|
it { expect(described_class.new('some-default-value', [:"players.name"], :"players.name").where_clause(SimpleVectorModel)).to eq(['players.name 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
|
# 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(['CAST(players.name AS TEXT) ILIKE ?', '%ab%']) }
|
it { expect(described_class.new('title:ab', [:"players.title"], :"players.title").where_clause(SimpleVectorModel)).to eq(['players.title ILIKE ?', '%ab%']) }
|
||||||
|
|
||||||
# tests simple search term with unknown column name and without quotes
|
# 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'") }
|
it { expect(described_class.new('unknown:ab', [:"players.name"], :"players.name").where_clause(SimpleVectorModel)).to raise_error(RuntimeError, "Unknown field 'unknown'") }
|
||||||
|
|
||||||
# tests simple search term without column name and with quotes
|
# 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%"]) }
|
it { expect(described_class.new('"ab"', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(["players.device_id ILIKE ?", "%ab"]) }
|
||||||
|
|
||||||
# tests simple search term with column name and with quotes
|
# 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(["CAST(players.tags AS TEXT) ILIKE ?", "%ab%"]) }
|
it { expect(described_class.new('tag:"ab"', [:"players.name", :"players.tag"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(["players.tag ILIKE ?", "%ab%"]) }
|
||||||
|
|
||||||
# tests search without operators
|
# 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%"]) }
|
it { expect(described_class.new('123 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(["(players.device_id ILIKE ? OR players.device_id ILIKE ?)", "%123%", "%456%"]) }
|
||||||
|
|
||||||
# tests search with OR operator
|
# 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%"]) }
|
it { expect(described_class.new('123 or 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(["(players.device_id ILIKE ? OR players.device_id ILIKE ?)", "%123%", "%456%"]) }
|
||||||
|
|
||||||
# tests search with AND operator
|
# 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%"]) }
|
it { expect(described_class.new('123 and 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(["(players.device_id ILIKE ? AND players.device_id ILIKE ?)", "%123%", "%456%"]) }
|
||||||
|
|
||||||
# tests search with NOT operator on default column
|
# 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%"]) }
|
it { expect(described_class.new('not 23', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(["NOT players.device_id ILIKE ?", "%23%"]) }
|
||||||
|
|
||||||
# tests search with NOT operator on non-default column
|
# 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 CAST(players.value AS TEXT) ILIKE ?", "%23%"]) }
|
it { expect(described_class.new('not tag:23', [:"players.name", :"players.tag"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(["NOT players.tag ILIKE ?", "%23%"]) }
|
||||||
|
|
||||||
# tests search with mixed logical operators
|
# 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(['(CAST(players.name AS TEXT) ILIKE ? AND NOT CAST(players.value AS TEXT) ILIKE ?)', '%ab%', '%hf-1%']) }
|
it { expect(described_class.new('title:ab and not tag:hf-1', [:"players.title", :"players.tag"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(['(players.title ILIKE ? AND NOT players.tag ILIKE ?)', '%ab%', '%hf-1%']) }
|
||||||
|
|
||||||
# tests search with mixed logical operators without NOT'
|
# 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(['((CAST(players.name AS TEXT) ILIKE ? AND CAST(players.name AS TEXT) ILIKE ?) OR CAST(players.name AS TEXT) ILIKE ?)', '%a%', '%b%', '%c%']) }
|
it { expect(described_class.new('title:a and title:b or title:c', [:"players.title", :"players.tag"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(['((players.title ILIKE ? AND players.title ILIKE ?) OR players.title ILIKE ?)', '%a%', '%b%', '%c%']) }
|
||||||
|
|
||||||
# tests search with brackets in expression
|
# 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(['(CAST(players.name AS TEXT) ILIKE ? AND (CAST(players.name AS TEXT) ILIKE ? OR CAST(players.name AS TEXT) ILIKE ?))', '%a%', '%b%', '%c%']) }
|
it { expect(described_class.new('title:a and (title:b or title:c)', [:"players.title", :"players.tag"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(['(players.title ILIKE ? AND (players.title ILIKE ? OR players.title ILIKE ?))', '%a%', '%b%', '%c%']) }
|
||||||
|
|
||||||
# tests search with brackets in expression and with NOT operator
|
# 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(['(CAST(players.name AS TEXT) ILIKE ? AND NOT (CAST(players.name AS TEXT) ILIKE ? OR CAST(players.name AS TEXT) ILIKE ?))', '%a%', '%b%', '%c%']) }
|
it { expect(described_class.new('title:a and not (title:b or title:c)', [:"players.title", :"players.tag"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(['(players.title ILIKE ? AND NOT (players.title ILIKE ? OR players.title ILIKE ?))', '%a%', '%b%', '%c%']) }
|
||||||
|
|
||||||
# tests search with special characters in search term
|
# 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(['CAST(players.name AS TEXT) ILIKE ?', '%\%a\_\\%']) }
|
it { expect(described_class.new('title:"%a_\"', [:"players.title", :"players.tag"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(['players.title ILIKE ?', '%\%a\_\\%']) }
|
||||||
|
|
||||||
# tests search with field mappings
|
# 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(['CAST(tags.value AS TEXT) ILIKE ?', '%h1-r%']) }
|
it { expect(described_class.new('tags:h1-r', [:'players.title', :'players.name', :'players.device_id'], :"players.device_id", { tags: "tags.name" }).where_clause(SimpleVectorModel)).to eq(['tags.name ILIKE ?', '%h1-r%']) }
|
||||||
|
|
||||||
# tests search with field mappings when fields array has same mapping
|
# 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(["CAST(tags.value AS TEXT) ILIKE ?", "%h1-r%"]) }
|
it { expect(described_class.new('tags:hs1-r', [:'players.title', :'players.tags', :'players.device_id'], :"players.device_id", { tags: "tags.name" }).where_clause(SimpleVectorModel)).to eq(["players.tag ILIKE ?", "%ab%"]) }
|
||||||
|
|
||||||
# tests complex query
|
# 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(['((CAST(players.device_id AS TEXT) ILIKE ? OR (CAST(tags.name AS TEXT) ILIKE ? OR (CAST(players.device_id AS TEXT) ILIKE ?))) OR ((CAST(players.device_id AS TEXT) ILIKE ?) AND ((((CAST(players.device_id AS TEXT) ILIKE ?) OR (CAST(players.device_id AS TEXT) ILIKE ?)) AND ((CAST(players.device_id AS TEXT) ILIKE ?) OR (CAST(players.device_id AS TEXT) ILIKE ?))) AND NOT (CAST(players.device_id AS TEXT) ILIKE ?))))', '%with space%', '%mta%', '%no-quotes-id-123%', '%id with quotes-5%', '%id with q 10%', '%id with q 20%', '%id with Q 30%', '%id with Q 40%', '%id-without-Q-50%']) }
|
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.title', :'players.name', :'players.device_id'], :"players.device_id", { tags: 'tags.name' }).where_clause(SimpleVectorModel)).to eq(['((players.device_id ILIKE ? OR (tags.name ILIKE ? OR players.device_id ILIKE ?)) OR (players.device_id ILIKE ? AND (((players.device_id ILIKE ? OR players.device_id ILIKE ?) AND (players.device_id ILIKE ? OR players.device_id ILIKE ?)) AND NOT players.device_id ILIKE ?)))', '%with space%', '%mta%', '%no-quotes-id-123%', '%id with quotes-5%', '%id with q 10%', '%id with q 20%', '%id with Q 30%', '%id with Q 40%', '%id-without-Q-50%']) }
|
||||||
|
|
||||||
# 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 CAST(players.device_id AS TEXT) ILIKE ?) OR ((CAST(players.name AS TEXT) ILIKE ? OR CAST(players.tags AS TEXT) ILIKE ?) AND NOT 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
|
||||||
end
|
end
|
||||||
|
|||||||
80
spec/lib/text_to_tsquery_spec.rb
Normal file
80
spec/lib/text_to_tsquery_spec.rb
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# 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,7 +27,6 @@ ActiveRecord::Schema.define do
|
|||||||
|
|
||||||
create_table :tags, force: true do |t|
|
create_table :tags, force: true do |t|
|
||||||
t.string :value
|
t.string :value
|
||||||
t.string :custom_attribute
|
|
||||||
t.references :category, index: true
|
t.references :category, index: true
|
||||||
t.references :taggable, polymorphic: true, index: true
|
t.references :taggable, polymorphic: true, index: true
|
||||||
t.timestamps null: false
|
t.timestamps null: false
|
||||||
@@ -35,7 +34,6 @@ ActiveRecord::Schema.define do
|
|||||||
|
|
||||||
create_table :categories, force: true do |t|
|
create_table :categories, force: true do |t|
|
||||||
t.string :name
|
t.string :name
|
||||||
t.references :categoriable, polymorphic: true, index: true
|
|
||||||
t.timestamps null: false
|
t.timestamps null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,7 @@
|
|||||||
|
|
||||||
class VectorModel < ActiveRecord::Base
|
class VectorModel < ActiveRecord::Base
|
||||||
include PgSearchable
|
include PgSearchable
|
||||||
pg_search fields: %i[vector_models.id vector_models.name vector_models.value], cache: :search_cache
|
pg_search fields: %i[id name 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
|
end
|
||||||
|
|
||||||
class VectorModelWithoutCallback < ActiveRecord::Base
|
class VectorModelWithoutCallback < ActiveRecord::Base
|
||||||
@@ -44,7 +32,7 @@ end
|
|||||||
class SimpleVectorModel < ActiveRecord::Base
|
class SimpleVectorModel < ActiveRecord::Base
|
||||||
self.table_name = :vector_models
|
self.table_name = :vector_models
|
||||||
include PgSearchable
|
include PgSearchable
|
||||||
pg_search fields: %i[vector_models.id vector_models.name vector_models.value], cache: :search_cache, language: :simple
|
pg_search fields: %i[id name value], cache: :search_cache, language: :simple
|
||||||
end
|
end
|
||||||
|
|
||||||
class VectorWithoutWildcardModel < ActiveRecord::Base
|
class VectorWithoutWildcardModel < ActiveRecord::Base
|
||||||
@@ -78,22 +66,7 @@ end
|
|||||||
class DynamicModelWithTagValues < ActiveRecord::Base
|
class DynamicModelWithTagValues < ActiveRecord::Base
|
||||||
self.table_name = :dynamic_models
|
self.table_name = :dynamic_models
|
||||||
include PgSearchable
|
include PgSearchable
|
||||||
pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value], fields_mappings: {tag: 'tags.value'}, joins: [:tags]
|
pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value 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
|
has_many :tags, as: :taggable
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -127,5 +100,4 @@ end
|
|||||||
|
|
||||||
class Category < ActiveRecord::Base
|
class Category < ActiveRecord::Base
|
||||||
has_many :tags
|
has_many :tags
|
||||||
belongs_to :categoriable, polymorphic: true
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module Version
|
module Version
|
||||||
MAJOR = 1
|
MAJOR = 1
|
||||||
MINOR = 0
|
MINOR = 0
|
||||||
PATCH = 27
|
PATCH = 13
|
||||||
|
|
||||||
def self.to_s
|
def self.to_s
|
||||||
[MAJOR, MINOR, PATCH].compact.join('.')
|
[MAJOR, MINOR, PATCH].compact.join('.')
|
||||||
|
|||||||
Reference in New Issue
Block a user