diff --git a/presto-analyzer/src/main/java/com/facebook/presto/sql/analyzer/Analysis.java b/presto-analyzer/src/main/java/com/facebook/presto/sql/analyzer/Analysis.java index ade11574c397..78fe32280e53 100644 --- a/presto-analyzer/src/main/java/com/facebook/presto/sql/analyzer/Analysis.java +++ b/presto-analyzer/src/main/java/com/facebook/presto/sql/analyzer/Analysis.java @@ -52,16 +52,19 @@ import com.facebook.presto.sql.tree.SubqueryExpression; import com.facebook.presto.sql.tree.Table; import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.HashMultiset; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimap; +import com.google.common.collect.Multiset; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Collection; import java.util.Deque; import java.util.HashMap; @@ -70,6 +73,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -190,6 +194,9 @@ public class Analysis // Keeps track of the subquery we are visiting, so we have access to base query information when processing materialized view status private Optional currentQuerySpecification = Optional.empty(); + private final Multiset rowFilterScopes = HashMultiset.create(); + private final Map, List> rowFilters = new LinkedHashMap<>(); + public Analysis(@Nullable Statement root, Map, Expression> parameters, boolean isDescribe) { this.root = root; @@ -994,6 +1001,32 @@ public Map> getInvokedFunctions() return functionMap.entrySet().stream().collect(toImmutableMap(Map.Entry::getKey, entry -> ImmutableSet.copyOf(entry.getValue()))); } + public boolean hasRowFilter(QualifiedObjectName table, String identity) + { + return rowFilterScopes.contains(new RowFilterScopeEntry(table, identity)); + } + + public void registerTableForRowFiltering(QualifiedObjectName table, String identity) + { + rowFilterScopes.add(new RowFilterScopeEntry(table, identity)); + } + + public void unregisterTableForRowFiltering(QualifiedObjectName table, String identity) + { + rowFilterScopes.remove(new RowFilterScopeEntry(table, identity)); + } + + public void addRowFilter(Table table, Expression filter) + { + rowFilters.computeIfAbsent(NodeRef.of(table), node -> new ArrayList<>()) + .add(filter); + } + + public List getRowFilters(Table node) + { + return rowFilters.getOrDefault(NodeRef.of(node), ImmutableList.of()); + } + @Immutable public static final class Insert { @@ -1177,4 +1210,36 @@ public boolean isFromView() return isFromView; } } + + private static class RowFilterScopeEntry + { + private final QualifiedObjectName table; + private final String identity; + + public RowFilterScopeEntry(QualifiedObjectName table, String identity) + { + this.table = requireNonNull(table, "table is null"); + this.identity = requireNonNull(identity, "identity is null"); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RowFilterScopeEntry that = (RowFilterScopeEntry) o; + return table.equals(that.table) && + identity.equals(that.identity); + } + + @Override + public int hashCode() + { + return Objects.hash(table, identity); + } + } } diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/security/LegacyAccessControl.java b/presto-hive/src/main/java/com/facebook/presto/hive/security/LegacyAccessControl.java index e8495d434c59..68cdc80c3e01 100644 --- a/presto-hive/src/main/java/com/facebook/presto/hive/security/LegacyAccessControl.java +++ b/presto-hive/src/main/java/com/facebook/presto/hive/security/LegacyAccessControl.java @@ -26,6 +26,7 @@ import com.facebook.presto.spi.security.ConnectorIdentity; import com.facebook.presto.spi.security.PrestoPrincipal; import com.facebook.presto.spi.security.Privilege; +import com.facebook.presto.spi.security.ViewExpression; import javax.inject.Inject; @@ -261,6 +262,11 @@ public void checkCanShowRoleGrants(ConnectorTransactionHandle transactionHandle, } @Override + public Optional getRowFilter(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) + { + return Optional.empty(); + } + public void checkCanDropConstraint(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) { if (!allowDropConstraint) { diff --git a/presto-hive/src/main/java/com/facebook/presto/hive/security/SqlStandardAccessControl.java b/presto-hive/src/main/java/com/facebook/presto/hive/security/SqlStandardAccessControl.java index 9ef0aa54ca7c..6f7533e0b41c 100644 --- a/presto-hive/src/main/java/com/facebook/presto/hive/security/SqlStandardAccessControl.java +++ b/presto-hive/src/main/java/com/facebook/presto/hive/security/SqlStandardAccessControl.java @@ -30,6 +30,7 @@ import com.facebook.presto.spi.security.PrestoPrincipal; import com.facebook.presto.spi.security.Privilege; import com.facebook.presto.spi.security.RoleGrant; +import com.facebook.presto.spi.security.ViewExpression; import javax.inject.Inject; @@ -411,6 +412,12 @@ public void checkCanShowRoleGrants(ConnectorTransactionHandle transactionHandle, { } + @Override + public Optional getRowFilter(ConnectorTransactionHandle transaction, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) + { + return Optional.empty(); + } + private boolean isAdmin(ConnectorTransactionHandle transaction, ConnectorIdentity identity, MetastoreContext metastoreContext) { SemiTransactionalHiveMetastore metastore = getMetastore(transaction); diff --git a/presto-main/src/main/java/com/facebook/presto/security/AccessControlManager.java b/presto-main/src/main/java/com/facebook/presto/security/AccessControlManager.java index 011db98e315a..6b00dc78b2cd 100644 --- a/presto-main/src/main/java/com/facebook/presto/security/AccessControlManager.java +++ b/presto-main/src/main/java/com/facebook/presto/security/AccessControlManager.java @@ -33,8 +33,10 @@ import com.facebook.presto.spi.security.Privilege; import com.facebook.presto.spi.security.SystemAccessControl; import com.facebook.presto.spi.security.SystemAccessControlFactory; +import com.facebook.presto.spi.security.ViewExpression; import com.facebook.presto.transaction.TransactionManager; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.weakref.jmx.Managed; @@ -737,6 +739,26 @@ public void checkCanShowRoleGrants(TransactionId transactionId, Identity identit } @Override + public List getRowFilters(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName) + { + requireNonNull(transactionId, "transactionId is null"); + requireNonNull(identity, "identity is null"); + requireNonNull(tableName, "catalogName is null"); + + ImmutableList.Builder filters = ImmutableList.builder(); + CatalogAccessControlEntry entry = getConnectorAccessControl(transactionId, tableName.getCatalogName()); + + if (entry != null) { + entry.getAccessControl().getRowFilter(entry.getTransactionHandle(transactionId), identity.toConnectorIdentity(tableName.getCatalogName()), context, toSchemaTableName(tableName)) + .ifPresent(filters::add); + } + + systemAccessControl.get().getRowFilter(identity, context, toCatalogSchemaTableName(tableName)) + .ifPresent(filters::add); + + return filters.build(); + } + public void checkCanDropConstraint(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName) { requireNonNull(identity, "identity is null"); diff --git a/presto-main/src/main/java/com/facebook/presto/security/AllowAllSystemAccessControl.java b/presto-main/src/main/java/com/facebook/presto/security/AllowAllSystemAccessControl.java index 01f260f64b6d..9a4f00081564 100644 --- a/presto-main/src/main/java/com/facebook/presto/security/AllowAllSystemAccessControl.java +++ b/presto-main/src/main/java/com/facebook/presto/security/AllowAllSystemAccessControl.java @@ -23,6 +23,7 @@ import com.facebook.presto.spi.security.Privilege; import com.facebook.presto.spi.security.SystemAccessControl; import com.facebook.presto.spi.security.SystemAccessControlFactory; +import com.facebook.presto.spi.security.ViewExpression; import java.security.Principal; import java.security.cert.X509Certificate; @@ -214,6 +215,11 @@ public void checkCanRevokeTablePrivilege(Identity identity, AccessControlContext } @Override + public Optional getRowFilter(Identity identity, AccessControlContext context, CatalogSchemaTableName tableName) + { + return Optional.empty(); + } + public void checkCanDropConstraint(Identity identity, AccessControlContext context, CatalogSchemaTableName table) { } diff --git a/presto-main/src/main/java/com/facebook/presto/security/FileBasedSystemAccessControl.java b/presto-main/src/main/java/com/facebook/presto/security/FileBasedSystemAccessControl.java index 8a4aab91a559..37547d16dd44 100644 --- a/presto-main/src/main/java/com/facebook/presto/security/FileBasedSystemAccessControl.java +++ b/presto-main/src/main/java/com/facebook/presto/security/FileBasedSystemAccessControl.java @@ -28,6 +28,7 @@ import com.facebook.presto.spi.security.Privilege; import com.facebook.presto.spi.security.SystemAccessControl; import com.facebook.presto.spi.security.SystemAccessControlFactory; +import com.facebook.presto.spi.security.ViewExpression; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import io.airlift.units.Duration; @@ -418,6 +419,12 @@ public void checkCanRevokeTablePrivilege(Identity identity, AccessControlContext } @Override + + public Optional getRowFilter(Identity identity, AccessControlContext context, CatalogSchemaTableName tableName) + { + return Optional.empty(); + } + public void checkCanDropConstraint(Identity identity, AccessControlContext context, CatalogSchemaTableName table) { if (!canAccessCatalog(identity, table.getCatalogName(), ALL)) { diff --git a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java index 226b4e2874a5..2d46d4a26aae 100644 --- a/presto-main/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java +++ b/presto-main/src/main/java/com/facebook/presto/sql/analyzer/StatementAnalyzer.java @@ -54,6 +54,7 @@ import com.facebook.presto.spi.security.AllowAllAccessControl; import com.facebook.presto.spi.security.Identity; import com.facebook.presto.spi.security.ViewAccessControl; +import com.facebook.presto.spi.security.ViewExpression; import com.facebook.presto.sql.ExpressionUtils; import com.facebook.presto.sql.MaterializedViewUtils; import com.facebook.presto.sql.SqlFormatterUtil; @@ -201,7 +202,9 @@ import static com.facebook.presto.metadata.MetadataUtil.toSchemaTableName; import static com.facebook.presto.spi.StandardErrorCode.INVALID_ARGUMENTS; import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_ROW_FILTER; import static com.facebook.presto.spi.StandardErrorCode.NOT_FOUND; +import static com.facebook.presto.spi.StandardErrorCode.ROW_FILTER_TYPE_MISMATCH; import static com.facebook.presto.spi.StandardWarningCode.PERFORMANCE_WARNING; import static com.facebook.presto.spi.StandardWarningCode.REDUNDANT_ORDER_BY; import static com.facebook.presto.spi.analyzer.AccessControlRole.TABLE_CREATE; @@ -441,6 +444,10 @@ protected Scope visitInsert(Insert insert, Optional scope) tableColumnsMetadata.getTableHandle().get(), insertColumns.stream().map(columnHandles::get).collect(toImmutableList()))); + if (!accessControl.getRowFilters(session.getRequiredTransactionId(), session.getIdentity(), session.getAccessControlContext(), targetTable).isEmpty()) { + throw new SemanticException(NOT_SUPPORTED, insert, "Insert into table with row filter is not supported"); + } + return createAndAssignScope(insert, scope, Field.newUnqualified(insert.getLocation(), "rows", BIGINT)); } @@ -591,6 +598,10 @@ protected Scope visitDelete(Delete node, Optional scope) analysis.addAccessControlCheckForTable(TABLE_DELETE, new AccessControlInfoForTable(accessControl, session.getIdentity(), session.getTransactionId(), session.getAccessControlContext(), tableName)); + if (!accessControl.getRowFilters(session.getRequiredTransactionId(), session.getIdentity(), session.getAccessControlContext(), tableName).isEmpty()) { + throw new SemanticException(NOT_SUPPORTED, node, "Delete from table with row filter is not supported"); + } + return createAndAssignScope(node, scope, Field.newUnqualified(node.getLocation(), "rows", BIGINT)); } @@ -1328,6 +1339,16 @@ protected Scope visitTable(Table table, Optional scope) analysis.setColumn(field, columnHandle); } + //add row filter + List outputFields = fields.build(); + + Scope accessControlScope = Scope.builder() + .withRelationType(RelationId.anonymous(), new RelationType(outputFields)) + .build(); + + accessControl.getRowFilters(session.getRequiredTransactionId(), session.getIdentity(), session.getAccessControlContext(), name) + .forEach(filter -> analyzeRowFilter(session.getIdentity().getUser(), table, name, accessControlScope, filter)); + analysis.registerTable(table, tableHandle.get()); if (statement instanceof RefreshMaterializedView) { @@ -1346,6 +1367,56 @@ protected Scope visitTable(Table table, Optional scope) return createAndAssignScope(table, scope, fields.build()); } + private void analyzeRowFilter(String currentIdentity, Table table, QualifiedObjectName name, Scope scope, ViewExpression filter) + { + if (analysis.hasRowFilter(name, currentIdentity)) { + throw new PrestoException(INVALID_ROW_FILTER, format("Row filter for '%s' is recursive", name)); + } + + Expression expression; + try { + expression = sqlParser.createExpression(filter.getExpression(), createParsingOptions(session)); + } + catch (ParsingException e) { + throw new PrestoException(INVALID_ROW_FILTER, format("Invalid row filter for '%s': %s", name, e.getErrorMessage()), e); + } + + analysis.registerTableForRowFiltering(name, currentIdentity); + ExpressionAnalysis expressionAnalysis; + try { + expressionAnalysis = ExpressionAnalyzer.analyzeExpression( + createViewSession(filter.getCatalog(), filter.getSchema(), new Identity(filter.getIdentity(), Optional.empty())), // TODO: path should be included in row filter + metadata, + accessControl, + sqlParser, + scope, + analysis, + expression, + warningCollector); + } + catch (PrestoException e) { + throw new PrestoException(e::getErrorCode, format("Invalid row filter for '%s': %s", name, e.getMessage()), e); + } + finally { + analysis.unregisterTableForRowFiltering(name, currentIdentity); + } + + verifyNoAggregateWindowOrGroupingFunctions(analysis.getFunctionHandles(), functionAndTypeResolver, expression, format("Row filter for '%s'", name)); + + analysis.recordSubqueries(expression, expressionAnalysis); + + Type actualType = expressionAnalysis.getType(expression); + if (!actualType.equals(BOOLEAN)) { + if (!metadata.getFunctionAndTypeManager().canCoerce(actualType, BOOLEAN)) { + throw new PrestoException(ROW_FILTER_TYPE_MISMATCH, format("Expected row filter for '%s' to be of type BOOLEAN, but was %s", name, actualType), null); + } + + analysis.addCoercion(expression, BOOLEAN, metadata.getFunctionAndTypeManager().isTypeOnlyCoercion(actualType, BOOLEAN)); + } + + analysis.addRowFilter(table, expression); + } + private Optional getTableHandle(TableColumnMetadata tableColumnsMetadata, Table table, QualifiedObjectName name, Optional scope) { // Process table version AS OF expression @@ -2826,21 +2897,7 @@ private RelationType analyzeView(Query query, QualifiedObjectName name, Optional viewAccessControl = accessControl; } - Session.SessionBuilder viewSessionBuilder = Session.builder(metadata.getSessionPropertyManager()) - .setQueryId(session.getQueryId()) - .setTransactionId(session.getTransactionId().orElse(null)) - .setIdentity(identity) - .setSource(session.getSource().orElse(null)) - .setCatalog(catalog.orElse(null)) - .setSchema(schema.orElse(null)) - .setTimeZoneKey(session.getTimeZoneKey()) - .setLocale(session.getLocale()) - .setRemoteUserAddress(session.getRemoteUserAddress().orElse(null)) - .setUserAgent(session.getUserAgent().orElse(null)) - .setClientInfo(session.getClientInfo().orElse(null)) - .setStartTime(session.getStartTime()); - session.getConnectorProperties().forEach((connectorId, properties) -> properties.forEach((k, v) -> viewSessionBuilder.setConnectionProperty(connectorId, k, v))); - Session viewSession = viewSessionBuilder.build(); + Session viewSession = createViewSession(catalog, schema, identity); StatementAnalyzer analyzer = new StatementAnalyzer(analysis, metadata, sqlParser, viewAccessControl, viewSession, warningCollector); Scope queryScope = analyzer.analyze(query, Scope.create()); return queryScope.getRelationType().withAlias(name.getObjectName(), null); @@ -2851,6 +2908,25 @@ private RelationType analyzeView(Query query, QualifiedObjectName name, Optional } } + private Session createViewSession(Optional catalog, Optional schema, Identity identity) + { + Session.SessionBuilder viewSessionBuilder = Session.builder(metadata.getSessionPropertyManager()) + .setQueryId(session.getQueryId()) + .setTransactionId(session.getTransactionId().orElse(null)) + .setIdentity(identity) + .setSource(session.getSource().orElse(null)) + .setCatalog(catalog.orElse(null)) + .setSchema(schema.orElse(null)) + .setTimeZoneKey(session.getTimeZoneKey()) + .setLocale(session.getLocale()) + .setRemoteUserAddress(session.getRemoteUserAddress().orElse(null)) + .setUserAgent(session.getUserAgent().orElse(null)) + .setClientInfo(session.getClientInfo().orElse(null)) + .setStartTime(session.getStartTime()); + session.getConnectorProperties().forEach((connectorId, properties) -> properties.forEach((k, v) -> viewSessionBuilder.setConnectionProperty(connectorId, k, v))); + return viewSessionBuilder.build(); + } + private Query parseView(String view, QualifiedObjectName name, Node node) { try { diff --git a/presto-main/src/main/java/com/facebook/presto/sql/planner/RelationPlanner.java b/presto-main/src/main/java/com/facebook/presto/sql/planner/RelationPlanner.java index 8e925730a1d1..c6735c2564dd 100644 --- a/presto-main/src/main/java/com/facebook/presto/sql/planner/RelationPlanner.java +++ b/presto-main/src/main/java/com/facebook/presto/sql/planner/RelationPlanner.java @@ -221,7 +221,26 @@ protected RelationPlan visitTable(Table node, SqlPlannerContext context) context.incrementLeafNodes(session); PlanNode root = new TableScanNode(getSourceLocation(node.getLocation()), idAllocator.getNextId(), handle, outputVariables, columns.build(), tableConstraints, TupleDomain.all(), TupleDomain.all()); - return new RelationPlan(root, scope, outputVariables); + RelationPlan tableScan = new RelationPlan(root, scope, outputVariables); + tableScan = addRowFilters(node, tableScan, context); + return tableScan; + } + + private RelationPlan addRowFilters(Table node, RelationPlan plan, SqlPlannerContext context) + { + PlanBuilder planBuilder = new PlanBuilder(new TranslationMap(plan, analysis, ImmutableMap.of()), plan.getRoot()); + + for (Expression filter : analysis.getRowFilters(node)) { + planBuilder = subqueryPlanner.handleSubqueries(planBuilder, filter, filter, context); + + planBuilder = planBuilder.withNewRoot(new FilterNode( + Optional.empty(), + idAllocator.getNextId(), + planBuilder.getRoot(), + rowExpression(planBuilder.rewrite(filter), context))); + } + + return new RelationPlan(planBuilder.getRoot(), plan.getScope(), plan.getFieldMappings()); } @Override diff --git a/presto-main/src/main/java/com/facebook/presto/testing/TestingAccessControlManager.java b/presto-main/src/main/java/com/facebook/presto/testing/TestingAccessControlManager.java index 86658c0d2992..e6b8a413af3a 100644 --- a/presto-main/src/main/java/com/facebook/presto/testing/TestingAccessControlManager.java +++ b/presto-main/src/main/java/com/facebook/presto/testing/TestingAccessControlManager.java @@ -21,14 +21,20 @@ import com.facebook.presto.security.AllowAllSystemAccessControl; import com.facebook.presto.spi.security.AccessControlContext; import com.facebook.presto.spi.security.Identity; +import com.facebook.presto.spi.security.ViewExpression; import com.facebook.presto.transaction.TransactionManager; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import javax.inject.Inject; import java.security.Principal; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -84,6 +90,7 @@ public class TestingAccessControlManager extends AccessControlManager { private final Set denyPrivileges = new HashSet<>(); + private final Map> rowFilters = new HashMap<>(); @Inject public TestingAccessControlManager(TransactionManager transactionManager) @@ -107,9 +114,16 @@ public void deny(TestingPrivilege... deniedPrivileges) Collections.addAll(this.denyPrivileges, deniedPrivileges); } + public void rowFilter(QualifiedObjectName table, String identity, ViewExpression filter) + { + rowFilters.computeIfAbsent(new RowFilterKey(identity, table), key -> new ArrayList<>()) + .add(filter); + } + public void reset() { denyPrivileges.clear(); + rowFilters.clear(); } @Override @@ -333,6 +347,11 @@ public void checkCanSelectFromColumns(TransactionId transactionId, Identity iden } @Override + public List getRowFilters(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName) + { + return rowFilters.getOrDefault(new RowFilterKey(identity.getUser(), tableName), ImmutableList.of()); + } + public void checkCanDropConstraint(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName) { if (shouldDenyPrivilege(identity.getUser(), tableName.getObjectName(), DROP_CONSTRAINT)) { @@ -422,4 +441,36 @@ public String toString() .toString(); } } + + private static class RowFilterKey + { + private final String identity; + private final QualifiedObjectName table; + + public RowFilterKey(String identity, QualifiedObjectName table) + { + this.identity = requireNonNull(identity, "identity is null"); + this.table = requireNonNull(table, "table is null"); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RowFilterKey that = (RowFilterKey) o; + return identity.equals(that.identity) && + table.equals(that.table); + } + + @Override + public int hashCode() + { + return Objects.hash(identity, table); + } + } } diff --git a/presto-main/src/test/java/com/facebook/presto/sql/query/QueryAssertions.java b/presto-main/src/test/java/com/facebook/presto/sql/query/QueryAssertions.java index 8f765ed6010b..acf3d1d70482 100644 --- a/presto-main/src/test/java/com/facebook/presto/sql/query/QueryAssertions.java +++ b/presto-main/src/test/java/com/facebook/presto/sql/query/QueryAssertions.java @@ -142,4 +142,15 @@ public void close() { runner.close(); } + + protected void executeExclusively(Runnable executionBlock) + { + runner.getExclusiveLock().lock(); + try { + executionBlock.run(); + } + finally { + runner.getExclusiveLock().unlock(); + } + } } diff --git a/presto-main/src/test/java/com/facebook/presto/sql/query/TestRowFilter.java b/presto-main/src/test/java/com/facebook/presto/sql/query/TestRowFilter.java new file mode 100644 index 000000000000..c3717c1ec421 --- /dev/null +++ b/presto-main/src/test/java/com/facebook/presto/sql/query/TestRowFilter.java @@ -0,0 +1,331 @@ +/* + * 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 com.facebook.presto.sql.query; +import com.facebook.presto.Session; +import com.facebook.presto.common.QualifiedObjectName; +import com.facebook.presto.spi.security.Identity; +import com.facebook.presto.spi.security.ViewExpression; +import com.facebook.presto.testing.LocalQueryRunner; +import com.facebook.presto.testing.TestingAccessControlManager; +import com.facebook.presto.tpch.TpchConnectorFactory; +import com.google.common.collect.ImmutableMap; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.Optional; + +import static com.facebook.presto.testing.TestingSession.testSessionBuilder; +import static com.facebook.presto.tpch.TpchMetadata.TINY_SCHEMA_NAME; + +public class TestRowFilter +{ + private static final String CATALOG = "local"; + private static final String USER = "user"; + private static final String RUN_AS_USER = "run-as-user"; + + private QueryAssertions assertions; + private TestingAccessControlManager accessControl; + + @BeforeClass + public void init() + { + Session session = testSessionBuilder() + .setCatalog(CATALOG) + .setSchema(TINY_SCHEMA_NAME) + .setIdentity(new Identity(USER, Optional.empty())).build(); + + LocalQueryRunner runner = new LocalQueryRunner(session); + + runner.createCatalog(CATALOG, new TpchConnectorFactory(1), ImmutableMap.of()); + + assertions = new QueryAssertions(runner); + accessControl = assertions.getQueryRunner().getAccessControl(); + } + + @AfterClass(alwaysRun = true) + public void teardown() + { + assertions.close(); + assertions = null; + } + + @Test + public void testSimpleFilter() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.empty(), Optional.empty(), "orderkey < 10")); + assertions.assertQuery("SELECT count(*) FROM orders", "VALUES BIGINT '7'"); + }); + + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.empty(), Optional.empty(), "NULL")); + assertions.assertQuery("SELECT count(*) FROM orders", "VALUES BIGINT '0'"); + }); + } + + @Test + public void testMultipleFilters() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.empty(), Optional.empty(), "orderkey < 10")); + + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.empty(), Optional.empty(), "orderkey > 5")); + + assertions.assertQuery("SELECT count(*) FROM orders", "VALUES BIGINT '2'"); + }); + } + + @Test + public void testCorrelatedSubquery() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.of(CATALOG), Optional.of("tiny"), "EXISTS (SELECT 1 FROM nation WHERE nationkey = orderkey)")); + assertions.assertQuery("SELECT count(*) FROM orders", "VALUES BIGINT '7'"); + }); + } + + @Test + public void testTableReferenceInWithClause() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.empty(), Optional.empty(), "orderkey = 1")); + assertions.assertQuery("WITH t AS (SELECT count(*) FROM orders) SELECT * FROM t", "VALUES BIGINT '1'"); + }); + } + + @Test + public void testOtherSchema() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.of(CATALOG), Optional.of("sf1"), "(SELECT count(*) FROM customer) = 150000")); // Filter is TRUE only if evaluating against sf1.customer + assertions.assertQuery("SELECT count(*) FROM orders", "VALUES BIGINT '15000'"); + }); + } + + @Test + public void testDifferentIdentity() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + RUN_AS_USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey = 1")); + + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey IN (SELECT orderkey FROM orders)")); + + assertions.assertQuery("SELECT count(*) FROM orders", "VALUES BIGINT '1'"); + }); + } + // + @Test + public void testRecursion() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey IN (SELECT orderkey FROM orders)")); + + assertions.assertFails("SELECT count(*) FROM orders", ".*\\QRow filter for 'local.tiny.orders' is recursive\\E.*"); + }); + + // different reference style to same table + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey IN (SELECT local.tiny.orderkey FROM orders)")); + assertions.assertFails("SELECT count(*) FROM orders", ".*\\QRow filter for 'local.tiny.orders' is recursive\\E.*"); + }); + + // mutual recursion + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + RUN_AS_USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey IN (SELECT orderkey FROM orders)")); + + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey IN (SELECT orderkey FROM orders)")); + + assertions.assertFails("SELECT count(*) FROM orders", ".*\\QRow filter for 'local.tiny.orders' is recursive\\E.*"); + }); + } + + @Test + public void testLimitedScope() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "customer"), + USER, + new ViewExpression(USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey = 1")); + assertions.assertFails( + "SELECT (SELECT min(name) FROM customer WHERE customer.custkey = orders.custkey) FROM orders", + "\\Qline 1:1: Column 'orderkey' cannot be resolved\\E"); + }); + } + + @Test + public void testSqlInjection() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "nation"), + USER, + new ViewExpression(USER, Optional.of(CATALOG), Optional.of("tiny"), "regionkey IN (SELECT regionkey FROM region WHERE name = 'ASIA')")); + assertions.assertQuery( + "WITH region(regionkey, name) AS (VALUES (0, 'ASIA'), (1, 'ASIA'), (2, 'ASIA'), (3, 'ASIA'), (4, 'ASIA'))" + + "SELECT name FROM nation ORDER BY name LIMIT 1", + "VALUES CAST('CHINA' AS VARCHAR(25))"); // if sql-injection would work then query would return ALGERIA + }); + } + + @Test + public void testInvalidFilter() + { + // parse error + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "$$$")); + + assertions.assertFails("SELECT count(*) FROM orders", "\\QInvalid row filter for 'local.tiny.orders': mismatched input '$'. Expecting: \\E"); + }); + + // unknown column + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "unknown_column")); + + assertions.assertFails("SELECT count(*) FROM orders", "line 1:1: Column 'unknown_column' cannot be resolved"); + }); + + // invalid type + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "1")); + + assertions.assertFails("SELECT count(*) FROM orders", "\\QExpected row filter for 'local.tiny.orders' to be of type BOOLEAN, but was integer\\E"); + }); + + // aggregation + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "count(*) > 0")); + + assertions.assertFails("SELECT count(*) FROM orders", "\\Qline 1:10: Row filter for 'local.tiny.orders' cannot contain aggregations, window functions or grouping operations: [\"count\"(*)]\\E"); + }); + + // window function + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "row_number() OVER () > 0")); + + assertions.assertFails("SELECT count(*) FROM orders", "\\Qline 1:22: Row filter for 'local.tiny.orders' cannot contain aggregations, window functions or grouping operations: [\"row_number\"() OVER ()]\\E"); + }); + + // window function + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(RUN_AS_USER, Optional.of(CATALOG), Optional.of("tiny"), "grouping(orderkey) = 0")); + + assertions.assertFails("SELECT count(*) FROM orders", "\\Qline 1:20: Row filter for 'local.tiny.orders' cannot contain aggregations, window functions or grouping operations: [GROUPING (orderkey)]\\E"); + }); + } + + @Test + public void testInsertWithRowFiltering() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey < 10")); + + assertions.assertFails("INSERT INTO orders SELECT * FROM orders", "Insert into table with row filter is not supported"); + }); + } + + @Test + public void testDeleteWithRowFiltering() + { + assertions.executeExclusively(() -> { + accessControl.reset(); + accessControl.rowFilter( + new QualifiedObjectName(CATALOG, "tiny", "orders"), + USER, + new ViewExpression(USER, Optional.of(CATALOG), Optional.of("tiny"), "orderkey < 10")); + + assertions.assertFails("DELETE FROM orders", "\\Qline 1:1: Delete from table with row filter is not supported\\E"); + }); + } +} diff --git a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/AllowAllAccessControl.java b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/AllowAllAccessControl.java index 5384a53a3f42..b13d24ef0e08 100644 --- a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/AllowAllAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/AllowAllAccessControl.java @@ -21,6 +21,7 @@ import com.facebook.presto.spi.security.ConnectorIdentity; import com.facebook.presto.spi.security.PrestoPrincipal; import com.facebook.presto.spi.security.Privilege; +import com.facebook.presto.spi.security.ViewExpression; import java.util.Optional; import java.util.Set; @@ -191,6 +192,11 @@ public void checkCanShowRoleGrants(ConnectorTransactionHandle transactionHandle, } @Override + public Optional getRowFilter(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) + { + return Optional.empty(); + } + public void checkCanDropConstraint(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) { } diff --git a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/FileBasedAccessControl.java b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/FileBasedAccessControl.java index f715fd34e606..7692079f2365 100644 --- a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/FileBasedAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/FileBasedAccessControl.java @@ -23,6 +23,7 @@ import com.facebook.presto.spi.security.ConnectorIdentity; import com.facebook.presto.spi.security.PrestoPrincipal; import com.facebook.presto.spi.security.Privilege; +import com.facebook.presto.spi.security.ViewExpression; import com.google.common.collect.ImmutableSet; import javax.inject.Inject; @@ -302,6 +303,11 @@ public void checkCanShowRoleGrants(ConnectorTransactionHandle transactionHandle, } @Override + public Optional getRowFilter(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) + { + return Optional.empty(); + } + public void checkCanDropConstraint(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) { if (!checkTablePermission(identity, tableName, OWNERSHIP)) { diff --git a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingConnectorAccessControl.java b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingConnectorAccessControl.java index c6f769f7c9c2..336631a08263 100644 --- a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingConnectorAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingConnectorAccessControl.java @@ -21,6 +21,7 @@ import com.facebook.presto.spi.security.ConnectorIdentity; import com.facebook.presto.spi.security.PrestoPrincipal; import com.facebook.presto.spi.security.Privilege; +import com.facebook.presto.spi.security.ViewExpression; import java.util.Optional; import java.util.Set; @@ -239,6 +240,11 @@ public void checkCanShowRoleGrants(ConnectorTransactionHandle transactionHandle, } @Override + public Optional getRowFilter(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) + { + return delegate().getRowFilter(transactionHandle, identity, context, tableName); + } + public void checkCanDropConstraint(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) { delegate().checkCanDropConstraint(transactionHandle, identity, context, tableName); diff --git a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingSystemAccessControl.java b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingSystemAccessControl.java index a280e1a54cd9..c8d4e8d68fcc 100644 --- a/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingSystemAccessControl.java +++ b/presto-plugin-toolkit/src/main/java/com/facebook/presto/plugin/base/security/ForwardingSystemAccessControl.java @@ -22,6 +22,7 @@ import com.facebook.presto.spi.security.PrestoPrincipal; import com.facebook.presto.spi.security.Privilege; import com.facebook.presto.spi.security.SystemAccessControl; +import com.facebook.presto.spi.security.ViewExpression; import java.security.Principal; import java.security.cert.X509Certificate; @@ -231,6 +232,11 @@ public void checkCanRevokeTablePrivilege(Identity identity, AccessControlContext } @Override + public Optional getRowFilter(Identity identity, AccessControlContext context, CatalogSchemaTableName tableName) + { + return delegate().getRowFilter(identity, context, tableName); + } + public void checkCanDropConstraint(Identity identity, AccessControlContext context, CatalogSchemaTableName table) { delegate().checkCanDropConstraint(identity, context, table); diff --git a/presto-spi/pom.xml b/presto-spi/pom.xml index ef048b50ffb2..8f23f2434a64 100644 --- a/presto-spi/pom.xml +++ b/presto-spi/pom.xml @@ -53,6 +53,12 @@ jol-core + + com.google.guava + guava + provided + + com.facebook.presto @@ -85,12 +91,6 @@ test - - com.google.guava - guava - test - - com.fasterxml.jackson.core jackson-databind diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/StandardErrorCode.java b/presto-spi/src/main/java/com/facebook/presto/spi/StandardErrorCode.java index 2aef128032b8..8e8512651fdb 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/StandardErrorCode.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/StandardErrorCode.java @@ -70,6 +70,8 @@ public enum StandardErrorCode WARNING_AS_ERROR(0x0000_002C, USER_ERROR), INVALID_ARGUMENTS(0x0000_002D, USER_ERROR), EXCEEDED_PLAN_NODE_LIMIT(0x0000_002E, USER_ERROR), + INVALID_ROW_FILTER(0x0000_002F, USER_ERROR), + ROW_FILTER_TYPE_MISMATCH(0x0000_0030, USER_ERROR), GENERIC_INTERNAL_ERROR(0x0001_0000, INTERNAL_ERROR), TOO_MANY_REQUESTS_FAILED(0x0001_0001, INTERNAL_ERROR, true), diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/connector/ConnectorAccessControl.java b/presto-spi/src/main/java/com/facebook/presto/spi/connector/ConnectorAccessControl.java index 422d34d3fcbf..fd1b60396893 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/connector/ConnectorAccessControl.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/connector/ConnectorAccessControl.java @@ -19,6 +19,7 @@ import com.facebook.presto.spi.security.ConnectorIdentity; import com.facebook.presto.spi.security.PrestoPrincipal; import com.facebook.presto.spi.security.Privilege; +import com.facebook.presto.spi.security.ViewExpression; import java.util.Optional; import java.util.Set; @@ -363,6 +364,18 @@ default void checkCanShowRoleGrants(ConnectorTransactionHandle transactionHandle denyShowRoleGrants(catalogName); } + /** + * Get a row filter associated with the given table and identity. + * + * The filter must be a scalar SQL expression of boolean type over the columns in the table. + * + * @return the filter, or {@link Optional#empty()} if not applicable + */ + default Optional getRowFilter(ConnectorTransactionHandle transactionHandle, ConnectorIdentity identity, AccessControlContext context, SchemaTableName tableName) + { + return Optional.empty(); + } + /** * Check if identity is allowed to drop constraints from the specified table in this catalog. * diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/security/AccessControl.java b/presto-spi/src/main/java/com/facebook/presto/spi/security/AccessControl.java index 0866803040a8..2aad4254008d 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/security/AccessControl.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/security/AccessControl.java @@ -18,6 +18,7 @@ import com.facebook.presto.common.Subfield; import com.facebook.presto.common.transaction.TransactionId; import com.facebook.presto.spi.SchemaTableName; +import com.google.common.collect.ImmutableList; import java.security.Principal; import java.security.cert.X509Certificate; @@ -294,6 +295,14 @@ default AuthorizedIdentity selectAuthorizedIdentity(Identity identity, AccessCon */ void checkCanShowRoleGrants(TransactionId transactionId, Identity identity, AccessControlContext context, String catalogName); + /** + * Get Row Filters + */ + default List getRowFilters(TransactionId transactionId, Identity identity, AccessControlContext context, QualifiedObjectName tableName) + { + return ImmutableList.of(); + } + /** * Check if identity is allowed to drop constraint from the specified table. * diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/security/SystemAccessControl.java b/presto-spi/src/main/java/com/facebook/presto/spi/security/SystemAccessControl.java index d51b51763138..7bc9f3957dbb 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/security/SystemAccessControl.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/security/SystemAccessControl.java @@ -339,6 +339,18 @@ default void checkCanRevokeTablePrivilege(Identity identity, AccessControlContex denyRevokeTablePrivilege(privilege.toString(), table.toString()); } + /** + * Get a row filter associated with the given table and identity. + * + * The filter must be a scalar SQL expression of boolean type over the columns in the table. + * + * @return the filter, or {@link Optional#empty()} if not applicable + */ + default Optional getRowFilter(Identity identity, AccessControlContext context, CatalogSchemaTableName tableName) + { + return Optional.empty(); + } + /** * Check if identity is allowed to drop constraints from the specified table in a catalog. * diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/security/ViewExpression.java b/presto-spi/src/main/java/com/facebook/presto/spi/security/ViewExpression.java new file mode 100644 index 000000000000..53a253ebcf13 --- /dev/null +++ b/presto-spi/src/main/java/com/facebook/presto/spi/security/ViewExpression.java @@ -0,0 +1,58 @@ +/* + * 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 com.facebook.presto.spi.security; + +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +public class ViewExpression +{ + private final String identity; + private final Optional catalog; + private final Optional schema; + private final String expression; + + public ViewExpression(String identity, Optional catalog, Optional schema, String expression) + { + this.identity = requireNonNull(identity, "identity is null"); + this.catalog = requireNonNull(catalog, "catalog is null"); + this.schema = requireNonNull(schema, "schema is null"); + this.expression = requireNonNull(expression, "expression is null"); + + if (!catalog.isPresent() && schema.isPresent()) { + throw new IllegalArgumentException("catalog must be present if schema is present"); + } + } + + public String getIdentity() + { + return identity; + } + + public Optional getCatalog() + { + return catalog; + } + + public Optional getSchema() + { + return schema; + } + + public String getExpression() + { + return expression; + } +}