Skip to content

Unflattening array fix for very large arrays #1432

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

Merged
merged 2 commits into from
Oct 23, 2024

Conversation

matt-aitken
Copy link
Member

@matt-aitken matt-aitken commented Oct 23, 2024

If JSON like this existed:

{
  "akey.with.a.large.number.1234567890123": "foo"

When this gets unflattened it creates a massive array. An array should not be created in this situation, it should be a string key.

{
  "akey.with.a.large.number.[0]": "foo"

This is what an array item looks like.

Summary by CodeRabbit

  • New Features

    • Improved handling of nested structures and null values in attribute processing.
    • Enhanced accuracy in converting array indices for better type consistency.
    • Added a new job step for running package unit tests in the CI workflow.
  • Bug Fixes

    • Refined logic for handling prefixes in nested objects and arrays.
  • Tests

    • Introduced comprehensive test cases for various scenarios in attribute flattening and unflattening, enhancing overall test coverage.

Copy link

changeset-bot bot commented Oct 23, 2024

⚠️ No Changeset found

Latest commit: d78da87

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

coderabbitai bot commented Oct 23, 2024

Walkthrough

The changes involve updates to the flattenAttributes and unflattenAttributes functions within the packages/core/src/v3/utils/flattenAttributes.ts file. Enhancements include improved null checks for array elements in flattenAttributes, ensuring proper prefix formatting for nested objects and arrays. The unflattenAttributes function now utilizes regex for more accurate key splitting and type consistency, particularly for handling numeric indices in array-like keys. These modifications aim to strengthen the handling of nested structures and null values.

Changes

File Path Change Summary
packages/core/src/v3/utils/flattenAttributes.ts - Enhanced null checks for array elements in flattenAttributes.
- Refined prefix handling for nested objects and arrays.
- Updated key splitting logic in unflattenAttributes using regex for numeric indices.
- Improved type consistency for array-like keys.
.github/workflows/unit-tests.yml - Added new job step 🧪 Run Package Unit Tests to execute package unit tests after webapp unit tests.
packages/core/test/flattenAttributes.test.ts - Introduced new test cases for flattenAttributes and unflattenAttributes covering various scenarios, including number keys, null values, and complex object structures.

Poem

In the burrow deep, where the code does play,
Attributes flatten, come out to stay.
Nulls checked with care, and prefixes align,
A hop and a skip, the functions now shine!
With regex in hand, we split with delight,
Nested structures dance, all through the night! 🐇✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (1)
packages/core/src/v3/utils/flattenAttributes.ts (1)

98-98: Consider adding documentation about numeric key handling.

It would be helpful to add a comment explaining how the function handles different types of numeric keys (array indices vs large numeric keys) to make the behavior clear for future maintainers.

Add documentation like this:

+  // Handle key splitting with special care for:
+  // 1. Array indices (e.g., "[0]") - converted to numbers
+  // 2. Large numeric keys (e.g., "1234567890123") - preserved as strings
   const parts = key.split(".").reduce(
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 7f9091f and 653e0b5.

📒 Files selected for processing (1)
  • packages/core/src/v3/utils/flattenAttributes.ts (2 hunks)
🧰 Additional context used
🔇 Additional comments (2)
packages/core/src/v3/utils/flattenAttributes.ts (2)

Line range hint 128-133: LGTM! The array detection logic complements the key splitting changes.

The type-based check typeof nextPart === "number" ensures that only properly parsed array indices (from "[n]" syntax) trigger array creation, preventing large numeric keys from being misinterpreted as array indices.


98-117: LGTM! The new key splitting logic correctly handles large numeric keys.

The implementation properly differentiates between array indices (e.g., "[0]") and large numeric keys (e.g., "1234567890123"), addressing the core issue described in the PR objectives.

Let's verify the fix with a test case:

Copy link

pkg-pr-new bot commented Oct 23, 2024

@trigger.dev/build

pnpm add https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/build@1432

@trigger.dev/core

pnpm add https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/core@1432

trigger.dev

pnpm add https://pkg.pr.new/triggerdotdev/trigger.dev@1432

@trigger.dev/react-hooks

pnpm add https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/react-hooks@1432

@trigger.dev/sdk

pnpm add https://pkg.pr.new/triggerdotdev/trigger.dev/@trigger.dev/sdk@1432

commit: 653e0b5

@ericallam ericallam merged commit a790759 into main Oct 23, 2024
5 of 7 checks passed
@ericallam ericallam deleted the hotfix/unflatten-attributes branch October 23, 2024 19:58
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (3)
packages/core/src/v3/utils/flattenAttributes.ts (1)

122-124: Document edge case handling for falsy values.

The condition !part && part !== 0 handles edge cases, but the behavior should be documented for maintainability.

Add a comment explaining the logic:

+// Skip empty parts but preserve 0 as a valid key
+// This ensures compatibility with arrays while preventing
+// issues with empty string keys
 if (!part && part !== 0) {
   continue;
 }
packages/core/test/flattenAttributes.test.ts (2)

4-4: Fix typo in test name.

The test name has a typo: "correctl" should be "correctly".

-  it("handles number keys correctl", () => {
+  it("handles number keys correctly", () => {

Line range hint 1-216: Improve test organization and documentation.

Consider restructuring the tests to better separate concerns:

  1. Basic functionality tests (primitives, simple objects)
  2. Edge cases and boundary conditions (large numbers, memory efficiency)
  3. Integration scenarios (complex nested structures)

Also, add JSDoc comments to document test groups and their purposes, especially for the new edge cases being tested.

Example structure:

/**
 * Basic functionality tests for primitive types and simple objects
 */
describe("basic functionality", () => {
  // existing primitive tests
});

/**
 * Edge cases and boundary conditions
 * Verifies handling of large numeric keys and memory efficiency
 */
describe("edge cases", () => {
  // large number tests
  // memory efficiency tests
});

/**
 * Integration scenarios with complex nested structures
 */
describe("complex structures", () => {
  // existing nested structure tests
});
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 653e0b5 and d78da87.

📒 Files selected for processing (3)
  • .github/workflows/unit-tests.yml (1 hunks)
  • packages/core/src/v3/utils/flattenAttributes.ts (1 hunks)
  • packages/core/test/flattenAttributes.test.ts (1 hunks)
🧰 Additional context used
🪛 yamllint
.github/workflows/unit-tests.yml

[error] 41-41: trailing spaces

(trailing-spaces)

🪛 Biome
packages/core/src/v3/utils/flattenAttributes.ts

[error] 103-103: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (3)
.github/workflows/unit-tests.yml (1)

42-43: LGTM! Good addition of package-specific tests.

The new test step correctly targets the "@trigger.dev/*" packages, which is essential for testing the changes to the core package's flattening utilities. The step is properly positioned in the workflow and inherits the necessary environment variables.

packages/core/src/v3/utils/flattenAttributes.ts (1)

98-115: Verify handling of large numeric keys with test cases.

The implementation looks solid, but we should verify it handles the PR's example cases correctly.

Also applies to: 126-130

✅ Verification successful

Let me gather more information by examining the existing test file for flattenAttributes since it was found in the output.

The existing test file might provide insights into current test coverage and help us verify if the changes handle the edge cases correctly. I'll analyze the implementation and test file contents when provided.


After analyzing the implementation and test files, I can now provide a final response since I have enough information to verify the changes.

The implementation correctly handles large numeric keys and array indices

The changes in flattenAttributes.ts are verified to be correct for the following reasons:

  1. The implementation properly handles numeric keys through the regex pattern /^\[(\d+)\]$/ which can handle numbers of any length
  2. The existing test suite already covers various edge cases including:
    • Numeric keys both as strings and numbers ("25" and 25)
    • Array indices with proper bracket notation
    • Complex nested objects with mixed types
  3. The new implementation maintains backward compatibility while adding more precise handling of array indices

The PR's example case of "akey.with.a.large.number.1234567890123" will work correctly because:

  • Non-bracketed numbers are treated as regular string parts
  • Only bracketed numbers [n] are parsed as array indices
  • The implementation correctly distinguishes between array indices and regular numeric keys
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Create a test file to verify the fix
cat << 'EOF' > test.ts
import { unflattenAttributes } from "./packages/core/src/v3/utils/flattenAttributes";

const testCases = [
  {
    input: { "akey.with.a.large.number.1234567890123": "value" },
    desc: "Large numeric key"
  },
  {
    input: { "array.[0]": "value" },
    desc: "Valid array index"
  }
];

console.log(JSON.stringify(testCases.map(tc => ({
  description: tc.desc,
  result: unflattenAttributes(tc.input)
})), null, 2));
EOF

# Find and list all existing test files
echo "Existing test files:"
fd -e test.ts -e spec.ts

Length of output: 602


Script:

#!/bin/bash
# Check the existing test cases in flattenAttributes.test.ts
cat packages/core/test/flattenAttributes.test.ts

# Also let's check the actual implementation to understand the full context
cat packages/core/src/v3/utils/flattenAttributes.ts

Length of output: 11939

🧰 Tools
🪛 Biome

[error] 103-103: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

packages/core/test/flattenAttributes.test.ts (1)

1-1: 🛠️ Refactor suggestion

Add edge case tests for memory efficiency.

To prevent regression of the large array issue, consider adding tests that verify memory efficiency when handling large numeric keys. This could include:

  1. Performance assertions for large numeric keys
  2. Memory usage verification
  3. Edge cases with mixed array indices and numeric keys

Let's check if there are any existing performance tests:

Add these test cases:

describe("performance and memory efficiency", () => {
  it("handles very large numeric keys efficiently", () => {
    const start = process.memoryUsage().heapUsed;
    const result = unflattenAttributes({
      "object.999999999999999": "value",
      "array.[0]": "actual_array"
    });
    const end = process.memoryUsage().heapUsed;
    
    // Verify correct structure
    expect(result).toEqual({
      object: { "999999999999999": "value" },
      array: ["actual_array"]
    });
    
    // Verify memory usage is reasonable (should be minimal for simple objects)
    const bytesUsed = end - start;
    expect(bytesUsed).toBeLessThan(1000); // Adjust threshold as needed
  });
});

@@ -38,7 +38,9 @@ jobs:
SESSION_SECRET: "secret"
MAGIC_LINK_SECRET: "secret"
ENCRYPTION_KEY: "secret"


Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove trailing whitespace.

There are trailing spaces on this line that should be removed.

Apply this diff to fix the formatting:

-      
+
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
🧰 Tools
🪛 yamllint

[error] 41-41: trailing spaces

(trailing-spaces)

Comment on lines +98 to +115
const parts = key.split(".").reduce(
(acc, part) => {
if (part.startsWith("[") && part.endsWith("]")) {
// Handle array indices more precisely
const match = part.match(/^\[(\d+)\]$/);
if (match && match[1]) {
acc.push(parseInt(match[1]));
} else {
// Remove brackets for non-numeric array keys
acc.push(part.slice(1, -1));
}
} else {
acc.push(part);
}
return acc;
},
[] as (string | number)[]
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Consider adding numeric bounds validation for array indices.

While the regex pattern correctly identifies numeric indices, it doesn't prevent the issue with very large numbers. Consider adding a size check to prevent array allocation attacks.

Apply this diff to add bounds validation:

 const match = part.match(/^\[(\d+)\]$/);
 if (match && match[1]) {
+  const index = parseInt(match[1]);
+  if (index > Number.MAX_SAFE_INTEGER) {
+    // Treat very large numbers as string keys
+    acc.push(part);
+  } else {
-    acc.push(parseInt(match[1]));
+    acc.push(index);
+  }
 } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const parts = key.split(".").reduce(
(acc, part) => {
if (part.startsWith("[") && part.endsWith("]")) {
// Handle array indices more precisely
const match = part.match(/^\[(\d+)\]$/);
if (match && match[1]) {
acc.push(parseInt(match[1]));
} else {
// Remove brackets for non-numeric array keys
acc.push(part.slice(1, -1));
}
} else {
acc.push(part);
}
return acc;
},
[] as (string | number)[]
);
const parts = key.split(".").reduce(
(acc, part) => {
if (part.startsWith("[") && part.endsWith("]")) {
// Handle array indices more precisely
const match = part.match(/^\[(\d+)\]$/);
if (match && match[1]) {
const index = parseInt(match[1]);
if (index > Number.MAX_SAFE_INTEGER) {
// Treat very large numbers as string keys
acc.push(part);
} else {
acc.push(index);
}
} else {
// Remove brackets for non-numeric array keys
acc.push(part.slice(1, -1));
}
} else {
acc.push(part);
}
return acc;
},
[] as (string | number)[]
);
🧰 Tools
🪛 Biome

[error] 103-103: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

Comment on lines +126 to +130
if (typeof nextPart === "number") {
// Ensure we create an array for numeric indices
current[part] = Array.isArray(current[part]) ? current[part] : [];
} else if (current[part] === undefined) {
// Create an object for non-numeric paths
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Strengthen type safety in array/object creation.

The current implementation could benefit from more explicit type checking and array initialization.

Consider these improvements:

-if (typeof nextPart === "number") {
+if (typeof nextPart === "number" && Number.isInteger(nextPart) && nextPart >= 0) {
   // Ensure we create an array for numeric indices
-  current[part] = Array.isArray(current[part]) ? current[part] : [];
+  current[part] = Array.isArray(current[part]) ? current[part] : new Array();
 } else if (current[part] === undefined) {
   // Create an object for non-numeric paths
-  current[part] = {};
+  current[part] = Object.create(null);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof nextPart === "number") {
// Ensure we create an array for numeric indices
current[part] = Array.isArray(current[part]) ? current[part] : [];
} else if (current[part] === undefined) {
// Create an object for non-numeric paths
if (typeof nextPart === "number" && Number.isInteger(nextPart) && nextPart >= 0) {
// Ensure we create an array for numeric indices
current[part] = Array.isArray(current[part]) ? current[part] : new Array();
} else if (current[part] === undefined) {
// Create an object for non-numeric paths
current[part] = Object.create(null);

Comment on lines +4 to +16
it("handles number keys correctl", () => {
expect(flattenAttributes({ bar: { "25": "foo" } })).toEqual({ "bar.25": "foo" });
expect(unflattenAttributes({ "bar.25": "foo" })).toEqual({ bar: { "25": "foo" } });
expect(flattenAttributes({ bar: ["foo", "baz"] })).toEqual({
"bar.[0]": "foo",
"bar.[1]": "baz",
});
expect(unflattenAttributes({ "bar.[0]": "foo", "bar.[1]": "baz" })).toEqual({
bar: ["foo", "baz"],
});
expect(flattenAttributes({ bar: { 25: "foo" } })).toEqual({ "bar.25": "foo" });
expect(unflattenAttributes({ "bar.25": "foo" })).toEqual({ bar: { 25: "foo" } });
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add test cases for large numeric keys.

While the test covers basic number keys, it doesn't verify the specific issue mentioned in the PR objectives regarding very large numeric keys. Consider adding test cases that validate the fix for keys like "akey.with.a.large.number.1234567890123".

Add these test cases:

expect(flattenAttributes({ "akey.with.a.large.number.1234567890123": "value" }))
  .toEqual({ "akey.with.a.large.number.1234567890123": "value" });

expect(unflattenAttributes({ "akey.with.a.large.number.1234567890123": "value" }))
  .toEqual({ "akey.with.a.large.number.1234567890123": "value" });

// Verify that array indices still work correctly
expect(flattenAttributes({ array: ["value"] }))
  .toEqual({ "array.[0]": "value" });

ericallam pushed a commit that referenced this pull request Oct 23, 2024
* fix unflattening of array indices

* Get tests passing

---------

Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
Co-authored-by: Eric Allam <eallam@icloud.com>
# Conflicts:
#	.github/workflows/unit-tests.yml
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.

3 participants