Skip to content

Commit

Permalink
relation: Add .readyset_explain
Browse files Browse the repository at this point in the history
This commit adds a `.readyset_explain` method to our relation extension.
This method invokes `EXPLAIN CREATE CACHE` upstream on ReadySet and
returns information about the query from ReadySet.
  • Loading branch information
ethowitz committed Jan 2, 2024
1 parent 0ad5112 commit 422358b
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 6 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
12 changes: 10 additions & 2 deletions lib/readyset/relation_extension.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module RelationExtension
# query already exists.
#
# NOTE: If the ActiveRecord query eager loads associations (e.g. via `#includes`), the
# the queries issues to do the eager loading will not have caches created. Those queries must
# the queries issued to do the eager loading will not have caches created. Those queries must
# have their caches created separately.
#
# @return [void]
Expand All @@ -19,13 +19,21 @@ def create_readyset_cache!
# for the query already doesn't exist.
#
# NOTE: If the ActiveRecord query eager loads associations (e.g. via `#includes`), the
# the queries issues to do the eager loading will not have caches dropped. Those queries must
# the queries issued to do the eager loading will not have caches dropped. Those queries must
# have their caches dropped separately.
#
# @return [void]
def drop_readyset_cache!
Readyset.drop_cache!(sql: to_sql)
end

# Gets information about this query from ReadySet, including the query's ID, the normalized
# query text, and whether the query is supported by ReadySet.
#
# @return [Readyset::Explain]
def readyset_explain
Readyset.explain(to_sql)
end
end
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
32 changes: 28 additions & 4 deletions spec/relation_extension_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
subject
end

it 'invokes `Readyset.create_cache!` with the parameterized query string that the ' \
'relation represents' do
it "invokes `Readyset.create_cache!` with the relation's query string" do
expect(Readyset).to have_received(:create_cache!).with(sql: query_string)
end
end
Expand All @@ -29,9 +28,34 @@
subject
end

it 'invokes `Readyset.drop_cache!` with the parameterized query string that the relation ' \
'represents' do
it "invokes `Readyset.drop_cache!` with the relation's query string" do
expect(Readyset).to have_received(:drop_cache!).with(sql: query_string)
end
end

describe '#readyset_explain' do
it "invokes `Readyset.readyset_explain` with the relation's query string" do
query = Cat.where(id: 1)
query_string = query.to_sql
allow(Readyset).to receive(:explain).with(query_string).
and_return(instance_double(Readyset::Explain))

query.readyset_explain

expect(Readyset).to have_received(:explain).with(query_string)
end

it 'returns the expected explain information' do
query = Cat.where(id: 1)
query_string = query.to_sql
explain = Readyset::Explain.new(id: 'q_0000000000000000',
text: 'SELECT * FROM "cats" WHERE ("id" = $1)',
supported: :yes)
allow(Readyset).to receive(:explain).with(query_string).and_return(explain)

output = query.readyset_explain

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

0 comments on commit 422358b

Please sign in to comment.