bmg 0.16.0.pre.rc1 → 0.16.0.pre.rc2

Sign up to get free protection for your applications and to get access to all the features.
@@ -42,6 +42,7 @@ module Bmg
42
42
  #
43
43
  selection = apply(sexpr.select_list)
44
44
  predicate = apply(sexpr.predicate) if sexpr.predicate
45
+ grouping = apply(sexpr.group_by_clause) if sexpr.group_by_clause
45
46
  order = apply(sexpr.order_by_clause) if sexpr.order_by_clause
46
47
  limit = apply(sexpr.limit_clause) if sexpr.limit_clause
47
48
  offset = apply(sexpr.offset_clause) if sexpr.offset_clause
@@ -49,6 +50,7 @@ module Bmg
49
50
  dataset = dataset.select(*selection)
50
51
  dataset = dataset.distinct if sexpr.distinct?
51
52
  dataset = dataset.where(predicate) if predicate
53
+ dataset = dataset.group(grouping) if grouping
52
54
  dataset = dataset.order_by(*order) if order
53
55
  dataset = dataset.limit(limit, offset == 0 ? nil : offset) if limit or offset
54
56
  dataset
@@ -68,13 +70,21 @@ module Bmg
68
70
  case kind = sexpr.left.first
69
71
  when :qualified_name
70
72
  left.column == right.value ? left : ::Sequel.as(left, right)
71
- when :literal
73
+ when :literal, :summarizer
72
74
  ::Sequel.as(left, right)
73
75
  else
74
76
  raise NotImplementedError, "Unexpected select item `#{kind}`"
75
77
  end
76
78
  end
77
79
 
80
+ def on_summarizer(sexpr)
81
+ if sexpr.summary_expr
82
+ ::Sequel.function(sexpr.summary_func, apply(sexpr.summary_expr))
83
+ else
84
+ ::Sequel.function(sexpr.summary_func).*
85
+ end
86
+ end
87
+
78
88
  def on_qualified_name(sexpr)
79
89
  apply(sexpr.last).qualify(sexpr.qualifier)
80
90
  end
@@ -116,6 +126,11 @@ module Bmg
116
126
  ::Sequel.as(apply(sexpr.subquery), ::Sequel.identifier(sexpr.as_name))
117
127
  end
118
128
 
129
+ def on_group_by_clause(sexpr)
130
+ return nil unless sexpr.size > 1
131
+ sexpr.sexpr_body.map{|c| apply(c)}
132
+ end
133
+
119
134
  def on_order_by_clause(sexpr)
120
135
  sexpr.sexpr_body.map{|c| apply(c)}
121
136
  end
@@ -146,6 +146,14 @@ module Bmg
146
146
  Predicate::Grammar.sexpr [ :exists, subquery ]
147
147
  end
148
148
 
149
+ def group_by_clause(attrlist, &desaliaser)
150
+ attrlist.map{|name|
151
+ name = name.to_s
152
+ (desaliaser && desaliaser[name]) || column_name(name)
153
+ }.unshift(:group_by_clause)
154
+ end
155
+ builder :group_by_clause
156
+
149
157
  def order_by_clause(ordering, &desaliaser)
150
158
  ordering.to_a.map{|(name,direction)|
151
159
  name = name.to_s
@@ -30,6 +30,7 @@ require_relative "nodes/table_as"
30
30
  require_relative "nodes/native_table_as"
31
31
  require_relative "nodes/subquery_as"
32
32
  require_relative "nodes/table_name"
33
+ require_relative "nodes/group_by_clause"
33
34
  require_relative "nodes/order_by_clause"
34
35
  require_relative "nodes/order_by_term"
35
36
  require_relative "nodes/limit_clause"
@@ -43,3 +44,4 @@ require_relative "nodes/name_intro"
43
44
  require_relative "nodes/where_clause"
44
45
  require_relative "nodes/cross_join"
45
46
  require_relative "nodes/inner_join"
47
+ require_relative "nodes/summarizer"
@@ -35,6 +35,7 @@ rules:
35
35
  [ select_list, select_star ],
36
36
  from_clause,
37
37
  where_clause,
38
+ group_by_clause,
38
39
  order_by_clause,
39
40
  limit_clause,
40
41
  offset_clause ]
