Detection patterns#

colref uses static AST analysis. It can only detect patterns where the field name appears as a literal in the source. References where the field name is constructed at runtime (e.g. getattr(obj, field_name)) are out of scope by design — static analysis cannot determine what string field_name holds at runtime.

This page documents exactly which patterns are and are not detected for each ORM. The ground truth is the golden test files in test_patterns/.

Output labels#

All three mean the reference was detected. The label indicates how it was found and how confident the match is.

ResultHow foundConfidence
AST attribute node (article.title)Highest — unambiguous
[string]Literal string or symbol passed to a known ORM method (.where(title: value), .pluck(:title))High — method is known to accept field names
[symbol]Symbol literal in general Ruby accessor (article[:title], article.send(:title))Medium — not Rails-specific; verify manually
[getattr]Literal string in getattr(obj, "field") or attrgetter("field")Lower — built-in, not model-specific; verify manually
[sql ref]Word-boundary substring match inside a raw SQL string (.where("title = ?", value))Lower — verify manually, false positives possible

Django#

Detected#

Attribute access
PatternExampleResult
Readarticle.title
Chainedqs.first().title
Multi-line chainArticle.objects.get(pk=1).title
Inside f-stringf"{article.title}"
Conditionalarticle.title if article else ""
List comprehension[a.title for a in qs]
Writearticle.title = value
Augmented writearticle.title += " suffix"

colref makes no read/write distinction — both are matched as attribute nodes.

getattr / attrgetter

The field name appears as a string literal. The [getattr] label signals lower confidence because getattr and attrgetter are general Python built-ins, not model-specific calls — any object with a matching attribute name will be reported.

PatternExampleResult
getattr literalgetattr(article, "title")[getattr]
getattr with defaultgetattr(article, "title", "")[getattr]
attrgetterattrgetter("title")(article)[getattr]
operator.attrgetteroperator.attrgetter("title")[getattr]
getattr with variablegetattr(article, field_name)❌ out of scope — field name not statically visible
ORM — keyword argument methods

The field name appears as a keyword argument. Lookup suffixes (__icontains, __in, etc.) are stripped before matching.

MethodExampleResult
filter.filter(title="x"), .filter(title__icontains="x")[string]
exclude.exclude(title="x")[string]
get.get(title="x")[string]
get_or_create.get_or_create(title="x")[string]
create.create(title="x")[string]
update (bulk).update(title="x")[string]
QQ(title="x"), Q(title__in=["x"])[string]
annotate.annotate(alias=expr) (keyword name)[string]
ORM — positional string argument methods

The field name appears as a positional string argument. For order_by, a leading - is stripped before matching.

MethodExampleResult
values.values("title")[string]
values_list.values_list("title", flat=True)[string]
only.only("title")[string]
defer.defer("title")[string]
order_by (asc).order_by("title")[string]
order_by (desc).order_by("-title")[string]
select_related.select_related("author")[string]
prefetch_related.prefetch_related("author")[string]
latest.latest("title")[string]
earliest.earliest("title")[string]
distinct (PostgreSQL).distinct("title")[string]
ORM — expression and aggregate functions

The field name appears as the first positional string argument.

FunctionExampleResult
FF("title"), .annotate(t=F("title"))[string]
AggregatesMax("title"), Min("title"), Avg("title"), Sum("title"), Count("title"), StdDev("title"), Variance("title")[string]
Database functionsCoalesce("title", Value("")), Concat("title", Value(" ")), Greatest("title", ...), Least("title", ...), NullIf("title", ...)[string]
SubqueryOuterRef("title"), Subquery(...)[string]
save with update_fields

The field name appears as a string element inside the update_fields list passed to Model.save().

PatternExampleResult
Single fieldarticle.save(update_fields=["title"])[string]
Multiple fieldsarticle.save(update_fields=["title", "slug"])[string]
Variable listarticle.save(update_fields=fields)❌ out of scope — list not statically visible
Raw SQL
MethodExampleResult
.raw()Article.objects.raw("SELECT title FROM ...")[sql ref]
cursor.execute()cursor.execute("SELECT title, slug FROM ...")[sql ref]

Not detected#

ORM — uncovered methods
PatternExampleResult
update_or_create.update_or_create(defaults={"title": "x"})
Meta API
PatternExampleResult
_meta.get_fieldArticle._meta.get_field("title")
vars() subscriptvars(article)["title"]
__dict__ subscriptarticle.__dict__["title"]
Django admin
PatternExampleResult
list_displaylist_display = ["title"]
list_filterlist_filter = ["title"]
search_fieldssearch_fields = ["title"]
readonly_fieldsreadonly_fields = ["title"]
fieldsetsfieldsets = (None, {"fields": ["title"]})
orderingordering = ["title"]
Django REST Framework
PatternExampleResult
Meta.fieldsfields = ["title", "slug"]
extra_kwargsextra_kwargs = {"title": {...}}
Django forms
PatternExampleResult
ModelForm.Meta.fieldsfields = ["title"]

