Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 130 additions & 6 deletions src/Network/Struct.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,14 @@ public function offsetExists(mixed $name): bool {
*/
public function offsetUnset(mixed $name): void {
unset($this->data[$name]);
$this->keys = array_keys($this->data);
$this->keys = array_values(
array_filter(
$this->keys,
static function (mixed $key) use ($name): bool {
return $key !== $name;
}
)
);
}

/**
Expand Down Expand Up @@ -139,7 +146,13 @@ public static function fromJson(string $json): self {
/** @var array<TKey, TValue> */
$result = (array)simdjson_decode($json, true, static::JSON_DEPTH);
$bigIntFields = [];
if (static::hasBigInt($json)) {

// PRIMARY: Extract bigint fields from column metadata if present
$hasColumns = isset($result['columns']) && is_array($result['columns']);
if ($hasColumns) {
static::addBigIntFieldsFromColumns($result, $bigIntFields);
} elseif (static::hasBigInt($json)) {
// FALLBACK: Only run heuristic if columns metadata is missing
// We need here to keep original json decode cuzit has bigIntFields
/** @var array<TKey, TValue> */
$modified = json_decode($json, true, static::JSON_DEPTH, static::JSON_FLAGS | JSON_BIGINT_AS_STRING);
Expand Down Expand Up @@ -213,7 +226,7 @@ static function (array $matches): string {
},
$serialized
);
if (!isset($json)) {
if (!is_string($json)) {
throw new Exception('Cannot encode data to JSON');
}
return $json;
Expand Down Expand Up @@ -241,6 +254,8 @@ private static function getReplacePattern(string $path): string {

/**
* Traverse the data and track all fields that are big integers
* Skips fields that were already identified via column metadata
*
* @param mixed $data
* @param mixed $originalData
* @param array<string> $bigIntFields
Expand All @@ -257,19 +272,100 @@ private static function traverseAndTrack(
return;
}

/** @var array<string, int> */
$bigIntFieldsLookup = array_flip($bigIntFields);
foreach ($data as $key => &$value) {
$currentPath = $path ? "$path.$key" : "$key";
if (!isset($originalData[$key])) {
continue;
}

$originalValue = $originalData[$key];
if (is_string($value) && is_numeric($originalValue) && strlen($value) > 9) {
$bigIntFields[] = $currentPath;
} elseif (is_array($value) && is_array($originalValue)) {
if (is_array($value) && is_array($originalValue)) {
static::traverseAndTrack($value, $originalValue, $bigIntFields, $currentPath);
$bigIntFieldsLookup = array_flip($bigIntFields);
continue;
}

static::trackBigIntIfNeeded($value, $originalValue, $currentPath, $bigIntFields, $bigIntFieldsLookup);
}
}

/**
* Check if a numeric string exceeds PHP_INT_MAX/MIN boundaries
* Uses optimized string length with conditional trimming for best performance
* Handles leading zeros and sign correctly
*
* @param string $value Numeric string to check
* @return bool True if value exceeds 64-bit integer range
*/
private static function isBigIntBoundary(string $value): bool {
$firstChar = $value[0];

// OPTIMIZATION: Only ltrim if value has leading sign or zeros
// This avoids expensive function call for ~95% of clean numeric values
$absValue = ($firstChar === '-' || $firstChar === '0')
? ltrim($value, '-0')
: $value;

$magnitude = strlen($absValue);

// PHP_INT_MAX has 19 digits, so we can quickly filter most values
// > 19 digits: definitely a bigint
if ($magnitude > 19) {
return true;
}

// <= 18 digits: definitely not a bigint
if ($magnitude < 19) {
return false;
}

// Exactly 19 digits: need string comparison against PHP_INT_MAX/MIN boundaries
// Note: We must use different boundaries for positive vs negative
// because PHP_INT_MAX and |PHP_INT_MIN| differ by 1
if ($firstChar === '-') {
// Negative: 19-digit negative is bigint if absolute value > |PHP_INT_MIN|
// |PHP_INT_MIN| = 9223372036854775808
return $absValue > '9223372036854775808';
}

// Positive: 19-digit positive is bigint if > PHP_INT_MAX
return $absValue > (string)PHP_INT_MAX;
}

/**
* Check if field is a big integer and add it to tracking list
*
* @param mixed $value
* @param mixed $originalValue
* @param string $currentPath
* @param array<string> &$bigIntFields
* @param array<string, int> $bigIntFieldsLookup Fast O(1) lookup set
* @return void
*/
private static function trackBigIntIfNeeded(
mixed $value,
mixed $originalValue,
string $currentPath,
array &$bigIntFields,
array $bigIntFieldsLookup = []
): void {
if (!is_string($value) || !is_numeric($originalValue)) {
return;
}

// Use precise boundary check instead of arbitrary strlen
if (!static::isBigIntBoundary($value)) {
return;
}

// Skip if already identified via column metadata
if (isset($bigIntFieldsLookup[$currentPath])) {
return;
}

$bigIntFields[] = $currentPath;
}

/**
Expand All @@ -281,6 +377,34 @@ private static function hasBigInt(string $json): bool {
return !!preg_match('/(?<!")[1-9]\d{18,}(?!")/', $json);
}

/**
* Extract bigint field paths from column metadata
* Only fields explicitly marked as 'long long' in column definitions are extracted
*
* @param array<mixed> $response Response with 'columns' metadata
* @param array<string> &$bigIntFields Fields array to populate with bigint paths
* @return void
*/
private static function addBigIntFieldsFromColumns(array $response, array &$bigIntFields): void {
if (!isset($response['columns']) || !is_array($response['columns'])) {
return;
}

foreach ($response['columns'] as $columnIndex => $columnDef) {
if (!is_array($columnDef)) {
continue;
}

foreach ($columnDef as $fieldName => $fieldInfo) {
if (!is_array($fieldInfo) || !(($fieldInfo['type'] ?? null) === 'long long')) {
continue;
}

$bigIntFields[] = "data.{$columnIndex}.{$fieldName}";
}
}
}

/**
* Count elements of an object
* @link https://php.net/manual/en/countable.count.php
Expand Down
Loading