Rails migration plan

Ask questions Research chat →

file:///Users/raymond/personal/knowledge_base/docs/rails_migration_plan.md · scraped

testing ruby on rails rails 8.1

Attachments

Content

— 3474 words ·

Roda → Rails Migration Plan

Overview

This document covers the full migration from the current Roda + Sequel stack to Rails + ActiveRecord, including an integration-spec-first strategy so existing behaviour is locked in before any routes are touched.


Stack (Before → After)

Layer Before After
Web framework Roda ~2.x Rails ~8.1
ORM Sequel ~5.96 ActiveRecord (Rails built-in)
Templates ERB via Roda :render plugin ERB via Rails layouts/views
Server Puma via Rackup Puma via Rails
DB PostgreSQL + pgvector Same
LLM / Embeddings ruby_llm, pgvector gem Same + neighbor gem for has_neighbors
Tests Minitest + Rack::Test (minimal — one file) Minitest + ActionDispatch::IntegrationTest (87 tests)
Rake tasks Root Rakefile with require_relative "lib/knowledge_base" lib/tasks/knowledge_base.rake with task foo: :environment

Note: lib/knowledge_base modules (Scraper, QA, FeedImporter, etc.) still use Sequel internally. They are framework-agnostic and were not rewritten.


Why This Migration Is Non-Trivial

  • 40+ routes across one 610-line file (app_roda.rb) — no controller separation
  • 15 Sequel models with custom association names, hooks, and filtered associations (propositions as a scoped one_to_many)
  • pgvector — needs pgvector Rails adapter or a raw SQL approach
  • Rake tasks reference Sequel models directly — need updating or dual-booting during transition
  • No meaningful test coverage — this is the biggest risk; behaviour must be captured first
  • Session/auth — simple cookie sessions via Roda; maps cleanly to Rails sessions
  • Helpers (h, markdown, status_badge) — defined inline in app_roda.rb; need to move to Rails helpers
  • ruby_llm, redcarpet — framework-agnostic, keep as-is

Phase 0: Integration Specs First ✅ COMPLETED

The goal is a suite of request specs that run against the current Roda app and pass. These same specs will be run again against the Rails app. When they all pass on Rails, the migration is done.

0.1 Add gems to Gemfile (test group)

group :test do
  gem 'rack-test'      # already present
  gem 'minitest'       # already present
  gem 'minitest-spec-rails'  # optional, nicer syntax
  gem 'vcr'            # record/replay HTTP calls (scraper tests)
  gem 'webmock'        # stub external HTTP
  gem 'factory_bot'    # test data
  gem 'database_cleaner-sequel'
end

0.2 Test helper setup

Create test/integration_helper.rb:

require 'rack/test'
require_relative '../config.ru'   # loads the Roda app

module IntegrationHelpers
  include Rack::Test::Methods

  def app
    # Points at the Roda app today; will be swapped to Rails app later
    Rack::Builder.parse_file('config.ru').first
  end
end

0.3 What to cover (priority order)

Write one test file per major feature area. Each file should cover happy-path responses (correct status codes, key content present), and the most important error/edge cases.

test/integration/bookmarks_test.rb

  • GET /bookmarks — 200, bookmark titles present
  • GET /bookmarks/:id — 200, scraped content shown
  • POST /bookmarks/:id/star — toggles starred, JSON response { starred: true/false }
  • POST /bookmarks/:id/title — updates title, JSON { ok: true, title: "..." }
  • POST /bookmarks/:id/note — saves note, 200
  • POST /bookmarks/:id/read_status — 200
  • POST /bookmarks/:id/tags — adds tag, JSON { ok: true, name: "..." }
  • POST /bookmarks/:id/tags/:name — removes tag, 200
  • GET /bookmarks/:id/links/search?q=foo — JSON array of bookmarks
  • POST /bookmarks/:id/links — links bookmark, JSON { ok: true, id:, title: }
  • POST /bookmarks/:id/links/:id/delete — unlinks, JSON { ok: true }
  • POST /bookmarks/:id/annotations — creates annotation, 200
  • POST /bookmarks/:id/annotations/:id/update — edits, 200
  • POST /bookmarks/:id/annotations/:id/delete — removes, 200
  • GET /bookmarks/:id/export.md — returns markdown content-type
  • GET /bookmarks/:id/export.md?include_related=true — includes related bookmarks
  • POST /bookmarks/:id/delete — deletes, redirects to /

