Class: ActiveFacts::CQL::Compiler::FactType
- Inherits:
-
Query
- Object
- Definition
- ObjectType
- Query
- ActiveFacts::CQL::Compiler::FactType
- Defined in:
- lib/activefacts/cql/compiler/fact_type.rb
Instance Attribute Summary collapse
-
#clauses ⇒ Object
readonly
Returns the value of attribute clauses.
-
#fact_type ⇒ Object
readonly
Returns the value of attribute fact_type.
-
#name ⇒ Object
writeonly
Sets the attribute name.
-
#pragmas ⇒ Object
writeonly
Sets the attribute pragmas.
Attributes inherited from Query
Attributes inherited from ObjectType
Attributes inherited from Definition
#constellation, #tree, #vocabulary
Instance Method Summary collapse
- #check_compatibility_of_matched_clauses ⇒ Object
- #compile ⇒ Object
- #create_implicit_readings(ifts) ⇒ Object
- #has_more_adjectives(less, more) ⇒ Object
-
#initialize(name, clauses, conditions = nil, returning = nil) ⇒ FactType
constructor
A new instance of FactType.
- #inspect ⇒ Object
-
#is_projected_role(rr) ⇒ Object
A Comparison in the conditions which projects a role is not treated as a comparison, just as projection.
- #make_default_identifier_for_fact_type(prefer = true) ⇒ Object
- #project_clause_roles(clause) ⇒ Object
- #verify_matching_roles ⇒ Object
Methods inherited from Query
#detect_projection_by_equality, #match_condition_fact_types, #prepare_roles, #to_s
Methods inherited from ObjectType
Methods inherited from Definition
#all_bindings_in_clauses, #build_all_steps, #build_step, #build_variables, #source, #to_s
Constructor Details
#initialize(name, clauses, conditions = nil, returning = nil) ⇒ FactType
Returns a new instance of FactType.
85 86 87 88 89 90 91 92 |
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 85 def initialize name, clauses, conditions = nil, returning = nil super name, conditions, returning @clauses = clauses if ec = @clauses.detect{|r| r.is_equality_comparison} @clauses.delete(ec) @conditions.unshift(ec) end end |
Instance Attribute Details
#clauses ⇒ Object (readonly)
Returns the value of attribute clauses.
81 82 83 |
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 81 def clauses @clauses end |
#fact_type ⇒ Object (readonly)
Returns the value of attribute fact_type.
80 81 82 |
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 80 def fact_type @fact_type end |
#name=(value) ⇒ Object (writeonly)
Sets the attribute name
82 83 84 |
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 82 def name=(value) @name = value end |
#pragmas=(value) ⇒ Object (writeonly)
Sets the attribute pragmas
83 84 85 |
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 83 def pragmas=(value) @pragmas = value end |
Instance Method Details
#check_compatibility_of_matched_clauses ⇒ Object
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 |
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 237 def check_compatibility_of_matched_clauses # REVISIT: If we have conditions, we must match all given clauses exactly (no side-effects) @existing_clauses = @clauses. select{ |clause| clause.match_existing_fact_type @context }. # subtyping match is not allowed for fact type extension: reject{ |clause| clause.side_effects.role_side_effects.detect{|se| se.common_supertype } }. sort_by{ |clause| clause.side_effects.cost } fact_types = @existing_clauses.map{ |clause| clause.fact_type }.uniq.compact return nil if fact_types.empty? # There are no matched fact types if @clauses.size == 1 && @existing_clauses[0].side_effects.cost != 0 trace :matching, "There's only a single clause, but it's not an exact match" return nil end if (fact_types.size > 1) # There must be only one fact type with exact matches: if @existing_clauses[0].side_effects.cost != 0 or @existing_clauses.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.}*"', '"}'" end # Try to make false-matched clauses match the chosen one instead @existing_clauses.reject!{|r| r.fact_type != fact_types[0] } end fact_types[0] end |
#compile ⇒ Object
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 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 94 def compile # Process: # * Identify all role players (must be done for both clauses and conditions BEFORE matching clauses) # * Match up the players in all @clauses # - Be aware of multiple roles with the same player, and bind tight/loose using subscripts/role_names/adjectives # - Reject the fact type unless all @clauses match # * Find any existing fact type that matches any clause, or make a new one # * Add each clause 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 # # Prepare to objectify the fact type (so readings for implicit fact types can be created) if @name entity_type = @vocabulary.valid_entity_type_name(@name) raise "You can't objectify #{@name}, it already exists" if entity_type @entity_type = @constellation.EntityType(@vocabulary, @name, :fact_type => @fact_type, :concept => :new) end prepare_roles @clauses # REVISIT: Compiling the conditions here make it impossible to define a self-referential (transitive) query. return super if @clauses.empty? # It's a query return true unless @clauses.size > 0 # Nothing interesting was said. if @entity_type # Extract readings for implicit fact types @implicit_readings, @clauses = @clauses.partition do |clause| clause.refs.size == 2 and clause.refs.detect{|ref| ref.player == @entity_type} end end # See if any existing fact type is being invoked (presumably to objectify or extend it) @fact_type = check_compatibility_of_matched_clauses verify_matching_roles # All clauses of a fact type must have the same roles if !@fact_type # Make a new fact type: first_clause = @clauses[0] @fact_type = first_clause.make_fact_type(@vocabulary) first_clause.make_reading(@vocabulary, @fact_type) first_clause. vocabulary @fact_type.create_implicit_fact_type_for_unary if @fact_type.all_role.size == 1 && !@name @existing_clauses = [first_clause] elsif (n = @clauses.size - @existing_clauses.size) > 0 raise "Cannot extend a negated fact type" if @existing_clauses.detect {|clause| clause.certainty == false } trace :binding, "Extending existing fact type with #{n} new readings" end # Now make any new readings: new_clauses = @clauses - @existing_clauses new_clauses.each do |clause| clause.make_reading(@vocabulary, @fact_type) clause. vocabulary end # If a clause matched but the match left extra adjectives, we need to make a new RoleSequence for them: @existing_clauses.each do |clause| clause.adjust_for_match # Add any new constraints that we found in the match (presence, ring, etc) clause.(vocabulary) end if @name # Objectify the fact type: @entity_type.fact_type = @fact_type 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 ifts = @entity_type.create_implicit_fact_types create_implicit_readings(ifts) if @pragmas @entity_type.is_independent = true if @pragmas.delete('independent') end end @pragmas.each do |p| @constellation.ConceptAnnotation(:concept => (@entity_type||@fact_type).concept, :mapping_annotation => p) end if @pragmas @clauses.each do |clause| next unless clause.context_note clause.context_note.compile(@constellation, @fact_type) 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 if @conditions.empty? # Compile the conditions: super unless @conditions.empty? @clauses.each do |clause| project_clause_roles(clause) end end @fact_type end |
#create_implicit_readings(ifts) ⇒ Object
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 196 def create_implicit_readings(ifts) @implicit_readings.each do |clause| #next false unless clause.refs.size == 2 and clause.refs.detect{|r| r.role.object_type == @entity_type } other_ref = clause.refs.detect{|ref| ref.player != @entity_type} ift = ifts.detect do |ift| other_ref.binding.refs.map{|r| r.role}.include?(ift.) end next unless ift # This clause is a reading for the implicit LinkFactType ift i = 0 reading_text = clause.phrases.map do |phrase| next phrase if String === phrase "{#{(i += 1)-1}}" end*' ' ir = ActiveFacts::Metamodel::LinkFactType::ImplicitReading irrs = ir::ImplicitReadingRoleSequence irrf = irrs::ImplicitReadingRoleRef reading = ir.new(ift, reading_text) ift.add_reading reading end end |
#has_more_adjectives(less, more) ⇒ Object
334 335 336 337 338 |
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 334 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 |
#inspect ⇒ Object
411 412 413 414 415 416 417 |
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 411 def inspect s = super "FactType: #{@conditions.size > 0 ? super+' ' : '' }#{@clauses.inspect}" + (@pragmas && @pragmas.size > 0 ? ", pragmas [#{@pragmas.flatten.sort*','}]" : '') # REVISIT: @returning = returning end |
#is_projected_role(rr) ⇒ Object
A Comparison in the conditions which projects a role is not treated as a comparison, just as projection
229 230 231 232 233 234 235 |
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 229 def is_projected_role(rr) # rr is a RoleRef on one side of the comparison. # If its binding contains a reference from our readings, it's projected. rr.binding.refs.detect do |ref| @readings.include?(ref.reading) end end |
#make_default_identifier_for_fact_type(prefer = true) ⇒ Object
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 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 |
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 265 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: @clauses.each do |clause| ref = clause.refs[-1] or next epc = ref. or next epc.max_frequency == 1 or next next if epc.enforcement trace :constraint, "Converting UC into PI for #{@fact_type.entity_type.name}" epc.is_preferred_identifier = true return end # 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 trace :constraint, "Need to check #{@fact_type.default_reading.inspect} for a uniqueness constraint" fact_type.check_and_add_spanning_uniqueness_constraint = proc do trace :constraint, "Checking #{@fact_type.default_reading.inspect} for a uniqueness constraint" existing_pc = nil found = @fact_type.all_role. detect do |role| role.all_role_ref.detect do |rr| # This RoleSequence, to be relevant, must only reference roles of this fact type rr.role_sequence.all_role_ref.all? {|rr2| rr2.role.fact_type == @fact_type} and # The RoleSequence must have at least one uniqueness constraint rr.role_sequence.all_presence_constraint.detect do |pc| if pc.max_frequency == 1 existing_pc = pc end end end end true # A place for a breakpoint if !found # There's no existing uniqueness constraint over the roles of this fact type. Add one pc = @constellation.PresenceConstraint( :new, :vocabulary => @vocabulary, :name => @fact_type.entity_type ? @fact_type.entity_type.name+"PK" : '', :role_sequence => (rs = @fact_type.preferred_reading.role_sequence), :max_frequency => 1, :is_preferred_identifier => true # (prefer || !!@fact_type.entity_type) ) pc.concept.topic = @fact_type.concept.topic trace :constraint, "Made new fact type implicit PC GUID=#{pc.concept.guid} #{pc.name} min=nil max=1 over #{rs.describe}" elsif pc trace :constraint, "Will rely on existing UC GUID=#{pc.concept.guid} #{pc.name} to be used as PI over #{rs.describe}" end end end |
#project_clause_roles(clause) ⇒ Object
218 219 220 221 222 223 224 225 226 |
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 218 def project_clause_roles(clause) # Attach the clause's role references to the projected roles of the query clause.refs.each_with_index do |ref, i| role, play = @roles_by_binding[ref.binding] raise "#{ref} must be a role projected from the conditions" unless role raise "#{ref} has already-projected play!" if play.role_ref ref.role_ref.play = play end end |
#verify_matching_roles ⇒ Object
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 |
# File 'lib/activefacts/cql/compiler/fact_type.rb', line 340 def verify_matching_roles refs_by_clause_and_key = {} clauses_by_refs = @clauses.inject({}) do |hash, clause| keys = clause.refs.map do |ref| key = ref.key.compact refs_by_clause_and_key[[clause, key]] = ref 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] ||= []) << clause hash end if clauses_by_refs.size != 1 and @conditions.empty? # Attempt loose binding here; it might merge some Compiler::References to share the same Variables variants = clauses_by_refs.keys (clauses_by_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] clauses_l = clauses_by_refs[variants[l]] clauses_m = clauses_by_refs[variants[m]] l_keys = variants[l]-common m_keys = variants[m]-common trace :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_ref = refs_by_clause_and_key[[clauses_l[0], l_key]] m_ref = refs_by_clause_and_key[[clauses_m[0], m_key]] trace :binding, "Can we match #{l_ref.inspect} (#{i}) with #{m_ref.inspect} (#{j})?" next if m_ref.player != l_ref.player if has_more_adjectives(m_ref, l_ref) trace :binding, "can rebind #{m_ref.inspect} to #{l_ref.inspect}" candidates << [m_ref, l_ref] elsif has_more_adjectives(l_ref, m_ref) trace :binding, "can rebind #{l_ref.inspect} to #{m_ref.inspect}" candidates << [l_ref, m_ref] end end # trace :binding, "found #{candidates.size} rebinding candidates for this role" trace :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 trace :binding, "Successfully rebound clauses #{clauses_l.map{|r|r.inspect}*'; '} on to #{clauses_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 (#{ clauses_by_refs.keys.map do |keys| keys.map{|key| key*'-' }*", " end*") with (" })" end end end # else all clauses already matched end end |