Class: ActiveFacts::CQL::Compiler::FactType

Inherits:
Concept show all
Defined in:
lib/activefacts/cql/compiler/fact_type.rb

Instance Attribute Summary collapse

Attributes inherited from Concept

#name

Attributes inherited from Definition

#constellation, #source, #vocabulary

Instance Method Summary collapse

Constructor Details

#initialize(name, readings, conditions = nil, returning = nil) ⇒ FactType

Returns a new instance of FactType.



9
10
11
12
13
14
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 9

def initialize name, readings, conditions = nil, returning = nil
  super name
  @readings = readings
  @conditions = conditions
  @returning = returning
end

Instance Attribute Details

#fact_typeObject (readonly)

Returns the value of attribute fact_type.



6
7
8
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 6

def fact_type
  @fact_type
end

#name=(value) ⇒ Object (writeonly)

Sets the attribute name

Parameters:

  • value

    the value to set the attribute name to.



7
8
9
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 7

def name=(value)
  @name = value
end

Instance Method Details

#check_compatibility_of_matched_readingsObject



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 90

def check_compatibility_of_matched_readings
  @existing_readings = @readings.
    select{ |reading| reading.match_existing_fact_type @context }.
    sort_by{ |reading| reading.side_effects.cost }
  fact_types = @existing_readings.map{ |reading| reading.fact_type }.uniq.compact
  return nil if fact_types.empty? || @existing_readings[0].side_effects.cost != 0
  if (fact_types.size > 1)
    # There must be only one fact type with exact matches:
    if @existing_readings[0].side_effects.cost != 0 or
      @existing_readings.detect{|r| r.fact_type != fact_types[0] && r.side_effects.cost == 0 }
      raise "Clauses match different existing fact types '#{fact_types.map{|ft| ft.preferred_reading.expand}*"', '"}'"
    end
    # Try to make false-matched readings match the chosen one instead
    @existing_readings.reject!{|r| r.fact_type != fact_types[0] }
  end
  fact_types[0]
end

#compileObject



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 16

def compile
  raise "Queries not yet handled: #{@source}" unless @conditions.empty? and !@returning

  #
  # Process:
  # * Identify all role players
  # * Match up the players in all @readings
  #   - Be aware of multiple roles with the same player, and bind tight/loose using subscripts/role_names/adjectives
  #   - Reject the fact type unless all @readings match
  # * Find any existing fact type that matches any reading, or make a new one
  # * Add each reading that doesn't already exist in the fact type
  # * Create any ring constraint(s)
  # * Create embedded presence constraints
  # * If fact type has no identifier, arrange to create the implicit one (before first use?)
  # * Objectify the fact type if @name
  #

  @context = CompilationContext.new(@vocabulary)
  @readings.each{ |reading| reading.identify_players_with_role_name(@context) }
  @readings.each{ |reading| reading.identify_other_players(@context) }
  @readings.each{ |reading| reading.bind_roles @context }  # Create the Compiler::Bindings

  verify_matching_roles   # All readings of a fact type must have the same roles

  # Ignore any useless readings:
  @readings.reject!{|reading| reading.is_existential_type }
  return true unless @readings.size > 0   # Nothing interesting was said.

  # See if any existing fact type is being invoked (presumably to objectify or extend it)
  @fact_type = check_compatibility_of_matched_readings

  if !@fact_type
    # Make a new fact type:
    first_reading = @readings[0]
    @fact_type = first_reading.make_fact_type(@vocabulary)
    first_reading.make_reading(@vocabulary, @fact_type)
    first_reading.make_embedded_constraints vocabulary
    @fact_type.create_implicit_fact_type_for_unary if @fact_type.all_role.size == 1 && !@name
    @existing_readings = [first_reading]
  elsif (n = @readings.size - @existing_readings.size) > 0
    debug :binding, "Extending existing fact type with #{n} new readings"
  end

  # Now make any new readings:
  new_readings = @readings - @existing_readings
  new_readings.each do |reading|
    reading.make_reading(@vocabulary, @fact_type)
    reading.make_embedded_constraints vocabulary
  end

  # If a reading matched but the match left extra adjectives, we need to make a new RoleSequence for them:
  @existing_readings.each do |reading|
    reading.adjust_for_match
  end

  # Objectify the fact type if necessary:
  if @name
    if @fact_type.entity_type and @name != @fact_type.entity_type.name
      raise "Cannot objectify fact type as #{@name} and as #{@fact_type.entity_type.name}"
    end
    @constellation.EntityType(@vocabulary, @name, :fact_type => @fact_type).create_implicit_fact_types
  end

  # REVISIT: This isn't the thing to do long term; it needs to be added later only if we find no other constraint
  make_default_identifier_for_fact_type

  @readings.each do |reading|
    next unless reading.context_note
    reading.context_note.compile(@constellation, @fact_type)
  end

  @fact_type
end

#has_more_adjectives(less, more) ⇒ Object



150
151
152
153
154
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 150

def has_more_adjectives(less, more)
  return false if less.leading_adjective && less.leading_adjective != more.leading_adjective
  return false if less.trailing_adjective && less.trailing_adjective != more.trailing_adjective
  return true
