Skip to content

Operators

Sleet ORM provides a complete set of operators for building type-safe query conditions. All operators automatically handle parameter binding to prevent SQL injection.

Comparison Operators

Basic Comparisons

lua
local sl = Sleet
local s  = require 'server.schema'
local db = sl.connect()

-- Equal to
local player = db.select()
    .from(s.players)
    .where(sl.eq(s.players.id, 123))
    .execute()[1]

-- Not equal to
local activePlayers = db.select()
    .from(s.players)
    .where(sl.ne(s.players.status, 'banned'))
    .execute()

-- Greater than
local highLevelPlayers = db.select()
    .from(s.players)
    .where(sl.gt(s.players.level, 50))
    .execute()

-- Greater than or equal to
local eligiblePlayers = db.select()
    .from(s.players)
    .where(sl.gte(s.players.age, 18))
    .execute()

-- Less than
local newPlayers = db.select()
    .from(s.players)
    .where(sl.lt(s.players.level, 10))
    .execute()

-- Less than or equal to
local moderateWarnings = db.select()
    .from(s.players)
    .where(sl.lte(s.players.warnings, 3))
    .execute()

Operator Reference

OperatorSQLDescriptionExample
sl.eq(col, val)=Equal tosl.eq(s.players.name, 'John')
sl.ne(col, val)!=Not equal tosl.ne(s.players.status, 'banned')
sl.gt(col, val)>Greater thansl.gt(s.players.level, 10)
sl.gte(col, val)>=Greater than or equalsl.gte(s.players.money, 1000)
sl.lt(col, val)<Less thansl.lt(s.players.age, 60)
sl.lte(col, val)<=Less than or equalsl.lte(s.players.warnings, 5)

String Operators

Pattern Matching

lua
-- LIKE pattern matching
local playersWithJohn = db.select()
    .from(s.players)
    .where(sl.like(s.players.name, '%John%'))
    .execute()

-- Starts with pattern
local steamPlayers = db.select()
    .from(s.players)
    .where(sl.like(s.players.identifier, 'steam:%'))
    .execute()

-- Ends with pattern
local gmailUsers = db.select()
    .from(s.players)
    .where(sl.like(s.players.email, '%@gmail.com'))
    .execute()

-- NOT LIKE
local nonDiscordPlayers = db.select()
    .from(s.players)
    .where(sl.notLike(s.players.identifier, 'discord:%'))
    .execute()

-- ILIKE case-insensitive (MySQL is case-insensitive by default)
local caseInsensitive = db.select()
    .from(s.players)
    .where(sl.ilike(s.players.name, '%JOHN%'))
    .execute()

String Operator Reference

OperatorSQLDescriptionExample
sl.like(col, pattern)LIKEPattern matchsl.like(s.players.name, '%John%')
sl.notLike(col, pattern)NOT LIKEDoes not matchsl.notLike(s.players.email, '%spam%')
sl.ilike(col, pattern)LIKECase-insensitive matchsl.ilike(s.players.name, '%ADMIN%')

Wildcard Reference

lua
-- % matches zero or more characters
sl.like(s.players.name, 'John%')    -- starts with 'John'
sl.like(s.players.name, '%Smith')   -- ends with 'Smith'
sl.like(s.players.name, '%John%')   -- contains 'John'

-- _ matches single character
sl.like(s.players.code, 'A_B')      -- one char between A and B
sl.like(s.players.phone, '138____1234') -- specific phone format

NULL Operators

lua
-- Check for NULL values
local playersWithoutEmail = db.select()
    .from(s.players)
    .where(sl.isNull(s.players.email))
    .execute()

-- Check for non-NULL values
local playersWithEmail = db.select()
    .from(s.players)
    .where(sl.isNotNull(s.players.email))
    .execute()

-- Find unbanned players (banned_until is NULL)
local activePlayers = db.select()
    .from(s.players)
    .where(sl.isNull(s.players.banned_until))
    .execute()

-- Find currently banned players
local bannedPlayers = db.select()
    .from(s.players)
    .where(sl.isNotNull(s.players.banned_until))
    .execute()

