import datetime
import re

import numpy as np
import pytest

from pandas._config import using_string_dtype

from pandas._libs.tslibs import Timestamp
from pandas.compat import is_platform_windows

import pandas as pd
import pandas._testing as tm
from pandas.util import _test_decorators as td

pytestmark = [pytest.mark.single_cpu]


def test_conv_read_write(temp_h5_path):
    def roundtrip(key, obj, **kwargs):
        obj.to_hdf(temp_h5_path, key=key, **kwargs)
        return pd.read_hdf(temp_h5_path, key)

    o = pd.Series(
        np.arange(10, dtype=np.float64), index=pd.date_range("2020-01-01", periods=10)
    )
    tm.assert_series_equal(o, roundtrip("series", o))

    o = pd.Series(range(10), dtype="float64", index=[f"i_{i}" for i in range(10)])
    tm.assert_series_equal(o, roundtrip("string_series", o))

    o = pd.DataFrame(
        1.1 * np.arange(120).reshape((30, 4)),
        columns=pd.Index(list("ABCD")),
        index=pd.Index([f"i-{i}" for i in range(30)]),
    )
    tm.assert_frame_equal(o, roundtrip("frame", o))

    # table
    df = pd.DataFrame({"A": range(5), "B": range(5)})
    df.to_hdf(temp_h5_path, key="table", append=True)
    result = pd.read_hdf(temp_h5_path, "table", where=["index>2"])
    tm.assert_frame_equal(df[df.index > 2], result)


def test_long_strings(temp_hdfstore):
    # GH6166
    data = ["a" * 50] * 10
    df = pd.DataFrame({"a": data}, index=data)

    temp_hdfstore.append("df", df, data_columns=["a"])

    result = temp_hdfstore.select("df")
    tm.assert_frame_equal(df, result)


def test_string_nan_in_index_fixed(temp_h5_path):
    # GH#9604 — literal "nan" string in a string Index was being converted to
    # NaN on read because the unconverter applied a NaN-sentinel substitution
    # that the writer never performed for indices.
    words = ["nan", "kai", "institute", "of", "technology"]
    ser = pd.Series(range(len(words)), index=words)

    ser.to_hdf(temp_h5_path, key="s", mode="w")
    result = pd.read_hdf(temp_h5_path, "s")

    tm.assert_series_equal(result, ser)


def test_string_nan_in_index_table(temp_hdfstore):
    # GH#9604 — same bug, table format. Also verifies that nan_rep is honored
    # for the index (it was previously silently ignored on the index read).
    words = ["nan", "kai", "institute", "of", "technology"]
    ser = pd.Series(range(len(words)), index=words)

    temp_hdfstore.append("s", ser, nan_rep="_nan_")
    result = temp_hdfstore.select("s")

    tm.assert_series_equal(result, ser)


def test_string_nan_in_dataframe_index(temp_h5_path):
    # GH#9604 — DataFrame index with literal "nan" strings, both formats.
    df = pd.DataFrame(
        {"a": [1, 2, 3, 4]},
        index=pd.Index(["nan", "kai", "institute", "of"], name="ix"),
    )

    df.to_hdf(temp_h5_path, key="fixed", mode="w")
    tm.assert_frame_equal(pd.read_hdf(temp_h5_path, "fixed"), df)

    df.to_hdf(temp_h5_path, key="table", mode="a", format="table")
    tm.assert_frame_equal(pd.read_hdf(temp_h5_path, "table"), df)


def test_string_column_literal_nan_and_real_nan(temp_hdfstore):
    # GH#9604 — companion to the index tests: ensure the symmetric nan_rep
    # substitution still works on a data column, i.e. a custom nan_rep lets
    # both a literal "nan" string and an actual NaN round-trip correctly.
    df = pd.DataFrame({"a": ["x", "nan", np.nan, "y"]}, index=["i1", "i2", "i3", "i4"])

    temp_hdfstore.append("df", df, nan_rep="_NA_")
    result = temp_hdfstore.select("df")

    tm.assert_frame_equal(result, df)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_index_real_na_roundtrips_fixed(temp_h5_path):
    # GH#9604 — a genuine missing value in a string Index round-trips in the
    # fixed format (previously it was read back as the literal string "nan").
    ser = pd.Series(range(3), index=pd.Index(["aaa", np.nan, "bbb"], dtype=str))

    ser.to_hdf(temp_h5_path, key="s", mode="w")
    tm.assert_series_equal(pd.read_hdf(temp_h5_path, "s"), ser)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_index_real_na_roundtrips_table(temp_hdfstore):
    # GH#9604 — same, table format.
    ser = pd.Series(range(3), index=pd.Index(["aaa", np.nan, "bbb"], dtype=str))

    temp_hdfstore.append("s", ser)
    tm.assert_series_equal(temp_hdfstore.select("s"), ser)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_index_na_read_from_file_without_sentinel_fixed(temp_hdfstore):
    # GH#9604 — pandas 3.0 stored a NaN in a string Index as the bare string
    # "nan" and read it back as missing. Those files carry no sentinel
    # attribute, so the reader has to keep substituting "nan" for them.
    ser = pd.Series(range(3), index=pd.Index(["aaa", np.nan, "bbb"], dtype=str))

    temp_hdfstore.put("s", ser, track_times=False)
    assert temp_hdfstore.get_storer("s").group.index[:].tolist() == [
        b"aaa",
        b"nan",
        b"bbb",
    ]
    del temp_hdfstore.get_storer("s").group.index._v_attrs.nan_rep

    tm.assert_series_equal(temp_hdfstore.get("s"), ser)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_index_na_read_from_file_without_sentinel_table(temp_hdfstore):
    # GH#9604 — same, table format.
    ser = pd.Series(range(3), index=pd.Index(["aaa", np.nan, "bbb"], dtype=str))

    temp_hdfstore.append("s", ser)
    del temp_hdfstore.get_storer("s").table.attrs.index_nan_rep

    tm.assert_series_equal(temp_hdfstore.select("s"), ser)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_index_append_to_file_without_sentinel(temp_hdfstore):
    # GH#9604 — appending to such a file adopts "nan" as the sentinel, so the
    # rows already stored and the appended missing values agree.
    ser1 = pd.Series(range(2), index=pd.Index(["aaa", np.nan], dtype=str))
    temp_hdfstore.append("s", ser1)
    del temp_hdfstore.get_storer("s").table.attrs.index_nan_rep

    ser2 = pd.Series(range(2, 4), index=pd.Index(["bbb", np.nan], dtype=str))
    temp_hdfstore.append("s", ser2)

    expected = pd.Series(
        range(4), index=pd.Index(["aaa", np.nan, "bbb", np.nan], dtype=str)
    )
    tm.assert_series_equal(temp_hdfstore.select("s"), expected)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_index_append_literal_nan_to_file_without_sentinel_raises(temp_hdfstore):
    # GH#9604 — a file written before the sentinel was persisted stores a NaN
    # as "nan", so a row labelled "nan" can no longer be appended to one: it
    # would be indistinguishable from those.
    temp_hdfstore.append(
        "s", pd.Series([0, 1], index=pd.Index(["aaa", np.nan], dtype=str))
    )
    del temp_hdfstore.get_storer("s").table.attrs.index_nan_rep

    ser2 = pd.Series([2], index=pd.Index(["nan"], dtype=str))
    with pytest.raises(ValueError, match="collides with the sentinel"):
        temp_hdfstore.append("s", ser2)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_index_append_na_to_file_without_missing_values(temp_hdfstore):
    # GH#9604 — the sentinel minted for the first missing value appended to such
    # a file has to dodge the values already stored, not just the ones in the
    # appended chunk: here "nan" and "_nan_" are both taken.
    temp_hdfstore.append(
        "s", pd.Series([0, 1], index=pd.Index(["_nan_", "bbbbbbbbbb"], dtype=str))
    )
    del temp_hdfstore.get_storer("s").table.attrs.index_nan_rep

    ser2 = pd.Series([2, 3], index=pd.Index(["nan", np.nan], dtype=str))
    temp_hdfstore.append("s", ser2)

    expected = pd.Series(
        range(4),
        index=pd.Index(["_nan_", "bbbbbbbbbb", "nan", np.nan], dtype=str),
    )
    tm.assert_series_equal(temp_hdfstore.select("s"), expected)


