bmg 0.23.5 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb0b47d8dbd6924eb1f4fb7e773635a5d1fc83fc09b09c60ead3ac913bbd2051
4
- data.tar.gz: ad0919fc045b59b5299254d0f476b31390069b6d39e086334cd55790eedf56e0
3
+ metadata.gz: 1cf55d89350451dc43a50e85413570b8525fdb1af1467cf33c854d65cd885324
4
+ data.tar.gz: a0a665602f3e4deceacfa6bc1bf301dd91e9344d96d93f3cc1df462e3b364fb1
5
5
  SHA512:
6
- metadata.gz: 1fa7c3dfa036762e82dd86275c522af811eba747901f288e8b1804388ddee0094350374795527b0884cf8d5355eea649f9c3ce4a66ec7c9742eeaef5fff81a16
7
- data.tar.gz: 5cee0487c9e70df6a049791ef48d93fda5ff516d769b7f8d25f9ea6f29d9d780bec8aebca78a8e88e435113acde6860fc1c7dbbbb2c9bb7f70edf88d22bfe739
6
+ metadata.gz: 2cf7cfe088705ba0ac9b90aaa47c457d2f5fbd7dc9d28f8dc246b91e8dbd4d492ff6ef0e892385d2fb6098998e9d07859d355b68f833154d4c4dbd2f6db87149
7
+ data.tar.gz: bd120334a2e20ff7b20cd15d4d96ac3c3651fac5011aadc3dc6f3acf301f9b5f1245fc8e46571f7ab67ed8db4a45d463728605172c0e8d4213b753289d64959e
@@ -3,7 +3,9 @@ module Bmg
3
3
  class DataFolder < Database
4
4
 
5
5
  DEFAULT_OPTIONS = {
6
- data_extensions: ['json', 'yml', 'yaml', 'csv']
6
+ data_extensions: ['json', 'yml', 'yaml', 'csv'],
7
+ relname_from_file: ->(file) { file.basename.rm_ext.to_sym },
8
+ filename_from_relname: ->(relname) { relname },
7
9
  }
8
10
 
9
11
  def initialize(folder, options = {})
@@ -25,15 +27,21 @@ module Bmg
25
27
  next unless @options[:data_extensions].find {|ext|
26
28
  path.ext == ".#{ext}" || path.ext == ext
27
29
  }
28
- yield(path.basename.rm_ext.to_sym, read_file(path))
30
+ yield(@options[:relname_from_file].call(path), read_file(path))
29
31
  end
30
32
  end
31
33
 
32
- def self.dump(database, path, ext = :json)
34
+ def self.dump(database, path, ext = :json, options = DEFAULT_OPTIONS)
35
+ options = DEFAULT_OPTIONS.merge(options)
33
36
  path = Path(path)
34
37
  path.mkdir_p
35
38
  database.each_relation_pair do |name, rel|
36
- (path/"#{name}.#{ext}").write(rel.public_send(:"to_#{ext}"))
39
+ filename = options[:filename_from_relname].call(name)
40
+ if ext === :json
41
+ (path/"#{filename}.#{ext}").write(JSON.pretty_generate(rel))
42
+ else
43
+ (path/"#{filename}.#{ext}").write(rel.public_send(:"to_#{ext}"))
44
+ end
37
45
  end
38
46
  path
39
47
  end
@@ -56,8 +64,8 @@ module Bmg
56
64
  def find_file(name)
57
65
  exts = @options[:data_extensions]
58
66
  exts.each do |ext|
59
- target = @folder/"#{name}.#{ext}"
60
- return target if target.file?
67
+ target = @folder.glob("*#{name}.#{ext}")
68
+ return target.first if target&.first&.file?
61
69
  end
62
70
  raise NotSuchRelationError, "#{@folder}/#{name}.#{exts.join(',')}"
63
71
  end
@@ -3,12 +3,13 @@ module Bmg
3
3
  class Xlsx < Database
4
4
 
5
5
  DEFAULT_OPTIONS = {
6
+ reader_options: {}
6
7
  }
7
8
 
8
9
  def initialize(path, options = {})
9
10
  path = Path(path) if path.is_a?(String)
10
11
  @path = path
