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|