You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@beam.apache.org by bh...@apache.org on 2020/04/23 00:01:02 UTC

[beam] branch master updated: [BEAM-8603] Add Python SqlTransform (#10055)

This is an automated email from the ASF dual-hosted git repository.

bhulette pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new b3596b8  [BEAM-8603] Add Python SqlTransform (#10055)
b3596b8 is described below

commit b3596b89dbc002c686bdaa7853074e757a81b6fb
Author: Brian Hulette <bh...@google.com>
AuthorDate: Wed Apr 22 17:00:40 2020 -0700

    [BEAM-8603] Add Python SqlTransform (#10055)
    
    * Add wordcount_xlang_sql example
    
    * Add external transform registration for SQLTransform
    
    * Add SQL expansion service artifact
    
    * Fix path_to_beam_jar error when package contains leading :
    
    * fix typo
    
    * Add tests for SqlTransform and gradle target
    
    * Add apache_beam.transforms.sql.SqlTransform
    
    * Disable validateShadowJar for SQL expansion serveice
    
    * yapf
    
    * skip unless a runner is configured
    
    * Address PR comments
    
    * Add missing import
    
    * MEAN -> AVG
    
    * Fix sql_test agg_test
    
    * lint
    
    * Fix pydoc
---
 .../org/apache/beam/gradle/BeamModulePlugin.groovy |  27 +++++
 .../extensions/sql/expansion-service/build.gradle  |  40 +++++++
 .../beam/sdk/extensions/sql/SqlTransform.java      |  39 +++++-
 .../apache_beam/examples/wordcount_xlang_sql.py    | 118 +++++++++++++++++++
 sdks/python/apache_beam/transforms/external.py     |   2 +-
 sdks/python/apache_beam/transforms/sql.py          |  74 ++++++++++++
 sdks/python/apache_beam/transforms/sql_test.py     | 131 +++++++++++++++++++++
 sdks/python/apache_beam/utils/subprocess_server.py |   2 +-
 settings.gradle                                    |   1 +
 9 files changed, 431 insertions(+), 3 deletions(-)

diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy
index 145fce3..2f39cda 100644
--- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy
+++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy
@@ -1772,6 +1772,33 @@ class BeamModulePlugin implements Plugin<Project> {
         cleanupTask.mustRunAfter pythonTask
         config.cleanupJobServer.mustRunAfter pythonTask
       }
+      // Task for running testcases in Python SDK
+      def testOpts = [
+        "--attr=UsesSqlExpansionService"
+      ]
+      def pipelineOpts = [
+        "--runner=PortableRunner",
+        "--environment_cache_millis=10000",
+        "--job_endpoint=${config.jobEndpoint}"
+      ]
+      def beamPythonTestPipelineOptions = [
+        "pipeline_opts": pipelineOpts,
+        "test_opts": testOpts,
+        "suite": "xlangSqlValidateRunner"
+      ]
+      def cmdArgs = project.project(':sdks:python').mapToArgString(beamPythonTestPipelineOptions)
+      def pythonSqlTask = project.tasks.create(name: config.name+"PythonUsingSql", type: Exec) {
+        group = "Verification"
+        description = "Validates runner for cross-language capability of using Java's SqlTransform from Python SDK"
+        executable 'sh'
+        args '-c', ". $envDir/bin/activate && cd $pythonDir && ./scripts/run_integration_test.sh $cmdArgs"
+        dependsOn config.startJobServer
+        dependsOn ':sdks:python:container:py'+pythonContainerSuffix+':docker'
+        dependsOn ':sdks:java:extensions:sql:expansion-service:shadowJar'
+        dependsOn ":sdks:python:installGcpTest"
+      }
+      mainTask.dependsOn pythonSqlTask
+      config.cleanupJobServer.mustRunAfter pythonSqlTask
     }
 
     /** ***********************************************************************************************/
diff --git a/sdks/java/extensions/sql/expansion-service/build.gradle b/sdks/java/extensions/sql/expansion-service/build.gradle
new file mode 100644
index 0000000..19a3270
--- /dev/null
+++ b/sdks/java/extensions/sql/expansion-service/build.gradle
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * License); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an AS IS BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+plugins { id 'org.apache.beam.module' }
+applyJavaNature(automaticModuleName: 'org.apache.beam.sdks.extensions.sql.expansion',
+                validateShadowJar: false,
+                shadowClosure: {
+                  manifest {
+                    attributes "Main-Class": "org.apache.beam.sdk.expansion.service.ExpansionService"
+                  }
+                })
+
+description = "Apache Beam :: SDKs :: Java :: SQL :: Expansion Service"
+ext.summary = """Contains code to run a SQL Expansion Service."""
+
+
+dependencies {
+  compile project(path: ":sdks:java:extensions:sql")
+  compile project(path: ":sdks:java:expansion-service")
+}
+
+task runExpansionService (type: JavaExec) {
+  main = "org.apache.beam.sdk.expansion.service.ExpansionService"
+  classpath = sourceSets.main.runtimeClasspath
+  args = [project.findProperty("constructionService.port") ?: "8097"]
+}
diff --git a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/SqlTransform.java b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/SqlTransform.java
index adc3f9d..ba27228 100644
--- a/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/SqlTransform.java
+++ b/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/SqlTransform.java
@@ -17,6 +17,7 @@
  */
 package org.apache.beam.sdk.extensions.sql;
 
+import com.google.auto.service.AutoService;
 import com.google.auto.value.AutoValue;
 import java.util.Collections;
 import java.util.HashMap;
@@ -24,6 +25,7 @@ import java.util.List;
 import java.util.Map;
 import javax.annotation.Nullable;
 import org.apache.beam.sdk.annotations.Experimental;
+import org.apache.beam.sdk.expansion.ExternalTransformRegistrar;
 import org.apache.beam.sdk.extensions.sql.impl.BeamSqlEnv;
 import org.apache.beam.sdk.extensions.sql.impl.BeamSqlEnv.BeamSqlEnvBuilder;
 import org.apache.beam.sdk.extensions.sql.impl.BeamSqlPipelineOptions;
@@ -34,6 +36,7 @@ import org.apache.beam.sdk.extensions.sql.meta.BeamSqlTable;
 import org.apache.beam.sdk.extensions.sql.meta.provider.ReadOnlyTableProvider;
 import org.apache.beam.sdk.extensions.sql.meta.provider.TableProvider;
 import org.apache.beam.sdk.transforms.Combine;
+import org.apache.beam.sdk.transforms.ExternalTransformBuilder;
 import org.apache.beam.sdk.transforms.PTransform;
 import org.apache.beam.sdk.transforms.SerializableFunction;
 import org.apache.beam.sdk.values.PCollection;
@@ -255,7 +258,8 @@ public abstract class SqlTransform extends PTransform<PInput, PCollection<Row>>
   }
 
   @AutoValue.Builder
