Laravel DynamoDB query safety — scan prevention and Tinker-based verification
php artisan boost:add-skill pekral/arch-app-services
Save this content to: .cursor/rules/laravel/dynamodb.mdc
# Laravel DynamoDB
## Scope
Apply these rules whenever the project integrates with DynamoDB — via the `baopham/laravel-dynamodb` package, a DynamoDB-backed Eloquent model, a custom DynamoDB query builder, or the AWS SDK called directly from PHP.
## Core Principle
- Every read must target a key. `Scan` is the slow, expensive default and must never be the access pattern for production code paths.
- Treat every change to a DynamoDB query builder, repository, or any code that issues DynamoDB requests as a place that may silently introduce a `Scan`.
- A missing partition key turns the call into a `Scan` even when a `FilterExpression` is present — filters run **after** the read and do not replace a key condition.
## Required Access Patterns
- `GetItem` / `BatchGetItem` — single-item or known-keys reads.
- `Query` with a partition key (and optionally a sort key) — collection reads inside one partition.
- `Query` against a Global Secondary Index (GSI) — non-primary-key access patterns. Pass the index name to the query (`->index('status-createdAt-index')` with `baopham/laravel-dynamodb`; `IndexName` in `QueryInput` when calling the AWS SDK directly).
- `Scan` is allowed only for explicit, documented offline/admin tasks (one-off backfills, maintenance scripts) — see the Review Checklist below for the justification requirement.
Reject patterns that omit a key:
```php
Model::all(); // full scan
Model::where('status', 'active')->get(); // scan if `status` is not the PK / GSI hash
Model::whereNotNull('email')->first(); // scan
```
Prefer patterns that target a key:
```php
Model::find($id); // GetItem
Model::where('userId', $userId)->get(); // Query on PK
Model::index('status-createdAt-index') // Query on GSI
->where('status', 'active')
->get();
```
## Review Checklist
For every PR that touches DynamoDB code, reviewers must verify:
- Each new or changed read specifies a partition key or uses `GetItem` / `BatchGetItem`.
- No new `Scan` requests were introduced.
- Any pre-existing `Scan` that is modified is re-justified in code or in the PR description.
- `FilterExpression` is not used as the primary access pattern.
- When a GSI is used, the index exists in the table definition and the query passes the correct index name.
## Testing and Debugging with Tinker
- If Laravel Tinker is available in the project (check `composer.json` for `laravel/tinker`), use `php artisan tinker` for every ad-hoc DynamoDB query verification or debug session instead of writing throwaway routes, commands, or seeders.
- Tinker complements but does not replace the Pest tests required by `@rules/code-testing/general.mdc` for every behavior change.
- Use Tinker to:
- Confirm whether a builder produces a `Query` or a `Scan` before merging — inspect the builder, dump the underlying AWS SDK request, or enable SDK logging.
- Verify the shape of items returned by repositories against real data.
- Reproduce production-like read paths against the configured DynamoDB endpoint.
- Never paste secrets or production credentials into Tinker — rely on the project's configured `.env` or IAM role.
## Red Flags
- A new method on a DynamoDB repository or query builder that has no `where()` on the partition key.
- `FilterExpression` used as the only constraint on a read.
- `Scan` calls added without a clear maintenance/offline justification.
- Ad-hoc verification scripts or temporary routes added "just to test" a query when Tinker would suffice.
Apply these rules whenever the project integrates with DynamoDB — via the baopham/laravel-dynamodb package, a DynamoDB-backed Eloquent model, a custom DynamoDB query builder, or the AWS SDK called directly from PHP.
Scan is the slow, expensive default and must never be the access pattern for production code paths.Scan.Scan even when a FilterExpression is present — filters run after the read and do not replace a key condition.GetItem / BatchGetItem — single-item or known-keys reads.Query with a partition key (and optionally a sort key) — collection reads inside one partition.Query against a Global Secondary Index (GSI) — non-primary-key access patterns. Pass the index name to the query (->index('status-createdAt-index') with baopham/laravel-dynamodb; IndexName in QueryInput when calling the AWS SDK directly).Scan is allowed only for explicit, documented offline/admin tasks (one-off backfills, maintenance scripts) — see the Review Checklist below for the justification requirement.Reject patterns that omit a key:
Model::all(); // full scan
Model::where('status', 'active')->get(); // scan if `status` is not the PK / GSI hash
Model::whereNotNull('email')->first(); // scan
Prefer patterns that target a key:
Model::find($id); // GetItem
Model::where('userId', $userId)->get(); // Query on PK
Model::index('status-createdAt-index') // Query on GSI
->where('status', 'active')
->get();
For every PR that touches DynamoDB code, reviewers must verify:
GetItem / BatchGetItem.Scan requests were introduced.Scan that is modified is re-justified in code or in the PR description.FilterExpression is not used as the primary access pattern.composer.json for laravel/tinker), use php artisan tinker for every ad-hoc DynamoDB query verification or debug session instead of writing throwaway routes, commands, or seeders.@rules/code-testing/general.mdc for every behavior change.Query or a Scan before merging — inspect the builder, dump the underlying AWS SDK request, or enable SDK logging..env or IAM role.where() on the partition key.FilterExpression used as the only constraint on a read.Scan calls added without a clear maintenance/offline justification.How can I help you explore Laravel packages today?