@batats/liveql
v1.0.0
Published
SQL-like live queries on plain JavaScript arrays — reactive queries without the database headache.
Maintainers
Readme
liveql
SQL-like queries on plain JavaScript arrays that stay alive. because manually re-filtering arrays every 2 seconds is character development
No dependencies. No build step. No "enterprise-grade cloud-native distributed reactive query engine" nonsense.
Just arrays and vibes.
built by batats
Install
npm install @batats/liveqlQuick example
import { liveql } from '@batats/liveql'
const users = [
{ name: 'batats', age: 20 },
{ name: 'maria', age: 17 },
]
const db = liveql(users)
const q = db.query('SELECT name WHERE age > 18 ORDER BY name')
q.onChange(results => {
console.log(results)
})
// Each of these triggers onChange automatically:
db.insert({ name: 'salama', age: 22 })
// → [
// { name: 'batats' },
// { name: 'salama' }
// ]
db.update(
u => u.name === 'maria',
{ age: 21 }
)
// → [
// { name: 'batats' },
// { name: 'maria' },
// { name: 'salama' }
// ]
db.delete(u => u.name === 'batats')
// → [
// { name: 'maria' },
// { name: 'salama' }
// ]How it works
- You create a
dbfrom an array withliveql(data). - You write a SQL-like query string and call
db.query(sql). - This returns a subscription handle.
- You attach a listener with
.onChange(fn). - Whenever data changes, liveql re-runs relevant queries automatically.
Basically:
you mutate data → liveql suffers for youbeautiful relationship honestly.
API
liveql(initialData)
Creates a new live database from an array of plain objects.
const db = liveql([{ id: 1, name: 'batats' }])
// or start empty:
const db = liveql([])Returns a db object with the methods below.
db.query(sql)
Parses a SQL-like query and returns a subscription handle.
The query is parsed once upfront, so parsing cost doesn't repeat on every update because we respect performance around here 😭
const q = db.query(`
SELECT name, age
WHERE age >= 18
ORDER BY name ASC
LIMIT 10
`)Supported SQL syntax
| Clause | Syntax | Example |
| -------- | ------------------------------------- | ---------------------------------- |
| SELECT | SELECT field1, field2 or SELECT * | SELECT name, age |
| WHERE | WHERE field op value | WHERE age > 18 |
| AND/OR | condition AND condition | WHERE age > 18 AND active = true |
| ORDER BY | ORDER BY field ASC\|DESC | ORDER BY name DESC |
| LIMIT | LIMIT n | LIMIT 5 |
Supported operators
> < >= <= = !=Value types in WHERE
Numbers
WHERE age > 20Strings
WHERE city = 'Cairo'or
WHERE city = "Cairo"Booleans
WHERE premium = trueNull
WHERE deletedAt = nullOperator precedence
AND binds tighter than OR, same as real SQL.
WHERE age < 10
OR score > 90 AND premium = trueis interpreted as:
WHERE age < 10
OR (
score > 90
AND premium = true
)because chaos has limits.
Subscription handle
The object returned by db.query().
.onChange(fn)
Registers a listener that fires whenever query results change.
Also fires immediately on registration with current results, because making you do an extra fetch would be rude.
q.onChange(results => {
console.log(results)
})Returns the handle so you can chain listeners:
q
.onChange(renderUI)
.onChange(logToConsole).getResults()
Returns the latest results without registering a listener.
const current = q.getResults()Useful when you just want a snapshot and not a whole reactive life commitment.
.unsubscribe()
Removes the subscription.
const q = db.query('SELECT *')
q.onChange(render)
// later...
q.unsubscribe()Your listener will stop firing. finally... peace.
db.insert(item)
Adds a new record and triggers active queries.
db.insert({
name: 'Nour',
age: 28,
city: 'Luxor'
})db.update(predicate, changes)
Updates all records matching the predicate.
Returns the number of updated records.
// change maria's age
db.update(
u => u.name === 'maria',
{ age: 21 }
)
// mark Cairo users as premium
db.update(
u => u.city === 'Cairo',
{ premium: true }
)db.delete(predicate)
Removes matching records.
Returns number of removed items.
db.delete(u => u.name === 'batats')
db.delete(u => u.age < 18)goodbye minors
db.getAll()
Returns a snapshot of all current records.
const allUsers = db.getAll()Returned objects are shallow copies.
So if you mutate nested objects directly...
here be dragonsMore examples
UI rendering pattern
const db = liveql(products)
// automatically re-renders on relevant updates
db.query(`
SELECT *
WHERE inStock = true
ORDER BY price ASC
`)
.onChange(renderProductList)Frontend frameworks after discovering arrays can be reactive too:
wait... it can do that without 17 hooks?Multiple independent queries
const db = liveql(tasks)
db.query(`
SELECT *
WHERE done = false
ORDER BY priority DESC
`)
.onChange(updateTodoList)
db.query(`
SELECT *
WHERE done = true
LIMIT 5
`)
.onChange(updateRecentlyCompleted)
// triggers only affected queries
db.update(
t => t.id === 42,
{ done: true }
)liveql does not wake up every query unnecessarily because we're not trying to cook your CPU.
Cleanup / teardown
const q = db.query(`
SELECT *
WHERE userId = 1
`)
q.onChange(render)
// later...
q.unsubscribe()Very important for UI frameworks. memory leaks are not a personality trait.
One-time reads
const q = db.query(`
SELECT name
WHERE active = true
ORDER BY name
`)
const snapshot = q.getResults()
q.unsubscribe()No listener overhead. grab data and disappear like Batman.
Error messages
liveql tries to give clear, actionable errors instead of emotionally damaging stack traces.
liveql: Query must start with SELECT.
Got: "FROM users WHERE..."liveql: Expected an operator after "age".
Got: "name"liveql: insert() expects a plain object.
Got array.liveql: update() first argument must be a predicate function.
Example: u => u.id === 1instead of:
TypeError: Cannot read properties of undefined
at internal/process/task_queues/whatever.js:937thank you JavaScript very cool
Notes & limitations
- In-memory only
- No persistence layer
- Uses shallow copies
- ESM only
- Uses
JSON.stringifyfor change detection
Works perfectly for plain data structures.
If you pass circular references:
may Allah guide youBrowser support
Works in:
- Node.js
- Browsers
- Bundlers
- That one side project at 3am you swore would be simple
No browser-specific APIs are used.
Philosophy
liveql is intentionally small.
It doesn't try to:
- replace databases
- become Redux
- become an ORM
- invent 14 layers of abstraction
- solve world hunger
It just makes arrays reactive using SQL-like syntax.
and honestly? that's enough sometimes.
License
MIT
Made with sleep deprivation, overthinking, and unnecessary passion by batats