def test_string_index_append_literal_nan_to_file_without_missing_values(temp_hdfstore):
    # GH#9604 — but a pre-sentinel file that stores no "nan" at all has no
    # sentinel to protect, so appending that label still works as it did.
    temp_hdfstore.append(
        "s", pd.Series([0, 1], index=pd.Index(["aaa", "bbb"], dtype=str))
    )
    del temp_hdfstore.get_storer("s").table.attrs.index_nan_rep

    temp_hdfstore.append("s", pd.Series([2], index=pd.Index(["nan"], dtype=str)))

    expected = pd.Series(range(3), index=pd.Index(["aaa", "bbb", "nan"], dtype=str))
    tm.assert_series_equal(temp_hdfstore.select("s"), expected)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_index_real_na_and_literal_nan_roundtrip(temp_h5_path):
    # GH#9604 — a real NA and the literal string "nan" coexist in one string
    # Index and both round-trip: the NaN sentinel is chosen to avoid colliding
    # with any real value (here it must dodge "nan").
    ser = pd.Series(range(4), index=pd.Index(["nan", np.nan, "bbb", "ccc"], dtype=str))

    ser.to_hdf(temp_h5_path, key="fixed", mode="w")
    tm.assert_series_equal(pd.read_hdf(temp_h5_path, "fixed"), ser)

    ser.to_hdf(temp_h5_path, key="table", mode="a", format="table")
    tm.assert_series_equal(pd.read_hdf(temp_h5_path, "table"), ser)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
@pytest.mark.parametrize("fmt", ["fixed", "table"])
def test_string_index_all_na_roundtrips(temp_h5_path, fmt):
    # GH#9604 — an all-missing string Index round-trips with both its values
    # and its dtype. Inference cannot recover str from an all-NaN array, so
    # the read path pins the dtype instead of letting it degrade to object.
    ser = pd.Series(range(2), index=pd.Index([np.nan, np.nan], dtype=str))

    ser.to_hdf(temp_h5_path, key="s", mode="w", format=fmt)
    tm.assert_series_equal(pd.read_hdf(temp_h5_path, "s"), ser)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_index_all_na_multiindex_level_roundtrips(temp_h5_path):
    # GH#9604 — same for an all-missing string level of a MultiIndex.
    mi = pd.MultiIndex.from_arrays(
        [pd.Index([np.nan, np.nan], dtype=str), [1, 2]], names=["a", "b"]
    )
    ser = pd.Series(range(2), index=mi)

    ser.to_hdf(temp_h5_path, key="s", mode="w")
    tm.assert_series_equal(pd.read_hdf(temp_h5_path, "s"), ser)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_index_dtype_does_not_depend_on_slice(temp_h5_path):
    # GH#9604 — reading only the missing row of a partially-missing string
    # Index used to give an object-dtype index while the full read gave str,
    # so the index dtype depended on which rows were selected.
    ser = pd.Series(range(3), index=pd.Index(["aaa", np.nan, "bbb"], dtype=str))
    ser.to_hdf(temp_h5_path, key="s", mode="w", format="table")

    expected_dtype = ser.index.dtype

    result = pd.read_hdf(temp_h5_path, "s", start=1, stop=2)
    tm.assert_series_equal(result, ser.iloc[1:2])
    assert result.index.dtype == expected_dtype

    with pd.HDFStore(temp_h5_path, mode="r") as store:
        chunks = list(store.select("s", chunksize=1))
    assert [chunk.index.dtype for chunk in chunks] == [expected_dtype] * 3


def test_string_index_empty_selection_keeps_dtype(temp_h5_path):
    # GH#9604 — a selection that matches no rows has no string left to infer
    # from, so the index came back as object even for an index that never held
    # a missing value.
    df = pd.DataFrame({"v": [1, 2, 3]}, index=pd.Index(["a", "b", "c"], dtype=str))
    df.to_hdf(temp_h5_path, key="t", mode="w", format="table", data_columns=True)

    expected_dtype = df.index.dtype
    assert pd.read_hdf(temp_h5_path, "t").index.dtype == expected_dtype

    assert pd.read_hdf(temp_h5_path, "t", start=0, stop=0).index.dtype == expected_dtype
    with pd.HDFStore(temp_h5_path, mode="r") as store:
        assert store.select("t", where="v > 99").index.dtype == expected_dtype


def test_empty_string_index_roundtrips(temp_h5_path):
    # GH#9604 — same for a string Index that is empty to begin with.
    ser = pd.Series([], index=pd.Index([], dtype=str), dtype="int64")

    ser.to_hdf(temp_h5_path, key="s", mode="w")
    tm.assert_series_equal(pd.read_hdf(temp_h5_path, "s"), ser)


