Module: Arfi::PostgreSQL::DatabaseStatementsPatch

Defined in:
lib/arfi/extensions/active_record/connection_adapters/postgresql/database_statements.rb,
sig/lib/arfi/extensions/active_record/connection_adapters/postgresql/database_statements.rbs

Overview

ActiveRecord adapter patch for PostgreSQL.

When a query fails with PG::UndefinedFunction, ARFI checks whether the missing function is managed by ARFI (file exists under db/functions). If yes, ARFI loads function SQL files via SqlFunctionLoader.load! and retries the failed query once.

Constant Summary collapse

ARFI_UNDEFINED_FUNCTION =

Returns:

  • (::Regexp)
/
  function\s+([a-zA-Z0-9_."]+)\s*\(.*?\)\s+does\s+not\s+exist
/ix.freeze
THREAD_GUARD_KEY =

Returns:

  • (:arfi_reloading_functions)
:arfi_reloading_functions

Instance Method Summary collapse

Instance Method Details

#arfi_extract_function_ident(message) ⇒ Array<nil>, Object

Extract the schema and function name from a PG::UndefinedFunction error message.

Parameters:

  • message (Object)

    The error message string

Returns:

  • (Array<nil>, Object)

    Array of [schema, function_name] or [nil, nil] if not matched



120
121
122
123
124
125
126
127
# File 'lib/arfi/extensions/active_record/connection_adapters/postgresql/database_statements.rb', line 120

def arfi_extract_function_ident(message)
  m = message.to_s.match(ARFI_UNDEFINED_FUNCTION)
  return [nil, nil] unless m

  ident = m[1].to_s.delete('"')
  parts = ident.split(".", 2)
  parts.length == 2 ? [parts[0], parts[1]] : [nil, parts[0]]
end

#arfi_has_function_file_for?(schema, fn) ⇒ Boolean, Object

Check whether a function file exists under db/functions for the given schema and function name.

Parameters:

  • schema (Object)

    Schema name (possibly nil)

  • fn (Object)

    Function name

Returns:

  • (Boolean, Object)

    Whether a matching file exists on disk



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/arfi/extensions/active_record/connection_adapters/postgresql/database_statements.rb', line 135

def arfi_has_function_file_for?(schema, fn)
  return false if fn.nil? || fn.empty?

  root = Rails.root.join("db", "functions")
  return false unless root.directory?

  candidates = []
  candidates << root.join("public", "#{fn}.sql")
  candidates << root.join("#{fn}.sql") # legacy generic

  pg_root = root.join("postgresql")
  if pg_root.directory?
    candidates << pg_root.join("public", "#{fn}.sql")
    candidates << pg_root.join("#{fn}.sql") # legacy adapter

    if schema && !schema.empty?
      candidates << pg_root.join(schema, "#{fn}.sql")
    else
      candidates.concat Dir.glob(pg_root.join("*", "#{fn}.sql").to_s)
    end
  end

  candidates.any? { |p| File.exist?(p) }
end

#arfi_try_reload_and_retry?(e) ⇒ Boolean

Check if the error is caused by a missing ARFI-managed function and attempt to reload it.

Uses a thread guard to prevent recursive retries.

Parameters:

  • e (Object)

    The raised exception (StandardError with possible PG::UndefinedFunction cause)

Returns:

  • (Boolean)

    Whether the error was handled (reload attempted)



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/arfi/extensions/active_record/connection_adapters/postgresql/database_statements.rb', line 92

def arfi_try_reload_and_retry?(e)
  pg_error = e.cause || e
  return false unless pg_error.class.name == "PG::UndefinedFunction"
  return false if Thread.current[THREAD_GUARD_KEY]

  schema, fn = arfi_extract_function_ident(pg_error.message)
  return false unless arfi_has_function_file_for?(schema, fn)

  Thread.current[THREAD_GUARD_KEY] = true
  begin
    Arfi::SqlFunctionLoader.load!(
      task_name: "arfi:runtime",
      connection: self,                # same connection
      clear_active_connections: false, # runtime retry path
      verbose: false
    )
  ensure
    Thread.current[THREAD_GUARD_KEY] = false
  end

  true
end

#exec_query(*args, **kwargs) ⇒ Object

Rails 6/7: SELECT often goes through exec_query

Parameters:

Returns:



34
35
36
37
38
39
# File 'lib/arfi/extensions/active_record/connection_adapters/postgresql/database_statements.rb', line 34

def exec_query(*args, **kwargs)
  super
rescue StandardError => e
  raise unless arfi_try_reload_and_retry?(e)
  retry
end

#execute(*args, **kwargs) ⇒ Object

DDL often goes through execute

Parameters:

Returns:



48
49
50
51
52
53
# File 'lib/arfi/extensions/active_record/connection_adapters/postgresql/database_statements.rb', line 48

def execute(*args, **kwargs)
  super
rescue StandardError => e
  raise unless arfi_try_reload_and_retry?(e)
  retry
end

#internal_exec_query(*args, **kwargs) ⇒ Object

Execute an internal query, reloading missing ARFI-managed functions and retrying on PG::UndefinedFunction.

Parameters:

  • args (Array<Object>)

    Positional arguments forwarded to the original internal_exec_query

  • kwargs (Object)

    Keyword arguments forwarded to the original internal_exec_query

Returns:

  • (Object)

    Query result

  • (Object)

    if StandardError (retry successful)

Raises:

  • (StandardError)

    If the error is not recoverable



76
77
78
79
80
81
# File 'lib/arfi/extensions/active_record/connection_adapters/postgresql/database_statements.rb', line 76

def internal_exec_query(*args, **kwargs)
  super
rescue StandardError => e
  raise unless arfi_try_reload_and_retry?(e)
  retry
end

#raw_execute(*args, **kwargs) ⇒ Object

Rails 7+/8 may use raw_execute

Parameters:

Returns:



62
63
64
65
66
67
# File 'lib/arfi/extensions/active_record/connection_adapters/postgresql/database_statements.rb', line 62

def raw_execute(*args, **kwargs)
  super
rescue StandardError => e
  raise unless arfi_try_reload_and_retry?(e)
  retry
end