Repository: dask/dask

Issue:
read_sql_table returns incomplete result if index_col is only column
<!-- Please include a self-contained copy-pastable example that generates the issue if possible.

Please be concise with code posted. See guidelines below on how to provide a good bug report:

- Craft Minimal Bug Reports http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports
- Minimal Complete Verifiable Examples https://stackoverflow.com/help/mcve

Bug reports that follow these guidelines are easier to diagnose, and so are often handled much more quickly.
-->

**What happened**:

When running ``dask.dataframe.read_sql_table(head_rows=0, meta=meta)`` with on a table which has only one column, or when selecting *only* the index_col from a table, then the returned result is incomplete.

**Minimal Complete Verifiable Example**:

```python
import dask.dataframe as dd
dd.read_sql_table(table='my_table', head_rows=0, meta=meta, uri=uri, index_col='ID')
```
where ``my_table`` has only the `ID` column and more than `head_rows` rows.

**Anything else we need to know?**:
This behaviour is due to this line:
https://github.com/dask/dask/blob/c5633c21e754df6b2c13195f1e2688e347c48963/dask/dataframe/io/sql.py#L227

The problem is that a pandas dataframe with only an index is considered empty if there are no other columns:
```python
import pandas as pd
df = pd.DataFrame([], index=range(10))
df.empty
# True
```
Thus the code takes a wrong branch there and returns the head with ``from_pandas`` - despite there being a whole lot other entries in the index column.

A simple fix might be to use ``len(head)`` instead of ``head.empty``.

**Environment**:

- Dask version: 2021.02.0



Discussion:
Thanks for raising an issue @c-thiel! 

> A simple fix might be to use len(head) instead of head.empty

Naively this seems reasonable to me -- though I'm not sure if there is any downstream logic that might assume there are row/column contents. Would you be interested in submitting a PR with the proposed change? 
@jrbourbeau I am happy to prepare a PR once I find a bit time for it - probably next weekend.
Sounds good, no rush. Thanks @c-thiel : )

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
@@ -141,7 +141,7 @@ def read_sql_table(
         q = sql.select(columns).limit(head_rows).select_from(table)
         head = pd.read_sql(q, engine, **kwargs)
 
-        if head.empty:
+        if len(head) == 0:
             # no results at all
             name = table.name
             schema = table.schema
@@ -224,8 +224,12 @@ def _read_sql_chunk(q, uri, meta, engine_kwargs=None, **kwargs):
     engine = sa.create_engine(uri, **engine_kwargs)
     df = pd.read_sql(q, engine, **kwargs)
     engine.dispose()
-    if df.empty:
+    if len(df) == 0:
         return meta
+    elif len(meta.dtypes.to_dict()) == 0:
+        # only index column in loaded
+        # required only for pandas < 1.0.0
+        return df
     else:
         return df.astype(meta.dtypes.to_dict(), copy=False)
 


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,43 @@ def test_empty(db):
         assert pd_dataframe.empty is True
 
 
+@pytest.mark.filterwarnings(
+    "ignore:The default dtype for empty Series " "will be 'object' instead of 'float64'"
+)
+@pytest.mark.parametrize("use_head", [True, False])
+def test_single_column(db, use_head):
+    from sqlalchemy import Column, Integer, MetaData, Table, create_engine
+
+    with tmpfile() as f:
+        uri = "sqlite:///%s" % f
+        metadata = MetaData()
+        engine = create_engine(uri)
+        table = Table(
+            "single_column",
+            metadata,
+            Column("id", Integer, primary_key=True),
+        )
+        metadata.create_all(engine)
+        test_data = pd.DataFrame({"id": list(range(50))}).set_index("id")
+        test_data.to_sql(table.name, uri, index=True, if_exists="replace")
+
+        if use_head:
+            dask_df = read_sql_table(table.name, uri, index_col="id", npartitions=2)
+        else:
+            dask_df = read_sql_table(
+                table.name,
+                uri,
+                head_rows=0,
+                npartitions=2,
+                meta=test_data.iloc[:0],
+                index_col="id",
+            )
+        assert dask_df.index.name == "id"
+        assert dask_df.npartitions == 2
+        pd_dataframe = dask_df.compute()
+        assert_eq(test_data, pd_dataframe)
+
+
 def test_passing_engine_as_uri_raises_helpful_error(db):
     # https://github.com/dask/dask/issues/6473
     from sqlalchemy import create_engine