Skip to main content

PyPI - Version PyPI - License Language1 Language2 Language3

Logo

A nimble squirrel swiftly gathers a golden forest’s worth of acorns!

PyPI - Status Documentation Status GitHub Actions Workflow Status Codacy Codecov

PyPI - Python Version

If you find omni-json-db useful, please consider giving it a ⭐️!

✨ Introduction

omni-json-db is a high-performance, embedded database engine designed for Python developers. It combines the raw speed of a Key-Value store with the flexible querying of a document database and the associative power of a graph database.

Built for high throughput and thread safety, omni-json-db utilizes modern serialization (e.g., JSON, MsgPack, marshal, Pickle, YAML) and efficient compression to provide a compact storage layer. Whether you are building a local cache, a log aggregator, or a complex knowledge graph, omni-json-db offers “Zero-Config” simplicity at scale.

  • Schema-LESS: Store complex, nested data without pre-defining tables.

  • Server-LESS: Access data directly on disk without a database server overhead.

  • SQL-LESS: Manipulate data using standard Python syntax, Regex, and Lambdas.

  • Dependency-LESS: Runs on a clean Python install with zero required third-party packages — optional C-accelerators and extra formats are one pip install "omni-json-db[full]" away.

🤔 Why omni-json-db?

Unlike traditional SQL or NoSQL databases, omni-json-db allows you to query and manipulate data using native Python syntax—including slicing, lambdas, regex, and set operations. It also features built-in “Time-Travel” (undo/redo), a property-graph engine, and pluggable serialization.

omni-json-db has been tested with Python 3.7+ and PyPy3. (~100% test coverage)

omni-json-db

TinyDB

DiskCache

UnQLite

LMDB

RocksDict

SQLite

DuckDB

Transactions / ACID

⚠️ (atomic ops)

❌

❌

❌

✅

✅

✅

✅

Thread-safe concurrency

✅ (MR/SW)

❌

✅

✅

✅

✅

✅

✅

Multi-process access

✅ (file lock)

❌

✅

✅

✅

⚠️ (RO)

✅

✅

In-memory mode

✅

✅

❌

✅

❌

❌

✅

✅

CSV / SQLite / Parquet built-in

✅

❌

❌

❌

❌

❌

⚠️ (CLI)

✅

Compression built-in

✅

❌

✅

❌

❌

✅

❌

✅

No schema (Schema-less)

✅

✅

✅

✅

✅

✅

❌

❌

Dataclass mapping [z]

✅

❌

⚠️

❌

❌

⚠️

❌

❌

Groups / Namespaces

✅

✅

⚠️

✅

✅

✅

✅

✅

Nested groups + fan-out queries

✅

⚠️ (flat)

❌

⚠️

⚠️ (flat)

⚠️ (CF)

⚠️ (SQL)

⚠️ (SQL)

Pure Python (PyPy-friendly)

✅

✅

✅

❌

❌

❌

❌

❌

Pythonic queries (Lambda/Regex)

✅

✅

❌

❌

❌

❌

❌

❌

Deep nested search

✅

❌

❌

❌

❌

❌

❌

❌

Graph database engine

✅

❌

❌

❌

❌

❌

❌

❌

Undo / Redo (Time-Travel)

✅

❌

❌

❌

❌

❌

❌

❌

Time-series date slicing

✅

❌

❌

❌

❌

❌

❌

❌

Network mode (incl. groups)

✅

❌

❌

❌

❌

❌

❌

❌

Relative speed 500K records [x]

1.00x (baseline)

41.47x

60.95x

4.28x

0.94x

0.45x

0.13x

0.21x

Relative speed 10M records [x]

1.00x (baseline)

N/A [y]

N/A [y]

24.07x

1.31x

0.55x

0.16x

0.05x

[x] (1,2)

Lower is faster. (see the benchmark for the full breakdown and methodology)

[y] (1,2)

Impractically slow

[z]

Write a @dataclass and read it back as an object. ⚠️ = the object can be stored, but only as an opaque pickle blob: its fields are invisible to queries, and the file is unreadable outside Python.

🚀 Features

  • Native Graph Engine: Transform your Key-Value store into a Property Graph. The GraphDb layer supports O(1) adjacency indexing and classic algorithms (BFS, Dijkstra, DFS, cycle detection) without sacrificing performance. [refer to Graph Database]

  • Pythonic Interaction: Interact with data using familiar Python dict methods, list slicing, and set operations, avoiding complex SQL queries. [refer to Basic + Operator]

  • Dataclass Objects: Read and write any @dataclass directly — jdb += user, jdb['u1'] = user, del jdb[user]. The object is flattened into a plain dict rather than pickled, so records written from objects stay fully queryable, exportable, and language-neutral. [refer to Dataclass Objects]

  • Advanced Serialization & Compression: Combine formats (JSON, MsgPack, Pickle, YAML) with algorithms like LZ4, Zstandard, or Brotli to optimize your I/O and disk usage. [refer to Change Type + Supported Data Formats + Supported Zip Formats]

  • Pluggable Codec & Encryption: Bring your own serialization or encryption logic via a simple dumps/loads interface — no forking required. Supports both a process-wide default and per-instance codecs (e.g. per-tenant encryption keys). [refer to Pluggable User-Defined Codec (U)]

  • Powerful Query Engine: Execute searches via Regex, Lambda filters, and rich operators (EQ, GT, LT, IN, HAS, RE, …). [refer to Query Engine + More Query Examples + Pythonic Query Examples]

  • Operational Modes: Supports In-Memory mode (JMemFiles) for high performance and Network mode (JNetFiles) to serve data over a network. [refer to In-memory Mode + Network Mode ]

  • State Management: Built-in “Time-Travel” allows you to track states, undo modifications (unmodify()), or recover deleted data (unremove()). history() lists every version still reachable, and min_safe decides how many of them survive. [refer to Unremove & Unmodify + Record History + Backup & Restore]

  • Batch Writes: Bulk inserts take a dedicated append path — one write() per VAL file and one encoded block for the KEY rows instead of a syscall per record — so jdb += rows, insert() and every importer land 1.4x–4.5x faster with no API change. [refer to Bulk Insert]

  • Fast Open at Scale: A disposable key-table snapshot (index_cache) cuts a cold open by ~4x, and warm_up moves what is left onto a background thread. [refer to Faster Open]

  • Data Migration: Effortlessly migrate from SQLite or import/export via CSV, Parquet, INI, and TOML with simple commands. Parquet imports stream in constant memory via pyarrow record batches, so multi-GB files are no problem. [refer to CSV Import / Export + Parquet Import + SQLite Import + INI / TOML Import]

  • Time-Series Ready: Native timestamping allows for efficient date-based slicing (e.g., jdb[yesterday:now]). [refer to Time-Series]

  • Memory Caching: Adjustable cache_limit to balance RAM usage and I/O speed. [refer to Supported Key Table Formats]

  • Grouping & Namespaces: Easily isolate and manage different data modules using groups. [refer to Groups Mode]

  • Per-Record Flags: Give any record file-system-like attributes with a chmod-style syntax — read-only, append-only, hidden, uncached, no-history — plus symbolic links to other records or groups, and per-record TTLs from one minute to 511 days. [refer to Record Flags + Symbolic Links + Expiring Records (TTL)]

  • Concurrency Control: Optimized for Many-Read / Single-Write environments using a robust file-locking and Lock mechanism. [refer to Advanced]

🛠️ Quick Start

Installation

omni-json-db has zero required dependencies. It runs on a clean Python install using built-in pure-Python fallbacks, so this is all you need:

pip install omni-json-db

For the fastest, full-featured build — C-accelerated serializers (orjson / ormsgpack / msgspec / msgpack / bitarray), extra compression codecs (zstandard / lz4 / brotli), YAML value formats, and the BTrees key-index backend — install the full extra:

pip install "omni-json-db[full]"

Or pick only what you need:

pip install "omni-json-db[speed]"        # faster serializers only (drop-in, no new features)
pip install "omni-json-db[compression]"  # zstd / lz4 / brotli value compression
pip install "omni-json-db[yaml]"         # YAML
pip install "omni-json-db[btree]"        # B-tree key-index backend

Basic

from omni_json_db import JDb
# Initialize the database from file
# Key-Value is Json+MsgPack without compression
jdb = JDb("example.jdb")

# Store data
jdb["user1"] = {"name" : "Ryan", "role": "Developer"}

# Retrieve data
user = jdb["user1"]
print(user["name"], user["role"]) # Output: Ryan Developer

All standard dict methods work: keys(), values(), items(), get(), pop(), setdefault(), update().

Bulk Insert

Writing many records at once goes through a dedicated append path, so there is nothing to switch on — every batch entry point already uses it:

from omni_json_db import JDb

jdb  = JDb('users.jdb')
rows = {f'user{i}': {'age': i % 80} for i in range(100_000)}

jdb += rows                 # __iadd__ -> add()
jdb.insert(rows)            # never overwrites a key that already exists
jdb.update(rows)            # only touches keys already present
jdb.from_csv('users.csv')   # every importer streams in IMPORT_BATCH_ROWS-sized batches

add() splits the batch by key. A key that already exists is rewritten one at a time by f_write(), so update semantics, dead-row reuse and history are unchanged. Every new key is collected and handed to f_extend(), which appends the whole group in one pass: values are serialized and padded ahead of the writes, the padded bytes reach each VAL file in a single write() instead of one per record, and the KEY rows are encoded into one block and written once.

A batch append never takes a row back from the DEAD area. n_lines grows by exactly one row per new record, and the space parked by f_delete() stays available for later rewrites instead of being consumed by the insert — which is why the gain widens on a database that has already seen deletes.

50,000 records, best of 4

v3.00.00

v3.00.10+

fresh insert

0.616 s

0.366 s (1.7x)

insert into a DB with 10,000 DEAD rows

1.892 s

0.419 s (4.5x)

in-memory insert

0.540 s

0.324 s (1.7x)

inline (KEY-row) values

0.305 s

