lib/activefacts/persistence/columns.rb in activefacts-0.7.0 vs lib/activefacts/persistence/columns.rb in activefacts-0.7.1

- old
+ new

@@ -1,6 +1,11 @@ # +# ActiveFacts Relational mapping and persistence. +# Columns in a relational table; each is derived from a sequence of References. +# +# Copyright (c) 2009 Clifford Heath. Read the LICENSE file. +# # Each Reference from a Concept creates one or more Columns. # A reference to a simple valuetype creates a single column, as # does a reference to a table entity identified by a single value. # # When referring to a concept that doesn't have its own table, @@ -8,19 +13,22 @@ # # When multiple values identify an entity that does have its own # table, a reference to that entity creates multiple columns, # a multi-part foreign key. # -# Copyright (c) 2008 Clifford Heath. Read the LICENSE file. -# module ActiveFacts - module Metamodel + module Persistence #:nodoc: class Column - attr_reader :references + include Metamodel + def initialize(reference = nil) #:nodoc: + references << reference if reference + end + + # A Column is created from a path through an array of References to a ValueType def references @references ||= [] end # All references up to and including the first non-absorbing reference @@ -31,39 +39,38 @@ break array unless ref.is_absorbing array end end + # How many of the initial references are involved in full absorption of an EntityType into this column's table def absorption_level l = 0 @references.detect do |ref| l += 1 if ref.is_absorbing false end l end - def initialize(reference = nil) - references << reference if reference - end - - def prepend reference + def prepend reference #:nodoc: references.insert 0, reference self end + # A Column name is a sequence of names (derived from the to_roles of the References) + # joined by a joiner string (pass nil to get the original array of names) def name(joiner = "") last_name = "" names = @references. reject do |ref| # Skip any object after the first which is identified by this reference ref != @references[0] and !ref.fact_type.is_a?(TypeInheritance) and ref.to and ref.to.is_a?(EntityType) and (role_refs = ref.to.preferred_identifier.role_sequence.all_role_ref).size == 1 and - role_refs[0].role == ref.from_role + role_refs.only.role == ref.from_role end. inject([]) do |a, ref| names = ref.to_names # When traversing type inheritances, keep the subtype name, not the supertype names as well: @@ -83,28 +90,31 @@ # Where the last name is like a reference mode but the preceeding name isn't the identified concept, # strip it down (so turn Driver.PartyID into Driver.ID for example): if names.size > 1 and (et = @references.last.from).is_a?(EntityType) and (role_refs = et.preferred_identifier.role_sequence.all_role_ref).size == 1 and - role_refs[0].role == @references.last.to_role and + role_refs.only.role == @references.last.to_role and names.last[0...et.name.size].downcase == et.name.downcase names[-1] = names.last[et.name.size..-1] names.pop if names.last == '' end name_array = names.map{|n| n.sub(/^[a-z]/){|s| s.upcase}} joiner ? name_array * joiner : name_array end + # Is this column mandatory or nullable? def is_mandatory !@references.detect{|ref| !ref.is_mandatory} end + # Is this column an auto-assigned value type? def is_auto_assigned (to = references[-1].to) && to.is_auto_assigned end + # What's the underlying SQL data type of this column? def type params = {} restrictions = [] return ["BIT", params, restrictions] if references[-1].is_unary # It's a unary @@ -124,25 +134,26 @@ vt = vt.supertype end return [vt.name, params, restrictions] end + # The comment is the readings from the References expressed as a join def comment @references.map do |ref| (ref.is_mandatory ? "" : "maybe ") + (ref.fact_type && ref.fact_type.entity_type ? ref.fact_type.entity_type.name+" is where " : "") + ref.reading end * " and " end - def to_s + def to_s #:nodoc: "#{@references[0].from.name} column #{name('.')}" end end class Reference - def columns(excluded_supertypes) + def columns(excluded_supertypes) #:nodoc: kind = "" cols = if is_unary kind = "unary " [Column.new()] @@ -165,67 +176,83 @@ debug :columns, "#{c}" } end end end + end + module Metamodel #:nodoc: + # The Concept class is defined in the metamodel; full documentation is not generated. + # This section shows the features relevant to relational Persistence. class Concept - attr_accessor :columns + # The array of columns for this Concept's table + def columns; @columns; end - def populate_columns + def populate_columns #:nodoc: @columns = all_columns({}) end end - class ValueType + # The ValueType class is defined in the metamodel; full documentation is not generated. + # This section shows the features relevant to relational Persistence. + class ValueType < Concept + # The identifier_columns for a ValueType can only ever be the self-value role that was injected def identifier_columns debug :columns, "Identifier Columns for #{name}" do raise "Illegal call to identifier_columns for absorbed ValueType #{name}" unless is_table columns.select{|column| column.references[0] == self_value_reference} end end - def reference_columns(excluded_supertypes) + # When creating a foreign key to this ValueType, what columns must we include? + # This must be a fresh copy, because the columns will have References prepended + def reference_columns(excluded_supertypes) #:nodoc: debug :columns, "Reference Columns for #{name}" do if is_table - [Column.new(self_value_reference)] + [ActiveFacts::Persistence::Column.new(self_value_reference)] else - [Column.new] + [ActiveFacts::Persistence::Column.new] end end end - def all_columns(excluded_supertypes) + # When absorbing this ValueType, what columns must be absorbed? + # This must be a fresh copy, because the columns will have References prepended. + def all_columns(excluded_supertypes) #:nodoc: columns = [] debug :columns, "All Columns for #{name}" do if is_table self_value_reference else - columns << Column.new + columns << ActiveFacts::Persistence::Column.new end references_from.each do |ref| debug :columns, "Columns absorbed via #{ref}" do columns += ref.columns({}) end end end columns end - def self_value_reference + # If someone asks for this, it's because it's needed, so create it. + def self_value_reference #:nodoc: # Make a reference for the self-value column - @self_value_reference ||= Reference.new(self, nil).tabulate + @self_value_reference ||= ActiveFacts::Persistence::Reference.new(self, nil).tabulate end end - class EntityType + # The EntityType class is defined in the metamodel; full documentation is not generated. + # This section shows the features relevant to relational Persistence. + class EntityType < Concept + # The identifier_columns for an EntityType are the columns that result from the identifying roles def identifier_columns debug :columns, "Identifier Columns for #{name}" do if absorbed_via and # If this is a subtype that has its own identification, use that. - (all_type_inheritance_by_subtype.size == 0 || - all_type_inheritance_by_subtype.detect{|ti| ti.provides_identification }) + (all_type_inheritance_as_subtype.size == 0 || + all_type_inheritance_as_subtype.detect{|ti| ti.provides_identification }) return absorbed_via.from.identifier_columns end preferred_identifier.role_sequence.all_role_ref.map do |role_ref| ref = references_from.detect {|ref| ref.to_role == role_ref.role} @@ -233,17 +260,19 @@ columns.select{|column| column.references[0] == ref} end.flatten end end - def reference_columns(excluded_supertypes) + # When creating a foreign key to this EntityType, what columns must we include (the identifier columns)? + # This must be a fresh copy, because the columns will have References prepended + def reference_columns(excluded_supertypes) #:nodoc: debug :columns, "Reference Columns for #{name}" do if absorbed_via and # If this is a subtype that has its own identification, use that. - (all_type_inheritance_by_subtype.size == 0 || - all_type_inheritance_by_subtype.detect{|ti| ti.provides_identification }) + (all_type_inheritance_as_subtype.size == 0 || + all_type_inheritance_as_subtype.detect{|ti| ti.provides_identification }) return absorbed_via.from.reference_columns(excluded_supertypes) end # REVISIT: Should have built preferred_identifier_references preferred_identifier.role_sequence.all_role_ref.map do |role_ref| @@ -255,11 +284,13 @@ ref.columns({}) end.flatten end end - def all_columns(excluded_supertypes) + # When absorbing this EntityType, what columns must be absorbed? + # This must be a fresh copy, because the columns will have References prepended. + def all_columns(excluded_supertypes) #:nodoc: debug :columns, "All Columns for #{name}" do columns = [] sups = supertypes references_from.sort_by do |ref| # Put supertypes first, in order, then non-subtype references, then subtypes, otherwise retaining their order: @@ -285,18 +316,21 @@ columns end end end + # The Vocabulary class is defined in the metamodel; full documentation is not generated. + # This section shows the features relevant to relational Persistence. class Vocabulary - # Do things like adding ID fields and ValueType self-value columns + # Make schema transformations like adding ValueType self-value columns (and later, Rails-friendly ID fields). + # Override this method to change the transformations def finish_schema all_feature.each do |feature| feature.self_value_reference if feature.is_a?(ValueType) && feature.is_table end end - def populate_all_columns + def populate_all_columns #:nodoc: # REVISIT: Is now a good time to apply schema transforms or should this be more explicit? finish_schema debug :columns, "Populating all columns" do all_feature.each do |feature|