Class: ActiveFacts::Metamodel::Concept

Inherits:
Feature
  • Object
show all
Defined in:
lib/activefacts/persistence/columns.rb,
lib/activefacts/persistence/index.rb,
lib/activefacts/vocabulary/metamodel.rb,
lib/activefacts/persistence/reference.rb,
lib/activefacts/persistence/foreignkey.rb

Overview

The Concept class is defined in the metamodel; full documentation is not generated. This section shows the features relevant to relational Persistence.

Direct Known Subclasses

EntityType, ValueType

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#is_table=(value) ⇒ Object (writeonly)

The two Concept subclasses provide the attr_reader method



185
186
187
# File 'lib/activefacts/persistence/reference.rb', line 185

def is_table=(value)
  @is_table = value
end

#tentativeObject

Say whether the independence of this object is still under consideration This is used in detecting dependency cycles, such as occurs in the Metamodel



184
185
186
# File 'lib/activefacts/persistence/reference.rb', line 184

def tentative
  @tentative
end

Instance Method Details

#all_absorbed_foreign_key_reference_pathObject

When an EntityType is fully absorbed, its foreign keys are too. Return an Array of Reference paths for such absorbed FKs



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/activefacts/persistence/foreignkey.rb', line 36

def all_absorbed_foreign_key_reference_path
  references_from.inject([]) do |array, ref|

    if ref.is_simple_reference
      # This catches references that would be created to secondary supertypes, when absorption is through primary.
      # There might be other cases where an exclusion like this is needed, but I can't reason it out.
      next array if TypeInheritance === ref.fact_type && absorbed_via && TypeInheritance === absorbed_via.fact_type
      array << [ref]
    elsif ref.is_absorbing
      ref.to.all_absorbed_foreign_key_reference_path.each{|aref|
        array << aref.insert(0, ref)
      }
    end
    array
  end
end

#clear_indicesObject

:nodoc:



85
86
87
88
# File 'lib/activefacts/persistence/index.rb', line 85

def clear_indices     #:nodoc:
  # Clear any previous indices
  @indices = nil
end

#clear_referencesObject

:nodoc:



227
228
229
230
231
# File 'lib/activefacts/persistence/reference.rb', line 227

def clear_references              #:nodoc:
  # Clear any previous references:
  @references_to = nil
  @references_from = nil
end

#columnsObject

The array of columns for this Concept’s table



192
193
194
# File 'lib/activefacts/persistence/columns.rb', line 192

def columns
  @columns
end

#definitely_not_tableObject

:nodoc:



197
198
199
200
# File 'lib/activefacts/persistence/reference.rb', line 197

def definitely_not_table          #:nodoc:
  @is_table = false
  @tentative = false
end

#definitely_tableObject

:nodoc:



192
193
194
195
# File 'lib/activefacts/persistence/reference.rb', line 192

def definitely_table              #:nodoc:
  @is_table = true
  @tentative = false
end

#foreign_keysObject

Return an array of all the foreign keys from this table



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/activefacts/persistence/foreignkey.rb', line 54

