Skip to content

feat(core): add DECIMAL (BigDecimal) property data type - #3209

Open
SebastianGruza wants to merge 1 commit into
apache:masterfrom
SebastianGruza:feat/decimal-datatype
Open

SebastianGruza wants to merge 1 commit into
apache:masterfrom
SebastianGruza:feat/decimal-datatype

Conversation

@SebastianGruza

Copy link
Copy Markdown
Contributor

Purpose of the PR

The numeric property types today are BYTE/INT/LONG/FLOAT/DOUBLE. Values that do not fit a long and must not be rounded (token balances in wei, up to 2^256 - 1; money amounts in general) can only be stored as TEXT, which loses the one place where the server itself does arithmetic: update_strategies in PUT /graph/{vertices,edges}/batch (SUM / BIGGER / SMALLER). UpdateStrategy already computes in BigDecimal, but the result goes back to the property's type: with DOUBLE a SUM of 10^18 + 1 is 10^18, and TEXT fails the strategy's Number type check. This PR adds an exact decimal type so that accumulating balances during an import works. The design points were posted in #3206 on 2026-09-14; no objections so far.

Main Changes

  • DataType.DECIMAL(12, "decimal", BigDecimal.class) in the server enum and in the hugegraph-struct copy, with isDecimal() and valueToDecimal() (exact for BigDecimal, BigInteger and integral Java numbers; Float/Double through their shortest decimal representation; decimal strings). PropertyKey.Builder.asDecimal(), REST data_type: DECIMAL.
  • No sort key, no index, no OLAP range: isNumber() stays false on purpose; PropertyKeyBuilder, IndexLabelBuilder and EdgeLabelBuilder reject these with an explicit message. There is no fixed-width byte-order-preserving encoding for a decimal, and faking one through LongEncoding would be lossy. SUM/MAX/MIN aggregate types on the property key are allowed, as for numbers.
  • Encoding in BytesBuffer (server core and struct): vint(len) + unscaled two's-complement bytes + vint(scale). Exact for any precision, scale preserved, 33 bytes for a uint256. Existing encodings untouched; OffheapCache gets the new value type appended at the end of its enum.
  • JSON: always a plain string on output (toPlainString(), HugeGraphSONModule); a string or a number literal accepted on input. A JSON number is a double to most clients, so a string is the only lossless representation.
  • ConditionQuery compares exactly when one side is a BigDecimal (instead of through doubleValue()); the store-side row decoder (GraphStoreIterator) maps a decimal to a string variant.
  • BatchAPI.updateExistElement: the JSON value is normalised through the property key before the update_strategies strategy runs, on both paths (two entries of one id within a request; request vs stored element). Found by the new API test: the strategy used to receive the raw JSON value, which only worked for the types Jackson happens to produce, so a decimal (or a date) sent as a string failed the type check.

Known limit, documented in the issue: in the batch update a fraction has to be sent as a string, because the request's properties map is parsed by Jackson before any schema is known (0.000000000000000001 becomes a double literal); integral literals are exact. hugegraph-client / loader / Hubble will get the type in a separate toolchain change.

