diff options
Diffstat (limited to 'activerecord')
21 files changed, 250 insertions, 289 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 96094a285f..6a095603fa 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,17 @@ +* Add `:default` option to `belongs_to`. + + Use it to specify that an association should be initialized with a particular + record before validation. For example: + + # Before + belongs_to :account + before_validation -> { self.account ||= Current.account } + + # After + belongs_to :account, default: -> { Current.account } + + *George Claghorn* + * Deprecate `Migrator.schema_migrations_table_name`. *Ryuta Kamizono* @@ -495,11 +509,6 @@ *Prathamesh Sonpatki* -* Optimistic locking: Added ability to update `locking_column` value. - Ignore optimistic locking if trying to update with new `locking_column` value. - - *bogdanvlviv* - * Fixed: Optimistic locking does not work well with `null` in the database. Fixes #26024. diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 4606c91ffd..6efa448d49 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1647,6 +1647,9 @@ module ActiveRecord # +:inverse_of+ to avoid an extra query during validation. # NOTE: <tt>required</tt> is set to <tt>true</tt> by default and is deprecated. If # you don't want to have association presence validated, use <tt>optional: true</tt>. + # [:default] + # Provide a callable (i.e. proc or lambda) to specify that the association should + # be initialized with a particular record before validation. # # Option examples: # belongs_to :firm, foreign_key: "client_of" @@ -1660,6 +1663,7 @@ module ActiveRecord # belongs_to :comment, touch: true # belongs_to :company, touch: :employees_last_updated_at # belongs_to :user, optional: true + # belongs_to :account, default: -> { company.account } def belongs_to(name, scope = nil, options = {}) reflection = Builder::BelongsTo.build(self, name, scope, options) Reflection.add_reflection self, name, reflection diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 64b2311911..4a4f88bb94 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -21,6 +21,10 @@ module ActiveRecord self.target = record end + def default(record) + writer(record) if reader.nil? + end + def reset super @updated = false diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index a1609ab0fb..50a1c39ccf 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder # :nodoc: end def self.valid_options(options) - super + [:polymorphic, :touch, :counter_cache, :optional] + super + [:polymorphic, :touch, :counter_cache, :optional, :default] end def self.valid_dependent_options @@ -16,6 +16,7 @@ module ActiveRecord::Associations::Builder # :nodoc: super add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache] add_touch_callbacks(model, reflection) if reflection.options[:touch] + add_default_callbacks(model, reflection) if reflection.options[:default] end def self.define_accessors(mixin, reflection) @@ -118,6 +119,12 @@ module ActiveRecord::Associations::Builder # :nodoc: model.after_destroy callback.(:changes_to_save) end + def self.add_default_callbacks(model, reflection) + model.before_validation lambda { |o| + o.association(reflection.name).default o.instance_exec(&reflection.options[:default]) + } + end + def self.add_destroy_callbacks(model, reflection) model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency } end 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 a497a354f7..1e826ff5ad 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -31,6 +31,8 @@ module ActiveRecord # Returns the relation names useable to back Active Record models. # For most adapters this means all #tables and #views. def data_sources + select_values(data_source_sql, "SCHEMA") + rescue NotImplementedError tables | views end @@ -39,12 +41,14 @@ module ActiveRecord # data_source_exists?(:ebooks) # def data_source_exists?(name) + select_values(data_source_sql(name), "SCHEMA").any? if name.present? + rescue NotImplementedError data_sources.include?(name.to_s) end # Returns an array of table names defined in the database. def tables - raise NotImplementedError, "#tables is not implemented" + select_values(data_source_sql(type: "BASE TABLE"), "SCHEMA") end # Checks to see if the table +table_name+ exists on the database. @@ -52,12 +56,14 @@ module ActiveRecord # table_exists?(:developers) # def table_exists?(table_name) + select_values(data_source_sql(table_name, type: "BASE TABLE"), "SCHEMA").any? if table_name.present? + rescue NotImplementedError tables.include?(table_name.to_s) end # Returns an array of view names defined in the database. def views - raise NotImplementedError, "#views is not implemented" + select_values(data_source_sql(type: "VIEW"), "SCHEMA") end # Checks to see if the view +view_name+ exists on the database. @@ -65,6 +71,8 @@ module ActiveRecord # view_exists?(:ebooks) # def view_exists?(view_name) + select_values(data_source_sql(view_name, type: "VIEW"), "SCHEMA").any? if view_name.present? + rescue NotImplementedError views.include?(view_name.to_s) end @@ -1304,6 +1312,14 @@ module ActiveRecord def can_remove_index_by_name?(options) options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty? end + + def data_source_sql(name = nil, type: nil) + raise NotImplementedError + end + + def quoted_scope(name = nil, type: nil) + raise NotImplementedError + end end end 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 e3b6327dd8..55ec112c17 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -6,6 +6,7 @@ require "active_record/connection_adapters/mysql/quoting" require "active_record/connection_adapters/mysql/schema_creation" require "active_record/connection_adapters/mysql/schema_definitions" require "active_record/connection_adapters/mysql/schema_dumper" +require "active_record/connection_adapters/mysql/schema_statements" require "active_record/connection_adapters/mysql/type_metadata" require "active_support/core_ext/string/strip" @@ -15,6 +16,7 @@ module ActiveRecord class AbstractMysqlAdapter < AbstractAdapter include MySQL::Quoting include MySQL::ColumnDumper + include MySQL::SchemaStatements def update_table_definition(table_name, base) # :nodoc: MySQL::Table.new(table_name, base) @@ -310,57 +312,6 @@ module ActiveRecord show_variable "collation_database" end - def tables # :nodoc: - sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE'" - sql << " AND table_schema = #{quote(@config[:database])}" - - select_values(sql, "SCHEMA") - end - - def views # :nodoc: - select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", "SCHEMA") - end - - def data_sources # :nodoc: - sql = "SELECT table_name FROM information_schema.tables " - sql << "WHERE table_schema = #{quote(@config[:database])}" - - select_values(sql, "SCHEMA") - end - - def table_exists?(table_name) # :nodoc: - return false unless table_name.present? - - schema, name = extract_schema_qualified_name(table_name) - - sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE'" - sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" - - select_values(sql, "SCHEMA").any? - end - - def data_source_exists?(table_name) # :nodoc: - return false unless table_name.present? - - schema, name = extract_schema_qualified_name(table_name) - - sql = "SELECT table_name FROM information_schema.tables " - sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}" - - select_values(sql, "SCHEMA").any? - end - - def view_exists?(view_name) # :nodoc: - return false unless view_name.present? - - schema, name = extract_schema_qualified_name(view_name) - - sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'" - sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" - - select_values(sql, "SCHEMA").any? - end - def truncate(table_name, name = nil) execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name end @@ -406,13 +357,13 @@ module ActiveRecord end def table_comment(table_name) # :nodoc: - schema, name = extract_schema_qualified_name(table_name) + scope = quoted_scope(table_name) select_value(<<-SQL.strip_heredoc, "SCHEMA") SELECT table_comment FROM information_schema.tables - WHERE table_schema = #{quote(schema)} - AND table_name = #{quote(name)} + WHERE table_schema = #{scope[:schema]} + AND table_name = #{scope[:name]} SQL end @@ -512,7 +463,7 @@ module ActiveRecord def foreign_keys(table_name) raise ArgumentError unless table_name.present? - schema, name = extract_schema_qualified_name(table_name) + scope = quoted_scope(table_name) fk_info = select_all(<<-SQL.strip_heredoc, "SCHEMA") SELECT fk.referenced_table_name AS 'to_table', @@ -525,9 +476,9 @@ module ActiveRecord JOIN information_schema.referential_constraints rc USING (constraint_schema, constraint_name) WHERE fk.referenced_column_name IS NOT NULL - AND fk.table_schema = #{quote(schema)} - AND fk.table_name = #{quote(name)} - AND rc.table_name = #{quote(name)} + AND fk.table_schema = #{scope[:schema]} + AND fk.table_name = #{scope[:name]} + AND rc.table_name = #{scope[:name]} SQL fk_info.map do |row| @@ -599,14 +550,14 @@ module ActiveRecord def primary_keys(table_name) # :nodoc: raise ArgumentError unless table_name.present? - schema, name = extract_schema_qualified_name(table_name) + scope = quoted_scope(table_name) select_values(<<-SQL.strip_heredoc, "SCHEMA") SELECT column_name FROM information_schema.key_column_usage WHERE constraint_name = 'PRIMARY' - AND table_schema = #{quote(schema)} - AND table_name = #{quote(name)} + AND table_schema = #{scope[:schema]} + AND table_name = #{scope[:name]} ORDER BY ordinal_position SQL end @@ -940,12 +891,6 @@ module ActiveRecord ) end - def extract_schema_qualified_name(string) # :nodoc: - schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) - schema, name = @config[:database], schema unless name - [schema, name] - end - def integer_to_sql(limit) # :nodoc: case limit when 1; "tinyint" diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb new file mode 100644 index 0000000000..10c8bd179a --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -0,0 +1,33 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module SchemaStatements # :nodoc: + private + def data_source_sql(name = nil, type: nil) + scope = quoted_scope(name, type: type) + + sql = "SELECT table_name FROM information_schema.tables" + sql << " WHERE table_schema = #{scope[:schema]}" + sql << " AND table_name = #{scope[:name]}" if scope[:name] + sql << " AND table_type = #{scope[:type]}" if scope[:type] + sql + end + + def quoted_scope(name = nil, type: nil) + schema, name = extract_schema_qualified_name(name) + scope = {} + scope[:schema] = schema ? quote(schema) : "database()" + scope[:name] = quote(name) if name + scope[:type] = quote(type) if type + scope + end + + def extract_schema_qualified_name(string) + schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) + schema, name = nil, schema unless name + [schema, name] + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb index 24dcf852e1..9ad6a6c0d0 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb @@ -2,6 +2,8 @@ module ActiveRecord module ConnectionAdapters module MySQL class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: + undef to_yaml if method_defined?(:to_yaml) + attr_reader :extra def initialize(type_metadata, extra: "") 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 afef0da5c7..a332375b78 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -54,81 +54,13 @@ module ActiveRecord execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" end - # Returns the list of all tables in the schema search path. - def tables - 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 - - def views # :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 ('v','m') -- (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) - def table_exists?(name) - name = Utils.extract_schema_qualified_name(name.to_s) - return false unless name.identifier - - select_values(<<-SQL, "SCHEMA").any? - SELECT tablename - FROM pg_tables - WHERE tablename = #{quote(name.identifier)} - AND schemaname = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} - SQL - end - - def data_source_exists?(name) # :nodoc: - name = Utils.extract_schema_qualified_name(name.to_s) - return false unless name.identifier - - select_values(<<-SQL, "SCHEMA").any? - 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 c.relname = #{quote(name.identifier)} - AND n.nspname = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} - SQL - end - - def view_exists?(view_name) # :nodoc: - name = Utils.extract_schema_qualified_name(view_name.to_s) - return false unless name.identifier - - select_values(<<-SQL, "SCHEMA").any? - SELECT c.relname - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view - AND c.relname = #{quote(name.identifier)} - AND n.nspname = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} - SQL - end - def drop_table(table_name, options = {}) # :nodoc: execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end # Returns true if schema exists. def schema_exists?(name) - select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = '#{name}'", "SCHEMA").to_i > 0 + select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = #{quote(name)}", "SCHEMA").to_i > 0 end # Verifies existence of an index with a given name. @@ -138,8 +70,8 @@ module ActiveRecord Passing default to #index_name_exists? is deprecated without replacement. MSG end - table = Utils.extract_schema_qualified_name(table_name.to_s) - index = Utils.extract_schema_qualified_name(index_name.to_s) + table = quoted_scope(table_name) + index = quoted_scope(index_name) select_value(<<-SQL, "SCHEMA").to_i > 0 SELECT COUNT(*) @@ -148,9 +80,9 @@ module ActiveRecord INNER JOIN pg_class i ON d.indexrelid = i.oid LEFT JOIN pg_namespace n ON n.oid = i.relnamespace WHERE i.relkind = 'i' - AND i.relname = '#{index.identifier}' - AND t.relname = '#{table.identifier}' - AND n.nspname = #{index.schema ? "'#{index.schema}'" : 'ANY (current_schemas(false))'} + AND i.relname = #{index[:name]} + AND t.relname = #{table[:name]} + AND n.nspname = #{index[:schema]} SQL end @@ -162,7 +94,7 @@ module ActiveRecord MSG end - table = Utils.extract_schema_qualified_name(table_name.to_s) + scope = quoted_scope(table_name) result = query(<<-SQL, "SCHEMA") SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid, @@ -176,8 +108,8 @@ module ActiveRecord LEFT JOIN pg_namespace n ON n.oid = i.relnamespace WHERE i.relkind = 'i' AND d.indisprimary = 'f' - AND t.relname = '#{table.identifier}' - AND n.nspname = #{table.schema ? "'#{table.schema}'" : 'ANY (current_schemas(false))'} + AND t.relname = #{scope[:name]} + AND n.nspname = #{scope[:schema]} ORDER BY i.relname SQL @@ -239,22 +171,22 @@ module ActiveRecord # 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) - if name.identifier + scope = quoted_scope(table_name, type: "BASE TABLE") + if scope[:name] 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 = #{quote(name.identifier)} - AND c.relkind IN ('r') -- (r)elation/table - AND n.nspname = #{name.schema ? quote(name.schema) : 'ANY (current_schemas(false))'} + WHERE c.relname = #{scope[:name]} + AND c.relkind IN (#{scope[:type]}) + AND n.nspname = #{scope[:schema]} SQL end end # Returns the current database name. def current_database - select_value("select current_database()", "SCHEMA") + select_value("SELECT current_database()", "SCHEMA") end # Returns the current schema name. @@ -430,17 +362,15 @@ module ActiveRecord end def primary_keys(table_name) # :nodoc: - name = Utils.extract_schema_qualified_name(table_name.to_s) + scope = quoted_scope(table_name) select_values(<<-SQL.strip_heredoc, "SCHEMA") SELECT column_name FROM information_schema.key_column_usage kcu JOIN information_schema.table_constraints tc - ON kcu.table_name = tc.table_name - AND kcu.table_schema = tc.table_schema - AND kcu.constraint_name = tc.constraint_name + USING (table_schema, table_name, constraint_name) WHERE constraint_type = 'PRIMARY KEY' - AND kcu.table_name = #{quote(name.identifier)} - AND kcu.table_schema = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} + AND kcu.table_name = #{scope[:name]} + AND kcu.table_schema = #{scope[:schema]} ORDER BY kcu.ordinal_position SQL end @@ -579,6 +509,7 @@ module ActiveRecord end def foreign_keys(table_name) + scope = quoted_scope(table_name) fk_info = select_all(<<-SQL.strip_heredoc, "SCHEMA") SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete FROM pg_constraint c @@ -588,8 +519,8 @@ module ActiveRecord JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid JOIN pg_namespace t3 ON c.connamespace = t3.oid WHERE c.contype = 'f' - AND t1.relname = #{quote(table_name)} - AND t3.nspname = ANY (current_schemas(false)) + AND t1.relname = #{scope[:name]} + AND t3.nspname = #{scope[:schema]} ORDER BY c.conname SQL @@ -673,6 +604,39 @@ module ActiveRecord ) PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod) end + + private + def data_source_sql(name = nil, type: nil) + scope = quoted_scope(name, type: type) + scope[:type] ||= "'r','v','m'" # (r)elation/table, (v)iew, (m)aterialized view + + sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace" + sql << " WHERE n.nspname = #{scope[:schema]}" + sql << " AND c.relname = #{scope[:name]}" if scope[:name] + sql << " AND c.relkind IN (#{scope[:type]})" + sql + end + + def quoted_scope(name = nil, type: nil) + schema, name = extract_schema_qualified_name(name) + type = \ + case type + when "BASE TABLE" + "'r'" + when "VIEW" + "'v','m'" + end + scope = {} + scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))" + scope[:name] = quote(name) if name + scope[:type] = type if type + scope + end + + def extract_schema_qualified_name(string) + name = Utils.extract_schema_qualified_name(string.to_s) + [name.schema, name.identifier] + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb index 311988625f..f57179ae59 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -1,6 +1,8 @@ module ActiveRecord module ConnectionAdapters class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata) + undef to_yaml if method_defined?(:to_yaml) + attr_reader :oid, :fmod, :array def initialize(type_metadata, oid: nil, fmod: nil) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index bc04565434..22c37abb78 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -3,6 +3,7 @@ gem "pg", "~> 0.18" require "pg" require "active_record/connection_adapters/abstract_adapter" +require "active_record/connection_adapters/statement_pool" require "active_record/connection_adapters/postgresql/column" require "active_record/connection_adapters/postgresql/database_statements" require "active_record/connection_adapters/postgresql/explain_pretty_printer" @@ -15,7 +16,6 @@ require "active_record/connection_adapters/postgresql/schema_dumper" require "active_record/connection_adapters/postgresql/schema_statements" require "active_record/connection_adapters/postgresql/type_metadata" require "active_record/connection_adapters/postgresql/utils" -require "active_record/connection_adapters/statement_pool" module ActiveRecord module ConnectionHandling # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb new file mode 100644 index 0000000000..4ba245a0d8 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -0,0 +1,32 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + module SchemaStatements # :nodoc: + private + def data_source_sql(name = nil, type: nil) + scope = quoted_scope(name, type: type) + scope[:type] ||= "'table','view'" + + sql = "SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'" + sql << " AND name = #{scope[:name]}" if scope[:name] + sql << " AND type IN (#{scope[:type]})" + sql + end + + def quoted_scope(name = nil, type: nil) + type = \ + case type + when "BASE TABLE" + "'table'" + when "VIEW" + "'view'" + end + scope = {} + scope[:name] = quote(name) if name + scope[:type] = type if type + scope + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 8b627a6d4d..d24bfc0c93 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -5,6 +5,7 @@ require "active_record/connection_adapters/sqlite3/quoting" require "active_record/connection_adapters/sqlite3/schema_creation" require "active_record/connection_adapters/sqlite3/schema_definitions" require "active_record/connection_adapters/sqlite3/schema_dumper" +require "active_record/connection_adapters/sqlite3/schema_statements" gem "sqlite3", "~> 1.3.6" require "sqlite3" @@ -55,6 +56,7 @@ module ActiveRecord include SQLite3::Quoting include SQLite3::ColumnDumper + include SQLite3::SchemaStatements NATIVE_DATABASE_TYPES = { primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL", @@ -265,45 +267,6 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def tables # :nodoc: - select_values("SELECT name FROM sqlite_master WHERE type = 'table' AND name <> 'sqlite_sequence'", "SCHEMA") - end - - def data_sources # :nodoc: - select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", "SCHEMA") - end - - def views # :nodoc: - select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", "SCHEMA") - end - - def table_exists?(table_name) # :nodoc: - return false unless table_name.present? - - sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name <> 'sqlite_sequence'" - sql << " AND name = #{quote(table_name)}" - - select_values(sql, "SCHEMA").any? - end - - def data_source_exists?(table_name) # :nodoc: - return false unless table_name.present? - - sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'" - sql << " AND name = #{quote(table_name)}" - - select_values(sql, "SCHEMA").any? - end - - def view_exists?(view_name) # :nodoc: - return false unless view_name.present? - - sql = "SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'" - sql << " AND name = #{quote(view_name)}" - - select_values(sql, "SCHEMA").any? - end - def new_column_from_field(table_name, field) # :nondoc: case field["dflt_value"] when /^null$/i @@ -535,7 +498,7 @@ module ActiveRecord end def sqlite_version - @sqlite_version ||= SQLite3Adapter::Version.new(select_value("select sqlite_version(*)")) + @sqlite_version ||= SQLite3Adapter::Version.new(select_value("SELECT sqlite_version(*)")) end def translate_exception(exception, message) diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 0028dc0edb..8f78330d4a 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -559,7 +559,6 @@ module ActiveRecord @marked_for_destruction = false @destroyed_by_association = nil @new_record = true - @txn = nil @_start_transaction_state = {} @transaction_state = nil end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 2659c60f1f..78ce9f8291 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -47,8 +47,6 @@ module ActiveRecord # self.locking_column = :lock_person # end # - # Please note that the optimistic locking will be ignored if you update the - # locking column's value. module Optimistic extend ActiveSupport::Concern @@ -80,13 +78,11 @@ module ActiveRecord def _update_record(attribute_names = self.attribute_names) return super unless locking_enabled? - - lock_col = self.class.locking_column - - return super if attribute_names.include?(lock_col) return 0 if attribute_names.empty? begin + lock_col = self.class.locking_column + previous_lock_value = read_attribute_before_type_cast(lock_col) increment_lock diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 6af05c1860..edbd20a6c1 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,6 +1,8 @@ module ActiveRecord module Type class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc: + undef to_yaml if method_defined?(:to_yaml) + include ActiveModel::Type::Helpers::Mutable attr_reader :subtype, :coder diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index fdd7b0157a..2179d1294c 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -260,8 +260,7 @@ module ActiveRecord def test_tables_logs_name sql = <<-SQL - SELECT name FROM sqlite_master - WHERE type = 'table' AND name <> 'sqlite_sequence' + SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND type IN ('table') SQL assert_logged [[sql.squish, "SCHEMA", []]] do @conn.tables @@ -279,8 +278,7 @@ module ActiveRecord def test_table_exists_logs_name with_example_table do sql = <<-SQL - SELECT name FROM sqlite_master - WHERE type = 'table' AND name <> 'sqlite_sequence' AND name = 'ex' + SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND name = 'ex' AND type IN ('table') SQL assert_logged [[sql.squish, "SCHEMA", []]] do assert @conn.table_exists?("ex") diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 5875a1871f..5b08ba1358 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -116,6 +116,26 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase ActiveRecord::Base.belongs_to_required_by_default = original_value end + def test_default + david = developers(:david) + jamis = developers(:jamis) + + model = Class.new(ActiveRecord::Base) do + self.table_name = "ships" + def self.name; "Temp"; end + belongs_to :developer, default: -> { david } + end + + ship = model.create! + assert_equal david, ship.developer + + ship = model.create!(developer: jamis) + assert_equal jamis, ship.developer + + ship.update!(developer: nil) + assert_equal david, ship.developer + end + def test_default_scope_on_relations_is_not_cached counter = 0 diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index c13a962e3e..f9eccfbda1 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -349,13 +349,14 @@ class DirtyTest < ActiveRecord::TestCase def test_partial_update_with_optimistic_locking person = Person.new(first_name: "foo") - old_lock_version = person.lock_version with_partial_writes Person, false do assert_queries(2) { 2.times { person.save! } } Person.where(id: person.id).update_all(first_name: "baz") end + old_lock_version = person.lock_version + with_partial_writes Person, true do assert_queries(0) { 2.times { person.save! } } assert_equal old_lock_version, person.reload.lock_version diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 23095618a4..3a3b8e51f9 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -247,17 +247,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal "new title2", t2.title end - def test_lock_without_default_should_update_with_lock_col - t1 = LockWithoutDefault.create(title: "title1", lock_version: 6) - - assert_equal 6, t1.lock_version - - t1.update(lock_version: 0) - t1.reload - - assert_equal 0, t1.lock_version - end - def test_lock_without_default_queries_count t1 = LockWithoutDefault.create(title: "title1") @@ -270,12 +259,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal "title2", t1.title assert_equal 1, t1.lock_version - assert_queries(1) { t1.update(title: "title3", lock_version: 6) } - - t1.reload - assert_equal "title3", t1.title - assert_equal 6, t1.lock_version - t2 = LockWithoutDefault.new(title: "title1") assert_queries(1) { t2.save! } @@ -321,17 +304,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal "new title2", t2.title end - def test_lock_with_custom_column_without_default_should_update_with_lock_col - t1 = LockWithCustomColumnWithoutDefault.create(title: "title1", custom_lock_version: 6) - - assert_equal 6, t1.custom_lock_version - - t1.update(custom_lock_version: 0) - t1.reload - - assert_equal 0, t1.custom_lock_version - end - def test_lock_with_custom_column_without_default_queries_count t1 = LockWithCustomColumnWithoutDefault.create(title: "title1") @@ -344,12 +316,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal "title2", t1.title assert_equal 1, t1.custom_lock_version - assert_queries(1) { t1.update(title: "title3", custom_lock_version: 6) } - - t1.reload - assert_equal "title3", t1.title - assert_equal 6, t1.custom_lock_version - t2 = LockWithCustomColumnWithoutDefault.new(title: "title1") assert_queries(1) { t2.save! } diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 14fb2fbbfa..a6c22ac672 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -10,8 +10,6 @@ require "concurrent/atomic/cyclic_barrier" class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts, :comments - self.use_transactional_tests = false - def test_default_scope expected = Developer.all.merge!(order: "salary DESC").to_a.collect(&:salary) received = DeveloperOrderedBySalary.all.collect(&:salary) @@ -61,17 +59,6 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal "Jamis", DeveloperCalledJamis.create!.name end - unless in_memory_db? - def test_default_scoping_with_threads - 2.times do - Thread.new { - assert_includes DeveloperOrderedBySalary.all.to_sql, "salary DESC" - DeveloperOrderedBySalary.connection.close - }.join - end - end - end - def test_default_scope_with_inheritance wheres = InheritedPoorDeveloperCalledJamis.all.where_values_hash assert_equal "Jamis", wheres["name"] @@ -435,29 +422,6 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal comment, CommentWithDefaultScopeReferencesAssociation.find_by(id: comment.id) end - unless in_memory_db? - def test_default_scope_is_threadsafe - threads = [] - assert_not_equal 1, ThreadsafeDeveloper.unscoped.count - - barrier_1 = Concurrent::CyclicBarrier.new(2) - barrier_2 = Concurrent::CyclicBarrier.new(2) - - threads << Thread.new do - Thread.current[:default_scope_delay] = -> { barrier_1.wait; barrier_2.wait } - assert_equal 1, ThreadsafeDeveloper.all.to_a.count - ThreadsafeDeveloper.connection.close - end - threads << Thread.new do - Thread.current[:default_scope_delay] = -> { barrier_2.wait } - barrier_1.wait - assert_equal 1, ThreadsafeDeveloper.all.to_a.count - ThreadsafeDeveloper.connection.close - end - threads.each(&:join) - end - end - test "additional conditions are ANDed with the default scope" do scope = DeveloperCalledJamis.where(name: "David") assert_equal 2, scope.where_clause.ast.children.length @@ -506,3 +470,37 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_match gender_pattern, Lion.female.to_sql end end + +class DefaultScopingWithThreadTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + def test_default_scoping_with_threads + 2.times do + Thread.new { + assert_includes DeveloperOrderedBySalary.all.to_sql, "salary DESC" + DeveloperOrderedBySalary.connection.close + }.join + end + end + + def test_default_scope_is_threadsafe + threads = [] + assert_not_equal 1, ThreadsafeDeveloper.unscoped.count + + barrier_1 = Concurrent::CyclicBarrier.new(2) + barrier_2 = Concurrent::CyclicBarrier.new(2) + + threads << Thread.new do + Thread.current[:default_scope_delay] = -> { barrier_1.wait; barrier_2.wait } + assert_equal 1, ThreadsafeDeveloper.all.to_a.count + ThreadsafeDeveloper.connection.close + end + threads << Thread.new do + Thread.current[:default_scope_delay] = -> { barrier_2.wait } + barrier_1.wait + assert_equal 1, ThreadsafeDeveloper.all.to_a.count + ThreadsafeDeveloper.connection.close + end + threads.each(&:join) + end +end unless in_memory_db? |