activefacts-compositions 1.9.5 → 1.9.6
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 +4 -4
- data/Gemfile +7 -5
- data/README.md +12 -17
- data/activefacts-compositions.gemspec +7 -6
- data/bin/schema_compositor +86 -68
- data/lib/activefacts/compositions.rb +7 -0
- data/lib/activefacts/compositions/binary.rb +3 -1
- data/lib/activefacts/compositions/compositor.rb +5 -2
- data/lib/activefacts/compositions/constraints.rb +86 -0
- data/lib/activefacts/compositions/names.rb +80 -0
- data/lib/activefacts/compositions/relational.rb +2 -2
- data/lib/activefacts/compositions/version.rb +1 -1
- data/lib/activefacts/generator.rb +14 -0
- data/lib/activefacts/generator/oo.rb +190 -0
- data/lib/activefacts/generator/ruby.rb +123 -0
- data/lib/activefacts/generator/sql.rb +431 -0
- data/lib/activefacts/generator/sql/server.rb +62 -0
- data/lib/activefacts/generator/summary.rb +98 -0
- data/lib/activefacts/{compositions/validator.rb → generator/validate.rb} +94 -74
- data/lib/activefacts/loadable.rb +28 -0
- metadata +32 -22
@@ -13,8 +13,6 @@ require "activefacts/compositions"
|
|
13
13
|
module ActiveFacts
|
14
14
|
module Compositions
|
15
15
|
class Relational < Compositor
|
16
|
-
private
|
17
|
-
MM = ActiveFacts::Metamodel
|
18
16
|
public
|
19
17
|
def initialize constellation, name, options = {}
|
20
18
|
# Extract recognised options so our superclass doesn't complain:
|
@@ -802,5 +800,7 @@ module ActiveFacts
|
|
802
800
|
end
|
803
801
|
|
804
802
|
end
|
803
|
+
|
804
|
+
publish_compositor(Relational)
|
805
805
|
end
|
806
806
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "activefacts/metamodel"
|
2
|
+
require "activefacts/compositions/version"
|
3
|
+
|
4
|
+
module ActiveFacts
|
5
|
+
module Generators
|
6
|
+
def self.generators
|
7
|
+
@@generators ||= {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.publish_generator klass
|
11
|
+
generators[klass.name.sub(/^ActiveFacts::Generators::/,'').gsub(/::/, '/').downcase] = klass
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Object-Oriented API 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/registry'
|
9
|
+
require 'activefacts/compositions'
|
10
|
+
require 'activefacts/generator'
|
11
|
+
|
12
|
+
module ActiveFacts
|
13
|
+
module Generators
|
14
|
+
# Options are comma or space separated:
|
15
|
+
class ObjectOriented
|
16
|
+
def initialize composition, options = {}
|
17
|
+
@composition = composition
|
18
|
+
@options = options
|
19
|
+
@comments = @options.delete("comments")
|
20
|
+
end
|
21
|
+
|
22
|
+
def generate
|
23
|
+
@composites_emitted = {}
|
24
|
+
|
25
|
+
retract_intrinsic_types
|
26
|
+
|
27
|
+
composites =
|
28
|
+
@composition.
|
29
|
+
all_composite.
|
30
|
+
sort_by{|composite| composite.mapping.name}
|
31
|
+
|
32
|
+
prelude(@composition) +
|
33
|
+
generate_classes(composites) +
|
34
|
+
finale
|
35
|
+
end
|
36
|
+
|
37
|
+
def generate_classes composites
|
38
|
+
composites.
|
39
|
+
map do |composite|
|
40
|
+
generate_class(composite)
|
41
|
+
end.
|
42
|
+
compact.
|
43
|
+
join("\n")
|
44
|
+
end
|
45
|
+
|
46
|
+
def composite_for object_type
|
47
|
+
@composition.all_composite.detect{|c| c.mapping.object_type == object_type }
|
48
|
+
end
|
49
|
+
|
50
|
+
# We don't need Composites for object types that are built-in to the Ruby API.
|
51
|
+
def is_intrinsic_type composite
|
52
|
+
o = composite.mapping.object_type
|
53
|
+
return true if o.name == "_ImplicitBooleanValueType"
|
54
|
+
return false if o.supertype
|
55
|
+
# A value type with no supertype must be emitted if it is the child in any absorption:
|
56
|
+
return !composite.mapping.all_member.detect{|m| m.forward_absorption}
|
57
|
+
end
|
58
|
+
|
59
|
+
def retract_intrinsic_types
|
60
|
+
@composition.
|
61
|
+
all_composite.
|
62
|
+
sort_by{|composite| composite.mapping.name}.
|
63
|
+
each do |composite|
|
64
|
+
o = composite.mapping.object_type
|
65
|
+
next unless o.is_a?(MM::ValueType)
|
66
|
+
composite.retract and next if is_intrinsic_type(composite)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def inherited_identification
|
71
|
+
''
|
72
|
+
end
|
73
|
+
|
74
|
+
def identified_by_roles identifying_roles
|
75
|
+
"REVISIT: override identified_by_roles\n"
|
76
|
+
end
|
77
|
+
|
78
|
+
def value_type_declaration object_type
|
79
|
+
"REVISIT: override value_type_declaration\n"
|
80
|
+
end
|
81
|
+
|
82
|
+
def class_prelude(object_type, supertype)
|
83
|
+
"REVISIT: override class_prelude\n"
|
84
|
+
end
|
85
|
+
|
86
|
+
def class_finale(object_type)
|
87
|
+
"REVISIT: override class_finale\n"
|
88
|
+
end
|
89
|
+
|
90
|
+
def generate_class composite, predefine_role_players = true
|
91
|
+
return nil if @composites_emitted[composite]
|
92
|
+
|
93
|
+
mapping = composite.mapping
|
94
|
+
object_type = mapping.object_type
|
95
|
+
is_entity_type = object_type.is_a?(MM::EntityType)
|
96
|
+
forward_declarations = []
|
97
|
+
|
98
|
+
# Emit supertypes before subtypes
|
99
|
+
supertype_composites =
|
100
|
+
object_type.all_supertype.map{|s| composite_for(s) }.compact
|
101
|
+
forward_declarations +=
|
102
|
+
supertype_composites.map{|c| generate_class(c, false)}.compact
|
103
|
+
|
104
|
+
@composites_emitted[composite] = true
|
105
|
+
|
106
|
+
# Select the members that will be declared as O-O roles:
|
107
|
+
mapping.re_rank
|
108
|
+
members = mapping.
|
109
|
+
all_member.
|
110
|
+
sort_by{|m| m.ordinal}.
|
111
|
+
reject do |m|
|
112
|
+
m.is_a?(MM::Absorption) and
|
113
|
+
m.forward_absorption || m.child_role.fact_type.is_a?(MM::TypeInheritance)
|
114
|
+
end
|
115
|
+
|
116
|
+
if predefine_role_players
|
117
|
+
# The idea was good, but we need to avoid triggering a forward reference problem.
|
118
|
+
# We only do it when we're not dumping a supertype dependency.
|
119
|
+
#
|
120
|
+
# For those roles that derive from Mappings, produce class definitions to avoid forward references:
|
121
|
+
forward_composites =
|
122
|
+
members.
|
123
|
+
select{ |m| m.is_a?(MM::Mapping) }.
|
124
|
+
map{ |m| composite_for m.object_type }.
|
125
|
+
compact.
|
126
|
+
sort_by{|c| c.mapping.name}
|
127
|
+
forward_declarations +=
|
128
|
+
forward_composites.map{|c| generate_class(c)}.compact
|
129
|
+
end
|
130
|
+
|
131
|
+
forward_declarations = forward_declarations.map{|f| "#{f}\n"}*''
|
132
|
+
|
133
|
+
primary_supertype =
|
134
|
+
if is_entity_type
|
135
|
+
object_type.identifying_supertype ||
|
136
|
+
object_type.supertypes[0] # Hopefully there's only one!
|
137
|
+
else
|
138
|
+
object_type.supertype || object_type
|
139
|
+
end
|
140
|
+
|
141
|
+
type_declaration =
|
142
|
+
if is_entity_type
|
143
|
+
if primary_supertype and object_type.identification_is_inherited
|
144
|
+
inherited_identification
|
145
|
+
else
|
146
|
+
identifying_roles =
|
147
|
+
if object_type.fact_type && object_type.fact_type.is_unary
|
148
|
+
# Objectified unary; find the absorption over the LinkFactType
|
149
|
+
members.
|
150
|
+
select{|m| m.is_a?(MM::Absorption) && m.child_role.base_role.fact_type.entity_type}.
|
151
|
+
map{|m| m.child_role}
|
152
|
+
else
|
153
|
+
object_type.preferred_identifier.role_sequence.all_role_ref.map(&:role).
|
154
|
+
map do |role|
|
155
|
+
members.detect{|m| m.all_role.include?(role)}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
identified_by_roles identifying_roles
|
159
|
+
end
|
160
|
+
else
|
161
|
+
value_type_declaration object_type
|
162
|
+
end
|
163
|
+
|
164
|
+
forward_declarations +
|
165
|
+
class_prelude(object_type, primary_supertype) +
|
166
|
+
type_declaration +
|
167
|
+
members.
|
168
|
+
map do |component|
|
169
|
+
(@comments ? comment(component) + "\n" : '') +
|
170
|
+
role_definition(component)
|
171
|
+
end*'' +
|
172
|
+
class_finale(object_type)
|
173
|
+
end
|
174
|
+
|
175
|
+
def role_definition component
|
176
|
+
"REVISIT: override role_definition\n"
|
177
|
+
end
|
178
|
+
|
179
|
+
def comment component
|
180
|
+
if component.is_a?(MM::Absorption)
|
181
|
+
component.parent_role.fact_type.reading_preferably_starting_with_role(component.parent_role).expand([], false)
|
182
|
+
else
|
183
|
+
component.name
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
MM = ActiveFacts::Metamodel
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Ruby API Generator
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009-2016 Clifford Heath. Read the LICENSE file.
|
5
|
+
#
|
6
|
+
require 'activefacts/metamodel'
|
7
|
+
require 'activefacts/compositions'
|
8
|
+
require 'activefacts/generator'
|
9
|
+
require 'activefacts/generator/oo'
|
10
|
+
|
11
|
+
module ActiveFacts
|
12
|
+
module Generators
|
13
|
+
# Options are comma or space separated:
|
14
|
+
class Ruby < ObjectOriented
|
15
|
+
def initialize composition, options = {}
|
16
|
+
super
|
17
|
+
@scope = options.delete('scope') || ''
|
18
|
+
@scope = @scope.split(/::/)
|
19
|
+
@scope_prefix = ' '*@scope.size
|
20
|
+
end
|
21
|
+
|
22
|
+
def prelude composition
|
23
|
+
"require 'activefacts/api'\n\n" +
|
24
|
+
(0...@scope.size).map{|i| ' '*i + "module #{@scope[i]}\n"}*'' +
|
25
|
+
"#{@scope_prefix}module #{composition.name}\n"
|
26
|
+
end
|
27
|
+
|
28
|
+
def finale
|
29
|
+
@scope.size.downto(0).map{|i| ' '*i+"end\n"}*''
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate_classes composites
|
33
|
+
super(composites).
|
34
|
+
gsub(/^/, ' '*@scope.size)
|
35
|
+
end
|
36
|
+
|
37
|
+
def identified_by_roles identifying_roles
|
38
|
+
" identified_by #{ identifying_roles.map{|m| ':'+ruby_role_name(m) }*', ' }\n"
|
39
|
+
end
|
40
|
+
|
41
|
+
def value_type_declaration object_type
|
42
|
+
" value_type#{object_type.length ? " length: #{object_type.length}" : ''}\n" # REVISIT: Add other parameters and value restrictions
|
43
|
+
end
|
44
|
+
|
45
|
+
def class_prelude(object_type, supertype)
|
46
|
+
global_qualifier = object_type == supertype ? '::' :''
|
47
|
+
" class #{object_type.name.words.capcase}" + (supertype ? " < #{global_qualifier}#{supertype.name.words.capcase}" : '') + "\n"
|
48
|
+
end
|
49
|
+
|
50
|
+
def class_finale(object_type)
|
51
|
+
" end\n"
|
52
|
+
end
|
53
|
+
|
54
|
+
def role_definition component
|
55
|
+
role_name = ruby_role_name component
|
56
|
+
|
57
|
+
# Is the role mandatory?
|
58
|
+
mandatory = component.is_mandatory ? ', mandatory: true' : ''
|
59
|
+
|
60
|
+
# Does the role name imply the matching class name?
|
61
|
+
if component.is_a?(MM::Absorption) and
|
62
|
+
counterpart = component.object_type and
|
63
|
+
counterpart_composite = composite_for(counterpart)
|
64
|
+
counterpart_class_emitted = @composites_emitted[counterpart_composite]
|
65
|
+
|
66
|
+
counterpart_class_name = ruby_class_name counterpart_composite
|
67
|
+
counterpart_default_role = ruby_role_name counterpart_composite.mapping
|
68
|
+
rolename_implies_class = role_name.words.capcase == counterpart_class_name
|
69
|
+
class_ref = counterpart_class_emitted ? counterpart_class_name : counterpart_class_name.inspect
|
70
|
+
class_spec = rolename_implies_class ? '' : ", class: #{class_ref}"
|
71
|
+
|
72
|
+
# Does the reverse role need explicit specification?
|
73
|
+
implied_reverse_role_name = ruby_role_name(component.root.mapping)
|
74
|
+
actual_reverse_role_name = ruby_role_name component.reverse_absorption
|
75
|
+
|
76
|
+
if implied_reverse_role_name != actual_reverse_role_name
|
77
|
+
counterpart_spec = ", counterpart: :#{actual_reverse_role_name}"
|
78
|
+
elsif !rolename_implies_class
|
79
|
+
# _as_XYZ is added where the forward role does not imply the class, and :counterpart role name is not specified
|
80
|
+
actual_reverse_role_name += "_as_#{role_name}"
|
81
|
+
end
|
82
|
+
all = component.child_role.is_unique ? '' : 'all_'
|
83
|
+
see = ", see #{counterpart_class_name}\##{all+actual_reverse_role_name}"
|
84
|
+
end
|
85
|
+
|
86
|
+
counterpart_comment = "# #{comment component}#{see}"
|
87
|
+
|
88
|
+
definition =
|
89
|
+
" #{role_specifier component}"
|
90
|
+
definition += ' '*(20-definition.length) if definition.length < 20
|
91
|
+
definition += ":#{ruby_role_name component}#{mandatory}#{class_spec}#{counterpart_spec} "
|
92
|
+
definition += ' '*(56-definition.length) if definition.length < 56
|
93
|
+
definition += "#{counterpart_comment}"
|
94
|
+
definition += "\n"
|
95
|
+
end
|
96
|
+
|
97
|
+
def role_specifier component
|
98
|
+
if component.is_a?(MM::Indicator)
|
99
|
+
'maybe'
|
100
|
+
else
|
101
|
+
if component.is_a?(MM::Absorption) and component.child_role.is_unique
|
102
|
+
'one_to_one'
|
103
|
+
else
|
104
|
+
'has_one'
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def ruby_role_name component
|
110
|
+
component.name.words.snakecase
|
111
|
+
end
|
112
|
+
|
113
|
+
def ruby_class_name composite
|
114
|
+
composite.mapping.name.words.capcase
|
115
|
+
end
|
116
|
+
|
117
|
+
def comment component
|
118
|
+
super
|
119
|
+
end
|
120
|
+
end
|
121
|
+
publish_generator Ruby
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,431 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Standard SQL 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/registry'
|
9
|
+
require 'activefacts/compositions'
|
10
|
+
require 'activefacts/generator'
|
11
|
+
|
12
|
+
module ActiveFacts
|
13
|
+
module Generators
|
14
|
+
# Options are comma or space separated:
|
15
|
+
# * delay_fks Leave all foreign keys until the end, not just those that contain forward-references
|
16
|
+
# * underscore
|
17
|
+
class SQL
|
18
|
+
def initialize composition, options = {}
|
19
|
+
@composition = composition
|
20
|
+
@options = options
|
21
|
+
@delay_fks = options.include? "delay_fks"
|
22
|
+
@underscore = options.include?("underscore") ? "_" : ""
|
23
|
+
end
|
24
|
+
|
25
|
+
def generate
|
26
|
+
@tables_emitted = {}
|
27
|
+
@delayed_foreign_keys = []
|
28
|
+
|
29
|
+
generate_schema +
|
30
|
+
@composition.
|
31
|
+
all_composite.
|
32
|
+
sort_by{|composite| composite.mapping.name}.
|
33
|
+
map{|composite| generate_table composite}*"\n" + "\n" +
|
34
|
+
@delayed_foreign_keys.sort*"\n"
|
35
|
+
end
|
36
|
+
|
37
|
+
def table_name_max
|
38
|
+
60
|
39
|
+
end
|
40
|
+
|
41
|
+
def column_name_max
|
42
|
+
40
|
43
|
+
end
|
44
|
+
|
45
|
+
def index_name_max
|
46
|
+
60
|
47
|
+
end
|
48
|
+
|
49
|
+
def schema_name_max
|
50
|
+
60
|
51
|
+
end
|
52
|
+
|
53
|
+
def safe_table_name composite
|
54
|
+
escape(table_name(composite), table_name_max)
|
55
|
+
end
|
56
|
+
|
57
|
+
def safe_column_name component
|
58
|
+
escape(column_name(component), column_name_max)
|
59
|
+
end
|
60
|
+
|
61
|
+
def table_name composite
|
62
|
+
composite.mapping.name.gsub(' ', @underscore)
|
63
|
+
end
|
64
|
+
|
65
|
+
def column_name component
|
66
|
+
component.column_name.capcase
|
67
|
+
end
|
68
|
+
|
69
|
+
def generate_schema
|
70
|
+
#go "CREATE SCHEMA #{escape(@composition.name, schema_name_max)}" +
|
71
|
+
''
|
72
|
+
end
|
73
|
+
|
74
|
+
def generate_table composite
|
75
|
+
@tables_emitted[composite] = true
|
76
|
+
delayed_indices = []
|
77
|
+
|
78
|
+
"CREATE TABLE #{safe_table_name composite} (\n" +
|
79
|
+
(
|
80
|
+
composite.mapping.leaves.flat_map do |leaf|
|
81
|
+
# Absorbed empty subtypes appear as leaves
|
82
|
+
next if leaf.is_a?(MM::Absorption) && leaf.parent_role.fact_type.is_a?(MM::TypeInheritance)
|
83
|
+
|
84
|
+
generate_column leaf
|
85
|
+
end +
|
86
|
+
composite.all_index.map do |index|
|
87
|
+
generate_index index, delayed_indices
|
88
|
+
end.compact.sort +
|
89
|
+
composite.all_foreign_key_as_source_composite.map do |fk|
|
90
|
+
fk_text = generate_foreign_key fk
|
91
|
+
if !@delay_fks and @tables_emitted[fk.composite]
|
92
|
+
fk_text
|
93
|
+
else
|
94
|
+
@delayed_foreign_keys <<
|
95
|
+
go("ALTER TABLE #{safe_table_name fk.composite}\n\tADD " + fk_text)
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
end.compact.sort +
|
99
|
+
composite.all_local_constraint.map do |constraint|
|
100
|
+
'-- '+constraint.inspect # REVISIT: Emit local constraints
|
101
|
+
end
|
102
|
+
).compact.flat_map{|f| "\t#{f}" }*",\n"+"\n" +
|
103
|
+
go(")") +
|
104
|
+
delayed_indices.sort.map do |delayed_index|
|
105
|
+
go delayed_index
|
106
|
+
end*"\n"
|
107
|
+
end
|
108
|
+
|
109
|
+
def generate_column leaf
|
110
|
+
column_name = safe_column_name(leaf)
|
111
|
+
padding = " "*(column_name.size >= column_name_max ? 1 : column_name_max-column_name.size)
|
112
|
+
constraints = leaf.all_leaf_constraint
|
113
|
+
|
114
|
+
identity = ''
|
115
|
+
"-- #{column_comment leaf}\n\t#{column_name}#{padding}#{component_type leaf, column_name}#{identity}"
|
116
|
+
end
|
117
|
+
|
118
|
+
def column_comment component
|
119
|
+
return '' unless cp = component.parent
|
120
|
+
prefix = column_comment(cp)
|
121
|
+
name = component.name
|
122
|
+
if component.is_a?(MM::Absorption)
|
123
|
+
reading = component.parent_role.fact_type.reading_preferably_starting_with_role(component.parent_role).expand([], false)
|
124
|
+
maybe = component.parent_role.is_mandatory ? '' : 'maybe '
|
125
|
+
cpname = cp.name
|
126
|
+
if prefix[(-cpname.size-1)..-1] == ' '+cpname && reading[0..cpname.size] == cpname+' '
|
127
|
+
prefix+' that ' + maybe + reading[cpname.size+1..-1]
|
128
|
+
else
|
129
|
+
(prefix.empty? ? '' : prefix+' and ') + maybe + reading
|
130
|
+
end
|
131
|
+
else
|
132
|
+
name
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def boolean_type
|
137
|
+
'BOOLEAN'
|
138
|
+
end
|
139
|
+
|
140
|
+
def surrogate_type
|
141
|
+
'BIGINT IDENTITY NOT NULL'
|
142
|
+
end
|
143
|
+
|
144
|
+
def component_type component, column_name
|
145
|
+
case component
|
146
|
+
when MM::Indicator
|
147
|
+
boolean_type
|
148
|
+
when MM::SurrogateKey
|
149
|
+
surrogate_type
|
150
|
+
when MM::ValueField, MM::Absorption
|
151
|
+
object_type = component.object_type
|
152
|
+
while object_type.is_a?(MM::EntityType)
|
153
|
+
rr = object_type.preferred_identifier.role_sequence.all_role_ref.single
|
154
|
+
raise "Can't produce a column for composite #{component.inspect}" unless rr
|
155
|
+
object_type = rr.role.object_type
|
156
|
+
end
|
157
|
+
raise "A column can only be produced from a ValueType" unless object_type.is_a?(MM::ValueType)
|
158
|
+
|
159
|
+
if component.is_a?(MM::Absorption)
|
160
|
+
value_constraint ||= component.child_role.role_value_constraint
|
161
|
+
end
|
162
|
+
|
163
|
+
supertype = object_type
|
164
|
+
begin
|
165
|
+
object_type = supertype
|
166
|
+
length ||= object_type.length
|
167
|
+
scale ||= object_type.scale
|
168
|
+
unless component.parent.parent and component.parent.foreign_key
|
169
|
+
# No need to enforce value constraints that are already enforced by a foreign key
|
170
|
+
value_constraint ||= object_type.value_constraint
|
171
|
+
end
|
172
|
+
end while supertype = object_type.supertype
|
173
|
+
type, length = normalise_type(object_type.name, length)
|
174
|
+
sql_type = "#{type}#{
|
175
|
+
if !length
|
176
|
+
''
|
177
|
+
else
|
178
|
+
'(' + length.to_s + (scale ? ", #{scale}" : '') + ')'
|
179
|
+
end
|
180
|
+
}#{
|
181
|
+
(component.path_mandatory ? '' : ' NOT') + ' NULL'
|
182
|
+
}#{
|
183
|
+
# REVISIT: This is an SQL Server-ism. Replace with a standard SQL SEQUENCE/
|
184
|
+
# Emit IDENTITY for columns auto-assigned on commit (except FKs)
|
185
|
+
if a = object_type.is_auto_assigned and a != 'assert' and
|
186
|
+
!component.all_foreign_key_field.detect{|fkf| fkf.foreign_key.source_composite == component.root}
|
187
|
+
' IDENTITY'
|
188
|
+
else
|
189
|
+
''
|
190
|
+
end
|
191
|
+
}#{
|
192
|
+
value_constraint ? check_clause(column_name, value_constraint) : ''
|
193
|
+
}"
|
194
|
+
else
|
195
|
+
raise "Can't make a column from #{component}"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def generate_index index, delayed_indices
|
200
|
+
nullable_columns =
|
201
|
+
index.all_index_field.select do |ixf|
|
202
|
+
!ixf.component.path_mandatory
|
203
|
+
end
|
204
|
+
contains_nullable_columns = nullable_columns.size > 0
|
205
|
+
|
206
|
+
primary = index.composite_as_primary_index && !contains_nullable_columns
|
207
|
+
column_names =
|
208
|
+
index.all_index_field.map do |ixf|
|
209
|
+
column_name(ixf.component)
|
210
|
+
end
|
211
|
+
clustering =
|
212
|
+
(index.composite_as_primary_index ? ' CLUSTERED' : ' NONCLUSTERED')
|
213
|
+
|
214
|
+
if contains_nullable_columns
|
215
|
+
delayed_indices <<
|
216
|
+
'CREATE UNIQUE'+clustering+' INDEX '+
|
217
|
+
escape("#{safe_table_name(index.composite)}By#{column_names*''}", index_name_max) +
|
218
|
+
" ON ("+column_names.map{|n| escape(n, column_name_max)}*', ' +
|
219
|
+
") WHERE #{
|
220
|
+
nullable_columns.
|
221
|
+
map{|ixf| safe_column_name ixf.component}.
|
222
|
+
map{|column_name| column_name + ' IS NOT NULL'} *
|
223
|
+
' AND '
|
224
|
+
}"
|
225
|
+
nil
|
226
|
+
else
|
227
|
+
'-- '+index.inspect + "\n\t" +
|
228
|
+
(primary ? 'PRIMARY KEY' : 'UNIQUE') +
|
229
|
+
clustering +
|
230
|
+
"(#{column_names.map{|n| escape(n, column_name_max)}*', '})"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def generate_foreign_key fk
|
235
|
+
'-- '+fk.inspect
|
236
|
+
"FOREIGN KEY (" +
|
237
|
+
fk.all_foreign_key_field.map{|fkf| safe_column_name fkf.component}*", " +
|
238
|
+
") REFERENCES #{safe_table_name fk.composite} (" +
|
239
|
+
fk.all_index_field.map{|ixf| safe_column_name ixf.component}*", " +
|
240
|
+
")"
|
241
|
+
end
|
242
|
+
|
243
|
+
def reserved_words
|
244
|
+
@sql_server_reserved_words ||= %w{
|
245
|
+
ADD ALL ALTER AND ANY AS ASC AUTHORIZATION BACKUP BEGIN
|
246
|
+
BETWEEN BREAK BROWSE BULK BY CASCADE CASE CHECK CHECKPOINT
|
247
|
+
CLOSE CLUSTERED COALESCE COLLATE COLUMN COMMIT COMPUTE
|
248
|
+
CONSTRAINT CONTAINS CONTAINSTABLE CONTINUE CONVERT CREATE
|
249
|
+
CROSS CURRENT CURRENT_DATE CURRENT_TIME CURRENT_TIMESTAMP
|
250
|
+
CURRENT_USER CURSOR DATABASE DBCC DEALLOCATE DECLARE
|
251
|
+
DEFAULT DELETE DENY DESC DISK DISTINCT DISTRIBUTED DOUBLE
|
252
|
+
DROP DUMMY DUMP ELSE END ERRLVL ESCAPE EXCEPT EXEC EXECUTE
|
253
|
+
EXISTS EXIT FETCH FILE FILLFACTOR FOR FOREIGN FREETEXT
|
254
|
+
FREETEXTTABLE FROM FULL FUNCTION GOTO GRANT GROUP HAVING
|
255
|
+
HOLDLOCK IDENTITY IDENTITYCOL IDENTITY_INSERT IF IN INDEX
|
256
|
+
INNER INSERT INTERSECT INTO IS JOIN KEY KILL LEFT LIKE
|
257
|
+
LINENO LOAD NATIONAL NOCHECK NONCLUSTERED NOT NULL NULLIF
|
258
|
+
OF OFF OFFSETS ON OPEN OPENDATASOURCE OPENQUERY OPENROWSET
|
259
|
+
OPENXML OPTION OR ORDER OUTER OVER PERCENT PLAN PRECISION
|
260
|
+
PRIMARY PRINT PROC PROCEDURE PUBLIC RAISERROR READ READTEXT
|
261
|
+
RECONFIGURE REFERENCES REPLICATION RESTORE RESTRICT RETURN
|
262
|
+
REVOKE RIGHT ROLLBACK ROWCOUNT ROWGUIDCOL RULE SAVE SCHEMA
|
263
|
+
SELECT SESSION_USER SET SETUSER SHUTDOWN SOME STATISTICS
|
264
|
+
SYSTEM_USER TABLE TEXTSIZE THEN TO TOP TRAN TRANSACTION
|
265
|
+
TRIGGER TRUNCATE TSEQUAL UNION UNIQUE UPDATE UPDATETEXT
|
266
|
+
USE USER VALUES VARYING VIEW WAITFOR WHEN WHERE WHILE
|
267
|
+
WITH WRITETEXT
|
268
|
+
}
|
269
|
+
|
270
|
+
@reserved_words ||= %w{
|
271
|
+
ABSOLUTE ACTION ADD AFTER ALL ALLOCATE ALTER AND ANY ARE
|
272
|
+
ARRAY AS ASC ASSERTION AT AUTHORIZATION BEFORE BEGIN
|
273
|
+
BETWEEN BINARY BIT BLOB BOOLEAN BOTH BREADTH BY CALL
|
274
|
+
CASCADE CASCADED CASE CAST CATALOG CHAR CHARACTER CHECK
|
275
|
+
CLOB CLOSE COLLATE COLLATION COLUMN COMMIT CONDITION
|
276
|
+
CONNECT CONNECTION CONSTRAINT CONSTRAINTS CONSTRUCTOR
|
277
|
+
CONTINUE CORRESPONDING CREATE CROSS CUBE CURRENT CURRENT_DATE
|
278
|
+
CURRENT_DEFAULT_TRANSFORM_GROUP CURRENT_TRANSFORM_GROUP_FOR_TYPE
|
279
|
+
CURRENT_PATH CURRENT_ROLE CURRENT_TIME CURRENT_TIMESTAMP
|
280
|
+
CURRENT_USER CURSOR CYCLE DATA DATE DAY DEALLOCATE DEC
|
281
|
+
DECIMAL DECLARE DEFAULT DEFERRABLE DEFERRED DELETE DEPTH
|
282
|
+
DEREF DESC DESCRIBE DESCRIPTOR DETERMINISTIC DIAGNOSTICS
|
283
|
+
DISCONNECT DISTINCT DO DOMAIN DOUBLE DROP DYNAMIC EACH
|
284
|
+
ELSE ELSEIF END EQUALS ESCAPE EXCEPT EXCEPTION EXEC EXECUTE
|
285
|
+
EXISTS EXIT EXTERNAL FALSE FETCH FIRST FLOAT FOR FOREIGN
|
286
|
+
FOUND FROM FREE FULL FUNCTION GENERAL GET GLOBAL GO GOTO
|
287
|
+
GRANT GROUP GROUPING HANDLE HAVING HOLD HOUR IDENTITY IF
|
288
|
+
IMMEDIATE IN INDICATOR INITIALLY INNER INOUT INPUT INSERT
|
289
|
+
INT INTEGER INTERSECT INTERVAL INTO IS ISOLATION JOIN KEY
|
290
|
+
LANGUAGE LARGE LAST LATERAL LEADING LEAVE LEFT LEVEL LIKE
|
291
|
+
LOCAL LOCALTIME LOCALTIMESTAMP LOCATOR LOOP MAP MATCH
|
292
|
+
METHOD MINUTE MODIFIES MODULE MONTH NAMES NATIONAL NATURAL
|
293
|
+
NCHAR NCLOB NESTING NEW NEXT NO NONE NOT NULL NUMERIC
|
294
|
+
OBJECT OF OLD ON ONLY OPEN OPTION OR ORDER ORDINALITY OUT
|
295
|
+
OUTER OUTPUT OVERLAPS PAD PARAMETER PARTIAL PATH PRECISION
|
296
|
+
PREPARE PRESERVE PRIMARY PRIOR PRIVILEGES PROCEDURE PUBLIC
|
297
|
+
READ READS REAL RECURSIVE REDO REF REFERENCES REFERENCING
|
298
|
+
RELATIVE RELEASE REPEAT RESIGNAL RESTRICT RESULT RETURN
|
299
|
+
RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP ROUTINE ROW
|
300
|
+
ROWS SAVEPOINT SCHEMA SCROLL SEARCH SECOND SECTION SELECT
|
301
|
+
SESSION SESSION_USER SET SETS SIGNAL SIMILAR SIZE SMALLINT
|
302
|
+
SOME SPACE SPECIFIC SPECIFICTYPE SQL SQLEXCEPTION SQLSTATE
|
303
|
+
SQLWARNING START STATE STATIC SYSTEM_USER TABLE TEMPORARY
|
304
|
+
THEN TIME TIMESTAMP TIMEZONE_HOUR TIMEZONE_MINUTE TO
|
305
|
+
TRAILING TRANSACTION TRANSLATION TREAT TRIGGER TRUE UNDER
|
306
|
+
UNDO UNION UNIQUE UNKNOWN UNNEST UNTIL UPDATE USAGE USER
|
307
|
+
USING VALUE VALUES VARCHAR VARYING VIEW WHEN WHENEVER
|
308
|
+
WHERE WHILE WITH WITHOUT WORK WRITE YEAR ZONE
|
309
|
+
}
|
310
|
+
end
|
311
|
+
|
312
|
+
def is_reserved_word w
|
313
|
+
@reserved_word_hash ||=
|
314
|
+
reserved_words.inject({}) do |h,w|
|
315
|
+
h[w] = true
|
316
|
+
h
|
317
|
+
end
|
318
|
+
@reserved_word_hash[w.upcase]
|
319
|
+
end
|
320
|
+
|
321
|
+
def go s = ''
|
322
|
+
"#{s}\nGO\n" # REVISIT: This is an SQL-Serverism. Move it to a subclass.
|
323
|
+
end
|
324
|
+
|
325
|
+
def escape s, max = table_name_max
|
326
|
+
# Escape SQL keywords and non-identifiers
|
327
|
+
if s.size > max
|
328
|
+
excess = s[max..-1]
|
329
|
+
s = s[0...max-(excess.size/8)] +
|
330
|
+
Digest::SHA1.hexdigest(excess)[0...excess.size/8]
|
331
|
+
end
|
332
|
+
|
333
|
+
if s =~ /[^A-Za-z0-9_]/ || is_reserved_word(s)
|
334
|
+
"[#{s}]"
|
335
|
+
else
|
336
|
+
s
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
# Return SQL type and (modified?) length for the passed base type
|
341
|
+
def normalise_type(type, length)
|
342
|
+
sql_type = case type
|
343
|
+
when /^Auto ?Counter$/
|
344
|
+
'int'
|
345
|
+
|
346
|
+
when /^Unsigned ?Integer$/,
|
347
|
+
/^Signed ?Integer$/,
|
348
|
+
/^Unsigned ?Small ?Integer$/,
|
349
|
+
/^Signed ?Small ?Integer$/,
|
350
|
+
/^Unsigned ?Tiny ?Integer$/
|
351
|
+
s = case
|
352
|
+
when length == nil
|
353
|
+
'int'
|
354
|
+
when length <= 8
|
355
|
+
'tinyint'
|
356
|
+
when length <= 16
|
357
|
+
'smallint'
|
358
|
+
when length <= 32
|
359
|
+
'int'
|
360
|
+
else
|
361
|
+
'bigint'
|
362
|
+
end
|
363
|
+
length = nil
|
364
|
+
s
|
365
|
+
|
366
|
+
when /^Decimal$/
|
367
|
+
'decimal'
|
368
|
+
|
369
|
+
when /^Fixed ?Length ?Text$/, /^Char$/
|
370
|
+
'char'
|
371
|
+
when /^Variable ?Length ?Text$/, /^String$/
|
372
|
+
'varchar'
|
373
|
+
when /^Large ?Length ?Text$/, /^Text$/
|
374
|
+
'text'
|
375
|
+
|
376
|
+
when /^Date ?And ?Time$/, /^Date ?Time$/
|
377
|
+
'datetime'
|
378
|
+
when /^Date$/
|
379
|
+
'datetime' # SQLSVR 2K5: 'date'
|
380
|
+
when /^Time$/
|
381
|
+
'datetime' # SQLSVR 2K5: 'time'
|
382
|
+
when /^Auto ?Time ?Stamp$/
|
383
|
+
'timestamp'
|
384
|
+
|
385
|
+
when /^Guid$/
|
386
|
+
'uniqueidentifier'
|
387
|
+
when /^Money$/
|
388
|
+
'decimal'
|
389
|
+
when /^Picture ?Raw ?Data$/, /^Image$/
|
390
|
+
'image'
|
391
|
+
when /^Variable ?Length ?Raw ?Data$/, /^Blob$/
|
392
|
+
'varbinary'
|
393
|
+
when /^BIT$/
|
394
|
+
'bit'
|
395
|
+
else type # raise "SQL type unknown for standard type #{type}"
|
396
|
+
end
|
397
|
+
[sql_type, length]
|
398
|
+
end
|
399
|
+
|
400
|
+
def sql_value(value)
|
401
|
+
value.is_literal_string ? sql_string(value.literal) : value.literal
|
402
|
+
end
|
403
|
+
|
404
|
+
def sql_string(str)
|
405
|
+
"'" + str.gsub(/'/,"''") + "'"
|
406
|
+
end
|
407
|
+
|
408
|
+
def check_clause column_name, value_constraint
|
409
|
+
" CHECK(" +
|
410
|
+
value_constraint.all_allowed_range_sorted.map do |ar|
|
411
|
+
vr = ar.value_range
|
412
|
+
min = vr.minimum_bound
|
413
|
+
max = vr.maximum_bound
|
414
|
+
if (min && max && max.value.literal == min.value.literal)
|
415
|
+
"#{column_name} = #{sql_value(min.value)}"
|
416
|
+
else
|
417
|
+
inequalities = [
|
418
|
+
min && "#{column_name} >#{min.is_inclusive ? "=" : ""} #{sql_value(min.value)}",
|
419
|
+
max && "#{column_name} <#{max.is_inclusive ? "=" : ""} #{sql_value(max.value)}"
|
420
|
+
].compact
|
421
|
+
inequalities.size > 1 ? "(" + inequalities*" AND " + ")" : inequalities[0]
|
422
|
+
end
|
423
|
+
end*" OR " +
|
424
|
+
")"
|
425
|
+
end
|
426
|
+
|
427
|
+
MM = ActiveFacts::Metamodel
|
428
|
+
end
|
429
|
+
publish_generator SQL
|
430
|
+
end
|
431
|
+
end
|