Skip to content

Commit

Permalink
Drop support for PostgreSQL < 12
Browse files Browse the repository at this point in the history
  • Loading branch information
fatkodima committed Jan 11, 2025
1 parent 39e1d2a commit b3daaeb
Show file tree
Hide file tree
Showing 18 changed files with 177 additions and 603 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## master (unreleased)

- Drop support for PostgreSQL < 12
- Drop support for Ruby < 3.0 and Rails < 7.0

## 0.22.0 (2025-01-03)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ See [comparison to `strong_migrations`](#comparison-to-strong_migrations)

- Ruby 3.0+
- Rails 7.0+
- PostgreSQL 9.6+
- PostgreSQL 12+

For older Ruby and Rails versions you can use older versions of this gem.

Expand Down
1 change: 0 additions & 1 deletion lib/online_migrations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ class UnsafeMigration < Error; end
autoload :VerboseSqlLogs
autoload :ForeignKeysCollector
autoload :IndexDefinition
autoload :IndexesCollector
autoload :CommandChecker
autoload :BackgroundMigration

Expand Down
52 changes: 12 additions & 40 deletions lib/online_migrations/change_column_type_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,24 +118,19 @@ def initialize_columns_type_change(table_name, columns_and_types, **options)
type_cast_functions[column_name] = type_cast_function if type_cast_function
tmp_column_name = conversions[column_name]

if database_version >= 11_00_00
if primary_key(table_name) == column_name.to_s && old_col.type == :integer
# For PG < 11 and Primary Key conversions, setting a column as the PK
# converts even check constraints to NOT NULL column constraints
# and forces an inline re-verification of the whole table.
# To avoid this, we instead set it to `NOT NULL DEFAULT 0` and we'll
# copy the correct values when backfilling.
add_column(table_name, tmp_column_name, new_type,
**old_col_options, **column_options, default: old_col.default || 0, null: false)
else
if !old_col.default.nil?
old_col_options = old_col_options.merge(default: old_col.default, null: old_col.null)
end
add_column(table_name, tmp_column_name, new_type, **old_col_options, **column_options)
end
if primary_key(table_name) == column_name.to_s && old_col.type == :integer
# For PG < 11 and Primary Key conversions, setting a column as the PK
# converts even check constraints to NOT NULL column constraints
# and forces an inline re-verification of the whole table.
# To avoid this, we instead set it to `NOT NULL DEFAULT 0` and we'll
# copy the correct values when backfilling.
add_column(table_name, tmp_column_name, new_type,
**old_col_options, **column_options, default: old_col.default || 0, null: false)
else
if !old_col.default.nil?
old_col_options = old_col_options.merge(default: old_col.default, null: old_col.null)
end
add_column(table_name, tmp_column_name, new_type, **old_col_options, **column_options)
change_column_default(table_name, tmp_column_name, old_col.default) if !old_col.default.nil?
end
end

Expand Down Expand Up @@ -264,7 +259,7 @@ def finalize_columns_type_change(table_name, *column_names)

# At this point we are sure there are no NULLs in this column
transaction do
__set_not_null(table_name, tmp_column_name)
change_column_null(table_name, tmp_column_name, false)
remove_not_null_constraint(table_name, tmp_column_name)
end
end
Expand Down Expand Up @@ -494,29 +489,6 @@ def __copy_exclusion_constraints(table_name, from_column, to_column)
end
end

def __set_not_null(table_name, column_name)
# For PG >= 12 we can "promote" CHECK constraint to NOT NULL constraint:
# https://github.com/postgres/postgres/commit/bbb96c3704c041d139181c6601e5bc770e045d26
if database_version >= 12_00_00
execute(<<~SQL)
ALTER TABLE #{quote_table_name(table_name)}
ALTER #{quote_column_name(column_name)}
SET NOT NULL
SQL
else
# For older versions we can set attribute as NOT NULL directly
# through PG internal tables.
# In-depth analysis of implications of this was made, so this approach
# is considered safe - https://habr.com/ru/company/haulmont/blog/493954/ (in russian).
execute(<<~SQL)
UPDATE pg_catalog.pg_attribute
SET attnotnull = true
WHERE attrelid = #{quote(table_name)}::regclass
AND attname = #{quote(column_name)}
SQL
end
end

