Class: ActiveFacts::CQL::Compiler::EntityType

Inherits:
ObjectType show all
Defined in:
lib/activefacts/cql/compiler/entity_type.rb

Instance Attribute Summary

Attributes inherited from ObjectType

#name

Attributes inherited from Definition

#constellation, #tree, #vocabulary

Instance Method Summary collapse

Methods inherited from Definition

#all_bindings_in_clauses, #build_all_steps, #build_steps, #build_variables, #source

Constructor Details

#initialize(name, supertypes, identification, pragmas, clauses, context_note) ⇒ EntityType

Returns a new instance of EntityType.



22
23
24
25
26
27
28
29
# File 'lib/activefacts/cql/compiler/entity_type.rb', line 22

def initialize name, supertypes, identification, pragmas, clauses, context_note
  super name
  @supertypes = supertypes
  @identification = identification
  @pragmas = pragmas
  @clauses = clauses || []
	  @context_note = context_note
end

Instance Method Details

#add_supertype(supertype_name, not_identifying) ⇒ Object



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
# File 'lib/activefacts/cql/compiler/entity_type.rb', line 231

def add_supertype(supertype_name, not_identifying)
  debug :supertype, "Adding #{not_identifying ? '' : 'identifying '}supertype #{supertype_name} to #{@entity_type.name}" do
    supertype = @vocabulary.valid_entity_type_name(supertype_name) ||
	      @constellation.EntityType(@vocabulary, supertype_name, :concept => :new) # Should always already exist

    # Did we already know about this supertyping?
    return if @entity_type.all_type_inheritance_as_subtype.detect{|ti| ti.supertype == supertype}

    # By default, the first supertype identifies this entity type
    is_identifying_supertype = !not_identifying && @entity_type.all_type_inheritance_as_subtype.size == 0

    inheritance_fact = @constellation.TypeInheritance(@entity_type, supertype, :concept => :new)

    assimilations = @pragmas.select { |p| ['absorbed', 'separate', 'partitioned'].include? p}
    raise "Conflicting assimilation pragmas #{assimilations*', '}" if assimilations.size > 1
    inheritance_fact.assimilation = assimilations[0]

    # Create a reading:
    sub_role = @constellation.Role(inheritance_fact, 0, :object_type => @entity_type, :concept => :new)
    super_role = @constellation.Role(inheritance_fact, 1, :object_type => supertype, :concept => :new)

    rs = @constellation.RoleSequence(:new)
    @constellation.RoleRef(rs, 0, :role => sub_role)
    @constellation.RoleRef(rs, 1, :role => super_role)
    @constellation.Reading(inheritance_fact, 0, :role_sequence => rs, :text => "{0} is a kind of {1}", :is_negative => false)

    rs2 = @constellation.RoleSequence(:new)
    @constellation.RoleRef(rs2, 0, :role => super_role)
    @constellation.RoleRef(rs2, 1, :role => sub_role)
    # Decide in which order to include is a/is an. Provide both, but in order.
    n = 'aeiouh'.include?(sub_role.object_type.name.downcase[0]) ? 'n' : ''
    @constellation.Reading(inheritance_fact, 2, :role_sequence => rs2, :text => "{0} is a#{n} {1}", :is_negative => false)

    if is_identifying_supertype
      inheritance_fact.provides_identification = true
    end

    # Create uniqueness constraints over the subtyping fact type.
    p1rs = @constellation.RoleSequence(:new)
    @constellation.RoleRef(p1rs, 0).role = sub_role
    pc1 = @constellation.PresenceConstraint(:new, :vocabulary => @vocabulary)
    pc1.name = "#{@entity_type.name}MustHaveSupertype#{supertype.name}"
    pc1.role_sequence = p1rs
    pc1.is_mandatory = true   # A subtype instance must have a supertype instance
    pc1.min_frequency = 1
    pc1.max_frequency = 1
    pc1.is_preferred_identifier = false
	    debug :constraint, "Made new subtype PC GUID=#{pc1.concept.guid} min=1 max=1 over #{p1rs.describe}"

    p2rs = @constellation.RoleSequence(:new)
    constellation.RoleRef(p2rs, 0).role = super_role
    pc2 = constellation.PresenceConstraint(:new, :vocabulary => @vocabulary)
    pc2.name = "#{supertype.name}MayBeA#{@entity_type.name}"
    pc2.role_sequence = p2rs
    pc2.is_mandatory = false
    pc2.min_frequency = 0
    pc2.max_frequency = 1
    # The supertype role often identifies the subtype:
    pc2.is_preferred_identifier = inheritance_fact.provides_identification
	    debug :supertype, "identification of #{@entity_type.name} via supertype #{supertype.name} was #{inheritance_fact.provides_identification ? '' : 'not '}added"
	    debug :constraint, "Made new supertype PC GUID=#{pc2.concept.guid} min=1 max=1 over #{p2rs.describe}"
  end
