diff --git a/docs/pages/guides/recipes.mdx b/docs/pages/guides/recipes.mdx
index c7bac95261abd..db4008b7f9399 100644
--- a/docs/pages/guides/recipes.mdx
+++ b/docs/pages/guides/recipes.mdx
@@ -10,6 +10,7 @@ These recipes will show you the best practices of using Cube.
### Analytics
- [Calculating daily, weekly, monthly active users](/guides/recipes/analytics/active-users)
+- [Calculating the internal rate of return (XIRR)](/guides/recipes/analytics/xirr)
- [Implementing event analytics](/guides/recipes/analytics/event-analytics)
- [Implementing funnel analysis](/guides/recipes/analytics/funnels)
- [Implementing retention analysis & cohorts](/guides/recipes/analytics/cohort-retention)
diff --git a/docs/pages/guides/recipes/analytics/_meta.js b/docs/pages/guides/recipes/analytics/_meta.js
index a5642e6b584a8..ab5c197514aed 100644
--- a/docs/pages/guides/recipes/analytics/_meta.js
+++ b/docs/pages/guides/recipes/analytics/_meta.js
@@ -1,5 +1,6 @@
module.exports = {
"active-users": "Daily, Weekly, Monthly Active Users (DAU, WAU, MAU)",
+ "xirr": "XIRR",
"event-analytics": "Implementing event analytics",
"cohort-retention": "Implementing retention analysis & cohorts",
"funnels": "Implementing Funnel Analysis"
diff --git a/docs/pages/guides/recipes/analytics/xirr.mdx b/docs/pages/guides/recipes/analytics/xirr.mdx
new file mode 100644
index 0000000000000..cd89ff901d225
--- /dev/null
+++ b/docs/pages/guides/recipes/analytics/xirr.mdx
@@ -0,0 +1,202 @@
+# Calculating the internal rate of return (XIRR)
+
+## Use case
+
+We'd like to calculate the internal rate of return (XIRR) for a schedule of cash
+flows that is not necessarily periodic.
+
+## Data modeling
+
+XIRR calculation is enabled by the `XIRR` function, implemented in [SQL API][ref-sql-api],
+[DAX API][ref-dax-api], and [MDX API][ref-mdx-api]. It means that queries to any of these
+APIs can use the this function.
+
+The `XIRR` function is also implemented in Cube Store, meaning that queries to the SQL API
+or the [REST API][ref-rest-api] that hit pre-aggregations can also use this function.
+That function would need to be used in a measure that makes use of [multi-stage
+calculations][ref-multi-stage-calculations].
+
+
+
+Consequently, queries that don't hit pre-aggregations would fail with the following error:
+`function xirr(numeric, date) does not exist`.
+
+
+
+
+
+Multi-stage calculations are powered by Tesseract, the [next-generation data modeling
+engine][link-tesseract]. Tesseract is currently in preview. Use the
+`CUBEJS_TESSERACT_SQL_PLANNER` environment variable to enable it.
+
+
+
+Consider the following data model:
+
+
+
+```yaml
+cubes:
+ - name: payments
+ sql: >
+ SELECT '2014-01-01'::date AS date, -10000.0 AS payment UNION ALL
+ SELECT '2014-03-01'::date AS date, 2750.0 AS payment UNION ALL
+ SELECT '2014-10-30'::date AS date, 4250.0 AS payment UNION ALL
+ SELECT '2015-02-15'::date AS date, 3250.0 AS payment UNION ALL
+ SELECT '2015-04-01'::date AS date, 2750.0 AS payment
+
+ dimensions:
+ - name: date
+ sql: date
+ type: time
+
+ - name: payment
+ sql: payment
+ type: number
+
+ # Everything below this line is only needed for querying
+ # pre-aggregations in Cube Store
+ dimensions:
+ - name: date__day
+ sql: "{date.day}"
+ type: time
+
+ measures:
+ - name: total_payments
+ sql: payment
+ type: sum
+
+ - name: xirr
+ multi_stage: true
+ sql: "XIRR({total_payments}, {date__day})"
+ type: number_agg
+ add_group_by:
+ - date__day
+
+ pre_aggregations:
+ - name: main_xirr
+ measures:
+ - total_payments
+ time_dimension: date
+ granularity: day
+```
+
+```javascript
+cube(`payments`, {
+ sql: `
+ SELECT '2014-01-01'::date AS date, -10000.0 AS payment UNION ALL
+ SELECT '2014-03-01'::date AS date, 2750.0 AS payment UNION ALL
+ SELECT '2014-10-30'::date AS date, 4250.0 AS payment UNION ALL
+ SELECT '2015-02-15'::date AS date, 3250.0 AS payment UNION ALL
+ SELECT '2015-04-01'::date AS date, 2750.0 AS payment
+ `,
+
+ dimensions: {
+ date: {
+ sql: `date`,
+ type: `time`
+ },
+
+ payment: {
+ sql: `payment`,
+ type: `number`
+ },
+
+ // Everything below this line is only needed for querying
+ // pre-aggregations in Cube Store
+ date__day: {
+ sql: `${CUBE.date.day}`,
+ type: `time`
+ }
+ },
+
+ measures: {
+ total_payments: {
+ sql: `payment`,
+ type: `sum`
+ },
+
+ xirr: {
+ multi_stage: true,
+ sql: `XIRR(${CUBE.total_payments}, ${CUBE.date__day})`,
+ type: `number_agg`,
+ add_group_by: [
+ date__day
+ ]
+ }
+ },
+
+ pre_aggregations: {
+ main_xirr: {
+ measures: [
+ total_payments
+ ],
+ time_dimension: date,
+ granularity: `day`
+ }
+ }
+})
+```
+
+
+
+## Query
+
+### DAX API
+
+You can use the `XIRR` function in DAX.
+
+### SQL API
+
+[Query with post-processing][ref-query-wpp] in the SQL API:
+
+```sql
+SELECT
+ XIRR(payment, date) AS xirr
+FROM (
+ SELECT
+ DATE_TRUNC('DAY', date) AS date,
+ SUM(payment) AS payment
+ FROM payments
+ GROUP BY 1
+) AS payments;
+```
+
+[Regular query][ref-query-regular] in the SQL API that hits a pre-aggregation in Cube Store:
+
+```sql
+SELECT MEASURE(xirr) AS xirr
+FROM payments;
+```
+
+### REST API
+
+Regular query in the REST API that hits a pre-aggregation in Cube Store:
+
+```json
+{
+ "measures": [
+ "payments.xirr"
+ ]
+}
+```
+
+## Result
+
+All queries above would yield the same result:
+
+```
+ xirr
+--------------------
+ 0.3748585976775555
+```
+
+
+[ref-sql-api]: /product/apis-integrations/sql-api/reference#custom-functions
+[ref-dax-api]: /product/apis-integrations/dax-api/reference#financial-functions
+[ref-mdx-api]: /product/apis-integrations/mdx-api
+[ref-rest-api]: /product/apis-integrations/rest-api
+[ref-query-wpp]: /product/apis-integrations/queries#query-with-post-processing
+[ref-query-regular]: /product/apis-integrations/queries#regular-query
+[link-tesseract]: https://cube.dev/blog/introducing-next-generation-data-modeling-engine
+[ref-multi-stage-calculations]: /product/data-modeling/concepts/multi-stage-calculations
\ No newline at end of file
diff --git a/docs/pages/product/apis-integrations/dax-api/reference.mdx b/docs/pages/product/apis-integrations/dax-api/reference.mdx
index ddaf9a219610e..8800ed1df1f0d 100644
--- a/docs/pages/product/apis-integrations/dax-api/reference.mdx
+++ b/docs/pages/product/apis-integrations/dax-api/reference.mdx
@@ -100,7 +100,15 @@ of the DAX documentation.
-No financial functions currently supported.
+| Function | Unsupported features | Caveats |
+| --- | --- | --- |
+| [`XIRR`](https://learn.microsoft.com/en-us/dax/xirr-function-dax) | — | — |
+
+
+
+See the [XIRR recipe](/guides/recipes/analytics/xirr) for more details.
+
+
### INFO functions
diff --git a/docs/pages/product/apis-integrations/sql-api/reference.mdx b/docs/pages/product/apis-integrations/sql-api/reference.mdx
index 28f7d57747749..aeb9235ac3453 100644
--- a/docs/pages/product/apis-integrations/sql-api/reference.mdx
+++ b/docs/pages/product/apis-integrations/sql-api/reference.mdx
@@ -150,7 +150,8 @@ SHOW ALL;
## SQL functions and operators
SQL API currently implements a subset of functions and operators [supported by
-PostgreSQL][link-postgres-funcs].
+PostgreSQL][link-postgres-funcs]. Additionally, it supports a few [custom
+functions](#custom-functions).
### Comparison operators
@@ -407,12 +408,24 @@ of the PostgreSQL documentation.
| `IN` | Returns `TRUE` if a left-side value matches **any** of right-side values | ✅ Yes | ✅ Outer
✅ Inner (selections)
✅ Inner (projections) |
| `NOT IN` | Returns `TRUE` if a left-side value matches **none** of right-side values | ✅ Yes | ✅ Outer
✅ Inner (selections)
✅ Inner (projections) |
+### Custom functions
+
+| Function | Description |
+| --- | --- |
+| `XIRR` | Calculates the [internal rate of return][link-xirr] for a series of cash flows |
+
+
+
+See the [XIRR recipe](/guides/recipes/analytics/xirr) for more details.
+
+
+
[ref-qpd]: /product/apis-integrations/sql-api/query-format#query-pushdown
[ref-qpp]: /product/apis-integrations/sql-api/query-format#query-post-processing
[ref-sql-api]: /product/apis-integrations/sql-api
[ref-sql-api-aggregate-functions]: /product/apis-integrations/sql-api/query-format#aggregate-functions
-
[link-postgres-funcs]: https://www.postgresql.org/docs/current/functions.html
[link-github-sql-api]: https://github.com/cube-js/cube/issues?q=is%3Aopen+is%3Aissue+label%3Aapi%3Asql
-[link-github-new-sql-api-issue]: https://github.com/cube-js/cube/issues/new?assignees=&labels=&projects=&template=sql_api_query_issue.md&title=
\ No newline at end of file
+[link-github-new-sql-api-issue]: https://github.com/cube-js/cube/issues/new?assignees=&labels=&projects=&template=sql_api_query_issue.md&title=
+[link-xirr]: https://support.microsoft.com/en-us/office/xirr-function-de1242ec-6477-445b-b11b-a303ad9adc9d
\ No newline at end of file
diff --git a/docs/pages/reference/data-model/types-and-formats.mdx b/docs/pages/reference/data-model/types-and-formats.mdx
index ff6099f573ea9..936e553fa5d4f 100644
--- a/docs/pages/reference/data-model/types-and-formats.mdx
+++ b/docs/pages/reference/data-model/types-and-formats.mdx
@@ -1,9 +1,3 @@
----
-redirect_from:
- - /types-and-formats
- - /schema/reference/types-and-formats
----
-
# Types and Formats
## Measure Types
@@ -139,7 +133,7 @@ cubes:
### `number`
-Type `number` is usually used, when performing
+The `number` type is usually used, when performing arithmetic operations on
arithmetic operations on measures. [Learn more about Calculated
Measures][ref-schema-ref-calc-measures].
@@ -207,10 +201,59 @@ cubes:
+### `number_agg`
+
+The `number_agg` type is used when you need to write a custom aggregate function
+in the `sql` parameter that isn't covered by standard measure types like `sum`,
+`avg`, `min`, etc.
+
+
+
+The `number_agg` type is only available in Tesseract, the [next-generation data modeling
+engine][link-tesseract]. Tesseract is currently in preview. Use the
+`CUBEJS_TESSERACT_SQL_PLANNER` environment variable to enable it.
+
+
+
+Unlike the `number` type which is used for calculations on measures (e.g.,
+`SUM(revenue) / COUNT(*)`), `number_agg` indicates that the `sql` parameter contains
+a direct SQL aggregate function.
+
+The `sql` parameter is required and must include a custom aggregate function that returns a numeric
+value.
+
+
+
+```javascript
+cube(`orders`, {
+ // ...
+
+ measures: {
+ median_price: {
+ sql: `PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY price)`,
+ type: `number_agg`
+ }
+ }
+})
+```
+
+```yaml
+cubes:
+ - name: orders
+ # ...
+
+ measures:
+ - name: median_price
+ sql: "PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY price)"
+ type: number_agg
+```
+
+
+
### `count`
-Performs a table count, similar to SQL’s `COUNT` function. However, unlike
-writing raw SQL, Cube will properly calculate counts even if your query’s
+Performs a table count, similar to SQL's `COUNT` function. However, unlike
+writing raw SQL, Cube will properly calculate counts even if your query's
joins will produce row multiplication.
You do not need to include a `sql` parameter for this type.
@@ -254,7 +297,7 @@ cubes:
### `count_distinct`
-Calculates the number of distinct values in a given field. It makes use of SQL’s
+Calculates the number of distinct values in a given field. It makes use of SQL's
`COUNT DISTINCT` function.
The `sql` parameter is required and must include any valid SQL expression
@@ -332,9 +375,9 @@ cubes:
### `sum`
-Adds up the values in a given field. It is similar to SQL’s `SUM` function.
+Adds up the values in a given field. It is similar to SQL's `SUM` function.
However, unlike writing raw SQL, Cube will properly calculate sums even if your
-query’s joins will result in row duplication.
+query's joins will result in row duplication.
The `sql` parameter is required and must include any valid SQL expression
of the numeric type (without an aggregate function).
@@ -387,9 +430,9 @@ cubes:
### `avg`
-Averages the values in a given field. It is similar to SQL’s AVG function.
+Averages the values in a given field. It is similar to SQL's AVG function.
However, unlike writing raw SQL, Cube will properly calculate averages even if
-your query’s joins will result in row duplication.
+your query's joins will result in row duplication.
The `sql` parameter is required and must include any valid SQL expression
of the numeric type (without an aggregate function).
@@ -494,7 +537,7 @@ cubes:
## Measure Formats
-When creating a **measure** you can explicitly define the format you’d like to
+When creating a **measure** you can explicitly define the format you'd like to
see as output.
### `percent`