def foreign_keys
  fk_ref_paths = all_absorbed_foreign_key_reference_path

  # Get the ForeignKey object for each absorbed reference path
  fk_ref_paths.map do |fk_ref_path|
    debug :fk, "\nFK: " + fk_ref_path.map{|fk_ref| fk_ref.reading }*" and " do

      from_columns = columns.select{|column|
        column.references[0...fk_ref_path.size] == fk_ref_path
      }
      debug :fk, "from_columns = #{from_columns.map { |column| column.name }*", "}"

      absorption_path = []
      to = fk_ref_path.last.to
      # REVISIT: There should be a better way to find where it's absorbed (especially since this fails for absorbed subtypes having their own identification!)
      while (r = to.absorbed_via)
        absorption_path << r
        to = r.to == to ? r.from : r.to
      end
      raise "REVISIT: #{fk_ref_path.inspect} is bad" unless to and to.columns

      unless absorption_path.empty?
        debug :fk, "Reference target #{fk_ref_path.last.to.name} is absorbed into #{to.name} via:" do
          debug :fk, "#{absorption_path.map(&:reading)*" and "}"
        end
      end

      debug :fk, "Looking at absorption depth of #{absorption_path.size} in #{to.name} for to_columns for #{from_columns.map(&:name)*", "}:"
      to_supertypes = to.supertypes_transitive
      to_columns = from_columns.map do |from_column|
        debug :fk, "\tLooking for counterpart of #{from_column.name}: #{from_column.comment}" do
          target_path = absorption_path + from_column.references[fk_ref_path.size..-1]
          debug :fk, "\tcounterpart MUST MATCH #{target_path.map(&:reading)*" and "}"
          c = to.columns.detect do |column|
            debug :fk, "Considering #{column.references.map(&:reading) * " and "}"
            debug :fk, "exact match: #{column.name}: #{column.comment}" if column.references == target_path
            # Column may be inherited into "to", in which case target_path is too long.
            cr = column.references
            allowed_type = fk_ref_path.last.to
            #debug :fk, "Check for absorption, need #{allowed_type.name}" if cr != target_path
            cr == target_path or
              cr == target_path[-cr.size..-1] &&
              !target_path[0...-cr.size].detect do |ref|
                ft = ref.fact_type
                next true if allowed_type.absorbed_via != ref   # Problems if it doesn't match
                allowed_type = ref.from
                false
              end
          end
          raise "REVISIT: Failed to find conterpart column for #{from_column.name}" unless c
          c
        end
      end
      debug :fk, "to_columns in #{to.name}: #{to_columns.map { |column| column ? column.name : "OOPS!" }*", "}"

      # Put the column pairs in a defined order, sorting key pairs by to-name:
      froms, tos = from_columns.zip(to_columns).sort_by { |pair|
        pair[1].name(nil)
      }.transpose

      ActiveFacts::Persistence::ForeignKey.new(self, to, fk_ref_path[-1], froms, tos)
    end
  end.
  sort_by do |fk|
    # Put the foreign keys in a defined order:
    [ fk.to.name,
      fk.to_columns.map{|col| col.name(nil).sort},
      fk.from_columns.map{|col| col.name(nil).sort}
    ]
  end
end

#has_referencesObject

True if this Concept has any References (to or from)



223
224
225
# File 'lib/activefacts/persistence/reference.rb', line 223

def has_references                #:nodoc:
  @references_from || @references_to
end

#indicesObject

An array of each Index for this table



83
# File 'lib/activefacts/persistence/index.rb', line 83

def indices; @indices; end

#populate_columnsObject

:nodoc:



196
197
198
199
# File 'lib/activefacts/persistence/columns.rb', line 196

def populate_columns  #:nodoc:
  @columns =
    all_columns({})
end

#populate_indicesObject

:nodoc:



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/activefacts/persistence/index.rb', line 90

