Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,70 @@ class UpdateSearchResultsToVersion2 < ActiveRecord::Migration
end
```

## Can I use Scenic with multiple databases?

You bet! If you're using Rails 6.0 or higher with multiple databases configured,
Scenic has you covered. Just pass a `--database` option when generating your
view:

```sh
$ rails generate scenic:view analytics --database=secondary
create db/secondary_views/analytics_v01.sql
create db/secondary_migrate/[TIMESTAMP]_create_analytics.rb
```

Scenic will create your view definition in a database-specific directory
(`db/secondary_views/` instead of `db/views/`) and the generated migration will
include the `database:` parameter:

```ruby
class CreateAnalytics < ActiveRecord::Migration[7.0]
def change
create_view :analytics, database: :secondary
end
end
```

Run the migration for your secondary database the same way you would for any
Rails multiple database setup:

```sh
$ rake db:migrate:secondary
```

All of Scenic's migration methods accept the `database:` parameter, so you can
create, update, and drop views on any configured database:

```ruby
def change
create_view :reports, version: 1, database: :secondary
update_view :reports, version: 2, database: :secondary
drop_view :reports, database: :secondary
end
```

If you need custom paths for your views or migrations, you can configure them
in `database.yml` just like Rails' `migrations_paths`:

```yaml
# config/database.yml
secondary:
database: my_secondary_db
migrations_paths: db/secondary_migrate
views_paths: db/secondary_views
```

If you're using different adapters for different databases (say, Postgres for
your primary database and MySQL for analytics), you can configure them in an
initializer:

```ruby
# config/initializers/scenic.rb
Scenic.configure do |config|
config.databases[:secondary] = Scenic::Adapters::Postgres.new(SecondaryRecord)
end
```

## I don't need this view anymore. Make it go away.

Scenic gives you `drop_view` too:
Expand All @@ -232,6 +296,7 @@ Scenic gives you `drop_view` too:
def change
drop_view :search_results, revert_to_version: 2
drop_view :materialized_admin_reports, revert_to_version: 3, materialized: true
drop_view :analytics_view, revert_to_version: 1, database: :secondary
end
```

Expand Down
19 changes: 19 additions & 0 deletions lib/generators/scenic/materializable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ module Materializable
required: false,
desc: "Uses replace_view instead of update_view",
default: false
class_option :database,
type: :string,
required: false,
desc: "The database to use (requires Rails 6.0+)",
default: nil
end

private
Expand All @@ -47,6 +52,20 @@ def side_by_side?
options[:side_by_side]
end

def database
options[:database]&.to_sym
end

def validate_rails_version_for_multiple_databases!
return unless database

if Rails::VERSION::MAJOR < 6
raise ArgumentError,
"Multiple database support requires Rails 6.0 or higher. " \
"You are using Rails #{Rails::VERSION::STRING}."
end
end

def materialized_view_update_options
set_options = {no_data: no_data?, side_by_side: side_by_side?}
.select { |_, v| v }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ class <%= migration_class_name %> < <%= activerecord_migration_class %>
<%= method_name %> <%= formatted_plural_name %>,
version: <%= version %>,
revert_to_version: <%= previous_version %>,
materialized: <%= materialized_view_update_options %>
materialized: <%= materialized_view_update_options %><%= update_view_options %>
<%- else -%>
<%= method_name %> <%= formatted_plural_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
<%= method_name %> <%= formatted_plural_name %>, version: <%= version %>, revert_to_version: <%= previous_version %><%= update_view_options %>
<%- end -%>
end
end
73 changes: 67 additions & 6 deletions lib/generators/scenic/view/view_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ class ViewGenerator < Rails::Generators::NamedBase

source_root File.expand_path("templates", __dir__)

def validate_multiple_database_support
validate_rails_version_for_multiple_databases!
end

def create_views_directory
unless views_directory_path.exist?
empty_directory(views_directory_path)
Expand All @@ -29,12 +33,12 @@ def create_migration_file
if creating_new_view? || destroying_initial_view?
migration_template(
"db/migrate/create_view.erb",
"db/migrate/create_#{plural_file_name}.rb"
File.join(migration_directory, "create_#{plural_file_name}.rb")
)
else
migration_template(
"db/migrate/update_view.erb",
"db/migrate/update_#{plural_file_name}_to_version_#{version}.rb"
File.join(migration_directory, "update_#{plural_file_name}_to_version_#{version}.rb")
)
end
end
Expand Down Expand Up @@ -81,7 +85,51 @@ def file_name
end