@@ -50,6 +51,8 @@ rules:
50
51
  - [ table_spec ]
51
52
  where_clause:
52
53
  - [ predicate ]
54
+ group_by_clause:
55
+ - [ a_name ]
53
56
  order_by_clause:
54
57
  - [ order_by_term+ ]
55
58
  order_by_term:
@@ -72,11 +75,19 @@ rules:
72
75
  scalar_exp:
73
76
  - qualified_name
74
77
  - column_name
78
+ - summarizer
75
79
  - literal
80
+ a_name:
81
+ - qualified_name
82
+ - column_name
76
83
  qualified_name:
77
84
  - [ range_var_name, column_name ]
78
85
  column_name:
79
86
  - [ name_rgx ]
87
+ summarizer:
88
+ - [ summary_func, qualified_name ]
89
+ summary_func:
90
+ - "::Symbol"
80
91
  table_name:
81
92
  - [ name_rgx ]
82
93
  range_var_name:
@@ -0,0 +1,20 @@
1
+ module Bmg
2
+ module Sql
3
+ module GroupByClause
4
+ include Expr
5
+
6
+ GROUP_BY = "GROUP BY".freeze
7
+
8
+ def to_sql(buffer, dialect)
9
+ return buffer if size == 1
10
+ buffer << GROUP_BY << SPACE
11
+ each_child do |item,index|
12
+ buffer << COMMA << SPACE unless index == 0
13
+ item.to_sql(buffer, dialect)
14
+ end
15
+ buffer
16
+ end
17
+
18
+ end # module GroupByClause
19
+ end # module Sql
20
+ end # module Bmg
@@ -67,6 +67,10 @@ module Bmg
67
67
  from_clause.table_spec
68
68
  end
69
69
 
70
+ def group_by_clause
71
+ find_child(:group_by_clause)
72
+ end
73
+
70
74
  def order_by_clause
71
75
  find_child(:order_by_clause)
72
76
  end
@@ -0,0 +1,23 @@
1
+ module Bmg
2
+ module Sql
3
+ module Summarizer
4
+ include Expr
5
+
6
+ def summary_func
7
+ self[1]
8
+ end
9
+
10
+ def summary_expr
11
+ self.last
12
+ end
13
+
14
+ def to_sql(buffer, dialect)
15
+ buffer << summary_func.upcase << "("
16
+ summary_expr.to_sql(buffer, dialect)
17
+ buffer << ")"
18
+ buffer
19
+ end
20
+
21
+ end # module Summarizer
22
+ end # module Sql
23
+ end # module Bmg
@@ -84,3 +84,4 @@ require_relative 'processor/join'
84
84
  require_relative 'processor/semi_join'
85
85
  require_relative 'processor/flatten'
86
86
  require_relative 'processor/requalify'
87
+ require_relative 'processor/summarize'
@@ -0,0 +1,48 @@
1
+ module Bmg
2
+ module Sql
3
+ class Processor
4
+ class Summarize < Processor
5
+
6
+ def initialize(by, summarization, builder)
7
+ super(builder)
8
+ @by = by
9
+ @summarization = summarization
10
+ end
11
+ attr_reader :by, :summarization
12
+
13
+ def on_set_operator(sexpr)
14
+ call(builder.from_self(sexpr))
15
+ end
16
+ alias :on_union :on_set_operator
17
+ alias :on_except :on_set_operator
18
+ alias :on_intersect :on_set_operator
19
+
20
+ def on_select_exp(sexpr)
21
+ if obc = sexpr.group_by_clause
22
+ sexpr = builder.from_self(sexpr)
23
+ call(sexpr)
24
+ else
25
+ sexpr = sexpr.with_update(:select_list, apply(sexpr.select_list))
26
+ group_by = builder.group_by_clause(by, &sexpr.desaliaser)
27
+ sexpr.push(group_by)
28
+ end
29
+ end
30
+
31
+ def on_select_list(sexpr)
32
+ by_list = sexpr.sexpr_body.select{|select_item|
33
+ by.include?(select_item.last.last.to_sym)
34
+ }
35
+ group_list = summarization.map{|attr,summarizer|
36
+ [:select_item,
37
+ [ :summarizer,
38
+ summarizer.to_summarizer_name,
39
+ sexpr.desaliaser[attr] ],
40
+ [:column_name, attr.to_s] ]
41
+ }
42
+ [:select_list] + by_list + group_list
43
+ end
44
+
45
+ end # class Summarize
46
+ end # class Processor
47
+ end # module Sql
48
+ end # module Bmg
@@ -20,12 +20,17 @@ module Bmg
20
20
  end
