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.
@@ -5,14 +5,14 @@ require './lib/db_fuel/version'
5
5
  Gem::Specification.new do |s|
6
6
  s.name = 'db_fuel'
7
7
  s.version = DbFuel::VERSION
8
- s.summary = 'TBD'
8
+ s.summary = 'Dbee and ActiveRecord jobs for Burner'
9
9
 
10
10
  s.description = <<-DESCRIPTION
11
- TBD
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[]
@@ -42,7 +42,7 @@ Gem::Specification.new do |s|
42
42
 
43
43
  s.add_dependency('activerecord', activerecord_version)
44
44
  s.add_dependency('acts_as_hashable', '~>1.2')
45
- s.add_dependency('burner', '~>1.0')
45
+ s.add_dependency('burner', '~>1.2')
46
46
  s.add_dependency('dbee', '~>2.1')
47
47
  s.add_dependency('dbee-active_record', '~>2.1')
48
48
  s.add_dependency('objectable', '~>1.0')
@@ -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')
@@ -14,4 +14,10 @@ require 'dbee'
14
14
  require 'dbee/providers/active_record_provider'
15
15
  require 'objectable'
16
16
 
17
+ # General purpose classes used by the main job classes.
18
+ require_relative 'db_fuel/modeling'
19
+
20
+ # Internal logic used across jobs.
21
+ require_relative 'db_fuel/db_provider'
22
+
17
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,8 +7,25 @@
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'
11
+ require_relative 'library/active_record/insert'
12
+ require_relative 'library/active_record/update'
13
+ require_relative 'library/active_record/update_all'
14
+ require_relative 'library/active_record/upsert'
15
+
10
16
  require_relative 'library/dbee/query'
11
17
  require_relative 'library/dbee/range'
12
18
 
