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
Original file line number Diff line number Diff line change
Expand Up @@ -183,14 +183,14 @@ class HavingFunctionEmissionSpec extends AnyFlatSpec with Matchers {
).mkString
}

"GREATEST over an aggregate" should "emit a bucket_selector, null-guarded" in {
"GREATEST over an aggregate" should "emit a bucket_selector skipping a NULL argument" in {
// `GREATEST(NULL, 0)` is 0 -- the docs, and WHERE: with a literal argument it is never NULL.
queryOf(group + "GREATEST(COUNT(*), 0) > 1") shouldBe Seq(
"""{"query":{"match_all":{}},"size":0,"_source":false,"aggs":{"status":{""",
terms,
""","aggs":{"c":{"value_count":{"field":"_index"}},""",
""""having_filter":{"bucket_selector":{"buckets_path":{"c":"c"},""",
""""script":{"source":"(params.c == null ? false : """,
"""(Math.max(params.c, 0) > 1))"}}}}}}}"""
""""script":{"source":"(params.c == null ? 0 : Math.max(params.c, 0)) > 1"}}}}}}}"""
).mkString
}

Expand Down Expand Up @@ -339,14 +339,14 @@ class HavingFunctionEmissionSpec extends AnyFlatSpec with Matchers {
"a conjunction of a bare and a wrapped aggregate" should "emit both conditions" in {
queryOf(group + "COUNT(*) > 1 AND GREATEST(COUNT(*), 0) > 2") should include(
""""script":{"source":"(params.c == null ? false : (params.c > 1)) && """ +
"""(params.c == null ? false : (Math.max(params.c, 0) > 2))"}"""
"""(params.c == null ? 0 : Math.max(params.c, 0)) > 2"}"""
)
}

"a disjunction" should "emit both conditions" in {
// Dropping a disjunct makes the filter STRICTER than written: rows disappear silently.
queryOf(group + "GREATEST(COUNT(*), 0) > 1 OR COUNT(*) > 5") should include(
"""(params.c == null ? false : (Math.max(params.c, 0) > 1)) || """ +
"""(params.c == null ? 0 : Math.max(params.c, 0)) > 1 || """ +
"""(params.c == null ? false : (params.c > 5))"""
)
}
Expand Down Expand Up @@ -392,8 +392,8 @@ class HavingFunctionEmissionSpec extends AnyFlatSpec with Matchers {
terms,
""","aggs":{"count_all":{"value_count":{"field":"_index"}},""",
""""having_filter":{"bucket_selector":{"buckets_path":{"count_all":"count_all"},""",
""""script":{"source":"(params.count_all == null ? false : """,
"""(Math.max(params.count_all, 0) > 1))"}}}}}}}"""
""""script":{"source":"(params.count_all == null ? 0 : """,
"""Math.max(params.count_all, 0)) > 1"}}}}}}}"""
).mkString
}

Expand All @@ -408,8 +408,8 @@ class HavingFunctionEmissionSpec extends AnyFlatSpec with Matchers {
""""aggs":{"e.name":{"terms":{"field":"emails.name","size":65536,"min_doc_count":1},""",
""""aggs":{"count_e_address":{"value_count":{"field":"emails.address"}},""",
""""having_filter":{"bucket_selector":{"buckets_path":{"count_e_address":"count_e_address"},""",
""""script":{"source":"(params.count_e_address == null ? false : """,
"""(Math.max(params.count_e_address, 0) > 1))"}}}}}}}}}"""
""""script":{"source":"(params.count_e_address == null ? 0 : """,
"""Math.max(params.count_e_address, 0)) > 1"}}}}}}}}}"""
).mkString
}

