Skip to content

Commit

Permalink
Add raw SQL queries to get and get! (#276)
Browse files Browse the repository at this point in the history
* Add raw SQL queries to `get` and `get!`

* Add testcases

* Add methods to model class

* Add documentation
  • Loading branch information
treagod authored Dec 7, 2024
1 parent 3e888df commit d3b7049
Show file tree
Hide file tree
Showing 6 changed files with 632 additions and 0 deletions.
7 changes: 7 additions & 0 deletions docs/docs/models-and-databases/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ It is also possible to chain a `#get` call on a query set that was already filte
Author.filter(first_name: "John").get(id: 42)
```

A record can also be retrived with a raw query, for example:

```crystal
Author.get("id=?", 42)
Author.get("id=:id", id: 42)
```

### Retrieving the first or last record

The `#first` and `#last` methods can be used to retrieve the first or last record for a given query set.
Expand Down
24 changes: 24 additions & 0 deletions docs/docs/models-and-databases/raw-sql.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,30 @@ Also, note that the parameters are left **unquoted** in the raw SQL queries: thi

Finally, it should be noted that Marten does not validate the SQL queries you specify to the [`#raw`](./reference/query-set.md#raw) query set method. It is the developer's responsibility to ensure that these queries are (i) valid and (ii) that they return records that correspond to the considered model.

## Fetching Single Records with `get` and `get!`

Marten allows you to fetch single records directly using raw SQL conditions with the `get` and `get!` methods. These methods provide an intuitive interface for retrieving individual records while maintaining the safety and flexibility of parameterized queries.

The `get` method retrieves a single record matching the raw SQL condition. It returns `nil` if no record matches the condition.

For example use `get` with positional parameters:

```crystal
article = Article.get("title = ? AND created_at > ?", "Hello World!", "2022-10-30")
```

The `get!` method is similar to get but raises an exception if no record is found.

For example use `get!` with named parameters:

```crystal
article = Article.get!(
"title = :title AND created_at > :created_at",
title: "Hello World!",
created_at: "2022-10-30"
)
```

## Filtering with raw SQL predicates

Marten provides a feature to filter query sets using raw SQL predicates within the `#filter` method. This is useful when you need more complex filtering logic than simple field comparisons but still want to leverage Marten's query building capabilities.
Expand Down
120 changes: 120 additions & 0 deletions spec/marten/db/model/querying_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,60 @@ describe Marten::DB::Model::Querying do
it "makes use of the default queryset when using a block defining an advanced predicates expression" do
Tag.get { q(name: "crystal") }.should be_nil
end

it "returns the object corresponding to the raw SQL predicate" do
user = TestUser.create!(username: "jd3", email: "[email protected]", first_name: "John", last_name: "Doe")
TestUser.get("username = 'jd3'").should eq user
end

it "returns the object corresponding to the raw SQL predicate with positional arguments" do
user = TestUser.create!(username: "jd3", email: "[email protected]", first_name: "John", last_name: "Doe")
TestUser.get("username = ?", "jd3").should eq user
end

it "returns nil if no record matches the raw SQL predicate with positional arguments" do
TestUser.get("username = ?", "unknown").should be_nil
end

it "returns the object when parameters are passed as an array" do
tag = Tag.create!(name: "elixir", is_active: true)
Tag.get("name = ? AND is_active = ?", ["elixir", true]).should eq tag
end

it "returns nil when no record matches and parameters are passed as an array" do
Tag.get("name = ? AND is_active = ?", ["nonexistent", true]).should be_nil
end

it "raises an error for an invalid SQL column in raw predicate" do
expect_raises(Exception) { TestUser.get("invalid_column = ?", "jd1") }
end

it "returns the object using a raw SQL predicate with named parameters" do
tag = Tag.create!(name: "custom", is_active: true)
Tag.get("name = :name AND is_active = :active", name: "custom", active: true).should eq tag
end

it "returns nil if no record matches the raw SQL predicate with named parameters" do
Tag.get("name = :name AND is_active = :active", name: "nonexistent", active: false).should be_nil
end

it "returns the object when parameters are passed as a named tuple" do
tag = Tag.create!(name: "rust", is_active: true)
Tag.get("name = :name AND is_active = :active", {name: "rust", active: true}).should eq tag
end

it "returns nil when no record matches and parameters are passed as a named tuple" do
Tag.get("name = :name AND is_active = :active", {name: "nonexistent", active: false}).should be_nil
end

it "returns the object when parameters are passed as a hash" do
tag = Tag.create!(name: "python", is_active: true)
Tag.get("name = :name AND is_active = :active", {"name" => "python", "active" => true}).should eq tag
end

it "returns nil when no record matches and parameters are passed as a hash" do
Tag.get("name = :name AND is_active = :active", {"name" => "nonexistent", "active" => false}).should be_nil
end
end

describe "::get!" do
Expand Down Expand Up @@ -477,6 +531,72 @@ describe Marten::DB::Model::Querying do
it "makes use of the default queryset when using a block defining an advanced predicates expression" do
expect_raises(Marten::DB::Errors::RecordNotFound) { Tag.get! { q(name: "crystal") } }
end

it "returns the object using a raw SQL predicate" do
tag = Tag.create!(name: "elixir", is_active: true)
Tag.get!("name = 'elixir' AND is_active = true").should eq tag
end

it "raises RecordNotFound when no record matches" do
expect_raises(Marten::DB::Errors::RecordNotFound) do
Tag.get!("name = 'nonexistent' AND is_active = true")
end
end

it "returns the object using a raw SQL predicate with positional arguments" do
tag = Tag.create!(name: "elixir", is_active: true)
Tag.get!("name = ? AND is_active = ?", "elixir", true).should eq tag
end

it "raises RecordNotFound when no record matches with positional arguments" do
expect_raises(Marten::DB::Errors::RecordNotFound) do
Tag.get!("name = ? AND is_active = ?", "nonexistent", true)
end
end

it "returns the object using a raw SQL predicate with named arguments" do
tag = Tag.create!(name: "python", is_active: true)
Tag.get!("name = :name AND is_active = :active", name: "python", active: true).should eq tag
end

it "raises RecordNotFound when no record matches with named arguments" do
expect_raises(Marten::DB::Errors::RecordNotFound) do
Tag.get!("name = :name AND is_active = :active", name: "nonexistent", active: false)
end
end

it "returns the object when parameters are passed as an array" do
tag = Tag.create!(name: "elixir", is_active: true)
Tag.get!("name = ? AND is_active = ?", ["elixir", true]).should eq tag
end

it "raises RecordNotFound when no record matches and parameters are passed as an array" do
expect_raises(Marten::DB::Errors::RecordNotFound) do
Tag.get!("name = ? AND is_active = ?", ["nonexistent", true])
end
end

it "returns the object when parameters are passed as a named tuple" do
tag = Tag.create!(name: "rust", is_active: true)
Tag.get!("name = :name AND is_active = :active", {name: "rust", active: true}).should eq tag
end

it "raises RecordNotFound when no record matches and parameters are passed as a named tuple" do
expect_raises(Marten::DB::Errors::RecordNotFound) do
Tag.get!("name = :name AND is_active = :active", {name: "nonexistent", active: false})
end
end

it "returns the object when parameters are passed as a hash" do
tag = Tag.create!(name: "rust", is_active: true)
Tag.get!("name = :name AND is_active = :active", {"name" => "rust", "active" => true}).should eq tag
end

it "raises RecordNotFound when no record matches and parameters are passed as a hash" do
expect_raises(Marten::DB::Errors::RecordNotFound) do
Tag.get!("name = :name AND is_active = :active", {"name" => "nonexistent", "active" => false})
end
end
end

describe "::get_or_create" do
Expand Down
Loading

0 comments on commit d3b7049

Please sign in to comment.