You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@asterixdb.apache.org by dl...@apache.org on 2021/10/01 20:03:22 UTC

[asterixdb] branch master updated: [NO ISSUE][MD] Support non-enforced foreign key declarations

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 914983f  [NO ISSUE][MD] Support non-enforced foreign key declarations
914983f is described below

commit 914983f0da5a44f8a1fe11518aa7dbcdbbb13ac0
Author: Dmitry Lychagin <dm...@couchbase.com>
AuthorDate: Thu Sep 30 15:11:11 2021 -0700

    [NO ISSUE][MD] Support non-enforced foreign key declarations
    
    - user model changes: yes
    - storage format changes: no
    - interface changes: no
    
    Details:
    - Support non-enforced foreign key declarations
      in CREATE VIEW for typed views
    - These declarations can only refer to other typed views
    - Add testcases
    
    Change-Id: I1478d6f59a66925a3e5ef3ad6c11ae219c8049be
    Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/13444
    Integration-Tests: Jenkins <je...@fulliautomatix.ics.uci.edu>
    Reviewed-by: Glenn Galvizo <gg...@uci.edu>
    Tested-by: Jenkins <je...@fulliautomatix.ics.uci.edu>
---
 .../asterix/translator/util/ValidateUtil.java      |  75 +++++++++--
 .../asterix/app/translator/QueryTranslator.java    | 113 +++++++++++++++--
 .../create-view-2-negative.12.ddl.sqlpp            |   2 +-
 ...l.sqlpp => create-view-2-negative.13.ddl.sqlpp} |   7 +-
 .../create-view-6-typed-negative.15.ddl.sqlpp}     |  23 ++--
 .../create-view-6-typed-negative.16.ddl.sqlpp}     |  22 ++--
 .../create-view-6-typed-negative.17.ddl.sqlpp}     |  25 ++--
 .../create-view-6-typed-negative.18.ddl.sqlpp}     |  25 ++--
 .../create-view-6-typed-negative.19.ddl.sqlpp}     |  25 ++--
 .../create-view-6-typed-negative.20.ddl.sqlpp}     |  27 ++--
 .../create-view-6-typed-negative.21.ddl.sqlpp}     |  27 ++--
 .../create-view-6-typed-negative.22.ddl.sqlpp}     |  28 +++--
 .../create-view-6-typed-negative.23.ddl.sqlpp}     |  28 +++--
 .../create-view-6-typed-negative.24.ddl.sqlpp}     |  28 +++--
 .../create-view-6-typed-negative.25.ddl.sqlpp}     |  28 +++--
 .../create-view-6-typed-negative.26.ddl.sqlpp}     |  28 +++--
 .../create-view-7-foreign-key.1.ddl.sqlpp          |  60 +++++++++
 .../create-view-7-foreign-key.2.query.sqlpp}       |  11 +-
 .../create-view-7-foreign-key.3.ddl.sqlpp          |  52 ++++++++
 .../create-view-7-foreign-key.4.query.sqlpp}       |  11 +-
 .../drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp  |  11 ++
 .../drop-dataverse-1.2.query.sqlpp}                |  10 +-
 .../drop-dataverse-2-negative.7.ddl.sqlpp}         |  28 +++--
 .../create-view-7-foreign-key.2.adm                |   3 +
 .../create-view-7-foreign-key.4.adm                |   3 +
 .../view/drop-dataverse-1/drop-dataverse-1.1.adm   |   1 +
 .../test/resources/runtimets/testsuite_sqlpp.xml   |  27 +++-
 .../asterix/common/exceptions/ErrorCode.java       |  10 +-
 .../src/main/resources/asx_errormsg/en.properties  |  12 +-
 .../lang/common/statement/CreateViewStatement.java |  76 +++++++++---
 .../apache/asterix/lang/common/util/ViewUtil.java  |  91 +++++++-------
 .../asterix-lang-sqlpp/src/main/javacc/SQLPP.jj    |  65 +++++-----
 .../metadata/bootstrap/MetadataRecordTypes.java    |   5 +
 .../asterix/metadata/entities/ViewDetails.java     | 137 ++++++++++++++++++---
 .../DatasetTupleTranslator.java                    |  49 +++++++-
 35 files changed, 863 insertions(+), 310 deletions(-)

diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/translator/util/ValidateUtil.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/translator/util/ValidateUtil.java
index 13bcfb6..93d9347 100644
--- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/translator/util/ValidateUtil.java
+++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/translator/util/ValidateUtil.java
@@ -19,12 +19,17 @@
 package org.apache.asterix.translator.util;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.apache.asterix.common.config.DatasetConfig.IndexType;
 import org.apache.asterix.common.exceptions.CompilationException;
 import org.apache.asterix.common.exceptions.ErrorCode;
+import org.apache.asterix.lang.common.statement.CreateViewStatement;
+import org.apache.asterix.metadata.entities.Index;
 import org.apache.asterix.metadata.utils.KeyFieldTypeUtil;
+import org.apache.asterix.om.typecomputer.impl.TypeComputeUtils;
 import org.apache.asterix.om.types.ARecordType;
 import org.apache.asterix.om.types.ATypeTag;
 import org.apache.asterix.om.types.IAType;
@@ -37,6 +42,9 @@ import org.apache.hyracks.api.exceptions.SourceLocation;
  * or a list of key fields are valid in a record type.
  */
 public class ValidateUtil {
+
+    private static final String PRIMARY = "primary";
+
     private ValidateUtil() {
     }
 
@@ -117,11 +125,19 @@ public class ValidateUtil {
     public static List<IAType> validatePartitioningExpressions(ARecordType recType, ARecordType metaRecType,
             List<List<String>> partitioningExprs, List<Integer> keySourceIndicators, boolean autogenerated,
             SourceLocation sourceLoc) throws AlgebricksException {
+        return validatePartitioningExpressionsImpl(recType, metaRecType, partitioningExprs, keySourceIndicators,
+                autogenerated, true, sourceLoc);
+    }
+
+    private static List<IAType> validatePartitioningExpressionsImpl(ARecordType recType, ARecordType metaRecType,
+            List<List<String>> partitioningExprs, List<Integer> keySourceIndicators, boolean autogenerated,
+            boolean forPrimaryKey, SourceLocation sourceLoc) throws AlgebricksException {
+        String keyKindDisplayName = forPrimaryKey ? PRIMARY : "";
         List<IAType> partitioningExprTypes = new ArrayList<>(partitioningExprs.size());
         if (autogenerated) {
             if (partitioningExprs.size() > 1) {
-                throw new CompilationException(ErrorCode.COMPILATION_CANNOT_AUTOGENERATE_COMPOSITE_PRIMARY_KEY,
-                        sourceLoc);
+                throw new CompilationException(ErrorCode.COMPILATION_CANNOT_AUTOGENERATE_COMPOSITE_KEY, sourceLoc,
+                        keyKindDisplayName);
             }
             List<String> fieldName = partitioningExprs.get(0);
             IAType fieldType = recType.getSubFieldType(fieldName);
@@ -133,7 +149,7 @@ public class ValidateUtil {
             ATypeTag pkTypeTag = fieldType.getTypeTag();
             if (pkTypeTag != ATypeTag.UUID) {
                 throw new CompilationException(ErrorCode.COMPILATION_ILLEGAL_AUTOGENERATED_TYPE, sourceLoc,
-                        pkTypeTag.name(), ATypeTag.UUID.name());
+                        keyKindDisplayName, pkTypeTag.name(), ATypeTag.UUID.name());
             }
         } else {
             partitioningExprTypes =
@@ -145,12 +161,16 @@ public class ValidateUtil {
                     throw new CompilationException(ErrorCode.COMPILATION_FIELD_NOT_FOUND, sourceLoc,
                             RecordUtil.toFullyQualifiedName(partitioningExpr));
                 }
-                boolean nullable = KeyFieldTypeUtil.chooseSource(keySourceIndicators, i, recType, metaRecType)
-                        .isSubFieldNullable(partitioningExpr);
-                if (nullable) {
-                    // key field is nullable
-                    throw new CompilationException(ErrorCode.COMPILATION_PRIMARY_KEY_CANNOT_BE_NULLABLE, sourceLoc,
-                            RecordUtil.toFullyQualifiedName(partitioningExpr));
+                if (forPrimaryKey) {
+                    boolean nullable = KeyFieldTypeUtil.chooseSource(keySourceIndicators, i, recType, metaRecType)
+                            .isSubFieldNullable(partitioningExpr);
+                    if (nullable) {
+                        // key field is nullable
+                        throw new CompilationException(ErrorCode.COMPILATION_KEY_CANNOT_BE_NULLABLE, sourceLoc,
+                                keyKindDisplayName, RecordUtil.toFullyQualifiedName(partitioningExpr));
+                    }
+                } else {
+                    fieldType = TypeComputeUtils.getActualType(fieldType);
                 }
                 switch (fieldType.getTypeTag()) {
                     case TINYINT:
@@ -169,11 +189,11 @@ public class ValidateUtil {
                     case DAYTIMEDURATION:
                         break;
                     case UNION:
-                        throw new CompilationException(ErrorCode.COMPILATION_PRIMARY_KEY_CANNOT_BE_NULLABLE, sourceLoc,
-                                RecordUtil.toFullyQualifiedName(partitioningExpr));
+                        throw new CompilationException(ErrorCode.COMPILATION_KEY_CANNOT_BE_NULLABLE, sourceLoc,
+                                keyKindDisplayName, RecordUtil.toFullyQualifiedName(partitioningExpr));
                     default:
-                        throw new CompilationException(ErrorCode.COMPILATION_ILLEGAL_PRIMARY_KEY_TYPE, sourceLoc,
-                                fieldType.getTypeTag());
+                        throw new CompilationException(ErrorCode.COMPILATION_ILLEGAL_KEY_TYPE, sourceLoc,
+                                fieldType.getTypeTag(), keyKindDisplayName);
                 }
             }
         }
@@ -290,4 +310,33 @@ public class ValidateUtil {
                         String.valueOf(indexType));
         }
     }
+
+    /**
+     * Validates the key fields that will be used as either primary or foreign keys of a view.
+     */
+    public static List<String> validateViewKeyFields(CreateViewStatement.KeyDecl keyDecl, ARecordType itemType,
+            boolean isForeignKey, SourceLocation sourceLoc) throws AlgebricksException {
+        List<Integer> sourceIndicators = keyDecl.getSourceIndicators();
+        List<List<String>> fields = keyDecl.getFields();
+        int n = fields.size();
+        List<String> keyFields = new ArrayList<>(n);
+        for (int i = 0; i < n; i++) {
+            if (sourceIndicators.get(i) != Index.RECORD_INDICATOR) {
+                throw new CompilationException(isForeignKey ? ErrorCode.INVALID_FOREIGN_KEY_DEFINITION
+                        : ErrorCode.INVALID_PRIMARY_KEY_DEFINITION, sourceLoc);
+            }
+            List<String> nestedField = fields.get(i);
+            if (nestedField.size() != 1) {
+                throw new CompilationException(isForeignKey ? ErrorCode.INVALID_FOREIGN_KEY_DEFINITION
+                        : ErrorCode.INVALID_PRIMARY_KEY_DEFINITION, sourceLoc);
+            }
+            keyFields.add(nestedField.get(0));
+        }
+
+        validatePartitioningExpressionsImpl(itemType, null,
+                keyFields.stream().map(Collections::singletonList).collect(Collectors.toList()),
+                Collections.nCopies(keyFields.size(), Index.RECORD_INDICATOR), false, !isForeignKey, sourceLoc);
+
+        return keyFields;
+    }
 }
diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java
index 0baf2b2..84a34a9 100644
--- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java
+++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/app/translator/QueryTranslator.java
@@ -234,6 +234,7 @@ import org.apache.hyracks.algebricks.common.utils.Pair;
 import org.apache.hyracks.algebricks.common.utils.Triple;
 import org.apache.hyracks.algebricks.core.algebra.expressions.AbstractFunctionCallExpression.FunctionKind;
 import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
+import org.apache.hyracks.algebricks.core.algebra.util.OperatorPropertiesUtil;
 import org.apache.hyracks.algebricks.data.IAWriterFactory;
 import org.apache.hyracks.algebricks.data.IResultSerializerFactoryProvider;
 import org.apache.hyracks.algebricks.runtime.serializer.ResultSerializerFactoryProvider;
@@ -2530,28 +2531,118 @@ public class QueryTranslator extends AbstractLangTranslator implements IStatemen
                 }
             }
 
