Class: Glancer::Workflow::Executor

Inherits:
Object
  • Object
show all
Defined in:
lib/glancer/workflow/executor.rb

Class Method Summary collapse

Class Method Details

.apply_statement_timeout(connection) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/glancer/workflow/executor.rb', line 57

def self.apply_statement_timeout(connection)
  timeout_ms = Glancer.configuration.statement_timeout.to_i * 1000
  adapter = Glancer.configuration.resolved_adapter.to_s

  case adapter
  when "postgres", "postgresql"
    connection.execute("SET statement_timeout = #{timeout_ms}")
  when "mysql", "mysql2"
    connection.execute("SET max_execution_time = #{timeout_ms}")
  end
rescue StandardError => e
  Glancer::Utils::Logger.warn("Workflow::Executor", "Could not set statement timeout: #{e.message}")
end

.execute(sql, original_question: nil, attempt: 1, message_id: nil) ⇒ Object



6
7
8
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
# File 'lib/glancer/workflow/executor.rb', line 6

def self.execute(sql, original_question: nil, attempt: 1, message_id: nil)
  # Security check: Ensure only read queries are executed (SELECT or CTEs starting with WITH)
  unless sql.strip.match?(/\A\s*(select|with)\b/i)
    Glancer::Utils::Logger.error("Workflow::Executor", "Blocked attempt to run non-SELECT SQL.")
    raise Glancer::Error, "Only SELECT queries are allowed for execution."
  end

  Glancer::Utils::Logger.info("Workflow::Executor", "Executing SQL (Attempt ##{attempt})...")

  run_id = SecureRandom.uuid
  # Appending a comment for easier database auditing
  sql_with_comment = "#{sql.strip} /*glancer,run_id:#{run_id}*/"

  begin
    result = nil
    Glancer::Utils::Transaction.make do |connection|
      apply_statement_timeout(connection)
      result = connection.exec_query(sql_with_comment).to_a
      raise ActiveRecord::Rollback
    end

    # Audit successful execution
    Glancer::Audit.create!(
      question: original_question,
      code: sql_with_comment,
      code_type: "sql",
      adapter: Glancer.configuration.resolved_adapter,
      run_id: run_id,
      executed_at: Time.current,
      message_id: message_id
    )

    result
  rescue StandardError => e
    # Stop recursion if we reached the maximum number of attempts (3)
    if attempt >= 3
      Glancer::Utils::Logger.error("Workflow::Executor", "Final failure after #{attempt} attempts: #{e.message}")
      return { error: true, message: e.message, last_code: sql }
    end

    Glancer::Utils::Logger.warn("Workflow::Executor",
                                "SQL Error (Attempt ##{attempt}): #{e.message}. Requesting correction...")

    # Invoke the Builder to analyze the error and fix the SQL
    fixed_sql = Glancer::Workflow::Builder.fix_sql(sql, e.message)

    # Retry execution with the corrected SQL
    execute(fixed_sql, original_question: original_question, attempt: attempt + 1, message_id: message_id)
  end
end