@pytest.mark.parametrize(
    "vals",
    [
        # slice lands on the wrong labels -> silently wrong index
        ["c", "d", "a", "b", "e", "f"],
        # slice leaves codes pointing past the truncated level -> ValueError
        ["a", "b", "c", "d", "e", "f"],
    ],
)
def test_fixed_multiindex_start_stop_matches_iloc(temp_h5_path, vals):
    # GH#68038 — a MultiIndex level holds the unique values, not one entry per
    # row, so start/stop must slice only the codes.
    mi = pd.MultiIndex.from_arrays([vals, vals], names=["x", "y"])
    ser = pd.Series(range(len(vals)), index=mi)
    ser.to_hdf(temp_h5_path, key="s", mode="w")

    result = pd.read_hdf(temp_h5_path, "s", start=2, stop=4)
    tm.assert_series_equal(result, ser.iloc[2:4])

    df = ser.to_frame("v")
    df.to_hdf(temp_h5_path, key="t", mode="w")
    tm.assert_frame_equal(pd.read_hdf(temp_h5_path, "t", start=2, stop=4), df.iloc[2:4])


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_index_real_na_multichunk_append(temp_hdfstore):
    # GH#9604 — a string Index with genuine missing values written across
    # several table-format appends keeps a single, stable NaN sentinel, so
    # every chunk's missing values round-trip (the sentinel used to be
    # recomputed per chunk and could silently reinterpret earlier rows).
    ser1 = pd.Series(range(2), index=pd.Index([np.nan, "x"], dtype=str))
    ser2 = pd.Series(range(2, 4), index=pd.Index(["y", np.nan], dtype=str))

    temp_hdfstore.append("s", ser1)
    temp_hdfstore.append("s", ser2)

    expected = pd.Series(
        range(4), index=pd.Index([np.nan, "x", "y", np.nan], dtype=str)
    )
    tm.assert_series_equal(temp_hdfstore.select("s"), expected)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_index_literal_nan_then_real_na_append(temp_hdfstore):
    # GH#9604 — a literal "nan" written first and a genuine missing value
    # appended later both round-trip: the sentinel minted for the second chunk
    # dodges the "nan" already stored (min_itemsize reserves room for it).
    ser1 = pd.Series(range(2), index=pd.Index(["nan", "x"], dtype=str))
    ser2 = pd.Series(range(2, 4), index=pd.Index([np.nan, "y"], dtype=str))

    temp_hdfstore.append("s", ser1, min_itemsize={"index": 5})
    temp_hdfstore.append("s", ser2)

    expected = pd.Series(range(4), index=pd.Index(["nan", "x", np.nan, "y"], dtype=str))
    tm.assert_series_equal(temp_hdfstore.select("s"), expected)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_index_append_value_colliding_with_sentinel_raises(temp_hdfstore):
    # GH#9604 — appending a real value equal to the NaN sentinel already in use
    # is refused loudly rather than silently corrupting the stored missing
    # values (the sentinel here is "nan", chosen for the first chunk).
    temp_hdfstore.append(
        "s", pd.Series([0, 1], index=pd.Index([np.nan, "aaaaaaa"], dtype=str))
    )

    ser2 = pd.Series([2, 3], index=pd.Index([np.nan, "nan"], dtype=str))
    with pytest.raises(ValueError, match="collides with the sentinel"):
        temp_hdfstore.append("s", ser2)


def test_string_index_literal_nan_multichunk_append_no_na(temp_hdfstore):
    # GH#9604 — a string Index that never has a missing value keeps no sentinel,
    # so a literal "nan" can be appended freely across chunks.
    ser1 = pd.Series(range(2), index=pd.Index(["nan", "x"], dtype=str))
    ser2 = pd.Series(range(2, 4), index=pd.Index(["nan", "z"], dtype=str))

    temp_hdfstore.append("s", ser1)
    temp_hdfstore.append("s", ser2)

    expected = pd.Series(range(4), index=pd.Index(["nan", "x", "nan", "z"], dtype=str))
    tm.assert_series_equal(temp_hdfstore.select("s"), expected)


def test_string_column_literal_nan(temp_h5_path):
    # GH#9604 — a literal "nan" string in a data column round-trips in the table
    # format. Data columns previously encoded missing values with the global
    # nan_rep ("nan"), so a real value equal to it was read back as missing.
    df = pd.DataFrame({"A": pd.Series(["nan", "aaa", "bbb"], dtype=str)})

    df.to_hdf(temp_h5_path, key="df", format="table")

    tm.assert_frame_equal(pd.read_hdf(temp_h5_path, "df"), df)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object column under infer_string=0",
)
def test_string_column_literal_nan_and_real_na(temp_h5_path):
    # GH#9604 — a literal "nan" and a genuine missing value coexist in one data
    # column and both round-trip (the per-column sentinel dodges "nan").
    df = pd.DataFrame({"A": pd.Series(["nan", np.nan, "bbb"], dtype=str)})

    df.to_hdf(temp_h5_path, key="df", format="table")

    tm.assert_frame_equal(pd.read_hdf(temp_h5_path, "df"), df)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object column under infer_string=0",
)
def test_string_data_column_literal_nan_and_real_na(temp_hdfstore):
    # GH#9604 — same for an indexable data column, which is also queryable.
    df = pd.DataFrame({"A": pd.Series(["nan", np.nan, "bbb"], dtype=str)})

    temp_hdfstore.append("df", df, data_columns=["A"])

    tm.assert_frame_equal(temp_hdfstore.select("df"), df)
    tm.assert_frame_equal(
        temp_hdfstore.select("df", "A == 'nan'"), df.iloc[[0]], check_index_type=False
    )


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object column under infer_string=0",
)
def test_string_column_literal_nan_append(temp_hdfstore):
    # GH#9604 — a literal "nan" written first and a genuine missing value
    # appended later both round-trip: the sentinel minted for the second chunk
    # avoids the value already stored.
    df1 = pd.DataFrame({"A": pd.Series(["nan", "wide_value"], dtype=str)})
    df2 = pd.DataFrame(
        {"A": pd.Series([np.nan, "y"], dtype=str, index=[2, 3])},
    )

    temp_hdfstore.append("df", df1)
    temp_hdfstore.append("df", df2)

    expected = pd.DataFrame(
        {"A": pd.Series(["nan", "wide_value", np.nan, "y"], dtype=str)}
    )
    tm.assert_frame_equal(temp_hdfstore.select("df"), expected)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object column under infer_string=0",
)
def test_string_column_append_value_colliding_with_sentinel_raises(temp_hdfstore):
    # GH#9604 — appending a real value equal to the NaN sentinel already in use
    # for the column would be read back as missing, so it is refused.
    df1 = pd.DataFrame({"A": pd.Series([np.nan, "b"], dtype=str)})
    df2 = pd.DataFrame({"A": pd.Series(["nan", "c"], dtype=str, index=[2, 3])})

    temp_hdfstore.append("df", df1)
    with pytest.raises(ValueError, match="collides with the sentinel"):
        temp_hdfstore.append("df", df2)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object column under infer_string=0",
)
def test_string_column_explicit_nan_rep_is_still_table_wide(temp_hdfstore):
    # GH#9604 — an explicit nan_rep keeps its documented meaning: it is the
    # on-disk representation of a missing value for the whole table, so a value
    # equal to it is read back as missing. Only the default is collision-free.
    df = pd.DataFrame({"A": pd.Series(["_NA_", np.nan, "bbb"], dtype=str)})

    temp_hdfstore.append("df", df, nan_rep="_NA_")

    expected = pd.DataFrame({"A": pd.Series([np.nan, np.nan, "bbb"], dtype=str)})
    tm.assert_frame_equal(temp_hdfstore.select("df"), expected)


