activefacts-compositions 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
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: []