db_fuel 1.1.0.pre.alpha → 1.1.0.pre.alpha.1

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: fb563756df642d23e7d268cf0e7b35735300ddfc84da31e0ee11d7e138d41584
4
- data.tar.gz: f5720ed0f4649ba82d0c233cf9ccbc4aff5d54d8d9c4267c830467f43f85cdf3
3
+ metadata.gz: 0d7fce3d73ed5e5c1cfabfe4a578cd00fc6e932c7b32d1f60a7e17968163b247
4
+ data.tar.gz: 88eab379caeac681a34a8d58cc5ea3d8de51d242638d7707d4938d00a99f490d
5
5
  SHA512:
6
- metadata.gz: 16c63b0939d36f32cfc72baeefc20fbb88e7285d5c5e8266d337f55362909a6f751f7f2b1c535f6c231a11adba2693807c68e6af1b4d5eaaf4b14a9213589d01
7
- data.tar.gz: a5143b01108dc065d98a5e085da06c8e82a5f0c084a0097128219e8c49542818a8ce754ef1af79be23d6493f259f745657bba4e74051cf856264fbe87a76dfde
6
+ metadata.gz: 91b4c18f3a081c296b005bbd6717f81d9595323dee3c69b7fca1594e4dfdf8063aba2052d54d6182bfa1a1440e3e8150117213961c4c92c010e7637ebb0f1560
7
+ data.tar.gz: b6e18bc0c58d6472465fe51fe06669bc54cb456526c6af1944de4c66b2e3e59281cf3ea21fbb54a78b9109e186536342b32dd2b15fc4561edc4ce4cc38abbe9b
@@ -2,6 +2,7 @@
2
2
 
3
3
  New Jobs:
4
4
 
5
+ * db_fuel/active_record/find_or_insert
5
6
  * db_fuel/active_record/insert
6
7
  * db_fuel/active_record/update
7
8
 
data/README.md CHANGED
@@ -24,8 +24,9 @@ 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.
27
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. Attributes defines which object properties to convert to SQL. Refer to the class and constructor specification for more detail.
28
- * **db_fuel/active_record/update** [table_name, attributes, debug, register, separator, timestamps, unique_keys]: 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_keys translate to WHERE clauses. 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.
29
30
 
30
31
  ### Dbee Jobs
31
32
 
@@ -195,6 +196,49 @@ Notes:
195
196
  * Since we specified the `primary_key`, the records' `id` attributes should be set to their respective primary key values.
196
197
  * Set `debug: true` to print out each INSERT statement in the output (not for production use.)
197
198
 
199
+ #### Inserting Only New Records
200
+
201
+ Another job `db_fuel/active_record/find_or_insert` allows for an existence check to performed each insertion. If a record is found then it will not insert the record. If `primary_key` is set then the existence check will also still set the primary key on the payload's respective object. We can build on the above insert example for only inserting new patients if their chart_number is unique:
202
+
203
+ ````ruby
204
+ pipeline = {
205
+ jobs: [
206
+ {
207
+ name: :load_patients,
208
+ type: 'b/value/static',
209
+ register: :patients,
210
+ value: [
211
+ { chart_number: 'B0001', first_name: 'Bugs', last_name: 'Bunny' },
212
+ { chart_number: 'B0002', first_name: 'Babs', last_name: 'Bunny' }
213
+ ]
214
+ },
215
+ {
216
+ name: 'insert_patients',
217
+ type: 'db_fuel/active_record/insert',
218
+ register: :patients,
219
+ attributes: [
220
+ { key: :chart_number },
221
+ { key: :first_name },
222
+ { key: :last_name }
223
+ ],
224
+ table_name: 'patients',
225
+ primary_key: {
226
+ key: :id
227
+ },
228
+ unique_attributes: [
229
+ { key: :chart_number }
230
+ ]
231
+ }
232
+ ]
233
+ }
234
+
235
+ payload = Burner::Payload.new
236
+
237
+ Burner::Pipeline.make(pipeline).execute(payload: payload)
238
+ ````
239
+
240
+ Now only records where the chart_number does not match an existing record will be inserted.
241
+
198
242
  #### Updating Records