Verifying these changes

  • Trivial rebase, no need to test
  • Unit tests / core tests / API tests added and passing locally (JDK 11, through the CI scripts):
    • unit/core/DataTypeTest: predicates, valueToDecimal for uint256 max, wei scale, integral and binary numbers, invalid strings
    • unit/serializer/BytesBufferTest: exact byte layout for -1.5, 0, uint256 max; scale round trip; decimal lists
    • unit/util/JsonUtilTest: string on output, string or number on input
    • core/PropertyKeyCoreTest: create, value normalisation, calcSum(), list cardinality
    • core/IndexLabelCoreTest: secondary / range / shard / unique on a decimal all rejected
    • core/EdgeLabelCoreTest: decimal sort key rejected, decimal edge property fine
    • core/VertexCoreTest: uint256 and 18-fraction-digit values through commit and reload, exact has() vs the neighbouring value, gt/lt/gte, update, invalid values
    • api/VertexApiTest: PUT /graph/vertices/batch with SUM: 2^256-2 + 1 (number literal), then two entries of one vertex in one request ("0.000000000000000000", "0.000000000000000001"), then BIGGER; response and GET carry the exact string
    • struct PropertyKeyTest: groovy schema string, struct BytesBuffer round trip
    • Results: struct 4/4; unit-test 687/688 (SecurityManagerTest.testFile fails identically on plain master on a non-English locale, unrelated); core-test on rocksdb and memory for the four touched classes green (383 tests on rocksdb); api-test,rocksdb 162 tests, 0 failures.
  • Cluster check on HStore (PD + 3 stores) and RocksDB: 10 000 vertices with balance/hi/lo DECIMAL, 5 rounds of PUT /graph/vertices/batch with update_strategies: {balance: SUM, hi: BIGGER, lo: SMALLER}, random increments up to 2^255 with 18 fraction digits, 30 % negative, batches of 500, 4 writer threads, 50 accounts per round appearing twice in one request; 2 000 edges with a decimal amount behind an INT sort key. Every value read back and compared exactly with a Python Decimal oracle: 50 250 upserts, 0 errors, 0 / 10 000 mismatches on both backends; decimal sort key / range index rejected with the intended message. Script and logs: cluster/decimal_sum_bench.py and results/decimal/ in https://github.com/SebastianGruza/hugegraph-validation.
  • Docs: the property-key data type list in the hugegraph-doc repository needs a DECIMAL row; I will open that PR once the type is in.

Note on public API

New enum constant DataType.DECIMAL (code 12), new builder method PropertyKey.Builder.asDecimal(), new REST value data_type: DECIMAL. No change to existing types, encodings or endpoints; graphs without decimal properties are unaffected.

A new DataType.DECIMAL(12) stores arbitrary-precision decimals exactly:
unscaled two's-complement bytes plus scale in BytesBuffer (server and
struct copies), a string on the JSON wire (both a string and a number
literal are accepted on input), exact equality in ConditionQuery, and the
store-side row decoder maps it to a string variant.

It is deliberately not a "number" in the DataType.isNumber() sense: there
is no fixed-width sortable encoding, so a decimal property key can't be a
sort key, an index field of any type, or an OLAP range property; the
schema builders reject those explicitly. SUM/MAX/MIN aggregate types and
the batch-update SUM/BIGGER/SMALLER strategies, which already compute in
BigDecimal, keep the full precision (e.g. uint256 token balances).

Tests: DataTypeTest, BytesBufferTest, JsonUtilTest, PropertyKeyCoreTest,
IndexLabelCoreTest, EdgeLabelCoreTest, VertexCoreTest, VertexApiTest
(batch update with SUM/BIGGER on 2^256-1), struct PropertyKeyTest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.58824% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.05%. Comparing base (60c8803) to head (a28554e).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...e/hugegraph/schema/builder/PropertyKeyBuilder.java 0.00% 4 Missing ⚠️
.../java/org/apache/hugegraph/api/graph/BatchAPI.java 78.57% 2 Missing and 1 partial ⚠️
...apache/hugegraph/backend/query/ConditionQuery.java 0.00% 3 Missing ⚠️
...va/org/apache/hugegraph/io/HugeGraphSONModule.java 84.61% 1 Missing and 1 partial ⚠️
.../java/org/apache/hugegraph/schema/PropertyKey.java 66.66% 0 Missing and 1 partial ⚠️
...che/hugegraph/schema/builder/EdgeLabelBuilder.java 50.00% 0 Missing and 1 partial ⚠️
...he/hugegraph/schema/builder/IndexLabelBuilder.java 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3209      +/-   ##
============================================
+ Coverage     37.86%   38.05%   +0.19%     
- Complexity     6586     6624      +38     
============================================
  Files           800      800              
  Lines         68985    69070      +85     
  Branches       9172     9192      +20     
============================================
+ Hits          26120    26286     +166     
+ Misses        39796    39693     -103     
- Partials       3069     3091      +22     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@imbajin imbajin left a comment

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.

Blocking issues remain in the struct-side DECIMAL schema integration; targeted exact-head tests passed, but this review is not an approval.

case UUID:
builder.append(".asUUID()");
break;
case DECIMAL:

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.

⚠️ Blocking: yes. Summary: This makes struct schema generation emit an API that the struct builder does not expose. Evidence: struct PropertyKey.Builder has no asDecimal(), and convSingleValue has no DECIMAL conversion/valueToDecimal; after schema/default JSON values arrive as strings, validValueOrThrow("1.5") returns null/throws, unlike server PropertyKey. Requested change: add the struct conversion and builder method, plus string/numeric/default-value tests.

