Scenic: versioned database views for Rails without leaving schema.rb
Versioned database views for Rails
At a glance
- What is it?
- Scenic adds create_view, update_view and replace_view to ActiveRecord migrations and keeps each view definition in a numbered SQL file. It is for Rails teams on PostgreSQL that want SQL views without switching the schema format to SQL.
- Who is it for?
- Adopt Scenic if you run Rails on PostgreSQL and want views defined in SQL files that stay reversible in schema.rb; skip it if your database is MySQL or SQLite and no third-party adapter fits, or if you need to change a materialized view in place, since the README states materialized views cannot be replaced that way.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 93 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem Scenic solves for Rails teams on PostgreSQL
Rails defaults to dumping schema.rb, which cannot express a database view. A team that wants a view has two options: switch the schema format to SQL, or paste the same CREATE VIEW statement into migration after migration. Scenic takes a third path. It adds methods to ActiveRecord::Migration for creating and managing database views while keeping schema.rb as the dump format.
The audience is narrow and specific: Rails applications on PostgreSQL that already use views, or want to promote a recurring query into a first-class object. The README frames the benefit as a convention for versioning views that keeps migration history consistent and reversible and avoids duplicating SQL strings across migrations. Defining the view in a .sql file also means the editor gives full SQL syntax highlighting, and the query can be tested directly in a database console during development. None of that matters if your schema is small enough that a scope or a plain ARel query does the job.
How the versioning convention works: numbered SQL files and reversible migrations
The core mechanism is a naming convention. Each view lives in db/views as a file named after the view and a two-digit version, such as search_results_v01.sql. The generator writes that file and a migration that references the version. Because the version is part of the filename and part of the migration, the migration is reversible: reverting to version 1 restores the previous definition rather than dropping the view entirely.
The README describes what happens on the second run. Scenic detects the existing search_results view at version 1, copies that definition to version 2, and generates a migration to move to the version 2 schema. The developer edits the new SQL file and runs the migration. The default update_view drops the view and recreates it, which is a problem when other views depend on it. For that case Scenic offers replace_view, which emits CREATE OR REPLACE VIEW and updates the view in place while retaining dependencies. The README is explicit that materialized views cannot be replaced this way, and points to the side_by_side update strategy as an alternative that may produce similar results. That strategy is named but not documented in the README, so treat it as something to read up on in the source before relying on it.
Installing Scenic and creating your first view
The README gives one install step for PostgreSQL: add the gem to the Gemfile and run bundle install.
gem "scenic"$ bundle installIf your database is not PostgreSQL, the README directs you to the third-party adapters linked from the project, and does not list them inline.
Generate a view. The generator produces a numbered SQL file and a migration in one command.
$ rails generate scenic:view search_results
create db/views/search_results_v01.sql
create db/migrate/[TIMESTAMP]_create_search_results.rbOpen db/views/search_results_v01.sql and write the SELECT that defines the view. The generated migration contains a create_view statement. Run it, and the README states the migration is reversible and the schema is dumped into schema.rb.
$ rake db:migrateTo change the view later, run the same generator again. Scenic creates search_results_v02.sql and a migration named update_search_results_to_version_2. Edit the version 2 file and migrate. If you need to change the view without dropping it, pass --replace to the generator; the resulting migration calls replace_view with version and revert_to_version arguments, as the README shows.
class UpdateSearchResultsToVersion2 < ActiveRecord::Migration
def change
replace_view :search_results, version: 2, revert_to_version: 1
end
endTo back the view with a model, use the scenic:model generator, which the README describes as a superset of scenic:view. It behaves like the Rails model generator but creates a Scenic view migration instead of a table migration. One detail matters: the README states that if you want Model.find to work, you must declare the primary key yourself, because it cannot be inferred from column information.
Materialized views and the refresh method Scenic generates
Both the scenic:view and scenic:model generators accept a --materialized option. With the model generator, the README states the model gets a refresh class method as a convenience for scheduling refreshes.
def self.refresh
Scenic.database.refresh_materialized_view(table_name, concurrently: false, cascade: false)
endThe default is a non-concurrent refresh, which locks the view for selects until the refresh completes. Passing concurrently: true avoids the lock, but the README states this requires both PostgreSQL 9.4 and at least one unique index on the view that covers all rows. That is a real constraint on the shape of the view, not a flag you can flip on any query. The cascade option exists for materialized views that depend on other materialized views: if view A selects from materialized view B, cascade is what brings A up to date after B refreshes.
Indexes are handled through ordinary table migration methods. The README states you can add or update indexes for materialized views with add_index table_name, and that those indexes are automatically re-applied when views are updated. That automatic re-application is the part worth verifying in your own setup, because it determines whether a view update silently drops an index you depend on for refresh performance.
Where Scenic is the wrong tool
The clearest boundary is the database. Scenic ships with support for PostgreSQL, and the README says so directly: if you are using something other than Postgres, check the third-party adapters. The adapter interface is configurable through Scenic::Configuration and kept minimal in Scenic::Adapters::Postgres so other gems can implement it, but that means non-PostgreSQL support is somebody else's gem, with its own release cadence and gaps. A project that needs views on MySQL or SQLite should evaluate that adapter first, not Scenic.
The second boundary is materialized views that need in-place changes. replace_view emits CREATE OR REPLACE VIEW, and the README states materialized views cannot be replaced in this fashion. If your materialized view sits under a hierarchy of dependent objects and you cannot tolerate a drop and recreate, Scenic's default path does not help you, and the side_by_side strategy is mentioned without documentation.
The third is conceptual. View-backed models are read-only in practice. The README's example model defines readonly? returning true and notes that preventing save is not strictly necessary but avoids a call that would fail anyway. If the object needs to be written to, a view is the wrong abstraction.
Scenic compared with the Rails schema formats and hand-written SQL migrations
The alternative most teams reach for is switching config.active_record.schema_format to :sql and managing the whole schema as one SQL dump. That gives you views and every other PostgreSQL feature, but you lose schema.rb, and every migration diff becomes a diff of a large SQL file. Scenic keeps schema.rb as the dump format and only moves the view bodies into separate files, so the rest of the schema stays in the Ruby format your team already reviews.
The other alternative is writing create_view calls by hand in each migration. That works, and Scenic is a thin layer over the same SQL. The difference is the versioning convention: numbered files, generators that detect the current version, and migrations that carry revert_to_version so the change is reversible. Hand-written migrations give you no filename convention and no automatic copy of the previous definition when you need a new version, which is exactly the duplication the README sets out to avoid.
Scenic does not replace ActiveRecord query objects or database functions. A view is a named query the database stores; if the goal is to compose queries at runtime, a scope or a query object is simpler and needs no migration.
Maintenance status, licence and upgrade cost
The repository is not archived, and the last push was on 2026-06-29. That is recent enough to treat the project as maintained, but the release history tells a different story about version numbers: v1.4.0 dates from 2017-05-19, v1.3.0 from 2016-05-27, and v1.2.0 from 2016-02-06. Commits continue; tagged releases have not. A team that pins to a released gem should check whether the behaviour it needs exists in v1.4.0 or only on main, because the README documents features that may postdate the last tag.
The licence is MIT, which permits commercial use and modification; the repository carries LICENSE.txt. That is a permissive licence with no copyleft obligation on your application, but this is not legal advice and the file is short enough to read.
Upgrade cost is mostly about the migrations you generate. Each view version adds a file under db/views and a migration. Long-lived projects accumulate version files, and the README does not document deleting old ones, so plan for that directory to grow. A replace_view migration is cheaper to run than a drop and recreate because it does not invalidate dependent objects, but it also means a failed replace leaves the old definition in place rather than a missing view.
Editorial conclusion
Adopt Scenic if you run Rails on PostgreSQL and want views defined in SQL files that stay reversible in schema.rb; skip it if your database is MySQL or SQLite and no third-party adapter fits, or if you need to change a materialized view in place, since the README states materialized views cannot be replaced that way. Before committing, verify that the primary key of any view-backed model is declared explicitly, because the README states it cannot be inferred from column information.
Frequently asked questions
How do I install Scenic in a Rails app?
Add gem "scenic" to your Gemfile and run bundle install. The README gives this step for PostgreSQL; for other databases it points to third-party adapters.
How do I use Scenic to create a database view?
Run rails generate scenic:view search_results, which creates db/views/search_results_v01.sql and a migration. Edit the SQL file with the SELECT that defines the view, then run rake db:migrate.
Can Scenic change a view without dropping it?
Yes, by passing --replace to the scenic:view generator, which produces a replace_view migration emitting CREATE OR REPLACE VIEW and retaining dependencies. The README states materialized views cannot be replaced this way.
Does Scenic support materialized views?
The scenic:view and scenic:model generators accept a --materialized option, and the model generator adds a self.refresh method that calls Scenic.database.refresh_materialized_view. Concurrent refreshes require PostgreSQL 9.4 and a unique index covering all rows.
Which databases does Scenic work with?
Scenic ships with support for PostgreSQL. The adapter is configurable through Scenic::Configuration and the interface in Scenic::Adapters::Postgres is minimal so other gems can provide adapters, which the README links to for non-Postgres databases.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/scenic-views-scenic)