Module: Txnap::LockHeuristics

Defined in:
lib/txnap/lock_heuristics.rb

Constant Summary collapse

IDENTIFIER_PART =
/(?:"(?:[^"]|"")*"|`(?:[^`]|``)*`|\[[^\]]+\]|[a-z_][a-z0-9_$]*)/i
QUALIFIED_IDENTIFIER =
/#{IDENTIFIER_PART}(?:\s*\.\s*#{IDENTIFIER_PART})*/
ADVISORY_FUNCTION =
/
  (?:
    pg_(?:try_)?advisory_(?:xact_)?lock(?:_shared)?
    |
    get_lock
  )
/ix

Class Method Summary collapse

Class Method Details

.detect(sql, call_site: nil) ⇒ Object



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
# File 'lib/txnap/lock_heuristics.rb', line 17

def detect(sql, call_site: nil)
  statement = strip_leading_comments(Array(sql).join(" "))
  return if statement.empty?

  operation, table, kind =
    case statement
    when /\AINSERT(?:\s+OR\s+\w+)?\s+INTO\s+(#{QUALIFIED_IDENTIFIER})/io
      ["INSERT", normalize_identifier(Regexp.last_match(1)), "row lock"]
    when /\AUPDATE\s+(?:ONLY\s+)?(#{QUALIFIED_IDENTIFIER})/io
      ["UPDATE", normalize_identifier(Regexp.last_match(1)), "row lock"]
    when /\ADELETE\s+FROM\s+(?:ONLY\s+)?(#{QUALIFIED_IDENTIFIER})/io
      ["DELETE", normalize_identifier(Regexp.last_match(1)), "row lock"]
    when /\ALOCK\s+TABLE\s+(?:ONLY\s+)?(#{QUALIFIED_IDENTIFIER})/io
      ["LOCK TABLE", normalize_identifier(Regexp.last_match(1)), "table lock"]
    else
      detect_select_or_advisory(statement)
    end

  return unless operation

  LockCandidate.new(
    statement: operation,
    table: table,
    kind: kind,
    call_site: call_site
  ).freeze
end

.detect_select_or_advisory(statement) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/txnap/lock_heuristics.rb', line 45

def detect_select_or_advisory(statement)
  if statement.match?(/\ASELECT\b/im) &&
      statement.match?(/\bFOR\s+(?:NO\s+KEY\s+)?(?:UPDATE|SHARE|KEY\s+SHARE)\b/im)
    table = statement[/\bFROM\s+(#{QUALIFIED_IDENTIFIER})/io, 1]
    return ["SELECT", normalize_identifier(table), "row lock"] if table
  end

  function = statement[ADVISORY_FUNCTION]
  return unless function

  ["ADVISORY LOCK", function.downcase, "advisory lock"]
end

.normalize_identifier(identifier) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/txnap/lock_heuristics.rb', line 76

def normalize_identifier(identifier)
  return unless identifier

  identifier.split(/\s*\.\s*/).map do |part|
    case part
    when /\A"(.*)"\z/m
      Regexp.last_match(1).gsub('""', '"')
    when /\A`(.*)`\z/m
      Regexp.last_match(1).gsub("``", "`")
    when /\A\[(.*)\]\z/m
      Regexp.last_match(1)
    else
      part
    end
  end.join(".")
end

.strip_leading_comments(sql) ⇒ Object



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/txnap/lock_heuristics.rb', line 58

def strip_leading_comments(sql)
  remaining = sql.lstrip

  loop do
    stripped =
      if remaining.start_with?("--")
        remaining.sub(/\A--[^\n]*(?:\n|\z)/, "").lstrip
      elsif remaining.start_with?("/*")
        remaining.sub(/\A\/\*.*?\*\//m, "").lstrip
      else
        remaining
      end
    return remaining if stripped == remaining

    remaining = stripped
  end
end