activerecord-dbt 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ module ActiveRecord
8
8
  include ActiveRecord::Dbt::Column::DataTestable::NotNullDataTestable
9
9
  include ActiveRecord::Dbt::Column::DataTestable::RelationshipsDataTestable
10
10
  include ActiveRecord::Dbt::Column::DataTestable::UniqueDataTestable
11
+ include ActiveRecord::Dbt::Validation::TableNameValidator
11
12
 
12
13
  attr_reader :table_name, :column, :primary_keys, :foreign_keys
13
14
 
@@ -15,11 +16,11 @@ module ActiveRecord
15
16
  delegate :source_config, to: :@config
16
17
 
17
18
  def initialize(table_name, column, primary_keys: [], foreign_keys: [{}])
18
- @table_name = table_name
19
+ @config = ActiveRecord::Dbt::Config.instance
20
+ @table_name = validate_table_name(table_name, @config)
19
21
  @column = column
20
22
  @primary_keys = primary_keys
21
23
  @foreign_keys = foreign_keys
22
- @config = ActiveRecord::Dbt::Config.instance
23
24
  end
24
25
 
25
26
  def properties
@@ -7,18 +7,21 @@ module ActiveRecord
7
7
  module AcceptedValuesDataTestable
8
8
  extend ActiveRecord::Dbt::RequiredMethods
9
9
 
10
+ using ActiveRecord::Dbt::CoreExt::ActiveRecordExt
11
+
10
12
  define_required_methods :@config, :column, :table_name, :column_name
11
13
 
12
- delegate :type, to: :column, prefix: true
13
14
  delegate :add_log, to: :@config
14
15
 
15
16
  def accepted_values_test
16
- return nil unless column_type == :boolean || enum_values.present?
17
+ return nil if values.blank?
17
18
 
18
19
  {
19
20
  'accepted_values' => {
20
- 'values' => values,
21
- 'quote' => quote?
21
+ 'arguments' => {
22
+ 'values' => values,
23
+ 'quote' => quote?
24
+ }
22
25
  }
23
26
  }
24
27
  end
@@ -26,11 +29,19 @@ module ActiveRecord
26
29
  private
27
30
 
28
31
  def values
29
- column_type == :boolean ? [true, false] : enum_accepted_values
32
+ if column_type == :boolean
33
+ [true, false]
34
+ elsif column.sql_type == 'tinyint(1)'
35
+ [0, 1]
36
+ elsif enum_values.present?
37
+ enum_accepted_values
38
+ else
39
+ []
40
+ end
30
41
  end
31
42
 
32
43
  def enum_accepted_values
33
- enum_values.map { |key| quote? ? key.to_s : key }
44
+ enum_values.compact.map { |key| quote? ? key.to_s : key }
34
45
  end
35
46
 
36
47
  def enum_values
@@ -48,6 +59,11 @@ module ActiveRecord
48
59
  def quote?
49
60
  @quote ||= %i[integer boolean].exclude?(column_type)
50
61
  end
62
+
63
+ # MEMO: With `delegate :type, to: :column, prefix: true` I could not rewrite the type method with a refine.
64
+ def column_type
65
+ @column_type ||= column.type
66
+ end
51
67
  end
52
68
  end
53
69
  end
@@ -11,18 +11,16 @@ module ActiveRecord
11
11
 
12
12
  define_required_methods :@config, :foreign_keys, :column_name
13
13
 
14
- delegate :source_name, :data_sync_delayed?, to: :@config
14
+ delegate :source_name, :data_sync_delayed?, :exclude_table_names, to: :@config
15
15
  delegate :to_table, to: :foreign_key
16
16
 
17
17
  def relationships_test
18
- return nil if foreign_key.blank?
18
+ return nil if foreign_key.blank? || exclude_table_names.include?(to_table)
19
19
 
20
20
  {
21
21
  'relationships' => {
22
- 'severity' => data_sync_delayed? ? 'warn' : nil,
23
- 'to' => "source('#{source_name}', '#{to_table}')",
24
- 'field' => primary_key,
25
- 'meta' => relationships_meta_relationship_type
22
+ 'arguments' => relationships_arguments,
23
+ 'config' => relationships_config
26
24
  }.compact
27
25
  }
28
26
  end
@@ -38,6 +36,22 @@ module ActiveRecord
38
36
  fk.dig(:options, :column) == column_name
39
37
  end
40
38
  end
