diff --git a/docs-mintlify/cube-core/migrate-from-core/import-git-repository-via-ssh.mdx b/docs-mintlify/cube-core/migrate-from-core/import-git-repository-via-ssh.mdx index f35d78c467a34..b784b7a21baec 100644 --- a/docs-mintlify/cube-core/migrate-from-core/import-git-repository-via-ssh.mdx +++ b/docs-mintlify/cube-core/migrate-from-core/import-git-repository-via-ssh.mdx @@ -73,6 +73,41 @@ repository's webhooks. Ensure that the Git repository can push events which should trigger a build on Cube Cloud. Back in Cube Cloud, click **Connect** to test the webhook. +### Webhook format + +Cube recognizes push webhooks in the formats sent by GitLab, Bitbucket, and +Azure DevOps out of the box, so you can usually point your repository's native +webhook integration at the webhook URL without any extra configuration. + +If your Git host does not send one of these formats (for example, AWS +CodeCommit), or you want to trigger a sync from a CI/CD pipeline or manually, +send a `POST` request to the webhook URL with the `x-git-event: push` header: + +```bash +curl -X POST "" \ + -H "x-git-event: push" \ + -H "Content-Type: application/json" \ + --data '{"ref": "refs/heads/main"}' +``` + +- Use the full webhook URL exactly as shown in the UI — it includes the + `token` query parameter that authenticates the request. +- The `x-git-event: push` header is required. Requests that do not carry a + recognized Git event are ignored: Cube responds with `204`, does not pull + the repository, and the request is not shown under recent webhook requests. +- `ref` is optional and names the branch to sync (in the `refs/heads/` + form). If omitted, the production branch configured for the deployment is + used. + +Response status codes: + +| Status | Meaning | +| ------ | ----------------------------------------------------------------------------------------- | +| `201` | The event was accepted and a build was triggered | +| `204` | The event was ignored — no recognized Git event in the request, or nothing new to build | +| `403` | The `token` query parameter is missing or invalid | +| `429` | Rate limited — there is a short per-branch cooldown between webhook calls; retry in ~30 s | + ## Step 4: Connect your Database Enter your credentials to connect to your database. Check the [connecting to diff --git a/docs-mintlify/docs/explore-analyze/scheduled-tasks.mdx b/docs-mintlify/docs/explore-analyze/scheduled-tasks.mdx index ad089b93edead..b409cc4978578 100644 --- a/docs-mintlify/docs/explore-analyze/scheduled-tasks.mdx +++ b/docs-mintlify/docs/explore-analyze/scheduled-tasks.mdx @@ -1,25 +1,18 @@ --- title: Scheduled Tasks -description: Save a natural-language agent prompt and have Cube run it automatically on a schedule, producing an Analytics Chat thread for each run. +description: Save a natural-language agent prompt and have Cube run it automatically on a schedule, producing a chat thread for each run. --- - - -Scheduled Tasks are currently in preview, and the user experience may still -change. Reach out to the [Cube support team](/admin/account-billing/support) -to activate this feature for your account. - - - -Scheduled Tasks let you save a natural-language prompt for the +Scheduled Tasks let you run tasks on a schedule — or whenever you need them. +Save a natural-language prompt for the [agent](/docs/explore-analyze/analytics-chat) and have Cube run it -automatically on a schedule — or on demand. Each run produces an -[Analytics Chat](/docs/explore-analyze/analytics-chat) thread you can open -later to read the agent's answer. +automatically on a schedule or on demand. Each run produces a chat thread +you can open later to read the agent's answer. You can also ask the agent to create a +scheduled task for you from any of your chats. For example, you might schedule a task to *"every weekday at 9am, summarize -yesterday's signups and flag anything anomalous"* and review the resulting -chat each morning. +yesterday's signups, flag anything anomalous, and email me the results"* and +review the summary each morning. Scheduled Tasks are **scoped to a deployment** — each task belongs to the deployment it was created in. @@ -30,7 +23,7 @@ In the deployment sidebar, open **Scheduled** (the clock icon, below **Explore**). The page lists the deployment's tasks with their name, schedule, status, and description, and is searchable. -{/* TODO: screenshot — Scheduled Tasks list page in the deployment sidebar */} +{/* TODO: screenshot — Scheduled Tasks list page in the deployment sidebar, showing the New task dropdown */} ## Anatomy of a task @@ -62,37 +55,88 @@ the schedule fires; it is stored per task. From the list, you can: -- **Create** a new task. +- **Create** a new task. The **New task** button is a dropdown with two + options: + - **Create with agent** — opens a new Analytics Chat pre-seeded with a + message asking the agent to explain scheduled tasks and interview you + about what the task should do and when it should run. The agent then + creates the task for you (see + [Managing tasks from chat](#managing-tasks-from-chat)). + - **Set up manually** — opens the create dialog where you fill in the + task's details yourself. - **Edit** an existing task's instructions, schedule, or details. - **Enable / Pause** a scheduled task to control whether it runs on schedule. - **Run now** — trigger a one-off run immediately. This works for both manual - and scheduled tasks. + and scheduled tasks. Triggering a run shows a notification with a **View** + link straight to the run's chat thread, and the thread appears in the + Recent Chats sidebar immediately. - **Delete** a task. Deleting removes its schedule and stops all future runs. +{/* TODO: screenshot — New task dropdown with Create with agent and Set up manually options */} + +## Task detail page + +Clicking a task in the list opens its detail page. The header shows a +breadcrumb back to Scheduled Tasks, the task name, a status tag (**Manual**, +**Active**, or **Paused**), and the description, along with actions to +**Edit** (pencil), **Delete** (trash), and a primary **Run now** button. + +The page shows: + +- **History** — the task's runs, newest first. Each entry is a timestamped + link that opens that run's chat thread. Currently-executing runs + show a **Running** tag, and failed runs a **Failed** tag. The list shows + the latest 50 runs; a "Showing the latest 50 runs" note appears once the + cap is hit. +- **Instructions** — the task's prompt. +- **Repeats** — the schedule in plain language. + +{/* TODO: screenshot — task detail page with History, Instructions, and Repeats */} + ## Reading the output -Each run creates an [Analytics Chat](/docs/explore-analyze/analytics-chat) -thread containing the agent's response. Open the thread from the chat UI to -read the full answer, ask follow-up questions, or +Each run creates a chat thread containing the agent's response. Open the +thread in [Analytics Chat](/docs/explore-analyze/analytics-chat) to read the +full answer, ask follow-up questions, or [save results to a Workbook](/docs/explore-analyze/workbooks). +Scheduled-run threads are marked in the Recent Chats sidebar with a clock +icon (hover over it to see the "Scheduled task" tooltip). + +A run shows as **Running** while it executes, then completes or fails. A +failed run's thread shows a failure notice instead of an empty thread. The task runs headlessly under the security context of the user who created it, so it sees exactly the data that user can access. +### What the agent can do in a scheduled run + +Beyond querying the semantic model, the agent in a scheduled run can: + +- **Send email to workspace members** — for example, *"summarize yesterday's + signups and email the summary to me."* This requires the agent email tool + to be enabled for the workspace; recipients are restricted to workspace + members. +- **Use web search**. +- **Create and update reports, workbooks, and dashboards**, using the task + creator's permissions. + +Scheduled runs do not yet have data-model access — editing the semantic +layer is currently available only in interactive chat. + ## Managing tasks from chat You can also create and manage Scheduled Tasks conversationally in -[Analytics Chat](/docs/explore-analyze/analytics-chat). Ask the agent to -schedule, update, list, or delete tasks in plain language — for example, -*"schedule a daily summary of yesterday's signups at 9am"* or *"list my -scheduled tasks."* - -## Preview limitations - -- Scheduled Tasks must be activated for your account by the - [Cube support team](/admin/account-billing/support). -- Results are delivered only as Analytics Chat threads — there is no email or - Slack delivery. -- Missed runs (for example, while a deployment is unavailable) are not caught - up automatically. -- There is no in-app run-history view yet. +[Analytics Chat](/docs/explore-analyze/analytics-chat) — from any chat, not +just ones started with **Create with agent** (that menu option simply opens +a chat pre-seeded for this flow). Ask the agent to schedule, update, list, +or delete tasks in plain language — for example, *"schedule a daily summary +of yesterday's signups at 9am"* or *"list my scheduled tasks."* + +The agent's actions render in the chat as labeled steps with a clock icon — +*"Creating scheduled task…"* / *"Created scheduled task"*, *"Listed +scheduled tasks"*, *"Updated scheduled task"*, and *"Deleted scheduled +task"*. + +The agent manages task definitions: when listing tasks, it can report each +task's id, name, description, schedule, timezone, and enabled state. A +task's run history lives on its [detail page](#task-detail-page). diff --git a/docs-mintlify/docs/getting-started/migrate-from-core/import-git-repository-via-ssh.mdx b/docs-mintlify/docs/getting-started/migrate-from-core/import-git-repository-via-ssh.mdx index f35d78c467a34..b784b7a21baec 100644 --- a/docs-mintlify/docs/getting-started/migrate-from-core/import-git-repository-via-ssh.mdx +++ b/docs-mintlify/docs/getting-started/migrate-from-core/import-git-repository-via-ssh.mdx @@ -73,6 +73,41 @@ repository's webhooks. Ensure that the Git repository can push events which should trigger a build on Cube Cloud. Back in Cube Cloud, click **Connect** to test the webhook. +### Webhook format + +Cube recognizes push webhooks in the formats sent by GitLab, Bitbucket, and +Azure DevOps out of the box, so you can usually point your repository's native +webhook integration at the webhook URL without any extra configuration. + +If your Git host does not send one of these formats (for example, AWS +CodeCommit), or you want to trigger a sync from a CI/CD pipeline or manually, +send a `POST` request to the webhook URL with the `x-git-event: push` header: + +```bash +curl -X POST "" \ + -H "x-git-event: push" \ + -H "Content-Type: application/json" \ + --data '{"ref": "refs/heads/main"}' +``` + +- Use the full webhook URL exactly as shown in the UI — it includes the + `token` query parameter that authenticates the request. +- The `x-git-event: push` header is required. Requests that do not carry a + recognized Git event are ignored: Cube responds with `204`, does not pull + the repository, and the request is not shown under recent webhook requests. +- `ref` is optional and names the branch to sync (in the `refs/heads/` + form). If omitted, the production branch configured for the deployment is + used. + +Response status codes: + +| Status | Meaning | +| ------ | ----------------------------------------------------------------------------------------- | +| `201` | The event was accepted and a build was triggered | +| `204` | The event was ignored — no recognized Git event in the request, or nothing new to build | +| `403` | The `token` query parameter is missing or invalid | +| `429` | Rate limited — there is a short per-branch cooldown between webhook calls; retry in ~30 s | + ## Step 4: Connect your Database Enter your credentials to connect to your database. Check the [connecting to diff --git a/packages/cubejs-databricks-jdbc-driver/src/DatabricksQuery.ts b/packages/cubejs-databricks-jdbc-driver/src/DatabricksQuery.ts index a01396914e7f5..17c7a14cf3ec0 100644 --- a/packages/cubejs-databricks-jdbc-driver/src/DatabricksQuery.ts +++ b/packages/cubejs-databricks-jdbc-driver/src/DatabricksQuery.ts @@ -168,6 +168,7 @@ export class DatabricksQuery extends BaseQuery { public sqlTemplates() { const templates = super.sqlTemplates(); templates.functions.CURRENTDATE = 'CURRENT_DATE'; + templates.functions.UTCTIMESTAMP = 'TO_UTC_TIMESTAMP(CURRENT_TIMESTAMP(), CURRENT_TIMEZONE())'; templates.functions.DATETRUNC = 'DATE_TRUNC({{ args_concat }})'; templates.functions.DATEPART = 'DATE_PART({{ args_concat }})'; templates.functions.BTRIM = 'TRIM({% if args[1] is defined %}{{ args[1] }} FROM {% endif %}{{ args[0] }})'; diff --git a/packages/cubejs-druid-driver/src/DruidQuery.ts b/packages/cubejs-druid-driver/src/DruidQuery.ts index a42a6d2c02671..8b3d84a1c8af3 100644 --- a/packages/cubejs-druid-driver/src/DruidQuery.ts +++ b/packages/cubejs-druid-driver/src/DruidQuery.ts @@ -60,6 +60,9 @@ export class DruidQuery extends BaseQuery { delete templates.expressions.like_escape; templates.filters.like_pattern = 'CONCAT({% if start_wild %}\'%\'{% else %}\'\'{% endif %}, LOWER({{ value }}), {% if end_wild %}\'%\'{% else %}\'\'{% endif %})'; templates.tesseract.ilike = 'LOWER({{ expr }}) {% if negated %}NOT {% endif %}LIKE {{ pattern }}'; + // Druid evaluates CURRENT_TIMESTAMP in the sqlTimeZone query context, which + // defaults to UTC — assumes the connection does not override sqlTimeZone + templates.functions.UTCTIMESTAMP = 'CURRENT_TIMESTAMP'; return templates; } diff --git a/packages/cubejs-duckdb-driver/src/DuckDBQuery.ts b/packages/cubejs-duckdb-driver/src/DuckDBQuery.ts index 27ed2bb535df2..567374a0b993c 100644 --- a/packages/cubejs-duckdb-driver/src/DuckDBQuery.ts +++ b/packages/cubejs-duckdb-driver/src/DuckDBQuery.ts @@ -61,6 +61,9 @@ export class DuckDBQuery extends BaseQuery { public sqlTemplates() { const templates = super.sqlTemplates(); templates.functions.DATETRUNC = 'DATE_TRUNC({{ args_concat }})'; + // AT TIME ZONE on TIMESTAMPTZ yields a naive TIMESTAMP in UTC (requires the ICU + // extension, which is bundled and autoloaded in the DuckDB builds used by the driver) + templates.functions.UTCTIMESTAMP = '(NOW() AT TIME ZONE \'UTC\')'; templates.functions.LEAST = 'LEAST({{ args_concat }})'; templates.functions.GREATEST = 'GREATEST({{ args_concat }})'; templates.functions.STRING_AGG = 'STRING_AGG({% if distinct %}DISTINCT {% endif %}{{ args[0] }}, COALESCE({{ args[1] }}, \'\'))'; diff --git a/packages/cubejs-pinot-driver/src/PinotQuery.ts b/packages/cubejs-pinot-driver/src/PinotQuery.ts index 204bce55d46b4..e44f20d1798cb 100644 --- a/packages/cubejs-pinot-driver/src/PinotQuery.ts +++ b/packages/cubejs-pinot-driver/src/PinotQuery.ts @@ -141,6 +141,9 @@ export class PinotQuery extends BaseQuery { public sqlTemplates() { const templates = super.sqlTemplates(); templates.functions.DATETRUNC = 'DATE_TRUNC({{ args_concat }})'; + // NOW() returns the current epoch millis (inherently UTC), matching the + // epoch-millis representation produced by the timestamp_literal template + templates.functions.UTCTIMESTAMP = 'NOW()'; templates.functions.STRING_AGG = 'LISTAGG({% if distinct %}DISTINCT {% endif %}{{ args_concat }})'; templates.statements.select = 'SELECT {{ select_concat | map(attribute=\'aliased\') | join(\', \') }} \n' + 'FROM (\n {{ from }}\n) AS {{ from_alias }} \n' + diff --git a/packages/cubejs-schema-compiler/src/adapter/BigqueryQuery.ts b/packages/cubejs-schema-compiler/src/adapter/BigqueryQuery.ts index 83834465db018..bae84bd3c51ab 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BigqueryQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/BigqueryQuery.ts @@ -332,6 +332,7 @@ export class BigqueryQuery extends BaseQuery { // DATEADD is being rewritten to DATE_ADD templates.functions.DATE_ADD = 'DATETIME_ADD(DATETIME({{ args[0] }}), INTERVAL {{ interval }} {{ date_part }})'; templates.functions.CURRENTDATE = 'CURRENT_DATE'; + templates.functions.UTCTIMESTAMP = 'CURRENT_TIMESTAMP()'; delete templates.functions.TO_CHAR; delete templates.functions.PERCENTILECONT; templates.expressions.binary = '{% if op == \'%\' %}MOD({{ left }}, {{ right }}){% else %}({{ left }} {{ op }} {{ right }}){% endif %}'; diff --git a/packages/cubejs-schema-compiler/src/adapter/ClickHouseQuery.ts b/packages/cubejs-schema-compiler/src/adapter/ClickHouseQuery.ts index 99b4f100c45bc..1f7a4113e6d6a 100644 --- a/packages/cubejs-schema-compiler/src/adapter/ClickHouseQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/ClickHouseQuery.ts @@ -263,6 +263,7 @@ export class ClickHouseQuery extends BaseQuery { public sqlTemplates() { const templates = super.sqlTemplates(); templates.functions.DATETRUNC = 'DATE_TRUNC({{ args_concat }})'; + templates.functions.UTCTIMESTAMP = 'now(\'UTC\')'; templates.functions.STRING_AGG = 'arrayStringConcat(group{% if distinct %}Uniq{% endif %}Array({{ args[0] }}), {{ args[1] }})'; // TODO: Introduce additional filter in jinja? or parseDateTimeBestEffort? // https://github.com/ClickHouse/ClickHouse/issues/19351 diff --git a/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts b/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts index 5021ef8b4c2c1..48915ddf41780 100644 --- a/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/MssqlQuery.ts @@ -268,6 +268,7 @@ export class MssqlQuery extends BaseQuery { const templates = super.sqlTemplates(); templates.functions.LEAST = 'LEAST({{ args_concat }})'; templates.functions.GREATEST = 'GREATEST({{ args_concat }})'; + templates.functions.UTCTIMESTAMP = 'GETUTCDATE()'; // MSSQL ROUND requires 2 arguments: ROUND(number, length) templates.functions.ROUND = 'ROUND({{ args_concat }}{% if args | length < 2 %}, 0{% endif %})'; // NOTE: MSSQL does not support DISTINCT clause. No workaround is available diff --git a/packages/cubejs-schema-compiler/src/adapter/MysqlQuery.ts b/packages/cubejs-schema-compiler/src/adapter/MysqlQuery.ts index afb78634d161b..ac45aa5fdd3cc 100644 --- a/packages/cubejs-schema-compiler/src/adapter/MysqlQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/MysqlQuery.ts @@ -186,6 +186,7 @@ export class MysqlQuery extends BaseQuery { public sqlTemplates() { const templates = super.sqlTemplates(); templates.functions.STRING_AGG = 'GROUP_CONCAT({% if distinct %}DISTINCT {% endif %}{{ args[0] }} SEPARATOR {{ args[1] }})'; + templates.functions.UTCTIMESTAMP = 'UTC_TIMESTAMP()'; // PERCENTILE_CONT works but requires PARTITION BY delete templates.functions.PERCENTILECONT; templates.quotes.identifiers = '`'; diff --git a/packages/cubejs-schema-compiler/src/adapter/OracleQuery.ts b/packages/cubejs-schema-compiler/src/adapter/OracleQuery.ts index 43fb1c0f89f88..789efef445a80 100644 --- a/packages/cubejs-schema-compiler/src/adapter/OracleQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/OracleQuery.ts @@ -215,6 +215,7 @@ export class OracleQuery extends BaseQuery { public sqlTemplates() { const templates = super.sqlTemplates(); + templates.functions.UTCTIMESTAMP = 'SYS_EXTRACT_UTC(SYSTIMESTAMP)'; // Oracle forbids `AS` before a table/subquery alias. templates.expressions.query_aliased = '{{ query }} {{ quoted_alias }}'; // Oracle does not support positional GROUP BY — group by expressions. diff --git a/packages/cubejs-schema-compiler/src/adapter/PostgresQuery.ts b/packages/cubejs-schema-compiler/src/adapter/PostgresQuery.ts index f6eea59be725d..5ebd05f05e8f1 100644 --- a/packages/cubejs-schema-compiler/src/adapter/PostgresQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/PostgresQuery.ts @@ -86,6 +86,7 @@ export class PostgresQuery extends BaseQuery { templates.functions.LEAST = 'LEAST({{ args_concat }})'; templates.functions.GREATEST = 'GREATEST({{ args_concat }})'; templates.functions.NOW = 'NOW({{ args_concat }})'; + templates.functions.UTCTIMESTAMP = '(NOW() AT TIME ZONE \'UTC\')'; // DATEADD is being rewritten to DATE_ADD // templates.functions.DATEADD = '({{ args[2] }} + \'{{ interval }} {{ date_part }}\'::interval)'; // TODO: is DATEDIFF expr worth documenting? diff --git a/packages/cubejs-schema-compiler/src/adapter/PrestodbQuery.ts b/packages/cubejs-schema-compiler/src/adapter/PrestodbQuery.ts index bcd92b71391aa..9e576be5554a1 100644 --- a/packages/cubejs-schema-compiler/src/adapter/PrestodbQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/PrestodbQuery.ts @@ -141,6 +141,7 @@ export class PrestodbQuery extends BaseQuery { templates.functions.DATEPART = 'DATE_PART({{ args_concat }})'; templates.functions.DATEDIFF = 'DATE_DIFF(\'{{ date_part }}\', {{ args[1] }}, {{ args[2] }})'; templates.functions.CURRENTDATE = 'CURRENT_DATE'; + templates.functions.UTCTIMESTAMP = 'CAST(NOW() AT TIME ZONE \'UTC\' AS TIMESTAMP)'; templates.functions.TRUNC = 'TRUNCATE({{ args_concat }})'; templates.functions.STRING_AGG = 'ARRAY_JOIN(ARRAY_AGG({% if distinct %}DISTINCT {% endif %}{{ args[0] }}), COALESCE({{ args[1] }}, \'\'))'; delete templates.functions.PERCENTILECONT; diff --git a/packages/cubejs-schema-compiler/src/adapter/RedshiftQuery.ts b/packages/cubejs-schema-compiler/src/adapter/RedshiftQuery.ts index cf56e1a7fff7a..e1063e0dd6597 100644 --- a/packages/cubejs-schema-compiler/src/adapter/RedshiftQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/RedshiftQuery.ts @@ -84,6 +84,9 @@ export class RedshiftQuery extends PostgresQuery { public sqlTemplates() { const templates = super.sqlTemplates(); templates.functions.DLOG10 = 'LOG(10, {{ args_concat }})'; + // Redshift clusters always run in UTC and GETDATE() is supported on compute + // nodes, unlike NOW(), which is a leader node–only function. + templates.functions.UTCTIMESTAMP = 'GETDATE()'; templates.functions.DATEDIFF = 'DATEDIFF({{ date_part }}, {{ args[1] }}, {{ args[2] }})'; templates.functions.STRING_AGG = 'LISTAGG({% if distinct %}DISTINCT {% endif %}{{ args_concat }})'; templates.statements.time_series_select = 'SELECT dates.f::timestamp date_from, dates.t::timestamp date_to \n' + diff --git a/packages/cubejs-schema-compiler/src/adapter/SnowflakeQuery.ts b/packages/cubejs-schema-compiler/src/adapter/SnowflakeQuery.ts index b945fbb838bb1..746bf0fc3cbf7 100644 --- a/packages/cubejs-schema-compiler/src/adapter/SnowflakeQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/SnowflakeQuery.ts @@ -108,6 +108,7 @@ export class SnowflakeQuery extends BaseQuery { templates.functions.DATEPART = 'DATE_PART({{ args_concat }})'; templates.functions.CURRENTDATE = 'CURRENT_DATE'; templates.functions.NOW = 'CURRENT_TIMESTAMP'; + templates.functions.UTCTIMESTAMP = 'SYSDATE()'; templates.functions.LOG = 'LOG({% if args[1] is undefined %}10, {% endif %}{{ args_concat }})'; templates.functions.DLOG10 = 'LOG(10, {{ args_concat }})'; templates.functions.CHARACTERLENGTH = 'LENGTH({{ args[0] }})'; diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/member-expression.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/member-expression.test.ts index df16364c3e15d..c233ab94ed3af 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/member-expression.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/member-expression.test.ts @@ -310,6 +310,26 @@ views: }, [{ count: 1, city: 'New York', cubejoinfield: 'NULL' }, { count: 1, city: 'New York', cubejoinfield: 'NULL' }])); + + it('dimension-only measure expression over multiple dimensions of the same cube', async () => runQueryTest({ + measures: [ + { + // eslint-disable-next-line no-new-func + expression: new Function( + 'customers', + // eslint-disable-next-line no-template-curly-in-string + 'return `SUM(CASE WHEN ${customers.state} = ${customers.city} THEN 1 ELSE 0 END)`' + ), + // eslint-disable-next-line no-template-curly-in-string + definition: 'SUM(CASE WHEN ${customers.state} = ${customers.city} THEN 1 ELSE 0 END)', + expressionName: 'same_state_city_count', + cubeName: 'customers', + }, + ], + }, + + [{ same_state_city_count: '0' }])); + if (getEnv('nativeSqlPlanner')) { it('member expression multi stage', async () => runQueryTest({ measures: [ diff --git a/packages/cubejs-testing/test/__snapshots__/smoke-cubesql.test.ts.snap b/packages/cubejs-testing/test/__snapshots__/smoke-cubesql.test.ts.snap index 38e9a467edb0d..0dc2826c656f3 100644 --- a/packages/cubejs-testing/test/__snapshots__/smoke-cubesql.test.ts.snap +++ b/packages/cubejs-testing/test/__snapshots__/smoke-cubesql.test.ts.snap @@ -545,6 +545,14 @@ Array [ ] `; +exports[`SQL API Postgres (Data) current_timestamp subquery filter push down: current_timestamp_push_down 1`] = ` +Array [ + Object { + "cn": "5", + }, +] +`; + exports[`SQL API Postgres (Data) date/string measures in view: date case 1`] = ` Array [ Object { diff --git a/packages/cubejs-testing/test/smoke-cubesql.test.ts b/packages/cubejs-testing/test/smoke-cubesql.test.ts index 389454d969d8b..aba83d17c8ba1 100644 --- a/packages/cubejs-testing/test/smoke-cubesql.test.ts +++ b/packages/cubejs-testing/test/smoke-cubesql.test.ts @@ -530,6 +530,18 @@ describe('SQL API', () => { expect(res.rows).toMatchSnapshot('powerbi_min_max_push_down'); }); + test('current_timestamp subquery filter push down', async () => { + // CURRENT_TIMESTAMP-based bounds wrapped in scalar subqueries force + // SQL push down and require the functions/UTCTIMESTAMP template. + // All fixture rows are in the past, so the result is stable over time. + const res = await connection.query(` + SELECT COUNT(*) as cn + FROM "public"."Orders" "orders" + WHERE ("orders"."createdAt" < ((SELECT DATE_TRUNC('year', DATE_TRUNC('day', CURRENT_TIMESTAMP))))) + `); + expect(res.rows).toMatchSnapshot('current_timestamp_push_down'); + }); + test('no limit for non matching count push down', async () => { const res = await connection.query(` select diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/member_expression_symbol.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/member_expression_symbol.rs index 672143a489d8c..1f3cdb7ca78a5 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/member_expression_symbol.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/member_expression_symbol.rs @@ -182,9 +182,12 @@ impl MemberExpressionSymbol { if childs.iter().any(|s| !s.is_dimension()) { Ok(None) } else { + // Single member expression can reference multiple dimensions from + // the same cube let cube_names = childs .into_iter() .map(|child| child.cube_name()) + .unique() .collect_vec(); Ok(Some(cube_names)) } diff --git a/rust/cubesql/cubesql/src/compile/test/mod.rs b/rust/cubesql/cubesql/src/compile/test/mod.rs index 1e25a9da0a27d..b65f633628f0b 100644 --- a/rust/cubesql/cubesql/src/compile/test/mod.rs +++ b/rust/cubesql/cubesql/src/compile/test/mod.rs @@ -694,6 +694,7 @@ pub fn sql_generator( ("functions/DATEDIFF".to_string(), "DATEDIFF({{ date_part }}, {{ args[1] }}, {{ args[2] }})".to_string()), ("functions/CURRENTDATE".to_string(), "CURRENT_DATE({{ args_concat }})".to_string()), ("functions/NOW".to_string(), "NOW({{ args_concat }})".to_string()), + ("functions/UTCTIMESTAMP".to_string(), "(NOW() AT TIME ZONE 'UTC')".to_string()), ("functions/DATE_ADD".to_string(), "DATE_ADD({{ args_concat }})".to_string()), ("functions/CONCAT".to_string(), "CONCAT({{ args_concat }})".to_string()), ("functions/DATE".to_string(), "DATE({{ args_concat }})".to_string()), diff --git a/rust/cubesql/cubesql/src/compile/test/test_wrapper.rs b/rust/cubesql/cubesql/src/compile/test/test_wrapper.rs index d08f94d71c2af..6051db3d686e2 100644 --- a/rust/cubesql/cubesql/src/compile/test/test_wrapper.rs +++ b/rust/cubesql/cubesql/src/compile/test/test_wrapper.rs @@ -285,6 +285,32 @@ async fn test_simple_subquery_wrapper_filter_empty_source() { //println!("phys plan {:?}", physical_plan); } +#[tokio::test] +async fn test_wrapper_filter_subquery_current_timestamp() { + if !Rewriter::sql_push_down_enabled() { + return; + } + init_testing_logger(); + + let query_plan = convert_select_to_query_plan( + r#" + SELECT customer_gender, AVG(avgPrice) mp + FROM KibanaSampleDataEcommerce a + WHERE order_date >= (SELECT DATE_TRUNC('year', DATE_TRUNC('day', CURRENT_TIMESTAMP))) + GROUP BY 1 + "# + .to_string(), + DatabaseProtocol::PostgreSQL, + ) + .await; + + let logical_plan = query_plan.as_logical_plan(); + let sql = logical_plan.find_cube_scan_wrapped_sql().wrapped_sql.sql; + assert!(sql.contains("NOW() AT TIME ZONE 'UTC'")); + + let _physical_plan = query_plan.as_physical_plan().await.unwrap(); +} + #[tokio::test] async fn test_simple_subquery_wrapper_projection_aggregate_empty_source() { if !Rewriter::sql_push_down_enabled() {