diff --git a/docs/docs/models-and-databases/queries.md b/docs/docs/models-and-databases/queries.md index dcad7cf8f..e29ca61f3 100644 --- a/docs/docs/models-and-databases/queries.md +++ b/docs/docs/models-and-databases/queries.md @@ -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. diff --git a/docs/docs/models-and-databases/raw-sql.md b/docs/docs/models-and-databases/raw-sql.md index fc0062ecd..1b5a886a0 100644 --- a/docs/docs/models-and-databases/raw-sql.md +++ b/docs/docs/models-and-databases/raw-sql.md @@ -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. diff --git a/spec/marten/db/model/querying_spec.cr b/spec/marten/db/model/querying_spec.cr index 0f8425109..b16f10809 100644 --- a/spec/marten/db/model/querying_spec.cr +++ b/spec/marten/db/model/querying_spec.cr @@ -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: "jd3@example.com", 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: "jd3@example.com", 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 @@ -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 diff --git a/spec/marten/db/query/set_spec.cr b/spec/marten/db/query/set_spec.cr index 7ec829a78..40cbee524 100644 --- a/spec/marten/db/query/set_spec.cr +++ b/spec/marten/db/query/set_spec.cr @@ -1882,6 +1882,132 @@ describe Marten::DB::Query::Set do qset.get(Marten::DB::Query::Node.new(name__startswith: "c")) end end + + it "does not allow getting a record using an empty raw SQL query", tags: "get_raw" do + expected_message = "Raw predicates cannot be empty" + + expect_raises(Marten::DB::Errors::UnmetQuerySetCondition, expected_message) do + Marten::DB::Query::Set(Tag).new.get("") + end + end + + it "gets a record using a raw SQL condition", tags: "get_raw" do + Tag.create!(name: "ruby", is_active: true) + tag_2 = Tag.create!(name: "crystal", is_active: true) + Tag.create!(name: "coding", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get("name='crystal'") + + result.should eq tag_2 + end + + it "gets a record using a raw SQL condition with one named parameter", tags: "get_raw" do + Tag.create!(name: "ruby", is_active: true) + tag_2 = Tag.create!(name: "crystal", is_active: true) + Tag.create!(name: "coding", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get("name=:name", name: "crystal") + + result.should eq tag_2 + end + + it "raises an error when getting with a misspelled column in a raw SQL condition", tags: "get_raw" do + Tag.create!(name: "crystal", is_active: true) + + expect_raises(Exception) do + Marten::DB::Query::Set(Tag).new.get("namme=:name", name: "crystal") + end + end + + it "gets a record using a raw negated SQL condition with one named parameter", tags: "get_raw" do + tag_1 = Tag.create!(name: "ruby", is_active: true) + Tag.create!(name: "crystal", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get("name!=:name", name: "crystal") + + result.should eq tag_1 + end + + it "gets a record using a raw negated SQL condition with a named tuple", tags: "get_raw" do + tag_1 = Tag.create!(name: "ruby", is_active: true) + Tag.create!(name: "crystal", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get("name!=:name", {name: "crystal"}) + + result.should eq tag_1 + end + + it "gets a record using a raw negated SQL condition with a hash", tags: "get_raw" do + tag_1 = Tag.create!(name: "ruby", is_active: true) + Tag.create!(name: "crystal", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get("name!=:name", {"name" => "crystal"}) + + result.should eq tag_1 + end + + it "gets a record using a raw SQL condition with an array as parameter", tags: "get_raw" do + Tag.create!(name: "ruby", is_active: true) + tag_2 = Tag.create!(name: "crystal", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get("name=?", ["crystal"]) + + result.should eq tag_2 + end + + it "gets a record using a raw SQL condition with one positional parameter", tags: "get_raw" do + Tag.create!(name: "ruby", is_active: true) + tag_2 = Tag.create!(name: "crystal", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get("name=?", "crystal") + + result.should eq tag_2 + end + + it "gets a record using a raw SQL condition with two positional parameters", tags: "get_raw" do + tag_1 = Tag.create!(name: "crystal", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get("name=? AND is_active=?", "crystal", true) + + result.should eq tag_1 + end + + it "raises an error if get receives an insufficient number of positional parameters", tags: "get_raw" do + expect_raises(Marten::DB::Errors::UnmetQuerySetCondition, "Wrong number of parameters provided for query") do + Marten::DB::Query::Set(Tag).new.get("name=? AND is_active=?", "crystal") + end + end + + it "raises an error if get receives too many positional parameters", tags: "get_raw" do + expect_raises(Marten::DB::Errors::UnmetQuerySetCondition, "Wrong number of parameters provided for query") do + Marten::DB::Query::Set(Tag).new.get("name=? AND is_active=?", "crystal", true, "extra") + end + end + + it "gets a record using a raw SQL condition with two named parameters", tags: "get_raw" do + Tag.create!(name: "ruby", is_active: true) + tag_2 = Tag.create!(name: "crystal", is_active: true) + Tag.create!(name: "coding", is_active: false) + + result = Marten::DB::Query::Set(Tag).new.get("name=:name AND is_active=:active", name: "crystal", active: true) + + result.should eq tag_2 + end + + it "raises an error when get is missing required named parameters", tags: "get_raw" do + expect_raises(Marten::DB::Errors::UnmetQuerySetCondition, "Missing parameter 'name' for query") do + Marten::DB::Query::Set(Tag).new.get("name=:name AND is_active=:active", active: true) + end + end + + it "gets a record using a combination of predicate and raw SQL in get", tags: "get_raw" do + tag_1 = Tag.create!(name: "ruby", is_active: true) + Tag.create!(name: "coding", is_active: false) + + result = Marten::DB::Query::Set(Tag).new.filter(name__startswith: "ru").get("is_active=?", true) + + result.should eq tag_1 + end end describe "#get!" do @@ -2017,6 +2143,95 @@ describe Marten::DB::Query::Set do qset.get!(Marten::DB::Query::Node.new(name__startswith: "c")) end end + + it "does not allow getting a record using an empty raw SQL query", tags: "get_raw" do + expected_message = "Raw predicates cannot be empty" + + expect_raises(Marten::DB::Errors::UnmetQuerySetCondition, expected_message) do + Marten::DB::Query::Set(Tag).new.get!("") + end + end + + it "raises an error if no matching record found with get! using a raw SQL condition", tags: "get_raw" do + Tag.create!(name: "ruby", is_active: true) + + expect_raises(Marten::DB::Errors::RecordNotFound) do + Marten::DB::Query::Set(Tag).new.get!("name = :name", name: "nonexistent") + end + end + + it "gets a record using a raw negated SQL condition with one named parameter", tags: "get_raw" do + tag_1 = Tag.create!(name: "ruby", is_active: true) + Tag.create!(name: "crystal", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get("name!=:name", name: "crystal") + + result.should eq tag_1 + end + + it "gets a record using a raw negated SQL condition with a named tuple", tags: "get_raw" do + tag_1 = Tag.create!(name: "ruby", is_active: true) + Tag.create!(name: "crystal", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get!("name!=:name", {name: "crystal"}) + + result.should eq tag_1 + end + + it "gets a record using a raw negated SQL condition with a hash", tags: "get_raw" do + tag_1 = Tag.create!(name: "ruby", is_active: true) + Tag.create!(name: "crystal", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get!("name!=:name", {"name" => "crystal"}) + + result.should eq tag_1 + end + + it "gets a record using a raw SQL condition with an array as parameter", tags: "get_raw" do + Tag.create!(name: "ruby", is_active: true) + tag_2 = Tag.create!(name: "crystal", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get!("name=?", ["crystal"]) + + result.should eq tag_2 + end + + it "gets a record using a raw SQL condition", tags: "get_raw" do + Tag.create!(name: "ruby", is_active: true) + tag_2 = Tag.create!(name: "crystal", is_active: true) + Tag.create!(name: "coding", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get!("name='crystal'") + + result.should eq tag_2 + end + + it "gets a record using a raw SQL condition with an array as parameter", tags: "get_raw" do + Tag.create!(name: "ruby", is_active: true) + tag_2 = Tag.create!(name: "crystal", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get!("name=?", ["crystal"]) + + result.should eq tag_2 + end + + it "gets a record using a raw SQL condition with one positional parameter", tags: "get_raw" do + Tag.create!(name: "ruby", is_active: true) + tag_2 = Tag.create!(name: "crystal", is_active: true) + + result = Marten::DB::Query::Set(Tag).new.get!("name=?", "crystal") + + result.should eq tag_2 + end + + it "gets a record using a raw SQL condition combined with a q expression in get!", tags: "get_raw" do + tag_2 = Tag.create!(name: "crystal", is_active: true) + Tag.create!(name: "coding", is_active: false) + + result = Marten::DB::Query::Set(Tag).new.get! { q("name=:name", name: "crystal") | q(is_active: true) } + + result.should eq tag_2 + end end describe "#get_or_create" do diff --git a/src/marten/db/model/querying.cr b/src/marten/db/model/querying.cr index d647e506a..3c7d80710 100644 --- a/src/marten/db/model/querying.cr +++ b/src/marten/db/model/querying.cr @@ -326,6 +326,61 @@ module Marten default_queryset.get(**kwargs) end + # Returns a single model instance matching the given raw SQL condition. + # Returns `nil` if no record matches. + # + # Example: + # ``` + # post = Post.get("is_published = true") + # ``` + def get(raw_predicate : String) + default_queryset.get(raw_predicate) + end + + # Returns a single model instance matching the given raw SQL condition with positional arguments. + # Returns `nil` if no record matches. + # + # Example: + # ``` + # post = Post.get("name = ?", "crystal") + # ``` + def get(raw_predicate : String, *args) + default_queryset.get(raw_predicate, *args) + end + + # Returns a single model instance matching the given raw SQL condition with positional parameters. + # Returns `nil` if no record matches. + # + # Example: + # ``` + # post = Post.get("name = ? AND is_published = ?", ["crystal", true]) + # ``` + def get(raw_predicate : String, params : Array) + default_queryset.get(raw_predicate, params) + end + + # Returns a single model instance matching the given raw SQL condition with named parameters. + # Returns `nil` if no record matches. + # + # Example: + # ``` + # post = Post.get("name = :name AND is_published = :published", name: "crystal", published: true) + # ``` + def get(raw_predicate : String, **kwargs) + default_queryset.get(raw_predicate, **kwargs) + end + + # Returns a single model instance matching the given raw SQL condition with a named parameters hash. + # Returns `nil` if no record matches. + # + # Example: + # ``` + # post = Post.get("name = :name", {name: "crystal"}) + # ``` + def get(raw_predicate : String, params : Hash | NamedTuple) + default_queryset.get(raw_predicate, params) + end + # Returns the model instance matching a specific set of advanced filters. # # Model fields such as primary keys or fields with a unique constraint should be used here in order to @@ -365,6 +420,61 @@ module Marten default_queryset.get!(**kwargs) end + # Returns a single model instance matching the given raw SQL condition. + # Raises a `RecordNotFound` exception if no record matches. + # + # Example: + # ``` + # post = Post.get!("is_published = true") + # ``` + def get!(raw_predicate : String) + default_queryset.get!(raw_predicate) + end + + # Returns a single model instance matching the given raw SQL condition with positional arguments. + # Raises a `RecordNotFound` exception if no record matches. + # + # Example: + # ``` + # post = Post.get!("name = ?", "crystal") + # ``` + def get!(raw_predicate : String, *args) + default_queryset.get!(raw_predicate, *args) + end + + # Returns a single model instance matching the given raw SQL condition with positional parameters. + # Raises a `RecordNotFound` exception if no record matches. + # + # Example: + # ``` + # post = Post.get!("name = ? AND is_published = ?", ["crystal", true]) + # ``` + def get!(raw_predicate : String, params : Array) + default_queryset.get!(raw_predicate, params) + end + + # Returns a single model instance matching the given raw SQL condition with named parameters. + # Raises a `RecordNotFound` exception if no record matches. + # + # Example: + # ``` + # post = Post.get!("name = :name AND is_published = :published", name: "crystal", published: true) + # ``` + def get!(raw_predicate : String, **kwargs) + default_queryset.get!(raw_predicate, **kwargs) + end + + # Returns a single model instance matching the given raw SQL condition with a named parameters hash. + # Raises a `RecordNotFound` exception if no record matches. + # + # Example: + # ``` + # post = Post.get!("name = :name", {name: "crystal"}) + # ``` + def get!(raw_predicate : String, params : Hash | NamedTuple) + default_queryset.get!(raw_predicate, params) + end + # Returns the model instance matching a specific set of advanced filters. # # Model fields such as primary keys or fields with a unique constraint should be used here in order to diff --git a/src/marten/db/query/set.cr b/src/marten/db/query/set.cr index ba3cd266e..eccce6463 100644 --- a/src/marten/db/query/set.cr +++ b/src/marten/db/query/set.cr @@ -704,6 +704,82 @@ module Marten nil end + # Returns the model instance matching the given raw SQL condition. + # + # This method allows retrieving a record based on a custom SQL condition without parameters. + # It returns `nil` if no record matches the condition. + # + # Example: + # ``` + # tag = Tag.all.get("is_active = true") + # ``` + def get(raw_predicate : String) + raise_empty_raw_predicate if raw_predicate.empty? + get(Node.new(raw_predicate)) + end + + # Returns the model instance matching the given raw SQL condition with positional arguments. + # + # This method allows retrieving a record based on a custom SQL condition with positional arguments. + # It returns `nil` if no record matches the condition. + # + # Example: + # ``` + # tag = Tag.all.get("name=?", "crystal") + # ``` + def get(raw_predicate : String, *args) + get(raw_predicate, args.to_a) + end + + # Returns the model instance matching the given raw SQL condition with positional parameters. + # + # This method allows retrieving a record based on a custom SQL condition using an array of parameters. + # It returns `nil` if no record matches the condition. + # + # Example: + # ``` + # tag = Tag.all.get("name=? AND is_active=?", ["crystal", true]) + # ``` + def get(raw_predicate : String, params : Array) + raise_empty_raw_predicate if raw_predicate.empty? + + raw_params = [] of ::DB::Any + raw_params += params + + get(Node.new(raw_predicate: raw_predicate, params: raw_params)) + end + + # Returns the model instance matching the given raw SQL condition with named parameters. + # + # This method allows retrieving a record based on a custom SQL condition using named parameters. + # It returns `nil` if no record matches the condition. + # + # Example: + # ``` + # tag = Tag.all.get("name=:name AND is_active=:active", name: "crystal", active: true) + # ``` + def get(raw_predicate : String, **kwargs) + get(raw_predicate, kwargs.to_h) + end + + # Returns the model instance matching the given raw SQL condition with a named parameters hash. + # + # This method allows retrieving a record based on a custom SQL condition using a hash of named parameters. + # It returns `nil` if no record matches the condition. + # + # Example: + # ``` + # tag = Tag.all.get("name=:name", {name: "crystal"}) + # ``` + def get(raw_predicate : String, params : Hash | NamedTuple) + raise_empty_raw_predicate if raw_predicate.empty? + + raw_params = {} of String => ::DB::Any + params.each { |k, v| raw_params[k.to_s] = v } + + get(Node.new(raw_predicate: raw_predicate, params: raw_params)) + end + # Returns the model instance matching the given set of filters. # # Model fields such as primary keys or fields with a unique constraint should be used here in order to retrieve @@ -754,6 +830,86 @@ module Marten raise Errors::MultipleRecordsFound.new("Multiple records (#{results.size}) found for get query") end + # Returns the model instance matching the given raw SQL condition, raising an error if not found. + # + # This method allows retrieving a record based on a custom SQL condition without parameters. + # If no record matches the condition, a `RecordNotFound` exception is raised. + # + # Example: + # ``` + # tag = Tag.all.get!("is_active = true") + # ``` + def get!(raw_predicate : String) + raise_empty_raw_predicate if raw_predicate.empty? + get!(Node.new(raw_predicate)) + end + + # Returns the model instance matching the given raw SQL condition with positional arguments, raising an + # error if not found. + # + # This method allows retrieving a record based on a custom SQL condition with positional arguments. + # If no record matches the condition, a `RecordNotFound` exception is raised. + # + # Example: + # ``` + # tag = Tag.all.get!("name=?", "crystal") + # ``` + def get!(raw_predicate : String, *args) + get!(raw_predicate, args.to_a) + end + + # Returns the model instance matching the given raw SQL condition with positional parameters, raising an + # error if not found. + # + # This method allows retrieving a record based on a custom SQL condition using an array of parameters. + # If no record matches the condition, a `RecordNotFound` exception is raised. + # + # Example: + # ``` + # tag = Tag.all.get!("name=? AND is_active=?", ["crystal", true]) + # ``` + def get!(raw_predicate : String, params : Array) + raise_empty_raw_predicate if raw_predicate.empty? + + raw_params = [] of ::DB::Any + raw_params += params + + get!(Node.new(raw_predicate: raw_predicate, params: raw_params)) + end + + # Returns the model instance matching the given raw SQL condition with named parameters, raising an + # error if not found. + # + # This method allows retrieving a record based on a custom SQL condition using named parameters. + # If no record matches the condition, a `RecordNotFound` exception is raised. + # + # Example: + # ``` + # tag = Tag.all.get!("name=:name AND is_active=:active", name: "crystal", active: true) + # ``` + def get!(raw_predicate : String, **kwargs) + get!(raw_predicate, kwargs.to_h) + end + + # Returns the model instance matching the given raw SQL condition with a named parameters hash, raising an + # error if not found. + # + # This method allows retrieving a record based on a custom SQL condition using a hash of named parameters. + # If no record matches the condition, a `RecordNotFound` exception is raised. + # + # Example: + # ``` + # tag = Tag.all.get!("name=:name", {name: "crystal"}) + # ``` + def get!(raw_predicate : String, params : Hash | NamedTuple) + raise_empty_raw_predicate if raw_predicate.empty? + + raw_params = {} of String => ::DB::Any + params.each { |k, v| raw_params[k.to_s] = v } + + get!(Node.new(raw_predicate: raw_predicate, params: raw_params)) + end + # Returns the model record matching the given set of filters or create a new one if no one is found. # # Model fields that uniquely identify a record should be used here. For example: