From 6f1f5cba0fdf80045b2f5dd295ecfd305fcca7c4 Mon Sep 17 00:00:00 2001 From: Igor Kasyanchuk Date: Thu, 10 Feb 2022 10:18:16 -0800 Subject: [PATCH] with dumper and loader --- Gemfile | 1 - Gemfile.lock | 1 - lib/generators/sql_view/view/USAGE | 20 ++-- .../view/templates/db/migrate/create_view.erb | 5 - .../sql_view/view/view_generator.rb | 83 +++++++++------- lib/sql_view.rb | 14 ++- lib/sql_view/railtie.rb | 6 ++ lib/sql_view/schema_dumper.rb | 94 +++++++++++++++++++ lib/sql_view/statements.rb | 7 ++ sql_view.gemspec | 1 - 10 files changed, 175 insertions(+), 57 deletions(-) delete mode 100755 lib/generators/sql_view/view/templates/db/migrate/create_view.erb mode change 100644 => 100755 lib/sql_view/railtie.rb create mode 100755 lib/sql_view/schema_dumper.rb create mode 100755 lib/sql_view/statements.rb diff --git a/Gemfile b/Gemfile index 8ff8b35..1330cde 100755 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,6 @@ gemspec gem "pg" gem "pry" gem "wrapped_print" -gem "scenic" # Start debugger with binding.b [https://github.com/ruby/debug] # gem "debug", ">= 1.0.0" diff --git a/Gemfile.lock b/Gemfile.lock index 445a22a..ad60900 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,6 @@ PATH specs: sql_view (0.1.0) rails - scenic GEM remote: https://rubygems.org/ diff --git a/lib/generators/sql_view/view/USAGE b/lib/generators/sql_view/view/USAGE index 6811fba..61d1f78 100755 --- a/lib/generators/sql_view/view/USAGE +++ b/lib/generators/sql_view/view/USAGE @@ -1,20 +1,16 @@ Description: - Create a new database view for your application. This will create a new - view definition file and the accompanying migration. - - If a view of the given name already exists, create a new version of the view - and a migration to replace the old version with the new. + Create a new database view in your application. To create a materialized view, pass the '--materialized' option. - To create a materialized view with NO DATA, pass '--no-data' option. Examples: - rails generate scenic:view searches + rails generate sql_view:view ActiveUser 'User.where(active: true)' - create: db/views/searches_v01.sql - create: db/migrate/20140803191158_create_searches.rb + create: app/sql_views/search_view.rb + create: db/migrate/20140803191158_create_active_users_views.rb - rails generate scenic:view searches +Examples: + rails generate sql_view:view ArchivedAccount Account.archived --materialized --view-name=inactive_accounts - create: db/views/searches_v02.sql - create: db/migrate/20140804191158_update_searches_to_version_2.rb + create: app/sql_views/search_view.rb + create: db/migrate/20140803191158_create_search_views.rb diff --git a/lib/generators/sql_view/view/templates/db/migrate/create_view.erb b/lib/generators/sql_view/view/templates/db/migrate/create_view.erb deleted file mode 100755 index 75b5160..0000000 --- a/lib/generators/sql_view/view/templates/db/migrate/create_view.erb +++ /dev/null @@ -1,5 +0,0 @@ -class <%= migration_class_name %> < <%= activerecord_migration_class %> - def change - create_view <%= formatted_plural_name %><%= create_view_options %> - end -end diff --git a/lib/generators/sql_view/view/view_generator.rb b/lib/generators/sql_view/view/view_generator.rb index 3138ddc..5bfacd4 100755 --- a/lib/generators/sql_view/view/view_generator.rb +++ b/lib/generators/sql_view/view/view_generator.rb @@ -3,26 +3,42 @@ module SqlView module Generators - # @api private class ViewGenerator < Rails::Generators::NamedBase include Rails::Generators::Migration - source_root File.expand_path("templates", __dir__) - def create_views_directory - unless views_directory_path.exist? - empty_directory(views_directory_path) - end - end + class_option :"view-name", type: :string, default: nil + class_option :materialized, type: :boolean, default: false - def create_view_definition - create_file definition.path - end + def create_everything + create_file "app/sql_views/#{file_name}_view.rb", <<-FILE +class #{class_name}View < SqlView::Model +#{top_code} + + schema -> {#{schema_code} } + + extend_model_with do + # sample how you can extend it, similar to regular AR model + # + # belongs_to :user + # has_many :posts + # + # scope :ordered, -> { order(:created_at) } + # scope :by_role, ->(role) { where(role: role) } + end +end +FILE - def create_migration_file - migration_template( - "db/migrate/create_view.erb", - "db/migrate/create_#{plural_file_name}.rb", - ) + create_file "db/migrate/#{self.class.next_migration_number("db/migrate")}_create_#{file_name}s_view.rb", <<-FILE +class #{migration_class_name} < #{activerecord_migration_class} + def up + #{class_name}View.sql_view.up + end + + def down + #{class_name}View.sql_view.down + end +end +FILE end def self.next_migration_number(dir) @@ -30,8 +46,24 @@ def self.next_migration_number(dir) end no_tasks do + def top_code + [view_name_code, materialized_code].compact.join("\n\n") + end + + def view_name_code + options["view-name"] ? " self.view_name = '#{options["view-name"]}'" : nil + end + + def materialized_code + options[:materialized] ? " materialized" : nil + end + + def schema_code + " #{args[0].presence || "\n # ActiveRecord::Relation or SQL\n # for example: User.where(active: true)\n " }" + end + def migration_class_name - "Create#{class_name.tr('.', '').pluralize}" + "Create#{class_name.tr('.', '').pluralize}View" end def activerecord_migration_class @@ -51,25 +83,6 @@ def file_name super.tr(".", "_") end - def views_directory_path - @views_directory_path ||= Rails.root.join("db", "views") - end - - def formatted_plural_name - if plural_name.include?(".") - "\"#{plural_name}\"" - else - ":#{plural_name}" - end - end - - def create_view_options - if materialized? - ", materialized: #{no_data? ? '{ no_data: true }' : true}" - else - "" - end - end end end end diff --git a/lib/sql_view.rb b/lib/sql_view.rb index be15f4e..7dcbd33 100755 --- a/lib/sql_view.rb +++ b/lib/sql_view.rb @@ -1,13 +1,24 @@ -require 'singleton' +require "singleton" +require_relative "./sql_view/schema_dumper.rb" +require_relative "./sql_view/statements.rb" require "sql_view/version" require "sql_view/railtie" module SqlView + # mattr_accessor :klasses + # @@klasses = {} + class Model class_attribute :view, :sql_view_options + class << self + delegate_missing_to :model + end + def self.inherited(subclass) + # puts subclass subclass.sql_view_options = {} + # SqlView.klasses[subclass] = subclass.sql_view end def self.view_name=(name) @@ -39,7 +50,6 @@ def self.extend_model_with(&block) end end - class Migration attr_reader :parent diff --git a/lib/sql_view/railtie.rb b/lib/sql_view/railtie.rb old mode 100644 new mode 100755 index 6bf2c3e..2b447b4 --- a/lib/sql_view/railtie.rb +++ b/lib/sql_view/railtie.rb @@ -1,4 +1,10 @@ module SqlView class Railtie < ::Rails::Railtie + initializer "sql_view.load" do + ActiveSupport.on_load :active_record do + ActiveRecord::ConnectionAdapters::AbstractAdapter.include SqlView::Statements + ActiveRecord::SchemaDumper.prepend SqlView::SchemaDumper + end + end end end diff --git a/lib/sql_view/schema_dumper.rb b/lib/sql_view/schema_dumper.rb new file mode 100755 index 0000000..36b883b --- /dev/null +++ b/lib/sql_view/schema_dumper.rb @@ -0,0 +1,94 @@ +require "rails" + +# Copy-pasted from scenic game. Scenic is a very nice gem + +module SqlView + # @api private + module SchemaDumper + class DBView < OpenStruct + + def materialized? + self.kind == "m" + end + + def materialized_or_not + materialized? ? " MATERIALIZED " : nil + end + + def to_schema + <<-DEFINITION + create_sql_view "#{self.viewname}", sql: <<-\SQL + CREATE #{materialized_or_not} VIEW "#{self.viewname}" AS + #{escaped_definition.indent(2)} + SQL + DEFINITION + end + + def escaped_definition + definition.gsub("\\", "\\\\\\") + end + end + + def tables(stream) + super + views(stream) + end + + def views(stream) + if dumpable_views_in_database.any? + stream.puts + end + + dumpable_views_in_database.each do |viewname| + view = DBView.new(get_view_info(viewname)) + #puts view.to_schema + stream.puts(view.to_schema) + #indexes(view.name, stream) + end + end + + private + + def dumpable_views_in_database + @dumpable_views_in_database ||= ActiveRecord::Base.connection.views.reject do |viewname| + ignored?(viewname) + end + end + + def get_view_info(viewname) + views_schema.detect{|e| e['viewname'] == viewname} + end + + def views_schema + @views_schema ||= ActiveRecord::Base.connection.execute(<<-SQL) + SELECT + c.relname as viewname, + pg_get_viewdef(c.oid) AS definition, + c.relkind AS kind, + n.nspname AS namespace + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE + c.relkind IN ('m', 'v') + AND c.relname NOT IN (SELECT extname FROM pg_extension) + AND n.nspname = ANY (current_schemas(false)) + ORDER BY c.oid + SQL + .to_a + end + + unless ActiveRecord::SchemaDumper.private_instance_methods(false).include?(:ignored?) + # This method will be present in Rails 4.2.0 and can be removed then. + def ignored?(table_name) + ["schema_migrations", ignore_tables].flatten.any? do |ignored| + case ignored + when String then remove_prefix_and_suffix(table_name) == ignored + when Regexp then remove_prefix_and_suffix(table_name) =~ ignored + else + raise StandardError, "ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values." + end + end + end + end + end +end diff --git a/lib/sql_view/statements.rb b/lib/sql_view/statements.rb new file mode 100755 index 0000000..1a2e716 --- /dev/null +++ b/lib/sql_view/statements.rb @@ -0,0 +1,7 @@ +module SqlView + module Statements + def create_sql_view(viewname, sql:) + ActiveRecord::Base.connection.execute(sql) + end + end +end diff --git a/sql_view.gemspec b/sql_view.gemspec index 5af92fa..4e80423 100755 --- a/sql_view.gemspec +++ b/sql_view.gemspec @@ -17,7 +17,6 @@ Gem::Specification.new do |spec| end spec.add_dependency "rails" - spec.add_dependency "scenic" spec.add_development_dependency "pg" spec.add_development_dependency "pry"