Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds an exists? class method #343

Merged
merged 9 commits into from
Jul 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/querying.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,27 @@ You can combine this with an argument to `all` or `first` for maximum flexibilit
```crystal
results = CustomView.all("WHERE articles.author = ?", ["Noah"])
```

## Exists?

The `exists?` class method returns `true` if a record exists in the table that matches the provided *id* or *criteria*, otherwise `false`.

If passed a `Number` or `String`, it will attempt to find a record with that primary key. If passed a `Hash` or `NamedTuple`, it will find the record that matches that criteria, similar to `find_by`.

```crystal
# Assume a model named Post with a title field
post = Post.new(title: "My Post")
post.save
post.id # => 1

Post.exists? 1 # => true
Post.exists? {"id" => 1, :title => "My Post"} # => true
Post.exists? {id: 1, title: "Some Post"} # => false
```

The `exists?` method can also be used with the query builder.

```crystal
Post.where(published: true, author_id: User.first!.id).exists?
Post.where(:created_at, :gt, Time.local - 7.days).exists?
```
2 changes: 1 addition & 1 deletion shard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies:
development_dependencies:
mysql:
github: crystal-lang/crystal-mysql
version: ~> 0.6.0
version: ~> 0.7.0

sqlite3:
github: crystal-lang/crystal-sqlite3
Expand Down
2 changes: 1 addition & 1 deletion spec/granite/associations/belongs_to_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe "belongs_to" do
book = Book.new
book.name = "Introduction to Granite"

expect_raises Granite::Querying::NotFound, "No Company found where id = " { book.publisher! }
expect_raises Granite::Querying::NotFound, "No Company found where id is NULL" { book.publisher! }
end

it "provides the ability to use a custom primary key" do
Expand Down
82 changes: 82 additions & 0 deletions spec/granite/querying/exists_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
require "../../spec_helper"

describe ".exists?" do
describe "when there is a record with that ID" do
describe "with a numeric PK" do
it "should return true" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.exists?(model.id).should be_true
end
end

describe "with a string PK" do
it "should return true" do
Kvs.new(k: "EXISTS_ID").save.should be_true
Kvs.exists?("EXISTS_ID").should be_true
end
end

describe "with a namedtuple of args" do
it "should return true" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.exists?(name: "Some Name", id: model.id).should be_true
end
end

describe "with a hash of args" do
it "should return true" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.exists?({:name => "Some Name", "id" => model.id}).should be_true
end
end

describe "with a nil value" do
it "should return true" do
model = Student.new
model.save.should be_true
Student.exists?(name: nil, id: model.id).should be_true
end
end
end

describe "when there is not a record with that ID" do
describe "with a numeric PK" do
it "should return false" do
Parent.exists?(234567).should be_false
end
end

describe "with a string PK" do
it "should return false" do
Kvs.exists?("SOME_KEY").should be_false
end
end

describe "with a namedtuple of args" do
it "should return false" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.exists?(name: "Some Other Name", id: model.id).should be_false
end
end

describe "with a hash of args" do
it "should return false" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.exists?({:name => "Some Other Name", "id" => model.id}).should be_false
end
end

describe "with a nil value" do
it "should return false" do
model = Student.new(name: "Jim")
model.save.should be_true
Student.exists?(name: nil, id: model.id).should be_false
end
end
end
end
34 changes: 30 additions & 4 deletions spec/granite/querying/find_by_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ describe "#find_by, #find_by!" do
Parent.clear
name = "robinson"

model = Parent.new
model.name = name
model.save
model = Parent.new(name: name)
model.save.should be_true

found = Parent.find_by(name: name)
found.not_nil!.id.should eq model.id
Expand All @@ -31,13 +30,26 @@ describe "#find_by, #find_by!" do
end
end

it "finds an object with nil value" do
Student.clear

model = Student.new
model.save.should be_true

found = Student.find_by(name: nil)
found.not_nil!.id.should eq model.id

found = Student.find_by!(name: nil)
found.should be_a(Student)
end

it "works with reserved words" do
Parent.clear
value = "robinson"

model = ReservedWord.new
model.all = value
model.save
model.save.should be_true

found = ReservedWord.find_by(all: value)
found.not_nil!.id.should eq model.id
Expand All @@ -46,6 +58,20 @@ describe "#find_by, #find_by!" do
found.id.should eq model.id
end

it "finds an object when provided a hash" do
Parent.clear
name = "johnson"

model = Parent.new(name: name)
model.save.should be_true

found = Parent.find_by({"name" => name})
found.not_nil!.id.should eq model.id

found = Parent.find_by!({"name" => name})
found.should be_a(Parent)
end

