[SYNPY-1883] convert date to epoch ms#1429
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds the missing write-side handling for Synapse DATE columns by converting Python/pandas date-like values into epoch milliseconds before rows are uploaded (DataFrame, dict, upsert, and CSV paths), and documents the timezone policy for those conversions.
Changes:
- Add
_convert_df_date_cols_to_epoch_time(and CSV helpers) and wire the conversion into store/upsert flows. - Add
date_columns/date_formatparameters for CSV uploads so formatted date strings can be parsed then converted before upload. - Expand docs and tests (unit + integration) to cover tz-aware, naive, DST-crossing, and
datetime.dateinputs.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/synapseclient/mixins/unit_test_table_components.py | Adds unit tests covering DataFrame/CSV date parsing + epoch conversion and store_rows plumbing. |
| tests/integration/synapseclient/models/async/test_table_async.py | Adds integration coverage for end-to-end DATE storage from DataFrames and CSVs (tz-aware/naive/date objects). |
| synapseclient/models/table.py | Public API doc updates + new CSV parameters (date_columns, date_format) for store_rows. |
| synapseclient/models/mixins/table_components.py | Implements and integrates the conversion/parsing helpers into store/upsert logic. |
| synapseclient/core/utils.py | Clarifies and documents to_unix_epoch_time conversion rules (tz-aware vs naive vs date). |
| docs/tutorials/python/table.md | Adds a tutorial section explaining DATE semantics and examples for storing/querying datetimes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Updated code block formatting for Python example in table.md.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
merge develop branch
| ```python | ||
| df = pd.DataFrame( | ||
| { | ||
| "sample_id": ["S1", "S2"], | ||
| "collected_on": [ | ||
| datetime(2017, 2, 14, 11, 23), # winter in Los Angeles (PST, UTC-8) | ||
| datetime(2018, 10, 1, 9, 30), # summer in Los Angeles (PDT, UTC-7) | ||
| ], | ||
| } | ||
| ) | ||
|
|
||
| # Localize the values so each one carries its own timezone information. | ||
| # tz_localize looks up the UTC offset in effect on each value's own date: | ||
| # UTC-8 for the February value, UTC-7 for the October value. | ||
| df["collected_on"] = df["collected_on"].dt.tz_localize("America/Los_Angeles") | ||
|
|
||
| table.store_rows(values=df) | ||
|
|
||
| ``` |
| ~~~python | ||
| # DATE columns are returned as timezone-aware datetimes in UTC. Use tz_convert to view them on another wall clock: | ||
| results["collected_on"] = results["collected_on"].dt.tz_convert("America/Los_Angeles") | ||
| print(results) | ||
| ~~~ |
There was a problem hiding this comment.
nit: Swap to ` character instead of ~
| async def upsert_rows_async( | ||
| self, | ||
| values: Union[str, Dict[str, Any], DATA_FRAME_TYPE], | ||
| primary_keys: List[str], |
There was a problem hiding this comment.
Does this upsert method also need to get a
date_columns: Optional[List[str]] = None,
date_format: Optional[Union[str, Dict[str, str]]] = None,
set of parameters too?
A CSV is valid to be passed into this method.
| # check if the column holds mixed timezones/offsets by extracting the UTC | ||
| # offsets; null values have no offset to extract and are dropped so they | ||
| # don't get counted as a spurious second "offset" | ||
| offsets = df[col].str.extract(r"([Zz\+\-]\d{2}:?\d{2})$")[0].dropna().unique() |
There was a problem hiding this comment.
| offsets = df[col].str.extract(r"([Zz\+\-]\d{2}:?\d{2})$")[0].dropna().unique() | |
| offsets = df[col].astype("string").str.extract(r"([Zz\+\-]\d{2}:?\d{2})$")[0].dropna().unique() |
There is a small edge case here. To recreate this:
import pandas as pd
import io
from synapseclient import Synapse
from synapseclient.models.mixins.table_components import _parse_df_date_cols_to_datetime
syn = Synapse()
# mimic csv_to_pandas_df's read_csv(...).convert_dtypes()
df = pd.read_csv(io.StringIO("col1,date_col\na,\nb,\n")).convert_dtypes()
print(df["date_col"].dtype) # Int64
res = _parse_df_date_cols_to_datetime(df, date_columns=["date_col"], synapse_client=syn)
print(res["date_col"].dtype) # datetime64[ns] is what is expected here after patching
# -> AttributeError: Can only use .str accessor with string values! raise AttributeError("Can only use .str accessor with string values!")
AttributeError: Can only use .str accessor with string values!. Did you mean: 'std'?
BryanFauble
left a comment
There was a problem hiding this comment.
There are a few small considerations that are required - However, this is looking great.
| # ROW_ID/ROW_VERSION must stay as regular columns so they survive the | ||
| # round-trip to the temporary upload file when `date_columns` is used — | ||
| # dropping them would turn a row update into an append. | ||
| values = csv_to_pandas_df( |
There was a problem hiding this comment.
Shouldn't this use the same parameters as df.to_csv at line 4971? For example, this test currently fails:
async def test_store_rows_async_csv_date_cols_respects_non_default_separator(self):
# GIVEN a TAB-separated CSV and a matching csv_table_descriptor telling
# the client the file is tab-delimited
table = self.ClassForTest(id="syn123", name="test_table")
table._last_persistent_instance = self.ClassForTest(
id="syn123", name="test_table"
)
with tempfile.NamedTemporaryFile(
mode="w", suffix=".csv", delete=False
) as csv_file:
csv_file.write("col1\tdate_col\na\t01/15/2024\nb\t02/20/2024\n")
uploaded = {}
def capture_upload(**kwargs):
# The temp upload file is written with the descriptor's separator,
# so read it back with the same separator
uploaded["df"] = pd.read_csv(kwargs["path_to_csv"], sep="\t")
try:
with patch.object(
table, "_chunk_and_upload_csv", new_callable=AsyncMock
) as mock_upload:
mock_upload.side_effect = capture_upload
# WHEN the tab-delimited file is stored with date_columns
await table.store_rows_async(
values=csv_file.name,
date_columns=["date_col"],
date_format="%m/%d/%Y",
csv_table_descriptor=CsvTableDescriptor(separator="\t"),
synapse_client=self.syn,
)
# THEN the file is parsed with the tab separator, so date_col is
# found and converted to epoch ms — rather than the whole header
# collapsing into a single "col1\tdate_col" column (which raises a
# "date column(s) not present" ValueError)
expected_df = pd.DataFrame(
{
"col1": ["a", "b"],
"date_col": [
1705276800000,
1708387200000,
], # date columns are converted to epoch ms (midnight local timezone, unit tests run with TZ=UTC)
}
).convert_dtypes()
pd.testing.assert_frame_equal(
uploaded["df"], expected_df, check_dtype=False
)
finally:
os.remove(csv_file.name)
| df.to_csv( | ||
| temp_path, | ||
| index=False, | ||
| float_format="%.12g", |
There was a problem hiding this comment.
whats the reason for this?
| 3 S4 2017-02-14 07:00:00+00:00 | ||
| 4 S5 2017-02-14 19:23:00+00:00 | ||
| 5 S6 2018-10-01 16:30:00+00:00 | ||
| ``` |
There was a problem hiding this comment.
Are these correct Los Angeles times?