@bitflicker64 bitflicker64 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.

Blocking: yes. Summary: The new BigDecimal serializer breaks typed GraphSON (v2/v3) for every BigDecimal Gremlin result, and an OLAP_SECONDARY decimal key still gets a secondary index despite the new no-index rule. The struct-side conversion gap is still open. Evidence: GraphSONMessageSerializerV2d0/V3d0 configured with HugeGraphIoRegistry at a28554e fail with "Type id handling not implemented for type java.math.BigDecimal" (same serializers without the registry emit gx:BigDecimal); a RocksDB probe created propertyKey("rank").asDecimal().writeType(OLAP_SECONDARY) and index label *olap_by_rank type=SECONDARY. Latest-head CI is green.

}
}

private static class BigDecimalSerializer extends StdSerializer<BigDecimal> {

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.

‼️ BigDecimalSerializer only overrides serialize(), but this module is registered into the typed GraphSON mappers used by gremlin-server (GraphSONMessageSerializerV2d0/V3d0 in gremlin-server.yaml, and V3d0 also answers application/json). Jackson then calls serializeWithType(), which StdSerializer does not implement. I checked this at a28554e by building a ResponseMessage with new BigDecimal("1.5") and serializing it through each serializer configured with ioRegistries: [HugeGraphIoRegistry]. V1d0 returns "1.5". V2d0 and V3d0 both fail with InvalidDefinitionException: Type id handling not implemented for type java.math.BigDecimal (by serializer of type ...HugeGraphSONModule$BigDecimalSerializer). Without the registry the same serializers emit {"@type":"gx:BigDecimal","@value":1.5}. So g.V().values('balance') on a DECIMAL key fails over GraphSON v2/v3, and so does any existing script that returns a BigDecimal, such as a Groovy decimal literal (g.inject(1.5)). That used to work. Requested change: implement serializeWithType (for example via typeSer.writeTypePrefix/writeTypeSuffix, as the other typed serializers in this module do), or limit the string serializer to JsonUtil and leave the TinkerPop gx:BigDecimal handling alone. Add a test that serializes a BigDecimal through GraphSON v2 and v3 with HugeGraphIoRegistry.

E.checkArgument(pkey.aggregateType().isIndexable(),
"The aggregate type %s is not indexable",
pkey.aggregateType());
E.checkArgument(!pkey.dataType().isDecimal(),

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.

⚠️ The decimal guard is in checkFields(), which only runs on the user-facing create() path. OLAP property keys build their index through SchemaTransaction.createIndexLabelForOlapPk(), which calls IndexLabelBuilder.build() directly and skips checkFields(). PropertyKeyBuilder.checkOlap() also rejects only OLAP_RANGE for non-numeric types. On RocksDB at a28554e, schema.propertyKey("rank").asDecimal().writeType(WriteType.OLAP_SECONDARY).create() succeeds and creates index label *olap_by_rank type=SECONDARY. That contradicts the rule this PR states (no index of any type on a decimal). The secondary index key is also built from value.toString() (SplicingIdGenerator.concatValues), and for BigDecimal that output depends on scale and can use exponent notation (1E+21), so equal numbers can map to different index keys. Requested change: reject OLAP_SECONDARY (and any OLAP write type that builds an index) for DataType.DECIMAL in PropertyKeyBuilder.checkOlap(), or move the decimal check into build(), and add a core test for it.

case UUID:
builder.append(".asUUID()");
break;
case DECIMAL:

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.

⚠️ The struct copy now knows DataType.DECIMAL, but struct PropertyKey.convSingleValue() has no decimal branch (only number/date/uuid/blob), and struct DataType has no valueToDecimal(). For a DECIMAL key, a String or Long value falls through to checkDataType() and returns null. One concrete case is defaultValue(): userdata is reloaded from JSON, so a decimal default arrives as a string, and validValueOrThrow(raw) then throws. The server-side PropertyKey in this PR converts these values correctly. Requested change: port valueToDecimal() into the struct DataType and add the decimal branch to struct convSingleValue(), with tests for string, integral and default-value input.

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.

[Feature] Add a DECIMAL (BigDecimal) property data type for exact amounts

3 participants