From c3a570989fa26cd14b00205e6fc62645c7dc0a6d Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Thu, 23 Apr 2026 15:44:25 +0200 Subject: [PATCH 1/5] perf(postgres-driver): Fast date, timestamp/tz parsers (#10737) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Microbenchmark (Node 22, Apple Silicon; `moment` baseline = `moment.utc(val).format(moment.HTML5_FMT.DATETIME_LOCAL_MS)`). ## Warm (N=2M, with warmup) Steady-state throughput after V8's JIT has fully optimized both paths — representative of large result sets. | input | moment | new | speedup | |-------|--------|-----|---------| | DATE | ~2770 ns | ~6 ns | ~450x | | TIMESTAMP (no frac) | ~4440 ns | ~28 ns | ~160x | | TIMESTAMP (ms) | ~4970 ns | ~48 ns | ~105x | | TIMESTAMP (us → ms trunc) | ~4920 ns | ~41 ns | ~120x | | TIMESTAMPTZ (+00) | ~4850 ns | ~35 ns | ~135x | | TIMESTAMPTZ (ms +00) | ~5360 ns | ~57 ns | ~95x | | TIMESTAMPTZ (us +00) | ~5250 ns | ~62 ns | ~85x | | TIMESTAMPTZ (+02) *(non-UTC session)* | ~4690 ns | ~390 ns | ~12x | | TIMESTAMPTZ (ms +05:30) *(non-UTC)* | ~5350 ns | ~435 ns | ~12x | | TIMESTAMPTZ (us -03) *(non-UTC)* | ~5200 ns | ~400 ns | ~13x | ## Cold (N=100, no warmup) No warmup, fresh module cache — representative of the first few rows / short-lived queries. moment sits at ~15 µs/call cold (its locale tables and regex compilation happen lazily on first use), which is where the biggest absolute wins live. | input | moment | new | speedup | |-------|--------|-----|---------| | DATE | ~13.8 µs | ~35 ns | ~390x | | TIMESTAMP (no frac) | ~18.5 µs | ~139 ns | ~135x | | TIMESTAMP (ms) | ~16.0 µs | ~135 ns | ~120x | | TIMESTAMP (us) | ~16.2 µs | ~128 ns | ~125x | | TIMESTAMPTZ (+00) | ~16.1 µs | ~1230 ns | ~13x | | TIMESTAMPTZ (ms +00) | ~16.5 µs | ~290 ns | ~55x | | TIMESTAMPTZ (us +00) | ~15.2 µs | ~390 ns | ~40x | | TIMESTAMPTZ (+02) *(non-UTC)* | ~14.7 µs | ~880 ns | ~17x | | TIMESTAMPTZ (ms +05:30) *(non-UTC)* | ~15.3 µs | ~1000 ns | ~15x | | TIMESTAMPTZ (us -03) *(non-UTC)* | ~16.7 µs | ~1000 ns | ~17x | --- packages/cubejs-postgres-driver/package.json | 1 - .../src/PostgresDriver.ts | 28 ++--- .../src/type-parsers.ts | 108 ++++++++++++++++++ .../test/type-parsers.test.ts | 63 ++++++++++ 4 files changed, 185 insertions(+), 15 deletions(-) create mode 100644 packages/cubejs-postgres-driver/src/type-parsers.ts create mode 100644 packages/cubejs-postgres-driver/test/type-parsers.test.ts diff --git a/packages/cubejs-postgres-driver/package.json b/packages/cubejs-postgres-driver/package.json index 9ef62d1acb452..d432a810e2d7f 100644 --- a/packages/cubejs-postgres-driver/package.json +++ b/packages/cubejs-postgres-driver/package.json @@ -31,7 +31,6 @@ "@cubejs-backend/shared": "1.6.38", "@types/pg": "^8.16.0", "@types/pg-query-stream": "^1.0.3", - "moment": "^2.24.0", "pg": "^8.18.0", "pg-query-stream": "^4.1.0" }, diff --git a/packages/cubejs-postgres-driver/src/PostgresDriver.ts b/packages/cubejs-postgres-driver/src/PostgresDriver.ts index 019f68f9894f4..f0b11bd31a2a8 100644 --- a/packages/cubejs-postgres-driver/src/PostgresDriver.ts +++ b/packages/cubejs-postgres-driver/src/PostgresDriver.ts @@ -8,7 +8,6 @@ import { getEnv, assertDataSource, Pool, type PoolUserOptions } from '@cubejs-ba import { types, FieldDef } from 'pg'; // eslint-disable-next-line import/no-extraneous-dependencies import { TypeId, TypeFormat } from 'pg-types'; -import * as moment from 'moment'; import { BaseDriver, DownloadQueryResultsOptions, DownloadTableMemoryData, DriverInterface, @@ -18,6 +17,7 @@ import { import { QueryStream } from './QueryStream'; import { PgClient, PgClientConfig } from './PgClient'; import { ConnectionError, PostgresError } from './errors'; +import { dateTypeParser, timestampTypeParser, timestampTzTypeParser } from './type-parsers'; const GenericTypeToPostgres: Record = { string: 'text', @@ -42,15 +42,6 @@ const PostgresToGenericType: Record = { hll: 'HLL_POSTGRES', }; -const timestampDataTypes = [ - // @link TypeId.DATE - 1082, - // @link TypeId.TIMESTAMP - 1114, - // @link TypeId.TIMESTAMPTZ - 1184 -]; -const timestampTypeParser = (val: string) => moment.utc(val).format(moment.HTML5_FMT.DATETIME_LOCAL_MS); const hllTypeParser = (val: string) => Buffer.from( // Postgres uses prefix as \x for encoding val.slice(2), @@ -241,19 +232,28 @@ export class PostgresDriver { - const isTimestamp = timestampDataTypes.includes(dataTypeID); - if (isTimestamp) { + // @link TypeId.DATE + if (dataTypeID === 1082) { + return dateTypeParser; + } + + // @link TypeId.TIMESTAMP + if (dataTypeID === 1114) { return timestampTypeParser; } + // @link TypeId.TIMESTAMPTZ + if (dataTypeID === 1184) { + return timestampTzTypeParser; + } + const typeName = this.getPostgresTypeForField(dataTypeID); if (typeName === 'hll') { // We are using base64 encoding as main format for all HLL sketches, but in pg driver it uses binary encoding return hllTypeParser; } - const parser = types.getTypeParser(dataTypeID, format); - return (val: any) => parser(val); + return types.getTypeParser(dataTypeID, format); }; /** diff --git a/packages/cubejs-postgres-driver/src/type-parsers.ts b/packages/cubejs-postgres-driver/src/type-parsers.ts new file mode 100644 index 0000000000000..db5211c95dba8 --- /dev/null +++ b/packages/cubejs-postgres-driver/src/type-parsers.ts @@ -0,0 +1,108 @@ +/** OID 1082 — Postgres emits `YYYY-MM-DD`. */ +export const dateTypeParser = (val: string): string => `${val}T00:00:00.000`; + +/** OID 1114 — `YYYY-MM-DD HH:mm:ss` or `YYYY-MM-DD HH:mm:ss.f{1,6}`, no TZ. */ +export const timestampTypeParser = (val: string): string => { + if (val.length === 19) { + return `${val.slice(0, 10)}T${val.slice(11, 19)}.000`; + } + + // val[19] is '.'; pad / truncate fractional digits to exactly 3. + const ms = `${val.slice(20, 23)}00`.slice(0, 3); + return `${val.slice(0, 10)}T${val.slice(11, 19)}.${ms}`; +}; + +// Hand-rolled zero-padders for the TIMESTAMPTZ hot path. `String(n).padStart` +// allocates an extra intermediate string per call; with six pad calls per value +// that measured ~15–20% slower in our microbenchmark than these range-checked +// template literals, so we keep the explicit versions. +const pad2 = (n: number): string => (n < 10 ? `0${n}` : `${n}`); +const pad3 = (n: number): string => { + if (n < 10) return `00${n}`; + if (n < 100) return `0${n}`; + + return `${n}`; +}; +const pad4 = (n: number): string => { + if (n < 1000) { + if (n < 10) return `000${n}`; + if (n < 100) return `00${n}`; + + return `0${n}`; + } + + return `${n}`; +}; + +/** + * OID 1184 — same as TIMESTAMP, suffixed with `(+|-)HH`, `(+|-)HH:MM`, or + * `(+|-)HH:MM:SS`. We shift the value into UTC before formatting. + */ +export const timestampTzTypeParser = (val: string): string => { + const len = val.length; + + // Timezone sign sits past the HH:MM:SS portion (index 19). + let tzIdx = 19; + for (; tzIdx < len; tzIdx++) { + const c = val.charCodeAt(tzIdx); + if (c === 43 /* + */ || c === 45 /* - */) break; + } + + const sign = val.charCodeAt(tzIdx) === 43 ? 1 : -1; + const tzHours = parseInt(val.slice(tzIdx + 1, tzIdx + 3), 10); + let tzMinutes = 0; + let tzSeconds = 0; + + if (len > tzIdx + 3) { + tzMinutes = parseInt(val.slice(tzIdx + 4, tzIdx + 6), 10); + if (len > tzIdx + 6) { + tzSeconds = parseInt(val.slice(tzIdx + 7, tzIdx + 9), 10); + } + } + + const offsetMs = sign * (tzHours * 3600000 + tzMinutes * 60000 + tzSeconds * 1000); + if (offsetMs === 0) { + // Fast path: the driver pins session timezone to UTC by default, so Postgres emits `+00`, + // `+00:00`, or `+00:00:00` for every TIMESTAMPTZ on the wire. + return timestampTypeParser(val.slice(0, tzIdx)); + } + + const year = parseInt(val.slice(0, 4), 10); + const month = parseInt(val.slice(5, 7), 10); + const day = parseInt(val.slice(8, 10), 10); + const hour = parseInt(val.slice(11, 13), 10); + const minute = parseInt(val.slice(14, 16), 10); + const second = parseInt(val.slice(17, 19), 10); + + let ms = 0; + if (tzIdx > 19) { + // val[19] is '.'; fractional digits run from index 20 up to tzIdx. + ms = parseInt(`${val.slice(20, 23)}00`.slice(0, 3), 10); + } + + // `Date.UTC(year, ...)` maps years 0-99 to 1900+year for legacy reasons, + // which would corrupt pre-100 AD dates that Postgres can emit. + let utc: Date; + + if (year >= 100) { + utc = new Date(Date.UTC(year, month - 1, day, hour, minute, second, ms) - offsetMs); + } else { + utc = new Date(0); + utc.setUTCFullYear(year, month - 1, day); + utc.setUTCHours(hour, minute, second, ms); + + if (offsetMs !== 0) { + utc.setTime(utc.getTime() - offsetMs); + } + } + + const yyyy = pad4(utc.getUTCFullYear()); + const MM = pad2(utc.getUTCMonth() + 1); + const dd = pad2(utc.getUTCDate()); + const HH = pad2(utc.getUTCHours()); + const mm = pad2(utc.getUTCMinutes()); + const ss = pad2(utc.getUTCSeconds()); + const sss = pad3(utc.getUTCMilliseconds()); + + return `${yyyy}-${MM}-${dd}T${HH}:${mm}:${ss}.${sss}`; +}; diff --git a/packages/cubejs-postgres-driver/test/type-parsers.test.ts b/packages/cubejs-postgres-driver/test/type-parsers.test.ts new file mode 100644 index 0000000000000..e4c8416720217 --- /dev/null +++ b/packages/cubejs-postgres-driver/test/type-parsers.test.ts @@ -0,0 +1,63 @@ +import { + dateTypeParser, + timestampTypeParser, + timestampTzTypeParser, +} from '../src/type-parsers'; + +describe('type parsers', () => { + test('dateTypeParser (OID 1082)', () => { + expect(dateTypeParser('2020-01-01')).toBe('2020-01-01T00:00:00.000'); + // Leap date + expect(dateTypeParser('2020-02-29')).toBe('2020-02-29T00:00:00.000'); + }); + + test('timestampTypeParser (OID 1114)', () => { + // no fractional seconds + expect(timestampTypeParser('2020-01-01 12:34:56')).toBe('2020-01-01T12:34:56.000'); + // millisecond precision + expect(timestampTypeParser('2020-01-01 12:34:56.789')).toBe('2020-01-01T12:34:56.789'); + // microsecond precision is truncated to ms + expect(timestampTypeParser('2020-01-01 12:34:56.123456')).toBe('2020-01-01T12:34:56.123'); + // sub-millisecond precision is padded + expect(timestampTypeParser('2020-01-01 12:34:56.5')).toBe('2020-01-01T12:34:56.500'); + expect(timestampTypeParser('2020-01-01 12:34:56.05')).toBe('2020-01-01T12:34:56.050'); + }); + + test('timestampTzTypeParser (OID 1184)', () => { + // positive HH-only offset (matches integration assertion) + expect(timestampTzTypeParser('2020-01-01 00:00:00+02')).toBe('2019-12-31T22:00:00.000'); + // zero offset — fast path (UTC session, every shape Postgres can emit) + expect(timestampTzTypeParser('2020-01-01 00:00:00+00')).toBe('2020-01-01T00:00:00.000'); + expect(timestampTzTypeParser('2020-01-01 00:00:00-00')).toBe('2020-01-01T00:00:00.000'); + expect(timestampTzTypeParser('2020-01-01 00:00:00+00:00')).toBe('2020-01-01T00:00:00.000'); + expect(timestampTzTypeParser('2020-06-15 08:15:30.250+00')).toBe('2020-06-15T08:15:30.250'); + expect(timestampTzTypeParser('2020-06-15 08:15:30.123456+00')).toBe('2020-06-15T08:15:30.123'); + // negative HH-only offset + expect(timestampTzTypeParser('2020-01-01 00:00:00-05')).toBe('2020-01-01T05:00:00.000'); + // HH:MM offset crossing day boundary + expect(timestampTzTypeParser('2020-01-01 23:30:00+05:30')).toBe('2020-01-01T18:00:00.000'); + expect(timestampTzTypeParser('2020-01-01 00:00:00+05:30:15')).toBe('2019-12-31T18:29:45.000'); + // milliseconds plus HH:MM offset + expect(timestampTzTypeParser('2020-06-15 08:15:30.250+05:45')).toBe('2020-06-15T02:30:30.250'); + // microseconds plus HH offset are truncated to ms + expect(timestampTzTypeParser('2020-06-15 08:15:30.123456-03')).toBe('2020-06-15T11:15:30.123'); + // Years 100-999 take the fast Date.UTC path; pad4 preserves leading zero. + expect(timestampTzTypeParser('0500-06-15 12:00:00+00')).toBe('0500-06-15T12:00:00.000'); + // Years 0-99 must NOT trigger Date.UTC's legacy "1900+year" remap + // (moment parity: `0099-01-01 00:00:00+02` → `0098-12-31T22:00:00.000`, + // not `1998-12-31T…`). + expect(timestampTzTypeParser('0099-01-01 00:00:00+00')).toBe('0099-01-01T00:00:00.000'); + expect(timestampTzTypeParser('0099-01-01 00:00:00+02')).toBe('0098-12-31T22:00:00.000'); + expect(timestampTzTypeParser('0001-01-01 02:00:00+05:00')).toBe('0000-12-31T21:00:00.000'); + // Year boundary rollover (forward / backward) + expect(timestampTzTypeParser('2020-12-31 23:30:00-01')).toBe('2021-01-01T00:30:00.000'); + expect(timestampTzTypeParser('2021-01-01 00:30:00+01')).toBe('2020-12-31T23:30:00.000'); + // Leap-year February edges + expect(timestampTzTypeParser('2020-02-28 23:30:00-01')).toBe('2020-02-29T00:30:00.000'); // into Feb 29 (leap) + expect(timestampTzTypeParser('2020-03-01 00:30:00+01')).toBe('2020-02-29T23:30:00.000'); // back to Feb 29 + expect(timestampTzTypeParser('2021-02-28 23:30:00-01')).toBe('2021-03-01T00:30:00.000'); // non-leap skips Feb 29 + // Centennial leap rule: 2000 IS a leap year, 1900 is NOT. + expect(timestampTzTypeParser('2000-02-28 23:30:00-01')).toBe('2000-02-29T00:30:00.000'); + expect(timestampTzTypeParser('1900-02-28 23:30:00-01')).toBe('1900-03-01T00:30:00.000'); + }); +}); From 4b93511a4682eb7b18b540b60c6f41608925e0fc Mon Sep 17 00:00:00 2001 From: Artyom Keydunov Date: Thu, 23 Apr 2026 08:01:57 -0700 Subject: [PATCH 2/5] docs: remove example pricing table from AI tokens page (#10740) The example table was being misread as actual pricing. The rule (grants equal half the seat price) is stated in prose, which is sufficient. Made-with: Cursor --- docs-mintlify/admin/account-billing/ai-tokens.mdx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs-mintlify/admin/account-billing/ai-tokens.mdx b/docs-mintlify/admin/account-billing/ai-tokens.mdx index 07c1257e2e145..f66335e49778b 100644 --- a/docs-mintlify/admin/account-billing/ai-tokens.mdx +++ b/docs-mintlify/admin/account-billing/ai-tokens.mdx @@ -38,13 +38,7 @@ is subject to change as the product evolves. Self-serve customers on paid plans receive **per-seat token grants** equal to **half of the seat price**. Each user is awarded an individual monthly token -allocation based on their role: - -| Example | Seat price | Monthly token grant | -| --- | --- | --- | -| Developer at $100/month | $100 | $50 | -| Explorer at $60/month | $60 | $30 | -| Viewer at $20/month | $20 | $10 | +allocation based on their role. Per-seat grants: From d12f310c8efd73001fe3128fb4eace78db4743b6 Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 09:40:08 -0700 Subject: [PATCH 3/5] docs: add prerequisites for Snowflake semantic views push (#10739) * docs: add prerequisites for Snowflake semantic views push Generated-By: mintlify-agent * docs: note Enable DDL operations setting for Snowflake push Generated-By: mintlify-agent --------- Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com> --- .../integrations/snowflake-semantic-views.mdx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs-mintlify/docs/integrations/snowflake-semantic-views.mdx b/docs-mintlify/docs/integrations/snowflake-semantic-views.mdx index d661b8a808f6f..05011b82904bc 100644 --- a/docs-mintlify/docs/integrations/snowflake-semantic-views.mdx +++ b/docs-mintlify/docs/integrations/snowflake-semantic-views.mdx @@ -48,6 +48,29 @@ Alternatively, you can push Cube views into Snowflake as native semantic views. This enables you to use Cube-authored views directly in Snowflake, maintaining consistency across both platforms. +### Prerequisites + +The push integration uses the SQL Runner to execute DDL statements in Snowflake. To +successfully create semantic views, ensure the following: + +- **Enable DDL operations** for your Cube deployment. In the Cube Cloud UI, go to + **Deployment Settings** → **Configuration** and turn on **Enable DDL operations**. + Without this setting, the SQL Runner will reject the DDL statements that the push + integration generates. +- The Snowflake role configured for your Cube data source (via [`CUBEJS_DB_SNOWFLAKE_ROLE`](/reference/configuration/environment-variables#cubejs_db_snowflake_role)) + has privileges to create semantic views in the target database and schema + (`CREATE SEMANTIC VIEW` on the schema, plus `USAGE` on the parent database and schema). +- The role has `USAGE` on the warehouse specified by [`CUBEJS_DB_SNOWFLAKE_WAREHOUSE`](/reference/configuration/environment-variables#cubejs_db_snowflake_warehouse) + and `SELECT` on the underlying tables referenced by the view. +- [`CUBEJS_DB_SNOWFLAKE_QUOTED_IDENTIFIERS_IGNORE_CASE`](/reference/configuration/environment-variables#cubejs_db_snowflake_quoted_identifiers_ignore_case) + is set consistently with how identifiers are defined in your Cube data model. The + default value is `false`. + +If a push fails with a permissions error, verify that **Enable DDL operations** is +turned on in your deployment configuration and that the configured role has the +required privileges listed above. See [Snowflake data source configuration](/admin/connect-to-data/data-sources/snowflake) +for the full list of relevant environment variables. + ## Benefits The Snowflake Semantic Views integration provides several advantages: From 2569028423d2bbe168e93e5cdf2224be92c2c8ce Mon Sep 17 00:00:00 2001 From: "James L. Walsh" Date: Thu, 23 Apr 2026 13:06:19 -0400 Subject: [PATCH 4/5] docs: Document CUBEJS_DB_MIN_POOL (#10720) Co-authored-by: Claude Opus 4.7 (1M context) --- .../configuration/reference/environment-variables.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/content/product/configuration/reference/environment-variables.mdx b/docs/content/product/configuration/reference/environment-variables.mdx index 0c7f42140b8e2..1aa2791b7f103 100644 --- a/docs/content/product/configuration/reference/environment-variables.mdx +++ b/docs/content/product/configuration/reference/environment-variables.mdx @@ -664,6 +664,14 @@ The maximum number of concurrent database connections to pool. | --------------- | ------------------------------------------- | ------------------------------------------- | | A valid number | [See database-specific page][ref-config-db] | [See database-specific page][ref-config-db] | +## `CUBEJS_DB_MIN_POOL` + +The minimum number of database connections to pool. + +| Possible Values | Default in Development | Default in Production | +| --------------- | ---------------------- | --------------------- | +| A valid number | `0` | `0` | + ## `CUBEJS_DB_NAME` The name of the database to connect to. From 408dc2616fa70a9dbdd0f34639aef58b2b4900a7 Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:25:48 -0700 Subject: [PATCH 5/5] Clarify certified queries are example library for agents (#10741) Generated-By: mintlify-agent Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com> --- docs-mintlify/admin/ai/spaces-agents-models.mdx | 2 +- docs-mintlify/admin/ai/yaml-config.mdx | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/docs-mintlify/admin/ai/spaces-agents-models.mdx b/docs-mintlify/admin/ai/spaces-agents-models.mdx index ccf4398a68411..2ce72d5979e34 100644 --- a/docs-mintlify/admin/ai/spaces-agents-models.mdx +++ b/docs-mintlify/admin/ai/spaces-agents-models.mdx @@ -22,7 +22,7 @@ Cube is an agentic analytics platform that combines AI agents with semantic data - **Agent Rules**: Instructions that guide how agents behave - **Memories**: Shared knowledge and past interactions -- **Certified Queries**: Pre-approved, trusted queries +- **Certified Queries**: Pre-approved, trusted queries provided as an example library the agent can reference, adapt, or extend (agents aren't restricted to using only certified queries) - **Context**: Business logic and domain expertise #### Example Use Cases: diff --git a/docs-mintlify/admin/ai/yaml-config.mdx b/docs-mintlify/admin/ai/yaml-config.mdx index b1c93dda46253..ba0c50e0b648c 100644 --- a/docs-mintlify/admin/ai/yaml-config.mdx +++ b/docs-mintlify/admin/ai/yaml-config.mdx @@ -503,7 +503,17 @@ rules: ## Certified Queries -Certified queries are pre-approved SQL queries that agents can use for specific user requests. +Certified queries are pre-approved SQL queries that serve as an **example library** provided to the agent. The agent treats them as reference examples rather than a strict set of queries it must use. + + +The agent is not limited to only running certified queries. When answering a user request, the agent may: + +- Use a certified query directly if it matches the request +- Use a certified query as a **starting point** and adapt it (for example, adding filters, dimensions, or measures) to fit the user's question +- Generate a new query independently if no certified query is relevant + +Certified queries help guide the agent toward trusted patterns and correct business logic, but the agent retains full flexibility to construct the query that best answers the user's question. + ```yaml certified_queries: