Subject: SQL Property Graph Queries (SQL/PGQ)

From: Peter Eisentraut <[email-masked]>
Date: Fri, 16 Feb 2024 15:53:11 +0100

Here is a prototype implementation of SQL property graph queries
(SQL/PGQ), following SQL:2023.  This was talked about briefly at the
FOSDEM developer meeting, and a few people were interested, so I
wrapped up what I had in progress into a presentable form.

There is some documentation to get started in doc/src/sgml/ddl.sgml
and doc/src/sgml/queries.sgml.

To learn more about this facility, here are some external resources:

* An article about a competing product:
   https://oracle-base.com/articles/23c/sql-property-graphs-and-sql-pgq-23c
   (All the queries in the article work, except the ones using
   vertex_id() and edge_id(), which are non-standard, and the JSON
   examples at the end, which require some of the in-progress JSON
   functionality for PostgreSQL.)

* An academic paper related to another competing product:
   https://www.cidrdb.org/cidr2023/papers/p66-wolde.pdf (The main part
   of this paper discusses advanced functionality that my patch doesn't
   have.)

* A 2019 presentation about graph databases:
   https://www.pgcon.org/2019/schedule/events/1300.en.html (There is
   also a video.)

* (Vik has a recent presentation "Property Graphs: When the Relational
   Model Is Not Enough", but I haven't found the content posted
   online.)

The patch is quite fragile, and treading outside the tested paths will
likely lead to grave misbehavior.  Use with caution.  But I feel that
the general structure is ok, and we just need to fill in the
proverbial few thousand lines of code in the designated areas.

From 22673830c106db994093b0c764f62459bb7e5f56 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email-masked]>
Date: Fri, 16 Feb 2024 15:50:43 +0100
Subject: [PATCH v0] WIP: SQL Property Graph Queries (SQL/PGQ)

Implementation of SQL property graph queries, according to SQL/PGQ
standard (ISO 9075-16:2023).
---
 doc/src/sgml/catalogs.sgml                    |   48 +-
 doc/src/sgml/ddl.sgml                         |  229 +++-
 doc/src/sgml/features.sgml                    |    4 +-
 doc/src/sgml/func.sgml                        |   15 +
 doc/src/sgml/information_schema.sgml          |   80 ++
 .../sgml/keywords/sql2023-16-nonreserved.txt  |   27 +
 doc/src/sgml/keywords/sql2023-16-reserved.txt |   12 +
 doc/src/sgml/queries.sgml                     |  156 +++
 doc/src/sgml/ref/allfiles.sgml                |    3 +
 doc/src/sgml/ref/alter_extension.sgml         |    3 +-
 doc/src/sgml/ref/alter_property_graph.sgml    |  150 +++
 doc/src/sgml/ref/comment.sgml                 |    1 +
 doc/src/sgml/ref/create_property_graph.sgml   |  232 ++++
 doc/src/sgml/ref/drop_property_graph.sgml     |  111 ++
 doc/src/sgml/ref/grant.sgml                   |    7 +-
 doc/src/sgml/ref/psql-ref.sgml                |   13 +-
 doc/src/sgml/ref/revoke.sgml                  |    7 +
 doc/src/sgml/ref/security_label.sgml          |    1 +
 doc/src/sgml/ref/select.sgml                  |   43 +
 doc/src/sgml/reference.sgml                   |    3 +
 src/backend/catalog/Makefile                  |    5 +-
 src/backend/catalog/aclchk.c                  |   24 +
 src/backend/catalog/dependency.c              |   20 +
 src/backend/catalog/information_schema.sql    |  363 ++++++
 src/backend/catalog/objectaddress.c           |  140 +++
 src/backend/catalog/pg_class.c                |    2 +
 src/backend/catalog/sql_features.txt          |  100 ++
 src/backend/commands/Makefile                 |    1 +
 src/backend/commands/alter.c                  |   31 +-
 src/backend/commands/dropcmds.c               |    1 +
 src/backend/commands/event_trigger.c          |    6 +
 src/backend/commands/meson.build              |    1 +
 src/backend/commands/propgraphcmds.c          | 1001 +++++++++++++++++
 src/backend/commands/seclabel.c               |    1 +
 src/backend/commands/tablecmds.c              |   14 +
 src/backend/executor/execMain.c               |    2 +-
 src/backend/nodes/nodeFuncs.c                 |   72 ++
 src/backend/nodes/outfuncs.c                  |    5 +
 src/backend/nodes/queryjumblefuncs.c          |    5 +
 src/backend/nodes/readfuncs.c                 |    5 +
 src/backend/optimizer/path/allpaths.c         |    4 +
 src/backend/optimizer/prep/prepjointree.c     |    8 +
 src/backend/parser/Makefile                   |    1 +
 src/backend/parser/gram.y                     |  693 +++++++++++-
 src/backend/parser/meson.build                |    1 +
 src/backend/parser/parse_clause.c             |   85 ++
 src/backend/parser/parse_collate.c            |    7 +
 src/backend/parser/parse_graphtable.c         |  251 +++++
 src/backend/parser/parse_relation.c           |   94 ++
 src/backend/parser/parse_target.c             |    5 +
 src/backend/parser/scan.l                     |   48 +
 src/backend/rewrite/Makefile                  |    1 +
 src/backend/rewrite/meson.build               |    1 +
 src/backend/rewrite/rewriteGraphTable.c       |  422 +++++++
 src/backend/rewrite/rewriteHandler.c          |   12 +
 src/backend/tcop/utility.c                    |   34 +
 src/backend/utils/adt/ruleutils.c             |  502 +++++++++
 src/bin/pg_dump/common.c                      |    3 +-
 src/bin/pg_dump/pg_backup_archiver.c          |    1 +
 src/bin/pg_dump/pg_dump.c                     |   67 +-
 src/bin/pg_dump/t/002_pg_dump.pl              |   11 +
 src/bin/psql/command.c                        |    3 +-
 src/bin/psql/describe.c                       |   43 +-
 src/bin/psql/help.c                           |    1 +
 src/bin/psql/tab-complete.c                   |   65 +-
 src/fe_utils/psqlscan.l                       |    8 +
 src/include/catalog/dependency.h              |    3 +
 src/include/catalog/meson.build               |    3 +
 src/include/catalog/pg_class.h                |    1 +
 src/include/catalog/pg_proc.dat               |    3 +
 src/include/catalog/pg_propgraph_element.h    |  103 ++
 src/include/catalog/pg_propgraph_label.h      |   59 +
 src/include/catalog/pg_propgraph_property.h   |   74 ++
 src/include/commands/propgraphcmds.h          |   23 +
 src/include/nodes/parsenodes.h                |  135 +++
 src/include/nodes/primnodes.h                 |   22 +
 src/include/parser/kwlist.h                   |   10 +
 src/include/parser/parse_graphtable.h         |   31 +
 src/include/parser/parse_relation.h           |    8 +
 src/include/rewrite/rewriteGraphTable.h       |   21 +
 src/include/tcop/cmdtaglist.h                 |    3 +
 src/include/utils/acl.h                       |    1 +
 src/pl/plpgsql/src/pl_gram.y                  |    1 +
 src/test/regress/expected/alter_generic.out   |   51 +-
 .../expected/create_property_graph.out        |  370 ++++++
 src/test/regress/expected/graph_table.out     |  122 ++
 src/test/regress/expected/object_address.out  |   14 +-
 src/test/regress/expected/oidjoins.out        |    9 +
 src/test/regress/parallel_schedule            |    4 +-
 src/test/regress/sql/alter_generic.sql        |   34 +
 .../regress/sql/create_property_graph.sql     |  130 +++
 src/test/regress/sql/graph_table.sql          |   96 ++
 src/test/regress/sql/object_address.sql       |    4 +-
 src/tools/pgindent/typedefs.list              |   19 +
 94 files changed, 6620 insertions(+), 49 deletions(-)
 create mode 100644 doc/src/sgml/keywords/sql2023-16-nonreserved.txt
 create mode 100644 doc/src/sgml/keywords/sql2023-16-reserved.txt
 create mode 100644 doc/src/sgml/ref/alter_property_graph.sgml
 create mode 100644 doc/src/sgml/ref/create_property_graph.sgml
 create mode 100644 doc/src/sgml/ref/drop_property_graph.sgml
 create mode 100644 src/backend/commands/propgraphcmds.c
 create mode 100644 src/backend/parser/parse_graphtable.c
 create mode 100644 src/backend/rewrite/rewriteGraphTable.c
 create mode 100644 src/include/catalog/pg_propgraph_element.h
 create mode 100644 src/include/catalog/pg_propgraph_label.h
 create mode 100644 src/include/catalog/pg_propgraph_property.h
 create mode 100644 src/include/commands/propgraphcmds.h
 create mode 100644 src/include/parser/parse_graphtable.h
 create mode 100644 src/include/rewrite/rewriteGraphTable.h
 create mode 100644 src/test/regress/expected/create_property_graph.out
 create mode 100644 src/test/regress/expected/graph_table.out
 create mode 100644 src/test/regress/sql/create_property_graph.sql
 create mode 100644 src/test/regress/sql/graph_table.sql

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 880f717b103..675b8b1ba8e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -240,6 +240,21 @@ <title>System Catalogs</title>
       <entry>functions and procedures</entry>
      </row>
 
+     <row>
+      <entry><link linkend="catalog-pg-propgraph-element"><structname>pg_propgraph_element</structname></link></entry>
+      <entry>property graph elements (vertices and edges)</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-propgraph-label"><structname>pg_propgraph_label</structname></link></entry>
+      <entry>property graph labels</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-propgraph-property"><structname>pg_propgraph_property</structname></link></entry>
+      <entry>property graph properties</entry>
+     </row>
+
      <row>
       <entry><link linkend="catalog-pg-publication"><structname>pg_publication</structname></link></entry>
       <entry>publications for logical replication</entry>
@@ -2115,7 +2130,8 @@ <title><structname>pg_class</structname> Columns</title>
        <literal>c</literal> = composite type,
        <literal>f</literal> = foreign table,
        <literal>p</literal> = partitioned table,
-       <literal>I</literal> = partitioned index
+       <literal>I</literal> = partitioned index,
+       <literal>g</literal> = property graph
       </para></entry>
      </row>
 
@@ -6258,6 +6274,36 @@ <title><structname>pg_proc</structname> Columns</title>
 
  </sect1>
 
+ <sect1 id="catalog-pg-propgraph-element">
+  <title><structname>pg_propgraph_element</structname></title>
+
+  <indexterm zone="catalog-pg-propgraph-element">
+   <primary>pg_propgraph_element</primary>
+  </indexterm>
+
+  <para>TODO</para>
+ </sect1>
+
+ <sect1 id="catalog-pg-propgraph-label">
+  <title><structname>pg_propgraph_label</structname></title>
+
+  <indexterm zone="catalog-pg-propgraph-label">
+   <primary>pg_propgraph_label</primary>
+  </indexterm>
+
+  <para>TODO</para>
+ </sect1>
+
+ <sect1 id="catalog-pg-propgraph-property">
+  <title><structname>pg_propgraph_property</structname></title>
+
+  <indexterm zone="catalog-pg-propgraph-property">
+   <primary>pg_propgraph_property</primary>
+  </indexterm>
+
+  <para>TODO</para>
+ </sect1>
+
  <sect1 id="catalog-pg-publication">
   <title><structname>pg_publication</structname></title>
 
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 9d7e2c756be..7333cf57e34 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1951,7 +1951,7 @@ <title>Privileges</title>
       <para>
        Allows <command>SELECT</command> from
        any column, or specific column(s), of a table, view, materialized
-       view, or other table-like object.
+       view, property graph, or other table-like object.
        Also allows use of <command>COPY TO</command>.
        This privilege is also needed to reference existing column values in
        <command>UPDATE</command>, <command>DELETE</command>,
@@ -5269,6 +5269,233 @@ <title>Foreign Data</title>
    </para>
  </sect1>
 
+ <sect1 id="ddl-property-graphs">
+  <title>Property Graphs</title>
+
+  <indexterm zone="ddl-property-graphs">
+   <primary>property graph</primary>
+  </indexterm>
+
+  <para>
+   A property graph is a way to represent database contents, instead of using
+   relational structures such as tables.  A property graph can then be queried
+   using graph pattern matching syntax, instead of join queries typical of
+   relational databases.  PostgreSQL implements SQL/PGQ<footnote><para>Here,
+   PGQ stands for <quote>property graph query</quote>.  In the jargon of graph
+   databases, <quote>property graph</quote> is normally abbreviated as PG,
+   which is clearly confusing for practioners of PostgreSQL, also usually
+   abbreviated as PG.</para></footnote>, which is part of the SQL standard,
+   where a property graph is defined as a kind of read-only view over
+   relational tables.  So the actual data is still in tables or table-like
+   objects, but is exposed as a graph for graph querying operations.  (This is
+   in contrast to native graph databases, where the data is stored directly in
+   a graph structure.)  Underneath, both relational queries and graph queries
+   use the same query planning and execution infrastucture, and in fact
+   relational and graph queries can be combined and mixed in single queries.
+  </para>
+
+  <para>
+   A graph is a set of vertices and edges.  Each edge has two distinguishable
+   associated vertices called the source and destination vertices.  (So in
+   this model all edges are directed.)  Vertices and edges together are called
+   the elements of the graph.  A property graph extends this well-known
+   mathematical structure with a way to represent user data.  In a property
+   graph, each vertex or edge has one or more associated labels, and each
+   label has zero or more properties.  The labels are similar to table row
+   types in that they define the kind of the contained data and its structure.
+   The properties are similar to columns in that they contain the actual data.
+   In fact, by default, a property graph definition exposes the underlying
+   tables and columns as labels and properties, but more complicated
+   definitions are possible.
+  </para>
+
+  <para>
+   Consider the following table definitions:
+<programlisting>
+CREATE TABLE products (
+    product_no integer PRIMARY KEY,
+    name varchar,
+    price numeric
+);
+
+CREATE TABLE customers (
+    customer_id integer PRIMARY KEY,
+    name varchar,
+    address varchar
+);
+
+CREATE TABLE orders (
+    order_id integer PRIMARY KEY,
+    ordered_when date
+);
+
+CREATE TABLE order_items (
+    order_items_id integer PRIMARY KEY,
+    order_id integer REFERENCES orders (order_id),
+    product_no integer REFERENCES products (product_no),
+    quantity integer
+);
+
+CREATE TABLE customer_orders (
+    customer_orders_id integer PRIMARY KEY,
+    customer_id integer REFERENCES customers (customer_id),
+    order_id integer REFERENCES orders (order_id)
+);
+</programlisting>
+   When mapping this to a graph, the first three tables would be the vertices
+   and the last two tables would be the edges.  The foreign-key definitions
+   correspond to the fact that edges link two vertices.  (Graph definitions
+   work more naturally with many-to-many relationships, so this example is
+   organized like that, even though one-to-many relationships might be used
+   here in a pure relational approach.)
+  </para>
+
+  <para>
+   Here is an example how a property graph could be defined on top of these
+   tables:
+<programlisting>
+CREATE PROPERTY GRAPH myshop
+    VERTEX TABLES (
+        products,
+        customers,
+        orders
+    )
+    EDGE TABLES (
+        order_items SOURCE orders DESTINATION products,
+        customer_orders SOURCE customers DESTINATION orders
+    );
+</programlisting>
+  </para>
+
+  <para>
+   This graph could then be queried like this:
+<programlisting>
+-- get list of customers active today
+SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[IS customer_orders]->(o IS orders WHERE ordered_when = current_date) COLUMNS (c.name AS customer_name));
+</programlisting>
+   corresponding approximately to this relational query:
+<programlisting>
+-- get list of customers active today
+SELECT customers.name FROM customers JOIN customer_orders USING (customer_id) JOIN orders USING (order_id) WHERE orders.ordered_when = current_date;
+</programlisting>
+  </para>
+
+  <para>
+   The above definition requires that all tables have primary keys and that
+   for each edge there is an appropriate foreign key.  Otherwise, additional
+   clauses have to be specified to identify the key columns.  For example,
+   this would be the fully verbose definition that does not rely on primary
+   and foreign keys:
+<programlisting>
+CREATE PROPERTY GRAPH myshop
+    VERTEX TABLES (
+        products KEY (product_no),
+        customers KEY (customer_id),
+        orders KEY (order_id)
+    )
+    EDGE TABLES (
+        order_items KEY (order_items_id)
+            SOURCE KEY (order_id) REFERENCES orders (order_id)
+            DESTINATION KEY (product_no) REFERENCES products (product_no),
+        customer_orders KEY (customer_orders_id)
+            SOURCE KEY (customer_id) REFERENCES customers (customer_id)
+            DESTINATION KEY (order_id) REFERENCES orders (order_id)
+    );
+</programlisting>
+  </para>
+
+  <para>
+   As mentioned above, by default, the names of the tables and columns are
+   exposed as labels and properties, respectively.  The clauses <literal>IS
+   customer</literal>, <literal>IS order</literal>, etc. in the
+   <literal>MATCH</literal> clause in fact refer to labels, not table names.
+  </para>
+
+  <para>
+   One use of labels is to expose a table through a different name in the
+   graph.  For example, in graphs, vertices typically have singular noun
+   labels and edges typically have verbs as labels, such as <quote>is</quote>,
+   <quote>has</quote>, <quote>contains</quote>, or something specific like
+   <quote>approves</quote>.  We can introduce such labels into our example
+   like this:
+<programlisting>
+CREATE PROPERTY GRAPH myshop
+    VERTEX TABLES (
+        products LABEL product,
+        customers LABEL customer,
+        orders LABEL order
+    )
+    EDGE TABLES (
+        order_items SOURCE orders DESTINATION products LABEL contains,
+        customer_orders SOURCE customers DESTINATION orders LABEL has
+    );
+</programlisting>
+  </para>
+
+  <para>
+   With this definition, we can write a query like this:
+<programlisting>
+SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c:customer)-[:has]->(o:order WHERE ordered_when = current_date) COLUMNS (c.name AS customer_name));
+</programlisting>
+   With the new labels and using the colon instead of <literal>IS</literal>,
+   which are equivalent, the <literal>MATCH</literal> clause is now more
+   compact and intuitive.
+  </para>
+
+  <para>
+   Another use is to apply the same label to multiple element tables.  For
+   example, consider this additional table:
+<programlisting>
+CREATE TABLE employees (
+    employee_id integer PRIMARY KEY,
+    employee_name varchar,
+    ...
+);
+</programlisting>
+and the following graph definition:
+<programlisting>
+CREATE PROPERTY GRAPH myshop
+    VERTEX TABLES (
+        products LABEL product,
+        customers LABEL customer LABEL person PROPERTIES (name),
+        orders LABEL order,
+        employees LABEL employee LABEL person PROPERTIES (employee_name AS name)
+    )
+    EDGE TABLES (
+        order_items SOURCE orders DESTINATION products LABEL contains,
+        customer_orders SOURCE customers DESTINATION orders LABEL has
+    );
+</programlisting>
+   (In practice, there ought to be an edge linking the
+   <literal>employees</literal> table to something, but it is allowed like
+   this.)
+  </para>
+
+  <para>
+   Then we can run a query like this (incomplete):
+<programlisting>
+SELECT ... FROM GRAPH_TABLE (myshop MATCH (:person WHERE name = '...')-[]->... COLUMNS (...));
+</programlisting>
+   This would automatically consider both the <literal>customers</literal> and
+   the <literal>employees</literal> tables when looking for an edge with the
+   <literal>person</literal> label.
+  </para>
+
+  <para>
+   When more than one element table has the same label, it is required that
+   the properties match in number, name, and type.  In the example, we specify
+   an explicit property list and in one case override the name of the column
+   to achieve this.
+  </para>
+
+  <para>
+   For more details on the syntax for creating property graphs, see <link
+   linkend="sql-create-property-graph"><command>CREATE PROPERTY
+   GRAPH</command></link>.  More details about the graph query syntax is in
+   <xref linkend="queries-graph"/>.
+  </para>
+ </sect1>
+
  <sect1 id="ddl-others">
   <title>Other Database Objects</title>
 
diff --git a/doc/src/sgml/features.sgml b/doc/src/sgml/features.sgml
index 966fd398827..1abe6ccd3d5 100644
--- a/doc/src/sgml/features.sgml
+++ b/doc/src/sgml/features.sgml
@@ -70,10 +70,10 @@ <title>SQL Conformance</title>
 
  <para>
   The <productname>PostgreSQL</productname> core covers parts 1, 2, 9,
-  11, and 14.  Part 3 is covered by the ODBC driver, and part 13 is
+  11, 14, and 16.  Part 3 is covered by the ODBC driver, and part 13 is
   covered by the PL/Java plug-in, but exact conformance is currently
   not being verified for these components.  There are currently no
-  implementations of parts 4, 10, 15, and 16
+  implementations of parts 4, 10, and 15
   for <productname>PostgreSQL</productname>.
  </para>
 
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index cf3de80394e..237136e2aec 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -25000,6 +25000,21 @@ <title>System Catalog Information Functions</title>
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_propgraphdef</primary>
+        </indexterm>
+        <function>pg_get_propgraphdef</function> ( <parameter>propgraph</parameter> <type>oid</type> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the creating command for a property graph.
+        (This is a decompiled reconstruction, not the original text
+        of the command.)
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/information_schema.sgml b/doc/src/sgml/information_schema.sgml
index 9e66be4e83d..02f18eedbd7 100644
--- a/doc/src/sgml/information_schema.sgml
+++ b/doc/src/sgml/information_schema.sgml
@@ -4171,6 +4171,86 @@ <title><structname>parameters</structname> Columns</title>
   </table>
  </sect1>
 
+ <sect1 id="infoschema-pg-edge-table-components">
+  <title><literal>pg_edge_table_components</literal></title>
+
+  <para>
+   TODO
+  </para>
+ </sect1>
+
+ <sect1 id="infoschema-pg-element-table-key-columns">
+  <title><literal>pg_element_table_key_columns</literal></title>
+
+  <para>
+   TODO
+  </para>
+ </sect1>
+
+ <sect1 id="infoschema-pg-element-table-labels">
+  <title><literal>pg_element_table_labels</literal></title>
+
+  <para>
+   TODO
+  </para>
+ </sect1>
+
+ <sect1 id="infoschema-pg-element-table-properties">
+  <title><literal>pg_element_table_properties</literal></title>
+
+  <para>
+   TODO
+  </para>
+ </sect1>
+
+ <sect1 id="infoschema-pg-element-tables">
+  <title><literal>pg_element_tables</literal></title>
+
+  <para>
+   TODO
+  </para>
+ </sect1>
+
+ <sect1 id="infoschema-pg-label-properties">
+  <title><literal>pg_element_label_properties</literal></title>
+
+  <para>
+   TODO
+  </para>
+ </sect1>
+
+ <sect1 id="infoschema-pg-labels">
+  <title><literal>pg_element_labels</literal></title>
+
+  <para>
+   TODO
+  </para>
+ </sect1>
+
+ <sect1 id="infoschema-pg-property-data-types">
+  <title><literal>pg_property_data_types</literal></title>
+
+  <para>
+   TODO
+  </para>
+ </sect1>
+
+ <sect1 id="infoschema-pg-property-graph-privileges">
+  <title><literal>pg_property_graph_privileges</literal></title>
+
+  <para>
+   TODO
+  </para>
+ </sect1>
+
+ <sect1 id="infoschema-property-graphs">
+  <title><literal>property_graphs</literal></title>
+
+  <para>
+   TODO
+  </para>
+ </sect1>
+
  <sect1 id="infoschema-referential-constraints">
   <title><literal>referential_constraints</literal></title>
 
diff --git a/doc/src/sgml/keywords/sql2023-16-nonreserved.txt b/doc/src/sgml/keywords/sql2023-16-nonreserved.txt
new file mode 100644
index 00000000000..39756c6067d
--- /dev/null
+++ b/doc/src/sgml/keywords/sql2023-16-nonreserved.txt
@@ -0,0 +1,27 @@
+ACYCLIC
+BINDINGS
+BOUND
+DESTINATION
+DIFFERENT
+DIRECTED
+EDGE
+EDGES
+ELEMENTS
+LABEL
+LABELED
+NODE
+PATHS
+PROPERTIES
+PROPERTY
+PROPERTY_GRAPH_CATALOG
+PROPERTY_GRAPH_NAME
+PROPERTY_GRAPH_SCHEMA
+RELATIONSHIP
+RELATIONSHIPS
+SHORTEST
+SINGLETONS
+STEP
+TABLES
+TRAIL
+VERTEX
+WALK
diff --git a/doc/src/sgml/keywords/sql2023-16-reserved.txt b/doc/src/sgml/keywords/sql2023-16-reserved.txt
new file mode 100644
index 00000000000..3bdd7e2b272
--- /dev/null
+++ b/doc/src/sgml/keywords/sql2023-16-reserved.txt
@@ -0,0 +1,12 @@
+ALL_DIFFERENT
+BINDING_COUNT
+ELEMENT_ID
+ELEMENT_NUMBER
+EXPORT
+GRAPH
+GRAPH_TABLE
+MATCHNUM
+PATH_LENGTH
+PATH_NAME
+PROPERTY_EXISTS
+SAME
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
index 648b283b064..5b088331c6e 100644
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2744,4 +2744,160 @@ <title>Data-Modifying Statements in <literal>WITH</literal></title>
 
  </sect1>
 
+ <sect1 id="queries-graph">
+  <title>Graph Queries</title>
+
+  <para>
+   This section describes the sublanguage for querying property graphs,
+   defined as described in <xref linkend="ddl-property-graphs"/>.
+  </para>
+
+  <sect2 id="queries-graph-overview">
+   <title>Overview</title>
+
+   <para>
+    Consider this example from <xref linkend="ddl-property-graphs"/>:
+<programlisting>
+-- get list of customers active today
+SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers)-[IS customer_orders]->(o IS orders WHERE o.ordered_when = current_date) COLUMNS (c.name AS customer_name));
+</programlisting>
+    The graph query part happens inside the <literal>GRAPH_TABLE</literal>
+    construct.  As far as the rest of the query is concerned, this acts like a
+    table function in that it produces a computed table as output.  Like other
+    <literal>FROM</literal> clause elements, table alias and column alias
+    names can be assigned to the result, and the result can be joined with
+    other tables, subsequently filtered, and so on, for example:
+<programlisting>
+SELECT ... FROM GRAPH_TABLE (mygraph MATCH ... COLUMNS (...)) AS myresult (a, b, c) JOIN othertable USING (a) WHERE b > 0 ORDER BY c;
+</programlisting>
+   </para>
+
+   <para>
+    The <literal>GRAPH_TABLE</literal> clause consists of the graph name,
+    followed by the keyword <literal>MATCH</literal>, followed by a graph
+    pattern expression (see below), followed by the keyword
+    <literal>COLUMNS</literal> and a column list.
+   </para>
+  </sect2>
+
+  <sect2 id="queries-graph-patterns">
+   <title>Graph Patterns</title>
+
+   <para>
+    The core of the graph querying functionality is the graph pattern, which
+    appears after the keyword <literal>MATCH</literal>.  Formally, a graph
+    pattern consists of one or more path patterns.  A path is a sequence of
+    graph elements, starting and ending with a vertex and alternating between
+    vertices and edges.  A path pattern is a syntactic expressions that
+    matches paths.
+   </para>
+
+   <para>
+    A path pattern thus matches a sequence of vertices and edges.  The
+    simplest possible path pattern is
+<programlisting>
+()
+</programlisting>
+    which matches a single vertex.  The next simplest pattern would be
+<programlisting>
+()-[]->()
+</programlisting>
+    which matches a vertex followed by an edge followed by a vertex.  The
+    characters <literal>()</literal> are a vertex pattern and the characters
+    <literal>-[]-></literal> are an edge pattern.
+   </para>
+
+   <tip>
+    <para>
+     A way to remember these symbols is that in visual representations of
+     property graphs, vertices are usually circles (like
+     <literal>()</literal>) and edges have rectangular labels (like
+     <literal>[]</literal>).
+    </para>
+   </tip>
+
+   <para>
+    The above patterns would match any vertex, or any two vertices connected
+    by any edge, which isn't very interesting.  Normally, we want to search
+    for elements (vertices and edges) that have certain characteristics.
+    These characteristics are written in between the parentheses or brackets.
+    (This is also called an element pattern filler.)  Typically, we would
+    search for elements with a certain label.  This is written either by
+    <literal>IS <replaceable>labelname</replaceable></literal> or equivalently
+    <literal>:<replaceable>labelname</replaceable></literal>.  For example,
+    this would match all vertices with the label <literal>person</literal>:
+<programlisting>
+(IS person)
+</programlisting>
+    or
+<programlisting>
+(:person)
+</programlisting>
+    (From now on, we will just use the colon syntax, for simplicity.  But it
+    helps to read it as <quote>is</quote> for understanding.)  The next
+    example would match a vertex with the label <literal>person</literal>
+    connected to a vertex with the label <literal>account</literal> connected
+    by an edge with the label <literal>has</literal>.
+<programlisting>
+(:person)-[:has]->(:account)
+</programlisting>
+    Multiple labels can also be matched, using <quote>or</quote> semantics:
+<programlisting>
+(:person)-[:has]->(:account|creditcard)
+</programlisting>
+   </para>
+
+   <para>
+    Recall that edges are directed.  The other direction is also possible in a
+    path pattern, for example:
+<programlisting>
+(:account)&lt;-[:has]-(:person)
+</programlisting>
+    It is also possible to match both directions:
+<programlisting>
+(:person)-[:is_friend_of]-(:person)
+</programlisting>
+    This has a meaning of <quote>or</quote>: An edge in either direction would
+    match.
+   </para>
+
+   <para>
+    In many cases, the edge patterns don't need a filler.  (All the filtering
+    then happens on the vertices.)  For these cases, an abbreviated edge
+    pattern syntax is available that omits the brackets, for example:
+<programlisting>
+(:person)->(:account)
+(:acount)&lt;-(:person)
+(:person)-(:person)
+</programlisting>
+    As is often the case, abbreviated syntax can make expressions more compact
+    but also sometimes harder to understand.
+   </para>
+
+   <para>
+    Furthermore, it is possible to define graph pattern variables in the path
+    pattern expressions.  These are bound to the matched elements and can be
+    used to refer to the property values from those elements.  The most
+    important use is to use them in the <literal>COLUMNS</literal> clause to
+    define the tabular result of the <literal>GRAPH_TABLE</literal> clause.
+    For example (assuming appropriate definitions of the property graph as
+    well as the underlying tables):
+<programlisting>
+GRAPH_TABLE (mygraph MATCH (p:person)-[h:has]->(a:account)
+             COLUMNS (p.name AS person_name, h.since AS has_account_since, a.num AS account_number)
+</programlisting>
+    <literal>WHERE</literal> clauses can be used inside element patterns to
+    filter matches:
+<programlisting>
+(:person)-[:has]->(a:account WHERE a.type = 'savings')
+</programlisting>
+   </para>
+
+   <!-- TODO: multiple path patterns in a graph pattern (comma-separated) -->
+
+   <!-- TODO: quantifiers -->
+
+  </sect2>
+ </sect1>
+
 </chapter>
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 4a42999b183..990d2b1af3c 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -27,6 +27,7 @@
 <!ENTITY alterOperatorFamily SYSTEM "alter_opfamily.sgml">
 <!ENTITY alterPolicy        SYSTEM "alter_policy.sgml">
 <!ENTITY alterProcedure     SYSTEM "alter_procedure.sgml">
+<!ENTITY alterPropertyGraph SYSTEM "alter_property_graph.sgml">
 <!ENTITY alterPublication   SYSTEM "alter_publication.sgml">
 <!ENTITY alterRole          SYSTEM "alter_role.sgml">
 <!ENTITY alterRoutine       SYSTEM "alter_routine.sgml">
@@ -79,6 +80,7 @@
 <!ENTITY createOperatorFamily SYSTEM "create_opfamily.sgml">
 <!ENTITY createPolicy       SYSTEM "create_policy.sgml">
 <!ENTITY createProcedure    SYSTEM "create_procedure.sgml">
+<!ENTITY createPropertyGraph SYSTEM "create_property_graph.sgml">
 <!ENTITY createPublication  SYSTEM "create_publication.sgml">
 <!ENTITY createRole         SYSTEM "create_role.sgml">
 <!ENTITY createRule         SYSTEM "create_rule.sgml">
@@ -127,6 +129,7 @@
 <!ENTITY dropOwned          SYSTEM "drop_owned.sgml">
 <!ENTITY dropPolicy         SYSTEM "drop_policy.sgml">
 <!ENTITY dropProcedure      SYSTEM "drop_procedure.sgml">
+<!ENTITY dropPropertyGraph  SYSTEM "drop_property_graph.sgml">
 <!ENTITY dropPublication    SYSTEM "drop_publication.sgml">
 <!ENTITY dropRole           SYSTEM "drop_role.sgml">
 <!ENTITY dropRoutine        SYSTEM "drop_routine.sgml">
diff --git a/doc/src/sgml/ref/alter_extension.sgml b/doc/src/sgml/ref/alter_extension.sgml
index c819c7bb4e3..60218fcd01c 100644
--- a/doc/src/sgml/ref/alter_extension.sgml
+++ b/doc/src/sgml/ref/alter_extension.sgml
@@ -46,6 +46,7 @@
   OPERATOR FAMILY <replaceable class="parameter">object_name</replaceable> USING <replaceable class="parameter">index_method</replaceable> |
   [ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
   PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
+  PROPERTY GRAPH <replaceable class="parameter">object_name</replaceable> |
   ROUTINE <replaceable class="parameter">routine_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
   SCHEMA <replaceable class="parameter">object_name</replaceable> |
   SEQUENCE <replaceable class="parameter">object_name</replaceable> |
@@ -179,7 +180,7 @@ <title>Parameters</title>
        The name of an object to be added to or removed from the extension.
        Names of tables,
        aggregates, domains, foreign tables, functions, operators,
-       operator classes, operator families, procedures, routines, sequences, text search objects,
+       operator classes, operator families, procedures, property graphs, routines, sequences, text search objects,
        types, and views can be schema-qualified.
       </para>
      </listitem>
diff --git a/doc/src/sgml/ref/alter_property_graph.sgml b/doc/src/sgml/ref/alter_property_graph.sgml
new file mode 100644
index 00000000000..e4e56ad652d
--- /dev/null
+++ b/doc/src/sgml/ref/alter_property_graph.sgml
@@ -0,0 +1,150 @@
+<!--
+doc/src/sgml/ref/alter_property_graph.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-property-graph">
+ <indexterm zone="sql-alter-property-graph">
+  <primary>ALTER PROPERTY GRAPH</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER PROPERTY GRAPH</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER PROPERTY GRAPH</refname>
+  <refpurpose>change the definition of an SQL-property graph</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER PROPERTY GRAPH <replaceable class="parameter">name</replaceable> ADD
+    [ {VERTEX|NODE} TABLES ( <replaceable class="parameter">vertex_table_name</replaceable> [, ...] ) ]
+    [ {EDGE|RELATIONSHIP} TABLES ( { <replaceable class="parameter">edge_table_name</replaceable> SOURCE <replaceable class="parameter">source_table_name</replaceable> DESTINATION <replaceable class="parameter">dest_table_name</replaceable> } [, ...] ) ]
+
+ALTER PROPERTY GRAPH <replaceable class="parameter">name</replaceable> DROP
+    {VERTEX|NODE} TABLES ( <replaceable class="parameter">vertex_table_alias</replaceable> [, ...] ) [ CASCADE | RESTRICT ]
+
+ALTER PROPERTY GRAPH <replaceable class="parameter">name</replaceable> DROP
+    {EDGE|RELATIONSHIP} TABLES ( <replaceable class="parameter">edge_table_alias</replaceable> [, ...] ) [ CASCADE | RESTRICT ]
+
+ALTER PROPERTY GRAPH <replaceable class="parameter">name</replaceable> ALTER
+    {VERTEX|NODE|EDGE|RELATIONSHIP} TABLE <replaceable class="parameter">element_table_alias</replaceable>
+    { ADD LABEL <replaceable class="parameter">label_name</replaceable> [ NO PROPERTIES | PROPERTIES ALL COLUMNS | PROPERTIES ( { <replaceable class="parameter">expression</replaceable> [ AS <replaceable class="parameter">property_name</replaceable> ] } [, ...] ) ] } [ ... ]
+
+ALTER PROPERTY GRAPH <replaceable class="parameter">name</replaceable> ALTER
+    {VERTEX|NODE|EDGE|RELATIONSHIP} TABLE <replaceable class="parameter">element_table_alias</replaceable>
+    DROP LABEL <replaceable class="parameter">label_name</replaceable> [ CASCADE | RESTRICT ]
+
+ALTER PROPERTY GRAPH <replaceable class="parameter">name</replaceable> ALTER
+    {VERTEX|NODE|EDGE|RELATIONSHIP} TABLE <replaceable class="parameter">element_table_alias</replaceable>
+    ALTER LABEL <replaceable class="parameter">label_name</replaceable> ADD PROPERTIES ( { <replaceable class="parameter">expression</replaceable> [ AS <replaceable class="parameter">property_name</replaceable> ] } [, ...] )
+
+ALTER PROPERTY GRAPH <replaceable class="parameter">name</replaceable> ALTER
+    {VERTEX|NODE|EDGE|RELATIONSHIP} TABLE <replaceable class="parameter">element_table_alias</replaceable>
+    ALTER LABEL <replaceable class="parameter">label_name</replaceable> DROP PROPERTIES ( <replaceable class="parameter">property_name</replaceable> [, ...] ) [ CASCADE | RESTRICT ]
+
+ALTER PROPERTY GRAPH <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable class="parameter">new_owner</replaceable> | CURRENT_USER | SESSION_USER }
+ALTER PROPERTY GRAPH <replaceable class="parameter">name</replaceable> RENAME TO <replaceable class="parameter">new_name</replaceable>
+ALTER PROPERTY GRAPH [ IF EXISTS ] <replaceable class="parameter">name</replaceable> SET SCHEMA <replaceable class="parameter">new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   TODO
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of a property graph to be altered.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the property graph does not exist.  A notice is
+      issued in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>TODO</term>
+    <listitem>
+     <para>
+      TODO
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The user name of the new owner of the property graph.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name for the property graph.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for the property graph.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+ALTER PROPERTY GRAPH g1 RENAME TO g2;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   TODO
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-property-graph"/></member>
+   <member><xref linkend="sql-drop-property-graph"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b133..7b251476e24 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -47,6 +47,7 @@
   POLICY <replaceable class="parameter">policy_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   [ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
   PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
+  PROPERTY GRAPH <replaceable class="parameter">object_name</replaceable>
   PUBLICATION <replaceable class="parameter">object_name</replaceable> |
   ROLE <replaceable class="parameter">object_name</replaceable> |
   ROUTINE <replaceable class="parameter">routine_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
diff --git a/doc/src/sgml/ref/create_property_graph.sgml b/doc/src/sgml/ref/create_property_graph.sgml
new file mode 100644
index 00000000000..257186a6a95
--- /dev/null
+++ b/doc/src/sgml/ref/create_property_graph.sgml
@@ -0,0 +1,232 @@
+<!--
+doc/src/sgml/ref/create_property_graph.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-property-graph">
+ <indexterm zone="sql-create-property-graph">
+  <primary>CREATE PROPERTY GRAPH</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE PROPERTY GRAPH</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE PROPERTY GRAPH</refname>
+  <refpurpose>define an SQL-property graph</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE [ TEMP | TEMPORARY ] PROPERTY GRAPH <replaceable class="parameter">name</replaceable>
+    [ {VERTEX|NODE} TABLES ( <replaceable class="parameter">vertex_table_definition</replaceable> [, ...] ) ]
+    [ {EDGE|RELATIONSHIP} TABLES ( <replaceable class="parameter">edge_table_definition</replaceable> [, ...] ) ]
+
+<phrase>where <replaceable class="parameter">vertex_table_definition</replaceable> is:</phrase>
+
+    <replaceable class="parameter">vertex_table_name</replaceable> [ AS <replaceable class="parameter">alias</replaceable> ] [ KEY ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] [ <replaceable class="parameter">element_table_label_and_properties</replaceable> ]
+
+<phrase>and <replaceable class="parameter">edge_table_definition</replaceable> is:</phrase>
+
+    <replaceable class="parameter">edge_table_name</replaceable> [ AS <replaceable class="parameter">alias</replaceable> ] [ KEY ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ]
+        SOURCE [ KEY ( <replaceable class="parameter">column_name</replaceable> [, ...] ) REFERENCES ] <replaceable class="parameter">source_table</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ]
+        DESTINATION [ KEY ( <replaceable class="parameter">column_name</replaceable> [, ...] ) REFERENCES ] <replaceable class="parameter">dest_table</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ]
+        [ <replaceable class="parameter">element_table_label_and_properties</replaceable> ]
+
+<phrase>and <replaceable class="parameter">element_table_label_and_properties</replaceable> is either:</phrase>
+
+    NO PROPERTIES | PROPERTIES ALL COLUMNS | PROPERTIES ( { <replaceable class="parameter">expression</replaceable> [ AS <replaceable class="parameter">property_name</replaceable> ] } [, ...] )
+
+<phrase>or:</phrase>
+
+   { { LABEL <replaceable class="parameter">label</replaceable> | DEFAULT LABEL } [ NO PROPERTIES | PROPERTIES ALL COLUMNS | PROPERTIES ( { <replaceable class="parameter">expression</replaceable> [ AS <replaceable class="parameter">property_name</replaceable> ] } [, ...] ) ] } [...]
+</synopsis>
+</refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE PROPERTY GRAPH</command> defines a property graph.  A
+   property graph consists of vertices and edges, together called elements,
+   each with associated labels and properties, and can be queried using the
+   <literal>GRAPH_TABLE</literal> clause of <xref linkend="sql-select"/> with
+   a special path matching syntax.  The data in the graph is stored in regular
+   tables (or views, foreign tables, etc.).  Each vertex or edge corresponds
+   to a table.  The property graph definition links these tables together into
+   a graph structure that can be queried using graph query techniques.
+  </para>
+
+  <para>
+   <command>CREATE PROPERTY GRAPH</command> does not physically materialize a
+   graph.  It is thus similar to <command>CREATE VIEW</command> in that it
+   records a structure that is used only when the defined object is queried.
+  </para>
+
+  <para>
+   If a schema name is given (for example, <literal>CREATE PROPERTY GRAPH
+   myschema.mygraph ...</literal>) then the property graph is created in the
+   specified schema.  Otherwise it is created in the current schema.
+   Temporary property graphs exist in a special schema, so a schema name
+   cannot be given when creating a temporary property graph.  Property graphs
+   share a namespace with tables and other relation types, so the name of the
+   property graph must be distinct from the name of any other relation (table,
+   sequence, index, view, materialized view, or foreign table) in the same
+   schema.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of the new property graph.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>VERTEX</literal>/<literal>NODE</literal></term>
+    <term><literal>EDGE</literal>/<literal>RELATIONSHIP</literal></term>
+    <listitem>
+     <para>
+      These keywords are synonyms, respectively.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">vertex_table_name</replaceable></term>
+    <listitem>
+     <para>
+      The name of a table that will contain vertices in the new property
+      graph.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">edge_table_name</replaceable></term>
+    <listitem>
+     <para>
+      The name of a table that will contain edges in the new property graph.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">alias</replaceable></term>
+    <listitem>
+     <para>
+      A unique identifier for the vertex or edge table.  This defaults to the
+      name of the table.  Aliases must be unique in a property graph
+      definition (across all vertex table and edge table definitions).
+      (Therefore, if a table is used more than once as a vertex or edge table,
+      then an explicit alias must be specified for at least one of them to
+      distinguish them.)
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>KEY ( <replaceable class="parameter">column_name</replaceable> [, ...] )</literal></term>
+    <listitem>
+     <para>
+      A set of columns that uniquely identifies a row in the vertex or edge
+      table.  Defaults to the primary key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">source_table</replaceable></term>
+    <term><replaceable class="parameter">dest_table</replaceable></term>
+    <listitem>
+     <para>
+      The vertex tables that the edge table is linked to.  These refer to the
+      aliases of a the vertex table.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>KEY ( <replaceable class="parameter">column_name</replaceable> [, ...] ) REFERENCES ... ( <replaceable class="parameter">column_name</replaceable> [, ...] )</literal></term>
+    <listitem>
+     <para>
+      Two sets of columns that connect the edge table and the source or
+      destination vertex table, like in a foreign-key relationship.  If a
+      foreign-key constraint between the two tables exists, it is used by
+      default.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">element_table_label_and_properties</replaceable></term>
+    <listitem>
+     <para>
+      Defines the labels and properties for the element (vertex or edge)
+      table.  Each element has at least one label.  By default, the label is
+      the same as the element table alias.  This can be specified explicitly
+      as <literal>DEFAULT LABEL</literal>.  Alternatively, one or more freely
+      chosen label names can be specified.  (Label names do not have to be
+      unique across a property graph.  It is can be useful to assign the same
+      label to different elements.)  Each label has a list (possibly empty) of
+      properties.  By default, all columns of a table are automatically
+      exposed as properties.  This can be specified explicitly as
+      <literal>PROPERTIES ALL COLUMNS</literal>.  Alternatively, a list of
+      expressions, which can refer to the columns of the underlying table, can
+      be specified as properties.  If the expressions are not a plain column
+      reference, then an explicit property name must also be specified.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Notes</title>
+
+  <para>
+   Property graphs are queried using the <literal>GRAPH_TABLE</literal> clause
+   of <xref linkend="sql-select"/>.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+CREATE PROPERTY GRAPH g1
+    VERTEX TABLES (v1, v2, v3)
+    EDGE TABLES (e1 SOURCE v1 DESTINATION v2,
+                 e2 SOURCE v1 DESTINATION v3);
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>CREATE PROPERTY GRAPH</command> conforms to ISO/IEC 9075-16
+   (SQL/PGQ).
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-property-graph"/></member>
+   <member><xref linkend="sql-drop-property-graph"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/drop_property_graph.sgml b/doc/src/sgml/ref/drop_property_graph.sgml
new file mode 100644
index 00000000000..31cb77a2af1
--- /dev/null
+++ b/doc/src/sgml/ref/drop_property_graph.sgml
@@ -0,0 +1,111 @@
+<!--
+doc/src/sgml/ref/drop_property_graph.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-property-graph">
+ <indexterm zone="sql-drop-property-graph">
+  <primary>DROP PROPERTY GRAPH</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP PROPERTY GRAPH</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP PROPERTY GRAPH</refname>
+  <refpurpose>remove an SQL-property graph</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP PROPERTY GRAPH [ IF EXISTS ] <replaceable class="parameter">name</replaceable> [, ...] [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP PROPERTY GRAPH</command> drops an existing property graph.
+   To execute this command you must be the owner of the property graph.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><literal>IF EXISTS</literal></term>
+    <listitem>
+     <para>
+      Do not throw an error if the property graph does not exist.  A notice is
+      issued in this case.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of the property graph to remove.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>CASCADE</literal></term>
+    <listitem>
+     <para>
+      Automatically drop objects that depend on the property graph, and in
+      turn all objects that depend on those objects (see <xref
+      linkend="ddl-depend"/>).
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><literal>RESTRICT</literal></term>
+    <listitem>
+     <para>
+      Refuse to drop the property graph if any objects depend on it.  This is
+      the default.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP PROPERTY GRAPH g1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   <command>DROP PROPERTY GRAPH</command> conforms to ISO/IEC 9075-16
+   (SQL/PGQ), except that the standard only allows one property graph to be
+   dropped per command, and apart from the <literal>IF EXISTS</literal>
+   option, which is a <productname>PostgreSQL</productname> extension..
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-property-graph"/></member>
+   <member><xref linkend="sql-alter-property-graph"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 9d27b7fcde5..35a3a0234ec 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -82,6 +82,11 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
+GRANT { SELECT | ALL [ PRIVILEGES ] }
+    ON PROPERTY GRAPH <replaceable>graph_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
 GRANT { { CREATE | USAGE } [, ...] | ALL [ PRIVILEGES ] }
     ON SCHEMA <replaceable>schema_name</replaceable> [, ...]
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
@@ -119,7 +124,7 @@ <title>Description</title>
    that grants privileges on a database object (table, column, view,
    foreign table, sequence, database, foreign-data wrapper, foreign server,
    function, procedure, procedural language, large object, configuration
-   parameter, schema, tablespace, or type), and one that grants
+   parameter, property graph, schema, tablespace, or type), and one that grants
    membership in a role.  These variants are similar in many ways, but
    they are different enough to be described separately.
   </para>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cc7d797159b..c613f16b437 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1218,7 +1218,7 @@ <title>Meta-Commands</title>
 
         <listitem>
         <para>
-        For each relation (table, view, materialized view, index, sequence,
+        For each relation (table, view, materialized view, index, property graph, sequence,
         or foreign table)
         or composite type matching the
         <replaceable class="parameter">pattern</replaceable>, show all
@@ -1258,9 +1258,9 @@ <title>Meta-Commands</title>
         <para>
         If <command>\d</command> is used without a
         <replaceable class="parameter">pattern</replaceable> argument, it is
-        equivalent to <command>\dtvmsE</command> which will show a list of
-        all visible tables, views, materialized views, sequences and
-        foreign tables.
+        equivalent to <command>\dtvmsEG</command> which will show a list of
+        all visible tables, views, materialized views, sequences,
+        foreign tables, and property graphs.
         This is purely a convenience measure.
         </para>
         </note>
@@ -1528,6 +1528,7 @@ <title>Meta-Commands</title>
 
       <varlistentry id="app-psql-meta-command-de">
         <term><literal>\dE[S+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <term><literal>\dG[S+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <term><literal>\di[S+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <term><literal>\dm[S+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <term><literal>\ds[S+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
@@ -1536,10 +1537,10 @@ <title>Meta-Commands</title>
 
         <listitem>
         <para>
-        In this group of commands, the letters <literal>E</literal>,
+        In this group of commands, the letters <literal>E</literal>, <literal>G</literal>,
         <literal>i</literal>, <literal>m</literal>, <literal>s</literal>,
         <literal>t</literal>, and <literal>v</literal>
-        stand for foreign table, index, materialized view,
+        stand for foreign table, index, property graph, materialized view,
         sequence, table, and view,
         respectively.
         You can specify any or all of
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index 2db66bbf378..8f43a1bbd71 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -104,6 +104,13 @@
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
     [ CASCADE | RESTRICT ]
 
+REVOKE [ GRANT OPTION FOR ]
+    { SELECT | ALL [ PRIVILEGES ] }
+    ON PROPERTY GRAPH <replaceable>graph_name</replaceable> [, ...]
+    FROM <replaceable class="parameter">role_specification</replaceable> [, ...]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+    [ CASCADE | RESTRICT ]
+
 REVOKE [ GRANT OPTION FOR ]
     { { CREATE | USAGE } [, ...] | ALL [ PRIVILEGES ] }
     ON SCHEMA <replaceable>schema_name</replaceable> [, ...]
diff --git a/doc/src/sgml/ref/security_label.sgml b/doc/src/sgml/ref/security_label.sgml
index 5f96b7e1ded..f9e35ba9fc1 100644
--- a/doc/src/sgml/ref/security_label.sgml
+++ b/doc/src/sgml/ref/security_label.sgml
@@ -35,6 +35,7 @@
   MATERIALIZED VIEW <replaceable class="parameter">object_name</replaceable> |
   [ PROCEDURAL ] LANGUAGE <replaceable class="parameter">object_name</replaceable> |
   PROCEDURE <replaceable class="parameter">procedure_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
+  PROPERTY GRAPH <replaceable class="parameter">object_name</replaceable>
   PUBLICATION <replaceable class="parameter">object_name</replaceable> |
   ROLE <replaceable class="parameter">object_name</replaceable> |
   ROUTINE <replaceable class="parameter">routine_name</replaceable> [ ( [ [ <replaceable class="parameter">argmode</replaceable> ] [ <replaceable class="parameter">argname</replaceable> ] <replaceable class="parameter">argtype</replaceable> [, ...] ] ) ] |
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 9917df7839b..2ab3eecb80f 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -59,6 +59,7 @@
     [ LATERAL ] <replaceable class="parameter">function_name</replaceable> ( [ <replaceable class="parameter">argument</replaceable> [, ...] ] ) AS ( <replaceable class="parameter">column_definition</replaceable> [, ...] )
     [ LATERAL ] ROWS FROM( <replaceable class="parameter">function_name</replaceable> ( [ <replaceable class="parameter">argument</replaceable> [, ...] ] ) [ AS ( <replaceable class="parameter">column_definition</replaceable> [, ...] ) ] [, ...] )
                 [ WITH ORDINALITY ] [ [ AS ] <replaceable class="parameter">alias</replaceable> [ ( <replaceable class="parameter">column_alias</replaceable> [, ...] ) ] ]
+    GRAPH_TABLE ( <replaceable class="parameter">graph_name</replaceable> MATCH <replaceable class="parameter">graph_pattern</replaceable> COLUMNS ( { <replaceable class="parameter">expression</replaceable> [ AS <replaceable class="parameter">name</replaceable> ] } [, ...] ) ) [ [ AS ] <replaceable class="parameter">alias</replaceable> [ ( <replaceable class="parameter">column_alias</replaceable> [, ...] ) ] ]
     <replaceable class="parameter">from_item</replaceable> <replaceable class="parameter">join_type</replaceable> <replaceable class="parameter">from_item</replaceable> { ON <replaceable class="parameter">join_condition</replaceable> | USING ( <replaceable class="parameter">join_column</replaceable> [, ...] ) [ AS <replaceable class="parameter">join_using_alias</replaceable> ] }
     <replaceable class="parameter">from_item</replaceable> NATURAL <replaceable class="parameter">join_type</replaceable> <replaceable class="parameter">from_item</replaceable>
     <replaceable class="parameter">from_item</replaceable> CROSS JOIN <replaceable class="parameter">from_item</replaceable>
@@ -587,6 +588,48 @@ <title><literal>FROM</literal> Clause</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><literal>GRAPH_TABLE ( <replaceable class="parameter">graph_name</replaceable> MATCH <replaceable class="parameter">graph_pattern</replaceable> COLUMNS ( { <replaceable class="parameter">expression</replaceable> [ AS <replaceable class="parameter">name</replaceable> ] } [, ...] ) )</literal></term>
+      <listitem>
+       <para>
+        This clause produces output from matching the specifying graph pattern
+        against a property graph.  See <xref linkend="ddl-property-graphs"/>
+        and <xref linkend="queries-graph"/> for more information.
+       </para>
+
+       <para>
+        <replaceable class="parameter">graph_name</replaceable> is the name
+        (optionally schema-qualified) of an existing property graph (defined
+        with <xref linkend="sql-create-property-graph"/>).
+       </para>
+
+       <para>
+        <replaceable class="parameter">graph_pattern</replaceable> is a graph
+        pattern in a special graph pattern sublanguage.  See <xref
+        linkend="queries-graph-patterns"/>.
+       </para>
+
+       <para>
+        The <literal>COLUMNS</literal> clause defines the output columns of
+        the <literal>GRAPH_TABLE</literal> clause.  <replaceable
+        class="parameter">expression</replaceable> is a scalar expression
+        using the graph pattern variables defined in the <replaceable
+        class="parameter">graph_pattern</replaceable>.  The name of the output
+        columns are specified using the <literal>AS</literal> clauses.  If the
+        expressions are simple property references, the property names are
+        used as the output names, otherwise an explicit name must be
+        specified.
+       </para>
+
+       <para>
+        Like for other <literal>FROM</literal> clause items, a table alias
+        name and column alias names may follow the <literal>GRAPH_TABLE
+        (...)</literal> clause.  (A column alias list would be redundant with
+        the <literal>COLUMNS</literal> clause.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><replaceable class="parameter">join_type</replaceable></term>
       <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index aa94f6adf6a..d3fb7836088 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -55,6 +55,7 @@ <title>SQL Commands</title>
    &alterOperatorFamily;
    &alterPolicy;
    &alterProcedure;
+   &alterPropertyGraph;
    &alterPublication;
    &alterRole;
    &alterRoutine;
@@ -107,6 +108,7 @@ <title>SQL Commands</title>
    &createOperatorFamily;
    &createPolicy;
    &createProcedure;
+   &createPropertyGraph;
    &createPublication;
    &createRole;
    &createRule;
@@ -155,6 +157,7 @@ <title>SQL Commands</title>
    &dropOwned;
    &dropPolicy;
    &dropProcedure;
+   &dropPropertyGraph;
    &dropPublication;
    &dropRole;
    &dropRoutine;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 196ecafc909..e7fc979fc22 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -118,7 +118,10 @@ CATALOG_HEADERS := \
 	pg_publication_namespace.h \
 	pg_publication_rel.h \
 	pg_subscription.h \
-	pg_subscription_rel.h
+	pg_subscription_rel.h \
+	pg_propgraph_element.h \
+	pg_propgraph_label.h \
+	pg_propgraph_property.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h syscache_ids.h syscache_info.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 1e44a71f61c..b9328fbe135 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -307,6 +307,9 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_PARAMETER_ACL:
 			whole_mask = ACL_ALL_RIGHTS_PARAMETER_ACL;
 			break;
+		case OBJECT_PROPGRAPH:
+			whole_mask = ACL_ALL_RIGHTS_PROPGRAPH;
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d", objtype);
 			/* not reached, but keep compiler quiet */
@@ -551,6 +554,10 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_PARAMETER_ACL;
 			errormsg = gettext_noop("invalid privilege type %s for parameter");
 			break;
+		case OBJECT_PROPGRAPH:
+			all_privileges = ACL_ALL_RIGHTS_PROPGRAPH;
+			errormsg = gettext_noop("invalid privilege type %s for property graph");
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) stmt->objtype);
@@ -621,6 +628,7 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 	{
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
+		case OBJECT_PROPGRAPH:
 			ExecGrant_Relation(istmt);
 			break;
 		case OBJECT_DATABASE:
@@ -693,6 +701,7 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 	{
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
+		case OBJECT_PROPGRAPH:
 			foreach(cell, objnames)
 			{
 				RangeVar   *relvar = (RangeVar *) lfirst(cell);
@@ -893,6 +902,10 @@ objectsInSchemaToOids(ObjectType objtype, List *nspnames)
 				objs = getRelationsInNamespace(namespaceId, RELKIND_SEQUENCE);
 				objects = list_concat(objects, objs);
 				break;
+			case OBJECT_PROPGRAPH:
+				objs = getRelationsInNamespace(namespaceId, RELKIND_PROPGRAPH);
+				objects = list_concat(objects, objs);
+				break;
 			case OBJECT_FUNCTION:
 			case OBJECT_PROCEDURE:
 			case OBJECT_ROUTINE:
@@ -1094,6 +1107,10 @@ ExecAlterDefaultPrivilegesStmt(ParseState *pstate, AlterDefaultPrivilegesStmt *s
 			all_privileges = ACL_ALL_RIGHTS_SCHEMA;
 			errormsg = gettext_noop("invalid privilege type %s for schema");
 			break;
+		case OBJECT_PROPGRAPH:
+			all_privileges = ACL_ALL_RIGHTS_PROPGRAPH;
+			errormsg = gettext_noop("invalid privilege type %s for property graph");
+			break;
 		default:
 			elog(ERROR, "unrecognized GrantStmt.objtype: %d",
 				 (int) action->objtype);
@@ -2778,6 +2795,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_PROCEDURE:
 						msg = gettext_noop("permission denied for procedure %s");
 						break;
+					case OBJECT_PROPGRAPH:
+						msg = gettext_noop("permission denied for property graph %s");
+						break;
 					case OBJECT_PUBLICATION:
 						msg = gettext_noop("permission denied for publication %s");
 						break;
@@ -2904,6 +2924,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_PROCEDURE:
 						msg = gettext_noop("must be owner of procedure %s");
 						break;
+					case OBJECT_PROPGRAPH:
+						msg = gettext_noop("must be owner of property graph %s");
+						break;
 					case OBJECT_PUBLICATION:
 						msg = gettext_noop("must be owner of publication %s");
 						break;
@@ -3040,6 +3063,7 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid,
 				pg_attribute_aclmask(object_oid, attnum, roleid, mask, how);
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
+		case OBJECT_PROPGRAPH:
 			return pg_class_aclmask(object_oid, roleid, mask, how);
 		case OBJECT_DATABASE:
 			return object_aclmask(DatabaseRelationId, object_oid, roleid, mask, how);
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index e742c78ea35..736410af776 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -50,6 +50,9 @@
 #include "catalog/pg_parameter_acl.h"
 #include "catalog/pg_policy.h"
 #include "catalog/pg_proc.h"
+#include "catalog/pg_propgraph_element.h"
+#include "catalog/pg_propgraph_label.h"
+#include "catalog/pg_propgraph_property.h"
 #include "catalog/pg_publication.h"
 #include "catalog/pg_publication_namespace.h"
 #include "catalog/pg_publication_rel.h"
@@ -70,6 +73,7 @@
 #include "commands/event_trigger.h"
 #include "commands/extension.h"
 #include "commands/policy.h"
+#include "commands/propgraphcmds.h"
 #include "commands/publicationcmds.h"
 #include "commands/seclabel.h"
 #include "commands/sequence.h"
@@ -184,6 +188,9 @@ static const Oid object_classes[] = {
 	EventTriggerRelationId,		/* OCLASS_EVENT_TRIGGER */
 	ParameterAclRelationId,		/* OCLASS_PARAMETER_ACL */
 	PolicyRelationId,			/* OCLASS_POLICY */
+	PropgraphElementRelationId, /* OCLASS_PROPGRAPH_ELEMENT */
+	PropgraphLabelRelationId,	/* OCLASS_PROPGRAPH_LABEL */
+	PropgraphPropertyRelationId,	/* OCLASS_PROPGRAPH_PROPERTY */
 	PublicationNamespaceRelationId, /* OCLASS_PUBLICATION_NAMESPACE */
 	PublicationRelationId,		/* OCLASS_PUBLICATION */
 	PublicationRelRelationId,	/* OCLASS_PUBLICATION_REL */
@@ -1500,6 +1507,9 @@ doDeletion(const ObjectAddress *object, int flags)
 		case OCLASS_AM:
 		case OCLASS_AMOP:
 		case OCLASS_AMPROC:
+		case OCLASS_PROPGRAPH_ELEMENT:
+		case OCLASS_PROPGRAPH_LABEL:
+		case OCLASS_PROPGRAPH_PROPERTY:
 		case OCLASS_SCHEMA:
 		case OCLASS_TSPARSER:
 		case OCLASS_TSDICT:
@@ -2212,6 +2222,7 @@ find_expr_references_walker(Node *node,
 			switch (rte->rtekind)
 			{
 				case RTE_RELATION:
+				case RTE_GRAPH_TABLE:
 					add_object_address(OCLASS_CLASS, rte->relid, 0,
 									   context->addrs);
 					break;
@@ -2952,6 +2963,15 @@ getObjectClass(const ObjectAddress *object)
 		case PolicyRelationId:
 			return OCLASS_POLICY;
 
+		case PropgraphElementRelationId:
+			return OCLASS_PROPGRAPH_ELEMENT;
+
+		case PropgraphLabelRelationId:
+			return OCLASS_PROPGRAPH_LABEL;
+
+		case PropgraphPropertyRelationId:
+			return OCLASS_PROPGRAPH_PROPERTY;
+
 		case PublicationNamespaceRelationId:
 			return OCLASS_PUBLICATION_NAMESPACE;
 
diff --git a/src/backend/catalog/information_schema.sql b/src/backend/catalog/information_schema.sql
index c402ae72741..aab28fd708c 100644
--- a/src/backend/catalog/information_schema.sql
+++ b/src/backend/catalog/information_schema.sql
@@ -3009,3 +3009,366 @@ CREATE VIEW user_mappings AS
     FROM _pg_user_mappings;
 
 GRANT SELECT ON user_mappings TO PUBLIC;
+
+
+-- SQL/PGQ views; these use section numbers from part 16 of the standard.
+
+/*
+ * 15.2
+ * PG_DEFINED_LABEL_SETS view
+ */
+
+-- TODO
+
+
+/*
+ * 15.3
+ * PG_DEFINED_LABEL_SET_LABELS view
+ */
+
+-- TODO
+
+
+/*
+ * 15.4
+ * PG_EDGE_DEFINED_LABEL_SETS view
+ */
+
+-- TODO
+
+
+/*
+ * 15.5
+ * PG_EDGE_TABLE_COMPONENTS view
+ */
+
+CREATE VIEW pg_edge_table_components AS
+    SELECT CAST(current_database() AS sql_identifier) AS property_graph_catalog,
+           CAST(npg.nspname AS sql_identifier) AS property_graph_schema,
+           CAST(pg.relname AS sql_identifier) AS property_graph_name,
+           CAST(eg.pgealias AS sql_identifier) AS edge_table_alias,
+           CAST(v.pgealias AS sql_identifier) AS vertex_table_alias,
+           CAST(CASE eg.end WHEN 'src' THEN 'SOURCE' WHEN 'dest' THEN 'DESTINATION' END AS character_data) AS edge_end,
+           CAST(ae.attname AS sql_identifier) AS edge_table_column_name,
+           CAST(av.attname AS sql_identifier) AS vertex_table_column_name,
+           CAST((eg.egkey).n AS cardinal_number) AS ordinal_position
+    FROM pg_namespace npg
+         JOIN
+         (SELECT * FROM pg_class WHERE relkind = 'g') AS pg
+           ON npg.oid = pg.relnamespace
+         JOIN
+         (SELECT pgepgid, pgealias, pgerelid, 'src' AS end, pgesrcvertexid AS vertexid, _pg_expandarray(pgesrckey) AS egkey, _pg_expandarray(pgesrcref) AS egref FROM pg_propgraph_element WHERE pgekind = 'e'
+          UNION ALL
+          SELECT pgepgid, pgealias, pgerelid, 'dest' AS end, pgedestvertexid AS vertexid, _pg_expandarray(pgedestkey) AS egkey, _pg_expandarray(pgedestref) AS egref FROM pg_propgraph_element WHERE pgekind = 'e'
+         ) AS eg
+           ON pg.oid = eg.pgepgid
+         JOIN
+         (SELECT * FROM pg_propgraph_element WHERE pgekind = 'v') AS v
+           ON eg.vertexid = v.oid
+         JOIN
+         (SELECT * FROM pg_attribute WHERE NOT attisdropped) AS ae
+           ON eg.pgerelid = ae.attrelid AND (eg.egkey).x = ae.attnum
+         JOIN
+         (SELECT * FROM pg_attribute WHERE NOT attisdropped) AS av
+           ON v.pgerelid = av.attrelid AND (eg.egref).x = av.attnum
+    WHERE NOT pg_is_other_temp_schema(npg.oid)
+          AND (pg_has_role(pg.relowner, 'USAGE')
+               OR has_table_privilege(pg.oid, 'SELECT'));
+
+GRANT SELECT ON pg_edge_table_components TO PUBLIC;
+
+
+/*
+ * 15.6
+ * PG_EDGE_TRIPLETS view
+ */
+
+-- TODO
+
+
+/*
+ * 15.7
+ * PG_ELEMENT_TABLE_KEY_COLUMNS view
+ */
+
+CREATE VIEW pg_element_table_key_columns AS
+    SELECT CAST(current_database() AS sql_identifier) AS property_graph_catalog,
+           CAST(npg.nspname AS sql_identifier) AS property_graph_schema,
+           CAST(pg.relname AS sql_identifier) AS property_graph_name,
+           CAST(pgealias AS sql_identifier) AS element_table_alias,
+           CAST(a.attname AS sql_identifier) AS column_name,
+           CAST((el.ekey).n AS cardinal_number) AS ordinal_position
+    FROM pg_namespace npg
+         JOIN
+         (SELECT * FROM pg_class WHERE relkind = 'g') AS pg
+           ON npg.oid = pg.relnamespace
+         JOIN
+         (SELECT pgepgid, pgealias, pgerelid, _pg_expandarray(pgekey) AS ekey FROM pg_propgraph_element) AS el
+           ON pg.oid = el.pgepgid
+         JOIN
+         (SELECT * FROM pg_attribute WHERE NOT attisdropped) AS a
+           ON el.pgerelid = a.attrelid AND (el.ekey).x = a.attnum
+    WHERE NOT pg_is_other_temp_schema(npg.oid)
+          AND (pg_has_role(pg.relowner, 'USAGE')
+               OR has_table_privilege(pg.oid, 'SELECT'));
+
+GRANT SELECT ON pg_element_table_key_columns TO PUBLIC;
+
+
+/*
+ * 15.8
+ * PG_ELEMENT_TABLE_LABELS view
+ */
+
+CREATE VIEW pg_element_table_labels AS
+    SELECT CAST(current_database() AS sql_identifier) AS property_graph_catalog,
+           CAST(npg.nspname AS sql_identifier) AS property_graph_schema,
+           CAST(pg.relname AS sql_identifier) AS property_graph_name,
+           CAST(e.pgealias AS sql_identifier) AS element_table_alias,
+           CAST(l.pgllabel AS sql_identifier) AS label_name
+    FROM pg_namespace npg, pg_class pg, pg_propgraph_element e, pg_propgraph_label l
+    WHERE pg.relnamespace = npg.oid
+          AND e.pgepgid = pg.oid
+          AND l.pglelid = e.oid
+          AND pg.relkind = 'g'
+          AND (NOT pg_is_other_temp_schema(npg.oid))
+          AND (pg_has_role(pg.relowner, 'USAGE')
+               OR has_table_privilege(pg.oid, 'SELECT'));
+
+GRANT SELECT ON pg_element_table_labels TO PUBLIC;
+
+
+/*
+ * 15.9
+ * PG_ELEMENT_TABLE_PROPERTIES view
+ */
+
+CREATE VIEW pg_element_table_properties AS
+    SELECT DISTINCT
+           CAST(current_database() AS sql_identifier) AS property_graph_catalog,
+           CAST(npg.nspname AS sql_identifier) AS property_graph_schema,
+           CAST(pg.relname AS sql_identifier) AS property_graph_name,
+           CAST(e.pgealias AS sql_identifier) AS element_table_alias,
+           CAST(pr.pgpname AS sql_identifier) AS property_name,
+           CAST(pg_get_expr(pr.pgpexpr, e.pgerelid) AS character_data) AS property_expression
+    FROM pg_namespace npg, pg_class pg, pg_propgraph_element e, pg_propgraph_label l, pg_propgraph_property pr
+    WHERE pg.relnamespace = npg.oid
+          AND e.pgepgid = pg.oid
+          AND l.pglelid = e.oid
+          AND pr.pgplabelid = l.oid
+          AND pg.relkind = 'g'
+          AND (NOT pg_is_other_temp_schema(npg.oid))
+          AND (pg_has_role(pg.relowner, 'USAGE')
+               OR has_table_privilege(pg.oid, 'SELECT'));
+
+GRANT SELECT ON pg_element_table_properties TO PUBLIC;
+
+
+/*
+ * 15.10
+ * PG_ELEMENT_TABLES view
+ */
+
+CREATE VIEW pg_element_tables AS
+    SELECT CAST(current_database() AS sql_identifier) AS property_graph_catalog,
+           CAST(npg.nspname AS sql_identifier) AS property_graph_schema,
+           CAST(pg.relname AS sql_identifier) AS property_graph_name,
+           CAST(e.pgealias AS sql_identifier) AS element_table_alias,
+           CAST(CASE e.pgekind WHEN 'e' THEN 'EDGE' WHEN 'v' THEN 'VERTEX' END AS character_data) AS element_table_kind,
+           CAST(current_database() AS sql_identifier) AS table_catalog,
+           CAST(nt.nspname AS sql_identifier) AS table_schema,
+           CAST(t.relname AS sql_identifier) AS table_name,
+           CAST(NULL AS character_data) AS element_table_definition
+    FROM pg_namespace npg, pg_class pg, pg_propgraph_element e, pg_class t, pg_namespace nt
+    WHERE pg.relnamespace = npg.oid
+          AND e.pgepgid = pg.oid
+          AND e.pgerelid = t.oid
+          AND t.relnamespace = nt.oid
+          AND pg.relkind = 'g'
+          AND (NOT pg_is_other_temp_schema(npg.oid))
+          AND (pg_has_role(pg.relowner, 'USAGE')
+               OR has_table_privilege(pg.oid, 'SELECT'));
+
+GRANT SELECT ON pg_element_tables TO PUBLIC;
+
+
+/*
+ * 15.11
+ * PG_LABEL_PROPERTIES view
+ */
+
+CREATE VIEW pg_label_properties AS
+    SELECT DISTINCT
+           CAST(current_database() AS sql_identifier) AS property_graph_catalog,
+           CAST(npg.nspname AS sql_identifier) AS property_graph_schema,
+           CAST(pg.relname AS sql_identifier) AS property_graph_name,
+           CAST(l.pgllabel AS sql_identifier) AS label_name,
+           CAST(pr.pgpname AS sql_identifier) AS property_name
+    FROM pg_namespace npg, pg_class pg, pg_propgraph_element e, pg_propgraph_label l, pg_propgraph_property pr
+    WHERE pg.relnamespace = npg.oid
+          AND e.pgepgid = pg.oid
+          AND l.pglelid = e.oid
+          AND pr.pgplabelid = l.oid
+          AND pg.relkind = 'g'
+          AND (NOT pg_is_other_temp_schema(npg.oid))
+          AND (pg_has_role(pg.relowner, 'USAGE')
+               OR has_table_privilege(pg.oid, 'SELECT'));
+
+GRANT SELECT ON pg_label_properties TO PUBLIC;
+
+
+/*
+ * 15.12
+ * PG_LABELS view
+ */
+
+CREATE VIEW pg_labels AS
+    SELECT DISTINCT
+           CAST(current_database() AS sql_identifier) AS property_graph_catalog,
+           CAST(npg.nspname AS sql_identifier) AS property_graph_schema,
+           CAST(pg.relname AS sql_identifier) AS property_graph_name,
+           CAST(l.pgllabel AS sql_identifier) AS label_name
+    FROM pg_namespace npg, pg_class pg, pg_propgraph_element e, pg_propgraph_label l
+    WHERE pg.relnamespace = npg.oid
+          AND e.pgepgid = pg.oid
+          AND l.pglelid = e.oid
+          AND pg.relkind = 'g'
+          AND (NOT pg_is_other_temp_schema(npg.oid))
+          AND (pg_has_role(pg.relowner, 'USAGE')
+               OR has_table_privilege(pg.oid, 'SELECT'));
+
+GRANT SELECT ON pg_labels TO PUBLIC;
+
+
+/*
+ * 15.13
+ * PG_PROPERTY_DATA_TYPES view
+ */
+
+CREATE VIEW pg_property_data_types AS
+    SELECT DISTINCT
+           CAST(current_database() AS sql_identifier) AS property_graph_catalog,
+           CAST(npg.nspname AS sql_identifier) AS property_graph_schema,
+           CAST(pg.relname AS sql_identifier) AS property_graph_name,
+           CAST(pgp.pgpname AS sql_identifier) AS property_name,
+
+           CAST(
+             CASE WHEN t.typtype = 'd' THEN
+               CASE WHEN bt.typelem <> 0 AND bt.typlen = -1 THEN 'ARRAY'
+                    WHEN nbt.nspname = 'pg_catalog' THEN format_type(t.typbasetype, null)
+                    ELSE 'USER-DEFINED' END
+             ELSE
+               CASE WHEN t.typelem <> 0 AND t.typlen = -1 THEN 'ARRAY'
+                    WHEN nt.nspname = 'pg_catalog' THEN format_type(pgp.pgptypid, null)
+                    ELSE 'USER-DEFINED' END
+             END
+             AS character_data)
+             AS data_type,
+
+           CAST(null AS cardinal_number) AS character_maximum_length,
+           CAST(null AS cardinal_number) AS character_octet_length,
+           CAST(null AS sql_identifier) AS character_set_catalog,
+           CAST(null AS sql_identifier) AS character_set_schema,
+           CAST(null AS sql_identifier) AS character_set_name,
+           CAST(null AS sql_identifier) AS collation_catalog, -- FIXME
+           CAST(null AS sql_identifier) AS collation_schema, -- FIXME
+           CAST(null AS sql_identifier) AS collation_name, -- FIXME
+           CAST(null AS cardinal_number) AS numeric_precision,
+           CAST(null AS cardinal_number) AS numeric_precision_radix,
+           CAST(null AS cardinal_number) AS numeric_scale,
+           CAST(null AS cardinal_number) AS datetime_precision,
+           CAST(null AS character_data) AS interval_type,
+           CAST(null AS cardinal_number) AS interval_precision,
+
+           CAST(current_database() AS sql_identifier) AS user_defined_type_catalog,
+           CAST(coalesce(nbt.nspname, nt.nspname) AS sql_identifier) AS user_defined_type_schema,
+           CAST(coalesce(bt.typname, t.typname) AS sql_identifier) AS user_defined_type_name,
+
+           CAST(null AS sql_identifier) AS scope_catalog,
+           CAST(null AS sql_identifier) AS scope_schema,
+           CAST(null AS sql_identifier) AS scope_name,
+
+           CAST(null AS cardinal_number) AS maximum_cardinality,
+           CAST(pgp.pgpname AS sql_identifier) AS dtd_identifier
+
+    FROM pg_propgraph_property pgp
+         JOIN (pg_class pg JOIN pg_namespace npg ON (pg.relnamespace = npg.oid)) ON pgp.pgppgid = pg.oid
+         JOIN (pg_type t JOIN pg_namespace nt ON (t.typnamespace = nt.oid)) ON pgp.pgptypid = t.oid
+         LEFT JOIN (pg_type bt JOIN pg_namespace nbt ON (bt.typnamespace = nbt.oid))
+           ON (t.typtype = 'd' AND t.typbasetype = bt.oid)
+
+    WHERE pg.relkind = 'g'
+          AND (NOT pg_is_other_temp_schema(npg.oid))
+          AND (pg_has_role(pg.relowner, 'USAGE')
+               OR has_table_privilege(pg.oid, 'SELECT'));
+
+GRANT SELECT ON pg_property_data_types TO PUBLIC;
+
+
+/*
+ * 15.14
+ * PG_PROPERTY_GRAPH_PRIVILEGES view
+ */
+
+CREATE VIEW pg_property_graph_privileges AS
+    SELECT CAST(u_grantor.rolname AS sql_identifier) AS grantor,
+           CAST(grantee.rolname AS sql_identifier) AS grantee,
+           CAST(current_database() AS sql_identifier) AS property_graph_catalog,
+           CAST(nc.nspname AS sql_identifier) AS property_graph_schema,
+           CAST(c.relname AS sql_identifier) AS property_graph_name,
+           CAST(c.prtype AS character_data) AS privilege_type,
+           CAST(
+             CASE WHEN
+                  -- object owner always has grant options
+                  pg_has_role(grantee.oid, c.relowner, 'USAGE')
+                  OR c.grantable
+                  THEN 'YES' ELSE 'NO' END AS yes_or_no) AS is_grantable
+
+    FROM (
+            SELECT oid, relname, relnamespace, relkind, relowner, (aclexplode(coalesce(relacl, acldefault('r', relowner)))).* FROM pg_class
+         ) AS c (oid, relname, relnamespace, relkind, relowner, grantor, grantee, prtype, grantable),
+         pg_namespace nc,
+         pg_authid u_grantor,
+         (
+           SELECT oid, rolname FROM pg_authid
+           UNION ALL
+           SELECT 0::oid, 'PUBLIC'
+         ) AS grantee (oid, rolname)
+
+    WHERE c.relnamespace = nc.oid
+          AND c.relkind IN ('g')
+          AND c.grantee = grantee.oid
+          AND c.grantor = u_grantor.oid
+          AND c.prtype IN ('SELECT')
+          AND (pg_has_role(u_grantor.oid, 'USAGE')
+               OR pg_has_role(grantee.oid, 'USAGE')
+               OR grantee.rolname = 'PUBLIC');
+
+GRANT SELECT ON pg_property_graph_privileges TO PUBLIC;
+
+
+/*
+ * 15.15
+ * PG_VERTEX_DEFINED_LABEL_SETS view
+ */
+
+-- TODO
+
+
+/*
+ * 15.16
+ * PROPERTY_GRAPHS view
+ */
+
+CREATE VIEW property_graphs AS
+    SELECT CAST(current_database() AS sql_identifier) AS property_graph_catalog,
+           CAST(nc.nspname AS sql_identifier) AS property_graph_schema,
+           CAST(c.relname AS sql_identifier) AS property_graph_name
+    FROM pg_namespace nc, pg_class c
+    WHERE c.relnamespace = nc.oid
+          AND c.relkind = 'g'
+          AND (NOT pg_is_other_temp_schema(nc.oid))
+          AND (pg_has_role(c.relowner, 'USAGE')
+               OR has_table_privilege(c.oid, 'SELECT'));
+
+GRANT SELECT ON property_graphs TO PUBLIC;
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 8bb2924b9cd..bf43790d5a4 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -49,6 +49,9 @@
 #include "catalog/pg_parameter_acl.h"
 #include "catalog/pg_policy.h"
 #include "catalog/pg_proc.h"
+#include "catalog/pg_propgraph_element.h"
+#include "catalog/pg_propgraph_label.h"
+#include "catalog/pg_propgraph_property.h"
 #include "catalog/pg_publication.h"
 #include "catalog/pg_publication_namespace.h"
 #include "catalog/pg_publication_rel.h"
@@ -373,6 +376,48 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_OPFAMILY,
 		true
 	},
+	{
+		"property graph element",
+		PropgraphElementRelationId,
+		PropgraphElementObjectIndexId,
+		PROPGRAPHELOID,
+		PROPGRAPHELALIAS,
+		Anum_pg_propgraph_element_oid,
+		Anum_pg_propgraph_element_pgealias,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		-1,
+		false
+	},
+	{
+		"property graph label",
+		PropgraphLabelRelationId,
+		PropgraphLabelObjectIndexId,
+		-1,
+		PROPGRAPHLABELNAME,
+		Anum_pg_propgraph_label_oid,
+		Anum_pg_propgraph_label_pgllabel,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		-1,
+		false
+	},
+	{
+		"property graph property",
+		PropgraphPropertyRelationId,
+		PropgraphPropertyObjectIndexId,
+		-1,
+		PROPGRAPHPROPNAME,
+		Anum_pg_propgraph_property_oid,
+		Anum_pg_propgraph_property_pgpname,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		-1,
+		false
+	},
 	{
 		"role",
 		AuthIdRelationId,
@@ -683,6 +728,9 @@ static const struct object_type_map
 	{
 		"foreign table", OBJECT_FOREIGN_TABLE
 	},
+	{
+		"property graph", OBJECT_PROPGRAPH
+	},
 	{
 		"table column", OBJECT_COLUMN
 	},
@@ -852,6 +900,18 @@ static const struct object_type_map
 	{
 		"policy", OBJECT_POLICY
 	},
+	/* OCLASS_PROPGRAPH_ELEMENT */
+	{
+		"property graph element", -1
+	},
+	/* OCLASS_PROPGRAPH_LABEL */
+	{
+		"property graph label", -1
+	},
+	/* OCLASS_PROPGRAPH_PROPERTY */
+	{
+		"property graph property", -1
+	},
 	/* OCLASS_PUBLICATION */
 	{
 		"publication", OBJECT_PUBLICATION
@@ -992,6 +1052,7 @@ get_object_address(ObjectType objtype, Node *object,
 			case OBJECT_VIEW:
 			case OBJECT_MATVIEW:
 			case OBJECT_FOREIGN_TABLE:
+			case OBJECT_PROPGRAPH:
 				address =
 					get_relation_by_qualified_name(objtype, castNode(List, object),
 												   &relation, lockmode,
@@ -1400,6 +1461,13 @@ get_relation_by_qualified_name(ObjectType objtype, List *object,
 						 errmsg("\"%s\" is not an index",
 								RelationGetRelationName(relation))));
 			break;
+		case OBJECT_PROPGRAPH:
+			if (relation->rd_rel->relkind != RELKIND_PROPGRAPH)
+				ereport(ERROR,
+						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+						 errmsg("\"%s\" is not a property graph",
+								RelationGetRelationName(relation))));
+			break;
 		case OBJECT_SEQUENCE:
 			if (relation->rd_rel->relkind != RELKIND_SEQUENCE)
 				ereport(ERROR,
@@ -2315,6 +2383,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_MATVIEW:
 		case OBJECT_INDEX:
 		case OBJECT_FOREIGN_TABLE:
+		case OBJECT_PROPGRAPH:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_COLLATION:
@@ -2434,6 +2503,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 		case OBJECT_VIEW:
 		case OBJECT_MATVIEW:
 		case OBJECT_FOREIGN_TABLE:
+		case OBJECT_PROPGRAPH:
 		case OBJECT_COLUMN:
 		case OBJECT_RULE:
 		case OBJECT_TRIGGER:
@@ -3964,6 +4034,43 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_PROPGRAPH_ELEMENT:
+			{
+				HeapTuple	tup;
+				Form_pg_propgraph_element pgeform;
+				StringInfoData rel;
+
+				tup = SearchSysCache1(PROPGRAPHELOID,
+									  ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+					elog(ERROR, "cache lookup failed for property graph element %u",
+						 object->objectId);
+
+				pgeform = (Form_pg_propgraph_element) GETSTRUCT(tup);
+
+				initStringInfo(&rel);
+				getRelationDescription(&rel, pgeform->pgepgid, false);
+
+				/* translator: second %s is, e.g., "property graph %s" */
+				appendStringInfo(&buffer, _("element %s in %s"),
+								 NameStr(pgeform->pgealias), rel.data);
+				pfree(rel.data);
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_PROPGRAPH_LABEL:
+			{
+				appendStringInfo(&buffer, "label %u TODO", object->objectId);
+				break;
+			}
+
+		case OCLASS_PROPGRAPH_PROPERTY:
+			{
+				appendStringInfo(&buffer, "property %u TODO", object->objectId);
+				break;
+			}
+
 		case OCLASS_PUBLICATION:
 			{
 				char	   *pubname = get_publication_name(object->objectId,
@@ -4151,6 +4258,10 @@ getRelationDescription(StringInfo buffer, Oid relid, bool missing_ok)
 			appendStringInfo(buffer, _("foreign table %s"),
 							 relname);
 			break;
+		case RELKIND_PROPGRAPH:
+			appendStringInfo(buffer, _("property graph %s"),
+							 relname);
+			break;
 		default:
 			/* shouldn't get here */
 			appendStringInfo(buffer, _("relation %s"),
@@ -4571,6 +4682,18 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "policy");
 			break;
 
+		case OCLASS_PROPGRAPH_ELEMENT:
+			appendStringInfoString(&buffer, "property graph element");
+			break;
+
+		case OCLASS_PROPGRAPH_LABEL:
+			appendStringInfoString(&buffer, "property graph label");
+			break;
+
+		case OCLASS_PROPGRAPH_PROPERTY:
+			appendStringInfoString(&buffer, "property graph property");
+			break;
+
 		case OCLASS_PUBLICATION:
 			appendStringInfoString(&buffer, "publication");
 			break;
@@ -4654,6 +4777,9 @@ getRelationTypeDescription(StringInfo buffer, Oid relid, int32 objectSubId,
 		case RELKIND_FOREIGN_TABLE:
 			appendStringInfoString(buffer, "foreign table");
 			break;
+		case RELKIND_PROPGRAPH:
+			appendStringInfoString(buffer, "property graph");
+			break;
 		default:
 			/* shouldn't get here */
 			appendStringInfoString(buffer, "relation");
@@ -5813,6 +5939,18 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_PROPGRAPH_ELEMENT:
+			appendStringInfo(&buffer, "%u TODO", object->objectId);
+			break;
+
+		case OCLASS_PROPGRAPH_LABEL:
+			appendStringInfo(&buffer, "%u TODO", object->objectId);
+			break;
+
+		case OCLASS_PROPGRAPH_PROPERTY:
+			appendStringInfo(&buffer, "%u TODO", object->objectId);
+			break;
+
 		case OCLASS_PUBLICATION:
 			{
 				char	   *pubname;
@@ -6121,6 +6259,8 @@ get_relkind_objtype(char relkind)
 			return OBJECT_MATVIEW;
 		case RELKIND_FOREIGN_TABLE:
 			return OBJECT_FOREIGN_TABLE;
+		case RELKIND_PROPGRAPH:
+			return OBJECT_PROPGRAPH;
 		case RELKIND_TOASTVALUE:
 			return OBJECT_TABLE;
 		default:
diff --git a/src/backend/catalog/pg_class.c b/src/backend/catalog/pg_class.c
index e05b0bbb2e0..748f06b3ff4 100644
--- a/src/backend/catalog/pg_class.c
+++ b/src/backend/catalog/pg_class.c
@@ -45,6 +45,8 @@ errdetail_relkind_not_supported(char relkind)
 			return errdetail("This operation is not supported for partitioned tables.");
 		case RELKIND_PARTITIONED_INDEX:
 			return errdetail("This operation is not supported for partitioned indexes.");
+		case RELKIND_PROPGRAPH:
+			return errdetail("This operation is not supported for property graphs.");
 		default:
 			elog(ERROR, "unrecognized relkind: '%c'", relkind);
 			return 0;
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 4085a2d18d2..fbfca2068d8 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -349,6 +349,106 @@ F866	FETCH FIRST clause: PERCENT option			NO
 F867	FETCH FIRST clause: WITH TIES option			YES	
 F868	ORDER BY in grouped table			YES	
 F869	SQL implementation info population			YES	
+G000	Graph pattern			YES	SQL/PGQ required
+G001	Repeatable-elements match mode			YES	SQL/PGQ required XXX
+G002	Different-edges match mode			NO	
+G003	Explicit REPEATABLE ELEMENTS keyword			NO	
+G004	Path variables			NO	
+G005	Path search prefix in a path pattern			NO	
+G006	Graph pattern KEEP clause: path mode prefix			NO	
+G007	Graph pattern KEEP clause: path search prefix			NO	
+G008	Graph pattern WHERE clause			YES	SQL/PGQ required
+G010	Explicit WALK keyword			NO	
+G011	Advanced path modes: TRAIL			NO	
+G012	Advanced path modes: SIMPLE			NO	
+G013	Advanced path modes: ACYCLIC			NO	
+G014	Explicit PATH/PATHS keywords			NO	
+G015	All path search: explicit ALL keyword			NO	
+G016	Any path search			NO	
+G017	All shortest path search			NO	
+G018	Any shortest path search			NO	
+G019	Counted shortest path search			NO	
+G020	Counted shortest group search			NO	
+G030	Path multiset alternation			NO	
+G031	Path multiset alternation: variable length path operands			NO	
+G032	Path pattern union			NO	
+G033	Path pattern union: variable length path operands			NO	
+G034	Path concatenation			YES	SQL/PGQ required
+G035	Quantified paths			NO	
+G036	Quantified edges			NO	
+G037	Questioned paths			NO	
+G038	Parenthesized path pattern expression			NO	
+G039	Simplified path pattern expression: full defaulting			NO	
+G040	Vertex pattern			YES	SQL/PGQ required
+G041	Non-local element pattern predicates			NO	
+G042	Basic full edge patterns			YES	SQL/PGQ required
+G043	Complete full edge patterns			NO	
+G044	Basic abbreviated edge patterns			YES	
+G045	Complete abbreviated edge patterns			NO	
+G046	Relaxed topological consistency: adjacent vertex patterns			NO	
+G047	Relaxed topological consistency: concise edge patterns			NO	
+G048	Parenthesized path pattern: subpath variable declaration			NO	
+G049	Parenthesized path pattern: path mode prefix			NO	
+G050	Parenthesized path pattern: WHERE clause			NO	
+G051	Parenthesized path pattern: non-local predicates			NO	
+G060	Bounded graph pattern quantifiers			NO	
+G061	Unbounded graph pattern quantifiers			NO	
+G070	Label expression: label disjunction			NO	SQL/PGQ required
+G071	Label expression: label conjunction			NO	
+G072	Label expression: label negation			NO	
+G073	Label expression: individual label name			YES	SQL/PGQ required
+G074	Label expression: wildcard label			NO	
+G075	Parenthesized label expression			NO	
+G080	Simplified path pattern expression: basic defaulting			NO	
+G081	Simplified path pattern expression: full overrides			NO	
+G082	Simplified path pattern expression: basic overrides			NO	
+G090	Property reference			YES	SQL/PGQ required
+G100	ELEMENT_ID function			NO	
+G110	IS DIRECTED predicate			NO	
+G111	IS LABELED predicate			NO	
+G112	IS SOURCE and IS DESTINATION predicate			NO	
+G113	ALL_DIFFERENT predicate			NO	
+G114	SAME predicate			NO	
+G115	PROPERTY_EXISTS predicate			NO	
+G120	Within-match aggregates			NO	
+G800	PATH_NAME function			NO	
+G801	ELEMENT_NUMBER function			NO	
+G802	PATH_LENGTH function			NO	
+G803	MATCHNUM function			NO	
+G810	IS BOUND predicate			NO	
+G811	IS BOUND predicate: AS option			NO	
+G820	BINDING_COUNT			NO	
+G830	Colon in 'is label' expression			YES	
+G840	Path-ordered aggregates			NO	
+G850	SQL/PGQ Information Schema views			YES	
+G860	GET DIAGNOSTICS enhancements for SQL-property graphs			NO	
+G900	GRAPH_TABLE			YES	SQL/PGQ required
+G901	GRAPH_TABLE: ONE ROW PER VERTEX			NO	
+G902	GRAPH_TABLE: ONE ROW PER STEP			NO	
+G903	GRAPH_TABLE: explicit ONE ROW PER MATCH keywords			NO	
+G904	All properties reference			NO	
+G905	GRAPH_TABLE: optional COLUMNS clause			NO	
+G906	GRAPH_TABLE: explicit EXPORT ALL			NO	
+G907	GRAPH_TABLE: EXPORT ALL EXCEPT			NO	
+G908	GRAPH_TABLE: EXPORT SINGLETONS list			NO	
+G909	GRAPH_TABLE: explicit EXPORT NO SINGLETONS			NO	
+G910	GRAPH_TABLE: 'in paths clause'			NO	
+G920	DDL-based SQL-property graphs			YES	SQL/PGQ required
+G921	Empty SQL-property graph			YES	
+G922	Views as element tables			YES	
+G923	In-line views as element tables			NO	
+G924	Explicit key clause for element tables			YES	SQL/PGQ required
+G925	Explicit label and properties clause for element tables			YES	SQL/PGQ required
+G926	More than one label for vertex tables			YES	
+G927	More than one label for edge tables			YES	
+G928	Value expressions as properties and renaming of properties			YES	
+G929	Labels and properties: EXCEPT list			NO	
+G940	Multi-sourced/destined edges			NO	XXX
+G941	Implicit removal of incomplete edges			NO	XXX
+G950	Alter property graph statement: ADD/DROP element table			YES	
+G960	Alter element table definition: ADD/DROP LABEL			YES	
+G970	Alter element table definition: ALTER LABEL			YES	
+G980	DROP PROPERTY GRAPH: CASCADE drop behavior			YES	
 R010	Row pattern recognition: FROM clause			NO	
 R020	Row pattern recognition: WINDOW clause			NO	
 R030	Row pattern recognition: full aggregate support			NO	
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 48f7348f91c..6260d6b79ed 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -46,6 +46,7 @@ OBJS = \
 	portalcmds.o \
 	prepare.o \
 	proclang.o \
+	propgraphcmds.o \
 	publicationcmds.o \
 	schemacmds.o \
 	seclabel.o \
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index b2e4260aef7..e8234ac34a0 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -383,6 +383,7 @@ ExecRenameStmt(RenameStmt *stmt)
 		case OBJECT_MATVIEW:
 		case OBJECT_INDEX:
 		case OBJECT_FOREIGN_TABLE:
+		case OBJECT_PROPGRAPH:
 			return RenameRelation(stmt);
 
 		case OBJECT_COLUMN:
@@ -539,6 +540,7 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 		case OBJECT_TABLE:
 		case OBJECT_VIEW:
 		case OBJECT_MATVIEW:
+		case OBJECT_PROPGRAPH:
 			address = AlterTableNamespace(stmt,
 										  oldSchemaAddr ? &oldNspOid : NULL);
 			break;
@@ -690,6 +692,9 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 		case OCLASS_EVENT_TRIGGER:
 		case OCLASS_PARAMETER_ACL:
 		case OCLASS_POLICY:
+		case OCLASS_PROPGRAPH_ELEMENT:
+		case OCLASS_PROPGRAPH_LABEL:
+		case OCLASS_PROPGRAPH_PROPERTY:
 		case OCLASS_PUBLICATION:
 		case OCLASS_PUBLICATION_NAMESPACE:
 		case OCLASS_PUBLICATION_REL:
@@ -912,6 +917,7 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 		case OBJECT_OPCLASS:
 		case OBJECT_OPFAMILY:
 		case OBJECT_PROCEDURE:
+		case OBJECT_PROPGRAPH:
 		case OBJECT_ROUTINE:
 		case OBJECT_STATISTIC_EXT:
 		case OBJECT_TABLESPACE:
@@ -921,16 +927,29 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 				Relation	relation;
 				ObjectAddress address;
 
-				address = get_object_address(stmt->objectType,
-											 stmt->object,
-											 &relation,
-											 AccessExclusiveLock,
-											 false);
-				Assert(relation == NULL);
+				if (stmt->relation)
+					address = get_object_address_rv(stmt->objectType,
+													stmt->relation,
+													NIL,
+													&relation,
+													AccessExclusiveLock,
+													false);
+				else
+				{
+					address = get_object_address(stmt->objectType,
+												 stmt->object,
+												 &relation,
+												 AccessExclusiveLock,
+												 false);
+					Assert(relation == NULL);
+				}
 
 				AlterObjectOwner_internal(address.classId, address.objectId,
 										  newowner);
 
+				if (relation)
+					relation_close(relation, NoLock);
+
 				return address;
 			}
 			break;
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 87a2db4e0f5..79ce0967995 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -487,6 +487,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_INDEX:
 		case OBJECT_MATVIEW:
+		case OBJECT_PROPGRAPH:
 		case OBJECT_ROLE:
 		case OBJECT_SEQUENCE:
 		case OBJECT_SUBSCRIPTION:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index c8b662131c3..243ba9e2b33 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -1168,6 +1168,7 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_OPFAMILY:
 		case OBJECT_POLICY:
 		case OBJECT_PROCEDURE:
+		case OBJECT_PROPGRAPH:
 		case OBJECT_PUBLICATION:
 		case OBJECT_PUBLICATION_NAMESPACE:
 		case OBJECT_PUBLICATION_REL:
@@ -1248,6 +1249,9 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_DEFACL:
 		case OCLASS_EXTENSION:
 		case OCLASS_POLICY:
+		case OCLASS_PROPGRAPH_ELEMENT:
+		case OCLASS_PROPGRAPH_LABEL:
+		case OCLASS_PROPGRAPH_PROPERTY:
 		case OCLASS_PUBLICATION:
 		case OCLASS_PUBLICATION_NAMESPACE:
 		case OCLASS_PUBLICATION_REL:
@@ -2266,6 +2270,7 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_OPERATOR:
 		case OBJECT_OPFAMILY:
 		case OBJECT_POLICY:
+		case OBJECT_PROPGRAPH:
 		case OBJECT_PUBLICATION:
 		case OBJECT_PUBLICATION_NAMESPACE:
 		case OBJECT_PUBLICATION_REL:
@@ -2350,6 +2355,7 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_OPFAMILY:
 		case OBJECT_PARAMETER_ACL:
 		case OBJECT_POLICY:
+		case OBJECT_PROPGRAPH:
 		case OBJECT_PUBLICATION:
 		case OBJECT_PUBLICATION_NAMESPACE:
 		case OBJECT_PUBLICATION_REL:
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 6dd00a4abde..34dd12d7d56 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -34,6 +34,7 @@ backend_sources += files(
   'portalcmds.c',
   'prepare.c',
   'proclang.c',
+  'propgraphcmds.c',
   'publicationcmds.c',
   'schemacmds.c',
   'seclabel.c',
diff --git a/src/backend/commands/propgraphcmds.c b/src/backend/commands/propgraphcmds.c
new file mode 100644
index 00000000000..25948861343
--- /dev/null
+++ b/src/backend/commands/propgraphcmds.c
@@ -0,0 +1,1001 @@
+/*-------------------------------------------------------------------------
+ *
+ * propgraphcmds.c
+ *	  property graph manipulation
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/backend/commands/propgraphcmds.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "access/xact.h"
+#include "catalog/catalog.h"
+#include "catalog/indexing.h"
+#include "catalog/namespace.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_propgraph_element.h"
+#include "catalog/pg_propgraph_label.h"
+#include "catalog/pg_propgraph_property.h"
+#include "commands/propgraphcmds.h"
+#include "commands/tablecmds.h"
+#include "nodes/nodeFuncs.h"
+#include "parser/parse_relation.h"
+#include "parser/parse_target.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+
+struct element_info
+{
+	Oid			elementid;
+	char		kind;
+	Oid			relid;
+	char	   *aliasname;
+	ArrayType  *key;
+
+	char	   *srcvertex;
+	Oid			srcvertexid;
+	ArrayType  *srckey;
+	ArrayType  *srcref;
+
+	char	   *destvertex;
+	Oid			destvertexid;
+	ArrayType  *destkey;
+	ArrayType  *destref;
+
+	List	   *labels;
+};
+
+
+static ArrayType *propgraph_element_get_key(ParseState *pstate, const List *key_clause, int location, Relation element_rel);
+static ArrayType *array_from_column_list(ParseState *pstate, const List *colnames, int location, Relation element_rel);
+static void insert_element_record(ObjectAddress pgaddress, struct element_info *einfo);
+static Oid	insert_label_record(Oid graphid, Oid peoid, const char *label);
+static void insert_property_records(Oid graphid, Oid labeloid, Oid pgerelid, const PropGraphProperties *properties);
+static void insert_property_record(Oid graphid, Oid labeloid, const char *propname, const Expr *expr);
+static Oid	get_vertex_oid(ParseState *pstate, Oid pgrelid, const char *alias, int location);
+static Oid	get_edge_oid(ParseState *pstate, Oid pgrelid, const char *alias, int location);
+static Oid	get_element_relid(Oid peid);
+
+
+/*
+ * CREATE PROPERTY GRAPH
+ */
+ObjectAddress
+CreatePropGraph(ParseState *pstate, const CreatePropGraphStmt *stmt)
+{
+	CreateStmt *cstmt = makeNode(CreateStmt);
+	char		components_persistence;
+	ListCell   *lc;
+	ObjectAddress pgaddress;
+	List	   *vertex_infos = NIL;
+	List	   *edge_infos = NIL;
+	List	   *element_aliases = NIL;
+
+	if (stmt->pgname->relpersistence == RELPERSISTENCE_UNLOGGED)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("property graphs cannot be unlogged because they do not have storage")));
+
+	components_persistence = RELPERSISTENCE_PERMANENT;
+
+	foreach(lc, stmt->vertex_tables)
+	{
+		PropGraphVertex *vertex = lfirst_node(PropGraphVertex, lc);
+		struct element_info *vinfo;
+		Relation	rel;
+
+		vinfo = palloc0_object(struct element_info);
+		vinfo->kind = PGEKIND_VERTEX;
+
+		vinfo->relid = RangeVarGetRelidExtended(vertex->vtable, AccessShareLock, 0, RangeVarCallbackOwnsRelation, NULL);
+
+		rel = table_open(vinfo->relid, NoLock);
+
+		if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+			components_persistence = RELPERSISTENCE_TEMP;
+
+		if (vertex->vtable->alias)
+			vinfo->aliasname = vertex->vtable->alias->aliasname;
+		else
+			vinfo->aliasname = vertex->vtable->relname;
+
+		if (list_member(element_aliases, makeString(vinfo->aliasname)))
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("alias \"%s\" used more than once as element table", vinfo->aliasname),
+					 parser_errposition(pstate, vertex->location)));
+
+		vinfo->key = propgraph_element_get_key(pstate, vertex->vkey, vertex->location, rel);
+
+		vinfo->labels = vertex->labels;
+
+		table_close(rel, NoLock);
+
+		vertex_infos = lappend(vertex_infos, vinfo);
+
+		element_aliases = lappend(element_aliases, makeString(vinfo->aliasname));
+	}
+
+	foreach(lc, stmt->edge_tables)
+	{
+		PropGraphEdge *edge = lfirst_node(PropGraphEdge, lc);
+		struct element_info *einfo;
+		Relation	rel;
+		ListCell   *lc2;
+		Oid			srcrelid;
+		Oid			destrelid;
+		Relation	srcrel;
+		Relation	destrel;
+
+		einfo = palloc0_object(struct element_info);
+		einfo->kind = PGEKIND_EDGE;
+
+		einfo->relid = RangeVarGetRelidExtended(edge->etable, AccessShareLock, 0, RangeVarCallbackOwnsRelation, NULL);
+
+		rel = table_open(einfo->relid, NoLock);
+
+		if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+			components_persistence = RELPERSISTENCE_TEMP;
+
+		if (edge->etable->alias)
+			einfo->aliasname = edge->etable->alias->aliasname;
+		else
+			einfo->aliasname = edge->etable->relname;
+
+		if (list_member(element_aliases, makeString(einfo->aliasname)))
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_TABLE),
+					 errmsg("alias \"%s\" used more than once as element table", einfo->aliasname),
+					 parser_errposition(pstate, edge->location)));
+
+		einfo->key = propgraph_element_get_key(pstate, edge->ekey, edge->location, rel);
+
+		einfo->srcvertex = edge->esrcvertex;
+		einfo->destvertex = edge->edestvertex;
+
+		srcrelid = 0;
+		destrelid = 0;
+		foreach(lc2, vertex_infos)
+		{
+			struct element_info *vinfo = lfirst(lc2);
+
+			if (strcmp(vinfo->aliasname, edge->esrcvertex) == 0)
+				srcrelid = vinfo->relid;
+
+			if (strcmp(vinfo->aliasname, edge->edestvertex) == 0)
+				destrelid = vinfo->relid;
+
+			if (srcrelid && destrelid)
+				break;
+		}
+		if (!srcrelid)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("source vertex \"%s\" of edge \"%s\" does not exist",
+							edge->esrcvertex, einfo->aliasname),
+					 parser_errposition(pstate, edge->location)));
+		if (!destrelid)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("destination vertex \"%s\" of edge \"%s\" does not exist",
+							edge->edestvertex, einfo->aliasname),
+					 parser_errposition(pstate, edge->location)));
+
+		if (!edge->esrckey || !edge->esrcvertexcols || !edge->edestkey || !edge->edestvertexcols)
+			elog(ERROR, "TODO foreign key support");
+
+		srcrel = table_open(srcrelid, NoLock);
+		destrel = table_open(destrelid, NoLock);
+
+		if (list_length(edge->esrckey) != list_length(edge->esrcvertexcols))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("mismatching number of columns in source vertex definition of edge \"%s\"",
+							einfo->aliasname),
+					 parser_errposition(pstate, edge->location)));
+
+		if (list_length(edge->edestkey) != list_length(edge->edestvertexcols))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("mismatching number of columns in destination vertex definition of edge \"%s\"",
+							einfo->aliasname),
+					 parser_errposition(pstate, edge->location)));
+
+		einfo->srckey = array_from_column_list(pstate, edge->esrckey, edge->location, rel);
+		einfo->srcref = array_from_column_list(pstate, edge->esrcvertexcols, edge->location, srcrel);
+		einfo->destkey = array_from_column_list(pstate, edge->edestkey, edge->location, rel);
+		einfo->destref = array_from_column_list(pstate, edge->edestvertexcols, edge->location, destrel);
+
+		/* TODO: various consistency checks */
+
+		einfo->labels = edge->labels;
+
+		table_close(destrel, NoLock);
+		table_close(srcrel, NoLock);
+
+		table_close(rel, NoLock);
+
+		edge_infos = lappend(edge_infos, einfo);
+
+		element_aliases = lappend(element_aliases, makeString(einfo->aliasname));
+	}
+
+	cstmt->relation = stmt->pgname;
+	cstmt->oncommit = ONCOMMIT_NOOP;
+
+	/*
+	 * Automatically make it temporary if any component tables are temporary
+	 * (see also DefineView()).
+	 */
+	if (stmt->pgname->relpersistence == RELPERSISTENCE_PERMANENT
+		&& components_persistence == RELPERSISTENCE_TEMP)
+	{
+		cstmt->relation = copyObject(cstmt->relation);
+		cstmt->relation->relpersistence = RELPERSISTENCE_TEMP;
+		ereport(NOTICE,
+				(errmsg("property graph \"%s\" will be temporary",
+						stmt->pgname->relname)));
+	}
+
+	pgaddress = DefineRelation(cstmt, RELKIND_PROPGRAPH, InvalidOid, NULL, NULL);
+
+	foreach(lc, vertex_infos)
+	{
+		struct element_info *vinfo = lfirst(lc);
+
+		insert_element_record(pgaddress, vinfo);
+	}
+
+	foreach(lc, edge_infos)
+	{
+		struct element_info *einfo = lfirst(lc);
+		ListCell   *lc2;
+
+		/*
+		 * Look up the vertices again.  Now the vertices have OIDs assigned,
+		 * which we need.
+		 */
+		foreach(lc2, vertex_infos)
+		{
+			struct element_info *vinfo = lfirst(lc2);
+
+			if (strcmp(vinfo->aliasname, einfo->srcvertex) == 0)
+				einfo->srcvertexid = vinfo->elementid;
+			if (strcmp(vinfo->aliasname, einfo->destvertex) == 0)
+				einfo->destvertexid = vinfo->elementid;
+			if (einfo->srcvertexid && einfo->destvertexid)
+				break;
+		}
+		Assert(einfo->srcvertexid);
+		Assert(einfo->destvertexid);
+		insert_element_record(pgaddress, einfo);
+	}
+
+	return pgaddress;
+}
+
+/*
+ * Process the key clause specified for an element.  If key_clause is non-NIL,
+ * then it is a list of column names.  Otherwise, the primary key of the
+ * relation is used.  The return value is an array of column numbers.
+ */
+static ArrayType *
+propgraph_element_get_key(ParseState *pstate, const List *key_clause, int location, Relation element_rel)
+{
+	ArrayType  *a;
+
+	if (key_clause == NIL)
+	{
+		Oid			pkidx = RelationGetPrimaryKeyIndex(element_rel);
+
+		if (!pkidx)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("element table key must be specified for table without primary key"),
+					 parser_errposition(pstate, location)));
+		else
+		{
+			Relation	indexDesc;
+			int			numattrs;
+			Datum	   *attnumsd;
+
+			indexDesc = index_open(pkidx, AccessShareLock);
+			numattrs = indexDesc->rd_index->indkey.dim1;
+			attnumsd = palloc_array(Datum, numattrs);
+			for (int i = 0; i < numattrs; i++)
+				attnumsd[i] = Int16GetDatum(indexDesc->rd_index->indkey.values[i]);
+			a = construct_array_builtin(attnumsd, numattrs, INT2OID);
+			index_close(indexDesc, NoLock);
+		}
+	}
+	else
+	{
+		a = array_from_column_list(pstate, key_clause, location, element_rel);
+	}
+
+	return a;
+}
+
+/*
+ * Convert list of column names in the specified relation into an array of
+ * column numbers.
+ */
+static ArrayType *
+array_from_column_list(ParseState *pstate, const List *colnames, int location, Relation element_rel)
+{
+	int			numattrs;
+	Datum	   *attnumsd;
+	int			i;
+	ListCell   *lc;
+
+	numattrs = list_length(colnames);
+	attnumsd = palloc_array(Datum, numattrs);
+
+	i = 0;
+	foreach(lc, colnames)
+	{
+		char	   *colname = strVal(lfirst(lc));
+		Oid			relid = RelationGetRelid(element_rel);
+		AttrNumber	attnum;
+
+		attnum = get_attnum(relid, colname);
+		if (!attnum)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column \"%s\" of relation \"%s\" does not exist",
+							colname, get_rel_name(relid)),
+					 parser_errposition(pstate, location)));
+		attnumsd[i++] = Int16GetDatum(attnum);
+	}
+
+	for (int j = 0; j < numattrs; j++)
+	{
+		for (int k = j + 1; k < numattrs; k++)
+		{
+			if (DatumGetInt16(attnumsd[j]) == DatumGetInt16(attnumsd[k]))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+						 errmsg("graph key columns list must not contain duplicates"),
+						 parser_errposition(pstate, location)));
+		}
+	}
+
+	return construct_array_builtin(attnumsd, numattrs, INT2OID);
+}
+
+/*
+ * Insert a record for an element into the pg_propgraph_element catalog.  Also
+ * inserts labels and properties into their respective catalogs.
+ */
+static void
+insert_element_record(ObjectAddress pgaddress, struct element_info *einfo)
+{
+	Oid			graphid = pgaddress.objectId;
+	Relation	rel;
+	NameData	aliasname;
+	Oid			peoid;
+	Datum		values[Natts_pg_propgraph_element] = {0};
+	bool		nulls[Natts_pg_propgraph_element] = {0};
+	HeapTuple	tup;
+	ObjectAddress myself;
+	ObjectAddress referenced;
+
+	rel = table_open(PropgraphElementRelationId, RowExclusiveLock);
+
+	peoid = GetNewOidWithIndex(rel, PropgraphElementObjectIndexId, Anum_pg_propgraph_element_oid);
+	einfo->elementid = peoid;
+	values[Anum_pg_propgraph_element_oid - 1] = ObjectIdGetDatum(peoid);
+	values[Anum_pg_propgraph_element_pgepgid - 1] = ObjectIdGetDatum(graphid);
+	values[Anum_pg_propgraph_element_pgerelid - 1] = ObjectIdGetDatum(einfo->relid);
+	namestrcpy(&aliasname, einfo->aliasname);
+	values[Anum_pg_propgraph_element_pgealias - 1] = NameGetDatum(&aliasname);
+	values[Anum_pg_propgraph_element_pgekind - 1] = CharGetDatum(einfo->kind);
+	values[Anum_pg_propgraph_element_pgesrcvertexid - 1] = ObjectIdGetDatum(einfo->srcvertexid);
+	values[Anum_pg_propgraph_element_pgedestvertexid - 1] = ObjectIdGetDatum(einfo->destvertexid);
+	values[Anum_pg_propgraph_element_pgekey - 1] = PointerGetDatum(einfo->key);
+
+	if (einfo->srckey)
+		values[Anum_pg_propgraph_element_pgesrckey - 1] = PointerGetDatum(einfo->srckey);
+	else
+		nulls[Anum_pg_propgraph_element_pgesrckey - 1] = true;
+	if (einfo->srcref)
+		values[Anum_pg_propgraph_element_pgesrcref - 1] = PointerGetDatum(einfo->srcref);
+	else
+		nulls[Anum_pg_propgraph_element_pgesrcref - 1] = true;
+	if (einfo->destkey)
+		values[Anum_pg_propgraph_element_pgedestkey - 1] = PointerGetDatum(einfo->destkey);
+	else
+		nulls[Anum_pg_propgraph_element_pgedestkey - 1] = true;
+	if (einfo->destref)
+		values[Anum_pg_propgraph_element_pgedestref - 1] = PointerGetDatum(einfo->destref);
+	else
+		nulls[Anum_pg_propgraph_element_pgedestref - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, PropgraphElementRelationId, peoid);
+
+	/* Add dependency on the property graph */
+	recordDependencyOn(&myself, &pgaddress, DEPENDENCY_AUTO);
+
+	/* Add dependency on the relation */
+	ObjectAddressSet(referenced, RelationRelationId, einfo->relid);
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+	/* Add dependencies on vertices */
+	/* TODO: columns */
+	if (einfo->srcvertexid)
+	{
+		ObjectAddressSet(referenced, PropgraphElementRelationId, einfo->srcvertexid);
+		recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+	}
+	if (einfo->destvertexid)
+	{
+		ObjectAddressSet(referenced, PropgraphElementRelationId, einfo->destvertexid);
+		recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+	}
+
+	table_close(rel, NoLock);
+
+	if (einfo->labels)
+	{
+		ListCell   *lc;
+
+		foreach(lc, einfo->labels)
+		{
+			PropGraphLabelAndProperties *lp = lfirst_node(PropGraphLabelAndProperties, lc);
+			Oid			labeloid;
+
+			if (lp->label)
+				labeloid = insert_label_record(graphid, peoid, lp->label);
+			else
+				labeloid = insert_label_record(graphid, peoid, einfo->aliasname);
+			insert_property_records(graphid, labeloid, einfo->relid, lp->properties);
+		}
+	}
+	else
+	{
+		Oid			labeloid;
+		PropGraphProperties *pr = makeNode(PropGraphProperties);
+
+		pr->all = true;
+		pr->location = -1;
+
+		labeloid = insert_label_record(graphid, peoid, einfo->aliasname);
+		insert_property_records(graphid, labeloid, einfo->relid, pr);
+	}
+}
+
+/*
+ * Insert a record for a label into the pg_propgraph_label catalog.
+ */
+static Oid
+insert_label_record(Oid graphid, Oid peoid, const char *label)
+{
+	Relation	rel;
+	NameData	labelname;
+	Oid			labeloid;
+	Datum		values[Natts_pg_propgraph_label] = {0};
+	bool		nulls[Natts_pg_propgraph_label] = {0};
+	HeapTuple	tup;
+	ObjectAddress myself;
+	ObjectAddress referenced;
+
+	rel = table_open(PropgraphLabelRelationId, RowExclusiveLock);
+
+	labeloid = GetNewOidWithIndex(rel, PropgraphLabelObjectIndexId, Anum_pg_propgraph_label_oid);
+	values[Anum_pg_propgraph_label_oid - 1] = ObjectIdGetDatum(labeloid);
+	values[Anum_pg_propgraph_label_pglpgid - 1] = ObjectIdGetDatum(graphid);
+	namestrcpy(&labelname, label);
+	values[Anum_pg_propgraph_label_pgllabel - 1] = NameGetDatum(&labelname);
+	values[Anum_pg_propgraph_label_pglelid - 1] = ObjectIdGetDatum(peoid);
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, PropgraphLabelRelationId, labeloid);
+
+	/* Add dependency on the property graph element */
+	ObjectAddressSet(referenced, PropgraphElementRelationId, peoid);
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
+
+	table_close(rel, NoLock);
+
+	return labeloid;
+}
+
+/*
+ * Insert records for properties into the pg_propgraph_property catalog.
+ */
+static void
+insert_property_records(Oid graphid, Oid labeloid, Oid pgerelid, const PropGraphProperties *properties)
+{
+	List	   *proplist = NIL;
+	ParseState *pstate;
+	ParseNamespaceItem *nsitem;
+	List	   *tp;
+	Relation	rel;
+	ListCell   *lc;
+
+	if (properties->all)
+	{
+		Relation	attRelation;
+		SysScanDesc scan;
+		ScanKeyData key[1];
+		HeapTuple	attributeTuple;
+
+		attRelation = table_open(AttributeRelationId, RowShareLock);
+		ScanKeyInit(&key[0],
+					Anum_pg_attribute_attrelid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(pgerelid));
+		scan = systable_beginscan(attRelation, AttributeRelidNumIndexId,
+								  true, NULL, 1, key);
+		while (HeapTupleIsValid(attributeTuple = systable_getnext(scan)))
+		{
+			Form_pg_attribute att = (Form_pg_attribute) GETSTRUCT(attributeTuple);
+			ColumnRef  *cr;
+			ResTarget  *rt;
+
+			if (att->attnum <= 0 || att->attisdropped)
+				continue;
+
+			cr = makeNode(ColumnRef);
+			rt = makeNode(ResTarget);
+
+			cr->fields = list_make1(makeString(NameStr(att->attname)));
+			cr->location = -1;
+
+			rt->name = pstrdup(NameStr(att->attname));
+			rt->val = (Node *) cr;
+			rt->location = -1;
+
+			proplist = lappend(proplist, rt);
+		}
+		systable_endscan(scan);
+		table_close(attRelation, RowShareLock);
+	}
+	else
+	{
+		proplist = properties->properties;
+
+		foreach(lc, proplist)
+		{
+			ResTarget  *rt = lfirst_node(ResTarget, lc);
+
+			if (!rt->name && !IsA(rt->val, ColumnRef))
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("property name required"),
+						parser_errposition(NULL, rt->location));
+		}
+	}
+
+	rel = table_open(pgerelid, AccessShareLock);
+
+	pstate = make_parsestate(NULL);
+	nsitem = addRangeTableEntryForRelation(pstate,
+										   rel,
+										   AccessShareLock,
+										   NULL,
+										   false,
+										   true);
+	addNSItemToQuery(pstate, nsitem, true, true, true);
+
+	table_close(rel, NoLock);
+
+	tp = transformTargetList(pstate, proplist, EXPR_KIND_OTHER);
+
+	foreach(lc, tp)
+	{
+		TargetEntry *te = lfirst_node(TargetEntry, lc);
+
+		insert_property_record(graphid, labeloid, te->resname, te->expr);
+	}
+}
+
+/*
+ * Insert one record for a property into the pg_propgraph_property catalog.
+ */
+static void
+insert_property_record(Oid graphid, Oid labeloid, const char *propname, const Expr *expr)
+{
+	Relation	rel;
+	NameData	propnamedata;
+	Oid			propoid;
+	Datum		values[Natts_pg_propgraph_property] = {0};
+	bool		nulls[Natts_pg_propgraph_property] = {0};
+	HeapTuple	tup;
+	ObjectAddress myself;
+	ObjectAddress referenced;
+
+	rel = table_open(PropgraphPropertyRelationId, RowExclusiveLock);
+
+	propoid = GetNewOidWithIndex(rel, PropgraphPropertyObjectIndexId, Anum_pg_propgraph_property_oid);
+	values[Anum_pg_propgraph_property_oid - 1] = ObjectIdGetDatum(propoid);
+	values[Anum_pg_propgraph_property_pgppgid - 1] = ObjectIdGetDatum(graphid);
+	namestrcpy(&propnamedata, propname);
+	values[Anum_pg_propgraph_property_pgpname - 1] = NameGetDatum(&propnamedata);
+	values[Anum_pg_propgraph_property_pgptypid - 1] = ObjectIdGetDatum(exprType((const Node *) expr));
+	values[Anum_pg_propgraph_property_pgplabelid - 1] = ObjectIdGetDatum(labeloid);
+	values[Anum_pg_propgraph_property_pgpexpr - 1] = CStringGetTextDatum(nodeToString(expr));
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, PropgraphPropertyRelationId, propoid);
+
+	/* Add dependency on the property graph label */
+	ObjectAddressSet(referenced, PropgraphLabelRelationId, labeloid);
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_AUTO);
+
+	table_close(rel, NoLock);
+}
+
+/*
+ * ALTER PROPERTY GRAPH
+ */
+ObjectAddress
+AlterPropGraph(ParseState *pstate, const AlterPropGraphStmt *stmt)
+{
+	Oid			pgrelid;
+	ListCell   *lc;
+	ObjectAddress pgaddress;
+
+	pgrelid = RangeVarGetRelidExtended(stmt->pgname,
+									   ShareRowExclusiveLock,
+									   stmt->missing_ok ? RVR_MISSING_OK : 0,
+									   RangeVarCallbackOwnsRelation,
+									   NULL);
+	if (pgrelid == InvalidOid)
+	{
+		ereport(NOTICE,
+				(errmsg("relation \"%s\" does not exist, skipping",
+						stmt->pgname->relname)));
+		return InvalidObjectAddress;
+	}
+
+	ObjectAddressSet(pgaddress, RelationRelationId, pgrelid);
+
+	foreach(lc, stmt->add_vertex_tables)
+	{
+		PropGraphVertex *vertex = lfirst_node(PropGraphVertex, lc);
+		struct element_info *vinfo;
+		Relation	rel;
+
+		vinfo = palloc0_object(struct element_info);
+		vinfo->kind = PGEKIND_VERTEX;
+
+		vinfo->relid = RangeVarGetRelidExtended(vertex->vtable, AccessShareLock, 0, RangeVarCallbackOwnsRelation, NULL);
+
+		rel = table_open(vinfo->relid, NoLock);
+
+		if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+			elog(ERROR, "TODO");
+
+		if (vertex->vtable->alias)
+			vinfo->aliasname = vertex->vtable->alias->aliasname;
+		else
+			vinfo->aliasname = vertex->vtable->relname;
+
+		vinfo->key = propgraph_element_get_key(pstate, vertex->vkey, vertex->location, rel);
+
+		vinfo->labels = vertex->labels;
+
+		table_close(rel, NoLock);
+
+		insert_element_record(pgaddress, vinfo);
+	}
+
+	CommandCounterIncrement();
+
+	foreach(lc, stmt->add_edge_tables)
+	{
+		PropGraphEdge *edge = lfirst_node(PropGraphEdge, lc);
+		struct element_info *einfo;
+		Relation	rel;
+		Oid			srcrelid;
+		Oid			destrelid;
+		Relation	srcrel;
+		Relation	destrel;
+
+		einfo = palloc0_object(struct element_info);
+		einfo->kind = PGEKIND_EDGE;
+
+		einfo->relid = RangeVarGetRelidExtended(edge->etable, AccessShareLock, 0, RangeVarCallbackOwnsRelation, NULL);
+
+		rel = table_open(einfo->relid, NoLock);
+
+		if (rel->rd_rel->relpersistence == RELPERSISTENCE_TEMP)
+			elog(ERROR, "TODO");
+
+		if (edge->etable->alias)
+			einfo->aliasname = edge->etable->alias->aliasname;
+		else
+			einfo->aliasname = edge->etable->relname;
+
+		einfo->key = propgraph_element_get_key(pstate, edge->ekey, edge->location, rel);
+
+		einfo->srcvertexid = get_vertex_oid(pstate, pgrelid, edge->esrcvertex, edge->location);
+		einfo->destvertexid = get_vertex_oid(pstate, pgrelid, edge->edestvertex, edge->location);
+
+		if (!edge->esrckey || !edge->esrcvertexcols || !edge->edestkey || !edge->edestvertexcols)
+			elog(ERROR, "TODO foreign key support");
+
+		srcrelid = get_element_relid(einfo->srcvertexid);
+		destrelid = get_element_relid(einfo->destvertexid);
+
+		srcrel = table_open(srcrelid, AccessShareLock);
+		destrel = table_open(destrelid, AccessShareLock);
+
+		if (list_length(edge->esrckey) != list_length(edge->esrcvertexcols))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("mismatching number of columns in source vertex definition of edge \"%s\"",
+							einfo->aliasname),
+					 parser_errposition(pstate, edge->location)));
+
+		if (list_length(edge->edestkey) != list_length(edge->edestvertexcols))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+					 errmsg("mismatching number of columns in destination vertex definition of edge \"%s\"",
+							einfo->aliasname),
+					 parser_errposition(pstate, edge->location)));
+
+		einfo->srckey = array_from_column_list(pstate, edge->esrckey, edge->location, rel);
+		einfo->srcref = array_from_column_list(pstate, edge->esrcvertexcols, edge->location, srcrel);
+		einfo->destkey = array_from_column_list(pstate, edge->edestkey, edge->location, rel);
+		einfo->destref = array_from_column_list(pstate, edge->edestvertexcols, edge->location, destrel);
+
+		/* TODO: various consistency checks */
+
+		einfo->labels = edge->labels;
+
+		table_close(destrel, NoLock);
+		table_close(srcrel, NoLock);
+
+		table_close(rel, NoLock);
+
+		insert_element_record(pgaddress, einfo);
+	}
+
+	foreach(lc, stmt->drop_vertex_tables)
+	{
+		char	   *alias = strVal(lfirst(lc));
+		Oid			peoid;
+		ObjectAddress obj;
+
+		peoid = get_vertex_oid(pstate, pgrelid, alias, -1);
+		ObjectAddressSet(obj, PropgraphElementRelationId, peoid);
+		performDeletion(&obj, stmt->drop_behavior, 0);
+	}
+
+	foreach(lc, stmt->drop_edge_tables)
+	{
+		char	   *alias = strVal(lfirst(lc));
+		Oid			peoid;
+		ObjectAddress obj;
+
+		peoid = get_edge_oid(pstate, pgrelid, alias, -1);
+		ObjectAddressSet(obj, PropgraphElementRelationId, peoid);
+		performDeletion(&obj, stmt->drop_behavior, 0);
+	}
+
+	foreach(lc, stmt->add_labels)
+	{
+		PropGraphLabelAndProperties *lp = lfirst_node(PropGraphLabelAndProperties, lc);
+		Oid			peoid;
+		Oid			pgerelid;
+		Oid			labeloid;
+
+		Assert(lp->label);
+
+		if (stmt->element_kind == PROPGRAPH_ELEMENT_KIND_VERTEX)
+			peoid = get_vertex_oid(pstate, pgrelid, stmt->element_alias, -1);
+		else
+			peoid = get_edge_oid(pstate, pgrelid, stmt->element_alias, -1);
+
+		pgerelid = get_element_relid(peoid);
+
+		labeloid = insert_label_record(pgrelid, peoid, lp->label);
+		insert_property_records(pgrelid, labeloid, pgerelid, lp->properties);
+	}
+
+	if (stmt->drop_label)
+	{
+		Oid			peoid;
+		Oid			labeloid;
+		ObjectAddress obj;
+
+		if (stmt->element_kind == PROPGRAPH_ELEMENT_KIND_VERTEX)
+			peoid = get_vertex_oid(pstate, pgrelid, stmt->element_alias, -1);
+		else
+			peoid = get_edge_oid(pstate, pgrelid, stmt->element_alias, -1);
+
+		labeloid = GetSysCacheOid2(PROPGRAPHLABELNAME,
+								   Anum_pg_propgraph_label_oid,
+								   ObjectIdGetDatum(peoid),
+								   CStringGetDatum(stmt->drop_label));
+		if (!labeloid)
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_OBJECT),
+					errmsg("property graph \"%s\" element \"%s\" has no label \"%s\"",
+						   get_rel_name(pgrelid), stmt->element_alias, stmt->drop_label),
+					parser_errposition(pstate, -1));
+
+		ObjectAddressSet(obj, PropgraphLabelRelationId, labeloid);
+		performDeletion(&obj, stmt->drop_behavior, 0);
+	}
+
+	if (stmt->add_properties)
+	{
+		Oid			peoid;
+		Oid			pgerelid;
+		Oid			labeloid;
+
+		if (stmt->element_kind == PROPGRAPH_ELEMENT_KIND_VERTEX)
+			peoid = get_vertex_oid(pstate, pgrelid, stmt->element_alias, -1);
+		else
+			peoid = get_edge_oid(pstate, pgrelid, stmt->element_alias, -1);
+
+		pgerelid = get_element_relid(peoid);
+
+		labeloid = GetSysCacheOid2(PROPGRAPHLABELNAME,
+								   Anum_pg_propgraph_label_oid,
+								   ObjectIdGetDatum(peoid),
+								   CStringGetDatum(stmt->alter_label));
+		if (!labeloid)
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_OBJECT),
+					errmsg("property graph \"%s\" element \"%s\" has no label \"%s\"",
+						   get_rel_name(pgrelid), stmt->element_alias, stmt->alter_label),
+					parser_errposition(pstate, -1));
+
+		insert_property_records(pgrelid, labeloid, pgerelid, stmt->add_properties);
+	}
+
+	if (stmt->drop_properties)
+	{
+		Oid			peoid;
+		Oid			labeloid;
+		ObjectAddress obj;
+
+		if (stmt->element_kind == PROPGRAPH_ELEMENT_KIND_VERTEX)
+			peoid = get_vertex_oid(pstate, pgrelid, stmt->element_alias, -1);
+		else
+			peoid = get_edge_oid(pstate, pgrelid, stmt->element_alias, -1);
+
+		labeloid = GetSysCacheOid2(PROPGRAPHLABELNAME,
+								   Anum_pg_propgraph_label_oid,
+								   ObjectIdGetDatum(peoid),
+								   CStringGetDatum(stmt->alter_label));
+		if (!labeloid)
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_OBJECT),
+					errmsg("property graph \"%s\" element \"%s\" has no label \"%s\"",
+						   get_rel_name(pgrelid), stmt->element_alias, stmt->drop_label),
+					parser_errposition(pstate, -1));
+
+		foreach(lc, stmt->drop_properties)
+		{
+			char	   *propname = strVal(lfirst(lc));
+			Oid			propoid;
+
+			propoid = GetSysCacheOid2(PROPGRAPHPROPNAME,
+									  Anum_pg_propgraph_property_oid,
+									  ObjectIdGetDatum(labeloid),
+									  CStringGetDatum(propname));
+			if (!propoid)
+				ereport(ERROR,
+						errcode(ERRCODE_UNDEFINED_OBJECT),
+						errmsg("property graph \"%s\" element \"%s\" label \"%s\" has no property \"%s\"",
+							   get_rel_name(pgrelid), stmt->element_alias, stmt->alter_label, propname),
+						parser_errposition(pstate, -1));
+
+			ObjectAddressSet(obj, PropgraphPropertyRelationId, propoid);
+			performDeletion(&obj, stmt->drop_behavior, 0);
+		}
+	}
+
+	return pgaddress;
+}
+
+/*
+ * Get OID of vertex from graph OID and element alias.  Element must be a
+ * vertex, otherwise error.
+ */
+static Oid
+get_vertex_oid(ParseState *pstate, Oid pgrelid, const char *alias, int location)
+{
+	HeapTuple	tuple;
+	Oid			peoid;
+
+	tuple = SearchSysCache2(PROPGRAPHELALIAS, ObjectIdGetDatum(pgrelid), CStringGetDatum(alias));
+	if (!tuple)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("property graph \"%s\" has no element with alias \"%s\"",
+					   get_rel_name(pgrelid), alias),
+				parser_errposition(pstate, location));
+
+	if (((Form_pg_propgraph_element) GETSTRUCT(tuple))->pgekind != PGEKIND_VERTEX)
+		ereport(ERROR,
+				errcode(ERRCODE_SYNTAX_ERROR),
+				errmsg("element \"%s\" of property graph \"%s\" is not a vertex",
+					   alias, get_rel_name(pgrelid)),
+				parser_errposition(pstate, location));
+
+	peoid = ((Form_pg_propgraph_element) GETSTRUCT(tuple))->oid;
+
+	ReleaseSysCache(tuple);
+
+	return peoid;
+}
+
+/*
+ * Get OID of edge from graph OID and element alias.  Element must be an edge,
+ * otherwise error.
+ */
+static Oid
+get_edge_oid(ParseState *pstate, Oid pgrelid, const char *alias, int location)
+{
+	HeapTuple	tuple;
+	Oid			peoid;
+
+	tuple = SearchSysCache2(PROPGRAPHELALIAS, ObjectIdGetDatum(pgrelid), CStringGetDatum(alias));
+	if (!tuple)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("property graph \"%s\" has no element with alias \"%s\"",
+					   get_rel_name(pgrelid), alias),
+				parser_errposition(pstate, location));
+
+	if (((Form_pg_propgraph_element) GETSTRUCT(tuple))->pgekind != PGEKIND_EDGE)
+		ereport(ERROR,
+				errcode(ERRCODE_SYNTAX_ERROR),
+				errmsg("element \"%s\" of property graph \"%s\" is not an edge",
+					   alias, get_rel_name(pgrelid)),
+				parser_errposition(pstate, location));
+
+	peoid = ((Form_pg_propgraph_element) GETSTRUCT(tuple))->oid;
+
+	ReleaseSysCache(tuple);
+
+	return peoid;
+}
+
+/*
+ * Get the element table relation OID from the OID of the element.
+ */
+static Oid
+get_element_relid(Oid peid)
+{
+	HeapTuple	tuple;
+	Oid			pgerelid;
+
+	tuple = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(peid));
+	if (!tuple)
+		elog(ERROR, "cache lookup failed for property graph element %u", peid);
+
+	pgerelid = ((Form_pg_propgraph_element) GETSTRUCT(tuple))->pgerelid;
+
+	ReleaseSysCache(tuple);
+
+	return pgerelid;
+}
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 5607273bf9f..0a5e34abe99 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -49,6 +49,7 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_LARGEOBJECT:
 		case OBJECT_MATVIEW:
 		case OBJECT_PROCEDURE:
+		case OBJECT_PROPGRAPH:
 		case OBJECT_PUBLICATION:
 		case OBJECT_ROLE:
 		case OBJECT_ROUTINE:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 237eeeec67e..a9e90b57eca 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -301,6 +301,12 @@ static const struct dropmsgstrings dropmsgstringarray[] = {
 		gettext_noop("index \"%s\" does not exist, skipping"),
 		gettext_noop("\"%s\" is not an index"),
 	gettext_noop("Use DROP INDEX to remove an index.")},
+	{RELKIND_PROPGRAPH,
+		ERRCODE_UNDEFINED_OBJECT,
+		gettext_noop("property graph \"%s\" does not exist"),
+		gettext_noop("property graph \"%s\" does not exist, skipping"),
+		gettext_noop("\"%s\" is not a property graph"),
+	gettext_noop("Use DROP PROPERTY GRAPH to remove a property graph.")},
 	{'\0', 0, NULL, NULL, NULL, NULL}
 };
 
@@ -1539,6 +1545,10 @@ RemoveRelations(DropStmt *drop)
 			relkind = RELKIND_FOREIGN_TABLE;
 			break;
 
+		case OBJECT_PROPGRAPH:
+			relkind = RELKIND_PROPGRAPH;
+			break;
+
 		default:
 			elog(ERROR, "unrecognized drop object type: %d",
 				 (int) drop->removeType);
@@ -13970,6 +13980,9 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 			case OCLASS_EXTENSION:
 			case OCLASS_EVENT_TRIGGER:
 			case OCLASS_PARAMETER_ACL:
+			case OCLASS_PROPGRAPH_ELEMENT:
+			case OCLASS_PROPGRAPH_LABEL:
+			case OCLASS_PROPGRAPH_PROPERTY:
 			case OCLASS_PUBLICATION:
 			case OCLASS_PUBLICATION_NAMESPACE:
 			case OCLASS_PUBLICATION_REL:
@@ -14795,6 +14808,7 @@ ATExecChangeOwner(Oid relationOid, Oid newOwnerId, bool recursing, LOCKMODE lock
 		case RELKIND_MATVIEW:
 		case RELKIND_FOREIGN_TABLE:
 		case RELKIND_PARTITIONED_TABLE:
+		case RELKIND_PROPGRAPH:
 			/* ok to change owner */
 			break;
 		case RELKIND_INDEX:
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 13a9b7da83b..73a914acdb6 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -602,7 +602,7 @@ ExecCheckPermissions(List *rangeTable, List *rteperminfos,
 			 */
 			Assert(rte->rtekind == RTE_RELATION ||
 				   (rte->rtekind == RTE_SUBQUERY &&
-					rte->relkind == RELKIND_VIEW));
+					(rte->relkind == RELKIND_VIEW || rte->relkind == RELKIND_PROPGRAPH)));
 
 			(void) getRTEPermissionInfo(rteperminfos, rte);
 			/* Many-to-one mapping not allowed */
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index e1a5bc7e95d..521b3ffa017 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -265,6 +265,9 @@ exprType(const Node *expr)
 		case T_PlaceHolderVar:
 			type = exprType((Node *) ((const PlaceHolderVar *) expr)->phexpr);
 			break;
+		case T_GraphPropertyRef:
+			type = ((const GraphPropertyRef *) expr)->typeId;
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
 			type = InvalidOid;	/* keep compiler quiet */
@@ -501,6 +504,9 @@ exprTypmod(const Node *expr)
 			return ((const SetToDefault *) expr)->typeMod;
 		case T_PlaceHolderVar:
 			return exprTypmod((Node *) ((const PlaceHolderVar *) expr)->phexpr);
+		case T_GraphPropertyRef:
+			/* TODO */
+			return -1;
 		default:
 			break;
 	}
@@ -1000,6 +1006,9 @@ exprCollation(const Node *expr)
 		case T_PlaceHolderVar:
 			coll = exprCollation((Node *) ((const PlaceHolderVar *) expr)->phexpr);
 			break;
+		case T_GraphPropertyRef:
+			coll = DEFAULT_COLLATION_OID;	/* FIXME */
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
 			coll = InvalidOid;	/* keep compiler quiet */
@@ -2035,6 +2044,7 @@ expression_tree_walker_impl(Node *node,
 		case T_RangeTblRef:
 		case T_SortGroupClause:
 		case T_CTESearchClause:
+		case T_GraphPropertyRef:
 			/* primitive node types with no expression subnodes */
 			break;
 		case T_WithCheckOption:
@@ -2535,6 +2545,26 @@ expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_GraphElementPattern:
+			{
+				GraphElementPattern *gep = (GraphElementPattern *) node;
+
+				if (WALK(gep->subexpr))
+					return true;
+				if (WALK(gep->whereClause))
+					return true;
+			}
+			break;
+		case T_GraphPattern:
+			{
+				GraphPattern *gp = (GraphPattern *) node;
+
+				if (LIST_WALK(gp->path_pattern_list))
+					return true;
+				if (WALK(gp->whereClause))
+					return true;
+			}
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d",
 				 (int) nodeTag(node));
@@ -2728,6 +2758,12 @@ range_table_entry_walker_impl(RangeTblEntry *rte,
 			if (WALK(rte->values_lists))
 				return true;
 			break;
+		case RTE_GRAPH_TABLE:
+			if (WALK(rte->graph_pattern))
+				return true;
+			if (WALK(rte->graph_table_columns))
+				return true;
+			break;
 		case RTE_CTE:
 		case RTE_NAMEDTUPLESTORE:
 		case RTE_RESULT:
@@ -3728,6 +3764,10 @@ range_table_mutator_impl(List *rtable,
 			case RTE_VALUES:
 				MUTATE(newrte->values_lists, rte->values_lists, List *);
 				break;
+			case RTE_GRAPH_TABLE:
+				MUTATE(newrte->graph_pattern, rte->graph_pattern, GraphPattern *);
+				MUTATE(newrte->graph_table_columns, rte->graph_table_columns, List *);
+				break;
 			case RTE_CTE:
 			case RTE_NAMEDTUPLESTORE:
 			case RTE_RESULT:
@@ -4279,6 +4319,18 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_RangeGraphTable:
+			{
+				RangeGraphTable *rgt = (RangeGraphTable *) node;
+
+				if (WALK(rgt->graph_pattern))
+					return true;
+				if (WALK(rgt->columns))
+					return true;
+				if (WALK(rgt->alias))
+					return true;
+			}
+			break;
 		case T_TypeName:
 			{
 				TypeName   *tn = (TypeName *) node;
@@ -4437,6 +4489,26 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_GraphElementPattern:
+			{
+				GraphElementPattern *gep = (GraphElementPattern *) node;
+
+				if (WALK(gep->subexpr))
+					return true;
+				if (WALK(gep->whereClause))
+					return true;
+			}
+			break;
+		case T_GraphPattern:
+			{
+				GraphPattern *gp = (GraphPattern *) node;
+
+				if (WALK(gp->path_pattern_list))
+					return true;
+				if (WALK(gp->whereClause))
+					return true;
+			}
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d",
 				 (int) nodeTag(node));
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 25171864dbf..555db730a48 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -555,6 +555,11 @@ _outRangeTblEntry(StringInfo str, const RangeTblEntry *node)
 			/* we re-use these RELATION fields, too: */
 			WRITE_OID_FIELD(relid);
 			break;
+		case RTE_GRAPH_TABLE:
+			WRITE_OID_FIELD(relid);
+			WRITE_NODE_FIELD(graph_pattern);
+			WRITE_NODE_FIELD(graph_table_columns);
+			break;
 		case RTE_RESULT:
 			/* no extra fields */
 			break;
diff --git a/src/backend/nodes/queryjumblefuncs.c b/src/backend/nodes/queryjumblefuncs.c
index 82f725baaa5..7a81959d057 100644
--- a/src/backend/nodes/queryjumblefuncs.c
+++ b/src/backend/nodes/queryjumblefuncs.c
@@ -394,6 +394,11 @@ _jumbleRangeTblEntry(JumbleState *jstate, Node *node)
 		case RTE_NAMEDTUPLESTORE:
 			JUMBLE_STRING(enrname);
 			break;
+		case RTE_GRAPH_TABLE:
+			JUMBLE_FIELD(relid);
+			JUMBLE_NODE(graph_pattern);
+			JUMBLE_NODE(graph_table_columns);
+			break;
 		case RTE_RESULT:
 			break;
 		default:
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index cfb552fde74..3b23cfa0613 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -563,6 +563,11 @@ _readRangeTblEntry(void)
 			/* we re-use these RELATION fields, too: */
 			READ_OID_FIELD(relid);
 			break;
+		case RTE_GRAPH_TABLE:
+			READ_OID_FIELD(relid);
+			READ_NODE_FIELD(graph_pattern);
+			READ_NODE_FIELD(graph_table_columns);
+			break;
 		case RTE_RESULT:
 			/* no extra fields */
 			break;
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index d404fbf262c..103ec0b9681 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -731,6 +731,10 @@ set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel,
 			 */
 			return;
 
+		case RTE_GRAPH_TABLE:
+			/* shouldn't happen here */
+			break;
+
 		case RTE_RESULT:
 			/* RESULT RTEs, in themselves, are no problem. */
 			break;
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index aa83dd3636f..e02c71cea3b 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1188,6 +1188,10 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 				case RTE_RESULT:
 					/* these can't contain any lateral references */
 					break;
+				case RTE_GRAPH_TABLE:
+					/* shouldn't happen here */
+					Assert(false);
+					break;
 			}
 		}
 	}
@@ -2249,6 +2253,10 @@ replace_vars_in_jointree(Node *jtnode,
 						/* these shouldn't be marked LATERAL */
 						Assert(false);
 						break;
+					case RTE_GRAPH_TABLE:
+						/* shouldn't happen here */
+						Assert(false);
+						break;
 				}
 			}
 		}
diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile
index 401c16686c6..b03dc4cdb05 100644
--- a/src/backend/parser/Makefile
+++ b/src/backend/parser/Makefile
@@ -23,6 +23,7 @@ OBJS = \
 	parse_enr.o \
 	parse_expr.o \
 	parse_func.o \
+	parse_graphtable.o \
 	parse_merge.o \
 	parse_node.o \
 	parse_oper.o \
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 60b31d9f852..4c75938ca7f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -297,6 +297,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 		CreateSchemaStmt CreateSeqStmt CreateStmt CreateStatsStmt CreateTableSpaceStmt
 		CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt
 		CreateAssertionStmt CreateTransformStmt CreateTrigStmt CreateEventTrigStmt
+		CreatePropGraphStmt AlterPropGraphStmt
 		CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt
 		CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt
 		DropOpClassStmt DropOpFamilyStmt DropStmt
@@ -660,6 +661,36 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				json_object_constructor_null_clause_opt
 				json_array_constructor_null_clause_opt
 
+%type <list>	vertex_tables_clause edge_tables_clause
+				opt_vertex_tables_clause opt_edge_tables_clause
+				vertex_table_list
+				opt_graph_table_key_clause
+				edge_table_list
+				source_vertex_table destination_vertex_table
+				opt_element_table_label_and_properties
+				label_and_properties_list
+				add_label_list
+%type <node>	vertex_table_definition edge_table_definition
+%type <alias>	opt_propgraph_table_alias
+%type <str>		element_table_label_clause
+%type <node>	label_and_properties element_table_properties
+				add_label
+%type <ival>	vertex_or_edge
+
+%type <list>	opt_graph_pattern_quantifier
+				path_pattern_list
+				path_pattern
+				path_pattern_expression
+				path_term
+%type <node>	graph_pattern
+				path_factor
+				path_primary
+				opt_is_label_expression
+				label_expression
+				label_disjunction
+				label_term
+%type <str>		opt_colid
+
 
 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -677,6 +708,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %token <ival>	ICONST PARAM
 %token			TYPECAST DOT_DOT COLON_EQUALS EQUALS_GREATER
 %token			LESS_EQUALS GREATER_EQUALS NOT_EQUALS
+%token			BRACKET_RIGHT_ARROW LEFT_ARROW_BRACKET MINUS_LEFT_BRACKET RIGHT_BRACKET_MINUS
 
 /*
  * If you want to make any keyword changes, update the keyword table in
@@ -703,18 +735,18 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER CURSOR CYCLE
 
 	DATA_P DATABASE DAY_P DEALLOCATE DEC DECIMAL_P DECLARE DEFAULT DEFAULTS
-	DEFERRABLE DEFERRED DEFINER DELETE_P DELIMITER DELIMITERS DEPENDS DEPTH DESC
+	DEFERRABLE DEFERRED DEFINER DELETE_P DELIMITER DELIMITERS DEPENDS DEPTH DESC DESTINATION
 	DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
 	DOUBLE_P DROP
 
-	EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT
+	EACH EDGE ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT
 	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
 	FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
 
-	GENERATED GLOBAL GRANT GRANTED GREATEST GROUP_P GROUPING GROUPS
+	GENERATED GLOBAL GRANT GRANTED GRAPH GRAPH_TABLE GREATEST GROUP_P GROUPING GROUPS
 
 	HANDLER HAVING HEADER_P HOLD HOUR_P
 
@@ -735,7 +767,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
 	MINUTE_P MINVALUE MODE MONTH_P MOVE
 
-	NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
+	NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NODE NONE
 	NORMALIZE NORMALIZED
 	NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF
 	NULLS_P NUMERIC
@@ -747,19 +779,19 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD
 	PLACING PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
-	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
+	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PROPERTIES PROPERTY PUBLICATION
 
 	QUOTE
 
 	RANGE READ REAL REASSIGN RECHECK RECURSIVE REF_P REFERENCES REFERENCING
-	REFRESH REINDEX RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA
+	REFRESH REINDEX RELATIONSHIP RELATIVE_P RELEASE RENAME REPEATABLE REPLACE REPLICA
 	RESET RESTART RESTRICT RETURN RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP
 	ROUTINE ROUTINES ROW ROWS RULE
 
 	SAVEPOINT SCALAR SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT
 	SEQUENCE SEQUENCES
 	SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
-	SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P
+	SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SOURCE SQL_P STABLE STANDALONE_P
 	START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P
 	SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER
 
@@ -772,7 +804,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	UNLISTEN UNLOGGED UNTIL UPDATE USER USING
 
 	VACUUM VALID VALIDATE VALIDATOR VALUE_P VALUES VARCHAR VARIADIC VARYING
-	VERBOSE VERSION_P VIEW VIEWS VOLATILE
+	VERBOSE VERSION_P VERTEX VIEW VIEWS VOLATILE
 
 	WHEN WHERE WHITESPACE_P WINDOW WITH WITHIN WITHOUT WORK WRAPPER WRITE
 
@@ -809,6 +841,11 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %token		MODE_PLPGSQL_ASSIGN2
 %token		MODE_PLPGSQL_ASSIGN3
 
+ /*
+  * FIXME: The brace characters are assigned token symbols because if we
+  * mention literal braces in the rules then the ecpg parser assembly breaks.
+  */
+%token		LEFT_BRACE RIGHT_BRACE
 
 /* Precedence: lowest to highest */
 %left		UNION EXCEPT
@@ -867,7 +904,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %nonassoc	UNBOUNDED		/* ideally would have same precedence as IDENT */
 %nonassoc	IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP
 			SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT
-%left		Op OPERATOR		/* multi-character ops and user-defined operators */
+%left		Op OPERATOR LEFT_ARROW RIGHT_ARROW	/* multi-character ops and user-defined operators */
 %left		'+' '-'
 %left		'*' '/' '%'
 %left		'^'
@@ -998,6 +1035,7 @@ stmt:
 			| AlterOperatorStmt
 			| AlterTypeStmt
 			| AlterPolicyStmt
+			| AlterPropGraphStmt
 			| AlterSeqStmt
 			| AlterSystemStmt
 			| AlterTableStmt
@@ -1038,6 +1076,7 @@ stmt:
 			| AlterOpFamilyStmt
 			| CreatePolicyStmt
 			| CreatePLangStmt
+			| CreatePropGraphStmt
 			| CreateSchemaStmt
 			| CreateSeqStmt
 			| CreateStmt
@@ -6891,6 +6930,7 @@ object_type_any_name:
 			| MATERIALIZED VIEW						{ $$ = OBJECT_MATVIEW; }
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
+			| PROPERTY GRAPH						{ $$ = OBJECT_PROPGRAPH; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
@@ -7747,6 +7787,15 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| PROPERTY GRAPH qualified_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_PROPGRAPH;
+					n->objs = $3;
+					$$ = n;
+				}
 			| SCHEMA name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -9064,6 +9113,366 @@ opt_if_exists: IF_P EXISTS						{ $$ = true; }
 		;

+/*****************************************************************************
+ *
+ *		CREATE PROPERTY GRAPH
+ *		ALTER PROPERTY GRAPH
+ *
+ *****************************************************************************/
+
+CreatePropGraphStmt: CREATE OptTemp PROPERTY GRAPH qualified_name opt_vertex_tables_clause opt_edge_tables_clause
+				{
+					CreatePropGraphStmt *n = makeNode(CreatePropGraphStmt);
+
+					n->pgname = $5;
+					n->pgname->relpersistence = $2;
+					n->vertex_tables = $6;
+					n->edge_tables = $7;
+
+					$$ = (Node *)n;
+				}
+		;
+
+opt_vertex_tables_clause:
+			vertex_tables_clause				{ $$ = $1; }
+			| /*EMPTY*/							{ $$ = NIL; }
+		;
+
+vertex_tables_clause:
+			vertex_synonym TABLES '(' vertex_table_list ')'	{ $$ = $4; }
+		;
+
+vertex_synonym: NODE | VERTEX
+		;
+
+vertex_table_list: vertex_table_definition						{ $$ = list_make1($1); }
+			| vertex_table_list ',' vertex_table_definition		{ $$ = lappend($1, $3); }
+		;
+
+vertex_table_definition: qualified_name opt_propgraph_table_alias opt_graph_table_key_clause
+				opt_element_table_label_and_properties
+				{
+					PropGraphVertex *n = makeNode(PropGraphVertex);
+
+					$1->alias = $2;
+					n->vtable = $1;
+					n->vkey = $3;
+					n->labels = $4;
+					n->location = @1;
+
+					$$ = (Node *) n;
+				}
+		;
+
+opt_propgraph_table_alias:
+			AS name
+				{
+					$$ = makeNode(Alias);
+					$$->aliasname = $2;
+				}
+			| /*EMPTY*/							{ $$ = NULL; }
+		;
+
+opt_graph_table_key_clause:
+			KEY '(' columnList ')'				{ $$ = $3; }
+			| /*EMPTY*/							{ $$ = NIL; }
+		;
+
+opt_edge_tables_clause:
+			edge_tables_clause					{ $$ = $1; }
+			| /*EMPTY*/							{ $$ = NIL; }
+		;
+
+edge_tables_clause:
+			edge_synonym TABLES '(' edge_table_list	')'			{ $$ = $4; }
+		;
+
+edge_synonym: EDGE | RELATIONSHIP
+		;
+
+edge_table_list: edge_table_definition						{ $$ = list_make1($1); }
+			| edge_table_list ',' edge_table_definition		{ $$ = lappend($1, $3); }
+		;
+
+edge_table_definition: qualified_name opt_propgraph_table_alias opt_graph_table_key_clause
+				source_vertex_table destination_vertex_table opt_element_table_label_and_properties
+				{
+					PropGraphEdge *n = makeNode(PropGraphEdge);
+
+					$1->alias = $2;
+					n->etable = $1;
+					n->ekey = $3;
+					n->esrckey = linitial($4);
+					n->esrcvertex = lsecond($4);
+					n->esrcvertexcols = lthird($4);
+					n->edestkey = linitial($5);
+					n->edestvertex = lsecond($5);
+					n->edestvertexcols = lthird($5);
+					n->labels = $6;
+					n->location = @1;
+
+					$$ = (Node *) n;
+				}
+		;
+
+source_vertex_table: SOURCE name
+				{
+					$$ = list_make3(NULL, $2, NULL);
+				}
+				| SOURCE KEY '(' columnList ')' REFERENCES name '(' columnList ')'
+				{
+					$$ = list_make3($4, $7, $9);
+				}
+		;
+
+destination_vertex_table: DESTINATION name
+				{
+					$$ = list_make3(NULL, $2, NULL);
+				}
+				| DESTINATION KEY '(' columnList ')' REFERENCES name '(' columnList ')'
+				{
+					$$ = list_make3($4, $7, $9);
+				}
+		;
+
+opt_element_table_label_and_properties:
+			element_table_properties
+				{
+					PropGraphLabelAndProperties *lp = makeNode(PropGraphLabelAndProperties);
+
+					lp->properties = (PropGraphProperties *) $1;
+					lp->location = @1;
+
+					$$ = list_make1(lp);
+				}
+			| label_and_properties_list
+				{
+					$$ = $1;
+				}
+			| /*EMPTY*/
+				{
+					PropGraphLabelAndProperties *lp = makeNode(PropGraphLabelAndProperties);
+					PropGraphProperties *pr = makeNode(PropGraphProperties);
+
+					pr->all = true;
+					pr->location = -1;
+					lp->properties = pr;
+					lp->location = -1;
+
+					$$ = list_make1(lp);
+				}
+		;
+
+element_table_properties:
+			NO PROPERTIES
+				{
+					PropGraphProperties *pr = makeNode(PropGraphProperties);
+
+					pr->properties = NIL;
+					pr->location = @1;
+
+					$$ = (Node *) pr;
+				}
+			| PROPERTIES ALL COLUMNS
+			/*| PROPERTIES ARE ALL COLUMNS */
+				{
+					PropGraphProperties *pr = makeNode(PropGraphProperties);
+
+					pr->all = true;
+					pr->location = @1;
+
+					$$ = (Node *) pr;
+				}
+			| PROPERTIES '(' xml_attribute_list ')'
+				{
+					PropGraphProperties *pr = makeNode(PropGraphProperties);
+
+					pr->properties = $3;
+					pr->location = @1;
+
+					$$ = (Node *) pr;
+				}
+		;
+
+label_and_properties_list:
+			label_and_properties
+				{
+					$$ = list_make1($1);
+				}
+			| label_and_properties_list label_and_properties
+				{
+					$$ = lappend($1, $2);
+				}
+		;
+
+label_and_properties:
+			element_table_label_clause
+				{
+					PropGraphLabelAndProperties *lp = makeNode(PropGraphLabelAndProperties);
+					PropGraphProperties *pr = makeNode(PropGraphProperties);
+
+					pr->all = true;
+					pr->location = -1;
+
+					lp->label = $1;
+					lp->properties = pr;
+					lp->location = @1;
+
+					$$ = (Node *) lp;
+				}
+			| element_table_label_clause element_table_properties
+				{
+					PropGraphLabelAndProperties *lp = makeNode(PropGraphLabelAndProperties);
+
+					lp->label = $1;
+					lp->properties = (PropGraphProperties *) $2;
+					lp->location = @1;
+
+					$$ = (Node *) lp;
+				}
+		;
+
+element_table_label_clause:
+			LABEL name
+				{
+					$$ = $2;
+				}
+			| DEFAULT LABEL
+				{
+					$$ = NULL;
+				}
+		;
+
+AlterPropGraphStmt:
+			ALTER PROPERTY GRAPH qualified_name ADD_P vertex_tables_clause
+				{
+					AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt);
+
+					n->pgname = $4;
+					n->add_vertex_tables = $6;
+
+					$$ = (Node *) n;
+				}
+			| ALTER PROPERTY GRAPH qualified_name ADD_P vertex_tables_clause ADD_P edge_tables_clause
+				{
+					AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt);
+
+					n->pgname = $4;
+					n->add_vertex_tables = $6;
+					n->add_edge_tables = $8;
+
+					$$ = (Node *) n;
+				}
+			| ALTER PROPERTY GRAPH qualified_name ADD_P edge_tables_clause
+				{
+					AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt);
+
+					n->pgname = $4;
+					n->add_edge_tables = $6;
+
+					$$ = (Node *) n;
+				}
+			| ALTER PROPERTY GRAPH qualified_name DROP vertex_synonym TABLES '(' name_list ')' opt_drop_behavior
+				{
+					AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt);
+
+					n->pgname = $4;
+					n->drop_vertex_tables = $9;
+					n->drop_behavior = $11;
+
+					$$ = (Node *) n;
+				}
+			| ALTER PROPERTY GRAPH qualified_name DROP edge_synonym TABLES '(' name_list ')' opt_drop_behavior
+				{
+					AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt);
+
+					n->pgname = $4;
+					n->drop_edge_tables = $9;
+					n->drop_behavior = $11;
+
+					$$ = (Node *) n;
+				}
+			| ALTER PROPERTY GRAPH qualified_name ALTER vertex_or_edge TABLE name
+				add_label_list
+				{
+					AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt);
+
+					n->pgname = $4;
+					n->element_kind = $6;
+					n->element_alias = $8;
+					n->add_labels = $9;
+
+					$$ = (Node *) n;
+				}
+			| ALTER PROPERTY GRAPH qualified_name ALTER vertex_or_edge TABLE name
+				DROP LABEL name opt_drop_behavior
+				{
+					AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt);
+
+					n->pgname = $4;
+					n->element_kind = $6;
+					n->element_alias = $8;
+					n->drop_label = $11;
+					n->drop_behavior = $12;
+
+					$$ = (Node *) n;
+				}
+			| ALTER PROPERTY GRAPH qualified_name ALTER vertex_or_edge TABLE name
+				ALTER LABEL name ADD_P PROPERTIES '(' xml_attribute_list ')'
+				{
+					AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt);
+					PropGraphProperties *pr = makeNode(PropGraphProperties);
+
+					n->pgname = $4;
+					n->element_kind = $6;
+					n->element_alias = $8;
+					n->alter_label = $11;
+
+					pr->properties = $15;
+					pr->location = @13;
+					n->add_properties = pr;
+
+					$$ = (Node *) n;
+				}
+			| ALTER PROPERTY GRAPH qualified_name ALTER vertex_or_edge TABLE name
+				ALTER LABEL name DROP PROPERTIES '(' name_list ')' opt_drop_behavior
+				{
+					AlterPropGraphStmt *n = makeNode(AlterPropGraphStmt);
+
+					n->pgname = $4;
+					n->element_kind = $6;
+					n->element_alias = $8;
+					n->alter_label = $11;
+					n->drop_properties = $15;
+					n->drop_behavior = $17;
+
+					$$ = (Node *) n;
+				}
+		;
+
+vertex_or_edge:
+			vertex_synonym						{ $$ = PROPGRAPH_ELEMENT_KIND_VERTEX; }
+			| edge_synonym						{ $$ = PROPGRAPH_ELEMENT_KIND_EDGE; }
+		;
+
+add_label_list:
+			add_label							{ $$ = list_make1($1); }
+			| add_label_list add_label			{ $$ = lappend($1, $2);	}
+		;
+
+add_label: ADD_P LABEL name element_table_properties
+				{
+					PropGraphLabelAndProperties *lp = makeNode(PropGraphLabelAndProperties);
+
+					lp->label = $3;
+					lp->properties = (PropGraphProperties *) $4;
+					lp->location = @1;
+
+					$$ = (Node *) lp;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		CREATE TRANSFORM / DROP TRANSFORM
@@ -9364,6 +9773,16 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER PROPERTY GRAPH qualified_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_PROPGRAPH;
+					n->relation = $4;
+					n->newname = $7;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
 			| ALTER PUBLICATION name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -9989,6 +10408,26 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER PROPERTY GRAPH qualified_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_PROPGRAPH;
+					n->relation = $4;
+					n->newschema = $7;
+					n->missing_ok = false;
+					$$ = (Node *)n;
+				}
+			| ALTER PROPERTY GRAPH IF_P EXISTS qualified_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_PROPGRAPH;
+					n->relation = $6;
+					n->newschema = $9;
+					n->missing_ok = true;
+					$$ = (Node *)n;
+				}
 			| ALTER ROUTINE function_with_argtypes SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -10332,6 +10771,15 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER PROPERTY GRAPH qualified_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_PROPGRAPH;
+					n->relation = $4;
+					n->newowner = $7;
+					$$ = (Node *) n;
+				}
 			| ALTER ROUTINE function_with_argtypes OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -13408,6 +13856,17 @@ table_ref:	relation_expr opt_alias_clause
 					n->alias = $3;
 					$$ = (Node *) n;
 				}
+			| GRAPH_TABLE '(' qualified_name MATCH graph_pattern COLUMNS '(' xml_attribute_list ')' ')' opt_alias_clause
+				{
+					RangeGraphTable *n = makeNode(RangeGraphTable);
+
+					n->graph_name = $3;
+					n->graph_pattern = castNode(GraphPattern, $5);
+					n->columns = $8;
+					n->alias = $11;
+					n->location = @1;
+					$$ = (Node *) n;
+				}
 			| select_with_parens opt_alias_clause
 				{
 					RangeSubselect *n = makeNode(RangeSubselect);
@@ -14589,6 +15048,10 @@ a_expr:		c_expr									{ $$ = $1; }
 				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, ">=", $1, $3, @2); }
 			| a_expr NOT_EQUALS a_expr
 				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "<>", $1, $3, @2); }
+			| a_expr LEFT_ARROW a_expr
+				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "<-", $1, $3, @2); }
+			| a_expr RIGHT_ARROW a_expr
+				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "->", $1, $3, @2); }
 
 			| a_expr qual_Op a_expr				%prec Op
 				{ $$ = (Node *) makeA_Expr(AEXPR_OP, $2, $1, $3, @2); }
@@ -15068,6 +15531,10 @@ b_expr:		c_expr
 				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, ">=", $1, $3, @2); }
 			| b_expr NOT_EQUALS b_expr
 				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "<>", $1, $3, @2); }
+			| b_expr LEFT_ARROW b_expr
+				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "<-", $1, $3, @2); }
+			| b_expr RIGHT_ARROW b_expr
+				{ $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "->", $1, $3, @2); }
 			| b_expr qual_Op b_expr				%prec Op
 				{ $$ = (Node *) makeA_Expr(AEXPR_OP, $2, $1, $3, @2); }
 			| qual_Op b_expr					%prec Op
@@ -16172,6 +16639,8 @@ MathOp:		 '+'									{ $$ = "+"; }
 			| LESS_EQUALS							{ $$ = "<="; }
 			| GREATER_EQUALS						{ $$ = ">="; }
 			| NOT_EQUALS							{ $$ = "<>"; }
+			| LEFT_ARROW							{ $$ = "<-"; }
+			| RIGHT_ARROW							{ $$ = "->"; }
 		;
 
 qual_Op:	Op
@@ -16683,6 +17152,192 @@ json_array_aggregate_order_by_clause_opt:
 			| /* EMPTY */							{ $$ = NIL; }
 		;
 
+
+/*****************************************************************************
+ *
+ *	graph patterns
+ *
+ *****************************************************************************/
+
+graph_pattern:
+			path_pattern_list where_clause
+				{
+					GraphPattern *gp = makeNode(GraphPattern);
+
+					gp->path_pattern_list = $1;
+					gp->whereClause = $2;
+					$$ = (Node *) gp;
+				}
+		;
+
+path_pattern_list:
+			path_pattern							{ $$ = list_make1($1); }
+			| path_pattern_list ',' path_pattern	{ $$ = lappend($1, $3); }
+		;
+
+path_pattern:
+			path_pattern_expression					{ $$ = $1; }
+		;
+
+/*
+ * path pattern expression
+ */
+
+path_pattern_expression:
+			path_term								{ $$ = $1; }
+			/* | path_multiset_alternation */
+			/* | path_pattern_union */
+		;
+
+path_term:
+			path_factor								{ $$ = list_make1($1); }
+			| path_term path_factor					{ $$ = lappend($1, $2); }
+		;
+
+path_factor:
+			path_primary opt_graph_pattern_quantifier
+				{
+					GraphElementPattern *gep = (GraphElementPattern *) $1;
+
+					gep->quantifier = $2;
+				}
+		;
+
+path_primary:
+			'(' opt_colid opt_is_label_expression where_clause ')'
+				{
+					GraphElementPattern *gep = makeNode(GraphElementPattern);
+
+					gep->kind = VERTEX_PATTERN;
+					gep->variable = $2;
+					gep->labelexpr = $3;
+					gep->whereClause = $4;
+					gep->location = @1;
+
+					$$ = (Node *) gep;
+				}
+			/* full edge pointing left: <-[ xxx ]- */
+			| LEFT_ARROW_BRACKET opt_colid opt_is_label_expression where_clause RIGHT_BRACKET_MINUS
+				{
+					GraphElementPattern *gep = makeNode(GraphElementPattern);
+
+					gep->kind = EDGE_PATTERN_LEFT;
+					gep->variable = $2;
+					gep->labelexpr = $3;
+					gep->whereClause = $4;
+					gep->location = @1;
+
+					$$ = (Node *) gep;
+				}
+			/* full edge pointing right: -[ xxx ]-> */
+			| MINUS_LEFT_BRACKET opt_colid opt_is_label_expression where_clause BRACKET_RIGHT_ARROW
+				{
+					GraphElementPattern *gep = makeNode(GraphElementPattern);
+
+					gep->kind = EDGE_PATTERN_RIGHT;
+					gep->variable = $2;
+					gep->labelexpr = $3;
+					gep->whereClause = $4;
+					gep->location = @1;
+
+					$$ = (Node *) gep;
+				}
+			/* full edge any direction: -[ xxx ]- */
+			| MINUS_LEFT_BRACKET opt_colid opt_is_label_expression where_clause RIGHT_BRACKET_MINUS
+				{
+					GraphElementPattern *gep = makeNode(GraphElementPattern);
+
+					gep->kind = EDGE_PATTERN_ANY;
+					gep->variable = $2;
+					gep->labelexpr = $3;
+					gep->whereClause = $4;
+					gep->location = @1;
+
+					$$ = (Node *) gep;
+				}
+			/* abbreviated edge patterns */
+			| LEFT_ARROW
+				{
+					GraphElementPattern *gep = makeNode(GraphElementPattern);
+
+					gep->kind = EDGE_PATTERN_LEFT;
+					gep->location = @1;
+
+					$$ = (Node *) gep;
+				}
+			| RIGHT_ARROW
+				{
+					GraphElementPattern *gep = makeNode(GraphElementPattern);
+
+					gep->kind = EDGE_PATTERN_RIGHT;
+					gep->location = @1;
+
+					$$ = (Node *) gep;
+				}
+			| '-'
+				{
+					GraphElementPattern *gep = makeNode(GraphElementPattern);
+
+					gep->kind = EDGE_PATTERN_ANY;
+					gep->location = @1;
+
+					$$ = (Node *) gep;
+				}
+			| '(' path_pattern_expression where_clause ')'
+				{
+					GraphElementPattern *gep = makeNode(GraphElementPattern);
+
+					gep->kind = PAREN_EXPR;
+					gep->subexpr = $2;
+					gep->whereClause = $3;
+					gep->location = @1;
+
+					$$ = (Node *) gep;
+				}
+		;
+
+opt_colid:
+			ColId			{ $$ = $1; }
+			| /*EMPTY*/		{ $$ = NULL; }
+		;
+
+opt_is_label_expression:
+			IS label_expression		{ $$ = $2; }
+			| ':' label_expression	{ $$ = $2; }
+			| /*EMPTY*/				{ $$ = NULL; }
+		;
+
+/*
+ * graph pattern quantifier
+ */
+
+opt_graph_pattern_quantifier:
+			LEFT_BRACE Iconst RIGHT_BRACE				{ $$ = list_make2_int($2, $2); }
+			| LEFT_BRACE ',' Iconst RIGHT_BRACE			{ $$ = list_make2_int(0, $3); }
+			| LEFT_BRACE Iconst ',' Iconst RIGHT_BRACE	{ $$ = list_make2_int($2, $4); }
+			| /*EMPTY*/									{ $$ = NULL; }
+		;
+
+/*
+ * label expression
+ */
+
+label_expression:
+			label_term
+			| label_disjunction
+		;
+
+label_disjunction:
+			label_expression '|' label_term
+				{ $$ = makeOrExpr($1, $3, @2); }
+		;
+
+label_term:
+			name
+				{ $$ = makeColumnRef($1, NIL, @1, yyscanner); }
+		;
+
+
 /*****************************************************************************
  *
  *	target list for SELECT
@@ -17201,6 +17856,7 @@ unreserved_keyword:
 			| DELIMITERS
 			| DEPENDS
 			| DEPTH
+			| DESTINATION
 			| DETACH
 			| DICTIONARY
 			| DISABLE_P
@@ -17210,6 +17866,7 @@ unreserved_keyword:
 			| DOUBLE_P
 			| DROP
 			| EACH
+			| EDGE
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
@@ -17237,6 +17894,7 @@ unreserved_keyword:
 			| GENERATED
 			| GLOBAL
 			| GRANTED
+			| GRAPH
 			| GROUPS
 			| HANDLER
 			| HEADER_P
@@ -17299,6 +17957,7 @@ unreserved_keyword:
 			| NFKC
 			| NFKD
 			| NO
+			| NODE
 			| NORMALIZED
 			| NOTHING
 			| NOTIFY
@@ -17337,6 +17996,8 @@ unreserved_keyword:
 			| PROCEDURE
 			| PROCEDURES
 			| PROGRAM
+			| PROPERTIES
+			| PROPERTY
 			| PUBLICATION
 			| QUOTE
 			| RANGE
@@ -17348,6 +18009,7 @@ unreserved_keyword:
 			| REFERENCING
 			| REFRESH
 			| REINDEX
+			| RELATIONSHIP
 			| RELATIVE_P
 			| RELEASE
 			| RENAME
@@ -17387,6 +18049,7 @@ unreserved_keyword:
 			| SIMPLE
 			| SKIP
 			| SNAPSHOT
+			| SOURCE
 			| SQL_P
 			| STABLE
 			| STANDALONE_P
@@ -17433,6 +18096,7 @@ unreserved_keyword:
 			| VALUE_P
 			| VARYING
 			| VERSION_P
+			| VERTEX
 			| VIEW
 			| VIEWS
 			| VOLATILE
@@ -17471,6 +18135,7 @@ col_name_keyword:
 			| EXISTS
 			| EXTRACT
 			| FLOAT_P
+			| GRAPH_TABLE
 			| GREATEST
 			| GROUPING
 			| INOUT
@@ -17756,6 +18421,7 @@ bare_label_keyword:
 			| DEPENDS
 			| DEPTH
 			| DESC
+			| DESTINATION
 			| DETACH
 			| DICTIONARY
 			| DISABLE_P
@@ -17767,6 +18433,7 @@ bare_label_keyword:
 			| DOUBLE_P
 			| DROP
 			| EACH
+			| EDGE
 			| ELSE
 			| ENABLE_P
 			| ENCODING
@@ -17802,6 +18469,8 @@ bare_label_keyword:
 			| GENERATED
 			| GLOBAL
 			| GRANTED
+			| GRAPH
+			| GRAPH_TABLE
 			| GREATEST
 			| GROUPING
 			| GROUPS
@@ -17890,6 +18559,7 @@ bare_label_keyword:
 			| NFKC
 			| NFKD
 			| NO
+			| NODE
 			| NONE
 			| NORMALIZE
 			| NORMALIZED
@@ -17941,6 +18611,8 @@ bare_label_keyword:
 			| PROCEDURE
 			| PROCEDURES
 			| PROGRAM
+			| PROPERTIES
+			| PROPERTY
 			| PUBLICATION
 			| QUOTE
 			| RANGE
@@ -17954,6 +18626,7 @@ bare_label_keyword:
 			| REFERENCING
 			| REFRESH
 			| REINDEX
+			| RELATIONSHIP
 			| RELATIVE_P
 			| RELEASE
 			| RENAME
@@ -18000,6 +18673,7 @@ bare_label_keyword:
 			| SMALLINT
 			| SNAPSHOT
 			| SOME
+			| SOURCE
 			| SQL_P
 			| STABLE
 			| STANDALONE_P
@@ -18064,6 +18738,7 @@ bare_label_keyword:
 			| VARIADIC
 			| VERBOSE
 			| VERSION_P
+			| VERTEX
 			| VIEW
 			| VIEWS
 			| VOLATILE
diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build
index 8e8295640bc..4c03346dec5 100644
--- a/src/backend/parser/meson.build
+++ b/src/backend/parser/meson.build
@@ -10,6 +10,7 @@ backend_sources += files(
   'parse_enr.c',
   'parse_expr.c',
   'parse_func.c',
+  'parse_graphtable.c',
   'parse_merge.c',
   'parse_node.c',
   'parse_oper.c',
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 4b50278fd0d..693a49bb7f2 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -37,6 +37,7 @@
 #include "parser/parse_collate.h"
 #include "parser/parse_expr.h"
 #include "parser/parse_func.h"
+#include "parser/parse_graphtable.h"
 #include "parser/parse_oper.h"
 #include "parser/parse_relation.h"
 #include "parser/parse_target.h"
@@ -69,6 +70,8 @@ static ParseNamespaceItem *transformRangeFunction(ParseState *pstate,
 												  RangeFunction *r);
 static ParseNamespaceItem *transformRangeTableFunc(ParseState *pstate,
 												   RangeTableFunc *rtf);
+static ParseNamespaceItem *transformRangeGraphTable(ParseState *pstate,
+													RangeGraphTable *rgt);
 static TableSampleClause *transformRangeTableSample(ParseState *pstate,
 													RangeTableSample *rts);
 static ParseNamespaceItem *getNSItemForSpecialRelationTypes(ParseState *pstate,
@@ -898,6 +901,76 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf)
 										  tf, rtf->alias, is_lateral, true);
 }
 
+/*
+ * transformRangeGraphTable -- transform a GRAPH_TABLE clause
+ */
+static ParseNamespaceItem *
+transformRangeGraphTable(ParseState *pstate, RangeGraphTable *rgt)
+{
+	Relation	rel;
+	Oid			graphid;
+	ParseState *pstate2;
+	GraphTableParseState *gpstate = palloc0_object(GraphTableParseState);
+	Node	   *gp;
+	List	   *columns = NIL;
+	List	   *colnames = NIL;
+	ListCell   *lc;
+	int			resno = 0;
+
+	rel = parserOpenTable(pstate, rgt->graph_name, AccessShareLock);
+	if (rel->rd_rel->relkind != RELKIND_PROPGRAPH)
+		ereport(ERROR,
+				errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				errmsg("\"%s\" is not a property graph",
+					   RelationGetRelationName(rel)),
+				parser_errposition(pstate, rgt->graph_name->location));
+
+	graphid = RelationGetRelid(rel);
+
+	gpstate->graphid = graphid;
+
+	gp = transformGraphPattern(gpstate, rgt->graph_pattern);
+
+	pstate2 = make_parsestate(NULL);
+	pstate2->p_pre_columnref_hook = graph_table_property_reference;
+	pstate2->p_ref_hook_state = gpstate;
+
+	foreach(lc, rgt->columns)
+	{
+		ResTarget  *rt = lfirst_node(ResTarget, lc);
+		Node	   *colexpr;
+		TargetEntry *te;
+		char	   *colname;
+
+		colexpr = transformExpr(pstate2, rt->val, EXPR_KIND_OTHER);
+
+		if (rt->name)
+			colname = rt->name;
+		else
+		{
+			if (IsA(colexpr, GraphPropertyRef))
+				colname = pstrdup(castNode(GraphPropertyRef, colexpr)->propname);
+			else
+			{
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("complex graph table column must specify an explicit column name"),
+						parser_errposition(pstate, rt->location));
+				colname = NULL;
+			}
+		}
+
+		colnames = lappend(colnames, makeString(colname));
+
+		te = makeTargetEntry((Expr *) colexpr, ++resno, colname, false);
+		columns = lappend(columns, te);
+	}
+
+	table_close(rel, NoLock);
+
+	return addRangeTableEntryForGraphTable(pstate, graphid, castNode(GraphPattern, gp), columns, colnames, rgt->alias, false, true);
+}
+
 /*
  * transformRangeTableSample --- transform a TABLESAMPLE clause
  *
@@ -1117,6 +1190,18 @@ transformFromClauseItem(ParseState *pstate, Node *n,
 		rtr->rtindex = nsitem->p_rtindex;
 		return (Node *) rtr;
 	}
+	else if (IsA(n, RangeGraphTable))
+	{
+		RangeTblRef *rtr;
+		ParseNamespaceItem *nsitem;
+
+		nsitem = transformRangeGraphTable(pstate, (RangeGraphTable *) n);
+		*top_nsitem = nsitem;
+		*namespace = list_make1(nsitem);
+		rtr = makeNode(RangeTblRef);
+		rtr->rtindex = nsitem->p_rtindex;
+		return (Node *) rtr;
+	}
 	else if (IsA(n, RangeTableSample))
 	{
 		/* TABLESAMPLE clause (wrapping some other valid FROM node) */
diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c
index 44529bb49e6..33a1f3f2413 100644
--- a/src/backend/parser/parse_collate.c
+++ b/src/backend/parser/parse_collate.c
@@ -571,6 +571,13 @@ assign_collations_walker(Node *node, assign_collations_context *context)
 			location = exprLocation(node);
 			break;
 
+		case T_GraphPropertyRef:
+			/* FIXME */
+			collation = DEFAULT_COLLATION_OID;
+			strength = COLLATE_IMPLICIT;
+			location = -1;
+			break;
+
 		default:
 			{
 				/*
diff --git a/src/backend/parser/parse_graphtable.c b/src/backend/parser/parse_graphtable.c
new file mode 100644
index 00000000000..8679b726279
--- /dev/null
+++ b/src/backend/parser/parse_graphtable.c
@@ -0,0 +1,251 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_graphtable.c
+ *	  parsing of GRAPH_TABLE
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/parser/parse_graphtable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "catalog/pg_propgraph_property.h"
+#include "miscadmin.h"
+#include "nodes/makefuncs.h"
+#include "parser/parse_collate.h"
+#include "parser/parse_expr.h"
+#include "parser/parse_graphtable.h"
+#include "parser/parse_node.h"
+#include "utils/fmgroids.h"
+#include "utils/relcache.h"
+
+
+/*
+ * Get the type of a property.
+ */
+static Oid
+get_property_type(Oid graphid, const char *propname)
+{
+	Relation	rel;
+	SysScanDesc scan;
+	ScanKeyData key[2];
+	HeapTuple	tuple;
+	Oid			result = InvalidOid;
+
+	rel = table_open(PropgraphPropertyRelationId, RowShareLock);
+	ScanKeyInit(&key[0],
+				Anum_pg_propgraph_property_pgppgid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(graphid));
+	ScanKeyInit(&key[1],
+				Anum_pg_propgraph_property_pgpname,
+				BTEqualStrategyNumber, F_NAMEEQ,
+				CStringGetDatum(propname));
+
+	scan = systable_beginscan(rel, PropgraphPropertyGraphNameIndexId, true, NULL, 2, key);
+	while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+	{
+		Form_pg_propgraph_property prop = (Form_pg_propgraph_property) GETSTRUCT(tuple);
+
+		result = prop->pgptypid;
+		break;
+	}
+
+	systable_endscan(scan);
+	table_close(rel, RowShareLock);
+
+	if (!result)
+		ereport(ERROR,
+				errcode(ERRCODE_SYNTAX_ERROR),
+				errmsg("property \"%s\" does not exist", propname));
+
+	return result;
+}
+
+/*
+ * Resolve a property reference.
+ */
+Node *
+graph_table_property_reference(ParseState *pstate, ColumnRef *cref)
+{
+	GraphTableParseState *gpstate = pstate->p_ref_hook_state;
+	GraphPropertyRef *gpr = makeNode(GraphPropertyRef);
+
+	gpr->location = cref->location;
+
+	if (list_length(cref->fields) == 2)
+	{
+		Node	   *field1 = linitial(cref->fields);
+		Node	   *field2 = lsecond(cref->fields);
+		char	   *elvarname;
+		char	   *propname;
+
+		elvarname = strVal(field1);
+		propname = strVal(field2);
+
+		if (!list_member(gpstate->variables, field1))
+			ereport(ERROR,
+					errcode(ERRCODE_SYNTAX_ERROR),
+					errmsg("graph pattern variable \"%s\" does not exist", elvarname),
+					parser_errposition(pstate, cref->location));
+
+		gpr->elvarname = elvarname;
+		gpr->propname = propname;
+
+	}
+	else
+		elog(ERROR, "invalid property reference");
+
+	gpr->typeId = get_property_type(gpstate->graphid, gpr->propname);
+
+	return (Node *) gpr;
+}
+
+/*
+ * Transform a label expression.
+ */
+static Node *
+transformLabelExpr(GraphTableParseState *gpstate, Node *labelexpr)
+{
+	Node	   *result;
+
+	if (labelexpr == NULL)
+		return NULL;
+
+	check_stack_depth();
+
+	switch (nodeTag(labelexpr))
+	{
+		case T_ColumnRef:
+			{
+				ColumnRef  *cref = (ColumnRef *) labelexpr;
+				const char *labelname;
+				GraphLabelRef *lref;
+
+				Assert(list_length(cref->fields) == 1);
+				labelname = strVal(linitial(cref->fields));
+
+				lref = makeNode(GraphLabelRef);
+				lref->labelname = labelname;
+				lref->location = cref->location;
+
+				result = (Node *) lref;
+				break;
+			}
+
+		case T_BoolExpr:
+			{
+				BoolExpr   *be = (BoolExpr *) labelexpr;
+				ListCell   *lc;
+				List	   *args = NIL;
+
+				foreach(lc, be->args)
+				{
+					Node	   *arg = (Node *) lfirst(lc);
+
+					arg = transformLabelExpr(gpstate, arg);
+					args = lappend(args, arg);
+				}
+
+				result = (Node *) makeBoolExpr(be->boolop, args, be->location);
+				break;
+			}
+
+		default:
+			/* should not reach here */
+			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(labelexpr));
+			result = NULL;		/* keep compiler quiet */
+			break;
+	}
+
+	return result;
+}
+
+/*
+ * Transform a GraphElementPattern.
+ */
+static Node *
+transformGraphElementPattern(GraphTableParseState *gpstate, GraphElementPattern *gep)
+{
+	ParseState *pstate2;
+
+	pstate2 = make_parsestate(NULL);
+	pstate2->p_pre_columnref_hook = graph_table_property_reference;
+	pstate2->p_ref_hook_state = gpstate;
+
+	if (gep->variable)
+		gpstate->variables = lappend(gpstate->variables, makeString(pstrdup(gep->variable)));
+
+	gep->labelexpr = transformLabelExpr(gpstate, gep->labelexpr);
+
+	gep->whereClause = transformExpr(pstate2, gep->whereClause, EXPR_KIND_OTHER);
+	assign_expr_collations(pstate2, gep->whereClause);
+
+	return (Node *) gep;
+}
+
+/*
+ * Transform a path term (list of GraphElementPattern's).
+ */
+static Node *
+transformPathTerm(GraphTableParseState *gpstate, List *path_term)
+{
+	List	   *result = NIL;
+	ListCell   *lc;
+
+	foreach(lc, path_term)
+	{
+		Node	   *n = transformGraphElementPattern(gpstate, lfirst_node(GraphElementPattern, lc));
+
+		result = lappend(result, n);
+	}
+
+	return (Node *) result;
+}
+
+/*
+ * Transform a path pattern list (list of path terms).
+ */
+static Node *
+transformPathPatternList(GraphTableParseState *gpstate, List *path_pattern)
+{
+	List	   *result = NIL;
+	ListCell   *lc;
+
+	foreach(lc, path_pattern)
+	{
+		Node	   *n = transformPathTerm(gpstate, lfirst(lc));
+
+		result = lappend(result, n);
+	}
+
+	return (Node *) result;
+}
+
+/*
+ * Transform a GraphPattern.
+ */
+Node *
+transformGraphPattern(GraphTableParseState *gpstate, GraphPattern *graph_pattern)
+{
+	ParseState *pstate2;
+
+	pstate2 = make_parsestate(NULL);
+	pstate2->p_pre_columnref_hook = graph_table_property_reference;
+	pstate2->p_ref_hook_state = gpstate;
+
+	graph_pattern->path_pattern_list = (List *) transformPathPatternList(gpstate, graph_pattern->path_pattern_list);
+	graph_pattern->whereClause = transformExpr(pstate2, graph_pattern->whereClause, EXPR_KIND_OTHER);
+	assign_expr_collations(pstate2, graph_pattern->whereClause);
+
+	return (Node *) graph_pattern;
+}
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 34a0ec59019..c4b2b613904 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2127,6 +2127,98 @@ addRangeTableEntryForTableFunc(ParseState *pstate,
 								rte->colcollations);
 }
 
+ParseNamespaceItem *
+addRangeTableEntryForGraphTable(ParseState *pstate,
+								Oid graphid,
+								GraphPattern *graph_pattern,
+								List *columns,
+								List *colnames,
+								Alias *alias,
+								bool lateral,
+								bool inFromCl)
+{
+	RangeTblEntry *rte = makeNode(RangeTblEntry);
+	char	   *refname = alias ? alias->aliasname : pstrdup("graph_table");
+	Alias	   *eref;
+	int			numaliases;
+	int			varattno;
+	ListCell   *lc;
+	List	   *coltypes = NIL;
+	List	   *coltypmods = NIL;
+	List	   *colcollations = NIL;
+	RTEPermissionInfo *perminfo;
+	ParseNamespaceItem *nsitem;
+
+	Assert(pstate != NULL);
+
+	rte->rtekind = RTE_GRAPH_TABLE;
+	rte->relid = graphid;
+	rte->relkind = RELKIND_PROPGRAPH;
+	rte->graph_pattern = graph_pattern;
+	rte->graph_table_columns = columns;
+	rte->alias = alias;
+
+	eref = alias ? copyObject(alias) : makeAlias(refname, NIL);
+
+	if (!eref->colnames)
+		eref->colnames = colnames;
+
+	numaliases = list_length(eref->colnames);
+
+	/* fill in any unspecified alias columns */
+	varattno = 0;
+	foreach(lc, colnames)
+	{
+		varattno++;
+		if (varattno > numaliases)
+			eref->colnames = lappend(eref->colnames, lfirst(lc));
+	}
+	if (varattno < numaliases)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+				 errmsg("GRAPH_TABLE \"%s\" has %d columns available but %d columns specified",
+						refname, varattno, numaliases)));
+
+	rte->eref = eref;
+
+	foreach(lc, columns)
+	{
+		TargetEntry *te = lfirst_node(TargetEntry, lc);
+		Node	   *colexpr = (Node *) te->expr;
+
+		coltypes = lappend_oid(coltypes, exprType(colexpr));
+		coltypmods = lappend_int(coltypmods, exprTypmod(colexpr));
+		colcollations = lappend_oid(colcollations, exprCollation(colexpr));
+	}
+
+	/*
+	 * Set flags and access permissions.
+	 */
+	rte->lateral = lateral;
+	rte->inFromCl = inFromCl;
+
+	perminfo = addRTEPermissionInfo(&pstate->p_rteperminfos, rte);
+	perminfo->requiredPerms = ACL_SELECT;
+
+	/*
+	 * Add completed RTE to pstate's range table list, so that we know its
+	 * index.  But we don't add it to the join list --- caller must do that if
+	 * appropriate.
+	 */
+	pstate->p_rtable = lappend(pstate->p_rtable, rte);
+
+	/*
+	 * Build a ParseNamespaceItem, but don't add it to the pstate's namespace
+	 * list --- caller must do that if appropriate.
+	 */
+	nsitem = buildNSItemFromLists(rte, list_length(pstate->p_rtable),
+								  coltypes, coltypmods, colcollations);
+
+	nsitem->p_perminfo = perminfo;
+
+	return nsitem;
+}
+
 /*
  * Add an entry for a VALUES list to the pstate's range table (p_rtable).
  * Then, construct and return a ParseNamespaceItem for the new RTE.
@@ -2941,6 +3033,7 @@ expandRTE(RangeTblEntry *rte, int rtindex, int sublevels_up,
 		case RTE_VALUES:
 		case RTE_CTE:
 		case RTE_NAMEDTUPLESTORE:
+		case RTE_GRAPH_TABLE:
 			{
 				/* Tablefunc, Values, CTE, or ENR RTE */
 				ListCell   *aliasp_item = list_head(rte->eref->colnames);
@@ -3318,6 +3411,7 @@ get_rte_attribute_is_dropped(RangeTblEntry *rte, AttrNumber attnum)
 		case RTE_TABLEFUNC:
 		case RTE_VALUES:
 		case RTE_CTE:
+		case RTE_GRAPH_TABLE:
 
 			/*
 			 * Subselect, Table Functions, Values, CTE RTEs never have dropped
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 0cd904f8dac..a3da95656bf 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -361,6 +361,10 @@ markTargetListOrigin(ParseState *pstate, TargetEntry *tle,
 			tle->resorigtbl = rte->relid;
 			tle->resorigcol = attnum;
 			break;
+		case RTE_GRAPH_TABLE:
+			tle->resorigtbl = rte->relid;
+			tle->resorigcol = InvalidAttrNumber;
+			break;
 		case RTE_SUBQUERY:
 			/* Subselect-in-FROM: copy up from the subselect */
 			if (attnum != InvalidAttrNumber)
@@ -1565,6 +1569,7 @@ expandRecordVariable(ParseState *pstate, Var *var, int levelsup)
 		case RTE_RELATION:
 		case RTE_VALUES:
 		case RTE_NAMEDTUPLESTORE:
+		case RTE_GRAPH_TABLE:
 		case RTE_RESULT:
 
 			/*
diff --git a/src/backend/parser/scan.l b/src/backend/parser/scan.l
index 5eaadf53b20..a8a5baef095 100644
--- a/src/backend/parser/scan.l
+++ b/src/backend/parser/scan.l
@@ -353,6 +353,11 @@ typecast		"::"
 dot_dot			\.\.
 colon_equals	":="
 
+bracket_right_arrow	"]->"
+left_arrow_bracket	"<-["
+minus_left_bracket	"-["
+right_bracket_minus	"]-"
+
 /*
  * These operator-like tokens (unlike the above ones) also match the {operator}
  * rule, which means that they might be overridden by a longer match if they
@@ -367,6 +372,9 @@ greater_equals	">="
 less_greater	"<>"
 not_equals		"!="
 
+left_arrow		"<-"
+right_arrow		"->"
+
 /*
  * "self" is the set of chars that should be returned as single-character
  * tokens.  "op_chars" is the set of chars that can make up "Op" tokens,
@@ -850,6 +858,26 @@ other			.
 					return COLON_EQUALS;
 				}
 
+{bracket_right_arrow} {
+					SET_YYLLOC();
+					return BRACKET_RIGHT_ARROW;
+				}
+
+{left_arrow_bracket} {
+					SET_YYLLOC();
+					return LEFT_ARROW_BRACKET;
+				}
+
+{minus_left_bracket} {
+					SET_YYLLOC();
+					return MINUS_LEFT_BRACKET;
+				}
+
+{right_bracket_minus} {
+					SET_YYLLOC();
+					return RIGHT_BRACKET_MINUS;
+				}
+
 {equals_greater} {
 					SET_YYLLOC();
 					return EQUALS_GREATER;
@@ -877,11 +905,31 @@ other			.
 					return NOT_EQUALS;
 				}
 
+{left_arrow}	{
+					SET_YYLLOC();
+					return LEFT_ARROW;
+				}
+
+{right_arrow}	{
+					SET_YYLLOC();
+					return RIGHT_ARROW;
+				}
+
 {self}			{
 					SET_YYLLOC();
 					return yytext[0];
 				}
 
+\{				{
+					SET_YYLLOC();
+					return LEFT_BRACE;
+				}
+
+\}				{
+					SET_YYLLOC();
+					return RIGHT_BRACE;
+				}
+
 {operator}		{
 					/*
 					 * Check for embedded slash-star or dash-dash; those
diff --git a/src/backend/rewrite/Makefile b/src/backend/rewrite/Makefile
index 4680752e6a7..09070047b7e 100644
--- a/src/backend/rewrite/Makefile
+++ b/src/backend/rewrite/Makefile
@@ -14,6 +14,7 @@ include $(top_builddir)/src/Makefile.global
 
 OBJS = \
 	rewriteDefine.o \
+	rewriteGraphTable.o \
 	rewriteHandler.o \
 	rewriteManip.o \
 	rewriteRemove.o \
diff --git a/src/backend/rewrite/meson.build b/src/backend/rewrite/meson.build
index 23043ca6e56..2bea20233a4 100644
--- a/src/backend/rewrite/meson.build
+++ b/src/backend/rewrite/meson.build
@@ -2,6 +2,7 @@
 
 backend_sources += files(
   'rewriteDefine.c',
+  'rewriteGraphTable.c',
   'rewriteHandler.c',
   'rewriteManip.c',
   'rewriteRemove.c',
diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c
new file mode 100644
index 00000000000..94f1be3de56
--- /dev/null
+++ b/src/backend/rewrite/rewriteGraphTable.c
@@ -0,0 +1,422 @@
+/*-------------------------------------------------------------------------
+ *
+ * rewriteGraphTable.c
+ *		Support for rewriting GRAPH_TABLE clauses.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/rewrite/rewriteGraphTable.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/table.h"
+#include "catalog/pg_propgraph_element.h"
+#include "catalog/pg_propgraph_label.h"
+#include "catalog/pg_propgraph_property.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "parser/parsetree.h"
+#include "rewrite/rewriteGraphTable.h"
+#include "rewrite/rewriteHandler.h"
+#include "rewrite/rewriteManip.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+
+static Oid	get_labelid(Oid graphid, const char *labelname);
+static List *get_elements_for_label(Oid graphid, const char *labelname);
+static Oid	get_table_for_element(Oid elid);
+static Node *replace_property_refs(Node *node, const List *mappings);
+static List *build_edge_vertex_link_quals(HeapTuple edgetup, int edgerti, int refrti, AttrNumber catalog_key_attnum, AttrNumber catalog_ref_attnum);
+
+struct elvar_rt_mapping
+{
+	const char *elvarname;
+	Oid			labelid;
+	int			rt_index;
+};
+
+
+/*
+ * Convert GRAPH_TABLE clause into a subquery using relational
+ * operators.
+ */
+Query *
+rewriteGraphTable(Query *parsetree, int rt_index)
+{
+	RangeTblEntry *rte;
+	Query	   *newsubquery;
+	ListCell   *lc;
+	List	   *element_patterns;
+	List	   *element_ids = NIL;
+	List	   *fromlist = NIL;
+	List	   *qual_exprs = NIL;
+	List	   *elvar_rt_mappings = NIL;
+
+	rte = rt_fetch(rt_index, parsetree->rtable);
+
+	newsubquery = makeNode(Query);
+	newsubquery->commandType = CMD_SELECT;
+
+	if (list_length(rte->graph_pattern->path_pattern_list) != 1)
+		elog(ERROR, "unsupported path pattern list length");
+
+	element_patterns = linitial(rte->graph_pattern->path_pattern_list);
+
+	foreach(lc, element_patterns)
+	{
+		GraphElementPattern *gep = lfirst_node(GraphElementPattern, lc);
+		Oid			labelid = InvalidOid;
+		struct elvar_rt_mapping *erm;
+
+		if (!(gep->kind == VERTEX_PATTERN || gep->kind == EDGE_PATTERN_LEFT || gep->kind == EDGE_PATTERN_RIGHT))
+			elog(ERROR, "unsupported element pattern kind: %u", gep->kind);
+
+		if (gep->quantifier)
+			elog(ERROR, "element pattern quantifier not supported yet");
+
+		if (IsA(gep->labelexpr, GraphLabelRef))
+		{
+			GraphLabelRef *glr = castNode(GraphLabelRef, gep->labelexpr);
+			RangeTblEntry *r;
+			Oid			elid;
+			Oid			relid;
+			RTEPermissionInfo *rpi;
+			RangeTblRef *rtr;
+
+			r = makeNode(RangeTblEntry);
+			r->rtekind = RTE_RELATION;
+			elid = linitial_oid(get_elements_for_label(rte->relid, glr->labelname));
+			element_ids = lappend_oid(element_ids, elid);
+			relid = get_table_for_element(elid);
+			labelid = get_labelid(rte->relid, glr->labelname);
+			r->relid = relid;
+			r->relkind = get_rel_relkind(relid);
+			r->rellockmode = AccessShareLock;
+			r->inh = true;
+			newsubquery->rtable = lappend(newsubquery->rtable, r);
+
+			rpi = makeNode(RTEPermissionInfo);
+			rpi->relid = relid;
+			rpi->checkAsUser = 0;
+			rpi->requiredPerms = ACL_SELECT;
+			newsubquery->rteperminfos = lappend(newsubquery->rteperminfos, rpi);
+
+			r->perminfoindex = list_length(newsubquery->rteperminfos);
+
+			rtr = makeNode(RangeTblRef);
+			rtr->rtindex = list_length(newsubquery->rtable);
+			fromlist = lappend(fromlist, rtr);
+		}
+		else
+			elog(ERROR, "unsupported label expression type: %d", (int) nodeTag(gep->labelexpr));
+
+		erm = palloc0_object(struct elvar_rt_mapping);
+
+		erm->elvarname = gep->variable;
+		erm->labelid = labelid;
+		erm->rt_index = list_length(newsubquery->rtable);
+
+		elvar_rt_mappings = lappend(elvar_rt_mappings, erm);
+
+		if (gep->whereClause)
+		{
+			Node	   *tr;
+
+			tr = replace_property_refs(gep->whereClause, list_make1(erm));
+
+			qual_exprs = lappend(qual_exprs, tr);
+		}
+	}
+
+	/* Iterate over edges only */
+	for (int k = 1; k < list_length(element_ids); k += 2)
+	{
+		Oid			elid = list_nth_oid(element_ids, k);
+		HeapTuple	tuple;
+		Form_pg_propgraph_element pgeform;
+		GraphElementPattern *gep = list_nth_node(GraphElementPattern, element_patterns, k);
+		int			srcvertexoffset;
+		int			destvertexoffset;
+
+		tuple = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(elid));
+		if (!tuple)
+			elog(ERROR, "cache lookup failed for property graph element %u", elid);
+		pgeform = ((Form_pg_propgraph_element) GETSTRUCT(tuple));
+
+		if (gep->kind == EDGE_PATTERN_RIGHT)
+		{
+			srcvertexoffset = -1;
+			destvertexoffset = +1;
+		}
+		else if (gep->kind == EDGE_PATTERN_LEFT)
+		{
+			srcvertexoffset = +1;
+			destvertexoffset = -1;
+		}
+		else
+		{
+			Assert(false);
+			srcvertexoffset = 0;
+			destvertexoffset = 0;
+		}
+
+		/*
+		 * source link
+		 */
+		if (pgeform->pgesrcvertexid != list_nth_oid(element_ids, k + srcvertexoffset))
+		{
+			qual_exprs = lappend(qual_exprs, makeBoolConst(false, false));
+		}
+		else
+		{
+			qual_exprs = list_concat(qual_exprs,
+									 build_edge_vertex_link_quals(tuple, k + 1, k + 1 + srcvertexoffset,
+																  Anum_pg_propgraph_element_pgesrckey, Anum_pg_propgraph_element_pgesrcref));
+		}
+
+		/*
+		 * dest link
+		 */
+		if (pgeform->pgedestvertexid != list_nth_oid(element_ids, k + destvertexoffset))
+		{
+			qual_exprs = lappend(qual_exprs, makeBoolConst(false, false));
+		}
+		else
+		{
+			qual_exprs = list_concat(qual_exprs,
+									 build_edge_vertex_link_quals(tuple, k + 1, k + 1 + destvertexoffset,
+																  Anum_pg_propgraph_element_pgedestkey, Anum_pg_propgraph_element_pgedestref));
+		}
+
+		ReleaseSysCache(tuple);
+	}
+
+	newsubquery->jointree = makeFromExpr(fromlist, (Node *) makeBoolExpr(AND_EXPR, qual_exprs, -1));
+
+	foreach(lc, rte->graph_table_columns)
+	{
+		TargetEntry *te = lfirst_node(TargetEntry, lc);
+		Node	   *nte;
+
+		nte = replace_property_refs((Node *) te, elvar_rt_mappings);
+		newsubquery->targetList = lappend(newsubquery->targetList, nte);
+	}
+
+	AcquireRewriteLocks(newsubquery, true, false);
+
+	rte->rtekind = RTE_SUBQUERY;
+	rte->subquery = newsubquery;
+
+	/*
+	 * Reset no longer applicable fields, to appease
+	 * WRITE_READ_PARSE_PLAN_TREES.
+	 */
+	rte->graph_pattern = NULL;
+	rte->graph_table_columns = NIL;
+
+	return parsetree;
+}
+
+/*
+ * Get label OID from graph OID and label name.
+ *
+ * TODO: This could match more than one entry.  Right now it only returns the
+ * first one.
+ */
+static Oid
+get_labelid(Oid graphid, const char *labelname)
+{
+	Relation	rel;
+	SysScanDesc scan;
+	ScanKeyData key[2];
+	HeapTuple	tup;
+	Oid			result = InvalidOid;
+
+	rel = table_open(PropgraphLabelRelationId, RowShareLock);
+	ScanKeyInit(&key[0],
+				Anum_pg_propgraph_label_pglpgid,
+				BTEqualStrategyNumber,
+				F_OIDEQ, ObjectIdGetDatum(graphid));
+	ScanKeyInit(&key[1],
+				Anum_pg_propgraph_label_pgllabel,
+				BTEqualStrategyNumber,
+				F_NAMEEQ, CStringGetDatum(labelname));
+
+	scan = systable_beginscan(rel, PropgraphLabelGraphNameIndexId,
+							  true, NULL, 2, key);
+
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		result = ((Form_pg_propgraph_label) GETSTRUCT(tup))->oid;
+		break;
+	}
+
+	systable_endscan(scan);
+	table_close(rel, RowShareLock);
+
+	return result;
+}
+
+/*
+ * Get list of element OIDs that have a given label.
+ */
+static List *
+get_elements_for_label(Oid graphid, const char *labelname)
+{
+	Relation	rel;
+	SysScanDesc scan;
+	ScanKeyData key[2];
+	HeapTuple	tup;
+	List	   *result = NIL;
+
+	rel = table_open(PropgraphLabelRelationId, RowShareLock);
+	ScanKeyInit(&key[0],
+				Anum_pg_propgraph_label_pglpgid,
+				BTEqualStrategyNumber,
+				F_OIDEQ, ObjectIdGetDatum(graphid));
+	ScanKeyInit(&key[1],
+				Anum_pg_propgraph_label_pgllabel,
+				BTEqualStrategyNumber,
+				F_NAMEEQ, CStringGetDatum(labelname));
+
+	scan = systable_beginscan(rel, PropgraphLabelGraphNameIndexId,
+							  true, NULL, 2, key);
+
+	while (HeapTupleIsValid(tup = systable_getnext(scan)))
+	{
+		result = lappend_oid(result, ((Form_pg_propgraph_label) GETSTRUCT(tup))->pglelid);
+	}
+
+	systable_endscan(scan);
+	table_close(rel, RowShareLock);
+
+	return result;
+}
+
+/*
+ * Get the element table OID for a given element.
+ */
+static Oid
+get_table_for_element(Oid elid)
+{
+	return GetSysCacheOid1(PROPGRAPHELOID, Anum_pg_propgraph_element_pgerelid, ObjectIdGetDatum(elid));
+}
+
+/*
+ * Mutating property references into table variables
+ */
+
+struct replace_property_refs_context
+{
+	const List *mappings;
+};
+
+static Node *
+replace_property_refs_mutator(Node *node, struct replace_property_refs_context *context)
+{
+	if (node == NULL)
+		return NULL;
+	if (IsA(node, GraphPropertyRef))
+	{
+		GraphPropertyRef *gpr = (GraphPropertyRef *) node;
+		HeapTuple	tup;
+		Node	   *n;
+		ListCell   *lc;
+		struct elvar_rt_mapping *found_mapping = NULL;
+
+		foreach(lc, context->mappings)
+		{
+			struct elvar_rt_mapping *m = lfirst(lc);
+
+			if (m->elvarname && strcmp(gpr->elvarname, m->elvarname) == 0)
+			{
+				found_mapping = m;
+				break;
+			}
+		}
+		if (!found_mapping)
+			elog(ERROR, "undefined element variable \"%s\"", gpr->elvarname);
+
+		tup = SearchSysCache2(PROPGRAPHPROPNAME, ObjectIdGetDatum(found_mapping->labelid), CStringGetDatum(gpr->propname));
+		if (!tup)
+			elog(ERROR, "property \"%s\" of label %u not found", gpr->propname, found_mapping->labelid);
+
+		n = stringToNode(TextDatumGetCString(SysCacheGetAttrNotNull(PROPGRAPHPROPNAME, tup, Anum_pg_propgraph_property_pgpexpr)));
+		ChangeVarNodes(n, 1, found_mapping->rt_index, 0);
+
+		ReleaseSysCache(tup);
+
+		return n;
+	}
+	return expression_tree_mutator(node, replace_property_refs_mutator, context);
+}
+
+static Node *
+replace_property_refs(Node *node, const List *mappings)
+{
+	struct replace_property_refs_context context;
+
+	context.mappings = mappings;
+
+	return expression_tree_mutator(node, replace_property_refs_mutator, &context);
+}
+
+/*
+ * Build join qualification expressions between edge and vertex tables.
+ */
+static List *
+build_edge_vertex_link_quals(HeapTuple edgetup, int edgerti, int refrti, AttrNumber catalog_key_attnum, AttrNumber catalog_ref_attnum)
+{
+	List	   *quals = NIL;
+	Form_pg_propgraph_element pgeform;
+	Datum		datum;
+	Datum	   *d1,
+			   *d2;
+	int			n1,
+				n2;
+
+	pgeform = (Form_pg_propgraph_element) GETSTRUCT(edgetup);
+
+	datum = SysCacheGetAttrNotNull(PROPGRAPHELOID, edgetup, catalog_key_attnum);
+	deconstruct_array_builtin(DatumGetArrayTypeP(datum), INT2OID, &d1, NULL, &n1);
+
+	datum = SysCacheGetAttrNotNull(PROPGRAPHELOID, edgetup, catalog_ref_attnum);
+	deconstruct_array_builtin(DatumGetArrayTypeP(datum), INT2OID, &d2, NULL, &n2);
+
+	if (n1 != n2)
+		elog(ERROR, "array size key (%d) vs ref (%d) mismatch for element ID %u", catalog_key_attnum, catalog_ref_attnum, pgeform->oid);
+
+	for (int i = 0; i < n1; i++)
+	{
+		AttrNumber	keyattn = DatumGetInt16(d1[i]);
+		AttrNumber	refattn = DatumGetInt16(d2[i]);
+		Oid			atttypid;
+		TypeCacheEntry *typentry;
+		OpExpr	   *op;
+
+		/*
+		 * TODO: Assumes types the same on both sides; no collations yet. Some
+		 * of this could probably be shared with foreign key triggers.
+		 */
+		atttypid = get_atttype(pgeform->pgerelid, keyattn);
+		typentry = lookup_type_cache(atttypid, TYPECACHE_EQ_OPR);
+
+		op = makeNode(OpExpr);
+		op->location = -1;
+		op->opno = typentry->eq_opr;
+		op->opresulttype = BOOLOID;
+		op->args = list_make2(makeVar(edgerti, keyattn, atttypid, -1, 0, 0),
+							  makeVar(refrti, refattn, atttypid, -1, 0, 0));
+		quals = lappend(quals, op);
+	}
+
+	return quals;
+}
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index f60b34deb64..6b8192c0748 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -38,6 +38,7 @@
 #include "parser/parse_relation.h"
 #include "parser/parsetree.h"
 #include "rewrite/rewriteDefine.h"
+#include "rewrite/rewriteGraphTable.h"
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
 #include "rewrite/rewriteSearchCycle.h"
@@ -2027,6 +2028,17 @@ fireRIRrules(Query *parsetree, List *activeRIRs)
 
 		rte = rt_fetch(rt_index, parsetree->rtable);
 
+		/*
+		 * Convert GRAPH_TABLE clause into a subquery using relational
+		 * operators.  (This will change the rtekind to subquery, so it must
+		 * be done before the subquery handling below.)
+		 */
+		if (rte->rtekind == RTE_GRAPH_TABLE)
+		{
+			parsetree = rewriteGraphTable(parsetree, rt_index);
+			continue;
+		}
+
 		/*
 		 * A subquery RTE can't have associated rules, so there's nothing to
 		 * do to this level of the query, but we must recurse into the
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 8de821f9607..06c2c3a2b9b 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -47,6 +47,7 @@
 #include "commands/portalcmds.h"
 #include "commands/prepare.h"
 #include "commands/proclang.h"
+#include "commands/propgraphcmds.h"
 #include "commands/publicationcmds.h"
 #include "commands/schemacmds.h"
 #include "commands/seclabel.h"
@@ -155,6 +156,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 		case T_AlterOperatorStmt:
 		case T_AlterOwnerStmt:
 		case T_AlterPolicyStmt:
+		case T_AlterPropGraphStmt:
 		case T_AlterPublicationStmt:
 		case T_AlterRoleSetStmt:
 		case T_AlterRoleStmt:
@@ -185,6 +187,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 		case T_CreateOpFamilyStmt:
 		case T_CreatePLangStmt:
 		case T_CreatePolicyStmt:
+		case T_CreatePropGraphStmt:
 		case T_CreatePublicationStmt:
 		case T_CreateRangeStmt:
 		case T_CreateRoleStmt:
@@ -1743,6 +1746,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				commandCollected = true;
 				break;
 
+			case T_CreatePropGraphStmt:
+				address = CreatePropGraph(pstate, (CreatePropGraphStmt *) parsetree);
+				break;
+
+			case T_AlterPropGraphStmt:
+				address = AlterPropGraph(pstate, (AlterPropGraphStmt *) parsetree);
+				break;
+
 			case T_CreateTransformStmt:
 				address = CreateTransform((CreateTransformStmt *) parsetree);
 				break;
@@ -2011,6 +2022,7 @@ ExecDropStmt(DropStmt *stmt, bool isTopLevel)
 		case OBJECT_VIEW:
 		case OBJECT_MATVIEW:
 		case OBJECT_FOREIGN_TABLE:
+		case OBJECT_PROPGRAPH:
 			RemoveRelations(stmt);
 			break;
 		default:
@@ -2288,6 +2300,9 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_PROCEDURE:
 			tag = CMDTAG_ALTER_PROCEDURE;
 			break;
+		case OBJECT_PROPGRAPH:
+			tag = CMDTAG_ALTER_PROPERTY_GRAPH;
+			break;
 		case OBJECT_ROLE:
 			tag = CMDTAG_ALTER_ROLE;
 			break;
@@ -2564,6 +2579,9 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_INDEX:
 					tag = CMDTAG_DROP_INDEX;
 					break;
+				case OBJECT_PROPGRAPH:
+					tag = CMDTAG_DROP_PROPERTY_GRAPH;
+					break;
 				case OBJECT_TYPE:
 					tag = CMDTAG_DROP_TYPE;
 					break;
@@ -2945,6 +2963,14 @@ CreateCommandTag(Node *parsetree)
 			}
 			break;
 
+		case T_CreatePropGraphStmt:
+			tag = CMDTAG_CREATE_PROPERTY_GRAPH;
+			break;
+
+		case T_AlterPropGraphStmt:
+			tag = CMDTAG_ALTER_PROPERTY_GRAPH;
+			break;
+
 		case T_CreateTransformStmt:
 			tag = CMDTAG_CREATE_TRANSFORM;
 			break;
@@ -3642,6 +3668,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_CreatePropGraphStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterPropGraphStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 		case T_CreateTransformStmt:
 			lev = LOGSTMT_DDL;
 			break;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index a928a8c55df..aa11c4def60 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -35,6 +35,9 @@
 #include "catalog/pg_operator.h"
 #include "catalog/pg_partitioned_table.h"
 #include "catalog/pg_proc.h"
+#include "catalog/pg_propgraph_element.h"
+#include "catalog/pg_propgraph_label.h"
+#include "catalog/pg_propgraph_property.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -346,6 +349,9 @@ static char *pg_get_indexdef_worker(Oid indexrelid, int colno,
 									bool attrsOnly, bool keysOnly,
 									bool showTblSpc, bool inherits,
 									int prettyFlags, bool missing_ok);
+static void make_propgraphdef_elements(StringInfo buf, Oid pgrelid, char pgekind);
+static void make_propgraphdef_labels(StringInfo buf, Oid elid, const char *elalias, Oid elrelid);
+static void make_propgraphdef_properties(StringInfo buf, Oid labelid, Oid elrelid);
 static char *pg_get_statisticsobj_worker(Oid statextid, bool columns_only,
 										 bool missing_ok);
 static char *pg_get_partkeydef_worker(Oid relid, int prettyFlags,
@@ -1570,6 +1576,298 @@ pg_get_querydef(Query *query, bool pretty)
 	return buf.data;
 }
 
+/*
+ * pg_get_propgraphdef - get the definition of a property graph
+ */
+Datum
+pg_get_propgraphdef(PG_FUNCTION_ARGS)
+{
+	Oid			pgrelid = PG_GETARG_OID(0);
+	StringInfoData buf;
+	HeapTuple	classtup;
+	Form_pg_class classform;
+	char	   *name;
+	char	   *nsp;
+
+	initStringInfo(&buf);
+
+	classtup = SearchSysCache1(RELOID, ObjectIdGetDatum(pgrelid));
+	if (!HeapTupleIsValid(classtup))
+		PG_RETURN_NULL();
+
+	classform = (Form_pg_class) GETSTRUCT(classtup);
+	name = NameStr(classform->relname);
+
+	if (classform->relkind != RELKIND_PROPGRAPH)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("\"%s\" is not a property graph", name)));
+
+	nsp = get_namespace_name(classform->relnamespace);
+
+	appendStringInfo(&buf, "CREATE PROPERTY GRAPH %s",
+					 quote_qualified_identifier(nsp, name));
+
+	ReleaseSysCache(classtup);
+
+	make_propgraphdef_elements(&buf, pgrelid, PGEKIND_VERTEX);
+	make_propgraphdef_elements(&buf, pgrelid, PGEKIND_EDGE);
+
+	PG_RETURN_TEXT_P(string_to_text(buf.data));
+}
+
+/*
+ * Generates a VERTEX TABLES (...) or EDGE TABLES (...) clause.  Pass in the
+ * property graph relation OID and the element kind (vertex or edge).  Result
+ * is appended to buf.
+ */
+static void
+make_propgraphdef_elements(StringInfo buf, Oid pgrelid, char pgekind)
+{
+	Relation	pgerel;
+	ScanKeyData scankey[1];
+	SysScanDesc scan;
+	bool		first;
+	HeapTuple	tup;
+
+	pgerel = table_open(PropgraphElementRelationId, AccessShareLock);
+
+	ScanKeyInit(&scankey[0],
+				Anum_pg_propgraph_element_pgepgid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(pgrelid));
+
+	scan = systable_beginscan(pgerel, PropgraphElementAliasIndexId, true, NULL, 1, scankey);
+
+	first = true;
+	while ((tup = systable_getnext(scan)))
+	{
+		Form_pg_propgraph_element pgeform = (Form_pg_propgraph_element) GETSTRUCT(tup);
+		char	   *relname;
+		Datum		datum;
+		bool		isnull;
+
+		if (pgeform->pgekind != pgekind)
+			continue;
+
+		if (first)
+		{
+			appendStringInfo(buf, "\n    %s TABLES (\n", pgekind == PGEKIND_VERTEX ? "VERTEX" : "EDGE");
+			first = false;
+		}
+		else
+			appendStringInfo(buf, ",\n");
+
+		relname = get_rel_name(pgeform->pgerelid);
+		if (relname && strcmp(relname, NameStr(pgeform->pgealias)) == 0)
+			appendStringInfo(buf, "        %s",
+							 generate_relation_name(pgeform->pgerelid, NIL));
+		else
+			appendStringInfo(buf, "        %s AS %s",
+							 generate_relation_name(pgeform->pgerelid, NIL),
+							 NameStr(pgeform->pgealias));
+
+		datum = heap_getattr(tup, Anum_pg_propgraph_element_pgekey, RelationGetDescr(pgerel), &isnull);
+		if (!isnull)
+		{
+			appendStringInfoString(buf, " KEY (");
+			decompile_column_index_array(datum, pgeform->pgerelid, buf);
+			appendStringInfoString(buf, ")");
+		}
+		else
+			elog(ERROR, "null pgekey for element %u", pgeform->oid);
+
+		if (pgekind == PGEKIND_EDGE)
+		{
+			Datum		srckey;
+			Datum		srcref;
+			Datum		destkey;
+			Datum		destref;
+			HeapTuple	tup2;
+			Form_pg_propgraph_element pgeform2;
+
+			datum = heap_getattr(tup, Anum_pg_propgraph_element_pgesrckey, RelationGetDescr(pgerel), &isnull);
+			srckey = isnull ? 0 : datum;
+			datum = heap_getattr(tup, Anum_pg_propgraph_element_pgesrcref, RelationGetDescr(pgerel), &isnull);
+			srcref = isnull ? 0 : datum;
+			datum = heap_getattr(tup, Anum_pg_propgraph_element_pgedestkey, RelationGetDescr(pgerel), &isnull);
+			destkey = isnull ? 0 : datum;
+			datum = heap_getattr(tup, Anum_pg_propgraph_element_pgedestref, RelationGetDescr(pgerel), &isnull);
+			destref = isnull ? 0 : datum;
+
+			appendStringInfoString(buf, " SOURCE");
+			tup2 = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(pgeform->pgesrcvertexid));
+			if (!tup2)
+				elog(ERROR, "cache lookup failed for property graph element %u", pgeform->pgesrcvertexid);
+			pgeform2 = (Form_pg_propgraph_element) GETSTRUCT(tup2);
+			if (srckey)
+			{
+				appendStringInfoString(buf, " KEY (");
+				decompile_column_index_array(srckey, pgeform->pgerelid, buf);
+				appendStringInfo(buf, ") REFERENCES %s (", NameStr(pgeform2->pgealias));
+				decompile_column_index_array(srcref, pgeform2->pgerelid, buf);
+				appendStringInfoString(buf, ")");
+			}
+			else
+				appendStringInfo(buf, " %s ", NameStr(pgeform2->pgealias));
+			ReleaseSysCache(tup2);
+
+			appendStringInfoString(buf, " DESTINATION");
+			tup2 = SearchSysCache1(PROPGRAPHELOID, ObjectIdGetDatum(pgeform->pgedestvertexid));
+			if (!tup2)
+				elog(ERROR, "cache lookup failed for property graph element %u", pgeform->pgedestvertexid);
+			pgeform2 = (Form_pg_propgraph_element) GETSTRUCT(tup2);
+			if (destkey)
+			{
+				appendStringInfoString(buf, " KEY (");
+				decompile_column_index_array(destkey, pgeform->pgerelid, buf);
+				appendStringInfo(buf, ") REFERENCES %s (", NameStr(pgeform2->pgealias));
+				decompile_column_index_array(destref, pgeform2->pgerelid, buf);
+				appendStringInfoString(buf, ")");
+			}
+			else
+				appendStringInfo(buf, " %s", NameStr(pgeform2->pgealias));
+			ReleaseSysCache(tup2);
+		}
+
+		make_propgraphdef_labels(buf, pgeform->oid, NameStr(pgeform->pgealias), pgeform->pgerelid);
+	}
+	if (!first)
+		appendStringInfo(buf, "\n    )");
+
+	systable_endscan(scan);
+	table_close(pgerel, AccessShareLock);
+}
+
+/*
+ * Generates label and properties list.  Pass in the element OID, the element
+ * alias, and the graph relation OID.  Result is append to buf.
+ */
+static void
+make_propgraphdef_labels(StringInfo buf, Oid elid, const char *elalias, Oid elrelid)
+{
+	Relation	pglrel;
+	ScanKeyData scankey[1];
+	SysScanDesc scan;
+	int			count;
+	HeapTuple	tup;
+
+	pglrel = table_open(PropgraphLabelRelationId, AccessShareLock);
+
+	ScanKeyInit(&scankey[0],
+				Anum_pg_propgraph_label_pglelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(elid));
+
+	count = 0;
+	scan = systable_beginscan(pglrel, PropgraphLabelLabelIndexId, true, NULL, 1, scankey);
+	while ((tup = systable_getnext(scan)))
+	{
+		count++;
+	}
+	systable_endscan(scan);
+
+	/* XXX need to re-init scan key for second scan */
+	ScanKeyInit(&scankey[0],
+				Anum_pg_propgraph_label_pglelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(elid));
+
+	scan = systable_beginscan(pglrel, PropgraphLabelLabelIndexId, true, NULL, 1, scankey);
+
+	while ((tup = systable_getnext(scan)))
+	{
+		Form_pg_propgraph_label pglform = (Form_pg_propgraph_label) GETSTRUCT(tup);
+
+		if (strcmp(NameStr(pglform->pgllabel), elalias) == 0)
+		{
+			/* If the default label is the only label, don't print anything. */
+			if (count != 1)
+				appendStringInfo(buf, " DEFAULT LABEL");
+		}
+		else
+			appendStringInfo(buf, " LABEL %s",
+							 quote_identifier(NameStr(pglform->pgllabel)));
+
+		make_propgraphdef_properties(buf, pglform->oid, elrelid);
+	}
+
+	systable_endscan(scan);
+
+	table_close(pglrel, AccessShareLock);
+}
+
+/*
+ * Generates element table properties clause (PROPERTIES (...) or NO
+ * PROPERTIES).  Pass in label ODI and element table OID.  Result is appended
+ * to buf.
+ */
+static void
+make_propgraphdef_properties(StringInfo buf, Oid labelid, Oid elrelid)
+{
+	List	   *context;
+	Relation	pgprel;
+	ScanKeyData scankey[1];
+	SysScanDesc scan;
+	HeapTuple	tup;
+	bool		first;
+
+	context = deparse_context_for(get_relation_name(elrelid), elrelid);
+
+	pgprel = table_open(PropgraphPropertyRelationId, AccessShareLock);
+
+	ScanKeyInit(&scankey[0],
+				Anum_pg_propgraph_property_pgplabelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(labelid));
+
+	/*
+	 * Note: Use of index on property name also ensures that properties are
+	 * put out in a deterministic order.
+	 */
+	scan = systable_beginscan(pgprel, PropgraphPropertyNameIndexId, true, NULL, 1, scankey);
+
+	first = true;
+	while ((tup = systable_getnext(scan)))
+	{
+		Form_pg_propgraph_property pgpform = (Form_pg_propgraph_property) GETSTRUCT(tup);
+		Datum		exprDatum;
+		bool		isnull;
+		char	   *tmp;
+		Node	   *expr;
+
+		if (first)
+		{
+			appendStringInfo(buf, " PROPERTIES (");
+			first = false;
+		}
+		else
+			appendStringInfo(buf, ", ");
+
+		exprDatum = heap_getattr(tup, Anum_pg_propgraph_property_pgpexpr, RelationGetDescr(pgprel), &isnull);
+		Assert(!isnull);
+		tmp = TextDatumGetCString(exprDatum);
+		expr = stringToNode(tmp);
+		pfree(tmp);
+
+		if (IsA(expr, Var) && strcmp(NameStr(pgpform->pgpname), get_attname(elrelid, castNode(Var, expr)->varattno, false)) == 0)
+			appendStringInfo(buf, "%s",
+							 NameStr(pgpform->pgpname));
+		else
+			appendStringInfo(buf, "%s AS %s",
+							 deparse_expression_pretty(expr, context, false, false, 0, 0),
+							 NameStr(pgpform->pgpname));
+	}
+
+	if (first)
+		appendStringInfo(buf, " NO PROPERTIES");
+	else
+		appendStringInfo(buf, ")");
+
+	systable_endscan(scan);
+	table_close(pgprel, AccessShareLock);
+}
+
 /*
  * pg_get_statisticsobjdef
  *		Get the definition of an extended statistics object
@@ -7232,6 +7530,171 @@ get_utility_query_def(Query *query, deparse_context *context)
 	}
 }
 
+
+/*
+ * Parse back a graph label expression
+ */
+static void
+get_graph_label_expr(Node *label_expr, deparse_context *context)
+{
+	StringInfo	buf = context->buf;
+
+	check_stack_depth();
+
+	switch (nodeTag(label_expr))
+	{
+		case T_GraphLabelRef:
+			{
+				GraphLabelRef *lref = (GraphLabelRef *) label_expr;
+
+				appendStringInfoString(buf, quote_identifier(lref->labelname));
+				break;
+			}
+
+		case T_BoolExpr:
+			{
+				BoolExpr   *be = (BoolExpr *) label_expr;
+				ListCell   *lc;
+				bool		first = true;
+
+				Assert(be->boolop == OR_EXPR);
+
+				foreach(lc, be->args)
+				{
+					if (!first)
+					{
+						if (be->boolop == OR_EXPR)
+							appendStringInfoString(buf, "|");
+					}
+					else
+						first = false;
+					get_graph_label_expr(lfirst(lc), context);
+				}
+
+				break;
+			}
+
+		default:
+			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(label_expr));
+			break;
+	}
+}
+
+/*
+ * Parse back a path pattern expression
+ */
+static void
+get_path_pattern_expr_def(List *path_pattern_expr, deparse_context *context)
+{
+	StringInfo	buf = context->buf;
+	ListCell   *lc;
+
+	foreach(lc, path_pattern_expr)
+	{
+		GraphElementPattern *gep = lfirst_node(GraphElementPattern, lc);
+		const char *sep = "";
+
+		switch (gep->kind)
+		{
+			case VERTEX_PATTERN:
+				appendStringInfoString(buf, "(");
+				break;
+			case EDGE_PATTERN_LEFT:
+				appendStringInfoString(buf, "<-[");
+				break;
+			case EDGE_PATTERN_RIGHT:
+			case EDGE_PATTERN_ANY:
+				appendStringInfoString(buf, "-[");
+				break;
+			case PAREN_EXPR:
+				appendStringInfoString(buf, "(");
+				break;
+		}
+
+		if (gep->variable)
+		{
+			appendStringInfoString(buf, gep->variable);
+			sep = " ";
+		}
+
+		if (gep->labelexpr)
+		{
+			appendStringInfoString(buf, sep);
+			appendStringInfoString(buf, "IS ");
+			get_graph_label_expr(gep->labelexpr, context);
+			sep = " ";
+		}
+
+		if (gep->subexpr)
+		{
+			appendStringInfoString(buf, sep);
+			get_path_pattern_expr_def(gep->subexpr, context);
+			sep = " ";
+		}
+
+		if (gep->whereClause)
+		{
+			appendStringInfoString(buf, sep);
+			appendStringInfoString(buf, "WHERE ");
+			get_rule_expr(gep->whereClause, context, false);
+		}
+
+		switch (gep->kind)
+		{
+			case VERTEX_PATTERN:
+				appendStringInfoString(buf, ")");
+				break;
+			case EDGE_PATTERN_LEFT:
+			case EDGE_PATTERN_ANY:
+				appendStringInfoString(buf, "]-");
+				break;
+			case EDGE_PATTERN_RIGHT:
+				appendStringInfoString(buf, "]->");
+				break;
+			case PAREN_EXPR:
+				appendStringInfoString(buf, ")");
+				break;
+		}
+
+		if (gep->quantifier)
+		{
+			int			lower = linitial_int(gep->quantifier);
+			int			upper = lsecond_int(gep->quantifier);
+
+			appendStringInfo(buf, "{%d,%d}", lower, upper);
+		}
+	}
+}
+
+/*
+ * Parse back a graph pattern
+ */
+static void
+get_graph_pattern_def(GraphPattern *graph_pattern, deparse_context *context)
+{
+	StringInfo	buf = context->buf;
+	ListCell   *lc;
+	bool		first = true;
+
+	foreach(lc, graph_pattern->path_pattern_list)
+	{
+		List	   *path_pattern_expr = lfirst_node(List, lc);
+
+		if (!first)
+			appendStringInfoString(buf, ", ");
+		else
+			first = false;
+
+		get_path_pattern_expr_def(path_pattern_expr, context);
+	}
+
+	if (graph_pattern->whereClause)
+	{
+		appendStringInfoString(buf, "WHERE ");
+		get_rule_expr(graph_pattern->whereClause, context, false);
+	}
+}
+
 /*
  * Display a Var appropriately.
  *
@@ -7794,6 +8257,7 @@ get_name_for_var_field(Var *var, int fieldno,
 		case RTE_RELATION:
 		case RTE_VALUES:
 		case RTE_NAMEDTUPLESTORE:
+		case RTE_GRAPH_TABLE:
 		case RTE_RESULT:
 
 			/*
@@ -9813,6 +10277,14 @@ get_rule_expr(Node *node, deparse_context *context,
 			get_tablefunc((TableFunc *) node, context, showimplicit);
 			break;
 
+		case T_GraphPropertyRef:
+			{
+				GraphPropertyRef *gpr = (GraphPropertyRef *) node;
+
+				appendStringInfo(buf, "%s.%s", quote_identifier(gpr->elvarname), quote_identifier(gpr->propname));
+				break;
+			}
+
 		default:
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
 			break;
@@ -11439,6 +11911,36 @@ get_from_clause_item(Node *jtnode, Query *query, deparse_context *context)
 			case RTE_TABLEFUNC:
 				get_tablefunc(rte->tablefunc, context, true);
 				break;
+			case RTE_GRAPH_TABLE:
+				appendStringInfoString(buf, "GRAPH_TABLE (");
+				appendStringInfoString(buf, generate_relation_name(rte->relid, context->namespaces));
+				appendStringInfoString(buf, " MATCH ");
+				get_graph_pattern_def(rte->graph_pattern, context);
+				appendStringInfoString(buf, " COLUMNS (");
+				{
+					ListCell   *lc;
+					bool		first = true;
+
+					foreach(lc, rte->graph_table_columns)
+					{
+						TargetEntry *te = lfirst_node(TargetEntry, lc);
+						deparse_context context = {0};
+
+						if (!first)
+							appendStringInfoString(buf, ", ");
+						else
+							first = false;
+
+						context.buf = buf;
+
+						get_rule_expr((Node *) te->expr, &context, false);
+						appendStringInfoString(buf, " AS ");
+						appendStringInfoString(buf, quote_identifier(te->resname));
+					}
+				}
+				appendStringInfoString(buf, ")");
+				appendStringInfoString(buf, ")");
+				break;
 			case RTE_VALUES:
 				/* Values list RTE */
 				appendStringInfoChar(buf, '(');
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 0ed18b72d63..bc34fc7d6b9 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -519,7 +519,8 @@ flagInhAttrs(Archive *fout, DumpOptions *dopt, TableInfo *tblinfo, int numTables
 		/* Some kinds never have parents */
 		if (tbinfo->relkind == RELKIND_SEQUENCE ||
 			tbinfo->relkind == RELKIND_VIEW ||
-			tbinfo->relkind == RELKIND_MATVIEW)
+			tbinfo->relkind == RELKIND_MATVIEW ||
+			tbinfo->relkind == RELKIND_PROPGRAPH)
 			continue;
 
 		/* Don't bother computing anything for non-target tables, either */
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index d97ebaff5b8..ebd74e40538 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3456,6 +3456,7 @@ _getObjectDescription(PQExpBuffer buf, const TocEntry *te)
 		strcmp(type, "DOMAIN") == 0 ||
 		strcmp(type, "FOREIGN TABLE") == 0 ||
 		strcmp(type, "MATERIALIZED VIEW") == 0 ||
+		strcmp(type, "PROPERTY GRAPH") == 0 ||
 		strcmp(type, "SEQUENCE") == 0 ||
 		strcmp(type, "STATISTICS") == 0 ||
 		strcmp(type, "TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 2225a12718b..80f9334e6ce 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -1601,10 +1601,10 @@ expand_table_name_patterns(Archive *fout,
 						  "\n     LEFT JOIN pg_catalog.pg_namespace n"
 						  "\n     ON n.oid OPERATOR(pg_catalog.=) c.relnamespace"
 						  "\nWHERE c.relkind OPERATOR(pg_catalog.=) ANY"
-						  "\n    (array['%c', '%c', '%c', '%c', '%c', '%c'])\n",
+						  "\n    (array['%c', '%c', '%c', '%c', '%c', '%c', '%c'])\n",
 						  RELKIND_RELATION, RELKIND_SEQUENCE, RELKIND_VIEW,
 						  RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE,
-						  RELKIND_PARTITIONED_TABLE);
+						  RELKIND_PARTITIONED_TABLE, RELKIND_PROPGRAPH);
 		initPQExpBuffer(&dbbuf);
 		processSQLNamePattern(GetConnection(fout), query, cell->val, true,
 							  false, "n.nspname", "c.relname", NULL,
@@ -2748,6 +2748,9 @@ makeTableDataInfo(DumpOptions *dopt, TableInfo *tbinfo)
 	if (tbinfo->dataObj != NULL)
 		return;
 
+	/* Skip property graphs (no data to dump) */
+	if (tbinfo->relkind == RELKIND_PROPGRAPH)
+		return;
 	/* Skip VIEWs (no data to dump) */
 	if (tbinfo->relkind == RELKIND_VIEW)
 		return;
@@ -6805,7 +6808,8 @@ getTables(Archive *fout, int *numTables)
 						 CppAsString2(RELKIND_COMPOSITE_TYPE) ", "
 						 CppAsString2(RELKIND_MATVIEW) ", "
 						 CppAsString2(RELKIND_FOREIGN_TABLE) ", "
-						 CppAsString2(RELKIND_PARTITIONED_TABLE) ")\n"
+						 CppAsString2(RELKIND_PARTITIONED_TABLE) ", "
+						 CppAsString2(RELKIND_PROPGRAPH) ")\n"
 						 "ORDER BY c.oid");
 
 	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
@@ -15869,8 +15873,6 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
 		reltypename = "VIEW";
 
-		appendPQExpBuffer(delq, "DROP VIEW %s;\n", qualrelname);
-
 		if (dopt->binary_upgrade)
 			binary_upgrade_set_pg_class_oids(fout, q,
 											 tbinfo->dobj.catId.oid, false);
@@ -15896,6 +15898,47 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 			appendPQExpBuffer(q, "\n  WITH %s CHECK OPTION", tbinfo->checkoption);
 		appendPQExpBufferStr(q, ";\n");
 	}
+	else if (tbinfo->relkind == RELKIND_PROPGRAPH)
+	{
+		PQExpBuffer query = createPQExpBuffer();
+		PGresult   *res;
+		int			len;
+
+		reltypename = "PROPERTY GRAPH";
+
+		if (dopt->binary_upgrade)
+			binary_upgrade_set_pg_class_oids(fout, q,
+											 tbinfo->dobj.catId.oid, false);
+
+		appendPQExpBuffer(query,
+						  "SELECT pg_catalog.pg_get_propgraphdef('%u'::pg_catalog.oid) AS pgdef",
+						  tbinfo->dobj.catId.oid);
+
+		res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+		if (PQntuples(res) != 1)
+		{
+			if (PQntuples(res) < 1)
+				pg_fatal("query to obtain definition of property graph \"%s\" returned no data",
+						 tbinfo->dobj.name);
+			else
+				pg_fatal("query to obtain definition of property graph \"%s\" returned more than one definition",
+						 tbinfo->dobj.name);
+		}
+
+		len = PQgetlength(res, 0, 0);
+
+		if (len == 0)
+			pg_fatal("definition of property graph \"%s\" appears to be empty (length zero)",
+					 tbinfo->dobj.name);
+
+		appendPQExpBufferStr(q, PQgetvalue(res, 0, 0));
+
+		PQclear(res);
+		destroyPQExpBuffer(query);
+
+		appendPQExpBufferStr(q, ";\n");
+	}
 	else
 	{
 		char	   *partkeydef = NULL;
@@ -15971,8 +16014,6 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		numParents = tbinfo->numParents;
 		parents = tbinfo->parents;
 
-		appendPQExpBuffer(delq, "DROP %s %s;\n", reltypename, qualrelname);
-
 		if (dopt->binary_upgrade)
 			binary_upgrade_set_pg_class_oids(fout, q,
 											 tbinfo->dobj.catId.oid, false);
@@ -16590,6 +16631,8 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 		appendPQExpBuffer(q, "\nALTER TABLE ONLY %s FORCE ROW LEVEL SECURITY;\n",
 						  qualrelname);
 
+	appendPQExpBuffer(delq, "DROP %s %s;\n", reltypename, qualrelname);
+
 	if (dopt->binary_upgrade)
 		binary_upgrade_extension_member(q, &tbinfo->dobj,
 										reltypename, qrelname,
@@ -18423,6 +18466,16 @@ getDependencies(Archive *fout)
 						 "classid = 'pg_amproc'::regclass AND objid = p.oid "
 						 "AND NOT (refclassid = 'pg_opfamily'::regclass AND amprocfamily = refobjid)\n");
 
+	/*
+	 * Translate dependencies of pg_propgraph_element entries into
+	 * dependencies of their parent pg_class entry.
+	 */
+	appendPQExpBufferStr(query, "UNION ALL\n"
+						 "SELECT 'pg_class'::regclass AS classid, pgepgid AS objid, refclassid, refobjid, deptype "
+						 "FROM pg_depend d, pg_propgraph_element pge "
+						 "WHERE deptype NOT IN ('p', 'e', 'i') AND "
+						 "classid = 'pg_propgraph_element'::regclass AND objid = pge.oid\n");
+
 	/* Sort the output for efficiency below */
 	appendPQExpBufferStr(query, "ORDER BY 1,2");
 
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 00b5092713d..ea0d6beb092 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -2959,6 +2959,17 @@
 		},
 	},
 
+	'CREATE PROPERTY GRAPH propgraph' => {
+		create_order => 20,
+		create_sql   => 'CREATE PROPERTY GRAPH dump_test.propgraph;',
+		regexp => qr/^
+			\QCREATE PROPERTY GRAPH dump_test.propgraph\E;
+			/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, only_dump_measurement => 1, },
+	},
+
 	'CREATE PUBLICATION pub1' => {
 		create_order => 50,
 		create_sql => 'CREATE PUBLICATION pub1;',
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 5c906e48068..ea53da0f5ad 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -775,7 +775,7 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 					success = describeTableDetails(pattern, show_verbose, show_system);
 				else
 					/* standard listing of interesting things */
-					success = listTables("tvmsE", NULL, show_verbose, show_system);
+					success = listTables("tvmsEG", NULL, show_verbose, show_system);
 				break;
 			case 'A':
 				{
@@ -906,6 +906,7 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 			case 'i':
 			case 's':
 			case 'E':
+			case 'G':
 				success = listTables(&cmd[1], pattern, show_verbose, show_system);
 				break;
 			case 'r':
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index b6a4eb1d565..60d23307613 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1025,6 +1025,7 @@ permissionsList(const char *pattern, bool showSystem)
 					  " WHEN " CppAsString2(RELKIND_MATVIEW) " THEN '%s'"
 					  " WHEN " CppAsString2(RELKIND_SEQUENCE) " THEN '%s'"
 					  " WHEN " CppAsString2(RELKIND_FOREIGN_TABLE) " THEN '%s'"
+					  " WHEN " CppAsString2(RELKIND_PROPGRAPH) " THEN '%s'"
 					  " WHEN " CppAsString2(RELKIND_PARTITIONED_TABLE) " THEN '%s'"
 					  " END as \"%s\",\n"
 					  "  ",
@@ -1035,6 +1036,7 @@ permissionsList(const char *pattern, bool showSystem)
 					  gettext_noop("materialized view"),
 					  gettext_noop("sequence"),
 					  gettext_noop("foreign table"),
+					  gettext_noop("property graph"),
 					  gettext_noop("partitioned table"),
 					  gettext_noop("Type"));
 
@@ -1126,6 +1128,7 @@ permissionsList(const char *pattern, bool showSystem)
 						 CppAsString2(RELKIND_MATVIEW) ","
 						 CppAsString2(RELKIND_SEQUENCE) ","
 						 CppAsString2(RELKIND_FOREIGN_TABLE) ","
+						 CppAsString2(RELKIND_PROPGRAPH) ","
 						 CppAsString2(RELKIND_PARTITIONED_TABLE) ")\n");
 
 	if (!showSystem && !pattern)
@@ -2009,6 +2012,10 @@ describeOneTableDetails(const char *schemaname,
 				printfPQExpBuffer(&title, _("Partitioned table \"%s.%s\""),
 								  schemaname, relationname);
 			break;
+		case RELKIND_PROPGRAPH:
+			printfPQExpBuffer(&title, _("Property graph \"%s.%s\""),
+							  schemaname, relationname);
+			break;
 		default:
 			/* untranslated unknown relkind */
 			printfPQExpBuffer(&title, "?%c? \"%s.%s\"",
@@ -3099,6 +3106,32 @@ describeOneTableDetails(const char *schemaname,
 		}
 	}
 
+	/* Add property graph definition in verbose mode */
+	if (tableinfo.relkind == RELKIND_PROPGRAPH && verbose)
+	{
+		PGresult   *result;
+		char	   *pgdef = NULL;
+
+		printfPQExpBuffer(&buf,
+						  "SELECT pg_catalog.pg_get_propgraphdef('%s'::pg_catalog.oid);",
+						  oid);
+		result = PSQLexec(buf.data);
+		if (!result)
+			goto error_return;
+
+		if (PQntuples(result) > 0)
+			pgdef = pg_strdup(PQgetvalue(result, 0, 0));
+
+		PQclear(result);
+
+		if (pgdef)
+		{
+			printTableAddFooter(&cont, _("Property graph definition:"));
+			printfPQExpBuffer(&buf, " %s", pgdef);
+			printTableAddFooter(&cont, buf.data);
+		}
+	}
+
 	/* Get view_def if table is a view or materialized view */
 	if ((tableinfo.relkind == RELKIND_VIEW ||
 		 tableinfo.relkind == RELKIND_MATVIEW) && verbose)
@@ -3950,6 +3983,7 @@ describeRoleGrants(const char *pattern, bool showSystem)
  * m - materialized views
  * s - sequences
  * E - foreign table (Note: different from 'f', the relkind value)
+ * G - property graphs
  * (any order of the above is fine)
  */
 bool
@@ -3961,6 +3995,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 	bool		showMatViews = strchr(tabtypes, 'm') != NULL;
 	bool		showSeq = strchr(tabtypes, 's') != NULL;
 	bool		showForeign = strchr(tabtypes, 'E') != NULL;
+	bool		showPropGraphs = strchr(tabtypes, 'G') != NULL;
 
 	PQExpBufferData buf;
 	PGresult   *res;
@@ -3969,8 +4004,8 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 	bool		translate_columns[] = {false, false, true, false, false, false, false, false, false};
 
 	/* If tabtypes is empty, we default to \dtvmsE (but see also command.c) */
-	if (!(showTables || showIndexes || showViews || showMatViews || showSeq || showForeign))
-		showTables = showViews = showMatViews = showSeq = showForeign = true;
+	if (!(showTables || showIndexes || showViews || showMatViews || showSeq || showForeign || showPropGraphs))
+		showTables = showViews = showMatViews = showSeq = showForeign = showPropGraphs = true;
 
 	initPQExpBuffer(&buf);
 
@@ -3987,6 +4022,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 					  " WHEN " CppAsString2(RELKIND_FOREIGN_TABLE) " THEN '%s'"
 					  " WHEN " CppAsString2(RELKIND_PARTITIONED_TABLE) " THEN '%s'"
 					  " WHEN " CppAsString2(RELKIND_PARTITIONED_INDEX) " THEN '%s'"
+					  " WHEN " CppAsString2(RELKIND_PROPGRAPH) " THEN '%s'"
 					  " END as \"%s\",\n"
 					  "  pg_catalog.pg_get_userbyid(c.relowner) as \"%s\"",
 					  gettext_noop("Schema"),
@@ -4000,6 +4036,7 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 					  gettext_noop("foreign table"),
 					  gettext_noop("partitioned table"),
 					  gettext_noop("partitioned index"),
+					  gettext_noop("property graph"),
 					  gettext_noop("Type"),
 					  gettext_noop("Owner"));
 	cols_so_far = 4;
@@ -4083,6 +4120,8 @@ listTables(const char *tabtypes, const char *pattern, bool verbose, bool showSys
 		appendPQExpBufferStr(&buf, "'s',"); /* was RELKIND_SPECIAL */
 	if (showForeign)
 		appendPQExpBufferStr(&buf, CppAsString2(RELKIND_FOREIGN_TABLE) ",");
+	if (showPropGraphs)
+		appendPQExpBufferStr(&buf, CppAsString2(RELKIND_PROPGRAPH) ",");
 
 	appendPQExpBufferStr(&buf, "''");	/* dummy */
 	appendPQExpBufferStr(&buf, ")\n");
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 4e79a819d87..a8f04c156b6 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -246,6 +246,7 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dFp[+] [PATTERN]      list text search parsers\n");
 	HELP0("  \\dFt[+] [PATTERN]      list text search templates\n");
 	HELP0("  \\dg[S+] [PATTERN]      list roles\n");
+	HELP0("  \\dG[S+] [PATTERN]      list property graphs");
 	HELP0("  \\di[S+] [PATTERN]      list indexes\n");
 	HELP0("  \\dl[+]                 list large objects, same as \\lo_list\n");
 	HELP0("  \\dL[S+] [PATTERN]      list procedural languages\n");
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 151a5211ee4..01c3924feef 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -788,6 +788,14 @@ static const SchemaQuery Query_for_list_of_partitioned_indexes = {
 	.result = "c.relname",
 };
 
+static const SchemaQuery Query_for_list_of_propgraphs = {
+	.catname = "pg_catalog.pg_class c",
+	.selcondition = "c.relkind IN (" CppAsString2(RELKIND_PROPGRAPH) ")",
+	.viscondition = "pg_catalog.pg_table_is_visible(c.oid)",
+	.namespace = "c.relnamespace",
+	.result = "pg_catalog.quote_ident(c.relname)",
+};
+
 
 /* All relations */
 static const SchemaQuery Query_for_list_of_relations = {
@@ -1255,6 +1263,7 @@ static const pgsql_thing_t words_after_create[] = {
 	{"PARSER", NULL, NULL, &Query_for_list_of_ts_parsers, NULL, THING_NO_SHOW},
 	{"POLICY", NULL, NULL, NULL},
 	{"PROCEDURE", NULL, NULL, Query_for_list_of_procedures},
+	{"PROPERTY GRAPH", NULL, NULL, &Query_for_list_of_propgraphs},
 	{"PUBLICATION", NULL, Query_for_list_of_publications},
 	{"ROLE", Query_for_list_of_roles},
 	{"ROUTINE", NULL, NULL, &Query_for_list_of_routines, NULL, THING_NO_CREATE},
@@ -2304,6 +2313,12 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("ALTER", "POLICY", MatchAny, "ON", MatchAny, "WITH", "CHECK"))
 		COMPLETE_WITH("(");
 
+	/* ALTER PROPERTY GRAPH */
+	else if (Matches("ALTER", "PROPERTY", "GRAPH"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_propgraphs);
+	else if (Matches("ALTER", "PROPERTY", "GRAPH", MatchAny))
+		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER RULE <name>, add ON */
 	else if (Matches("ALTER", "RULE", MatchAny))
 		COMPLETE_WITH("ON");
@@ -2781,7 +2796,7 @@ psql_completion(const char *text, int start, int end)
 					  "FOREIGN DATA WRAPPER", "FOREIGN TABLE",
 					  "FUNCTION", "INDEX", "LANGUAGE", "LARGE OBJECT",
 					  "MATERIALIZED VIEW", "OPERATOR", "POLICY",
-					  "PROCEDURE", "PROCEDURAL LANGUAGE", "PUBLICATION", "ROLE",
+					  "PROCEDURE", "PROCEDURAL LANGUAGE", "PROPERTY GRAPH", "PUBLICATION", "ROLE",
 					  "ROUTINE", "RULE", "SCHEMA", "SEQUENCE", "SERVER",
 					  "STATISTICS", "SUBSCRIPTION", "TABLE",
 					  "TABLESPACE", "TEXT SEARCH", "TRANSFORM FOR",
@@ -2819,6 +2834,8 @@ psql_completion(const char *text, int start, int end)
 	}
 	else if (Matches("COMMENT", "ON", "PROCEDURAL", "LANGUAGE"))
 		COMPLETE_WITH_QUERY(Query_for_list_of_languages);
+	else if (Matches("COMMENT", "ON", "PROPERTY", "GRAPH"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_propgraphs);
 	else if (Matches("COMMENT", "ON", "RULE", MatchAny))
 		COMPLETE_WITH("ON");
 	else if (Matches("COMMENT", "ON", "RULE", MatchAny, "ON"))
@@ -3133,6 +3150,31 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("CREATE", "POLICY", MatchAny, "ON", MatchAny, "AS", MatchAny, "USING"))
 		COMPLETE_WITH("(");
 
+/* CREATE PROPERTY GRAPH */
+	else if (Matches("CREATE", "PROPERTY"))
+		COMPLETE_WITH("GRAPH");
+	else if (Matches("CREATE", "PROPERTY", "GRAPH", MatchAny))
+		COMPLETE_WITH("VERTEX");
+	else if (Matches("CREATE", "PROPERTY", "GRAPH", MatchAny, "VERTEX|NODE"))
+		COMPLETE_WITH("TABLES");
+	else if (Matches("CREATE", "PROPERTY", "GRAPH", MatchAny, "VERTEX|NODE", "TABLES"))
+		COMPLETE_WITH("(");
+	else if (Matches("CREATE", "PROPERTY", "GRAPH", MatchAny, "VERTEX|NODE", "TABLES", "("))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
+	else if (Matches("CREATE", "PROPERTY", "GRAPH", MatchAny, "VERTEX|NODE", "TABLES", "(", MatchAny))
+		COMPLETE_WITH(",", ")");
+	else if (HeadMatches("CREATE", "PROPERTY", "GRAPH", MatchAny, "VERTEX|NODE", "TABLES", "(") &&
+			 TailMatches(")"))
+		COMPLETE_WITH("EDGE");	/* FIXME: doesn't seem to work */
+	else if (HeadMatches("CREATE", "PROPERTY", "GRAPH") &&
+			 TailMatches("EDGE|RELATIONSHIP"))
+		COMPLETE_WITH("TABLES");
+	else if (HeadMatches("CREATE", "PROPERTY", "GRAPH") &&
+			 TailMatches("EDGE|RELATIONSHIP", "TABLES"))
+		COMPLETE_WITH("(");
+	else if (HeadMatches("CREATE", "PROPERTY", "GRAPH") &&
+			 TailMatches("EDGE|RELATIONSHIP", "TABLES", "("))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables);
 
 /* CREATE PUBLICATION */
 	else if (Matches("CREATE", "PUBLICATION", MatchAny))
@@ -3800,6 +3842,12 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("DROP", "POLICY", MatchAny, "ON", MatchAny))
 		COMPLETE_WITH("CASCADE", "RESTRICT");
 
+	/* DROP PROPERTY GRAPH */
+	else if (Matches("DROP", "PROPERTY"))
+		COMPLETE_WITH("GRAPH");
+	else if (Matches("DROP", "PROPERTY", "GRAPH"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_propgraphs);
+
 	/* DROP RULE */
 	else if (Matches("DROP", "RULE", MatchAny))
 		COMPLETE_WITH("ON");
@@ -4034,6 +4082,7 @@ psql_completion(const char *text, int start, int end)
 											"LARGE OBJECT",
 											"PARAMETER",
 											"PROCEDURE",
+											"PROPERTY GRAPH",
 											"ROUTINE",
 											"SCHEMA",
 											"SEQUENCE",
@@ -4160,6 +4209,14 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("FROM");
 	}
 
+/* GRAPH_TABLE */
+	else if (TailMatches("GRAPH_TABLE"))
+		COMPLETE_WITH("(");
+	else if (TailMatches("GRAPH_TABLE", "("))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_propgraphs);
+	else if (TailMatches("GRAPH_TABLE", "(", MatchAny))
+		COMPLETE_WITH("MATCH");
+
 /* GROUP BY */
 	else if (TailMatches("FROM", MatchAny, "GROUP"))
 		COMPLETE_WITH("BY");
@@ -4465,8 +4522,10 @@ psql_completion(const char *text, int start, int end)
 		COMPLETE_WITH("TABLE", "COLUMN", "AGGREGATE", "DATABASE", "DOMAIN",
 					  "EVENT TRIGGER", "FOREIGN TABLE", "FUNCTION",
 					  "LARGE OBJECT", "MATERIALIZED VIEW", "LANGUAGE",
-					  "PUBLICATION", "PROCEDURE", "ROLE", "ROUTINE", "SCHEMA",
+					  "PROPERTY GRAPH", "PUBLICATION", "PROCEDURE", "ROLE", "ROUTINE", "SCHEMA",
 					  "SEQUENCE", "SUBSCRIPTION", "TABLESPACE", "TYPE", "VIEW");
+	else if (Matches("SECURITY", "LABEL", "ON", "PROPERTY", "GRAPH"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_propgraphs);
 	else if (Matches("SECURITY", "LABEL", "ON", MatchAny, MatchAny))
 		COMPLETE_WITH("IS");
 
@@ -4883,6 +4942,8 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("OBJECT");
 		else if (TailMatches("CREATE|ALTER|DROP", "MATERIALIZED"))
 			COMPLETE_WITH("VIEW");
+		else if (TailMatches("CREATE|ALTER|DROP", "PROPERTY"))
+			COMPLETE_WITH("GRAPH");
 		else if (TailMatches("CREATE|ALTER|DROP", "TEXT"))
 			COMPLETE_WITH("SEARCH");
 		else if (TailMatches("CREATE|ALTER|DROP", "USER"))
diff --git a/src/fe_utils/psqlscan.l b/src/fe_utils/psqlscan.l
index c9df0594fda..3a10a924180 100644
--- a/src/fe_utils/psqlscan.l
+++ b/src/fe_utils/psqlscan.l
@@ -788,6 +788,14 @@ other			.
 					ECHO;
 				}
 
+\{				{
+					ECHO;
+				}
+
+\}				{
+					ECHO;
+				}
+
 {operator}		{
 					/*
 					 * Check for embedded slash-star or dash-dash; those
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index fd588659014..b269994ffd8 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -123,6 +123,9 @@ typedef enum ObjectClass
 	OCLASS_EVENT_TRIGGER,		/* pg_event_trigger */
 	OCLASS_PARAMETER_ACL,		/* pg_parameter_acl */
 	OCLASS_POLICY,				/* pg_policy */
+	OCLASS_PROPGRAPH_ELEMENT,	/* pg_propgraph_element */
+	OCLASS_PROPGRAPH_LABEL,		/* pg_propgraph_label */
+	OCLASS_PROPGRAPH_PROPERTY,	/* pg_propgraph_property */
 	OCLASS_PUBLICATION,			/* pg_publication */
 	OCLASS_PUBLICATION_NAMESPACE,	/* pg_publication_namespace */
 	OCLASS_PUBLICATION_REL,		/* pg_publication_rel */
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 6b3c56c20e8..e4b7b626be3 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -69,6 +69,9 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.h',
+  'pg_propgraph_element.h',
+  'pg_propgraph_label.h',
+  'pg_propgraph_property.h',
 ]
 
 # The .dat files we need can just be listed alphabetically.
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 3b7533e7bb3..1635d2c5b35 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -171,6 +171,7 @@ MAKE_SYSCACHE(RELNAMENSP, pg_class_relname_nsp_index, 128);
 #define		  RELKIND_FOREIGN_TABLE   'f'	/* foreign table */
 #define		  RELKIND_PARTITIONED_TABLE 'p' /* partitioned table */
 #define		  RELKIND_PARTITIONED_INDEX 'I' /* partitioned index */
+#define		  RELKIND_PROPGRAPH		  'g'	/* property graph */
 
 #define		  RELPERSISTENCE_PERMANENT	'p' /* regular table */
 #define		  RELPERSISTENCE_UNLOGGED	'u' /* unlogged permanent table */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9c120fc2b7f..207558f97a8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3780,6 +3780,9 @@
   proargtypes => 'oid oid', prosrc => 'oidge' },
 
 # System-view support functions
+{ oid => '8302', descr => 'source text of a property graph',
+  proname => 'pg_get_propgraphdef', provolatile => 's', prorettype => 'text',
+  proargtypes => 'oid', prosrc => 'pg_get_propgraphdef' },
 { oid => '1573', descr => 'source text of a rule',
   proname => 'pg_get_ruledef', provolatile => 's', prorettype => 'text',
   proargtypes => 'oid', prosrc => 'pg_get_ruledef' },
diff --git a/src/include/catalog/pg_propgraph_element.h b/src/include/catalog/pg_propgraph_element.h
new file mode 100644
index 00000000000..fb4bbf81544
--- /dev/null
+++ b/src/include/catalog/pg_propgraph_element.h
@@ -0,0 +1,103 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_propgraph_element.h
+ *	  definition of the "property graph elements" system catalog (pg_propgraph_element)
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_propgraph_element.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_PROPGRAPH_ELEMENT_H
+#define PG_PROPGRAPH_ELEMENT_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_propgraph_element_d.h"
+
+/* ----------------
+ *		pg_propgraph_element definition.  cpp turns this into
+ *		typedef struct FormData_pg_propgraph_element
+ * ----------------
+ */
+CATALOG(pg_propgraph_element,8299,PropgraphElementRelationId)
+{
+	Oid			oid;
+
+	/* OID of the property graph relation */
+	Oid			pgepgid BKI_LOOKUP(pg_class);
+
+	/* OID of the element table */
+	Oid			pgerelid BKI_LOOKUP(pg_class);
+
+	/* element alias */
+	NameData	pgealias;
+
+	/* vertex or edge? -- see PGEKIND_* below */
+	char		pgekind;
+
+	/* for edges: source vertex */
+	Oid			pgesrcvertexid BKI_LOOKUP_OPT(pg_propgraph_element);
+
+	/* for edges: destination vertex */
+	Oid			pgedestvertexid BKI_LOOKUP_OPT(pg_propgraph_element);
+
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	/* element key (column numbers in pgerelid relation) */
+	int16		pgekey[1] BKI_FORCE_NOT_NULL;
+
+	/*
+	 * for edges: source vertex key (column numbers in pgerelid relation)
+	 */
+	int16		pgesrckey[1];
+
+	/*
+	 * for edges: source vertex table referenced columns (column numbers in
+	 * relation reached via pgesrcvertexid)
+	 */
+	int16		pgesrcref[1];
+
+	/*
+	 * for edges: destination vertex key (column numbers in pgerelid relation)
+	 */
+	int16		pgedestkey[1];
+
+	/*
+	 * for edges: destination vertex table referenced columns (column numbers
+	 * in relation reached via pgedestvertexid)
+	 */
+	int16		pgedestref[1];
+#endif
+} FormData_pg_propgraph_element;
+
+/* ----------------
+ *		Form_pg_propgraph_element corresponds to a pointer to a tuple with
+ *		the format of pg_propgraph_element relation.
+ * ----------------
+ */
+typedef FormData_pg_propgraph_element *Form_pg_propgraph_element;
+
+DECLARE_TOAST(pg_propgraph_element, 8312, 8313);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_propgraph_element_oid_index, 8300, PropgraphElementObjectIndexId, pg_propgraph_element, btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_propgraph_element_alias_index, 8301, PropgraphElementAliasIndexId, pg_propgraph_element, btree(pgepgid oid_ops, pgealias name_ops));
+
+MAKE_SYSCACHE(PROPGRAPHELOID, pg_propgraph_element_oid_index, 128);
+MAKE_SYSCACHE(PROPGRAPHELALIAS, pg_propgraph_element_alias_index, 128);
+
+#ifdef EXPOSE_TO_CLIENT_CODE
+
+/*
+ * Symbolic values for pgekind column
+ */
+#define PGEKIND_VERTEX 'v'
+#define PGEKIND_EDGE 'e'
+
+#endif							/* EXPOSE_TO_CLIENT_CODE */
+
+#endif							/* PG_PROPGRAPH_ELEMENT_H */
diff --git a/src/include/catalog/pg_propgraph_label.h b/src/include/catalog/pg_propgraph_label.h
new file mode 100644
index 00000000000..a704bdfabd1
--- /dev/null
+++ b/src/include/catalog/pg_propgraph_label.h
@@ -0,0 +1,59 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_propgraph_label.h
+ *	  definition of the "property graph labels" system catalog (pg_propgraph_label)
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_propgraph_label.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_PROPGRAPH_LABEL_H
+#define PG_PROPGRAPH_LABEL_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_propgraph_label_d.h"
+
+/* ----------------
+ *		pg_propgraph_label definition.  cpp turns this into
+ *		typedef struct FormData_pg_propgraph_label
+ * ----------------
+ */
+CATALOG(pg_propgraph_label,8303,PropgraphLabelRelationId)
+{
+	Oid			oid;
+
+	/*
+	 * OID of the property graph relation.  This can also be found out by
+	 * chasing via pglelid, but having it here is more efficient.
+	 */
+	Oid			pglpgid BKI_LOOKUP(pg_class);
+
+	/* label name */
+	NameData	pgllabel;
+
+	/* OID of the property graph element */
+	Oid			pglelid BKI_LOOKUP(pg_propgraph_element);
+} FormData_pg_propgraph_label;
+
+/* ----------------
+ *		Form_pg_propgraph_label corresponds to a pointer to a tuple with
+ *		the format of pg_propgraph_label relation.
+ * ----------------
+ */
+typedef FormData_pg_propgraph_label *Form_pg_propgraph_label;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_propgraph_label_oid_index, 8304, PropgraphLabelObjectIndexId, pg_propgraph_label, btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_propgraph_label_label_index, 8305, PropgraphLabelLabelIndexId, pg_propgraph_label, btree(pglelid oid_ops, pgllabel name_ops));
+
+DECLARE_INDEX(pg_propgraph_label_graph_name_index, 8314, PropgraphLabelGraphNameIndexId, pg_propgraph_label, btree(pglpgid oid_ops, pgllabel name_ops));
+
+MAKE_SYSCACHE(PROPGRAPHLABELNAME, pg_propgraph_label_label_index, 128);
+
+#endif							/* PG_PROPGRAPH_LABEL_H */
diff --git a/src/include/catalog/pg_propgraph_property.h b/src/include/catalog/pg_propgraph_property.h
new file mode 100644
index 00000000000..2d5f2b8fe7e
--- /dev/null
+++ b/src/include/catalog/pg_propgraph_property.h
@@ -0,0 +1,74 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_propgraph_property.h
+ *	  definition of the "property graph properties" system catalog (pg_propgraph_property)
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_propgraph_property.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_PROPGRAPH_PROPERTY_H
+#define PG_PROPGRAPH_PROPERTY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_propgraph_property_d.h"
+
+/* ----------------
+ *		pg_propgraph_property definition.  cpp turns this into
+ *		typedef struct FormData_pg_propgraph_property
+ * ----------------
+ */
+CATALOG(pg_propgraph_property,8306,PropgraphPropertyRelationId)
+{
+	Oid			oid;
+
+	/*
+	 * OID of the property graph relation.  This can also be found out by
+	 * chasing via pgplabelid, but having it here is more efficient.
+	 */
+	Oid			pgppgid BKI_LOOKUP(pg_class);
+
+	/* property name */
+	NameData	pgpname;
+
+	/*
+	 * Type of the property.  (This can be computed from pgpexpr, but storing
+	 * it makes parsing GRAPH_TABLE more efficient.)
+	 */
+	Oid			pgptypid BKI_LOOKUP_OPT(pg_type);
+
+	/* OID of the label */
+	Oid			pgplabelid BKI_LOOKUP(pg_propgraph_label);
+
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+
+	/* property expression */
+	pg_node_tree pgpexpr BKI_FORCE_NOT_NULL;
+
+#endif
+} FormData_pg_propgraph_property;
+
+/* ----------------
+ *		Form_pg_propgraph_property corresponds to a pointer to a tuple with
+ *		the format of pg_propgraph_property relation.
+ * ----------------
+ */
+typedef FormData_pg_propgraph_property *Form_pg_propgraph_property;
+
+DECLARE_TOAST(pg_propgraph_property, 8309, 8310);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_propgraph_property_oid_index, 8307, PropgraphPropertyObjectIndexId, pg_propgraph_property, btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_propgraph_property_name_index, 8308, PropgraphPropertyNameIndexId, pg_propgraph_property, btree(pgplabelid oid_ops, pgpname name_ops));
+
+DECLARE_INDEX(pg_propgraph_property_graph_name_index, 8311, PropgraphPropertyGraphNameIndexId, pg_propgraph_property, btree(pgppgid oid_ops, pgpname name_ops));
+
+MAKE_SYSCACHE(PROPGRAPHPROPNAME, pg_propgraph_property_name_index, 128);
+
+#endif							/* PG_PROPGRAPH_PROPERTY_H */
diff --git a/src/include/commands/propgraphcmds.h b/src/include/commands/propgraphcmds.h
new file mode 100644
index 00000000000..2440bd4a60c
--- /dev/null
+++ b/src/include/commands/propgraphcmds.h
@@ -0,0 +1,23 @@
+/*-------------------------------------------------------------------------
+ *
+ * propgraphcmds.h
+ *	  prototypes for propgraphcmds.c.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/propgraphcmds.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef PROPGRAPHCMDS_H
+#define PROPGRAPHCMDS_H
+
+#include "catalog/objectaddress.h"
+#include "parser/parse_node.h"
+
+extern ObjectAddress CreatePropGraph(ParseState *pstate, const CreatePropGraphStmt *stmt);
+extern ObjectAddress AlterPropGraph(ParseState *pstate, const AlterPropGraphStmt *stmt);
+
+#endif							/* PROPGRAPHCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index d5b08ded446..8fab0daec3d 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -669,6 +669,19 @@ typedef struct RangeTableFuncCol
 	int			location;		/* token location, or -1 if unknown */
 } RangeTableFuncCol;
 
+/*
+ * RangeGraphTable - raw form of GRAPH_TABLE clause
+ */
+typedef struct RangeGraphTable
+{
+	NodeTag		type;
+	RangeVar   *graph_name;
+	struct GraphPattern *graph_pattern;
+	List	   *columns;
+	Alias	   *alias;			/* table alias & optional column aliases */
+	int			location;		/* token location, or -1 if unknown */
+} RangeGraphTable;
+
 /*
  * RangeTableSample - TABLESAMPLE appearing in a raw FROM clause
  *
@@ -934,6 +947,38 @@ typedef struct PartitionCmd
 	bool		concurrent;
 } PartitionCmd;
 
+/*
+ * Nodes for graph pattern
+ */
+
+typedef struct GraphPattern
+{
+	NodeTag		type;
+	List	   *path_pattern_list;
+	Node	   *whereClause;
+} GraphPattern;
+
+typedef enum GraphElementPatternKind
+{
+	VERTEX_PATTERN,
+	EDGE_PATTERN_LEFT,
+	EDGE_PATTERN_RIGHT,
+	EDGE_PATTERN_ANY,
+	PAREN_EXPR,
+} GraphElementPatternKind;
+
+typedef struct GraphElementPattern
+{
+	NodeTag		type;
+	GraphElementPatternKind kind;
+	const char *variable;
+	Node	   *labelexpr;
+	List	   *subexpr;
+	Node	   *whereClause;
+	List	   *quantifier;
+	int			location;
+} GraphElementPattern;
+
 /****************************************************************************
  *	Nodes for a Query tree
  ****************************************************************************/
@@ -1010,6 +1055,7 @@ typedef enum RTEKind
 	RTE_VALUES,					/* VALUES (<exprlist>), (<exprlist>), ... */
 	RTE_CTE,					/* common table expr (WITH list element) */
 	RTE_NAMEDTUPLESTORE,		/* tuplestore, e.g. for AFTER triggers */
+	RTE_GRAPH_TABLE,			/* GRAPH_TABLE clause */
 	RTE_RESULT,					/* RTE represents an empty FROM clause; such
 								 * RTEs are added by the planner, they're not
 								 * present during parsing or rewriting */
@@ -1144,6 +1190,12 @@ typedef struct RangeTblEntry
 	 */
 	TableFunc  *tablefunc;
 
+	/*
+	 * Fields valid for a graph table RTE (else NULL):
+	 */
+	GraphPattern *graph_pattern;
+	List	   *graph_table_columns;
+
 	/*
 	 * Fields valid for a values RTE (else NIL):
 	 */
@@ -2122,6 +2174,7 @@ typedef enum ObjectType
 	OBJECT_PARAMETER_ACL,
 	OBJECT_POLICY,
 	OBJECT_PROCEDURE,
+	OBJECT_PROPGRAPH,
 	OBJECT_PUBLICATION,
 	OBJECT_PUBLICATION_NAMESPACE,
 	OBJECT_PUBLICATION_REL,
@@ -3861,6 +3914,88 @@ typedef struct CreateCastStmt
 	bool		inout;
 } CreateCastStmt;
 
+/* ----------------------
+ *	CREATE PROPERTY GRAPH Statement
+ * ----------------------
+ */
+typedef struct CreatePropGraphStmt
+{
+	NodeTag		type;
+	RangeVar   *pgname;
+	List	   *vertex_tables;
+	List	   *edge_tables;
+} CreatePropGraphStmt;
+
+typedef struct PropGraphVertex
+{
+	NodeTag		type;
+	RangeVar   *vtable;
+	List	   *vkey;
+	List	   *labels;
+	int			location;
+} PropGraphVertex;
+
+typedef struct PropGraphEdge
+{
+	NodeTag		type;
+	RangeVar   *etable;
+	List	   *ekey;
+	List	   *esrckey;
+	char	   *esrcvertex;
+	List	   *esrcvertexcols;
+	List	   *edestkey;
+	char	   *edestvertex;
+	List	   *edestvertexcols;
+	List	   *labels;
+	int			location;
+} PropGraphEdge;
+
+typedef struct PropGraphLabelAndProperties
+{
+	NodeTag		type;
+	const char *label;
+	struct PropGraphProperties *properties;
+	int			location;
+} PropGraphLabelAndProperties;
+
+typedef struct PropGraphProperties
+{
+	NodeTag		type;
+	List	   *properties;
+	bool		all;
+	int			location;
+} PropGraphProperties;
+
+/* ----------------------
+ *	ALTER PROPERTY GRAPH Statement
+ * ----------------------
+ */
+
+typedef enum AlterPropGraphElementKind
+{
+	PROPGRAPH_ELEMENT_KIND_VERTEX = 1,
+	PROPGRAPH_ELEMENT_KIND_EDGE = 2,
+} AlterPropGraphElementKind;
+
+typedef struct AlterPropGraphStmt
+{
+	NodeTag		type;
+	RangeVar   *pgname;
+	bool		missing_ok;
+	List	   *add_vertex_tables;
+	List	   *add_edge_tables;
+	List	   *drop_vertex_tables;
+	List	   *drop_edge_tables;
+	DropBehavior drop_behavior;
+	AlterPropGraphElementKind element_kind;
+	const char *element_alias;
+	List	   *add_labels;
+	const char *drop_label;
+	const char *alter_label;
+	PropGraphProperties *add_properties;
+	List	   *drop_properties;
+} AlterPropGraphStmt;
+
 /* ----------------------
  *	CREATE TRANSFORM Statement
  * ----------------------
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 4a154606d2b..43c94aa9f97 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1860,6 +1860,28 @@ typedef struct InferenceElem
 	Oid			inferopclass;	/* OID of att opclass, or InvalidOid */
 } InferenceElem;
 
+/*
+ * GraphLabelRef - label reference in label expression inside GRAPH_TABLE clause
+ */
+typedef struct GraphLabelRef
+{
+	NodeTag		type;
+	const char *labelname;
+	int			location;
+} GraphLabelRef;
+
+/*
+ * GraphPropertyRef - property reference inside GRAPH_TABLE clause
+ */
+typedef struct GraphPropertyRef
+{
+	Expr		xpr;
+	const char *elvarname;
+	const char *propname;
+	Oid			typeId;
+	int			location;
+} GraphPropertyRef;
+
 /*--------------------
  * TargetEntry -
  *	   a target entry (used in query target lists)
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 2331acac091..34092ed075c 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -135,6 +135,7 @@ PG_KEYWORD("delimiters", DELIMITERS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("depends", DEPENDS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("depth", DEPTH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("desc", DESC, RESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("destination", DESTINATION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("detach", DETACH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("dictionary", DICTIONARY, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("disable", DISABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -146,6 +147,7 @@ PG_KEYWORD("domain", DOMAIN_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("double", DOUBLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("drop", DROP, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("each", EACH, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("edge", EDGE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -187,6 +189,8 @@ PG_KEYWORD("generated", GENERATED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("global", GLOBAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("grant", GRANT, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("granted", GRANTED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("graph", GRAPH, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("graph_table", GRAPH_TABLE, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("greatest", GREATEST, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("group", GROUP_P, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("grouping", GROUPING, COL_NAME_KEYWORD, BARE_LABEL)
@@ -284,6 +288,7 @@ PG_KEYWORD("nfd", NFD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("nfkc", NFKC, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("nfkd", NFKD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("no", NO, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("node", NODE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("none", NONE, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("normalize", NORMALIZE, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("normalized", NORMALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -342,6 +347,8 @@ PG_KEYWORD("procedural", PROCEDURAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("procedure", PROCEDURE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("procedures", PROCEDURES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("program", PROGRAM, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("properties", PROPERTIES, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("property", PROPERTY, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("publication", PUBLICATION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("quote", QUOTE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("range", RANGE, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -355,6 +362,7 @@ PG_KEYWORD("references", REFERENCES, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("referencing", REFERENCING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("refresh", REFRESH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("reindex", REINDEX, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("relationship", RELATIONSHIP, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("relative", RELATIVE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("release", RELEASE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("rename", RENAME, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -403,6 +411,7 @@ PG_KEYWORD("skip", SKIP, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("smallint", SMALLINT, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("snapshot", SNAPSHOT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("some", SOME, RESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("source", SOURCE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("sql", SQL_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("stable", STABLE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("standalone", STANDALONE_P, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -470,6 +479,7 @@ PG_KEYWORD("variadic", VARIADIC, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("varying", VARYING, UNRESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("verbose", VERBOSE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("version", VERSION_P, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("vertex", VERTEX, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("view", VIEW, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("views", VIEWS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("volatile", VOLATILE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_graphtable.h b/src/include/parser/parse_graphtable.h
new file mode 100644
index 00000000000..7de98a71f76
--- /dev/null
+++ b/src/include/parser/parse_graphtable.h
@@ -0,0 +1,31 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_graphtable.h
+ *		parsing of GRAPH_TABLE
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/parser/parse_graphtable.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PARSE_GRAPHTABLE_H
+#define PARSE_GRAPHTABLE_H
+
+#include "nodes/parsenodes.h"
+#include "nodes/pg_list.h"
+#include "parser/parse_node.h"
+
+typedef struct GraphTableParseState
+{
+	Oid			graphid;
+	List	   *variables;
+} GraphTableParseState;
+
+extern Node *graph_table_property_reference(ParseState *pstate, ColumnRef *cref);
+
+Node	   *transformGraphPattern(GraphTableParseState *gpstate, GraphPattern *graph_pattern);
+
+#endif							/* PARSE_GRAPHTABLE_H */
diff --git a/src/include/parser/parse_relation.h b/src/include/parser/parse_relation.h
index bea2da54961..c11b895b7bb 100644
--- a/src/include/parser/parse_relation.h
+++ b/src/include/parser/parse_relation.h
@@ -81,6 +81,14 @@ extern ParseNamespaceItem *addRangeTableEntryForTableFunc(ParseState *pstate,
 														  Alias *alias,
 														  bool lateral,
 														  bool inFromCl);
+extern ParseNamespaceItem *addRangeTableEntryForGraphTable(ParseState *pstate,
+														   Oid graphid,
+														   GraphPattern *graph_pattern,
+														   List *columns,
+														   List *colnames,
+														   Alias *alias,
+														   bool lateral,
+														   bool inFromCl);
 extern ParseNamespaceItem *addRangeTableEntryForJoin(ParseState *pstate,
 													 List *colnames,
 													 ParseNamespaceColumn *nscolumns,
diff --git a/src/include/rewrite/rewriteGraphTable.h b/src/include/rewrite/rewriteGraphTable.h
new file mode 100644
index 00000000000..0c0319f5cfa
--- /dev/null
+++ b/src/include/rewrite/rewriteGraphTable.h
@@ -0,0 +1,21 @@
+/*-------------------------------------------------------------------------
+ *
+ * rewriteGraphTable.h
+ *		Support for rewriting GRAPH_TABLE clauses.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/rewrite/rewriteGraphTable.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef REWRITEGRAPHTABLE_H
+#define REWRITEGRAPHTABLE_H
+
+#include "nodes/parsenodes.h"
+
+extern Query *rewriteGraphTable(Query *parsetree, int rt_index);
+
+#endif							/* REWRITEGRAPHTABLE_H */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 7fdcec6dd93..5582483d06c 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -48,6 +48,7 @@ PG_CMDTAG(CMDTAG_ALTER_OPERATOR_CLASS, "ALTER OPERATOR CLASS", true, false, fals
 PG_CMDTAG(CMDTAG_ALTER_OPERATOR_FAMILY, "ALTER OPERATOR FAMILY", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_POLICY, "ALTER POLICY", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_PROCEDURE, "ALTER PROCEDURE", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_PROPERTY_GRAPH, "ALTER PROPERTY GRAPH", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_PUBLICATION, "ALTER PUBLICATION", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_ROLE, "ALTER ROLE", false, false, false)
 PG_CMDTAG(CMDTAG_ALTER_ROUTINE, "ALTER ROUTINE", true, false, false)
@@ -103,6 +104,7 @@ PG_CMDTAG(CMDTAG_CREATE_OPERATOR_CLASS, "CREATE OPERATOR CLASS", true, false, fa
 PG_CMDTAG(CMDTAG_CREATE_OPERATOR_FAMILY, "CREATE OPERATOR FAMILY", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_POLICY, "CREATE POLICY", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_PROCEDURE, "CREATE PROCEDURE", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_PROPERTY_GRAPH, "CREATE PROPERTY GRAPH", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_PUBLICATION, "CREATE PUBLICATION", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ROLE, "CREATE ROLE", false, false, false)
 PG_CMDTAG(CMDTAG_CREATE_ROUTINE, "CREATE ROUTINE", true, false, false)
@@ -156,6 +158,7 @@ PG_CMDTAG(CMDTAG_DROP_OPERATOR_FAMILY, "DROP OPERATOR FAMILY", true, false, fals
 PG_CMDTAG(CMDTAG_DROP_OWNED, "DROP OWNED", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_POLICY, "DROP POLICY", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_PROCEDURE, "DROP PROCEDURE", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_PROPERTY_GRAPH, "DROP PROPERTY GRAPH", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_PUBLICATION, "DROP PUBLICATION", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_ROLE, "DROP ROLE", false, false, false)
 PG_CMDTAG(CMDTAG_DROP_ROUTINE, "DROP ROUTINE", true, false, false)
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index a803eb12910..b659040716c 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -165,6 +165,7 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_LANGUAGE		(ACL_USAGE)
 #define ACL_ALL_RIGHTS_LARGEOBJECT	(ACL_SELECT|ACL_UPDATE)
 #define ACL_ALL_RIGHTS_PARAMETER_ACL (ACL_SET|ACL_ALTER_SYSTEM)
+#define ACL_ALL_RIGHTS_PROPGRAPH	(ACL_SELECT)
 #define ACL_ALL_RIGHTS_SCHEMA		(ACL_USAGE|ACL_CREATE)
 #define ACL_ALL_RIGHTS_TABLESPACE	(ACL_CREATE)
 #define ACL_ALL_RIGHTS_TYPE			(ACL_USAGE)
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index e630498e82c..21e3fc84e81 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -233,6 +233,7 @@ static	void			check_raise_parameters(PLpgSQL_stmt_raise *stmt);
 %token <ival>	ICONST PARAM
 %token			TYPECAST DOT_DOT COLON_EQUALS EQUALS_GREATER
 %token			LESS_EQUALS GREATER_EQUALS NOT_EQUALS
+%token			BRACKET_RIGHT_ARROW LEFT_ARROW_BRACKET MINUS_LEFT_BRACKET RIGHT_BRACKET_MINUS
 
 /*
  * Other tokens recognized by plpgsql's lexer interface layer (pl_scanner.c).
diff --git a/src/test/regress/expected/alter_generic.out b/src/test/regress/expected/alter_generic.out
index ae54cb254f9..ab9ce9e9acd 100644
--- a/src/test/regress/expected/alter_generic.out
+++ b/src/test/regress/expected/alter_generic.out
@@ -520,6 +520,49 @@ ERROR:  left and right associated data types for operator class options parsing
 ALTER OPERATOR FAMILY alt_opf19 USING btree ADD FUNCTION 5 (int4) test_opclass_options_func(internal); -- Ok
 ALTER OPERATOR FAMILY alt_opf19 USING btree DROP FUNCTION 5 (int4, int4);
 DROP OPERATOR FAMILY alt_opf19 USING btree;
+--
+-- Property Graph
+--
+SET SESSION AUTHORIZATION regress_alter_generic_user1;
+CREATE PROPERTY GRAPH alt_graph1;
+CREATE PROPERTY GRAPH alt_graph2;
+CREATE PROPERTY GRAPH alt_graph3;
+ALTER PROPERTY GRAPH alt_graph1 RENAME TO alt_graph2; -- failed (name conflict)
+ERROR:  relation "alt_graph2" already exists
+ALTER PROPERTY GRAPH alt_graph1 RENAME TO alt_graph4; -- OK
+ALTER PROPERTY GRAPH alt_graph2 OWNER TO regress_alter_generic_user2;  -- failed (no role membership)
+ERROR:  must be able to SET ROLE "regress_alter_generic_user2"
+ALTER PROPERTY GRAPH alt_graph2 OWNER TO regress_alter_generic_user3;  -- OK
+ALTER PROPERTY GRAPH alt_graph4 SET SCHEMA alt_nsp2;  -- OK
+ALTER PROPERTY GRAPH alt_nsp2.alt_graph4 RENAME TO alt_graph2;  -- OK
+ALTER PROPERTY GRAPH alt_graph2 SET SCHEMA alt_nsp2;  -- failed (name conflict)
+ERROR:  relation "alt_graph2" already exists in schema "alt_nsp2"
+SET SESSION AUTHORIZATION regress_alter_generic_user2;
+CREATE PROPERTY GRAPH alt_graph5;
+ALTER PROPERTY GRAPH alt_graph3 RENAME TO alt_graph5;  -- failed (not owner)
+ERROR:  must be owner of property graph alt_graph3
+ALTER PROPERTY GRAPH alt_graph5 RENAME TO alt_graph6;  -- OK
+ALTER PROPERTY GRAPH alt_graph3 OWNER TO regress_alter_generic_user2;  -- failed (not owner)
+ERROR:  must be owner of property graph alt_graph3
+ALTER PROPERTY GRAPH alt_graph6 OWNER TO regress_alter_generic_user3;  -- failed (no role membership)
+ERROR:  must be able to SET ROLE "regress_alter_generic_user3"
+ALTER PROPERTY GRAPH alt_graph3 SET SCHEMA alt_nsp2;  -- failed (not owner)
+ERROR:  must be owner of property graph alt_graph3
+RESET SESSION AUTHORIZATION;
+SELECT nspname, relname, rolname
+  FROM pg_class c, pg_namespace n, pg_authid a
+  WHERE c.relnamespace = n.oid AND c.relowner = a.oid
+    AND n.nspname in ('alt_nsp1', 'alt_nsp2')
+    AND c.relkind = 'g'
+  ORDER BY nspname, relname;
+ nspname  |  relname   |           rolname           
+----------+------------+-----------------------------
+ alt_nsp1 | alt_graph2 | regress_alter_generic_user3
+ alt_nsp1 | alt_graph3 | regress_alter_generic_user1
+ alt_nsp1 | alt_graph6 | regress_alter_generic_user2
+ alt_nsp2 | alt_graph2 | regress_alter_generic_user1
+(4 rows)
+
 --
 -- Statistics
 --
@@ -710,7 +753,7 @@ NOTICE:  drop cascades to server alt_fserv3
 DROP LANGUAGE alt_lang2 CASCADE;
 DROP LANGUAGE alt_lang3 CASCADE;
 DROP SCHEMA alt_nsp1 CASCADE;
-NOTICE:  drop cascades to 28 other objects
+NOTICE:  drop cascades to 31 other objects
 DETAIL:  drop cascades to function alt_func3(integer)
 drop cascades to function alt_agg3(integer)
 drop cascades to function alt_func4(integer)
@@ -727,6 +770,9 @@ drop cascades to operator family alt_opc1 for access method hash
 drop cascades to operator family alt_opc2 for access method hash
 drop cascades to operator family alt_opf4 for access method hash
 drop cascades to operator family alt_opf2 for access method hash
+drop cascades to property graph alt_graph2
+drop cascades to property graph alt_graph3
+drop cascades to property graph alt_graph6
 drop cascades to table alt_regress_1
 drop cascades to table alt_regress_2
 drop cascades to text search dictionary alt_ts_dict3
@@ -740,12 +786,13 @@ drop cascades to text search template alt_ts_temp2
 drop cascades to text search parser alt_ts_prs3
 drop cascades to text search parser alt_ts_prs2
 DROP SCHEMA alt_nsp2 CASCADE;
-NOTICE:  drop cascades to 9 other objects
+NOTICE:  drop cascades to 10 other objects
 DETAIL:  drop cascades to function alt_nsp2.alt_func2(integer)
 drop cascades to function alt_nsp2.alt_agg2(integer)
 drop cascades to conversion alt_nsp2.alt_conv2
 drop cascades to operator alt_nsp2.@-@(integer,integer)
 drop cascades to operator family alt_nsp2.alt_opf2 for access method hash
+drop cascades to property graph alt_nsp2.alt_graph2
 drop cascades to text search dictionary alt_nsp2.alt_ts_dict2
 drop cascades to text search configuration alt_nsp2.alt_ts_conf2
 drop cascades to text search template alt_nsp2.alt_ts_temp2
diff --git a/src/test/regress/expected/create_property_graph.out b/src/test/regress/expected/create_property_graph.out
new file mode 100644
index 00000000000..9ad28f6fc07
--- /dev/null
+++ b/src/test/regress/expected/create_property_graph.out
@@ -0,0 +1,370 @@
+CREATE SCHEMA create_property_graph_tests;
+GRANT USAGE ON SCHEMA create_property_graph_tests TO PUBLIC;
+SET search_path = create_property_graph_tests;
+CREATE ROLE regress_graph_user1;
+CREATE ROLE regress_graph_user2;
+CREATE PROPERTY GRAPH g1;
+COMMENT ON PROPERTY GRAPH g1 IS 'a graph';
+CREATE PROPERTY GRAPH g1;  -- error: duplicate
+ERROR:  relation "g1" already exists
+CREATE TABLE t1 (a int, b text);
+CREATE TABLE t2 (i int PRIMARY KEY, j int, k int);
+CREATE TABLE t3 (x int, y text, z text);
+CREATE TABLE e1 (a int, i int, t text, PRIMARY KEY (a, i));
+CREATE TABLE e2 (a int, x int, t text);
+CREATE PROPERTY GRAPH g2
+    VERTEX TABLES (t1 KEY (a), t2 DEFAULT LABEL, t3 KEY (x) LABEL t3l1 LABEL t3l2)
+    EDGE TABLES (
+        e1
+            SOURCE KEY (a) REFERENCES t1 (a)
+            DESTINATION KEY (i) REFERENCES t2 (i),
+        e2 KEY (a, x)
+            SOURCE KEY (a) REFERENCES t1 (a)
+            DESTINATION KEY (x, t) REFERENCES t3 (x, y)
+    );
+-- like g2 but assembled with ALTER
+CREATE PROPERTY GRAPH g3;
+ALTER PROPERTY GRAPH g3 ADD VERTEX TABLES (t1 KEY (a), t2 DEFAULT LABEL);
+ALTER PROPERTY GRAPH g3
+    ADD VERTEX TABLES (t3 KEY (x) LABEL t3l1)
+    ADD EDGE TABLES (
+        e1 SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (i) REFERENCES t2 (i),
+        e2 KEY (a, x) SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (x, t) REFERENCES t3 (x, y)
+    );
+ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 ADD LABEL t3l2 PROPERTIES ALL COLUMNS ADD LABEL t3l3 PROPERTIES ALL COLUMNS;
+ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l3x;  -- error
+ERROR:  property graph "g3" element "t3" has no label "t3l3x"
+ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l3;
+ALTER PROPERTY GRAPH g3 DROP VERTEX TABLES (t2);  -- fail (TODO: dubious error message)
+ERROR:  cannot drop element t2 in property graph g3 because other objects depend on it
+DETAIL:  element e1 in property graph g3 depends on element t2 in property graph g3
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER PROPERTY GRAPH g3 DROP VERTEX TABLES (t2) CASCADE;
+NOTICE:  drop cascades to element e1 in property graph g3
+ALTER PROPERTY GRAPH g3 DROP EDGE TABLES (e2);
+CREATE PROPERTY GRAPH g4
+    VERTEX TABLES (
+        t1 KEY (a) NO PROPERTIES,
+        t2 DEFAULT LABEL PROPERTIES (i + j AS i_j, k),
+        t3 KEY (x) LABEL t3l1 PROPERTIES (x, y AS yy) LABEL t3l2 PROPERTIES (x, z AS zz)
+    )
+    EDGE TABLES (
+        e1
+            SOURCE KEY (a) REFERENCES t1 (a)
+            DESTINATION KEY (i) REFERENCES t2 (i)
+            PROPERTIES ALL COLUMNS,
+        e2 KEY (a, x)
+            SOURCE KEY (a) REFERENCES t1 (a)
+            DESTINATION KEY (x, t) REFERENCES t3 (x, y)
+            PROPERTIES ALL COLUMNS
+    );
+ALTER PROPERTY GRAPH g4 ALTER VERTEX TABLE t2 ALTER LABEL t2 ADD PROPERTIES (k * 2 AS kk);
+ALTER PROPERTY GRAPH g4 ALTER VERTEX TABLE t2 ALTER LABEL t2 DROP PROPERTIES (k);
+-- error cases
+CREATE PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
+ERROR:  relation "xx" does not exist
+CREATE PROPERTY GRAPH gx VERTEX TABLES (t1 KEY (a), t2 KEY (i), t1 KEY (a));
+ERROR:  alias "t1" used more than once as element table
+LINE 1: ...Y GRAPH gx VERTEX TABLES (t1 KEY (a), t2 KEY (i), t1 KEY (a)...
+                                                             ^
+CREATE PROPERTY GRAPH gx
+    VERTEX TABLES (t1 AS tt KEY (a), t2 KEY (i))
+    EDGE TABLES (
+        e1 SOURCE t1 DESTINATION t2
+    );
+ERROR:  source vertex "t1" of edge "e1" does not exist
+LINE 4:         e1 SOURCE t1 DESTINATION t2
+                ^
+CREATE PROPERTY GRAPH gx
+    VERTEX TABLES (t1 KEY (a), t2 KEY (i))
+    EDGE TABLES (
+        e1 SOURCE t1 DESTINATION tx
+    );
+ERROR:  destination vertex "tx" of edge "e1" does not exist
+LINE 4:         e1 SOURCE t1 DESTINATION tx
+                ^
+COMMENT ON PROPERTY GRAPH gx IS 'not a graph';
+ERROR:  relation "gx" does not exist
+ALTER PROPERTY GRAPH g1 OWNER TO regress_graph_user1;
+SET ROLE regress_graph_user1;
+GRANT SELECT ON PROPERTY GRAPH g1 TO regress_graph_user2;
+GRANT UPDATE ON PROPERTY GRAPH g1 TO regress_graph_user2;  -- fail
+ERROR:  invalid privilege type UPDATE for property graph
+RESET ROLE;
+-- information schema
+SELECT * FROM information_schema.property_graphs ORDER BY property_graph_name;
+ property_graph_catalog |    property_graph_schema    | property_graph_name 
+------------------------+-----------------------------+---------------------
+ regression             | create_property_graph_tests | g1
+ regression             | create_property_graph_tests | g2
+ regression             | create_property_graph_tests | g3
+ regression             | create_property_graph_tests | g4
+(4 rows)
+
+SELECT * FROM information_schema.pg_element_tables ORDER BY property_graph_name, element_table_alias;
+ property_graph_catalog |    property_graph_schema    | property_graph_name | element_table_alias | element_table_kind | table_catalog |        table_schema         | table_name | element_table_definition 
+------------------------+-----------------------------+---------------------+---------------------+--------------------+---------------+-----------------------------+------------+--------------------------
+ regression             | create_property_graph_tests | g2                  | e1                  | EDGE               | regression    | create_property_graph_tests | e1         | 
+ regression             | create_property_graph_tests | g2                  | e2                  | EDGE               | regression    | create_property_graph_tests | e2         | 
+ regression             | create_property_graph_tests | g2                  | t1                  | VERTEX             | regression    | create_property_graph_tests | t1         | 
+ regression             | create_property_graph_tests | g2                  | t2                  | VERTEX             | regression    | create_property_graph_tests | t2         | 
+ regression             | create_property_graph_tests | g2                  | t3                  | VERTEX             | regression    | create_property_graph_tests | t3         | 
+ regression             | create_property_graph_tests | g3                  | t1                  | VERTEX             | regression    | create_property_graph_tests | t1         | 
+ regression             | create_property_graph_tests | g3                  | t3                  | VERTEX             | regression    | create_property_graph_tests | t3         | 
+ regression             | create_property_graph_tests | g4                  | e1                  | EDGE               | regression    | create_property_graph_tests | e1         | 
+ regression             | create_property_graph_tests | g4                  | e2                  | EDGE               | regression    | create_property_graph_tests | e2         | 
+ regression             | create_property_graph_tests | g4                  | t1                  | VERTEX             | regression    | create_property_graph_tests | t1         | 
+ regression             | create_property_graph_tests | g4                  | t2                  | VERTEX             | regression    | create_property_graph_tests | t2         | 
+ regression             | create_property_graph_tests | g4                  | t3                  | VERTEX             | regression    | create_property_graph_tests | t3         | 
+(12 rows)
+
+SELECT * FROM information_schema.pg_element_table_key_columns ORDER BY property_graph_name, element_table_alias, ordinal_position;
+ property_graph_catalog |    property_graph_schema    | property_graph_name | element_table_alias | column_name | ordinal_position 
+------------------------+-----------------------------+---------------------+---------------------+-------------+------------------
+ regression             | create_property_graph_tests | g2                  | e1                  | a           |                1
+ regression             | create_property_graph_tests | g2                  | e1                  | i           |                2
+ regression             | create_property_graph_tests | g2                  | e2                  | a           |                1
+ regression             | create_property_graph_tests | g2                  | e2                  | x           |                2
+ regression             | create_property_graph_tests | g2                  | t1                  | a           |                1
+ regression             | create_property_graph_tests | g2                  | t2                  | i           |                1
+ regression             | create_property_graph_tests | g2                  | t3                  | x           |                1
+ regression             | create_property_graph_tests | g3                  | t1                  | a           |                1
+ regression             | create_property_graph_tests | g3                  | t3                  | x           |                1
+ regression             | create_property_graph_tests | g4                  | e1                  | a           |                1
+ regression             | create_property_graph_tests | g4                  | e1                  | i           |                2
+ regression             | create_property_graph_tests | g4                  | e2                  | a           |                1
+ regression             | create_property_graph_tests | g4                  | e2                  | x           |                2
+ regression             | create_property_graph_tests | g4                  | t1                  | a           |                1
+ regression             | create_property_graph_tests | g4                  | t2                  | i           |                1
+ regression             | create_property_graph_tests | g4                  | t3                  | x           |                1
+(16 rows)
+
+SELECT * FROM information_schema.pg_edge_table_components ORDER BY property_graph_name, edge_table_alias, edge_end DESC, ordinal_position;
+ property_graph_catalog |    property_graph_schema    | property_graph_name | edge_table_alias | vertex_table_alias |  edge_end   | edge_table_column_name | vertex_table_column_name | ordinal_position 
+------------------------+-----------------------------+---------------------+------------------+--------------------+-------------+------------------------+--------------------------+------------------
+ regression             | create_property_graph_tests | g2                  | e1               | t1                 | SOURCE      | a                      | a                        |                1
+ regression             | create_property_graph_tests | g2                  | e1               | t2                 | DESTINATION | i                      | i                        |                1
+ regression             | create_property_graph_tests | g2                  | e2               | t1                 | SOURCE      | a                      | a                        |                1
+ regression             | create_property_graph_tests | g2                  | e2               | t3                 | DESTINATION | x                      | x                        |                1
+ regression             | create_property_graph_tests | g2                  | e2               | t3                 | DESTINATION | t                      | y                        |                2
+ regression             | create_property_graph_tests | g4                  | e1               | t1                 | SOURCE      | a                      | a                        |                1
+ regression             | create_property_graph_tests | g4                  | e1               | t2                 | DESTINATION | i                      | i                        |                1
+ regression             | create_property_graph_tests | g4                  | e2               | t1                 | SOURCE      | a                      | a                        |                1
+ regression             | create_property_graph_tests | g4                  | e2               | t3                 | DESTINATION | x                      | x                        |                1
+ regression             | create_property_graph_tests | g4                  | e2               | t3                 | DESTINATION | t                      | y                        |                2
+(10 rows)
+
+SELECT * FROM information_schema.pg_element_table_labels ORDER BY property_graph_name, element_table_alias, label_name;
+ property_graph_catalog |    property_graph_schema    | property_graph_name | element_table_alias | label_name 
+------------------------+-----------------------------+---------------------+---------------------+------------
+ regression             | create_property_graph_tests | g2                  | e1                  | e1
+ regression             | create_property_graph_tests | g2                  | e2                  | e2
+ regression             | create_property_graph_tests | g2                  | t1                  | t1
+ regression             | create_property_graph_tests | g2                  | t2                  | t2
+ regression             | create_property_graph_tests | g2                  | t3                  | t3l1
+ regression             | create_property_graph_tests | g2                  | t3                  | t3l2
+ regression             | create_property_graph_tests | g3                  | t1                  | t1
+ regression             | create_property_graph_tests | g3                  | t3                  | t3l1
+ regression             | create_property_graph_tests | g3                  | t3                  | t3l2
+ regression             | create_property_graph_tests | g4                  | e1                  | e1
+ regression             | create_property_graph_tests | g4                  | e2                  | e2
+ regression             | create_property_graph_tests | g4                  | t1                  | t1
+ regression             | create_property_graph_tests | g4                  | t2                  | t2
+ regression             | create_property_graph_tests | g4                  | t3                  | t3l1
+ regression             | create_property_graph_tests | g4                  | t3                  | t3l2
+(15 rows)
+
+SELECT * FROM information_schema.pg_element_table_properties ORDER BY property_graph_name, element_table_alias, property_name;
+ property_graph_catalog |    property_graph_schema    | property_graph_name | element_table_alias | property_name | property_expression 
+------------------------+-----------------------------+---------------------+---------------------+---------------+---------------------
+ regression             | create_property_graph_tests | g2                  | e1                  | a             | a
+ regression             | create_property_graph_tests | g2                  | e1                  | i             | i
+ regression             | create_property_graph_tests | g2                  | e1                  | t             | t
+ regression             | create_property_graph_tests | g2                  | e2                  | a             | a
+ regression             | create_property_graph_tests | g2                  | e2                  | t             | t
+ regression             | create_property_graph_tests | g2                  | e2                  | x             | x
+ regression             | create_property_graph_tests | g2                  | t1                  | a             | a
+ regression             | create_property_graph_tests | g2                  | t1                  | b             | b
+ regression             | create_property_graph_tests | g2                  | t2                  | i             | i
+ regression             | create_property_graph_tests | g2                  | t2                  | j             | j
+ regression             | create_property_graph_tests | g2                  | t2                  | k             | k
+ regression             | create_property_graph_tests | g2                  | t3                  | x             | x
+ regression             | create_property_graph_tests | g2                  | t3                  | y             | y
+ regression             | create_property_graph_tests | g2                  | t3                  | z             | z
+ regression             | create_property_graph_tests | g3                  | t1                  | a             | a
+ regression             | create_property_graph_tests | g3                  | t1                  | b             | b
+ regression             | create_property_graph_tests | g3                  | t3                  | x             | x
+ regression             | create_property_graph_tests | g3                  | t3                  | y             | y
+ regression             | create_property_graph_tests | g3                  | t3                  | z             | z
+ regression             | create_property_graph_tests | g4                  | e1                  | a             | a
+ regression             | create_property_graph_tests | g4                  | e1                  | i             | i
+ regression             | create_property_graph_tests | g4                  | e1                  | t             | t
+ regression             | create_property_graph_tests | g4                  | e2                  | a             | a
+ regression             | create_property_graph_tests | g4                  | e2                  | t             | t
+ regression             | create_property_graph_tests | g4                  | e2                  | x             | x
+ regression             | create_property_graph_tests | g4                  | t2                  | i_j           | (i + j)
+ regression             | create_property_graph_tests | g4                  | t2                  | kk            | (k * 2)
+ regression             | create_property_graph_tests | g4                  | t3                  | x             | x
+ regression             | create_property_graph_tests | g4                  | t3                  | yy            | y
+ regression             | create_property_graph_tests | g4                  | t3                  | zz            | z
+(30 rows)
+
+SELECT * FROM information_schema.pg_label_properties ORDER BY property_graph_name, label_name, property_name;
+ property_graph_catalog |    property_graph_schema    | property_graph_name | label_name | property_name 
+------------------------+-----------------------------+---------------------+------------+---------------
+ regression             | create_property_graph_tests | g2                  | e1         | a
+ regression             | create_property_graph_tests | g2                  | e1         | i
+ regression             | create_property_graph_tests | g2                  | e1         | t
+ regression             | create_property_graph_tests | g2                  | e2         | a
+ regression             | create_property_graph_tests | g2                  | e2         | t
+ regression             | create_property_graph_tests | g2                  | e2         | x
+ regression             | create_property_graph_tests | g2                  | t1         | a
+ regression             | create_property_graph_tests | g2                  | t1         | b
+ regression             | create_property_graph_tests | g2                  | t2         | i
+ regression             | create_property_graph_tests | g2                  | t2         | j
+ regression             | create_property_graph_tests | g2                  | t2         | k
+ regression             | create_property_graph_tests | g2                  | t3l1       | x
+ regression             | create_property_graph_tests | g2                  | t3l1       | y
+ regression             | create_property_graph_tests | g2                  | t3l1       | z
+ regression             | create_property_graph_tests | g2                  | t3l2       | x
+ regression             | create_property_graph_tests | g2                  | t3l2       | y
+ regression             | create_property_graph_tests | g2                  | t3l2       | z
+ regression             | create_property_graph_tests | g3                  | t1         | a
+ regression             | create_property_graph_tests | g3                  | t1         | b
+ regression             | create_property_graph_tests | g3                  | t3l1       | x
+ regression             | create_property_graph_tests | g3                  | t3l1       | y
+ regression             | create_property_graph_tests | g3                  | t3l1       | z
+ regression             | create_property_graph_tests | g3                  | t3l2       | x
+ regression             | create_property_graph_tests | g3                  | t3l2       | y
+ regression             | create_property_graph_tests | g3                  | t3l2       | z
+ regression             | create_property_graph_tests | g4                  | e1         | a
+ regression             | create_property_graph_tests | g4                  | e1         | i
+ regression             | create_property_graph_tests | g4                  | e1         | t
+ regression             | create_property_graph_tests | g4                  | e2         | a
+ regression             | create_property_graph_tests | g4                  | e2         | t
+ regression             | create_property_graph_tests | g4                  | e2         | x
+ regression             | create_property_graph_tests | g4                  | t2         | i_j
+ regression             | create_property_graph_tests | g4                  | t2         | kk
+ regression             | create_property_graph_tests | g4                  | t3l1       | x
+ regression             | create_property_graph_tests | g4                  | t3l1       | yy
+ regression             | create_property_graph_tests | g4                  | t3l2       | x
+ regression             | create_property_graph_tests | g4                  | t3l2       | zz
+(37 rows)
+
+SELECT * FROM information_schema.pg_labels ORDER BY property_graph_name, label_name;
+ property_graph_catalog |    property_graph_schema    | property_graph_name | label_name 
+------------------------+-----------------------------+---------------------+------------
+ regression             | create_property_graph_tests | g2                  | e1
+ regression             | create_property_graph_tests | g2                  | e2
+ regression             | create_property_graph_tests | g2                  | t1
+ regression             | create_property_graph_tests | g2                  | t2
+ regression             | create_property_graph_tests | g2                  | t3l1
+ regression             | create_property_graph_tests | g2                  | t3l2
+ regression             | create_property_graph_tests | g3                  | t1
+ regression             | create_property_graph_tests | g3                  | t3l1
+ regression             | create_property_graph_tests | g3                  | t3l2
+ regression             | create_property_graph_tests | g4                  | e1
+ regression             | create_property_graph_tests | g4                  | e2
+ regression             | create_property_graph_tests | g4                  | t1
+ regression             | create_property_graph_tests | g4                  | t2
+ regression             | create_property_graph_tests | g4                  | t3l1
+ regression             | create_property_graph_tests | g4                  | t3l2
+(15 rows)
+
+SELECT * FROM information_schema.pg_property_data_types ORDER BY property_graph_name, property_name;
+ property_graph_catalog |    property_graph_schema    | property_graph_name | property_name | data_type | character_maximum_length | character_octet_length | character_set_catalog | character_set_schema | character_set_name | collation_catalog | collation_schema | collation_name | numeric_precision | numeric_precision_radix | numeric_scale | datetime_precision | interval_type | interval_precision | user_defined_type_catalog | user_defined_type_schema | user_defined_type_name | scope_catalog | scope_schema | scope_name | maximum_cardinality | dtd_identifier 
+------------------------+-----------------------------+---------------------+---------------+-----------+--------------------------+------------------------+-----------------------+----------------------+--------------------+-------------------+------------------+----------------+-------------------+-------------------------+---------------+--------------------+---------------+--------------------+---------------------------+--------------------------+------------------------+---------------+--------------+------------+---------------------+----------------
+ regression             | create_property_graph_tests | g2                  | a             | integer   |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | int4                   |               |              |            |                     | a
+ regression             | create_property_graph_tests | g2                  | b             | text      |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | text                   |               |              |            |                     | b
+ regression             | create_property_graph_tests | g2                  | i             | integer   |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | int4                   |               |              |            |                     | i
+ regression             | create_property_graph_tests | g2                  | j             | integer   |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | int4                   |               |              |            |                     | j
+ regression             | create_property_graph_tests | g2                  | k             | integer   |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | int4                   |               |              |            |                     | k
+ regression             | create_property_graph_tests | g2                  | t             | text      |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | text                   |               |              |            |                     | t
+ regression             | create_property_graph_tests | g2                  | x             | integer   |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | int4                   |               |              |            |                     | x
+ regression             | create_property_graph_tests | g2                  | y             | text      |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | text                   |               |              |            |                     | y
+ regression             | create_property_graph_tests | g2                  | z             | text      |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | text                   |               |              |            |                     | z
+ regression             | create_property_graph_tests | g3                  | a             | integer   |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | int4                   |               |              |            |                     | a
+ regression             | create_property_graph_tests | g3                  | b             | text      |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | text                   |               |              |            |                     | b
+ regression             | create_property_graph_tests | g3                  | x             | integer   |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | int4                   |               |              |            |                     | x
+ regression             | create_property_graph_tests | g3                  | y             | text      |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | text                   |               |              |            |                     | y
+ regression             | create_property_graph_tests | g3                  | z             | text      |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | text                   |               |              |            |                     | z
+ regression             | create_property_graph_tests | g4                  | a             | integer   |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | int4                   |               |              |            |                     | a
+ regression             | create_property_graph_tests | g4                  | i             | integer   |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | int4                   |               |              |            |                     | i
+ regression             | create_property_graph_tests | g4                  | i_j           | integer   |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | int4                   |               |              |            |                     | i_j
+ regression             | create_property_graph_tests | g4                  | kk            | integer   |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | int4                   |               |              |            |                     | kk
+ regression             | create_property_graph_tests | g4                  | t             | text      |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | text                   |               |              |            |                     | t
+ regression             | create_property_graph_tests | g4                  | x             | integer   |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | int4                   |               |              |            |                     | x
+ regression             | create_property_graph_tests | g4                  | yy            | text      |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | text                   |               |              |            |                     | yy
+ regression             | create_property_graph_tests | g4                  | zz            | text      |                          |                        |                       |                      |                    |                   |                  |                |                   |                         |               |                    |               |                    | regression                | pg_catalog               | text                   |               |              |            |                     | zz
+(22 rows)
+
+SELECT * FROM information_schema.pg_property_graph_privileges WHERE grantee LIKE 'regress%' ORDER BY property_graph_name;
+       grantor       |       grantee       | property_graph_catalog |    property_graph_schema    | property_graph_name | privilege_type | is_grantable 
+---------------------+---------------------+------------------------+-----------------------------+---------------------+----------------+--------------
+ regress_graph_user1 | regress_graph_user1 | regression             | create_property_graph_tests | g1                  | SELECT         | YES
+ regress_graph_user1 | regress_graph_user2 | regression             | create_property_graph_tests | g1                  | SELECT         | NO
+(2 rows)
+
+\a\t
+SELECT pg_get_propgraphdef('g2'::regclass);
+CREATE PROPERTY GRAPH create_property_graph_tests.g2
+    VERTEX TABLES (
+        t1 KEY (a) PROPERTIES (a, b),
+        t2 KEY (i) PROPERTIES (i, j, k),
+        t3 KEY (x) LABEL t3l1 PROPERTIES (x, y, z) LABEL t3l2 PROPERTIES (x, y, z)
+    )
+    EDGE TABLES (
+        e1 KEY (a, i) SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (i) REFERENCES t2 (i) PROPERTIES (a, i, t),
+        e2 KEY (a, x) SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (x, t) REFERENCES t3 (x, y) PROPERTIES (a, t, x)
+    )
+SELECT pg_get_propgraphdef('g3'::regclass);
+CREATE PROPERTY GRAPH create_property_graph_tests.g3
+    VERTEX TABLES (
+        t1 KEY (a) PROPERTIES (a, b),
+        t3 KEY (x) LABEL t3l1 PROPERTIES (x, y, z) LABEL t3l2 PROPERTIES (x, y, z)
+    )
+SELECT pg_get_propgraphdef('g4'::regclass);
+CREATE PROPERTY GRAPH create_property_graph_tests.g4
+    VERTEX TABLES (
+        t1 KEY (a) NO PROPERTIES,
+        t2 KEY (i) PROPERTIES ((i + j) AS i_j, (k * 2) AS kk),
+        t3 KEY (x) LABEL t3l1 PROPERTIES (x, y AS yy) LABEL t3l2 PROPERTIES (x, z AS zz)
+    )
+    EDGE TABLES (
+        e1 KEY (a, i) SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (i) REFERENCES t2 (i) PROPERTIES (a, i, t),
+        e2 KEY (a, x) SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (x, t) REFERENCES t3 (x, y) PROPERTIES (a, t, x)
+    )
+SELECT pg_get_propgraphdef('pg_type'::regclass);  -- error
+ERROR:  "pg_type" is not a property graph
+\a\t
+\dG g1
+                             List of relations
+           Schema            | Name |      Type      |        Owner        
+-----------------------------+------+----------------+---------------------
+ create_property_graph_tests | g1   | property graph | regress_graph_user1
+(1 row)
+
+-- TODO
+\d g1
+Property graph "create_property_graph_tests.g1"
+ Column | Type 
+--------+------
+
+\d+ g1
+Property graph "create_property_graph_tests.g1"
+ Column | Type | Storage 
+--------+------+---------
+Property graph definition:
+ CREATE PROPERTY GRAPH create_property_graph_tests.g1
+
+DROP TABLE g2;  -- error: wrong object type
+ERROR:  "g2" is not a table
+HINT:  Use DROP PROPERTY GRAPH to remove a property graph.
+DROP PROPERTY GRAPH g1;
+DROP PROPERTY GRAPH g1;  -- error: does not exist
+ERROR:  property graph "g1" does not exist
+DROP PROPERTY GRAPH IF EXISTS g1;
+NOTICE:  property graph "g1" does not exist, skipping
+-- leave for pg_upgrade/pg_dump tests
+--DROP SCHEMA create_property_graph_tests CASCADE;
+DROP ROLE regress_graph_user1, regress_graph_user2;
diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out
new file mode 100644
index 00000000000..f22ba091a7e
--- /dev/null
+++ b/src/test/regress/expected/graph_table.out
@@ -0,0 +1,122 @@
+CREATE SCHEMA graph_table_tests;
+GRANT USAGE ON SCHEMA graph_table_tests TO PUBLIC;
+SET search_path = graph_table_tests;
+CREATE TABLE products (
+    product_no integer PRIMARY KEY,
+    name varchar,
+    price numeric
+);
+CREATE TABLE customers (
+    customer_id integer PRIMARY KEY,
+    name varchar,
+    address varchar
+);
+CREATE TABLE orders (
+    order_id integer PRIMARY KEY,
+    ordered_when date
+);
+CREATE TABLE order_items (
+    order_items_id integer PRIMARY KEY,
+    order_id integer REFERENCES orders (order_id),
+    product_no integer REFERENCES products (product_no),
+    quantity integer
+);
+CREATE TABLE customer_orders (
+    customer_orders_id integer PRIMARY KEY,
+    customer_id integer REFERENCES customers (customer_id),
+    order_id integer REFERENCES orders (order_id)
+);
+CREATE PROPERTY GRAPH myshop
+    VERTEX TABLES (
+        products,
+        customers,
+        orders
+    )
+    EDGE TABLES (
+        order_items KEY (order_items_id)
+            SOURCE KEY (order_id) REFERENCES orders (order_id)
+            DESTINATION KEY (product_no) REFERENCES products (product_no),
+        customer_orders KEY (customer_orders_id)
+            SOURCE KEY (customer_id) REFERENCES customers (customer_id)
+            DESTINATION KEY (order_id) REFERENCES orders (order_id)
+    );
+SELECT customer_name FROM GRAPH_TABLE (xxx MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name));  -- error
+ERROR:  relation "xxx" does not exist
+LINE 1: SELECT customer_name FROM GRAPH_TABLE (xxx MATCH (c IS custo...
+                                               ^
+SELECT customer_name FROM GRAPH_TABLE (pg_class MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name));  -- error
+ERROR:  "pg_class" is not a property graph
+LINE 1: SELECT customer_name FROM GRAPH_TABLE (pg_class MATCH (c IS ...
+                                               ^
+SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (cx.name AS customer_name));  -- error
+ERROR:  graph pattern variable "cx" does not exist
+SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.namex AS customer_name));  -- error
+ERROR:  property "namex" does not exist
+INSERT INTO products VALUES
+    (1, 'product1', 10),
+    (2, 'product2', 20),
+    (3, 'product3', 30);
+INSERT INTO customers VALUES
+    (1, 'customer1', 'US'),
+    (2, 'customer2', 'CA'),
+    (3, 'customer3', 'GL');
+INSERT INTO orders VALUES
+    (1, '2024-01-01'),
+    (2, '2024-01-02'),
+    (3, '2024-01-03');
+INSERT INTO order_items (order_items_id, order_id, product_no, quantity) VALUES
+    (1, 1, 1, 5),
+    (2, 1, 2, 10),
+    (3, 2, 1, 7);
+INSERT INTO customer_orders (customer_orders_id, customer_id, order_id) VALUES
+    (1, 1, 1),
+    (2, 2, 2);
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name));
+ customer_name 
+---------------
+ customer1
+(1 row)
+
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c:customers)-[co:customer_orders]->(o:orders WHERE o.ordered_when = '2024-01-02') COLUMNS (c.name, c.address));
+   name    | address 
+-----------+---------
+ customer2 | CA
+(1 row)
+
+SELECT * FROM GRAPH_TABLE (myshop MATCH (o IS orders)-[IS customer_orders]->(c IS customers) COLUMNS (c.name, o.ordered_when));
+ name | ordered_when 
+------+--------------
+(0 rows)
+
+SELECT * FROM GRAPH_TABLE (myshop MATCH (o IS orders)<-[IS customer_orders]-(c IS customers) COLUMNS (c.name, o.ordered_when));
+   name    | ordered_when 
+-----------+--------------
+ customer1 | 01-01-2024
+ customer2 | 01-02-2024
+(2 rows)
+
+-- TODO: should approximately match this query:
+SET debug_print_parse = on;
+SELECT customer_name FROM (
+    SELECT c.name AS customer_name
+    FROM customers c, customer_orders _co, orders o
+    WHERE _co.customer_id = c.customer_id
+      AND _co.order_id = o.order_id
+      AND c.address = 'US'
+) myshop;
+ customer_name 
+---------------
+ customer1
+(1 row)
+
+RESET debug_print_parse;
+CREATE VIEW customers_us AS SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name));
+SELECT pg_get_viewdef('customers_us'::regclass);
+                                                                          pg_get_viewdef                                                                           
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  SELECT customer_name                                                                                                                                            +
+    FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE ((c.address)::text = 'US'::text))-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name));
+(1 row)
+
+-- leave for pg_upgrade/pg_dump tests
+--DROP SCHEMA graph_table_tests CASCADE;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index fc42d418bf1..432ba471fe3 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -34,6 +34,7 @@ CREATE FUNCTION addr_nsp.trig() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN END
 CREATE TRIGGER t BEFORE INSERT ON addr_nsp.gentable FOR EACH ROW EXECUTE PROCEDURE addr_nsp.trig();
 CREATE POLICY genpol ON addr_nsp.gentable;
 CREATE PROCEDURE addr_nsp.proc(int4) LANGUAGE SQL AS $$ $$;
+CREATE PROPERTY GRAPH addr_nsp.gengraph;
 CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
@@ -98,7 +99,7 @@ DECLARE
 BEGIN
     FOR objtype IN VALUES
         ('table'), ('index'), ('sequence'), ('view'),
-        ('materialized view'), ('foreign table'),
+        ('materialized view'), ('foreign table'), ('property graph'),
         ('table column'), ('foreign table column'),
         ('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
         ('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
@@ -159,6 +160,12 @@ WARNING:  error for foreign table,{addr_nsp,zwei},{}: relation "addr_nsp.zwei" d
 WARNING:  error for foreign table,{addr_nsp,zwei},{integer}: relation "addr_nsp.zwei" does not exist
 WARNING:  error for foreign table,{eins,zwei,drei},{}: cross-database references are not implemented: "eins.zwei.drei"
 WARNING:  error for foreign table,{eins,zwei,drei},{integer}: cross-database references are not implemented: "eins.zwei.drei"
+WARNING:  error for property graph,{eins},{}: relation "eins" does not exist
+WARNING:  error for property graph,{eins},{integer}: relation "eins" does not exist
+WARNING:  error for property graph,{addr_nsp,zwei},{}: relation "addr_nsp.zwei" does not exist
+WARNING:  error for property graph,{addr_nsp,zwei},{integer}: relation "addr_nsp.zwei" does not exist
+WARNING:  error for property graph,{eins,zwei,drei},{}: cross-database references are not implemented: "eins.zwei.drei"
+WARNING:  error for property graph,{eins,zwei,drei},{integer}: cross-database references are not implemented: "eins.zwei.drei"
 WARNING:  error for table column,{eins},{}: column name must be qualified
 WARNING:  error for table column,{eins},{integer}: column name must be qualified
 WARNING:  error for table column,{addr_nsp,zwei},{}: relation "addr_nsp" does not exist
@@ -398,6 +405,7 @@ WITH objects (type, name, args) AS (VALUES
     ('view', '{addr_nsp, genview}', '{}'),
     ('materialized view', '{addr_nsp, genmatview}', '{}'),
     ('foreign table', '{addr_nsp, genftable}', '{}'),
+    ('property graph', '{addr_nsp, gengraph}', '{}'),
     ('table column', '{addr_nsp, gentable, b}', '{}'),
     ('foreign table column', '{addr_nsp, genftable, a}', '{}'),
     ('aggregate', '{addr_nsp, genaggr}', '{int4}'),
@@ -474,6 +482,7 @@ view|addr_nsp|genview|addr_nsp.genview|t
 materialized view|addr_nsp|genmatview|addr_nsp.genmatview|t
 foreign table|addr_nsp|genftable|addr_nsp.genftable|t
 foreign table column|addr_nsp|genftable|addr_nsp.genftable.a|t
+property graph|addr_nsp|gengraph|addr_nsp.gengraph|t
 role|NULL|regress_addr_user|regress_addr_user|t
 server|NULL|addr_fserv|addr_fserv|t
 user mapping|NULL|NULL|regress_addr_user on server integer|t
@@ -518,7 +527,7 @@ DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
 DROP SCHEMA addr_nsp CASCADE;
-NOTICE:  drop cascades to 14 other objects
+NOTICE:  drop cascades to 15 other objects
 DETAIL:  drop cascades to text search dictionary addr_ts_dict
 drop cascades to text search configuration addr_ts_conf
 drop cascades to text search template addr_ts_temp
@@ -533,6 +542,7 @@ drop cascades to function genaggr(integer)
 drop cascades to type gendomain
 drop cascades to function trig()
 drop cascades to function proc(integer)
+drop cascades to property graph gengraph
 DROP OWNED BY regress_addr_user;
 DROP USER regress_addr_user;
 --
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be3..08b84962c8a 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -266,3 +266,12 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_propgraph_element {pgepgid} => pg_class {oid}
+NOTICE:  checking pg_propgraph_element {pgerelid} => pg_class {oid}
+NOTICE:  checking pg_propgraph_element {pgesrcvertexid} => pg_propgraph_element {oid}
+NOTICE:  checking pg_propgraph_element {pgedestvertexid} => pg_propgraph_element {oid}
+NOTICE:  checking pg_propgraph_label {pglpgid} => pg_class {oid}
+NOTICE:  checking pg_propgraph_label {pglelid} => pg_propgraph_element {oid}
+NOTICE:  checking pg_propgraph_property {pgppgid} => pg_class {oid}
+NOTICE:  checking pg_propgraph_property {pgptypid} => pg_type {oid}
+NOTICE:  checking pg_propgraph_property {pgplabelid} => pg_propgraph_label {oid}
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1d8a414eea7..09e69e16cd8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -48,7 +48,7 @@ test: create_index create_index_spgist create_view index_including index_includi
 # ----------
 # Another group of parallel tests
 # ----------
-test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse
+test: create_aggregate create_function_sql create_cast constraints triggers select inherit typed_table vacuum drop_if_exists updatable_views roleattributes create_am hash_func errors infinite_recurse create_property_graph
 
 # ----------
 # sanity_check does a vacuum, affecting the sort order of SELECT *
@@ -78,7 +78,7 @@ test: brin_bloom brin_multi
 # psql depends on create_am
 # amutils depends on geometry, create_index_spgist, hash_index, brin
 # ----------
-test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort create_role without_overlaps
+test: create_table_like alter_generic alter_operator misc async dbsize merge misc_functions sysviews tsrf tid tidscan tidrangescan collate.icu.utf8 incremental_sort create_role without_overlaps graph_table
 
 # collate.*.utf8 tests cannot be run in parallel with each other
 test: rules psql psql_crosstab amutils stats_ext collate.linux.utf8 collate.windows.win1252
diff --git a/src/test/regress/sql/alter_generic.sql b/src/test/regress/sql/alter_generic.sql
index de58d268d31..067d5c2b490 100644
--- a/src/test/regress/sql/alter_generic.sql
+++ b/src/test/regress/sql/alter_generic.sql
@@ -456,6 +456,40 @@ CREATE OPERATOR FAMILY alt_opf19 USING btree;
 ALTER OPERATOR FAMILY alt_opf19 USING btree DROP FUNCTION 5 (int4, int4);
 DROP OPERATOR FAMILY alt_opf19 USING btree;
 
+--
+-- Property Graph
+--
+SET SESSION AUTHORIZATION regress_alter_generic_user1;
+CREATE PROPERTY GRAPH alt_graph1;
+CREATE PROPERTY GRAPH alt_graph2;
+CREATE PROPERTY GRAPH alt_graph3;
+
+ALTER PROPERTY GRAPH alt_graph1 RENAME TO alt_graph2; -- failed (name conflict)
+ALTER PROPERTY GRAPH alt_graph1 RENAME TO alt_graph4; -- OK
+ALTER PROPERTY GRAPH alt_graph2 OWNER TO regress_alter_generic_user2;  -- failed (no role membership)
+ALTER PROPERTY GRAPH alt_graph2 OWNER TO regress_alter_generic_user3;  -- OK
+ALTER PROPERTY GRAPH alt_graph4 SET SCHEMA alt_nsp2;  -- OK
+ALTER PROPERTY GRAPH alt_nsp2.alt_graph4 RENAME TO alt_graph2;  -- OK
+ALTER PROPERTY GRAPH alt_graph2 SET SCHEMA alt_nsp2;  -- failed (name conflict)
+
+SET SESSION AUTHORIZATION regress_alter_generic_user2;
+CREATE PROPERTY GRAPH alt_graph5;
+
+ALTER PROPERTY GRAPH alt_graph3 RENAME TO alt_graph5;  -- failed (not owner)
+ALTER PROPERTY GRAPH alt_graph5 RENAME TO alt_graph6;  -- OK
+ALTER PROPERTY GRAPH alt_graph3 OWNER TO regress_alter_generic_user2;  -- failed (not owner)
+ALTER PROPERTY GRAPH alt_graph6 OWNER TO regress_alter_generic_user3;  -- failed (no role membership)
+ALTER PROPERTY GRAPH alt_graph3 SET SCHEMA alt_nsp2;  -- failed (not owner)
+
+RESET SESSION AUTHORIZATION;
+
+SELECT nspname, relname, rolname
+  FROM pg_class c, pg_namespace n, pg_authid a
+  WHERE c.relnamespace = n.oid AND c.relowner = a.oid
+    AND n.nspname in ('alt_nsp1', 'alt_nsp2')
+    AND c.relkind = 'g'
+  ORDER BY nspname, relname;
+
 --
 -- Statistics
 --
diff --git a/src/test/regress/sql/create_property_graph.sql b/src/test/regress/sql/create_property_graph.sql
new file mode 100644
index 00000000000..2f0b42ecd2d
--- /dev/null
+++ b/src/test/regress/sql/create_property_graph.sql
@@ -0,0 +1,130 @@
+CREATE SCHEMA create_property_graph_tests;
+GRANT USAGE ON SCHEMA create_property_graph_tests TO PUBLIC;
+SET search_path = create_property_graph_tests;
+
+CREATE ROLE regress_graph_user1;
+CREATE ROLE regress_graph_user2;
+
+CREATE PROPERTY GRAPH g1;
+
+COMMENT ON PROPERTY GRAPH g1 IS 'a graph';
+
+CREATE PROPERTY GRAPH g1;  -- error: duplicate
+
+CREATE TABLE t1 (a int, b text);
+CREATE TABLE t2 (i int PRIMARY KEY, j int, k int);
+CREATE TABLE t3 (x int, y text, z text);
+
+CREATE TABLE e1 (a int, i int, t text, PRIMARY KEY (a, i));
+CREATE TABLE e2 (a int, x int, t text);
+
+CREATE PROPERTY GRAPH g2
+    VERTEX TABLES (t1 KEY (a), t2 DEFAULT LABEL, t3 KEY (x) LABEL t3l1 LABEL t3l2)
+    EDGE TABLES (
+        e1
+            SOURCE KEY (a) REFERENCES t1 (a)
+            DESTINATION KEY (i) REFERENCES t2 (i),
+        e2 KEY (a, x)
+            SOURCE KEY (a) REFERENCES t1 (a)
+            DESTINATION KEY (x, t) REFERENCES t3 (x, y)
+    );
+
+-- like g2 but assembled with ALTER
+CREATE PROPERTY GRAPH g3;
+ALTER PROPERTY GRAPH g3 ADD VERTEX TABLES (t1 KEY (a), t2 DEFAULT LABEL);
+ALTER PROPERTY GRAPH g3
+    ADD VERTEX TABLES (t3 KEY (x) LABEL t3l1)
+    ADD EDGE TABLES (
+        e1 SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (i) REFERENCES t2 (i),
+        e2 KEY (a, x) SOURCE KEY (a) REFERENCES t1 (a) DESTINATION KEY (x, t) REFERENCES t3 (x, y)
+    );
+ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 ADD LABEL t3l2 PROPERTIES ALL COLUMNS ADD LABEL t3l3 PROPERTIES ALL COLUMNS;
+ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l3x;  -- error
+ALTER PROPERTY GRAPH g3 ALTER VERTEX TABLE t3 DROP LABEL t3l3;
+ALTER PROPERTY GRAPH g3 DROP VERTEX TABLES (t2);  -- fail (TODO: dubious error message)
+ALTER PROPERTY GRAPH g3 DROP VERTEX TABLES (t2) CASCADE;
+ALTER PROPERTY GRAPH g3 DROP EDGE TABLES (e2);
+
+CREATE PROPERTY GRAPH g4
+    VERTEX TABLES (
+        t1 KEY (a) NO PROPERTIES,
+        t2 DEFAULT LABEL PROPERTIES (i + j AS i_j, k),
+        t3 KEY (x) LABEL t3l1 PROPERTIES (x, y AS yy) LABEL t3l2 PROPERTIES (x, z AS zz)
+    )
+    EDGE TABLES (
+        e1
+            SOURCE KEY (a) REFERENCES t1 (a)
+            DESTINATION KEY (i) REFERENCES t2 (i)
+            PROPERTIES ALL COLUMNS,
+        e2 KEY (a, x)
+            SOURCE KEY (a) REFERENCES t1 (a)
+            DESTINATION KEY (x, t) REFERENCES t3 (x, y)
+            PROPERTIES ALL COLUMNS
+    );
+
+ALTER PROPERTY GRAPH g4 ALTER VERTEX TABLE t2 ALTER LABEL t2 ADD PROPERTIES (k * 2 AS kk);
+ALTER PROPERTY GRAPH g4 ALTER VERTEX TABLE t2 ALTER LABEL t2 DROP PROPERTIES (k);
+
+-- error cases
+CREATE PROPERTY GRAPH gx VERTEX TABLES (xx, yy);
+CREATE PROPERTY GRAPH gx VERTEX TABLES (t1 KEY (a), t2 KEY (i), t1 KEY (a));
+CREATE PROPERTY GRAPH gx
+    VERTEX TABLES (t1 AS tt KEY (a), t2 KEY (i))
+    EDGE TABLES (
+        e1 SOURCE t1 DESTINATION t2
+    );
+CREATE PROPERTY GRAPH gx
+    VERTEX TABLES (t1 KEY (a), t2 KEY (i))
+    EDGE TABLES (
+        e1 SOURCE t1 DESTINATION tx
+    );
+COMMENT ON PROPERTY GRAPH gx IS 'not a graph';
+
+
+ALTER PROPERTY GRAPH g1 OWNER TO regress_graph_user1;
+SET ROLE regress_graph_user1;
+GRANT SELECT ON PROPERTY GRAPH g1 TO regress_graph_user2;
+GRANT UPDATE ON PROPERTY GRAPH g1 TO regress_graph_user2;  -- fail
+RESET ROLE;
+
+
+-- information schema
+
+SELECT * FROM information_schema.property_graphs ORDER BY property_graph_name;
+SELECT * FROM information_schema.pg_element_tables ORDER BY property_graph_name, element_table_alias;
+SELECT * FROM information_schema.pg_element_table_key_columns ORDER BY property_graph_name, element_table_alias, ordinal_position;
+SELECT * FROM information_schema.pg_edge_table_components ORDER BY property_graph_name, edge_table_alias, edge_end DESC, ordinal_position;
+SELECT * FROM information_schema.pg_element_table_labels ORDER BY property_graph_name, element_table_alias, label_name;
+SELECT * FROM information_schema.pg_element_table_properties ORDER BY property_graph_name, element_table_alias, property_name;
+SELECT * FROM information_schema.pg_label_properties ORDER BY property_graph_name, label_name, property_name;
+SELECT * FROM information_schema.pg_labels ORDER BY property_graph_name, label_name;
+SELECT * FROM information_schema.pg_property_data_types ORDER BY property_graph_name, property_name;
+SELECT * FROM information_schema.pg_property_graph_privileges WHERE grantee LIKE 'regress%' ORDER BY property_graph_name;
+
+
+\a\t
+SELECT pg_get_propgraphdef('g2'::regclass);
+SELECT pg_get_propgraphdef('g3'::regclass);
+SELECT pg_get_propgraphdef('g4'::regclass);
+
+SELECT pg_get_propgraphdef('pg_type'::regclass);  -- error
+\a\t
+
+\dG g1
+
+-- TODO
+\d g1
+\d+ g1
+
+DROP TABLE g2;  -- error: wrong object type
+
+DROP PROPERTY GRAPH g1;
+
+DROP PROPERTY GRAPH g1;  -- error: does not exist
+
+DROP PROPERTY GRAPH IF EXISTS g1;
+
+-- leave for pg_upgrade/pg_dump tests
+--DROP SCHEMA create_property_graph_tests CASCADE;
+
+DROP ROLE regress_graph_user1, regress_graph_user2;
diff --git a/src/test/regress/sql/graph_table.sql b/src/test/regress/sql/graph_table.sql
new file mode 100644
index 00000000000..0632cbda8b5
--- /dev/null
+++ b/src/test/regress/sql/graph_table.sql
@@ -0,0 +1,96 @@
+CREATE SCHEMA graph_table_tests;
+GRANT USAGE ON SCHEMA graph_table_tests TO PUBLIC;
+SET search_path = graph_table_tests;
+
+CREATE TABLE products (
+    product_no integer PRIMARY KEY,
+    name varchar,
+    price numeric
+);
+
+CREATE TABLE customers (
+    customer_id integer PRIMARY KEY,
+    name varchar,
+    address varchar
+);
+
+CREATE TABLE orders (
+    order_id integer PRIMARY KEY,
+    ordered_when date
+);
+
+CREATE TABLE order_items (
+    order_items_id integer PRIMARY KEY,
+    order_id integer REFERENCES orders (order_id),
+    product_no integer REFERENCES products (product_no),
+    quantity integer
+);
+
+CREATE TABLE customer_orders (
+    customer_orders_id integer PRIMARY KEY,
+    customer_id integer REFERENCES customers (customer_id),
+    order_id integer REFERENCES orders (order_id)
+);
+
+CREATE PROPERTY GRAPH myshop
+    VERTEX TABLES (
+        products,
+        customers,
+        orders
+    )
+    EDGE TABLES (
+        order_items KEY (order_items_id)
+            SOURCE KEY (order_id) REFERENCES orders (order_id)
+            DESTINATION KEY (product_no) REFERENCES products (product_no),
+        customer_orders KEY (customer_orders_id)
+            SOURCE KEY (customer_id) REFERENCES customers (customer_id)
+            DESTINATION KEY (order_id) REFERENCES orders (order_id)
+    );
+
+SELECT customer_name FROM GRAPH_TABLE (xxx MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name));  -- error
+SELECT customer_name FROM GRAPH_TABLE (pg_class MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name));  -- error
+SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (cx.name AS customer_name));  -- error
+SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.namex AS customer_name));  -- error
+
+INSERT INTO products VALUES
+    (1, 'product1', 10),
+    (2, 'product2', 20),
+    (3, 'product3', 30);
+INSERT INTO customers VALUES
+    (1, 'customer1', 'US'),
+    (2, 'customer2', 'CA'),
+    (3, 'customer3', 'GL');
+INSERT INTO orders VALUES
+    (1, '2024-01-01'),
+    (2, '2024-01-02'),
+    (3, '2024-01-03');
+INSERT INTO order_items (order_items_id, order_id, product_no, quantity) VALUES
+    (1, 1, 1, 5),
+    (2, 1, 2, 10),
+    (3, 2, 1, 7);
+INSERT INTO customer_orders (customer_orders_id, customer_id, order_id) VALUES
+    (1, 1, 1),
+    (2, 2, 2);
+
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name));
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c:customers)-[co:customer_orders]->(o:orders WHERE o.ordered_when = '2024-01-02') COLUMNS (c.name, c.address));
+SELECT * FROM GRAPH_TABLE (myshop MATCH (o IS orders)-[IS customer_orders]->(c IS customers) COLUMNS (c.name, o.ordered_when));
+SELECT * FROM GRAPH_TABLE (myshop MATCH (o IS orders)<-[IS customer_orders]-(c IS customers) COLUMNS (c.name, o.ordered_when));
+
+-- TODO: should approximately match this query:
+SET debug_print_parse = on;
+SELECT customer_name FROM (
+    SELECT c.name AS customer_name
+    FROM customers c, customer_orders _co, orders o
+    WHERE _co.customer_id = c.customer_id
+      AND _co.order_id = o.order_id
+      AND c.address = 'US'
+) myshop;
+RESET debug_print_parse;
+
+CREATE VIEW customers_us AS SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name));
+
+SELECT pg_get_viewdef('customers_us'::regclass);
+
+-- leave for pg_upgrade/pg_dump tests
+--DROP SCHEMA graph_table_tests CASCADE;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d5..93f9f9c704f 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -37,6 +37,7 @@ CREATE FUNCTION addr_nsp.trig() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN END
 CREATE TRIGGER t BEFORE INSERT ON addr_nsp.gentable FOR EACH ROW EXECUTE PROCEDURE addr_nsp.trig();
 CREATE POLICY genpol ON addr_nsp.gentable;
 CREATE PROCEDURE addr_nsp.proc(int4) LANGUAGE SQL AS $$ $$;
+CREATE PROPERTY GRAPH addr_nsp.gengraph;
 CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
@@ -90,7 +91,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 BEGIN
     FOR objtype IN VALUES
         ('table'), ('index'), ('sequence'), ('view'),
-        ('materialized view'), ('foreign table'),
+        ('materialized view'), ('foreign table'), ('property graph'),
         ('table column'), ('foreign table column'),
         ('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
         ('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
@@ -163,6 +164,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('view', '{addr_nsp, genview}', '{}'),
     ('materialized view', '{addr_nsp, genmatview}', '{}'),
     ('foreign table', '{addr_nsp, genftable}', '{}'),
+    ('property graph', '{addr_nsp, gengraph}', '{}'),
     ('table column', '{addr_nsp, gentable, b}', '{}'),
     ('foreign table column', '{addr_nsp, genftable, a}', '{}'),
     ('aggregate', '{addr_nsp, genaggr}', '{int4}'),
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index d808aad8b05..d2d7250da08 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4052,3 +4052,22 @@ rfile
 ws_options
 ws_file_info
 PathKeyInfo
+
+# TODO
+AlterPropGraphElementKind
+AlterPropGraphStmt
+CreatePropGraphStmt
+FormData_pg_propgraph_element
+FormData_pg_propgraph_label
+FormData_pg_propgraph_property
+GraphElementPattern
+GraphElementPatternKind
+GraphLabelRef
+GraphPattern
+GraphPropertyRef
+GraphTableParseState
+PropGraphEdge
+PropGraphLabelAndProperties
+PropGraphProperties
+PropGraphVertex
+RangeGraphTable

base-commit: 0413a556990ba628a3de8a0b58be020fd9a14ed0
-- 
2.43.1

---

From: Andres Freund <[email-masked]>
Date: Fri, 16 Feb 2024 11:23:01 -0800

Hi,

> proverbial few thousand lines of code in the designated areas.

One aspect that I m concerned with structurally is that the transformation,
from property graph queries to something postgres understands, is done via the
rewrite system. I doubt that that is a good idea. For one it bars the planner
from making plans that benefit from the graph query formulation. But more
importantly, we IMO should reduce usage of the rewrite system, not increase
it.

Greetings,

Andres Freund

---

From: Peter Eisentraut <[email-masked]>
Date: Fri, 23 Feb 2024 17:15:41 +0100

> importantly, we IMO should reduce usage of the rewrite system, not increase

PGQ is meant to be implemented like that, like views expanding to joins 
and unions.  This is what I have gathered during the specification 
process, and from other implementations, and from academics.  There are 
certainly other ways to combine relational and graph database stuff, 
like with native graph storage and specialized execution support, but 
this is not that, and to some extent PGQ was created to supplant those 
other approaches.

Many people will agree that the rewriter is sort of weird and archaic at 
this point.  But I'm not aware of any plans or proposals to do anything 
about it.  As long as the view expansion takes place there, it makes 
sense to align with that.  For example, all the view security stuff 
(privileges, security barriers, etc.) will eventually need to be 
considered, and it would make sense to do that in a consistent way.  So 
for now, I'm working with what we have, but let's see where it goes.

(Note to self: Check that graph inside view inside graph inside view ... 
works.)

---

From: Henson Choi <[email-masked]>
Date: Thu, 1 Jan 2026 15:16:00 +0900

Hi hackers,

I ran Valgrind memcheck against the SQL/PGQ (Property Graph) implementation
to verify there are no memory issues introduced by the new graph features.

== Test Environment ==

- PostgreSQL: 19devel (master branch)
- Valgrind: 3.22.0
- Platform: Linux x86_64 (RHEL 9)
- Tests executed: run_pgq_tests.sql
  - create_property_graph.sql
  - graph_table.sql
  - graph_table_rls.sql
  - alter_generic.sql
  - object_address.sql

== Executive Summary ==

  +----------------------------------------------------------+
  | SQL/PGQ Implementation: NO MEMORY LEAKS DETECTED         |
  +----------------------------------------------------------+

All detected leaks originate from existing PostgreSQL core components.
No graph-related functions appear in any leak stack traces.

== Backend Process 487751: Itemized Leak Analysis ==

Total: 555 bytes definitely lost in 47 blocks, 21 unique leak contexts

+================================================================================+
| #  | Bytes | Blocks | Allocator           | Root Cause       | PGQ? |
Verdict  |
+================================================================================+
| 1  | 1     | 1      | text_to_cstring     | SQL func cache   | NO   |
HARMLESS |
| 2  | 3     | 1      | pstrdup             | PL/pgSQL lexer   | NO   |
HARMLESS |
| 3  | 3     | 1      | pstrdup             | PL/pgSQL lexer   | NO   |
HARMLESS |
| 4  | 12    | 2      | downcase_identifier | PL/pgSQL parser  | NO   |
HARMLESS |
| 5  | 12    | 2      | downcase_identifier | PL/pgSQL parser  | NO   |
HARMLESS |
| 6  | 12    | 2      | downcase_identifier | PL/pgSQL parser  | NO   |
HARMLESS |
| 7  | 13    | 2      | downcase_identifier | PL/pgSQL parser  | NO   |
HARMLESS |
| 8  | 21    | 3      | downcase_identifier | PL/pgSQL parser  | NO   |
HARMLESS |
| 9  | 25    | 3      | downcase_identifier | PL/pgSQL parser  | NO   |
HARMLESS |
| 10 | 27    | 1      | detoast_attr        | SQL func cache   | NO   |
HARMLESS |
| 11 | 27    | 1      | detoast_attr        | SQL func cache   | NO   |
HARMLESS |
| 12 | 33    | 5      | downcase_identifier | PL/pgSQL parser  | NO   |
HARMLESS |
| 13 | 33    | 5      | downcase_identifier | PL/pgSQL parser  | NO   |
HARMLESS |
| 14 | 33    | 5      | downcase_identifier | PL/pgSQL parser  | NO   |
HARMLESS |
| 15 | 37    | 1      | strdup              | Postmaster init  | NO   |
HARMLESS |
| 16 | 38    | 5      | downcase_identifier | PL/pgSQL parser  | NO   |
HARMLESS |
| 17 | 68    | 1      | deconstruct_array   | SQL func cache   | NO   |
HARMLESS |
| 18 | 84    | 1      | deconstruct_array   | SQL func cache   | NO   |
HARMLESS |
| 19 | 164   | 2      | lappend             | PL/pgSQL parser  | NO   |
HARMLESS |
| 20 | 164   | 2      | lappend             | PL/pgSQL parser  | NO   |
HARMLESS |
| 21 | 358   | 1      | plpgsql_ns_push     | PL/pgSQL parser  | NO   |
HARMLESS |
+================================================================================+
     TOTAL: 555 bytes in 47 blocks
     PGQ-RELATED LEAKS: 0

== SQL/PGQ Verification ==

All 21 leak stack traces were examined. Key functions NOT present:

  - CreatePropertyGraph, DropPropertyGraph, AlterPropertyGraph
  - transformGraphTable, ExecGraphTableScan
  - Any function from src/backend/commands/propertygraphcmds.c
  - Any function from src/backend/parser/parse_graph.c
  - Any GRAPH_TABLE related executor nodes

The SQL/PGQ tests exercised CREATE PROPERTY GRAPH, DROP PROPERTY GRAPH,
and GRAPH_TABLE queries, yet none of these code paths appear in leaks.

== Process Summary ==

+--------+----------------+-----------------+----------+--------------------+
| PID    | Type           | Definitely Lost | Errors   | Notes
 |
+--------+----------------+-----------------+----------+--------------------+
| 487724 | postmaster     | 37 bytes        | 1        | strdup at startup
 |
| 487751 | backend        | 555 bytes       | 30       | See above analysis
|
| 487735 | checkpointer   | 37 bytes        | 1        | strdup at startup
 |
| 487736 | bgwriter       | 37 bytes        | 1        | strdup at startup
 |
| 487737 | walwriter      | 37 bytes        | 1        | strdup at startup
 |
| 487738 | autovacuum     | 37 bytes        | 1        | strdup at startup
 |
| 487739 | logical rep    | 37 bytes        | 1        | strdup at startup
 |
| 487740 | syslogger      | 37 bytes        | 1        | strdup at startup
 |
| Others | aux workers    | 37 bytes each   | 1 each   | strdup at startup
 |
+--------+----------------+-----------------+----------+--------------------+

== Possibly Lost (LLVM JIT) ==

Additionally, ~15KB in ~155 blocks reported as "possibly lost" from LLVM
JIT:
  - llvm::MDTuple, llvm::User allocations
  - Origin: llvm_compile_expr -> jit_compile_expr -> ExecReadyExpr
  - These are LLVM's internal caches, not PostgreSQL leaks

== Other Memory Errors ==

+--------------------------------------------------------------------------+
| NO memory access errors detected:                                        |
|   - Invalid read/write: 0                                                |
|   - Use of uninitialized value: 0                                        |
|   - Conditional jump on uninitialized value: 0                           |
|   - Syscall param errors: 0                                              |
|   - Overlap in memcpy/strcpy: 0                                          |
+--------------------------------------------------------------------------+

Suppressed errors (via valgrind.supp):
  - 487751 (backend): 330 errors from 3 contexts suppressed
  - 487762 (aux worker): 9 errors suppressed
  - Other processes: ~9 errors each suppressed

Suppression rules in valgrind.supp (12 categories):
  1. Padding        - write() syscalls with uninitialized struct padding
  2. CRC            - CRC32 calculation on padded buffers
  3. Overlap        - memcpy overlap in bootstrap/relmap
  4. glibc          - Dynamic loader internals
  5. Regex/Pattern  - RE_compile_and_cache, patternsel
  6. Planner        - query_planner memcpy overlap
  7. Cache          - RelCache, CatCache, TypeCache possible leaks
  8. XLogReader     - WAL reader allocations
  9. Parallel       - ParallelWorkerMain allocations
  10. AutoVacuum    - AutoVacWorkerMain allocations
  11. GUC/Backend   - set_config_option, InitPostgres
  12. PL/pgSQL      - plpgsql_compile, SPI_prepare, CachedPlan

All suppressed errors are known, harmless PostgreSQL behaviors.
See attached valgrind.supp for details.

== Conclusion ==

The SQL/PGQ implementation introduces NO memory leaks. All detected issues
are pre-existing PostgreSQL behaviors related to:

  1. PL/pgSQL function compilation caching
  2. SQL function metadata caching
  3. Postmaster startup allocations
  4. LLVM JIT internal caching

These are by-design patterns that trade minimal memory for performance.

== Files Attached ==

- valgrind.supp: Suppression file used for this test
- 487724.log: Postmaster log
- 487751.log: Backend log with full leak details (126KB)
- 4877xx.log: Auxiliary process logs

--
Regards

2025년 12월 30일 (화) PM 8:27, Ashutosh Bapat <[email-masked]>님이
작성:

> On Tue, Dec 30, 2025 at 3:14 PM Ashutosh Bapat
> <[email-masked]> wrote:
> >
> > On Thu, Dec 18, 2025 at 2:45 PM Ashutosh Bapat
> > <[email-masked]> wrote:
> > >
> > >
> > > >
> > > > I did another investigation about whether this level of checking is
> > > > necessary.  I think according to the letter of the SQL standard, the
> > > > typmods must indeed match.  It seems Oracle does not check (the
> example
> > > > mentioned above came from an Oracle source).  But I think it's okay
> to
> > > > keep the check.  In PostgreSQL, it is much less common to write like
> > > > varchar(1000).  And we can always consider relaxing it later.
> > >
> > > +1.
> > >
> > > Attached patch adds a couple more test statements.
> > >
> >
> > Squashed this into the main patchset.
> >
> > > >
> > > > 2) I had it in my notes to consider whether we should support the
> colon
> > > > syntax for label expressions.  I think we might have talked about
> that
> > > > before.
> > > >
> > > > I'm now leaning toward not supporting it in the first iteration.  I
> > > > don't know that we have fully explored possible conflicts with host
> > > > variable syntax in ecpg and psql and the like.  Maybe avoid that for
> now.
> > > >
> > >
> > > I was aware of ecpg and I vaguely remember we fixed something in ECPG
> > > to allow : in MATCH statement. Probably following changes in
> > > psqlscan.l and pgc.l
> > > -self                   [,()\[\].;\:\+\-\*\/\%\^\<\>\=]
> > > +self                   [,()\[\].;\:\|\+\-\*\/\%\^\<\>\=]
> > >
> > > Those changes add | after : (and not the : itself) so maybe they are
> > > not about supporting : . Do you remember what those are?
> >
> > I reverted those changes from both the files and ran "meson test". I
> > did not observe any failure. It seems those changes are not needed.
> > But adding them as a separate commit (0004) in case CI bot reveals any
> > failures without them.
> >
> > I noticed that there were no ECPG tests for SQL/PGQ. Added a basic
> > test in patch 0003.
> >
> > >
> > > I spotted some examples that use : in ddl.sgml.
> > > <programlisting>
> > > SELECT customer_name FROM GRAPH_TABLE (myshop MATCH
> > > (c:customer)-[:has]->(o:"order" WHERE o.ordered_when = current_date)
> > > COLUMNS (c.name AS customer_name));
> > > </programlisting>
> > >
> > > The query demonstrates that one can use label names in a way that will
> > > make the pattern look like an English sentence. Replacing : with IS
> > > defeats that purpose.
> > >
> > > As written in that paragraph, the labels serve the purpose of exposing
> > > the table with a different logical view (using different label and
> > > property names). So we need that paragraph, but I think we should
> > > change the example to use IS instead of :. Attached is suggested
> > > minimal change, but I am not happy with it. Another possibility is we
> > > completely remove that paragraph; I don't think we need to discuss all
> > > possible usages the users will come up with.
> > >
> > > The patch changes one more instance of : by IS. But that's straight
> forward.
> > >
> > > In ddl.sgml I noticed a seemingly incomplete sentence
> > >    A property graph is a way to represent database contents, instead
> of using
> > >    relational structures such as tables.
> > >
> > > Represent the contents as what? I feel the complete sentence should be
> > > one of the following
> > > property graph is a way to represent database contents as a graph,
> > > instead of representing those as relational structures OR
> > > property graph is another way to represent database contents instead
> > > of using relational structures such as tables
> > >
> > > But I can't figure out what was originally intended.
> >
> >
> > 0002 contains some edits to this part of documentation. I think the
> > paragraph reads better than before. Let me know what you think.
> >
> > Please let me know which of 0002 to 0004 look good to you. I will
> > squash those into the patchset in the next version.
>
> The previous patchset had a whitespace difference in ECPG expected
> output files. Fixed in the attached patchset.
>
> --
> Best Wishes,
> Ashutosh Bapat
>

---

From: Henson Choi <[email-masked]>
Date: Fri, 9 Jan 2026 15:53:29 +0900

Hi Ashutosh,

Thank you for reviewing the path pattern list implementation.
I've added comprehensive tests to address your concerns below.

2025년 12월 30일 (화) PM 6:50, Ashutosh Bapat <[email-masked]>님이
작성:

> Hi Henson,
>
> > 3. Multiple path patterns
> >    - Syntax: MATCH (a)->(b), (b)->(c)
> >    - Implements SQL/PGQ standard feature (was TODO)
> >
>
> This path pattern list feature is discussed in section 10.4 of the
> standard. I think supporting this feature would be a good addition to
> SQL/PGQ compliance. The current patchset is already large. I (and
> AFAIK Peter as well) am focusing on getting that patchset in a
> committable shape. Once committed, it will facilitate adding features
> like path pattern list, quantified path patterns, label conjunction
> etc. as incremental relatively smaller commits.
>

Agreed. Now is not the time to add this feature - the current patchset
should be stabilized first.

> Since you could build one of the complex features without changing a
> lot of earlier implementation, it validates that the basic
> implementation is extensible and yet robust. Thanks for that
> validation.
>

Thank you. That was indeed the intent.

> The way you have implemented it, it can lead to a path pattern with
> two vertex patterns next to each other without an interleaving edge
> pattern. That would throw an error or assertion.

This does NOT throw an error. Single vertex patterns produce a
Cartesian product correctly (vl1 has 3 rows, vl3 has 6 rows = 18 rows):

    SELECT a_name, b_name FROM GRAPH_TABLE (g1
        MATCH (a IS vl1), (b IS vl3)
        COLUMNS (a.vname AS a_name, b.vname AS b_name)
    ) ORDER BY 1, 2;

     a_name | b_name
    --------+--------
     v11    | v21
     v11    | v22
     ...
    (18 rows)  -- 3 x 6 = 18, confirming cross join

> If you try path
> patterns without a common vertex, you will get that error.

Disconnected patterns (no shared variables) work correctly and produce
a Cartesian product (vl1->vl2 has 3 paths, vl1->vl3 has 5 paths = 15 rows):

    SELECT a_name, b_name, c_name, d_name FROM GRAPH_TABLE (g1
        MATCH (a IS vl1)-[]->(b IS vl2),
              (c IS vl1)-[]->(d IS vl3)
        COLUMNS (a.vname AS a_name, b.vname AS b_name,
                 c.vname AS c_name, d.vname AS d_name)
    ) ORDER BY 1, 2, 3, 4;

     a_name | b_name | c_name | d_name
    --------+--------+--------+--------
     v11    | v22    | v11    | v22
     v11    | v22    | v11    | v31
     v11    | v22    | v11    | v33
     ...
    (15 rows)  -- 3 x 5 = 15, confirming cross join

> That is not
> covered by your tests.

I have now added comprehensive tests:

Disconnected patterns (cross product):
  1. Two independent paths - 3 x 5 = 15 rows
  2. EXPLAIN verification - confirms Nested Loop (cross join)
  3. Single vertex patterns - 3 x 6 = 18 rows
  4. Three-way cross product - 3 x 3 x 6 = 54 rows
  5. Mixed connected + disconnected - 1 path x 3 vertices = 3 rows
  6. Filtered cross product - 1 x 6 = 6 rows

Connected patterns (join via shared variable):
  7. Star pattern: A->B, A->C, D->A with shared hub 'a' - 2 rows (NOT cross
product)

    MATCH (a IS vl1)-[]->(b IS vl2),
          (a)-[]->(c IS vl3),
          (d IS vl2)-[]->(a)

     a_name | b_name | c_name | d_name
    --------+--------+--------+--------
     v12    | v21    | v21    | v21
     v13    | v23    | v23    | v23
    (2 rows)  -- all joined via 'a', NOT cross product

All row counts match expected behavior.

> In such cases, the paths should end up being
> full-joined rather than concatenated. I don't see it being implemented
> that way.

The implementation DOES produce cross joins for disconnected patterns.
The EXPLAIN output confirms this:

    EXPLAIN (COSTS OFF) SELECT ... FROM GRAPH_TABLE (g1
        MATCH (a IS vl1)-[]->(b IS vl2),
              (c IS vl1)-[]->(d IS vl3)
        ...
    );
                                  QUERY PLAN
    -----------------------------------------------
     Append
       ->  Merge Join
             ...
             ->  Nested Loop           <-- Cross join here
                   ->  Index Scan ...
                   ->  Materialize
                         ->  Nested Loop
                               ...

The Nested Loop without join condition IS the cross join (Cartesian
product).

> I think more work is needed here.
>

All test cases pass. This work was intended to validate extensibility
of the current design, not to push for immediate inclusion at this stage.

> [1]
> https://www.postgresql.org/message-id/[email-masked]
>
> --
> Best Wishes,
> Ashutosh Bapat
>

Best regards,
Henson
From 5d74b7157270f056556d88e700396d1cbd82bbb1 Mon Sep 17 00:00:00 2001
From: Henson Choi <[email-masked]>
Date: Fri, 26 Dec 2025 20:17:29 +0900
Subject: [PATCH] SQL/PGQ: Support multi-pattern path matching in GRAPH_TABLE

Add support for comma-separated path patterns in MATCH clause, e.g.:
  MATCH (a)->(b), (b)->(c)

Path patterns with shared variables are merged into a single join.
Disconnected patterns (no shared variables) produce a Cartesian product,
consistent with SQL/PGQ standard and Neo4j Cypher semantics.

The parser already supported this syntax; this commit enables execution
by merging multiple path patterns instead of rejecting them.
---
 src/backend/rewrite/rewriteGraphTable.c   |  34 +-
 src/test/regress/expected/graph_table.out | 375 +++++++++++++++++++++-
 src/test/regress/sql/graph_table.sql      | 138 +++++++-
 3 files changed, 539 insertions(+), 8 deletions(-)

diff --git a/src/backend/rewrite/rewriteGraphTable.c b/src/backend/rewrite/rewriteGraphTable.c
index 44bdc8a1f20..1420786fbb4 100644
--- a/src/backend/rewrite/rewriteGraphTable.c
+++ b/src/backend/rewrite/rewriteGraphTable.c
@@ -121,16 +121,40 @@ rewriteGraphTable(Query *parsetree, int rt_index)
 {
 	RangeTblEntry *rte;
 	Query	   *graph_table_query;
-	List	   *path_pattern;
 	List	   *pathqueries = NIL;
+	int			num_patterns;
 
 	rte = rt_fetch(rt_index, parsetree->rtable);
+	num_patterns = list_length(rte->graph_pattern->path_pattern_list);
 
-	if (list_length(rte->graph_pattern->path_pattern_list) != 1)
-		elog(ERROR, "unsupported path pattern list length");
+	if (num_patterns == 1)
+	{
+		/* Single path pattern - original behavior */
+		List	   *path_pattern = linitial(rte->graph_pattern->path_pattern_list);
+
+		pathqueries = generate_queries_for_path_pattern(rte, path_pattern);
+	}
+	else
+	{
+		/*
+		 * Multiple path patterns - merge all element patterns into a single
+		 * combined path pattern. Variables with the same name will be merged
+		 * by generate_queries_for_path_pattern().
+		 */
+		List	   *combined_pattern = NIL;
+		ListCell   *lc;
+
+		foreach(lc, rte->graph_pattern->path_pattern_list)
+		{
+			List	   *path_pattern = (List *) lfirst(lc);
+
+			combined_pattern = list_concat(combined_pattern,
+										   list_copy(path_pattern));
+		}
+
+		pathqueries = generate_queries_for_path_pattern(rte, combined_pattern);
+	}
 
-	path_pattern = linitial(rte->graph_pattern->path_pattern_list);
-	pathqueries = generate_queries_for_path_pattern(rte, path_pattern);
 	graph_table_query = generate_union_from_pathqueries(&pathqueries);
 
 	AcquireRewriteLocks(graph_table_query, true, false);
diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out
index f2bb3512a2d..812c1ed836f 100644
--- a/src/test/regress/expected/graph_table.out
+++ b/src/test/regress/expected/graph_table.out
@@ -93,8 +93,11 @@ SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.addr
 ERROR:  syntax error at or near "COLUMNS"
 LINE 1: ...mers WHERE c.address = 'US')-[IS customer_orders] COLUMNS (c...
                                                              ^
-SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers), (o IS orders) COLUMNS (c.name AS customer_name));  -- error
-ERROR:  unsupported path pattern list length
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers), (o IS orders) COLUMNS (c.name AS customer_name));  -- disconnected patterns: cross product with empty orders table
+ customer_name 
+---------------
+(0 rows)
+
 SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers)->{1,2}(o IS orders) COLUMNS (c.name AS customer_name));  -- error
 ERROR:  element pattern quantifier not supported yet
 -- a property graph can be referenced only from within GRAPH_TABLE clause.
@@ -1407,4 +1410,372 @@ SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_NAMES
 ERROR:  PROPERTY_NAMES() argument must be an element variable
 LINE 1: ...APH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_N...
                                                              ^
+-- Multi-pattern path tests (comma-separated path patterns with shared variables)
+-- Basic multi-pattern: (a)->(b), (b)->(c) where b is shared
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[e1 IS el1]->(b IS vl2),
+          (b)-[e2 IS el2]->(c IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY a_name, b_name, c_name;
+ a_name | b_name | c_name 
+--------+--------+--------
+ v11    | v22    | v32
+(1 row)
+
+-- Multi-pattern with same start vertex reaching different destinations
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2),
+          (a)-[]->(c IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY a_name, b_name, c_name;
+ a_name | b_name | c_name 
+--------+--------+--------
+ v11    | v22    | v22
+ v11    | v22    | v31
+ v11    | v22    | v33
+ v12    | v21    | v21
+ v13    | v23    | v23
+(5 rows)
+
+-- Multi-pattern with three patterns sharing one variable
+SELECT a_name, b2_name, b3_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b2 IS vl2),
+          (a)-[]->(b3 IS vl3)
+    COLUMNS (a.vname AS a_name, b2.vname AS b2_name, b3.vname AS b3_name)
+) ORDER BY a_name, b2_name, b3_name;
+ a_name | b2_name | b3_name 
+--------+---------+---------
+ v11    | v22     | v22
+ v11    | v22     | v31
+ v11    | v22     | v33
+ v12    | v21     | v21
+ v13    | v23     | v23
+(5 rows)
+
+-- Same variable with same label (should work)
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2),
+          (b IS vl2)-[]->(c IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+ a_name | b_name | c_name 
+--------+--------+--------
+ v11    | v22    | v32
+(1 row)
+
+-- Same variable with different labels (conflict - what happens?)
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2),
+          (b IS vl3)-[]->(c IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+ERROR:  element patterns with same variable name "b" but different label expressions are not supported
+-- Same variable with label OR expression (b IS vl2|vl3)
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+          (b)-[]->(c IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+ a_name | b_name | c_name 
+--------+--------+--------
+ v11    | v22    | v32
+ v11    | v33    | v33
+ v11    | v33    | v33
+(3 rows)
+
+-- Multi-pattern with LABELS() filtering in WHERE clause
+-- Filter shared variable b by label using LABELS() function
+EXPLAIN (COSTS OFF) SELECT a_name, b_name, b_labels, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+          (b)-[]->(c IS vl3)
+    WHERE 'vl2' = ANY(LABELS(b))
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, LABELS(b) AS b_labels, c.vname AS c_name)
+);
+                                        QUERY PLAN                                         
+-------------------------------------------------------------------------------------------
+ Nested Loop
+   ->  Nested Loop
+         ->  Hash Join
+               Hash Cond: ((e2_3.id_2_1 = v2.id1) AND (e2_3.id_2_2 = v2.id2))
+               ->  Seq Scan on e2_3
+               ->  Hash
+                     ->  Merge Join
+                           Merge Cond: ((e1_2.id_2_1 = v2.id1) AND (e1_2.id_2_2 = v2.id2))
+                           ->  Sort
+                                 Sort Key: e1_2.id_2_1, e1_2.id_2_2
+                                 ->  Seq Scan on e1_2
+                           ->  Sort
+                                 Sort Key: v2.id1, v2.id2
+                                 ->  Seq Scan on v2
+         ->  Index Scan using v1_pkey on v1
+               Index Cond: (id = e1_2.id_1)
+   ->  Index Scan using v3_pkey on v3
+         Index Cond: (id = e2_3.id_3)
+(18 rows)
+
+SELECT a_name, b_name, b_labels, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+          (b)-[]->(c IS vl3)
+    WHERE 'vl2' = ANY(LABELS(b))
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, LABELS(b) AS b_labels, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+ a_name | b_name |   b_labels   | c_name 
+--------+--------+--------------+--------
+ v11    | v22    | {l1,vl2,vl3} | v32
+(1 row)
+
+-- Multi-pattern with LABELS() filtering - no common labels (should prune all)
+-- b IS vl2|vl3 but WHERE filters by 'vl1' which has no overlap
+EXPLAIN (COSTS OFF) SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+          (b)-[]->(c IS vl3)
+    WHERE 'vl1' = ANY(LABELS(b))
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+);
+           QUERY PLAN            
+---------------------------------
+ Result
+   Replaces: Scan on graph_table
+   One-Time Filter: false
+(3 rows)
+
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+          (b)-[]->(c IS vl3)
+    WHERE 'vl1' = ANY(LABELS(b))
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+ a_name | b_name | c_name 
+--------+--------+--------
+(0 rows)
+
+-- Disconnected patterns (no shared variables) - should produce cross product
+-- (a)->(b) and (c)->(d) are independent, result is Cartesian product
+-- vl1->vl2 has 3 paths, vl1->vl3 has 3 paths, so cross product = 3 x 3 = 9 rows
+SELECT a_name, b_name, c_name, d_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2),
+          (c IS vl1)-[]->(d IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name,
+             c.vname AS c_name, d.vname AS d_name)
+) ORDER BY 1, 2, 3, 4;
+ a_name | b_name | c_name | d_name 
+--------+--------+--------+--------
+ v11    | v22    | v11    | v22
+ v11    | v22    | v11    | v31
+ v11    | v22    | v11    | v33
+ v11    | v22    | v12    | v21
+ v11    | v22    | v13    | v23
+ v12    | v21    | v11    | v22
+ v12    | v21    | v11    | v31
+ v12    | v21    | v11    | v33
+ v12    | v21    | v12    | v21
+ v12    | v21    | v13    | v23
+ v13    | v23    | v11    | v22
+ v13    | v23    | v11    | v31
+ v13    | v23    | v11    | v33
+ v13    | v23    | v12    | v21
+ v13    | v23    | v13    | v23
+(15 rows)
+
+-- Disconnected patterns: EXPLAIN should show cross join (Nested Loop without join condition)
+EXPLAIN (COSTS OFF) SELECT a_name, b_name, c_name, d_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2),
+          (c IS vl1)-[]->(d IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name,
+             c.vname AS c_name, d.vname AS d_name)
+);
+                                                  QUERY PLAN                                                   
+---------------------------------------------------------------------------------------------------------------
+ Append
+   ->  Merge Join
+         Merge Cond: (v1.id = e1_2.id_1)
+         ->  Nested Loop
+               ->  Index Scan using v1_pkey on v1
+               ->  Materialize
+                     ->  Nested Loop
+                           ->  Merge Join
+                                 Merge Cond: ((e1_2_1.id_2_1 = v2_1.id1) AND (e1_2_1.id_2_2 = v2_1.id2))
+                                 ->  Sort
+                                       Sort Key: e1_2_1.id_2_1, e1_2_1.id_2_2
+                                       ->  Seq Scan on e1_2 e1_2_1
+                                 ->  Sort
+                                       Sort Key: v2_1.id1, v2_1.id2
+                                       ->  Seq Scan on v2 v2_1
+                           ->  Index Scan using v1_pkey on v1 v1_1
+                                 Index Cond: (id = e1_2_1.id_1)
+         ->  Sort
+               Sort Key: e1_2.id_1
+               ->  Merge Join
+                     Merge Cond: ((e1_2.id_2_1 = v2.id1) AND (e1_2.id_2_2 = v2.id2))
+                     ->  Sort
+                           Sort Key: e1_2.id_2_1, e1_2.id_2_2
+                           ->  Seq Scan on e1_2
+                     ->  Sort
+                           Sort Key: v2.id1, v2.id2
+                           ->  Seq Scan on v2
+   ->  Hash Join
+         Hash Cond: (e1_3.id_3 = v3.id)
+         ->  Hash Join
+               Hash Cond: (e1_3.id_1 = v1_3.id)
+               ->  Nested Loop
+                     ->  Seq Scan on e1_3
+                     ->  Materialize
+                           ->  Nested Loop
+                                 ->  Merge Join
+                                       Merge Cond: ((e1_2_2.id_2_1 = v2_2.id1) AND (e1_2_2.id_2_2 = v2_2.id2))
+                                       ->  Sort
+                                             Sort Key: e1_2_2.id_2_1, e1_2_2.id_2_2
+                                             ->  Seq Scan on e1_2 e1_2_2
+                                       ->  Sort
+                                             Sort Key: v2_2.id1, v2_2.id2
+                                             ->  Seq Scan on v2 v2_2
+                                 ->  Index Scan using v1_pkey on v1 v1_2
+                                       Index Cond: (id = e1_2_2.id_1)
+               ->  Hash
+                     ->  Seq Scan on v1 v1_3
+         ->  Hash
+               ->  Seq Scan on v3
+(49 rows)
+
+-- Disconnected patterns: single vertex patterns (no edges)
+-- vl1 has 3 rows (v1), vl3 has 6 rows (v2+v3), cross product = 18 rows
+SELECT a_name, b_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1), (b IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name)
+) ORDER BY 1, 2;
+ a_name | b_name 
+--------+--------
+ v11    | v21
+ v11    | v22
+ v11    | v23
+ v11    | v31
+ v11    | v32
+ v11    | v33
+ v12    | v21
+ v12    | v22
+ v12    | v23
+ v12    | v31
+ v12    | v32
+ v12    | v33
+ v13    | v21
+ v13    | v22
+ v13    | v23
+ v13    | v31
+ v13    | v32
+ v13    | v33
+(18 rows)
+
+-- Disconnected patterns: three independent patterns (3-way cross product)
+-- vl1 has 3 rows, vl2 has 3 rows, vl3 has 6 rows = 54 rows
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1), (b IS vl2), (c IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+ a_name | b_name | c_name 
+--------+--------+--------
+ v11    | v21    | v21
+ v11    | v21    | v22
+ v11    | v21    | v23
+ v11    | v21    | v31
+ v11    | v21    | v32
+ v11    | v21    | v33
+ v11    | v22    | v21
+ v11    | v22    | v22
+ v11    | v22    | v23
+ v11    | v22    | v31
+ v11    | v22    | v32
+ v11    | v22    | v33
+ v11    | v23    | v21
+ v11    | v23    | v22
+ v11    | v23    | v23
+ v11    | v23    | v31
+ v11    | v23    | v32
+ v11    | v23    | v33
+ v12    | v21    | v21
+ v12    | v21    | v22
+ v12    | v21    | v23
+ v12    | v21    | v31
+ v12    | v21    | v32
+ v12    | v21    | v33
+ v12    | v22    | v21
+ v12    | v22    | v22
+ v12    | v22    | v23
+ v12    | v22    | v31
+ v12    | v22    | v32
+ v12    | v22    | v33
+ v12    | v23    | v21
+ v12    | v23    | v22
+ v12    | v23    | v23
+ v12    | v23    | v31
+ v12    | v23    | v32
+ v12    | v23    | v33
+ v13    | v21    | v21
+ v13    | v21    | v22
+ v13    | v21    | v23
+ v13    | v21    | v31
+ v13    | v21    | v32
+ v13    | v21    | v33
+ v13    | v22    | v21
+ v13    | v22    | v22
+ v13    | v22    | v23
+ v13    | v22    | v31
+ v13    | v22    | v32
+ v13    | v22    | v33
+ v13    | v23    | v21
+ v13    | v23    | v22
+ v13    | v23    | v23
+ v13    | v23    | v31
+ v13    | v23    | v32
+ v13    | v23    | v33
+(54 rows)
+
+-- Mixed: partially connected + disconnected patterns
+-- (a)->(b), (b)->(c) are connected via 'b', but (d) is disconnected
+-- vl1->vl2->vl3 chain + independent vl1 vertex
+SELECT a_name, b_name, c_name, d_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2),
+          (b)-[]->(c IS vl3),
+          (d IS vl1)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name,
+             c.vname AS c_name, d.vname AS d_name)
+) ORDER BY 1, 2, 3, 4;
+ a_name | b_name | c_name | d_name 
+--------+--------+--------+--------
+ v11    | v22    | v32    | v11
+ v11    | v22    | v32    | v12
+ v11    | v22    | v32    | v13
+(3 rows)
+
+-- Disconnected patterns: verify row count with different sizes
+-- Using WHERE to filter one side: (a IS vl1 WHERE vprop1 = 10) has 1 row
+-- Cross with (b IS vl3) which has 6 rows = 6 rows
+SELECT a_name, b_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1 WHERE a.vprop1 = 10), (b IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name)
+) ORDER BY 1, 2;
+ a_name | b_name 
+--------+--------
+ v11    | v21
+ v11    | v22
+ v11    | v23
+ v11    | v31
+ v11    | v32
+ v11    | v33
+(6 rows)
+
+-- Star pattern: three patterns with shared hub 'a' (A->B, A->C, D->A)
+-- All joined via 'a', NOT a cross product
+SELECT a_name, b_name, c_name, d_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2),
+          (a)-[]->(c IS vl3),
+          (d IS vl2)-[]->(a)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name,
+             c.vname AS c_name, d.vname AS d_name)
+) ORDER BY 1, 2, 3, 4;
+ a_name | b_name | c_name | d_name 
+--------+--------+--------+--------
+ v12    | v21    | v21    | v21
+ v13    | v23    | v23    | v23
+(2 rows)
+
 -- leave the objects behind for pg_upgrade/pg_dump tests
diff --git a/src/test/regress/sql/graph_table.sql b/src/test/regress/sql/graph_table.sql
index e8658348b5d..08c1ff685b9 100644
--- a/src/test/regress/sql/graph_table.sql
+++ b/src/test/regress/sql/graph_table.sql
@@ -89,7 +89,7 @@ SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.addr
 SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.namex AS customer_name));  -- error
 SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers|employees WHERE c.address = 'US')-[IS customer_orders]->(o IS orders) COLUMNS (c.name AS customer_name));  -- error
 SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers WHERE c.address = 'US')-[IS customer_orders] COLUMNS (c.name AS customer_name));  -- error
-SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers), (o IS orders) COLUMNS (c.name AS customer_name));  -- error
+SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers), (o IS orders) COLUMNS (c.name AS customer_name));  -- disconnected patterns: cross product with empty orders table
 SELECT customer_name FROM GRAPH_TABLE (myshop MATCH (c IS customers)->{1,2}(o IS orders) COLUMNS (c.name AS customer_name));  -- error
 
 -- a property graph can be referenced only from within GRAPH_TABLE clause.
@@ -780,4 +780,140 @@ SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_NAMES
 -- PROPERTY_NAMES() error: non-variable argument
 SELECT * FROM GRAPH_TABLE (myshop MATCH (c IS customers) COLUMNS (PROPERTY_NAMES(123)));
 
+-- Multi-pattern path tests (comma-separated path patterns with shared variables)
+-- Basic multi-pattern: (a)->(b), (b)->(c) where b is shared
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[e1 IS el1]->(b IS vl2),
+          (b)-[e2 IS el2]->(c IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY a_name, b_name, c_name;
+
+-- Multi-pattern with same start vertex reaching different destinations
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2),
+          (a)-[]->(c IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY a_name, b_name, c_name;
+
+-- Multi-pattern with three patterns sharing one variable
+SELECT a_name, b2_name, b3_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b2 IS vl2),
+          (a)-[]->(b3 IS vl3)
+    COLUMNS (a.vname AS a_name, b2.vname AS b2_name, b3.vname AS b3_name)
+) ORDER BY a_name, b2_name, b3_name;
+
+-- Same variable with same label (should work)
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2),
+          (b IS vl2)-[]->(c IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+
+-- Same variable with different labels (conflict - what happens?)
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2),
+          (b IS vl3)-[]->(c IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+
+-- Same variable with label OR expression (b IS vl2|vl3)
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+          (b)-[]->(c IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+
+-- Multi-pattern with LABELS() filtering in WHERE clause
+-- Filter shared variable b by label using LABELS() function
+EXPLAIN (COSTS OFF) SELECT a_name, b_name, b_labels, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+          (b)-[]->(c IS vl3)
+    WHERE 'vl2' = ANY(LABELS(b))
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, LABELS(b) AS b_labels, c.vname AS c_name)
+);
+
+SELECT a_name, b_name, b_labels, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+          (b)-[]->(c IS vl3)
+    WHERE 'vl2' = ANY(LABELS(b))
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, LABELS(b) AS b_labels, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+
+-- Multi-pattern with LABELS() filtering - no common labels (should prune all)
+-- b IS vl2|vl3 but WHERE filters by 'vl1' which has no overlap
+EXPLAIN (COSTS OFF) SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+          (b)-[]->(c IS vl3)
+    WHERE 'vl1' = ANY(LABELS(b))
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+);
+
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2|vl3),
+          (b)-[]->(c IS vl3)
+    WHERE 'vl1' = ANY(LABELS(b))
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+
+-- Disconnected patterns (no shared variables) - should produce cross product
+-- (a)->(b) and (c)->(d) are independent, result is Cartesian product
+-- vl1->vl2 has 3 paths, vl1->vl3 has 3 paths, so cross product = 3 x 3 = 9 rows
+SELECT a_name, b_name, c_name, d_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2),
+          (c IS vl1)-[]->(d IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name,
+             c.vname AS c_name, d.vname AS d_name)
+) ORDER BY 1, 2, 3, 4;
+
+-- Disconnected patterns: EXPLAIN should show cross join (Nested Loop without join condition)
+EXPLAIN (COSTS OFF) SELECT a_name, b_name, c_name, d_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2),
+          (c IS vl1)-[]->(d IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name,
+             c.vname AS c_name, d.vname AS d_name)
+);
+
+-- Disconnected patterns: single vertex patterns (no edges)
+-- vl1 has 3 rows (v1), vl3 has 6 rows (v2+v3), cross product = 18 rows
+SELECT a_name, b_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1), (b IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name)
+) ORDER BY 1, 2;
+
+-- Disconnected patterns: three independent patterns (3-way cross product)
+-- vl1 has 3 rows, vl2 has 3 rows, vl3 has 6 rows = 54 rows
+SELECT a_name, b_name, c_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1), (b IS vl2), (c IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name, c.vname AS c_name)
+) ORDER BY 1, 2, 3;
+
+-- Mixed: partially connected + disconnected patterns
+-- (a)->(b), (b)->(c) are connected via 'b', but (d) is disconnected
+-- vl1->vl2->vl3 chain + independent vl1 vertex
+SELECT a_name, b_name, c_name, d_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2),
+          (b)-[]->(c IS vl3),
+          (d IS vl1)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name,
+             c.vname AS c_name, d.vname AS d_name)
+) ORDER BY 1, 2, 3, 4;
+
+-- Disconnected patterns: verify row count with different sizes
+-- Using WHERE to filter one side: (a IS vl1 WHERE vprop1 = 10) has 1 row
+-- Cross with (b IS vl3) which has 6 rows = 6 rows
+SELECT a_name, b_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1 WHERE a.vprop1 = 10), (b IS vl3)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name)
+) ORDER BY 1, 2;
+
+-- Star pattern: three patterns with shared hub 'a' (A->B, A->C, D->A)
+-- All joined via 'a', NOT a cross product
+SELECT a_name, b_name, c_name, d_name FROM GRAPH_TABLE (g1
+    MATCH (a IS vl1)-[]->(b IS vl2),
+          (a)-[]->(c IS vl3),
+          (d IS vl2)-[]->(a)
+    COLUMNS (a.vname AS a_name, b.vname AS b_name,
+             c.vname AS c_name, d.vname AS d_name)
+) ORDER BY 1, 2, 3, 4;
+
 -- leave the objects behind for pg_upgrade/pg_dump tests
-- 
2.50.1 (Apple Git-155)

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 13 Jan 2026 11:12:08 +0100

> not about supporting : . Do you remember what those are?

These are required to support label disjunctions.  If you add one of 
those to the ecpg sqlpgq tests you added, like (c IS customer|customer), 
then it will fail to compile without this change.

The psql change has perhaps no such impact, but in general, these are 
supposed to be kept in sync with the main scan.l.

>     A property graph is a way to represent database contents, instead of using

Maybe it's clearer like this:

"A property graph is a way to represent database contents, as an 
alternative to the usual (in SQL) approach of representing database 
contents using relational structures such as tables."

> 0002 has extra tests mentioned above. It also removes "TODO: dubious
> error message" from a comment. I don't see anything dubious in the
> error message. I think this patch is safe to be merged into 0001.

Hmm, yeah, this might have been a leftover from a different catalog 
structure that resulted in different detail messages from the dependency 
infrastructure.  I can't see anything wrong with the current output either.