npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@batats/liveql

v1.0.0

Published

SQL-like live queries on plain JavaScript arrays — reactive queries without the database headache.

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/liveql

Quick 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

  1. You create a db from an array with liveql(data).
  2. You write a SQL-like query string and call db.query(sql).
  3. This returns a subscription handle.
  4. You attach a listener with .onChange(fn).
  5. Whenever data changes, liveql re-runs relevant queries automatically.

Basically:

you mutate data → liveql suffers for you

beautiful 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 > 20

Strings

WHERE city = 'Cairo'

or

WHERE city = "Cairo"

Booleans

WHERE premium = true

Null

WHERE deletedAt = null

Operator precedence

AND binds tighter than OR, same as real SQL.

WHERE age < 10
OR score > 90 AND premium = true

is 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 dragons

More 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 === 1

instead of:

TypeError: Cannot read properties of undefined
at internal/process/task_queues/whatever.js:937

thank you JavaScript very cool


Notes & limitations

  • In-memory only
  • No persistence layer
  • Uses shallow copies
  • ESM only
  • Uses JSON.stringify for change detection

Works perfectly for plain data structures.

If you pass circular references:

may Allah guide you

Browser 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