11
- @options = options.merge(DEFAULT_OPTIONS)
12
+ @options = DEFAULT_OPTIONS.merge(options)
12
13
  end
13
14
 
14
15
  def method_missing(name, *args, &bl)
@@ -33,7 +34,9 @@ module Bmg
33
34
  end
34
35
 
35
36
  def rel_for(sheet_name)
36
- Bmg.excel(@path, { sheet: sheet_name.to_s })
37
+ Bmg.excel(@path, @options[:reader_options].merge({
38
+ sheet: sheet_name.to_s
39
+ }))
37
40
  end
38
41
 
39
42
  end # class Sequel
data/lib/bmg/database.rb CHANGED
@@ -24,6 +24,7 @@ module Bmg
24
24
  end
25
25
 
26
26
  def to_data_folder(*args)
27
+ require_relative 'database/data_folder'
27
28
  DataFolder.dump(self, *args)
28
29
  end
29
30
 
@@ -31,5 +32,9 @@ module Bmg
31
32
  raise NotImplementedError
32
33
  end
33
34
 
35
+ def output_preferences_for(name)
36
+ nil
37
+ end
38
+
34
39
  end # class Database
35
40
  end # module Bmg
@@ -6,7 +6,8 @@ module Bmg
6
6
  DEFAULT_OPTIONS = {
7
7
  sheet: 0,
8
8
  skip: 0,
9
- row_num: true
9
+ row_num: true,
10
+ grouping_character: nil,
10
11
  }
11
12
 
12
13
  def initialize(type, path, options = {})
@@ -23,13 +24,16 @@ module Bmg
23
24
  headers = headers[1..-1] if generate_row_num?
24
25
  start_at = @options[:skip] + 2
25
26
  end_at = spreadsheet.last_row
27
+
28
+ previous = {}
26
29
  (start_at..end_at).each do |i|
27
30
  row = spreadsheet.row(i)
28
31
  init = init_tuple(i - start_at + 1)
29
32
  tuple = (0...headers.size).each_with_object(init){|i,t|
30
- t[headers[i]] = row[i]
33
+ t[headers[i]] = extract_value(headers[i], row[i], previous)
31
34
  }
32
35
  yield(tuple)
36
+ previous = tuple
33
37
  end
34
38
  end
35
39
 
@@ -75,6 +79,12 @@ module Bmg
75
79
  { row_num_name => i }
76
80
  end
77
81
 
82
+ def extract_value(attribute, value, previous)
83
+ return value unless c = @options[:grouping_character]
84
+
85
+ value == c ? previous[attribute] : value
86
+ end
87
+
78
88
  end # class Excel
79
89
  end # module Reader
80
90
  end # module Bmg
@@ -70,13 +70,21 @@ 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, :func_call
73
+ when :literal, :summarizer, :func_call, :case_when
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_case_when(sexpr)
81
+ operand = apply(sexpr[1])
82
+ mapping = sexpr[2..-1].each_slice(2).each_with_object({}){|(w, t), h|
83
+ h[w.last] = t.last
84
+ }
85
+ ::Sequel.case(mapping, nil, operand)
86
+ end
87
+
80
88
  def on_func_call(sexpr)
81
89
  case sexpr.func_name
82
90
  when :cast
@@ -48,3 +48,4 @@ require_relative "nodes/inner_join"
48
48
  require_relative "nodes/left_join"
49
49
  require_relative "nodes/summarizer"
50
50
  require_relative "nodes/func_call"
51
+ require_relative "nodes/case_when"
@@ -81,6 +81,9 @@ rules:
81
81
  - summarizer
82
82
  - func_call
83
83
  - literal
84
+ - case_when
85
+ case_when:
86
+ - [ scalar_exp, literal+ ]
84
87
  a_name:
85
88
  - qualified_name
86
89
  - column_name
