activerecord-dbt 0.1.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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +921 -0
  4. data/Rakefile +5 -0
  5. data/lib/active_record/dbt/column/column.rb +84 -0
  6. data/lib/active_record/dbt/column/test.rb +61 -0
  7. data/lib/active_record/dbt/column/testable/accepted_values_testable.rb +59 -0
  8. data/lib/active_record/dbt/column/testable/not_null_testable.rb +23 -0
  9. data/lib/active_record/dbt/column/testable/relationships_testable.rb +49 -0
  10. data/lib/active_record/dbt/column/testable/unique_testable.rb +37 -0
  11. data/lib/active_record/dbt/config.rb +45 -0
  12. data/lib/active_record/dbt/configuration/data_sync.rb +15 -0
  13. data/lib/active_record/dbt/configuration/logger.rb +41 -0
  14. data/lib/active_record/dbt/configuration/parser.rb +15 -0
  15. data/lib/active_record/dbt/configuration/used_dbt_package.rb +25 -0
  16. data/lib/active_record/dbt/dbt_package/dbt_utils/table/testable/unique_combination_of_columns_testable.rb +42 -0
  17. data/lib/active_record/dbt/dbt_package/dbterd/column/testable/relationships_meta_relationship_type.rb +138 -0
  18. data/lib/active_record/dbt/factory/columns_factory.rb +31 -0
  19. data/lib/active_record/dbt/factory/model/staging_factory.rb +22 -0
  20. data/lib/active_record/dbt/factory/source_factory.rb +16 -0
  21. data/lib/active_record/dbt/factory/table_factory.rb +16 -0
  22. data/lib/active_record/dbt/factory/tables_factory.rb +15 -0
  23. data/lib/active_record/dbt/model/staging/base.rb +73 -0
  24. data/lib/active_record/dbt/model/staging/sql.rb +43 -0
  25. data/lib/active_record/dbt/model/staging/yml.rb +108 -0
  26. data/lib/active_record/dbt/railtie.rb +8 -0
  27. data/lib/active_record/dbt/source/yml.rb +37 -0
  28. data/lib/active_record/dbt/table/base.rb +16 -0
  29. data/lib/active_record/dbt/table/test.rb +19 -0
  30. data/lib/active_record/dbt/table/yml.rb +75 -0
  31. data/lib/active_record/dbt/version.rb +7 -0
  32. data/lib/active_record/dbt.rb +17 -0
  33. data/lib/generators/active_record/dbt/config/USAGE +9 -0
  34. data/lib/generators/active_record/dbt/config/config_generator.rb +30 -0
  35. data/lib/generators/active_record/dbt/config/templates/source_config.yml.tt +68 -0
  36. data/lib/generators/active_record/dbt/initializer/USAGE +8 -0
  37. data/lib/generators/active_record/dbt/initializer/initializer_generator.rb +15 -0
  38. data/lib/generators/active_record/dbt/initializer/templates/dbt.rb +10 -0
  39. data/lib/generators/active_record/dbt/source/USAGE +8 -0
  40. data/lib/generators/active_record/dbt/source/source_generator.rb +22 -0
  41. data/lib/generators/active_record/dbt/staging_model/USAGE +9 -0
  42. data/lib/generators/active_record/dbt/staging_model/staging_model_generator.rb +38 -0
  43. data/lib/generators/active_record/dbt/staging_model/templates/staging_model.sql.tt +29 -0
  44. data/lib/tasks/active_record/dbt_tasks.rake +6 -0
  45. metadata +133 -0
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Factory
6
+ module Model
7
+ module StagingFactory
8
+ def self.yml_build(table_name)
9
+ table_factory = ActiveRecord::Dbt::Factory::TableFactory.build(table_name)
10
+ yml = ActiveRecord::Dbt::Model::Staging::Yml.new(table_factory)
11
+ struct = Struct.new(:export_path, :yml_dump, keyword_init: true)
12
+
13
+ struct.new(
14
+ export_path: yml.export_path,
15
+ yml_dump: YAML.dump(yml.config.deep_stringify_keys)
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Factory
6
+ module SourceFactory
7
+ def self.build
8
+ tables_factory = ActiveRecord::Dbt::Factory::TablesFactory.build
9
+ config = ActiveRecord::Dbt::Source::Yml.new(tables_factory).config
10
+
11
+ YAML.dump(config.deep_stringify_keys)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Factory
6
+ module TableFactory
7
+ def self.build(table_name)
8
+ table_test = ActiveRecord::Dbt::Table::Test.new(table_name)
9
+ columns = ActiveRecord::Dbt::Factory::ColumnsFactory.build(table_name)
10
+
11
+ ActiveRecord::Dbt::Table::Yml.new(table_name, table_test, columns)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Factory
6
+ module TablesFactory
7
+ def self.build
8
+ ActiveRecord::Base.connection.tables.sort.map do |table_name|
9
+ ActiveRecord::Dbt::Factory::TableFactory.build(table_name)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Model
6
+ module Staging
7
+ module Base
8
+ SORT_COLUMN_TYPES = %w[
9
+ ids enums
10
+ strings texts
11
+ integers floats decimals
12
+ binaries booleans
13
+ dates times
14
+ datetimes timestamps
15
+ ].freeze
16
+
17
+ delegate :source_name, :export_directory_path, to: :@config
18
+
19
+ def initialize(table_name)
20
+ @table_name = table_name
21
+ @config = ActiveRecord::Dbt::Config.instance
22
+ end
23
+
24
+ def rename_primary_id
25
+ @rename_primary_id ||= primary_key_eql_id? ? "#{table_name.singularize}_id" : nil
26
+ end
27
+
28
+ def primary_key_eql_id?
29
+ single_column_primary_key? && primary_keys.first == 'id'
30
+ end
31
+
32
+ def primary_key?(column_name)
33
+ primary_keys.include?(column_name)
34
+ end
35
+
36
+ private
37
+
38
+ def basename
39
+ "#{export_directory_path}/#{model_name}"
40
+ end
41
+
42
+ def model_name
43
+ "stg_#{source_name}__#{table_name}"
44
+ end
45
+
46
+ def single_column_primary_key?
47
+ primary_keys.size == 1
48
+ end
49
+
50
+ def primary_keys
51
+ @primary_keys = ActiveRecord::Base.connection.primary_keys(table_name)
52
+ end
53
+
54
+ def id?(column_name)
55
+ primary_key?(column_name) || foreign_key?(column_name)
56
+ end
57
+
58
+ def enum?(column_name)
59
+ table_name.singularize.classify.constantize.defined_enums.include?(column_name)
60
+ rescue NameError => e
61
+ add_log(self.class, e)
62
+
63
+ false
64
+ end
65
+
66
+ def foreign_key?(column_name)
67
+ ActiveRecord::Base.connection.foreign_key_exists?(table_name, column: column_name)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Model
6
+ module Staging
7
+ class Sql
8
+ include ActiveRecord::Dbt::Model::Staging::Base
9
+
10
+ attr_reader :table_name
11
+
12
+ def export_path
13
+ "#{basename}.sql"
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
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Model
6
+ module Staging
7
+ class Yml
8
+ include ActiveRecord::Dbt::Model::Staging::Base
9
+
10
+ attr_reader :table
11
+
12
+ delegate :table_name, to: :table
13
+ delegate :source_name, :data_sync_delayed?, :used_dbterd?, to: :@config
14
+
15
+ def initialize(table)
16
+ @table = table
17
+ super(table_name)
18
+ end
19
+
20
+ def export_path
21
+ "#{basename}.yml"
22
+ end
23
+
24
+ def config
25
+ {
26
+ 'version' => 2,
27
+ 'models' => [
28
+ {
29
+ 'name' => model_name,
30
+ **table.config.except('name', 'columns'),
31
+ 'columns' => override_columns
32
+ }
33
+ ]
34
+ }
35
+ end
36
+
37
+ private
38
+
39
+ def columns
40
+ @columns ||= sort_columns(table.config['columns'])
41
+ end
42
+
43
+ def sort_columns(columns)
44
+ columns.sort_by do |column|
45
+ [
46
+ SORT_COLUMN_TYPES.index(column_type(column)) || -1,
47
+ columns.index(column)
48
+ ]
49
+ end
50
+ end
51
+
52
+ def column_type(column)
53
+ if id?(column['name'])
54
+ 'ids'
55
+ elsif enum?(column['name'])
56
+ 'enums'
57
+ else
58
+ column.dig('meta', 'column_type').pluralize
59
+ end
60
+ end
61
+
62
+ def override_columns
63
+ return columns unless single_column_primary_key?
64
+
65
+ columns.map { |column| override_column(column) }
66
+ end
67
+
68
+ def override_column(column)
69
+ return column unless primary_key?(column['name'])
70
+
71
+ column.tap do |c|
72
+ add_relationship_test(c)
73
+ rename_primary_id_in_column(c) if primary_key_eql_id?
74
+ end
75
+ end
76
+
77
+ def add_relationship_test(column)
78
+ column['tests'].push(relationships_test(column['name']))
79
+ end
80
+
81
+ def rename_primary_id_in_column(column)
82
+ column['description'] = rename_primary_id if column['name'] == column['description']
83
+ column['name'] = rename_primary_id
84
+ end
85
+
86
+ def relationships_test(column_name)
87
+ {
88
+ 'relationships' => {
89
+ 'severity' => data_sync_delayed? ? 'warn' : nil,
90
+ 'to' => "source('#{source_name}', '#{table_name}')",
91
+ 'field' => column_name,
92
+ 'meta' => relationships_meta_relationship_type
93
+ }.compact
94
+ }
95
+ end
96
+
97
+ def relationships_meta_relationship_type
98
+ return nil unless used_dbterd?
99
+
100
+ {
101
+ 'relationship_type' => 'one-to-one'
102
+ }
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ class Railtie < ::Rails::Railtie
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Source
6
+ class Yml
7
+ attr_reader :tables
8
+
9
+ delegate :source_config, to: :@config
10
+
11
+ def initialize(tables)
12
+ @tables = tables
13
+ @config = ActiveRecord::Dbt::Config.instance
14
+ end
15
+
16
+ def config
17
+ {
18
+ 'version' => 2,
19
+ 'sources' => [
20
+ source_properties.merge('tables' => tables_properties)
21
+ ]
22
+ }
23
+ end
24
+
25
+ private
26
+
27
+ def source_properties
28
+ source_config[:sources]
29
+ end
30
+
31
+ def tables_properties
32
+ tables.map(&:config)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Table
6
+ module Base
7
+ attr_reader :table_name
8
+
9
+ def initialize(table_name)
10
+ @table_name = table_name
11
+ @config = ActiveRecord::Dbt::Config.instance
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Table
6
+ class Test
7
+ include ActiveRecord::Dbt::DbtPackage::DbtUtils::Table::Testable::UniqueCombinationOfColumnsTestable
8
+
9
+ include ActiveRecord::Dbt::Table::Base
10
+
11
+ def config
12
+ [
13
+ *unique_combination_of_columns_test
14
+ ].compact.presence
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Table
6
+ class Yml
7
+ include ActiveRecord::Dbt::Table::Base
8
+
9
+ attr_reader :table_test, :columns
10
+
11
+ delegate :source_config, to: :@config
12
+
13
+ def initialize(table_name, table_test, columns)
14
+ super(table_name)
15
+ @table_test = table_test
16
+ @columns = columns
17
+ end
18
+
19
+ def config
20
+ {
21
+ **table_properties,
22
+ 'columns' => columns.map(&:config)
23
+ }.compact
24
+ end
25
+
26
+ private
27
+
28
+ def table_properties
29
+ {
30
+ 'name' => table_name,
31
+ 'description' => description,
32
+ **table_overrides.except(:columns),
33
+ 'tests' => table_test.config
34
+ }
35
+ end
36
+
37
+ def description
38
+ return logical_name if table_description.blank?
39
+
40
+ [
41
+ "# #{logical_name}",
42
+ table_description
43
+ ].join("\n")
44
+ end
45
+
46
+ def logical_name
47
+ @logical_name ||=
48
+ source_config.dig(:table_descriptions, table_name, :logical_name) ||
49
+ translated_table_name ||
50
+ default_logical_name ||
51
+ "Write a logical_name of the '#{table_name}' table."
52
+ end
53
+
54
+ def translated_table_name
55
+ I18n.t("activerecord.models.#{table_name.singularize}", default: nil)
56
+ end
57
+
58
+ def default_logical_name
59
+ source_config.dig(:defaults, :table_descriptions, :logical_name)
60
+ &.gsub(/{{\s*table_name\s*}}/, table_name)
61
+ end
62
+
63
+ def table_description
64
+ @table_description ||= source_config.dig(:table_descriptions, table_name, :description)
65
+ end
66
+
67
+ def table_overrides
68
+ @table_overrides ||=
69
+ source_config.dig(:table_overrides, table_name) ||
70
+ {}
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+ loader = Zeitwerk::Loader.for_gem_extension(ActiveRecord)
5
+ loader.setup
6
+
7
+ module ActiveRecord
8
+ module Dbt
9
+ def self.configure
10
+ yield config = ActiveRecord::Dbt::Config.instance
11
+
12
+ config
13
+ end
14
+
15
+ class Error < StandardError; end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Create configuration files for dbt.
3
+
4
+ Example:
5
+ bin/rails generate active_record:dbt:config
6
+
7
+ This will create:
8
+ #{config_directory_path}/source_config.yml
9
+ #{config_directory_path}/staging_model.sql.tt
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Generators
6
+ class ConfigGenerator < Rails::Generators::Base
7
+ source_root File.expand_path('templates', __dir__)
8
+
9
+ def create_source_config_file
10
+ template 'source_config.yml.tt', config.source_config_path
11
+ end
12
+
13
+ def copy_staging_model_sql_tt_file
14
+ copy_file File.expand_path('../staging_model/templates/staging_model.sql.tt', __dir__),
15
+ "#{config.config_directory_path}/staging_model.sql.tt"
16
+ end
17
+
18
+ private
19
+
20
+ def config
21
+ @config ||= ActiveRecord::Dbt::Config.instance
22
+ end
23
+
24
+ def application_name
25
+ Rails.application.class.name.sub(/::Application$/, '').downcase
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,68 @@
1
+ sources:
2
+ name: <%= application_name %>
3
+ meta:
4
+ generated_by: activerecord-dbt
5
+ description: |-
6
+ Write a description of the '<%= application_name %>' source.
7
+ You can write multiple lines.
8
+
9
+ table_overrides:
10
+ <table_name>:
11
+ meta: {<dictionary>}
12
+ identifier: <table_name>
13
+ loaded_at_field: <column_name>
14
+ tests:
15
+ - <test>
16
+ tags: [<string>]
17
+ freshness:
18
+ warn_after:
19
+ count: <positive_integer>
20
+ period: minute | hour | day
21
+ error_after:
22
+ count: <positive_integer>
23
+ period: minute | hour | day
24
+ filter: <where-condition>
25
+ quoting:
26
+ database: true | false
27
+ schema: true | false
28
+ identifier: true | false
29
+ external: {<dictionary>}
30
+ columns:
31
+ <column_name>:
32
+ meta: {<dictionary>}
33
+ quote: true | false
34
+ tests:
35
+ - <test>
36
+ tags: [<string>]
37
+
38
+ defaults:
39
+ table_descriptions:
40
+ logical_name: Write a logical_name of the '{{ table_name }}' table.
41
+ columns:
42
+ description: Write a description of the '{{ table_name }}.{{ column_name }}' column.
43
+
44
+ table_descriptions:
45
+ ar_internal_metadata:
46
+ logical_name: Internal Metadata
47
+ description: |-
48
+ By default Rails will store information about your Rails environment and schema
49
+ in an internal table named `ar_internal_metadata`.
50
+ columns:
51
+ key: Key
52
+ value: Value
53
+ created_at: Created At
54
+ updated_at: Updated At
55
+ schema_migrations:
56
+ logical_name: Schema Migrations
57
+ description: |-
58
+ Rails keeps track of which migrations have been committed to the database and
59
+ stores them in a neighboring table in that same database called `schema_migrations`.
60
+ columns:
61
+ version: The version number of the migration.
62
+ <table_name>:
63
+ logical_name: Write a table logical name.
64
+ description: |-
65
+ Write a description of the table.
66
+ You can write multiple lines.
67
+ columns:
68
+ <colume_name>: Write a description of the column.
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Create an initializer file for dbt:.
3
+
4
+ Example:
5
+ bin/rails generate active_record:dbt:initializer
6
+
7
+ This will create:
8
+ config/initializers/dbt.rb
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Generators
6
+ class InitializerGenerator < Rails::Generators::Base
7
+ source_root File.expand_path('templates', __dir__)
8
+
9
+ def copy_initializer_file
10
+ copy_file 'dbt.rb', 'config/initializers/dbt.rb'
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/dbt'
4
+
5
+ ActiveRecord::Dbt.configure do |c|
6
+ c.config_directory_path = 'lib/dbt'
7
+ c.export_directory_path = 'doc/dbt'
8
+ c.data_sync_delayed = false
9
+ c.used_dbt_package_names = []
10
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generate a source file for dbt.
3
+
4
+ Example:
5
+ bin/rails generate active_record:dbt:source
6
+
7
+ This will create:
8
+ #{export_directory_path}/src_#{source_name}.yml
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Dbt
5
+ module Generators
6
+ class SourceGenerator < Rails::Generators::Base
7
+ source_root File.expand_path('templates', __dir__)
8
+
9
+ def create_source_yml_file
10
+ create_file "#{config.export_directory_path}/src_#{config.source_name}.yml",
11
+ ActiveRecord::Dbt::Factory::SourceFactory.build
12
+ end
13
+
14
+ private
15
+
16
+ def config
17
+ @config ||= ActiveRecord::Dbt::Config.instance
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Generate staging model files for dbt that reference the specified TABLE_NAME.
3
+
4
+ Example:
5
+ bin/rails generate active_record:dbt:staging_model TABLE_NAME
6
+
7
+ This will create:
8
+ #{export_directory_path}/stg_#{source_name}__#{table_name}.sql
9
+ #{export_directory_path}/stg_#{source_name}__#{table_name}.yml