NULL Operator Reference

OperatorSQLDescriptionExample
sl.isNull(col)IS NULLValue is nullsl.isNull(s.players.deleted_at)
sl.isNotNull(col)IS NOT NULLValue is not nullsl.isNotNull(s.players.email)

Range Operators

IN and NOT IN

lua
-- IN - value in list
local vipPlayers = db.select()
    .from(s.players)
    .where(sl.inArray(s.players.role, { 'admin', 'moderator', 'vip' }))
    .execute()

-- Find specific levels
local specificLevels = db.select()
    .from(s.players)
    .where(sl.inArray(s.players.level, { 10, 20, 30, 40, 50 }))
    .execute()

-- NOT IN - value not in list
local normalPlayers = db.select()
    .from(s.players)
    .where(sl.notInArray(s.players.status, { 'banned', 'suspended', 'deleted' }))
    .execute()

-- Exclude specific IDs
local otherPlayers = db.select()
    .from(s.players)
    .where(sl.notInArray(s.players.id, { 1, 2, 3 }))
    .execute()

BETWEEN

lua
-- BETWEEN range query
local middleAged = db.select()
    .from(s.players)
    .where(sl.between(s.players.age, 25, 65))
    .execute()

-- Date range
local recentPlayers = db.select()
    .from(s.players)
    .where(sl.between(s.players.created_at, '2024-01-01', '2024-12-31'))
    .execute()

-- Money range
local middleClass = db.select()
    .from(s.players)
    .where(sl.between(s.players.money, 10000, 100000))
    .execute()

Range Operator Reference

OperatorSQLDescriptionExample
sl.inArray(col, values)IN (...)Value in listsl.inArray(s.players.role, {'admin', 'user'})
sl.notInArray(col, values)NOT IN (...)Value not in listsl.notInArray(s.players.status, {'banned'})
sl.between(col, min, max)BETWEEN ... AND ...Value in rangesl.between(s.players.level, 10, 50)

Logical Operators

AND Conditions

lua
-- AND - all conditions must match
local activeVipPlayers = db.select()
    .from(s.players)
    .where(sl.and_(
        sl.eq(s.players.is_active, true),
        sl.eq(s.players.is_vip, true),
        sl.gte(s.players.level, 10)
    ))
    .execute()

-- Complex AND
local specificPlayers = db.select()
    .from(s.players)
    .where(sl.and_(
        sl.like(s.players.name, 'John%'),
        sl.gte(s.players.money, 5000),
        sl.lte(s.players.warnings, 2),
        sl.isNotNull(s.players.email)
    ))
    .execute()

OR Conditions

lua
-- OR - any condition matches
local privilegedPlayers = db.select()
    .from(s.players)
    .where(sl.or_(
        sl.eq(s.players.is_admin, true),
        sl.eq(s.players.is_moderator, true),
        sl.gte(s.players.level, 100)
    ))
    .execute()

-- Multiple status check
local availablePlayers = db.select()
    .from(s.players)
    .where(sl.or_(
        sl.eq(s.players.status, 'online'),
        sl.eq(s.players.status, 'idle'),
        sl.eq(s.players.status, 'away')
    ))
    .execute()

NOT Conditions

lua
-- NOT - negate condition
local notBannedPlayers = db.select()
    .from(s.players)
    .where(sl.not_(sl.eq(s.players.is_banned, true)))
    .execute()

-- Equivalent to
local notBannedPlayers2 = db.select()
    .from(s.players)
    .where(sl.eq(s.players.is_banned, false))
    .execute()

-- Complex NOT
local exceptionalPlayers = db.select()
    .from(s.players)
    .where(sl.not_(sl.and_(
        sl.lt(s.players.level, 10),
        sl.eq(s.players.is_premium, false)
    )))
    .execute()

Logical Operator Reference