end

#bind_identifying_roles(context) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/activefacts/cql/compiler/entity_type.rb', line 93

def bind_identifying_roles context
  return unless @identification
  @identification.map do |id|
    if id.is_a?(Reference)
      binding = id.binding
      roles = binding.refs.map{|r|r.role || (rr=r.role_ref and rr.role)}.compact.uniq
      raise "Looking for an occurrence of identifying role #{id.inspect}, but found #{roles.size == 0 ? "none" : roles.size}" if roles.size != 1
      roles[0]
    else
      # id is a clause of a unary fact type.
      id.identify_other_players context
      id.bind context
      matching_clause =
        @clauses.detect { |clause| clause.phrases_match id.phrases }
      raise "Unary identifying role '#{id.inspect}' is not found in the defined fact types" unless matching_clause
      matching_clause.fact_type.all_role.single
    end
  end
end

#compileObject



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/activefacts/cql/compiler/entity_type.rb', line 31

def compile
  @entity_type = @vocabulary.valid_entity_type_name(@name) ||
	    @constellation.EntityType(@vocabulary, @name, :concept => :new)
  @entity_type.is_independent = true if (@pragmas.include? 'independent')

  # REVISIT: CQL needs a way to indicate whether subtype migration can occur.
  # For example by saying "Xyz is a role of Abc".
  @supertypes.each_with_index do |supertype_name, i|
    add_supertype(supertype_name, @identification || i > 0)
  end

  context = CompilationContext.new(@vocabulary)

  # Identification may be via a mode (create it) or by forward-referenced entity types (allow those):
  prepare_identifier context

  context.bind @clauses, @identification.is_a?(Array) ? @identification : []

  # Create the fact types that define the identifying roles:
  fact_types = create_identifying_fact_types context

  # At this point, @identification is an array of References and/or Clauses (for unary fact types)
  # Have to do this after creating the necessary fact types
  complete_reference_mode_fact_type fact_types

  # Find the roles to use if we have to create an identifying uniqueness constraint:
  identifying_roles = bind_identifying_roles context

  make_preferred_identifier_over_roles identifying_roles

	  if @context_note
	    @context_note.compile(@constellation, @entity_type)
	  end

  @clauses.each do |clause|
    next unless clause.context_note
    clause.context_note.compile(@constellation, @entity_type)
  end

  @entity_type
end

#complete_reference_mode_fact_type(fact_types) ⇒ Object



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/activefacts/cql/compiler/entity_type.rb', line 324