test/integration/search_test.rb

  • GET /search?q=foo — 200, results section present (stub embedding + LLM calls)
  • GET /search (no query) — 200, empty state

test/integration/qa_test.rb

  • GET /bookmarks/:id/qa — 200, conversation UI
  • POST /bookmarks/:id/qa — stubs LLM, returns 200 with response

test/integration/feeds_test.rb

  • GET /feeds — 200, folder list
  • GET /feeds/:folder — 200, feed list
  • GET /feeds/feed/:id — 200, items
  • POST /feeds/feed/:id/refresh — 200 or redirect
  • POST /feeds/item/:id/save — saves as bookmark

test/integration/misc_test.rb

  • GET / — 200, uploaded files list
  • GET /uploads — same as /
  • GET /tags/:name — 200, filtered bookmarks
  • GET /favorited — 200, starred bookmarks
  • GET /topics — 200
  • GET /attachments/:id — 200 with binary body or redirect to signed URL

0.4 Test data strategy

Use plain helper methods in the base IntegrationTest class to create the minimum data needed per test. Wrap each test in DB.transaction(rollback: :always, auto_savepoint: true) so no data persists between tests. No external gems (databasecleaner, factorybot) are needed.

class IntegrationTest < Minitest::Test
  include Rack::Test::Methods

  def app; App; end

  def run(*)
    DB.transaction(rollback: :always, auto_savepoint: true) { super }
  end

  def create_bookmark(**attrs)
    defaults = {
      source_id: 2, url: "https://example.com/test-#{SecureRandom.hex(6)}",
      title: "Test Bookmark", status: "scraped", read_status: "unread",
      starred: false, metadata: "{}", created_at: Time.now, updated_at: Time.now
    }
    KnowledgeBase::Bookmark.create(**defaults.merge(attrs))
  end

  def create_scraped_content(bookmark, **attrs)
    # Must use DB insert directly — ScrapedContent blocks created_at= via mass assignment
    defaults = { bookmark_id: bookmark.id, content: "Test content.",
                 word_count: 3, excerpt: "Test.", scraped_at: Time.now }
    id = DB[:scraped_contents].insert(defaults.merge(attrs))
    KnowledgeBase::ScrapedContent.first(id: id)
  end

  # ... create_tag, create_annotation, create_rss_feed, create_attachment, create_rss_item
  # Note: create_rss_item must also use DB[:rss_items].insert — same mass assignment issue
end

0.5 Stubbing AI calls

All LLM and embedding calls must be stubbed in tests — they're slow, cost money, and introduce flakiness. Do not use Minitest::Mock or Object#stub — Minitest 6 does not expose Object#stub. Instead, use define_singleton_method with an ensure-based restore:

def with_stub(object, method_name, return_value)
  original = object.method(method_name)
  object.define_singleton_method(method_name) { |*_args| return_value }
  yield
ensure
  object.define_singleton_method(method_name, original)
end

STUB_QA_RESULT = {
  answer: "Stubbed LLM answer for testing.",
  context: [], optimized_query: "stubbed query"
}.freeze

# Usage in a test:
with_stub(KnowledgeBase::QA, :ask, STUB_QA_RESULT) do
  get "/search?query=what+is+ruby"
end

0.6 App bugs uncovered during Phase 0

Writing the specs found three bugs in app_roda.rb that had to be fixed before tests could pass:

1. r.halt wrong signature (6 places)

Roda 3.101's halt only accepts a single Rack array argument. The app was calling r.halt(404, "message") throughout, which raises ArgumentError. Fix:
```ruby

Wrong

r.halt(404, "Not found")

Correct

r.halt([404, {}, ["Not found"]])
```

2. Sequel::Model mass assignment restriction on created_at

Several models (ScrapedContent, RssItem) block created_at= via Sequel's mass assignment restrictions. Calling Model.create(created_at: Time.now, ...) raises Sequel::MassAssignmentRestriction. Fix: use DB[:table].insert(row) directly in factory helpers, then reload with Model.first(id: id).

3. add_bookmark_note doesn't exist

Sequel only generates add_* methods for one_to_many associations, not one_to_one. Bookmark has one_to_one :bookmark_note, so @bookmark.add_bookmark_note(...) raises NoMethodError. Fix in app_roda.rb:
```ruby

Wrong

@bookmark.addbookmarknote(content: content)

Correct

KnowledgeBase::BookmarkNote.create(
bookmarkid: @bookmark.id, content: content,
created
at: Time.now, updated_at: Time.now
)
```