21
21
 
22
22
  def on_select_exp(sexpr)
23
- pred = @predicate.rename(sexpr.desaliaser(true))
24
- if sexpr.where_clause
25
- sexpr_p = Predicate.new(sexpr.where_clause.predicate)
26
- sexpr.with_update(:where_clause, [ :where_clause, (sexpr_p & pred).sexpr ])
23
+ if sexpr.group_by_clause
24
+ sexpr = builder.from_self(sexpr)
25
+ call(sexpr)
27
26
  else
28
- sexpr.with_insert(4, [ :where_clause, pred.sexpr ])
27
+ pred = @predicate.rename(sexpr.desaliaser(true))
28
+ if sexpr.where_clause
29
+ sexpr_p = Predicate.new(sexpr.where_clause.predicate)
30
+ sexpr.with_update(:where_clause, [ :where_clause, (sexpr_p & pred).sexpr ])
31
+ else
32
+ sexpr.with_insert(4, [ :where_clause, pred.sexpr ])
33
+ end
29
34
  end
30
35
  end
31
36
 
@@ -114,6 +114,23 @@ module Bmg
114
114
  _instance(type, builder, expr)
115
115
  end
116
116
 
117
+ def _summarize(type, by, summarization)
118
+ summarization = Operator::Summarize.compile(summarization)
119
+ if can_compile_summarization?(summarization)
120
+ expr = before_use(self.expr)
121
+ expr = Processor::Summarize.new(by, summarization, builder).call(self.expr)
122
+ _instance(type, builder, expr)
123
+ else
124
+ super
125
+ end
126
+ end
127
+
128
+ def can_compile_summarization?(summarization)
129
+ summarization.values.all?{|s|
130
+ [:avg, :count, :max, :min, :sum].include?(s.to_summarizer_name)
131
+ }
132
+ end
133
+
117
134
  def _union(type, right, options)
118
135
  if right_expr = extract_compatible_sexpr(right)
119
136
  expr = before_use(self.expr)
@@ -3,51 +3,140 @@ module Bmg
3
3
  module Support
4
4
  class FromClauseOrderer
5
5
 
6
- # Takes a from_clause AST as input and generates an relationally
7
- # equivalent list of (type,table,predicate) triplets, where:
6
+ # Given a `from_clause` AST as input, e.g.
7
+ #
8
+ # [ :from_clause,
9
+ # [ :cross_join
10
+ # [ :inner_join,
11
+ # [ :inner_join,
12
+ # [ :table_as, "suppliers", "s" ],
13
+ # [ :table_as, "supplies", "sp" ],
14
+ # [ :eq, [ :qualified, "s", "sid" ], [ :qualified, "sp", "sid" ] ]
15
+ # ],
16
+ # [ :table_as, "parts", "p" ],
17
+ # [ :eq, [ :qualified, "p", "pid" ], [ :qualified "sp", "pid" ] ]
18
+ # ],
19
+ # [ :table_as, "cities", "c" ],
20
+ # ]
21
+ # ]
22
+ #
23
+ # Generates a relationally equivalent list of (type,table,predicate)
24
+ # triplets, where:
8
25
  #
9
26
  # - type is :base, :cross_join or :inner_join
10
27
  # - table is table_as, native_table_as or subquery_as