Problem:
Synapse DATE columns store an exact moment in time as an integer, milliseconds, since the Unix epoch. But the client only handled that conversion in one direction:
This hit every input type:
pandas DataFrames,dictandcsvfiles — and the gap was hidden because the integration test exercising itmanually pre-converted dates with utils.to_unix_epoch_time before calling store_rows, instead of relying on the client to do it.Also, there is unresolved timezone ambiguity. Miss document to explain how different datetime and date, including
tz-aware,naive,mixed timezonedatetime and plain date, are converted in the client. Also, csv file columns with mixed UTC offsets had no normalization rule either.Solution:
The fix adds the missing write-side conversion, plumbed into every path that sends rows to Synapse.
New conversion function _convert_df_date_cols_to_epoch_time auto-detects datetime/date columns and converts them via to_unix_epoch_time and is called in DataFrame, csv uploads and upserts.
New CSV parameters date_columns/date_format in
store_rows_asyncsince raw CSVs have no dtype info, the client needs to be told which columns are dates and how to parse them._parse_df_date_cols_to_datetime is introduced to parses the strings, _convert_csv_date_cols_to_epoch_time converts and writes a temp CSV that's actually uploadedin finally.
Explicit timezone policy: tz-aware datetimes convert to UTC exactly (recommended); naive datetimes/dates use the upload machine's current local timezone (documented DST caveat in to_unix_epoch_time's docstring; CSV columns with mixed offsets are normalized to UTC.
New tutorial section table.md covers tz-aware vs. naive datetimes, plain dates, CSV date columns, and querying back with convert_to_datetime=True.
Testing:
Three new integration tests in
test_table_async.pyupload DataFrames/CSVs with tz-aware, DST-crossing, naive, and date-object columns without any manual pre-conversion, verifying the exact epoch values land correctly .Unit tests for newly added functions and
store_row_asynchave been added.