Rails#

Detected#

Attribute access
PatternExampleResult
Readarticle.title
ChainedArticle.find(1).title
Multi-line chainArticle.where(...).first.title
String interpolation"#{article.title}"
Safe navigationarticle&.title
Writearticle.title = value
ActiveRecord — creation
MethodExampleResult
newArticle.new(title: value)[string]
createArticle.create(title: value)[string]
find_or_create_byArticle.find_or_create_by(title: value)[string]
find_or_initialize_byArticle.find_or_initialize_by(title: value)[string]
ActiveRecord — bulk write (Rails 6+)

The field name appears as a hash key. The first positional argument may be a single hash (for insert, insert!, upsert) or an array of hashes (for the _all variants). Bang variants are treated identically to their non-bang counterparts.

MethodExampleResult
insertArticle.insert({title: "a"})[string]
insert!Article.insert!({title: "a"})[string]
insert_allArticle.insert_all([{title: "a"}, ...])[string]
insert_all!Article.insert_all!([{title: "a"}])[string]
upsertArticle.upsert({title: "a"})[string]
upsert_allArticle.upsert_all([{title: "a"}, ...])[string]
Variable argumentArticle.insert_all(records)❌ out of scope — hash keys not statically visible
ActiveRecord — instance update
MethodExampleResult
updatearticle.update(title: value)[string]
assign_attributesarticle.assign_attributes(title: value)[string]
update_column (symbol)article.update_column(:title, value)[string]
update_columns (hash)article.update_columns(title: value)[string]
ActiveRecord — query methods
MethodExampleResult
where (hash).where(title: value)[string]
where (string).where("title = ?", value)[sql ref]
where.not.where.not(title: value)[string]
find_by.find_by(title: value)[string]
exists?.exists?(title: value)[string]
order (symbol).order(:title)[string]
order (hash).order(title: :desc)[string]
order (string).order("title ASC")[sql ref]
pluck (symbol).pluck(:title)[string]
pluck (string).pluck("title")[string]
select (symbol).select(:title)[string]
select (string).select("title, slug")[sql ref]
group.group(:title)[string]
pick.pick(:title)[string]
reorder.reorder(:title)[string]
update_all.update_all(title: value)[string]
ActiveRecord — aggregation
MethodExampleResult
minimum.minimum(:title)[string]
maximum.maximum(:title)[string]
sum.sum(:title)[string]
average.average(:price)[string]
count (column form).count(:status)[string]
calculate.calculate(:sum, :price)[string] (second arg)
Arel
PatternExampleResult
Table subscriptArticle.arel_table[:title][string]
Arel conditionArticle.arel_table[:title].eq(value)[string]
Implicit selfarel_table[:title][string]
Raw SQL
MethodExampleResult
find_by_sql (string)Article.find_by_sql("SELECT title FROM articles ...")[sql ref]
find_by_sql (heredoc)Article.find_by_sql(<<~SQL) with title in body[sql ref]
executeconnection.execute("UPDATE articles SET title = ...")[sql ref]
select_allconnection.select_all("SELECT title, slug FROM articles")[sql ref]
Hash / symbol access

The field name appears as a symbol literal. These are general Ruby patterns — not Rails-specific — so the [symbol] label signals lower confidence than [string] hits from known ORM methods. Variable symbols (article.send(field_var)) remain out of scope. send and public_send require a receiver.

PatternExampleResult
Symbol subscriptarticle[:title][symbol]
read_attributearticle.read_attribute(:title)[symbol]
write_attributearticle.write_attribute(:title, value)[symbol]
sendarticle.send(:title)[symbol]
public_sendarticle.public_send(:title)[symbol]
Variable symbolarticle.send(field_var)❌ out of scope — symbol not statically visible
Model declarations (partial)

The scope declaration itself is not matched, but calls inside the scope body are scanned normally.

PatternExampleResult
scope (body)scope :titled, ->(t) { where(title: t) } — detected via where[string]

Not detected#

Model declarations
PatternExampleResult
validatesvalidates :title, presence: true
scope (declaration)scope :titled, ->(t) { ... }
Serialization / presentation
PatternExampleResult
slicearticle.slice(:title, :slug)[string]
as_json(only:)article.as_json(only: [:title])[string]
as_json(except:)article.as_json(except: [:created_at])[string]
to_json(only:)article.to_json(only: [:title])[string]
to_xml(only:)article.to_xml(only: [:title])[string]
Strong params permitparams.require(:article).permit(:title, :slug)
ActiveModel Serializer attributesattributes :title, :slug
as_json(only:) with dynamic arrayarticle.as_json(only: fields)
Dynamic / metaprogramming
PatternExampleResult
respond_to?article.respond_to?(:title)
instance_variable_getarticle.instance_variable_get(:@title)
attribute_changed?article.title_changed?
Dynamic finderArticle.find_by_title(value)