Skip to main content

Query Builder

Classo includes a query builder for each SQL database that lets you build complex queries with filters, sorting, pagination, and joins. For fetching related data from hasMany and hasOne relations, you can use preload directly on the query.

Many-to-many queries

Fetching data across many-to-many relations via preload is not yet supported. For now use innerJoin to query across join tables manually. This will be added in a future release.


Import

Each database has its own query class:

// SQLite
const { SqliteQuery, ACTION_TYPES, LikePatterns, ORDER_DIRECTIONS } = require("@lol44lol/classo/query/sqliteQuery")

// PostgreSQL
const { PostgresqlQuery, ACTION_TYPES, LikePatterns, ORDER_DIRECTIONS } = require("@lol44lol/classo/query/postgresqlQuery")

// MySQL
const { MySQLQuery, ACTION_TYPES, LikePatterns, ORDER_DIRECTIONS } = require("@lol44lol/classo/query/mysqlQuery")

Basic Query Structure

Every query follows the same steps:

  1. Create a query instance with your DataClass
  2. Set the action type (SELECT, INSERT, UPDATE, DELETE)
  3. Set fields and table name
  4. Add filters (optional)
  5. Add limit, skip, orderBy (optional)
  6. Call build() to get the query and values
  7. Run with db.runQuery()

Select

const query = new SqliteQuery(UserDataClass)

query.setActionType(ACTION_TYPES.SELECT)
query.setSelectingFields("*") // or ["username", "age"] for specific fields
query.setTableName(UserDataClass)

query.startFiltering()
query.equals("username", "johndoe")
query.endFiltering()

query.limit(10).skip(0).orderBy("username", ORDER_DIRECTIONS.ASCENDING)

const { query: sql, values } = query.build()
const results = await db.runQuery(ACTION_TYPES.SELECT, sql, values) // SQLite needs action type
// PostgreSQL / MySQL: await db.runQuery(sql, values)
SQLite runQuery

SQLite's runQuery takes the action type as the first argument:

await db.runQuery(ACTION_TYPES.SELECT, sql, values)

PostgreSQL and MySQL do not need it:

await db.runQuery(sql, values)

Filters

All filters can be chained:

query.startFiltering()
.equals("username", "johndoe")
.greaterThan("age", 18)
.lessThan("age", 65)
.like("username", "john", LikePatterns.CONTAINS)
query.endFiltering()

Available filters

MethodDescriptionExample
equals(field, value)Exact match.equals("username", "john")
notEqual(field, value)Not equal.notEqual("status", "banned")
greaterThan(field, value)Greater than.greaterThan("age", 18)
lessThan(field, value)Less than.lessThan("age", 65)
like(field, value, pattern)Pattern match.like("name", "john", LikePatterns.CONTAINS)

LikePatterns

PatternMatches
LikePatterns.CONTAINSValue appears anywhere
LikePatterns.STARTS_WITHValue appears at the start
LikePatterns.ENDS_WITHValue appears at the end

Insert

const query = new SqliteQuery(UserDataClass)

query.setActionType(ACTION_TYPES.INSERT)
query.setTableName(UserDataClass)
query.insert({ username: "johndoe", password: "hashedpassword" })

const { query: sql, values } = query.build()
await db.runQuery(ACTION_TYPES.INSERT, sql, values)

Update

const query = new SqliteQuery(UserDataClass)

query.setActionType(ACTION_TYPES.UPDATE)
query.setTableName(UserDataClass)
query.update({ username: "janedoe" }) // new values

query.startFiltering()
query.equals("username", "johndoe") // which records to update
query.endFiltering()

const { query: sql, values } = query.build()
await db.runQuery(ACTION_TYPES.UPDATE, sql, values)

Delete

const query = new SqliteQuery(UserDataClass)

query.setActionType(ACTION_TYPES.DELETE)
query.setTableName(UserDataClass)

query.startFiltering()
query.equals("username", "johndoe")
query.endFiltering()

const { query: sql, values } = query.build()
await db.runQuery(ACTION_TYPES.DELETE, sql, values)

Use preload to fetch related data from hasMany and hasOne relations in a single call. Classo batches the queries efficiently — one query per relation level, not one per record.

const query = new SqliteQuery(UserDataClass)

query.setActionType(ACTION_TYPES.SELECT)
query.setSelectingFields("*")
query.setTableName(UserDataClass)

// fetch users with their messages and each message's meta
query.preload("messages.meta")

const { query: sql, values } = query.build()
const results = await query.execute(db)
console.log(results)
// [{ _id: "...", username: "john", messages: [{ text: "...", meta: {...} }] }]

Nested preload

Use dot notation to preload nested relations:

query.preload("messages.meta") // users → messages → meta

Preload with filters

Pass an optional query object per relation to filter preloaded data:

const messageQuery = new SqliteQuery(MessageDataClass)
messageQuery.startFiltering().equals("text", "Hello world").endFiltering()

query.preload("messages.meta", {
messages: messageQuery // only load messages matching this filter
})

const results = await query.execute(db)

execute()

When using preload, call execute(db) instead of build() + runQuery():

const results = await query.execute(db)

execute runs the main query, fetches all preloaded relations in batched queries, and returns the fully nested result.


Inner Join

Use innerJoin to query across related tables manually. This is the recommended approach for many-to-many relations until native preload support is added:

const { getFromTable } = require("@lol44lol/classo/query/utils")

const query = new MySQLQuery(OrderDataClass)

query.setActionType(ACTION_TYPES.SELECT)
query.setSelectingFields([
...getFromTable("orders", "orderDate", "status"),
getFromTable("customers", "customerName")
])
query.setTableName(OrderDataClass)

query.innerJoin(
"customers",
getFromTable("customers", "customerNumber"),
getFromTable("orders", "customerNumber")
)

query.startFiltering()
.greaterThan("orders.orderDate", "2024-01-01")
query.endFiltering()

query.limit(20)

const { query: sql, values } = query.build()
const results = await db.runQuery(sql, values)

getFromTable(table, ...fields) formats field names as table.field to avoid ambiguity when joining tables.

Other join types are also available:

query.leftJoin(joinTable, fieldOne, fieldTwo)
query.rightJoin(joinTable, fieldOne, fieldTwo)
query.fullJoin(joinTable, fieldOne, fieldTwo)

What's Next

  • Migration — update your schema as your DataClasses change