Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support PostgreSQL materialized views #13128

Closed
wants to merge 5 commits into from
Closed
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
4 changes: 4 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
* Enable support for materialized views on PostgreSQL >= 9.3.

*Dave Lee*

* Previously, the `has_one` macro incorrectly accepted the `counter_cache`
option, but never actually supported it. Now it will raise an `ArgumentError`
when using `has_one` with `counter_cache`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,11 @@ def quote_string(s) #:nodoc:
# - "schema.name".table_name
# - "schema.name"."table.name"
def quote_table_name(name)
schema, name_part = extract_pg_identifier_from_name(name.to_s)
schema, table_name = Utils.extract_schema_and_table(name.to_s)

unless name_part
quote_column_name(schema)
if schema.blank?
quote_column_name(table_name)
else
table_name, name_part = extract_pg_identifier_from_name(name_part)
"#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,12 @@ def table_exists?(name)
schema, table = Utils.extract_schema_and_table(name.to_s)
return false unless table

binds = [[nil, table]]
binds << [nil, schema] if schema

exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
SELECT COUNT(*)
FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind in ('v','r')
AND c.relname = '#{table.gsub(/(^"|"$)/,'')}'
WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view
AND c.relname = '#{table}'
AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
SQL
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,10 @@ def supports_ranges?
postgresql_version >= 90200
end

def supports_materialized_views?
postgresql_version >= 90300
end

def enable_extension(name)
exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap {
reload_type_map
Expand Down Expand Up @@ -940,16 +944,6 @@ def column_definitions(table_name) #:nodoc:
end_sql
end

def extract_pg_identifier_from_name(name)
match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)

if match_data
rest = name[match_data[0].length, name.length]
rest = rest[1, rest.length] if rest.start_with? "."
[match_data[1], (rest.length > 0 ? rest : nil)]
end
end

def extract_table_ref_from_insert_sql(sql)
sql[/into\s+([^\(]*).*values\s*\(/i]
$1.strip if $1
Expand Down
30 changes: 23 additions & 7 deletions activerecord/test/cases/adapters/postgresql/view_test.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
require "cases/helper"

class ViewTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
module ViewTestConcern
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to reopen the same module in the same file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did that purely for top-down readability. If I define ViewTestConcern entirely up front, that pushes the two test class definitions to the bottom of the file. I thought it would be preferable to put them at the top. So no technical reason, it can be changed.

extend ActiveSupport::Concern

included do
self.use_transactional_fixtures = false
mattr_accessor :view_type
end

SCHEMA_NAME = 'test_schema'
TABLE_NAME = 'things'
VIEW_NAME = 'view_things'
COLUMNS = [
'id integer',
'name character varying(50)',
Expand All @@ -14,14 +18,14 @@ class ViewTest < ActiveRecord::TestCase
]

class ThingView < ActiveRecord::Base
self.table_name = 'test_schema.view_things'
end

def setup
ThingView.table_name = "#{SCHEMA_NAME}.#{view_type}_things"

@connection = ActiveRecord::Base.connection
@connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
@connection.execute "CREATE TABLE #{SCHEMA_NAME}.\"#{TABLE_NAME}.table\" (#{COLUMNS.join(',')})"
@connection.execute "CREATE VIEW #{SCHEMA_NAME}.#{VIEW_NAME} AS SELECT id,name,email,moment FROM #{SCHEMA_NAME}.#{TABLE_NAME}"
@connection.execute "CREATE #{view_type.humanize} #{ThingView.table_name} AS SELECT * FROM #{SCHEMA_NAME}.#{TABLE_NAME}"
end

def teardown
Expand All @@ -35,7 +39,7 @@ def test_table_exists

def test_column_definitions
assert_nothing_raised do
assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{VIEW_NAME}")
assert_equal COLUMNS, columns(ThingView.table_name)
end
end

Expand All @@ -47,3 +51,15 @@ def columns(table_name)
end

end

class ViewTest < ActiveRecord::TestCase
include ViewTestConcern
self.view_type = 'view'
end

if ActiveRecord::Base.connection.supports_materialized_views?
class MaterializedViewTest < ActiveRecord::TestCase
include ViewTestConcern
self.view_type = 'materialized_view'
end
end