PHPackages                             joby/smol-query - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. joby/smol-query

ActiveLibrary[Database &amp; ORM](/categories/database)

joby/smol-query
===============

A lightweight and simple SQLite query builder.

v1.4.0(1w ago)03111MITPHPPHP &gt;=8.1CI passing

Since Feb 20Pushed 1w agoCompare

[ Source](https://github.com/joby-lol/smol-query)[ Packagist](https://packagist.org/packages/joby/smol-query)[ RSS](/packages/joby-smol-query/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (5)Dependencies (10)Versions (6)Used By (1)

smolQuery
=========

[](#smolquery)

A lightweight SQLite query builder and migration tool for PHP 8.3+.

Installation
------------

[](#installation)

```
composer require joby-lol/smol-query
```

About
-----

[](#about)

smolQuery provides a fluent interface for building SQLite queries and managing schema migrations. It is deliberately SQLite-only, which keeps the implementation simple and the API focused.

Basic Usage
-----------

[](#basic-usage)

```
use Joby\Smol\Query\DB;

$db = new DB('/path/to/database.db');

// Select
$rows = $db->select('users')
    ->where('active', 1)
    ->order('name ASC')
    ->limit(10)
    ->fetchAll();

// Insert
$db->insert('users')
    ->row(['name' => 'Alice', 'email' => 'alice@example.com'])
    ->execute();

// Update
$db->update('users')
    ->value('name', 'Alicia')
    ->where('id', 123)
    ->execute();

// Delete
$db->delete('users')
    ->where('id', 123)
    ->execute();
```

Select Queries
--------------

[](#select-queries)

### Fetching

[](#fetching)

```
$query = $db->select('users')->where('active', 1);

// Fetch one row (returns null when exhausted)
$row = $query->fetch();

// Fetch all rows as a generator
foreach ($query->fetchAll() as $row) {
    // ...
}

// Fetch a single column as a generator
foreach ($query->fetchColumn('email') as $email) {
    // ...
}

// Count results
$count = $query->count();
```

### Where Clauses

[](#where-clauses)

```
// Shorthand column = value
$query->where('status', 'active');

// Raw statement
$query->where('created_at > ?', $timestamp);

// Multiple parameters
$query->where('role = ? OR role = ?', ['admin', 'moderator']);

// NULL checks
$query->whereNull('deleted_at');
$query->whereNotNull('email');

// LIKE (case-sensitive by default, SQLite default is case-insensitive for ASCII)
$query->whereLike('name', 'alice%');
$query->whereLike('name', 'alice%', case_insensitive: true);
```

Multiple `where()` calls are AND'd together. For OR logic, use a single raw statement.

### Column selection, Order, Limit, Offset

[](#column-selection-order-limit-offset)

```
$db->select('users')
    ->column('id')
    ->column('name')
    ->order('name ASC')
    ->limit(20)
    ->offset(40)
    ->fetchAll();
```

### Hydration

[](#hydration)

By default, rows are returned as associative arrays. You can hydrate to objects using a callable or a class string.

```
// Callable hydrator
$query->hydrate(fn($row) => new User($row));

// Class string (uses PDO's FETCH_CLASS — note: not compatible with readonly properties)
$query->hydrate(User::class);

// Reset to arrays
$query->hydrate(null);
```

Parameters and values accept scalars, `Stringable` objects, backed enums (automatically unwrapped), Closures (lazily evaluated), and `null`.

Insert Queries
--------------

[](#insert-queries)

```
// Single row
$db->insert('users')
    ->row(['name' => 'Alice', 'email' => 'alice@example.com'])
    ->execute();

// Multiple rows (must have identical keys)
$db->insert('users')
    ->row(['name' => 'Alice', 'email' => 'alice@example.com'])
    ->row(['name' => 'Bob', 'email' => 'bob@example.com'])
    ->execute();
```

`execute()` returns the number of rows inserted.

Update Queries
--------------

[](#update-queries)

```
// Single value
$db->update('users')
    ->value('name', 'Alicia')
    ->where('id', 123)
    ->execute();

// Multiple values
$db->update('users')
    ->values(['name' => 'Alicia', 'email' => 'alicia@example.com'])
    ->where('id', 123)
    ->execute();

// Initial values via constructor
$db->update('users', ['name' => 'Alicia'])->where('id', 123)->execute();
```

`execute()` returns the number of affected rows. Without a WHERE clause, an exception is thrown unless you pass `execute(without_where: true)`.

Delete Queries
--------------

[](#delete-queries)

```
$db->delete('users')->where('id', 123)->execute();
```

Same `without_where` safety guard as UPDATE.

Upsert Queries
--------------

[](#upsert-queries)

```
// Insert or update on conflict
// updates all non-conflict columns by default, with conflict column defaulting to 'id'
$db->upsert('users')
    ->row(['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com'])
    ->execute();

// Explicit conflict column (defaults to 'id')
$db->upsert('users')
    ->conflictColumns('email')
    ->row(['name' => 'Alice', 'email' => 'alice@example.com'])
    ->execute();

// Composite conflict columns
$db->upsert('user_roles')
    ->conflictColumns(['user_id', 'role'])
    ->row(['user_id' => 1, 'role' => 'admin', 'granted_at' => time()])
    ->execute();

// Partial update — only update specific columns on conflict, preserve the rest
$db->upsert('users')
    ->updateColumns(['name', 'email'])
    ->row(['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com', 'created_at' => time()])
    ->execute();

// Multiple rows
$db->upsert('users')
    ->row(['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com'])
    ->row(['id' => 2, 'name' => 'Bob', 'email' => 'bob@example.com'])
    ->execute();
```

By default, all columns not used for conflict detection are updated. Use `updateColumns()` to restrict which columns are written on conflict.

`execute()` returns the number of affected rows. The conflict target must match an existing primary key or unique constraint.

Joins
-----

[](#joins)

```
// INNER JOIN — only rows with matches in both tables
$db->select('posts')
    ->join('users', 'users.id = posts.user_id')
    ->where('users.active', 1)
    ->fetchAll();

// LEFT JOIN — all rows from the left table, nulls for unmatched right table columns
$db->select('users')
    ->leftJoin('posts', 'posts.user_id = users.id')
    ->fetchAll();

// Multiple joins
$db->select('order_items')
    ->join('users', 'users.id = order_items.user_id')
    ->join('products', 'products.id = order_items.product_id')
    ->fetchAll();
```

By default, joined queries select `*` from all tables. If joined tables have columns with the same name, use explicit column selection to avoid collisions:

```
$db->select('posts')
    ->join('users', 'users.id = posts.user_id')
    ->column('posts.id')
    ->column('posts.title')
    ->column('users.name')
    ->fetchAll();
```

Custom query classes
--------------------

[](#custom-query-classes)

If you would like to extend the included query classes, such as to add your own helper methods for common operations, you can pass your own class name to the DB methods that create them:

```
$db->select('posts',YourCustomSelectClass::class)
    ->customFilter()
    ->fetchAll();
```

Migrations
----------

[](#migrations)

```
use Joby\Smol\Query\Migrator;

$migrator = new Migrator('/path/to/database.db');

// Programmatic migrations
$migrator->addMigration('001_create_users', '
    CREATE TABLE users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT UNIQUE NOT NULL
    );
    CREATE INDEX idx_users_email ON users(email);
');

// Directory of .sql files (sorted by filename, natural order)
$migrator->addMigrationDirectory(__DIR__ . '/migrations');

$migrator->run();
```

Migration files are named with a sortable prefix: `001_create_users.sql`, `002_add_email_index.sql`, etc. Each migration is recorded by name in a `_migrations` table. Migrations run inside a transaction and roll back on failure. Already-run migrations are skipped; out-of-order migrations throw an exception.

Multi-statement SQL files are fully supported.

### Custom Log Table

[](#custom-log-table)

```
$migrator = new Migrator('/path/to/database.db', log_table: '_schema_versions');
```

Requirements
------------

[](#requirements)

Fully tested on PHP 8.3+. Requires the `pdo_sqlite` and `sqlite3` PHP extensions (both enabled by default in most PHP installations).

License
-------

[](#license)

MIT License - See [LICENSE](LICENSE) file for details.

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance98

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community11

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~41 days

Total

5

Last Release

11d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/f4e606aa41ebfd7d82c365c6dc4ec158fb1b0bfff5f8dcbfad4ab53e3092e6a6?d=identicon)[jobyone](/maintainers/jobyone)

---

Top Contributors

[![joby-lol](https://avatars.githubusercontent.com/u/856610?v=4)](https://github.com/joby-lol "joby-lol (9 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/joby-smol-query/health.svg)

```
[![Health](https://phpackages.com/badges/joby-smol-query/health.svg)](https://phpackages.com/packages/joby-smol-query)
```

###  Alternatives

[jdorn/sql-formatter

a PHP SQL highlighting library

3.8k117.8M121](/packages/jdorn-sql-formatter)[backup-manager/backup-manager

A framework agnostic database backup manager with user-definable procedures and support for S3, Dropbox, FTP, SFTP, and more with drivers for popular frameworks.

1.7k1.6M11](/packages/backup-manager-backup-manager)[propel/propel1

Propel is an open-source Object-Relational Mapping (ORM) for PHP5.

8351.6M88](/packages/propel-propel1)[insolita/yii2-migration-generator

Set of gii tools for generating files for migration by schema of table , phpdoc or table data

108508.0k5](/packages/insolita-yii2-migration-generator)[xpdo/xpdo

A PDO-based Object/Relational Bridge Library

7088.4k4](/packages/xpdo-xpdo)[voku/session2db

A PHP library acting as a wrapper for PHP's default session handling functions which stores data in a MySQL database, providing both better performance and better security and protection against session fixation and session hijacking.

2920.0k](/packages/voku-session2db)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