0.7 Running the suite

The Roda integration tests (now archived in archive/roda/test/integration/) were run with:

bundle exec ruby -Itest \
  test/integration/bookmarks_test.rb \
  test/integration/feeds_test.rb \
  test/integration/misc_test.rb \
  test/integration/search_test.rb \
  test/integration/qa_test.rb

The equivalent Rails tests (in test/integration/) now run as part of the standard suite:

bundle exec rails test

Phase 1: Rails App Scaffolding ✅ COMPLETED

Stand up a Rails app alongside the Roda app. Both can read from the same database.

1.1 Generate the Rails app

# In a sibling directory or a subfolder
rails new knowledge_base_rails \
  --database=postgresql \
  --skip-action-mailer \
  --skip-action-mailbox \
  --skip-action-cable \
  --skip-active-storage \
  --skip-action-text \
  --skip-jbuilder \
  --no-rc

For a personal app with no asset pipeline complexity, add --asset-pipeline=none or just use import maps.

1.2 Database config

Point config/database.yml at the same Postgres database. No new migrations yet — Rails will read the existing schema.

1.3 Keep lib/knowledge_base/ as-is

The lib/ directory is framework-agnostic. Copy or symlink it. All the heavy logic (QA, scraper, embedder, chunker, etc.) stays unchanged.

Add to config/application.rb — use autoload_lib with knowledge_base excluded from Zeitwerk (it uses Sequel and has its own loading structure):
ruby
config.autoload_lib(ignore: %w[assets tasks knowledge_base])

Note: config.autoload_paths << Rails.root.join('lib') was the original plan but autoload_lib is the Rails 7.1+ idiom. The knowledge_base directory must be in the ignore list because Zeitwerk cannot auto-load Sequel models that live alongside AR models under the same namespace.

1.4 Gemfile migration

Carry over all non-Roda gems. Swap:

Remove Add
roda — (gone)
sequel activerecord (bundled with Rails)
rackup — (Rails uses Puma directly)
tilt — (Rails uses ERB natively)
sinatra_helpers (roda plugin) Rails helpers
database_cleaner-sequel database_cleaner-active_record

Keep: ruby_llm, pgvector, redcarpet, nokogiri, ruby-readability, reverse_markdown, notion-ruby-client, rss, google-cloud-storage, dotenv-rails.

Note: pgvector has a Rails/ActiveRecord adapter — add gem 'pgvector' and it works with has_neighbors.


Phase 2: ActiveRecord Models ✅ COMPLETED

15 models created in app/models/ (13 mirroring Sequel models + 2 new join-table models), all plain ApplicationRecord subclasses — no KnowledgeBase:: namespace.

2.1 Association mapping (Sequel → ActiveRecord)

Sequel ActiveRecord
many_to_one :source belongs_to :source
one_to_many :bookmarks has_many :bookmarks
many_to_many :tags has_many :tags, through: :bookmark_tags
one_to_one :scraped_content has_one :scraped_content
one_to_many :bookmark_annotations, order: :created_at has_many :bookmark_annotations, -> { order(:created_at) }
before_save { self.updated_at = Time.now } Not needed — AR handles timestamps automatically

Two extra model files added that have no Sequel equivalent: BookmarkTag and BookmarkLink (join table models needed for has_many :through and related_bookmarks).

2.2 model_name dangerous attribute

The embeddings table has a model_name column which conflicts with ActiveRecord::Base.model_name (a class-level method from ActiveModel::Naming). Rails raises ActiveRecord::DangerousAttributeError at boot when it tries to define the attribute accessor. Fix:

# app/models/embedding.rb
def self.dangerous_attribute_method?(name)
  return false if name.to_s == "model_name"
  super
end

This suppresses the error so AR defines the model_name instance getter/setter normally. The column value is accessible as embedding.model_name and holds the embedding model string (e.g. "gemini-embedding-001").

2.3 Filtered association (propositions)

ScrapedContent's propositions association filters embeddings by chunk_type. In AR, this uses a scope lambda:

has_many :propositions,
  -> { where(chunk_type: "proposition").order(:chunk_index) },
  class_name: "Embedding",
  foreign_key: :scraped_content_id

2.4 Related bookmarks (bidirectional join)

