ridgepole-view 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 69fdd1019a8766f9e1d4518915f2bb35dd8c396e1667f4bd92e731ed6edc3e35
4
+ data.tar.gz: 769a340d1ac813e6c02546124e55caca5bc2d6c964afa95d53bac24a5fa2b005
5
+ SHA512:
6
+ metadata.gz: 1785451331df5dc46efb40feb52878897d953cdba7355c7ad0ed2e7a1b54eefbef2a9c3eb22c84978a2b04df4cfbeca4dc573530489ece91d4e97f74f29a9cc4
7
+ data.tar.gz: 53a60e1f9ccac14ae825a35df3d07d7a6a796514def981b685ae8867050e3767a7bee48ba3354d041aadb7cc2665832a4740ae8d0fa0a5be79028a66f139c8fa
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ridgepole
4
+ module View
5
+ module Delta
6
+ def script
7
+ table_script = super
8
+ view_script = generate_view_script
9
+ [table_script, view_script].map(&:strip).reject(&:empty?).join("\n\n")
10
+ end
11
+
12
+ def differ?
13
+ !!(super || view_delta_present?)
14
+ end
15
+
16
+ private
17
+
18
+ def view_delta_present?
19
+ vd = @delta[:views]
20
+ vd && (vd[:add]&.any? || vd[:change]&.any? || vd[:delete]&.any?)
21
+ end
22
+
23
+ def generate_view_script
24
+ buf = StringIO.new
25
+ views = @delta[:views] || {}
26
+
27
+ (views[:delete] || {}).each do |name, attrs|
28
+ append_drop_view(name, attrs, buf)
29
+ end
30
+
31
+ (views[:change] || {}).each do |name, change_attrs|
32
+ append_drop_view(name, change_attrs[:from], buf)
33
+ end
34
+
35
+ (views[:add] || {}).each do |name, attrs|
36
+ append_create_view(name, attrs, buf)
37
+ end
38
+
39
+ (views[:change] || {}).each do |name, change_attrs|
40
+ append_create_view(name, change_attrs[:to], buf)
41
+ end
42
+
43
+ buf.string
44
+ end
45
+
46
+ def append_create_view(name, attrs, buf)
47
+ mat = attrs[:materialized] ? ", materialized: true" : ""
48
+ sql = attrs[:sql_definition]
49
+ buf.puts "create_view #{name.inspect}, sql_definition: #{sql.inspect}#{mat}"
50
+ end
51
+
52
+ def append_drop_view(name, attrs, buf)
53
+ mat = attrs[:materialized] ? ", materialized: true" : ""
54
+ buf.puts "drop_view #{name.inspect}#{mat}"
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ridgepole/view/view_definition"
4
+
5
+ module Ridgepole
6
+ module View
7
+ module Diff
8
+ def diff(from, to, options = {})
9
+ from = (from || {}).deep_dup
10
+ to = (to || {}).deep_dup
11
+
12
+ # Ridgepole::Diff#diff iterates all keys in the definition hash as table names,
13
+ # accessing table-specific attributes (:definition, :options) via scan_change etc.
14
+ # The :views data structure differs from tables, so passing it through causes NoMethodError.
15
+ # Extract :views before calling super and handle view diffing separately.
16
+ from_views = from.delete(:views) || {}
17
+ to_views = to.delete(:views) || {}
18
+
19
+ delta = super(from, to, options)
20
+
21
+ view_delta = diff_views(from_views, to_views)
22
+ unless view_delta.values.all?(&:empty?)
23
+ delta.instance_variable_get(:@delta)[:views] = view_delta
24
+ end
25
+
26
+ delta
27
+ end
28
+
29
+ private
30
+
31
+ def diff_views(from_views, to_views)
32
+ result = { add: {}, change: {}, delete: {} }
33
+ all_names = (from_views.keys + to_views.keys).uniq
34
+
35
+ all_names.each do |name|
36
+ from = from_views[name]
37
+ to = to_views[name]
38
+
39
+ if from.nil? && to
40
+ result[:add][name] = to
41
+ elsif from && to.nil?
42
+ result[:delete][name] = from
43
+ elsif ViewDefinition.changed?(from, to)
44
+ result[:change][name] = { from: from, to: to }
45
+ end
46
+ end
47
+
48
+ result
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ridgepole
4
+ module View
5
+ module DSLParser
6
+ module Context
7
+ def create_view(name, sql_definition:, materialized: false)
8
+ name = name.to_s
9
+ @__definition[:views] ||= {}
10
+
11
+ raise "View `#{name}` already defined" if @__definition[:views][name]
12
+
13
+ @__definition[:views][name] = {
14
+ sql_definition: sql_definition,
15
+ materialized: materialized,
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ridgepole
4
+ module View
5
+ module DSLParser
6
+ private
7
+
8
+ def check_definition(definition)
9
+ # Ridgepole::DSLParser#check_definition iterates all keys as table names and
10
+ # validates table-specific attributes via check_orphan_index etc.
11
+ # Temporarily remove :views to prevent false validation errors, then restore after super.
12
+ views = definition.delete(:views)
13
+ super(definition)
14
+ ensure
15
+ definition[:views] = views if views
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ridgepole
4
+ module View
5
+ module Dumper
6
+ def dump(&block)
7
+ result = super(&block)
8
+ view_dsl = dump_views
9
+ view_dsl.empty? ? result : [result, view_dsl].join("\n\n")
10
+ end
11
+
12
+ private
13
+
14
+ def dump_views
15
+ views = Scenic.database.views
16
+ views = views.reject { |v| ignored_view?(v.name) }
17
+ views.map(&:to_schema).join("\n")
18
+ end
19
+
20
+ def ignored_view?(name)
21
+ return false unless @options[:ignore_tables]
22
+
23
+ @options[:ignore_tables].any? { |pattern| pattern =~ name }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ridgepole
4
+ module View
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ridgepole
4
+ module View
5
+ module ViewDefinition
6
+ module_function
7
+
8
+ # Normalize SQL for comparison so that semantically identical definitions
9
+ # written differently in the Schemafile vs dumped from PostgreSQL are not
10
+ # treated as changes (which would cause unnecessary drop_view + create_view).
11
+ #
12
+ # e.g. Schemafile: "SELECT name\n FROM users;"
13
+ # PG dump: "select name from users"
14
+ def normalize(sql)
15
+ sql.to_s
16
+ .gsub(/\s+/, " ") # collapse newlines, tabs, multiple spaces into single space
17
+ .gsub(/;\s*\z/, "") # strip trailing semicolons
18
+ .strip # remove leading/trailing whitespace
19
+ .downcase # PG may dump keywords in lowercase
20
+ end
21
+
22
+ def changed?(from, to)
23
+ from[:materialized] != to[:materialized] ||
24
+ normalize(from[:sql_definition]) != normalize(to[:sql_definition])
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tsort"
4
+ require "ridgepole"
5
+ require "scenic"
6
+
7
+ require "ridgepole/view/version"
8
+ require "ridgepole/view/view_definition"
9
+ require "ridgepole/view/dsl_parser/context"
10
+ require "ridgepole/view/dsl_parser"
11
+ require "ridgepole/view/dumper"
12
+ require "ridgepole/view/diff"
13
+ require "ridgepole/view/delta"
14
+
15
+ Ridgepole::DSLParser::Context.prepend(Ridgepole::View::DSLParser::Context)
16
+ Ridgepole::DSLParser.prepend(Ridgepole::View::DSLParser)
17
+ Ridgepole::Dumper.prepend(Ridgepole::View::Dumper)
18
+ Ridgepole::Diff.prepend(Ridgepole::View::Diff)
19
+ Ridgepole::Delta.prepend(Ridgepole::View::Delta)
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ridgepole-view
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yuhi-Sato
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: ridgepole
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: scenic
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.5'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1.5'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ description: A Ridgepole plugin that adds database view management using the Scenic
55
+ gem
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - lib/ridgepole-view.rb
61
+ - lib/ridgepole/view/delta.rb
62
+ - lib/ridgepole/view/diff.rb
63
+ - lib/ridgepole/view/dsl_parser.rb
64
+ - lib/ridgepole/view/dsl_parser/context.rb
65
+ - lib/ridgepole/view/dumper.rb
66
+ - lib/ridgepole/view/version.rb
67
+ - lib/ridgepole/view/view_definition.rb
68
+ homepage: https://github.com/Yuhi-Sato/ridgepole-view
69
+ licenses:
70
+ - MIT
71
+ metadata:
72
+ homepage_uri: https://github.com/Yuhi-Sato/ridgepole-view
73
+ source_code_uri: https://github.com/Yuhi-Sato/ridgepole-view
74
+ changelog_uri: https://github.com/Yuhi-Sato/ridgepole-view/blob/main/CHANGELOG.md
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '2.7'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubygems_version: 3.6.9
90
+ specification_version: 4
91
+ summary: Scenic view support for Ridgepole
92
+ test_files: []