199
243
 
200
244
  Let's say we now want to update those records' last names:
@@ -219,7 +263,7 @@ pipeline = {
219
263
  { key: :last_name }
220
264
  ],
221
265
  table_name: 'patients',
222
- unique_keys: [
266
+ unique_attributes: [
223
267
  { key: :chart_number }
224
268
  ]
225
269
  }
@@ -235,7 +279,7 @@ Each database record should have been updated with their new respective last nam
235
279
 
236
280
  Notes:
237
281
 
238
- * The unique_keys translate to WHERE clauses.
282
+ * The `unique_attributes` translate to WHERE clauses.
239
283
  * Set `debug: true` to print out each UPDATE statement in the output (not for production use.)
240
284
  ## Contributing
241
285
 
@@ -17,4 +17,7 @@ require 'objectable'
17
17
  # General purpose classes used by the main job classes.
18
18
  require_relative 'db_fuel/modeling'
19
19
 
20
+ # Internal logic used across jobs.
21
+ require_relative 'db_fuel/db_provider'
22
+
20
23
  require_relative 'db_fuel/library'
@@ -0,0 +1,82 @@
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
+ # Intermediate internal API for Arel/ActiveRecord. There is some overlap in job needs when
12
+ # it comes to the Arel interface so this class condenses down those needs into this class.
13
+ class DbProvider # :nodoc: all
14
+ attr_reader :arel_table
15
+
16
+ def initialize(table_name)
17
+ raise ArgumentError, 'table_name is required' if table_name.to_s.empty?
18
+
19
+ @arel_table = ::Arel::Table.new(table_name.to_s)
20
+
21
+ freeze
22
+ end
23
+
24
+ def first(object)
25
+ sql = first_sql(object)
26
+
27
+ ::ActiveRecord::Base.connection.exec_query(sql).first
28
+ end
29
+
30
+ def first_sql(object)
31
+ relation = arel_table.project(Arel.star).take(1)
32
+ manager = apply_where(object, relation)
33
+
34
+ manager.to_sql
35
+ end
36
+
37
+ def insert_sql(object)
38
+ insert_manager(object).to_sql
39
+ end
40
+
41
+ def insert(object)
42
+ manager = insert_manager(object)
43
+
44
+ ::ActiveRecord::Base.connection.insert(manager)
45
+ end
46
+
47
+ def update(set_object, where_object)
48
+ manager = update_manager(set_object, where_object)
49
+
50
+ ::ActiveRecord::Base.connection.update(manager)
51
+ end
52
+
53
+ def update_sql(set_object, where_object)
54
+ update_manager(set_object, where_object).to_sql
55
+ end
56
+
57
+ private
58
+
59
+ def update_manager(set_object, where_object)
60
+ arel_row = make_arel_row(set_object)
61
+ update_manager = ::Arel::UpdateManager.new.set(arel_row).table(arel_table)
62
+
63
+ apply_where(where_object, update_manager)
64
+ end
65
+
66
+ def apply_where(hash, manager)
67
+ (hash || {}).inject(manager) do |memo, (key, value)|
68
+ memo.where(arel_table[key].eq(value))
69
+ end
70
+ end
71
+
72
+ def insert_manager(object)
73
+ arel_row = make_arel_row(object)
74
+
75
+ ::Arel::InsertManager.new.insert(arel_row)
76
+ end
77
+
78
+ def make_arel_row(row)
79
+ row.map { |key, value| [arel_table[key], value] }
80
+ end
81
+ end
82
+ end
@@ -7,6 +7,7 @@
7
7
  # LICENSE file in the root directory of this source tree.
8
8
  #
9
9
 
10
+ require_relative 'library/active_record/find_or_insert'
10
11
  require_relative 'library/active_record/insert'
11
12
  require_relative 'library/active_record/update'
12
13
 
@@ -16,10 +17,11 @@ require_relative 'library/dbee/range'
16
17
  module Burner
17
18
  # Open up Burner::Jobs and add registrations for this libraries jobs.
18
19
  class Jobs