def complete_reference_mode_fact_type(fact_types)
  return unless identifying_type = @reference_mode_value_type

  # Find an existing fact type, if any:
  entity_role = identifying_role = nil
  fact_type = fact_types.detect do |ft|
    identifying_role = ft.all_role.detect{|r| r.object_type == identifying_type } and
    entity_role = ft.all_role.detect{|r| r.object_type == @entity_type }
  end

  # Create an identifying fact type if needed:
  unless fact_type
    fact_type = @constellation.FactType(:new)
    fact_types << fact_type
    entity_role = @constellation.Role(fact_type, 0, :object_type => @entity_type, :concept => :new)
    identifying_role = @constellation.Role(fact_type, 1, :object_type => identifying_type, :concept => :new)
  end
  @identification[0].role = identifying_role

  if (value_constraint = @identification[0].value_constraint)
    # The value_constraint applies only to the value role, not to the underlying value type
    # Decide whether this puts the value_constraint in the right place:
    value_constraint.constellation = fact_type.constellation
    identifying_role.role_value_constraint = value_constraint.compile
  end

  # Find all role sequences over the fact type's two roles
  rss = entity_role.all_role_ref.select do |rr|
    rr.role_sequence.all_role_ref.size == 2 &&
      (rr.role_sequence.all_role_ref.to_a-[rr])[0].role == identifying_role
  end.map{|rr| rr.role_sequence}

  # Make a forward reading, if there is none already:
  # Find or create RoleSequences for the forward and reverse readings:
  rs01 = rss.select{|rs| rs.all_role_ref.sort_by{|rr| rr.ordinal}.map(&:role) == [entity_role, identifying_role] }[0]
  if !rs01
    rs01 = @constellation.RoleSequence(:new)
    @constellation.RoleRef(rs01, 0, :role => entity_role)
    @constellation.RoleRef(rs01, 1, :role => identifying_role)
  end
  if rs01.all_reading.empty?
    @constellation.Reading(fact_type, fact_type.all_reading.size, :role_sequence => rs01, :text => "{0} has {1}", :is_negative => false)
    debug :mode, "Creating new forward reading '#{entity_role.object_type.name} has #{identifying_type.name}'"
  else
    debug :mode, "Using existing forward reading"
  end

  # Make a reverse reading if none exists
  rs10 = rss.select{|rs| rs.all_role_ref.sort_by{|rr| rr.ordinal}.map(&:role) == [identifying_role, entity_role] }[0]
  if !rs10
    rs10 = @constellation.RoleSequence(:new)
    @constellation.RoleRef(rs10, 0, :role => identifying_role)
    @constellation.RoleRef(rs10, 1, :role => entity_role)
  end
  if rs10.all_reading.empty?
    @constellation.Reading(fact_type, fact_type.all_reading.size, :role_sequence => rs10, :text => "{0} is of {1}", :is_negative => false)
    debug :mode, "Creating new reverse reading '#{identifying_type.name} is of #{entity_role.object_type.name}'"
  else
    debug :mode, "Using existing reverse reading"
  end

  # Entity must have one identifying instance. Find or create the role sequence, then create a PC if necessary
  rs0 = entity_role.all_role_ref.select{|rr| rr.role_sequence.all_role_ref.size == 1}[0]
  if rs0
    rs0 = rs0.role_sequence
    debug :mode, "Using existing EntityType role sequence"
  else
    rs0 = @constellation.RoleSequence(:new)
    @constellation.RoleRef(rs0, 0, :role => entity_role)
    debug :mode, "Creating new EntityType role sequence"
  end
  if (rs0.all_presence_constraint.size == 0)
    constraint = @constellation.PresenceConstraint(
      :new,
      :name => '',
      :vocabulary => @vocabulary,
      :role_sequence => rs0,
      :min_frequency => 1,
      :max_frequency => 1,
      :is_preferred_identifier => false,
      :is_mandatory => true
    )
	    debug :constraint, "Made new refmode PC GUID=#{constraint.concept.guid} min=1 max=1 over #{rs0.describe}"
  else
    debug :mode, "Using existing EntityType PresenceConstraint"
  end

  # Value Type must have a value type. Find or create the role sequence, then create a PC if necessary
  debug :mode, "identifying_role has #{identifying_role.all_role_ref.size} attached sequences"
  debug :mode, "identifying_role has #{identifying_role.all_role_ref.select{|rr| rr.role_sequence.all_role_ref.size == 1}.size} unary sequences"
  rs1 = identifying_role.all_role_ref.select{|rr| rr.role_sequence.all_role_ref.size == 1 ? rr.role_sequence : nil }.compact[0]
  if (!rs1)
    rs1 = @constellation.RoleSequence(:new)
    @constellation.RoleRef(rs1, 0, :role => identifying_role)
    debug :mode, "Creating new ValueType role sequence"
  else
    rs1 = rs1.role_sequence
    debug :mode, "Using existing ValueType role sequence"
  end
  if (rs1.all_presence_constraint.size == 0)
    constraint = @constellation.PresenceConstraint(
      :new,
      :name => '',
      :vocabulary => @vocabulary,
      :role_sequence => rs1,
      :min_frequency => 0,
      :max_frequency => 1,
      :is_preferred_identifier => true,
      :is_mandatory => false
    )
	    debug :constraint, "Made new refmode ValueType PC GUID=#{constraint.concept.guid} min=0 max=1 over #{rs1.describe}"
  else
    debug :mode, "Marking existing ValueType PresenceConstraint as preferred"
    rs1.all_presence_constraint.single.is_preferred_identifier = true
  end