def __rename_constraint(table_name, old_name, new_name)
execute(<<~SQL)
ALTER TABLE #{quote_table_name(table_name)}
Expand Down
84 changes: 16 additions & 68 deletions lib/online_migrations/command_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ def version_safe?
short_primary_key_type: "using-primary-key-with-short-integer-type",
drop_table_multiple_foreign_keys: "removing-a-table-with-multiple-foreign-keys",
rename_table: "renaming-a-table",
add_column_with_default_null: "adding-a-column-with-a-default-value",
add_column_with_default: "adding-a-column-with-a-default-value",
add_column_generated_stored: "adding-a-stored-generated-column",
add_column_json: "adding-a-json-column",
Expand All @@ -69,7 +68,6 @@ def version_safe?
change_column_null: "setting-not-null-on-an-existing-column",
remove_column: "removing-a-column",
add_timestamps_with_default: "adding-a-column-with-a-default-value",
add_hash_index: "hash-indexes",
add_reference: "adding-a-reference",
add_index: "adding-an-index-non-concurrently",
replace_index: "replacing-an-index",
Expand All @@ -89,8 +87,8 @@ def check_database_version
adapter = connection.adapter_name
case adapter
when /postg/i
if postgresql_version < Gem::Version.new("9.6")
raise "#{adapter} < 9.6 is not supported"
if postgresql_version < Gem::Version.new("12")
raise "#{adapter} < 12 is not supported"
end
else
raise "#{adapter} is not supported"
Expand Down Expand Up @@ -181,11 +179,7 @@ def create_table(table_name, **options, &block)
# But I think this check is enough for now.
raise_error :short_primary_key_type if short_primary_key_type?(options)

if block
collect_foreign_keys(&block)
check_for_hash_indexes(&block) if postgresql_version < Gem::Version.new("10")
end

collect_foreign_keys(&block) if block
@new_tables << table_name.to_s
end

Expand Down Expand Up @@ -230,18 +224,11 @@ def add_column(table_name, column_name, type, **options)
@new_columns << [table_name.to_s, column_name.to_s]

if !new_or_small_table?(table_name)
if options.key?(:default) &&
(postgresql_version < Gem::Version.new("11") || (!default.nil? && (volatile_default = Utils.volatile_default?(connection, type, default))))

if default.nil?
raise_error :add_column_with_default_null,
code: command_str(:add_column, table_name, column_name, type, options.except(:default))
else
raise_error :add_column_with_default,
code: command_str(:add_column_with_default, table_name, column_name, type, options),
not_null: options[:null] == false,
volatile_default: volatile_default
end
if options.key?(:default) && !default.nil? && (volatile_default = Utils.volatile_default?(connection, type, default))
raise_error :add_column_with_default,
code: command_str(:add_column_with_default, table_name, column_name, type, options),
not_null: options[:null] == false,
volatile_default: volatile_default
end

if type == :virtual && options[:stored]
Expand Down Expand Up @@ -357,9 +344,7 @@ def change_column(table_name, column_name, type, **options)

[:datetime, :timestamp, :timestamptz].include?(existing_type) &&
precision >= existing_precision &&
(type == existing_type ||
(postgresql_version >= Gem::Version.new("12") &&
connection.select_value("SHOW timezone") == "UTC"))
(type == existing_type || connection.select_value("SHOW timezone") == "UTC")
when :interval
precision = options[:precision] || options[:limit] || 6
existing_precision = existing_column.precision || existing_column.limit || 6
Expand Down Expand Up @@ -398,32 +383,25 @@ def change_column_default(table_name, column_name, _default_or_changes)

def change_column_null(table_name, column_name, allow_null, default = nil, **)
if !allow_null && !new_or_small_table?(table_name)
safe = false
# In PostgreSQL 12+ you can add a check constraint to the table
# and then "promote" it to NOT NULL for the column.
if postgresql_version >= Gem::Version.new("12")
safe = check_constraints(table_name).any? do |c|
c["def"] == "CHECK ((#{column_name} IS NOT NULL))" ||
c["def"] == "CHECK ((#{connection.quote_column_name(column_name)} IS NOT NULL))"
end
safe = check_constraints(table_name).any? do |c|
c["def"] == "CHECK ((#{column_name} IS NOT NULL))" ||
c["def"] == "CHECK ((#{connection.quote_column_name(column_name)} IS NOT NULL))"
end

if !safe
constraint_name = "#{table_name}_#{column_name}_null"
vars = {
add_constraint_code: command_str(:add_not_null_constraint, table_name, column_name, name: constraint_name, validate: false),
validate_constraint_code: command_str(:validate_not_null_constraint, table_name, column_name, name: constraint_name),
remove_constraint_code: nil,
table_name: table_name,
column_name: column_name,
default: default,
remove_constraint_code: command_str(:remove_check_constraint, table_name, name: constraint_name),
change_column_null_code: command_str(:change_column_null, table_name, column_name, false),
}