bookmark_links is a directional join table. related_bookmarks queries both directions. In AR, this becomes two pluck calls on the BookmarkLink model rather than raw SQL:

def related_bookmarks
  forward_ids = BookmarkLink.where(bookmark_id: id).pluck(:linked_bookmark_id)
  reverse_ids = BookmarkLink.where(linked_bookmark_id: id).pluck(:bookmark_id)
  ids = (forward_ids + reverse_ids).uniq
  return [] if ids.empty?
  Bookmark.where(id: ids).order(:title)
end

2.5 record_timestamps = false

Several tables don't have both created_at and updated_at. Rails will error if it tries to write a column that doesn't exist, so those models need self.record_timestamps = false:

Model Situation
Tag, Attachment, Embedding, QaMessage, RssFeed, BookmarkLink created_at only, no updated_at
ScrapedContent Neither — has scraped_at instead
RssItem Neither — has fetched_at and published_at
Library Neither — has indexed_at

2.6 pgvector / has_neighbors

The plan said gem 'pgvector' would provide has_neighbors. It does not — pgvector 0.3.3 only provides the Sequel adapter and the raw PG type. has_neighbors comes from the separate neighbor gem:

# Gemfile
gem "pgvector"   # vector type registration
gem "neighbor"   # has_neighbors for ActiveRecord

# config/initializers/pgvector.rb
require "neighbor"

# app/models/embedding.rb
class Embedding < ApplicationRecord
  has_neighbors :embedding
end

The raw vector SQL in lib/knowledge_base/qa.rb (find_context) continues to use Sequel for now and is untouched.


Phase 3: Routes → Controllers ✅ COMPLETED

12 controllers created in app/controllers/. The BookmarkActionsController from the original plan was folded into BookmarksController to keep related actions together.

Controller Actions
BookmarksController uploads (root + /uploads), favorited, index, show, star, scrape, extractmetadata, title, note, readstatus, destroy
TagsController show (tag filter page), create (on bookmark), destroy (from bookmark)
LinksController search, create, destroy
AnnotationsController create, update, destroy
PropositionsController delete_all, destroy
QaController show, create
ExportsController show (markdown download)
SearchController index
TopicsController index, create
FeedsController index, folder, show, refresh, destroy, discover, add
FeedItemsController save
AttachmentsController show

3.1 URL structure

Rails resources DSL was not used — routes are defined as flat explicit paths to exactly match the Roda app's URLs. This means the integration tests written against Roda can run against Rails without any URL changes.

Key ordering rule: /feeds/feed/:id must be declared before /feeds/:folder in routes.rb, otherwise "feed" matches as a folder name.

CSRF protection is disabled (protect_from_forgery with: :null_session) since this is a local personal app. The Rails integration tests use ActionDispatch::IntegrationTest which does not send CSRF tokens by default.

3.2 Sequel → ActiveRecord method translations

Roda (Sequel) Rails (ActiveRecord)
Tag.find_or_create(name: name) Tag.find_or_create_by(name: name)
@bookmark.add_tag(tag) BookmarkTag.find_or_create_by(bookmark_id:, tag_id:)
@bookmark.remove_tag(tag) @bookmark.tags.delete(tag)
@bookmark.add_bookmark_annotation(...) @bookmark.bookmark_annotations.create!(...)
conversation.add_qa_message(...) conversation.qa_messages.create!(...)
conversation.update(updated_at: Time.now) conversation.touch
db[:bookmark_links].insert(...) BookmarkLink.create!(...)
db[:embeddings].where(...).delete Embedding.where(...).delete_all
QaConversation.find_or_create(bookmark_id: id) QaConversation.find_or_create_by(bookmark_id: id)
r.halt([404, {}, ["msg"]]) render plain: "msg", status: :not_found
r.halt([422, {}, []]) head :unprocessable_entity

3.3 Helpers

Moved from inline Roda methods to ApplicationHelper:
- markdown(text) — Redcarpet rendering, returns html_safe string
- status_badge(status) — coloured HTML span, returns html_safe
- read_status_badge(status) — same
- h(text) — already available in Rails via ERB::Util


Phase 4: Views ✅ COMPLETED

ERB templates ported with the following adaptations:

