Module: SqlChatbot::Grammar::Modifiers
- Defined in:
- lib/sql_chatbot/grammar/modifiers.rb
Constant Summary collapse
- WINDOWS =
{ "today" => "DATE_TRUNC('day', NOW())", "yesterday" => "DATE_TRUNC('day', NOW() - INTERVAL '1 day')", "last_7_days" => "NOW() - INTERVAL '7 days'", "last_30_days" => "NOW() - INTERVAL '30 days'", "this_week" => "DATE_TRUNC('week', NOW())", "this_month" => "DATE_TRUNC('month', NOW())", "this_year" => "DATE_TRUNC('year', NOW())", }.freeze
- OPS =
{ "eq" => "=", "neq" => "!=", "lt" => "<", "lte" => "<=", "gt" => ">", "gte" => ">=", }.freeze
Class Method Summary collapse
- .append_clause(sql, clause) ⇒ Object
- .apply(sql, modifier, entity) ⇒ Object
- .apply_group_by(sql, modifier, entity) ⇒ Object
- .apply_having(sql, modifier, entity) ⇒ Object
- .apply_join(sql, modifier, entity) ⇒ Object
- .apply_limit(sql, modifier) ⇒ Object
- .apply_order_by(sql, modifier, entity) ⇒ Object
- .apply_time(sql, modifier, entity) ⇒ Object
- .apply_where(sql, modifier, entity) ⇒ Object
- .q(name) ⇒ Object
- .qc(table, col) ⇒ Object
Class Method Details
.append_clause(sql, clause) ⇒ Object
52 53 54 55 56 57 58 |
# File 'lib/sql_chatbot/grammar/modifiers.rb', line 52 def self.append_clause(sql, clause) if /\bWHERE\b/i.match?(sql) "#{sql} AND #{clause}" else "#{sql} WHERE #{clause}" end end |
.apply(sql, modifier, entity) ⇒ Object
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/sql_chatbot/grammar/modifiers.rb', line 36 def self.apply(sql, modifier, entity) kind = modifier[:kind].to_s case kind when "where" then apply_where(sql, modifier, entity) when "time" then apply_time(sql, modifier, entity) when "join" then apply_join(sql, modifier, entity) when "group_by" then apply_group_by(sql, modifier, entity) when "having" then apply_having(sql, modifier, entity) when "order_by" then apply_order_by(sql, modifier, entity) when "limit" then apply_limit(sql, modifier) when "distinct" then sql.sub(/^SELECT /, "SELECT DISTINCT ") else raise "unknown modifier kind #{kind}" end end |
.apply_group_by(sql, modifier, entity) ⇒ Object
114 115 116 117 118 |
# File 'lib/sql_chatbot/grammar/modifiers.rb', line 114 def self.apply_group_by(sql, modifier, entity) field_name = modifier[:field].to_s raise "group_by field '#{field_name}' not on entity #{entity.name}" unless entity.fields[field_name] "#{sql} GROUP BY #{qc(entity.table, field_name)}" end |
.apply_having(sql, modifier, entity) ⇒ Object
120 121 122 123 124 |
# File 'lib/sql_chatbot/grammar/modifiers.rb', line 120 def self.apply_having(sql, modifier, entity) raise "HAVING requires GROUP BY" unless /GROUP BY/i.match?(sql) op = OPS[modifier[:op].to_s] || "=" "#{sql} HAVING #{modifier[:field]} #{op} #{modifier[:value]}" end |
.apply_join(sql, modifier, entity) ⇒ Object
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/sql_chatbot/grammar/modifiers.rb', line 92 def self.apply_join(sql, modifier, entity) assoc_name = modifier[:association].to_s assoc = entity.associations[assoc_name] raise "association '#{assoc_name}' not on entity #{entity.name}" unless assoc join_clause = assoc.join_clause # Re-emit the join clause with quoted identifiers. # joinClause format: "src_table.src_col = tgt_table.tgt_col" lhs, rhs = join_clause.split("=").map(&:strip) lt, lc = lhs.split(".") rt, rc = rhs.split(".") target_table = rt quoted_clause = "#{qc(lt, lc)} = #{qc(rt, rc)}" join_sql = " JOIN #{q(target_table)} ON #{quoted_clause}" if /\bWHERE\b/i.match?(sql) sql.sub(/\bWHERE\b/i) { "#{join_sql} WHERE " } else "#{sql}#{join_sql}" end end |
.apply_limit(sql, modifier) ⇒ Object
135 136 137 138 139 140 141 142 |
# File 'lib/sql_chatbot/grammar/modifiers.rb', line 135 def self.apply_limit(sql, modifier) limit_val = modifier[:value] if /LIMIT \d+/i.match?(sql) sql.sub(/LIMIT \d+/i, "LIMIT #{limit_val}") else "#{sql} LIMIT #{limit_val}" end end |
.apply_order_by(sql, modifier, entity) ⇒ Object
126 127 128 129 130 131 132 133 |
# File 'lib/sql_chatbot/grammar/modifiers.rb', line 126 def self.apply_order_by(sql, modifier, entity) field_name = modifier[:field].to_s raise "order_by field '#{field_name}' not on entity #{entity.name}" unless entity.fields[field_name] # The intent extractor LLM emits direction as either `direction` or # `op` (it's prompted with the where-modifier shape and reuses `op`). direction = (modifier[:direction] || modifier["direction"] || modifier[:op] || modifier["op"] || "desc").to_s.upcase "#{sql} ORDER BY #{qc(entity.table, field_name)} #{direction}" end |
.apply_time(sql, modifier, entity) ⇒ Object
85 86 87 88 89 90 |
# File 'lib/sql_chatbot/grammar/modifiers.rb', line 85 def self.apply_time(sql, modifier, entity) window_key = modifier[:window].to_s expr = WINDOWS[window_key] raise "unknown time window #{window_key}" unless expr append_clause(sql, "#{qc(entity.table, modifier[:field])} >= #{expr}") end |
.apply_where(sql, modifier, entity) ⇒ Object
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/sql_chatbot/grammar/modifiers.rb', line 60 def self.apply_where(sql, modifier, entity) field_name = modifier[:field].to_s field = entity.fields[field_name] raise "field '#{field_name}' not on entity #{entity.name}" unless field value = modifier[:value] ftype = field.type.to_s if ftype == "enum" enum_values = field.enum_values || {} str_value = value.to_s unless enum_values.key?(str_value) || enum_values.key?(str_value.to_sym) raise "enum value '#{value}' not in registry for #{entity.name}.#{field_name}" end value = enum_values[str_value] || enum_values[str_value.to_sym] elsif (ftype == "int" || ftype == "decimal") && value.is_a?(String) && value !~ /\A-?\d+(\.\d+)?\z/ raise "type mismatch: #{ftype} column #{entity.name}.#{field_name} cannot equal string '#{value}'" elsif ftype == "bool" && value.is_a?(String) && value !~ /\A(true|false|t|f|0|1)\z/i raise "type mismatch: bool column #{entity.name}.#{field_name} cannot equal string '#{value}'" end op = OPS[modifier[:op].to_s] || "=" formatted = value.is_a?(String) ? "'#{value.gsub("'", "''")}'" : value append_clause(sql, "#{qc(entity.table, field_name)} #{op} #{formatted}") end |
.q(name) ⇒ Object
9 10 11 |
# File 'lib/sql_chatbot/grammar/modifiers.rb', line 9 def self.q(name) Primitives.q(name) end |
.qc(table, col) ⇒ Object
13 14 15 |
# File 'lib/sql_chatbot/grammar/modifiers.rb', line 13 def self.qc(table, col) Primitives.qc(table, col) end |