Skip to content

Commit

Permalink
Add cross_product shortcut
Browse files Browse the repository at this point in the history
  * Sugar over join with empty on-list
  * Checks that there are no duplicate attributes between the relations
  • Loading branch information
felixyz committed Mar 8, 2024
1 parent 8e99975 commit 6c5b1a5
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 1 deletion.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.21.4 - 2024-03-08

* Add `cross_product` (aliased to `cross_join`) as a shortcut for `join`
with no join attributes (`r.join(right)`). Checks that there are no
shared attributes between the two relations, and raises an error if
there are.

## 0.21.3 - 2024-01-18

* Add more output references to CSV and XLSX writers. It is now possible
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ r.allbut([:a, :b, ...]) # remove specified attributes
r.autowrap(split: '_') # structure a flat relation, split: '_' is the default
r.autosummarize([:a, :b, ...], x: :sum) # (experimental) usual summarizers supported
r.constants(x: 12, ...) # add constant attributes (sometimes useful in unions)
r.cross_product(right) # cross product, raises error if left and right have any common attributes (alias `cross_join`)
r.extend(x: ->(t){ ... }, ...) # add computed attributes
r.extend(x: :y) # shortcut for r.extend(x: ->(t){ t[:y] })
r.exclude(predicate) # shortcut for restrict(!predicate)
Expand Down
9 changes: 9 additions & 0 deletions lib/bmg/algebra/shortcuts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ def left_join(right, on = [], *args)
self.left_join(right.rename(renaming), on.keys, *args)
end

def cross_product(right)
duplicates = self.type.attrlist & right.type.attrlist
unless duplicates.empty?
raise TypeError, "Cross product incompatible — duplicate attribute(s): #{duplicates.join(', ')}"
end

return join(right)
end

def matching(right, on = [])
return super unless on.is_a?(Hash)
renaming = Hash[on.map{|k,v| [v,k] }]
Expand Down
56 changes: 56 additions & 0 deletions spec/unit/algebra/shortcuts/test_cross_product.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
require 'spec_helper'
module Bmg
module Algebra
describe Shortcuts, "cross_product" do

let(:left) {
Relation.new([
{ a: "foo", xyz: 1 },
{ a: "bar", xyz: 2 }
], Type::ANY.with_attrlist([:xyz, :b]))
}

let(:right) {
Relation.new([
{ c: "baz", d: 3 },
{ c: "zap", d: 4 }
], Type::ANY.with_attrlist([:c, :d]))
}

subject {
left.cross_product(right)
}

it 'compiles as expected' do
expect(subject).to be_a(Operator::Join)
expect(left_operand(subject)).to be(left)
expect(right_operand(subject)).to be(right)
expect(subject.send(:on)).to eql([])
end

it 'works as expected' do
expect(subject.to_set).to eql([
{ a: "bar", xyz: 2, c: "zap", d: 4 },
{ a: "foo", xyz: 1, c: "baz", d: 3 },
{ a: "foo", xyz: 1, c: "zap", d: 4 },
{ a: "bar", xyz: 2, c: "baz", d: 3 },
].to_set)
end

context 'with duplicate attributes' do
let(:right) {
Relation.new([
{ xyz: "baz", d: 3 },
{ xyz: "zap", d: 4 }
], Type::ANY.with_attrlist([:xyz, :d]))
}

it 'raises an error' do
expect{
left.cross_product(right)
}.to raise_error(Bmg::TypeError, /Cross product incompatible.*xyz/)
end
end
end
end
end
58 changes: 57 additions & 1 deletion spec/unit/operator/test_join.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,63 @@ module Operator
:variant => :left,
:default_right_tuple => { c: 10 }
}
expect(op.to_a). to eql(expected)
expect(op.to_a).to eql(expected)
end

context 'without join list an no overlapping attributes' do
let(:left) {
[
{ a: 1, b: 2, x: "a" },
{ a: 3, b: 4, x: "b" },
]
}

let(:right) {
[
{ q: 10, p: "80" },
{ q: 20, p: "90" },
]
}

it 'does a cross join' do
expected = [
{ a: 1, b: 2, x: "a", q: 10, p: "80"},
{ a: 1, b: 2, x: "a", q: 20, p: "90"},
{ a: 3, b: 4, x: "b", q: 10, p: "80"},
{ a: 3, b: 4, x: "b", q: 20, p: "90"},
].to_set

op = Join.new Type::ANY, left, right, []
expect(op.to_set).to eql(expected)
end
end

context 'without join list and shared attributes' do
let(:left) {
[
{ a: 1, b: 2, x: "a" },
{ a: 3, b: 4, x: "b" },
]
}

let(:right) {
[
{ a: 1, q: 10, p: "80" },
{ a: 5, q: 20, p: "90" },
]
}

it 'does a cross join, discaring the shared attribute from right' do
expected = [
{ a: 1, b: 2, x: "a", q: 10, p: "80"},
{ a: 1, b: 2, x: "a", q: 20, p: "90"},
{ a: 3, b: 4, x: "b", q: 10, p: "80"},
{ a: 3, b: 4, x: "b", q: 20, p: "90"},
].to_set

op = Join.new Type::ANY, left, right, []
expect(op.to_set).to eql(expected)
end
end

end
Expand Down

0 comments on commit 6c5b1a5

Please sign in to comment.