1 Commits

Author SHA1 Message Date
Bilal Catic
0eb85eaec2 integrate parser; use parser instead of regex query 2020-02-03 09:28:27 +01:00
37 changed files with 1226 additions and 4308 deletions

View File

@@ -1,5 +0,0 @@
lib/**/*.rb
bin/*
-
features/**/*.feature
LICENSE.txt

View File

@@ -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
View File

@@ -1,53 +1 @@
# rcov generated
coverage
coverage.data
# rdoc generated
rdoc
# yard generated
doc
.yardoc
# bundler
.bundle
.env.local
.env.test.local
*.gem
*.gemspec
# juwelier generated
pkg
# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
#
# * Create a file at ~/.gitignore
# * Include files you want ignored
# * Run: git config --global core.excludesfile ~/.gitignore
#
# After doing this, these files will be ignored in all your git projects,
# saving you from having to 'pollute' every project you touch with them
#
# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
#
# For MacOS:
#
#.DS_Store
# For TextMate
#*.tmproj
#tmtags
# For emacs:
#*~
#\#*
#.\#*
# For vim:
#*.swp
# For redcar:
#.redcar
# For rubinius:
#*.rbc
.idea

1
.rspec
View File

@@ -1 +0,0 @@
--require spec_helper

View File

@@ -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

View File

@@ -1 +0,0 @@
2.1.5

View File

@@ -23,6 +23,4 @@ group :development do
gem 'rubocop', '~> 0.54', require: false
gem 'rubocop-rspec', '~> 1.23', require: false
gem 'simplecov', '~> 0.16', require: false
gem 'rexical', '~> 1.0', require: false
gem 'racc', '~> 1.4', require: false
end

View File

@@ -76,13 +76,11 @@ GEM
coderay (~> 1.1.0)
method_source (~> 0.9.0)
public_suffix (3.1.1)
racc (1.4.16)
rack (1.6.11)
rainbow (3.0.0)
rake (12.3.2)
rdoc (3.12.2)
json (~> 1.4)
rexical (1.0.7)
rspec (3.8.0)
rspec-core (~> 3.8.0)
rspec-expectations (~> 3.8.0)
@@ -134,9 +132,7 @@ DEPENDENCIES
juwelier (~> 2.1)
pg (~> 0.15)
pry (~> 0.12)
racc (~> 1.4)
rdoc (~> 3.12)
rexical (~> 1.0)
rspec (~> 3.8)
rubocop (~> 0.54)
rubocop-rspec (~> 1.23)

View File

@@ -119,11 +119,3 @@ To run the test suite create `.env.test.local` file containing the same entries
## CONTRIBUTING
Make sure the test coverage remains at 100%, there are no rubocop complaints (`bundle exec rubocop`) and make a Pull Request.
## Modifying parser and lexer
* `rake lexer` - generates `lexer.rb` file based on `specification.rex` file
* `rake parser` - generates `parser.rb` file based on `grammar.y` file
* `rake generate` - generates both `lexer.rb` and `parser.rb` files

View File

@@ -44,16 +44,3 @@ Rake::RDocTask.new do |rdoc|
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
desc 'Generate Lexer'
task :lexer do
`rex ./lib/specification.rex -o ./lib/lexer.rb`
end
desc 'Generate Parser'
task :parser do
`racc ./lib/grammar.y -o ./lib/parser.rb`
end
desc 'Generate Lexer and Parser'
task generate: %i[lexer parser]

View File

@@ -1,5 +1,5 @@
{
"result": {
"covered_percent": 78.1
"covered_percent": 66.92
}
}

View File