def views_directory_path
@views_directory_path ||= Rails.root.join("db", "views")
@views_directory_path ||= begin
db_config = ActiveRecord::Base.configurations.configs_for(
env_name: Rails.env,
name: database_name
)

if db_config&.respond_to?(:views_paths) && (configured_path = Array(db_config.views_paths).first)
Rails.root.join(configured_path)
else
conventional_views_path
end
end
end

def conventional_views_path
if database && database != :default
Rails.root.join("db", "#{database}_views")
else
Rails.root.join("db", "views")
end
end

def migration_directory
db_config = ActiveRecord::Base.configurations.configs_for(
env_name: Rails.env,
name: database_name
)

Array(db_config&.migrations_paths).first || conventional_migration_path
end

def database_name
(database || :primary).to_s
end

def conventional_migration_path
if database && database != :default
"db/#{database}_migrate"
else
"db/migrate"
end
end

def different_database_set?
database && database != :default
end

def version_regex
Expand All @@ -93,11 +141,11 @@ def creating_new_view?
end

def definition
Scenic::Definition.new(plural_file_name, version)
Scenic::Definition.new(plural_file_name, version, database)
end

def previous_definition
Scenic::Definition.new(plural_file_name, previous_version)
Scenic::Definition.new(plural_file_name, previous_version, database)
end

def destroying?
Expand All @@ -113,8 +161,21 @@ def formatted_plural_name
end

def create_view_options
options = ""
if materialized?
", materialized: #{no_data? ? "{ no_data: true }" : true}"
options << ", materialized: #{no_data? ? "{ no_data: true }" : true}"
end

if different_database_set?
options << ", database: :#{database}"
end

options
end

def update_view_options
if different_database_set?
", database: :#{database}"
else
""
end
Expand Down
9 changes: 6 additions & 3 deletions lib/scenic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@ def self.load
ActiveRecord::SchemaDumper.prepend Scenic::SchemaDumper
end

# The current database adapter used by Scenic.
# Returns the database adapter for the specified database.
#
# This defaults to {Adapters::Postgres} but can be overridden
# via {Configuration}.
def self.database
configuration.database
#
# @param name [Symbol] the database name (defaults to :default)
# @return [Scenic::Adapters::Postgres] the database adapter
def self.database(name = :default)
configuration.database_adapter(name)
end
end
30 changes: 28 additions & 2 deletions lib/scenic/configuration.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
module Scenic
class Configuration
# Collection of database adapters for multi-database support.
# Access specific databases using database names as keys.
#
# @example
# Scenic.configure do |config|
# config.databases[:secondary] = Scenic::Adapters::Postgres.new(SecondaryRecord)
# end
#
# @return [ActiveSupport::OrderedOptions] hash of database adapters
attr_reader :databases

# The Scenic database adapter instance to use when executing SQL.
#
# Defaults to an instance of {Adapters::Postgres}
# @return Scenic adapter
attr_accessor :database
def database
@databases[:default]
end

def database=(adapter)
@databases[:default] = adapter
end

def initialize
@database = Scenic::Adapters::Postgres.new
@databases = ActiveSupport::OrderedOptions.new
@databases[:default] = Scenic::Adapters::Postgres.new
end

# Returns the database adapter for the specified database name.
#
# @param name [Symbol] the database name (defaults to :default)
# @return [Scenic::Adapters::Postgres] the database adapter
def database_adapter(name = :default)
@databases[name] || @databases[:default]
end
end

Expand Down
10 changes: 8 additions & 2 deletions lib/scenic/definition.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
module Scenic
# @api private
class Definition
def initialize(name, version)
def initialize(name, version, database = nil)
@name = name.to_s
@version = version.to_i
@database = database
end

def to_sql
Expand All @@ -19,7 +20,12 @@ def full_path
end

def path
File.join("db", "views", filename)
views_dir = if @database && @database != :default
"#{@database}_views"
else
"views"
end
File.join("db", views_dir, filename)
end

def version
Expand Down
Loading
Loading