end

#create_identifying_fact_type(context, clauses) ⇒ Object



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
# File 'lib/activefacts/cql/compiler/entity_type.rb', line 172

def create_identifying_fact_type context, clauses
  # Remove uninteresting assertions:
  clauses.reject!{|clause| clause.is_existential_type }
  return nil unless clauses.size > 0    # Nothing interesting was said.

  # See if any fact type already exists (this ET cannot be a player, but might objectify it)
  existing_clauses = clauses.select{ |clause| clause.match_existing_fact_type context }
	  if negation = existing_clauses.detect{|c| c.certainty == false }
	    raise "#{@name} cannot be identified by negated fact type #{negation.inspect}"
	  end
  any_matched = existing_clauses.size > 0

  operation = any_matched ? 'Objectifying' : 'Creating'
  player_names = clauses[0].refs.map{|vr| vr.key.compact*'-'}
  debug :matching, "#{operation} fact type for #{clauses.size} clauses over (#{player_names*', '})" do
    if any_matched  # There's an existing fact type we must be objectifying
      fact_type = objectify_existing_fact_type(existing_clauses[0].fact_type)
    end

    unless fact_type
      fact_type = clauses[0].make_fact_type(@vocabulary)
      clauses[0].make_reading(@vocabulary, fact_type)
      clauses[0].make_embedded_constraints vocabulary
      existing_clauses = [clauses[0]]
    end

    (clauses - existing_clauses).each do |clause|
      clause.make_reading(@vocabulary, fact_type)
      clause.make_embedded_constraints vocabulary
    end

    fact_type
  end
end

#create_identifying_fact_types(context) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/activefacts/cql/compiler/entity_type.rb', line 153

def create_identifying_fact_types context
  fact_types = []
  # Categorise the clauses into fact types according to the roles they play.
  @clauses.inject({}) do |hash, clause|
    players_key = clause.refs.map{|vr| vr.key.compact}.sort
    (hash[players_key] ||= []) << clause
    hash
  end.each do |players_key, clauses|
    # REVISIT: Loose binding goes here; it might merge some Compiler#Roles

    fact_type = create_identifying_fact_type(context, clauses)
    fact_types << fact_type if fact_type
	    unless fact_type.all_role.detect{|r| r.object_type == @entity_type}
	      objectify_existing_fact_type(fact_type)
	    end
  end
  fact_types
end

#find_pc_over_roles(roles) ⇒ Object



142
143
144
145
146
147
148
149
150
151
# File 'lib/activefacts/cql/compiler/entity_type.rb', line 142

def find_pc_over_roles(roles)
  return nil if roles.size == 0 # Safeguard; this would chuck an exception otherwise
  roles[0].all_role_ref.each do |role_ref|
    next if role_ref.role_sequence.all_role_ref.map(&:role) != roles
    pc = role_ref.role_sequence.all_presence_constraint.single  # Will return nil if there's more than one.
    #puts "Existing PresenceConstraint matches those roles!" if pc
    return pc if pc
  end
  nil
end

Names used in the identifying roles list may be forward referenced:



87
88
89
90
91
# File 'lib/activefacts/cql/compiler/entity_type.rb', line 87

def legal_forward_references(identification_phrases)
  identification_phrases.map do |phrase|
    phrase.is_a?(Reference) ? phrase.term : nil
  end.compact.uniq
end

#make_entity_type_refmode_valuetypes(name, mode, parameters) ⇒ Object



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/activefacts/cql/compiler/entity_type.rb', line 295

def make_entity_type_refmode_valuetypes(name, mode, parameters)
  vt_name = "#{name}#{mode}"
  vt = nil
  debug :entity, "Preparing value type #{vt_name} for reference mode" do
    # Find an existing ValueType called 'vt_name' or 'name vtname'
	    # or find/create the supertype '#{mode}' and the subtype
    unless vt = @vocabulary.valid_object_type_name(vt_name) or
		   vt = @vocabulary.valid_object_type_name(vt_name = "#{name} #{mode}")
      base_vt = @vocabulary.valid_value_type_name(mode) ||
		  @constellation.ValueType(@vocabulary, mode, :concept => :new)
      vt = @constellation.ValueType(@vocabulary, vt_name, :supertype => base_vt, :concept => :new)
      if parameters
        length, scale = *parameters
        vt.length = length if length
        vt.scale = scale if scale
      end
    else
      debug :entity, "Value type #{vt_name} already exists"
    end
  end

  # REVISIT: If we do this, it gets emitted twice when we generate CQL.
  # The generator should detect that the value_constraint is the same and not emit it.
  #if (ranges = identification[:value_constraint])
  #  vt.value_constraint = value_constraint(ranges, identification[:enforcement])
  #end
  @reference_mode_value_type = vt