-  abstract static class Builder {
+  abstract static class Builder
+      implements ExternalTransformBuilder<External.Configuration, PInput, PCollection<Row>> {
     abstract Builder setQueryString(String queryString);
 
     abstract Builder setQueryParameters(QueryParameters queryParameters);
@@ -271,6 +275,19 @@ public abstract class SqlTransform extends PTransform<PInput, PCollection<Row>>
     abstract Builder setDefaultTableProvider(@Nullable String defaultTableProvider);
 
     abstract SqlTransform build();
+
+    @Override
+    public PTransform<PInput, PCollection<Row>> buildExternal(
+        External.Configuration configuration) {
+      return builder()
+          .setQueryString(configuration.query)
+          .setQueryParameters(QueryParameters.ofNone())
+          .setUdafDefinitions(Collections.emptyList())
+          .setUdfDefinitions(Collections.emptyList())
+          .setTableProviderMap(Collections.emptyMap())
+          .setAutoUdfUdafLoad(false)
+          .build();
+    }
   }
 
   @AutoValue
@@ -296,4 +313,24 @@ public abstract class SqlTransform extends PTransform<PInput, PCollection<Row>>
       return new AutoValue_SqlTransform_UdafDefinition(udafName, combineFn);
     }
   }
+
+  @AutoService(ExternalTransformRegistrar.class)
+  public static class External implements ExternalTransformRegistrar {
+
+    private static final String URN = "beam:external:java:sql:v1";
+
+    @Override
+    public Map<String, Class<? extends ExternalTransformBuilder>> knownBuilders() {
+      return org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableMap.of(
+          URN, AutoValue_SqlTransform.Builder.class);
+    }
+
+    public static class Configuration {
+      String query;
+
+      public void setQuery(String query) {
+        this.query = query;
+      }
+    }
+  }
 }
