activefacts-cql 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +19 -0
- data/Rakefile +6 -0
- data/activefacts-cql.gemspec +29 -0
- data/bin/setup +7 -0
- data/lib/activefacts/cql.rb +7 -0
- data/lib/activefacts/cql/.gitignore +0 -0
- data/lib/activefacts/cql/Rakefile +14 -0
- data/lib/activefacts/cql/compiler.rb +156 -0
- data/lib/activefacts/cql/compiler/clause.rb +1137 -0
- data/lib/activefacts/cql/compiler/constraint.rb +581 -0
- data/lib/activefacts/cql/compiler/entity_type.rb +457 -0
- data/lib/activefacts/cql/compiler/expression.rb +443 -0
- data/lib/activefacts/cql/compiler/fact.rb +390 -0
- data/lib/activefacts/cql/compiler/fact_type.rb +421 -0
- data/lib/activefacts/cql/compiler/query.rb +106 -0
- data/lib/activefacts/cql/compiler/shared.rb +161 -0
- data/lib/activefacts/cql/compiler/value_type.rb +174 -0
- data/lib/activefacts/cql/parser.rb +234 -0
- data/lib/activefacts/cql/parser/CQLParser.treetop +167 -0
- data/lib/activefacts/cql/parser/Context.treetop +48 -0
- data/lib/activefacts/cql/parser/Expressions.treetop +67 -0
- data/lib/activefacts/cql/parser/FactTypes.treetop +358 -0
- data/lib/activefacts/cql/parser/Language/English.treetop +315 -0
- data/lib/activefacts/cql/parser/Language/French.treetop +315 -0
- data/lib/activefacts/cql/parser/Language/Mandarin.treetop +304 -0
- data/lib/activefacts/cql/parser/LexicalRules.treetop +253 -0
- data/lib/activefacts/cql/parser/ObjectTypes.treetop +210 -0
- data/lib/activefacts/cql/parser/Terms.treetop +183 -0
- data/lib/activefacts/cql/parser/ValueTypes.treetop +202 -0
- data/lib/activefacts/cql/parser/nodes.rb +49 -0
- data/lib/activefacts/cql/require.rb +36 -0
- data/lib/activefacts/cql/verbaliser.rb +804 -0
- data/lib/activefacts/cql/version.rb +5 -0
- data/lib/activefacts/input/cql.rb +43 -0
- data/lib/rubygems_plugin.rb +12 -0
- metadata +167 -0
@@ -0,0 +1,421 @@
|
|
1
|
+
module ActiveFacts
|
2
|
+
module CQL
|
3
|
+
class Compiler < ActiveFacts::CQL::Parser
|
4
|
+
|
5
|
+
class Query < ObjectType # A fact type which is objectified (whether derived or not) is also an ObjectType
|
6
|
+
attr_reader :context # Exposed for testing purposes
|
7
|
+
attr_reader :conditions
|
8
|
+
|
9
|
+
def initialize name, conditions = nil, returning = nil
|
10
|
+
super name
|
11
|
+
@conditions = conditions
|
12
|
+
@returning = returning || []
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
inspect
|
17
|
+
end
|
18
|
+
|
19
|
+
def inspect
|
20
|
+
"Query: " +
|
21
|
+
if @conditions.empty?
|
22
|
+
''
|
23
|
+
else
|
24
|
+
'where ' + @conditions.map{|c| ((j=c.conjunction) ? j+' ' : '') + c.inspect}*' '
|
25
|
+
end
|
26
|
+
# REVISIT: @returning = returning
|
27
|
+
end
|
28
|
+
|
29
|
+
def prepare_roles clauses = nil
|
30
|
+
trace :binding, "preparing roles" do
|
31
|
+
@context ||= CompilationContext.new(@vocabulary)
|
32
|
+
@context.bind clauses||[], @conditions, @returning
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def compile
|
37
|
+
# Match roles with players, and match clauses with existing fact types
|
38
|
+
prepare_roles unless @context
|
39
|
+
|
40
|
+
@context.left_contraction_allowed = true
|
41
|
+
match_condition_fact_types
|
42
|
+
|
43
|
+
# Build the query:
|
44
|
+
unless @conditions.empty? and @returning.empty?
|
45
|
+
trace :query, "building query for derived fact type (returning #{@returning}) with #{@conditions.size} conditions: (#{@conditions.map{|c|c.inspect}*', '})" do
|
46
|
+
@query = build_variables(@conditions.flatten)
|
47
|
+
@roles_by_binding = build_all_steps(@conditions)
|
48
|
+
@query.validate
|
49
|
+
@query
|
50
|
+
end
|
51
|
+
end
|
52
|
+
@context.left_contraction_allowed = false
|
53
|
+
@query
|
54
|
+
end
|
55
|
+
|
56
|
+
def match_condition_fact_types
|
57
|
+
@conditions.each do |condition|
|
58
|
+
trace :projection, "matching condition fact_type #{condition.inspect}" do
|
59
|
+
fact_type = condition.match_existing_fact_type @context
|
60
|
+
raise "Unrecognised fact type #{condition.inspect} in #{self.class}" unless fact_type
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def detect_projection_by_equality condition
|
66
|
+
return false unless condition.is_a?(Comparison)
|
67
|
+
if is_projected_role(condition.e1)
|
68
|
+
condition.project :left
|
69
|
+
elsif is_projected_role(condition.e2)
|
70
|
+
condition.project :right
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def is_projected_role(rr)
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class FactType < ActiveFacts::CQL::Compiler::Query
|
80
|
+
attr_reader :fact_type
|
81
|
+
attr_reader :clauses
|
82
|
+
attr_writer :name
|
83
|
+
attr_writer :pragmas
|
84
|
+
|
85
|
+
def initialize name, clauses, conditions = nil, returning = nil
|
86
|
+
super name, conditions, returning
|
87
|
+
@clauses = clauses
|
88
|
+
if ec = @clauses.detect{|r| r.is_equality_comparison}
|
89
|
+
@clauses.delete(ec)
|
90
|
+
@conditions.unshift(ec)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def compile
|
95
|
+
# Process:
|
96
|
+
# * Identify all role players (must be done for both clauses and conditions BEFORE matching clauses)
|
97
|
+
# * Match up the players in all @clauses
|
98
|
+
# - Be aware of multiple roles with the same player, and bind tight/loose using subscripts/role_names/adjectives
|
99
|
+
# - Reject the fact type unless all @clauses match
|
100
|
+
# * Find any existing fact type that matches any clause, or make a new one
|
101
|
+
# * Add each clause that doesn't already exist in the fact type
|
102
|
+
# * Create any ring constraint(s)
|
103
|
+
# * Create embedded presence constraints
|
104
|
+
# * If fact type has no identifier, arrange to create the implicit one (before first use?)
|
105
|
+
# * Objectify the fact type if @name
|
106
|
+
#
|
107
|
+
|
108
|
+
# Prepare to objectify the fact type (so readings for implicit fact types can be created)
|
109
|
+
if @name
|
110
|
+
entity_type = @vocabulary.valid_entity_type_name(@name)
|
111
|
+
raise "You can't objectify #{@name}, it already exists" if entity_type
|
112
|
+
@entity_type = @constellation.EntityType(@vocabulary, @name, :fact_type => @fact_type, :concept => :new)
|
113
|
+
end
|
114
|
+
|
115
|
+
prepare_roles @clauses
|
116
|
+
|
117
|
+
# REVISIT: Compiling the conditions here make it impossible to define a self-referential (transitive) query.
|
118
|
+
return super if @clauses.empty? # It's a query
|
119
|
+
|
120
|
+
return true unless @clauses.size > 0 # Nothing interesting was said.
|
121
|
+
|
122
|
+
if @entity_type
|
123
|
+
# Extract readings for implicit fact types
|
124
|
+
@implicit_readings, @clauses =
|
125
|
+
@clauses.partition do |clause|
|
126
|
+
clause.refs.size == 2 and clause.refs.detect{|ref| ref.player == @entity_type}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# See if any existing fact type is being invoked (presumably to objectify or extend it)
|
131
|
+
@fact_type = check_compatibility_of_matched_clauses
|
132
|
+
verify_matching_roles # All clauses of a fact type must have the same roles
|
133
|
+
|
134
|
+
if !@fact_type
|
135
|
+
# Make a new fact type:
|
136
|
+
first_clause = @clauses[0]
|
137
|
+
@fact_type = first_clause.make_fact_type(@vocabulary)
|
138
|
+
first_clause.make_reading(@vocabulary, @fact_type)
|
139
|
+
first_clause.make_embedded_constraints vocabulary
|
140
|
+
@fact_type.create_implicit_fact_type_for_unary if @fact_type.all_role.size == 1 && !@name
|
141
|
+
@existing_clauses = [first_clause]
|
142
|
+
elsif (n = @clauses.size - @existing_clauses.size) > 0
|
143
|
+
raise "Cannot extend a negated fact type" if @existing_clauses.detect {|clause| clause.certainty == false }
|
144
|
+
trace :binding, "Extending existing fact type with #{n} new readings"
|
145
|
+
end
|
146
|
+
|
147
|
+
# Now make any new readings:
|
148
|
+
new_clauses = @clauses - @existing_clauses
|
149
|
+
new_clauses.each do |clause|
|
150
|
+
clause.make_reading(@vocabulary, @fact_type)
|
151
|
+
clause.make_embedded_constraints vocabulary
|
152
|
+
end
|
153
|
+
|
154
|
+
# If a clause matched but the match left extra adjectives, we need to make a new RoleSequence for them:
|
155
|
+
@existing_clauses.each do |clause|
|
156
|
+
clause.adjust_for_match
|
157
|
+
# Add any new constraints that we found in the match (presence, ring, etc)
|
158
|
+
clause.make_embedded_constraints(vocabulary)
|
159
|
+
end
|
160
|
+
|
161
|
+
if @name
|
162
|
+
# Objectify the fact type:
|
163
|
+
@entity_type.fact_type = @fact_type
|
164
|
+
if @fact_type.entity_type and @name != @fact_type.entity_type.name
|
165
|
+
raise "Cannot objectify fact type as #{@name} and as #{@fact_type.entity_type.name}"
|
166
|
+
end
|
167
|
+
ifts = @entity_type.create_implicit_fact_types
|
168
|
+
create_implicit_readings(ifts)
|
169
|
+
if @pragmas
|
170
|
+
@entity_type.is_independent = true if @pragmas.delete('independent')
|
171
|
+
end
|
172
|
+
end
|
173
|
+
@pragmas.each do |p|
|
174
|
+
@constellation.ConceptAnnotation(:concept => (@entity_type||@fact_type).concept, :mapping_annotation => p)
|
175
|
+
end if @pragmas
|
176
|
+
|
177
|
+
@clauses.each do |clause|
|
178
|
+
next unless clause.context_note
|
179
|
+
clause.context_note.compile(@constellation, @fact_type)
|
180
|
+
end
|
181
|
+
|
182
|
+
# REVISIT: This isn't the thing to do long term; it needs to be added later only if we find no other constraint
|
183
|
+
make_default_identifier_for_fact_type if @conditions.empty?
|
184
|
+
|
185
|
+
# Compile the conditions:
|
186
|
+
super
|
187
|
+
unless @conditions.empty?
|
188
|
+
@clauses.each do |clause|
|
189
|
+
project_clause_roles(clause)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
@fact_type
|
194
|
+
end
|
195
|
+
|
196
|
+
def create_implicit_readings(ifts)
|
197
|
+
@implicit_readings.each do |clause|
|
198
|
+
#next false unless clause.refs.size == 2 and clause.refs.detect{|r| r.role.object_type == @entity_type }
|
199
|
+
other_ref = clause.refs.detect{|ref| ref.player != @entity_type}
|
200
|
+
ift = ifts.detect do |ift|
|
201
|
+
other_ref.binding.refs.map{|r| r.role}.include?(ift.implying_role)
|
202
|
+
end
|
203
|
+
next unless ift
|
204
|
+
# This clause is a reading for the implicit LinkFactType ift
|
205
|
+
i = 0
|
206
|
+
reading_text = clause.phrases.map do |phrase|
|
207
|
+
next phrase if String === phrase
|
208
|
+
"{#{(i += 1)-1}}"
|
209
|
+
end*' '
|
210
|
+
ir = ActiveFacts::Metamodel::LinkFactType::ImplicitReading
|
211
|
+
irrs = ir::ImplicitReadingRoleSequence
|
212
|
+
irrf = irrs::ImplicitReadingRoleRef
|
213
|
+
reading = ir.new(ift, reading_text)
|
214
|
+
ift.add_reading reading
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def project_clause_roles(clause)
|
219
|
+
# Attach the clause's role references to the projected roles of the query
|
220
|
+
clause.refs.each_with_index do |ref, i|
|
221
|
+
role, play = @roles_by_binding[ref.binding]
|
222
|
+
raise "#{ref} must be a role projected from the conditions" unless role
|
223
|
+
raise "#{ref} has already-projected play!" if play.role_ref
|
224
|
+
ref.role_ref.play = play
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# A Comparison in the conditions which projects a role is not treated as a comparison, just as projection
|
229
|
+
def is_projected_role(rr)
|
230
|
+
# rr is a RoleRef on one side of the comparison.
|
231
|
+
# If its binding contains a reference from our readings, it's projected.
|
232
|
+
rr.binding.refs.detect do |ref|
|
233
|
+
@readings.include?(ref.reading)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def check_compatibility_of_matched_clauses
|
238
|
+
# REVISIT: If we have conditions, we must match all given clauses exactly (no side-effects)
|
239
|
+
@existing_clauses = @clauses.
|
240
|
+
select{ |clause| clause.match_existing_fact_type @context }.
|
241
|
+
# subtyping match is not allowed for fact type extension:
|
242
|
+
reject{ |clause| clause.side_effects.role_side_effects.detect{|se| se.common_supertype } }.
|
243
|
+
sort_by{ |clause| clause.side_effects.cost }
|
244
|
+
fact_types = @existing_clauses.map{ |clause| clause.fact_type }.uniq.compact
|
245
|
+
|
246
|
+
return nil if fact_types.empty? # There are no matched fact types
|
247
|
+
|
248
|
+
if @clauses.size == 1 && @existing_clauses[0].side_effects.cost != 0
|
249
|
+
trace :matching, "There's only a single clause, but it's not an exact match"
|
250
|
+
return nil
|
251
|
+
end
|
252
|
+
|
253
|
+
if (fact_types.size > 1)
|
254
|
+
# There must be only one fact type with exact matches:
|
255
|
+
if @existing_clauses[0].side_effects.cost != 0 or
|
256
|
+
@existing_clauses.detect{|r| r.fact_type != fact_types[0] && r.side_effects.cost == 0 }
|
257
|
+
raise "Clauses match different existing fact types '#{fact_types.map{|ft| ft.preferred_reading.expand}*"', '"}'"
|
258
|
+
end
|
259
|
+
# Try to make false-matched clauses match the chosen one instead
|
260
|
+
@existing_clauses.reject!{|r| r.fact_type != fact_types[0] }
|
261
|
+
end
|
262
|
+
fact_types[0]
|
263
|
+
end
|
264
|
+
|
265
|
+
def make_default_identifier_for_fact_type(prefer = true)
|
266
|
+
# Non-objectified unaries don't need a PI:
|
267
|
+
return if @fact_type.all_role.size == 1 && !@fact_type.entity_type
|
268
|
+
|
269
|
+
# It's possible that this fact type is objectified and inherits identification through a supertype.
|
270
|
+
return if @fact_type.entity_type and @fact_type.entity_type.all_type_inheritance_as_subtype.detect{|ti| ti.provides_identification}
|
271
|
+
|
272
|
+
# If it's a non-objectified binary and there's an alethic uniqueness constraint over the fact type already, we're done
|
273
|
+
return if !@fact_type.entity_type &&
|
274
|
+
@fact_type.all_role.size == 2 &&
|
275
|
+
@fact_type.all_role.
|
276
|
+
detect do |r|
|
277
|
+
r.all_role_ref.detect do |rr|
|
278
|
+
rr.role_sequence.all_presence_constraint.detect do |pc|
|
279
|
+
pc.max_frequency == 1 && !pc.enforcement
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# If there's an existing presence constraint that can be converted into a PC, do that:
|
285
|
+
@clauses.each do |clause|
|
286
|
+
ref = clause.refs[-1] or next
|
287
|
+
epc = ref.embedded_presence_constraint or next
|
288
|
+
epc.max_frequency == 1 or next
|
289
|
+
next if epc.enforcement
|
290
|
+
trace :constraint, "Converting UC into PI for #{@fact_type.entity_type.name}"
|
291
|
+
epc.is_preferred_identifier = true
|
292
|
+
return
|
293
|
+
end
|
294
|
+
|
295
|
+
# We need to check uniqueness constraints after processing the whole vocabulary
|
296
|
+
# raise "Fact type must be named as it has no identifying uniqueness constraint" unless @name || @fact_type.all_role.size == 1
|
297
|
+
trace :constraint, "Need to check #{@fact_type.default_reading.inspect} for a uniqueness constraint"
|
298
|
+
fact_type.check_and_add_spanning_uniqueness_constraint = proc do
|
299
|
+
trace :constraint, "Checking #{@fact_type.default_reading.inspect} for a uniqueness constraint"
|
300
|
+
existing_pc = nil
|
301
|
+
found = @fact_type.all_role.
|
302
|
+
detect do |role|
|
303
|
+
role.all_role_ref.detect do |rr|
|
304
|
+
# This RoleSequence, to be relevant, must only reference roles of this fact type
|
305
|
+
rr.role_sequence.all_role_ref.all? {|rr2| rr2.role.fact_type == @fact_type} and
|
306
|
+
# The RoleSequence must have at least one uniqueness constraint
|
307
|
+
rr.role_sequence.all_presence_constraint.detect do |pc|
|
308
|
+
if pc.max_frequency == 1
|
309
|
+
existing_pc = pc
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
true # A place for a breakpoint
|
315
|
+
|
316
|
+
if !found
|
317
|
+
# There's no existing uniqueness constraint over the roles of this fact type. Add one
|
318
|
+
pc = @constellation.PresenceConstraint(
|
319
|
+
:new,
|
320
|
+
:vocabulary => @vocabulary,
|
321
|
+
:name => @fact_type.entity_type ? @fact_type.entity_type.name+"PK" : '',
|
322
|
+
:role_sequence => (rs = @fact_type.preferred_reading.role_sequence),
|
323
|
+
:max_frequency => 1,
|
324
|
+
:is_preferred_identifier => true # (prefer || !!@fact_type.entity_type)
|
325
|
+
)
|
326
|
+
pc.concept.topic = @fact_type.concept.topic
|
327
|
+
trace :constraint, "Made new fact type implicit PC GUID=#{pc.concept.guid} #{pc.name} min=nil max=1 over #{rs.describe}"
|
328
|
+
elsif pc
|
329
|
+
trace :constraint, "Will rely on existing UC GUID=#{pc.concept.guid} #{pc.name} to be used as PI over #{rs.describe}"
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
def has_more_adjectives(less, more)
|
335
|
+
return false if less.leading_adjective && less.leading_adjective != more.leading_adjective
|
336
|
+
return false if less.trailing_adjective && less.trailing_adjective != more.trailing_adjective
|
337
|
+
return true
|
338
|
+
end
|
339
|
+
|
340
|
+
def verify_matching_roles
|
341
|
+
refs_by_clause_and_key = {}
|
342
|
+
clauses_by_refs =
|
343
|
+
@clauses.inject({}) do |hash, clause|
|
344
|
+
keys = clause.refs.map do |ref|
|
345
|
+
key = ref.key.compact
|
346
|
+
refs_by_clause_and_key[[clause, key]] = ref
|
347
|
+
key
|
348
|
+
end.sort_by{|a| a.map{|k|k.to_s}}
|
349
|
+
raise "Fact types may not have duplicate roles" if keys.uniq.size < keys.size
|
350
|
+
(hash[keys] ||= []) << clause
|
351
|
+
hash
|
352
|
+
end
|
353
|
+
|
354
|
+
if clauses_by_refs.size != 1 and @conditions.empty?
|
355
|
+
# Attempt loose binding here; it might merge some Compiler::References to share the same Variables
|
356
|
+
variants = clauses_by_refs.keys
|
357
|
+
(clauses_by_refs.size-1).downto(1) do |m| # Start with the last one
|
358
|
+
0.upto(m-1) do |l| # Try to rebind onto any lower one
|
359
|
+
common = variants[m]&variants[l]
|
360
|
+
clauses_l = clauses_by_refs[variants[l]]
|
361
|
+
clauses_m = clauses_by_refs[variants[m]]
|
362
|
+
l_keys = variants[l]-common
|
363
|
+
m_keys = variants[m]-common
|
364
|
+
trace :binding, "Try to collapse variant #{m} onto #{l}; diffs are #{l_keys.inspect} -> #{m_keys.inspect}"
|
365
|
+
rebindings = 0
|
366
|
+
l_keys.each_with_index do |l_key, i|
|
367
|
+
# Find possible rebinding candidates; there must be exactly one.
|
368
|
+
candidates = []
|
369
|
+
(0...m_keys.size).each do |j|
|
370
|
+
m_key = m_keys[j]
|
371
|
+
l_ref = refs_by_clause_and_key[[clauses_l[0], l_key]]
|
372
|
+
m_ref = refs_by_clause_and_key[[clauses_m[0], m_key]]
|
373
|
+
trace :binding, "Can we match #{l_ref.inspect} (#{i}) with #{m_ref.inspect} (#{j})?"
|
374
|
+
next if m_ref.player != l_ref.player
|
375
|
+
if has_more_adjectives(m_ref, l_ref)
|
376
|
+
trace :binding, "can rebind #{m_ref.inspect} to #{l_ref.inspect}"
|
377
|
+
candidates << [m_ref, l_ref]
|
378
|
+
elsif has_more_adjectives(l_ref, m_ref)
|
379
|
+
trace :binding, "can rebind #{l_ref.inspect} to #{m_ref.inspect}"
|
380
|
+
candidates << [l_ref, m_ref]
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
# trace :binding, "found #{candidates.size} rebinding candidates for this role"
|
385
|
+
trace :binding, "rebinding is ambiguous so not attempted" if candidates.size > 1
|
386
|
+
if (candidates.size == 1)
|
387
|
+
candidates[0][0].rebind_to(@context, candidates[0][1])
|
388
|
+
rebindings += 1
|
389
|
+
end
|
390
|
+
|
391
|
+
end
|
392
|
+
if (rebindings == l_keys.size)
|
393
|
+
# Successfully rebound this fact type
|
394
|
+
trace :binding, "Successfully rebound clauses #{clauses_l.map{|r|r.inspect}*'; '} on to #{clauses_m.map{|r|r.inspect}*'; '}"
|
395
|
+
break
|
396
|
+
else
|
397
|
+
# No point continuing, we failed on this one.
|
398
|
+
raise "All readings in a fact type definition must have matching role players, compare (#{
|
399
|
+
clauses_by_refs.keys.map do |keys|
|
400
|
+
keys.map{|key| key*'-' }*", "
|
401
|
+
end*") with ("
|
402
|
+
})"
|
403
|
+
end
|
404
|
+
|
405
|
+
end
|
406
|
+
end
|
407
|
+
# else all clauses already matched
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
def inspect
|
412
|
+
s = super
|
413
|
+
"FactType: #{@conditions.size > 0 ? super+' ' : '' }#{@clauses.inspect}" +
|
414
|
+
(@pragmas && @pragmas.size > 0 ? ", pragmas [#{@pragmas.flatten.sort*','}]" : '')
|
415
|
+
|
416
|
+
# REVISIT: @returning = returning
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module ActiveFacts
|
2
|
+
module CQL
|
3
|
+
class Compiler < ActiveFacts::CQL::Parser
|
4
|
+
class Definition
|
5
|
+
# Make a Variable for every binding present in these clauses
|
6
|
+
def build_variables(clauses_list)
|
7
|
+
trace :query, "Building variables" do
|
8
|
+
query = @constellation.Query(:new)
|
9
|
+
all_bindings_in_clauses(clauses_list).
|
10
|
+
each do |binding|
|
11
|
+
trace :query, "Creating variable #{query.all_variable.size} for #{binding.inspect}"
|
12
|
+
binding.variable = @constellation.Variable(query, query.all_variable.size, :object_type => binding.player)
|
13
|
+
if literal = binding.refs.detect{|r| r.literal}
|
14
|
+
if literal.kind_of?(ActiveFacts::CQL::Compiler::Reference)
|
15
|
+
# REVISIT: Fix this crappy ad-hoc polymorphism hack
|
16
|
+
literal = literal.literal
|
17
|
+
end
|
18
|
+
unit = @constellation.Unit.detect{|k, v| [v.name, v.plural_name].include? literal.unit} if literal.unit
|
19
|
+
binding.variable.value = [literal.literal.to_s, literal.literal.is_a?(String), unit]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
query
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_all_steps(clauses_list)
|
27
|
+
roles_by_binding = {}
|
28
|
+
trace :query, "Building steps" do
|
29
|
+
clauses_list.each do |clause|
|
30
|
+
build_step(clause, roles_by_binding)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
roles_by_binding
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_step clause, roles_by_binding = {}, parent_variable = nil
|
37
|
+
return unless clause.refs.size > 0 # Empty clause... really?
|
38
|
+
|
39
|
+
step = @constellation.Step(
|
40
|
+
:guid => :new,
|
41
|
+
:fact_type => clause.fact_type,
|
42
|
+
:alternative_set => nil,
|
43
|
+
:is_disallowed => clause.certainty == false,
|
44
|
+
:is_optional => clause.certainty == nil
|
45
|
+
)
|
46
|
+
|
47
|
+
trace :query, "Creating Plays for #{clause.inspect} with #{clause.refs.size} refs" do
|
48
|
+
is_input = true
|
49
|
+
clause.refs.each do |ref|
|
50
|
+
# These refs are the Compiler::References, which have associated Metamodel::RoleRefs,
|
51
|
+
# but we need to create Plays for those roles.
|
52
|
+
# REVISIT: Plays may need to save residual_adjectives
|
53
|
+
binding = ref.binding
|
54
|
+
role = (ref && ref.role) || (ref.role_ref && ref.role_ref.role)
|
55
|
+
|
56
|
+
objectification_step = nil
|
57
|
+
if ref.nested_clauses
|
58
|
+
ref.nested_clauses.each do |nested_clause|
|
59
|
+
objectification_step = build_step nested_clause, roles_by_binding
|
60
|
+
if ref.binding.player.is_a?(ActiveFacts::Metamodel::EntityType) and
|
61
|
+
ref.binding.player.fact_type == nested_clause.fact_type
|
62
|
+
objectification_step.objectification_variable = binding.variable
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
if clause.is_naked_object_type
|
67
|
+
raise "#{self} lacks a proper objectification" if clause.refs[0].nested_clauses and !objectification_step
|
68
|
+
return objectification_step
|
69
|
+
end
|
70
|
+
|
71
|
+
if binding.variable.object_type != role.object_type # Type mismatch
|
72
|
+
if binding.variable.object_type.common_supertype(role.object_type)
|
73
|
+
# REVISIT: there's an implicit subtyping step here, create it; then always raise the error here.
|
74
|
+
# I don't want to do this for now because the verbaliser will always verbalise all steps.
|
75
|
+
raise "Disallowing implicit subtyping step from #{role.object_type.name} to #{binding.variable.object_type.name} in #{clause.fact_type.default_reading.inspect}"
|
76
|
+
end
|
77
|
+
raise "A #{role.object_type.name} cannot satisfy #{binding.variable.object_type.name} in #{clause.fact_type.default_reading.inspect}"
|
78
|
+
end
|
79
|
+
|
80
|
+
trace :query, "Creating Play for #{ref.inspect}"
|
81
|
+
play = @constellation.Play(:step => step, :role => role, :variable => binding.variable)
|
82
|
+
play.is_input = is_input
|
83
|
+
is_input = false
|
84
|
+
|
85
|
+
roles_by_binding[binding] = [role, play]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
step
|
90
|
+
end
|
91
|
+
|
92
|
+
# Return the unique array of all bindings in these clauses, including in objectification steps
|
93
|
+
def all_bindings_in_clauses clauses
|
94
|
+
clauses.map do |clause|
|
95
|
+
clause.refs.map do |ref|
|
96
|
+
raise "Binding reference #{ref.inspect} is not bound to a binding" unless ref.binding
|
97
|
+
[ref.binding] + (ref.nested_clauses ? all_bindings_in_clauses(ref.nested_clauses) : [])
|
98
|
+
end
|
99
|
+
end.
|
100
|
+
flatten.
|
101
|
+
uniq
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|