SQL Lint Rules
LeapSQL includes 32 SQL lint rules organized into 5 categories.
Aliasing
Rules about alias usage and naming conventions.
AL03 - aliasing.expression
Severity: info
Expression columns should have explicit aliases.
Why This Matters
Expressions without aliases produce auto-generated column names that vary by database (e.g., "?column?", "expr0", "count(*)"). Explicit aliases make query results predictable and self-documenting, improving usability for downstream consumers.
Bad
SELECT
first_name || ' ' || last_name,
UPPER(email),
COUNT(*)
FROM users
GROUP BY 1, 2Good
SELECT
first_name || ' ' || last_name AS full_name,
UPPER(email) AS email_upper,
COUNT(*) AS user_count
FROM users
GROUP BY 1, 2How to Fix
Add an explicit alias using AS to give the expression a meaningful name.
AL04 - aliasing.unique_table
Severity: error
Table aliases should be unique within a query.
Why This Matters
Duplicate table aliases cause ambiguity when referencing columns. Most databases will reject queries with duplicate aliases. Even if accepted, it makes the query confusing and error-prone. Each table reference should have a unique alias.
Bad
SELECT a.id, a.name
FROM customers a
JOIN orders a ON a.customer_id = a.idGood
SELECT c.id, c.name
FROM customers c
JOIN orders o ON o.customer_id = c.idHow to Fix
Rename one of the duplicate aliases to be unique within the query.
AL05 - aliasing.unused
Severity: warning
Table alias is defined but not referenced.
Why This Matters
Unused table aliases add noise without providing clarity. They may indicate incomplete refactoring or copy-paste errors. If you alias a table, use that alias consistently to improve query readability.
Bad
SELECT id, name, email
FROM customers c
WHERE status = 'active'Good
SELECT c.id, c.name, c.email
FROM customers c
WHERE c.status = 'active'How to Fix
Either use the alias when referencing columns from this table, or remove the alias if it's not needed.
AL06 - aliasing.length
Severity: info
Alias length should be between min and max characters.
Why This Matters
Overly short aliases (single letters) lack meaning and make queries harder to understand. Overly long aliases add verbosity without improving clarity and may exceed database identifier limits. Balance brevity with descriptiveness.
Bad
SELECT a.customer_name, b.order_total
FROM customers_with_active_subscriptions_table a
JOIN order_history_last_30_days b ON b.customer_id = a.idGood
SELECT cust.customer_name, orders.order_total
FROM customers_with_active_subscriptions_table cust
JOIN order_history_last_30_days orders ON orders.customer_id = cust.idHow to Fix
Choose aliases that are descriptive but concise, typically 2-10 characters. Use meaningful abbreviations.
Configuration
This rule accepts the following configuration options: min_length, max_length
AL07 - aliasing.forbid
Severity: warning
Forbidden alias patterns (e.g., single letters, t1/t2).
Why This Matters
Generic aliases like single letters (a, b, c) or numbered tables (t1, t2) provide no semantic meaning. They make queries harder to understand and maintain, especially in complex queries with multiple joins. Use descriptive aliases instead.
Bad
SELECT a.name, b.total, c.date
FROM customers a
JOIN orders b ON b.customer_id = a.id
JOIN shipments c ON c.order_id = b.idGood
SELECT cust.name, ord.total, ship.date
FROM customers cust
JOIN orders ord ON ord.customer_id = cust.id
JOIN shipments ship ON ship.order_id = ord.idHow to Fix
Replace forbidden aliases with meaningful names that describe what the table represents in this query context.
Configuration
This rule accepts the following configuration options: forbidden_patterns, forbidden_names
AL08 - aliasing.unique_column
Severity: warning
Column aliases should be unique within SELECT clause.
Why This Matters
Duplicate column aliases create ambiguity in the result set. Downstream consumers (reports, APIs, other queries) may not be able to reliably reference the correct column. Some databases will error, others will silently pick one column.
Bad
SELECT
first_name AS name,
last_name AS name,
company_name AS name
FROM contactsGood
SELECT
first_name AS first_name,
last_name AS last_name,
company_name AS company_name
FROM contactsHow to Fix
Rename column aliases to be unique within the SELECT clause.
AL09 - aliasing.self_alias
Severity: hint
Table aliased to its own name is redundant.
Why This Matters
Aliasing a table to its own name (e.g., customers AS customers) adds verbosity without any benefit. It may indicate copy-paste errors or incomplete refactoring. Either use a shorter alias or remove the redundant alias entirely.
Bad
SELECT customers.id, customers.name
FROM customers AS customers
WHERE customers.status = 'active'Good
SELECT customers.id, customers.name
FROM customers
WHERE customers.status = 'active'How to Fix
Remove the redundant alias, or use a shorter meaningful alias if abbreviation is desired.
Ambiguous
Rules about ambiguous SQL constructs that may cause confusion or errors.
AM01 - ambiguous.distinct
Severity: warning
Using DISTINCT with GROUP BY is redundant.
Why This Matters
GROUP BY already guarantees unique rows for the grouped columns. Adding DISTINCT is redundant and may confuse readers about the query's intent. It can also mislead developers into thinking DISTINCT is needed for correctness.
Bad
SELECT DISTINCT department, COUNT(*)
FROM employees
GROUP BY departmentGood
SELECT department, COUNT(*)
FROM employees
GROUP BY departmentHow to Fix
Remove the DISTINCT keyword when using GROUP BY.
AM02 - ambiguous.union
Severity: info
UNION without ALL performs implicit DISTINCT which may be unintended.
Why This Matters
UNION (without ALL) automatically removes duplicate rows, which has performance implications and may not be the intended behavior. Explicitly using UNION ALL or UNION DISTINCT makes the intent clear and avoids accidental deduplication.
Bad
SELECT name FROM customers
UNION
SELECT name FROM suppliersGood
-- If duplicates should be removed:
SELECT name FROM customers
UNION DISTINCT
SELECT name FROM suppliers
-- If duplicates should be kept:
SELECT name FROM customers
UNION ALL
SELECT name FROM suppliersHow to Fix
Use UNION ALL if duplicates are acceptable, or UNION DISTINCT to make deduplication explicit.
AM03 - ambiguous.order_by
Severity: warning
ORDER BY column may be ambiguous in set operation.
Why This Matters
In set operations (UNION, INTERSECT, EXCEPT), column names from different queries may differ. Using column names in ORDER BY can be ambiguous and may behave differently across databases. Column positions (1, 2, 3) are unambiguous.
Bad
SELECT name, email FROM customers
UNION ALL
SELECT company_name, contact_email FROM suppliers
ORDER BY nameGood
SELECT name, email FROM customers
UNION ALL
SELECT company_name, contact_email FROM suppliers
ORDER BY 1How to Fix
Use column positions (1, 2, etc.) instead of column names in ORDER BY for set operations.
AM04 - ambiguous.column_count
Severity: error
Mismatched column counts in set operation.
Why This Matters
Set operations (UNION, INTERSECT, EXCEPT) require all queries to have the same number of columns. A mismatch will cause a runtime error. This rule catches the issue at development time.
Bad
SELECT id, name, email FROM customers
UNION ALL
SELECT id, name FROM suppliersGood
SELECT id, name, email FROM customers
UNION ALL
SELECT id, name, contact_email FROM suppliersHow to Fix
Ensure all queries in the set operation have the same number of columns.
AM05 - ambiguous.join
Severity: info
Comma-separated tables create an implicit cross join.
Why This Matters
The old-style comma join syntax (FROM a, b WHERE a.id = b.id) is harder to read than explicit JOIN syntax. It's easy to accidentally create a cross join by forgetting the WHERE condition. Explicit JOINs make intent clear.
Bad
SELECT c.name, o.total
FROM customers c, orders o
WHERE c.id = o.customer_idGood
SELECT c.name, o.total
FROM customers c
JOIN orders o ON c.id = o.customer_idHow to Fix
Replace comma-separated tables with explicit JOIN syntax (INNER JOIN, LEFT JOIN, etc.).
AM06 - ambiguous.column_refs
Severity: warning
Unqualified column reference may be ambiguous with multiple tables.
Why This Matters
When multiple tables are joined, unqualified column names may exist in more than one table. The database may pick an unexpected source, or error out. Qualifying columns prevents ambiguity and makes the query self-documenting.
Bad
SELECT name, email, created_at
FROM customers c
JOIN orders o ON o.customer_id = c.idGood
SELECT c.name, c.email, o.created_at
FROM customers c
JOIN orders o ON o.customer_id = c.idHow to Fix
Prefix column references with the table alias (e.g., c.name instead of name).
AM08 - ambiguous.join_condition
Severity: warning
Join condition should reference both tables being joined.
Why This Matters
A JOIN condition that doesn't reference the joined table is likely a bug. It effectively creates a cross join filtered by the condition, which is rarely intended. Each JOIN's ON clause should reference both the preceding and joining tables.
Bad
SELECT c.name, o.total
FROM customers c
JOIN orders o ON c.status = 'active'Good
SELECT c.name, o.total
FROM customers c
JOIN orders o ON o.customer_id = c.idHow to Fix
Ensure the JOIN condition references columns from both the left and right tables.
AM09 - ambiguous.order_by_limit
Severity: warning
ORDER BY/LIMIT with set operation may have unexpected scope.
Why This Matters
In set operations, ORDER BY and LIMIT without parentheses apply to the entire combined result, not individual queries. This behavior may be surprising. Use parentheses to make the intended scope explicit.
Bad
SELECT name FROM customers
UNION ALL
SELECT name FROM suppliers
ORDER BY name
LIMIT 10Good
-- To order/limit the final result:
(SELECT name FROM customers
UNION ALL
SELECT name FROM suppliers)
ORDER BY name
LIMIT 10
-- To order/limit individual queries:
(SELECT name FROM customers ORDER BY name LIMIT 10)
UNION ALL
(SELECT name FROM suppliers ORDER BY name LIMIT 10)How to Fix
Use parentheses to clarify whether ORDER BY/LIMIT applies to individual queries or the combined result.
Convention
Rules about SQL coding conventions and style consistency.
CV01 - convention.not_equal
Severity: hint
Prefer != over <> for not equal operator (NOT IMPLEMENTED: AST normalizes both operators).
Why This Matters
Using a consistent not-equal operator (either != or <>) throughout a codebase improves readability. The != operator is more common in modern programming languages, while <> is standard SQL. Pick one and use it consistently.
Bad
SELECT * FROM orders
WHERE status <> 'cancelled'
AND type != 'test'Good
SELECT * FROM orders
WHERE status != 'cancelled'
AND type != 'test'How to Fix
Use a consistent not-equal operator throughout your queries. This rule is not currently enforced due to AST limitations.
CV02 - convention.coalesce
Severity: hint
Prefer COALESCE over IFNULL/NVL for better portability.
Why This Matters
COALESCE is ANSI SQL standard and works across all major databases. IFNULL (MySQL) and NVL (Oracle) are database-specific. Using COALESCE improves query portability and is more flexible as it can handle multiple arguments.
Bad
SELECT
IFNULL(phone, 'N/A') AS phone,
NVL(email, 'unknown') AS email
FROM contactsGood
SELECT
COALESCE(phone, 'N/A') AS phone,
COALESCE(email, 'unknown') AS email
FROM contactsHow to Fix
Replace IFNULL or NVL with COALESCE for better SQL portability.
CV04 - convention.count_rows
Severity: hint
Prefer COUNT(*) over COUNT(1) for counting rows.
Why This Matters
COUNT() is the standard and most readable way to count rows. COUNT(1) achieves the same result but is less intuitive. Modern query optimizers treat them identically, so there's no performance benefit to COUNT(1). Use COUNT() for clarity.
Bad
SELECT
department,
COUNT(1) AS employee_count
FROM employees
GROUP BY departmentGood
SELECT
department,
COUNT(*) AS employee_count
FROM employees
GROUP BY departmentHow to Fix
Replace COUNT(1) with COUNT(*) for counting rows.
CV05 - convention.is_null
Severity: warning
Use IS NULL instead of = NULL for NULL comparisons.
Why This Matters
In SQL, NULL represents unknown, and comparing anything to NULL with = or != always yields NULL (unknown), not true or false. This is a common source of bugs. Use IS NULL or IS NOT NULL for correct NULL handling.
Bad
SELECT * FROM orders
WHERE shipped_date = NULLGood
SELECT * FROM orders
WHERE shipped_date IS NULLHow to Fix
Replace = NULL with IS NULL, and != NULL with IS NOT NULL.
CV08 - convention.left_join
Severity: hint
Prefer LEFT JOIN over RIGHT JOIN for consistency.
Why This Matters
LEFT JOIN is more intuitive because it preserves all rows from the table you naturally read first (left to right). RIGHT JOIN can always be rewritten as LEFT JOIN by swapping table order. Consistently using LEFT JOIN improves readability.
Bad
SELECT o.id, c.name
FROM orders o
RIGHT JOIN customers c ON c.id = o.customer_idGood
SELECT o.id, c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.idHow to Fix
Swap the table order and use LEFT JOIN instead of RIGHT JOIN.
CV09 - convention.blocked_words
Severity: warning
Block dangerous SQL keywords like DELETE, DROP, TRUNCATE.
Why This Matters
In data transformation pipelines (dbt, LeapSQL), destructive operations like DELETE, DROP, and TRUNCATE are usually mistakes. Models should be declarative transformations, not imperative modifications. Block these keywords to prevent accidents.
Bad
-- This could accidentally delete production data
DELETE FROM customers WHERE status = 'inactive'Good
-- Use a filter in your SELECT instead
SELECT * FROM customers
WHERE status != 'inactive'How to Fix
Remove or refactor destructive SQL statements. For data pipelines, use incremental logic or WHERE filters instead of DELETE/TRUNCATE.
Configuration
This rule accepts the following configuration options: blocked_words
References
Rules about column and table references in queries.
RF02 - references.qualification
Severity: warning
Qualify column references in queries with multiple tables.
Why This Matters
In queries involving multiple tables, unqualified column names can be ambiguous. If two tables have a column with the same name, the query may fail or return unexpected results. Qualifying columns with table names or aliases makes the query explicit and prevents errors when schemas change.
Bad
SELECT name, amount
FROM customers
JOIN orders ON customers.id = orders.customer_idGood
SELECT customers.name, orders.amount
FROM customers
JOIN orders ON customers.id = orders.customer_idHow to Fix
Prefix each column reference with its table name or alias.
RF03 - references.consistent
Severity: info
Column qualification style should be consistent.
Why This Matters
Mixing qualified and unqualified column references in the same query reduces readability. A consistent style makes it easier to understand which table each column comes from and helps reviewers quickly verify query correctness.
Bad
SELECT customers.name, amount, customers.email
FROM customers
JOIN orders ON customers.id = orders.customer_idGood
SELECT customers.name, orders.amount, customers.email
FROM customers
JOIN orders ON customers.id = orders.customer_idHow to Fix
Use the same qualification style (qualified or unqualified) for all column references.
Structure
Rules about SQL query structure and organization.
ST01 - structure.else_null
Severity: hint
ELSE NULL is redundant in CASE expressions.
Why This Matters
CASE expressions implicitly return NULL when no WHEN clause matches and no ELSE is specified. Writing ELSE NULL explicitly adds verbosity without changing behavior. Removing it keeps the query concise while maintaining the same semantics.
Bad
SELECT
CASE status
WHEN 'active' THEN 1
WHEN 'inactive' THEN 0
ELSE NULL
END AS status_code
FROM usersGood
SELECT
CASE status
WHEN 'active' THEN 1
WHEN 'inactive' THEN 0
END AS status_code
FROM usersHow to Fix
Remove the ELSE NULL clause from the CASE expression.
ST02 - structure.simple_case
Severity: hint
Searched CASE can be simplified to simple CASE expression.
Why This Matters
When all WHEN conditions compare the same column to different literal values using equality, a searched CASE can be rewritten as a simple CASE. Simple CASE expressions are more concise and clearly communicate the intent of mapping values from a single column.
Bad
SELECT
CASE
WHEN status = 'A' THEN 'Active'
WHEN status = 'I' THEN 'Inactive'
WHEN status = 'P' THEN 'Pending'
END AS status_label
FROM ordersGood
SELECT
CASE status
WHEN 'A' THEN 'Active'
WHEN 'I' THEN 'Inactive'
WHEN 'P' THEN 'Pending'
END AS status_label
FROM ordersHow to Fix
Convert to simple CASE by moving the common column after CASE and removing it from WHEN clauses.
ST03 - structure.unused_cte
Severity: warning
CTE is defined but never referenced.
Why This Matters
Unused CTEs add complexity without benefit. They consume mental overhead for readers trying to understand the query, and may indicate incomplete refactoring or copy-paste errors. Removing them improves query clarity.
Bad
WITH unused_cte AS (
SELECT * FROM orders
),
active_customers AS (
SELECT * FROM customers WHERE active = true
)
SELECT * FROM active_customersGood
WITH active_customers AS (
SELECT * FROM customers WHERE active = true
)
SELECT * FROM active_customersHow to Fix
Remove the unused CTE definition, or reference it in your query if it was intended to be used.
ST04 - structure.nested_case
Severity: info
Nested CASE expressions reduce readability.
Why This Matters
Nested CASE expressions are difficult to read and understand. They often indicate complex business logic that could be simplified by restructuring the query, using CTEs, or extracting the logic into a separate model or view.
Bad
SELECT
CASE
WHEN status = 'A' THEN
CASE
WHEN priority = 1 THEN 'High Active'
ELSE 'Low Active'
END
ELSE 'Inactive'
END AS label
FROM tasksGood
SELECT
CASE
WHEN status = 'A' AND priority = 1 THEN 'High Active'
WHEN status = 'A' THEN 'Low Active'
ELSE 'Inactive'
END AS label
FROM tasksHow to Fix
Flatten nested CASE expressions by combining conditions, or extract complex logic into a CTE or separate model.
ST06 - structure.column_order
Severity: hint
Wildcards should appear last in SELECT clause.
Why This Matters
Placing explicit columns before wildcards improves readability and makes the query's output structure clearer. The explicitly named columns are typically the most important ones, so listing them first highlights their significance.
Bad
SELECT *, created_at, updated_at
FROM ordersGood
SELECT created_at, updated_at, *
FROM ordersHow to Fix
Move wildcard expressions (* or table.*) to the end of the SELECT clause.
ST07 - structure.using
Severity: hint
Prefer USING clause for simple equality joins on same-named columns.
Why This Matters
The USING clause is more concise than ON when joining tables on columns with identical names. It clearly communicates that the join is on matching column names and automatically deduplicates the join column in the result set.
Bad
SELECT *
FROM orders o
JOIN customers c ON o.customer_id = c.customer_idGood
SELECT *
FROM orders o
JOIN customers c USING (customer_id)How to Fix
Replace ON with USING when joining on columns that have the same name in both tables.
ST08 - structure.distinct
Severity: info
Consider GROUP BY instead of DISTINCT when selecting columns for aggregation.
Why This Matters
Using GROUP BY instead of DISTINCT on simple column selections makes the query's intent clearer and positions the code better for future aggregation needs. GROUP BY explicitly shows which columns define the unique rows, while DISTINCT can be ambiguous in complex queries.
Bad
SELECT DISTINCT department, location
FROM employeesGood
SELECT department, location
FROM employees
GROUP BY department, locationHow to Fix
Replace SELECT DISTINCT with GROUP BY on the same columns.
ST09 - structure.join_condition_order
Severity: hint
Join condition should reference left table first (e.g., a.id = b.id, not b.id = a.id).
Why This Matters
Consistently ordering join conditions with the left (existing) table first improves readability. It follows the natural reading order of the query: FROM table_a JOIN table_b ON table_a.col = table_b.col. This convention makes it easier to trace relationships through the query.
Bad
SELECT *
FROM orders o
JOIN customers c ON c.id = o.customer_idGood
SELECT *
FROM orders o
JOIN customers c ON o.customer_id = c.idHow to Fix
Reorder the join condition to reference the left table first.
ST10 - structure.constant_expression
Severity: info
Unnecessary constant expressions like WHERE 1=1 or WHERE true.
Why This Matters
Constant expressions like WHERE 1=1 or WHERE true are often artifacts of dynamic SQL generation. In static SQL models, they add noise without affecting results. Removing them makes the query cleaner and easier to understand.
Bad
SELECT *
FROM orders
WHERE 1=1
AND status = 'active'Good
SELECT *
FROM orders
WHERE status = 'active'How to Fix
Remove constant expressions from WHERE clauses.