Skip to content

Loading…

Add nested relations #40

Merged
merged 17 commits into from

1 participant

@dkubb
Owner

This branch will add support for nested relations. It will add #nest and #unnest operators to Axiom::Relation. It should add an Axiom::Relation attribute type. It should also propagate materialization, insertion and deletion across nested relations.

  • Add Axiom::Attribute::Relation
  • Add Axiom::Relation::Operation::Nest and Axiom::Relation#nest
  • Add Axiom::Relation::Operation::Unnest and Axiom::Relation#unnest
  • Refactor Axiom::Relation::Nest and Axiom::Algebra::Join to use the same object to build the join index.
@dkubb dkubb was assigned
@dkubb
Owner

I will need to consider #wrap and #unwrap for the future too.

EDIT: These are for nested tuples, not nested relations like nest/unnest.

@dkubb dkubb merged commit f0a531d into master

1 check passed

Details default The Travis CI build passed
@dkubb dkubb deleted the nested-relation branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Showing with 779 additions and 51 deletions.
  1. +2 −1 .rspec
  2. +2 −3 .travis.yml
  3. +0 −2 TODO
  4. +1 −1 axiom.gemspec
  5. +1 −1 config/flay.yml
  6. +1 −1 config/flog.yml
  7. +1 −0 config/reek.yml
  8. +12 −5 lib/axiom.rb
  9. +4 −27 lib/axiom/algebra/join.rb
  10. +4 −4 lib/axiom/attribute/comparable.rb
  11. +64 −0 lib/axiom/attribute/relation.rb
  12. +91 −0 lib/axiom/relation/index.rb
  13. +96 −0 lib/axiom/relation/operation/nest.rb
  14. +82 −0 lib/axiom/relation/operation/unnest.rb
  15. +25 −1 lib/axiom/tuple.rb
  16. +13 −0 lib/axiom/types/relation.rb
  17. +4 −4 spec/spec_helper.rb
  18. +48 −0 spec/unit/axiom/attribute/relation/class_methods/new_spec.rb
  19. +13 −0 spec/unit/axiom/attribute/relation/class_methods/type_spec.rb
  20. +13 −0 spec/unit/axiom/attribute/relation/header_spec.rb
  21. +15 −0 spec/unit/axiom/attribute/relation/new_relation_spec.rb
  22. +1 −1 spec/unit/axiom/relation/header/context_spec.rb
  23. +33 −0 spec/unit/axiom/relation/index/each_spec.rb
  24. +17 −0 spec/unit/axiom/relation/index/element_reader_spec.rb
  25. +23 −0 spec/unit/axiom/relation/index/left_shift_operator_spec.rb
  26. +19 −0 spec/unit/axiom/relation/index/merge_spec.rb
  27. +33 −0 spec/unit/axiom/relation/operation/nest/each_spec.rb
  28. +22 −0 spec/unit/axiom/relation/operation/nest/header_spec.rb
  29. +25 −0 spec/unit/axiom/relation/operation/nest/methods/nest_spec.rb
  30. +33 −0 spec/unit/axiom/relation/operation/unnest/each_spec.rb
  31. +19 −0 spec/unit/axiom/relation/operation/unnest/header_spec.rb
  32. +21 −0 spec/unit/axiom/relation/operation/unnest/methods/unnest_spec.rb
  33. +14 −0 spec/unit/axiom/tuple/inspect_spec.rb
  34. +14 −0 spec/unit/axiom/tuple/to_hash_spec.rb
  35. +13 −0 spec/unit/axiom/types/relation/class_methods/primitive_spec.rb
