bulkcopy() silently truncates fractional seconds in string datetime values to microseconds
Mantenedores costumam responder em até 2 dias
Avaliação
- Dificuldade
- 4/5
- Tempo estimado
- 3-5 dias
- Facilidade para iniciantes
- 48/100
Direção de pesquisa
Start by tracing how Cursor.bulkcopy() parses string values into DATETIME2 and DATETIMEOFFSET, then compare that path with execute()/executemany() and the Arrow timestamp path. Reproduce with the supplied seven-digit fractional value and trailing-Z example, checking behavior on Python 3.10 and 3.11. Done means bulkcopy preserves all seven digits like executemany and accepts server-valid input, or rejects precision loss explicitly.
Escrita pelo modelo de indexação a partir do texto da issue.
Descrição
Summary
When a string is bound to a DATETIME2 column through Cursor.bulkcopy(), the Rust core parses it with Python's datetime.fromisoformat before sending it. datetime only holds microseconds, so a seven-digit fraction is truncated to six with no warning. The same string through execute()/executemany() is sent to the server as text and SQL Server stores all seven digits.
On Python 3.10 the same parser also rejects a trailing Z, which the server accepts. From 3.11 that case works. This is probably fine since python 3.10 is EOL in a few days, but I wanted to raise since your docs explicitly say you need >=3.10 for bulkcopy.
To reproduce
import uuid
import mssql_python
conn = mssql_python.connect(CONN_STR, autocommit=True)
cur = conn.cursor()
table = f"dbo.Repro_{uuid.uuid4().hex[:8]}"
cur.execute(f"CREATE TABLE {table} (lbl VARCHAR(20), dt DATETIME2)")
value = "2024-07-01 12:00:00.1234567"
cur.executemany(f"INSERT INTO {table} VALUES (?, ?)", [("executemany", value)])
cur.bulkcopy(table, [("bulkcopy", value)])
cur.execute(f"SELECT lbl, CONVERT(varchar(27), dt, 121) FROM {table}")
print(cur.fetchall())
cur.execute(f"DROP TABLE {table}")
Observed:
Python 3.11:
[('executemany', '2024-07-01 12:00:00.1234567'),
('bulkcopy', '2024-07-01 12:00:00.1234560')]
Python 3.10, where fromisoformat also rejects Z:
cur.bulkcopy(table, [("bulkcopy", "2024-07-01T12:00:00Z")])
ValueError: Invalid datetime format '2024-07-01T12:00:00Z'. Expected ISO format like
'2024-01-15 09:30:00' or '2024-01-15T09:30:00': ValueError: Invalid isoformat string: ...
Note:
bulkcopy_arrow() with a timestamp[ns] column stores all seven digits, into both DATETIME2 and DATETIMEOFFSET:
import pyarrow as pa
ns = pa.array([1719835200_123456700], pa.timestamp("ns")) # 2024-07-01 12:00:00.1234567
cur.bulkcopy_arrow(table, pa.table({"lbl": ["arrow"], "dt": ns}))
# -> '2024-07-01 12:00:00.1234567'
Expected behavior
bulkcopy() stores the same value as executemany() for the same input string. In particular, a DATETIME2(7) column receives all seven digits, and inputs that SQL Server accepts are not rejected on the client.
Suggested fixes, in order of preference
- Parse ISO 8601 strings in Rust directly into the TDS DATETIME2/DATETIMEOFFSET representation, which has 100 ns resolution, the same way the Arrow path already consumes nanosecond buffers. This also removes the dependence on the running interpreter's fromisoformat behaviour.
- Alternatively, send string values for datetime columns to the server as text and let it convert, matching the execute path.
- At minimum, raise when a fraction has more digits than can be represented instead of truncating. Silent data loss is the worst of the three outcomes.
- Linguagem predominante
- Python
- Estrelas
- 481
- Forks
- 62
- Merge médio
- 2d 14h
- PRs com merge (30d)
- 14
Preparar o ambiente
Inicia o contêiner de desenvolvimento do projeto no navegador, com a sua própria conta do GitHub.
- Sem Dockerfile nem arquivo Docker Compose
- Tem um modelo de pull request
- Ler o guia de contribuição
Primeiros passos
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Mais de microsoft/mssql-python
-
Make azure-identity an optional dependencyTalvez já em andamento @jahnvi480 assumiu há 2 dias. Abertaarea: connectivity-auth dependencies enhancement triage needed
Dificuldade 4/5 3-5 dias Facilidade para iniciantes 48/100
microsoft/mssql-python#862 · 3 comentários · 1 responsável ·
Mantenedores costumam responder em até 2 dias
-
triage needed
Dificuldade 5/5 Mais de uma semana Facilidade para iniciantes 38/100
microsoft/mssql-python#850 · 2 comentários · 1 reação ·
Mantenedores costumam responder em até 2 dias
-
bulkcopy() & bulkcopy_arrow() fail with Schannel AcquireCredentialsHandleW error 0x8009030D (SEC_E_UNKNOWN_CREDENTIALS)Talvez já em andamento @jahnvi480 assumiu há 10 dias. Abertaarea: bulk-copy bug inADO triage done
Dificuldade 4/5 3-5 dias Facilidade para iniciantes 20/100
microsoft/mssql-python#848 · 5 comentários · 1 responsável ·
Mantenedores costumam responder em até 2 dias
-
macOS: encrypted connections fail after Homebrew moved the `openssl` alias to `openssl@4`Talvez livre de novo @bewithgaurav assumiu há 30 dias e não há nenhum pull request aberto. Abertaarea: packaging-platform bug triage needed
Dificuldade 4/5 3-5 dias Facilidade para iniciantes 55/100
microsoft/mssql-python#826 · 2 comentários · 4 reações · 1 responsável ·
Mantenedores costumam responder em até 2 dias
-
Announcement: mssql-python is coming to Django through mssql-django 2.0Talvez livre de novo @bewithgaurav assumiu há 56 dias e não há nenhum pull request aberto. Abertaannouncement
microsoft/mssql-python#748 · 1 responsável ·
Mantenedores costumam responder em até 2 dias
Todas as issues de microsoft/mssql-python
Issues semelhantes
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 82/100
Mantenedores costumam responder em até 2 dias
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 86/100
-
Validate finite output clamping limits in StepPredictor.prepare_clamping_params to prevent silent NaN/Inf propagationTalvez já em andamento @muskan-creates352 assumiu hoje. Aberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 84/100
mllam/neural-lam#848 ·
Mantenedores costumam responder em até 2 dias
-
infra
Dificuldade 1/5 Menos de uma hora Facilidade para iniciantes 88/100
fab-geocommuns/RNB-coeur#1166 ·
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
deepspeedai/DeepSpeed#8951 ·
Mantenedores costumam responder em até 2 dias