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:
@Nitrino the second `CREATE INDEX` statement looks legit, i.e. it matches what I see in Postgres' syntax spec. Can you point me to examples on the docs page for the other two? I don't see the `USING <method> (key value)` syntax like you showed in the first one:

```
CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] name ] ON [ ONLY ] table_name [ USING method ]
    ( { column_name | ( expression ) } [ COLLATE collation ] [ opclass [ ( opclass_parameter = value [, ... ] ) ] ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [, ...] )
    [ INCLUDE ( column_name [, ...] ) ]
    [ NULLS [ NOT ] DISTINCT ]
    [ WITH ( storage_parameter [= value] [, ... ] ) ]
    [ TABLESPACE tablespace_name ]
    [ WHERE predicate ]
```
@GeorgeSittas I can't find a suitable example in the documentation, but my examples above are taken from a real Gitlab database schema from the Prisma repository
https://github.com/prisma/database-schema-examples/blob/8cea37d834f3effd6aad53e98c5bc865b4d85335/postgres/gitlab/schema.sql#L10740
What are the semantics of `USING btree (path varchar_pattern_ops)`? I can't understand based on the docs
@GeorgeSittas Hi, sorry for the long reply. 
I'm not sure I fully understood your question. 

The documentation on index creation does not describe the semantics of operator class in much detail:

```sql
CREATE [ UNIQUE ] INDEX ... [ opclass [ ( opclass_parameter = value [, ... ] ) ] ] ...
```

But there is a clarification in [Section 11.10](https://www.postgresql.org/docs/current/indexes-opclass.html).

> An index definition can specify an operator class for each column of an index.

```sql
CREATE INDEX name ON table (column opclass [ ( opclass_options ) ] [sort options] [, ...]);
```

>  The main reason for having operator classes is that for some data types, there could be more than one meaningful index behavior.

Thus an operator class can be specified for any index field. 
And these classes can be different for different types of indexes (e.g. `USING btree`)
I see, I misread the doc page for opclasses I think, I'll try to completely fix this in a followup PR.

Resolving patch:
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -1613,6 +1613,11 @@ def output_name(self) -> str:
         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
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -2851,6 +2851,9 @@ def comprehension_sql(self, expression: exp.Comprehension) -> str:
     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
--- 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 @@ def _parse_join(
         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 @@ def _parse_index(
         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 @@ def _parse_sort(self, exp_class: t.Type[E], token: TokenType) -> t.Optional[E]:
             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
--- 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[][])",