bmg 0.16.6 → 0.17.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a88edadd3b79efbe81e2717b2fd38af1e245d7cbc8e90973d48fb44f55a4836
4
- data.tar.gz: 43f43cdd43c69427147fa435158fb31d0a485858c0bc04b2300a8cdda4fef703
3
+ metadata.gz: 90ed0f35cc6ced83ada1d6aeb3162b7fb8c1bb3f6f20f870f030c589e991e97e
4
+ data.tar.gz: 959415999253759d072fae5e5280417cdbd5fd8774bbf90e541b74f2324d197c
5
5
  SHA512:
6
- metadata.gz: 509259030f8e36e0c8729278ab7c7a58341f3ee330401f5a9bcaad08756b1f35e5340ced468498713fb65d3807f37521e7e699ae76b795165fa309ffe921ae96
7
- data.tar.gz: 836cf9ea9f47cef2689c2618977fd3fe955301b2095d808e24447d0f6f023a10cdd01dedb8e656546cf1015c4b5e14a2e499537679307f308bc7e2d409d2cc3c
6
+ metadata.gz: fbb3cb7ea042873933e6c15f5ee73084980066cc3ee8c8c58c5169b3221a1aa4c2d97a1521618c456da0d34097df456ef34081a8a418429d1627ed2ada36c1ec
7
+ data.tar.gz: 687eed5fc05caa9c7dba8258aeb70b929877d6c56c45212ac04358df562acf4ffdc57f199c8d774d43631f82088fa70cac78b1754ae2bfbdd8eaa4d61b156014
data/Gemfile CHANGED
@@ -1,2 +1,5 @@
1
1
  source "https://rubygems.org"
2
2
  gemspec
3
+
4
+ # gem "predicate", github: "enspirit/predicate", branch: "placeholders"
5
+ # gem "predicate", path: "../predicate"
@@ -80,6 +80,19 @@ module Bmg
80
80
  end
81
81
  protected :_joined_with
82
82
 
83
+ def left_join(right, on = [], default_right_tuple = {})
84
+ drt = default_right_tuple
85
+ _left_join self.type.left_join(right.type, on, drt), right, on, drt
86
+ end
87
+
88
+ def _left_join(type, right, on, default_right_tuple)
89
+ Operator::Join.new(type, self, right, on, {
90
+ variant: :left,
91
+ default_right_tuple: default_right_tuple
92
+ })
93
+ end
94
+ protected :_left_join
95
+
83
96
  def matching(right, on = [])
84
97
  _matching self.type.matching(right.type, on), right, on
85
98
  end
@@ -12,7 +12,21 @@ module Bmg
12
12
 
13
13
  # Whether we need to convert each image as an Array,
14
14
  # instead of keeping a Relation instance
15
- array: false
15
+ array: false,
16
+
17
+ # The strategy to use for actual image algorithm. Default is
18
+ # :refilter_right. Possible values are:
19
+ #
20
+ # - :index_right : builds a memory index with tuples from right, then
21
+ # passes left tuples and joins them with the index values.
22
+ #
23
+ # - :refilter_right : the left operand is materialized and all
24
+ # distinct values collected. The right operand is lately restricted
25
+ # to only those matching values. :index_right is then applied on
26
+ # resulting operabds. This option only applies when (optimized) `on`
27
+ # contains one attribute only. ; it fallbacks on :index_right
28
+ # otherwise.
29
+ strategy: :refilter_right
16
30
 
17
31
  }
18
32
 
@@ -31,7 +45,55 @@ module Bmg
31
45
 
32
46
  public
33
47
 
