agentleFS
vhmarquezvhmarquez/argo/skills/soql-analyzer/SKILL.md

soql-analyzer

Skill1 starsChanged 7 months agoMITView on GitHub

Static-analyze SOQL queries in Apex source for selectivity issues — non-selective WHERE clauses, missing indexed fields, full-table scans on LDV objects, missing LIMIT. Cross-references the org cache (when present) for actual indexed fields and row volumes.

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Look up in the org cache: actual indexed fields (custom-indexed flag), row count if available

    line 79
  2. Very specific:

    Honor the org cache: If ${CACHE} lists a custom index on Status__c, don't flag WHERE Status__c = as non-selective.

    line 131

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

You are analyzing SOQL queries for selectivity. A selective query uses an indexed field with a value that returns a small fraction of total rows; non-selective queries scan large tables and trigger query timeouts on LDV (Large Data Volume) objects. This skill flags candidate problems before they become production incidents.

Read Project Config First

source "${CLAUDE_PLUGIN_ROOT}/hooks/lib/config.sh"
APEX_SRC="$(sf_config_get '.paths.apexSource' "$ENV")"
ORG="$(sf_config_get '.platform.defaultTargetOrg' "$ENV")"
CACHE="${CLAUDE_PLUGIN_DATA:-$HOME/.claude/plugin-data}/argo/org-cache/${ORG}.json"

Indexed Fields (Salesforce-managed by default)

These fields are always indexed unless the query disables the index:

  • Id
  • Foreign-key (lookup / master-detail) fields
  • Name on most standard objects
  • RecordTypeId
  • OwnerId
  • CreatedDate, LastModifiedDate, SystemModstamp
  • External Id and Unique custom fields
  • Custom fields explicitly indexed (custom indexes — visible only via the org cache, not source)

Non-indexed by default:

  • Other custom fields (Status__c, Region__c, etc.)
  • Description, Body, long text areas

Selectivity Rules

Filter patternSelective?
Id = :var or Id IN :ids✅ always
<Lookup>__c = :var✅ if cardinality is reasonable
<Indexed> = :var✅
<NonIndexed> = :var (small object, < 10K rows)✅ acceptable
<NonIndexed> = :var (LDV object, > 1M rows)❌ non-selective; needs index or different approach
LIKE '%value' (leading wildcard)❌ never uses index
LIKE 'value%' (trailing wildcard)✅ uses index if field is indexed
NOT IN, !=❌ generally non-selective
<NULL> = NULL❌ NULL doesn't index
Multi-OR across non-indexed fields❌

Input

$ARGUMENTS:

  • (empty) — analyze all .cls and .trigger files
  • <ClassName> — analyze a single class
  • --ldv-threshold <rows> — flag any object with > N rows in the org cache as LDV (default 1000000)
  • --target-org <alias> / --env <name> — for live cardinality / index lookup
  • --ci / --format json|sarif / --out <path>

Steps

1. Walk the Apex source

For each file, find SOQL statements:

\[\s*SELECT\s+(.+?)\s+FROM\s+(\w+)(?:\s+(?:USING\s+SCOPE\s+\w+)?\s*WHERE\s+(.+?))?(?:\s+ORDER\s+BY\s+(.+?))?(?:\s+LIMIT\s+(\d+))?\s*\]

Multi-line / dynamic SOQL via Database.query: capture the string literal portion; if dynamic, flag for manual review.

2. Parse each WHERE clause

For each predicate:

  • Identify the field
  • Identify the operator
  • Identify the right-hand value (literal, bind variable, dynamic)

3. Classify the query

For the FROM object:

  • Look up in the org cache: actual indexed fields (custom-indexed flag), row count if available
  • Map predicates against the indexed/non-indexed table above

Findings:

IssueSeverityRule ID
Query has no WHERE clause on a custom or sensitive standard objecterrorSOQL-NO-WHERE
WHERE filters on only non-indexed fields, FROM is custom or LDV-flagged objectwarningSOQL-NON-SELECTIVE
Leading % in LIKEwarningSOQL-LIKE-LEADING-WILDCARD
NOT IN or != as primary filterwarningSOQL-NEGATIVE-FILTER
No LIMIT and FROM is LDVwarningSOQL-NO-LIMIT-LDV
ORDER BY on non-indexed field with no LIMITwarningSOQL-ORDER-BY-NON-INDEXED
Dynamic SOQL — string concatenation builds WHEREnoteSOQL-DYNAMIC (pair with /security-scan for SOQL injection check)

4. Output

Default Markdown:

# SOQL Analysis: <project.name>

Apex files scanned: 47 | Queries found: 124
LDV threshold: 1,000,000 rows
Org cache: <CLAUDE_PLUGIN_DATA>/.../DevVM.json (cachedAt: 2026-04-28T12:30:00Z)

## Findings

### Critical (3)
- `force-app/.../OrderReportController.cls:42` — `[SELECT ... FROM Order__c]` (no WHERE) — Order__c has 2.4M rows
- ...

### High (5)
- `force-app/.../SearchService.cls:18` — `[SELECT Id FROM Account WHERE Description LIKE '%...']` — leading wildcard, full scan
- `force-app/.../UnusedJob.cls:55` — `[SELECT ... FROM Lead]` (no LIMIT) on LDV
- ...

### Medium (8)
- `force-app/.../StatusBatch.cls:30` — `[SELECT ... FROM Order__c WHERE Status__c = :s]` — Status__c not indexed; consider custom index or external archive

### Notes (2)
- `force-app/.../DynamicReportController.cls:88` — Dynamic SOQL (Database.query) — manual review needed

CI mode: SARIF emit per finding.

5. Exit codes

  • 0 — no error/warning findings
  • 1 — any finding
  • 2 — config error / no Apex source

Rules

  • Heuristic, not perfect. Apex parsing without an AST is approximate. Mark dynamic SOQL as a note and let the human inspect
  • Honor the org cache. If ${CACHE} lists a custom index on Status__c, don't flag WHERE Status__c = as non-selective. Run /argo:org-explore --refresh to update
  • Don't flag tests. *<testSuffix>.cls is excluded by default
  • Cross-link to fixes. Each finding's message should mention the appropriate fix: "add a custom index" / "filter by Id or lookup field" / "consider archiving" / "convert to a paginated report"

Consumers

  • /argo:code-review consumes the JSON output and rolls up into the review report
  • @architect and @data-architect reference the report when designing new query paths

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.