Repository: kuzudb/kuzu
Pull request #5311: Fix date conversion overflow issue for Node.js and WASM
Author: login: mewim
Created: 2025-05-01T04:18:47Z | Merged: 2025-05-01T05:55:24Z



Diff:
diff --git a/tools/nodejs_api/src_cpp/node_util.cpp b/tools/nodejs_api/src_cpp/node_util.cpp
index 00130029ac6..f994ce4346b 100644
--- a/tools/nodejs_api/src_cpp/node_util.cpp
+++ b/tools/nodejs_api/src_cpp/node_util.cpp
@@ -67,10 +67,9 @@ Napi::Value Util::ConvertToNapiObject(const Value& value, Napi::Env env) {
     }
     case LogicalTypeID::DATE: {
         auto dateVal = value.getValue<date_t>();
-        // Javascript Date type contains both date and time information. This returns the Date at
-        // 00:00:00 in UTC timezone.
-        auto milliseconds = Date::getEpochNanoSeconds(dateVal) / Interval::NANOS_PER_MICRO /
-                            Interval::MICROS_PER_MSEC;
+        auto days = dateVal.days;
+        auto millisecondsPerDay = Interval::MICROS_PER_DAY / Interval::MICROS_PER_MSEC;
+        int64_t milliseconds = days * millisecondsPerDay;
         return Napi::Date::New(env, milliseconds);
     }
     case LogicalTypeID::TIMESTAMP_TZ: {
diff --git a/tools/nodejs_api/test/test_data_type.js b/tools/nodejs_api/test/test_data_type.js
index c6ee70ed7d0..68aa682516f 100644
--- a/tools/nodejs_api/test/test_data_type.js
+++ b/tools/nodejs_api/test/test_data_type.js
@@ -230,6 +230,32 @@ describe("DATE", function () {
       Number(new Date("1900-01-01"))
     );
   });
