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 +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: []
|