diff --git a/doc/user/content/release-notes.md b/doc/user/content/release-notes.md
index a670d2884051b..bec4dfb499c2e 100644
--- a/doc/user/content/release-notes.md
+++ b/doc/user/content/release-notes.md
@@ -120,6 +120,20 @@ changes that have not yet been documented.
- Add the `array_cat` function.
+- **Breaking change.** Return an error when [`extract`](/sql/functions/extract/)
+ is called with a [`date`] value but a time-related field (e.g., `SECOND`).
+
+ Previous versions of Materialize would incorrectly return `0` in these cases.
+ The new behavior matches PostgreSQL.
+
+ [`date_part`](/sql/functions/date-part/) still returns a `0` in these cases,
+ which matches the PostgreSQL behavior.
+
+- **Breaking change.** Change the return type of [`extract`](/sql/functions/extract/)
+ from [`float`](/sql/types/float/) to [`numeric`](/sql/types/numeric/).
+
+ This new behavior matches PostgreSQL v14.
+
{{< comment >}}
Only add new release notes above this line.
diff --git a/doc/user/content/sql/functions/date-part.md b/doc/user/content/sql/functions/date-part.md
new file mode 100644
index 0000000000000..aabc2d29c762e
--- /dev/null
+++ b/doc/user/content/sql/functions/date-part.md
@@ -0,0 +1,72 @@
+---
+title: "date_part Function"
+description: "Returns a specified time component from a time-based value"
+menu:
+ main:
+ parent: 'sql-functions'
+---
+
+`date_part` returns some time component from a time-based value, such as the year from a Timestamp.
+It is mostly functionally equivalent to the function [`EXTRACT`](../extract), except to maintain
+PostgreSQL compatibility, `date_part` returns values of type [`float`](../../types/float). This can
+result in a loss of precision in certain uses. Using [`EXTRACT`](../extract) is recommended instead.
+
+## Signatures
+
+{{< diagram "func-date-part.svg" >}}
+
+Parameter | Type | Description
+----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|------------
+_val_ | [`time`](../../types/time), [`timestamp`](../../types/timestamp), [`timestamp with time zone`](../../types/timestamptz), [`interval`](../../types/interval), [`date`](../../types/date) | The value from which you want to extract a component. vals of type [`date`](../../types/date) are first cast to type [`timestamp`](../../types/timestamp).
+
+### Arguments
+
+`date_part` supports multiple synonyms for most time periods.
+
+Time period | Synonyms
+------------|---------
+epoch | `EPOCH`
+millennium | `MIL`, `MILLENNIUM`, `MILLENNIA`
+century | `C`, `CENT`, `CENTURY`, `CENTURIES`
+decade | `DEC`, `DECS`, `DECADE`, `DECADES`
+year | `Y`, `YEAR`, `YEARS`, `YR`, `YRS`
+quarter | `QTR`, `QUARTER`
+month | `MON`, `MONS`, `MONTH`, `MONTHS`
+week | `W`, `WEEK`, `WEEKS`
+day | `D`, `DAY`, `DAYS`
+hour |`H`, `HR`, `HRS`, `HOUR`, `HOURS`
+minute | `M`, `MIN`, `MINS`, `MINUTE`, `MINUTES`
+second | `S`, `SEC`, `SECS`, `SECOND`, `SECONDS`
+microsecond | `US`, `USEC`, `USECS`, `USECONDS`, `MICROSECOND`, `MICROSECONDS`
+millisecond | `MS`, `MSEC`, `MSECS`, `MSECONDS`, `MILLISECOND`, `MILLISECONDS`
+day of week |`DOW`
+ISO day of week | `ISODOW`
+day of year | `DOY`
+
+### Return value
+
+`date_part` returns a [`float`](../../types/float) value.
+
+## Examples
+
+### Extract second from timestamptz
+
+```sql
+SELECT date_part('S', TIMESTAMP '2006-01-02 15:04:05.06');
+```
+```nofmt
+ date_part
+-----------
+ 5.06
+```
+
+### Extract century from date
+
+```sql
+SELECT date_part('CENTURIES', DATE '2006-01-02');
+```
+```nofmt
+ date_part
+-----------
+ 21
+```
diff --git a/doc/user/content/sql/functions/extract.md b/doc/user/content/sql/functions/extract.md
index 1699eae17d84e..6a079b7710525 100644
--- a/doc/user/content/sql/functions/extract.md
+++ b/doc/user/content/sql/functions/extract.md
@@ -12,9 +12,9 @@ menu:
{{< diagram "func-extract.svg" >}}
-Parameter | Type | Description
-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------|------------
-_val_ | [`date`](../../types/date), [`time`](../../types/time), [`timestamp`](../../types/timestamp), [`timestamp with time zone`](../../types/timestamptz) | The value from which you want to extract a component.
+Parameter | Type | Description
+----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------
+_val_ | [`date`](../../types/date), [`time`](../../types/time), [`timestamp`](../../types/timestamp), [`timestamp with time zone`](../../types/timestamptz), [`interval`](../../types/interval) | The value from which you want to extract a component.
### Arguments
@@ -42,30 +42,28 @@ decade | `DEC`, `DECS`, `DECADE`, `DECADES`
### Return value
-`EXTRACT` returns a [`float`](../../types/float) value.
+`EXTRACT` returns a [`numeric`](../../types/numeric) value.
## Examples
### Extract second from timestamptz
```sql
-SELECT EXTRACT(S FROM TIMESTAMP '2006-01-02 15:04:05.06')
-AS sec_extr;
+SELECT EXTRACT(S FROM TIMESTAMP '2006-01-02 15:04:05.06');
```
```nofmt
- sec_extr
-----------
- 5.06
+ extract
+---------
+ 5.06
```
### Extract century from date
```sql
-SELECT EXTRACT(CENTURIES FROM DATE '2006-01-02')
-AS sec_extr;
+SELECT EXTRACT(CENTURIES FROM DATE '2006-01-02');
```
```nofmt
- sec_extr
-----------
+ extract
+---------
21
```
diff --git a/doc/user/data/sql_funcs.yml b/doc/user/data/sql_funcs.yml
index e1ae98aaeaa72..b4297987b7e4e 100644
--- a/doc/user/data/sql_funcs.yml
+++ b/doc/user/data/sql_funcs.yml
@@ -403,10 +403,14 @@
description: Largest `time_component` <= `val`
url: date-trunc
- - signature: EXTRACT(extract_expr) -> float
+ - signature: EXTRACT(extract_expr) -> numeric
description: Specified time component from value
url: extract
+ - signature: 'date_part(time_component: str, val: timestamp) -> float'
+ description: Specified time component from value
+ url: date-part
+
- signature: mz_logical_timestamp() -> numeric
description: 'The logical time at which a query executes. Used for temporal filters and internal debugging.'
url: now_and_mz_logical_timestamp
diff --git a/doc/user/layouts/partials/sql-grammar/func-date-part.svg b/doc/user/layouts/partials/sql-grammar/func-date-part.svg
new file mode 100644
index 0000000000000..6f6366d46f681
--- /dev/null
+++ b/doc/user/layouts/partials/sql-grammar/func-date-part.svg
@@ -0,0 +1,195 @@
+
diff --git a/doc/user/sql-grammar/sql-grammar.bnf b/doc/user/sql-grammar/sql-grammar.bnf
index a05de602f5d60..01dea1c331936 100644
--- a/doc/user/sql-grammar/sql-grammar.bnf
+++ b/doc/user/sql-grammar/sql-grammar.bnf
@@ -346,6 +346,8 @@ func_date_trunc ::=
'date_trunc' '(' "'" ( 'microseconds' | 'milliseconds' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' | 'decade' | 'century' | 'millenium' ) "'" ',' ts_val ')'
func_extract ::=
'EXTRACT' '(' ( 'EPOCH' | 'MILLENNIUM' | 'CENTURY' | 'DECADE' | 'YEAR' | 'QUARTER' | 'MONTH' | 'WEEK' | 'DAY' | 'HOUR' | 'MINUTE' | 'SECOND' | 'MICROSECOND' | 'MILLISECOND' | 'DOW' | 'ISODOW' | 'DOY' ) 'FROM' val ')'
+func_date_part ::=
+ 'date_part' '(' "'" ( 'epoch' | 'millennium' | 'century' | 'decade' | 'year' | 'quarter' | 'month' | 'week' | 'dat' | 'hour' | 'minute' | 'second' | 'microsecond' | 'millisecond' | 'dow' | 'isodow' | 'doy' ) "'" ',' val ')'
func_length ::=
'length' '(' str (',' encoding_name)? ')'
func_substring ::=
diff --git a/src/expr/src/scalar/func.rs b/src/expr/src/scalar/func.rs
index fc2c93246aea6..7cfe2a5399311 100644
--- a/src/expr/src/scalar/func.rs
+++ b/src/expr/src/scalar/func.rs
@@ -36,7 +36,7 @@ use repr::adt::array::ArrayDimension;
use repr::adt::datetime::{DateTimeUnits, Timezone};
use repr::adt::interval::Interval;
use repr::adt::jsonb::JsonbRef;
-use repr::adt::numeric::{self, Numeric};
+use repr::adt::numeric::{self, DecimalLike, Numeric};
use repr::adt::regex::Regex;
use repr::{strconv, ColumnName, ColumnType, Datum, DatumType, Row, RowArena, ScalarType};
@@ -1363,49 +1363,100 @@ fn ascii<'a>(a: Datum<'a>) -> Datum<'a> {
}
}
-/// A timestamp with only a time component.
+/// Common set of methods for time component.
pub trait TimeLike: chrono::Timelike {
- fn extract_hour(&self) -> f64 {
- f64::from(self.hour())
+ fn extract_epoch(&self) -> T
+ where
+ T: DecimalLike,
+ {
+ T::from(self.hour() * 60 * 60 + self.minute() * 60) + self.extract_second::()
+ }
+
+ fn extract_second(&self) -> T
+ where
+ T: DecimalLike,
+ {
+ let s = T::from(self.second());
+ let ns = T::from(self.nanosecond()) / T::from(1e9);
+ s + ns
}
- fn extract_minute(&self) -> f64 {
- f64::from(self.minute())
+ fn extract_millisecond(&self) -> T
+ where
+ T: DecimalLike,
+ {
+ let s = T::from(self.second() * 1_000);
+ let ns = T::from(self.nanosecond()) / T::from(1e6);
+ s + ns
}
- fn extract_second(&self) -> f64 {
- let s = f64::from(self.second());
- let ns = f64::from(self.nanosecond()) / 1e9;
+ fn extract_microsecond(&self) -> T
+ where
+ T: DecimalLike,
+ {
+ let s = T::from(self.second() * 1_000_000);
+ let ns = T::from(self.nanosecond()) / T::from(1e3);
s + ns
}
+}
- fn extract_millisecond(&self) -> f64 {
- let s = f64::from(self.second() * 1_000);
- let ns = f64::from(self.nanosecond()) / 1e6;
- s + ns
+impl TimeLike for T where T: chrono::Timelike {}
+
+/// Common set of methods for date component.
+pub trait DateLike: chrono::Datelike {
+ fn extract_epoch(&self) -> i64 {
+ let naive_date =
+ NaiveDate::from_ymd(self.year(), self.month(), self.day()).and_hms(0, 0, 0);
+ naive_date.timestamp()
}
- fn extract_microsecond(&self) -> f64 {
- let s = f64::from(self.second() * 1_000_000);
- let ns = f64::from(self.nanosecond()) / 1e3;
- s + ns
+ fn millennium(&self) -> i32 {
+ (self.year() + if self.year() > 0 { 999 } else { -1_000 }) / 1_000
+ }
+
+ fn century(&self) -> i32 {
+ (self.year() + if self.year() > 0 { 99 } else { -100 }) / 100
+ }
+
+ fn decade(&self) -> i32 {
+ self.year().div_euclid(10)
+ }
+
+ fn quarter(&self) -> f64 {
+ (f64::from(self.month()) / 3.0).ceil()
+ }
+
+ /// Extract the iso week of the year
+ ///
+ /// Note that because isoweeks are defined in terms of January 4th, Jan 1 is only in week
+ /// 1 about half of the time
+ fn week(&self) -> u32 {
+ self.iso_week().week()
+ }
+
+ fn day_of_week(&self) -> u32 {
+ self.weekday().num_days_from_sunday()
+ }
+
+ fn iso_day_of_week(&self) -> u32 {
+ self.weekday().number_from_monday()
}
}
-impl TimeLike for T where T: chrono::Timelike {}
+impl DateLike for T where T: chrono::Datelike {}
/// A timestamp with both a date and a time component, but not necessarily a
/// timezone component.
pub trait TimestampLike:
Clone
+ PartialOrd
- + chrono::Datelike
+ std::ops::Add
+ std::ops::Sub
+ std::ops::Sub