39
+
40
+ def relationships_arguments
41
+ {
42
+ 'to' => "source('#{source_name}', '#{to_table}')",
43
+ 'field' => primary_key,
44
+ 'meta' => relationships_meta_relationship_type
45
+ }
46
+ end
47
+
48
+ def relationships_config
49
+ return nil unless data_sync_delayed?
50
+
51
+ {
52
+ 'severity' => 'warn'
53
+ }
54
+ end
41
55
  end
42
56
  end
43
57
  end
@@ -6,43 +6,60 @@ module ActiveRecord
6
6
  class Yml
7
7
  include ActiveRecord::Dbt::DataType::Mapper
8
8
  include ActiveRecord::Dbt::I18nWrapper::Translate
9
+ include ActiveRecord::Dbt::Validation::TableNameValidator
10
+
11
+ using ActiveRecord::Dbt::CoreExt::ActiveRecordExt
9
12
 
10
13
  attr_reader :table_name, :column, :column_data_test, :primary_keys
11
14
 
15
+ # MEMO: GitHub copilot taught me this.
16
+ SQL_KEYWORDS = %w[
17
+ ADD ALL ALTER AND ANY AS ASC BACKUP BETWEEN CASE CHECK COLUMN CONSTRAINT CREATE
18
+ DATABASE DEFAULT DELETE DESC DISTINCT DROP EXEC EXISTS FOREIGN FROM FULL GROUP
19
+ HAVING IN INDEX INNER INSERT INTO IS JOIN LEFT LIKE LIMIT NOT NULL OR ORDER OUTER
20
+ PRIMARY PROCEDURE RIGHT ROWNUM SELECT SET TABLE TOP TRUNCATE UNION UNIQUE UPDATE
21
+ VALUES VIEW WHERE
22
+ ].freeze
23
+
12
24
  delegate :name, :comment, to: :column, prefix: true
13
25
  delegate :source_config, to: :@config
14
26
 
15
- def initialize(table_name, column, column_data_test, primary_keys: [])
16
- @table_name = table_name
27
+ def initialize(table_name, column, column_data_test = Struct.new(:properties).new, primary_keys: [])
28
+ @config = ActiveRecord::Dbt::Config.instance
29
+ @table_name = validate_table_name(table_name, @config)
17
30
  @column = column
18
31
  @column_data_test = column_data_test
19
32
  @primary_keys = primary_keys
20
- @config = ActiveRecord::Dbt::Config.instance
21
33
  end
22
34
 
23
35
  def properties
24
36
  {
25
37
  'name' => column_name,
26
38
  'description' => description,
39
+ 'quote' => quote?,
27
40
  'data_type' => data_type(column.type),
28
41
  **column_overrides.except(:data_tests),
29
42
  'data_tests' => column_data_test.properties
30
43
  }.compact
31
44
  end
32
45
 
46
+ def column_description
47
+ config_column_description ||
48
+ translated_column_name ||
49
+ column_comment ||
50
+ key_column_name ||
51
+ default_column_description
52
+ end
53
+
33
54
  private
34
55
 
35
56
  def description
36
57
  @description ||=
37
58
  column_description ||
38
- translated_attribute_name ||
39
- column_comment ||
40
- key_column_name ||
41
- default_column_description ||
42
59
  "Write a description of the '#{table_name}.#{column_name}' column."
43
60
  end
44
61
 
45
- def column_description
62
+ def config_column_description
46
63
  source_config.dig(:table_descriptions, table_name, :columns, column_name)
47
64
  end
48
65
 
@@ -64,6 +81,11 @@ module ActiveRecord
64
81
  &.gsub(/{{\s*column_name\s*}}/, column_name)
65
82
  end
66
83
 
84
+ # MEMO: [quote | dbt Developer Hub](https://docs.getdbt.com/reference/resource-properties/quote)
85
+ def quote?
86
+ true if SQL_KEYWORDS.include?(column_name.upcase)
87
+ end
88
+
67
89
  def column_overrides
68
90
  @column_overrides ||=
69
91
  source_config.dig(:table_overrides, table_name, :columns, column_name) ||
@@ -33,6 +33,18 @@ module ActiveRecord
33
33
  end
34
34
  end
35
35
 
36
+ def project_name
37
+ @project_name ||=
38
+ source_config.dig(:sources, :config, :meta, :project_name) ||
39
+ source_name
40
+ end
41
+
42
+ def exclude_table_names
43
+ @exclude_table_names ||=
44
+ source_config.dig(:sources, :config, :meta, :exclude, :table_names) ||
45
+ []
46
+ end
47
+
36
48
  class SourceNameIsNullError < StandardError; end
37
49
  end
