Skip to content

Commit

Permalink
#127 - Make it possible to combine query sets using binary operators
Browse files Browse the repository at this point in the history
  • Loading branch information
ellmetha committed May 22, 2024
1 parent 61635df commit fc6c889
Show file tree
Hide file tree
Showing 10 changed files with 733 additions and 31 deletions.
30 changes: 30 additions & 0 deletions docs/docs/models-and-databases/reference/query-set.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,36 @@ qset_2 = Article.all
qset_2[2..6]? # returns a "sliced" query set
```

### `&` (AND)

Combines the current query set with another one using the **AND** operator.

This method returns a new query set that is the result of combining the current query set with another one using the AND SQL operator.

For example:

```crystal
query_set_1 = Post.all.filter(title: "Test")
query_set_2 = Post.all.filter(is_published: true)
combined_query_set = query_set_1 & query_set_2
```

### `|` (OR)

Combines the current query set with another one using the **OR** operator.

This method returns a new query set that is the result of combining the current query set with another one using the OR SQL operator.

For example:

```crystal
query_set_1 = Post.all.filter(title: "Test")
query_set_2 = Post.all.filter(is_published: true)
combined_query_set = query_set_1 | query_set_2
```

### `all`

Allows retrieving all the records of a specific model. `#all` can be used as a class method from any model class, or it can be used as an instance method from any query set object. In this last case, calling `#all` returns a copy of the current query set.
Expand Down
90 changes: 90 additions & 0 deletions spec/marten/db/query/set_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,96 @@ describe Marten::DB::Query::Set do
end
end

describe "#&" do
it "combines two query sets using the AND operator" do
tag_1 = Tag.create!(name: "ruby", is_active: true)
Tag.create!(name: "rust", is_active: false)
Tag.create!(name: "crystal", is_active: true)

qset_1 = Tag.all.filter(name__startswith: "r")
qset_2 = Tag.all.filter(is_active: true)

combined = qset_1 & qset_2

combined.count.should eq 1
combined.to_a.should eq [tag_1]
end

it "returns the other query set if it is empty" do
Tag.create!(name: "ruby", is_active: true)
Tag.create!(name: "rust", is_active: false)
Tag.create!(name: "crystal", is_active: true)

qset_1 = Tag.all.filter(name__startswith: "r")
qset_2 = Tag.all.none

combined = qset_1 & qset_2

combined.should be qset_2
combined.exists?.should be_false
end

it "returns the current query set if it is empty" do
Tag.create!(name: "ruby", is_active: true)
Tag.create!(name: "rust", is_active: false)
Tag.create!(name: "crystal", is_active: true)

qset_1 = Tag.all.none
qset_2 = Tag.all.filter(name__startswith: "r")

combined = qset_1 & qset_2

combined.should be qset_1
combined.exists?.should be_false
end
end

describe "#|" do
it "combines two query sets using the AND operator" do
tag_1 = Tag.create!(name: "ruby", is_active: true)
Tag.create!(name: "go", is_active: false)
tag_3 = Tag.create!(name: "crystal", is_active: true)

qset_1 = Tag.all.filter(name__startswith: "r")
qset_2 = Tag.all.filter(name__startswith: "c")

combined = qset_1 | qset_2

combined.count.should eq 2
combined.to_set.should eq [tag_1, tag_3].to_set
end

it "returns the current query set if the other one is empty" do
tag_1 = Tag.create!(name: "ruby", is_active: true)
tag_2 = Tag.create!(name: "rust", is_active: true)
Tag.create!(name: "crystal", is_active: true)

qset_1 = Tag.all.filter(name__startswith: "r")
qset_2 = Tag.all.none

combined = qset_1 | qset_2

combined.should be qset_1
combined.count.should eq 2
combined.to_set.should eq [tag_1, tag_2].to_set
end

it "returns the other query set if the current one is empty" do
tag_1 = Tag.create!(name: "ruby", is_active: true)
tag_2 = Tag.create!(name: "rust", is_active: true)
Tag.create!(name: "crystal", is_active: true)

qset_1 = Tag.all.none
qset_2 = Tag.all.filter(name__startswith: "r")

combined = qset_1 | qset_2

combined.should be qset_2
combined.count.should eq 2
combined.to_set.should eq [tag_1, tag_2].to_set
end
end

describe "#accumulate" do
it "raises NotImplementedError" do
expect_raises(NotImplementedError) { Marten::DB::Query::Set(Tag).new.accumulate }
Expand Down
78 changes: 78 additions & 0 deletions spec/marten/db/query/sql/join_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,49 @@ describe Marten::DB::Query::SQL::Join do
end
end