Roda Rails
h(text) h(text) (same — from ERB::Util)
<%= markdown(sc.content) %> Same, helper defined in ApplicationHelper
r.redirect '/' redirect_to root_path
Layout via :render plugin app/views/layouts/application.html.erb
@vars set in route block @vars set in controller action
request.params["notice"] params[:notice]

View file mapping (all 14 templates ported)

Roda view Rails view
views/layout.erb app/views/layouts/application.html.erb
views/index.erb app/views/bookmarks/uploads.html.erb
views/uploads.erb merged into bookmarks/uploads.html.erb
views/bookmarks.erb app/views/bookmarks/index.html.erb
views/show.erb app/views/bookmarks/show.html.erb
views/favorited.erb app/views/bookmarks/favorited.html.erb
views/tag.erb app/views/tags/show.html.erb
views/search.erb app/views/search/index.html.erb
views/qa.erb app/views/qa/show.html.erb
views/topics.erb app/views/topics/index.html.erb
views/feeds_index.erb app/views/feeds/index.html.erb
views/feeds_folder.erb app/views/feeds/folder.html.erb
views/feeds_feed.erb app/views/feeds/show.html.erb
views/export_md.erb app/views/exports/show.erb

Non-trivial adaptations

@body_class = "show-page" in view → controller
- In Roda, the show template set @body_class inline; in Rails, moved to BookmarksController#show as @body_class = "show-page" so it's available when the layout renders.

Inline DB queries moved to controllers
- search.erb called KnowledgeBase::Library.order(...) inline → moved to SearchController#index as @libraries
- qa.erb called KnowledgeBase::Embedding.join(...) inline → moved to QaController#show as @has_embeddings

Feeds index: AR aggregate access vs hash access
- Roda's Sequel query returned plain hashes (row[:folder], row[:feed_count]); AR returns model instances with attribute methods → changed to row.folder, row.feed_count.to_i, row.last_fetched_at

Export view
- exports/show.erb renders plain Markdown (no HTML layout); controller already sets layout: false and content_type: "text/markdown" — no changes needed in the view itself

.to_json.html_safe for ANNOTATIONS JS constant
- In show.html.erb, the annotations JSON embedded in a <script> tag uses .to_json.html_safe to prevent double-escaping

confirm() strings in folder view
- Replaced feed.title.gsub("'", "\\'") Ruby-side escaping with Rails' j() helper for JS-safe string embedding


Phase 5: Rake Tasks ✅ COMPLETED

All tasks ported to lib/tasks/knowledge_base.rake. Each task depends on :environment so Rails + AR models are loaded automatically. The lib/knowledge_base modules still use Sequel internally (fine — Sequel remains in the Gemfile); only the rake task shell code itself uses AR.

Sequel → AR translation in tasks

Sequel (old Rakefile) ActiveRecord (new .rake file)
KnowledgeBase::ScrapedContent ScrapedContent
KnowledgeBase::Embedding Embedding
KnowledgeBase::BookmarkNote BookmarkNote
KnowledgeBase::BookmarkAnnotation BookmarkAnnotation
KnowledgeBase::Library Library
KnowledgeBase::Tag Tag
KnowledgeBase::Bookmark[id] Bookmark.find_by(id: id)
.exclude(id: list) .where.not(id: list)
.select_map(:field) .pluck(:field)
.all .to_a
Model.where(...).delete Model.where(...).delete_all
lib.delete lib.destroy
KnowledgeBase::Database.connection ActiveRecord::Base.connection (via kb_connection)
db.literal(text) conn.quote(text)
db.run(sql) conn.execute(sql)
DB[sql, arg1, arg2] conn.select_all(sql) with literal interpolation
Sequel.function(:length, :content) "length(content)" in .sum(...)
task :foo do ... require_relative "lib/kb" ... end task foo: :environment do ... end

What was NOT ported

  • db:migrate — Rails built-in (bundle exec rails db:migrate) replaces the Sequel migrator task
  • server task — not needed; use rails server or bin/rails s

Note: db:dump and db:restore were ported to lib/tasks/knowledge_base.rake (they use docker compose exec + pg_dump/psql, no ORM dependency, but still live in the same .rake file for discoverability).

lib/knowledge_base modules

These modules (Scraper, NotionSync, NotionContent, KeepImport, FeedImporter, PropositionExtractor, LibraryDocScraper, LibrarySourceExtractor) still use Sequel internally. They continue to work because:
1. Sequel is still in Gemfile
2. config/initializers/knowledge_base.rb does require_relative "../../lib/knowledge_base" which sets up the Sequel connection
3. These are called from rake tasks and controllers that go through lib/knowledge_base — no change needed


