feat: Links in the data model#10852
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
Claude finished @paveltiunov's task in 3m 3s —— View job Review: Documentation for
|
| param_name_for_filters: my_precious_filters | ||
| ``` | ||
|
|
||
| #### Dimensions |
There was a problem hiding this comment.
Suggestion: Ambiguous heading. The #### Dimensions heading is used as a sub-section under both links and html to describe the synthetic dimensions generated in the result set. Since this appears inside a page titled "Dimensions," the heading reads like a circular reference. Consider a more descriptive title like #### Generated dimensions or #### Synthetic dimensions in the result set to make it immediately clear what this sub-section covers.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #10852 +/- ##
===========================================
+ Coverage 58.36% 83.47% +25.11%
===========================================
Files 216 254 +38
Lines 16939 75350 +58411
Branches 3435 0 -3435
===========================================
+ Hits 9887 62901 +53014
- Misses 6550 12449 +5899
+ Partials 502 0 -502
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Claude finished @paveltiunov's task in 10m 8s —— View job Comprehensive Code Review — PR #10852
SummaryThe "links as synthetic dimensions" design is clean and well-chosen — it avoids special-casing in the query pipeline and gets Tesseract support for free. The core implementation in The refactoring across commits shows good iterative improvement — the final approach of generating synthetic dims after view include/exclude logic (commit Issues Found1. Link The 2. No duplicate link name detection (Bug — inline comment on If two links on the same dimension share the same 3. The docs show 4. Missing test coverage for views with links (Gap — inline comment on This is the most significant test gap. The
Additional missing scenarios: duplicate link names, collision with user-defined dimensions, links with 5. Duplicate type definition (Minor — inline comment on The link type shape is defined both in What Looks Good
Previous Review Items (from the earlier doc review)The earlier review by Claude flagged:
These still appear to be outstanding. Items 1 and 3 are worth addressing before merge. |
| ]); | ||
|
|
||
| const LinkItemSchema = Joi.object().keys({ | ||
| name: Joi.string().required(), |
There was a problem hiding this comment.
Bug: name should be validated against the identifier regex.
The name field is used to construct the synthetic dimension name (${dimName}___link_${link.name}_url), which becomes a queryable dimension identifier. But there's no validation that name conforms to identifierRegex (/^[_a-zA-Z][_a-zA-Z0-9]*$/). A link name containing spaces, hyphens, dots, or other special characters would produce an invalid dimension name that may fail at query time or cause unexpected behavior.
| name: Joi.string().required(), | |
| name: Joi.string().regex(/^[_a-zA-Z][_a-zA-Z0-9]*$/, 'identifier').required(), |
| for (const [dimName, dimDef] of Object.entries<any>(cube.dimensions)) { | ||
| if (dimDef.links && Array.isArray(dimDef.links)) { | ||
| dimDef.links.forEach((link: any) => { | ||
| const syntheticName = `${dimName}___link_${link.name}_url`; | ||
| syntheticDims[syntheticName] = { | ||
| sql: link.url, | ||
| type: 'string', | ||
| synthetic: true, | ||
| ownedByCube: true, | ||
| public: false, | ||
| }; | ||
| }); | ||
| } |
There was a problem hiding this comment.
Issue: No duplicate link name detection.
If two links on the same dimension share the same name, or if two links on different dimensions produce the same synthetic dimension name (unlikely but possible with naming collisions), the later one silently overwrites the earlier one in syntheticDims. Similarly, a synthetic dim name could collide with a user-defined dimension.
Consider adding a check:
if (syntheticDims[syntheticName] || cube.dimensions[syntheticName]) {
// warn or error about collision
}Also, duplicate name values within a single dimension's links array should ideally be caught at the validator level (e.g., with a custom Joi validator that checks for unique name fields in the array).
| url: Joi.func().required(), | ||
| icon: Joi.string(), | ||
| target: Joi.string().valid('blank', 'self'), | ||
| params: Joi.object().pattern(Joi.string(), Joi.string()), |
There was a problem hiding this comment.
Question: Should params values support SQL expression references?
The docs describe params values using {id} and {FILTER_PARAMS.users.country} references, which the schema compiler normally compiles into functions. However, the validator expects Joi.string() for param values, not Joi.func().
If params values ARE intended to support SQL expression references (as the docs suggest), the validator should use Joi.func(). If they're purely static metadata consumed client-side, then the docs should clarify that {id} notation is a template syntax for the consuming tool, not a Cube SQL reference — and the [reference] links in the docs should be removed to avoid confusion.
| aggType?: string; | ||
| keyReference?: string; | ||
| currency?: string; | ||
| links?: Array<{ | ||
| name: string; | ||
| label: string; | ||
| url: (...args: any[]) => string; | ||
| icon?: string; | ||
| target?: 'blank' | 'self'; | ||
| params?: Record<string, string>; | ||
| propagate_filters_to_params?: boolean; | ||
| param_name_for_filters?: string; |
There was a problem hiding this comment.
Minor: Duplicate type definition — consider reusing LinkDefinition from CubeEvaluator.ts.
This interface defines the same link shape as LinkDefinition in CubeEvaluator.ts. Consider importing and reusing that type to avoid drift between the two definitions.
| import { PostgresQuery } from '../../src'; | ||
| import { prepareYamlCompiler } from './PrepareCompiler'; | ||
|
|
||
| describe('Links', () => { | ||
| const schemaWithLinks = ` | ||
| cubes: | ||
| - name: users | ||
| sql_table: users | ||
|
|
||
| dimensions: | ||
| - name: id | ||
| sql: id | ||
| type: number | ||
| primary_key: true | ||
|
|
||
| - name: full_name | ||
| sql: full_name | ||
| type: string | ||
| links: | ||
| - name: google_search | ||
| label: Search on Google | ||
| url: "CONCAT('https://www.google.com/search?q=', {CUBE}.full_name)" | ||
| icon: brand-google | ||
| target: blank | ||
| - name: email | ||
| label: Write an email | ||
| url: "CONCAT('mailto:', {email})" | ||
| icon: send | ||
|
|
||
| - name: email | ||
| sql: email | ||
| type: string | ||
| `; | ||
|
|
||
| it('should create synthetic link URL dimensions', async () => { | ||
| const compilers = prepareYamlCompiler(schemaWithLinks); | ||
| await compilers.compiler.compile(); | ||
|
|
||
| const googleDef = compilers.cubeEvaluator.dimensionByPath('users.full_name___link_google_search_url'); | ||
| expect(googleDef).toBeDefined(); | ||
| expect(googleDef.type).toBe('string'); | ||
| expect((googleDef as any).synthetic).toBe(true); | ||
|
|
||
| const emailDef = compilers.cubeEvaluator.dimensionByPath('users.full_name___link_email_url'); | ||
| expect(emailDef).toBeDefined(); | ||
| expect(emailDef.type).toBe('string'); | ||
| expect((emailDef as any).synthetic).toBe(true); | ||
| }); | ||
|
|
||
| it('should generate correct SQL when synthetic link dimension is queried', async () => { | ||
| const compilers = prepareYamlCompiler(schemaWithLinks); | ||
| await compilers.compiler.compile(); | ||
|
|
||
| const query = new PostgresQuery(compilers, { | ||
| measures: [], | ||
| dimensions: ['users.full_name', 'users.full_name___link_google_search_url'], | ||
| }); | ||
|
|
||
| const queryAndParams = query.buildSqlAndParams(); | ||
| const sql = queryAndParams[0]; | ||
|
|
||
| expect(sql).toContain('"users__full_name___link_google_search_url"'); | ||
| expect(sql).toContain('https://www.google.com/search?q='); | ||
| expect(sql).toContain('"users".full_name'); | ||
| }); | ||
|
|
||
| it('should NOT include link URL columns unless explicitly queried', async () => { | ||
| const compilers = prepareYamlCompiler(schemaWithLinks); | ||
| await compilers.compiler.compile(); | ||
|
|
||
| const query = new PostgresQuery(compilers, { | ||
| measures: [], | ||
| dimensions: ['users.full_name'], | ||
| }); | ||
|
|
||
| const queryAndParams = query.buildSqlAndParams(); | ||
| const sql = queryAndParams[0]; | ||
|
|
||
| expect(sql).not.toContain('___link_'); | ||
| }); | ||
|
|
||
| it('should expose links metadata and synthetic flag in meta config', async () => { | ||
| const compilers = prepareYamlCompiler(schemaWithLinks); | ||
| await compilers.compiler.compile(); | ||
|
|
||
| const metaTransformer = compilers.metaTransformer; | ||
| const cubes = metaTransformer.cubes; | ||
| const usersCube = cubes.find((c: any) => c.config.name === 'users'); | ||
| expect(usersCube).toBeDefined(); | ||
|
|
||
| const fullNameDim = usersCube!.config.dimensions.find( | ||
| (d: any) => d.name === 'users.full_name' | ||
| ); | ||
| expect(fullNameDim).toBeDefined(); | ||
| expect(fullNameDim!.links).toBeDefined(); | ||
| expect(fullNameDim!.links).toHaveLength(2); | ||
| expect(fullNameDim!.links![0].label).toBe('Search on Google'); | ||
| expect(fullNameDim!.links![0].icon).toBe('brand-google'); | ||
| expect(fullNameDim!.links![0].target).toBe('blank'); | ||
|
|
||
| const syntheticDim = usersCube!.config.dimensions.find( | ||
| (d: any) => d.name === 'users.full_name___link_google_search_url' | ||
| ); | ||
| expect(syntheticDim).toBeDefined(); | ||
| expect(syntheticDim!.synthetic).toBe(true); | ||
| }); | ||
|
|
||
| it('synthetic link dimensions should not be public by default', async () => { | ||
| const compilers = prepareYamlCompiler(schemaWithLinks); | ||
| await compilers.compiler.compile(); | ||
|
|
||
| const metaTransformer = compilers.metaTransformer; | ||
| const cubes = metaTransformer.cubes; | ||
| const usersCube = cubes.find((c: any) => c.config.name === 'users'); | ||
| expect(usersCube).toBeDefined(); | ||
|
|
||
| const syntheticDim = usersCube!.config.dimensions.find( | ||
| (d: any) => d.name === 'users.full_name___link_google_search_url' | ||
| ); | ||
| expect(syntheticDim).toBeDefined(); | ||
| expect(syntheticDim!.public).toBe(false); | ||
| }); | ||
|
|
||
| it('should validate links schema - label is required', async () => { | ||
| const invalidSchema = ` | ||
| cubes: | ||
| - name: users | ||
| sql_table: users | ||
|
|
||
| dimensions: | ||
| - name: full_name | ||
| sql: full_name | ||
| type: string | ||
| links: | ||
| - name: test | ||
| url: "'https://example.com'" | ||
| `; | ||
| const compilers = prepareYamlCompiler(invalidSchema); | ||
|
|
||
| try { | ||
| await compilers.compiler.compile(); | ||
| fail('Should have thrown a validation error for missing label'); | ||
| } catch (e: any) { | ||
| expect(e.message || e.toString()).toContain('label'); | ||
| } | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Missing test coverage for views, edge cases, and params.
The existing tests cover the happy path for cubes well. However, several important scenarios are missing:
-
Views with links: When a view includes a dimension with links, are the synthetic link dimensions correctly generated on the view? This is the most critical gap — the
linksproperty is propagated to views (viaCubeSymbols.ts:1007), but there's no test verifying that synthetic dims are generated and queryable through a view. -
Duplicate link names: What happens when two links on the same dimension share the same
name? Currently the second silently overwrites the first. -
Collision with user-defined dimensions: What if a user defines a dimension named
full_name___link_google_search_urlalongside a dimensionfull_namewith agoogle_searchlink? -
Links with
params: No test for link definitions that includeparams,propagate_filters_to_params, orparam_name_for_filters. -
Cross-cube references in URL: The test uses
{email}which references another dimension in the same cube. Testing with cross-cube references (e.g.,{other_cube.dim}) would verify the foreign cube check behavior.
| export type LinkDefinition = { | ||
| name: string; | ||
| label: string; | ||
| url: (...args: any[]) => string; | ||
| icon?: string; | ||
| target?: 'blank' | 'self'; | ||
| params?: Record<string, string>; | ||
| propagate_filters_to_params?: boolean; | ||
| param_name_for_filters?: string; | ||
| }; |
There was a problem hiding this comment.
Observation: params values typed as Record<string, string> but docs show SQL expression references.
The docs show params values like "{id}" and "{FILTER_PARAMS.users.country}". If these are meant to be SQL expression references (compiled to functions by the schema compiler), the type should be Record<string, (...args: any[]) => string>. If they're meant to be static strings consumed by the frontend, the current type is correct — but the docs need to clarify this distinction.
This ties into the validator question on CubeValidator.ts:320.
Add documentation for the new `links`, `html`, and `synthetic` parameters on dimensions. Links allow defining navigable URLs associated with dimension values, rendered as synthetic dimensions in the result set. HTML fragments enable rich formatting beyond the `format` parameter. The `synthetic` parameter marks auto-generated dimensions. Also update the `meta` parameter description and add a cross-reference from the FILTER_PARAMS context variable documentation to the new links feature. Changes applied to both the Next.js docs (docs/content/) and the Mintlify docs (docs-mintlify/reference/). Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
- Add links validation schema to CubeValidator.ts (label, url, icon, target, params, propagate_filters_to_params, param_name_for_filters) - Add LinkDefinition type to CubeEvaluator.ts - Add links to DimensionConfig and ExtendedCubeSymbolDefinition types in CubeToMetaTransformer.ts for /v1/meta exposure - Generate synthetic link URL columns in BaseQuery.js when includeLinks option is set (only url is rendered as SQL; label/icon/target are constant metadata exposed via /v1/meta) - Add includeLinks flag to Query type and query validation schema - Wire includeLinks through /v1/cubesql endpoint via request options stored on SQLServer, injected into the sql callback query - Add unit tests for links feature Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
The url field in links is now a SQL function (like mask.sql) that gets
evaluated through the standard evaluateSql/autoPrefixAndEvaluateSql
pipeline. This means:
- url uses standard {CUBE}.column and {dimension} references
- url supports any SQL expression (CONCAT, CASE, etc.)
- No custom template parsing is needed
The url is no longer exposed in /v1/meta (it's a server-side SQL
expression). Only constant metadata (label, icon, target, params config)
is exposed in meta. The computed URL value appears only as a SQL column
in query results when includeLinks is set.
Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
- Add include_links to BaseQueryOptionsStatic and pass it through BaseQuery → QueryTools - Create LinkItem bridge (cube_bridge/link_item.rs) with url sql field - Add links() method to DimensionDefinition bridge trait - Compile link url SQL calls in DimensionSymbolFactory and store them as link_url_sqls on DimensionSymbol - Add includeLinks to buildSqlAndParamsRust query params in BaseQuery.js The link URL SQL expressions are compiled and stored on DimensionSymbol, ready to be projected as additional columns when the query processor handles include_links. The actual projection in the physical plan builder will emit these as synthetic columns alongside their parent dimension. Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
…ed injection Links are now proper synthetic dimensions generated at compile time in CubeEvaluator.prepareSyntheticLinkDimensions(). Each link definition creates a dimension named <dim>___link_<id>_url with the link's url SQL expression as its sql property. This means: - No special flag needed (removed includeLinks from Query type, API gateway, sql-server, BaseQuery) - Users of SQL API query link URLs as regular dimensions - Works natively with both JS BaseQuery and Tesseract (they're just dimensions in the evaluated cube) - Synthetic dimensions are marked with synthetic:true and public:false Removed all Tesseract-specific link plumbing (include_links, LinkItem bridge, link_url_sqls on DimensionSymbol) since synthetic dimensions flow through the standard dimension pipeline. Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
Each link now requires a 'name' property (in addition to 'label') that serves as the identifier in the synthetic dimension name: <dimension>___link_<name>_url This gives meaningful, stable column names instead of index-based ones. Example: full_name___link_google_search_url Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
…in a view When a dimension with links is included in a view (via explicit includes list), its synthetic link dimensions are now automatically included as well. This mirrors how hierarchy level dimensions are auto-included. For includes: '*', synthetic dims are already picked up since they exist as regular dimensions on the source cube. Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
Restructured so the compilation order is:
1. View include/exclude logic runs first (CubeSymbols.prepareIncludes)
- links property is propagated to view dimensions alongside other
properties like format, granularities, mask
- Exclude works correctly since synthetic dims don't exist yet
2. Then prepareSyntheticLinkDimensions runs in prepareCube for both
cubes AND views, generating synthetic dims from whatever dimensions
survived the include/exclude phase
Removed the previous approach of auto-including synthetic dims during
the include resolution (they didn't exist at that point anyway).
Moved prepareSyntheticLinkDimensions before prepareMembers(dimensions)
so the synthetic dims get full member processing.
Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
The no-continue eslint rule prohibits continue statements. Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
ebb1b02 to
4217ebb
Compare
Check List
Description of Changes Made
Implements the
linksfeature for dimensions in the data model, as specified in #10203.Design
Links are implemented as synthetic dimensions. Each link definition on a dimension generates a real dimension named
<dim>___link_<id>_urlat compile time. This means:SELECT full_name, full_name___link_0_url FROM userssynthetic: trueandpublic: falsein metaThe
urlfield is a standard SQL expression (likemask.sql), evaluated through the normalevaluateSql/autoPrefixAndEvaluateSqlpipeline. Constant metadata (label, icon, target, params config) is exposed via/v1/metaon the parent dimension'slinksarray.Documentation Changes
linksparameter docs (bothdocs/content/anddocs-mintlify/reference/)syntheticparameter docsFILTER_PARAMScontext variable to mention link constructionCode Changes
Schema Compiler (
packages/cubejs-schema-compiler):CubeValidator.ts:linksvalidation schema —urlisJoi.func()(SQL expression)CubeEvaluator.ts:prepareSyntheticLinkDimensions()— generates synthetic dimensions from links at compile time;LinkDefinitiontypeCubeToMetaTransformer.ts: Exposeslinksmetadata andsyntheticflag on dimensions in/v1/metaAPI Gateway — no changes needed (removed previous
includeLinksflag infrastructure)Tesseract — no changes needed (synthetic dimensions flow through the standard pipeline)