activefacts-generators 1.7.1

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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +30 -0
  8. data/Rakefile +6 -0
  9. data/activefacts-generators.gemspec +26 -0
  10. data/lib/activefacts/dependency_analyser.rb +182 -0
  11. data/lib/activefacts/generators/absorption.rb +71 -0
  12. data/lib/activefacts/generators/composition.rb +119 -0
  13. data/lib/activefacts/generators/cql.rb +715 -0
  14. data/lib/activefacts/generators/diagrams/json.rb +340 -0
  15. data/lib/activefacts/generators/help.rb +64 -0
  16. data/lib/activefacts/generators/helpers/inject.rb +16 -0
  17. data/lib/activefacts/generators/helpers/oo.rb +162 -0
  18. data/lib/activefacts/generators/helpers/ordered.rb +605 -0
  19. data/lib/activefacts/generators/helpers/rails.rb +57 -0
  20. data/lib/activefacts/generators/html/glossary.rb +462 -0
  21. data/lib/activefacts/generators/metadata/json.rb +204 -0
  22. data/lib/activefacts/generators/null.rb +32 -0
  23. data/lib/activefacts/generators/rails/models.rb +247 -0
  24. data/lib/activefacts/generators/rails/schema.rb +217 -0
  25. data/lib/activefacts/generators/ruby.rb +134 -0
  26. data/lib/activefacts/generators/sql/mysql.rb +281 -0
  27. data/lib/activefacts/generators/sql/server.rb +274 -0
  28. data/lib/activefacts/generators/stats.rb +70 -0
  29. data/lib/activefacts/generators/text.rb +29 -0
  30. data/lib/activefacts/generators/traits/datavault.rb +241 -0
  31. data/lib/activefacts/generators/traits/oo.rb +73 -0
  32. data/lib/activefacts/generators/traits/ordered.rb +33 -0
  33. data/lib/activefacts/generators/traits/ruby.rb +210 -0
  34. data/lib/activefacts/generators/transform/datavault.rb +303 -0
  35. data/lib/activefacts/generators/transform/surrogate.rb +215 -0
  36. data/lib/activefacts/registry.rb +11 -0
  37. metadata +176 -0
