diff --git a/db/sqlite3_schema.rb b/db/sqlite3_schema.rb index 42c78a9..fe10f75 100644 --- a/db/sqlite3_schema.rb +++ b/db/sqlite3_schema.rb @@ -21,7 +21,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_01_21_230456) do end create_table "notes", force: :cascade do |t| - t.text "text", null: false + t.text "text", limit: 65535, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end @@ -29,7 +29,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_01_21_230456) do create_table "quantities", force: :cascade do |t| t.integer "user_id" t.string "name", limit: 31, null: false - t.text "description" + t.text "description", limit: 65535 t.integer "parent_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -60,7 +60,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_01_21_230456) do create_table "units", force: :cascade do |t| t.integer "user_id" t.string "symbol", limit: 15, null: false - t.text "description" + t.text "description", limit: 65535 t.decimal "multiplier", precision: 30, scale: 15, default: "1.0", null: false t.integer "base_id" t.datetime "created_at", null: false diff --git a/lib/default_settings_strategy.rb b/lib/default_settings_strategy.rb index 26cc12b..35abbf1 100644 --- a/lib/default_settings_strategy.rb +++ b/lib/default_settings_strategy.rb @@ -1,18 +1,40 @@ class DefaultSettingsStrategy < ActiveRecord::Migration::DefaultStrategy - # Without `:if_not_exists`/`:if_exists` options it's impossible to change - # migration status once migration fails partially. If `up` migration creates - # some, but not all objects, its status is not updated from `down` to `up`. - # Then it's impossible to migrate: a) up - due to `already exists` errors and - # b) down - due to migration status. - # Using `force: :cascade` does nothing on MySQL, so `force:` is useless. - # Adding `null: false` here somehow does not show up in schema files, be careful! - # TODO: add_foreign_key {on_delete: :cascade} ? - DEFAULTS = { + COLUMN_DEFAULTS = { + # TODO: all types `null: false` ? + # TODO: references `foreign_key: {on_delete: :cascade}` ? + # `timestamps` - Rails defaults to `null: false, precision: true`. + # `limit:` for `text` - `text` can be theoretically up to 1GB or + # longer, so roughly unlimited for practical purposes. But: + # * the actual usable length depends on additional factors, like compile + # time limits (`SQLITE_MAX_LENGTH` in SQLite), runtime settings + # (`max_allowed_packet` in MySQL) and probably other, + # * Rails does not report limit for `text` column types, unless it is + # explicitly set. + # The decision is to always set safe limit and enforce it by validations, to + # avoid surprises (e.g. text truncation) when saving to dabatase. + text: {limit: 2**16 - 1} + } + COLUMN_DEFAULTS.default = {} + COLUMN_DEFAULTS.freeze + + module ColumnSettingsStrategy + def column(name, type, **options) + super(name, type, **COLUMN_DEFAULTS[type].merge(options)) + end + end + ActiveRecord::ConnectionAdapters::TableDefinition.prepend ColumnSettingsStrategy + + # `force: :cascade` - does nothing on MySQL, so `force:` is useless. + # `if_not_exists: true`/`if_exists: true` - without these options it's impossible + # to change migration status once migration fails partially. If `up` migration + # creates some, but not all objects, its status is not updated from `down` to + # `up`. Then it's impossible to migrate: a) up - due to `already exists` + # errors and b) down - due to migration status. + MIGRATION_DEFAULTS = { add_check_constraint: {if_not_exists: true}, add_column: {if_not_exists: true}, add_foreign_key: {if_not_exists: true}, add_index: {if_not_exists: true, unique: true}, - # Timestamps are by default `null: false, precision: true`. add_timestamps: {if_not_exists: true}, create_table: {if_not_exists: true}, drop_table: {if_exists: true}, @@ -22,11 +44,12 @@ class DefaultSettingsStrategy < ActiveRecord::Migration::DefaultStrategy remove_index: {if_exists: true}, remove_timestamps: {if_exists: true}, } - DEFAULTS.default = {} - DEFAULTS.freeze + MIGRATION_DEFAULTS.default = {} + MIGRATION_DEFAULTS.freeze def method_missing(method, *args, **kwargs, &) conflicts = kwargs.has_key?(:force) ? [:if_not_exists, :if_exists] : [] - super(method, *args, **DEFAULTS[method.to_sym].except(*conflicts).merge(kwargs), &) + defaults = MIGRATION_DEFAULTS[method.to_sym].except(*conflicts) + super(method, *args, **defaults.merge(kwargs), &) end end