38
50
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module CoreExt
6
+ module ActiveRecordExt
7
+ if defined?(ActiveRecord::ConnectionAdapters::MySQL::Column)
8
+ refine ActiveRecord::ConnectionAdapters::MySQL::Column do
9
+ def type
10
+ sql_type == 'tinyint(1)' ? :integer : super
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -17,13 +17,14 @@ module ActiveRecord
17
17
  return nil unless used_dbt_utils?
18
18
 
19
19
  indexes.each_with_object([]) do |index, array|
20
- next if unique_indexes?(index)
20
+ next if single_column_index?(index)
21
21
 
22
22
  array.push(
23
23
  {
24
24
  'dbt_utils.unique_combination_of_columns' => {
25
- 'combination_of_columns' => index.columns
26
- }
25
+ 'arguments' => unique_combination_of_columns_arguments(index.columns),
26
+ 'config' => unique_combination_of_columns_config(index.columns)
27
+ }.compact
27
28
  }
28
29
  )
29
30
  end.presence
@@ -35,11 +36,38 @@ module ActiveRecord
35
36
  ActiveRecord::Base.connection.indexes(table_name)
36
37
  end
37
38
 
38
- def unique_indexes?(index)
39
+ def single_column_index?(index)
39
40
  return true if index.unique == false
40
41
 
41
42
  index.columns.size == 1
42
43
  end
44
+
45
+ def unique_combination_of_columns_arguments(combination_of_columns)
46
+ {
47
+ 'combination_of_columns' => combination_of_columns
48
+ }
49
+ end
50
+
51
+ def unique_combination_of_columns_config(combination_of_columns)
52
+ config_where = condition_for_unique_combination_of_columns(combination_of_columns)
53
+ return nil if config_where.blank?
54
+
55
+ {
56
+ 'where' => config_where
57
+ }
58
+ end
59
+
60
+ def condition_for_unique_combination_of_columns(unique_combination_of_columns)
61
+ columns.each_with_object([]) do |column, array|
62
+ if unique_combination_of_columns.include?(column.name) && column.null == true
63
+ array.push("#{column.name} is not null")
64
+ end
65
+ end.join(' and ').presence
66
+ end
67
+
68
+ def columns
69
+ ActiveRecord::Base.connection.columns(table_name)
70
+ end
43
71
  end
44
72
  end
45
73
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Factory
6
+ module Enum
7
+ module YmlFactory
8
+ def self.build(
9
+ table_name,
10
+ column_name,
11
+ primary_keys: ActiveRecord::Base.connection.primary_keys(table_name)
12
+ )
13
+ table = ActiveRecord::Dbt::Table::Yml.new(table_name)
14
+ column = ActiveRecord::Base.connection.columns(table_name).find { |c| c.name == column_name }
15
+ enum_column = ActiveRecord::Dbt::Column::Yml.new(
16
+ table_name,
17
+ column,
18
+ primary_keys: primary_keys
19
+ )
20
+
21
+ ActiveRecord::Dbt::Seed::Enum::Yml.new(table, enum_column)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -6,7 +6,10 @@ module ActiveRecord
6
6
  module Tables
7
7
  module YmlFactory
8
8
  def self.build
9
- ActiveRecord::Base.connection.tables.sort.map do |table_name|
9
+ config = ActiveRecord::Dbt::Config.instance
10
+ table_names = ActiveRecord::Base.connection.tables - config.exclude_table_names
11
+
12
+ table_names.sort.map do |table_name|
10
13
  ActiveRecord::Dbt::Factory::Table::YmlFactory.build(table_name)
11
14
  end
12
15
  end
@@ -14,14 +14,15 @@ module ActiveRecord
14
14
  I18n.t("activerecord.models.#{table_name_singularize}", default: nil)
15
15
  end
16
16
 
17
- def translated_attribute_name
18
- @translated_attribute_name ||=
19
- translated_column_name || translated_default_column_name
17
+ def translated_column_name
18
+ @translated_column_name ||=
19
+ translated_attribute_name ||
20
+ translated_default_column_name
20
21
  end
21
22
 
22
23
  private
23
24
 
24
- def translated_column_name
25
+ def translated_attribute_name
25
26
  I18n.t("activerecord.attributes.#{table_name_singularize}.#{column_name}", default: nil)
26
27
  end
27
28
 
@@ -5,6 +5,10 @@ module ActiveRecord
5
5
  module Model
6
6
  module Staging
7
7
  module Base
