activefacts 1.1.0 → 1.2.0

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/bin/cql +137 -91
  3. data/css/style.css +3 -3
  4. data/examples/CQL/Insurance.cql +1 -1
  5. data/examples/CQL/SeparateSubtype.cql +2 -2
  6. data/lib/activefacts/cql/Language/English.treetop +9 -0
  7. data/lib/activefacts/cql/ObjectTypes.treetop +1 -1
  8. data/lib/activefacts/cql/Terms.treetop +3 -1
  9. data/lib/activefacts/cql/ValueTypes.treetop +10 -4
  10. data/lib/activefacts/cql/compiler.rb +1 -0
  11. data/lib/activefacts/cql/compiler/clause.rb +53 -23
  12. data/lib/activefacts/cql/compiler/entity_type.rb +0 -4
  13. data/lib/activefacts/cql/compiler/expression.rb +9 -13
  14. data/lib/activefacts/cql/compiler/fact.rb +49 -48
  15. data/lib/activefacts/cql/compiler/fact_type.rb +23 -20
  16. data/lib/activefacts/cql/compiler/query.rb +49 -121
  17. data/lib/activefacts/cql/compiler/shared.rb +5 -1
  18. data/lib/activefacts/cql/compiler/value_type.rb +4 -2
  19. data/lib/activefacts/generate/rails/schema.rb +138 -108
  20. data/lib/activefacts/generate/transform/surrogate.rb +1 -2
  21. data/lib/activefacts/mapping/rails.rb +52 -45
  22. data/lib/activefacts/persistence/columns.rb +5 -5
  23. data/lib/activefacts/persistence/tables.rb +6 -4
  24. data/lib/activefacts/support.rb +0 -2
  25. data/lib/activefacts/version.rb +1 -1
  26. data/lib/activefacts/vocabulary/extensions.rb +64 -42
  27. data/lib/activefacts/vocabulary/metamodel.rb +14 -12
  28. data/lib/activefacts/vocabulary/verbaliser.rb +98 -92
  29. data/spec/cql/expressions_spec.rb +8 -3
  30. data/spec/cql/parser/entity_types_spec.rb +1 -1
  31. data/spec/cql/parser/expressions_spec.rb +66 -52
  32. data/spec/cql/parser/fact_types_spec.rb +1 -1
  33. data/spec/cql/parser/literals_spec.rb +10 -10
  34. data/spec/cql/parser/pragmas_spec.rb +3 -3
  35. data/spec/cql/parser/value_types_spec.rb +1 -1
  36. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6e791a369dca69a80ccd666e7eec9441e9f00483
4
- data.tar.gz: d894b07ab82cfdf942a8e93561fa86a8a3bedd8a
3
+ metadata.gz: 8ed82ab2bf0c4e8d8dc9fce824d69e9fff87ab0b
4
+ data.tar.gz: 03644b12bd3d6717a457a7526342b797966fb712
5
5
  SHA512:
6
- metadata.gz: c5f05a7b8b3b116f478b55819baba8162b12c8807122f9825474c93819a9f0f3255dd5c2040eaf54dd59e6e445c5572bcd1d82267d3b9312af140ae8b51d8ce5
7
- data.tar.gz: e6d40c1c950bbca20c96700002c88fa640c1a019c3dfe72b4e88da5a549d401d6d5d87eead9c20d6ced53c688750f7458f2ae598181a6a9c02d84775c6407ca4
6
+ metadata.gz: 07c66c621a9009612baa4e90abfb03dfee833e8daba654089f75f80ee14fe2881aab1e7e9e90c0b55109f80e5472cc7a05bde2f9dc1d1534ae3357e4097bcf8a
7
+ data.tar.gz: d10faaa64f3c03eacc742888310a0ddb88158e9de646c80c3176040e970bf61e5e67f6a81d014b7acc794b8c91bf27f874391ef0c6b7a66d6210176b10bf0171
data/bin/cql CHANGED
@@ -9,6 +9,7 @@ require 'readline'
9
9
 
10
10
  require 'activefacts'
11
11
  require 'activefacts/cql/compiler'
12
+ require 'activefacts/vocabulary/query_evaluator'
12
13
 
13
14
  class InteractiveCQL < ActiveFacts::CQL::Compiler
14
15
  def initialize *a
@@ -16,6 +17,7 @@ class InteractiveCQL < ActiveFacts::CQL::Compiler
16
17
  @show_highlight = false # Show the highlighted CQL text from the parse tree
17
18
  super *a
