Repository: tobymao/sqlglot

Issue:
Support Postgres partial indexes
Postgres supports partial indexes and an error occurs when parsing such expressions

**Fully reproducible code snippet**
```python
    sql = "CREATE INDEX idx_x ON x USING btree (x, y) WHERE (y IS NOT NULL);"
    parsed_sql_exprs = sqlglot.parse_one(sql, read="postgres")
    
    # Invalid expression / Unexpected token. Line 1, Col: 48.
    #   CREATE INDEX idx_x ON x USING btree (x, y) [4mWHERE[0m (y IS NOT NULL);
```

A more complex example of indexes:
```sql
CREATE INDEX index_ci_builds_on_commit_id_and_artifacts_expireatandidpartial 
    ON public.ci_builds 
    USING btree (commit_id, artifacts_expire_at, id) 
    WHERE (
        ((type)::text = 'Ci::Build'::text) 
        AND ((retried = false) OR (retried IS NULL)) 
        AND ((name)::text = ANY (ARRAY[
            ('sast'::character varying)::text, 
            ('dependency_scanning'::character varying)::text, 
            ('sast:container'::character varying)::text, 
            ('container_scanning'::character varying)::text, 
            ('dast'::character varying)::text
        ]))
    );
```

**Official Documentation**
https://www.postgresql.org/docs/current/indexes-partial.html


Discussion:
I'll take a look

Resolving patch:
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -1607,6 +1607,7 @@ class Index(Expression):
         "primary": False,
         "amp": False,  # teradata
         "partition_by": False,  # teradata
+        "where": False,  # postgres partial indexes
     }
 
 
diff --git a/sqlglot/generator.py b/sqlglot/generator.py
--- a/sqlglot/generator.py
+++ b/sqlglot/generator.py
@@ -992,13 +992,14 @@ def index_sql(self, expression: exp.Index) -> str:
         table = self.sql(expression, "table")
         table = f"{self.INDEX_ON} {table}" if table else ""
         using = self.sql(expression, "using")
-        using = f" USING {using} " if using else ""
+        using = f" USING {using}" if using else ""
         index = "INDEX " if not table else ""
         columns = self.expressions(expression, key="columns", flat=True)
         columns = f"({columns})" if columns else ""
         partition_by = self.expressions(expression, key="partition_by", flat=True)
         partition_by = f" PARTITION BY {partition_by}" if partition_by else ""
-        return f"{unique}{primary}{amp}{index}{name}{table}{using}{columns}{partition_by}"
+        where = self.sql(expression, "where")
+        return f"{unique}{primary}{amp}{index}{name}{table}{using}{columns}{partition_by}{where}"
 
     def identifier_sql(self, expression: exp.Identifier) -> str:
         text = expression.name
diff --git a/sqlglot/parser.py b/sqlglot/parser.py
--- a/sqlglot/parser.py
+++ b/sqlglot/parser.py
@@ -2480,6 +2480,7 @@ def _parse_index(
             primary=primary,
             amp=amp,
             partition_by=self._parse_partition_by(),
+            where=self._parse_where(),
         )
 
     def _parse_table_hints(self) -> t.Optional[t.List[exp.Expression]]:


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
@@ -13,6 +13,7 @@ def test_ddl(self):
             "CREATE TABLE test (x TIMESTAMP WITHOUT TIME ZONE[][])",
             "CREATE TABLE test (x TIMESTAMP[][])",
         )
+        self.validate_identity("CREATE INDEX idx_x ON x USING BTREE(x, y) WHERE (NOT y IS NULL)")
         self.validate_identity("CREATE TABLE test (elems JSONB[])")
         self.validate_identity("CREATE TABLE public.y (x TSTZRANGE NOT NULL)")
         self.validate_identity("CREATE TABLE test (foo HSTORE)")
@@ -83,6 +84,25 @@ def test_ddl(self):
                 " CONSTRAINT valid_discount CHECK (price > discounted_price))"
             },
         )
+        self.validate_identity(
+            """
+            CREATE INDEX index_ci_builds_on_commit_id_and_artifacts_expireatandidpartial
+            ON public.ci_builds
+            USING btree (commit_id, artifacts_expire_at, id)
+            WHERE (
+                ((type)::text = 'Ci::Build'::text)
+                AND ((retried = false) OR (retried IS NULL))
+                AND ((name)::text = ANY (ARRAY[
+                    ('sast'::character varying)::text,
+                    ('dependency_scanning'::character varying)::text,
+                    ('sast:container'::character varying)::text,
+                    ('container_scanning'::character varying)::text,
+                    ('dast'::character varying)::text
+                ]))
+            )
+            """,
+            "CREATE INDEX index_ci_builds_on_commit_id_and_artifacts_expireatandidpartial ON public.ci_builds USING BTREE(commit_id, artifacts_expire_at, id) WHERE ((CAST((type) AS TEXT) = CAST('Ci::Build' AS TEXT)) AND ((retried = FALSE) OR (retried IS NULL)) AND (CAST((name) AS TEXT) = ANY (ARRAY[CAST((CAST('sast' AS VARCHAR)) AS TEXT), CAST((CAST('dependency_scanning' AS VARCHAR)) AS TEXT), CAST((CAST('sast:container' AS VARCHAR)) AS TEXT), CAST((CAST('container_scanning' AS VARCHAR)) AS TEXT), CAST((CAST('dast' AS VARCHAR)) AS TEXT)])))",
+        )
 
         with self.assertRaises(ParseError):
             transpile("CREATE TABLE products (price DECIMAL CHECK price > 0)", read="postgres")