Repository: geopandas/geopandas

Issue:
BUG: OutOfBoundsDatetime on read of faulty datetimes
- [x ] I have checked that this issue has not already been reported.

- [ x] I have confirmed this bug exists on the latest version of geopandas.

- [x ] (optional) I have confirmed this bug exists on the main branch of geopandas.

---

#### Code Sample, a copy-pastable example
Expected behaviour converting 'fully corrupt' datetime to NaT.
```python
import geopandas
from shapely.geometry import Point
import tempfile

# Fully corrupt datetime (row 4)

data = [[1, '1990-02-0300:00:00'],[2, '2002-08-2118:00:00'],[3, '2019-10-3019:00:05'],
        [4, '2008-02-2906:30:45'],[5, '9999-99-9900:00:00']]
geom = [Point(1,1),Point(2,2),Point(3,3),Point(4,4),Point(5,5)]

gdf = geopandas.GeoDataFrame(data = data, columns = ['uid', 'date'], crs = None, geometry=geom)

myschema = {'geometry': 'Point', 'properties': {'uid': 'int', 'date':'datetime'}}

with tempfile.TemporaryDirectory() as td:
    gdf.to_file(f"{td}/temp.gpkg", driver='GPKG', schema=myschema)
    
    print(geopandas.read_file(f"{td}/temp.gpkg"))
```
Unexpected(?) behaviour erroring 'partially corrupt' datetime:

```python
import geopandas
from shapely.geometry import Point
import tempfile

# Semi corrupt datetime (row 4)

data = [[1, '1990-02-0300:00:00'],[2, '2002-08-2118:00:00'],[3, '2019-10-3019:00:05'],
        [4, '2008-02-2906:30:45'],[5, '9999-12-3100:00:00']]
geom = [Point(1,1),Point(2,2),Point(3,3),Point(4,4),Point(5,5)]

gdf = geopandas.GeoDataFrame(data = data, columns = ['uid', 'date'], crs = None, geometry=geom)

myschema = {'geometry': 'Point', 'properties': {'uid': 'int', 'date':'datetime'}}

with tempfile.TemporaryDirectory() as td:
    gdf.to_file(f"{td}/temp.gpkg", driver='GPKG', schema=myschema)
    
    print(geopandas.read_file(f"{td}/temp.gpkg"))
```

This second example returns `OutOfBoundsDatetime: Out of bounds nanosecond timestamp: 9999-12-31 00:00:00`.

#### Problem description

I have a geopackage with datetime in which some datetimes are arbitrarily broken, e.g. strings as '9999-12-3100:00:00', the schema has these set as 'datetime', which means that on read geopandas.io.file is applying `pandas.to_datetime()` to make these strings into datetime objects. This throws an error when these are out of bounds, but curiously only when they're partially out of bounds as shown in the difference between the two examples above.

There would appear to be a solution in which `pandas.to_datetime()` has the `errors` argument defaulting to raise, if instead this is set to coerce or ignore it would allow the file to be read.

I suspect if this were desirable it would have to be as a `geopandas.read_file` argument, the field would then have to be subject to further inspection by the user to resolve any datetime issues, or else it would need to be clearly documented as not guaranteeing datetime objects on read. 

An alternative might be to allow user-defined schemas for read, but that is a more substantial change.

I also accept this might be considered such an edge case that the current behaviour is acceptable.

#### Expected Output

File is read with corrupt datetimes either set to NaT, or ignored and retained as strings. 

#### Output of ``geopandas.show_versions()``

<details>

SYSTEM INFO
-----------
python     : 3.8.5 (default, Mar  2 2021, 09:39:24)  [GCC 9.3.0]
executable : /home/user/.cache/pypoetry/virtualenvs/project-YfbsymT8-py3.8/bin/python
machine    : Linux-5.15.0-1015-aws-x86_64-with-glibc2.29

GEOS, GDAL, PROJ INFO
---------------------
GEOS       : 3.10.2
GEOS lib   : /usr/lib/x86_64-linux-gnu/libgeos_c.so
GDAL       : 3.4.1
GDAL data dir: /home/user/.cache/pypoetry/virtualenvs/project-YfbsymT8-py3.8/lib/python3.8/site-packages/fiona/gdal_data
PROJ       : 8.2.0
PROJ data dir: /home/user/.cache/pypoetry/virtualenvs/project-YfbsymT8-py3.8/lib/python3.8/site-packages/pyproj/proj_dir/share/proj

PYTHON DEPENDENCIES
-------------------
geopandas  : 0.11.0
pandas     : 1.4.1
fiona      : 1.8.21
numpy      : 1.23.1
shapely    : 1.8.1.post1
rtree      : 0.9.7
pyproj     : 3.3.0
matplotlib : 3.5.1
mapclassify: 2.4.3
geopy      : None
psycopg2   : None
geoalchemy2: None
pyarrow    : 4.0.1
pygeos     : 0.12.0

</details>


Discussion:
jorisvandenbossche: @m-richards I updated this now the other PR is merged.
m-richards: > @m-richards I updated this now the other PR is merged.