-            List<String> primaryKeyFields = cvs.getPrimaryKeyFields();
+            DatasetFullyQualifiedName viewQualifiedName = new DatasetFullyQualifiedName(dataverseName, viewName);
+
             Datatype itemTypeEntity = null;
             boolean itemTypeIsInline = false;
+            CreateViewStatement.KeyDecl primaryKeyDecl = cvs.getPrimaryKeyDecl();
+            List<String> primaryKeyFields = null;
+            List<CreateViewStatement.ForeignKeyDecl> foreignKeyDecls = cvs.getForeignKeyDecls();
+            List<ViewDetails.ForeignKey> foreignKeys = null;
+            String datetimeFormat = null, dateFormat = null, timeFormat = null;
             if (cvs.hasItemType()) {
                 Pair<Datatype, Boolean> itemTypePair = fetchDatasetItemType(mdTxnCtx, DatasetType.VIEW,
                         itemTypeDataverseName, itemTypeName, cvs.getItemType(), false, metadataProvider, sourceLoc);
                 itemTypeEntity = itemTypePair.first;
                 itemTypeIsInline = itemTypePair.second;
-                if (primaryKeyFields != null) {
-                    ValidateUtil.validatePartitioningExpressions((ARecordType) itemTypeEntity.getDatatype(), null,
-                            primaryKeyFields.stream().map(Collections::singletonList).collect(Collectors.toList()),
-                            Collections.nCopies(primaryKeyFields.size(), Index.RECORD_INDICATOR), false, sourceLoc);
+                ARecordType itemType = (ARecordType) itemTypeEntity.getDatatype();
+                if (primaryKeyDecl != null) {
+                    primaryKeyFields = ValidateUtil.validateViewKeyFields(primaryKeyDecl, itemType, false, sourceLoc);
+                }
+                if (foreignKeyDecls != null) {
+                    foreignKeys = new ArrayList<>(foreignKeyDecls.size());
+                    for (CreateViewStatement.ForeignKeyDecl foreignKeyDecl : foreignKeyDecls) {
+                        List<String> foreignKeyFields =
+                                ValidateUtil.validateViewKeyFields(foreignKeyDecl, itemType, true, sourceLoc);
+                        DataverseName refDataverseName = foreignKeyDecl.getReferencedDataverseName();
+                        if (refDataverseName == null) {
+                            refDataverseName = dataverseName;
+                        } else {
+                            Dataverse refDataverse = MetadataManager.INSTANCE.getDataverse(mdTxnCtx, refDataverseName);
+                            if (refDataverse == null) {
+                                throw new CompilationException(ErrorCode.UNKNOWN_DATAVERSE, sourceLoc,
+                                        refDataverseName);
+                            }
+                        }
+                        String refDatasetName = foreignKeyDecl.getReferencedDatasetName().getValue();
+                        boolean isSelfRef = refDataverseName.equals(dataverseName) && refDatasetName.equals(viewName);
+                        DatasetType refDatasetType;
+                        DatasetFullyQualifiedName refQualifiedName;
+                        List<String> refPrimaryKeyFields;
+                        if (isSelfRef) {
+                            refDatasetType = DatasetType.VIEW;
+                            refQualifiedName = viewQualifiedName;
+                            refPrimaryKeyFields = primaryKeyFields;
+                        } else {
+                            // findDataset() will acquire lock on referenced dataset (view)
+                            Dataset refDataset = metadataProvider.findDataset(refDataverseName, refDatasetName, true);
+                            if (refDataset == null || DatasetUtil.isNotView(refDataset)) {
+                                throw new CompilationException(ErrorCode.UNKNOWN_VIEW, sourceLoc,
+                                        DatasetUtil.getFullyQualifiedDisplayName(refDataverseName, refDatasetName));
+                            }
+                            ViewDetails refViewDetails = (ViewDetails) refDataset.getDatasetDetails();
+                            refDatasetType = refDataset.getDatasetType();
+                            refQualifiedName = new DatasetFullyQualifiedName(refDataverseName, refDatasetName);
+                            refPrimaryKeyFields = refViewDetails.getPrimaryKeyFields();
+                        }
+
+                        if (refPrimaryKeyFields == null) {
+                            throw new CompilationException(ErrorCode.INVALID_FOREIGN_KEY_DEFINITION_REF_PK_NOT_FOUND,
+                                    sourceLoc, DatasetUtil.getDatasetTypeDisplayName(refDatasetType),
+                                    DatasetUtil.getFullyQualifiedDisplayName(refDataverseName, refDatasetName));
+                        } else if (refPrimaryKeyFields.size() != foreignKeyFields.size()) {
+                            throw new CompilationException(ErrorCode.INVALID_FOREIGN_KEY_DEFINITION_REF_PK_MISMATCH,
+                                    sourceLoc, DatasetUtil.getDatasetTypeDisplayName(refDatasetType),
+                                    DatasetUtil.getFullyQualifiedDisplayName(refDataverseName, refDatasetName));
+                        } else if (isSelfRef
+                                && !OperatorPropertiesUtil.disjoint(refPrimaryKeyFields, foreignKeyFields)) {
+                            throw new CompilationException(ErrorCode.INVALID_FOREIGN_KEY_DEFINITION, sourceLoc);
+                        }
+
+                        foreignKeys.add(new ViewDetails.ForeignKey(foreignKeyFields, refQualifiedName));
+                    }
+                }
+
+                Map<String, String> viewConfig =
+                        ViewUtil.validateViewConfiguration(cvs.getViewConfiguration(), cvs.getSourceLocation());
+                datetimeFormat = ViewUtil.getDatetimeFormat(viewConfig);
+                dateFormat = ViewUtil.getDateFormat(viewConfig);
+                timeFormat = ViewUtil.getTimeFormat(viewConfig);
+
+            } else {
+                if (primaryKeyDecl != null) {
+                    throw new CompilationException(ErrorCode.INVALID_PRIMARY_KEY_DEFINITION, cvs.getSourceLocation());
+                }
+                if (foreignKeyDecls != null) {
+                    throw new CompilationException(ErrorCode.INVALID_FOREIGN_KEY_DEFINITION, cvs.getSourceLocation());
+                }
+                if (cvs.getViewConfiguration() != null) {
+                    throw new CompilationException(ErrorCode.ILLEGAL_SET_PARAMETER, cvs.getSourceLocation(),
+                            cvs.getViewConfiguration().keySet().iterator().next());
+                }
+            }
+
+            if (existingDataset != null) {
+                ViewDetails existingViewDetails = (ViewDetails) existingDataset.getDatasetDetails();
+                List<String> existingPrimaryKeyFields = existingViewDetails.getPrimaryKeyFields();
+                // For now don't allow view replacement if existing view has primary keys and they are different
+                // from the new view's primary keys, because there could be another view that references
+                // these primary keys via its foreign keys declaration.
+                // In the future we should relax this check: scan datasets metadata and allow replacement in this case
+                // if there's no view that references this view
+                boolean allowToReplace =
+                        existingPrimaryKeyFields == null || existingPrimaryKeyFields.equals(primaryKeyFields);
+                if (!allowToReplace) {
+                    throw new CompilationException(ErrorCode.CANNOT_CHANGE_PRIMARY_KEY, cvs.getSourceLocation(),
+                            DatasetUtil.getDatasetTypeDisplayName(existingDataset.getDatasetType()),
+                            DatasetUtil.getFullyQualifiedDisplayName(existingDataset));
                 }
-            } else if (primaryKeyFields != null) {
-                throw new CompilationException(ErrorCode.INVALID_PRIMARY_KEY_DEFINITION, sourceLoc);
             }
 
             // Check whether the view is usable:
             // create a view declaration for this function,
             // and a query body that queries this view:
-            ViewDecl viewDecl =
-                    new ViewDecl(new DatasetFullyQualifiedName(dataverseName, viewName), cvs.getViewBodyExpression());
+            ViewDecl viewDecl = new ViewDecl(viewQualifiedName, cvs.getViewBodyExpression());
             viewDecl.setSourceLocation(sourceLoc);
             IQueryRewriter queryRewriter = rewriterFactory.createQueryRewriter();
             Query wrappedQuery = queryRewriter.createViewAccessorQuery(viewDecl);
@@ -2560,10 +2651,10 @@ public class QueryTranslator extends AbstractLangTranslator implements IStatemen
                     wrappedQuery, sessionOutput, false, false, Collections.emptyList(), warningCollector);
 
             List<List<Triple<DataverseName, String, String>>> dependencies =