34
- def each
48
+ def each(*args, &bl)
49
+ (options[:jit_optimized] ? self : jit_optimize)._each(*args, &bl)
50
+ end
51
+
52
+ def to_ast
53
+ [ :image, left.to_ast, right.to_ast, as, on, options.dup ]
54
+ end
55
+
56
+ protected
57
+
58
+ def _each(*args, &bl)
59
+ case s = options[:strategy]
60
+ when :index_right then _each_index_right(*args, &bl)
61
+ when :refilter_right then _each_refilter_right(*args, &bl)
62
+ else
63
+ raise ArgumentError, "Unknown strategy `#{s}`"
64
+ end
65
+ end
66
+
67
+ def _each_index_right(*args, &bl)
68
+ left_rel, right_rel = self.left, self.right
69
+ _each_implem(left_rel, right_rel, *args, &bl)
70
+ end
71
+
72
+ def _each_refilter_right(*args, &bl)
73
+ left_rel, right_rel = self.left, self.right
74
+
75
+ # find matching keys on left and rebind the right
76
+ # placeholder to them
77
+ values = left_rel.map{|t| t[on.first] }
78
+ placeholder = options[:refilter_right][:placeholder]
79
+ right_rel = right_rel.bind(placeholder => values)
80
+
81
+ _each_implem(left_rel, right_rel, *args, &bl)
82
+ end
83
+
84
+ def _each_implem(left_rel, right_rel, *args)
85
+ # build right index
86
+ index = build_right_index(right_rel)
87
+
88
+ # each left with image from right index
89
+ left_rel.each do |tuple|
90
+ key = tuple_project(tuple, on)
91
+ image = index[key] || (options[:array] ? [] : empty_image)
92
+ yield tuple.merge(as => image)
93
+ end
94
+ end
95
+
96
+ def build_right_index(right)
35
97
  index = Hash.new{|h,k| h[k] = empty_image }
36
98
  right.each_with_object(index) do |t, index|
37
99
  key = tuple_project(t, on)
@@ -42,15 +104,54 @@ module Bmg
42
104
  ix[k] = v.to_a
43
105
  end
44
106
  end
45
- left.each do |tuple|
46
- key = tuple_project(tuple, on)
47
- image = index[key] || (options[:array] ? [] : empty_image)
48
- yield tuple.merge(as => image)
107
+ index
108
+ end
109
+
110
+ protected ### jit_optimization
111
+
112
+ def jit_optimize
113
+ case s = options[:strategy]
114
+ when :index_right then jit_index_right
115
+ when :refilter_right then jit_refilter_right
116
+ else
117
+ raise ArgumentError, "Unknown strategy `#{s}`"
49
118
  end
50
119
  end
51
120
 
52
- def to_ast
53
- [ :image, left.to_ast, right.to_ast, as, on, options.dup ]
121
+ def jit_index_right
122
+ Image.new(
123
+ type,
124
+ left,
125
+ right,
126
+ as,
127
+ on,
128
+ options.merge(jit_optimized: true))
129
+ end
130
+
131
+ def jit_refilter_right
132
+ ltc = left.type.predicate.constants
133
+ rtc = right.type.predicate.constants
134
+ jit_allbut, jit_on = on.partition{|attr|
135
+ ltc.has_key?(attr) && rtc.has_key?(attr) && ltc[attr] == rtc[attr]
136
+ }
137
+ if jit_on.size == 1
138
+ p = Predicate.placeholder
139
+ Image.new(
140
+ type,
141
+ left.materialize,
142
+ right.restrict(Predicate.in(jit_on.first, p)).allbut(jit_allbut),
143
+ as,
144
+ jit_on,
145
+ options.merge(jit_optimized: true, refilter_right: { placeholder: p }))
146
+ else
147
+ Image.new(
148
+ type,
149
+ left,
150
+ right.allbut(jit_allbut),
151
+ as,
152
+ jit_on,
153
+ options.merge(jit_optimized: true, strategy: :index_right))
154
+ end
54
155
  end
55
156
 
56
157
  protected ### optimization
@@ -68,7 +169,7 @@ module Bmg
68
169
  def _restrict(type, predicate)
69
170
  on_as, rest = predicate.and_split([as])
70
171
  if rest.tautology?
71
- # push none situation: on_as is still the full predicate
172
+ # push index_right situation: on_as is still the full predicate
72
173
  super
73
174
  else
74
175
  # rest makes no reference to `as` and can be pushed
@@ -126,6 +227,16 @@ module Bmg
126
227
  Relation::InMemory.new(image_type, Set.new)
127
228
  end
128
229
 
230
+ public
231
+
232
+ def to_s
233
+ options[:jit_optimized] ? super : jit_optimize.to_s
234
+ end
235
+
236
+ def inspect
237
+ options[:jit_optimized] ? super : jit_optimize.inspect
238
+ end
239
+
129
240
  end # class Project
130
241
  end # module Operator
131
242
  end # module Bmg
@@ -8,16 +8,19 @@ module Bmg
8
8
  class Join
9
9
  include Operator::Binary
10
10
 
11
- def initialize(type, left, right, on)
11
+ DEFAULT_OPTIONS = {}
12
+
13
+ def initialize(type, left, right, on, options = {})
12
14
  @type = type
13
15
  @left = left
14
16
  @right = right
15
17
  @on = on
