Getting Started
Installation
Add Poly to your Gemfile:
gem "poly"Then install it:
bundle installRequirements
- Ruby >= 3.2
- ActiveRecord >= 7.1
Supported Databases
Poly is tested in CI against SQLite and PostgreSQL.
MySQL is explicitly not supported. Poly::Migration#poly_prime_index
relies on a partial/conditional unique index —
add_index table, [...], unique: true, where: 'is_prime' — to enforce
“exactly one prime row per resource+role, many non-primes allowed.”
PostgreSQL’s and SQLite3’s ActiveRecord adapters both override
supports_partial_index? to true; ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
does not override it, so it inherits the abstract adapter’s default of
false. Because schema_creation.rb only emits the index’s WHERE clause
when supports_partial_index? is true, MySQL silently drops the clause
instead of raising — producing a full-table unique index instead of a
partial one, and quietly breaking the single-prime-per-role invariant.
This is a silent correctness bug, not just reduced support, so running
Poly against MySQL is unsupported rather than merely uncautioned-against.
A Combined Example
The example below walks through a single model, Tagging, that uses all
three of Poly’s core building blocks together: Poly::Migration to build
the schema, Poly::Role to give the polymorphic association semantic
meaning, and Poly::Joins to generate a type-safe join back to the
resources it tags.
Migration
class CreateTaggings < ActiveRecord::Migration[7.1] include Poly::Migration
def change create_table :taggings do |t| poly_resource t, :taggable, null: false, id_type: :integer poly_role t, :taggable, null: false t.timestamps end
poly_resource_index :taggings, :taggable endendpoly_resource adds the taggable_type/taggable_id columns, poly_role
adds taggable_role, and poly_resource_index adds a composite index on
the resource columns. (id_type: :integer matches this example’s posts
table, which uses ActiveRecord’s default integer primary key — pick
whichever id_type matches your own resource tables’ primary keys.)
Model
class Post < ApplicationRecord has_many :taggings, as: :taggableend
class Tagging < ApplicationRecord belongs_to :taggable, polymorphic: true
include Poly::Role include Poly::Joins
poly_role :taggableendpoly_role :taggable validates and normalizes taggable_role (presence,
lowercase, a-z0-9_ only). Including Poly::Joins generates a
joins_taggable class method for every resource class that declares the
matching reverse association — here, Post’s has_many :taggings, as: :taggable.
Using it
post = Post.create!(title: "Hello World")
tagging = Tagging.create!(taggable: post, taggable_role: "Primary")tagging.taggable_role # => "primary" (normalized on validation)
Tagging.joins_taggable(Post).where(posts: { title: "Hello World" })# SELECT "taggings".* FROM "taggings"# INNER JOIN "posts"# ON "taggings"."taggable_id" = "posts"."id"# AND "taggings"."taggable_type" = 'Post'# WHERE "posts"."title" = 'Hello World'Next Steps
Continue with Joins to see the full range of what
Poly::Joins can generate.