type, boolean required)
+ {
+ TableSourceArgument argument = getArgument(name, index, required);
+
+ if (!required && argument == null)
+ {
+ return null;
+ }
+ Object value = argument.getValue();
+
+ if (type.isInstance(value))
+ {
+ return (T) value;
+ }
+
+ throw new IllegalArgumentException("Argument of name '" + name + "' or index '" + index + "' is not of type " + type.getName());
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ return EqualsBuilder.reflectionEquals(this, o);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return HashCodeBuilder.reflectionHashCode(this);
+ }
+
+ @Override
+ public String toString()
+ {
+ return ToStringBuilder.reflectionToString(this);
+ }
+}
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-query/src/main/java/org/finos/legend/engine/query/sql/api/sources/TableSourceArgument.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-core/src/main/java/org/finos/legend/engine/query/sql/providers/core/TableSourceArgument.java
similarity index 96%
rename from legend-engine-xts-sql/legend-engine-xt-sql-query/src/main/java/org/finos/legend/engine/query/sql/api/sources/TableSourceArgument.java
rename to legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-core/src/main/java/org/finos/legend/engine/query/sql/providers/core/TableSourceArgument.java
index 58bbe1bd904..28f4e3b86c2 100644
--- a/legend-engine-xts-sql/legend-engine-xt-sql-query/src/main/java/org/finos/legend/engine/query/sql/api/sources/TableSourceArgument.java
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-core/src/main/java/org/finos/legend/engine/query/sql/providers/core/TableSourceArgument.java
@@ -13,7 +13,7 @@
// limitations under the License.
//
-package org.finos.legend.engine.query.sql.api.sources;
+package org.finos.legend.engine.query.sql.providers.core;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/pom.xml b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/pom.xml
new file mode 100644
index 00000000000..1651e8868f1
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/pom.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+ org.finos.legend.engine
+ legend-engine-xt-sql-providers
+ 4.32.1-SNAPSHOT
+
+ 4.0.0
+
+ legend-engine-xt-sql-providers-relationalStore
+ jar
+ Legend Engine - XT - SQL - Providers - Relational Store
+
+
+
+
+ org.finos.legend.engine
+ legend-engine-xt-sql-providers-core
+
+
+ org.finos.legend.engine
+ legend-engine-xt-sql-providers-shared
+
+
+ org.finos.legend.engine
+ legend-engine-xt-relationalStore-protocol
+
+
+ org.finos.legend.engine
+ legend-engine-protocol-pure
+
+
+
+
+
+ org.eclipse.collections
+ eclipse-collections-api
+
+
+ org.eclipse.collections
+ eclipse-collections
+
+
+
+
+
+ junit
+ junit
+ test
+
+
+ org.mockito
+ mockito-core
+
+
+ org.finos.legend.engine
+ legend-engine-xt-sql-providers-shared
+ test-jar
+ test
+
+
+ org.finos.legend.engine
+ legend-engine-xt-relationalStore-grammar
+ test
+
+
+
+
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/src/main/java/org/finos/legend/engine/query/sql/providers/RelationalStoreSQLSourceProvider.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/src/main/java/org/finos/legend/engine/query/sql/providers/RelationalStoreSQLSourceProvider.java
new file mode 100644
index 00000000000..38b7d14d0ec
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/src/main/java/org/finos/legend/engine/query/sql/providers/RelationalStoreSQLSourceProvider.java
@@ -0,0 +1,89 @@
+// Copyright 2023 Goldman Sachs
+//
+// Licensed 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 org.finos.legend.engine.query.sql.providers;
+
+import org.eclipse.collections.impl.list.mutable.FastList;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.connection.ConnectionPointer;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.connection.PackageableConnection;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.runtime.LegacyRuntime;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.store.relational.model.Database;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.store.relational.model.Schema;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.store.relational.model.Table;
+import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.raw.Lambda;
+import org.finos.legend.engine.query.sql.providers.core.SQLSource;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceArgument;
+import org.finos.legend.engine.query.sql.providers.core.TableSource;
+import org.finos.legend.engine.query.sql.providers.shared.AbstractLegendStoreSQLSourceProvider;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateLoader;
+import org.finos.legend.engine.query.sql.providers.shared.utils.SQLProviderUtils;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This class serves for handling the **relationalStore** source type
+ *
+ * Sample Select statement
+ * select * from relationalStore(connection => 'my::Connection', store => 'my::Store', schema => 'schema1', table => 'table1', coordinates => 'com.gs:proj1:1.0.0')
+ * select * from relationalStore(connection => 'my::Connection', store => 'my::Store', schema => 'schema1', table => 'table1', project => 'PROD-12345', workspace => 'myworkspace')
+ * select * from relationalStore(connection => 'my::Connection', store => 'my::Store', schema => 'schema1', table => 'table1', project => 'PROD-12345', groupWorkspace => 'myworkspace')
+ */
+public class RelationalStoreSQLSourceProvider extends AbstractLegendStoreSQLSourceProvider
+{
+
+ private static final String TYPE = "relationalStore";
+ private static final String ARG_SCHEMA = "schema";
+ private static final String ARG_TABLE = "table";
+
+ public RelationalStoreSQLSourceProvider(ProjectCoordinateLoader projectCoordinateLoader)
+ {
+ super(Database.class, projectCoordinateLoader);
+ }
+
+ @Override
+ public String getType()
+ {
+ return TYPE;
+ }
+
+ @Override
+ protected SQLSource createSource(TableSource source, Database store, PackageableConnection connection, List keys, PureModelContextData pmcd)
+ {
+ String schemaName = source.getArgumentValueAs(ARG_SCHEMA, -1, String.class, true);
+ String tableName = source.getArgumentValueAs(ARG_TABLE, -1, String.class, true);
+
+ Lambda lambda = tableToTDS(store, schemaName, tableName);
+
+ ConnectionPointer connectionPtr = new ConnectionPointer();
+ connectionPtr.connection = connection.getPath();
+
+ LegacyRuntime runtime = new LegacyRuntime();
+ runtime.connections = FastList.newListWith(connectionPtr);
+
+ Collections.addAll(keys, new SQLSourceArgument(ARG_SCHEMA, null, schemaName), new SQLSourceArgument(ARG_TABLE, null, tableName));
+
+ return new SQLSource(TYPE, lambda, null, runtime, null, null, keys);
+ }
+
+ public static Lambda tableToTDS(Database database, String schemaName, String tableName)
+ {
+ Schema schema = SQLProviderUtils.extractElement("schema", database.schemas, s -> SQLProviderUtils.equalsEscaped(s.name, schemaName));
+ Table table = SQLProviderUtils.extractElement("table", schema.tables, t -> SQLProviderUtils.equalsEscaped(t.name, tableName));
+
+ return SQLProviderUtils.tableToTDS(database.getPath(), schema.name, table.name);
+ }
+}
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/src/test/java/org/finos/legend/engine/query/sql/providers/TestRelationalStoreSQLSourceProvider.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/src/test/java/org/finos/legend/engine/query/sql/providers/TestRelationalStoreSQLSourceProvider.java
new file mode 100644
index 00000000000..c99585700bb
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/src/test/java/org/finos/legend/engine/query/sql/providers/TestRelationalStoreSQLSourceProvider.java
@@ -0,0 +1,227 @@
+// Copyright 2023 Goldman Sachs
+//
+// Licensed 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 org.finos.legend.engine.query.sql.providers;
+
+import org.eclipse.collections.impl.list.mutable.FastList;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContext;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextPointer;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.connection.ConnectionPointer;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.runtime.LegacyRuntime;
+import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.raw.Lambda;
+import org.finos.legend.engine.query.sql.providers.core.SQLSource;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceArgument;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceProvider;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceResolvedContext;
+import org.finos.legend.engine.query.sql.providers.core.TableSource;
+import org.finos.legend.engine.query.sql.providers.core.TableSourceArgument;
+import org.finos.legend.engine.query.sql.providers.shared.AbstractTestLegendStoreSQLSourceProvider;
+import org.finos.legend.engine.query.sql.providers.shared.SQLSourceProviderTestUtils;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateLoader;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateWrapper;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectResolvedContext;
+import org.finos.legend.engine.query.sql.providers.shared.utils.SQLProviderUtils;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.List;
+
+import static org.finos.legend.engine.query.sql.providers.shared.SQLSourceProviderTestUtils.loadPureModelContextFromResource;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TestRelationalStoreSQLSourceProvider extends AbstractTestLegendStoreSQLSourceProvider
+{
+ private static final String CONNECTION_NAME = "simple::store::DB::H2Connection";
+
+ @Mock
+ private ProjectCoordinateLoader projectCoordinateLoader;
+
+ private RelationalStoreSQLSourceProvider provider;
+
+ @Before
+ public void setup()
+ {
+ provider = new RelationalStoreSQLSourceProvider(projectCoordinateLoader);
+ }
+
+ @Override
+ protected SQLSourceProvider getProvider()
+ {
+ return provider;
+ }
+
+ @Override
+ protected ProjectCoordinateLoader getProjectCoordinateLoader()
+ {
+ return projectCoordinateLoader;
+ }
+
+ @Test
+ public void testType()
+ {
+ Assert.assertEquals("relationalStore", provider.getType());
+ }
+
+ @Test
+ public void testMissingSchema()
+ {
+ TableSource tableSource = new TableSource("relationalStore", FastList.newListWith(
+ new TableSourceArgument("store", null, "simple::store::DBForSQL"),
+ new TableSourceArgument("coordinates", null, "group:artifact:version"),
+ new TableSourceArgument("connection", null, CONNECTION_NAME)));
+
+ testError(tableSource, ProjectCoordinateWrapper.coordinates("group:artifact:version"), "'schema' parameter is required");
+ }
+
+ @Test
+ public void testMissingTable()
+ {
+ TableSource tableSource = new TableSource("relationalStore", FastList.newListWith(
+ new TableSourceArgument("store", null, "simple::store::DBForSQL"),
+ new TableSourceArgument("schema", null, "nonexistent"),
+ new TableSourceArgument("connection", null, CONNECTION_NAME),
+ new TableSourceArgument("coordinates", null, "group:artifact:version")));
+
+ testError(tableSource, ProjectCoordinateWrapper.coordinates("group:artifact:version"), "'table' parameter is required");
+ }
+
+ @Test
+ public void testDatabaseFoundSchemaNotFound()
+ {
+ testNotFound("nonexistent", "nonexistent", "No element found for 'schema'");
+ }
+
+ @Test
+ public void testDatabaseFoundSchemaFoundTableNotFound()
+ {
+ testNotFound("DBSchema", "nonexistent", "No element found for 'table'");
+ }
+
+ @Test
+ public void testSingleFromCoordinates()
+ {
+ testSuccess(
+ ProjectCoordinateWrapper.coordinates("group:artifact:version"),
+ new PureModelContextPointer(),
+ FastList.newListWith(new TableSourceArgument("coordinates", null, "group:artifact:version")),
+ FastList.newListWith(new SQLSourceArgument("coordinates", null, "group:artifact:version")));
+
+ }
+
+ @Test
+ public void testSingleFromProjectWorkspace()
+ {
+ testSuccess(
+ ProjectCoordinateWrapper.workspace("proj1", "ws1"),
+ new PureModelContextPointer(),
+ FastList.newListWith(
+ new TableSourceArgument("project", null, "proj1"),
+ new TableSourceArgument("workspace", null, "ws1")),
+ FastList.newListWith(
+ new SQLSourceArgument("project", null, "proj1"),
+ new SQLSourceArgument("workspace", null, "ws1")
+ )
+ );
+ }
+
+ @Test
+ public void testSingleFromProjectGroupWorkspace()
+ {
+ testSuccess(
+ ProjectCoordinateWrapper.groupWorkspace("proj1", "ws1"),
+ new PureModelContextPointer(),
+ FastList.newListWith(
+ new TableSourceArgument("project", null, "proj1"),
+ new TableSourceArgument("groupWorkspace", null, "ws1")),
+ FastList.newListWith(
+ new SQLSourceArgument("project", null, "proj1"),
+ new SQLSourceArgument("groupWorkspace", null, "ws1")
+ )
+ );
+ }
+
+ private void testNotFound(String schema, String table, String error)
+ {
+ PureModelContextData pmcd = loadPureModelContextFromResource("pmcd.pure", this.getClass());
+ when(projectCoordinateLoader.resolve(eq(ProjectCoordinateWrapper.coordinates("group:artifact:version")), any())).thenReturn(new ProjectResolvedContext(pmcd, pmcd));
+
+ TableSource tableSource = new TableSource("relationalStore", FastList.newListWith(
+ new TableSourceArgument("store", null, "simple::store::DBForSQL"),
+ new TableSourceArgument("connection", null, CONNECTION_NAME),
+ new TableSourceArgument("schema", null, schema),
+ new TableSourceArgument("table", null, table),
+ new TableSourceArgument("coordinates", null, "group:artifact:version")));
+
+ testError(tableSource, error);
+ }
+
+ private void testError(TableSource tableSource, ProjectCoordinateWrapper projectCoordinateWrapper, String error)
+ {
+ PureModelContextData pmcd = loadPureModelContextFromResource("pmcd.pure", this.getClass());
+ when(projectCoordinateLoader.resolve(eq(projectCoordinateWrapper), any())).thenReturn(new ProjectResolvedContext(pmcd, pmcd));
+
+ testError(tableSource, error);
+ }
+
+ private void testSuccess(ProjectCoordinateWrapper projectCoordinateWrapper, PureModelContext expectedContext, List tableSourceKeys, List sourceKeys)
+ {
+ PureModelContextData pmcd = loadPureModelContextFromResource("pmcd.pure", this.getClass());
+ when(projectCoordinateLoader.resolve(eq(projectCoordinateWrapper), any())).thenReturn(new ProjectResolvedContext(expectedContext, pmcd));
+
+ String databaseName = "simple::store::DBForSQL";
+ String schemaName = "DBSchema";
+ String tableName = "FIRM_TABLE";
+
+ TableSource tablesource = new TableSource("relationalStore", FastList.newListWith(
+ new TableSourceArgument("store", null, databaseName),
+ new TableSourceArgument("schema", null, schemaName),
+ new TableSourceArgument("table", null, tableName),
+ new TableSourceArgument("connection", null, CONNECTION_NAME)).withAll(tableSourceKeys)
+ );
+
+ List keys = FastList.newListWith(
+ new SQLSourceArgument("store", null, databaseName),
+ new SQLSourceArgument("connection", null, CONNECTION_NAME))
+ .withAll(sourceKeys)
+ .with(new SQLSourceArgument("schema", null, schemaName))
+ .with(new SQLSourceArgument("table", null, tableName));
+
+ SQLSourceResolvedContext result = provider.resolve(FastList.newListWith(tablesource), null, FastList.newList());
+
+ Lambda lambda = SQLProviderUtils.tableToTDS(databaseName, schemaName, tableName);
+
+ ConnectionPointer connectionPtr = new ConnectionPointer();
+ connectionPtr.connection = CONNECTION_NAME;
+
+ LegacyRuntime runtime = new LegacyRuntime();
+ runtime.connections = FastList.newListWith(connectionPtr);
+
+ SQLSource expected = new SQLSource("relationalStore", lambda, null, runtime, null, null, keys);
+
+ //ASSERT
+ Assert.assertEquals(FastList.newListWith(expectedContext), result.getPureModelContexts());
+ Assert.assertEquals(1, result.getSources().size());
+
+ SQLSourceProviderTestUtils.assertLogicalEquality(expected, result.getSources().get(0));
+ }
+}
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/src/test/resources/META-INF/services/org.finos.legend.engine.language.pure.grammar.from.extension.PureGrammarParserExtension b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/src/test/resources/META-INF/services/org.finos.legend.engine.language.pure.grammar.from.extension.PureGrammarParserExtension
new file mode 100644
index 00000000000..14ee6d447b1
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/src/test/resources/META-INF/services/org.finos.legend.engine.language.pure.grammar.from.extension.PureGrammarParserExtension
@@ -0,0 +1 @@
+org.finos.legend.engine.language.pure.grammar.from.RelationalGrammarParserExtension
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/src/test/resources/pmcd.pure b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/src/test/resources/pmcd.pure
new file mode 100644
index 00000000000..948018fa779
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-relationalStore/src/test/resources/pmcd.pure
@@ -0,0 +1,45 @@
+###Relational
+Database simple::store::DBForSQL
+(
+ Schema DBSchema
+ (
+ Table FIRM_TABLE
+ (
+ ID INTEGER PRIMARY KEY,
+ LEGAL_NAME VARCHAR(100)
+ )
+
+ Table PERSON_TABLE
+ (
+ ID INTEGER PRIMARY KEY,
+ FIRST_NAME VARCHAR(100),
+ LAST_NAME VARCHAR(100),
+ FIRM_ID INTEGER
+ )
+ )
+)
+
+###Connection
+RelationalDatabaseConnection simple::store::DB::H2Connection{
+ store: simple::store::DB;
+ type: H2;
+ specification: LocalH2{
+ testDataSetupSqls: [
+ 'DROP TABLE IF EXISTS PERSON_TABLE;',
+ 'CREATE TABLE PERSON_TABLE(ID INT PRIMARY KEY, FIRST_NAME VARCHAR(100), LAST_NAME VARCHAR(100), FIRM_ID INT);',
+ 'INSERT INTO PERSON_TABLE(ID,FIRST_NAME,LAST_NAME,FIRM_ID) VALUES (1,\'Peter\',\'Smith\',1);',
+ 'INSERT INTO PERSON_TABLE(ID,FIRST_NAME,LAST_NAME,FIRM_ID) VALUES (2,\'John\',\'Johnson\',1);',
+ 'INSERT INTO PERSON_TABLE(ID,FIRST_NAME,LAST_NAME,FIRM_ID) VALUES (3,\'John\',\'Hill\',1);',
+ 'INSERT INTO PERSON_TABLE(ID,FIRST_NAME,LAST_NAME,FIRM_ID) VALUES (4,\'Anthony\',\'Allen\',1)',
+ 'INSERT INTO PERSON_TABLE(ID,FIRST_NAME,LAST_NAME,FIRM_ID) VALUES (5,\'Fabrice\',\'Roberts\',2)',
+ 'INSERT INTO PERSON_TABLE(ID,FIRST_NAME,LAST_NAME,FIRM_ID) VALUES (6,\'Oliver\',\'Hill\',3)',
+ 'INSERT INTO PERSON_TABLE(ID,FIRST_NAME,LAST_NAME,FIRM_ID) VALUES (7,\'David\',\'Harris\',3)',
+ 'DROP TABLE IF EXISTS FIRM_TABLE;',
+ 'CREATE TABLE FIRM_TABLE(ID INT PRIMARY KEY, LEGAL_NAME VARCHAR(100));',
+ 'INSERT INTO FIRM_TABLE(ID,LEGAL_NAME) VALUES (1,\'Firm X\');',
+ 'INSERT INTO FIRM_TABLE(ID,LEGAL_NAME) VALUES (2,\'Firm A\');',
+ 'INSERT INTO FIRM_TABLE(ID,LEGAL_NAME) VALUES (3,\'Firm B\');'
+ ];
+ };
+ auth: DefaultH2;
+}
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/pom.xml b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/pom.xml
new file mode 100644
index 00000000000..f34c9ce12a6
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/pom.xml
@@ -0,0 +1,97 @@
+
+
+
+
+
+ org.finos.legend.engine
+ legend-engine-xt-sql-providers
+ 4.32.1-SNAPSHOT
+
+ 4.0.0
+
+ legend-engine-xt-sql-providers-service
+ jar
+ Legend Engine - XT - SQL - Providers - Service
+
+
+
+
+
+ org.finos.legend.engine
+ legend-engine-xt-sql-providers-core
+
+
+ org.finos.legend.engine
+ legend-engine-xt-sql-providers-shared
+
+
+ org.finos.legend.engine
+ legend-engine-language-pure-dsl-service
+
+
+ org.finos.legend.engine
+ legend-engine-protocol-pure
+
+
+ org.finos.legend.engine
+ legend-engine-shared-core
+
+
+
+
+
+ org.eclipse.collections
+ eclipse-collections-api
+
+
+ org.eclipse.collections
+ eclipse-collections
+
+
+
+
+
+ org.pac4j
+ pac4j-core
+
+
+
+
+
+ junit
+ junit
+ test
+
+
+ org.mockito
+ mockito-core
+
+
+ org.finos.legend.engine
+ legend-engine-xt-sql-providers-shared
+ test-jar
+ test
+
+
+ org.finos.legend.engine
+ legend-engine-xt-relationalStore-grammar
+ test
+
+
+
+
+
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/src/main/java/org/finos/legend/engine/query/sql/providers/LegendServiceSQLSourceProvider.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/src/main/java/org/finos/legend/engine/query/sql/providers/LegendServiceSQLSourceProvider.java
new file mode 100644
index 00000000000..ada0258cda0
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/src/main/java/org/finos/legend/engine/query/sql/providers/LegendServiceSQLSourceProvider.java
@@ -0,0 +1,120 @@
+// Copyright 2023 Goldman Sachs
+//
+// Licensed 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 org.finos.legend.engine.query.sql.providers;
+
+import org.eclipse.collections.api.list.MutableList;
+import org.eclipse.collections.api.tuple.Pair;
+import org.eclipse.collections.impl.list.mutable.FastList;
+import org.eclipse.collections.impl.tuple.Tuples;
+import org.eclipse.collections.impl.utility.ListIterate;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContext;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.service.KeyedExecutionParameter;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.service.PureMultiExecution;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.service.PureSingleExecution;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.service.Service;
+import org.finos.legend.engine.query.sql.providers.core.SQLContext;
+import org.finos.legend.engine.query.sql.providers.core.SQLSource;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceArgument;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceProvider;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceResolvedContext;
+import org.finos.legend.engine.query.sql.providers.core.TableSource;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateLoader;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateWrapper;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectResolvedContext;
+import org.finos.legend.engine.query.sql.providers.shared.utils.SQLProviderUtils;
+import org.finos.legend.engine.shared.core.operational.errorManagement.EngineException;
+import org.pac4j.core.profile.CommonProfile;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * This class serves for handling the **service** source type
+ *
+ * Sample Select statement
+ * select * from service('/my/service', coordinates => 'com.gs:proj1:1.0.0')
+ * select * from service('/my/service', project => 'PROD-12345', workspace => 'myWorkspace')
+ * select * from service('/my/service', project => 'PROD-12345', groupWorkspace => 'myGroupWorkspace')
+ */
+public class LegendServiceSQLSourceProvider implements SQLSourceProvider
+{
+ private static final String PATTERN = "pattern";
+ private static final String SERVICE = "service";
+
+ private final ProjectCoordinateLoader projectCoordinateLoader;
+
+ public LegendServiceSQLSourceProvider(ProjectCoordinateLoader projectCoordinateLoader)
+ {
+ this.projectCoordinateLoader = projectCoordinateLoader;
+ }
+
+ @Override
+ public String getType()
+ {
+ return SERVICE;
+ }
+
+ @Override
+ public SQLSourceResolvedContext resolve(List sources, SQLContext context, MutableList profiles)
+ {
+ MutableList> resolved = ListIterate.collect(sources, source ->
+ {
+ String pattern = source.getArgumentValueAs(PATTERN, 0, String.class, true);
+ ProjectCoordinateWrapper projectCoordinateWrapper = ProjectCoordinateWrapper.extractFromTableSource(source);
+
+ ProjectResolvedContext resolvedProject = projectCoordinateLoader.resolve(projectCoordinateWrapper, profiles);
+
+ Service service = SQLProviderUtils.extractElement("service", Service.class, resolvedProject.getData(), s -> pattern.equals(s.pattern));
+ FastList keys = FastList.newListWith(new SQLSourceArgument(PATTERN, 0, pattern));
+ projectCoordinateWrapper.addProjectCoordinatesAsSQLSourceArguments(keys);
+ SQLSource resolvedSource;
+
+ if (service.execution instanceof PureSingleExecution)
+ {
+ resolvedSource = from((PureSingleExecution) service.execution, keys);
+ }
+ else if (service.execution instanceof PureMultiExecution)
+ {
+ resolvedSource = from((PureMultiExecution) service.execution, source, keys);
+ }
+ else
+ {
+ throw new EngineException("Execution Type Unsupported");
+ }
+
+ return Tuples.pair(resolvedSource, resolvedProject.getContext());
+ });
+
+ return new SQLSourceResolvedContext(resolved.collect(Pair::getTwo), resolved.collect(Pair::getOne));
+ }
+
+ private SQLSource from(PureSingleExecution pse, List keys)
+ {
+ return new SQLSource(SERVICE, pse.func, pse.mapping, pse.runtime, pse.executionOptions, null, keys);
+ }
+
+ private SQLSource from(PureMultiExecution pme, TableSource source, List keys)
+ {
+ String key = (String) source.getArgument(pme.executionKey, -1).getValue();
+ Optional optional = ListIterate.select(pme.executionParameters, e -> e.key.equals(key)).getFirstOptional();
+
+ KeyedExecutionParameter execution = optional.orElseThrow(() -> new IllegalArgumentException("No execution found for key " + key));
+
+ keys.add(new SQLSourceArgument(pme.executionKey, null, key));
+
+ return new SQLSource(SERVICE, pme.func, execution.mapping, execution.runtime, execution.executionOptions, null, keys);
+ }
+}
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/src/test/java/org/finos/legend/engine/query/sql/providers/TestLegendServiceSQLSourceProvider.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/src/test/java/org/finos/legend/engine/query/sql/providers/TestLegendServiceSQLSourceProvider.java
new file mode 100644
index 00000000000..3a6a8759df3
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/src/test/java/org/finos/legend/engine/query/sql/providers/TestLegendServiceSQLSourceProvider.java
@@ -0,0 +1,194 @@
+// Copyright 2023 Goldman Sachs
+//
+// Licensed 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 org.finos.legend.engine.query.sql.providers;
+
+import org.eclipse.collections.api.block.function.Function;
+import org.eclipse.collections.api.block.procedure.Procedure;
+import org.eclipse.collections.impl.list.mutable.FastList;
+import org.eclipse.collections.impl.utility.ListIterate;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextPointer;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.service.KeyedExecutionParameter;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.service.PureMultiExecution;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.service.PureSingleExecution;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.service.Service;
+import org.finos.legend.engine.query.sql.providers.core.SQLSource;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceArgument;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceResolvedContext;
+import org.finos.legend.engine.query.sql.providers.core.TableSource;
+import org.finos.legend.engine.query.sql.providers.core.TableSourceArgument;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateLoader;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateWrapper;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectResolvedContext;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import static org.finos.legend.engine.query.sql.providers.shared.SQLSourceProviderTestUtils.assertLogicalEquality;
+import static org.finos.legend.engine.query.sql.providers.shared.SQLSourceProviderTestUtils.loadPureModelContextFromResource;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TestLegendServiceSQLSourceProvider
+{
+ @Mock
+ private ProjectCoordinateLoader projectCoordinateLoader;
+
+ private LegendServiceSQLSourceProvider provider;
+
+ @Before
+ public void setup()
+ {
+ provider = new LegendServiceSQLSourceProvider(projectCoordinateLoader);
+ }
+
+ @Test
+ public void testType()
+ {
+ Assert.assertEquals("service", provider.getType());
+ }
+
+ public void testSingleService(String pattern, TableSource table, Procedure pmcdSupplier, Function expectedFunc)
+ {
+ PureModelContextData pmcd = loadPureModelContextFromResource("pmcd.pure", this.getClass());
+
+ Service service = ListIterate.select(pmcd.getElementsOfType(Service.class), s -> s.pattern.equals(pattern)).getOnly();
+ PureSingleExecution execution = (PureSingleExecution) service.execution;
+
+ pmcdSupplier.accept(pmcd);
+
+ SQLSource expected = expectedFunc.apply(execution);
+
+ SQLSourceResolvedContext resolved = provider.resolve(FastList.newListWith(table), null, FastList.newList());
+ Assert.assertNotNull(resolved.getPureModelContext());
+ Assert.assertEquals(1, resolved.getSources().size());
+
+ assertLogicalEquality(expected, resolved.getSources().get(0));
+ }
+
+ @Test
+ public void testSingleServicePatternAndCoordinates()
+ {
+ String pattern = "/people";
+ TableSource table = new TableSource("service", FastList.newListWith(
+ new TableSourceArgument("pattern", 0, pattern),
+ new TableSourceArgument("coordinates", null, "group:artifact:version")
+ ));
+
+ testSingleService(pattern, table, pmcd ->
+ {
+ PureModelContextPointer pointer = new PureModelContextPointer();
+ when(projectCoordinateLoader.resolve(eq(ProjectCoordinateWrapper.coordinates("group:artifact:version")), any())).thenReturn(new ProjectResolvedContext(pointer, pmcd));
+ }, execution -> new SQLSource("service", execution.func, execution.mapping, execution.runtime, execution.executionOptions, null, FastList.newListWith(
+ new SQLSourceArgument("pattern", 0, pattern),
+ new SQLSourceArgument("coordinates", null, "group:artifact:version")
+ )));
+ }
+
+ @Test
+ public void testMultiServicePatternAndCoordinates()
+ {
+ String pattern = "/people/{key}";
+ PureModelContextData pmcd = loadPureModelContextFromResource("pmcd.pure", this.getClass());
+
+
+ PureModelContextPointer pointer = new PureModelContextPointer();
+ when(projectCoordinateLoader.resolve(eq(ProjectCoordinateWrapper.coordinates("group:artifact:version")), any())).thenReturn(new ProjectResolvedContext(pointer, pmcd));
+
+ Service service = ListIterate.select(pmcd.getElementsOfType(Service.class), s -> s.pattern.equals(pattern)).getOnly();
+ PureMultiExecution multi = (PureMultiExecution) service.execution;
+ KeyedExecutionParameter execution = multi.executionParameters.get(1);
+
+
+ TableSource table = new TableSource("service", FastList.newListWith(
+ new TableSourceArgument("pattern", 0, pattern),
+ new TableSourceArgument("key", null, "k2"),
+ new TableSourceArgument("coordinates", null, "group:artifact:version")
+ ));
+
+ SQLSource expected = new SQLSource("service", multi.func, execution.mapping, execution.runtime, execution.executionOptions, null, FastList.newListWith(
+ new SQLSourceArgument("pattern", 0, pattern),
+ new SQLSourceArgument("coordinates", null, "group:artifact:version"),
+ new SQLSourceArgument("key", null, "k2")
+ ));
+
+ SQLSourceResolvedContext resolved = provider.resolve(FastList.newListWith(table), null, FastList.newList());
+ Assert.assertEquals(FastList.newListWith(pointer), resolved.getPureModelContexts());
+ Assert.assertEquals(1, resolved.getSources().size());
+
+ assertLogicalEquality(expected, resolved.getSources().get(0));
+ }
+
+ @Test
+ public void testSingleServicePatternPatternAndWorkspace()
+ {
+ String pattern = "/people";
+ TableSource table = new TableSource("service", FastList.newListWith(
+ new TableSourceArgument("pattern", 0, pattern),
+ new TableSourceArgument("project", null, "p1"),
+ new TableSourceArgument("workspace", null, "ws")
+ ));
+
+ testSingleService(pattern, table, pmcd ->
+ when(projectCoordinateLoader.resolve(eq(ProjectCoordinateWrapper.workspace("p1", "ws")), any())).thenReturn(new ProjectResolvedContext(pmcd, pmcd)),
+ execution -> new SQLSource("service", execution.func, execution.mapping, execution.runtime, execution.executionOptions, null, FastList.newListWith(
+ new SQLSourceArgument("pattern", 0, pattern),
+ new SQLSourceArgument("project", null, "p1"),
+ new SQLSourceArgument("workspace", null, "ws")
+ )));
+
+ }
+
+ @Test
+ public void testSingleServicePatternPatternAndGroupWorkspace()
+ {
+ String pattern = "/people";
+ TableSource table = new TableSource("service", FastList.newListWith(
+ new TableSourceArgument("pattern", 0, pattern),
+ new TableSourceArgument("project", null, "p1"),
+ new TableSourceArgument("groupWorkspace", null, "gws")
+ ));
+
+ testSingleService(pattern, table, pmcd ->
+ when(projectCoordinateLoader.resolve(eq(ProjectCoordinateWrapper.groupWorkspace("p1", "gws")), any())).thenReturn(new ProjectResolvedContext(pmcd, pmcd)),
+ execution -> new SQLSource("service", execution.func, execution.mapping, execution.runtime, execution.executionOptions, null, FastList.newListWith(
+ new SQLSourceArgument("pattern", 0, pattern),
+ new SQLSourceArgument("project", null, "p1"),
+ new SQLSourceArgument("groupWorkspace", null, "gws")
+ )));
+
+ }
+
+ @Test
+ public void testNoServiceFound()
+ {
+ PureModelContextData pmcd = loadPureModelContextFromResource("pmcd.pure", this.getClass());
+ when(projectCoordinateLoader.resolve(eq(ProjectCoordinateWrapper.workspace("p1", "ws")), any())).thenReturn(new ProjectResolvedContext(pmcd, pmcd));
+
+ TableSource table = new TableSource("service", FastList.newListWith(
+ new TableSourceArgument("pattern", 0, "notfound"),
+ new TableSourceArgument("project", null, "p1"),
+ new TableSourceArgument("workspace", null, "ws")
+ ));
+ IllegalArgumentException exception = Assert.assertThrows("Should throw given no service found", IllegalArgumentException.class, () -> provider.resolve(FastList.newListWith(table), null, FastList.newList()));
+ Assert.assertEquals("No element found for 'service'", exception.getMessage());
+ }
+}
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/src/test/resources/META-INF/services/org.finos.legend.engine.language.pure.grammar.from.extension.PureGrammarParserExtension b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/src/test/resources/META-INF/services/org.finos.legend.engine.language.pure.grammar.from.extension.PureGrammarParserExtension
new file mode 100644
index 00000000000..14ee6d447b1
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/src/test/resources/META-INF/services/org.finos.legend.engine.language.pure.grammar.from.extension.PureGrammarParserExtension
@@ -0,0 +1 @@
+org.finos.legend.engine.language.pure.grammar.from.RelationalGrammarParserExtension
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/src/test/resources/pmcd.pure b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/src/test/resources/pmcd.pure
new file mode 100644
index 00000000000..fa96a3b66fb
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-service/src/test/resources/pmcd.pure
@@ -0,0 +1,125 @@
+###Pure
+import simple::model::*;
+
+Class simple::model::Person
+{
+ firstName: String[1];
+ lastName: String[1];
+}
+
+###Relational
+Database simple::store::DB
+(
+ Table PERSON_TABLE
+ (
+ FIRST_NAME VARCHAR(100),
+ LAST_NAME VARCHAR(100)
+ )
+)
+
+###Mapping
+import simple::model::*;
+import simple::store::*;
+
+Mapping simple::mapping::Mapping
+(
+ Person : Relational
+ {
+ firstName: [DB]PERSON_TABLE.FIRST_NAME,
+ lastName: [DB]PERSON_TABLE.LAST_NAME
+ }
+)
+
+Mapping simple::mapping::Mapping2
+(
+ Person : Relational
+ {
+ firstName: [DB]PERSON_TABLE.FIRST_NAME,
+ lastName: [DB]PERSON_TABLE.LAST_NAME
+ }
+)
+
+###Runtime
+Runtime simple::runtime::Runtime
+{
+ mappings :
+ [
+ simple::mapping::Mapping
+ ];
+ connections :
+ [
+ simple::store::DB :
+ [
+ connection_1 : #{
+ RelationalDatabaseConnection {
+ store: simple::store::DB;
+ type: H2;
+ specification: LocalH2{
+ testDataSetupSqls: [
+ 'DROP TABLE IF EXISTS PERSON_TABLE;',
+ 'CREATE TABLE PERSON_TABLE(ID INT PRIMARY KEY, FIRST_NAME VARCHAR(100), LAST_NAME VARCHAR(100), FIRM_ID INT);',
+ 'INSERT INTO PERSON_TABLE(ID,FIRST_NAME,LAST_NAME,FIRM_ID) VALUES (1,\'Peter\',\'Smith\',1);'
+ ];
+ };
+ auth: DefaultH2;
+ }
+ }#
+ ]
+ ];
+}
+
+###Service
+Service simple::service::PeopleService
+{
+ pattern: '/people';
+ owners:
+ [
+ 'person1',
+ 'person2'
+ ];
+ documentation: '';
+ autoActivateUpdates: true;
+ execution: Single
+ {
+ query: {|
+ simple::model::Person.all()->project([
+ col(x | $x.firstName, 'first name'),
+ col(x | $x.lastName, 'last name')
+ ])
+ };
+ mapping: simple::mapping::Mapping;
+ runtime: simple::runtime::Runtime;
+ }
+}
+
+Service simple::service::MultiExecutionService
+{
+ pattern: '/people/{key}';
+ owners:
+ [
+ 'person1',
+ 'person2'
+ ];
+ documentation: '';
+ autoActivateUpdates: true;
+ execution: Multi
+ {
+ query: {|
+ simple::model::Person.all()->project([
+ col(x | $x.firstName, 'first name'),
+ col(x | $x.lastName, 'last name')
+ ])
+ };
+ key: 'key';
+ executions['k1']:
+ {
+ mapping: simple::mapping::Mapping;
+ runtime: simple::runtime::Runtime;
+ }
+ executions['k2']:
+ {
+ mapping: simple::mapping::Mapping2;
+ runtime: simple::runtime::Runtime;
+ }
+ }
+}
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/pom.xml b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/pom.xml
new file mode 100644
index 00000000000..25e0e30f21e
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/pom.xml
@@ -0,0 +1,159 @@
+
+
+
+
+
+ org.finos.legend.engine
+ legend-engine-xt-sql-providers
+ 4.32.1-SNAPSHOT
+
+ 4.0.0
+
+ legend-engine-xt-sql-providers-shared
+ jar
+ Legend Engine - XT - SQL - Providers - Shared
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+
+
+
+
+
+
+
+ javax.ws.rs
+ javax.ws.rs-api
+
+
+
+
+ org.finos.legend.engine
+ legend-engine-protocol-pure
+
+
+ org.finos.legend.engine
+ legend-engine-xt-sql-providers-core
+
+
+ org.finos.legend.engine
+ legend-engine-language-pure-modelManager-sdlc
+
+
+ org.finos.legend.engine
+ legend-engine-language-pure-grammar
+
+
+ org.finos.legend.engine
+ legend-engine-language-pure-modelManager
+
+
+ org.finos.legend.engine
+ legend-engine-shared-core
+
+
+
+
+
+
+ org.eclipse.collections
+ eclipse-collections-api
+
+
+ org.eclipse.collections
+ eclipse-collections
+
+
+
+
+
+ org.apache.httpcomponents
+ httpclient
+
+
+ org.apache.httpcomponents
+ httpcore
+
+
+
+
+
+ org.pac4j
+ pac4j-core
+
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+
+
+
+ io.opentracing
+ opentracing-util
+
+
+ io.opentracing
+ opentracing-api
+
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
+
+ junit
+ junit
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ commons-io
+ commons-io
+ test
+
+
+
+
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/AbstractLegendStoreSQLSourceProvider.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/AbstractLegendStoreSQLSourceProvider.java
new file mode 100644
index 00000000000..c55a8bfad34
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/AbstractLegendStoreSQLSourceProvider.java
@@ -0,0 +1,83 @@
+// Copyright 2023 Goldman Sachs
+//
+// Licensed 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 org.finos.legend.engine.query.sql.providers.shared;
+
+import org.eclipse.collections.api.list.MutableList;
+import org.eclipse.collections.impl.list.mutable.FastList;
+import org.eclipse.collections.impl.utility.ListIterate;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContext;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.connection.PackageableConnection;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.store.Store;
+import org.finos.legend.engine.query.sql.providers.core.SQLContext;
+import org.finos.legend.engine.query.sql.providers.core.SQLSource;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceArgument;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceProvider;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceResolvedContext;
+import org.finos.legend.engine.query.sql.providers.core.TableSource;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateLoader;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateWrapper;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectResolvedContext;
+import org.finos.legend.engine.query.sql.providers.shared.utils.SQLProviderUtils;
+import org.pac4j.core.profile.CommonProfile;
+
+import java.util.List;
+
+public abstract class AbstractLegendStoreSQLSourceProvider implements SQLSourceProvider
+{
+
+ private static final String ARG_CONNECTION = "connection";
+ private static final String ARG_STORE = "store";
+
+ private final Class storeType;
+ private final ProjectCoordinateLoader projectCoordinateLoader;
+
+ public AbstractLegendStoreSQLSourceProvider(Class storeType, ProjectCoordinateLoader projectCoordinateLoader)
+ {
+ this.storeType = storeType;
+ this.projectCoordinateLoader = projectCoordinateLoader;
+ }
+
+ protected abstract SQLSource createSource(TableSource source, T store, PackageableConnection connection, List keys, PureModelContextData pmcd);
+
+ @Override
+ public SQLSourceResolvedContext resolve(List sources, SQLContext context, MutableList profiles)
+ {
+ List contexts = FastList.newList();
+ List sqlSources = FastList.newList();
+
+ ListIterate.forEach(sources, source ->
+ {
+ ProjectCoordinateWrapper projectCoordinateWrapper = ProjectCoordinateWrapper.extractFromTableSource(source);
+ ProjectResolvedContext resolved = projectCoordinateLoader.resolve(projectCoordinateWrapper, profiles);
+
+ String storeName = source.getArgumentValueAs(ARG_STORE, -1, String.class, true);
+ String connectionName = source.getArgumentValueAs(ARG_CONNECTION, -1, String.class, true);
+
+ T store = SQLProviderUtils.extractElement(ARG_STORE, this.storeType, resolved.getData(), s -> storeName.equals(s.getPath()));
+ PackageableConnection connection = SQLProviderUtils.extractElement(ARG_CONNECTION, PackageableConnection.class, resolved.getData(), c -> connectionName.equals(c.getPath()));
+
+ List keys = FastList.newListWith(new SQLSourceArgument(ARG_STORE, null, storeName), new SQLSourceArgument(ARG_CONNECTION, null, connectionName));
+ projectCoordinateWrapper.addProjectCoordinatesAsSQLSourceArguments(keys);
+
+ sqlSources.add(createSource(source, store, connection, keys, resolved.getData()));
+
+ contexts.add(resolved.getContext());
+ });
+
+ return new SQLSourceResolvedContext(contexts, sqlSources);
+ }
+}
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/FunctionSQLSourceProvider.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/FunctionSQLSourceProvider.java
new file mode 100644
index 00000000000..7c2a3312601
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/FunctionSQLSourceProvider.java
@@ -0,0 +1,104 @@
+// Copyright 2023 Goldman Sachs
+//
+// Licensed 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 org.finos.legend.engine.query.sql.providers.shared;
+
+import org.eclipse.collections.api.factory.Sets;
+import org.eclipse.collections.api.list.MutableList;
+import org.eclipse.collections.api.set.ImmutableSet;
+import org.eclipse.collections.api.tuple.Pair;
+import org.eclipse.collections.impl.list.mutable.FastList;
+import org.eclipse.collections.impl.tuple.Tuples;
+import org.eclipse.collections.impl.utility.ListIterate;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContext;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Function;
+import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.raw.Lambda;
+import org.finos.legend.engine.query.sql.providers.core.SQLContext;
+import org.finos.legend.engine.query.sql.providers.core.SQLSource;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceArgument;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceProvider;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceResolvedContext;
+import org.finos.legend.engine.query.sql.providers.core.TableSource;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateLoader;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateWrapper;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectResolvedContext;
+import org.finos.legend.engine.query.sql.providers.shared.utils.SQLProviderUtils;
+import org.finos.legend.engine.shared.core.operational.errorManagement.EngineException;
+import org.pac4j.core.profile.CommonProfile;
+
+import java.util.List;
+
+/**
+ * This class serves for handling the **function** source type
+ *
+ * Sample Select statement
+ * select * from func('my::func__TabularDataSet_1_', coordinates => 'com.gs:proj1:1.0.0')
+ * select * from func('my::func__TabularDataSet_1_', project => 'PROD-12345', workspace => 'myWorkspace')
+ * select * from func('my::func__TabularDataSet_1_', project => 'PROD-12345', groupWorkspace => 'myGroupWorkspace')
+ * select * from func('my::func_String_1__TabularDataSet_1_', project => 'PROD-12345', groupWorkspace => 'myGroupWorkspace', myParam => 'abc')
+ */
+public class FunctionSQLSourceProvider implements SQLSourceProvider
+{
+
+ private static final String FUNCTION = "func";
+ private static final String PATH = "path";
+
+ private static final ImmutableSet TABULAR_TYPES = Sets.immutable.of(
+ "meta::pure::tds::TabularDataSet"
+ );
+
+ private final ProjectCoordinateLoader projectCoordinateLoader;
+
+ public FunctionSQLSourceProvider(ProjectCoordinateLoader projectCoordinateLoader)
+ {
+ this.projectCoordinateLoader = projectCoordinateLoader;
+ }
+
+ @Override
+ public String getType()
+ {
+ return FUNCTION;
+ }
+
+ @Override
+ public SQLSourceResolvedContext resolve(List sources, SQLContext context, MutableList profiles)
+ {
+ MutableList> resolved = ListIterate.collect(sources, source ->
+ {
+ String path = source.getArgumentValueAs(PATH, 0, String.class, true);
+ ProjectCoordinateWrapper projectCoordinateWrapper = ProjectCoordinateWrapper.extractFromTableSource(source);
+
+ ProjectResolvedContext resolvedProject = projectCoordinateLoader.resolve(projectCoordinateWrapper, profiles);
+
+ Function function = SQLProviderUtils.extractElement("function", Function.class, resolvedProject.getData(), f -> path.equals(f.getPath()));
+
+ if (!TABULAR_TYPES.contains(function.returnType))
+ {
+ throw new EngineException("Function " + path + " does not return Tabular data type");
+ }
+
+ Lambda lambda = new Lambda();
+ lambda.parameters = function.parameters;
+ lambda.body = function.body;
+
+ List keys = FastList.newListWith(new SQLSourceArgument(PATH, 0, path));
+ projectCoordinateWrapper.addProjectCoordinatesAsSQLSourceArguments(keys);
+
+ return Tuples.pair(new SQLSource(getType(), lambda, null, null, FastList.newList(), null, keys), resolvedProject.getContext());
+ });
+
+ return new SQLSourceResolvedContext(resolved.collect(Pair::getTwo), resolved.collect(Pair::getOne));
+ }
+}
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/project/ProjectCoordinateLoader.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/project/ProjectCoordinateLoader.java
new file mode 100644
index 00000000000..95aa2f04a02
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/project/ProjectCoordinateLoader.java
@@ -0,0 +1,252 @@
+// Copyright 2023 Goldman Sachs
+//
+// Licensed 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 org.finos.legend.engine.query.sql.providers.shared.project;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.BasicCookieStore;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.eclipse.collections.api.block.function.Function;
+import org.eclipse.collections.api.list.MutableList;
+import org.finos.legend.engine.language.pure.modelManager.ModelManager;
+import org.finos.legend.engine.language.pure.modelManager.sdlc.SDLCLoader;
+import org.finos.legend.engine.language.pure.modelManager.sdlc.configuration.ServerConnectionConfiguration;
+import org.finos.legend.engine.protocol.pure.PureClientVersions;
+import org.finos.legend.engine.protocol.pure.v1.model.context.AlloySDLC;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextPointer;
+import org.finos.legend.engine.query.sql.providers.shared.utils.TraceUtils;
+import org.finos.legend.engine.shared.core.ObjectMapperFactory;
+import org.finos.legend.engine.shared.core.kerberos.HttpClientBuilder;
+import org.finos.legend.engine.shared.core.kerberos.ProfileManagerHelper;
+import org.finos.legend.engine.shared.core.operational.errorManagement.EngineException;
+import org.finos.legend.engine.shared.core.operational.logs.LoggingEventType;
+import org.pac4j.core.profile.CommonProfile;
+
+import javax.security.auth.Subject;
+import javax.ws.rs.core.Response;
+import java.security.PrivilegedAction;
+import java.util.List;
+import java.util.Optional;
+
+public class ProjectCoordinateLoader
+{
+ private static final ObjectMapper OBJECT_MAPPER = ObjectMapperFactory.getNewStandardObjectMapperWithPureProtocolExtensionSupports();
+
+ private final ModelManager modelManager;
+ private final ServerConnectionConfiguration sdlcServerConfig;
+ private final Function, CloseableHttpClient> httpClientProvider;
+
+ public ProjectCoordinateLoader(ModelManager modelManager, ServerConnectionConfiguration sdlcServerConfig)
+ {
+ this(modelManager, sdlcServerConfig, profiles -> (CloseableHttpClient) HttpClientBuilder.getHttpClient(new BasicCookieStore()));
+ }
+
+ public ProjectCoordinateLoader(ModelManager modelManager, ServerConnectionConfiguration sdlcServerConfig, Function, CloseableHttpClient> httpClientProvider)
+ {
+ this.modelManager = modelManager;
+ this.sdlcServerConfig = sdlcServerConfig;
+ this.httpClientProvider = httpClientProvider;
+ }
+
+ public ProjectResolvedContext resolve(ProjectCoordinateWrapper projectCoordinateWrapper, MutableList profiles)
+ {
+ return resolve(projectCoordinateWrapper, true, profiles);
+ }
+
+ public ProjectResolvedContext resolve(ProjectCoordinateWrapper projectCoordinateWrapper, boolean required, MutableList profiles)
+ {
+ Optional coordinates = projectCoordinateWrapper.getCoordinates();
+ if (coordinates.isPresent())
+ {
+ PureModelContextPointer pointer = pointerFromCoordinates(coordinates.get());
+
+ PureModelContextData pmcd = modelManager.loadData(pointer, PureClientVersions.production, profiles);
+
+ return new ProjectResolvedContext(pointer, pmcd);
+ }
+ Optional project = projectCoordinateWrapper.getProject();
+ if (project.isPresent())
+ {
+ Optional workspace = projectCoordinateWrapper.getWorkspace();
+ Optional groupWorkspace = projectCoordinateWrapper.getGroupWorkspace();
+ String workspaceId = workspace.orElseGet(groupWorkspace::get);
+ boolean isGroup = groupWorkspace.isPresent();
+ String projectId = project.get();
+
+ PureModelContextData pmcd = loadProjectPureModelContextData(projectId, workspaceId, isGroup, profiles);
+
+ return new ProjectResolvedContext(pmcd, pmcd);
+ }
+
+ if (required)
+ {
+ throw new EngineException("project/workspace or coordinates must be supplied");
+ }
+
+ return null;
+ }
+
+ private PureModelContextPointer pointerFromCoordinates(String coordinates)
+ {
+ AlloySDLC sdlc = new AlloySDLC();
+ enrichCoordinates(sdlc, coordinates);
+ PureModelContextPointer pointer = new PureModelContextPointer();
+ pointer.sdlcInfo = sdlc;
+ return pointer;
+ }
+
+ private void enrichCoordinates(AlloySDLC alloySDLC, String coordinates)
+ {
+ String[] parts = coordinates.split(":");
+ if (parts.length != 3)
+ {
+ throw new IllegalArgumentException("Invalid coordinates on service " + coordinates);
+ }
+
+ alloySDLC.groupId = parts[0];
+ alloySDLC.artifactId = parts[1];
+ alloySDLC.version = parts[2];
+ }
+
+ private PureModelContextData loadProjectPureModelContextData(String project, String workspace, boolean isGroup, MutableList profiles)
+ {
+ return doAs(ProfileManagerHelper.extractSubject(profiles), () ->
+ {
+ String url = String.format("%s/api/projects/%s/%s/%s/pureModelContextData", sdlcServerConfig.getBaseUrl(), project, isGroup ? "groupWorkspaces" : "workspaces", workspace);
+ PureModelContextData projectPMCD = SDLCLoader.loadMetadataFromHTTPURL(profiles, LoggingEventType.METADATA_REQUEST_ALLOY_PROJECT_START, LoggingEventType.METADATA_REQUEST_ALLOY_PROJECT_STOP, url, httpClientProvider);
+ PureModelContextData dependencyPMCD = getSDLCDependenciesPMCD(project, workspace, isGroup, profiles);
+
+ return projectPMCD.combine(dependencyPMCD);
+ });
+ }
+
+ private PureModelContextData getSDLCDependenciesPMCD(String project, String workspace, boolean isGroup, MutableList profiles)
+ {
+ return TraceUtils.trace("Get SDLC Dependencies", span ->
+ {
+
+ span.setTag("project", project);
+ span.setTag("workspace", workspace);
+ span.setTag("group", isGroup);
+
+ if (sdlcServerConfig == null)
+ {
+ throw new EngineException("SDLC Server configuration must be supplied");
+ }
+ try (CloseableHttpClient client = this.httpClientProvider.apply(profiles))
+ {
+ String url = String.format("%s/api/projects/%s/%s/%s/revisions/HEAD/upstreamProjects",
+ sdlcServerConfig.getBaseUrl(),
+ project,
+ isGroup ? "groupWorkspaces" : "workspaces",
+ workspace);
+
+ try (CloseableHttpResponse response = client.execute(new HttpGet(url)))
+ {
+ StatusLine status = response.getStatusLine();
+ if (!Response.Status.Family.familyOf(status.getStatusCode()).equals(Response.Status.Family.SUCCESSFUL))
+ {
+ throw new RuntimeException(String.format("Status Code: %s\nReason: %s\nMessage: %s",
+ status.getStatusCode(), status.getReasonPhrase(), "Error fetching from " + url));
+ }
+
+ List dependencies = OBJECT_MAPPER.readValue(EntityUtils.toString(response.getEntity()), new TypeReference>()
+ {
+ });
+ PureModelContextData.Builder builder = PureModelContextData.newBuilder();
+ dependencies.forEach(dependency ->
+ {
+ try
+ {
+ builder.addPureModelContextData(loadProjectData(profiles, dependency.getGroupId(), dependency.getArtifactId(), dependency.versionId));
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ });
+ builder.removeDuplicates();
+ return builder.build();
+ }
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ private PureModelContextData loadProjectData(MutableList profiles, String groupId, String artifactId, String versionId)
+ {
+ return TraceUtils.trace("Loading Project Data", span ->
+ {
+
+ span.setTag("groupId", groupId);
+ span.setTag("artifactId", artifactId);
+ span.setTag("versionId", versionId);
+
+ Subject subject = ProfileManagerHelper.extractSubject(profiles);
+ PureModelContextPointer pointer = new PureModelContextPointer();
+ AlloySDLC sdlcInfo = new AlloySDLC();
+ sdlcInfo.groupId = groupId;
+ sdlcInfo.artifactId = artifactId;
+ sdlcInfo.version = versionId;
+ pointer.sdlcInfo = sdlcInfo;
+
+ return doAs(subject, () -> this.modelManager.loadData(pointer, PureClientVersions.production, profiles));
+ });
+ }
+
+ private T doAs(Subject subject, PrivilegedAction action)
+ {
+ return subject != null ? Subject.doAs(subject, action) : action.run();
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ private static class SDLCProjectDependency
+ {
+ private final String projectId;
+ private final String versionId;
+
+ public SDLCProjectDependency(@JsonProperty("projectId") String projectId, @JsonProperty("versionId") String versionId)
+ {
+ this.projectId = projectId;
+ this.versionId = versionId;
+ }
+
+ public String getGroupId()
+ {
+ return projectId.split(":")[0];
+ }
+
+ public String getArtifactId()
+ {
+ return projectId.split(":")[1];
+ }
+
+ public String getVersionId()
+ {
+ return versionId;
+ }
+ }
+}
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/project/ProjectCoordinateWrapper.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/project/ProjectCoordinateWrapper.java
new file mode 100644
index 00000000000..76e453291c0
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/project/ProjectCoordinateWrapper.java
@@ -0,0 +1,137 @@
+// Copyright 2023 Goldman Sachs
+//
+// Licensed 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 org.finos.legend.engine.query.sql.providers.shared.project;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceArgument;
+import org.finos.legend.engine.query.sql.providers.core.TableSource;
+
+import java.util.List;
+import java.util.Optional;
+
+public class ProjectCoordinateWrapper
+{
+
+ private static final String ARG_COORDINATES = "coordinates";
+ private static final String ARG_PROJECT = "project";
+ private static final String ARG_WORKSPACE = "workspace";
+ private static final String ARG_GROUP_WORKSPACE = "groupWorkspace";
+
+ private final Optional coordinates;
+ private final Optional project;
+ private final Optional workspace;
+ private final Optional groupWorkspace;
+
+ private ProjectCoordinateWrapper(Optional coordinates, Optional project, Optional workspace, Optional groupWorkspace)
+ {
+ this.coordinates = coordinates;
+ this.project = project;
+ this.workspace = workspace;
+ this.groupWorkspace = groupWorkspace;
+ }
+
+ public static ProjectCoordinateWrapper coordinates(String coordinates)
+ {
+ return new ProjectCoordinateWrapper(Optional.of(coordinates), Optional.empty(), Optional.empty(), Optional.empty());
+ }
+
+ public static ProjectCoordinateWrapper workspace(String project, String workspace)
+ {
+ return new ProjectCoordinateWrapper(Optional.empty(), Optional.of(project), Optional.of(workspace), Optional.empty());
+ }
+
+ public static ProjectCoordinateWrapper groupWorkspace(String project, String groupWorkspace)
+ {
+ return new ProjectCoordinateWrapper(Optional.empty(), Optional.of(project), Optional.empty(), Optional.of(groupWorkspace));
+ }
+
+ public static ProjectCoordinateWrapper extractFromTableSource(TableSource source)
+ {
+ return extractFromTableSource(source, true);
+ }
+
+ public static ProjectCoordinateWrapper extractFromTableSource(TableSource source, boolean required)
+ {
+ Optional coordinates = Optional.ofNullable(source.getArgumentValueAs(ARG_COORDINATES, -1, String.class, false));
+ Optional project = Optional.ofNullable(source.getArgumentValueAs(ARG_PROJECT, -1, String.class, false));
+ Optional workspace = Optional.ofNullable(source.getArgumentValueAs(ARG_WORKSPACE, -1, String.class, false));
+ Optional groupWorkspace = Optional.ofNullable(source.getArgumentValueAs(ARG_GROUP_WORKSPACE, -1, String.class, false));
+
+ validateArguments(coordinates, project, workspace, groupWorkspace, required);
+
+ return new ProjectCoordinateWrapper(coordinates, project, workspace, groupWorkspace);
+ }
+
+ public void addProjectCoordinatesAsSQLSourceArguments(List keys)
+ {
+ coordinates.ifPresent(value -> keys.add(new SQLSourceArgument(ARG_COORDINATES, null, value)));
+ project.ifPresent(value -> keys.add(new SQLSourceArgument(ARG_PROJECT, null, value)));
+ workspace.ifPresent(value -> keys.add(new SQLSourceArgument(ARG_WORKSPACE, null, value)));
+ groupWorkspace.ifPresent(value -> keys.add(new SQLSourceArgument(ARG_GROUP_WORKSPACE, null, value)));
+ }
+
+ private static void validateArguments(Optional coordinates, Optional project, Optional workspace, Optional groupWorkspace, boolean required)
+ {
+ if (coordinates.isPresent() && (project.isPresent() || workspace.isPresent() || groupWorkspace.isPresent()))
+ {
+ throw new IllegalArgumentException("cannot mix coordinates with project/workspace");
+ }
+ if (project.isPresent() && !(workspace.isPresent() || groupWorkspace.isPresent()))
+ {
+ throw new IllegalArgumentException("workspace/group workspace must be supplied if loading from project");
+ }
+
+ if (required && !(coordinates.isPresent() || project.isPresent()))
+ {
+ throw new IllegalArgumentException("coordinates or project/workspace must be supplied");
+ }
+ }
+
+
+ public Optional getCoordinates()
+ {
+ return coordinates;
+ }
+
+ public Optional getProject()
+ {
+ return project;
+ }
+
+ public Optional getWorkspace()
+ {
+ return workspace;
+ }
+
+ public Optional getGroupWorkspace()
+ {
+ return groupWorkspace;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ return EqualsBuilder.reflectionEquals(this, o);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return HashCodeBuilder.reflectionHashCode(this);
+ }
+
+}
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/project/ProjectResolvedContext.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/project/ProjectResolvedContext.java
new file mode 100644
index 00000000000..8546465c896
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/project/ProjectResolvedContext.java
@@ -0,0 +1,45 @@
+// Copyright 2023 Goldman Sachs
+//
+// Licensed 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 org.finos.legend.engine.query.sql.providers.shared.project;
+
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContext;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData;
+
+/**
+ * This class acts as a holder for a ProjectCoordinateWrapper resolved data
+ */
+public class ProjectResolvedContext
+{
+ /** this will be the smallest unit possible, eg. a pointer instead of the full pmcd if available*/
+ private final PureModelContext context;
+ private final PureModelContextData data;
+
+ public ProjectResolvedContext(PureModelContext context, PureModelContextData data)
+ {
+ this.context = context;
+ this.data = data;
+ }
+
+ public PureModelContext getContext()
+ {
+ return context;
+ }
+
+ public PureModelContextData getData()
+ {
+ return data;
+ }
+}
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/utils/SQLProviderUtils.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/utils/SQLProviderUtils.java
new file mode 100644
index 00000000000..0e74abaf1ed
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/utils/SQLProviderUtils.java
@@ -0,0 +1,83 @@
+// Copyright 2023 Goldman Sachs
+//
+// Licensed 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 org.finos.legend.engine.query.sql.providers.shared.utils;
+
+import org.eclipse.collections.api.list.MutableList;
+import org.eclipse.collections.impl.list.mutable.FastList;
+import org.eclipse.collections.impl.utility.ListIterate;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.PackageableElement;
+import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.application.AppliedFunction;
+import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.raw.CString;
+import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.raw.Lambda;
+import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.raw.PackageableElementPtr;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Predicate;
+
+public class SQLProviderUtils
+{
+
+ public static T extractElement(String argumentName, Class type, PureModelContextData pmcd, Predicate predicate)
+ {
+ return extractElement(argumentName, pmcd.getElementsOfType(type), predicate);
+ }
+
+ public static T extractElement(String argumentName, List list, Predicate predicate)
+ {
+ MutableList elements = ListIterate.select(list,
+ element -> predicate.test(element));
+
+ if (elements.isEmpty())
+ {
+ throw new IllegalArgumentException("No element found for '" + argumentName + "'");
+ }
+
+ if (elements.size() > 1)
+ {
+ throw new IllegalArgumentException("Multiple elements found for '" + argumentName + "'");
+ }
+
+ return elements.getOnly();
+ }
+
+ public static Lambda tableToTDS(String databasePath, String schemaName, String tableName)
+ {
+ PackageableElementPtr databasePtr = new PackageableElementPtr();
+ databasePtr.fullPath = databasePath;
+
+ AppliedFunction tableReferenceFunc = new AppliedFunction();
+ tableReferenceFunc.function = "tableReference";
+ tableReferenceFunc.fControl = "tableReference_Database_1__String_1__String_1__Table_1_";
+ tableReferenceFunc.parameters = FastList.newListWith(databasePtr, new CString(schemaName), new CString(tableName));
+
+ AppliedFunction tableToTdsFunc = new AppliedFunction();
+ tableToTdsFunc.function = "tableToTDS";
+ tableToTdsFunc.fControl = "tableToTDS_Table_1__TableTDS_1_";
+ tableToTdsFunc.parameters = Collections.singletonList(tableReferenceFunc);
+
+ Lambda lambda = new Lambda();
+ lambda.body = Collections.singletonList(tableToTdsFunc);
+
+ return lambda;
+ }
+
+ public static boolean equalsEscaped(String value, String toMatch)
+ {
+ return value.equals(toMatch) || value.equals("\"" + toMatch + "\"");
+ }
+}
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/utils/TraceUtils.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/utils/TraceUtils.java
new file mode 100644
index 00000000000..baed8276557
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/main/java/org/finos/legend/engine/query/sql/providers/shared/utils/TraceUtils.java
@@ -0,0 +1,72 @@
+// Copyright 2023 Goldman Sachs
+//
+// Licensed 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 org.finos.legend.engine.query.sql.providers.shared.utils;
+
+import io.opentracing.Scope;
+import io.opentracing.Span;
+import io.opentracing.util.GlobalTracer;
+import org.eclipse.collections.api.block.procedure.Procedure;
+
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class TraceUtils
+{
+ private static final String PREFIX = "Legend SQL: ";
+
+ public static void trace(String name, Procedure procedure)
+ {
+ Span span = GlobalTracer.get().buildSpan(PREFIX + name).start();
+
+ try (Scope ignored = GlobalTracer.get().activateSpan(span))
+ {
+ procedure.accept(span);
+ }
+ finally
+ {
+ span.finish();
+ }
+ }
+
+ public static T trace(String name, Supplier supplier)
+ {
+
+ Span span = GlobalTracer.get().buildSpan(PREFIX + name).start();
+
+ try (Scope ignored = GlobalTracer.get().activateSpan(span))
+ {
+ return supplier.get();
+ }
+ finally
+ {
+ span.finish();
+ }
+ }
+
+ public static T trace(String name, Function supplier)
+ {
+ Span span = GlobalTracer.get().buildSpan(PREFIX + name).start();
+
+ try (Scope ignored = GlobalTracer.get().activateSpan(span))
+ {
+ return supplier.apply(span);
+ }
+ finally
+ {
+ span.finish();
+ }
+ }
+}
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/test/java/org/finos/legend/engine/query/sql/providers/shared/AbstractTestLegendStoreSQLSourceProvider.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/test/java/org/finos/legend/engine/query/sql/providers/shared/AbstractTestLegendStoreSQLSourceProvider.java
new file mode 100644
index 00000000000..80d8c9e2104
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/test/java/org/finos/legend/engine/query/sql/providers/shared/AbstractTestLegendStoreSQLSourceProvider.java
@@ -0,0 +1,109 @@
+// Copyright 2023 Goldman Sachs
+//
+// Licensed 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 org.finos.legend.engine.query.sql.providers.shared;
+
+import org.eclipse.collections.impl.list.mutable.FastList;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceProvider;
+import org.finos.legend.engine.query.sql.providers.core.TableSource;
+import org.finos.legend.engine.query.sql.providers.core.TableSourceArgument;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateLoader;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectResolvedContext;
+import org.junit.Assert;
+import org.junit.Test;
+
+import static org.finos.legend.engine.query.sql.providers.shared.SQLSourceProviderTestUtils.loadPureModelContextFromResource;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public abstract class AbstractTestLegendStoreSQLSourceProvider
+{
+ @Test
+ public void testNoProjectOrCoordindates()
+ {
+ TableSource tableSource = new TableSource("store", FastList.newListWith(
+ new TableSourceArgument("store", null, "notfound"))
+ );
+
+ testError(tableSource, "coordinates or project/workspace must be supplied");
+ }
+
+ @Test
+ public void testMissingWorkspace()
+ {
+ TableSource tableSource = new TableSource("store", FastList.newListWith(
+ new TableSourceArgument("store", null, "notfound"),
+ new TableSourceArgument("project", null, "proj1"))
+ );
+
+ testError(tableSource, "workspace/group workspace must be supplied if loading from project");
+ }
+
+ @Test
+ public void testMixedCoordinatesWorkspace()
+ {
+ TableSource tableSource = new TableSource("store", FastList.newListWith(
+ new TableSourceArgument("store", null, "notfound"),
+ new TableSourceArgument("project", null, "proj1"),
+ new TableSourceArgument("coordinates", null, "group:artifact:version"))
+ );
+
+ testError(tableSource, "cannot mix coordinates with project/workspace");
+ }
+
+ @Test
+ public void testMissingStoreParams()
+ {
+ String connectionName = "simple::store::DB::H2Connection";
+
+ PureModelContextData pmcd = loadPureModelContextFromResource("pmcd.pure", this.getClass());
+ when(getProjectCoordinateLoader().resolve(any(), any())).thenReturn(new ProjectResolvedContext(pmcd, pmcd));
+
+ TableSource table = new TableSource("relationalStore", FastList.newListWith(
+ new TableSourceArgument("coordinates", null, "group:artifact:version"),
+ new TableSourceArgument("connection", null, connectionName)));
+
+ IllegalArgumentException exception = Assert.assertThrows("Should throw given no store found", IllegalArgumentException.class, () -> getProvider().resolve(FastList.newListWith(table), null, FastList.newList()));
+ Assert.assertEquals("'store' parameter is required", exception.getMessage());
+ }
+
+ @Test
+ public void testStoreNotFound()
+ {
+ when(getProjectCoordinateLoader().resolve(any(), any())).thenReturn(new ProjectResolvedContext(mock(PureModelContextData.class), mock(PureModelContextData.class)));
+ String connectionName = "simple::store::DB::H2Connection";
+
+ TableSource table = new TableSource("store", FastList.newListWith(
+ new TableSourceArgument("store", null, "simple::store::DBForSQL"),
+ new TableSourceArgument("connection", null, connectionName),
+ new TableSourceArgument("coordinates", null, "group:artifact:version")));
+
+ IllegalArgumentException exception = Assert.assertThrows("Should throw given no store found", IllegalArgumentException.class, () -> getProvider().resolve(FastList.newListWith(table), null, FastList.newList()));
+ Assert.assertEquals("No element found for 'store'", exception.getMessage());
+ }
+
+ protected void testError(TableSource tableSource, String error)
+ {
+ IllegalArgumentException exception = Assert.assertThrows("Should throw error", IllegalArgumentException.class, () -> getProvider().resolve(FastList.newListWith(tableSource), null, FastList.newList()));
+ Assert.assertEquals(error, exception.getMessage());
+ }
+
+ protected abstract SQLSourceProvider getProvider();
+
+ protected abstract ProjectCoordinateLoader getProjectCoordinateLoader();
+
+}
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/test/java/org/finos/legend/engine/query/sql/providers/shared/SQLSourceProviderTestUtils.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/test/java/org/finos/legend/engine/query/sql/providers/shared/SQLSourceProviderTestUtils.java
new file mode 100644
index 00000000000..ad318655867
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/test/java/org/finos/legend/engine/query/sql/providers/shared/SQLSourceProviderTestUtils.java
@@ -0,0 +1,79 @@
+// Copyright 2023 Goldman Sachs
+//
+// Licensed 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 org.finos.legend.engine.query.sql.providers.shared;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.io.IOUtils;
+import org.finos.legend.engine.language.pure.grammar.from.PureGrammarParser;
+import org.finos.legend.engine.protocol.pure.v1.PureProtocolObjectMapperFactory;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData;
+import org.junit.Assert;
+
+import java.io.IOException;
+import java.util.Objects;
+
+public class SQLSourceProviderTestUtils
+{
+
+ //TODO replace once equals method added in legend
+ public static void assertLogicalEquality(Object expected, Object actual)
+ {
+ try
+ {
+ ObjectMapper mapper = PureProtocolObjectMapperFactory.getNewObjectMapper();
+ Assert.assertEquals(
+ mapper.writeValueAsString(expected),
+ mapper.writeValueAsString(actual));
+ }
+ catch (JsonProcessingException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static PureModelContextData loadPureModelContextFromResource(String resource, Class> clazz)
+ {
+ String model = getResource(resource, clazz);
+ return PureModelContextData.newBuilder().withPureModelContextData(PureGrammarParser.newInstance().parseModel(model)).build();
+ }
+
+ public static String getResource(String resource, Class> clazz)
+ {
+ try
+ {
+ return IOUtils.toString(Objects.requireNonNull(clazz.getClassLoader().getResourceAsStream(resource)));
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static T loadFromResources(String resource, TypeReference typeReference, Class> clazz)
+ {
+ String sources = getResource(resource, clazz);
+ try
+ {
+ return PureProtocolObjectMapperFactory.getNewObjectMapper().readValue(sources, typeReference);
+ }
+ catch (JsonProcessingException e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/test/java/org/finos/legend/engine/query/sql/providers/shared/TestFunctionSQLSourceProvider.java b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/test/java/org/finos/legend/engine/query/sql/providers/shared/TestFunctionSQLSourceProvider.java
new file mode 100644
index 00000000000..7b3f4e168dd
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/test/java/org/finos/legend/engine/query/sql/providers/shared/TestFunctionSQLSourceProvider.java
@@ -0,0 +1,181 @@
+// Copyright 2023 Goldman Sachs
+//
+// Licensed 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 org.finos.legend.engine.query.sql.providers.shared;
+
+import org.eclipse.collections.impl.list.mutable.FastList;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContext;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextData;
+import org.finos.legend.engine.protocol.pure.v1.model.context.PureModelContextPointer;
+import org.finos.legend.engine.protocol.pure.v1.model.packageableElement.domain.Function;
+import org.finos.legend.engine.protocol.pure.v1.model.valueSpecification.raw.Lambda;
+import org.finos.legend.engine.query.sql.providers.core.SQLSource;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceArgument;
+import org.finos.legend.engine.query.sql.providers.core.SQLSourceResolvedContext;
+import org.finos.legend.engine.query.sql.providers.core.TableSource;
+import org.finos.legend.engine.query.sql.providers.core.TableSourceArgument;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateLoader;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectCoordinateWrapper;
+import org.finos.legend.engine.query.sql.providers.shared.project.ProjectResolvedContext;
+import org.finos.legend.engine.query.sql.providers.shared.utils.SQLProviderUtils;
+import org.finos.legend.engine.shared.core.operational.errorManagement.EngineException;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import static org.finos.legend.engine.query.sql.providers.shared.SQLSourceProviderTestUtils.loadPureModelContextFromResource;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TestFunctionSQLSourceProvider
+{
+ @Mock
+ private ProjectCoordinateLoader projectCoordinateLoader;
+
+ private FunctionSQLSourceProvider provider;
+
+ @Before
+ public void setup()
+ {
+ this.provider = new FunctionSQLSourceProvider(projectCoordinateLoader);
+ }
+
+ @Test
+ public void testType()
+ {
+ Assert.assertEquals("func", provider.getType());
+ }
+
+ @Test
+ public void testWorkspace()
+ {
+ String functionName = "simple::func::simpleFunction_String_MANY__TabularDataSet_1_";
+
+ ProjectCoordinateWrapper coordinates = ProjectCoordinateWrapper.workspace("proj1", "ws1");
+
+ PureModelContextData pmcd = loadPureModelContextFromResource("function-pmcd.pure", this.getClass());
+ Function function = SQLProviderUtils.extractElement("function", pmcd.getElementsOfType(Function.class), f -> f.getPath().equals(functionName));
+
+ when(projectCoordinateLoader.resolve(eq(coordinates), any())).thenReturn(new ProjectResolvedContext(pmcd, pmcd));
+
+ TableSource tableSource = createTableSource(functionName,
+ new TableSourceArgument("project", null, "proj1"),
+ new TableSourceArgument("workspace", null, "ws1")
+ );
+
+ Lambda lambda = new Lambda();
+ lambda.body = function.body;
+ lambda.parameters = function.parameters;
+
+ SQLSource expected = new SQLSource("func", lambda, null, null, FastList.newList(), null, FastList.newListWith(
+ new SQLSourceArgument("path", 0, functionName),
+ new SQLSourceArgument("project", null, "proj1"),
+ new SQLSourceArgument("workspace", null, "ws1")
+ ));
+
+ testSuccess(tableSource, pmcd, expected);
+ }
+
+ @Test
+ public void testCoordinates()
+ {
+ String functionName = "simple::func::simpleFunction_String_MANY__TabularDataSet_1_";
+
+ ProjectCoordinateWrapper coordinates = ProjectCoordinateWrapper.coordinates("proj1:art:1.0.0");
+
+ PureModelContextData pmcd = loadPureModelContextFromResource("function-pmcd.pure", this.getClass());
+ Function function = SQLProviderUtils.extractElement("function", pmcd.getElementsOfType(Function.class), f -> f.getPath().equals(functionName));
+ PureModelContextPointer pointer = new PureModelContextPointer();
+
+ when(projectCoordinateLoader.resolve(eq(coordinates), any())).thenReturn(new ProjectResolvedContext(pointer, pmcd));
+
+ TableSource tableSource = createTableSource(functionName,
+ new TableSourceArgument("coordinates", null, "proj1:art:1.0.0")
+ );
+
+ Lambda lambda = new Lambda();
+ lambda.body = function.body;
+ lambda.parameters = function.parameters;
+
+ SQLSource expected = new SQLSource("func", lambda, null, null, FastList.newList(), null, FastList.newListWith(
+ new SQLSourceArgument("path", 0, functionName),
+ new SQLSourceArgument("coordinates", null, "proj1:art:1.0.0")
+ ));
+
+ testSuccess(tableSource, pointer, expected);
+ }
+
+ @Test
+ public void testNoProjectOrCoordinates()
+ {
+ TableSource tableSource = createTableSource("simple::func__TabularDataSet_1_");
+ testException(tableSource, IllegalArgumentException.class, "coordinates or project/workspace must be supplied");
+ }
+
+ @Test
+ public void testNoWorkspaceWithProject()
+ {
+ TableSource tableSource = createTableSource("simple::func__TabularDataSet_1_", new TableSourceArgument("project", null, "proj1"));
+ testException(tableSource, IllegalArgumentException.class, "workspace/group workspace must be supplied if loading from project");
+ }
+
+ @Test
+ public void testNotTDSFunc()
+ {
+ String functionName = "simple::func::nonTdsFunction__String_1_";
+
+ ProjectCoordinateWrapper coordinates = ProjectCoordinateWrapper.coordinates("proj1:art:1.0.0");
+
+ PureModelContextData pmcd = loadPureModelContextFromResource("function-pmcd.pure", this.getClass());
+
+ when(projectCoordinateLoader.resolve(eq(coordinates), any())).thenReturn(new ProjectResolvedContext(pmcd, pmcd));
+
+ TableSource tableSource = createTableSource(functionName,
+ new TableSourceArgument("coordinates", null, "proj1:art:1.0.0")
+ );
+
+ testException(tableSource, EngineException.class, "Function " + functionName + " does not return Tabular data type");
+ }
+
+ private void testException(TableSource tableSource, Class throwable, String expected)
+ {
+ T exception = Assert.assertThrows("Should throw given no service found", throwable, () -> provider.resolve(FastList.newListWith(tableSource), null, FastList.newList()));
+ Assert.assertEquals(expected, exception.getMessage());
+ }
+
+ private void testSuccess(TableSource tableSource, PureModelContext expectedContext, SQLSource expected)
+ {
+ SQLSourceResolvedContext result = provider.resolve(FastList.newListWith(tableSource), null, FastList.newList());
+
+ //ASSERT
+ Assert.assertEquals(FastList.newListWith(expectedContext), result.getPureModelContexts());
+ Assert.assertEquals(1, result.getSources().size());
+
+ SQLSourceProviderTestUtils.assertLogicalEquality(expected, result.getSources().get(0));
+ }
+
+ private final TableSource createTableSource(String func, TableSourceArgument... extraArguments)
+ {
+ return new TableSource("func", FastList.newListWith(
+ new TableSourceArgument(null, 0, func)).with(extraArguments)
+ );
+ }
+}
+
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/test/resources/function-pmcd.pure b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/test/resources/function-pmcd.pure
new file mode 100644
index 00000000000..fbeb18c1a2d
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/legend-engine-xt-sql-providers-shared/src/test/resources/function-pmcd.pure
@@ -0,0 +1,22 @@
+###Pure
+import simple::model::*;
+
+Class simple::model::Person
+{
+ firstName: String[1];
+ lastName: String[1];
+}
+function simple::func::simpleFunction(lastNames:String[*]):meta::pure::tds::TabularDataSet[1]
+{
+ simple::model::Person.all()
+ ->filter(p | $p.lastName->in($lastNames))
+ ->project([
+ col(x | $x.firstName, 'first name'),
+ col(x | $x.lastName, 'last name')
+ ])
+}
+
+function simple::func::nonTdsFunction():String[1]
+{
+ ''
+}
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-providers/pom.xml b/legend-engine-xts-sql/legend-engine-xt-sql-providers/pom.xml
new file mode 100644
index 00000000000..a5498551eed
--- /dev/null
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-providers/pom.xml
@@ -0,0 +1,35 @@
+
+
+
+
+ org.finos.legend.engine
+ legend-engine-xts-sql
+ 4.32.1-SNAPSHOT
+
+ 4.0.0
+
+ legend-engine-xt-sql-providers
+ pom
+ Legend Engine - XTS - SQL - Providers
+
+
+ legend-engine-xt-sql-providers-core
+ legend-engine-xt-sql-providers-shared
+ legend-engine-xt-sql-providers-relationalStore
+ legend-engine-xt-sql-providers-service
+
+
\ No newline at end of file
diff --git a/legend-engine-xts-sql/legend-engine-xt-sql-query/pom.xml b/legend-engine-xts-sql/legend-engine-xt-sql-query/pom.xml
index c1e0cd03af2..f4b79ad6c04 100644
--- a/legend-engine-xts-sql/legend-engine-xt-sql-query/pom.xml
+++ b/legend-engine-xts-sql/legend-engine-xt-sql-query/pom.xml
@@ -96,6 +96,10 @@
org.finos.legend.engine
legend-engine-xt-sql-compiler