diff --git a/Gemfile b/Gemfile index 54ad0fd..2283ebe 100644 --- a/Gemfile +++ b/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" - gem "racc" + gem "rexical", '~> 1.0', require: false + gem "racc", '~> 1.4', require: false end diff --git a/Gemfile.lock b/Gemfile.lock index ca4785b..d3ec410 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -134,9 +134,9 @@ DEPENDENCIES juwelier (~> 2.1) pg (~> 0.15) pry (~> 0.12) - racc + racc (~> 1.4) rdoc (~> 3.12) - rexical + rexical (~> 1.0) rspec (~> 3.8) rubocop (~> 0.54) rubocop-rspec (~> 1.23) diff --git a/lib/grammar.y b/lib/grammar.y index 5484994..2eed955 100644 --- a/lib/grammar.y +++ b/lib/grammar.y @@ -14,7 +14,9 @@ class Query | TERM_WITHOUT_QUOTES COLON TERM_WITH_QUOTES { result = {val[0] => val[2]} } | expression OPERATOR_OR expression { result = {:OPERATOR_OR => [val[0], val[2]]} } | expression OPERATOR_AND expression { result = {:OPERATOR_AND => [val[0], val[2]]} } + | OPERATOR_NOT expression { result = {:OPERATOR_NOT => val[1]} } | L_BRACKET expression R_BRACKET { result = val[1] } + | expression expression { result = {:OPERATOR_OR => [val[0], val[1]]} } end ---- header diff --git a/lib/parser.rb b/lib/parser.rb index 58fc3c7..165b293 100644 --- a/lib/parser.rb +++ b/lib/parser.rb @@ -10,7 +10,7 @@ require 'racc/parser.rb' class Query < Racc::Parser -module_eval(<<'...end grammar.y/module_eval...', 'grammar.y', 24) +module_eval(<<'...end grammar.y/module_eval...', 'grammar.y', 26) def parse(input) scan_str(input) end @@ -18,34 +18,44 @@ module_eval(<<'...end grammar.y/module_eval...', 'grammar.y', 24) ##### State transition tables begin ### racc_action_table = [ - 8, 7, 3, 4, 11, 5, 16, 3, 4, 9, - 5, 3, 4, 6, 5, 3, 4, 8, 5, 8, - 7, 14, 15 ] + 5, 10, 9, 3, 4, 5, 6, 19, 3, 4, + 7, 6, 5, 10, 9, 3, 4, 5, 6, 11, + 3, 4, 5, 6, 14, 3, 4, nil, 6, 5, + 10, 9, 3, 4, 5, 6, nil, 3, 4, 5, + 6, nil, 3, 4, nil, 6, 5, 10, nil, 3, + 4, 5, 6, nil, 3, 4, nil, 6, 3, 4, + nil, 6, 17, 18 ] racc_action_check = [ - 10, 10, 8, 8, 6, 8, 10, 0, 0, 3, - 0, 5, 5, 1, 5, 7, 7, 12, 7, 2, - 2, 9, 9 ] + 13, 13, 13, 13, 13, 0, 13, 13, 0, 0, + 1, 0, 2, 2, 2, 2, 2, 5, 2, 3, + 5, 5, 6, 5, 7, 6, 6, nil, 6, 8, + 8, 8, 8, 8, 9, 8, nil, 9, 9, 10, + 9, nil, 10, 10, nil, 10, 15, 15, nil, 15, + 15, 16, 15, nil, 16, 16, nil, 16, 12, 12, + nil, 12, 11, 11 ] racc_action_pointer = [ - 2, 13, 16, 2, nil, 6, 4, 10, -3, 16, - -3, nil, 14, nil, nil, nil, nil ] + 3, 10, 10, 12, nil, 15, 20, 24, 27, 32, + 37, 57, 53, -2, nil, 44, 49, nil, nil, nil ] racc_action_default = [ - -2, -10, -1, -3, -4, -10, -10, -10, -10, -10, - -10, 17, -7, -8, -5, -6, -9 ] + -2, -12, -1, -3, -4, -12, -12, -12, -11, -12, + -12, -12, -9, -12, 20, -7, -8, -5, -6, -10 ] racc_goto_table = [ - 2, 1, nil, nil, nil, 10, nil, 12, 13 ] + 2, 1, nil, nil, nil, 12, 13, nil, nil, 15, + 16 ] racc_goto_check = [ - 2, 1, nil, nil, nil, 2, nil, 2, 2 ] + 2, 1, nil, nil, nil, 2, 2, nil, nil, 2, + 2 ] racc_goto_pointer = [ nil, 1, 0 ] racc_goto_default = [ - nil, nil, nil ] + nil, nil, 8 ] racc_reduce_table = [ 0, 0, :racc_error, @@ -57,11 +67,13 @@ racc_reduce_table = [ 3, 12, :_reduce_6, 3, 12, :_reduce_7, 3, 12, :_reduce_8, - 3, 12, :_reduce_9 ] + 2, 12, :_reduce_9, + 3, 12, :_reduce_10, + 2, 12, :_reduce_11 ] -racc_reduce_n = 10 +racc_reduce_n = 12 -racc_shift_n = 17 +racc_shift_n = 20 racc_token_table = { false => 0, @@ -169,11 +181,25 @@ module_eval(<<'.,.,', 'grammar.y', 15) module_eval(<<'.,.,', 'grammar.y', 16) def _reduce_9(val, _values, result) + result = {:OPERATOR_NOT => val[1]} + result + end +.,., + +module_eval(<<'.,.,', 'grammar.y', 17) + def _reduce_10(val, _values, result) result = val[1] result end .,., +module_eval(<<'.,.,', 'grammar.y', 18) + def _reduce_11(val, _values, result) + result = {:OPERATOR_OR => [val[0], val[1]]} + result + end +.,., + def _reduce_none(val, _values, result) val[0] end diff --git a/lib/text_to_sql_query.rb b/lib/text_to_sql_query.rb index c7cc6f8..d473f48 100644 --- a/lib/text_to_sql_query.rb +++ b/lib/text_to_sql_query.rb @@ -42,6 +42,7 @@ class TextToSqlQuery end not_expression = not_array.shift + not_params = not_array ["NOT #{not_expression}"] + not_params diff --git a/pkg/pg_searchable_regex-1.0.21.gem b/pkg/pg_searchable_regex-1.0.21.gem deleted file mode 100644 index 1d3941a..0000000 Binary files a/pkg/pg_searchable_regex-1.0.21.gem and /dev/null differ diff --git a/spec/lib/query_lexer_spec.rb b/spec/lib/query_lexer_spec.rb index b97a65a..f531899 100644 --- a/spec/lib/query_lexer_spec.rb +++ b/spec/lib/query_lexer_spec.rb @@ -1,4 +1,4 @@ -require './lexer' +require_relative '../../lib/lexer' class QueryLexerTester describe 'Testing the Lexer' do diff --git a/spec/lib/query_parser_spec.rb b/spec/lib/query_parser_spec.rb index f26c4a3..16439fc 100644 --- a/spec/lib/query_parser_spec.rb +++ b/spec/lib/query_parser_spec.rb @@ -1,4 +1,4 @@ -require './parser' +require_relative '../../lib/parser' class QueryParserTester describe 'Testing the Parser' do diff --git a/spec/lib/text_to_sql_query_spec.rb b/spec/lib/text_to_sql_query_spec.rb index fd76b8f..c8f39c0 100644 --- a/spec/lib/text_to_sql_query_spec.rb +++ b/spec/lib/text_to_sql_query_spec.rb @@ -1,61 +1,61 @@ # frozen_string_literal: true +require_relative '../../lib/text_to_sql_query' -describe TextToRegexQuery do - include_examples 'pg_search', SimpleVectorModel +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(SimpleVectorModel)).to eq(['players.name ILIKE ?', '%some-default-value%']) } + it { expect(described_class.new('some-default-value', [:"players.name"], :"players.name").where_clause).to eq(['players.name ILIKE ?', '%some-default-value%']) } # tests simple search term with column name and without quotes - it { expect(described_class.new('title:ab', [:"players.title"], :"players.title").where_clause(SimpleVectorModel)).to eq(['players.title ILIKE ?', '%ab%']) } + it { expect(described_class.new('name:ab', [:"players.name"], :"players.name").where_clause).to eq(['players.name 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(SimpleVectorModel)).to raise_error(RuntimeError, "Unknown field 'unknown'") } + 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(SimpleVectorModel)).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(["players.device_id ILIKE ?", "%ab%"]) } # tests simple search term with column name and with quotes - it { expect(described_class.new('tag:"ab"', [:"players.name", :"players.tag"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(["players.tag ILIKE ?", "%ab%"]) } + it { expect(described_class.new('tags:"ab"', [:"players.name", :"players.tags"], :"players.device_id").where_clause).to eq(["players.tags ILIKE ?", "%ab%"]) } # tests search without operators - it { expect(described_class.new('123 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(["(players.device_id ILIKE ? OR players.device_id ILIKE ?)", "%123%", "%456%"]) } + 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%"]) } # tests search with OR operator - it { expect(described_class.new('123 or 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(["(players.device_id ILIKE ? OR players.device_id ILIKE ?)", "%123%", "%456%"]) } + 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%"]) } # tests search with AND operator - it { expect(described_class.new('123 and 456', [:"players.name", :"players.device_id"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(["(players.device_id ILIKE ? AND players.device_id ILIKE ?)", "%123%", "%456%"]) } + 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%"]) } # 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(SimpleVectorModel)).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 players.device_id ILIKE ?", "%23%"]) } # tests search with NOT operator on non-default column - it { expect(described_class.new('not tag:23', [:"players.name", :"players.tag"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(["NOT players.tag ILIKE ?", "%23%"]) } + it { expect(described_class.new('not value:23', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(["NOT players.value ILIKE ?", "%23%"]) } # tests search with mixed logical operators - it { expect(described_class.new('title:ab and not tag:hf-1', [:"players.title", :"players.tag"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(['(players.title ILIKE ? AND NOT players.tag ILIKE ?)', '%ab%', '%hf-1%']) } + 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%']) } # tests search with mixed logical operators without NOT' - it { expect(described_class.new('title:a and title:b or title:c', [:"players.title", :"players.tag"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(['((players.title ILIKE ? AND players.title ILIKE ?) OR players.title ILIKE ?)', '%a%', '%b%', '%c%']) } + 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%']) } # tests search with brackets in expression - it { expect(described_class.new('title:a and (title:b or title:c)', [:"players.title", :"players.tag"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(['(players.title ILIKE ? AND (players.title ILIKE ? OR players.title ILIKE ?))', '%a%', '%b%', '%c%']) } + 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%']) } # tests search with brackets in expression and with NOT operator - it { expect(described_class.new('title:a and not (title:b or title:c)', [:"players.title", :"players.tag"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(['(players.title ILIKE ? AND NOT (players.title ILIKE ? OR players.title ILIKE ?))', '%a%', '%b%', '%c%']) } + 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%']) } # tests search with special characters in search term - it { expect(described_class.new('title:"%a_\"', [:"players.title", :"players.tag"], :"players.device_id").where_clause(SimpleVectorModel)).to eq(['players.title ILIKE ?', '%\%a\_\\%']) } + it { expect(described_class.new('name:"%a_\"', [:"players.name", :"players.value"], :"players.device_id").where_clause).to eq(['players.name ILIKE ?', '%\%a\_\\%']) } # tests search with field mappings - it { expect(described_class.new('tags:h1-r', [:'players.title', :'players.name', :'players.device_id'], :"players.device_id", { tags: "tags.name" }).where_clause(SimpleVectorModel)).to eq(['tags.name ILIKE ?', '%h1-r%']) } + 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%']) } # tests search with field mappings when fields array has same mapping - it { expect(described_class.new('tags:hs1-r', [:'players.title', :'players.tags', :'players.device_id'], :"players.device_id", { tags: "tags.name" }).where_clause(SimpleVectorModel)).to eq(["players.tag ILIKE ?", "%ab%"]) } + 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%"]) } # 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.title', :'players.name', :'players.device_id'], :"players.device_id", { tags: 'tags.name' }).where_clause(SimpleVectorModel)).to eq(['((players.device_id ILIKE ? OR (tags.name ILIKE ? OR players.device_id ILIKE ?)) OR (players.device_id ILIKE ? AND (((players.device_id ILIKE ? OR players.device_id ILIKE ?) AND (players.device_id ILIKE ? OR players.device_id ILIKE ?)) AND NOT players.device_id ILIKE ?)))', '%with space%', '%mta%', '%no-quotes-id-123%', '%id with quotes-5%', '%id with q 10%', '%id with q 20%', '%id with Q 30%', '%id with Q 40%', '%id-without-Q-50%']) } + 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%']) } end end diff --git a/spec/schema.rb b/spec/schema.rb index 0ddb2b9..7cb1a78 100644 --- a/spec/schema.rb +++ b/spec/schema.rb @@ -42,4 +42,11 @@ 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 diff --git a/spec/support/models.rb b/spec/support/models.rb index e8589d8..f18beb4 100755 --- a/spec/support/models.rb +++ b/spec/support/models.rb @@ -35,6 +35,12 @@ class SimpleVectorModel < ActiveRecord::Base 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 +end + class VectorWithoutWildcardModel < ActiveRecord::Base self.table_name = :vector_models include PgSearchable diff --git a/version.rb b/version.rb index fc5959a..f0966b2 100644 --- a/version.rb +++ b/version.rb @@ -3,7 +3,7 @@ module Version MAJOR = 1 MINOR = 0 - PATCH = 21 + PATCH = 24 def self.to_s [MAJOR, MINOR, PATCH].compact.join('.')