Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions docs/en/integrations/looker/tools/looker-query-sql.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@ description: >
The `looker-query-sql` generates a sql query using the Looker
semantic model.

`looker-query-sql` takes nine parameters:
`looker-query-sql` takes ten parameters:

1. the `model`
2. the `explore`
3. the `fields` list
4. an optional set of `filters`
5. an optional `filter_expression`
6. an optional set of `pivots`
7. an optional set of `sorts`
8. an optional `limit`
9. an optional `tz`
6. an optional `dynamic_fields`
7. an optional set of `pivots`
8. an optional set of `sorts`
9. an optional `limit`
10. an optional `tz`

Starting in Looker v25.18, these queries can be identified in Looker's
System Activity. In the History explore, use the field API Client Name
Expand All @@ -47,7 +48,7 @@ description: |
Parameters:
All parameters for this tool are identical to those of the `query` tool.
This includes `model_name`, `explore_name`, `fields` (required),
and optional parameters like `pivots`, `filters`, `filter_expression`, `sorts`, `limit`, and `query_timezone`.
and optional parameters like `pivots`, `filters`, `filter_expression`, `dynamic_fields`, `sorts`, `limit`, and `query_timezone`.

Output:
The result of this tool is the raw SQL text.
Expand Down
15 changes: 8 additions & 7 deletions docs/en/integrations/looker/tools/looker-query-url.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@ description: >
The `looker-query-url` generates a url link to an explore in
Looker so the query can be investigated further.

`looker-query-url` takes ten parameters:
`looker-query-url` takes eleven parameters:

1. the `model`
2. the `explore`
3. the `fields` list
4. an optional set of `filters`
5. an optional `filter_expression`
6. an optional set of `pivots`
7. an optional set of `sorts`
8. an optional `limit`
9. an optional `tz`
10. an optional `vis_config`
6. an optional `dynamic_fields`
7. an optional set of `pivots`
8. an optional set of `sorts`
9. an optional `limit`
10. an optional `tz`
11. an optional `vis_config`

## Compatible Sources

Expand All @@ -42,7 +43,7 @@ description: |

Parameters:
All query parameters (e.g., `model_name`, `explore_name`, `fields`, `pivots`,
`filters`, `filter_expression`, `sorts`, `limit`, `query_timezone`) are the same as the `query` tool.
`filters`, `filter_expression`, `dynamic_fields`, `sorts`, `limit`, `query_timezone`) are the same as the `query` tool.

Additionally, it accepts an optional `vis_config` parameter:
- vis_config (optional): A JSON object that controls the default visualization
Expand Down
18 changes: 13 additions & 5 deletions docs/en/integrations/looker/tools/looker-query.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@ description: >
The `looker-query` tool runs a query using the Looker
semantic model.

`looker-query` takes nine parameters:
`looker-query` takes ten parameters:

1. the `model`
2. the `explore`
3. the `fields` list
4. an optional set of `filters`
5. an optional `filter_expression`
6. an optional set of `pivots`
7. an optional set of `sorts`
8. an optional `limit`
9. an optional `tz`
6. an optional `dynamic_fields`
7. an optional set of `pivots`
8. an optional set of `sorts`
9. an optional `limit`
10. an optional `tz`

Starting in Looker v25.18, these queries can be identified in Looker's
System Activity. In the History explore, use the field API Client Name
Expand Down Expand Up @@ -61,6 +62,13 @@ description: |
- `${orders.order_date} < add_years(-1, now())`
- `${activity.email} != ${activity_drive_facts.current_owner_email}`
- `matches_filter(${order.order_month}, '24 months') AND matches_filter(${order.order_month}, 'before 2024/07/01')`
- dynamic_fields: An optional array of dynamic fields (table calculations, custom measures, custom dimensions) defined as JSON objects.
- Useful for ad-hoc calculations that are not defined in the LookML model.
- Reference fields using `${view.field_name}` syntax.
- Examples:
- Table Calculation: `[{"table_calculation": "test", "label": "test", "expression": "${order_items.total_sale_price} * 0.8", "_type_hint": "number"}]`
- Custom Dimension: `[{"dimension": "days_since_order", "label": "days since order", "expression": "diff_days(${order.order_date}, now())", "_type_hint": "number"}]`
- Custom Measure: `[{"measure": "sum_of_revenue", "label": "Sum of Revenue", "based_on": "training.revenue", "type": "sum", "_type_hint": "number"}]`
- sorts: A list of fields to sort by, optionally including direction (e.g., `["view.field desc"]`).
- limit: Row limit (default 500). Use "-1" for unlimited.
- query_timezone: specific timezone for the query (e.g. `America/Los_Angeles`).
Expand Down
11 changes: 9 additions & 2 deletions internal/prebuiltconfigs/tools/looker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ description: |
- `${orders.order_date} < add_years(-1, now())`
- `${activity.email} != ${activity_drive_facts.current_owner_email}`
- `matches_filter(${order.order_month}, '24 months') AND matches_filter(${order.order_month}, 'before 2024/07/01')`
- dynamic_fields: An optional array of dynamic fields (table calculations, custom measures, custom dimensions) defined as JSON objects.
- Useful for ad-hoc calculations that are not defined in the LookML model.
- Reference fields using `${view.field_name}` syntax.
- Examples:
- Table Calculation: `[{"table_calculation": "test", "label": "test", "expression": "${order_items.total_sale_price} * 0.8", "_type_hint": "number"}]`
- Custom Dimension: `[{"dimension": "days_since_order", "label": "days since order", "expression": "diff_days(${order.order_date}, now())", "_type_hint": "number"}]`
- Custom Measure: `[{"measure": "sum_of_revenue", "label": "Sum of Revenue", "based_on": "training.revenue", "type": "sum", "_type_hint": "number"}]`
- sorts: A list of fields to sort by, optionally including direction (e.g., `["view.field desc"]`).
- limit: Row limit (default 500). Use "-1" for unlimited.
- query_timezone: specific timezone for the query (e.g. `America/Los_Angeles`).
Expand All @@ -166,7 +173,7 @@ description: |
Parameters:
All parameters for this tool are identical to those of the `query` tool.
This includes `model_name`, `explore_name`, `fields` (required),
and optional parameters like `pivots`, `filters`, `filter_expression`, `sorts`, `limit`, and `query_timezone`.
and optional parameters like `pivots`, `filters`, `filter_expression`, `dynamic_fields`, `sorts`, `limit`, and `query_timezone`.