18
+ @options = DEFAULT_OPTIONS.merge(options)
16
19
  end
17
20
 
18
21
  private
19
22
 
20
- attr_reader :on
23
+ attr_reader :on, :options
21
24
 
22
25
  public
23
26
 
@@ -34,12 +37,24 @@ module Bmg
34
37
  to_join.each do |right|
35
38
  yield right.merge(tuple)
36
39
  end
40
+ elsif left_join?
41
+ yield(tuple.merge(default_right_tuple))
37
42
  end
38
43
  end
39
44
  end
40
45
 
41
46
  def to_ast
42
- [ :join, left.to_ast, right.to_ast, on ]
47
+ [ :join, left.to_ast, right.to_ast, on, extra_opts ].compact
48
+ end
49
+
50
+ protected
51
+
52
+ def left_join?
53
+ options[:variant] == :left
54
+ end
55
+
56
+ def default_right_tuple
57
+ options[:default_right_tuple]
43
58
  end
44
59
 
45
60
  protected ### optimization
@@ -63,8 +78,13 @@ module Bmg
63
78
 
64
79
  protected ### inspect
65
80
 
81
+ def extra_opts
82
+ extra = options.dup.delete_if{|k,v| DEFAULT_OPTIONS[k] == v }
83
+ extra.empty? ? nil : extra
84
+ end
85
+
66
86
  def args
67
- [ on ]
87
+ [ on, extra_opts ].compact
68
88
  end
69
89
 
70
90
  private
@@ -21,6 +21,10 @@ module Bmg
21
21
 
22
22
  public
23
23
 
24
+ def bind(binding)
25
+ Restrict.new(type, operand.bind(binding), predicate.bind(binding))
26
+ end
27
+
24
28
  def each
25
29
  @operand.each do |tuple|
26
30
  yield(tuple) if @predicate.evaluate(tuple)
@@ -3,9 +3,13 @@ module Bmg
3
3
  module Binary
4
4
  include Operator
5
5
 
6
+ def bind(binding)
7
+ _with_operands(left.bind(binding), right.bind(binding))
8
+ end
9
+
6
10
  protected
7
11
 
8
- attr_reader :left, :right
12
+ attr_accessor :left, :right
9
13
 
10
14
  def _visit(parent, visitor)
11
15
  visitor.call(self, parent)
@@ -13,6 +17,13 @@ module Bmg
13
17
  right.send(:_visit, self, visitor)
14
18
  end
15
19
 
20
+ def _with_operands(left, right)
21
+ dup.tap{|d|
22
+ d.left = left
23
+ d.right = right
24
+ }
25
+ end
26
+
16
27
  def operands
17
28
  [left, right]
18
29
  end
@@ -3,9 +3,17 @@ module Bmg
3
3
  module Nary
4
4
  include Operator
5
5
 
6
+ def bind(binding)
7
+ _with_operands(operands.map{|op| op.bind(binding) })
8
+ end
9
+
6
10
  protected
7
11
 
8
- attr_reader :operands
12
+ attr_accessor :operands
13
+
14
+ def _with_operands(operands)
15
+ dup.tap{|d| d.operands = operands }
16
+ end
9
17
 
10
18
  def _visit(parent, visitor)
11
19
  visitor.call(self, parent)
@@ -3,15 +3,23 @@ module Bmg
3
3
  module Unary
4
4
  include Operator
5
5
 
6
+ def bind(binding)
7
+ _with_operand(operand.bind(binding))
8
+ end
9
+
6
10
  protected
7
11
 
8
- attr_reader :operand
12
+ attr_accessor :operand
9
13
 
10
14
  def _visit(parent, visitor)
11
15
  visitor.call(self, parent)
12
16
  operand._visit(self, visitor)
13
17
  end
14
18
 
19
+ def _with_operand(operand)
20
+ dup.tap{|d| d.operand = operand }
21
+ end
22
+
15
23
  def operands
16
24
  [operand]
17
25
  end
@@ -13,6 +13,10 @@ module Bmg
13
13
  Relation::Empty.new(type)
14
14
  end
15
15
 
16
+ def bind(binding)
17
+ self
18
+ end
19
+
16
20
  def with_typecheck
