activerecord-multi-tenant icon indicating copy to clipboard operation
activerecord-multi-tenant copied to clipboard

Bug: Arel.sql in update_all becomes a literal value inside MultiTenant.with

Open pkmuldoon opened this issue 4 months ago • 0 comments

Affected versions / refs

  • Gem: activerecord-multi-tenant
  • Commit tested for Rails 7.2 support: 0ac43aa
  • Rails 8 work is also affected (see Support rails 8.0.0)
  • Note: latest release (2.4.0) still declares support only up to Rails 7.0, so people may be pinning to these commits.

Summary

When calling relation.update_all with an assignment that uses Arel.sql(column_name) to reference another column, it works correctly outside a tenant block.

Inside MultiTenant.with(...), the Arel.sql value is coerced to a literal (e.g. 1 for booleans), so every row gets the same constant instead of the column value.

Expected behavior

Arel.sql("active") should be treated as a SQL expression and compiled as:

UPDATE "users" SET "user_valid" = "users"."active", "active" = ?  [["active", 0]]
WHERE "users"."practice_id" = 1;

Actual behavior

Within MultiTenant.with, the assignment is type-cast/quoted as a literal:

UPDATE "users"
SET "user_valid" = 1, "active" = 0
WHERE "users"."id" IN (
  SELECT "users"."id" FROM "users" WHERE "users"."practice_id" = 1
) AND "users"."practice_id" = 1;

Reproduction

Schema

# users: id, practice_id:bigint, user_valid:boolean, active:boolean, timestamps
class User < ApplicationRecord
  multi_tenant :practice
end

Repo script:

users = User.all

# Baseline: OUTSIDE tenant
users.update_all(user_valid: Arel.sql("active"), active: false)
# => UPDATE "users" SET "user_valid" = (active), "active" = 0

practice = Practice.first
MultiTenant.with(practice) do
  users = User.all
  users.update_all(user_valid: Arel.sql("active"), active: false)
end
# => UPDATE "users" SET "user_valid" = 1, "active" = 0 ...

Why this likely happens

Active Record normally treats Arel::Nodes::SqlLiteral as “don’t quote/type-cast this.” Inside the gem’s tenant scoping path, update_all seems to flow through a code path that type-casts the update hash, converting the Arel.sql(...) node into a bound literal.

Environment

  • Rails: 7.2.x (also visible with Rails 8 prerelease)
  • DB: PostgreSQL (likely adapter-agnostic)
  • activerecord-multi-tenant: GitHub ref 0ac43aa

pkmuldoon avatar Oct 10 '25 15:10 pkmuldoon