Add TimestampWithOffset canonical extension type - #558

Merged
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go
Jul 19, 2026
Merged

Add TimestampWithOffset canonical extension type#558
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go

Conversation

@serramatutu

@serramatutuserramatutu commented Oct 30, 2025

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Implements the new arrow.timestamp_with_offset canonical extension type for arrow-go.

Rationale for this change

Be compatible with the Arrow columnar spec's arrow.timestamp_with_offset canonical extension type.

What changes are included in this PR?

Adds a TimestampWithOffset extension type. This type represents a timestamp column that stores a potentially different timezone offset per value: the timestamp is stored in UTC alongside the original timezone offset in minutes. The offset-in-minutes field can be primitive-, dictionary-, or run-end-encoded.

This PR has been decoupled from the inner-nullability parity work it was previously based on. Those comparison / JSON-marshaling changes now live in #918 and can be reviewed and merged independently. This PR is now scoped to just the extension type and touches only:

  • arrow/extensions/timestamp_with_offset.go (new)
  • arrow/extensions/timestamp_with_offset_test.go (new)
  • arrow/extensions/extensions.go (register the canonical type)

To keep this PR self-contained, the JSON round-trip test asserts the values the type guarantees (per-row instant, timezone offset, and validity) rather than array.RecordEqual, which would compare inner non-nullable struct child validity bitmaps — the parity concern tracked separately in #918.

Are these changes tested?

Yes — primitive/dictionary/run-end offset encodings, JSON marshal + round-trip, and IPC round-trip.

Are there any user-facing changes?

Yes, this is a new canonical extension type.