17
21
  dup.tap{|r|
18
22
  r.type = r.type.with_typecheck
@@ -50,7 +50,7 @@ module Bmg
50
50
  dataset = dataset.select(*selection)
51
51
  dataset = dataset.distinct if sexpr.distinct?
52
52
  dataset = dataset.where(predicate) if predicate
53
- dataset = dataset.group(grouping) if grouping
53
+ dataset = dataset.group(*grouping) if grouping
54
54
  dataset = dataset.order_by(*order) if order
55
55
  dataset = dataset.limit(limit, offset == 0 ? nil : offset) if limit or offset
56
56
  dataset
@@ -70,13 +70,18 @@ module Bmg
70
70
  case kind = sexpr.left.first
71
71
  when :qualified_name
72
72
  left.column == right.value ? left : ::Sequel.as(left, right)
73
- when :literal, :summarizer
73
+ when :literal, :summarizer, :func_call
74
74
  ::Sequel.as(left, right)
75
75
  else
76
76
  raise NotImplementedError, "Unexpected select item `#{kind}`"
77
77
  end
78
78
  end
79
79
 
80
+ def on_func_call(sexpr)
81
+ args = sexpr.func_args.map{|fa| apply(fa) }
82
+ ::Sequel.function(sexpr.func_name, *args)
83
+ end
84
+
80
85
  def on_summarizer(sexpr)
81
86
  if sexpr.summary_expr
82
87
  ::Sequel.function(sexpr.summary_func, apply(sexpr.summary_expr))
@@ -106,6 +111,13 @@ module Bmg
106
111
  ds.join_table(:inner, apply(table), nil, options){|*args|
107
112
  apply(on)
108
113
  }
114
+ elsif kind == :left_join
115
+ options = { qualify: false, table_alias: false }
116
+ ds.join_table(:left, apply(table), nil, options){|*args|
117
+ apply(on)
118
+ }
119
+ else
120
+ raise IllegalArgumentError, "Unrecognized from clause: `#{sexpr}`"
109
121
  end
110
122
  end
111
123
  end
@@ -42,6 +42,9 @@ require_relative "nodes/with_exp"
42
42
  require_relative "nodes/with_spec"
43
43
  require_relative "nodes/name_intro"
44
44
  require_relative "nodes/where_clause"
45
+ require_relative "nodes/join"
45
46
  require_relative "nodes/cross_join"
46
47
  require_relative "nodes/inner_join"
48
+ require_relative "nodes/left_join"
47
49
  require_relative "nodes/summarizer"
50
+ require_relative "nodes/func_call"
@@ -24,10 +24,13 @@ rules:
24
24
  join_exp:
25
25
  - cross_join
26
26
  - inner_join
27
+ - left_join
27
28
  cross_join:
28
29
  - [ table_spec, table_spec ]
29
30
  inner_join:
30
31
  - [ table_spec, table_spec, predicate ]
32
+ left_join:
33
+ - [ table_spec, table_spec, predicate ]
31
34
  using:
32
35
  - [ column_name+ ]
33
36
  select_exp:
@@ -76,6 +79,7 @@ rules:
76
79
  - qualified_name
77
80
  - column_name
78
81
  - summarizer
82
+ - func_call
79
83
  - literal
80
84
  a_name:
81
85
  - qualified_name
@@ -88,6 +92,10 @@ rules:
88
92
  - [ summary_func, qualified_name ]
89
93
  summary_func:
90
94
  - "::Symbol"
95
+ func_call:
96
+ - [ func_name, scalar_exp+ ]
97
+ func_name:
98
+ - "::Symbol"
91
99
  table_name:
92
100
  - [ name_rgx ]
93
101
  range_var_name:
@@ -96,6 +104,8 @@ rules:
96
104
  - [ integer ]
97
105
  offset_clause:
98
106
  - [ integer ]
107
+ default_right_tuple:
108
+ - "::Hash"
99
109
  integer:
100
110
  - "::Integer"
101
111
  literal:
@@ -2,18 +2,7 @@ module Bmg
2
2
  module Sql
3
3
  module CrossJoin
4
4
  include Expr
5
-
6
- def join?
7
- true
8
- end
9
-
10
- def left
11
- self[1]
12
- end
13
-
14
- def right
15
- self[2]
16
- end
5
+ include Join
17
6
 
18
7
  def to_sql(buffer, dialect)
19
8
  each_child do |child, index|
@@ -0,0 +1,23 @@
1
+ module Bmg
2
+ module Sql
3
+ module FuncCall
4
+ include Expr
5
+
6
+ def func_name
7
+ self[1]
8
+ end
9
+
10
+ def func_args
11
+ self[2..-1]
12
+ end
13
+
14
+ def to_sql(buffer, dialect)
15
+ buffer << summary_name.upcase << "("
16
+ buffer << func_args.map{|fa| fa.to_sql("", dialect) }.join(', ')
17
+ buffer << ")"
18
+ buffer
19
+ end
20
+
21
+ end # module FuncCall
22
+ end # module Sql
23
+ end # module Bmg
@@ -2,34 +2,12 @@ module Bmg
2
2
  module Sql
3
3
  module InnerJoin
4
4
  include Expr
5
+ include Join
5
6
 
6
7
  INNER = "INNER".freeze
7
- JOIN = "JOIN".freeze
8
- ON = "ON".freeze
9
8
 
10
- def join?
11
- true
12
- end
13
-
14
- def left
15
- self[1]
16
- end
17
-
18
- def right
19
- self[2]
20
- end
21
-
22
- def predicate
23
- last
24
- end
25
-
26
- def to_sql(buffer, dialect)
27
- left.to_sql(buffer, dialect)
28
- buffer << SPACE << JOIN << SPACE
29
- right.to_sql(buffer, dialect)
30
- buffer << SPACE << ON << SPACE
31
- predicate.to_sql(buffer, dialect)
32
- buffer
9
+ def type
10
+ INNER
33
11
  end
34
12
 
35
13
  end # module InnerJoin
@@ -0,0 +1,44 @@
1
+ module Bmg
2
+ module Sql
3
+ module Join
4
+ include Expr
5
+
6
+ JOIN = "JOIN".freeze
7
+ ON = "ON".freeze
8
+
9
+ def type
10
+ nil
11
+ end
12
+
13
+ def join?
14
+ true
15
+ end
16
+
17
+ def left
18
+ self[1]
19
+ end
20
+
21
+ def right
22
+ self[2]
23
+ end
24
+
25
+ def predicate
26
+ last
27
+ end
28
+
29
+ def to_sql(buffer, dialect)
30
+ left.to_sql(buffer, dialect)
31
+ if type.nil?
32
+ buffer << SPACE << JOIN << SPACE
33
+ else
34
+ buffer << SPACE << TYPE << SPACE << JOIN << SPACE
35
+ end
36
+ right.to_sql(buffer, dialect)
37
+ buffer << SPACE << ON << SPACE
38
+ predicate.to_sql(buffer, dialect)
39
+ buffer
40
+ end
41
+
42
+ end # module Join
43
+ end # module Sql
44
+ end # module Bmg
@@ -0,0 +1,15 @@
1
+ module Bmg
2
+ module Sql
3
+ module LeftJoin
4
+ include Expr
5
+ include Join
6
+
7
+ LEFT = "LEFT".freeze
8
+
9
+ def type
10
+ LEFT
11
+ end
12
+
13
+ end # module LeftJoin
14
+ end # module Sql
15
+ end # module Bmg
@@ -85,3 +85,4 @@ require_relative 'processor/semi_join'
85
85
  require_relative 'processor/flatten'
86
86
  require_relative 'processor/requalify'
87
87
  require_relative 'processor/summarize'
88
+ require_relative 'processor/bind'
@@ -0,0 +1,23 @@
1
+ module Bmg
2
+ module Sql
3
+ class Processor
4
+ class Bind < Processor
5
+
6
+ def initialize(binding, builder)
7
+ super(builder)
8
+ @binding = binding
9
+ end
10
+
11
+ def on_select_exp(sexpr)
12
+ if w = sexpr.where_clause
13
+ pred = Predicate::Grammar.sexpr(w.predicate.bind(@binding))
14
+ sexpr.with_update(:where_clause, [ :where_clause, pred ])
15
+ else
16
+ sexpr
17
+ end
18
+ end
19
+
20
+ end # class Bind
21
+ end # class Processor
22
+ end # module Sql
23
+ end # module Bmg
@@ -4,18 +4,19 @@ module Bmg
4
4
  class Join < Processor
5
5
  include JoinSupport
6
6
 
7
- def initialize(right, on, builder)
7
+ def initialize(right, on, options, builder)
8
8
  super(builder)
9
9
  @right = right
10
10
  @on = on
11
+ @options = options
11
12
  end
12
- attr_reader :right, :on
13
+ attr_reader :right, :on, :options
13
14
 
14
15
  def call(sexpr)
15
16
  if unjoinable?(sexpr)
16
17
  call(builder.from_self(sexpr))
17
18
  elsif unjoinable?(right)
18
- Join.new(builder.from_self(right), on, builder).call(sexpr)
19
+ Join.new(builder.from_self(right), on, options, builder).call(sexpr)
19
20
  else
20
21
  super(sexpr)
21
22
  end
@@ -45,14 +46,21 @@ module Bmg
45
46
  left_list, right_list = left.select_list, right.select_list
46
47
  list = left_list.dup
47
48
  right_list.each_child do |child, index|
48
- list << child unless left_list.knows?(child.as_name)
49
+ next if left_list.knows?(child.as_name)
50
+ if left_join?
51
+ list << coalesced(child)
52
+ else
53
+ list << child
54
+ end
49
55
  end
50
56
  list
51
57
  end
52
58
 
53
59
  def join_from_clauses(left, right)
54
60
  joincon = join_predicate(left, right, on)
55
- join = if joincon.tautology?
61
+ join = if left_join?
62
+ [:left_join, left.table_spec, right.table_spec, joincon]
63
+ elsif joincon.tautology?
56
64
  [:cross_join, left.table_spec, right.table_spec]
57
65
  else
58
66
  [:inner_join, left.table_spec, right.table_spec, joincon]
@@ -75,6 +83,26 @@ module Bmg
75
83
  order_by.first + order_by.last.sexpr_body
76
84
  end
77
85
 
86
+ private
87
+
88
+ def left_join?
89
+ options[:kind] == :left
90
+ end
91
+
92
+ def coalesced(child)
93
+ drt, as_name = options[:default_right_tuple], child.as_name.to_sym
94
+ if drt && drt.has_key?(as_name)
95
+ child.with_update(1, [
96
+ :func_call,
97
+ :coalesce,
98
+ child.left,
99
+ [:literal, drt[as_name]]
100
+ ])
101
+ else
102
+ child
103
+ end
104
+ end
105
+
78
106
  end # class Join
79
107
  end # class Processor
80
108
  end # module Sql
@@ -9,7 +9,7 @@ module Bmg
9
9
  h[k.to_s] = builder.next_qualifier!
10
10
  }
