Skip to content

Commit

Permalink
feat: Repository is non-generic
Browse files Browse the repository at this point in the history
Closes #3
  • Loading branch information
vladfaust committed Sep 29, 2017
1 parent 8b73b43 commit 65f1c7a
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 46 deletions.
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,26 +87,24 @@ end
db = DB.open(ENV["DATABASE_URL"])
query_logger = Core::QueryLogger.new(STDOUT)
user_repo = Core::Repository(User).new(db, query_logger)
post_repo = Core::Repository(Post).new(db, query_logger)
repo = Core::Repository.new(db, query_logger)
user = User.new(name: "Fo")
user.valid? # => false
user.errors # => [{:name => "length must be >= 3"}]
user.name = "Foo"
user.valid? # => true
user.id = user_repo.insert(user) # See ^1
user.id = repo.insert(user) # See ^1
# INSERT INTO users (name, created_at) VALUES ($1, $2) RETURNING id
post = Post.new(author: user, content: "Foo Bar")
post.id = post_repo.insert(post) # See ^1
post.id = repo.insert(post) # See ^1
# INSERT INTO posts (author_id, content, created_at) VALUES ($1, $2, $3) RETURNING id
alias Query = Core::Query
posts = post_repo.query(Query(Post).where(author: user))
posts = repo.query(Query(Post).where(author: user))
# SELECT * FROM posts WHERE author_id = $1
posts.first.content # => "Foo Bar"
Expand All @@ -116,7 +114,7 @@ query = Query(User)
.select(:*, :"COUNT(posts.id) AS posts_count")
.group_by(%i(users.id posts.id))
.one
user = user_repo.query(query).first
user = repo.query(query).first
# SELECT *, COUNT (posts.id) AS posts_count
# FROM users JOIN posts AS posts ON posts.author_id = users.id
# GROUP BY users.id, posts.id LIMIT 1
Expand All @@ -125,10 +123,10 @@ user.posts_count # => 1
user.name = "Bar"
user.changes # => {:name => "Bar"}
user_repo.update(user)
repo.update(user)
# UPDATE users SET name = $1 WHERE id = $2 RETURNING id
post_repo.delete(posts.first)
repo.delete(posts.first)
# DELETE FROM posts WHERE id = $1
```

Expand Down
31 changes: 15 additions & 16 deletions spec/repository_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ db = DB.open(ENV["DATABASE_URL"] || raise "No DATABASE_URL is set!")
query_logger = Core::QueryLogger.new(nil)

describe Repo do
user_repo = Repo(User).new(db, query_logger)
post_repo = Repo(Post).new(db, query_logger)
repo = Repo.new(db, query_logger)

user_created_at = uninitialized Time

describe "#insert" do
user = User.new(name: "Test User")
result = user_repo.insert(user)
result = repo.insert(user)
query = Query(User).last

it "sets created_at field" do
Expand All @@ -29,12 +28,12 @@ describe Repo do
user.id = db.scalar(query.select(:id).to_s).as(Int32)

post = Post.new(author: user, content: "Some content")
post_repo.insert(post).should be_truthy
repo.insert(post).should be_truthy
end

pending "returns fresh id" do
previous_id = db.scalar(query.select(:id).to_s).as(Int32)
user_repo.insert(user).should eq(previous_id + 1)
repo.insert(user).should eq(previous_id + 1)
end
end

Expand All @@ -46,7 +45,7 @@ describe Repo do
.order_by(:"users.id DESC")
.limit(1)

user = user_repo.query(complex_query).first
user = repo.query(complex_query).first

it "returns a valid instance" do
user.id.should be_a(Int32)
Expand All @@ -59,28 +58,28 @@ describe Repo do

pending "handles DB errors" do
expect_raises do
user_repo.query("INVALID QUERY")
repo.query("INVALID QUERY")
end
end
end

describe "#update" do
user = user_repo.query(Query(User).last).first
user = repo.query(Query(User).last).first

it "ignores empty changes" do
user_repo.update(user).should eq nil
repo.update(user).should eq nil
end

pending "handles DB errors" do
user.id = nil
expect_raises do
user_repo.update(user)
repo.update(user)
end
end

user.name = "Updated User"
update = user_repo.update(user)
updated_user = user_repo.query(Query(User).last).first
update = repo.update(user)
updated_user = repo.query(Query(User).last).first

it "actually updates" do
updated_user.name.should eq "Updated User"
Expand All @@ -92,13 +91,13 @@ describe Repo do
end

describe "#delete" do
post = post_repo.query(Query(Post).last).first
post = repo.query(Query(Post).last).first
post_id = post.id
delete = post_repo.delete(post)
delete = repo.delete(post)

it do
delete.should be_truthy
post_repo.query(Query(Post).where(id: post_id)).empty?.should eq true
repo.query(Query(Post).where(id: post_id)).empty?.should eq true
end

pending "returns an amount of affected rows" do
Expand All @@ -108,7 +107,7 @@ describe Repo do
pending "handles DB errors" do
# It's already deleted, so
expect_raises do
post_repo.delete(post)
repo.delete(post)
end
end
end
Expand Down
52 changes: 31 additions & 21 deletions src/core/repository.cr
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require "./query_logger"
#
# ```
# logger = Core::QueryLogger.new(STDOUT)
# repo = Core::Repository(User).new(db, logger)
# repo = Core::Repository.new(db, logger)
#
# user = User.new(name: "Foo")
# repo.insert(user) # TODO: [RFC] Return last inserted ID
Expand All @@ -30,7 +30,7 @@ require "./query_logger"
# # DELETE FROM users WHERE id = $1
# # 1.628ms
# ```
class Core::Repository(ModelType)
class Core::Repository
# :nodoc:
property db
# :nodoc:
Expand All @@ -44,23 +44,33 @@ class Core::Repository(ModelType)
def initialize(@db : DB::Database, @query_logger : QueryLogger)
end