@@ -0,0 +1,274 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ # Generate SQL for SQL Server from an ActiveFacts vocabulary.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ require 'activefacts/metamodel'
8
+ require 'activefacts/rmap'
9
+ require 'activefacts/registry'
10
+
11
+ module ActiveFacts
12
+ module Generators
13
+ module SQL #:nodoc:
14
+ # Generate SQL for SQL Server for an ActiveFacts vocabulary.
15
+ # Invoke as
16
+ # afgen --sql/server[=options] <file>.cql
17
+ # Options are comma or space separated:
18
+ # * delay_fks Leave all foreign keys until the end, not just those that contain forward-references
19
+ class SERVER
20
+ private
21
+ include RMap
22
+ ColumnNameMax = 40
23
+
24
+ RESERVED_WORDS = %w{
25
+ ADD ALL ALTER AND ANY AS ASC AUTHORIZATION BACKUP BEGIN BETWEEN
26
+ BREAK BROWSE BULK BY CASCADE CASE CHECK CHECKPOINT CLOSE CLUSTERED
27
+ COALESCE COLLATE COLUMN COMMIT COMPUTE CONSTRAINT CONTAINS CONTAINSTABLE
28
+ CONTINUE CONVERT CREATE CROSS CURRENT CURRENT_DATE CURRENT_TIME
29
+ CURRENT_TIMESTAMP CURRENT_USER CURSOR DATABASE DBCC DEALLOCATE
30
+ DECLARE DEFAULT DELETE DENY DESC DISK DISTINCT DISTRIBUTED DOUBLE
31
+ DROP DUMMY DUMP ELSE END ERRLVL ESCAPE EXCEPT EXEC EXECUTE EXISTS
32
+ EXIT FETCH FILE FILLFACTOR FOR FOREIGN FREETEXT FREETEXTTABLE FROM
33
+ FULL FUNCTION GOTO GRANT GROUP HAVING HOLDLOCK IDENTITY IDENTITYCOL
34
+ IDENTITY_INSERT IF IN INDEX INNER INSERT INTERSECT INTO IS JOIN KEY
35
+ KILL LEFT LIKE LINENO LOAD NATIONAL NOCHECK NONCLUSTERED NOT NULL
36
+ NULLIF OF OFF OFFSETS ON OPEN OPENDATASOURCE OPENQUERY OPENROWSET
37
+ OPENXML OPTION OR ORDER OUTER OVER PERCENT PLAN PRECISION PRIMARY
38
+ PRINT PROC PROCEDURE PUBLIC RAISERROR READ READTEXT RECONFIGURE
39
+ REFERENCES REPLICATION RESTORE RESTRICT RETURN REVOKE RIGHT ROLLBACK
40
+ ROWCOUNT ROWGUIDCOL RULE SAVE SCHEMA SELECT SESSION_USER SET SETUSER
41
+ SHUTDOWN SOME STATISTICS SYSTEM_USER TABLE TEXTSIZE THEN TO TOP
42
+ TRAN TRANSACTION TRIGGER TRUNCATE TSEQUAL UNION UNIQUE UPDATE
43
+ UPDATETEXT USE USER VALUES VARYING VIEW WAITFOR WHEN WHERE WHILE
44
+ WITH WRITETEXT
45
+ }.inject({}){ |h,w| h[w] = true; h }
46
+
47
+ def initialize(vocabulary, *options)
48
+ @vocabulary = vocabulary
49
+ @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
50
+ @delay_fks = options.include? "delay_fks"
51
+ @underscore = options.include?("underscore") ? "_" : ""
52
+ end
53
+
54
+ def puts s
55
+ @out.puts s
56
+ end
57
+
58
+ def go s
59
+ puts s
60
+ puts "GO\n\n"
61
+ end
62
+
63
+ def escape s
64
+ # Escape SQL keywords and non-identifiers
65
+ s = s[0...120]
66
+ if s =~ /[^A-Za-z0-9_]/ || RESERVED_WORDS[s.upcase]
67
+ "[#{s}]"
68
+ else
69
+ s
70
+ end
71
+ end
72
+
73
+ # Return SQL type and (modified?) length for the passed base type
74
+ def normalise_type(type, length)
75
+ sql_type = case type
76
+ when /^Auto ?Counter$/
77
+ 'int'
78
+
79
+ when /^Unsigned ?Integer$/,
80
+ /^Signed ?Integer$/,
81
+ /^Unsigned ?Small ?Integer$/,
82
+ /^Signed ?Small ?Integer$/,
83
+ /^Unsigned ?Tiny ?Integer$/
84
+ s = case
85
+ when length <= 8
86
+ 'tinyint'
87
+ when length <= 16
88
+ 'smallint'
89
+ when length <= 32
90
+ 'int'
91
+ else
92
+ 'bigint'
93
+ end
94
+ length = nil
95
+ s
96
+
97
+ when /^Decimal$/
98
+ 'decimal'
99
+
100
+ when /^Fixed ?Length ?Text$/, /^Char$/
101
+ 'char'
102
+ when /^Variable ?Length ?Text$/, /^String$/
103
+ 'varchar'
104
+ when /^Large ?Length ?Text$/, /^Text$/
105
+ 'text'
106
+
107
+ when /^Date ?And ?Time$/, /^Date ?Time$/
108
+ 'datetime'
109
+ when /^Date$/
110
+ 'datetime' # SQLSVR 2K5: 'date'
111
+ when /^Time$/
112
+ 'datetime' # SQLSVR 2K5: 'time'
113
+ when /^Auto ?Time ?Stamp$/
114
+ 'timestamp'
115
+
116
+ when /^Guid$/
117
+ 'uniqueidentifier'
118
+ when /^Money$/
119
+ 'decimal'
120
+ when /^Picture ?Raw ?Data$/, /^Image$/
121
+ 'image'
122
+ when /^Variable ?Length ?Raw ?Data$/, /^Blob$/
123
+ 'varbinary'
124
+ when /^BIT$/
125
+ 'bit'
126
+ else type # raise "SQL type unknown for standard type #{type}"
127
+ end
128
+ [sql_type, length]
129
+ end
130
+
131
+ public
132
+ def generate(out = $>) #:nodoc:
133
+ @out = out
134
+ #go "CREATE SCHEMA #{@vocabulary.name}"
135
+
136
+ tables_emitted = {}
137
+ delayed_foreign_keys = []
138
+
139
+ @vocabulary.tables.each do |table|
140
+ puts "CREATE TABLE #{escape table.name.gsub(' ',@underscore)} ("
141
+
142
+ pk = table.identifier_columns
143
+ identity_column = pk[0] if pk[0].is_auto_assigned
144
+
145
+ fk_refs = table.references_from.select{|ref| ref.is_simple_reference }
146
+ fk_columns = table.columns.select do |column|
147
+ column.references[0].is_simple_reference
148
+ end
149
+
150
+ # We sort the columns here, not in the rmap layer, because it affects
151
+ # the ordering of columns in an index :-(.
152
+ columns = table.columns.sort_by { |column| column.name(@underscore) }.map do |column|
153
+ name = escape column.name(@underscore)
154
+ padding = " "*(name.size >= ColumnNameMax ? 1 : ColumnNameMax-name.size)
155
+ type, params, constraints = column.type
156
+ constraints = [] if (fk_columns.include?(column)) # Don't enforce VT constraints on FK columns
157
+ length = params[:length]
158
+ length &&= length.to_i
159
+ scale = params[:scale]
160
+ scale &&= scale.to_i
161
+ type, length = normalise_type(type, length)
162
+ sql_type = "#{type}#{
163
+ if !length
164
+ ""
165
+ else
166
+ "(" + length.to_s + (scale ? ", #{scale}" : "") + ")"
167
+ end
168
+ }"
169
+ # Emit IDENTITY for auto-assigned columns, unless it's assigned at assert:
170
+ identity = column == identity_column && column.references[-1].to.transaction_phase != 'assert' ? " IDENTITY" : ""
171
+ null = (column.is_mandatory ? "NOT " : "") + "NULL"
172
+ check = check_clause(name, constraints)
173
+ comment = column.comment
174
+ [ "-- #{comment}", "#{name}#{padding}#{sql_type}#{identity} #{null}#{check}" ]
175
+ end.flatten
176
+
177
+ pk_def = (pk.detect{|column| !column.is_mandatory} ? "UNIQUE(" : "PRIMARY KEY(") +
178
+ pk.map{|column| escape column.name(@underscore)}*", " +
179
+ ")"
180
+
181
+ inline_fks = []
182
+ table.foreign_keys.each do |fk|
183
+ fk_text = "FOREIGN KEY (" +
184
+ fk.from_columns.map{|column| column.name(@underscore)}*", " +
185
+ ") REFERENCES #{escape fk.to.name.gsub(' ',@underscore)} (" +
186
+ fk.to_columns.map{|column| column.name(@underscore)}*", " +
187
+ ")"
188
+ if !@delay_fks and # We don't want to delay all Fks
189
+ (tables_emitted[fk.to] or # The target table has been emitted
190
+ fk.to == table && !fk.to_columns.detect{|column| !column.is_mandatory}) # The reference columns already have the required indexes
191
+ inline_fks << fk_text
192
+ else
193
+ delayed_foreign_keys << ("ALTER TABLE #{escape fk.from.name.gsub(' ',@underscore)}\n\tADD " + fk_text)
194
+ end
195
+ end
196
+
197
+ indices = table.indices
198
+ inline_indices = []
199
+ delayed_indices = []
200
+ indices.each do |index|
201
+ next if index.over == table && index.is_primary # Already did the primary keys
202
+ abbreviated_column_names = index.abbreviated_column_names(@underscore)*""
203
+ column_names = index.column_names(@underscore)
204
+ column_name_list = column_names.map{|n| escape(n)}*", "
205
+ if index.columns.all?{|column| column.is_mandatory}
206
+ inline_indices << "UNIQUE(#{column_name_list})"
207
+ else
208
+ view_name = escape "#{index.view_name}_#{abbreviated_column_names}"
209
+ delayed_indices <<
210
+ %Q{CREATE VIEW dbo.#{view_name} (#{column_name_list}) WITH SCHEMABINDING AS
211
+ \tSELECT #{column_name_list} FROM dbo.#{escape index.on.name.gsub(' ',@underscore)}
212
+ \tWHERE\t#{
213
+ index.columns.
214
+ select{|column| !column.is_mandatory }.
215
+ map{|column|
216
+ escape(column.name(@underscore)) + " IS NOT NULL"
217
+ }*"\n\t AND\t"
218
+ }
219
+ GO
220
+
221
+ CREATE UNIQUE CLUSTERED INDEX #{escape index.name} ON dbo.#{view_name}(#{index.columns.map{|column| column.name(@underscore)}*", "})
222
+ }
223
+ end
224
+ end
225
+
226
+ tables_emitted[table] = true
227
+
228
+ puts("\t" + (columns + [pk_def] + inline_indices + inline_fks)*",\n\t")
229
+ go ")"
230
+ delayed_indices.each {|index_text|
231
+ go index_text
232
+ }
233
+ end
234
+
235
+ delayed_foreign_keys.each do |fk|
236
+ go fk
237
+ end
238
+ end
239
+
240
+ private
241
+ def sql_value(value)
242
+ value.is_literal_string ? sql_string(value.literal) : value.literal
243
+ end
244
+
245
+ def sql_string(str)
246
+ "'" + str.gsub(/'/,"''") + "'"
247
+ end
248
+
249
+ def check_clause(column_name, constraints)
250
+ return "" if constraints.empty?
251
+ # REVISIT: Merge all constraints (later; now just use the first)
252
+ " CHECK(" +
253
+ constraints[0].all_allowed_range_sorted.map do |ar|
254
+ vr = ar.value_range
255
+ min = vr.minimum_bound
256
+ max = vr.maximum_bound
257
+ if (min && max && max.value.literal == min.value.literal)
258
+ "#{column_name} = #{sql_value(min.value)}"
259
+ else
260
+ inequalities = [
261
+ min && "#{column_name} >#{min.is_inclusive ? "=" : ""} #{sql_value(min.value)}",
262
+ max && "#{column_name} <#{max.is_inclusive ? "=" : ""} #{sql_value(max.value)}"
263
+ ].compact
264
+ inequalities.size > 1 ? "(" + inequalities*" AND " + ")" : inequalities[0]
265
+ end
266
+ end*" OR " +
267
+ ")"
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ ActiveFacts::Registry.generator('sql/server', ActiveFacts::Generators::SQL::SERVER)
@@ -0,0 +1,70 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ # Generate metamodel statistics fora compiled vocabulary
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ require 'activefacts/rmap'
8
+ require 'activefacts/registry'
9
+
10
+ module ActiveFacts
11
+ module Generators
12
+ # Generate a text verbalisation of the metamodel constellation created for an ActiveFacts vocabulary.
13
+ # Invoke as
14
+ # afgen --text <file>.cql
15
+ class Statistics
16
+ private
17
+ def initialize(vocabulary)
18
+ @vocabulary = vocabulary
19
+ @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
20
+ end
21
+
22
+ public
23
+ def generate(out = $>)
24
+ constellation = @vocabulary.constellation
25
+ object_types = constellation.ObjectType.values
26
+ fact_types = constellation.FactType.values
27
+
28
+ # All metamodel object types:
29
+ object_count = 0
30
+ populated_object_type_count = 0
31
+ fact_types_processed = {}
32
+ fact_count = 0
33
+ role_played_count = 0
34
+ constellation.vocabulary.object_type.map do |object_type_name, object_type|
35
+ objects = constellation.send(object_type_name)
36
+ next unless objects.size > 0
37
+ puts "\t#{object_type_name}: #{objects.size} instances (which play #{object_type.all_role.size} roles)"
38
+ populated_object_type_count += 1
39
+ object_count += objects.size
40
+
41
+ #puts "#{object_type_name} has #{object_type.all_role.size} roles"
42
+ object_type.all_role.each do |name, role|
43
+ next unless role.unique
44
+ next if fact_types_processed[role.fact_type]
45
+ next if role.fact_type.is_a?(ActiveFacts::API::TypeInheritanceFactType)
46
+ role_population_count =
47
+ objects.values.inject(0) do |count, object|
48
+ count += 1 if object.send(role.name) != nil
49
+ count
50
+ end
51
+ puts "\t\t#{object_type_name}.#{role.name} has #{role_population_count} instances" if role_population_count > 0
52
+ fact_count += role_population_count
53
+ role_played_count += role_population_count*role.fact_type.all_role.size
54
+
55
+ fact_types_processed[role.fact_type] = true
56
+ end
57
+
58
+ end
59
+ puts "#{@vocabulary.name} has"
60
+ puts "\t#{object_types.size} object types"
61
+ puts "\t#{fact_types.size} fact types"
62
+ puts "\tcompiles to #{object_count} objects in total, of #{populated_object_type_count} metamodel types"
63
+ puts "\tcompiles to #{fact_count} facts in total, of #{fact_types_processed.size} metamodel fact types"
64
+ puts "\tcompiles to #{role_played_count} role instances in total"
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ ActiveFacts::Registry.generator('records', ActiveFacts::Generators::Statistics)
@@ -0,0 +1,29 @@
1
+ #
2
+ # ActiveFacts Generators.
3
+ # Generate text output (verbalise the meta-vocabulary) for ActiveFacts vocabularies.
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ require 'activefacts/registry'
8
+
9
+ module ActiveFacts
10
+ module Generators
11
+ # Generate a text verbalisation of the metamodel constellation created for an ActiveFacts vocabulary.
12
+ # Invoke as
13
+ # afgen --text <file>.cql
14
+ class TEXT
15
+ private
16
+ def initialize(vocabulary)
17
+ @vocabulary = vocabulary
18
+ @vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
19
+ end
20
+
21
+ public
22
+ def generate(out = $>)
23
+ out.puts @vocabulary.constellation.verbalise
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ ActiveFacts::Registry.generator('text', ActiveFacts::Generators::TEXT)
@@ -0,0 +1,241 @@
1
+ #
2
+ # ActiveFacts Schema Transform
3
+ # Transform a loaded ActiveFacts vocabulary to suit ActiveRecord
4
+ #
5
+ # Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
6
+ #
7
+ require 'activefacts/generators/helpers/inject'
8
+
9
+ module ActiveFacts
10
+ module Generators
11
+ module DataVaultTraits
12
+
13
+ module ObjectType
14
+
15
+ def dv_add_surrogate type_name = 'Auto Counter', suffix = 'ID'
16
+ # Find or assert the surrogate value type
17
+ auto_counter = vocabulary.valid_value_type_name(type_name) ||
18
+ constellation.ValueType(:vocabulary => vocabulary, :name => type_name, :concept => :new)
19
+
20
+ # Create a subtype to identify this entity type:
21
+ vt_name = self.name + ' '+suffix
22
+ my_id = @vocabulary.valid_value_type_name(vt_name) ||
23
+ constellation.ValueType(:vocabulary => vocabulary, :name => vt_name, :concept => :new, :supertype => auto_counter)
24
+
25
+ # Create a fact type
26
+ identifying_fact_type = constellation.FactType(:concept => :new)
27
+ my_role = constellation.Role(:concept => :new, :fact_type => identifying_fact_type, :ordinal => 0, :object_type => self)
28
+ self.injected_surrogate_role = my_role
29
+ id_role = constellation.Role(:concept => :new, :fact_type => identifying_fact_type, :ordinal => 1, :object_type => my_id)
30
+
31
+ # Create a reading (which needs a RoleSequence)
32
+ reading = constellation.Reading(
33
+ :fact_type => identifying_fact_type,
34
+ :ordinal => 0,
35
+ :role_sequence => [:new],
36
+ :text => "{0} has {1}"
37
+ )
38
+ constellation.RoleRef(:role_sequence => reading.role_sequence, :ordinal => 0, :role => my_role)
39
+ constellation.RoleRef(:role_sequence => reading.role_sequence, :ordinal => 1, :role => id_role)
40
+
41
+ # Create two uniqueness constraints for the one-to-one. Each needs a RoleSequence (two RoleRefs)
42
+ one_id = constellation.PresenceConstraint(
43
+ :concept => :new,
44
+ :vocabulary => vocabulary,
45
+ :name => self.name+'HasOne'+suffix,
46
+ :role_sequence => [:new],
47
+ :is_mandatory => true,
48
+ :min_frequency => 1,
49
+ :max_frequency => 1,
50
+ :is_preferred_identifier => false
51
+ )
52
+ @constellation.RoleRef(:role_sequence => one_id.role_sequence, :ordinal => 0, :role => my_role)
53
+
54
+ one_me = constellation.PresenceConstraint(
55
+ :concept => :new,
56
+ :vocabulary => vocabulary,
57
+ :name => self.name+suffix+'IsOfOne'+self.name,
58
+ :role_sequence => [:new],
59
+ :is_mandatory => false,
60
+ :min_frequency => 0,
61
+ :max_frequency => 1,
62
+ :is_preferred_identifier => true
63
+ )
64
+ @constellation.RoleRef(:role_sequence => one_me.role_sequence, :ordinal => 0, :role => id_role)
65
+ end
66
+ end
67
+
68
+ module ValueType
69
+ def dv_needs_surrogate
70
+ !is_auto_assigned
71
+ end
72
+
73
+ def dv_inject_surrogate
74
+ trace :transform_surrogate, "Adding surrogate ID to Value Type #{name}"
75
+ add_surrogate('Auto Counter', 'ID')
76
+ end
77
+ end
78
+
79
+ module EntityType
80
+ def dv_identifying_refs_from
81
+ pi = preferred_identifier
82
+ rrs = pi.role_sequence.all_role_ref
83
+
84
+ # REVISIT: This is actually a ref to us, not from
85
+ # if absorbed_via
86
+ # return [absorbed_via]
87
+ # end
88
+
89
+ rrs.map do |rr|
90
+ r = references_from.detect{|ref| rr.role == ref.to_role }
91
+ unless r
92
+ debugger
93
+ raise "failed to find #{name} identifying reference for #{rr.role.object_type.name} in #{references_from.inspect}"
94
+ end
95
+ r
96
+ end
97
+ end
98
+
99
+ def dv_needs_surrogate
100
+
101
+ # A recursive proc to replace any reference to an Entity Type by its identifying references:
102
+ trace :transform_surrogate_expansion, "Expanding key for #{name}"
103
+ substitute_identifying_refs = proc do |object|
104
+ if ref = object.absorbed_via
105
+ # This shouldn't be necessary, but see the absorbed_via comment above.
106
+ absorbed_into = ref.from
107
+ trace :transform_surrogate_expansion, "recursing to handle absorption of #{object.name} into #{absorbed_into.name}"
108
+ [substitute_identifying_refs.call(absorbed_into)]
109
+ else
110
+ irf = object.dv_identifying_refs_from
111
+ trace :transform_surrogate_expansion, "Iterating for #{object.name} over #{irf.inspect}" do
112
+ irf.each_with_index do |ref, i|
113
+ next if ref.is_unary
114
+ next if ref.to_role.object_type.kind_of?(ActiveFacts::Metamodel::ValueType)
115
+ recurse_to = ref.to_role.object_type
116
+
117
+ trace :transform_surrogate_expansion, "#{i}: recursing to expand #{recurse_to.name} key in #{ref}" do
118
+ irf[i] = substitute_identifying_refs.call(recurse_to)
119
+ end
120
+ end
121
+ end
122
+ irf
123
+ end
124
+ end
125
+ irf = substitute_identifying_refs.call(self)
126
+
127
+ trace :transform_surrogate, "Does #{name} need a surrogate? it's identified by #{irf.inspect}" do
128
+
129
+ pk_fks = dv_identifying_refs_from.map do |ref|
130
+ ref.to && ref.to.is_table ? ref.to : nil
131
+ end
132
+
133
+ irf.flatten!
134
+
135
+ # Multi-part identifiers are only allowed if:
136
+ # * each part is a foreign key (i.e. it's a join table),
137
+ # * there are no other columns (that might require updating) and
138
+ # * the object is not the target of a foreign key:
139
+ if irf.size >= 2
140
+ if pk_fks.include?(nil)
141
+ trace :transform_surrogate, "#{self.name} needs a surrogate because its multi-part key contains a non-table"
142
+ return true
143
+ elsif references_to.size != 0
144
+ trace :transform_surrogate, "#{self.name} is a join table between #{pk_fks.map(&:name).inspect} but is also an FK target"
145
+ return true
146
+ elsif (references_from-dv_identifying_refs_from).size > 0
147
+ # There are other attributes to worry about
148
+ return true
149
+ else
150
+ trace :transform_surrogate, "#{self.name} is a join table between #{pk_fks.map(&:name).inspect}"
151
+ return false
152
+ end
153
+ return true
154
+ end
155
+
156
+ # Single-part key. It must be an Auto Counter, or we will add a surrogate
157
+
158
+ identifying_type = irf[0].to
159
+ if identifying_type.dv_needs_surrogate
160
+ trace :transform_surrogate, "#{self.name} needs a surrogate because #{irf[0].to.name} is not an AutoCounter, but #{identifying_type.supertypes_transitive.map(&:name).inspect}"
161
+ return true
162
+ end
163
+
164
+ false
165
+ end
166
+ end
167
+
168
+ def dv_inject_surrogate
169
+ trace :transform_surrogate, "Injecting a surrogate key into #{self.name}"
170
+
171
+ # Disable the preferred identifier:
172
+ pi = preferred_identifier
173
+ trace :transform_surrogate, "pi for #{name} was '#{pi.describe}'"
174
+ pi.is_preferred_identifier = false
175
+ @preferred_identifier = nil # Kill the cache
176
+
177
+ dv_add_surrogate
178
+
179
+ trace :transform_surrogate, "pi for #{name} is now '#{preferred_identifier.describe}'"
180
+ end
181
+
182
+ def dv_add_surrogate type_name = 'Auto Counter', suffix = 'ID'
183
+ # Find or assert the surrogate value type
184
+ auto_counter = vocabulary.valid_value_type_name(type_name) ||
185
+ constellation.ValueType(:vocabulary => vocabulary, :name => type_name, :concept => :new)
186
+
187
+ # Create a subtype to identify this entity type:
188
+ vt_name = self.name + ' '+suffix
189
+ my_id = @vocabulary.valid_value_type_name(vt_name) ||
190
+ constellation.ValueType(:vocabulary => vocabulary, :name => vt_name, :concept => :new, :supertype => auto_counter)
191
+
192
+ # Create a fact type
193
+ identifying_fact_type = constellation.FactType(:concept => :new)
194
+ my_role = constellation.Role(:concept => :new, :fact_type => identifying_fact_type, :ordinal => 0, :object_type => self)
195
+ @injected_surrogate_role = my_role
196
+ id_role = constellation.Role(:concept => :new, :fact_type => identifying_fact_type, :ordinal => 1, :object_type => my_id)
197
+
198
+ # Create a reading (which needs a RoleSequence)
199
+ reading = constellation.Reading(
200
+ :fact_type => identifying_fact_type,
201
+ :ordinal => 0,
202
+ :role_sequence => [:new],
203
+ :text => "{0} has {1}"
204
+ )
205
+ constellation.RoleRef(:role_sequence => reading.role_sequence, :ordinal => 0, :role => my_role)
206
+ constellation.RoleRef(:role_sequence => reading.role_sequence, :ordinal => 1, :role => id_role)
207
+
208
+ # Create two uniqueness constraints for the one-to-one. Each needs a RoleSequence (two RoleRefs)
209
+ one_id = constellation.PresenceConstraint(
210
+ :concept => :new,
211
+ :vocabulary => vocabulary,
212
+ :name => self.name+'HasOne'+suffix,
213
+ :role_sequence => [:new],
214
+ :is_mandatory => true,
215
+ :min_frequency => 1,
216
+ :max_frequency => 1,
217
+ :is_preferred_identifier => false
218
+ )
219
+ @constellation.RoleRef(:role_sequence => one_id.role_sequence, :ordinal => 0, :role => my_role)
220
+
221
+ one_me = constellation.PresenceConstraint(
222
+ :concept => :new,
223
+ :vocabulary => vocabulary,
224
+ :name => self.name+suffix+'IsOfOne'+self.name,
225
+ :role_sequence => [:new],
226
+ :is_mandatory => false,
227
+ :min_frequency => 0,
228
+ :max_frequency => 1,
229
+ :is_preferred_identifier => true
230
+ )
231
+ @constellation.RoleRef(:role_sequence => one_me.role_sequence, :ordinal => 0, :role => id_role)
232
+
233
+ return my_id
234
+ end
235
+
236
+ end
237
+
238
+ include ActiveFacts::TraitInjector # Must be last in this module, after all submodules have been defined
239
+ end
240
+ end
241
+ end