def test_string_columns_sharing_a_block_literal_nan(temp_h5_path):
    # GH#9604 — object-dtype string columns share one stored block and therefore
    # one sentinel; a literal "nan" in either of them still round-trips. A
    # dtype=str column is an ExtensionArray and gets a block of its own, so this
    # deliberately uses object dtype to exercise the 2-D block.
    df = pd.DataFrame({"A": ["nan", np.nan], "B": ["nan", "y"]}, dtype=object)

    df.to_hdf(temp_h5_path, key="df", format="table")

    # check_dtype: an object column of strings is read back as str under
    # infer_string; the values are what this test is about
    tm.assert_frame_equal(pd.read_hdf(temp_h5_path, "df"), df, check_dtype=False)


def test_string_columns_sharing_a_block_collision_names_the_column(temp_hdfstore):
    # GH#9604 — the sentinel is minted per stored block, so the collision error
    # has to name the frame column that actually holds the offending value
    # rather than the internal block name.
    df1 = pd.DataFrame({"A": ["x", np.nan], "B": ["y", "z"]}, dtype=object)
    df2 = pd.DataFrame({"A": ["p", "q"], "B": ["nan", "r"]}, index=[2, 3], dtype=object)

    temp_hdfstore.append("df", df1)
    with pytest.raises(ValueError, match=r"in column \[B\]"):
        temp_hdfstore.append("df", df2)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object column under infer_string=0",
)
def test_string_column_append_needing_a_longer_sentinel_raises(temp_hdfstore):
    # GH#9604 — a stored literal "nan" forces the longer "_nan_" sentinel, which
    # may not fit a column sized from short strings. Refusing is the honest
    # outcome (the alternative is reading the stored "nan" back as missing), but
    # the room has to be reserved when the table is created.
    df1 = pd.DataFrame({"A": pd.Series(["nan", "abc"], dtype=str)})
    df2 = pd.DataFrame({"A": pd.Series([np.nan, "z"], dtype=str, index=[2, 3])})

    temp_hdfstore.append("df", df1)
    with pytest.raises(ValueError, match="NaN representation is too large"):
        temp_hdfstore.append("df", df2)

    temp_hdfstore.append("wide", df1, min_itemsize={"A": 5})
    temp_hdfstore.append("wide", df2)
    expected = pd.DataFrame({"A": pd.Series(["nan", "abc", np.nan, "z"], dtype=str)})
    tm.assert_frame_equal(temp_hdfstore.select("wide"), expected)


def test_string_column_append_to_file_with_only_the_mi_marker(temp_hdfstore):
    # GH#9604 — a file from an intermediate build that has the MultiIndex-level
    # marker but not the data-column one: the level keeps its per-level sentinel
    # while the data column stays on the legacy table-wide nan_rep.
    mi1 = pd.MultiIndex.from_arrays(
        [pd.Index(["nan", "wide_value"], dtype=str), [1, 2]], names=["s", "i"]
    )
    df1 = pd.DataFrame(
        {"A": pd.Series(["nan", "wide_value"], dtype=str).values}, index=mi1
    )
    temp_hdfstore.append("t", df1)
    storer = temp_hdfstore.get_storer("t")
    del storer.attrs.data_col_nan_rep
    storer.attrs.nan_rep = "nan"

    mi2 = pd.MultiIndex.from_arrays(
        [pd.Index([np.nan, "y"], dtype=str), [3, 4]], names=["s", "i"]
    )
    df2 = pd.DataFrame({"A": pd.Series([np.nan, "y"], dtype=str).values}, index=mi2)
    temp_hdfstore.append("t", df2)

    result = temp_hdfstore.select("t")
    # the level round-trips both the literal and the missing value ...
    assert list(result.index.get_level_values("s")) == [
        "nan",
        "wide_value",
        np.nan,
        "y",
    ]
    # ... while the data column reads the stored "nan" back as missing, as it
    # did before the per-column sentinel existed
    expected = pd.Series(
        [np.nan, "wide_value", np.nan, "y"], dtype=str, index=result.index, name="A"
    )
    tm.assert_series_equal(result["A"], expected)


def test_string_column_append_to_file_without_marker(temp_hdfstore):
    # GH#9604 — the per-column sentinel is honored on read only when the table
    # carries the marker attribute, which is written when the table is created.
    # Appending a missing value to a file written before the marker existed must
    # keep using the table-wide nan_rep, otherwise the sentinel would be read
    # back as a literal string.
    df1 = pd.DataFrame({"A": pd.Series(["nan", "wide_value"], dtype=str)})
    temp_hdfstore.append("df", df1)
    storer = temp_hdfstore.get_storer("df")
    del storer.attrs.data_col_nan_rep
    storer.attrs.nan_rep = "nan"  # what a pre-marker writer stored

    df2 = pd.DataFrame({"A": pd.Series([np.nan, "y"], dtype=str, index=[2, 3])})
    temp_hdfstore.append("df", df2)

    # the stored "nan" reads back as missing, as it did before the sentinel was
    # introduced, and so does the appended missing value
    expected = pd.DataFrame(
        {"A": pd.Series([np.nan, "wide_value", np.nan, "y"], dtype=str)}
    )
    tm.assert_frame_equal(temp_hdfstore.select("df"), expected)