11
11
  end
12
- attr_reader :requalify
12
+ attr_reader :requalify
13
13
 
14
14
  alias :on_select_exp :copy_and_apply
15
15
  alias :on_missing :copy_and_apply
@@ -39,8 +39,8 @@ module Bmg
39
39
  def falsy?(sexpr)
40
40
  return false unless sexpr.respond_to?(:predicate)
41
41
  return false if sexpr.predicate.nil?
42
- left = Predicate.new(Predicate::Grammar.sexpr(sexpr.predicate))
43
- right = Predicate.new(Predicate::Grammar.sexpr(@predicate.sexpr))
42
+ left = Predicate.new(Predicate::Grammar.sexpr(sexpr.predicate)).unqualify
43
+ right = Predicate.new(Predicate::Grammar.sexpr(@predicate.sexpr)).unqualify
44
44
  return (left & right).contradiction?
45
45
  end
46
46
 
@@ -18,6 +18,12 @@ module Bmg
18
18
 
19
19
  public
20
20
 
21
+ def bind(binding)
22
+ expr = before_use(self.expr)
23
+ expr = Processor::Bind.new(binding, builder).call(expr)
24
+ _instance(type, builder, expr)
25
+ end
26
+
21
27
  def each(&bl)
22
28
  raise NotImplementedError
