aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrey Novikov <envek@envek.name>2016-01-03 21:13:53 +0300
committerAndrey Novikov <envek@envek.name>2016-04-16 10:17:26 +0300
commitc690b9ce39c893077b48fa5b27af87e995f97e9b (patch)
tree925a00ba04b8a144041b6c823bd8bceacd6084f1
parent39e087cbf5628ecc351fc86a3a1e320be193bf29 (diff)
downloadrails-c690b9ce39c893077b48fa5b27af87e995f97e9b.tar.gz
rails-c690b9ce39c893077b48fa5b27af87e995f97e9b.tar.bz2
rails-c690b9ce39c893077b48fa5b27af87e995f97e9b.zip
Add support for specifying comments for tables, columns, and indexes.
Comments are specified in migrations, stored in database itself (in its schema), and dumped into db/schema.rb file. This allows to generate good documentation and explain columns and tables' purpose to everyone from new developers to database administrators. For PostgreSQL and MySQL only. SQLite does not support comments at the moment. See docs for PostgreSQL: http://www.postgresql.org/docs/current/static/sql-comment.html See docs for MySQL: http://dev.mysql.com/doc/refman/5.7/en/create-table.html
-rw-r--r--activerecord/CHANGELOG.md17
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb36
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb33
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb13
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb4
-rw-r--r--activerecord/test/cases/comment_test.rb91
-rw-r--r--guides/source/active_record_migrations.md8
15 files changed, 286 insertions, 37 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index e6cbd6bd68..2562e26a11 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,20 @@
+* Add support for database schema comments for tables, columns and indexes
+ for PostgreSQL and MySQL.
+
+ It allows to specify commentaries for database objects in migrations and
+ store them in database itself, allowing to see them with DBA tools and
+ in `db/schema.rb` file and thus automatically documents database schema:
+
+ create_table "pages", force: :cascade, comment: 'Arbitrary content pages' do |t|
+ # ...
+ t.string "path", comment: "Path fragment of page URL used for routing"
+ t.string "locale", comment: "RFC 3066 locale code of website language section"
+ t.index ["locale", "path"], name: 'page_uri_index' comment: "Main index used to lookup page by it's URI."
+ # ...
+ end
+
+ *Andrey Novikov*
+
* Add `quoted_time` for truncating the date part of a TIME column value.
This fixes queries on TIME column on MariaDB, as it doesn't ignore the
date part of the string when it coerces to time.
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
index 0ba4d94e3c..f3f1618473 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -54,6 +54,7 @@ module ActiveRecord
end
create_sql << "(#{statements.join(', ')}) " if statements.present?
+ add_table_options!(create_sql, table_options(o))
create_sql << "#{o.options}"
create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
create_sql
@@ -82,6 +83,16 @@ module ActiveRecord
"DROP CONSTRAINT #{quote_column_name(name)}"
end
+ def table_options(o)
+ options = {}
+ options[:comment] = o.comment
+ options
+ end
+
+ def add_table_options!(sql, _options)
+ sql
+ end
+
def column_options(o)
column_options = {}
column_options[:null] = o.null unless o.null.nil?
@@ -92,6 +103,7 @@ module ActiveRecord
column_options[:auto_increment] = o.auto_increment
column_options[:primary_key] = o.primary_key
column_options[:collation] = o.collation
+ column_options[:comment] = o.comment
column_options
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 4f97c7c065..1603c38e35 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -3,14 +3,14 @@ module ActiveRecord
# Abstract representation of an index definition on a table. Instances of
# this type are typically created and returned by methods in database
# adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
- class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using) #:nodoc:
+ class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment) #:nodoc:
end
# Abstract representation of a column definition. Instances of this type
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type) #:nodoc:
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment) #:nodoc:
def primary_key?
primary_key || type.to_sym == :primary_key
@@ -207,9 +207,9 @@ module ActiveRecord
include ColumnMethods
attr_accessor :indexes
- attr_reader :name, :temporary, :options, :as, :foreign_keys
+ attr_reader :name, :temporary, :options, :as, :foreign_keys, :comment
- def initialize(name, temporary, options, as = nil)
+ def initialize(name, temporary, options, as = nil, comment = nil)
@columns_hash = {}
@indexes = {}
@foreign_keys = []
@@ -218,6 +218,7 @@ module ActiveRecord
@options = options
@as = as
@name = name
+ @comment = comment
end
def primary_keys(name = nil) # :nodoc:
@@ -373,6 +374,7 @@ module ActiveRecord
column.auto_increment = options[:auto_increment]
column.primary_key = type == :primary_key || options[:primary_key]
column.collation = options[:collation]
+ column.comment = options[:comment]
column
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
index 4880d216d6..69d2780f7c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -46,12 +46,14 @@ module ActiveRecord
spec[:collation] = collation
end
+ spec[:comment] = column.comment.inspect if column.comment
+
spec
end
# Lists the valid migration options
def migration_keys
- [:name, :limit, :precision, :scale, :default, :null, :collation]
+ [:name, :limit, :precision, :scale, :default, :null, :collation, :comment]
end
private
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 020d9bbdca..3bbd5a9515 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -18,6 +18,11 @@ module ActiveRecord
nil
end
+ # Returns comment associated with given table in database
+ def table_comment(table_name)
+ nil
+ end
+
# Truncates a table alias according to the limits of the current adapter.
def table_alias_for(table_name)
table_name[0...table_alias_length].tr('.', '_')
@@ -255,7 +260,7 @@ module ActiveRecord
#
# See also TableDefinition#column for details on how to create columns.
def create_table(table_name, options = {})
- td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
+ td = create_table_definition table_name, options[:temporary], options[:options], options[:as], options[:comment]
if options[:id] != false && !options[:as]
pk = options.fetch(:primary_key) do
@@ -265,7 +270,7 @@ module ActiveRecord
if pk.is_a?(Array)
td.primary_keys pk
else
- td.primary_key pk, options.fetch(:id, :primary_key), options
+ td.primary_key pk, options.fetch(:id, :primary_key), options.except(:comment)
end
end
@@ -283,6 +288,13 @@ module ActiveRecord
end
end
+ if supports_comments? && !supports_comments_in_create?
+ change_table_comment(table_name, options[:comment]) if options[:comment]
+ td.columns.each do |column|
+ change_column_comment(table_name, column.name, column.comment) if column.comment
+ end
+ end
+
result
end
@@ -1078,7 +1090,7 @@ module ActiveRecord
def add_index_options(table_name, column_name, options = {}) #:nodoc:
column_names = Array(column_name)
- options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
+ options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :comment)
index_type = options[:type].to_s if options.key?(:type)
index_type ||= options[:unique] ? "UNIQUE" : ""
@@ -1106,13 +1118,25 @@ module ActiveRecord
end
index_columns = quoted_columns_for_index(column_names, options).join(", ")
- [index_name, index_type, index_columns, index_options, algorithm, using]
+ comment = options[:comment] if options.key?(:comment)
+
+ [index_name, index_type, index_columns, index_options, algorithm, using, comment]
end
def options_include_default?(options)
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
+ # Adds comment for given table or drops it if +nil+ given
+ def change_table_comment(table_name, comment)
+ raise NotImplementedError, "change_table_comment is not implemented"
+ end
+
+ # Adds comment for given table column or drops it if +nil+ given
+ def change_column_comment(table_name, column_name, comment) #:nodoc:
+ raise NotImplementedError, "change_column_comment is not implemented"
+ end
+
protected
def add_index_sort_order(option_strings, column_names, options = {})
if options.is_a?(Hash) && order = options[:order]
@@ -1194,8 +1218,8 @@ module ActiveRecord
end
private
- def create_table_definition(name, temporary = false, options = nil, as = nil)
- TableDefinition.new(name, temporary, options, as)
+ def create_table_definition(name, temporary = false, options = nil, as = nil, comment = nil)
+ TableDefinition.new(name, temporary, options, as, comment)
end
def create_alter_table(name)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 069346253a..b753c348de 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -278,6 +278,16 @@ module ActiveRecord
false
end
+ # Does adapter supports comments on database objects (tables, columns, indexes)?
+ def supports_comments?
+ false
+ end
+
+ # Can comments for tables, columns, and indexes be specified in create/alter table statements?
+ def supports_comments_in_create?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index 8bd9da4fbc..cea5a04006 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -160,8 +160,8 @@ module ActiveRecord
raise NotImplementedError
end
- def new_column(field, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil) # :nodoc:
- MySQL::Column.new(field, default, sql_type_metadata, null, table_name, default_function, collation)
+ def new_column(field, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil, comment = nil) # :nodoc:
+ MySQL::Column.new(field, default, sql_type_metadata, null, table_name, default_function, collation, comment)
end
# Must return the MySQL error number from the exception, if the exception has an
@@ -373,7 +373,7 @@ module ActiveRecord
mysql_index_type = row[:Index_type].downcase.to_sym
index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil
index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil
- indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using)
+ indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using, row[:Index_comment].presence)
end
indexes.last.columns << row[:Column_name]
@@ -394,10 +394,18 @@ module ActiveRecord
else
default, default_function = field[:Default], nil
end
- new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation])
+ new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation], field[:Comment].presence)
end
end
+ def table_comment(table_name)
+ select_value(<<-SQL.strip_heredoc, 'SCHEMA')
+ SELECT table_comment
+ FROM INFORMATION_SCHEMA.TABLES
+ WHERE table_name=#{quote(table_name)};
+ SQL
+ end
+
def create_table(table_name, options = {}) #:nodoc:
super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
end
@@ -482,8 +490,10 @@ module ActiveRecord
end
def add_index(table_name, column_name, options = {}) #:nodoc:
- index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options)
- execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}"
+ index_name, index_type, index_columns, _, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options)
+ sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}"
+ sql << " COMMENT #{quote(comment)}" if comment
+ execute sql
end
def foreign_keys(table_name)
@@ -521,7 +531,12 @@ module ActiveRecord
raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip
# strip AUTO_INCREMENT
- raw_table_options.sub(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1')
+ raw_table_options.sub!(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1')
+
+ # strip COMMENT
+ raw_table_options.sub!(/ COMMENT='.+'/, '')
+
+ raw_table_options
end
# Maps logical Rails types to MySQL-specific data types.
@@ -866,8 +881,8 @@ module ActiveRecord
create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
end
- def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
- MySQL::TableDefinition.new(name, temporary, options, as)
+ def create_table_definition(name, temporary = false, options = nil, as = nil, comment = nil) # :nodoc:
+ MySQL::TableDefinition.new(name, temporary, options, as, comment)
end
def integer_to_sql(limit) # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 2e718b29fa..1c798d99f2 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -5,7 +5,7 @@ module ActiveRecord
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
- attr_reader :name, :default, :sql_type_metadata, :null, :table_name, :default_function, :collation
+ attr_reader :name, :default, :sql_type_metadata, :null, :table_name, :default_function, :collation, :comment
delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
@@ -15,7 +15,7 @@ module ActiveRecord
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
# +sql_type_metadata+ is various information about the type of the column
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil)
+ def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment = nil)
@name = name.freeze
@table_name = table_name
@sql_type_metadata = sql_type_metadata
@@ -23,6 +23,7 @@ module ActiveRecord
@default = default
@default_function = default_function
@collation = collation
+ @comment = comment
end
def has_default?
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
index 1e2c859af9..5ab81640e8 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
@@ -2,6 +2,9 @@ module ActiveRecord
module ConnectionAdapters
module MySQL
class SchemaCreation < AbstractAdapter::SchemaCreation
+ delegate :quote, to: :@conn
+ private :quote
+
private
def visit_DropForeignKey(name)
@@ -22,6 +25,13 @@ module ActiveRecord
add_column_position!(change_column_sql, column_options(o.column))
end
+ def add_table_options!(sql, options)
+ super
+ if options[:comment]
+ sql << "COMMENT #{quote(options[:comment])} "
+ end
+ end
+
def column_options(o)
column_options = super
column_options[:charset] = o.charset
@@ -35,7 +45,11 @@ module ActiveRecord
if options[:collation]
sql << " COLLATE #{options[:collation]}"
end
- super
+ new_sql = super
+ if options[:comment]
+ new_sql << " COMMENT #{quote(options[:comment])}"
+ end
+ new_sql
end
def add_column_position!(sql, options)
@@ -48,8 +62,9 @@ module ActiveRecord
end
def index_in_create(table_name, column_name, options)
- index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options)
- "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) "
+ index_name, index_type, index_columns, _, _, index_using, comment = @conn.add_index_options(table_name, column_name, options)
+ index_option = " COMMENT #{quote(comment)}" if comment
+ "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_option} "
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index e7541748de..ec343a5a57 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -45,6 +45,14 @@ module ActiveRecord
!mariadb? && version >= '5.7.8'
end
+ def supports_comments?
+ true
+ end
+
+ def supports_comments_in_create?
+ true
+ end
+
# HELPER METHODS ===========================================
def each_hash(result) # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 39e8492688..36331204f7 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/string/strip'
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -172,7 +174,8 @@ module ActiveRecord
table = Utils.extract_schema_qualified_name(table_name.to_s)
result = query(<<-SQL, 'SCHEMA')
- SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
+ pg_catalog.obj_description(i.oid, 'pg_class') AS comment
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
@@ -190,6 +193,7 @@ module ActiveRecord
indkey = row[2].split(" ").map(&:to_i)
inddef = row[3]
oid = row[4]
+ comment = row[5]
columns = Hash[query(<<-SQL, "SCHEMA")]
SELECT a.attnum, a.attname
@@ -207,7 +211,7 @@ module ActiveRecord
where = inddef.scan(/WHERE (.+)$/).flatten[0]
using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
- IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
+ IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using, comment)
end
end.compact
end
@@ -215,18 +219,33 @@ module ActiveRecord
# Returns the list of all column definitions for a table.
def columns(table_name) # :nodoc:
table_name = table_name.to_s
- column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation|
+ column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation, comment|
oid = oid.to_i
fmod = fmod.to_i
type_metadata = fetch_type_metadata(column_name, type, oid, fmod)
default_value = extract_value_from_default(default)
default_function = extract_default_function(default_value, default)
- new_column(column_name, default_value, type_metadata, !notnull, table_name, default_function, collation)
+ new_column(column_name, default_value, type_metadata, !notnull, table_name, default_function, collation, comment)
end
end
- def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil) # :nodoc:
- PostgreSQLColumn.new(name, default, sql_type_metadata, null, table_name, default_function, collation)
+ def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil, comment = nil) # :nodoc:
+ PostgreSQLColumn.new(name, default, sql_type_metadata, null, table_name, default_function, collation, comment)
+ end
+
+ # Returns a comment stored in database for given table
+ def table_comment(table_name) # :nodoc:
+ name = Utils.extract_schema_qualified_name(table_name.to_s)
+ return nil unless name.identifier
+
+ select_value(<<-SQL.strip_heredoc, 'SCHEMA')
+ SELECT pg_catalog.obj_description(c.oid, 'pg_class')
+ FROM pg_catalog.pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relname = '#{name.identifier}'
+ AND c.relkind IN ('r') -- (r)elation/table
+ AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
+ SQL
end
# Returns the current database name.
@@ -445,6 +464,7 @@ module ActiveRecord
def add_column(table_name, column_name, type, options = {}) #:nodoc:
clear_cache!
super
+ change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
@@ -466,6 +486,7 @@ module ActiveRecord
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
end
# Changes the default value of a table column.
@@ -494,6 +515,18 @@ module ActiveRecord
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
end
+ # Adds comment for given table column or drops it if +comment+ is a +nil+
+ def change_column_comment(table_name, column_name, comment) # :nodoc:
+ clear_cache!
+ execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS #{quote(comment)}"
+ end
+
+ # Adds comment for given table or drops it if +comment+ is a +nil+
+ def change_table_comment(table_name, comment) # :nodoc:
+ clear_cache!
+ execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS #{quote(comment)}"
+ end
+
# Renames a column in a table.
def rename_column(table_name, column_name, new_column_name) #:nodoc:
clear_cache!
@@ -502,8 +535,10 @@ module ActiveRecord
end
def add_index(table_name, column_name, options = {}) #:nodoc:
- index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
- execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}"
+ index_name, index_type, index_columns, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options)
+ result = execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}"
+ execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment
+ result # Result of execute is used in tests in activerecord/test/cases/adapters/postgresql/active_schema_test.rb
end
def remove_index(table_name, options = {}) #:nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 6497b1cc31..cf04f32206 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -159,6 +159,10 @@ module ActiveRecord
postgresql_version >= 90200
end
+ def supports_comments?
+ true
+ end
+
def index_algorithms
{ concurrently: 'CONCURRENTLY' }
end
@@ -712,7 +716,7 @@ module ActiveRecord
# Returns the list of a table's column names, data types, and default values.
#
# The underlying query is roughly:
- # SELECT column.name, column.type, default.value
+ # SELECT column.name, column.type, default.value, column.comment
# FROM column LEFT JOIN default
# ON column.table_id = default.table_id
# AND column.num = default.column_num
@@ -732,7 +736,8 @@ module ActiveRecord
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
(SELECT c.collname FROM pg_collation c, pg_type t
- WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation)
+ WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation),
+ col_description(a.attrelid, a.attnum) AS comment
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
@@ -746,8 +751,8 @@ module ActiveRecord
$1.strip if $1
end
- def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
- PostgreSQL::TableDefinition.new(name, temporary, options, as)
+ def create_table_definition(name, temporary = false, options = nil, as = nil, comment = nil) # :nodoc:
+ PostgreSQL::TableDefinition.new(name, temporary, options, as, comment)
end
def can_perform_case_insensitive_comparison_for?(column)
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index affcd9aed1..bc4adb4ed7 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -138,6 +138,9 @@ HEADER
table_options = @connection.table_options(table)
tbl.print ", options: #{table_options.inspect}" unless table_options.blank?
+ comment = @connection.table_comment(table)
+ tbl.print ", comment: #{comment.inspect}" if comment
+
tbl.puts " do |t|"
# then dump all non-primary key columns
@@ -209,6 +212,7 @@ HEADER
statement_parts << "where: #{index.where.inspect}" if index.where
statement_parts << "using: #{index.using.inspect}" if index.using
statement_parts << "type: #{index.type.inspect}" if index.type
+ statement_parts << "comment: #{index.comment.inspect}" if index.comment
" #{statement_parts.join(', ')}"
end
diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb
new file mode 100644
index 0000000000..53a490cf21
--- /dev/null
+++ b/activerecord/test/cases/comment_test.rb
@@ -0,0 +1,91 @@
+require 'cases/helper'
+require 'support/schema_dumping_helper'
+
+class CommentTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false if current_adapter?(:Mysql2Adapter)
+
+ class Commented < ActiveRecord::Base
+ self.table_name = 'commenteds'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+
+ @connection.transaction do
+ @connection.create_table('commenteds', comment: 'A table with comment', force: true) do |t|
+ t.string 'name', comment: 'Comment should help clarify the column purpose'
+ t.boolean 'obvious', comment: 'Question is: should you comment obviously named objects?'
+ t.string 'content'
+ t.index 'name', comment: %Q["Very important" index that powers all the performance.\nAnd it's fun!]
+ end
+ end
+ end
+
+ teardown do
+ @connection.drop_table 'commenteds', if_exists: true
+ end
+
+ if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
+
+ def test_column_created_in_block
+ Commented.reset_column_information
+ column = Commented.columns_hash['name']
+ assert_equal :string, column.type
+ assert_equal 'Comment should help clarify the column purpose', column.comment
+ end
+
+ def test_add_column_with_comment_later
+ @connection.add_column :commenteds, :rating, :integer, comment: 'I am running out of imagination'
+ Commented.reset_column_information
+ column = Commented.columns_hash['rating']
+
+ assert_equal :integer, column.type
+ assert_equal 'I am running out of imagination', column.comment
+ end
+
+ def test_add_index_with_comment_later
+ @connection.add_index :commenteds, :obvious, name: 'idx_obvious', comment: 'We need to see obvious comments'
+ index = @connection.indexes('commenteds').find { |idef| idef.name == 'idx_obvious' }
+ assert_equal 'We need to see obvious comments', index.comment
+ end
+
+ def test_add_comment_to_column
+ @connection.change_column :commenteds, :content, :string, comment: 'Whoa, content describes itself!'
+
+ Commented.reset_column_information
+ column = Commented.columns_hash['content']
+
+ assert_equal :string, column.type
+ assert_equal 'Whoa, content describes itself!', column.comment
+ end
+
+ def test_remove_comment_from_column
+ @connection.change_column :commenteds, :obvious, :string, comment: nil
+
+ Commented.reset_column_information
+ column = Commented.columns_hash['obvious']
+
+ assert_equal :string, column.type
+ assert_nil column.comment
+ end
+
+ def test_schema_dump_with_comments
+ # Do all the stuff from other tests
+ @connection.add_column :commenteds, :rating, :integer, comment: 'I am running out of imagination'
+ @connection.change_column :commenteds, :content, :string, comment: 'Whoa, content describes itself!'
+ @connection.change_column :commenteds, :obvious, :string, comment: nil
+ @connection.add_index :commenteds, :obvious, name: 'idx_obvious', comment: 'We need to see obvious comments'
+ # And check that these changes are reflected in dump
+ output = dump_table_schema 'commenteds'
+ assert_match %r[create_table "commenteds",.+\s+comment: "A table with comment"], output
+ assert_match %r[t\.string\s+"name",\s+comment: "Comment should help clarify the column purpose"], output
+ assert_match %r[t\.string\s+"obvious"\n], output
+ assert_match %r[t\.string\s+"content",\s+comment: "Whoa, content describes itself!"], output
+ assert_match %r[t\.integer\s+"rating",\s+comment: "I am running out of imagination"], output
+ assert_match %r[add_index\s+.+\s+comment: "\\\"Very important\\\" index that powers all the performance.\\nAnd it's fun!"], output
+ assert_match %r[add_index\s+.+\s+name: "idx_obvious",.+\s+comment: "We need to see obvious comments"], output
+ end
+
+ end
+end
diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md
index b89485b945..c111996b58 100644
--- a/guides/source/active_record_migrations.md
+++ b/guides/source/active_record_migrations.md
@@ -355,6 +355,13 @@ end
will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table
(when using MySQL or MariaDB, the default is `ENGINE=InnoDB`).
+Also you can pass the `:comment` option with any description for the table
+that will be stored in database itself and can be viewed with database administration
+tools, such as MySQL Workbench or PgAdmin III. It's highly recommended to specify
+comments in migrations for applications with large databases as it helps people
+to understand data model and generate documentation.
+Currently only MySQL and PostgreSQL supports comments.
+
### Creating a Join Table
The migration method `create_join_table` creates an HABTM (has and belongs to
@@ -454,6 +461,7 @@ number of digits after the decimal point.
are using a dynamic value (such as a date), the default will only be calculated
the first time (i.e. on the date the migration is applied).
* `index` Adds an index for the column.
+* `comment` Adds a comment for the column.
Some adapters may support additional options; see the adapter specific API docs
for further information.