end

#make_preferred_identifier_over_roles(identifying_roles) ⇒ Object



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
# File 'lib/activefacts/cql/compiler/entity_type.rb', line 113

def make_preferred_identifier_over_roles identifying_roles
  return unless identifying_roles && identifying_roles.size > 0
  role_sequence = @constellation.RoleSequence(:new)
  identifying_roles.each_with_index do |identifying_role, index|
    @constellation.RoleRef(role_sequence, index, :role => identifying_role)
  end

  # Find a uniqueness constraint as PI, or make one
  pc = find_pc_over_roles(identifying_roles)
  if (pc)
    pc.is_preferred_identifier = true
    pc.name = "#{@entity_type.name}PK" unless pc.name
    debug "Existing PC #{pc.verbalise} is now PK for #{@entity_type.name}"
  else
    # Add a unique constraint over all identifying roles
    pc = @constellation.PresenceConstraint(
        :new,
        :vocabulary => @vocabulary,
        :name => "#{@entity_type.name}PK",            # Is this a useful name?
        :role_sequence => role_sequence,
        :is_preferred_identifier => true,
        :max_frequency => 1              # Unique
        #:is_mandatory => true,
        #:min_frequency => 1,
      )
	    debug :constraint, "Made new preferred PC GUID=#{pc.concept.guid} min=nil max=1 over #{role_sequence.describe}"
  end
end

#objectify_existing_fact_type(fact_type) ⇒ Object



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/activefacts/cql/compiler/entity_type.rb', line 207

def objectify_existing_fact_type fact_type
  raise "#{@name} cannot objectify fact type '#{fact_type.entity_type.name}' that's already objectified" if fact_type.entity_type
	  if @fact_type
	    raise "#{@name} cannot objectify '#{fact_type.default_reading}', it already objectifies '#{@fact_type.default_reading}'"
	  end

  if fact_type.internal_presence_constraints.select{|pc| pc.max_frequency == 1}.size == 0
    # If there's no existing uniqueness constraint over this fact type, make a spanning one.
    pc = @constellation.PresenceConstraint(
      :new,
      :vocabulary => @vocabulary,
      :name => @entity_type.name+"UQ",
      :role_sequence => fact_type.preferred_reading.role_sequence,
      :is_preferred_identifier => false,  # We only get here when there is a reference mode on the entity type
      :max_frequency => 1
    )
	    debug :constraint, "Made new objectification PC GUID=#{pc.concept.guid} min=nil max=1 over #{fact_type.preferred_reading.role_sequence.describe}"
  end

  @fact_type = @entity_type.fact_type = fact_type
  @entity_type.create_implicit_fact_types # REVISIT: Could there be readings for the implicit fact types here?
  @fact_type
end

#prepare_identifier(context) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/activefacts/cql/compiler/entity_type.rb', line 73

def prepare_identifier context
  # Figure out the identification mode or roles, if any:
  if @identification
    if @identification.is_a? ReferenceMode
      make_entity_type_refmode_valuetypes(name, @identification.name, @identification.parameters)
      vt_name = @reference_mode_value_type.name
      @identification = [Compiler::Reference.new(vt_name, nil, nil, nil, nil, nil, @identification.value_constraint, nil)]
    else
      context.allowed_forward_terms = legal_forward_references(@identification)
    end
  end
end

#to_sObject



441
442
443
444
445
446
447
448
449
450
451
# File 'lib/activefacts/cql/compiler/entity_type.rb', line 441

def to_s
  "EntityType: #{super} #{
    @supertypes.size > 0 ? "< #{@supertypes*','} " : ''
  }#{
    @identification.is_a?(ReferenceMode) ? @identification.to_s : @identification.inspect
  }#{
    @clauses.size > 0 ? " where #{@clauses.inspect}" : ''
  }#{
    @pragmas.size > 0 ? ", pragmas [#{@pragmas*','}]" : ''
  };"
end