activefacts-compositions 1.9.6 → 1.9.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Rakefile +33 -0
- data/activefacts-compositions.gemspec +3 -3
- data/bin/schema_compositor +142 -85
- data/lib/activefacts/compositions/binary.rb +19 -15
- data/lib/activefacts/compositions/compositor.rb +126 -125
- data/lib/activefacts/compositions/constraints.rb +74 -54
- data/lib/activefacts/compositions/datavault.rb +545 -0
- data/lib/activefacts/compositions/names.rb +58 -58
- data/lib/activefacts/compositions/relational.rb +801 -692
- data/lib/activefacts/compositions/traits/rails.rb +180 -0
- data/lib/activefacts/compositions/version.rb +1 -1
- data/lib/activefacts/generator/doc/css/ldm.css +45 -0
- data/lib/activefacts/generator/doc/cwm.rb +764 -0
- data/lib/activefacts/generator/doc/glossary.rb +473 -0
- data/lib/activefacts/generator/doc/graphviz.rb +134 -0
- data/lib/activefacts/generator/doc/ldm.rb +698 -0
- data/lib/activefacts/generator/oo.rb +130 -124
- data/lib/activefacts/generator/rails/models.rb +237 -0
- data/lib/activefacts/generator/rails/schema.rb +273 -0
- data/lib/activefacts/generator/ruby.rb +75 -67
- data/lib/activefacts/generator/sql.rb +333 -351
- data/lib/activefacts/generator/sql/server.rb +100 -39
- data/lib/activefacts/generator/summary.rb +67 -59
- data/lib/activefacts/generator/validate.rb +19 -134
- metadata +18 -15
@@ -0,0 +1,273 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Rails 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/registry'
|
10
|
+
require 'activefacts/compositions'
|
11
|
+
require 'activefacts/generator'
|
12
|
+
require 'activefacts/compositions/traits/rails'
|
13
|
+
|
14
|
+
module ActiveFacts
|
15
|
+
module Generators
|
16
|
+
module Rails
|
17
|
+
class Schema
|
18
|
+
MM = ActiveFacts::Metamodel unless const_defined?(:MM)
|
19
|
+
HEADER = "# Auto-generated from CQL, edits will be lost"
|
20
|
+
def self.options
|
21
|
+
({
|
22
|
+
exclude_fks: ['Boolean', "Don't generate foreign key definitions"],
|
23
|
+
include_comments: ['Boolean', "Generate a comment for each column showing the absorption path"],
|
24
|
+
closed_world: ['Boolean', "Set this if your DBMS only allows one null in a unique index (MS SQL)"],
|
25
|
+
})
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize composition, options = {}
|
29
|
+
@composition = composition
|
30
|
+
@options = options
|
31
|
+
@option_exclude_fks = options.delete("exclude_fks")
|
32
|
+
@option_include_comments = options.delete("include_comments")
|
33
|
+
@option_closed_world = options.delete("closed_world")
|
34
|
+
end
|
35
|
+
|
36
|
+
def warn *a
|
37
|
+
$stderr.puts *a
|
38
|
+
end
|
39
|
+
|
40
|
+
def data_type_context
|
41
|
+
@data_type_context ||= RailsDataTypeContext.new
|
42
|
+
end
|
43
|
+
|
44
|
+
def generate
|
45
|
+
@foreign_keys = []
|
46
|
+
# If we get index names that need to be truncated, add a counter to ensure uniqueness
|
47
|
+
@dup_id = 0
|
48
|
+
|
49
|
+
tables =
|
50
|
+
@composition.
|
51
|
+
all_composite.
|
52
|
+
sort_by{|composite| composite.mapping.name}.
|
53
|
+
map{|composite| generate_composite composite}.
|
54
|
+
compact
|
55
|
+
|
56
|
+
header =
|
57
|
+
[
|
58
|
+
'#',
|
59
|
+
"# schema.rb auto-generated for #{@composition.name}",
|
60
|
+
'#',
|
61
|
+
'',
|
62
|
+
"ActiveRecord::Base.logger = Logger.new(STDOUT)",
|
63
|
+
"ActiveRecord::Schema.define(version: #{Time.now.strftime('%Y%m%d%H%M%S')}) do",
|
64
|
+
" enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')",
|
65
|
+
'',
|
66
|
+
]
|
67
|
+
foreign_keys =
|
68
|
+
if @option_exclude_fks
|
69
|
+
[
|
70
|
+
'end'
|
71
|
+
]
|
72
|
+
else
|
73
|
+
[
|
74
|
+
' unless ENV["EXCLUDE_FKS"]',
|
75
|
+
*@foreign_keys.sort,
|
76
|
+
' end',
|
77
|
+
'end'
|
78
|
+
]
|
79
|
+
end
|
80
|
+
|
81
|
+
(
|
82
|
+
header +
|
83
|
+
tables +
|
84
|
+
foreign_keys
|
85
|
+
)*"\n"+"\n"
|
86
|
+
end
|
87
|
+
|
88
|
+
def generate_composite composite
|
89
|
+
ar_table_name = composite.rails.plural_name
|
90
|
+
|
91
|
+
pk = composite.primary_index.all_index_field.to_a
|
92
|
+
if pk[0].component.is_auto_assigned
|
93
|
+
identity_column = pk[0].component
|
94
|
+
warn "Warning: redundant column(s) after #{identity_column.name} in primary key of #{ar_table_name}" if pk.size > 1
|
95
|
+
end
|
96
|
+
|
97
|
+
# Detect if this table is a join table.
|
98
|
+
# Join tables have multi-part primary keys that are made up only of foreign keys
|
99
|
+
is_join_table = pk.length > 1 and
|
100
|
+
!pk.detect do |pk_field|
|
101
|
+
pk_field.component.all_foreign_key_field.size == 0
|
102
|
+
end
|
103
|
+
warn "Warning: #{table.name} has a multi-part primary key" if pk.length > 1 and !is_join_table
|
104
|
+
|
105
|
+
create_table = %Q{ create_table "#{ar_table_name}", id: false, force: true do |t|}
|
106
|
+
columns = generate_columns composite
|
107
|
+
|
108
|
+
unless @option_exclude_fks
|
109
|
+
composite.all_foreign_key_as_source_composite.each do |fk|
|
110
|
+
from_column_names = fk.all_foreign_key_field.map{|fxf| fxf.component.column_name.snakecase}
|
111
|
+
to_column_names = fk.all_index_field.map{|ixf| ixf.component.column_name.snakecase}
|
112
|
+
|
113
|
+
@foreign_keys.concat(
|
114
|
+
if (from_column_names.length == 1)
|
115
|
+
index_name = ACTR::name_trunc("index_#{ar_table_name}_on_#{from_column_names[0]}")
|
116
|
+
[
|
117
|
+
" add_foreign_key :#{ar_table_name}, :#{fk.composite.mapping.rails.plural_name}, column: :#{from_column_names[0]}, primary_key: :#{to_column_names[0]}, on_delete: :cascade",
|
118
|
+
# Index it non-uniquely only if it's not unique already:
|
119
|
+
fk.absorption && fk.absorption.child_role.is_unique ? nil :
|
120
|
+
" add_index :#{ar_table_name}, [:#{from_column_names[0]}], unique: false, name: :#{index_name}"
|
121
|
+
].compact
|
122
|
+
else
|
123
|
+
[ ]
|
124
|
+
end
|
125
|
+
)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
index_texts = []
|
130
|
+
composite.all_index.each do |index|
|
131
|
+
next if index.composite_as_primary_index && index.all_index_field.size == 1 # We've handled this already
|
132
|
+
|
133
|
+
index_column_names = index.all_index_field.map{|ixf| ixf.component.column_name.snakecase}
|
134
|
+
index_name = ACTR::name_trunc("index_#{ar_table_name}_on_#{index_column_names*'_'}")
|
135
|
+
|
136
|
+
index_texts << '' if index_texts.empty?
|
137
|
+
|
138
|
+
all_mandatory = index.all_index_field.to_a.all?{|ixf| ixf.component.path_mandatory}
|
139
|
+
index_texts << %Q{ add_index "#{ar_table_name}", #{index_column_names.inspect}, name: :#{index_name}#{
|
140
|
+
# Avoid problems with closed-world uniqueness: only all_mandatory indices can be unique on closed-world index semantics (MS SQL)
|
141
|
+
index.is_unique && (!@option_closed_world || all_mandatory) ? ", unique: true" : ''
|
142
|
+
}}
|
143
|
+
end
|
144
|
+
|
145
|
+
[
|
146
|
+
create_table,
|
147
|
+
*columns,
|
148
|
+
" end",
|
149
|
+
*index_texts.sort
|
150
|
+
]*"\n"+"\n"
|
151
|
+
end
|
152
|
+
|
153
|
+
def generate_columns composite
|
154
|
+
composite.mapping.all_leaf.flat_map do |component|
|
155
|
+
# Absorbed empty subtypes appear as leaves
|
156
|
+
next [] if component.is_a?(MM::Absorption) && component.parent_role.fact_type.is_a?(MM::TypeInheritance)
|
157
|
+
generate_column component
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def generate_column component
|
162
|
+
type_name, options = component.data_type(data_type_context)
|
163
|
+
options ||= {}
|
164
|
+
length = options[:length]
|
165
|
+
value_constraint = options[:value_constraint]
|
166
|
+
type, type_name = *normalise_type(type_name)
|
167
|
+
|
168
|
+
if a = options[:auto_assign]
|
169
|
+
case type_name
|
170
|
+
when 'integer'
|
171
|
+
type_name = 'primary_key' if a != 'assert'
|
172
|
+
when 'uuid'
|
173
|
+
type_name = "uuid, default: 'gen_random_uuid()', primary_key: true"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
valid_parameters = MM::DataType::TypeParameters[type]
|
178
|
+
length_ok = valid_parameters &&
|
179
|
+
![MM::DataType::TYPE_Real, MM::DataType::TYPE_Integer].include?(type) &&
|
180
|
+
(valid_parameters.include?(:length) || valid_parameters.include?(:precision))
|
181
|
+
scale_ok = length_ok && valid_parameters.include?(:scale)
|
182
|
+
length_option = length_ok && options[:length] ? ", limit: #{options[:length]}" : ''
|
183
|
+
scale_option = scale_ok && options[:scale] ? ", scale: #{options[:scale]}" : ''
|
184
|
+
null_option = ", null: #{!options[:mandatory]}"
|
185
|
+
|
186
|
+
(@option_include_comments ? [" \# #{component.comment}"] : []) +
|
187
|
+
[%Q{ t.column "#{component.column_name.snakecase}", :#{type_name}#{length_option}#{scale_option}#{null_option}}]
|
188
|
+
end
|
189
|
+
|
190
|
+
class RailsDataTypeContext < MM::DataType::Context
|
191
|
+
def integer_ranges
|
192
|
+
[
|
193
|
+
['integer', -2**63, 2**63-1]
|
194
|
+
]
|
195
|
+
end
|
196
|
+
|
197
|
+
def default_length data_type, type_name
|
198
|
+
case data_type
|
199
|
+
when MM::DataType::TYPE_Real
|
200
|
+
53 # IEEE Double precision floating point
|
201
|
+
when MM::DataType::TYPE_Integer
|
202
|
+
63
|
203
|
+
else
|
204
|
+
nil
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def default_surrogate_length
|
209
|
+
64
|
210
|
+
end
|
211
|
+
|
212
|
+
def boolean_type
|
213
|
+
'boolean'
|
214
|
+
end
|
215
|
+
|
216
|
+
def surrogate_type
|
217
|
+
type_name, = choose_integer_type(0, 2**(default_surrogate_length-1)-1)
|
218
|
+
type_name
|
219
|
+
end
|
220
|
+
|
221
|
+
def valid_from_type
|
222
|
+
date_time_type
|
223
|
+
end
|
224
|
+
|
225
|
+
def date_time_type
|
226
|
+
'datetime'
|
227
|
+
end
|
228
|
+
|
229
|
+
def default_char_type
|
230
|
+
'string'
|
231
|
+
end
|
232
|
+
|
233
|
+
def default_varchar_type
|
234
|
+
'string'
|
235
|
+
end
|
236
|
+
|
237
|
+
def default_text_type
|
238
|
+
default_varchar_type
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# Return SQL type and (modified?) length for the passed base type
|
243
|
+
def normalise_type type_name
|
244
|
+
type = MM::DataType.normalise(type_name)
|
245
|
+
|
246
|
+
[
|
247
|
+
type,
|
248
|
+
case type
|
249
|
+
when MM::DataType::TYPE_Boolean; 'boolean'
|
250
|
+
when MM::DataType::TYPE_Integer; 'integer'
|
251
|
+
when MM::DataType::TYPE_Real; 'float'
|
252
|
+
when MM::DataType::TYPE_Decimal; 'decimal'
|
253
|
+
when MM::DataType::TYPE_Money; 'datatime'
|
254
|
+
when MM::DataType::TYPE_Char; 'string'
|
255
|
+
when MM::DataType::TYPE_String; 'string'
|
256
|
+
when MM::DataType::TYPE_Text; 'text'
|
257
|
+
when MM::DataType::TYPE_Date; 'datetime'
|
258
|
+
when MM::DataType::TYPE_Time; 'time'
|
259
|
+
when MM::DataType::TYPE_DateTime; 'datetime'
|
260
|
+
when MM::DataType::TYPE_Timestamp;'datetime'
|
261
|
+
when MM::DataType::TYPE_Binary; 'binary'
|
262
|
+
else
|
263
|
+
type_name
|
264
|
+
end
|
265
|
+
]
|
266
|
+
end
|
267
|
+
|
268
|
+
end
|
269
|
+
end
|
270
|
+
publish_generator Rails::Schema
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
@@ -12,110 +12,118 @@ module ActiveFacts
|
|
12
12
|
module Generators
|
13
13
|
# Options are comma or space separated:
|
14
14
|
class Ruby < ObjectOriented
|
15
|
+
def self.options
|
16
|
+
super.merge(
|
17
|
+
{
|
18
|
+
scope: [String, "Generate a Ruby module that's nested inside the module you name here"]
|
19
|
+
}
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
15
23
|
def initialize composition, options = {}
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
24
|
+
super
|
25
|
+
@scope = options.delete('scope') || ''
|
26
|
+
@scope = @scope.split(/::/)
|
27
|
+
@scope_prefix = ' '*@scope.size
|
20
28
|
end
|
21
29
|
|
22
30
|
def prelude composition
|
23
|
-
|
24
|
-
|
25
|
-
|
31
|
+
"require 'activefacts/api'\n\n" +
|
32
|
+
(0...@scope.size).map{|i| ' '*i + "module #{@scope[i]}\n"}*'' +
|
33
|
+
"#{@scope_prefix}module #{composition.name.words.capcase}\n"
|
26
34
|
end
|
27
35
|
|
28
36
|
def finale
|
29
|
-
|
37
|
+
@scope.size.downto(0).map{|i| ' '*i+"end\n"}*''
|
30
38
|
end
|
31
39
|
|
32
40
|
def generate_classes composites
|
33
|
-
|
34
|
-
|
41
|
+
super(composites).
|
42
|
+
gsub(/^/, ' '*@scope.size)
|
35
43
|
end
|
36
44
|
|
37
45
|
def identified_by_roles identifying_roles
|
38
|
-
|
46
|
+
" identified_by #{ identifying_roles.map{|m| ':'+ruby_role_name(m) }*', ' }\n"
|
39
47
|
end
|
40
48
|
|
41
49
|
def value_type_declaration object_type
|
42
|
-
|
50
|
+
" value_type#{object_type.length ? " length: #{object_type.length}" : ''}\n" # REVISIT: Add other parameters and value restrictions
|
43
51
|
end
|
44
52
|
|
45
53
|
def class_prelude(object_type, supertype)
|
46
|
-
|
47
|
-
|
54
|
+
global_qualifier = object_type == supertype ? '::' :''
|
55
|
+
" class #{object_type.name.words.capcase}" + (supertype ? " < #{global_qualifier}#{supertype.name.words.capcase}" : '') + "\n"
|
48
56
|
end
|
49
57
|
|
50
58
|
def class_finale(object_type)
|
51
|
-
|
59
|
+
" end\n"
|
52
60
|
end
|
53
61
|
|
54
62
|
def role_definition component
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
63
|
+
role_name = ruby_role_name component
|
64
|
+
|
65
|
+
# Is the role mandatory?
|
66
|
+
mandatory = component.is_mandatory ? ', mandatory: true' : ''
|
67
|
+
|
68
|
+
# Does the role name imply the matching class name?
|
69
|
+
if component.is_a?(MM::Absorption) and
|
70
|
+
counterpart = component.object_type and
|
71
|
+
counterpart_composite = composite_for(counterpart)
|
72
|
+
counterpart_class_emitted = @composites_emitted[counterpart_composite]
|
73
|
+
|
74
|
+
counterpart_class_name = ruby_class_name counterpart_composite
|
75
|
+
counterpart_default_role = ruby_role_name counterpart_composite.mapping
|
76
|
+
rolename_implies_class = role_name.words.capcase == counterpart_class_name
|
77
|
+
class_ref = counterpart_class_emitted ? counterpart_class_name : counterpart_class_name.inspect
|
78
|
+
class_spec = rolename_implies_class ? '' : ", class: #{class_ref}"
|
79
|
+
|
80
|
+
# Does the reverse role need explicit specification?
|
81
|
+
implied_reverse_role_name = ruby_role_name(component.root.mapping)
|
82
|
+
actual_reverse_role_name = ruby_role_name component.reverse_absorption
|
83
|
+
|
84
|
+
if implied_reverse_role_name != actual_reverse_role_name
|
85
|
+
counterpart_spec = ", counterpart: :#{actual_reverse_role_name}"
|
86
|
+
elsif !rolename_implies_class
|
87
|
+
# _as_XYZ is added where the forward role does not imply the class, and :counterpart role name is not specified
|
88
|
+
actual_reverse_role_name += "_as_#{role_name}"
|
89
|
+
end
|
90
|
+
all = component.child_role.is_unique ? '' : 'all_'
|
91
|
+
see = ", see #{counterpart_class_name}\##{all+actual_reverse_role_name}"
|
92
|
+
end
|
93
|
+
|
94
|
+
counterpart_comment = "# #{comment component}#{see}"
|
95
|
+
|
96
|
+
definition =
|
97
|
+
" #{role_specifier component}"
|
98
|
+
definition += ' '*(20-definition.length) if definition.length < 20
|
99
|
+
definition += ":#{ruby_role_name component}#{mandatory}#{class_spec}#{counterpart_spec} "
|
100
|
+
definition += ' '*(56-definition.length) if definition.length < 56
|
101
|
+
definition += "#{counterpart_comment}"
|
102
|
+
definition += "\n"
|
95
103
|
end
|
96
104
|
|
97
105
|
def role_specifier component
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
106
|
+
if component.is_a?(MM::Indicator)
|
107
|
+
'maybe'
|
108
|
+
else
|
109
|
+
if component.is_a?(MM::Absorption) and component.child_role.is_unique
|
110
|
+
'one_to_one'
|
111
|
+
else
|
112
|
+
'has_one'
|
113
|
+
end
|
114
|
+
end
|
107
115
|
end
|
108
116
|
|
109
117
|
def ruby_role_name component
|
110
|
-
|
118
|
+
component.name.words.snakecase
|
111
119
|
end
|
112
120
|
|
113
121
|
def ruby_class_name composite
|
114
|
-
|
122
|
+
composite.mapping.name.words.capcase
|
115
123
|
end
|
116
124
|
|
117
125
|
def comment component
|
118
|
-
|
126
|
+
super
|
119
127
|
end
|
120
128
|
end
|
121
129
|
publish_generator Ruby
|