Phase 6: Cutover ✅ COMPLETED

All 85 integration tests pass against the Rails app. Cutover actions taken:

Files archived to archive/roda/

  • app_roda.rb — the 610-line Roda route file
  • config.ru — Rack entry point for Roda
  • views/ — all 14 Roda ERB templates (ported to app/views/)
  • test/integration_helper.rb — Roda integration test base class
  • test/integration/ — Roda integration test files
  • test/routes/ — Roda route unit tests (obsolete after cutover)

Gemfile

Removed Roda-specific gems: roda, tilt, rackup. Sequel is kept because lib/knowledge_base modules (scraper, embedder, etc.) still use it internally.

Rails app moved to repo root

The Rails app was initially built in a rails/ subdirectory, then moved to the repo root. Files from rails/ were merged:
- app/, config/, public/, script/, vendor/, log/ — moved directly
- bin/ — merged (Rails scripts + existing kb-add, dump-production, delete-character-embeddings, mdview)
- lib/tasks/knowledge_base.rake — moved to lib/tasks/ (root's lib/knowledge_base/ was identical; duplicate deleted)
- test/integration/ and test/integration_helper.rb — moved to root test/
- test/test_helper.rb — replaced with Rails version
- db/seeds.rb, db/cache_schema.rb, db/queue_schema.rb — added to root db/
- Gemfile, Gemfile.lock, Rakefile, config.ru, Dockerfile — replaced root versions
- .gitignore, .rubocop.yml, .ruby-version, .kamal/, .github/ — moved
- rails/ directory removed

Rakefile

The root Rakefile was replaced with the standard Rails one (require_relative "config/application" + Rails.application.load_tasks). All rake tasks now live in lib/tasks/knowledge_base.rake and are loaded automatically.

test/queryrewritertest.rb

The existing unit test for KnowledgeBase::QueryRewriter was updated to load lib/knowledge_base directly instead of going through the Roda test_helper. It is now picked up by bundle exec rails test.

Final test count

87 runs, 146 assertions, 0 failures (85 integration + 2 query_rewriter unit tests).

What was NOT done

lib/knowledge_base modules were not rewritten to use ActiveRecord — they still use Sequel internally. This is intentional: the modules are framework-agnostic and work correctly with Sequel in the Gemfile.


What to Watch Out For

Sequel vs ActiveRecord query semantics

  • Sequel is explicit (no magic updated_at); ActiveRecord has more conventions
  • Sequel uses dataset chaining; ActiveRecord uses scope and where
  • Sequel::Model.db raw queries → ActiveRecord::Base.connection.execute

pgvector

  • pgvector registers the vector type; the neighbor gem provides has_neighbors for ActiveRecord (they are separate gems — pgvector alone is not sufficient).
  • Raw vector SQL in qa.rb (find_context) continues to run through Sequel and is untouched.

Session handling

  • Roda uses a simple cookie session. Rails sessions work the same way by default — no change needed for users.

CSRF

  • Roda has no CSRF middleware by default (this app has none). Rails adds CSRF by default. For JSON endpoints, use protect_from_forgery with: :null_session or skip_before_action :verify_authenticity_token on API-like actions — but audit this carefully; this is a local-only personal app so it's low risk.

Migration Complete

All 6 phases done. The app runs on Rails 8.1 + ActiveRecord at the repo root. Test suite: 87 runs, 146 assertions, 0 failures (bundle exec rails test).

Lessons learned

  • Write the integration specs first — Phase 0 uncovered 3 real bugs in the Roda app before any Rails code was written. Even without the migration the specs would have been worth it.
  • autoload_lib must ignore knowledge_base — Zeitwerk cannot auto-load a directory of Sequel models alongside AR models. The ignore list is essential.
  • neighbor is separate from pgvector — easy to miss when reading docs; pgvector only registers the PG type, neighbor adds has_neighbors.
  • model_name is a reserved AR method — any table with a model_name column needs dangerous_attribute_method? overridden in the model.
  • rss_items uses fetched_at, not created_at — discovered during test setup; the column has a DB default so it never needed explicit assignment.

Visibility

Visible to everyone

Reading Status

Related Bookmarks

My Note


Saved!

Annotations

Export as Markdown
+ Annotate selection

Add Annotation