@@ -13,7 +13,7 @@
1,
null,
1,
13,
12,
null,
null,
null,
@@ -32,31 +32,27 @@
null,
null,
null,
13,
13,
13,
13,
13,
13,
13,
13,
13,
13,
12,
12,
12,
12,
12,
12,
12,
12,
12,
12,
null,
null,
1,
13,
13,
24,
23,
null,
12,
43,
null,
null,
null,
1,
24,
null,
24,
31,
30,
null,
null,
1,
@@ -64,11 +60,11 @@
null,
null,
1,
16,
0,
null,
null,
1,
13,
12,
null,
null,
1,
@@ -184,420 +180,75 @@
null,
null
],
"/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/text_to_sql_query.rb": [
1,
"/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/text_to_regex_query.rb": [
null,
null,
null,
null,
null,
1,
1,
44,
44,
44,
44,
114,
114,
114,
35,
35,
35,
35,
37,
37,
7,
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,
null,
5,
8,
null,
5,
null,
1,
44,
44,
44,
null,
null,
1,
null,
1,
128,
128,
128,
null,
23,
23,
null,
22,
null,
15,
null,
10,
null,
10,
0,
null,
null,
10,
10,
null,
10,
null,
null,
null,
58,
58,
58,
2,
null,
56,
null,
null,
null,
null,
1,
37,
0,
null,
null,
37,
37,
null,
37,
0,
null,
null,
37,
0,
null,
null,
37,
37,
null,
37,
37,
null,
37,
null,
null,
1,
81,
81,
81,
81,
81,
null,
null
],
"/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/parser.rb": [
null,
null,
null,
null,
null,
null,
1,
null,
1,
null,
1,
null,
1,
null,
null,
null,
null,
null,
null,
1,
null,
null,
null,
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,
5,
null,
null
]
},
"timestamp": 1583928139
"timestamp": 1579269179
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,9 +14,7 @@ class Query
| TERM_WITHOUT_QUOTES COLON TERM_WITH_QUOTES { result = {val[0] => val[2]} }
| expression OPERATOR_OR expression { result = {:OPERATOR_OR => [val[0], val[2]]} }
| expression OPERATOR_AND expression { result = {:OPERATOR_AND => [val[0], val[2]]} }
| OPERATOR_NOT expression { result = {:OPERATOR_NOT => val[1]} }
| L_BRACKET expression R_BRACKET { result = val[1] }
| expression expression { result = {:OPERATOR_OR => [val[0], val[1]]} }
end
---- header

View File

@@ -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
View File

@@ -0,0 +1,4 @@
.idea
lexer.rb
parser.rb

