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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +30 -0
- data/Rakefile +6 -0
- data/activefacts-generators.gemspec +26 -0
- data/lib/activefacts/dependency_analyser.rb +182 -0
- data/lib/activefacts/generators/absorption.rb +71 -0
- data/lib/activefacts/generators/composition.rb +119 -0
- data/lib/activefacts/generators/cql.rb +715 -0
- data/lib/activefacts/generators/diagrams/json.rb +340 -0
- data/lib/activefacts/generators/help.rb +64 -0
- data/lib/activefacts/generators/helpers/inject.rb +16 -0
- data/lib/activefacts/generators/helpers/oo.rb +162 -0
- data/lib/activefacts/generators/helpers/ordered.rb +605 -0
- data/lib/activefacts/generators/helpers/rails.rb +57 -0
- data/lib/activefacts/generators/html/glossary.rb +462 -0
- data/lib/activefacts/generators/metadata/json.rb +204 -0
- data/lib/activefacts/generators/null.rb +32 -0
- data/lib/activefacts/generators/rails/models.rb +247 -0
- data/lib/activefacts/generators/rails/schema.rb +217 -0
- data/lib/activefacts/generators/ruby.rb +134 -0
- data/lib/activefacts/generators/sql/mysql.rb +281 -0
- data/lib/activefacts/generators/sql/server.rb +274 -0
- data/lib/activefacts/generators/stats.rb +70 -0
- data/lib/activefacts/generators/text.rb +29 -0
- data/lib/activefacts/generators/traits/datavault.rb +241 -0
- data/lib/activefacts/generators/traits/oo.rb +73 -0
- data/lib/activefacts/generators/traits/ordered.rb +33 -0
- data/lib/activefacts/generators/traits/ruby.rb +210 -0
- data/lib/activefacts/generators/transform/datavault.rb +303 -0
- data/lib/activefacts/generators/transform/surrogate.rb +215 -0
- data/lib/activefacts/registry.rb +11 -0
- 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
|