[ASTERIXDB-2954][COMP] Support PRIMARY KEY declaration in CREATE VIEW

- user model changes: no
- storage format changes: no
- interface changes: no

Details:
- Allow not-enforced primary key to be specified in
  CREATE VIEW for typed views
- Primary key fields must be declared as "not unknown"
  in the type definition
- If a type cast operation produces NULL for a primary key
  field then the whole tuple is excluded from the view
- Add testcases

Change-Id: I7ad08dcb0e1437c1e791daab4ee8eadd9c8135e1
Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/12963
Integration-Tests: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Tested-by: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Reviewed-by: Dmitry Lychagin <dmitry.lychagin@couchbase.com>
Reviewed-by: Ali Alsuliman <ali.al.solaiman@gmail.com>
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 bf6e3b5..abef74e 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
@@ -2499,6 +2499,7 @@
                 }
             }
 
+            List<String> primaryKeyFields = cvs.getPrimaryKeyFields();
             Datatype itemTypeEntity = null;
             boolean itemTypeIsInline = false;
             if (cvs.hasItemType()) {
@@ -2506,6 +2507,13 @@
                         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);
+                }
+            } else if (primaryKeyFields != null) {
+                throw new CompilationException(ErrorCode.INVALID_PRIMARY_KEY_DEFINITION, sourceLoc);
             }
 
             // Check whether the view is usable:
@@ -2523,10 +2531,8 @@
             List<List<Triple<DataverseName, String, String>>> dependencies =
                     ViewUtil.getViewDependencies(viewDecl, queryRewriter);
 
-            ViewDetails viewDetails = cvs.hasItemType()
-                    ? new ViewDetails(cvs.getViewBody(), dependencies, cvs.getDefaultNull(), cvs.getDatetimeFormat(),
-                            cvs.getDateFormat(), cvs.getTimeFormat())
-                    : new ViewDetails(cvs.getViewBody(), dependencies, null, null, null, null);
+            ViewDetails viewDetails = new ViewDetails(cvs.getViewBody(), dependencies, cvs.getDefaultNull(),
+                    primaryKeyFields, cvs.getDatetimeFormat(), cvs.getDateFormat(), cvs.getTimeFormat());
 
             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
new file mode 100644
index 0000000..633af81
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-2-negative/create-view-2-negative.12.ddl.sqlpp
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+--- 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;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.1.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.1.ddl.sqlpp
index 2e1f472..3576628 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.1.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.1.ddl.sqlpp
@@ -70,3 +70,55 @@
 ) default null
   date 'MM/DD/YYYY'
 as t2;
+
+/* primary key (not enforced) */
+
+create view v5_pk(
+  c_id int32 not unknown,
+  c_datetime datetime
+) default null
+  datetime 'MM/DD/YYYY hh:mm:ss.nnna'
+  primary key (c_id) not enforced
+as t2;
+
+/* primary key (not enforced), check that invalid tuples are eliminated */
+
+create view v6_pk_no_nulls(
+  c_i64 int64 not unknown,
+  c_id int32
+) default null
+  primary key (c_i64) not enforced
+as t1;
+
+/* no primary key, check that invalid tuples are eliminated if target field type is declared as not unknown */
+
+create view v7_no_nulls(
+  c_i64 int64 not unknown,
+  c_id int32
+) default null
+as t1;
+
+/* no primary key, check that invalid tuples are eliminated if target field type is declared as not unknown */
+
+create view v8_no_nulls_multi(
+  c_id int32,
+  c_x int64 not unknown,
+  c_y int64 not unknown
+) default null
+as
+  select
+    c_id,
+    case when to_bigint(c_i32) >= 0 then to_bigint(c_i32) when to_bigint(c_i32) < 0 then null else 0 end as c_x,
+    case when to_bigint(c_i64) >= 0 then null when to_bigint(c_i64) < 0 then to_bigint(c_i64) else 0 end as c_y
+  from t1;
+
+/* composite pk */
+
+create view v9_pk_composite(
+  c_id1 int32 not unknown,
+  c_id2 int32 not unknown
+) default null
+  primary key (c_id1, c_id2) not enforced
+as
+  select c_id as c_id1, -c_id as c_id2
+  from t1;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.10.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.10.query.sqlpp
new file mode 100644
index 0000000..62dddff
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.10.query.sqlpp
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+use test1;
+
+select c_id, c_x, c_y
+from v8_no_nulls_multi
+order by c_id;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.11.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.11.query.sqlpp
new file mode 100644
index 0000000..71bb255
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.11.query.sqlpp
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+use test1;
+
+select c_id1, c_id2
+from v9_pk_composite
+order by c_id1, c_id2;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.12.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.12.query.sqlpp
new file mode 100644
index 0000000..8c5d268
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.12.query.sqlpp
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+select DataverseName, DatasetName, ViewDetails
+from Metadata.`Dataset` d
+where DatasetType='VIEW'
+order by DataverseName, DatasetName;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.7.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.7.query.sqlpp
index 8c5d268..6513352 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.7.query.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.7.query.sqlpp
@@ -17,7 +17,8 @@
  * under the License.
  */
 
