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
121 changes: 121 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,126 @@ class UpdateSearchResultsToVersion2 < ActiveRecord::Migration
end
```

## Can I use Scenic with multiple databases?

Yes! Scenic supports Rails' multiple database feature introduced in Rails 6.0.
This allows you to manage views across different databases in your application.

### Requirements

- Rails 6.0 or higher
- Multiple databases configured in your `database.yml`

### Configuring Database Adapters

If you need to customize the database adapter for a specific database, you can
configure it in an initializer:

```ruby
# config/initializers/scenic.rb
Scenic.configure do |config|
# The default database adapter is automatically configured
# config.database = Scenic::Adapters::Postgres.new

# Configure adapters for additional databases
config.databases[:secondary] = Scenic::Adapters::Postgres.new(SecondaryRecord)
end
```

### Generating Views for Specific Databases

Use the `--database` option to specify which database a view should be created in:

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

This will:
- Create the view definition in `db/views_secondary/` (instead of `db/views/`)
- Create the migration in your database-specific migrations directory

The migration will include the `database:` parameter:

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

### Working with Multiple Database Views

All Scenic methods (`create_view`, `update_view`, `replace_view`, `drop_view`)
support the `database:` parameter:

```ruby
# In a migration
def change
create_view :reports, version: 1, database: :secondary
update_view :analytics, version: 2, database: :secondary
drop_view :old_stats, database: :secondary
end
```

### Running Migrations

Use Rails' standard rake tasks for running migrations on specific databases:

```sh
$ rake db:migrate:secondary # Run pending migrations
$ rake db:rollback:secondary # Rollback last migration
```

### Example: Multiple Database Setup

Here's a complete example of using Scenic with multiple databases:

```yaml
# config/database.yml
production:
primary:
<<: *default
database: my_app_production
analytics:
<<: *default
database: my_app_analytics
migrations_paths: db/migrate_analytics
```

```ruby
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :primary, reading: :primary }
end

# app/models/analytics_record.rb
class AnalyticsRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: { writing: :analytics, reading: :analytics }
end

# app/models/user_activity_summary.rb
class UserActivitySummary < AnalyticsRecord
# This view is defined in the analytics database
def readonly?
true
end
end
```

Generate the view:

```sh
$ rails generate scenic:model user_activity_summary --database=analytics
```

The generated migration will automatically include `database: :analytics` and will
be placed in your database-specific migrations directory.

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

Scenic gives you `drop_view` too:
Expand All @@ -232,6 +352,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
1 change: 1 addition & 0 deletions lib/generators/scenic/model/model_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module Generators
# @api private
class ModelGenerator < Rails::Generators::NamedBase
include Scenic::Generators::Materializable

source_root File.expand_path("templates", __dir__)

def invoke_rails_model_generator
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
49 changes: 43 additions & 6 deletions lib/generators/scenic/view/view_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ module Generators
class ViewGenerator < Rails::Generators::NamedBase
include Rails::Generators::Migration
include Scenic::Generators::Materializable

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 @@ -28,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 @@ -80,7 +85,26 @@ def file_name
end

def views_directory_path
@views_directory_path ||= Rails.root.join("db", "views")
@views_directory_path ||= if database && database != :default
Rails.root.join("db", "views_#{database}")
else
Rails.root.join("db", "views")
end
end

def migration_directory
if different_database_set?
db_config = ActiveRecord::Base.configurations.configs_for(
env_name: Rails.env,
name: database.to_s
)
end

db_config&.migrations_paths || "db/migrate"
end

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

def version_regex
Expand All @@ -92,11 +116,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 @@ -112,8 +136,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
"views_#{@database}"
else
"views"
end
File.join("db", views_dir, filename)
end

def version
Expand Down
Loading