activefacts-compositions 1.9.17 → 1.9.18
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/activefacts-compositions.gemspec +2 -2
- data/lib/activefacts/compositions/binary.rb +1 -1
- data/lib/activefacts/compositions/compositor.rb +16 -12
- data/lib/activefacts/compositions/datavault.rb +110 -115
- data/lib/activefacts/compositions/relational.rb +137 -94
- data/lib/activefacts/compositions/staging.rb +89 -27
- data/lib/activefacts/compositions/traits/datavault.rb +116 -49
- data/lib/activefacts/compositions/traits/rails.rb +2 -2
- data/lib/activefacts/compositions/version.rb +1 -1
- data/lib/activefacts/generator/doc/cwm.rb +6 -18
- data/lib/activefacts/generator/doc/ldm.rb +1 -1
- data/lib/activefacts/generator/etl/unidex.rb +341 -0
- data/lib/activefacts/generator/oo.rb +31 -14
- data/lib/activefacts/generator/rails/models.rb +6 -5
- data/lib/activefacts/generator/rails/schema.rb +5 -9
- data/lib/activefacts/generator/ruby.rb +2 -2
- data/lib/activefacts/generator/sql/mysql.rb +3 -184
- data/lib/activefacts/generator/sql/oracle.rb +3 -152
- data/lib/activefacts/generator/sql/postgres.rb +3 -145
- data/lib/activefacts/generator/sql/server.rb +3 -126
- data/lib/activefacts/generator/sql.rb +54 -422
- data/lib/activefacts/generator/summary.rb +15 -6
- data/lib/activefacts/generator/traits/expr.rb +41 -0
- data/lib/activefacts/generator/traits/sql/mysql.rb +280 -0
- data/lib/activefacts/generator/traits/sql/oracle.rb +265 -0
- data/lib/activefacts/generator/traits/sql/postgres.rb +287 -0
- data/lib/activefacts/generator/traits/sql/server.rb +262 -0
- data/lib/activefacts/generator/traits/sql.rb +538 -0
- metadata +13 -8
- data/lib/activefacts/compositions/docgraph.rb +0 -798
- data/lib/activefacts/compositions/staging/persistent.rb +0 -107
@@ -0,0 +1,341 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Unified Index Schema Generator
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009-2016 Clifford Heath. Read the LICENSE file.
|
5
|
+
#
|
6
|
+
require 'digest/sha1'
|
7
|
+
require 'activefacts/metamodel'
|
8
|
+
require 'activefacts/metamodel/datatypes'
|
9
|
+
require 'activefacts/compositions'
|
10
|
+
require 'activefacts/compositions/names'
|
11
|
+
require 'activefacts/generator'
|
12
|
+
require 'activefacts/generator/traits/sql'
|
13
|
+
|
14
|
+
module ActiveFacts
|
15
|
+
module Generators
|
16
|
+
module ETL
|
17
|
+
class Unidex
|
18
|
+
|
19
|
+
MM = ActiveFacts::Metamodel unless const_defined?(:MM)
|
20
|
+
def self.options
|
21
|
+
# REVISIT: There's no way to support SQL dialect options here
|
22
|
+
sql_trait = ActiveFacts::Generators::Traits::SQL
|
23
|
+
Class.new.extend(sql_trait). # Anonymous class to enable access to traits module instance methods
|
24
|
+
options.
|
25
|
+
merge(
|
26
|
+
{
|
27
|
+
dialect: [String, "SQL Dialect to use"],
|
28
|
+
value_width: [Integer, "Number of characters to index from long values"],
|
29
|
+
phonetic_confidence: [Integer, "Percentage confidence for a phonetic match"],
|
30
|
+
}
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize composition, options = {}
|
35
|
+
@composition = composition
|
36
|
+
@options = options
|
37
|
+
|
38
|
+
@trait = ActiveFacts::Generators::Traits::SQL
|
39
|
+
if @dialect = options.delete("dialect")
|
40
|
+
require 'activefacts/generator/traits/sql/'+@dialect
|
41
|
+
trait_name = ActiveFacts::Generators::Traits::SQL.constants.detect{|c| c.to_s =~ %r{#{@dialect}}i}
|
42
|
+
@trait = @trait.const_get(trait_name)
|
43
|
+
end
|
44
|
+
self.class.include @trait
|
45
|
+
self.class.extend @trait
|
46
|
+
extend @trait
|
47
|
+
|
48
|
+
process_options options
|
49
|
+
end
|
50
|
+
|
51
|
+
def process_options options
|
52
|
+
@value_width = (options.delete('value_width') || 32).to_i
|
53
|
+
@phonetic_confidence = (options.delete('phonetic_confidence') || 70).to_i
|
54
|
+
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
def generate
|
59
|
+
@all_table_unions = []
|
60
|
+
header +
|
61
|
+
@composition.
|
62
|
+
all_composite.
|
63
|
+
sort_by{|c| c.mapping.name}.
|
64
|
+
map{|c| generate_composite c}.
|
65
|
+
concat([all_union(@all_table_unions)]).
|
66
|
+
compact*"\n" +
|
67
|
+
trailer
|
68
|
+
end
|
69
|
+
|
70
|
+
def all_union unions
|
71
|
+
return '' if unions.empty?
|
72
|
+
create_or_replace("#{schema_name}_unidex", 'VIEW') + " AS\n" +
|
73
|
+
unions.compact.map{|s| "SELECT * FROM "+s } *
|
74
|
+
"\nUNION ALL " +
|
75
|
+
";\n"
|
76
|
+
end
|
77
|
+
|
78
|
+
def header
|
79
|
+
schema_prefix
|
80
|
+
end
|
81
|
+
|
82
|
+
def generate_composite composite
|
83
|
+
return nil if composite.mapping.injection_annotation
|
84
|
+
return nil if composite.mapping.object_type.is_static
|
85
|
+
|
86
|
+
trace :unidex, "Generating view for #{table_name(composite)}" do
|
87
|
+
union =
|
88
|
+
composite.mapping.all_member.to_a.sort_by{|m| m.name}.flat_map do |member|
|
89
|
+
next nil if member.injection_annotation
|
90
|
+
rank_key = member.rank_key
|
91
|
+
|
92
|
+
case key_type = rank_key[0]
|
93
|
+
when MM::Component::RANK_SURROGATE, # A surrogate key; these do not get indexed
|
94
|
+
MM::Component::RANK_DISCRIMINATOR, # Replacement for exclusive indicators, often subtypes
|
95
|
+
MM::Component::RANK_MULTIPLE # Nested absorption
|
96
|
+
trace :unidex, "Ignoring #{MM::DataTypes::TypeNames[key_type]} #{column_name member}"
|
97
|
+
next nil
|
98
|
+
when MM::Component::RANK_INJECTION # ValueField (index), ValidFrom (don't) or an Absorption with an injection annotation
|
99
|
+
if MM::ValueField === member
|
100
|
+
generate_value leaf
|
101
|
+
else
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
when MM::Component::RANK_INDICATOR
|
105
|
+
generate_indicator member
|
106
|
+
else
|
107
|
+
raise "Unexpected non-Absorption" unless MM::Absorption === member
|
108
|
+
if member.foreign_key
|
109
|
+
# Index this record by the natural key of the FK target record, if possible
|
110
|
+
generate_joined_value member
|
111
|
+
# elsif member.full_absorption # REVISIT: Anything special to do here?
|
112
|
+
else
|
113
|
+
(member.all_member.size > 0 ? member.all_leaf : [member]).flat_map do |leaf|
|
114
|
+
generate_value leaf
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end.compact * "\nUNION ALL"
|
119
|
+
|
120
|
+
if union.size > 0
|
121
|
+
union_name = "#{table_name(composite)}_unidex"
|
122
|
+
@all_table_unions << union_name
|
123
|
+
|
124
|
+
"/*\n"+
|
125
|
+
" * View to extract unified index values for #{table_name(composite)}\n"+
|
126
|
+
" */\n"+
|
127
|
+
create_or_replace("#{union_name}", 'VIEW') + " AS" +
|
128
|
+
union +
|
129
|
+
";\n"
|
130
|
+
else
|
131
|
+
''
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def trailer
|
137
|
+
''
|
138
|
+
end
|
139
|
+
|
140
|
+
def generate_indicator leaf
|
141
|
+
nil # REVISIT: Do we need anything here?
|
142
|
+
# select leaf.root, safe_column_name(leaf), 1, column_name(leaf), 1
|
143
|
+
end
|
144
|
+
|
145
|
+
# This foreign key connects two composites (tables)
|
146
|
+
def generate_joined_value member
|
147
|
+
foreign_key = member.foreign_key
|
148
|
+
# REVISIT: Is this restriction even necessary?
|
149
|
+
return nil unless foreign_key.composite.mapping.object_type.is_static
|
150
|
+
|
151
|
+
# Index the source table by the natural key of the target, if we can find one
|
152
|
+
indices = foreign_key.composite.all_index
|
153
|
+
return null if indices.empty?
|
154
|
+
|
155
|
+
search_index_by = {}
|
156
|
+
searchable_indices =
|
157
|
+
indices.select do |ix|
|
158
|
+
next false if !ix.is_unique
|
159
|
+
non_fk_components = ix.all_index_field.map(&:component) - foreign_key.all_index_field.map(&:component)
|
160
|
+
next unless non_fk_components.size == 1
|
161
|
+
component = non_fk_components[0]
|
162
|
+
next unless MM::Absorption === component
|
163
|
+
value_type = component.object_type
|
164
|
+
search_methods = value_type.applicable_parameter_restrictions('Search')
|
165
|
+
search_methods.reject!{|vtpr| m = vtpr.value_range.minimum_bound and m.value == 'none'}
|
166
|
+
search_methods.map!{|sm| sm.value_range.minimum_bound.value.effective_value}
|
167
|
+
if search_methods.empty?
|
168
|
+
false
|
169
|
+
else
|
170
|
+
search_index_by[ix] = search_methods
|
171
|
+
end
|
172
|
+
end
|
173
|
+
return nil if search_index_by.empty?
|
174
|
+
|
175
|
+
search_index_by.flat_map do |search_index, search_methods|
|
176
|
+
trace :unidex, "Search #{table_name foreign_key.source_composite} via #{table_name search_index.composite}.#{column_name search_index.all_index_field.to_a[0].component} using #{search_methods.map(&:inspect)*', '}"
|
177
|
+
|
178
|
+
fk_pairs =
|
179
|
+
foreign_key.all_foreign_key_field.to_a.
|
180
|
+
zip foreign_key.all_index_field.to_a
|
181
|
+
leaf = search_index.all_index_field.to_a[0].component # Returning this natural index value
|
182
|
+
source_table = table_name(foreign_key.composite)
|
183
|
+
source_field = safe_column_name(member)
|
184
|
+
type_name, options = leaf.data_type(data_type_context) # Which has this type_name
|
185
|
+
intrinsic_type = MM::DataType.intrinsic_type(type_name) # Which corresponds to this intrinsic type
|
186
|
+
|
187
|
+
col_expr = Expression.new(
|
188
|
+
%Q{
|
189
|
+
(SELECT #{safe_column_name(leaf)}
|
190
|
+
FROM #{source_table} AS f
|
191
|
+
WHERE #{
|
192
|
+
fk_pairs.map do |fkf, ixf|
|
193
|
+
"#{table_name foreign_key.source_composite}.#{safe_column_name(fkf.component)} = f.#{safe_column_name(ixf.component)}"
|
194
|
+
end*' AND '
|
195
|
+
})}.
|
196
|
+
gsub(/\s+/,' '),
|
197
|
+
intrinsic_type,
|
198
|
+
foreign_key.all_foreign_key_field.to_a.all?{|fkf| fkf.component.path_mandatory}
|
199
|
+
)
|
200
|
+
search_expr foreign_key.source_composite, intrinsic_type, col_expr, search_methods, source_field
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def generate_value leaf
|
205
|
+
return nil unless leaf.is_a?(MM::Absorption)
|
206
|
+
|
207
|
+
value_type = leaf.object_type
|
208
|
+
type_name, options = leaf.data_type(data_type_context)
|
209
|
+
length = options[:length]
|
210
|
+
value_constraint = options[:value_constraint]
|
211
|
+
|
212
|
+
# Look for instructions on how to index this leaf for search:
|
213
|
+
search_methods = value_type.applicable_parameter_restrictions('Search')
|
214
|
+
search_methods.reject!{|vtpr| m = vtpr.value_range.minimum_bound and m.value == 'none'}
|
215
|
+
return nil if search_methods.empty?
|
216
|
+
search_methods.map!{|sm| sm.value_range.minimum_bound.value.effective_value}
|
217
|
+
|
218
|
+
# Convert from the model's data type to a metamodel type, if possible
|
219
|
+
intrinsic_type = MM::DataType.intrinsic_type(type_name)
|
220
|
+
data_type_name = intrinsic_type ? MM::DataType::TypeNames[intrinsic_type] : type_name
|
221
|
+
trace :unidex, "Search #{table_name leaf.root}.#{column_name(leaf)} as #{data_type_name} using #{search_methods.map(&:inspect)*', '}"
|
222
|
+
|
223
|
+
col_expr = Expression.new(safe_column_name(leaf), intrinsic_type, leaf.is_mandatory)
|
224
|
+
source_field = safe_column_name(leaf)
|
225
|
+
|
226
|
+
search_expr leaf.root, intrinsic_type, col_expr, search_methods, source_field
|
227
|
+
end
|
228
|
+
|
229
|
+
def search_expr composite, intrinsic_type, col_expr, search_methods, source_field
|
230
|
+
case intrinsic_type
|
231
|
+
when MM::DataType::TYPE_Char,
|
232
|
+
MM::DataType::TYPE_String,
|
233
|
+
MM::DataType::TYPE_Text
|
234
|
+
# Produce a truncated value with the requested search
|
235
|
+
search_methods.flat_map do |sm|
|
236
|
+
case sm
|
237
|
+
when 'none' # Do not index this value
|
238
|
+
nil
|
239
|
+
when 'simple' # Disregard white-space only
|
240
|
+
select(composite, truncate(col_expr, @value_width), 'simple', source_field, 1.0)
|
241
|
+
|
242
|
+
when 'alpha' # Strip white space and punctuation, just use alphabetic characters
|
243
|
+
select(composite, truncate(as_alpha(col_expr), @value_width), 'alpha', source_field, 0.9)
|
244
|
+
|
245
|
+
when 'words' # Break the text into words and match each word like alpha
|
246
|
+
nil # REVISIT: Implement this type
|
247
|
+
|
248
|
+
# when 'phrases' # Words, but where adjacent sequences of words matter
|
249
|
+
when 'typo' # Use trigram similarity to detect typographic errors
|
250
|
+
# REVISIT: Implement this type properly
|
251
|
+
select(composite, trigram(as_alpha(col_expr)), 'typo', source_field, 0.9)
|
252
|
+
|
253
|
+
when 'phonetic' # Use phonetic matching as well as trigrams
|
254
|
+
phonetics(col_expr).map do |p|
|
255
|
+
select(composite, p, 'phonetic', source_field, @phonetic_confidence/100.0)
|
256
|
+
end
|
257
|
+
|
258
|
+
when 'names' # Break the text into words and match each word like phonetic
|
259
|
+
nil # REVISIT: Implement this type
|
260
|
+
|
261
|
+
when 'text' # Index a large text field using significant words and phrases
|
262
|
+
nil # REVISIT: Implement this type
|
263
|
+
else
|
264
|
+
raise "Unknown search method #{sm}"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
when MM::DataType::TYPE_Boolean
|
269
|
+
nil # REVISIT: Implement this type
|
270
|
+
|
271
|
+
when MM::DataType::TYPE_Integer,
|
272
|
+
MM::DataType::TYPE_Real,
|
273
|
+
MM::DataType::TYPE_Decimal,
|
274
|
+
MM::DataType::TYPE_Money
|
275
|
+
# Produce a right-justified value
|
276
|
+
select(composite, lexical_decimal(col_expr, @value_width, value_type.scale), 'simple', source_field, 1)
|
277
|
+
|
278
|
+
when MM::DataType::TYPE_Date
|
279
|
+
# Produce an ISO representation that sorts lexically (YYYY-MM-DD)
|
280
|
+
# REVISIT: Support search methods here
|
281
|
+
select(composite, lexical_date(col_expr), 'simple', source_field, 1)
|
282
|
+
|
283
|
+
when MM::DataType::TYPE_DateTime,
|
284
|
+
MM::DataType::TYPE_Timestamp
|
285
|
+
# Produce an ISO representation that sorts lexically (YYYY-MM-DD HH:mm:ss)
|
286
|
+
# REVISIT: Support search methods here
|
287
|
+
select(composite, lexical_datetime(col_expr), 'simple', source_field, 1)
|
288
|
+
|
289
|
+
when MM::DataType::TYPE_Time
|
290
|
+
# Produce an ISO representation that sorts lexically (YYYY-MM-DD HH:mm:ss)
|
291
|
+
select(composite, lexical_time(col_expr), 'simple', source_field, 1)
|
292
|
+
|
293
|
+
when MM::DataType::TYPE_Binary
|
294
|
+
nil # No indexing applied
|
295
|
+
when nil # Data Type is unknown
|
296
|
+
else
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def stylise_column_name name
|
301
|
+
name.words.send(@column_case)*@column_joiner
|
302
|
+
end
|
303
|
+
|
304
|
+
def select composite, expression, processing, source_field, confidence = 1, where = []
|
305
|
+
# These fields are in order of index precedence, to co-locate
|
306
|
+
# comparable values regardless of source record type or column
|
307
|
+
where << 'Value IS NOT NULL' if expression.to_s =~ /\bNULL\b/
|
308
|
+
processing_name = stylise_column_name("Processing")
|
309
|
+
value_name = stylise_column_name("Value")
|
310
|
+
load_batch_id_name = stylise_column_name("LoadBatchID")
|
311
|
+
record_guid_name = stylise_column_name("RecordGUID")
|
312
|
+
confidence_name = stylise_column_name("Confidence")
|
313
|
+
source_table_name = stylise_column_name("SourceTable")
|
314
|
+
source_field_name = stylise_column_name("SourceField")
|
315
|
+
expression_text = expression.to_s
|
316
|
+
expression_text = "ARRAY[#{expression_text}]" unless expression.is_array
|
317
|
+
select = %Q{
|
318
|
+
SELECT '#{processing}' AS #{processing_name},
|
319
|
+
#{expression_text} AS #{value_name},
|
320
|
+
#{load_batch_id_name},
|
321
|
+
#{"%.2f" % confidence} AS #{confidence_name},
|
322
|
+
#{record_guid_name},
|
323
|
+
'#{safe_table_name(composite)}' AS #{source_table_name},
|
324
|
+
'#{source_field}' AS #{source_field_name}
|
325
|
+
FROM #{safe_table_name(composite)}
|
326
|
+
WHERE COALESCE(#{expression},'') <> ''}.
|
327
|
+
unindent
|
328
|
+
|
329
|
+
if where.empty?
|
330
|
+
select
|
331
|
+
else
|
332
|
+
"\nSELECT * FROM (#{select}\n) AS s WHERE #{where*' AND '}"
|
333
|
+
end
|
334
|
+
|
335
|
+
end
|
336
|
+
|
337
|
+
end
|
338
|
+
end
|
339
|
+
publish_generator ETL::Unidex
|
340
|
+
end
|
341
|
+
end
|
@@ -25,7 +25,8 @@ module ActiveFacts
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def generate
|
28
|
-
@
|
28
|
+
@composites_started = {}
|
29
|
+
@composites_finished = {}
|
29
30
|
|
30
31
|
retract_intrinsic_types
|
31
32
|
|
@@ -58,7 +59,7 @@ module ActiveFacts
|
|
58
59
|
return true if o.name == "_ImplicitBooleanValueType"
|
59
60
|
return false if o.supertype
|
60
61
|
# A value type with no supertype must be emitted if it is the child in any absorption:
|
61
|
-
return !composite.mapping.all_member.detect{|m| m.
|
62
|
+
return !composite.mapping.all_member.detect{|m| m.forward_mapping}
|
62
63
|
end
|
63
64
|
|
64
65
|
def retract_intrinsic_types
|
@@ -92,8 +93,25 @@ module ActiveFacts
|
|
92
93
|
"REVISIT: override class_finale\n"
|
93
94
|
end
|
94
95
|
|
95
|
-
def
|
96
|
-
|
96
|
+
def exclude_as_counterpart member
|
97
|
+
# If emitting a role also creates its counterpart, we can exclude the counterparts.
|
98
|
+
# This depends on the subtype, so centralise the knowledge here where it can be overridden.
|
99
|
+
# Non-Mappings always get emitted:
|
100
|
+
return false unless MM::Mapping === member
|
101
|
+
|
102
|
+
# TypeInheritance always gets skipped:
|
103
|
+
return true if MM::Absorption === member && member.child_role.fact_type.is_a?(MM::TypeInheritance)
|
104
|
+
|
105
|
+
# Don't skip the mandatory counterpart of a one-to-one:
|
106
|
+
return false if member.is_one_to_one and member.is_mandatory
|
107
|
+
|
108
|
+
# Otherwise skip all reverse mappings (these have a forward mapping)
|
109
|
+
return member.forward_mapping
|
110
|
+
end
|
111
|
+
|
112
|
+
def generate_class composite, precursor_to = [], predefine_role_players = true
|
113
|
+
return nil if @composites_started[composite]
|
114
|
+
@composites_started[composite] = true
|
97
115
|
|
98
116
|
mapping = composite.mapping
|
99
117
|
object_type = mapping.object_type
|
@@ -104,9 +122,7 @@ module ActiveFacts
|
|
104
122
|
supertype_composites =
|
105
123
|
object_type.all_supertype.map{|s| composite_for(s) }.compact
|
106
124
|
forward_declarations +=
|
107
|
-
supertype_composites.map{|c| generate_class(c, false)}.compact
|
108
|
-
|
109
|
-
@composites_emitted[composite] = true
|
125
|
+
supertype_composites.map{|c| generate_class(c, precursor_to+[composite], false)}.compact
|
110
126
|
|
111
127
|
# Select the members that will be declared as O-O roles:
|
112
128
|
mapping.re_rank
|
@@ -114,23 +130,22 @@ module ActiveFacts
|
|
114
130
|
all_member.
|
115
131
|
sort_by{|m| m.ordinal}.
|
116
132
|
reject do |m|
|
117
|
-
m
|
118
|
-
m.forward_absorption || m.child_role.fact_type.is_a?(MM::TypeInheritance)
|
133
|
+
exclude_as_counterpart(m)
|
119
134
|
end
|
120
135
|
|
121
136
|
if predefine_role_players
|
122
|
-
#
|
123
|
-
#
|
124
|
-
#
|
125
|
-
# For those roles that derive from Mappings, produce class definitions to avoid forward references:
|
137
|
+
# To reduce forward references, emit any classes that play a role
|
138
|
+
# we're about to emit, unless one is a subtype of any class that
|
139
|
+
# is currently being processed.
|
126
140
|
forward_composites =
|
127
141
|
members.
|
128
142
|
select{ |m| m.is_a?(MM::Mapping) }.
|
129
143
|
map{ |m| composite_for m.object_type }.
|
144
|
+
reject{|c| precursor_to.detect{|p| c.mapping.object_type.supertypes_transitive.include?(p.mapping.object_type) }}.
|
130
145
|
compact.
|
131
146
|
sort_by{|c| c.mapping.name}
|
132
147
|
forward_declarations +=
|
133
|
-
forward_composites.map{|c| generate_class(c)}.compact
|
148
|
+
forward_composites.map{|c| generate_class(c, precursor_to+[composite])}.compact
|
134
149
|
end
|
135
150
|
|
136
151
|
forward_declarations = forward_declarations.map{|f| "#{f}\n"}*''
|
@@ -166,6 +181,8 @@ module ActiveFacts
|
|
166
181
|
value_type_declaration object_type
|
167
182
|
end
|
168
183
|
|
184
|
+
@composites_finished[composite] = true
|
185
|
+
|
169
186
|
forward_declarations +
|
170
187
|
class_prelude(object_type, primary_supertype) +
|
171
188
|
type_declaration +
|
@@ -172,9 +172,9 @@ module ActiveFacts
|
|
172
172
|
end
|
173
173
|
|
174
174
|
[
|
175
|
-
fk.
|
175
|
+
fk.mapping ? " \# #{fk.mapping.comment}" : nil,
|
176
176
|
" belongs_to :#{association_name}#{class_name}#{foreign_key}",
|
177
|
-
fk.
|
177
|
+
fk.mapping ? '' : nil,
|
178
178
|
]
|
179
179
|
end.compact
|
180
180
|
end
|
@@ -193,13 +193,13 @@ module ActiveFacts
|
|
193
193
|
|
194
194
|
[
|
195
195
|
# REVISIT: We want the reverse-order comment here really
|
196
|
-
fk.
|
196
|
+
fk.mapping ? " \# #{fk.mapping.comment}" : nil,
|
197
197
|
%Q{ #{association_type} :#{association_name}} +
|
198
198
|
%Q{, :class_name => '#{fk.source_composite.rails.class_name}'} +
|
199
199
|
%Q{, :foreign_key => :#{fk.all_foreign_key_field.single.component.column_name.snakecase}} +
|
200
200
|
%Q{, :dependent => :destroy}
|
201
201
|
] +
|
202
|
-
# If fk.
|
202
|
+
# If fk.mapping.source_composite is a join table, we can emit a has_many :through for each other key
|
203
203
|
# REVISIT: We could alternately do this for all belongs_to's in the source composite
|
204
204
|
if fk.source_composite.primary_index.all_index_field.size > 1
|
205
205
|
fk.source_composite.primary_index.all_index_field.map(&:component).flat_map do |ic|
|
@@ -212,7 +212,7 @@ module ActiveFacts
|
|
212
212
|
else
|
213
213
|
[]
|
214
214
|
end +
|
215
|
-
[fk.
|
215
|
+
[fk.mapping ? '' : nil]
|
216
216
|
end
|
217
217
|
end
|
218
218
|
|
@@ -221,6 +221,7 @@ module ActiveFacts
|
|
221
221
|
ccs =
|
222
222
|
composite.mapping.all_leaf.flat_map do |component|
|
223
223
|
next unless component.path_mandatory && !component.is_a?(Metamodel::Indicator)
|
224
|
+
next if composite.primary_index != composite.natural_index && composite.primary_index.all_index_field.detect{|ixf| ixf.component == component}
|
224
225
|
next if component.is_a?(Metamodel::Mapping) && component.object_type.is_a?(Metamodel::ValueType) && component.is_auto_assigned
|
225
226
|
[ " validates :#{component.column_name.snakecase}, :presence => true" ]
|
226
227
|
end.compact
|
@@ -18,7 +18,7 @@ module ActiveFacts
|
|
18
18
|
HEADER = "# Auto-generated from CQL, edits will be lost"
|
19
19
|
def self.options
|
20
20
|
({
|
21
|
-
|
21
|
+
fks: ['Boolean', "Generate foreign key definitions"],
|
22
22
|
include_comments: ['Boolean', "Generate a comment for each column showing the absorption path"],
|
23
23
|
closed_world: ['Boolean', "Set this if your DBMS only allows one null in a unique index (MS SQL)"],
|
24
24
|
})
|
@@ -27,7 +27,7 @@ module ActiveFacts
|
|
27
27
|
def initialize composition, options = {}
|
28
28
|
@composition = composition
|
29
29
|
@options = options
|
30
|
-
@option_exclude_fks = options.delete("
|
30
|
+
@option_exclude_fks = [false, 'f', 'n', 'no'].include?(options.delete("fks"))
|
31
31
|
@option_include_comments = options.delete("include_comments")
|
32
32
|
@option_closed_world = options.delete("closed_world")
|
33
33
|
end
|
@@ -147,7 +147,7 @@ module ActiveFacts
|
|
147
147
|
ixf.ordinal == 0 # It's first in this index
|
148
148
|
end
|
149
149
|
already_indexed = from_index_needed.any?{|ixf| @indexes_generated[ixf.access_path]}
|
150
|
-
is_one_to_one = fk.
|
150
|
+
is_one_to_one = fk.mapping && fk.mapping.child_role.is_unique
|
151
151
|
|
152
152
|
index_name = ACTR::name_trunc("index_#{ar_table_name}_on_#{from_column_names[0]}")
|
153
153
|
[
|
@@ -244,14 +244,10 @@ module ActiveFacts
|
|
244
244
|
end
|
245
245
|
|
246
246
|
def surrogate_type
|
247
|
-
type_name, =
|
247
|
+
type_name, = choose_integer_range(0, 2**(default_surrogate_length-1)-1)
|
248
248
|
type_name
|
249
249
|
end
|
250
250
|
|
251
|
-
def valid_from_type
|
252
|
-
date_time_type
|
253
|
-
end
|
254
|
-
|
255
251
|
def date_time_type
|
256
252
|
'datetime'
|
257
253
|
end
|
@@ -271,7 +267,7 @@ module ActiveFacts
|
|
271
267
|
|
272
268
|
# Return SQL type and (modified?) length for the passed base type
|
273
269
|
def normalise_type type_name
|
274
|
-
type = MM::DataType.
|
270
|
+
type = MM::DataType.intrinsic_type(type_name)
|
275
271
|
|
276
272
|
[
|
277
273
|
type,
|
@@ -69,7 +69,7 @@ module ActiveFacts
|
|
69
69
|
if component.is_a?(MM::Absorption) and
|
70
70
|
counterpart = component.object_type and
|
71
71
|
counterpart_composite = composite_for(counterpart)
|
72
|
-
counterpart_class_emitted = @
|
72
|
+
counterpart_class_emitted = @composites_finished[counterpart_composite]
|
73
73
|
|
74
74
|
counterpart_class_name = ruby_class_name counterpart_composite
|
75
75
|
counterpart_default_role = ruby_role_name counterpart_composite.mapping
|
@@ -79,7 +79,7 @@ module ActiveFacts
|
|
79
79
|
|
80
80
|
# Does the reverse role need explicit specification?
|
81
81
|
implied_reverse_role_name = ruby_role_name(component.root.mapping)
|
82
|
-
actual_reverse_role_name = ruby_role_name component.
|
82
|
+
actual_reverse_role_name = ruby_role_name(component.reverse_mapping || component.forward_mapping)
|
83
83
|
|
84
84
|
if implied_reverse_role_name != actual_reverse_role_name
|
85
85
|
counterpart_spec = ", counterpart: :#{actual_reverse_role_name}"
|