Chain - Fluent Query Syntax

Chain is a lightweight, flexible, and efficient data processing library inspired by LINQ. It allows you to write expressive, chainable queries for working with tables in Luau, making data manipulation simpler, cleaner, and more intuitive.

You may know similar concepts from LINQ (C#), Streams (Java), or built-in array methods in JavaScript/TypeScript. See more here.

Chain includes 26+ methods and uses a hybrid execution model combining lazy and eager evaluation. It also includes native Luau optimizations, memory pre-allocation and automatic query optimization, which may improve performance. It is also completely reusable!

Chain is mutable by default. Lazy methods do not immediately modify the original data; instead, they define transformations that produce a new table when executed. If a lazy method is called before an eager one, the original data will remain unchanged. To ensure immutability, you can use Chain.ArrayFromCopy() or Chain.DictFromCopy() to create a deep copy of the table.

Usage examples

Get the top 10 players with most kills after a game round.

local Chain = require(path.to.Chain)
local players = game.Players:GetPlayers()

local result = Chain.new(players)
	:Filter(function(p)
		return p.leaderstats.Kills > 0 -- Filter players with at least one kill
	end)
	:SortDescending(function(p)
		return p.leaderstats.Kills 	   -- Sort by kills in descending order
	end)
	:Take(10) 						   -- Take the top 10 players with the most kills
	:ToTable()						   -- Get the values in table format
See more examples

Example 1

Group players by their teams

local Chain = require(path.to.Chain)
local players = game.Players:GetPlayers()

local result = Chain.new(players)
	:Filter(function(p)
		return p.Team ~= nil  -- Filter players who are on a team
	end)
	:GroupBy(function(p)
		return item.Team.Name -- Group players by team
	end)
	:ToTable()				  -- Get the values in table format

print(result) -- Example output: {["Red"] = {player1, player2}, ["Blue"] = {player3}}

String Queries

Chain supports simple string queries, making common operations simpler and more readable. These strings are parsed into functions using regex (lua patterns). String queries can only compare numbers and strings.

local Chain = require(path.to.Chain)
local itemShop = {
	{ Item = "Sword", Price = 100 },
	{ Item = "Hat", Price = 25 },
	{ Item = "Toy", Price = 90 },
	{ Item = "Gun", Price = 250 },
}

local result = Chain.Array(itemShop)
	:Filter("Price >= 100") -- Get items with property "Price" higher or equals 100
	:ToTable()				-- Get the values in table format

--[[
Output: {
	{ Item = "Sword", Price = 100 }
	{ Item = "Gun", Price = 250 },
}
]]--
print(result)
See more about string queries

Properties comparison

Notice that we used Filter("Price >= 100"). The name “Price” is recognized as a property. Price >= 100 would be the same as: function(item) return item.Price >= 100 end. It works on anything with keys or properties. Instances and dictionaries are examples.

Currently, if you try to use string queries in Chain Dictionaries, it will use the dictionary key, not the value.

Direct comparison

You already know how to compare properties/keys. However, you might have an array of items without properties. Strings and numbers are some examples. The comparison works mostly the same way. In this case, you do not need to select a property. Consider a Chain of numbers: Filter(">=10")* is the same as function(num) return num >= 10 end. We didn’t check any property, only the item itself.

Selection

Sometimes you don’t want to perform a comparison, such as when using the Map() method. In that case, you just want to transform the collection. For example, you can select each item’s price without filtering by it.

local result = Chain.Array(itemShop)
	:Map("Price") -- Map items selecting the property "Price" of each one
	:ToTable()

We selected the Price property. But notice that we did not compare it with anything. Map("Price") would be the same as: function(item) return item.Price end. No comparisons, just selecting a value.

String Comparisons

  • “>=”
  • “<=”
  • “==”
  • “~=”
  • “>”
  • “<”

Some methods require a predicate (a query that returns a boolean), while others require a selection (a query that returns a value, except nil).

How fast it really is?

I’ve found just a few modules that do something like this. I compared each of them, and you can see the results here. Lower values mean faster execution times.

See how I measured it

This is the script used to measure the time
TestScript.rbxm (94.0 KB)

Feedback

What do you think about the module?
  • Liked it and I will use it.
  • Maybe I’ll give it a try.
  • After some changes, I might give it a chance…
  • Sorry, I don’t see the point.
0 voters
4 Likes