@pytest.mark.parametrize("fmt", ["fixed", "table"])
def test_string_multiindex_level_literal_nan(temp_h5_path, fmt):
    # GH#9604 — a literal "nan" in a MultiIndex level round-trips in both
    # formats. In the fixed format the empty sentinel attribute is what keeps it
    # a string: an absent one marks a pre-3.1 file and means substitute.
    mi = pd.MultiIndex.from_arrays(
        [pd.Index(["nan", "a", "b"], dtype=str), [1, 2, 3]], names=["s", "i"]
    )
    ser = pd.Series(range(3), index=mi)

    ser.to_hdf(temp_h5_path, key="t", format=fmt)
    tm.assert_series_equal(pd.read_hdf(temp_h5_path, "t"), ser)


def test_string_multiindex_level_query_literal_nan(temp_hdfstore):
    # GH#9604 — a literal "nan" is stored as itself (not a sentinel), so it
    # remains queryable through a where clause on the level.
    mi = pd.MultiIndex.from_arrays(
        [pd.Index(["nan", "a", "b"], dtype=str), [1, 2, 3]], names=["s", "i"]
    )
    temp_hdfstore.append("t", pd.Series(range(3), index=mi))

    result = temp_hdfstore.select("t", where="s == 'nan'")
    assert len(result) == 1


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_multiindex_level_real_na(temp_h5_path):
    # GH#9604 — a genuine missing value in a MultiIndex level round-trips.
    mi = pd.MultiIndex.from_arrays(
        [pd.Index(["a", np.nan, "b"], dtype=str), [1, 2, 3]], names=["s", "i"]
    )
    ser = pd.Series(range(3), index=mi)

    ser.to_hdf(temp_h5_path, key="t", format="table")
    tm.assert_series_equal(pd.read_hdf(temp_h5_path, "t"), ser)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_multiindex_level_literal_and_real_na(temp_h5_path):
    # GH#9604 — a literal "nan" and a genuine missing value coexist in one
    # MultiIndex level and both round-trip (the per-level sentinel dodges "nan").
    mi = pd.MultiIndex.from_arrays(
        [pd.Index(["nan", np.nan, "b", "c"], dtype=str), [1, 2, 3, 4]], names=["s", "i"]
    )
    ser = pd.Series(range(4), index=mi)

    ser.to_hdf(temp_h5_path, key="t", format="table")
    tm.assert_series_equal(pd.read_hdf(temp_h5_path, "t"), ser)


def test_string_multiindex_level_literal_nan_append(temp_hdfstore):
    # GH#9604 — a literal "nan" in a MultiIndex level survives across appends.
    s1 = pd.Series(
        range(2),
        index=pd.MultiIndex.from_arrays(
            [pd.Index(["nan", "a"], dtype=str), [1, 2]], names=["s", "i"]
        ),
    )
    s2 = pd.Series(
        range(2, 4),
        index=pd.MultiIndex.from_arrays(
            [pd.Index(["b", "nan"], dtype=str), [3, 4]], names=["s", "i"]
        ),
    )

    temp_hdfstore.append("t", s1)
    temp_hdfstore.append("t", s2)

    result = temp_hdfstore.select("t")
    assert list(result.index.get_level_values("s")) == ["nan", "a", "b", "nan"]


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_multiindex_level_with_explicit_nan_rep(temp_hdfstore):
    # GH#9604 — a MultiIndex level is part of the index, so it keeps its own
    # sentinel even when the caller supplies a nan_rep for the data columns.
    mi = pd.MultiIndex.from_arrays(
        [pd.Index([np.nan, "_NA_"], dtype=str), [1, 2]], names=["s", "i"]
    )
    ser = pd.Series(range(2), index=mi)

    temp_hdfstore.append("t", ser, nan_rep="_NA_")

    tm.assert_series_equal(temp_hdfstore.select("t"), ser)


@pytest.mark.skipif(
    not using_string_dtype(),
    reason="a real NaN in dtype=str is an object/mixed Index under infer_string=0",
)
def test_string_multiindex_level_append_to_file_without_marker(temp_hdfstore):
    # GH#9604 — the per-level sentinel is honored on read only when the table
    # carries the marker attribute, which is written when the table is created.
    # Appending a missing value to a file written before the marker existed must
    # keep using the global nan_rep, otherwise the sentinel is read back as a
    # literal string.
    s1 = pd.Series(
        range(2),
        index=pd.MultiIndex.from_arrays(
            [pd.Index(["nan", "wide_value"], dtype=str), [1, 2]], names=["s", "i"]
        ),
    )
    temp_hdfstore.append("t", s1)
    del temp_hdfstore.get_storer("t").attrs.mi_level_nan_rep
    del temp_hdfstore.get_storer("t").attrs.data_col_nan_rep

    s2 = pd.Series(
        range(2, 4),
        index=pd.MultiIndex.from_arrays(
            [pd.Index([np.nan, "y"], dtype=str), [3, 4]], names=["s", "i"]
        ),
    )
    temp_hdfstore.append("t", s2)

    # the stored "nan" reads back as missing, as it did before the sentinel was
    # introduced, and so does the appended missing value
    expected = pd.Series(
        range(4),
        index=pd.MultiIndex.from_arrays(
            [
                pd.Index([np.nan, "wide_value", np.nan, "y"], dtype=str),
                [1, 2, 3, 4],
            ],
            names=["s", "i"],
        ),
    )
    tm.assert_series_equal(temp_hdfstore.select("t"), expected)


def test_api(temp_h5_path):
    # GH4584
    # API issue when to_hdf doesn't accept append AND format args
    path = temp_h5_path

    df = pd.DataFrame(range(20))
    df.iloc[:10].to_hdf(path, key="df", append=True, format="table")
    df.iloc[10:].to_hdf(path, key="df", append=True, format="table")
    tm.assert_frame_equal(pd.read_hdf(path, "df"), df)

    # append to False
    df.iloc[:10].to_hdf(path, key="df", append=False, format="table")
    df.iloc[10:].to_hdf(path, key="df", append=True, format="table")
    tm.assert_frame_equal(pd.read_hdf(path, "df"), df)


