From e4d43fd76a328c0fab92ea2843f4649a07c9732c Mon Sep 17 00:00:00 2001 From: gowtham raj j Date: Wed, 10 Jun 2026 12:39:59 +0530 Subject: [PATCH 1/8] Fix yaml parse issue (#59) * fix(framework): quote YAML 1.1 reserved boolean tokens on emit * bump minor version and update changelog --- CHANGELOG.md | 8 +- package-lock.json | 4 +- package.json | 2 +- .../normalize-time-intervals.test.ts | 60 +++ src/services/framework/utils/sql-utils.ts | 430 +++++++++++------- src/services/sync/constants.ts | 12 +- src/shared/index.test.ts | 45 +- src/shared/index.ts | 10 +- 8 files changed, 387 insertions(+), 184 deletions(-) create mode 100644 src/services/framework/__tests__/normalize-time-intervals.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index be4a909..4a3d1f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,18 @@ # Change Log +## 1.8.0 + +### Bug fixes + +- **YAML reserved tokens round-trip safely.** Values like `OFF`, `ON`, `YES`, `NO` (and lowercase variants) are now quoted on emit and tolerated on load, so `time_intervals: OFF` no longer turns into `false` in the manifest and crashes sync. Per-column meta failures also name the offending column. +- **Sync errors surface the real cause.** SQL/YML generation failures now show the underlying message instead of always pointing at `expr` syntax. + ## 1.7.1 ### Iceberg write strategy update - **Write strategy** — Iceberg incremental writes now use an event-date literal directly instead of creating and querying a temporary table, improving write performance - ## 1.7.0 ### Adhoc SQL Editor / Query Draft diff --git a/package-lock.json b/package-lock.json index dbea2ff..fd32581 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dj", - "version": "1.7.1", + "version": "1.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dj", - "version": "1.7.1", + "version": "1.8.0", "license": "Apache-2.0", "workspaces": [ "web" diff --git a/package.json b/package.json index 658d652..ec9b48e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "type": "git", "url": "https://github.com/Workday/dj.git" }, - "version": "1.7.1", + "version": "1.8.0", "workspaces": [ "web" ], diff --git a/src/services/framework/__tests__/normalize-time-intervals.test.ts b/src/services/framework/__tests__/normalize-time-intervals.test.ts new file mode 100644 index 0000000..0030c57 --- /dev/null +++ b/src/services/framework/__tests__/normalize-time-intervals.test.ts @@ -0,0 +1,60 @@ +import { normalizeTimeIntervals } from '@services/framework/utils/sql-utils'; + +describe('normalizeTimeIntervals', () => { + const ctx = { modelName: 'm', columnName: 'datetime' }; + + test('passes through "OFF" string', () => { + expect(normalizeTimeIntervals('OFF', ctx)).toBe('OFF'); + }); + + // Regression: dbt's PyYAML (YAML 1.1) parses unquoted `OFF` as boolean + // false; that value reaches us via the manifest. Treat it as the user's + // original "OFF" intent rather than crashing on a non-iterable value. + test('coerces boolean false (YAML 1.1 OFF) back to "OFF"', () => { + expect(normalizeTimeIntervals(false, ctx)).toBe('OFF'); + }); + + test('treats boolean true (YAML 1.1 ON) as the empty default', () => { + expect(normalizeTimeIntervals(true, ctx)).toEqual([]); + }); + + test('returns sorted, de-duplicated copy of an array', () => { + const input = ['MONTH', 'DAY', 'YEAR', 'DAY']; + const out = normalizeTimeIntervals(input, ctx); + expect(out).toEqual(['DAY', 'MONTH', 'YEAR']); + // Should be a new array, never mutate input order + expect(input).toEqual(['MONTH', 'DAY', 'YEAR', 'DAY']); + }); + + test('treats null and undefined as empty array', () => { + expect(normalizeTimeIntervals(undefined, ctx)).toEqual([]); + expect(normalizeTimeIntervals(null, ctx)).toEqual([]); + }); + + test('falls back to empty array and warns for unsupported types, naming the column', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + try { + const out = normalizeTimeIntervals( + { malformed: true }, + { modelName: 'parent_model', columnName: 'datetime' }, + ); + expect(out).toEqual([]); + expect(warn).toHaveBeenCalledTimes(1); + const message = warn.mock.calls[0][0] as string; + expect(message).toContain('parent_model.datetime'); + expect(message).toContain('object'); + } finally { + warn.mockRestore(); + } + }); + + test('falls back to empty array for primitive numbers and strings other than OFF', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + try { + expect(normalizeTimeIntervals(42, ctx)).toEqual([]); + expect(normalizeTimeIntervals('NOT_OFF', ctx)).toEqual([]); + } finally { + warn.mockRestore(); + } + }); +}); diff --git a/src/services/framework/utils/sql-utils.ts b/src/services/framework/utils/sql-utils.ts index d0c6ffb..2e89360 100644 --- a/src/services/framework/utils/sql-utils.ts +++ b/src/services/framework/utils/sql-utils.ts @@ -58,6 +58,7 @@ import type { FrameworkSource, FrameworkSourceMeta, } from '@shared/framework/types'; +import type { LightdashDimension } from '@shared/lightdash/types'; import type { SchemaModelFromJoinModels } from '@shared/schema/types/model.from.join.models.schema'; import type { SchemaModelTypeIntJoinColumn, @@ -111,6 +112,69 @@ import { frameworkMakeSourceName, } from './source-utils'; +// ======================================================================== +// Lightdash Dimension Helpers +// ======================================================================== + +/** + * Normalize a `time_intervals` value coming from a Lightdash dimension meta. + * + * Per the schema, valid inputs are the literal string "OFF" or an array of + * interval strings. In practice we have to be defensive here because: + * + * 1. The framework writes YAML which dbt then loads with PyYAML (YAML 1.1). + * Older YAML files on disk may contain an unquoted `time_intervals: OFF` + * that PyYAML parses as the boolean `false`. When the manifest is then + * consumed back into the framework, the column's `time_intervals` is + * `false` rather than `"OFF"`. The emit-side fix in `yamlStringify` + * prevents new YAML from being written this way, but existing manifests + * on disk in user projects can still carry the corrupted value. + * 2. A handcrafted JSON model could put any value here. Without coercion + * the spread operator below crashes generation for the entire model + * (and any descendant whose parent meta we needed). + * + * Behaviour: + * - undefined / null -> `[]` + * - "OFF" -> "OFF" (passed through) + * - boolean false -> "OFF" (preserve original user intent) + * - boolean true -> `[]` (treat as a default / unset signal) + * - array -> sorted, de-duplicated copy + * - any other type -> `[]`, with a console warning that names + * the model + column so the source of the bad value is debuggable + */ +export function normalizeTimeIntervals( + value: unknown, + context: { modelName: string; columnName: string }, +): LightdashDimension['time_intervals'] { + type TimeIntervals = NonNullable; + if (value === undefined || value === null) { + return [] as TimeIntervals; + } + if (value === 'OFF' || value === false) { + // YAML 1.1 turns the string "OFF" into a boolean false during dbt's + // manifest load, so accept either form as the user-intended "OFF". + return 'OFF'; + } + if (value === true) { + // `true` is not a valid time_intervals value but is the symmetric + // YAML 1.1 token for "ON" -- fall back to the empty default rather + // than crashing. + return [] as TimeIntervals; + } + if (Array.isArray(value)) { + return _.chain([...value]) + .sort() + .uniq() + .value() as TimeIntervals; + } + // eslint-disable-next-line no-console + console.warn( + `[dj] Ignoring unsupported time_intervals value on ${context.modelName}.${context.columnName}: ` + + `expected "OFF" or an array of strings, got ${typeof value} (${JSON.stringify(value)}).`, + ); + return [] as TimeIntervals; +} + // ======================================================================== // SQL Clause Generators // ======================================================================== @@ -2524,201 +2588,215 @@ export function frameworkModelProperties({ project, }); - // Persist columns on the model properties + // Persist columns on the model properties. + // Each column is built inside a try/catch so that an unexpected failure + // (e.g. malformed meta inherited from an upstream manifest) is reported + // with the offending column name. Without this context the parent log line + // only shows the model name, which makes diagnosing schema-shape regressions + // painful. const modelPropertiesColumns: DbtModelPropertiesColumn[] = []; for (const c of columns) { - // Control ordering of column properties - const column: DbtModelPropertiesColumn = { - name: frameworkColumnName({ column: c, modelJson }), - data_type: c.data_type || 'varchar', - description: c.description || textToStartCase(c.name), - tags: c.tags, - // Switch to data_tests on the yml once dbt is updated to >=1.8 - // data_tests: c.data_tests, - tests: c.data_tests, - meta: c.meta, - }; + const columnContextName = frameworkColumnName({ column: c, modelJson }); + try { + // Control ordering of column properties + const column: DbtModelPropertiesColumn = { + name: columnContextName, + data_type: c.data_type || 'varchar', + description: c.description || textToStartCase(c.name), + tags: c.tags, + // Switch to data_tests on the yml once dbt is updated to >=1.8 + // data_tests: c.data_tests, + tests: c.data_tests, + meta: c.meta, + }; - const isIncrementalModel = - ('materialized' in modelJson && - modelJson.materialized === 'incremental') || - ('materialization' in modelJson && - (modelJson.materialization === 'incremental' || - (typeof modelJson.materialization === 'object' && - modelJson.materialization?.type === 'incremental'))); - - if (isIncrementalModel) { - switch (column.name) { - case PARTITION_MONTHLY: - case PARTITION_DAILY: - case PARTITION_HOURLY: { - const dataTests = column.tests ?? []; - if (!dataTests.includes('not_null')) { - dataTests.push('not_null'); + const isIncrementalModel = + ('materialized' in modelJson && + modelJson.materialized === 'incremental') || + ('materialization' in modelJson && + (modelJson.materialization === 'incremental' || + (typeof modelJson.materialization === 'object' && + modelJson.materialization?.type === 'incremental'))); + + if (isIncrementalModel) { + switch (column.name) { + case PARTITION_MONTHLY: + case PARTITION_DAILY: + case PARTITION_HOURLY: { + const dataTests = column.tests ?? []; + if (!dataTests.includes('not_null')) { + dataTests.push('not_null'); + } + column.tests = dataTests; + break; } - column.tests = dataTests; - break; } } - } - // Setting lightdash dimension meta - let dimension = { ...c.meta.dimension }; - if (typeof dimension.time_intervals !== 'string') { - // If the time_intervals aren't a string, sort alphabetically - dimension.time_intervals = _.chain([...(dimension.time_intervals ?? [])]) - .sort() - .uniq() - .value(); - } - // Set defaults for column level properties at the mart layer - if (modelLayer === 'mart') { - if (dimension.hidden === undefined) { - dimension.hidden = - column.meta?.type === 'fct' || - FRAMEWORK_PARTITIONS.includes(column.name as FrameworkPartitionName); - } - if (!dimension.label) { - dimension.label = textToStartCase(column.name); - } - if (!dimension.type) { - dimension.type = lightdashConvertDimensionType(column.data_type); - } + // Setting lightdash dimension meta. `normalizeTimeIntervals` defends + // against the YAML 1.1 boolean coercion of unquoted `OFF` -> `false` + // and other unexpected shapes coming back from the dbt manifest. + let dimension = { ...c.meta.dimension }; + dimension.time_intervals = normalizeTimeIntervals( + dimension.time_intervals, + { modelName, columnName: column.name }, + ); + // Set defaults for column level properties at the mart layer + if (modelLayer === 'mart') { + if (dimension.hidden === undefined) { + dimension.hidden = + column.meta?.type === 'fct' || + FRAMEWORK_PARTITIONS.includes( + column.name as FrameworkPartitionName, + ); + } + if (!dimension.label) { + dimension.label = textToStartCase(column.name); + } + if (!dimension.type) { + dimension.type = lightdashConvertDimensionType(column.data_type); + } - if (column.name === 'datetime') { - // Find a partitioned column to use for time intervals - const partitionedColumn = - columns.find((c) => c.name === PARTITION_HOURLY) || - columns.find((c) => c.name === PARTITION_DAILY) || - columns.find((c) => c.name === PARTITION_MONTHLY); - if (partitionedColumn) { - dimension.sql = partitionedColumn.name; + if (column.name === 'datetime') { + // Find a partitioned column to use for time intervals + const partitionedColumn = + columns.find((c) => c.name === PARTITION_HOURLY) || + columns.find((c) => c.name === PARTITION_DAILY) || + columns.find((c) => c.name === PARTITION_MONTHLY); + if (partitionedColumn) { + dimension.sql = partitionedColumn.name; + } } } - } - // If dimension has an ai_hint, automatically add an 'ai' tag - if (aiHintTags.length && dimension.ai_hint) { - dimension.tags = [..._.union(dimension.tags ?? [], aiHintTags)]; - } - dimension.tags?.sort(); - - // Control ordering of lightdash dimension properties - dimension = orderKeys(dimension, [ - 'ai_hint', - 'tags', - 'type', - 'label', - 'group_label', - 'groups', - 'case_sensitive', - ]); - - // Order lightdash metric keys and remove empty properties - const metrics = _.reduce( - column.meta?.metrics ?? {}, - (m, metric, name) => { - return { - ...m, - [name]: removeEmpty( - orderKeys(metric, [ - 'ai_hint', - 'tags', - 'type', - 'label', - 'group_label', - 'groups', - ]), - ), - }; - }, - {}, - ); + // If dimension has an ai_hint, automatically add an 'ai' tag + if (aiHintTags.length && dimension.ai_hint) { + dimension.tags = [..._.union(dimension.tags ?? [], aiHintTags)]; + } + dimension.tags?.sort(); + + // Control ordering of lightdash dimension properties + dimension = orderKeys(dimension, [ + 'ai_hint', + 'tags', + 'type', + 'label', + 'group_label', + 'groups', + 'case_sensitive', + ]); + + // Order lightdash metric keys and remove empty properties + const metrics = _.reduce( + column.meta?.metrics ?? {}, + (m, metric, name) => { + return { + ...m, + [name]: removeEmpty( + orderKeys(metric, [ + 'ai_hint', + 'tags', + 'type', + 'label', + 'group_label', + 'groups', + ]), + ), + }; + }, + {}, + ); - // Column meta emit strategy (free-form meta support): - // - // After the `FrameworkColumn` split, all SQL-generation state lives - // on `c.internal.*` and is never emitted. Values on `c.meta` fall - // into three buckets: - // a. Populated-reserved keys (type / origin / dimension / metrics / - // case_sensitive) -- framework-written, will be re-layered below. - // b. SQL-internal reserved key names (agg / expr / prefix / ...) -- - // the user placed them under `meta` by mistake. They have no - // effect on SQL (the framework reads SQL state from - // `c.internal.*`, which is populated from top-level select-item - // siblings). We strip them so they don't silently leak to the - // emitted YAML; the reserved-key lint (meta-lint.ts) separately - // surfaces the authoring mistake as a Warning diagnostic. - // c. Free-form user keys -- passed through verbatim. - // - // Strategy: - // 1. Spread-destructure to drop (a) and (b) from the free-form bag. - // 2. Layer the framework-populated reserved keys back on top so the - // framework silently wins on collision with any free-form key - // of the same name. - // 3. Apply `case_sensitive` AFTER `removeEmpty` so a valid `false` - // value isn't stripped. - const rawMeta = (c.meta ?? {}) as Record; - const metaFreeForm: Record = {}; - for (const [key, value] of Object.entries(rawMeta)) { - // Skip populated-reserved keys -- they are re-layered below. - if ( - key === 'type' || - key === 'origin' || - key === 'dimension' || - key === 'metrics' || - key === 'case_sensitive' - ) { - continue; + // Column meta emit strategy (free-form meta support): + // + // After the `FrameworkColumn` split, all SQL-generation state lives + // on `c.internal.*` and is never emitted. Values on `c.meta` fall + // into three buckets: + // a. Populated-reserved keys (type / origin / dimension / metrics / + // case_sensitive) -- framework-written, will be re-layered below. + // b. SQL-internal reserved key names (agg / expr / prefix / ...) -- + // the user placed them under `meta` by mistake. They have no + // effect on SQL (the framework reads SQL state from + // `c.internal.*`, which is populated from top-level select-item + // siblings). We strip them so they don't silently leak to the + // emitted YAML; the reserved-key lint (meta-lint.ts) separately + // surfaces the authoring mistake as a Warning diagnostic. + // c. Free-form user keys -- passed through verbatim. + // + // Strategy: + // 1. Spread-destructure to drop (a) and (b) from the free-form bag. + // 2. Layer the framework-populated reserved keys back on top so the + // framework silently wins on collision with any free-form key + // of the same name. + // 3. Apply `case_sensitive` AFTER `removeEmpty` so a valid `false` + // value isn't stripped. + const rawMeta = (c.meta ?? {}) as Record; + const metaFreeForm: Record = {}; + for (const [key, value] of Object.entries(rawMeta)) { + // Skip populated-reserved keys -- they are re-layered below. + if ( + key === 'type' || + key === 'origin' || + key === 'dimension' || + key === 'metrics' || + key === 'case_sensitive' + ) { + continue; + } + // Skip SQL-internal reserved key names -- never emitted. + if ( + ( + COLUMN_META_SQL_INTERNAL_RESERVED_KEYS as readonly string[] + ).includes(key) + ) { + continue; + } + metaFreeForm[key] = value; } - // Skip SQL-internal reserved key names -- never emitted. - if ( - (COLUMN_META_SQL_INTERNAL_RESERVED_KEYS as readonly string[]).includes( - key, - ) + + // Preserve the explicit case_sensitive value before removeEmpty strips `false`. + const explicitCaseSensitive = dimension.case_sensitive; + const cleanedDimension = removeEmpty(dimension); + + // Re-inject case_sensitive after removeEmpty. Explicit values from + // lightdash.dimension.case_sensitive take priority; otherwise auto-set + // true on partition columns to prevent Lightdash from wrapping them in + // UPPER(), which breaks Trino-Iceberg predicate pushdown. + if (explicitCaseSensitive !== undefined) { + cleanedDimension.case_sensitive = explicitCaseSensitive; + } else if ( + dj.config.lightdashDefaultPartitionColumnCaseSensitive && + partitionColumnNames.includes(column.name) ) { - continue; + cleanedDimension.case_sensitive = true; } - metaFreeForm[key] = value; - } - - // Preserve the explicit case_sensitive value before removeEmpty strips `false`. - const explicitCaseSensitive = dimension.case_sensitive; - const cleanedDimension = removeEmpty(dimension); - // Re-inject case_sensitive after removeEmpty. Explicit values from - // lightdash.dimension.case_sensitive take priority; otherwise auto-set - // true on partition columns to prevent Lightdash from wrapping them in - // UPPER(), which breaks Trino-Iceberg predicate pushdown. - if (explicitCaseSensitive !== undefined) { - cleanedDimension.case_sensitive = explicitCaseSensitive; - } else if ( - dj.config.lightdashDefaultPartitionColumnCaseSensitive && - partitionColumnNames.includes(column.name) - ) { - cleanedDimension.case_sensitive = true; - } + column.meta = removeEmpty({ + ...metaFreeForm, + type: c.meta?.type, + origin: c.meta?.origin, + dimension: cleanedDimension, + metrics, + }); - column.meta = removeEmpty({ - ...metaFreeForm, - type: c.meta?.type, - origin: c.meta?.origin, - dimension: cleanedDimension, - metrics, - }); + // Re-inject column-level case_sensitive AFTER removeEmpty so a valid + // `false` value (intentionally opting OUT of case sensitivity) isn't + // stripped along with other empty/falsy values. + if (c.meta.case_sensitive !== undefined) { + column.meta = { + ...column.meta, + case_sensitive: c.meta.case_sensitive, + }; + } - // Re-inject column-level case_sensitive AFTER removeEmpty so a valid - // `false` value (intentionally opting OUT of case sensitivity) isn't - // stripped along with other empty/falsy values. - if (c.meta.case_sensitive !== undefined) { - column.meta = { - ...column.meta, - case_sensitive: c.meta.case_sensitive, - }; + // Remove any remaining empty column properties + modelPropertiesColumns.push(removeEmpty(column)); + } catch (err) { + const cause = err instanceof Error ? err.message : String(err); + throw new Error( + `Failed to build column "${columnContextName}" for model "${modelName}": ${cause}`, + ); } - - // Remove any remaining empty column properties - modelPropertiesColumns.push(removeEmpty(column)); } // Set data_tests at model level diff --git a/src/services/sync/constants.ts b/src/services/sync/constants.ts index feea30f..481763d 100644 --- a/src/services/sync/constants.ts +++ b/src/services/sync/constants.ts @@ -26,11 +26,19 @@ export const SYNC_BATCH_SIZES = { */ export const ERROR_MESSAGES = { /** - * Error message for invalid model SQL generation. + * Error message for a failed SQL/YML generation for a model. + * + * The wording intentionally avoids blaming any specific authoring construct + * (e.g. "double check any expr") because this wrapper catches every kind of + * failure -- malformed inherited dimension meta, missing parents, invalid + * schema-shape after manifest round-trip, etc. -- not just SQL expressions. + * Misleading users into auditing `expr` syntax for an unrelated failure + * caused real debugging pain in production. + * * @param errorMsg - The underlying error message from the generator */ INVALID_MODEL_SQL: (errorMsg: string) => - `Invalid model sql detected, please double check any "expr" \n\n${errorMsg}`, + `Failed to generate SQL/YML for this model:\n\n${errorMsg}`, /** Error when model prefix cannot be determined */ UNABLE_TO_GET_PREFIX: (resourceName: string) => diff --git a/src/shared/index.test.ts b/src/shared/index.test.ts index d5d5274..3a6b237 100644 --- a/src/shared/index.test.ts +++ b/src/shared/index.test.ts @@ -1,4 +1,4 @@ -import { mergeDeep } from '@shared'; +import { mergeDeep, yamlParse, yamlStringify } from '@shared'; import type { FrameworkColumn } from '@shared/framework/types'; describe('mergeDeep', () => { @@ -38,3 +38,46 @@ describe('mergeDeep', () => { expect(actual).toEqual(expected); }); }); + +describe('yamlStringify', () => { + // Regression guard for: dbt loads YAML with PyYAML (YAML 1.1) which parses + // the unquoted tokens OFF / NO / YES / ON (case-insensitive) as booleans. + // Our own emit must quote them so the manifest round-trip preserves the + // string value users actually wrote (e.g. lightdash time_intervals: "OFF"). + test.each([ + ['OFF', '"OFF"'], + ['off', '"off"'], + ['Off', '"Off"'], + ['NO', '"NO"'], + ['no', '"no"'], + ['YES', '"YES"'], + ['ON', '"ON"'], + ['Y', '"Y"'], + ['N', '"N"'], + ])('quotes YAML 1.1 boolean token %p as %p', (input, expected) => { + const out = yamlStringify({ time_intervals: input }); + expect(out).toContain(`time_intervals: ${expected}`); + // And it round-trips back to the same string under default (1.2) parse. + expect(yamlParse(out)).toEqual({ time_intervals: input }); + }); + + test('still emits ordinary strings unquoted', () => { + const out = yamlStringify({ label: 'Datetime', name: 'datetime' }); + expect(out).toBe('label: Datetime\nname: datetime\n'); + }); + + test('quotes nested boolean-token strings inside lightdash-style dimensions', () => { + const input = { + columns: [ + { + name: 'stop_date', + meta: { dimension: { time_intervals: 'OFF' } }, + }, + ], + }; + const out = yamlStringify(input); + expect(out).toContain('time_intervals: "OFF"'); + // Round-trip must preserve the string under the default (1.2) parser. + expect(yamlParse(out)).toEqual(input); + }); +}); diff --git a/src/shared/index.ts b/src/shared/index.ts index 933bfba..471260e 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -26,8 +26,16 @@ export type DJ = { }; const yamlParse = yaml.parse; +// Stringify with YAML 1.1 quoting semantics so reserved boolean tokens +// (OFF / NO / YES / ON / Y / N / off / no / yes / on / etc.) are emitted as +// quoted strings. dbt loads YAML with PyYAML which uses YAML 1.1 — without +// this, an unquoted scalar like `time_intervals: OFF` round-trips through the +// dbt manifest as the boolean `false`, which then crashes downstream code +// that expects either the literal string "OFF" or a list per the lightdash +// dimension schema. Parsing intentionally stays on the YAML 1.2 default so +// our own re-reads keep treating quoted values as strings. const yamlStringify = (obj: object) => - yaml.stringify(obj, { aliasDuplicateObjects: false }); + yaml.stringify(obj, { aliasDuplicateObjects: false, version: '1.1' }); export { yamlParse, yamlStringify }; export { From 815db8a5c13e741c4122ff17df6e829e331a4331 Mon Sep 17 00:00:00 2001 From: gowtham raj j Date: Mon, 15 Jun 2026 11:48:40 +0530 Subject: [PATCH 2/8] Trino Query Control Center (#60) * New Trino Query Control Center - a master-detail panel for inspecting and triaging Trino queries, replacing the Query View. --- AGENTS.md | 13 + CHANGELOG.md | 6 + docs/integrations/trino-integration.md | 22 +- package.json | 105 +- src/services/__tests__/agent.test.ts | 41 + src/services/api.ts | 16 +- src/services/constants.ts | 5 + src/services/framework/index.ts | 1 - src/services/framework/utils/sql-utils.ts | 2 +- src/services/trino.ts | 935 ++++++++++++++---- .../trino/__tests__/findModelForSql.test.ts | 196 ++++ src/services/trino/__tests__/profiles.test.ts | 197 ++++ .../__tests__/queryJsonSanitizer.test.ts | 560 +++++++++++ .../trino/__tests__/restClient.test.ts | 119 +++ src/services/trino/findModelForSql.ts | 176 ++++ src/services/trino/profiles.ts | 391 ++++++++ src/services/trino/queryJsonSanitizer.ts | 896 +++++++++++++++++ src/services/trino/restClient.ts | 479 +++++++++ src/shared/trino/parse.ts | 123 +++ src/shared/trino/types.ts | 367 +++++-- templates/skills/dj-trino-analyzer/_SKILL.md | 312 ++++++ .../references/operator-stats.md | 140 +++ .../references/query-info.md | 78 ++ .../references/query-stats.md | 108 ++ .../dj-trino-analyzer/references/recipes.md | 186 ++++ .../references/stage-and-task-stats.md | 141 +++ .../references/types-and-enums.md | 127 +++ tests/fixtures/manifest.json | 2 +- web/src/context/app.tsx | 297 ++++-- web/src/elements/Button.tsx | 2 + web/src/elements/Chip.tsx | 41 + web/src/elements/CodeBlock.tsx | 12 +- web/src/elements/SlimBanner.tsx | 42 + web/src/elements/index.ts | 2 + .../DataModeling/components/ErrorMessage.tsx | 4 +- web/src/main.css | 25 +- web/src/pages/DataExplorer/DataExplorer.tsx | 10 +- web/src/pages/Home.tsx | 4 +- web/src/pages/ModelLineage/ModelLineage.tsx | 2 +- .../ConnectionPanel/StatusLine.tsx | 73 ++ .../ConnectionPanel/index.tsx | 83 ++ .../ProfilesManager/ConnectionEditor.tsx | 304 ++++++ .../ProfilesManager/SectionHeader.tsx | 15 + .../ProfilesManager/index.tsx | 119 +++ .../QueryDetail/ErrorTab.tsx | 52 + .../QueryDetail/LabeledValue.tsx | 18 + .../QueryDetail/MetadataCard.tsx | 70 ++ .../QueryDetail/ModelLabel.tsx | 69 ++ .../QueryDetail/OperatorTable.tsx | 153 +++ .../QueryDetail/OverviewTab.tsx | 28 + .../QueryDetail/QueryInfoCard.tsx | 80 ++ .../QueryDetail/QuerySqlCard.tsx | 117 +++ .../QueryDetail/Section.tsx | 16 + .../QueryDetail/StageNode.tsx | 118 +++ .../QueryDetail/StageTree.tsx | 27 + .../QueryDetail/StatCard.tsx | 14 + .../QueryDetail/StatsGrid.tsx | 70 ++ .../QueryControlCenter/QueryDetail/index.tsx | 434 ++++++++ .../QueryList/HistoryTab.tsx | 357 +++++++ .../QueryList/LiveListingControls.tsx | 62 ++ .../QueryControlCenter/QueryList/LiveTab.tsx | 189 ++++ .../QueryList/QueryFilterBar.tsx | 166 ++++ .../QueryControlCenter/QueryList/QueryRow.tsx | 117 +++ .../QueryControlCenter/QueryList/index.tsx | 25 + .../QueryList/queryFilters.ts | 62 ++ .../QueryControlCenter/TrinoLiveContext.ts | 83 ++ .../QueryControlCenter/TrinoLiveProvider.tsx | 355 +++++++ web/src/pages/QueryControlCenter/format.ts | 66 ++ web/src/pages/QueryControlCenter/index.tsx | 91 ++ web/src/pages/QueryControlCenter/types.ts | 33 + .../pages/QueryControlCenter/useTrinoLive.ts | 14 + web/src/pages/QueryView.tsx | 203 ---- web/src/stores/dataExplorerStore.ts | 1 - 73 files changed, 9345 insertions(+), 524 deletions(-) create mode 100644 src/services/trino/__tests__/findModelForSql.test.ts create mode 100644 src/services/trino/__tests__/profiles.test.ts create mode 100644 src/services/trino/__tests__/queryJsonSanitizer.test.ts create mode 100644 src/services/trino/__tests__/restClient.test.ts create mode 100644 src/services/trino/findModelForSql.ts create mode 100644 src/services/trino/profiles.ts create mode 100644 src/services/trino/queryJsonSanitizer.ts create mode 100644 src/services/trino/restClient.ts create mode 100644 src/shared/trino/parse.ts create mode 100644 templates/skills/dj-trino-analyzer/_SKILL.md create mode 100644 templates/skills/dj-trino-analyzer/references/operator-stats.md create mode 100644 templates/skills/dj-trino-analyzer/references/query-info.md create mode 100644 templates/skills/dj-trino-analyzer/references/query-stats.md create mode 100644 templates/skills/dj-trino-analyzer/references/recipes.md create mode 100644 templates/skills/dj-trino-analyzer/references/stage-and-task-stats.md create mode 100644 templates/skills/dj-trino-analyzer/references/types-and-enums.md create mode 100644 web/src/elements/Chip.tsx create mode 100644 web/src/elements/SlimBanner.tsx create mode 100644 web/src/pages/QueryControlCenter/ConnectionPanel/StatusLine.tsx create mode 100644 web/src/pages/QueryControlCenter/ConnectionPanel/index.tsx create mode 100644 web/src/pages/QueryControlCenter/ProfilesManager/ConnectionEditor.tsx create mode 100644 web/src/pages/QueryControlCenter/ProfilesManager/SectionHeader.tsx create mode 100644 web/src/pages/QueryControlCenter/ProfilesManager/index.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryDetail/ErrorTab.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryDetail/LabeledValue.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryDetail/MetadataCard.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryDetail/ModelLabel.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryDetail/OperatorTable.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryDetail/OverviewTab.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryDetail/QueryInfoCard.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryDetail/QuerySqlCard.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryDetail/Section.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryDetail/StageNode.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryDetail/StageTree.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryDetail/StatCard.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryDetail/StatsGrid.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryDetail/index.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryList/HistoryTab.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryList/LiveListingControls.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryList/LiveTab.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryList/QueryFilterBar.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryList/QueryRow.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryList/index.tsx create mode 100644 web/src/pages/QueryControlCenter/QueryList/queryFilters.ts create mode 100644 web/src/pages/QueryControlCenter/TrinoLiveContext.ts create mode 100644 web/src/pages/QueryControlCenter/TrinoLiveProvider.tsx create mode 100644 web/src/pages/QueryControlCenter/format.ts create mode 100644 web/src/pages/QueryControlCenter/index.tsx create mode 100644 web/src/pages/QueryControlCenter/types.ts create mode 100644 web/src/pages/QueryControlCenter/useTrinoLive.ts delete mode 100644 web/src/pages/QueryView.tsx diff --git a/AGENTS.md b/AGENTS.md index 89238cb..b6ba2da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -422,6 +422,16 @@ import { useModelStore } from '@web/stores/useModelStore'; - **Strict mode**: enabled - Paths configured for import aliases (see above) +### Comment & Doc Style + +Comments and JSDoc must be neutral, self-contained descriptions of the code as it exists. They serve both newcomers and contributors familiar with the codebase, so phrasing that references past iterations, chat threads, code reviews, or the bug that prompted a change is noise to either audience. Apply these rules: + +- **No chat or review artifacts.** Don't write "the user reported …", "as discussed", "we just fixed …", or quote bug reports. +- **No before/after framing.** Avoid "used to", "previously", "this used to flash", "now updated", "has been fixed". Describe present behavior and the invariant it maintains, regardless of whether the code has shipped. +- **Cite the alternative, not the bug.** When a comment justifies a non-obvious design choice, contrast it with the alternative ("a poll here would race the in-flight RPC"), not with the prior broken version or the discussion that motivated it. Chat / review context can become a comment only when reframed as a forward-looking design rationale. +- **Prefer one sentence of intent over a paragraph of narration.** Skip restating what the code clearly does ("Set loading to true.") and only call out non-obvious why. +- **No duplicated docstrings.** If the JSDoc on a context field already says everything, don't repeat it inline at the call site. + ### Test Configuration **Jest Configuration** (jest.config.js): @@ -451,6 +461,9 @@ When adding features or making notable changes, update `CHANGELOG.md`: - Use the same style as existing entries — short, descriptive, no prefix labels - Group related changes into a single bullet when possible - Do not add date suffixes or create new version headings unless explicitly asked +- **Write for end users, not for reviewers.** Lead with capability ("Lets you do X") and the user-facing command / setting name. Skip internal identifiers — REST endpoint paths, API parameter names (`prefer: 'persisted' | 'rest'`), TypeScript class names, CSS / theme tokens, schema filenames — unless the user will actually type or read them. +- **Keep each bullet skim-readable.** Aim for 2–4 short sentences. If a bullet runs longer than ~100 words, split it or trim it. Bombarding the reader with every implementation detail makes the changelog unreadable. +- **Cite paths users will inspect** (e.g. `.dj/diagnostics/`, `~/.dbt/profiles.yml`, `templates/skills//SKILL.md`) and omit paths they won't (internal source files under `src/` or `web/src/`). ### Naming Conventions diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a3d1f2..57d8698 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## 1.8.0 +### Trino Query Control Center + +- **New Query Control Center (`DJ: Query Control Center`)** — a master-detail panel that replaces the Query View for inspecting and triaging Trino queries. A **Live** tab (queries from your active coordinator, or the local Trino CLI when no profile is set, with a "dbt runs only" filter) and a **History** tab both support search, state, and user/source filtering. Selecting a query shows its summary, stage tree, slowest operators, failure details, and SQL, plus **Jump to Model** (opens the matching `.model.json`) and **Analyze with AI**, which saves sanitized JSON under `.dj/diagnostics/` so analyzed queries reopen even after the coordinator evicts them (~15 min). +- **Named Trino connection profiles.** `dj.trino.profiles` and `dj.trino.activeProfile` define coordinator profiles (dev / staging / prod) you switch from the panel or `DJ: Select Trino Connection Profile...`. Each profile resolves its secret at request time from VS Code SecretStorage (set via `DJ: Set Trino Credentials...`), an environment variable, a password file, or your `~/.dbt/profiles.yml` — never plain text in settings — and the panel shows a coordinator status indicator with one-click refresh for expired tokens. +- **New `dj-trino-analyzer` agent skill** (`.agents/skills/dj-trino-analyzer/SKILL.md`, written when `dj.codingAgent` is on) gives a coding agent operator-level heuristics for diagnosing slow or failed Trino queries from the sanitized JSON, plus a bundled Trino QueryInfo field reference (`references/`, verified against the Trino 479 source) for deep dives into the raw `.dj/diagnostics/.full.json` — schema tables, enum gotchas, and ready-to-paste jq recipes. The sanitizer doubles as a tool firewall: payloads containing row data are rejected before they reach disk, so customer data never reaches an LLM prompt. + ### Bug fixes - **YAML reserved tokens round-trip safely.** Values like `OFF`, `ON`, `YES`, `NO` (and lowercase variants) are now quoted on emit and tolerated on load, so `time_intervals: OFF` no longer turns into `false` in the manifest and crashes sync. Per-column meta failures also name the offending column. diff --git a/docs/integrations/trino-integration.md b/docs/integrations/trino-integration.md index 3102c27..5c0dfc0 100644 --- a/docs/integrations/trino-integration.md +++ b/docs/integrations/trino-integration.md @@ -54,34 +54,30 @@ When you create a source, DJ connects to Trino to browse catalogs and retrieve t ## Query Engine Monitoring -The Query Engine view in the sidebar provides real-time monitoring of your Trino cluster activity. +The Query Engine view in the sidebar gives at-a-glance Trino cluster status and a one-click entry point to the full Query Control Center. **What You Can Monitor:** - **Nodes** - Active Trino worker nodes and their status (active/inactive) -- **My Queries** - Your currently running queries with states: - - 🟢 FINISHED - Query completed successfully - - 🔵 RUNNING - Query is currently executing - - ⚪ QUEUED - Query waiting to execute - - 🔴 FAILED - Query encountered an error +- **Query Control Center** - Click to open the master-detail webview that lists live and persisted Trino queries with stage/operator stats, an `Analyze with AI` action that writes a sanitized diagnostic JSON, and a `Jump to Model` action that resolves the SQL back to the originating `.model.json`. **Accessing Query Engine View:** 1. Open DJ sidebar in VS Code (Activity Bar icon) 2. Locate "Query Engine" view (database icon) -3. Expand sections to see: +3. The view shows: - **Nodes** (with count of active nodes) - - **My Queries** (with count of queries) + - **Query Control Center** shortcut **What's Monitored:** -This view shows queries executed by DJ when: +The Query Control Center surfaces: -- Running dbt models in Data Explorer -- Compiling models that query Trino -- Creating sources (Trino introspection queries) +- **Live** queries on the active Trino coordinator (polled every 5s; optional "My dbt runs only" filter chip) +- **Recent (persisted)** queries from `.dj/diagnostics/` — survives coordinator in-memory eviction +- For each query: state, wall/CPU time, peak memory, splits, blocked time, data-skew score, largest operator, stage tree, operator table with heuristic chips, the failure block for `FAILED` queries, and the SQL viewer -**Note:** This is a **monitoring view only** - you cannot execute custom SQL queries through this interface. It displays activity from DJ operations that use Trino internally. +**Note:** Custom SQL execution still happens through dbt / the Data Explorer; the Query Engine view itself is a monitoring entry point only. ## Model Execution diff --git a/package.json b/package.json index ec9b48e..fbfcc8c 100644 --- a/package.json +++ b/package.json @@ -218,8 +218,25 @@ "title": "DJ: Jump to YAML" }, { - "command": "dj.command.queryView", - "title": "DJ: Query View" + "command": "dj.command.queryControlCenter", + "title": "DJ: Query Control Center", + "icon": "$(dashboard)" + }, + { + "command": "dj.command.analyzeQuery", + "title": "DJ: Analyze Trino Query with AI" + }, + { + "command": "dj.command.setTrinoCredentials", + "title": "DJ: Set Trino Credentials..." + }, + { + "command": "dj.command.selectTrinoProfile", + "title": "DJ: Select Trino Connection Profile..." + }, + { + "command": "dj.command.jumpToModelFromQuery", + "title": "DJ: Jump to Model from Trino Query" }, { "command": "dj.command.openDataExplorerWithModel", @@ -348,6 +365,90 @@ "default": "trino-cli", "description": "Path to Trino CLI. Supports: command name ('trino', 'trino-cli'), full path ('/usr/local/bin/trino'), or directory ('/usr/local/bin' → checks for 'trino-cli' then 'trino'). Defaults to 'trino-cli' from PATH. Takes effect on next command execution." }, + "dj.trino.profiles": { + "type": "array", + "default": [], + "markdownDescription": "Named Trino coordinator profiles used by the **Query Control Center** (`DJ: Query Control Center`). Each profile is non-secret connection metadata — passwords / bearer tokens are stored separately in VS Code SecretStorage, env vars, password files, or `~/.dbt/profiles.yml` based on `authSource`. Plain-text secrets in this setting are intentionally not supported.\n\nRun **DJ: Set Trino Credentials...** to store a SecretStorage-backed credential, and **DJ: Select Trino Connection Profile...** to switch the active profile.", + "items": { + "type": "object", + "required": [ + "name", + "coordinatorUrl", + "user", + "authMethod", + "authSource" + ], + "properties": { + "name": { + "type": "string", + "description": "Unique profile name (e.g. 'dev', 'staging', 'prod')." + }, + "coordinatorUrl": { + "type": "string", + "description": "Trino coordinator base URL (e.g. https://trino.prod.example.com)." + }, + "user": { + "type": "string", + "description": "X-Trino-User header value." + }, + "authMethod": { + "type": "string", + "enum": [ + "none", + "basic", + "bearer", + "password-file" + ], + "default": "none", + "description": "HTTP auth scheme to send. 'none' = no Authorization header." + }, + "authSource": { + "type": "string", + "enum": [ + "secret-storage", + "env-var", + "password-file", + "dbt-profile" + ], + "default": "secret-storage", + "markdownDescription": "Where to read the secret from at request time. Never stored in this settings file.\n\n- `secret-storage` — VS Code SecretStorage (OS keychain). Use `DJ: Set Trino Credentials...` to store.\n- `env-var` — process env var named in `secretEnvVar`.\n- `password-file` — file path in `passwordFilePath`.\n- `dbt-profile` — read `password` / `password_file` / `jwt_token` from `~/.dbt/profiles.yml`." + }, + "verifyTls": { + "type": "boolean", + "default": true, + "description": "Verify TLS certificates. Set to false for self-signed certs on private deployments." + }, + "secretEnvVar": { + "type": "string", + "description": "Env var name (used when authSource = 'env-var')." + }, + "passwordFilePath": { + "type": "string", + "description": "Path to a file containing the credential (used when authSource = 'password-file'). Supports leading '~/'." + }, + "dbtProfile": { + "type": "string", + "description": "dbt profile name in ~/.dbt/profiles.yml (used when authSource = 'dbt-profile')." + }, + "dbtTarget": { + "type": "string", + "description": "dbt target name (defaults to the profile's `target`)." + } + } + } + }, + "dj.trino.activeProfile": { + "type": "string", + "default": "", + "markdownDescription": "Name of the active Trino coordinator profile from `dj.trino.profiles`. Falls back to the first profile when blank. The **Query Control Center** header dropdown also writes here." + }, + "dj.trino.diagnosticsRetentionDays": { + "type": "number", + "default": 30, + "minimum": 1, + "maximum": 365, + "markdownDescription": "How long to keep sanitized Trino query JSON in `.dj/diagnostics/` before the **History** tab garbage-collects it. Coordinator memory only retains query metadata for ~15 minutes; persisted diagnostics survive coordinator eviction." + }, "dj.lightdashProjectPath": { "type": "string", "description": "Custom path to the dbt project directory for Lightdash preview (relative to workspace root). If not set, will auto-detect from available dbt projects. Takes effect on next command execution." diff --git a/src/services/__tests__/agent.test.ts b/src/services/__tests__/agent.test.ts index cf17d3d..fe303d6 100644 --- a/src/services/__tests__/agent.test.ts +++ b/src/services/__tests__/agent.test.ts @@ -39,6 +39,47 @@ describe('Skills', () => { }, ); + test.each(skillDirs.map((d) => d.name))( + '%s relative markdown links resolve to bundled files', + (dirName) => { + const skillDir = path.join(SKILLS_DIR, dirName); + const mdFiles: string[] = []; + const walk = (dir: string) => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(full); + } else if (entry.name.endsWith('.md')) { + mdFiles.push(full); + } + } + }; + walk(skillDir); + + for (const mdFile of mdFiles) { + const content = fs.readFileSync(mdFile, 'utf-8'); + const links = [...content.matchAll(/\]\(([^)\s]+)\)/g)].map( + (m) => m[1], + ); + for (const target of links) { + // Skip external URLs (scheme prefix) and in-page anchors. + if (/^[a-z][a-z+.-]*:/i.test(target) || target.startsWith('#')) { + continue; + } + const resolved = path.resolve( + path.dirname(mdFile), + target.split('#')[0], + ); + const link = { in: path.relative(SKILLS_DIR, mdFile), target }; + expect({ ...link, exists: fs.existsSync(resolved) }).toEqual({ + ...link, + exists: true, + }); + } + } + }, + ); + test('_AGENTS.md template exists and contains expected content', () => { const content = fs.readFileSync(AGENTS_TEMPLATE, 'utf-8'); expect(content).toBeTruthy(); diff --git a/src/services/api.ts b/src/services/api.ts index 9b1f4c3..17c3e4d 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -129,14 +129,22 @@ export class Api { )) as ApiResponse; case 'trino-fetch-catalogs': case 'trino-fetch-columns': - case 'trino-fetch-current-schema': case 'trino-fetch-etl-sources': case 'trino-fetch-schemas': case 'trino-fetch-system-nodes': - case 'trino-fetch-system-queries': - case 'trino-fetch-system-query-with-task': - case 'trino-fetch-system-query-sql': case 'trino-fetch-tables': + case 'trino-fetch-query-info': + case 'trino-fetch-active-queries': + case 'trino-fetch-persisted-queries': + case 'trino-delete-persisted-query': + case 'trino-analyze-query': + case 'trino-list-profiles': + case 'trino-save-profile': + case 'trino-delete-profile': + case 'trino-set-active-profile': + case 'trino-set-credentials': + case 'trino-ping-coordinator': + case 'trino-jump-to-model-from-query': return (await this.trino.handleApi(payload as any)) as ApiResponse; case 'state-load': case 'state-save': diff --git a/src/services/constants.ts b/src/services/constants.ts index 93d7b82..dc58c58 100644 --- a/src/services/constants.ts +++ b/src/services/constants.ts @@ -85,6 +85,11 @@ export const COMMAND_ID = { FRAMEWORK_JUMP_MODEL: 'dj.command.frameworkJumpModel', FRAMEWORK_JUMP_YAML: 'dj.command.frameworkYaml', JSON_SYNC: 'dj.command.jsonSync', + QUERY_CONTROL_CENTER: 'dj.command.queryControlCenter', + ANALYZE_QUERY: 'dj.command.analyzeQuery', + SET_TRINO_CREDENTIALS: 'dj.command.setTrinoCredentials', + SELECT_TRINO_PROFILE: 'dj.command.selectTrinoProfile', + JUMP_TO_MODEL_FROM_QUERY: 'dj.command.jumpToModelFromQuery', TEST_TRINO_CONNECTION: 'dj.command.testTrinoConnection', } as const; diff --git a/src/services/framework/index.ts b/src/services/framework/index.ts index b4f1afb..e71d846 100644 --- a/src/services/framework/index.ts +++ b/src/services/framework/index.ts @@ -99,7 +99,6 @@ export class Framework implements ApiEnabledService<'framework'> { private statusBarItem!: vscode.StatusBarItem; validateSourceJson: ValidateFunction | undefined; webviewPanelModelCreate: vscode.WebviewPanel | undefined; - webviewPanelQueryView: vscode.WebviewPanel | undefined; webviewPanelSourceCreate: vscode.WebviewPanel | undefined; // Track files that are locked during DJ Sync operations diff --git a/src/services/framework/utils/sql-utils.ts b/src/services/framework/utils/sql-utils.ts index 2e89360..6343ca0 100644 --- a/src/services/framework/utils/sql-utils.ts +++ b/src/services/framework/utils/sql-utils.ts @@ -167,7 +167,7 @@ export function normalizeTimeIntervals( .uniq() .value() as TimeIntervals; } - // eslint-disable-next-line no-console + console.warn( `[dj] Ignoring unsupported time_intervals value on ${context.modelName}.${context.columnName}: ` + `expected "OFF" or an array of strings, got ${typeof value} (${JSON.stringify(value)}).`, diff --git a/src/services/trino.ts b/src/services/trino.ts index fb46e66..4cf0f10 100644 --- a/src/services/trino.ts +++ b/src/services/trino.ts @@ -1,5 +1,27 @@ import type { Coder } from '@services/coder'; import { COMMAND_ID } from '@services/constants'; +import { findModelForSql } from '@services/trino/findModelForSql'; +import { + deleteProfile, + getActiveProfile, + getProfileByName, + listProfiles, + setActiveProfile, + storeSecret, + upsertProfile, +} from '@services/trino/profiles'; +import { + listPersistedQueries, + readPersistedSanitizedQuery, + reapOldDiagnostics, + sanitizeAndPersist, +} from '@services/trino/queryJsonSanitizer'; +import { + shapeQueryInfo, + shapeQuerySummary, + TrinoCoordinatorError, + TrinoRestClient, +} from '@services/trino/restClient'; import type { ApiEnabledService } from '@services/types'; import { buildProcessEnv } from '@services/utils/process'; import { getHtml } from '@services/webview/utils'; @@ -7,12 +29,12 @@ import { assertExhaustive } from '@shared'; import type { ApiMessage, ApiPayload, ApiResponse } from '@shared/api/types'; import { apiResponse } from '@shared/api/utils'; import type { FrameworkEtlSource } from '@shared/framework/types'; -import { QUERY_NOT_AVAILABLE } from '@shared/trino/constants'; import type { + TrinoActiveQueriesResponse, TrinoApi, + TrinoProfile, + TrinoQuerySummary, TrinoSystemNode, - TrinoSystemQuery, - TrinoSystemQueryWithTask, TrinoTable, TrinoTableColumn, } from '@shared/trino/types'; @@ -24,18 +46,23 @@ import { WORKSPACE_ROOT, } from 'admin'; import { spawn } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; import * as vscode from 'vscode'; const POLLING_INTERVAL_SYSTEM_INFO = 60000; // 60 seconds export class Trino implements ApiEnabledService<'trino'> { coder: Coder; - currentSchema: string | null = null; handleApi: (payload: ApiPayload<'trino'>) => Promise; systemNodes: TrinoSystemNode[] | null = null; tables = new Map(); timeoutSystemInfo: NodeJS.Timeout | null = null; viewQueryEngine: TreeDataInstance; + /** Singleton Query Control Center panel (refocused on subsequent opens). */ + private queryControlCenterPanel?: vscode.WebviewPanel; + /** Cached REST client. Invalidated whenever the active profile changes. */ + private restClient?: { client: TrinoRestClient; profileName: string }; constructor({ coder }: { coder: Coder }) { this.coder = coder; @@ -62,13 +89,6 @@ export class Trino implements ApiEnabledService<'trino'> { }); return apiResponse(columns); } - case 'trino-fetch-current-schema': { - const result = await this.handleQuery( - `select current_schema as schema`, - ); - const currentSchema: string = result[0]?.['schema'] ?? ''; - return apiResponse(currentSchema); - } case 'trino-fetch-etl-sources': { const { projectName, etlSchema } = payload.request; const schemaName = etlSchema || 'source_etl'; @@ -108,100 +128,201 @@ export class Trino implements ApiEnabledService<'trino'> { }); return apiResponse(nodes); } - case 'trino-fetch-system-queries': { - const { schema } = payload.request; - let sql = ` -select - "created", - "end", - "query_id", - "source", - "started", - "state" -from - system.runtime.queries -where - source like 'dbt-trino-%'`; - if (schema) { - sql += ` - and (query like '%."${schema}".%' or query like '%"schema": "${schema}"%')`; + case 'trino-fetch-tables': { + const { catalog, schema } = payload.request; + const tablesRaw = await this.handleQuery( + `show tables from ${catalog}.${schema}`, + ); + const tables = tablesRaw.map((r) => r['Table']); + return apiResponse(tables); + } + case 'trino-fetch-query-info': { + const { queryId, prefer = 'persisted' } = payload.request; + const diagnosticsPath = path.join( + WORKSPACE_ROOT, + '.dj', + 'diagnostics', + `${queryId}.json`, + ); + const fullDiagnosticsPath = path.join( + WORKSPACE_ROOT, + '.dj', + 'diagnostics', + `${queryId}.full.json`, + ); + // Persisted-first (default): the detail pane sends `prefer: + // 'persisted'` on every row click and only flips to `'rest'` + // when the user clicks "Refresh from coordinator" or + // "Analyze with AI". This means browsing the History tab + // never hits the network, and re-opening a previously- + // analyzed Live query is also free. + if (prefer === 'persisted') { + const persisted = await readPersistedSanitizedQuery(queryId); + if (persisted) { + const info = { + ...persisted, + modelMatch: findModelForSql( + persisted.query, + this.coder.dbt.models, + ), + loadedFrom: 'persisted' as const, + jsonPath: diagnosticsPath, + // The raw coordinator snapshot is written alongside the + // sanitized JSON by `sanitizeAndPersist`. Surface its + // path only when the file actually exists on disk — + // older diagnostics created before the full snapshot + // was added (or hand-edited setups) may not have one. + fullJsonPath: fs.existsSync(fullDiagnosticsPath) + ? fullDiagnosticsPath + : undefined, + }; + return apiResponse(info); + } + // No local copy yet — fall through to a REST fetch and + // persist the sanitized result so the next click is free. } - sql += ` -order by created desc;`; - const queriesRaw = await this.handleQuery(sql, { - filename: payload.type, - }); - const queries: TrinoSystemQuery[] = queriesRaw.map((r) => { - return { - // analysis_time_ms: Number(r['analysis_time_ms']), - created: r['created'], - end: r['end'], - // error_code: r['error_code'], - // error_type: r['error_type'], - // last_heartbeat: r['last_heartbeat'], - // planning_time_ms: Number(r['planning_time_ms']), - // queued_time_ms: Number(r['queued_time_ms']), - // query: r['query'], - query_id: r['query_id'], - // resource_group_id: r['resource_group_id'], - source: r['source'], - started: r['started'], - state: r['state'], - // user: r['user'], - }; + + // Fetch the raw QueryInfo so we can both sanitize-and-persist + // (filling the local diagnostics cache so the next click on + // this same queryId is free) and return a clean shaped form + // to the UI in one round-trip. + const rawQueryInfo = + await this.getRestClient().getRawQueryInfo(queryId); + // Persistence is best-effort. If the firewall rejects the + // payload or disk is unwritable, the user still gets the + // detail pane (just no on-disk cache for next time). + const persisted = await sanitizeAndPersist(rawQueryInfo, { + source: this.activeProfileSource(), + }).catch((err: unknown) => { + this.coder.log.warn( + `Failed to persist sanitized QueryInfo for ${queryId}: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + return null; }); - return apiResponse(queries); + const info = persisted + ? { ...persisted.sanitized } + : shapeQueryInfo(rawQueryInfo); + info.modelMatch = findModelForSql(info.query, this.coder.dbt.models); + info.loadedFrom = 'rest'; + if (persisted) { + info.jsonPath = persisted.jsonPath; + if (fs.existsSync(persisted.fullJsonPath)) { + info.fullJsonPath = persisted.fullJsonPath; + } + } + // Stamp the active profile onto the response too. For + // persisted-then-refreshed snapshots the sanitized JSON + // already carries these (sanitizeAndPersist copies them in) + // but the persisted-first path below also wants them, so we + // set them unconditionally from the active profile here. + const profileSource = this.activeProfileSource(); + if (profileSource) { + info.profileName = profileSource.profileName; + info.coordinatorUrl = profileSource.coordinatorUrl; + } + return apiResponse(info); } - case 'trino-fetch-system-query-with-task': { - const sql = ` -select - t."completed_splits", - q."created", - q."end", - q."query_id", - t."running_splits", - q."source", - t."splits", - q."started", - q."state", - t."queued_splits" -from - system.runtime.queries q left join system.runtime.tasks t on q.query_id = t.query_id -where - q.query_id = '${payload.request.id}';`; - // We are handling this as a non json output because of this trino issue: https://github.com/trinodb/trino/issues/18525 - const queryRaw = await this.handleQuery(sql, { - filename: payload.type, - }); - const r = queryRaw[0]; - if (!r) { - throw new Error(QUERY_NOT_AVAILABLE); + case 'trino-fetch-active-queries': { + const summaries = await this.fetchActiveQueries( + payload.request.filter, + ); + return apiResponse(summaries); + } + case 'trino-fetch-persisted-queries': { + const retentionDays = vscode.workspace + .getConfiguration() + .get('dj.trino.diagnosticsRetentionDays', 30); + // Opportunistic cleanup so .dj/diagnostics/ doesn't grow forever. + void reapOldDiagnostics(retentionDays).catch(() => undefined); + const persisted = await listPersistedQueries(); + return apiResponse(persisted); + } + case 'trino-delete-persisted-query': { + const queryId = payload.request.queryId; + const dir = path.join(WORKSPACE_ROOT, '.dj', 'diagnostics'); + const targets = [ + path.join(dir, `${queryId}.json`), + path.join(dir, `${queryId}.full.json`), + ]; + let deleted = false; + for (const target of targets) { + try { + await fs.promises.unlink(target); + deleted = true; + } catch (err) { + // ENOENT just means the file was already gone — fine. + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { + throw err; + } + } } - const queryWithTask = r as TrinoSystemQueryWithTask; - return apiResponse(queryWithTask); + return apiResponse({ queryId, deleted }); } - case 'trino-fetch-system-query-sql': { - this.coder.log.info('Fetching system query', payload.request.id); - // We are handling this as a non json output because of this trino issue: https://github.com/trinodb/trino/issues/18525 - const querySqlRaw = await this.handleQuery( - `select "query" from system.runtime.queries where query_id = '${payload.request.id}';`, - { - raw: true, - filename: payload.type, - }, + case 'trino-analyze-query': { + const result = await this.analyzeQuery(payload.request.queryId); + return apiResponse(result); + } + case 'trino-list-profiles': { + const profiles = listProfiles(); + const active = getActiveProfile()?.name ?? null; + return apiResponse({ profiles, active }); + } + case 'trino-save-profile': { + await upsertProfile( + this.coder.context, + payload.request.profile, + payload.request.previousName, ); - const querySql = querySqlRaw; - // - this.coder.log.info('Fetched system query', querySql); - return apiResponse(querySql); + this.invalidateRestClient(); + return apiResponse({ ok: true }); } - case 'trino-fetch-tables': { - const { catalog, schema } = payload.request; - const tablesRaw = await this.handleQuery( - `show tables from ${catalog}.${schema}`, + case 'trino-delete-profile': { + await deleteProfile(this.coder.context, payload.request.name); + this.invalidateRestClient(); + return apiResponse({ ok: true }); + } + case 'trino-set-active-profile': { + await setActiveProfile(payload.request.name); + this.invalidateRestClient(); + return apiResponse({ ok: true }); + } + case 'trino-set-credentials': { + await storeSecret( + this.coder.context, + payload.request.profile, + payload.request.kind, + payload.request.secret, + ); + this.invalidateRestClient(); + return apiResponse({ ok: true }); + } + case 'trino-ping-coordinator': { + const profile = payload.request.profile + ? getProfileByName(payload.request.profile) + : getActiveProfile(); + if (!profile) { + return apiResponse({ + ok: false, + error: 'No Trino profile configured.', + }); + } + const client = new TrinoRestClient( + this.coder.context, + profile, + this.coder.log, + ); + const ping = await client.pingCoordinator(); + return apiResponse(ping); + } + case 'trino-jump-to-model-from-query': { + const match = await this.jumpToModelFromQuery( + payload.request.queryId, + ); + return apiResponse( + match ? { matched: true, modelMatch: match } : { matched: false }, ); - const tables = tablesRaw.map((r) => r['Table']); - return apiResponse(tables); } default: return assertExhaustive(payload); @@ -223,31 +344,84 @@ where } /** - * Register Trino-specific commands - Query View, Test Trino Connection + * Register Trino-specific commands: Query Control Center, profile + credential + * management, Analyze, Jump-to-Model, Test Trino Connection. * @param context */ registerCommands(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand( - COMMAND_ID.QUERY_VIEW, - (queryId: string) => { - const panel = vscode.window.createWebviewPanel( - `dj_query_view_${queryId}`, - `Query View: ${queryId}`, - vscode.ViewColumn.One, - { enableFindWidget: true, enableScripts: true }, - ); - panel.webview.html = getHtml({ - extensionUri: context.extensionUri, - route: `/query/view/${queryId}`, - webview: panel.webview, - }); - panel.webview.onDidReceiveMessage(async (message: ApiMessage) => - this.coder.handleWebviewMessage({ - message, - webview: panel.webview, - }), - ); + COMMAND_ID.QUERY_CONTROL_CENTER, + (arg?: { queryId?: string } | string) => + this.openQueryControlCenter(context, arg), + ), + + vscode.commands.registerCommand( + COMMAND_ID.ANALYZE_QUERY, + async (arg?: { queryId?: string } | string) => { + const queryId = await this.resolveQueryId(arg); + if (!queryId) { + return; + } + try { + const result = await this.analyzeQuery(queryId); + const action = await vscode.window.showInformationMessage( + `Trino query ${queryId} sanitized to ${vscode.workspace.asRelativePath(result.jsonPath)}. ` + + `Open it in your AI agent with the dj-trino-analyzer skill loaded.`, + 'Open JSON', + 'Copy Prompt', + ); + if (action === 'Open JSON') { + await vscode.window.showTextDocument( + vscode.Uri.file(result.jsonPath), + ); + } else if (action === 'Copy Prompt') { + await vscode.env.clipboard.writeText(result.promptSnippet); + vscode.window.showInformationMessage( + 'Analysis prompt copied to clipboard.', + ); + } + } catch (err: unknown) { + vscode.window.showErrorMessage( + `Failed to analyze query: ${err instanceof Error ? err.message : String(err)}`, + ); + } + }, + ), + + vscode.commands.registerCommand( + COMMAND_ID.SET_TRINO_CREDENTIALS, + async () => this.setTrinoCredentialsQuickPick(), + ), + + vscode.commands.registerCommand( + COMMAND_ID.SELECT_TRINO_PROFILE, + async () => this.selectTrinoProfileQuickPick(), + ), + + vscode.commands.registerCommand( + COMMAND_ID.JUMP_TO_MODEL_FROM_QUERY, + async (arg?: { queryId?: string } | string) => { + const queryId = await this.resolveQueryId(arg); + if (!queryId) { + return; + } + const match = await this.jumpToModelFromQuery(queryId); + if (!match) { + vscode.window.showInformationMessage( + `No DJ model match found for query ${queryId}.`, + ); + return; + } + if (match.modelJsonPath) { + await vscode.window.showTextDocument( + vscode.Uri.file(match.modelJsonPath), + ); + } else { + vscode.window.showInformationMessage( + `Matched ${match.project}:${match.modelName} but .model.json file not found.`, + ); + } }, ), @@ -291,21 +465,33 @@ where sql: string, options?: { raw?: false; - filename?: TrinoApi['type'] | 'data-explorer-query.sql'; + filename?: + | TrinoApi['type'] + | 'data-explorer-query.sql' + | 'trino-fetch-active-queries-cli' + | 'trino-fetch-query-sql-cli'; }, ): Promise[]>; handleQuery( sql: string, options?: { raw: true; - filename?: TrinoApi['type'] | 'data-explorer-query.sql'; + filename?: + | TrinoApi['type'] + | 'data-explorer-query.sql' + | 'trino-fetch-active-queries-cli' + | 'trino-fetch-query-sql-cli'; }, ): Promise; async handleQuery( sql: string, options?: { raw?: boolean; - filename?: TrinoApi['type'] | 'data-explorer-query.sql'; + filename?: + | TrinoApi['type'] + | 'data-explorer-query.sql' + | 'trino-fetch-active-queries-cli' + | 'trino-fetch-query-sql-cli'; }, ): Promise[] | string> { const { path: trinoCommand } = getTrinoConfig(); @@ -410,35 +596,75 @@ where } /** - * Parse CSV output from Trino CLI into an array of objects - * Handles complex types like arrays, maps, and structs that are represented as strings + * Parse CSV output from Trino CLI into an array of objects. + * + * Per RFC 4180, newlines inside quoted cells belong to the cell, not + * the row separator. The previous implementation split on `\n` before + * checking quote state, which broke any column containing multi-line + * text — in particular the `query` column of `system.runtime.queries`, + * whose SQL bodies turned each newline into a fake row. This walks + * the buffer character-by-character and only emits a row when a + * newline is reached while OUTSIDE a quoted cell. */ private parseCsvOutput(csvData: string): Record[] { - const lines = csvData.split('\n').filter(Boolean); - if (lines.length === 0) { + const records = this.splitCsvRecords(csvData); + if (records.length === 0) { return []; } - // Parse header row - const headers = this.parseCsvLine(lines[0]); - - // Parse data rows + const headers = this.parseCsvLine(records[0]); const results: Record[] = []; - for (let i = 1; i < lines.length; i++) { - const values = this.parseCsvLine(lines[i]); + for (let i = 1; i < records.length; i++) { + const values = this.parseCsvLine(records[i]); const row: Record = {}; - for (let j = 0; j < headers.length; j++) { - const value = values[j]; - row[headers[j]] = this.parseTrinoValue(value); + row[headers[j]] = this.parseTrinoValue(values[j]); } - results.push(row); } - return results; } + /** + * Split CSV text into records, respecting quoted multi-line cells. + * A record boundary is a `\n` (or `\r\n`) seen while not in a quoted + * cell. Doubled quotes (`""`) inside a quoted cell are literal and + * don't toggle the quote state. + */ + private splitCsvRecords(csvData: string): string[] { + const records: string[] = []; + let current = ''; + let inQuotes = false; + for (let i = 0; i < csvData.length; i++) { + const ch = csvData[i]; + if (ch === '"') { + if (inQuotes && csvData[i + 1] === '"') { + current += '""'; + i += 1; + } else { + inQuotes = !inQuotes; + current += ch; + } + continue; + } + if ((ch === '\n' || ch === '\r') && !inQuotes) { + if (ch === '\r' && csvData[i + 1] === '\n') { + i += 1; + } + if (current.length > 0) { + records.push(current); + } + current = ''; + continue; + } + current += ch; + } + if (current.length > 0) { + records.push(current); + } + return records; + } + /** * Parse a single CSV line, handling quoted fields and embedded commas */ @@ -572,21 +798,16 @@ where async handleSystemInfo() { try { - this.currentSchema ??= await this.coder.api.handleApi({ - type: 'trino-fetch-current-schema', - request: null, - }); - this.systemNodes = await this.coder.api.handleApi({ type: 'trino-fetch-system-nodes', request: null, }); const queryNodes = this.systemNodes?.filter((n) => !n.coordinator) ?? []; - const queries = await this.coder.api.handleApi({ - type: 'trino-fetch-system-queries', - request: { schema: this.currentSchema }, - }); + // Sidebar layout: Trino (placeholder) → Nodes (at-a-glance + // coordinator/worker status) → Query Control Center (shortcut + // into the webview where per-query browsing lives, under its + // Live and History tabs). this.viewQueryEngine.setData([ { label: 'Trino' }, { @@ -603,29 +824,13 @@ where collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, }, { - label: 'My Queries', - description: String(queries.length), - children: - queries.map((q) => ({ - label: q.state, - description: q.query_id, - iconPath: - q.state === 'FINISHED' - ? new vscode.ThemeIcon('pass-filled') - : q.state === 'RUNNING' - ? new vscode.ThemeIcon('sync') - : q.state === 'QUEUED' - ? new vscode.ThemeIcon('circle-large') - : q.state === 'FAILED' - ? new vscode.ThemeIcon('error') - : undefined, - command: { - title: 'View Query', - command: COMMAND_ID.QUERY_VIEW, - arguments: [q.query_id], - }, - })) ?? [], - collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + label: 'Query Control Center', + iconPath: new vscode.ThemeIcon('dashboard'), + command: { + title: 'Open Query Control Center', + command: COMMAND_ID.QUERY_CONTROL_CENTER, + arguments: [], + }, }, ]); } catch (err: unknown) { @@ -637,9 +842,405 @@ where ); } + // ──────────────────────────────────────────────────────────────────── + // Query Control Center + REST API helpers + // ──────────────────────────────────────────────────────────────────── + + /** + * Open (or refocus) the singleton Query Control Center webview panel. + * When `arg.queryId` is supplied the master pane preselects that query. + */ + private openQueryControlCenter( + context: vscode.ExtensionContext, + arg?: { queryId?: string } | string, + ): void { + const queryId = typeof arg === 'string' ? arg : arg?.queryId ?? undefined; + + if (this.queryControlCenterPanel) { + this.queryControlCenterPanel.reveal(vscode.ViewColumn.Active); + if (queryId) { + this.queryControlCenterPanel.webview.postMessage({ + type: 'query-control-center-select', + queryId, + }); + } + return; + } + + const panel = vscode.window.createWebviewPanel( + 'dj_query_control_center', + 'DJ: Query Control Center', + vscode.ViewColumn.Active, + { + enableFindWidget: true, + enableScripts: true, + retainContextWhenHidden: true, + }, + ); + this.queryControlCenterPanel = panel; + panel.iconPath = new vscode.ThemeIcon('dashboard'); + + panel.webview.html = getHtml({ + extensionUri: context.extensionUri, + route: queryId + ? `/query/control-center?queryId=${encodeURIComponent(queryId)}` + : '/query/control-center', + webview: panel.webview, + }); + + panel.webview.onDidReceiveMessage(async (message: ApiMessage) => + this.coder.handleWebviewMessage({ + message, + webview: panel.webview, + }), + ); + + panel.onDidDispose(() => { + this.queryControlCenterPanel = undefined; + }); + } + + /** + * Lazily build a REST client for the currently-active profile. The + * client is cached on the service so we don't re-resolve the profile on + * every request. `invalidateRestClient()` clears the cache when the + * profile or its credentials change. + */ + getRestClient(): TrinoRestClient { + const profile = getActiveProfile(); + if (!profile) { + throw new TrinoCoordinatorError( + 'No Trino profile configured. Add one in Settings under `dj.trino.profiles` or open the Query Control Center → "Edit connections…".', + 'PROFILE_ERROR', + ); + } + if (this.restClient?.profileName !== profile.name) { + this.restClient = { + client: new TrinoRestClient( + this.coder.context, + profile, + this.coder.log, + ), + profileName: profile.name, + }; + } + return this.restClient.client; + } + + private invalidateRestClient(): void { + this.restClient = undefined; + } + + /** + * Snapshot of the currently-active profile, shaped for + * `sanitizeAndPersist`'s `source` option. Returns `undefined` when + * no profile is active or when the caller doesn't need to stamp a + * profile onto the diagnostic (e.g. the CLI listing path, which + * never persists). + */ + private activeProfileSource(): + | { profileName: string; coordinatorUrl: string } + | undefined { + const profile = getActiveProfile(); + if (!profile) { + return undefined; + } + return { + profileName: profile.name, + coordinatorUrl: profile.coordinatorUrl, + }; + } + + /** + * Resolve the active list of queries for the Query Control Center's + * Live tab. The decision is **deterministic on the active profile**, + * not on "did REST happen to work": + * + * - Profile active → REST `/v1/query` against THAT coordinator, + * so the rows always match the coordinator the rest of the panel + * (status pill, per-query JSON, Analyze-with-AI) is talking to. + * Intentionally NO silent CLI fallback: silently swapping the + * user's view from prod → dev rows on a transient REST blip + * would break coordinator identity. REST errors are re-thrown + * so the webview shows a clean error banner instead. + * - No profile → Trino CLI's `system.runtime.queries`. Works + * without any setup, mirroring the old "My Queries" sidebar. + * + * The envelope carries `source` + `profileName` so the UI can render + * a "Listing from: …" subtitle and the user never has to guess which + * coordinator they're looking at. The "My dbt runs only" filter is + * also applied server-side here for parity with the webview chip. + */ + async fetchActiveQueries( + filter?: 'all' | 'dbt-trino-only', + ): Promise { + const profile = getActiveProfile(); + // The QCC's own listing queries always touch `system.runtime.queries` + // (CLI path) — and analogous REST calls — and would otherwise + // dominate every poll. Filter them out so the Live tab shows real + // user work, not the panel watching itself. + const isSelfQuery = (q: TrinoQuerySummary): boolean => + typeof q.query === 'string' && + /\bsystem\.runtime\.queries\b/i.test(q.query); + const applyFilters = (rows: TrinoQuerySummary[]): TrinoQuerySummary[] => { + const dropSelf = rows.filter((q) => !isSelfQuery(q)); + return filter === 'dbt-trino-only' + ? dropSelf.filter((q) => (q.source ?? '').startsWith('dbt-trino-')) + : dropSelf; + }; + + if (profile) { + const rows = await this.getRestClient().listActiveQueries(); + return { + source: 'rest', + profileName: profile.name, + rows: applyFilters(rows), + }; + } + + return { + source: 'cli', + profileName: null, + rows: applyFilters(await this.fetchActiveQueriesViaCli()), + }; + } + + private async fetchActiveQueriesViaCli(): Promise { + // `system.runtime.queries` columns are stable across Trino versions: + // query_id, state, user, source, query, resource_group_id, + // queued, analysis_time, planning_time, created, started, + // last_heartbeat, end, error_type, error_code. + // `catalog` / `schema` are NOT exposed here (they're session-scoped, + // not query-scoped); selecting them errors out with "Column + // 'catalog' cannot be resolved" on most coordinators. They only + // arrive on the REST path via `session.catalog` / `session.schema`. + const sql = ` +select + "created", + "end", + "query_id", + "source", + "started", + "state", + "user", + "query" +from + system.runtime.queries +order by created desc +limit 200;`; + let rows: Record[]; + try { + rows = await this.handleQuery(sql, { + filename: 'trino-fetch-active-queries-cli', + }); + } catch (err: unknown) { + // Re-throw rather than swallowing into []: a silent empty list + // under the "Listing from: local Trino CLI" subtitle is + // indistinguishable from a true empty result, hiding genuine + // SQL/CLI failures behind the generic retention empty-state. + const message = err instanceof Error ? err.message : String(err); + throw new Error( + `Trino CLI listing failed (system.runtime.queries): ${message}`, + ); + } + return rows.map((r) => + shapeQuerySummary({ + queryId: r['query_id'], + state: r['state'], + query: r['query'], + session: { + user: r['user'], + source: r['source'], + }, + queryStats: { + createTime: r['created'], + executionStartTime: r['started'], + endTime: r['end'], + }, + }), + ); + } + + /** + * Look up the originating DJ model and emit the analyzer prompt for + * the sanitized JSON. + * + * Persisted-first: if `.json` already exists under + * `.dj/diagnostics/` (true for any query the user has analyzed or + * loaded full details on), reuse it and skip REST entirely. This is + * what lets the History tab analyze queries the coordinator no + * longer retains (otherwise HTTP 410 territory). + * + * Only when nothing is on disk do we hit REST to fetch + sanitize + + * persist a fresh copy — the live / fresh-query path. + */ + async analyzeQuery(queryId: string) { + const diagnosticsDir = path.join(WORKSPACE_ROOT, '.dj', 'diagnostics'); + const jsonPath = path.join(diagnosticsDir, `${queryId}.json`); + const fullJsonPath = path.join(diagnosticsDir, `${queryId}.full.json`); + + const persisted = await readPersistedSanitizedQuery(queryId); + let querySql: string; + if (persisted) { + querySql = persisted.query ?? ''; + } else { + const client = this.getRestClient(); + const raw = await client.getRawQueryInfo(queryId); + const result = await sanitizeAndPersist(raw, { + source: this.activeProfileSource(), + }); + querySql = result.sanitized.query ?? ''; + } + + const match = findModelForSql(querySql, this.coder.framework.dbt.models); + const promptSnippet = `Use the dj-trino-analyzer skill to analyze .dj/diagnostics/${queryId}.json (Trino QueryInfo for ${match ? `${match.project}:${match.modelName}` : 'an unknown DJ model'}). Start with the summary, then explain the slowest operator and recommend changes.`; + + return { + queryId, + jsonPath, + fullJsonPath, + modelMatch: match, + promptSnippet, + }; + } + + async jumpToModelFromQuery(queryId: string) { + // Prefer a persisted sanitized copy (no network round-trip). Fall + // back to a fresh REST fetch when nothing's persisted yet. + const persisted = await listPersistedQueries(); + let sql = persisted.find((p) => p.queryId === queryId)?.summary.query; + if (!sql) { + try { + const info = await this.getRestClient().getQueryInfo(queryId); + sql = info.query; + } catch (err: unknown) { + this.coder.log.warn('jumpToModelFromQuery: REST fetch failed:', err); + } + } + if (!sql) { + // Final fallback: query the system table directly. + try { + const querySql = await this.handleQuery( + `select "query" from system.runtime.queries where query_id = '${queryId}';`, + { raw: true, filename: 'trino-fetch-query-sql-cli' }, + ); + sql = querySql; + } catch { + return null; + } + } + if (!sql) { + return null; + } + return findModelForSql(sql, this.coder.framework.dbt.models); + } + + private async resolveQueryId( + arg?: { queryId?: string } | string, + ): Promise { + if (typeof arg === 'string') { + return arg; + } + if (arg?.queryId) { + return arg.queryId; + } + const entered = await vscode.window.showInputBox({ + title: 'Trino Query ID', + prompt: 'Enter the Trino query ID (e.g. 20250101_120000_00001_abcde)', + validateInput: (v) => + v?.trim() ? undefined : 'Query ID cannot be empty', + }); + return entered?.trim() || undefined; + } + + private async setTrinoCredentialsQuickPick(): Promise { + const profiles = listProfiles(); + if (profiles.length === 0) { + const action = await vscode.window.showInformationMessage( + 'No Trino profiles configured yet. Add one in Settings (`dj.trino.profiles`) or open Query Control Center → "Edit connections…".', + 'Open Settings', + ); + if (action === 'Open Settings') { + await vscode.commands.executeCommand( + 'workbench.action.openSettings', + 'dj.trino.profiles', + ); + } + return; + } + const profileChoice = await vscode.window.showQuickPick( + profiles.map((p) => ({ + label: p.name, + description: `${p.coordinatorUrl} (authSource: ${p.authSource})`, + profile: p, + })), + { title: 'Select a Trino profile' }, + ); + if (!profileChoice) { + return; + } + + const profile: TrinoProfile = profileChoice.profile; + if (profile.authSource !== 'secret-storage') { + vscode.window.showInformationMessage( + `Profile "${profile.name}" uses authSource "${profile.authSource}" — nothing to store in SecretStorage. ` + + (profile.authSource === 'env-var' + ? `The secret is read at request time from env var "${profile.secretEnvVar}".` + : profile.authSource === 'password-file' + ? `The secret is read at request time from file "${profile.passwordFilePath}".` + : `The secret is read from ~/.dbt/profiles.yml (${profile.dbtProfile}.${profile.dbtTarget ?? 'default'}).`), + ); + return; + } + const kind = profile.authMethod === 'bearer' ? 'bearerToken' : 'password'; + const secret = await vscode.window.showInputBox({ + title: `Enter ${kind} for profile "${profile.name}"`, + password: true, + placeHolder: + 'Stored securely in the OS keychain via VS Code SecretStorage.', + ignoreFocusOut: true, + }); + if (!secret) { + return; + } + await storeSecret(this.coder.context, profile.name, kind, secret); + this.invalidateRestClient(); + vscode.window.showInformationMessage( + `Stored ${kind} for profile "${profile.name}".`, + ); + } + + private async selectTrinoProfileQuickPick(): Promise { + const profiles = listProfiles(); + if (profiles.length === 0) { + vscode.window.showInformationMessage( + 'No Trino profiles configured. Add one in Settings (`dj.trino.profiles`).', + ); + return; + } + const active = getActiveProfile()?.name; + const pick = await vscode.window.showQuickPick( + profiles.map((p) => ({ + label: p.name === active ? `$(check) ${p.name}` : ` ${p.name}`, + description: p.coordinatorUrl, + name: p.name, + })), + { title: 'Select active Trino connection profile' }, + ); + if (!pick) { + return; + } + await setActiveProfile(pick.name); + this.invalidateRestClient(); + vscode.window.showInformationMessage( + `Active Trino profile set to "${pick.name}".`, + ); + } + deactivate() { if (this.timeoutSystemInfo) { clearTimeout(this.timeoutSystemInfo); } + this.queryControlCenterPanel?.dispose(); } } diff --git a/src/services/trino/__tests__/findModelForSql.test.ts b/src/services/trino/__tests__/findModelForSql.test.ts new file mode 100644 index 0000000..56323e2 --- /dev/null +++ b/src/services/trino/__tests__/findModelForSql.test.ts @@ -0,0 +1,196 @@ +import { describe, expect, it } from '@jest/globals'; +import { findModelForSql } from '@services/trino/findModelForSql'; +import type { DbtModel } from '@shared/dbt/types'; + +function makeModel(name: string, pathSystemFile: string): DbtModel { + return { + name, + description: '', + childMap: [], + parentMap: [], + pathRelativeDirectory: '', + pathSystemDirectory: '', + pathSystemFile, + } as DbtModel; +} + +describe('findModelForSql', () => { + it('returns null for empty SQL', () => { + expect(findModelForSql('', new Map())).toBeNull(); + }); + + describe('dbt query_comment (`comment`) match', () => { + it('matches the node_id emitted by dbt-trino', () => { + // Real-world shape of the query_comment dbt-trino prepends. + const sql = `/* {"app": "dbt", "dbt_version": "1.7.0", "profile_name": "jaffle_shop", "target_name": "dev", "node_id": "model.my_project.int__finance__billing__daily"} */ +create or replace view "hive"."analytics"."int__finance__billing__daily" as +select * from foo`; + const models = new Map([ + [ + 'model.my_project.int__finance__billing__daily', + makeModel( + 'int__finance__billing__daily', + '/ws/my_project/models/int/finance/billing/int__finance__billing__daily.sql', + ), + ], + ]); + const m = findModelForSql(sql, models); + expect(m).not.toBeNull(); + expect(m?.project).toBe('my_project'); + expect(m?.modelName).toBe('int__finance__billing__daily'); + expect(m?.matchedBy).toBe('comment'); + }); + + it('tolerates single-quoted JSON keys', () => { + const sql = `/* {'app':'dbt','node_id':'model.proj.foo'} */ +select 1`; + const m = findModelForSql(sql, new Map()); + expect(m?.project).toBe('proj'); + expect(m?.modelName).toBe('foo'); + expect(m?.matchedBy).toBe('comment'); + }); + + it('returns the match even when the model is not in the manifest', () => { + // Best-effort: surface the project/model name from the comment so + // the UI can still display it, just without a clickable path. + const sql = `/* {"node_id": "model.ghost_project.ghost_model"} */ +select 1`; + const m = findModelForSql(sql, new Map()); + expect(m).not.toBeNull(); + expect(m?.project).toBe('ghost_project'); + expect(m?.modelName).toBe('ghost_model'); + expect(m?.modelJsonPath).toBeUndefined(); + expect(m?.matchedBy).toBe('comment'); + }); + + it('prefers the comment over the FQN when both are present', () => { + const sql = `/* {"node_id": "model.proj.alpha"} */ +create table "hive"."schema"."beta" as select 1`; + const models = new Map([ + ['model.proj.alpha', makeModel('alpha', '/ws/proj/models/alpha.sql')], + ['model.proj.beta', makeModel('beta', '/ws/proj/models/beta.sql')], + ]); + const m = findModelForSql(sql, models); + expect(m?.modelName).toBe('alpha'); + expect(m?.matchedBy).toBe('comment'); + }); + }); + + describe('FQN (`fqn`) match', () => { + it('matches `create table catalog.schema.name`', () => { + const sql = `create table "hive"."analytics"."mart__sales" as +select * from foo`; + const models = new Map([ + [ + 'model.analytics_project.mart__sales', + makeModel( + 'mart__sales', + '/ws/analytics_project/models/mart/mart__sales.sql', + ), + ], + ]); + const m = findModelForSql(sql, models); + expect(m?.modelName).toBe('mart__sales'); + expect(m?.project).toBe('analytics_project'); + expect(m?.matchedBy).toBe('fqn'); + }); + + it('matches `create or replace view catalog.schema.name`', () => { + const sql = `create or replace view hive.analytics.mart__customers as +select * from foo`; + const models = new Map([ + [ + 'model.proj.mart__customers', + makeModel('mart__customers', '/ws/proj/models/mart__customers.sql'), + ], + ]); + const m = findModelForSql(sql, models); + expect(m?.modelName).toBe('mart__customers'); + expect(m?.matchedBy).toBe('fqn'); + }); + + it('matches `insert into catalog.schema.name` (incremental)', () => { + const sql = `insert into "hive"."analytics"."int__orders" select * from foo`; + const models = new Map([ + [ + 'model.orders_project.int__orders', + makeModel( + 'int__orders', + '/ws/orders_project/models/int/int__orders.sql', + ), + ], + ]); + const m = findModelForSql(sql, models); + expect(m?.modelName).toBe('int__orders'); + expect(m?.project).toBe('orders_project'); + expect(m?.matchedBy).toBe('fqn'); + }); + + it('returns null when the FQN is present but not in the manifest', () => { + const sql = `create table "hive"."schema"."unknown_table" as select 1`; + const models = new Map([ + ['model.proj.other', makeModel('other', '/ws/proj/models/other.sql')], + ]); + expect(findModelForSql(sql, models)).toBeNull(); + }); + }); + + describe('trailing CTE (`cte`) fallback', () => { + it('matches the trailing `SELECT * FROM ` in compiled SQL', () => { + // Matches the shape of dbt-trino's `target/compiled/.../foo.sql` + // when it isn't wrapped in a DDL. + const sql = ` +WITH __dbt__cte__stg__foo AS (select * from bar), +int__finance__billing__daily AS ( + select * from __dbt__cte__stg__foo +) +SELECT + * +FROM + int__finance__billing__daily +`; + const models = new Map([ + [ + 'model.my_project.int__finance__billing__daily', + makeModel( + 'int__finance__billing__daily', + '/ws/my_project/models/int/int__finance__billing__daily.sql', + ), + ], + ]); + const m = findModelForSql(sql, models); + expect(m?.modelName).toBe('int__finance__billing__daily'); + expect(m?.project).toBe('my_project'); + expect(m?.matchedBy).toBe('cte'); + }); + + it('only inspects the tail (ignores intermediate `FROM `)', () => { + // `__dbt__cte__stg__foo` appears as an intermediate FROM but the + // matcher should land on the final `FROM int__top`. + const sql = `WITH int__top AS ( + select * from __dbt__cte__stg__foo +) +SELECT * FROM int__top`; + const models = new Map([ + [ + 'model.proj.int__top', + makeModel('int__top', '/ws/proj/models/int__top.sql'), + ], + // `__dbt__cte__stg__foo` happens to also be a model name — but + // because we only look at the tail, the matcher must still pick + // `int__top`. + [ + 'model.proj.__dbt__cte__stg__foo', + makeModel('__dbt__cte__stg__foo', '/ws/proj/models/x.sql'), + ], + ]); + const m = findModelForSql(sql, models); + expect(m?.modelName).toBe('int__top'); + expect(m?.matchedBy).toBe('cte'); + }); + }); + + it('returns null when no strategy matches', () => { + expect(findModelForSql('select 1', new Map())).toBeNull(); + }); +}); diff --git a/src/services/trino/__tests__/profiles.test.ts b/src/services/trino/__tests__/profiles.test.ts new file mode 100644 index 0000000..317008e --- /dev/null +++ b/src/services/trino/__tests__/profiles.test.ts @@ -0,0 +1,197 @@ +import { afterEach, describe, expect, it, jest } from '@jest/globals'; + +// Mock vscode before importing anything that pulls it in transitively. +// The profiles module only relies on `vscode.workspace.getConfiguration` for +// the (settings-bound) profile list + active-profile pointer, and +// `vscode.ConfigurationTarget.Workspace` as an enum constant. Stubbing those +// is enough for the pure-logic tests below. +jest.mock( + 'vscode', + () => ({ + workspace: { + getConfiguration: () => ({ + get: (_key: string, fallback: T) => fallback, + update: jest.fn(), + }), + }, + ConfigurationTarget: { Workspace: 2 }, + }), + { virtual: true }, +); + +import { + resolveProfileSecret, + TrinoProfileError, + validateProfile, +} from '@services/trino/profiles'; +import type { TrinoProfile } from '@shared/trino/types'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +function fakeContext(secret?: string): { + secrets: { get: jest.Mock; store: jest.Mock; delete: jest.Mock }; +} { + return { + secrets: { + get: jest.fn(async () => secret), + store: jest.fn(async () => undefined), + delete: jest.fn(async () => undefined), + }, + }; +} + +describe('validateProfile', () => { + it('throws when the name is missing', () => { + const p: TrinoProfile = { + name: '', + coordinatorUrl: 'https://x', + user: 'u', + authMethod: 'basic', + authSource: 'secret-storage', + }; + expect(() => validateProfile(p)).toThrow(TrinoProfileError); + }); + + it('throws when the coordinator URL is missing', () => { + const p: TrinoProfile = { + name: 'dev', + coordinatorUrl: '', + user: 'u', + authMethod: 'basic', + authSource: 'secret-storage', + }; + expect(() => validateProfile(p)).toThrow(TrinoProfileError); + }); + + it('throws when the auth method is unknown', () => { + const p = { + name: 'dev', + coordinatorUrl: 'https://x', + user: 'u', + authMethod: 'oauth-magic' as unknown as TrinoProfile['authMethod'], + authSource: 'secret-storage' as const, + } satisfies TrinoProfile; + expect(() => validateProfile(p)).toThrow(TrinoProfileError); + }); + + it('accepts a well-formed profile', () => { + const p: TrinoProfile = { + name: 'dev', + coordinatorUrl: 'https://x', + user: 'u', + authMethod: 'basic', + authSource: 'secret-storage', + }; + expect(() => validateProfile(p)).not.toThrow(); + }); +}); + +describe('resolveProfileSecret', () => { + const originalEnv = { ...process.env }; + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it('returns null for authMethod: "none"', async () => { + const profile: TrinoProfile = { + name: 'dev', + coordinatorUrl: 'https://x', + user: 'u', + authMethod: 'none', + authSource: 'secret-storage', + }; + const ctx = fakeContext('ignored'); + const out = await resolveProfileSecret(ctx as never, profile); + expect(out).toBeNull(); + expect(ctx.secrets.get).not.toHaveBeenCalled(); + }); + + it('reads the secret from the OS keychain for secret-storage profiles', async () => { + const profile: TrinoProfile = { + name: 'dev', + coordinatorUrl: 'https://x', + user: 'u', + authMethod: 'basic', + authSource: 'secret-storage', + }; + const ctx = fakeContext('top-secret'); + const out = await resolveProfileSecret(ctx as never, profile); + expect(out).toBe('top-secret'); + }); + + it('throws NO_SECRET when secret-storage profile has no stored value', async () => { + const profile: TrinoProfile = { + name: 'dev', + coordinatorUrl: 'https://x', + user: 'u', + authMethod: 'basic', + authSource: 'secret-storage', + }; + const ctx = fakeContext(undefined); + await expect(resolveProfileSecret(ctx as never, profile)).rejects.toThrow( + TrinoProfileError, + ); + }); + + it('reads from process.env for env-var profiles', async () => { + process.env.MY_TRINO_PROD_BEARER = 'abc-token'; + const profile: TrinoProfile = { + name: 'prod', + coordinatorUrl: 'https://x', + user: 'u', + authMethod: 'bearer', + authSource: 'env-var', + secretEnvVar: 'MY_TRINO_PROD_BEARER', + }; + const out = await resolveProfileSecret(fakeContext() as never, profile); + expect(out).toBe('abc-token'); + }); + + it('throws AUTH_SOURCE_MISCONFIGURED when env-var profile is missing secretEnvVar', async () => { + const profile: TrinoProfile = { + name: 'prod', + coordinatorUrl: 'https://x', + user: 'u', + authMethod: 'bearer', + authSource: 'env-var', + }; + await expect( + resolveProfileSecret(fakeContext() as never, profile), + ).rejects.toThrow(TrinoProfileError); + }); + + it('reads password-file content (trimmed of trailing newlines)', async () => { + const tmp = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'dj-pw-')); + const file = path.join(tmp, 'p.txt'); + try { + await fs.promises.writeFile(file, 'file-secret\n', 'utf8'); + const profile: TrinoProfile = { + name: 'staging', + coordinatorUrl: 'https://x', + user: 'u', + authMethod: 'basic', + authSource: 'password-file', + passwordFilePath: file, + }; + const out = await resolveProfileSecret(fakeContext() as never, profile); + expect(out).toBe('file-secret'); + } finally { + await fs.promises.rm(tmp, { recursive: true, force: true }); + } + }); + + it('throws NO_SECRET when password-file does not exist', async () => { + const profile: TrinoProfile = { + name: 'staging', + coordinatorUrl: 'https://x', + user: 'u', + authMethod: 'basic', + authSource: 'password-file', + passwordFilePath: '/definitely/does/not/exist/dj-test.pw', + }; + await expect( + resolveProfileSecret(fakeContext() as never, profile), + ).rejects.toThrow(TrinoProfileError); + }); +}); diff --git a/src/services/trino/__tests__/queryJsonSanitizer.test.ts b/src/services/trino/__tests__/queryJsonSanitizer.test.ts new file mode 100644 index 0000000..04ba450 --- /dev/null +++ b/src/services/trino/__tests__/queryJsonSanitizer.test.ts @@ -0,0 +1,560 @@ +import { describe, expect, it } from '@jest/globals'; +import { + QueryInfoFirewallError, + readPersistedSanitizedQuery, + sanitize, + sanitizeAndPersist, + summarizeQueryInfo, + toolFirewall, +} from '@services/trino/queryJsonSanitizer'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +function makeRawQueryInfo(overrides: Record = {}) { + return { + queryId: '20250511_120000_00000_test', + state: 'FINISHED', + session: { + user: 'tester', + source: 'dbt-trino-1.7.17', + catalog: 'hive', + schema: 'analytics', + }, + queryStats: { + createTime: '2025-05-11T12:00:00Z', + executionStartTime: '2025-05-11T12:00:01Z', + endTime: '2025-05-11T12:00:55Z', + totalCpuTime: '40.20s', + elapsedTime: '54.00s', + queuedTime: '0.50s', + analysisTime: '0.20s', + totalPlanningTime: '0.30s', + totalBlockedTime: '4.00s', + peakUserMemoryReservation: '800MB', + peakTotalMemoryReservation: '900MB', + processedInputPositions: 1_200_000, + processedInputDataSize: '120MB', + totalSplits: 180, + completedSplits: 180, + queuedSplits: 0, + runningSplits: 0, + stageGcStatistics: { large: 'blob' }, + operatorSummaries: [{ operatorType: 'LookupJoinOperator' }], + }, + query: + '/* {"app": "dbt", "node_id": "model.my_project.int__finance__daily"} */\nselect 1', + outputStage: { + stageId: 'stage_0', + state: 'FINISHED', + stageStats: { + totalCpuTime: '20s', + operatorSummaries: [ + { + operatorType: 'LookupJoinOperator', + pipelineId: 1, + planNodeId: 'pl1', + inputPositions: 1_200_000, + outputPositions: 1_180_000, + totalCpuTime: '18s', + peakUserMemoryReservation: '600MB', + inputDataSizeDistribution: { max: '50MB', avg: '5MB' }, + }, + { + operatorType: 'TableScanOperator', + pipelineId: 0, + planNodeId: 'pl0', + inputPositions: 1_200_000, + outputPositions: 1_200_000, + totalCpuTime: '8s', + peakUserMemoryReservation: '120MB', + info: { connectorName: 'hive', catalog: 'hive' }, + }, + ], + }, + tasks: [ + { + taskId: 't1', + stats: { foo: 'bar', pipelines: ['drop-me'] }, + outputBuffers: { drop: 'me' }, + }, + ], + subStages: [], + }, + ...overrides, + }; +} + +describe('toolFirewall', () => { + it('rejects payloads with top-level result key', () => { + expect(() => toolFirewall({ result: { data: [[1]] } })).toThrow( + QueryInfoFirewallError, + ); + }); + + it('rejects payloads with top-level data key', () => { + expect(() => toolFirewall({ data: [[1]] })).toThrow(QueryInfoFirewallError); + }); + + it('rejects queryStats.rows row data', () => { + expect(() => toolFirewall({ queryStats: { rows: [[1, 2]] } })).toThrow( + QueryInfoFirewallError, + ); + }); + + it('accepts well-formed QueryInfo payloads', () => { + expect(() => toolFirewall(makeRawQueryInfo())).not.toThrow(); + }); +}); + +describe('summarizeQueryInfo', () => { + it('extracts headline stats from queryStats and session', () => { + const s = summarizeQueryInfo(makeRawQueryInfo()); + expect(s.queryId).toBe('20250511_120000_00000_test'); + expect(s.state).toBe('FINISHED'); + expect(s.user).toBe('tester'); + expect(s.source).toBe('dbt-trino-1.7.17'); + expect(s.catalog).toBe('hive'); + expect(s.schema).toBe('analytics'); + expect(s.wallTimeMs).toBeGreaterThan(0); + expect(s.cpuTimeMs).toBeGreaterThan(0); + expect(s.peakUserMemoryBytes).toBeGreaterThan(0); + expect(s.totalSplits).toBe(180); + }); + + it('reports the largest operator and its peak memory', () => { + const s = summarizeQueryInfo(makeRawQueryInfo()); + expect(s.largestOperator).toBe('LookupJoinOperator'); + }); + + it('computes data-skew score from inputDataSizeDistribution', () => { + const s = summarizeQueryInfo(makeRawQueryInfo()); + // 50MB max / 5MB avg = 10 + expect(s.dataSkewScore).toBeCloseTo(10, 1); + }); + + it('parses planning time from the legacy totalPlanningTime key', () => { + const s = summarizeQueryInfo(makeRawQueryInfo()); + expect(s.planningTimeMs).toBe(300); + }); + + it('surfaces connector types from TableScanOperator info', () => { + const s = summarizeQueryInfo(makeRawQueryInfo()); + expect(s.connectorTypes).toContain('hive'); + }); + + it('surfaces error code and failure message on failed queries', () => { + const raw = makeRawQueryInfo({ + state: 'FAILED', + errorCode: { + name: 'EXCEEDED_LOCAL_MEMORY_LIMIT', + type: 'INSUFFICIENT_RESOURCES', + }, + failureInfo: { message: 'Per-node memory limit exceeded' }, + }); + const s = summarizeQueryInfo(raw); + expect(s.state).toBe('FAILED'); + expect(s.errorCode).toBe('EXCEEDED_LOCAL_MEMORY_LIMIT'); + expect(s.errorType).toBe('INSUFFICIENT_RESOURCES'); + expect(s.failureMessage).toBe('Per-node memory limit exceeded'); + }); +}); + +/** + * Build a fixture matching the newer Trino stage shape: no + * `outputStage` / `rootStage`, instead a `stages` envelope with the + * root identified by `outputStageId` and children in a `stages` array. + * Also exercises the `totalDrivers` / `completedDrivers` counters and a + * flat `queryStats.operatorSummaries` list (instead of per-stage). + */ +function makeNewShapeQueryInfo(overrides: Record = {}) { + return { + queryId: '20260515_182811_09678_vd3zk', + state: 'FINISHED', + session: { user: 'tester', source: 'trino-js-client' }, + queryStats: { + createTime: '2026-05-15T18:28:11Z', + executionStartTime: '2026-05-15T18:28:11Z', + endTime: '2026-05-15T18:33:11Z', + totalCpuTime: '1.04h', + elapsedTime: '5.01m', + queuedTime: '0.50s', + totalBlockedTime: '4.28d', + peakUserMemoryReservation: '19755869049B', + processedInputPositions: 1_683_119_817, + processedInputDataSize: '1920805930523B', + totalDrivers: 8441, + completedDrivers: 8441, + queuedDrivers: 0, + runningDrivers: 0, + operatorSummaries: [ + { + operatorType: 'ExchangeOperator', + peakUserMemoryReservation: '72B', + }, + { + operatorType: 'LookupJoinOperator', + peakUserMemoryReservation: '600MB', + }, + ], + }, + query: 'select 1', + stages: { + outputStageId: 'stage_0', + stages: [ + { + stageId: 'stage_0', + state: 'FINISHED', + stageStats: { totalCpuTime: '20s' }, + subStages: [ + { + stageId: 'stage_1', + state: 'FINISHED', + stageStats: { totalCpuTime: '10s' }, + }, + ], + }, + { + stageId: 'stage_1', + state: 'FINISHED', + stageStats: { totalCpuTime: '10s' }, + }, + ], + }, + ...overrides, + }; +} + +describe('summarizeQueryInfo (newer Trino shape)', () => { + it('falls back to totalDrivers / completedDrivers when totalSplits is absent', () => { + const s = summarizeQueryInfo(makeNewShapeQueryInfo()); + expect(s.totalSplits).toBe(8441); + expect(s.completedSplits).toBe(8441); + expect(s.queuedSplits).toBe(0); + expect(s.runningSplits).toBe(0); + }); + + it('reports largestOperator from the flat queryStats.operatorSummaries when per-stage list is empty', () => { + const s = summarizeQueryInfo(makeNewShapeQueryInfo()); + expect(s.largestOperator).toBe('LookupJoinOperator'); + }); + + it('parses planning time from the current planningTime key', () => { + const raw = makeNewShapeQueryInfo(); + (raw.queryStats as Record).planningTime = '0.30s'; + const s = summarizeQueryInfo(raw); + expect(s.planningTimeMs).toBe(300); + }); + + it('falls back to per-task input skew when operator distributions are absent', () => { + const raw = makeNewShapeQueryInfo({ + stages: { + outputStageId: 'stage_0', + stages: [ + { + stageId: 'stage_0', + state: 'FINISHED', + stageStats: { totalCpuTime: '20s' }, + subStages: ['stage_1'], + }, + { + stageId: 'stage_1', + state: 'FINISHED', + stageStats: { totalCpuTime: '10s' }, + // One hot task vs three light siblings: + // avg = (100 + 10 + 10 + 10) / 4 = 32.5MB → 100 / 32.5 ≈ 3.08 + tasks: [ + { stats: { processedInputDataSize: '100MB' } }, + { stats: { processedInputDataSize: '10MB' } }, + { stats: { processedInputDataSize: '10MB' } }, + { stats: { processedInputDataSize: '10MB' } }, + ], + subStages: [], + }, + ], + }, + }); + const s = summarizeQueryInfo(raw); + expect(s.dataSkewScore).toBeCloseTo(3.08, 2); + }); + + it('surfaces connector types from splitOperatorInfo catalogName', () => { + const raw = makeNewShapeQueryInfo(); + (raw.queryStats as Record).operatorSummaries = [ + { + operatorType: 'TableScanOperator', + info: { catalogName: 'iceberg', splitInfo: {} }, + }, + ]; + const s = summarizeQueryInfo(raw); + expect(s.connectorTypes).toContain('iceberg'); + }); +}); + +describe('sanitize (newer Trino shape)', () => { + it('picks the root stage from the new stages envelope', () => { + const s = sanitize(makeNewShapeQueryInfo()); + expect(s.rootStage).toBeDefined(); + expect(s.rootStage?.stageId).toBe('stage_0'); + expect(s.rootStage?.subStages?.length).toBe(1); + expect(s.rootStage?.subStages?.[0].stageId).toBe('stage_1'); + }); + + it('falls back to stages[0] when outputStageId is missing', () => { + const raw = makeNewShapeQueryInfo({ + stages: { + stages: [ + { stageId: 'stage_alpha', state: 'FINISHED' }, + { stageId: 'stage_beta', state: 'FINISHED' }, + ], + }, + }); + const s = sanitize(raw); + expect(s.rootStage?.stageId).toBe('stage_alpha'); + }); + + it('emits operatorSummary from queryStats.operatorSummaries when per-stage list is empty', () => { + const s = sanitize(makeNewShapeQueryInfo()); + expect(s.operatorSummary?.length).toBe(2); + expect(s.operatorSummary?.[0].operatorType).toBe('ExchangeOperator'); + }); + + it('sums addInputCpu + getOutputCpu + finishCpu when totalCpuTime is absent', () => { + // Newer Trino operator summaries split CPU across the three + // pipeline phases — there's no single `totalCpuTime` to read. + const raw = makeNewShapeQueryInfo({ + queryStats: { + ...(makeNewShapeQueryInfo().queryStats as Record), + operatorSummaries: [ + { + operatorType: 'LookupJoinOperator', + addInputCpu: '2.00s', + getOutputCpu: '500ms', + finishCpu: '500ms', + blockedWall: '1.00s', + peakUserMemoryReservation: '600MB', + }, + { + operatorType: 'TableScanOperator', + // Missing per-phase fields entirely — should yield + // undefined (placeholder rendered). + peakUserMemoryReservation: '120MB', + }, + ], + }, + }); + const s = sanitize(raw); + expect(s.operatorSummary?.[0].cpuNanos).toBe(3_000_000_000); + expect(s.operatorSummary?.[0].blockedWallNanos).toBe(1_000_000_000); + expect(s.operatorSummary?.[1].cpuNanos).toBeUndefined(); + }); + + it('resolves string-id subStages references against the flat stages array', () => { + // Most newer Trino builds emit `subStages` as an array of stage + // IDs (strings) and store every stage flat under + // `raw.stages.stages`. The tree has to be reconstructed by + // resolving each id against the flat list. + const raw = makeNewShapeQueryInfo({ + stages: { + outputStageId: 'stage_0', + stages: [ + { + stageId: 'stage_0', + state: 'FINISHED', + stageStats: { + totalCpuTime: '20s', + operatorSummaries: [{ operatorType: 'OutputOperator' }], + }, + subStages: ['stage_1', 'stage_2'], + }, + { + stageId: 'stage_1', + state: 'FINISHED', + stageStats: { + totalCpuTime: '10s', + operatorSummaries: [{ operatorType: 'ScanFilterOperator' }], + }, + subStages: ['stage_3'], + }, + { + stageId: 'stage_2', + state: 'FINISHED', + stageStats: { totalCpuTime: '5s' }, + subStages: [], + }, + { + stageId: 'stage_3', + state: 'FINISHED', + stageStats: { totalCpuTime: '2s' }, + subStages: [], + }, + ], + }, + }); + const s = sanitize(raw); + expect(s.rootStage?.stageId).toBe('stage_0'); + expect(s.rootStage?.subStages?.map((c) => c.stageId)).toEqual([ + 'stage_1', + 'stage_2', + ]); + expect(s.rootStage?.subStages?.[0].subStages?.[0].stageId).toBe('stage_3'); + // collectOperators should now find the per-stage operators along + // the resolved tree; the largestOperator metric is computed from + // them in summarizeQueryInfo. + expect( + s.operatorSummary?.some((o) => o.operatorType === 'ScanFilterOperator'), + ).toBe(true); + }); +}); + +describe('sanitize', () => { + it('drops stageGcStatistics and per-driver task detail', () => { + const s = sanitize(makeRawQueryInfo()); + expect(s.queryStats.stageGcStatistics).toBeUndefined(); + expect(s.queryStats.operatorSummaries).toBeUndefined(); + // tasks[].outputBuffers and tasks[].stats.pipelines stripped + const task = (s.rootStage?.tasks ?? [])[0]; + expect(task?.outputBuffers).toBeUndefined(); + const stats = task?.stats as Record | undefined; + expect(stats?.pipelines).toBeUndefined(); + expect(stats?.foo).toBe('bar'); + }); + + it('reproduces the operator summary at top level with trimmed fields', () => { + const s = sanitize(makeRawQueryInfo()); + expect(s.operatorSummary).toBeDefined(); + expect(s.operatorSummary!.length).toBe(2); + const first = s.operatorSummary![0]; + expect(first.operatorType).toBe('LookupJoinOperator'); + expect(first.cpuNanos).toBeGreaterThan(0); + }); + + it('keeps the query_comment verbatim so findModelForSql can read the node_id', () => { + const s = sanitize(makeRawQueryInfo()); + expect(s.query).toContain( + '"node_id": "model.my_project.int__finance__daily"', + ); + }); +}); + +describe('sanitizeAndPersist', () => { + it('writes .json and .full.json to the workspace .dj/diagnostics dir', async () => { + const tmp = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'dj-trino-test-'), + ); + try { + const raw = makeRawQueryInfo(); + const res = await sanitizeAndPersist(raw, { workspaceRoot: tmp }); + expect(res.queryId).toBe('20250511_120000_00000_test'); + expect(fs.existsSync(res.jsonPath)).toBe(true); + expect(fs.existsSync(res.fullJsonPath)).toBe(true); + const sanitizedOnDisk = JSON.parse( + await fs.promises.readFile(res.jsonPath, 'utf8'), + ); + expect(sanitizedOnDisk.summary.queryId).toBe( + '20250511_120000_00000_test', + ); + } finally { + await fs.promises.rm(tmp, { recursive: true, force: true }); + } + }); + + it('reuses cached sanitized output when called twice in quick succession', async () => { + const tmp = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'dj-trino-test-'), + ); + try { + const raw = makeRawQueryInfo(); + const first = await sanitizeAndPersist(raw, { workspaceRoot: tmp }); + const firstMtime = (await fs.promises.stat(first.jsonPath)).mtimeMs; + await sanitizeAndPersist(raw, { workspaceRoot: tmp }); + const secondMtime = (await fs.promises.stat(first.jsonPath)).mtimeMs; + expect(secondMtime).toBe(firstMtime); + } finally { + await fs.promises.rm(tmp, { recursive: true, force: true }); + } + }); + + it('refuses to sanitize a payload that smells like row data', async () => { + const tmp = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'dj-trino-test-'), + ); + try { + const raw = makeRawQueryInfo({ result: { data: [[1, 2, 3]] } }); + await expect( + sanitizeAndPersist(raw, { workspaceRoot: tmp }), + ).rejects.toThrow(QueryInfoFirewallError); + } finally { + await fs.promises.rm(tmp, { recursive: true, force: true }); + } + }); +}); + +describe('readPersistedSanitizedQuery', () => { + it('returns null when there is no diagnostics dir', async () => { + const tmp = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'dj-trino-test-'), + ); + try { + const out = await readPersistedSanitizedQuery( + '20250511_120000_00000_test', + tmp, + ); + expect(out).toBeNull(); + } finally { + await fs.promises.rm(tmp, { recursive: true, force: true }); + } + }); + + it('round-trips a previously-persisted sanitized payload', async () => { + const tmp = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'dj-trino-test-'), + ); + try { + const raw = makeRawQueryInfo(); + await sanitizeAndPersist(raw, { workspaceRoot: tmp }); + const out = await readPersistedSanitizedQuery( + '20250511_120000_00000_test', + tmp, + ); + expect(out).not.toBeNull(); + expect(out!.summary.queryId).toBe('20250511_120000_00000_test'); + expect(out!.summary.state).toBe('FINISHED'); + expect(out!.query).toContain('node_id'); + expect(out!.operatorSummary?.length).toBeGreaterThan(0); + } finally { + await fs.promises.rm(tmp, { recursive: true, force: true }); + } + }); + + it('returns null for an empty queryId', async () => { + const tmp = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'dj-trino-test-'), + ); + try { + const out = await readPersistedSanitizedQuery('', tmp); + expect(out).toBeNull(); + } finally { + await fs.promises.rm(tmp, { recursive: true, force: true }); + } + }); + + it('returns null when the persisted file is corrupt JSON', async () => { + const tmp = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'dj-trino-test-'), + ); + try { + const dir = path.join(tmp, '.dj', 'diagnostics'); + await fs.promises.mkdir(dir, { recursive: true }); + await fs.promises.writeFile( + path.join(dir, 'bad_query.json'), + '{not json', + 'utf8', + ); + const out = await readPersistedSanitizedQuery('bad_query', tmp); + expect(out).toBeNull(); + } finally { + await fs.promises.rm(tmp, { recursive: true, force: true }); + } + }); +}); diff --git a/src/services/trino/__tests__/restClient.test.ts b/src/services/trino/__tests__/restClient.test.ts new file mode 100644 index 0000000..58df03a --- /dev/null +++ b/src/services/trino/__tests__/restClient.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it } from '@jest/globals'; +import { + parseDataSize, + parseDurationMs, + shapeQueryInfo, + shapeQuerySummary, + TrinoCoordinatorError, +} from '@services/trino/restClient'; + +describe('parseDurationMs', () => { + it('handles numeric inputs as raw milliseconds', () => { + expect(parseDurationMs(1234)).toBe(1234); + }); + + it('handles standard Trino duration strings', () => { + expect(parseDurationMs('12.5s')).toBe(12_500); + expect(parseDurationMs('500ms')).toBe(500); + expect(parseDurationMs('1m')).toBe(60_000); + expect(parseDurationMs('1h')).toBe(3_600_000); + }); + + it('returns undefined for nonsense', () => { + expect(parseDurationMs(null)).toBeUndefined(); + expect(parseDurationMs(undefined)).toBeUndefined(); + expect(parseDurationMs('not-a-duration')).toBeUndefined(); + }); +}); + +describe('parseDataSize', () => { + it('handles numeric inputs as raw bytes', () => { + expect(parseDataSize(2048)).toBe(2048); + }); + + it('handles standard Trino data-size strings', () => { + expect(parseDataSize('512B')).toBe(512); + expect(parseDataSize('1KB')).toBe(1024); + expect(parseDataSize('2MB')).toBe(2 * 1024 ** 2); + expect(parseDataSize('1.5GB')).toBe(Math.round(1.5 * 1024 ** 3)); + }); + + it('returns undefined for nonsense', () => { + expect(parseDataSize(null)).toBeUndefined(); + expect(parseDataSize('weird')).toBeUndefined(); + }); +}); + +describe('shapeQuerySummary', () => { + it('extracts the queryId from the self URL when not given inline', () => { + const s = shapeQuerySummary({ + self: 'https://trino.example.com/v1/query/20250511_120000_00000_abc', + state: 'RUNNING', + }); + expect(s.queryId).toBe('20250511_120000_00000_abc'); + expect(s.state).toBe('RUNNING'); + }); + + it('prefers explicit queryId over self', () => { + const s = shapeQuerySummary({ + queryId: 'explicit_id', + self: 'https://trino.example.com/v1/query/from_self', + state: 'FINISHED', + }); + expect(s.queryId).toBe('explicit_id'); + }); + + it('surfaces error code + failure message on failed queries', () => { + const s = shapeQuerySummary({ + queryId: 'q1', + state: 'FAILED', + errorCode: { name: 'EXCEEDED_TIME_LIMIT', type: 'USER_ERROR' }, + failureInfo: { message: 'Query exceeded time limit' }, + }); + expect(s.errorCode).toBe('EXCEEDED_TIME_LIMIT'); + expect(s.errorType).toBe('USER_ERROR'); + expect(s.failureMessage).toBe('Query exceeded time limit'); + }); +}); + +describe('shapeQueryInfo', () => { + it('preserves the SQL text so findModelForSql can read the dbt query_comment', () => { + const info = shapeQueryInfo({ + queryId: 'q1', + state: 'FINISHED', + query: '/* {"node_id": "model.foo.bar"} */\nselect 1', + queryStats: {}, + outputStage: { + stageId: 'stage_0', + stageStats: { + operatorSummaries: [ + { + operatorType: 'TableScanOperator', + pipelineId: 0, + totalCpuTime: '1s', + peakUserMemoryReservation: '10MB', + }, + ], + }, + }, + }); + expect(info.query).toContain('"node_id": "model.foo.bar"'); + expect(info.operatorSummary?.[0]?.operatorType).toBe('TableScanOperator'); + expect(info.operatorSummary?.[0]?.cpuNanos).toBeGreaterThan(0); + }); +}); + +describe('TrinoCoordinatorError', () => { + it('carries the code, status, and retry-after for inspection', () => { + const err = new TrinoCoordinatorError( + 'rate limited', + 'RATE_LIMITED', + 429, + 30, + ); + expect(err.code).toBe('RATE_LIMITED'); + expect(err.status).toBe(429); + expect(err.retryAfterSec).toBe(30); + expect(err.message).toBe('rate limited'); + }); +}); diff --git a/src/services/trino/findModelForSql.ts b/src/services/trino/findModelForSql.ts new file mode 100644 index 0000000..c1c87a7 --- /dev/null +++ b/src/services/trino/findModelForSql.ts @@ -0,0 +1,176 @@ +/** + * Map a Trino query's SQL text back to the DJ model that produced it. + * + * All three strategies operate purely on the SQL text returned by the + * Trino coordinator — DJ never modifies generated SQL. + * + * 1. **`dbt` query_comment** — `dbt-trino` (and core dbt) prepend a JSON + * comment to every query they issue. Its `node_id` field looks like + * `model..` and gives us an exact match. This is + * enabled by default in dbt; users only opt out by overriding + * `query-comment` in `dbt_project.yml`. + * + * 2. **Materialization FQN** — `dbt-trino` wraps every materialized + * model in `CREATE TABLE .. AS …`, + * `CREATE OR REPLACE VIEW …`, `CREATE MATERIALIZED VIEW …`, or + * `INSERT INTO …` (incremental). We pull the table name out of the + * wrapping clause and look it up in the manifest by name. + * + * 3. **Trailing CTE pattern** — dbt's compiled SQL form ends in + * `…), AS (…) SELECT * FROM `. Useful for + * `dbt compile` / `dbt show` outputs that aren't wrapped in a DDL. + * + * If none of the three resolves, callers get `null` and the UI shows a + * disabled "Jump to Model" button with an explanatory tooltip. + */ + +import type { DbtModel } from '@shared/dbt/types'; +import type { DjModelMatch } from '@shared/trino/types'; +import * as fs from 'fs'; +import * as path from 'path'; + +// `/* {"app": "dbt", …, "node_id": "model.project.name", …} */` +// We tolerate single-quoted JSON (which dbt sometimes emits) and any +// whitespace around the colon. Stop at the first quote that follows the +// model name so we don't accidentally span into the next key. +const QUERY_COMMENT_NODE_ID_REGEX = + /["']node_id["']\s*:\s*["']model\.([\w.-]+)\.([\w.-]+)["']/i; +const CREATE_TABLE_REGEX = + /create\s+(?:or\s+replace\s+)?(?:table|view|materialized\s+view)\s+(?:if\s+not\s+exists\s+)?"?([\w_.-]+)"?\s*\.\s*"?([\w_.-]+)"?\s*\.\s*"?([\w_.-]+)"?/i; +const INSERT_INTO_REGEX = + /insert\s+into\s+"?([\w_.-]+)"?\s*\.\s*"?([\w_.-]+)"?\s*\.\s*"?([\w_.-]+)"?/i; +// `…)… AS ( … ) SELECT * FROM ` — the dbt-trino +// compiled-CTE shape. We anchor on the final `FROM ` near the end +// of the text to keep this cheap on huge SQL bodies. +const TRAILING_FROM_REGEX = /\bfrom\s+"?([a-z_][\w]*)"?\s*;?\s*$/i; + +export function findModelForSql( + sqlText: string, + models: Map, +): DjModelMatch | null { + if (!sqlText) { + return null; + } + + // 1) dbt query_comment — preferred. Works for `dbt run`, `dbt build`, + // `dbt compile`, `dbt show`, and incremental materializations. + const comment = sqlText.match(QUERY_COMMENT_NODE_ID_REGEX); + if (comment) { + const [, project, modelName] = comment; + const modelJsonPath = resolveModelJsonPath(project, modelName, models); + return { + project, + modelName, + modelJsonPath, + matchedBy: 'comment', + }; + } + + // 2) Materialization FQN — extract from `CREATE TABLE/VIEW … AS` or + // `INSERT INTO …` and resolve by model name. + const fqn = + sqlText.match(CREATE_TABLE_REGEX) ?? sqlText.match(INSERT_INTO_REGEX); + if (fqn) { + const tableName = fqn[3]; + if (tableName) { + const match = matchByModelName(tableName, models); + if (match) { + return { ...match, matchedBy: 'fqn' }; + } + } + } + + // 3) Trailing CTE fallback — for compiled SQL bodies that aren't + // wrapped in a DDL. We scan only the tail of the SQL to avoid + // matching on a `FROM ` deep inside the body. + const tail = sqlText.slice(-256).replace(/[\s;]+$/, ''); + const trailing = tail.match(TRAILING_FROM_REGEX); + if (trailing) { + const candidate = trailing[1]; + if (candidate) { + const match = matchByModelName(candidate, models); + if (match) { + return { ...match, matchedBy: 'cte' }; + } + } + } + + return null; +} + +/** + * Resolve a model name against the manifest. Matches the first model + * whose `name` equals the candidate; if multiple projects publish a + * model with the same name we return the first one (the manifest's + * insertion order is stable enough for the common single-project case). + */ +function matchByModelName( + candidate: string, + models: Map, +): Omit | null { + for (const model of models.values()) { + if (model.name === candidate) { + const project = inferProjectFromPath(model.pathSystemFile); + if (!project) { + continue; + } + return { + project, + modelName: model.name, + modelJsonPath: deriveModelJsonPath(model.pathSystemFile), + }; + } + } + return null; +} + +/** + * Given a `.sql` file path under `models/`, infer the sibling + * `.model.json`. Returns `undefined` when the JSON file isn't on disk + * so the caller can fall back to the .sql path instead of opening a + * phantom file. + */ +function deriveModelJsonPath(sqlFile: string): string | undefined { + if (!sqlFile) { + return undefined; + } + const candidate = sqlFile.replace(/\.sql$/i, '.model.json'); + try { + if (fs.existsSync(candidate)) { + return candidate; + } + } catch { + // ignore + } + return undefined; +} + +function resolveModelJsonPath( + projectName: string, + modelName: string, + models: Map, +): string | undefined { + const id = `model.${projectName}.${modelName}`; + const model = models.get(id); + if (!model?.pathSystemFile) { + return undefined; + } + return deriveModelJsonPath(model.pathSystemFile); +} + +/** + * Best-effort project inference from a model SQL path. The DJ project + * structure is `//models/<…>/.sql`; we pick + * the directory immediately above the `models/` segment. + */ +function inferProjectFromPath(sqlFile: string): string | null { + if (!sqlFile) { + return null; + } + const parts = sqlFile.split(path.sep); + const idx = parts.lastIndexOf('models'); + if (idx <= 0) { + return null; + } + return parts[idx - 1] ?? null; +} diff --git a/src/services/trino/profiles.ts b/src/services/trino/profiles.ts new file mode 100644 index 0000000..c028e3c --- /dev/null +++ b/src/services/trino/profiles.ts @@ -0,0 +1,391 @@ +/** + * Trino connection profile management. + * + * Profiles are non-secret connection metadata stored in workspace settings + * (`dj.trino.profiles`). The active profile is picked via + * `dj.trino.activeProfile`. Secrets are NEVER stored in settings.json — they + * resolve at request time from one of four sources: + * + * 1. `secret-storage` (default) — VS Code SecretStorage (OS keychain). + * 2. `env-var` — process.env[profile.secretEnvVar] at request time. + * 3. `password-file` — read profile.passwordFilePath at request time. + * 4. `dbt-profile` — parse ~/.dbt/profiles.yml for password / jwt_token. + * + * Headless-Linux / no-libsecret hosts: SecretStorage may return null. The + * resolver throws a typed error suggesting the user switch the profile to + * one of the file-based / env-based tiers. + */ + +import type { TrinoAuthMethod, TrinoProfile } from '@shared/trino/types'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { parse as yamlParse } from 'yaml'; + +const SETTINGS_KEY = 'dj.trino'; +const SECRET_NS = 'dj.trino'; + +export class TrinoProfileError extends Error { + constructor( + message: string, + public readonly code: + | 'NO_PROFILES' + | 'NO_ACTIVE_PROFILE' + | 'UNKNOWN_PROFILE' + | 'NO_SECRET' + | 'AUTH_SOURCE_MISCONFIGURED' + | 'KEYCHAIN_UNAVAILABLE' + | 'DBT_PROFILE_NOT_FOUND', + ) { + super(message); + this.name = 'TrinoProfileError'; + } +} + +/** + * Read all profiles from workspace settings (`dj.trino.profiles`). + */ +export function listProfiles(): TrinoProfile[] { + const cfg = vscode.workspace.getConfiguration(); + const profiles = cfg.get(`${SETTINGS_KEY}.profiles`, []); + return Array.isArray(profiles) ? profiles : []; +} + +export function getActiveProfileName(): string | null { + const cfg = vscode.workspace.getConfiguration(); + const name = cfg.get(`${SETTINGS_KEY}.activeProfile`, '').trim(); + return name || null; +} + +/** + * Resolve the active profile, falling back to the first profile in the list + * when `activeProfile` is unset. Returns null if no profiles are configured — + * callers decide whether that's a hard error or fall-through to CLI mode. + */ +export function getActiveProfile(): TrinoProfile | null { + const profiles = listProfiles(); + if (profiles.length === 0) { + return null; + } + const activeName = getActiveProfileName(); + if (activeName) { + const found = profiles.find((p) => p.name === activeName); + if (found) { + return found; + } + } + return profiles[0]; +} + +export function getProfileByName(name: string): TrinoProfile | null { + return listProfiles().find((p) => p.name === name) ?? null; +} + +/** + * Save or overwrite a profile in workspace settings. If `previousName` is + * supplied and differs from `profile.name`, this is treated as a rename and + * the old entry is removed (and any associated secrets are migrated). + * + * Does NOT touch secrets — credentials always flow through + * `storeSecret(...)` which writes to SecretStorage. + */ +export async function upsertProfile( + context: vscode.ExtensionContext, + profile: TrinoProfile, + previousName?: string, +): Promise { + validateProfile(profile); + + const cfg = vscode.workspace.getConfiguration(); + const existing = listProfiles(); + + let next: TrinoProfile[]; + if (previousName && previousName !== profile.name) { + next = existing.filter( + (p) => p.name !== previousName && p.name !== profile.name, + ); + next.push(profile); + await migrateSecrets(context, previousName, profile.name); + } else { + next = existing.filter((p) => p.name !== profile.name); + next.push(profile); + } + + await cfg.update( + `${SETTINGS_KEY}.profiles`, + next, + vscode.ConfigurationTarget.Workspace, + ); +} + +export async function deleteProfile( + context: vscode.ExtensionContext, + name: string, +): Promise { + const cfg = vscode.workspace.getConfiguration(); + const remaining = listProfiles().filter((p) => p.name !== name); + await cfg.update( + `${SETTINGS_KEY}.profiles`, + remaining, + vscode.ConfigurationTarget.Workspace, + ); + // Clear any secrets associated with the deleted profile. + for (const kind of ['password', 'bearerToken'] as const) { + await context.secrets.delete(secretKey(name, kind)); + } + // If the deleted profile was the active one, clear the pointer. + if (getActiveProfileName() === name) { + await cfg.update( + `${SETTINGS_KEY}.activeProfile`, + '', + vscode.ConfigurationTarget.Workspace, + ); + } +} + +export async function setActiveProfile(name: string): Promise { + const cfg = vscode.workspace.getConfiguration(); + await cfg.update( + `${SETTINGS_KEY}.activeProfile`, + name, + vscode.ConfigurationTarget.Workspace, + ); +} + +/** + * Store a secret in VS Code SecretStorage, keyed per-profile. Callers pass + * the secret in via the API message bus; this helper never reads from + * settings.json. + */ +export async function storeSecret( + context: vscode.ExtensionContext, + profileName: string, + kind: 'password' | 'bearerToken', + value: string, +): Promise { + await context.secrets.store(secretKey(profileName, kind), value); +} + +/** + * Resolve the secret string for a profile based on its `authSource` tier. + * Returns `null` for `authMethod: 'none'`. Throws TrinoProfileError on any + * fixable misconfiguration (e.g. missing env var, unreadable password file). + */ +export async function resolveProfileSecret( + context: vscode.ExtensionContext, + profile: TrinoProfile, +): Promise { + if (profile.authMethod === 'none') { + return null; + } + + switch (profile.authSource) { + case 'secret-storage': { + const kind: 'password' | 'bearerToken' = + profile.authMethod === 'bearer' ? 'bearerToken' : 'password'; + let secret: string | undefined; + try { + secret = await context.secrets.get(secretKey(profile.name, kind)); + } catch (err: unknown) { + // libsecret/keyring not installed (Linux headless). Surface a + // helpful error pointing the user at the env-var / password-file + // tiers instead of bubbling the cryptic underlying error. + throw new TrinoProfileError( + `Could not access OS keychain for profile "${profile.name}". On headless Linux hosts, switch the profile authSource to "env-var" or "password-file". Underlying error: ${formatErr(err)}`, + 'KEYCHAIN_UNAVAILABLE', + ); + } + if (!secret) { + throw new TrinoProfileError( + `No saved credential for profile "${profile.name}". Run "DJ: Set Trino Credentials" to store a password or bearer token in SecretStorage.`, + 'NO_SECRET', + ); + } + return secret; + } + case 'env-var': { + if (!profile.secretEnvVar) { + throw new TrinoProfileError( + `Profile "${profile.name}" has authSource="env-var" but no secretEnvVar configured.`, + 'AUTH_SOURCE_MISCONFIGURED', + ); + } + const value = process.env[profile.secretEnvVar]; + if (!value) { + throw new TrinoProfileError( + `Environment variable "${profile.secretEnvVar}" is unset for profile "${profile.name}".`, + 'NO_SECRET', + ); + } + return value; + } + case 'password-file': { + if (!profile.passwordFilePath) { + throw new TrinoProfileError( + `Profile "${profile.name}" has authSource="password-file" but no passwordFilePath configured.`, + 'AUTH_SOURCE_MISCONFIGURED', + ); + } + const expanded = expandTilde(profile.passwordFilePath); + try { + const raw = await fs.promises.readFile(expanded, 'utf8'); + // Trim trailing newlines that file editors love to append. + return raw.replace(/\r?\n$/, ''); + } catch (err: unknown) { + throw new TrinoProfileError( + `Failed to read password file "${expanded}" for profile "${profile.name}": ${formatErr(err)}`, + 'NO_SECRET', + ); + } + } + case 'dbt-profile': { + if (!profile.dbtProfile) { + throw new TrinoProfileError( + `Profile "${profile.name}" has authSource="dbt-profile" but no dbtProfile configured.`, + 'AUTH_SOURCE_MISCONFIGURED', + ); + } + return resolveDbtProfileSecret(profile); + } + default: { + throw new TrinoProfileError( + `Unknown authSource for profile "${profile.name}".`, + 'AUTH_SOURCE_MISCONFIGURED', + ); + } + } +} + +/** + * Parse `~/.dbt/profiles.yml` looking for `outputs..password` or + * `password_file` or `jwt_token` for the configured dbt profile name. + */ +async function resolveDbtProfileSecret(profile: TrinoProfile): Promise { + const profilesPath = path.join(os.homedir(), '.dbt', 'profiles.yml'); + let raw: string; + try { + raw = await fs.promises.readFile(profilesPath, 'utf8'); + } catch (err: unknown) { + throw new TrinoProfileError( + `Could not read dbt profiles file at "${profilesPath}": ${formatErr(err)}`, + 'DBT_PROFILE_NOT_FOUND', + ); + } + let parsed: any; + try { + parsed = yamlParse(raw); + } catch (err: unknown) { + throw new TrinoProfileError( + `Failed to parse "${profilesPath}": ${formatErr(err)}`, + 'DBT_PROFILE_NOT_FOUND', + ); + } + + const dbtProfileName = profile.dbtProfile!; + const dbtTargetName = + profile.dbtTarget ?? parsed?.[dbtProfileName]?.target ?? 'default'; + const output = + parsed?.[dbtProfileName]?.outputs?.[dbtTargetName] ?? undefined; + + if (!output) { + throw new TrinoProfileError( + `dbt profile "${dbtProfileName}.${dbtTargetName}" not found in "${profilesPath}".`, + 'DBT_PROFILE_NOT_FOUND', + ); + } + + if (profile.authMethod === 'bearer') { + const token = output.jwt_token ?? output.bearer_token ?? output.token; + if (token) { + return String(token); + } + } + if (output.password) { + return String(output.password); + } + if (output.password_file) { + const expanded = expandTilde(String(output.password_file)); + try { + return (await fs.promises.readFile(expanded, 'utf8')).replace( + /\r?\n$/, + '', + ); + } catch (err: unknown) { + throw new TrinoProfileError( + `Failed to read dbt password_file "${expanded}": ${formatErr(err)}`, + 'NO_SECRET', + ); + } + } + + throw new TrinoProfileError( + `dbt profile "${dbtProfileName}.${dbtTargetName}" does not define a password / password_file / jwt_token.`, + 'NO_SECRET', + ); +} + +function secretKey(profileName: string, kind: 'password' | 'bearerToken') { + return `${SECRET_NS}.${profileName}.${kind}`; +} + +async function migrateSecrets( + context: vscode.ExtensionContext, + oldName: string, + newName: string, +): Promise { + for (const kind of ['password', 'bearerToken'] as const) { + const value = await context.secrets.get(secretKey(oldName, kind)); + if (value) { + await context.secrets.store(secretKey(newName, kind), value); + await context.secrets.delete(secretKey(oldName, kind)); + } + } +} + +function expandTilde(p: string): string { + if (p.startsWith('~')) { + return path.join(os.homedir(), p.slice(1)); + } + return p; +} + +function formatErr(err: unknown): string { + if (err instanceof Error) { + return err.message; + } + return String(err); +} + +export function validateProfile(profile: TrinoProfile): void { + if (!profile.name?.trim()) { + throw new TrinoProfileError( + 'Profile name is required.', + 'AUTH_SOURCE_MISCONFIGURED', + ); + } + if (!profile.coordinatorUrl?.trim()) { + throw new TrinoProfileError( + `Profile "${profile.name}" is missing coordinatorUrl.`, + 'AUTH_SOURCE_MISCONFIGURED', + ); + } + if (!profile.user?.trim()) { + throw new TrinoProfileError( + `Profile "${profile.name}" is missing user.`, + 'AUTH_SOURCE_MISCONFIGURED', + ); + } + const validMethods: TrinoAuthMethod[] = [ + 'none', + 'basic', + 'bearer', + 'password-file', + ]; + if (!validMethods.includes(profile.authMethod)) { + throw new TrinoProfileError( + `Profile "${profile.name}" has invalid authMethod "${profile.authMethod}".`, + 'AUTH_SOURCE_MISCONFIGURED', + ); + } +} diff --git a/src/services/trino/queryJsonSanitizer.ts b/src/services/trino/queryJsonSanitizer.ts new file mode 100644 index 0000000..860aab7 --- /dev/null +++ b/src/services/trino/queryJsonSanitizer.ts @@ -0,0 +1,896 @@ +/** + * Trino QueryInfo sanitizer + persistence. + * + * Takes the raw `/v1/query/{id}` payload from the coordinator and writes + * two artifacts to `/.dj/diagnostics/`: + * + * .full.json — raw coordinator response (audit copy). + * .json — LLM-friendly: summary + key stats only, with + * operator detail trimmed, per-driver task detail + * dropped, and a tool firewall that rejects any + * field shaped like row data. + * + * Hash-based cache: skip re-sanitization when the queryId already has a + * sanitized file < 60s old (configurable via SANITIZE_CACHE_TTL_MS). The + * full.json is also reused — fetching the same query twice in close + * succession is a no-op. + */ + +import { + parseDataSize, + parseDurationMs, + parseDurationNanos, +} from '@shared/trino/parse'; +import type { + TrinoOperatorSummaryEntry, + TrinoPersistedQuery, + TrinoQuerySummary, + TrinoStage, +} from '@shared/trino/types'; +import { WORKSPACE_ROOT } from 'admin'; +import * as fs from 'fs'; +import * as path from 'path'; + +// Re-export the shared parsers so existing callers that imported them +// from this module continue to work (the implementations now live in +// `@shared/trino/parse`, but downstream tests + extension code still +// pull them through here). +export { parseDataSize, parseDurationMs }; + +const DIAGNOSTICS_SUBDIR = path.join('.dj', 'diagnostics'); +const DEFAULT_RETENTION_DAYS = 30; +export const SANITIZE_CACHE_TTL_MS = 60_000; + +/** + * Field-name patterns that suggest row data leakage. These should never + * appear in QueryInfo, but the tool firewall throws if they do — any + * change in upstream Trino that exposes row data won't quietly leak + * sensitive customer data into LLM prompts. + */ +const FIREWALL_FIELD_NAMES = new Set(['data', 'rows', 'values', 'rowData']); +/** + * Containers that are known to legitimately have row data — we walk the + * input and reject if any of these top-level keys are present. The + * sanitized payload never contains these. + */ +const FIREWALL_BLOCKED_TOPLEVEL = new Set(['result', 'queryResult', 'data']); + +export class QueryInfoFirewallError extends Error { + constructor(message: string) { + super(message); + this.name = 'QueryInfoFirewallError'; + } +} + +export type SanitizeResult = { + queryId: string; + jsonPath: string; + fullJsonPath: string; + sanitized: SanitizedQueryInfo; +}; + +export type SanitizedQueryInfo = { + summary: TrinoQuerySummary; + queryStats: Record; + failureInfo?: Record; + errorCode?: Record | string; + dynamicFiltersStats?: Record; + query: string; + rootStage?: TrinoStage; + operatorSummary?: TrinoOperatorSummaryEntry[]; + /** + * Profile + coordinator URL captured at persist time. Lets the + * History tab show which Trino cluster a sanitized JSON came from + * and filter by it. Optional: if absent on a file, the History UI + * treats the profile as unknown — those entries bucket under + * "(none)" in the profile filter and skip the row pill. + */ + profileName?: string; + coordinatorUrl?: string; +}; + +export type SanitizeOptions = { + workspaceRoot?: string; + cacheTtlMs?: number; + /** + * Identifies the Trino profile + coordinator the raw QueryInfo came + * from. Stamped onto the sanitized JSON so the History tab can show + * + filter by source profile after the user has moved on. + */ + source?: { + profileName?: string; + coordinatorUrl?: string; + }; +}; + +/** + * Sanitize a raw QueryInfo and persist both copies. + * + * If a sanitized copy < `cacheTtlMs` old already exists for `queryId`, + * reads + returns it without re-writing. + */ +export async function sanitizeAndPersist( + rawQueryInfo: Record, + opts: SanitizeOptions = {}, +): Promise { + const workspaceRoot = opts.workspaceRoot ?? WORKSPACE_ROOT; + const cacheTtlMs = opts.cacheTtlMs ?? SANITIZE_CACHE_TTL_MS; + + const queryId = + typeof rawQueryInfo.queryId === 'string' + ? rawQueryInfo.queryId + : typeof rawQueryInfo.id === 'string' + ? rawQueryInfo.id + : ''; + if (!queryId) { + throw new Error( + 'Cannot sanitize QueryInfo without a queryId (got neither queryId nor id).', + ); + } + + toolFirewall(rawQueryInfo); + + const dir = path.join(workspaceRoot, DIAGNOSTICS_SUBDIR); + await fs.promises.mkdir(dir, { recursive: true }); + + const jsonPath = path.join(dir, `${queryId}.json`); + const fullJsonPath = path.join(dir, `${queryId}.full.json`); + + // Cache hit? + try { + const stat = await fs.promises.stat(jsonPath); + if (Date.now() - stat.mtimeMs < cacheTtlMs) { + const cached = JSON.parse( + await fs.promises.readFile(jsonPath, 'utf8'), + ) as SanitizedQueryInfo; + return { queryId, jsonPath, fullJsonPath, sanitized: cached }; + } + } catch { + // No cached file - fall through. + } + + const sanitized = sanitize(rawQueryInfo); + if (opts.source?.profileName) { + sanitized.profileName = opts.source.profileName; + } + if (opts.source?.coordinatorUrl) { + sanitized.coordinatorUrl = opts.source.coordinatorUrl; + } + + await fs.promises.writeFile( + fullJsonPath, + JSON.stringify(rawQueryInfo, null, 2), + 'utf8', + ); + await fs.promises.writeFile( + jsonPath, + JSON.stringify(sanitized, null, 2), + 'utf8', + ); + + return { queryId, jsonPath, fullJsonPath, sanitized }; +} + +/** + * Pure sanitization (no I/O). Exported for testability. + */ +export function sanitize( + rawQueryInfo: Record, +): SanitizedQueryInfo { + toolFirewall(rawQueryInfo); + + const summary = summarizeQueryInfo(rawQueryInfo); + const queryStats = stripQueryStats( + rawQueryInfo.queryStats as Record | undefined, + ); + const stageLookup = buildStageLookup(rawQueryInfo); + const rootStage = trimStage(pickRootStage(rawQueryInfo), stageLookup); + const operatorSummary = flattenAndTrimOperatorSummary(rawQueryInfo); + + return { + summary, + queryStats, + failureInfo: rawQueryInfo.failureInfo as + | Record + | undefined, + errorCode: rawQueryInfo.errorCode as + | Record + | string + | undefined, + dynamicFiltersStats: rawQueryInfo.dynamicFiltersStats as + | Record + | undefined, + query: typeof rawQueryInfo.query === 'string' ? rawQueryInfo.query : '', + rootStage, + operatorSummary, + }; +} + +/** + * Compute the compact summary used by the master list, the detail header, + * and the analyzer skill's "headline" pass. + */ +export function summarizeQueryInfo( + raw: Record, +): TrinoQuerySummary { + const stats = (raw.queryStats as Record) || {}; + const session = (raw.session as Record) || {}; + const errorCode = raw.errorCode as Record | undefined; + const failureInfo = raw.failureInfo as Record | undefined; + const queryId = (raw.queryId as string) ?? (raw.id as string) ?? ''; + + const operators = collectOperators(raw); + const { + largestOperator, + dataSkewScore: operatorSkewScore, + peakOpBytes, + } = computeOperatorMetrics(operators); + const joinDistributionTypes = collectJoinDistributions(operators); + const connectorTypes = collectConnectors(operators); + + return { + queryId, + state: (raw.state as string) ?? 'UNKNOWN', + query: typeof raw.query === 'string' ? raw.query : undefined, + user: (session.user as string) ?? (raw.user as string) ?? undefined, + source: (session.source as string) ?? (raw.source as string) ?? undefined, + catalog: (session.catalog as string) ?? undefined, + schema: (session.schema as string) ?? undefined, + created: (stats.createTime as string) ?? undefined, + started: (stats.executionStartTime as string) ?? undefined, + ended: (stats.endTime as string) ?? undefined, + cpuTimeMs: parseDurationMs(stats.totalCpuTime), + wallTimeMs: parseDurationMs(stats.elapsedTime), + queuedTimeMs: parseDurationMs(stats.queuedTime), + analysisTimeMs: parseDurationMs(stats.analysisTime), + // Current Trino emits `planningTime`; older builds used + // `totalPlanningTime`. Same wall-clock planner phase. + planningTimeMs: parseDurationMs( + stats.planningTime ?? stats.totalPlanningTime, + ), + peakUserMemoryBytes: + parseDataSize(stats.peakUserMemoryReservation) ?? peakOpBytes, + peakTotalMemoryBytes: parseDataSize(stats.peakTotalMemoryReservation), + processedRows: numberOrUndefined(stats.processedInputPositions), + processedBytes: parseDataSize(stats.processedInputDataSize), + // Newer Trino builds emit `totalDrivers` / `completedDrivers` + // and have dropped `totalSplits` / `completedSplits` from + // queryStats. Both names refer to the same scheduling unit, so + // fall back to the drivers counters when the split counters are + // absent. Same for queued / running. + totalSplits: numberOrUndefined(stats.totalSplits ?? stats.totalDrivers), + completedSplits: numberOrUndefined( + stats.completedSplits ?? stats.completedDrivers, + ), + queuedSplits: numberOrUndefined(stats.queuedSplits ?? stats.queuedDrivers), + runningSplits: numberOrUndefined( + stats.runningSplits ?? stats.runningDrivers, + ), + blockedTimeMs: parseDurationMs(stats.totalBlockedTime), + dataSkewScore: operatorSkewScore ?? computeTaskSkewScore(raw), + largestOperator, + joinDistributionTypes, + connectorTypes, + errorCode: asScalarString(errorCode?.name), + errorType: asScalarString(errorCode?.type), + failureMessage: asScalarString(failureInfo?.message), + }; +} + +function asScalarString(v: unknown): string | undefined { + if (v === null || v === undefined) { + return undefined; + } + if (typeof v === 'string') { + return v; + } + if (typeof v === 'number' || typeof v === 'boolean') { + return String(v); + } + return undefined; +} + +/** + * Strip verbose info blobs from queryStats while keeping the diagnostic + * skeleton intact. + */ +function stripQueryStats( + stats: Record | undefined, +): Record { + if (!stats) { + return {}; + } + const out: Record = {}; + for (const [k, v] of Object.entries(stats)) { + // operatorSummaries is reproduced as a top-level field after + // trimming; including it again on queryStats just bloats the + // payload. + if (k === 'operatorSummaries') { + continue; + } + if (k === 'stageGcStatistics') { + continue; + } + if (k === 'rootOperator') { + continue; + } + out[k] = v; + } + return out; +} + +/** + * Resolve the root stage of a raw QueryInfo across the two shapes + * Trino has shipped: + * + * - Older builds (≤ 4xx): `outputStage` or `rootStage` directly on + * the root, with `subStages: [...]` for nested children objects. + * - Newer builds: a `stages` envelope `{ outputStageId, stages: [ + * stage, … ] }`. The `stages` array is flat — every stage in the + * plan, including the root, sits at the same level. Each stage's + * `subStages` array may carry either nested child objects (some + * versions) or just child `stageId` strings (most newer versions) + * that have to be resolved against the flat list. + * + * Returns undefined when neither shape is present (e.g. preview rows + * that never carry a stage tree). + */ +function pickRootStage(raw: Record): unknown { + if (raw.outputStage) { + return raw.outputStage; + } + if (raw.rootStage) { + return raw.rootStage; + } + const envelope = raw.stages as Record | undefined; + const children = envelope?.stages; + if (Array.isArray(children) && children.length > 0) { + const outputId = envelope?.outputStageId; + if (typeof outputId === 'string') { + const match = (children as Array>).find( + (s) => s.stageId === outputId, + ); + if (match) { + return match; + } + } + return children[0]; + } + return undefined; +} + +/** + * Index the flat `raw.stages.stages` array by `stageId` so that the + * stage walk can resolve string-id `subStages` references back to the + * underlying stage objects. Returns an empty map for the older nested + * shape (where children are inline objects and don't need resolving). + */ +function buildStageLookup( + raw: Record, +): Map> { + const lookup = new Map>(); + const envelope = raw.stages as Record | undefined; + const children = envelope?.stages; + if (Array.isArray(children)) { + for (const stage of children) { + if (stage && typeof stage === 'object') { + const obj = stage as Record; + if (typeof obj.stageId === 'string') { + lookup.set(obj.stageId, obj); + } + } + } + } + return lookup; +} + +/** + * Walk the stage tree and emit a trimmed copy: keep stageStats, drop + * per-driver task detail (just count + per-task scalar stats), recurse. + * + * `subStages` entries can be either nested stage objects (older Trino) + * or string `stageId` references into the flat `raw.stages.stages` + * array (newer Trino). The lookup resolves the string case; if a + * reference can't be resolved it's silently dropped (rare, but better + * than leaking a stray string into the tree). `seen` short-circuits + * pathological cycles since the new envelope can technically reference + * a stage from more than one parent. + */ +function trimStage( + raw: unknown, + lookup: Map>, + seen: Set = new Set(), +): TrinoStage | undefined { + if (!raw || typeof raw !== 'object') { + return undefined; + } + const obj = raw as Record; + const stageId = typeof obj.stageId === 'string' ? obj.stageId : undefined; + if (stageId && seen.has(stageId)) { + return undefined; + } + if (stageId) { + seen.add(stageId); + } + + const stats = obj.stageStats as Record | undefined; + const trimmedStats = stats ? stripStageStats(stats) : undefined; + const tasks = Array.isArray(obj.tasks) + ? (obj.tasks as Array>).map(trimTask) + : undefined; + const subStages = Array.isArray(obj.subStages) + ? (obj.subStages as unknown[]) + .map((child) => { + // String entry → resolve via the flat lookup. Object entry → + // already a nested stage, walk it directly. + if (typeof child === 'string') { + return trimStage(lookup.get(child), lookup, seen); + } + return trimStage(child, lookup, seen); + }) + .filter((s): s is TrinoStage => Boolean(s)) + : undefined; + + return { + stageId, + state: typeof obj.state === 'string' ? obj.state : undefined, + rootStage: typeof obj.rootStage === 'boolean' ? obj.rootStage : undefined, + stageStats: trimmedStats, + tasks, + subStages, + }; +} + +function stripStageStats( + stats: Record, +): Record { + const out: Record = {}; + for (const [k, v] of Object.entries(stats)) { + if (k === 'operatorSummaries') { + continue; + } // reproduced separately + if (k === 'gcInfo') { + continue; + } + out[k] = v; + } + return out; +} + +function trimTask(task: Record): Record { + const out: Record = {}; + for (const [k, v] of Object.entries(task)) { + if (k === 'outputBuffers') { + continue; + } // huge per-driver detail + if (k === 'stats' && v && typeof v === 'object') { + out.stats = trimTaskStats(v as Record); + continue; + } + out[k] = v; + } + return out; +} + +function trimTaskStats( + stats: Record, +): Record { + const out: Record = {}; + for (const [k, v] of Object.entries(stats)) { + if (k === 'pipelines') { + continue; + } + if (k === 'gcInfo') { + continue; + } + if (k === 'runtimeStats') { + continue; + } + out[k] = v; + } + return out; +} + +/** + * Sum a list of duration values to nanoseconds, skipping `undefined` + * entries. Returns `undefined` when *every* entry is missing so the + * downstream "—" placeholder behaves as before. + */ +function sumDurationNanos(parts: Array): number | undefined { + let total = 0; + let any = false; + for (const part of parts) { + const n = parseDurationNanos(part); + if (n !== undefined) { + total += n; + any = true; + } + } + return any ? total : undefined; +} + +function flattenAndTrimOperatorSummary( + raw: Record, +): TrinoOperatorSummaryEntry[] { + const out: TrinoOperatorSummaryEntry[] = []; + for (const op of collectOperators(raw)) { + out.push({ + operatorType: op.operatorType as string | undefined, + pipelineId: op.pipelineId as number | string | undefined, + planNodeId: op.planNodeId as string | undefined, + inputPositions: numberOrUndefined(op.inputPositions), + outputPositions: numberOrUndefined(op.outputPositions), + inputDataSize: op.inputDataSize as number | string | undefined, + outputDataSize: op.outputDataSize as number | string | undefined, + // Trino operator summaries don't carry a single `totalCpuTime` + // field — CPU is split across the three pipeline phases + // (`addInputCpu` + `getOutputCpu` + `finishCpu`). Older payloads + // occasionally roll the total into `totalCpuTime` / `cpuTime`, + // so prefer those when present and fall back to the per-phase + // sum. Same shape for blocked wall (`addInputBlocked` / + // `getOutputBlocked` / `finishBlocked` on newer Trino). + cpuNanos: + parseDurationNanos(op.totalCpuTime ?? op.cpuTime) ?? + sumDurationNanos([op.addInputCpu, op.getOutputCpu, op.finishCpu]), + blockedWallNanos: + parseDurationNanos(op.blockedWall) ?? + sumDurationNanos([ + op.addInputBlocked, + op.getOutputBlocked, + op.finishBlocked, + ]), + peakMemoryReservation: op.peakUserMemoryReservation as + | number + | string + | undefined, + // `info` blobs on operatorSummary are notoriously chatty + // (`hashCollisions`, `predicateRanges`, etc). The skill heuristics + // only need runtimeStats so we keep that and drop info. + runtimeStats: op.runtimeStats as Record | undefined, + }); + } + return out; +} + +function collectOperators( + raw: Record, +): Array> { + const out: Array> = []; + const lookup = buildStageLookup(raw); + const seen = new Set(); + function visit(stage: unknown) { + if (!stage || typeof stage !== 'object') { + return; + } + const obj = stage as Record; + const stageId = typeof obj.stageId === 'string' ? obj.stageId : undefined; + if (stageId && seen.has(stageId)) { + return; + } + if (stageId) { + seen.add(stageId); + } + const stats = obj.stageStats as Record | undefined; + const ops = Array.isArray(stats?.operatorSummaries) + ? (stats.operatorSummaries as Array>) + : []; + for (const op of ops) { + out.push(op); + } + const subStages = obj.subStages as unknown[] | undefined; + if (Array.isArray(subStages)) { + for (const sub of subStages) { + // Resolve string-id `subStages` references via the flat + // lookup so newer Trino payloads contribute their per-stage + // operator summaries. + visit(typeof sub === 'string' ? lookup.get(sub) : sub); + } + } + } + visit(pickRootStage(raw)); + + // Newer Trino builds carry a flat `queryStats.operatorSummaries` in + // addition to (or instead of) per-stage summaries. When the stage + // walk produces nothing, fall back to the flat list so the Operators + // tab + `largestOperator` still populate. + if (out.length === 0) { + const stats = raw.queryStats as Record | undefined; + const flat = stats?.operatorSummaries; + if (Array.isArray(flat)) { + for (const op of flat) { + if (op && typeof op === 'object') { + out.push(op as Record); + } + } + } + } + return out; +} + +function computeOperatorMetrics(ops: Array>): { + largestOperator?: string; + dataSkewScore?: number; + peakOpBytes?: number; +} { + let largestOperator: string | undefined; + let largestBytes = -1; + let dataSkewScore: number | undefined; + let peakOpBytes: number | undefined; + + for (const op of ops) { + const bytes = parseDataSize(op.peakUserMemoryReservation) ?? 0; + if (bytes > largestBytes) { + largestBytes = bytes; + largestOperator = (op.operatorType as string) ?? largestOperator; + } + if (peakOpBytes === undefined || bytes > peakOpBytes) { + peakOpBytes = bytes; + } + } + + // Heuristic data-skew score: ratio of max-task input to avg-task input + // across operators. > 5 typically indicates serious skew. We accumulate + // the worst-case ratio seen. + for (const op of ops) { + const inputDistribution = op.inputDataSizeDistribution as + | Record + | undefined; + if (!inputDistribution) { + continue; + } + const max = parseDataSize(inputDistribution.max) ?? 0; + const avg = parseDataSize(inputDistribution.avg) ?? 0; + if (avg > 0) { + const ratio = max / avg; + if (dataSkewScore === undefined || ratio > dataSkewScore) { + dataSkewScore = ratio; + } + } + } + + return { largestOperator, dataSkewScore, peakOpBytes }; +} + +/** + * Skew from per-task input volumes, for payloads whose operator + * summaries carry no `inputDataSizeDistribution` (newer Trino dropped + * it). Same semantic as the operator-based score: worst-case ratio of + * max-task input to avg-task input, here taken per stage across its + * tasks. Stages with fewer than two tasks (or no input bytes) can't + * exhibit skew and are skipped. + */ +function computeTaskSkewScore( + raw: Record, +): number | undefined { + const lookup = buildStageLookup(raw); + const seen = new Set(); + let worst: number | undefined; + + function visit(stage: unknown) { + if (!stage || typeof stage !== 'object') { + return; + } + const obj = stage as Record; + const stageId = typeof obj.stageId === 'string' ? obj.stageId : undefined; + if (stageId && seen.has(stageId)) { + return; + } + if (stageId) { + seen.add(stageId); + } + + const tasks = Array.isArray(obj.tasks) + ? (obj.tasks as Array>) + : []; + const inputs: number[] = []; + for (const task of tasks) { + const stats = task.stats as Record | undefined; + const bytes = parseDataSize(stats?.processedInputDataSize); + if (bytes !== undefined) { + inputs.push(bytes); + } + } + if (inputs.length >= 2) { + const avg = inputs.reduce((a, b) => a + b, 0) / inputs.length; + if (avg > 0) { + const ratio = Math.max(...inputs) / avg; + if (worst === undefined || ratio > worst) { + worst = ratio; + } + } + } + + const subStages = obj.subStages as unknown[] | undefined; + if (Array.isArray(subStages)) { + for (const sub of subStages) { + visit(typeof sub === 'string' ? lookup.get(sub) : sub); + } + } + } + visit(pickRootStage(raw)); + return worst; +} + +function collectJoinDistributions( + ops: Array>, +): string[] { + const out = new Set(); + for (const op of ops) { + const type = (op.operatorType as string) ?? ''; + if (/Join|Hash/i.test(type)) { + // Operators like "HashBuilderOperator", "LookupJoinOperator" don't + // carry distribution directly, but the join distribution shows up + // on the plan node. We surface the operator name itself as a proxy. + out.add(type); + } + } + return [...out]; +} + +function collectConnectors(ops: Array>): string[] { + const out = new Set(); + for (const op of ops) { + const type = (op.operatorType as string) ?? ''; + if (/Scan|TableScan/i.test(type)) { + const info = op.info as Record | undefined; + // Older payloads expose `connectorName` / `catalog`; current + // Trino table scans carry a splitOperatorInfo with `catalogName`. + const connector = + (info?.connectorName as string) ?? + (info?.catalogName as string) ?? + (info?.catalog as string) ?? + undefined; + if (connector) { + out.add(connector); + } + } + } + return [...out]; +} + +/** + * Reject any payload that contains row-data containers at the top level, + * or any nested field name that matches FIREWALL_FIELD_NAMES on a + * non-trivial value. Pure data-shape check — no LLM tokens are spent on + * payloads we don't trust. + */ +export function toolFirewall(payload: Record): void { + for (const blocked of FIREWALL_BLOCKED_TOPLEVEL) { + if (blocked in payload) { + throw new QueryInfoFirewallError( + `Tool firewall rejected QueryInfo payload: top-level "${blocked}" present (suspected row-data leak).`, + ); + } + } + // Walk one level deep on common offenders. Going deeper costs CPU and + // these top-level + 1-level checks have caught every known leak shape. + const queryStats = payload.queryStats as Record | undefined; + if (queryStats) { + for (const name of FIREWALL_FIELD_NAMES) { + if ( + name in queryStats && + Array.isArray(queryStats[name]) && + (queryStats[name] as unknown[]).length > 0 + ) { + throw new QueryInfoFirewallError( + `Tool firewall rejected QueryInfo payload: queryStats.${name} contains rows.`, + ); + } + } + } +} + +/** + * Read the sanitized JSON for a single previously-analyzed query. + * + * Returns `null` when there's no `/.dj/diagnostics/.json` + * yet, or when the file is unreadable/corrupt. Callers should fall back + * to a REST fetch in that case. This path never touches the network and + * is the cheap default for the Query Control Center detail pane. + */ +export async function readPersistedSanitizedQuery( + queryId: string, + workspaceRoot: string = WORKSPACE_ROOT, +): Promise { + if (!queryId) { + return null; + } + const jsonPath = path.join( + workspaceRoot, + DIAGNOSTICS_SUBDIR, + `${queryId}.json`, + ); + try { + const raw = await fs.promises.readFile(jsonPath, 'utf8'); + const parsed = JSON.parse(raw) as SanitizedQueryInfo; + if (!parsed?.summary?.queryId) { + return null; + } + return parsed; + } catch { + return null; + } +} + +export async function listPersistedQueries( + workspaceRoot: string = WORKSPACE_ROOT, +): Promise { + const dir = path.join(workspaceRoot, DIAGNOSTICS_SUBDIR); + let entries: string[] = []; + try { + entries = await fs.promises.readdir(dir); + } catch { + return []; + } + const result: TrinoPersistedQuery[] = []; + for (const entry of entries) { + if (!entry.endsWith('.json') || entry.endsWith('.full.json')) { + continue; + } + const jsonPath = path.join(dir, entry); + try { + const stat = await fs.promises.stat(jsonPath); + const raw = JSON.parse( + await fs.promises.readFile(jsonPath, 'utf8'), + ) as SanitizedQueryInfo; + if (!raw?.summary?.queryId) { + continue; + } + result.push({ + queryId: raw.summary.queryId, + persistedAt: new Date(stat.mtimeMs).toISOString(), + jsonPath, + summary: raw.summary, + profileName: raw.profileName, + coordinatorUrl: raw.coordinatorUrl, + }); + } catch { + // ignore unreadable / non-conforming entries + } + } + result.sort((a, b) => (a.persistedAt < b.persistedAt ? 1 : -1)); + return result; +} + +/** + * Reap diagnostics older than `retentionDays`. Called opportunistically + * (e.g. when listing persisted queries). + */ +export async function reapOldDiagnostics( + retentionDays: number = DEFAULT_RETENTION_DAYS, + workspaceRoot: string = WORKSPACE_ROOT, +): Promise { + const dir = path.join(workspaceRoot, DIAGNOSTICS_SUBDIR); + let entries: string[] = []; + try { + entries = await fs.promises.readdir(dir); + } catch { + return 0; + } + const cutoff = Date.now() - retentionDays * 86_400_000; + let removed = 0; + for (const entry of entries) { + const fp = path.join(dir, entry); + try { + const stat = await fs.promises.stat(fp); + if (stat.mtimeMs < cutoff) { + await fs.promises.unlink(fp); + removed++; + } + } catch { + // ignore + } + } + return removed; +} + +function numberOrUndefined(raw: unknown): number | undefined { + if (raw === null || raw === undefined) { + return undefined; + } + if (typeof raw === 'number') { + return raw; + } + const n = Number(raw); + return Number.isFinite(n) ? n : undefined; +} diff --git a/src/services/trino/restClient.ts b/src/services/trino/restClient.ts new file mode 100644 index 0000000..8f224a4 --- /dev/null +++ b/src/services/trino/restClient.ts @@ -0,0 +1,479 @@ +/** + * Trino REST API client. + * + * Wraps the coordinator's metadata-plane endpoints: + * GET /v1/info → version + environment, drives the status pill + * GET /v1/query → live + recently-finished query summaries + * GET /v1/query/{queryId} → full QueryInfo JSON (used by sanitizer) + * + * Auth tier resolved via {@link resolveProfileSecret} so credentials never + * leak into settings.json. 401s surface a one-click "refresh credential" + * notification. 429 / 503 surface as actionable errors with the suggested + * retry delay parsed from `Retry-After` when present. + */ + +import type { DJLogger } from '@services/djLogger'; +import { resolveProfileSecret } from '@services/trino/profiles'; +import { summarizeQueryInfo } from '@services/trino/queryJsonSanitizer'; +import { + parseDataSize, + parseDurationMs, + parseDurationNanos, +} from '@shared/trino/parse'; +import type { + TrinoCoordinatorPing, + TrinoOperatorSummaryEntry, + TrinoProfile, + TrinoQueryInfo, + TrinoQuerySummary, + TrinoStage, +} from '@shared/trino/types'; + +// Re-export the shared parsers so the existing `restClient.test.ts` +// imports stay valid. New code should reach for `@shared/trino/parse` +// directly. +export { parseDataSize, parseDurationMs }; +import * as http from 'http'; +import * as https from 'https'; +import { URL } from 'url'; +import type * as vscode from 'vscode'; + +const DEFAULT_TIMEOUT_MS = 10000; + +export class TrinoCoordinatorError extends Error { + constructor( + message: string, + public readonly code: + | 'NETWORK' + | 'TIMEOUT' + | 'UNAUTHORIZED' + | 'RATE_LIMITED' + | 'UNAVAILABLE' + | 'BAD_RESPONSE' + | 'HTTP_ERROR' + | 'PROFILE_ERROR', + public readonly status?: number, + public readonly retryAfterSec?: number, + ) { + super(message); + this.name = 'TrinoCoordinatorError'; + } +} + +export class TrinoRestClient { + constructor( + private readonly context: vscode.ExtensionContext, + private readonly profile: TrinoProfile, + private readonly log: DJLogger, + private readonly timeoutMs: number = DEFAULT_TIMEOUT_MS, + ) {} + + /** + * GET /v1/query/{queryId} — full QueryInfo JSON. + */ + async getQueryInfo(queryId: string): Promise { + const raw = await this.request>( + `/v1/query/${encodeURIComponent(queryId)}`, + ); + return shapeQueryInfo(raw); + } + + /** + * GET /v1/query/{queryId} — raw, unshaped JSON. Used by the sanitizer + * which needs the full original payload to persist alongside the + * trimmed-down one. + */ + async getRawQueryInfo(queryId: string): Promise> { + return this.request>( + `/v1/query/${encodeURIComponent(queryId)}`, + ); + } + + /** + * GET /v1/query — short, recent query summaries from the coordinator + * this profile is bound to. Used by the Query Control Center's Live + * tab when a profile is active, so the rows always match the + * coordinator the rest of the panel (per-query JSON, status pill, + * Analyze-with-AI) is talking to. + */ + async listActiveQueries(): Promise { + const raw = await this.request>>(`/v1/query`); + return Array.isArray(raw) ? raw.map(shapeQuerySummary) : []; + } + + /** + * GET /v1/info — coordinator version + environment. + */ + async pingCoordinator(): Promise { + try { + const raw = await this.request<{ + nodeVersion?: { version?: string }; + environment?: string; + }>(`/v1/info`, this.timeoutMs); + return { + ok: true, + version: raw?.nodeVersion?.version ?? undefined, + environment: raw?.environment ?? undefined, + }; + } catch (err: unknown) { + const message = + err instanceof TrinoCoordinatorError + ? err.message + : err instanceof Error + ? err.message + : String(err); + return { ok: false, error: message }; + } + } + + /** + * Low-level HTTP request. Resolves the JSON body or throws a typed + * TrinoCoordinatorError. Adds X-Trino-User and Authorization headers. + */ + private async request( + pathName: string, + timeoutMs: number = this.timeoutMs, + ): Promise { + let url: URL; + try { + url = new URL(pathName, this.profile.coordinatorUrl); + } catch { + throw new TrinoCoordinatorError( + `Invalid coordinator URL "${this.profile.coordinatorUrl}".`, + 'PROFILE_ERROR', + ); + } + + const headers: Record = { + 'X-Trino-User': this.profile.user, + Accept: 'application/json', + 'User-Agent': 'dj-extension', + }; + + // Resolve secret via profile tier (never reads settings.json). + let secret: string | null = null; + try { + secret = await resolveProfileSecret(this.context, this.profile); + } catch (err: unknown) { + throw new TrinoCoordinatorError( + err instanceof Error ? err.message : String(err), + 'PROFILE_ERROR', + ); + } + if (secret) { + switch (this.profile.authMethod) { + case 'basic': { + const token = Buffer.from( + `${this.profile.user}:${secret}`, + 'utf8', + ).toString('base64'); + headers.Authorization = `Basic ${token}`; + break; + } + case 'bearer': + case 'password-file': { + headers.Authorization = `Bearer ${secret}`; + break; + } + } + } + + const isHttps = url.protocol === 'https:'; + const transport = isHttps ? https : http; + const options: https.RequestOptions = { + method: 'GET', + hostname: url.hostname, + port: url.port || (isHttps ? 443 : 80), + path: `${url.pathname}${url.search}`, + headers, + // Respect profile.verifyTls (defaults to verifying). The + // `rejectUnauthorized: false` path is opt-in for self-signed certs + // common in on-prem / private deployments. + ...(isHttps && this.profile.verifyTls === false + ? { rejectUnauthorized: false } + : {}), + }; + + this.log.info( + `[trino-rest] ${url.pathname} (profile=${this.profile.name}, host=${url.host})`, + ); + + return new Promise((resolve, reject) => { + const controller = new AbortController(); + const timer = setTimeout(() => { + controller.abort(); + reject( + new TrinoCoordinatorError( + `Trino coordinator request timed out after ${timeoutMs}ms (${url.pathname}).`, + 'TIMEOUT', + ), + ); + }, timeoutMs); + + const req = transport.request( + { ...options, signal: controller.signal as unknown as AbortSignal }, + (res) => { + const chunks: Buffer[] = []; + res.on('data', (chunk: Buffer) => chunks.push(chunk)); + res.on('end', () => { + clearTimeout(timer); + const body = Buffer.concat(chunks).toString('utf8'); + const status = res.statusCode ?? 0; + + if (status === 401) { + return reject( + new TrinoCoordinatorError( + `Trino coordinator rejected credentials for profile "${this.profile.name}".`, + 'UNAUTHORIZED', + 401, + ), + ); + } + if (status === 429 || status === 503) { + const retryAfter = parseRetryAfter(res.headers['retry-after']); + return reject( + new TrinoCoordinatorError( + status === 429 + ? `Trino coordinator is rate-limiting requests (429).` + : `Trino coordinator is unavailable (503).`, + status === 429 ? 'RATE_LIMITED' : 'UNAVAILABLE', + status, + retryAfter, + ), + ); + } + if (status < 200 || status >= 300) { + return reject( + new TrinoCoordinatorError( + `Trino coordinator returned HTTP ${status} for ${url.pathname}: ${body.slice(0, 500)}`, + 'HTTP_ERROR', + status, + ), + ); + } + try { + const parsed = body.length ? (JSON.parse(body) as T) : ({} as T); + resolve(parsed); + } catch (err: unknown) { + reject( + new TrinoCoordinatorError( + `Trino coordinator returned invalid JSON for ${url.pathname}: ${err instanceof Error ? err.message : String(err)}`, + 'BAD_RESPONSE', + status, + ), + ); + } + }); + res.on('error', (err) => { + clearTimeout(timer); + reject( + new TrinoCoordinatorError( + `Trino coordinator read error: ${err.message}`, + 'NETWORK', + ), + ); + }); + }, + ); + + req.on('error', (err: NodeJS.ErrnoException) => { + clearTimeout(timer); + if (err.name === 'AbortError') { + return; + } // already rejected via timeout + reject( + new TrinoCoordinatorError( + `Trino coordinator request failed: ${err.message}`, + 'NETWORK', + ), + ); + }); + + req.end(); + }); + } +} + +function parseRetryAfter( + raw: string | string[] | undefined, +): number | undefined { + if (!raw) { + return undefined; + } + const value = Array.isArray(raw) ? raw[0] : raw; + const asInt = parseInt(value, 10); + if (!Number.isNaN(asInt)) { + return asInt; + } + const asDate = Date.parse(value); + if (!Number.isNaN(asDate)) { + const delta = Math.round((asDate - Date.now()) / 1000); + return delta > 0 ? delta : 0; + } + return undefined; +} + +/** + * Shape an entry from `GET /v1/query` (or any QueryInfo-like blob) into our + * compact summary type. + */ +export function shapeQuerySummary( + raw: Record, +): TrinoQuerySummary { + const stats = (raw.queryStats as Record) || {}; + const session = (raw.session as Record) || {}; + const errorCode = raw.errorCode as Record | undefined; + const failureInfo = raw.failureInfo as Record | undefined; + const self = (raw.self as string) || ''; + const queryId = + (raw.queryId as string) || + (typeof raw.id === 'string' ? raw.id : '') || + extractQueryIdFromSelf(self); + + return { + queryId, + state: (raw.state as string) ?? 'UNKNOWN', + query: typeof raw.query === 'string' ? raw.query : undefined, + user: (session.user as string) ?? (raw.user as string) ?? undefined, + source: (session.source as string) ?? (raw.source as string) ?? undefined, + catalog: (session.catalog as string) ?? undefined, + schema: (session.schema as string) ?? undefined, + created: (stats.createTime as string) ?? undefined, + started: (stats.executionStartTime as string) ?? undefined, + ended: (stats.endTime as string) ?? undefined, + cpuTimeMs: parseDurationMs(stats.totalCpuTime), + wallTimeMs: parseDurationMs(stats.elapsedTime), + queuedTimeMs: parseDurationMs(stats.queuedTime), + analysisTimeMs: parseDurationMs(stats.analysisTime), + planningTimeMs: parseDurationMs(stats.totalPlanningTime), + peakUserMemoryBytes: parseDataSize(stats.peakUserMemoryReservation), + peakTotalMemoryBytes: parseDataSize(stats.peakTotalMemoryReservation), + processedRows: numberOrUndefined(stats.processedInputPositions), + processedBytes: parseDataSize(stats.processedInputDataSize), + totalSplits: numberOrUndefined(stats.totalSplits), + completedSplits: numberOrUndefined(stats.completedSplits), + queuedSplits: numberOrUndefined(stats.queuedSplits), + runningSplits: numberOrUndefined(stats.runningSplits), + blockedTimeMs: parseDurationMs(stats.totalBlockedTime), + errorCode: asScalarString(errorCode?.name), + errorType: asScalarString(errorCode?.type), + failureMessage: asScalarString(failureInfo?.message), + }; +} + +function asScalarString(v: unknown): string | undefined { + if (v === null || v === undefined) { + return undefined; + } + if (typeof v === 'string') { + return v; + } + if (typeof v === 'number' || typeof v === 'boolean') { + return String(v); + } + return undefined; +} + +/** + * Shape a raw /v1/query/{id} body into a TrinoQueryInfo. Computes the + * summary, preserves stages + operator summary, and keeps the SQL text. + * + * NOTE: This does NOT sanitize the result. Use queryJsonSanitizer for the + * sanitized + persisted artifact. + */ +export function shapeQueryInfo(raw: Record): TrinoQueryInfo { + const summary = summarizeQueryInfo(raw); + return { + summary, + queryStats: raw.queryStats as Record | undefined, + failureInfo: raw.failureInfo as Record | undefined, + errorCode: raw.errorCode as Record | string | undefined, + dynamicFiltersStats: raw.dynamicFiltersStats as + | Record + | undefined, + query: typeof raw.query === 'string' ? raw.query : '', + rootStage: shapeStage(raw.outputStage ?? raw.rootStage), + operatorSummary: flattenOperatorSummary(raw), + }; +} + +function shapeStage(raw: unknown): TrinoStage | undefined { + if (!raw || typeof raw !== 'object') { + return undefined; + } + const obj = raw as Record; + const subStages = Array.isArray(obj.subStages) + ? (obj.subStages as unknown[]) + .map((s) => shapeStage(s)) + .filter((s): s is TrinoStage => Boolean(s)) + : undefined; + return { + stageId: typeof obj.stageId === 'string' ? obj.stageId : undefined, + state: typeof obj.state === 'string' ? obj.state : undefined, + rootStage: typeof obj.rootStage === 'boolean' ? obj.rootStage : undefined, + stageStats: obj.stageStats as Record | undefined, + tasks: Array.isArray(obj.tasks) + ? (obj.tasks as Array>) + : undefined, + subStages, + }; +} + +function flattenOperatorSummary( + raw: Record, +): TrinoOperatorSummaryEntry[] { + const out: TrinoOperatorSummaryEntry[] = []; + function visit(stage: unknown) { + if (!stage || typeof stage !== 'object') { + return; + } + const obj = stage as Record; + const stats = obj.stageStats as Record | undefined; + const ops = Array.isArray(stats?.operatorSummaries) + ? (stats.operatorSummaries as Array>) + : []; + for (const op of ops) { + out.push({ + operatorType: op.operatorType as string | undefined, + pipelineId: op.pipelineId as number | string | undefined, + planNodeId: op.planNodeId as string | undefined, + inputPositions: numberOrUndefined(op.inputPositions), + outputPositions: numberOrUndefined(op.outputPositions), + inputDataSize: op.inputDataSize as number | string | undefined, + outputDataSize: op.outputDataSize as number | string | undefined, + cpuNanos: parseDurationNanos(op.totalCpuTime ?? op.cpuTime), + blockedWallNanos: parseDurationNanos(op.blockedWall), + peakMemoryReservation: op.peakUserMemoryReservation as + | number + | string + | undefined, + runtimeStats: op.runtimeStats as Record | undefined, + }); + } + const subStages = obj.subStages as unknown[] | undefined; + if (Array.isArray(subStages)) { + for (const sub of subStages) { + visit(sub); + } + } + } + visit(raw.outputStage ?? raw.rootStage); + return out; +} + +function numberOrUndefined(raw: unknown): number | undefined { + if (raw === null || raw === undefined) { + return undefined; + } + if (typeof raw === 'number') { + return raw; + } + const n = Number(raw); + return Number.isFinite(n) ? n : undefined; +} + +function extractQueryIdFromSelf(self: string): string { + // .../v1/query/ + const m = self.match(/\/v1\/query\/([^/?#]+)/); + return m ? m[1] : ''; +} diff --git a/src/shared/trino/parse.ts b/src/shared/trino/parse.ts new file mode 100644 index 0000000..f89563e --- /dev/null +++ b/src/shared/trino/parse.ts @@ -0,0 +1,123 @@ +/** + * Parsers for Trino's heterogeneous duration / data-size encodings. + * + * Trino's REST APIs return durations and data sizes in two shapes + * depending on the endpoint and version: + * + * - Numeric: raw milliseconds (durations) or bytes (sizes). + * - String: human-readable values like "12.34s", "1.5GB", + * "10ms", "500MB", "1h", "1d". + * + * These helpers normalise either shape into a number so consumers + * (sanitizer, REST client shaper, webview stage row renderer) can + * just `formatMs(parseDurationMs(value))` without branching. + * + * Lives under `src/shared/` so both the extension host and the + * webview can import the same implementation. + */ + +/** + * Parse a Trino duration into milliseconds. + * + * Accepts numeric milliseconds (returned as-is, rounded) or strings + * of the form `` with unit one of `ns`, `us`, `ms`, + * `s`, `m`, `h`, `d`. A missing unit is treated as seconds (Trino's + * default when serialising small values). Returns `undefined` for + * inputs that don't match — callers can then render a placeholder. + */ +export function parseDurationMs(raw: unknown): number | undefined { + if (raw === null || raw === undefined) { + return undefined; + } + if (typeof raw === 'number') { + return Math.round(raw); + } + if (typeof raw !== 'string') { + return undefined; + } + const m = raw.match(/^([\d.]+)\s*(ns|us|ms|s|m|h|d)?$/); + if (!m) { + return undefined; + } + const n = parseFloat(m[1]); + if (Number.isNaN(n)) { + return undefined; + } + switch (m[2]) { + case 'ns': + return Math.round(n / 1e6); + case 'us': + return Math.round(n / 1e3); + case 'ms': + return Math.round(n); + case 's': + case undefined: + return Math.round(n * 1000); + case 'm': + return Math.round(n * 60_000); + case 'h': + return Math.round(n * 3_600_000); + case 'd': + return Math.round(n * 86_400_000); + default: + return undefined; + } +} + +/** + * Parse a Trino data size into bytes. + * + * Accepts numeric bytes (returned as-is, rounded) or strings of the + * form `` with unit one of `B`, `kB` / `KB`, `MB`, + * `GB`, `TB`, `PB` (case-insensitive). A missing unit is treated as + * bytes. Returns `undefined` for inputs that don't match. + */ +export function parseDataSize(raw: unknown): number | undefined { + if (raw === null || raw === undefined) { + return undefined; + } + if (typeof raw === 'number') { + return Math.round(raw); + } + if (typeof raw !== 'string') { + return undefined; + } + const m = raw.match(/^([\d.]+)\s*(B|kB|KB|MB|GB|TB|PB)?$/); + if (!m) { + return undefined; + } + const n = parseFloat(m[1]); + if (Number.isNaN(n)) { + return undefined; + } + const unit = (m[2] ?? 'B').toUpperCase(); + switch (unit) { + case 'B': + return Math.round(n); + case 'KB': + return Math.round(n * 1024); + case 'MB': + return Math.round(n * 1024 ** 2); + case 'GB': + return Math.round(n * 1024 ** 3); + case 'TB': + return Math.round(n * 1024 ** 4); + case 'PB': + return Math.round(n * 1024 ** 5); + default: + return undefined; + } +} + +/** + * Parse a Trino duration into nanoseconds (millis * 1e6). Convenience + * wrapper used by operator-summary trimming where the consumer expects + * nanos — e.g. `operatorSummary[].cpuNanos`. + */ +export function parseDurationNanos(raw: unknown): number | undefined { + const ms = parseDurationMs(raw); + if (ms === undefined) { + return undefined; + } + return ms * 1e6; +} diff --git a/src/shared/trino/types.ts b/src/shared/trino/types.ts index 4918c17..3f0e314 100644 --- a/src/shared/trino/types.ts +++ b/src/shared/trino/types.ts @@ -1,5 +1,225 @@ import type { FrameworkEtlSource } from '@shared/framework/types'; +export type TrinoAuthMethod = 'none' | 'basic' | 'bearer' | 'password-file'; + +export type TrinoAuthSource = + | 'secret-storage' + | 'env-var' + | 'password-file' + | 'dbt-profile'; + +export type TrinoProfile = { + name: string; + coordinatorUrl: string; + user: string; + authMethod: TrinoAuthMethod; + authSource: TrinoAuthSource; + verifyTls?: boolean; + // authSource-specific fields + secretEnvVar?: string; // when authSource = 'env-var' + passwordFilePath?: string; // when authSource = 'password-file' + dbtProfile?: string; // when authSource = 'dbt-profile' + dbtTarget?: string; // when authSource = 'dbt-profile' +}; + +export type TrinoCoordinatorPing = { + ok: boolean; + version?: string; + environment?: string; + error?: string; +}; + +/** + * Compact summary derived from a full TrinoQueryInfo. The "headline" view that + * fits in an LLM prompt without blowing the token budget. + */ +export type TrinoQuerySummary = { + queryId: string; + /** + * Trino reports state as one of: + * QUEUED | PLANNING | STARTING | RUNNING | FINISHED | FAILED + * Typed as `string` because connectors occasionally introduce new + * states (e.g. `WAITING_FOR_PREREQUISITES`). + */ + state: string; + query?: string; + user?: string; + source?: string; + catalog?: string; + schema?: string; + created?: string; + started?: string; + ended?: string; + cpuTimeMs?: number; + wallTimeMs?: number; + queuedTimeMs?: number; + analysisTimeMs?: number; + planningTimeMs?: number; + peakUserMemoryBytes?: number; + peakTotalMemoryBytes?: number; + processedRows?: number; + processedBytes?: number; + totalSplits?: number; + completedSplits?: number; + queuedSplits?: number; + runningSplits?: number; + blockedTimeMs?: number; + dataSkewScore?: number; + largestOperator?: string; + joinDistributionTypes?: string[]; + connectorTypes?: string[]; + errorCode?: string; + errorType?: string; + failureMessage?: string; +}; + +/** + * Envelope returned by `trino-fetch-active-queries`. The `source` and + * `profileName` let the Query Control Center surface where the rows + * came from (the configured profile's coordinator over REST, or the + * local Trino CLI's `system.runtime.queries`), so the user never has + * to guess which coordinator the Live tab is looking at. + */ +export type TrinoActiveQueriesResponse = { + source: 'rest' | 'cli'; + profileName: string | null; + rows: TrinoQuerySummary[]; +}; + +/** + * Sanitized form of /v1/query/{id}. Loosely typed because the Trino JSON + * shape varies by version and connector — we keep only what we know we + * care about and pass the rest through. + */ +export type TrinoQueryInfo = { + summary: TrinoQuerySummary; + queryStats?: Record; + failureInfo?: Record; + errorCode?: Record | string; + dynamicFiltersStats?: Record; + /** SQL text submitted to Trino — used by findModelForSql() */ + query: string; + /** Top-level stage tree */ + rootStage?: TrinoStage; + /** Flat operator summary list. */ + operatorSummary?: TrinoOperatorSummaryEntry[]; + /** + * Resolved DJ model for this query, computed by the extension via + * `findModelForSql()` against the manifest. `null` when no strategy + * (`comment` / `fqn` / `cte`) resolves — the UI surfaces a disabled + * "Jump to Model" button with an explanatory tooltip in that case. + */ + modelMatch?: DjModelMatch | null; + /** + * Where this snapshot came from: + * - `persisted` — read from `.dj/diagnostics/.json` (free, + * may be stale). + * - `rest` — fresh fetch from the coordinator (requires a profile). + * + * Set by the extension on every `trino-fetch-query-info` response so + * the detail pane can label the source and offer a "Refresh from + * coordinator" affordance when the local copy is stale. + */ + loadedFrom?: 'persisted' | 'rest'; + /** + * Absolute path to the sanitized diagnostics file backing this + * snapshot (`/.dj/diagnostics/.json`). Set + * whenever the file exists on disk — either because we just wrote + * it (REST + sanitize-and-persist) or because we just read it + * (persisted-first). The UI surfaces this so users can hand the + * exact file path to their AI agent without guessing. + */ + jsonPath?: string; + /** + * Absolute path to the raw `.full.json` coordinator + * snapshot saved beside the sanitized JSON. The full file is the + * unfiltered `/v1/query/{id}` response (typically multi-MB) and is + * useful when the sanitized summary isn't enough — e.g. when an AI + * agent needs the full execution plan or per-driver task detail. + * Set whenever both files exist on disk; the backend guards the + * field with a `fs.existsSync` check before emitting it. + */ + fullJsonPath?: string; + /** + * Profile + coordinator URL of the cluster this snapshot was + * captured against. Always set for REST snapshots; set on persisted + * snapshots when the on-disk JSON records them. Both fields may be + * `undefined` when reading a diagnostic that has no profile metadata + * — the History UI buckets those under "(none)". + */ + profileName?: string; + coordinatorUrl?: string; +}; + +export type TrinoStage = { + stageId?: string; + state?: string; + rootStage?: boolean; + stageStats?: Record; + tasks?: Array>; + subStages?: TrinoStage[]; +}; + +export type TrinoOperatorSummaryEntry = { + operatorType?: string; + pipelineId?: number | string; + planNodeId?: string; + inputPositions?: number; + outputPositions?: number; + inputDataSize?: number | string; + outputDataSize?: number | string; + cpuNanos?: number; + blockedWallNanos?: number; + peakMemoryReservation?: number | string; + runtimeStats?: Record; +}; + +/** Match result for findModelForSql(). */ +export type DjModelMatch = { + project: string; + modelName: string; + modelJsonPath?: string; + /** + * How the match was resolved. + * - `comment` — extracted from the `dbt` query_comment JSON (`node_id`). + * This is the highest-confidence signal because it's authored by dbt + * itself and survives any wrapping (CREATE TABLE AS, INSERT INTO, …). + * - `fqn` — extracted from the materialization target FQN + * (`catalog.schema.table`) in `CREATE TABLE/VIEW … AS …` or + * `INSERT INTO …`. Reliable for `dbt run` / incremental, may collide + * if two projects publish a model with the same name. + * - `cte` — extracted from the trailing CTE name pattern in the + * compiled SQL (`), AS (…) SELECT * FROM `). Used as a + * last resort for `dbt compile` / `dbt show` outputs. + */ + matchedBy: 'comment' | 'fqn' | 'cte'; +}; + +/** Persisted analysis artifact written by trino-analyze-query. */ +export type TrinoAnalysisResult = { + queryId: string; + jsonPath: string; + fullJsonPath: string; + modelMatch: DjModelMatch | null; + promptSnippet: string; +}; + +export type TrinoPersistedQuery = { + queryId: string; + persistedAt: string; + jsonPath: string; + summary: TrinoQuerySummary; + /** + * Profile name + coordinator URL captured when the sanitized JSON + * was first written. Used by the History tab to surface a per-row + * profile pill and a profile filter dropdown. Optional — entries + * without profile metadata leave both fields `undefined` and bucket + * under "(none)" in the filter. + */ + profileName?: string; + coordinatorUrl?: string; +}; + export type TrinoApi = | { type: 'trino-fetch-catalogs'; @@ -13,12 +233,6 @@ export type TrinoApi = request: { catalog: string; schema: string; table: string }; response: TrinoTableColumn[]; } - | { - type: 'trino-fetch-current-schema'; - service: 'trino'; - request: null; - response: string; - } | { type: 'trino-fetch-etl-sources'; service: 'trino'; @@ -38,28 +252,97 @@ export type TrinoApi = response: TrinoSystemNode[]; } | { - type: 'trino-fetch-system-queries'; + type: 'trino-fetch-tables'; service: 'trino'; - request: { schema?: string }; - response: TrinoSystemQuery[]; + request: { catalog: string; schema: string }; + response: string[]; } | { - type: 'trino-fetch-system-query-with-task'; + type: 'trino-fetch-query-info'; service: 'trino'; - request: { id: string }; - response: TrinoSystemQueryWithTask; + request: { + queryId: string; + /** + * `persisted` (default) — try the local sanitized JSON first; + * fall back to REST only if nothing is on disk yet. Free for + * already-analyzed queries. + * `rest` — always hit the coordinator and persist the fresh + * sanitized copy. Used by the "Refresh from coordinator" + * affordance and by the "Analyze with AI" flow. + */ + prefer?: 'persisted' | 'rest'; + }; + response: TrinoQueryInfo; } | { - type: 'trino-fetch-system-query-sql'; + type: 'trino-fetch-active-queries'; service: 'trino'; - request: { id: string }; - response: string; + request: { filter?: 'all' | 'dbt-trino-only' }; + response: TrinoActiveQueriesResponse; } | { - type: 'trino-fetch-tables'; + type: 'trino-fetch-persisted-queries'; service: 'trino'; - request: { catalog: string; schema: string }; - response: string[]; + request: null; + response: TrinoPersistedQuery[]; + } + | { + type: 'trino-delete-persisted-query'; + service: 'trino'; + request: { queryId: string }; + response: { queryId: string; deleted: boolean }; + } + | { + type: 'trino-analyze-query'; + service: 'trino'; + request: { queryId: string }; + response: TrinoAnalysisResult; + } + | { + type: 'trino-list-profiles'; + service: 'trino'; + request: null; + response: { profiles: TrinoProfile[]; active: string | null }; + } + | { + type: 'trino-save-profile'; + service: 'trino'; + request: { profile: TrinoProfile; previousName?: string }; + response: { ok: true }; + } + | { + type: 'trino-delete-profile'; + service: 'trino'; + request: { name: string }; + response: { ok: true }; + } + | { + type: 'trino-set-active-profile'; + service: 'trino'; + request: { name: string }; + response: { ok: true }; + } + | { + type: 'trino-set-credentials'; + service: 'trino'; + request: { + profile: string; + kind: 'password' | 'bearerToken'; + secret: string; + }; + response: { ok: true }; + } + | { + type: 'trino-ping-coordinator'; + service: 'trino'; + request: { profile?: string }; + response: TrinoCoordinatorPing; + } + | { + type: 'trino-jump-to-model-from-query'; + service: 'trino'; + request: { queryId: string }; + response: { matched: boolean; modelMatch?: DjModelMatch }; }; export type TrinoSystemNode = { @@ -70,54 +353,6 @@ export type TrinoSystemNode = { state: string; }; -export type TrinoSystemQuery = { - // analysis_time_ms: number; - created: string; // ISO 8601 format - end: string; // ISO 8601 format - // error_code: string; - // error_type: string; - // last_heartbeat: string; // ISO 8601 format - // planning_time_ms: number; - // queued_time_ms: number; - query?: string; - query_id: string; - // resource_group_id: string[]; - source: string; - started: string; // ISO 8601 format - state: 'FAILED' | 'FINISHED' | 'QUEUED' | 'RUNNING'; - // user: string; -}; - -export type TrinoSystemQueryWithTask = TrinoSystemQuery & - Partial>; - -export type TrinoSystemTask = { - completed_splits: number; - created: string; // ISO 8601 format - end: string; // ISO 8601 format - last_heartbeat: string; // ISO 8601 format - node_id: string; - output_bytes: number; - output_rows: number; - physical_input_bytes: number; - physical_written_bytes: number; - processed_input_bytes: number; - processed_input_rows: number; - query_id: string; - queued_splits: number; - raw_input_bytes: number; - raw_input_rows: number; - running_splits: number; - split_blocked_time_ms: number; - split_cpu_time_ms: number; - split_scheduled_time_ms: number; - splits: number; - stage_id: string; - start: string; // ISO 8601 format - state: string; - task_id: string; -}; - export type TrinoTable = { catalog: string; columns: TrinoTableColumn[]; diff --git a/templates/skills/dj-trino-analyzer/_SKILL.md b/templates/skills/dj-trino-analyzer/_SKILL.md new file mode 100644 index 0000000..be2f2d3 --- /dev/null +++ b/templates/skills/dj-trino-analyzer/_SKILL.md @@ -0,0 +1,312 @@ +--- +name: dj-trino-analyzer +description: >- + Diagnose Trino query performance from the QueryInfo JSONs written by + the DJ Query Control Center to .dj/diagnostics/ — the sanitized + .json and the raw .full.json. Use when the user + mentions "trino slow", "explain why query is slow", "trino query + plan", "broadcast vs partitioned join", "data skew", "trino blocked + time", "operator memory", asks about raw Trino queryInfo / queryStats + fields (physicalInputReadTime, totalDrivers, blockedReasons, + connectorMetrics), wants to compare two queries (e.g. before vs after + a config change), or asks to investigate a specific Trino query ID. +compatibility: DJ (Data JSON) Framework workspace with .dj/diagnostics/ written by `DJ: Analyze Trino Query with AI` +metadata: + dj-skill: '1.0' +--- + +# Analyze a Trino query plan + runtime stats + +The DJ extension writes two files per analyzed query: + +- **`.dj/diagnostics/.json`** — sanitized, shaped for LLM + token budgets. **Read it first**; most diagnoses end here. +- **`.dj/diagnostics/.full.json`** — the raw coordinator + response from `/v1/query/{queryId}`. Open it only for a targeted + deep dive, and read only the slice you need (it can be tens of MB — + use `jq`, never read it whole). + +## File shapes + +Sanitized `.json` top-level keys: + +| Key | Contents | +| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `summary` | Computed headline: state, timings in ms, peak memory bytes, splits, `dataSkewScore`, `largestOperator`, connectors, error fields. | +| `queryStats` | Raw passthrough of the coordinator's `queryStats` (minus `operatorSummaries`, `stageGcStatistics`, `rootOperator`). Values keep raw string forms — `"5.01m"`, `"288482816B"`. | +| `failureInfo`, `errorCode`, `dynamicFiltersStats`, `query` | Raw passthrough. | +| `rootStage` | Trimmed nested stage tree: per-stage `stageStats` and `tasks[]`, minus operator summaries, pipelines, GC info, and output buffers. | +| `operatorSummary` | Flat trimmed operator list (below). | +| `profileName`, `coordinatorUrl` | Which Trino cluster the file came from. | + +Sanitized `operatorSummary[]` entries vs raw `OperatorStats` names: + +| Sanitized | Raw (full.json) | +| ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | +| `operatorType`, `pipelineId`, `planNodeId`, `inputPositions`, `outputPositions`, `inputDataSize`, `outputDataSize` | Same names. | +| `cpuNanos` (number) | `addInputCpu` + `getOutputCpu` + `finishCpu` (Duration strings). | +| `blockedWallNanos` (number) | `blockedWall`. | +| `peakMemoryReservation` | `peakUserMemoryReservation`. | + +Caveats baked into `summary` (computed against multiple Trino +versions): + +- `totalSplits` / `completedSplits` fall back to `totalDrivers` / + `completedDrivers` on newer Trino — same scheduling unit. +- `dataSkewScore` is the worst max/avg input ratio observed — from + per-operator input distributions when the payload carries them + (older Trino), else across each stage's tasks. Absent only when + neither level has the data (e.g. all stages single-task). +- `connectorTypes` / `joinDistributionTypes` are operator-derived + proxies and may still be empty when operators carry no `info`; + identify connectors from `summary.catalog`, catalog names in + `query`, or `inputs[]` in full.json. + +## Reading order + +1. **`summary`** — the headline view. **Always start here.** Most + diagnoses can be made from `summary` alone. +2. **`failureInfo` + `errorCode`** — present when `state === "FAILED"`. + Quote the message back to the user and skip to the recommendation. +3. **`operatorSummary`** — flat operator list. Sort by + `peakMemoryReservation` desc to find the hot spot. +4. **`rootStage`** — full stage tree, only if the operator summary + alone doesn't explain the slowness. +5. **`dynamicFiltersStats`** — dynamic-filter effectiveness; key for + join-pushdown diagnoses. +6. **`query`** (the SQL text) — map the query back to a DJ model. dbt + injects `/* {"app": "dbt", …, "node_id": +"model..", …} */` at the top of every query it + submits (enabled by default in `dbt_core`); read the model name from + `node_id`. Without the comment, infer it from the materialization + target (`CREATE TABLE/VIEW "".""."" AS …` + / `INSERT INTO …`) or the trailing `SELECT * FROM ` of + dbt's compiled form. The Query Control Center UI shows this same + match, but it is computed at display time — it is **not** stored in + the JSON. If none resolves, the query is ad-hoc (or dbt's + `query-comment` was disabled) — say so plainly rather than guessing. + +Do **not** ask the coordinator for additional data. If you need +something not in the sanitized JSON, drill into `full.json` (see the +deep dive below) or recommend the user re-run the analysis. + +## Performance heuristics + +Apply these in order. Cite the field you used. + +### 1. Broadcast-join blow-up + +Symptom: a `HashBuilderOperator` or `LookupJoinOperator` with +`peakMemoryReservation` close to the per-node memory limit, or +`summary.peakUserMemoryBytes` > 50% of it. (There is no +`HashJoinOperator` in Trino — the build side is `HashBuilderOperator`, +the probe side `LookupJoinOperator`.) + +- The build side is the `HashBuilderOperator`: `inputPositions` > ~1M + rows there is the classic blow-up. Confirm the distribution in + full.json — the build-side exchange's `outputBuffers.type` is + `"BROADCAST"`, or the stage's plan fragment says replicated. +- Fix: force `PARTITIONED` distribution (session property + `join_distribution_type=PARTITIONED`), or shrink the build side by + pushing predicates / using an `int_join_models` with explicit + `where` filters upstream of the join. + +### 2. Data skew + +Symptom: `summary.dataSkewScore` > 5. To pinpoint the offender and +classify the skew, compare `stats.totalCpuTime`, +`stats.processedInputDataSize`, and `stats.totalDrivers` across +sibling tasks of the dominant stage in `rootStage.tasks[]`. + +- Max/min task CPU > 3x with similar `totalDrivers` ⇒ **data skew** — + a join key with high null/empty cardinality or a power-law key + distribution. Fix: add null-handling to the join condition, or salt + the key (`coalesce(key, rand() * 1000)` on one side). +- `totalDrivers` also skewed ⇒ **split-distribution skew** — one + worker got more splits; fix file sizing (heuristic 7), not the key. + +### 3. JSON parsing CPU + +Symptom: JSON-heavy SQL (`json_extract*`, `json_parse`, +`CAST(… AS json)` in `query`) with a `ScanFilterAndProjectOperator` or +`FilterAndProjectOperator` dominating `cpuNanos`. There is no dedicated +JSON operator type — parsing burns CPU inside scan/project operators. + +- Confirm in full.json via that operator's `metrics` entries + `"Projection CPU time"` / `"Filter CPU time"`. +- Fix: cast JSON columns to native types in a `stg_*` model so + downstream models don't re-parse on every query. For one-off + filtering, use `json_extract_scalar` with explicit paths instead of + full deserialization. + +### 4. High `blockedTimeMs` + +Symptom: `summary.blockedTimeMs` > 30% of `wallTimeMs`. + +- Check `queryStats.blockedReasons` first. It has exactly one possible + entry: `WAITING_FOR_MEMORY` ⇒ memory pressure (see heuristics 1 + and the memory fields). **Empty + high blocked time ⇒ I/O or + exchange back-pressure**, not memory. +- For back-pressure, find the dominant operator's `blockedWallNanos`; + in full.json check `stageStats.outputBufferUtilization` — sustained + `max ≈ 1.0` means the downstream stage can't drain fast enough. +- Fix: parallelize the slow consumer (raise `task.concurrency`), + reduce the upstream's output with a predicate, or split a monolithic + `mart_*` into smaller intermediate models. + +### 5. Object-store scan latency + +Symptom: a `TableScanOperator` / `ScanFilterAndProjectOperator` on a +`hive` / `iceberg` / `delta_lake` catalog with small `outputDataSize` +but high `blockedWallNanos` (or `physicalInputReadTime` in full.json). + +- The scan is paying per-object latency overhead. +- Fix: enlarge file sizes upstream (target ~128MB-1GB parquet files), + or restrict the scan with a partition predicate. If the model is a + DJ staging model, set `materialization.partitions` on its parent. + +### 6. Dynamic filter effectiveness + +Symptom: `dynamicFiltersStats.dynamicFiltersCompleted` much less than +`totalDynamicFilters`, or `lazyDynamicFilters` > 0. + +- Per-filter detail is in `dynamicFilterDomainStats[]`: + `simplifiedDomain === "ALL"` means the filter pruned nothing; a + `collectionDuration` close to the execution time arrived too late. +- Fix: ensure the join columns have statistics (`ANALYZE TABLE` if the + connector supports it); if the probe table is bucketed, partition or + bucket the build table on the same key. + +### 7. Many small splits + +Symptom: `summary.totalSplits` > 10 000 with low `processedBytes`. + +- Per-split scheduling overhead dominates. +- Fix: compact source files (target Trino split size, typically + ~64MB); on DJ models, set materialization partitions so dbt-trino + produces fewer, larger output files. + +### 8. Failed query + +Symptom: `summary.state === "FAILED"`. + +- Quote `summary.failureMessage` / `summary.errorCode` to the user. +- For `EXCEEDED_TIME_LIMIT`, examine `summary.queuedTimeMs` and + `summary.planningTimeMs` — long queue / planning suggests the + cluster is saturated, not the query. +- For `EXCEEDED_LOCAL_MEMORY_LIMIT`, fall back to the broadcast-join + heuristic above. + +## Deep dive: `full.json` + +The raw QueryInfo, for when the sanitized file isn't enough. Mental +model: + +```text +QueryInfo +└─ queryStats cluster-wide aggregates — start here +└─ stages.stages[] flat list; parent→child via subStages ids + └─ stageStats per-stage aggregates (~same keys as queryStats) + └─ tasks[] → stats per-worker task stats + └─ pipelines[] → operatorSummaries[] per-operator detail +``` + +The same metric names recur verbatim at every level, scoped to that +level — pivot on any field and roll up or down. + +Symptom → where to drill (on `queryStats` unless noted): + +| Symptom | Likely cause | Drill into | +| -------------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------- | +| `queuedTime` large vs `elapsedTime` | Resource-group queueing, not a worker problem | `resourceGroupId` | +| `planningTime` / `analysisTime` > 1s | Heavy metadata, many partitions | `catalogMetadataMetrics`, `optimizerRulesSummaries` | +| `totalScheduledTime` >> `totalCpuTime`, `blockedReasons` empty | I/O bound | Per-stage `physicalInputReadTime`; `connectorMetrics` | +| `blockedReasons` has `WAITING_FOR_MEMORY` | Memory pressure | `peakUserMemoryReservation`, `peakTaskUserMemory`, `spilledDataSize` | +| `totalDrivers` very high for the data size | Many small files / row groups | `stageStats.getSplitDistribution`, `connectorMetrics.dataFiles` | +| One operator's `blockedWall` large | Skew or back-pressure | That operator's `inputPositions` + per-task stats | +| `stageStats.outputBufferUtilization.max ≈ 1.0` | Downstream can't keep up | The consumer stage | +| `failedTasks` > 0 | Worker died / preempted (retries may have recovered) | `tasks[].taskStatus.failures`, `failed*` twins | + +Hard rules when reading either file: + +1. **Never invent a field.** If a key isn't in the reference tables + below, it isn't in the JSON — re-check which level you're at. +2. **State the level a number came from.** `totalScheduledTime` on + `queryStats` is a cluster-wide sum (dozens of hours on a 1-minute + query is normal); the same name on one operator is just that + operator's share. +3. **Check finality.** Trust `queryStats` only when + `finalQueryInfo == true`; trust per-stage detail only when + `pruned == false` (full.json top-level fields). +4. **Watch the `failed*` twins** on `queryStats` / `stageStats` when + retries happened — the unprefixed metric includes discarded work. +5. **Parse, don't eyeball, units.** Durations are strings like + `"5.01m"`; DataSizes are `"288482816B"`. Parsing rules are in the + types reference. + +## Reference files + +Load at most the one file the question needs: + +- [references/query-info.md](references/query-info.md) — every + top-level full.json key; `failureInfo` shape; common error codes. +- [references/query-stats.md](references/query-stats.md) — every + `queryStats` key (applies to both files), grouped by category. +- [references/stage-and-task-stats.md](references/stage-and-task-stats.md) + — stages, tasks, pipelines; how the sanitized `rootStage` was trimmed. +- [references/operator-stats.md](references/operator-stats.md) — raw + `OperatorStats` keys; `metrics` / `connectorMetrics` catalogs + (Iceberg, Parquet, cache wrappers); `info` subtypes. +- [references/types-and-enums.md](references/types-and-enums.md) — + parsing Duration / DataSize; `Metric` shapes; all enum values and + their gotchas. +- [references/recipes.md](references/recipes.md) — jq / Python + snippets: vital signs, wall-clock decomposition, skew, memory, + dynamic filters, before/after compare. + +## DJ model layering — performance expectations by type + +When you've resolved the model name (from the dbt query_comment +`node_id`), the prefix tells you what shape of work to expect: + +- **`stg_*`** — Trino → conformed columns. Should be cheap; high CPU + here usually means JSON parsing or a `stg_union_sources` fanning out + too many sources. +- **`int_*`** — joins, lookbacks, rollups. The expensive layer. Most + broadcast-join blow-ups and data-skew issues land here. +- **`mart_*`** — analytics-ready. Materialized as views in DJ, so + every query against a `mart_*` re-runs the entire upstream DAG. + When a `mart_*` is slow, the fix is almost always upstream + (cache an `int_*` as `materialization: incremental`). + +Tie any recommendation back to the model layer when you can: + +> "This query runs `int__finance__billing__daily_summary` which is the +> int layer where broadcast-join blow-ups are most common. The build +> side here is ~12M rows from `stg__finance__accounts` — shrink it with +> a `where` filter on `account_status = 'active'` upstream." + +## Output format + +Produce **three sections** in order: + +1. **Headline** — one sentence: `state`, `wallTimeMs`, + `peakUserMemoryBytes`, and the single most likely root cause from + the heuristics above. Cite the field you used. +2. **Evidence** — bullet list of the supporting numbers from the + sanitized JSON (operator name, CPU %, memory %, skew evidence, etc.). +3. **Recommendations** — at most 3 actionable items. Each item names + the file or setting to change. + +Do **not** output speculative changes to the SQL — the JSON sources of +truth are the `.model.json` files. Suggest the column / filter / +materialization knob to flip; the user will edit the model JSON. + +## Safety rails + +- **Never** request or accept row-level query results — the sanitizer + strips those out and rejects payloads that contain them. If you + catch yourself wanting `result.data`, stop: the diagnosis can + always be made from operator + stage statistics. +- **Never** suggest editing the generated `.sql` or `.yml` — only + the `.model.json` source of truth. diff --git a/templates/skills/dj-trino-analyzer/references/operator-stats.md b/templates/skills/dj-trino-analyzer/references/operator-stats.md new file mode 100644 index 0000000..8cfd0b3 --- /dev/null +++ b/templates/skills/dj-trino-analyzer/references/operator-stats.md @@ -0,0 +1,140 @@ +# OperatorStats and connectorMetrics + +Field semantics verified against the Trino 479 source +(`io.trino.operator.OperatorStats`). One entry per +`(stageId, pipelineId, operatorId, planNodeId)`. + +In `.full.json`, `OperatorStats` appears in three places: + +- `queryStats.operatorSummaries[]` — cluster-wide rollup per logical operator. +- `stages.stages[].stageStats.operatorSummaries[]` — same rollup, one stage. +- `stages.stages[].tasks[].stats.pipelines[].operatorSummaries[]` — per task/pipeline. + +The sanitized `.json` instead exposes a flat trimmed +`operatorSummary[]` (see the SKILL.md field mapping); everything below +is the raw shape. + +## 1. `OperatorStats` schema + +### Identity + +| JSON key | Type | Meaning | +| -------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `stageId` | int | Integer suffix of the StageId. | +| `pipelineId` | int | Pipeline index within the task. | +| `operatorId` | int | Operator index within the pipeline (0 = pipeline source). | +| `planNodeId` | string | Same id used in `EXPLAIN ANALYZE`. | +| `sourceId` | string or null | Set when the operator pulls from another plan node (joins, exchanges). | +| `operatorType` | string | Java simple-class name. Common: `ScanFilterAndProjectOperator`, `TableScanOperator`, `FilterAndProjectOperator`, `HashAggregationOperator`, `StreamingAggregationOperator`, `HashBuilderOperator`, `LookupJoinOperator`, `NestedLoopJoinOperator`, `ExchangeOperator`, `TaskOutputOperator`, `PartitionedOutputOperator`, `TableWriterOperator`, `MergeWriterOperator`, `OrderByOperator`, `TopNOperator`, `LimitOperator`, `WindowOperator`. There is no `HashJoinOperator` — hash joins are `HashBuilderOperator` (build side) + `LookupJoinOperator` (probe side). | +| `totalDrivers` | long | Driver executions that traversed this operator, summed over all splits — not a per-driver count. | + +### Input + +| JSON key | Type | Meaning | +| ---------------------------------------------------------------- | -------------------------- | ----------------------------------------------------------------- | +| `addInputCalls` / `addInputWall` / `addInputCpu` | long / Duration / Duration | `Operator.addInput(page)` call count, wall, CPU. | +| `physicalInputDataSize` / `physicalInputPositions` | DataSize / long | Bytes/rows read from storage. Non-zero only for source operators. | +| `physicalInputReadTime` | Duration | Time inside storage reads. | +| `internalNetworkInputDataSize` / `internalNetworkInputPositions` | DataSize / long | Received via exchange. Non-zero only for exchange operators. | +| `inputDataSize` / `inputPositions` | DataSize / long | Input after decompression / column reconstruction. | +| `sumSquaredInputPositions` | double | Σ (rows per page)² — page-size variance for skew analysis. | + +### Output, blocking, finish + +| JSON key | Type | Meaning | +| --------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------- | +| `getOutputCalls` / `getOutputWall` / `getOutputCpu` | long / Duration / Duration | `Operator.getOutput()` call count, wall, CPU. | +| `outputDataSize` / `outputPositions` | DataSize / long | Produced bytes/rows. | +| `blockedWall` | Duration | Σ wall time blocked (upstream input, buffer space, dynamic filters, …). | +| `blockedReason` | string or null | Only possible value: `WAITING_FOR_MEMORY`. Null + high `blockedWall` ⇒ non-memory wait. | +| `finishCalls` / `finishWall` / `finishCpu` | long / Duration / Duration | `Operator.finish()` phases. | + +There are no `failed*` twin fields at the operator level (unlike +`queryStats` / `stageStats`). + +### Memory, writer, metrics + +| JSON key | Type | Meaning | +| --------------------------------------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------- | +| `userMemoryReservation` / `revocableMemoryReservation` | DataSize | Current reservations. | +| `peakUserMemoryReservation` / `peakRevocableMemoryReservation` / `peakTotalMemoryReservation` | DataSize | Operator peaks. | +| `spilledDataSize` | DataSize | Bytes spilled by this operator. | +| `physicalWrittenDataSize` | DataSize | Bytes written (writer operators only). | +| `dynamicFilterSplitsProcessed` | long | Splits processed under a dynamic filter on this operator. | +| `metrics` | map | Operator-internal metrics (§2). | +| `connectorMetrics` | map | Connector-provided metrics (§3). | +| `pipelineMetrics` | map | From the driver loop; rarely populated. | +| `info` | object or null | Operator-specific extra info (§4). Dropped from non-final summarized snapshots. | + +## 2. Known `metrics` keys + +Operator-internal entries commonly observed (exact string literals): + +| Key | Metric type | Meaning | +| ------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------- | +| `Filter CPU time` | DurationTiming | CPU inside the compiled filter expression (`ScanFilterAndProject` / `FilterAndProject`). | +| `Projection CPU time` | DurationTiming | CPU inside the compiled projection expression. | +| `CPU time distribution (s)` / `Scheduled time distribution (s)` / `Blocked time distribution (s)` | TDigestHistogram | Per-driver distributions — skew per driver. | +| `Input rows distribution` / `Output rows distribution` | TDigestHistogram | Per-page row counts. | + +## 3. Known `connectorMetrics` keys + +The storage layer reports format/plugin-specific signals on leaf-scan +operators. Keys differ per connector and Trino version — when a key +isn't listed here, grep the connector source rather than guessing: + +```text +rg -n 'new Metrics\(' plugin/trino-/src/main +``` + +### Iceberg split source + +`scanPlanningDuration` (DurationTiming), and LongCounts: `dataFiles`, +`dataFileSizeBytes`, `deleteFileSizeBytes`, `dataManifests`, +`deleteManifests`, `equalityDeleteFiles`, `positionalDeleteFiles`. +`dataFiles` far larger than the predicate should hit ⇒ partition / +column-statistics pruning is not kicking in. + +### Parquet reader + +- `ParquetReaderCompressionFormat_` (LongCount) — one entry per + codec (`SNAPPY`, `ZSTD`, …); value = compressed column-chunk bytes read. +- `ParquetColumnIndexRowsFiltered` (LongCount) — rows skipped by + column-index push-down. + +### Filesystem / cache wrappers + +Base filesystem reports nothing; caching wrappers (e.g. an Alluxio +client) may add LongCounts such as `bytesReadFromCache` and +`bytesReadExternally`. Cache-hit ratio = +`bytesReadFromCache / (bytesReadFromCache + bytesReadExternally)`. +Exact key names come from the wrapper, not upstream Trino. + +## 4. `info` subtypes + +Tagged `"@type"` in full.json. Ones worth opening: + +| `@type` | On operator | Carries | +| ----------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------- | +| `exchangeClientStatus` | ExchangeOperator | `bufferedBytes`, `maxBufferedBytes`, `averageBytesPerRequest`, `bufferedPages`, per-client statuses. | +| `tableWriterInfo` | TableWriterOperator | `statisticsWallTime`, `statisticsCpuTime`, `validationCpuTime`, peak on/off-heap usage. | +| `tableFinishInfo` | TableFinishOperator | Connector commit metrics. | +| `splitOperatorInfo` | TableScanOperator | `{catalogName, splitInfo}` — which file a scan touched. | +| `hashCollisionsInfo` | HashAggregation / HashBuilder | Hash collision stats. | +| `joinOperatorInfo` | LookupJoinOperator | Join-side position stats. | +| `windowInfo` | WindowOperator | Partition/page counts per index. | +| `partitionedOutputInfo` | PartitionedOutputOperator | `{rowsAdded, pagesAdded, outputBufferPeakMemoryUsage}`. | + +## 5. Reading order for one slow operator + +1. `operatorType`, `planNodeId`, `stageId` — confirm which plan node + (the same `planNodeId` can appear in multiple stages). +2. `totalDrivers`, `inputPositions`, `outputPositions` — sanity-check + the work volume. +3. `addInputWall + getOutputWall + finishWall` vs `blockedWall` — + on-thread vs waiting. +4. Wall vs the matching `*Cpu` sums — the gap is stall / I/O. +5. `physicalInputReadTime` + `connectorMetrics` — leaf scans only. +6. `peakUserMemoryReservation`, `spilledDataSize` — joins, aggregations, sorts. +7. `metrics` distributions — per-driver skew. +8. `info` — operator-specific deep dive. diff --git a/templates/skills/dj-trino-analyzer/references/query-info.md b/templates/skills/dj-trino-analyzer/references/query-info.md new file mode 100644 index 0000000..70857e1 --- /dev/null +++ b/templates/skills/dj-trino-analyzer/references/query-info.md @@ -0,0 +1,78 @@ +# Top-level QueryInfo schema (`.full.json`) + +Field semantics verified against the Trino 479 source +(`io.trino.execution.QueryInfo`). Applies to the raw coordinator +response from `/v1/query/{queryId}` that DJ saves as +`.dj/diagnostics/.full.json`. Of these keys, the sanitized +`.json` keeps only `query`, `failureInfo`, `errorCode`, and +`dynamicFiltersStats` (from `queryStats`) verbatim. + +## Root keys + +| JSON key | Type | Meaning | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `queryId` | string | Always present. | +| `session` | object | User, catalog, schema, source, client info, full session-property map. Long; skim only. | +| `state` | `QueryState` enum | See [types-and-enums.md](types-and-enums.md). `FINISHED`/`FAILED` are terminal. | +| `self` | URI | Coordinator URI for this query. | +| `fieldNames` | string[] | Output column names. Empty for DDL/INSERT. | +| `query` | string | SQL text after server-side rewrites. | +| `preparedQuery` | string or null | Only for `EXECUTE` of a prepared statement. | +| `queryStats` | object | **The main metrics blob.** See [query-stats.md](query-stats.md). | +| `updateType` | string or null | E.g. `"INSERT"`, `"CREATE TABLE"`. Null for read-only. | +| `stages` | object or null | `{outputStageId, stages: [...]}` — flat list of all stages. See [stage-and-task-stats.md](stage-and-task-stats.md). Null when the query failed before planning. | +| `failureInfo` | object or null | See below. Drill here first on failed queries. | +| `errorCode` | object or null | `{"code": , "name": , "type": }`. Matches `failureInfo.errorCode`. | +| `errorType` | string or null | Computed from `errorCode.type`. | +| `warnings` | array | Non-fatal advisories: `{warningCode: {code, name}, message}`. | +| `inputs` | array | One per leaf scan: `connectorName`, `catalogName`, `catalogVersion`, `schema`, `table`, `connectorInfo`, `columns[]` (`{name, type}`), `fragmentId`, `planNodeId`. `fragmentId` + `planNodeId` point at the exact operator entry that scanned the table. | +| `output` | object or null | Write target for INSERT/CTAS/MERGE: `connectorName`, `catalogName`, `catalogVersion`, `schema`, `table`, `columns`. | +| `referencedTables` | array | Wider lineage list (includes tables the optimizer filtered out). | +| `routines` | array | UDFs referenced. | +| `selectColumnsLineageInfo` | array or null | Column lineage; empty unless lineage is enabled. | +| `finalQueryInfo` | boolean | `true` iff the query reached a terminal state. **Don't trust counters from non-final dumps.** | +| `resourceGroupId` | array or null | Resource-group path, e.g. `["global", "users", "alice"]`. | +| `queryType` | string or null | See [types-and-enums.md](types-and-enums.md). | +| `retryPolicy` | enum | `NONE` / `TASK` / `QUERY` (fault-tolerant execution). | +| `pruned` | boolean | `true` ⇒ the coordinator dropped stage/task detail to save memory. `queryStats` is still complete; `stages` may not be. | +| `scheduled` / `progressPercentage` / `runningPercentage` | bool / number / number | Mirrors of the same values on `queryStats`; read either copy. | +| `version` | object | Coordinator build, e.g. `{"version": "479"}`. | +| Session side-effect fields (`setCatalog`, `setSchema`, `setPath`, `setAuthorizationUser`, `resetAuthorizationUser`, `setOriginalRoles`, `setSessionProperties`, `resetSessionProperties`, `setRoles`, `addedPreparedStatements`, `deallocatedPreparedStatements`, `startedTransactionId`, `clearTransactionId`) | various | Side-effects the client should apply. Irrelevant for performance work. | + +Most performance work touches only `queryStats`, `stages`, `failureInfo`, +and `session`. + +## `failureInfo` shape + +Always check `failureInfo.type`, `failureInfo.message`, and +`failureInfo.errorLocation` first. + +```json +{ + "type": "io.trino.spi.TrinoException", + "message": "...", + "cause": { "...recursive failureInfo or null...": "" }, + "suppressed": [], + "stack": ["io.trino...", "..."], + "errorLocation": { "lineNumber": 5, "columnNumber": 12 }, + "errorCode": { + "code": 13, + "name": "EXCEEDED_TIME_LIMIT", + "type": "USER_ERROR" + }, + "remoteHost": "..." +} +``` + +Common `errorCode.name` values: + +- `EXCEEDED_LOCAL_MEMORY_LIMIT`, `EXCEEDED_GLOBAL_MEMORY_LIMIT` +- `EXCEEDED_TIME_LIMIT`, `EXCEEDED_CPU_LIMIT` +- `CLUSTER_OUT_OF_MEMORY`, `INSUFFICIENT_RESOURCES` +- `USER_CANCELED`, `ABANDONED_QUERY` +- `REMOTE_HOST_GONE`, `REMOTE_TASK_ERROR`, `REMOTE_TASK_MISMATCH` +- `GENERIC_INTERNAL_ERROR`, `NOT_SUPPORTED`, `SYNTAX_ERROR`, + `INVALID_FUNCTION_ARGUMENT` + +`errorCode.type` is one of `USER_ERROR`, `INTERNAL_ERROR`, +`INSUFFICIENT_RESOURCES`, `EXTERNAL`. diff --git a/templates/skills/dj-trino-analyzer/references/query-stats.md b/templates/skills/dj-trino-analyzer/references/query-stats.md new file mode 100644 index 0000000..e4b600f --- /dev/null +++ b/templates/skills/dj-trino-analyzer/references/query-stats.md @@ -0,0 +1,108 @@ +# `queryStats` schema + +Field semantics verified against the Trino 479 source +(`io.trino.execution.QueryStats`). These keys appear in **both** +diagnostics files: the sanitized `.json` passes `queryStats` +through verbatim (minus `operatorSummaries`, `stageGcStatistics`, and +`rootOperator`), so values keep their raw string forms — parse them per +[types-and-enums.md](types-and-enums.md). + +All values are cluster-level aggregates — sums across every worker, +every stage, every driver unless stated otherwise. + +## 1. Timeline + +| JSON key | Type | Meaning | +| --------------------- | --------------- | --------------------------------------------- | +| `createTime` | Instant | Coordinator accepted the query. | +| `executionStartTime` | Instant or null | First transition to RUNNING. Null until then. | +| `lastHeartbeat` | Instant | Last client poll. | +| `endTime` | Instant or null | Terminal state reached. Null until then. | +| `elapsedTime` | Duration | `endTime − createTime`. Wall clock. | +| `queuedTime` | Duration | Time in QUEUED (resource-group queue). | +| `resourceWaitingTime` | Duration | Waiting for cluster resources after dequeue. | +| `dispatchingTime` | Duration | Time in DISPATCHING. | +| `executionTime` | Duration | Time in STARTING + RUNNING + FINISHING. | +| `analysisTime` | Duration | Parser + analyzer wall time. | +| `planningTime` | Duration | Planner wall time, includes optimizer rules. | +| `planningCpuTime` | Duration | CPU-only share of planning. | +| `startingTime` | Duration | Dispatching tasks to workers. | +| `finishingTime` | Duration | Post-execution commit/cleanup. | + +Wall-clock decomposition: +`elapsedTime ≈ queuedTime + resourceWaitingTime + dispatchingTime + executionTime + finishingTime`. +Use it to answer "where did the wall clock go". + +## 2. Tasks and drivers + +| JSON key | Type | Meaning | +| -------------------------------------------------------------------------- | ---------------------- | -------------------------------------------------------------------- | +| `totalTasks` / `runningTasks` / `completedTasks` | int | Task = one container per stage per worker. | +| `failedTasks` | int | Non-zero ≠ query failed (`retryPolicy != NONE` may have recovered). | +| `totalDrivers` | int | Driver = one execution unit, typically one split. Σ ever started. | +| `queuedDrivers` / `runningDrivers` / `blockedDrivers` / `completedDrivers` | int | Current counts per state. | +| `scheduled` | boolean | All splits generated; progress fields meaningful after this. | +| `progressPercentage` / `runningPercentage` | double or null (0-100) | `completedDrivers ÷ totalDrivers` / `runningDrivers ÷ totalDrivers`. | + +## 3. Memory + +| JSON key | Type | Meaning | +| --------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------- | +| `userMemoryReservation` / `revocableMemoryReservation` / `totalMemoryReservation` | DataSize | Current reservations across all workers. | +| `peakUserMemoryReservation` | DataSize | Cluster high-water mark, user memory only. | +| `peakRevocableMemoryReservation` / `peakTotalMemoryReservation` | DataSize | Same for revocable / total. | +| `peakTaskUserMemory` / `peakTaskRevocableMemory` / `peakTaskTotalMemory` | DataSize | Largest single-task peaks. Compare against the per-node task memory limit. | +| `spilledDataSize` | DataSize | Bytes spilled to disk. Non-zero ⇒ revocable memory was reclaimed. | +| `cumulativeUserMemory` / `failedCumulativeUserMemory` | double | Σ (user memory × seconds held). Resource accounting, not peak spotting. | + +## 4. Time decomposition + +| JSON key | Type | Meaning | +| -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `totalScheduledTime` | Duration | Σ wall time drivers were on a worker thread. Dozens of hours on a 1-minute query is normal for a large cluster. | +| `totalCpuTime` | Duration | Σ JVM-thread CPU time. `scheduled − cpu` ≈ I/O / syscall stall. | +| `totalBlockedTime` | Duration | Σ off-thread blocked wall time. | +| `fullyBlocked` | boolean | True iff every running driver is currently blocked. | +| `blockedReasons` | array | Only possible entry: `WAITING_FOR_MEMORY`. **Empty array + high blocked time ⇒ non-memory blocking (almost always I/O or exchange).** | + +## 5. I/O boundaries + +Keep the three input families distinct: + +| JSON key | Type | Boundary | +| ---------------------------------------------------------------- | --------------- | ----------------------------------------------------------------------------------- | +| `physicalInputDataSize` / `physicalInputPositions` | DataSize / long | Bytes/rows pulled from storage (compressed chunk size for Parquet). | +| `physicalInputReadTime` | Duration | Σ time inside storage reads. Compare to `totalScheduledTime` to size the I/O share. | +| `internalNetworkInputDataSize` / `internalNetworkInputPositions` | DataSize / long | Bytes/rows received via exchange from other Trino stages. | +| `processedInputDataSize` / `processedInputPositions` | DataSize / long | Bytes/rows fed into operator chains (post-decompression). | +| `inputBlockedTime` | Duration | Σ time leaf drivers blocked waiting on input. | +| `outputDataSize` / `outputPositions` | DataSize / long | Returned to the client (or final output stage). | +| `outputBlockedTime` | Duration | Σ time the output stage blocked on a slow consumer. | +| `physicalWrittenDataSize` | DataSize | Bytes written to storage (INSERT/CTAS). | + +Every input/output metric above also has a `failed*` twin +(`failedPhysicalInputDataSize`, `failedCpuTime`, …) counting work done +by tasks that ultimately failed and were discarded. A high +`failedX / X` ratio means retry waste. + +## 6. Writers, GC, dynamic filters, metadata + +| JSON key | Type | Meaning | +| --------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `writtenPositions` / `logicalWrittenDataSize` | long / DataSize | Computed: Σ input rows/bytes over writer operators (`TableWriterOperator`, `MergeWriterOperator`). | +| `stageGcStatistics` | array | Per stage: `{stageId, tasks, fullGcTasks, minFullGcSec, maxFullGcSec, totalFullGcSec, averageFullGcSec}`. Stripped from the sanitized file. | +| `dynamicFiltersStats` | object | `{dynamicFilterDomainStats: [{dynamicFilterId, simplifiedDomain, collectionDuration}], lazyDynamicFilters, replicatedDynamicFilters, totalDynamicFilters, dynamicFiltersCompleted}`. Also surfaced top-level in the sanitized file. | +| `catalogMetadataMetrics` | map | Per-catalog metadata fetch metrics (metastore calls, Iceberg manifest reads). Values are `Metrics` maps. | +| `operatorSummaries` | array of OperatorStats | Cluster-wide rollup per `(stageId, pipelineId, operatorId, planNodeId)`. See [operator-stats.md](operator-stats.md). Stripped from the sanitized file (trimmed top-level `operatorSummary` replaces it). | +| `optimizerRulesSummaries` | array | Per-optimizer-rule invocation counts and timings. For slow-planning hunts. | + +## 7. Field-group quick map + +- **`peak*`** — high-water marks; compare across runs. +- **`failed*`** — discarded work from failed tasks. +- **`physical*`** — storage boundary. **`internalNetwork*`** — exchange boundary. **`processed*`** — operator-input boundary. **`output*`** — query-output boundary. +- **`*Drivers`** — logical work units; **`*Tasks`** — per-worker containers. +- **`*Time`** — Duration string; **`*DataSize`** — DataSize string; **`*Positions`** — row count number. + +The same names recur verbatim on `StageStats`, `TaskStats`, +`PipelineStats`, and `OperatorStats`, scoped to that level. diff --git a/templates/skills/dj-trino-analyzer/references/recipes.md b/templates/skills/dj-trino-analyzer/references/recipes.md new file mode 100644 index 0000000..e316b97 --- /dev/null +++ b/templates/skills/dj-trino-analyzer/references/recipes.md @@ -0,0 +1,186 @@ +# Diagnostic recipes + +Copy-pasteable jq / Python snippets for `.full.json` (raw QueryInfo +shape). On the sanitized `.json`, the `.queryStats.*` expressions +work as-is; stage/task recipes need `rootStage` (nested tree) instead of +`.stages.stages[]`, and operator recipes use the trimmed top-level +`operatorSummary[]`. + +If a number looks wrong, re-check the aggregation level +(query vs stage vs task vs operator). + +## R1. One-line vital signs + +```bash +jq '{ + id: .queryId, state: .state, + err: (.errorCode.name // null), failureType: (.failureInfo.type // null), + elapsed: .queryStats.elapsedTime, cpu: .queryStats.totalCpuTime, + scheduled: .queryStats.totalScheduledTime, blocked: .queryStats.totalBlockedTime, + blockedReasons: .queryStats.blockedReasons, + drivers: .queryStats.totalDrivers, + peakMem: .queryStats.peakUserMemoryReservation, + peakTaskMem: .queryStats.peakTaskUserMemory, + read: .queryStats.physicalInputDataSize, readTime: .queryStats.physicalInputReadTime, + rows: .queryStats.processedInputPositions, + retry: .retryPolicy, final: .finalQueryInfo, pruned: .pruned +}' file.full.json +``` + +Anything off here decides which recipe to run next. Trust `queryStats` +only when `finalQueryInfo == true`; per-stage detail only when +`pruned == false`. + +## R2. Where did the wall clock go? + +```bash +jq '.queryStats | {elapsed: .elapsedTime, queued: .queuedTime, + resourceWaiting: .resourceWaitingTime, dispatching: .dispatchingTime, + planning: .planningTime, starting: .startingTime, + execution: .executionTime, finishing: .finishingTime}' file.full.json +``` + +Sum check: `elapsed ≈ queued + resourceWaiting + dispatching + execution + finishing`. + +## R3. Slowest stages + +```bash +jq -r '.stages.stages[] | [.stageId, .state, + .stageStats.totalScheduledTime, .stageStats.physicalInputReadTime, + .stageStats.totalDrivers, + (.stageStats.outputBufferUtilization.max // 0)] | @tsv' file.full.json | column -t +``` + +Order by `totalScheduledTime` to find the dominant stage. + +## R4. Per-worker skew within a stage + +```bash +STAGE=.1 +jq -r --arg s "$STAGE" '.stages.stages[] | select(.stageId == $s) | .tasks[] | + [.taskStatus.taskId, .taskStatus.nodeId, .taskStatus.state, + .stats.totalCpuTime, .stats.physicalInputReadTime, + .stats.totalDrivers, .stats.peakUserMemoryReservation] | @tsv' \ + file.full.json | column -t | sort -k4 -h +``` + +Max/min `totalCpuTime` > 3x with similar `totalDrivers` ⇒ **data skew** +(one worker got the heavy partitions). `totalDrivers` also skewed ⇒ +**split-distribution skew** (one worker got more splits). + +## R5. Slowest operators (cluster rollup) + +```bash +jq -r '.queryStats.operatorSummaries | sort_by(.addInputWall) | reverse | .[0:15] | + .[] | [.stageId, .planNodeId, .operatorType, + .inputPositions, .outputPositions, + .addInputCpu, .addInputWall, .blockedWall, + .peakUserMemoryReservation, .spilledDataSize] | @tsv' \ + file.full.json | column -t +``` + +- `blockedWall / addInputWall > 1` ⇒ mostly waiting. +- `addInputWall >> addInputCpu` on a scan ⇒ storage-read bound (R6). +- `spilledDataSize > 0` ⇒ memory pressure pushed it off-heap. + +## R6. Scan-leaf deep dive (Iceberg / Parquet) + +```bash +jq '.queryStats.operatorSummaries[] + | select(.operatorType == "ScanFilterAndProjectOperator" + or .operatorType == "TableScanOperator") + | {planNodeId, totalDrivers, inputPositions, inputDataSize, + physicalInputDataSize, physicalInputReadTime, + connectorMetrics, metrics}' file.full.json +``` + +Interpretation table: [operator-stats.md](operator-stats.md) §3. + +## R7. Memory-pressure check + +```bash +jq '{peakUserMem: .queryStats.peakUserMemoryReservation, + peakTotalMem: .queryStats.peakTotalMemoryReservation, + perTaskPeak: .queryStats.peakTaskUserMemory, + spilled: .queryStats.spilledDataSize, + blockedReasons: .queryStats.blockedReasons, + fullyBlocked: .queryStats.fullyBlocked}' file.full.json +``` + +`blockedReasons` contains `WAITING_FOR_MEMORY` ⇒ a memory pool hit a +limit. `spilledDataSize > 0` ⇒ revocable spill triggered (joins, sorts, +aggregations). + +## R8. Output-buffer back-pressure + +```bash +jq -r '.stages.stages[] | [.stageId, + (.stageStats.outputBufferUtilization.p50 // null), + (.stageStats.outputBufferUtilization.p95 // null), + (.stageStats.outputBufferUtilization.max // null)] | @tsv' file.full.json | column -t +``` + +`max ≈ 1.0` with `p95 > 0.9` ⇒ the downstream stage can't keep up. + +## R9. Dynamic-filter health + +```bash +jq '.queryStats.dynamicFiltersStats' file.full.json +``` + +Shape: `{dynamicFilterDomainStats: [{dynamicFilterId, simplifiedDomain, +collectionDuration}], lazyDynamicFilters, replicatedDynamicFilters, +totalDynamicFilters, dynamicFiltersCompleted}`. An entry with +`simplifiedDomain == "ALL"` was useless; a tight domain whose +`collectionDuration` is close to `executionTime` arrived too late to +prune the scan. + +## R10. Compare two queries side by side + +```python +import json + +KEYS = [ + "elapsedTime", "totalScheduledTime", "totalCpuTime", "totalBlockedTime", + "physicalInputDataSize", "physicalInputPositions", "physicalInputReadTime", + "internalNetworkInputDataSize", "processedInputPositions", "totalDrivers", + "peakUserMemoryReservation", "peakTaskUserMemory", "spilledDataSize", + "outputDataSize", "outputPositions", +] + +def stats(path): + with open(path) as f: + q = json.load(f)["queryStats"] + return {k: q.get(k) for k in KEYS} + +a, b = stats("before.full.json"), stats("after.full.json") +for k in KEYS: + print(f"{k:32} A={a[k]!s:25} B={b[k]!s}") +``` + +Compare on the same predicate. `totalDrivers` and +`physicalInputReadTime` differences are the two most common drivers of +wall-clock regressions between connectors / configs. + +## R11. Writer breakdown (INSERT / CTAS) + +```bash +jq '.queryStats.operatorSummaries[] + | select(.operatorType == "TableWriterOperator") + | {planNodeId, stageId, inputPositions, inputDataSize, + physicalWrittenDataSize, info}' file.full.json +``` + +Per-task write volumes live in +`.stages.stages[] | .tasks[].stats.physicalWrittenDataSize`. + +## R12. Slow-planning hunt + +```bash +jq '.queryStats | {planningTime, planningCpuTime, analysisTime}' file.full.json +jq '.queryStats.optimizerRulesSummaries | sort_by(.totalTime) | reverse | .[0:10]' file.full.json +``` + +`planningTime >> planningCpuTime` ⇒ planner blocked on metadata +(metastore round-trips, Iceberg manifest reads). Cross-check +`queryStats.catalogMetadataMetrics`. diff --git a/templates/skills/dj-trino-analyzer/references/stage-and-task-stats.md b/templates/skills/dj-trino-analyzer/references/stage-and-task-stats.md new file mode 100644 index 0000000..d3d4fab --- /dev/null +++ b/templates/skills/dj-trino-analyzer/references/stage-and-task-stats.md @@ -0,0 +1,141 @@ +# Stages, tasks, pipelines + +Field semantics verified against the Trino 479 source +(`io.trino.execution.StagesInfo` / `StageInfo` / `StageStats` / +`TaskInfo` / `TaskStatus`, `io.trino.operator.TaskStats` / +`PipelineStats`). Full detail exists only in `.full.json`; the +sanitized `.json` carries a trimmed tree under `rootStage` (see the +note at the end). + +## 1. `stages` — the envelope (full.json) + +```json +{ + "outputStageId": "20260527_042254_01014_ff2rr.0", + "stages": ["", "..."] +} +``` + +`stages` is a **flat list** of every stage; parent → child edges live in +each entry's `subStages` (stage-id strings). `outputStageId` is the root +stage (writes results to the client / output target). A stage with +`subStages == []` is a leaf scan stage. + +## 2. `StageInfo` — one entry of `stages.stages[]` + +| JSON key | Type | Meaning | +| ----------------- | ----------------- | -------------------------------------------------------------------------- | +| `stageId` | string | E.g. `".0"`. | +| `state` | `StageState` enum | See [types-and-enums.md](types-and-enums.md). | +| `plan` | object or null | Per-stage physical plan fragment (operators, partitioning, output layout). | +| `coordinatorOnly` | boolean | Stage runs only on the coordinator. | +| `types` | array | Output column types of this stage. | +| `stageStats` | object | See §3. | +| `tasks` | array of TaskInfo | One per worker task in this stage. | +| `subStages` | array | Child stage ids (data dependencies). | +| `tables` | map | Tables read by leaf scans in this stage. | +| `failureCause` | object or null | Non-null if this stage failed. | + +## 3. `stageStats` — per-stage aggregates + +Nearly identical to `queryStats` (see [query-stats.md](query-stats.md)) +— same metric names and units, scoped to one stage. Stage-specific +extras: + +| JSON key | Type | Meaning | +| ------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `schedulingComplete` | Instant | All splits generated and dispatched. | +| `getSplitDistribution` | map | Per-leaf-scan distribution of split-generation latency (DistributionSnapshot). Spots split-source skew. | +| `splitSourceMetrics` | map | Per-leaf-scan connector split-source metrics; merged into the leaf operator's `connectorMetrics`. | +| `bufferedDataSize` | DataSize | Bytes currently in the stage's output buffer (sampled). | +| `outputBufferUtilization` | object or null | DistributionSnapshot of buffer fullness (0.0-1.0). Sustained `max ≈ 1.0` ⇒ the downstream stage is the bottleneck. | +| `outputBufferMetrics` | map | Output-buffer implementation metrics. | +| `gcInfo` | object | Stage-scoped GC stats (same shape as `queryStats.stageGcStatistics[i]`). | +| `operatorSummaries` | array of OperatorStats | Per-operator rollup within this stage. | + +`StageStats` carries `peakUserMemoryReservation` and +`peakRevocableMemoryReservation`, but not `peakTotalMemoryReservation` +or the `peakTask*` fields — those exist only at the query level. + +## 4. `tasks[]` — `TaskInfo` + +| JSON key | Type | Meaning | +| ----------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `taskStatus` | object | Lifecycle + lightweight counters (§5). | +| `lastHeartbeat` | Instant | Last coordinator → worker ping. | +| `outputBuffers` | object | Output-buffer state: `type` (`BROADCAST` / `PARTITIONED` / `ARBITRARY` / `SPOOLING`), `state`, `canAddBuffers`, `canAddPages`, `totalBufferedBytes`, `totalBufferedPages`, `totalRowsSent`, `totalPagesSent`, `pipelinedBufferStates`, `utilization`, `spoolingOutputStats`, `metrics`. | +| `noMoreSplits` | array | Leaf scan ids whose split source hit end-of-stream. | +| `stats` | object | Detailed per-task stats (§6). | +| `estimatedMemory` | DataSize or null | Coordinator estimate; only on final snapshots. | +| `needsPlan` | boolean | True until the worker acknowledged the plan. | + +## 5. `taskStatus` + +| JSON key | Type | Meaning | +| ---------------------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------- | +| `taskId` | string | `".."`. | +| `taskInstanceId` | string | Per-attempt UUID. | +| `version` | long | Status snapshot version. | +| `state` | `TaskState` enum | See [types-and-enums.md](types-and-enums.md). | +| `self` | URI | Worker URL. | +| `nodeId` | string | Worker node id — the right field for per-worker skew analysis. | +| `speculative` | boolean | Speculative task (fault-tolerant execution). | +| `failures` | array | Failures observed by this task. | +| `queuedPartitionedDrivers` / `runningPartitionedDrivers` | int | Drivers queued / executing for partitioned splits. | +| `queuedPartitionedSplitsWeight` / `runningPartitionedSplitsWeight` | long | Weighted sums for the priority scheduler. | +| `outputBufferStatus` | object | `{outputBufferStateMachineVersion, outputBuffersFull, overutilized}`. | +| `outputDataSize` | DataSize | Bytes produced by this task. | +| `writerInputDataSize` / `physicalWrittenDataSize` | DataSize | Writer input / bytes physically written. | +| `writerCount` | int or null | Max parallel writers used. | +| `memoryReservation` / `peakMemoryReservation` / `revocableMemoryReservation` | DataSize | Current / peak / revocable user memory. | +| `fullGcCount` / `fullGcTime` | long / Duration | Worker-wide full GCs observed during the task. | +| `dynamicFiltersVersion` | long | Highest dynamic-filter version consumed. | + +## 6. `stats` — `TaskStats` + +Most fields mirror `queryStats` (same names, scoped to one task; no +`failed*` twins at this level). Task-specific extras: + +| JSON key | Type | Meaning | +| ------------------------------------------------------------------------------------------------------ | ---------------------- | -------------------------------------------------------------------------- | +| `createTime` / `firstStartTime` / `lastStartTime` / `terminatingStartTime` / `lastEndTime` / `endTime` | Instant or null | Task lifecycle timeline. | +| `queuedPartitionedDrivers` / `runningPartitionedDrivers` (+ `*SplitsWeight`) | int / long | Subsets of `queuedDrivers` / `runningDrivers` handling partitioned splits. | +| `writerInputDataSize` / `physicalWrittenDataSize` | DataSize | Writer-side totals. | +| `writerCount` | int or null | Max parallel writers in this task. | +| `fullGcCount` / `fullGcTime` | int / Duration | Full GCs observed by this worker during the task. | +| `pipelines` | array of PipelineStats | One per pipeline (§7). Dropped from the sanitized file. | + +## 7. `pipelines[]` — `PipelineStats` + +A pipeline is a chain of operators with the same partitioning; each has +many drivers. Fields mirror the task level (`totalScheduledTime`, +`totalCpuTime`, `totalBlockedTime`, the input/output families, +driver-state counters, memory reservations) plus: + +- `pipelineId`, `inputPipeline` / `outputPipeline` flags, timeline Instants. +- `queuedTime` and `elapsedTime` are **DistributionSnapshots** + (percentiles over individual drivers), not sums — unlike at higher levels. +- `operatorSummaries[]` — per-operator stats within this pipeline. +- `drivers[]` — per-driver stats; usually empty (the coordinator + summarizes them away to keep snapshots small). + +## 8. Three identities you can audit + +For a finished, non-pruned query (modulo rounding of succinct units): + +- `queryStats.totalDrivers ≈ Σ stages[].stageStats.totalDrivers` +- `stageStats.totalDrivers ≈ Σ that stage's tasks[].stats.totalDrivers` +- `stageStats.totalScheduledTime ≈ Σ that stage's tasks[].stats.totalScheduledTime` + +Mid-flight snapshots violate them; trust per-stage breakdowns only when +`finalQueryInfo == true` and `pruned == false`. + +## 9. Sanitized `rootStage` vs full.json `stages` + +The sanitized `.json` re-roots the flat list as a nested tree: +each node keeps `stageId`, `state`, `stageStats` (minus +`operatorSummaries` and `gcInfo`), `tasks[]`, and recursive +`subStages[]`. Task entries drop `outputBuffers`; task `stats` drop +`pipelines` and `gcInfo`. For anything dropped (pipelines, output +buffers, per-stage operator summaries, plan fragments), open +`.full.json`. diff --git a/templates/skills/dj-trino-analyzer/references/types-and-enums.md b/templates/skills/dj-trino-analyzer/references/types-and-enums.md new file mode 100644 index 0000000..82bd71f --- /dev/null +++ b/templates/skills/dj-trino-analyzer/references/types-and-enums.md @@ -0,0 +1,127 @@ +# Type and enum reference + +Verified against the Trino 479 source. Applies to both diagnostics +files — the sanitized `.json` passes raw value strings through +unchanged outside its computed `summary`. + +## 1. Value types + +### Duration + +Succinct string, two decimals, unit auto-selected from +`{ns, us, ms, s, m, h, d}`: `"5.01m"` (minutes), `"20.21h"`, +`"140.14us"`. `DurationTiming.duration` values are sometimes left in +nanoseconds (`"23361077778.00ns"`). + +```python +import re +DUR = re.compile(r"^\s*([0-9.]+)\s*(ns|us|ms|s|m|h|d)\s*$") +_NS = {"ns": 1, "us": 1e3, "ms": 1e6, "s": 1e9, "m": 60e9, "h": 3600e9, "d": 86400e9} +def to_seconds(s): + m = DUR.match(s) + return float(m.group(1)) * _NS[m.group(2)] / 1e9 +``` + +### DataSize + +Two output forms: + +| Form | Example | When | +| --------------------------- | ----------------------- | ------------------------------------------------------------------------------------- | +| Bytes-with-suffix (default) | `"288482816B"` | `/v1/query/{id}` — i.e. every DJ diagnostics file. Parse as `int(value.rstrip("B"))`. | +| Succinct unit | `"275MB"`, `"148.95GB"` | Only the Web-UI endpoint with `?pretty`; you won't see this in DJ files. | + +### Instant / Optional / collections + +- `Instant`: ISO-8601 with `Z`, up to nanosecond precision. +- `Optional` (and OptionalDouble/OptionalInt): `null` when empty, + bare value when present — never a wrapper object. +- Sets and lists are JSON arrays; set order is not stable. + +## 2. `Metric` values (in `metrics` / `connectorMetrics` / `catalogMetadataMetrics`) + +A `Metrics` container serializes as a plain map — no wrapper: + +```json +"connectorMetrics": { + "dataFiles": {"@class": "io.trino.plugin.base.metrics.LongCount", "total": 17}, + "scanPlanningDuration": {"@class": "io.trino.plugin.base.metrics.DurationTiming", "duration": "3.21s"} +} +``` + +Concrete shapes: + +| Class | JSON shape | Notes | +| ---------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| `LongCount` | `{"total": }` | The canonical counter. Carries `@class` in `/v1/query` output. | +| `DurationTiming` | `{"duration": ""}` | Carries `@class`. | +| `TDigestHistogram` | `{"digest": "", "min", "max", "p01", "p05", "p10", "p25", "p50", "p75", "p90", "p95", "p99", "total"}` | **No `@class`** (type info disabled on the class). Ignore `digest`; the percentiles are pre-computed. | +| `DistributionSnapshot` | `{"total", "min", "max", "p01", "p05", "p10", "p25", "p50", "p75", "p90", "p95", "p99"}` | **No `@class`.** Used for `outputBufferUtilization`, `getSplitDistribution`, pipeline `queuedTime` / `elapsedTime`. | + +The fixed percentile list is **1, 5, 10, 25, 50, 75, 90, 95, 99** — +there is no p99.9. The Web-UI endpoint strips `@class` everywhere and +drops `digest`; DJ's files come from `/v1/query` so expect `@class` on +LongCount/DurationTiming and `digest` present. + +## 3. Enums + +### `QueryState` + +`QUEUED`, `WAITING_FOR_RESOURCES`, `DISPATCHING`, `PLANNING`, +`STARTING`, `RUNNING`, `FINISHING`, then terminal `FINISHED` (success) +or `FAILED`. + +### `StageState` + +`PLANNED`, `SCHEDULING`, `RUNNING`, `PENDING` (all tasks done, more may +be scheduled), terminal `FINISHED`, `ABORTED` (failure elsewhere), +`FAILED` (this stage failed). + +### `TaskState` + +`PLANNED`, `RUNNING`, `FLUSHING` (no more drivers, buffer draining), +`FINISHED`, and the cancel/abort/fail pairs: `CANCELING`/`CANCELED`, +`ABORTING`/`ABORTED`, `FAILING`/`FAILED`. The `-ING` forms are +terminating, not terminal. + +### `BlockedReason` + +Exactly one value exists: `WAITING_FOR_MEMORY`. A driver blocked on +input, exchange, output buffer, or dynamic filters reports an empty +`blockedReasons` set while `blockedWall` / `totalBlockedTime` grows. +This is the most common cause of "blocked time is high but +blockedReasons is empty" confusion — it means I/O or back-pressure, +not memory. + +### `RetryPolicy` + +`NONE`, `TASK` (fault-tolerant execution), `QUERY`. When not `NONE`, a +`FINISHED` query can have `failedTasks > 0` — those tasks were retried +and recovered. + +### `ErrorType` + +`USER_ERROR` (bad SQL, limits set by the user), `INTERNAL_ERROR` +(Trino bug), `INSUFFICIENT_RESOURCES` (memory pool / queue / cluster), +`EXTERNAL` (connector or external system). + +### `QueryType` + +`SELECT`, `EXPLAIN`, `DESCRIBE`, `INSERT`, `UPDATE`, `DELETE`, +`ANALYZE`, `DATA_DEFINITION`, `ALTER_TABLE_EXECUTE`, `MERGE`. + +### `BufferState` (in `outputBuffers.state`) + +`OPEN`, `NO_MORE_BUFFERS`, `NO_MORE_PAGES`, `FLUSHING`, `FINISHED`, +`ABORTED`, `FAILED`. + +## 4. Edge cases that bite + +- `Duration` and `DataSize` fields are always present and non-null, + even on failed queries — zero renders as `"0.00ns"`-style strings or + `"0B"`. +- `progressPercentage` / `runningPercentage` are `null` until + `scheduled == true`; `endTime` is `null` until terminal. +- `pruned == true` queries can have empty stage detail even when + `FINISHED` — don't conclude anything per-stage from a pruned snapshot. +- `failedTasks > 0` does **not** mean the query failed; check `state`. diff --git a/tests/fixtures/manifest.json b/tests/fixtures/manifest.json index 1463813..a722d87 100644 --- a/tests/fixtures/manifest.json +++ b/tests/fixtures/manifest.json @@ -1 +1 @@ -{"metadata":{"dbt_schema_version":"https://schemas.getdbt.com/dbt/manifest/v12.json","dbt_version":"1.10.17","generated_at":"2026-01-17T15:39:33.129685Z","invocation_id":"5666c70e-bedd-439a-b86a-085c601841fa","invocation_started_at":"2026-01-14T05:13:34.064503+00:00","env":{},"project_name":"jaffle_shop","project_id":"06e5b98c2db46f8a72cc4f66410e9b3b","user_id":null,"send_anonymous_usage_stats":false,"adapter_type":"trino","quoting":{"database":false,"schema":false,"identifier":false,"column":null}},"nodes":{"model.jaffle_shop.stg__customers__profiles__clean":{"database":"development","schema":"jaffle_shop_dev_staging","name":"stg__customers__profiles__clean","resource_type":"model","package_name":"jaffle_shop","path":"staging/customers/profiles/stg__customers__profiles__clean.sql","original_file_path":"models/staging/customers/profiles/stg__customers__profiles__clean.sql","unique_id":"model.jaffle_shop.stg__customers__profiles__clean","fqn":["jaffle_shop","staging","customers","profiles","stg__customers__profiles__clean"],"alias":"stg__customers__profiles__clean","checksum":{"name":"sha256","checksum":"287111d3952cfe726dbe9e981ee7081b346589275a6360c596261ca94f51a776"},"config":{"enabled":true,"alias":null,"schema":"staging","database":null,"tags":["json","staging"],"meta":{},"group":"customers","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#B6AB33"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","staging"],"description":"","columns":{"customer_first_name":{"name":"customer_first_name","description":"Customer first name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_id":{"name":"customer_id","description":"Unique identifier for each customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_last_name":{"name":"customer_last_name","description":"Customer last name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"customers","docs":{"show":true,"node_color":"#B6AB33"},"patch_path":"jaffle_shop://models/staging/customers/profiles/stg__customers__profiles__clean.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","schema":"staging","group":"customers","tags":["json","staging"],"docs":{"node_color":"#B6AB33","show":true},"contract":{"enforced":false}},"created_at":1768367615.1287491,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tstg__customers__profiles__clean AS (\n\t\tSELECT\n\t\t\tsplit(TRIM(name), ' ') [1] AS customer_first_name,\n\t\t\tid AS customer_id,\n\t\t\tCASE\n\t\t\t\tWHEN cardinality(split(TRIM(name), ' ')) > 1 THEN split(TRIM(name), ' ') [cardinality(split(TRIM(name), ' '))]\n\t\t\t\tELSE NULL\n\t\t\tEND AS customer_last_name,\n\t\t\tTRIM(name) AS customer_name,\n\t\t\t1 AS portal_source_count\n\t\tFROM\n\t\t\t{{ source('development__jaffle_shop_dev_seeds','raw_customers') }}\n\t)\nSELECT\n\t*\nFROM\n\tstg__customers__profiles__clean","doc_blocks":[],"language":"sql","refs":[],"sources":[["development__jaffle_shop_dev_seeds","raw_customers"]],"metrics":[],"depends_on":{"macros":[],"nodes":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_customers"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.stg__products__catalog__catalog":{"database":"development","schema":"jaffle_shop_dev_staging","name":"stg__products__catalog__catalog","resource_type":"model","package_name":"jaffle_shop","path":"staging/products/catalog/stg__products__catalog__catalog.sql","original_file_path":"models/staging/products/catalog/stg__products__catalog__catalog.sql","unique_id":"model.jaffle_shop.stg__products__catalog__catalog","fqn":["jaffle_shop","staging","products","catalog","stg__products__catalog__catalog"],"alias":"stg__products__catalog__catalog","checksum":{"name":"sha256","checksum":"9dc9de44d2bf73d9d9a961d348ae987846711bc1695ed792141e8aa513fd224a"},"config":{"enabled":true,"alias":null,"schema":"staging","database":null,"tags":["json","staging"],"meta":{},"group":"products","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#B6AB33"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","staging"],"description":"","columns":{"description":{"name":"description","description":"Product description","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_beverage":{"name":"is_beverage","description":"Boolean flag for beverage products","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_jaffle":{"name":"is_jaffle","description":"Boolean flag for jaffle products","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_name":{"name":"product_name","description":"Product name, trimmed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_price_cents":{"name":"product_price_cents","description":"Product price in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_price_dollars":{"name":"product_price_dollars","description":"Product price in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Product SKU identifier","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_type":{"name":"product_type","description":"Product category (jaffle, beverage)","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"products","docs":{"show":true,"node_color":"#B6AB33"},"patch_path":"jaffle_shop://models/staging/products/catalog/stg__products__catalog__catalog.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","schema":"staging","group":"products","tags":["json","staging"],"docs":{"node_color":"#B6AB33","show":true},"contract":{"enforced":false}},"created_at":1768367615.1306171,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tstg__products__catalog__catalog AS (\n\t\tSELECT\n\t\t\tTRIM(description) AS description,\n\t\t\tLOWER(TRIM(TYPE)) = 'beverage' AS is_beverage,\n\t\t\tLOWER(TRIM(TYPE)) = 'jaffle' AS is_jaffle,\n\t\t\t1 AS portal_source_count,\n\t\t\tTRIM(name) AS product_name,\n\t\t\tprice AS product_price_cents,\n\t\t\tCAST(price AS DECIMAL(10, 2)) / 100.0 AS product_price_dollars,\n\t\t\tsku AS product_sku,\n\t\t\tTRIM(TYPE) AS product_type\n\t\tFROM\n\t\t\t{{ source('development__jaffle_shop_dev_seeds','raw_products') }}\n\t)\nSELECT\n\t*\nFROM\n\tstg__products__catalog__catalog","doc_blocks":[],"language":"sql","refs":[],"sources":[["development__jaffle_shop_dev_seeds","raw_products"]],"metrics":[],"depends_on":{"macros":[],"nodes":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_products"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.stg__sales__stores__locations":{"database":"development","schema":"jaffle_shop_dev_staging","name":"stg__sales__stores__locations","resource_type":"model","package_name":"jaffle_shop","path":"staging/sales/stores/stg__sales__stores__locations.sql","original_file_path":"models/staging/sales/stores/stg__sales__stores__locations.sql","unique_id":"model.jaffle_shop.stg__sales__stores__locations","fqn":["jaffle_shop","staging","sales","stores","stg__sales__stores__locations"],"alias":"stg__sales__stores__locations","checksum":{"name":"sha256","checksum":"f540771873516a9be7d0c9255e6cb666d633e2aeceb3236f8f3a00dd7dbc5b40"},"config":{"enabled":true,"alias":null,"schema":"staging","database":null,"tags":["json","staging"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#B6AB33"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","staging"],"description":"","columns":{"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Unique identifier for each store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_opened_at":{"name":"store_opened_at","description":"Timestamp when store opened","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_opened_date":{"name":"store_opened_date","description":"Date when store opened","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_tax_rate":{"name":"store_tax_rate","description":"Local tax rate for the store","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_tax_rate_percent":{"name":"store_tax_rate_percent","description":"Tax rate as percentage","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":"#B6AB33"},"patch_path":"jaffle_shop://models/staging/sales/stores/stg__sales__stores__locations.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","schema":"staging","group":"sales","tags":["json","staging"],"docs":{"node_color":"#B6AB33","show":true},"contract":{"enforced":false}},"created_at":1768367615.131998,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tstg__sales__stores__locations AS (\n\t\tSELECT\n\t\t\t1 AS portal_source_count,\n\t\t\tid AS store_id,\n\t\t\tTRIM(name) AS store_name,\n\t\t\tCAST(opened_at AS TIMESTAMP) AS store_opened_at,\n\t\t\tDATE(CAST(opened_at AS TIMESTAMP)) AS store_opened_date,\n\t\t\ttax_rate AS store_tax_rate,\n\t\t\ttax_rate * 100 AS store_tax_rate_percent\n\t\tFROM\n\t\t\t{{ source('development__jaffle_shop_dev_seeds','raw_stores') }}\n\t)\nSELECT\n\t*\nFROM\n\tstg__sales__stores__locations","doc_blocks":[],"language":"sql","refs":[],"sources":[["development__jaffle_shop_dev_seeds","raw_stores"]],"metrics":[],"depends_on":{"macros":[],"nodes":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_stores"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.stg__sales__orders__standardized":{"database":"development","schema":"jaffle_shop_dev_staging","name":"stg__sales__orders__standardized","resource_type":"model","package_name":"jaffle_shop","path":"staging/sales/orders/stg__sales__orders__standardized.sql","original_file_path":"models/staging/sales/orders/stg__sales__orders__standardized.sql","unique_id":"model.jaffle_shop.stg__sales__orders__standardized","fqn":["jaffle_shop","staging","sales","orders","stg__sales__orders__standardized"],"alias":"stg__sales__orders__standardized","checksum":{"name":"sha256","checksum":"c6f5fb75f6eb8b31fb5d26838e257c14b2330b60f5b4b289a0a16a92330d58f7"},"config":{"enabled":true,"alias":null,"schema":"staging","database":null,"tags":["json","staging"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#B6AB33"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","staging"],"description":"","columns":{"customer_id":{"name":"customer_id","description":"Foreign key to customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"Unique identifier for each order","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_cents":{"name":"order_total_cents","description":"Total order amount in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"ordered_at":{"name":"ordered_at","description":"Timestamp when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"subtotal_cents":{"name":"subtotal_cents","description":"Order subtotal in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"tax_paid_cents":{"name":"tax_paid_cents","description":"Tax amount paid in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":"#B6AB33"},"patch_path":"jaffle_shop://models/staging/sales/orders/stg__sales__orders__standardized.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","schema":"staging","group":"sales","tags":["json","staging"],"docs":{"node_color":"#B6AB33","show":true},"contract":{"enforced":false}},"created_at":1768367615.13364,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tstg__sales__orders__standardized AS (\n\t\tSELECT\n\t\t\tcustomer AS customer_id,\n\t\t\tDATE(CAST(ordered_at AS TIMESTAMP)) AS order_date,\n\t\t\tid AS order_id,\n\t\t\torder_total AS order_total_cents,\n\t\t\tCAST(ordered_at AS TIMESTAMP) AS ordered_at,\n\t\t\t1 AS portal_source_count,\n\t\t\tstore_id AS store_id,\n\t\t\tsubtotal AS subtotal_cents,\n\t\t\ttax_paid AS tax_paid_cents\n\t\tFROM\n\t\t\t{{ source('development__jaffle_shop_dev_seeds','raw_orders') }}\n\t)\nSELECT\n\t*\nFROM\n\tstg__sales__orders__standardized","doc_blocks":[],"language":"sql","refs":[],"sources":[["development__jaffle_shop_dev_seeds","raw_orders"]],"metrics":[],"depends_on":{"macros":[],"nodes":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_orders"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.stg__sales__items__order_details":{"database":"development","schema":"jaffle_shop_dev_staging","name":"stg__sales__items__order_details","resource_type":"model","package_name":"jaffle_shop","path":"staging/sales/items/stg__sales__items__order_details.sql","original_file_path":"models/staging/sales/items/stg__sales__items__order_details.sql","unique_id":"model.jaffle_shop.stg__sales__items__order_details","fqn":["jaffle_shop","staging","sales","items","stg__sales__items__order_details"],"alias":"stg__sales__items__order_details","checksum":{"name":"sha256","checksum":"3e1cb4978cc09f8884d2012ddd69b41ff08fda10376e04b342723e7c8dc4641a"},"config":{"enabled":true,"alias":null,"schema":"staging","database":null,"tags":["json","staging"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#B6AB33"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","staging"],"description":"","columns":{"is_beverage_item":{"name":"is_beverage_item","description":"Boolean flag for beverage items","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_jaffle_item":{"name":"is_jaffle_item","description":"Boolean flag for jaffle items","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"item_id":{"name":"item_id","description":"Unique identifier for each order item","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"Foreign key to orders table","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Product SKU identifier","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_type":{"name":"product_type","description":"Product type derived from SKU pattern","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":"#B6AB33"},"patch_path":"jaffle_shop://models/staging/sales/items/stg__sales__items__order_details.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","schema":"staging","group":"sales","tags":["json","staging"],"docs":{"node_color":"#B6AB33","show":true},"contract":{"enforced":false}},"created_at":1768367615.134757,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tstg__sales__items__order_details AS (\n\t\tSELECT\n\t\t\tsku LIKE 'BEV-%' AS is_beverage_item,\n\t\t\tsku LIKE 'JAF-%' AS is_jaffle_item,\n\t\t\tid AS item_id,\n\t\t\torder_id AS order_id,\n\t\t\t1 AS portal_source_count,\n\t\t\tsku AS product_sku,\n\t\t\tCASE\n\t\t\t\tWHEN sku LIKE 'JAF-%' THEN 'jaffle'\n\t\t\t\tWHEN sku LIKE 'BEV-%' THEN 'beverage'\n\t\t\t\tELSE 'unknown'\n\t\t\tEND AS product_type\n\t\tFROM\n\t\t\t{{ source('development__jaffle_shop_dev_seeds','raw_items') }}\n\t)\nSELECT\n\t*\nFROM\n\tstg__sales__items__order_details","doc_blocks":[],"language":"sql","refs":[],"sources":[["development__jaffle_shop_dev_seeds","raw_items"]],"metrics":[],"depends_on":{"macros":[],"nodes":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_items"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.stg__supply_chain__supplies__inventory":{"database":"development","schema":"jaffle_shop_dev_staging","name":"stg__supply_chain__supplies__inventory","resource_type":"model","package_name":"jaffle_shop","path":"staging/supply_chain/supplies/stg__supply_chain__supplies__inventory.sql","original_file_path":"models/staging/supply_chain/supplies/stg__supply_chain__supplies__inventory.sql","unique_id":"model.jaffle_shop.stg__supply_chain__supplies__inventory","fqn":["jaffle_shop","staging","supply_chain","supplies","stg__supply_chain__supplies__inventory"],"alias":"stg__supply_chain__supplies__inventory","checksum":{"name":"sha256","checksum":"1809b8f0c6f71c29c5300ad12fdacfec24bc22280612dcd2a8ebadc7d3c6f5d6"},"config":{"enabled":true,"alias":null,"schema":"staging","database":null,"tags":["json","staging"],"meta":{},"group":"supply_chain","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#B6AB33"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","staging"],"description":"","columns":{"cost_cents":{"name":"cost_cents","description":"Supply cost in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"cost_dollars":{"name":"cost_dollars","description":"Supply cost in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_perishable":{"name":"is_perishable","description":"Boolean flag for perishable supplies","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Associated product SKU","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_category":{"name":"supply_category","description":"Supply category classification","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_id":{"name":"supply_id","description":"Unique identifier for each supply item","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_name":{"name":"supply_name","description":"Supply item name, trimmed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"supply_chain","docs":{"show":true,"node_color":"#B6AB33"},"patch_path":"jaffle_shop://models/staging/supply_chain/supplies/stg__supply_chain__supplies__inventory.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","schema":"staging","group":"supply_chain","tags":["json","staging"],"docs":{"node_color":"#B6AB33","show":true},"contract":{"enforced":false}},"created_at":1768367615.136171,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tstg__supply_chain__supplies__inventory AS (\n\t\tSELECT\n\t\t\tcost AS cost_cents,\n\t\t\tCAST(cost AS DECIMAL(10, 2)) / 100.0 AS cost_dollars,\n\t\t\tCASE\n\t\t\t\tWHEN perishable = TRUE THEN TRUE\n\t\t\t\tELSE FALSE\n\t\t\tEND AS is_perishable,\n\t\t\t1 AS portal_source_count,\n\t\t\tsku AS product_sku,\n\t\t\tCASE\n\t\t\t\tWHEN LOWER(name) LIKE '%cutlery%'\n\t\t\t\tOR LOWER(name) LIKE '%fork%'\n\t\t\t\tOR LOWER(name) LIKE '%knife%' THEN 'utensils'\n\t\t\t\tWHEN LOWER(name) LIKE '%napkin%'\n\t\t\t\tOR LOWER(name) LIKE '%boat%' THEN 'packaging'\n\t\t\t\tWHEN perishable = TRUE THEN 'ingredients'\n\t\t\t\tELSE 'other'\n\t\t\tEND AS supply_category,\n\t\t\tid AS supply_id,\n\t\t\tTRIM(name) AS supply_name\n\t\tFROM\n\t\t\t{{ source('development__jaffle_shop_dev_seeds','raw_supplies') }}\n\t)\nSELECT\n\t*\nFROM\n\tstg__supply_chain__supplies__inventory","doc_blocks":[],"language":"sql","refs":[],"sources":[["development__jaffle_shop_dev_seeds","raw_supplies"]],"metrics":[],"depends_on":{"macros":[],"nodes":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_supplies"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.mart__customers__dashboard__analytics":{"database":"development","schema":"jaffle_shop_dev","name":"mart__customers__dashboard__analytics","resource_type":"model","package_name":"jaffle_shop","path":"marts/customers/dashboard/mart__customers__dashboard__analytics.sql","original_file_path":"models/marts/customers/dashboard/mart__customers__dashboard__analytics.sql","unique_id":"model.jaffle_shop.mart__customers__dashboard__analytics","fqn":["jaffle_shop","marts","customers","dashboard","mart__customers__dashboard__analytics"],"alias":"mart__customers__dashboard__analytics","checksum":{"name":"sha256","checksum":"d74eb1171643e36fdf8303e9d0ede6fde081029d1b3378551e46c2b5d261f72a"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["json","lightdash-explore","mart"],"meta":{},"group":"customers","materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#059669"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","lightdash-explore","mart"],"description":"","columns":{"customer_first_name":{"name":"customer_first_name","description":"Customer first name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer First Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer First Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_id":{"name":"customer_id","description":"Unique identifier for each customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Id"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Id"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_last_name":{"name":"customer_last_name","description":"Customer last name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Last Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Last Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_segment":{"name":"customer_segment","description":"Customer segment classification based on order count","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__customers__dashboard__analytics"},"dimension":{"type":"string","label":"Customer Segment"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__customers__dashboard__analytics"},"dimension":{"type":"string","label":"Customer Segment"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"has_last_name":{"name":"has_last_name","description":"Whether customer provided a last name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"},"dimension":{"type":"string","label":"Has Last Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"},"dimension":{"type":"string","label":"Has Last Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_orders":{"name":"total_orders","description":"Total number of orders placed by customer","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"},"dimension":{"type":"string","label":"Total Orders","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"},"dimension":{"type":"string","label":"Total Orders","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"customers","docs":{"show":true,"node_color":"#059669"},"patch_path":"jaffle_shop://models/marts/customers/dashboard/mart__customers__dashboard__analytics.yml","build_path":null,"unrendered_config":{"materialized":"view","group":"customers","tags":["json","lightdash-explore","mart"],"docs":{"node_color":"#059669","show":true},"contract":{"enforced":false}},"created_at":1768367615.138334,"relation_name":"development.jaffle_shop_dev.mart__customers__dashboard__analytics","raw_code":"{{\n config(\n materialized=\"view\"\n )\n}}\n\nWITH\n\tmart__customers__dashboard__analytics AS (\n\t\tSELECT\n\t\t\tcustomer_first_name,\n\t\t\tcustomer_id,\n\t\t\tcustomer_last_name,\n\t\t\tcustomer_name,\n\t\t\tCASE\n\t\t\t\tWHEN total_orders >= 10 THEN 'VIP'\n\t\t\t\tWHEN total_orders >= 3 THEN 'Regular'\n\t\t\t\tELSE 'New'\n\t\t\tEND AS customer_segment,\n\t\t\thas_last_name,\n\t\t\tportal_source_count,\n\t\t\ttotal_orders\n\t\tFROM\n\t\t\t{{ ref('int__customers__profiles__summary') }}\n\t)\nSELECT\n\t*\nFROM\n\tmart__customers__dashboard__analytics","doc_blocks":[],"language":"sql","refs":[{"name":"int__customers__profiles__summary","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__customers__profiles__summary"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.mart__products__reporting__cost_efficiency":{"database":"development","schema":"jaffle_shop_dev","name":"mart__products__reporting__cost_efficiency","resource_type":"model","package_name":"jaffle_shop","path":"marts/products/reporting/mart__products__reporting__cost_efficiency.sql","original_file_path":"models/marts/products/reporting/mart__products__reporting__cost_efficiency.sql","unique_id":"model.jaffle_shop.mart__products__reporting__cost_efficiency","fqn":["jaffle_shop","marts","products","reporting","mart__products__reporting__cost_efficiency"],"alias":"mart__products__reporting__cost_efficiency","checksum":{"name":"sha256","checksum":"d36e0bd5cc45bd1e8d9636dfaa9b1fa96cc06b5612eaba5f658f3ddbcb9e3b49"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["json","lightdash-explore","mart"],"meta":{},"group":"products","materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#059669"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","lightdash-explore","mart"],"description":"","columns":{"cost_efficiency_category":{"name":"cost_efficiency_category","description":"Cost efficiency classification","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Cost Efficiency Category"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Cost Efficiency Category"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"cost_to_price_ratio":{"name":"cost_to_price_ratio","description":"Individual supply cost as ratio of selling price","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Cost To Price Ratio","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Cost To Price Ratio","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_perishable_supply":{"name":"is_perishable_supply","description":"Whether this supply is perishable","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Is Perishable Supply"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Is Perishable Supply"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"perishable_risk_level":{"name":"perishable_risk_level","description":"Risk level based on perishable supplies","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Perishable Risk Level"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Perishable Risk Level"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_name":{"name":"product_name","description":"Product name, trimmed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_price_dollars":{"name":"product_price_dollars","description":"Product price in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Price Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Price Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Associated product SKU","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"},"dimension":{"type":"string","label":"Product Sku"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"},"dimension":{"type":"string","label":"Product Sku"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_type":{"name":"product_type","description":"Product category (jaffle, beverage)","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Type"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Type"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_cost_dollars":{"name":"supply_cost_dollars","description":"Individual supply cost in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Supply Cost Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Supply Cost Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"products","docs":{"show":true,"node_color":"#059669"},"patch_path":"jaffle_shop://models/marts/products/reporting/mart__products__reporting__cost_efficiency.yml","build_path":null,"unrendered_config":{"materialized":"view","group":"products","tags":["json","lightdash-explore","mart"],"docs":{"node_color":"#059669","show":true},"contract":{"enforced":false}},"created_at":1768367615.140745,"relation_name":"development.jaffle_shop_dev.mart__products__reporting__cost_efficiency","raw_code":"{{\n config(\n materialized=\"view\"\n )\n}}\n\nWITH\n\tmart__products__reporting__cost_efficiency AS (\n\t\tSELECT\n\t\t\tCASE\n\t\t\t\tWHEN cost_dollars / product_price_dollars <= 0.30 THEN 'Highly Efficient'\n\t\t\t\tWHEN cost_dollars / product_price_dollars <= 0.50 THEN 'Efficient'\n\t\t\t\tWHEN cost_dollars / product_price_dollars <= 0.70 THEN 'Moderate'\n\t\t\t\tELSE 'Low Efficiency'\n\t\t\tEND AS cost_efficiency_category,\n\t\t\tCASE\n\t\t\t\tWHEN product_price_dollars > 0 THEN cost_dollars / product_price_dollars\n\t\t\t\tELSE NULL\n\t\t\tEND AS cost_to_price_ratio,\n\t\t\tis_perishable AS is_perishable_supply,\n\t\t\tCASE\n\t\t\t\tWHEN is_perishable THEN 'High Risk'\n\t\t\t\tELSE 'Low Risk'\n\t\t\tEND AS perishable_risk_level,\n\t\t\tportal_source_count,\n\t\t\tproduct_name,\n\t\t\tproduct_price_dollars,\n\t\t\tproduct_sku,\n\t\t\tproduct_type,\n\t\t\tcost_dollars AS supply_cost_dollars\n\t\tFROM\n\t\t\t{{ ref('int__supply_chain__supplies__cost_analysis') }}\n\t)\nSELECT\n\t*\nFROM\n\tmart__products__reporting__cost_efficiency","doc_blocks":[],"language":"sql","refs":[{"name":"int__supply_chain__supplies__cost_analysis","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__supply_chain__supplies__cost_analysis"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.mart__products__reporting__menu_analytics":{"database":"development","schema":"jaffle_shop_dev","name":"mart__products__reporting__menu_analytics","resource_type":"model","package_name":"jaffle_shop","path":"marts/products/reporting/mart__products__reporting__menu_analytics.sql","original_file_path":"models/marts/products/reporting/mart__products__reporting__menu_analytics.sql","unique_id":"model.jaffle_shop.mart__products__reporting__menu_analytics","fqn":["jaffle_shop","marts","products","reporting","mart__products__reporting__menu_analytics"],"alias":"mart__products__reporting__menu_analytics","checksum":{"name":"sha256","checksum":"a0f496985a432632cb87c7fbac4a525d8c9916d1ca1d201849f517601fb4e0fc"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["json","lightdash-explore","mart"],"meta":{},"group":"products","materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#059669"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","lightdash-explore","mart"],"description":"","columns":{"item_id":{"name":"item_id","description":"Unique identifier for each order item","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Item Id"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Item Id"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Product SKU identifier","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Sku"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Sku"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_type":{"name":"product_type","description":"Product type derived from SKU pattern","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Type"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Type"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_items_sold":{"name":"total_items_sold","description":"Total quantity of this product sold","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Items Sold","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Items Sold","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_orders":{"name":"total_orders","description":"Number of unique orders containing this product","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Orders","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Orders","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"products","docs":{"show":true,"node_color":"#059669"},"patch_path":"jaffle_shop://models/marts/products/reporting/mart__products__reporting__menu_analytics.yml","build_path":null,"unrendered_config":{"materialized":"view","group":"products","tags":["json","lightdash-explore","mart"],"docs":{"node_color":"#059669","show":true},"contract":{"enforced":false}},"created_at":1768367615.141846,"relation_name":"development.jaffle_shop_dev.mart__products__reporting__menu_analytics","raw_code":"{{\n config(\n materialized=\"view\"\n )\n}}\n\nWITH\n\tmart__products__reporting__menu_analytics AS (\n\t\tSELECT\n\t\t\titem_id,\n\t\t\tportal_source_count,\n\t\t\tproduct_sku,\n\t\t\tproduct_type,\n\t\t\ttotal_items_sold,\n\t\t\ttotal_orders\n\t\tFROM\n\t\t\t{{ ref('int__products__analytics__product_popularity') }}\n\t)\nSELECT\n\t*\nFROM\n\tmart__products__reporting__menu_analytics","doc_blocks":[],"language":"sql","refs":[{"name":"int__products__analytics__product_popularity","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__products__analytics__product_popularity"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.mart__sales__reporting__revenue":{"database":"development","schema":"jaffle_shop_dev","name":"mart__sales__reporting__revenue","resource_type":"model","package_name":"jaffle_shop","path":"marts/sales/reporting/mart__sales__reporting__revenue.sql","original_file_path":"models/marts/sales/reporting/mart__sales__reporting__revenue.sql","unique_id":"model.jaffle_shop.mart__sales__reporting__revenue","fqn":["jaffle_shop","marts","sales","reporting","mart__sales__reporting__revenue"],"alias":"mart__sales__reporting__revenue","checksum":{"name":"sha256","checksum":"228949d9803f147afdcc929fe95d006fd04b9d4d4464f23ecdf458fcc9c16750"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["json","lightdash-explore","mart"],"meta":{},"group":"sales","materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#059669"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","lightdash-explore","mart"],"description":"","columns":{"customer_id":{"name":"customer_id","description":"Foreign key to customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Customer Id"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Customer Id"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_weekend_order":{"name":"is_weekend_order","description":"Boolean flag for weekend orders","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Is Weekend Order"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Is Weekend Order"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Order Date"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Order Date"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"Unique identifier for each order","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Order Id"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Order Id"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_month":{"name":"order_month","description":"Order month","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Order Month"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Order Month"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_quarter":{"name":"order_quarter","description":"Order quarter","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Order Quarter"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Order Quarter"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_year":{"name":"order_year","description":"Order year","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Order Year"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Order Year"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"revenue_tier":{"name":"revenue_tier","description":"Revenue tier classification","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Revenue Tier"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Revenue Tier"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Store Id"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Store Id"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"},"dimension":{"type":"string","label":"Store Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"},"dimension":{"type":"string","label":"Store Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"subtotal_dollars":{"name":"subtotal_dollars","description":"Order subtotal in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Subtotal Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Subtotal Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"tax_dollars":{"name":"tax_dollars","description":"Tax amount in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Tax Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Tax Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_dollars":{"name":"total_dollars","description":"Total order amount in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Total Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Total Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":"#059669"},"patch_path":"jaffle_shop://models/marts/sales/reporting/mart__sales__reporting__revenue.yml","build_path":null,"unrendered_config":{"materialized":"view","group":"sales","tags":["json","lightdash-explore","mart"],"docs":{"node_color":"#059669","show":true},"contract":{"enforced":false}},"created_at":1768367615.144565,"relation_name":"development.jaffle_shop_dev.mart__sales__reporting__revenue","raw_code":"{{\n config(\n materialized=\"view\"\n )\n}}\n\nWITH\n\tmart__sales__reporting__revenue AS (\n\t\tSELECT\n\t\t\tcustomer_id,\n\t\t\tcustomer_name,\n\t\t\tday_of_week(order_date) IN (6, 7) AS is_weekend_order,\n\t\t\torder_date,\n\t\t\torder_id,\n\t\t\tEXTRACT(\n\t\t\t\tMONTH\n\t\t\t\tFROM\n\t\t\t\t\torder_date\n\t\t\t) AS order_month,\n\t\t\tEXTRACT(\n\t\t\t\tQUARTER\n\t\t\t\tFROM\n\t\t\t\t\torder_date\n\t\t\t) AS order_quarter,\n\t\t\tEXTRACT(\n\t\t\t\tYEAR\n\t\t\t\tFROM\n\t\t\t\t\torder_date\n\t\t\t) AS order_year,\n\t\t\tportal_source_count,\n\t\t\tCASE\n\t\t\t\tWHEN order_total_dollars >= 15.00 THEN 'High'\n\t\t\t\tWHEN order_total_dollars >= 8.00 THEN 'Medium'\n\t\t\t\tELSE 'Low'\n\t\t\tEND AS revenue_tier,\n\t\t\tstore_id,\n\t\t\tstore_name,\n\t\t\tCAST(subtotal_cents AS DECIMAL(10, 2)) / 100.0 AS subtotal_dollars,\n\t\t\tCAST(tax_paid_cents AS DECIMAL(10, 2)) / 100.0 AS tax_dollars,\n\t\t\torder_total_dollars AS total_dollars\n\t\tFROM\n\t\t\t{{ ref('int__sales__orders__enriched') }}\n\t)\nSELECT\n\t*\nFROM\n\tmart__sales__reporting__revenue","doc_blocks":[],"language":"sql","refs":[{"name":"int__sales__orders__enriched","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__sales__orders__enriched"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.mart__sales__reporting__profitability":{"database":"development","schema":"jaffle_shop_dev","name":"mart__sales__reporting__profitability","resource_type":"model","package_name":"jaffle_shop","path":"marts/sales/reporting/mart__sales__reporting__profitability.sql","original_file_path":"models/marts/sales/reporting/mart__sales__reporting__profitability.sql","unique_id":"model.jaffle_shop.mart__sales__reporting__profitability","fqn":["jaffle_shop","marts","sales","reporting","mart__sales__reporting__profitability"],"alias":"mart__sales__reporting__profitability","checksum":{"name":"sha256","checksum":"9e8829af38433ae219ddae8f68a8a2b4b976f679185a9ddb8b4666f440732cea"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["json","lightdash-explore","mart"],"meta":{},"group":"sales","materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#059669"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","lightdash-explore","mart"],"description":"","columns":{"avg_order_value_dollars":{"name":"avg_order_value_dollars","description":"Average order value in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Avg Order Value Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Avg Order Value Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_count":{"name":"order_count","description":"Number of orders","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Order Count","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Order Count","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Order Date"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Order Date"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_year_month":{"name":"order_year_month","description":"Order year-month","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Order Year Month"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Order Year Month"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"revenue_per_day":{"name":"revenue_per_day","description":"Daily revenue per store","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Revenue Per Day","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Revenue Per Day","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Store Id"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Store Id"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"},"dimension":{"type":"string","label":"Store Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"},"dimension":{"type":"string","label":"Store Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_revenue_dollars":{"name":"total_revenue_dollars","description":"Total revenue in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Total Revenue Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Total Revenue Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_tax_collected_dollars":{"name":"total_tax_collected_dollars","description":"Total tax collected in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Total Tax Collected Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Total Tax Collected Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":"#059669"},"patch_path":"jaffle_shop://models/marts/sales/reporting/mart__sales__reporting__profitability.yml","build_path":null,"unrendered_config":{"materialized":"view","group":"sales","tags":["json","lightdash-explore","mart"],"docs":{"node_color":"#059669","show":true},"contract":{"enforced":false}},"created_at":1768367615.146687,"relation_name":"development.jaffle_shop_dev.mart__sales__reporting__profitability","raw_code":"{{\n config(\n materialized=\"view\"\n )\n}}\n\nWITH\n\tmart__sales__reporting__profitability AS (\n\t\tSELECT\n\t\t\tAVG(order_total_dollars) AS avg_order_value_dollars,\n\t\t\tCOUNT(*) AS order_count,\n\t\t\torder_date,\n\t\t\tDATE_TRUNC('month', order_date) AS order_year_month,\n\t\t\tsum(portal_source_count) AS portal_source_count,\n\t\t\tSUM(order_total_dollars) AS revenue_per_day,\n\t\t\tstore_id,\n\t\t\tstore_name,\n\t\t\tSUM(order_total_dollars) AS total_revenue_dollars,\n\t\t\tSUM(CAST(tax_paid_cents AS DECIMAL(10, 2)) / 100.0) AS total_tax_collected_dollars\n\t\tFROM\n\t\t\t{{ ref('int__sales__orders__enriched') }}\n\t\tGROUP BY\n\t\t\torder_date,\n\t\t\tDATE_TRUNC('month', order_date),\n\t\t\tstore_id,\n\t\t\tstore_name\n\t)\nSELECT\n\t*\nFROM\n\tmart__sales__reporting__profitability","doc_blocks":[],"language":"sql","refs":[{"name":"int__sales__orders__enriched","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__sales__orders__enriched"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics":{"database":"development","schema":"jaffle_shop_dev","name":"mart__analytics__dashboard__comprehensive_analytics","resource_type":"model","package_name":"jaffle_shop","path":"marts/analytics/dashboard/mart__analytics__dashboard__comprehensive_analytics.sql","original_file_path":"models/marts/analytics/dashboard/mart__analytics__dashboard__comprehensive_analytics.sql","unique_id":"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics","fqn":["jaffle_shop","marts","analytics","dashboard","mart__analytics__dashboard__comprehensive_analytics"],"alias":"mart__analytics__dashboard__comprehensive_analytics","checksum":{"name":"sha256","checksum":"f632e9b79f77dd90892f2e965d5670ebb1d2c56d54de6185c71b086cc4673837"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["json","lightdash-explore","mart"],"meta":{},"group":"analytics","materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#059669"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","lightdash-explore","mart"],"description":"","columns":{"business_impact_score":{"name":"business_impact_score","description":"Business impact score based on cost efficiency (0-100)","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics"},"dimension":{"type":"string","label":"Business Impact Score","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics"},"dimension":{"type":"string","label":"Business Impact Score","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"cost_to_price_ratio":{"name":"cost_to_price_ratio","description":"Individual supply cost as ratio of selling price","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Cost To Price Ratio","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Cost To Price Ratio","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_perishable_supply":{"name":"is_perishable_supply","description":"Whether this supply is perishable","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Is Perishable Supply"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Is Perishable Supply"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_name":{"name":"product_name","description":"Product name, trimmed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_price_dollars":{"name":"product_price_dollars","description":"Product price in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Price Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Price Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Product SKU identifier","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Sku"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Sku"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_type":{"name":"product_type","description":"Product type derived from SKU pattern","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Type"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Type"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"strategic_recommendation":{"name":"strategic_recommendation","description":"Strategic business recommendation","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics"},"dimension":{"type":"string","label":"Strategic Recommendation"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics"},"dimension":{"type":"string","label":"Strategic Recommendation"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_items_sold":{"name":"total_items_sold","description":"Total quantity of this product sold","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Items Sold","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Items Sold","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_orders":{"name":"total_orders","description":"Number of unique orders containing this product","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Orders","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Orders","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"analytics","docs":{"show":true,"node_color":"#059669"},"patch_path":"jaffle_shop://models/marts/analytics/dashboard/mart__analytics__dashboard__comprehensive_analytics.yml","build_path":null,"unrendered_config":{"materialized":"view","group":"analytics","tags":["json","lightdash-explore","mart"],"docs":{"node_color":"#059669","show":true},"contract":{"enforced":false}},"created_at":1768367615.148032,"relation_name":"development.jaffle_shop_dev.mart__analytics__dashboard__comprehensive_analytics","raw_code":"{{\n config(\n materialized=\"view\"\n )\n}}\n\nWITH\n\tmart__analytics__dashboard__comprehensive_analytics AS (\n\t\tSELECT\n\t\t\tCASE\n\t\t\t\tWHEN mart__products__reporting__cost_efficiency.cost_efficiency_category = 'Highly Efficient' THEN 100\n\t\t\t\tWHEN mart__products__reporting__cost_efficiency.cost_efficiency_category = 'Efficient' THEN 85\n\t\t\t\tWHEN mart__products__reporting__cost_efficiency.cost_efficiency_category = 'Moderate' THEN 70\n\t\t\t\tELSE 50\n\t\t\tEND AS business_impact_score,\n\t\t\tmart__products__reporting__cost_efficiency.cost_to_price_ratio,\n\t\t\tmart__products__reporting__cost_efficiency.is_perishable_supply,\n\t\t\tmart__products__reporting__cost_efficiency.portal_source_count,\n\t\t\tmart__products__reporting__cost_efficiency.product_name,\n\t\t\tmart__products__reporting__cost_efficiency.product_price_dollars,\n\t\t\tmart__products__reporting__menu_analytics.product_sku,\n\t\t\tmart__products__reporting__menu_analytics.product_type,\n\t\t\tCASE\n\t\t\t\tWHEN mart__products__reporting__cost_efficiency.cost_efficiency_category = 'Highly Efficient' THEN 'Promote & Expand'\n\t\t\t\tWHEN mart__products__reporting__cost_efficiency.cost_efficiency_category = 'Efficient' THEN 'Monitor Performance'\n\t\t\t\tWHEN mart__products__reporting__cost_efficiency.cost_efficiency_category = 'Moderate' THEN 'Optimize Costs'\n\t\t\t\tELSE 'Review Strategy'\n\t\t\tEND AS strategic_recommendation,\n\t\t\tmart__products__reporting__menu_analytics.total_items_sold,\n\t\t\tmart__products__reporting__menu_analytics.total_orders\n\t\tFROM\n\t\t\t{{ ref('mart__products__reporting__menu_analytics') }} mart__products__reporting__menu_analytics\n\t\t\tLEFT JOIN {{ ref('mart__products__reporting__cost_efficiency') }} mart__products__reporting__cost_efficiency ON mart__products__reporting__menu_analytics.product_sku = mart__products__reporting__cost_efficiency.product_sku\n\t)\nSELECT\n\t*\nFROM\n\tmart__analytics__dashboard__comprehensive_analytics","doc_blocks":[],"language":"sql","refs":[{"name":"mart__products__reporting__menu_analytics","package":null,"version":null},{"name":"mart__products__reporting__cost_efficiency","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.mart__products__reporting__menu_analytics","model.jaffle_shop.mart__products__reporting__cost_efficiency"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.int__customers__profiles__summary":{"database":"development","schema":"jaffle_shop_dev","name":"int__customers__profiles__summary","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/customers/profiles/int__customers__profiles__summary.sql","original_file_path":"models/intermediate/customers/profiles/int__customers__profiles__summary.sql","unique_id":"model.jaffle_shop.int__customers__profiles__summary","fqn":["jaffle_shop","intermediate","customers","profiles","int__customers__profiles__summary"],"alias":"int__customers__profiles__summary","checksum":{"name":"sha256","checksum":"41250064d49f00fabea3a0c5ec7b604be27f73251cb9ef0a86f9950979f380a5"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"customers","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#DAA520"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"","columns":{"customer_first_name":{"name":"customer_first_name","description":"Customer first name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_id":{"name":"customer_id","description":"Unique identifier for each customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_last_name":{"name":"customer_last_name","description":"Customer last name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_segment":{"name":"customer_segment","description":"Customer segment classification based on order count","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"has_last_name":{"name":"has_last_name","description":"Whether customer provided a last name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_orders":{"name":"total_orders","description":"Total number of orders placed by customer","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"customers","docs":{"show":true,"node_color":"#DAA520"},"patch_path":"jaffle_shop://models/intermediate/customers/profiles/int__customers__profiles__summary.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"customers","tags":["intermediate","json"],"docs":{"node_color":"#DAA520","show":true},"contract":{"enforced":false}},"created_at":1768367615.1850848,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__customers__profiles__summary AS (\n\t\tSELECT\n\t\t\tstg__customers__profiles__clean.customer_first_name,\n\t\t\tstg__customers__profiles__clean.customer_id,\n\t\t\tstg__customers__profiles__clean.customer_last_name,\n\t\t\tstg__customers__profiles__clean.customer_name,\n\t\t\t{{ customer_segment() }} AS customer_segment,\n\t\t\tstg__customers__profiles__clean.customer_last_name IS NOT NULL AS has_last_name,\n\t\t\tsum(\n\t\t\t\tstg__customers__profiles__clean.portal_source_count\n\t\t\t) AS portal_source_count,\n\t\t\tCOUNT(stg__sales__orders__standardized.order_id) AS total_orders\n\t\tFROM\n\t\t\t{{ ref('stg__customers__profiles__clean') }} stg__customers__profiles__clean\n\t\t\tLEFT JOIN {{ ref('stg__sales__orders__standardized') }} stg__sales__orders__standardized ON stg__customers__profiles__clean.customer_id = stg__sales__orders__standardized.customer_id\n\t\tGROUP BY\n\t\t\tstg__customers__profiles__clean.customer_first_name,\n\t\t\tstg__customers__profiles__clean.customer_id,\n\t\t\tstg__customers__profiles__clean.customer_last_name,\n\t\t\tstg__customers__profiles__clean.customer_name\n\t)\nSELECT\n\t*\nFROM\n\tint__customers__profiles__summary","doc_blocks":[],"language":"sql","refs":[{"name":"stg__customers__profiles__clean","package":null,"version":null},{"name":"stg__sales__orders__standardized","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":["macro.jaffle_shop.customer_segment"],"nodes":["model.jaffle_shop.stg__customers__profiles__clean","model.jaffle_shop.stg__sales__orders__standardized"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.int__products__analytics__product_popularity":{"database":"development","schema":"jaffle_shop_dev","name":"int__products__analytics__product_popularity","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/products/analytics/int__products__analytics__product_popularity.sql","original_file_path":"models/intermediate/products/analytics/int__products__analytics__product_popularity.sql","unique_id":"model.jaffle_shop.int__products__analytics__product_popularity","fqn":["jaffle_shop","intermediate","products","analytics","int__products__analytics__product_popularity"],"alias":"int__products__analytics__product_popularity","checksum":{"name":"sha256","checksum":"49fea0d39aaeabc54f366cd5764defac0160afc7e2574c87afef171435a4903f"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"products","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#DAA520"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"","columns":{"item_id":{"name":"item_id","description":"Unique identifier for each order item","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Product SKU identifier","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_type":{"name":"product_type","description":"Product type derived from SKU pattern","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_items_sold":{"name":"total_items_sold","description":"Total quantity of this product sold","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_orders":{"name":"total_orders","description":"Number of unique orders containing this product","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"products","docs":{"show":true,"node_color":"#DAA520"},"patch_path":"jaffle_shop://models/intermediate/products/analytics/int__products__analytics__product_popularity.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"products","tags":["intermediate","json"],"docs":{"node_color":"#DAA520","show":true},"contract":{"enforced":false}},"created_at":1768367615.185868,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__products__analytics__product_popularity AS (\n\t\tSELECT\n\t\t\titem_id,\n\t\t\tsum(portal_source_count) AS portal_source_count,\n\t\t\tproduct_sku,\n\t\t\tproduct_type,\n\t\t\tCOUNT(*) AS total_items_sold,\n\t\t\tCOUNT(DISTINCT order_id) AS total_orders\n\t\tFROM\n\t\t\t{{ ref('stg__sales__items__order_details') }}\n\t\tGROUP BY\n\t\t\titem_id,\n\t\t\tproduct_sku,\n\t\t\tproduct_type\n\t)\nSELECT\n\t*\nFROM\n\tint__products__analytics__product_popularity","doc_blocks":[],"language":"sql","refs":[{"name":"stg__sales__items__order_details","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.stg__sales__items__order_details"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.int__sales__orders__enriched":{"database":"development","schema":"jaffle_shop_dev","name":"int__sales__orders__enriched","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/sales/orders/int__sales__orders__enriched.sql","original_file_path":"models/intermediate/sales/orders/int__sales__orders__enriched.sql","unique_id":"model.jaffle_shop.int__sales__orders__enriched","fqn":["jaffle_shop","intermediate","sales","orders","int__sales__orders__enriched"],"alias":"int__sales__orders__enriched","checksum":{"name":"sha256","checksum":"8caf6828d75ffc47b0d22853b7c3ba56d8b52bc3143ca54ca12bc249c6d068eb"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#DAA520"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"","columns":{"customer_first_name":{"name":"customer_first_name","description":"Customer first name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_id":{"name":"customer_id","description":"Foreign key to customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"Unique identifier for each order","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_cents":{"name":"order_total_cents","description":"Total order amount in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_dollars":{"name":"order_total_dollars","description":"Order total in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"ordered_at":{"name":"ordered_at","description":"Timestamp when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_tax_rate":{"name":"store_tax_rate","description":"Local tax rate for the store","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"subtotal_cents":{"name":"subtotal_cents","description":"Order subtotal in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"tax_paid_cents":{"name":"tax_paid_cents","description":"Tax amount paid in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":"#DAA520"},"patch_path":"jaffle_shop://models/intermediate/sales/orders/int__sales__orders__enriched.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"sales","tags":["intermediate","json"],"docs":{"node_color":"#DAA520","show":true},"contract":{"enforced":false}},"created_at":1768367615.186986,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__sales__orders__enriched AS (\n\t\tSELECT\n\t\t\tstg__customers__profiles__clean.customer_first_name,\n\t\t\tstg__sales__orders__standardized.customer_id,\n\t\t\tstg__customers__profiles__clean.customer_name,\n\t\t\tstg__sales__orders__standardized.order_date,\n\t\t\tstg__sales__orders__standardized.order_id,\n\t\t\tstg__sales__orders__standardized.order_total_cents,\n\t\t\tCAST(\n\t\t\t\tstg__sales__orders__standardized.order_total_cents AS DECIMAL(10, 2)\n\t\t\t) / 100.0 AS order_total_dollars,\n\t\t\tstg__sales__orders__standardized.ordered_at,\n\t\t\tstg__sales__orders__standardized.portal_source_count,\n\t\t\tstg__sales__orders__standardized.store_id,\n\t\t\tstg__sales__stores__locations.store_name,\n\t\t\tstg__sales__stores__locations.store_tax_rate,\n\t\t\tstg__sales__orders__standardized.subtotal_cents,\n\t\t\tstg__sales__orders__standardized.tax_paid_cents\n\t\tFROM\n\t\t\t{{ ref('stg__sales__orders__standardized') }} stg__sales__orders__standardized\n\t\t\tLEFT JOIN {{ ref('stg__customers__profiles__clean') }} stg__customers__profiles__clean ON stg__sales__orders__standardized.customer_id = stg__customers__profiles__clean.customer_id\n\t\t\tLEFT JOIN {{ ref('stg__sales__stores__locations') }} stg__sales__stores__locations ON stg__sales__orders__standardized.store_id = stg__sales__stores__locations.store_id\n\t)\nSELECT\n\t*\nFROM\n\tint__sales__orders__enriched","doc_blocks":[],"language":"sql","refs":[{"name":"stg__sales__orders__standardized","package":null,"version":null},{"name":"stg__customers__profiles__clean","package":null,"version":null},{"name":"stg__sales__stores__locations","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.stg__sales__orders__standardized","model.jaffle_shop.stg__customers__profiles__clean","model.jaffle_shop.stg__sales__stores__locations"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.int__sales__orders__regional_combined":{"database":"development","schema":"jaffle_shop_dev","name":"int__sales__orders__regional_combined","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/sales/orders/int__sales__orders__regional_combined.sql","original_file_path":"models/intermediate/sales/orders/int__sales__orders__regional_combined.sql","unique_id":"model.jaffle_shop.int__sales__orders__regional_combined","fqn":["jaffle_shop","intermediate","sales","orders","int__sales__orders__regional_combined"],"alias":"int__sales__orders__regional_combined","checksum":{"name":"sha256","checksum":"b988eded0f7a248aabaa73ad50962b0a8b6db43ea229e52a98e8232c49dfeeb5"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"Union of orders with regional classification for consolidated reporting across all stores","columns":{"customer_first_name":{"name":"customer_first_name","description":"Customer first name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_id":{"name":"customer_id","description":"Foreign key to customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"Unique identifier for each order","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_cents":{"name":"order_total_cents","description":"Total order amount in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_dollars":{"name":"order_total_dollars","description":"Order total in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"ordered_at":{"name":"ordered_at","description":"Timestamp when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"region":{"name":"region","description":"Geographic region classification","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__sales__orders__regional_combined"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__sales__orders__regional_combined"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_tax_rate":{"name":"store_tax_rate","description":"Local tax rate for the store","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"subtotal_cents":{"name":"subtotal_cents","description":"Order subtotal in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"tax_paid_cents":{"name":"tax_paid_cents","description":"Tax amount paid in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":null},"patch_path":"jaffle_shop://models/intermediate/sales/orders/int__sales__orders__regional_combined.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"sales","tags":["intermediate","json"],"docs":{"show":true},"contract":{"enforced":false}},"created_at":1768367615.188111,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__sales__orders__regional_combined AS (\n\t\tSELECT\n\t\t\tcustomer_first_name,\n\t\t\tcustomer_id,\n\t\t\tcustomer_name,\n\t\t\torder_date,\n\t\t\torder_id,\n\t\t\torder_total_cents,\n\t\t\torder_total_dollars,\n\t\t\tordered_at,\n\t\t\tportal_source_count,\n\t\t\tCASE\n\t\t\t\tWHEN store_name IN ('Philadelphia', 'Brooklyn') THEN 'East Coast'\n\t\t\t\tWHEN store_name IN ('San Francisco', 'Los Angeles') THEN 'West Coast'\n\t\t\t\tELSE 'Other'\n\t\t\tEND AS region,\n\t\t\tstore_id,\n\t\t\tstore_name,\n\t\t\tstore_tax_rate,\n\t\t\tsubtotal_cents,\n\t\t\ttax_paid_cents\n\t\tFROM\n\t\t\t{{ ref('int__sales__orders__enriched') }}\n\t\tUNION ALL\n\t\tSELECT\n\t\t\tcustomer_first_name,\n\t\t\tcustomer_id,\n\t\t\tcustomer_name,\n\t\t\torder_date,\n\t\t\torder_id,\n\t\t\torder_total_cents,\n\t\t\torder_total_dollars,\n\t\t\tordered_at,\n\t\t\tportal_source_count,\n\t\t\tCASE\n\t\t\t\tWHEN store_name IN ('Philadelphia', 'Brooklyn') THEN 'East Coast'\n\t\t\t\tWHEN store_name IN ('San Francisco', 'Los Angeles') THEN 'West Coast'\n\t\t\t\tELSE 'Other'\n\t\t\tEND AS region,\n\t\t\tstore_id,\n\t\t\tstore_name,\n\t\t\tstore_tax_rate,\n\t\t\tsubtotal_cents,\n\t\t\ttax_paid_cents\n\t\tFROM\n\t\t\t{{ ref('int__sales__orders__enriched') }}\n\t)\nSELECT\n\t*\nFROM\n\tint__sales__orders__regional_combined","doc_blocks":[],"language":"sql","refs":[{"name":"int__sales__orders__enriched","package":null,"version":null},{"name":"int__sales__orders__enriched","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__sales__orders__enriched"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.int__sales__analytics__rolling_30day":{"database":"development","schema":"jaffle_shop_dev","name":"int__sales__analytics__rolling_30day","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/sales/analytics/int__sales__analytics__rolling_30day.sql","original_file_path":"models/intermediate/sales/analytics/int__sales__analytics__rolling_30day.sql","unique_id":"model.jaffle_shop.int__sales__analytics__rolling_30day","fqn":["jaffle_shop","intermediate","sales","analytics","int__sales__analytics__rolling_30day"],"alias":"int__sales__analytics__rolling_30day","checksum":{"name":"sha256","checksum":"78f7c85c824c65a077a0a270549607daa259992d89dbaf3c9845a746d4b4f806"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"30-day rolling window analysis of sales metrics partitioned by store for trend analysis","columns":{"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id_count":{"name":"order_id_count","description":"Number of orders in the last 30 days","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"number","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_dollars_sum":{"name":"order_total_dollars_sum","description":"Revenue metrics for the last 30 days (sum, min, max)","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"data_type":"number","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_dollars_min":{"name":"order_total_dollars_min","description":"Revenue metrics for the last 30 days (sum, min, max)","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"data_type":"number","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_dollars_max":{"name":"order_total_dollars_max","description":"Revenue metrics for the last 30 days (sum, min, max)","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"data_type":"number","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_partition_daily":{"name":"portal_partition_daily","description":"Portal Partition Daily","meta":{"type":"dim"},"data_type":"date","constraints":[],"quote":null,"config":{"meta":{"type":"dim"},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":null},"patch_path":"jaffle_shop://models/intermediate/sales/analytics/int__sales__analytics__rolling_30day.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"sales","tags":["intermediate","json"],"docs":{"show":true},"contract":{"enforced":false}},"created_at":1768367615.189556,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__sales__analytics__rolling_30day AS (\n\t\tSELECT\n\t\t\torder_date,\n\t\t\tcount(order_id) AS order_id_count,\n\t\t\tsum(order_total_dollars) AS order_total_dollars_sum,\n\t\t\tmin(order_total_dollars) AS order_total_dollars_min,\n\t\t\tmax(order_total_dollars) AS order_total_dollars_max,\n\t\t\tstore_id,\n\t\t\tstore_name,\n\t\t\t-- partition columns\n\t\t\t_ext_event_date AS portal_partition_daily\n\t\tFROM\n\t\t\t{{ _ext_event_dates_table() }}\n\t\t\tINNER JOIN {{ ref('int__sales__orders__enriched') }} ON portal_partition_daily <= _ext_event_date\n\t\t\tAND portal_partition_daily >= date_add('day', -30, _ext_event_date)\n\t\tGROUP BY\n\t\t\torder_date,\n\t\t\tstore_id,\n\t\t\tstore_name,\n\t\t\t_ext_event_date\n\t)\nSELECT\n\t*\nFROM\n\tint__sales__analytics__rolling_30day","doc_blocks":[],"language":"sql","refs":[{"name":"int__sales__orders__enriched","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":["macro.jaffle_shop._ext_event_dates_table"],"nodes":["model.jaffle_shop.int__sales__orders__enriched"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.int__sales__analytics__monthly_revenue":{"database":"development","schema":"jaffle_shop_dev","name":"int__sales__analytics__monthly_revenue","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/sales/analytics/int__sales__analytics__monthly_revenue.sql","original_file_path":"models/intermediate/sales/analytics/int__sales__analytics__monthly_revenue.sql","unique_id":"model.jaffle_shop.int__sales__analytics__monthly_revenue","fqn":["jaffle_shop","intermediate","sales","analytics","int__sales__analytics__monthly_revenue"],"alias":"int__sales__analytics__monthly_revenue","checksum":{"name":"sha256","checksum":"2e374db1319200fc81c8c6ffce9489bc66a84f800320d29b4bfca64d438d2733"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"Monthly rollup of sales data aggregating revenue, orders, and customer metrics by store","columns":{"customer_first_name":{"name":"customer_first_name","description":"Customer first name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_id":{"name":"customer_id","description":"Foreign key to customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"datetime":{"name":"datetime","description":"Datetime","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__sales__analytics__monthly_revenue"},"dimension":{"label":"Datetime","time_intervals":["MONTH","YEAR"]}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__sales__analytics__monthly_revenue"},"dimension":{"label":"Datetime","time_intervals":["MONTH","YEAR"]}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"Unique identifier for each order","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"ordered_at":{"name":"ordered_at","description":"Timestamp when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":null},"patch_path":"jaffle_shop://models/intermediate/sales/analytics/int__sales__analytics__monthly_revenue.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"sales","tags":["intermediate","json"],"docs":{"show":true},"contract":{"enforced":false}},"created_at":1768367615.190794,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__sales__analytics__monthly_revenue AS (\n\t\tSELECT\n\t\t\tcustomer_first_name,\n\t\t\tcustomer_id,\n\t\t\tcustomer_name,\n\t\t\tdate_trunc('month', datetime) AS datetime,\n\t\t\torder_date,\n\t\t\torder_id,\n\t\t\tordered_at,\n\t\t\tportal_source_count,\n\t\t\tstore_id,\n\t\t\tstore_name\n\t\tFROM\n\t\t\t{{ ref('int__sales__orders__enriched') }}\n\t\tGROUP BY\n\t\t\tcustomer_first_name,\n\t\t\tcustomer_id,\n\t\t\tcustomer_name,\n\t\t\tdate_trunc('month', datetime),\n\t\t\torder_date,\n\t\t\torder_id,\n\t\t\tordered_at,\n\t\t\tstore_id,\n\t\t\tstore_name\n\t)\nSELECT\n\t*\nFROM\n\tint__sales__analytics__monthly_revenue","doc_blocks":[],"language":"sql","refs":[{"name":"int__sales__orders__enriched","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__sales__orders__enriched"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.int__supply_chain__supplies__cost_analysis":{"database":"development","schema":"jaffle_shop_dev","name":"int__supply_chain__supplies__cost_analysis","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/supply_chain/supplies/int__supply_chain__supplies__cost_analysis.sql","original_file_path":"models/intermediate/supply_chain/supplies/int__supply_chain__supplies__cost_analysis.sql","unique_id":"model.jaffle_shop.int__supply_chain__supplies__cost_analysis","fqn":["jaffle_shop","intermediate","supply_chain","supplies","int__supply_chain__supplies__cost_analysis"],"alias":"int__supply_chain__supplies__cost_analysis","checksum":{"name":"sha256","checksum":"c569a2c4c33d074e0dcf3e5c65df4e583b8730580d837f2f8ccb474b59bedfca"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"supply_chain","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#DAA520"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"","columns":{"cost_cents":{"name":"cost_cents","description":"Supply cost in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"cost_dollars":{"name":"cost_dollars","description":"Supply cost in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"cost_tier":{"name":"cost_tier","description":"Supply cost tier classification","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__supply_chain__supplies__cost_analysis"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__supply_chain__supplies__cost_analysis"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_perishable":{"name":"is_perishable","description":"Boolean flag for perishable supplies","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_name":{"name":"product_name","description":"Product name, trimmed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_price_dollars":{"name":"product_price_dollars","description":"Product price in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Associated product SKU","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_type":{"name":"product_type","description":"Product category (jaffle, beverage)","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_category":{"name":"supply_category","description":"Supply category classification","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_id":{"name":"supply_id","description":"Unique identifier for each supply item","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_name":{"name":"supply_name","description":"Supply item name, trimmed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_to_price_ratio":{"name":"supply_to_price_ratio","description":"Supply cost as percentage of product price","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__supply_chain__supplies__cost_analysis"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__supply_chain__supplies__cost_analysis"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"supply_chain","docs":{"show":true,"node_color":"#DAA520"},"patch_path":"jaffle_shop://models/intermediate/supply_chain/supplies/int__supply_chain__supplies__cost_analysis.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"supply_chain","tags":["intermediate","json"],"docs":{"node_color":"#DAA520","show":true},"contract":{"enforced":false}},"created_at":1768367615.1917849,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__supply_chain__supplies__cost_analysis AS (\n\t\tSELECT\n\t\t\tstg__supply_chain__supplies__inventory.cost_cents,\n\t\t\tstg__supply_chain__supplies__inventory.cost_dollars,\n\t\t\t{{ cost_tier() }} AS cost_tier,\n\t\t\tstg__supply_chain__supplies__inventory.is_perishable,\n\t\t\tstg__supply_chain__supplies__inventory.portal_source_count,\n\t\t\tstg__products__catalog__catalog.product_name,\n\t\t\tstg__products__catalog__catalog.product_price_dollars,\n\t\t\tstg__supply_chain__supplies__inventory.product_sku,\n\t\t\tstg__products__catalog__catalog.product_type,\n\t\t\tstg__supply_chain__supplies__inventory.supply_category,\n\t\t\tstg__supply_chain__supplies__inventory.supply_id,\n\t\t\tstg__supply_chain__supplies__inventory.supply_name,\n\t\t\t{{ supply_to_price_ratio() }} AS supply_to_price_ratio\n\t\tFROM\n\t\t\t{{ ref('stg__supply_chain__supplies__inventory') }} stg__supply_chain__supplies__inventory\n\t\t\tLEFT JOIN {{ ref('stg__products__catalog__catalog') }} stg__products__catalog__catalog ON stg__supply_chain__supplies__inventory.product_sku = stg__products__catalog__catalog.product_sku\n\t)\nSELECT\n\t*\nFROM\n\tint__supply_chain__supplies__cost_analysis","doc_blocks":[],"language":"sql","refs":[{"name":"stg__supply_chain__supplies__inventory","package":null,"version":null},{"name":"stg__products__catalog__catalog","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":["macro.jaffle_shop.cost_tier","macro.jaffle_shop.supply_to_price_ratio"],"nodes":["model.jaffle_shop.stg__supply_chain__supplies__inventory","model.jaffle_shop.stg__products__catalog__catalog"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"seed.jaffle_shop.raw_items":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_items","resource_type":"seed","package_name":"jaffle_shop","path":"raw_items.csv","original_file_path":"seeds/raw_items.csv","unique_id":"seed.jaffle_shop.raw_items","fqn":["jaffle_shop","raw_items"],"alias":"raw_items","checksum":{"name":"path","checksum":"seeds/raw_items.csv"},"config":{"enabled":true,"alias":null,"schema":"seeds","database":null,"tags":[],"meta":{},"group":null,"materialized":"seed","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"delimiter":",","quote_columns":false},"tags":[],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{"quote_columns":false,"schema":"seeds"},"created_at":1768367615.015794,"relation_name":"development.jaffle_shop_dev_seeds.raw_items","raw_code":"","doc_blocks":[],"root_path":"/path/to/dbt/project","depends_on":{"macros":[]}},"seed.jaffle_shop.raw_customers":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_customers","resource_type":"seed","package_name":"jaffle_shop","path":"raw_customers.csv","original_file_path":"seeds/raw_customers.csv","unique_id":"seed.jaffle_shop.raw_customers","fqn":["jaffle_shop","raw_customers"],"alias":"raw_customers","checksum":{"name":"sha256","checksum":"d91ca12deb29112b84ef9b56856fcb69645497bbca8315379dedf61b55b504b1"},"config":{"enabled":true,"alias":null,"schema":"seeds","database":null,"tags":[],"meta":{},"group":null,"materialized":"seed","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"delimiter":",","quote_columns":false},"tags":[],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{"quote_columns":false,"schema":"seeds"},"created_at":1768367615.016643,"relation_name":"development.jaffle_shop_dev_seeds.raw_customers","raw_code":"","doc_blocks":[],"root_path":"/path/to/dbt/project","depends_on":{"macros":[]}},"seed.jaffle_shop.raw_stores":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_stores","resource_type":"seed","package_name":"jaffle_shop","path":"raw_stores.csv","original_file_path":"seeds/raw_stores.csv","unique_id":"seed.jaffle_shop.raw_stores","fqn":["jaffle_shop","raw_stores"],"alias":"raw_stores","checksum":{"name":"sha256","checksum":"3766ca1319b55ea44a16296e24a5faefe26a182b85b0f5142c986482f9801ccc"},"config":{"enabled":true,"alias":null,"schema":"seeds","database":null,"tags":[],"meta":{},"group":null,"materialized":"seed","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"delimiter":",","quote_columns":false},"tags":[],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{"quote_columns":false,"schema":"seeds"},"created_at":1768367615.0173051,"relation_name":"development.jaffle_shop_dev_seeds.raw_stores","raw_code":"","doc_blocks":[],"root_path":"/path/to/dbt/project","depends_on":{"macros":[]}},"seed.jaffle_shop.raw_orders":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_orders","resource_type":"seed","package_name":"jaffle_shop","path":"raw_orders.csv","original_file_path":"seeds/raw_orders.csv","unique_id":"seed.jaffle_shop.raw_orders","fqn":["jaffle_shop","raw_orders"],"alias":"raw_orders","checksum":{"name":"path","checksum":"seeds/raw_orders.csv"},"config":{"enabled":true,"alias":null,"schema":"seeds","database":null,"tags":[],"meta":{},"group":null,"materialized":"seed","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"delimiter":",","quote_columns":false},"tags":[],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{"quote_columns":false,"schema":"seeds"},"created_at":1768367615.017958,"relation_name":"development.jaffle_shop_dev_seeds.raw_orders","raw_code":"","doc_blocks":[],"root_path":"/path/to/dbt/project","depends_on":{"macros":[]}},"seed.jaffle_shop.raw_supplies":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_supplies","resource_type":"seed","package_name":"jaffle_shop","path":"raw_supplies.csv","original_file_path":"seeds/raw_supplies.csv","unique_id":"seed.jaffle_shop.raw_supplies","fqn":["jaffle_shop","raw_supplies"],"alias":"raw_supplies","checksum":{"name":"sha256","checksum":"da4dedefd0350e023054c208a20bda46ecc4b39e037b42be2ed4c39ed19ddf2d"},"config":{"enabled":true,"alias":null,"schema":"seeds","database":null,"tags":[],"meta":{},"group":null,"materialized":"seed","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"delimiter":",","quote_columns":false},"tags":[],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{"quote_columns":false,"schema":"seeds"},"created_at":1768367615.018603,"relation_name":"development.jaffle_shop_dev_seeds.raw_supplies","raw_code":"","doc_blocks":[],"root_path":"/path/to/dbt/project","depends_on":{"macros":[]}},"seed.jaffle_shop.raw_products":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_products","resource_type":"seed","package_name":"jaffle_shop","path":"raw_products.csv","original_file_path":"seeds/raw_products.csv","unique_id":"seed.jaffle_shop.raw_products","fqn":["jaffle_shop","raw_products"],"alias":"raw_products","checksum":{"name":"sha256","checksum":"a272e2f54aea0708c26ea3d8fb0cc6062196b68c8e47ab6197426c7cd3f841d8"},"config":{"enabled":true,"alias":null,"schema":"seeds","database":null,"tags":[],"meta":{},"group":null,"materialized":"seed","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"delimiter":",","quote_columns":false},"tags":[],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{"quote_columns":false,"schema":"seeds"},"created_at":1768367615.0194612,"relation_name":"development.jaffle_shop_dev_seeds.raw_products","raw_code":"","doc_blocks":[],"root_path":"/path/to/dbt/project","depends_on":{"macros":[]}},"operation.jaffle_shop.jaffle_shop-on-run-start-0":{"database":"development","schema":"jaffle_shop_dev","name":"jaffle_shop-on-run-start-0","resource_type":"operation","package_name":"jaffle_shop","path":"hooks/jaffle_shop-on-run-start-0.sql","original_file_path":"./dbt_project.yml","unique_id":"operation.jaffle_shop.jaffle_shop-on-run-start-0","fqn":["jaffle_shop","hooks","jaffle_shop-on-run-start-0"],"alias":"jaffle_shop-on-run-start-0","checksum":{"name":"sha256","checksum":"e42f2fc0c2471eec762729e58a016137d1f2e025600e3083e4ab37bc4432329b"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":[],"meta":{},"group":null,"materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null},"tags":["on-run-start"],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{},"created_at":1768367615.0695171,"relation_name":null,"raw_code":"{{ log('Run started: ' ~ run_started_at, info=True) }}","doc_blocks":[],"language":"sql","refs":[],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":[]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"index":0},"operation.jaffle_shop.jaffle_shop-on-run-end-0":{"database":"development","schema":"jaffle_shop_dev","name":"jaffle_shop-on-run-end-0","resource_type":"operation","package_name":"jaffle_shop","path":"hooks/jaffle_shop-on-run-end-0.sql","original_file_path":"./dbt_project.yml","unique_id":"operation.jaffle_shop.jaffle_shop-on-run-end-0","fqn":["jaffle_shop","hooks","jaffle_shop-on-run-end-0"],"alias":"jaffle_shop-on-run-end-0","checksum":{"name":"sha256","checksum":"e42f2fc0c2471eec762729e58a016137d1f2e025600e3083e4ab37bc4432329b"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":[],"meta":{},"group":null,"materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null},"tags":["on-run-end"],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{},"created_at":1768367615.073832,"relation_name":null,"raw_code":"{{ log('Run finished: ' ~ run_finished_at, info=True) }}","doc_blocks":[],"language":"sql","refs":[],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":[]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"index":0}},"sources":{"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_customers":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_customers","resource_type":"source","package_name":"jaffle_shop","path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","original_file_path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","unique_id":"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_customers","fqn":["jaffle_shop","sources","development","development__jaffle_shop_dev_seeds","raw_customers"],"source_name":"development__jaffle_shop_dev_seeds","source_description":"","loader":"","identifier":"raw_customers","quoting":{"database":null,"schema":null,"identifier":null,"column":null},"loaded_at_field":null,"loaded_at_query":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"external":null,"description":"","columns":{"id":{"name":"id","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"name":{"name":"name","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"source_meta":{},"tags":[],"config":{"enabled":true,"event_time":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"patch_path":null,"unrendered_config":{"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"relation_name":"development.jaffle_shop_dev_seeds.raw_customers","created_at":1768367615.200182,"unrendered_database":"development","unrendered_schema":"jaffle_shop_dev_seeds","doc_blocks":[]},"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_items":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_items","resource_type":"source","package_name":"jaffle_shop","path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","original_file_path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","unique_id":"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_items","fqn":["jaffle_shop","sources","development","development__jaffle_shop_dev_seeds","raw_items"],"source_name":"development__jaffle_shop_dev_seeds","source_description":"","loader":"","identifier":"raw_items","quoting":{"database":null,"schema":null,"identifier":null,"column":null},"loaded_at_field":null,"loaded_at_query":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"external":null,"description":"","columns":{"id":{"name":"id","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"sku":{"name":"sku","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"source_meta":{},"tags":[],"config":{"enabled":true,"event_time":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"patch_path":null,"unrendered_config":{"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"relation_name":"development.jaffle_shop_dev_seeds.raw_items","created_at":1768367615.200856,"unrendered_database":"development","unrendered_schema":"jaffle_shop_dev_seeds","doc_blocks":[]},"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_orders":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_orders","resource_type":"source","package_name":"jaffle_shop","path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","original_file_path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","unique_id":"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_orders","fqn":["jaffle_shop","sources","development","development__jaffle_shop_dev_seeds","raw_orders"],"source_name":"development__jaffle_shop_dev_seeds","source_description":"","loader":"","identifier":"raw_orders","quoting":{"database":null,"schema":null,"identifier":null,"column":null},"loaded_at_field":null,"loaded_at_query":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"external":null,"description":"","columns":{"id":{"name":"id","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer":{"name":"customer","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"ordered_at":{"name":"ordered_at","description":"","meta":{},"data_type":"timestamp(3)","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"subtotal":{"name":"subtotal","description":"","meta":{},"data_type":"integer","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"tax_paid":{"name":"tax_paid","description":"","meta":{},"data_type":"integer","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total":{"name":"order_total","description":"","meta":{},"data_type":"integer","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"source_meta":{},"tags":[],"config":{"enabled":true,"event_time":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"patch_path":null,"unrendered_config":{"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"relation_name":"development.jaffle_shop_dev_seeds.raw_orders","created_at":1768367615.201026,"unrendered_database":"development","unrendered_schema":"jaffle_shop_dev_seeds","doc_blocks":[]},"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_products":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_products","resource_type":"source","package_name":"jaffle_shop","path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","original_file_path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","unique_id":"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_products","fqn":["jaffle_shop","sources","development","development__jaffle_shop_dev_seeds","raw_products"],"source_name":"development__jaffle_shop_dev_seeds","source_description":"","loader":"","identifier":"raw_products","quoting":{"database":null,"schema":null,"identifier":null,"column":null},"loaded_at_field":null,"loaded_at_query":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"external":null,"description":"","columns":{"sku":{"name":"sku","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"name":{"name":"name","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"type":{"name":"type","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"price":{"name":"price","description":"","meta":{},"data_type":"integer","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"description":{"name":"description","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"source_meta":{},"tags":[],"config":{"enabled":true,"event_time":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"patch_path":null,"unrendered_config":{"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"relation_name":"development.jaffle_shop_dev_seeds.raw_products","created_at":1768367615.201184,"unrendered_database":"development","unrendered_schema":"jaffle_shop_dev_seeds","doc_blocks":[]},"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_stores":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_stores","resource_type":"source","package_name":"jaffle_shop","path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","original_file_path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","unique_id":"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_stores","fqn":["jaffle_shop","sources","development","development__jaffle_shop_dev_seeds","raw_stores"],"source_name":"development__jaffle_shop_dev_seeds","source_description":"","loader":"","identifier":"raw_stores","quoting":{"database":null,"schema":null,"identifier":null,"column":null},"loaded_at_field":null,"loaded_at_query":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"external":null,"description":"","columns":{"id":{"name":"id","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"name":{"name":"name","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"opened_at":{"name":"opened_at","description":"","meta":{},"data_type":"timestamp(3)","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"tax_rate":{"name":"tax_rate","description":"","meta":{},"data_type":"double","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"source_meta":{},"tags":[],"config":{"enabled":true,"event_time":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"patch_path":null,"unrendered_config":{"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"relation_name":"development.jaffle_shop_dev_seeds.raw_stores","created_at":1768367615.201335,"unrendered_database":"development","unrendered_schema":"jaffle_shop_dev_seeds","doc_blocks":[]},"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_supplies":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_supplies","resource_type":"source","package_name":"jaffle_shop","path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","original_file_path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","unique_id":"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_supplies","fqn":["jaffle_shop","sources","development","development__jaffle_shop_dev_seeds","raw_supplies"],"source_name":"development__jaffle_shop_dev_seeds","source_description":"","loader":"","identifier":"raw_supplies","quoting":{"database":null,"schema":null,"identifier":null,"column":null},"loaded_at_field":null,"loaded_at_query":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"external":null,"description":"","columns":{"id":{"name":"id","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"name":{"name":"name","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"cost":{"name":"cost","description":"","meta":{},"data_type":"integer","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"perishable":{"name":"perishable","description":"","meta":{},"data_type":"boolean","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"sku":{"name":"sku","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"source_meta":{},"tags":[],"config":{"enabled":true,"event_time":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"patch_path":null,"unrendered_config":{"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"relation_name":"development.jaffle_shop_dev_seeds.raw_supplies","created_at":1768367615.201489,"unrendered_database":"development","unrendered_schema":"jaffle_shop_dev_seeds","doc_blocks":[]}},"docs":{"doc.dbt.__overview__":{"name":"__overview__","resource_type":"doc","package_name":"dbt","path":"overview.md","original_file_path":"docs/overview.md","unique_id":"doc.dbt.__overview__","block_contents":"### Welcome!\n\nWelcome to the auto-generated documentation for your dbt project!\n\n### Navigation\n\nYou can use the `Project` and `Database` navigation tabs on the left side of the window to explore the models\nin your project.\n\n#### Project Tab\nThe `Project` tab mirrors the directory structure of your dbt project. In this tab, you can see all of the\nmodels defined in your dbt project, as well as models imported from dbt packages.\n\n#### Database Tab\nThe `Database` tab also exposes your models, but in a format that looks more like a database explorer. This view\nshows relations (tables and views) grouped into database schemas. Note that ephemeral models are _not_ shown\nin this interface, as they do not exist in the database.\n\n### Graph Exploration\nYou can click the blue icon on the bottom-right corner of the page to view the lineage graph of your models.\n\nOn model pages, you'll see the immediate parents and children of the model you're exploring. By clicking the `Expand`\nbutton at the top-right of this lineage pane, you'll be able to see all of the models that are used to build,\nor are built from, the model you're exploring.\n\nOnce expanded, you'll be able to use the `--select` and `--exclude` model selection syntax to filter the\nmodels in the graph. For more information on model selection, check out the [dbt docs](https://docs.getdbt.com/docs/model-selection-syntax).\n\nNote that you can also right-click on models to interactively filter and explore the graph.\n\n---\n\n### More information\n\n- [What is dbt](https://docs.getdbt.com/docs/introduction)?\n- Read the [dbt viewpoint](https://docs.getdbt.com/docs/viewpoint)\n- [Installation](https://docs.getdbt.com/docs/installation)\n- Join the [dbt Community](https://www.getdbt.com/community/) for questions and discussion"}},"exposures":{},"metrics":{},"groups":{"group.jaffle_shop.sales":{"name":"sales","resource_type":"group","package_name":"jaffle_shop","path":"groups.yml","original_file_path":"models/groups.yml","unique_id":"group.jaffle_shop.sales","owner":{"email":"sales@example.com","name":"Sales Team"},"description":"Order processing, revenue tracking, profitability analysis, and sales performance metrics","config":{"meta":{}}},"group.jaffle_shop.products":{"name":"products","resource_type":"group","package_name":"jaffle_shop","path":"groups.yml","original_file_path":"models/groups.yml","unique_id":"group.jaffle_shop.products","owner":{"email":"product@example.com","name":"Product Team"},"description":"Product catalog management, menu analytics, cost efficiency analysis, and supply chain optimization","config":{"meta":{}}},"group.jaffle_shop.customers":{"name":"customers","resource_type":"group","package_name":"jaffle_shop","path":"groups.yml","original_file_path":"models/groups.yml","unique_id":"group.jaffle_shop.customers","owner":{"email":"customers@example.com","name":"Customer Team"},"description":"Customer profiles, behavior analysis, segmentation, and customer-focused dashboards","config":{"meta":{}}},"group.jaffle_shop.supply_chain":{"name":"supply_chain","resource_type":"group","package_name":"jaffle_shop","path":"groups.yml","original_file_path":"models/groups.yml","unique_id":"group.jaffle_shop.supply_chain","owner":{"email":"supply-chain@example.com","name":"Supply Chain Team"},"description":"Supply cost analysis, inventory management, and supplier performance tracking","config":{"meta":{}}},"group.jaffle_shop.analytics":{"name":"analytics","resource_type":"group","package_name":"jaffle_shop","path":"groups.yml","original_file_path":"models/groups.yml","unique_id":"group.jaffle_shop.analytics","owner":{"email":"analytics@example.com","name":"Analytics Team"},"description":"Cross-functional business intelligence, comprehensive analytics, and strategic insights across all business areas","config":{"meta":{}}}},"selectors":{},"disabled":{},"parent_map":{"model.jaffle_shop.stg__customers__profiles__clean":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_customers"],"model.jaffle_shop.stg__products__catalog__catalog":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_products"],"model.jaffle_shop.stg__sales__stores__locations":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_stores"],"model.jaffle_shop.stg__sales__orders__standardized":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_orders"],"model.jaffle_shop.stg__sales__items__order_details":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_items"],"model.jaffle_shop.stg__supply_chain__supplies__inventory":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_supplies"],"model.jaffle_shop.mart__customers__dashboard__analytics":["model.jaffle_shop.int__customers__profiles__summary"],"model.jaffle_shop.mart__products__reporting__cost_efficiency":["model.jaffle_shop.int__supply_chain__supplies__cost_analysis"],"model.jaffle_shop.mart__products__reporting__menu_analytics":["model.jaffle_shop.int__products__analytics__product_popularity"],"model.jaffle_shop.mart__sales__reporting__revenue":["model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.mart__sales__reporting__profitability":["model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics":["model.jaffle_shop.mart__products__reporting__cost_efficiency","model.jaffle_shop.mart__products__reporting__menu_analytics"],"model.jaffle_shop.int__customers__profiles__summary":["model.jaffle_shop.stg__customers__profiles__clean","model.jaffle_shop.stg__sales__orders__standardized"],"model.jaffle_shop.int__products__analytics__product_popularity":["model.jaffle_shop.stg__sales__items__order_details"],"model.jaffle_shop.int__sales__orders__enriched":["model.jaffle_shop.stg__customers__profiles__clean","model.jaffle_shop.stg__sales__orders__standardized","model.jaffle_shop.stg__sales__stores__locations"],"model.jaffle_shop.int__sales__orders__regional_combined":["model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.int__sales__analytics__rolling_30day":["model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.int__sales__analytics__monthly_revenue":["model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.int__supply_chain__supplies__cost_analysis":["model.jaffle_shop.stg__products__catalog__catalog","model.jaffle_shop.stg__supply_chain__supplies__inventory"],"seed.jaffle_shop.raw_items":[],"seed.jaffle_shop.raw_customers":[],"seed.jaffle_shop.raw_stores":[],"seed.jaffle_shop.raw_orders":[],"seed.jaffle_shop.raw_supplies":[],"seed.jaffle_shop.raw_products":[],"operation.jaffle_shop.jaffle_shop-on-run-start-0":[],"operation.jaffle_shop.jaffle_shop-on-run-end-0":[],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_customers":[],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_items":[],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_orders":[],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_products":[],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_stores":[],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_supplies":[]},"child_map":{"model.jaffle_shop.stg__customers__profiles__clean":["model.jaffle_shop.int__customers__profiles__summary","model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.stg__products__catalog__catalog":["model.jaffle_shop.int__supply_chain__supplies__cost_analysis"],"model.jaffle_shop.stg__sales__stores__locations":["model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.stg__sales__orders__standardized":["model.jaffle_shop.int__customers__profiles__summary","model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.stg__sales__items__order_details":["model.jaffle_shop.int__products__analytics__product_popularity"],"model.jaffle_shop.stg__supply_chain__supplies__inventory":["model.jaffle_shop.int__supply_chain__supplies__cost_analysis"],"model.jaffle_shop.mart__customers__dashboard__analytics":[],"model.jaffle_shop.mart__products__reporting__cost_efficiency":["model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics"],"model.jaffle_shop.mart__products__reporting__menu_analytics":["model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics"],"model.jaffle_shop.mart__sales__reporting__revenue":[],"model.jaffle_shop.mart__sales__reporting__profitability":[],"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics":[],"model.jaffle_shop.int__customers__profiles__summary":["model.jaffle_shop.mart__customers__dashboard__analytics"],"model.jaffle_shop.int__products__analytics__product_popularity":["model.jaffle_shop.mart__products__reporting__menu_analytics"],"model.jaffle_shop.int__sales__orders__enriched":["model.jaffle_shop.int__sales__analytics__monthly_revenue","model.jaffle_shop.int__sales__analytics__rolling_30day","model.jaffle_shop.int__sales__orders__regional_combined","model.jaffle_shop.mart__sales__reporting__profitability","model.jaffle_shop.mart__sales__reporting__revenue"],"model.jaffle_shop.int__sales__orders__regional_combined":[],"model.jaffle_shop.int__sales__analytics__rolling_30day":[],"model.jaffle_shop.int__sales__analytics__monthly_revenue":[],"model.jaffle_shop.int__supply_chain__supplies__cost_analysis":["model.jaffle_shop.mart__products__reporting__cost_efficiency"],"seed.jaffle_shop.raw_items":[],"seed.jaffle_shop.raw_customers":[],"seed.jaffle_shop.raw_stores":[],"seed.jaffle_shop.raw_orders":[],"seed.jaffle_shop.raw_supplies":[],"seed.jaffle_shop.raw_products":[],"operation.jaffle_shop.jaffle_shop-on-run-start-0":[],"operation.jaffle_shop.jaffle_shop-on-run-end-0":[],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_customers":["model.jaffle_shop.stg__customers__profiles__clean"],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_items":["model.jaffle_shop.stg__sales__items__order_details"],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_orders":["model.jaffle_shop.stg__sales__orders__standardized"],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_products":["model.jaffle_shop.stg__products__catalog__catalog"],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_stores":["model.jaffle_shop.stg__sales__stores__locations"],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_supplies":["model.jaffle_shop.stg__supply_chain__supplies__inventory"]},"group_map":{"sales":["model.jaffle_shop.stg__sales__stores__locations","model.jaffle_shop.stg__sales__orders__standardized","model.jaffle_shop.stg__sales__items__order_details","model.jaffle_shop.mart__sales__reporting__revenue","model.jaffle_shop.mart__sales__reporting__profitability","model.jaffle_shop.int__sales__orders__enriched","model.jaffle_shop.int__sales__orders__regional_combined","model.jaffle_shop.int__sales__analytics__rolling_30day","model.jaffle_shop.int__sales__analytics__monthly_revenue"],"products":["model.jaffle_shop.stg__products__catalog__catalog","model.jaffle_shop.mart__products__reporting__cost_efficiency","model.jaffle_shop.mart__products__reporting__menu_analytics","model.jaffle_shop.int__products__analytics__product_popularity"],"customers":["model.jaffle_shop.stg__customers__profiles__clean","model.jaffle_shop.mart__customers__dashboard__analytics","model.jaffle_shop.int__customers__profiles__summary"],"supply_chain":["model.jaffle_shop.stg__supply_chain__supplies__inventory","model.jaffle_shop.int__supply_chain__supplies__cost_analysis"],"analytics":["model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics"]},"saved_queries":{},"semantic_models":{},"unit_tests":{}} \ No newline at end of file +{"metadata":{"dbt_schema_version":"https://schemas.getdbt.com/dbt/manifest/v12.json","dbt_version":"1.10.17","generated_at":"2026-05-04T13:21:53.112125Z","invocation_id":"aff8a422-d597-4b53-b155-301a8038905a","invocation_started_at":"2026-05-04T13:08:16.905880+00:00","env":{},"project_name":"jaffle_shop","project_id":"06e5b98c2db46f8a72cc4f66410e9b3b","user_id":null,"send_anonymous_usage_stats":false,"adapter_type":"trino","quoting":{"database":false,"schema":false,"identifier":false,"column":null}},"nodes":{"model.jaffle_shop.stg__customers__profiles__clean":{"database":"development","schema":"jaffle_shop_dev_staging","name":"stg__customers__profiles__clean","resource_type":"model","package_name":"jaffle_shop","path":"staging/customers/profiles/stg__customers__profiles__clean.sql","original_file_path":"models/staging/customers/profiles/stg__customers__profiles__clean.sql","unique_id":"model.jaffle_shop.stg__customers__profiles__clean","fqn":["jaffle_shop","staging","customers","profiles","stg__customers__profiles__clean"],"alias":"stg__customers__profiles__clean","checksum":{"name":"sha256","checksum":"287111d3952cfe726dbe9e981ee7081b346589275a6360c596261ca94f51a776"},"config":{"enabled":true,"alias":null,"schema":"staging","database":null,"tags":["json","staging"],"meta":{},"group":"customers","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#B6AB33"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","staging"],"description":"","columns":{"customer_first_name":{"name":"customer_first_name","description":"Customer first name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_id":{"name":"customer_id","description":"Unique identifier for each customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_last_name":{"name":"customer_last_name","description":"Customer last name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"customers","docs":{"show":true,"node_color":"#B6AB33"},"patch_path":"jaffle_shop://models/staging/customers/profiles/stg__customers__profiles__clean.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","schema":"staging","group":"customers","tags":["json","staging"],"docs":{"node_color":"#B6AB33","show":true},"contract":{"enforced":false}},"created_at":1777900097.9705758,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tstg__customers__profiles__clean AS (\n\t\tSELECT\n\t\t\tsplit(TRIM(name), ' ') [1] AS customer_first_name,\n\t\t\tid AS customer_id,\n\t\t\tCASE\n\t\t\t\tWHEN cardinality(split(TRIM(name), ' ')) > 1 THEN split(TRIM(name), ' ') [cardinality(split(TRIM(name), ' '))]\n\t\t\t\tELSE NULL\n\t\t\tEND AS customer_last_name,\n\t\t\tTRIM(name) AS customer_name,\n\t\t\t1 AS portal_source_count\n\t\tFROM\n\t\t\t{{ source('development__jaffle_shop_dev_seeds','raw_customers') }}\n\t)\nSELECT\n\t*\nFROM\n\tstg__customers__profiles__clean","doc_blocks":[],"language":"sql","refs":[],"sources":[["development__jaffle_shop_dev_seeds","raw_customers"]],"metrics":[],"depends_on":{"macros":[],"nodes":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_customers"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.stg__products__catalog__catalog":{"database":"development","schema":"jaffle_shop_dev_staging","name":"stg__products__catalog__catalog","resource_type":"model","package_name":"jaffle_shop","path":"staging/products/catalog/stg__products__catalog__catalog.sql","original_file_path":"models/staging/products/catalog/stg__products__catalog__catalog.sql","unique_id":"model.jaffle_shop.stg__products__catalog__catalog","fqn":["jaffle_shop","staging","products","catalog","stg__products__catalog__catalog"],"alias":"stg__products__catalog__catalog","checksum":{"name":"sha256","checksum":"9dc9de44d2bf73d9d9a961d348ae987846711bc1695ed792141e8aa513fd224a"},"config":{"enabled":true,"alias":null,"schema":"staging","database":null,"tags":["json","staging"],"meta":{},"group":"products","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#B6AB33"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","staging"],"description":"","columns":{"description":{"name":"description","description":"Product description","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_beverage":{"name":"is_beverage","description":"Boolean flag for beverage products","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_jaffle":{"name":"is_jaffle","description":"Boolean flag for jaffle products","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_name":{"name":"product_name","description":"Product name, trimmed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_price_cents":{"name":"product_price_cents","description":"Product price in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_price_dollars":{"name":"product_price_dollars","description":"Product price in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Product SKU identifier","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_type":{"name":"product_type","description":"Product category (jaffle, beverage)","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"products","docs":{"show":true,"node_color":"#B6AB33"},"patch_path":"jaffle_shop://models/staging/products/catalog/stg__products__catalog__catalog.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","schema":"staging","group":"products","tags":["json","staging"],"docs":{"node_color":"#B6AB33","show":true},"contract":{"enforced":false}},"created_at":1777900097.9725068,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tstg__products__catalog__catalog AS (\n\t\tSELECT\n\t\t\tTRIM(description) AS description,\n\t\t\tLOWER(TRIM(TYPE)) = 'beverage' AS is_beverage,\n\t\t\tLOWER(TRIM(TYPE)) = 'jaffle' AS is_jaffle,\n\t\t\t1 AS portal_source_count,\n\t\t\tTRIM(name) AS product_name,\n\t\t\tprice AS product_price_cents,\n\t\t\tCAST(price AS DECIMAL(10, 2)) / 100.0 AS product_price_dollars,\n\t\t\tsku AS product_sku,\n\t\t\tTRIM(TYPE) AS product_type\n\t\tFROM\n\t\t\t{{ source('development__jaffle_shop_dev_seeds','raw_products') }}\n\t)\nSELECT\n\t*\nFROM\n\tstg__products__catalog__catalog","doc_blocks":[],"language":"sql","refs":[],"sources":[["development__jaffle_shop_dev_seeds","raw_products"]],"metrics":[],"depends_on":{"macros":[],"nodes":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_products"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.stg__sales__stores__locations":{"database":"development","schema":"jaffle_shop_dev_staging","name":"stg__sales__stores__locations","resource_type":"model","package_name":"jaffle_shop","path":"staging/sales/stores/stg__sales__stores__locations.sql","original_file_path":"models/staging/sales/stores/stg__sales__stores__locations.sql","unique_id":"model.jaffle_shop.stg__sales__stores__locations","fqn":["jaffle_shop","staging","sales","stores","stg__sales__stores__locations"],"alias":"stg__sales__stores__locations","checksum":{"name":"sha256","checksum":"f540771873516a9be7d0c9255e6cb666d633e2aeceb3236f8f3a00dd7dbc5b40"},"config":{"enabled":true,"alias":null,"schema":"staging","database":null,"tags":["json","staging"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#B6AB33"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","staging"],"description":"","columns":{"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Unique identifier for each store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_opened_at":{"name":"store_opened_at","description":"Timestamp when store opened","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_opened_date":{"name":"store_opened_date","description":"Date when store opened","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_tax_rate":{"name":"store_tax_rate","description":"Local tax rate for the store","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_tax_rate_percent":{"name":"store_tax_rate_percent","description":"Tax rate as percentage","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":"#B6AB33"},"patch_path":"jaffle_shop://models/staging/sales/stores/stg__sales__stores__locations.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","schema":"staging","group":"sales","tags":["json","staging"],"docs":{"node_color":"#B6AB33","show":true},"contract":{"enforced":false}},"created_at":1777900097.973921,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tstg__sales__stores__locations AS (\n\t\tSELECT\n\t\t\t1 AS portal_source_count,\n\t\t\tid AS store_id,\n\t\t\tTRIM(name) AS store_name,\n\t\t\tCAST(opened_at AS TIMESTAMP) AS store_opened_at,\n\t\t\tDATE(CAST(opened_at AS TIMESTAMP)) AS store_opened_date,\n\t\t\ttax_rate AS store_tax_rate,\n\t\t\ttax_rate * 100 AS store_tax_rate_percent\n\t\tFROM\n\t\t\t{{ source('development__jaffle_shop_dev_seeds','raw_stores') }}\n\t)\nSELECT\n\t*\nFROM\n\tstg__sales__stores__locations","doc_blocks":[],"language":"sql","refs":[],"sources":[["development__jaffle_shop_dev_seeds","raw_stores"]],"metrics":[],"depends_on":{"macros":[],"nodes":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_stores"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.stg__sales__orders__standardized":{"database":"development","schema":"jaffle_shop_dev_staging","name":"stg__sales__orders__standardized","resource_type":"model","package_name":"jaffle_shop","path":"staging/sales/orders/stg__sales__orders__standardized.sql","original_file_path":"models/staging/sales/orders/stg__sales__orders__standardized.sql","unique_id":"model.jaffle_shop.stg__sales__orders__standardized","fqn":["jaffle_shop","staging","sales","orders","stg__sales__orders__standardized"],"alias":"stg__sales__orders__standardized","checksum":{"name":"sha256","checksum":"c6f5fb75f6eb8b31fb5d26838e257c14b2330b60f5b4b289a0a16a92330d58f7"},"config":{"enabled":true,"alias":null,"schema":"staging","database":null,"tags":["json","staging"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#B6AB33"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","staging"],"description":"","columns":{"customer_id":{"name":"customer_id","description":"Foreign key to customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"Unique identifier for each order","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_cents":{"name":"order_total_cents","description":"Total order amount in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"ordered_at":{"name":"ordered_at","description":"Timestamp when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"subtotal_cents":{"name":"subtotal_cents","description":"Order subtotal in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"tax_paid_cents":{"name":"tax_paid_cents","description":"Tax amount paid in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":"#B6AB33"},"patch_path":"jaffle_shop://models/staging/sales/orders/stg__sales__orders__standardized.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","schema":"staging","group":"sales","tags":["json","staging"],"docs":{"node_color":"#B6AB33","show":true},"contract":{"enforced":false}},"created_at":1777900097.975872,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tstg__sales__orders__standardized AS (\n\t\tSELECT\n\t\t\tcustomer AS customer_id,\n\t\t\tDATE(CAST(ordered_at AS TIMESTAMP)) AS order_date,\n\t\t\tid AS order_id,\n\t\t\torder_total AS order_total_cents,\n\t\t\tCAST(ordered_at AS TIMESTAMP) AS ordered_at,\n\t\t\t1 AS portal_source_count,\n\t\t\tstore_id AS store_id,\n\t\t\tsubtotal AS subtotal_cents,\n\t\t\ttax_paid AS tax_paid_cents\n\t\tFROM\n\t\t\t{{ source('development__jaffle_shop_dev_seeds','raw_orders') }}\n\t)\nSELECT\n\t*\nFROM\n\tstg__sales__orders__standardized","doc_blocks":[],"language":"sql","refs":[],"sources":[["development__jaffle_shop_dev_seeds","raw_orders"]],"metrics":[],"depends_on":{"macros":[],"nodes":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_orders"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.stg__sales__items__order_details":{"database":"development","schema":"jaffle_shop_dev_staging","name":"stg__sales__items__order_details","resource_type":"model","package_name":"jaffle_shop","path":"staging/sales/items/stg__sales__items__order_details.sql","original_file_path":"models/staging/sales/items/stg__sales__items__order_details.sql","unique_id":"model.jaffle_shop.stg__sales__items__order_details","fqn":["jaffle_shop","staging","sales","items","stg__sales__items__order_details"],"alias":"stg__sales__items__order_details","checksum":{"name":"sha256","checksum":"3e1cb4978cc09f8884d2012ddd69b41ff08fda10376e04b342723e7c8dc4641a"},"config":{"enabled":true,"alias":null,"schema":"staging","database":null,"tags":["json","staging"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#B6AB33"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","staging"],"description":"","columns":{"is_beverage_item":{"name":"is_beverage_item","description":"Boolean flag for beverage items","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_jaffle_item":{"name":"is_jaffle_item","description":"Boolean flag for jaffle items","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"item_id":{"name":"item_id","description":"Unique identifier for each order item","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"Foreign key to orders table","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Product SKU identifier","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_type":{"name":"product_type","description":"Product type derived from SKU pattern","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":"#B6AB33"},"patch_path":"jaffle_shop://models/staging/sales/items/stg__sales__items__order_details.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","schema":"staging","group":"sales","tags":["json","staging"],"docs":{"node_color":"#B6AB33","show":true},"contract":{"enforced":false}},"created_at":1777900097.977076,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tstg__sales__items__order_details AS (\n\t\tSELECT\n\t\t\tsku LIKE 'BEV-%' AS is_beverage_item,\n\t\t\tsku LIKE 'JAF-%' AS is_jaffle_item,\n\t\t\tid AS item_id,\n\t\t\torder_id AS order_id,\n\t\t\t1 AS portal_source_count,\n\t\t\tsku AS product_sku,\n\t\t\tCASE\n\t\t\t\tWHEN sku LIKE 'JAF-%' THEN 'jaffle'\n\t\t\t\tWHEN sku LIKE 'BEV-%' THEN 'beverage'\n\t\t\t\tELSE 'unknown'\n\t\t\tEND AS product_type\n\t\tFROM\n\t\t\t{{ source('development__jaffle_shop_dev_seeds','raw_items') }}\n\t)\nSELECT\n\t*\nFROM\n\tstg__sales__items__order_details","doc_blocks":[],"language":"sql","refs":[],"sources":[["development__jaffle_shop_dev_seeds","raw_items"]],"metrics":[],"depends_on":{"macros":[],"nodes":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_items"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.stg__supply_chain__supplies__inventory":{"database":"development","schema":"jaffle_shop_dev_staging","name":"stg__supply_chain__supplies__inventory","resource_type":"model","package_name":"jaffle_shop","path":"staging/supply_chain/supplies/stg__supply_chain__supplies__inventory.sql","original_file_path":"models/staging/supply_chain/supplies/stg__supply_chain__supplies__inventory.sql","unique_id":"model.jaffle_shop.stg__supply_chain__supplies__inventory","fqn":["jaffle_shop","staging","supply_chain","supplies","stg__supply_chain__supplies__inventory"],"alias":"stg__supply_chain__supplies__inventory","checksum":{"name":"sha256","checksum":"1809b8f0c6f71c29c5300ad12fdacfec24bc22280612dcd2a8ebadc7d3c6f5d6"},"config":{"enabled":true,"alias":null,"schema":"staging","database":null,"tags":["json","staging"],"meta":{},"group":"supply_chain","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#B6AB33"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","staging"],"description":"","columns":{"cost_cents":{"name":"cost_cents","description":"Supply cost in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"cost_dollars":{"name":"cost_dollars","description":"Supply cost in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_perishable":{"name":"is_perishable","description":"Boolean flag for perishable supplies","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Associated product SKU","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_category":{"name":"supply_category","description":"Supply category classification","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_id":{"name":"supply_id","description":"Unique identifier for each supply item","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_name":{"name":"supply_name","description":"Supply item name, trimmed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"supply_chain","docs":{"show":true,"node_color":"#B6AB33"},"patch_path":"jaffle_shop://models/staging/supply_chain/supplies/stg__supply_chain__supplies__inventory.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","schema":"staging","group":"supply_chain","tags":["json","staging"],"docs":{"node_color":"#B6AB33","show":true},"contract":{"enforced":false}},"created_at":1777900097.978841,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tstg__supply_chain__supplies__inventory AS (\n\t\tSELECT\n\t\t\tcost AS cost_cents,\n\t\t\tCAST(cost AS DECIMAL(10, 2)) / 100.0 AS cost_dollars,\n\t\t\tCASE\n\t\t\t\tWHEN perishable = TRUE THEN TRUE\n\t\t\t\tELSE FALSE\n\t\t\tEND AS is_perishable,\n\t\t\t1 AS portal_source_count,\n\t\t\tsku AS product_sku,\n\t\t\tCASE\n\t\t\t\tWHEN LOWER(name) LIKE '%cutlery%'\n\t\t\t\tOR LOWER(name) LIKE '%fork%'\n\t\t\t\tOR LOWER(name) LIKE '%knife%' THEN 'utensils'\n\t\t\t\tWHEN LOWER(name) LIKE '%napkin%'\n\t\t\t\tOR LOWER(name) LIKE '%boat%' THEN 'packaging'\n\t\t\t\tWHEN perishable = TRUE THEN 'ingredients'\n\t\t\t\tELSE 'other'\n\t\t\tEND AS supply_category,\n\t\t\tid AS supply_id,\n\t\t\tTRIM(name) AS supply_name\n\t\tFROM\n\t\t\t{{ source('development__jaffle_shop_dev_seeds','raw_supplies') }}\n\t)\nSELECT\n\t*\nFROM\n\tstg__supply_chain__supplies__inventory","doc_blocks":[],"language":"sql","refs":[],"sources":[["development__jaffle_shop_dev_seeds","raw_supplies"]],"metrics":[],"depends_on":{"macros":[],"nodes":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_supplies"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.mart__customers__dashboard__analytics":{"database":"development","schema":"jaffle_shop_dev","name":"mart__customers__dashboard__analytics","resource_type":"model","package_name":"jaffle_shop","path":"marts/customers/dashboard/mart__customers__dashboard__analytics.sql","original_file_path":"models/marts/customers/dashboard/mart__customers__dashboard__analytics.sql","unique_id":"model.jaffle_shop.mart__customers__dashboard__analytics","fqn":["jaffle_shop","marts","customers","dashboard","mart__customers__dashboard__analytics"],"alias":"mart__customers__dashboard__analytics","checksum":{"name":"sha256","checksum":"d74eb1171643e36fdf8303e9d0ede6fde081029d1b3378551e46c2b5d261f72a"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["json","lightdash-explore","mart"],"meta":{},"group":"customers","materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#059669"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","lightdash-explore","mart"],"description":"","columns":{"customer_first_name":{"name":"customer_first_name","description":"Customer first name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer First Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer First Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_id":{"name":"customer_id","description":"Unique identifier for each customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Id"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Id"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_last_name":{"name":"customer_last_name","description":"Customer last name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Last Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Last Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_segment":{"name":"customer_segment","description":"Customer segment classification based on order count","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__customers__dashboard__analytics"},"dimension":{"type":"string","label":"Customer Segment"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__customers__dashboard__analytics"},"dimension":{"type":"string","label":"Customer Segment"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"has_last_name":{"name":"has_last_name","description":"Whether customer provided a last name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"},"dimension":{"type":"string","label":"Has Last Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"},"dimension":{"type":"string","label":"Has Last Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_orders":{"name":"total_orders","description":"Total number of orders placed by customer","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"},"dimension":{"type":"string","label":"Total Orders","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"},"dimension":{"type":"string","label":"Total Orders","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"customers","docs":{"show":true,"node_color":"#059669"},"patch_path":"jaffle_shop://models/marts/customers/dashboard/mart__customers__dashboard__analytics.yml","build_path":null,"unrendered_config":{"materialized":"view","group":"customers","tags":["json","lightdash-explore","mart"],"docs":{"node_color":"#059669","show":true},"contract":{"enforced":false}},"created_at":1777900097.981371,"relation_name":"development.jaffle_shop_dev.mart__customers__dashboard__analytics","raw_code":"{{\n config(\n materialized=\"view\"\n )\n}}\n\nWITH\n\tmart__customers__dashboard__analytics AS (\n\t\tSELECT\n\t\t\tcustomer_first_name,\n\t\t\tcustomer_id,\n\t\t\tcustomer_last_name,\n\t\t\tcustomer_name,\n\t\t\tCASE\n\t\t\t\tWHEN total_orders >= 10 THEN 'VIP'\n\t\t\t\tWHEN total_orders >= 3 THEN 'Regular'\n\t\t\t\tELSE 'New'\n\t\t\tEND AS customer_segment,\n\t\t\thas_last_name,\n\t\t\tportal_source_count,\n\t\t\ttotal_orders\n\t\tFROM\n\t\t\t{{ ref('int__customers__profiles__summary') }}\n\t)\nSELECT\n\t*\nFROM\n\tmart__customers__dashboard__analytics","doc_blocks":[],"language":"sql","refs":[{"name":"int__customers__profiles__summary","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__customers__profiles__summary"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.mart__products__reporting__cost_efficiency":{"database":"development","schema":"jaffle_shop_dev","name":"mart__products__reporting__cost_efficiency","resource_type":"model","package_name":"jaffle_shop","path":"marts/products/reporting/mart__products__reporting__cost_efficiency.sql","original_file_path":"models/marts/products/reporting/mart__products__reporting__cost_efficiency.sql","unique_id":"model.jaffle_shop.mart__products__reporting__cost_efficiency","fqn":["jaffle_shop","marts","products","reporting","mart__products__reporting__cost_efficiency"],"alias":"mart__products__reporting__cost_efficiency","checksum":{"name":"sha256","checksum":"d36e0bd5cc45bd1e8d9636dfaa9b1fa96cc06b5612eaba5f658f3ddbcb9e3b49"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["json","lightdash-explore","mart"],"meta":{},"group":"products","materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#059669"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","lightdash-explore","mart"],"description":"","columns":{"cost_efficiency_category":{"name":"cost_efficiency_category","description":"Cost efficiency classification","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Cost Efficiency Category"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Cost Efficiency Category"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"cost_to_price_ratio":{"name":"cost_to_price_ratio","description":"Individual supply cost as ratio of selling price","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Cost To Price Ratio","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Cost To Price Ratio","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_perishable_supply":{"name":"is_perishable_supply","description":"Whether this supply is perishable","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Is Perishable Supply"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Is Perishable Supply"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"perishable_risk_level":{"name":"perishable_risk_level","description":"Risk level based on perishable supplies","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Perishable Risk Level"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Perishable Risk Level"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_name":{"name":"product_name","description":"Product name, trimmed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_price_dollars":{"name":"product_price_dollars","description":"Product price in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Price Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Price Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Associated product SKU","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"},"dimension":{"type":"string","label":"Product Sku"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"},"dimension":{"type":"string","label":"Product Sku"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_type":{"name":"product_type","description":"Product category (jaffle, beverage)","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Type"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Type"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_cost_dollars":{"name":"supply_cost_dollars","description":"Individual supply cost in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Supply Cost Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Supply Cost Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"products","docs":{"show":true,"node_color":"#059669"},"patch_path":"jaffle_shop://models/marts/products/reporting/mart__products__reporting__cost_efficiency.yml","build_path":null,"unrendered_config":{"materialized":"view","group":"products","tags":["json","lightdash-explore","mart"],"docs":{"node_color":"#059669","show":true},"contract":{"enforced":false}},"created_at":1777900097.983947,"relation_name":"development.jaffle_shop_dev.mart__products__reporting__cost_efficiency","raw_code":"{{\n config(\n materialized=\"view\"\n )\n}}\n\nWITH\n\tmart__products__reporting__cost_efficiency AS (\n\t\tSELECT\n\t\t\tCASE\n\t\t\t\tWHEN cost_dollars / product_price_dollars <= 0.30 THEN 'Highly Efficient'\n\t\t\t\tWHEN cost_dollars / product_price_dollars <= 0.50 THEN 'Efficient'\n\t\t\t\tWHEN cost_dollars / product_price_dollars <= 0.70 THEN 'Moderate'\n\t\t\t\tELSE 'Low Efficiency'\n\t\t\tEND AS cost_efficiency_category,\n\t\t\tCASE\n\t\t\t\tWHEN product_price_dollars > 0 THEN cost_dollars / product_price_dollars\n\t\t\t\tELSE NULL\n\t\t\tEND AS cost_to_price_ratio,\n\t\t\tis_perishable AS is_perishable_supply,\n\t\t\tCASE\n\t\t\t\tWHEN is_perishable THEN 'High Risk'\n\t\t\t\tELSE 'Low Risk'\n\t\t\tEND AS perishable_risk_level,\n\t\t\tportal_source_count,\n\t\t\tproduct_name,\n\t\t\tproduct_price_dollars,\n\t\t\tproduct_sku,\n\t\t\tproduct_type,\n\t\t\tcost_dollars AS supply_cost_dollars\n\t\tFROM\n\t\t\t{{ ref('int__supply_chain__supplies__cost_analysis') }}\n\t)\nSELECT\n\t*\nFROM\n\tmart__products__reporting__cost_efficiency","doc_blocks":[],"language":"sql","refs":[{"name":"int__supply_chain__supplies__cost_analysis","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__supply_chain__supplies__cost_analysis"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.mart__products__reporting__menu_analytics":{"database":"development","schema":"jaffle_shop_dev","name":"mart__products__reporting__menu_analytics","resource_type":"model","package_name":"jaffle_shop","path":"marts/products/reporting/mart__products__reporting__menu_analytics.sql","original_file_path":"models/marts/products/reporting/mart__products__reporting__menu_analytics.sql","unique_id":"model.jaffle_shop.mart__products__reporting__menu_analytics","fqn":["jaffle_shop","marts","products","reporting","mart__products__reporting__menu_analytics"],"alias":"mart__products__reporting__menu_analytics","checksum":{"name":"sha256","checksum":"a0f496985a432632cb87c7fbac4a525d8c9916d1ca1d201849f517601fb4e0fc"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["json","lightdash-explore","mart"],"meta":{},"group":"products","materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#059669"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","lightdash-explore","mart"],"description":"","columns":{"item_id":{"name":"item_id","description":"Unique identifier for each order item","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Item Id"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Item Id"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Product SKU identifier","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Sku"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Sku"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_type":{"name":"product_type","description":"Product type derived from SKU pattern","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Type"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Type"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_items_sold":{"name":"total_items_sold","description":"Total quantity of this product sold","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Items Sold","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Items Sold","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_orders":{"name":"total_orders","description":"Number of unique orders containing this product","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Orders","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Orders","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"products","docs":{"show":true,"node_color":"#059669"},"patch_path":"jaffle_shop://models/marts/products/reporting/mart__products__reporting__menu_analytics.yml","build_path":null,"unrendered_config":{"materialized":"view","group":"products","tags":["json","lightdash-explore","mart"],"docs":{"node_color":"#059669","show":true},"contract":{"enforced":false}},"created_at":1777900097.985304,"relation_name":"development.jaffle_shop_dev.mart__products__reporting__menu_analytics","raw_code":"{{\n config(\n materialized=\"view\"\n )\n}}\n\nWITH\n\tmart__products__reporting__menu_analytics AS (\n\t\tSELECT\n\t\t\titem_id,\n\t\t\tportal_source_count,\n\t\t\tproduct_sku,\n\t\t\tproduct_type,\n\t\t\ttotal_items_sold,\n\t\t\ttotal_orders\n\t\tFROM\n\t\t\t{{ ref('int__products__analytics__product_popularity') }}\n\t)\nSELECT\n\t*\nFROM\n\tmart__products__reporting__menu_analytics","doc_blocks":[],"language":"sql","refs":[{"name":"int__products__analytics__product_popularity","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__products__analytics__product_popularity"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.mart__sales__reporting__revenue":{"database":"development","schema":"jaffle_shop_dev","name":"mart__sales__reporting__revenue","resource_type":"model","package_name":"jaffle_shop","path":"marts/sales/reporting/mart__sales__reporting__revenue.sql","original_file_path":"models/marts/sales/reporting/mart__sales__reporting__revenue.sql","unique_id":"model.jaffle_shop.mart__sales__reporting__revenue","fqn":["jaffle_shop","marts","sales","reporting","mart__sales__reporting__revenue"],"alias":"mart__sales__reporting__revenue","checksum":{"name":"sha256","checksum":"228949d9803f147afdcc929fe95d006fd04b9d4d4464f23ecdf458fcc9c16750"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["json","lightdash-explore","mart"],"meta":{},"group":"sales","materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#059669"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","lightdash-explore","mart"],"description":"","columns":{"customer_id":{"name":"customer_id","description":"Foreign key to customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Customer Id"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Customer Id"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"},"dimension":{"type":"string","label":"Customer Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_weekend_order":{"name":"is_weekend_order","description":"Boolean flag for weekend orders","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Is Weekend Order"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Is Weekend Order"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Order Date"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Order Date"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"Unique identifier for each order","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Order Id"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Order Id"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_month":{"name":"order_month","description":"Order month","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Order Month"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Order Month"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_quarter":{"name":"order_quarter","description":"Order quarter","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Order Quarter"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Order Quarter"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_year":{"name":"order_year","description":"Order year","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Order Year"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Order Year"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"revenue_tier":{"name":"revenue_tier","description":"Revenue tier classification","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Revenue Tier"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Revenue Tier"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Store Id"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Store Id"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"},"dimension":{"type":"string","label":"Store Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"},"dimension":{"type":"string","label":"Store Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"subtotal_dollars":{"name":"subtotal_dollars","description":"Order subtotal in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Subtotal Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Subtotal Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"tax_dollars":{"name":"tax_dollars","description":"Tax amount in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Tax Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Tax Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_dollars":{"name":"total_dollars","description":"Total order amount in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Total Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__revenue"},"dimension":{"type":"string","label":"Total Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":"#059669"},"patch_path":"jaffle_shop://models/marts/sales/reporting/mart__sales__reporting__revenue.yml","build_path":null,"unrendered_config":{"materialized":"view","group":"sales","tags":["json","lightdash-explore","mart"],"docs":{"node_color":"#059669","show":true},"contract":{"enforced":false}},"created_at":1777900097.988441,"relation_name":"development.jaffle_shop_dev.mart__sales__reporting__revenue","raw_code":"{{\n config(\n materialized=\"view\"\n )\n}}\n\nWITH\n\tmart__sales__reporting__revenue AS (\n\t\tSELECT\n\t\t\tcustomer_id,\n\t\t\tcustomer_name,\n\t\t\tday_of_week(order_date) IN (6, 7) AS is_weekend_order,\n\t\t\torder_date,\n\t\t\torder_id,\n\t\t\tEXTRACT(\n\t\t\t\tMONTH\n\t\t\t\tFROM\n\t\t\t\t\torder_date\n\t\t\t) AS order_month,\n\t\t\tEXTRACT(\n\t\t\t\tQUARTER\n\t\t\t\tFROM\n\t\t\t\t\torder_date\n\t\t\t) AS order_quarter,\n\t\t\tEXTRACT(\n\t\t\t\tYEAR\n\t\t\t\tFROM\n\t\t\t\t\torder_date\n\t\t\t) AS order_year,\n\t\t\tportal_source_count,\n\t\t\tCASE\n\t\t\t\tWHEN order_total_dollars >= 15.00 THEN 'High'\n\t\t\t\tWHEN order_total_dollars >= 8.00 THEN 'Medium'\n\t\t\t\tELSE 'Low'\n\t\t\tEND AS revenue_tier,\n\t\t\tstore_id,\n\t\t\tstore_name,\n\t\t\tCAST(subtotal_cents AS DECIMAL(10, 2)) / 100.0 AS subtotal_dollars,\n\t\t\tCAST(tax_paid_cents AS DECIMAL(10, 2)) / 100.0 AS tax_dollars,\n\t\t\torder_total_dollars AS total_dollars\n\t\tFROM\n\t\t\t{{ ref('int__sales__orders__enriched') }}\n\t)\nSELECT\n\t*\nFROM\n\tmart__sales__reporting__revenue","doc_blocks":[],"language":"sql","refs":[{"name":"int__sales__orders__enriched","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__sales__orders__enriched"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.mart__sales__reporting__profitability":{"database":"development","schema":"jaffle_shop_dev","name":"mart__sales__reporting__profitability","resource_type":"model","package_name":"jaffle_shop","path":"marts/sales/reporting/mart__sales__reporting__profitability.sql","original_file_path":"models/marts/sales/reporting/mart__sales__reporting__profitability.sql","unique_id":"model.jaffle_shop.mart__sales__reporting__profitability","fqn":["jaffle_shop","marts","sales","reporting","mart__sales__reporting__profitability"],"alias":"mart__sales__reporting__profitability","checksum":{"name":"sha256","checksum":"9e8829af38433ae219ddae8f68a8a2b4b976f679185a9ddb8b4666f440732cea"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["json","lightdash-explore","mart"],"meta":{},"group":"sales","materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#059669"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","lightdash-explore","mart"],"description":"","columns":{"avg_order_value_dollars":{"name":"avg_order_value_dollars","description":"Average order value in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Avg Order Value Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Avg Order Value Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_count":{"name":"order_count","description":"Number of orders","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Order Count","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Order Count","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Order Date"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Order Date"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_year_month":{"name":"order_year_month","description":"Order year-month","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Order Year Month"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Order Year Month"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"revenue_per_day":{"name":"revenue_per_day","description":"Daily revenue per store","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Revenue Per Day","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Revenue Per Day","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Store Id"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"},"dimension":{"type":"string","label":"Store Id"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"},"dimension":{"type":"string","label":"Store Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"},"dimension":{"type":"string","label":"Store Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_revenue_dollars":{"name":"total_revenue_dollars","description":"Total revenue in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Total Revenue Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Total Revenue Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_tax_collected_dollars":{"name":"total_tax_collected_dollars","description":"Total tax collected in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Total Tax Collected Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__sales__reporting__profitability"},"dimension":{"type":"string","label":"Total Tax Collected Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":"#059669"},"patch_path":"jaffle_shop://models/marts/sales/reporting/mart__sales__reporting__profitability.yml","build_path":null,"unrendered_config":{"materialized":"view","group":"sales","tags":["json","lightdash-explore","mart"],"docs":{"node_color":"#059669","show":true},"contract":{"enforced":false}},"created_at":1777900097.9906409,"relation_name":"development.jaffle_shop_dev.mart__sales__reporting__profitability","raw_code":"{{\n config(\n materialized=\"view\"\n )\n}}\n\nWITH\n\tmart__sales__reporting__profitability AS (\n\t\tSELECT\n\t\t\tAVG(order_total_dollars) AS avg_order_value_dollars,\n\t\t\tCOUNT(*) AS order_count,\n\t\t\torder_date,\n\t\t\tDATE_TRUNC('month', order_date) AS order_year_month,\n\t\t\tsum(portal_source_count) AS portal_source_count,\n\t\t\tSUM(order_total_dollars) AS revenue_per_day,\n\t\t\tstore_id,\n\t\t\tstore_name,\n\t\t\tSUM(order_total_dollars) AS total_revenue_dollars,\n\t\t\tSUM(CAST(tax_paid_cents AS DECIMAL(10, 2)) / 100.0) AS total_tax_collected_dollars\n\t\tFROM\n\t\t\t{{ ref('int__sales__orders__enriched') }}\n\t\tGROUP BY\n\t\t\torder_date,\n\t\t\tDATE_TRUNC('month', order_date),\n\t\t\tstore_id,\n\t\t\tstore_name\n\t)\nSELECT\n\t*\nFROM\n\tmart__sales__reporting__profitability","doc_blocks":[],"language":"sql","refs":[{"name":"int__sales__orders__enriched","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__sales__orders__enriched"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics":{"database":"development","schema":"jaffle_shop_dev","name":"mart__analytics__dashboard__comprehensive_analytics","resource_type":"model","package_name":"jaffle_shop","path":"marts/analytics/dashboard/mart__analytics__dashboard__comprehensive_analytics.sql","original_file_path":"models/marts/analytics/dashboard/mart__analytics__dashboard__comprehensive_analytics.sql","unique_id":"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics","fqn":["jaffle_shop","marts","analytics","dashboard","mart__analytics__dashboard__comprehensive_analytics"],"alias":"mart__analytics__dashboard__comprehensive_analytics","checksum":{"name":"sha256","checksum":"f632e9b79f77dd90892f2e965d5670ebb1d2c56d54de6185c71b086cc4673837"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["json","lightdash-explore","mart"],"meta":{},"group":"analytics","materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#059669"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["json","lightdash-explore","mart"],"description":"","columns":{"business_impact_score":{"name":"business_impact_score","description":"Business impact score based on cost efficiency (0-100)","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics"},"dimension":{"type":"string","label":"Business Impact Score","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics"},"dimension":{"type":"string","label":"Business Impact Score","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"cost_to_price_ratio":{"name":"cost_to_price_ratio","description":"Individual supply cost as ratio of selling price","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Cost To Price Ratio","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Cost To Price Ratio","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_perishable_supply":{"name":"is_perishable_supply","description":"Whether this supply is perishable","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Is Perishable Supply"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__products__reporting__cost_efficiency"},"dimension":{"type":"string","label":"Is Perishable Supply"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"type":"number","label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_name":{"name":"product_name","description":"Product name, trimmed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Name"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Name"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_price_dollars":{"name":"product_price_dollars","description":"Product price in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Price Dollars","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"},"dimension":{"type":"string","label":"Product Price Dollars","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Product SKU identifier","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Sku"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Sku"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_type":{"name":"product_type","description":"Product type derived from SKU pattern","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Type"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"},"dimension":{"type":"string","label":"Product Type"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"strategic_recommendation":{"name":"strategic_recommendation","description":"Strategic business recommendation","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics"},"dimension":{"type":"string","label":"Strategic Recommendation"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics"},"dimension":{"type":"string","label":"Strategic Recommendation"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_items_sold":{"name":"total_items_sold","description":"Total quantity of this product sold","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Items Sold","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Items Sold","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_orders":{"name":"total_orders","description":"Number of unique orders containing this product","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Orders","hidden":true}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"},"dimension":{"type":"string","label":"Total Orders","hidden":true}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"analytics","docs":{"show":true,"node_color":"#059669"},"patch_path":"jaffle_shop://models/marts/analytics/dashboard/mart__analytics__dashboard__comprehensive_analytics.yml","build_path":null,"unrendered_config":{"materialized":"view","group":"analytics","tags":["json","lightdash-explore","mart"],"docs":{"node_color":"#059669","show":true},"contract":{"enforced":false}},"created_at":1777900097.992011,"relation_name":"development.jaffle_shop_dev.mart__analytics__dashboard__comprehensive_analytics","raw_code":"{{\n config(\n materialized=\"view\"\n )\n}}\n\nWITH\n\tmart__analytics__dashboard__comprehensive_analytics AS (\n\t\tSELECT\n\t\t\tCASE\n\t\t\t\tWHEN mart__products__reporting__cost_efficiency.cost_efficiency_category = 'Highly Efficient' THEN 100\n\t\t\t\tWHEN mart__products__reporting__cost_efficiency.cost_efficiency_category = 'Efficient' THEN 85\n\t\t\t\tWHEN mart__products__reporting__cost_efficiency.cost_efficiency_category = 'Moderate' THEN 70\n\t\t\t\tELSE 50\n\t\t\tEND AS business_impact_score,\n\t\t\tmart__products__reporting__cost_efficiency.cost_to_price_ratio,\n\t\t\tmart__products__reporting__cost_efficiency.is_perishable_supply,\n\t\t\tmart__products__reporting__cost_efficiency.portal_source_count,\n\t\t\tmart__products__reporting__cost_efficiency.product_name,\n\t\t\tmart__products__reporting__cost_efficiency.product_price_dollars,\n\t\t\tmart__products__reporting__menu_analytics.product_sku,\n\t\t\tmart__products__reporting__menu_analytics.product_type,\n\t\t\tCASE\n\t\t\t\tWHEN mart__products__reporting__cost_efficiency.cost_efficiency_category = 'Highly Efficient' THEN 'Promote & Expand'\n\t\t\t\tWHEN mart__products__reporting__cost_efficiency.cost_efficiency_category = 'Efficient' THEN 'Monitor Performance'\n\t\t\t\tWHEN mart__products__reporting__cost_efficiency.cost_efficiency_category = 'Moderate' THEN 'Optimize Costs'\n\t\t\t\tELSE 'Review Strategy'\n\t\t\tEND AS strategic_recommendation,\n\t\t\tmart__products__reporting__menu_analytics.total_items_sold,\n\t\t\tmart__products__reporting__menu_analytics.total_orders\n\t\tFROM\n\t\t\t{{ ref('mart__products__reporting__menu_analytics') }} mart__products__reporting__menu_analytics\n\t\t\tLEFT JOIN {{ ref('mart__products__reporting__cost_efficiency') }} mart__products__reporting__cost_efficiency ON mart__products__reporting__menu_analytics.product_sku = mart__products__reporting__cost_efficiency.product_sku\n\t)\nSELECT\n\t*\nFROM\n\tmart__analytics__dashboard__comprehensive_analytics","doc_blocks":[],"language":"sql","refs":[{"name":"mart__products__reporting__menu_analytics","package":null,"version":null},{"name":"mart__products__reporting__cost_efficiency","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.mart__products__reporting__menu_analytics","model.jaffle_shop.mart__products__reporting__cost_efficiency"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.int__customers__profiles__summary":{"database":"development","schema":"jaffle_shop_dev","name":"int__customers__profiles__summary","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/customers/profiles/int__customers__profiles__summary.sql","original_file_path":"models/intermediate/customers/profiles/int__customers__profiles__summary.sql","unique_id":"model.jaffle_shop.int__customers__profiles__summary","fqn":["jaffle_shop","intermediate","customers","profiles","int__customers__profiles__summary"],"alias":"int__customers__profiles__summary","checksum":{"name":"sha256","checksum":"41250064d49f00fabea3a0c5ec7b604be27f73251cb9ef0a86f9950979f380a5"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"customers","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#DAA520"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"","columns":{"customer_first_name":{"name":"customer_first_name","description":"Customer first name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_id":{"name":"customer_id","description":"Unique identifier for each customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_last_name":{"name":"customer_last_name","description":"Customer last name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_segment":{"name":"customer_segment","description":"Customer segment classification based on order count","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"has_last_name":{"name":"has_last_name","description":"Whether customer provided a last name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_orders":{"name":"total_orders","description":"Total number of orders placed by customer","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__customers__profiles__summary"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"customers","docs":{"show":true,"node_color":"#DAA520"},"patch_path":"jaffle_shop://models/intermediate/customers/profiles/int__customers__profiles__summary.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"customers","tags":["intermediate","json"],"docs":{"node_color":"#DAA520","show":true},"contract":{"enforced":false}},"created_at":1777900098.0313191,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__customers__profiles__summary AS (\n\t\tSELECT\n\t\t\tstg__customers__profiles__clean.customer_first_name,\n\t\t\tstg__customers__profiles__clean.customer_id,\n\t\t\tstg__customers__profiles__clean.customer_last_name,\n\t\t\tstg__customers__profiles__clean.customer_name,\n\t\t\t{{ customer_segment() }} AS customer_segment,\n\t\t\tstg__customers__profiles__clean.customer_last_name IS NOT NULL AS has_last_name,\n\t\t\tsum(\n\t\t\t\tstg__customers__profiles__clean.portal_source_count\n\t\t\t) AS portal_source_count,\n\t\t\tCOUNT(stg__sales__orders__standardized.order_id) AS total_orders\n\t\tFROM\n\t\t\t{{ ref('stg__customers__profiles__clean') }} stg__customers__profiles__clean\n\t\t\tLEFT JOIN {{ ref('stg__sales__orders__standardized') }} stg__sales__orders__standardized ON stg__customers__profiles__clean.customer_id = stg__sales__orders__standardized.customer_id\n\t\tGROUP BY\n\t\t\tstg__customers__profiles__clean.customer_first_name,\n\t\t\tstg__customers__profiles__clean.customer_id,\n\t\t\tstg__customers__profiles__clean.customer_last_name,\n\t\t\tstg__customers__profiles__clean.customer_name\n\t)\nSELECT\n\t*\nFROM\n\tint__customers__profiles__summary","doc_blocks":[],"language":"sql","refs":[{"name":"stg__customers__profiles__clean","package":null,"version":null},{"name":"stg__sales__orders__standardized","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":["macro.jaffle_shop.customer_segment"],"nodes":["model.jaffle_shop.stg__customers__profiles__clean","model.jaffle_shop.stg__sales__orders__standardized"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.int__products__analytics__product_popularity":{"database":"development","schema":"jaffle_shop_dev","name":"int__products__analytics__product_popularity","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/products/analytics/int__products__analytics__product_popularity.sql","original_file_path":"models/intermediate/products/analytics/int__products__analytics__product_popularity.sql","unique_id":"model.jaffle_shop.int__products__analytics__product_popularity","fqn":["jaffle_shop","intermediate","products","analytics","int__products__analytics__product_popularity"],"alias":"int__products__analytics__product_popularity","checksum":{"name":"sha256","checksum":"49fea0d39aaeabc54f366cd5764defac0160afc7e2574c87afef171435a4903f"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"products","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#DAA520"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"","columns":{"item_id":{"name":"item_id","description":"Unique identifier for each order item","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Product SKU identifier","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_type":{"name":"product_type","description":"Product type derived from SKU pattern","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__items__order_details"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_items_sold":{"name":"total_items_sold","description":"Total quantity of this product sold","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"total_orders":{"name":"total_orders","description":"Number of unique orders containing this product","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__products__analytics__product_popularity"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"products","docs":{"show":true,"node_color":"#DAA520"},"patch_path":"jaffle_shop://models/intermediate/products/analytics/int__products__analytics__product_popularity.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"products","tags":["intermediate","json"],"docs":{"node_color":"#DAA520","show":true},"contract":{"enforced":false}},"created_at":1777900098.0321128,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__products__analytics__product_popularity AS (\n\t\tSELECT\n\t\t\titem_id,\n\t\t\tsum(portal_source_count) AS portal_source_count,\n\t\t\tproduct_sku,\n\t\t\tproduct_type,\n\t\t\tCOUNT(*) AS total_items_sold,\n\t\t\tCOUNT(DISTINCT order_id) AS total_orders\n\t\tFROM\n\t\t\t{{ ref('stg__sales__items__order_details') }}\n\t\tGROUP BY\n\t\t\titem_id,\n\t\t\tproduct_sku,\n\t\t\tproduct_type\n\t)\nSELECT\n\t*\nFROM\n\tint__products__analytics__product_popularity","doc_blocks":[],"language":"sql","refs":[{"name":"stg__sales__items__order_details","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.stg__sales__items__order_details"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.int__sales__orders__enriched":{"database":"development","schema":"jaffle_shop_dev","name":"int__sales__orders__enriched","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/sales/orders/int__sales__orders__enriched.sql","original_file_path":"models/intermediate/sales/orders/int__sales__orders__enriched.sql","unique_id":"model.jaffle_shop.int__sales__orders__enriched","fqn":["jaffle_shop","intermediate","sales","orders","int__sales__orders__enriched"],"alias":"int__sales__orders__enriched","checksum":{"name":"sha256","checksum":"8caf6828d75ffc47b0d22853b7c3ba56d8b52bc3143ca54ca12bc249c6d068eb"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#DAA520"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"","columns":{"customer_first_name":{"name":"customer_first_name","description":"Customer first name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_id":{"name":"customer_id","description":"Foreign key to customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"Unique identifier for each order","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_cents":{"name":"order_total_cents","description":"Total order amount in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_dollars":{"name":"order_total_dollars","description":"Order total in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"ordered_at":{"name":"ordered_at","description":"Timestamp when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_tax_rate":{"name":"store_tax_rate","description":"Local tax rate for the store","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"subtotal_cents":{"name":"subtotal_cents","description":"Order subtotal in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"tax_paid_cents":{"name":"tax_paid_cents","description":"Tax amount paid in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":"#DAA520"},"patch_path":"jaffle_shop://models/intermediate/sales/orders/int__sales__orders__enriched.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"sales","tags":["intermediate","json"],"docs":{"node_color":"#DAA520","show":true},"contract":{"enforced":false}},"created_at":1777900098.033312,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__sales__orders__enriched AS (\n\t\tSELECT\n\t\t\tstg__customers__profiles__clean.customer_first_name,\n\t\t\tstg__sales__orders__standardized.customer_id,\n\t\t\tstg__customers__profiles__clean.customer_name,\n\t\t\tstg__sales__orders__standardized.order_date,\n\t\t\tstg__sales__orders__standardized.order_id,\n\t\t\tstg__sales__orders__standardized.order_total_cents,\n\t\t\tCAST(\n\t\t\t\tstg__sales__orders__standardized.order_total_cents AS DECIMAL(10, 2)\n\t\t\t) / 100.0 AS order_total_dollars,\n\t\t\tstg__sales__orders__standardized.ordered_at,\n\t\t\tstg__sales__orders__standardized.portal_source_count,\n\t\t\tstg__sales__orders__standardized.store_id,\n\t\t\tstg__sales__stores__locations.store_name,\n\t\t\tstg__sales__stores__locations.store_tax_rate,\n\t\t\tstg__sales__orders__standardized.subtotal_cents,\n\t\t\tstg__sales__orders__standardized.tax_paid_cents\n\t\tFROM\n\t\t\t{{ ref('stg__sales__orders__standardized') }} stg__sales__orders__standardized\n\t\t\tLEFT JOIN {{ ref('stg__customers__profiles__clean') }} stg__customers__profiles__clean ON stg__sales__orders__standardized.customer_id = stg__customers__profiles__clean.customer_id\n\t\t\tLEFT JOIN {{ ref('stg__sales__stores__locations') }} stg__sales__stores__locations ON stg__sales__orders__standardized.store_id = stg__sales__stores__locations.store_id\n\t)\nSELECT\n\t*\nFROM\n\tint__sales__orders__enriched","doc_blocks":[],"language":"sql","refs":[{"name":"stg__sales__orders__standardized","package":null,"version":null},{"name":"stg__customers__profiles__clean","package":null,"version":null},{"name":"stg__sales__stores__locations","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.stg__sales__orders__standardized","model.jaffle_shop.stg__customers__profiles__clean","model.jaffle_shop.stg__sales__stores__locations"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.int__sales__orders__regional_combined":{"database":"development","schema":"jaffle_shop_dev","name":"int__sales__orders__regional_combined","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/sales/orders/int__sales__orders__regional_combined.sql","original_file_path":"models/intermediate/sales/orders/int__sales__orders__regional_combined.sql","unique_id":"model.jaffle_shop.int__sales__orders__regional_combined","fqn":["jaffle_shop","intermediate","sales","orders","int__sales__orders__regional_combined"],"alias":"int__sales__orders__regional_combined","checksum":{"name":"sha256","checksum":"b988eded0f7a248aabaa73ad50962b0a8b6db43ea229e52a98e8232c49dfeeb5"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"Union of orders with regional classification for consolidated reporting across all stores","columns":{"customer_first_name":{"name":"customer_first_name","description":"Customer first name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_id":{"name":"customer_id","description":"Foreign key to customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"Unique identifier for each order","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_cents":{"name":"order_total_cents","description":"Total order amount in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_dollars":{"name":"order_total_dollars","description":"Order total in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"ordered_at":{"name":"ordered_at","description":"Timestamp when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"region":{"name":"region","description":"Geographic region classification","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__sales__orders__regional_combined"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__sales__orders__regional_combined"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_tax_rate":{"name":"store_tax_rate","description":"Local tax rate for the store","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"subtotal_cents":{"name":"subtotal_cents","description":"Order subtotal in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"tax_paid_cents":{"name":"tax_paid_cents","description":"Tax amount paid in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":null},"patch_path":"jaffle_shop://models/intermediate/sales/orders/int__sales__orders__regional_combined.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"sales","tags":["intermediate","json"],"docs":{"show":true},"contract":{"enforced":false}},"created_at":1777900098.034524,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__sales__orders__regional_combined AS (\n\t\tSELECT\n\t\t\tcustomer_first_name,\n\t\t\tcustomer_id,\n\t\t\tcustomer_name,\n\t\t\torder_date,\n\t\t\torder_id,\n\t\t\torder_total_cents,\n\t\t\torder_total_dollars,\n\t\t\tordered_at,\n\t\t\tportal_source_count,\n\t\t\tCASE\n\t\t\t\tWHEN store_name IN ('Philadelphia', 'Brooklyn') THEN 'East Coast'\n\t\t\t\tWHEN store_name IN ('San Francisco', 'Los Angeles') THEN 'West Coast'\n\t\t\t\tELSE 'Other'\n\t\t\tEND AS region,\n\t\t\tstore_id,\n\t\t\tstore_name,\n\t\t\tstore_tax_rate,\n\t\t\tsubtotal_cents,\n\t\t\ttax_paid_cents\n\t\tFROM\n\t\t\t{{ ref('int__sales__orders__enriched') }}\n\t\tUNION ALL\n\t\tSELECT\n\t\t\tcustomer_first_name,\n\t\t\tcustomer_id,\n\t\t\tcustomer_name,\n\t\t\torder_date,\n\t\t\torder_id,\n\t\t\torder_total_cents,\n\t\t\torder_total_dollars,\n\t\t\tordered_at,\n\t\t\tportal_source_count,\n\t\t\tCASE\n\t\t\t\tWHEN store_name IN ('Philadelphia', 'Brooklyn') THEN 'East Coast'\n\t\t\t\tWHEN store_name IN ('San Francisco', 'Los Angeles') THEN 'West Coast'\n\t\t\t\tELSE 'Other'\n\t\t\tEND AS region,\n\t\t\tstore_id,\n\t\t\tstore_name,\n\t\t\tstore_tax_rate,\n\t\t\tsubtotal_cents,\n\t\t\ttax_paid_cents\n\t\tFROM\n\t\t\t{{ ref('int__sales__orders__enriched') }}\n\t)\nSELECT\n\t*\nFROM\n\tint__sales__orders__regional_combined","doc_blocks":[],"language":"sql","refs":[{"name":"int__sales__orders__enriched","package":null,"version":null},{"name":"int__sales__orders__enriched","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__sales__orders__enriched"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.int__sales__analytics__rolling_30day":{"database":"development","schema":"jaffle_shop_dev","name":"int__sales__analytics__rolling_30day","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/sales/analytics/int__sales__analytics__rolling_30day.sql","original_file_path":"models/intermediate/sales/analytics/int__sales__analytics__rolling_30day.sql","unique_id":"model.jaffle_shop.int__sales__analytics__rolling_30day","fqn":["jaffle_shop","intermediate","sales","analytics","int__sales__analytics__rolling_30day"],"alias":"int__sales__analytics__rolling_30day","checksum":{"name":"sha256","checksum":"78f7c85c824c65a077a0a270549607daa259992d89dbaf3c9845a746d4b4f806"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"30-day rolling window analysis of sales metrics partitioned by store for trend analysis","columns":{"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id_count":{"name":"order_id_count","description":"Number of orders in the last 30 days","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"number","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_dollars_sum":{"name":"order_total_dollars_sum","description":"Revenue metrics for the last 30 days (sum, min, max)","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"data_type":"number","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_dollars_min":{"name":"order_total_dollars_min","description":"Revenue metrics for the last 30 days (sum, min, max)","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"data_type":"number","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_dollars_max":{"name":"order_total_dollars_max","description":"Revenue metrics for the last 30 days (sum, min, max)","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"data_type":"number","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_partition_daily":{"name":"portal_partition_daily","description":"Portal Partition Daily","meta":{"type":"dim"},"data_type":"date","constraints":[],"quote":null,"config":{"meta":{"type":"dim"},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":null},"patch_path":"jaffle_shop://models/intermediate/sales/analytics/int__sales__analytics__rolling_30day.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"sales","tags":["intermediate","json"],"docs":{"show":true},"contract":{"enforced":false}},"created_at":1777900098.0362828,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__sales__analytics__rolling_30day AS (\n\t\tSELECT\n\t\t\torder_date,\n\t\t\tcount(order_id) AS order_id_count,\n\t\t\tsum(order_total_dollars) AS order_total_dollars_sum,\n\t\t\tmin(order_total_dollars) AS order_total_dollars_min,\n\t\t\tmax(order_total_dollars) AS order_total_dollars_max,\n\t\t\tstore_id,\n\t\t\tstore_name,\n\t\t\t-- partition columns\n\t\t\t_ext_event_date AS portal_partition_daily\n\t\tFROM\n\t\t\t{{ _ext_event_dates_table() }}\n\t\t\tINNER JOIN {{ ref('int__sales__orders__enriched') }} ON portal_partition_daily <= _ext_event_date\n\t\t\tAND portal_partition_daily >= date_add('day', -30, _ext_event_date)\n\t\tGROUP BY\n\t\t\torder_date,\n\t\t\tstore_id,\n\t\t\tstore_name,\n\t\t\t_ext_event_date\n\t)\nSELECT\n\t*\nFROM\n\tint__sales__analytics__rolling_30day","doc_blocks":[],"language":"sql","refs":[{"name":"int__sales__orders__enriched","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":["macro.jaffle_shop._ext_event_dates_table"],"nodes":["model.jaffle_shop.int__sales__orders__enriched"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.int__sales__analytics__monthly_revenue":{"database":"development","schema":"jaffle_shop_dev","name":"int__sales__analytics__monthly_revenue","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/sales/analytics/int__sales__analytics__monthly_revenue.sql","original_file_path":"models/intermediate/sales/analytics/int__sales__analytics__monthly_revenue.sql","unique_id":"model.jaffle_shop.int__sales__analytics__monthly_revenue","fqn":["jaffle_shop","intermediate","sales","analytics","int__sales__analytics__monthly_revenue"],"alias":"int__sales__analytics__monthly_revenue","checksum":{"name":"sha256","checksum":"cdf26dc4ad7142b7e0725fe845726a2bf53624200f27215c1ca466116e3d137c"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"Monthly rollup of sales data aggregating revenue, orders, and customer metrics by store","columns":{"customer_first_name":{"name":"customer_first_name","description":"Customer first name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_id":{"name":"customer_id","description":"Foreign key to customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"datetime":{"name":"datetime","description":"Datetime","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__sales__analytics__monthly_revenue"},"dimension":{"label":"Datetime","time_intervals":["MONTH","YEAR"]}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__sales__analytics__monthly_revenue"},"dimension":{"label":"Datetime","time_intervals":["MONTH","YEAR"]}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"Unique identifier for each order","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"ordered_at":{"name":"ordered_at","description":"Timestamp when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":null},"patch_path":"jaffle_shop://models/intermediate/sales/analytics/int__sales__analytics__monthly_revenue.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"sales","tags":["intermediate","json"],"docs":{"show":true},"contract":{"enforced":false}},"created_at":1777900098.037596,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__sales__analytics__monthly_revenue AS (\n\t\tSELECT\n\t\t\tcustomer_first_name,\n\t\t\tcustomer_id,\n\t\t\tcustomer_name,\n\t\t\tdate_trunc('month', datetime) AS datetime,\n\t\t\torder_date,\n\t\t\torder_id,\n\t\t\tordered_at,\n\t\t\tsum(portal_source_count) AS portal_source_count,\n\t\t\tstore_id,\n\t\t\tstore_name\n\t\tFROM\n\t\t\t{{ ref('int__sales__orders__enriched') }}\n\t\tGROUP BY\n\t\t\tcustomer_first_name,\n\t\t\tcustomer_id,\n\t\t\tcustomer_name,\n\t\t\tdate_trunc('month', datetime),\n\t\t\torder_date,\n\t\t\torder_id,\n\t\t\tordered_at,\n\t\t\tstore_id,\n\t\t\tstore_name\n\t)\nSELECT\n\t*\nFROM\n\tint__sales__analytics__monthly_revenue","doc_blocks":[],"language":"sql","refs":[{"name":"int__sales__orders__enriched","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__sales__orders__enriched"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"model.jaffle_shop.int__supply_chain__supplies__cost_analysis":{"database":"development","schema":"jaffle_shop_dev","name":"int__supply_chain__supplies__cost_analysis","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/supply_chain/supplies/int__supply_chain__supplies__cost_analysis.sql","original_file_path":"models/intermediate/supply_chain/supplies/int__supply_chain__supplies__cost_analysis.sql","unique_id":"model.jaffle_shop.int__supply_chain__supplies__cost_analysis","fqn":["jaffle_shop","intermediate","supply_chain","supplies","int__supply_chain__supplies__cost_analysis"],"alias":"int__supply_chain__supplies__cost_analysis","checksum":{"name":"sha256","checksum":"c569a2c4c33d074e0dcf3e5c65df4e583b8730580d837f2f8ccb474b59bedfca"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"supply_chain","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":"#DAA520"},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"","columns":{"cost_cents":{"name":"cost_cents","description":"Supply cost in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"cost_dollars":{"name":"cost_dollars","description":"Supply cost in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"cost_tier":{"name":"cost_tier","description":"Supply cost tier classification","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__supply_chain__supplies__cost_analysis"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.int__supply_chain__supplies__cost_analysis"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"is_perishable":{"name":"is_perishable","description":"Boolean flag for perishable supplies","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_name":{"name":"product_name","description":"Product name, trimmed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_price_dollars":{"name":"product_price_dollars","description":"Product price in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_sku":{"name":"product_sku","description":"Associated product SKU","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"product_type":{"name":"product_type","description":"Product category (jaffle, beverage)","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__products__catalog__catalog"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_category":{"name":"supply_category","description":"Supply category classification","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_id":{"name":"supply_id","description":"Unique identifier for each supply item","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_name":{"name":"supply_name","description":"Supply item name, trimmed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__supply_chain__supplies__inventory"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"supply_to_price_ratio":{"name":"supply_to_price_ratio","description":"Supply cost as percentage of product price","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__supply_chain__supplies__cost_analysis"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__supply_chain__supplies__cost_analysis"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"supply_chain","docs":{"show":true,"node_color":"#DAA520"},"patch_path":"jaffle_shop://models/intermediate/supply_chain/supplies/int__supply_chain__supplies__cost_analysis.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"supply_chain","tags":["intermediate","json"],"docs":{"node_color":"#DAA520","show":true},"contract":{"enforced":false}},"created_at":1777900098.038665,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__supply_chain__supplies__cost_analysis AS (\n\t\tSELECT\n\t\t\tstg__supply_chain__supplies__inventory.cost_cents,\n\t\t\tstg__supply_chain__supplies__inventory.cost_dollars,\n\t\t\t{{ cost_tier() }} AS cost_tier,\n\t\t\tstg__supply_chain__supplies__inventory.is_perishable,\n\t\t\tstg__supply_chain__supplies__inventory.portal_source_count,\n\t\t\tstg__products__catalog__catalog.product_name,\n\t\t\tstg__products__catalog__catalog.product_price_dollars,\n\t\t\tstg__supply_chain__supplies__inventory.product_sku,\n\t\t\tstg__products__catalog__catalog.product_type,\n\t\t\tstg__supply_chain__supplies__inventory.supply_category,\n\t\t\tstg__supply_chain__supplies__inventory.supply_id,\n\t\t\tstg__supply_chain__supplies__inventory.supply_name,\n\t\t\t{{ supply_to_price_ratio() }} AS supply_to_price_ratio\n\t\tFROM\n\t\t\t{{ ref('stg__supply_chain__supplies__inventory') }} stg__supply_chain__supplies__inventory\n\t\t\tLEFT JOIN {{ ref('stg__products__catalog__catalog') }} stg__products__catalog__catalog ON stg__supply_chain__supplies__inventory.product_sku = stg__products__catalog__catalog.product_sku\n\t)\nSELECT\n\t*\nFROM\n\tint__supply_chain__supplies__cost_analysis","doc_blocks":[],"language":"sql","refs":[{"name":"stg__supply_chain__supplies__inventory","package":null,"version":null},{"name":"stg__products__catalog__catalog","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":["macro.jaffle_shop.cost_tier","macro.jaffle_shop.supply_to_price_ratio"],"nodes":["model.jaffle_shop.stg__supply_chain__supplies__inventory","model.jaffle_shop.stg__products__catalog__catalog"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":[],"time_spine":null},"seed.jaffle_shop.raw_items":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_items","resource_type":"seed","package_name":"jaffle_shop","path":"raw_items.csv","original_file_path":"seeds/raw_items.csv","unique_id":"seed.jaffle_shop.raw_items","fqn":["jaffle_shop","raw_items"],"alias":"raw_items","checksum":{"name":"path","checksum":"seeds/raw_items.csv"},"config":{"enabled":true,"alias":null,"schema":"seeds","database":null,"tags":[],"meta":{},"group":null,"materialized":"seed","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"delimiter":",","quote_columns":false},"tags":[],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{"quote_columns":false,"schema":"seeds"},"created_at":1777900097.85508,"relation_name":"development.jaffle_shop_dev_seeds.raw_items","raw_code":"","doc_blocks":[],"root_path":"/path/to/dbt/project","depends_on":{"macros":[]}},"seed.jaffle_shop.raw_customers":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_customers","resource_type":"seed","package_name":"jaffle_shop","path":"raw_customers.csv","original_file_path":"seeds/raw_customers.csv","unique_id":"seed.jaffle_shop.raw_customers","fqn":["jaffle_shop","raw_customers"],"alias":"raw_customers","checksum":{"name":"sha256","checksum":"d91ca12deb29112b84ef9b56856fcb69645497bbca8315379dedf61b55b504b1"},"config":{"enabled":true,"alias":null,"schema":"seeds","database":null,"tags":[],"meta":{},"group":null,"materialized":"seed","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"delimiter":",","quote_columns":false},"tags":[],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{"quote_columns":false,"schema":"seeds"},"created_at":1777900097.856012,"relation_name":"development.jaffle_shop_dev_seeds.raw_customers","raw_code":"","doc_blocks":[],"root_path":"/path/to/dbt/project","depends_on":{"macros":[]}},"seed.jaffle_shop.raw_stores":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_stores","resource_type":"seed","package_name":"jaffle_shop","path":"raw_stores.csv","original_file_path":"seeds/raw_stores.csv","unique_id":"seed.jaffle_shop.raw_stores","fqn":["jaffle_shop","raw_stores"],"alias":"raw_stores","checksum":{"name":"sha256","checksum":"3766ca1319b55ea44a16296e24a5faefe26a182b85b0f5142c986482f9801ccc"},"config":{"enabled":true,"alias":null,"schema":"seeds","database":null,"tags":[],"meta":{},"group":null,"materialized":"seed","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"delimiter":",","quote_columns":false},"tags":[],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{"quote_columns":false,"schema":"seeds"},"created_at":1777900097.8566961,"relation_name":"development.jaffle_shop_dev_seeds.raw_stores","raw_code":"","doc_blocks":[],"root_path":"/path/to/dbt/project","depends_on":{"macros":[]}},"seed.jaffle_shop.raw_orders":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_orders","resource_type":"seed","package_name":"jaffle_shop","path":"raw_orders.csv","original_file_path":"seeds/raw_orders.csv","unique_id":"seed.jaffle_shop.raw_orders","fqn":["jaffle_shop","raw_orders"],"alias":"raw_orders","checksum":{"name":"path","checksum":"seeds/raw_orders.csv"},"config":{"enabled":true,"alias":null,"schema":"seeds","database":null,"tags":[],"meta":{},"group":null,"materialized":"seed","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"delimiter":",","quote_columns":false},"tags":[],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{"quote_columns":false,"schema":"seeds"},"created_at":1777900097.8573592,"relation_name":"development.jaffle_shop_dev_seeds.raw_orders","raw_code":"","doc_blocks":[],"root_path":"/path/to/dbt/project","depends_on":{"macros":[]}},"seed.jaffle_shop.raw_supplies":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_supplies","resource_type":"seed","package_name":"jaffle_shop","path":"raw_supplies.csv","original_file_path":"seeds/raw_supplies.csv","unique_id":"seed.jaffle_shop.raw_supplies","fqn":["jaffle_shop","raw_supplies"],"alias":"raw_supplies","checksum":{"name":"sha256","checksum":"da4dedefd0350e023054c208a20bda46ecc4b39e037b42be2ed4c39ed19ddf2d"},"config":{"enabled":true,"alias":null,"schema":"seeds","database":null,"tags":[],"meta":{},"group":null,"materialized":"seed","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"delimiter":",","quote_columns":false},"tags":[],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{"quote_columns":false,"schema":"seeds"},"created_at":1777900097.858015,"relation_name":"development.jaffle_shop_dev_seeds.raw_supplies","raw_code":"","doc_blocks":[],"root_path":"/path/to/dbt/project","depends_on":{"macros":[]}},"seed.jaffle_shop.raw_products":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_products","resource_type":"seed","package_name":"jaffle_shop","path":"raw_products.csv","original_file_path":"seeds/raw_products.csv","unique_id":"seed.jaffle_shop.raw_products","fqn":["jaffle_shop","raw_products"],"alias":"raw_products","checksum":{"name":"sha256","checksum":"a272e2f54aea0708c26ea3d8fb0cc6062196b68c8e47ab6197426c7cd3f841d8"},"config":{"enabled":true,"alias":null,"schema":"seeds","database":null,"tags":[],"meta":{},"group":null,"materialized":"seed","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"delimiter":",","quote_columns":false},"tags":[],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{"quote_columns":false,"schema":"seeds"},"created_at":1777900097.858969,"relation_name":"development.jaffle_shop_dev_seeds.raw_products","raw_code":"","doc_blocks":[],"root_path":"/path/to/dbt/project","depends_on":{"macros":[]}},"operation.jaffle_shop.jaffle_shop-on-run-start-0":{"database":"development","schema":"jaffle_shop_dev","name":"jaffle_shop-on-run-start-0","resource_type":"operation","package_name":"jaffle_shop","path":"hooks/jaffle_shop-on-run-start-0.sql","original_file_path":"./dbt_project.yml","unique_id":"operation.jaffle_shop.jaffle_shop-on-run-start-0","fqn":["jaffle_shop","hooks","jaffle_shop-on-run-start-0"],"alias":"jaffle_shop-on-run-start-0","checksum":{"name":"sha256","checksum":"f4d5f4fec0c794219b54d382a7559fbbf9dc34179f018f10c31c6973d773da01"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":[],"meta":{},"group":null,"materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null},"tags":["on-run-start"],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{},"created_at":1777900097.912394,"relation_name":null,"raw_code":"{{ log('Run started: ' ~ run_started_at, info=True) }}","doc_blocks":[],"language":"sql","refs":[],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":[]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"index":0},"operation.jaffle_shop.jaffle_shop-on-run-end-0":{"database":"development","schema":"jaffle_shop_dev","name":"jaffle_shop-on-run-end-0","resource_type":"operation","package_name":"jaffle_shop","path":"hooks/jaffle_shop-on-run-end-0.sql","original_file_path":"./dbt_project.yml","unique_id":"operation.jaffle_shop.jaffle_shop-on-run-end-0","fqn":["jaffle_shop","hooks","jaffle_shop-on-run-end-0"],"alias":"jaffle_shop-on-run-end-0","checksum":{"name":"sha256","checksum":"f4d5f4fec0c794219b54d382a7559fbbf9dc34179f018f10c31c6973d773da01"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":[],"meta":{},"group":null,"materialized":"view","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null},"tags":["on-run-end"],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{},"created_at":1777900097.916812,"relation_name":null,"raw_code":"{{ log('Run finished: ' ~ run_finished_at, info=True) }}","doc_blocks":[],"language":"sql","refs":[],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":[]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"index":0},"model.jaffle_shop.int__sales__orders__quality_checks":{"database":"development","schema":"jaffle_shop_dev","name":"int__sales__orders__quality_checks","resource_type":"model","package_name":"jaffle_shop","path":"intermediate/sales/orders/int__sales__orders__quality_checks.sql","original_file_path":"models/intermediate/sales/orders/int__sales__orders__quality_checks.sql","unique_id":"model.jaffle_shop.int__sales__orders__quality_checks","fqn":["jaffle_shop","intermediate","sales","orders","int__sales__orders__quality_checks"],"alias":"int__sales__orders__quality_checks","checksum":{"name":"sha256","checksum":"d0ffc5ccfc3c6fd77f16ed08874b92d413a30a2730aafc6ff4a26c1239cc08dd"},"config":{"enabled":true,"alias":null,"schema":null,"database":null,"tags":["intermediate","json"],"meta":{},"group":"sales","materialized":"ephemeral","incremental_strategy":null,"batch_size":null,"lookback":1,"begin":null,"persist_docs":{},"post-hook":[],"pre-hook":[],"quoting":{},"column_types":{},"full_refresh":null,"unique_key":null,"on_schema_change":"ignore","on_configuration_change":"apply","grants":{},"packages":[],"docs":{"show":true,"node_color":null},"contract":{"enforced":false,"alias_types":true},"event_time":null,"concurrent_batches":null,"access":"protected","freshness":null},"tags":["intermediate","json"],"description":"Demonstrates DJ test abstractions: data_tests with config, singular_tests, and unit_tests","columns":{"customer_first_name":{"name":"customer_first_name","description":"Customer first name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_id":{"name":"customer_id","description":"Foreign key to customer","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer_name":{"name":"customer_name","description":"Customer full name, trimmed of whitespace","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__customers__profiles__clean"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_date":{"name":"order_date","description":"Date when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"Unique identifier for each order","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_cents":{"name":"order_total_cents","description":"Total order amount in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total_dollars":{"name":"order_total_dollars","description":"Order total in dollars","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.int__sales__orders__enriched"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"ordered_at":{"name":"ordered_at","description":"Timestamp when order was placed","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"portal_source_count":{"name":"portal_source_count","description":"Portal Source Count","meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"data_type":"bigint","constraints":[],"quote":null,"config":{"meta":{"type":"fct","dimension":{"label":"Portal Source Count","hidden":true},"metrics":{"metric_portal_source_count":{"type":"sum","label":"Portal Source Count"}}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"Foreign key to store","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_name":{"name":"store_name","description":"Store location name","meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"dim","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_tax_rate":{"name":"store_tax_rate","description":"Local tax rate for the store","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__stores__locations"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"subtotal_cents":{"name":"subtotal_cents","description":"Order subtotal in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"tax_paid_cents":{"name":"tax_paid_cents","description":"Tax amount paid in cents","meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{"type":"fct","origin":{"id":"model.jaffle_shop.stg__sales__orders__standardized"}},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"group":"sales","docs":{"show":true,"node_color":null},"patch_path":"jaffle_shop://models/intermediate/sales/orders/int__sales__orders__quality_checks.yml","build_path":null,"unrendered_config":{"materialized":"ephemeral","group":"sales","tags":["intermediate","json"],"docs":{"show":true},"contract":{"enforced":false}},"created_at":1777900913.3752708,"relation_name":null,"raw_code":"{{\n config(\n materialized=\"ephemeral\"\n )\n}}\n\nWITH\n\tint__sales__orders__quality_checks AS (\n\t\tSELECT\n\t\t\tcustomer_first_name,\n\t\t\tcustomer_id,\n\t\t\tcustomer_name,\n\t\t\torder_date,\n\t\t\torder_id,\n\t\t\torder_total_cents,\n\t\t\torder_total_dollars,\n\t\t\tordered_at,\n\t\t\tportal_source_count,\n\t\t\tstore_id,\n\t\t\tstore_name,\n\t\t\tstore_tax_rate,\n\t\t\tsubtotal_cents,\n\t\t\ttax_paid_cents\n\t\tFROM\n\t\t\t{{ ref('int__sales__orders__enriched') }}\n\t)\nSELECT\n\t*\nFROM\n\tint__sales__orders__quality_checks","doc_blocks":[],"language":"sql","refs":[{"name":"int__sales__orders__enriched","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__sales__orders__enriched"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"access":"protected","constraints":[],"version":null,"latest_version":null,"deprecation_date":null,"primary_key":["order_id"],"time_spine":null},"test.jaffle_shop.test_positive_order_totals":{"database":"development","schema":"jaffle_shop_dev_dbt_test__audit","name":"test_positive_order_totals","resource_type":"test","package_name":"jaffle_shop","path":"singular/test_positive_order_totals.sql","original_file_path":"tests/singular/test_positive_order_totals.sql","unique_id":"test.jaffle_shop.test_positive_order_totals","fqn":["jaffle_shop","singular","test_positive_order_totals"],"alias":"test_positive_order_totals","checksum":{"name":"sha256","checksum":"1ba41ea1ae0f4a02c5f1e5864cd3446b4f2f8d2ef87987325bd91a0410aa7729"},"config":{"enabled":true,"alias":null,"schema":"dbt_test__audit","database":null,"tags":["data_quality"],"meta":{},"group":null,"materialized":"test","severity":"error","store_failures":true,"store_failures_as":"table","where":null,"limit":null,"fail_calc":"count(*)","warn_if":"!= 0","error_if":"!= 0"},"tags":["data_quality"],"description":"","columns":{},"meta":{},"group":null,"docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{"store_failures":true,"severity":"error","tags":["data_quality"]},"created_at":1777900913.323618,"relation_name":"development.jaffle_shop_dev_dbt_test__audit.test_positive_order_totals","raw_code":"/*\n * test_positive_order_totals\n * \n * Ensure all order totals are positive\n * \n * @dj-owner: int__sales__orders__quality_checks\n * @generated: DJ Framework - Do not edit this metadata line\n */\n\n{{ config(\n severity=\"error\",\n tags=[\"data_quality\"]\n) }}\n\nselect order_id, order_total_dollars from {{ ref('int__sales__orders__quality_checks') }} where order_total_dollars <= 0","doc_blocks":[],"language":"sql","refs":[{"name":"int__sales__orders__quality_checks","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__sales__orders__quality_checks"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null}},"test.jaffle_shop.unique_int__sales__orders__quality_checks_order_id.ca17cad549":{"database":"development","schema":"jaffle_shop_dev_dbt_test__audit","name":"unique_int__sales__orders__quality_checks_order_id","resource_type":"test","package_name":"jaffle_shop","path":"unique_int__sales__orders__quality_checks_order_id.sql","original_file_path":"models/intermediate/sales/orders/int__sales__orders__quality_checks.yml","unique_id":"test.jaffle_shop.unique_int__sales__orders__quality_checks_order_id.ca17cad549","fqn":["jaffle_shop","intermediate","sales","orders","unique_int__sales__orders__quality_checks_order_id"],"alias":"unique_int__sales__orders__quality_checks_order_id","checksum":{"name":"none","checksum":""},"config":{"enabled":true,"alias":null,"schema":"dbt_test__audit","database":null,"tags":["critical"],"meta":{},"group":null,"materialized":"test","severity":"error","store_failures":true,"store_failures_as":"table","where":null,"limit":null,"fail_calc":"count(*)","warn_if":"!= 0","error_if":"!= 0"},"tags":["critical"],"description":"","columns":{},"meta":{},"group":"sales","docs":{"show":true,"node_color":null},"patch_path":null,"build_path":null,"unrendered_config":{"store_failures":true,"severity":"error","tags":["critical"]},"created_at":1777900913.406522,"relation_name":"development.jaffle_shop_dev_dbt_test__audit.unique_int__sales__orders__quality_checks_order_id","raw_code":"{{ test_unique(**_dbt_generic_test_kwargs) }}{{ config(severity=\"error\",tags=['critical']) }}","doc_blocks":[],"language":"sql","refs":[{"name":"int__sales__orders__quality_checks","package":null,"version":null}],"sources":[],"metrics":[],"depends_on":{"macros":["macro.dbt.test_unique"],"nodes":["model.jaffle_shop.int__sales__orders__quality_checks"]},"compiled_path":null,"contract":{"enforced":false,"alias_types":true,"checksum":null},"column_name":null,"file_key_name":"models.int__sales__orders__quality_checks","attached_node":"model.jaffle_shop.int__sales__orders__quality_checks","test_metadata":{"name":"unique","kwargs":{"column_name":"order_id","model":"{{ get_where_subquery(ref('int__sales__orders__quality_checks')) }}"},"namespace":null}}},"sources":{"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_customers":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_customers","resource_type":"source","package_name":"jaffle_shop","path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","original_file_path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","unique_id":"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_customers","fqn":["jaffle_shop","sources","development","development__jaffle_shop_dev_seeds","raw_customers"],"source_name":"development__jaffle_shop_dev_seeds","source_description":"","loader":"","identifier":"raw_customers","quoting":{"database":null,"schema":null,"identifier":null,"column":null},"loaded_at_field":null,"loaded_at_query":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"external":null,"description":"","columns":{"id":{"name":"id","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"name":{"name":"name","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"source_meta":{},"tags":[],"config":{"enabled":true,"event_time":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"patch_path":null,"unrendered_config":{"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"relation_name":"development.jaffle_shop_dev_seeds.raw_customers","created_at":1777900098.047325,"unrendered_database":"development","unrendered_schema":"jaffle_shop_dev_seeds","doc_blocks":[]},"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_items":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_items","resource_type":"source","package_name":"jaffle_shop","path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","original_file_path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","unique_id":"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_items","fqn":["jaffle_shop","sources","development","development__jaffle_shop_dev_seeds","raw_items"],"source_name":"development__jaffle_shop_dev_seeds","source_description":"","loader":"","identifier":"raw_items","quoting":{"database":null,"schema":null,"identifier":null,"column":null},"loaded_at_field":null,"loaded_at_query":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"external":null,"description":"","columns":{"id":{"name":"id","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_id":{"name":"order_id","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"sku":{"name":"sku","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"source_meta":{},"tags":[],"config":{"enabled":true,"event_time":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"patch_path":null,"unrendered_config":{"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"relation_name":"development.jaffle_shop_dev_seeds.raw_items","created_at":1777900098.0480871,"unrendered_database":"development","unrendered_schema":"jaffle_shop_dev_seeds","doc_blocks":[]},"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_orders":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_orders","resource_type":"source","package_name":"jaffle_shop","path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","original_file_path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","unique_id":"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_orders","fqn":["jaffle_shop","sources","development","development__jaffle_shop_dev_seeds","raw_orders"],"source_name":"development__jaffle_shop_dev_seeds","source_description":"","loader":"","identifier":"raw_orders","quoting":{"database":null,"schema":null,"identifier":null,"column":null},"loaded_at_field":null,"loaded_at_query":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"external":null,"description":"","columns":{"id":{"name":"id","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"customer":{"name":"customer","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"ordered_at":{"name":"ordered_at","description":"","meta":{},"data_type":"timestamp(3)","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"store_id":{"name":"store_id","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"subtotal":{"name":"subtotal","description":"","meta":{},"data_type":"integer","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"tax_paid":{"name":"tax_paid","description":"","meta":{},"data_type":"integer","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"order_total":{"name":"order_total","description":"","meta":{},"data_type":"integer","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"source_meta":{},"tags":[],"config":{"enabled":true,"event_time":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"patch_path":null,"unrendered_config":{"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"relation_name":"development.jaffle_shop_dev_seeds.raw_orders","created_at":1777900098.04828,"unrendered_database":"development","unrendered_schema":"jaffle_shop_dev_seeds","doc_blocks":[]},"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_products":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_products","resource_type":"source","package_name":"jaffle_shop","path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","original_file_path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","unique_id":"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_products","fqn":["jaffle_shop","sources","development","development__jaffle_shop_dev_seeds","raw_products"],"source_name":"development__jaffle_shop_dev_seeds","source_description":"","loader":"","identifier":"raw_products","quoting":{"database":null,"schema":null,"identifier":null,"column":null},"loaded_at_field":null,"loaded_at_query":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"external":null,"description":"","columns":{"sku":{"name":"sku","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"name":{"name":"name","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"type":{"name":"type","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"price":{"name":"price","description":"","meta":{},"data_type":"integer","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"description":{"name":"description","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"source_meta":{},"tags":[],"config":{"enabled":true,"event_time":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"patch_path":null,"unrendered_config":{"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"relation_name":"development.jaffle_shop_dev_seeds.raw_products","created_at":1777900098.048457,"unrendered_database":"development","unrendered_schema":"jaffle_shop_dev_seeds","doc_blocks":[]},"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_stores":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_stores","resource_type":"source","package_name":"jaffle_shop","path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","original_file_path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","unique_id":"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_stores","fqn":["jaffle_shop","sources","development","development__jaffle_shop_dev_seeds","raw_stores"],"source_name":"development__jaffle_shop_dev_seeds","source_description":"","loader":"","identifier":"raw_stores","quoting":{"database":null,"schema":null,"identifier":null,"column":null},"loaded_at_field":null,"loaded_at_query":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"external":null,"description":"","columns":{"id":{"name":"id","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"name":{"name":"name","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"opened_at":{"name":"opened_at","description":"","meta":{},"data_type":"timestamp(3)","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"tax_rate":{"name":"tax_rate","description":"","meta":{},"data_type":"double","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"source_meta":{},"tags":[],"config":{"enabled":true,"event_time":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"patch_path":null,"unrendered_config":{"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"relation_name":"development.jaffle_shop_dev_seeds.raw_stores","created_at":1777900098.04862,"unrendered_database":"development","unrendered_schema":"jaffle_shop_dev_seeds","doc_blocks":[]},"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_supplies":{"database":"development","schema":"jaffle_shop_dev_seeds","name":"raw_supplies","resource_type":"source","package_name":"jaffle_shop","path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","original_file_path":"models/sources/development/development__jaffle_shop_dev_seeds.yml","unique_id":"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_supplies","fqn":["jaffle_shop","sources","development","development__jaffle_shop_dev_seeds","raw_supplies"],"source_name":"development__jaffle_shop_dev_seeds","source_description":"","loader":"","identifier":"raw_supplies","quoting":{"database":null,"schema":null,"identifier":null,"column":null},"loaded_at_field":null,"loaded_at_query":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"external":null,"description":"","columns":{"id":{"name":"id","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"name":{"name":"name","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"cost":{"name":"cost","description":"","meta":{},"data_type":"integer","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"perishable":{"name":"perishable","description":"","meta":{},"data_type":"boolean","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]},"sku":{"name":"sku","description":"","meta":{},"data_type":"varchar","constraints":[],"quote":null,"config":{"meta":{},"tags":[]},"tags":[],"granularity":null,"doc_blocks":[]}},"meta":{},"source_meta":{},"tags":[],"config":{"enabled":true,"event_time":null,"freshness":{"warn_after":{"count":null,"period":null},"error_after":{"count":null,"period":null},"filter":null},"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"patch_path":null,"unrendered_config":{"loaded_at_field":null,"loaded_at_query":null,"meta":{},"tags":[]},"relation_name":"development.jaffle_shop_dev_seeds.raw_supplies","created_at":1777900098.048795,"unrendered_database":"development","unrendered_schema":"jaffle_shop_dev_seeds","doc_blocks":[]}},"docs":{"doc.dbt.__overview__":{"name":"__overview__","resource_type":"doc","package_name":"dbt","path":"overview.md","original_file_path":"docs/overview.md","unique_id":"doc.dbt.__overview__","block_contents":"### Welcome!\n\nWelcome to the auto-generated documentation for your dbt project!\n\n### Navigation\n\nYou can use the `Project` and `Database` navigation tabs on the left side of the window to explore the models\nin your project.\n\n#### Project Tab\nThe `Project` tab mirrors the directory structure of your dbt project. In this tab, you can see all of the\nmodels defined in your dbt project, as well as models imported from dbt packages.\n\n#### Database Tab\nThe `Database` tab also exposes your models, but in a format that looks more like a database explorer. This view\nshows relations (tables and views) grouped into database schemas. Note that ephemeral models are _not_ shown\nin this interface, as they do not exist in the database.\n\n### Graph Exploration\nYou can click the blue icon on the bottom-right corner of the page to view the lineage graph of your models.\n\nOn model pages, you'll see the immediate parents and children of the model you're exploring. By clicking the `Expand`\nbutton at the top-right of this lineage pane, you'll be able to see all of the models that are used to build,\nor are built from, the model you're exploring.\n\nOnce expanded, you'll be able to use the `--select` and `--exclude` model selection syntax to filter the\nmodels in the graph. For more information on model selection, check out the [dbt docs](https://docs.getdbt.com/docs/model-selection-syntax).\n\nNote that you can also right-click on models to interactively filter and explore the graph.\n\n---\n\n### More information\n\n- [What is dbt](https://docs.getdbt.com/docs/introduction)?\n- Read the [dbt viewpoint](https://docs.getdbt.com/docs/viewpoint)\n- [Installation](https://docs.getdbt.com/docs/installation)\n- Join the [dbt Community](https://www.getdbt.com/community/) for questions and discussion"}},"exposures":{},"metrics":{},"groups":{"group.jaffle_shop.sales":{"name":"sales","resource_type":"group","package_name":"jaffle_shop","path":"groups.yml","original_file_path":"models/groups.yml","unique_id":"group.jaffle_shop.sales","owner":{"email":"sales@example.com","name":"Sales Team"},"description":"Order processing, revenue tracking, profitability analysis, and sales performance metrics","config":{"meta":{}}},"group.jaffle_shop.products":{"name":"products","resource_type":"group","package_name":"jaffle_shop","path":"groups.yml","original_file_path":"models/groups.yml","unique_id":"group.jaffle_shop.products","owner":{"email":"product@example.com","name":"Product Team"},"description":"Product catalog management, menu analytics, cost efficiency analysis, and supply chain optimization","config":{"meta":{}}},"group.jaffle_shop.customers":{"name":"customers","resource_type":"group","package_name":"jaffle_shop","path":"groups.yml","original_file_path":"models/groups.yml","unique_id":"group.jaffle_shop.customers","owner":{"email":"customers@example.com","name":"Customer Team"},"description":"Customer profiles, behavior analysis, segmentation, and customer-focused dashboards","config":{"meta":{}}},"group.jaffle_shop.supply_chain":{"name":"supply_chain","resource_type":"group","package_name":"jaffle_shop","path":"groups.yml","original_file_path":"models/groups.yml","unique_id":"group.jaffle_shop.supply_chain","owner":{"email":"supply-chain@example.com","name":"Supply Chain Team"},"description":"Supply cost analysis, inventory management, and supplier performance tracking","config":{"meta":{}}},"group.jaffle_shop.analytics":{"name":"analytics","resource_type":"group","package_name":"jaffle_shop","path":"groups.yml","original_file_path":"models/groups.yml","unique_id":"group.jaffle_shop.analytics","owner":{"email":"analytics@example.com","name":"Analytics Team"},"description":"Cross-functional business intelligence, comprehensive analytics, and strategic insights across all business areas","config":{"meta":{}}}},"selectors":{},"disabled":{},"parent_map":{"model.jaffle_shop.stg__customers__profiles__clean":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_customers"],"model.jaffle_shop.stg__products__catalog__catalog":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_products"],"model.jaffle_shop.stg__sales__stores__locations":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_stores"],"model.jaffle_shop.stg__sales__orders__standardized":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_orders"],"model.jaffle_shop.stg__sales__items__order_details":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_items"],"model.jaffle_shop.stg__supply_chain__supplies__inventory":["source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_supplies"],"model.jaffle_shop.mart__customers__dashboard__analytics":["model.jaffle_shop.int__customers__profiles__summary"],"model.jaffle_shop.mart__products__reporting__cost_efficiency":["model.jaffle_shop.int__supply_chain__supplies__cost_analysis"],"model.jaffle_shop.mart__products__reporting__menu_analytics":["model.jaffle_shop.int__products__analytics__product_popularity"],"model.jaffle_shop.mart__sales__reporting__revenue":["model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.mart__sales__reporting__profitability":["model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics":["model.jaffle_shop.mart__products__reporting__cost_efficiency","model.jaffle_shop.mart__products__reporting__menu_analytics"],"model.jaffle_shop.int__customers__profiles__summary":["model.jaffle_shop.stg__customers__profiles__clean","model.jaffle_shop.stg__sales__orders__standardized"],"model.jaffle_shop.int__products__analytics__product_popularity":["model.jaffle_shop.stg__sales__items__order_details"],"model.jaffle_shop.int__sales__orders__enriched":["model.jaffle_shop.stg__customers__profiles__clean","model.jaffle_shop.stg__sales__orders__standardized","model.jaffle_shop.stg__sales__stores__locations"],"model.jaffle_shop.int__sales__orders__regional_combined":["model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.int__sales__analytics__rolling_30day":["model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.int__sales__analytics__monthly_revenue":["model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.int__supply_chain__supplies__cost_analysis":["model.jaffle_shop.stg__products__catalog__catalog","model.jaffle_shop.stg__supply_chain__supplies__inventory"],"seed.jaffle_shop.raw_items":[],"seed.jaffle_shop.raw_customers":[],"seed.jaffle_shop.raw_stores":[],"seed.jaffle_shop.raw_orders":[],"seed.jaffle_shop.raw_supplies":[],"seed.jaffle_shop.raw_products":[],"operation.jaffle_shop.jaffle_shop-on-run-start-0":[],"operation.jaffle_shop.jaffle_shop-on-run-end-0":[],"model.jaffle_shop.int__sales__orders__quality_checks":["model.jaffle_shop.int__sales__orders__enriched"],"test.jaffle_shop.test_positive_order_totals":["model.jaffle_shop.int__sales__orders__quality_checks"],"test.jaffle_shop.unique_int__sales__orders__quality_checks_order_id.ca17cad549":["model.jaffle_shop.int__sales__orders__quality_checks"],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_customers":[],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_items":[],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_orders":[],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_products":[],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_stores":[],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_supplies":[],"unit_test.jaffle_shop.int__sales__orders__quality_checks.test_order_logic":["model.jaffle_shop.int__sales__orders__quality_checks"]},"child_map":{"model.jaffle_shop.stg__customers__profiles__clean":["model.jaffle_shop.int__customers__profiles__summary","model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.stg__products__catalog__catalog":["model.jaffle_shop.int__supply_chain__supplies__cost_analysis"],"model.jaffle_shop.stg__sales__stores__locations":["model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.stg__sales__orders__standardized":["model.jaffle_shop.int__customers__profiles__summary","model.jaffle_shop.int__sales__orders__enriched"],"model.jaffle_shop.stg__sales__items__order_details":["model.jaffle_shop.int__products__analytics__product_popularity"],"model.jaffle_shop.stg__supply_chain__supplies__inventory":["model.jaffle_shop.int__supply_chain__supplies__cost_analysis"],"model.jaffle_shop.mart__customers__dashboard__analytics":[],"model.jaffle_shop.mart__products__reporting__cost_efficiency":["model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics"],"model.jaffle_shop.mart__products__reporting__menu_analytics":["model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics"],"model.jaffle_shop.mart__sales__reporting__revenue":[],"model.jaffle_shop.mart__sales__reporting__profitability":[],"model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics":[],"model.jaffle_shop.int__customers__profiles__summary":["model.jaffle_shop.mart__customers__dashboard__analytics"],"model.jaffle_shop.int__products__analytics__product_popularity":["model.jaffle_shop.mart__products__reporting__menu_analytics"],"model.jaffle_shop.int__sales__orders__enriched":["model.jaffle_shop.int__sales__analytics__monthly_revenue","model.jaffle_shop.int__sales__analytics__rolling_30day","model.jaffle_shop.int__sales__orders__quality_checks","model.jaffle_shop.int__sales__orders__regional_combined","model.jaffle_shop.mart__sales__reporting__profitability","model.jaffle_shop.mart__sales__reporting__revenue"],"model.jaffle_shop.int__sales__orders__regional_combined":[],"model.jaffle_shop.int__sales__analytics__rolling_30day":[],"model.jaffle_shop.int__sales__analytics__monthly_revenue":[],"model.jaffle_shop.int__supply_chain__supplies__cost_analysis":["model.jaffle_shop.mart__products__reporting__cost_efficiency"],"seed.jaffle_shop.raw_items":[],"seed.jaffle_shop.raw_customers":[],"seed.jaffle_shop.raw_stores":[],"seed.jaffle_shop.raw_orders":[],"seed.jaffle_shop.raw_supplies":[],"seed.jaffle_shop.raw_products":[],"operation.jaffle_shop.jaffle_shop-on-run-start-0":[],"operation.jaffle_shop.jaffle_shop-on-run-end-0":[],"model.jaffle_shop.int__sales__orders__quality_checks":["test.jaffle_shop.test_positive_order_totals","test.jaffle_shop.unique_int__sales__orders__quality_checks_order_id.ca17cad549","unit_test.jaffle_shop.int__sales__orders__quality_checks.test_order_logic"],"test.jaffle_shop.test_positive_order_totals":[],"test.jaffle_shop.unique_int__sales__orders__quality_checks_order_id.ca17cad549":[],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_customers":["model.jaffle_shop.stg__customers__profiles__clean"],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_items":["model.jaffle_shop.stg__sales__items__order_details"],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_orders":["model.jaffle_shop.stg__sales__orders__standardized"],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_products":["model.jaffle_shop.stg__products__catalog__catalog"],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_stores":["model.jaffle_shop.stg__sales__stores__locations"],"source.jaffle_shop.development__jaffle_shop_dev_seeds.raw_supplies":["model.jaffle_shop.stg__supply_chain__supplies__inventory"],"unit_test.jaffle_shop.int__sales__orders__quality_checks.test_order_logic":[]},"group_map":{"sales":["model.jaffle_shop.stg__sales__stores__locations","model.jaffle_shop.stg__sales__orders__standardized","model.jaffle_shop.stg__sales__items__order_details","model.jaffle_shop.mart__sales__reporting__revenue","model.jaffle_shop.mart__sales__reporting__profitability","model.jaffle_shop.int__sales__orders__enriched","model.jaffle_shop.int__sales__orders__regional_combined","model.jaffle_shop.int__sales__analytics__rolling_30day","model.jaffle_shop.int__sales__analytics__monthly_revenue","model.jaffle_shop.int__sales__orders__quality_checks","test.jaffle_shop.unique_int__sales__orders__quality_checks_order_id.ca17cad549"],"products":["model.jaffle_shop.stg__products__catalog__catalog","model.jaffle_shop.mart__products__reporting__cost_efficiency","model.jaffle_shop.mart__products__reporting__menu_analytics","model.jaffle_shop.int__products__analytics__product_popularity"],"customers":["model.jaffle_shop.stg__customers__profiles__clean","model.jaffle_shop.mart__customers__dashboard__analytics","model.jaffle_shop.int__customers__profiles__summary"],"supply_chain":["model.jaffle_shop.stg__supply_chain__supplies__inventory","model.jaffle_shop.int__supply_chain__supplies__cost_analysis"],"analytics":["model.jaffle_shop.mart__analytics__dashboard__comprehensive_analytics"]},"saved_queries":{},"semantic_models":{},"unit_tests":{"unit_test.jaffle_shop.int__sales__orders__quality_checks.test_order_logic":{"model":"int__sales__orders__quality_checks","given":[{"input":"ref('int__sales__orders__enriched')","rows":[{"order_id":"1","order_total_dollars":"100"}],"format":"dict","fixture":null}],"expect":{"rows":[{"order_id":"1"}],"format":"dict","fixture":null},"name":"test_order_logic","resource_type":"unit_test","package_name":"jaffle_shop","path":"unit/int__sales__orders__quality_checks.unit.yml","original_file_path":"tests/unit/int__sales__orders__quality_checks.unit.yml","unique_id":"unit_test.jaffle_shop.int__sales__orders__quality_checks.test_order_logic","fqn":["jaffle_shop","unit","int__sales__orders__quality_checks","test_order_logic"],"description":"Test order processing","overrides":null,"depends_on":{"macros":[],"nodes":["model.jaffle_shop.int__sales__orders__quality_checks"]},"config":{"tags":[],"meta":{},"enabled":true},"checksum":"a9962bf1a87173e84124f1ec709d770a3d009dea1afaadc65e44a9b0fd82722a","schema":"jaffle_shop_dev","created_at":1777900913.346285,"versions":null,"version":null}}} \ No newline at end of file diff --git a/web/src/context/app.tsx b/web/src/context/app.tsx index 20ab471..bee3190 100644 --- a/web/src/context/app.tsx +++ b/web/src/context/app.tsx @@ -14,7 +14,7 @@ import { LightdashPreviewManager } from '@web/pages/LightdashPreviewManager'; import { ModelCreate } from '@web/pages/ModelCreate'; import { ModelRun } from '@web/pages/ModelRun'; import { ModelTest } from '@web/pages/ModelTest'; -import { QueryView } from '@web/pages/QueryView'; +import { QueryControlCenter } from '@web/pages/QueryControlCenter'; import { SourceCreate } from '@web/pages/SourceCreate'; import { useCallback, useMemo, useState } from 'react'; import { @@ -76,10 +76,10 @@ const routeConfigs: WebRoute[] = [ regex: /^\/model\/lineage(\?.*)?$/, }, { - element: , - label: 'Query View', - path: '/query/view/:queryId', - regex: /^\/query\/view\/([0-9]|[a-z]|_)+$/, + element: , + label: 'Query Control Center', + path: '/query/control-center', + regex: /^\/query\/control-center(\?.*)?$/, }, { element: , @@ -988,9 +988,6 @@ SELECT * FROM final`, ]), ); } - case 'trino-fetch-current-schema': { - return resolve(apiResponse('my_schema')); - } case 'trino-fetch-etl-sources': { return resolve( apiResponse([ @@ -1038,70 +1035,249 @@ SELECT * FROM final`, ]), ); } - case 'trino-fetch-system-queries': { + case 'trino-fetch-tables': { + return resolve( + apiResponse([ + 'table_1', + 'table_2', + 'table_3', + 'table_4', + 'table_5', + ]), + ); + } + case 'trino-fetch-active-queries': { + return resolve( + apiResponse({ + source: 'cli', + profileName: null, + rows: [ + { + queryId: '20250511_180000_00001_abc', + state: 'RUNNING', + user: 'mock_user', + source: 'dbt-trino-1.7.17', + catalog: 'hive', + schema: 'analytics', + created: new Date(Date.now() - 60_000).toISOString(), + started: new Date(Date.now() - 55_000).toISOString(), + wallTimeMs: 55_000, + cpuTimeMs: 38_000, + peakUserMemoryBytes: 1_200_000_000, + totalSplits: 240, + completedSplits: 142, + runningSplits: 12, + queuedSplits: 86, + }, + { + queryId: '20250511_175900_00000_xyz', + state: 'FINISHED', + user: 'mock_user', + source: 'lightdash', + catalog: 'hive', + schema: 'analytics', + created: new Date(Date.now() - 600_000).toISOString(), + started: new Date(Date.now() - 595_000).toISOString(), + ended: new Date(Date.now() - 540_000).toISOString(), + wallTimeMs: 55_000, + cpuTimeMs: 41_200, + peakUserMemoryBytes: 850_000_000, + totalSplits: 180, + completedSplits: 180, + }, + ], + }), + ); + } + case 'trino-fetch-persisted-queries': { return resolve( apiResponse([ { - // analysis_time_ms: 0, - created: '', - end: '', - // error_code: '', - // error_type: '', - // last_heartbeat: '', - // planning_time_ms: 0, - // queued_time_ms: 0, - // query: '', - query_id: '', - // resource_group_id: [], - source: '', - started: '', - state: 'FINISHED', - // user: '', + queryId: '20250510_120000_00000_yesterday', + persistedAt: new Date( + Date.now() - 24 * 3600 * 1000, + ).toISOString(), + jsonPath: + '/mock/workspace/.dj/diagnostics/20250510_120000_00000_yesterday.json', + profileName: 'prod', + coordinatorUrl: 'https://trino-prod.example.com:443', + summary: { + queryId: '20250510_120000_00000_yesterday', + state: 'FAILED', + user: 'mock_user', + source: 'dbt-trino-1.7.17', + wallTimeMs: 12_000, + peakUserMemoryBytes: 950_000_000, + errorCode: 'EXCEEDED_LOCAL_MEMORY_LIMIT', + failureMessage: + 'Query exceeded per-node memory limit of 8GB', + }, + }, + { + // Legacy entry written before profile-stamping + // landed — exercises the History tab's + // "(none)" profile filter bucket and the + // "no badge" QueryRow rendering path. + queryId: '20250509_090000_00000_legacy', + persistedAt: new Date( + Date.now() - 48 * 3600 * 1000, + ).toISOString(), + jsonPath: + '/mock/workspace/.dj/diagnostics/20250509_090000_00000_legacy.json', + summary: { + queryId: '20250509_090000_00000_legacy', + state: 'FINISHED', + user: 'other_user', + source: 'lightdash', + wallTimeMs: 3_500, + peakUserMemoryBytes: 120_000_000, + }, }, ]), ); } - case 'trino-fetch-system-query-with-task': { + case 'trino-delete-persisted-query': { + const req = payload.request as { queryId: string }; return resolve( apiResponse({ - // analysis_time_ms: 0, - created: '2025-01-01T00:00:00Z', - end: '2025-01-01T01:00:00Z', - // error_code: '', - // error_type: '', - // last_heartbeat: '', - // planning_time_ms: 0, - // queued_time_ms: 0, - // query: '', - query_id: 'abc', - // resource_group_id: [], - source: '', - started: '2025-01-01T00:01:00Z', - state: 'FINISHED', - // user: '', + queryId: req.queryId, + deleted: true, }), ); } - case 'trino-fetch-system-query-sql': { + case 'trino-fetch-query-info': { + const req = payload.request as { + queryId: string; + prefer?: 'persisted' | 'rest'; + }; return resolve( - apiResponse(` -/* {""app"": ""dbt"", ""dbt_version"": ""1.7.17"", ""profile_name"": ""profile"", ""target_name"": ""default"", ""node_id"": ""model.project.name""} */ - -select a, b, c -from table -where a = 1 -`), + apiResponse({ + summary: { + queryId: req.queryId, + state: 'FINISHED', + user: 'mock_user', + source: 'dbt-trino-1.7.17', + catalog: 'hive', + schema: 'analytics', + wallTimeMs: 55_000, + cpuTimeMs: 41_200, + peakUserMemoryBytes: 850_000_000, + peakTotalMemoryBytes: 950_000_000, + totalSplits: 180, + completedSplits: 180, + blockedTimeMs: 4_000, + dataSkewScore: 1.2, + largestOperator: 'LookupJoinOperator', + connectorTypes: ['hive'], + }, + query: + '/* {"app": "dbt", "node_id": "model.mock_project.int__finance__billing__daily"} */\nselect * from analytics.daily', + modelMatch: { + project: 'mock_project', + modelName: 'int__finance__billing__daily', + modelJsonPath: + '/mock/workspace/models/intermediate/finance/billing/int__finance__billing__daily.model.json', + matchedBy: 'comment', + }, + operatorSummary: [ + { + operatorType: 'LookupJoinOperator', + pipelineId: 1, + inputPositions: 12_000_000, + outputPositions: 11_800_000, + cpuNanos: 18_000_000_000, + peakMemoryReservation: 600_000_000, + }, + { + operatorType: 'TableScanOperator', + pipelineId: 0, + inputPositions: 12_000_000, + outputPositions: 12_000_000, + cpuNanos: 8_000_000_000, + peakMemoryReservation: 120_000_000, + }, + ], + loadedFrom: req.prefer === 'rest' ? 'rest' : 'persisted', + jsonPath: `/mock/workspace/.dj/diagnostics/${req.queryId}.json`, + }), ); } - case 'trino-fetch-tables': { + case 'trino-analyze-query': { + const req = payload.request as { queryId: string }; return resolve( - apiResponse([ - 'table_1', - 'table_2', - 'table_3', - 'table_4', - 'table_5', - ]), + apiResponse({ + queryId: req.queryId, + jsonPath: `/mock/workspace/.dj/diagnostics/${req.queryId}.json`, + fullJsonPath: `/mock/workspace/.dj/diagnostics/${req.queryId}.full.json`, + modelMatch: { + project: 'mock_project', + modelName: 'int__finance__billing__daily', + modelJsonPath: + '/mock/workspace/models/intermediate/finance/billing/int__finance__billing__daily.model.json', + matchedBy: 'comment', + }, + promptSnippet: `Read .dj/diagnostics/${req.queryId}.json and apply the dj-trino-analyzer skill.`, + }), + ); + } + case 'trino-list-profiles': { + return resolve( + apiResponse({ + active: 'dev', + profiles: [ + { + name: 'dev', + coordinatorUrl: 'https://trino.dev.example.com', + user: 'mock_user', + authMethod: 'basic', + authSource: 'secret-storage', + verifyTls: true, + }, + { + name: 'prod', + coordinatorUrl: 'https://trino.prod.example.com', + user: 'mock_user', + authMethod: 'bearer', + authSource: 'env-var', + secretEnvVar: 'TRINO_PROD_BEARER', + }, + ], + }), + ); + } + case 'trino-save-profile': { + return resolve(apiResponse({ ok: true })); + } + case 'trino-delete-profile': { + return resolve(apiResponse({ ok: true })); + } + case 'trino-set-active-profile': { + return resolve(apiResponse({ ok: true })); + } + case 'trino-set-credentials': { + return resolve(apiResponse({ ok: true })); + } + case 'trino-ping-coordinator': { + return resolve( + apiResponse({ + ok: true, + version: '470', + environment: 'mock', + }), + ); + } + case 'trino-jump-to-model-from-query': { + return resolve( + apiResponse({ + matched: true, + modelMatch: { + project: 'mock_project', + modelName: 'int__finance__billing__daily', + modelJsonPath: + '/mock/workspace/models/intermediate/finance/billing/int__finance__billing__daily.model.json', + matchedBy: 'comment', + }, + }), ); } case 'state-save': { @@ -1573,10 +1749,17 @@ function RenderRoute({ route }: { route: string | null }) { if (!routeConfig) { return
404: Route not found
; } + // Split off the query string so react-router's `pathname` doesn't + // include `?queryId=…`. Treat everything after the first `?` as + // `search` verbatim — we never re-parse it here, so a malformed + // trailer can't break route matching. + const qIdx = route.indexOf('?'); + const pathname = qIdx >= 0 ? route.slice(0, qIdx) : route; + const search = qIdx >= 0 ? route.slice(qIdx) : ''; // Even though we're running in the extesion, wrap this like a browser route, so we can use react-router hooks return ( - + diff --git a/web/src/elements/Button.tsx b/web/src/elements/Button.tsx index 3643b4a..afce8e8 100644 --- a/web/src/elements/Button.tsx +++ b/web/src/elements/Button.tsx @@ -84,6 +84,7 @@ export function Button({ {..._props} className={makeClassName( 'p-2 rounded flex gap-1 items-center hover:text-primary', + 'disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:text-inherit', !className?.includes('justify-') && 'justify-center', fullWidth && 'w-full', className, @@ -104,6 +105,7 @@ export function Button({ {..._props} className={makeClassName( 'p-2 rounded flex gap-2 items-center justify-center text-primary border border-primary', + 'disabled:opacity-50 disabled:cursor-not-allowed', fullWidth && 'w-full', className, )} diff --git a/web/src/elements/Chip.tsx b/web/src/elements/Chip.tsx new file mode 100644 index 0000000..9385c36 --- /dev/null +++ b/web/src/elements/Chip.tsx @@ -0,0 +1,41 @@ +import { makeClassName } from '@web'; + +export type ChipProps = { + selected: boolean; + /** Renders a dashed border (used for "+ New profile"-style affordances). */ + dashed?: boolean; + onClick: () => void; + title?: string; + children: React.ReactNode; +}; + +/** + * Pill-style toggle. Compact alternative to `Button` for filter rows + * and small chip strips. Pairs well with multi-select state filters + * (free toggles) or single-select strips where every item is rendered + * as its own pill. + */ +export function Chip({ + selected, + dashed = false, + onClick, + title, + children, +}: ChipProps) { + return ( + + ); +} diff --git a/web/src/elements/CodeBlock.tsx b/web/src/elements/CodeBlock.tsx index 6aa2072..f5c46f3 100644 --- a/web/src/elements/CodeBlock.tsx +++ b/web/src/elements/CodeBlock.tsx @@ -42,11 +42,21 @@ export function CodeBlock({ }, }; + // The `dj-code-block` class anchors a global CSS override in + // `web/src/main.css` that neutralizes the VS Code webview's default + // `` styling (background, padding, border-radius, border). + // Without it those defaults paint a per-line tint over the highlighted + // tokens and produce the "striped lines" artifact in both light and + // dark themes. Keeping this class on every CodeBlock means consumers + // don't need to remember to wrap with `.live-preview` / + // `.final-preview` / `.compiled-sql-preview` etc. to get clean output. + const wrapperClassName = `dj-code-block ${className}`.trim(); + return ( +
+
+ {actions && ( +
{actions}
+ )} + + ); +} diff --git a/web/src/elements/index.ts b/web/src/elements/index.ts index b409af8..9b010c4 100644 --- a/web/src/elements/index.ts +++ b/web/src/elements/index.ts @@ -3,6 +3,7 @@ export * from './Box'; export * from './Button'; export * from './ButtonGroup'; export * from './Checkbox'; +export * from './Chip'; export * from './CodeBlock'; export * from './DialogBox'; export * from './DiffView'; @@ -20,6 +21,7 @@ export * from './Progress'; export * from './RadioGroup'; export * from './SelectMulti'; export * from './SelectSingle'; +export * from './SlimBanner'; export * from './Spinner'; export * from './Stepper'; export * from './Switch'; diff --git a/web/src/features/DataModeling/components/ErrorMessage.tsx b/web/src/features/DataModeling/components/ErrorMessage.tsx index fc6caca..d2231fb 100644 --- a/web/src/features/DataModeling/components/ErrorMessage.tsx +++ b/web/src/features/DataModeling/components/ErrorMessage.tsx @@ -49,7 +49,9 @@ export const ErrorMessage: React.FC = ({ {false && ( )} diff --git a/web/src/main.css b/web/src/main.css index 44ee0e1..f77138a 100644 --- a/web/src/main.css +++ b/web/src/main.css @@ -26,7 +26,16 @@ } } - /* Override VSCode default code tag styles in SyntaxHighlighter */ + /* Override VS Code's default `` styling inside SyntaxHighlighter + output. The webview ships defaults (background tint, padding, + border-radius, border) that paint over the highlighter theme and + produce a "striped line" artifact in both light and dark mode. + `dj-code-block` is applied unconditionally by `CodeBlock` (see + `web/src/elements/CodeBlock.tsx`) so every consumer is covered. + The page-specific classes are kept for callers that wrap CodeBlock + with extra chrome and rely on those wrappers historically. */ + .dj-code-block code, + .dj-code-block pre code, .live-preview pre code, .final-preview pre code, .dbt-run-preview pre code, @@ -39,6 +48,20 @@ border: none !important; } + /* The highlighter writes inline `color: ` on each ; + we only neutralize the box styles (background/padding/border) + and intentionally leave `color` alone so token colors survive. */ + .dj-code-block code span, + .live-preview pre code span, + .final-preview pre code span, + .dbt-run-preview pre code span, + .compiled-sql-preview pre code span { + background-color: transparent !important; + padding: 0 !important; + border-radius: 0 !important; + border: none !important; + } + /* Split view grid layout */ .diff-view-split { display: grid; diff --git a/web/src/pages/DataExplorer/DataExplorer.tsx b/web/src/pages/DataExplorer/DataExplorer.tsx index 87700e7..b0b932b 100644 --- a/web/src/pages/DataExplorer/DataExplorer.tsx +++ b/web/src/pages/DataExplorer/DataExplorer.tsx @@ -20,14 +20,20 @@ export default function DataExplorer() { if (!msg?.type) return; if (msg.type === 'show-adhoc-query') { setActiveView('sql'); - } else if (msg.type === 'set-active-model' || msg.type === 'select-model') { + } else if ( + msg.type === 'set-active-model' || + msg.type === 'select-model' + ) { if (msg.modelName) setActiveView('model'); } else if ( msg.type === 'column-lineage-init' || msg.type === 'column-lineage-source-init' ) { setActiveView('column'); - } else if (msg.type === 'trigger-compilation' || msg.type === 'trigger-run-query') { + } else if ( + msg.type === 'trigger-compilation' || + msg.type === 'trigger-run-query' + ) { setActiveView('model'); } }; diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 79355dc..6b12b31 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -39,8 +39,8 @@ export function Home() {
  • - - Query View + + Query Control Center
  • diff --git a/web/src/pages/ModelLineage/ModelLineage.tsx b/web/src/pages/ModelLineage/ModelLineage.tsx index 5f2144d..74b5388 100644 --- a/web/src/pages/ModelLineage/ModelLineage.tsx +++ b/web/src/pages/ModelLineage/ModelLineage.tsx @@ -717,7 +717,7 @@ export default function ModelLineage({ onShowAdhocQuery }: ModelLineageProps) { )} - + {/* Split Mode Toggle */}