Output:
The result of this tool is the raw SQL text.
Expand All @@ -182,7 +189,7 @@ description: |

Parameters:
All query parameters (e.g., `model_name`, `explore_name`, `fields`, `pivots`,
`filters`, `filter_expression`, `sorts`, `limit`, `query_timezone`) are the same as the `query` tool.
`filters`, `filter_expression`, `dynamic_fields`, `sorts`, `limit`, `query_timezone`) are the same as the `query` tool.

Additionally, it accepts an optional `vis_config` parameter:
- vis_config (optional): A JSON object that controls the default visualization
Expand Down
21 changes: 21 additions & 0 deletions internal/tools/looker/lookercommon/lookercommon.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package lookercommon

import (
"context"
"encoding/json"
"fmt"
"net/url"
"strings"
Expand Down Expand Up @@ -141,6 +142,12 @@ func GetQueryParameters() parameters.Parameters {
limitParameter := parameters.NewIntParameter("limit", "The row limit.", parameters.WithIntDefault(500))
tzParameter := parameters.NewStringParameter("tz", "The query timezone.", parameters.WithStringRequired(false))
filterExpressionParameter := parameters.NewStringParameter("filter_expression", "An optional filter expression string.", parameters.WithStringRequired(false))
dynamicFieldsParameter := parameters.NewArrayParameter(
"dynamic_fields",
"An optional array of dynamic fields (table calculations, custom measures, custom dimensions).",
parameters.NewMapParameter("dynamic_field", "A dynamic field definition", ""),
parameters.WithArrayDefault([]any{}),
)

return parameters.Parameters{
modelParameter,
Expand All @@ -152,6 +159,7 @@ func GetQueryParameters() parameters.Parameters {
limitParameter,
tzParameter,
filterExpressionParameter,
dynamicFieldsParameter,
}
}

Expand Down Expand Up @@ -343,6 +351,18 @@ func ProcessQueryArgs(ctx context.Context, params parameters.ParamValues) (*v4.W
}
}

var dynamicFieldsPtr *string
if val, ok := paramsMap["dynamic_fields"]; ok && val != nil {
if sliceVal, ok := val.([]any); ok && len(sliceVal) > 0 {
jsonBytes, err := json.Marshal(sliceVal)
if err != nil {
return nil, fmt.Errorf("error marshaling dynamic_fields: %w", err)
}
jsonStr := string(jsonBytes)
dynamicFieldsPtr = &jsonStr
}
}
Comment thread
drstrangelooker marked this conversation as resolved.

wq := v4.WriteQuery{
Model: paramsMap["model"].(string),
View: paramsMap["explore"].(string),
Expand All @@ -353,6 +373,7 @@ func ProcessQueryArgs(ctx context.Context, params parameters.ParamValues) (*v4.W
QueryTimezone: &tz,
Limit: &limit,
FilterExpression: filterExpressionPtr,
DynamicFields: dynamicFieldsPtr,
}
return &wq, nil
}
Expand Down
84 changes: 84 additions & 0 deletions internal/tools/looker/lookercommon/lookercommon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,3 +463,87 @@ func TestProcessQueryArgsWithFilterExpression(t *testing.T) {
})
}
}

func TestProcessQueryArgsWithDynamicFields(t *testing.T) {
ctx, err := testutils.ContextWithNewLogger()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}