18
19
  self.root = :definition
20
+ @population_name = '' # Use the default population
19
21
  end
20
22
 
21
23
  def add_logger logger
@@ -70,20 +72,20 @@ class InteractiveCQL < ActiveFacts::CQL::Compiler
70
72
 
71
73
  def metacommand(line)
72
74
  # meta-commands start with /
73
- words = line.split
74
- case cmd = words.shift
75
- when "/trace"
76
- if words.empty?
77
- puts trace_keys*", "
78
- else
79
- toggle_trace words
80
- end
81
- when "/tree"
82
- @show_tree = !@show_tree
83
- puts "Will #{@show_tree ? "" : "not "}show the parse tree"
75
+ cmd = line.sub(/(\A\/\p{Alpha}*).*/,'\1')
76
+ words = line[cmd.size..-1].split
77
+ case cmd
78
+ when "/about"
79
+ list_connotations (words*' ')
80
+ when "/cql"
81
+ process(words*' ')
84
82
  when "/highlight"
85
83
  @show_highlight = !@show_highlight
86
84
  puts "Will #{@show_highlight ? "" : "not "}show the highlighted parse"
85
+ when "/list"
86
+ list_object_types
87
+ when "/load"
88
+ load_file(words*' ')
87
89
  when "/log"
88
90
  @logger ||= proc {|*a|
89
91
  unless @undoing
@@ -91,26 +93,42 @@ class InteractiveCQL < ActiveFacts::CQL::Compiler
91
93
  end
92
94
  }
93
95
  add_logger @logger
94
- when "/timings"
95
- $show_timings = !$show_timings
96
- puts "Will #{$show_timings ? "" : "not "}show command timings"
96
+ when "/population"
97
+ # REVISIT: Set default population for queries
98
+ if words == ['?']
99
+ puts "Defined populations are: "+@constellation.Population.keys.map{|v, p| p.empty? ? "<default>" : p}*', '
100
+ else
101
+ @population_name = words[0] || ''
102
+ @vocabulary = @constellation.Vocabulary.values.first
103
+ @population =
104
+ if @vocabulary
105
+ @constellation.Population[[[@vocabulary.name], @population_name]]
106
+ else
107
+ nil
108
+ end
109
+ puts "Using #{@population ? 'existing ' : ''}population #{@population_name}"
110
+ end
97
111
  when "/root"
98
112
  self.root = words[0] && words[0].to_sym || :definition
99
113
  puts "Looking for a #{self.root}"
100
- when "/list"
101
- list_object_types
102
- when "/about"
103
- words.each do |word|
104
- list_connotations word
114
+ when "/timings"
115
+ $show_timings = !$show_timings
116
+ puts "Will #{$show_timings ? "" : "not "}show command timings"
117
+ when "/trace"
118
+ if words.empty?
119
+ puts trace_keys*", "
120
+ else
121
+ toggle_trace words
105
122
  end
106
- when "/load"
107
- load_file(words*' ')
123
+ when "/tree"
124
+ @show_tree = !@show_tree
125
+ puts "Will #{@show_tree ? "" : "not "}show the parse tree"
108
126
  when "/quit"
109
127
  exit
110
128
  when "/help", "/"
111
129
  help words
112
130
  else
113
- puts "Unknown metacommand #{line}?"
131
+ puts "Unknown metacommand #{line}"
114
132
  help
115
133
  end
116
134
  end
@@ -130,53 +148,67 @@ class InteractiveCQL < ActiveFacts::CQL::Compiler
130
148
  end
131
149
  end
132
150
 
133
- def process_query(query)
134
- result_projection = {} # A set of {hash of Variable to Instance}
135
- query_block =
136
- proc do |unbound_variables = [], bindings = {}|
137
- # unbound_variables is an array of the remaining dimensions of the search
138
- # bindings is a hash of Varable to Instance
139
- next_variable = unbound_variables[0]
140
- if next_variable
141
- ins = next_variable.object_type.all_instance
142
- trace :result, "Query search through #{ins.size} #{next_variable.role_name || next_variable.object_type.name}" do
143
- next_variable.object_type.all_instance.each do |instance|
144
- trace :result, "Considering #{instance.verbalise}"
145
- # REVISIT: Check whether this instance satisfies the query steps that contain the bound variables
146
- (b = bindings.dup).store(next_variable, instance)
147
- result_projection[b] = true
148
- query_block.call(unbound_variables[1..-1], b)
149
- end
150
- end
151
- end
151
+ def query(query)
152
+ @population = @constellation.Population[[[@vocabulary.name], @population_name]]
153
+ unless @population
154
+ puts "Population #{@population_name.inspect} has not yet been instantiated"
155
+ return
156
+ end
157
+
158
+ q = ActiveFacts::Metamodel::QueryEvaluator.new(query, @population)
159
+ results = q.evaluate
160
+ verbalise_results(q, results)
161
+ end
162
+
163
+ def verbalise_results(q, results)
164
+ trace :result, "Found #{results.size} results:"
165
+ if q.free_variables.size == 0
166
+ if results.size > 0
167
+ puts "Yes."
168
+ else
169
+ puts "No."
152
170
  end
