Module: SqlChatbot::Grammar::Primitives
- Defined in:
- lib/sql_chatbot/grammar/primitives.rb
Constant Summary collapse
- PREFERRED_DISPLAY_FIELDS =
%w[id name title label email].freeze
Class Method Summary collapse
- .build(primitive:, entity:, field: nil, which: nil, n: nil, rank_field: nil, direction: nil, group_by: nil) ⇒ Object
- .pick_display_fields(entity) ⇒ Object
-
.q(name) ⇒ Object
Quote a single SQL identifier — wraps in double quotes and escapes embedded quotes.
-
.qc(table, col) ⇒ Object
Qualified column reference: “table”.“column”.
- .require_field!(entity, field, name) ⇒ Object
Class Method Details
.build(primitive:, entity:, field: nil, which: nil, n: nil, rank_field: nil, direction: nil, group_by: nil) ⇒ Object
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/sql_chatbot/grammar/primitives.rb', line 22 def self.build(primitive:, entity:, field: nil, which: nil, n: nil, rank_field: nil, direction: nil, group_by: nil) t = q(entity.table) case primitive when :COUNT "SELECT COUNT(*) FROM #{t}" when :LIST "SELECT #{pick_display_fields(entity).map { |c| q(c) }.join(", ")} FROM #{t}" when :SUM require_field!(entity, field, "SUM") "SELECT SUM(#{qc(entity.table, field)}) FROM #{t}" when :AVG require_field!(entity, field, "AVG") "SELECT ROUND(AVG(#{qc(entity.table, field)}), 2) FROM #{t}" when :MIN_MAX require_field!(entity, field, "MIN_MAX") raise "MIN_MAX requires which" unless %i[MIN MAX].include?(which) "SELECT #{which}(#{qc(entity.table, field)}) FROM #{t}" when :TOP_N rank = rank_field || entity.ranking_candidates.first raise "TOP_N requires rankField" unless rank limit = n || 10 dir = (direction || "desc").to_s.upcase # V1.3-V: NULLS LAST in both directions. PG default for DESC is # NULLS FIRST, which surfaced a NULL-rank row for 2BN's # "biggest review by rating" — answer LLM rendered "N/A". # NULL rank is never the answer the user wants. "SELECT #{pick_display_fields(entity).map { |c| q(c) }.join(", ")}, #{qc(entity.table, rank)} FROM #{t} ORDER BY #{qc(entity.table, rank)} #{dir} NULLS LAST LIMIT #{limit}" when :RANK raise "RANK requires rankField and groupBy" unless rank_field && group_by "SELECT #{t}.*, DENSE_RANK() OVER (PARTITION BY #{qc(entity.table, group_by)} ORDER BY #{qc(entity.table, rank_field)} DESC) AS rank FROM #{t}" else raise "unknown primitive #{primitive}" end end |
.pick_display_fields(entity) ⇒ Object
57 58 59 60 61 |
# File 'lib/sql_chatbot/grammar/primitives.rb', line 57 def self.pick_display_fields(entity) present = PREFERRED_DISPLAY_FIELDS.select { |p| entity.fields.key?(p) } return present.map { |p| entity.fields[p].column } if present.any? entity.fields.keys.first(4) end |
.q(name) ⇒ Object
Quote a single SQL identifier — wraps in double quotes and escapes embedded quotes. Prevents PG reserved-word collisions (“user”, “order” etc.) which silently resolve to functions and corrupt counts.
13 14 15 |
# File 'lib/sql_chatbot/grammar/primitives.rb', line 13 def self.q(name) %("#{name.to_s.gsub('"', '""')}") end |
.qc(table, col) ⇒ Object
Qualified column reference: “table”.“column”
18 19 20 |
# File 'lib/sql_chatbot/grammar/primitives.rb', line 18 def self.qc(table, col) "#{q(table)}.#{q(col)}" end |
.require_field!(entity, field, name) ⇒ Object
63 64 65 66 |
# File 'lib/sql_chatbot/grammar/primitives.rb', line 63 def self.require_field!(entity, field, name) raise "#{name} requires field" unless field raise "#{name} field '#{field}' not in entity" unless entity.fields.key?(field.to_s) end |