13
- Burner::Jobs.register('db_fuel/dbee/query', DbFuel::Library::Dbee::Query)
14
- Burner::Jobs.register('db_fuel/dbee/range', DbFuel::Library::Dbee::Range)
19
+ module Burner
20
+ # Open up Burner::Jobs and add registrations for this libraries jobs.
21
+ class Jobs
22
+ register 'db_fuel/active_record/find_or_insert', DbFuel::Library::ActiveRecord::FindOrInsert
23
+ register 'db_fuel/active_record/insert', DbFuel::Library::ActiveRecord::Insert
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
27
+
28
+ register 'db_fuel/dbee/query', DbFuel::Library::Dbee::Query
29
+ register 'db_fuel/dbee/range', DbFuel::Library::Dbee::Range
30
+ end
31
+ end
@@ -0,0 +1,55 @@
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 Library
12
+ module ActiveRecord
13
+ # This job can take the objects in a register and insert them into a database table.
14
+ #
15
+ # Expected Payload[register] input: array of objects
16
+ # Payload[register] output: array of objects.
17
+ class Base < Burner::JobWithRegister
18
+ CREATED_AT = :created_at
19
+ NOW_TYPE = 'r/value/now'
20
+ UPDATED_AT = :updated_at
21
+
22
+ attr_reader :attribute_renderers,
23
+ :db_provider,
24
+ :debug,
25
+ :resolver,
26
+ :attribute_renderers_set
27
+
28
+ def initialize(
29
+ name:,
30
+ table_name:,
31
+ attributes: [],
32
+ debug: false,
33
+ register: Burner::DEFAULT_REGISTER,
34
+ separator: ''
35
+ )
36
+ super(name: name, register: register)
37
+
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)
42
+ @debug = debug || false
43
+ end
44
+
45
+ private
46
+
47
+ def debug_detail(output, message)
48
+ return unless debug
49
+
50
+ output.detail(message)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -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
@@ -0,0 +1,80 @@
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 insert them into a database table.
16
+ #
17
+ # Expected Payload[register] input: array of objects
18
+ # Payload[register] output: array of objects.
19
+ class Insert < Upsert
20
+ # attr_reader :primary_key
21
+
22
+ # Arguments:
23
+ # name [required]: name of the job within the Burner::Pipeline.
24
+ #
25
+ # table_name [required]: name of the table to use for the INSERT statements.
26
+ #
27
+ # attributes: Used to specify which object properties to put into the
28
+ # SQL statement and also allows for one last custom transformation
29
+ # pipeline, in case the data calls for SQL-specific transformers
30
+ # before insertion.
31
+ #
32
+ # debug: If debug is set to true (defaults to false) then the SQL statements and
33
+ # returned objects will be printed in the output. Only use this option while
34
+ # debugging issues as it will fill up the output with (potentially too much) data.
35
+ #
36
+ # primary_key: If primary_key is present then it will be used to set the object's
37
+ # property to the returned primary key from the INSERT statement.
38
+ #
39
+ # separator: Just like other jobs with a 'separator' option, if the objects require
40
+ # key-path notation or nested object support, you can set the separator
41
+ # to something non-blank (like a period for notation in the
42
+ # form of: name.first).
43
+ #
44
+ # timestamps: If timestamps is true (default behavior) then both created_at
45
+ # and updated_at columns will automatically have their values set
46
+ # to the current UTC timestamp.
47
+ def initialize(
48
+ name:,
49
+ table_name:,
50
+ attributes: [],
51
+ debug: false,
52
+ primary_key: nil,
53
+ register: Burner::DEFAULT_REGISTER,
54
+ separator: '',
55
+ timestamps: true
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: primary_key,
66
+ register: register,
67
+ separator: separator,
68
+ timestamps: timestamps
69
+ )
70
+ end
71
+
72
+ def perform(output, payload)
73
+ payload[register] = array(payload[register])
74
+
75
+ payload[register].each { |row| insert_record(output, row, payload.time) }
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,105 @@
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 unique objects in a register and updates them within database table.
16
+ # The attributes translate to SQL SET clauses and the unique_keys translate to
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.
20
+ #
21
+ # Expected Payload[register] input: array of objects
22
+ # Payload[register] output: array of objects.
23
+ class Update < Upsert
24
+ # Arguments:
25
+ # name [required]: name of the job within the Burner::Pipeline.
26
+ #
27
+ # table_name [required]: name of the table to use for the INSERT statements.
28
+ #
29
+ # attributes: Used to specify which object properties to put into the
30
+ # SQL statement and also allows for one last custom transformation
31
+ # pipeline, in case the data calls for SQL-specific transformers
32
+ # before mutation.
33
+ #
34
+ # debug: If debug is set to true (defaults to false) then the SQL statements and
35
+ # returned objects will be printed in the output. Only use this option while
36
+ # debugging issues as it will fill up the output with (potentially too much) data.
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
+ #
43
+ # separator: Just like other jobs with a 'separator' option, if the objects require
44
+ # key-path notation or nested object support, you can set the separator
45
+ # to something non-blank (like a period for notation in the
46
+ # form of: name.first).
47
+ #
48
+ # timestamps: If timestamps is true (default behavior) then the updated_at column will
49
+ # automatically have its value set to the current UTC timestamp.
50
+ #
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.
54
+ def initialize(
55
+ name:,
56
+ table_name:,
57
+ attributes: [],
58
+ debug: false,
59
+ primary_key: nil,
60
+ register: Burner::DEFAULT_REGISTER,
61
+ separator: '',
62
+ timestamps: true,
63
+ unique_attributes: []
64
+ )
65
+
66
+ attributes = Burner::Modeling::Attribute.array(attributes)
67
+
68
+ super(
69
+ name: name,
70
+ table_name: table_name,
71
+ attributes: attributes,
72
+ debug: debug,
73
+ primary_key: primary_key,
74
+ register: register,
75
+ separator: separator,
76
+ timestamps: timestamps,
77
+ unique_attributes: unique_attributes
78
+ )
79
+
80
+ freeze
81
+ end
82
+
83
+ def perform(output, payload)
84
+ total_rows_affected = 0
85
+
86
+ payload[register] = array(payload[register])
87
+
88
+ payload[register].each do |row|
89
+ rows_affected = 0
90
+
91
+ first_record = update_record(output, row, payload.time)
92
+
93
+ rows_affected = 1 if first_record
94
+
95
+ debug_detail(output, "Individual Rows Affected: #{rows_affected}")
96
+
97
+ total_rows_affected += rows_affected
98
+ end
99
+
100
+ output.detail("Total Rows Affected: #{total_rows_affected}")
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end