Skip to content

Commit

Permalink
Add initial support for Query Objects
Browse files Browse the repository at this point in the history
A new main component of Rectify is the Query Object. It allows a
database query to be encapsulated into a class. This allows logic that
is specific to the query as well as the query itself to be removed from
the ActiveRecord model as another strategy to reducing their size.

Query Objects need to be derived from `Rectify::Query` and must
implement the `query` method that returns either an
`ActiveRecord::Relation` or an array (via
`ActiveRecord::Quering#find_by_sql`)

This commit also adds a simple way to stub Query Objects with the
`stub_query` RSpec helper.

See the readme for full details of how to use Query Objects in a Rails
app.
  • Loading branch information
andypike committed Apr 10, 2016
1 parent c7574f9 commit df13f65
Show file tree
Hide file tree
Showing 26 changed files with 860 additions and 37 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -1 +1,2 @@
*.gem
*.sqlite3
6 changes: 5 additions & 1 deletion Gemfile.lock
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
rectify (0.2.0)
rectify (0.3.0)
activemodel (~> 4.2, >= 4.2.0)
activerecord (~> 4.2, >= 4.2.0)
activesupport (~> 4.2, >= 4.2.0)
Expand Down Expand Up @@ -77,6 +77,7 @@ GEM
rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
rake (10.4.2)
rspec (3.4.0)
rspec-core (~> 3.4.0)
rspec-expectations (~> 3.4.0)
Expand All @@ -93,6 +94,7 @@ GEM
rspec-support (~> 3.4.0)
rspec-support (3.4.1)
slop (3.6.0)
sqlite3 (1.3.11)
thread_safe (0.3.5)
tzinfo (1.2.2)
thread_safe (~> 0.1)
Expand All @@ -111,9 +113,11 @@ DEPENDENCIES
actionpack (~> 4.2, >= 4.2.0)
awesome_print (~> 1.6)
pry (~> 0.10.3)
rake
rectify!
rspec (~> 3.4)
rspec-collection_matchers (~> 1.1, >= 1.1.2)
sqlite3
wisper-rspec (~> 0.0.2)

BUNDLED WITH
Expand Down
50 changes: 50 additions & 0 deletions Rakefile
@@ -0,0 +1,50 @@
# Stolen from https://gist.github.com/schickling/6762581
# Thank you <3
require "yaml"
require "active_record"

namespace :db do
db_config = YAML.load(File.open("spec/config/database.yml"))

desc "Migrate the database"
task :migrate do
ActiveRecord::Base.establish_connection(db_config)
ActiveRecord::Migrator.migrate("spec/db/migrate")
Rake::Task["db:schema"].invoke
puts "Database migrated."
end

desc "Create a db/schema.rb file that is portable against any supported DB"
task :schema do
ActiveRecord::Base.establish_connection(db_config)
require "active_record/schema_dumper"
filename = "spec/db/schema.rb"
File.open(filename, "w:utf-8") do |file|
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
end
end
end

namespace :g do
desc "Generate migration"
task :migration do
name = ARGV[1] || fail("Specify name: rake g:migration your_migration")
timestamp = Time.now.strftime("%Y%m%d%H%M%S")
folder = "../spec/db/migrate"
path = File.expand_path("#{folder}/#{timestamp}_#{name}.rb", __FILE__)

migration_class = name.split("_").map(&:capitalize).join

File.open(path, "w") do |file|
file.write <<-EOF.strip_heredoc
class #{migration_class} < ActiveRecord::Migration
def change
end
end
EOF
end

puts "Migration #{path} created"
abort # needed stop other tasks
end
end
2 changes: 2 additions & 0 deletions lib/rectify.rb
Expand Up @@ -10,4 +10,6 @@
require "rectify/form"
require "rectify/command"
require "rectify/presenter"
require "rectify/query"
require "rectify/controller_helpers"
require "rectify/errors"
11 changes: 11 additions & 0 deletions lib/rectify/errors.rb
@@ -0,0 +1,11 @@
module Rectify
class UnableToComposeQueries < StandardError
def initialize(query, other)
super(
"Unable to composite queries #{query.class.name} and " \
"#{other.class.name}. You cannot compose queries where #query " \
"returns an ActiveRecord::Relation in one and an array in the other."
)
end
end
end
65 changes: 65 additions & 0 deletions lib/rectify/query.rb
@@ -0,0 +1,65 @@
module Rectify
module SqlQuery
def query
model.find_by_sql([sql, params])
end
end

class Query
def initialize(scope = ActiveRecord::NullRelation)
@scope = scope
end

def query
@scope
end

def |(other)
if relation? && other.relation?
Rectify::Query.new(cached_query.merge(other.cached_query))
elsif eager? && other.eager?
Rectify::Query.new(cached_query | other.cached_query)
else
fail UnableToComposeQueries.new(self, other)
end
end

def count
cached_query.count
end

def first
cached_query.first
end

def each(&block)
cached_query.each(&block)
end

def exists?
return cached_query.exists? if relation?

cached_query.present?
end

def none?
!exists?
end

def to_a
cached_query.to_a
end

def relation?
cached_query.is_a?(ActiveRecord::Relation)
end

def eager?
cached_query.is_a?(Array)
end

def cached_query
@cached_query ||= query
end
end
end
3 changes: 3 additions & 0 deletions lib/rectify/rspec.rb
@@ -0,0 +1,3 @@
require "rectify/rspec/stub_query"
require "rectify/rspec/helpers"
require "rectify/rspec/matchers"
10 changes: 10 additions & 0 deletions lib/rectify/rspec/helpers.rb
@@ -0,0 +1,10 @@
module Rectify
module RSpec
module Helpers
def stub_query(query_class, options = {})
results = options.fetch(:results, [])
allow(query_class).to receive(:new) { StubQuery.new(results) }
end
end
end
end
51 changes: 51 additions & 0 deletions lib/rectify/rspec/matchers.rb
@@ -0,0 +1,51 @@
require "rspec/expectations"

module Rectify
module DatabaseReporting
SQL_TO_IGNORE = /
pg_table|
pg_attribute|
pg_namespace|
current_database|
information_schema|
^TRUNCATE TABLE|
^ALTER TABLE|
^BEGIN|
^COMMIT|
^ROLLBACK|
^RELEASE|
^SAVEPOINT|
^SHOW|
^PRAGMA
/xi
end
end

RSpec::Matchers.define :make_database_queries_of do |expected|
supports_block_expectations

queries = []

match do |proc|
ActiveSupport::Notifications
.subscribe("sql.active_record") do |_, _, _, _, query|
sql = query[:sql]

unless Rectify::DatabaseReporting::SQL_TO_IGNORE.match(sql)
queries << sql
end
end

proc.call

queries.size == expected
end

failure_message do |_|
all_queries = queries.join("\n")

"expected the number of queries to be #{expected} " \
"but there were #{queries.size}.\n\n" \
"Here are the queries that were made:\n\n#{all_queries}"
end
end
13 changes: 13 additions & 0 deletions lib/rectify/rspec/stub_query.rb
@@ -0,0 +1,13 @@
module Rectify
module RSpec
class StubQuery < Query
def initialize(results)
@results = Array(results)
end

def query
@results
end
end
end
end
2 changes: 1 addition & 1 deletion lib/rectify/version.rb
@@ -1,3 +1,3 @@
module Rectify
VERSION = "0.2.0"
VERSION = "0.3.0"
end

0 comments on commit df13f65

Please sign in to comment.