OperatorSQLDescriptionExample
sl.and_(cond1, cond2, ...)ANDAll conditions matchsl.and_(sl.eq(...), sl.gt(...))
sl.or_(cond1, cond2, ...)ORAny condition matchessl.or_(sl.eq(...), sl.eq(...))
sl.not_(condition)NOTNegate conditionsl.not_(sl.eq(s.players.active, false))

Complex Condition Combinations

Nested Logic

lua
-- Complex condition nesting
local targetPlayers = db.select()
    .from(s.players)
    .where(sl.and_(
        -- Base active conditions
        sl.eq(s.players.is_active, true),
        sl.isNull(s.players.deleted_at),
        
        -- Complex permission or level conditions
        sl.or_(
            -- Admin or moderator
            sl.or_(
                sl.eq(s.players.is_admin, true),
                sl.eq(s.players.is_moderator, true)
            ),
            -- Or high-level VIP
            sl.and_(
                sl.eq(s.players.is_vip, true),
                sl.gte(s.players.level, 50)
            ),
            -- Or wealthy regular player
            sl.and_(
                sl.gte(s.players.money, 100000),
                sl.gte(s.players.bank, 500000)
            )
        )
    ))
    .execute()

Dynamic Condition Building

lua
-- Build query conditions from parameters
function FindPlayers(filters)
    local conditions = {}
    
    -- Base: non-deleted users
    table.insert(conditions, sl.isNull(s.players.deleted_at))
    
    -- Optional filters
    if filters.name then
        table.insert(conditions, sl.like(s.players.name, '%' .. filters.name .. '%'))
    end
    
    if filters.minLevel then
        table.insert(conditions, sl.gte(s.players.level, filters.minLevel))
    end
    
    if filters.maxLevel then
        table.insert(conditions, sl.lte(s.players.level, filters.maxLevel))
    end
    
    if filters.roles and #filters.roles > 0 then
        table.insert(conditions, sl.inArray(s.players.role, filters.roles))
    end
    
    if filters.isActive ~= nil then
        table.insert(conditions, sl.eq(s.players.is_active, filters.isActive))
    end
    
    -- Combine all conditions
    local whereClause = (#conditions == 1) and conditions[1] or sl.and_(table.unpack(conditions))
    
    return db.select()
        .from(s.players)
        .where(whereClause)
        .execute()
end

-- Usage
local results = FindPlayers({
    name = 'John',
    minLevel = 10,
    roles = { 'admin', 'moderator' },
    isActive = true
})

Best Practices

1. Use Indexed Columns

lua
-- Good: use indexed columns for equality
db.select()
    .from(s.players)
    .where(sl.eq(s.players.identifier, identifier))  -- identifier usually has unique index
    .execute()

-- Avoid: leading wildcard prevents index usage on large tables
-- db.select().from(s.players).where(sl.like(s.players.name, '%John%'))
-- Prefer full-text search or dedicated search columns

2. Condition Order

lua
-- Put most selective conditions first
db.select()
    .from(s.players)
    .where(sl.and_(
        sl.eq(s.players.identifier, identifier),  -- most selective (unique)
        sl.eq(s.players.is_active, true),        -- medium selectivity
        sl.isNotNull(s.players.name)             -- least selective
    ))
    .execute()

3. Avoid Deep Nesting

lua
-- Break complex conditions into variables
local roleCondition = sl.inArray(s.players.role, { 'admin', 'moderator' })
local levelCondition = sl.gte(s.players.level, 50)
local moneyCondition = sl.gte(s.players.money, 100000)

local finalCondition = sl.and_(
    sl.eq(s.players.is_active, true),
    sl.or_(roleCondition, levelCondition, moneyCondition)
)

local results = db.select()
    .from(s.players)
    .where(finalCondition)
    .execute()

4. Parameter Safety

lua
-- Correct: parameter binding (automatic protection)
local userInput = "John'; DROP TABLE users; --"
db.select()
    .from(s.players)
    .where(sl.eq(s.players.name, userInput))  -- safe, auto-escaped
    .execute()

-- Wrong: string concatenation leads to SQL injection
-- local sql = "SELECT * FROM players WHERE name = '" .. userInput .. "'"

Next Steps

Released under the MIT License.