Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/sphinx/source/reference/sql_commands/DQL.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Syntax
DQL/SELECT
DQL/WITH
DQL/WHERE
DQL/CASE
DQL/ORDER_BY
DQL/EXPLAIN

Expand Down
18 changes: 18 additions & 0 deletions docs/sphinx/source/reference/sql_commands/DQL/CASE.diagram
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Diagram(
Terminal('CASE'),
OneOrMore(
Sequence(
Terminal('WHEN'),
NonTerminal('condition'),
Terminal('THEN'),
NonTerminal('result')
)
),
Optional(
Sequence(
Terminal('ELSE'),
NonTerminal('result')
)
),
Terminal('END')
)
303 changes: 303 additions & 0 deletions docs/sphinx/source/reference/sql_commands/DQL/CASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
====
CASE
====

.. _case:

Returns a result based on conditional evaluation, similar to if-then-else logic.

Syntax
======

.. raw:: html
:file: CASE.diagram.svg

CASE expressions use conditional evaluation:

.. code-block:: sql

CASE
WHEN condition1 THEN result1
WHEN condition2 THEN result2
...
ELSE default_result
END

Parameters
==========

``condition``
A boolean expression evaluated for each WHEN clause.

``result``
The value to return if the corresponding condition is TRUE. Can be any expression, column, or literal.

``ELSE result`` (optional)
The default value returned if no conditions are TRUE. If omitted and no conditions match, returns NULL.

Returns
=======

Returns the result of the first matching WHEN clause, or the ELSE value if no matches are found, or NULL if no ELSE clause is specified and no matches are found.

All result expressions must be of compatible types (or NULL).

Examples
Copy link
Member