@@ -0,0 +1,33 @@
1
+ module Bmg
2
+ module Sql
3
+ module CaseWhen
4
+ include Expr
5
+
6
+ def operand
7
+ self[1]
8
+ end
9
+
10
+ def whens
11
+ self[2..-1].each_slice(2).to_a
12
+ end
13
+
14
+ def is_computed?
15
+ true
16
+ end
17
+
18
+ def to_sql(buffer, dialect)
19
+ buffer << "CASE "
20
+ operand.to_sql(buffer, dialect)
21
+ whens.each do |(when_val, then_val)|
22
+ buffer << " WHEN "
23
+ when_val.to_sql(buffer, dialect)
24
+ buffer << " THEN "
25
+ then_val.to_sql(buffer, dialect)
26
+ end
27
+ buffer << " END"
28
+ buffer
29
+ end
30
+
31
+ end # module CaseWhen
32
+ end # module Sql
33
+ end # module Bmg
@@ -50,6 +50,14 @@ module Bmg
50
50
  SplitSupported.split_supported(*args, &bl)
51
51
  end
52
52
 
53
+ # An SQL-able element the transform can emit verbatim: an object that
54
+ # carries its own SQL (the predicate gem's `:sql`/`:sql_literal`
55
+ # convention) or a Sequel expression (rendered through `Sequel.expr`).
56
+ def self.sql_expr?(x)
57
+ x.respond_to?(:sql_literal) || x.respond_to?(:sql) ||
58
+ (defined?(::Sequel::SQL::Expression) && x.is_a?(::Sequel::SQL::Expression))
59
+ end
60
+
53
61
  def on_select_list(sexpr)
54
62
  sexpr.each_with_index.map{|child,index|
55
63
  index == 0 ? child : apply(child)
@@ -66,8 +74,17 @@ module Bmg
66
74
  func_call_node(sexpr, Array(t).reverse),
67
75
  sexpr[2]
68
76
  ])
77
+ when Hash
78
+ sexpr([:select_item,
79
+ case_when_node(sexpr, t),
80
+ sexpr[2]
81
+ ])
69
82
  else
70
- raise NotSupportedError
83
+ # An SQL-able value (e.g. a Sequel expression) carries its own SQL
84
+ # — including the column reference — so it rides through as a
85
+ # literal, which the Sequel translator emits verbatim.
86
+ raise NotSupportedError unless Transform.sql_expr?(t)
87
+ sexpr([:select_item, [:literal, t], sexpr[2]])
71
88
  end
72
89
  end
73
90
 
@@ -89,6 +106,15 @@ module Bmg
89
106
  [ :literal, head ] ]
90
107
  end
91
108
 
109
+ def case_when_node(sexpr, mapping)
110
+ node = [:case_when, sexpr[1]]
111
+ mapping.each do |when_val, then_val|
112
+ node << [:literal, when_val]
113
+ node << [:literal, then_val]
114
+ end
115
+ node
116
+ end
117
+
92
118
  def transformation_for(as)
93
119
  case t = transformation
94
120
  when Class then t
@@ -167,7 +167,8 @@ module Bmg
167
167
  def _transform(type, transformation, options)
168
168
  expr = before_use(self.expr)
169
169
  sup, unsup = Processor::Transform.split_supported(transformation){|x|
170
- [String, Integer, Float, Date, DateTime].include?(x)
170
+ [String, Integer, Float, Date, DateTime].include?(x) || x.is_a?(Hash) ||
171
+ Processor::Transform.sql_expr?(x)
171
172
  }
172
173
  return super if sup.nil?
173
174
  expr = Processor::Transform.new(sup, options, builder).call(expr)
@@ -4,6 +4,7 @@ module Bmg
4
4
  DEFAULT_PREFS = {
5
5
  attributes_ordering: nil,
6
6
  tuple_ordering: nil,
7
+ grouping_attributes: nil,
7
8
  extra_attributes: :after
8
9
  }
9
10
 
@@ -36,14 +37,20 @@ module Bmg
36
37
  options[:grouping_attributes]
37
38
  end
38
39
 
40
+ def grouping_character
41
+ options[:grouping_character]
42
+ end
43
+
39
44
  def erase_redundance_in_group(before, current)
40
45
  return [nil, current] unless ga = grouping_attributes
41
46
  return [current, current] unless before
42
47
 
43
48
  new_before, new_current = current.dup, current.dup
44
49
  ga.each do |attr|
