activefacts-compositions 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +6 -0
- data/activefacts-compositions.gemspec +29 -0
- data/bin/schema_compositor +49 -0
- data/lib/activefacts/compositions.rb +8 -0
- data/lib/activefacts/compositions/binary.rb +33 -0
- data/lib/activefacts/compositions/compositor.rb +182 -0
- data/lib/activefacts/compositions/relational.rb +406 -0
- data/lib/activefacts/compositions/version.rb +5 -0
- metadata +168 -0
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
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--format documentation
|
data/.travis.yml
ADDED
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,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,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
|
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: []
|