Analyzer Plugin
Ormed ships an optional analyzer plugin (no separate package) that inspects your query builder usage, DTOs, and model metadata during analysis. It helps catch unsafe patterns, invalid field names, and other common pitfalls before runtime.
Enable the plugin
- Add
ormedtodev_dependencies. - Enable the plugin in your project's
analysis_options.yaml:
analyzer:
plugins:
- ormed
After changing
analysis_options.yaml, restart the Dart Analysis Server. If you previously used the standaloneormed_analyzerpackage, remove it from yourpubspec.yamland updateanalysis_options.yamlto useormed.
How it works
- The plugin scans generated
*.orm.dartfiles to build a model index. Runbuild_runnerfirst so the definitions exist. - Query modifiers are tracked within the same function body, including split
chains and cascades:
final q = query.where('email', '=', 'a@example.com');
q.get(); // no warning because the earlier where() is tracked - The tracker is intra-procedural (no cross-function tracking) and does not model control-flow branches precisely.
- Only literal field names can be validated. Dynamic strings are ignored.
Diagnostics (grouped)
Field and selection validation:
ormed_unknown_field: unknown field/column in query builder calls.
Model.query<Post>()
.where('missing_field', true); // ormed_unknown_field
ormed_unknown_select_field: unknown column inselect([...]).
Model.query<Post>()
.select(['missing_select']); // ormed_unknown_select_field
ormed_duplicate_select_field: duplicate column inselect([...]).
Model.query<Post>()
.select(['title', 'title']); // ormed_duplicate_select_field
ormed_unknown_order_field: unknown column inorderBy(...).
Model.query<Post>()
.orderBy('missing_order'); // ormed_unknown_order_field
ormed_unknown_group_field: unknown column ingroupBy(...).
Model.query<Post>()
.groupBy(['missing_group']); // ormed_unknown_group_field
ormed_unknown_having_field: unknown column inhaving(...).
Model.query<Post>()
.having('missing_having', PredicateOperator.equals, 1); // ormed_unknown_having_field
Relation validation:
ormed_unknown_relation: unknown relation inwithRelation(...)or similar.
Model.query<Post>()
.withRelation('missingRelation'); // ormed_unknown_relation
ormed_unknown_nested_relation: unknown nested relation path.
Model.query<Post>()
.withRelation('comments.missingRelation'); // ormed_unknown_nested_relation
ormed_invalid_where_has:whereHas(...)targets a missing relation.
Model.query<Post>()
.whereHas('missingRelation'); // ormed_invalid_where_has
ormed_relation_field_mismatch: relation callback uses a field from the wrong model.
Model.query<Post>()
.whereHas('comments', (q) => q.where('email', 'oops')); // ormed_relation_field_mismatch
ormed_missing_pivot_field: missing pivot field in many-to-many definitions.
Model.query<Post>()
.withRelation(const RelationDefinition(name: 'tags', kind: RelationKind.manyToMany, targetModel: 'Tag', pivotColumns: ['missing_pivot'], pivotModel: 'PostTag')); // ormed_missing_pivot_field
Type-aware predicate checks:
ormed_type_mismatch_eq:whereEquals(...)value type mismatches the field.
Model.query<Post>()
.whereEquals('userId', 'not_an_int'); // ormed_type_mismatch_eq
ormed_where_in_type_mismatch:whereIn(...)values mismatch the field type.
Model.query<Post>()
.whereIn('userId', ['not_an_int']); // ormed_where_in_type_mismatch
ormed_where_between_type_mismatch:whereBetween(...)values mismatch the field type.
Model.query<Post>()
.whereBetween('userId', 'a', 'z'); // ormed_where_between_type_mismatch
ormed_typed_predicate_field: typed predicate field does not exist on the model.
Model.query<Post>()
.whereTyped((q) => q.legacy.eq('oops')); // ormed_typed_predicate_field
Query safety and performance:
ormed_update_delete_without_where:update()ordelete()without constraints.
Model.query<Post>()
.update({'title': 'updated'}); // ormed_update_delete_without_where
Model.query<Post>()
.delete(); // ormed_update_delete_without_where
ormed_offset_without_order:offset()withoutorderBy.
Model.query<Post>()
.offset(10); // ormed_offset_without_order
ormed_limit_without_order:limit()withoutorderBy.
Model.query<Post>()
.limit(10); // ormed_limit_without_order
ormed_get_without_limit:get(),rows(),getPartial(),Model.all(),ModelCompanion.all(), or generated companionPosts.all()used without alimit()or chunk/paginate alternative.
Model.query<Post>()
.get(); // ormed_get_without_limit
Posts.all(); // ormed_get_without_limit
Raw SQL safety:
ormed_raw_sql_interpolation: raw SQL with string interpolation and no bindings.
Model.query<Post>()
.whereRaw('title = $title'); // ormed_raw_sql_interpolation
ormed_raw_sql_alias_missing:selectRaw(...)without an alias.
Model.query<Post>()
.selectRaw('count(*)'); // ormed_raw_sql_alias_missing
DTO validation:
ormed_insert_missing_required: insert DTO missing required fields.
Model.repository<Post>()
.insert(PostInsertDto()); // ormed_insert_missing_required
ormed_update_missing_pk: update DTO missing a primary key (or missing where).
Model.repository<Post>()
.update(PostUpdateDto(title: 'updated')); // ormed_update_missing_pk
Soft delete and timestamp checks:
ormed_with_trashed_on_non_soft_delete
Model.query<User>()
.withTrashed(); // ormed_with_trashed_on_non_soft_delete
ormed_without_timestamps_on_timestamped_model
User(email: 'a@b.com', name: 'Test')
.withoutTimestamps(() {}); // ormed_without_timestamps_on_timestamped_model
ormed_updated_at_access_on_without_timestamps
Tag(id: 1, name: 'ormed')
.updatedAt; // ormed_updated_at_access_on_without_timestamps
Suppressing diagnostics
Suppress a single diagnostic with:
// ignore: ormed/ormed_unknown_field
Or suppress an entire file:
// ignore_for_file: ormed/ormed_get_without_limit
Generated code warnings
The plugin analyzes generated .orm.dart files by default (it needs them to
build the model index). If you do not want warnings in generated files, add
excludes to analysis_options.yaml:
analyzer:
exclude:
- "**/*.g.dart"
- "**/*.orm.dart"
AOT snapshot workaround
If your project uses ormed_sqlite, the analyzer plugin may fail to compile
an AOT snapshot because ormed_sqlite depends on build hooks. In that case,
run analysis with:
dart analyze --no-use-aot-snapshot
Example
final users = await context.query<User>()
.where('emali', 'a@b.com'); // flagged by the plugin