Repository: tobymao/sqlglot

Issue:
Support postgres Operator Classes and Operator Families
An index definition can specify an operator class for each column of an index. 
Right now, the library does not support such cases

**Fully reproducible code snippet**
```python
    sql = """
        CREATE INDEX index_routes_on_path_text_pattern_ops ON public.routes USING btree (path varchar_pattern_ops);
        CREATE INDEX index_ci_pipelines_on_project_idandrefandiddesc ON public.ci_pipelines USING btree (project_id, ref, id DESC);
        CREATE INDEX index_issues_on_title_trigram ON public.issues USING gin (title public.gin_trgm_ops);
    """
    parsed_sql_exprs = sqlglot.parse(sql, read="postgres")
```

**Official Documentation**
https://www.postgresql.org/docs/current/indexes-opclass.html

Discussion:

Patch:
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
index 467ef523..b137a668 100644
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -1613,6 +1613,11 @@ class Identifier(Expression):
         return self.name
 
 
+# https://www.postgresql.org/docs/current/indexes-opclass.html
+class Opclass(Expression):
+    arg_types = {"this": True, "expression": True}
+
+
 class Index(Expression):
     arg_types = {
         "this": False,
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
index f5654284..7a6bd8bd 100644
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -2851,6 +2851,9 @@ class Generator:
     def columnprefix_sql(self, expression: exp.ColumnPrefix) -> str:
         return f"{self.sql(expression, 'this')}({self.sql(expression, 'expression')})"
 
+    def opclass_sql(self, expression: exp.Opclass) -> str:
+        return f"{self.sql(expression, 'this')} {self.sql(expression, 'expression')}"
+
 
 def cached_generator(
     cache: t.Optional[t.Dict[int, str]] = None
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
index bde52f92..84f7e46d 100644
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -850,6 +850,8 @@ class Parser(metaclass=_Parser):
     CLONE_KEYWORDS = {"CLONE", "COPY"}
     CLONE_KINDS = {"TIMESTAMP", "OFFSET", "STATEMENT"}
 
+    OPCLASS_FOLLOW_KEYWORDS = {"ASC", "DESC", "NULLS"}
+
     TABLE_INDEX_HINT_TOKENS = {TokenType.FORCE, TokenType.IGNORE, TokenType.USE}
 
     WINDOW_ALIAS_TOKENS = ID_VAR_TOKENS - {TokenType.ROWS}
@@ -2463,6 +2465,17 @@ class Parser(metaclass=_Parser):
         comments = [c for token in (method, side, kind) if token for c in token.comments]
         return self.expression(exp.Join, comments=comments, **kwargs)
 
+    def _parse_opclass(self) -> t.Optional[exp.Expression]:
+        this = self._parse_conjunction()
+        if self._match_texts(self.OPCLASS_FOLLOW_KEYWORDS, advance=False):
+            return this
+
+        opclass = self._parse_var(any_token=True)
+        if opclass:
+            return self.expression(exp.Opclass, this=this, expression=opclass)
+
+        return this
+
     def _parse_index(
         self,
         index: t.Optional[exp.Expression] = None,
@@ -2489,7 +2502,7 @@ class Parser(metaclass=_Parser):
         using = self._parse_var(any_token=True) if self._match(TokenType.USING) else None
 
         if self._match(TokenType.L_PAREN, advance=False):
-            columns = self._parse_wrapped_csv(self._parse_ordered)
+            columns = self._parse_wrapped_csv(lambda: self._parse_ordered(self._parse_opclass))
         else:
             columns = None
 
@@ -2968,8 +2981,8 @@ class Parser(metaclass=_Parser):
             return None
         return self.expression(exp_class, expressions=self._parse_csv(self._parse_ordered))
 
-    def _parse_ordered(self) -> exp.Ordered:
-        this = self._parse_conjunction()
+    def _parse_ordered(self, parse_method: t.Optional[t.Callable] = None) -> exp.Ordered:
+        this = parse_method() if parse_method else self._parse_conjunction()
 
         asc = self._match(TokenType.ASC)
         desc = self._match(TokenType.DESC) or (asc and False)


Test patch:
diff --git a/tests/dialects/test_postgres.py b/tests/dialects/test_postgres.py
index 290c6af7..74a3f1c9 100644
--- a/tests/dialects/test_postgres.py
+++ b/tests/dialects/test_postgres.py
@@ -9,6 +9,9 @@ class TestPostgres(Validator):
     dialect = "postgres"
 
     def test_ddl(self):
+        self.validate_identity(
+            "CREATE INDEX foo ON bar.baz USING btree(col1 varchar_pattern_ops ASC, col2)"
+        )
         self.validate_identity(
             "CREATE TABLE test (x TIMESTAMP WITHOUT TIME ZONE[][])",
             "CREATE TABLE test (x TIMESTAMP[][])",