activerecord-dbt 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +921 -0
- data/Rakefile +5 -0
- data/lib/active_record/dbt/column/column.rb +84 -0
- data/lib/active_record/dbt/column/test.rb +61 -0
- data/lib/active_record/dbt/column/testable/accepted_values_testable.rb +59 -0
- data/lib/active_record/dbt/column/testable/not_null_testable.rb +23 -0
- data/lib/active_record/dbt/column/testable/relationships_testable.rb +49 -0
- data/lib/active_record/dbt/column/testable/unique_testable.rb +37 -0
- data/lib/active_record/dbt/config.rb +45 -0
- data/lib/active_record/dbt/configuration/data_sync.rb +15 -0
- data/lib/active_record/dbt/configuration/logger.rb +41 -0
- data/lib/active_record/dbt/configuration/parser.rb +15 -0
- data/lib/active_record/dbt/configuration/used_dbt_package.rb +25 -0
- data/lib/active_record/dbt/dbt_package/dbt_utils/table/testable/unique_combination_of_columns_testable.rb +42 -0
- data/lib/active_record/dbt/dbt_package/dbterd/column/testable/relationships_meta_relationship_type.rb +138 -0
- data/lib/active_record/dbt/factory/columns_factory.rb +31 -0
- data/lib/active_record/dbt/factory/model/staging_factory.rb +22 -0
- data/lib/active_record/dbt/factory/source_factory.rb +16 -0
- data/lib/active_record/dbt/factory/table_factory.rb +16 -0
- data/lib/active_record/dbt/factory/tables_factory.rb +15 -0
- data/lib/active_record/dbt/model/staging/base.rb +73 -0
- data/lib/active_record/dbt/model/staging/sql.rb +43 -0
- data/lib/active_record/dbt/model/staging/yml.rb +108 -0
- data/lib/active_record/dbt/railtie.rb +8 -0
- data/lib/active_record/dbt/source/yml.rb +37 -0
- data/lib/active_record/dbt/table/base.rb +16 -0
- data/lib/active_record/dbt/table/test.rb +19 -0
- data/lib/active_record/dbt/table/yml.rb +75 -0
- data/lib/active_record/dbt/version.rb +7 -0
- data/lib/active_record/dbt.rb +17 -0
- data/lib/generators/active_record/dbt/config/USAGE +9 -0
- data/lib/generators/active_record/dbt/config/config_generator.rb +30 -0
- data/lib/generators/active_record/dbt/config/templates/source_config.yml.tt +68 -0
- data/lib/generators/active_record/dbt/initializer/USAGE +8 -0
- data/lib/generators/active_record/dbt/initializer/initializer_generator.rb +15 -0
- data/lib/generators/active_record/dbt/initializer/templates/dbt.rb +10 -0
- data/lib/generators/active_record/dbt/source/USAGE +8 -0
- data/lib/generators/active_record/dbt/source/source_generator.rb +22 -0
- data/lib/generators/active_record/dbt/staging_model/USAGE +9 -0
- data/lib/generators/active_record/dbt/staging_model/staging_model_generator.rb +38 -0
- data/lib/generators/active_record/dbt/staging_model/templates/staging_model.sql.tt +29 -0
- data/lib/tasks/active_record/dbt_tasks.rake +6 -0
- metadata +133 -0
data/Rakefile
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Dbt
|
5
|
+
module Column
|
6
|
+
class Column
|
7
|
+
attr_reader :table_name, :column, :column_test, :primary_keys
|
8
|
+
|
9
|
+
delegate :name, :comment, to: :column, prefix: true
|
10
|
+
delegate :source_config, to: :@config
|
11
|
+
|
12
|
+
def initialize(table_name, column, column_test, primary_keys: [])
|
13
|
+
@table_name = table_name
|
14
|
+
@column = column
|
15
|
+
@column_test = column_test
|
16
|
+
@primary_keys = primary_keys
|
17
|
+
@config = ActiveRecord::Dbt::Config.instance
|
18
|
+
end
|
19
|
+
|
20
|
+
def config
|
21
|
+
{
|
22
|
+
'name' => column_name,
|
23
|
+
'description' => description,
|
24
|
+
'meta' => { 'column_type' => column.type.to_s },
|
25
|
+
**column_overrides.except(:tests),
|
26
|
+
'tests' => column_test.config
|
27
|
+
}.compact
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def description
|
33
|
+
@description ||=
|
34
|
+
column_description ||
|
35
|
+
translated_attribute_name ||
|
36
|
+
column_comment ||
|
37
|
+
key_column_name ||
|
38
|
+
default_column_description ||
|
39
|
+
"Write a description of the '#{table_name}.#{column_name}' column."
|
40
|
+
end
|
41
|
+
|
42
|
+
def column_description
|
43
|
+
source_config.dig(:table_descriptions, table_name, :columns, column_name)
|
44
|
+
end
|
45
|
+
|
46
|
+
def translated_attribute_name
|
47
|
+
translated_column_name || translated_default_column_name
|
48
|
+
end
|
49
|
+
|
50
|
+
def translated_column_name
|
51
|
+
I18n.t("activerecord.attributes.#{table_name.singularize}.#{column_name}", default: nil)
|
52
|
+
end
|
53
|
+
|
54
|
+
def translated_default_column_name
|
55
|
+
I18n.t("attributes.#{column_name}", default: nil)
|
56
|
+
end
|
57
|
+
|
58
|
+
def key_column_name
|
59
|
+
column_name if primary_key? || foreign_key?
|
60
|
+
end
|
61
|
+
|
62
|
+
def primary_key?
|
63
|
+
primary_keys.include?(column_name)
|
64
|
+
end
|
65
|
+
|
66
|
+
def foreign_key?
|
67
|
+
ActiveRecord::Base.connection.foreign_key_exists?(table_name, column: column_name)
|
68
|
+
end
|
69
|
+
|
70
|
+
def default_column_description
|
71
|
+
source_config.dig(:defaults, :table_descriptions, :columns, :description)
|
72
|
+
&.gsub(/{{\s*table_name\s*}}/, table_name)
|
73
|
+
&.gsub(/{{\s*column_name\s*}}/, column_name)
|
74
|
+
end
|
75
|
+
|
76
|
+
def column_overrides
|
77
|
+
@column_overrides ||=
|
78
|
+
source_config.dig(:table_overrides, table_name, :columns, column_name) ||
|
79
|
+
{}
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Dbt
|
5
|
+
module Column
|
6
|
+
class Test
|
7
|
+
include ActiveRecord::Dbt::Column::Testable::AcceptedValuesTestable
|
8
|
+
include ActiveRecord::Dbt::Column::Testable::NotNullTestable
|
9
|
+
include ActiveRecord::Dbt::Column::Testable::RelationshipsTestable
|
10
|
+
include ActiveRecord::Dbt::Column::Testable::UniqueTestable
|
11
|
+
|
12
|
+
attr_reader :table_name, :column, :primary_keys, :foreign_keys
|
13
|
+
|
14
|
+
delegate :name, to: :column, prefix: true
|
15
|
+
delegate :source_config, to: :@config
|
16
|
+
|
17
|
+
def initialize(table_name, column, primary_keys: [], foreign_keys: [{}])
|
18
|
+
@table_name = table_name
|
19
|
+
@column = column
|
20
|
+
@primary_keys = primary_keys
|
21
|
+
@foreign_keys = foreign_keys
|
22
|
+
@config = ActiveRecord::Dbt::Config.instance
|
23
|
+
end
|
24
|
+
|
25
|
+
def config
|
26
|
+
(tests.keys | tests_overrides_hash.keys).map do |key|
|
27
|
+
tests_overrides_hash[key] || tests[key]
|
28
|
+
end.presence
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def tests
|
34
|
+
{
|
35
|
+
'unique_test' => unique_test,
|
36
|
+
'not_null_test' => not_null_test,
|
37
|
+
'relationships_test' => relationships_test,
|
38
|
+
'accepted_values_test' => accepted_values_test
|
39
|
+
}.compact
|
40
|
+
end
|
41
|
+
|
42
|
+
def tests_overrides_hash
|
43
|
+
@tests_overrides_hash ||=
|
44
|
+
tests_overrides.index_by do |tests_override|
|
45
|
+
"#{extract_key(tests_override)}_test"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def extract_key(item)
|
50
|
+
item.is_a?(Hash) ? item.keys.first : item
|
51
|
+
end
|
52
|
+
|
53
|
+
def tests_overrides
|
54
|
+
@tests_overrides ||=
|
55
|
+
source_config.dig(:table_overrides, table_name, :columns, column_name, :tests) ||
|
56
|
+
[]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Dbt
|
5
|
+
module Column
|
6
|
+
module Testable
|
7
|
+
module AcceptedValuesTestable
|
8
|
+
REQUIRED_ACCEPTED_VALUES_TESTABLE_METHODS = %i[@config column table_name column_name].freeze
|
9
|
+
|
10
|
+
delegate :type, to: :column, prefix: true
|
11
|
+
delegate :add_log, to: :@config
|
12
|
+
|
13
|
+
REQUIRED_ACCEPTED_VALUES_TESTABLE_METHODS.each do |method_name|
|
14
|
+
define_method(method_name) do
|
15
|
+
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def accepted_values_test
|
20
|
+
return nil unless column_type == :boolean || enum_values.present?
|
21
|
+
|
22
|
+
{
|
23
|
+
'accepted_values' => {
|
24
|
+
'values' => values,
|
25
|
+
'quote' => quote?
|
26
|
+
}
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def values
|
33
|
+
column_type == :boolean ? [true, false] : enum_accepted_values
|
34
|
+
end
|
35
|
+
|
36
|
+
def enum_accepted_values
|
37
|
+
enum_values.map { |key| quote? ? key.to_s : key }
|
38
|
+
end
|
39
|
+
|
40
|
+
def enum_values
|
41
|
+
@enum_values ||= enums[column_name]&.values
|
42
|
+
end
|
43
|
+
|
44
|
+
def enums
|
45
|
+
table_name.singularize.classify.constantize.defined_enums
|
46
|
+
rescue NameError => e
|
47
|
+
add_log(self.class, e)
|
48
|
+
|
49
|
+
{}
|
50
|
+
end
|
51
|
+
|
52
|
+
def quote?
|
53
|
+
@quote ||= %i[integer boolean].exclude?(column_type)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Dbt
|
5
|
+
module Column
|
6
|
+
module Testable
|
7
|
+
module NotNullTestable
|
8
|
+
REQUIRED_NOT_NULL_TESTABLE_METHODS = %i[column].freeze
|
9
|
+
|
10
|
+
REQUIRED_NOT_NULL_TESTABLE_METHODS.each do |method_name|
|
11
|
+
define_method(method_name) do
|
12
|
+
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def not_null_test
|
17
|
+
column.null == true ? nil : 'not_null'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Dbt
|
5
|
+
module Column
|
6
|
+
module Testable
|
7
|
+
module RelationshipsTestable
|
8
|
+
REQUIRED_RELATIONSHIPS_TESTABLE_METHODS = %i[@config foreign_keys column_name].freeze
|
9
|
+
|
10
|
+
include ActiveRecord::Dbt::DbtPackage::Dbterd::Column::Testable::RelationshipsMetaRelationshipType
|
11
|
+
|
12
|
+
delegate :source_name, :data_sync_delayed?, to: :@config
|
13
|
+
delegate :to_table, to: :foreign_key
|
14
|
+
|
15
|
+
REQUIRED_RELATIONSHIPS_TESTABLE_METHODS.each do |method_name|
|
16
|
+
define_method(method_name) do
|
17
|
+
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def relationships_test
|
22
|
+
return nil if foreign_key.blank?
|
23
|
+
|
24
|
+
{
|
25
|
+
'relationships' => {
|
26
|
+
'severity' => data_sync_delayed? ? 'warn' : nil,
|
27
|
+
'to' => "source('#{source_name}', '#{to_table}')",
|
28
|
+
'field' => primary_key,
|
29
|
+
'meta' => relationships_meta_relationship_type
|
30
|
+
}.compact
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def primary_key
|
37
|
+
foreign_key.dig(:options, :primary_key)
|
38
|
+
end
|
39
|
+
|
40
|
+
def foreign_key
|
41
|
+
@foreign_key ||= foreign_keys.find do |fk|
|
42
|
+
fk.dig(:options, :column) == column_name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Dbt
|
5
|
+
module Column
|
6
|
+
module Testable
|
7
|
+
module UniqueTestable
|
8
|
+
REQUIRED_UNIQUE_TESTABLE_METHODS = %i[table_name column_name primary_keys].freeze
|
9
|
+
|
10
|
+
REQUIRED_UNIQUE_TESTABLE_METHODS.each do |method_name|
|
11
|
+
define_method(method_name) do
|
12
|
+
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def unique_test
|
17
|
+
unique? ? 'unique' : nil
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def unique?
|
23
|
+
primary_keys.include?(column_name) || unique_columns.include?(column_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def unique_columns
|
27
|
+
ActiveRecord::Base.connection.indexes(table_name).each_with_object([]) do |index, array|
|
28
|
+
if index.unique == true && (unique_indexes = index.columns).size == 1
|
29
|
+
array << unique_indexes.first
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module Dbt
|
7
|
+
class Config
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
include ActiveRecord::Dbt::Configuration::DataSync
|
11
|
+
include ActiveRecord::Dbt::Configuration::Logger
|
12
|
+
include ActiveRecord::Dbt::Configuration::Parser
|
13
|
+
include ActiveRecord::Dbt::Configuration::UsedDbtPackage
|
14
|
+
|
15
|
+
DEFAULT_CONFIG_DIRECTORY_PATH = 'lib/dbt'
|
16
|
+
DEFAULT_EXPORT_DIRECTORY_PATH = 'doc/dbt'
|
17
|
+
|
18
|
+
attr_writer :config_directory_path, :export_directory_path
|
19
|
+
|
20
|
+
def source_config_path
|
21
|
+
@source_config_path ||= "#{config_directory_path}/source_config.yml"
|
22
|
+
end
|
23
|
+
|
24
|
+
def source_config
|
25
|
+
@source_config ||= parse_yaml(source_config_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def source_name
|
29
|
+
@source_name ||= source_config.dig(:sources, :name).tap do |source_name|
|
30
|
+
raise SourceNameIsNullError, "'sources.name' is required in '#{source_config_path}'." if source_name.nil?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def config_directory_path
|
35
|
+
@config_directory_path ||= DEFAULT_CONFIG_DIRECTORY_PATH
|
36
|
+
end
|
37
|
+
|
38
|
+
def export_directory_path
|
39
|
+
@export_directory_path ||= DEFAULT_EXPORT_DIRECTORY_PATH
|
40
|
+
end
|
41
|
+
|
42
|
+
class SourceNameIsNullError < StandardError; end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Dbt
|
5
|
+
module Configuration
|
6
|
+
module Logger
|
7
|
+
DEFAULT_LOG_FILE_PATH = './log/active_record_dbt.log'
|
8
|
+
EXCLUDE_EXCEPTION_CLASS_NAMES = %w[ArInternalMetadatum SchemaMigration].freeze
|
9
|
+
|
10
|
+
attr_writer :logger
|
11
|
+
|
12
|
+
def logger
|
13
|
+
@logger ||= ::Logger.new(DEFAULT_LOG_FILE_PATH)
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_log(class_name, exception)
|
17
|
+
return if include_exception_class_names?(exception)
|
18
|
+
|
19
|
+
logger.info(class_name) { format_log(exception) }
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def include_exception_class_names?(exception)
|
25
|
+
exception.instance_of?(NameError) &&
|
26
|
+
EXCLUDE_EXCEPTION_CLASS_NAMES.include?(exception.name.to_s)
|
27
|
+
end
|
28
|
+
|
29
|
+
def format_log(exception)
|
30
|
+
{
|
31
|
+
exception: [
|
32
|
+
exception.class,
|
33
|
+
exception.message
|
34
|
+
],
|
35
|
+
exception_backtrace: exception.backtrace.first(5)
|
36
|
+
}.to_json
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Dbt
|
5
|
+
module Configuration
|
6
|
+
module UsedDbtPackage
|
7
|
+
attr_writer :used_dbt_package_names
|
8
|
+
|
9
|
+
def used_dbt_utils?
|
10
|
+
used_dbt_package_names.include?('dbt-labs/dbt_utils')
|
11
|
+
end
|
12
|
+
|
13
|
+
def used_dbterd?
|
14
|
+
used_dbt_package_names.include?('datnguye/dbterd')
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def used_dbt_package_names
|
20
|
+
@used_dbt_package_names ||= []
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Dbt
|
5
|
+
module DbtPackage
|
6
|
+
module DbtUtils
|
7
|
+
module Table
|
8
|
+
module Testable
|
9
|
+
module UniqueCombinationOfColumnsTestable
|
10
|
+
REQUIRED_UNIQUE_COMBINATION_OF_COLUMNS_TESTABLE_METHODS = %i[table_name @config].freeze
|
11
|
+
|
12
|
+
delegate :used_dbt_utils?, to: :@config
|
13
|
+
|
14
|
+
REQUIRED_UNIQUE_COMBINATION_OF_COLUMNS_TESTABLE_METHODS.each do |method_name|
|
15
|
+
define_method(method_name) do
|
16
|
+
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def unique_combination_of_columns_test
|
21
|
+
return nil unless used_dbt_utils?
|
22
|
+
|
23
|
+
ActiveRecord::Base.connection.indexes(table_name).each_with_object([]) do |index, array|
|
24
|
+
next if index.unique == false
|
25
|
+
next if (unique_indexes = index.columns).size == 1
|
26
|
+
|
27
|
+
array.push(
|
28
|
+
{
|
29
|
+
'dbt_utils.unique_combination_of_columns' => {
|
30
|
+
'combination_of_columns' => unique_indexes
|
31
|
+
}
|
32
|
+
}
|
33
|
+
)
|
34
|
+
end.presence
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Dbt
|
5
|
+
module DbtPackage
|
6
|
+
module Dbterd
|
7
|
+
module Column
|
8
|
+
module Testable
|
9
|
+
module RelationshipsMetaRelationshipType
|
10
|
+
REQUIRED_RELATIONSHIP_TYPE_TESTABLE_METHODS = %i[@config foreign_key].freeze
|
11
|
+
|
12
|
+
delegate :used_dbterd?, :add_log, to: :@config
|
13
|
+
|
14
|
+
REQUIRED_RELATIONSHIP_TYPE_TESTABLE_METHODS.each do |method_name|
|
15
|
+
define_method(method_name) do
|
16
|
+
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def relationships_meta_relationship_type
|
21
|
+
return nil unless used_dbterd?
|
22
|
+
return nil if foreign_key.nil? || relationship_type.blank?
|
23
|
+
|
24
|
+
{
|
25
|
+
'relationship_type' => relationship_type
|
26
|
+
}
|
27
|
+
rescue NotSpecifiedOrNotInvalidIdError, StandardError => e
|
28
|
+
add_log(self.class, e)
|
29
|
+
|
30
|
+
{
|
31
|
+
'relationship_type' => 'many-to-one',
|
32
|
+
'active_record_dbt_error' => {
|
33
|
+
'class' => e.class.to_s,
|
34
|
+
'message' => e.message.to_s
|
35
|
+
}
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# MEMO: It seems to be a good idea to set only 'many-to-one', 'one-to-one', and 'many-to-one'.
|
42
|
+
# * [Relationship Types - DaaC from dbt artifacts](https://dbterd.datnguyen.de/1.13/nav/metadata/relationship_type.html)
|
43
|
+
# * [Syntax | DBML](https://dbml.dbdiagram.io/docs/#relationships--foreign-key-definitions)
|
44
|
+
# * DBML supports 'one-to-many', 'many-to-one', 'one-to-one', and 'many-to-many'.
|
45
|
+
def relationship_type
|
46
|
+
# if one_to_many?
|
47
|
+
# 'one-to-many'
|
48
|
+
# elsif zero_to_many?
|
49
|
+
# 'zero-to-many'
|
50
|
+
# elsif many_to_many?
|
51
|
+
# 'many-to-many'
|
52
|
+
# elsif one_to_one?
|
53
|
+
if one_to_one?
|
54
|
+
'one-to-one'
|
55
|
+
elsif many_to_one?
|
56
|
+
'many-to-one'
|
57
|
+
else
|
58
|
+
raise NotSpecifiedOrNotInvalidIdError, 'Not specified/Invalid value'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# MEMO: If 'many-to-one' is specified, 'one-to-many' should not be necessary.
|
63
|
+
# def one_to_many?
|
64
|
+
# end
|
65
|
+
|
66
|
+
# MEMO:
|
67
|
+
# * It seems that `zero-to-many` cannot be specified in a dbt relationship.
|
68
|
+
# * The reverse may be possible, but cannot be specified in dbterd.
|
69
|
+
# * It doesn't look like it can be configured with dbml.
|
70
|
+
# * [Syntax | DBML](https://dbml.dbdiagram.io/docs/#relationships--foreign-key-definitions)
|
71
|
+
# def zero_to_many?
|
72
|
+
# end
|
73
|
+
|
74
|
+
# # TODO: Usually, there is always an intermediate table, so there is no `many-to-many`.
|
75
|
+
# def many_to_many?
|
76
|
+
# from_model_find_association_to_model?(:has_and_belongs_to_many) &&
|
77
|
+
# to_model_find_association_from_model?(:has_and_belongs_to_many)
|
78
|
+
# end
|
79
|
+
|
80
|
+
def one_to_one?
|
81
|
+
from_model_find_association_to_model?(:belongs_to) &&
|
82
|
+
to_model_find_association_from_model?(:has_one)
|
83
|
+
end
|
84
|
+
|
85
|
+
def many_to_one?
|
86
|
+
from_model_find_association_to_model?(:belongs_to) &&
|
87
|
+
to_model_find_association_from_model?(:has_many)
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_model_find_association_from_model?(association_type)
|
91
|
+
to_model.reflect_on_all_associations(association_type).any? do |association|
|
92
|
+
association_klass(association) == from_model &&
|
93
|
+
association_foreign_key(association) == foreign_key_column
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def from_model_find_association_to_model?(association_type)
|
98
|
+
from_model.reflect_on_all_associations(association_type).any? do |association|
|
99
|
+
association_klass(association) == to_model &&
|
100
|
+
association_foreign_key(association) == foreign_key_column
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def association_klass(association)
|
105
|
+
association.klass
|
106
|
+
rescue NoMethodError
|
107
|
+
association.options.fetch(:through).to_s.classify.constantize
|
108
|
+
end
|
109
|
+
|
110
|
+
def association_foreign_key(association)
|
111
|
+
association.foreign_key
|
112
|
+
rescue NoMethodError
|
113
|
+
association.options.fetch(
|
114
|
+
:foreign_key,
|
115
|
+
"#{association.active_record.to_s.underscore}_id"
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
def foreign_key_column
|
120
|
+
foreign_key.dig(:options, :column)
|
121
|
+
end
|
122
|
+
|
123
|
+
def from_model
|
124
|
+
@from_model ||= foreign_key.from_table.classify.constantize
|
125
|
+
end
|
126
|
+
|
127
|
+
def to_model
|
128
|
+
@to_model ||= foreign_key.to_table.classify.constantize
|
129
|
+
end
|
130
|
+
|
131
|
+
class NotSpecifiedOrNotInvalidIdError < StandardError; end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Dbt
|
5
|
+
module Factory
|
6
|
+
module ColumnsFactory
|
7
|
+
def self.build(
|
8
|
+
table_name,
|
9
|
+
primary_keys: ActiveRecord::Base.connection.primary_keys(table_name),
|
10
|
+
foreign_keys: ActiveRecord::Base.connection.foreign_keys(table_name)
|
11
|
+
)
|
12
|
+
ActiveRecord::Base.connection.columns(table_name).map do |column|
|
13
|
+
column_test = ActiveRecord::Dbt::Column::Test.new(
|
14
|
+
table_name,
|
15
|
+
column,
|
16
|
+
primary_keys: primary_keys,
|
17
|
+
foreign_keys: foreign_keys
|
18
|
+
)
|
19
|
+
|
20
|
+
ActiveRecord::Dbt::Column::Column.new(
|
21
|
+
table_name,
|
22
|
+
column,
|
23
|
+
column_test,
|
24
|
+
primary_keys: primary_keys
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|