require_relative 'parser' class TextToSqlQuery def initialize(text, fields, default_fields, fields_mappings = {}, joins = []) @text = text.to_s.strip @fields = fields.map(&:to_sym) # Keep compatibility with previous version where default_field(s) was string/symbol @default_fields = if default_fields.is_a? Array default_fields.map(&:to_sym) else [default_fields.to_sym] end @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 @joins = joins end def where_clause @parser = Query.new @parsed_tree = @parser.parse(@text) generate_sql @parsed_tree end def join_clause return nil if @joins.empty? return *@joins 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 query_part = '(' params_part = [] @default_fields.each do |default_field| query_part += "CAST(#{default_field.to_s} AS TEXT) ILIKE ? OR " params_part << "%#{escaped_node_value}%" end query_part = query_part[0...-4] + ')' [query_part, *params_part] 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.shift not_params = not_array ["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 ["STRING_AGG(CAST(#{mapping.to_s} AS TEXT), '') 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.shift first_operand_params = first_operand second_operand_expression = second_operand.shift second_operand_params = second_operand ["(#{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