Thanks, was going to try have a look tonight but you beat me to it. Hopefully this is good to go now - there's an awkward pyogrio edge case that I'm skipping in one of the tests (pyogrio is calling pandas internally and converting dates there) but otherwise should be sensible.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d543f3f..2fd3f56a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,8 @@ Small bug-fix release:
   casted to GeometryDtype (#2482). 
 - Fix a crash in datetime column reading where the file contains mixed timezone
   offsets (#2479). These will be read as UTC localized values.
+- Fix a crash in datetime column reading where the file contains datetimes
+  outside the range supported by [ns] precision (#2505).
 - Fix regression in passing the Parquet or Feather format ``version`` in
   ``to_parquet`` and ``to_feather``. As a result, the ``version`` parameter
   for the ``to_parquet`` and ``to_feather`` methods has been replaced with
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
index 3b4336e8..8c62af5e 100644
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -341,13 +341,14 @@ def _read_file_fiona(
                     f_filt, crs=crs, columns=columns + ["geometry"]
                 )
             for k in datetime_fields:
-                as_dt = pd.to_datetime(df[k])
+                as_dt = pd.to_datetime(df[k], errors="ignore")
                 # if to_datetime failed, try again for mixed timezone offsets
                 if as_dt.dtype == "object":
-                    # This can fail if there are invalid datetimes
-                    as_dt = pd.to_datetime(df[k], utc=True)
+                    # This can still fail if there are invalid datetimes
+                    as_dt = pd.to_datetime(df[k], errors="ignore", utc=True)
                 # if to_datetime succeeded, round datetimes as
-                # fiona only supports up to ms precision
+                # fiona only supports up to ms precision (any microseconds are
+                # floating point rounding error)
                 if not (as_dt.dtype == "object"):
                     df[k] = as_dt.dt.round(freq="ms")
             return df


Test patch:
diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py
index 6ef58582..2ccbd949 100644
--- a/geopandas/io/tests/test_file.py
+++ b/geopandas/io/tests/test_file.py
@@ -33,12 +33,14 @@ except ImportError:
 try:
     import fiona
 
-    FIONA_GE_1814 = Version(fiona.__version__) >= Version(
-        "1.8.14"
-    )  # datetime roundtrip
+    # datetime roundtrip
+    FIONA_GE_1814 = Version(fiona.__version__) >= Version("1.8.14")
+    # invalid datetime handling
+    FIONA_GE_1821 = Version(fiona.__version__) >= Version("1.8.21")
 except ImportError:
     fiona = False
     FIONA_GE_1814 = False
+    FIONA_GE_1821 = False
 
 
 PYOGRIO_MARK = pytest.mark.skipif(not pyogrio, reason="pyogrio not installed")
@@ -232,7 +234,63 @@ def test_to_file_datetime(tmpdir, driver, ext, time, engine):
         assert_series_equal(df["b"], df_read["b"])
 
 
-def test_read_file_mixed_datetimes(tmpdir):
+dt_exts = ["gpkg", "geojson"]
+
+
+def write_invalid_date_file(date_str, tmpdir, ext, engine):
+    tempfilename = os.path.join(str(tmpdir), f"test_invalid_datetime.{ext}")
+    df = GeoDataFrame(
+        {
+            "date": ["2014-08-26T10:01:23", "2014-08-26T10:01:23", date_str],
+            "geometry": [Point(1, 1), Point(1, 1), Point(1, 1)],
+        }
+    )
+    # Schema not required for GeoJSON since not typed, but needed for GPKG
+    if ext == "geojson":
+        df.to_file(tempfilename)
+    else:
+        schema = {"geometry": "Point", "properties": {"date": "datetime"}}
+        if engine == "pyogrio" and not fiona:
+            # (use schema to write the invalid date without pandas datetimes
+            pytest.skip("test requires fiona kwarg schema")
+        df.to_file(tempfilename, schema=schema, engine="fiona")
+    return tempfilename
+
+
+@pytest.mark.parametrize("ext", dt_exts)
+def test_read_file_datetime_invalid(tmpdir, ext, engine):
+    # https://github.com/geopandas/geopandas/issues/2502
+    if not FIONA_GE_1821 and ext == "gpkg":
+        # https://github.com/Toblerity/Fiona/issues/1035
+        pytest.skip("Invalid datetime throws in Fiona<1.8.21")
+
+    date_str = "9999-99-99T00:00:00"  # invalid date handled by GDAL
+    tempfilename = write_invalid_date_file(date_str, tmpdir, ext, engine)
+    res = read_file(tempfilename)
+    if ext == "gpkg":
+        assert is_datetime64_any_dtype(res["date"])
+        assert pd.isna(res["date"].iloc[-1])
+    else:
+        assert res["date"].dtype == "object"
+        assert isinstance(res["date"].iloc[-1], str)
+
+
+@pytest.mark.parametrize("ext", dt_exts)
+def test_read_file_datetime_out_of_bounds_ns(tmpdir, ext, engine):
+    # https://github.com/geopandas/geopandas/issues/2502
+    if ext == "geojson":
+        skip_pyogrio_not_supported(engine)
+
+    date_str = "9999-12-31T00:00:00"  # valid to GDAL, not to [ns] format
+    tempfilename = write_invalid_date_file(date_str, tmpdir, ext, engine)
+    res = read_file(tempfilename)
+    # Pandas invalid datetimes are read in as object dtype (strings)
+    assert res["date"].dtype == "object"
+    assert isinstance(res["date"].iloc[0], str)
+
+
+def test_read_file_datetime_mixed_offsets(tmpdir):
+    # https://github.com/geopandas/geopandas/issues/2478
     tempfilename = os.path.join(str(tmpdir), "test_mixed_datetime.geojson")
     df = GeoDataFrame(
         {
@@ -244,10 +302,11 @@ def test_read_file_mixed_datetimes(tmpdir):
         }
     )
     df.to_file(tempfilename)
-    res = read_file(tempfilename)  # check mixed tz don't crash GH2478
-    assert is_datetime64_any_dtype(res["date"])
+    # check mixed tz don't crash GH2478
+    res = read_file(tempfilename)
     if FIONA_GE_1814:
         # Convert mixed timezones to UTC equivalent
+        assert is_datetime64_any_dtype(res["date"])
         assert res["date"].dt.tz == pytz.utc
     else:
         # old fiona and pyogrio ignore timezones and read as datetimes successfully