describe "#clone" do
it "returns a deep copy of the join node and its children" do
parent_join = Marten::DB::Query::SQL::Join.new(
id: 1,
type: Marten::DB::Query::SQL::JoinType::INNER,
from_model: ShowcasedPost,
from_common_field: ShowcasedPost.get_field("post_id"),
reverse_relation: nil,
to_model: Post,
to_common_field: Post.get_field("id"),
selected: true,
)
child_join = Marten::DB::Query::SQL::Join.new(
id: 2,
type: Marten::DB::Query::SQL::JoinType::INNER,
from_model: Post,
from_common_field: Post.get_field("author_id"),
reverse_relation: nil,
to_model: TestUser,
to_common_field: TestUser.get_field("id"),
selected: true,
)

parent_join.add_child(child_join)

clone = parent_join.clone

clone.should_not eq parent_join
clone.from_model.should eq parent_join.from_model
clone.from_common_field.should eq parent_join.from_common_field
clone.to_model.should eq parent_join.to_model
clone.to_common_field.should eq parent_join.to_common_field
clone.selected?.should eq parent_join.selected?
clone.children.should_not eq parent_join.children
clone.children.first.should_not eq parent_join.children.first
clone.children.first.from_model.should eq parent_join.children.first.from_model
clone.children.first.from_common_field.should eq parent_join.children.first.from_common_field
clone.children.first.to_model.should eq parent_join.children.first.to_model
clone.children.first.to_common_field.should eq parent_join.children.first.to_common_field
clone.children.first.selected?.should eq parent_join.children.first.selected?
end
end

describe "#column_name" do
it "returns a valid column name with the table prefix" do
join = Marten::DB::Query::SQL::Join.new(
Expand Down Expand Up @@ -118,6 +161,41 @@ describe Marten::DB::Query::SQL::Join do
end
end

describe "#replace_table_alias_prefix" do
it "replaces the table alias prefix of the join and its children and returns a hash of the old and new aliases" do
parent_join = Marten::DB::Query::SQL::Join.new(
id: 1,
type: Marten::DB::Query::SQL::JoinType::INNER,
from_model: ShowcasedPost,
from_common_field: ShowcasedPost.get_field("post_id"),
reverse_relation: nil,
to_model: Post,
to_common_field: Post.get_field("id"),
selected: true,
table_alias_prefix: "p",
)
child_join = Marten::DB::Query::SQL::Join.new(
id: 2,
type: Marten::DB::Query::SQL::JoinType::INNER,
from_model: Post,
from_common_field: Post.get_field("author_id"),
reverse_relation: nil,
to_model: TestUser,
to_common_field: TestUser.get_field("id"),
selected: true,
table_alias_prefix: "p",
)

parent_join.add_child(child_join)

old_aliases = parent_join.replace_table_alias_prefix("t")

parent_join.table_alias.should eq "t1"
child_join.table_alias.should eq "t2"
old_aliases.should eq({"p1" => "t1", "p2" => "t2"})
end
end

describe "#table_alias" do
it "returns the alias of the table based on the join node ID" do
parent_join = Marten::DB::Query::SQL::Join.new(
Expand Down
28 changes: 28 additions & 0 deletions spec/marten/db/query/sql/predicate_node_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,34 @@ describe Marten::DB::Query::SQL::PredicateNode do
end
end

describe "#replace_table_alias_prefix" do
it "properly replaces the table alias prefix in the predicate node, its children, and predicates" do
predicate_1 = Marten::DB::Query::SQL::Predicate::Exact.new(Post.get_field("title"), "Foo", "t1")
predicate_2 = Marten::DB::Query::SQL::Predicate::Exact.new(Post.get_field("title"), "Bar", "t2")

node_1 = Marten::DB::Query::SQL::PredicateNode.new(
children: Array(Marten::DB::Query::SQL::PredicateNode).new,
connector: Marten::DB::Query::SQL::PredicateConnector::AND,
negated: false,
predicates: [predicate_1, predicate_2] of Marten::DB::Query::SQL::Predicate::Base
)

node_2 = Marten::DB::Query::SQL::PredicateNode.new(
children: [node_1],
connector: Marten::DB::Query::SQL::PredicateConnector::OR,
negated: true,
predicates: [predicate_1] of Marten::DB::Query::SQL::Predicate::Base
)

node_1.replace_table_alias_prefix({"t1" => "p1", "t2" => "p2"})

node_1.predicates[0].alias_prefix.should eq "p1"
node_1.predicates[1].alias_prefix.should eq "p2"

node_2.children[0].predicates[0].alias_prefix.should eq "p1"
end
end

describe "#to_sql" do
it "properly generates the expected SQL for a simple predicate with an AND connector" do
predicate_1 = Marten::DB::Query::SQL::Predicate::Exact.new(Post.get_field("title"), "Foo", "t1")
Expand Down
Loading

0 comments on commit fc6c889

Please sign in to comment.