45
- return [new_before, new_current] unless before[attr] == current[attr]
46
- new_current[attr] = nil
50
+ unless before[attr] == current[attr]
51
+ return [new_before, new_current]
52
+ end
53
+ new_current[attr] = grouping_character
47
54
  end
48
55
  [new_before, new_current]
49
56
  end
data/lib/bmg/version.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  module Bmg
2
2
  module Version
3
3
  MAJOR = 0
4
- MINOR = 23
5
- TINY = 5
4
+ MINOR = 24
5
+ TINY = 0
6
6
  end
7
7
  VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::TINY}"
8
8
  end
@@ -25,7 +25,7 @@ module Bmg
25
25
  rel.to_xlsx({
26
26
  workbook: workbook,
27
27
  worksheet: worksheet,
28
- })
28
+ }, nil, database.output_preferences_for(name))
29
29
  end
30
30
  workbook.close
31
31
  end
@@ -39,31 +39,48 @@ module Bmg
39
39
  @worksheet = workbook.add_worksheet(@worksheet) if @worksheet.is_a?(String)
40
40
 
41
41
  headers = infer_headers(relation.type)
42
+ max_widths = Hash.new{|h,k| h[k] = 5 }
42
43
  before = nil
44
+
45
+ write_headers(headers, worksheet, max_widths) unless headers.nil?
46
+
43
47
  each_tuple(relation) do |tuple,i|
44
- headers = infer_headers(tuple) if headers.nil?
45
- headers.each_with_index do |h,i|
46
- worksheet.write_string(0, i, h)
47
- end if i == 0
48
+ if headers.nil?
49
+ headers = infer_headers(tuple)
50
+ write_headers(headers, worksheet, max_widths)
51
+ end
48
52
  before, tuple = output_preferences.erase_redundance_in_group(before, tuple)
49
53
  headers.each_with_index do |h,j|
50
- meth, *args = write_pair(tuple[h])
54
+ meth, args, approx_width = write_pair(tuple[h])
51
55
  worksheet.send(meth, 1+i, j, *args)
56
+ max_widths[j] = [max_widths[j], approx_width].max
52
57
  end
53
58
  end
54
59
 
60
+ max_widths.each_pair do |col, width|
61
+ worksheet.set_column(col, col, [1+width, 100].min)
62
+ end
63
+ worksheet.freeze_panes(1, 0)
55
64
  workbook.close unless xlsx_options[:workbook]
56
65
  path
57
66
  end
58
67
 
68
+ def write_headers(headers, worksheet, max_widths)
69
+ header_format = workbook.add_format(bold: true)
70
+ headers.each_with_index do |h,i|
71
+ worksheet.write_string(0, i, h, header_format)
72
+ max_widths[i] = [max_widths[i], h.to_s.size].max
73
+ end
74
+ end
75
+
59
76
  def write_pair(value)
60
77
  case value
61
78
  when Numeric
62
- [:write_number, value]
79
+ [:write_number, [value], value.to_s.size]
63
80
  when Date
64
- [:write_date_time, value, date_format]
81
+ [:write_date_time, [value, date_format], value.to_s.size]
65
82
  else
66
- [:write_string, value.to_s]
83
+ [:write_string, [value.to_s], value.to_s.size]
67
84
  end
68
85
  end
69
86
 
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.23.5
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bernard Lambeau
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-09-29 00:00:00.000000000 Z
11
+ date: 2026-09-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: predicate
@@ -233,6 +233,7 @@ files:
233
233
  - lib/bmg/sql/ext/predicate/tautology.rb
234
234
  - lib/bmg/sql/grammar.rb
235
235
  - lib/bmg/sql/grammar.sexp.yml
236
+ - lib/bmg/sql/nodes/case_when.rb
236
237
  - lib/bmg/sql/nodes/column_name.rb
237
238
  - lib/bmg/sql/nodes/cross_join.rb
238
239
  - lib/bmg/sql/nodes/except.rb
@@ -347,7 +348,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
347
348
  - !ruby/object:Gem::Version
348
349
  version: '0'
349
350
  requirements: []
350
- rubygems_version: 3.3.27
351
+ rubygems_version: 3.4.19
351
352
  signing_key:
352
353
  specification_version: 4
353
354
  summary: Bmg is Alf's successor.