tcs := []struct {
desc string
dynamicFields any
wantVal *string
wantErr bool
}{
{
desc: "dynamic fields is nil",
dynamicFields: nil,
wantVal: nil,
wantErr: false,
},
{
desc: "dynamic fields is valid array of maps",
dynamicFields: []any{
map[string]any{
"category": "table_calculation",
"expression": "${order_items.total_sale_price} * 0.8",
"label": "test",
"table_calculation": "test",
"_type_hint": "number",
},
},
wantVal: func() *string {
s := `[{"_type_hint":"number","category":"table_calculation","expression":"${order_items.total_sale_price} * 0.8","label":"test","table_calculation":"test"}]`
return &s
}(),
wantErr: false,
},
}

for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
params := parameters.ParamValues{
{Name: "model", Value: "marketing"},
{Name: "explore", Value: "cohort_marketing_performance"},
{Name: "fields", Value: []any{"view.channel"}},
{Name: "filters", Value: map[string]any{}},
{Name: "pivots", Value: []any{}},
{Name: "sorts", Value: []any{}},
{Name: "limit", Value: 10},
{Name: "tz", Value: "Etc/UTC"},
}
if tc.dynamicFields != nil {
params = append(params, parameters.ParamValue{Name: "dynamic_fields", Value: tc.dynamicFields})
}
wq, err := lookercommon.ProcessQueryArgs(ctx, params)
if tc.wantErr {
if err == nil {
t.Fatalf("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if wq.DynamicFields == nil && tc.wantVal != nil {
t.Fatalf("expected DynamicFields %v, got nil", *tc.wantVal)
}
if wq.DynamicFields != nil && tc.wantVal == nil {
t.Fatalf("expected DynamicFields nil, got %v", *wq.DynamicFields)
}
if wq.DynamicFields != nil && tc.wantVal != nil {
var gotObj, wantObj any
if err := json.Unmarshal([]byte(*wq.DynamicFields), &gotObj); err != nil {
t.Fatalf("failed to unmarshal got dynamic fields: %v", err)
}
if err := json.Unmarshal([]byte(*tc.wantVal), &wantObj); err != nil {
t.Fatalf("failed to unmarshal want dynamic fields: %v", err)
}
if diff := cmp.Diff(wantObj, gotObj); diff != "" {
t.Fatalf("incorrect DynamicFields: diff %v", diff)
}
}
})
}
}
80 changes: 80 additions & 0 deletions tests/looker/looker_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,22 @@ func TestLooker(t *testing.T) {
"required": false,
"type": "string",
},
map[string]any{
"authServices": []any{},
"default": []any{},
"description": "An optional array of dynamic fields (table calculations, custom measures, custom dimensions).",
"items": map[string]any{
"additionalProperties": true,
"authServices": []any{},
"description": "A dynamic field definition",
"name": "dynamic_field",
"required": false,
"type": "object",
},
"name": "dynamic_fields",
"required": false,
"type": "array",
},
},
},
},
Expand Down Expand Up @@ -671,6 +687,22 @@ func TestLooker(t *testing.T) {
"required": false,
"type": "string",
},
map[string]any{
"authServices": []any{},
"default": []any{},
"description": "An optional array of dynamic fields (table calculations, custom measures, custom dimensions).",
"items": map[string]any{
"additionalProperties": true,
"authServices": []any{},
"description": "A dynamic field definition",
"name": "dynamic_field",
"required": false,
"type": "object",
},
"name": "dynamic_fields",
"required": false,
"type": "array",
},
},
},
},
Expand Down Expand Up @@ -770,6 +802,22 @@ func TestLooker(t *testing.T) {
"required": false,
"type": "string",
},
map[string]any{
"authServices": []any{},
"default": []any{},
"description": "An optional array of dynamic fields (table calculations, custom measures, custom dimensions).",
"items": map[string]any{
"additionalProperties": true,
"authServices": []any{},
"description": "A dynamic field definition",
"name": "dynamic_field",
"required": false,
"type": "object",
},
"name": "dynamic_fields",
"required": false,
"type": "array",
},
map[string]any{
"additionalProperties": true,
"authServices": []any{},
Expand Down Expand Up @@ -1029,6 +1077,22 @@ func TestLooker(t *testing.T) {
"required": false,
"type": "string",
},
map[string]any{
"authServices": []any{},
"default": []any{},
"description": "An optional array of dynamic fields (table calculations, custom measures, custom dimensions).",
"items": map[string]any{
"additionalProperties": true,
"authServices": []any{},
"description": "A dynamic field definition",
"name": "dynamic_field",
"required": false,
"type": "object",
},
"name": "dynamic_fields",
"required": false,
"type": "array",
},
map[string]any{
"authServices": []any{},
"description": "The title of the Look",
Expand Down Expand Up @@ -1318,6 +1382,22 @@ func TestLooker(t *testing.T) {
"required": false,
"type": "string",
},
map[string]any{
"authServices": []any{},
"default": []any{},
"description": "An optional array of dynamic fields (table calculations, custom measures, custom dimensions).",
"items": map[string]any{
"additionalProperties": true,
"authServices": []any{},
"description": "A dynamic field definition",
"name": "dynamic_field",
"required": false,
"type": "object",
},
"name": "dynamic_fields",
"required": false,
"type": "array",
},
map[string]any{
"authServices": []any{},
"description": "The id of the dashboard where this tile will exist",
Expand Down
Loading