rdt 0.0.9

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e699a3cc4e7b1bcfb20fceb193ee0d7b1e805d62b70f79bfbc2d528a77e7d4b7
4
+ data.tar.gz: 46d2140d1b30ce7ff8533c85e941cde449d92432a77c147cb59709aaed2492af
5
+ SHA512:
6
+ metadata.gz: e77b10bca5337de9917267ee5197f1452a4a24bfde5aa9999b0e3e187f37eb15bee77defd9a5f4285b20a30ab1c030835aacf10dd0d9d08b72006f5b07eb71ce
7
+ data.tar.gz: 43ddb567994a3c91e95d0f22f3d444074b8d889b5ac1ed2b6285c0d2c78a67512024a880b2c9f7b61869f0bd19b59c555babea878367a946fe4e592236716296
@@ -0,0 +1,57 @@
1
+ require "base64"
2
+ module Dbt
3
+ class Mermaid
4
+ class << self
5
+ def markdown_for(dag)
6
+ mermaid = "flowchart LR\n"
7
+ dag.each do |model, dependencies|
8
+ mermaid += "#{model}\n"
9
+ dependencies.each do |dependency|
10
+ mermaid += "#{dependency} --> #{model}\n"
11
+ end
12
+ end
13
+ mermaid
14
+ end
15
+
16
+ # does not work
17
+ def encode_to_editor_url(md)
18
+ json = {
19
+ code: md,
20
+ mermaid: {
21
+ theme: "default"
22
+ },
23
+ updateEditor: false,
24
+ autoSync: true,
25
+ updateDiagram: false
26
+ }
27
+ encoded = Base64.urlsafe_encode64(json.to_json.force_encoding("ASCII"))
28
+ "https://mermaid.ink/img/#{encoded}"
29
+ #url = encode_mermaid(diagram)
30
+ encoded
31
+ end
32
+
33
+ def generate_file(chart)
34
+ html = <<~HTML
35
+ <!DOCTYPE html>
36
+ <html lang="en">
37
+ <body>
38
+ <style>svg { max-width: none; width: 2000px; }</style>
39
+ <pre class="mermaid">
40
+ #{chart}
41
+ </pre>
42
+ <script type="module">
43
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
44
+ </script>
45
+ </body>
46
+ </html>
47
+ HTML
48
+
49
+ begin
50
+ File.write("dependencies.html", html)
51
+ rescue Errno::EACCES => e
52
+ puts "Failed to write to file: #{e.message}"
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
data/lib/dbt/model.rb ADDED
@@ -0,0 +1,188 @@
1
+ module Dbt
2
+ class Model
3
+ include SqlTemplateHelpers
4
+
5
+ attr_reader :name,
6
+ :code,
7
+ :materialize_as,
8
+ :sources,
9
+ :refs,
10
+ :built,
11
+ :filepath,
12
+ :skip,
13
+ :is_incremental,
14
+ :unique_by_column
15
+
16
+ def initialize(filepath, schema = SCHEMA)
17
+ @filepath = filepath
18
+ @name = File.basename(filepath, ".sql")
19
+ @original_code = File.read(filepath)
20
+ @sources = []
21
+ @refs = []
22
+ @built = false
23
+ @materialize_as = "VIEW"
24
+ @schema = schema
25
+
26
+ def source(table)
27
+ @sources << table.to_s
28
+ table.to_s
29
+ end
30
+
31
+ def ref(model)
32
+ @refs << model.to_s
33
+ "#{@schema}.#{model}"
34
+ end
35
+
36
+ def this
37
+ "#{@schema}.#{@name}"
38
+ end
39
+
40
+ def build_as kind, unique_by: "unique_by"
41
+ case kind.to_s.downcase
42
+ when "view"
43
+ @materialize_as = "VIEW"
44
+ when "table"
45
+ @materialize_as = "TABLE"
46
+ when "incremental"
47
+ if get_relation_type(this) != "TABLE"
48
+ @materialize_as = "TABLE"
49
+ @is_incremental = false
50
+ else
51
+ @is_incremental = true
52
+ @unique_by_column = unique_by
53
+ end
54
+ else
55
+ raise "Invalid build_as materialization: #{kind}"
56
+ end
57
+ ""
58
+ end
59
+
60
+ def materialize
61
+ # legacy, use build_as :table
62
+ # will add warning in the future
63
+ build_as :table
64
+ ""
65
+ end
66
+
67
+ def skip
68
+ @skip = true
69
+ ""
70
+ end
71
+
72
+ @code = ERB.new(@original_code).result(binding)
73
+ end
74
+
75
+ def build
76
+ if @skip
77
+ puts "SKIPPING #{@name}"
78
+ elsif @is_incremental
79
+ puts "INCREMENTAL #{@name}"
80
+ assert_column_uniqueness(unique_by_column, this)
81
+ temp_table = "#{@schema}.#{@name}_incremental_build_temp_table"
82
+
83
+ # drop the temp table if it exists
84
+ ActiveRecord::Base.connection.execute <<~SQL
85
+ DROP TABLE IF EXISTS #{temp_table};
86
+ SQL
87
+
88
+ # create a temp table with the same schema as the source
89
+ ActiveRecord::Base.connection.execute <<~SQL
90
+ CREATE TABLE #{temp_table} AS (
91
+ #{code}
92
+ );
93
+ SQL
94
+ assert_column_uniqueness(unique_by_column, temp_table)
95
+ # delete rows from the table that are in the source
96
+ ActiveRecord::Base.connection.execute <<~SQL
97
+ DELETE FROM #{this}
98
+ USING #{temp_table}
99
+ WHERE #{this}.#{unique_by_column} = #{temp_table}.#{unique_by_column};
100
+ SQL
101
+
102
+ # insert rows from the source into the table
103
+ ActiveRecord::Base.connection.execute <<~SQL
104
+ INSERT INTO #{this}
105
+ SELECT * FROM #{temp_table};
106
+ SQL
107
+
108
+ # drop the temp table
109
+ ActiveRecord::Base.connection.execute <<~SQL
110
+ DROP TABLE #{temp_table};
111
+ SQL
112
+
113
+ else
114
+ puts "BUILDING #{@name}"
115
+ curent_relation_type = get_relation_type(this)
116
+ case @materialize_as
117
+ when "VIEW"
118
+ ActiveRecord::Base.connection.execute <<~SQL
119
+ BEGIN;
120
+ #{drop_relation(this)}
121
+ CREATE VIEW #{this} AS (
122
+ #{@code}
123
+ );
124
+ COMMIT;
125
+ SQL
126
+ when "TABLE"
127
+ temp_table = "#{@schema}.#{@name}_build_step_temp_table"
128
+ ActiveRecord::Base.connection.execute <<~SQL
129
+ DROP TABLE IF EXISTS #{temp_table};
130
+ CREATE TABLE #{temp_table} AS (
131
+ #{@code}
132
+ );
133
+ BEGIN;
134
+ #{drop_relation(this)}
135
+ ALTER TABLE #{temp_table} RENAME TO #{@name};
136
+ DROP TABLE IF EXISTS #{temp_table};
137
+ COMMIT;
138
+ SQL
139
+ else
140
+ raise "Invalid materialize_as: #{@materialize_as}"
141
+ end
142
+
143
+ @built = true
144
+ end
145
+ end
146
+
147
+ def drop_relation(relation)
148
+ type = get_relation_type(relation)
149
+ if type.present?
150
+ "DROP #{type} #{relation} CASCADE;"
151
+ else
152
+ ""
153
+ end
154
+ end
155
+
156
+ def get_relation_type(relation)
157
+ relnamespace, relname = relation.split(".")
158
+ type =
159
+ ActiveRecord::Base
160
+ .connection
161
+ .execute(
162
+ "
163
+ SELECT
164
+ CASE c.relkind
165
+ WHEN 'r' THEN 'TABLE'
166
+ WHEN 'v' THEN 'VIEW'
167
+ WHEN 'm' THEN 'MATERIALIZED VIEW'
168
+ END AS relation_type
169
+ FROM pg_class c
170
+ JOIN pg_namespace n ON n.oid = c.relnamespace
171
+ WHERE c.relname = '#{relname}' AND n.nspname = '#{relnamespace}';"
172
+ )
173
+ .values
174
+ .first
175
+ &.first
176
+ end
177
+
178
+ def assert_column_uniqueness(column, relation)
179
+ result = ActiveRecord::Base.connection.execute <<~SQL
180
+ SELECT COUNT(*) = COUNT(DISTINCT #{column}) FROM #{relation};
181
+ SQL
182
+ if result.values.first.first == false
183
+ raise "Column #{column} is not unique in #{relation}"
184
+ end
185
+ true
186
+ end
187
+ end
188
+ end
data/lib/dbt/runner.rb ADDED
@@ -0,0 +1,41 @@
1
+ module Dbt
2
+ class Runner
3
+ class << self
4
+ def run(custom_schema = nil, glob_path = "app/sql/**/*.sql")
5
+ schema = custom_schema || Dbt.settings["schema"] || Dbt::SCHEMA
6
+ ActiveRecord::Base.connection.execute "CREATE SCHEMA IF NOT EXISTS #{schema}"
7
+ file_paths = Dir.glob(glob_path)
8
+ models = file_paths.map { |fp| Model.new(fp, schema) }
9
+ dependencies =
10
+ models.map { |m| { m.name => m.refs } }.reduce({}, :merge!)
11
+ check_if_all_refs_have_sql_files dependencies
12
+ graph = Dagwood::DependencyGraph.new dependencies
13
+ md = Mermaid.markdown_for dependencies
14
+ Mermaid.generate_file md
15
+ graph.order.each do |model_name|
16
+ models.find { |m| m.name == model_name }.build
17
+ end
18
+ end
19
+
20
+ def test
21
+ puts "Running tests..."
22
+ schema = Dbt.settings["schema"] || Dbt::SCHEMA
23
+ tables = run(schema, "app/sql_test/**/*.sql")
24
+ tables.each do |table|
25
+ puts "TEST #{table}"
26
+ raise "Table #{table} is not empty" unless ActiveRecord::Base.connection.execute("SELECT COUNT(*) FROM #{schema}.#{table}").to_a[0]["count"] == 0
27
+ end
28
+ puts "All tests passed!"
29
+ end
30
+
31
+ def check_if_all_refs_have_sql_files(dependencies)
32
+ dependencies.each do |key, value|
33
+ sem_arquivo = (value || []) - dependencies.keys
34
+ unless sem_arquivo.empty?
35
+ raise "Missing .sql model files for ref #{sem_arquivo} in model #{key}"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,54 @@
1
+ module Dbt
2
+ module SqlTemplateHelpers
3
+
4
+ def star relation, *exclued_columns
5
+ columns = ActiveRecord::Base.connection.execute("select * from #{relation} limit 0").fields - exclued_columns.map(&:to_s)
6
+ columns.map { |column| "#{relation}.#{column}" }.join(', ')
7
+ end
8
+
9
+ ### JSON SQL helpers
10
+ def j(key)
11
+ "body ->> '#{key}' #{key.underscore}"
12
+ end
13
+
14
+ def j_numeric(key)
15
+ "(body ->> '#{key}')::numeric #{key.underscore}"
16
+ end
17
+
18
+ def j_numeric_comma(key)
19
+ "REPLACE(REPLACE((body ->> '#{key}'),'.',''),',','.')::numeric #{key.underscore}"
20
+ end
21
+
22
+ def j_except *keys
23
+ "body - '#{keys.join("','")}'"
24
+ end
25
+
26
+ ### XML SQL helpers
27
+ def x(key)
28
+ "(xpath('//cmd[@t=''#{key}'']/text()', body))[1]::text AS #{key.underscore}"
29
+ end
30
+
31
+ def x_numeric(key)
32
+ # x_numeric was replacing '.' and ',' to '' and '.' to convert to numeric
33
+ # which should be done by x_numeric_comma.
34
+ puts "WARNING: x_numeric will not change , to . in a future version. Use x_numeric_comma instead."
35
+ #"(xpath('//cmd[@t=''#{key}'']/text()', body))[1]::numeric #{key.underscore}"
36
+ x_numeric_comma(key)
37
+ end
38
+
39
+ def x_numeric_comma(key)
40
+ "REPLACE(REPLACE((xpath('/xjx/cmd[@t=''#{key}'']/text()', body))[1]::text, '.', ''), ',', '.')::numeric #{key.underscore}"
41
+ end
42
+
43
+ def x_date(key)
44
+ "TO_DATE((xpath('/xjx/cmd[@t=''#{key}'']/text()', body))[1]::text, 'DD/MM/YYYY') #{key.underscore}"
45
+ end
46
+
47
+ def x_except *keys
48
+ not_in_clause = keys.map { |key| "''#{key}''" }
49
+ .join('or @t = ')
50
+ "xpath('//cmd[not(@t = #{not_in_clause})]', body)"
51
+ end
52
+
53
+ end
54
+ end
data/lib/dbt.rb ADDED
@@ -0,0 +1,28 @@
1
+ require "zeitwerk"
2
+ loader = Zeitwerk::Loader.for_gem
3
+ loader.setup
4
+
5
+ require "dagwood"
6
+
7
+ module Dbt
8
+ SCHEMA = "felipe_dbt"
9
+
10
+ def self.settings
11
+ @settings ||= begin
12
+ path = Rails.root.join("config", "dbt.yml").to_s
13
+ if File.exist?(path)
14
+ YAML.safe_load(ERB.new(File.read(path)).result, aliases: true)
15
+ else
16
+ {}
17
+ end
18
+ end
19
+ end
20
+
21
+ def self.run(...)
22
+ Runner.run(...)
23
+ end
24
+
25
+ def self.test(...)
26
+ Runner.test(...)
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rdt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.9
5
+ platform: ruby
6
+ authors:
7
+ - Felipe Mesquita
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dagwood
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: zeitwerk
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: activerecord
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '6.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '6.0'
54
+ description: SQL-based data modeling for Rails applications and Postgres
55
+ email: felipemesquita@hey.com
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - lib/dbt.rb
61
+ - lib/dbt/mermaid.rb
62
+ - lib/dbt/model.rb
63
+ - lib/dbt/runner.rb
64
+ - lib/dbt/sql_template_helpers.rb
65
+ homepage: https://github.com/felipedmesquita/dbt
66
+ licenses:
67
+ - MIT
68
+ metadata: {}
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubygems_version: 3.6.9
84
+ specification_version: 4
85
+ summary: Ruby Data Tool
86
+ test_files: []