Convert empty timestamp (Timestamp(0, 0)) to current timestamp on insert#11
Convert empty timestamp (Timestamp(0, 0)) to current timestamp on insert#11richardsimmonds wants to merge 1 commit into
Conversation
Native MongoDB replaces an empty timestamp value Timestamp(0, 0) in a top-level field of an inserted document with the current timestamp. DocumentDB previously stored the empty timestamp literally. Add ReplaceEmptyTimestampsWithCurrentTime to the insert preprocessing path (PreprocessInsertionDoc), which rewrites any top-level Timestamp(0, 0) field to the current time. Matching MongoDB semantics: - only top-level fields are converted (not nested documents or arrays) - the _id field is exempt and stored as-is - non-empty timestamps are unchanged The increment portion is populated from the clock's nanosecond component, consistent with the existing $currentDate update operator (HandleUpdateDollarCurrentDate). Add regression test insert_empty_timestamp_tests covering insert_one and batch insert paths, nested/array/_id exemptions, and validation that the converted value falls within the insert time window. Fixes documentdb#513 Signed-off-by: richardsimmonds <richardsimmonds314@gmail.com>
patty-chow
left a comment
There was a problem hiding this comment.
Review Summary
Verdict: LGTM — recommend merge (pending CI), with two non-blocking follow-ups noted below.
Correctness
- Placement of the rewrite in
PreprocessInsertionDocis right: it runs afterRewriteDocumentValueAddObjectIdand beforeComputeShardKeyHashForDocument/PgbsonGetDocumentId, so the shard key hash is computed over the converted value — the stored document and its hash stay consistent even if a timestamp field is part of the shard key. - All insert entry points funnel through this function: batch path (
DoMultiInsertWithoutTransactionId, insert.c:621) and single/retryable path (ProcessInsertion, insert.c:1003). The worker path (CallInsertWorkerForInsertOne→command_insert_worker) receives the already-preprocessed document, so conversion happens exactly once on the coordinator, and retryable writes return the retry record before preprocessing — no double-conversion or retry skew. - Detection scan + fast-path return of the original pgbson for the common case is the right shape; no extra allocation when no empty timestamps are present.
- Semantics match documented MongoDB behavior: top-level only,
_idexempt, non-empty timestamps untouched.
C safety
- No leaks or null derefs spotted. The writer-built pgbson is allocated in the caller's memory context with the same lifetime as the document
RewriteDocumentValueAddObjectIdalready produces there (including inside the batch path's subtransaction PG_TRY block). Stack iterators, no manual frees needed. - Symbols check out:
IdFieldStringViewvia commands_common.h (already included),bson_iter_key_string_view/StringViewEqualsvia bson_core.h,clock_gettimemirrors the compiling usage in bson_update_operators.c:603.
Tests
- Good coverage: insert_one, batch insert command, multi-field conversion,
_idexemption, nested/array non-conversion, non-empty passthrough. The clock-window assertion would genuinely fail on old code (stored t=0 falls outside the window), so this is a real regression test, not a tautology. - Test helpers mirror the established
test_update_currentDatepattern in bson_update_document_tests.sql (clock window + ±1s flake buffer). Unique collection id 1985100; schedule line reuses an existing parallel group — consistent with repo conventions.
Non-blocking follow-ups
- Upsert path gap — replace-style upserts (
UpsertDocument→InsertOrReplaceDocumentin update.c) don't go throughPreprocessInsertionDoc, so an upserted document withTimestamp(0,0)is stored literally. Native MongoDB converts on the upsert-insert path too. Worth a follow-up issue rather than scope-creeping this PR. - Increment semantics — MongoDB populates
iwith an ordinal counter (distinct value per replaced timestamp within a second); this PR storestv_nsec, matching the existing$currentDateimplementation's documented TODO. Consistent-with-codebase is the right call; just noting the known divergence.
DCO / scope
- Signed-off-by present on the single commit. Diff is minimal — one static function, call site, test, schedule line. No unrelated changes.
CI
- Status at review time: gateway tests in progress; the build/functional jobs (
functional-pr-gate, image build+test, build-documentdb-local) show skipped on this fork — so the self-review's "CI is the compile gate" assumption may not hold here. Recommend confirming at least one compile-exercising job actually ran green before merge.
| * Native Mongo compatibility: an empty timestamp value (Timestamp(0, 0)) | ||
| * in a top-level field is replaced with the current timestamp on insert. | ||
| */ | ||
| insertDoc = ReplaceEmptyTimestampsWithCurrentTime(insertDoc); |
There was a problem hiding this comment.
Placement is correct — conversion before ComputeShardKeyHashForDocument means the hash is computed over the converted value, keeping the stored document and shard key hash consistent.
One gap to track separately: replace-style upserts (UpsertDocument -> InsertOrReplaceDocument in update.c) don't funnel through PreprocessInsertionDoc, so an upserted document containing Timestamp(0,0) will still be stored literally. Native MongoDB converts on that path too. Suggest a follow-up issue rather than expanding this PR.
| bson_value_t currentTimestampValue = { 0 }; | ||
| currentTimestampValue.value_type = BSON_TYPE_TIMESTAMP; | ||
| currentTimestampValue.value.v_timestamp.timestamp = spec.tv_sec; | ||
| currentTimestampValue.value.v_timestamp.increment = spec.tv_nsec; |
There was a problem hiding this comment.
Non-blocking: native MongoDB sets i to an ordinal counter (so multiple empty timestamps replaced in the same second get distinct, ordered values); storing tv_nsec matches the existing $currentDate implementation (bson_update_operators.c:615, which carries the same TODO). Fine to keep consistent with the codebase — just flagging the known divergence so it's a deliberate choice.
| LANGUAGE plpgsql; | ||
|
|
||
| -- an empty timestamp in a top-level field is converted to the current timestamp on insert | ||
| SELECT test_insert_one_empty_timestamp('{ "_id": 1, "k": { "$timestamp": { "t": 0, "i": 0 } } }', '{ "_id": 1 }', ARRAY['k']::text[]); |
There was a problem hiding this comment.
Nice — this assertion is a genuine regression guard: old code stores t=0, which falls outside the clock window, so the test fails pre-fix. Helper pattern matches the established test_update_currentDate approach in bson_update_document_tests.sql.
Fixes documentdb#513
Problem
Native MongoDB replaces an empty timestamp value
Timestamp(0, 0)in a top-level field of an inserted document with the current timestamp. DocumentDB stored the empty timestamp literally:Change
Adds
ReplaceEmptyTimestampsWithCurrentTimeto the insert preprocessing path (PreprocessInsertionDocinpg_documentdb/src/commands/insert.c), which rewrites any top-levelTimestamp(0, 0)field to the current time. Matching MongoDB semantics:_idfield is exempt and stored as-isThe common case (no empty timestamps) takes a scan-only fast path and returns the original document without rewriting. The increment portion is populated from the clock's nanosecond component, consistent with the existing
$currentDateupdate operator (HandleUpdateDollarCurrentDate).Tests
New regression test
insert_empty_timestamp_tests(added tobasic_schedule) covering:insert_oneinsertcommand path_idexemption, nested-document and array non-conversiont: 0)Self-review
Checked: symbol usage verified against headers (
PgbsonWriterAppendValue,bson_iter_key_string_view,StringViewEquals,IdFieldStringView);clock_gettimeusage mirrors the compiling pattern inbson_update_operators.c; unique regress collection id (1985100); DCO sign-off present; diff is minimal (one C function + tests + schedule line). Known limitation: full extension build not run locally — CI is the authoritative compile/regress gate.