19
- register 'db_fuel/active_record/insert', DbFuel::Library::ActiveRecord::Insert
20
- register 'db_fuel/active_record/update', DbFuel::Library::ActiveRecord::Update
20
+ register 'db_fuel/active_record/find_or_insert', DbFuel::Library::ActiveRecord::FindOrInsert
21
+ register 'db_fuel/active_record/insert', DbFuel::Library::ActiveRecord::Insert
22
+ register 'db_fuel/active_record/update', DbFuel::Library::ActiveRecord::Update
21
23
 
22
- register 'db_fuel/dbee/query', DbFuel::Library::Dbee::Query
23
- register 'db_fuel/dbee/range', DbFuel::Library::Dbee::Range
24
+ register 'db_fuel/dbee/query', DbFuel::Library::Dbee::Query
25
+ register 'db_fuel/dbee/range', DbFuel::Library::Dbee::Range
24
26
  end
25
27
  end
@@ -19,8 +19,8 @@ module DbFuel
19
19
  NOW_TYPE = 'r/value/now'
20
20
  UPDATED_AT = :updated_at
21
21
 
22
- attr_reader :arel_table,
23
- :attribute_renderers,
22
+ attr_reader :attribute_renderers,
23
+ :db_provider,
24
24
  :debug,
25
25
  :resolver
26
26
 
@@ -34,19 +34,37 @@ module DbFuel
34
34
  )
35
35
  super(name: name, register: register)
36
36
 
37
- @arel_table = ::Arel::Table.new(table_name.to_s)
38
- @debug = debug || false
39
-
40
37
  # set resolver first since make_attribute_renderers needs it.
41
- @resolver = Objectable.resolver(separator: separator)
42
-
43
- @attribute_renderers = Burner::Modeling::Attribute
44
- .array(attributes)
45
- .map { |a| Burner::Modeling::AttributeRenderer.new(a, resolver) }
38
+ @resolver = Objectable.resolver(separator: separator)
39
+ @attribute_renderers = make_attribute_renderers(attributes)
40
+ @db_provider = DbProvider.new(table_name)
41
+ @debug = debug || false
46
42
  end
47
43
 
48
44
  private
49
45
 
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
+
50
68
  def timestamp_attribute(key)