if postgresql_version >= Gem::Version.new("12")
vars[:remove_constraint_code] = command_str(:remove_check_constraint, table_name, name: constraint_name)
vars[:change_column_null_code] = command_str(:change_column_null, table_name, column_name, false)
end

raise_error :change_column_null, **vars
end
end
Expand Down Expand Up @@ -466,26 +444,20 @@ def add_timestamps(table_name, **options)
@new_columns << [table_name.to_s, "created_at"]
@new_columns << [table_name.to_s, "updated_at"]

volatile_default = false
if !new_or_small_table?(table_name) && !options[:default].nil? &&
(postgresql_version < Gem::Version.new("11") || (volatile_default = Utils.volatile_default?(connection, :datetime, options[:default])))
Utils.volatile_default?(connection, :datetime, options[:default])

raise_error :add_timestamps_with_default,
code: [command_str(:add_column_with_default, table_name, :created_at, :datetime, options),
command_str(:add_column_with_default, table_name, :updated_at, :datetime, options)].join("\n "),
not_null: options[:null] == false,
volatile_default: volatile_default
not_null: options[:null] == false
end
end

def add_reference(table_name, ref_name, **options)
# Always added by default in 5.0+
index = options.fetch(:index, true)

if index.is_a?(Hash) && index[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
raise_error :add_hash_index
end

concurrently_set = index.is_a?(Hash) && index[:algorithm] == :concurrently
bad_index = index && !concurrently_set

Expand Down Expand Up @@ -518,13 +490,6 @@ def add_reference(table_name, ref_name, **options)
alias add_belongs_to add_reference

def add_reference_concurrently(table_name, ref_name, **options)
# Always added by default in 5.0+
index = options.fetch(:index, true)

if index.is_a?(Hash) && index[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
raise_error :add_hash_index
end

foreign_key = options.fetch(:foreign_key, false)

if foreign_key
Expand All @@ -542,10 +507,6 @@ def add_reference_concurrently(table_name, ref_name, **options)
end

def add_index(table_name, column_name, **options)
if options[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
raise_error :add_hash_index
end

if !new_or_small_table?(table_name)
if options[:algorithm] != :concurrently
raise_error :add_index,
Expand Down Expand Up @@ -698,19 +659,6 @@ def collect_foreign_keys(&block)
@foreign_key_tables |= collector.referenced_tables
end

def check_for_hash_indexes(&block)
indexes = collect_indexes(&block)
if indexes.any? { |index| index.using == "hash" }
raise_error :add_hash_index
end
end

def collect_indexes(&block)
collector = IndexesCollector.new
collector.collect(&block)
collector.indexes
end

def new_or_small_table?(table_name)
new_table?(table_name) || small_table?(table_name)
end
Expand Down
13 changes: 2 additions & 11 deletions lib/online_migrations/error_messages.rb
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ def change
<% end %>",

add_timestamps_with_default:
"Adding timestamps columns with non-null defaults blocks reads and writes while the entire table is rewritten.
"Adding timestamp columns with volatile defaults blocks reads and writes while the entire table is rewritten.
A safer approach is to, for both timestamps columns:
1. add the column without a default value
Expand All @@ -327,7 +327,6 @@ def change
4. add the NOT NULL constraint
<% end %>
<% unless volatile_default %>
add_column_with_default takes care of all this steps:
class <%= migration_name %> < <%= migration_parent %>
Expand All @@ -336,8 +335,7 @@ class <%= migration_name %> < <%= migration_parent %>
def change
<%= code %>
end
end
<% end %>",
end",

add_reference:
"<% if bad_foreign_key %>
Expand All @@ -356,13 +354,6 @@ def change
end
end",

add_hash_index:
"Hash index operations are not WAL-logged, so hash indexes might need to be rebuilt with REINDEX
after a database crash if there were unwritten changes. Also, changes to hash indexes are not replicated
over streaming or file-based replication after the initial base backup, so they give wrong answers
to queries that subsequently use them. For these reasons, hash index use is discouraged.
Use B-tree indexes instead.",

add_index:
"Adding an index non-concurrently blocks writes. Instead, use:
Expand Down
46 changes: 0 additions & 46 deletions lib/online_migrations/indexes_collector.rb

This file was deleted.

Loading

0 comments on commit b3daaeb

Please sign in to comment.