bmg 0.16.7 → 0.17.4

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: 3ca0b8ab7a34fca3dbf7cb37774b099289a8e15cf42fd381b9ee88f3ac01ff81
4
- data.tar.gz: e96b6905b920b0cf70e52227ffe19dcd40ce3a9263deadf032a628348f4c340c
3
+ metadata.gz: 1e48d4a2d3c4bea8271e40ad4db52f43438de7f754079cc4c5bcd5e98ce4865d
4
+ data.tar.gz: 3fff9f251a2b766ab3de631748ccc6fe6f6b96ccef88507cb3ed76766fea3992
5
5
  SHA512:
6
- metadata.gz: 6727a3c5b475e5a69765b0f2d4093df4f2aa2ee67364589ee8c0a8e3d368acf50cbe092e7651791e3b13f70ee4d51718921166cac62595998f0b0de2775b1b54
7
- data.tar.gz: ef2c1b8eb520f5a4a3a7a24a95f7afcbb96be47e77872dbd55100a0369bd65d7c4d173234d4966591a803106b4937505c5d36ab8ee59d8fde2d6c261988e3ab8
6
+ metadata.gz: facfbb798258cfdec3803cc7b93999d23c8a0776ead53c074266f4f658a27feee1a3e1dcc1145e7ad45bb03579d22f5d65108152516ac0af0d79aa59805ee016
7
+ data.tar.gz: 10271e608276bc2e00ccee3d6a94287cb2211d945898cb9cfa54764a05027c9e6a6c34a11e9725f9ed47df4df537fe054d8c3f17b24f58da9713761121523e73
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"
data/README.md CHANGED
@@ -14,7 +14,7 @@ also much simpler, and make its easier to implement user-defined relations.
14
14
 
15
15
  ## Example
16
16
 
17
- ```
17
+ ```ruby
18
18
  require 'bmg'
19
19
  require 'json'
20
20
 
@@ -32,13 +32,14 @@ by_city = suppliers
32
32
  .group([:sid, :name, :status], :suppliers_in)
33
33
 
34
34
  puts JSON.pretty_generate(by_city)
35
+ # [{...},...]
35
36
  ```
36
37
 
37
38
  ## Connecting to a SQL database
38
39
 
39
40
  Bmg requires `sequel >= 3.0` to connect to SQL databases.
40
41
 
41
- ```
42
+ ```ruby
42
43
  require 'sqlite3'
43
44
  require 'bmg'
44
45
  require 'bmg/sequel'
@@ -47,16 +48,67 @@ DB = Sequel.connect("sqlite://suppliers-and-parts.db")
47
48
 
48
49
  suppliers = Bmg.sequel(:suppliers, DB)
49
50
 
50
- puts suppliers
51
+ big_suppliers = suppliers
51
52
  .restrict(Predicate.neq(status: 30))
52
- .to_sql
53
53
 
54
+ puts big_suppliers.to_sql
54
55
  # SELECT `t1`.`sid`, `t1`.`name`, `t1`.`status`, `t1`.`city` FROM `suppliers` AS 't1' WHERE (`t1`.`status` != 30)
56
+
57
+ puts JSON.pretty_generate(big_suppliers)
58
+ # [{...},...]
55
59
  ```
56
60
 
61
+ ## How is this different from similar libraries?
62
+
63
+ 1. The libraries you probably know (Sequel, Arel, SQLAlchemy, Korma, jOOQ,
64
+ etc.) do not implement a genuine relational algebra: their support for
65
+ chaining relational operators is limited (yielding errors or wrong SQL
66
+ queries). Bmg **always** allows chaining operators. If it does not, it's
67
+ a bug. In other words, the following query is 100% valid:
68
+
69
+ relation
70
+ .restrict(...) # aka where
71
+ .union(...)
72
+ .summarize(...) # aka group by
73
+ .restrict(...)
74
+
75
+ 2. Bmg supports in memory relations, json relations, csv relations, SQL
76
+ relations and so on. It's not tight to SQL generation, and supports
77
+ queries accross multiple data sources.
78
+
79
+ 3. Bmg makes a best effort to optimize queries, simplifying both generated
80
+ SQL code (low-level accesses to datasources) and in-memory operations.
81
+
82
+ 4. Bmg supports various *structuring* operators (group, image, autowrap,
83
+ autosummarize, etc.) and allows building 'non flat' relations.
84
+
85
+ ## How is this different from Alf?
86
+
87
+ 1. Bmg's implementation is much simpler than Alf, and uses no ruby core
88
+ extention.
89
+
90
+ 2. We are confident using Bmg in production. Systematic inspection of query
91
+ plans is suggested though. Alf was a bit too experimental to be used on
92
+ (critical) production systems.
93
+
94
+ 2. Alf exposes a functional syntax, command line tool, restful tools and
95
+ many more. Bmg is limited to the core algebra, main Relation abstraction
96
+ and SQL generation.
97
+
98
+ 3. Bmg is less strict regarding conformance to relational theory, and
99
+ may actually expose non relational features (such as support for null,
100
+ left_join operator, etc.). Sharp tools hurt, use them with great care.
101
+
102
+ 4. Bmg does not yet implement all operators documented on try-alf.org, even
103
+ if we plan to eventually support them all.
104
+
105
+ 5. Bmg has a few additional operators that prove very useful on real
106
+ production use cases: prefix, suffix, autowrap, autosummarize, left_join,
107
+ rxmatch, etc.
108
+
57
109
  ## Supported operators