def populate_indices     #:nodoc:
  # The absorption path of a column indicates how it came to be in this table.
  # It might be a direct many:one valuetype relationship, or it might be in such
  # a relationship to an entity that was absorbed into this table (and so on).
  # The reference path is the set of absorption references and one past it.
  # Stopping here means we don't dig into the definitions of FK column counterparts.
  # Note that many columns of an object may have the same ref_path.
  #
  # REVISIT:
  # Note also that this produces columns ordered for each refpath the same as the
  # order of the columns, not the same as the columns in the PK for which they might be an FK.
  all_column_by_ref_path =
    debug :index2, "Indexing columns by ref_path" do
      @columns.inject({}) do |hash, column|
        debug :index2, "References in column #{name}.#{column.name}" do
          ref_path = column.absorption_references
          raise "No absorption_references for #{column.name} from #{column.references.map(&:to_s)*" and "}" if !ref_path || ref_path.empty?
          (hash[ref_path] ||= []) << column
          debug :index2, "#{column.name} involves #{ref_path.map(&:to_s)*" and "}"
        end
        hash
      end
    end

  columns_by_unique_constraint = {}
  all_column_by_role_ref =
    all_column_by_ref_path.
      keys.                       # Go through all refpaths and find uniqueness constraints
      inject({}) do |hash, ref_path|
        ref_path.each do |ref|
          next unless ref.to_role
          #debug :index2, "Considering #{ref_path.map(&:to_s)*" and "} yielding columns #{all_column_by_ref_path[ref_path].map{|c| c.name(".")}*", "}"
          #debugger if name == 'VehicleIncident' && ref.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance)
          ref.to_role.all_role_ref.each do |role_ref|
            all_pcs = role_ref.role_sequence.all_presence_constraint
    #puts "pcs over #{ref_path.map{|r| r.to_names}.flatten*"."}: #{role_ref.role_sequence.all_presence_constraint.map(&:describe)*"; "}" if all_pcs.size > 0
            pcs = all_pcs.
              reject do |pc|
                !pc.max_frequency or      # No maximum freq; cannot be a uniqueness constraint
                pc.max_frequency != 1 or  # maximum is not 1
                                          # Constraint is not over a unary fact type role (NORMA does this)
                pc.role_sequence.all_role_ref.size == 1 && ref_path[-1].to_role.fact_type.all_role.size == 1
              end
            next unless pcs.size > 0
            # The columns for this ref_path support the UCs in "pcs".
            pcs.each do |pc|
              ref_columns = all_column_by_ref_path[ref_path]
              ordinal = role_ref.ordinal  # Position in priority order
              ref_columns.each_with_index do |column, index|
                #puts "Adding index column #{column.name} in rank[#{ordinal},#{index}]"
                # REVISIT: the "index" here might be a duplicate in some cases: change sort_by below to just sort and run the SeparateSubtypes CQL model for example.
                (columns_by_unique_constraint[pc] ||= []) << [ordinal, index, column]
              end
            end
            hash[role_ref] = all_column_by_ref_path[ref_path]
          end
        end
        hash
      end

  debug :index, "All Indices in #{name}:" do
    @indices = columns_by_unique_constraint.map do |uc, columns_with_ordinal|
      #puts "Index on #{name} over (#{columns_with_ordinal.sort.map{|ca| [ca[0], ca[1], ca[2].name].inspect}})"
      columns = columns_with_ordinal.sort_by{|ca| [ca[0,2], ca[2].name]}.map{|ca| ca[2]}
      absorption_level = columns.map(&:absorption_level).min
      over = columns[0].references[absorption_level].from

      # Absorption through a one-to-one forms a UC that we don't need to enforce using an index:
      next nil if over != self and
        over.absorbed_via == columns[0].references[absorption_level-1] and
        (rr = uc.role_sequence.all_role_ref.single) and
        over.absorbed_via.fact_type.all_role.include?(rr.role)

      index = ActiveFacts::Persistence::Index.new(
        uc,
        self,
        over,
        columns,
        uc.is_preferred_identifier
      )
      debug :index, index
      index
    end.
    compact.
    sort_by do |index|
      # Put the indices in a defined order:
      index.columns.map(&:name)
    end
  end
end

#populate_reference(role) ⇒ Object

:nodoc:



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/activefacts/persistence/reference.rb', line 239

