Repository navigation
Fix upgrades from 0.x and make the v1 conversion atomic #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d2fecae
a4c8142
5d49f3c
421fa49
3a29a96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,3 +8,4 @@ | |
| /tmp/ | ||
| /.history/ | ||
| .idea | ||
| /gemfiles/*.lock | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| # Runs the suite against one Active Record version and a MySQL database: | ||
| # BUNDLE_GEMFILE=gemfiles/activerecord.gemfile ACTIVE_RECORD_VERSION='~> 8.1.0' bundle exec rake | ||
| eval_gemfile File.expand_path('../Gemfile', __dir__) | ||
|
|
||
| gem 'activerecord', ENV.fetch('ACTIVE_RECORD_VERSION') | ||
| gem 'mysql2', '~> 0.5' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,24 +17,48 @@ def find(*args) | |
| end | ||
| end | ||
|
|
||
| # ClassMethods | ||
| module ClassMethods | ||
| include Finder | ||
| # Reads 16-byte UUID columns as UUID strings. The types are installed when the | ||
| # model loads its schema, so defining a model needs no database connection. | ||
| module SchemaTypes | ||
| # Rails 7.2 started asking the model for the type of each column. | ||
| def self.type_for_column_hook? | ||
| ::ActiveRecord::ModelSchema::ClassMethods.private_method_defined?(:type_for_column) | ||
| end | ||
|
|
||
| def uuidable(as_param: true) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity | ||
| # Configure all uuid columns for MySQL. Database may not be connected (i.e. on assets precompile), so we must supress errors. | ||
| conn_config = respond_to?(:connection_db_config) ? connection_db_config.configuration_hash : connection_config | ||
| private | ||
|
|
||
| if conn_config[:adapter].include?('mysql') && connection.data_source_exists?(table_name) | ||
| begin | ||
| columns.select { |c| c.type == :binary && c.limit == 16 && c.name.include?('uuid') }.each do |column| | ||
| attribute column.name.to_sym, MySQLBinUUID::Type.new | ||
| end | ||
| def load_schema! | ||
| super | ||
| install_uuid_types unless SchemaTypes.type_for_column_hook? | ||
| include V1ModelMigration if columns_hash.each_key.any? { |name| name.include?(V1MigrationHelpers::OLD_POSTFIX) } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should fix: |
||
| end | ||
|
|
||
| include V1ModelMigration if columns.any? { |c| c.name.include?(V1MigrationHelpers::OLD_POSTFIX) } | ||
| rescue ::ActiveRecord::ConnectionNotEstablished, Mysql2::Error::ConnectionError, ::ActiveRecord::NoDatabaseError # rubocop:disable Lint/SuppressedException | ||
| end | ||
| def type_for_column(connection, column) | ||
| uuid_column?(connection, column) ? MySQLBinUUID::Type.new : super | ||
| end | ||
|
|
||
| # Before Rails 7.2: replace the schema types, keeping types declared with `attribute`. | ||
| def install_uuid_types | ||
| columns_hash.each_value do |column| | ||
| next if !uuid_column?(connection, column) || attributes_to_define_after_schema_loads.key?(column.name) | ||
|
|
||
| define_attribute(column.name, MySQLBinUUID::Type.new, default: column.default, user_provided_default: false) | ||
| end | ||
| end | ||
|
|
||
| def uuid_column?(connection, column) | ||
| column.type == :binary && column.limit == 16 && column.name.include?('uuid') && | ||
| connection.adapter_name.downcase.include?('mysql') | ||
| end | ||
| end | ||
|
|
||
| # ClassMethods | ||
| module ClassMethods | ||
| include Finder | ||
|
|
||
| def uuidable(as_param: true) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity | ||
| singleton_class.prepend(SchemaTypes) unless singleton_class.include?(SchemaTypes) | ||
| reload_schema_from_cache if schema_loaded? | ||
|
|
||
| after_initialize do | ||
| self.uuid = Uuidable.generate_uuid if attributes.keys.include?('uuid') && uuid.blank? | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,29 +5,72 @@ module Uuidable | |
| COLUMN_TYPE = :binary | ||
| COLUMN_OPTIONS = { limit: 16, null: false }.freeze | ||
| INDEX_OPTIONS = { unique: true }.freeze | ||
| STORAGE_LIMITS = { binary: 16, text: 36 }.freeze | ||
|
|
||
| class << self | ||
| # Name of a new UUID column, given positionally or with the pre-1.0 `column_name:` option. | ||
| def column_name(positional, keyword) | ||
| if positional && keyword && positional.to_s != keyword.to_s | ||
| raise ArgumentError, "UUID column named twice: #{positional.inspect} and column_name: #{keyword.inspect}" | ||
| end | ||
|
|
||
| positional || keyword || COLUMN_NAME | ||
| end | ||
|
|
||
| # Storage for new UUID columns that set neither `storage:` nor `limit:`. | ||
| # Apps upgrading from 0.x can set :text so new columns match their 36-byte ones. | ||
| def default_storage | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: |
||
| @default_storage || :binary | ||
| end | ||
|
|
||
| def default_storage=(storage) | ||
| storage_limit(storage) | ||
| @default_storage = storage | ||
| end | ||
|
|
||
| # Column options for a new UUID column, resolving `storage:` into a limit. | ||
| def column_options(options) | ||
| options = options.dup | ||
| storage = options.delete(:storage) | ||
| limit = storage_limit(storage || default_storage) | ||
| if storage && options.key?(:limit) && options[:limit] != limit | ||
| raise ArgumentError, "storage: #{storage.inspect} conflicts with limit: #{options[:limit].inspect}" | ||
| end | ||
|
|
||
| COLUMN_OPTIONS.merge(limit: limit).merge(options) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def storage_limit(storage) | ||
| STORAGE_LIMITS.fetch(storage) do | ||
| raise ArgumentError, "UUID storage must be one of #{STORAGE_LIMITS.keys.inspect}, got #{storage.inspect}" | ||
| end | ||
| end | ||
| end | ||
|
|
||
| # Module adds method to table definition | ||
| module TableDefinition | ||
| def uuid(column_name = COLUMN_NAME, **opts) | ||
| def uuid(column_name = nil, **opts) | ||
| index_opts = opts.delete(:index) | ||
| index_opts = {} if index_opts.nil? | ||
|
|
||
| column_name ||= opts.delete(:column_name) | ||
| column_name = Uuidable.column_name(column_name, opts.delete(:column_name)) | ||
|
|
||
| column column_name, COLUMN_TYPE, **COLUMN_OPTIONS.merge(opts) | ||
| column column_name, COLUMN_TYPE, **Uuidable.column_options(opts) | ||
| index column_name, **INDEX_OPTIONS.merge(index_opts) if index_opts | ||
| end | ||
| end | ||
|
|
||
| # Module adds method to alter table migration | ||
| module Migration | ||
| def add_uuid_column(table_name, column_name = COLUMN_NAME, **opts) | ||
| def add_uuid_column(table_name, column_name = nil, **opts) | ||
| index_opts = opts.delete(:index) | ||
| index_opts = {} if index_opts == true | ||
|
|
||
| column_name ||= opts.delete(:column_name) | ||
| column_name = Uuidable.column_name(column_name, opts.delete(:column_name)) | ||
|
|
||
| add_column table_name, column_name, COLUMN_TYPE, **COLUMN_OPTIONS.merge(opts) | ||
| add_column table_name, column_name, COLUMN_TYPE, **Uuidable.column_options(opts) | ||
|
|
||
| add_uuid_index(table_name, index_opts.merge(column_name: column_name)) if index_opts | ||
| end | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should fix: on Ruby 3.4 with Active Record 6.1 or 7.0
require 'active_record'fails withcannot load such file -- mutex_m, while the README says the suite runs against any supported version. Conditionalmutex_m,bigdecimal,base64anddrbfor AR < 7.1 make the gemfile work on current Ruby.