# Issue a query to the `#db`. Returns an array of `Model` instances.
# Query `#db` returning an array of *model* instances.
#
# ```
# repo.query(User, "SELECT * FROM users") # => Array(User)
# ```
#
# TODO: Handle errors (PQ::PQError)
def query(query : String, *params) : Array(ModelType)
def query(model : Model.class, query : String, *params) : Array
query = prepare_query(query)
params = prepare_params(*params) if params.any?

query_logger.wrap(query) do
db.query_all(query, *params) do |rs|
rs.read(ModelType)
rs.read(model)
end
end
end

# ditto
def query(query : Query)
query(query.to_s, query.params)
# Query `#db` returning an array of model instances inherited from *query*.
#
# ```
# repo.query(Query(User).all) # => Array(User)
# ```
#
# TODO: Handle errors (PQ::PQError)
def query(query : Query(T)) forall T
query(T, query.to_s, query.params)
end

private SQL_INSERT = <<-SQL
Expand All @@ -75,19 +85,19 @@ class Core::Repository(ModelType)
# TODO: Handle errors.
# TODO: Multiple inserts.
# TODO: [RFC] Call `#query` and return `Model` instance instead (see https://github.com/will/crystal-pg/issues/101).
def insert(instance : ModelType) : Int64
def insert(instance : Model) : Int64
fields = instance.db_fields.dup.tap do |f|
f.each do |k, _|
f[k] = now if ModelType.created_at_fields.includes?(k) && f[k].nil?
f.delete(k) if k == ModelType.primary_key
f[k] = now if instance.class.created_at_fields.includes?(k) && f[k].nil?
f.delete(k) if k == instance.class.primary_key
end
end

query = SQL_INSERT % {
table_name: ModelType.table_name,
table_name: instance.class.table_name,
keys: fields.keys.join(", "),
values: (1..fields.size).map { "?" }.join(", "),
returning: ModelType.primary_key,
returning: instance.class.primary_key,
}

query = prepare_query(query)
Expand All @@ -111,22 +121,22 @@ class Core::Repository(ModelType)
# TODO: Handle errors.
# TODO: Multiple updates.
# TODO: [RFC] Call `#query` and return `Model` instance instead (see https://github.com/will/crystal-pg/issues/101).
def update(instance : ModelType)
def update(instance : Model)
fields = instance.db_fields.select do |k, _|
instance.changes.keys.includes?(k)
end.tap do |f|
f.each do |k, _|
f[k] = now if ModelType.updated_at_fields.includes?(k)
f[k] = now if instance.class.updated_at_fields.includes?(k)
end
end

return unless fields.any?

query = SQL_UPDATE % {
table_name: ModelType.table_name,
table_name: instance.class.table_name,
set_fields: fields.keys.map { |k| k.to_s + " = ?" }.join(", "),
primary_key: ModelType.primary_key, # TODO: Handle empty primary key
returning: ModelType.primary_key,
primary_key: instance.class.primary_key, # TODO: Handle empty primary key
returning: instance.class.primary_key,
}

query = prepare_query(query)
Expand All @@ -146,10 +156,10 @@ class Core::Repository(ModelType)
#
# TODO: Handle errors.
# TODO: Multiple deletes.
def delete(instance : ModelType)
def delete(instance : Model)
query = SQL_DELETE % {
table_name: ModelType.table_name,
primary_key: ModelType.primary_key,
table_name: instance.class.table_name,
primary_key: instance.class.primary_key,
}

query = prepare_query(query)
Expand Down

0 comments on commit 65f1c7a

Please sign in to comment.