end

#make_default_identifier_for_fact_type(prefer = true) ⇒ Object



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

def make_default_identifier_for_fact_type(prefer = true)
  # Non-objectified unaries don't need a PI:
  return if @fact_type.all_role.size == 1 && !@fact_type.entity_type

  # It's possible that this fact type is objectified and inherits identification through a supertype.
  return if @fact_type.entity_type and @fact_type.entity_type.all_type_inheritance_as_subtype.detect{|ti| ti.provides_identification}

  # If it's a non-objectified binary and there's an alethic uniqueness constraint over the fact type already, we're done
  return if !@fact_type.entity_type &&
    @fact_type.all_role.size == 2 &&
    @fact_type.all_role.
      detect do |r|
        r.all_role_ref.detect do |rr|
          rr.role_sequence.all_presence_constraint.detect do |pc|
            pc.max_frequency == 1 && !pc.enforcement
          end
        end
      end

  # If there's an existing presence constraint that can be converted into a PC, do that:
  @readings.each do |reading|
    rr = reading.role_refs[-1] or next
    epc = rr.embedded_presence_constraint or next
    epc.max_frequency == 1 or next
    next if epc.enforcement
    epc.is_preferred_identifier = true
    return
  end

  # REVISIT: We need to check uniqueness constraints after processing the whole vocabulary
  # raise "Fact type must be named as it has no identifying uniqueness constraint" unless @name || @fact_type.all_role.size == 1

  @constellation.PresenceConstraint(
    :new,
    :vocabulary => @vocabulary,
    :name => @fact_type.entity_type ? @fact_type.entity_type.name+"PK" : '',
    :role_sequence => @fact_type.preferred_reading.role_sequence,
    :max_frequency => 1,
    :is_preferred_identifier => prefer
  )
end

#verify_matching_rolesObject



156
157
158
159
160
161
162
163
164
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
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 156

def verify_matching_roles
  role_refs_by_reading_and_key = {}
  readings_by_role_refs =
    @readings.inject({}) do |hash, reading|
      keys = reading.role_refs.map do |rr|
        key = rr.key.compact
        role_refs_by_reading_and_key[[reading, key]] = rr
        key
      end.sort_by{|a| a.map{|k|k.to_s}}
      raise "Fact types may not have duplicate roles" if keys.uniq.size < keys.size
      (hash[keys] ||= []) << reading
      hash
    end

  if readings_by_role_refs.size != 1
    # Attempt loose binding here; it might merge some Compiler::RoleRefs to share the same Bindings
    variants = readings_by_role_refs.keys
    (readings_by_role_refs.size-1).downto(1) do |m|   # Start with the last one
      0.upto(m-1) do |l|                              # Try to rebind onto any lower one
        common = variants[m]&variants[l]
        readings_l = readings_by_role_refs[variants[l]]
        readings_m = readings_by_role_refs[variants[m]]
        l_keys = variants[l]-common
        m_keys = variants[m]-common
        debug :binding, "Try to collapse variant #{m} onto #{l}; diffs are #{l_keys.inspect} -> #{m_keys.inspect}"
        rebindings = 0
        l_keys.each_with_index do |l_key, i|
          # Find possible rebinding candidates; there must be exactly one.
          candidates = []
          (0...m_keys.size).each do |j|
            m_key = m_keys[j]
            l_role_ref = role_refs_by_reading_and_key[[readings_l[0], l_key]]
            m_role_ref = role_refs_by_reading_and_key[[readings_m[0], m_key]]
            debug :binding, "Can we match #{l_role_ref.inspect} (#{i}) with #{m_role_ref.inspect} (#{j})?"
            next if m_role_ref.player != l_role_ref.player
            if has_more_adjectives(m_role_ref, l_role_ref)
              debug :binding, "can rebind #{m_role_ref.inspect} to #{l_role_ref.inspect}"
              candidates << [m_role_ref, l_role_ref]
            elsif has_more_adjectives(l_role_ref, m_role_ref)
              debug :binding, "can rebind #{l_role_ref.inspect} to #{m_role_ref.inspect}"
              candidates << [l_role_ref, m_role_ref]
            end
          end

          # debug :binding, "found #{candidates.size} rebinding candidates for this role"
          debug :binding, "rebinding is ambiguous so not attempted" if candidates.size > 1
          if (candidates.size == 1)
            candidates[0][0].rebind_to(@context, candidates[0][1])
            rebindings += 1
          end

        end
        if (rebindings == l_keys.size)
          # Successfully rebound this fact type
          debug :binding, "Successfully rebound readings #{readings_l.map{|r|r.inspect}*'; '} on to #{readings_m.map{|r|r.inspect}*'; '}"
          break
        else
          # No point continuing, we failed on this one.
          raise "All readings in a fact type definition must have matching role players, compare (#{
              readings_by_role_refs.keys.map do |keys|
                keys.map{|key| key*'-' }*", "
              end*") with ("
            })"
        end

      end
    end
  # else all readings already matched
  end
end