Skip to content

Commit

Permalink
introduce conn.data_source_exists? and conn.data_sources.
Browse files Browse the repository at this point in the history
These new methods are used from the Active Record model layer to
determine which relations are viable to back a model. These new methods
allow us to change `conn.tables` in the future to only return tables and
no views. Same for `conn.table_exists?`.

The goal is to provide the following introspection methods on the
connection:

* `tables`
* `table_exists?`
* `views`
* `view_exists?`
* `data_sources` (views + tables)
* `data_source_exists?` (views + tables)
  • Loading branch information
senny committed Sep 22, 2015
1 parent 1165e9c commit 5a5ffaf
Show file tree
Hide file tree
Showing 16 changed files with 107 additions and 34 deletions.
10 changes: 10 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
* Introduce `connection.data_sources` and `connection.data_source_exists?`.
These methods determine what relations can be used to back Active Record
models (usually tables and views).

Also deprecate `SchemaCache#tables`, `SchemaCache#table_exists?` and
`SchemaCache#clear_table_cache!` in favor of their new data source
counterparts.

*Yves Senn*, *Matthew Draper*

* `ActiveRecord::Tasks::MySQLDatabaseTasks` fails if shellout to
mysql commands (like `mysqldump`) is not successful.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ def table_alias_for(table_name)
table_name[0...table_alias_length].tr('.', '_')
end

# Returns the relations names usuable to back Active Record models.
# Defaults to all #tables and #views.
def data_sources
tables | views
end

# Checks to see if the data source +name+ exists on the database.
#
# data_source_exists?(:ebooks)
#
def data_source_exists?(name)
data_sources.include?(name.to_s)
end

# Returns an array of table names defined in the database.
def tables(name = nil)
raise NotImplementedError, "#tables is not implemented"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ def collation
def tables(name = nil) # :nodoc:
select_values("SHOW FULL TABLES", 'SCHEMA')
end
alias data_sources tables

def truncate(table_name, name = nil)
execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
Expand All @@ -644,6 +645,7 @@ def table_exists?(table_name)

select_values(sql, 'SCHEMA').any?
end
alias data_source_exists? table_exists?

def views # :nodoc:
select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", 'SCHEMA')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ def tables(name = nil)
select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA')
end

def data_sources # :nodoc
select_values(<<-SQL, 'SCHEMA')
SELECT c.relname
FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'v','m') -- (r)elation/table, (v)iew, (m)aterialized view
AND n.nspname = ANY (current_schemas(false))
SQL
end

# Returns true if table exists.
# If the schema is not specified as part of +name+ then it will only find tables within
# the current schema search path (regardless of permissions to access tables in other schemas)
Expand All @@ -89,6 +99,7 @@ def table_exists?(name)
AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
SQL
end
alias data_source_exists? table_exists?

def views # :nodoc:
select_values(<<-SQL, 'SCHEMA')
Expand Down
51 changes: 29 additions & 22 deletions activerecord/lib/active_record/connection_adapters/schema_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,46 @@ def initialize(conn)
@columns = {}
@columns_hash = {}
@primary_keys = {}
@tables = {}
@data_sources = {}
end

def initialize_dup(other)
super
@columns = @columns.dup
@columns_hash = @columns_hash.dup
@primary_keys = @primary_keys.dup
@tables = @tables.dup
@data_sources = @data_sources.dup
end

def primary_keys(table_name)
@primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil
@primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil
end

# A cached lookup for table existence.
def table_exists?(name)
prepare_tables if @tables.empty?
return @tables[name] if @tables.key? name
def data_source_exists?(name)
prepare_data_sources if @data_sources.empty?
return @data_sources[name] if @data_sources.key? name

@tables[name] = connection.table_exists?(name)
@data_sources[name] = connection.data_source_exists?(name)
end
alias table_exists? data_source_exists?
deprecate :table_exists? => "use #data_source_exists? instead"


# Add internal cache for table with +table_name+.
def add(table_name)
if table_exists?(table_name)
if data_source_exists?(table_name)
primary_keys(table_name)
columns(table_name)
columns_hash(table_name)
end
end

def tables(name)
@tables[name]
def data_sources(name)
@data_sources[name]
end
alias tables data_sources
deprecate :tables => "use #data_sources instead"

# Get the columns for a table
def columns(table_name)
Expand All @@ -64,36 +69,38 @@ def clear!
@columns.clear
@columns_hash.clear
@primary_keys.clear
@tables.clear
@data_sources.clear
@version = nil
end

def size
[@columns, @columns_hash, @primary_keys, @tables].map(&:size).inject :+
[@columns, @columns_hash, @primary_keys, @data_sources].map(&:size).inject :+
end

# Clear out internal caches for table with +table_name+.
def clear_table_cache!(table_name)
@columns.delete table_name
@columns_hash.delete table_name
@primary_keys.delete table_name
@tables.delete table_name
# Clear out internal caches for the data source +name+.
def clear_data_source_cache!(name)
@columns.delete name
@columns_hash.delete name
@primary_keys.delete name
@data_sources.delete name
end
alias clear_table_cache! clear_data_source_cache!
deprecate :clear_table_cache! => "use #clear_data_source_cache! instead"

def marshal_dump
# if we get current version during initialization, it happens stack over flow.
@version = ActiveRecord::Migrator.current_version
[@version, @columns, @columns_hash, @primary_keys, @tables]
[@version, @columns, @columns_hash, @primary_keys, @data_sources]
end

def marshal_load(array)
@version, @columns, @columns_hash, @primary_keys, @tables = array
@version, @columns, @columns_hash, @primary_keys, @data_sources = array
end