-select DataverseName, DatasetName, ViewDetails
-from Metadata.`Dataset` d
-where DatasetType='VIEW'
-order by DataverseName, DatasetName;
+use test1;
+
+select c_id, c_date
+from v5_pk
+order by c_id;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.8.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.8.query.sqlpp
new file mode 100644
index 0000000..35a7799
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.8.query.sqlpp
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+use test1;
+
+select c_id, c_i64
+from v6_pk_no_nulls
+order by c_id;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.9.query.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.9.query.sqlpp
new file mode 100644
index 0000000..6f14510
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-3-typed/create-view-3-typed.9.query.sqlpp
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+use test1;
+
+select c_id, c_i64
+from v7_no_nulls
+order by c_id;
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.10.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.10.ddl.sqlpp
new file mode 100644
index 0000000..f3c2076
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.10.ddl.sqlpp
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+--- Negative: primary key declaration requires "not enforced" modifier
+
+drop dataverse test if exists;
+create dataverse test;
+
+create view test.v1(r bigint)
+  default null
+  primary key (r)
+  as select r from range(1,2) r;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.11.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.11.ddl.sqlpp
new file mode 100644
index 0000000..fcb6727
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.11.ddl.sqlpp
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+--- Negative: primary key field must be declared as "not unknown"
+
+drop dataverse test if exists;
+create dataverse test;
+
+create view test.v1(r bigint)
+  default null
+  primary key (r) not enforced
+  as select r from range(1,2) r;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.12.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.12.ddl.sqlpp
new file mode 100644
index 0000000..343967a
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.12.ddl.sqlpp
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+--- Negative: primary key field must be declared as "not unknown"
+
+drop dataverse test if exists;
+create dataverse test;
+
+create view test.v1(r bigint not unknown, r2 bigint)
+  default null
+  primary key (r, r2) not enforced
+  as select r, -r as r2 from range(1,2) r;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.13.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.13.ddl.sqlpp
new file mode 100644
index 0000000..101ed8f
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.13.ddl.sqlpp
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+--- Negative: primary key declaration refers to a non-existent nested field
+
+drop dataverse test if exists;
+create dataverse test;
+
+create view test.v1(r bigint not unknown)
+  default null
+  primary key (r.unknown_field) not enforced
+  as select r from range(1,2) r;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.14.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.14.ddl.sqlpp
new file mode 100644
index 0000000..26a9668
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.14.ddl.sqlpp
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+--- Negative: primary key declaration uses
+---           meta() reference
+
+drop dataverse test if exists;
+create dataverse test;
+
+create view test.v1(r bigint not unknown)
+  default null
+  primary key (meta().unknown_field) not enforced
+  as select r from range(1,2) r;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.6.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.6.ddl.sqlpp
index dc344c3..2e01c03 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.6.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.6.ddl.sqlpp
@@ -17,14 +17,13 @@
  * under the License.
  */
 
---- Negative: view type has non-optional fields
+--- Negative: invalid view parameter clause
 
 drop dataverse test if exists;
 create dataverse test;
 
-create type test.t1 as closed {
-  r:int64
-};
-
-create view test.v1(t1) default null as
-  select r from range(1,2) r;
+create view test.v1(cd date) default null
+  date 'YYYY-MM-DD'
+  date_illegal_property_name 'YYYY-MM-DD'
+as
+  select string(current_date()) cd from range(1,2) r;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.7.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.7.ddl.sqlpp
index 2e01c03..adb53a8 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.7.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.7.ddl.sqlpp
@@ -17,13 +17,10 @@
  * under the License.
  */
 
---- Negative: invalid view parameter clause
+--- Negative: default null is required
 
 drop dataverse test if exists;
 create dataverse test;
 
-create view test.v1(cd date) default null
-  date 'YYYY-MM-DD'
-  date_illegal_property_name 'YYYY-MM-DD'
-as
-  select string(current_date()) cd from range(1,2) r;
+create view test.v1(r bigint) as
+  select r from range(1,2) r;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.8.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.8.ddl.sqlpp
index adb53a8..dd7e995 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.8.ddl.sqlpp
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.8.ddl.sqlpp
@@ -17,10 +17,12 @@
  * under the License.
  */
 
---- Negative: default null is required
+--- Negative: primary key declaration refers to a non-existent field
 
 drop dataverse test if exists;
 create dataverse test;
 