8
+ include ActiveRecord::Dbt::Validation::TableNameValidator
9
+
10
+ using ActiveRecord::Dbt::CoreExt::ActiveRecordExt
11
+
8
12
  SORT_COLUMN_TYPES = %w[
9
13
  ids enums
10
14
  strings texts
@@ -17,8 +21,14 @@ module ActiveRecord
17
21
  delegate :source_name, :export_directory_path, to: :@config
18
22
 
19
23
  def initialize(table_name)
20
- @table_name = table_name
21
24
  @config = ActiveRecord::Dbt::Config.instance
25
+ @table_name = validate_table_name(table_name, @config)
26
+ end
27
+
28
+ def select_column_names
29
+ columns_group_by_column_type.sort_by do |key, _|
30
+ SORT_COLUMN_TYPES.index(key)
31
+ end.to_h
22
32
  end
23
33
 
24
34
  def rename_primary_id
@@ -51,6 +61,22 @@ module ActiveRecord
51
61
  @primary_keys = ActiveRecord::Base.connection.primary_keys(table_name)
52
62
  end
53
63
 
64
+ def connection_columns
65
+ ActiveRecord::Base.connection.columns(table_name)
66
+ end
67
+
68
+ def columns_group_by_column_type
69
+ connection_columns.group_by do |column|
70
+ if id?(column.name)
71
+ 'ids'
72
+ elsif enum?(column.name)
73
+ 'enums'
74
+ else
75
+ column.type.to_s.pluralize
76
+ end
77
+ end
78
+ end
79
+
54
80
  def id?(column_name)
55
81
  primary_key?(column_name) || foreign_key?(column_name)
56
82
  end
@@ -12,30 +12,6 @@ module ActiveRecord
12
12
  def export_path
13
13
  "#{basename}.sql"
14
14
  end
15
-
16
- def select_column_names
17
- columns_group_by_column_type.sort_by do |key, _|
18
- SORT_COLUMN_TYPES.index(key)
19
- end.to_h
20
- end
21
-
22
- private
23
-
24
- def columns
25
- ActiveRecord::Base.connection.columns(table_name)
26
- end
27
-
28
- def columns_group_by_column_type
29
- columns.group_by do |column|
30
- if id?(column.name)
31
- 'ids'
32
- elsif enum?(column.name)
33
- 'enums'
34
- else
35
- column.type.to_s.pluralize
36
- end
37
- end
38
- end
39
15
  end
40
16
  end
41
17
  end
@@ -46,21 +46,12 @@ module ActiveRecord
46
46
 
47
47
  def sort_columns(columns)
48
48
  columns.sort_by do |column|
49
- [
50
- SORT_COLUMN_TYPES.index(data_type(column)) || -1,
51
- columns.index(column)
52
- ]
49
+ sort_column_names.index(column['name'])
53
50
  end
54
51
  end
55
52
 
56
- def data_type(column)
57
- if id?(column['name'])
58
- 'ids'
59
- elsif enum?(column['name'])
60
- 'enums'
61
- else
62
- column.fetch('data_type', 'unknown').pluralize
63
- end
53
+ def sort_column_names
54
+ @sort_column_names ||= select_column_names.values.flatten.map(&:name)
64
55
  end
65
56
 
66
57
  def override_columns
@@ -90,14 +81,28 @@ module ActiveRecord
90
81
  def relationships_test(column_name)
91
82
  {
92
83
  'relationships' => {
93
- 'severity' => data_sync_delayed? ? 'warn' : nil,
94
- 'to' => "source('#{source_name}', '#{table_name}')",
95
- 'field' => column_name,
96
- 'meta' => relationships_meta_relationship_type
84
+ 'arguments' => relationships_arguments(column_name),
85
+ 'config' => relationships_config
97
86
  }.compact
98
87
  }
99
88
  end
100
89
 
90
+ def relationships_arguments(column_name)
91
+ {
92
+ 'to' => "source('#{source_name}', '#{table_name}')",
93
+ 'field' => column_name,
94
+ 'meta' => relationships_meta_relationship_type
95
+ }
96
+ end
97
+
98
+ def relationships_config
99
+ return nil unless data_sync_delayed?
100
+
101
+ {
102
+ 'severity' => 'warn'
103
+ }
104
+ end
105
+
101
106
  def relationships_meta_relationship_type
102
107
  return nil unless used_dbterd?
103
108
 
@@ -5,15 +5,17 @@ module ActiveRecord
5
5
  module Seed
6
6
  module Enum
7
7
  module Base
8
+ include ActiveRecord::Dbt::Validation::TableNameValidator
9
+
8
10
  attr_reader :table_name, :enum_column_name
9
11
 
10
12
  delegate :source_name, :export_directory_path, to: :@config
11
13
  delegate :singularize, to: :table_name, prefix: true
12
14
 
13
15
  def initialize(table_name, enum_column_name)
14
- @table_name = table_name
15
- @enum_column_name = enum_column_name
16
16
  @config = ActiveRecord::Dbt::Config.instance
17
+ @table_name = validate_table_name(table_name, @config)
18
+ @enum_column_name = enum_column_name
17
19
  end
18
20
 
19
21
  private
@@ -8,13 +8,21 @@ module ActiveRecord
8
8
  include ActiveRecord::Dbt::Column::DataTestable::UniqueDataTestable
9
9
  include ActiveRecord::Dbt::Column::DataTestable::NotNullDataTestable
10
10
  include ActiveRecord::Dbt::DataType::Mapper
11
- include ActiveRecord::Dbt::I18nWrapper::Translate
12
11
  include ActiveRecord::Dbt::Seed::Enum::Base
13
12
 
14
- delegate :source_config, to: :@config
13
+ attr_reader :table, :enum_column
14
+
15
+ delegate :source_config, :project_name, to: :@config
16
+ delegate :fetch_logical_name, to: :table
15
17
 
16
18
  alias column_name enum_column_name
17
19
 
20
+ def initialize(table, enum_column)
21
+ @table = table
22
+ @enum_column = enum_column
23
+ super(table.table_name, enum_column.column_name)
24
+ end
25
+
18
26
  def export_path
19
27
  "#{basename}.yml"
20
28
  end
@@ -40,15 +48,20 @@ module ActiveRecord
40
48
  end
41
49
 
42
50
  def seed_description
43
- default_seed_description ||
44
- "#{source_name} #{translated_table_name} #{translated_attribute_name} enum".strip
51
+ source_config_description.gsub(/{{\s*project_name\s*}}/, project_name)
52
+ .gsub(/{{\s*table_logical_name\s*}}/, fetch_logical_name)
53
+ .gsub(/{{\s*column_description\s*}}/, column_description)
54
+ end
55
+
56
+ def source_config_description
57
+ source_config.dig(:defaults, :seed_descriptions, :enum, :description) ||
58
+ '{{ project_name }} {{ table_logical_name }} {{ column_description }} enum'
45
59
  end
46
60
 
47
- def default_seed_description
48
- source_config.dig(:defaults, :seed_descriptions, :enum, :description)
49
- &.gsub(/{{\s*source_name\s*}}/, source_name)
50
- &.gsub(/{{\s*translated_table_name\s*}}/, translated_table_name)
51
- &.gsub(/{{\s*translated_attribute_name\s*}}/, translated_attribute_name)
61
+ def column_description
62
+ @column_description ||=
63
+ enum_column.column_description ||
64
+ enum_column_name
52
65
  end
53
66
 
54
67
  def column_types
@@ -80,7 +93,8 @@ module ActiveRecord
80
93
  def before_type_of_cast_column
81
94
  {
82
95
  'name' => "#{enum_column_name}_before_type_of_cast",
83
- 'description' => translated_attribute_name,
96
+ 'description' => "#{column_description}(before_type_of_cast)",
97
+ 'data_type' => data_type(before_type_of_cast_type),
84
98
  'data_tests' => data_tests
85
99
  }.compact
86
100
  end
@@ -88,7 +102,8 @@ module ActiveRecord
88
102
  def enum_key_column
89
103
  {
90
104
  'name' => "#{enum_column_name}_key",
91
- 'description' => "#{translated_attribute_name}(key)",
105
+ 'description' => "#{column_description}(key)",
106
+ 'data_type' => data_type(:string),
92
107
  'data_tests' => data_tests
93
108
  }.compact
94
109
  end
@@ -98,7 +113,8 @@ module ActiveRecord
98
113
  array.push(
99
114
  {
100
115
  'name' => "#{enum_column_name}_#{locale}",
101
- 'description' => "#{translated_attribute_name}(#{locale})",
116
+ 'description' => "#{column_description}(#{locale})",
117
+ 'data_type' => data_type(:string),
102
118
  'data_tests' => data_tests
103
119
  }.compact
104
120
  )
@@ -4,11 +4,13 @@ module ActiveRecord
4
4
  module Dbt
5
5
  module Table
6
6
  module Base
7
+ include ActiveRecord::Dbt::Validation::TableNameValidator
8
+
7
9
  attr_reader :table_name
8
10
 
9
11
  def initialize(table_name)
10
- @table_name = table_name
11
12
  @config = ActiveRecord::Dbt::Config.instance
13
+ @table_name = validate_table_name(table_name, @config)
12
14
  end
13
15
  end
14
16
  end