# # ActiveFacts Common Warehouse Metamodel Generator # # This generator produces an CWM XMI-formatted model of a Composition. # # Copyright (c) 2016 Infinuendo. Read the LICENSE file. # require 'digest/sha1' require 'activefacts/metamodel' require 'activefacts/metamodel/datatypes' require 'activefacts/compositions' require 'activefacts/generator' require 'activefacts/support' module ActiveFacts module Generators module Doc class CWM MM = ActiveFacts::Metamodel unless const_defined?(:MM) def self.options { underscore: [String, "Use 'str' instead of underscore between words in table names"] } end def self.compatibility [1, %i{relational}] # one relational composition end def initialize constellation, composition, options = {} @constellation = constellation @composition = composition @options = options @underscore = options.has_key?("underscore") ? (options['underscore'] || '_') : '' @vocabulary = @composition.constellation.Vocabulary.values[0] # REVISIT when importing from other vocabularies end def data_type_context @data_type_context ||= CWMDataTypeContext.new end def generate # @tables_emitted = {} @ns = 0 @datatypes = Array.new trace.enable 'cwm' model_ns, schema_ns = populate_namespace_ids generate_header + generate_content(model_ns, schema_ns) + generate_footer end def table_name_max 60 end def column_name_max 40 end def index_name_max 60 end def schema_name_max 60 end def safe_table_name composite escape(table_name(composite), table_name_max) end def safe_column_name component escape(column_name(component), column_name_max) end def table_name composite composite.mapping.name.gsub(' ', @underscore) end def column_name component component.column_name.capcase end def indent depth, str " " * depth + str + "\n" end def rawnsdef @ns += 1 "_#{@ns}" end def nsdef obj, pref = nil if obj.xmiid == nil obj.xmiid = "#{pref}#{rawnsdef}" end obj.xmiid end def populate_namespace_ids model_ns = rawnsdef schema_ns = rawnsdef @composition. all_composite. sort_by{|composite| composite.mapping.name}. map{|composite| populate_table_ids(composite)} [model_ns, schema_ns] end def populate_table_ids(table) tname = table_name(table) nsdef(table) table.mapping.all_leaf.flat_map.sort_by{|c| column_name(c)}.map do |leaf| # Absorbed empty subtypes appear as leaves next if leaf.is_a?(MM::Absorption) && leaf.parent_role.fact_type.is_a?(MM::TypeInheritance) nsdef(leaf) end table.all_index.sort_by do |idx| idx.all_index_field.map { |ixf| ixf.component.xmiid } end.map do |index| nsdef(index) index.all_index_field.map{|idf| idf.component.index_xmiid = index.xmiid} end table.all_foreign_key_as_source_composite.sort_by{|fk| [fk.source_composite.mapping.name, fk.mapping.inspect] }.map do |fk| nsdef(fk) end end def generate_header "\n" + "\n" + "\n" + "\n" + " \n" + " \n" + " ActiveFacts\n" + " 0.1\n" + " \n" + " " + " \n" end def generate_content(model_ns, schema_ns) " \n" + generate_catalog(2, model_ns, schema_ns) + generate_data_types(2) + " \n" end def generate_footer "\n" end def generate_catalog(depth, model_ns, schema_ns) catalog_start = indent(depth, "") + indent(depth, " ") + indent(depth, " ") + indent(depth, " ") catalog_body = @composition. all_composite. sort_by{|composite| composite.mapping.name}. map{|composite| generate_table(depth+4, schema_ns, composite)}*"\n" + "\n" catalog_end = indent(depth, " ") + indent(depth, " ") + indent(depth, " ") + indent(depth, "") catalog_start + catalog_body + catalog_end end def generate_data_types(depth) @datatypes.map do | dt | indent(depth, dt) end * "" end def generate_table(depth, schema_ns, table) name = table_name(table) delayed_indices = [] table_start = indent(depth, "") table_columns = indent(depth, " ") + (table.mapping.all_leaf.flat_map.sort_by{|c| column_name(c)}.map do |leaf| # Absorbed empty subtypes appear as leaves next if leaf.is_a?(MM::Absorption) && leaf.parent_role.fact_type.is_a?(MM::TypeInheritance) generate_column(depth+2, table.xmiid, leaf) end ).compact.flat_map{|f| "#{f}" } * "" + indent(depth, " ") table_keys = indent(depth, " ") + (table.all_index.sort_by do |idx| idx.all_index_field.map { |ixf| ixf.component.xmiid } end.map do |index| generate_index(depth+2, table.xmiid, index, name, table.all_foreign_key_as_target_composite) end ) * "" + (table.all_foreign_key_as_source_composite.sort_by{|fk| [fk.source_composite.mapping.name, fk.mapping.inspect] }.map do |fk| generate_foreign_key(depth+2, table.xmiid, fk) end ) * "" + indent(depth, " ") table_end = indent(depth, "") table_start + table_columns + table_keys + table_end end def generate_column(depth, table_ns, column) name = safe_column_name(column) is_nullable = column.path_mandatory ? "columnNoNulls" : "columnNullable" constraints = column.all_leaf_constraint type_name, options = column.data_type(data_type_context) options ||= {} length = options[:length] type_name, type_num = normalise_type_cwm(type_name, length) column_params = "" type_params = "" type_ns = create_data_type(type_name, type_num, type_params) indent(depth, "") end def create_data_type(type_name, type_num, type_params) type_ns = rawnsdef cwm_data_type = "" @datatypes << cwm_data_type type_ns end def generate_index(depth, table_ns, index, table_name, all_fks_as_target) key_ns = index.xmiid 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_ids = index.all_index_field.map do |ixf| ixf.component.xmiid end # clustering = # (index.composite_as_primary_index ? ' CLUSTERED' : ' NONCLUSTERED') key_type = primary ? 'CWMRDB:PrimaryKey' : 'CWM:UniqueKey' # find target foreign keys for this index fks_as_target = all_fks_as_target if column_ids.count == 1 && fks_as_target.count == 0 colid = column_ids[0] indent(depth, "<#{key_type} xmi.id=\"#{key_ns}\" name=\"XPK#{table_name}\" visibility=\"public\" namespace=\"#{table_ns}\" feature=\"#{colid}\"/>") else if column_ids.count == 1 colid = column_ids[0] indent(depth, "<#{key_type} xmi.id=\"#{key_ns}\" name=\"XPK#{table_name}\" visibility=\"public\" namespace=\"#{table_ns}\" feature=\"#{colid}\">") else indent(depth, "<#{key_type} xmi.id=\"#{key_ns}\" name=\"XPK#{table_name}\" visibility=\"public\" namespace=\"#{table_ns}\">") + indent(depth, " ") + column_ids.map do |id| indent(depth, " ") end * "" + indent(depth, " ") end + if fks_as_target.count > 0 indent(depth, "") + fks_as_target.map do |fk| indent(depth, " ") end * "" + indent(depth, "") else "" end + indent(depth, "") end end def generate_foreign_key(depth, table_ns, fk) key_ns = fk.xmiid if fk.all_foreign_key_field.size == 1 fkf = fk.all_foreign_key_field[0] ixf = fk.all_index_field[0] indent(depth, "") else indent(depth, "") + indent(depth, " ") + begin out = "" for i in 0..(fk.all_foreign_key_field.size - 1) fkf = fk.all_foreign_key_field[i] ixf = fk.all_index_field[i] out += indent(depth, " ") end out end + indent(depth, " ") + indent(depth, "") end # fk.all_foreign_key_field.map{|fkf| safe_column_name fkf.component}*", " + # ") REFERENCES #{safe_table_name fk.composite} (" + # fk.all_index_field.map{|ixf| safe_column_name ixf.component}*", " + # indent(depth, "") end def boolean_type 'boolean' end def surrogate_type 'bigint' 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 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 CWM type, typenum for the passed base type def normalise_type_cwm(type_name, length) type = MM::DataType.intrinsic_type(type_name) case type when MM::DataType::TYPE_Boolean; ['boolean', 16] when MM::DataType::TYPE_Integer case type_name when /^Auto ?Counter$/i ['int', 4] when /([a-z ]|\b)Tiny([a-z ]|\b)/i ['tinyint', -6] when /([a-z ]|\b)Small([a-z ]|\b)/i, /([a-z ]|\b)Short([a-z ]|\b)/i ['smallint', 5] when /([a-z ]|\b)Big(INT)?([a-z ]|\b)/i ['bigint', -5] else ['int', 4] end when MM::DataType::TYPE_Real; ['real', 7, data_type_context.default_length(type, type_name)] when MM::DataType::TYPE_Decimal; ['decimal', 3] when MM::DataType::TYPE_Money; ['decimal', 3] when MM::DataType::TYPE_Char; ['char', 12, length] when MM::DataType::TYPE_String; ['varchar', 12, length] when MM::DataType::TYPE_Text; ['text', 2005, length || 'MAX'] when MM::DataType::TYPE_Date; ['date', 91] when MM::DataType::TYPE_Time; ['time', 92] when MM::DataType::TYPE_DateTime; ['datetime', 93] when MM::DataType::TYPE_Timestamp;['timestamp', -3] when MM::DataType::TYPE_Binary; length ||= 16 if type_name =~ /^(guid|uuid)$/i if length ['BINARY', -2] else ['VARBINARY', -2] end else ['int', 4] end 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 class CWMDataTypeContext < MM::DataType::Context def integer_ranges [ ['SMALLINT', -2**15, 2**15-1], # The standard says -10^5..10^5 (less than 16 bits) ['INTEGER', -2**31, 2**31-1], # The standard says -10^10..10^10 (more than 32 bits!) ['BIGINT', -2**63, 2**63-1], # The standard says -10^19..10^19 (less than 64 bits) ] end def default_length data_type, type_name case data_type when MM::DataType::TYPE_Real 53 # IEEE Double precision floating point when MM::DataType::TYPE_Integer case type_name when /([a-z ]|\b)Tiny([a-z ]|\b)/i 8 when /([a-z ]|\b)Small([a-z ]|\b)/i, /([a-z ]|\b)Short([a-z ]|\b)/i 16 when /([a-z ]|\b)Big(INT)?([a-z ]|\b)/i 64 else 32 end else nil end end def boolean_type 'BOOLEAN' end def surrogate_type type_name, = choose_integer_range(0, 2**(default_surrogate_length-1)-1) type_name end def date_time_type 'TIMESTAMP' end def default_char_type (@unicode ? 'NATIONAL ' : '') + 'CHARACTER' end def default_varchar_type (@unicode ? 'NATIONAL ' : '') + 'VARCHAR' end def default_surrogate_length 64 end def default_text_type default_varchar_type end end end end publish_generator Doc::CWM, "Common Warehouse Metamodel represented as an XMI file. Use a relational compositor" end module Metamodel # Add namespace id to metamodel forward referencing class Composite # for tables attr_accessor :xmiid end class Component # for columns attr_accessor :xmiid attr_accessor :index_xmiid end class Index # for primary and unique indexes attr_accessor :xmiid end class ForeignKey # for foreign keys attr_accessor :xmiid end end end