Skip to content

Commit

Permalink
Add Readyset.explain
Browse files Browse the repository at this point in the history
Adds the ability get information about a query from ReadySet via a
`Readyset.explain` method that invokes ReadySet's
`EXPLAIN CREATE CACHE FROM` command.
  • Loading branch information
ethowitz committed Jan 2, 2024
1 parent 0ad5112 commit a5fd8fc
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 0 deletions.
13 changes: 13 additions & 0 deletions lib/readyset.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'readyset/configuration'
require 'readyset/controller_extension'
require 'readyset/model_extension'
require 'readyset/explain'
require 'readyset/query'
require 'readyset/query/cached_query'
require 'readyset/query/proxied_query'
Expand Down Expand Up @@ -83,6 +84,18 @@ def self.drop_cache!(name_or_id: nil, sql: nil)
nil
end

# Gets information about the given query from ReadySet, including whether it's supported to be
# cached, its current status, the rewritten query text, and the query ID.
#
# The information about the given query is retrieved by invoking `EXPLAIN CREATE CACHE FROM` on
# ReadySet.
#
# @param [String] a query about which information should be retrieved
# @return [Explain]
def self.explain(query)
Explain.call(query)
end

# Executes a raw SQL query against ReadySet. The query is sanitized prior to being executed.
# @note This method is not part of the public API.
# @param sql_array [Array<Object>] the SQL array to be executed against ReadySet.
Expand Down
59 changes: 59 additions & 0 deletions lib/readyset/explain.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module Readyset
class Explain
attr_reader :id, :text, :supported

# Gets information about the given query from ReadySet, including whether it's supported to be
# cached, its current status, the rewritten query text, and the query ID.
#
# The information about the given query is retrieved by invoking `EXPLAIN CREATE CACHE FROM` on
# ReadySet.
#
# @param [String] a query about which information should be retrieved
# @return [Explain]
def self.call(query)
raw_results = Readyset.raw_query('EXPLAIN CREATE CACHE FROM %s', query)
from_readyset_results(**raw_results.first.to_h.symbolize_keys)
end

# Creates a new `Explain` with the given attributes.
#
# @param [String] id the ID of the query
# @param [String] text the query text
# @param [Symbol] supported the supported status of the query
# @return [Explain]
def initialize(id:, text:, supported:) # :nodoc:
@id = id
@text = text
@supported = supported
end

# Compares `self` with another `Explain` by comparing them attribute-wise.
#
# @param [Explain] other the `Explain` to which `self` should be compared
# @return [Boolean]
def ==(other)
id == other.id &&
text == other.text &&
supported == other.supported
end

# Returns true if the explain information returned by ReadySet indicates that the query is
# unsupported.
#
# @return [Boolean]
def unsupported?
supported == :unsupported
end

private

def self.from_readyset_results(**attributes)
new(
id: attributes[:'query id'],
text: attributes[:query],
supported: attributes[:'readyset supported'].to_sym,
)
end
private_class_method :from_readyset_results
end
end
78 changes: 78 additions & 0 deletions spec/explain_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Readyset::Explain do
describe '.call' do
it 'retrieves the explain information from ReadySet' do
explain = build(:explain)
raw_result = {
:'query id' => explain.id,
:'readyset supported' => explain.supported,
query: explain.text,
}
allow(Readyset).to receive(:raw_query).with('EXPLAIN CREATE CACHE FROM %s', explain.text).
and_return([raw_result])

result = Readyset::Explain.call(explain.text)

expect(result).to eq(explain)
end
end

describe '.new' do
it 'creates a new `Explain` with the given attributes' do
attributes = attributes_for(:explain)

explain = Readyset::Explain.new(**attributes)

expect(explain).to eq(build(:explain))
end
end

describe '#==' do
context "when the other `Explain` has an attribute that doesn't match self's" do
it 'returns false' do
explain = build(:explain)
other = build(:explain, supported: :pending)

result = explain == other

expect(result).to eq(false)
end
end

context 'when the attributes of the other `Explain` match those of `self`' do
it 'returns true' do
explain = build(:explain)
other = build(:explain)

result = explain == other

expect(result).to eq(true)
end
end
end

describe '#unsupported?' do
context 'when the `Explain` indicates that the query is supported' do
it 'returns false' do
explain = build(:explain)

result = explain.unsupported?

expect(result).to eq(false)
end
end

context 'when the `Explain` indicates that the query is unsupported' do
it 'returns false' do
explain = build(:explain, supported: :unsupported)

result = explain.unsupported?

expect(result).to eq(true)
end
end
end
end
9 changes: 9 additions & 0 deletions spec/factories/explain.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FactoryBot.define do
factory :explain, class: 'Readyset::Explain' do
id { 'q_eafb620c78f5b9ac' }
text { 'SELECT * FROM "t" WHERE ("x" = $1)' }
supported { :yes }

initialize_with { new(**attributes) }
end
end
20 changes: 20 additions & 0 deletions spec/ready_set_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,26 @@
end
end

describe '.explain' do
it 'invokes `Explain.call` with the given query' do
explain = build(:explain)
allow(Readyset::Explain).to receive(:call).with(explain.text).and_return(explain)

Readyset.explain(explain.text)

expect(Readyset::Explain).to have_received(:call).with(explain.text)
end

it 'returns a `Explain`' do
explain = build(:explain)
allow(Readyset::Explain).to receive(:call).with(explain.text).and_return(explain)

result = Readyset.explain(explain.text)

expect(result).to eq(explain)
end
end

describe '.raw_query' do
subject { Readyset.raw_query(*query) }

Expand Down

0 comments on commit a5fd8fc

Please sign in to comment.