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

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,103 @@
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 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 < Upsert
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
+
65
+ super(
66
+ name: name,
67
+ table_name: table_name,
68
+ attributes: attributes,
69
+ debug: debug,
70
+ primary_key: primary_key,
71
+ register: register,
72
+ separator: separator,
73
+ timestamps: timestamps,
74
+ unique_attributes: unique_attributes
75
+ )
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 = find_record(output, row, payload.time)
86
+
87
+ if exists
88
+ total_existed += 1
89
+ next
90
+ end
91
+
92
+ insert_record(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
+ end
101
+ end
102
+ end
103
+ 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.
@@ -26,7 +26,7 @@ module DbFuel
26
26
  #
27
27
  # attributes: Used to specify which object properties to put into the
28
28
  # SQL statement and also allows for one last custom transformation
29
- # pipeline, in case the data calls for sql-specific transformers
29
+ # pipeline, in case the data calls for SQL-specific transformers
30
30
  # before insertion.
31
31
  #
32
32
  # debug: If debug is set to true (defaults to false) then the SQL statements and
@@ -54,48 +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
-
72
- freeze
73
70
  end
74
71
 
75
72
  def perform(output, payload)
76
73
  payload[register] = array(payload[register])
77
74
 
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)
81
-
82
- debug_detail(output, "Insert Statement: #{insert_manager.to_sql}")
83
-
84
- id = ::ActiveRecord::Base.connection.insert(insert_manager)
85
-
86
- resolver.set(row, primary_key.key, id) if primary_key
87
-
88
- debug_detail(output, "Insert Return: #{row}")
89
- end
90
- end
91
-
92
- private
93
-
94
- def timestamp_attributes
95
- [
96
- timestamp_attribute(CREATED_AT),
97
- timestamp_attribute(UPDATED_AT)
98
- ]
75
+ payload[register].each { |row| insert_record(output, row, payload.time) }
99
76
  end
100
77
  end
101
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_keys
23
-
23
+ class Update < Upsert
24
24
  # Arguments:
25
25
  # name [required]: name of the job within the Burner::Pipeline.
26
26
  #
@@ -28,13 +28,18 @@ module DbFuel
28
28
  #
29
29
  # attributes: Used to specify which object properties to put into the
30
30
  # SQL statement and also allows for one last custom transformation
31
- # pipeline, in case the data calls for sql-specific transformers
31
+ # pipeline, in case the data calls for SQL-specific transformers
32
32
  # before mutation.
33
33
  #
34
34
  # debug: If debug is set to true (defaults to false) then the SQL statements and
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_keys: 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
- unique_keys: []
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_keys = Modeling::KeyedColumn.array(unique_keys)
72
-
73
80
  freeze
74
81
  end
75
82
 
@@ -79,11 +86,11 @@ module DbFuel
79
86
  payload[register] = array(payload[register])
80
87
 
81
88
  payload[register].each do |row|
82
- update_manager = make_update_manager(row, payload.time)
89
+ rows_affected = 0
83
90
 
84
- debug_detail(output, "Update Statement: #{update_manager.to_sql}")
91
+ first_record = update_record(output, row, payload.time)
85
92
 
86
- rows_affected = ::ActiveRecord::Base.connection.update(update_manager)
93
+ rows_affected = 1 if first_record
87
94
 
88
95
  debug_detail(output, "Individual Rows Affected: #{rows_affected}")
89
96
 
@@ -92,32 +99,6 @@ module DbFuel
92
99
 
93
100
  output.detail("Total Rows Affected: #{total_rows_affected}")
94
101
  end
95
-
96
- private
97
-
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
- def timestamp_attributes
119
- [timestamp_attribute(UPDATED_AT)]
120
- end
121
102
  end
122
103
  end
123
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_renderers_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_renderers_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