lemmaly 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +238 -0
- package/cli/gen-agents-md.js +60 -0
- package/cli/gen-rule-docs.js +885 -0
- package/cli/lemmaly.js +162 -0
- package/commands/benchmark.md +40 -0
- package/commands/budget.md +53 -0
- package/commands/complexity.md +26 -0
- package/commands/cut.md +27 -0
- package/commands/hotpath.md +22 -0
- package/commands/invariant.md +22 -0
- package/commands/n-plus-one.md +20 -0
- package/commands/profile.md +34 -0
- package/commands/regress.md +43 -0
- package/commands/scale-check.md +37 -0
- package/commands/ship-check.md +26 -0
- package/package.json +48 -0
- package/rules/cpp.json +46 -0
- package/rules/csharp.json +38 -0
- package/rules/go.json +46 -0
- package/rules/java.json +38 -0
- package/rules/javascript.json +102 -0
- package/rules/php.json +38 -0
- package/rules/python.json +62 -0
- package/rules/ruby.json +38 -0
- package/rules/rust.json +38 -0
- package/rules/shell.json +38 -0
- package/rules/sql.json +54 -0
- package/skills/complexity-cuts/SKILL.md +259 -0
- package/skills/invariant-guard/SKILL.md +310 -0
- package/skills/lemmaly/AGENTS.md +1869 -0
- package/skills/lemmaly/SKILL.md +365 -0
- package/skills/lemmaly/references/async.md +135 -0
- package/skills/lemmaly/references/complexity.md +66 -0
- package/skills/lemmaly/references/hot-paths.md +87 -0
- package/skills/lemmaly/references/memory.md +118 -0
- package/skills/lemmaly/references/n-plus-one.md +139 -0
- package/skills/lemmaly/rules/cpp-map-double-lookup.md +38 -0
- package/skills/lemmaly/rules/cpp-range-loop-copy.md +33 -0
- package/skills/lemmaly/rules/cpp-raw-new.md +36 -0
- package/skills/lemmaly/rules/cpp-string-concat-in-loop.md +45 -0
- package/skills/lemmaly/rules/cpp-vector-push-no-reserve.md +40 -0
- package/skills/lemmaly/rules/cs-async-void.md +45 -0
- package/skills/lemmaly/rules/cs-disposable-no-using.md +32 -0
- package/skills/lemmaly/rules/cs-list-contains-in-loop.md +36 -0
- package/skills/lemmaly/rules/cs-string-concat-in-loop.md +42 -0
- package/skills/lemmaly/rules/go-defer-in-loop.md +39 -0
- package/skills/lemmaly/rules/go-err-not-checked.md +38 -0
- package/skills/lemmaly/rules/go-loop-var-capture.md +47 -0
- package/skills/lemmaly/rules/go-slice-append-no-cap.md +39 -0
- package/skills/lemmaly/rules/go-string-concat-in-loop.md +44 -0
- package/skills/lemmaly/rules/java-arraylist-remove-in-for-i.md +44 -0
- package/skills/lemmaly/rules/java-bare-catch-exception.md +42 -0
- package/skills/lemmaly/rules/java-list-contains-in-loop.md +40 -0
- package/skills/lemmaly/rules/java-string-concat-in-loop.md +42 -0
- package/skills/lemmaly/rules/js-anonymous-handler-jsx.md +31 -0
- package/skills/lemmaly/rules/js-array-key-index.md +29 -0
- package/skills/lemmaly/rules/js-async-in-foreach.md +43 -0
- package/skills/lemmaly/rules/js-await-in-for-loop.md +41 -0
- package/skills/lemmaly/rules/js-deep-clone-via-json.md +33 -0
- package/skills/lemmaly/rules/js-helper-call-in-iterator.md +41 -0
- package/skills/lemmaly/rules/js-includes-in-iterator.md +37 -0
- package/skills/lemmaly/rules/js-inline-object-jsx-prop.md +35 -0
- package/skills/lemmaly/rules/js-nested-for-loops.md +45 -0
- package/skills/lemmaly/rules/js-spread-in-reduce.md +38 -0
- package/skills/lemmaly/rules/js-unique-via-indexof.md +35 -0
- package/skills/lemmaly/rules/js-useeffect-missing-deps.md +33 -0
- package/skills/lemmaly/rules/php-count-in-for-condition.md +45 -0
- package/skills/lemmaly/rules/php-in-array-in-loop.md +42 -0
- package/skills/lemmaly/rules/php-loose-equality.md +35 -0
- package/skills/lemmaly/rules/php-query-in-loop.md +47 -0
- package/skills/lemmaly/rules/py-bare-except.md +39 -0
- package/skills/lemmaly/rules/py-django-loop-without-eager.md +42 -0
- package/skills/lemmaly/rules/py-in-list-literal.md +37 -0
- package/skills/lemmaly/rules/py-mutable-default-arg.md +39 -0
- package/skills/lemmaly/rules/py-open-without-with.md +33 -0
- package/skills/lemmaly/rules/py-range-len.md +35 -0
- package/skills/lemmaly/rules/py-string-concat-in-loop.md +43 -0
- package/skills/lemmaly/rules/rb-bare-rescue.md +41 -0
- package/skills/lemmaly/rules/rb-include-in-iterator.md +37 -0
- package/skills/lemmaly/rules/rb-n-plus-one-activerecord.md +39 -0
- package/skills/lemmaly/rules/rb-string-concat-in-loop.md +39 -0
- package/skills/lemmaly/rules/rs-clone-in-loop.md +38 -0
- package/skills/lemmaly/rules/rs-string-push-no-capacity.md +43 -0
- package/skills/lemmaly/rules/rs-unwrap-in-prod.md +36 -0
- package/skills/lemmaly/rules/rs-vec-push-no-capacity.md +42 -0
- package/skills/lemmaly/rules/sh-for-ls.md +41 -0
- package/skills/lemmaly/rules/sh-set-e-no-pipefail.md +37 -0
- package/skills/lemmaly/rules/sh-unquoted-var.md +35 -0
- package/skills/lemmaly/rules/sh-useless-cat-pipe.md +32 -0
- package/skills/lemmaly/rules/sql-leading-wildcard-like.md +34 -0
- package/skills/lemmaly/rules/sql-not-in-subquery.md +38 -0
- package/skills/lemmaly/rules/sql-or-in-where.md +35 -0
- package/skills/lemmaly/rules/sql-select-no-limit.md +37 -0
- package/skills/lemmaly/rules/sql-select-star.md +29 -0
- package/skills/lemmaly/rules/sql-update-no-where.md +35 -0
- package/skills/mathguard/SKILL.md +277 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: rs-clone-in-loop
|
|
3
|
+
title: .clone() inside iterator — avoid if a borrow works
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: rust
|
|
8
|
+
tags: rust, info, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# .clone() inside iterator — avoid if a borrow works
|
|
12
|
+
|
|
13
|
+
A `.clone()` inside an iterator allocates a fresh copy per element. If a borrow (`&x`) or `Rc::clone` (cheap atomic increment for shared ownership) would do, the deep clone is wasted work.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Borrow with &, use Rc/Arc for shared ownership, or move once outside the loop.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```rust
|
|
22
|
+
// Deep clones every element
|
|
23
|
+
let names: Vec<String> = users.iter().map(|u| u.name.clone()).collect();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```rust
|
|
29
|
+
// Borrow when possible
|
|
30
|
+
let names: Vec<&str> = users.iter().map(|u| u.name.as_str()).collect();
|
|
31
|
+
|
|
32
|
+
// Cheap reference-counted clone when shared ownership is needed
|
|
33
|
+
let shared: Vec<Rc<String>> = users.iter().map(|u| Rc::clone(&u.name)).collect();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Escalate to
|
|
37
|
+
|
|
38
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: rs-string-push-no-capacity
|
|
3
|
+
title: String::new() + push_str in loop — repeated reallocation
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: rust
|
|
8
|
+
tags: rust, info, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# String::new() + push_str in loop — repeated reallocation
|
|
12
|
+
|
|
13
|
+
`String::new()` starts at capacity 0. Each `push_str` past capacity reallocates. `with_capacity` or `join` avoids it.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Preallocate: `String::with_capacity(n)`, or `parts.join(sep)` for known parts.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```rust
|
|
22
|
+
let mut s = String::new();
|
|
23
|
+
for part in parts.iter() {
|
|
24
|
+
s.push_str(part); // reallocates as it grows
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Correct
|
|
29
|
+
|
|
30
|
+
```rust
|
|
31
|
+
let total: usize = parts.iter().map(|p| p.len()).sum();
|
|
32
|
+
let mut s = String::with_capacity(total);
|
|
33
|
+
for part in parts.iter() {
|
|
34
|
+
s.push_str(part);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Or, simplest:
|
|
38
|
+
let s = parts.join("");
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Escalate to
|
|
42
|
+
|
|
43
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: rs-unwrap-in-prod
|
|
3
|
+
title: .unwrap() / .expect() panics on None/Err — surface the error
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: rust
|
|
8
|
+
tags: rust, warning, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# .unwrap() / .expect() panics on None/Err — surface the error
|
|
12
|
+
|
|
13
|
+
`.unwrap()` and `.expect()` panic on `None`/`Err`. In production code they crash the process and lose the structured error. Rust gives you `?`, `match`, and `ok_or` to surface the error to the caller.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use `?`, `match`, `unwrap_or`, `unwrap_or_else`, or `ok_or(...)?`. Reserve unwrap for tests and provably infallible paths.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```rust
|
|
22
|
+
let value = map.get(&key).unwrap(); // panics if key missing
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Correct
|
|
26
|
+
|
|
27
|
+
```rust
|
|
28
|
+
let value = map.get(&key).ok_or(Error::MissingKey)?;
|
|
29
|
+
|
|
30
|
+
// Or, when None has a meaningful default:
|
|
31
|
+
let value = map.get(&key).copied().unwrap_or_default();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Escalate to
|
|
35
|
+
|
|
36
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: rs-vec-push-no-capacity
|
|
3
|
+
title: Vec::new() + push in loop — repeated reallocation
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: rust
|
|
8
|
+
tags: rust, info, complexity-cuts
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Vec::new() + push in loop — repeated reallocation
|
|
12
|
+
|
|
13
|
+
`Vec::new()` starts with capacity 0; each `push` past the current capacity reallocates and copies. Preallocating with `Vec::with_capacity(n)` does one allocation.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Preallocate: `Vec::with_capacity(n)`, or use `.collect::<Vec<_>>()` over an iterator that has a size hint.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```rust
|
|
22
|
+
let mut out = Vec::new();
|
|
23
|
+
for x in input.iter() {
|
|
24
|
+
out.push(transform(x)); // grows; reallocates log(n) times
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Correct
|
|
29
|
+
|
|
30
|
+
```rust
|
|
31
|
+
let mut out = Vec::with_capacity(input.len());
|
|
32
|
+
for x in input.iter() {
|
|
33
|
+
out.push(transform(x));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Or, when transform is pure:
|
|
37
|
+
let out: Vec<_> = input.iter().map(transform).collect();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Escalate to
|
|
41
|
+
|
|
42
|
+
If this pattern is widespread in the codebase, load **complexity-cuts** for the corrective workflow.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sh-for-ls
|
|
3
|
+
title: for f in $(ls ...) — breaks on filenames with spaces / newlines
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: shell
|
|
8
|
+
tags: shell, warning, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# for f in $(ls ...) — breaks on filenames with spaces / newlines
|
|
12
|
+
|
|
13
|
+
`for f in $(ls ...)` breaks on filenames with spaces, tabs, newlines, or globs. The output of `ls` is meant for humans, not programs. Use a glob or `find -print0 | xargs -0`.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use glob `for f in *.txt` or `find ... -print0 | xargs -0`. Never parse `ls`.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```shell
|
|
22
|
+
for f in $(ls *.txt); do
|
|
23
|
+
process "$f" # breaks on "my file.txt"
|
|
24
|
+
done
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Correct
|
|
28
|
+
|
|
29
|
+
```shell
|
|
30
|
+
# Glob directly
|
|
31
|
+
for f in *.txt; do
|
|
32
|
+
process "$f"
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
# Or, when find is necessary
|
|
36
|
+
find . -name '*.txt' -print0 | xargs -0 -n1 process
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Escalate to
|
|
40
|
+
|
|
41
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sh-set-e-no-pipefail
|
|
3
|
+
title: set -e without set -o pipefail — failures inside pipes are masked
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: shell
|
|
8
|
+
tags: shell, warning, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# set -e without set -o pipefail — failures inside pipes are masked
|
|
12
|
+
|
|
13
|
+
`set -e` exits on a failed command, but a failure in the middle of a pipe is masked — only the *last* command's exit status counts. `set -o pipefail` fixes it. `set -u` catches unset variables.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use `set -euo pipefail` so the script also fails when an earlier stage in a pipe fails.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```shell
|
|
22
|
+
#!/bin/bash
|
|
23
|
+
set -e
|
|
24
|
+
some_failing_step | grep needle # if some_failing_step fails, script keeps going
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Correct
|
|
28
|
+
|
|
29
|
+
```shell
|
|
30
|
+
#!/bin/bash
|
|
31
|
+
set -euo pipefail
|
|
32
|
+
some_failing_step | grep needle # any failure in the pipe aborts the script
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Escalate to
|
|
36
|
+
|
|
37
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sh-unquoted-var
|
|
3
|
+
title: Unquoted $var — word splitting and glob expansion
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: shell
|
|
8
|
+
tags: shell, warning, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Unquoted $var — word splitting and glob expansion
|
|
12
|
+
|
|
13
|
+
An unquoted `$var` is subject to word splitting (on `$IFS`) and glob expansion. A path with a space, a tab, or a `*` will silently do the wrong thing.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Quote: `"$var"`. Use `"${arr[@]}"` for arrays. Required even for paths you trust.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```shell
|
|
22
|
+
if [ -d $dir ]; then echo yes; fi
|
|
23
|
+
# If $dir = "/tmp/with space", expands to: [ -d /tmp/with space ] — syntax error
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```shell
|
|
29
|
+
if [ -d "$dir" ]; then echo yes; fi
|
|
30
|
+
# Always quote. For arrays: "${arr[@]}".
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Escalate to
|
|
34
|
+
|
|
35
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sh-useless-cat-pipe
|
|
3
|
+
title: cat file | cmd — useless use of cat (UUOC)
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: shell
|
|
8
|
+
tags: shell, info
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# cat file | cmd — useless use of cat (UUOC)
|
|
12
|
+
|
|
13
|
+
`cat file | cmd` reads the file and pipes through `cat` just to feed `cmd`. Every command that takes a file argument can read it directly — one fewer process, clearer intent.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Run the command on the file directly: `grep ... file` instead of `cat file | grep ...`.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```shell
|
|
22
|
+
cat access.log | grep "500"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Correct
|
|
26
|
+
|
|
27
|
+
```shell
|
|
28
|
+
grep "500" access.log
|
|
29
|
+
|
|
30
|
+
# Or stdin redirection if the command takes only stdin:
|
|
31
|
+
cmd < access.log
|
|
32
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sql-leading-wildcard-like
|
|
3
|
+
title: LIKE with leading wildcard — cannot use a B-tree index
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: sql
|
|
8
|
+
tags: sql, warning
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# LIKE with leading wildcard — cannot use a B-tree index
|
|
12
|
+
|
|
13
|
+
A B-tree index sorts by prefix. `LIKE '%foo'` cannot use it — the query scans the table. Use a trigram index (Postgres `pg_trgm`), reverse the column for suffix search, or a full-text search index.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Use a trigram index (pg_trgm), full-text search, or a reversed-column index.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```sql
|
|
22
|
+
SELECT * FROM products WHERE name LIKE '%phone';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Correct
|
|
26
|
+
|
|
27
|
+
```sql
|
|
28
|
+
-- Postgres: GIN index on trigrams
|
|
29
|
+
CREATE INDEX products_name_trgm ON products USING gin (name gin_trgm_ops);
|
|
30
|
+
SELECT * FROM products WHERE name ILIKE '%phone%';
|
|
31
|
+
|
|
32
|
+
-- Or full-text:
|
|
33
|
+
SELECT * FROM products WHERE to_tsvector(name) @@ to_tsquery('phone');
|
|
34
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sql-not-in-subquery
|
|
3
|
+
title: NOT IN (subquery) — null-unsafe; usually slower than NOT EXISTS
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: sql
|
|
8
|
+
tags: sql, warning, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# NOT IN (subquery) — null-unsafe; usually slower than NOT EXISTS
|
|
12
|
+
|
|
13
|
+
`NOT IN (subquery)` is null-unsafe: if any row in the subquery is NULL, the whole predicate is NULL (not TRUE), and the outer row is dropped. `NOT EXISTS` is null-safe and usually has a better plan.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Rewrite as `WHERE NOT EXISTS (SELECT 1 FROM ... WHERE ...)`.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```sql
|
|
22
|
+
SELECT * FROM orders
|
|
23
|
+
WHERE user_id NOT IN (SELECT id FROM banned_users);
|
|
24
|
+
-- One NULL in banned_users.id → returns zero rows
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Correct
|
|
28
|
+
|
|
29
|
+
```sql
|
|
30
|
+
SELECT o.* FROM orders o
|
|
31
|
+
WHERE NOT EXISTS (
|
|
32
|
+
SELECT 1 FROM banned_users b WHERE b.id = o.user_id
|
|
33
|
+
);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Escalate to
|
|
37
|
+
|
|
38
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sql-or-in-where
|
|
3
|
+
title: OR in WHERE — can prevent index use
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: sql
|
|
8
|
+
tags: sql, info
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# OR in WHERE — can prevent index use
|
|
12
|
+
|
|
13
|
+
A query planner can use an index on one side of an `OR` but often not both, falling back to a sequential scan. `UNION ALL` or `IN (...)` (when both sides are equality on the same column) usually wins.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Equality on same column → `WHERE col IN (...)`. Otherwise consider UNION ALL.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```sql
|
|
22
|
+
SELECT * FROM events
|
|
23
|
+
WHERE user_id = $1 OR account_id = $1;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```sql
|
|
29
|
+
SELECT * FROM events WHERE user_id = $1
|
|
30
|
+
UNION ALL
|
|
31
|
+
SELECT * FROM events WHERE account_id = $1;
|
|
32
|
+
|
|
33
|
+
-- Or, when both sides are the same column:
|
|
34
|
+
SELECT * FROM events WHERE user_id IN ($1, $2, $3);
|
|
35
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sql-select-no-limit
|
|
3
|
+
title: SELECT without LIMIT — unbounded result set
|
|
4
|
+
severity: info
|
|
5
|
+
impact: MEDIUM
|
|
6
|
+
impactDescription: Suboptimal; flag when n is large or on a hot path
|
|
7
|
+
language: sql
|
|
8
|
+
tags: sql, info
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# SELECT without LIMIT — unbounded result set
|
|
12
|
+
|
|
13
|
+
A query with no `LIMIT` returns however many rows match. On a small table this is fine; on a growing one it eventually OOMs the client or the page. Add a bound, or paginate.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Add LIMIT, or paginate by id range. Unbounded reads OOM under growth.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```sql
|
|
22
|
+
SELECT id, email FROM users ORDER BY created_at DESC;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Correct
|
|
26
|
+
|
|
27
|
+
```sql
|
|
28
|
+
SELECT id, email FROM users
|
|
29
|
+
ORDER BY created_at DESC
|
|
30
|
+
LIMIT 100;
|
|
31
|
+
|
|
32
|
+
-- Keyset pagination for next page:
|
|
33
|
+
SELECT id, email FROM users
|
|
34
|
+
WHERE created_at < $1
|
|
35
|
+
ORDER BY created_at DESC
|
|
36
|
+
LIMIT 100;
|
|
37
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sql-select-star
|
|
3
|
+
title: SELECT * — fetches unused columns; blocks index-only scans
|
|
4
|
+
severity: warning
|
|
5
|
+
impact: HIGH
|
|
6
|
+
impactDescription: Hot-path or correctness risk at realistic n
|
|
7
|
+
language: sql
|
|
8
|
+
tags: sql, warning
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# SELECT * — fetches unused columns; blocks index-only scans
|
|
12
|
+
|
|
13
|
+
`SELECT *` fetches every column, defeating index-only scans, inflating wire traffic, and breaking downstream code when a column is added/renamed. Project only what you need.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Name the columns. Smaller payload and more covering-index opportunities.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```sql
|
|
22
|
+
SELECT * FROM users WHERE id = $1;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Correct
|
|
26
|
+
|
|
27
|
+
```sql
|
|
28
|
+
SELECT id, email, created_at FROM users WHERE id = $1;
|
|
29
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sql-update-no-where
|
|
3
|
+
title: UPDATE / DELETE without WHERE — touches every row
|
|
4
|
+
severity: error
|
|
5
|
+
impact: CRITICAL
|
|
6
|
+
impactDescription: Will break or scale-fail in production
|
|
7
|
+
language: sql
|
|
8
|
+
tags: sql, error, invariant-guard
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# UPDATE / DELETE without WHERE — touches every row
|
|
12
|
+
|
|
13
|
+
An `UPDATE` or `DELETE` without a `WHERE` clause rewrites every row in the table. In production this is an incident, not a bug.
|
|
14
|
+
|
|
15
|
+
## Fix
|
|
16
|
+
|
|
17
|
+
Add a WHERE clause, or confirm in a comment that touching all rows is intentional.
|
|
18
|
+
|
|
19
|
+
## Incorrect
|
|
20
|
+
|
|
21
|
+
```sql
|
|
22
|
+
UPDATE users SET active = false;
|
|
23
|
+
DELETE FROM sessions;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Correct
|
|
27
|
+
|
|
28
|
+
```sql
|
|
29
|
+
UPDATE users SET active = false WHERE last_seen_at < NOW() - INTERVAL '90 days';
|
|
30
|
+
DELETE FROM sessions WHERE expires_at < NOW();
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Escalate to
|
|
34
|
+
|
|
35
|
+
If this pattern is widespread in the codebase, load **invariant-guard** for the corrective workflow.
|