0.215 s (1.4x)

500 x 20-record batches

0.167 s

0.110 s (1.5x)

Under an explicit file handle, f_extend() is the batch counterpart of f_write() — it takes the write lock itself, so the caller does not have to:

with jdb.open(read_only=False) as fp:
   new = {'a': 1, 'b': [2, 3]}
   jdb.f_extend(fp, new, ttl=7)              # Output: True
   # on return, `new` holds exactly the records that were written
   jdb.f_extend(fp, {'a': 99}, replace=True) # rewrite existing keys too: a bulk upsert

ttl= and key_flags= are shared by every record in the call, and the work is done in bounded passes, so memory stays flat however long records is.

In-Memory Mode

from omni_json_db import JDb
# Initialize the database in memory
# Key-Value is Json+MsgPack without compression
jdb1 = JDb()

# Store data
jdb1 += {"user1" : {"name" : "Joe", "role": "Senior Developer"}}

# Retrieve data
print(jdb1["user1"]["name"]) # Output: Joe

# create 2nd JDb sharing same memory
jdb2 = JDb(jdb1)

# Store data to 2nd JDb
jdb2["user2"] = {"name" : "Kathy", "role": "CEO"}

# newly inserted data (by 2nd JDb)
print(jdb1["user2"]["name"]) # Output: Kathy

Query Engine

from omni_json_db import JDb
# Initialize the database in memory
# Key-Value is Json+Marshal with no compression
jdb = JDb(data_type="J+M")

# insert many records without key
jdb += [{'name': 'John', 'age': 22}, {'name': 'John', 'age': 37}, \
         {'name': 'Bob', 'age': 42}, {'name': 'Megan', 'age': 27}]

# get all records from database
print(jdb[:]) # print all records from jdb

# show table
jdb.show()

# Use FUNCTION to find record(s) matching the name 'John'
matches = jdb.find(FUNC=lambda key,val: val['name'] == 'John')
print(matches) # Output : {'0': {'name': 'John', 'age': 22}, '1': {'name': 'John', 'age': 37}}

# Use Regex to find record(s) matching the name 'John' or 'Bob'
matches = jdb.find(RE='John|Bob')
print(matches) # {'0': {'name': 'John', 'age': 22}, '1': {'name': 'John', 'age': 37}, '2': {'name': 'Bob', 'age': 42}}

Condition operators: EQ, NE, GT, LT, GTE, LTE, HAS, RE, RE2, FUNC, AND, OR, NOR, NOT, NAND, SIZE, ANY, ALL, NONE, IHAS, NHAS, EXISTS, TYPE, MOD, BETWEEN, NEAR, MATCH, SW, EW, IN, NIN, ANYIN.

Transform operators: ABS, CEIL, FLOOR, ROUND, FLOAT, INT, NEG, STR, AVG, STD, MAX, MID, MIN, SUM, FIRST, FLAT, LAST, LEN, SORT, UNIQUE, LOWER, UPPER, STRIP.

See More Query Examples or Pythonic Query Examples

Dataclass Objects

Any @dataclass with an id field can be written and read directly. The object is flattened into a plain dict — id is lifted out to become the record key — so the stored record is an ordinary one: still queryable, still exportable to CSV, still readable under every data_type.

from dataclasses import dataclass, field
from datetime import date
from decimal import Decimal
from enum import Enum
from typing import List, Optional
from omni_json_db import JDb, Query

class Role(str, Enum):
    ADMIN = 'admin'
    USER  = 'user'

@dataclass
class Address:
    city: str
    zipcode: str = ''

@dataclass
class User:
    id: str                                       # required: becomes the record key
    name: str
    age: int = 0
    role: Role = Role.USER
    tags: List[str] = field(default_factory=list)
    address: Optional[Address] = None
    joined: Optional[date] = None
    balance: Decimal = Decimal('0')

jdb = JDb('users.jdb')

# --- Create ---
jdb += User('u1', 'Alice', 30, Role.ADMIN, ['python'], Address('Taipei', '110'), date(2024, 5, 1), Decimal('12.50'))
jdb += [User('u2', 'Bob', 25), User('u3', 'Charlie', 35)] # batch: one transaction
jdb |= User('u1', 'Ignored')                              # insert only: u1 exists, nothing changes
jdb &= User('u2', 'Bobby', 26)                            # replace only
jdb['u4'] = User('u4', 'Diana', 28)                       # explicit key

# --- Read: members are filled in place, so one buffer can be reused ---
user = User(id='', name='')
jdb.get_dc('u1', user)
print(user.role, user.address.city, user.balance) # Output: Role.ADMIN Taipei 12.50

# --- Update ---
user.age += 1
jdb += user

# --- Delete ---
jdb -= user
del jdb[User('u2', '')]
del jdb[User('u3', ''), User('u4', '')] # del jdb['u3', 'u4']

# --- Still an ordinary record: queries run against the fields ---
jdb += [User('u5', 'Eve', 41, tags=['python']), User('u6', 'Frank', 22, address=Address('Taipei'))]
print(jdb.find(Query().age > 28))                # Output: {'u5': {'name': 'Eve', 'age': 41, ...}}
print(jdb.find(vals={'address.city': 'Taipei'})) # Output: {'u6': {'name': 'Frank', 'age': 22, ...}}

For explicit control over the file pointer, f_write_dc / f_read_dc are the low-level equivalents of f_write / f_read:

with jdb.open(read_only=False) as fp:
   jdb.f_write_dc(fp, User('u9', 'Grace'), ttl=30)

buf = User(id='', name='')
with jdb.open() as fp:
   for key in jdb.key_table:
      jdb.f_read_dc(fp, key, buf) # filled in place: no allocation per record

Supported field types: nested dataclasses, Optional / Union, Enum, date / time / datetime, Decimal, UUID, Path, List / Dict / Set / Tuple, frozen=True, and init=False fields. Values are rebuilt from the annotations, so a Set[str] comes back as a set even under a data_type whose codec has no native set type.

Unremove & Unmodify

from omni_json_db import JDb
# Initialize the database from file
# Key-Value is Json+Pickle with zstandard compression
jdb = JDb("fruit.jdb", data_type="J+P", zip_type='zs')

# add key
jdb["apple"] = "red"

# modify key
jdb["apple"] = "blue"

# unmodify key (equivalent to jdb.unmodify())
jdb.revert("apple")
assert jdb["apple"] == 'red'

# remove key
del jdb["apple"]
assert "apple" not in jdb

# unremove key (equivalent to jdb.unremove())
jdb.revert("apple")
assert jdb["apple"] == "red"

Record History

A superseded version stays parked in the index’s dead region for as long as JFlag.REVERT is active. history() lists what is still reachable, newest first, and hands back the ver numbers that revert(), unmodify() and unremove() accept as version=:

from omni_json_db import JDb

jdb = JDb('fruit.jdb')      # JFlag.REVERT is what parks the old row
jdb['apple'] = 'red'
jdb['apple'] = 'green'
jdb['apple'] = 'blue'

versions = jdb.history('apple', with_value=True, show=True)
#   history[apple]
#   ╔═════╤═════╤══════╤════════════╤════════════╤═══════╤═════╤══════╤═══════╗
#   ║ ver │ row │ live │ created    │ modified   │ flags │ TTL │ size │ value ║
#   ╟─────┼─────┼──────┼────────────┼────────────┼───────┼─────┼──────┼───────╢
#   ║ 2   │ 0   │ YES  │ 2026-08-20 │ 2026-08-20 │       │     │ 5    │ blue  ║
#   ║ 2   │ 1   │ NO   │ 2026-08-20 │ 2026-08-20 │       │     │ 6    │ green ║
#   ╚═════╧═════╧══════╧════════════╧════════════╧═══════╧═════╧══════╧═══════╝

print([(v['live'], v['value']) for v in versions])
# Output: [(True, 'blue'), (False, 'green')]

Each entry is a dict holding row, key, file_id, offset, row_size, val_size, ver, cdays, mdays, ttl, key_flags and live — plus value when with_value=True. live is True for the one record jdb[key] reads today and False for every parked version, so a deleted key has no live entry at all.

Only two versions come back above: 'red'’s row had already been reclaimed to make room for a later write. That is the rule, not a fault — history() is a best-effort audit trail, never a write-ahead log. Treat a missing version as expected.

Backup & Restore

from omni_json_db import JDb
# Initialize the database from file
# Key-Value is MsgPack+Json with Bzip2 compression
jdb = JDb("fruit.jdb", data_type="S+J", zip_type='bz')

# Add fruit to jdb
fruits = {'apple':'red', 'banana':'yellow', 'mango':'yellow', 'lemon':'yellow', 'tomato':'red'}
jdb += fruits
assert jdb == fruits

# backup jdb to bak folder = ./bak/fruit.jdb
jdb_bak = jdb.backup(folder='bak')
assert jdb_bak == jdb

# del all jdb data
del jdb[fruits]
assert len(jdb) == 0

# restore bak folder to jdb
jdb.restore(folder='bak')
assert jdb == fruits

Groups Mode

from omni_json_db import JDb
# Initialize the database from file
# Key-Value is Json+MsgPack with no compression
jdb = JDb('fruit_group.jdb')

# add red group
r_jdb = jdb.add_group('red')
assert r_jdb is jdb['red']

# add yellow group
y_jdb = jdb.add_group('yellow')
assert y_jdb is jdb['yellow']

# add fruits to red group
r_jdb += {'apple': {'qty':1}, 'tomato': {'qty':2}}

# add fruits to yellow group
y_jdb += {'banana': {'qty':4}, 'lemon': {'qty':6}, 'mango': {'qty':8}}

# read group records
print(jdb['red']['apple']['qty'])   # Output: 1
print(jdb['red:::apple'])           # Output: {'red:::apple': {'qty': 1}}
print(jdb['yellow:::banana'])       # Output: {'yellow:::banana': {'qty': 4}}

