activerecord-dbt 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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