23
29
  end
@@ -56,7 +62,21 @@ module Bmg
56
62
  if right_expr = extract_compatible_sexpr(right)
57
63
  right_expr = Processor::Requalify.new(builder).call(right_expr)
58
64
  expr = before_use(self.expr)
59
- expr = Processor::Join.new(right_expr, on, builder).call(expr)
65
+ expr = Processor::Join.new(right_expr, on, {}, builder).call(expr)
66
+ _instance(type, builder, expr)
67
+ else
68
+ super
69
+ end
70
+ end
71
+
72
+ def _left_join(type, right, on, default_right_tuple)
73
+ if right_expr = extract_compatible_sexpr(right)
74
+ right_expr = Processor::Requalify.new(builder).call(right_expr)
75
+ expr = before_use(self.expr)
76
+ expr = Processor::Join.new(right_expr, on, {
77
+ kind: :left,
78
+ default_right_tuple: default_right_tuple
79
+ }, builder).call(expr)
60
80
  _instance(type, builder, expr)
61
81
  else
62
82
  super
@@ -23,14 +23,14 @@ module Bmg
23
23
  # Generates a relationally equivalent list of (type,table,predicate)
24
24
  # triplets, where:
25
25
  #
26
- # - type is :base, :cross_join or :inner_join
26
+ # - type is :base, :cross_join, :inner_join, or :left_join
27
27
  # - table is table_as, native_table_as or subquery_as