def test_api_append(temp_h5_path):
    path = temp_h5_path

    df = pd.DataFrame(range(20))
    df.iloc[:10].to_hdf(path, key="df", append=True)
    df.iloc[10:].to_hdf(path, key="df", append=True, format="table")
    tm.assert_frame_equal(pd.read_hdf(path, "df"), df)

    # append to False
    df.iloc[:10].to_hdf(path, key="df", append=False, format="table")
    df.iloc[10:].to_hdf(path, key="df", append=True)
    tm.assert_frame_equal(pd.read_hdf(path, "df"), df)


def test_api_2(temp_h5_path):
    df = pd.DataFrame(range(20))
    df.to_hdf(temp_h5_path, key="df", append=False, format="fixed")
    tm.assert_frame_equal(pd.read_hdf(temp_h5_path, "df"), df)

    df.to_hdf(temp_h5_path, key="df", append=False, format="f")
    tm.assert_frame_equal(pd.read_hdf(temp_h5_path, "df"), df)

    df.to_hdf(temp_h5_path, key="df", append=False)
    tm.assert_frame_equal(pd.read_hdf(temp_h5_path, "df"), df)

    df.to_hdf(temp_h5_path, key="df")
    tm.assert_frame_equal(pd.read_hdf(temp_h5_path, "df"), df)


def test_api_3(temp_hdfstore):
    df = pd.DataFrame(range(20))

    temp_hdfstore.append("df", df.iloc[:10], append=True, format="table")
    temp_hdfstore.append("df", df.iloc[10:], append=True, format="table")
    tm.assert_frame_equal(temp_hdfstore.select("df"), df)

    # append to False
    temp_hdfstore.remove("df")
    temp_hdfstore.append("df", df.iloc[:10], append=False, format="table")
    temp_hdfstore.append("df", df.iloc[10:], append=True, format="table")
    tm.assert_frame_equal(temp_hdfstore.select("df"), df)

    # formats
    temp_hdfstore.remove("df")
    temp_hdfstore.append("df", df.iloc[:10], append=False, format="table")
    temp_hdfstore.append("df", df.iloc[10:], append=True, format="table")
    tm.assert_frame_equal(temp_hdfstore.select("df"), df)

    temp_hdfstore.remove("df")
    temp_hdfstore.append("df", df.iloc[:10], append=False, format="table")
    temp_hdfstore.append("df", df.iloc[10:], append=True, format=None)
    tm.assert_frame_equal(temp_hdfstore.select("df"), df)


def test_api_invalid(temp_h5_path):
    path = temp_h5_path
    # Invalid.
    df = pd.DataFrame(
        1.1 * np.arange(120).reshape((30, 4)),
        columns=pd.Index(list("ABCD")),
        index=pd.Index([f"i-{i}" for i in range(30)]),
    )

    msg = "Can only append to Tables"

    with pytest.raises(ValueError, match=msg):
        df.to_hdf(path, key="df", append=True, format="f")

    with pytest.raises(ValueError, match=msg):
        df.to_hdf(path, key="df", append=True, format="fixed")

    msg = r"invalid HDFStore format specified \[foo\]"

    with pytest.raises(TypeError, match=msg):
        df.to_hdf(path, key="df", append=True, format="foo")

    with pytest.raises(TypeError, match=msg):
        df.to_hdf(path, key="df", append=False, format="foo")

    # File path doesn't exist
    path = ""
    msg = f"File {path} does not exist"

    with pytest.raises(FileNotFoundError, match=msg):
        pd.read_hdf(path, "df")


def test_get(temp_hdfstore):
    temp_hdfstore["a"] = pd.Series(
        np.arange(10, dtype=np.float64), index=pd.date_range("2020-01-01", periods=10)
    )
    left = temp_hdfstore.get("a")
    right = temp_hdfstore["a"]
    tm.assert_series_equal(left, right)

    left = temp_hdfstore.get("/a")
    right = temp_hdfstore["/a"]
    tm.assert_series_equal(left, right)

    with pytest.raises(KeyError, match="'No object named b in the file'"):
        temp_hdfstore.get("b")


def test_put_integer(temp_h5_path):
    # non-date, non-string index
    df = pd.DataFrame(np.random.default_rng(2).standard_normal((50, 100)))
    _check_roundtrip(df, tm.assert_frame_equal, temp_h5_path)


def test_table_values_dtypes_roundtrip(temp_hdfstore, using_infer_string):
    df1 = pd.DataFrame({"a": [1, 2, 3]}, dtype="f8")
    temp_hdfstore.append("df_f8", df1)
    tm.assert_series_equal(df1.dtypes, temp_hdfstore["df_f8"].dtypes)

    df2 = pd.DataFrame({"a": [1, 2, 3]}, dtype="i8")
    temp_hdfstore.append("df_i8", df2)
    tm.assert_series_equal(df2.dtypes, temp_hdfstore["df_i8"].dtypes)

    # incompatible dtype
    msg = re.escape(
        "Cannot serialize the column [a] "
        "because its data contents are not [float] "
        "but [integer] object dtype"
    )
    with pytest.raises(ValueError, match=msg):
        temp_hdfstore.append("df_i8", df1)

    # check creation/storage/retrieval of float32 (a bit hacky to
    # actually create them thought)
    df1 = pd.DataFrame(np.array([[1], [2], [3]], dtype="f4"), columns=["A"])
    temp_hdfstore.append("df_f4", df1)
    tm.assert_series_equal(df1.dtypes, temp_hdfstore["df_f4"].dtypes)
    assert df1.dtypes.iloc[0] == "float32"

    # check with mixed dtypes
    df1 = pd.DataFrame(
        {
            c: pd.Series(np.random.default_rng(2).integers(5), dtype=c)
            for c in ["float32", "float64", "int32", "int64", "int16", "int8"]
        }
    )
    df1["string"] = "foo"
    df1["float322"] = 1.0
    df1["float322"] = df1["float322"].astype("float32")
    df1["bool"] = df1["float32"] > 0
    df1["time_s_1"] = Timestamp("20130101").as_unit("s")
    df1["time_s_2"] = Timestamp("20130101 00:00:00").as_unit("s")
    df1["time_ms"] = Timestamp("20130101 00:00:00.000").as_unit("ms")
    df1["time_ns"] = Timestamp("20130102 00:00:00.000000000")

    temp_hdfstore.append("df_mixed_dtypes1", df1)
    result = temp_hdfstore.select("df_mixed_dtypes1").dtypes.value_counts()
    result.index = [str(i) for i in result.index]
    str_dtype = "str" if using_infer_string else "object"
    expected = pd.Series(
        {
            "float32": 2,
            "float64": 1,
            "int32": 1,
            "bool": 1,
            "int16": 1,
            "int8": 1,
            "int64": 1,
            str_dtype: 1,
            "datetime64[s]": 2,
            "datetime64[ms]": 1,
            "datetime64[ns]": 1,
        },
        name="count",
    )
    result = result.sort_index()
    expected = expected.sort_index()
    tm.assert_series_equal(result, expected)