def populate_reference role       #:nodoc:
  role_type = role.role_type
  debug :references, "#{name} has #{role_type} role in '#{role.fact_type.describe}'"
  case role_type
  when :many_one
    ActiveFacts::Persistence::Reference.new(self, role).tabulate      # A simple reference

  when :one_many
    if role.fact_type.entity_type == self   # A Role of this objectified FactType
      ActiveFacts::Persistence::Reference.new(self, role).tabulate    # A simple reference; check that
    else
      # Can't absorb many of these into one of those
      #debug :references, "Ignoring #{role_type} reference from #{name} to #{Reference.new(self, role).to.name}"
    end

  when :unary
    ActiveFacts::Persistence::Reference.new(self, role).tabulate      # A simple reference

  when :supertype   # A subtype absorbs a reference to its supertype when separate, or all when partitioned
    # REVISIT: Or when partitioned
    if role.fact_type.subtype.is_independent
      debug :references, "supertype #{name} doesn't absorb a reference to separate subtype #{role.fact_type.subtype.name}"
    else
      r = ActiveFacts::Persistence::Reference.new(self, role)
      r.to.absorbed_via = r
      debug :references, "supertype #{name} absorbs subtype #{r.to.name}"
      r.tabulate
    end

  when :subtype    # This object is a supertype, which can absorb the subtype unless that's independent
    if is_independent    # REVISIT: Or when partitioned
      ActiveFacts::Persistence::Reference.new(self, role).tabulate
      # If partitioned, the supertype is absorbed into *each* subtype; a reference to the supertype needs to know which
    else
      # debug :references, "subtype #{name} is absorbed into #{role.fact_type.supertype.name}"
    end

  when :one_one
    r = ActiveFacts::Persistence::Reference.new(self, role)

    # Decide which way the one-to-one is likely to go; it will be flipped later if necessary.
    # Force the decision if just one is independent:
    r.tabulate and return if is_independent and !r.to.is_independent
    return if !is_independent and r.to.is_independent

    if is_a?(ValueType)
      # Never absorb an entity type into a value type
      return if r.to.is_a?(EntityType)  # Don't tabulate it
    else
      if r.to.is_a?(ValueType)
        r.tabulate  # Always absorb a value type into an entity type
        return 
      end

      # Force the decision if one EntityType identifies another:
      if preferred_identifier.role_sequence.all_role_ref.detect{|rr| rr.role == r.to_role}
        debug :references, "EntityType #{name} is identified by EntityType #{r.to.name}, so gets absorbed elsewhere"
        return
      end
      if r.to.preferred_identifier.role_sequence.all_role_ref.detect{|rr| rr.role == role}
        debug :references, "EntityType #{name} identifies EntityType #{r.to.name}, so absorbs it"
        r.to.absorbed_via = r
        r.tabulate
        return
      end
    end

    # Either both EntityTypes, or both ValueTypes.
    # Make an arbitrary (but stable) decision which way to go. We might flip it later.
    unless r.from.name < r.to.name or
      (r.from == r.to && references_to.detect{|ref| ref.to_role == role}) # one-to-one self reference, done already
      r.tabulate
    end
  else
    raise "Illegal role type, #{role.fact_type.describe(role)} no uniqueness constraint"
  end
end

#populate_referencesObject

:nodoc:



233
234
235
236
237
# File 'lib/activefacts/persistence/reference.rb', line 233

def populate_references           #:nodoc:
  all_role.each do |role|
    populate_reference role
  end
end

#probably_not_tableObject

:nodoc:



207
208
209
210
# File 'lib/activefacts/persistence/reference.rb', line 207

def probably_not_table            #:nodoc:
  @is_table = false
  @tentative = true
end

#probably_tableObject

:nodoc:



202
203
204
205
# File 'lib/activefacts/persistence/reference.rb', line 202

def probably_table                #:nodoc:
  @is_table = true
  @tentative = true
end

#references_fromObject

References from this Concept



213
214
215
# File 'lib/activefacts/persistence/reference.rb', line 213

def references_from
  @references_from ||= []
end

#references_toObject

References to this Concept



218
219
220
# File 'lib/activefacts/persistence/reference.rb', line 218

def references_to
  @references_to ||= []
end

#show_tabularObject

:nodoc:



187
188
189
190
# File 'lib/activefacts/persistence/reference.rb', line 187

def show_tabular                  #:nodoc:
  (tentative ? "tentatively " : "") +
  (is_table ? "" : "not ")+"a table"
end