51
69
  Burner::Modeling::Attribute.make(
52
70
  key: key,
@@ -61,18 +79,6 @@ module DbFuel
61
79
 
62
80
  output.detail(message)
63
81
  end
64
-
65
- def make_arel_row(row)
66
- row.map { |key, value| [arel_table[key], value] }
67
- end
68
-
69
- def transform(row, time)
70
- attribute_renderers.each_with_object({}) do |attribute_renderer, memo|
71
- value = attribute_renderer.transform(row, time)
72
-
73
- resolver.set(memo, attribute_renderer.key, value)
74
- end
75
- end
76
82
  end
77
83
  end
78
84
  end
@@ -0,0 +1,126 @@
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 'insert'
11
+
12
+ module DbFuel
13
+ module Library
14
+ module ActiveRecord
15
+ # This job is a slight enhancement to the insert job, in that it will only insert new
16
+ # records. 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 primary_key is specified and a record is
18
+ # found then the first record's id will be set to the primary_key.
19
+ #
20
+ # Expected Payload[register] input: array of objects
21
+ # Payload[register] output: array of objects.
22
+ class FindOrInsert < Insert
23
+ attr_reader :unique_attribute_renderers
24
+
25
+ # Arguments:
26
+ # name [required]: name of the job within the Burner::Pipeline.
27
+ #
28
+ # table_name [required]: name of the table to use for the INSERT statements.
29
+ #
30
+ # attributes: Used to specify which object properties to put into the
31
+ # SQL statement and also allows for one last custom transformation
32
+ # pipeline, in case the data calls for sql-specific transformers
33
+ # before insertion.
34
+ #
35
+ # debug: If debug is set to true (defaults to false) then the SQL statements and
36
+ # returned objects will be printed in the output. Only use this option while
37
+ # debugging issues as it will fill up the output with (potentially too much) data.
38
+ #
39
+ # primary_key: If primary_key is present then it will be used to set the object's
40
+ # property to the returned primary key from the INSERT statement.
41
+ #
42
+ # separator: Just like other jobs with a 'separator' option, if the objects require
43
+ # key-path notation or nested object support, you can set the separator
44
+ # to something non-blank (like a period for notation in the
45
+ # form of: name.first).
46
+ #
47
+ # timestamps: If timestamps is true (default behavior) then both created_at
48
+ # and updated_at columns will automatically have their values set
49
+ # to the current UTC timestamp.
50
+ #
51
+ # unique_attributes: Each key will become a WHERE clause in order check for record
52
+ # existence before insertion attempt.
53
+ def initialize(
54
+ name:,
55
+ table_name:,
56
+ attributes: [],
57
+ debug: false,
58
+ primary_key: nil,
59
+ register: Burner::DEFAULT_REGISTER,
60
+ separator: '',
61
+ timestamps: true,
62
+ unique_attributes: []
63
+ )
64
+ super(
65
+ name: name,
66
+ table_name: table_name,
67
+ attributes: attributes,
68
+ debug: debug,
69
+ primary_key: primary_key,
70
+ register: register,
71
+ separator: separator,
72
+ timestamps: timestamps
73
+ )
74
+
75
+ @unique_attribute_renderers = make_attribute_renderers(unique_attributes)
76
+ end
77
+
78
+ def perform(output, payload)
79
+ total_inserted = 0
80
+ total_existed = 0
81
+
82
+ payload[register] = array(payload[register])
83
+
84
+ payload[register].each do |row|
85
+ exists = existence_check_and_mutate(output, row, payload.time)
86
+
87
+ if exists
88
+ total_existed += 1
89
+ next
90
+ end
91
+
92
+ insert(output, row, payload.time)
93
+
94
+ total_inserted += 1
95
+ end
96
+
97
+ output.detail("Total Existed: #{total_existed}")
98
+ output.detail("Total Inserted: #{total_inserted}")
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
+ end
124
+ end
125
+ end
126
+ end
@@ -68,33 +68,41 @@ module DbFuel
68
68
  )
69
69
 
70
70
  @primary_key = Modeling::KeyedColumn.make(primary_key, nullable: true)
71
-
72
- freeze
73
71
  end
74
72
 
75
73
  def perform(output, payload)
76
74
  payload[register] = array(payload[register])
77
75
 
78
- payload[register].each do |row|
79
- arel_row = make_arel_row(transform(row, payload.time))
80
- insert_manager = ::Arel::InsertManager.new.insert(arel_row)
76
+ payload[register].each { |row| insert(output, row, payload.time) }
77
+ end
81
78
 
82
- debug_detail(output, "Insert Statement: #{insert_manager.to_sql}")
79
+ private
83
80
 
84
- id = ::ActiveRecord::Base.connection.insert(insert_manager)
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
85
87
 
86
- resolver.set(row, primary_key.key, id) if primary_key
88
+ def output_sql(output, row)
89
+ sql = db_provider.insert_sql(row)
87
90
 
88
- debug_detail(output, "Insert Return: #{row}")
89
- end
91
+ debug_detail(output, "Insert Statement: #{sql}")
90
92
  end
91
93
 
92
- private
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
93
101
 
94
102
  def timestamp_attributes
95
103
  [
96
- timestamp_attribute(CREATED_AT),
97
- timestamp_attribute(UPDATED_AT)
104
+ created_at_timestamp_attribute,
105
+ updated_at_timestamp_attribute
98
106
  ]
99
107
  end
100
108
  end
@@ -19,7 +19,7 @@ module DbFuel
19
19
  # Expected Payload[register] input: array of objects
20
20
  # Payload[register] output: array of objects.
21
21
  class Update < Base
22
- attr_reader :unique_keys
22
+ attr_reader :unique_attribute_renderers
23
23
 
24
24
  # Arguments:
25
25
  # name [required]: name of the job within the Burner::Pipeline.
@@ -43,8 +43,8 @@ module DbFuel
43
43
  # timestamps: If timestamps is true (default behavior) then the updated_at column will
