Class: ActiveFacts::Metamodel::Vocabulary

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

Overview

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

Instance Method Summary collapse

Instance Method Details

#decide_tablesObject

:nodoc:



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
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
316
# File 'lib/activefacts/persistence/tables.rb', line 165

def decide_tables #:nodoc:
  # Strategy:
  # 1) Populate references for all Concepts
  # 2) Decide which Concepts must be and must not be tables
  #  a. Concepts labelled is_independent are tables (See the is_table methods above)
  #  b. Entity types having no references to them must be tables
  #  c. subtypes are not tables unless marked with assimilation = separate or partitioned
  #  d. ValueTypes are never tables unless they can have references (to other ValueTypes)
  #  e. An EntityType having an identifying AutoInc field must be a table unless it has exactly one reference
  #  f. An EntityType whose only reference is through its single preferred_identifier role gets absorbed
  #  g. An EntityType that must has references other than its PI must be a table (unless it has exactly one reference to it)
  #  h. supertypes are elided if all roles are absorbed into subtypes:
  #    - partitioned subtype exhaustion
  #    - subtype extension where supertype has only PI roles and no AutoInc
  # 3) any ValueType that has references from it must become a table if not already

  populate_all_references

  debug :absorption, "Calculating relational composition" do
    # Evaluate the possible independence of each concept, building an array of concepts of indeterminate status:
    undecided =
      all_concept.select do |concept|
        concept.is_table          # Ask it whether it thinks it should be a table
        concept.tentative         # Selection criterion
      end

    if debug :absorption, "Generating tables, #{undecided.size} undecided"
      (all_concept-undecided).each {|concept|
        next if ValueType === concept && !concept.is_table  # Skip unremarkable cases
        debug :absorption do
          debug :absorption, "#{concept.name} is #{concept.is_table ? "" : "not "}a table#{concept.tentative ? ", tentatively" : ""}"
        end
      }
    end

    pass = 0
    begin                         # Loop while we continue to make progress
      pass += 1
      debug :absorption, "Starting composition pass #{pass} with #{undecided.size} undecided tables"
      possible_flips = {}         # A hash by table containing an array of references that can be flipped
      finalised =                 # Make an array of things we finalised during this pass
        undecided.select do |concept|
          debug :absorption, "Considering #{concept.name}:" do
            debug :absorption, "refs to #{concept.name} are from #{concept.references_to.map{|ref| ref.from.name}*", "}" if concept.references_to.size > 0
            debug :absorption, "refs from #{concept.name} are to #{concept.references_from.map{|ref| ref.to.name rescue ref.fact_type.default_reading}*", "}" if concept.references_from.size > 0

            # Always absorb an objectified unary into its role player:
            if concept.fact_type && concept.fact_type.all_role.size == 1
              debug :absorption, "Absorb objectified unary #{concept.name} into #{concept.fact_type.entity_type.name}"
              concept.definitely_not_table
              next concept
            end

            # If the PI contains one role only, played by an entity type that can absorb us, do that.
            pi_roles = concept.preferred_identifier.role_sequence.all_role_ref.map(&:role)
            debug :absorption, "pi_roles are played by #{pi_roles.map{|role| role.concept.name}*", "}"
            first_pi_role = pi_roles[0]
            pi_ref = nil
            if pi_roles.size == 1 and
              concept.references_to.detect{|ref| pi_ref = ref if ref.from_role == first_pi_role && ref.from.is_a?(EntityType)}

              debug :absorption, "#{concept.name} is fully absorbed along its sole reference path into entity type #{pi_ref.from.name}"
              concept.definitely_not_table
              next concept
            end

            # If there's more than one absorption path and any functional dependencies that can't absorb us, it's a table
            non_identifying_refs_from =
              concept.references_from.reject{|ref|
                pi_roles.include?(ref.to_role)
              }
            debug :absorption, "#{concept.name} has #{non_identifying_refs_from.size} non-identifying functional roles"

            if concept.references_to.size > 1 and
                non_identifying_refs_from.size > 0
              debug :absorption, "#{concept.name} has non-identifying functional dependencies so 3NF requires it be a table"
              concept.definitely_table
              next concept
            end

            absorption_paths =
              (
                non_identifying_refs_from.reject do |ref|
                  !ref.to or ref.to.absorbed_via == ref
                end+concept.references_to
              ).reject do |ref|
                next true if !ref.to.is_table or
                  ![:one_one, :supertype, :subtype].include?(ref.role_type)

                # Don't absorb an object along a non-mandatory role (otherwise if it doesn't play that role, it can't exist either)
                from_is_mandatory = !!ref.is_mandatory
                to_is_mandatory = !ref.to_role || !!ref.to_role.is_mandatory

                bad = !(ref.from == concept ? from_is_mandatory : to_is_mandatory)
                debug :absorption, "Not absorbing #{concept.name} through non-mandatory #{ref}" if bad
                bad
              end

            # If this object can be fully absorbed, do that (might require flipping some references)
            if absorption_paths.size > 0
              debug :absorption, "#{concept.name} is fully absorbed through #{absorption_paths.inspect}"
              absorption_paths.each do |ref|
                debug :absorption, "flip #{ref} so #{concept.name} can be absorbed"
                ref.flip if concept == ref.from
              end
              concept.definitely_not_table
              next concept
            end

            if non_identifying_refs_from.size == 0