+
+  it("should convert DATE type for very old date", async function () {
+    const queryResult = await conn.query(
+      "RETURN MAKE_DATE(1000,11,22);"
+    );
+    const result = await queryResult.getAll();
+    assert.equal(result.length, 1);
+    assert.equal(Object.keys(result[0]).length, 1);
+
+    const value = Object.values(result[0])[0];
+    const isoString = value.toISOString();
+    assert.equal(isoString, "1000-11-22T00:00:00.000Z");
+  });
+
+  it("should convert DATE type for very future date", async function () {
+    const queryResult = await conn.query(
+      "RETURN MAKE_DATE(3000,11,22);"
+    );
+    const result = await queryResult.getAll();
+    assert.equal(result.length, 1);
+    assert.equal(Object.keys(result[0]).length, 1);
+
+    const value = Object.values(result[0])[0];
+    const isoString = value.toISOString();
+    assert.equal(isoString, "3000-11-22T00:00:00.000Z");
+  });
 });
 
 describe("TIMESTAMP", function () {
diff --git a/tools/wasm/build.mjs b/tools/wasm/build.mjs
index b4bab5afff0..631f3c2b5dc 100644
--- a/tools/wasm/build.mjs
+++ b/tools/wasm/build.mjs
@@ -11,7 +11,7 @@ const ES_BUILD_CONFIG = {
   entryPoints: ['./build/sync/index.js', './build/index.js', 'build/kuzu_wasm_worker.js'],
   bundle: true,
   format: 'esm',
-  external: ['fs', 'path', 'ws', 'crypto', "worker_threads", "os"],
+  external: ['fs', 'path', 'ws', 'crypto', "worker_threads", "os", "util"],
   outdir: 'package',
   logLevel: 'info',
   define: {
diff --git a/tools/wasm/src_cpp/main.cpp b/tools/wasm/src_cpp/main.cpp
index 56c93c255d9..14b0aca2579 100644
--- a/tools/wasm/src_cpp/main.cpp
+++ b/tools/wasm/src_cpp/main.cpp
@@ -229,8 +229,9 @@ val valueGetAsEmscriptenValue(const Value& value) {
     }
     case LogicalTypeID::DATE: {
         auto dateVal = value.getValue<date_t>();
-        auto milliseconds = Date::getEpochNanoSeconds(dateVal) / Interval::NANOS_PER_MICRO /
-                            Interval::MICROS_PER_MSEC;
+        auto days = dateVal.days;
+        auto millisecondsPerDay = Interval::MICROS_PER_DAY / Interval::MICROS_PER_MSEC;
+        int64_t milliseconds = days * millisecondsPerDay;
         return JsDate.new_(JsNumber.new_(milliseconds));
     }
     case LogicalTypeID::TIMESTAMP_TZ: {
diff --git a/tools/wasm/test/test_data_type.js b/tools/wasm/test/test_data_type.js
index d4a24356df3..ef74f46f6a9 100644
--- a/tools/wasm/test/test_data_type.js
+++ b/tools/wasm/test/test_data_type.js
@@ -237,6 +237,34 @@ describe("DATE", function () {
     );
     await queryResult.close();
   });
+
+  it("should convert DATE type for very old date", async function () {
+    const queryResult = await conn.query(
+      "RETURN MAKE_DATE(1000,11,22);"
+    );
+    const result = await queryResult.getAllObjects();
+    assert.equal(result.length, 1);
+    assert.equal(Object.keys(result[0]).length, 1);
+    const value = Object.values(result[0])[0];
+    assert.equal(typeof value, "object");
+    const isoString = value.toISOString();
+    assert.equal(isoString, "1000-11-22T00:00:00.000Z");
+    await queryResult.close();
+  });
+
+  it("should convert DATE type for very future date", async function () {
+    const queryResult = await conn.query(
+      "RETURN MAKE_DATE(9999,11,22);"
+    );
+    const result = await queryResult.getAllObjects();
+    assert.equal(result.length, 1);
+    assert.equal(Object.keys(result[0]).length, 1);
+    const value = Object.values(result[0])[0];
+    assert.equal(typeof value, "object");
+    const isoString = value.toISOString();
+    assert.equal(isoString, "9999-11-22T00:00:00.000Z");
+    await queryResult.close();
+  });
 });
 
 describe("TIMESTAMP", function () {


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR addresses a date conversion overflow issue for Node.js and WASM by updating the conversion logic and augmenting test coverage for extreme date values.  
- Updated conversion logic in C++ source files to avoid overflow by using the days field instead of nanosecond conversions.  
- Added new test cases in both WASM and Node.js API tests to validate very old and future dates.  
- Updated the build configuration to include the "util" dependency.

### Reviewed Changes

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

<details>
<summary>Show a summary per file</summary>

| File                               | Description                                                                                      |
| ---------------------------------- | ------------------------------------------------------------------------------------------------ |
| tools/wasm/test/test_data_type.js  | Added test cases for converting very old and very future dates for the WASM environment.          |
| tools/wasm/src_cpp/main.cpp        | Modified date conversion logic to use the days field for computing milliseconds.                |
| tools/wasm/build.mjs               | Updated build configuration to include "util" as an external dependency.                        |
| tools/nodejs_api/test/test_data_type.js | Added test cases for Node.js API with updated date conversion, including a new future date test. |
| tools/nodejs_api/src_cpp/node_util.cpp | Modified date conversion logic in the Node.js API to use the days field for computing milliseconds. |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**tools/wasm/src_cpp/main.cpp:233**
* Please confirm that using 'days' multiplied by MICROS_PER_DAY ensures correct conversion to milliseconds without precision loss, given the intended date range.
```
auto milliseconds = days * Interval::MICROS_PER_DAY / Interval::MICROS_PER_MSEC;
```
**tools/nodejs_api/src_cpp/node_util.cpp:71**
* Ensure that the arithmetic operation correctly converts days to milliseconds as intended, without integer division issues.
```
auto milliseconds = days * Interval::MICROS_PER_DAY / Interval::MICROS_PER_MSEC;
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T04:19:37Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR fixes a date conversion overflow issue for both Node.js and WASM components by updating how milliseconds are computed from date values. The changes include adjusted conversion logic based on day values for WASM and Node.js, added tests for very old and future dates, and an update to external dependencies in the build configuration.

### Reviewed Changes

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

<details>
<summary>Show a summary per file</summary>

| File                                             | Description                                                           |
| ------------------------------------------------ | --------------------------------------------------------------------- |
| tools/wasm/test/test_data_type.js                | Added test cases for very old and very future DATE conversions.       |
| tools/wasm/src_cpp/main.cpp                       | Changed date conversion logic to use day values for calculating milliseconds. |
| tools/wasm/build.mjs                              | Updated external dependencies by including "util".                    |
| tools/nodejs_api/test/test_data_type.js           | Added test cases for date conversion in the Node.js API with differing future date values. |
| tools/nodejs_api/src_cpp/node_util.cpp            | Updated date conversion logic similar to the WASM changes.             |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T04:26:20Z
user: login: andyfengHKU
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T05:52:32Z

Review comments:
diff_hunk: @@ -237,6 +237,34 @@ describe("DATE", function () {
     );
     await queryResult.close();
   });
+
+  it("should convert DATE type for very old date", async function () {
+    const queryResult = await conn.query(
+      "RETURN MAKE_DATE(1000,11,22);"
+    );
+    const result = await queryResult.getAllObjects();
+    assert.equal(result.length, 1);
+    assert.equal(Object.keys(result[0]).length, 1);
+    const value = Object.values(result[0])[0];
+    assert.equal(typeof value, "object");
+    const isoString = value.toISOString();
+    assert.equal(isoString, "1000-11-22T00:00:00.000Z");
+    await queryResult.close();
+  });
+
+  it("should convert DATE type for very future date", async function () {
+    const queryResult = await conn.query(
+      "RETURN MAKE_DATE(9999,11,22);"
+    );
+    const result = await queryResult.getAllObjects();
+    assert.equal(result.length, 1);
+    assert.equal(Object.keys(result[0]).length, 1);
+    const value = Object.values(result[0])[0];
+    assert.equal(typeof value, "object");
+    const isoString = value.toISOString();
+    assert.equal(isoString, "9999-11-22T00:00:00.000Z");
path: tools/wasm/test/test_data_type.js
user: login: Copilot
body: Future date expected value differs from Node.js API tests (3000-11-22) – please document the rationale behind these differing test boundaries.
```suggestion
    assert.equal(isoString, "3000-11-22T00:00:00.000Z");
```
created_at: 2025-05-01T04:19:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,7 +11,7 @@ const ES_BUILD_CONFIG = {
   entryPoints: ['./build/sync/index.js', './build/index.js', 'build/kuzu_wasm_worker.js'],
   bundle: true,
   format: 'esm',
path: tools/wasm/build.mjs
user: login: Copilot
body: [nitpick] Please add a comment explaining why 'util' is added as an external dependency in the build configuration.
```suggestion
  format: 'esm',
  // 'util' is a Node.js core module. It is included as an external dependency to ensure it is not bundled,
  // as it will be provided by the runtime environment.
```
created_at: 2025-05-01T04:19:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -229,8 +229,9 @@ val valueGetAsEmscriptenValue(const Value& value) {
     }
     case LogicalTypeID::DATE: {
         auto dateVal = value.getValue<date_t>();
-        auto milliseconds = Date::getEpochNanoSeconds(dateVal) / Interval::NANOS_PER_MICRO /
-                            Interval::MICROS_PER_MSEC;
+        auto days = dateVal.days;
path: tools/wasm/src_cpp/main.cpp
user: login: Copilot
body: Consider adding a clarifying comment on how converting days to milliseconds prevents potential overflow and why this method is preferred over using epoch nanoseconds.
```suggestion
        auto days = dateVal.days;
        // Convert days to milliseconds to prevent potential overflow issues
        // and ensure compatibility with JavaScript's Date object, which uses milliseconds.
```
created_at: 2025-05-01T04:26:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -230,6 +230,32 @@ describe("DATE", function () {
       Number(new Date("1900-01-01"))
     );
   });
+
+  it("should convert DATE type for very old date", async function () {
+    const queryResult = await conn.query(
+      "RETURN MAKE_DATE(1000,11,22);"
+    );
+    const result = await queryResult.getAll();
+    assert.equal(result.length, 1);
+    assert.equal(Object.keys(result[0]).length, 1);
+
+    const value = Object.values(result[0])[0];
+    const isoString = value.toISOString();
+    assert.equal(isoString, "1000-11-22T00:00:00.000Z");
+  });
+
+  it("should convert DATE type for very future date", async function () {
+    const queryResult = await conn.query(
+      "RETURN MAKE_DATE(3000,11,22);"
path: tools/nodejs_api/test/test_data_type.js
user: login: Copilot
body: [nitpick] The future date test uses 3000-11-22, which is inconsistent with the WASM test case that uses 9999-11-22. Consider aligning the test cases for clarity and maintainability.
```suggestion
      "RETURN MAKE_DATE(9999,11,22);"
```
created_at: 2025-05-01T04:26:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-05-01T04:53:02Z
body: ## [Codecov](https://app.codecov.io/gh/kuzudb/kuzu/pull/5311?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kuzudb) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 86.57%. Comparing base [(`d584f39`)](https://app.codecov.io/gh/kuzudb/kuzu/commit/d584f399a1e5a8cc431f331ee171b159a67d2f4d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kuzudb) to head [(`dd50db8`)](https://app.codecov.io/gh/kuzudb/kuzu/commit/dd50db81ee9e92ed12da02b23b843e6d910364e9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kuzudb).
> Report is 1 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5311      +/-   ##
==========================================
- Coverage   86.58%   86.57%   -0.01%     
==========================================
  Files        1410     1410              
  Lines       61978    61978              
  Branches     7607     7604       -3     
==========================================
- Hits        53662    53659       -3     
- Misses       8141     8144       +3     
  Partials      175      175              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/kuzudb/kuzu/pull/5311?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kuzudb).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kuzudb).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-01T05:04:02Z
body: # Benchmark Result

Master commit hash: `d584f399a1e5a8cc431f331ee171b159a67d2f4d` 
Branch commit hash: `3c7d9a18b7cac9049f97d6b48ddb17e0b34bfba2` 

| Query Group | Query Name | Mean Time - Commit (ms) | Mean Time - Master (ms) | Diff |
| --- | --- | --- | --- | --- |
| aggregation | q24 | 727.87 | 729.57 | -1.70 (-0.23%) |
| aggregation | q28 | 6552.29 | 6555.24 | -2.95 (-0.05%) |
| filter | q14 | 125.64 | 128.11 | -2.47 (-1.93%) |
| filter | q15 | 128.99 | 128.44 | 0.55 (0.43%) |
| filter | q16 | 342.03 | 340.03 | 2.00 (0.59%) |
| filter | q17 | 447.45 | 447.80 | -0.35 (-0.08%) |
| filter | q18 | 1929.37 | 1960.27 | -30.91 (-1.58%) |
| filter | zonemap-node | 87.27 | 88.65 | -1.38 (-1.55%) |
| filter | zonemap-node-lhs-cast | 87.92 | 89.69 | -1.76 (-1.97%) |
| filter | zonemap-node-null | 87.53 | 88.70 | -1.17 (-1.31%) |
| filter | zonemap-rel | 5752.76 | 5736.49 | 16.26 (0.28%) |
| fixed_size_expr_evaluator | q07 | 685.19 | 684.00 | 1.19 (0.17%) |
| fixed_size_expr_evaluator | q08 | 979.00 | 966.36 | 12.64 (1.31%) |
| fixed_size_expr_evaluator | q09 | 967.37 | 967.10 | 0.27 (0.03%) |
| fixed_size_expr_evaluator | q10 | 267.86 | 262.56 | 5.30 (2.02%) |
| fixed_size_expr_evaluator | q11 | 264.92 | 262.81 | 2.10 (0.80%) |
| fixed_size_expr_evaluator | q12 | 241.54 | 240.43 | 1.11 (0.46%) |
| fixed_size_expr_evaluator | q13 | 1569.07 | 1565.61 | 3.46 (0.22%) |
| fixed_size_seq_scan | q23 | 121.01 | 118.63 | 2.38 (2.00%) |
| join | q29 | 693.23 | 703.39 | -10.15 (-1.44%) |
| join | q30 | 1553.13 | 1736.11 | -182.98 (-10.54%) |
| **join** | **q31** | **7.14** | **9.89** | **-2.76 (-27.86%)** |
| join | SelectiveTwoHopJoin | 43.14 | 47.23 | -4.09 (-8.66%) |
| **ldbc_snb_ic** | **q35** | **9.25** | **7.21** | **2.04 (28.26%)** |
| ldbc_snb_ic | q36 | 98.21 | 87.36 | 10.85 (12.42%) |
| ldbc_snb_is | q32 | 5.73 | 5.85 | -0.12 (-1.99%) |
| ldbc_snb_is | q33 | 15.60 | 14.97 | 0.63 (4.19%) |
| ldbc_snb_is | q34 | 1.19 | 1.28 | -0.09 (-6.73%) |
| limit | push-down-limit-into-distinct | 2019.11 | 1957.17 | 61.95 (3.17%) |
| multi-rel | multi-rel-large-scan | 1741.80 | 1764.06 | -22.26 (-1.26%) |
| multi-rel | multi-rel-lookup | 11.89 | 11.23 | 0.66 (5.92%) |
| multi-rel | multi-rel-small-scan | 199.91 | 205.09 | -5.18 (-2.52%) |
| order_by | q25 | 131.85 | 134.58 | -2.73 (-2.03%) |
| order_by | q26 | 442.57 | 441.56 | 1.01 (0.23%) |
| order_by | q27 | 1375.75 | 1385.55 | -9.79 (-0.71%) |
| recursive_join | recursive-join-bidirection | 332.60 | 350.84 | -18.24 (-5.20%) |
| recursive_join | recursive-join-dense | 7107.54 | 7125.04 | -17.50 (-0.25%) |
| recursive_join | recursive-join-path | 23410.09 | 23554.60 | -144.51 (-0.61%) |
| **recursive_join** | **recursive-join-sparse** | **7.47** | **10.38** | **-2.91 (-28.03%)** |
| recursive_join | recursive-join-trail | 6985.40 | 7010.68 | -25.28 (-0.36%) |
| scan_after_filter | q01 | 168.00 | 167.42 | 0.58 (0.35%) |
| scan_after_filter | q02 | 154.29 | 153.56 | 0.73 (0.48%) |
| shortest_path_ldbc100 | q37 | 73.41 | 82.87 | -9.46 (-11.42%) |
| shortest_path_ldbc100 | q38 | 413.76 | 441.84 | -28.08 (-6.35%) |
| shortest_path_ldbc100 | q39 | 81.19 | 90.28 | -9.09 (-10.07%) |
| shortest_path_ldbc100 | q40 | 514.12 | 557.45 | -43.33 (-7.77%) |
| var_size_expr_evaluator | q03 | 2091.72 | 2088.89 | 2.83 (0.14%) |
| var_size_expr_evaluator | q04 | 2255.33 | 2251.71 | 3.62 (0.16%) |
| var_size_expr_evaluator | q05 | 2712.02 | 2716.04 | -4.02 (-0.15%) |
| var_size_expr_evaluator | q06 | 1335.13 | 1335.91 | -0.78 (-0.06%) |
| var_size_seq_scan | q19 | 1422.48 | 1419.91 | 2.57 (0.18%) |
| var_size_seq_scan | q20 | 2783.92 | 2776.22 | 7.70 (0.28%) |
| var_size_seq_scan | q21 | 2243.39 | 2241.43 | 1.96 (0.09%) |
| var_size_seq_scan | q22 | 125.25 | 125.37 | -0.12 (-0.10%) |

author_association: NONE