private

def prepare_tables
connection.tables.each { |table| @tables[table] = true }
def prepare_data_sources
connection.data_sources.each { |source| @data_sources[source] = true }
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,10 +319,12 @@ def tables(name = nil, table_name = nil) #:nodoc:
row['name']
end
end
alias data_sources tables

def table_exists?(table_name)
table_name && tables(nil, table_name).any?
end
alias data_source_exists? table_exists?

def views # :nodoc:
select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", 'SCHEMA')
Expand Down
4 changes: 2 additions & 2 deletions activerecord/lib/active_record/model_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ def sequence_name=(value)

# Indicates whether the table associated with this class exists
def table_exists?
connection.schema_cache.table_exists?(table_name)
connection.schema_cache.data_source_exists?(table_name)
end

def attributes_builder # :nodoc:
Expand Down Expand Up @@ -290,7 +290,7 @@ def content_columns
def reset_column_information
connection.clear_cache!
undefine_attribute_methods
connection.schema_cache.clear_table_cache!(table_name)
connection.schema_cache.clear_data_source_cache!(table_name)

reload_schema_from_cache
end
Expand Down
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/railties/databases.rake
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ db_namespace = namespace :db do
filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")

con.schema_cache.clear!
con.tables.each { |table| con.schema_cache.add(table) }
con.data_sources.each { |table| con.schema_cache.add(table) }
open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) }
end

Expand Down
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/schema_dumper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def extensions(stream)
end

def tables(stream)
sorted_tables = @connection.tables.sort - @connection.views
sorted_tables = @connection.data_sources.sort - @connection.views

sorted_tables.each do |table_name|
table(table_name, stream) unless ignored?(table_name)
Expand Down
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/type_caster/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def type_cast_for_database(attribute_name, value)
private

def column_for(attribute_name)
if connection.schema_cache.table_exists?(table_name)
if connection.schema_cache.data_source_exists?(table_name)
connection.schema_cache.columns_hash(table_name)[attribute_name.to_s]
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def fake_connection(config)

module ConnectionAdapters
class FakeAdapter < AbstractAdapter
attr_accessor :tables, :primary_keys
attr_accessor :data_sources, :primary_keys

@columns = Hash.new { |h,k| h[k] = [] }
class << self
Expand All @@ -16,7 +16,7 @@ class << self

def initialize(connection, logger)
super
@tables = []
@data_sources = []
@primary_keys = {}
@columns = self.class.columns
end
Expand All @@ -37,7 +37,7 @@ def columns(table_name)
@columns[table_name]
end

def table_exists?(*)
def data_source_exists?(*)
true
end

Expand Down
15 changes: 15 additions & 0 deletions activerecord/test/cases/adapter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ def test_table_exists?
assert !@connection.table_exists?(nil)
end

def test_data_sources
tables = @connection.data_sources
assert tables.include?("accounts")
assert tables.include?("authors")
assert tables.include?("tasks")
assert tables.include?("topics")
end

def test_data_source_exists?
assert @connection.data_source_exists?("accounts")
assert @connection.data_source_exists?(:accounts)
assert_not @connection.data_source_exists?("nonexistingtable")
assert_not @connection.data_source_exists?(nil)
end

def test_indexes
idx_name = "accounts_idx"

Expand Down
2 changes: 2 additions & 0 deletions activerecord/test/cases/adapters/postgresql/schema_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
module PGSchemaHelper
def with_schema_search_path(schema_search_path)
@connection.schema_search_path = schema_search_path
@connection.schema_cache.clear!
yield if block_given?
ensure
@connection.schema_search_path = "'$user', public"
@connection.schema_cache.clear!
end
end

Expand Down
11 changes: 8 additions & 3 deletions activerecord/test/cases/connection_adapters/schema_cache_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_caches_columns_hash
def test_clearing
@cache.columns('posts')
@cache.columns_hash('posts')
@cache.tables('posts')
@cache.data_sources('posts')
@cache.primary_keys('posts')

@cache.clear!
Expand All @@ -40,17 +40,22 @@ def test_clearing
def test_dump_and_load
@cache.columns('posts')
@cache.columns_hash('posts')
@cache.tables('posts')
@cache.data_sources('posts')
@cache.primary_keys('posts')

@cache = Marshal.load(Marshal.dump(@cache))

assert_equal 11, @cache.columns('posts').size
assert_equal 11, @cache.columns_hash('posts').size
assert @cache.tables('posts')
assert @cache.data_sources('posts')
assert_equal 'id', @cache.primary_keys('posts')
end

def test_table_methods_deprecation
assert_deprecated { assert @cache.table_exists?('posts') }
assert_deprecated { assert @cache.tables('posts') }
assert_deprecated { @cache.clear_table_cache!('posts') }
end
end
end
end
5 changes: 5 additions & 0 deletions activerecord/test/cases/view_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ def test_table_exists
assert @connection.table_exists?(view_name), "'#{view_name}' table should exist"
end

def test_views_ara_valid_data_sources
view_name = Ebook.table_name
assert @connection.view_exists?(view_name), "'#{view_name}' should be a data source"
end

def test_column_definitions
assert_equal([["id", :integer],
["name", :string],
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/models/contact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ def self.extended(base)
base.class_eval do
establish_connection(:adapter => 'fake')

connection.tables = [table_name]
connection.data_sources = [table_name]
connection.primary_keys = {
table_name => 'id'
}
Expand Down

0 comments on commit 5a5ffaf

Please sign in to comment.