Migrating to the new LookML runtime

Looker's new LookML runtime has been available since Looker 22.6. A "runtime" is the part of Looker that interprets LookML code. The new runtime is faster and checks for more LookML errors than the legacy runtime.

Looker will require all customers migrate to the new runtime in early 2025 and strongly encourages all customers to complete the migration before that time. The new LookML runtime is able to catch previously overlooked errors, so enabling the new runtime may cause new LookML errors to appear. These errors are not caused by the new runtime; rather, they are pre-existing errors that are now being found.

Additionally, customers that want to change their instance to Looker (Google Cloud core) must migrate to the new runtime beforehand.

How to switch to the new runtime

1. Turn off the "Use Legacy LookML Runtime" legacy feature if available

Some Looker are enabled with the Use Legacy LookML Runtime legacy feature. Disable the Use Legacy LookML Runtime legacy feature to transition the Looker instance to the new runtime.

If the Use Legacy LookML Runtime legacy feature is not available in the Legacy Features admin page of your Looker instance, your instance is already using the new runtime.

2. Ensure that your LookML projects are not configured with new_lookml_runtime:no

It is possible to override a Looker instance's global Use Legacy LookML Runtime setting by adding the new_lookml_runtime:no statement in the manifest file of a LookML project.

Ensure that your LookML project manifest files do not have the new_lookml_runtime parameter, or that new_lookml_runtime is set to yes on all LookML projects.

LookML issues the new runtime may find

After transitioning to the new runtime you may notice new errors in your LookML. The new errors are not caused by the new runtime; rather, they are pre-existing issues that are now being found.

Depending upon your LookML developer settings, you may be required to fix these errors before continuing to submit LookML changes. The following sections describe some of the issues that the new LookML runtime may find in your project and how to remedy them:

Stricter type checking within Liquid expressions

When comparing values in Liquid, the new runtime requires that the two values are of the same type. This error arises most often if a decimal number is being compared to an integer number.

In this example Liquid is being used to color code a user's age if they are less than or greater than the average age of buyers for a product:

dimension: age {
  type: integer
  html:
    {% if value > product.average_age._value %}
      <div style="color:orange">{{rendered_value}}</div>
    {% else %}
      <div style="color:blue">{{rendered_value}}</div>
    {% endif %}
    ;;
}

Since average_age is a decimal number, and age is an integer, the new runtime will find an error. Use the assign expression combined with a multiplication filter to change the integer into a decimal:

dimension: age {
  sql: ${TABLE}.age
  type: integer
  html:
    {% assign dec_value = value |times(1.0) %}
    {% if dec_value > product.average_age._value %}
      <div style="color:orange">{{rendered_value}}</div>
    {% else %}
      <div style="color:blue">{{rendered_value}}</div>
    {% endif %}
    ;;
}

Different handling of undefined values

The legacy runtime converts undefined values to NULL, while the new runtime will throw an exception. In this example the dimension is trying to display the second value in a comma separated list of values:

dimension: second_flag {
  sql: ${TABLE}.flag_list
  type: string
  html:
    {% assign flag_values = value |split(",") %}
    {% if flag_values[1] %}
      {{ flag_values[1] }}
    {% else %}
      No second flag
    {% endif %}
    ;;
}

This would work in the legacy runtime because flag_values[1] would be undefined in some cases, converted to NULL, and then the Liquid could be interpreted. However, in the new runtime, flag_values[1] will remain undefined.

To address this problem check explicitly for nil:

dimension: second_flag {
  sql: ${TABLE}.flag_list
  type: string
  html:
    {% assign flag_values = value |split(",") %}
    {% if flag_values[1] != nil %}
      {{ flag_values[1] }}
    {% else %}
      No second flag
    {% endif %}
    ;;
}

Some Persistent Derived Tables may rebuild

Persistent Derived Table (PDT) keys are based on SQL generated by the LookML runtime. In some cases the new runtime may generate different (but equivalent) SQL for a PDT, resulting in a different PDT key. A change of a PDT key will cause the PDT to rebuild.

HTML literals inside Liquid expressions may be converted to Unicode

HTML tags in Liquid expressions may get converted to their unicode equivalent by the new runtime. For example, a <strong> tag may get converted to &lt;strong&gt;. In the legacy runtime HTML tags could be compared directly, as in this example:

html:
  {{ value |replace("<strong>"), "[" |replace("</strong>"), "]" }} ;;

In the new runtime comparisons need to be made against the unicode instead:

html:
  {{ value |replace("&lt;strong&gt;"), "[" |replace("&lt;/strong&gt;"), "]" }} ;;

Invalid references in sql_distinct_key results in "unknown view"

With the new runtime a sql_distinct_key that references an unknown field or view will throw an exception. For example:

measure: total_shipping {
  type: sum_distinct
  sql: ${order_shipping} ;;
  sql_distinct_key: ${some_incorrect_field_name} ;;
}

"Distinct" type measure with no primary key produce different SQL

A distinct type measure (average_distinct, count_distinct, median_distinct, percentile_distinct, sum_distinct) with no primary-key or sql_distinct_key parameter may produce different SQL in the new runtime.

Be sure to specify a primary-key or sql_distinct_key when building distinct type measures.

Accessing _filters[] in Liquid with a bare field reference will add the referenced field as a selected column

In Looker a "bare field reference" is one that is not enclosed in curly braces, such as users.created_date instead of ${users.created_date}.

The legacy runtime ignored bare field references when used with the _filters Liquid variable. The new runtime will add the field to the SQL query.

For example, in this dimension users.created_date is a bare reference:

dimension: name {
  html:
    {% if _filters[users.created_date] != NULL %}
      {{rendered_value}} (created: {{_filters[users.created_date]}})
    {% else %}
      {{rendered_value}}
    {% endif %}
    ;;
}

In the legacy runtime _filters[users.created_date] would have always been ignored and only the second condition if the {% if %} would ever have been met. In the new runtime users.created_date will be added to the SELECT clause of the SQL query so that the condition can be evaluated.

The automatic addition of unexpected fields to Looker queries can be confusing to users, so best practice is not to use bare field references and instead use the ${field_name} syntax.