View
3 .rspec
@@ -1,5 +1,6 @@
+--backtrace
--color
--format progress
+--order random
--profile
--warnings
---order random
View
5 .travis.yml
@@ -5,6 +5,7 @@ script: "bundle exec rake ci:metrics"
rvm:
- 1.9.3
- 2.0.0
+ - ruby-head
- rbx-19mode
matrix:
include:
@@ -13,10 +14,8 @@ matrix:
- rvm: jruby-head
env: JRUBY_OPTS="$JRUBY_OPTS --debug"
allow_failures:
- - rvm: 1.9.3 # mutant fails
- - rvm: 2.0.0 # mutant fails
+ - rvm: jruby-head # travis broken
- rvm: ruby-head # travis broken
- - rvm: rbx-19mode # mutant fails
notifications:
irc:
channels:
View
2 TODO
@@ -52,8 +52,6 @@
and instead wrap materialized relations in the Order object
if any.
-* Add Relation#group and Relation#ungroup
-
* Update Attributes so that constraints are modelled using predicates,
so that when "join-ability" is tested, the predicates can just be
compared for equality.
View
2 axiom.gemspec
@@ -10,7 +10,7 @@ Gem::Specification.new do |gem|
gem.description = 'Simplifies querying of structured data using relational algebra'
gem.summary = 'Ruby Relational Algebra'
gem.homepage = 'https://github.com/dkubb/axiom'
- gem.licenses = 'MIT'
+ gem.license = 'MIT'
gem.require_paths = %w[lib]
gem.files = `git ls-files`.split("\n")
View
2 config/flay.yml
@@ -1,3 +1,3 @@
---
threshold: 41
-total_score: 919
+total_score: 933
View
2 config/flog.yml
@@ -1,2 +1,2 @@
---
-threshold: 15.3
+threshold: 16.9
View
1 config/reek.yml
@@ -92,6 +92,7 @@ NestedIterators:
- Axiom::Equalizer#define_cmp_method
- Axiom::Equalizer#define_hash_method
- Axiom::Equalizer#define_inspect_method
+ - Axiom::Relation::Operation::Unnest#each
max_allowed_nesting: 1
ignore_iterators: []
NilCheck:
View
17 lib/axiom.rb
@@ -132,6 +132,7 @@ class ManyTuplesError < SetSizeError; end
require 'axiom/relation/keys'
require 'axiom/relation/header'
+require 'axiom/relation/index'
require 'axiom/relation/base'
require 'axiom/relation/variable'
@@ -142,13 +143,16 @@ class ManyTuplesError < SetSizeError; end
require 'axiom/relation/operation/unary'
require 'axiom/relation/operation/binary'
require 'axiom/relation/operation/combination'
-require 'axiom/relation/operation/set'
+
+require 'axiom/relation/operation/limit'
+require 'axiom/relation/operation/nest'
require 'axiom/relation/operation/offset'
require 'axiom/relation/operation/order'
require 'axiom/relation/operation/order/direction'
require 'axiom/relation/operation/order/direction_set'
-require 'axiom/relation/operation/limit'
require 'axiom/relation/operation/reverse'
+require 'axiom/relation/operation/set'
+require 'axiom/relation/operation/unnest'
require 'axiom/algebra/difference'
require 'axiom/algebra/extension'
@@ -167,11 +171,15 @@ class ManyTuplesError < SetSizeError; end
require 'axiom/relation/operation/deletion'
require 'axiom/relation/operation/insertion'
+require 'axiom-types'
+require 'axiom/types/relation'
+
require 'axiom/attribute/comparable'
require 'axiom/attribute'
require 'axiom/attribute/object'
require 'axiom/attribute/numeric'
+
require 'axiom/attribute/boolean'
require 'axiom/attribute/class'
require 'axiom/attribute/date'
@@ -179,8 +187,9 @@ class ManyTuplesError < SetSizeError; end
require 'axiom/attribute/decimal'
require 'axiom/attribute/float'
require 'axiom/attribute/integer'
-require 'axiom/attribute/time'
+require 'axiom/attribute/relation'
require 'axiom/attribute/string'
+require 'axiom/attribute/time'
require 'axiom/function/numeric'
@@ -200,8 +209,6 @@ class ManyTuplesError < SetSizeError; end
require 'axiom/tuple'
require 'axiom/version'
-require 'axiom-types'
-
module Axiom
# Represent a relation with an empty header and no tuples
View
31 lib/axiom/algebra/join.rb
@@ -47,12 +47,9 @@ def each(&block)
return to_enum unless block_given?
util = Relation::Operation::Combination
index = build_index
-
left.each do |left_tuple|
- right_tuples = index[join_tuple(left_tuple)]
- util.combine_tuples(header, left_tuple, right_tuples, &block)
+ util.combine_tuples(header, left_tuple, index[left_tuple], &block)
end
-
self
end
@@ -94,27 +91,7 @@ def delete(other)
#
# @api private
def build_index
- index = Hash.new { |hash, tuple| hash[tuple] = Set.new }
- right.each { |tuple| index[join_tuple(tuple)] << disjoint_tuple(tuple) }
- index
- end
-
- # Generate a tuple with the join attributes between relations
- #
- # @return [Tuple]
- #
- # @api private
- def join_tuple(tuple)
- tuple.project(join_header)
- end
-
- # Generate a tuple with the disjoint attributes between relations
- #
- # @return [Tuple]
- #
- # @api private
- def disjoint_tuple(tuple)
- tuple.project(@disjoint_header)
+ Index.new(join_header, @disjoint_header).merge(right)
end
# Insert the other relation into the left operand
@@ -189,9 +166,9 @@ module Methods
# @return [Join, Restriction]
#
# @api public
- def join(other)
+ def join(other, &block)
relation = Join.new(self, other)
- relation = relation.restrict { |context| yield context } if block_given?
+ relation = relation.restrict(&block) if block
relation
end
View
8 lib/axiom/attribute/comparable.rb
@@ -20,11 +20,11 @@ module Comparable
# @example
# ascending = attribute.asc
#
- # @return [Relation::Operation::Order::Ascending]
+ # @return [Axiom::Relation::Operation::Order::Ascending]
#
# @api public
def asc
- Relation::Operation::Order::Ascending.new(self)
+ Axiom::Relation::Operation::Order::Ascending.new(self)
end
# Sort the attribute in descending order
@@ -32,11 +32,11 @@ def asc
# @example
# descending = attribute.desc
#
- # @return [Relation::Operation::Order::Descending]
+ # @return [Axiom::Relation::Operation::Order::Descending]
#
# @api public
def desc
- Relation::Operation::Order::Descending.new(self)
+ Axiom::Relation::Operation::Order::Descending.new(self)
end
end # module Comparable
View
64 lib/axiom/attribute/relation.rb
@@ -0,0 +1,64 @@
+# encoding: utf-8
+
+module Axiom
+ class Attribute
+
+ # Represents a Relation value in a relation tuple
+ class Relation < Object
+ include Equalizer.new(:name, :header)
+
+ # The tuple header
+ #
+ # @return [Header]
+ #
+ # @api private
+ attr_reader :header
+
+ # Initialize a Relation Attribute
+ #
+ # @param [#to_sym] _name
+ # the attribute name
+ # @param [Hash] options
+ # the options for the attribute
+ # @option options [Boolean] :required (true)
+ # if true, then the value cannot be nil
+ # @option options [Header] :header
+ # the header for the relation
+ #
+ # @return [undefined]
+ #
+ # @api private
+ def initialize(_name, options = EMPTY_HASH)
+ super
+ @header = Axiom::Relation::Header.coerce(options.fetch(:header))
+ end
+
+ # The attribute type
+ #
+ # @example
+ # type = Axiom::Attribute::Relation.type # => Axiom::Types::Relation
+ #
+ # @return [Class<Types::Relation>]
+ #
+ # @api public
+ def self.type
+ Types::Relation
+ end
+
+ # Initialize a new relation with the tuples provided
+ #
+ # @example
+ # relation = attribute.new_relation(tuples)
+ #
+ # @param [Enumerable] tuples
+ #
+ # @return [Relation]
+ #
+ # @api public
+ def new_relation(tuples)
+ type.primitive.new(@header, tuples)
+ end
+
+ end # class Relation
+ end # class Attribute
+end # module Axiom
View
91 lib/axiom/relation/index.rb
@@ -0,0 +1,91 @@
+# encoding: utf-8
+
+module Axiom
+ class Relation
+
+ # Tuples keyed by a tuple
+ class Index
+
+ # Initialize an index
+ #
+ # @param [Header] key
+ # @param [Header] header
+ #
+ # @return [undefined]
+ #
+ # @api private
+ def initialize(key, header)
+ @key = key
+ @header = header
+ @index = Hash.new { |hash, tuple| hash[tuple] = Set.new }
+ end
+
+ # Add a set of tuples to the index
+ #
+ # @example
+ # index.merge(tuples)
+ #
+ # @param [Enumerable<Tuple>] tuples
+ #
+ # @return [Index]
+ #
+ # @api public
+ def merge(tuples)
+ tuples.each(&method(:<<))
+ self
+ end
+
+ # Add a tuple to the index
+ #
+ # @example
+ # index << tuple
+ #
+ # @param [Tuple]
+ #
+ # @return [Index]
+ #
+ # @api public
+ def <<(tuple)
+ self[tuple] << tuple.project(@header)
+ self
+ end
+
+ # Iterate over each entry in the index
+ #
+ # @example
+ # index = Index.new(key_header, tuple_header)
+ # index.each { |key, tuples| ... }
+ #
+ # @yield [key, tuples]
+ #
+ # @yieldparam [Tuple] key
+ # the key for the tuples
+ # @yieldparam [Set<Tuple>] tuples
+ # the indexed tuples
+ #
+ # @return [Index]
+ #
+ # @api public
+ def each(&block)
+ return to_enum unless block_given?
+ @index.each(&block)
+ self
+ end
+
+ # Return the tuples in the index based on the tuple key
+ #
+ # @example
+ # index[tuple] # => tuples
+ #
+ # @param [Tuple] tuple
+ #
+ # @return [Set<Tuple>]
+ #
+ # @api public
+ def [](tuple)
+ @index[tuple.project(@key)]
+ end
+
+ end # class Index
+ end # class Relation
+end # module Axiom
View
96 lib/axiom/relation/operation/nest.rb
@@ -0,0 +1,96 @@
+# encoding: utf-8
+
+module Axiom
+ class Relation
+ module Operation
+
+ # A class representing a nested relation
+ class Nest < Relation
+ include Unary
+ include Equalizer.new(:operand, :attribute)
+
+ # The nested attribute
+ #
+ # @return [Attribute::Relation]
+ #
+ # @api private
+ attr_reader :attribute
+
+ # Initialize a nested relation
+ #
+ # @param [Relation] operand
+ # @param [#to_sym] name
+ # @param [Enumerable<Axiom::Attribute>] attributes
+ #
+ # @return [undefined]
+ #
+ # @api private
+ def initialize(operand, name, attributes)
+ super(operand)
+ inner = header.project(attributes)
+ @outer = header - inner
+ @attribute = Attribute::Relation.new(name, header: inner)
+ @header = @outer.extend(attribute)
+ end
+
+ # Iterate over each tuple in the set
+ #
+ # @example
+ # nested = Nest.new(left, right)
+ # nested.each { |tuple| ... }
+ #
+ # @yield [tuple]
+ #
+ # @yieldparam [Tuple] tuple
+ # each tuple in the set
+ #
+ # @return [self]
+ #
+ # @api public
+ def each
+ return to_enum unless block_given?
+ build_index.each do |outer_tuple, inner_tuples|
+ yield outer_tuple.extend(
+ header,
+ [attribute.new_relation(inner_tuples)]
+ )
+ end
+ self
+ end
+
+ private
+
+ # Build an index using every tuple in the operand
+ #
+ # @return [Index]
+ #
+ # @api private
+ def build_index
+ Index.new(@outer, @attribute.header).merge(operand)
+ end
+
+ module Methods
+
+ # Return a nested relation
+ #
+ # @example
+ # nested = relation.nest(:location, [:latitude, :longitude])
+ #
+ # @param [#to_sym] name
+ # @param [Enumerable<Axiom::Attribute>] attributes
+ #
+ # @return [Nest]
+ #
+ # @api public
+ def nest(name, attributes)
+ Nest.new(self, name, attributes)
+ end
+
+ end # module Methods
+
+ Relation.class_eval { include Methods }
+
+ end # class Nest
+ end # module Operation
+ end # class Relation
+end # module Axiom
View
82 lib/axiom/relation/operation/unnest.rb
@@ -0,0 +1,82 @@
+# encoding: utf-8
+
+module Axiom
+ class Relation
+ module Operation
+
+ # A class representing a unnested relation
+ class Unnest < Relation
+ include Unary
+ include Equalizer.new(:operand, :attribute)
+
+ # The nested attribute
+ #
+ # @return [Attribute::Relation]
+ #
+ # @api private
+ attr_reader :attribute
+
+ # Initialize a unnested relation
+ #
+ # @param [Relation] operand
+ # @param [#to_sym] name
+ #
+ # @return [undefined]
+ #
+ # @api private
+ def initialize(operand, name)
+ super(operand)
+ @attribute = header[name]
+ @outer = header - [attribute]
+ @header = @outer.extend(attribute.header)
+ end
+
+ # Iterate over each tuple in the set
+ #
+ # @example
+ # unnested = Unnest.new(left, right)
+ # unnested.each { |tuple| ... }
+ #
+ # @yield [tuple]
+ #
+ # @yieldparam [Tuple] tuple
+ # each tuple in the set
+ #
+ # @return [self]
+ #
+ # @api public
+ def each
+ return to_enum unless block_given?
+ operand.each do |tuple|
+ outer_tuple = tuple.project(@outer)
+ tuple[attribute].each do |inner_tuple|
+ yield outer_tuple.extend(header, inner_tuple.to_ary)
+ end
+ end
+ self
+ end
+
+ module Methods
+
+ # Return a unnested relation
+ #
+ # @example
+ # unnested = relation.unnest(:location)
+ #
+ # @param [#to_sym] name
+ #
+ # @return [Unnest]
+ #
+ # @api public
+ def unnest(name)
+ Unnest.new(self, name)
+ end
+
+ end # module Methods
+
+ Relation.class_eval { include Methods }
+
+ end # class Unnest
+ end # module Operation
+ end # class Relation
+end # module Axiom
View
26 lib/axiom/tuple.rb
@@ -127,6 +127,30 @@ def to_ary
data.values_at(*header).freeze
end
+ # Coerce the tuple into a Hash
+ #
+ # @example
+ # tuple.to_hash # => data as a Hash
+ #
+ # @return [Hash{Symbol => Object}]
+ #
+ # @api public
+ def to_hash
+ Hash[data.map { |attribute, value| [attribute.name, value] }]
+ end
+
+ # Display the tuple data in a human readable form
+ #
+ # @example
+ # tuple.inspect # => data as a String
+ #
+ # @return [String]
+ #
+ # @api public
+ def inspect
+ to_hash.inspect
+ end
+
private
# Coerce an Array-like object into a Tuple
@@ -155,7 +179,7 @@ def self.coerce(header, object)
object.kind_of?(Tuple) ? object : new(header, object.to_ary)
end
- memoize :predicate, :to_ary
+ memoize :predicate, :to_ary, :to_hash, :inspect
end # class Tuple
end # module Axiom
View
13 lib/axiom/types/relation.rb
@@ -0,0 +1,13 @@
+# encoding: utf-8
+
+module Axiom
+ module Types
+
+ # Represents a relation type
+ class Relation < Object
+
+ primitive Axiom::Relation
+
+ end # class Relation
+ end # module Types
+end # module Axiom
View
8 spec/spec_helper.rb
@@ -44,13 +44,13 @@
# Record the original Type descendants
config.before do
- Axiom::Types.finalize
- @original_type_descendants = Axiom::Types::Type.descendants.dup
+ Types.finalize
+ @original_type_descendants = Types::Type.descendants.dup
end
# Reset the Type descendants
config.after do
- Axiom::Types::Type.descendants.replace(@original_type_descendants)
- Axiom::Types.instance_variable_get(:@inference_cache).clear
+ Types::Type.descendants.replace(@original_type_descendants)
+ Types.instance_variable_get(:@inference_cache).clear
end
end
View
48 spec/unit/axiom/attribute/relation/class_methods/new_spec.rb
@@ -0,0 +1,48 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Attribute::Relation, '.new' do
+ subject { object.new(name, options) }
+
+ let(:object) { described_class }
+ let(:name) { :ids }
+
+ context 'with a Header' do
+ let(:options) { { header: header } }
+ let(:header) { Relation::Header.coerce([[:id, Integer]]) }
+
+ it { should be_instance_of(described_class) }
+
+ it 'does not freeze the options' do
+ options.should_not be_frozen
+ expect { subject }.to_not change(options, :frozen?)
+ end
+
+ it 'sets the expected header' do
+ expect(subject.header).to be(header)
+ end
+ end
+
+ context 'with attributes' do
+ let(:options) { { header: header } }
+ let(:header) { [[:id, Integer]] }
+
+ it { should be_instance_of(described_class) }
+
+ it 'does not freeze the options' do
+ options.should_not be_frozen
+ expect { subject }.to_not change(options, :frozen?)
+ end
+
+ it 'sets the expected header' do
+ expect(subject.header).to eql(Relation::Header.coerce(header))
+ end
+ end
+
+ context 'without a Header' do
+ let(:options) { {} }
+
+ specify { expect { subject }.to raise_error }
+ end
+end
View
13 spec/unit/axiom/attribute/relation/class_methods/type_spec.rb
@@ -0,0 +1,13 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Attribute::Relation, '.type' do
+ subject { object.type }
+
+ let(:object) { described_class }
+
+ it_should_behave_like 'an idempotent method'
+
+ it { should be(Types::Relation) }
+end
View
13 spec/unit/axiom/attribute/relation/header_spec.rb
@@ -0,0 +1,13 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Attribute::Relation, '#header' do
+ subject { object.header }
+
+ let(:object) { described_class.new(:ids, options) }
+ let(:options) { { header: header } }
+ let(:header) { Relation::Header.coerce([[:id, Integer]]) }
+
+ it { should be(header) }
+end
View
15 spec/unit/axiom/attribute/relation/new_relation_spec.rb
@@ -0,0 +1,15 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Attribute::Relation, '#new_relation' do
+ subject { object.new_relation(tuples) }
+
+ let(:object) { described_class.new(:ids, header: header) }
+ let(:header) { [[:id, Integer]] }
+ let(:tuples) { [[1]] }
+
+ it 'returns the expected relation' do
+ expect(subject).to eql(Relation.new(header, tuples))
+ end
+end
View
2 spec/unit/axiom/relation/header/context_spec.rb
@@ -11,7 +11,7 @@
let(:context) { double('context') }
before do
- Axiom::Evaluator::Context.should_receive(:new).with(object)
+ Evaluator::Context.should_receive(:new).with(object)
.and_yield(context)
.and_return(context)
end
View
33 spec/unit/axiom/relation/index/each_spec.rb
@@ -0,0 +1,33 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Relation::Index, '#each' do
+ subject { described_class.new(key, header) << tuple }
+
+ let(:key) { Relation::Header.coerce([[:id, Integer]]) }
+ let(:header) { Relation::Header.coerce([[:id, Integer], [:name, String]]) }
+ let(:tuple) { Tuple.new(header, [1, 'John Doe']) }
+
+ context 'with a block' do
+ it 'returns an index' do
+ expect(subject.each { }).to be_instance_of(described_class)
+ end
+
+ it 'yields each key and tuples' do
+ expect { |block| subject.each(&block) }
+ .to yield_with_args([[1], Set[tuple]])
+ end
+ end
+
+ context 'without a block' do
+ it 'returns an enumerator' do
+ expect(subject.each).to be_instance_of(to_enum.class)
+ end
+
+ it 'yields each key and tuples' do
+ expect { |block| subject.each.each(&block) }
+ .to yield_with_args([[1], Set[tuple]])
+ end
+ end
+end
View
17 spec/unit/axiom/relation/index/element_reader_spec.rb
@@ -0,0 +1,17 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Relation::Index, '#[]' do
+ subject { object[tuple] }
+
+ let(:object) { described_class.new(key, header) }
+ let(:key) { Relation::Header.coerce([[:id, Integer]]) }
+ let(:header) { Relation::Header.coerce([[:id, Integer], [:name, String]]) }
+ let(:tuple) { Tuple.new(header, [1, 'John Doe']) }
+
+ it 'returns the tuples in the index' do
+ object << tuple
+ expect(object[tuple]).to eql(Set[tuple])
+ end
+end
View
23 spec/unit/axiom/relation/index/left_shift_operator_spec.rb
@@ -0,0 +1,23 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Relation::Index, '#<<' do
+ subject { object << tuple }
+
+ let(:object) { described_class.new(key, header.project(name_only)) }
+ let(:key) { Relation::Header.coerce([[:id, Integer]]) }
+ let(:header) { Relation::Header.coerce([[:id, Integer], [:name, String]]) }
+ let(:tuple) { Tuple.new(header, [1, 'John Doe']) }
+
+ let(:name_only) do
+ header.project([:name])
+ end
+
+ it_should_behave_like 'a command method'
+
+ it 'adds the tuple to the index' do
+ expect { subject }.to change { object[tuple] }
+ .from(Set[]).to(Set[tuple.project(name_only)])
+ end
+end
View
19 spec/unit/axiom/relation/index/merge_spec.rb
@@ -0,0 +1,19 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Relation::Index, '#merge' do
+ subject { object.merge(tuples) }
+
+ let(:object) { described_class.new(key, header) }
+ let(:key) { Relation::Header.coerce([[:id, Integer]]) }
+ let(:header) { Relation::Header.coerce([[:id, Integer], [:name, String]]) }
+ let(:tuples) { [tuple] }
+ let(:tuple) { Tuple.new(header, [1, 'John Doe']) }
+
+ it_should_behave_like 'a command method'
+
+ it 'add the tuples to the index' do
+ expect { subject }.to change { object[tuple] }.from(Set[]).to(Set[tuple])
+ end
+end
View
33 spec/unit/axiom/relation/operation/nest/each_spec.rb
@@ -0,0 +1,33 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Relation::Operation::Nest, '#each' do
+ subject { object }
+
+ let(:object) { described_class.new(relation, :names, [:name]) }
+ let(:relation) { Relation.new(header, body) }
+ let(:header) { [[:id, Integer], [:name, String]] }
+ let(:body) { [[1, 'John Doe']] }
+
+ it_should_behave_like 'an #each method'
+
+ it 'is a command method' do
+ expect(subject.each(&EMPTY_PROC)).to be(object)
+ end
+
+ it 'yields only tuples' do
+ expect { |block| subject.each(&block) }.to yield_with_args(Tuple)
+ end
+
+ it 'yields only tuples with the expected header' do
+ tuples = []
+ subject.each(&tuples.method(:<<))
+ expect(tuples.first.header).to be(object.header)
+ end
+
+ it 'yields only tuples with the expected data' do
+ expect { |block| subject.each(&block) }
+ .to yield_with_args([1, [['John Doe']]])
+ end
+end
View
22 spec/unit/axiom/relation/operation/nest/header_spec.rb
@@ -0,0 +1,22 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Relation::Operation::Nest, '#header' do
+ subject { object.header }
+
+ let(:object) { described_class.new(relation, :names, [:name]) }
+ let(:relation) { Relation.new(header, []) }
+ let(:header) { [[:id, Integer], [:name, String]] }
+
+ it_should_behave_like 'an idempotent method'
+
+ it { should be_instance_of(Relation::Header) }
+
+ it 'returns the expected attributes' do
+ should == [
+ Attribute::Integer.new(:id),
+ Attribute::Relation.new(:names, header: [[:name, String]]),
+ ]
+ end
+end
View
25 spec/unit/axiom/relation/operation/nest/methods/nest_spec.rb
@@ -0,0 +1,25 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Relation::Operation::Nest::Methods, '#nest' do
+ subject { object.nest(name, attributes) }
+
+ let(:object) { described_class.new(header, body) }
+ let(:described_class) { Relation }
+ let(:header) { [[:id, Integer], [:name, String]] }
+ let(:body) { [[1, 'John Doe']] }
+ let(:name) { :names }
+ let(:attributes) { [:name] }
+
+ it { should be_instance_of(Relation::Operation::Nest) }
+
+ its(:header) do
+ should == [
+ Attribute::Integer.new(:id),
+ Attribute::Relation.new(:names, header: [[:name, String]]),
+ ]
+ end
+
+ its(:to_a) { should eq([[1, [['John Doe']]]]) }
+end
View
33 spec/unit/axiom/relation/operation/unnest/each_spec.rb
@@ -0,0 +1,33 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Relation::Operation::Unnest, '#each' do
+ subject { object }
+
+ let(:object) { described_class.new(relation, :names) }
+ let(:relation) { Relation.new(header, body).nest(:names, [:name]) }
+ let(:header) { [[:id, Integer], [:name, String]] }
+ let(:body) { [[1, 'John Doe']] }
+
+ it_should_behave_like 'an #each method'
+
+ it 'is a command method' do
+ expect(subject.each(&EMPTY_PROC)).to be(object)
+ end
+
+ it 'yields only tuples' do
+ expect { |block| subject.each(&block) }.to yield_with_args(Tuple)
+ end
+
+ it 'yields only tuples with the expected header' do
+ tuples = []
+ subject.each(&tuples.method(:<<))
+ expect(tuples.first.header).to eq(header)
+ end
+
+ it 'yields only tuples with the expected data' do
+ expect { |block| subject.each(&block) }
+ .to yield_with_args([1, 'John Doe'])
+ end
+end
View
19 spec/unit/axiom/relation/operation/unnest/header_spec.rb
@@ -0,0 +1,19 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Relation::Operation::Unnest, '#header' do
+ subject { object.header }
+
+ let(:object) { described_class.new(relation, :names) }
+ let(:relation) { Relation.new(header, []).nest(:names, [:name]) }
+ let(:header) { [[:id, Integer], [:name, String]] }
+
+ it_should_behave_like 'an idempotent method'
+
+ it { should be_instance_of(Relation::Header) }
+
+ it 'returns the expected attributes' do
+ should == header
+ end
+end
View
21 spec/unit/axiom/relation/operation/unnest/methods/unnest_spec.rb
@@ -0,0 +1,21 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Relation::Operation::Unnest::Methods, '#unnest' do
+ subject { object.unnest(name) }
+
+ let(:object) { relation.nest(name, attributes) }
+ let(:described_class) { Relation }
+ let(:relation) { described_class.new(header, body) }
+ let(:header) { [[:id, Integer], [:name, String]] }
+ let(:body) { [[1, 'John Doe']] }
+ let(:name) { :names }
+ let(:attributes) { [:name] }
+
+ it { should be_instance_of(Relation::Operation::Unnest) }
+
+ its(:header) { should == header }
+
+ its(:to_a) { should eq([[1, 'John Doe']]) }
+end
View
14 spec/unit/axiom/tuple/inspect_spec.rb
@@ -0,0 +1,14 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Tuple, '#inspect' do
+ subject { object.inspect }
+
+ let(:header) { Relation::Header.coerce([[:id, Integer]]) }
+ let(:object) { described_class.new(header, [1]) }
+
+ it_should_behave_like 'an idempotent method'
+
+ it { should eql({ id: 1 }.inspect) }
+end
View
14 spec/unit/axiom/tuple/to_hash_spec.rb
@@ -0,0 +1,14 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Tuple, '#to_hash' do
+ subject { object.to_hash }
+
+ let(:header) { Relation::Header.coerce([[:id, Integer]]) }
+ let(:object) { described_class.new(header, [1]) }
+
+ it_should_behave_like 'an idempotent method'
+
+ it { should eql(id: 1) }
+end
View
13 spec/unit/axiom/types/relation/class_methods/primitive_spec.rb
@@ -0,0 +1,13 @@
+# encoding: utf-8
+
+require 'spec_helper'
+
+describe Types::Relation, '.primitive' do
+ subject { object.primitive }
+
+ let(:object) { described_class }
+
+ it_should_behave_like 'an idempotent method'
+
+ it { should be(Relation) }
+end
Something went wrong with that request. Please try again.