New fix #1

Merged
bilal.catic merged 16 commits from new-fix into master 2020-04-16 10:05:32 +02:00
19 changed files with 3556 additions and 638 deletions

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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(/"([^"]*)"/))

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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