58
110
 
59
- ```
111
+ ```ruby
60
112
  r.allbut([:a, :b, ...]) # remove specified attributes
61
113
  r.autowrap(split: '_') # structure a flat relation, split: '_' is the default
62
114
  r.autosummarize([:a, :b, ...], x: :sum) # (experimental) usual summarizers supported
@@ -66,6 +118,8 @@ r.group([:a, :b, ...], :x) # relation-valued attribute from at
66
118
  r.image(right, :x, [:a, :b, ...]) # relation-valued attribute from another relation
67
119
  r.join(right, [:a, :b, ...]) # natural join on a join key
68
120
  r.join(right, :a => :x, :b => :y, ...) # natural join after right reversed renaming
121
+ r.left_join(right, [:a, :b, ...], {...}) # left join with optional default right tuple
122
+ r.left_join(right, {:a => :x, ...}, {...}) # left join after right reversed renaming
69
123
  r.matching(right, [:a, :b, ...]) # semi join, aka where exists
70
124
  r.matching(right, :a => :x, :b => :y, ...) # semi join, after right reversed renaming
71
125
  r.not_matching(right, [:a, :b, ...]) # inverse semi join, aka where not exists
@@ -80,3 +134,13 @@ r.summarize([:a, :b, ...], x: :sum) # relational summarization
80
134
  r.suffix(:_foo, but: [:a, ...]) # suffix kind of renaming
81
135
  r.union(right) # relational union
82
136
  ```
137
+
138
+ ## Who is behind Bmg?
139
+
140
+ Bernard Lambeau (bernard@klaro.cards) is Alf & Bmg main engineer & maintainer.
141
+
142
+ Enspirit (https://enspirit.be) and Klaro App (https://klaro.cards) are both
143
+ actively using and contributing to the library.
144
+
145
+ Feel free to contact us for help, ideas and/or contributions. Please use github
146
+ issues and pull requests if possible if code is involved.
@@ -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
@@ -37,6 +37,12 @@ module Bmg
37
37
  self.join(right.rename(renaming), on.keys)
38
38
  end
39
39
 
40
+ def left_join(right, on = [], *args)
41
+ return super unless on.is_a?(Hash)
42
+ renaming = Hash[on.map{|k,v| [v,k] }]
43
+ self.left_join(right.rename(renaming), on.keys, *args)
44
+ end
45
+
40
46
  def matching(right, on = [])
41
47
  return super unless on.is_a?(Hash)
42
48
  renaming = Hash[on.map{|k,v| [v,k] }]
@@ -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
@@ -41,7 +41,7 @@ module Bmg
41
41
  dataset = apply(sexpr.from_clause) if sexpr.from_clause
42
42
  #
43
43
  selection = apply(sexpr.select_list)
44
- predicate = apply(sexpr.predicate) if sexpr.predicate
44
+ predicate = compile_predicate(sexpr.predicate) if sexpr.predicate
45
45
  grouping = apply(sexpr.group_by_clause) if sexpr.group_by_clause
46
46
  order = apply(sexpr.order_by_clause) if sexpr.order_by_clause
47
47
  limit = apply(sexpr.limit_clause) if sexpr.limit_clause
@@ -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))
@@ -104,8 +109,15 @@ module Bmg
104
109
  elsif kind == :inner_join
105
110
  options = { qualify: false, table_alias: false }
106
111
  ds.join_table(:inner, apply(table), nil, options){|*args|
107
- apply(on)
112
+ compile_predicate(on)
113
+ }
114
+ elsif kind == :left_join
115
+ options = { qualify: false, table_alias: false }
116
+ ds.join_table(:left, apply(table), nil, options){|*args|
117
+ compile_predicate(on)
108
118
  }
119
+ else
120
+ raise IllegalArgumentError, "Unrecognized from clause: `#{sexpr}`"
109
121
  end
110
122
  end
111
123
  end
@@ -153,22 +165,35 @@ module Bmg
153
165
  sexpr.last
154
166
  end
155
167
 