# find fruits which contain 'a' from all groups
matches = jdb.find(r':::a')
print(matches) # Output: ['red:::apple', 'red:::tomato', 'yellow:::banana', 'yellow:::mango']

Graph Database

omni-json-db natively supports Property Graph structures with the GraphDb class. You can easily manage nodes, edges, and run complex graph algorithms out of the box.

from omni_json_db import GraphDb, Query

# Initialize the graph database in memory (or from a file)
db = GraphDb()

# 1. Add Nodes with Schema-less Properties
db.add_node('Alice', age=25, role='admin')
db.add_node('Bob', age=30, role='user')
db.add_node('Charlie', age=35, role='user')

# 2. Add Edges (Directed or Undirected) with Properties
db.add_edge('Alice', 'Bob', directed=True, weight=1.5, relation='friend')
db.add_edge('Bob', 'Charlie', directed=True, weight=2.0, relation='colleague')
db.add_edge('Charlie', 'Alice', directed=False, weight=0.5) # Undirected edge

# 3. Neighborhood & Adjacency queries (O(1) lookups)
print(db.neighbors('Alice'))
# Output: {'Bob', 'Charlie'}

print(db.degree('Alice'))
# Output: {'in': 0, 'out': 1, 'undirected': 1, 'total': 2}

# 4. Classic Graph Algorithms Built-in
# Find the shortest path using Dijkstra based on edge weights
dist, path = db.dijkstra_shortest_path('Alice', 'Charlie', weight_key='weight')
print(f"Distance: {dist}, Path: {path}")
# Output: Distance: 0.5, Path: ['Alice', 'Charlie']

# Detect cycles in the graph (Alice -> Bob -> Charlie -> Alice)
print(db.is_cyclic())
# Output: True

# 5. Seamless Query Engine Integration
# You can still use the powerful Query object to filter nodes/edges!
q = Query()
admin_nodes = db.find_nodes(q.role == 'admin')
print(list(admin_nodes))
# Output: ['Alice']

# 6. Cascade Deletion
# Removing a node automatically cleans up all connected edges
db.remove_node('Bob')
print(db.has_node('Bob')) # Output: False
print(db.get_edge('Alice', 'Bob', directed=True)) # Output: None

CSV Import / Export

from omni_json_db import JDb
# Initialize the database in memory
# Key-Value is Json+Json with no compression
jdb1 = JDb(data_type="J+J")

# insert value without key
jdb1 += [{'name': 'John', 'age': 22}, {'name': 'John', 'age': 37}, \
         {'name': 'Bob', 'age': 42}, {'name': 'Megan', 'age': 27}]

# export the data to CSV
jdb1.to_csv('example.csv')

# show table
jdb1.show()

# create another JDb in memory
jdb2 = JDb()

# import the data from CSV
jdb2.from_csv('example.csv')
print(jdb2.find(RE='Bob')) # Output: {'2': {'name': 'Bob', 'age': 42}}

# show table
jdb2.show(RE='Bob')

INI / TOML Import

from omni_json_db import JDb
import io

jdb = JDb()

# --- Load INI Format ---
ini_data = """
[server]
host = 127.0.0.1
port = 8080
"""

jdb.from_ini(io.StringIO(ini_data)) # Also supports direct file paths like 'config.ini'
print(jdb['server/host']) # Output: 127.0.0.1

# --- Load TOML Format ---
toml_data = """
app_name = "Omni Test"
[network]
ip = "192.168.1.1"
port = 8181
"""

jdb.from_toml(io.StringIO(toml_data))

print(jdb['/app_name'])    # Output: Omni Test
print(jdb['network/ip'])   # Output: 192.168.1.1

SQLite Import

Step 1: Prepare sample.sqlite

import sqlite3
conn = sqlite3.connect('sample.sql')
cursor = conn.cursor()

cursor.execute('''
CREATE TABLE IF NOT EXISTS projects (
  id INTEGER PRIMARY KEY,
  name text NOT NULL,
  begin_date DATE,
  end_date DATE
)
''')

cursor.execute('''
CREATE TABLE IF NOT EXISTS project_logs (
  project_id INTEGER,
  action TEXT NOT NULL,
  log_date DATE
)
''')

cursor.execute('DELETE FROM projects')
cursor.execute('DELETE FROM project_logs')

projects_data = [
  (1, 'cooking', '2000-01-02', '2003-01-13'),
  (2, 'reading', '2023-05-01', '2023-12-31'),
  (3, 'coding', '2024-01-01', '2024-06-30')
]
cursor.executemany('INSERT INTO projects (id, name, begin_date, end_date) VALUES (?, ?, ?, ?)', projects_data)

logs_data = [
  (1, 'bought ingredients', '2000-01-01'),
  (1, 'started cooking', '2000-01-02'),
  (2, 'bought books', '2023-04-20'),
  (3, 'setup environment', '2024-01-01')
]
cursor.executemany('INSERT INTO project_logs (project_id, action, log_date) VALUES (?, ?, ?)', logs_data)

conn.commit()
conn.close()

Step 2: Import to JDb

from omni_json_db import JDb
jdb = JDb("migrated_data.jdb")

# Load an entire SQLite database with one line of code
jdb.from_sqlite('sample.sqlite')

# SQLite tables (e.g., 'projects' and 'project_logs') automatically become groups
projects = jdb['projects']
logs = jdb['project_logs']

# Query relational data using the NoSQL interface
print(projects[3]['name'])  # Get the name of the project with ID 3
print(len(logs))            # Get the total number of logs

# Combine with powerful Lambda queries to find logs for a specific project
project_3_logs = logs.find(FUNC=lambda val: val['project_id'] == 3)

Parquet Import

Requires the optional pyarrow dependency (pip install pyarrow).

import pyarrow as pa
import pyarrow.parquet as pq

# write a sample Parquet file
table = pa.table({
    '_id':   ['p1', 'p2', 'p3'],
    'name':  ['Alice', 'Bob', 'Charlie'],
    'age':   [30, 25, 35],
})
pq.write_table(table, 'sample.parquet')
from omni_json_db import JDb
jdb = JDb()

# by default, the first column ('_id') becomes the record key
jdb.from_parquet('sample.parquet')
print(jdb['p1'])  # Output: {'name': 'Alice', 'age': 30}

# rows stream in via pyarrow RecordBatches, so multi-GB files
# import with a small, constant memory footprint
jdb.from_parquet('sample.parquet', batch_size=65536)

# pick a different key column, and prune to only the columns you need
jdb.from_parquet('sample.parquet', key='_id', columns=['name'])

Network Mode

Server side

from omni_json_db import JDb, run_files_server

jdb = JDb('storage.jdb')

# equivalent to: files='storage.jdb'
run_files_server(host='127.0.0.1', port=59898, files=jdb)

# write key to JDb
jdb['remote-key'] = 'secret'

Client side

from omni_json_db import JDb

# connect to files server
jdb = JDb('127.0.0.1:59898')

# read remote key from JDb
print(jdb['remote-key']) # Output: secret

Change Type

from omni_json_db import JDb

# Initialize the database in memory
# Key-Value is Json+Json with no compression
jdb = JDb(data_type='J+J')

fruits = {'apple':'red', 'banana':'yellow', 'mango':'yellow', 'lemon':'yellow', 'tomato':'red'}

# add all fruits to database
jdb += fruits
assert jdb == fruits
print(jdb.data_type, jdb.zip_type) # Output: J+J no

# change data_type to 'S+S' and zip_type to 'lz'
jdb.upgrade(data_type='S+S', zip_type='lz')
assert jdb == fruits
print(jdb.data_type, jdb.zip_type) # Output: S+S lz

# only change KEY type from 'S' to 'J'
jdb.change_KEY('J')
assert jdb == fruits
print(jdb.data_type, jdb.zip_type) # Output: J+S lz

Time-Series

from omni_json_db import JDb
import datetime as dt

# Initialize the database in memory
# Key+Value is Json+Json with Gzip compression
# using BTree as Key Table for better memory usage
jdb = JDb(data_type="J+J(gz)", key_limit="bt")

# insert data
fruits = {'apple':'red', 'banana':'yellow', 'mango':'yellow', 'lemon':'yellow', 'tomato':'red'}
jdb += fruits

# datetime for create date, date for modify date
now = dt.datetime.now()
today = now.date()

# find create date: date == now
matches = jdb[now]
assert matches == fruits

# find create date: date >= now
matches = jdb[now:]
assert matches == fruits

# find create date: date < now
matches = jdb[:now]
assert len(matches) == 0

# find create date: now <= date <= now+1
next_date = now + dt.timedelta(days=1)
matches = jdb[now:next_date]
assert matches == fruits

prev_date = now - dt.timedelta(days=1)
prev_week = now - dt.timedelta(days=7)

# change key create date
jdb.keys['apple', 'tomato'] = prev_date
jdb.keys['mango'] = prev_week
assert jdb[prev_date] == {'apple':'red', 'tomato':'red'}
assert jdb[prev_week] == {'mango':'yellow'}

# find create date: date == now
matches = jdb[now]
assert set(matches) == {'banana', 'lemon'}

# find create date: date < now
matches = jdb[:now]
assert set(matches) == {'apple', 'mango', 'tomato'}

# find modify date: date == today
matches = jdb[today]
assert matches == fruits

# change key modify date + create date
new_modify_date = prev_date.date()
new_create_date = prev_week.date()
assert new_modify_date >= new_create_date
jdb.keys['lemon'] = f'{new_modify_date} {new_create_date}'

# find modify date: date == today
matches = jdb[today]
assert set(matches) == {'apple', 'banana', 'mango', 'tomato'}

# find modify date: date == prev_date
matches = jdb[prev_date.date()]
assert set(matches) == {'lemon'}

# change all keys create date
jdb.keys[:] = today
assert jdb[today] == fruits

Operator

from omni_json_db import JDb
# Initialize the database in memory
# Key+Value is MsgPack+MsgPack with lz4 compression
jdb = JDb(data_type="S+S(lz)")

