db_fuel 1.0.0.pre.alpha → 1.2.0.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -16,6 +16,11 @@ module DbFuel
16
16
  :provider,
17
17
  :query
18
18
 
19
+ # Arguments:
20
+ # - model: Dbee Model configuration
21
+ # - query: Dbee Query configuration
22
+ # - register: Name of the register to use for gathering the IN clause values and where
23
+ # to store the resulting recordset.
19
24
  def initialize(
20
25
  name:,
21
26
  model: {},
@@ -12,8 +12,8 @@ require_relative 'base'
12
12
  module DbFuel
13
13
  module Library
14
14
  module Dbee
15
- # Execute a Dbee Query against a Dbee Model and store the resulting records in the designated
16
- # payload register.
15
+ # Executes a Dbee Query against a Dbee Model and stores the resulting records
16
+ # in the designated payload register.
17
17
  #
18
18
  # Expected Payload[register] input: nothing
19
19
  # Payload[register] output: array of objects.
@@ -23,6 +23,15 @@ module DbFuel
23
23
  :key_path,
24
24
  :resolver
25
25
 
26
+ # Arguments:
27
+ # - key: Specifies which key to use to aggregate a list of values for within
28
+ # the specified register's dataset.
29
+ # - key_path: Specifies the Dbee identifier (column) to use for the IN filter.
30
+ # - model: Dbee Model configuration
31
+ # - query: Dbee Query configuration
32
+ # - register: Name of the register to use for gathering the IN clause values and where
33
+ # to store the resulting recordset.
34
+ # - separator: Character to use to split the key-path for nested object support.
26
35
  def initialize(
27
36
  name:,
28
37
  key:,
@@ -58,20 +67,24 @@ module DbFuel
58
67
  array(payload[register]).map { |o| resolver.get(o, key) }.compact
59
68
  end
60
69
 
61
- def dynamic_filter(payload)
70
+ def dynamic_filters(payload)
62
71
  values = map_values(payload)
63
72
 
64
- {
65
- type: :equals,
66
- key_path: key_path,
67
- value: values,
68
- }
73
+ return [] if values.empty?
74
+
75
+ [
76
+ {
77
+ type: :equals,
78
+ key_path: key_path,
79
+ value: values,
80
+ }
81
+ ]
69
82
  end
70
83
 
71
84
  def compile_dbee_query(payload)
72
85
  ::Dbee::Query.make(
73
86
  fields: query.fields,
74
- filters: query.filters + [dynamic_filter(payload)],
87
+ filters: query.filters + dynamic_filters(payload),
75
88
  limit: query.limit,
76
89
  sorters: query.sorters
77
90
  )
@@ -0,0 +1,11 @@
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 '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