Skip to content

[Do not merge] URN Data Type#206

Draft
dinoocch wants to merge 24 commits into
masterfrom
dino/urns
Draft

[Do not merge] URN Data Type#206
dinoocch wants to merge 24 commits into
masterfrom
dino/urns

Conversation

@dinoocch

Copy link
Copy Markdown
Member

Adds a first-class URN logical data type for LinkedIn-style URN columns (urn:<namespace>:<type>:<key>), with compact per-entity-typed storage, polymorphic multi-entity dictionaries, and predicate / transform support tuned for the URN representation.

End-to-end JMH benchmarks show 6.9× to 11× speedups vs the equivalent STRING baseline on the queries that matter, and 32–42% smaller on-disk storage.


What this gives you

  • A new logical DataType.URN you can declare on dimension columns.
  • Optional per-column urnTypes schema annotation that declares which entity-type prefixes are valid (e.g. urn:li:memberId) and the physical key type (LONG / INT / STRING). Unspecified valueType defaults to STRING.
  • One unified storage layout for every URN column, the polymorphic dictionary (see below). Single-entity columns are just polymorphic columns with one entry.
  • Untyped URN columns (URN with no urnTypes) auto-detect entity types and per-entity value types at segment build — LONG when every key for an entity parses as a long, otherwise STRING.
  • Two new transform functions: urnEntity(col) and urnKeyLong(col) (plus underscore aliases).
  • Two new ingestion transformers: UrnValidationTransformer, UrnKeyEncodingTransformer.
  • Predicate evaluator support: =, !=, IN, NOT IN, range, REGEXP_LIKE.
  • Filter pushdown: WHERE urnKeyLong(col) OP N rewrites to a direct LONG predicate on the underlying typed column.
  • No companion column anywhere: the only place entity index info lives per row is during build, in a transient row attribute that's dropped before metadata write.

Polymorphic dictionary (the only URN dictionary)

Every URN column with a dictionary uses UrnPolymorphicDictionary — a list of per-entity sub-dictionaries with cumulative global dict-ID offsets. Each sub-dictionary is a standard Pinot scalar dictionary chosen per entity from its declared valueType.

URN column "actorId"
├── entity: urn:li:memberId         → LongDictionary    (global dict IDs [0, N0))
├── entity: urn:li:corpUser         → StringDictionary  (global dict IDs [N0, N0+N1))
└── entity: urn:li:applicationName  → IntDictionary     (global dict IDs [N0+N1, N0+N1+N2))

The dictionary file on disk is the per-entity sub-buffers concatenated back-to-back. Per-entity ranges + dictClass + numBytesPerValue live in column metadata as urn.entityDictRanges, format urn:li:memberId,N0,LONG,8;urn:li:corpUser,N1,STRING,16;.... The reader slices the file at the byte boundaries derived from numBytesPerValue and wraps each region in the matching sub-dictionary class.

UrnPolymorphicDictionary itself is a thin router:

  • getStringValue(globalId) → linear scan over the small per-entity offset table, compute the local ID, ask the sub-dict for its value, prepend the entity prefix.
  • indexOf(urnString) → parse the entity prefix, call the matching sub-dict's indexOf(suffix), add the entity's global offset.
  • getDictIdsInRange(lo, hi) → same-entity ranges binary-search the sub-dict via insertionIndexOf; cross-entity ranges return empty (semantically undefined).
  • isSorted() returns true for single-entity columns (the one sub-dict is sorted, prefix is constant) and false for multi-entity (cross-entity dict-IDs are not lex-sorted in URN string space).

Single-entity URN columns are just polymorphic columns with one entry — no special-case reader, no historical UrnReconstructing*Dictionary wrappers. The prefix is prepended on read by the polymorphic wrapper itself.


Schema configuration

Single-entity typed (fastest path, smallest storage)

{
  "dimensionFieldSpecs": [
    { "name": "memberId", "dataType": "URN",
      "urnTypes": [{ "entityType": "urn:li:memberId", "valueType": "LONG" }] }
  ]
}
  • Physical storage: a polymorphic dictionary with one LongDictionary sub-dict, keys only — the urn:li:memberId: prefix is stripped from every entry.
  • Reads return full URN strings.
  • All predicates accept full URN string literals.

Multi-entity typed (numeric — original sorted-LONG case)

{
  "name": "actorId", "dataType": "URN",
  "urnTypes": [
    { "entityType": "urn:li:corpUser",        "valueType": "LONG" },
    { "entityType": "urn:li:corpGroup",       "valueType": "LONG" },
    { "entityType": "urn:li:applicationName", "valueType": "LONG" }
  ]
}
  • Three LongDictionary sub-dicts concatenated; per-entity ranges in metadata.
  • Same LONG key can occur in multiple ranges (one per owning entity).
  • Predicate WHERE col = 'urn:li:corpUser:100' parses the prefix, routes to the corpUser sub-dict, does a binary search for 100, returns a single absolute dict ID.

Multi-entity typed (mixed-type)

{
  "name": "subject", "dataType": "URN",
  "urnTypes": [
    { "entityType": "urn:li:memberId",  "valueType": "LONG"   },
    { "entityType": "urn:li:corpuser",  "valueType": "STRING" }
  ]
}
  • One LongDictionary region + one StringDictionary region in the same dictionary file.
  • The polymorphic wrapper routes per-row predicates and reads to the appropriate sub-dict.
  • No more "fall back to plain STRING storage of the full URN" for mixed columns.

Untyped (auto-detected)

{ "name": "rawUrn", "dataType": "URN" }
  • At segment build the creator parses every value as a URN, groups by entity prefix, and chooses a per-entity sub-dict type: LONG when every key for that entity parses as a long, otherwise STRING.
  • The synthesized urnTypes are persisted in segment metadata so the column behaves like a declared polymorphic column at load and query time.
  • UrnValidationTransformer rejects non-URN values at ingest (configurable fail / skip via continueOnError, mirroring JSON ingest).

valueType defaults to STRING

If a declared UrnType omits valueType, setUrnTypes normalizes it to STRING. Every URN entity ends up with a concrete sub-dictionary type — STRING is the safe default that accepts any key shape.


Query usage

Transform functions

