-
-
Notifications
You must be signed in to change notification settings - Fork 95
Index keys: normalize to double only where the conversion is exact #1282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| /* | ||
| * Copyright (c) 2017-2021 Nitrite author or authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| * | ||
| */ | ||
|
|
||
| package org.dizitart.no2.common; | ||
|
|
||
| import org.junit.Test; | ||
|
|
||
| import java.math.BigInteger; | ||
|
|
||
| import static org.junit.Assert.assertEquals; | ||
| import static org.junit.Assert.assertNotEquals; | ||
|
|
||
| public class DBValueTest { | ||
|
|
||
| @Test | ||
| public void testSmallNumbersAreNormalizedToDouble() { | ||
| // cross-type equality for values a double holds exactly, including in stores that | ||
| // compare the encoded key rather than going through compareTo | ||
| assertEquals(new DBValue(5.0), new DBValue(5)); | ||
| assertEquals(new DBValue(5.0), new DBValue(5L)); | ||
| assertEquals(new DBValue(5.0), new DBValue((short) 5)); | ||
| assertEquals(new DBValue(5.0), new DBValue((byte) 5)); | ||
| assertEquals(new DBValue(5.0), new DBValue(BigInteger.valueOf(5))); | ||
| } | ||
|
|
||
| @Test | ||
| public void testLargeLongsKeepTheirValue() { | ||
| long id = 870000000000000123L; // beyond 2^53, doubles are 128 apart here | ||
| assertEquals(id, new DBValue(id).getValue()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testLongsCloserThanDoublePrecisionStayDistinct() { | ||
| long id = 870000000000000123L; | ||
| assertNotEquals(new DBValue(id), new DBValue(id + 1)); | ||
| assertNotEquals(0, new DBValue(id).compareTo(new DBValue(id + 1))); | ||
| } | ||
|
|
||
| @Test | ||
| public void testLargeBigIntegerKeepsItsValue() { | ||
| // odd and far beyond 2^53, so no double holds it exactly | ||
| BigInteger value = BigInteger.ONE.shiftLeft(70).add(BigInteger.ONE); | ||
| assertEquals(value, new DBValue(value).getValue()); | ||
| assertNotEquals(new DBValue(value), new DBValue(value.add(BigInteger.valueOf(2)))); | ||
| } | ||
|
|
||
| @Test | ||
| public void testLongAtTheEdgeOfExactRange() { | ||
| long exact = 1L << 53; // the largest power of two a double still steps by one | ||
| assertEquals(2.0 * (1L << 52), new DBValue(exact).getValue()); | ||
| // one above it is not representable, so it has to keep its own value | ||
| assertEquals(exact + 1, new DBValue(exact + 1).getValue()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testExactlyRepresentableLargeValuesStillNormalize() { | ||
| // 2^63 is a power of two, so the conversion loses nothing and folding is safe | ||
| BigInteger powerOfTwo = BigInteger.ONE.shiftLeft(63); | ||
| assertEquals(Math.pow(2, 63), new DBValue(powerOfTwo).getValue()); | ||
| } | ||
|
Comment on lines
+70
to
+74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add Lines 100-102 in As per coding guidelines, " 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| @Test | ||
| public void testNumbersStillCompareAcrossTypes() { | ||
| // compareTo goes through Comparables/Numbers, so this holds whatever the stored form is | ||
| long id = 870000000000000123L; | ||
| assertEquals(0, new DBValue(id).compareTo(new DBValue(BigInteger.valueOf(id)))); | ||
| assertEquals(0, new DBValue(5).compareTo(new DBValue(5.0))); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| /* | ||
| * Copyright (c) 2017-2021 Nitrite author or authors. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| * | ||
| */ | ||
|
|
||
| package org.dizitart.no2.integration.collection; | ||
|
|
||
| import org.dizitart.no2.collection.Document; | ||
| import org.dizitart.no2.index.IndexType; | ||
| import org.junit.Test; | ||
|
|
||
| import static org.dizitart.no2.collection.Document.createDocument; | ||
| import static org.dizitart.no2.filters.FluentFilter.where; | ||
| import static org.dizitart.no2.index.IndexOptions.indexOptions; | ||
| import static org.junit.Assert.assertEquals; | ||
|
|
||
| /** | ||
| * Ids above 2^53 - snowflake ids, TSIDs and the like - are further apart than a double can | ||
| * step, so an index that keyed them as doubles could not tell them apart. | ||
| */ | ||
| public class CollectionLargeIdIndexTest extends BaseCollectionTest { | ||
|
|
||
| // two ids 1 apart; the nearest doubles around here are 128 apart | ||
| private static final long FIRST_ID = 870000000000000123L; | ||
| private static final long SECOND_ID = FIRST_ID + 1; | ||
|
|
||
| @Test | ||
| public void testUniqueIndexAcceptsIdsCloserThanDoublePrecision() { | ||
| collection.remove(org.dizitart.no2.filters.Filter.ALL); | ||
| collection.createIndex(indexOptions(IndexType.UNIQUE), "entityId"); | ||
|
|
||
| collection.insert(createDocument("entityId", FIRST_ID)); | ||
| collection.insert(createDocument("entityId", SECOND_ID)); | ||
|
|
||
| assertEquals(2, collection.find().size()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testIndexedLookupReturnsOnlyTheMatchingId() { | ||
| collection.remove(org.dizitart.no2.filters.Filter.ALL); | ||
| collection.createIndex(indexOptions(IndexType.NON_UNIQUE), "entityId"); | ||
|
|
||
| collection.insert(createDocument("entityId", FIRST_ID)); | ||
| collection.insert(createDocument("entityId", SECOND_ID)); | ||
|
|
||
| Document found = collection.find(where("entityId").eq(FIRST_ID)).firstOrNull(); | ||
| assertEquals(1, collection.find(where("entityId").eq(FIRST_ID)).size()); | ||
| assertEquals(FIRST_ID, (long) found.get("entityId", Long.class)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject non-finite
Floatvalues before the type check.Float.NaNandFloat.POSITIVE_INFINITYenter theFloatbranch at Lines 81-84. The method returnstruebefore it reaches the non-finite check at Lines 87-89.normalizeNumberthen changes these values toDouble.Move the non-finite check before the
Floatbranch so the helper rejects all non-finite conversions.Proposed fix
private static boolean isExactAsDouble(Number value, double normalized) { + if (Double.isNaN(normalized) || Double.isInfinite(normalized)) { + return false; + } + if (value instanceof Integer || value instanceof Short || value instanceof Byte || value instanceof Float) { // every value of these types survives the widening unchanged return true; } - - if (Double.isNaN(normalized) || Double.isInfinite(normalized)) { - return false; - }📝 Committable suggestion
🤖 Prompt for AI Agents