171
+ return
172
+ end
153
173
 
154
- unbound_variables = query.all_variable.to_a
155
- bound_variables = {}
156
- unbound_variables.reject!{|v| v.value != nil ? bound_variables[v] = v.value : nil }
157
- print 'bound variables: '; p bound_variables.keys.map{|v| [v.object_type.name, v.value.inspect] }
158
- print 'unbound variables: '; p unbound_variables.map{|v| v.object_type.name}
174
+ return if verbalise_single_fact_result(q, results)
159
175
 
160
- query_block.call(unbound_variables, {})
161
- result_projection.map do |result, _|
162
- puts(result.map{|v, i| i.verbalise }.join(', '))
176
+ # No smart verbalisation for this scenario, just show the variable values:
177
+ results.map do |result, _|
178
+ puts(result.map{|var, i| i.verbalise }.join(', '))
163
179
  end
180
+ end
164
181
 
165
- =begin
166
- if query.all_variable.size == 1 and
167
- (v = query.all_variable.single).all_play.map{|p| p.all_step.to_a}.flatten.uniq.size == 0
168
- if v.object_type.all_instance.size == 0
169
- puts "There are no instances of #{v.object_type.name}"
170
- else
171
- list_instances(v.object_type)
182
+ def verbalise_single_fact_result(q, results)
183
+ # If all unbound variables play in the same step, just expand the fact type's reading with the values
184
+ # REVISIT: Ignore steps over facts that are existential for other variable's object types
185
+ trace :result, "Attempting single fact verbalisation" do
186
+ common_steps =
187
+ q.free_variables.inject(nil) do |memo, var|
188
+ steps = (var.all_play.map{|p| p.step} + Array(var.step)).uniq
189
+ trace :result, "variable #{var.object_type.name} spans steps #{steps.map(&:fact_type).map(&:default_reading).inspect}"
190
+ steps.reject! do |step|
191
+ step.fact_type.is_existential
192
+ end
193
+ memo ? memo & steps : steps
194
+ end
195
+ trace :result, "There are #{common_steps.size} common steps for attempted single fact verbalisation"
196
+ if step = common_steps[0]
197
+ reading = step.fact_type.preferred_reading
198
+ reading_roles = reading.role_sequence.all_role_ref_in_order.map(&:role)
199
+ reading_variables = reading_roles.map { |role| step.all_play.detect{|p| p.role == role}.variable }
200
+
201
+ results.map do |result, _|
202
+ # Verbalise result the fact, don't just dump the variable values
203
+ literals = reading_variables.map{|v| [nil, result[v].verbalise] }
204
+ puts (step.is_disallowed ? 'it is not the case that ' : '') +
205
+ reading.expand(literals) +
206
+ ';'
207
+ end
208
+ return true
172
209
  end
173
- else
174
- # REVISIT: Identify all the unbound Variables, and do a nested-loops iteration checking all Steps
175
- puts "Can't yet process complex queries (involving #{
176
- query.all_variable.map{|v| v.object_type.name}
177
- }*', '})"
210
+ return false
178
211
  end
179
- =end
180
212
  end
181
213
 
182
214
  def process(statement)
@@ -184,7 +216,7 @@ class InteractiveCQL < ActiveFacts::CQL::Compiler
184
216
  @results = []
185
217
  compile(statement)
186
218
  if @results.size == 1 && @results[0].is_a?(ActiveFacts::Metamodel::Query)
187
- process_query(@results[0])
219
+ query(@results[0])
188
220
  else
189
221
  puts(@results.map{|r| "\t"+r.inspect}*"\n")
190
222
  end