Expand All @@ -423,14 +423,13 @@ class HavingFunctionEmissionSpec extends AnyFlatSpec with Matchers {
terms,
""","aggs":{"c":{"value_count":{"field":"_index"}},"max_x":{"max":{"field":"x"}},""",
""""having_filter":{"bucket_selector":{"buckets_path":{"c":"c","max_x":"max_x"},""",
""""script":{"source":"(params.c == null""",
""" || ((def) (params.max_x == null""",
""""script":{"source":"(params.c == null ? false : (params.c > (""",
"""((def) (params.max_x == null""",
""" || Double.isNaN(params.max_x)""",
""" || Double.isInfinite(params.max_x) ? null : params.max_x)) == null ? false : """,
"""(params.c > Math.max""",
"""(((def) (params.max_x == null""",
""" || Double.isInfinite(params.max_x) ? null : params.max_x)) == null ? 0 : Math.max(""",
"""((def) (params.max_x == null""",
""" || Double.isNaN(params.max_x)""",
""" || Double.isInfinite(params.max_x) ? null : params.max_x)), 0)))"}}}}}}}"""
""" || Double.isInfinite(params.max_x) ? null : params.max_x)), 0))))"}}}}}}}"""
).mkString
}

Expand All @@ -444,8 +443,7 @@ class HavingFunctionEmissionSpec extends AnyFlatSpec with Matchers {
""""aggs":{"__whole_table_having__":{"filters":{"filters":{"_all":{"match_all":{}}}},""",
""""aggs":{"c":{"value_count":{"field":"_index"}},""",
""""having_filter":{"bucket_selector":{"buckets_path":{"c":"c"},""",
""""script":{"source":"(params.c == null ? false : """,
"""(Math.max(params.c, 0) > 1))"}}}}}}}"""
""""script":{"source":"(params.c == null ? 0 : Math.max(params.c, 0)) > 1"}}}}}}}"""
).mkString
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1655,7 +1655,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
| "diff": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['createdAt'].size() == 0 ? null : doc['createdAt'].value.toInstant().atZone(ZoneId.of('Z')).toLocalDate()); def param2 = (doc['updatedAt'].size() == 0 ? null : doc['updatedAt'].value.toInstant().atZone(ZoneId.of('Z')).toLocalDate()); (param1 == null || param2 == null) ? null : Long.valueOf(ChronoUnit.DAYS.between(param1, param2))"
| "source": "def param1 = (doc['createdAt'].size() == 0 ? null : doc['createdAt'].value.toInstant().atZone(ZoneId.of('Z'))); def param2 = (doc['updatedAt'].size() == 0 ? null : doc['updatedAt'].value.toInstant().atZone(ZoneId.of('Z'))); (param1 == null || param2 == null) ? null : Long.valueOf(ChronoUnit.DAYS.between(param1.toLocalDate(), param2.toLocalDate()))"
| }
| }
| },
Expand Down Expand Up @@ -1712,7 +1712,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers {
| "max": {
| "script": {
| "lang": "painless",
| "source": "def param1 = (doc['createdAt'].size() == 0 ? null : doc['createdAt'].value.toLocalDate()); def param2 = (doc['updatedAt'].size() == 0 ? null : doc['updatedAt'].value.toInstant().atZone(ZoneId.of('Z')).toLocalDate()); def param3 = (param1 == null) ? null : ZonedDateTime.parse(param1, new DateTimeFormatterBuilder().appendPattern(\"yyyy-MM-dd HH:mm:ss\").appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true).toFormatter().withZone(ZoneId.of('Z'))); def param4 = (param3 != null ? param3.toLocalDate() : null); (param1 == null || param2 == null) ? null : Long.valueOf(ChronoUnit.DAYS.between(param4, param2))"
| "source": "def param1 = (doc['createdAt'].size() == 0 ? null : doc['createdAt'].value); def param2 = (doc['updatedAt'].size() == 0 ? null : doc['updatedAt'].value.toInstant().atZone(ZoneId.of('Z'))); def param3 = (param1 == null) ? null : ZonedDateTime.parse(param1, new DateTimeFormatterBuilder().appendPattern(\"yyyy-MM-dd HH:mm:ss\").appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true).toFormatter().withZone(ZoneId.of('Z'))); (param1 == null || param2 == null) ? null : Long.valueOf(ChronoUnit.DAYS.between(param3.toLocalDate(), param2.toLocalDate()))"
| }
| }
| }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ class MaterializedViewHavingGatewaySpec
"CREATE MATERIALIZED VIEW mv AS SELECT city, COUNT(*) AS c FROM customers GROUP BY city HAVING MAX(amount) > 1",
"an aggregate no transform can compute" ->
"CREATE MATERIALIZED VIEW mv AS SELECT city, STDDEV(amount) AS sd FROM customers GROUP BY city HAVING STDDEV(amount) > 1",
"an expression over aggregates" ->
"CREATE MATERIALIZED VIEW mv AS SELECT city, MAX(amount) - MIN(amount) AS d FROM customers GROUP BY city HAVING d > 3",
"two aggregates compared" ->
"CREATE MATERIALIZED VIEW mv AS SELECT city, MAX(amount) AS mx, MIN(amount) AS mn FROM customers GROUP BY city HAVING MAX(amount) > MIN(amount)"
"an expression over an aggregate no transform can compute" ->
"CREATE MATERIALIZED VIEW mv AS SELECT city, STDDEV(amount) - MIN(amount) AS d FROM customers GROUP BY city HAVING d > 3",
"a child predicate beside a metric" ->
"CREATE MATERIALIZED VIEW mv AS SELECT city, SUM(amount) AS s FROM customers GROUP BY city HAVING s > 5 AND child(c.x = 1)"
)