11
- # - predicate is a join predicate
28
+ # - predicate is a join predicate `ti.attri = tj.attrj AND ...`
29
+ #
30
+ # So that
31
+ #
32
+ # 1) the types are observed in strict increasing order (one :base, zero
33
+ # or more :cross_join, zero or more :inner_join)
34
+ #
35
+ # 2) the list is such that it can be safely written as an expression
36
+ # of the following SQL syntax:
37
+ #
38
+ # t1 # [ :base, t1, nil ]
39
+ # cross_join t2 # [ :cross_join, t2, nil ]
40
+ # cross_join t3 # [ :cross_join, t3, nil ]
41
+ # inner_join t4 ON p4 # [ :inner_join, t4, p4 ]
42
+ # inner_join t5 ON p5 # [ :inner_join, t5, p5 ]
43
+ #
44
+ # that is, the linearization is correct only if each predicate `pi`
45
+ # only makes reference to tables introduced before it (no forward
46
+ # reference).
12
47
  #
13
- # The types are observed in strict increasing order
14
- # (one :base, zero or more :cross_join, zero or more
15
- # :inner_join). The list is such that it can be safely
16
- # written as an expression of the following SQL form:
48
+ # For the example above, a solution might be:
17
49
  #
18
- # t1 # [ :base, t1, nil ]
19
- # cross_join t2 # [ :cross_join, t2, nil ]
20
- # cross_join t3 # [ :cross_join, t3, nil ]
21
- # inner_join t4 ON p4 # [ :inner_join, t4, p4 ]
22
- # inner_join t5 ON p5 # [ :inner_join, t5, p5 ]
50
+ # [
51
+ # [ :base, [ :table_as, "suppliers", "s" ], nil ],
52
+ # [ :cross_join, [ :table_as, "cities", "c" ], nil ],
53
+ # [ :inner_join, [ :table_as, "supplies", "sp" ],
54
+ # [ :eq, [ :qualified, "s", "sid" ], [ :qualified, "sp", "sid" ] ] ],
55
+ # [ :inner_join, [ :table_as, "parts", "p" ],
56
+ # [ :eq, [ :qualified, "p", "pid" ], [ :qualified "sp", "pid" ] ] ]
57
+ # ]
23
58
  #
24
- # A NotImplementedError may be raised if no linearization can
25
- # be found.
59
+ # A NotImplementedError may be raised if no linearization can be found.
26
60
  #
27
61
  def call(sexpr)
62
+ # The algorithm works in two phases: we first collect all table
63
+ # references and JOIN clauses by simple inspection of the AST.
28
64
  tables, joins = collect(sexpr)
65
+
66
+ # Then we order the tables and join clauses so as to find the
67
+ # linearization.
29
68
  order_all(tables, joins)
30
69
  end
31
70
 
32
- protected
71
+ protected ## Second phase: linearization per se
33
72
 
34
- def order_all(tables, joins, result = [])
73
+ # Given a non empty list of tables `ti` and a possibly empty list of
74
+ # join conditions `ti.attri = tj.attrj`, returns a linearization of
75
+ # join triplets meeting the following POST conditions:
76
+ #
77
+ # 1. A triplet is either `[ :base, ti, nil ]`, `[:cross_join, ti, nil]`
78
+ # or `[ :inner_join, ti, AND([eq]) ]` where `eq` is of the form
79
+ # `ti.attri = tj.attrj`
80
+ #
81
+ # 2. one and only on `:base` triplet comes first, then `:cross_join`
82
+ # ones, then `:inner_join` ones.
83
+ #
84
+ # 3. an inner clause at position x in the resulting list is such that
85
+ # its join conditions `eq` only make reference to tables `ti` that
86
+ # have been introduced before or in x itself (i.e. no forward
87
+ # reference to tables not introduced yet)
88
+ #
89
+ def order_all(tables, joins)
90
+ # Our first strategy is simple: let sort the tables by moving the ones
91
+ # not referenced in join clauses at the beginning of the list => they
92
+ # will yield the base an cross join clauses first.
93
+ tables = tables.sort{|t1,t2|
94
+ t1js = joins.select{|j| uses?(j, t1) }.size
95
+ t2js = joins.select{|j| uses?(j, t2) }.size
96
+ t1js == 0 ? (t2js == 0 ? 0 : -1) : (t2js == 0 ? 1 : 0)
97
+ }
98
+
99
+ # Then order all recursively in that order of tables, filling a result
100
+ # array that will be returned
101
+ _order_all(tables, joins, [])
102
+ end
103
+
104
+ def _order_all(tables, joins, result)
35
105
  if tables.empty? and joins.empty?
106
+ # end or recusion
36
107
  result