def test_series(temp_h5_path):
    s = pd.Series(range(10), dtype="float64", index=[f"i_{i}" for i in range(10)])
    _check_roundtrip(s, tm.assert_series_equal, path=temp_h5_path)

    ts = pd.Series(
        np.arange(10, dtype=np.float64), index=pd.date_range("2020-01-01", periods=10)
    )
    _check_roundtrip(ts, tm.assert_series_equal, path=temp_h5_path)

    ts2 = pd.Series(ts.index, pd.Index(ts.index))
    _check_roundtrip(ts2, tm.assert_series_equal, path=temp_h5_path)

    ts3 = pd.Series(ts.values, pd.Index(np.asarray(ts.index)))
    _check_roundtrip(
        ts3, tm.assert_series_equal, path=temp_h5_path, check_index_type=False
    )


def test_float_index(temp_h5_path):
    # GH #454
    index = np.random.default_rng(2).standard_normal(10)
    s = pd.Series(np.random.default_rng(2).standard_normal(10), index=index)
    _check_roundtrip(s, tm.assert_series_equal, path=temp_h5_path)


def test_tuple_index(temp_h5_path, performance_warning):
    # GH #492
    col = np.arange(10)
    idx = [(0.0, 1.0), (2.0, 3.0), (4.0, 5.0)]
    data = np.random.default_rng(2).standard_normal(30).reshape((3, 10))
    DF = pd.DataFrame(data, index=idx, columns=col)

    msg = "your performance may suffer as PyTables will pickle object types"
    with tm.assert_produces_warning(performance_warning, match=msg):
        _check_roundtrip(DF, tm.assert_frame_equal, path=temp_h5_path)


@pytest.mark.filterwarnings("ignore::pandas.errors.PerformanceWarning")
def test_index_types(temp_h5_path):
    values = np.random.default_rng(2).standard_normal(2)

    func = lambda lhs, rhs: tm.assert_series_equal(lhs, rhs, check_index_type=True)

    ser = pd.Series(values, [0, "y"])
    _check_roundtrip(ser, func, path=temp_h5_path)

    ser = pd.Series(values, [datetime.datetime(2011, 1, 1), 0])
    _check_roundtrip(ser, func, path=temp_h5_path)

    ser = pd.Series(values, ["y", 0])
    _check_roundtrip(ser, func, path=temp_h5_path)

    ser = pd.Series(values, [datetime.date(2011, 1, 1), "a"])
    _check_roundtrip(ser, func, path=temp_h5_path)

    ser = pd.Series(values, [0, "y"])
    _check_roundtrip(ser, func, path=temp_h5_path)

    ser = pd.Series(values, [datetime.datetime(2011, 1, 1), 0])
    _check_roundtrip(ser, func, path=temp_h5_path)

    ser = pd.Series(values, ["y", 0])
    _check_roundtrip(ser, func, path=temp_h5_path)

    ser = pd.Series(values, [datetime.date(2011, 1, 1), "a"])
    _check_roundtrip(ser, func, path=temp_h5_path)

    ser = pd.Series(values, [1.23, "b"])
    _check_roundtrip(ser, func, path=temp_h5_path)

    ser = pd.Series(values, [1, 1.53])
    _check_roundtrip(ser, func, path=temp_h5_path)

    ser = pd.Series(values, [1, 5])
    _check_roundtrip(ser, func, path=temp_h5_path)

    dti = pd.DatetimeIndex(["2012-01-01", "2012-01-02"], dtype="M8[ns]")
    ser = pd.Series(values, index=dti)
    _check_roundtrip(ser, func, path=temp_h5_path)

    ser.index = ser.index.as_unit("s")
    _check_roundtrip(ser, func, path=temp_h5_path)


def test_timeseries_preepoch(temp_h5_path, request):
    dr = pd.bdate_range("1/1/1940", "1/1/1960")
    ts = pd.Series(np.random.default_rng(2).standard_normal(len(dr)), index=dr)
    try:
        _check_roundtrip(ts, tm.assert_series_equal, path=temp_h5_path)
    except OverflowError:
        if is_platform_windows():
            request.applymarker(
                pytest.mark.xfail("known failure on some windows platforms")
            )
        raise


@pytest.mark.parametrize(
    "compression", [False, pytest.param(True, marks=td.skip_if_windows)]
)
def test_frame(compression, temp_h5_path):
    df = pd.DataFrame(
        1.1 * np.arange(120).reshape((30, 4)),
        columns=pd.Index(list("ABCD")),
        index=pd.Index([f"i-{i}" for i in range(30)]),
    )

    # put in some random NAs
    df.iloc[0, 0] = np.nan
    df.iloc[5, 3] = np.nan

    _check_roundtrip_table(
        df, tm.assert_frame_equal, path=temp_h5_path, compression=compression
    )
    _check_roundtrip(
        df, tm.assert_frame_equal, path=temp_h5_path, compression=compression
    )

    tdf = pd.DataFrame(
        np.random.default_rng(2).standard_normal((10, 4)),
        columns=pd.Index(list("ABCD")),
        index=pd.date_range("2000-01-01", periods=10, freq="B"),
    )
    _check_roundtrip(
        tdf, tm.assert_frame_equal, path=temp_h5_path, compression=compression
    )

    with pd.HDFStore(temp_h5_path) as store:
        # not consolidated
        df["foo"] = np.random.default_rng(2).standard_normal(len(df))
        store["df"] = df
        recons = store["df"]
        assert recons._mgr.is_consolidated()

    # empty
    df2 = df[:0]
    # Prevent df2 from having index with inferred_type as string
    df2.index = pd.Index([])
    _check_roundtrip(df2[:0], tm.assert_frame_equal, path=temp_h5_path)


