From d9dc75ff36233181ab8cef4623d6882fa9a520f8 Mon Sep 17 00:00:00 2001 From: Felix Date: Tue, 5 Mar 2024 18:03:31 +0100 Subject: [PATCH] Add cross_product shortcut * Sugar over join with empty on-list * Checks that there are no duplicate attributes between the relations --- lib/bmg/algebra/shortcuts.rb | 4 ++ lib/bmg/type.rb | 6 +++ .../algebra/shortcuts/test_cross_product.rb | 42 +++++++++++++++++++ spec/unit/type/test_type_check.rb | 7 ++++ 4 files changed, 59 insertions(+) create mode 100644 spec/unit/algebra/shortcuts/test_cross_product.rb diff --git a/lib/bmg/algebra/shortcuts.rb b/lib/bmg/algebra/shortcuts.rb index c7c5378..36ac87f 100644 --- a/lib/bmg/algebra/shortcuts.rb +++ b/lib/bmg/algebra/shortcuts.rb @@ -57,6 +57,10 @@ def left_join(right, on = [], *args) self.left_join(right.rename(renaming), on.keys, *args) end + def cross_product(right) + return join(right) + end + def matching(right, on = []) return super unless on.is_a?(Hash) renaming = Hash[on.map{|k,v| [v,k] }] diff --git a/lib/bmg/type.rb b/lib/bmg/type.rb index 1a00c78..51b44f3 100644 --- a/lib/bmg/type.rb +++ b/lib/bmg/type.rb @@ -323,6 +323,12 @@ def unknown_attributes!(attrs) end def join_compatible!(right, on) + if on.empty? # Cross product + duplicates = self.attrlist & right.attrlist + return if duplicates.empty? + raise TypeError, "Cross product incompatible - duplicate attribute(s): #{duplicates.join(', ')}" + end + extra = on - self.attrlist raise TypeError, "Unknow attributes #{extra.join(', ')}" unless extra.empty? if right.knows_attrlist? diff --git a/spec/unit/algebra/shortcuts/test_cross_product.rb b/spec/unit/algebra/shortcuts/test_cross_product.rb new file mode 100644 index 0000000..471d21c --- /dev/null +++ b/spec/unit/algebra/shortcuts/test_cross_product.rb @@ -0,0 +1,42 @@ +require 'spec_helper' +module Bmg + module Algebra + describe Shortcuts, "cross_product" do + + let(:left) { + Relation.new([ + { a: "foo", b: 1 }, + { a: "bar", b: 2 } + ], Type::ANY.with_attrlist([:a, :b])) + } + + let(:right) { + Relation.new([ + { c: "baz", d: 3 }, + { c: "zap", d: 4 } + ], Type::ANY.with_attrlist([:z, :c])) + } + + 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", b: 2, c: "zap", d: 4 }, + { a: "foo", b: 1, c: "baz", d: 3 }, + { a: "foo", b: 1, c: "zap", d: 4 }, + { a: "bar", b: 2, c: "baz", d: 3 }, + ].to_set) + end + + end + end +end diff --git a/spec/unit/type/test_type_check.rb b/spec/unit/type/test_type_check.rb index 98a95e5..90d96e3 100644 --- a/spec/unit/type/test_type_check.rb +++ b/spec/unit/type/test_type_check.rb @@ -120,6 +120,13 @@ module Bmg type.join(right_type, [:id]) }.to raise_error(Bmg::TypeError, /id/) end + + it 'detects duplicate attributes when on is empty' do + right_type = Type::ANY.with_attrlist([:name]) + expect{ + type.join(right_type, []) + }.to raise_error(Bmg::TypeError, /Cross product incompatible.*name/) + end end describe "matching typecheck" do