Skip to content

Commit

Permalink
Handle ActiveRecord table name prefix and suffix
Browse files Browse the repository at this point in the history
ActiveRecord allows you to set `table_name_prefix` and
`table_name_suffix` both via configuration (which ultimately sets the
options on `ActiveRecord::Base`) and in any subclass of ActiveRecord.
When set in configuration or directly on `ActiveRecord::Base`, rails
migrations and the schema dumper make the setting transparent to
migrations, `schema.rb`, etc.

For example, if I run the following:

```
ActiveRecord::Base.table_name_prefix = "api_"

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users
  end
end
```

then the table that is created in the database is `app_users`, but
`schema.rb` calls the table `users`. If the user runs `rake
db:schema:load` then the table is created as `app_users` once again.
Setting the prefix or suffix in any way other than in configuration or
directly on `ActiveRecord::Base` does not influence schema generation at
all.

Prior to this change, Scenic didn't handle this situation at all. If you
had a prefix set and tried to run `create_view :searches` Scenic would
fail looking for the file `app_searches_v01.sql`. It turns out, rails
migrations mutate the arguments to most every method call automatically.
See: https://github.com/rails/rails/blob/4607108f91881cd1c24285dca63dc8e0f3f8a4f1/activerecord/lib/active_record/migration.rb#L917-L933

To get around this, I added `Scenic::UnaffixedName` so we could reverse
this process. This is essentially a copy of what Rails itself does in
its own schema dumper here: https://github.com/rails/rails/blob/4607108f91881cd1c24285dca63dc8e0f3f8a4f1/activerecord/lib/active_record/schema_dumper.rb#L295-L299

By unaffixing the view name in two places, we're able to fully support
table name prefix and suffix. This was probably more investigative work
than was warranted for this edge case feature, but I dove in thinking it
was going to be easy and now here we are...

Fixes #295
  • Loading branch information
derekprior committed Dec 17, 2021
1 parent 4b6264d commit b1544dc
Show file tree
Hide file tree
Showing 8 changed files with 71 additions and 3 deletions.
1 change: 1 addition & 0 deletions lib/scenic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require "scenic/railtie"
require "scenic/schema_dumper"
require "scenic/statements"
require "scenic/unaffixed_name"
require "scenic/version"
require "scenic/view"
require "scenic/index"
Expand Down
6 changes: 4 additions & 2 deletions lib/scenic/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module Scenic
# @api private
class Definition
def initialize(name, version)
@name = name
@name = name.to_s
@version = version.to_i
end

Expand All @@ -28,8 +28,10 @@ def version

private

attr_reader :name

def filename
"#{@name.to_s.tr('.', '_')}_v#{version}.sql"
"#{UnaffixedName.for(name).tr('.', '_')}_v#{version}.sql"
end
end
end
31 changes: 31 additions & 0 deletions lib/scenic/unaffixed_name.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module Scenic
# The name of a view or table according to rails.
#
# This removes any table name prefix or suffix that is configured via
# ActiveRecord. This allows, for example, the SchemaDumper to dump a view with
# its unaffixed name, consistent with how rails handles table dumping.
class UnaffixedName
# Gets the unaffixed name for the provided string
# @return [String]
#
# @param name [String] The (potentially) affixed view name
def self.for(name)
new(name, config: ActiveRecord::Base).call
end

def initialize(name, config:)
@name = name
@config = config
end

def call
prefix = Regexp.escape(config.table_name_prefix)
suffix = Regexp.escape(config.table_name_suffix)
name.sub(/\A#{prefix}(.+)#{suffix}\z/, "\\1")
end

private

attr_reader :name, :config
end
end
2 changes: 1 addition & 1 deletion lib/scenic/view.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def to_schema
materialized_option = materialized ? "materialized: true, " : ""

<<-DEFINITION
create_view #{name.inspect}, #{materialized_option}sql_definition: <<-\SQL
create_view #{UnaffixedName.for(name).inspect}, #{materialized_option}sql_definition: <<-\SQL
#{definition.indent(2)}
SQL
DEFINITION
Expand Down
8 changes: 8 additions & 0 deletions spec/scenic/definition_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ module Scenic

expect(definition.path).to eq "db/views/non_public_searches_v01.sql"
end

it "handles active record view prefix and suffixing" do
with_affixed_tables(prefix: "foo_", suffix: "_bar") do
definition = Definition.new("foo_searches_bar", 1)

expect(definition.path).to eq "db/views/searches_v01.sql"
end
end
end

describe "full_path" do
Expand Down
14 changes: 14 additions & 0 deletions spec/scenic/schema_dumper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ class SearchInAHaystack < ActiveRecord::Base
end
end

it "handles active record table name prefixes and suffixes" do
with_affixed_tables(prefix: "a_", suffix: "_z") do
view_definition = "SELECT 'needle'::text AS haystack"
Search.connection.create_view :a_searches_z, sql_definition: view_definition
stream = StringIO.new

ActiveRecord::SchemaDumper.dump(Search.connection, stream)

output = stream.string

expect(output).to include 'create_view "searches"'
end
end

it "ignores tables internal to Rails" do
view_definition = "SELECT 'needle'::text AS haystack"
Search.connection.create_view :searches, sql_definition: view_definition
Expand Down
2 changes: 2 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
require "database_cleaner"

require File.expand_path("dummy/config/environment", __dir__)
require "support/rails_configuration_helpers"
require "support/generator_spec_setup"
require "support/view_definition_helpers"

RSpec.configure do |config|
config.order = "random"
config.include ViewDefinitionHelpers
config.include RailsConfigurationHelpers
DatabaseCleaner.strategy = :transaction

config.around(:each, db: true) do |example|
Expand Down
10 changes: 10 additions & 0 deletions spec/support/rails_configuration_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module RailsConfigurationHelpers
def with_affixed_tables(prefix: "", suffix: "")
ActiveRecord::Base.table_name_prefix = prefix
ActiveRecord::Base.table_name_suffix = suffix
yield
ensure
ActiveRecord::Base.table_name_prefix = ""
ActiveRecord::Base.table_name_suffix = ""
end
end

0 comments on commit b1544dc

Please sign in to comment.