activefacts-compositions 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: af89821431e548908e46672ee76691aad15ebd4b
4
+ data.tar.gz: 7edd1f9827bac4270fcf8ee99fc91f84adbd4ba3
5
+ SHA512:
6
+ metadata.gz: f3747c23b121ab364f9b1fcdd543436df2a3a81c56f269059210eb66ba69041520f9aa26ae505cdd5ac7dbc7a19aa1e05421a3d15a039896f170d4c16a9a299e
7
+ data.tar.gz: e2ece71a45faf5ee7559b7f2171e52d00605f3df4d7f037358c0d29bb14691e47e37e1b14e9ea8eb53fc776cee6b8697e95114783adb84dd19ef2396a6166e13
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.swp
11
+ .DS_Store
12
+ *.rej
13
+ *.orig
14
+ *.diff
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.5
4
+ before_install: gem install bundler -v 1.10.0.rc
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ if ENV['PWD'] =~ %r{\A#{ENV['HOME']}/work}
6
+ $stderr.puts "Using work area gems for #{File.basename(File.dirname(__FILE__))} from activefacts-compositions"
7
+ gem 'activefacts-api', path: '/Users/cjh/work/activefacts/api'
8
+ gem 'activefacts-metamodel', path: '/Users/cjh/work/activefacts/metamodel'
9
+ gem 'activefacts-cql', path: '/Users/cjh/work/activefacts/cql'
10
+ # gem 'activefacts-metamodel', git: 'git://github.com/cjheath/activefacts-metamodel.git'
11
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Clifford Heath
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # ActiveFacts::Compositions
2
+
3
+ Create and represent composite schemas, schema transforms and data transforms over a fact-based model.
4
+
5
+ This gem works with the Fact Modeling tools as part of ActiveFacts.
6
+
7
+ ## Installation
8
+
9
+ Install via the activefacts gem bundle, or add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'activefacts-compositions'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install activefacts-compositions
22
+
23
+ ## Usage
24
+
25
+ This gem adds schema manipulation tools (mappers, composers, transformations, generators) to the generator framework for activefacts. Refer to the afgen command-line tool for help:
26
+
27
+ $ afgen --help
28
+
29
+ ## Development
30
+
31
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests.
32
+
33
+ To install this gem onto your local machine, run `bundle exec rake install`.
34
+
35
+ ## Contributing
36
+
37
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cjheath/activefacts-compositions.
38
+
39
+
40
+ ## License
41
+
42
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
43
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'activefacts/compositions/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "activefacts-compositions"
8
+ spec.version = ActiveFacts::Compositions::VERSION
9
+ spec.authors = ["Clifford Heath"]
10
+ spec.email = ["clifford.heath@gmail.com"]
11
+
12
+ spec.summary = %q{Create and represent composite schemas, schema transforms and data transforms over a fact-based model}
13
+ spec.description = %q{Create and represent composite schemas, schema transforms and data transforms over a fact-based model}
14
+ spec.homepage = "https://github.com/cjheath/activefacts-compositions"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", ">= 1.10", "~> 1.10.6"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec", "~> 3.3"
25
+
26
+ spec.add_runtime_dependency("activefacts-metamodel", ">= 1.8", "~> 1.8.3")
27
+ spec.add_development_dependency "activefacts", "~> 1.8", "~> 1.8.0"
28
+ spec.add_development_dependency "activefacts-cql", "~> 1.8", "~> 1.8.0"
29
+ end
@@ -0,0 +1,49 @@
1
+ #! /usr/bin/env ruby
2
+ #
3
+ # ActiveFacts: Read a Vocabulary (from a NORMA, CQL or other file) and run a generator
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
8
+ require 'bundler/setup' # Set up gems listed in the Gemfile.
9
+ $:.unshift File.dirname(File.expand_path(__FILE__))+"/../lib"
10
+
11
+ require 'activefacts/metamodel'
12
+ require 'activefacts/compositions/binary'
13
+ require 'activefacts/compositions/relational'
14
+
15
+ arg = ARGV.shift
16
+
17
+ # Load the file type input method
18
+ if arg
19
+ arg, *options = *arg.split(/=/)
20
+ extension = arg.sub(/\A.*\./,'').downcase
21
+ input_handler = "activefacts/input/#{extension}"
22
+ require input_handler
23
+
24
+ input_class = extension.upcase
25
+ input_klass = ActiveFacts::Input.const_get(input_class.to_sym)
26
+ raise "Expected #{input_handler} to define #{input_class}" unless input_klass
27
+ end
28
+
29
+ # Read the input file:
30
+ begin
31
+ if input_klass
32
+ vocabulary = input_klass.readfile(arg, *options)
33
+ else
34
+ vocabulary = true
35
+ end
36
+
37
+ exit 0 unless vocabulary
38
+
39
+ vocabulary.finalise unless vocabulary == true
40
+
41
+ compositor = ActiveFacts::Compositions::Relational.new(vocabulary.constellation, "test")
42
+ compositor.generate
43
+
44
+ rescue => e
45
+ $stderr.puts "#{e.message}"
46
+ # puts "\t#{e.backtrace*"\n\t"}"
47
+ $stderr.puts "\t#{e.backtrace*"\n\t"}" if trace :exception
48
+ exit 1
49
+ end
@@ -0,0 +1,8 @@
1
+ require "activefacts/metamodel"
2
+ require "activefacts/compositions/version"
3
+ require "activefacts/compositions/compositor"
4
+
5
+ module ActiveFacts
6
+ module Compositions
7
+ end
8
+ end
@@ -0,0 +1,33 @@
1
+ #
2
+ # ActiveFacts Compositions, Binary Compositor.
3
+ #
4
+ # Fans of RDF will like this one.
5
+ #
6
+ # Copyright (c) 2015 Clifford Heath. Read the LICENSE file.
7
+ #
8
+ require "activefacts/compositions"
9
+
10
+ module ActiveFacts
11
+ module Compositions
12
+ class Binary < Compositor
13
+ def generate
14
+ super
15
+
16
+ trace :binary_, "Constructing Binary Composition" do
17
+ @binary_mappings.keys.sort_by(&:name).each do |object_type|
18
+ mapping = @binary_mappings[object_type]
19
+ mapping.re_rank
20
+ end
21
+ end
22
+
23
+ trace :binary_, "Full binary composition" do
24
+ @binary_mappings.keys.sort_by(&:name).each do |object_type|
25
+ mapping = @binary_mappings[object_type]
26
+ mapping.show_trace
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,182 @@
1
+ #
2
+ # ActiveFacts Compositions, Fundamental Compositor
3
+ #
4
+ # All Compositors derive from this one, which can calculate the basic binary bi-directional mapping
5
+ #
6
+ # The term "reference" used here means either an Absorption
7
+ # (one direction of a binary fact type relating two object types),
8
+ # or an Indicator (for a unary fact type).
9
+ #
10
+ # n-ary fact types and other objectified fact types are factored out by using the associated LinkFactTypes.
11
+ #
12
+ # Copyright (c) 2015 Clifford Heath. Read the LICENSE file.
13
+ #
14
+ require "activefacts/metamodel"
15
+
16
+ module ActiveFacts
17
+ module Compositions
18
+ class Compositor
19
+ def initialize constellation, name, options = {}
20
+ @constellation = constellation
21
+ @name = name
22
+ @options = options
23
+ end
24
+
25
+ # Generate all Mappings into @binary_mappings for a binary composition of all ObjectTypes in this constellation
26
+ def generate
27
+ # Retract an existing composition by this name
28
+ if existing = @constellation.Name[[@name]] and
29
+ @composition = existing.composition
30
+ @composition.all_composite.to_a.each{|composite| composite.retract}
31
+ @composition.retract
32
+ end
33
+
34
+ @composition = @constellation.Composition(:new, :name => @name)
35
+ populate_references
36
+ end
37
+
38
+ private
39
+ def populate_reference object_type, role
40
+ parent = @binary_mappings[role.object_type]
41
+
42
+ return if role.fact_type.all_role.size > 2
43
+ if role.fact_type.all_role.size != 1
44
+ counterpart = role.counterpart
45
+ rt = role_type(counterpart)
46
+ if rt == :many_many
47
+ raise "Can't absorb many-to-many (until we absorb derived fact types, or don't require explicit objectification)"
48
+ end
49
+
50
+ a = @constellation.Absorption(
51
+ :new,
52
+ name: counterpart.name,
53
+ parent: parent,
54
+ object_type: counterpart.object_type,
55
+ parent_role: role,
56
+ child_role: counterpart
57
+ )
58
+ # Populate the absorption/reverse_absorption (putting the "many" or optional side as reverse)
59
+ if r = @component_by_fact[role.fact_type]
60
+ # Second occurrence of this fact type, set the direction:
61
+ if a.is_preferred_direction
62
+ a.absorption = r
63
+ else # Set this as the reverse absorption
64
+ a.reverse_absorption = r
65
+ end
66
+ else
67
+ # First occurrence of this fact type
68
+ @component_by_fact[role.fact_type] = a
69
+ end
70
+ else # It's an indicator
71
+ a = @constellation.Indicator(
72
+ :new,
73
+ name: role.name,
74
+ parent: parent,
75
+ role: role
76
+ )
77
+ @component_by_fact[role.fact_type] = a # For completeness, in case a subclass uses it
78
+ end
79
+ trace :binarize, "Populating #{a.inspect}"
80
+ end
81
+
82
+ def populate_references
83
+ # A table of Mappings by object type, with a default Mapping for each:
84
+ @binary_mappings = Hash.new do |h, object_type|
85
+ h[object_type] = @constellation.Mapping(
86
+ :new,
87
+ name: object_type.name,
88
+ object_type: object_type
89
+ )
90
+ end
91
+ @component_by_fact = {}
92
+
93
+ @constellation.ObjectType.each do |key, object_type|
94
+ trace :binarize, "Populating possible absorptions for #{object_type.name}" do
95
+ @binary_mappings[object_type] # Ensure we create the top Mapping even if it has no references
96
+
97
+ object_type.all_role.each do |role|
98
+ next if role.mirror_role_as_base_role # Exclude base roles, just use link fact types
99
+ next if role.variable_as_projection # REVISIT: Continue to ignore roles in derived fact types?
100
+ populate_reference object_type, role
101
+ end
102
+ if object_type.is_a?(ActiveFacts::Metamodel::ValueType)
103
+ # This requires a change in the metamodel to use TypeInheritance for ValueTypes
104
+ if object_type.supertype
105
+ trace :binarize, "REVISIT: Eliding supertype #{object_type.supertype.name} for #{object_type.name}"
106
+ end
107
+ object_type.all_value_type_as_supertype.each do |subtype|
108
+ trace :binarize, "REVISIT: Eliding subtype #{subtype.name} for #{object_type.name}"
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def role_type role
116
+ fact_type = role.fact_type
117
+ if fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
118
+ return role.object_type == fact_type.supertype ? :supertype : :subtype
119
+ end
120
+
121
+ return :unary if fact_type.all_role.size == 1
122
+
123
+ if fact_type.is_a?(ActiveFacts::Metamodel::LinkFactType)
124
+ # Prevent an unnecessary from-1 search:
125
+ from_1 = true
126
+ # Change the to_1 search to detect a one-to-one:
127
+ role = fact_type.implying_role
128
+ fact_type = role.fact_type
129
+ end
130
+
131
+ # List the UCs on this fact type:
132
+ all_uniqueness_constraints =
133
+ fact_type.all_role.map do |fact_role|
134
+ fact_role.all_role_ref.map do |rr|
135
+ rr.role_sequence.all_presence_constraint.select do |pc|
136
+ pc.max_frequency == 1
137
+ end
138
+ end
139
+ end.flatten.uniq
140
+
141
+ # It's to-1 if a UC exists over exactly this role:
142
+ to_1 =
143
+ all_uniqueness_constraints.
144
+ detect do |c|
145
+ (rr = c.role_sequence.all_role_ref.single) and
146
+ rr.role == role
147
+ end
148
+
149
+ if from_1 || fact_type.entity_type
150
+ # This is a role in an objectified fact type
151
+ from_1 = true
152
+ else
153
+ # It's from-1 if a UC exists over roles of this FT that doesn't cover this role:
154
+ from_1 = all_uniqueness_constraints.detect{|uc|
155
+ !uc.role_sequence.all_role_ref.detect{|rr| rr.role == role || rr.role.fact_type != fact_type}
156
+ }
157
+ end
158
+
159
+ if from_1
160
+ return to_1 ? :one_one : :one_many
161
+ else
162
+ return to_1 ? :many_one : :many_many
163
+ end
164
+ end
165
+
166
+ # Display the primitive binary mapping:
167
+ def show_references
168
+ trace :composition, "Displaying the mappings:" do
169
+ @binary_mappings.keys.sort_by(&:name).each do |object_type|
170
+ mapping = @binary_mappings[object_type]
171
+ trace :composition, "#{object_type.name}" do
172
+ mapping.all_member.each do |component|
173
+ trace :composition, component.inspect
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,406 @@
1
+ #
2
+ # ActiveFacts Compositions, Relational Compositor.
3
+ #
4
+ # Computes an Optimal Normal Form (close to 5NF) relational schema.
5
+ #
6
+ # Copyright (c) 2015 Clifford Heath. Read the LICENSE file.
7
+ #
8
+ require "activefacts/compositions"
9
+
10
+ module ActiveFacts
11
+ module Compositions
12
+ class Relational < Compositor
13
+ private
14
+ MM = ActiveFacts::Metamodel
15
+ public
16
+ def generate
17
+ trace :relational_mapping, "Generating relational composition" do
18
+ super
19
+
20
+ # Make a data structure to help in computing the tables
21
+ make_candidates
22
+
23
+ # Apply any obvious table/non-table factors
24
+ assign_default_tabulation
25
+
26
+ # Figure out how best to absorb things to reduce the number of tables
27
+ optimise_absorption
28
+
29
+ # Remove the un-used absorption paths
30
+ delete_reverse_absorptions
31
+
32
+ # Actually make a Composite object for each table:
33
+ make_composites
34
+
35
+ # If a value type has been mapped to a table, add a column to hold its value
36
+ inject_value_fields
37
+
38
+ # Traverse the absorbed objects to build the path to each required column, including foreign keys:
39
+ absorb_all_columns
40
+
41
+ # Remove mappings for objects we have absorbed
42
+ clean_unused_mappings
43
+
44
+ trace :relational_, "Full relational composition" do
45
+ @composition.all_composite.sort_by{|composite| composite.mapping.name}.each do |composite|
46
+ composite.mapping.show_trace
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ def make_candidates
53
+ @candidates = @binary_mappings.inject({}) do |hash, (absorption, mapping)|
54
+ hash[mapping.object_type] = Candidate.new(mapping)
55
+ hash
56
+ end
57
+ end
58
+
59
+ def assign_default_tabulation
60
+ trace :relational_mapping, "Preparing relational composition by setting default assumptions" do
61
+ @candidates.each do |object_type, candidate|
62
+ candidate.assign_default
63
+ end
64
+ end
65
+ end
66
+
67
+ def optimise_absorption
68
+ trace :relational_mapping, "Optimise Relational Composition" do
69
+ undecided = @candidates.keys.select{|object_type| @candidates[object_type].is_tentative}
70
+ pass = 0
71
+ finalised = []
72
+ begin
73
+ pass += 1
74
+ trace :relational_mapping, "Starting optimisation pass #{pass}" do
75
+ finalised = optimise_absorption_pass(undecided)
76
+ end
77
+ trace :relational_mapping, "Finalised #{finalised.size} on this pass: #{finalised.map{|f| f.name}*', '}"
78
+ undecided -= finalised
79
+ end while !finalised.empty?
80
+ end
81
+ end
82
+
83
+ def optimise_absorption_pass undecided
84
+ possible_flips = {}
85
+ undecided.select do |object_type|
86
+ candidate = @candidates[object_type]
87
+ trace :relational_mapping, "Considering possible status of #{object_type.name}" do
88
+
89
+ # Rule 1: Always absorb an objectified unary into its role player:
90
+ if (f = object_type.fact_type) && f.all_role.size == 1
91
+ trace :relational_mapping, "Absorb objectified unary #{object_type.name} into #{f.all_role.single.object_type.name}"
92
+ candidate.definitely_not_table
93
+ next object_type
94
+ end
95
+
96
+ # Rule 2: If the preferred_identifier contains one role only, played by an entity type that can absorb us, do that:
97
+ absorbing_ref = nil
98
+ pi_roles = []
99
+ if object_type.is_a?(MM::EntityType) and # We're an entity type
100
+ (pi_roles = object_type.preferred_identifier_roles).size == 1 and # Our PI has one role
101
+ pi_roles[0].object_type.is_a?(MM::EntityType) and # played by another Entity Type
102
+ candidate.references_from.detect do |absorption|
103
+ next unless absorption.is_a?(MM::Absorption)
104
+ next unless absorption.child_role == pi_roles[0] # Not the identifying absorption
105
+
106
+ # Look at the other end; make sure it's a forward absorption:
107
+ absorption = absorption.reverse_absorption ? absorption.reverse_absorption : absorption.flip!
108
+
109
+ next absorbing_ref = absorption
110
+ end
111
+ trace :relational_mapping, "#{object_type.name} is fully absorbed along its sole reference path #{absorbing_ref.inspect}"
112
+ candidate.definitely_not_table
113
+ next object_type
114
+ end
115
+
116
+ # Rule 3: If there's more than one absorption path and any functional dependencies that can't absorb us, it's a table
117
+ # REVISIT: If one of the absorption paths is our identifier, we can be absorbed into that with out dependencies, and the other absorption paths can just reference us there...
118
+ non_identifying_refs_from =
119
+ candidate.references_from.reject do |absorption|
120
+ absorption.is_a?(MM::Absorption) && pi_roles.include?(absorption.child_role.base_role)
121
+ end
122
+ trace :relational_mapping, "#{object_type.name} has #{non_identifying_refs_from.size} non-identifying functional roles" do
123
+ non_identifying_refs_from.each do |a|
124
+ trace :relational_mapping, a.inspect
125
+ end
126
+ end
127
+
128
+ trace :relational_mapping, "#{object_type.name} has #{candidate.references_to.size} references to it" do
129
+ candidate.references_to.each do |a|
130
+ trace :relational_mapping, a.inspect
131
+ end
132
+ end
133
+ if candidate.references_to.size > 1 and
134
+ non_identifying_refs_from.size > 0
135
+ trace :relational_mapping, "#{object_type.name} has #{non_identifying_refs_from.size} non-identifying functional dependencies and #{candidate.references_to.size} absorption paths so 3NF requires it be a table"
136
+ candidate.definitely_table
137
+ next object_type
138
+ end
139
+
140
+ # At this point, this object has no functional dependencies that would prevent its absorption
141
+ next false if !candidate.is_table # We can't reduce the number of tables by absorbing this one
142
+
143
+ absorption_paths =
144
+ ( non_identifying_refs_from + # But we should exclude any that are already involved in an absorption; pre-decided ET=>ET or supertype absorption!
145
+ candidate.references_to
146
+ ).reject do |a|
147
+ next true unless a.is_a?(MM::Absorption)
148
+ cc = @candidates[a.child_role.object_type]
149
+ next true if !cc.is_table
150
+ next true if !(a.child_role.is_unique && a.parent_role.is_unique)
151
+
152
+ # Allow the sole identifying role for this object
153
+ next false if pi_roles.size == 1 && pi_roles.include?(a.parent_role)
154
+ next true unless a.parent_role.is_mandatory
155
+ next true if cc.is_absorbed # REVISIT: We can be absorbed into something that's also absorbed, but not into us!
156
+ false
157
+ end
158
+
159
+ trace :relational_mapping, "#{object_type.name} has #{absorption_paths.size} absorption paths"
160
+
161
+ # Rule 4: If this object can be fully absorbed along non-identifying roles, do that (maybe flip some absorptions)
162
+ if absorption_paths.size > 0
163
+ trace :relational_mapping, "#{object_type.name} is fully absorbed in #{absorption_paths.size} places" do
164
+ absorption_paths.each do |a|
165
+ flip = a.reverse_absorption
166
+ a.flip! if flip
167
+ trace :relational_mapping, "#{object_type.name} is FULLY ABSORBED via #{a.inspect}#{flip ? ' (flipped)' : ''}"
168
+ end
169
+ end
170
+
171
+ candidate.definitely_not_table
172
+ candidate.is_absorbed = true
173
+ next object_type
174
+ end
175
+
176
+ # Rule 5: If this object has no functional dependencies, it can be fully absorbed (must be along an identifying role?)
177
+ if non_identifying_refs_from.size == 0
178
+ trace :relational_mapping, "#{object_type.name} is fully absorbed in #{candidate.references_to.size} places: #{candidate.references_to.map{|ref| ref.inspect}*", "}"
179
+ candidate.definitely_not_table
180
+ candidate.is_absorbed = true
181
+ next object_type
182
+ end
183
+
184
+ false # Otherwise we failed to make a decision about this object type
185
+ end
186
+ end
187
+ end
188
+
189
+ # Remove the unused reverse absorptions:
190
+ def delete_reverse_absorptions
191
+ @binary_mappings.each do |object_type, mapping|
192
+ mapping.all_member.to_a. # Avoid problems with deletion from all_member
193
+ each do |member|
194
+ next unless member.is_a?(MM::Absorption)
195
+ member.retract if member.reverse_absorption # This is the reverse of some absorption
196
+ end
197
+ mapping.re_rank
198
+ end
199
+ end
200
+
201
+ # Inject a ValueField for each value type that's a table:
202
+ def inject_value_fields
203
+ @constellation.Composite.each do |key, composite|
204
+ mapping = composite.mapping
205
+ if mapping.object_type.is_a?(MM::ValueType) and !mapping.all_member.detect{|m| m.is_a?(MM::ValueField)}
206
+ trace :relational_mapping, "Adding value field for #{mapping.object_type.name}"
207
+ @constellation.ValueField(
208
+ :new,
209
+ parent: mapping,
210
+ name: mapping.object_type.name+" Value",
211
+ object_type: mapping.object_type
212
+ )
213
+ mapping.re_rank
214
+ end
215
+ end
216
+ end
217
+
218
+ # After all table/non-table decisions are made, convert Mappings for tables into Composites and retract the rest:
219
+ def make_composites
220
+ @candidates.keys.to_a.each do |object_type|
221
+ candidate = @candidates[object_type]
222
+ mapping = candidate.mapping
223
+ if candidate.is_table
224
+ composite = @constellation.Composite(mapping, composition: @composition)
225
+ else
226
+ @candidates.delete(object_type)
227
+ end
228
+ end
229
+ end
230
+
231
+ def clean_unused_mappings
232
+ @candidates.keys.to_a.each do |object_type|
233
+ candidate = @candidates[object_type]
234
+ next if candidate.is_table
235
+ mapping = candidate.mapping
236
+ mapping.retract
237
+ @binary_mappings.delete(object_type)
238
+ end
239
+ end
240
+
241
+ # Absorb all items which aren't tables (and keys to those which are) recursively
242
+ def absorb_all_columns
243
+ trace :relational_mapping, "Absorbing full contents of all tables" do
244
+ @composition.all_composite_by_name.each do |composite|
245
+ trace :relational_mapping, "Absorbing contents of #{composite.mapping.name}" do
246
+ absorb_all composite.mapping, nil
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ def absorb_all mapping, from
253
+ (from||mapping).all_member.each do |member|
254
+ member = fork_component_to_new_parent mapping, member if from # Top-level members are already instantiated
255
+ if member.is_a?(MM::Absorption)
256
+ # Should we absorb a foreign key or the whole contents?
257
+ table = @candidates[member.child_role.object_type]
258
+ trace :relational_mapping, "Absorbing #{table ? 'key' : 'contents'} of #{member.child_role.name} in #{member.inspect_reading}" do
259
+ target = @binary_mappings[member.child_role.object_type]
260
+ if table
261
+ absorb_key member, target
262
+ else
263
+ absorb_all member, target
264
+ end
265
+ end
266
+ end
267
+ end
268
+ # mapping.re_rank
269
+ end
270
+
271
+ # Recursively add members to this component for the existential roles of
272
+ # the composite mapping for the absorbed (child_role) object:
273
+ def absorb_key mapping, target
274
+ target.all_member.each do |member|
275
+ next unless member.rank_key[0] == MM::Component::RANK_IDENT
276
+ member = fork_component_to_new_parent mapping, member
277
+ if member.is_a?(MM::Absorption)
278
+ absorb_key member, @binary_mappings[member.child_role.object_type]
279
+ end
280
+ end
281
+ # mapping.re_rank
282
+ end
283
+
284
+ def fork_component_to_new_parent parent, component
285
+ case component
286
+ # A place to put more special cases.
287
+ when MM::ValueField
288
+ # When we fork from a ValueField, we want to use the name of the ValueType, not the ValueField name
289
+ @constellation.fork component, guid: :new, parent: parent, name: component.object_type.name
290
+ else
291
+ @constellation.fork component, guid: :new, parent: parent
292
+ end
293
+ end
294
+
295
+ # A candidate is a Mapping of an object type which may become a Composition (a table, in relational-speak)
296
+ class Candidate
297
+ attr_reader :mapping, :is_table, :is_tentative
298
+ attr_accessor :is_absorbed
299
+
300
+ def initialize mapping
301
+ @mapping = mapping
302
+ end
303
+
304
+ def object_type
305
+ @mapping.object_type
306
+ end
307
+
308
+ # References from us are things we can absorb and have a forward absorption for
309
+ def references_from
310
+ # Anything that's not a Mapping must be an Absorption
311
+ @mapping.all_member.select{|m| !m.is_a?(MM::Mapping) or !m.reverse_absorption && m.parent_role.is_unique }
312
+ end
313
+ alias_method :rf, :references_from
314
+
315
+ # References to us are things that can absorb us and they have a forward absorption for
316
+ def references_to
317
+ @mapping.all_member.map{|m| m.is_a?(MM::Mapping) ? m.reverse_absorption : nil}.compact.select{|r| r.parent_role.is_unique }
318
+ end
319
+ alias_method :rt, :references_to
320
+
321
+ def has_references
322
+ @mapping.all_member.select{|m| m.is_a?(MM::Absorption) }
323
+ end
324
+
325
+ def definitely_not_table
326
+ @is_tentative = @is_table = false
327
+ end
328
+
329
+ def definitely_table
330
+ @is_tentative = false
331
+ @is_table = true
332
+ end
333
+
334
+ def probably_not_table
335
+ @is_tentative = true
336
+ @is_table = false
337
+ end
338
+
339
+ def probably_table
340
+ @is_tentative = @is_table = true
341
+ end
342
+
343
+ def assign_default
344
+ o = object_type
345
+ if o.is_separate
346
+ trace :relational_mapping, "#{o.name} is a table because it's declared independent or separate"
347
+ definitely_table
348
+ return
349
+ end
350
+
351
+ case o
352
+ when MM::ValueType
353
+ if o.is_auto_assigned
354
+ trace :relational_mapping, "#{o.name} is not a table because it is auto assigned"
355
+ definitely_not_table
356
+ elsif references_from.size > 0
357
+ trace :relational_mapping, "#{o.name} is a table because it has references to absorb"
358
+ definitely_table
359
+ else
360
+ trace :relational_mapping, "#{o.name} is not a table because it will be absorbed wherever needed"
361
+ definitely_not_table
362
+ end
363
+
364
+ when MM::EntityType
365
+ if references_to.empty? and
366
+ !references_from.detect do |absorption| # detect whether anything can absorb this entity type
367
+ absorption.is_a?(MM::Mapping) && absorption.parent_role.is_unique && absorption.child_role.is_unique
368
+ end
369
+ trace :relational_mapping, "#{o.name} is a table because it has nothing to absorb it"
370
+ definitely_table
371
+ return
372
+ end
373
+ if !o.supertypes.empty?
374
+ # We know that this entity type is not a separate or partitioned subtype, so a supertype that can absorb us does
375
+ identifying_fact_type = o.preferred_identifier.role_sequence.all_role_ref.to_a[0].role.fact_type
376
+ if identifying_fact_type.is_a?(MM::TypeInheritance)
377
+ trace :relational_mapping, "#{o.name} is absorbed into supertype #{identifying_fact_type.supertype_role.name}"
378
+ definitely_not_table
379
+ else
380
+ trace :relational_mapping, "Subtype #{o.name} is initially presumed to be a table"
381
+ probably_not_table
382
+ end
383
+ return
384
+ end # subtype
385
+
386
+ v = nil
387
+ if references_to.size > 1 and # Can be absorbed in more than one place
388
+ o.preferred_identifier.role_sequence.all_role_ref.detect do |rr|
389
+ (v = rr.role.object_type).is_a?(MM::ValueType) and v.is_auto_assigned
390
+ end
391
+ trace :relational_mapping, "#{o.name} must be a table to support its auto-assigned identifier #{v.name}"
392
+ definitely_table
393
+ return
394
+ end
395
+
396
+ trace :relational_mapping, "#{o.name} is initially presumed to be a table"
397
+ probably_table
398
+
399
+ end # case
400
+ end
401
+
402
+ end
403
+
404
+ end
405
+ end
406
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveFacts
2
+ module Compositions
3
+ VERSION = "1.9.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,168 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activefacts-compositions
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Clifford Heath
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-12-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ - - "~>"
21
+ - !ruby/object:Gem::Version
22
+ version: 1.10.6
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.10'
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 1.10.6
33
+ - !ruby/object:Gem::Dependency
34
+ name: rake
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '10.0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '10.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.3'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.3'
61
+ - !ruby/object:Gem::Dependency
62
+ name: activefacts-metamodel
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '1.8'
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: 1.8.3
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '1.8'
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: 1.8.3
81
+ - !ruby/object:Gem::Dependency
82
+ name: activefacts
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '1.8'
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: 1.8.0
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '1.8'
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: 1.8.0
101
+ - !ruby/object:Gem::Dependency
102
+ name: activefacts-cql
103
+ requirement: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - "~>"
106
+ - !ruby/object:Gem::Version
107
+ version: '1.8'
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 1.8.0
111
+ type: :development
112
+ prerelease: false
113
+ version_requirements: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.8'
118
+ - - "~>"
119
+ - !ruby/object:Gem::Version
120
+ version: 1.8.0
121
+ description: Create and represent composite schemas, schema transforms and data transforms
122
+ over a fact-based model
123
+ email:
124
+ - clifford.heath@gmail.com
125
+ executables: []
126
+ extensions: []
127
+ extra_rdoc_files: []
128
+ files:
129
+ - ".gitignore"
130
+ - ".rspec"
131
+ - ".travis.yml"
132
+ - Gemfile
133
+ - LICENSE.txt
134
+ - README.md
135
+ - Rakefile
136
+ - activefacts-compositions.gemspec
137
+ - bin/schema_compositor
138
+ - lib/activefacts/compositions.rb
139
+ - lib/activefacts/compositions/binary.rb
140
+ - lib/activefacts/compositions/compositor.rb
141
+ - lib/activefacts/compositions/relational.rb
142
+ - lib/activefacts/compositions/version.rb
143
+ homepage: https://github.com/cjheath/activefacts-compositions
144
+ licenses:
145
+ - MIT
146
+ metadata: {}
147
+ post_install_message:
148
+ rdoc_options: []
149
+ require_paths:
150
+ - lib
151
+ required_ruby_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ required_rubygems_version: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - ">="
159
+ - !ruby/object:Gem::Version
160
+ version: '0'
161
+ requirements: []
162
+ rubyforge_project:
163
+ rubygems_version: 2.2.2
164
+ signing_key:
165
+ specification_version: 4
166
+ summary: Create and represent composite schemas, schema transforms and data transforms
167
+ over a fact-based model
168
+ test_files: []