db_fuel 1.1.0 → 1.2.0.pre.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -1
- data/.tool-versions +1 -0
- data/README.md +93 -5
- data/db_fuel.gemspec +3 -3
- data/lib/db_fuel/library.rb +4 -0
- data/lib/db_fuel/library/active_record/base.rb +6 -36
- data/lib/db_fuel/library/active_record/find_or_insert.rb +8 -31
- data/lib/db_fuel/library/active_record/insert.rb +8 -39
- data/lib/db_fuel/library/active_record/update.rb +23 -27
- data/lib/db_fuel/library/active_record/update_all.rb +96 -0
- data/lib/db_fuel/library/active_record/upsert.rb +210 -0
- data/lib/db_fuel/modeling.rb +1 -0
- data/lib/db_fuel/modeling/attribute_renderer_set.rb +83 -0
- data/lib/db_fuel/version.rb +1 -1
- metadata +12 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b4f5691edb9519be2c43b787f9eeee14d2e2e83fcf9071e21dbef2442449d3fa
|
4
|
+
data.tar.gz: a95cc6d63d3eba9256c022891328352662b64cf8e0391a92962609f6d3a8062c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 36c2477f3010bd60a7ef7a49f60f7a5302d2cec2ad8be034d83e18923eed07820578a87993a9ebc07e1121c8853d45e5060eaca4d4895222afb3b69b8a8b61f9
|
7
|
+
data.tar.gz: 54f65f7f0d56271bacb6b67512f10135b4d22ddb64dd0a7d1fc864e6d2cd29a0d7e4579056f518cf517b5b3353715bafc489743d596cc8bd8e3e9a0812d6755b
|
data/.rubocop.yml
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
AllCops:
|
2
2
|
TargetRubyVersion: 2.5
|
3
3
|
NewCops: enable
|
4
|
+
SuggestExtensions: false
|
4
5
|
|
5
6
|
Layout/LineLength:
|
6
7
|
Max: 100
|
@@ -8,7 +9,7 @@ Layout/LineLength:
|
|
8
9
|
- db_fuel.gemspec
|
9
10
|
|
10
11
|
Metrics/BlockLength:
|
11
|
-
|
12
|
+
IgnoredMethods:
|
12
13
|
- let
|
13
14
|
- it
|
14
15
|
- describe
|
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 2.6.6
|
data/README.md
CHANGED
@@ -24,9 +24,11 @@ Refer to the [Burner](https://github.com/bluemarblepayroll/burner) library for m
|
|
24
24
|
|
25
25
|
### ActiveRecord Jobs
|
26
26
|
|
27
|
-
* **db_fuel/active_record/find_or_insert** [table_name, attributes, debug, primary_key, register, separator, timestamps, unique_attributes]: An extension of the `db_fuel/active_record/insert` job that adds an existence check before SQL insertion. The `unique_attributes` will be converted to WHERE clauses for performing the existence check.
|
28
|
-
* **db_fuel/active_record/insert** [table_name, attributes, debug, primary_key, register, separator, timestamps]: This job can take the objects in a register and insert them into a database table. If primary_key is specified then its key will be set to the primary key. Note that composite primary keys are not supported. Attributes defines which object properties to convert to SQL. Refer to the class and constructor specification for more detail.
|
29
|
-
* **db_fuel/active_record/
|
27
|
+
* **db_fuel/active_record/find_or_insert** [name, table_name, attributes, debug, primary_key, register, separator, timestamps, unique_attributes]: An extension of the `db_fuel/active_record/insert` job that adds an existence check before SQL insertion. The `unique_attributes` will be converted to WHERE clauses for performing the existence check.
|
28
|
+
* **db_fuel/active_record/insert** [name, table_name, attributes, debug, primary_key, register, separator, timestamps]: This job can take the objects in a register and insert them into a database table. If primary_key is specified then its key will be set to the primary key. Note that composite primary keys are not supported. Attributes defines which object properties to convert to SQL. Refer to the class and constructor specification for more detail.
|
29
|
+
* **db_fuel/active_record/update_all** [name, table_name, attributes, debug, register, separator, timestamps, unique_attributes]: This job can take the objects in a register and updates them within a database table. Attributes defines which object properties to convert to SQL SET clauses while unique_attributes translate to WHERE clauses. One or more records may be updated at a time. Refer to the class and constructor specification for more detail.
|
30
|
+
* **db_fuel/active_record/update** [name, table_name, attributes, debug, register, primary_key, separator, timestamps, unique_attributes]: This job can take the unique objects in a register and updates them within a database table. Attributes defines which object properties to convert to SQL SET clauses while unique_attributes translate to WHERE clauses to find the records to update. The primary_key is used to update the unique record. Only one record will be updated per statement. Refer to the class and constructor specification for more detail.
|
31
|
+
* **db_fuel/active_record/upsert** [name, table_name, attributes, debug, primary_key, register, separator, timestamps, unique_attributes]: This job can take the objects in a register and either inserts or updates them within a database table. Attributes defines which object properties to convert to SQL SET clauses while each key in unique_attributes become a WHERE clause in order to check for the existence of a specific record. The updated record will use the primary_key specified to perform the UPDATE operation. Note that composite primary keys are not supported. Refer to the class and constructor specification for more detail.
|
30
32
|
|
31
33
|
### Dbee Jobs
|
32
34
|
|
@@ -242,7 +244,7 @@ Now only records where the chart_number does not match an existing record will b
|
|
242
244
|
|
243
245
|
#### Updating Records
|
244
246
|
|
245
|
-
Let's say we now want to update
|
247
|
+
Let's say we now want to update these unique records' last names:
|
246
248
|
|
247
249
|
````ruby
|
248
250
|
pipeline = {
|
@@ -264,6 +266,91 @@ pipeline = {
|
|
264
266
|
{ key: :last_name }
|
265
267
|
],
|
266
268
|
table_name: 'patients',
|
269
|
+
primary_key: {
|
270
|
+
key: :id
|
271
|
+
},
|
272
|
+
unique_attributes: [
|
273
|
+
{ key: :chart_number }
|
274
|
+
]
|
275
|
+
}
|
276
|
+
]
|
277
|
+
}
|
278
|
+
|
279
|
+
payload = Burner::Payload.new
|
280
|
+
|
281
|
+
Burner::Pipeline.make(pipeline).execute(payload: payload)
|
282
|
+
````
|
283
|
+
|
284
|
+
Each database record should have been updated with their new respective last names based on the primary key specified.
|
285
|
+
|
286
|
+
#### Updating All Records
|
287
|
+
|
288
|
+
Let's say we want to update those records' midddle names:
|
289
|
+
|
290
|
+
````ruby
|
291
|
+
pipeline = {
|
292
|
+
jobs: [
|
293
|
+
{
|
294
|
+
name: :load_patients,
|
295
|
+
type: 'b/value/static',
|
296
|
+
register: :patients,
|
297
|
+
value: [
|
298
|
+
{ chart_number: 'B0001', middle_name: 'Rabbit' },
|
299
|
+
{ chart_number: 'C0001', middle_name: 'Elf' }
|
300
|
+
]
|
301
|
+
},
|
302
|
+
{
|
303
|
+
name: 'update_patients',
|
304
|
+
type: 'db_fuel/active_record/update_all',
|
305
|
+
register: :patients,
|
306
|
+
attributes: [
|
307
|
+
{ key: :last_name }
|
308
|
+
],
|
309
|
+
table_name: 'patients',
|
310
|
+
unique_attributes: [
|
311
|
+
{ key: :chart_number }
|
312
|
+
]
|
313
|
+
}
|
314
|
+
]
|
315
|
+
}
|
316
|
+
|
317
|
+
payload = Burner::Payload.new
|
318
|
+
|
319
|
+
Burner::Pipeline.make(pipeline).execute(payload: payload)
|
320
|
+
````
|
321
|
+
|
322
|
+
Each database record should have been updated with their new respective middle names based on chart_number.
|
323
|
+
|
324
|
+
#### Upserting Records
|
325
|
+
|
326
|
+
Let's say we don't know if these chart_number values already exist or not.
|
327
|
+
So we want db_fuel to either insert a record if the chart_number doesn't exist or update the record if the chart_number already exists.
|
328
|
+
|
329
|
+
````ruby
|
330
|
+
pipeline = {
|
331
|
+
jobs: [
|
332
|
+
{
|
333
|
+
name: :load_patients,
|
334
|
+
type: 'b/value/static',
|
335
|
+
register: :patients,
|
336
|
+
value: [
|
337
|
+
{ chart_number: 'B0002', first_name: 'Babs', last_name: 'Bunny' },
|
338
|
+
{ chart_number: 'B0003', first_name: 'Daffy', last_name: 'Duck' }
|
339
|
+
]
|
340
|
+
},
|
341
|
+
{
|
342
|
+
name: 'update_patients',
|
343
|
+
type: 'db_fuel/active_record/upsert',
|
344
|
+
register: :patients,
|
345
|
+
attributes: [
|
346
|
+
{ key: :chart_number },
|
347
|
+
{ key: :first_name },
|
348
|
+
{ key: :last_name }
|
349
|
+
],
|
350
|
+
table_name: 'patients',
|
351
|
+
primary_key: {
|
352
|
+
key: :id
|
353
|
+
},
|
267
354
|
unique_attributes: [
|
268
355
|
{ key: :chart_number }
|
269
356
|
]
|
@@ -276,7 +363,8 @@ payload = Burner::Payload.new
|
|
276
363
|
Burner::Pipeline.make(pipeline).execute(payload: payload)
|
277
364
|
````
|
278
365
|
|
279
|
-
Each database record should have been updated with their
|
366
|
+
Each database record should have been either inserted or updated with their corresponding values. In this case Babs' last name
|
367
|
+
was switched back to Bunny and a new record was created for Daffy Duck.
|
280
368
|
|
281
369
|
Notes:
|
282
370
|
|
data/db_fuel.gemspec
CHANGED
@@ -11,8 +11,8 @@ Gem::Specification.new do |s|
|
|
11
11
|
This library adds database-centric jobs to the Burner library. Burner does not ship with database jobs out of the box.
|
12
12
|
DESCRIPTION
|
13
13
|
|
14
|
-
s.authors = ['Matthew Ruggio']
|
15
|
-
s.email = ['mruggio@bluemarblepayroll.com']
|
14
|
+
s.authors = ['Matthew Ruggio', 'John Bosko']
|
15
|
+
s.email = ['mruggio@bluemarblepayroll.com', 'jbosko@bluemarblepayroll.com']
|
16
16
|
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
17
|
s.bindir = 'exe'
|
18
18
|
s.executables = %w[]
|
@@ -51,7 +51,7 @@ Gem::Specification.new do |s|
|
|
51
51
|
s.add_development_dependency('pry', '~>0')
|
52
52
|
s.add_development_dependency('rake', '~> 13')
|
53
53
|
s.add_development_dependency('rspec', '~> 3.8')
|
54
|
-
s.add_development_dependency('rubocop', '~>
|
54
|
+
s.add_development_dependency('rubocop', '~>1.7.0')
|
55
55
|
s.add_development_dependency('simplecov', '~>0.18.5')
|
56
56
|
s.add_development_dependency('simplecov-console', '~>0.7.0')
|
57
57
|
s.add_development_dependency('sqlite3', '~>1')
|
data/lib/db_fuel/library.rb
CHANGED
@@ -10,6 +10,8 @@
|
|
10
10
|
require_relative 'library/active_record/find_or_insert'
|
11
11
|
require_relative 'library/active_record/insert'
|
12
12
|
require_relative 'library/active_record/update'
|
13
|
+
require_relative 'library/active_record/update_all'
|
14
|
+
require_relative 'library/active_record/upsert'
|
13
15
|
|
14
16
|
require_relative 'library/dbee/query'
|
15
17
|
require_relative 'library/dbee/range'
|
@@ -20,6 +22,8 @@ module Burner
|
|
20
22
|
register 'db_fuel/active_record/find_or_insert', DbFuel::Library::ActiveRecord::FindOrInsert
|
21
23
|
register 'db_fuel/active_record/insert', DbFuel::Library::ActiveRecord::Insert
|
22
24
|
register 'db_fuel/active_record/update', DbFuel::Library::ActiveRecord::Update
|
25
|
+
register 'db_fuel/active_record/update_all', DbFuel::Library::ActiveRecord::UpdateAll
|
26
|
+
register 'db_fuel/active_record/upsert', DbFuel::Library::ActiveRecord::Upsert
|
23
27
|
|
24
28
|
register 'db_fuel/dbee/query', DbFuel::Library::Dbee::Query
|
25
29
|
register 'db_fuel/dbee/range', DbFuel::Library::Dbee::Range
|
@@ -22,7 +22,8 @@ module DbFuel
|
|
22
22
|
attr_reader :attribute_renderers,
|
23
23
|
:db_provider,
|
24
24
|
:debug,
|
25
|
-
:resolver
|
25
|
+
:resolver,
|
26
|
+
:attribute_renderers_set
|
26
27
|
|
27
28
|
def initialize(
|
28
29
|
name:,
|
@@ -34,46 +35,15 @@ module DbFuel
|
|
34
35
|
)
|
35
36
|
super(name: name, register: register)
|
36
37
|
|
37
|
-
|
38
|
-
@
|
39
|
-
|
40
|
-
@db_provider
|
38
|
+
@resolver = Objectable.resolver(separator: separator)
|
39
|
+
@attribute_renderers_set = Modeling::AttributeRendererSet.new(attributes: attributes,
|
40
|
+
resolver: resolver)
|
41
|
+
@db_provider = DbProvider.new(table_name)
|
41
42
|
@debug = debug || false
|
42
43
|
end
|
43
44
|
|
44
45
|
private
|
45
46
|
|
46
|
-
def make_attribute_renderers(attributes)
|
47
|
-
Burner::Modeling::Attribute
|
48
|
-
.array(attributes)
|
49
|
-
.map { |a| Burner::Modeling::AttributeRenderer.new(a, resolver) }
|
50
|
-
end
|
51
|
-
|
52
|
-
def transform(attribute_renderers, row, time)
|
53
|
-
attribute_renderers.each_with_object({}) do |attribute_renderer, memo|
|
54
|
-
value = attribute_renderer.transform(row, time)
|
55
|
-
|
56
|
-
resolver.set(memo, attribute_renderer.key, value)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
def created_at_timestamp_attribute
|
61
|
-
timestamp_attribute(CREATED_AT)
|
62
|
-
end
|
63
|
-
|
64
|
-
def updated_at_timestamp_attribute
|
65
|
-
timestamp_attribute(UPDATED_AT)
|
66
|
-
end
|
67
|
-
|
68
|
-
def timestamp_attribute(key)
|
69
|
-
Burner::Modeling::Attribute.make(
|
70
|
-
key: key,
|
71
|
-
transformers: [
|
72
|
-
{ type: NOW_TYPE }
|
73
|
-
]
|
74
|
-
)
|
75
|
-
end
|
76
|
-
|
77
47
|
def debug_detail(output, message)
|
78
48
|
return unless debug
|
79
49
|
|
@@ -7,7 +7,7 @@
|
|
7
7
|
# LICENSE file in the root directory of this source tree.
|
8
8
|
#
|
9
9
|
|
10
|
-
require_relative '
|
10
|
+
require_relative 'upsert'
|
11
11
|
|
12
12
|
module DbFuel
|
13
13
|
module Library
|
@@ -19,8 +19,8 @@ module DbFuel
|
|
19
19
|
#
|
20
20
|
# Expected Payload[register] input: array of objects
|
21
21
|
# Payload[register] output: array of objects.
|
22
|
-
class FindOrInsert <
|
23
|
-
attr_reader :unique_attribute_renderers
|
22
|
+
class FindOrInsert < Upsert
|
23
|
+
# attr_reader :unique_attribute_renderers
|
24
24
|
|
25
25
|
# Arguments:
|
26
26
|
# name [required]: name of the job within the Burner::Pipeline.
|
@@ -61,6 +61,7 @@ module DbFuel
|
|
61
61
|
timestamps: true,
|
62
62
|
unique_attributes: []
|
63
63
|
)
|
64
|
+
|
64
65
|
super(
|
65
66
|
name: name,
|
66
67
|
table_name: table_name,
|
@@ -69,10 +70,9 @@ module DbFuel
|
|
69
70
|
primary_key: primary_key,
|
70
71
|
register: register,
|
71
72
|
separator: separator,
|
72
|
-
timestamps: timestamps
|
73
|
+
timestamps: timestamps,
|
74
|
+
unique_attributes: unique_attributes
|
73
75
|
)
|
74
|
-
|
75
|
-
@unique_attribute_renderers = make_attribute_renderers(unique_attributes)
|
76
76
|
end
|
77
77
|
|
78
78
|
def perform(output, payload)
|
@@ -82,14 +82,14 @@ module DbFuel
|
|
82
82
|
payload[register] = array(payload[register])
|
83
83
|
|
84
84
|
payload[register].each do |row|
|
85
|
-
exists =
|
85
|
+
exists = find_record(output, row, payload.time)
|
86
86
|
|
87
87
|
if exists
|
88
88
|
total_existed += 1
|
89
89
|
next
|
90
90
|
end
|
91
91
|
|
92
|
-
|
92
|
+
insert_record(output, row, payload.time)
|
93
93
|
|
94
94
|
total_inserted += 1
|
95
95
|
end
|
@@ -97,29 +97,6 @@ module DbFuel
|
|
97
97
|
output.detail("Total Existed: #{total_existed}")
|
98
98
|
output.detail("Total Inserted: #{total_inserted}")
|
99
99
|
end
|
100
|
-
|
101
|
-
private
|
102
|
-
|
103
|
-
def existence_check_and_mutate(output, row, time)
|
104
|
-
unique_row = transform(unique_attribute_renderers, row, time)
|
105
|
-
|
106
|
-
first_sql = db_provider.first_sql(unique_row)
|
107
|
-
debug_detail(output, "Find Statement: #{first_sql}")
|
108
|
-
|
109
|
-
first_record = db_provider.first(unique_row)
|
110
|
-
|
111
|
-
return false unless first_record
|
112
|
-
|
113
|
-
if primary_key
|
114
|
-
id = resolver.get(first_record, primary_key.column)
|
115
|
-
|
116
|
-
resolver.set(row, primary_key.key, id)
|
117
|
-
end
|
118
|
-
|
119
|
-
debug_detail(output, "Record Exists: #{first_record}")
|
120
|
-
|
121
|
-
true
|
122
|
-
end
|
123
100
|
end
|
124
101
|
end
|
125
102
|
end
|
@@ -7,7 +7,7 @@
|
|
7
7
|
# LICENSE file in the root directory of this source tree.
|
8
8
|
#
|
9
9
|
|
10
|
-
require_relative '
|
10
|
+
require_relative 'upsert'
|
11
11
|
|
12
12
|
module DbFuel
|
13
13
|
module Library
|
@@ -16,8 +16,8 @@ module DbFuel
|
|
16
16
|
#
|
17
17
|
# Expected Payload[register] input: array of objects
|
18
18
|
# Payload[register] output: array of objects.
|
19
|
-
class Insert <
|
20
|
-
attr_reader :primary_key
|
19
|
+
class Insert < Upsert
|
20
|
+
# attr_reader :primary_key
|
21
21
|
|
22
22
|
# Arguments:
|
23
23
|
# name [required]: name of the job within the Burner::Pipeline.
|
@@ -54,56 +54,25 @@ module DbFuel
|
|
54
54
|
separator: '',
|
55
55
|
timestamps: true
|
56
56
|
)
|
57
|
-
explicit_attributes = Burner::Modeling::Attribute.array(attributes)
|
58
57
|
|
59
|
-
attributes =
|
58
|
+
attributes = Burner::Modeling::Attribute.array(attributes)
|
60
59
|
|
61
60
|
super(
|
62
61
|
name: name,
|
63
62
|
table_name: table_name,
|
64
63
|
attributes: attributes,
|
65
64
|
debug: debug,
|
65
|
+
primary_key: primary_key,
|
66
66
|
register: register,
|
67
|
-
separator: separator
|
67
|
+
separator: separator,
|
68
|
+
timestamps: timestamps
|
68
69
|
)
|
69
|
-
|
70
|
-
@primary_key = Modeling::KeyedColumn.make(primary_key, nullable: true)
|
71
70
|
end
|
72
71
|
|
73
72
|
def perform(output, payload)
|
74
73
|
payload[register] = array(payload[register])
|
75
74
|
|
76
|
-
payload[register].each { |row|
|
77
|
-
end
|
78
|
-
|
79
|
-
private
|
80
|
-
|
81
|
-
def insert(output, row, time)
|
82
|
-
transformed_row = transform(attribute_renderers, row, time)
|
83
|
-
|
84
|
-
output_sql(output, transformed_row)
|
85
|
-
insert_and_mutate(output, transformed_row, row)
|
86
|
-
end
|
87
|
-
|
88
|
-
def output_sql(output, row)
|
89
|
-
sql = db_provider.insert_sql(row)
|
90
|
-
|
91
|
-
debug_detail(output, "Insert Statement: #{sql}")
|
92
|
-
end
|
93
|
-
|
94
|
-
def insert_and_mutate(output, row_to_insert, row_to_return)
|
95
|
-
id = db_provider.insert(row_to_insert)
|
96
|
-
|
97
|
-
resolver.set(row_to_return, primary_key.key, id) if primary_key
|
98
|
-
|
99
|
-
debug_detail(output, "Insert Return: #{row_to_return}")
|
100
|
-
end
|
101
|
-
|
102
|
-
def timestamp_attributes
|
103
|
-
[
|
104
|
-
created_at_timestamp_attribute,
|
105
|
-
updated_at_timestamp_attribute
|
106
|
-
]
|
75
|
+
payload[register].each { |row| insert_record(output, row, payload.time) }
|
107
76
|
end
|
108
77
|
end
|
109
78
|
end
|
@@ -7,20 +7,20 @@
|
|
7
7
|
# LICENSE file in the root directory of this source tree.
|
8
8
|
#
|
9
9
|
|
10
|
-
require_relative '
|
10
|
+
require_relative 'upsert'
|
11
11
|
|
12
12
|
module DbFuel
|
13
13
|
module Library
|
14
14
|
module ActiveRecord
|
15
|
-
# This job can take the objects in a register and updates them within database table.
|
15
|
+
# This job can take the unique objects in a register and updates them within database table.
|
16
16
|
# The attributes translate to SQL SET clauses and the unique_keys translate to
|
17
|
-
# WHERE clauses.
|
17
|
+
# WHERE clauses to find the records to update.
|
18
|
+
# The primary_key is used to update the unique record.
|
19
|
+
# Only one record will be updated per statement.
|
18
20
|
#
|
19
21
|
# Expected Payload[register] input: array of objects
|
20
22
|
# Payload[register] output: array of objects.
|
21
|
-
class Update <
|
22
|
-
attr_reader :unique_attribute_renderers
|
23
|
-
|
23
|
+
class Update < Upsert
|
24
24
|
# Arguments:
|
25
25
|
# name [required]: name of the job within the Burner::Pipeline.
|
26
26
|
#
|
@@ -35,6 +35,11 @@ module DbFuel
|
|
35
35
|
# returned objects will be printed in the output. Only use this option while
|
36
36
|
# debugging issues as it will fill up the output with (potentially too much) data.
|
37
37
|
#
|
38
|
+
# primary_key [required]: Primary key column for the corresponding table.
|
39
|
+
# Used as the WHERE clause for the UPDATE statement.
|
40
|
+
# Only one record will be updated at a time
|
41
|
+
# using the primary key specified.
|
42
|
+
#
|
38
43
|
# separator: Just like other jobs with a 'separator' option, if the objects require
|
39
44
|
# key-path notation or nested object support, you can set the separator
|
40
45
|
# to something non-blank (like a period for notation in the
|
@@ -43,33 +48,35 @@ module DbFuel
|
|
43
48
|
# timestamps: If timestamps is true (default behavior) then the updated_at column will
|
44
49
|
# automatically have its value set to the current UTC timestamp.
|
45
50
|
#
|
46
|
-
# unique_attributes: Each key will become a WHERE clause in order to only
|
47
|
-
# records.
|
51
|
+
# unique_attributes: Each key will become a WHERE clause in order to only find specific
|
52
|
+
# records. The UPDATE statement's WHERE
|
53
|
+
# clause will use the primary key specified.
|
48
54
|
def initialize(
|
49
55
|
name:,
|
50
56
|
table_name:,
|
51
57
|
attributes: [],
|
52
58
|
debug: false,
|
59
|
+
primary_key: nil,
|
53
60
|
register: Burner::DEFAULT_REGISTER,
|
54
61
|
separator: '',
|
55
62
|
timestamps: true,
|
56
63
|
unique_attributes: []
|
57
64
|
)
|
58
|
-
explicit_attributes = Burner::Modeling::Attribute.array(attributes)
|
59
65
|
|
60
|
-
attributes =
|
66
|
+
attributes = Burner::Modeling::Attribute.array(attributes)
|
61
67
|
|
62
68
|
super(
|
63
69
|
name: name,
|
64
70
|
table_name: table_name,
|
65
71
|
attributes: attributes,
|
66
72
|
debug: debug,
|
73
|
+
primary_key: primary_key,
|
67
74
|
register: register,
|
68
|
-
separator: separator
|
75
|
+
separator: separator,
|
76
|
+
timestamps: timestamps,
|
77
|
+
unique_attributes: unique_attributes
|
69
78
|
)
|
70
79
|
|
71
|
-
@unique_attribute_renderers = make_attribute_renderers(unique_attributes)
|
72
|
-
|
73
80
|
freeze
|
74
81
|
end
|
75
82
|
|
@@ -79,14 +86,11 @@ module DbFuel
|
|
79
86
|
payload[register] = array(payload[register])
|
80
87
|
|
81
88
|
payload[register].each do |row|
|
82
|
-
|
83
|
-
where_object = transform(unique_attribute_renderers, row, payload.time)
|
84
|
-
|
85
|
-
sql = db_provider.update_sql(set_object, where_object)
|
89
|
+
rows_affected = 0
|
86
90
|
|
87
|
-
|
91
|
+
first_record = update_record(output, row, payload.time)
|
88
92
|
|
89
|
-
rows_affected =
|
93
|
+
rows_affected = 1 if first_record
|
90
94
|
|
91
95
|
debug_detail(output, "Individual Rows Affected: #{rows_affected}")
|
92
96
|
|
@@ -95,14 +99,6 @@ module DbFuel
|
|
95
99
|
|
96
100
|
output.detail("Total Rows Affected: #{total_rows_affected}")
|
97
101
|
end
|
98
|
-
|
99
|
-
private
|
100
|
-
|
101
|
-
def timestamp_attributes
|
102
|
-
[
|
103
|
-
updated_at_timestamp_attribute
|
104
|
-
]
|
105
|
-
end
|
106
102
|
end
|
107
103
|
end
|
108
104
|
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2020-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
require_relative 'upsert'
|
11
|
+
|
12
|
+
module DbFuel
|
13
|
+
module Library
|
14
|
+
module ActiveRecord
|
15
|
+
# This job can take the objects in a register and updates them within database table.
|
16
|
+
# The attributes translate to SQL SET clauses
|
17
|
+
# and the unique_keys translate to WHERE clauses.
|
18
|
+
# One or more records may be updated at a time.
|
19
|
+
#
|
20
|
+
# Expected Payload[register] input: array of objects
|
21
|
+
# Payload[register] output: array of objects.
|
22
|
+
class UpdateAll < Upsert
|
23
|
+
# Arguments:
|
24
|
+
# name [required]: name of the job within the Burner::Pipeline.
|
25
|
+
#
|
26
|
+
# table_name [required]: name of the table to use for the INSERT statements.
|
27
|
+
#
|
28
|
+
# attributes: Used to specify which object properties to put into the
|
29
|
+
# SQL statement and also allows for one last custom transformation
|
30
|
+
# pipeline, in case the data calls for SQL-specific transformers
|
31
|
+
# before mutation.
|
32
|
+
#
|
33
|
+
# debug: If debug is set to true (defaults to false) then the SQL statements and
|
34
|
+
# returned objects will be printed in the output. Only use this option while
|
35
|
+
# debugging issues as it will fill up the output with (potentially too much) data.
|
36
|
+
#
|
37
|
+
# separator: Just like other jobs with a 'separator' option, if the objects require
|
38
|
+
# key-path notation or nested object support, you can set the separator
|
39
|
+
# to something non-blank (like a period for notation in the
|
40
|
+
# form of: name.first).
|
41
|
+
#
|
42
|
+
# timestamps: If timestamps is true (default behavior) then the updated_at column will
|
43
|
+
# automatically have its value set to the current UTC timestamp.
|
44
|
+
#
|
45
|
+
# unique_attributes: Each key will become a WHERE clause in order to only update specific
|
46
|
+
# records.
|
47
|
+
def initialize(
|
48
|
+
name:,
|
49
|
+
table_name:,
|
50
|
+
attributes: [],
|
51
|
+
debug: false,
|
52
|
+
register: Burner::DEFAULT_REGISTER,
|
53
|
+
separator: '',
|
54
|
+
timestamps: true,
|
55
|
+
unique_attributes: []
|
56
|
+
)
|
57
|
+
|
58
|
+
attributes = Burner::Modeling::Attribute.array(attributes)
|
59
|
+
|
60
|
+
super(
|
61
|
+
name: name,
|
62
|
+
table_name: table_name,
|
63
|
+
attributes: attributes,
|
64
|
+
debug: debug,
|
65
|
+
primary_key: nil,
|
66
|
+
register: register,
|
67
|
+
separator: separator,
|
68
|
+
timestamps: timestamps,
|
69
|
+
unique_attributes: unique_attributes
|
70
|
+
)
|
71
|
+
|
72
|
+
freeze
|
73
|
+
end
|
74
|
+
|
75
|
+
def perform(output, payload)
|
76
|
+
total_rows_affected = 0
|
77
|
+
|
78
|
+
payload[register] = array(payload[register])
|
79
|
+
|
80
|
+
payload[register].each do |row|
|
81
|
+
where_object = attribute_renderers_set
|
82
|
+
.transform(unique_attribute_renderers, row, payload.time)
|
83
|
+
|
84
|
+
rows_affected = update(output, row, payload.time, where_object)
|
85
|
+
|
86
|
+
debug_detail(output, "Individual Rows Affected: #{rows_affected}")
|
87
|
+
|
88
|
+
total_rows_affected += rows_affected
|
89
|
+
end
|
90
|
+
|
91
|
+
output.detail("Total Rows Affected: #{total_rows_affected}")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2020-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
require_relative 'base'
|
11
|
+
|
12
|
+
module DbFuel
|
13
|
+
module Library
|
14
|
+
module ActiveRecord
|
15
|
+
# This job will insert or update records.
|
16
|
+
# It will use the unique_keys to first run a query to see if it exists.
|
17
|
+
# Each unique_key becomes a WHERE clause. If a record is found it will then
|
18
|
+
# update the found record using the primary key specified.
|
19
|
+
# If a record is updated or created the record's id will be set to the primary_key.
|
20
|
+
#
|
21
|
+
# Expected Payload[register] input: array of objects
|
22
|
+
# Payload[register] output: array of objects.
|
23
|
+
class Upsert < Base
|
24
|
+
attr_reader :primary_key, :timestamps, :unique_attribute_renderers
|
25
|
+
|
26
|
+
# Arguments:
|
27
|
+
# name [required]: name of the job within the Burner::Pipeline.
|
28
|
+
#
|
29
|
+
# table_name [required]: name of the table to use for the INSERT OR UPDATE statements.
|
30
|
+
#
|
31
|
+
# attributes: Used to specify which object properties to put into the
|
32
|
+
# SQL statement and also allows for one last custom transformation
|
33
|
+
# pipeline, in case the data calls for SQL-specific transformers
|
34
|
+
# before mutation.
|
35
|
+
#
|
36
|
+
# debug: If debug is set to true (defaults to false) then the SQL statements and
|
37
|
+
# returned objects will be printed in the output. Only use this option while
|
38
|
+
# debugging issues as it will fill
|
39
|
+
# up the output with (potentially too much) data.
|
40
|
+
#
|
41
|
+
# primary_key [required]: Used to set the object's property to the returned primary key
|
42
|
+
# from the INSERT statement or used as the
|
43
|
+
# WHERE clause for the UPDATE statement.
|
44
|
+
#
|
45
|
+
# separator: Just like other jobs with a 'separator' option, if the objects require
|
46
|
+
# key-path notation or nested object support, you can set the separator
|
47
|
+
# to something non-blank (like a period for notation in the
|
48
|
+
# form of: name.first).
|
49
|
+
#
|
50
|
+
# timestamps: If timestamps is true (default behavior) then the updated_at column will
|
51
|
+
# automatically have its value set
|
52
|
+
# to the current UTC timestamp if a record was updated.
|
53
|
+
# If a record was created the
|
54
|
+
# created_at and updated_at columns will be set.
|
55
|
+
#
|
56
|
+
# unique_attributes: Each key will become a WHERE clause in
|
57
|
+
# order to check for the existence of a specific record.
|
58
|
+
def initialize(
|
59
|
+
name:,
|
60
|
+
table_name:,
|
61
|
+
primary_key:,
|
62
|
+
attributes: [],
|
63
|
+
debug: false,
|
64
|
+
register: Burner::DEFAULT_REGISTER,
|
65
|
+
separator: '',
|
66
|
+
timestamps: true,
|
67
|
+
unique_attributes: []
|
68
|
+
)
|
69
|
+
super(
|
70
|
+
name: name,
|
71
|
+
table_name: table_name,
|
72
|
+
attributes: attributes,
|
73
|
+
debug: debug,
|
74
|
+
register: register,
|
75
|
+
separator: separator
|
76
|
+
)
|
77
|
+
|
78
|
+
@primary_key = Modeling::KeyedColumn.make(primary_key, nullable: true)
|
79
|
+
|
80
|
+
@unique_attribute_renderers = attribute_renderers_set
|
81
|
+
.make_attribute_renderers(unique_attributes)
|
82
|
+
|
83
|
+
@timestamps = timestamps
|
84
|
+
|
85
|
+
freeze
|
86
|
+
end
|
87
|
+
|
88
|
+
def perform(output, payload)
|
89
|
+
raise ArgumentError, 'primary_key is required' unless primary_key
|
90
|
+
|
91
|
+
total_inserted = 0
|
92
|
+
total_updated = 0
|
93
|
+
|
94
|
+
payload[register] = array(payload[register])
|
95
|
+
|
96
|
+
payload[register].each do |row|
|
97
|
+
record_updated = insert_or_update(output, row, payload.time)
|
98
|
+
|
99
|
+
if record_updated
|
100
|
+
total_updated += 1
|
101
|
+
else
|
102
|
+
total_inserted += 1
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
output.detail("Total Updated: #{total_updated}")
|
107
|
+
output.detail("Total Inserted: #{total_inserted}")
|
108
|
+
end
|
109
|
+
|
110
|
+
protected
|
111
|
+
|
112
|
+
def find_record(output, row, time)
|
113
|
+
unique_row = attribute_renderers_set.transform(unique_attribute_renderers, row, time)
|
114
|
+
|
115
|
+
first_sql = db_provider.first_sql(unique_row)
|
116
|
+
|
117
|
+
debug_detail(output, "Find Statement: #{first_sql}")
|
118
|
+
|
119
|
+
first_record = db_provider.first(unique_row)
|
120
|
+
|
121
|
+
id = resolver.get(first_record, primary_key.column)
|
122
|
+
|
123
|
+
resolver.set(row, primary_key.key, id)
|
124
|
+
|
125
|
+
debug_detail(output, "Record Exists: #{first_record}") if first_record
|
126
|
+
|
127
|
+
first_record
|
128
|
+
end
|
129
|
+
|
130
|
+
def insert_record(output, row, time)
|
131
|
+
dynamic_attrs = if timestamps
|
132
|
+
# doing an INSERT and timestamps should be set
|
133
|
+
# set the created_at and updated_at fields
|
134
|
+
attribute_renderers_set.timestamp_created_attribute_renderers
|
135
|
+
else
|
136
|
+
attribute_renderer_set.attribute_renderers
|
137
|
+
end
|
138
|
+
|
139
|
+
set_object = attribute_renderers_set.transform(dynamic_attrs, row, time)
|
140
|
+
|
141
|
+
insert_sql = db_provider.insert_sql(set_object)
|
142
|
+
|
143
|
+
debug_detail(output, "Insert Statement: #{insert_sql}")
|
144
|
+
|
145
|
+
id = db_provider.insert(set_object)
|
146
|
+
|
147
|
+
# add the primary key name and value to row if primary_key was specified
|
148
|
+
resolver.set(row, primary_key.key, id) if primary_key
|
149
|
+
|
150
|
+
debug_detail(output, "Insert Return: #{row}")
|
151
|
+
end
|
152
|
+
|
153
|
+
# Updates only a single record. Lookups primary key to update the record.
|
154
|
+
def update_record(output, row, time)
|
155
|
+
raise ArgumentError, 'primary_key is required' unless primary_key
|
156
|
+
|
157
|
+
first_record = find_record(output, row, time)
|
158
|
+
|
159
|
+
if first_record
|
160
|
+
debug_detail(output, "Record Exists: #{first_record}")
|
161
|
+
|
162
|
+
id = resolver.get(first_record, primary_key.column)
|
163
|
+
|
164
|
+
where_object = { primary_key.key => id }
|
165
|
+
|
166
|
+
# update record using the primary key as the WHERE clause
|
167
|
+
update(output, row, time, where_object)
|
168
|
+
end
|
169
|
+
|
170
|
+
first_record
|
171
|
+
end
|
172
|
+
|
173
|
+
# Updates one or many records depending on where_object passed
|
174
|
+
def update(output, row, time, where_object)
|
175
|
+
dynamic_attrs = if timestamps
|
176
|
+
# doing an UPDATE and timestamps should be set,
|
177
|
+
# modify the updated_at field, don't modify the created_at field
|
178
|
+
attribute_renderers_set.timestamp_updated_attribute_renderers
|
179
|
+
else
|
180
|
+
attribute_renderer_set.attribute_renderers
|
181
|
+
end
|
182
|
+
|
183
|
+
set_object = attribute_renderers_set.transform(dynamic_attrs, row, time)
|
184
|
+
|
185
|
+
update_sql = db_provider.update_sql(set_object, where_object)
|
186
|
+
|
187
|
+
debug_detail(output, "Update Statement: #{update_sql}")
|
188
|
+
|
189
|
+
debug_detail(output, "Update Return: #{row}")
|
190
|
+
|
191
|
+
db_provider.update(set_object, where_object)
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
def insert_or_update(output, row, time)
|
197
|
+
first_record = update_record(output, row, time)
|
198
|
+
|
199
|
+
if first_record
|
200
|
+
first_record
|
201
|
+
else
|
202
|
+
# create the record
|
203
|
+
insert_record(output, row, time)
|
204
|
+
nil
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
data/lib/db_fuel/modeling.rb
CHANGED
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Copyright (c) 2020-present, Blue Marble Payroll, LLC
|
5
|
+
#
|
6
|
+
# This source code is licensed under the MIT license found in the
|
7
|
+
# LICENSE file in the root directory of this source tree.
|
8
|
+
#
|
9
|
+
|
10
|
+
module DbFuel
|
11
|
+
module Modeling
|
12
|
+
# Creates attribute renderers based on attributes passed.
|
13
|
+
# Also constains methods to transform attribute renderers
|
14
|
+
# and include timestamp attributes if needed.
|
15
|
+
class AttributeRendererSet
|
16
|
+
CREATED_AT = :created_at
|
17
|
+
NOW_TYPE = 'r/value/now'
|
18
|
+
UPDATED_AT = :updated_at
|
19
|
+
|
20
|
+
attr_reader :attribute_renderers, :resolver
|
21
|
+
|
22
|
+
def initialize(attributes: [], resolver: nil)
|
23
|
+
raise ArgumentError, 'resolver is required' unless resolver
|
24
|
+
|
25
|
+
@resolver = resolver
|
26
|
+
@attribute_renderers = make_attribute_renderers(attributes)
|
27
|
+
|
28
|
+
freeze
|
29
|
+
end
|
30
|
+
|
31
|
+
# Adds the attributes for created_at and updated_at to the currrent attribute renderers.
|
32
|
+
def timestamp_created_attribute_renderers
|
33
|
+
timestamp_attributes = [created_at_timestamp_attribute, updated_at_timestamp_attribute]
|
34
|
+
|
35
|
+
timestamp_attributes.map do |a|
|
36
|
+
Burner::Modeling::AttributeRenderer.new(a, resolver)
|
37
|
+
end + attribute_renderers
|
38
|
+
end
|
39
|
+
|
40
|
+
# Adds the attribute for updated_at to the currrent attribute renderers.
|
41
|
+
def timestamp_updated_attribute_renderers
|
42
|
+
timestamp_attributes = [updated_at_timestamp_attribute]
|
43
|
+
|
44
|
+
timestamp_attributes.map do |a|
|
45
|
+
Burner::Modeling::AttributeRenderer.new(a, resolver)
|
46
|
+
end + attribute_renderers
|
47
|
+
end
|
48
|
+
|
49
|
+
def make_attribute_renderers(attributes)
|
50
|
+
Burner::Modeling::Attribute
|
51
|
+
.array(attributes)
|
52
|
+
.map { |a| Burner::Modeling::AttributeRenderer.new(a, resolver) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def transform(attribute_renderers, row, time)
|
56
|
+
attribute_renderers.each_with_object({}) do |attribute_renderer, memo|
|
57
|
+
value = attribute_renderer.transform(row, time)
|
58
|
+
|
59
|
+
resolver.set(memo, attribute_renderer.key, value)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def created_at_timestamp_attribute
|
66
|
+
timestamp_attribute(CREATED_AT)
|
67
|
+
end
|
68
|
+
|
69
|
+
def updated_at_timestamp_attribute
|
70
|
+
timestamp_attribute(UPDATED_AT)
|
71
|
+
end
|
72
|
+
|
73
|
+
def timestamp_attribute(key)
|
74
|
+
Burner::Modeling::Attribute.make(
|
75
|
+
key: key,
|
76
|
+
transformers: [
|
77
|
+
{ type: NOW_TYPE }
|
78
|
+
]
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/db_fuel/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: db_fuel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0.pre.alpha
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew Ruggio
|
8
|
+
- John Bosko
|
8
9
|
autorequire:
|
9
10
|
bindir: exe
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2021-01-13 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: activerecord
|
@@ -162,14 +163,14 @@ dependencies:
|
|
162
163
|
requirements:
|
163
164
|
- - "~>"
|
164
165
|
- !ruby/object:Gem::Version
|
165
|
-
version:
|
166
|
+
version: 1.7.0
|
166
167
|
type: :development
|
167
168
|
prerelease: false
|
168
169
|
version_requirements: !ruby/object:Gem::Requirement
|
169
170
|
requirements:
|
170
171
|
- - "~>"
|
171
172
|
- !ruby/object:Gem::Version
|
172
|
-
version:
|
173
|
+
version: 1.7.0
|
173
174
|
- !ruby/object:Gem::Dependency
|
174
175
|
name: simplecov
|
175
176
|
requirement: !ruby/object:Gem::Requirement
|
@@ -216,6 +217,7 @@ description: " This library adds database-centric jobs to the Burner library.
|
|
216
217
|
does not ship with database jobs out of the box.\n"
|
217
218
|
email:
|
218
219
|
- mruggio@bluemarblepayroll.com
|
220
|
+
- jbosko@bluemarblepayroll.com
|
219
221
|
executables: []
|
220
222
|
extensions: []
|
221
223
|
extra_rdoc_files: []
|
@@ -224,6 +226,7 @@ files:
|
|
224
226
|
- ".gitignore"
|
225
227
|
- ".rubocop.yml"
|
226
228
|
- ".ruby-version"
|
229
|
+
- ".tool-versions"
|
227
230
|
- ".travis.yml"
|
228
231
|
- CHANGELOG.md
|
229
232
|
- CODE_OF_CONDUCT.md
|
@@ -242,10 +245,13 @@ files:
|
|
242
245
|
- lib/db_fuel/library/active_record/find_or_insert.rb
|
243
246
|
- lib/db_fuel/library/active_record/insert.rb
|
244
247
|
- lib/db_fuel/library/active_record/update.rb
|
248
|
+
- lib/db_fuel/library/active_record/update_all.rb
|
249
|
+
- lib/db_fuel/library/active_record/upsert.rb
|
245
250
|
- lib/db_fuel/library/dbee/base.rb
|
246
251
|
- lib/db_fuel/library/dbee/query.rb
|
247
252
|
- lib/db_fuel/library/dbee/range.rb
|
248
253
|
- lib/db_fuel/modeling.rb
|
254
|
+
- lib/db_fuel/modeling/attribute_renderer_set.rb
|
249
255
|
- lib/db_fuel/modeling/keyed_column.rb
|
250
256
|
- lib/db_fuel/version.rb
|
251
257
|
homepage: https://github.com/bluemarblepayroll/db_fuel
|
@@ -268,9 +274,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
268
274
|
version: '2.5'
|
269
275
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
270
276
|
requirements:
|
271
|
-
- - "
|
277
|
+
- - ">"
|
272
278
|
- !ruby/object:Gem::Version
|
273
|
-
version:
|
279
|
+
version: 1.3.1
|
274
280
|
requirements: []
|
275
281
|
rubygems_version: 3.0.3
|
276
282
|
signing_key:
|