New fix #1
4
Gemfile
4
Gemfile
@@ -23,6 +23,6 @@ group :development do
|
||||
gem 'rubocop', '~> 0.54', require: false
|
||||
gem 'rubocop-rspec', '~> 1.23', require: false
|
||||
gem 'simplecov', '~> 0.16', require: false
|
||||
gem "rexical", '~> 1.0', require: false
|
||||
gem "racc", '~> 1.4', require: false
|
||||
gem 'rexical', '~> 1.0', require: false
|
||||
gem 'racc', '~> 1.4', require: false
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"result": {
|
||||
"covered_percent": 66.92
|
||||
"covered_percent": 78.1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,69 +9,73 @@
|
||||
1,
|
||||
1,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
null,
|
||||
0,
|
||||
1,
|
||||
13,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
13,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
13,
|
||||
13,
|
||||
24,
|
||||
23,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
24,
|
||||
null,
|
||||
24,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
16,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
13,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
@@ -181,57 +185,121 @@
|
||||
null
|
||||
],
|
||||
"/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/text_to_sql_query.rb": [
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
1,
|
||||
44,
|
||||
44,
|
||||
44,
|
||||
44,
|
||||
114,
|
||||
114,
|
||||
114,
|
||||
null,
|
||||
44,
|
||||
13,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
44,
|
||||
44,
|
||||
44,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
128,
|
||||
128,
|
||||
128,
|
||||
null,
|
||||
23,
|
||||
23,
|
||||
null,
|
||||
22,
|
||||
null,
|
||||
15,
|
||||
null,
|
||||
10,
|
||||
null,
|
||||
10,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
10,
|
||||
10,
|
||||
null,
|
||||
10,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
58,
|
||||
58,
|
||||
58,
|
||||
2,
|
||||
null,
|
||||
56,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
37,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
37,
|
||||
37,
|
||||
null,
|
||||
37,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
37,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
37,
|
||||
37,
|
||||
null,
|
||||
37,
|
||||
37,
|
||||
null,
|
||||
37,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
81,
|
||||
81,
|
||||
81,
|
||||
81,
|
||||
81,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/parser.rb": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@@ -240,6 +308,7 @@
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@@ -248,22 +317,29 @@
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@@ -273,9 +349,255 @@
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"/home/hamo/projects/toptal/outfrontmedia/pg_searchable/lib/lexer.rb": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
132,
|
||||
132,
|
||||
132,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
702,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
94,
|
||||
94,
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
834,
|
||||
null,
|
||||
null,
|
||||
703,
|
||||
703,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
939,
|
||||
939,
|
||||
939,
|
||||
null,
|
||||
null,
|
||||
939,
|
||||
null,
|
||||
null,
|
||||
702,
|
||||
106,
|
||||
null,
|
||||
649,
|
||||
106,
|
||||
null,
|
||||
596,
|
||||
84,
|
||||
null,
|
||||
554,
|
||||
84,
|
||||
null,
|
||||
512,
|
||||
46,
|
||||
null,
|
||||
489,
|
||||
132,
|
||||
null,
|
||||
423,
|
||||
610,
|
||||
null,
|
||||
118,
|
||||
236,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
939,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
null,
|
||||
237,
|
||||
null,
|
||||
null,
|
||||
1,
|
||||
38,
|
||||
38,
|
||||
38,
|
||||
174,
|
||||
null,
|
||||
38,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"timestamp": 1580827025
|
||||
"timestamp": 1583928139
|
||||
}
|
||||
}
|
||||
|
||||
2462
coverage/index.html
2462
coverage/index.html
File diff suppressed because it is too large
Load Diff
@@ -66,13 +66,13 @@ class Query < Racc::Parser
|
||||
when (text = @ss.scan(/\)/))
|
||||
action { return [:R_BRACKET, text] }
|
||||
|
||||
when (text = @ss.scan(/(?i)or/))
|
||||
when (text = @ss.scan(/(?i)\bor\b/))
|
||||
action { return [:OPERATOR_OR, text] }
|
||||
|
||||
when (text = @ss.scan(/(?i)and/))
|
||||
when (text = @ss.scan(/(?i)\band\b/))
|
||||
action { return [:OPERATOR_AND, text] }
|
||||
|
||||
when (text = @ss.scan(/(?i)not/))
|
||||
when (text = @ss.scan(/(?i)\bnot\b/))
|
||||
action { return [:OPERATOR_NOT, text] }
|
||||
|
||||
when (text = @ss.scan(/"([^"]*)"/))
|
||||
|
||||
@@ -36,20 +36,23 @@ module PgSearchable
|
||||
@ts_skip_cache_update = skip_callback
|
||||
@ts_wildcard = wildcard
|
||||
@ts_joins = joins
|
||||
@default_field = (default_field.to_sym || fields.first)
|
||||
@default_field = default_field.to_s.empty? ? fields.first : default_field.to_sym
|
||||
ts_add_scope
|
||||
end
|
||||
|
||||
def ts_add_scope
|
||||
class_eval do
|
||||
scope ts_scope_method, ->(value) { ts_search(value) }
|
||||
scope ts_scope_method, ->(value) do
|
||||
resulting_ids = ts_search(value).map(&:id)
|
||||
where(id: resulting_ids)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ts_search(value)
|
||||
return if @ts_search_fields.blank? || value.blank?
|
||||
includes(@ts_joins).references(:all).where(
|
||||
TextToSqlQuery.new(value, @ts_search_fields, @default_field, @ts_search_fields_mappings).where_clause)
|
||||
TextToSqlQuery.new(value, @ts_search_fields, @default_field, @ts_search_fields_mappings).where_clause).distinct
|
||||
end
|
||||
|
||||
def should_update_cache_field?
|
||||
|
||||
@@ -3,9 +3,9 @@ macro
|
||||
L_BRACKET \(
|
||||
R_BRACKET \)
|
||||
SPACE \ + # Space char
|
||||
OPERATOR_OR (?i)or
|
||||
OPERATOR_AND (?i)and
|
||||
OPERATOR_NOT (?i)not
|
||||
OPERATOR_OR (?i)\bor\b
|
||||
OPERATOR_AND (?i)\band\b
|
||||
OPERATOR_NOT (?i)\bnot\b
|
||||
TERM_WITH_QUOTES "([^"]*)"
|
||||
TERM_WITHOUT_QUOTES [a-zA-Z0-9\-_]+
|
||||
COLON \:
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# transforms "english like" text queries into a where clause with regex
|
||||
# https://www.postgresql.org/docs/9.5/textsearch-controls.html#TEXTSEARCH-PARSING-QUERIES
|
||||
|
||||
class TextToRegexQuery
|
||||
def initialize(text, fields, default_field, fields_mappings = {})
|
||||
@text = text.to_s.strip
|
||||
@fields = fields.map(&:to_sym)
|
||||
@default_field = default_field.to_sym
|
||||
@fields_mappings = fields_mappings.merge(@fields.reduce({}) do |mappings, field|
|
||||
table_name, field_name = field.to_s.split(".")
|
||||
mappings[field_name.to_sym] = field
|
||||
mappings
|
||||
end)
|
||||
end
|
||||
|
||||
def where_clause(query)
|
||||
@cleared_text = @text.dup
|
||||
@column_chunks = []
|
||||
remove_duplicated_spaces
|
||||
extract_columns
|
||||
escape_special_characters
|
||||
generate_where_clause(query)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def remove_duplicated_spaces
|
||||
@cleared_text.gsub!(/\s+/, ' ')
|
||||
end
|
||||
|
||||
def escape_special_characters
|
||||
@cleared_text.gsub!(/\_/, '\_')
|
||||
@cleared_text.tr!('\\', '\\')
|
||||
@cleared_text.gsub!(/%/, '\%')
|
||||
end
|
||||
|
||||
def extract_columns
|
||||
column_search_term_pairs = @cleared_text.scan(/([A-Za-z0-9_]+:[\w\_-]+)/)
|
||||
|
||||
@column_chunks = (column_search_term_pairs.flatten.map do |pair|
|
||||
column, term = pair.split(':')
|
||||
next unless @fields_mappings.include?(column.to_sym)
|
||||
@cleared_text.gsub!(pair, '')
|
||||
{ @fields_mappings[column.to_sym] => term }
|
||||
end).compact
|
||||
unless @cleared_text.strip.empty?
|
||||
@column_chunks = [{ @default_field.to_s => @cleared_text.strip }] + @column_chunks
|
||||
end
|
||||
@column_chunks
|
||||
end
|
||||
|
||||
def generate_where_clause(query)
|
||||
where_clause = ''
|
||||
columns = @column_chunks.map { |c| c.keys.first }
|
||||
values = @column_chunks.map { |c| c.values.first }
|
||||
|
||||
columns.each do |column|
|
||||
quoted_column = '"' + column.to_s.gsub(".",'"."') + '"'
|
||||
where_clause += "#{quoted_column} ILIKE ? OR "
|
||||
end
|
||||
where_clause += " 1<>1 "
|
||||
regexed_values = values.map { |v| "%#{v}%" }
|
||||
query.where([where_clause] + regexed_values)
|
||||
end
|
||||
end
|
||||
@@ -29,7 +29,7 @@ class TextToSqlQuery
|
||||
case first_key
|
||||
when :DEFAULT_COLUMN
|
||||
escaped_node_value = handle_special_chars node_value
|
||||
["#{@default_field.to_s} ILIKE ?", "%#{escaped_node_value}%"]
|
||||
["CAST(#{@default_field.to_s} AS TEXT) ILIKE ?", "%#{escaped_node_value}%"]
|
||||
when :OPERATOR_OR
|
||||
generate_expression_for_logical_operator(:OR, node_value)
|
||||
when :OPERATOR_AND
|
||||
@@ -53,7 +53,7 @@ class TextToSqlQuery
|
||||
if mapping.nil?
|
||||
raise "Unknown field '#{first_key.to_s}'"
|
||||
else
|
||||
["#{mapping.to_s} ILIKE ?", "%#{escaped_node_value}%"]
|
||||
["CAST(#{mapping.to_s} AS TEXT) ILIKE ?", "%#{escaped_node_value}%"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
181
spec/lib/pg_searchable_new_spec.rb
Normal file
181
spec/lib/pg_searchable_new_spec.rb
Normal file
@@ -0,0 +1,181 @@
|
||||
# frozen_string_literal: true
|
||||
require_relative '../../lib/pg_searchable_regex'
|
||||
|
||||
describe PgSearchable do
|
||||
describe 'pg_search' do
|
||||
|
||||
describe 'properties' do
|
||||
describe 'scope' do
|
||||
it 'defaults to "scope_search"' do
|
||||
expect(VectorModel).to respond_to(:scope_search)
|
||||
end
|
||||
|
||||
it 'can use a different scope name' do
|
||||
expect(VectorModelWithCustomSearchScope).to respond_to(:fulltext)
|
||||
end
|
||||
|
||||
it 'doesnt pollutes the default method name if customized' do
|
||||
expect(VectorModelWithCustomSearchScope).not_to respond_to(:scope_search)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'language' do
|
||||
it 'defaults to english lexemes' do
|
||||
record = VectorModel.create name: 'something', value: 'amazing'
|
||||
expect(VectorModel.scope_search('value:amaz')).to include(record)
|
||||
end
|
||||
|
||||
it 'can be changed to simple to avoid lexeme truncation' do
|
||||
record = SimpleVectorModel.create name: 'something', value: 'amazing'
|
||||
expect(SimpleVectorModel.scope_search('value:amazings')).not_to include(record)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'single model' do
|
||||
it 'fails if column name is unknown' do
|
||||
record1 = VectorModel.create name: 'hamo', value: '100'
|
||||
|
||||
expect{(VectorModel.scope_search("device_id:#{record1.id}"))}.to raise_error(RuntimeError, "Unknown field 'device_id'")
|
||||
end
|
||||
|
||||
it 'searches only default column if no column name is used' do
|
||||
record1 = VectorModel.create name: 'hamo', value: '-45'
|
||||
record2 = VectorModel.create name: 'meho', value: '120'
|
||||
|
||||
expect(VectorModel.scope_search('45')).to be_empty
|
||||
expect(VectorModel.scope_search("#{record2.id}")).to contain_exactly(record2)
|
||||
end
|
||||
|
||||
it 'searches column declared in search term' do
|
||||
record1 = VectorModel.create name: 'hamo', value: '-45'
|
||||
record2 = VectorModel.create name: 'meho', value: '120'
|
||||
|
||||
expect(VectorModel.scope_search('name:45')).to be_empty
|
||||
expect(VectorModel.scope_search('value:120')).to contain_exactly(record2)
|
||||
end
|
||||
|
||||
it 'searches column declared in search term with only partial match' do
|
||||
record1 = VectorModel.create name: 'hamo', value: '-45'
|
||||
record2 = VectorModel.create name: 'meho', value: '120'
|
||||
|
||||
expect(VectorModel.scope_search('value:-4')).to contain_exactly(record1)
|
||||
expect(VectorModel.scope_search('value:12')).to contain_exactly(record2)
|
||||
end
|
||||
|
||||
it 'handles multiple search terms without logical operator between' do
|
||||
record1 = VectorModel.create name: 'hamo', value: '-45'
|
||||
record2 = VectorModel.create name: 'meho', value: '120'
|
||||
record3 = VectorModel.create name: 'atif', value: '80'
|
||||
|
||||
expect(VectorModel.scope_search('value:-4 name:meho')).to contain_exactly(record1, record2)
|
||||
end
|
||||
|
||||
it 'handles multiple search terms with AND operator between' do
|
||||
record1 = VectorModel.create name: 'hamo', value: '-45'
|
||||
record2 = VectorModel.create name: 'meho', value: '120'
|
||||
record3 = VectorModel.create name: 'atif', value: '80'
|
||||
|
||||
expect(VectorModel.scope_search('value:-4 AND name:meho')).to be_empty
|
||||
end
|
||||
|
||||
it 'handles search term with NOT operator' do
|
||||
record1 = VectorModel.create name: 'hamo', value: '-45'
|
||||
record2 = VectorModel.create name: 'meho', value: '120'
|
||||
record3 = VectorModel.create name: 'atif', value: '80'
|
||||
|
||||
expect(VectorModel.scope_search('NOT value:0')).to contain_exactly(record1)
|
||||
end
|
||||
|
||||
it 'handles multiple search terms with mixed logical operators between without brackets' do
|
||||
record1 = VectorModel.create name: 'hamo', value: '-45'
|
||||
record2 = VectorModel.create name: 'meho', value: '120'
|
||||
record3 = VectorModel.create name: 'atif', value: '80'
|
||||
|
||||
expect(VectorModel.scope_search('name:hamo AND value:120 OR value:80')).to contain_exactly(record3)
|
||||
end
|
||||
|
||||
it 'handles multiple search terms with mixed logical operators between with brackets' do
|
||||
record1 = VectorModel.create name: 'hamo', value: '-45'
|
||||
record2 = VectorModel.create name: 'meho', value: '120'
|
||||
record3 = VectorModel.create name: 'atif', value: '80'
|
||||
|
||||
expect(VectorModel.scope_search('name:hamo AND (value:120 OR value:80)')).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe 'field mappings and joins' do
|
||||
it 'does not fail if column is unknown but mapping with that name exists' do
|
||||
record1 = VectorModelWithMappings.create name: 'hamo', value: '-45'
|
||||
|
||||
expect(VectorModelWithMappings.scope_search("device_id:#{record1.id}")).to contain_exactly(record1)
|
||||
end
|
||||
|
||||
it 'can search in referenced column' do
|
||||
record = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
|
||||
Tag.create(taggable: record, value: 'red')
|
||||
|
||||
expect(DynamicModelWithTagValues.scope_search('tag:red')).to contain_exactly(record)
|
||||
end
|
||||
|
||||
it 'can search in referenced column with multiple search terms' do
|
||||
record1 = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
|
||||
record2 = DynamicModelWithTagValues.create name: 'new record', value: 'not so amazing'
|
||||
Tag.create(taggable: record1, value: 'red')
|
||||
Tag.create(taggable: record2, value: 'green')
|
||||
|
||||
expect(DynamicModelWithTagValues.scope_search('tag:red tag:green')).to contain_exactly(record1, record2)
|
||||
end
|
||||
|
||||
it 'can search in referenced column and in model columns with multiple search terms' do
|
||||
record1 = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
|
||||
record2 = DynamicModelWithTagValues.create name: 'new record', value: 'not so amazing'
|
||||
Tag.create(taggable: record1, value: 'red')
|
||||
Tag.create(taggable: record2, value: 'green')
|
||||
|
||||
expect(DynamicModelWithTagValues.scope_search('tag:red value:"not"')).to contain_exactly(record1, record2)
|
||||
end
|
||||
|
||||
it 'can find models without tags' do
|
||||
record1 = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
|
||||
record2 = DynamicModelWithTagValues.create name: 'new record', value: 'not so amazing'
|
||||
Tag.create(taggable: record1, value: 'green')
|
||||
|
||||
expect(DynamicModelWithTagValues.scope_search('tag:green or value:"not"')).to contain_exactly(record1, record2)
|
||||
end
|
||||
|
||||
|
||||
|
||||
it 'can search in referenced column and in model columns with multiple search terms connected with logical operators' do
|
||||
record1 = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
|
||||
record2 = DynamicModelWithTagValues.create name: 'new record', value: 'not so amazing'
|
||||
record3 = DynamicModelWithTagValues.create name: 'last one', value: 'no value'
|
||||
record4 = DynamicModelWithTagValues.create name: 'really last one', value: 'no value'
|
||||
Tag.create(taggable: record1, value: 'red')
|
||||
Tag.create(taggable: record1, value: 'green')
|
||||
Tag.create(taggable: record2, value: 'black')
|
||||
Tag.create(taggable: record3, value: '-12')
|
||||
Tag.create(taggable: record4, value: '-')
|
||||
|
||||
expect(DynamicModelWithTagValues.scope_search('tag:red or tag:black')).to contain_exactly(record1, record2)
|
||||
expect(DynamicModelWithTagValues.scope_search('tag:red and tag:black')).to be_empty
|
||||
expect(DynamicModelWithTagValues.scope_search('tag:red or tag:green')).to contain_exactly(record1)
|
||||
expect(DynamicModelWithTagValues.scope_search('not tag:-12 and not value:amazing')).to contain_exactly(record4)
|
||||
end
|
||||
|
||||
it 'can search in referenced column and in model columns with multiple search terms connected with logical operators and with brackets' do
|
||||
record1 = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
|
||||
record2 = DynamicModelWithTagValues.create name: 'new record', value: 'not so amazing'
|
||||
record3 = DynamicModelWithTagValues.create name: 'last one', value: 'no value'
|
||||
record4 = DynamicModelWithTagValues.create name: 'really last one', value: 'no value'
|
||||
Tag.create(taggable: record1, value: 'red')
|
||||
Tag.create(taggable: record1, value: 'green')
|
||||
Tag.create(taggable: record2, value: 'black')
|
||||
Tag.create(taggable: record3, value: '-12')
|
||||
Tag.create(taggable: record4, value: '-')
|
||||
|
||||
expect(DynamicModelWithTagValues.scope_search('(tag:- and not tag:12) or (value:"amazing" and not value:"not")')).to contain_exactly(record1, record4)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,115 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe PgSearchable do
|
||||
include_examples 'pg_search', VectorModel
|
||||
include_examples 'pg_search', VectorWithCustomPrimaryKeyModel
|
||||
include_examples 'pg_search', VectorWithCustomCallback
|
||||
include_examples 'pg_search', SimpleVectorModel
|
||||
include_examples 'pg_search', VectorWithoutWildcardModel
|
||||
include_examples 'pg_search', VectorModelWithCustomSearchScope, 'fulltext'
|
||||
include_examples 'pg_search', VectorModelWithTagValues
|
||||
include_examples 'pg_search', DynamicModel
|
||||
include_examples 'pg_search', DynamicModelWithTagValues
|
||||
include_examples 'pg_search', DynamicModelWithCategory
|
||||
include_examples 'pg_search', DynamicModelWithSectionsTrhough
|
||||
|
||||
describe 'pg_search' do
|
||||
describe 'joins' do
|
||||
it 'can dynamically query compound relation' do
|
||||
record = DynamicModelWithCategory.create(name: 'something', value: 'amazing')
|
||||
category = Category.create(name: 'searchable')
|
||||
Tag.create(value: 'impressive', category: category, taggable: record)
|
||||
expect(DynamicModelWithCategory.scope_search('searchable')).to include(record)
|
||||
end
|
||||
|
||||
it 'can use has_many :through relation' do
|
||||
record = DynamicModelWithSectionsTrhough.create(name: 'something', value: 'amazing')
|
||||
tag = Tag.create(value: 'impressive', taggable: record)
|
||||
Section.create(name: 'searchable', tag: tag)
|
||||
expect(DynamicModelWithSectionsTrhough.scope_search('searchable')).to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'properties' do
|
||||
describe 'skip_callback' do
|
||||
context 'when enabled' do
|
||||
let(:record) { VectorModel.create(name: 'something', value: 'amazing') }
|
||||
|
||||
it 'can find the record after it updates' do
|
||||
record.update(name: 'cookie')
|
||||
expect(VectorModel.scope_search('cookie')).to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when disabled' do
|
||||
let(:record) { VectorModelWithoutCallback.create(name: 'something', value: 'amazing') }
|
||||
|
||||
it 'cannot find the record after it updates' do
|
||||
record.update(name: 'cookie')
|
||||
expect(VectorModelWithoutCallback.scope_search('cookie')).not_to include(record)
|
||||
end
|
||||
|
||||
it 'can find the record after manually calling .update_pg_search_cache' do
|
||||
record.update(name: 'cookie')
|
||||
record.update_pg_search_cache
|
||||
expect(VectorModelWithoutCallback.scope_search('cookie')).to include(record)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'scope' do
|
||||
it 'defaults to "scope_search"' do
|
||||
expect(VectorModel).to respond_to(:scope_search)
|
||||
end
|
||||
|
||||
it 'can use a different scope name' do
|
||||
expect(VectorModelWithCustomSearchScope).to respond_to(:fulltext)
|
||||
end
|
||||
|
||||
it 'doesnt pollutes the default method name if customized' do
|
||||
expect(VectorModelWithCustomSearchScope).not_to respond_to(:scope_search)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'language' do
|
||||
it 'defaults to english lexemes' do
|
||||
record = VectorModel.create name: 'something', value: 'amazing'
|
||||
expect(VectorModel.scope_search('amaz')).to include(record)
|
||||
end
|
||||
|
||||
it 'can be changed to simple to avoid lexeme truncation' do
|
||||
record = SimpleVectorModel.create name: 'something', value: 'amazing'
|
||||
expect(SimpleVectorModel.scope_search('amazings')).not_to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'wildcard' do
|
||||
it 'by default uses it' do
|
||||
record = VectorModel.create name: '12345', value: 'amazing'
|
||||
expect(VectorModel.scope_search('123')).to include(record)
|
||||
end
|
||||
|
||||
it 'can be set to false' do
|
||||
record = VectorWithoutWildcardModel.create name: '12345', value: 'amazing'
|
||||
expect(VectorWithoutWildcardModel.scope_search('123')).not_to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'tags' do
|
||||
it 'allow indexing fields of other associations' do
|
||||
record = DynamicModelWithTagValues.create name: 'something', value: 'amazing'
|
||||
Tag.create(taggable: record, value: 'red')
|
||||
expect(DynamicModelWithTagValues.scope_search('red')).to include(record)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'external_cache_data' do
|
||||
it 'can index external data using a method' do
|
||||
record = VectorModelWithTagValues.create name: 'something', value: 'amazing'
|
||||
Tag.create(taggable: record, value: 'red')
|
||||
expect(VectorModelWithTagValues.scope_search('red')).to include(record)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -255,5 +255,278 @@ class QueryLexerTester
|
||||
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[2][1]).to eq '"tag with or and not inside"'
|
||||
end
|
||||
|
||||
it 'tests query without column name containing AND word inside without quotes' do
|
||||
@result = @evaluator.tokenize('land')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'land'
|
||||
end
|
||||
|
||||
it 'tests query without column name containing AND word inside with quotes' do
|
||||
@result = @evaluator.tokenize('"land"')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[0][1]).to eq '"land"'
|
||||
end
|
||||
|
||||
it 'tests query without column name and with word starting with AND without quotes' do
|
||||
@result = @evaluator.tokenize('andromeda')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'andromeda'
|
||||
end
|
||||
|
||||
it 'tests query without column name and with word starting with AND with quotes' do
|
||||
@result = @evaluator.tokenize('"andromeda"')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[0][1]).to eq '"andromeda"'
|
||||
end
|
||||
|
||||
it 'tests query with column name containing AND word inside' do
|
||||
@result = @evaluator.tokenize('land:some-search-term')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'land'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq 'some-search-term'
|
||||
end
|
||||
|
||||
it 'tests query with column name starting with AND' do
|
||||
@result = @evaluator.tokenize('andromeda:some-search-term')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'andromeda'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq 'some-search-term'
|
||||
end
|
||||
|
||||
it 'tests query with search term without quotes containing AND word inside' do
|
||||
@result = @evaluator.tokenize('name:land')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'name'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq 'land'
|
||||
end
|
||||
|
||||
it 'tests query with search term with quotes containing AND word inside' do
|
||||
@result = @evaluator.tokenize('name:"land"')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'name'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[2][1]).to eq '"land"'
|
||||
end
|
||||
|
||||
it 'tests query with search term without quotes and search term starting with AND without quotes' do
|
||||
@result = @evaluator.tokenize('name:andromeda')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'name'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq 'andromeda'
|
||||
end
|
||||
|
||||
it 'tests query with search term without quotes and search term starting with AND with quotes' do
|
||||
@result = @evaluator.tokenize('name:"andrew"')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'name'
|
||||
expect(@result[1][0]).to eq :COLON
|
||||
expect(@result[2][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[2][1]).to eq '"andrew"'
|
||||
end
|
||||
|
||||
it 'tests query with search term in brackets with AND inside word' do
|
||||
@result = @evaluator.tokenize('(land)')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'land'
|
||||
expect(@result[2][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests query with search term in brackets starting with AND' do
|
||||
@result = @evaluator.tokenize('(andromeda)')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'andromeda'
|
||||
expect(@result[2][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests query with search term in brackets with AND inside word with quotes' do
|
||||
@result = @evaluator.tokenize('("land")')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[1][1]).to eq '"land"'
|
||||
expect(@result[2][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests query with search term in brackets starting with AND with quotes' do
|
||||
@result = @evaluator.tokenize('("andromeda")')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[1][1]).to eq '"andromeda"'
|
||||
expect(@result[2][0]).to eq :R_BRACKET
|
||||
end
|
||||
|
||||
it 'tests query with multiple search terms with mixed and-or-not after dash and underscore' do
|
||||
@result = @evaluator.tokenize('123-and-456 -or-2 -not_not_1')
|
||||
|
||||
expect(@result.length).to eq 3
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq '123-and-456'
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq '-or-2'
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq '-not_not_1'
|
||||
end
|
||||
|
||||
it 'tests query with multiple search terms starting with AND' do
|
||||
@result = @evaluator.tokenize('land andrew and andromeda or orlando')
|
||||
|
||||
expect(@result.length).to eq 6
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'land'
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'andrew'
|
||||
expect(@result[2][0]).to eq :OPERATOR_AND
|
||||
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[3][1]).to eq 'andromeda'
|
||||
expect(@result[4][0]).to eq :OPERATOR_OR
|
||||
expect(@result[5][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[5][1]).to eq 'orlando'
|
||||
end
|
||||
|
||||
it 'tests query with multiple search terms with dash and underscore chars' do
|
||||
@result = @evaluator.tokenize('land 123-andrew or -andrew- and not _ornela_ or ornela')
|
||||
|
||||
expect(@result.length).to eq 9
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'land'
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq '123-andrew'
|
||||
expect(@result[2][0]).to eq :OPERATOR_OR
|
||||
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[3][1]).to eq '-andrew-'
|
||||
expect(@result[4][0]).to eq :OPERATOR_AND
|
||||
expect(@result[5][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[6][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[6][1]).to eq '_ornela_'
|
||||
expect(@result[7][0]).to eq :OPERATOR_OR
|
||||
expect(@result[8][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[8][1]).to eq 'ornela'
|
||||
|
||||
end
|
||||
|
||||
it 'tests query with multiple search terms with dash and underscore chars under quotes' do
|
||||
@result = @evaluator.tokenize('"land" "123-andrew" or "-andrew-"')
|
||||
|
||||
expect(@result.length).to eq 4
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[0][1]).to eq '"land"'
|
||||
expect(@result[1][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[1][1]).to eq '"123-andrew"'
|
||||
expect(@result[2][0]).to eq :OPERATOR_OR
|
||||
expect(@result[3][0]).to eq :TERM_WITH_QUOTES
|
||||
expect(@result[3][1]).to eq '"-andrew-"'
|
||||
end
|
||||
|
||||
it 'tests query with multiple search terms with mixed and-or-not on start' do
|
||||
@result = @evaluator.tokenize('sindy andora ornoty notary')
|
||||
|
||||
expect(@result.length).to eq 4
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'sindy'
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'andora'
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq 'ornoty'
|
||||
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[3][1]).to eq 'notary'
|
||||
end
|
||||
|
||||
it 'tests query with multiple search terms with mixed and-or-not on start and with logical operators between' do
|
||||
@result = @evaluator.tokenize('sindy and andora-notary or not ornoty notary')
|
||||
|
||||
expect(@result.length).to eq 7
|
||||
|
||||
expect(@result[0][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[0][1]).to eq 'sindy'
|
||||
expect(@result[1][0]).to eq :OPERATOR_AND
|
||||
expect(@result[2][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[2][1]).to eq 'andora-notary'
|
||||
expect(@result[3][0]).to eq :OPERATOR_OR
|
||||
expect(@result[4][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[5][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[5][1]).to eq 'ornoty'
|
||||
expect(@result[6][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[6][1]).to eq 'notary'
|
||||
end
|
||||
|
||||
it 'tests query with multiple search terms with mixed and-or-not on start and with logical operators between' do
|
||||
@result = @evaluator.tokenize('(sindy and andora-notary)or not(ornoty notary)')
|
||||
|
||||
expect(@result.length).to eq 11
|
||||
|
||||
expect(@result[0][0]).to eq :L_BRACKET
|
||||
expect(@result[1][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[1][1]).to eq 'sindy'
|
||||
expect(@result[2][0]).to eq :OPERATOR_AND
|
||||
expect(@result[3][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[3][1]).to eq 'andora-notary'
|
||||
expect(@result[4][0]).to eq :R_BRACKET
|
||||
expect(@result[5][0]).to eq :OPERATOR_OR
|
||||
expect(@result[6][0]).to eq :OPERATOR_NOT
|
||||
expect(@result[7][0]).to eq :L_BRACKET
|
||||
expect(@result[8][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[8][1]).to eq 'ornoty'
|
||||
expect(@result[9][0]).to eq :TERM_WITHOUT_QUOTES
|
||||
expect(@result[9][1]).to eq 'notary'
|
||||
expect(@result[10][0]).to eq :R_BRACKET
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -133,7 +133,7 @@ class QueryParserTester
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_array_total
|
||||
end
|
||||
|
||||
it 'tests operator precedence' do
|
||||
it 'tests operator precedence, or - and' do
|
||||
@result1 = @evaluator.parse('tag:mta or name:JF and 12_4')
|
||||
@result2 = @evaluator.parse('tag:mta or (name:JF and 12_4)')
|
||||
|
||||
@@ -155,10 +155,469 @@ class QueryParserTester
|
||||
|
||||
end
|
||||
|
||||
# Tests to write :
|
||||
# * query with multiple column names and search terms without logical operators
|
||||
# * AND NOT, OR NOT tests
|
||||
it 'tests operator precedence, and - or' do
|
||||
@result1 = @evaluator.parse('tag:mta and name:JF or 12_4')
|
||||
@result2 = @evaluator.parse('(tag:mta and name:JF) or 12_4')
|
||||
|
||||
expect(@result1).to eq @result2
|
||||
|
||||
expect(@result1.length).to eq 1
|
||||
|
||||
@expected_array_part_2 = [
|
||||
{'tag' => 'mta'},
|
||||
{'name' => 'JF'}
|
||||
]
|
||||
|
||||
@expected_array_total = [
|
||||
{:OPERATOR_AND => @expected_array_part_2},
|
||||
{:DEFAULT_COLUMN => '12_4'}
|
||||
]
|
||||
|
||||
expect(@result1[:OPERATOR_OR]).to eq @expected_array_total
|
||||
|
||||
end
|
||||
|
||||
it 'tests simple query with not operator' do
|
||||
@result = @evaluator.parse('not -54')
|
||||
|
||||
expect(@result.length).to be 1
|
||||
expect(@result[:OPERATOR_NOT][:DEFAULT_COLUMN]).to eq '-54'
|
||||
end
|
||||
|
||||
it 'tests query with mixed NOT and AND logic operator' do
|
||||
@result = @evaluator.parse('name:"some wild name" and not -123-456')
|
||||
|
||||
@expected_and_array = [
|
||||
{'name' => '"some wild name"'},
|
||||
{:OPERATOR_NOT => {:DEFAULT_COLUMN => '-123-456'}}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @expected_and_array
|
||||
end
|
||||
|
||||
it 'tests query with mixed NOT, AND and OR logic operators' do
|
||||
@result = @evaluator.parse('name:"some wild name" or not (tag:mta and -123-456)')
|
||||
|
||||
@expected_and_array_operands = [
|
||||
{'tag' => 'mta'},
|
||||
{:DEFAULT_COLUMN => '-123-456'}
|
||||
]
|
||||
|
||||
@expected_or_array_operands = [
|
||||
{'name' => '"some wild name"'},
|
||||
{:OPERATOR_NOT => {:OPERATOR_AND => @expected_and_array_operands}}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_or_array_operands
|
||||
end
|
||||
|
||||
it 'tests query with two default column search terms without quotes and without logical operators' do
|
||||
@result = @evaluator.parse('id-5 -456')
|
||||
|
||||
@expected_array = [
|
||||
{:DEFAULT_COLUMN => 'id-5'},
|
||||
{:DEFAULT_COLUMN => '-456'}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two default column search terms with quotes and without logical operators' do
|
||||
@result = @evaluator.parse('"id-5 no Q" -456')
|
||||
|
||||
@expected_array = [
|
||||
{:DEFAULT_COLUMN => '"id-5 no Q"'},
|
||||
{:DEFAULT_COLUMN => '-456'}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two default column search terms with quotes on both terms and without logical operators' do
|
||||
@result = @evaluator.parse('"id-5 no Q" "wild id -456"')
|
||||
|
||||
@expected_array = [
|
||||
{:DEFAULT_COLUMN => '"id-5 no Q"'},
|
||||
{:DEFAULT_COLUMN => '"wild id -456"'}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with one default column and one non defualt column, search terms without quotes and without logical operators' do
|
||||
@result = @evaluator.parse('tag:mta -456')
|
||||
|
||||
@expected_array = [
|
||||
{'tag' => 'mta'},
|
||||
{:DEFAULT_COLUMN => '-456'}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with one default column and one non defualt column, search terms with quotes and without logical operators' do
|
||||
@result = @evaluator.parse('-1-23 name:"WILD name"')
|
||||
|
||||
@expected_array = [
|
||||
{:DEFAULT_COLUMN => '-1-23'},
|
||||
{'name' => '"WILD name"'}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two columns, search terms with quotes and without logical operators' do
|
||||
@result = @evaluator.parse('tag:-1-23 name:"WILD name"')
|
||||
|
||||
@expected_array = [
|
||||
{'tag' => '-1-23'},
|
||||
{'name' => '"WILD name"'}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with two columns, search terms with quotes on both and without logical operators' do
|
||||
@result = @evaluator.parse('tag:"1 2 3" name:"WILD name"')
|
||||
|
||||
@expected_array = [
|
||||
{'tag' => '"1 2 3"'},
|
||||
{'name' => '"WILD name"'}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests query with three columns, search terms with quotes on both and without logical operators' do
|
||||
@result = @evaluator.parse('tag:"1 2 3" name:"WILD name" name:"ANOTHER wild name"')
|
||||
|
||||
@expected_array = [
|
||||
{'tag' => '"1 2 3"'},
|
||||
{:OPERATOR_OR => [
|
||||
{'name' => '"WILD name"'},
|
||||
{'name' => '"ANOTHER wild name"'}
|
||||
]}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_array
|
||||
end
|
||||
|
||||
it 'tests complex query' do
|
||||
@result = @evaluator.parse('(device-id:"with space" tag:mta no-quotes-id-123)'\
|
||||
'or "id with quotes-5" and ( ("id with q 10" or "id with q 20")'\
|
||||
'and ("id with Q 30" "id with Q 40") and not id-without-Q-50)')
|
||||
|
||||
# (device-id:"with space" tag:mta no-quotes-id-123) or "id with quotes-5" and ( ("id with q 10" or "id with q 20") and ("id with Q 30" "id with Q 40") and not id-without-Q-50)
|
||||
# _____________________A___________________________ or _______B___________ and __________________________________________________C______________________________________________
|
||||
# (____A1________________ ___A2__ ______A3________) or _______B___________ and ( ____________C1____________________ and ______________C2_______________ and ______C3___________)
|
||||
|
||||
|
||||
@A = {:OPERATOR_OR => [
|
||||
{'device-id' => '"with space"'},
|
||||
{:OPERATOR_OR => [
|
||||
{'tag' => 'mta'},
|
||||
{:DEFAULT_COLUMN => 'no-quotes-id-123'}]
|
||||
}
|
||||
]}
|
||||
|
||||
@B = {:DEFAULT_COLUMN => '"id with quotes-5"'}
|
||||
|
||||
|
||||
@C1 = {:OPERATOR_OR => [
|
||||
{:DEFAULT_COLUMN => '"id with q 10"'},
|
||||
{:DEFAULT_COLUMN => '"id with q 20"'}
|
||||
]}
|
||||
|
||||
|
||||
@C2 = {:OPERATOR_OR => [
|
||||
{:DEFAULT_COLUMN => '"id with Q 30"'},
|
||||
{:DEFAULT_COLUMN => '"id with Q 40"'}
|
||||
]}
|
||||
|
||||
@C3 = {:OPERATOR_NOT => {:DEFAULT_COLUMN => 'id-without-Q-50'}}
|
||||
|
||||
@C = {:OPERATOR_AND => [
|
||||
{:OPERATOR_AND => [
|
||||
@C1,
|
||||
@C2
|
||||
]},
|
||||
@C3
|
||||
]}
|
||||
|
||||
@expected_final_array_result = [
|
||||
@A,
|
||||
{:OPERATOR_AND => [
|
||||
@B,
|
||||
@C
|
||||
]}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @expected_final_array_result
|
||||
end
|
||||
|
||||
it 'tests query without column name containing AND word inside without quotes' do
|
||||
@result = @evaluator.parse('land')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq 'land'
|
||||
end
|
||||
|
||||
it 'tests query without column name containing AND word inside with quotes' do
|
||||
@result = @evaluator.parse('"land"')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '"land"'
|
||||
end
|
||||
|
||||
it 'tests query without column name and with word starting with AND without quotes' do
|
||||
@result = @evaluator.parse('andromeda')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq 'andromeda'
|
||||
end
|
||||
|
||||
it 'tests query without column name and with word starting with AND with quotes' do
|
||||
@result = @evaluator.parse('"andromeda"')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '"andromeda"'
|
||||
end
|
||||
|
||||
it 'tests query with column name containing AND word inside' do
|
||||
@result = @evaluator.parse('land:some-search-term')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result['land']).to eq 'some-search-term'
|
||||
end
|
||||
|
||||
it 'tests query with column name starting with AND' do
|
||||
@result = @evaluator.parse('andromeda:some-search-term')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result['andromeda']).to eq 'some-search-term'
|
||||
end
|
||||
|
||||
it 'tests query with search term without quotes containing AND word inside' do
|
||||
@result = @evaluator.parse('name:land')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result['name']).to eq 'land'
|
||||
end
|
||||
|
||||
it 'tests query with search term with quotes containing AND word inside' do
|
||||
@result = @evaluator.parse('name:"land"')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result['name']).to eq '"land"'
|
||||
end
|
||||
|
||||
it 'tests query with search term without quotes and search term starting with AND without quotes' do
|
||||
@result = @evaluator.parse('name:andromeda')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result['name']).to eq 'andromeda'
|
||||
end
|
||||
|
||||
it 'tests query with search term without quotes and search term starting with AND with quotes' do
|
||||
@result = @evaluator.parse('name:"andrew"')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result['name']).to eq '"andrew"'
|
||||
end
|
||||
|
||||
it 'tests query with search term in brackets with AND inside word' do
|
||||
@result = @evaluator.parse('(land)')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq 'land'
|
||||
end
|
||||
|
||||
it 'tests query with search term in brackets starting with AND' do
|
||||
@result = @evaluator.parse('(andromeda)')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq 'andromeda'
|
||||
end
|
||||
|
||||
it 'tests query with multiple search terms with mixed and-or-not after dash and underscore' do
|
||||
@result = @evaluator.parse('123-and-456 -or-2 -not_not_1')
|
||||
|
||||
@or_array_1 = [
|
||||
{:DEFAULT_COLUMN => '-or-2'},
|
||||
{:DEFAULT_COLUMN => '-not_not_1'}
|
||||
]
|
||||
|
||||
@last_or_array = [
|
||||
{:DEFAULT_COLUMN => '123-and-456'},
|
||||
{:OPERATOR_OR => @or_array_1}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @last_or_array
|
||||
end
|
||||
|
||||
it 'tests query with search term in brackets with AND inside word with quotes' do
|
||||
@result = @evaluator.parse('("land")')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '"land"'
|
||||
end
|
||||
|
||||
it 'tests query with search term in brackets starting with AND with quotes' do
|
||||
@result = @evaluator.parse('("andromeda")')
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:DEFAULT_COLUMN]).to eq '"andromeda"'
|
||||
end
|
||||
|
||||
it 'tests query with multiple search terms starting with AND' do
|
||||
@result = @evaluator.parse('land andrew and andromeda or orlando')
|
||||
|
||||
@and_array = [
|
||||
{:DEFAULT_COLUMN=>'andrew'},
|
||||
{:DEFAULT_COLUMN=>'andromeda'}
|
||||
]
|
||||
|
||||
@last_or_array = [
|
||||
{:OPERATOR_AND => @and_array},
|
||||
{:DEFAULT_COLUMN => 'orlando'}
|
||||
]
|
||||
|
||||
@main_or_array = [
|
||||
{:DEFAULT_COLUMN => 'land'},
|
||||
{:OPERATOR_OR => @last_or_array}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @main_or_array
|
||||
end
|
||||
|
||||
it 'tests query with multiple search terms with dash and underscore chars' do
|
||||
@result = @evaluator.parse('land 123-andrew or -andrew- and not _ornela_ or ornela')
|
||||
|
||||
@and_array = [
|
||||
{:DEFAULT_COLUMN=>'-andrew-'},
|
||||
{:OPERATOR_NOT=>{:DEFAULT_COLUMN => '_ornela_'}}
|
||||
]
|
||||
|
||||
@intra_or_array = [
|
||||
{:DEFAULT_COLUMN => '123-andrew'},
|
||||
{:OPERATOR_AND => @and_array}
|
||||
]
|
||||
|
||||
@last_or_array = [
|
||||
{:OPERATOR_OR => @intra_or_array},
|
||||
{:DEFAULT_COLUMN => 'ornela'}
|
||||
]
|
||||
|
||||
@main_or_array = [
|
||||
{:DEFAULT_COLUMN => 'land'},
|
||||
{:OPERATOR_OR => @last_or_array}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @main_or_array
|
||||
|
||||
end
|
||||
|
||||
it 'tests query with multiple search terms with dash and underscore chars under quotes' do
|
||||
@result = @evaluator.parse('"land" "123-andrew" or "-andrew-"')
|
||||
|
||||
|
||||
@last_or_array = [
|
||||
{:DEFAULT_COLUMN => '"123-andrew"'},
|
||||
{:DEFAULT_COLUMN => '"-andrew-"'}
|
||||
]
|
||||
|
||||
@main_or_array = [
|
||||
{:DEFAULT_COLUMN => '"land"'},
|
||||
{:OPERATOR_OR => @last_or_array}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @main_or_array
|
||||
end
|
||||
|
||||
it 'tests query with multiple search terms with mixed and-or-not on start' do
|
||||
@result = @evaluator.parse('sindy andora ornoty notary')
|
||||
|
||||
@or_array_1 = [
|
||||
{:DEFAULT_COLUMN=>'ornoty'},
|
||||
{:DEFAULT_COLUMN=>'notary'}
|
||||
]
|
||||
|
||||
@or_array_2 = [
|
||||
{:DEFAULT_COLUMN => 'andora'},
|
||||
{:OPERATOR_OR => @or_array_1}
|
||||
]
|
||||
|
||||
@main_or_array = [
|
||||
{:DEFAULT_COLUMN => 'sindy'},
|
||||
{:OPERATOR_OR => @or_array_2}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @main_or_array
|
||||
end
|
||||
|
||||
it 'tests query with multiple search terms with mixed and-or-not on start and with logical operators between' do
|
||||
@result = @evaluator.parse('sindy and andora-notary or not ornoty notary')
|
||||
|
||||
@and_array = [
|
||||
{:DEFAULT_COLUMN=>'sindy'},
|
||||
{:DEFAULT_COLUMN=>'andora-notary'}
|
||||
]
|
||||
|
||||
@not_or_array = [
|
||||
{:DEFAULT_COLUMN => 'ornoty'},
|
||||
{:DEFAULT_COLUMN => 'notary'}
|
||||
]
|
||||
|
||||
@main_or_array = [
|
||||
{:OPERATOR_AND => @and_array},
|
||||
{:OPERATOR_NOT => {
|
||||
:OPERATOR_OR => @not_or_array
|
||||
}}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_OR]).to eq @main_or_array
|
||||
end
|
||||
|
||||
it 'tests query with multiple search terms with mixed and-or-not on start and with logical operators between' do
|
||||
@result = @evaluator.parse('(sindy or andora-notary)and not(ornoty notary)')
|
||||
|
||||
@or_array_1 = [
|
||||
{:DEFAULT_COLUMN=>'sindy'},
|
||||
{:DEFAULT_COLUMN=>'andora-notary'}
|
||||
]
|
||||
|
||||
@not_or_array = [
|
||||
{:DEFAULT_COLUMN => 'ornoty'},
|
||||
{:DEFAULT_COLUMN => 'notary'}
|
||||
]
|
||||
|
||||
@main_and_array = [
|
||||
{:OPERATOR_OR => @or_array_1},
|
||||
{:OPERATOR_NOT => {
|
||||
:OPERATOR_OR => @not_or_array
|
||||
}}
|
||||
]
|
||||
|
||||
expect(@result.length).to eq 1
|
||||
expect(@result[:OPERATOR_AND]).to eq @main_and_array
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe TextToRegexQuery do
|
||||
include_examples 'pg_search', SimpleVectorModel
|
||||
describe '.new' do
|
||||
# just default
|
||||
it { expect(described_class.new('some-default-value', [:"players.name"], :"players.name").where_clause(SimpleVectorModel)).to eq([' players.name like ? OR 1<>1', '%some-default-value%']) }
|
||||
|
||||
# default and named
|
||||
it { expect(described_class.new('name:hamo id:1', [:"players.name"], :"players.name").where_clause(SimpleVectorModel)).to eq([' players.name like ? OR players.name like ? OR 1<>1', '%id:1%', '%hamo%']) }
|
||||
|
||||
# escape characters
|
||||
it { expect(described_class.new('name:hamo id:1',[:"players.name"], :"players.name").where_clause(SimpleVectorModel)).to eq([' players.name like ? OR players.name like ? OR 1<>1', '%id:1%', '%hamo%']) }
|
||||
|
||||
# default and explicit with underscore
|
||||
it { expect(described_class.new('device_id:-123', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq([" players.device_id like ? OR 1<>1", "%-123%"]) }
|
||||
|
||||
# default and explicit with underscore with multiple columns
|
||||
it { expect(described_class.new('name:bla device_id:-123', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq([" players.name like ? OR players.device_id like ? OR 1<>1", "%bla%", "%-123%"]) }
|
||||
|
||||
end
|
||||
end
|
||||
@@ -4,58 +4,63 @@ require_relative '../../lib/text_to_sql_query'
|
||||
describe TextToSqlQuery do
|
||||
describe '.new' do
|
||||
# tests simple search term without column name and without quotes
|
||||
it { expect(described_class.new('some-default-value', [:"players.name"], :"players.name").where_clause).to eq(['players.name ILIKE ?', '%some-default-value%']) }
|
||||
it { expect(described_class.new('some-default-value', [:"players.name"], :"players.name").where_clause).to eq(['CAST(players.name AS TEXT) ILIKE ?', '%some-default-value%']) }
|
||||
|
||||
# tests simple search term with column name and without quotes
|
||||
it { expect(described_class.new('name:ab', [:"players.name"], :"players.name").where_clause).to eq(['players.name ILIKE ?', '%ab%']) }
|
||||
it { expect(described_class.new('name:ab', [:"players.name"], :"players.name").where_clause).to eq(['CAST(players.name AS TEXT) ILIKE ?', '%ab%']) }
|
||||
|
||||
# tests simple search term with unknown column name and without quotes
|
||||
it { expect{described_class.new('unknown:ab', [:"players.name"], :"players.name").where_clause}.to raise_error(RuntimeError, "Unknown field 'unknown'") }
|
||||
|
||||
# tests simple search term without column name and with quotes
|
||||
it { expect(described_class.new('"ab"', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["players.device_id ILIKE ?", "%ab%"]) }
|
||||
it { expect(described_class.new('"ab"', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["CAST(players.device_id AS TEXT) ILIKE ?", "%ab%"]) }
|
||||
|
||||
# tests simple search term with column name and with quotes
|
||||
it { expect(described_class.new('tags:"ab"', [:"players.name", :"players.tags"], :"players.device_id").where_clause).to eq(["players.tags ILIKE ?", "%ab%"]) }
|
||||
it { expect(described_class.new('tags:"ab"', [:"players.name", :"players.tags"], :"players.device_id").where_clause).to eq(["CAST(players.tags AS TEXT) ILIKE ?", "%ab%"]) }
|
||||
|
||||
# tests search without operators
|
||||
it { expect(described_class.new('123 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["(players.device_id ILIKE ? OR players.device_id ILIKE ?)", "%123%", "%456%"]) }
|
||||
it { expect(described_class.new('123 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["(CAST(players.device_id AS TEXT) ILIKE ? OR CAST(players.device_id AS TEXT) ILIKE ?)", "%123%", "%456%"]) }
|
||||
|
||||
# tests search with OR operator
|
||||
it { expect(described_class.new('123 or 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["(players.device_id ILIKE ? OR players.device_id ILIKE ?)", "%123%", "%456%"]) }
|
||||
it { expect(described_class.new('123 or 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["(CAST(players.device_id AS TEXT) ILIKE ? OR CAST(players.device_id AS TEXT) ILIKE ?)", "%123%", "%456%"]) }
|
||||
|
||||
# tests search with AND operator
|
||||
it { expect(described_class.new('123 and 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["(players.device_id ILIKE ? AND players.device_id ILIKE ?)", "%123%", "%456%"]) }
|
||||
it { expect(described_class.new('123 and 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["(CAST(players.device_id AS TEXT) ILIKE ? AND CAST(players.device_id AS TEXT) ILIKE ?)", "%123%", "%456%"]) }
|
||||
|
||||
# tests search with NOT operator on default column
|
||||
it { expect(described_class.new('not 23', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["NOT players.device_id ILIKE ?", "%23%"]) }
|
||||
it { expect(described_class.new('not 23', [:"players.name", :"players.device_id"], :"players.device_id").where_clause).to eq(["NOT CAST(players.device_id AS TEXT) ILIKE ?", "%23%"]) }
|
||||
|
||||
# tests search with NOT operator on non-default column
|
||||
it { expect(described_class.new('not value:23', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(["NOT players.value ILIKE ?", "%23%"]) }
|
||||
it { expect(described_class.new('not value:23', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(["NOT CAST(players.value AS TEXT) ILIKE ?", "%23%"]) }
|
||||
|
||||
# tests search with mixed logical operators
|
||||
it { expect(described_class.new('name:ab and not value:hf-1', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['(players.name ILIKE ? AND NOT players.value ILIKE ?)', '%ab%', '%hf-1%']) }
|
||||
it { expect(described_class.new('name:ab and not value:hf-1', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['(CAST(players.name AS TEXT) ILIKE ? AND NOT CAST(players.value AS TEXT) ILIKE ?)', '%ab%', '%hf-1%']) }
|
||||
|
||||
# tests search with mixed logical operators without NOT'
|
||||
it { expect(described_class.new('name:a and name:b or name:c', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['((players.name ILIKE ? AND players.name ILIKE ?) OR players.name ILIKE ?)', '%a%', '%b%', '%c%']) }
|
||||
it { expect(described_class.new('name:a and name:b or name:c', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['((CAST(players.name AS TEXT) ILIKE ? AND CAST(players.name AS TEXT) ILIKE ?) OR CAST(players.name AS TEXT) ILIKE ?)', '%a%', '%b%', '%c%']) }
|
||||
|
||||
# tests search with brackets in expression
|
||||
it { expect(described_class.new('name:a and (name:b or name:c)', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['(players.name ILIKE ? AND (players.name ILIKE ? OR players.name ILIKE ?))', '%a%', '%b%', '%c%']) }
|
||||
it { expect(described_class.new('name:a and (name:b or name:c)', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['(CAST(players.name AS TEXT) ILIKE ? AND (CAST(players.name AS TEXT) ILIKE ? OR CAST(players.name AS TEXT) ILIKE ?))', '%a%', '%b%', '%c%']) }
|
||||
|
||||
# tests search with brackets in expression and with NOT operator
|
||||
it { expect(described_class.new('name:a and not (name:b or name:c)', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['(players.name ILIKE ? AND NOT (players.name ILIKE ? OR players.name ILIKE ?))', '%a%', '%b%', '%c%']) }
|
||||
it { expect(described_class.new('name:a and not (name:b or name:c)', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['(CAST(players.name AS TEXT) ILIKE ? AND NOT (CAST(players.name AS TEXT) ILIKE ? OR CAST(players.name AS TEXT) ILIKE ?))', '%a%', '%b%', '%c%']) }
|
||||
|
||||
# tests search with special characters in search term
|
||||
it { expect(described_class.new('name:"%a_\"', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['players.name ILIKE ?', '%\%a\_\\%']) }
|
||||
it { expect(described_class.new('name:"%a_\"', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['CAST(players.name AS TEXT) ILIKE ?', '%\%a\_\\%']) }
|
||||
|
||||
# tests search with field mappings
|
||||
it { expect(described_class.new('tags:h1-r', [:'players.name', :'players.value', :'players.device_id'], :"players.device_id", { tags: "tags.value" }).where_clause).to eq(['tags.value ILIKE ?', '%h1-r%']) }
|
||||
it { expect(described_class.new('tags:h1-r', [:'players.name', :'players.value', :'players.device_id'], :"players.device_id", { tags: "tags.value" }).where_clause).to eq(['CAST(tags.value AS TEXT) ILIKE ?', '%h1-r%']) }
|
||||
|
||||
# tests search with field mappings when fields array has same mapping
|
||||
it { expect(described_class.new('tags:h1-r', [:'players.name', :'players.tags', :'players.device_id'], :"players.device_id", { tags: "tags.value" }).where_clause).to eq(["tags.value ILIKE ?", "%h1-r%"]) }
|
||||
it { expect(described_class.new('tags:h1-r', [:'players.name', :'players.tags', :'players.device_id'], :"players.device_id", { tags: "tags.value" }).where_clause).to eq(["CAST(tags.value AS TEXT) ILIKE ?", "%h1-r%"]) }
|
||||
|
||||
# tests complex query
|
||||
it { expect(described_class.new('(device_id:"with space" tags:mta no-quotes-id-123) or "id with quotes-5" and ( ("id with q 10" or "id with q 20") and ("id with Q 30" "id with Q 40") and not id-without-Q-50)', [:'players.name', :'players.value', :'players.device_id'], :"players.device_id", { tags: 'tags.name' }).where_clause).to eq(['((players.device_id ILIKE ? OR (tags.name ILIKE ? OR players.device_id ILIKE ?)) OR (players.device_id ILIKE ? AND (((players.device_id ILIKE ? OR players.device_id ILIKE ?) AND (players.device_id ILIKE ? OR players.device_id ILIKE ?)) AND NOT players.device_id ILIKE ?)))', '%with space%', '%mta%', '%no-quotes-id-123%', '%id with quotes-5%', '%id with q 10%', '%id with q 20%', '%id with Q 30%', '%id with Q 40%', '%id-without-Q-50%']) }
|
||||
it { expect(described_class.new('(device_id:"with space" tags:mta no-quotes-id-123) or "id with quotes-5" and ( ("id with q 10" or "id with q 20") and ("id with Q 30" "id with Q 40") and not id-without-Q-50)', [:'players.name', :'players.value', :'players.device_id'], :"players.device_id", { tags: 'tags.name' }).where_clause).to eq(['((CAST(players.device_id AS TEXT) ILIKE ? OR (CAST(tags.name AS TEXT) ILIKE ? OR CAST(players.device_id AS TEXT) ILIKE ?)) OR (CAST(players.device_id AS TEXT) ILIKE ? AND (((CAST(players.device_id AS TEXT) ILIKE ? OR CAST(players.device_id AS TEXT) ILIKE ?) AND (CAST(players.device_id AS TEXT) ILIKE ? OR CAST(players.device_id AS TEXT) ILIKE ?)) AND NOT CAST(players.device_id AS TEXT) ILIKE ?)))', '%with space%', '%mta%', '%no-quotes-id-123%', '%id with quotes-5%', '%id with q 10%', '%id with q 20%', '%id with Q 30%', '%id with Q 40%', '%id-without-Q-50%']) }
|
||||
|
||||
# tests query with multiple search terms with mixed and-or-not after dash and underscore
|
||||
it { expect(described_class.new('123-and-456 -or-2 -not_not_1', [:'players.title', :'players.tag', :'players.device_id'], :'players.device_id').where_clause).to eq(['(CAST(players.device_id AS TEXT) ILIKE ? OR (CAST(players.device_id AS TEXT) ILIKE ? OR CAST(players.device_id AS TEXT) ILIKE ?))', '%123-and-456%', '%-or-2%', '%-not\_not\_1%'])}
|
||||
|
||||
# tests query with multiple search terms with mixed and-or-not after dash and underscore
|
||||
it { expect(described_class.new('andrew or ornela', [:'players.title', :'players.tag', :'players.device_id'], :'players.device_id').where_clause).to eq(['(CAST(players.device_id AS TEXT) ILIKE ? OR CAST(players.device_id AS TEXT) ILIKE ?)', '%andrew%', '%ornela%'])}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe TextToTsquery do
|
||||
describe '.new' do
|
||||
# partial match
|
||||
it { expect(described_class.new('A').tsquery).to eq('A:*') }
|
||||
it { expect(described_class.new('A', wildcard: false).tsquery).to eq('A:') }
|
||||
it { expect(described_class.new(' A ').tsquery).to eq('A:*') }
|
||||
|
||||
# AND operations
|
||||
it { expect(described_class.new('A B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A B C').tsquery).to eq('A:*&B:*&C:*') }
|
||||
it { expect(described_class.new('A and B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A AND B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A & B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A && B').tsquery).to eq('A:*&B:*') }
|
||||
it { expect(described_class.new('A & B && C and D AND E F').tsquery).to eq('A:*&B:*&C:*&D:*&E:*&F:*') }
|
||||
|
||||
# OR operations
|
||||
it { expect(described_class.new('A or B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A or B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A OR B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A OR B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A | B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A | B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A || B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A || B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A or or B').tsquery).to eq('A:*|B:*') }
|
||||
it { expect(described_class.new('A or or B', wildcard: false).tsquery).to eq('A:|B:') }
|
||||
it { expect(described_class.new('A | B || C or D OR E').tsquery).to eq('A:*|B:*|C:*|D:*|E:*') }
|
||||
it { expect(described_class.new('A | B || C or D OR E', wildcard: false).tsquery).to eq('A:|B:|C:|D:|E:') }
|
||||
|
||||
# () Precedence
|
||||
it { expect(described_class.new('(A)').tsquery).to eq('(A:*)') }
|
||||
it { expect(described_class.new('(A)', wildcard: false).tsquery).to eq('(A:)') }
|
||||
it { expect(described_class.new('(A B)').tsquery).to eq('(A:*&B:*)') }
|
||||
it { expect(described_class.new('(A B)', wildcard: false).tsquery).to eq('(A:&B:)') }
|
||||
it { expect(described_class.new('A (B !C)').tsquery).to eq('A:*&(B:*&!C)') }
|
||||
it { expect(described_class.new('A (B !C)', wildcard: false).tsquery).to eq('A:&(B:&!C)') }
|
||||
it { expect(described_class.new('(A AND B) OR C').tsquery).to eq('(A:*&B:*)|C:*') }
|
||||
it { expect(described_class.new('(A AND B) OR C', wildcard: false).tsquery).to eq('(A:&B:)|C:') }
|
||||
it { expect(described_class.new('A AND (B OR C)').tsquery).to eq('A:*&(B:*|C:*)') }
|
||||
it { expect(described_class.new('A AND (B OR C)', wildcard: false).tsquery).to eq('A:&(B:|C:)') }
|
||||
it { expect(described_class.new('(A & B) || C').tsquery).to eq('(A:*&B:*)|C:*') }
|
||||
it { expect(described_class.new('(A & B) || C', wildcard: false).tsquery).to eq('(A:&B:)|C:') }
|
||||
it { expect(described_class.new('A && (B | C)').tsquery).to eq('A:*&(B:*|C:*)') }
|
||||
it { expect(described_class.new('A && (B | C)', wildcard: false).tsquery).to eq('A:&(B:|C:)') }
|
||||
it { expect(described_class.new('A && !D (B | C | !E)').tsquery).to eq('A:*&!D&(B:*|C:*|!E)') }
|
||||
it { expect(described_class.new('A && !D (B | C | !E)', wildcard: false).tsquery).to eq('A:&!D&(B:|C:|!E)') }
|
||||
|
||||
# Exact Matches
|
||||
it { expect(described_class.new('"A"').tsquery).to eq("'A'") }
|
||||
it { expect(described_class.new('"A B"').tsquery).to eq("'A B'") }
|
||||
it { expect(described_class.new('"A&B"').tsquery).to eq("'A&B'") }
|
||||
it { expect(described_class.new('"-A|B"').tsquery).to eq("'-A|B'") }
|
||||
it { expect(described_class.new('"A-B"').tsquery).to eq("'A-B'") }
|
||||
it { expect(described_class.new('"A" B').tsquery).to eq("'A'&B:*") }
|
||||
it { expect(described_class.new('"A" B', wildcard: false).tsquery).to eq("'A'&B:") }
|
||||
it { expect(described_class.new('"A B" C').tsquery).to eq("'A B'&C:*") }
|
||||
it { expect(described_class.new('"A B" C', wildcard: false).tsquery).to eq("'A B'&C:") }
|
||||
it { expect(described_class.new('("A B" or C) and D').tsquery).to eq("('A B'|C:*)&D:*") }
|
||||
it { expect(described_class.new('("A B" or C) and D', wildcard: false).tsquery).to eq("('A B'|C:)&D:") }
|
||||
|
||||
describe 'validations' do
|
||||
it { expect { described_class.new('(') }.to raise_error(ArgumentError, /parenthesis/) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.valid_search_parenthesis?' do
|
||||
it { expect(described_class.valid_search_parenthesis?('')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('()')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('()()')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('(()())')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('((())())')).to eq true }
|
||||
it { expect(described_class.valid_search_parenthesis?('(')).to eq false }
|
||||
it { expect(described_class.valid_search_parenthesis?(')(')).to eq false }
|
||||
it { expect(described_class.valid_search_parenthesis?('())')).to eq false }
|
||||
it { expect(described_class.valid_search_parenthesis?('((()())')).to eq false }
|
||||
end
|
||||
end
|
||||
@@ -42,11 +42,4 @@ ActiveRecord::Schema.define do
|
||||
t.string :name
|
||||
t.timestamps null: false
|
||||
end
|
||||
|
||||
create_table :players, force: true do |t|
|
||||
t.references :tag
|
||||
t.string :name
|
||||
t.string :value
|
||||
t.string :device_id
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
class VectorModel < ActiveRecord::Base
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache
|
||||
pg_search fields: %i[vector_models.id vector_models.name vector_models.value], cache: :search_cache
|
||||
end
|
||||
|
||||
class VectorModelWithMappings < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[vector_models.id vector_models.name vector_models.value], fields_mappings: {device_id: "vector_models.id"}, cache: :search_cache
|
||||
end
|
||||
|
||||
class VectorModelWithoutCallback < ActiveRecord::Base
|
||||
@@ -32,13 +38,7 @@ end
|
||||
class SimpleVectorModel < ActiveRecord::Base
|
||||
self.table_name = :vector_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[id name value], cache: :search_cache, language: :simple
|
||||
end
|
||||
|
||||
class SimplePlayerModel < ActiveRecord::Base
|
||||
self.table_name = :players
|
||||
include PgSearchable
|
||||
pg_search fields: %i[device_id name value tags], cache: :search_cache, language: :simple
|
||||
pg_search fields: %i[vector_models.id vector_models.name vector_models.value], cache: :search_cache, language: :simple
|
||||
end
|
||||
|
||||
class VectorWithoutWildcardModel < ActiveRecord::Base
|
||||
@@ -72,7 +72,7 @@ end
|
||||
class DynamicModelWithTagValues < ActiveRecord::Base
|
||||
self.table_name = :dynamic_models
|
||||
include PgSearchable
|
||||
pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value tags.value], joins: [:tags]
|
||||
pg_search fields: %i[dynamic_models.id dynamic_models.name dynamic_models.value], fields_mappings: {tag: 'tags.value'}, joins: [:tags]
|
||||
has_many :tags, as: :taggable
|
||||
end
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
module Version
|
||||
MAJOR = 1
|
||||
MINOR = 0
|
||||
PATCH = 24
|
||||
PATCH = 27
|
||||
|
||||
def self.to_s
|
||||
[MAJOR, MINOR, PATCH].compact.join('.')
|
||||
|
||||
Reference in New Issue
Block a user