2020-02-05 13:40:37 +01:00
require_relative 'parser'
class TextToSqlQuery
2020-04-20 19:33:29 +02:00
def initialize ( text , fields , default_fields , fields_mappings = { } , joins = [ ] , model = nil )
2020-02-05 13:40:37 +01:00
@text = text . to_s . strip
@fields = fields . map ( & :to_sym )
2020-04-16 14:59:51 +02:00
# 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
2020-02-05 13:40:37 +01:00
@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
2020-04-20 19:33:29 +02:00
@joins = joins
@model = model
2020-02-05 13:40:37 +01:00
end
2020-02-07 11:40:03 +01:00
def where_clause
2020-02-05 13:40:37 +01:00
@parser = Query . new
@parsed_tree = @parser . parse ( @text )
2020-02-07 11:40:03 +01:00
generate_sql @parsed_tree
2020-02-05 13:40:37 +01:00
end
2020-04-20 19:33:29 +02:00
def join_clause
return if @joins . empty?
table_column_mappings
model_association_mappings
join_clause_part = ''
@joins . each do | join |
join_sql_part = generate_join_sql_part_for join
join_clause_part += join_sql_part
end
join_clause_part
end
2020-02-05 13:40:37 +01:00
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
2020-04-16 14:59:51 +02:00
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 ]
2020-02-05 13:40:37 +01:00
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
2020-02-07 16:09:57 +01:00
not_params = not_array
2020-02-05 13:40:37 +01:00
[ " 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
2020-02-10 16:42:59 +01:00
[ " CAST( #{ mapping . to_s } AS TEXT) ILIKE ? " , " % #{ escaped_node_value } % " ]
2020-02-05 13:40:37 +01:00
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
2020-04-20 19:33:29 +02:00
def table_column_mappings
@table_column_mappings = { }
@fields_mappings . each_value do | table_with_column |
split_names = table_with_column . to_s . split '.'
table_name = split_names . first
column_name = split_names . second
@table_column_mappings [ table_name ] = [ ] if @table_column_mappings [ table_name ] . nil?
@table_column_mappings [ table_name ] << column_name
end
@table_column_mappings
end
def model_association_mappings
@model_associations = { }
@model . reflect_on_all_associations . each do | association |
name = association . name
@model_associations [ name ] = {
option_as : association . options [ :as ] || name ,
type : association . type
}
end
@model_associations
end
def generate_join_sql_part_for ( join )
association_data = @model_associations [ join ]
join_table_name = join . to_s
raise " Join table #{ join_table_name } has no association data " if association_data . nil?
select_sql_part = ''
columns_for_table = @table_column_mappings [ join_table_name ] || [ ]
# TODO: Can be optimized - do not include columns that are not referenced in user query
columns_for_table . each do | column_name |
select_sql_part += " string_agg( #{ column_name } , '') AS #{ column_name } , "
end
option_as = association_data [ :option_as ]
type = association_data [ :type ]
model_name = @model . to_s
table_name = @model . table_name
" LEFT JOIN (SELECT #{ option_as } _id, #{ select_sql_part } #{ type } FROM #{ join_table_name } GROUP BY #{ option_as } _id, #{ type } ) #{ join_table_name } on #{ join_table_name } . #{ option_as } _id = #{ table_name } .id AND #{ join_table_name } . #{ type } = ' #{ model_name } ' "
end
2020-02-05 13:40:37 +01:00
end