@@ -278,16 +310,21 @@ class InteractiveCQL < ActiveFacts::CQL::Compiler
278
310
  puts %Q{
279
311
  Meta-commands are:
280
312
  /about term\t\tDisplay the fact types in which term plays a part
281
- /help\t\t\tThis help message
313
+ /cql text\t\tProcess this single CQL definition or query
282
314
  /help topic\t\thelp on a specific topic
283
315
  /help topics\t\tList the available help topics
284
- /highlight\t\t\tRe-display the parsed CQL with HTML highlighting markup
316
+ /help\t\t\tThis help message
317
+ /highlight\t\tRe-display the parsed CQL with HTML highlighting markup
285
318
  /list\t\t\tList all object type names (terms)
286
319
  /load file.cql\tLoad a CQL file
320
+ /population [? | name] List or set the active population
321
+ /quit\t\t\tExit CQL
287
322
  /root rule\t\tParse just a fragment of a CQL statement, matching syntax rule only
288
- /timings\t\t\tDisplay the elapsed time to execute each command
289
- /tree\t\t\tDisplay the abstract syntax tree from each statement parsed
323
+ /timings\t\tDisplay the elapsed time to execute each command
290
324
  /trace key\t\tToggle tracing key, or list available keys
325
+ /tree\t\t\tDisplay the abstract syntax tree from each statement parsed
326
+
327
+ Meta-commands are available on the shell command-line - use -- instead of /
291
328
  }
292
329
  else
293
330
  words.each do |word|
@@ -503,34 +540,43 @@ end
503
540
  compiler = InteractiveCQL.new
504
541
  statement = nil
505
542
  loaded_files = false
506
- ARGV.each do |arg|
543
+ args = ARGV.dup
544
+ while !args.empty?
545
+ arg = args.shift # Gather up the arguments until the next one starting with -
507
546
  if arg =~ /^--(.*)/
508
- compiler.metacommand('/'+$1)
509
- else
510
- loaded_files = false or break if arg == '-'
547
+ arg = '/'+$1
548
+ while args[0] and args[0] !~ /^-/
549
+ arg = arg+' '+args.shift
550
+ end
551
+ compiler.metacommand arg
552
+ elsif arg == '-' # Either "stdin" or immediate-command
553
+ unless args.empty?
554
+ compiler.process args*' '
555
+ exit
556
+ end
557
+ break
558
+ else # Just a file
511
559
  compiler.load_file(arg)
512
- loaded_files = true
560
+ exit if args.empty?
513
561
  end
514
562
  end
515
563
 