"an unmaterializable HAVING" should "answer 400, naming the clause" in {
Expand Down Expand Up @@ -129,6 +129,8 @@ class MaterializedViewHavingGatewaySpec
Seq(
"CREATE MATERIALIZED VIEW mv AS SELECT city, COUNT(*) AS c FROM customers GROUP BY city HAVING COUNT(*) > 1",
"CREATE MATERIALIZED VIEW mv AS SELECT city, MAX(amount) AS mx FROM customers GROUP BY city HAVING MAX(amount) > 1",
"CREATE MATERIALIZED VIEW mv AS SELECT city, MAX(amount) - MIN(amount) AS d FROM customers GROUP BY city HAVING d > 3",
"CREATE MATERIALIZED VIEW mv AS SELECT city, MAX(amount) AS mx, MIN(amount) AS mn FROM customers GROUP BY city HAVING MAX(amount) > MIN(amount)",
"CREATE OR REPLACE MATERIALIZED VIEW mv AS SELECT city, COUNT(*) AS c FROM customers GROUP BY city HAVING COUNT(*) > 1",
"CREATE MATERIALIZED VIEW mv AS SELECT city, COUNT(*) AS c FROM customers GROUP BY city"
).foreach { sql =>
Expand Down
18 changes: 9 additions & 9 deletions documentation/sql/dql_statements.md
Original file line number Diff line number Diff line change
Expand Up @@ -1291,14 +1291,14 @@ FROM dql_users;

##### **Arithmetic:**

| Function | Description |
|-------------------------------------|----------------------------------|
| `DATE_ADD(date, INTERVAL n unit)` | Add interval |
| `DATE_SUB(date, INTERVAL n unit)` | Subtract interval |
| `DATETIME_ADD(ts, INTERVAL n unit)` | Add interval to timestamp |
| `DATETIME_SUB(ts, INTERVAL n unit)` | Subtract interval from timestamp |
| `DATE_DIFF(date1, date2, unit)` | Difference in units |
| `DATE_TRUNC(date, unit)` | Truncate to unit |
| Function | Description |
|-------------------------------------|-----------------------------------------------------------------------------------------------|
| `DATE_ADD(date, INTERVAL n unit)` | Add interval |
| `DATE_SUB(date, INTERVAL n unit)` | Subtract interval |
| `DATETIME_ADD(ts, INTERVAL n unit)` | Add interval to timestamp |
| `DATETIME_SUB(ts, INTERVAL n unit)` | Subtract interval from timestamp |
| `DATE_DIFF(date1, date2, unit)` | Difference in units: elapsed `HOUR` / `MINUTE` / `SECOND`, calendar dates from `DAY` up (UTC) |
| `DATE_TRUNC(date, unit)` | Truncate to unit |

##### **Formatting & parsing:**

Expand Down Expand Up @@ -1327,7 +1327,7 @@ SELECT id,
MONTH(CURRENT_DATE) AS current_month,
DAY(CURRENT_DATE) AS current_day,
YEAR(birthdate) AS year_b,
DATE_DIFF(CURRENT_DATE, birthdate, YEAR) AS diff_years,
DATE_DIFF(birthdate, CURRENT_DATE, YEAR) AS diff_years,
DATE_TRUNC(birthdate, MONTH) AS trunc_month,
DATETIME_FORMAT(birthdate, '%Y-%m-%d') AS birth_str
FROM dql_users;
Expand Down
28 changes: 19 additions & 9 deletions documentation/sql/functions_date_time.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@ SELECT DATETIME_SUB('2025-01-10T12:00:00Z'::TIMESTAMP, INTERVAL 1 MONTH) AS last

#### DATEDIFF / DATE_DIFF

Difference between 2 dates (date1 - date2) in the specified time unit.
Difference between 2 dates (date2 - date1) in the specified time unit: `date1` is the start and `date2` the end.
MySQL's two-argument `DATEDIFF(a, b)` gives `a - b`.

**Syntax:**
```sql
Expand All @@ -337,38 +338,47 @@ DATE_DIFF(date1, date2, unit)
**Output:**
- `BIGINT`

**Units:**
- `HOUR`, `MINUTE`, `SECOND` count the elapsed whole units between the two instants, in UTC, truncated toward zero. A `DATE` operand counts from the start of its day (00:00 UTC).
- `DAY`, `WEEK`, `MONTH`, `QUARTER`, `YEAR` compare the two calendar dates, in UTC, whatever the time of day: `2025-01-10T23:30:00Z` and `2025-01-11T00:30:00Z` are 1 day apart, as MySQL's `DATEDIFF` counts them. A week is 7 whole days, a month counts once its day of month is reached, a quarter is 3 whole months and a year 12; every count is truncated toward zero.

**Literals:**
- A string literal is read as the temporal it spells: `'2025-01-10'` (or `'2025/01/10'`) is a `DATE`; a literal with a time of day (`'2025-01-10 14:00:00'`, `'2025-01-10T14:00:00Z'`) is a `TIMESTAMP`, in UTC unless it names a zone.
- This holds in every clause, for each row and for each group: `DATEDIFF(MAX(created_at), '2025-01-10 08:00:00', HOUR)`.
- A `NULL` operand, or an aggregate over a group that has no value, gives `NULL`.

**Examples:**
```sql
-- Difference in days (default)
-- Difference in days (default), MySQL's two-argument form: date1 - date2
SELECT DATEDIFF('2025-01-10'::DATE, '2025-01-01'::DATE) AS diff;
-- Result: 9

-- Difference in days (explicit)
SELECT DATEDIFF('2025-01-10'::DATE, '2025-01-01'::DATE, DAY) AS diff_days;
SELECT DATEDIFF('2025-01-01'::DATE, '2025-01-10'::DATE, DAY) AS diff_days;
-- Result: 9

-- Difference in weeks
SELECT DATE_DIFF('2025-01-31'::DATE, '2025-01-01'::DATE, WEEK) AS diff_weeks;
SELECT DATE_DIFF('2025-01-01'::DATE, '2025-01-31'::DATE, WEEK) AS diff_weeks;
-- Result: 4

-- Difference in months
SELECT DATEDIFF('2025-06-01'::DATE, '2025-01-01'::DATE, MONTH) AS diff_months;
SELECT DATEDIFF('2025-01-01'::DATE, '2025-06-01'::DATE, MONTH) AS diff_months;
-- Result: 5

-- Difference in years
SELECT DATEDIFF('2027-01-01'::DATE, '2025-01-01'::DATE, YEAR) AS diff_years;
SELECT DATEDIFF('2025-01-01'::DATE, '2027-01-01'::DATE, YEAR) AS diff_years;
-- Result: 2

-- Difference in hours (with timestamps)
SELECT DATEDIFF('2025-01-10T14:00:00Z'::TIMESTAMP, '2025-01-10T12:00:00Z'::TIMESTAMP, HOUR) AS diff_hours;
SELECT DATEDIFF('2025-01-10T12:00:00Z'::TIMESTAMP, '2025-01-10T14:00:00Z'::TIMESTAMP, HOUR) AS diff_hours;
-- Result: 2

-- Difference in minutes
SELECT DATEDIFF('2025-01-10T12:30:00Z'::TIMESTAMP, '2025-01-10T12:00:00Z'::TIMESTAMP, MINUTE) AS diff_minutes;
SELECT DATEDIFF('2025-01-10T12:00:00Z'::TIMESTAMP, '2025-01-10T12:30:00Z'::TIMESTAMP, MINUTE) AS diff_minutes;
-- Result: 30

-- Difference in seconds
SELECT DATEDIFF('2025-01-10T12:00:45Z'::TIMESTAMP, '2025-01-10T12:00:00Z'::TIMESTAMP, SECOND) AS diff_seconds;
SELECT DATEDIFF('2025-01-10T12:00:00Z'::TIMESTAMP, '2025-01-10T12:00:45Z'::TIMESTAMP, SECOND) AS diff_seconds;
-- Result: 45
```

Expand Down
2 changes: 1 addition & 1 deletion documentation/sql/functions_math.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ SELECT FLOOR(123.999) AS f;
SELECT
user_id,
name,
FLOOR(DATEDIFF(CURRENT_DATE, birth_date, DAY) / 365.25) AS age
FLOOR(DATEDIFF(birth_date, CURRENT_DATE, DAY) / 365.25) AS age
FROM users;

-- Bucket values
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,14 +189,14 @@ class HavingFunctionEmissionSpec extends AnyFlatSpec with Matchers {
).mkString
}

"GREATEST over an aggregate" should "emit a bucket_selector, null-guarded" in {
"GREATEST over an aggregate" should "emit a bucket_selector skipping a NULL argument" in {
// `GREATEST(NULL, 0)` is 0 -- the docs, and WHERE: with a literal argument it is never NULL.
queryOf(group + "GREATEST(COUNT(*), 0) > 1") shouldBe Seq(
"""{"query":{"match_all":{}},"size":0,"_source":false,"aggs":{"status":{""",
terms,
""","aggs":{"c":{"value_count":{"field":"_index"}},""",
""""having_filter":{"bucket_selector":{"buckets_path":{"c":"c"},""",
""""script":{"source":"(params.c == null ? false : """,
"""(Math.max(params.c, 0) > 1))"}}}}}}}"""
""""script":{"source":"(params.c == null ? 0 : Math.max(params.c, 0)) > 1"}}}}}}}"""
).mkString
}

Expand Down Expand Up @@ -353,14 +353,14 @@ class HavingFunctionEmissionSpec extends AnyFlatSpec with Matchers {
"a conjunction of a bare and a wrapped aggregate" should "emit both conditions" in {
queryOf(group + "COUNT(*) > 1 AND GREATEST(COUNT(*), 0) > 2") should include(
""""script":{"source":"(params.c == null ? false : (params.c > 1)) && """ +
"""(params.c == null ? false : (Math.max(params.c, 0) > 2))"}"""
"""(params.c == null ? 0 : Math.max(params.c, 0)) > 2"}"""
)
}

"a disjunction" should "emit both conditions" in {
// Dropping a disjunct makes the filter STRICTER than written: rows disappear silently.
queryOf(group + "GREATEST(COUNT(*), 0) > 1 OR COUNT(*) > 5") should include(
"""(params.c == null ? false : (Math.max(params.c, 0) > 1)) || """ +
"""(params.c == null ? 0 : Math.max(params.c, 0)) > 1 || """ +
"""(params.c == null ? false : (params.c > 5))"""
)
}
Expand Down Expand Up @@ -407,8 +407,8 @@ class HavingFunctionEmissionSpec extends AnyFlatSpec with Matchers {
terms,
""","aggs":{"count_all":{"value_count":{"field":"_index"}},""",
""""having_filter":{"bucket_selector":{"buckets_path":{"count_all":"count_all"},""",
""""script":{"source":"(params.count_all == null ? false : """,
"""(Math.max(params.count_all, 0) > 1))"}}}}}}}"""
""""script":{"source":"(params.count_all == null ? 0 : """,
"""Math.max(params.count_all, 0)) > 1"}}}}}}}"""
).mkString
}

Expand All @@ -423,8 +423,8 @@ class HavingFunctionEmissionSpec extends AnyFlatSpec with Matchers {
""""aggs":{"e.name":{"terms":{"field":"emails.name","size":65536,"min_doc_count":1},""",
""""aggs":{"count_e_address":{"value_count":{"field":"emails.address"}},""",
""""having_filter":{"bucket_selector":{"buckets_path":{"count_e_address":"count_e_address"},""",
""""script":{"source":"(params.count_e_address == null ? false : """,
"""(Math.max(params.count_e_address, 0) > 1))"}}}}}}}}}"""
""""script":{"source":"(params.count_e_address == null ? 0 : """,
"""Math.max(params.count_e_address, 0)) > 1"}}}}}}}}}"""
).mkString
}

Expand All @@ -438,14 +438,13 @@ class HavingFunctionEmissionSpec extends AnyFlatSpec with Matchers {
terms,
""","aggs":{"c":{"value_count":{"field":"_index"}},"max_x":{"max":{"field":"x"}},""",
""""having_filter":{"bucket_selector":{"buckets_path":{"c":"c","max_x":"max_x"},""",
""""script":{"source":"(params.c == null""",
""" || ((def) (params.max_x == null""",
""""script":{"source":"(params.c == null ? false : (params.c > (""",
"""((def) (params.max_x == null""",
""" || Double.isNaN(params.max_x)""",
""" || Double.isInfinite(params.max_x) ? null : params.max_x)) == null ? false : """,
"""(params.c > Math.max""",
"""(((def) (params.max_x == null""",
""" || Double.isInfinite(params.max_x) ? null : params.max_x)) == null ? 0 : Math.max(""",
"""((def) (params.max_x == null""",
""" || Double.isNaN(params.max_x)""",
""" || Double.isInfinite(params.max_x) ? null : params.max_x)), 0)))"}}}}}}}"""
""" || Double.isInfinite(params.max_x) ? null : params.max_x)), 0))))"}}}}}}}"""
).mkString
}

Expand All @@ -459,8 +458,7 @@ class HavingFunctionEmissionSpec extends AnyFlatSpec with Matchers {
""""aggs":{"__whole_table_having__":{"filters":{"filters":{"_all":{"match_all":{}}}},""",
""""aggs":{"c":{"value_count":{"field":"_index"}},""",
""""having_filter":{"bucket_selector":{"buckets_path":{"c":"c"},""",
""""script":{"source":"(params.c == null ? false : """,
"""(Math.max(params.c, 0) > 1))"}}}}}}}"""
""""script":{"source":"(params.c == null ? 0 : Math.max(params.c, 0)) > 1"}}}}}}}"""
).mkString
}

Expand Down
Loading
Loading