Repository: ibis-project/ibis

Issue:
create_table in base/sql/alchemy/__init__.py uses the wrong schema function
There is a suspect function call in the alchemy backend in the `create_table` method. The `schema` parameter of that method is annotated as being a `sch.Schema`. The following code uses that object as a parameter to `ibis.schema` which only accepts pairs of tuples or a dict.
```
            if not expr.schema().equals(ibis.schema(schema)):
```
There is another `schema` function in `ibis.expr.schema` that I believe is the correct one to call in this case. That would change the above line to:
```
            if not expr.schema().equals(sch.schema(schema)):
```
The fact that there are two `schema` functions that act slightly differently is something that might be addressed in addition to fixing this.


Discussion:
Hi @kesmit13, thanks for noticing this!  I think you're right, and we'll change this as you suggest, and also think about how we can differentiate them (even though the second one should only be used internally).

Resolving patch:
diff --git a/ibis/expr/api.py b/ibis/expr/api.py
--- a/ibis/expr/api.py
+++ b/ibis/expr/api.py
@@ -4,7 +4,10 @@
 
 import datetime
 import functools
-from typing import Iterable, Mapping, Sequence, TypeVar
+from typing import Iterable, Mapping, Sequence
+from typing import Tuple as _Tuple
+from typing import TypeVar
+from typing import Union as _Union
 
 import dateutil.parser
 import numpy as np
@@ -216,6 +219,13 @@
 
 negate = ir.NumericValue.negate
 
+SupportsSchema = TypeVar(
+    "SupportsSchema",
+    Iterable[_Tuple[str, _Union[str, dt.DataType]]],
+    Mapping[str, dt.DataType],
+    sch.Schema,
+)
+
 
 def param(type: dt.DataType) -> ir.Scalar:
     """Create a deferred parameter of a given type.
@@ -261,9 +271,7 @@ def sequence(values: Sequence[T | None]) -> ir.ValueList:
 
 
 def schema(
-    pairs: Iterable[tuple[str, dt.DataType]]
-    | Mapping[str, dt.DataType]
-    | None = None,
+    pairs: SupportsSchema | None = None,
     names: Iterable[str] | None = None,
     types: Iterable[str | dt.DataType] | None = None,
 ) -> sch.Schema:
@@ -273,7 +281,7 @@ def schema(
     ----------
     pairs
         List or dictionary of name, type pairs. Mutually exclusive with `names`
-        and `types`.
+        and `types` arguments.
     names
         Field names. Mutually exclusive with `pairs`.
     types
@@ -281,12 +289,14 @@ def schema(
 
     Examples
     --------
-    >>> from ibis import schema
+    >>> from ibis import schema, Schema
     >>> sc = schema([('foo', 'string'),
     ...              ('bar', 'int64'),
     ...              ('baz', 'boolean')])
-    >>> sc2 = schema(names=['foo', 'bar', 'baz'],
-    ...              types=['string', 'int64', 'boolean'])
+    >>> sc = schema(names=['foo', 'bar', 'baz'],
+    ...             types=['string', 'int64', 'boolean'])
+    >>> sc = schema(dict(foo="string"))
+    >>> sc = schema(Schema(['foo'], ['string']))  # no-op
 
     Returns
     -------
@@ -294,34 +304,30 @@ def schema(
         An ibis schema
     """  # noqa: E501
     if pairs is not None:
-        return Schema.from_dict(dict(pairs))
+        return sch.schema(pairs)
     else:
-        return Schema(names, types)
-
+        return sch.schema(names, types)
 
-_schema = schema
-
-
-def table(schema: sch.Schema, name: str | None = None) -> ir.Table:
-    """Create an unbound table for build expressions without data.
 
+def table(
+    schema: SupportsSchema,
+    name: str | None = None,
+) -> ir.Table:
+    """Create an unbound table for building expressions without data.
 
     Parameters
     ----------
     schema
         A schema for the table
     name
-        Name for the table
+        Name for the table. One is generated if this value is `None`.
 
     Returns
     -------
     Table
         An unbound table expression
     """
-    if not isinstance(schema, Schema):
-        schema = _schema(pairs=schema)
-
-    node = ops.UnboundTable(schema, name=name)
+    node = ops.UnboundTable(sch.schema(schema), name=name)
     return node.to_expr()
 
 


Test patch:
diff --git a/ibis/tests/expr/test_schema.py b/ibis/tests/expr/test_schema.py
--- a/ibis/tests/expr/test_schema.py
+++ b/ibis/tests/expr/test_schema.py
@@ -188,3 +188,14 @@ def test_apply_to_column_order(df):
     expected = df.rename({"A": "a"}, axis=1)
     new_df = schema.apply_to(df.copy())
     tm.assert_frame_equal(new_df, expected)
+
+
+def test_api_accepts_schema_objects():
+    s1 = ibis.schema(dict(a="int", b="str"))
+    s2 = ibis.schema(s1)
+    assert s1 == s2
+
+
+def test_names_types():
+    s = ibis.schema(names=["a"], types=["array<float64>"])
+    assert s == ibis.schema(dict(a="array<float64>"))