@g31pranjal g31pranjal Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we also need an example of the CASE construct in SELECT and ORDER BY clauses (as indicated in PR desc)? (I didn't know we could use it in ORDER BY (can we?), and how that would look!)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two examples with CASE constructs in SELECT (Value Matching with CASE, and Nested CASE).
The ORDER BY case is totally not supported, I'll remove that from the description.

We essentially cannot generate an index with a dispatch as part of a KeyExpression at the moment, therefore we cannot plan an order by query with that.

========

Setup
-----

For these examples, assume we have a ``products`` table:

.. code-block:: sql

CREATE TABLE products(
id BIGINT,
name STRING,
category STRING,
price BIGINT,
stock INTEGER,
PRIMARY KEY(id))

INSERT INTO products VALUES
(1, 'Widget A', 'Electronics', 100, 50),
(2, 'Widget B', 'Electronics', 150, 5),
(3, 'Gadget X', 'Electronics', 200, 0),
(4, 'Tool A', 'Hardware', 80, 100),
(5, 'Tool B', 'Hardware', 120, 15)

Simple Condition
----------------

Categorize products by stock level:

.. code-block:: sql

SELECT name,
stock,
CASE
WHEN stock = 0 THEN 'Out of Stock'
WHEN stock < 10 THEN 'Low Stock'
WHEN stock < 50 THEN 'In Stock'
ELSE 'Well Stocked'
END AS stock_status
FROM products

.. list-table::
:header-rows: 1

* - :sql:`name`
- :sql:`stock`
- :sql:`stock_status`
* - :json:`"Widget A"`
- :json:`50`
- :json:`"Well Stocked"`
* - :json:`"Widget B"`
- :json:`5`
- :json:`"Low Stock"`
* - :json:`"Gadget X"`
- :json:`0`
- :json:`"Out of Stock"`
* - :json:`"Tool A"`
- :json:`100`
- :json:`"Well Stocked"`
* - :json:`"Tool B"`
- :json:`15`
- :json:`"In Stock"`

Multiple Conditions
-------------------

Calculate discount based on price and category:

.. code-block:: sql

SELECT name,
price,
CASE
WHEN category = 'Electronics' AND price > 150 THEN price * 0.85
WHEN category = 'Electronics' THEN price * 0.90
WHEN price > 100 THEN price * 0.95
ELSE price
END AS discounted_price
FROM products

.. list-table::
:header-rows: 1

* - :sql:`name`
- :sql:`price`
- :sql:`discounted_price`
* - :json:`"Widget A"`
- :json:`100`
- :json:`90.0`
* - :json:`"Widget B"`
- :json:`150`
- :json:`135.0`
* - :json:`"Gadget X"`
- :json:`200`
- :json:`170.0`
* - :json:`"Tool A"`
- :json:`80`
- :json:`80.0`
* - :json:`"Tool B"`
- :json:`120`
- :json:`114.0`

Value Matching with CASE
-------------------------

Map category codes to names using WHEN conditions:

.. code-block:: sql

SELECT name,
category,
CASE
WHEN category = 'Electronics' THEN 'E'
WHEN category = 'Hardware' THEN 'H'
WHEN category = 'Media' THEN 'M'
ELSE 'Other'
END AS category_code
FROM products

.. list-table::
:header-rows: 1

* - :sql:`name`
- :sql:`category`
- :sql:`category_code`
* - :json:`"Widget A"`
- :json:`"Electronics"`
- :json:`"E"`
* - :json:`"Widget B"`
- :json:`"Electronics"`
- :json:`"E"`
* - :json:`"Gadget X"`
- :json:`"Electronics"`
- :json:`"E"`
* - :json:`"Tool A"`
- :json:`"Hardware"`
- :json:`"H"`
* - :json:`"Tool B"`
- :json:`"Hardware"`
- :json:`"H"`

Nested CASE
-----------

CASE expressions can be nested:

.. code-block:: sql

SELECT name,
price,
stock,
CASE
WHEN stock = 0 THEN 'Unavailable'
ELSE CASE
WHEN price < 100 THEN 'Budget'
WHEN price < 150 THEN 'Standard'
ELSE 'Premium'
END
END AS product_tier
FROM products

.. list-table::
:header-rows: 1

* - :sql:`name`
- :sql:`price`
- :sql:`stock`
- :sql:`product_tier`
* - :json:`"Widget A"`
- :json:`100`
- :json:`50`
- :json:`"Standard"`
* - :json:`"Widget B"`
- :json:`150`
- :json:`5`
- :json:`"Premium"`
* - :json:`"Gadget X"`
- :json:`200`
- :json:`0`
- :json:`"Unavailable"`
* - :json:`"Tool A"`
- :json:`80`
- :json:`100`
- :json:`"Budget"`
* - :json:`"Tool B"`
- :json:`120`
- :json:`15`
- :json:`"Standard"`

Important Notes
===============

Evaluation Order
----------------

CASE evaluates conditions **in order** and returns the first matching result. Once a match is found, remaining conditions are not evaluated:

.. code-block:: sql

CASE
WHEN x > 100 THEN 'High' -- Evaluated first
WHEN x > 50 THEN 'Medium' -- Only checked if first is false
WHEN x > 0 THEN 'Low' -- Only checked if previous are false
ELSE 'Zero or Negative'
END

Order your conditions from most specific to least specific.

NULL Handling
-------------

NULL values in conditions are treated as FALSE:

.. code-block:: sql

-- If price IS NULL, condition is FALSE
CASE
WHEN price > 100 THEN 'Expensive'
ELSE 'Other'
END

To explicitly check for NULL:

.. code-block:: sql

CASE
WHEN price IS NULL THEN 'No Price'
WHEN price > 100 THEN 'Expensive'
ELSE 'Affordable'
END

Type Compatibility
------------------

.. important::

All result expressions in a CASE statement must return compatible types.
Mixing types (e.g., strings and numbers) will cause an error.
Comment on lines +282 to +283
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could a result be NULL and be compatible to other results? If it so, then we can avoid writing that case altogether, and not have ELSE; but just curious if the former is possible

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, a result can be NULL. It could still make sense to write that case down, since you may want it to be evaluated before some of the later cases though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we need to have an explicit example for that here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is documented in the Evaluation Order section


.. code-block:: sql

-- VALID: All results are strings
CASE
WHEN x > 100 THEN 'High'
WHEN x > 50 THEN 'Medium'
ELSE 'Low'
END

-- INVALID: Mixed types (string and number)
CASE
WHEN x > 100 THEN 'High'
ELSE 0
END

See Also
========

* :ref:`Comparison Operators <comparison-operators>` - Operators used in CASE conditions
5 changes: 5 additions & 0 deletions yaml-tests/src/test/java/DocumentationQueriesTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ void betweenOperatorQueriesTests(YamlTest.Runner runner) throws Exception {
runner.runYamsql(PREFIX + "/between-operator-queries.yamsql");
}

@TestTemplate
void caseDocumentationQueriesTests(YamlTest.Runner runner) throws Exception {
runner.runYamsql(PREFIX + "/case-documentation-queries.yamsql");
}

@TestTemplate
void castDocumentationQueriesTests(YamlTest.Runner runner) throws Exception {
runner.runYamsql(PREFIX + "/cast-documentation-queries.yamsql");
Expand Down
Loading
Loading