Repository navigation
docs: document safety, lint and policy rules for check #1595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -42,17 +42,17 @@ altimate-code check --fail-on warning | |||||
|
|
||||||
| | Check | What It Does | Needs Schema? | Default | | ||||||
| |------------|--------------------------------------------------------------|---------------|---------| | ||||||
| | `lint` | Anti-pattern detection (SELECT *, cartesian JOINs, etc.) | Optional | Yes | | ||||||
| | `lint` | Anti-pattern detection (SELECT *, cartesian JOINs, etc.) — [rules](#lint-rules) | Optional | Yes | | ||||||
| | `validate` | DataFusion SQL validation (column exists, type matches) | Yes | No | | ||||||
| | `safety` | SQL injection and dangerous pattern detection | No | Yes | | ||||||
| | `policy` | Custom guardrails (block SELECT *, require LIMIT, etc.) | Optional | No | | ||||||
| | `safety` | SQL injection and dangerous pattern detection — [rules](#safety-rules) | No | Yes | | ||||||
| | `policy` | Custom guardrails (block SELECT *, require LIMIT, etc.) — [rules](#policy-rules) | Optional | No | | ||||||
| | `pii` | PII column detection and query exposure | Optional | No | | ||||||
| | `semantic` | Semantic validation (cartesian products, wrong JOINs) | Optional | No | | ||||||
| | `grade` | SQL quality grading with recommendations | Optional | No | | ||||||
|
|
||||||
| `policy` and `semantic` never report a statement the engine cannot plan as clean. Such statements include table-valued functions (`TABLE(fn())`, Snowflake `LATERAL FLATTEN`, DuckDB `read_csv(...)`). `policy` fails closed: it reports a `policy_not_evaluated` error and treats the statement as blocked. `semantic` reports the skip as a finding: an error when `--schema` is given, and a warning without it, which does not fail a run with `--fail-on error`. Earlier versions rejected these statements as invalid SQL. | ||||||
|
|
||||||
| Findings usually carry a machine-readable `rule` field (e.g. `unbalanced_quote`, `sql_injection`, `L003`) visible in `--format json` output — see the [Finding Object](#finding-object) schema below. `safety` and `policy` findings always include one; `lint` findings may omit both `rule` and `code` if the engine didn't attach them, so switch on presence rather than assuming. | ||||||
| Findings usually carry a machine-readable `rule` field (e.g. `unbalanced_quote`, `select_star`) visible in `--format json` output — see the [Finding Object](#finding-object) schema below and the [Rule Catalog](#rule-catalog). `safety` and `policy` findings always include one; `lint` findings also carry the engine's `code` (e.g. `L001`), but may omit both `rule` and `code` if the engine didn't attach them, so switch on presence rather than assuming. | ||||||
|
|
||||||
| By default, `lint` and `safety` are enabled. Override with `--checks`: | ||||||
|
|
||||||
|
|
@@ -97,11 +97,11 @@ When using `--format json`, the command writes structured JSON to stdout (diagno | |||||
| { | ||||||
| "file": "models/staging/stg_orders.sql", | ||||||
| "line": 5, | ||||||
| "column": 1, | ||||||
| "rule": "L003", | ||||||
| "code": "L001", | ||||||
| "rule": "select_star", | ||||||
| "severity": "warning", | ||||||
| "message": "SELECT * used -- enumerate columns explicitly", | ||||||
| "suggestion": "Replace SELECT * with explicit column list" | ||||||
| "message": "SELECT * is used — consider listing columns explicitly", | ||||||
| "suggestion": "Replace * with specific column names for clarity and performance" | ||||||
| } | ||||||
| ], | ||||||
| "error_count": 0, | ||||||
|
|
@@ -161,8 +161,8 @@ The default `text` format is designed for human consumption: | |||||
| Checked 3 file(s) with [lint, safety] | ||||||
|
|
||||||
| --- LINT --- | ||||||
| WARNING models/staging/stg_orders.sql:5:1 [L003]: SELECT * used -- enumerate columns explicitly | ||||||
| suggestion: Replace SELECT * with explicit column list | ||||||
| WARNING models/staging/stg_orders.sql:5 [select_star]: SELECT * is used — consider listing columns explicitly | ||||||
| suggestion: Replace * with specific column names for clarity and performance | ||||||
|
|
||||||
| 1 finding(s): 0 error(s), 1 warning(s), 0 info | ||||||
| PASS | ||||||
|
|
@@ -283,6 +283,103 @@ altimate-code check --severity info --fail-on warning | |||||
|
|
||||||
| --- | ||||||
|
|
||||||
| ## Rule Catalog | ||||||
|
|
||||||
| The engine does not publish a rule list. The rules below are the ones `check` actually reported when run against a probe corpus with the `@altimateai/altimate-core` version pinned in `packages/opencode/package.json` (0.9.0 when this list was made). Rules change when the engine is upgraded, and the engine may emit rules not listed here. To see what fires on your SQL, run `--format json` and read each finding's `rule` field (and `code` for lint). | ||||||
|
|
||||||
| `check` itself adds a `<check>-error` rule (for example `safety-error`) when an engine call throws. | ||||||
|
|
||||||
| ### Safety Rules | ||||||
|
|
||||||
| `check --checks safety` maps engine severities to `check` severities: `critical` and `high` become `error`, `medium` becomes `warning`, `low` becomes `info`. Safety findings carry no `code`. | ||||||
|
|
||||||
| | Rule | What it flags | Example | Severity | | ||||||
| |------|---------------|---------|----------| | ||||||
| | `multi_statement` | A semicolon followed by a dangerous statement, or a disallowed statement type (`DROP`, `BEGIN`, `COMMIT`, …) | `SELECT 1; DROP TABLE users` | error | | ||||||
| | `stacked_queries` | More than one statement in a single input | `SELECT 1; SELECT 2` | error | | ||||||
| | `union_injection` | `UNION SELECT`, including comment-obfuscated `UNION/**/SELECT` | `SELECT name FROM users WHERE id = 1 UNION SELECT password FROM admins` | error | | ||||||
| | `tautology_attack` | Always-true predicates: `OR 1=1`, `OR 'a'='a'`, `OR TRUE`, `OR NOT FALSE` | `SELECT * FROM users WHERE name = 'a' OR 1=1` | error | | ||||||
| | `unbalanced_quote` | An unclosed single-quoted string (possible injection breakout) | `SELECT * FROM users WHERE name = 'abc` | error | | ||||||
| | `comment_injection` | A string literal followed by `--`, `#` or `/* */`, or the `';--` truncation pattern | `SELECT * FROM users WHERE name = 'a' -- x` | error | | ||||||
| | `info_schema_probe` | Reads from `information_schema` or Snowflake system functions | `SELECT * FROM information_schema.tables` | error | | ||||||
| | `transaction_escape` | A transaction control statement | `COMMIT; SELECT 1` | error | | ||||||
| | `string_concat_predicate` | `CONCAT()`, `\|\|` or `+` string concatenation in a `WHERE` clause | `SELECT * FROM users WHERE name = CONCAT('a', 'b')` | warning | | ||||||
| | `sleep_attack` | Time-based blind injection (`SLEEP(...)`) | `SELECT * FROM users WHERE id = 1 AND SLEEP(5)` | error | | ||||||
| | `encoding_bypass` | `CHAR()` encoding, URL-encoded payloads, Jinja statement tags in SQL, hex literals | `SELECT * FROM users WHERE name = CHAR(97,100,109,105,110)` | error (hex literal `0x…`: warning) | | ||||||
|
|
||||||
| ### Lint Rules | ||||||
|
|
||||||
| Lint findings carry both a `code` and a `rule`. Almost every `SELECT` without a `LIMIT` also reports `L030`, so the examples below list only the rule they demonstrate. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This says lint findings always include both fields, contradicting the earlier note that either may be absent; state that both are normally present but optional. Prompt for AI agents
Suggested change
|
||||||
|
|
||||||
| | Code | Rule | What it flags | Example | Severity | | ||||||
| |------|------|---------------|---------|----------| | ||||||
| | `L001` | `select_star` | `SELECT *` | `SELECT * FROM orders` | warning | | ||||||
| | `L002` | `missing_where_update` | `UPDATE` without `WHERE` | `UPDATE orders SET status = 'x'` | error | | ||||||
| | `L003` | `missing_where_delete` | `DELETE` without `WHERE` | `DELETE FROM orders` | error | | ||||||
| | `L004` | `cartesian_product` | Several tables in `FROM` with no `WHERE` relating them | `SELECT a.id, b.id FROM a, b` | warning | | ||||||
| | `L005` | `select_distinct_smell` | `SELECT DISTINCT` with joins (may hide a bad join) | `SELECT DISTINCT o.id FROM orders o JOIN customers c ON o.customer_id = c.id` | info | | ||||||
| | `L006` | `order_by_in_subquery` | `ORDER BY` inside a subquery | `SELECT id FROM (SELECT id FROM orders ORDER BY id) t` | warning | | ||||||
| | `L008` | `like_leading_wildcard` | `LIKE` pattern starting with `%` | `SELECT id FROM orders WHERE name LIKE '%abc'` | warning | | ||||||
| | `L009` | `not_null_comparison` | `= NULL` or `!= NULL` | `SELECT id FROM orders WHERE status = NULL` | error | | ||||||
| | `L010` | `count_star_vs_column` | `COUNT(column)`, which skips NULLs | `SELECT count(id) FROM orders` | info | | ||||||
| | `L011` | `redundant_distinct_with_group_by` | `DISTINCT` together with `GROUP BY` | `SELECT DISTINCT id FROM orders GROUP BY id` | info | | ||||||
| | `L012` | `missing_table_alias` | Multi-table query without table aliases | `SELECT a.id, b.id FROM a, b` | info | | ||||||
| | `L013` | `correlated_subquery` | Correlated subquery (N+1 pattern) | `SELECT id FROM orders WHERE NOT EXISTS (SELECT 1 FROM x WHERE x.id = orders.id)` | warning | | ||||||
| | `L014` | `non_sargable_predicate` | Column wrapped in a function in a predicate | `SELECT id FROM orders WHERE upper(name) = 'X'` | warning | | ||||||
| | `L015` | `not_in_nullable` | `NOT IN (subquery)`, which returns nothing if the subquery yields a NULL | `SELECT id FROM orders WHERE name NOT IN (SELECT name FROM x)` | error | | ||||||
| | `L016` | `unused_cte` | CTE defined but never referenced | `WITH a AS (SELECT 1 AS x) SELECT 2` | info | | ||||||
| | `L017` | `union_without_all` | `UNION` without `ALL` (forces a dedup sort) | `SELECT id FROM orders UNION SELECT id FROM other` | warning | | ||||||
| | `L018` | `implicit_cross_join` | Comma-separated tables in `FROM` | `SELECT a.id, b.id FROM a, b` | warning | | ||||||
| | `L019` | `select_star_in_subquery` | `SELECT *` inside a subquery | `SELECT id FROM orders o WHERE EXISTS (SELECT * FROM x)` | warning | | ||||||
| | `L020` | `or_in_join` | `OR` in a `JOIN ... ON` condition | `SELECT o.id FROM orders o JOIN customers c ON o.customer_id = c.id OR o.id = c.id` | warning | | ||||||
| | `L021` | `order_by_without_limit_subquery` | `ORDER BY` in a subquery that has no `LIMIT` | `SELECT id FROM (SELECT id FROM orders ORDER BY id) t` | warning | | ||||||
| | `L022` | `large_in_list` | `IN` list with more than 100 values | `SELECT id FROM orders WHERE id IN (1, 2, …, 101)` | warning | | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: This L022 example is not executable SQL because Prompt for AI agents |
||||||
| | `L024` | `non_equi_join` | Join on a non-equality condition | `SELECT o.id FROM orders o JOIN customers c ON o.customer_id > c.id` | warning | | ||||||
| | `L025` | `window_without_partition` | `ROW_NUMBER()` and similar window functions without `PARTITION BY` | `SELECT row_number() OVER () FROM t` | info | | ||||||
| | `L027` | `function-on-filter-column` | Function applied to a column in `WHERE` | `SELECT id FROM orders WHERE upper(name) = 'X'` | warning | | ||||||
| | `L028` | `function-on-join-column` | Function applied to a column in a join condition | `SELECT o.id FROM orders o JOIN customers c ON upper(o.name) = c.name` | warning | | ||||||
| | `L029` | `aggregation-before-join` | Aggregation in a subquery that is then joined | `SELECT o.id FROM orders o JOIN (SELECT customer_id, sum(amount) s FROM orders GROUP BY customer_id) a ON a.customer_id = o.customer_id` | error | | ||||||
| | `L030` | `select-without-limit` | `SELECT` without `LIMIT` | `SELECT id FROM orders` | warning | | ||||||
| | `L031` | `create-or-replace-table` | `CREATE OR REPLACE TABLE` (drops and recreates) | `CREATE OR REPLACE TABLE x AS SELECT id FROM orders` | info | | ||||||
| | `L032` | `division_by_column_no_guard` | Division by a column with no zero guard | `SELECT amount / qty FROM orders` | warning | | ||||||
| | `L033` | `non_portable_function` | Dialect-specific function (`NVL`, `IFF`, …) | `SELECT nvl(a, 0) FROM t` | info | | ||||||
| | `L035` | `non_portable_type` | Dialect-specific type (`VARIANT`, …) | `SELECT id::VARIANT FROM orders` | info | | ||||||
| | `L038` | `timezone_in_hash_key` | Row-key hash over a timezone-dependent value | `SELECT md5(convert_timezone('UTC', created_at)::varchar) FROM orders` | warning | | ||||||
| | `L040` | `monetary_float_cast` | Money-like column cast to `FLOAT`/`DOUBLE`/`REAL` | `SELECT CAST(amount AS FLOAT) FROM orders` | warning | | ||||||
| | `L042` | `case_sensitive_like` | `LIKE`, which is case-sensitive on most warehouses | `SELECT id FROM orders WHERE name LIKE 'abc%'` | info | | ||||||
| | `L043` | `outer_join_filter_in_where` | `WHERE` predicate on the outer side of a `LEFT`/`RIGHT` join (turns it into an inner join) | `SELECT o.id FROM orders o LEFT JOIN customers c ON o.cid = c.id WHERE c.name = 'x'` | warning | | ||||||
| | `L044` | `null_propagating_concat` | `\|\|` concatenation, which is NULL if any operand is NULL | `SELECT a \|\| b FROM t` | warning | | ||||||
| | `L045` | `greatest_least_null` | `GREATEST`/`LEAST` over nullable columns | `SELECT greatest(qty, price) FROM orders` | warning | | ||||||
| | `L046` | `distinct_with_window` | `SELECT DISTINCT` together with a window function | `SELECT DISTINCT id, row_number() OVER (PARTITION BY id ORDER BY id) FROM orders` | warning | | ||||||
| | `L048` | `cast_division_to_int` | Division result cast to an integer (truncates) | `SELECT CAST(qty / price AS INTEGER) FROM orders` | warning | | ||||||
| | `L049` | `clock_in_filter` | `current_date`/`now()` in a `WHERE` or join predicate | `SELECT id FROM orders WHERE created_at > current_date` | warning | | ||||||
| | `L051` | `full_outer_join` | `FULL OUTER JOIN` | `SELECT id FROM orders FULL OUTER JOIN x ON orders.id = x.id` | info | | ||||||
| | `L052` | `join_without_condition` | Join with no real condition (`ON 1=1`, `ON true`, no `ON`) | `SELECT id FROM orders o JOIN customers c ON 1=1` | warning | | ||||||
| | `L053` | `null_in_in_list` | `NULL` inside an `IN (...)` list | `SELECT id FROM orders WHERE id IN (1, NULL)` | warning | | ||||||
| | `L054` | `sum_of_ratio` | `SUM()` of a per-row ratio | `SELECT sum(amount / qty) FROM orders` | warning | | ||||||
|
|
||||||
| ### Policy Rules | ||||||
|
|
||||||
| Policy findings are named after the built-in check that fired. Each check is switched on by a key in the `--policy` JSON file. In this engine version the `rules` / `pattern` entries shown under [Policy File Format](#policy-file-format) produced no findings in testing; use the keys below. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔎 Supported by static analysis
Replace or label the legacy policy examples. The CLI passes the policy JSON unchanged to Use
|
||||||
|
|
||||||
| Except `allowed_statements` and `blocked_tables`, these checks run only when `--schema` resolves the query's tables. Otherwise `check` reports `validation_failed`, and `blocked_columns` reports nothing. The examples were checked with a JSON schema file such as `{"tables": {"customers": {"columns": [{"name": "email", "type": "VARCHAR"}]}}}`. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: The sample schema defines only Prompt for AI agents
Suggested change
|
||||||
|
|
||||||
| | Rule | Enabled by | Example | Severity | | ||||||
| |------|------------|---------|----------| | ||||||
| | `max_joins` | `{"cost_control": {"max_joins": 1}}` | A query with two joins | error | | ||||||
| | `max_tables` | `{"cost_control": {"max_tables": 1}}` | `SELECT o.id FROM orders o JOIN customers c ON o.customer_id = c.id` | error | | ||||||
| | `block_select_star` | `{"cost_control": {"block_select_star": true}}` | `SELECT * FROM orders` | error | | ||||||
| | `require_limit` | `{"cost_control": {"require_limit": true}}` | `SELECT id FROM orders` | error | | ||||||
| | `max_complexity_score` | `{"cost_control": {"max_complexity_score": 5}}` | A join plus an `IN` subquery and `GROUP BY` | error | | ||||||
| | `blocked_columns` | `{"data_protection": {"blocked_columns": [{"table": "customers", "columns": ["email"]}]}}` | `SELECT email FROM customers` | error | | ||||||
| | `blocked_tables` | `{"data_protection": {"blocked_tables": [{"table": "customers"}]}}` | `SELECT id FROM customers` | error | | ||||||
| | `require_where_clause` | `{"data_protection": {"require_where_clause": true}}` | `DELETE FROM orders` | error | | ||||||
| | `allowed_statements` | `{"query_patterns": {"allowed_statements": ["SELECT"]}}` | `DROP TABLE orders` | error | | ||||||
| | `validation_failed` | Any policy (even `{}`), when the query does not validate against `--schema` or no `--schema` is given | `SELECT * FROM orders` with no schema | error | | ||||||
| | `policy_not_evaluated` | Any policy, on statements the engine cannot plan (see [Available Checks](#available-checks)) | `SELECT * FROM TABLE(my_fn())` | error | | ||||||
|
|
||||||
| --- | ||||||
|
|
||||||
| ## CI/CD Integration | ||||||
|
|
||||||
| ### GitHub Actions | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reconcile the lint finding contract.
The earlier guidance says lint findings may omit both
ruleandcode, but this line says they always carry both fields. State that both fields are normally present but optional, or update the earlier guidance. Inconsistent documentation can cause JSON consumers to mishandle valid findings.