@felipecrv
felipecrv self-requested a review November 1, 2025 01:55
felipecrv added a commit to apache/arrow that referenced this pull request Dec 5, 2025
…48002)
### Rationale for this change
Closes#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: #44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from 95230ad to b9b8bf2CompareDecember 22, 2025 14:33
@serramatutuserramatutu changed the title [DRAFT] Add TimestampWithOffset extension typeAdd TimestampWithOffset extension typeDec 22, 2025
@serramatutu
serramatutu marked this pull request as ready for review December 22, 2025 14:42
@serramatutuserramatutu changed the title Add TimestampWithOffset extension typeAdd TimestampWithOffset canonical extension typeDec 22, 2025
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from b9b8bf2 to ccdd288CompareDecember 22, 2025 15:17
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +506 to +507
timestamps.UnsafeAppendBoolToBitmap(false)
offsets.UnsafeAppendBoolToBitmap(false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, these are non-nullable according to the spec.

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did change this but now the IPC roundtrip tests are failing because the internal count of nulls is different between the thing before IPC and after IPC.

To make them pass we'll need to relax the arrow.RecordEqual() to ignore NullN() when the field is not nullable. Alternatively we can change it so that an array builder sets nulls = 0 if the array is not nullable, regardless of what has been set before in calls to UnsafeAppendBoolToBitmap() etc

Related: https://lists.apache.org/thread/7gbqjwykh1ob3xbvwph3ljsdl5c7kxpd

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from ccdd288 to cee17a3CompareJanuary 30, 2026 12:21
Comment threadarrow/array/encoded.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +151 to +152
*arrow.Int8Type | *arrow.Int16Type | *arrow.Int32Type | *arrow.Int64Type |
*arrow.Uint8Type | *arrow.Uint16Type | *arrow.Uint32Type | *arrow.Uint64Type

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this and the DictIndexType are both identical, instead of duplicating this we should either create a single type constraint or embed one in the other:

typeTimestampWithOffsetRunEndsTypeinterface {
DictIndexType
}

My personal preference would be to have a single type though, but I'm not averse to having the two separate ones if necessary.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: 231cb1e

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I had to revert. Spec says those types are different:

Dict index is either signed or unsigned int of 8-64 bits

The index type of a Dictionary type can only be an integer type, preferably signed, with width 8 to 64 bits.

Run ends is a signed int of 16-64 bits

The run end type of a Run-End Encoded type can only be a signed integer type with width 16 to 64 bits.

https://arrow.apache.org/docs/format/Columnar.html#data-types

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from bc577e9 to 8a7d6d7CompareApril 15, 2026 11:05
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from f060911 to 9f931fbCompareApril 30, 2026 11:33
Mottl pushed a commit to Mottl/arrow that referenced this pull request May 26, 2026
…type (apache#48002)
### Rationale for this change
Closesapache#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: apache#44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from 49da897 to b6f73a4CompareJuly 2, 2026 18:11
@zeroshade
zeroshade requested review from Copilot, kou and lidavidmJuly 2, 2026 21:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the canonical extension type arrow.timestamp_with_offset to match the Arrow spec, and updates JSON marshaling/unmarshaling plus array/record equality behavior to better respect schema nullability (especially for nested types and IPC-vs-JSON roundtrips).

Changes:

  • Add TimestampWithOffsetType, TimestampWithOffsetArray, and a convenience builder supporting primitive/dictionary/REE encodings for per-row timezone offsets.
  • Update JSON marshaling (GetOneForMarshal) to take schema nullability into account, and adjust struct/record JSON unmarshaling to handle explicit null for required fields.
  • Update array/record/table equality logic to optionally ignore validity bitmaps when fields are considered non-nullable, and refresh test fixtures accordingly.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
internal/json/json.goAdd IsNullMessage helper for null detection.
internal/json/json_stdlib.goStdlib variant of IsNullMessage.
arrow/ipc/cmd/arrow-ls/main_test.goUpdate expected struct field nullability formatting in output.
arrow/internal/arrjson/arrjson_test.goUpdate expected JSON schema nullability for struct children.
arrow/internal/arrdata/arrdata.goUpdate struct test data to mark inner fields nullable and adjust validity masks.
arrow/extensions/variant.goUpdate GetOneForMarshal signature to include nullability.
arrow/extensions/uuid.goUpdate GetOneForMarshal signature + call sites.
arrow/extensions/uuid_test.goMark UUID field nullable in record-builder test.
arrow/extensions/timestamp_with_offset.goNew extension type implementation + builder/array logic.
arrow/extensions/timestamp_with_offset_test.goNew tests covering primitive/dict/REE encodings, JSON roundtrip, IPC roundtrip.
arrow/extensions/json.goMake JSON extension marshaling honor caller-provided nullability.
arrow/extensions/extensions.goRegister TimestampWithOffsetType as a canonical extension type.
arrow/extensions/bool8.goUpdate GetOneForMarshal signature to include nullability.
arrow/compute/vector_sort_test.goMark fields nullable in C++ parity tests (to align with new nullability-aware behavior).
arrow/array/util.goPass schema field nullability into GetOneForMarshal during Record-to-JSON.
arrow/array/util_test.goExercise record JSON roundtrip under nullable vs non-nullable schemas.
arrow/array/union.goMake union JSON marshaling and approx-equality respect child-field nullability.
arrow/array/timestamp.goUpdate GetOneForMarshal and equality to respect nullable option.
arrow/array/struct.goMake struct JSON marshaling/unmarshaling nullability-aware; adjust struct equality accordingly.
arrow/array/struct_test.goAdd coverage for explicit null in required struct fields.
arrow/array/string.goUpdate string array marshaling and equality to respect nullable option.
arrow/array/record.goMake record JSON unmarshaling treat explicit null in required fields as empty value.
arrow/array/record_test.goExtend record-builder tests for new null-handling + JSON roundtrip.
arrow/array/numeric_generic.goUpdate numeric marshaling and equality to respect nullable option.
arrow/array/null.goUpdate GetOneForMarshal signature.
arrow/array/map.goThread nullable option through map equality.
arrow/array/list.goUpdate list marshaling and equality to respect element-field nullability.
arrow/array/json_reader_test.goUpdate JSON reader test to pass nullable flag when marshaling one value.
arrow/array/interval.goUpdate interval marshaling and equality to respect nullable option.
arrow/array/float16.goUpdate GetOneForMarshal signature to include nullability.
arrow/array/fixedsize_binary.goUpdate fixed-size-binary marshaling and equality to respect nullable option.
arrow/array/fixed_size_list.goUpdate fixed-size-list marshaling and equality to respect element-field nullability.
arrow/array/extension.goThread nullable option through extension array equality and marshaling.
arrow/array/encoded.goUpdate run-end-encoded marshaling and equality to thread nullable option.
arrow/array/dictionary.goUpdate dictionary marshaling and equality to thread nullable option.
arrow/array/decimal256_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal128_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal.goUpdate decimal marshaling and equality to respect nullable option.
arrow/array/compare.goAdd nullable-aware equality plumbing and new APIs; thread schema nullability into comparisons.
arrow/array/compare_test.goAdd tests for WithNullable(false) behavior.
arrow/array/boolean.goUpdate boolean marshaling and equality to respect nullable option.
arrow/array/binary.goUpdate binary marshaling and equality to respect nullable option.
arrow/array.goChange GetOneForMarshal interface to accept nullability flag.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/record_test.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated

@serramatutuserramatutu left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zeroshade some comments:

Can you think of a way to decouple these 2 things (timestamp with offset and the inner nullability fixes)? So we can merge the extension type without having to wait on all the fixes for that and do that more carefully?

I'm sorry I left the branch based off of an old version of the inner nullability parity fixes. There are some differences with what I was doing in the new version of the JSON PR:

  • I ended up dropping the equalOpts because this is not what C++ does. It looks like they just validate the schemas on Validate() instead of making Equal() more lenient. I had not fixed that in that PR because there are some issues with the CSV reader (it calls Validate()) and it was becoming somewhat of a sprawl to fix it all. There are lots of behavior in arrow-go that never checks for the schema of structs.
  • I did not do the GetOneForMarshalNullable() and instead I changed the struct/record builder to check for IsValid() before calling GetOneForMarshal().

This commit adds a new `TimestampWithOffset` extension type that can be
used to represent timestamps with per-row timezone information. It
stores information in a `struct` with 2 fields, `timestamp=[T, "UTC"]`,
where `T` can be any `arrow.TimeUnit` and `offset_minutes=int16`, which
represents the offset in minutes from the UTC timestamp.
serramatutuand others added 13 commits July 9, 2026 15:29
Stop using `time=0` as a sentinel for null in `iterValues()`. Instead,
return a tuple of `(time.Time, bool)` where the bool indicates whether the
value is valid or not.
Added a test for it.
Start a run when none exists yet (e.g. leading null rows) instead of
continuing a nonexistent run, and only update lastOffset when a new run
starts. This stops a leading null from corrupting the run-ends/values
children and stops a null between equal offsets from splitting one
contiguous run into two. Add a noLastOffset sentinel, handle nil valids,
and add regression tests across all offset encodings.
Address roborev review findings on the run-end-encoded offset builder:
- AppendValues now treats a nil valids slice as all-valid so the parent
struct and its child builders stay the same length. Previously the
struct received zero slots while the children received len(values),
producing an inconsistent extension array.
- Override NewArray/NewExtensionArray to reset lastOffset to noLastOffset
on finalization, so a reused builder starts a fresh run instead of
continuing a run that belonged to the array just finalized (which could
emit run-ends without a matching value on a run-end-encoded offset).
- Add regression tests for nil validity and builder reuse across all
offset encodings.
Follow-up to the nil-validity fix: an empty (len 0) valids slice is also
all-valid, matching the convention of the other Arrow builders.
AppendValues now normalizes both nil and empty validity, and panics on a
genuine length mismatch (a non-empty valids whose length differs from
values).
Extend the validity regression test to cover the empty slice and add a
test asserting the length-mismatch panic.
Follow-up to the run-end-encoding fixes: AppendNull (and therefore the
UnmarshalOne(null) and AppendValueFromString(null) paths that call it)
appends a null offset run via the embedded struct builder but left
lastOffset stale. A subsequent value repeating the pre-null offset would
then continue the null run instead of starting its own, encoding that
value with the null offset run.
Override AppendNull/AppendNulls to reset lastOffset to noLastOffset, and
add a regression test for Append(value), AppendNull(), Append(same-offset
value) with run-end-encoded offsets.
Replace array.RecordEqual with a per-row check of the extension's
guarantees (instant, timezone offset, validity). RecordEqual compares the
inner non-nullable struct child validity bitmaps, which legitimately differ
between builder output (children valid under a null parent) and JSON reader
output (children null). That inner-nullability parity is tracked separately
and is out of scope for this extension type.
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch from 03b049f to 2ef684cCompareJuly 9, 2026 19:30
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu updated this PR to be only the timestamp with offset extension type

…rays
iterValues started the run-end tracker at physical index 0 and compared
the logical row against absolute run ends, ignoring the array offset. A
sliced array whose offset began inside a later run decoded offsets from
the wrong run in Values() and MarshalJSON(). Initialize the tracker from
GetPhysicalOffset() and advance using absolute positions (Offset()+i),
and add a regression test slicing an REE-encoded array across run
boundaries.
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu filed #918 to split out the nullability stuff

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
- timeFromFieldValues: derive the zone-name sign from the whole offset so
a sub-hour negative offset keeps its sign (e.g. -30 minutes now formats
as "UTC-00:30" instead of "UTC+00:30"; integer hours is 0 there and
cannot carry the sign). Add a regression test.
- MarshalJSON: store the timestamp by value instead of taking the address
of the range variable.
- Values: rename the loop variable so it no longer shadows the time
package.
Widen offsetMinutes to int before negating so an out-of-range int16
offset (e.g. math.MinInt16) cannot overflow when computing the absolute
value used for the zone name.
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
zeroshadeand others added 3 commits July 12, 2026 13:05
…erialize
Deserialize now rejects serialized metadata other than "" or "{}", matching the canonical extension spec (empty metadata) and the existing JSONType pattern. Adds a regression test.
Also documents (NOTE apache#918) that AppendNull writes a null into the non-nullable offset_minutes storage field; inner-field comparison/JSON parity is tracked in apache#918.
…set Deserialize
Adds TestTimestampWithOffsetTypeDeserializeInvalidStorage covering non-struct storage, wrong field count/names/order, non-UTC and empty timezone, nullable timestamp/offset fields, and invalid offset encodings (wrong primitive type, dictionary value not Int16, run-end-encoded value not Int16).
Comment on lines +49 to +52
// FIXME: Technically this should be non-nullable, but a Arrow IPC does not deserialize
// ValueNullable properly, so enforcing this here would always fail when reading from an IPC
// stream
// !offsetType.ValueNullable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty major right? Can we link this to a tracking issue? (Does integration catch this?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of what was pulled out into #918 and friends

@zeroshade
zeroshade merged commit c9791ae into apache:mainJul 19, 2026
23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@serramatutu@zeroshade@lidavidm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add TimestampWithOffset canonical extension type - #558

Merged
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go
Jul 19, 2026
Merged

Add TimestampWithOffset canonical extension type#558
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go

Conversation

@serramatutu

@serramatutuserramatutu commented Oct 30, 2025

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Implements the new arrow.timestamp_with_offset canonical extension type for arrow-go.

Rationale for this change

Be compatible with the Arrow columnar spec's arrow.timestamp_with_offset canonical extension type.

What changes are included in this PR?

Adds a TimestampWithOffset extension type. This type represents a timestamp column that stores a potentially different timezone offset per value: the timestamp is stored in UTC alongside the original timezone offset in minutes. The offset-in-minutes field can be primitive-, dictionary-, or run-end-encoded.

This PR has been decoupled from the inner-nullability parity work it was previously based on. Those comparison / JSON-marshaling changes now live in #918 and can be reviewed and merged independently. This PR is now scoped to just the extension type and touches only:

  • arrow/extensions/timestamp_with_offset.go (new)
  • arrow/extensions/timestamp_with_offset_test.go (new)
  • arrow/extensions/extensions.go (register the canonical type)

To keep this PR self-contained, the JSON round-trip test asserts the values the type guarantees (per-row instant, timezone offset, and validity) rather than array.RecordEqual, which would compare inner non-nullable struct child validity bitmaps — the parity concern tracked separately in #918.

Are these changes tested?

Yes — primitive/dictionary/run-end offset encodings, JSON marshal + round-trip, and IPC round-trip.

Are there any user-facing changes?

Yes, this is a new canonical extension type.

@felipecrv
felipecrv self-requested a review November 1, 2025 01:55
felipecrv added a commit to apache/arrow that referenced this pull request Dec 5, 2025
…48002)
### Rationale for this change
Closes#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: #44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from 95230ad to b9b8bf2CompareDecember 22, 2025 14:33
@serramatutuserramatutu changed the title [DRAFT] Add TimestampWithOffset extension typeAdd TimestampWithOffset extension typeDec 22, 2025
@serramatutu
serramatutu marked this pull request as ready for review December 22, 2025 14:42
@serramatutuserramatutu changed the title Add TimestampWithOffset extension typeAdd TimestampWithOffset canonical extension typeDec 22, 2025
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from b9b8bf2 to ccdd288CompareDecember 22, 2025 15:17
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +506 to +507
timestamps.UnsafeAppendBoolToBitmap(false)
offsets.UnsafeAppendBoolToBitmap(false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, these are non-nullable according to the spec.

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did change this but now the IPC roundtrip tests are failing because the internal count of nulls is different between the thing before IPC and after IPC.

To make them pass we'll need to relax the arrow.RecordEqual() to ignore NullN() when the field is not nullable. Alternatively we can change it so that an array builder sets nulls = 0 if the array is not nullable, regardless of what has been set before in calls to UnsafeAppendBoolToBitmap() etc

Related: https://lists.apache.org/thread/7gbqjwykh1ob3xbvwph3ljsdl5c7kxpd

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from ccdd288 to cee17a3CompareJanuary 30, 2026 12:21
Comment threadarrow/array/encoded.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +151 to +152
*arrow.Int8Type | *arrow.Int16Type | *arrow.Int32Type | *arrow.Int64Type |
*arrow.Uint8Type | *arrow.Uint16Type | *arrow.Uint32Type | *arrow.Uint64Type

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this and the DictIndexType are both identical, instead of duplicating this we should either create a single type constraint or embed one in the other:

typeTimestampWithOffsetRunEndsTypeinterface {
DictIndexType
}

My personal preference would be to have a single type though, but I'm not averse to having the two separate ones if necessary.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: 231cb1e

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I had to revert. Spec says those types are different:

Dict index is either signed or unsigned int of 8-64 bits

The index type of a Dictionary type can only be an integer type, preferably signed, with width 8 to 64 bits.

Run ends is a signed int of 16-64 bits

The run end type of a Run-End Encoded type can only be a signed integer type with width 16 to 64 bits.

https://arrow.apache.org/docs/format/Columnar.html#data-types

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from bc577e9 to 8a7d6d7CompareApril 15, 2026 11:05
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from f060911 to 9f931fbCompareApril 30, 2026 11:33
Mottl pushed a commit to Mottl/arrow that referenced this pull request May 26, 2026
…type (apache#48002)
### Rationale for this change
Closesapache#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: apache#44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from 49da897 to b6f73a4CompareJuly 2, 2026 18:11
@zeroshade
zeroshade requested review from Copilot, kou and lidavidmJuly 2, 2026 21:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the canonical extension type arrow.timestamp_with_offset to match the Arrow spec, and updates JSON marshaling/unmarshaling plus array/record equality behavior to better respect schema nullability (especially for nested types and IPC-vs-JSON roundtrips).

Changes:

  • Add TimestampWithOffsetType, TimestampWithOffsetArray, and a convenience builder supporting primitive/dictionary/REE encodings for per-row timezone offsets.
  • Update JSON marshaling (GetOneForMarshal) to take schema nullability into account, and adjust struct/record JSON unmarshaling to handle explicit null for required fields.
  • Update array/record/table equality logic to optionally ignore validity bitmaps when fields are considered non-nullable, and refresh test fixtures accordingly.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
internal/json/json.goAdd IsNullMessage helper for null detection.
internal/json/json_stdlib.goStdlib variant of IsNullMessage.
arrow/ipc/cmd/arrow-ls/main_test.goUpdate expected struct field nullability formatting in output.
arrow/internal/arrjson/arrjson_test.goUpdate expected JSON schema nullability for struct children.
arrow/internal/arrdata/arrdata.goUpdate struct test data to mark inner fields nullable and adjust validity masks.
arrow/extensions/variant.goUpdate GetOneForMarshal signature to include nullability.
arrow/extensions/uuid.goUpdate GetOneForMarshal signature + call sites.
arrow/extensions/uuid_test.goMark UUID field nullable in record-builder test.
arrow/extensions/timestamp_with_offset.goNew extension type implementation + builder/array logic.
arrow/extensions/timestamp_with_offset_test.goNew tests covering primitive/dict/REE encodings, JSON roundtrip, IPC roundtrip.
arrow/extensions/json.goMake JSON extension marshaling honor caller-provided nullability.
arrow/extensions/extensions.goRegister TimestampWithOffsetType as a canonical extension type.
arrow/extensions/bool8.goUpdate GetOneForMarshal signature to include nullability.
arrow/compute/vector_sort_test.goMark fields nullable in C++ parity tests (to align with new nullability-aware behavior).
arrow/array/util.goPass schema field nullability into GetOneForMarshal during Record-to-JSON.
arrow/array/util_test.goExercise record JSON roundtrip under nullable vs non-nullable schemas.
arrow/array/union.goMake union JSON marshaling and approx-equality respect child-field nullability.
arrow/array/timestamp.goUpdate GetOneForMarshal and equality to respect nullable option.
arrow/array/struct.goMake struct JSON marshaling/unmarshaling nullability-aware; adjust struct equality accordingly.
arrow/array/struct_test.goAdd coverage for explicit null in required struct fields.
arrow/array/string.goUpdate string array marshaling and equality to respect nullable option.
arrow/array/record.goMake record JSON unmarshaling treat explicit null in required fields as empty value.
arrow/array/record_test.goExtend record-builder tests for new null-handling + JSON roundtrip.
arrow/array/numeric_generic.goUpdate numeric marshaling and equality to respect nullable option.
arrow/array/null.goUpdate GetOneForMarshal signature.
arrow/array/map.goThread nullable option through map equality.
arrow/array/list.goUpdate list marshaling and equality to respect element-field nullability.
arrow/array/json_reader_test.goUpdate JSON reader test to pass nullable flag when marshaling one value.
arrow/array/interval.goUpdate interval marshaling and equality to respect nullable option.
arrow/array/float16.goUpdate GetOneForMarshal signature to include nullability.
arrow/array/fixedsize_binary.goUpdate fixed-size-binary marshaling and equality to respect nullable option.
arrow/array/fixed_size_list.goUpdate fixed-size-list marshaling and equality to respect element-field nullability.
arrow/array/extension.goThread nullable option through extension array equality and marshaling.
arrow/array/encoded.goUpdate run-end-encoded marshaling and equality to thread nullable option.
arrow/array/dictionary.goUpdate dictionary marshaling and equality to thread nullable option.
arrow/array/decimal256_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal128_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal.goUpdate decimal marshaling and equality to respect nullable option.
arrow/array/compare.goAdd nullable-aware equality plumbing and new APIs; thread schema nullability into comparisons.
arrow/array/compare_test.goAdd tests for WithNullable(false) behavior.
arrow/array/boolean.goUpdate boolean marshaling and equality to respect nullable option.
arrow/array/binary.goUpdate binary marshaling and equality to respect nullable option.
arrow/array.goChange GetOneForMarshal interface to accept nullability flag.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/record_test.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated

@serramatutuserramatutu left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zeroshade some comments:

Can you think of a way to decouple these 2 things (timestamp with offset and the inner nullability fixes)? So we can merge the extension type without having to wait on all the fixes for that and do that more carefully?

I'm sorry I left the branch based off of an old version of the inner nullability parity fixes. There are some differences with what I was doing in the new version of the JSON PR:

  • I ended up dropping the equalOpts because this is not what C++ does. It looks like they just validate the schemas on Validate() instead of making Equal() more lenient. I had not fixed that in that PR because there are some issues with the CSV reader (it calls Validate()) and it was becoming somewhat of a sprawl to fix it all. There are lots of behavior in arrow-go that never checks for the schema of structs.
  • I did not do the GetOneForMarshalNullable() and instead I changed the struct/record builder to check for IsValid() before calling GetOneForMarshal().

This commit adds a new `TimestampWithOffset` extension type that can be
used to represent timestamps with per-row timezone information. It
stores information in a `struct` with 2 fields, `timestamp=[T, "UTC"]`,
where `T` can be any `arrow.TimeUnit` and `offset_minutes=int16`, which
represents the offset in minutes from the UTC timestamp.
serramatutuand others added 13 commits July 9, 2026 15:29
Stop using `time=0` as a sentinel for null in `iterValues()`. Instead,
return a tuple of `(time.Time, bool)` where the bool indicates whether the
value is valid or not.
Added a test for it.
Start a run when none exists yet (e.g. leading null rows) instead of
continuing a nonexistent run, and only update lastOffset when a new run
starts. This stops a leading null from corrupting the run-ends/values
children and stops a null between equal offsets from splitting one
contiguous run into two. Add a noLastOffset sentinel, handle nil valids,
and add regression tests across all offset encodings.
Address roborev review findings on the run-end-encoded offset builder:
- AppendValues now treats a nil valids slice as all-valid so the parent
struct and its child builders stay the same length. Previously the
struct received zero slots while the children received len(values),
producing an inconsistent extension array.
- Override NewArray/NewExtensionArray to reset lastOffset to noLastOffset
on finalization, so a reused builder starts a fresh run instead of
continuing a run that belonged to the array just finalized (which could
emit run-ends without a matching value on a run-end-encoded offset).
- Add regression tests for nil validity and builder reuse across all
offset encodings.
Follow-up to the nil-validity fix: an empty (len 0) valids slice is also
all-valid, matching the convention of the other Arrow builders.
AppendValues now normalizes both nil and empty validity, and panics on a
genuine length mismatch (a non-empty valids whose length differs from
values).
Extend the validity regression test to cover the empty slice and add a
test asserting the length-mismatch panic.
Follow-up to the run-end-encoding fixes: AppendNull (and therefore the
UnmarshalOne(null) and AppendValueFromString(null) paths that call it)
appends a null offset run via the embedded struct builder but left
lastOffset stale. A subsequent value repeating the pre-null offset would
then continue the null run instead of starting its own, encoding that
value with the null offset run.
Override AppendNull/AppendNulls to reset lastOffset to noLastOffset, and
add a regression test for Append(value), AppendNull(), Append(same-offset
value) with run-end-encoded offsets.
Replace array.RecordEqual with a per-row check of the extension's
guarantees (instant, timezone offset, validity). RecordEqual compares the
inner non-nullable struct child validity bitmaps, which legitimately differ
between builder output (children valid under a null parent) and JSON reader
output (children null). That inner-nullability parity is tracked separately
and is out of scope for this extension type.
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch from 03b049f to 2ef684cCompareJuly 9, 2026 19:30
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu updated this PR to be only the timestamp with offset extension type

…rays
iterValues started the run-end tracker at physical index 0 and compared
the logical row against absolute run ends, ignoring the array offset. A
sliced array whose offset began inside a later run decoded offsets from
the wrong run in Values() and MarshalJSON(). Initialize the tracker from
GetPhysicalOffset() and advance using absolute positions (Offset()+i),
and add a regression test slicing an REE-encoded array across run
boundaries.
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu filed #918 to split out the nullability stuff

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
- timeFromFieldValues: derive the zone-name sign from the whole offset so
a sub-hour negative offset keeps its sign (e.g. -30 minutes now formats
as "UTC-00:30" instead of "UTC+00:30"; integer hours is 0 there and
cannot carry the sign). Add a regression test.
- MarshalJSON: store the timestamp by value instead of taking the address
of the range variable.
- Values: rename the loop variable so it no longer shadows the time
package.
Widen offsetMinutes to int before negating so an out-of-range int16
offset (e.g. math.MinInt16) cannot overflow when computing the absolute
value used for the zone name.
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
zeroshadeand others added 3 commits July 12, 2026 13:05
…erialize
Deserialize now rejects serialized metadata other than "" or "{}", matching the canonical extension spec (empty metadata) and the existing JSONType pattern. Adds a regression test.
Also documents (NOTE apache#918) that AppendNull writes a null into the non-nullable offset_minutes storage field; inner-field comparison/JSON parity is tracked in apache#918.
…set Deserialize
Adds TestTimestampWithOffsetTypeDeserializeInvalidStorage covering non-struct storage, wrong field count/names/order, non-UTC and empty timezone, nullable timestamp/offset fields, and invalid offset encodings (wrong primitive type, dictionary value not Int16, run-end-encoded value not Int16).
Comment on lines +49 to +52
// FIXME: Technically this should be non-nullable, but a Arrow IPC does not deserialize
// ValueNullable properly, so enforcing this here would always fail when reading from an IPC
// stream
// !offsetType.ValueNullable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty major right? Can we link this to a tracking issue? (Does integration catch this?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of what was pulled out into #918 and friends

@zeroshade
zeroshade merged commit c9791ae into apache:mainJul 19, 2026
23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@serramatutu@zeroshade@lidavidm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add TimestampWithOffset canonical extension type - #558

Merged
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go
Jul 19, 2026
Merged

Add TimestampWithOffset canonical extension type#558
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go

Conversation

@serramatutu

@serramatutuserramatutu commented Oct 30, 2025

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Implements the new arrow.timestamp_with_offset canonical extension type for arrow-go.

Rationale for this change

Be compatible with the Arrow columnar spec's arrow.timestamp_with_offset canonical extension type.

What changes are included in this PR?

Adds a TimestampWithOffset extension type. This type represents a timestamp column that stores a potentially different timezone offset per value: the timestamp is stored in UTC alongside the original timezone offset in minutes. The offset-in-minutes field can be primitive-, dictionary-, or run-end-encoded.

This PR has been decoupled from the inner-nullability parity work it was previously based on. Those comparison / JSON-marshaling changes now live in #918 and can be reviewed and merged independently. This PR is now scoped to just the extension type and touches only:

  • arrow/extensions/timestamp_with_offset.go (new)
  • arrow/extensions/timestamp_with_offset_test.go (new)
  • arrow/extensions/extensions.go (register the canonical type)

To keep this PR self-contained, the JSON round-trip test asserts the values the type guarantees (per-row instant, timezone offset, and validity) rather than array.RecordEqual, which would compare inner non-nullable struct child validity bitmaps — the parity concern tracked separately in #918.

Are these changes tested?

Yes — primitive/dictionary/run-end offset encodings, JSON marshal + round-trip, and IPC round-trip.

Are there any user-facing changes?

Yes, this is a new canonical extension type.

@felipecrv
felipecrv self-requested a review November 1, 2025 01:55
felipecrv added a commit to apache/arrow that referenced this pull request Dec 5, 2025
…48002)
### Rationale for this change
Closes#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: #44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from 95230ad to b9b8bf2CompareDecember 22, 2025 14:33
@serramatutuserramatutu changed the title [DRAFT] Add TimestampWithOffset extension typeAdd TimestampWithOffset extension typeDec 22, 2025
@serramatutu
serramatutu marked this pull request as ready for review December 22, 2025 14:42
@serramatutuserramatutu changed the title Add TimestampWithOffset extension typeAdd TimestampWithOffset canonical extension typeDec 22, 2025
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from b9b8bf2 to ccdd288CompareDecember 22, 2025 15:17
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +506 to +507
timestamps.UnsafeAppendBoolToBitmap(false)
offsets.UnsafeAppendBoolToBitmap(false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, these are non-nullable according to the spec.

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did change this but now the IPC roundtrip tests are failing because the internal count of nulls is different between the thing before IPC and after IPC.

To make them pass we'll need to relax the arrow.RecordEqual() to ignore NullN() when the field is not nullable. Alternatively we can change it so that an array builder sets nulls = 0 if the array is not nullable, regardless of what has been set before in calls to UnsafeAppendBoolToBitmap() etc

Related: https://lists.apache.org/thread/7gbqjwykh1ob3xbvwph3ljsdl5c7kxpd

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from ccdd288 to cee17a3CompareJanuary 30, 2026 12:21
Comment threadarrow/array/encoded.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +151 to +152
*arrow.Int8Type | *arrow.Int16Type | *arrow.Int32Type | *arrow.Int64Type |
*arrow.Uint8Type | *arrow.Uint16Type | *arrow.Uint32Type | *arrow.Uint64Type

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this and the DictIndexType are both identical, instead of duplicating this we should either create a single type constraint or embed one in the other:

typeTimestampWithOffsetRunEndsTypeinterface {
DictIndexType
}

My personal preference would be to have a single type though, but I'm not averse to having the two separate ones if necessary.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: 231cb1e

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I had to revert. Spec says those types are different:

Dict index is either signed or unsigned int of 8-64 bits

The index type of a Dictionary type can only be an integer type, preferably signed, with width 8 to 64 bits.

Run ends is a signed int of 16-64 bits

The run end type of a Run-End Encoded type can only be a signed integer type with width 16 to 64 bits.

https://arrow.apache.org/docs/format/Columnar.html#data-types

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from bc577e9 to 8a7d6d7CompareApril 15, 2026 11:05
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from f060911 to 9f931fbCompareApril 30, 2026 11:33
Mottl pushed a commit to Mottl/arrow that referenced this pull request May 26, 2026
…type (apache#48002)
### Rationale for this change
Closesapache#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: apache#44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from 49da897 to b6f73a4CompareJuly 2, 2026 18:11
@zeroshade
zeroshade requested review from Copilot, kou and lidavidmJuly 2, 2026 21:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the canonical extension type arrow.timestamp_with_offset to match the Arrow spec, and updates JSON marshaling/unmarshaling plus array/record equality behavior to better respect schema nullability (especially for nested types and IPC-vs-JSON roundtrips).

Changes:

  • Add TimestampWithOffsetType, TimestampWithOffsetArray, and a convenience builder supporting primitive/dictionary/REE encodings for per-row timezone offsets.
  • Update JSON marshaling (GetOneForMarshal) to take schema nullability into account, and adjust struct/record JSON unmarshaling to handle explicit null for required fields.
  • Update array/record/table equality logic to optionally ignore validity bitmaps when fields are considered non-nullable, and refresh test fixtures accordingly.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
internal/json/json.goAdd IsNullMessage helper for null detection.
internal/json/json_stdlib.goStdlib variant of IsNullMessage.
arrow/ipc/cmd/arrow-ls/main_test.goUpdate expected struct field nullability formatting in output.
arrow/internal/arrjson/arrjson_test.goUpdate expected JSON schema nullability for struct children.
arrow/internal/arrdata/arrdata.goUpdate struct test data to mark inner fields nullable and adjust validity masks.
arrow/extensions/variant.goUpdate GetOneForMarshal signature to include nullability.
arrow/extensions/uuid.goUpdate GetOneForMarshal signature + call sites.
arrow/extensions/uuid_test.goMark UUID field nullable in record-builder test.
arrow/extensions/timestamp_with_offset.goNew extension type implementation + builder/array logic.
arrow/extensions/timestamp_with_offset_test.goNew tests covering primitive/dict/REE encodings, JSON roundtrip, IPC roundtrip.
arrow/extensions/json.goMake JSON extension marshaling honor caller-provided nullability.
arrow/extensions/extensions.goRegister TimestampWithOffsetType as a canonical extension type.
arrow/extensions/bool8.goUpdate GetOneForMarshal signature to include nullability.
arrow/compute/vector_sort_test.goMark fields nullable in C++ parity tests (to align with new nullability-aware behavior).
arrow/array/util.goPass schema field nullability into GetOneForMarshal during Record-to-JSON.
arrow/array/util_test.goExercise record JSON roundtrip under nullable vs non-nullable schemas.
arrow/array/union.goMake union JSON marshaling and approx-equality respect child-field nullability.
arrow/array/timestamp.goUpdate GetOneForMarshal and equality to respect nullable option.
arrow/array/struct.goMake struct JSON marshaling/unmarshaling nullability-aware; adjust struct equality accordingly.
arrow/array/struct_test.goAdd coverage for explicit null in required struct fields.
arrow/array/string.goUpdate string array marshaling and equality to respect nullable option.
arrow/array/record.goMake record JSON unmarshaling treat explicit null in required fields as empty value.
arrow/array/record_test.goExtend record-builder tests for new null-handling + JSON roundtrip.
arrow/array/numeric_generic.goUpdate numeric marshaling and equality to respect nullable option.
arrow/array/null.goUpdate GetOneForMarshal signature.
arrow/array/map.goThread nullable option through map equality.
arrow/array/list.goUpdate list marshaling and equality to respect element-field nullability.
arrow/array/json_reader_test.goUpdate JSON reader test to pass nullable flag when marshaling one value.
arrow/array/interval.goUpdate interval marshaling and equality to respect nullable option.
arrow/array/float16.goUpdate GetOneForMarshal signature to include nullability.
arrow/array/fixedsize_binary.goUpdate fixed-size-binary marshaling and equality to respect nullable option.
arrow/array/fixed_size_list.goUpdate fixed-size-list marshaling and equality to respect element-field nullability.
arrow/array/extension.goThread nullable option through extension array equality and marshaling.
arrow/array/encoded.goUpdate run-end-encoded marshaling and equality to thread nullable option.
arrow/array/dictionary.goUpdate dictionary marshaling and equality to thread nullable option.
arrow/array/decimal256_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal128_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal.goUpdate decimal marshaling and equality to respect nullable option.
arrow/array/compare.goAdd nullable-aware equality plumbing and new APIs; thread schema nullability into comparisons.
arrow/array/compare_test.goAdd tests for WithNullable(false) behavior.
arrow/array/boolean.goUpdate boolean marshaling and equality to respect nullable option.
arrow/array/binary.goUpdate binary marshaling and equality to respect nullable option.
arrow/array.goChange GetOneForMarshal interface to accept nullability flag.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/record_test.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated

@serramatutuserramatutu left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zeroshade some comments:

Can you think of a way to decouple these 2 things (timestamp with offset and the inner nullability fixes)? So we can merge the extension type without having to wait on all the fixes for that and do that more carefully?

I'm sorry I left the branch based off of an old version of the inner nullability parity fixes. There are some differences with what I was doing in the new version of the JSON PR:

  • I ended up dropping the equalOpts because this is not what C++ does. It looks like they just validate the schemas on Validate() instead of making Equal() more lenient. I had not fixed that in that PR because there are some issues with the CSV reader (it calls Validate()) and it was becoming somewhat of a sprawl to fix it all. There are lots of behavior in arrow-go that never checks for the schema of structs.
  • I did not do the GetOneForMarshalNullable() and instead I changed the struct/record builder to check for IsValid() before calling GetOneForMarshal().

This commit adds a new `TimestampWithOffset` extension type that can be
used to represent timestamps with per-row timezone information. It
stores information in a `struct` with 2 fields, `timestamp=[T, "UTC"]`,
where `T` can be any `arrow.TimeUnit` and `offset_minutes=int16`, which
represents the offset in minutes from the UTC timestamp.
serramatutuand others added 13 commits July 9, 2026 15:29
Stop using `time=0` as a sentinel for null in `iterValues()`. Instead,
return a tuple of `(time.Time, bool)` where the bool indicates whether the
value is valid or not.
Added a test for it.
Start a run when none exists yet (e.g. leading null rows) instead of
continuing a nonexistent run, and only update lastOffset when a new run
starts. This stops a leading null from corrupting the run-ends/values
children and stops a null between equal offsets from splitting one
contiguous run into two. Add a noLastOffset sentinel, handle nil valids,
and add regression tests across all offset encodings.
Address roborev review findings on the run-end-encoded offset builder:
- AppendValues now treats a nil valids slice as all-valid so the parent
struct and its child builders stay the same length. Previously the
struct received zero slots while the children received len(values),
producing an inconsistent extension array.
- Override NewArray/NewExtensionArray to reset lastOffset to noLastOffset
on finalization, so a reused builder starts a fresh run instead of
continuing a run that belonged to the array just finalized (which could
emit run-ends without a matching value on a run-end-encoded offset).
- Add regression tests for nil validity and builder reuse across all
offset encodings.
Follow-up to the nil-validity fix: an empty (len 0) valids slice is also
all-valid, matching the convention of the other Arrow builders.
AppendValues now normalizes both nil and empty validity, and panics on a
genuine length mismatch (a non-empty valids whose length differs from
values).
Extend the validity regression test to cover the empty slice and add a
test asserting the length-mismatch panic.
Follow-up to the run-end-encoding fixes: AppendNull (and therefore the
UnmarshalOne(null) and AppendValueFromString(null) paths that call it)
appends a null offset run via the embedded struct builder but left
lastOffset stale. A subsequent value repeating the pre-null offset would
then continue the null run instead of starting its own, encoding that
value with the null offset run.
Override AppendNull/AppendNulls to reset lastOffset to noLastOffset, and
add a regression test for Append(value), AppendNull(), Append(same-offset
value) with run-end-encoded offsets.
Replace array.RecordEqual with a per-row check of the extension's
guarantees (instant, timezone offset, validity). RecordEqual compares the
inner non-nullable struct child validity bitmaps, which legitimately differ
between builder output (children valid under a null parent) and JSON reader
output (children null). That inner-nullability parity is tracked separately
and is out of scope for this extension type.
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch from 03b049f to 2ef684cCompareJuly 9, 2026 19:30
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu updated this PR to be only the timestamp with offset extension type

…rays
iterValues started the run-end tracker at physical index 0 and compared
the logical row against absolute run ends, ignoring the array offset. A
sliced array whose offset began inside a later run decoded offsets from
the wrong run in Values() and MarshalJSON(). Initialize the tracker from
GetPhysicalOffset() and advance using absolute positions (Offset()+i),
and add a regression test slicing an REE-encoded array across run
boundaries.
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu filed #918 to split out the nullability stuff

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
- timeFromFieldValues: derive the zone-name sign from the whole offset so
a sub-hour negative offset keeps its sign (e.g. -30 minutes now formats
as "UTC-00:30" instead of "UTC+00:30"; integer hours is 0 there and
cannot carry the sign). Add a regression test.
- MarshalJSON: store the timestamp by value instead of taking the address
of the range variable.
- Values: rename the loop variable so it no longer shadows the time
package.
Widen offsetMinutes to int before negating so an out-of-range int16
offset (e.g. math.MinInt16) cannot overflow when computing the absolute
value used for the zone name.
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
zeroshadeand others added 3 commits July 12, 2026 13:05
…erialize
Deserialize now rejects serialized metadata other than "" or "{}", matching the canonical extension spec (empty metadata) and the existing JSONType pattern. Adds a regression test.
Also documents (NOTE apache#918) that AppendNull writes a null into the non-nullable offset_minutes storage field; inner-field comparison/JSON parity is tracked in apache#918.
…set Deserialize
Adds TestTimestampWithOffsetTypeDeserializeInvalidStorage covering non-struct storage, wrong field count/names/order, non-UTC and empty timezone, nullable timestamp/offset fields, and invalid offset encodings (wrong primitive type, dictionary value not Int16, run-end-encoded value not Int16).
Comment on lines +49 to +52
// FIXME: Technically this should be non-nullable, but a Arrow IPC does not deserialize
// ValueNullable properly, so enforcing this here would always fail when reading from an IPC
// stream
// !offsetType.ValueNullable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty major right? Can we link this to a tracking issue? (Does integration catch this?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of what was pulled out into #918 and friends

@zeroshade
zeroshade merged commit c9791ae into apache:mainJul 19, 2026
23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@serramatutu@zeroshade@lidavidm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add TimestampWithOffset canonical extension type - #558

Merged
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go
Jul 19, 2026
Merged

Add TimestampWithOffset canonical extension type#558
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go

Conversation

@serramatutu

@serramatutuserramatutu commented Oct 30, 2025

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Implements the new arrow.timestamp_with_offset canonical extension type for arrow-go.

Rationale for this change

Be compatible with the Arrow columnar spec's arrow.timestamp_with_offset canonical extension type.

What changes are included in this PR?

Adds a TimestampWithOffset extension type. This type represents a timestamp column that stores a potentially different timezone offset per value: the timestamp is stored in UTC alongside the original timezone offset in minutes. The offset-in-minutes field can be primitive-, dictionary-, or run-end-encoded.

This PR has been decoupled from the inner-nullability parity work it was previously based on. Those comparison / JSON-marshaling changes now live in #918 and can be reviewed and merged independently. This PR is now scoped to just the extension type and touches only:

  • arrow/extensions/timestamp_with_offset.go (new)
  • arrow/extensions/timestamp_with_offset_test.go (new)
  • arrow/extensions/extensions.go (register the canonical type)

To keep this PR self-contained, the JSON round-trip test asserts the values the type guarantees (per-row instant, timezone offset, and validity) rather than array.RecordEqual, which would compare inner non-nullable struct child validity bitmaps — the parity concern tracked separately in #918.

Are these changes tested?

Yes — primitive/dictionary/run-end offset encodings, JSON marshal + round-trip, and IPC round-trip.

Are there any user-facing changes?

Yes, this is a new canonical extension type.

@felipecrv
felipecrv self-requested a review November 1, 2025 01:55
felipecrv added a commit to apache/arrow that referenced this pull request Dec 5, 2025
…48002)
### Rationale for this change
Closes#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: #44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from 95230ad to b9b8bf2CompareDecember 22, 2025 14:33
@serramatutuserramatutu changed the title [DRAFT] Add TimestampWithOffset extension typeAdd TimestampWithOffset extension typeDec 22, 2025
@serramatutu
serramatutu marked this pull request as ready for review December 22, 2025 14:42
@serramatutuserramatutu changed the title Add TimestampWithOffset extension typeAdd TimestampWithOffset canonical extension typeDec 22, 2025
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from b9b8bf2 to ccdd288CompareDecember 22, 2025 15:17
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +506 to +507
timestamps.UnsafeAppendBoolToBitmap(false)
offsets.UnsafeAppendBoolToBitmap(false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, these are non-nullable according to the spec.

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did change this but now the IPC roundtrip tests are failing because the internal count of nulls is different between the thing before IPC and after IPC.

To make them pass we'll need to relax the arrow.RecordEqual() to ignore NullN() when the field is not nullable. Alternatively we can change it so that an array builder sets nulls = 0 if the array is not nullable, regardless of what has been set before in calls to UnsafeAppendBoolToBitmap() etc

Related: https://lists.apache.org/thread/7gbqjwykh1ob3xbvwph3ljsdl5c7kxpd

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from ccdd288 to cee17a3CompareJanuary 30, 2026 12:21
Comment threadarrow/array/encoded.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +151 to +152
*arrow.Int8Type | *arrow.Int16Type | *arrow.Int32Type | *arrow.Int64Type |
*arrow.Uint8Type | *arrow.Uint16Type | *arrow.Uint32Type | *arrow.Uint64Type

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this and the DictIndexType are both identical, instead of duplicating this we should either create a single type constraint or embed one in the other:

typeTimestampWithOffsetRunEndsTypeinterface {
DictIndexType
}

My personal preference would be to have a single type though, but I'm not averse to having the two separate ones if necessary.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: 231cb1e

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I had to revert. Spec says those types are different:

Dict index is either signed or unsigned int of 8-64 bits

The index type of a Dictionary type can only be an integer type, preferably signed, with width 8 to 64 bits.

Run ends is a signed int of 16-64 bits

The run end type of a Run-End Encoded type can only be a signed integer type with width 16 to 64 bits.

https://arrow.apache.org/docs/format/Columnar.html#data-types

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from bc577e9 to 8a7d6d7CompareApril 15, 2026 11:05
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from f060911 to 9f931fbCompareApril 30, 2026 11:33
Mottl pushed a commit to Mottl/arrow that referenced this pull request May 26, 2026
…type (apache#48002)
### Rationale for this change
Closesapache#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: apache#44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from 49da897 to b6f73a4CompareJuly 2, 2026 18:11
@zeroshade
zeroshade requested review from Copilot, kou and lidavidmJuly 2, 2026 21:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the canonical extension type arrow.timestamp_with_offset to match the Arrow spec, and updates JSON marshaling/unmarshaling plus array/record equality behavior to better respect schema nullability (especially for nested types and IPC-vs-JSON roundtrips).

Changes:

  • Add TimestampWithOffsetType, TimestampWithOffsetArray, and a convenience builder supporting primitive/dictionary/REE encodings for per-row timezone offsets.
  • Update JSON marshaling (GetOneForMarshal) to take schema nullability into account, and adjust struct/record JSON unmarshaling to handle explicit null for required fields.
  • Update array/record/table equality logic to optionally ignore validity bitmaps when fields are considered non-nullable, and refresh test fixtures accordingly.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
internal/json/json.goAdd IsNullMessage helper for null detection.
internal/json/json_stdlib.goStdlib variant of IsNullMessage.
arrow/ipc/cmd/arrow-ls/main_test.goUpdate expected struct field nullability formatting in output.
arrow/internal/arrjson/arrjson_test.goUpdate expected JSON schema nullability for struct children.
arrow/internal/arrdata/arrdata.goUpdate struct test data to mark inner fields nullable and adjust validity masks.
arrow/extensions/variant.goUpdate GetOneForMarshal signature to include nullability.
arrow/extensions/uuid.goUpdate GetOneForMarshal signature + call sites.
arrow/extensions/uuid_test.goMark UUID field nullable in record-builder test.
arrow/extensions/timestamp_with_offset.goNew extension type implementation + builder/array logic.
arrow/extensions/timestamp_with_offset_test.goNew tests covering primitive/dict/REE encodings, JSON roundtrip, IPC roundtrip.
arrow/extensions/json.goMake JSON extension marshaling honor caller-provided nullability.
arrow/extensions/extensions.goRegister TimestampWithOffsetType as a canonical extension type.
arrow/extensions/bool8.goUpdate GetOneForMarshal signature to include nullability.
arrow/compute/vector_sort_test.goMark fields nullable in C++ parity tests (to align with new nullability-aware behavior).
arrow/array/util.goPass schema field nullability into GetOneForMarshal during Record-to-JSON.
arrow/array/util_test.goExercise record JSON roundtrip under nullable vs non-nullable schemas.
arrow/array/union.goMake union JSON marshaling and approx-equality respect child-field nullability.
arrow/array/timestamp.goUpdate GetOneForMarshal and equality to respect nullable option.
arrow/array/struct.goMake struct JSON marshaling/unmarshaling nullability-aware; adjust struct equality accordingly.
arrow/array/struct_test.goAdd coverage for explicit null in required struct fields.
arrow/array/string.goUpdate string array marshaling and equality to respect nullable option.
arrow/array/record.goMake record JSON unmarshaling treat explicit null in required fields as empty value.
arrow/array/record_test.goExtend record-builder tests for new null-handling + JSON roundtrip.
arrow/array/numeric_generic.goUpdate numeric marshaling and equality to respect nullable option.
arrow/array/null.goUpdate GetOneForMarshal signature.
arrow/array/map.goThread nullable option through map equality.
arrow/array/list.goUpdate list marshaling and equality to respect element-field nullability.
arrow/array/json_reader_test.goUpdate JSON reader test to pass nullable flag when marshaling one value.
arrow/array/interval.goUpdate interval marshaling and equality to respect nullable option.
arrow/array/float16.goUpdate GetOneForMarshal signature to include nullability.
arrow/array/fixedsize_binary.goUpdate fixed-size-binary marshaling and equality to respect nullable option.
arrow/array/fixed_size_list.goUpdate fixed-size-list marshaling and equality to respect element-field nullability.
arrow/array/extension.goThread nullable option through extension array equality and marshaling.
arrow/array/encoded.goUpdate run-end-encoded marshaling and equality to thread nullable option.
arrow/array/dictionary.goUpdate dictionary marshaling and equality to thread nullable option.
arrow/array/decimal256_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal128_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal.goUpdate decimal marshaling and equality to respect nullable option.
arrow/array/compare.goAdd nullable-aware equality plumbing and new APIs; thread schema nullability into comparisons.
arrow/array/compare_test.goAdd tests for WithNullable(false) behavior.
arrow/array/boolean.goUpdate boolean marshaling and equality to respect nullable option.
arrow/array/binary.goUpdate binary marshaling and equality to respect nullable option.
arrow/array.goChange GetOneForMarshal interface to accept nullability flag.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/record_test.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated

@serramatutuserramatutu left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zeroshade some comments:

Can you think of a way to decouple these 2 things (timestamp with offset and the inner nullability fixes)? So we can merge the extension type without having to wait on all the fixes for that and do that more carefully?

I'm sorry I left the branch based off of an old version of the inner nullability parity fixes. There are some differences with what I was doing in the new version of the JSON PR:

  • I ended up dropping the equalOpts because this is not what C++ does. It looks like they just validate the schemas on Validate() instead of making Equal() more lenient. I had not fixed that in that PR because there are some issues with the CSV reader (it calls Validate()) and it was becoming somewhat of a sprawl to fix it all. There are lots of behavior in arrow-go that never checks for the schema of structs.
  • I did not do the GetOneForMarshalNullable() and instead I changed the struct/record builder to check for IsValid() before calling GetOneForMarshal().

This commit adds a new `TimestampWithOffset` extension type that can be
used to represent timestamps with per-row timezone information. It
stores information in a `struct` with 2 fields, `timestamp=[T, "UTC"]`,
where `T` can be any `arrow.TimeUnit` and `offset_minutes=int16`, which
represents the offset in minutes from the UTC timestamp.
serramatutuand others added 13 commits July 9, 2026 15:29
Stop using `time=0` as a sentinel for null in `iterValues()`. Instead,
return a tuple of `(time.Time, bool)` where the bool indicates whether the
value is valid or not.
Added a test for it.
Start a run when none exists yet (e.g. leading null rows) instead of
continuing a nonexistent run, and only update lastOffset when a new run
starts. This stops a leading null from corrupting the run-ends/values
children and stops a null between equal offsets from splitting one
contiguous run into two. Add a noLastOffset sentinel, handle nil valids,
and add regression tests across all offset encodings.
Address roborev review findings on the run-end-encoded offset builder:
- AppendValues now treats a nil valids slice as all-valid so the parent
struct and its child builders stay the same length. Previously the
struct received zero slots while the children received len(values),
producing an inconsistent extension array.
- Override NewArray/NewExtensionArray to reset lastOffset to noLastOffset
on finalization, so a reused builder starts a fresh run instead of
continuing a run that belonged to the array just finalized (which could
emit run-ends without a matching value on a run-end-encoded offset).
- Add regression tests for nil validity and builder reuse across all
offset encodings.
Follow-up to the nil-validity fix: an empty (len 0) valids slice is also
all-valid, matching the convention of the other Arrow builders.
AppendValues now normalizes both nil and empty validity, and panics on a
genuine length mismatch (a non-empty valids whose length differs from
values).
Extend the validity regression test to cover the empty slice and add a
test asserting the length-mismatch panic.
Follow-up to the run-end-encoding fixes: AppendNull (and therefore the
UnmarshalOne(null) and AppendValueFromString(null) paths that call it)
appends a null offset run via the embedded struct builder but left
lastOffset stale. A subsequent value repeating the pre-null offset would
then continue the null run instead of starting its own, encoding that
value with the null offset run.
Override AppendNull/AppendNulls to reset lastOffset to noLastOffset, and
add a regression test for Append(value), AppendNull(), Append(same-offset
value) with run-end-encoded offsets.
Replace array.RecordEqual with a per-row check of the extension's
guarantees (instant, timezone offset, validity). RecordEqual compares the
inner non-nullable struct child validity bitmaps, which legitimately differ
between builder output (children valid under a null parent) and JSON reader
output (children null). That inner-nullability parity is tracked separately
and is out of scope for this extension type.
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch from 03b049f to 2ef684cCompareJuly 9, 2026 19:30
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu updated this PR to be only the timestamp with offset extension type

…rays
iterValues started the run-end tracker at physical index 0 and compared
the logical row against absolute run ends, ignoring the array offset. A
sliced array whose offset began inside a later run decoded offsets from
the wrong run in Values() and MarshalJSON(). Initialize the tracker from
GetPhysicalOffset() and advance using absolute positions (Offset()+i),
and add a regression test slicing an REE-encoded array across run
boundaries.
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu filed #918 to split out the nullability stuff

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
- timeFromFieldValues: derive the zone-name sign from the whole offset so
a sub-hour negative offset keeps its sign (e.g. -30 minutes now formats
as "UTC-00:30" instead of "UTC+00:30"; integer hours is 0 there and
cannot carry the sign). Add a regression test.
- MarshalJSON: store the timestamp by value instead of taking the address
of the range variable.
- Values: rename the loop variable so it no longer shadows the time
package.
Widen offsetMinutes to int before negating so an out-of-range int16
offset (e.g. math.MinInt16) cannot overflow when computing the absolute
value used for the zone name.
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
zeroshadeand others added 3 commits July 12, 2026 13:05
…erialize
Deserialize now rejects serialized metadata other than "" or "{}", matching the canonical extension spec (empty metadata) and the existing JSONType pattern. Adds a regression test.
Also documents (NOTE apache#918) that AppendNull writes a null into the non-nullable offset_minutes storage field; inner-field comparison/JSON parity is tracked in apache#918.
…set Deserialize
Adds TestTimestampWithOffsetTypeDeserializeInvalidStorage covering non-struct storage, wrong field count/names/order, non-UTC and empty timezone, nullable timestamp/offset fields, and invalid offset encodings (wrong primitive type, dictionary value not Int16, run-end-encoded value not Int16).
Comment on lines +49 to +52
// FIXME: Technically this should be non-nullable, but a Arrow IPC does not deserialize
// ValueNullable properly, so enforcing this here would always fail when reading from an IPC
// stream
// !offsetType.ValueNullable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty major right? Can we link this to a tracking issue? (Does integration catch this?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of what was pulled out into #918 and friends

@zeroshade
zeroshade merged commit c9791ae into apache:mainJul 19, 2026
23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@serramatutu@zeroshade@lidavidm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add TimestampWithOffset canonical extension type - #558

Merged
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go
Jul 19, 2026
Merged

Add TimestampWithOffset canonical extension type#558
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go

Conversation

@serramatutu

@serramatutuserramatutu commented Oct 30, 2025

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Implements the new arrow.timestamp_with_offset canonical extension type for arrow-go.

Rationale for this change

Be compatible with the Arrow columnar spec's arrow.timestamp_with_offset canonical extension type.

What changes are included in this PR?

Adds a TimestampWithOffset extension type. This type represents a timestamp column that stores a potentially different timezone offset per value: the timestamp is stored in UTC alongside the original timezone offset in minutes. The offset-in-minutes field can be primitive-, dictionary-, or run-end-encoded.

This PR has been decoupled from the inner-nullability parity work it was previously based on. Those comparison / JSON-marshaling changes now live in #918 and can be reviewed and merged independently. This PR is now scoped to just the extension type and touches only:

  • arrow/extensions/timestamp_with_offset.go (new)
  • arrow/extensions/timestamp_with_offset_test.go (new)
  • arrow/extensions/extensions.go (register the canonical type)

To keep this PR self-contained, the JSON round-trip test asserts the values the type guarantees (per-row instant, timezone offset, and validity) rather than array.RecordEqual, which would compare inner non-nullable struct child validity bitmaps — the parity concern tracked separately in #918.

Are these changes tested?

Yes — primitive/dictionary/run-end offset encodings, JSON marshal + round-trip, and IPC round-trip.

Are there any user-facing changes?

Yes, this is a new canonical extension type.

@felipecrv
felipecrv self-requested a review November 1, 2025 01:55
felipecrv added a commit to apache/arrow that referenced this pull request Dec 5, 2025
…48002)
### Rationale for this change
Closes#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: #44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from 95230ad to b9b8bf2CompareDecember 22, 2025 14:33
@serramatutuserramatutu changed the title [DRAFT] Add TimestampWithOffset extension typeAdd TimestampWithOffset extension typeDec 22, 2025
@serramatutu
serramatutu marked this pull request as ready for review December 22, 2025 14:42
@serramatutuserramatutu changed the title Add TimestampWithOffset extension typeAdd TimestampWithOffset canonical extension typeDec 22, 2025
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from b9b8bf2 to ccdd288CompareDecember 22, 2025 15:17
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +506 to +507
timestamps.UnsafeAppendBoolToBitmap(false)
offsets.UnsafeAppendBoolToBitmap(false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, these are non-nullable according to the spec.

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did change this but now the IPC roundtrip tests are failing because the internal count of nulls is different between the thing before IPC and after IPC.

To make them pass we'll need to relax the arrow.RecordEqual() to ignore NullN() when the field is not nullable. Alternatively we can change it so that an array builder sets nulls = 0 if the array is not nullable, regardless of what has been set before in calls to UnsafeAppendBoolToBitmap() etc

Related: https://lists.apache.org/thread/7gbqjwykh1ob3xbvwph3ljsdl5c7kxpd

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from ccdd288 to cee17a3CompareJanuary 30, 2026 12:21
Comment threadarrow/array/encoded.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +151 to +152
*arrow.Int8Type | *arrow.Int16Type | *arrow.Int32Type | *arrow.Int64Type |
*arrow.Uint8Type | *arrow.Uint16Type | *arrow.Uint32Type | *arrow.Uint64Type

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this and the DictIndexType are both identical, instead of duplicating this we should either create a single type constraint or embed one in the other:

typeTimestampWithOffsetRunEndsTypeinterface {
DictIndexType
}

My personal preference would be to have a single type though, but I'm not averse to having the two separate ones if necessary.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: 231cb1e

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I had to revert. Spec says those types are different:

Dict index is either signed or unsigned int of 8-64 bits

The index type of a Dictionary type can only be an integer type, preferably signed, with width 8 to 64 bits.

Run ends is a signed int of 16-64 bits

The run end type of a Run-End Encoded type can only be a signed integer type with width 16 to 64 bits.

https://arrow.apache.org/docs/format/Columnar.html#data-types

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from bc577e9 to 8a7d6d7CompareApril 15, 2026 11:05
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from f060911 to 9f931fbCompareApril 30, 2026 11:33
Mottl pushed a commit to Mottl/arrow that referenced this pull request May 26, 2026
…type (apache#48002)
### Rationale for this change
Closesapache#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: apache#44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from 49da897 to b6f73a4CompareJuly 2, 2026 18:11
@zeroshade
zeroshade requested review from Copilot, kou and lidavidmJuly 2, 2026 21:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the canonical extension type arrow.timestamp_with_offset to match the Arrow spec, and updates JSON marshaling/unmarshaling plus array/record equality behavior to better respect schema nullability (especially for nested types and IPC-vs-JSON roundtrips).

Changes:

  • Add TimestampWithOffsetType, TimestampWithOffsetArray, and a convenience builder supporting primitive/dictionary/REE encodings for per-row timezone offsets.
  • Update JSON marshaling (GetOneForMarshal) to take schema nullability into account, and adjust struct/record JSON unmarshaling to handle explicit null for required fields.
  • Update array/record/table equality logic to optionally ignore validity bitmaps when fields are considered non-nullable, and refresh test fixtures accordingly.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
internal/json/json.goAdd IsNullMessage helper for null detection.
internal/json/json_stdlib.goStdlib variant of IsNullMessage.
arrow/ipc/cmd/arrow-ls/main_test.goUpdate expected struct field nullability formatting in output.
arrow/internal/arrjson/arrjson_test.goUpdate expected JSON schema nullability for struct children.
arrow/internal/arrdata/arrdata.goUpdate struct test data to mark inner fields nullable and adjust validity masks.
arrow/extensions/variant.goUpdate GetOneForMarshal signature to include nullability.
arrow/extensions/uuid.goUpdate GetOneForMarshal signature + call sites.
arrow/extensions/uuid_test.goMark UUID field nullable in record-builder test.
arrow/extensions/timestamp_with_offset.goNew extension type implementation + builder/array logic.
arrow/extensions/timestamp_with_offset_test.goNew tests covering primitive/dict/REE encodings, JSON roundtrip, IPC roundtrip.
arrow/extensions/json.goMake JSON extension marshaling honor caller-provided nullability.
arrow/extensions/extensions.goRegister TimestampWithOffsetType as a canonical extension type.
arrow/extensions/bool8.goUpdate GetOneForMarshal signature to include nullability.
arrow/compute/vector_sort_test.goMark fields nullable in C++ parity tests (to align with new nullability-aware behavior).
arrow/array/util.goPass schema field nullability into GetOneForMarshal during Record-to-JSON.
arrow/array/util_test.goExercise record JSON roundtrip under nullable vs non-nullable schemas.
arrow/array/union.goMake union JSON marshaling and approx-equality respect child-field nullability.
arrow/array/timestamp.goUpdate GetOneForMarshal and equality to respect nullable option.
arrow/array/struct.goMake struct JSON marshaling/unmarshaling nullability-aware; adjust struct equality accordingly.
arrow/array/struct_test.goAdd coverage for explicit null in required struct fields.
arrow/array/string.goUpdate string array marshaling and equality to respect nullable option.
arrow/array/record.goMake record JSON unmarshaling treat explicit null in required fields as empty value.
arrow/array/record_test.goExtend record-builder tests for new null-handling + JSON roundtrip.
arrow/array/numeric_generic.goUpdate numeric marshaling and equality to respect nullable option.
arrow/array/null.goUpdate GetOneForMarshal signature.
arrow/array/map.goThread nullable option through map equality.
arrow/array/list.goUpdate list marshaling and equality to respect element-field nullability.
arrow/array/json_reader_test.goUpdate JSON reader test to pass nullable flag when marshaling one value.
arrow/array/interval.goUpdate interval marshaling and equality to respect nullable option.
arrow/array/float16.goUpdate GetOneForMarshal signature to include nullability.
arrow/array/fixedsize_binary.goUpdate fixed-size-binary marshaling and equality to respect nullable option.
arrow/array/fixed_size_list.goUpdate fixed-size-list marshaling and equality to respect element-field nullability.
arrow/array/extension.goThread nullable option through extension array equality and marshaling.
arrow/array/encoded.goUpdate run-end-encoded marshaling and equality to thread nullable option.
arrow/array/dictionary.goUpdate dictionary marshaling and equality to thread nullable option.
arrow/array/decimal256_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal128_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal.goUpdate decimal marshaling and equality to respect nullable option.
arrow/array/compare.goAdd nullable-aware equality plumbing and new APIs; thread schema nullability into comparisons.
arrow/array/compare_test.goAdd tests for WithNullable(false) behavior.
arrow/array/boolean.goUpdate boolean marshaling and equality to respect nullable option.
arrow/array/binary.goUpdate binary marshaling and equality to respect nullable option.
arrow/array.goChange GetOneForMarshal interface to accept nullability flag.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/record_test.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated

@serramatutuserramatutu left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zeroshade some comments:

Can you think of a way to decouple these 2 things (timestamp with offset and the inner nullability fixes)? So we can merge the extension type without having to wait on all the fixes for that and do that more carefully?

I'm sorry I left the branch based off of an old version of the inner nullability parity fixes. There are some differences with what I was doing in the new version of the JSON PR:

  • I ended up dropping the equalOpts because this is not what C++ does. It looks like they just validate the schemas on Validate() instead of making Equal() more lenient. I had not fixed that in that PR because there are some issues with the CSV reader (it calls Validate()) and it was becoming somewhat of a sprawl to fix it all. There are lots of behavior in arrow-go that never checks for the schema of structs.
  • I did not do the GetOneForMarshalNullable() and instead I changed the struct/record builder to check for IsValid() before calling GetOneForMarshal().

This commit adds a new `TimestampWithOffset` extension type that can be
used to represent timestamps with per-row timezone information. It
stores information in a `struct` with 2 fields, `timestamp=[T, "UTC"]`,
where `T` can be any `arrow.TimeUnit` and `offset_minutes=int16`, which
represents the offset in minutes from the UTC timestamp.
serramatutuand others added 13 commits July 9, 2026 15:29
Stop using `time=0` as a sentinel for null in `iterValues()`. Instead,
return a tuple of `(time.Time, bool)` where the bool indicates whether the
value is valid or not.
Added a test for it.
Start a run when none exists yet (e.g. leading null rows) instead of
continuing a nonexistent run, and only update lastOffset when a new run
starts. This stops a leading null from corrupting the run-ends/values
children and stops a null between equal offsets from splitting one
contiguous run into two. Add a noLastOffset sentinel, handle nil valids,
and add regression tests across all offset encodings.
Address roborev review findings on the run-end-encoded offset builder:
- AppendValues now treats a nil valids slice as all-valid so the parent
struct and its child builders stay the same length. Previously the
struct received zero slots while the children received len(values),
producing an inconsistent extension array.
- Override NewArray/NewExtensionArray to reset lastOffset to noLastOffset
on finalization, so a reused builder starts a fresh run instead of
continuing a run that belonged to the array just finalized (which could
emit run-ends without a matching value on a run-end-encoded offset).
- Add regression tests for nil validity and builder reuse across all
offset encodings.
Follow-up to the nil-validity fix: an empty (len 0) valids slice is also
all-valid, matching the convention of the other Arrow builders.
AppendValues now normalizes both nil and empty validity, and panics on a
genuine length mismatch (a non-empty valids whose length differs from
values).
Extend the validity regression test to cover the empty slice and add a
test asserting the length-mismatch panic.
Follow-up to the run-end-encoding fixes: AppendNull (and therefore the
UnmarshalOne(null) and AppendValueFromString(null) paths that call it)
appends a null offset run via the embedded struct builder but left
lastOffset stale. A subsequent value repeating the pre-null offset would
then continue the null run instead of starting its own, encoding that
value with the null offset run.
Override AppendNull/AppendNulls to reset lastOffset to noLastOffset, and
add a regression test for Append(value), AppendNull(), Append(same-offset
value) with run-end-encoded offsets.
Replace array.RecordEqual with a per-row check of the extension's
guarantees (instant, timezone offset, validity). RecordEqual compares the
inner non-nullable struct child validity bitmaps, which legitimately differ
between builder output (children valid under a null parent) and JSON reader
output (children null). That inner-nullability parity is tracked separately
and is out of scope for this extension type.
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch from 03b049f to 2ef684cCompareJuly 9, 2026 19:30
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu updated this PR to be only the timestamp with offset extension type

…rays
iterValues started the run-end tracker at physical index 0 and compared
the logical row against absolute run ends, ignoring the array offset. A
sliced array whose offset began inside a later run decoded offsets from
the wrong run in Values() and MarshalJSON(). Initialize the tracker from
GetPhysicalOffset() and advance using absolute positions (Offset()+i),
and add a regression test slicing an REE-encoded array across run
boundaries.
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu filed #918 to split out the nullability stuff

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
- timeFromFieldValues: derive the zone-name sign from the whole offset so
a sub-hour negative offset keeps its sign (e.g. -30 minutes now formats
as "UTC-00:30" instead of "UTC+00:30"; integer hours is 0 there and
cannot carry the sign). Add a regression test.
- MarshalJSON: store the timestamp by value instead of taking the address
of the range variable.
- Values: rename the loop variable so it no longer shadows the time
package.
Widen offsetMinutes to int before negating so an out-of-range int16
offset (e.g. math.MinInt16) cannot overflow when computing the absolute
value used for the zone name.
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
zeroshadeand others added 3 commits July 12, 2026 13:05
…erialize
Deserialize now rejects serialized metadata other than "" or "{}", matching the canonical extension spec (empty metadata) and the existing JSONType pattern. Adds a regression test.
Also documents (NOTE apache#918) that AppendNull writes a null into the non-nullable offset_minutes storage field; inner-field comparison/JSON parity is tracked in apache#918.
…set Deserialize
Adds TestTimestampWithOffsetTypeDeserializeInvalidStorage covering non-struct storage, wrong field count/names/order, non-UTC and empty timezone, nullable timestamp/offset fields, and invalid offset encodings (wrong primitive type, dictionary value not Int16, run-end-encoded value not Int16).
Comment on lines +49 to +52
// FIXME: Technically this should be non-nullable, but a Arrow IPC does not deserialize
// ValueNullable properly, so enforcing this here would always fail when reading from an IPC
// stream
// !offsetType.ValueNullable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty major right? Can we link this to a tracking issue? (Does integration catch this?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of what was pulled out into #918 and friends

@zeroshade
zeroshade merged commit c9791ae into apache:mainJul 19, 2026
23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@serramatutu@zeroshade@lidavidm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add TimestampWithOffset canonical extension type - #558

Merged
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go
Jul 19, 2026
Merged

Add TimestampWithOffset canonical extension type#558
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go

Conversation

@serramatutu

@serramatutuserramatutu commented Oct 30, 2025

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Implements the new arrow.timestamp_with_offset canonical extension type for arrow-go.

Rationale for this change

Be compatible with the Arrow columnar spec's arrow.timestamp_with_offset canonical extension type.

What changes are included in this PR?

Adds a TimestampWithOffset extension type. This type represents a timestamp column that stores a potentially different timezone offset per value: the timestamp is stored in UTC alongside the original timezone offset in minutes. The offset-in-minutes field can be primitive-, dictionary-, or run-end-encoded.

This PR has been decoupled from the inner-nullability parity work it was previously based on. Those comparison / JSON-marshaling changes now live in #918 and can be reviewed and merged independently. This PR is now scoped to just the extension type and touches only:

  • arrow/extensions/timestamp_with_offset.go (new)
  • arrow/extensions/timestamp_with_offset_test.go (new)
  • arrow/extensions/extensions.go (register the canonical type)

To keep this PR self-contained, the JSON round-trip test asserts the values the type guarantees (per-row instant, timezone offset, and validity) rather than array.RecordEqual, which would compare inner non-nullable struct child validity bitmaps — the parity concern tracked separately in #918.

Are these changes tested?

Yes — primitive/dictionary/run-end offset encodings, JSON marshal + round-trip, and IPC round-trip.

Are there any user-facing changes?

Yes, this is a new canonical extension type.

@felipecrv
felipecrv self-requested a review November 1, 2025 01:55
felipecrv added a commit to apache/arrow that referenced this pull request Dec 5, 2025
…48002)
### Rationale for this change
Closes#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: #44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from 95230ad to b9b8bf2CompareDecember 22, 2025 14:33
@serramatutuserramatutu changed the title [DRAFT] Add TimestampWithOffset extension typeAdd TimestampWithOffset extension typeDec 22, 2025
@serramatutu
serramatutu marked this pull request as ready for review December 22, 2025 14:42
@serramatutuserramatutu changed the title Add TimestampWithOffset extension typeAdd TimestampWithOffset canonical extension typeDec 22, 2025
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from b9b8bf2 to ccdd288CompareDecember 22, 2025 15:17
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +506 to +507
timestamps.UnsafeAppendBoolToBitmap(false)
offsets.UnsafeAppendBoolToBitmap(false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, these are non-nullable according to the spec.

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did change this but now the IPC roundtrip tests are failing because the internal count of nulls is different between the thing before IPC and after IPC.

To make them pass we'll need to relax the arrow.RecordEqual() to ignore NullN() when the field is not nullable. Alternatively we can change it so that an array builder sets nulls = 0 if the array is not nullable, regardless of what has been set before in calls to UnsafeAppendBoolToBitmap() etc

Related: https://lists.apache.org/thread/7gbqjwykh1ob3xbvwph3ljsdl5c7kxpd

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from ccdd288 to cee17a3CompareJanuary 30, 2026 12:21
Comment threadarrow/array/encoded.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +151 to +152
*arrow.Int8Type | *arrow.Int16Type | *arrow.Int32Type | *arrow.Int64Type |
*arrow.Uint8Type | *arrow.Uint16Type | *arrow.Uint32Type | *arrow.Uint64Type

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this and the DictIndexType are both identical, instead of duplicating this we should either create a single type constraint or embed one in the other:

typeTimestampWithOffsetRunEndsTypeinterface {
DictIndexType
}

My personal preference would be to have a single type though, but I'm not averse to having the two separate ones if necessary.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: 231cb1e

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I had to revert. Spec says those types are different:

Dict index is either signed or unsigned int of 8-64 bits

The index type of a Dictionary type can only be an integer type, preferably signed, with width 8 to 64 bits.

Run ends is a signed int of 16-64 bits

The run end type of a Run-End Encoded type can only be a signed integer type with width 16 to 64 bits.

https://arrow.apache.org/docs/format/Columnar.html#data-types

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from bc577e9 to 8a7d6d7CompareApril 15, 2026 11:05
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from f060911 to 9f931fbCompareApril 30, 2026 11:33
Mottl pushed a commit to Mottl/arrow that referenced this pull request May 26, 2026
…type (apache#48002)
### Rationale for this change
Closesapache#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: apache#44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from 49da897 to b6f73a4CompareJuly 2, 2026 18:11
@zeroshade
zeroshade requested review from Copilot, kou and lidavidmJuly 2, 2026 21:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the canonical extension type arrow.timestamp_with_offset to match the Arrow spec, and updates JSON marshaling/unmarshaling plus array/record equality behavior to better respect schema nullability (especially for nested types and IPC-vs-JSON roundtrips).

Changes:

  • Add TimestampWithOffsetType, TimestampWithOffsetArray, and a convenience builder supporting primitive/dictionary/REE encodings for per-row timezone offsets.
  • Update JSON marshaling (GetOneForMarshal) to take schema nullability into account, and adjust struct/record JSON unmarshaling to handle explicit null for required fields.
  • Update array/record/table equality logic to optionally ignore validity bitmaps when fields are considered non-nullable, and refresh test fixtures accordingly.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
internal/json/json.goAdd IsNullMessage helper for null detection.
internal/json/json_stdlib.goStdlib variant of IsNullMessage.
arrow/ipc/cmd/arrow-ls/main_test.goUpdate expected struct field nullability formatting in output.
arrow/internal/arrjson/arrjson_test.goUpdate expected JSON schema nullability for struct children.
arrow/internal/arrdata/arrdata.goUpdate struct test data to mark inner fields nullable and adjust validity masks.
arrow/extensions/variant.goUpdate GetOneForMarshal signature to include nullability.
arrow/extensions/uuid.goUpdate GetOneForMarshal signature + call sites.
arrow/extensions/uuid_test.goMark UUID field nullable in record-builder test.
arrow/extensions/timestamp_with_offset.goNew extension type implementation + builder/array logic.
arrow/extensions/timestamp_with_offset_test.goNew tests covering primitive/dict/REE encodings, JSON roundtrip, IPC roundtrip.
arrow/extensions/json.goMake JSON extension marshaling honor caller-provided nullability.
arrow/extensions/extensions.goRegister TimestampWithOffsetType as a canonical extension type.
arrow/extensions/bool8.goUpdate GetOneForMarshal signature to include nullability.
arrow/compute/vector_sort_test.goMark fields nullable in C++ parity tests (to align with new nullability-aware behavior).
arrow/array/util.goPass schema field nullability into GetOneForMarshal during Record-to-JSON.
arrow/array/util_test.goExercise record JSON roundtrip under nullable vs non-nullable schemas.
arrow/array/union.goMake union JSON marshaling and approx-equality respect child-field nullability.
arrow/array/timestamp.goUpdate GetOneForMarshal and equality to respect nullable option.
arrow/array/struct.goMake struct JSON marshaling/unmarshaling nullability-aware; adjust struct equality accordingly.
arrow/array/struct_test.goAdd coverage for explicit null in required struct fields.
arrow/array/string.goUpdate string array marshaling and equality to respect nullable option.
arrow/array/record.goMake record JSON unmarshaling treat explicit null in required fields as empty value.
arrow/array/record_test.goExtend record-builder tests for new null-handling + JSON roundtrip.
arrow/array/numeric_generic.goUpdate numeric marshaling and equality to respect nullable option.
arrow/array/null.goUpdate GetOneForMarshal signature.
arrow/array/map.goThread nullable option through map equality.
arrow/array/list.goUpdate list marshaling and equality to respect element-field nullability.
arrow/array/json_reader_test.goUpdate JSON reader test to pass nullable flag when marshaling one value.
arrow/array/interval.goUpdate interval marshaling and equality to respect nullable option.
arrow/array/float16.goUpdate GetOneForMarshal signature to include nullability.
arrow/array/fixedsize_binary.goUpdate fixed-size-binary marshaling and equality to respect nullable option.
arrow/array/fixed_size_list.goUpdate fixed-size-list marshaling and equality to respect element-field nullability.
arrow/array/extension.goThread nullable option through extension array equality and marshaling.
arrow/array/encoded.goUpdate run-end-encoded marshaling and equality to thread nullable option.
arrow/array/dictionary.goUpdate dictionary marshaling and equality to thread nullable option.
arrow/array/decimal256_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal128_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal.goUpdate decimal marshaling and equality to respect nullable option.
arrow/array/compare.goAdd nullable-aware equality plumbing and new APIs; thread schema nullability into comparisons.
arrow/array/compare_test.goAdd tests for WithNullable(false) behavior.
arrow/array/boolean.goUpdate boolean marshaling and equality to respect nullable option.
arrow/array/binary.goUpdate binary marshaling and equality to respect nullable option.
arrow/array.goChange GetOneForMarshal interface to accept nullability flag.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/record_test.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated

@serramatutuserramatutu left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zeroshade some comments:

Can you think of a way to decouple these 2 things (timestamp with offset and the inner nullability fixes)? So we can merge the extension type without having to wait on all the fixes for that and do that more carefully?

I'm sorry I left the branch based off of an old version of the inner nullability parity fixes. There are some differences with what I was doing in the new version of the JSON PR:

  • I ended up dropping the equalOpts because this is not what C++ does. It looks like they just validate the schemas on Validate() instead of making Equal() more lenient. I had not fixed that in that PR because there are some issues with the CSV reader (it calls Validate()) and it was becoming somewhat of a sprawl to fix it all. There are lots of behavior in arrow-go that never checks for the schema of structs.
  • I did not do the GetOneForMarshalNullable() and instead I changed the struct/record builder to check for IsValid() before calling GetOneForMarshal().

This commit adds a new `TimestampWithOffset` extension type that can be
used to represent timestamps with per-row timezone information. It
stores information in a `struct` with 2 fields, `timestamp=[T, "UTC"]`,
where `T` can be any `arrow.TimeUnit` and `offset_minutes=int16`, which
represents the offset in minutes from the UTC timestamp.
serramatutuand others added 13 commits July 9, 2026 15:29
Stop using `time=0` as a sentinel for null in `iterValues()`. Instead,
return a tuple of `(time.Time, bool)` where the bool indicates whether the
value is valid or not.
Added a test for it.
Start a run when none exists yet (e.g. leading null rows) instead of
continuing a nonexistent run, and only update lastOffset when a new run
starts. This stops a leading null from corrupting the run-ends/values
children and stops a null between equal offsets from splitting one
contiguous run into two. Add a noLastOffset sentinel, handle nil valids,
and add regression tests across all offset encodings.
Address roborev review findings on the run-end-encoded offset builder:
- AppendValues now treats a nil valids slice as all-valid so the parent
struct and its child builders stay the same length. Previously the
struct received zero slots while the children received len(values),
producing an inconsistent extension array.
- Override NewArray/NewExtensionArray to reset lastOffset to noLastOffset
on finalization, so a reused builder starts a fresh run instead of
continuing a run that belonged to the array just finalized (which could
emit run-ends without a matching value on a run-end-encoded offset).
- Add regression tests for nil validity and builder reuse across all
offset encodings.
Follow-up to the nil-validity fix: an empty (len 0) valids slice is also
all-valid, matching the convention of the other Arrow builders.
AppendValues now normalizes both nil and empty validity, and panics on a
genuine length mismatch (a non-empty valids whose length differs from
values).
Extend the validity regression test to cover the empty slice and add a
test asserting the length-mismatch panic.
Follow-up to the run-end-encoding fixes: AppendNull (and therefore the
UnmarshalOne(null) and AppendValueFromString(null) paths that call it)
appends a null offset run via the embedded struct builder but left
lastOffset stale. A subsequent value repeating the pre-null offset would
then continue the null run instead of starting its own, encoding that
value with the null offset run.
Override AppendNull/AppendNulls to reset lastOffset to noLastOffset, and
add a regression test for Append(value), AppendNull(), Append(same-offset
value) with run-end-encoded offsets.
Replace array.RecordEqual with a per-row check of the extension's
guarantees (instant, timezone offset, validity). RecordEqual compares the
inner non-nullable struct child validity bitmaps, which legitimately differ
between builder output (children valid under a null parent) and JSON reader
output (children null). That inner-nullability parity is tracked separately
and is out of scope for this extension type.
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch from 03b049f to 2ef684cCompareJuly 9, 2026 19:30
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu updated this PR to be only the timestamp with offset extension type

…rays
iterValues started the run-end tracker at physical index 0 and compared
the logical row against absolute run ends, ignoring the array offset. A
sliced array whose offset began inside a later run decoded offsets from
the wrong run in Values() and MarshalJSON(). Initialize the tracker from
GetPhysicalOffset() and advance using absolute positions (Offset()+i),
and add a regression test slicing an REE-encoded array across run
boundaries.
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu filed #918 to split out the nullability stuff

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
- timeFromFieldValues: derive the zone-name sign from the whole offset so
a sub-hour negative offset keeps its sign (e.g. -30 minutes now formats
as "UTC-00:30" instead of "UTC+00:30"; integer hours is 0 there and
cannot carry the sign). Add a regression test.
- MarshalJSON: store the timestamp by value instead of taking the address
of the range variable.
- Values: rename the loop variable so it no longer shadows the time
package.
Widen offsetMinutes to int before negating so an out-of-range int16
offset (e.g. math.MinInt16) cannot overflow when computing the absolute
value used for the zone name.
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
zeroshadeand others added 3 commits July 12, 2026 13:05
…erialize
Deserialize now rejects serialized metadata other than "" or "{}", matching the canonical extension spec (empty metadata) and the existing JSONType pattern. Adds a regression test.
Also documents (NOTE apache#918) that AppendNull writes a null into the non-nullable offset_minutes storage field; inner-field comparison/JSON parity is tracked in apache#918.
…set Deserialize
Adds TestTimestampWithOffsetTypeDeserializeInvalidStorage covering non-struct storage, wrong field count/names/order, non-UTC and empty timezone, nullable timestamp/offset fields, and invalid offset encodings (wrong primitive type, dictionary value not Int16, run-end-encoded value not Int16).
Comment on lines +49 to +52
// FIXME: Technically this should be non-nullable, but a Arrow IPC does not deserialize
// ValueNullable properly, so enforcing this here would always fail when reading from an IPC
// stream
// !offsetType.ValueNullable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty major right? Can we link this to a tracking issue? (Does integration catch this?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of what was pulled out into #918 and friends

@zeroshade
zeroshade merged commit c9791ae into apache:mainJul 19, 2026
23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@serramatutu@zeroshade@lidavidm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add TimestampWithOffset canonical extension type - #558

Merged
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go
Jul 19, 2026
Merged

Add TimestampWithOffset canonical extension type#558
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go

Conversation

@serramatutu

@serramatutuserramatutu commented Oct 30, 2025

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Implements the new arrow.timestamp_with_offset canonical extension type for arrow-go.

Rationale for this change

Be compatible with the Arrow columnar spec's arrow.timestamp_with_offset canonical extension type.

What changes are included in this PR?

Adds a TimestampWithOffset extension type. This type represents a timestamp column that stores a potentially different timezone offset per value: the timestamp is stored in UTC alongside the original timezone offset in minutes. The offset-in-minutes field can be primitive-, dictionary-, or run-end-encoded.

This PR has been decoupled from the inner-nullability parity work it was previously based on. Those comparison / JSON-marshaling changes now live in #918 and can be reviewed and merged independently. This PR is now scoped to just the extension type and touches only:

  • arrow/extensions/timestamp_with_offset.go (new)
  • arrow/extensions/timestamp_with_offset_test.go (new)
  • arrow/extensions/extensions.go (register the canonical type)

To keep this PR self-contained, the JSON round-trip test asserts the values the type guarantees (per-row instant, timezone offset, and validity) rather than array.RecordEqual, which would compare inner non-nullable struct child validity bitmaps — the parity concern tracked separately in #918.

Are these changes tested?

Yes — primitive/dictionary/run-end offset encodings, JSON marshal + round-trip, and IPC round-trip.

Are there any user-facing changes?

Yes, this is a new canonical extension type.

@felipecrv
felipecrv self-requested a review November 1, 2025 01:55
felipecrv added a commit to apache/arrow that referenced this pull request Dec 5, 2025
…48002)
### Rationale for this change
Closes#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: #44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from 95230ad to b9b8bf2CompareDecember 22, 2025 14:33
@serramatutuserramatutu changed the title [DRAFT] Add TimestampWithOffset extension typeAdd TimestampWithOffset extension typeDec 22, 2025
@serramatutu
serramatutu marked this pull request as ready for review December 22, 2025 14:42
@serramatutuserramatutu changed the title Add TimestampWithOffset extension typeAdd TimestampWithOffset canonical extension typeDec 22, 2025
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from b9b8bf2 to ccdd288CompareDecember 22, 2025 15:17
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +506 to +507
timestamps.UnsafeAppendBoolToBitmap(false)
offsets.UnsafeAppendBoolToBitmap(false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, these are non-nullable according to the spec.

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did change this but now the IPC roundtrip tests are failing because the internal count of nulls is different between the thing before IPC and after IPC.

To make them pass we'll need to relax the arrow.RecordEqual() to ignore NullN() when the field is not nullable. Alternatively we can change it so that an array builder sets nulls = 0 if the array is not nullable, regardless of what has been set before in calls to UnsafeAppendBoolToBitmap() etc

Related: https://lists.apache.org/thread/7gbqjwykh1ob3xbvwph3ljsdl5c7kxpd

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from ccdd288 to cee17a3CompareJanuary 30, 2026 12:21
Comment threadarrow/array/encoded.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +151 to +152
*arrow.Int8Type | *arrow.Int16Type | *arrow.Int32Type | *arrow.Int64Type |
*arrow.Uint8Type | *arrow.Uint16Type | *arrow.Uint32Type | *arrow.Uint64Type

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this and the DictIndexType are both identical, instead of duplicating this we should either create a single type constraint or embed one in the other:

typeTimestampWithOffsetRunEndsTypeinterface {
DictIndexType
}

My personal preference would be to have a single type though, but I'm not averse to having the two separate ones if necessary.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: 231cb1e

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I had to revert. Spec says those types are different:

Dict index is either signed or unsigned int of 8-64 bits

The index type of a Dictionary type can only be an integer type, preferably signed, with width 8 to 64 bits.

Run ends is a signed int of 16-64 bits

The run end type of a Run-End Encoded type can only be a signed integer type with width 16 to 64 bits.

https://arrow.apache.org/docs/format/Columnar.html#data-types

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from bc577e9 to 8a7d6d7CompareApril 15, 2026 11:05
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from f060911 to 9f931fbCompareApril 30, 2026 11:33
Mottl pushed a commit to Mottl/arrow that referenced this pull request May 26, 2026
…type (apache#48002)
### Rationale for this change
Closesapache#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: apache#44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from 49da897 to b6f73a4CompareJuly 2, 2026 18:11
@zeroshade
zeroshade requested review from Copilot, kou and lidavidmJuly 2, 2026 21:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the canonical extension type arrow.timestamp_with_offset to match the Arrow spec, and updates JSON marshaling/unmarshaling plus array/record equality behavior to better respect schema nullability (especially for nested types and IPC-vs-JSON roundtrips).

Changes:

  • Add TimestampWithOffsetType, TimestampWithOffsetArray, and a convenience builder supporting primitive/dictionary/REE encodings for per-row timezone offsets.
  • Update JSON marshaling (GetOneForMarshal) to take schema nullability into account, and adjust struct/record JSON unmarshaling to handle explicit null for required fields.
  • Update array/record/table equality logic to optionally ignore validity bitmaps when fields are considered non-nullable, and refresh test fixtures accordingly.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
internal/json/json.goAdd IsNullMessage helper for null detection.
internal/json/json_stdlib.goStdlib variant of IsNullMessage.
arrow/ipc/cmd/arrow-ls/main_test.goUpdate expected struct field nullability formatting in output.
arrow/internal/arrjson/arrjson_test.goUpdate expected JSON schema nullability for struct children.
arrow/internal/arrdata/arrdata.goUpdate struct test data to mark inner fields nullable and adjust validity masks.
arrow/extensions/variant.goUpdate GetOneForMarshal signature to include nullability.
arrow/extensions/uuid.goUpdate GetOneForMarshal signature + call sites.
arrow/extensions/uuid_test.goMark UUID field nullable in record-builder test.
arrow/extensions/timestamp_with_offset.goNew extension type implementation + builder/array logic.
arrow/extensions/timestamp_with_offset_test.goNew tests covering primitive/dict/REE encodings, JSON roundtrip, IPC roundtrip.
arrow/extensions/json.goMake JSON extension marshaling honor caller-provided nullability.
arrow/extensions/extensions.goRegister TimestampWithOffsetType as a canonical extension type.
arrow/extensions/bool8.goUpdate GetOneForMarshal signature to include nullability.
arrow/compute/vector_sort_test.goMark fields nullable in C++ parity tests (to align with new nullability-aware behavior).
arrow/array/util.goPass schema field nullability into GetOneForMarshal during Record-to-JSON.
arrow/array/util_test.goExercise record JSON roundtrip under nullable vs non-nullable schemas.
arrow/array/union.goMake union JSON marshaling and approx-equality respect child-field nullability.
arrow/array/timestamp.goUpdate GetOneForMarshal and equality to respect nullable option.
arrow/array/struct.goMake struct JSON marshaling/unmarshaling nullability-aware; adjust struct equality accordingly.
arrow/array/struct_test.goAdd coverage for explicit null in required struct fields.
arrow/array/string.goUpdate string array marshaling and equality to respect nullable option.
arrow/array/record.goMake record JSON unmarshaling treat explicit null in required fields as empty value.
arrow/array/record_test.goExtend record-builder tests for new null-handling + JSON roundtrip.
arrow/array/numeric_generic.goUpdate numeric marshaling and equality to respect nullable option.
arrow/array/null.goUpdate GetOneForMarshal signature.
arrow/array/map.goThread nullable option through map equality.
arrow/array/list.goUpdate list marshaling and equality to respect element-field nullability.
arrow/array/json_reader_test.goUpdate JSON reader test to pass nullable flag when marshaling one value.
arrow/array/interval.goUpdate interval marshaling and equality to respect nullable option.
arrow/array/float16.goUpdate GetOneForMarshal signature to include nullability.
arrow/array/fixedsize_binary.goUpdate fixed-size-binary marshaling and equality to respect nullable option.
arrow/array/fixed_size_list.goUpdate fixed-size-list marshaling and equality to respect element-field nullability.
arrow/array/extension.goThread nullable option through extension array equality and marshaling.
arrow/array/encoded.goUpdate run-end-encoded marshaling and equality to thread nullable option.
arrow/array/dictionary.goUpdate dictionary marshaling and equality to thread nullable option.
arrow/array/decimal256_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal128_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal.goUpdate decimal marshaling and equality to respect nullable option.
arrow/array/compare.goAdd nullable-aware equality plumbing and new APIs; thread schema nullability into comparisons.
arrow/array/compare_test.goAdd tests for WithNullable(false) behavior.
arrow/array/boolean.goUpdate boolean marshaling and equality to respect nullable option.
arrow/array/binary.goUpdate binary marshaling and equality to respect nullable option.
arrow/array.goChange GetOneForMarshal interface to accept nullability flag.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/record_test.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated

@serramatutuserramatutu left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zeroshade some comments:

Can you think of a way to decouple these 2 things (timestamp with offset and the inner nullability fixes)? So we can merge the extension type without having to wait on all the fixes for that and do that more carefully?

I'm sorry I left the branch based off of an old version of the inner nullability parity fixes. There are some differences with what I was doing in the new version of the JSON PR:

  • I ended up dropping the equalOpts because this is not what C++ does. It looks like they just validate the schemas on Validate() instead of making Equal() more lenient. I had not fixed that in that PR because there are some issues with the CSV reader (it calls Validate()) and it was becoming somewhat of a sprawl to fix it all. There are lots of behavior in arrow-go that never checks for the schema of structs.
  • I did not do the GetOneForMarshalNullable() and instead I changed the struct/record builder to check for IsValid() before calling GetOneForMarshal().

This commit adds a new `TimestampWithOffset` extension type that can be
used to represent timestamps with per-row timezone information. It
stores information in a `struct` with 2 fields, `timestamp=[T, "UTC"]`,
where `T` can be any `arrow.TimeUnit` and `offset_minutes=int16`, which
represents the offset in minutes from the UTC timestamp.
serramatutuand others added 13 commits July 9, 2026 15:29
Stop using `time=0` as a sentinel for null in `iterValues()`. Instead,
return a tuple of `(time.Time, bool)` where the bool indicates whether the
value is valid or not.
Added a test for it.
Start a run when none exists yet (e.g. leading null rows) instead of
continuing a nonexistent run, and only update lastOffset when a new run
starts. This stops a leading null from corrupting the run-ends/values
children and stops a null between equal offsets from splitting one
contiguous run into two. Add a noLastOffset sentinel, handle nil valids,
and add regression tests across all offset encodings.
Address roborev review findings on the run-end-encoded offset builder:
- AppendValues now treats a nil valids slice as all-valid so the parent
struct and its child builders stay the same length. Previously the
struct received zero slots while the children received len(values),
producing an inconsistent extension array.
- Override NewArray/NewExtensionArray to reset lastOffset to noLastOffset
on finalization, so a reused builder starts a fresh run instead of
continuing a run that belonged to the array just finalized (which could
emit run-ends without a matching value on a run-end-encoded offset).
- Add regression tests for nil validity and builder reuse across all
offset encodings.
Follow-up to the nil-validity fix: an empty (len 0) valids slice is also
all-valid, matching the convention of the other Arrow builders.
AppendValues now normalizes both nil and empty validity, and panics on a
genuine length mismatch (a non-empty valids whose length differs from
values).
Extend the validity regression test to cover the empty slice and add a
test asserting the length-mismatch panic.
Follow-up to the run-end-encoding fixes: AppendNull (and therefore the
UnmarshalOne(null) and AppendValueFromString(null) paths that call it)
appends a null offset run via the embedded struct builder but left
lastOffset stale. A subsequent value repeating the pre-null offset would
then continue the null run instead of starting its own, encoding that
value with the null offset run.
Override AppendNull/AppendNulls to reset lastOffset to noLastOffset, and
add a regression test for Append(value), AppendNull(), Append(same-offset
value) with run-end-encoded offsets.
Replace array.RecordEqual with a per-row check of the extension's
guarantees (instant, timezone offset, validity). RecordEqual compares the
inner non-nullable struct child validity bitmaps, which legitimately differ
between builder output (children valid under a null parent) and JSON reader
output (children null). That inner-nullability parity is tracked separately
and is out of scope for this extension type.
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch from 03b049f to 2ef684cCompareJuly 9, 2026 19:30
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu updated this PR to be only the timestamp with offset extension type

…rays
iterValues started the run-end tracker at physical index 0 and compared
the logical row against absolute run ends, ignoring the array offset. A
sliced array whose offset began inside a later run decoded offsets from
the wrong run in Values() and MarshalJSON(). Initialize the tracker from
GetPhysicalOffset() and advance using absolute positions (Offset()+i),
and add a regression test slicing an REE-encoded array across run
boundaries.
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu filed #918 to split out the nullability stuff

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
- timeFromFieldValues: derive the zone-name sign from the whole offset so
a sub-hour negative offset keeps its sign (e.g. -30 minutes now formats
as "UTC-00:30" instead of "UTC+00:30"; integer hours is 0 there and
cannot carry the sign). Add a regression test.
- MarshalJSON: store the timestamp by value instead of taking the address
of the range variable.
- Values: rename the loop variable so it no longer shadows the time
package.
Widen offsetMinutes to int before negating so an out-of-range int16
offset (e.g. math.MinInt16) cannot overflow when computing the absolute
value used for the zone name.
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
zeroshadeand others added 3 commits July 12, 2026 13:05
…erialize
Deserialize now rejects serialized metadata other than "" or "{}", matching the canonical extension spec (empty metadata) and the existing JSONType pattern. Adds a regression test.
Also documents (NOTE apache#918) that AppendNull writes a null into the non-nullable offset_minutes storage field; inner-field comparison/JSON parity is tracked in apache#918.
…set Deserialize
Adds TestTimestampWithOffsetTypeDeserializeInvalidStorage covering non-struct storage, wrong field count/names/order, non-UTC and empty timezone, nullable timestamp/offset fields, and invalid offset encodings (wrong primitive type, dictionary value not Int16, run-end-encoded value not Int16).
Comment on lines +49 to +52
// FIXME: Technically this should be non-nullable, but a Arrow IPC does not deserialize
// ValueNullable properly, so enforcing this here would always fail when reading from an IPC
// stream
// !offsetType.ValueNullable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty major right? Can we link this to a tracking issue? (Does integration catch this?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of what was pulled out into #918 and friends

@zeroshade
zeroshade merged commit c9791ae into apache:mainJul 19, 2026
23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@serramatutu@zeroshade@lidavidm
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add TimestampWithOffset canonical extension type - #558

Merged
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go
Jul 19, 2026
Merged

Add TimestampWithOffset canonical extension type#558
zeroshade merged 31 commits into
apache:mainfrom
serramatutu:serramatutu/TimestampWithOffset/go

Conversation

@serramatutu

@serramatutuserramatutu commented Oct 30, 2025

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Implements the new arrow.timestamp_with_offset canonical extension type for arrow-go.

Rationale for this change

Be compatible with the Arrow columnar spec's arrow.timestamp_with_offset canonical extension type.

What changes are included in this PR?

Adds a TimestampWithOffset extension type. This type represents a timestamp column that stores a potentially different timezone offset per value: the timestamp is stored in UTC alongside the original timezone offset in minutes. The offset-in-minutes field can be primitive-, dictionary-, or run-end-encoded.

This PR has been decoupled from the inner-nullability parity work it was previously based on. Those comparison / JSON-marshaling changes now live in #918 and can be reviewed and merged independently. This PR is now scoped to just the extension type and touches only:

  • arrow/extensions/timestamp_with_offset.go (new)
  • arrow/extensions/timestamp_with_offset_test.go (new)
  • arrow/extensions/extensions.go (register the canonical type)

To keep this PR self-contained, the JSON round-trip test asserts the values the type guarantees (per-row instant, timezone offset, and validity) rather than array.RecordEqual, which would compare inner non-nullable struct child validity bitmaps — the parity concern tracked separately in #918.

Are these changes tested?

Yes — primitive/dictionary/run-end offset encodings, JSON marshal + round-trip, and IPC round-trip.

Are there any user-facing changes?

Yes, this is a new canonical extension type.

@felipecrv
felipecrv self-requested a review November 1, 2025 01:55
felipecrv added a commit to apache/arrow that referenced this pull request Dec 5, 2025
…48002)
### Rationale for this change
Closes#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: #44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from 95230ad to b9b8bf2CompareDecember 22, 2025 14:33
@serramatutuserramatutu changed the title [DRAFT] Add TimestampWithOffset extension typeAdd TimestampWithOffset extension typeDec 22, 2025
@serramatutu
serramatutu marked this pull request as ready for review December 22, 2025 14:42
@serramatutuserramatutu changed the title Add TimestampWithOffset extension typeAdd TimestampWithOffset canonical extension typeDec 22, 2025
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from b9b8bf2 to ccdd288CompareDecember 22, 2025 15:17
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +506 to +507
timestamps.UnsafeAppendBoolToBitmap(false)
offsets.UnsafeAppendBoolToBitmap(false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above, these are non-nullable according to the spec.

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did change this but now the IPC roundtrip tests are failing because the internal count of nulls is different between the thing before IPC and after IPC.

To make them pass we'll need to relax the arrow.RecordEqual() to ignore NullN() when the field is not nullable. Alternatively we can change it so that an array builder sets nulls = 0 if the array is not nullable, regardless of what has been set before in calls to UnsafeAppendBoolToBitmap() etc

Related: https://lists.apache.org/thread/7gbqjwykh1ob3xbvwph3ljsdl5c7kxpd

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch from ccdd288 to cee17a3CompareJanuary 30, 2026 12:21
Comment threadarrow/array/encoded.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment on lines +151 to +152
*arrow.Int8Type | *arrow.Int16Type | *arrow.Int32Type | *arrow.Int64Type |
*arrow.Uint8Type | *arrow.Uint16Type | *arrow.Uint32Type | *arrow.Uint64Type

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this and the DictIndexType are both identical, instead of duplicating this we should either create a single type constraint or embed one in the other:

typeTimestampWithOffsetRunEndsTypeinterface {
DictIndexType
}

My personal preference would be to have a single type though, but I'm not averse to having the two separate ones if necessary.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: 231cb1e

@serramatutuserramatutuApr 15, 2026

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I had to revert. Spec says those types are different:

Dict index is either signed or unsigned int of 8-64 bits

The index type of a Dictionary type can only be an integer type, preferably signed, with width 8 to 64 bits.

Run ends is a signed int of 16-64 bits

The run end type of a Run-End Encoded type can only be a signed integer type with width 16 to 64 bits.

https://arrow.apache.org/docs/format/Columnar.html#data-types

Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 3 times, most recently from bc577e9 to 8a7d6d7CompareApril 15, 2026 11:05
@serramatutu
serramatutuforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from f060911 to 9f931fbCompareApril 30, 2026 11:33
Mottl pushed a commit to Mottl/arrow that referenced this pull request May 26, 2026
…type (apache#48002)
### Rationale for this change
Closesapache#44248
Arrow has no built-in canonical way of representing the `TIMESTAMP WITH
TIME ZONE` SQL type, which is present across multiple different database
systems. Not having a native way to represent this forces users to
either convert to UTC and drop the time zone, which may have correctness
implications, or use bespoke workarounds. A new
`arrow.timestamp_with_offset` extension type would introduce a standard
canonical way of representing that information.
Rust implementation: apache/arrow-rs#8743
Go implementation: apache/arrow-go#558
[DISCUSS] [thread in the mailing
list](https://lists.apache.org/thread/yhbr3rj9l59yoxv92o2s6dqlop16sfnk).
### What changes are included in this PR?
Proposal and documentation for `arrow.timestamp_with_offset` canonical
extension type.
### Are these changes tested?
N/A
### Are there any user-facing changes?
Yes, this is an extension to the arrow format.
* GitHub Issue: apache#44248
---------
Co-authored-by: David Li <li.davidm96@gmail.com>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
Co-authored-by: Felipe Oliveira Carvalho <felipekde@gmail.com>
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch 2 times, most recently from 49da897 to b6f73a4CompareJuly 2, 2026 18:11
@zeroshade
zeroshade requested review from Copilot, kou and lidavidmJuly 2, 2026 21:08

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the canonical extension type arrow.timestamp_with_offset to match the Arrow spec, and updates JSON marshaling/unmarshaling plus array/record equality behavior to better respect schema nullability (especially for nested types and IPC-vs-JSON roundtrips).

Changes:

  • Add TimestampWithOffsetType, TimestampWithOffsetArray, and a convenience builder supporting primitive/dictionary/REE encodings for per-row timezone offsets.
  • Update JSON marshaling (GetOneForMarshal) to take schema nullability into account, and adjust struct/record JSON unmarshaling to handle explicit null for required fields.
  • Update array/record/table equality logic to optionally ignore validity bitmaps when fields are considered non-nullable, and refresh test fixtures accordingly.

Reviewed changes

Copilot reviewed 43 out of 43 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
internal/json/json.goAdd IsNullMessage helper for null detection.
internal/json/json_stdlib.goStdlib variant of IsNullMessage.
arrow/ipc/cmd/arrow-ls/main_test.goUpdate expected struct field nullability formatting in output.
arrow/internal/arrjson/arrjson_test.goUpdate expected JSON schema nullability for struct children.
arrow/internal/arrdata/arrdata.goUpdate struct test data to mark inner fields nullable and adjust validity masks.
arrow/extensions/variant.goUpdate GetOneForMarshal signature to include nullability.
arrow/extensions/uuid.goUpdate GetOneForMarshal signature + call sites.
arrow/extensions/uuid_test.goMark UUID field nullable in record-builder test.
arrow/extensions/timestamp_with_offset.goNew extension type implementation + builder/array logic.
arrow/extensions/timestamp_with_offset_test.goNew tests covering primitive/dict/REE encodings, JSON roundtrip, IPC roundtrip.
arrow/extensions/json.goMake JSON extension marshaling honor caller-provided nullability.
arrow/extensions/extensions.goRegister TimestampWithOffsetType as a canonical extension type.
arrow/extensions/bool8.goUpdate GetOneForMarshal signature to include nullability.
arrow/compute/vector_sort_test.goMark fields nullable in C++ parity tests (to align with new nullability-aware behavior).
arrow/array/util.goPass schema field nullability into GetOneForMarshal during Record-to-JSON.
arrow/array/util_test.goExercise record JSON roundtrip under nullable vs non-nullable schemas.
arrow/array/union.goMake union JSON marshaling and approx-equality respect child-field nullability.
arrow/array/timestamp.goUpdate GetOneForMarshal and equality to respect nullable option.
arrow/array/struct.goMake struct JSON marshaling/unmarshaling nullability-aware; adjust struct equality accordingly.
arrow/array/struct_test.goAdd coverage for explicit null in required struct fields.
arrow/array/string.goUpdate string array marshaling and equality to respect nullable option.
arrow/array/record.goMake record JSON unmarshaling treat explicit null in required fields as empty value.
arrow/array/record_test.goExtend record-builder tests for new null-handling + JSON roundtrip.
arrow/array/numeric_generic.goUpdate numeric marshaling and equality to respect nullable option.
arrow/array/null.goUpdate GetOneForMarshal signature.
arrow/array/map.goThread nullable option through map equality.
arrow/array/list.goUpdate list marshaling and equality to respect element-field nullability.
arrow/array/json_reader_test.goUpdate JSON reader test to pass nullable flag when marshaling one value.
arrow/array/interval.goUpdate interval marshaling and equality to respect nullable option.
arrow/array/float16.goUpdate GetOneForMarshal signature to include nullability.
arrow/array/fixedsize_binary.goUpdate fixed-size-binary marshaling and equality to respect nullable option.
arrow/array/fixed_size_list.goUpdate fixed-size-list marshaling and equality to respect element-field nullability.
arrow/array/extension.goThread nullable option through extension array equality and marshaling.
arrow/array/encoded.goUpdate run-end-encoded marshaling and equality to thread nullable option.
arrow/array/dictionary.goUpdate dictionary marshaling and equality to thread nullable option.
arrow/array/decimal256_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal128_test.goUpdate test call sites for new GetOneForMarshal signature.
arrow/array/decimal.goUpdate decimal marshaling and equality to respect nullable option.
arrow/array/compare.goAdd nullable-aware equality plumbing and new APIs; thread schema nullability into comparisons.
arrow/array/compare_test.goAdd tests for WithNullable(false) behavior.
arrow/array/boolean.goUpdate boolean marshaling and equality to respect nullable option.
arrow/array/binary.goUpdate binary marshaling and equality to respect nullable option.
arrow/array.goChange GetOneForMarshal interface to accept nullability flag.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/compare.go Outdated
Comment threadarrow/array/record_test.go Outdated
Comment threadarrow/extensions/timestamp_with_offset.go Outdated

@serramatutuserramatutu left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zeroshade some comments:

Can you think of a way to decouple these 2 things (timestamp with offset and the inner nullability fixes)? So we can merge the extension type without having to wait on all the fixes for that and do that more carefully?

I'm sorry I left the branch based off of an old version of the inner nullability parity fixes. There are some differences with what I was doing in the new version of the JSON PR:

  • I ended up dropping the equalOpts because this is not what C++ does. It looks like they just validate the schemas on Validate() instead of making Equal() more lenient. I had not fixed that in that PR because there are some issues with the CSV reader (it calls Validate()) and it was becoming somewhat of a sprawl to fix it all. There are lots of behavior in arrow-go that never checks for the schema of structs.
  • I did not do the GetOneForMarshalNullable() and instead I changed the struct/record builder to check for IsValid() before calling GetOneForMarshal().

This commit adds a new `TimestampWithOffset` extension type that can be
used to represent timestamps with per-row timezone information. It
stores information in a `struct` with 2 fields, `timestamp=[T, "UTC"]`,
where `T` can be any `arrow.TimeUnit` and `offset_minutes=int16`, which
represents the offset in minutes from the UTC timestamp.
serramatutuand others added 13 commits July 9, 2026 15:29
Stop using `time=0` as a sentinel for null in `iterValues()`. Instead,
return a tuple of `(time.Time, bool)` where the bool indicates whether the
value is valid or not.
Added a test for it.
Start a run when none exists yet (e.g. leading null rows) instead of
continuing a nonexistent run, and only update lastOffset when a new run
starts. This stops a leading null from corrupting the run-ends/values
children and stops a null between equal offsets from splitting one
contiguous run into two. Add a noLastOffset sentinel, handle nil valids,
and add regression tests across all offset encodings.
Address roborev review findings on the run-end-encoded offset builder:
- AppendValues now treats a nil valids slice as all-valid so the parent
struct and its child builders stay the same length. Previously the
struct received zero slots while the children received len(values),
producing an inconsistent extension array.
- Override NewArray/NewExtensionArray to reset lastOffset to noLastOffset
on finalization, so a reused builder starts a fresh run instead of
continuing a run that belonged to the array just finalized (which could
emit run-ends without a matching value on a run-end-encoded offset).
- Add regression tests for nil validity and builder reuse across all
offset encodings.
Follow-up to the nil-validity fix: an empty (len 0) valids slice is also
all-valid, matching the convention of the other Arrow builders.
AppendValues now normalizes both nil and empty validity, and panics on a
genuine length mismatch (a non-empty valids whose length differs from
values).
Extend the validity regression test to cover the empty slice and add a
test asserting the length-mismatch panic.
Follow-up to the run-end-encoding fixes: AppendNull (and therefore the
UnmarshalOne(null) and AppendValueFromString(null) paths that call it)
appends a null offset run via the embedded struct builder but left
lastOffset stale. A subsequent value repeating the pre-null offset would
then continue the null run instead of starting its own, encoding that
value with the null offset run.
Override AppendNull/AppendNulls to reset lastOffset to noLastOffset, and
add a regression test for Append(value), AppendNull(), Append(same-offset
value) with run-end-encoded offsets.
Replace array.RecordEqual with a per-row check of the extension's
guarantees (instant, timezone offset, validity). RecordEqual compares the
inner non-nullable struct child validity bitmaps, which legitimately differ
between builder output (children valid under a null parent) and JSON reader
output (children null). That inner-nullability parity is tracked separately
and is out of scope for this extension type.
@zeroshade
zeroshadeforce-pushed the serramatutu/TimestampWithOffset/go branch from 03b049f to 2ef684cCompareJuly 9, 2026 19:30
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu updated this PR to be only the timestamp with offset extension type

…rays
iterValues started the run-end tracker at physical index 0 and compared
the logical row against absolute run ends, ignoring the array offset. A
sliced array whose offset began inside a later run decoded offsets from
the wrong run in Values() and MarshalJSON(). Initialize the tracker from
GetPhysicalOffset() and advance using absolute positions (Offset()+i),
and add a regression test slicing an REE-encoded array across run
boundaries.
@zeroshade

Copy link
Copy Markdown
Member

@serramatutu filed #918 to split out the nullability stuff

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
Comment threadarrow/extensions/timestamp_with_offset.go
- timeFromFieldValues: derive the zone-name sign from the whole offset so
a sub-hour negative offset keeps its sign (e.g. -30 minutes now formats
as "UTC-00:30" instead of "UTC+00:30"; integer hours is 0 there and
cannot carry the sign). Add a regression test.
- MarshalJSON: store the timestamp by value instead of taking the address
of the range variable.
- Values: rename the loop variable so it no longer shadows the time
package.
Widen offsetMinutes to int before negating so an out-of-range int16
offset (e.g. math.MinInt16) cannot overflow when computing the absolute
value used for the zone name.
Comment threadarrow/extensions/timestamp_with_offset_test.go Outdated
zeroshadeand others added 3 commits July 12, 2026 13:05
…erialize
Deserialize now rejects serialized metadata other than "" or "{}", matching the canonical extension spec (empty metadata) and the existing JSONType pattern. Adds a regression test.
Also documents (NOTE apache#918) that AppendNull writes a null into the non-nullable offset_minutes storage field; inner-field comparison/JSON parity is tracked in apache#918.
…set Deserialize
Adds TestTimestampWithOffsetTypeDeserializeInvalidStorage covering non-struct storage, wrong field count/names/order, non-UTC and empty timezone, nullable timestamp/offset fields, and invalid offset encodings (wrong primitive type, dictionary value not Int16, run-end-encoded value not Int16).
Comment on lines +49 to +52
// FIXME: Technically this should be non-nullable, but a Arrow IPC does not deserialize
// ValueNullable properly, so enforcing this here would always fail when reading from an IPC
// stream
// !offsetType.ValueNullable

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty major right? Can we link this to a tracking issue? (Does integration catch this?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of what was pulled out into #918 and friends

@zeroshade
zeroshade merged commit c9791ae into apache:mainJul 19, 2026
23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@serramatutu@zeroshade@lidavidm