44
44
  # automatically have its value set to the current UTC timestamp.
45
45
  #
46
- # unique_keys: Each key will become a WHERE clause in order to only update specific
47
- # records.
46
+ # unique_attributes: Each key will become a WHERE clause in order to only update specific
47
+ # records.
48
48
  def initialize(
49
49
  name:,
50
50
  table_name:,
@@ -53,7 +53,7 @@ module DbFuel
53
53
  register: Burner::DEFAULT_REGISTER,
54
54
  separator: '',
55
55
  timestamps: true,
56
- unique_keys: []
56
+ unique_attributes: []
57
57
  )
58
58
  explicit_attributes = Burner::Modeling::Attribute.array(attributes)
59
59
 
@@ -68,7 +68,7 @@ module DbFuel
68
68
  separator: separator
69
69
  )
70
70
 
71
- @unique_keys = Modeling::KeyedColumn.array(unique_keys)
71
+ @unique_attribute_renderers = make_attribute_renderers(unique_attributes)
72
72
 
73
73
  freeze
74
74
  end
@@ -79,11 +79,14 @@ module DbFuel
79
79
  payload[register] = array(payload[register])
80
80
 
81
81
  payload[register].each do |row|
82
- update_manager = make_update_manager(row, payload.time)
82
+ set_object = transform(attribute_renderers, row, payload.time)
83
+ where_object = transform(unique_attribute_renderers, row, payload.time)
83
84
 
84
- debug_detail(output, "Update Statement: #{update_manager.to_sql}")
85
+ sql = db_provider.update_sql(set_object, where_object)
85
86
 
86
- rows_affected = ::ActiveRecord::Base.connection.update(update_manager)
87
+ debug_detail(output, "Update Statement: #{sql}")
88
+
89
+ rows_affected = db_provider.update(set_object, where_object)
87
90
 
88
91
  debug_detail(output, "Individual Rows Affected: #{rows_affected}")
89
92
 
@@ -95,28 +98,10 @@ module DbFuel
95
98
 
96
99
  private
97
100
 
98
- def make_update_manager(row, time)
99
- arel_row = make_arel_row(transform(row, time))
100
- unique_values = make_unique_column_values(row)
101
- update_manager = ::Arel::UpdateManager.new.set(arel_row).table(arel_table)
102
-
103
- apply_where(unique_values, update_manager)
104
- end
105
-
106
- def make_unique_column_values(row)
107
- unique_keys.each_with_object({}) do |unique_key, memo|
108
- memo[unique_key.column] = resolver.get(row, unique_key.key)
109
- end
110
- end
111
-
112
- def apply_where(hash, manager)
113
- (hash || {}).inject(manager) do |memo, (key, value)|
114
- memo.where(arel_table[key].eq(value))
115
- end
116
- end
117
-
118
101
  def timestamp_attributes
119
- [timestamp_attribute(UPDATED_AT)]
102
+ [
103
+ updated_at_timestamp_attribute
104
+ ]
120
105
  end
121
106
  end
122
107
  end
@@ -8,5 +8,5 @@
8
8
  #
9
9
 
10
10
  module DbFuel
11
- VERSION = '1.1.0-alpha'
11
+ VERSION = '1.1.0-alpha.1'
12
12
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: db_fuel
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0.pre.alpha
4
+ version: 1.1.0.pre.alpha.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Ruggio
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-25 00:00:00.000000000 Z
11
+ date: 2020-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -236,8 +236,10 @@ files:
236
236
  - db_fuel.gemspec
237
237
  - exe/.gitkeep
238
238
  - lib/db_fuel.rb
239
+ - lib/db_fuel/db_provider.rb
239
240
  - lib/db_fuel/library.rb
240
241
  - lib/db_fuel/library/active_record/base.rb
242
+ - lib/db_fuel/library/active_record/find_or_insert.rb
241
243
  - lib/db_fuel/library/active_record/insert.rb
242
244
  - lib/db_fuel/library/active_record/update.rb
243
245
  - lib/db_fuel/library/dbee/base.rb