Repository: dask/dask

Issue:
to_sql fails with distributed scheduler
**What happened**:
`to_sql` fails with distributed scheduler. pickle error.

**Minimal Complete Verifiable Example**:

```python
import pandas as pd
import dask.dataframe as dd
import sqlalchemy
from dask.distributed import Client

client = Client(processes=False, threads_per_worker=2)
engine = sqlalchemy.create_engine(
    <db_connection_string_here>,
    fast_executemany=True,
)
df = dd.from_pandas(pd.DataFrame([1,2, 3,4,5]), npartitions=1)
df.to_sql('test', engine, if_exists='replace')
```

error:
```python
TypeError: cannot pickle '_thread._local' object
```

uncomment `client = Client(processes=False, threads_per_worker=2)` above and the example works, it is tied to the distributed scheduler.

**Environment**:

- Dask version: 2.21.0
- Python version: 3.8.5
- Operating System: linux 64bit (manjaro)
- Install method (conda, pip, source): conda



Discussion:
Thanks for the bug report and sorry that it has taken us a while to get to. I am having trouble reproducing the error that you are seeing. This is what I am doing:

```python
from sqlalchemy import create_engine
import pandas as pd
from dask.utils import tmpfile
from dask.dataframe import from_pandas
from dask.distributed import Client

client = Client(processes=False, threads_per_worker=2)

df = pd.DataFrame([ {'i':i, 's':str(i)*2 } for i in range(4) ])
ddf = from_pandas(df, npartitions=2)

with tmpfile() as f:
    db = 'sqlite:///%s' % f
    ddf.to_sql('test', db , if_exists='replace')
    engine = create_engine(db)
    result = engine.execute("SELECT * FROM test").fetchall()

result
```

Can you see if you can get that to fail in the way that you were seeing?

Hi @jsignell and sorry for the delay (work + vacations)
Your code works fine, no fail.
`f` is just a file (correct?) but `sqlalchemy.create_engine` is something more no?

Can we mock the db connection in a different way to be more representative?

P.S. - was using prefect with a DaskExecutor and had the same problem of not being able to pickle the sqlalchemy engine with the same `TypeError: cannot pickle '_thread._local' object`

Resolving patch:
diff --git a/dask/dataframe/io/sql.py b/dask/dataframe/io/sql.py
--- a/dask/dataframe/io/sql.py
+++ b/dask/dataframe/io/sql.py
@@ -22,7 +22,7 @@ def read_sql_table(
     schema=None,
     meta=None,
     engine_kwargs=None,
-    **kwargs
+    **kwargs,
 ):
     """
     Create dataframe from an SQL table.
@@ -351,6 +351,8 @@ def to_sql(
     >>> result
     [(0, 0, '00'), (1, 1, '11'), (2, 2, '22'), (3, 3, '33')]
     """
+    if not isinstance(uri, str):
+        raise ValueError(f"Expected URI to be a string, got {type(uri)}.")
 
     # This is the only argument we add on top of what Pandas supports
     kwargs = dict(
@@ -388,7 +390,7 @@ def make_meta(meta):
                 d.to_sql,
                 extras=meta_task,
                 **worker_kwargs,
-                dask_key_name="to_sql-%s" % tokenize(d, **worker_kwargs)
+                dask_key_name="to_sql-%s" % tokenize(d, **worker_kwargs),
             )
             for d in df.to_delayed()
         ]
@@ -402,7 +404,7 @@ def make_meta(meta):
                     d.to_sql,
                     extras=last,
                     **worker_kwargs,
-                    dask_key_name="to_sql-%s" % tokenize(d, **worker_kwargs)
+                    dask_key_name="to_sql-%s" % tokenize(d, **worker_kwargs),
                 )
             )
             last = result[-1]


Test patch:
diff --git a/dask/dataframe/io/tests/test_sql.py b/dask/dataframe/io/tests/test_sql.py
--- a/dask/dataframe/io/tests/test_sql.py
+++ b/dask/dataframe/io/tests/test_sql.py
@@ -59,6 +59,20 @@ def test_empty(db):
         assert pd_dataframe.empty is True
 
 
+def test_passing_engine_as_uri_raises_helpful_error(db):
+    # https://github.com/dask/dask/issues/6473
+    from sqlalchemy import create_engine
+
+    df = pd.DataFrame([{"i": i, "s": str(i) * 2} for i in range(4)])
+    ddf = dd.from_pandas(df, npartitions=2)
+
+    with tmpfile() as f:
+        db = "sqlite:///%s" % f
+        engine = create_engine(db)
+        with pytest.raises(ValueError, match="Expected URI to be a string"):
+            ddf.to_sql("test", engine, if_exists="replace")
+
+
 @pytest.mark.skip(
     reason="Requires a postgres server. Sqlite does not support multiple schemas."
 )