Class: Pgtk::Stash

Inherits:
Object
  • Object
show all
Defined in:
lib/pgtk/stash.rb

Overview

Database query cache implementation.

Provides a caching layer for PostgreSQL queries, automatically invalidating the cache when tables are modified. Read queries are cached while write queries bypass the cache and invalidate related cached entries.

Thread-safe with read-write locking.

The implementation is very naive! Use it at your own risk.

Author

Yegor Bugayenko ([email protected])

Copyright

Copyright © 2019-2025 Yegor Bugayenko

License

MIT

Instance Method Summary collapse

Constructor Details

#initialize(pgsql, stash = {}) ⇒ Stash

Initialize a new Stash with query caching.

Parameters:

  • pgsql (Object)

    PostgreSQL connection object

  • stash (Hash) (defaults to: {})

    Optional existing stash to use (default: new empty stash)

  • loog (Loog)

    Logger for debugging (default: null logger)



30
31
32
33
34
35
36
# File 'lib/pgtk/stash.rb', line 30

def initialize(pgsql, stash = {})
  @pgsql = pgsql
  @stash = stash
  @stash[:queries] ||= {}
  @stash[:tables] ||= {}
  @entrance = Concurrent::ReentrantReadWriteLock.new
end

Instance Method Details

#exec(query, params = []) ⇒ PG::Result

Execute a SQL query with optional caching.

Read queries are cached, while write queries bypass the cache and invalidate related entries.

Parameters:

  • query (String, Array<String>)

    The SQL query to execute

  • params (Array) (defaults to: [])

    Query parameters

Returns:

  • (PG::Result)

    Query result



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/pgtk/stash.rb', line 45

def exec(query, params = [])
  pure = (query.is_a?(Array) ? query.join(' ') : query).gsub(/\s+/, ' ').strip
  if /(^|\s)(INSERT|DELETE|UPDATE|LOCK)\s/.match?(pure) || /(^|\s)pg_[a-z_]+\(/.match?(pure)
    tables = pure.scan(/(?<=^|\s)(?:UPDATE|INSERT INTO|DELETE FROM|TRUNCATE)\s([a-z]+)(?=[^a-z]|$)/).map(&:first).uniq
    ret = @pgsql.exec(pure, params)
    @entrance.with_write_lock do
      tables.each do |t|
        @stash[:tables][t]&.each do |q|
          @stash[:queries].delete(q)
        end
        @stash[:tables].delete(t)
      end
    end
  else
    key = params.map(&:to_s).join(' -*&%^- ')
    @entrance.with_write_lock { @stash[:queries][pure] ||= {} }
    ret = @stash[:queries][pure][key]
    if ret.nil?
      ret = @pgsql.exec(pure, params)
      unless /(?<=^|\s)(NOW\(\)|COMMIT|ROLLBACK|START TRANSACTION|TRUNCATE|TO WARNING)(?=;|\s|$)/.match?(pure)
        @entrance.with_write_lock do
          @stash[:queries][pure] ||= {}
          @stash[:queries][pure][key] = ret
          tables = pure.scan(/(?<=^|\s)(?:FROM|JOIN) ([a-z_]+)(?=\s|$)/).map(&:first).uniq
          tables.each do |t|
            @stash[:tables][t] = [] if @stash[:tables][t].nil?
            @stash[:tables][t].append(pure).uniq!
          end
          raise "No tables at #{pure.inspect}" if tables.empty?
        end
      end
    end
  end
  ret
end

#start(*args) ⇒ Pgtk::Stash

Start a new connection pool with the given arguments.

Parameters:

  • args

    Arguments to pass to the underlying pool’s start method

Returns:

  • (Pgtk::Stash)

    A new stash that shares the same cache



97
98
99
# File 'lib/pgtk/stash.rb', line 97

def start(*args)
  Pgtk::Stash.new(@pgsql.start(*args), @stash)
end

#transaction {|Pgtk::Stash| ... } ⇒ Object

Execute a database transaction.

Yields a new Stash that shares the same cache but uses the transaction connection.

Yields:

  • (Pgtk::Stash)

    A stash connected to the transaction

Returns:

  • (Object)

    The result of the block



87
88
89
90
91
# File 'lib/pgtk/stash.rb', line 87

def transaction
  @pgsql.transaction do |t|
    yield Pgtk::Stash.new(t, @stash)
  end
end

#versionString

Get the PostgreSQL server version.

Returns:

  • (String)

    Version string of the database server



104
105
106
# File 'lib/pgtk/stash.rb', line 104

def version
  @pgsql.version
end