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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e2fc7d0ced8034053e409df7d34fa7127e21e357454221ad0262a59cc5bf2be
4
- data.tar.gz: 6b2ce0fd3f4b6661f84820bb2394e49e1e11b111d3df02ce7981996b1eae386e
3
+ metadata.gz: b4f5691edb9519be2c43b787f9eeee14d2e2e83fcf9071e21dbef2442449d3fa
4
+ data.tar.gz: a95cc6d63d3eba9256c022891328352662b64cf8e0391a92962609f6d3a8062c
5
5
  SHA512:
6
- metadata.gz: 73019f998b6db2d10d397e017ab5764dae1e7f4c42ceac403c0a345d91d21fe9da680a46a512a7dad0de85e0a731d6b81bffa0bfab85e7d9b6ca4912c571f0b0
7
- data.tar.gz: f0e9b1e9dc28d29659c8532ef39229a8fa7e13c37f2476a75b37dbb62a46469e96ba6916a3eb0df1cc7cdb392c63031271443c0ab54d6933b8c7985f3fcb871b
6
+ metadata.gz: 36c2477f3010bd60a7ef7a49f60f7a5302d2cec2ad8be034d83e18923eed07820578a87993a9ebc07e1121c8853d45e5060eaca4d4895222afb3b69b8a8b61f9
7
+ data.tar.gz: 54f65f7f0d56271bacb6b67512f10135b4d22ddb64dd0a7d1fc864e6d2cd29a0d7e4579056f518cf517b5b3353715bafc489743d596cc8bd8e3e9a0812d6755b
@@ -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
- ExcludedMethods:
12
+ IgnoredMethods:
12
13
  - let
13
14
  - it
14
15
  - describe
@@ -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/update** [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. Refer to the class and constructor specification for more detail.
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 those records' last names:
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 new respective last names.
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
 
@@ -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', '~>0.90.0')
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')
@@ -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
- # set resolver first since make_attribute_renderers needs it.
38
- @resolver = Objectable.resolver(separator: separator)
39
- @attribute_renderers = make_attribute_renderers(attributes)
40
- @db_provider = DbProvider.new(table_name)
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 'insert'
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 < Insert
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 = existence_check_and_mutate(output, row, payload.time)
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
- insert(output, row, payload.time)
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 'base'
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 < Base
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 = timestamps ? timestamp_attributes + explicit_attributes : explicit_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| insert(output, row, payload.time) }
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 'base'
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 < Base
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 update specific
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 = timestamps ? timestamp_attributes + explicit_attributes : explicit_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
- set_object = transform(attribute_renderers, row, payload.time)
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
- debug_detail(output, "Update Statement: #{sql}")
91
+ first_record = update_record(output, row, payload.time)
88
92
 
89
- rows_affected = db_provider.update(set_object, where_object)
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
@@ -8,3 +8,4 @@
8
8
  #
9
9
 
10
10
  require_relative 'modeling/keyed_column'
11
+ require_relative 'modeling/attribute_renderer_set'
@@ -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
@@ -8,5 +8,5 @@
8
8
  #
9
9
 
10
10
  module DbFuel
11
- VERSION = '1.1.0'
11
+ VERSION = '1.2.0-alpha'
12
12
  end
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.1.0
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: 2020-12-01 00:00:00.000000000 Z
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: 0.90.0
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: 0.90.0
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: '0'
279
+ version: 1.3.1
274
280
  requirements: []
275
281
  rubygems_version: 3.0.3
276
282
  signing_key: