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 +7 -0
- data/lib/dbt/mermaid.rb +57 -0
- data/lib/dbt/model.rb +188 -0
- data/lib/dbt/runner.rb +41 -0
- data/lib/dbt/sql_template_helpers.rb +54 -0
- data/lib/dbt.rb +28 -0
- metadata +86 -0
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
|
data/lib/dbt/mermaid.rb
ADDED
@@ -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: []
|