diff --git a/sdks/python/apache_beam/examples/wordcount_xlang_sql.py b/sdks/python/apache_beam/examples/wordcount_xlang_sql.py
new file mode 100644
index 0000000..4d2054a
--- /dev/null
+++ b/sdks/python/apache_beam/examples/wordcount_xlang_sql.py
@@ -0,0 +1,118 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""A cross-language word-counting workflow."""
+
+from __future__ import absolute_import
+
+import argparse
+import logging
+import typing
+
+from past.builtins import unicode
+
+import apache_beam as beam
+from apache_beam import coders
+from apache_beam.io import ReadFromText
+from apache_beam.io import WriteToText
+from apache_beam.options.pipeline_options import PipelineOptions
+from apache_beam.options.pipeline_options import SetupOptions
+from apache_beam.transforms.sql import SqlTransform
+from apache_beam.utils import subprocess_server
+
+MyRow = typing.NamedTuple('MyRow', [('word', unicode)])
+coders.registry.register_coder(MyRow, coders.RowCoder)
+
+# Some more fun queries:
+# ------
+# SELECT
+#   word as key,
+#   COUNT(*) as `count`
+# FROM PCOLLECTION
+# GROUP BY word
+# ORDER BY `count` DESC
+# LIMIT 100
+# ------
+# SELECT
+#   len as key,
+#   COUNT(*) as `count`
+# FROM (
+#   SELECT
+#     LENGTH(word) AS len
+#   FROM PCOLLECTION
+# )
+# GROUP BY len
+
+
+def run(p, input_file, output_file):
+  #pylint: disable=expression-not-assigned
+  (
+      p
+      | 'read' >> ReadFromText(input_file)
+      | 'split' >> beam.FlatMap(str.split)
+      | 'row' >> beam.Map(MyRow).with_output_types(MyRow)
+      | 'sql!!' >> SqlTransform(
+          """
+                   SELECT
+                     word as key,
+                     COUNT(*) as `count`
+                   FROM PCOLLECTION
+                   GROUP BY word""")
+      | 'format' >> beam.Map(lambda row: '{}: {}'.format(row.key, row.count))
+      | 'write' >> WriteToText(output_file))
+
+  result = p.run()
+  result.wait_until_finish()
+
+
+def main():
+  logging.getLogger().setLevel(logging.INFO)
+
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      '--input',
+      dest='input',
+      default='gs://dataflow-samples/shakespeare/kinglear.txt',
+      help='Input file to process.')
+  parser.add_argument(
+      '--output',
+      dest='output',
+      required=True,
+      help='Output file to write results to.')
+
+  known_args, pipeline_args = parser.parse_known_args()
+
+  path_to_jar = subprocess_server.JavaJarServer.path_to_beam_jar(
+      ":sdks:java:extensions:sql:expansion-service:shadowJar")
+  # TODO(BEAM-9238): Remove this when it's no longer needed for artifact
+  # staging.
+  pipeline_args.extend(['--experiment', 'jar_packages=%s' % path_to_jar])
+  pipeline_options = PipelineOptions(pipeline_args)
+
+  # We use the save_main_session option because one or more DoFn's in this
+  # workflow rely on global context (e.g., a module imported at module level).
+  pipeline_options.view_as(SetupOptions).save_main_session = True
+
+  p = beam.Pipeline(options=pipeline_options)
+  # Preemptively start due to BEAM-6666.
+  p.runner.create_job_service(pipeline_options)
+
+  run(p, known_args.input, known_args.output)
+
+
+if __name__ == '__main__':
+  main()
diff --git a/sdks/python/apache_beam/transforms/external.py b/sdks/python/apache_beam/transforms/external.py
index bd7caca..4f31cbb 100644
--- a/sdks/python/apache_beam/transforms/external.py
+++ b/sdks/python/apache_beam/transforms/external.py
@@ -470,7 +470,7 @@ class JavaJarExpansionService(object):
 class BeamJarExpansionService(JavaJarExpansionService):
   """An expansion service based on an Beam Java Jar file.
 
