notpoiu/query_builder

A verbose query builder for QueryDescendants.

QueryBuilder

A verbose query builder for QueryDescendants.

Devforum Post: QueryBuilder

Wally Package: QueryBuilder

Installation

Add notpoiu/query-builder to your wally.toml:

[dependencies]
query-builder = "notpoiu/[email protected]"

Documentation

Selectors

MethodDescriptionSyntax Match
:SetClass(className)Matches by ClassNameClassName
:SetName(name)Matches by instance Name#Name
:AddTag(tag)Matches by CollectionService Tag.Tag
:SetProperty(key, value)Matches by property value[Key = Value]
:SetAttribute(key, value)Matches by attribute value[$Key = Value]

Combinators & Modifiers

MethodDescriptionLogic
:Child(query)Matches direct children:has(> query)
:Descendant(query)Matches descendants:has(>> query)
:Not(query)Negates the query:not(query)
:Or(query)
:Also(query)
Adds an alternative pathquery1, query2
:Has(query)Checks for existence of descendant/child:has(...)

Output

MethodDescription
:ToQuery()Returns the generated query string

Usage

local QueryBuilder = require(path.to.QueryBuilder)

local Query = QueryBuilder.new()
    :SetClass("Model")
    :SetName("Car")
    :Child(
        QueryBuilder
            :SetClass("VehicleSeat")
            :SetProperty("Disabled", false)
            :Not(QueryBuilder:AddTag("Broken"))
    )
    :ToQuery()

print(#game:QueryDescendants(Query))

Function-Based Queries

Instead of manually building strings or objects, you can also write a function that describes the object you want to find. QueryBuilder will reflect on your function to generate the correct query string.

Supported Operations

The instance passed to your function is a proxy, not a real Instance. Only the following members are supported for reflection:

MemberDescription
.PropertyANY property access (e.g. Part.Transparency).
.TagsSpecial table for table.find(part.Tags, "Tag") checks.
:GetAttribute(name)Reads an attribute value.
:FindFirstChild(name, recursive?)Checks for a direct child with the given name.
:FindFirstChildOfClass(className, recursive?)Checks for a direct child with the given ClassName.
:FindFirstChildWhichIsA(className, recursive?)Checks for a direct child that inherits from the given ClassName.

Danger

Calling any other methods (like :IsA(), :Clone(), :GetChildren()) will result in an error or undefined behavior.

Basic Queries

Match based on properties, attributes, and tags.

local QueryBuilder = require(path.to.QueryBuilder)
local v = QueryBuilder.value

local Query = QueryBuilder.fromOperation(function(part)
    -- Properties
    return part.Name == v("Coin")
       and part.Transparency == v(0)
       -- Attributes
       and part:GetAttribute("IsCollectable") == v(true)
       -- CollectionService Tags
       and table.find(part.Tags, "Interactive")
end):ToQuery()

-- Output: "#Coin.Interactive[Transparency = 0][$IsCollectable = true]"

Info

When comparing against literal values, use QueryBuilder.define(value)/QueryBuilder.value(value)/QueryBuilder.defineValue(value) to ensure proper reflection. Simple comparisons can often work, but if you encounter issues, use the defined functions.

Hierarchy & Negation

Check for children (Recursive=false), descendants (Recursive=true)

local QueryBuilder = require(path.to.QueryBuilder)
local v = QueryBuilder.value

local Query = QueryBuilder.fromOperation(function(model)
    -- Must have a specific child
    local head = model:FindFirstChild("Head")

    -- Must have a Humanoid descendant
    local humanoid = model:FindFirstChildOfClass("Humanoid", true)

    -- Must NOT be "DestroyedModel"
    return head ~= nil
       and humanoid ~= nil
       and model.Name ~= v("DestroyedModel")
end):ToQuery()

-- Output: ":not(#DestroyedModel):has(> #Head):has(Humanoid)"

Complex Queries (Alternatives)

You can use standard control flow (if/else) to describe alternative valid states. The builder will generate an Or query (comma-separated alternatives).

local QueryBuilder = require(path.to.QueryBuilder)
local v = QueryBuilder.value

local Query = QueryBuilder.fromOperation(function(part)
    local prompt = part:FindFirstChildOfClass("ProximityPrompt")

    if prompt then
        -- Path 1: If it has a prompt, it must be the "Interact" prompt
        return prompt.Name == v("Interact")
    else
        -- Path 2: If no prompt, it must be named "StaticPart"
        return part.Name == v("StaticPart")
    end
end):ToQuery()

-- Output: ":has(> ProximityPrompt[Name = "Interact"]), #StaticPart:not(:has(> ProximityPrompt))"

This allows you to express "A OR B" logic naturally.