Compare commits
1 Commits
master
...
generate-q
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0eb85eaec2 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.idea
|
||||
@@ -3,7 +3,7 @@
|
||||
require 'active_support'
|
||||
require 'squeel'
|
||||
require_relative './text_to_tsquery'
|
||||
require_relative './text_to_regex_query'
|
||||
require_relative './text_to_sql_query'
|
||||
|
||||
module PgSearchable
|
||||
extend ActiveSupport::Concern
|
||||
@@ -48,7 +48,7 @@ module PgSearchable
|
||||
|
||||
def ts_search(value)
|
||||
return if @ts_search_fields.blank? || value.blank?
|
||||
TextToRegexQuery.new(value, @ts_search_fields, @default_field, @ts_search_fields_mappings).where_clause( includes(@ts_joins).references(:all))
|
||||
TextToSqlQuery.new(value, @ts_search_fields, @default_field, @ts_search_fields_mappings).where_clause( includes(@ts_joins).references(:all))
|
||||
end
|
||||
|
||||
def should_update_cache_field?
|
||||
|
||||
93
lib/text_to_sql_query.rb
Normal file
93
lib/text_to_sql_query.rb
Normal file
@@ -0,0 +1,93 @@
|
||||
require './parser'
|
||||
|
||||
class TextToSqlQuery
|
||||
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)
|
||||
fields_mappings.each do |field, value|
|
||||
@fields_mappings[field] = value if @fields_mappings[field]
|
||||
end
|
||||
end
|
||||
|
||||
def where_clause
|
||||
@parser = Query.new
|
||||
@parsed_tree = @parser.parse(@text)
|
||||
generate_sql @parsed_tree
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_sql(tree)
|
||||
first_key = tree.keys.first
|
||||
node_value = tree[first_key]
|
||||
case first_key
|
||||
when :DEFAULT_COLUMN
|
||||
escaped_node_value = handle_special_chars node_value
|
||||
["#{@default_field.to_s} ILIKE ?", "%#{escaped_node_value}%"]
|
||||
when :OPERATOR_OR
|
||||
generate_expression_for_logical_operator(:OR, node_value)
|
||||
when :OPERATOR_AND
|
||||
generate_expression_for_logical_operator(:AND, node_value)
|
||||
when :OPERATOR_NOT
|
||||
not_array = generate_sql node_value
|
||||
|
||||
if not_array.length < 2
|
||||
raise "There should be more than 1 element for expression following NOT operator"
|
||||
end
|
||||
|
||||
not_expression = not_array.first
|
||||
not_params = not_array[1..]
|
||||
|
||||
["NOT #{not_expression}"] + not_params
|
||||
|
||||
else
|
||||
# key is column name
|
||||
escaped_node_value = handle_special_chars node_value
|
||||
mapping = @fields_mappings[first_key.to_sym]
|
||||
if mapping.nil?
|
||||
raise "Unknown field '#{first_key.to_s}'"
|
||||
else
|
||||
["#{mapping.to_s} ILIKE ?", "%#{escaped_node_value}%"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def generate_expression_for_logical_operator(operator, operator_array)
|
||||
if operator_array.length != 2
|
||||
raise "There should be two array elements for #{operator.to_s} operator"
|
||||
end
|
||||
|
||||
first_operand = generate_sql operator_array.first
|
||||
second_operand = generate_sql operator_array.last
|
||||
|
||||
if first_operand.length < 2
|
||||
raise 'There should be more than 1 element in first operand array'
|
||||
end
|
||||
|
||||
if second_operand.length < 2
|
||||
raise 'There should be more than 1 element in second operand array'
|
||||
end
|
||||
|
||||
first_operand_expression = first_operand.first
|
||||
first_operand_params = first_operand[1..]
|
||||
|
||||
second_operand_expression = second_operand.first
|
||||
second_operand_params = second_operand[1..]
|
||||
|
||||
["(#{first_operand_expression} #{operator.to_s} #{second_operand_expression})"] + first_operand_params + second_operand_params
|
||||
end
|
||||
|
||||
def handle_special_chars(text)
|
||||
result = text.gsub(/\"/, '')
|
||||
result.gsub!(/\_/, '\_')
|
||||
result.tr!('\\', '\\')
|
||||
result.gsub!(/%/, '\%')
|
||||
result
|
||||
end
|
||||
end
|
||||
61
spec/lib/text_to_sql_query_spec.rb
Normal file
61
spec/lib/text_to_sql_query_spec.rb
Normal file
@@ -0,0 +1,61 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
describe TextToRegexQuery do
|
||||
include_examples 'pg_search', SimpleVectorModel
|
||||
describe '.new' do
|
||||
# tests simple search term without column name and without quotes
|
||||
it { expect(described_class.new('some-default-value', [:"players.name"], :"players.name").where_clause(SimpleVectorModel)).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%']) }
|
||||
|
||||
# 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'") }
|
||||
|
||||
# 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"]) }
|
||||
|
||||
# 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%"]) }
|
||||
|
||||
# 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%"]) }
|
||||
|
||||
# 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%"]) }
|
||||
|
||||
# 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%"]) }
|
||||
|
||||
# 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%"]) }
|
||||
|
||||
# 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%"]) }
|
||||
|
||||
# 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%']) }
|
||||
|
||||
# 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%']) }
|
||||
|
||||
# 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%']) }
|
||||
|
||||
# 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%']) }
|
||||
|
||||
# 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\_\\%']) }
|
||||
|
||||
# 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%']) }
|
||||
|
||||
# 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%"]) }
|
||||
|
||||
# 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%']) }
|
||||
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user