-  Attempts to use a locally-build copy of the jar based on the gradle target,
+  Attempts to use a locally-built copy of the jar based on the gradle target,
   if it exists, otherwise attempts to download and cache the released artifact
   corresponding to this version of Beam from the apache maven repository.
   """
diff --git a/sdks/python/apache_beam/transforms/sql.py b/sdks/python/apache_beam/transforms/sql.py
new file mode 100644
index 0000000..8642376
--- /dev/null
+++ b/sdks/python/apache_beam/transforms/sql.py
@@ -0,0 +1,74 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Package for SqlTransform and related classes."""
+
+# pytype: skip-file
+
+from __future__ import absolute_import
+
+import typing
+
+from past.builtins import unicode
+
+from apache_beam.transforms.external import BeamJarExpansionService
+from apache_beam.transforms.external import ExternalTransform
+from apache_beam.transforms.external import NamedTupleBasedPayloadBuilder
+
+__all__ = ['SqlTransform']
+
+SqlTransformSchema = typing.NamedTuple(
+    'SqlTransformSchema', [('query', unicode)])
+
+
+class SqlTransform(ExternalTransform):
+  """A transform that can translate a SQL query into PTransforms.
+
+  Input PCollections must have a schema. Currently, this means the PCollection
+  *must* have a NamedTuple output type, and that type must be registered to use
+  RowCoder. For example::
+
+    Purchase = typing.NamedTuple('Purchase',
+                                 [('item_name', unicode), ('price', float)])
+    coders.registry.register_coder(Purchase, coders.RowCoder)
+
+  Similarly, the output of SqlTransform is a PCollection with a generated
+  NamedTuple type, and columns can be accessed as fields. For example::
+
+    purchases | SqlTransform(\"\"\"
+                  SELECT item_name, COUNT(*) AS `count`
+                  FROM PCOLLECTION GROUP BY item_name\"\"\")
+              | beam.Map(lambda row: "We've sold %d %ss!" % (row.count,
+                                                             row.item_name))
+
+  Additional examples can be found in
+  `apache_beam.examples.wordcount_xlang_sql`, and
+  `apache_beam.transforms.sql_test`.
+
+  For more details about Beam SQL in general see the `Java transform
+  <https://beam.apache.org/releases/javadoc/current/org/apache/beam/sdk/extensions/sql/SqlTransform.html>`_,
+  and the `documentation
+  <https://beam.apache.org/documentation/dsls/sql/overview/>`_.
+  """
+  URN = 'beam:external:java:sql:v1'
+
+  def __init__(self, query):
+    super(SqlTransform, self).__init__(
+        self.URN,
+        NamedTupleBasedPayloadBuilder(SqlTransformSchema(query=query)),
+        BeamJarExpansionService(
+            ':sdks:java:extensions:sql:expansion-service:shadowJar'))
diff --git a/sdks/python/apache_beam/transforms/sql_test.py b/sdks/python/apache_beam/transforms/sql_test.py
new file mode 100644
index 0000000..d4527bb
--- /dev/null
+++ b/sdks/python/apache_beam/transforms/sql_test.py
@@ -0,0 +1,131 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Tests for transforms that use the SQL Expansion service."""
+
+# pytype: skip-file
+
+from __future__ import absolute_import
+
+import logging
+import typing
+import unittest
+
+from nose.plugins.attrib import attr
+from past.builtins import unicode
+
+import apache_beam as beam
+from apache_beam import coders
+from apache_beam.options.pipeline_options import DebugOptions
+from apache_beam.options.pipeline_options import StandardOptions
+from apache_beam.testing.test_pipeline import TestPipeline
+from apache_beam.testing.util import assert_that
+from apache_beam.testing.util import equal_to
+from apache_beam.transforms.sql import SqlTransform
+from apache_beam.utils import subprocess_server
+
+SimpleRow = typing.NamedTuple(
+    "SimpleRow", [("int", int), ("str", unicode), ("flt", float)])
+coders.registry.register_coder(SimpleRow, coders.RowCoder)
+
+
+@attr('UsesSqlExpansionService')
+@unittest.skipIf(
+    TestPipeline().get_pipeline_options().view_as(StandardOptions).runner is
+    None,
+    "Must be run with a runner that supports staging java artifacts.")
+class SqlTransformTest(unittest.TestCase):
+  """Tests that exercise the cross-language SqlTransform (implemented in java).
+
+  Note this test must be executed with pipeline options that run jobs on a local
+  job server. The easiest way to accomplish this is to run the
+  `validatesCrossLanguageRunnerPythonUsingSql` gradle target for a particular
+  job server, which will start the runner and job server for you. For example,
+  `:runners:flink:1.10:job-server:validatesCrossLanguageRunnerPythonUsingSql` to
+  test on Flink 1.10.
+
+  Alternatively, you may be able to iterate faster if you run the tests directly
+  using a runner like `FlinkRunner`, which can start a local Flink cluster and
+  job server for you:
+    $ pip install -e './sdks/python[gcp,test]'
+    $ python ./sdks/python/setup.py nosetests \\
+        --tests apache_beam.transforms.sql_test \\
+        --test-pipeline-options="--runner=FlinkRunner"
+  """
+  @staticmethod
+  def make_test_pipeline():
+    path_to_jar = subprocess_server.JavaJarServer.path_to_beam_jar(
+        ":sdks:java:extensions:sql:expansion-service:shadowJar")
+    test_pipeline = TestPipeline()
+    # TODO(BEAM-9238): Remove this when it's no longer needed for artifact
+    # staging.
+    test_pipeline.get_pipeline_options().view_as(DebugOptions).experiments = [
+        'jar_packages=' + path_to_jar
+    ]
+    return test_pipeline
+
+  def test_generate_data(self):
+    with self.make_test_pipeline() as p:
+      out = p | SqlTransform(
+          """SELECT
+            CAST(1 AS INT) AS `int`,
+            CAST('foo' AS VARCHAR) AS `str`,
+            CAST(3.14  AS DOUBLE) AS `flt`""")
+      assert_that(out, equal_to([(1, "foo", 3.14)]))
+
+  def test_project(self):
+    with self.make_test_pipeline() as p:
+      out = (
+          p | beam.Create([SimpleRow(1, "foo", 3.14)])
+          | SqlTransform("SELECT `int`, `flt` FROM PCOLLECTION"))
+      assert_that(out, equal_to([(1, 3.14)]))
+
+  def test_filter(self):
+    with self.make_test_pipeline() as p:
+      out = (
+          p
+          | beam.Create([SimpleRow(1, "foo", 3.14), SimpleRow(2, "bar", 1.414)])
+          | SqlTransform("SELECT * FROM PCOLLECTION WHERE `str` = 'bar'"))
+      assert_that(out, equal_to([(2, "bar", 1.414)]))
+
+  def test_agg(self):
+    with self.make_test_pipeline() as p:
+      out = (
+          p
+          | beam.Create([
+              SimpleRow(1, "foo", 1.),
+              SimpleRow(1, "foo", 2.),
+              SimpleRow(1, "foo", 3.),
+              SimpleRow(2, "bar", 1.414),
+              SimpleRow(2, "bar", 1.414),
+              SimpleRow(2, "bar", 1.414),
+              SimpleRow(2, "bar", 1.414),
+          ])
+          | SqlTransform(
+              """
+              SELECT
+                `str`,
+                COUNT(*) AS `count`,
+                SUM(`int`) AS `sum`,
+                AVG(`flt`) AS `avg`
+              FROM PCOLLECTION GROUP BY `str`"""))
+      assert_that(out, equal_to([("foo", 3, 3, 2), ("bar", 4, 8, 1.414)]))
+
+
+if __name__ == "__main__":
+  logging.getLogger().setLevel(logging.INFO)
+  unittest.main()
diff --git a/sdks/python/apache_beam/utils/subprocess_server.py b/sdks/python/apache_beam/utils/subprocess_server.py
index d9c5085..9d62917 100644
--- a/sdks/python/apache_beam/utils/subprocess_server.py
+++ b/sdks/python/apache_beam/utils/subprocess_server.py
@@ -173,7 +173,7 @@ class JavaJarServer(SubprocessServer):
 
   @classmethod
   def path_to_beam_jar(cls, gradle_target, appendix=None, version=beam_version):
-    gradle_package = gradle_target.strip(':')[:gradle_target.rindex(':')]
+    gradle_package = gradle_target.strip(':').rsplit(':', 1)[0]
     artifact_id = 'beam-' + gradle_package.replace(':', '-')
     project_root = os.path.sep.join(
         os.path.abspath(__file__).split(os.path.sep)[:-5])
diff --git a/settings.gradle b/settings.gradle
index ea18ec4..255aab1 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -82,6 +82,7 @@ include ":sdks:java:extensions:sql:shell"
 include ":sdks:java:extensions:sql:hcatalog"
 include ":sdks:java:extensions:sql:datacatalog"
 include ":sdks:java:extensions:sql:zetasql"
+include ":sdks:java:extensions:sql:expansion-service"
 include ":sdks:java:extensions:zetasketch"
 include ":sdks:java:fn-execution"
 include ":sdks:java:harness"