Module: SqlChatbot::Services::SqlExecutor
- Defined in:
- lib/sql_chatbot/services/sql_executor.rb
Constant Summary collapse
- KEYWORD_BLOCKLIST =
%w[INSERT UPDATE DELETE DROP ALTER CREATE GRANT TRUNCATE EXECUTE REVOKE COPY INTO].freeze
- FUNCTION_BLOCKLIST =
%w[pg_read_file pg_read_binary_file dblink pg_terminate_backend lo_import lo_export pg_sleep set_config current_setting].freeze
- CATALOG_BLOCKLIST =
%w[pg_shadow pg_roles pg_authid pg_user information_schema].freeze
- AGGREGATE_PATTERN =
/\b(COUNT|SUM|AVG|MIN|MAX)\s*\(/i
Class Method Summary collapse
Class Method Details
.execute_sql(sql) ⇒ Object
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/sql_chatbot/services/sql_executor.rb', line 59 def self.execute_sql(sql) connection = ActiveRecord::Base.connection connection.execute("SET statement_timeout = '10s'") connection.execute("BEGIN") connection.execute("SET TRANSACTION READ ONLY") result = connection.execute(sql) rows = result.to_a columns = rows.first&.keys || [] connection.execute("COMMIT") { columns: columns, rows: rows, row_count: rows.length } rescue => e connection.execute("ROLLBACK") rescue nil raise e ensure # Reset statement_timeout on shared AR connection connection.execute("SET statement_timeout = '0'") rescue nil end |
.validate_sql(sql) ⇒ Object
9 10 11 12 13 14 15 16 17 18 19 20 21 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 56 57 |
# File 'lib/sql_chatbot/services/sql_executor.rb', line 9 def self.validate_sql(sql) trimmed = sql.gsub(/^\s+/, "") trimmed = trimmed.gsub(/--[^\n]*/, "").gsub(/\/\*[\s\S]*?\*\//, "") trimmed = trimmed.strip # Fix missing space after SELECT trimmed = trimmed.sub(/^SELECT(?=[A-Z])/i, "SELECT ") # Single statement check parts = trimmed.split(";").select { |p| p.strip.length > 0 } if parts.length > 1 return { valid: false, reason: "Only a single statement is allowed" } end working_sql = trimmed.sub(/;\s*$/, "").strip # Must start with SELECT unless working_sql.match?(/^SELECT\b/i) return { valid: false, reason: "Only SELECT queries are allowed" } end # Keyword blocklist KEYWORD_BLOCKLIST.each do |keyword| if working_sql.match?(/\b#{keyword}\b/i) return { valid: false, reason: "Blocked keyword: #{keyword}" } end end # Function blocklist FUNCTION_BLOCKLIST.each do |fn| if working_sql.match?(/\b#{fn}\b/i) return { valid: false, reason: "Blocked function: #{fn}" } end end # Catalog blocklist CATALOG_BLOCKLIST.each do |catalog| if working_sql.match?(/\b#{catalog}\b/i) return { valid: false, reason: "Blocked system catalog: #{catalog}" } end end # Auto-add LIMIT has_limit = working_sql.match?(/\bLIMIT\b/i) is_aggregate = AGGREGATE_PATTERN.match?(working_sql) working_sql += " LIMIT 500" if !has_limit && !is_aggregate { valid: true, sql: working_sql } end |