activefacts-compositions 1.9.5 → 1.9.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 61900437c96a5fa32c68fdcc195c8a681ace3cd9
4
- data.tar.gz: 9f4917265c5b36482552e74c554197e2fa390874
3
+ metadata.gz: 9dd27f8a8f6a05f883483391f31e2a026b9a52b4
4
+ data.tar.gz: cc0355b8f29a879c654d7e36e542a95045e71994
5
5
  SHA512:
6
- metadata.gz: c052e52630a0a814533cab84d1880bf9518968be13dde299d167514496c411acc4a5708c4c9bd35f0f670c0e15f28a5973ee0fc8b8868921f6df4b146bf5b8e4
7
- data.tar.gz: 7f475031a473c7f0ee566d33baaf49c9785cca3e4c2b142e16e0e176993ee387d64bf03e893debb3ee575e4f72afbd6aa652e059bdaec8fc644dea524cad71fe
6
+ metadata.gz: 2dc13c0b861580a23517fe1fe50f6dd4d8b88a42fb7ee15827119992c090172386efb984855a7ecf81f97a682e3439201f718bd1ae7fcb7aeeb3420317c86230
7
+ data.tar.gz: 45854d64978972f11839794e6c6fe0cd38a0e09c0c13f66081e9a0580cd4d01503350f144946dd22174b06530c0394b72632f5ac0fa660ba1ddf706e036bcb55
data/Gemfile CHANGED
@@ -2,9 +2,11 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- if ENV['PWD'] =~ %r{\A#{ENV['HOME']}/work}i
6
- $stderr.puts "Using work area gems for #{File.basename(File.dirname(__FILE__))} from activefacts-compositions"
7
- gem 'activefacts-api', path: '../api'
8
- gem 'activefacts-metamodel', path: '../metamodel'
9
- gem 'activefacts-cql', path: '../cql'
5
+ this_file = File.absolute_path(__FILE__)
6
+ if this_file =~ %r{\A#{ENV['HOME']}}i
7
+ dir = File.dirname(File.dirname(this_file))
8
+ $stderr.puts "Using work area gems in #{dir} from activefacts-compositions"
9
+ gem 'activefacts-api', path: dir+'/api'
10
+ gem 'activefacts-metamodel', path: dir+'/metamodel'
11
+ gem 'activefacts-cql', path: dir+'/cql'
10
12
  end
data/README.md CHANGED
@@ -2,16 +2,17 @@
2
2
 
3
3
  Fact-based schemas are always in highly normalised or *elementary* form.
4
4
  Most other schemas are composite (object-oriented, relational, warehousing, analytical, messaging, APIs, etc).
5
- This gem provides the framework for *Compositions*, which are representations of the two-way mapping between an elementary schema and a composite schema.
6
- As such, it supports any-to-any mappings between different composite forms.
7
5
 
8
- It also provides automated generators for some types of composite schemas, especially relational and Data Vault schemas.
6
+ A *Composition* is a representation of the two-way mapping between an elementary schema and the composite schemas.
9
7
 
10
- This gem works with the Fact Modeling tools as part of ActiveFacts.
8
+ This gem provides:
9
+ * an API for compositions,
10
+ * several compositors which create Compositions - O-O, Relational and Data Vault and
11
+ * some generators which emit various kinds of output (Ruby, SQL) etc, for composed schemas.
11
12
 
12
- ## Installation
13
+ This gem builds on the Fact Modeling Metamodel and languages of ActiveFacts.
13
14
 
14
- Install as part of activefacts, just "gem install" directly, or add this line to your application's Gemfile:
15
+ ## Installation
15
16
 
16
17
  ```ruby
17
18
  gem 'activefacts-compositions'
@@ -19,24 +20,18 @@ gem 'activefacts-compositions'
19
20
 
20
21
  And then execute:
21
22
 
22
- $ bundle
23
+ $ schema_compositor --help
23
24
 
24
25
  ## Usage
25
26
 
26
- This gem adds schema manipulation tools (mappers, composers, transformations) to the generator framework for *activefacts*.
27
- Refer to the afgen command-line tool for help:
28
-
29
- $ afgen --help
30
-
31
- A stand-alone relational generator program is provided, mostly for exploratory purposes; use tracing to see what it is doing, e.g.:
32
-
33
- $ TRACE=relational bin/schema_compositor --surrogate spec/relational/CompanyDirectorEmployee.cql
27
+ $ bin/schema_compositor --relational --sql spec/relational/CompanyDirectorEmployee.cql
28
+ $ bin/schema_compositor --binary --ruby spec/relational/CompanyDirectorEmployee.cql
34
29
 
35
30
  ## Development
36
31
 
37
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests.
32
+ After checking out the repo, run `bundle` to install dependencies. Then, run `rake rspec` to run the tests.
38
33
 
39
- To install this gem onto your local machine, run `bundle exec rake install`.
34
+ To install this gem onto your local machine from local source code, run `rake install`.
40
35
 
41
36
  ## Contributing
42
37
 
@@ -15,18 +15,19 @@ Gem::Specification.new do |spec|
15
15
  spec.license = "MIT"
16
16
 
17
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) }
18
+ spec.bindir = "bin"
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
20
  spec.require_paths = ["lib"]
21
21
 
22
22
  spec.add_development_dependency "bundler", ">= 1.10", "~> 1.10.6"
23
23
  spec.add_development_dependency "rake", "~> 10.0"
24
24
  spec.add_development_dependency "rspec", "~> 3.3"
25
25
 
26
- spec.add_runtime_dependency("activefacts-api", "~> 1", ">= 1.9.4")
27
- spec.add_runtime_dependency("activefacts-metamodel", "~> 1", ">= 1.9.5")
26
+ spec.add_development_dependency "activefacts", "~> 1", ">= 1.8"
27
+
28
+ spec.add_runtime_dependency("activefacts-api", "~> 1", ">= 1.9.5")
29
+ spec.add_runtime_dependency("activefacts-metamodel", "~> 1", ">= 1.9.6")
28
30
  spec.add_runtime_dependency "tracing", "~> 2", ">= 2.0.6"
29
31
 
30
- spec.add_development_dependency "activefacts", "~> 1", ">= 1.8"
31
- spec.add_development_dependency "activefacts-cql", "~> 1", ">= 1.8"
32
+ spec.add_runtime_dependency "activefacts-cql", "~> 1", ">= 1.8"
32
33
  end
@@ -1,6 +1,6 @@
1
1
  #! /usr/bin/env ruby
2
2
  #
3
- # ActiveFacts: Read a Vocabulary (from a NORMA, CQL or other file) and run a generator
3
+ # ActiveFacts: Read a model (CQL, ORM, etc), run a compositor, then a generator
4
4
  #
5
5
  # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
6
  #
@@ -8,90 +8,108 @@ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
8
8
  require 'bundler/setup' # Set up gems listed in the Gemfile.
9
9
  $:.unshift File.dirname(File.expand_path(__FILE__))+"/../lib"
10
10
 
11
+ require 'activefacts/loadable'
11
12
  require 'activefacts/metamodel'
12
- require 'activefacts/compositions/binary'
13
- require 'activefacts/compositions/relational'
14
- require 'activefacts/compositions/validator'
13
+ require 'activefacts/compositions'
14
+ require 'activefacts/generator'
15
15
 
16
+ # Parse options into a hash, and values for each option into a hash
16
17
  options = {}
17
- while arg = ARGV.shift and arg =~ /^-/
18
- option, value = arg.split(/=/, 2)
19
- options[option.sub(/^-*/,'')] = value =~ /,/ ? value.split(',') : (value || true)
18
+ while ARGV[0] =~ /^-/
19
+ option, value = ARGV.shift.split(/=/, 2)
20
+ options[option.sub(/^-*/,'')] =
21
+ (value =~ /,/ ? value.split(',') : Array(value)).
22
+ inject({}){|h,s| k, v = s.split(/=/, 2); h[k] = v || true; h }
20
23
  end
21
24
 
22
- validate = options.delete('validate')
23
- show = options.delete('show')
25
+ # Load and enumerate all available compositors:
26
+ compositions_path = "activefacts/compositions"
27
+ Loadable.new(compositions_path).
28
+ enumerate.
29
+ select do |filename|
30
+ begin
31
+ require(pathname = compositions_path+"/"+filename)
32
+ rescue LoadError => e
33
+ rescue Exception => e
34
+ puts "Can't load #{pathname}: #{e.class}: #{e.message} #{e.backtrace[0]}"
35
+ end
36
+ end
24
37
 
25
- # Load the file type input method
26
- if arg
27
- arg, input_options = *arg.split(/=/, 2)
28
- extension = arg.sub(/\A.*\./,'').downcase
29
- input_handler = "activefacts/input/#{extension}"
30
- require input_handler
38
+ # Load and enumerate all available generators
39
+ generators_path = "activefacts/generator"
40
+ Loadable.new(generators_path).
41
+ enumerate.
42
+ select do |filename|
43
+ begin
44
+ require(pathname = generators_path+"/"+filename)
45
+ rescue LoadError => e
46
+ rescue Exception => e
47
+ puts "Can't load #{pathname}: #{e.class}: #{e.message} #{e.backtrace[0]}"
48
+ end
49
+ end
31
50
 
32
- input_class = extension.upcase
33
- input_klass = ActiveFacts::Input.const_get(input_class.to_sym)
34
- raise "Expected #{input_handler} to define #{input_class}" unless input_klass
51
+ if options['help']
52
+ puts "Available compositors:\n\t#{ActiveFacts::Compositions.compositors.keys.sort*"\n\t"}\n\n"
53
+ puts "Available generators:\n\t#{ActiveFacts::Generators.generators.keys.sort*"\n\t"}\n\n"
54
+ exit
35
55
  end
36
56
 
37
- # Read the input file:
38
- begin
39
- if input_klass
40
- vocabulary = input_klass.readfile(arg, *input_options)
41
- else
42
- vocabulary = true
57
+ # Arrange the requested compositors and generators:
58
+ compositors = []
59
+ generators = []
60
+ options.clone.each do |option, mode|
61
+ if action = ActiveFacts::Compositions.compositors[option]
62
+ options.delete(option)
63
+ compositors << [action, mode]
64
+ elsif action = ActiveFacts::Generators.generators[option]
65
+ options.delete(option)
66
+ generators << [action, mode]
43
67
  end
68
+ if mode && mode['help']
69
+ puts "REVISIT: Help for #{option} is not yet available"
70
+ end
71
+ end
44
72
 
45
- exit 0 unless vocabulary
73
+ # Process each input file:
74
+ ARGV.each do |arg|
75
+ filename, input_options = *arg.split(/=/, 2)
46
76
 
47
- vocabulary.finalise unless vocabulary == true
77
+ # Load the correct file type input method
78
+ pathname, basename, extension = * /(?:(.*)[\/\\])?(.*)\.([^.]*)$/.match(filename).captures
79
+ input_handler = "activefacts/input/#{extension}"
80
+ require input_handler
48
81
 
49
- compositor = ActiveFacts::Compositions::Relational.new(vocabulary.constellation, "test", options)
50
- compositor.generate
82
+ input_class = extension.upcase
83
+ input_klass = ActiveFacts::Input.const_get(input_class.to_sym)
84
+ raise "Expected #{input_handler} to define #{input_class}" unless input_klass
51
85
 
52
- if validate
53
- trace.enable 'composition_validator'
54
- compositor.validate do |component, problem|
55
- trace :composition_validator, "!!PROBLEM!! #{component.inspect}: #{problem}"
86
+ # Read the input file:
87
+ vocabulary =
88
+ if input_klass
89
+ begin
90
+ input_klass.readfile(filename, *input_options)
91
+ rescue => e
92
+ $stderr.puts "#{e.message}"
93
+ if trace :exception
94
+ $stderr.puts "\t#{e.backtrace*"\n\t"}"
95
+ else
96
+ $stderr.puts "\t#{e.backtrace[0]}"
97
+ end
98
+ exit 1
99
+ end
56
100
  end
57
- end
58
-
59
- if show
60
- compositor.
61
- composition.
62
- all_composite.
63
- sort_by{|composite| composite.mapping.name}.
64
- each do |composite|
65
- puts composite.mapping.name
66
- indices = composite.all_indices_by_rank
101
+ exit 0 unless vocabulary
102
+ vocabulary.finalise unless vocabulary == true
67
103
 
68
- composite.mapping.leaves.each do |leaf|
69
- # Build a symbolic representation of the index participation of this leaf
70
- pos = 0
71
- indexing = indices.inject([]) do |a, index|
72
- pos += 1
73
- if part = index.position_in_index(leaf)
74
- a << "#{pos}.#{part}"
75
- end
76
- a
77
- end
104
+ # Run each compositor
105
+ compositors.each do |compositor_klass, mode|
106
+ compositor = compositor_klass.new(vocabulary.constellation, basename, mode||{})
107
+ compositor.generate
78
108
 
79
- puts "\t#{leaf.path.map{|component|
80
- if component.is_a?(ActiveFacts::Metamodel::Absorption) && component.foreign_key
81
- "[#{component.name}]"
82
- else
83
- component.name
84
- end +
85
- (component.is_a?(ActiveFacts::Metamodel::Absorption) && !component.parent_role.is_mandatory ? '?' : '')
86
- }*'->'}" +
87
- (indexing.empty? ? '' : "[#{indexing*','}]") # Show the indexing
88
- end
109
+ # Run each generator
110
+ generators.each do |generator, mode|
111
+ output = generator.new(compositor.composition, mode||{}).generate
112
+ puts output if output
89
113
  end
90
114
  end
91
-
92
- rescue => e
93
- $stderr.puts "#{e.message}"
94
- # puts "\t#{e.backtrace*"\n\t"}"
95
- $stderr.puts "\t#{e.backtrace*"\n\t"}" if trace :exception
96
- exit 1
97
115
  end
@@ -4,5 +4,12 @@ require "activefacts/compositions/compositor"
4
4
 
5
5
  module ActiveFacts
6
6
  module Compositions
7
+ def self.compositors
8
+ @@compositors ||= {}
9
+ end
10
+
11
+ def self.publish_compositor klass
12
+ compositors[klass.name.sub(/^ActiveFacts::Compositions::/,'').gsub(/::/, '/').downcase] = klass
13
+ end
7
14
  end
8
15
  end
@@ -1,4 +1,4 @@
1
- #
1
+ ### Composition
2
2
  # ActiveFacts Compositions, Binary Compositor.
3
3
  #
4
4
  # Fans of RDF will like this one.
@@ -17,6 +17,7 @@ module ActiveFacts
17
17
  @binary_mappings.keys.sort_by(&:name).each do |object_type|
18
18
  mapping = @binary_mappings[object_type]
19
19
  mapping.re_rank
20
+ composite = @constellation.Composite(mapping, composition: @composition)
20
21
  end
21
22
  end
22
23
 
@@ -29,5 +30,6 @@ module ActiveFacts
29
30
 
30
31
  end
31
32
  end
33
+ publish_compositor(Binary)
32
34
  end
33
35
  end
@@ -15,6 +15,8 @@ require "activefacts/metamodel"
15
15
 
16
16
  module ActiveFacts
17
17
  module Compositions
18
+ private
19
+ MM = ActiveFacts::Metamodel
18
20
  class Compositor
19
21
  attr_reader :options, :name, :composition
20
22
 
@@ -107,7 +109,7 @@ module ActiveFacts
107
109
  object_type.all_role.each do |role|
108
110
  # Exclude base roles in objectified fact types (unless unary); just use link fact types
109
111
  next if role.fact_type.entity_type && role.fact_type.all_role.size != 1
110
- next if role.variable_as_projection # REVISIT: Continue to ignore roles in derived fact types?
112
+ next if role.variable # REVISIT: Continue to ignore roles in derived fact types?
111
113
  populate_reference object_type, role
112
114
  end
113
115
  if object_type.is_a?(ActiveFacts::Metamodel::ValueType)
@@ -127,7 +129,8 @@ module ActiveFacts
127
129
  # if role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) && role == role.fact_type.subtype_role
128
130
  # return "Is "+role.object_type.name
129
131
  # end
130
- String::Words.new(role.base_role.preferred_reference.role_name(nil)).capwords*' '
132
+ role = role.base_role unless role.base_role.fact_type.all_role.size == 1
133
+ String::Words.new(role.preferred_reference.role_name(nil)).capwords*' '
131
134
  end
132
135
 
133
136
  def role_type role
@@ -0,0 +1,86 @@
1
+ #
2
+ # ActiveFacts Compositions, Metamodel aspect for Constraint classification
3
+ #
4
+ # Copyright (c) 2016 Clifford Heath. Read the LICENSE file.
5
+ #
6
+ require "activefacts/metamodel"
7
+
8
+ module ActiveFacts
9
+ module Metamodel
10
+ class Composition
11
+ def retract_constraint_classifications
12
+ all_composite.each(&:retract_constraint_classifications)
13
+ end
14
+
15
+ def classify_constraints
16
+ retract_constraint_classifications
17
+ all_composite.each(&:classify_constraints)
18
+ end
19
+ end
20
+
21
+ class Composite
22
+ def retract_constraint_classifications
23
+ all_spanning_constraint.to_a.each(&:retract)
24
+ all_local_constraint.to_a.each(&:retract)
25
+ mapping.leaves.each do |component|
26
+ component.all_leaf_constraint.to_a.each(&:retract)
27
+ end
28
+ end
29
+
30
+ def classify_constraints
31
+ leaves = mapping.leaves
32
+
33
+ # Categorise and index all constraints not already baked-in to the composition
34
+ all_composite_roles = []
35
+ all_composite_constraints = []
36
+ constraints_by_leaf = {}
37
+ leaves.each do |leaf|
38
+ all_composite_roles += leaf.path.flat_map(&:all_role) # May be non-unique, fix later
39
+ leaf.all_role.each do |role|
40
+ role.all_constraint.each do |constraint|
41
+ if constraint.is_a?(PresenceConstraint)
42
+ # Exclude single-role mandatory constraints and all uniqueness constraints:
43
+ if constraint.role_sequence.all_role_ref.size == 1 && constraint.min_frequency == 1 && constraint.is_mandatory or
44
+ constraint.max_frequency == 1
45
+ next
46
+ end
47
+ end
48
+ all_composite_constraints << constraint
49
+ (constraints_by_leaf[leaf] ||= []) << constraint
50
+ end
51
+ end
52
+ end
53
+
54
+ all_composite_roles.uniq!
55
+ all_composite_constraints.uniq!
56
+ spanning_constraints =
57
+ all_composite_constraints.reject do |constraint|
58
+ (constraint.all_constrained_role-all_composite_roles).size == 0
59
+ end
60
+ local_constraints = all_composite_constraints - spanning_constraints
61
+
62
+ spanning_constraints.each do |spanning_constraint|
63
+ constellation.SpanningConstraint(composite: self, spanning_constraint: spanning_constraint)
64
+ end
65
+
66
+ leaves.each do |leaf|
67
+ # Find any constraints that affect just this leaf:
68
+ leaf_constraints = (constraints_by_leaf[leaf]||[]).
69
+ reject do |constraint|
70
+ (constraint.all_constrained_role - leaf.all_role).size > 0
71
+ end
72
+ local_constraints -= leaf_constraints
73
+ leaf_constraints.each do |leaf_constraint|
74
+ constellation.LeafConstraint(component: leaf, leaf_constraint: leaf_constraint)
75
+ end
76
+ end
77
+
78
+ local_constraints.each do |local_constraint|
79
+ constellation.LocalConstraint(composite: self, local_constraint: local_constraint)
80
+ end
81
+
82
+ end
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,80 @@
1
+ #
2
+ # ActiveFacts Compositions, Metamodel aspect to build compacted column names for (leaf) Components
3
+ #
4
+ # Compresses the names arising from absorption paths into usable column names
5
+ #
6
+ # Copyright (c) 2016 Clifford Heath. Read the LICENSE file.
7
+ #
8
+ require "activefacts/compositions"
9
+
10
+ module ActiveFacts
11
+ module Metamodel
12
+ class Component
13
+ def column_name
14
+ column_path = path[1..-1]
15
+ prev_words = []
16
+ String::Words.new(
17
+ column_path.
18
+ inject([]) do |na, member|
19
+ is_absorption = member.is_a?(Absorption)
20
+ is_type_inheritance = is_absorption && member.parent_role.fact_type.is_a?(TypeInheritance)
21
+ fact_type = is_absorption && member.parent_role.fact_type
22
+
23
+ # If the parent object identifies the child via this absorption, skip it.
24
+ if member != column_path.first and
25
+ is_absorption and
26
+ !is_type_inheritance and
27
+ member.parent_role.base_role.is_identifying
28
+ trace :names, "Skipping #{member}, identifies non-initial object"
29
+ next na
30
+ end
31
+
32
+ words = member.name.words
33
+
34
+ if na.size > 0 && is_type_inheritance
35
+ # When traversing type inheritances, keep the subtype name, not the supertype names as well:
36
+ if member.child_role != fact_type.subtype_role
37
+ trace :names, "Skipping supertype #{member}"
38
+ next na
39
+ end
40
+ trace :names, "Eliding supertype in #{member}"
41
+ prev_words.size.times{na.pop}
42
+
43
+ elsif member.parent && member != column_path.first && is_absorption && member.child_role.base_role.is_identifying
44
+ # When Xyz is followed by identifying XyzID (even if we skipped the Xyz), truncate that to just ID
45
+ pnames = member.parent.name.words
46
+ if pnames == words[0, pnames.size]
47
+ pnames.size.times do
48
+ pnames.shift
49
+ words.shift
50
+ end
51
+ end
52
+ end
53
+
54
+ # If the reference is to the single identifying role of the object_type making the reference,
55
+ # strip the object_type name from the start of the reference role
56
+ if na.size > 0 and
57
+ is_absorption and
58
+ member.child_role.base_role.is_identifying and
59
+ (et = member.object_type).is_a?(EntityType) and
60
+ et.preferred_identifier.role_sequence.all_role_ref.size == 0 and
61
+ et.name.downcase == words[0][0...et.name.size].downcase
62
+ trace :columns, "truncating transitive identifying role #{words.inspect}"
63
+ words[0] = words[0][et.name.size..-1]
64
+ words.shift if words[0] == ''
65
+ end
66
+
67
+ prev_words = words
68
+ na += words.to_a
69
+ end.elide_repeated_subsequences do |a, b|
70
+ if a.is_a?(Array)
71
+ a.map{|e| e.downcase} == b.map{|e| e.downcase}
72
+ else
73
+ a.downcase == b.downcase
74
+ end
75
+ end
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end