# [1] KEY+VAL operators
# <jdb += data> == jdb.update(data)
data = {f'key{v}':v for v in range(100)}
jdb += data
assert len(jdb) == 100

# <jdb == data>
assert jdb == data

# <jdb |= ..> == jdb.insert(..)
jdb |= {f'key{v}':v+1 for v in range(102)}
assert jdb['key100'] == 101
assert jdb[-2.:] == {'key100':101, 'key101':102} # get last two modified records
assert jdb[(f'key{v}' for v in range(100))] == data # equivalent to jdb[data] == data

# <jdb -= ..> == jdb.remove(..)
jdb -= ['key100', 'key101', 'key102', 'key103']
assert jdb == data

# <jdb &= ..> == jdb.replace(..)
jdb &= {f'key{v}':v+1 for v in range(200)}
assert jdb == {f'key{v}':v+1 for v in range(100)}

# <jdb ^= ..> == jdb.unmodify(..)
jdb ^= {f'key{v}' for v in range(100)} # equivalent to jdb ^= data
assert jdb == data

# <jdb[:] = ..> == jdb.update(..)
jdb[:] = 0 # set all records to zero
assert jdb == {f'key{v}':0 for v in range(100)}
assert jdb.find(NE=0) == {}

# remove all records
jdb -= jdb # equivalent to del jdb[:]
assert len(jdb) == 0

# <jdb ^= ..> == jdb.unremove(..)
jdb ^= {f'key{v}' for v in range(100)} # equivalent to jdb ^= data
assert all(val == 0 for key,val in jdb.items())

# lambda VALUE operation
jdb[:] = lambda key,val: int(key.replace('key', '')) + val
assert jdb == data

# <del jdb[..]> == jdb.remove_fast(..)
del jdb[data] # equivalent to del jdb[:]

# unremove all data
jdb ^= data
assert jdb == data

# <jdb[..]> == jdb.get_n(..) or jdb.get_all()
matches = jdb[('key2', 'key22', 'key44', 'key111')]
assert matches == {'key2':2, 'key22':22, 'key44':44}

# lambda KEY operation
matches = jdb[lambda key:key.endswith('1')]
assert set(matches) == {'key1', 'key11', 'key21', 'key31', 'key41', 'key51', 'key61', 'key71', 'key81', 'key91'}

# set all matched records to -1
jdb[matches] = -1
matches_2 = jdb[lambda key,val: val == -1]
assert set(matches) == set(matches_2)
assert matches_2 == jdb.find(EQ=-1)
assert matches_2 == jdb.find(FUNC=lambda val: val == -1)

# RE search
matches_3 = jdb[::r'1$']
assert matches_2 == matches_3

# unmodify
jdb ^= matches
assert jdb == data

# [2] KEY operators
# <jdb & {..}> == jdb.intersection(..)
matches = jdb & {f'key{v}' for v in range(98, 120)}
assert matches == {'key98', 'key99'}

# <{..} & jdb> == {..}.intersection(jdb)
matches_2 = {f'key{v}' for v in range(98, 120)} & jdb
assert matches == matches_2

# <jdb | {..}> == jdb.union(..)
matches = jdb | {f'key{v}' for v in range(10, 120)}
assert matches == {f'key{v}' for v in range(0, 120)}

# <{..} | jdb> == {..}.union(jdb)
matches_2 = {f'key{v}' for v in range(10, 120)} | jdb
assert matches == matches_2

# <jdb + {..}> == jdb.union(..)
matches = jdb + {f'key{v}' for v in range(10, 120)}
assert matches == matches_2

# <{..} + jdb> == {..}.union(jdb)
matches_2 = {f'key{v}' for v in range(10, 120)} + jdb
assert matches == matches_2

# <jdb - {..}> == jdb.difference(..)
matches = jdb - {f'key{v}' for v in range(0, 98)}
assert matches == {'key98', 'key99'}

# <{..} - jdb> == {..}.difference(jdb)
matches = {f'key{v}' for v in range(2, 102)} - jdb
assert matches == {'key100', 'key101'}

# <jdb ^ {..}> == jdb.non_intersection(..)
matches = jdb ^ {f'key{v}' for v in range(1, 101)}
assert matches == {'key0', 'key100'}

# <{..} ^ jdb> == {..}.non_intersection(jdb)
matches_2 = {f'key{v}' for v in range(1, 101)} ^ jdb
assert matches == matches_2

# <.. in jdb> == jdb.has_all(..)
assert 'key10' in jdb
assert {'key10', 'key90'} in jdb
assert {'key10', 'key90', 'key110', 'key190'} not in jdb
assert jdb.has('key10')
assert jdb.has_all('key10')
assert jdb.has_any('key10')
assert jdb.has_all({'key10', 'key90'})
assert jdb.has_any({'key10', 'key90', 'key110', 'key190'})
assert jdb.is_disjoint({'key110', 'key190'})

All standard set methods work: union(), intersection(), difference(), isdisjoint(), issubset(), issuperset().

More Query Examples

Below are examples of how to utilize the various parameters and NoSQL syntax.

from omni_json_db import JDb
import re

# Initialize an in-memory database
jdb = JDb()

# Sample user records
users = {
   'user_1': {'name': 'Alice', 'age': 30, 'email': 'alice@example.com', 'role': 'admin', 'tags': ['python', 'database']},
   'user_2': {'name': 'Bob', 'age': 25, 'role': 'developer', 'tags': ['javascript', 'web']},
   'user_3': {'name': 'Charlie', 'age': 35, 'role': 'developer', 'tags': ['python', 'linux', 'aws']},
   'user_4': {'name': 'Diana', 'age': 28, 'email': 'diana@test.com', 'role': 'designer', 'tags': ['ui', 'ux']}
}

# Insert data
jdb += users

# 1. Exact Match & Global Search (ANY, RE, RE2)
#----------------------------------------------------------
# Find users where any attribute exactly matches 'Alice'
res = jdb.find(ANY='Alice')
assert list(res) == ['user_1']

res2 = jdb.find(vals={'name': 'Alice'})
assert res == res2

# converts values into JSON string format for searching.
# Find any record that has the string 'designer' inside it
res = jdb.find(RE=r'designer') # vals={'$re':r'designer'}
assert list(res) == ['user_4']

# RE2 removes JSON symbols (,[]{}") before searching
res = jdb.find(RE2=r'role:designer')
assert list(res) == ['user_4']

res2 = jdb.show(vals={'role.$re': r'd.+ner'}) # {'role': re.compile(r'd.+ner')}
assert res == res2

# 2. Relational & Conditional Operators (vals)
#----------------------------------------------------------
# Age is greater than or equal to 30
res = jdb.find(vals={'age': {'$gte': 30}}) # find(ANY={'$gte': 30})
assert list(res) == ['user_1', 'user_3']

res2 = jdb.find(vals={'age.$ge': 30})
assert res == res2

# Age is strictly less than 30
res = jdb.find(vals={'age': {'$lt': 30}}) # find(ANY={'$lt': 30})
assert list(res) == ['user_2', 'user_4']

res2 = jdb.find(vals={'age.$lt': 30})
assert res == res2

# Role is either 'admin' or 'designer'
res = jdb.find(vals={'role': {'$in': ['admin', 'designer']}})
assert list(res) == ['user_1', 'user_4']

res2 = jdb.find(vals={'role': ['admin', 'designer']}) # {'role.$in': ['admin', 'designer']}
assert res == res2

# tags contains 'python'
res = jdb.find(vals={'tags': {'$has': 'python'}})
assert list(res) == ['user_1', 'user_3']

res2 = jdb.find(vals={'tags.$has': 'python'})
assert res == res2

# Age is NOT 30
res = jdb.find(vals={'age': {'$ne': 30}}) # find(ANY={'$ne': 30})
assert list(res) == ['user_2', 'user_3', 'user_4']

res2 = jdb.find(vals={'!age': 30}) # {'age.$ne': 30}
assert res == res2

# Age is 28
res = jdb.find(vals={'age': {'$eq': 28}}) # find(ANY={'$eq': 28})
assert list(res) == ['user_4']

res2 = jdb.find(vals={'age': 28})
assert res == res2

# 40 >= Age > 25
res = jdb.find(vals={'age': {'$gt': 25, '$lte': 40}}) # {'age.$gt':25, 'age.$lte':40}
assert list(res) == ['user_1', 'user_3', 'user_4']

res2 = jdb.show(vals={'age.$between': (26, 40)})
assert res == res2

# 3. Logical Grouping (AND, OR, NOR, NOT)
#----------------------------------------------------------
# Age >= 25 AND Age <= 30
res = jdb.find(AND=[{'age': {'$gte': 25}}, {'age': {'$lte': 30}}])
assert list(res) == ['user_1', 'user_2', 'user_4']

res2 = jdb.find(vals={'age.$ge': 25, 'age.$le': 30})
assert res == res2

# Role is 'admin' OR Age > 30
res = jdb.find(OR=[{'role': 'admin'}, {'age': {'$gt': 30}}])
assert list(res) == ['user_1', 'user_3']

res2 = jdb.find(OR=[{'r*e': 'admin'}, {'age.$gt': 30}])
assert res == res2

# Role is not 'admin' AND Age <= 30
res = jdb.find(NOR=[{'role': 'admin'}, {'age.$gt': 30}])
assert list(res) == ['user_2', 'user_4']

# User is NOT a developer
res = jdb.find(NOT={'role': 'developer'})
assert list(res) == ['user_1', 'user_4']

res2 = jdb.find(vals={'!role': 'developer'})
assert res == res2

# (Role is 'admin' OR Age > 30) AND 'linux' not in tags
res = jdb.find(AND=[
   {'$or': [
      {'role': 'admin'},
      {'age': {'$gt': 30}}
   ]},
   {'$not': {'tags': {'$has': 'linux'}}}
])
assert list(res) == ['user_1']

res2 = jdb.show(vals={'$or': [{'role': 'admin'}, {'age.$gt': 30}], '!tags.$has': 'linux'})
assert res == res2

