feat: deterministic record lookups, configurable search minimum, and an option-count search threshold [3.x] - #205
Merged
Conversation
ManukMinasyan
force-pushed
the
feat/select-picker-behavior
branch
from
August 20, 2026 18:59
b496c92 to
fc822b5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three changes to the picker paths, all reachable from a new
selectsconfig block so 3.7 behavior can be restored exactly.1. Deterministic record lookups
RecordSelectInputComponent::getInitialOptions()andsearchRecords()ranlimit(50)with noorderBy, so the initial page was whatever the database happened to return and could differ between two renders of the same form.Both now order by the model key descending, which is deterministic and index-backed. A configured column is ordered by first with the key appended as a tiebreaker, since rows sharing a timestamp are the normal case in bulk-imported data.
Column existence is resolved without a schema query, since a runtime
Schema::hasColumn()call would be a per-request round trip. A configured column is trusted, with one documented exception:'updated_at'on a model that returns false fromusesTimestamps()falls back to the key.Driver-neutral: plain
orderBy, no raw SQL.2. Minimum search length, configurable end to end
getSearchResultsForJs()returned[]below 2 characters. It now returns the unfiltered first page, and the minimum comes from config rather than a literal.On scope: the packaged Blade already filters the loaded first page client-side below the minimum, so this is not a visible "no results" fix in the shipped UI. The view had to change because the minimum was hardcoded as
2in four places in the Alpine component. With a server-side-only knob, settingmin_search_lengthto 3 would make the client request a filtered search at 2 characters and the server answer it with an unfiltered page of 50 records. The view now readsgetMinSearchLength(), so client and server always agree.3. Search box only above an option-count threshold (behavior change)
SelectComponentandMultiSelectComponentcalled->searchable()unconditionally, so a three-option Status field rendered a search box above three items. They now gate on option count, default 10.This is a taste change, not a bug fix. It carries no performance benefit:
getSearchResultsUsingis only assigned insiderelationship(), sohasDynamicSearchResults()is false for static options and filtering already happens client-side at zero server cost. The only effect is that the search box disappears on small option sets.Opt-out: set
custom-fields.selects.searchable_thresholdto0to always render the search box, which is exactly the pre-3.8 behavior.Ordering default, decided by measurement
The first cut of this PR defaulted
record_lookup.order_columnto'updated_at'. Measured on a seeded 50,000-row tenant withEXPLAIN (ANALYZE, BUFFERS):updated_at desc, id descORDER BY(3.7 behaviour)Ordering by the model key is deterministic and costs the same as the unordered query it replaces, because the key is already the tiebreaker. Ordering by an unindexed
updated_atsorts the whole tenant on every render.So the default is now
order_column => null, meaning the model key. Set it to'updated_at'if you want most-recently-touched-first, and index that column when the lookup table is large.Config
Consumers who published
config/custom-fields.phpbefore 3.8 do not have this block. Every read passes the same value as its default, so nothing breaks and the new defaults apply.Tests
15 new tests:
0opt-out, and the multi-select path.The tiebreaker was verified as load-bearing by removing it and watching the stability test fail.
Verification
pint,phpstan,rector --dry-runand the full Pest suite pass (846 passed, 3 todos) against the dependency set CI resolves (Laravel 13, Filament 5.7.6).The raised-minimum client behavior is covered by a render assertion rather than a browser click: this repo has no browser suite, and the consuming app installs from Packagist.