168
+ private
169
+
170
+ def dataset(expr)
171
+ return expr if ::Sequel::Dataset===expr
172
+ sequel_db[expr]
173
+ end
174
+
175
+ def compile_predicate(predicate)
176
+ PredicateTranslator.new(self).call(predicate)
177
+ end
178
+
179
+ class PredicateTranslator < Sexpr::Processor
180
+ include ::Predicate::ToSequel::Methods
181
+
182
+ def initialize(parent)
183
+ @parent = parent
184
+ end
185
+
156
186
  public ### Predicate hack
157
187
 
158
188
  def on_opaque(sexpr)
159
- apply(sexpr.last)
189
+ @parent.apply(sexpr.last)
160
190
  end
161
191
 
162
192
  def on_exists(sexpr)
163
- apply(sexpr.last).exists
193
+ @parent.apply(sexpr.last).exists
164
194
  end
165
195
 
166
- private
167
-
168
- def dataset(expr)
169
- return expr if ::Sequel::Dataset===expr
170
- sequel_db[expr]
171
- end
196
+ end
172
197
 
173
198
  end # class Translator
174
199
  end # module Sequel
@@ -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:
@@ -15,6 +15,10 @@ module Bmg
15
15
  last
16
16
  end
17
17
 
18
+ def is_computed?
19
+ false
20
+ end
21
+
18
22
  def to_sql(buffer, dialect)
19
23
  buffer << dialect.quote_identifier(last)
20
24
  buffer
@@ -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,27 @@
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 is_computed?
15
+ true
16
+ end
17
+
18
+ def to_sql(buffer, dialect)
19
+ buffer << summary_name.upcase << "("
20
+ buffer << func_args.map{|fa| fa.to_sql("", dialect) }.join(', ')
21
+ buffer << ")"
22
+ buffer
23
+ end
24
+
25
+ end # module FuncCall
26
+ end # module Sql
27
+ 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
@@ -8,6 +8,10 @@ module Bmg
8
8
  nil
9
9
  end
10
10
 
11
+ def is_computed?
12
+ false
13
+ end
14
+
11
15
  def to_sql(buffer, dialect)
12
16
  to_sql_literal(buffer, last)
13
17
  buffer
@@ -20,6 +20,10 @@ module Bmg
20
20
  as_name
21
21
  end
22
22
 
23
+ def is_computed?
24
+ false
25
+ end
26
+
23
27
  def to_sql(buffer, dialect)
24
28
  self[1].to_sql(buffer, dialect)
25
29
  buffer << '.'
@@ -27,6 +27,10 @@ module Bmg
27
27
  set_quantifier.all?
28
28
  end
29
29
 
30
+ def has_computed_attributes?
31
+ select_list.has_computed_attributes?
32
+ end
33
+
30
34
  def join?
31
35
  from_clause && from_clause.join?
32
36
  end
@@ -23,6 +23,10 @@ module Bmg
23
23
  last.as_name
24
24
  end
25
25
 
26
+ def is_computed?
27
+ left.is_computed?
28
+ end
29
+
26
30
  def to_sql(buffer, dialect)
27
31
  self[1].to_sql(buffer, dialect)
28
32
  unless would_be_name == as_name
@@ -23,6 +23,10 @@ module Bmg
23
23
  Builder::IS_TABLE_DEE == self
24
24
  end
25
25
 
26
+ def has_computed_attributes?
27
+ sexpr_body.any?{|item| item.is_computed? }
28
+ end
29
+
26
30
  def knows?(as_name)
27
31
  find_child{|child| child.as_name == as_name }
28
32
  end
@@ -11,6 +11,10 @@ module Bmg
11
11
  }
12
12
  end
13
13
 
14
+ def has_computed_attributes?
15
+ false
16
+ end
17
+
14
18
  def to_sql(buffer, dialect)
15
19
  last.to_sql(buffer, dialect)
16
20
  buffer << DOT << STAR
@@ -11,6 +11,10 @@ module Bmg
11
11
  self.last
12
12
  end
13
13
 
14
+ def is_computed?
15
+ true
16
+ end
17
+
14
18
  def to_sql(buffer, dialect)
15
19
  buffer << summary_func.upcase << "("
16
20
  summary_expr.to_sql(buffer, dialect)
@@ -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
@@ -20,7 +20,7 @@ module Bmg
20
20
  end
21
21
 
22
22
  def on_select_exp(sexpr)
23
- if sexpr.group_by_clause
23
+ if sexpr.group_by_clause || sexpr.has_computed_attributes?
24
24
  sexpr = builder.from_self(sexpr)
25
25
  call(sexpr)
26
26
  else
@@ -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 = 7
4
+ MINOR = 17
5
+ TINY = 4
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.7
4
+ version: 0.17.4
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-05-15 00:00:00.000000000 Z
11
+ date: 2020-07-23 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.4'
20
20
  - - ">="
21
21
  - !ruby/object:Gem::Version
22
- version: 2.2.1
22
+ version: 2.4.0
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.4'
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 2.2.1
32
+ version: 2.4.0
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