activefacts-generators 1.7.1

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