bmg 0.17.0 → 0.17.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d94c440e4e3c70b16735c6d31e3ba50a4be12430d347d9ac8cff6622bc64fb3
4
- data.tar.gz: 1443c66b0c1c621f97b2f2f8bea2c522c07151ec4bbef71b45585078635c5b7b
3
+ metadata.gz: faea9567e3ec11347ccd8d1e063d027a720095bc8623221597a40e26aade3d99
4
+ data.tar.gz: 01b649d2810c6460822c06c83ce06911d438f68cc9b8d297afb766c389619544
5
5
  SHA512:
6
- metadata.gz: 7eba5332a684097f8cd95a1c28463bbb1e5b004788bf3b3133a55a0c1cf1b5b5335a41924b9ba98cae10fe81f2c4b0ad70bb27d4f97618a8d18a7dbc0f48265f
7
- data.tar.gz: 0241a9d8b895ca70df1a7f705ceecce062369810a8b4f60e1f314db0ab2888dc0060ad2f37697b7c83ab11cd2eaefd13e8445d1486d9827f294766bd9f3471aa
6
+ metadata.gz: e75f1778fc7fd0578b37fda44c6b2ccf5f5aa4441c16efeb42b495c38bc9361495b6597d70ce0ea8829caceaceb708c247c24b8531464d6c16f8f3faef1ee01f
7
+ data.tar.gz: 3762248213c27bf9e11276fe3a2b9deb75a124e80cc2f7cdf8fbe9a70856649f85fe55eb71474ae6f4a2836fd3d2d144ff6ae44b7b3810ff08bde51d813136a1
data/Gemfile CHANGED
@@ -1,5 +1,5 @@
1
1
  source "https://rubygems.org"
2
2
  gemspec
3
3
 
4
- gem "predicate", github: "enspirit/predicate", branch: "placeholders"
4
+ # gem "predicate", github: "enspirit/predicate", branch: "placeholders"
5
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.
data/lib/bmg.rb CHANGED
@@ -38,6 +38,7 @@ module Bmg
38
38
  require_relative 'bmg/operator'
39
39
 
40
40
  require_relative 'bmg/reader'
41
+ require_relative 'bmg/writer'
41
42
 
42
43
  require_relative 'bmg/relation/empty'
43
44
  require_relative 'bmg/relation/in_memory'
@@ -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] }]
@@ -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
@@ -105,6 +105,19 @@ module Bmg
105
105
  to_a.to_json(*args, &bl)
106
106
  end
107
107
 
108
+ # Writes the relation data to CSV.
109
+ #
110
+ # `string_or_io` and `options` are what CSV::new itself
111
+ # recognizes, default options are CSV's.
112
+ #
113
+ # When no string_or_io is used, the method uses a string.
114
+ #
115
+ # The method always returns the string_or_io.
116
+ def to_csv(options = {}, string_or_io = nil)
117
+ options, string_or_io = {}, options unless options.is_a?(Hash)
118
+ Writer::Csv.new(options).call(self, string_or_io)
119
+ end
120
+
108
121
  # Converts to an sexpr expression.
109
122
  def to_ast
110
123
  raise "Bmg is missing a feature!"
@@ -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
@@ -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))
@@ -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)
@@ -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
@@ -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
 
@@ -62,7 +62,21 @@ module Bmg
62
62
  if right_expr = extract_compatible_sexpr(right)
63
63
  right_expr = Processor::Requalify.new(builder).call(right_expr)
64
64
  expr = before_use(self.expr)
65
- 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)
66
80
  _instance(type, builder, expr)
67
81
  else
68
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
@@ -2,7 +2,7 @@ module Bmg
2
2
  module Version
3
3
  MAJOR = 0
4
4
  MINOR = 17
5
- TINY = 0
5
+ TINY = 5
6
6
  end
7
7
  VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::TINY}"
8
8
  end
@@ -0,0 +1 @@
1
+ require_relative 'writer/csv'
@@ -0,0 +1,31 @@
1
+ module Bmg
2
+ module Writer
3
+ class Csv
4
+ include Writer
5
+
6
+ DEFAULT_OPTIONS = {
7
+ }
8
+
9
+ def initialize(options)
10
+ @options = DEFAULT_OPTIONS.merge(options)
11
+ end
12
+ attr_reader :options
13
+
14
+ def call(relation, string_or_io = nil)
15
+ require 'csv'
16
+ string_or_io, to_s = string_or_io.nil? ? [StringIO.new, true] : [string_or_io, false]
17
+ headers = relation.type.to_attrlist if relation.type.knows_attrlist?
18
+ csv = nil
19
+ relation.each do |tuple|
20
+ if csv.nil?
21
+ headers = tuple.keys if headers.nil?
22
+ csv = CSV.new(string_or_io, options.merge(headers: headers))
23
+ end
24
+ csv << headers.map{|h| tuple[h] }
25
+ end
26
+ to_s ? string_or_io.string : string_or_io
27
+ end
28
+
29
+ end # class Csv
30
+ end # module Writer
31
+ end # module Bmg
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.17.0
4
+ version: 0.17.5
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-04-21 00:00:00.000000000 Z
11
+ date: 2020-08-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: predicate
@@ -16,62 +16,62 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2.3'
19
+ version: '2.4'
20
20
  - - ">="
21
21
  - !ruby/object:Gem::Version
22
- version: 2.3.0
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.3'
29
+ version: '2.4'
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 2.3.0
32
+ version: 2.4.0
33
33
  - !ruby/object:Gem::Dependency
34
- name: rake
34
+ name: path
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '10'
40
- type: :development
39
+ version: '1.3'
40
+ type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "~>"
44
+ - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '10'
46
+ version: '1.3'
47
47
  - !ruby/object:Gem::Dependency
48
- name: rspec
48
+ name: rake
49
49
  requirement: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '3.6'
53
+ version: '13'
54
54
  type: :development
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '3.6'
60
+ version: '13'
61
61
  - !ruby/object:Gem::Dependency
62
- name: path
62
+ name: rspec
63
63
  requirement: !ruby/object:Gem::Requirement
64
64
  requirements:
65
- - - ">="
65
+ - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '1.3'
67
+ version: '3.6'
68
68
  type: :development
69
69
  prerelease: false
70
70
  version_requirements: !ruby/object:Gem::Requirement
71
71
  requirements:
72
- - - ">="
72
+ - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '1.3'
74
+ version: '3.6'
75
75
  - !ruby/object:Gem::Dependency
76
76
  name: roo
77
77
  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
@@ -258,6 +261,8 @@ files:
258
261
  - lib/bmg/support/tuple_algebra.rb
259
262
  - lib/bmg/type.rb
260
263
  - lib/bmg/version.rb
264
+ - lib/bmg/writer.rb
265
+ - lib/bmg/writer/csv.rb
261
266
  - tasks/gem.rake
262
267
  - tasks/test.rake
263
268
  homepage: http://github.com/enspirit/bmg