From 82bef134d9af8bf71f8d28e8ed144d5df9867961 Mon Sep 17 00:00:00 2001 From: Bram Jetten Date: Mon, 12 Jan 2026 11:37:39 +0100 Subject: [PATCH 1/2] Add MySQL and SQLite database support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spina now supports PostgreSQL, MySQL, and SQLite databases. Changes: - Remove hard pg gem dependency from gemspec - Convert JSONB columns to JSON for cross-database compatibility - Replace PostgreSQL-specific ILIKE with LOWER()/LIKE - Refactor ReplaceSignedIdJob to use Ruby instead of PostgreSQL SQL - Change Mobility backend from :jsonb to :json - Add CI workflow to test all three databases - Update documentation Existing PostgreSQL installations are unaffected - JSONB columns continue to work with the JSON-based code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/ruby.yml | 95 ++++++++++++++++--- Gemfile | 5 + Gemfile.lock | 16 +++- .../admin/page_select_options_controller.rb | 2 +- .../resource_select_options_controller.rb | 2 +- app/jobs/spina/replace_signed_id_job.rb | 21 ++-- app/models/concerns/spina/attachable.rb | 2 +- app/models/spina/resource.rb | 2 +- ...3_add_json_attributes_to_spina_accounts.rb | 2 +- .../14_add_json_attributes_to_spina_pages.rb | 2 +- db/migrate/15_add_slug_to_spina_resources.rb | 2 +- .../18_change_default_spina_resources_slug.rb | 4 +- db/migrate/7_create_spina_settings.rb | 2 +- docs/v2/advanced/1_create_custom_parts.md | 2 +- docs/v2/getting_started/2_installing_spina.md | 12 ++- docs/v2/getting_started/3_existing_project.md | 2 +- spina.gemspec | 1 - test/dummy/config/database.yml | 26 +---- test/dummy/config/database.yml.mysql | 9 ++ ...atabase.yml.ci => database.yml.postgresql} | 0 test/dummy/config/database.yml.sqlite | 5 + test/dummy/db/schema.rb | 54 +++++------ 22 files changed, 181 insertions(+), 87 deletions(-) create mode 100644 test/dummy/config/database.yml.mysql rename test/dummy/config/{database.yml.ci => database.yml.postgresql} (100%) create mode 100644 test/dummy/config/database.yml.sqlite diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index fdba8fd62..f904d617e 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -5,14 +5,10 @@ on: branches: [ main ] pull_request: branches: [ main ] - + jobs: - test: + test-postgresql: runs-on: ubuntu-latest - strategy: - matrix: - ruby-version: ['3.4'] - services: postgres: image: postgres:latest @@ -27,30 +23,103 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 - + steps: - uses: actions/checkout@v4 - + - name: Install libvips run: sudo apt-get install -y libvips - + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: ${{matrix.ruby-version}} + ruby-version: '3.4' bundler-cache: true - name: Create DB env: RAILS_ENV: test run: | - cp test/dummy/config/database.yml.ci test/dummy/config/database.yml + cp test/dummy/config/database.yml.postgresql test/dummy/config/database.yml bin/rails db:setup - + - name: Compile assets env: RAILS_ENV: test run: cd test/dummy && bin/rails assets:precompile - + + - name: Run tests + run: bin/rails test test/* + + test-mysql: + runs-on: ubuntu-latest + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: mysql + MYSQL_DATABASE: spina_test + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + + - name: Install libvips + run: sudo apt-get install -y libvips + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: true + + - name: Create DB + env: + RAILS_ENV: test + run: | + cp test/dummy/config/database.yml.mysql test/dummy/config/database.yml + bin/rails db:setup + + - name: Compile assets + env: + RAILS_ENV: test + run: cd test/dummy && bin/rails assets:precompile + + - name: Run tests + run: bin/rails test test/* + + test-sqlite: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install libvips + run: sudo apt-get install -y libvips + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: true + + - name: Create DB + env: + RAILS_ENV: test + run: | + cp test/dummy/config/database.yml.sqlite test/dummy/config/database.yml + bin/rails db:setup + + - name: Compile assets + env: + RAILS_ENV: test + run: cd test/dummy && bin/rails assets:precompile + - name: Run tests run: bin/rails test test/* diff --git a/Gemfile b/Gemfile index ff4e156a3..bb418bc1e 100644 --- a/Gemfile +++ b/Gemfile @@ -14,6 +14,11 @@ group :test do gem "factory_bot" gem "rails-controller-testing" + # Database adapters + gem "pg" + gem "mysql2" + gem "sqlite3" + # CodeClimate gem "minitest-reporters" gem "simplecov" diff --git a/Gemfile.lock b/Gemfile.lock index 24a392d54..b09099ee0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,7 +13,6 @@ PATH jsonapi-serializer kaminari mobility (>= 1.3.0) - pg rack-rewrite (>= 1.5.0) rails (>= 7.0, < 9.0) stimulus-rails (>= 0.7.0) @@ -213,6 +212,8 @@ GEM request_store (~> 1.0) mocha (3.0.1) ruby2_keywords (>= 0.0.5) + mysql2 (0.5.7) + bigdecimal net-imap (0.6.2) date net-protocol @@ -340,6 +341,16 @@ GEM json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) + sqlite3 (2.9.0-aarch64-linux-gnu) + sqlite3 (2.9.0-aarch64-linux-musl) + sqlite3 (2.9.0-arm-linux-gnu) + sqlite3 (2.9.0-arm-linux-musl) + sqlite3 (2.9.0-arm64-darwin) + sqlite3 (2.9.0-x86-linux-gnu) + sqlite3 (2.9.0-x86-linux-musl) + sqlite3 (2.9.0-x86_64-darwin) + sqlite3 (2.9.0-x86_64-linux-gnu) + sqlite3 (2.9.0-x86_64-linux-musl) stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.2.0) @@ -396,6 +407,8 @@ DEPENDENCIES letter_opener minitest-reporters mocha + mysql2 + pg propshaft pry-rails puma @@ -403,6 +416,7 @@ DEPENDENCIES selenium-webdriver (~> 4.11.0) simplecov spina! + sqlite3 BUNDLED WITH 2.5.17 diff --git a/app/controllers/spina/admin/page_select_options_controller.rb b/app/controllers/spina/admin/page_select_options_controller.rb index f63e1d65f..6e955c883 100644 --- a/app/controllers/spina/admin/page_select_options_controller.rb +++ b/app/controllers/spina/admin/page_select_options_controller.rb @@ -15,7 +15,7 @@ def search end @pages ||= Page.all - @pages = @pages.joins(:translations).where("spina_page_translations.title ILIKE :query OR materialized_path ILIKE :query", query: "%#{params[:search]}%").order(created_at: :desc).distinct.page(params[:page]).per(20) + @pages = @pages.joins(:translations).where("LOWER(spina_page_translations.title) LIKE LOWER(:query) OR LOWER(materialized_path) LIKE LOWER(:query)", query: "%#{params[:search]}%").order(created_at: :desc).distinct.page(params[:page]).per(20) render :index end diff --git a/app/controllers/spina/admin/resource_select_options_controller.rb b/app/controllers/spina/admin/resource_select_options_controller.rb index 708108599..fd35ee0f9 100644 --- a/app/controllers/spina/admin/resource_select_options_controller.rb +++ b/app/controllers/spina/admin/resource_select_options_controller.rb @@ -11,7 +11,7 @@ def index def search @resources ||= Resource.all - @resources = @resources.where("name ILIKE :query OR label ILIKE :query", query: "%#{params[:search]}%").order(created_at: :desc).distinct.page(params[:page]).per(20) + @resources = @resources.where("LOWER(name) LIKE LOWER(:query) OR LOWER(label) LIKE LOWER(:query)", query: "%#{params[:search]}%").order(created_at: :desc).distinct.page(params[:page]).per(20) render :index end diff --git a/app/jobs/spina/replace_signed_id_job.rb b/app/jobs/spina/replace_signed_id_job.rb index 74a6ba875..c6b73f662 100644 --- a/app/jobs/spina/replace_signed_id_job.rb +++ b/app/jobs/spina/replace_signed_id_job.rb @@ -5,19 +5,22 @@ class ReplaceSignedIdJob < ApplicationJob def perform(old_signed_id, new_signed_id) return if old_signed_id.blank? || new_signed_id.blank? - pages = get_pages(old_signed_id) - accounts = Spina::Account.all - - [pages, accounts].each do |records| - records.update_all("json_attributes = REGEXP_REPLACE(json_attributes::text, '#{old_signed_id}', '#{new_signed_id}', 'g')::jsonb") - end + replace_in_records(Spina::Page, old_signed_id, new_signed_id) + replace_in_records(Spina::Account, old_signed_id, new_signed_id) end private - def get_pages(signed_id) - return Spina::Page.none unless signed_id.present? - Spina::Page.where("json_attributes::text LIKE ?", "%#{signed_id}%") + def replace_in_records(model, old_signed_id, new_signed_id) + model.find_each(batch_size: 100) do |record| + next unless record.json_attributes.present? + + json_string = record.json_attributes.to_json + next unless json_string.include?(old_signed_id) + + updated_json = json_string.gsub(old_signed_id, new_signed_id) + record.update_column(:json_attributes, JSON.parse(updated_json)) + end end end end diff --git a/app/models/concerns/spina/attachable.rb b/app/models/concerns/spina/attachable.rb index c7adb916d..2dab5dd3b 100644 --- a/app/models/concerns/spina/attachable.rb +++ b/app/models/concerns/spina/attachable.rb @@ -7,7 +7,7 @@ module Attachable scope :with_filename, ->(query) do joins(:file_blob).where( - "active_storage_blobs.filename ILIKE ?", + "LOWER(active_storage_blobs.filename) LIKE LOWER(?)", "%" + Image.sanitize_sql_like(query) + "%" ) end diff --git a/app/models/spina/resource.rb b/app/models/spina/resource.rb index 35325ef1c..009b847ee 100644 --- a/app/models/spina/resource.rb +++ b/app/models/spina/resource.rb @@ -6,7 +6,7 @@ class Resource < ApplicationRecord after_commit :update_resource_pages, on: [:update] - translates :slug, backend: :jsonb + translates :slug, backend: :json def pages case order_by diff --git a/db/migrate/13_add_json_attributes_to_spina_accounts.rb b/db/migrate/13_add_json_attributes_to_spina_accounts.rb index f1f3c3087..515b30b89 100644 --- a/db/migrate/13_add_json_attributes_to_spina_accounts.rb +++ b/db/migrate/13_add_json_attributes_to_spina_accounts.rb @@ -1,5 +1,5 @@ class AddJsonAttributesToSpinaAccounts < ActiveRecord::Migration[5.2] def change - add_column :spina_accounts, :json_attributes, :jsonb + add_column :spina_accounts, :json_attributes, :json end end diff --git a/db/migrate/14_add_json_attributes_to_spina_pages.rb b/db/migrate/14_add_json_attributes_to_spina_pages.rb index c7b82935d..41ca4a132 100644 --- a/db/migrate/14_add_json_attributes_to_spina_pages.rb +++ b/db/migrate/14_add_json_attributes_to_spina_pages.rb @@ -1,5 +1,5 @@ class AddJsonAttributesToSpinaPages < ActiveRecord::Migration[5.2] def change - add_column :spina_pages, :json_attributes, :jsonb + add_column :spina_pages, :json_attributes, :json end end diff --git a/db/migrate/15_add_slug_to_spina_resources.rb b/db/migrate/15_add_slug_to_spina_resources.rb index 910e8bcc9..3341f1a64 100644 --- a/db/migrate/15_add_slug_to_spina_resources.rb +++ b/db/migrate/15_add_slug_to_spina_resources.rb @@ -1,5 +1,5 @@ class AddSlugToSpinaResources < ActiveRecord::Migration[5.2] def change - add_column :spina_resources, :slug, :jsonb + add_column :spina_resources, :slug, :json end end diff --git a/db/migrate/18_change_default_spina_resources_slug.rb b/db/migrate/18_change_default_spina_resources_slug.rb index a87892edb..960e6282a 100644 --- a/db/migrate/18_change_default_spina_resources_slug.rb +++ b/db/migrate/18_change_default_spina_resources_slug.rb @@ -1,9 +1,9 @@ class ChangeDefaultSpinaResourcesSlug < ActiveRecord::Migration[7.0] def up - change_column :spina_resources, :slug, :jsonb, default: {} + change_column :spina_resources, :slug, :json, default: {} end def down - change_column :spina_resources, :slug, :jsonb, default: nil + change_column :spina_resources, :slug, :json, default: nil end end diff --git a/db/migrate/7_create_spina_settings.rb b/db/migrate/7_create_spina_settings.rb index f4ee57c9f..e68a490ad 100644 --- a/db/migrate/7_create_spina_settings.rb +++ b/db/migrate/7_create_spina_settings.rb @@ -2,7 +2,7 @@ class CreateSpinaSettings < ActiveRecord::Migration[5.0] def change create_table :spina_settings do |t| t.string :plugin - t.jsonb :preferences, default: {} + t.json :preferences, default: {} t.timestamps end diff --git a/docs/v2/advanced/1_create_custom_parts.md b/docs/v2/advanced/1_create_custom_parts.md index 5f556912b..112db0281 100644 --- a/docs/v2/advanced/1_create_custom_parts.md +++ b/docs/v2/advanced/1_create_custom_parts.md @@ -1,6 +1,6 @@ # Custom parts -All page content is stored in a single JSONB-column in the database. Spina uses the `attr_json` gem to work with these nested json objects. All default parts are `AttrJson::Model` objects. Follow the steps below to create your own custom part. +All page content is stored in a single JSON column in the database. Spina uses the `attr_json` gem to work with these nested json objects. All default parts are `AttrJson::Model` objects. Follow the steps below to create your own custom part. ## Step 1. Create a part Let's imagine our app contains a movie database. We'd like to add a part to select one of the movies in our collection. First we need to create the object that can be stored as page content. diff --git a/docs/v2/getting_started/2_installing_spina.md b/docs/v2/getting_started/2_installing_spina.md index 126b5916d..de6d9276b 100644 --- a/docs/v2/getting_started/2_installing_spina.md +++ b/docs/v2/getting_started/2_installing_spina.md @@ -1,11 +1,21 @@ # Install Spina CMS -Start by creating a new Rails app using a PostgreSQL database. +Start by creating a new Rails app. Spina supports PostgreSQL, MySQL, and SQLite databases. ``` rails new yourwebsite --database=postgresql ``` +Or for MySQL: +``` +rails new yourwebsite --database=mysql +``` + +Or for SQLite (development only): +``` +rails new yourwebsite --database=sqlite3 +``` + Create a database. ``` diff --git a/docs/v2/getting_started/3_existing_project.md b/docs/v2/getting_started/3_existing_project.md index 0b03440e4..71388a729 100644 --- a/docs/v2/getting_started/3_existing_project.md +++ b/docs/v2/getting_started/3_existing_project.md @@ -1,6 +1,6 @@ # Existing project -If you already have an existing Rails project, just add `gem "spina"` to your Gemfile and run `bundle install`. (Make sure you're using PostgreSQL as your database) +If you already have an existing Rails project, just add `gem "spina"` to your Gemfile and run `bundle install`. Spina supports PostgreSQL, MySQL (5.7.8+), and SQLite (3.38+) databases. Run the installer to get started. diff --git a/spina.gemspec b/spina.gemspec index 3837222bd..d9ec6c4b0 100644 --- a/spina.gemspec +++ b/spina.gemspec @@ -34,7 +34,6 @@ Gem::Specification.new do |gem| gem.files = Dir["{app,config,db,lib,vendor}/**/*"] + ["Rakefile", "README.md"] gem.add_dependency "rails", ">= 7.0", "< 9.0" - gem.add_dependency "pg" gem.add_dependency "bcrypt" gem.add_dependency "image_processing" gem.add_dependency "ancestry" diff --git a/test/dummy/config/database.yml b/test/dummy/config/database.yml index 4b17b52a2..1ca5fe17b 100644 --- a/test/dummy/config/database.yml +++ b/test/dummy/config/database.yml @@ -1,23 +1,5 @@ -# SQLite. Versions 3.8.0 and up are supported. -# gem install sqlite3 -# -# Ensure the SQLite 3 gem is defined in your Gemfile -# gem 'sqlite3' -# -# -default: &default - adapter: postgresql - encoding: unicode - pool: 5 - host: localhost - -development: - <<: *default - database: spina_development - -# Warning: The database defined as "test" will be erased and -# re-generated from your development database when you run "rake". -# Do not set this db to the same as development or production. test: - <<: *default - database: spina_test + adapter: sqlite3 + pool: 5 + timeout: 5000 + database: db/test.sqlite3 diff --git a/test/dummy/config/database.yml.mysql b/test/dummy/config/database.yml.mysql new file mode 100644 index 000000000..6290a8402 --- /dev/null +++ b/test/dummy/config/database.yml.mysql @@ -0,0 +1,9 @@ +test: + adapter: mysql2 + encoding: utf8mb4 + pool: 5 + host: 127.0.0.1 + port: 3306 + username: root + password: mysql + database: spina_test diff --git a/test/dummy/config/database.yml.ci b/test/dummy/config/database.yml.postgresql similarity index 100% rename from test/dummy/config/database.yml.ci rename to test/dummy/config/database.yml.postgresql diff --git a/test/dummy/config/database.yml.sqlite b/test/dummy/config/database.yml.sqlite new file mode 100644 index 000000000..1ca5fe17b --- /dev/null +++ b/test/dummy/config/database.yml.sqlite @@ -0,0 +1,5 @@ +test: + adapter: sqlite3 + pool: 5 + timeout: 5000 + database: db/test.sqlite3 diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index b4ad287f9..d327e079a 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -11,8 +11,6 @@ # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema[8.0].define(version: 2025_04_21_162022) do - # These are extensions that must be enabled in order to support this database - enable_extension "pg_catalog.plpgsql" create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false @@ -42,7 +40,7 @@ t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end - create_table "spina_accounts", id: :serial, force: :cascade do |t| + create_table "spina_accounts", force: :cascade do |t| t.string "name" t.string "address" t.string "postal_code" @@ -53,20 +51,20 @@ t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.boolean "robots_allowed", default: false - t.jsonb "json_attributes" + t.json "json_attributes" end - create_table "spina_attachment_collections", id: :serial, force: :cascade do |t| + create_table "spina_attachment_collections", force: :cascade do |t| t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end - create_table "spina_attachment_collections_attachments", id: :serial, force: :cascade do |t| + create_table "spina_attachment_collections_attachments", force: :cascade do |t| t.integer "attachment_collection_id" t.integer "attachment_id" end - create_table "spina_attachments", id: :serial, force: :cascade do |t| + create_table "spina_attachments", force: :cascade do |t| t.string "file" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false @@ -77,7 +75,7 @@ t.datetime "updated_at", precision: nil, null: false end - create_table "spina_image_collections_images", id: :serial, force: :cascade do |t| + create_table "spina_image_collections_images", force: :cascade do |t| t.integer "image_collection_id" t.integer "image_id" t.integer "position" @@ -92,7 +90,7 @@ t.index ["media_folder_id"], name: "index_spina_images_on_media_folder_id" end - create_table "spina_layout_parts", id: :serial, force: :cascade do |t| + create_table "spina_layout_parts", force: :cascade do |t| t.string "title" t.string "name" t.integer "layout_partable_id" @@ -102,7 +100,7 @@ t.integer "account_id" end - create_table "spina_line_translations", id: :serial, force: :cascade do |t| + create_table "spina_line_translations", force: :cascade do |t| t.integer "spina_line_id", null: false t.string "locale", null: false t.string "content" @@ -112,7 +110,7 @@ t.index ["spina_line_id"], name: "index_spina_line_translations_on_spina_line_id" end - create_table "spina_lines", id: :serial, force: :cascade do |t| + create_table "spina_lines", force: :cascade do |t| t.datetime "created_at", precision: nil t.datetime "updated_at", precision: nil end @@ -123,7 +121,7 @@ t.datetime "updated_at", precision: nil, null: false end - create_table "spina_navigation_items", id: :serial, force: :cascade do |t| + create_table "spina_navigation_items", force: :cascade do |t| t.integer "page_id" t.integer "navigation_id", null: false t.integer "position", default: 0, null: false @@ -136,7 +134,7 @@ t.index ["page_id", "navigation_id"], name: "index_spina_navigation_items_on_page_id_and_navigation_id", unique: true end - create_table "spina_navigations", id: :serial, force: :cascade do |t| + create_table "spina_navigations", force: :cascade do |t| t.string "name", null: false t.string "label", null: false t.boolean "auto_add_pages", default: false, null: false @@ -146,13 +144,13 @@ t.index ["name"], name: "index_spina_navigations_on_name", unique: true end - create_table "spina_options", id: :serial, force: :cascade do |t| + create_table "spina_options", force: :cascade do |t| t.string "value" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false end - create_table "spina_page_parts", id: :serial, force: :cascade do |t| + create_table "spina_page_parts", force: :cascade do |t| t.string "title" t.string "name" t.datetime "created_at", precision: nil, null: false @@ -162,7 +160,7 @@ t.string "page_partable_type" end - create_table "spina_page_translations", id: :serial, force: :cascade do |t| + create_table "spina_page_translations", force: :cascade do |t| t.integer "spina_page_id", null: false t.string "locale", null: false t.string "title" @@ -177,7 +175,7 @@ t.index ["spina_page_id"], name: "index_spina_page_translations_on_spina_page_id" end - create_table "spina_pages", id: :serial, force: :cascade do |t| + create_table "spina_pages", force: :cascade do |t| t.boolean "show_in_menu", default: true t.string "slug" t.boolean "deletable", default: true @@ -193,7 +191,7 @@ t.integer "position" t.boolean "active", default: true t.integer "resource_id" - t.jsonb "json_attributes" + t.json "json_attributes" t.integer "ancestry_depth", default: 0 t.integer "ancestry_children_count" t.index ["resource_id"], name: "index_spina_pages_on_resource_id" @@ -207,26 +205,26 @@ t.string "order_by" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false - t.jsonb "slug", default: {} + t.json "slug", default: {} t.index ["parent_page_id"], name: "index_spina_resources_on_parent_page_id" end - create_table "spina_rewrite_rules", id: :serial, force: :cascade do |t| + create_table "spina_rewrite_rules", force: :cascade do |t| t.string "old_path" t.string "new_path" t.datetime "created_at", precision: nil t.datetime "updated_at", precision: nil end - create_table "spina_settings", id: :serial, force: :cascade do |t| + create_table "spina_settings", force: :cascade do |t| t.string "plugin" - t.jsonb "preferences", default: {} + t.json "preferences", default: {} t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.index ["plugin"], name: "index_spina_settings_on_plugin" end - create_table "spina_structure_items", id: :serial, force: :cascade do |t| + create_table "spina_structure_items", force: :cascade do |t| t.integer "structure_id" t.integer "position" t.datetime "created_at", precision: nil @@ -234,7 +232,7 @@ t.index ["structure_id"], name: "index_spina_structure_items_on_structure_id" end - create_table "spina_structure_parts", id: :serial, force: :cascade do |t| + create_table "spina_structure_parts", force: :cascade do |t| t.integer "structure_item_id" t.integer "structure_partable_id" t.string "structure_partable_type" @@ -246,12 +244,12 @@ t.index ["structure_partable_id"], name: "index_spina_structure_parts_on_structure_partable_id" end - create_table "spina_structures", id: :serial, force: :cascade do |t| + create_table "spina_structures", force: :cascade do |t| t.datetime "created_at", precision: nil t.datetime "updated_at", precision: nil end - create_table "spina_text_translations", id: :serial, force: :cascade do |t| + create_table "spina_text_translations", force: :cascade do |t| t.integer "spina_text_id", null: false t.string "locale", null: false t.text "content" @@ -261,12 +259,12 @@ t.index ["spina_text_id"], name: "index_spina_text_translations_on_spina_text_id" end - create_table "spina_texts", id: :serial, force: :cascade do |t| + create_table "spina_texts", force: :cascade do |t| t.datetime "created_at", precision: nil t.datetime "updated_at", precision: nil end - create_table "spina_users", id: :serial, force: :cascade do |t| + create_table "spina_users", force: :cascade do |t| t.string "name" t.string "email" t.string "password_digest" From a2efca5fb59fc2571a1c37592b4e3af8c5afcca0 Mon Sep 17 00:00:00 2001 From: Bram Jetten Date: Mon, 12 Jan 2026 11:44:27 +0100 Subject: [PATCH 2/2] Fix MySQL compatibility for JSON column defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MySQL doesn't allow default values on JSON columns. Move defaults to the model layer using Rails' attribute API instead. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/models/spina/resource.rb | 2 ++ app/models/spina/setting.rb | 2 ++ db/migrate/18_change_default_spina_resources_slug.rb | 4 ++-- db/migrate/7_create_spina_settings.rb | 2 +- test/dummy/db/schema.rb | 4 ++-- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/models/spina/resource.rb b/app/models/spina/resource.rb index 009b847ee..526484e9b 100644 --- a/app/models/spina/resource.rb +++ b/app/models/spina/resource.rb @@ -2,6 +2,8 @@ module Spina class Resource < ApplicationRecord extend Mobility + attribute :slug, :json, default: -> { {} } + has_many :pages, dependent: :restrict_with_exception after_commit :update_resource_pages, on: [:update] diff --git a/app/models/spina/setting.rb b/app/models/spina/setting.rb index eb7fbfd01..f9e11e98c 100644 --- a/app/models/spina/setting.rb +++ b/app/models/spina/setting.rb @@ -1,5 +1,7 @@ module Spina class Setting < ApplicationRecord + attribute :preferences, :json, default: -> { {} } + validates :plugin, presence: true end end diff --git a/db/migrate/18_change_default_spina_resources_slug.rb b/db/migrate/18_change_default_spina_resources_slug.rb index 960e6282a..9524ffd04 100644 --- a/db/migrate/18_change_default_spina_resources_slug.rb +++ b/db/migrate/18_change_default_spina_resources_slug.rb @@ -1,9 +1,9 @@ class ChangeDefaultSpinaResourcesSlug < ActiveRecord::Migration[7.0] def up - change_column :spina_resources, :slug, :json, default: {} + # Default handled in model layer for MySQL compatibility end def down - change_column :spina_resources, :slug, :json, default: nil + # Default handled in model layer for MySQL compatibility end end diff --git a/db/migrate/7_create_spina_settings.rb b/db/migrate/7_create_spina_settings.rb index e68a490ad..acb2f6df6 100644 --- a/db/migrate/7_create_spina_settings.rb +++ b/db/migrate/7_create_spina_settings.rb @@ -2,7 +2,7 @@ class CreateSpinaSettings < ActiveRecord::Migration[5.0] def change create_table :spina_settings do |t| t.string :plugin - t.json :preferences, default: {} + t.json :preferences t.timestamps end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index d327e079a..571e636e8 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -205,7 +205,7 @@ t.string "order_by" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false - t.json "slug", default: {} + t.json "slug" t.index ["parent_page_id"], name: "index_spina_resources_on_parent_page_id" end @@ -218,7 +218,7 @@ create_table "spina_settings", force: :cascade do |t| t.string "plugin" - t.json "preferences", default: {} + t.json "preferences" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.index ["plugin"], name: "index_spina_settings_on_plugin"