#                    and (!concept.is_a?(EntityType) ||
#                      # REVISIT: The roles may be collectively but not individually mandatory.
#                      concept.references_to.detect { |ref| !ref.from_role || ref.from_role.is_mandatory })
              debug :absorption, "#{concept.name} is fully absorbed in #{concept.references_to.size} places: #{concept.references_to.map{|ref| ref.from.name}*", "}"
              concept.definitely_not_table
              next concept
            end

            false   # Failed to decide about this entity_type this time around
          end
        end

      undecided -= finalised
      debug :absorption, "Finalised #{finalised.size} this pass: #{finalised.map{|f| f.name}*", "}"
    end while !finalised.empty?

    # A ValueType that isn't explicitly a table and isn't needed anywhere doesn't matter,
    # unless it should absorb something else (another ValueType is all it could be):
    all_concept.each do |concept|
      if (!concept.is_table and concept.references_to.size == 0 and concept.references_from.size > 0)
        debug :absorption, "Making #{concept.name} a table; it has nowhere else to go and needs to absorb things"
        concept.probably_table
      end
    end

    # Now, evaluate all possibilities of the tentative assignments
    # Incomplete. Apparently unnecessary as well... so far. We'll see.
    if debug :absorption
      undecided.each do |concept|
        debug :absorption, "Unable to decide independence of #{concept.name}, going with #{concept.show_tabular}"
      end
    end
  end

  populate_all_columns
  populate_all_indices

  @tables =
    all_concept.
    select { |f| f.is_table }.
    sort_by { |table| table.name }
end

#finish_schemaObject

Make schema transformations like adding ValueType self-value columns (and later, Rails-friendly ID fields). Override this method to change the transformations



365
366
367
368
369
# File 'lib/activefacts/persistence/columns.rb', line 365

def finish_schema
  all_concept.each do |concept|
    concept.self_value_reference if concept.is_a?(ActiveFacts::Metamodel::ValueType) && concept.is_table
  end
end

#populate_all_columnsObject

:nodoc:



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/activefacts/persistence/columns.rb', line 371

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_concept.each do |concept|
      next if !concept.is_table
      debug :columns, "Populating columns for table #{concept.name}" do
        concept.populate_columns
      end
    end
  end
  debug :columns, "Finished columns" do
    all_concept.each do |concept|
      next if !concept.is_table
      debug :columns, "Finished columns for table #{concept.name}" do
        concept.columns.each do |column|
          debug :columns, "#{column}"
        end
      end
    end
  end
end

#populate_all_indicesObject

:nodoc:



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/activefacts/persistence/index.rb', line 183

def populate_all_indices  #:nodoc:
  debug :index, "Populating all concept indices" do
    all_concept.each do |concept|
      concept.clear_indices
    end
    all_concept.each do |concept|
      next unless concept.is_table
      debug :index, "Populating indices for #{concept.name}" do
        concept.populate_indices
      end
    end
  end
  debug :index, "Finished concept indices" do
    all_concept.each do |concept|
      next unless concept.is_table
      next unless concept.indices.size > 0
      debug :index, "#{concept.name}:" do
        concept.indices.each do |index|
          debug :index, index
        end
      end
    end
  end
end

#populate_all_referencesObject

:nodoc:



342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/activefacts/persistence/reference.rb', line 342

def populate_all_references #:nodoc:
  debug :references, "Populating all concept references" do
    all_concept.each do |concept|
      concept.clear_references
      concept.is_table = nil      # Undecided; force an attempt to decide
      concept.tentative = true    # Uncertain
    end
    all_concept.each do |concept|
      debug :references, "Populating references for #{concept.name}" do
        concept.populate_references
      end
    end
  end
  debug :references, "Finished concept references" do
    all_concept.each do |concept|
      next unless concept.references_from.size > 0
      debug :references, "#{concept.name}:" do
        concept.references_from.each do |ref|
          debug :references, "#{ref}"
        end
      end
    end
  end
end

#tablesObject

return an Array of Concepts that will have their own tables



160
161
162
163
# File 'lib/activefacts/persistence/tables.rb', line 160

def tables
  decide_tables if !@tables
  @tables
end