SELECT urnEntity(memberId), urnKeyLong(memberId), COUNT(*)
FROM events
GROUP BY 1, 2
  • urnEntity(col) → returns the entity prefix string (e.g. "urn:li:memberId"). Three strategies picked at init(); none parse URN strings per row:
    • CONSTANT — single-entity column: Arrays.fill once per block.
    • RANGE_LOOKUP — multi-entity column with per-entity dict-ID ranges in metadata: linear scan over the (typically ≤16) range table to find the owning entity per dict ID.
    • DICT_LOOKUP — defensive fallback for multi-entity STRING-stored columns without entity ranges (legacy segments). Precomputes a dictId → entityPrefix array at init time; O(1) per row.
  • urnKeyLong(col) → returns the numeric key as long. Direct dictionary read on typed LONG entities.

Filtering

SELECT * FROM events WHERE memberId = 'urn:li:memberId:12345';
SELECT * FROM events WHERE memberId IN ('urn:li:memberId:1', 'urn:li:memberId:2');
SELECT * FROM events WHERE memberId BETWEEN 'urn:li:memberId:100' AND 'urn:li:memberId:200';
SELECT * FROM events WHERE urnKeyLong(memberId) > 1000;
SELECT * FROM events WHERE urnEntity(actorId) = 'urn:li:corpUser';

EQ / IN / range queries on full URN literals route through standard dictionary-based predicate evaluators — the polymorphic wrapper translates between full URN and per-entity-typed sub-dict lookups transparently.


Performance comparison

Dictionary storage (per entry)

Column type Per-entry width Dict file for 1M unique values
STRING (full URN as string) ~25 bytes padded to max length ~25 MB
URN(LONG) 8 bytes 8 MB (~3× smaller)
URN(INT) 4 bytes 4 MB (~6× smaller)
URN(STRING) (key suffix only) length of key suffix a few bytes less per entry than the full-URN STRING column (prefix is stripped)

The savings come from stripping the constant urn:li:memberId: prefix once into segment metadata instead of repeating it in every dictionary entry. The forward index is identical regardless — it stores dict IDs, not raw values.

For real LinkedIn-scale workloads the gap widens with key length:

Key length         STRING dict / entry (full URN)    LONG dict / entry    Ratio
4-digit            ~20 bytes                          8 bytes              2.5×
10-digit           ~26 bytes                          8 bytes              3.3×
13-digit           ~29 bytes                          8 bytes              3.6×

Lookup latency

Operation STRING column URN(LONG / INT) URN(STRING)
col = 'urn:li:memberId:42' binary search over UTF-8 strings strip prefix + parse 42 → binary search 8-byte longs strip prefix → binary search padded strings (byte compare)
Range col BETWEEN x AND y string lex compare per probe long compare per probe string lex compare per probe
urnEntity(col) per-row URN parse (or DICT_LOOKUP legacy fallback) Arrays.fill with the constant prefix same as LONG
urnKeyLong(col) per-row substring + Long.parseLong direct LongDictionary.getLongValue(dictId) per-row Long.parseLong(suffix)

The LONG/INT path wins biggest on urnKeyLong and on range/equality predicates — primitive comparisons vs UTF-8 string comparisons over longer payloads. urnEntity() is constant-time on any single-entity URN column because the prefix is in metadata.

Multi-entity URN configurations

Configuration Behavior
All entities LONG/INT Per-entity LongDictionary/IntDictionary regions; per-entity dict-ID ranges in metadata
Mixed (LONG + STRING) LongDictionary region back-to-back with StringDictionary region in one dictionary file; per-entity ranges select the sub-dict
Untyped (no schema urnTypes) Auto-detected at build: per-entity LONG when all keys numeric, else STRING; same wire format as declared

Ingest cost

UrnKeyEncodingTransformer parses each URN row once. For polymorphic columns it replaces the URN string with the per-entity-typed key (Long/Integer/String) and stashes the entity index in a transient row field. The stats collector dedupes on (entityIdx, value) instead of full URN strings — smaller hash keys, faster comparison.

Net summary

For numeric-keyed URN columns (the common LinkedIn case): 3–6× dictionary storage win plus measurably faster predicates and urnKey*() transforms because everything is primitive-typed end to end. For STRING-keyed entities the storage savings are smaller (just the prefix bytes), but urnEntity() becomes O(1) instead of per-row URN parsing.


JMH benchmark results

pinot-perf/src/main/java/org/apache/pinot/perf/BenchmarkUrnTransform.java — 100K rows, 10K unique keys. Single-entity uses urn:li:memberId. Multi-entity rotates among urn:li:corpUser, urn:li:corpGroup, urn:li:applicationName.

Storage (bytes on disk)

Total segment size:

Layout STRING baseline (untyped) URN typed Reduction
Single-entity 376,736 256,782 32%
Multi-entity 446,692 256,971 42%

Dictionary file alone:

Layout STRING baseline (untyped) URN typed Reduction
Single-entity 40,008 80,008 — (auto-detected single-prefix STRING dict happens to beat fixed-width LONG for 4-digit keys)
Multi-entity 269,981 80,000 70% (3.37×)

So a multi-entity URN column with 1M unique URNs at LinkedIn scale: ~30 MB STRING dict → ~8 MB LONG dict, a ~22 MB-per-segment saving on the dict alone.

End-to-end filter (full FilterPlanNode → execution, µs/op)

Query STRING baseline URN typed (single) URN typed (multi)
WHERE col = 'urn:...' 240 230 234
WHERE urnEntity(col) = 'urn:...' 13,941 1,647 (8.5×) 1,252 (11×)

Microbenchmarks (single-entity, µs/op)

Op STRING URN typed Speedup
urnEntity() transform 1,028 156 6.6×
urnKeyLong() transform 1,028 138 7.5×
Dictionary.indexOf(String) 3,044 911 3.3×
Dictionary.getStringValue(dictId) 443 217 2.0×

Simple EQ filter is essentially a wash because forward-index scan dominates the per-query cost. The dict-lookup speedup is visible in microbenchmarks and in IN-with-many-values queries.


Implementation notes