28
28
  # - predicate is a join predicate `ti.attri = tj.attrj AND ...`
29
29
  #
30
30
  # So that
31
31
  #
32
32
  # 1) the types are observed in strict increasing order (one :base, zero
33
- # or more :cross_join, zero or more :inner_join)
33
+ # or more :cross_join, zero or more :inner_join, zero or more :left_join)
34
34
  #
35
35
  # 2) the list is such that it can be safely written as an expression
36
36
  # of the following SQL syntax:
@@ -40,6 +40,7 @@ module Bmg
40
40
  # cross_join t3 # [ :cross_join, t3, nil ]
41
41
  # inner_join t4 ON p4 # [ :inner_join, t4, p4 ]
42
42
  # inner_join t5 ON p5 # [ :inner_join, t5, p5 ]
43
+ # left_join t6 ON p6 # [ :left_join, t6, p6 ]
43
44
  #
44
45
  # that is, the linearization is correct only if each predicate `pi`
45
46
  # only makes reference to tables introduced before it (no forward
@@ -87,13 +88,18 @@ module Bmg
87
88
  # reference to tables not introduced yet)
88
89
  #
89
90
  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)
91
+ # Our first strategy is simple: let sort the tables by moving the
92
+ # all left joins at the end, and all not referenced in join clauses
93
+ # at the beginning of the list => they will yield the base an cross
94
+ # join clauses first.
95
+ tables = tables.sort{|(t1,k1),(t2,k2)|
96
+ if k1 == :left_join || k2 == :left_join
97
+ k1 == k2 ? 0 : (k1 == :left_join ? 1 : -1)
98
+ else
99
+ t1js = joins.select{|j| uses?(j, t1) }.size
100
+ t2js = joins.select{|j| uses?(j, t2) }.size
101
+ t1js == 0 ? (t2js == 0 ? 0 : -1) : (t2js == 0 ? 1 : 0)
102
+ end
97
103
  }
98
104
 
99
105
  # Then order all recursively in that order of tables, filling a result
@@ -119,7 +125,15 @@ module Bmg
119
125
 
120
126
  # Decide which kind of join it is, according to the result and
121
127
  # the number of join clauses that will be used
122
- join_kind = result.empty? ? :base : (on.empty? ? :cross_join : :inner_join)
128
+ join_kind = if result.empty?
129
+ :base
130
+ elsif table.last == :left_join
131
+ :left_join
132
+ elsif on.empty?
133
+ :cross_join
134
+ else
135
+ :inner_join
136
+ end
123
137
 
124
138
  # Compute the AND([eq]) predicate on selected join clauses
125
139
  predicate = on.inject(nil){|p,clause|
@@ -127,7 +141,7 @@ module Bmg
127
141
  }
128
142
 
129
143
  # Recurse with that new clause in the result
130
- clause = [ join_kind, table, predicate ]
144
+ clause = [ join_kind, table[0], predicate ]
131
145
  _order_all(tables_tail, joins_tail, result + [clause])
132
146
  end
133
147
  end
@@ -139,13 +153,13 @@ module Bmg
139
153
  # `ti` and making no reference to non introduced tables, and the others.
140
154
  def split_joins(joins, table, tables_tail)
