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 +4 -4
- data/lib/bmg/database/data_folder.rb +14 -6
- data/lib/bmg/database/xlsx.rb +5 -2
- data/lib/bmg/database.rb +5 -0
- data/lib/bmg/reader/xlsx.rb +12 -2
- data/lib/bmg/sequel/translator.rb +9 -1
- data/lib/bmg/sql/grammar.rb +1 -0
- data/lib/bmg/sql/grammar.sexp.yml +3 -0
- data/lib/bmg/sql/nodes/case_when.rb +33 -0
- data/lib/bmg/sql/processor/transform.rb +27 -1
- data/lib/bmg/sql/relation.rb +2 -1
- data/lib/bmg/support/output_preferences.rb +9 -2
- data/lib/bmg/version.rb +2 -2
- data/lib/bmg/writer/xlsx.rb +26 -9
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1cf55d89350451dc43a50e85413570b8525fdb1af1467cf33c854d65cd885324
|
|
4
|
+
data.tar.gz: a0a665602f3e4deceacfa6bc1bf301dd91e9344d96d93f3cc1df462e3b364fb1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
(
|
|
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
|
|
60
|
-
return target if target
|
|
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
|
data/lib/bmg/database/xlsx.rb
CHANGED
|
@@ -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 =
|
|
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,
|
|
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
|
data/lib/bmg/reader/xlsx.rb
CHANGED
|
@@ -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
|
data/lib/bmg/sql/grammar.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/bmg/sql/relation.rb
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
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
data/lib/bmg/writer/xlsx.rb
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
end
|
|
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,
|
|
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.
|
|
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:
|
|
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.
|
|
351
|
+
rubygems_version: 3.4.19
|
|
351
352
|
signing_key:
|
|
352
353
|
specification_version: 4
|
|
353
354
|
summary: Bmg is Alf's successor.
|