SPI (pinot-spi)

  • DataType.URN — logical URN type.
  • DimensionFieldSpec.urnTypes, setUrnTypes (normalizes unspecified valueType to STRING).
  • Predicates: usesPolymorphicUrnDictionary() (any URN with declared entities + valueTypes), usesSortedUrnDictionary() (all-numeric subset), hasTypedUrnStorage() (numeric stored type), isSingleUrnEntityType(), transientUrnEntityIdxFieldName().
  • DimensionFieldSpec.EntityDictRange — data class carrying entityType, endDictIdExclusive, dictClass, numBytesPerValue.
  • parseEntityDictRanges / serializeEntityDictRanges — 4-field-per-range wire format.
  • Urn.tryParse / Urn.parse utility.

Build (pinot-segment-local)

  • UrnValidationTransformer — rejects non-URN-shaped data at ingest (FAIL by default, SKIP when continueOnError=true, mirroring JSON ingest mode). Covers both declared-prefix and untyped columns.
  • UrnKeyEncodingTransformer — for polymorphic columns: parses the URN, looks up the entity index, replaces the column value with the per-entity-typed key (Long/Integer/String), stashes the entity index in a transient row field.
  • UrnMultiEntityPreIndexStatsCollector — dedupes (entityIdx, value) pairs via EntityValuePair.
  • UrnPolymorphicDictionaryCreator — writes per-entity typed sub-dictionaries concatenated into one file; computes per-entity dict-ID ranges + byte ranges; handles empty input (null-only columns) gracefully.
  • SegmentColumnarIndexCreator — routes any URN column to the polymorphic creator; tryUrnAutoDetection synthesizes polymorphic urnTypes for untyped columns (per-entity LONG when all keys numeric, else STRING).