-create view test.v1(r bigint) as
-  select r from range(1,2) r;
+create view test.v1(r bigint)
+  default null
+  primary key (unknown_field) not enforced
+  as select r from range(1,2) r;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.9.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.9.ddl.sqlpp
new file mode 100644
index 0000000..8aba1c8
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/view/create-view-6-typed-negative/create-view-6-typed-negative.9.ddl.sqlpp
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+--- Negative: primary key declaration refers to a non-existent field
+
+drop dataverse test if exists;
+create dataverse test;
+
+create view test.v1(r bigint not unknown)
+  default null
+  primary key (r, unknown_field_2) not enforced
+  as select r from range(1,2) r;
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.10.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.10.adm
new file mode 100644
index 0000000..af0378d
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.10.adm
@@ -0,0 +1,3 @@
+{ "c_id": 2, "c_x": 0, "c_y": 0 }
+{ "c_id": 3, "c_x": 0, "c_y": 0 }
+{ "c_id": 4, "c_x": 0, "c_y": 0 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.11.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.11.adm
new file mode 100644
index 0000000..c32dff1
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.11.adm
@@ -0,0 +1,5 @@
+{ "c_id1": 0, "c_id2": 0 }
+{ "c_id1": 1, "c_id2": -1 }
+{ "c_id1": 2, "c_id2": -2 }
+{ "c_id1": 3, "c_id2": -3 }
+{ "c_id1": 4, "c_id2": -4 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.12.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.12.adm
new file mode 100644
index 0000000..e561054
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.12.adm
@@ -0,0 +1,9 @@
+{ "DataverseName": "test1", "DatasetName": "v1", "ViewDetails": { "Definition": "t1", "Dependencies": [ [ [ "test1", "t1" ] ], [  ], [  ] ], "Default": null } }
+{ "DataverseName": "test1", "DatasetName": "v2_ref_type", "ViewDetails": { "Definition": "select c_id,\n    c_i8, c_i16, c_i32, c_i64, c_f, c_d,\n    c_b, c_s,\n    c_datetime, c_date, c_time,\n    c_dur, c_ymdur, c_dtdur\n  from t1", "Dependencies": [ [ [ "test1", "t1" ] ], [  ], [  ] ], "Default": null } }
+{ "DataverseName": "test1", "DatasetName": "v3_datetime_format", "ViewDetails": { "Definition": "t2", "Dependencies": [ [ [ "test1", "t2" ] ], [  ], [  ] ], "Default": null, "DataFormat": [ "MM/DD/YYYY hh:mm:ss.nnna", "MM/DD/YYYY", "hh:mm:ss.nnna" ] } }
+{ "DataverseName": "test1", "DatasetName": "v4_date_format_only", "ViewDetails": { "Definition": "t2", "Dependencies": [ [ [ "test1", "t2" ] ], [  ], [  ] ], "Default": null, "DataFormat": [ null, "MM/DD/YYYY", null ] } }
+{ "DataverseName": "test1", "DatasetName": "v5_pk", "ViewDetails": { "Definition": "t2", "Dependencies": [ [ [ "test1", "t2" ] ], [  ], [  ] ], "Default": null, "PrimaryKey": [ [ "c_id" ] ], "PrimaryKeyEnforced": false, "DataFormat": [ "MM/DD/YYYY hh:mm:ss.nnna", null, null ] } }
+{ "DataverseName": "test1", "DatasetName": "v6_pk_no_nulls", "ViewDetails": { "Definition": "t1", "Dependencies": [ [ [ "test1", "t1" ] ], [  ], [  ] ], "Default": null, "PrimaryKey": [ [ "c_i64" ] ], "PrimaryKeyEnforced": false } }
+{ "DataverseName": "test1", "DatasetName": "v7_no_nulls", "ViewDetails": { "Definition": "t1", "Dependencies": [ [ [ "test1", "t1" ] ], [  ], [  ] ], "Default": null } }
+{ "DataverseName": "test1", "DatasetName": "v8_no_nulls_multi", "ViewDetails": { "Definition": "select\n    c_id,\n    case when to_bigint(c_i32) >= 0 then to_bigint(c_i32) when to_bigint(c_i32) < 0 then null else 0 end as c_x,\n    case when to_bigint(c_i64) >= 0 then null when to_bigint(c_i64) < 0 then to_bigint(c_i64) else 0 end as c_y\n  from t1", "Dependencies": [ [ [ "test1", "t1" ] ], [  ], [  ] ], "Default": null } }
+{ "DataverseName": "test1", "DatasetName": "v9_pk_composite", "ViewDetails": { "Definition": "select c_id as c_id1, -c_id as c_id2\n  from t1", "Dependencies": [ [ [ "test1", "t1" ] ], [  ], [  ] ], "Default": null, "PrimaryKey": [ [ "c_id1" ], [ "c_id2" ] ], "PrimaryKeyEnforced": false } }
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.7.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.7.adm
index adcf41f..83cff58 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.7.adm
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.7.adm
@@ -1,4 +1,3 @@
-{ "DataverseName": "test1", "DatasetName": "v1", "ViewDetails": { "Definition": "t1", "Dependencies": [ [ [ "test1", "t1" ] ], [  ], [  ] ], "Default": null } }
-{ "DataverseName": "test1", "DatasetName": "v2_ref_type", "ViewDetails": { "Definition": "select c_id,\n    c_i8, c_i16, c_i32, c_i64, c_f, c_d,\n    c_b, c_s,\n    c_datetime, c_date, c_time,\n    c_dur, c_ymdur, c_dtdur\n  from t1", "Dependencies": [ [ [ "test1", "t1" ] ], [  ], [  ] ], "Default": null } }
-{ "DataverseName": "test1", "DatasetName": "v3_datetime_format", "ViewDetails": { "Definition": "t2", "Dependencies": [ [ [ "test1", "t2" ] ], [  ], [  ] ], "Default": null, "DataFormat": [ "MM/DD/YYYY hh:mm:ss.nnna", "MM/DD/YYYY", "hh:mm:ss.nnna" ] } }
-{ "DataverseName": "test1", "DatasetName": "v4_date_format_only", "ViewDetails": { "Definition": "t2", "Dependencies": [ [ [ "test1", "t2" ] ], [  ], [  ] ], "Default": null, "DataFormat": [ null, "MM/DD/YYYY", null ] } }
\ No newline at end of file
+{ "c_id": 0 }
+{ "c_id": 1 }
+{ "c_id": 2 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.8.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.8.adm
new file mode 100644
index 0000000..6136866
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.8.adm
@@ -0,0 +1,2 @@
+{ "c_id": 0, "c_i64": 64 }
+{ "c_id": 1, "c_i64": -64 }
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.9.adm b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.9.adm
new file mode 100644
index 0000000..6136866
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/results/view/create-view-3-typed/create-view-3-typed.9.adm
@@ -0,0 +1,2 @@
+{ "c_id": 0, "c_i64": 64 }
+{ "c_id": 1, "c_i64": -64 }
\ 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 94c43ed..29526f3 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_sqlpp.xml
@@ -13106,6 +13106,7 @@
         <expected-error>ASX1149: Illegal function or view recursion (in line 31, at column 1)</expected-error>
         <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>
       </compilation-unit>
     </test-case>
     <test-case FilePath="view">
@@ -13159,9 +13160,15 @@
         <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>ASX1079: Compilation error: Invalid type for field r. The type must allow MISSING and NULL (in line 29, at column 1)</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><![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>
         <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 79d663e..ecec240 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
@@ -246,6 +246,7 @@
     UNKNOWN_VIEW(1159),
     VIEW_EXISTS(1160),
     UNSUPPORTED_TYPE_FOR_PARQUET(1161),
+    INVALID_PRIMARY_KEY_DEFINITION(1162),
 
     // 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 159e0ef..e964531 100644
--- a/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
+++ b/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
@@ -248,6 +248,7 @@
 1159 = Cannot find view with name %1$s
 1160 = A view with this name %1$s already exists
 1161 = Type '%1$s' contains declared fields, which is not supported for 'parquet' format
+1162 = Invalid primary key definition
 
 # 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 ae3c50e..74586eb 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
@@ -19,6 +19,7 @@
 
 package org.apache.asterix.lang.common.statement;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
@@ -30,6 +31,7 @@
 import org.apache.asterix.lang.common.expression.TypeExpression;
 import org.apache.asterix.lang.common.util.ViewUtil;
 import org.apache.asterix.lang.common.visitor.base.ILangVisitor;
+import org.apache.hyracks.algebricks.common.utils.Pair;
 
 public final class CreateViewStatement extends AbstractStatement {
 
@@ -45,6 +47,8 @@
 
     private final Map<String, String> viewConfig;
 
+    private final List<String> primaryKeyFields;
+
     private final Boolean defaultNull;
 
     private final boolean replaceIfExists;
@@ -52,15 +56,18 @@
     private final boolean ifNotExists;
 
     public CreateViewStatement(DataverseName dataverseName, String viewName, TypeExpression itemType, String viewBody,
-            Expression viewBodyExpression, Boolean defaultNull, Map<String, String> viewConfig, boolean replaceIfExists,
-            boolean ifNotExists) throws CompilationException {
+            Expression viewBodyExpression, Boolean defaultNull, Map<String, String> viewConfig,
+            Pair<List<Integer>, List<List<String>>> primaryKeyFields, boolean replaceIfExists, boolean ifNotExists)
+            throws CompilationException {
         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, itemType != null);
+        this.viewConfig = ViewUtil.validateViewConfiguration(viewConfig, hasItemType);
+        this.primaryKeyFields = ViewUtil.validateViewPrimaryKey(primaryKeyFields, hasItemType);
         this.replaceIfExists = replaceIfExists;
         this.ifNotExists = ifNotExists;
     }
@@ -113,6 +120,10 @@
         return defaultNull;
     }
 
+    public List<String> getPrimaryKeyFields() {
+        return primaryKeyFields;
+    }
+
     public String getDatetimeFormat() {
         return viewConfig.get(ViewUtil.DATETIME_PARAMETER_NAME);
     }
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 3dc08ee..7b87ffe 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,6 +43,7 @@
 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;
@@ -50,6 +51,7 @@
 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;
@@ -110,16 +112,17 @@
         IAType[] fieldTypes = recordType.getFieldTypes();
         for (int i = 0, n = fieldNames.length; i < n; i++) {
             IAType fieldType = fieldTypes[i];
-            if (fieldType.getTypeTag() != ATypeTag.UNION) {
-                throw new CompilationException(ErrorCode.COMPILATION_ERROR, sourceLoc, String
-                        .format("Invalid type for field %s. The type must allow MISSING and NULL", fieldNames[i]));
+            IAType primeType;
+            if (fieldType.getTypeTag() == ATypeTag.UNION) {
+                AUnionType unionType = (AUnionType) fieldType;
+                if (!unionType.isNullableType()) {
+                    throw new CompilationException(ErrorCode.COMPILATION_ERROR, sourceLoc,
+                            String.format("Invalid type for field %s. Optional type must allow NULL", fieldNames[i]));
+                }
+                primeType = unionType.getActualType();
+            } else {
+                primeType = fieldType;
             }
-            AUnionType unionType = (AUnionType) fieldType;
-            if (!unionType.isMissableType() || !unionType.isNullableType()) {
-                throw new CompilationException(ErrorCode.COMPILATION_ERROR, sourceLoc, String
-                        .format("Invalid type for field %s. The type must allow MISSING and NULL", fieldNames[i]));
-            }
-            IAType primeType = unionType.getActualType();
             if (getTypeConstructor(primeType) == null) {
                 throw new CompilationException(ErrorCode.COMPILATION_TYPE_UNSUPPORTED, sourceLoc, "view",
                         primeType.getTypeName());
@@ -151,6 +154,31 @@
         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 {
@@ -183,6 +211,18 @@
         return missing2NullExpr;
     }
 
+    public static Expression createNotIsNullExpression(Expression inExpr, SourceLocation sourceLoc) {
+        List<Expression> isNullArgs = new ArrayList<>(1);
+        isNullArgs.add(inExpr);
+        CallExpr isNullExpr = new CallExpr(new FunctionSignature(BuiltinFunctions.IS_NULL), isNullArgs);
+        isNullExpr.setSourceLocation(sourceLoc);
+        List<Expression> notExprArgs = new ArrayList<>(1);
+        notExprArgs.add(isNullExpr);
+        CallExpr notExpr = new CallExpr(new FunctionSignature(BuiltinFunctions.NOT), notExprArgs);
+        notExpr.setSourceLocation(sourceLoc);
+        return notExpr;
+    }
+
     public static Expression createFieldAccessExpression(VarIdentifier inVar, String fieldName,
             SourceLocation sourceLoc) {
         VariableExpr inVarRef = new VariableExpr(inVar);
diff --git a/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/rewrites/SqlppFunctionBodyRewriter.java b/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/rewrites/SqlppFunctionBodyRewriter.java
index 5ac1e2d..868469b 100644
--- a/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/rewrites/SqlppFunctionBodyRewriter.java
+++ b/asterixdb/asterix-lang-sqlpp/src/main/java/org/apache/asterix/lang/sqlpp/rewrites/SqlppFunctionBodyRewriter.java
@@ -25,26 +25,35 @@
 
 import org.apache.asterix.common.exceptions.CompilationException;
 import org.apache.asterix.common.exceptions.ErrorCode;
+import org.apache.asterix.common.functions.FunctionSignature;
 import org.apache.asterix.common.metadata.DatasetFullyQualifiedName;
+import org.apache.asterix.lang.common.base.AbstractClause;
 import org.apache.asterix.lang.common.base.Expression;
 import org.apache.asterix.lang.common.base.IParserFactory;
 import org.apache.asterix.lang.common.base.IReturningStatement;
+import org.apache.asterix.lang.common.clause.LetClause;
+import org.apache.asterix.lang.common.clause.WhereClause;
+import org.apache.asterix.lang.common.expression.CallExpr;
+import org.apache.asterix.lang.common.expression.FieldBinding;
+import org.apache.asterix.lang.common.expression.LiteralExpr;
+import org.apache.asterix.lang.common.expression.RecordConstructor;
 import org.apache.asterix.lang.common.expression.VariableExpr;
+import org.apache.asterix.lang.common.literal.StringLiteral;
 import org.apache.asterix.lang.common.rewrites.LangRewritingContext;
 import org.apache.asterix.lang.common.struct.VarIdentifier;
 import org.apache.asterix.lang.common.util.ViewUtil;
 import org.apache.asterix.lang.sqlpp.clause.FromClause;
 import org.apache.asterix.lang.sqlpp.clause.FromTerm;
-import org.apache.asterix.lang.sqlpp.clause.Projection;
 import org.apache.asterix.lang.sqlpp.clause.SelectBlock;
 import org.apache.asterix.lang.sqlpp.clause.SelectClause;
-import org.apache.asterix.lang.sqlpp.clause.SelectRegular;
+import org.apache.asterix.lang.sqlpp.clause.SelectElement;
 import org.apache.asterix.lang.sqlpp.clause.SelectSetOperation;
 import org.apache.asterix.lang.sqlpp.expression.SelectExpression;
 import org.apache.asterix.lang.sqlpp.struct.SetOperationInput;
-import org.apache.asterix.om.typecomputer.impl.TypeComputeUtils;
+import org.apache.asterix.om.functions.BuiltinFunctions;
 import org.apache.asterix.om.types.ARecordType;
 import org.apache.asterix.om.types.ATypeTag;
+import org.apache.asterix.om.types.AUnionType;
 import org.apache.asterix.om.types.IAType;
 import org.apache.hyracks.algebricks.common.utils.Triple;
 import org.apache.hyracks.api.exceptions.SourceLocation;
@@ -142,16 +151,49 @@
             throw new CompilationException(ErrorCode.COMPILATION_TYPE_UNSUPPORTED, sourceLoc, viewName,
                     itemType.getTypeName());
         }
-        List<Projection> projections = new ArrayList<>(n);
+        List<FieldBinding> projections = new ArrayList<>(n);
+        List<AbstractClause> letWhereClauseList = new ArrayList<>(n + 1);
+        List<Expression> filters = null;
         VarIdentifier fromVar = context.newVariable();
         for (int i = 0; i < n; i++) {
             String fieldName = fieldNames[i];
-            IAType targetType = TypeComputeUtils.getActualType(fieldTypes[i]);
+            IAType fieldType = fieldTypes[i];
+            IAType primeType;
+            boolean fieldTypeNullable;
+            if (fieldType.getTypeTag() == ATypeTag.UNION) {
+                AUnionType unionType = (AUnionType) fieldType;
+                fieldTypeNullable = unionType.isNullableType();
+                if (!fieldTypeNullable) {
+                    throw new CompilationException(ErrorCode.COMPILATION_TYPE_UNSUPPORTED, sourceLoc, viewName,
+                            unionType.toString());
+                }
+                primeType = unionType.getActualType();
+            } else {
+                fieldTypeNullable = false;
+                primeType = fieldType;
+            }
             Expression expr = ViewUtil.createFieldAccessExpression(fromVar, fieldName, sourceLoc);
             expr = ViewUtil.createMissingToNullExpression(expr, sourceLoc); // Default Null handling
             Expression projectExpr =
-                    ViewUtil.createTypeConvertExpression(expr, targetType, temporalDataFormat, viewName, sourceLoc);
-            projections.add(new Projection(projectExpr, fieldName, false, false));
+                    ViewUtil.createTypeConvertExpression(expr, primeType, temporalDataFormat, viewName, sourceLoc);
+            VarIdentifier projectVar = context.newVariable();
+            VariableExpr projectVarRef1 = new VariableExpr(projectVar);
+            projectVarRef1.setSourceLocation(sourceLoc);
+            LetClause letClause = new LetClause(projectVarRef1, projectExpr);
+            letWhereClauseList.add(letClause);
+            VariableExpr projectVarRef2 = new VariableExpr(projectVar);
+            projectVarRef2.setSourceLocation(sourceLoc);
+            projections.add(new FieldBinding(new LiteralExpr(new StringLiteral(fieldName)), projectVarRef2));
+
+            if (!fieldTypeNullable) {
+                VariableExpr projectVarRef3 = new VariableExpr(projectVar);
+                projectVarRef3.setSourceLocation(sourceLoc);
+                Expression notIsNullExpr = ViewUtil.createNotIsNullExpression(projectVarRef3, sourceLoc);
+                if (filters == null) {
+                    filters = new ArrayList<>();
+                }
+                filters.add(notIsNullExpr);
+            }
         }
 
         VariableExpr fromVarRef = new VariableExpr(fromVar);
@@ -159,9 +201,27 @@
         FromClause fromClause =
                 new FromClause(Collections.singletonList(new FromTerm(bodyExpr, fromVarRef, null, null)));
         fromClause.setSourceLocation(sourceLoc);
-        SelectClause selectClause = new SelectClause(null, new SelectRegular(projections), false);
+
+        if (filters != null && !filters.isEmpty()) {
+            Expression whereExpr;
+            if (filters.size() == 1) {
+                whereExpr = filters.get(0);
+            } else {
+                CallExpr andExpr = new CallExpr(new FunctionSignature(BuiltinFunctions.AND), filters);
+                andExpr.setSourceLocation(sourceLoc);
+                whereExpr = andExpr;
+            }
+            WhereClause whereClause = new WhereClause(whereExpr);
+            whereClause.setSourceLocation(sourceLoc);
+            letWhereClauseList.add(whereClause);
+        }
+
+        RecordConstructor recordConstr = new RecordConstructor(projections);
+        recordConstr.setSourceLocation(sourceLoc);
+
+        SelectClause selectClause = new SelectClause(new SelectElement(recordConstr), null, false);
         selectClause.setSourceLocation(sourceLoc);
-        SelectBlock selectBlock = new SelectBlock(selectClause, fromClause, null, null, null);
+        SelectBlock selectBlock = new SelectBlock(selectClause, fromClause, letWhereClauseList, null, null);
         selectBlock.setSourceLocation(sourceLoc);
         SelectSetOperation selectSetOperation = new SelectSetOperation(new SetOperationInput(selectBlock, null), null);
         selectSetOperation.setSourceLocation(sourceLoc);
diff --git a/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj b/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
index 0e6bb5e..f3e19a3 100644
--- a/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
+++ b/asterixdb/asterix-lang-sqlpp/src/main/javacc/SQLPP.jj
@@ -1438,6 +1438,7 @@
   Expression viewBodyExpr = null;
   Boolean defaultNull = null;
   Map<String, String> viewConfig = null;
+  Pair<List<Integer>, List<List<String>>> primaryKeyFields = null;
   DataverseName currentDataverse = defaultDataverse;
 }
 {
@@ -1448,6 +1449,7 @@
         ifNotExists = IfNotExists()
         <IDENTIFIER> { expectToken(DEFAULT); } <NULL> { defaultNull = true; }
         viewConfig = ViewConfiguration()
+        ( <PRIMARY> <KEY> <LEFTPAREN> primaryKeyFields = PrimaryKeyFields() <RIGHTPAREN> <NOT> <ENFORCED> )?
       )
       |
       ( ifNotExists = IfNotExists() )
@@ -1474,7 +1476,7 @@
     defaultDataverse = currentDataverse;
     try {
       CreateViewStatement stmt = new CreateViewStatement(nameComponents.first, nameComponents.second.getValue(),
-        typeExpr, viewBody, viewBodyExpr, defaultNull, viewConfig, orReplace, ifNotExists);
+        typeExpr, viewBody, viewBodyExpr, defaultNull, viewConfig, primaryKeyFields, orReplace, ifNotExists);
     return addSourceLocation(stmt, startStmtToken);
     } catch (CompilationException e) {
        throw new SqlppParseException(getSourceLocation(startStmtToken), e.getMessage());
@@ -1950,12 +1952,23 @@
 
 Pair<List<Integer>, List<List<String>>> PrimaryKey() throws ParseException:
 {
+  Pair<List<Integer>, List<List<String>>> primaryKeyFields = null;
+}
+{
+  <PRIMARY> <KEY> primaryKeyFields = PrimaryKeyFields()
+  {
+    return primaryKeyFields;
+  }
+}
+
+Pair<List<Integer>, List<List<String>>> PrimaryKeyFields() throws ParseException:
+{
   Pair<Integer, List<String>> tmp = null;
   List<Integer> keyFieldSourceIndicators = new ArrayList<Integer>();
   List<List<String>> primaryKeyFields = new ArrayList<List<String>>();
 }
 {
-  <PRIMARY> <KEY> tmp = NestedField()
+  tmp = NestedField()
     {
       keyFieldSourceIndicators.add(tmp.first);
       primaryKeyFields.add(tmp.second);
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 567568d..a7af77e 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
@@ -95,6 +95,7 @@
     public static final String FIELD_NAME_PENDING_OP = "PendingOp";
     public static final String FIELD_NAME_POLICY_NAME = "PolicyName";
     public static final String FIELD_NAME_PRIMARY_KEY = "PrimaryKey";
+    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_RETURN_TYPE = "ReturnType";
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 4e6f96d..cd98f32 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
@@ -36,6 +36,7 @@
 import org.apache.asterix.metadata.IDatasetDetails;
 import org.apache.asterix.metadata.bootstrap.MetadataRecordTypes;
 import org.apache.asterix.metadata.entitytupletranslators.AbstractTupleTranslator;
+import org.apache.asterix.om.base.ABoolean;
 import org.apache.asterix.om.base.AMutableString;
 import org.apache.asterix.om.base.ANull;
 import org.apache.asterix.om.base.AString;
@@ -67,14 +68,18 @@
 
     private final String timeFormat;
 
+    private final List<String> primaryKeyFields;
+
     public ViewDetails(String viewBody, List<List<Triple<DataverseName, String, String>>> dependencies,
-            Boolean defaultNull, String datetimeFormat, String dateFormat, String timeFormat) {
+            Boolean defaultNull, List<String> primaryKeyFields, String datetimeFormat, String dateFormat,
+            String timeFormat) {
         this.viewBody = Objects.requireNonNull(viewBody);
         this.dependencies = Objects.requireNonNull(dependencies);
         this.defaultNull = defaultNull;
         this.datetimeFormat = datetimeFormat;
         this.dateFormat = dateFormat;
         this.timeFormat = timeFormat;
+        this.primaryKeyFields = primaryKeyFields;
     }
 
     @Override
@@ -96,6 +101,10 @@
         return defaultNull;
     }
 
+    public List<String> getPrimaryKeyFields() {
+        return primaryKeyFields;
+    }
+
     public String getDatetimeFormat() {
         return datetimeFormat;
     }
@@ -120,7 +129,8 @@
         AMutableString aString = new AMutableString("");
         ISerializerDeserializer<AString> stringSerde =
                 SerializerDeserializerProvider.INSTANCE.getSerializerDeserializer(BuiltinType.ASTRING);
-
+        ISerializerDeserializer<ABoolean> booleanSerde =
+                SerializerDeserializerProvider.INSTANCE.getSerializerDeserializer(BuiltinType.ABOOLEAN);
         ISerializerDeserializer<ANull> nullSerde =
                 SerializerDeserializerProvider.INSTANCE.getSerializerDeserializer(BuiltinType.ANULL);
 
@@ -180,6 +190,40 @@
             viewRecordBuilder.addField(fieldName, fieldValue);
         }
 
+        // write field 'PrimaryKey'
+        if (primaryKeyFields != null && !primaryKeyFields.isEmpty()) {
+            fieldName.reset();
+            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);
+            }
+            fieldValue.reset();
+            primaryKeyListBuilder.write(fieldValue.getDataOutput(), true);
+            viewRecordBuilder.addField(fieldName, fieldValue);
+
+            // write field 'PrimaryKeyEnforced'
+            fieldName.reset();
+            aString.setValue(MetadataRecordTypes.FIELD_NAME_PRIMARY_KEY_ENFORCED);
+            stringSerde.serialize(aString, fieldName.getDataOutput());
+            fieldValue.reset();
+            booleanSerde.serialize(ABoolean.FALSE, fieldValue.getDataOutput());
+            viewRecordBuilder.addField(fieldName, fieldValue);
+        }
+
         // write field 'Format'
         if (datetimeFormat != null || dateFormat != null || timeFormat != null) {
             fieldName.reset();
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 1fe51b3..6caf649 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
@@ -35,6 +35,8 @@
 import org.apache.asterix.builders.UnorderedListBuilder;
 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.DataverseName;
 import org.apache.asterix.metadata.IDatasetDetails;
 import org.apache.asterix.metadata.bootstrap.MetadataPrimaryIndexes;
@@ -274,6 +276,25 @@
                     defaultNull = defaultValue.getType().getTypeTag() == ATypeTag.NULL;
                 }
 
+                // Primary Key
+                List<String> primaryKeyFields = null;
+                int primaryKeyFieldPos =
+                        datasetDetailsRecord.getType().getFieldIndex(MetadataRecordTypes.FIELD_NAME_PRIMARY_KEY);
+                if (primaryKeyFieldPos >= 0) {
+                    AOrderedList primaryKeyFieldList =
+                            ((AOrderedList) datasetDetailsRecord.getValueByPos(primaryKeyFieldPos));
+                    int n = primaryKeyFieldList.size();
+                    primaryKeyFields = new ArrayList<>(n);
+                    for (int i = 0; i < n; i++) {
+                        AOrderedList list = (AOrderedList) primaryKeyFieldList.getItem(i);
+                        if (list.size() != 1) {
+                            throw new AsterixException(ErrorCode.METADATA_ERROR, list.toJSON());
+                        }
+                        AString str = (AString) list.getItem(0);
+                        primaryKeyFields.add(str.getStringValue());
+                    }
+                }
+
                 // Format fields
                 String datetimeFormat = null, dateFormat = null, timeFormat = null;
                 int formatFieldPos =
@@ -292,8 +313,8 @@
                     }
                 }
 
-                datasetDetails =
-                        new ViewDetails(definition, dependencies, defaultNull, datetimeFormat, dateFormat, timeFormat);
+                datasetDetails = new ViewDetails(definition, dependencies, defaultNull, primaryKeyFields,
+                        datetimeFormat, dateFormat, timeFormat);
                 break;
             }
         }