From 766cc54d9a5e67327438bcaf5d6f3e0e8f2c222c Mon Sep 17 00:00:00 2001 From: Chris Zetter <253059100+zetter-rpf@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:48:12 +0100 Subject: [PATCH 1/2] Backfill instruction_steps from legacy instructions column Previously, projects created before instruction_steps existed only had their instructions stored in the legacy text column, so Project#instructions had to fall back to it at read time. This change adds a data migration that copies the legacy instructions value into instruction_steps for any row where instruction_steps is still null, so every project can be read from instruction_steps alone. This is a step toward removing the instructions column entirely. --- ...backfill_instruction_steps_from_instructions.rb | 14 ++++++++++++++ db/schema.rb | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20260825144345_backfill_instruction_steps_from_instructions.rb diff --git a/db/migrate/20260825144345_backfill_instruction_steps_from_instructions.rb b/db/migrate/20260825144345_backfill_instruction_steps_from_instructions.rb new file mode 100644 index 000000000..d188887dd --- /dev/null +++ b/db/migrate/20260825144345_backfill_instruction_steps_from_instructions.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class BackfillInstructionStepsFromInstructions < ActiveRecord::Migration[8.1] + def up + execute <<~SQL.squish + UPDATE projects + SET instruction_steps = to_jsonb(instructions) + WHERE instruction_steps IS NULL AND instructions IS NOT NULL + SQL + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index e21d13889..0b3d42f5f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_08_20_122510) do +ActiveRecord::Schema[8.1].define(version: 2026_08_25_144345) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" From 77621ed8793436e9b81597c3cc1e6272587318e3 Mon Sep 17 00:00:00 2001 From: Chris Zetter <253059100+zetter-rpf@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:48:19 +0100 Subject: [PATCH 2/2] Stop reading and writing the legacy instructions column Previously, Project#instructions fell back to the legacy text column for rows written before instruction_steps existed, and the setter kept writing that column too. Now that instruction_steps has been backfilled for every project, that fallback is unnecessary and would break once the column is dropped. This change marks instructions as an ignored column and simplifies the instructions accessor and mutator to delegate directly to instruction_steps, removing the last runtime dependency on the legacy column so it can be safely dropped in a follow-up migration. --- app/models/project.rb | 7 ++++--- spec/models/project_spec.rb | 25 +++---------------------- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 8315c3700..8375d1203 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Project < ApplicationRecord + self.ignored_columns += [:instructions] + module Types PYTHON = 'python' HTML = 'html' @@ -83,12 +85,11 @@ def scratch_component=(value) end def instructions - self[:instruction_steps].nil? ? self[:instructions] : self[:instruction_steps] + instruction_steps end def instructions=(value) - self[:instructions] = value unless value.is_a?(Array) - self[:instruction_steps] = value + self.instruction_steps = value end def last_edited_at diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index cbfe99353..0f27463f7 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -437,31 +437,12 @@ end describe '#instructions' do - let(:project) { create(:project, :with_instructions, school:, user_id: create(:teacher, school:).id) } + let(:project) { create(:project, school:, user_id: create(:teacher, school:).id) } - it 'falls back to the legacy text column for rows that predate instruction_steps' do - project.instruction_steps = nil - project.save! - expect(project.instructions).to eq(project[:instructions]) - end - - it 'returns instruction_steps once set, without touching the legacy column for arrays' do - legacy_value = project[:instructions] + it 'delegates to instruction_steps' do project.update!(instructions: [{ markdown_content: 'step 1' }]) expect(project.instructions).to eq([{ 'markdown_content' => 'step 1' }]) - expect(project[:instructions]).to eq(legacy_value) - end - - it 'does not fall back to stale legacy text once instruction_steps is set to an empty array' do - project.update!(instructions: []) - expect(project.instructions).to eq([]) - end - - it 'also clears the legacy column when instructions are set to nil' do - project.update!(instructions: [{ markdown_content: 'step 1' }]) - project.update!(instructions: nil) - expect(project.instructions).to be_nil - expect(project[:instructions]).to be_nil + expect(project.instruction_steps).to eq([{ 'markdown_content' => 'step 1' }]) end end