it "returns nil or raises if no result" do
Parent.clear
found = Parent.find_by(name: "xxx")
Expand Down
38 changes: 37 additions & 1 deletion spec/granite/querying/query_builder_spec.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require "../../spec_helper"

describe "#query_builder_methods" do
describe Granite::Query::BuilderMethods do
describe "#where" do
describe "with array argument" do
it "correctly queries string fields" do
Expand Down Expand Up @@ -47,4 +47,40 @@ describe "#query_builder_methods" do
{% end %}
end
end

describe "#exists?" do
describe "when there is a record with that ID" do
describe "when querying on the PK" do
it "should return true" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.where(id: model.id).exists?.should be_true
end
end

describe "with multiple args" do
it "should return true" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.where(name: "Some Name", id: model.id).exists?.should be_true
end
end
end

describe "when there is not a record with that ID" do
describe "when querying on the PK" do
it "should return false" do
Parent.where(id: 234567).exists?.should be_false
end
end

describe "with multiple args" do
it "should return false" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.where(name: "Some Other Name", id: model.id).exists?.should be_false
end
end
end
end
end
20 changes: 18 additions & 2 deletions src/adapter/base.cr
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ abstract class Granite::Adapter::Base
# remove all rows from a table and reset the counter on the id.
abstract def clear(table_name)

# select performs a query against a table. The query object containes table_name,
# select performs a query against a table. The query object contains table_name,
# fields (configured using the sql_mapping directive in your model), and an optional
# raw query string. The clause and params is the query and params that is passed
# in via .all() method
Expand All @@ -60,7 +60,23 @@ abstract class Granite::Adapter::Base
log statement, elapsed_time, params
end

def ensure_clause_template(clause)
# Returns `true` if a record exists that matches *criteria*, otherwise `false`.
def exists?(table_name : String, criteria : String, params = [] of Granite::Fields::Type) : Bool
statement = "SELECT EXISTS(SELECT 1 FROM #{table_name} WHERE #{ensure_clause_template(criteria)})"

exists = false
elapsed_time = Time.measure do
open do |db|
exists = db.query_one?(statement, params, as: Bool) || exists
end
end

log statement, elapsed_time, params

exists
end

protected def ensure_clause_template(clause : String) : String
clause
end

Expand Down
2 changes: 1 addition & 1 deletion src/adapter/pg.cr
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class Granite::Adapter::Pg < Granite::Adapter::Base
log statement, elapsed_time, value
end

def ensure_clause_template(clause : String) : String
protected def ensure_clause_template(clause : String) : String
robacarp marked this conversation as resolved.
Show resolved Hide resolved
if clause.includes?("?")
num_subs = clause.count("?")

Expand Down
11 changes: 11 additions & 0 deletions src/granite/query/assemblers/base.cr
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,17 @@ module Granite::Query::Assembler
Executor::List(Model).new sql, numbered_parameters
end

def exists? : Executor::Value(Model, Bool)
sql = build_sql do |s|
s << "SELECT EXISTS(SELECT 1 "
s << "FROM #{table_name} "
s << where
s << ")"
end

Executor::Value(Model, Bool).new sql, numbered_parameters, default: false
end
robacarp marked this conversation as resolved.
Show resolved Hide resolved

OPERATORS = {"eq": "=", "gteq": ">=", "lteq": "<=", "neq": "!=", "ltgt": "<>", "gt": ">", "lt": "<", "ngt": "!>", "nlt": "!<", "in": "IN", "nin": "NOT IN", "like": "LIKE", "nlike": "NOT LIKE"}

def sql_operator(operator : Symbol) : String
Expand Down
4 changes: 4 additions & 0 deletions src/granite/query/builder.cr
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ class Granite::Query::Builder(Model)
assembler.select.run
end

def exists? : Bool
assembler.exists?.run
end

robacarp marked this conversation as resolved.
Show resolved Hide resolved
def size
count
end
Expand Down
8 changes: 2 additions & 6 deletions src/granite/query/executors/value.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,11 @@ module Granite::Query::Executor
# db.scalar raises when a query returns 0 results, so I'm using query_one?
# https://github.com/crystal-lang/crystal-db/blob/7d30e9f50e478cb6404d16d2ce91e639b6f9c476/src/db/statement.cr#L18

raise "No default provided" unless @default
raise "No default provided" if @default.nil?
robacarp marked this conversation as resolved.
Show resolved Hide resolved

Model.adapter.open do |db|
db.query_one? @sql, @args do |record_set|
return record_set.read.as Scalar
end
db.query_one?(@sql, @args, as: Scalar) || @default.not_nil!
end

@default.not_nil!
end

delegate :<, :>, :<=, :>=, to: :run
Expand Down
Loading