View 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`

View 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]

View 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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -26,7 +26,7 @@ module PgSearchable
wildcard: true,
external_cache_data: nil,
joins: [],
default_fields: []
default_field: ""
)
@ts_search_fields = fields
@ts_search_fields_mappings = fields_mappings
@@ -36,27 +36,19 @@ module PgSearchable
@ts_skip_cache_update = skip_callback
@ts_wildcard = wildcard
@ts_joins = joins
@default_fields = if default_fields.is_a? Array
default_fields.empty? ? [fields.first] : default_fields
else
default_fields.to_s.empty? ? [fields.first] : [default_fields.to_sym]
end
@default_field = (default_field.to_sym || fields.first)
ts_add_scope
end
def ts_add_scope
class_eval do
scope ts_scope_method, ->(value) do
resulting_ids = ts_search(value).map(&:id)
where(id: resulting_ids)
end
scope ts_scope_method, ->(value) { ts_search(value) }
end
end
def ts_search(value)
return if @ts_search_fields.blank? || value.blank?
includes(@ts_joins).references(:all).where(
TextToSqlQuery.new(value, @ts_search_fields, @default_fields, @ts_search_fields_mappings).where_clause).distinct
TextToSqlQuery.new(value, @ts_search_fields, @default_field, @ts_search_fields_mappings).where_clause( includes(@ts_joins).references(:all))
end
def should_update_cache_field?

View File

@@ -3,11 +3,11 @@ macro
L_BRACKET \(
R_BRACKET \)
SPACE \ + # Space char
OPERATOR_OR (?i)\bor\b
OPERATOR_AND (?i)\band\b
OPERATOR_NOT (?i)\bnot\b
OPERATOR_OR (?i)or
OPERATOR_AND (?i)and
OPERATOR_NOT (?i)not
TERM_WITH_QUOTES "([^"]*)"
TERM_WITHOUT_QUOTES [a-zA-Z0-9\-_]+
TERM_WITHOUT_QUOTES [a-zA-Z0-9-_]+
COLON \:
@@ -32,4 +32,4 @@ inner
end
tokens
end
end
end

View 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

View File

@@ -1,17 +1,10 @@
require_relative 'parser'
require './parser'
class TextToSqlQuery
def initialize(text, fields, default_fields, fields_mappings = {})
def initialize(text, fields, default_field, fields_mappings = {})
@text = text.to_s.strip
@fields = fields.map(&:to_sym)
# Keep compatibility with previous version where default_field(s) was string/symbol
@default_fields = if default_fields.is_a? Array
default_fields.map(&:to_sym)
else
[default_fields.to_sym]
end
@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
@@ -36,14 +29,7 @@ class TextToSqlQuery
case first_key
when :DEFAULT_COLUMN
escaped_node_value = handle_special_chars node_value
query_part = '('
params_part = []
@default_fields.each do |default_field|
query_part += "CAST(#{default_field.to_s} AS TEXT) ILIKE ? OR "
params_part << "%#{escaped_node_value}%"
end
query_part = query_part[0...-4] + ')'
[query_part, *params_part]
["#{@default_field.to_s} ILIKE ?", "%#{escaped_node_value}%"]
when :OPERATOR_OR
generate_expression_for_logical_operator(:OR, node_value)
when :OPERATOR_AND
@@ -55,9 +41,9 @@ class TextToSqlQuery
raise "There should be more than 1 element for expression following NOT operator"
end
not_expression = not_array.shift
not_params = not_array
not_expression = not_array.first
not_params = not_array[1..]
["NOT #{not_expression}"] + not_params
else
@@ -67,7 +53,7 @@ class TextToSqlQuery
if mapping.nil?
raise "Unknown field '#{first_key.to_s}'"
else
["CAST(#{mapping.to_s} AS TEXT) ILIKE ?", "%#{escaped_node_value}%"]
["#{mapping.to_s} ILIKE ?", "%#{escaped_node_value}%"]
end
end
end
@@ -88,11 +74,11 @@ class TextToSqlQuery
raise 'There should be more than 1 element in second operand array'
end
first_operand_expression = first_operand.shift
first_operand_params = first_operand
first_operand_expression = first_operand.first
first_operand_params = first_operand[1..]
second_operand_expression = second_operand.shift
second_operand_params = second_operand
second_operand_expression = second_operand.first
second_operand_params = second_operand[1..]
["(#{first_operand_expression} #{operator.to_s} #{second_operand_expression})"] + first_operand_params + second_operand_params
end
@@ -104,4 +90,4 @@ class TextToSqlQuery
result.gsub!(/%/, '\%')
result
end
end
end

Binary file not shown.

View File

@@ -1,193 +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
it 'can search in referenced column and in model columns with multiple search terms connected with logical operators' do
record1 = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
record2 = DynamicModelWithTagValues.create name: 'new record', value: 'not so amazing'
record3 = DynamicModelWithTagValues.create name: 'last one', value: 'no value'
record4 = DynamicModelWithTagValues.create name: 'really last one', value: 'no value'
Tag.create(taggable: record1, value: 'red')
Tag.create(taggable: record1, value: 'green')
Tag.create(taggable: record2, value: 'black')
Tag.create(taggable: record3, value: '-12')
Tag.create(taggable: record4, value: '-')
expect(DynamicModelWithTagValues.scope_search('tag:red or tag:black')).to contain_exactly(record1, record2)
expect(DynamicModelWithTagValues.scope_search('tag:red and tag:black')).to be_empty
expect(DynamicModelWithTagValues.scope_search('tag:red or tag:green')).to contain_exactly(record1)
expect(DynamicModelWithTagValues.scope_search('not tag:-12 and not value:amazing')).to contain_exactly(record4)
end
it 'can search in referenced column and in model columns with multiple search terms connected with logical operators and with brackets' do
record1 = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
record2 = DynamicModelWithTagValues.create name: 'new record', value: 'not so amazing'
record3 = DynamicModelWithTagValues.create name: 'last one', value: 'no value'
record4 = DynamicModelWithTagValues.create name: 'really last one', value: 'no value'
Tag.create(taggable: record1, value: 'red')
Tag.create(taggable: record1, value: 'green')
Tag.create(taggable: record2, value: 'black')
Tag.create(taggable: record3, value: '-12')
Tag.create(taggable: record4, value: '-')
expect(DynamicModelWithTagValues.scope_search('(tag:- and not tag:12) or (value:"amazing" and not value:"not")')).to contain_exactly(record1, record4)
end
end
end
end

View 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

View File

@@ -1,4 +1,4 @@
require_relative '../../lib/lexer'
require './lexer'
class QueryLexerTester
describe 'Testing the Lexer' do
@@ -255,278 +255,5 @@ class QueryLexerTester
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
expect(@result[2][1]).to eq '"tag with or and not inside"'
end
it 'tests query without column name containing AND word inside without quotes' do
@result = @evaluator.tokenize('land')
expect(@result.length).to eq 1
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[0][1]).to eq 'land'
end
it 'tests query without column name containing AND word inside with quotes' do
@result = @evaluator.tokenize('"land"')
expect(@result.length).to eq 1
expect(@result[0][0]).to eq :TERM_WITH_QUOTES
expect(@result[0][1]).to eq '"land"'
end
it 'tests query without column name and with word starting with AND without quotes' do
@result = @evaluator.tokenize('andromeda')
expect(@result.length).to eq 1
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[0][1]).to eq 'andromeda'
end
it 'tests query without column name and with word starting with AND with quotes' do
@result = @evaluator.tokenize('"andromeda"')
expect(@result.length).to eq 1
expect(@result[0][0]).to eq :TERM_WITH_QUOTES
expect(@result[0][1]).to eq '"andromeda"'
end
it 'tests query with column name containing AND word inside' do
@result = @evaluator.tokenize('land:some-search-term')
expect(@result.length).to eq 3
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[0][1]).to eq 'land'
expect(@result[1][0]).to eq :COLON
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[2][1]).to eq 'some-search-term'
end
it 'tests query with column name starting with AND' do
@result = @evaluator.tokenize('andromeda:some-search-term')
expect(@result.length).to eq 3
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[0][1]).to eq 'andromeda'
expect(@result[1][0]).to eq :COLON
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[2][1]).to eq 'some-search-term'
end
it 'tests query with search term without quotes containing AND word inside' do
@result = @evaluator.tokenize('name:land')
expect(@result.length).to eq 3
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[0][1]).to eq 'name'
expect(@result[1][0]).to eq :COLON
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[2][1]).to eq 'land'
end
it 'tests query with search term with quotes containing AND word inside' do
@result = @evaluator.tokenize('name:"land"')
expect(@result.length).to eq 3
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[0][1]).to eq 'name'
expect(@result[1][0]).to eq :COLON
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
expect(@result[2][1]).to eq '"land"'
end
it 'tests query with search term without quotes and search term starting with AND without quotes' do
@result = @evaluator.tokenize('name:andromeda')
expect(@result.length).to eq 3
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[0][1]).to eq 'name'
expect(@result[1][0]).to eq :COLON
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[2][1]).to eq 'andromeda'
end
it 'tests query with search term without quotes and search term starting with AND with quotes' do
@result = @evaluator.tokenize('name:"andrew"')
expect(@result.length).to eq 3
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[0][1]).to eq 'name'
expect(@result[1][0]).to eq :COLON
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
expect(@result[2][1]).to eq '"andrew"'
end
it 'tests query with search term in brackets with AND inside word' do
@result = @evaluator.tokenize('(land)')
expect(@result.length).to eq 3
expect(@result[0][0]).to eq :L_BRACKET
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[1][1]).to eq 'land'
expect(@result[2][0]).to eq :R_BRACKET
end
it 'tests query with search term in brackets starting with AND' do
@result = @evaluator.tokenize('(andromeda)')
expect(@result.length).to eq 3
expect(@result[0][0]).to eq :L_BRACKET
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[1][1]).to eq 'andromeda'
expect(@result[2][0]).to eq :R_BRACKET
end
it 'tests query with search term in brackets with AND inside word with quotes' do
@result = @evaluator.tokenize('("land")')
expect(@result.length).to eq 3
expect(@result[0][0]).to eq :L_BRACKET
expect(@result[1][0]).to eq :TERM_WITH_QUOTES
expect(@result[1][1]).to eq '"land"'
expect(@result[2][0]).to eq :R_BRACKET
end
it 'tests query with search term in brackets starting with AND with quotes' do
@result = @evaluator.tokenize('("andromeda")')
expect(@result.length).to eq 3
expect(@result[0][0]).to eq :L_BRACKET
expect(@result[1][0]).to eq :TERM_WITH_QUOTES
expect(@result[1][1]).to eq '"andromeda"'
expect(@result[2][0]).to eq :R_BRACKET
end
it 'tests query with multiple search terms with mixed and-or-not after dash and underscore' do
@result = @evaluator.tokenize('123-and-456 -or-2 -not_not_1')
expect(@result.length).to eq 3
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[0][1]).to eq '123-and-456'
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[1][1]).to eq '-or-2'
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[2][1]).to eq '-not_not_1'
end
it 'tests query with multiple search terms starting with AND' do
@result = @evaluator.tokenize('land andrew and andromeda or orlando')
expect(@result.length).to eq 6
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[0][1]).to eq 'land'
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[1][1]).to eq 'andrew'
expect(@result[2][0]).to eq :OPERATOR_AND
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[3][1]).to eq 'andromeda'
expect(@result[4][0]).to eq :OPERATOR_OR
expect(@result[5][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[5][1]).to eq 'orlando'
end
it 'tests query with multiple search terms with dash and underscore chars' do
@result = @evaluator.tokenize('land 123-andrew or -andrew- and not _ornela_ or ornela')
expect(@result.length).to eq 9
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[0][1]).to eq 'land'
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[1][1]).to eq '123-andrew'
expect(@result[2][0]).to eq :OPERATOR_OR
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[3][1]).to eq '-andrew-'
expect(@result[4][0]).to eq :OPERATOR_AND
expect(@result[5][0]).to eq :OPERATOR_NOT
expect(@result[6][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[6][1]).to eq '_ornela_'
expect(@result[7][0]).to eq :OPERATOR_OR
expect(@result[8][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[8][1]).to eq 'ornela'
end
it 'tests query with multiple search terms with dash and underscore chars under quotes' do
@result = @evaluator.tokenize('"land" "123-andrew" or "-andrew-"')
expect(@result.length).to eq 4
expect(@result[0][0]).to eq :TERM_WITH_QUOTES
expect(@result[0][1]).to eq '"land"'
expect(@result[1][0]).to eq :TERM_WITH_QUOTES
expect(@result[1][1]).to eq '"123-andrew"'
expect(@result[2][0]).to eq :OPERATOR_OR
expect(@result[3][0]).to eq :TERM_WITH_QUOTES
expect(@result[3][1]).to eq '"-andrew-"'
end
it 'tests query with multiple search terms with mixed and-or-not on start' do
@result = @evaluator.tokenize('sindy andora ornoty notary')
expect(@result.length).to eq 4
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[0][1]).to eq 'sindy'
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[1][1]).to eq 'andora'
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[2][1]).to eq 'ornoty'
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[3][1]).to eq 'notary'
end
it 'tests query with multiple search terms with mixed and-or-not on start and with logical operators between' do
@result = @evaluator.tokenize('sindy and andora-notary or not ornoty notary')
expect(@result.length).to eq 7
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[0][1]).to eq 'sindy'
expect(@result[1][0]).to eq :OPERATOR_AND
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[2][1]).to eq 'andora-notary'
expect(@result[3][0]).to eq :OPERATOR_OR
expect(@result[4][0]).to eq :OPERATOR_NOT
expect(@result[5][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[5][1]).to eq 'ornoty'
expect(@result[6][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[6][1]).to eq 'notary'
end
it 'tests query with multiple search terms with mixed and-or-not on start and with logical operators between' do
@result = @evaluator.tokenize('(sindy and andora-notary)or not(ornoty notary)')
expect(@result.length).to eq 11
expect(@result[0][0]).to eq :L_BRACKET
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[1][1]).to eq 'sindy'
expect(@result[2][0]).to eq :OPERATOR_AND
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[3][1]).to eq 'andora-notary'
expect(@result[4][0]).to eq :R_BRACKET
expect(@result[5][0]).to eq :OPERATOR_OR
expect(@result[6][0]).to eq :OPERATOR_NOT
expect(@result[7][0]).to eq :L_BRACKET
expect(@result[8][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[8][1]).to eq 'ornoty'
expect(@result[9][0]).to eq :TERM_WITHOUT_QUOTES
expect(@result[9][1]).to eq 'notary'
expect(@result[10][0]).to eq :R_BRACKET
end
end
end

View File

@@ -1,4 +1,4 @@
require_relative '../../lib/parser'
require './parser'
class QueryParserTester
describe 'Testing the Parser' do
@@ -133,7 +133,7 @@ class QueryParserTester
expect(@result[:OPERATOR_AND]).to eq @expected_array_total
end
it 'tests operator precedence, or - and' do
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)')
@@ -155,469 +155,10 @@ class QueryParserTester
end
it 'tests operator precedence, and - or' do
@result1 = @evaluator.parse('tag:mta and name:JF or 12_4')
@result2 = @evaluator.parse('(tag:mta and name:JF) or 12_4')
# Tests to write :
# * query with multiple column names and search terms without logical operators
# * 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

View 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

View File

@@ -1,78 +1,61 @@
# 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
# tests simple search term without column name and without quotes
it { expect(described_class.new('some-default-value', [:"players.name"], :"players.name").where_clause).to eq(['(CAST(players.name AS TEXT) ILIKE ?)', '%some-default-value%']) }
# tests simple search with two default columns without quotes
it { expect(described_class.new('search-term', [:'players.name', :'players.tags', 'players.id'], [:'players.name', :'players.tags']).where_clause).to eq(['(CAST(players.name AS TEXT) ILIKE ? OR CAST(players.tags AS TEXT) ILIKE ?)', '%search-term%', '%search-term%']) }
# tests simple search with two default columns with quotes
it { expect(described_class.new('"search-term"', [:'players.name', :'players.tags', 'players.id'], [:'players.name', :'players.tags']).where_clause).to eq(['(CAST(players.name AS TEXT) ILIKE ? OR CAST(players.tags AS TEXT) ILIKE ?)', '%search-term%', '%search-term%']) }
# tests simple search with three default columns without quotes
it { expect(described_class.new('search-term', [:'players.name', :'players.tags', 'players.id'], [:'players.name', :'players.tags', :'players.id']).where_clause).to eq(['(CAST(players.name AS TEXT) ILIKE ? OR CAST(players.tags AS TEXT) ILIKE ? OR CAST(players.id AS TEXT) ILIKE ?)', '%search-term%', '%search-term%', '%search-term%']) }
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 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
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
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
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
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
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
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
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
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
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'
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
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
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
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
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
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
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

View 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

View File

@@ -2,19 +2,7 @@
class VectorModel < ActiveRecord::Base
include PgSearchable
pg_search fields: %i[vector_models.id vector_models.name vector_models.value], cache: :search_cache
end
class VectorModelWithTwoDefaultColumns < ActiveRecord::Base
self.table_name = :vector_models
include PgSearchable
pg_search fields: %i[vector_models.id vector_models.name vector_models.value], default_fields: %i[vector_models.name vector_models.value], cache: :search_cache
end
class VectorModelWithMappings < ActiveRecord::Base
self.table_name = :vector_models
include PgSearchable
pg_search fields: %i[vector_models.id vector_models.name vector_models.value], fields_mappings: {device_id: "vector_models.id"}, cache: :search_cache
pg_search fields: %i[id name value], cache: :search_cache
end
class VectorModelWithoutCallback < ActiveRecord::Base
@@ -44,7 +32,7 @@ end
class SimpleVectorModel < ActiveRecord::Base
self.table_name = :vector_models
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
class VectorWithoutWildcardModel < ActiveRecord::Base
@@ -78,7 +66,7 @@ end
class DynamicModelWithTagValues < ActiveRecord::Base
self.table_name = :dynamic_models
include PgSearchable
pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value], 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

View File

@@ -3,7 +3,7 @@
module Version
MAJOR = 1
MINOR = 0
PATCH = 27
PATCH = 13
def self.to_s
[MAJOR, MINOR, PATCH].compact.join('.')