516
- if !loaded_files
517
- puts "Enter / for help on special commands"
518
-
519
- while line = Readline::readline(statement ? "CQL+ " : "CQL? ", [])
520
- statement = statement ? statement + "\n"+line : line
521
- start = Time.now
522
- case
523
- when line =~ %r{\A/}
524
- compiler.metacommand(line)
525
- statement = nil
526
- when compiler.root != :definition || line.gsub(/(['"])([^\1\\]|\\.)*\1/,'') =~ /[;?]/
527
- # After stripping string literals the line contains a ';' or /?', we've found the last line of the command:
528
- compiler.process(statement)
529
- statement = nil
530
- end
531
- if $show_timings && statement == nil
532
- puts "Done in #{((Time.now.to_f-start.to_f)*1000000).to_i} usec"
533
- end
564
+ puts "Enter / for help on special commands"
565
+
566
+ while line = Readline::readline(statement ? "CQL+ " : "CQL? ", [])
567
+ statement = statement ? statement + "\n"+line : line
568
+ start = Time.now
569
+ case
570
+ when line =~ %r{\A/}
571
+ compiler.metacommand(line)
572
+ statement = nil
573
+ when compiler.root != :definition || line.gsub(/(['"])([^\1\\]|\\.)*\1/,'') =~ /[;?]/
574
+ # After stripping string literals the line contains a ';' or /?', we've found the last line of the command:
575
+ compiler.process(statement)
576
+ statement = nil
577
+ end
578
+ if $show_timings && statement == nil
579
+ puts "Done in #{((Time.now.to_f-start.to_f)*1000000).to_i} usec"
534
580
  end
535
- puts
536
581
  end
582
+ puts
@@ -328,14 +328,14 @@ tt,
328
328
  code,
329
329
  pre
330
330
  {
331
- font-family : Consolas, "Lucida Console", monospace;
331
+ font-family : monospace;
332
332
  }
333
333
 
334
334
  tt
335
335
  {
336
336
  font-weight : bold;
337
- color : #A52A2A;
338
- background-color : #FFFAF0;
337
+ /* color : #A52A2A; */
338
+ /* background-color : #FFFAF0; */
339
339
  }
340
340
 
341
341
  /* output of syntax colorizer */
@@ -162,7 +162,7 @@ Vehicle Type is identified by Make and Model and Badge where
162
162
  Vehicle is of one Vehicle Type;
163
163
 
164
164
  Witness is identified by Incident and Name where
165
- Incident was seen by Witness,
165
+ Incident was independently witnessed by Witness,
166
166
  Witness saw one Incident,
167
167
  Witness is called one Name;
168
168
  Witness has at most one contact-Phone;
@@ -23,8 +23,8 @@ Vehicle Incident is a kind of Incident [separate];
23
23
 
24
24
  Witness is a kind of Person;
25
25
  Witness saw Incident,
26
- Incident was seen by at most one Witness;
26
+ Incident was independently witnessed by at most one Witness;
27
27
 
28
28
  Driver is a kind of Person;
29
- Vehicle Incident occured while at most one Driver was in charge;
29
+ Vehicle Incident occurred while at most one Driver was in charge;
30
30
 
@@ -14,6 +14,15 @@ module ActiveFacts
14
14
  s 'is' s 'written' S as s
15
15
  end
16
16
 
17
+ rule auto_assignment
18
+ 'auto-assigned' S at s time:('assert' / 'commit') !alphanumeric s
19
+ {
20
+ def auto_assigned_at
21
+ time.text_value
22
+ end
23
+ }
24
+ end
25
+
17
26
  # The pattern to introduce an Entity Type
18
27
  rule identified_by
19
28
  identified s by s
@@ -160,7 +160,7 @@ module ActiveFacts
160
160
  { # A forward-referenced entity type
161
161
  # REVISIT: A change in this rule might allow forward-referencing a multi-word term
162
162
  def ast
163
- Compiler::Reference.new(id.text_value, nil, nil, nil, ss.empty? ? nil : ss.value)
163
+ Compiler::Reference.new(id.text_value, nil, nil, nil, nil, ss.empty? ? nil : ss.value)
164
164
  end
165
165
  }
166
166
  end
@@ -18,7 +18,9 @@ module ActiveFacts
18
18
  / mapping_pragmas is_where # Objectified type
19
19
  / non_phrase
20
20
  / identified_by # as in: "a kind of X identified by..."
21
- / unit
21
+ / in_units
22
+ / auto_assignment
23
+ / value_constraint
22
24
  end
23
25
 
24
26
  rule entity_prefix
@@ -16,7 +16,8 @@ module ActiveFacts
16
16
  any? s
17
17
  base:(term/implicit_value_type_name) s
18
18
  value_type_parameters
19
- u:units?
19
+ u:in_units?
20
+ a:auto_assignment?
20
21
  c:context_note?
21
22
  r:(value_constraint enforcement)?
22
23
  m2:mapping_pragmas
@@ -30,14 +31,19 @@ module ActiveFacts
30
31
  unless r.empty?
31
32
  value_constraint = Compiler::ValueConstraint.new(r.value_constraint.ast, r.enforcement.ast)
32
33
  end
33
- units = u.empty? ? [] : u.value
34
+ units = u.empty? ? [] : u.units.value
35
+ auto_assigned_at = a.empty? ? nil : a.auto_assigned_at
34
36
  pragmas = m1.value+m2.value
35
37
  context_note = !c.empty? ? c.ast : (!c2.empty? ? c2.ast : nil)
36
- Compiler::ValueType.new name, base.value, params, units, value_constraint, pragmas, context_note
38
+ Compiler::ValueType.new name, base.value, params, units, value_constraint, pragmas, context_note, auto_assigned_at
37
39
  end
38
40
  }
39
41
  end
40
42
 
43
+ rule in_units
44
+ in S units
45
+ end
46
+
41
47
  rule implicit_value_type_name
42
48
  id
43
49
  {
@@ -130,7 +136,7 @@ module ActiveFacts
130
136
  end
131
137
 
132
138
  rule non_unit
133
- restricted_to / conversion / approximately / ephemera
139
+ restricted_to / conversion / approximately / ephemera / auto_assignment
134
140
  end
135
141
 
136
142
  rule unit
@@ -65,6 +65,7 @@ module ActiveFacts
65
65
  # parse_all returns an array of the block's non-nil return values.
66
66
  ok = parse_all(@string, :definition) do |node|
67
67
  trace :parse, "Parsed '#{node.text_value.gsub(/\s+/,' ').strip}'" do
68
+ trace :lex, (proc { node.inspect })
68
69
  begin
69
70
  ast = node.ast
70
71
  next unless ast