def test_empty_series_frame(temp_h5_path):
    s0 = pd.Series(dtype=object)
    s1 = pd.Series(name="myseries", dtype=object)
    df0 = pd.DataFrame()
    df1 = pd.DataFrame(index=["a", "b", "c"])
    df2 = pd.DataFrame(columns=["d", "e", "f"])

    _check_roundtrip(s0, tm.assert_series_equal, path=temp_h5_path)
    _check_roundtrip(s1, tm.assert_series_equal, path=temp_h5_path)
    _check_roundtrip(df0, tm.assert_frame_equal, path=temp_h5_path)
    _check_roundtrip(df1, tm.assert_frame_equal, path=temp_h5_path)
    _check_roundtrip(df2, tm.assert_frame_equal, path=temp_h5_path)


@pytest.mark.parametrize("dtype", [np.int64, np.float64, object, "m8[ns]", "M8[ns]"])
def test_empty_series(dtype, temp_h5_path):
    s = pd.Series(dtype=dtype)
    _check_roundtrip(s, tm.assert_series_equal, path=temp_h5_path)


def test_can_serialize_dates(temp_h5_path):
    rng = [x.date() for x in pd.bdate_range("1/1/2000", "1/30/2000")]
    frame = pd.DataFrame(
        np.random.default_rng(2).standard_normal((len(rng), 4)), index=rng
    )

    _check_roundtrip(frame, tm.assert_frame_equal, path=temp_h5_path)


def test_store_hierarchical(temp_h5_path, multiindex_dataframe_random_data):
    frame = multiindex_dataframe_random_data

    _check_roundtrip(frame, tm.assert_frame_equal, path=temp_h5_path)
    _check_roundtrip(frame.T, tm.assert_frame_equal, path=temp_h5_path)
    _check_roundtrip(frame["A"], tm.assert_series_equal, path=temp_h5_path)

    # check that the names are stored
    with pd.HDFStore(temp_h5_path) as store:
        store["frame"] = frame
        recons = store["frame"]
        tm.assert_frame_equal(recons, frame)


@pytest.mark.parametrize(
    "compression", [False, pytest.param(True, marks=td.skip_if_windows)]
)
def test_store_mixed(compression, temp_h5_path):
    def _make_one():
        df = pd.DataFrame(
            1.1 * np.arange(120).reshape((30, 4)),
            columns=pd.Index(list("ABCD")),
            index=pd.Index([f"i-{i}" for i in range(30)]),
        )
        df["obj1"] = "foo"
        df["obj2"] = "bar"
        df["bool1"] = df["A"] > 0
        df["bool2"] = df["B"] > 0
        df["int1"] = 1
        df["int2"] = 2
        return df._consolidate()

    df1 = _make_one()
    df2 = _make_one()

    _check_roundtrip(df1, tm.assert_frame_equal, path=temp_h5_path)
    _check_roundtrip(df2, tm.assert_frame_equal, path=temp_h5_path)

    with pd.HDFStore(temp_h5_path) as store:
        store["obj"] = df1
        tm.assert_frame_equal(store["obj"], df1)
        store["obj"] = df2
        tm.assert_frame_equal(store["obj"], df2)

    # check that can store Series of all of these types
    _check_roundtrip(
        df1["obj1"],
        tm.assert_series_equal,
        path=temp_h5_path,
        compression=compression,
    )
    _check_roundtrip(
        df1["bool1"],
        tm.assert_series_equal,
        path=temp_h5_path,
        compression=compression,
    )
    _check_roundtrip(
        df1["int1"],
        tm.assert_series_equal,
        path=temp_h5_path,
        compression=compression,
    )


def _check_roundtrip(obj, comparator, path, compression=False, **kwargs):
    options = {}
    if compression:
        # complevel is required for complib to take effect (GH#29310)
        options["complib"] = "blosc"
        options["complevel"] = 9

    with pd.HDFStore(path, "w", **options) as store:
        store["obj"] = obj
        retrieved = store["obj"]
        comparator(retrieved, obj, **kwargs)


def _check_roundtrip_table(obj, comparator, path, compression=False):
    options = {}
    if compression:
        # complevel is required for complib to take effect (GH#29310)
        options["complib"] = "blosc"
        options["complevel"] = 9

    with pd.HDFStore(path, "w", **options) as store:
        store.put("obj", obj, format="table", track_times=False)
        retrieved = store["obj"]

        comparator(retrieved, obj)


def test_unicode_index(temp_h5_path):
    unicode_values = ["\u03c3", "\u03c3\u03c3"]

    s = pd.Series(
        np.random.default_rng(2).standard_normal(len(unicode_values)),
        unicode_values,
    )
    _check_roundtrip(s, tm.assert_series_equal, path=temp_h5_path)


def test_unicode_longer_encoded(temp_hdfstore):
    # GH 11234
    char = "\u0394"
    df = pd.DataFrame({"A": [char]})
    temp_hdfstore.put("df", df, format="table", encoding="utf-8", track_times=False)
    result = temp_hdfstore.get("df")
    tm.assert_frame_equal(result, df)

    df = pd.DataFrame({"A": ["a", char], "B": ["b", "b"]})
    temp_hdfstore.remove("df")
    temp_hdfstore.put("df", df, format="table", encoding="utf-8", track_times=False)
    result = temp_hdfstore.get("df")
    tm.assert_frame_equal(result, df)


def test_store_datetime_mixed(temp_h5_path):
    df = pd.DataFrame({"a": [1, 2, 3], "b": [1.0, 2.0, 3.0], "c": ["a", "b", "c"]})
    ts = pd.Series(
        np.arange(10, dtype=np.float64), index=pd.date_range("2020-01-01", periods=10)
    )
    df["d"] = ts.index[:3]
    _check_roundtrip(df, tm.assert_frame_equal, path=temp_h5_path)


def test_round_trip_equals(temp_h5_path):
    # GH 9330
    df = pd.DataFrame({"B": [1, 2], "A": ["x", "y"]})

    df.to_hdf(temp_h5_path, key="df", format="table")
    other = pd.read_hdf(temp_h5_path, "df")
    tm.assert_frame_equal(df, other)
    assert df.equals(other)
    assert other.equals(df)


def test_infer_string_columns(temp_h5_path):
    df = pd.DataFrame(1, columns=list("ABCD"), index=list(range(10))).set_index(
        ["A", "B"]
    )
    expected = df.copy()
    df.to_hdf(temp_h5_path, key="df", format="table")

    result = pd.read_hdf(temp_h5_path, "df")
    tm.assert_frame_equal(result, expected)
