Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog.d/3350-uniqueness-scope-guard.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `validatesUniquenessOf(property="x", scope="y")` now returns a validation result instead of throwing `Component [Model] has no accessible Member with name [y]` when the scope property was never assigned. Building the uniqueness `WHERE` clause dereferenced every scope property without an existence guard, and a scope property is easy to leave absent rather than empty: `$setDefaultValues()` only seeds properties with an explicit `property()` mapping, so a column with a database-level default but no mapping is missing from a `new()`-ed object entirely. An absent scope property is now treated as blank, matching what a present-but-empty one has always done — including the existing conversion of an empty numeric scope to `IS NULL`. The `property(name="<scopeProperty>", defaultValue="")` workaround is no longer needed (#3350)
12 changes: 11 additions & 1 deletion vendor/wheels/model/validations.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -783,8 +783,18 @@ component {
* and converting blank numeric properties to IS NULL.
*/
public string function $buildWhereClausePart(required string property) {
// A property named in `scope=` may be absent rather than empty: `$setDefaultValues()`
// only seeds properties with an explicit `property()` mapping, so a column that has a
// database-level default but no mapping never appears on a `new()`-ed object. Reading
// it unguarded threw "has no accessible Member" out of a validation, so an absent
// scope property produced an exception where a blank one produced a validation result
// (issue #3350). The validated property itself cannot be absent here —
// `$shouldInvokeValidation()` skips the validation in that case — so this guard only
// ever fires for scopes. Treat absent as blank, which is the branch below that turns
// an empty numeric into `IS NULL`.
local.value = StructKeyExists(this, arguments.property) ? this[arguments.property] : "";
local.part = arguments.property & "=" & variables.wheels.class.adapter.$quoteValue(
str = this[arguments.property],
str = local.value,
type = validationTypeForProperty(arguments.property)
);
if (Right(local.part, 3) == "=''" && ListFindNoCase("integer,float,boolean", validationTypeForProperty(arguments.property))) {
Expand Down
26 changes: 26 additions & 0 deletions vendor/wheels/tests/specs/model/validationsSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,32 @@ component extends="wheels.WheelsTest" {
expect(combiKey.valid()).toBeFalse()
})

// Issue #3350: a property named in `scope=` that was never assigned is ABSENT
// from the object, not present-and-empty — `$setDefaultValues()` only seeds
// properties with an explicit `property()` mapping. The scope dereference had no
// existence guard, so building the uniqueness WHERE clause threw
// "Component [X] has no accessible Member with name [Y]" out of a method whose
// entire job is to return a validation result. The validated property itself
// cannot hit this — `$shouldInvokeValidation()` skips the validation when it is
// absent — so only `scope=` was exposed.
it("validatesUniquenessOf_with_absent_scope_property", () => {
combiKey = g.model("combiKey").new(id1 = 1, id2 = 1)
StructDelete(combiKey, "id2")

// must return a validation result rather than throwing
expect(combiKey.valid()).toBeFalse()
})

// An absent scope property must behave exactly like a present-but-empty one —
// the split between the two was the defect, not the value itself.
it("validatesUniquenessOf_absent_scope_matches_blank_scope", () => {
absent = g.model("combiKey").new(id1 = 1, id2 = "")
StructDelete(absent, "id2")
blank = g.model("combiKey").new(id1 = 1, id2 = "")

expect(absent.$buildWhereClausePart("id2")).toBe(blank.$buildWhereClausePart("id2"))
})

it("validatesUniquenessOf_with_blank_property_value", () => {
user.blank = ""
user.validatesUniquenessOf(properties = "blank")
Expand Down
Loading