# 4. Regular Expressions (RE, RE2, re.compile)
#----------------------------------------------------------
# Values matching an email domain regex
res = jdb.find(vals={'email': re.compile(r'.@example.com')})
assert list(res) == ['user_1']

# Find users where any attribute exactly matches regex
res = jdb.find(ANY=re.compile(r'.@example.com'))
assert list(res) == ['user_1']

# Global regex search for strings containing 'li' (matches 'Alice', 'Charlie', 'linux')
res = jdb.find(RE=r'li[a-z]')
assert list(res) == ['user_1', 'user_3']

# Match specific Database Keys using compiled regex (e.g., matching 'user_1', 'user_2')
res = jdb.show(re.compile(r'^user_[1-2]$'))
assert list(res) == ['user_1', 'user_2']

# 5. Array / List Operations
#----------------------------------------------------------
# Users with exactly 2 tags in their list
res = jdb.find(vals={'tags': {'$size': 2}})
assert list(res) == ['user_1', 'user_2', 'user_4']

res2 = jdb.find(vals={'tags.$size': 2})
assert res == res2

# Users whose FIRST tag (index 0) is 'python'
res = jdb.find(vals={'tags': {'$0': 'python'}})
assert list(res) == ['user_1', 'user_3']

res2 = jdb.show(vals={'tags.0': 'python'})
assert res == res2

# 6. Lambda / Custom Functions (FUNC) & Pagination (limit)
#----------------------------------------------------------
# Pass a lambda to evaluate both the key and the value dynamically
# Example: Find the first users whose age is an even number
res = jdb.find(
    FUNC=lambda k, v: isinstance(v, dict) and v.get('age', 1) % 2 == 0,
   limit=1
)
assert list(res) == ['user_1']

res2 = jdb.find(vals={'age.$mod': (2, 0)}, limit=1)
assert res == res2

# Users with email
res = jdb.find(vals={'email': lambda v: v != ''})
assert list(res) == ['user_1', 'user_4']

res2 = jdb.find(EXISTS='email')
assert res == res2

# Users without email
res = jdb.find(NOT={'email': lambda v: v != ''})
assert list(res) == ['user_2', 'user_3']

res2 = jdb.find(vals={'!$exists': 'email'})
assert res == res2

# For primitive stored values (non-nested), you can use quick keyword arguments:
jdb['simple_counter'] = 50
res = jdb.find(EQ=50)
assert list(res) == ['simple_counter']

res = jdb.show(IN=[40, 50]) # Value in list
assert list(res) == ['simple_counter']

Operators Reference

Operator

Description

Example Usage

. | /

Accesses nested fields within a document using a deep path.

{'user.profile.age': {'$gt': 20}}, {'user|tags|0': 'db'}

?

[Single-char Wildcard] Matches exactly one single character within a key name.

{'user?.prof???.?ge': {'$gt': 20}}, {'user?.tags.?': 'db'}

*

[Wildcard] Matches any key at the current level in the document structure.

{'users.*.role': 'admin'}, {'user*|t*gs|*': 'db'}

**

[Recursive Wildcard] Recursively searches and matches the specified key or field at any depth level within the document.

{'**.role': 'admin'}, {'meta.**': 'database'}

$0, $1, …

Matches the element exactly at the specified index (0, 1…) of an array.

{'$0': 'python'}

$date / _date

Targets the database record’s internal date for condition matching.

{'$date': {'$lt': date(2001, 1, 1)}}, {'_date': date(2011,12,1)}

$key / _id

Targets the database record’s dictionary key/ID for condition matching.

{'$key': 'user_1'}, {'_id': 'user_1'}

$not / !

Inverts the effect of a query expression (Logical NOT).

{'$not': {'tags': {'$has': 'linux'}}}, {'!tags': {'$has': 'linux'}}, {'tags': {'!$has': 'linux'}}

$and

Joins query clauses with a logical AND.

{'$and': [{'$has':'python'}, {'$has':'linux'}]}

$nand / !$and

Joins query clauses with a logical NAND (Not AND).

{'$nand': [{'$has':'python'}, {'$has':'linux'}]}

$or

Joins query clauses with a logical OR.

{'$or': [{'$eq': 2000}, {'$eq': 2010}]}

$nor / !$or

Joins query clauses with a logical NOR.

{'$nor': [{'$eq': 2000}, {'$eq': 2010}]}

$all

Matches if ALL elements in the value array/iterable match the condition.

{'$all': {'$ne': 0}}

$any

Matches if ANY element in the value array/iterable matches the condition.

{'$any': 'python'}

$none / !$any

Matches if NO elements in the value array/iterable match the condition.

{'$none': {'age': 30}}

$func

Evaluates a custom lambda function on the field to determine match.

{'$func': lambda x: x > 0}

$eq

Matches values that are exactly equal to the specified value.

{'$eq': 28}

!$eq / $ne

Matches values that are not equal to the specified value.

{'$ne': 30}, {'!$eq': 30}

$gt

Matches values strictly greater than the specified value.

{'$gt': 25}

$gte / $ge

Matches values greater than or equal to the specified value.

{'$gte': 30}

$lt

Matches values strictly less than the specified value.

{'$lt': 30}

$lte / $le

Matches values less than or equal to the specified value.

{'$lte': 40}

$in

Matches if the value is any of the elements specified in an array/set.

{'$in': ['admin', 'designer']}

!$in / $nin

Matches if the value does NOT exist in the specified array/set.

{'$nin': ['python', 'db']}, {'!$in': ['python', 'db']}

$anyin

Matches if ANY element in the value array/iterable exists in the specified array/set.

{'$anyin': ['admin', 'manager']}

$between

Matches values within a specified inclusive range (min, max).

{'$between': (26, 40)}

!$between

Matches values strictly outside a specified range.

{'!$between': (26, 40)}

$near

Matches numeric/date values within a tolerance range (target, offset).

{'$near': (20, 9)}

$mod

Matches values where value % divisor == remainder (passed as a tuple).

{'$mod': (10, 5)}

$has

Matches arrays or strings containing the specified element/substring.

{'$has': 'python'}

!$has / $nhas

Matches if the specified element or substring is NOT contained.

{'$nhas': 'r_1'}, {'!$has': 'r_1'}

$ihas

Case-insensitive match for arrays or strings containing the specified element/substring.

{'$ihas': 'UseR_'}

$re / $regex

Matches string values using a Regular Expression.

{'$re': r'li[a-z]'}, {'$re': re.compile(r'li[a-z]')}

$re2

Matches using Regex after stripping JSON formatting symbols ([]{}"") from the string.

{'$re2': r'role:admin'}

$ew

Matches string values that end with a specified substring.

{'$ew': '_suffix'}

$sw

Matches string values that start with a specified substring.

{'$sw': 'prefix_'}

$exists

Matches documents that have the specified field/key.

{'$exists': ['age', 'tags']}, {'$exists': 'tags'}

!$exists

Matches documents that lack the specified field/key.

{'!$exists': 'age'}

$size

Matches if the size/length of an array/string equals the specified value.

{'$size': [1,2,3]}

!$size

Matches if the size/length does NOT equal the specified value(s).

{'!$size': [1,2,3]}

$type

Matches if the value is of the specified Python variable type.

{'$type': list}

$abs

Takes the absolute value of a number before comparing.

{'$abs': 3.14}

$ceil

Takes the ceiling (math.ceil) of a number before comparing.

{'$ceil': 2}

$floor

Takes the floor (math.floor) of a number before comparing.

{'$floor': 2}

$round

Round a number before comparing.

{'$round': 2}

$float

Casts the value to a float before comparing.

{'$float': 1.0}

$int

Casts the value to an integer before comparing.

{'$int': 1.0}

$neg

Negates the value (-val) before comparing.

{'$neg': -1.2}

$str

Casts the value to a string before comparing.

{'$str': '1.2'}

$avg

Calculates the arithmetic mean of an iterable before comparing.

{'$avg': 2.0}

$std

Calculates the population standard deviation of an iterable before comparing.

{'$std': 2.0}

$max

Finds the maximum value in an iterable before comparing.

{'$max': 4}

$mid