37
108
  elsif tables.empty?
109
+ # Why will this never happen exactly??
38
110
  raise NotImplementedError, "Orphan joins: `#{joins.inspect}`"
39
111
  else
112
+ # Greedy strategy: we take the first table in the list and keep the
113
+ # rest for recursion later
40
114
  table, tables_tail = tables.first, tables[1..-1]
115
+
116
+ # Split the remaining joins in two lists: those referencing only
117
+ # introduced tables, and those making forward references
41
118
  on, joins_tail = split_joins(joins, table, tables_tail)
119
+
120
+ # Decide which kind of join it is, according to the result and
121
+ # the number of join clauses that will be used
42
122
  join_kind = result.empty? ? :base : (on.empty? ? :cross_join : :inner_join)
123
+
124
+ # Compute the AND([eq]) predicate on selected join clauses
43
125
  predicate = on.inject(nil){|p,clause|
44
126
  p.nil? ? clause : Predicate::Factory.and(p, clause)
45
127
  }
128
+
129
+ # Recurse with that new clause in the result
46
130
  clause = [ join_kind, table, predicate ]
47
- order_all(tables_tail, joins_tail, result + [clause])
131
+ _order_all(tables_tail, joins_tail, result + [clause])
48
132
  end
49
133
  end
50
134
 
135
+ # Given a list of join `ti.attri = tj.attrj` clauses, a newly introduced
136
+ # ti `table`, and a set of non-yet-introduced tables `tables_tail`,...
137
+ #
138
+ # ... split the joins in two sublists: those making references to table
139
+ # `ti` and making no reference to non introduced tables, and the others.
51
140
  def split_joins(joins, table, tables_tail)
52
141
  joins.partition{|j|
53
142
  uses?(j, table) && !tables_tail.find{|t|
@@ -56,17 +145,34 @@ module Bmg
56
145
  }
57
146
  end
58
147
 
59
- protected
148
+ # Returns whether the join
149
+ def uses?(condition, table)
150
+ name = table.as_name.to_s
151
+ left_name = var_name(condition[1])
152
+ right_name = var_name(condition[2])
153
+ (left_name == name) or (right_name == name)
154
+ end
155
+
156
+ # Given a `ti.attri` expression (AST node), returns `ti`
157
+ def var_name(qualified)
158
+ case qualified.first
159
+ when :qualified_identifier then qualified[1].to_s
160
+ when :qualified_name then qualified[1][1].to_s
161
+ else
162
+ raise NotImplementedError, "Unexpected qualified name `#{qualified.inspect}`"
163
+ end
164
+ end
165
+
166
+ protected ## First phase: collection of tables and join clauses
60
167
 
168
+ # Given a `from_clause` AST (see grammar.sexp.yml), returns two
169
+ # lists:
170
+ # - one with tables `ti` (`table_as`, `native_table_as` & `subquery_as`)
171
+ # - another one with all equality conditions of the form `ti.attri = tj.attrj`
61
172
  def collect(sexpr)
62
173
  tables = []
63
174
  joins = []
64
175
  _collect(sexpr, tables, joins)
65
- tables.sort!{|t1,t2|
66
- t1js = joins.select{|j| uses?(j, t1) }.size
67
- t2js = joins.select{|j| uses?(j, t2) }.size
68
- t1js == 0 ? (t2js == 0 ? 0 : -1) : (t2js == 0 ? 1 : 0)
69
- }
70
176
  [ tables, joins ]
71
177
  end
72
178
 
@@ -98,22 +204,6 @@ module Bmg
98
204
  end
99
205
  end
100
206
 
101
- def uses?(join, table)
102
- name = table.as_name.to_s
103
- left_name = var_name(join[1])
104
- right_name = var_name(join[2])
105
- (left_name == name) or (right_name == name)
106
- end
107
-
108
- def var_name(qualified)
109
- case qualified.first
110
- when :qualified_identifier then qualified[1].to_s
111
- when :qualified_name then qualified[1][1].to_s
112
- else
113
- raise NotImplementedError, "Unexpected qualified name `#{qualified.inspect}`"
114
- end
115
- end
116
-
117
207
  end # class FromClauseOrderer
118
208
  end # module Support
119
209
  end # module Sql