@@ -591,7 +591,7 @@ public function sort(int $flags = SORT_REGULAR): static
591591
592592 /**
593593 * Returns a new SmartArray sorted ascending by the specified field.
594- * Only works on nested arrays ( throws on flat) .
594+ * Works on arrays of rows only: throws if the array is flat or any element is not a row .
595595 *
596596 * Rows missing the field sort first: the missing value counts as null for
597597 * ordering only (like MySQL ORDER BY), and rows are returned unchanged.
@@ -616,7 +616,7 @@ public function sortBy(string $field, int $flags = SORT_REGULAR): static
616616 }
617617 $ this ->warnIfMissing ($ field );
618618
619- // sort by field value, treating missing fields as null (?? also covers non-array rows in mixed data)
619+ // sort by field value, treating missing fields as null
620620 $ sorted = $ this ->toArray ();
621621 $ fieldValues = array_map (fn ($ row ) => $ row [$ field ] ?? null , $ sorted );
622622 array_multisort ($ fieldValues , SORT_ASC , $ flags , $ sorted );
@@ -665,7 +665,7 @@ public function filter(?callable $callback = null): static
665665
666666 /**
667667 * Returns a new SmartArray containing only elements where a field matches a value.
668- * Only works on nested arrays ( throws on flat) .
668+ * Works on arrays of rows only: throws if the array is flat or any element is not a row .
669669 *
670670 * How values match:
671671 * - Numbers match numeric strings: where('id', 5) matches '5', where('price', 1) matches '1.00'
@@ -698,7 +698,7 @@ public function where(array|string $field, mixed $value = null): static
698698 // change one copy, change all four.
699699 $ matches = [];
700700 foreach ($ this ->toArray () as $ key => $ row ) {
701- if (is_array ( $ row ) && !empty ($ row [$ field ])) {
701+ if (!empty ($ row [$ field ])) {
702702 $ matches [$ key ] = $ row ;
703703 }
704704 }
@@ -713,7 +713,7 @@ public function where(array|string $field, mixed $value = null): static
713713 // repeated 4x, see the first where() loop for why
714714 $ matches = [];
715715 foreach ($ this ->toArray () as $ key => $ row ) {
716- if (is_array ( $ row ) && array_key_exists ($ field , $ row ) && self ::valueMatches ($ row [$ field ], $ value )) {
716+ if (array_key_exists ($ field , $ row ) && self ::valueMatches ($ row [$ field ], $ value )) {
717717 $ matches [$ key ] = $ row ;
718718 }
719719 }
@@ -767,7 +767,7 @@ public function whereNot(string $field, mixed $value = null): static
767767 // repeated 4x, see the first where() loop for why
768768 $ matches = [];
769769 foreach ($ this ->toArray () as $ key => $ row ) {
770- if (is_array ( $ row ) && empty ($ row [$ field ])) {
770+ if (empty ($ row [$ field ])) {
771771 $ matches [$ key ] = $ row ;
772772 }
773773 }
@@ -779,7 +779,7 @@ public function whereNot(string $field, mixed $value = null): static
779779 // repeated 4x, see the first where() loop for why
780780 $ matches = [];
781781 foreach ($ this ->toArray () as $ key => $ row ) {
782- if (is_array ( $ row ) && ( !array_key_exists ($ field , $ row ) || !self ::valueMatches ($ row [$ field ], $ value) )) {
782+ if (!array_key_exists ($ field , $ row ) || !self ::valueMatches ($ row [$ field ], $ value )) {
783783 $ matches [$ key ] = $ row ;
784784 }
785785 }
@@ -928,9 +928,6 @@ public function indexBy(string $field): static
928928 // Index by field; rows with a null or missing value index under '' (duplicates: last wins)
929929 $ values = [];
930930 foreach ($ this ->toArray () as $ row ) {
931- if (!is_array ($ row )) {
932- continue ; // scalar rows have no fields to index by
933- }
934931 $ key = $ row [$ field ] ?? '' ;
935932 $ key = is_bool ($ key ) ? (int )$ key : (string )$ key ; // string cast keeps float precision; ints re-key as ints, bools as 1/0
936933 $ values [$ key ] = $ row ;
@@ -981,9 +978,6 @@ public function groupBy(string $field): static
981978
982979 $ values = [];
983980 foreach ($ this ->toArray () as $ row ) {
984- if (!is_array ($ row )) {
985- continue ; // scalar rows have no fields to group by
986- }
987981 $ key = $ row [$ field ] ?? '' ;
988982 $ key = is_bool ($ key ) ? (int )$ key : (string )$ key ; // string cast keeps float precision; ints re-key as ints, bools as 1/0
989983 $ values [$ key ][] = $ row ;
@@ -1016,9 +1010,6 @@ public function columnAt(int $index): static
10161010
10171011 $ values = [];
10181012 foreach ($ this ->toArray () as $ row ) {
1019- if (!is_array ($ row )) {
1020- continue ; // scalar rows have no columns to extract
1021- }
10221013 $ count = count ($ row );
10231014 $ rowIndex = ($ index < 0 ) ? $ count + $ index : $ index ; // Convert negative indexes to positive
10241015
@@ -1572,17 +1563,35 @@ private function assertFlatArray(): void
15721563 }
15731564
15741565 /**
1575- * Assert that array has at least one nested array in values.
1566+ * Assert that every element is a row (nested array). Empty arrays pass, so
1567+ * empty result sets flow through row-only methods without error.
15761568 *
1577- * @throws InvalidArgumentException If the array is flat.
1569+ * Row-only methods can rely on every element being a child SmartArray, so
1570+ * their loops don't need per-row is_array() checks.
1571+ *
1572+ * @throws InvalidArgumentException If the array is flat or contains non-row elements.
15781573 */
15791574 private function assertNestedArray (): void
15801575 {
1581- if (!empty ($ this ->data ) && $ this ->isFlat ()) {
1582- $ function = debug_backtrace (DEBUG_BACKTRACE_IGNORE_ARGS , 2 )[1 ]['function ' ];
1583- $ error = "$ function(): Expected a nested array, but got a flat array " ;
1584- throw new InvalidArgumentException ($ error );
1576+ // Construction and writes maintain rowsOnly, so result sets pass in O(1)
1577+ if ($ this ->rowsOnly ) {
1578+ return ;
1579+ }
1580+
1581+ // rowsOnly false means a scalar was stored at some point, but an unset may
1582+ // have removed it since, so scan to see what's really here
1583+ foreach ($ this ->data as $ key => $ value ) {
1584+ if (!$ value instanceof self) {
1585+ $ function = debug_backtrace (DEBUG_BACKTRACE_IGNORE_ARGS , 2 )[1 ]['function ' ];
1586+ $ error = $ this ->isNested ()
1587+ ? "$ function(): Expected a nested array of rows, but element ' $ key' is not a row ( " . get_debug_type ($ value ) . ") "
1588+ : "$ function(): Expected a nested array, but got a flat array " ;
1589+ throw new InvalidArgumentException ($ error );
1590+ }
15851591 }
1592+
1593+ // All rows after all: the flag went stale-false after an unset, set it right
1594+ $ this ->rowsOnly = true ;
15861595 }
15871596
15881597 /**
@@ -1591,8 +1600,6 @@ private function assertNestedArray(): void
15911600 * names, so a miss there is almost always a typo. Everywhere else (lookup maps
15921601 * from indexBy()/column(), standalone arrays) keys are data, a miss is a normal
15931602 * no-match, and the access renders blank silently.
1594- * Skipped for method-argument checks on mixed data (scalar config + array fields)
1595- * since there's no first row to check against.
15961603 *
15971604 * @param string|int $key The key to check for
15981605 * @param bool $isOffset True for key access ($array->key), false for method args (where, sortBy, etc.)
@@ -1611,7 +1618,7 @@ private function warnIfMissing(string|int $key, bool $isOffset = false): void
16111618 if (!$ isOffset ) {
16121619 $ first = $ this ->first ();
16131620 if (!($ first instanceof self)) {
1614- return ; // Non-uniform data (e.g., schemas with scalar config + array fields)
1621+ return ; // empty array: first() returns SmartNull, no row to sample
16151622 }
16161623 $ target = $ first ;
16171624 }
0 commit comments