Extracts the middle element or character (index len//2) before comparing.

{'$mid': 4}

$min

Finds the minimum value in an iterable before comparing.

{'$min': 1}

$sum

Calculates the sum of an iterable before comparing.

{'$sum': 8}

$first

Extracts the first item or character before comparing.

{'$first': 1}

$flat

Flattens a nested iterable before comparing.

{'$flat': [1,2,2,3]}

$last

Extracts the last item or character before comparing.

{'$last': 3}

$len

Calculates the length of an array or string before comparing.

{'$len': 3}

$sort

Sorts the iterable values before comparing.

{'$sort': [1,2,3]}

$unique

Performs order-preserving deduplication on an iterable before comparing.

{'$unique': [2,3,1]}

$lower

Converts a string to lowercase before comparing.

{'$lower': 'alice'}

$upper

Converts a string to uppercase before comparing.

{'$upper': 'ALICE'}

$strip

Strips leading and trailing whitespaces from a string before comparing.

{'$strip': 'hi'}

Pythonic Query Examples

For developers who prefer a Pythonic and object-oriented syntax for filtering data (similar to the TinyDB experience), omni-json-db provides the Query object. You can elegantly use native Python operators (e.g., ==, >, &, |, ~) and chained methods to construct complex search conditions.

from omni_json_db import JDb, Query

# 1. Initialize database and add test data
jdb = JDb()
jdb += {
    'user_1': {'name': 'Alice', 'age': 30, 'email': 'alice@example.com', 'role': 'admin', 'tags': ['python', 'database']},
    'user_2': {'name': 'Bob', 'age': 25, 'role': 'developer', 'tags': ['javascript', 'web']},
    'user_3': {'name': 'Charlie', 'age': 35, 'role': 'developer', 'tags': ['python', 'linux', 'aws']},
    'user_4': {'name': 'Diana', 'age': 28, 'email': 'diana@test.com', 'role': 'designer', 'tags': ['ui', 'ux']}
}

# 2. Create a Query instance
User = Query()

# Basic Comparison: Find users older than 28
res = jdb.find(User.age > 28)
assert set(res) == {'user_1', 'user_3'}

# Logical Combinations (AND & OR): Find developers under 30 OR admins
res = jdb.find((User.role == 'developer') & (User.age < 30) | (User.role == 'admin'))
assert set(res) == {'user_1', 'user_2'}

# Array Query: Find users whose tags include 'python'
res = jdb.find(User.tags.has('python'))
assert set(res) == {'user_1', 'user_3'}

# Path Wildcard: Regex search across all fields recursively (Find email containing example.com)
res = jdb.find(User['**'].matches(r'.@example\.com'))
assert set(res) == {'user_1'}

# Advanced Filters: Find users who DO NOT have an 'email' field (~ is the NOT operator)
res = jdb.find(~User.exists('email'))
assert set(res) == {'user_2', 'user_3'}

# Lambda Test: Find users whose age is an even number
res = jdb.find(User.age.test(lambda age: age % 2 == 0), sort=User.name, reverse=True)
assert set(res) == {'user_1', 'user_4'}

# rename admin to Administrator
res = jdb.update_if(User.role == 'admin', {'role': 'Administrator'})
assert res == 1 and (User.role == 'Administrator') in jdb

# sorted by user name
res = jdb.show(sort=User.name, reverse=True)
assert res == jdb

# grouped by user role
res = jdb.show(group_by=User.role) # jdb.show(group_by='role')
assert 'developer' in res

res = jdb.show(group_by={'role':['name', 'age.$avg']})
assert 'developer' in res

Methods & Operators Reference

Syntax / Operator

Description

Example Usage

==, !=

Equals / Not equals

User.name != 'Bob'

>, >=, <, <=

Numeric comparison

User.age > 30, User.age < 30

&

Logical AND

(User.age > 20) & (User.role == 'admin')

|

Logical OR

(User.name == 'Alice') | (User.age < 30)

~

Logical NOT

~ User.exists('email')

.has(val)

Contains specific string or array element

User.tags.has('database')

.not_has(val)

Does not contain specific string or array element

User.name.not_has('ice')

.ihas(val)

Case-insensitive contains

User.name.ihas('alice')

.startswith(val)

String starts with prefix

User.city.startswith(('L', 'H'))

.endswith(val)

String ends with suffix

User.name.endswith('b')

.matches(pattern)

Regular expression search (equivalent to re.search)

User.name.matches(r'[bB]ob')

.fullmatch(pattern)

Regular expression full match (equivalent to re.fullmatch)

User.name.fullmatch(r'.lic.')

.one_of(col)

Value is within the specified collection

User.role.one_of(['admin', 'dev'])

.not_in(col)

Value is not within the specified collection

User.role.not_in(['admin', 'dev'])

.any_in(col)

Any element in the array is within the specified collection

User.role.any_in(['admin', 'ceo'])

.between(low, high)

Value or string is within the specified range

User.age.between(20, 30)

.size_of(size)

Array or string length matches

User.tags.size_of(2)

.exists(fields)

Checks if specified fields exist

User.exists('email')

.type_of(type)

Checks the data type

User.age.type_of(int)

.mod(div, rem)

Modulo condition (remainder is rem when divided by div)

User._date.mod(7, 5)

.near(target, tol)

Numeric value is near the target within tolerance tol

User._date.near(today, 1)

.test(func)

Passes a custom Lambda function for condition evaluation

User.age.test(lambda v: 40 >= v > 18)

.abs()

Takes the absolute value of a number before comparing.

Query().price.abs() == 3.14

.ceil()

Takes the ceiling (math.ceil) of a number before comparing.

Query().price.ceil() > 3

.floor()

Takes the floor (math.floor) of a number before comparing.

Query().price.floor() <= 2

.round()

Round a number before comparing.

Query().price.round() == 2

.float()

Casts the value to a float before comparing.

Query().price.float() == 2.1

.int()

Casts the value to a integer before comparing.

Query().price.int() != 1`

.neg()

Negates the value (-val) before comparing.

Query().price.neg() == -1.2

.str()

Casts the value to a string before comparing.

Query().price.str() == '1.2'

.avg()

Calculates the arithmetic mean of an iterable before comparing.

Query().prices.avg() == 2.5`

.std()

Calculates the population standard deviation of an iterable before comparing.

Query().prices.std() >= 2.0

.max()

Finds the maximum value in an iterable before comparing.

Query().prices.max() == 4

.mid()

Extracts the middle element or character (index len//2) before comparing.

Query().prices.mid() == 4

.min()

Finds the minimum value in an iterable before comparing.

Query().prices.min() == 1

.sum()

Calculates the sum of an iterable before comparing.

Query().prices.sum() == 8

.first()

Extracts the first item or character before comparing.

Query().prices.first() == 1

.flat()

Flattens a nested iterable before comparing.

Query().prices.flat().max() == 4

.last()

Extracts the last item or character before comparing.

Query().prices.last() == 3

.len()

Calculates the length of an array or string before comparing.

Query().prices.len() == 3

.sort()

Sorts the iterable values before comparing.

Query().prices.sort().mid() == 2

.unique()

Performs order-preserving deduplication on an iterable before comparing.

Query().prices.unique().first() == 1

.lower()

Converts a string to lowercase before comparing.

Query().name.lower() == 'alice'

.upper()

Converts a string to uppercase before comparing.

Query().name.upper() == 'ALICE'

.strip()

Strips leading and trailing whitespaces from a string before comparing.

Query().name.strip() == 'Hi'

field['field']

Accesses a specific field

User['addr'].city, User.addr.city

.field[0]

specific index of an array (supports negative index like User.tags[-1])

User.tags[1].has('db')

'*' / '**' / '?'

First-level wildcard / Recursive multi-level wildcard / Single-char wildcard path search

User['*'], User['**'], User['ci?y'], User['c*y']

._id / ._date

system reserved keys: access Document ID (Primary key) and Timestamp respectively

User._id, User._date

Record Flags

Every record carries a set of JKeyFlag bits in the key index, giving it file-system-like attributes. Set them with a chmod-style string: a bare letter or + sets a flag, - clears it, and anything you don’t name keeps its current value.

Flags come in three kinds. Stored flags are properties of the record and persist on disk. Derived flags are maintained by the engine and cannot be set by name. Transient flags describe a single call rather than a record — they are never written to disk, and passing one leaves the record’s stored flags untouched.

Stored — properties of the record

Letter

Flag

Effect

r

READ_ONLY

Writes and deletes are refused.

a

APPEND_ONLY

The value may only grow; shrinking, rewriting and deleting are refused.

p

NO_DELETE

Deletes are refused, but updates still go through — editable, never removed.

h

HIDDEN

Skipped by find() / show(); every other API still sees it.

c

NO_CACHE

Never enters the LRU read cache — keeps big blobs from evicting hot records.

v

NO_REVERT

No previous version is kept, so a hot counter never grows the index.

0

USER0

Free for your application (0–3); carries no engine behaviour.

READ_ONLY and APPEND_ONLY block deleting as well as writing, so NO_DELETE is the only way to say this record may change, but must never disappear — the retention guarantee a config row or an account record wants.

Derived — maintained by the engine

Letter

Flag

Effect

e

EXPIRE

The record has a TTL. Set it with ttl= (days, or a timedelta), never by name.

g

GROUP

The record holds a group. Never settable.

l

LINK

A symbolic link; set it with set_link().

Transient — properties of a single call

Letter

Flag

Effect

u

UNLOCK

Waives the read-only / append-only / no-delete locks for this one call, like sudo.

w

EXCL

Refuse the write if the key already exists — create-only.

m

MUST_EXIST

Refuse the write if the key does not exist — update-only. The mirror of EXCL.

n

NO_FOLLOW

Read a LINK as its own target path instead of following it; on write, retarget it.

y

NO_ATIME

Write without moving the modified date, so an existing TTL is not renewed.

q

QUIET

Fail quietly: a lock refuses by return value instead of raising JLockError.

Transient flags are passed to the operation itself, not to set_key_flags(). They are consumed rather than stored, so jdb.set(k, v, key_flags='w+r') refuses an existing key and stores READ_ONLY on a new one. Naming a transient flag in a find() predicate raises JValueError rather than silently matching every record.

Refusals are loud, typos are louder

Naming a locked record raises JLockError (a JKeyError, so a KeyError): jdb['config'] = x on a READ_ONLY row no longer looks like a successful write. Two flags opt out, per call: 'u' (UNLOCK) does it anyway, 'q' (QUIET) refuses the old way — set() returns None, remove() returns {}, f_write() returns False and f_delete() returns LOCKED. Selector-driven sweeps (jdb[re.compile(...)] = v, update_if(), jdb -= jdb, the CSV/INI/SQL importers, insert() and the other batch writers) keep skipping what they may not touch — they are best-effort by design; only an explicitly named key refuses loudly. EXCL ('w') and MUST_EXIST ('m') also stay quiet: those are conditions the caller asked for, not a record saying no, which is exactly what setdefault() is built on.

A malformed key_flags is a different thing and always raises JValueError, 'q' or not: an unknown letter (key_flags='f') and an undefined bit (key_flags=0x200, which KEY_FLAG_MASK was wide enough to write to disk) used to be dropped without a word.

from omni_json_db import JDb, JKeyFlag, JLockError

jdb = JDb()
jdb['config']        = {'theme': 'dark'}
jdb['audit/2026-08'] = ['created']

# read-only: writes are refused, and say so
jdb.set_key_flags('config', 'r')
try:
    jdb['config'] = {'theme': 'light'}
except JLockError as e:
    print(e)                            # write[config] refused: record is locked ...

print(jdb.set('config', {'theme': 'light'}, key_flags='q'))
                                        # Output: None   (refused quietly)
print(jdb['config'])                    # Output: {'theme': 'dark'}

# append-only + no history: an audit log that can only grow
jdb.set_key_flags('audit/2026-08', 'av')
jdb['audit/2026-08'] = ['created', 'updated']   # extension -> allowed
for bad in (['wiped'], 'not a list'):           # not an extension -> refused
    try:
        jdb['audit/2026-08'] = bad
    except JLockError:
        pass

print(jdb.remove('audit/2026-08', key_flags='q'))   # delete refused too
                                        # Output: {}
print(jdb['audit/2026-08'])             # Output: ['created', 'updated']

# inspect, then unlock again ('-r' clears only READ_ONLY).
# every flag API returns {key: (flags, ttl)}
flags, ttl = jdb.get_key_flags('audit/2026-08')['audit/2026-08']
print(str(JKeyFlag(flags)), ttl)        # Output: __a_v______________ 0
jdb.set_key_flags('config', '-r')
jdb['config'] = {'theme': 'light'}
print(jdb['config'])                    # Output: {'theme': 'light'}

# hidden: kept out of query results, but not a secret — items() still sees it
jdb['_scratch'] = 'internal'
jdb.set_key_flags('_scratch', 'h')
print(list(jdb.find()))                 # Output: ['config', 'audit/2026-08']
print(jdb['_scratch'])                  # Output: internal
print(list(jdb.find(with_hidden=True))) # Output: ['config', 'audit/2026-08', '_scratch']

# tag records for your own use, and combine flags freely
jdb.set_key_flags('config', '+0')       # USER0
jdb.keys.set_flags('config', '+c-0')    # add NO_CACHE, drop USER0

# no-delete: the record stays editable, but nothing can remove it
jdb['account/42'] = {'plan': 'pro'}
jdb.set_key_flags('account/42', '+p')
jdb['account/42'] = {'plan': 'team'}    # update -> allowed
print(jdb['account/42'])                # Output: {'plan': 'team'}
print(jdb.remove('account/42', key_flags='q'))
                                        # Output: {}
print(jdb.remove('account/42', key_flags='u'))
                                        # Output: {'account/42': {'plan': 'team'}}

# create-only ('w') and update-only ('m'), decided under the write handle
# rather than by a test-then-write, so neither races another writer
print(jdb.set('config', {'theme': 'dark'}, key_flags='w'))   # exists -> refused
                                        # Output: None
print(jdb.set('session/abc', ['new'], key_flags='w'))
                                        # Output: ['new']
print(jdb.set('session/xyz', ['nope'], key_flags='m'))       # absent -> refused
                                        # Output: None
print(jdb.set('session/abc', ['touched'], key_flags='m'))
                                        # Output: ['touched']

# no-follow: read the link itself instead of what it points at
jdb.set_link('latest', 'config')
print(jdb['latest'])                    # Output: {'theme': 'light'}
print(jdb.get('latest', key_flags='n')) # Output: config

Expiring Records (TTL)

A record can be given a lifetime with ttl=, counted from the moment it was last modified. Three argument types are accepted, and one rule decides the unit:

ttl=

Meaning

int

Whole days — ttl=7 is a week. Clamped to MAX_TTL_DAYS (511).

float

Days, so a fraction falls through to minutes — 0.25 is 6 hours.

timedelta

Taken as it stands — timedelta(minutes=30), timedelta(hours=6).

Anything under a day rounds up to whole minutes; a day or more rounds up to whole days. So timedelta(hours=23) is 1380 minutes, timedelta(hours=24) is 1 day and timedelta(hours=40) is 2 days. A positive lifetime never rounds down to nothing: timedelta(seconds=1) is one minute, not “no TTL”.

ttl reads back as days remaining in the same form ttl= accepts — an int for a whole-day TTL, a float below 1.0 for a minute one — so a value taken from get_key_flags() can be written straight back. Multiply by MIN_OF_DAY (1440) to read a sub-day TTL in minutes.

A TTL never expires early. Both units are stored coarsely — to the day or to the minute — and a record stays readable through the end of its last day or minute. A record written today with ttl=1 is still there today and gone tomorrow; a one-minute TTL lives between 61 and 120 seconds.

EXPIRE is derived: it is set whenever the record has a TTL and cleared when it does not. Naming 'e' yourself does nothing — use ttl=.

Because the countdown runs from the modified date, an ordinary write renews it. Pass key_flags='y' (NO_ATIME) to update a record without extending its life — the difference between touching a session and prolonging it.

import datetime as dt
from omni_json_db import JDb, JKeyFlag, MIN_OF_DAY

jdb = JDb()
jdb['session/abc'] = {'user': 'ana'}

# give an existing record a lifetime, in days
print(jdb.set_key_flags('session/abc', ttl=7))
                                        # Output: {'session/abc': (128, 7)}
flags, ttl = jdb.get_key_flags('session/abc')['session/abc']
print(str(JKeyFlag(flags)), ttl)        # Output: _______e___________ 7

# a TTL combines with the other flags, and survives a rewrite
print(jdb.set_key_flags('session/abc', '+r'))
                                        # Output: {'session/abc': (129, 7)}

# ttl=0 removes it; ttl=None (the default) leaves it untouched
print(jdb.set_key_flags('session/abc', ttl=0))
                                        # Output: {'session/abc': (1, 0)}

# sub-day lifetimes: a timedelta, or a fraction of a day
jdb['cart/9'] = ['socks']
print(jdb.set_key_flags('cart/9', ttl=dt.timedelta(minutes=30)))
                                        # Output: {'cart/9': (128, 0.020833333333333332)}
ttl = jdb.get_key_flags('cart/9')['cart/9'][1]
print(round(ttl * MIN_OF_DAY), 'minutes')
                                        # Output: 30 minutes

# 0.25 of a day is 6 hours; a day or more becomes whole days
print(jdb.set_key_flags('cart/9', ttl=0.25))
                                        # Output: {'cart/9': (128, 0.25)}
print(jdb.set_key_flags('cart/9', ttl=1.5))
                                        # Output: {'cart/9': (128, 2)}

# or set it as the record is written
with jdb.open() as fp:
    jdb.f_write(fp, 'otp/123', '558021', ttl=1)
    jdb.f_write(fp, 'otp/456', '331902', ttl=dt.timedelta(seconds=90))

print(jdb.get_key_flags('otp/123'))     # Output: {'otp/123': (128, 1)}
print(round(jdb.keys['otp/456'][8] * MIN_OF_DAY), 'minutes')
                                        # Output: 2 minutes

# jdb.set() takes it too, so a one-liner cache entry needs no open()
jdb.set('tmp/quote', 41.2, ttl=dt.timedelta(minutes=10))

An expired record is gone from every read surface — jdb[key], in, items(), find() and show() all skip it — and the row is reclaimed lazily on the next write that needs space, so expiry costs no background thread.

Faster Open

Opening a database with key_limit='no' builds the key table by scanning the whole KEY file. Two independent options take that cost off the critical path; both are off by default.

Key-table snapshot — index_cache=True

Keeps a disposable copy of the table beside the database — <KEY>.KIX on disk, a shared buffer for JMemFiles, a server-side sidecar over JNetFiles — so the next handle fast-forwards to the snapshot and scans only the rows written since.

Warm-up — warm_up=True

Builds the table on a background thread the moment the instance is created, so application start-up overlaps with the load instead of the first open() paying for all of it.

from omni_json_db import JDb

jdb = JDb('data/app.KEY', key_limit='no', index_cache=True, warm_up=True)
with jdb.open() as fp:       # restored from the snapshot, and already warmed
   ...

# or drive both by hand
jdb = JDb('data/app.KEY', key_limit='no', index_cache=True)
th  = jdb.warm_up()          # the Thread, or None when there is nothing to warm
jdb.insert(rows)
jdb.save_index()             # take control of when the snapshot lands, e.g. after an import

Measured on 300,000 records (16-byte keys, key_limit='no'):

full scan

.KIX

cold open (median)

326 ms

80 ms (4.1x)

save_index()

—

19.5 ms

sidecar size

—

7.5 MB (25 B/key)

The snapshot is written on its own when a close leaves the sidecar far enough behind, and is skipped entirely below JIo.min_kix_rows (16,384 records), where a full scan is cheaper than maintaining one. A snapshot that does not match the KEY file — different layout or data_type, a database newer than the sidecar, a truncated, corrupted or foreign file — is rejected and the full scan runs, so it can never serve a stale table. clear(), an API change and resize_index_size() delete it outright.

Advanced

from omni_json_db import JDb
# Initialize the database in memory
# Key-Value is Json+MsgPack with no compression
jdb = JDb()

fruits = {'apple':'red', 'banana':'yellow', 'mango':'yellow', 'lemon':'yellow', 'tomato':'red'}

# insert records
with jdb.open() as fp:
   for fruit,color in fruits.items():
      jdb.f_write(fp, fruit, color)

assert jdb == fruits

# ... or append the whole group in one pass (see the Bulk Insert section)
with jdb.open(read_only=False) as fp:
   jdb.f_extend(fp, dict(fruits))

# modify records
with jdb.open() as fp:
   for fruit in fruits:
      color = jdb.f_read(fp, fruit)
      jdb.f_write(fp, fruit, color.upper())

assert jdb != fruits
assert set(jdb) == set(fruits)

# unmodify records
with jdb.open() as fp:
   for fruit in fruits:
      jdb.f_unwrite(fp, fruit)

assert jdb == fruits

# remove records
with jdb.open() as fp:
   for fruit in fruits:
      jdb.f_delete(fp, fruit)

assert len(jdb) == 0

# unremove records
with jdb.open() as fp:
   for fruit in fruits:
      jdb.f_undelete(fp, fruit)

assert jdb == fruits

#---------------------------------------
with jdb.open() as fp:
   key_table = jdb.key_table

   # replace
   for fruit in key_table:
      color = jdb.f_read(fp, fruit)
      jdb.f_write(fp, fruit, color.upper())

   # unmodify
   for fruit in key_table:
      jdb.f_unwrite(fp, fruit)

   # remove
   for fruit in fruits:
      jdb.f_delete(fp, fruit)

   # unremove
   for fruit in fruits:
      jdb.f_undelete(fp, fruit)

assert jdb == fruits

#---------------------------------------
# replace all
jdb[:] = lambda k,v: v.upper()

# unmodify all
jdb ^= jdb

# remove all
jdb -= jdb

# unremove all
jdb ^= fruits

assert jdb == fruits

📝 Specifications

Supported Data Formats

Configure data_type during initialization:

  • J+J or S+J: JSON/MsgPack Key + JSON Value

  • J+S or S+S: JSON/MsgPack Key + MsgPack Value (default)

  • J+M or S+M: JSON/MsgPack Key + Marshal Value

  • J+P or S+P: JSON/MsgPack Key + Pickle Value

  • J+Y or S+Y: JSON/MsgPack Key + YAML Value

  • J+U or S+U: JSON/MsgPack Key + User-Defined Value (pluggable codec, e.g. encryption)

  • U+U: User-Defined Key + User-Defined Value (pluggable codec for both)

Two legacy layouts are still accepted for reading databases written by early releases: L+J (split Key + JSON Value) and M+M (Marshal Key + Marshal Value). Neither is a good choice for a new database.

Data size = 70,840,580 bytes (MB = 1,000,000 bytes, no zip)

data_type

size

ratio

read

write

Pros

Cons

J+J or S+J

70,840,580

1.00

75.3MB/s

358.0MB/s

  • fastest write

  • faster read

  • readable

J+S or S+S

47,616,008

1.48

77.4MB/s

354.2MB/s

  • smallest size

  • faster read

  • faster write

  • no tuple [a]

  • unreadable

J+M or S+M

72,430,958

0.97

81.4MB/s

177.1MB/s

  • all type [d]

  • fastest read

  • bigger size

  • unreadable

  • security issue

J+P or S+P

70,207,207

1.01

64.9MB/s

22.8MB/s

  • slower read

  • slower write

  • unreadable

  • security issue

J+Y or S+Y

181,894,885

2.57

0.146MB/s

0.352MB/s

  • readable

  • biggest size

  • slowest read

  • slowest write

  • no tuple [a]

[a] (1,2,3,4)

convert to list

[b]

convert to hex string

[c]

only support string key

[d] (1,2)

all type = str, bytes, bool, int, float, list, tuple, set, dict, None

Pluggable User-Defined Codec (U)

J+U, S+U, and U+U let you plug in your own codec instead of using a built-in format. This is the extension point for encryption, custom compression, protobuf, or any other serialization you want full control over.

  • J+U / S+U: Key stays JSON/MsgPack (readable/small), only the Value goes through your codec.

  • U+U: Both the Key row and the Value go through your own codec.

There are two ways to register a codec:

  1. Process-wide default — register once, every JDb opened with a U data_type uses it.

from marshal import loads as marshal_loads, dumps as marshal_dumps
from omni_json_db import JDb, register_user_val_codec

# --- Process-wide default: encrypt every Value with Fernet ---
from cryptography.fernet import Fernet
fernet = Fernet(Fernet.generate_key())

register_user_val_codec(
    dumps=lambda data: fernet.encrypt(marshal_dumps(data)),
    loads=lambda raw: marshal_loads(fernet.decrypt(raw)),
)

# Key=JSON (readable), Value=encrypted
jdb = JDb("secure.jdb", data_type="J+U")
jdb["alice"] = {"ssn": "123-45-6789", "balance": 42.5}

# Re-open and decrypt transparently
jdb2 = JDb("secure.jdb", data_type="J+U")
assert jdb2["alice"] == {"ssn": "123-45-6789", "balance": 42.5}
  1. Per-instance — pass val_codec= (and optionally key_codec=) to JDb() so different instances (e.g. different tenants) can use different codecs/keys at the same time.

from marshal import loads as marshal_loads, dumps as marshal_dumps
from omni_json_db import JDb, JIoVAL_U
from cryptography.fernet import Fernet

# --- Per-instance codec: each tenant gets its own encryption key ---
def make_codec(fernet):
    codec = JIoVAL_U()
    codec.register(
        dumps=lambda data: fernet.encrypt(marshal_dumps(data)),
        loads=lambda raw: marshal_loads(fernet.decrypt(raw)),
    )
    return codec

key_a = Fernet.generate_key()
key_b = Fernet.generate_key()
tenant_a = JDb("tenant_a.jdb", data_type="J+U", val_codec=make_codec(Fernet(key_a)))
tenant_b = JDb("tenant_b.jdb", data_type="S+U", val_codec=make_codec(Fernet(key_b)))

Supported Zip Formats

Configure zip_type during initialization:

  • no: no compression for Value (default)

  • gz: Gzip (mode=1) compression for Value

  • bz: Bzip2 (mode=9) compression for Value

  • xz: LZMA compression for Value

  • zs: Zstandard (mode=22) compression for Value (better than xz)

  • br: Brotli (mode=6) compression for Value (better than gz)

  • z1: Zstandard (mode=6) compression for Value (better than gz)

  • z2: Zstandard (mode=11) compression for Value (better than gz)

  • lz: LZ4 (mode=0) compression for Value

Data size = 70,840,580 (MB = 1,000,000B)

zip_type

size

ratio

read

write

Pros

Cons

no

70,840,580

1.00

75.3MB/s

358.0MB/s

  • fastest speed

  • biggest size

gz

16,915,844

4.18

65.5MB/s

5.1MB/s

  • built-in

  • slower zip

bz

11,394,042

6.21

26.4MB/s

10.8MB/s

  • better ratio

  • built-in

  • slowest unzip

  • slower unzip

xz

11,340,548

6.24

54.9MB/s

2.3MB/s

  • better ratio

  • built-in

  • slower zip

  • slower unzip

zs

11,119,665

6.37

73.0MB/s

1.7MB/s

  • best ratio

  • faster unzip

  • better xz

  • slowest zip

br

13,700,696

5.17

65.8MB/s

25.3MB/s

  • better gz

z1

14,738,859

4.80

73.6MB/s

70.8MB/s

  • faster zip

  • faster unzip

  • better gz

z2

13,799,407

5.13

72.7MB/s

23.6MB/s

  • faster unzip

  • better gz

lz

26,226,039

2.70

75.6MB/s

202.4MB/s

  • fastest zip

  • fastest unzip

  • worst ratio

Supported Key Table Formats

Configure key_limit during initialization:

  • no: dict for key_table (default)

  • bt: BTree for key_table (save 44.3% vs dict)

  • l0 - l5: LiteKeyTable modes (save 60-75% vs dict)

  • +ve: PartialKeyTable — an integer caps how many keys stay resident, the rest are read from the KEY file on demand

no and bt load the whole table on open, which is what index_cache and warm_up are for (see Faster Open). l0–l5 and +ve load lazily and need neither.

Table size = 3,241,854 keys

key_limit

memory

key search

HIT > get()

MISS > get()

no

519MB

48.59Mo/s

29.28Mo/s

18.3Mo/s

bt

289MB

3.46Mo/s

3.07Mo/s

8.04Mo/s

l3

85MB

2.01Mo/s

2.01Mo/s

1.59Mo/s

📊 Benchmarking

Testing

>>> from omni_json_db import JDb
>>> size = 1_000_000
>>> jdb = JDb(data_type='J+J')
>>> data = {f'key{k}':k for k in range(size)}

>>> # Benchmarking operations
>>> jdb += data        # insert
>>> jdb[:]             # get_all
>>> jdb -= data        # remove
>>> jdb ^= data        # revert=unremove
>>> jdb[data] = -1     # replace
>>> jdb ^= data        # revert=unmodify
>>> print(jdb == data) # Output: True

Results

size

insert

get_all

remove

unremove

replace

unmodify

1

132 μs

89 μs

111 μs

96 μs

91 μs

83 μs

10

136 μs

93 μs

142 μs

145 μs

183 μs

177 μs

100

442 μs

319 μs

594 μs

680 μs

876 μs

976 μs

1K

3.37 ms

2.71 ms

5.24 ms

5.9 ms

7.61 ms

9.12 ms

10K

32.2 ms

26 ms

54.3 ms

55.8 ms

77.5 ms

91.1 ms

100K

358 ms

262 ms

626 ms

583 ms

774 ms

930 ms

1M

3.87 s

2.78 s

7 s

6.09 s

8.15 s

9.83 s

👥 Contributing

Contributions to omni-json-db are highly welcome! Whether you are reporting bugs, proposing new features, or improving documentation:

  1. Check the existing issues for open tasks or start a discussion.

  2. Fork the repository and create a new branch.

  3. Include tests for any new features or bug fixes.

  4. Open a Pull Request and reach out to the maintainers for review.

📄 License

omni-json-db is released under the terms of the LICENSE file.

Metadata

Release files for omni-json-db 3.0.10

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for omni-json-db 3.0.10
File Size Uploaded
omni_json_db-3.0.10.tar.gz 341.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for omni-json-db 3.0.10
File Interpreter ABI Platform
omni_json_db-3.0.10-py3-none-any.whl Python 3 none any Details

Total release size: 631.0 kB

Release files / omni_json_db-3.0.10.tar.gz

Download URL omni_json_db-3.0.10.tar.gz
Size 341.3 kB
Tags Source
SHA-256 checksum
How to use checksums
37b6befcafe0b9f466339a22a1075f4ef92ab07c05c4c4ff434c4d8e51d0d120
BLAKE2b-256 checksum
How to use checksums
4cd7783871aea119e41b4d1ee4bafa8e930f9b44325326995f798f6296894090
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.13

Release files / omni_json_db-3.0.10-py3-none-any.whl

Download URL omni_json_db-3.0.10-py3-none-any.whl
Size 289.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ab1269b4ceebe0fefe60e5bf92983078c41df1b999fcf95674f8b2782d41094f
BLAKE2b-256 checksum
How to use checksums
0294cfb218351e06f6aaeaf81dd7dcaeea2d71d6c7af6c56049fb987b24e2d12
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.13
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page