-                    ViewUtil.getViewDependencies(viewDecl, queryRewriter);
+                    ViewUtil.getViewDependencies(viewDecl, foreignKeys, queryRewriter);
 
             ViewDetails viewDetails = new ViewDetails(cvs.getViewBody(), dependencies, cvs.getDefaultNull(),
-                    primaryKeyFields, cvs.getDatetimeFormat(), cvs.getDateFormat(), cvs.getTimeFormat());
+                    primaryKeyFields, foreignKeys, datetimeFormat, dateFormat, timeFormat);
 
             Dataset view = new Dataset(dataverseName, viewName, itemTypeDataverseName, itemTypeName,
                     MetadataConstants.METADATA_NODEGROUP_NAME, "", Collections.emptyMap(), viewDetails,
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp
index 633af81..b5acfc7 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp
@@ -17,7 +17,7 @@
  * under the License.
  */
 
---- Negative: cannot declare primary key
+--- Negative: untyped view cannot declare primary key
 
 drop dataverse test if exists;
 create dataverse test;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.13.ddl.sqlpp
similarity index 83%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.13.ddl.sqlpp
index 633af81..7ad5c5e 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.13.ddl.sqlpp
@@ -17,10 +17,13 @@
  * under the License.
  */
 
---- Negative: cannot declare primary key
+--- Negative: untyped view cannot declare foreign key
 
 drop dataverse test if exists;
 create dataverse test;
 
-create view test.v1 primary key (r) not enforced as
+create view test.v1 as
+  select r from range(1,2) r;
+
+create view test.v2 foreign key (r) references v1 not enforced as
   select r from range(1,2) r;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.15.ddl.sqlpp
similarity index 71%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.15.ddl.sqlpp
index 41a64fc..00c186e 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.15.ddl.sqlpp
@@ -17,21 +17,20 @@
  * under the License.
  */
 
---- test drop dataverse with views
+/*
+ * Negative: foreign key definition requires 'not enforced' modifier
+ */
 
 drop dataverse test1 if exists;
 create dataverse test1;
 
-create view test1.v1 as
-  select r from range(0,2) r;
-
-create view test1.v2 as
-  select v1.* from v1;
-
-create view test1.v3(r bigint) default null as v2;
-
-create type test1.t4 as closed { r:int64? };
+use test1;
 
-create view test1.v4(t4) default null as v2;
+create dataset employee(e_id int not unknown, e_name string, e_mgr_id int)
+  primary key e_id;
 
-drop dataverse test1;
+create view employee_v1(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  foreign key (e_mgr_id) references employee_v1
+  as employee;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.16.ddl.sqlpp
similarity index 70%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.16.ddl.sqlpp
index 41a64fc..212f31d 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.16.ddl.sqlpp
@@ -17,21 +17,19 @@
  * under the License.
  */
 
---- test drop dataverse with views
+/*
+ * Negative: cannot create self-referenced foreign key if primary key is undefined
+ */
 
 drop dataverse test1 if exists;
 create dataverse test1;
 
-create view test1.v1 as
-  select r from range(0,2) r;
-
-create view test1.v2 as
-  select v1.* from v1;
-
-create view test1.v3(r bigint) default null as v2;
-
-create type test1.t4 as closed { r:int64? };
+use test1;
 
-create view test1.v4(t4) default null as v2;
+create dataset employee(e_id int not unknown, e_name string, e_mgr_id int, e_hrr_id int)
+  primary key e_id;
 
-drop dataverse test1;
+create view employee_v1(e_id int not unknown, e_mgr_id int)
+  default null
+  foreign key (e_mgr_id) references employee_v1 not enforced
+  as employee;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.17.ddl.sqlpp
similarity index 63%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.17.ddl.sqlpp
index 41a64fc..2b33d73 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.17.ddl.sqlpp
@@ -17,21 +17,22 @@
  * under the License.
  */
 
---- test drop dataverse with views
+/*
+ * Negative: cannot create self-referenced foreign key if
+ *           its definition doesn't match the primary key
+ *           (foreign key declaration has more fields)
+ */
 
 drop dataverse test1 if exists;
 create dataverse test1;
 
-create view test1.v1 as
-  select r from range(0,2) r;
-
-create view test1.v2 as
-  select v1.* from v1;
-
-create view test1.v3(r bigint) default null as v2;
-
-create type test1.t4 as closed { r:int64? };
+use test1;
 
-create view test1.v4(t4) default null as v2;
+create dataset employee(e_id int not unknown, e_name string, e_mgr_id int, e_hrr_id int)
+  primary key e_id;
 
-drop dataverse test1;
+create view employee_v1(e_id int not unknown, e_mgr_id int, e_hrr_id int)
+  default null
+  primary key (e_id) not enforced
+  foreign key (e_mgr_id, e_hrr_id) references employee_v1 not enforced
+  as employee;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.18.ddl.sqlpp
similarity index 60%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.18.ddl.sqlpp
index 41a64fc..493f31b 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.18.ddl.sqlpp
@@ -17,21 +17,22 @@
  * under the License.
  */
 
---- test drop dataverse with views
+/*
+ * Negative: cannot create self-referenced foreign key if
+ *           its definition doesn't match the primary key
+ *           (foreign key declaration has fewer fields)
+ */
 
 drop dataverse test1 if exists;
 create dataverse test1;
 
-create view test1.v1 as
-  select r from range(0,2) r;
-
-create view test1.v2 as
-  select v1.* from v1;
-
-create view test1.v3(r bigint) default null as v2;
-
-create type test1.t4 as closed { r:int64? };
+use test1;
 
-create view test1.v4(t4) default null as v2;
+create dataset employee(e_id1 int not unknown, e_id2 int not unknown, e_name string, e_mgr_id int, e_hrr_id int)
+  primary key e_id1, e_id2;
 
-drop dataverse test1;
+create view employee_v2(e_id1 int not unknown, e_id2 int not unknown, e_mgr_id int, e_hrr_id int)
+  default null
+  primary key (e_id1, e_id2) not enforced
+  foreign key (e_mgr_id) references employee_v2 not enforced
+  as employee;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.19.ddl.sqlpp
similarity index 60%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.19.ddl.sqlpp
index 41a64fc..af69c77 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.19.ddl.sqlpp
@@ -17,21 +17,22 @@
  * under the License.
  */
 
---- test drop dataverse with views
+/*
+ * Negative: cannot create self-referenced foreign key if
+ *           its definition doesn't match the primary key
+ *           (foreign key declaration has same fields as primary key)
+ */
 
 drop dataverse test1 if exists;
 create dataverse test1;
 
-create view test1.v1 as
-  select r from range(0,2) r;
-
-create view test1.v2 as
-  select v1.* from v1;
-
-create view test1.v3(r bigint) default null as v2;
-
-create type test1.t4 as closed { r:int64? };
+use test1;
 
-create view test1.v4(t4) default null as v2;
+create dataset employee(e_id1 int not unknown, e_id2 int not unknown, e_name string, e_mgr_id int, e_hrr_id int)
+  primary key e_id1, e_id2;
 
-drop dataverse test1;
+create view employee_v2(e_id1 int not unknown, e_id2 int not unknown, e_mgr_id int, e_hrr_id int)
+  default null
+  primary key (e_id1, e_id2) not enforced
+  foreign key (e_id2, e_id1) references employee_v2 not enforced
+  as employee;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.20.ddl.sqlpp
similarity index 59%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.20.ddl.sqlpp
index 41a64fc..11c8f18 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.20.ddl.sqlpp
@@ -17,21 +17,30 @@
  * under the License.
  */
 
---- test drop dataverse with views
+/*
+ * Negative: cannot create foreign key if referenced dataverse does not exist
+ */
 
 drop dataverse test1 if exists;
 create dataverse test1;
 
-create view test1.v1 as
-  select r from range(0,2) r;
+drop dataverse test2 if exists;
+create dataverse test2;
 
-create view test1.v2 as
-  select v1.* from v1;
+use test1;
 
-create view test1.v3(r bigint) default null as v2;
+create dataset employee(e_id int not unknown, e_name string, e_mgr_id int, e_hrr_id int)
+  primary key e_id;
 
-create type test1.t4 as closed { r:int64? };
+create view employee_v1(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  as employee;
 
-create view test1.v4(t4) default null as v2;
+use test2;
 
-drop dataverse test1;
+create view employee_v2(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  foreign key (e_mgr_id) references test3.employee_v1 not enforced
+  as test1.employee;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.21.ddl.sqlpp
similarity index 59%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.21.ddl.sqlpp
index 41a64fc..9504a91 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.21.ddl.sqlpp
@@ -17,21 +17,30 @@
  * under the License.
  */
 
---- test drop dataverse with views
+/*
+ * Negative: cannot create foreign key if referenced view does not exist
+ */
 
 drop dataverse test1 if exists;
 create dataverse test1;
 
-create view test1.v1 as
-  select r from range(0,2) r;
+drop dataverse test2 if exists;
+create dataverse test2;
 
-create view test1.v2 as
-  select v1.* from v1;
+use test1;
 
-create view test1.v3(r bigint) default null as v2;
+create dataset employee(e_id int not unknown, e_name string, e_mgr_id int, e_hrr_id int)
+  primary key e_id;
 
-create type test1.t4 as closed { r:int64? };
+create view employee_v1(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  as employee;
 
-create view test1.v4(t4) default null as v2;
+use test2;
 
-drop dataverse test1;
+create view employee_v2(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  foreign key (e_mgr_id) references test1.employee_v3 not enforced
+  as test1.employee;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.22.ddl.sqlpp
similarity index 56%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.22.ddl.sqlpp
index 41a64fc..6a1dcc1 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.22.ddl.sqlpp
@@ -17,21 +17,31 @@
  * under the License.
  */
 
---- test drop dataverse with views
+/*
+ * Negative: cannot create foreign key if referenced object exist, but is not a view
+ *           (this use-case will be allowed in the future)
+ */
 
 drop dataverse test1 if exists;
 create dataverse test1;
 
-create view test1.v1 as
-  select r from range(0,2) r;
+drop dataverse test2 if exists;
+create dataverse test2;
 
-create view test1.v2 as
-  select v1.* from v1;
+use test1;
 
-create view test1.v3(r bigint) default null as v2;
+create dataset employee(e_id int not unknown, e_name string, e_mgr_id int, e_hrr_id int)
+  primary key e_id;
 
-create type test1.t4 as closed { r:int64? };
+create view employee_v1(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  as employee;
 
-create view test1.v4(t4) default null as v2;
+use test2;
 
-drop dataverse test1;
+create view employee_v2(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  foreign key (e_mgr_id) references test1.employee not enforced
+  as test1.employee_v1;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.23.ddl.sqlpp
similarity index 59%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.23.ddl.sqlpp
index 41a64fc..d751f03 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.23.ddl.sqlpp
@@ -17,21 +17,31 @@
  * under the License.
  */
 
---- test drop dataverse with views
+/*
+ * Negative: invalid foreign key definition. nested field specified.
+ *
+ */
 
 drop dataverse test1 if exists;
 create dataverse test1;
 
-create view test1.v1 as
-  select r from range(0,2) r;
+drop dataverse test2 if exists;
+create dataverse test2;
 
-create view test1.v2 as
-  select v1.* from v1;
+use test1;
 
-create view test1.v3(r bigint) default null as v2;
+create dataset employee(e_id int not unknown, e_name string, e_mgr_id int, e_hrr_id int)
+  primary key e_id;
 
-create type test1.t4 as closed { r:int64? };
+create view employee_v1(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  as employee;
 
-create view test1.v4(t4) default null as v2;
+use test2;
 
-drop dataverse test1;
+create view employee_v2(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  foreign key (e_mgr_id.e_mgr_id2) references test1.employee_v1 not enforced
+  as test1.employee;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.24.ddl.sqlpp
similarity index 59%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.24.ddl.sqlpp
index 41a64fc..d1d3be8 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.24.ddl.sqlpp
@@ -17,21 +17,27 @@
  * under the License.
  */
 
---- test drop dataverse with views
+/*
+ * Negative: invalid foreign key definition. meta() field specified.
+ *
+ */
 
 drop dataverse test1 if exists;
 create dataverse test1;
 
-create view test1.v1 as
-  select r from range(0,2) r;
-
-create view test1.v2 as
-  select v1.* from v1;
-
-create view test1.v3(r bigint) default null as v2;
+drop dataverse test2 if exists;
+create dataverse test2;
 
-create type test1.t4 as closed { r:int64? };
+create dataset test1.employee(e_id int not unknown, e_name string, e_mgr_id int, e_hrr_id int)
+  primary key e_id;
 
-create view test1.v4(t4) default null as v2;
+create view test1.employee_v1(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  as employee;
 
-drop dataverse test1;
+create view test2.employee_v2(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  foreign key (meta().e_mgr_id) references test1.employee_v1 not enforced
+  as test1.employee;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.25.ddl.sqlpp
similarity index 56%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.25.ddl.sqlpp
index 41a64fc..d718fa7 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.25.ddl.sqlpp
@@ -17,21 +17,27 @@
  * under the License.
  */
 
---- test drop dataverse with views
+/*
+ * Negative: invalid foreign key definition. number of fields
+ *           doesn't match number of primary key fields of the referred view
+ */
 
 drop dataverse test1 if exists;
 create dataverse test1;
 
-create view test1.v1 as
-  select r from range(0,2) r;
-
-create view test1.v2 as
-  select v1.* from v1;
-
-create view test1.v3(r bigint) default null as v2;
+drop dataverse test2 if exists;
+create dataverse test2;
 
-create type test1.t4 as closed { r:int64? };
+create dataset test1.employee(e_id int not unknown, e_name string, e_mgr_id int, e_hrr_id int)
+  primary key e_id;
 
-create view test1.v4(t4) default null as v2;
+create view test1.employee_v1(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  as employee;
 
-drop dataverse test1;
+create view test2.employee_v2(e_id int not unknown, e_mgr_id int, e_hrr_id int)
+  default null
+  primary key (e_id) not enforced
+  foreign key (e_mgr_id, e_hrr_id) references test1.employee_v1 not enforced
+  as test1.employee;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.26.ddl.sqlpp
similarity index 55%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.26.ddl.sqlpp
index 41a64fc..53e94fc 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.26.ddl.sqlpp
@@ -17,21 +17,25 @@
  * under the License.
  */
 
---- test drop dataverse with views
+/*
+ * Negative: cannot replace typed view if the replacement declaration has a different primary key
+ *           because there might be another typed view that refers to its primary key
+ *           via foreign key declaration.
+ *           (this limitation will be relaxed in the future)
+ */
 
 drop dataverse test1 if exists;
 create dataverse test1;
 
-create view test1.v1 as
-  select r from range(0,2) r;
-
-create view test1.v2 as
-  select v1.* from v1;
-
-create view test1.v3(r bigint) default null as v2;
-
-create type test1.t4 as closed { r:int64? };
+create dataset test1.employee(e_id int not unknown, e_name string, e_mgr_id int, e_hrr_id int)
+  primary key e_id;
 
-create view test1.v4(t4) default null as v2;
+create view test1.employee_v1(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  as employee;
 
-drop dataverse test1;
+create or replace view test1.employee_v1(e_id int not unknown, e_mgr_id int not unknown)
+  default null
+  primary key (e_id, e_mgr_id) not enforced
+  as employee;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-7-foreign-key/create-view-7-foreign-key.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-7-foreign-key/create-view-7-foreign-key.1.ddl.sqlpp
new file mode 100644
index 0000000..2e6f347
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-7-foreign-key/create-view-7-foreign-key.1.ddl.sqlpp
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * Test 'self reference' foreign keys
+ */
+
+drop dataverse test1 if exists;
+create dataverse test1;
+
+drop dataverse test2 if exists;
+create dataverse test2;
+
+use test1;
+
+create dataset employee(e_id int not unknown, e_name string, e_mgr_id int, e_hrr_id int)
+  primary key e_id;
+
+create view employee_v1(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  foreign key (e_mgr_id) references employee_v1 not enforced
+  as employee;
+
+create view employee_v2(e_id int not unknown, e_mgr_id int, e_hrr_id int)
+  default null
+  primary key (e_id) not enforced
+  foreign key (e_mgr_id) references test1.employee_v2 not enforced
+  foreign key (e_hrr_id) references test1.employee_v2 not enforced
+  as employee;
+
+use test2;
+
+create dataset employee2(e_id1 int not unknown, e_id2 int not unknown, e_name string,
+  e_mgr_id1 int, e_mgr_id2 int, e_hrr_id1 int, e_hrr_id2 int)
+  primary key e_id1, e_id2;
+
+create view employee2_v1(e_id1 int not unknown, e_id2 int not unknown,
+  e_mgr_id1 int, e_mgr_id2 int, e_hrr_id1 int, e_hrr_id2 int)
+  default null
+  primary key (e_id1, e_id2) not enforced
+  foreign key (e_mgr_id1, e_mgr_id2) references employee2_v1 not enforced
+  foreign key (e_hrr_id1, e_hrr_id2) references employee2_v1 not enforced
+  as employee2;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-7-foreign-key/create-view-7-foreign-key.2.query.sqlpp
similarity index 81%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-7-foreign-key/create-view-7-foreign-key.2.query.sqlpp
index 633af81..2f44238 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-7-foreign-key/create-view-7-foreign-key.2.query.sqlpp
@@ -17,10 +17,7 @@
  * under the License.
  */
 
---- Negative: cannot declare primary key
-
-drop dataverse test if exists;
-create dataverse test;
-
-create view test.v1 primary key (r) not enforced as
-  select r from range(1,2) r;
+select d.DataverseName, d.DatasetName, d.ViewDetails
+from Metadata.`Dataset` d
+where d.DataverseName like "test%" and d.DatasetType = "VIEW"
+order by d.DataverseName, d.DatasetName;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-7-foreign-key/create-view-7-foreign-key.3.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-7-foreign-key/create-view-7-foreign-key.3.ddl.sqlpp
new file mode 100644
index 0000000..2f23876
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-7-foreign-key/create-view-7-foreign-key.3.ddl.sqlpp
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * Test multiple foreign keys, cross-dataverse references, composite keys
+ */
+
+drop dataverse test1 if exists;
+create dataverse test1;
+
+drop dataverse test2 if exists;
+create dataverse test2;
+
+use test2;
+
+create dataset customers(c_id int not unknown, c_name string) primary key c_id;
+
+create dataset stores(s_id1 int not unknown, s_id2 int not unknown, s_name string) primary key s_id1, s_id2;
+
+create dataset orders(o_id int not unknown, o_cid int, o_sidX int, o_sidY int, o_amount int) primary key o_id;
+
+create view customers_v(c_id int not unknown, c_name string) default null
+   primary key (c_id) not enforced
+   as customers;
+
+use test1;
+
+create view stores_v(s_id1 int not unknown, s_id2 int not unknown, s_name string) default null
+   primary key (s_id1, s_id2) not enforced
+   as test2.stores;
+
+create view orders_v(o_id int not unknown, o_cid int, o_sidX int, o_sidY int, o_amount int) default null
+  primary key (o_id) not enforced
+  foreign key (o_cid) references test2.customers_v not enforced
+  foreign key (o_sidX, o_sidY) references test1.stores_v not enforced
+  as test2.orders;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-7-foreign-key/create-view-7-foreign-key.4.query.sqlpp
similarity index 81%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-7-foreign-key/create-view-7-foreign-key.4.query.sqlpp
index 633af81..2f44238 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-7-foreign-key/create-view-7-foreign-key.4.query.sqlpp
@@ -17,10 +17,7 @@
  * under the License.
  */
 
---- Negative: cannot declare primary key
-
-drop dataverse test if exists;
-create dataverse test;
-
-create view test.v1 primary key (r) not enforced as
-  select r from range(1,2) r;
+select d.DataverseName, d.DatasetName, d.ViewDetails
+from Metadata.`Dataset` d
+where d.DataverseName like "test%" and d.DatasetType = "VIEW"
+order by d.DataverseName, d.DatasetName;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
index 41a64fc..87c26cb 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
@@ -34,4 +34,15 @@ create type test1.t4 as closed { r:int64? };
 
 create view test1.v4(t4) default null as v2;
 
+create view test1.v5(r bigint not unknown)
+  default null
+  primary key (r) not enforced
+  as v3;
+
+create view test1.v6(r1 bigint not unknown, r2 bigint)
+  default null
+  primary key (r1) not enforced
+  foreign key (r2) references v5 not enforced
+  as select r r1, r+1 r2 from range(0,2) r;
+
 drop dataverse test1;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.2.query.sqlpp
similarity index 81%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.2.query.sqlpp
index 633af81..b707271 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.2.query.sqlpp
@@ -17,10 +17,6 @@
  * under the License.
  */
 
---- Negative: cannot declare primary key
-
-drop dataverse test if exists;
-create dataverse test;
-
-create view test.v1 primary key (r) not enforced as
-  select r from range(1,2) r;
+select count(*) cnt
+from Metadata.`Dataverse` d
+where d.DataverseName like "test%";
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-2-negative/drop-dataverse-2-negative.7.ddl.sqlpp
similarity index 52%
copy from asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
copy to asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-2-negative/drop-dataverse-2-negative.7.ddl.sqlpp
index 41a64fc..0456ab7 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-1/drop-dataverse-1.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/drop-dataverse-2-negative/drop-dataverse-2-negative.7.ddl.sqlpp
@@ -17,21 +17,31 @@
  * under the License.
  */
 
---- test drop dataverse with views
+--- Test that DROP DATAVERSE fails due to cross-dataverse dependencies
+
+--- View refers to another view view foreign key declaration
 
 drop dataverse test1 if exists;
 create dataverse test1;
 
-create view test1.v1 as
-  select r from range(0,2) r;
+drop dataverse test2 if exists;
+create dataverse test2;
 
-create view test1.v2 as
-  select v1.* from v1;
+create dataset test2.employee_2(e_id int not unknown, e_name string, e_mgr_id int, e_hrr_id int)
+  primary key e_id;
 
-create view test1.v3(r bigint) default null as v2;
+create view test2.employee_v2(e_id int not unknown, e_mgr_id int)
+  default null
+  primary key (e_id) not enforced
+  as employee_2;
 
-create type test1.t4 as closed { r:int64? };
+create dataset test1.employee_1(e_id int not unknown, e_name string, e_mgr_id int, e_hrr_id int)
+  primary key e_id;
 
-create view test1.v4(t4) default null as v2;
+create view test1.employee_v1(e_id int not unknown, e_mgr_id int, e_hrr_id int)
+  default null
+  primary key (e_id) not enforced
+  foreign key (e_mgr_id) references test2.employee_v2 not enforced
+  as employee_1;
 
-drop dataverse test1;
+drop dataverse test2;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-7-foreign-key/create-view-7-foreign-key.2.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-7-foreign-key/create-view-7-foreign-key.2.adm
new file mode 100644
index 0000000..3af588c
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-7-foreign-key/create-view-7-foreign-key.2.adm
@@ -0,0 +1,3 @@
+{ "DataverseName": "test1", "DatasetName": "employee_v1", "ViewDetails": { "Definition": "employee", "Dependencies": [ [ [ "test1", "employee" ] ], [  ], [  ] ], "Default": null, "PrimaryKey": [ [ "e_id" ] ], "PrimaryKeyEnforced": false, "ForeignKeys": [ { "ForeignKey": [ [ "e_mgr_id" ] ], "RefDataverseName": "test1", "RefDatasetName": "employee_v1", "IsEnforced": false } ] } }
+{ "DataverseName": "test1", "DatasetName": "employee_v2", "ViewDetails": { "Definition": "employee", "Dependencies": [ [ [ "test1", "employee" ] ], [  ], [  ] ], "Default": null, "PrimaryKey": [ [ "e_id" ] ], "PrimaryKeyEnforced": false, "ForeignKeys": [ { "ForeignKey": [ [ "e_mgr_id" ] ], "RefDataverseName": "test1", "RefDatasetName": "employee_v2", "IsEnforced": false }, { "ForeignKey": [ [ "e_hrr_id" ] ], "RefDataverseName": "test1", "RefDatasetName": "employee_v2", "IsEnforced": fals [...]
+{ "DataverseName": "test2", "DatasetName": "employee2_v1", "ViewDetails": { "Definition": "employee2", "Dependencies": [ [ [ "test2", "employee2" ] ], [  ], [  ] ], "Default": null, "PrimaryKey": [ [ "e_id1" ], [ "e_id2" ] ], "PrimaryKeyEnforced": false, "ForeignKeys": [ { "ForeignKey": [ [ "e_mgr_id1" ], [ "e_mgr_id2" ] ], "RefDataverseName": "test2", "RefDatasetName": "employee2_v1", "IsEnforced": false }, { "ForeignKey": [ [ "e_hrr_id1" ], [ "e_hrr_id2" ] ], "RefDataverseName": "test2 [...]
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-7-foreign-key/create-view-7-foreign-key.4.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-7-foreign-key/create-view-7-foreign-key.4.adm
new file mode 100644
index 0000000..1391c82
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-7-foreign-key/create-view-7-foreign-key.4.adm
@@ -0,0 +1,3 @@
+{ "DataverseName": "test1", "DatasetName": "orders_v", "ViewDetails": { "Definition": "test2.orders", "Dependencies": [ [ [ "test2", "orders" ], [ "test2", "customers_v" ], [ "test1", "stores_v" ] ], [  ], [  ] ], "Default": null, "PrimaryKey": [ [ "o_id" ] ], "PrimaryKeyEnforced": false, "ForeignKeys": [ { "ForeignKey": [ [ "o_cid" ] ], "RefDataverseName": "test2", "RefDatasetName": "customers_v", "IsEnforced": false }, { "ForeignKey": [ [ "o_sidX" ], [ "o_sidY" ] ], "RefDataverseName": [...]
+{ "DataverseName": "test1", "DatasetName": "stores_v", "ViewDetails": { "Definition": "test2.stores", "Dependencies": [ [ [ "test2", "stores" ] ], [  ], [  ] ], "Default": null, "PrimaryKey": [ [ "s_id1" ], [ "s_id2" ] ], "PrimaryKeyEnforced": false } }
+{ "DataverseName": "test2", "DatasetName": "customers_v", "ViewDetails": { "Definition": "customers", "Dependencies": [ [ [ "test2", "customers" ] ], [  ], [  ] ], "Default": null, "PrimaryKey": [ [ "c_id" ] ], "PrimaryKeyEnforced": false } }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/view/drop-dataverse-1/drop-dataverse-1.1.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/drop-dataverse-1/drop-dataverse-1.1.adm
new file mode 100644
index 0000000..bacb60c
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/drop-dataverse-1/drop-dataverse-1.1.adm
@@ -0,0 +1 @@
+{ "cnt": 0 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
index d32fafe..a76e768 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
@@ -13178,6 +13178,7 @@
         <expected-error>ASX1149: Illegal function or view recursion (in line 32, at column 1)</expected-error>
         <expected-error>ASX1149: Illegal function or view recursion (in line 33, at column 1)</expected-error>
         <expected-error><![CDATA[ASX1001: Syntax error: In line 25 >>create view test.v1 primary key (r) not enforced as<< Encountered "primary" at column 21]]></expected-error>
+        <expected-error><![CDATA[ASX1001: Syntax error: In line 28 >>create view test.v2 foreign key (r) references v1 not enforced as<< Encountered <IDENTIFIER> "foreign" at column 21]]></expected-error>
       </compilation-unit>
     </test-case>
     <test-case FilePath="view">
@@ -13231,21 +13232,38 @@
         <expected-error>ASX1079: Compilation error: view type cannot have open fields (in line 29, at column 1)</expected-error>
         <expected-error>ASX1004: Unsupported type: view cannot process input type t1_a (in line 30, at column 1)</expected-error>
         <expected-error><![CDATA[ASX1001: Syntax error: In line 25 >>create view test.v1(r bigint, a [bigint]) default null as<< Encountered "[" at column 33]]></expected-error>
-        <expected-error>ASX1001: Syntax error: ASX1092: Parameter date_illegal_property_name cannot be set (in line 25, at column 1)</expected-error>
+        <expected-error>ASX1092: Parameter date_illegal_property_name cannot be set (in line 25, at column 1)</expected-error>
         <expected-error><![CDATA[ASX1001: Syntax error: In line 25 >>create view test.v1(r bigint) as<< Encountered "as" at column 31]]></expected-error>
         <expected-error><![CDATA[ASX1014: Field "unknown_field" is not found (in line 25, at column 1)]]></expected-error>
         <expected-error><![CDATA[ASX1014: Field "unknown_field_2" is not found (in line 25, at column 1)]]></expected-error>
         <expected-error><![CDATA[ASX1001: Syntax error: In line 28 >>  as select r from range(1,2) r;<< Encountered "as" at column 3]]></expected-error>
         <expected-error><![CDATA[ASX1021: The primary key field "r" cannot be nullable (in line 25, at column 1)]]></expected-error>
         <expected-error><![CDATA[ASX1021: The primary key field "r2" cannot be nullable (in line 25, at column 1)]]></expected-error>
-        <expected-error><![CDATA[ASX1001: Syntax error: ASX1162: Invalid primary key definition (in line 25, at column 1)]]></expected-error>
-        <expected-error><![CDATA[ASX1001: Syntax error: ASX1162: Invalid primary key definition (in line 26, at column 1)]]></expected-error>
+        <expected-error><![CDATA[ASX1162: Invalid primary key definition (in line 25, at column 1)]]></expected-error>
+        <expected-error><![CDATA[ASX1162: Invalid primary key definition (in line 26, at column 1)]]></expected-error>
+        <expected-error><![CDATA[ASX1001: Syntax error: In line 36 >>  as employee;<< Encountered "as" at column 3]]></expected-error>
+        <expected-error><![CDATA[ASX1165: Invalid foreign key definition: view test1.employee_v1 does not have a primary key (in line 32, at column 1)]]></expected-error>
+        <expected-error><![CDATA[ASX1166: Invalid foreign key definition: foreign key does not match primary key of view test1.employee_v1 (in line 34, at column 1)]]></expected-error>
+        <expected-error><![CDATA[ASX1166: Invalid foreign key definition: foreign key does not match primary key of view test1.employee_v2 (in line 34, at column 1)]]></expected-error>
+        <expected-error><![CDATA[ASX1164: Invalid foreign key definition (in line 34, at column 1)]]></expected-error>
+        <expected-error><![CDATA[ASX1063: Cannot find dataverse with name test3 (in line 42, at column 1)]]></expected-error>
+        <expected-error><![CDATA[ASX1159: Cannot find view with name test1.employee_v3 (in line 42, at column 1)]]></expected-error>
+        <expected-error><![CDATA[ASX1159: Cannot find view with name test1.employee (in line 43, at column 1)]]></expected-error>
+        <expected-error><![CDATA[ASX1164: Invalid foreign key definition (in line 43, at column 1)]]></expected-error>
+        <expected-error><![CDATA[ASX1164: Invalid foreign key definition (in line 39, at column 1)]]></expected-error>
+        <expected-error><![CDATA[ASX1166: Invalid foreign key definition: foreign key does not match primary key of view test1.employee_v1 (in line 39, at column 1)]]></expected-error>
+        <expected-error><![CDATA[ASX1167: Cannot change primary key of view test1.employee_v1 (in line 38, at column 1)]]></expected-error>
         <source-location>false</source-location>
       </compilation-unit>
     </test-case>
     <test-case FilePath="view">
+      <compilation-unit name="create-view-7-foreign-key">
+        <output-dir compare="Text">create-view-7-foreign-key</output-dir>
+      </compilation-unit>
+    </test-case>
+    <test-case FilePath="view">
       <compilation-unit name="drop-dataverse-1">
-        <output-dir compare="Text">none</output-dir>
+        <output-dir compare="Text">drop-dataverse-1</output-dir>
       </compilation-unit>
     </test-case>
     <test-case FilePath="view">
@@ -13257,6 +13275,7 @@
         <expected-error>ASX1147: Cannot drop dataverse: function test2.f2() being used by view test1.v1</expected-error>
         <expected-error>ASX1147: Cannot drop dataverse: synonym test2.s3 being used by view test1.v1</expected-error>
         <expected-error>ASX1147: Cannot drop dataverse: type test2.t1 being used by dataset test1.v1</expected-error>
+        <expected-error>ASX1147: Cannot drop dataverse: dataset (or view) test2.employee_v2 being used by view test1.employee_v1</expected-error>
         <source-location>false</source-location>
       </compilation-unit>
     </test-case>
diff --git a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
index 893fe49..1f96cc8 100644
--- a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
+++ b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
@@ -105,10 +105,10 @@ public enum ErrorCode implements IError {
     COMPILATION_INDEX_TYPE_NOT_SUPPORTED_FOR_DATASET_TYPE(1016),
     COMPILATION_FILTER_CANNOT_BE_NULLABLE(1017),
     COMPILATION_ILLEGAL_FILTER_TYPE(1018),
-    COMPILATION_CANNOT_AUTOGENERATE_COMPOSITE_PRIMARY_KEY(1019),
+    COMPILATION_CANNOT_AUTOGENERATE_COMPOSITE_KEY(1019),
     COMPILATION_ILLEGAL_AUTOGENERATED_TYPE(1020),
-    COMPILATION_PRIMARY_KEY_CANNOT_BE_NULLABLE(1021),
-    COMPILATION_ILLEGAL_PRIMARY_KEY_TYPE(1022),
+    COMPILATION_KEY_CANNOT_BE_NULLABLE(1021),
+    COMPILATION_ILLEGAL_KEY_TYPE(1022),
     COMPILATION_CANT_DROP_ACTIVE_DATASET(1023),
     COMPILATION_FUNC_EXPRESSION_CANNOT_UTILIZE_INDEX(1026),
     COMPILATION_DATASET_TYPE_DOES_NOT_HAVE_PRIMARY_INDEX(1027),
@@ -248,6 +248,10 @@ public enum ErrorCode implements IError {
     UNSUPPORTED_TYPE_FOR_PARQUET(1161),
     INVALID_PRIMARY_KEY_DEFINITION(1162),
     UNSUPPORTED_AUTH_METHOD(1163),
+    INVALID_FOREIGN_KEY_DEFINITION(1164),
+    INVALID_FOREIGN_KEY_DEFINITION_REF_PK_NOT_FOUND(1165),
+    INVALID_FOREIGN_KEY_DEFINITION_REF_PK_MISMATCH(1166),
+    CANNOT_CHANGE_PRIMARY_KEY(1167),
 
     // Feed errors
     DATAFLOW_ILLEGAL_STATE(3001),
diff --git a/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties b/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
index 6f673de..f40fdcf 100644
--- a/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
+++ b/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
@@ -106,10 +106,10 @@
 1016 = Index of type %1$s is not supported for dataset of type %2$s
 1017 = The filter field \"%1$s\" cannot be an optional field
 1018 = Field of type %1$s cannot be used as a filter field
-1019 = Cannot autogenerate a composite primary key
-1020 = Cannot autogenerate a primary key for primary key of type %1$s. Autogenerated primary keys must be of type %2$s
-1021 = The primary key field \"%1$s\" cannot be nullable
-1022 = Field of type %1$s cannot be used as a primary key field
+1019 = Cannot autogenerate a composite %1$s key
+1020 = Cannot autogenerate a %1$s key for %1$s key of type %2$s. Autogenerated %1$s keys must be of type %3$s
+1021 = The %1$s key field \"%2$s\" cannot be nullable
+1022 = Field of type %1$s cannot be used as a %2$s key field
 1023 = Cannot drop dataset %1$s since it is connected to active entity: %2$s
 #1024 is no longer used
 #1025 is no longer used
@@ -250,6 +250,10 @@
 1161 = Type '%1$s' contains declared fields, which is not supported for 'parquet' format
 1162 = Invalid primary key definition
 1163 = Authenticating with '%1$s' is not supported for '%2$s' format
+1164 = Invalid foreign key definition
+1165 = Invalid foreign key definition: %1$s %2$s does not have a primary key
+1166 = Invalid foreign key definition: foreign key does not match primary key of %1$s %2$s
+1167 = Cannot change primary key of %1$s %2$s
 
 # Feed Errors
 3001 = Illegal state.
diff --git a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/CreateViewStatement.java b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/CreateViewStatement.java
index 74586eb..7e4a0ef 100644
--- a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/CreateViewStatement.java
+++ b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/statement/CreateViewStatement.java
@@ -29,9 +29,8 @@ import org.apache.asterix.lang.common.base.AbstractStatement;
 import org.apache.asterix.lang.common.base.Expression;
 import org.apache.asterix.lang.common.base.Statement;
 import org.apache.asterix.lang.common.expression.TypeExpression;
-import org.apache.asterix.lang.common.util.ViewUtil;
+import org.apache.asterix.lang.common.struct.Identifier;
 import org.apache.asterix.lang.common.visitor.base.ILangVisitor;
-import org.apache.hyracks.algebricks.common.utils.Pair;
 
 public final class CreateViewStatement extends AbstractStatement {
 
@@ -47,7 +46,9 @@ public final class CreateViewStatement extends AbstractStatement {
 
     private final Map<String, String> viewConfig;
 
-    private final List<String> primaryKeyFields;
+    private final KeyDecl primaryKeyDecl;
+
+    private final List<ForeignKeyDecl> foreignKeyDecls;
 
     private final Boolean defaultNull;
 
@@ -56,18 +57,17 @@ public final class CreateViewStatement extends AbstractStatement {
     private final boolean ifNotExists;
 
     public CreateViewStatement(DataverseName dataverseName, String viewName, TypeExpression itemType, String viewBody,
-            Expression viewBodyExpression, Boolean defaultNull, Map<String, String> viewConfig,
-            Pair<List<Integer>, List<List<String>>> primaryKeyFields, boolean replaceIfExists, boolean ifNotExists)
-            throws CompilationException {
+            Expression viewBodyExpression, Boolean defaultNull, Map<String, String> viewConfig, KeyDecl primaryKeyDecl,
+            List<ForeignKeyDecl> foreignKeyDecls, boolean replaceIfExists, boolean ifNotExists) {
         this.dataverseName = dataverseName;
         this.viewName = Objects.requireNonNull(viewName);
         this.itemType = itemType;
-        boolean hasItemType = itemType != null;
         this.viewBody = Objects.requireNonNull(viewBody);
         this.viewBodyExpression = Objects.requireNonNull(viewBodyExpression);
         this.defaultNull = defaultNull;
-        this.viewConfig = ViewUtil.validateViewConfiguration(viewConfig, hasItemType);
-        this.primaryKeyFields = ViewUtil.validateViewPrimaryKey(primaryKeyFields, hasItemType);
+        this.viewConfig = viewConfig;
+        this.primaryKeyDecl = primaryKeyDecl;
+        this.foreignKeyDecls = foreignKeyDecls;
         this.replaceIfExists = replaceIfExists;
         this.ifNotExists = ifNotExists;
     }
@@ -120,24 +120,62 @@ public final class CreateViewStatement extends AbstractStatement {
         return defaultNull;
     }
 
-    public List<String> getPrimaryKeyFields() {
-        return primaryKeyFields;
-    }
-
-    public String getDatetimeFormat() {
-        return viewConfig.get(ViewUtil.DATETIME_PARAMETER_NAME);
+    public KeyDecl getPrimaryKeyDecl() {
+        return primaryKeyDecl;
     }
 
-    public String getDateFormat() {
-        return viewConfig.get(ViewUtil.DATE_PARAMETER_NAME);
+    public List<ForeignKeyDecl> getForeignKeyDecls() {
+        return foreignKeyDecls;
     }
 
-    public String getTimeFormat() {
-        return viewConfig.get(ViewUtil.TIME_PARAMETER_NAME);
+    public Map<String, String> getViewConfiguration() {
+        return viewConfig;
     }
 
     @Override
     public <R, T> R accept(ILangVisitor<R, T> visitor, T arg) throws CompilationException {
         return visitor.visit(this, arg);
     }
+
+    public static class KeyDecl {
+
+        protected final List<List<String>> fields;
+
+        protected final List<Integer> sourceIndicators;
+
+        public KeyDecl(List<List<String>> fields, List<Integer> sourceIndicators) {
+            this.fields = fields;
+            this.sourceIndicators = sourceIndicators;
+        }
+
+        public List<List<String>> getFields() {
+            return fields;
+        }
+
+        public List<Integer> getSourceIndicators() {
+            return sourceIndicators;
+        }
+    }
+
+    public static class ForeignKeyDecl extends KeyDecl {
+
+        private final DataverseName referencedDataverseName;
+
+        private final Identifier referencedDatasetName;
+
+        public ForeignKeyDecl(List<List<String>> fields, List<Integer> sourceIndicators,
+                DataverseName referencedDataverseName, Identifier referencedDatasetName) {
+            super(fields, sourceIndicators);
+            this.referencedDataverseName = referencedDataverseName;
+            this.referencedDatasetName = referencedDatasetName;
+        }
+
+        public DataverseName getReferencedDataverseName() {
+            return referencedDataverseName;
+        }
+
+        public Identifier getReferencedDatasetName() {
+            return referencedDatasetName;
+        }
+    }
 }
diff --git a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/util/ViewUtil.java b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/util/ViewUtil.java
index 7b87ffe..7da44ee 100644
--- a/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/util/ViewUtil.java
+++ b/asterixdb/asterix-lang-common/src/main/java/org/apache/asterix/lang/common/util/ViewUtil.java
@@ -43,7 +43,6 @@ import org.apache.asterix.lang.common.literal.StringLiteral;
 import org.apache.asterix.lang.common.statement.ViewDecl;
 import org.apache.asterix.lang.common.struct.Identifier;
 import org.apache.asterix.lang.common.struct.VarIdentifier;
-import org.apache.asterix.metadata.entities.Index;
 import org.apache.asterix.metadata.entities.ViewDetails;
 import org.apache.asterix.om.functions.BuiltinFunctions;
 import org.apache.asterix.om.types.ARecordType;
@@ -51,7 +50,6 @@ import org.apache.asterix.om.types.ATypeTag;
 import org.apache.asterix.om.types.AUnionType;
 import org.apache.asterix.om.types.BuiltinType;
 import org.apache.asterix.om.types.IAType;
-import org.apache.hyracks.algebricks.common.utils.Pair;
 import org.apache.hyracks.algebricks.common.utils.Triple;
 import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
 import org.apache.hyracks.api.exceptions.IWarningCollector;
@@ -84,7 +82,7 @@ public final class ViewUtil {
     }
 
     public static List<List<Triple<DataverseName, String, String>>> getViewDependencies(ViewDecl viewDecl,
-            IQueryRewriter rewriter) throws CompilationException {
+            List<ViewDetails.ForeignKey> foreignKeys, IQueryRewriter rewriter) throws CompilationException {
         Expression normBody = viewDecl.getNormalizedViewBody();
         if (normBody == null) {
             throw new CompilationException(ErrorCode.COMPILATION_ILLEGAL_STATE, viewDecl.getSourceLocation(),
@@ -98,11 +96,33 @@ public final class ViewUtil {
         ExpressionUtils.collectDependencies(normBody, rewriter, datasetDependencies, synonymDependencies,
                 functionDependencies);
 
+        if (foreignKeys != null) {
+            DatasetFullyQualifiedName viewName = viewDecl.getViewName();
+            for (ViewDetails.ForeignKey foreignKey : foreignKeys) {
+                DatasetFullyQualifiedName refName = foreignKey.getReferencedDatasetName();
+                boolean isSelfReference = refName.equals(viewName);
+                if (isSelfReference || containsDependency(datasetDependencies, refName)) {
+                    continue;
+                }
+                datasetDependencies.add(new Triple<>(refName.getDataverseName(), refName.getDatasetName(), null));
+            }
+        }
+
         List<Triple<DataverseName, String, String>> typeDependencies = Collections.emptyList();
         return ViewDetails.createDependencies(datasetDependencies, functionDependencies, typeDependencies,
                 synonymDependencies);
     }
 
+    private static boolean containsDependency(List<Triple<DataverseName, String, String>> inList,
+            DatasetFullyQualifiedName searchName) {
+        for (Triple<DataverseName, String, String> d : inList) {
+            if (d.first.equals(searchName.getDataverseName()) && d.second.equals(searchName.getDatasetName())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     public static void validateViewItemType(ARecordType recordType, SourceLocation sourceLoc)
             throws CompilationException {
         if (recordType.isOpen()) {
@@ -130,55 +150,26 @@ public final class ViewUtil {
         }
     }
 
-    public static Map<String, String> validateViewConfiguration(Map<String, String> viewConfig, boolean hasItemType)
-            throws CompilationException {
+    public static Map<String, String> validateViewConfiguration(Map<String, String> viewConfig,
+            SourceLocation sourceLoc) throws CompilationException {
         if (viewConfig == null) {
-            viewConfig = Collections.emptyMap();
+            return Collections.emptyMap();
         }
-        if (hasItemType) {
-            for (Map.Entry<String, String> me : viewConfig.entrySet()) {
-                String name = me.getKey();
-                String value = me.getValue();
-                if (DATETIME_PARAMETER_NAME.equals(name) || DATE_PARAMETER_NAME.equals(name)
-                        || TIME_PARAMETER_NAME.equals(name)) {
-                    if (value == null) {
-                        throw new CompilationException(ErrorCode.INVALID_REQ_PARAM_VAL, name, value);
-                    }
-                } else {
-                    throw new CompilationException(ErrorCode.ILLEGAL_SET_PARAMETER, name);
+        for (Map.Entry<String, String> me : viewConfig.entrySet()) {
+            String name = me.getKey();
+            String value = me.getValue();
+            if (DATETIME_PARAMETER_NAME.equals(name) || DATE_PARAMETER_NAME.equals(name)
+                    || TIME_PARAMETER_NAME.equals(name)) {
+                if (value == null) {
+                    throw new CompilationException(ErrorCode.INVALID_REQ_PARAM_VAL, sourceLoc, name, value);
                 }
+            } else {
+                throw new CompilationException(ErrorCode.ILLEGAL_SET_PARAMETER, sourceLoc, name);
             }
-        } else if (!viewConfig.isEmpty()) {
-            throw new CompilationException(ErrorCode.ILLEGAL_SET_PARAMETER, viewConfig.keySet().iterator().next());
         }
         return viewConfig;
     }
 
-    public static List<String> validateViewPrimaryKey(Pair<List<Integer>, List<List<String>>> primaryKeyFieldsPair,
-            boolean hasItemType) throws CompilationException {
-        if (primaryKeyFieldsPair == null || primaryKeyFieldsPair.second.isEmpty()) {
-            return null;
-        }
-        if (!hasItemType) {
-            throw new CompilationException(ErrorCode.INVALID_PRIMARY_KEY_DEFINITION);
-        }
-        List<Integer> sourceIndicators = primaryKeyFieldsPair.first;
-        List<List<String>> primaryKeyFields = primaryKeyFieldsPair.second;
-        int n = primaryKeyFields.size();
-        List<String> resultFields = new ArrayList<>(n);
-        for (int i = 0; i < n; i++) {
-            if (sourceIndicators.get(i) != Index.RECORD_INDICATOR) {
-                throw new CompilationException(ErrorCode.INVALID_PRIMARY_KEY_DEFINITION);
-            }
-            List<String> nestedField = primaryKeyFields.get(i);
-            if (nestedField.size() != 1) {
-                throw new CompilationException(ErrorCode.INVALID_PRIMARY_KEY_DEFINITION);
-            }
-            resultFields.add(nestedField.get(0));
-        }
-        return resultFields;
-    }
-
     public static Expression createTypeConvertExpression(Expression inExpr, IAType targetType,
             Triple<String, String, String> temporalDataFormat, DatasetFullyQualifiedName viewName,
             SourceLocation sourceLoc) throws CompilationException {
@@ -296,4 +287,16 @@ public final class ViewUtil {
                 return null;
         }
     }
+
+    public static String getDatetimeFormat(Map<String, String> viewConfig) {
+        return viewConfig.get(DATETIME_PARAMETER_NAME);
+    }
+
+    public static String getDateFormat(Map<String, String> viewConfig) {
+        return viewConfig.get(DATE_PARAMETER_NAME);
+    }
+
+    public static String getTimeFormat(Map<String, String> viewConfig) {
+        return viewConfig.get(TIME_PARAMETER_NAME);
+    }
 }
diff --git a/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj b/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
index 735cae5..502d3ae 100644
--- a/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
+++ b/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
@@ -230,6 +230,7 @@ class SQLPPParser extends ScopeChecker implements IParser {
     private static final String INCLUDE = "INCLUDE";
     private static final String FIRST = "FIRST";
     private static final String FOLLOWING = "FOLLOWING";
+    private static final String FOREIGN = "FOREIGN";
     private static final String GROUPING = "GROUPING";
     private static final String GROUPS = "GROUPS";
     private static final String IGNORE = "IGNORE";
@@ -241,6 +242,7 @@ class SQLPPParser extends ScopeChecker implements IParser {
     private static final String PARTITION = "PARTITION";
     private static final String PRECEDING = "PRECEDING";
     private static final String RANGE = "RANGE";
+    private static final String REFERENCES = "REFERENCES";
     private static final String RESPECT = "RESPECT";
     private static final String ROLLUP = "ROLLUP";
     private static final String ROW = "ROW";
@@ -1438,7 +1440,11 @@ CreateViewStatement ViewSpecification(Token startStmtToken, boolean orReplace) t
   Expression viewBodyExpr = null;
   Boolean defaultNull = null;
   Map<String, String> viewConfig = null;
+  String propertyName = null, propertyValue = null;
   Pair<List<Integer>, List<List<String>>> primaryKeyFields = null;
+  Pair<List<Integer>, List<List<String>>> foreignKeyFields = null;
+  Pair<DataverseName, Identifier> refNameComponents = null;
+  List<CreateViewStatement.ForeignKeyDecl> foreignKeyDecls = null;
   DataverseName currentDataverse = defaultDataverse;
 }
 {
@@ -1448,8 +1454,31 @@ CreateViewStatement ViewSpecification(Token startStmtToken, boolean orReplace) t
         typeExpr = DatasetTypeSpecification()
         ifNotExists = IfNotExists()
         <IDENTIFIER> { expectToken(DEFAULT); } <NULL> { defaultNull = true; }
-        viewConfig = ViewConfiguration()
-        ( <PRIMARY> <KEY> <LEFTPAREN> primaryKeyFields = PrimaryKeyFields() <RIGHTPAREN> <NOT> <ENFORCED> )?
+        (
+          LOOKAHEAD(2) <IDENTIFIER> { propertyName = token.image.toLowerCase(); } propertyValue = StringLiteral()
+          {
+            if (viewConfig == null) {
+              viewConfig = new HashMap<String, String>();
+            }
+            viewConfig.put(propertyName, propertyValue);
+          }
+        )*
+        (
+          <PRIMARY> <KEY> <LEFTPAREN> primaryKeyFields = PrimaryKeyFields() <RIGHTPAREN>
+          <NOT> <ENFORCED>
+        )?
+        (
+          <IDENTIFIER> { expectToken(FOREIGN); } <KEY> <LEFTPAREN> foreignKeyFields = PrimaryKeyFields() <RIGHTPAREN>
+          <IDENTIFIER> { expectToken(REFERENCES); } refNameComponents = QualifiedName()
+          <NOT> <ENFORCED>
+          {
+            if (foreignKeyDecls == null) {
+              foreignKeyDecls = new ArrayList<CreateViewStatement.ForeignKeyDecl>();
+            }
+            foreignKeyDecls.add(new CreateViewStatement.ForeignKeyDecl(foreignKeyFields.second,
+              foreignKeyFields.first, refNameComponents.first, refNameComponents.second));
+          }
+        )*
       )
       |
       ( ifNotExists = IfNotExists() )
@@ -1474,13 +1503,12 @@ CreateViewStatement ViewSpecification(Token startStmtToken, boolean orReplace) t
       endPos.endColumn + 1);
     removeCurrentScope();
     defaultDataverse = currentDataverse;
-    try {
-      CreateViewStatement stmt = new CreateViewStatement(nameComponents.first, nameComponents.second.getValue(),
-        typeExpr, viewBody, viewBodyExpr, defaultNull, viewConfig, primaryKeyFields, orReplace, ifNotExists);
+    CreateViewStatement.KeyDecl primaryKeyDecl = primaryKeyFields != null ?
+      new CreateViewStatement.KeyDecl(primaryKeyFields.second, primaryKeyFields.first) : null;
+    CreateViewStatement stmt = new CreateViewStatement(nameComponents.first, nameComponents.second.getValue(),
+      typeExpr, viewBody, viewBodyExpr, defaultNull, viewConfig, primaryKeyDecl, foreignKeyDecls, orReplace,
+      ifNotExists);
     return addSourceLocation(stmt, startStmtToken);
-    } catch (CompilationException e) {
-       throw new SqlppParseException(getSourceLocation(startStmtToken), e.getMessage());
-    }
   }
 }
 
@@ -1498,27 +1526,6 @@ Expression ViewBody() throws ParseException:
   }
 }
 
-Map<String, String> ViewConfiguration() throws ParseException:
-{
-  Map<String, String> config = null;
-  String name = null, value = null;
-}
-{
-  (
-    <IDENTIFIER> { name = token.image.toLowerCase(); }
-    value = StringLiteral()
-    {
-      if (config == null) {
-        config = new LinkedHashMap<String, String>();
-      }
-      config.put(name, value);
-    }
-  )*
-  {
-    return config;
-  }
-}
-
 CreateFunctionStatement CreateFunctionStatement(Token startStmtToken, boolean orReplace) throws ParseException:
 {
   CreateFunctionStatement stmt = null;
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataRecordTypes.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataRecordTypes.java
index a7af77e..48d4fc5 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataRecordTypes.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/bootstrap/MetadataRecordTypes.java
@@ -64,12 +64,15 @@ public final class MetadataRecordTypes {
     public static final String FIELD_NAME_FILE_NUMBER = "FileNumber";
     public static final String FIELD_NAME_FILE_SIZE = "FileSize";
     public static final String FIELD_NAME_FILE_STRUCTURE = "FileStructure";
+    public static final String FIELD_NAME_FOREIGN_KEY = "ForeignKey";
+    public static final String FIELD_NAME_FOREIGN_KEYS = "ForeignKeys";
     public static final String FIELD_NAME_GROUP_NAME = "GroupName";
     public static final String FIELD_NAME_HINTS = "Hints";
     public static final String FIELD_NAME_INDEX_NAME = "IndexName";
     public static final String FIELD_NAME_INDEX_STRUCTURE = "IndexStructure";
     public static final String FIELD_NAME_INTERNAL_DETAILS = "InternalDetails";
     public static final String FIELD_NAME_IS_ANONYMOUS = "IsAnonymous";
+    public static final String FIELD_NAME_IS_ENFORCED = "IsEnforced";
     public static final String FIELD_NAME_IS_MISSABLE = "IsMissable";
     public static final String FIELD_NAME_IS_NULLABLE = "IsNullable";
     public static final String FIELD_NAME_IS_OPEN = "IsOpen";
@@ -98,6 +101,8 @@ public final class MetadataRecordTypes {
     public static final String FIELD_NAME_PRIMARY_KEY_ENFORCED = "PrimaryKeyEnforced";
     public static final String FIELD_NAME_PROPERTIES = "Properties";
     public static final String FIELD_NAME_RECORD = "Record";
+    public static final String FIELD_NAME_REF_DATAVERSE_NAME = "RefDataverseName";
+    public static final String FIELD_NAME_REF_DATASET_NAME = "RefDatasetName";
     public static final String FIELD_NAME_RETURN_TYPE = "ReturnType";
     public static final String FIELD_NAME_RETURN_TYPE_DATAVERSE_NAME = "ReturnTypeDataverseName";
     public static final String FIELD_NAME_SEARCH_KEY = "SearchKey";
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entities/ViewDetails.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entities/ViewDetails.java
index cd98f32..dc60b9e 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entities/ViewDetails.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entities/ViewDetails.java
@@ -22,6 +22,7 @@ package org.apache.asterix.metadata.entities;
 import static org.apache.asterix.om.types.AOrderedListType.FULL_OPEN_ORDEREDLIST_TYPE;
 
 import java.io.DataOutput;
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -31,6 +32,7 @@ import org.apache.asterix.builders.IARecordBuilder;
 import org.apache.asterix.builders.OrderedListBuilder;
 import org.apache.asterix.builders.RecordBuilder;
 import org.apache.asterix.common.config.DatasetConfig;
+import org.apache.asterix.common.metadata.DatasetFullyQualifiedName;
 import org.apache.asterix.common.metadata.DataverseName;
 import org.apache.asterix.formats.nontagged.SerializerDeserializerProvider;
 import org.apache.asterix.metadata.IDatasetDetails;
@@ -70,16 +72,19 @@ public class ViewDetails implements IDatasetDetails {
 
     private final List<String> primaryKeyFields;
 
+    private final List<ForeignKey> foreignKeys;
+
     public ViewDetails(String viewBody, List<List<Triple<DataverseName, String, String>>> dependencies,
-            Boolean defaultNull, List<String> primaryKeyFields, String datetimeFormat, String dateFormat,
-            String timeFormat) {
+            Boolean defaultNull, List<String> primaryKeyFields, List<ForeignKey> foreignKeys, String datetimeFormat,
+            String dateFormat, String timeFormat) {
         this.viewBody = Objects.requireNonNull(viewBody);
         this.dependencies = Objects.requireNonNull(dependencies);
         this.defaultNull = defaultNull;
+        this.primaryKeyFields = primaryKeyFields;
+        this.foreignKeys = foreignKeys;
         this.datetimeFormat = datetimeFormat;
         this.dateFormat = dateFormat;
         this.timeFormat = timeFormat;
-        this.primaryKeyFields = primaryKeyFields;
     }
 
     @Override
@@ -105,6 +110,10 @@ public class ViewDetails implements IDatasetDetails {
         return primaryKeyFields;
     }
 
+    public List<ForeignKey> getForeignKeys() {
+        return foreignKeys;
+    }
+
     public String getDatetimeFormat() {
         return datetimeFormat;
     }
@@ -196,23 +205,12 @@ public class ViewDetails implements IDatasetDetails {
             aString.setValue(MetadataRecordTypes.FIELD_NAME_PRIMARY_KEY);
             stringSerde.serialize(aString, fieldName.getDataOutput());
 
-            // write as list of lists to be consistent with how InternalDatasetDetails writes its primary key
-            OrderedListBuilder primaryKeyListBuilder = new OrderedListBuilder();
-            OrderedListBuilder listBuilder = new OrderedListBuilder();
-
-            primaryKeyListBuilder.reset(FULL_OPEN_ORDEREDLIST_TYPE);
-            for (String field : primaryKeyFields) {
-                listBuilder.reset(FULL_OPEN_ORDEREDLIST_TYPE);
-                itemValue.reset();
-                aString.setValue(field);
-                stringSerde.serialize(aString, itemValue.getDataOutput());
-                listBuilder.addItem(itemValue);
-                itemValue.reset();
-                listBuilder.write(itemValue.getDataOutput(), true);
-                primaryKeyListBuilder.addItem(itemValue);
-            }
+            // write value as list of lists to be consistent with how InternalDatasetDetails writes its primary key
             fieldValue.reset();
-            primaryKeyListBuilder.write(fieldValue.getDataOutput(), true);
+            OrderedListBuilder keyListBuilder = new OrderedListBuilder();
+            OrderedListBuilder fieldPathListBuilder = new OrderedListBuilder();
+            writeKeyFieldsList(primaryKeyFields, keyListBuilder, fieldPathListBuilder, aString, stringSerde, itemValue);
+            keyListBuilder.write(fieldValue.getDataOutput(), true);
             viewRecordBuilder.addField(fieldName, fieldValue);
 
             // write field 'PrimaryKeyEnforced'
@@ -224,6 +222,69 @@ public class ViewDetails implements IDatasetDetails {
             viewRecordBuilder.addField(fieldName, fieldValue);
         }
 
+        // write field 'ForeignKeys'
+        if (foreignKeys != null && !foreignKeys.isEmpty()) {
+            OrderedListBuilder foreignKeysListBuilder = new OrderedListBuilder();
+            foreignKeysListBuilder.reset(FULL_OPEN_ORDEREDLIST_TYPE);
+
+            IARecordBuilder foreignKeyRecordBuilder = new RecordBuilder();
+            OrderedListBuilder keyListBuilder = new OrderedListBuilder();
+            OrderedListBuilder fieldPathListBuilder = new OrderedListBuilder();
+
+            for (ViewDetails.ForeignKey foreignKey : foreignKeys) {
+                foreignKeyRecordBuilder.reset(RecordUtil.FULLY_OPEN_RECORD_TYPE);
+
+                // write field 'ForeignKey'
+                fieldName.reset();
+                aString.setValue(MetadataRecordTypes.FIELD_NAME_FOREIGN_KEY);
+                stringSerde.serialize(aString, fieldName.getDataOutput());
+                // write value as list of lists to be consistent with how InternalDatasetDetails writes its primary key
+                fieldValue.reset();
+                writeKeyFieldsList(foreignKey.getForeignKeyFields(), keyListBuilder, fieldPathListBuilder, aString,
+                        stringSerde, itemValue);
+                keyListBuilder.write(fieldValue.getDataOutput(), true);
+                foreignKeyRecordBuilder.addField(fieldName, fieldValue);
+
+                // write field 'RefDataverseName'
+                fieldName.reset();
+                aString.setValue(MetadataRecordTypes.FIELD_NAME_REF_DATAVERSE_NAME);
+                stringSerde.serialize(aString, fieldName.getDataOutput());
+                fieldValue.reset();
+                aString.setValue(foreignKey.getReferencedDatasetName().getDataverseName().getCanonicalForm());
+                stringSerde.serialize(aString, fieldValue.getDataOutput());
+                foreignKeyRecordBuilder.addField(fieldName, fieldValue);
+
+                // write field 'RefDatasetName'
+                fieldName.reset();
+                aString.setValue(MetadataRecordTypes.FIELD_NAME_REF_DATASET_NAME);
+                stringSerde.serialize(aString, fieldName.getDataOutput());
+                fieldValue.reset();
+                aString.setValue(foreignKey.getReferencedDatasetName().getDatasetName());
+                stringSerde.serialize(aString, fieldValue.getDataOutput());
+                foreignKeyRecordBuilder.addField(fieldName, fieldValue);
+
+                // write field 'IsEnforced'
+                fieldName.reset();
+                aString.setValue(MetadataRecordTypes.FIELD_NAME_IS_ENFORCED);
+                stringSerde.serialize(aString, fieldName.getDataOutput());
+                fieldValue.reset();
+                booleanSerde.serialize(ABoolean.FALSE, fieldValue.getDataOutput());
+                foreignKeyRecordBuilder.addField(fieldName, fieldValue);
+
+                fieldValue.reset();
+                foreignKeyRecordBuilder.write(fieldValue.getDataOutput(), true);
+                foreignKeysListBuilder.addItem(fieldValue);
+            }
+
+            fieldName.reset();
+            aString.setValue(MetadataRecordTypes.FIELD_NAME_FOREIGN_KEYS);
+            stringSerde.serialize(aString, fieldName.getDataOutput());
+            fieldValue.reset();
+            foreignKeysListBuilder.write(fieldValue.getDataOutput(), true);
+
+            viewRecordBuilder.addField(fieldName, fieldValue);
+        }
+
         // write field 'Format'
         if (datetimeFormat != null || dateFormat != null || timeFormat != null) {
             fieldName.reset();
@@ -250,6 +311,22 @@ public class ViewDetails implements IDatasetDetails {
         viewRecordBuilder.write(out, true);
     }
 
+    private void writeKeyFieldsList(List<String> keyFields, OrderedListBuilder keyListBuilder,
+            OrderedListBuilder fieldListBuilder, AMutableString aString, ISerializerDeserializer<AString> stringSerde,
+            ArrayBackedValueStorage itemValue) throws HyracksDataException {
+        keyListBuilder.reset(FULL_OPEN_ORDEREDLIST_TYPE);
+        for (String field : keyFields) {
+            fieldListBuilder.reset(FULL_OPEN_ORDEREDLIST_TYPE);
+            itemValue.reset();
+            aString.setValue(field);
+            stringSerde.serialize(aString, itemValue.getDataOutput());
+            fieldListBuilder.addItem(itemValue);
+            itemValue.reset();
+            fieldListBuilder.write(itemValue.getDataOutput(), true);
+            keyListBuilder.addItem(itemValue);
+        }
+    }
+
     public static List<List<Triple<DataverseName, String, String>>> createDependencies(
             List<Triple<DataverseName, String, String>> datasetDependencies,
             List<Triple<DataverseName, String, String>> functionDependencies,
@@ -264,4 +341,26 @@ public class ViewDetails implements IDatasetDetails {
         }
         return depList;
     }
+
+    public static final class ForeignKey implements Serializable {
+
+        private static final long serialVersionUID = 1L;
+
+        private final List<String> foreignKeyFields;
+
+        private final DatasetFullyQualifiedName referencedDatasetName;
+
+        public ForeignKey(List<String> foreignKeyFields, DatasetFullyQualifiedName referencedDatasetName) {
+            this.foreignKeyFields = Objects.requireNonNull(foreignKeyFields);
+            this.referencedDatasetName = Objects.requireNonNull(referencedDatasetName);
+        }
+
+        public List<String> getForeignKeyFields() {
+            return foreignKeyFields;
+        }
+
+        public DatasetFullyQualifiedName getReferencedDatasetName() {
+            return referencedDatasetName;
+        }
+    }
 }
diff --git a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entitytupletranslators/DatasetTupleTranslator.java b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entitytupletranslators/DatasetTupleTranslator.java
index 6caf649..cf4b714 100644
--- a/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entitytupletranslators/DatasetTupleTranslator.java
+++ b/asterixdb/asterix-metadata/src/main/java/org/apache/asterix/metadata/entitytupletranslators/DatasetTupleTranslator.java
@@ -37,6 +37,7 @@ import org.apache.asterix.common.config.DatasetConfig.DatasetType;
 import org.apache.asterix.common.config.DatasetConfig.TransactionState;
 import org.apache.asterix.common.exceptions.AsterixException;
 import org.apache.asterix.common.exceptions.ErrorCode;
+import org.apache.asterix.common.metadata.DatasetFullyQualifiedName;
 import org.apache.asterix.common.metadata.DataverseName;
 import org.apache.asterix.metadata.IDatasetDetails;
 import org.apache.asterix.metadata.bootstrap.MetadataPrimaryIndexes;
@@ -295,6 +296,52 @@ public class DatasetTupleTranslator extends AbstractTupleTranslator<Dataset> {
                     }
                 }
 
+                // Foreign Keys
+                List<ViewDetails.ForeignKey> foreignKeys = null;
+                int foreignKeysFieldPos =
+                        datasetDetailsRecord.getType().getFieldIndex(MetadataRecordTypes.FIELD_NAME_FOREIGN_KEYS);
+                if (foreignKeysFieldPos >= 0) {
+                    AOrderedList foreignKeyRecordsList =
+                            ((AOrderedList) datasetDetailsRecord.getValueByPos(foreignKeysFieldPos));
+                    int nForeignKeys = foreignKeyRecordsList.size();
+                    foreignKeys = new ArrayList<>(nForeignKeys);
+                    for (int i = 0; i < nForeignKeys; i++) {
+                        ARecord foreignKeyRecord = (ARecord) foreignKeyRecordsList.getItem(i);
+                        // 'ForeignKey'
+                        int foreignKeyFieldPos =
+                                foreignKeyRecord.getType().getFieldIndex(MetadataRecordTypes.FIELD_NAME_FOREIGN_KEY);
+                        AOrderedList foreignKeyFieldList =
+                                ((AOrderedList) foreignKeyRecord.getValueByPos(foreignKeyFieldPos));
+                        int nForeignKeyFields = foreignKeyFieldList.size();
+                        List<String> foreignKeyFields = new ArrayList<>(nForeignKeyFields);
+                        for (int j = 0; j < nForeignKeyFields; j++) {
+                            AOrderedList list = (AOrderedList) foreignKeyFieldList.getItem(j);
+                            if (list.size() != 1) {
+                                throw new AsterixException(ErrorCode.METADATA_ERROR, list.toJSON());
+                            }
+                            AString str = (AString) list.getItem(0);
+                            foreignKeyFields.add(str.getStringValue());
+                        }
+
+                        // 'RefDataverseName'
+                        int refDataverseNameFieldPos = foreignKeyRecord.getType()
+                                .getFieldIndex(MetadataRecordTypes.FIELD_NAME_REF_DATAVERSE_NAME);
+                        String refDataverseCanonicalName =
+                                ((AString) foreignKeyRecord.getValueByPos(refDataverseNameFieldPos)).getStringValue();
+                        DataverseName refDataverseName =
+                                DataverseName.createFromCanonicalForm(refDataverseCanonicalName);
+
+                        // 'RefDatasetName'
+                        int refDatasetNameFieldPos = foreignKeyRecord.getType()
+                                .getFieldIndex(MetadataRecordTypes.FIELD_NAME_REF_DATASET_NAME);
+                        String refDatasetName =
+                                ((AString) foreignKeyRecord.getValueByPos(refDatasetNameFieldPos)).getStringValue();
+
+                        foreignKeys.add(new ViewDetails.ForeignKey(foreignKeyFields,
+                                new DatasetFullyQualifiedName(refDataverseName, refDatasetName)));
+                    }
+                }
+
                 // Format fields
                 String datetimeFormat = null, dateFormat = null, timeFormat = null;
                 int formatFieldPos =
@@ -313,7 +360,7 @@ public class DatasetTupleTranslator extends AbstractTupleTranslator<Dataset> {
                     }
                 }
 
-                datasetDetails = new ViewDetails(definition, dependencies, defaultNull, primaryKeyFields,
+                datasetDetails = new ViewDetails(definition, dependencies, defaultNull, primaryKeyFields, foreignKeys,
                         datetimeFormat, dateFormat, timeFormat);
                 break;
             }