Repository: ibis-project/ibis

Issue:
bug: cannot convert mysql `datetime(6)` type to ibis datatype
### What happened?

`TypeError: MySQLType._from_sqlglot_DATETIME() takes 1 positional argument but 2 were given` when calling `db.table(table_name)` 

### What version of ibis are you using?

9.0.0

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

MySQL

### Relevant log output

```sh
{
	"name": "TypeError",
	"message": "MySQLType._from_sqlglot_DATETIME() takes 1 positional argument but 2 were given",
	"stack": "---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[1], line 14
     12 doc = frappe.get_doc(\"Insights Data Source\", \"Site DB\")
     13 with doc._get_ibis_backend() as db:
---> 14 \tuser = db.table(\"tabToDo\")
     15 \tprint(user)

File ~/frappe/bench-v13/env/lib/python3.10/site-packages/ibis/backends/sql/__init__.py:137, in SQLBackend.table(self, name, schema, database)
    134     catalog = table_loc.catalog or None
    135     database = table_loc.db or None
--> 137 table_schema = self.get_schema(name, catalog=catalog, database=database)
    138 return ops.DatabaseTable(
    139     name,
    140     schema=table_schema,
    141     source=self,
    142     namespace=ops.Namespace(catalog=catalog, database=database),
    143 ).to_expr()

File ~/frappe/bench-v13/env/lib/python3.10/site-packages/ibis/backends/mysql/__init__.py:209, in Backend.get_schema(self, name, catalog, database)
    206     result = cur.fetchall()
    208 type_mapper = self.compiler.type_mapper
--> 209 fields = {
    210     name: type_mapper.from_string(type_string, nullable=is_nullable == \"YES\")
    211     for name, type_string, is_nullable, *_ in result
    212 }
    214 return sch.Schema(fields)

File ~/frappe/bench-v13/env/lib/python3.10/site-packages/ibis/backends/mysql/__init__.py:210, in <dictcomp>(.0)
    206     result = cur.fetchall()
    208 type_mapper = self.compiler.type_mapper
    209 fields = {
--> 210     name: type_mapper.from_string(type_string, nullable=is_nullable == \"YES\")
    211     for name, type_string, is_nullable, *_ in result
    212 }
    214 return sch.Schema(fields)

File ~/frappe/bench-v13/env/lib/python3.10/site-packages/ibis/backends/sql/datatypes.py:196, in SqlglotType.from_string(cls, text, nullable)
    193     return dtype
    195 sgtype = sg.parse_one(text, into=sge.DataType, read=cls.dialect)
--> 196 return cls.to_ibis(sgtype, nullable=nullable)

File ~/frappe/bench-v13/env/lib/python3.10/site-packages/ibis/backends/sql/datatypes.py:172, in SqlglotType.to_ibis(cls, typ, nullable)
    169     typecode = typ.this
    171 if method := getattr(cls, f\"_from_sqlglot_{typecode.name}\", None):
--> 172     dtype = method(*typ.expressions)
    173 else:
    174     dtype = _from_sqlglot_types[typecode](nullable=cls.default_nullable)

TypeError: MySQLType._from_sqlglot_DATETIME() takes 1 positional argument but 2 were given"
}
```


### Code of Conduct

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


Discussion:

Resolving patch:
diff --git a/ibis/backends/sql/datatypes.py b/ibis/backends/sql/datatypes.py
--- a/ibis/backends/sql/datatypes.py
+++ b/ibis/backends/sql/datatypes.py
@@ -487,8 +487,19 @@ def _from_sqlglot_BIT(cls, nbits: sge.DataTypeParam) -> dt.Integer:
             return dt.Int8(nullable=cls.default_nullable)
 
     @classmethod
-    def _from_sqlglot_DATETIME(cls) -> dt.Timestamp:
-        return dt.Timestamp(nullable=cls.default_nullable)
+    def _from_sqlglot_DATETIME(cls, scale=None) -> dt.Timestamp:
+        if scale is not None:
+            scale = int(scale.this.this)
+        return dt.Timestamp(
+            # scale of zero means "no scale", which differs from the SQL
+            # standard
+            #
+            # see
+            # https://dev.mysql.com/doc/refman/8.4/en/fractional-seconds.html
+            # for details
+            scale=scale or None,
+            nullable=cls.default_nullable,
+        )
 
     @classmethod
     def _from_sqlglot_TIMESTAMP(cls) -> dt.Timestamp:


Test patch:
diff --git a/ibis/backends/mysql/tests/test_client.py b/ibis/backends/mysql/tests/test_client.py
--- a/ibis/backends/mysql/tests/test_client.py
+++ b/ibis/backends/mysql/tests/test_client.py
@@ -57,6 +57,13 @@
     param("mediumblob", dt.binary, id="mediumblob"),
     param("blob", dt.binary, id="blob"),
     param("uuid", dt.uuid, id="uuid"),
+] + [
+    param(
+        f"datetime({scale:d})",
+        dt.Timestamp(scale=scale or None),
+        id=f"datetime{scale:d}",
+    )
+    for scale in range(7)
 ]