\n" +
h1("Logical Data Model for " + @composition.name)
end
def generate_definitions
defns =
@composition.
all_composite.
reject {|c| c.mapping.object_type.is_a?(ActiveFacts::Metamodel::ValueType)}.
reject {|c| c.mapping.object_type.is_static}.
reject {|c| c.mapping.object_type.fact_type}.
map {|c| c.mapping.object_type}
@definitions = {}
defns.each do |o|
@definitions[o] = true
end
defns.each do |o|
ftm = relevant_fact_types(o)
trace :ldm, "expanding #{o.name}"
ftm.each do |r, ft|
next if ft.is_a?(ActiveFacts::Metamodel::TypeInheritance)
ft.all_role.each do |ftr|
next if @definitions[ftr.object_type]
next if ftr.object_type.is_a?(ActiveFacts::Metamodel::ValueType)
trace :ldm, "adding #{ftr.object_type.name}"
defns = defns << ftr.object_type
@definitions[ftr.object_type] = true
end
end
end
"
Business Definitions and Relationships
\n" +
defns.sort_by{|o| o.name.gsub(/ /, '').downcase}.map do |o|
entity_type_dump(o, 0)
end * "\n" + "\n"
end
def generate_diagrams
''
end
def generate_details
h2("Logical Data Model Details") +
@composition.
all_composite.
sort_by{|composite| composite.mapping.name}.
map{|composite| generate_table(composite)}*"\n" + "\n"
end
def generate_footer
"
\n" +
"
\n" +
"
\n" +
" \n" +
" \n" +
" \n" +
" \n" +
# @glossary.glossary_end +
" \n" +
"\n"
end
#
# Standard document elements
#
def element(text, attrs, tag = 'span')
"<#{tag}#{attrs.empty? ? '' : attrs.map{|k,v| " #{k}='#{v}'"}*''}>#{text}#{tag}>"
end
def span(text, klass = nil)
element(text, klass ? {:class => klass} : {})
end
def div(text, klass = nil)
element(text, klass ? {:class => klass} : {}, 'div')
end
def h1(text, klass = nil)
element(text, klass ? {:class => klass} : {}, 'h1')
end
def h2(text, klass = nil)
element(text, klass ? {:class => klass} : {}, 'h2')
end
def h3(text, klass = nil)
element(text, klass ? {:class => klass} : {}, 'h3')
end
def dl(text, klass = nil)
element(text, klass ? {:class => klass} : {}, 'dl')
end
# A definition of a term
def termdef(name)
element(name, {:name => name, :class => 'object_type'}, 'a')
end
# A reference to a defined term (excluding role adjectives)
def termref(name, role_name = nil, o = nil)
if o && !@definitions[o]
element(name, :class=>:object_type)
else
role_name ||= name
element(role_name, {:href=>'#'+name, :class=>:object_type}, 'a')
end
end
# Text that should appear as part of a term (including role adjectives)
def term(name)
element(name, :class=>:object_type)
end
#
# Dump functions
#
def entity_type_dump(o, level)
pi = o.preferred_identifier
supers = o.supertypes
if (supers.size > 0) # Ignore identification by a supertype:
pi = nil if pi && pi.role_sequence.all_role_ref.detect{ |rr|
rr.role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
}
end
cn_array = o.concept.all_context_note_as_relevant_concept.map{|cn| [cn.context_note_kind, cn.discussion] }
cn_hash = cn_array.inject({}) do |hash, value|
hash[value.first] = value.last
hash
end
informal_defn = cn_hash["because"]
defn_term =
"
"
# "-- #{column_comment leaf}\n\t#{column_name}#{padding}#{component_type leaf, column_name}#{identity}"
end
def column_comment component
return '' unless cp = component.parent
prefix = column_comment(cp)
name = component.name
if component.is_a?(MM::Absorption)
reading = component.parent_role.fact_type.reading_preferably_starting_with_role(component.parent_role).expand([], false)
maybe = component.parent_role.is_mandatory ? '' : 'maybe '
cpname = cp.name
if prefix[(-cpname.size-1)..-1] == ' '+cpname && reading[0..cpname.size] == cpname+' '
prefix+' that ' + maybe + reading[cpname.size+1..-1]
else
(prefix.empty? ? '' : prefix+' and ') + maybe + reading
end
else
name
end
end
def boolean_type
'boolean'
end
def surrogate_type
'bigint'
end
def component_type component, column_name
case component
when MM::Indicator
boolean_type
when MM::SurrogateKey
surrogate_type
when MM::ValueField, MM::Absorption
object_type = component.object_type
while object_type.is_a?(MM::EntityType)
rr = object_type.preferred_identifier.role_sequence.all_role_ref.single
raise "Can't produce a column for composite #{component.inspect}" unless rr
object_type = rr.role.object_type
end
raise "A column can only be produced from a ValueType" unless object_type.is_a?(MM::ValueType)
if component.is_a?(MM::Absorption)
value_constraint ||= component.child_role.role_value_constraint
end
supertype = object_type
begin
object_type = supertype
length ||= object_type.length
scale ||= object_type.scale
unless component.parent.parent and component.parent.foreign_key
# No need to enforce value constraints that are already enforced by a foreign key
value_constraint ||= object_type.value_constraint
end
end while supertype = object_type.supertype
type, length = normalise_type(object_type.name, length)
sql_type = "#{type}#{
if !length
''
else
'(' + length.to_s + (scale ? ", #{scale}" : '') + ')'
end
# }#{
# (component.path_mandatory ? '' : ' NOT') + ' NULL'
# }#{
# # REVISIT: This is an SQL Server-ism. Replace with a standard SQL SEQUENCE/
# # Emit IDENTITY for columns auto-assigned on commit (except FKs)
# if a = object_type.is_auto_assigned and a != 'assert' and
# !component.all_foreign_key_field.detect{|fkf| fkf.foreign_key.source_composite == component.root}
# ' IDENTITY'
# else
# ''
# end
}#{
value_constraint ? check_clause(column_name, value_constraint) : ''
}"
when MM::Injection
component.object_type.name
else
raise "Can't make a column from #{component}"
end
end
def generate_index index, delayed_indices, indent
nullable_columns =
index.all_index_field.select do |ixf|
!ixf.component.path_mandatory
end
contains_nullable_columns = nullable_columns.size > 0
primary = index.composite_as_primary_index && !contains_nullable_columns
column_names =
index.all_index_field.map do |ixf|
column_name(ixf.component)
end
clustering =
(index.composite_as_primary_index ? ' CLUSTERED' : ' NONCLUSTERED')
if contains_nullable_columns
table_name = safe_table_name(index.composite)
delayed_indices <<
'CREATE UNIQUE'+clustering+' INDEX '+
escape("#{table_name(index.composite)}By#{column_names*''}", index_name_max) +
" ON #{table_name}("+column_names.map{|n| escape(n, column_name_max)}*', ' +
") WHERE #{
nullable_columns.
map{|ixf| safe_column_name ixf.component}.
map{|column_name| column_name + ' IS NOT NULL'} *
' AND '
}"
nil
else
# '-- '+index.inspect
" " * indent + (primary ? 'PRIMARY KEY' : 'UNIQUE') +
clustering +
"(#{column_names.map{|n| escape(n, column_name_max)}*', '})"
end
end
def generate_foreign_key fk, indent
# '-- '+fk.inspect
" " * indent + "FOREIGN KEY (" +
fk.all_foreign_key_field.map{|fkf| safe_column_name fkf.component}*", " +
") REFERENCES #{table_name fk.composite} (" +
fk.all_index_field.map{|ixf| safe_column_name ixf.component}*", " +
")"
end
def reserved_words
@reserved_words ||= %w{ }
end
def is_reserved_word w
@reserved_word_hash ||=
reserved_words.inject({}) do |h,w|
h[w] = true
h
end
@reserved_word_hash[w.upcase]
end
def go s = ''
"#{s}\nGO\n" # REVISIT: This is an SQL-Serverism. Move it to a subclass.
end
def escape s, max = table_name_max
# Escape SQL keywords and non-identifiers
if s.size > max
excess = s[max..-1]
s = s[0...max-(excess.size/8)] +
Digest::SHA1.hexdigest(excess)[0...excess.size/8]
end
if s =~ /[^A-Za-z0-9_]/ || is_reserved_word(s)
"[#{s}]"
else
s
end
end
# Return SQL type and (modified?) length for the passed base type
def normalise_type(type, length)
sql_type = case type
when /^Auto ?Counter$/
'int'
when /^Unsigned ?Integer$/,
/^Signed ?Integer$/,
/^Unsigned ?Small ?Integer$/,
/^Signed ?Small ?Integer$/,
/^Unsigned ?Tiny ?Integer$/
s = case
when length == nil
'int'
when length <= 8
'tinyint'
when length <= 16
'smallint'
when length <= 32
'int'
else
'bigint'
end
length = nil
s
when /^Decimal$/
'decimal'
when /^Fixed ?Length ?Text$/, /^Char$/
'char'
when /^Variable ?Length ?Text$/, /^String$/
'varchar'
when /^Large ?Length ?Text$/, /^Text$/
'text'
when /^Date ?And ?Time$/, /^Date ?Time$/
'datetime'
when /^Date$/
'datetime' # SQLSVR 2K5: 'date'
when /^Time$/
'datetime' # SQLSVR 2K5: 'time'
when /^Auto ?Time ?Stamp$/
'timestamp'
when /^Guid$/
'uniqueidentifier'
when /^Money$/
'decimal'
when /^Picture ?Raw ?Data$/, /^Image$/
'image'
when /^Variable ?Length ?Raw ?Data$/, /^Blob$/
'varbinary'
when /^BIT$/
'bit'
when /^BOOLEAN$/
'boolean'
else type # raise "SQL type unknown for standard type #{type}"
end
[sql_type, length]
end
def sql_value(value)
value.is_literal_string ? sql_string(value.literal) : value.literal
end
def sql_string(str)
"'" + str.gsub(/'/,"''") + "'"
end
def check_clause column_name, value_constraint
" CHECK(" +
value_constraint.all_allowed_range_sorted.map do |ar|
vr = ar.value_range
min = vr.minimum_bound
max = vr.maximum_bound
if (min && max && max.value.literal == min.value.literal)
"#{column_name} = #{sql_value(min.value)}"
else
inequalities = [
min && "#{column_name} >#{min.is_inclusive ? "=" : ""} #{sql_value(min.value)}",
max && "#{column_name} <#{max.is_inclusive ? "=" : ""} #{sql_value(max.value)}"
].compact
inequalities.size > 1 ? "(" + inequalities*" AND " + ")" : inequalities[0]
end
end*" OR " +
")"
end
MM = ActiveFacts::Metamodel unless const_defined?(:MM)
end
end
publish_generator Doc::LDM
end
end