Repository: ibis-project/ibis

Issue:
feat: `Schema.from_sqlglot()`
### Is your feature request related to a problem?

## issue: `Schema.to_sqlglot()`
The semantics of `.to_sqlglot()` are a bit confusing. I expected the output to be a [sqlglot.schema.Schema](https://sqlglot.com/sqlglot/schema.html#MappingSchema) type with a single table. 

Maybe it could be renamed to `.to_sqlglot_columns_definition()` ?

edit: I see some of this logic in available in `ibis/expr/sql.py`

## add: `Schema.from_sqlglot()`
I'm working on [dlt-hub/dlt](https://github.com/dlt-hub/dlt) and we use Ibis and SQLGlot for some internal component.

Currently, we maintain type-mapping between `dlt <-> ibis` and `dlt <-> sqlglot`. Some operations need schema conversion from `ibis -> sqlglot` or `sqlglot -> ibis`.

I would be convenient if `Schema.from_sqlglot()` was supported (`Schema.to_sqlglot()` already existing). This would streamline conversions between tools.

### What is the motivation behind your request?

_No response_

### Describe the solution you'd like

```python
import sqlglot.schema

class Schema:
  # ...
  @classmethod 
  def from_sqlglot(cls, sqlglot_schema: sqlglot.schema.Schema) -> "Schema":
    from ibis.formats.sqlglot import SQLGlotSchema
    
    return SQLGlotSchema.to_ibis(sqlglot_schema)

  def to_sqlglot(self) -> sqlglot.schema.Schema:
    from ibis.formats.sqlglot import SQLGlotSchema

    return SQLGlotSchema.from_ibis(self)

  def to_sqlglot_columns_definition(self):
    # logic of the current `.to_sqlglot()`
```

### What version of ibis are you running?

10.5.0

### What backend(s) are you using, if any?

Used to support multiple backends

### Code of Conduct

- [x] I agree to follow this project's Code of Conduct


Discussion:
+1 on this change.
I'd like to work on adding `from_sqlglot`! 



Patch:
diff --git a/ibis/backends/athena/__init__.py b/ibis/backends/athena/__init__.py
index 9d820e5154b0..bc53d3292c47 100644
--- a/ibis/backends/athena/__init__.py
+++ b/ibis/backends/athena/__init__.py
@@ -156,7 +156,8 @@ def create_table(
 
         if schema is not None and obj is None:
             target = sge.Schema(
-                this=table_ref, expressions=schema.to_sqlglot(self.dialect)
+                this=table_ref,
+                expressions=schema.to_sqlglot_column_defs(self.dialect),
             )
         else:
             target = table_ref
@@ -179,7 +180,9 @@ def create_table(
             property_list.append(
                 sge.PartitionedByProperty(
                     this=sge.Schema(
-                        expressions=ibis.schema(partitioned_by).to_sqlglot(self.dialect)
+                        expressions=ibis.schema(partitioned_by).to_sqlglot_column_defs(
+                            self.dialect
+                        )
                     )
                 )
             )
@@ -409,7 +412,7 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None:
                     catalog=self.current_catalog,
                     quoted=quoted,
                 ),
-                expressions=schema.to_sqlglot(self.dialect),
+                expressions=schema.to_sqlglot_column_defs(self.dialect),
             ),
             properties=sge.Properties(
                 expressions=[
diff --git a/ibis/backends/bigquery/__init__.py b/ibis/backends/bigquery/__init__.py
index 44911a466849..df82daf193a5 100644
--- a/ibis/backends/bigquery/__init__.py
+++ b/ibis/backends/bigquery/__init__.py
@@ -1195,7 +1195,9 @@ def create_table(
             kind="TABLE",
             this=sge.Schema(
                 this=table,
-                expressions=schema.to_sqlglot(self.dialect) if schema else None,
+                expressions=schema.to_sqlglot_column_defs(self.dialect)
+                if schema
+                else None,
             ),
             replace=overwrite,
             properties=sge.Properties(expressions=properties),
diff --git a/ibis/backends/clickhouse/__init__.py b/ibis/backends/clickhouse/__init__.py
index 22c992b115cf..f73ab24f3b3c 100644
--- a/ibis/backends/clickhouse/__init__.py
+++ b/ibis/backends/clickhouse/__init__.py
@@ -678,7 +678,7 @@ def create_table(
 
         this = sge.Schema(
             this=sg.table(name, db=database, quoted=self.compiler.quoted),
-            expressions=(schema or obj.schema()).to_sqlglot(self.dialect),
+            expressions=(schema or obj.schema()).to_sqlglot_column_defs(self.dialect),
         )
         properties = [
             # the engine cannot be quoted, since clickhouse won't allow e.g.,
diff --git a/ibis/backends/databricks/__init__.py b/ibis/backends/databricks/__init__.py
index d98266158a07..1ce20dc91a90 100644
--- a/ibis/backends/databricks/__init__.py
+++ b/ibis/backends/databricks/__init__.py
@@ -212,7 +212,10 @@ def create_table(
         dialect = self.dialect
 
         initial_table = sg.table(temp_name, catalog=catalog, db=database, quoted=quoted)
-        target = sge.Schema(this=initial_table, expressions=schema.to_sqlglot(dialect))
+        target = sge.Schema(
+            this=initial_table,
+            expressions=schema.to_sqlglot_column_defs(dialect),
+        )
 
         properties = sge.Properties(expressions=properties)
         create_stmt = sge.Create(kind="TABLE", this=target, properties=properties)
diff --git a/ibis/backends/datafusion/__init__.py b/ibis/backends/datafusion/__init__.py
index 60f4270d587d..be9fc48dd6e1 100644
--- a/ibis/backends/datafusion/__init__.py
+++ b/ibis/backends/datafusion/__init__.py
@@ -673,7 +673,9 @@ def create_table(
         if query is None:
             target = sge.Schema(
                 this=table_ident,
-                expressions=(schema or table.schema()).to_sqlglot(self.dialect),
+                expressions=(schema or table.schema()).to_sqlglot_column_defs(
+                    self.dialect
+                ),
             )
         else:
             target = table_ident
diff --git a/ibis/backends/duckdb/__init__.py b/ibis/backends/duckdb/__init__.py
index 0b0aaa66c78f..6afd0f74d57b 100644
--- a/ibis/backends/duckdb/__init__.py
+++ b/ibis/backends/duckdb/__init__.py
@@ -200,7 +200,10 @@ def create_table(
             temp_name = name
 
         initial_table = sg.table(temp_name, catalog=catalog, db=database, quoted=quoted)
-        target = sge.Schema(this=initial_table, expressions=schema.to_sqlglot(dialect))
+        target = sge.Schema(
+            this=initial_table,
+            expressions=schema.to_sqlglot_column_defs(dialect),
+        )
 
         create_stmt = sge.Create(
             kind="TABLE",
diff --git a/ibis/backends/exasol/__init__.py b/ibis/backends/exasol/__init__.py
index 2da7966095b9..833a2aae3853 100644
--- a/ibis/backends/exasol/__init__.py
+++ b/ibis/backends/exasol/__init__.py
@@ -284,7 +284,10 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None:
         ident = sg.to_identifier(name, quoted=quoted)
         create_stmt = sg.exp.Create(
             kind="TABLE",
-            this=sg.exp.Schema(this=ident, expressions=schema.to_sqlglot(self.dialect)),
+            this=sg.exp.Schema(
+                this=ident,
+                expressions=schema.to_sqlglot_column_defs(self.dialect),
+            ),
         )
         create_stmt_sql = create_stmt.sql(self.name)
 
@@ -382,7 +385,8 @@ def create_table(
 
         table_expr = sg.table(temp_name, catalog=database, quoted=quoted)
         target = sge.Schema(
-            this=table_expr, expressions=schema.to_sqlglot(self.dialect)
+            this=table_expr,
+            expressions=schema.to_sqlglot_column_defs(self.dialect),
         )
 
         create_stmt = sge.Create(kind="TABLE", this=target)
diff --git a/ibis/backends/impala/__init__.py b/ibis/backends/impala/__init__.py
index 3fe43da6d0d4..b9c8c2cad553 100644
--- a/ibis/backends/impala/__init__.py
+++ b/ibis/backends/impala/__init__.py
@@ -1403,7 +1403,7 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None:
             kind="TABLE",
             this=sg.exp.Schema(
                 this=sg.to_identifier(name, quoted=quoted),
-                expressions=schema.to_sqlglot(self.dialect),
+                expressions=schema.to_sqlglot_column_defs(self.dialect),
             ),
         ).sql(self.name, pretty=True)
 
diff --git a/ibis/backends/mssql/__init__.py b/ibis/backends/mssql/__init__.py
index ff8733cfb409..4e700b6787e1 100644
--- a/ibis/backends/mssql/__init__.py
+++ b/ibis/backends/mssql/__init__.py
@@ -688,7 +688,7 @@ def create_table(
             this=sg.table(
                 "#" * bool(temp) + temp_name, catalog=catalog, db=db, quoted=quoted
             ),
-            expressions=schema.to_sqlglot(self.dialect),
+            expressions=schema.to_sqlglot_column_defs(self.dialect),
         )
 
         create_stmt = sge.Create(
@@ -755,7 +755,7 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None:
             kind="TABLE",
             this=sg.exp.Schema(
                 this=sg.to_identifier(name, quoted=quoted),
-                expressions=schema.to_sqlglot(self.dialect),
+                expressions=schema.to_sqlglot_column_defs(self.dialect),
             ),
         )
 
diff --git a/ibis/backends/mysql/__init__.py b/ibis/backends/mysql/__init__.py
index f8c7a8e0805a..5d476b696c15 100644
--- a/ibis/backends/mysql/__init__.py
+++ b/ibis/backends/mysql/__init__.py
@@ -408,7 +408,9 @@ def create_table(
         dialect = self.dialect
 
         table_expr = sg.table(temp_name, catalog=database, quoted=quoted)
-        target = sge.Schema(this=table_expr, expressions=schema.to_sqlglot(dialect))
+        target = sge.Schema(
+            this=table_expr, expressions=schema.to_sqlglot_column_defs(dialect)
+        )
 
         create_stmt = sge.Create(
             kind="TABLE", this=target, properties=sge.Properties(expressions=properties)
@@ -454,7 +456,7 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None:
             kind="TABLE",
             this=sg.exp.Schema(
                 this=sg.to_identifier(name, quoted=quoted),
-                expressions=schema.to_sqlglot(dialect),
+                expressions=schema.to_sqlglot_column_defs(dialect),
             ),
             properties=sg.exp.Properties(expressions=[sge.TemporaryProperty()]),
         )
diff --git a/ibis/backends/oracle/__init__.py b/ibis/backends/oracle/__init__.py
index 63cfbf84c147..ffc9acca65d7 100644
--- a/ibis/backends/oracle/__init__.py
+++ b/ibis/backends/oracle/__init__.py
@@ -442,7 +442,7 @@ def create_table(
         initial_table = sg.table(temp_name, db=database, quoted=self.compiler.quoted)
         target = sge.Schema(
             this=initial_table,
-            expressions=(schema or table.schema()).to_sqlglot(self.dialect),
+            expressions=(schema or table.schema()).to_sqlglot_column_defs(self.dialect),
         )
 
         create_stmt = sge.Create(
@@ -513,7 +513,7 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None:
             kind="TABLE",
             this=sg.exp.Schema(
                 this=sg.to_identifier(name, quoted=quoted),
-                expressions=schema.to_sqlglot(self.dialect),
+                expressions=schema.to_sqlglot_column_defs(self.dialect),
             ),
             properties=sge.Properties(expressions=[sge.TemporaryProperty()]),
         ).sql(self.name)
diff --git a/ibis/backends/postgres/__init__.py b/ibis/backends/postgres/__init__.py
index 801e289cb4a9..6b6dc4908e18 100644
--- a/ibis/backends/postgres/__init__.py
+++ b/ibis/backends/postgres/__init__.py
@@ -91,7 +91,7 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None:
             kind="TABLE",
             this=sg.exp.Schema(
                 this=sg.to_identifier(name, quoted=quoted),
-                expressions=schema.to_sqlglot(self.dialect),
+                expressions=schema.to_sqlglot_column_defs(self.dialect),
             ),
             properties=sg.exp.Properties(expressions=[sge.TemporaryProperty()]),
         )
@@ -631,7 +631,9 @@ def create_table(
         dialect = self.dialect
 
         table_expr = sg.table(temp_name, db=database, quoted=quoted)
-        target = sge.Schema(this=table_expr, expressions=schema.to_sqlglot(dialect))
+        target = sge.Schema(
+            this=table_expr, expressions=schema.to_sqlglot_column_defs(dialect)
+        )
 
         create_stmt = sge.Create(
             kind="TABLE",
diff --git a/ibis/backends/risingwave/__init__.py b/ibis/backends/risingwave/__init__.py
index ef0f3b243077..6df56f7d3e5a 100644
--- a/ibis/backends/risingwave/__init__.py
+++ b/ibis/backends/risingwave/__init__.py
@@ -561,7 +561,8 @@ def create_table(
 
         table_expr = sg.table(temp_name, db=database, quoted=self.compiler.quoted)
         target = sge.Schema(
-            this=table_expr, expressions=schema.to_sqlglot(self.dialect)
+            this=table_expr,
+            expressions=schema.to_sqlglot_column_defs(self.dialect),
         )
 
         if connector_properties is None:
@@ -617,7 +618,7 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None:
             kind="TABLE",
             this=sg.exp.Schema(
                 this=sg.to_identifier(name, quoted=quoted),
-                expressions=schema.to_sqlglot(self.dialect),
+                expressions=schema.to_sqlglot_column_defs(self.dialect),
             ),
         )
         create_stmt_sql = create_stmt.sql(self.dialect)
@@ -765,7 +766,9 @@ def create_source(
         """
         quoted = self.compiler.quoted
         table = sg.table(name, db=database, quoted=quoted)
-        target = sge.Schema(this=table, expressions=schema.to_sqlglot(self.dialect))
+        target = sge.Schema(
+            this=table, expressions=schema.to_sqlglot_column_defs(self.dialect)
+        )
 
         properties = sge.Properties.from_dict(connector_properties)
         properties.expressions.extend(
diff --git a/ibis/backends/snowflake/__init__.py b/ibis/backends/snowflake/__init__.py
index 84e311ff182c..55db0473a798 100644
--- a/ibis/backends/snowflake/__init__.py
+++ b/ibis/backends/snowflake/__init__.py
@@ -792,7 +792,8 @@ def create_table(
 
         if schema:
             target = sge.Schema(
-                this=target, expressions=schema.to_sqlglot(self.dialect)
+                this=target,
+                expressions=schema.to_sqlglot_column_defs(self.dialect),
             )
 
         properties = []
@@ -1089,7 +1090,10 @@ def read_parquet(
             f"CREATE TEMP STAGE {stage} FILE_FORMAT = (TYPE = PARQUET {options})",
             sge.Create(
                 kind="TABLE",
-                this=sge.Schema(this=qtable, expressions=schema.to_sqlglot(dialect)),
+                this=sge.Schema(
+                    this=qtable,
+                    expressions=schema.to_sqlglot_column_defs(dialect),
+                ),
                 properties=sge.Properties(expressions=[sge.TemporaryProperty()]),
             ).sql(dialect),
         ]
diff --git a/ibis/backends/sqlite/__init__.py b/ibis/backends/sqlite/__init__.py
index f679ba06c509..aa8d30b0cf28 100644
--- a/ibis/backends/sqlite/__init__.py
+++ b/ibis/backends/sqlite/__init__.py
@@ -334,7 +334,9 @@ def to_pyarrow_batches(
         return table.to_reader(max_chunksize=chunk_size)
 
     def _generate_create_table(self, table: sge.Table, schema: sch.Schema):
-        target = sge.Schema(this=table, expressions=schema.to_sqlglot(self.dialect))
+        target = sge.Schema(
+            this=table, expressions=schema.to_sqlglot_column_defs(self.dialect)
+        )
 
         return sge.Create(kind="TABLE", this=target)
 
diff --git a/ibis/backends/trino/__init__.py b/ibis/backends/trino/__init__.py
index 4d5778d18bc8..f707662fe340 100644
--- a/ibis/backends/trino/__init__.py
+++ b/ibis/backends/trino/__init__.py
@@ -464,7 +464,8 @@ def create_table(
 
         if schema is not None and obj is None:
             target = sge.Schema(
-                this=table_ref, expressions=schema.to_sqlglot(self.dialect)
+                this=table_ref,
+                expressions=schema.to_sqlglot_column_defs(self.dialect),
             )
         else:
             target = table_ref
@@ -564,7 +565,7 @@ def _register_in_memory_table(self, op: ops.InMemoryTable) -> None:
             kind="TABLE",
             this=sg.exp.Schema(
                 this=sg.to_identifier(name, quoted=quoted),
-                expressions=schema.to_sqlglot(self.dialect),
+                expressions=schema.to_sqlglot_column_defs(self.dialect),
             ),
         ).sql(self.name)
 
diff --git a/ibis/expr/schema.py b/ibis/expr/schema.py
index 53ff9cb775a0..f0c1d0653829 100644
--- a/ibis/expr/schema.py
+++ b/ibis/expr/schema.py
@@ -11,7 +11,7 @@
 from ibis.common.exceptions import InputTypeError, IntegrityError
 from ibis.common.grounds import Concrete
 from ibis.common.patterns import Coercible
-from ibis.util import indent
+from ibis.util import deprecated, indent
 
 if TYPE_CHECKING:
     import numpy as np
@@ -235,7 +235,7 @@ def name_at_position(self, i: int) -> str:
         """
         return self.names[i]
 
-    def to_sqlglot(self, dialect: str | sg.Dialect) -> list[sge.ColumnDef]:
+    def to_sqlglot_column_defs(self, dialect: str | sg.Dialect) -> list[sge.ColumnDef]:
         """Convert the schema to a list of SQL column definitions.
 
         Parameters
@@ -257,7 +257,7 @@ def to_sqlglot(self, dialect: str | sg.Dialect) -> list[sge.ColumnDef]:
           a  int64
           b  !string
         }
-        >>> columns = sch.to_sqlglot(dialect="duckdb")
+        >>> columns = sch.to_sqlglot_column_defs(dialect="duckdb")
         >>> columns
         [ColumnDef(
           this=Identifier(this='a', quoted=True),
@@ -303,6 +303,13 @@ def to_sqlglot(self, dialect: str | sg.Dialect) -> list[sge.ColumnDef]:
             for name, dtype in self.items()
         ]
 
+    @deprecated(
+        instead="use Schema.to_sqlglot_column_defs() instead. In a future release, to_sqlglot() will return a sqlglot.schema.Schema object"
+    )
+    def to_sqlglot(self, dialect: str | sg.Dialect) -> list[sge.ColumnDef]:
+        """DEPRECATED: use `to_sqlglot_column_defs()` instead."""
+        return self.to_sqlglot_column_defs(dialect)
+
 
 SchemaLike: TypeAlias = Union[
     Schema,


Test patch:
diff --git a/ibis/backends/trino/tests/conftest.py b/ibis/backends/trino/tests/conftest.py
index 4e6538306247..a2e0b7aa1dca 100644
--- a/ibis/backends/trino/tests/conftest.py
+++ b/ibis/backends/trino/tests/conftest.py
@@ -182,7 +182,7 @@ def generate_tpc_tables(suite_name, *, data_dir):
             exists=True,
             this=sge.Schema(
                 this=sg.table(name, db=suite_name, catalog="hive", quoted=True),
-                expressions=schema.to_sqlglot("trino"),
+                expressions=schema.to_sqlglot_column_defs("trino"),
             ),
             properties=sge.Properties(
                 expressions=[
diff --git a/ibis/expr/tests/test_schema.py b/ibis/expr/tests/test_schema.py
index 8192e21652e8..3078bf054c1b 100644
--- a/ibis/expr/tests/test_schema.py
+++ b/ibis/expr/tests/test_schema.py
@@ -437,3 +437,44 @@ def test_null_fields():
     assert sch.schema({"a": "int64", "b": "string"}).null_fields == ()
     assert sch.schema({"a": "null", "b": "string"}).null_fields == ("a",)
     assert sch.schema({"a": "null", "b": "null"}).null_fields == ("a", "b")
+
+
+def test_to_sqlglot_column_defs():
+    import sqlglot.expressions as sge
+
+    schema = sch.schema({"a": "int64", "b": "string", "c": "!string"})
+    columns = schema.to_sqlglot_column_defs("duckdb")
+
+    assert len(columns) == 3
+    assert all(isinstance(col, sge.ColumnDef) for col in columns)
+    assert all(col.this.quoted is True for col in columns)
+    assert [col.this.this for col in columns] == ["a", "b", "c"]
+
+    assert not columns[0].constraints
+    assert not columns[1].constraints
+    assert len(columns[2].constraints) == 1
+    assert isinstance(columns[2].constraints[0].kind, sge.NotNullColumnConstraint)
+
+
+def test_to_sqlglot_column_defs_empty_schema():
+    schema = sch.schema({})
+    columns = schema.to_sqlglot_column_defs("duckdb")
+    assert columns == []
+
+
+def test_to_sqlglot_column_defs_create_table_integration():
+    import sqlglot as sg
+    import sqlglot.expressions as sge
+
+    schema = sch.schema({"id": "!int64", "name": "string"})
+    columns = schema.to_sqlglot_column_defs("duckdb")
+
+    table = sg.table("test_table", quoted=True)
+    create_stmt = sge.Create(
+        kind="TABLE",
+        this=sge.Schema(this=table, expressions=columns),
+    )
+
+    sql = create_stmt.sql(dialect="duckdb")
+    expected = 'CREATE TABLE "test_table" ("id" BIGINT NOT NULL, "name" TEXT)'
+    assert sql == expected