141
155
  joins.partition{|j|
142
- uses?(j, table) && !tables_tail.find{|t|
143
- uses?(j, t)
156
+ uses?(j, table[0]) && !tables_tail.find{|t|
157
+ uses?(j, t[0])
144
158
  }
145
159
  }
146
160
  end
147
161
 
148
- # Returns whether the join
162
+ # Returns whether the join conditions references the given table
149
163
  def uses?(condition, table)
150
164
  name = table.as_name.to_s
151
165
  left_name = var_name(condition[1])
@@ -172,23 +186,27 @@ module Bmg
172
186
  def collect(sexpr)
173
187
  tables = []
174
188
  joins = []
175
- _collect(sexpr, tables, joins)
189
+ _collect(sexpr, tables, joins, :base)
176
190
  [ tables, joins ]
177
191
  end
178
192
 
179
- def _collect(sexpr, tables, joins)
193
+ def _collect(sexpr, tables, joins, kind)
180
194
  case sexpr.first
181
195
  when :from_clause
182
- _collect(sexpr.table_spec, tables, joins)
196
+ _collect(sexpr.table_spec, tables, joins, kind)
183
197
  when :table_as, :native_table_as, :subquery_as
184
- tables << sexpr
198
+ tables << [sexpr, kind]
199
+ when :cross_join
200
+ _collect(sexpr.left, tables, joins, :cross_join)
201
+ _collect(sexpr.right, tables, joins, :cross_join)
185
202
  when :inner_join
186
203
  _collect_joins(sexpr.predicate, joins)
187
- _collect(sexpr.left, tables, joins)
188
- _collect(sexpr.right, tables, joins)
189
- when :cross_join
190
- _collect(sexpr.left, tables, joins)
191
- _collect(sexpr.right, tables, joins)
204
+ _collect(sexpr.left, tables, joins, :inner_join)
205
+ _collect(sexpr.right, tables, joins, :inner_join)
206
+ when :left_join
207
+ _collect_joins(sexpr.predicate, joins)
208
+ _collect(sexpr.left, tables, joins, kind)
209
+ _collect(sexpr.right, tables, joins, :left_join)
192
210
  end
193
211
  end
194
212
 
@@ -175,6 +175,15 @@ module Bmg
175
175
  }
176
176
  end
177
177
 
178
+ def left_join(right, on, default_right_tuple)
179
+ join_compatible!(right, on) if typechecked? && knows_attrlist?
180
+ dup.tap{|x|
181
+ x.attrlist = (knows_attrlist? and right.knows_attrlist?) ? (self.attrlist + right.attrlist).uniq : nil
182
+ x.predicate = Predicate.tautology
183
+ x.keys = nil
184
+ }
185
+ end
186
+
178
187
  def matching(right, on)
179
188
  join_compatible!(right, on) if typechecked? && knows_attrlist?
180
189
  self
@@ -1,8 +1,8 @@
1
1
  module Bmg
2
2
  module Version
3
3
  MAJOR = 0
4
- MINOR = 16
5
- TINY = 6
4
+ MINOR = 17
5
+ TINY = 3
6
6
  end
7
7
  VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::TINY}"
8
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bmg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.6
4
+ version: 0.17.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bernard Lambeau
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-31 00:00:00.000000000 Z
11
+ date: 2020-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: predicate
@@ -16,20 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.2'
19
+ version: '2.3'
20
20
  - - ">="
21
21
  - !ruby/object:Gem::Version
22
- version: 2.2.1
22
+ version: 2.3.3
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - "~>"
28
28
  - !ruby/object:Gem::Version
29
- version: '2.2'
29
+ version: '2.3'
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 2.2.1
32
+ version: 2.3.3
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: rake
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -194,9 +194,12 @@ files:
194
194
  - lib/bmg/sql/nodes/except.rb
195
195
  - lib/bmg/sql/nodes/expr.rb
196
196
  - lib/bmg/sql/nodes/from_clause.rb
197
+ - lib/bmg/sql/nodes/func_call.rb
197
198
  - lib/bmg/sql/nodes/group_by_clause.rb
198
199
  - lib/bmg/sql/nodes/inner_join.rb
199
200
  - lib/bmg/sql/nodes/intersect.rb
201
+ - lib/bmg/sql/nodes/join.rb
202
+ - lib/bmg/sql/nodes/left_join.rb
200
203
  - lib/bmg/sql/nodes/limit_clause.rb
201
204
  - lib/bmg/sql/nodes/literal.rb
202
205
  - lib/bmg/sql/nodes/name_intro.rb
@@ -222,6 +225,7 @@ files:
222
225
  - lib/bmg/sql/nodes/with_spec.rb
223
226
  - lib/bmg/sql/processor.rb
224
227
  - lib/bmg/sql/processor/all.rb
228
+ - lib/bmg/sql/processor/bind.rb
225
229
  - lib/bmg/sql/processor/clip.rb
226
230
  - lib/bmg/sql/processor/constants.rb
227
231
  - lib/bmg/sql/processor/distinct.rb
@@ -278,8 +282,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
278
282
  - !ruby/object:Gem::Version
279
283
  version: '0'
280
284
  requirements: []
281
- rubyforge_project:
282
- rubygems_version: 2.7.6
285
+ rubygems_version: 3.1.2
283
286
  signing_key:
284
287
  specification_version: 4
285
288
  summary: Bmg is Alf's successor.