Read (pinot-segment-local, pinot-segment-spi, pinot-core)

  • UrnPolymorphicDictionary — the only URN dictionary. Routes indexOf, getStringValue, getDictIdsInRange, etc. to the matching per-entity sub-dictionary.
  • DictionaryIndexType.read — when entityDictRanges is set, slices the file at per-entity byte offsets and dispatches each region to the declared sub-dict class (LongDictionary, IntDictionary, StringDictionary, ...).
  • ColumnMetadataImpl.fromPropertiesConfiguration reads urnTypes and urn.entityDictRanges (the latter using getStringArray + rejoin to survive PropertiesConfiguration's comma split).
  • Predicate evaluator factories (PredicateUtils, RangePredicateEvaluatorFactory, RegexpLikePredicateEvaluatorFactory, EQ/IN/NEQ/NIN) — case URN: routes URN columns through the STRING path; the polymorphic wrapper handles prefix/range translation.
  • UrnEntityTransformFunctionCONSTANT / RANGE_LOOKUP / DICT_LOOKUP strategies; never parses URN strings per row.
  • UrnKeyLongTransformFunction — direct LONG read on typed columns.
  • TransformFunctionFactory — registers urnEntity, urnKeyLong, and underscore aliases.
  • FilterPlanNode — rewrites urnKeyLong(typed_col) OP literal into a direct LONG-typed predicate on the underlying column.

Common (pinot-common)

  • UrnFunctions — scalar UDF helpers.
  • Standard case URN: additions in switch statements throughout serdes / encoders so URN flows through the STRING path.

Test coverage

98 URN-specific tests pass across pinot-spi, pinot-segment-local, pinot-core. Suites:

Suite Module Tests
DimensionFieldSpecUrnTest, UrnTest, FieldSpecTest pinot-spi 21+
UrnFunctionsTest pinot-common 29
UrnColumnSegmentTest, UrnPolymorphicDictionaryTest, UrnPolymorphicDictionaryCreatorTest, UrnKeyEncodingTransformerTest, UrnValidationTransformerTest, DictionaryIndexTypeTest pinot-segment-local 62
UrnTransformFunctionTest, UrnEntityMultiEntityTest, UrnEntityDictLookupTest, FilterPlanNodeUrnRewriteTest pinot-core 15+

Run locally with:

mvn test -pl pinot-segment-local,pinot-core,pinot-spi -am \
  -Dtest="DimensionFieldSpecUrnTest,UrnColumnSegmentTest,UrnPolymorphicDictionaryTest,UrnPolymorphicDictionaryCreatorTest,UrnKeyEncodingTransformerTest,UrnValidationTransformerTest,UrnTransformFunctionTest,UrnEntityMultiEntityTest,UrnEntityDictLookupTest" \
  -Dsurefire.failIfNoSpecifiedTests=false -Dscan=false -Dcheckstyle.skip

Always run the URN test suite with -am (or build all five modules together). A -pl pinot-core-only invocation will use stale upstream JARs from ~/.m2/.


Compatibility

  • Untyped URN columns auto-detect their structure at build; no schema change is required to opt in.
  • Adding urnTypes to a column is a forward-only schema change; existing segments aren't rewritten, but new segments will use the compact polymorphic storage.
  • Each segment is fully self-describing — its urnTypes list, per-entity dict-ID ranges, and per-entity dictClass/numBytesPerValue are persisted in segment metadata and read back at load time.

Adding entity types over time

When you update a table's schema to add a new entity type to a URN column, existing segments are not retroactively rewritten — they keep using the urnTypes ordering and per-entity types they were built with, and continue to query correctly for the entity types they know about. Newly ingested segments pick up the extended schema. Lookups against an old segment for an entity type it doesn't know about return zero matches, which is the semantically correct answer (the segment contains no rows with that entity). Validated by UrnColumnSegmentTest.multiEntitySegmentsWithDifferentUrnTypesBothQueryable and ...ReorderedUrnTypesReconstructCorrectly.

Layout history

Earlier drafts of this PR used: a STRING-encoded dictionary plus a per-row __urnEntityIdx companion column; then a sorted-LONG UrnSortedDictionary; then a polymorphic wrapper over typed sub-dicts. The current implementation is the polymorphic wrapper as the only URN dictionary path, with no companion column and no legacy wire format. Segments built with earlier drafts of this PR need re-ingestion. First-time ingestion against any released version of the URN feature uses the final layout directly.

dinoocch and others added 24 commits May 26, 2026 22:08
Adds the URN logical DataType with two storage paths:
- Single-entity typed (e.g., urn:li:memberId) — stores compact LONG keys
  with UrnReconstructingLongDictionary for transparent SELECT.
- Multi-entity / untyped — stores full URN strings; multi-entity also
  writes a companion <col>__urnEntityIdx INT column for fast urnEntity()
  lookup at query time.

Includes ingestion transformers (UrnKeyEncoding, UrnValidation), scalar
functions (urnEntity, urnKeyLong + underscore aliases), predicate
evaluator support (EQ/IN/RANGE/REGEXP_LIKE), and a JMH benchmark
showing 2.1x–7.4x speedups vs untyped STRING storage across four
operations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the two query-time optimizations that the previous URN commit
deferred.

Multi-entity companion-column fast path for urnEntity():
- UrnEntityTransformFunction gains a COMPANION_LOOKUP strategy that
  reads the pre-computed entity index from the companion
  <col>__urnEntityIdx INT column and indexes into a String[] of entity
  types. No URN parsing per row.
- ProjectPlanNode auto-projects the companion column whenever
  urnEntity(col) is referenced on a multi-entity URN column.
- SegmentColumnarIndexCreator now includes synthetic companion columns
  in the DIMENSIONS list written to metadata.properties so they are
  loaded as physical columns when the segment is opened.

Filter pushdown for urnKeyLong(typed col) OP literal:
- FilterPlanNode rewrites predicates of the form
  urnKeyLong(typed_long_urn_col) OP literal into
  col OP entityPrefix:literal, sending them through the existing
  identifier-based dictionary fast path with prefix stripping inside
  UrnReconstructingLongDictionary. Supports EQ/NEQ/IN/NOT_IN/RANGE.
  Multi-entity URN columns are skipped because the literal alone
  cannot identify which entity prefix to apply.

Adds 13 new tests (UrnEntityMultiEntityTest x3,
FilterPlanNodeUrnRewriteTest x10). All 119 URN tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lterOperator

Extends BenchmarkUrnTransform with end-to-end filter benchmarks that
compare typed vs untyped URN columns for "col = literal" and
"urnEntity(col) = literal" queries on both single-entity and multi-entity
columns, and prints on-disk segment sizes at setup.

The first benchmark run showed that "WHERE urnEntity(actorId) = ..."
queries on multi-entity typed columns were no faster than untyped — the
companion column was only being projected by ProjectPlanNode (used by
SELECT), not by ExpressionFilterOperator (used by WHERE on function
expressions). Hoisted the companion-projection helper into
UrnProjectionUtils so both call sites apply it. Multi-entity urnEntity
filter is now ~11x faster on typed vs untyped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multi-entity typed URN columns previously stored full URN strings in the
main dictionary (e.g. "urn:li:corpUser:docchial"). With ~16-20 bytes of
repeated prefix per dictionary entry, this dwarfed the actual key bytes.

UrnKeyEncodingTransformer now writes the main column as
"<entityIdx>:<suffix>" so the dictionary only carries the entity index
plus the URN key. UrnReconstructingMultiEntityDictionary wraps the
loaded STRING dictionary to reconstruct full URN strings on read and to
encode incoming full-URN predicate literals before delegating to the
underlying STRING lookup.

Benchmark (100K rows, 10K unique URNs, 3 entity types):
  multi-entity typed before: 472,879 bytes
  multi-entity typed after:  262,862 bytes  (44% reduction)
  vs untyped:                446,692 bytes  (1.70x smaller now)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Untyped URN columns now get the same prefix-stripping treatment as
typed single-entity columns, at segment build time, whenever every
unique value happens to share a common urn:<ns>:<type>: prefix.

SegmentColumnarIndexCreator scans the sorted unique values when
building a URN column's dictionary; if a single common prefix is
detected, the dictionary stores only the suffixes and the discovered
entity type is recorded in column metadata as urnTypes. Row-time
indexing strips the same prefix so forward-index dictId lookups still
match. UrnReconstructingStringDictionary wraps the loaded STRING dict
to reconstruct full URN strings on read and to strip the prefix from
incoming full-URN predicate literals.

UrnEntityTransformFunction now takes the CONSTANT fast path for *any*
single-entity URN column (declared or auto-detected), not just those
with LONG/INT storage. This makes urnEntity() effectively free on
both declared single-entity columns and on untyped columns where a
single prefix was discovered.

Benchmark (100K rows, 10K unique URNs):
  single-entity untyped storage: 376,736 -> 216,812 bytes (-42%)
  urnEntity() filter on untyped: 14,994 -> 1,543 us/op (~10x faster)
  urnEntity() transform untyped:  1,050 ->   156 us/op (~7x faster)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Defines the storage-level data class and segment-metadata property for the
sorted-LONG UrnDictionary design described in ~/urn_sorted_dict_design.md.
No behavior change yet -- segment build still emits the existing
encoded-STRING multi-entity layout. Commit 2 wires up the new dict
creator; commits 3-5 add the read path, drop the companion column, and
update tests/benchmark.

Adds:
  * DimensionFieldSpec.EntityDictRange data class: (entityType, endDictIdExclusive)
  * DimensionFieldSpec.parseEntityDictRanges / serializeEntityDictRanges
    for the "entityType,end;entityType,end;..." header form
  * V1Constants.URN_ENTITY_DICT_RANGES metadata key
  * SegmentColumnarIndexCreator writes urn.entityDictRanges when set
  * ColumnMetadataImpl reads urn.entityDictRanges at segment load
  * 6 unit tests in DimensionFieldSpecUrnTest

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the dictionary reader class for the sorted-LONG UrnDictionary layout
described in ~/urn_sorted_dict_design.md. Wraps a LONG buffer delegate
and uses the per-entity dictId ranges loaded in commit 1.

Lookups:
  * indexOf("urn:li:corpUser:100") parses the entity prefix to find the
    range, then range-restricted binary searches the LONG key.
  * getStringValue(dictId) finds which entity range owns the id and
    reconstructs the URN string from delegate.getLongValue.
  * getLongValue / getIntValue pass straight through to the delegate
    (urnKeyLong-style consumers get the raw key with zero overhead).
  * getDictIdsInRange supports same-entity range queries; cross-entity
    range queries return empty (semantically undefined).

No integration yet -- DictionaryIndexType still returns the existing
LongDictionary / UrnReconstructingMultiEntityDictionary depending on
layout. Commit 3 wires up the build + load paths.

Unit tests cover: range-restricted indexOf, key-collision disambiguation
across entities, reconstruction across all ranges, same-entity range
queries (inclusive/exclusive), cross-entity / unknown-entity rejection,
batch readStringValues, min/max, and pass-through accessors. 20 new
tests in UrnSortedDictionaryTest (145 URN tests pass total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ed LONG

End-to-end wiring of the sorted-LONG UrnDictionary layout for multi-entity
URN columns whose declared entity types all have numeric value types. The
encoded "<entityIdx>:<suffix>" STRING dictionary is replaced with a LONG
dictionary partitioned by entity range; UrnSortedDictionary handles all
read-time reconstruction and lookup.

Storage / build path:
  * DimensionFieldSpec.getEffectiveStoredType -> LONG for multi-entity
    URN (was STRING).
  * UrnKeyEncodingTransformer multi-entity branch now encodes only the
    numeric key into the main column. The companion entity-idx INT
    column is still written (parallel-write; commit 4 will drop it).
  * UrnMultiEntityPreIndexStatsCollector tracks unique (entityIdx,
    longKey) pairs by reading the companion column from the row.
    Reports isSorted=false so the sorted-forward-index path isn't
    triggered (dict IDs are not monotonic across entity boundaries even
    when the LONG values are).
  * SegmentPreIndexStatsCollectorImpl dispatches the row to the new
    collector via collect(value, row) for multi-entity URN columns.
  * UrnSortedDictionaryCreator (extends SegmentDictionaryCreator):
    consumes EntityKeyPair[] from the stats collector, writes a LONG
    buffer sorted by (entityIdx, longKey), and exposes the per-entity
    dict-ID ranges. indexOfSV takes an EntityKeyPair from the row
    indexer.
  * SegmentColumnarIndexCreator branches to UrnSortedDictionaryCreator
    for multi-entity URN columns, plumbs EntityKeyPair through
    indexRow, and surfaces the computed ranges on the FieldSpec so the
    metadata writer persists them.

Read path:
  * DictionaryIndexType.read returns UrnSortedDictionary (commit 2)
    when the loaded FieldSpec has entityDictRanges.
  * ColumnMetadataImpl.parseEntityDictRanges (commit 1) handles the
    PropertiesConfiguration comma-split via getStringArray + rejoin.

Tests / benchmarks:
  * UrnReconstructingMultiEntityDictionary still exists but is now
    unused (commit 4 will delete it).
  * UrnKeyEncodingTransformerTest multi-entity assertions updated to
    expect numeric LONG keys on the main column. All 145 URN tests
    pass.

Companion column is still auto-created in this commit -- the query
layer still uses COMPANION_LOOKUP for urnEntity(). Commit 4 will drop
the companion column entirely and switch urnEntity() to RANGE_LOOKUP
over the entity ranges.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eliminates the per-row __urnEntityIdx INT companion column for
multi-entity URN columns. Entity index is now stashed on the row in a
transient field at ingest time (UrnKeyEncodingTransformer), consumed by
the stats collector and forward-index code, and dropped before metadata
write. No new physical column lives in the segment.

SPI:
  * DimensionFieldSpec.needsCompanionEntityIdxColumn -> usesSortedUrnDictionary
  * companionEntityIdxColumnName / createCompanionEntityIdxFieldSpec /
    COMPANION_ENTITY_IDX_SUFFIX -> deleted, replaced by
    transientUrnEntityIdxFieldName / URN_TRANSIENT_ENTITY_IDX_PREFIX

Build path:
  * UrnKeyEncodingTransformer writes the entity index to
    __urnEntityIdxBuild_<col> (transient) instead of a column.
  * SegmentColumnarIndexCreator / SegmentIndexCreationDriverImpl /
    SegmentPreIndexStatsCollectorImpl no longer auto-create, register,
    or write the companion column. _companionFieldSpecs map removed,
    DIMENSIONS list no longer gets the synthetic suffix appended.
  * UrnMultiEntityPreIndexStatsCollector reads the transient row field
    instead of a sibling column.

Query path:
  * UrnEntityTransformFunction: COMPANION_LOOKUP strategy replaced with
    RANGE_LOOKUP. It reads the main column's dict IDs and finds the
    owning entity range with a linear scan over the metadata range
    table (typically <=16 entries). No per-row companion read, no URN
    string parsing.
  * UrnProjectionUtils deleted -- nothing to inject into projections.
    Callers in ProjectPlanNode and ExpressionFilterOperator updated.
  * DictionaryIndexType: branch returning UrnReconstructingMultiEntityDictionary
    removed (obsoleted by UrnSortedDictionary returned via the LONG path).
  * UrnReconstructingMultiEntityDictionary file deleted.

Tests:
  * UrnEntityMultiEntityTest: companionColumnIsPresent ->
    entityDictRangesPersistedInMetadata; urnEntityUsesCompanionFastPath
    -> urnEntityUsesRangeLookupFastPath; the
    urnEntityFallsBackToParseStringWhenCompanionNotProjected case is
    obsolete (no companion column to omit) and was dropped.
  * UrnKeyEncodingTransformerTest multi-entity asserts updated to read
    the transient field instead of a companion column.
  * All 144 URN tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backwards-compatibility tests confirming that segments built with
different urnTypes lists (extended set, reordered set) both load and
query correctly. Each segment uses the urnTypes ordering it was built
with -- existing segments do not need to be rewritten when a column's
urnTypes list is extended in the table schema.

Tests:
  * multiEntitySegmentsWithDifferentUrnTypesBothQueryable: a v1 segment
    with [memberId, groupId] and a v2 segment with [memberId, groupId,
    applicationName] both load; v1 metadata keeps its 2-entry list and
    queries for the new applicationName entity miss (zero rows have it,
    semantically correct), v2 queries for all three.
  * multiEntitySegmentsWithReorderedUrnTypesReconstructCorrectly: a v1
    segment with [memberId, groupId] and a v2 segment with [groupId,
    memberId] both reconstruct the same URN strings on read -- per-
    entity ranges in segment metadata carry the entity type, not just
    its ordinal.

146 URN tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a v3-layout index_map parser so the benchmark prints the dictionary
file size for each segment alongside the total. Total segment size is
dominated by the forward index (~70% of bytes), so the dict-only
number is what actually shows the URN feature's storage win.

Numbers from the run (100K rows, 10K unique keys):
  multi-entity untyped (STRING dict): 269,981 bytes
  multi-entity typed   (sorted LONG): 80,000  bytes  (3.37x smaller)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces UrnSortedDictionary with UrnPolymorphicDictionary: a list of
per-entity sub-dictionaries with cumulative dict-ID offsets. Each
sub-dict is a standard Pinot Dictionary (LongDictionary, StringDictionary,
IntDictionary, ...). The wrapper routes every operation -- indexOf,
getStringValue, getLongValue, getDictIdsInRange -- to the appropriate
sub-dictionary and re-attaches the entity prefix on read.

This is the architecturally-cleaner restatement of the same design: the
current all-numeric-keys case still produces one composite LONG buffer
sliced into per-entity LongDictionary views, with the existing
urn.entityDictRanges metadata recording the slice boundaries. Wire
format is unchanged; this commit is a reader-side refactor.

The next commits extend this to genuinely polymorphic sub-dictionaries
(mixed LONG / STRING / INT per entity), wire up the build path to
choose the natural delegate type per entity, add multi-prefix
auto-detect for untyped URN columns, and introduce a fail/skip/pause
ingest mode for non-URN-shaped data so PARSE_STRING goes away entirely.

  * New: UrnPolymorphicDictionary + 19 unit tests (mixed LONG + STRING
    sub-dicts exercise the polymorphic routing).
  * Removed: UrnSortedDictionary and its 20 tests (superseded).
  * DictionaryIndexType.read constructs per-entity LongDictionary views
    by slicing the main LONG buffer at the entity-range offsets, then
    composes them into a UrnPolymorphicDictionary.

129 URN tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends segment-build URN auto-detection from single-prefix-only to
multi-prefix. If every value in an untyped URN column parses as a
simple URN with a numeric key and at least two distinct entity
prefixes appear in the data, the column is promoted to multi-entity
URN for the rest of the build: urnTypes are synthesized in observed
order, the sorted-LONG UrnDictionary layout is used, and the segment
loads as if the user had declared the column with these urnTypes.

Build-path changes:
  * maybeStripUrnPrefix tries single-prefix first; on failure, runs
    tryMultiPrefixUrnDetection. When successful, returns EntityKeyPair[]
    so UrnSortedDictionaryCreator handles the rest.
  * SegmentColumnarIndexCreator picks the dict creator after
    preprocessing so EntityKeyPair[] from auto-detect routes through
    UrnSortedDictionaryCreator just like declared multi-entity.
  * indexRow handles auto-detected multi-entity columns by parsing the
    URN string inline (no transformer ran on the untyped column).
  * resolveFieldSpec caches synthetic FieldSpecs so per-build mutations
    (setEntityDictRanges) persist across resolve calls.
  * Skip min/max metadata for multi-entity URN columns: cross-entity
    comparisons aren't semantically meaningful and the values won't
    round-trip through LONG parsing at load.

Test updates:
  * UrnColumnSegmentTest.untypedUrnHasNoUrnTypesInMetadata renamed to
    untypedUrnAutoDetectsMultiEntityFromIngestedData; asserts that
    multi-prefix all-numeric data triggers auto-detection.
  * New untypedUrnAutoDetectedSegmentIsQueryable verifies the segment
    round-trips: all four ingested URNs are indexable, getStringValue
    reconstructs correctly, and unknown lookups return NULL_VALUE_INDEX.

148 URN tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UrnValidationTransformer now rejects any value that is not a simple URN
in untyped URN columns, mirroring the existing typed-column check. With
shape guaranteed at ingest time, every URN column reaches query time
with structured metadata, so urnEntity() never needs to parse strings
per row.

UrnEntityTransformFunction's PARSE_STRING strategy is replaced with
DICT_LOOKUP for multi-entity STRING-stored columns (mixed-type entities):
init walks the dictionary once to build a dictId -> entityPrefix array,
then per row reads the dict ID and indexes into the array. The CONSTANT
and RANGE_LOOKUP strategies are unchanged. Rejection behaves like JSON
ingest: throw when continueOnError=false, or null the value and mark the
row INCOMPLETE when continueOnError=true.
…ader + creator)

Extends multi-entity URN dictionary storage so each entity can use its own
typed sub-dictionary instead of a single uniform LONG buffer. Wire format
adds two fields per EntityDictRange:

  - dictClass: INT/LONG/STRING/...; selects the sub-dictionary reader
  - numBytesPerValue: fixed-width entry size in the entity's sub-buffer

Metadata serialization stays backwards compatible. Ranges that are all
default LONG/8-byte still emit the legacy two-field "entityType,end" form;
ranges with mixed sub-dict types emit the four-field
"entityType,end,dictClass,numBytesPerValue" form. The parser accepts both.

DictionaryIndexType.read() hoists multi-entity URN columns above the
effective-stored-type switch and slices the file into per-entity byte
regions, wrapping each region in its declared sub-dict reader
(LongDictionary, StringDictionary, ...). Per-region byte ranges are
derived cumulatively from numBytesPerValue. Existing all-LONG segments
still load through the LongDictionary path with identical byte layouts.

UrnPolymorphicDictionaryCreator writes the polymorphic file: per-entity
typed sub-dictionaries concatenated back-to-back. When every entity is
LONG/8-byte the file is byte-identical to the prior sorted-LONG layout,
so swapping it in for UrnSortedDictionaryCreator is wire-compatible.

EntityValuePair generalizes EntityKeyPair to any Comparable value so
mixed-type rows can flow through the same sort/index pipeline.

Not yet wired into SegmentColumnarIndexCreator -- that swap (and the
stats-collector generalization needed to feed mixed-type values into the
new creator) is the next step.
…Creator

Transparent swap -- the polymorphic creator writes byte-identical output
to the legacy sorted-LONG creator when every entity is LONG/8-byte, so
all 121 existing URN tests pass without modification.

This unifies the multi-entity URN build path around one creator. Mixing
in non-numeric entities (next step) just requires generalizing the stats
collector and per-row encoder to feed EntityValuePair[] instead of
EntityKeyPair[]; the creator side is already ready.
End-to-end wire-up for the polymorphic dictionary layout. Mixed-type
multi-entity URN columns (e.g. LONG memberId + STRING corpuser) now
write per-entity typed sub-dictionaries instead of falling back to plain
STRING storage.

DimensionFieldSpec gains usesPolymorphicUrnDictionary(): true for any
multi-entity URN with declared LONG/INT/STRING valueTypes (superset of
the existing usesSortedUrnDictionary which stays as the narrower
all-numeric predicate). Wiring uses the broader predicate everywhere
except where the all-numeric fast path is actually load-bearing.

UrnKeyEncodingTransformer encodes per-entity-typed values for polymorphic
columns: Long for LONG entities, Integer for INT, String for STRING. The
column value type is heterogeneous across rows, but the transient entity
index keeps the type paired with its entity so downstream readers stay
type-safe via EntityValuePair.

UrnMultiEntityPreIndexStatsCollector decoupled from LongColumnPreIndexStats
and rebuilt around EntityValuePair so mixed-type values flow through the
same dedup/sort pipeline as the all-LONG case.

SegmentColumnarIndexCreator.indexRow builds EntityValuePair (not
EntityKeyPair) for polymorphic columns, with the value pulled directly
from the row's typed column value.

UrnEntityDictLookupTest now verifies the mixed-type column persists typed
EntityDictRanges (LONG memberId + STRING corpuser) and that urnEntity()
returns the right prefix per row via RANGE_LOOKUP. DICT_LOOKUP becomes
unreachable for new segments since every URN column now has structured
per-entity metadata.
Drops the size >= 2 restriction on usesPolymorphicUrnDictionary(): a
single-entity URN column becomes a polymorphic dictionary with one
EntityDictRange entry. The standalone UrnReconstructingLongDictionary
and UrnReconstructingStringDictionary wrappers no longer need to fire
for declared single-entity columns -- the polymorphic wrapper already
prepends the entity prefix on read, and the metadata overhead is one
extra range row.

To make this work end to end:

- UrnPolymorphicDictionaryCreator handles empty input (a column whose
  only rows are null) by emitting zero-width per-entity ranges.

- UrnMultiEntityPreIndexStatsCollector associates the column's default
  null sentinel with entity 0 so it gets a dict ID and the polymorphic
  forward index can resolve null rows. SegmentColumnarIndexCreator.indexRow
  mirrors that, wrapping the bare typed value as EntityValuePair(0, ...)
  when the transient entity index is absent.

- UrnPolymorphicDictionary.isSorted() reports true for single-entity
  columns -- one sub-dict, prefix prepended on read, ordering preserved
  -- so the sorted range-predicate evaluator runs. For multi-entity it
  stays false (cross-entity isn't lex-sorted as URN strings).

- UrnPolymorphicDictionary.getDictIdsInRange computes range bounds via
  the sub-dict's insertionIndexOf instead of delegating to the sub-dict's
  getDictIdsInRange (which inherits the base "should not be called"
  implementation from BaseImmutableDictionary). The result is the
  half-open [from, to) relative dict-ID range, shifted by the entity's
  offset.
…ults to STRING

All URN columns with dictionaries now use the polymorphic layout. Two
unifications land here:

1. DimensionFieldSpec.setUrnTypes defaults any UrnType with no declared
   valueType to STRING. The polymorphic layout needs a concrete
   sub-dictionary type per entity; STRING accepts any key shape and is
   the safe default. Schemas that explicitly set valueType=LONG/INT/STRING
   are unchanged.

2. Untyped URN columns (no schema-declared urnTypes) flow through a
   single auto-detect path. tryUrnAutoDetection parses every value as a
   URN, groups by entity prefix, and chooses a per-entity sub-dict type:
   LONG when every key for that entity parses as long, otherwise STRING.
   It then emits an EntityValuePair[] for the polymorphic creator and
   stashes the discovered urnTypes (with their per-entity valueType) so
   resolveFieldSpec surfaces a synthetic polymorphic FieldSpec at
   metadata-write time.

The old single-prefix-strip path is gone: _discoveredUrnEntities,
maybeStripUrnPrefixForIndex, discoverCommonUrnEntity, and
extractEntityPrefix are all removed. Single-prefix columns now run
through the same polymorphic codepath as multi-prefix.
With every URN column now flowing through the polymorphic dictionary,
the legacy single-entity reconstructing wrappers are dead code. The
polymorphic dictionary's getStringValue() already prepends the entity
prefix on read and its indexOf()/insertionIndexOf() already accept full
URN strings, so wrapping a plain LongDictionary or StringDictionary is
no longer needed.

DictionaryIndexType.read drops both special-case branches and the
wrapper imports; the LONG and STRING cases become plain scalar loads
again. Single-entity URN columns either populate entityDictRanges and
take the polymorphic load path at the top of read(), or fall through to
the scalar load and behave like any other column.
- untypedMixedKeyTypesDiscoverPerEntityValueType: untyped column with
  mixed LONG (memberId) + STRING (corpuser) keys auto-detects per-entity
  sub-dict types and round-trips full URN strings through the polymorphic
  dictionary.

- untypedSinglePrefixDiscoveredAsPolymorphic: single-prefix untyped
  column becomes a 1-entity polymorphic column with LONG sub-dict; no
  more special-case prefix-stripping path.

- declaredUrnTypesWithoutValueTypeDefaultToString: setUrnTypes
  normalizes any UrnType missing a valueType to STRING.
The all-LONG byte-identical wire compat was a refactor safety net while
swapping UrnSortedDictionaryCreator -> UrnPolymorphicDictionaryCreator;
it's not needed in production because the URN feature itself is new in
this PR, so no existing segments use the legacy form.

Collapsing to one format:

- serializeEntityDictRanges always emits the 4-field
  "entityType,end,dictClass,numBytesPerValue" form.

- parseEntityDictRanges only accepts the 4-field form; 2-field input
  is rejected as malformed.

- EntityDictRange drops the 2-arg legacy constructor; all callers
  now pass dictClass and numBytesPerValue explicitly.

Segments are now self-describing -- the sub-dict type for each entity
is stated in the metadata rather than inferred from defaults.
… file

1. usesSortedUrnDictionary() lost its size > 1 guard, causing
   FilterPlanNode to skip the urnKeyLong-pushdown rewrite for
   single-entity LONG URN columns. Restored the guard; the
   FilterPlanNodeUrnRewriteTest single-entity cases pass again.

2. Mixed-type polymorphic columns + null URN row caused a build-time
   ClassCastException because NullValueTransformer filled the column
   with the URN sentinel String, but the polymorphic creator's LONG/INT
   branch unconditionally cast the value to Number. Made
   DimensionFieldSpec.getDefaultNullValue() return the entity-0-typed
   default for polymorphic columns, so the sentinel matches entity 0's
   sub-dictionary type.

3. UrnPolymorphicDictionary.indexOf / insertionIndexOf /
   getDictIdsInRange would propagate NumberFormatException out of the
   query path when a range bound's suffix was non-numeric on a LONG
   sub-dict (e.g. WHERE col BETWEEN 'urn:li:memberId:abc' AND
   'urn:li:memberId:zzz'). Wrapped sub-dict calls in safeIndexOf /
   safeInsertionIndexOf helpers that treat parse failure as
   "no such value" (NULL_VALUE_INDEX / ~length).

4. tryUrnAutoDetection promoted the URN null sentinel
   (DEFAULT_DIMENSION_NULL_VALUE_OF_URN = "urn:pinot:null:0") to a
   phantom "urn:pinot:null" entity in every untyped URN column with
   null rows. Filter the sentinel before entity discovery and reserve
   a type-appropriate null placeholder in entity 0's sub-dictionary so
   per-row indexer can still resolve null rows. indexRow's auto-detect
   branch maps sentinels to the same placeholder.

Plus: removed .claude/scheduled_tasks.lock from the index and added
.claude/ to .gitignore. The lockfile was an accidentally-committed
Claude Code runtime artifact with no license header, causing
apache-rat:check to fail with "Too many files with unapproved license".
RAT now reports Unapproved: 0.

Tests: 4 new regression tests, all 112 URN tests pass.
@siddharthteotia

siddharthteotia commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

@dinoocch - I can spend time reviewing this today if this is somewhat ready / in shape. Let me know if not yet.

*/
@JsonIgnore
@Nullable
public List<EntityDictRange> getEntityDictRanges() {

@siddharthteotia siddharthteotia Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Assuming DictRange here is referring to segment dictionary info -- I don't think this information should be kept here. Dictionary is a property of segment storage format. Abstraction wise, schema / fieldSpec should not be aware of segment storage internals / state.

This belongs in ColumnMetadata or any of it's equivalents that live closer to segment layout. May be create a new UrnSegmentLayout.

* the sorted-LONG dictionary, then dropped — it is not part of the segment schema and is
* never persisted as a column.
*/
public static final String URN_TRANSIENT_ENTITY_IDX_PREFIX = "__urnEntityIdxBuild_";

@siddharthteotia siddharthteotia Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Similar feedback here (as I gave earlier - https://github.com/linkedin/pinot/pull/206/changes#r3343763062).

This should not be in SPI

It should ideally be in pinot-segment-local (and may be exposed via a helper if needed)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

lol yeah lots to clean up

* </pre>
*/
@Nullable
private List<UrnType> _urnTypes;

@siddharthteotia siddharthteotia Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Clarifying question - Is it possible to declare an URN without urnTypes ?

* standard dictionary encoding. Correct but least efficient.</li>
* </ul>
*
* Example schema snippet:

@siddharthteotia siddharthteotia Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we support nested / composite URN ?

Something like "urn:li:dataset: (<another urn)"

How will the urnType be defined in this case? The valueType for the entityType will be URN ? I did not see an example for this in the PR description and yet to see in the code (so far during the review).

@siddharthteotia siddharthteotia Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Upon reading more code, I am somewhat more confused about how we are handling these. Suggest clearly calling out our stance on this in the PR description.

  • The UrnKeyEncodingTransformer seems to be silently NULLing out composite URN row if it detects URN is not simple.
  • The UrnValidationTransformer is explicitly rejecting composite URNs by enforcing urn.isSimple()
  • The UrnValidationTransformer is potentially accepting them. It checks only entrityPrefix membership and that will not catch urn:li:dataset( < another urn>)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oops sorry this is not done! It's just a draft that I pointed claude at and steered it away from stupid ideas...

So there's probably a lot of kinda weird things - I wanted to start with a proof of concept to iterate on the savings a little and then invest in some more polish, but don't have much (any) dedicated time on this :)

I figure simple urns are a huge proportion of our current usage. This pr will handle the complex ones by treating the remaining part as a string

@siddharthteotia siddharthteotia Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No problem. I happen to read the PR for some time and left feedback as otherwise I would have forgotten.

* overlap that motivates the sorted-LONG layout; and</li>
* <li>every entity's {@code valueType} is {@code LONG} or {@code INT}.</li>
* </ul>
* Callers that need only "is this column polymorphic" (entity-idx-stashing, polymorphic creator

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What are we referring to as "callers" here ?

Who is making a decision on how the URN is being laid out on disk ?

@siddharthteotia

Copy link
Copy Markdown
Collaborator

Partial Review - At a high level, I think DimensionFieldSpec is being bloated with decision ./ info / state on how the URN is stored on disk. I don't think that's the right abstraction. I recommend revisiting it.

return _totalLength == 0 ? null : getStringValue(0);
}

@Override

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is it not possible to sort in lex order at the time of construction ? The current implementation will give incorrect min and max.

I see that for immutable segments, we are simply skipping the min and max for multi-entity URNs.

However, mutable segments cache min and max as rows arrive IIRC. Similarly during segment commit, we may copy wrong min and max ?

@siddharthteotia

Copy link
Copy Markdown
Collaborator

Leaving some more feedback / questions before I context switch out of this PR review for some time. Will review again when you find time to iterate on it.

  • Do we need urnKeyLong() as a SQL function ? I feel the same outcome can potentially be achieved if the planner does implicit literal coercion

Example --

WHERE urnKeyLong(memberId) = 1000

works via FilterPlanNode rewrite to

WHERE memberId = 'urn:li:memberId:1000'.

Alternative - Planner sees memberId is a single-entity LONG-typed URN, auto-prepends the prefix.

WHERE memberId = 1000
  • What are the semantics (functionality) for a filter on URN column matching multiple entities ? I checked this out locally and ran a test for WHERE actorId = 12345 on a multi-entity URN column and got empty results whereas the data did contain 12345

  • In the initial version, do we plan to support for consuming segments as well ?

  • Part of me thinks that in the initial version, we should restrict the support to only simple URNs and then treat URN as a first class data type in predicate evaluator like other primitives. My first impression is that the current implementation is trying to cater to both simpler and complex URNs while leaving the latter as standard STRING operations and that is preventing us from truly supporting URN as a first class type in the engine (across all dimensions of storage, query, and user abstraction / SQL construct wise).

Sign up for free to 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.

2 participants