HeliosDB Nano SQLite Compatibility - Frequently Asked Questions
What “SQLite compatibility” means here
Two separate things travel under this name. Keep them apart:
- SQL-dialect compatibility in the engine. A pre-parser translation layer accepts common SQLite-isms so SQLite-shaped SQL runs against Nano. This is part of the engine and works over the embedded API, the REPL and the wire protocols.
- The
heliosdb_sqlitePython adapter. Asqlite3-shaped Python module that drives Nano. It is a separate SDK component with its own, narrower scope — see The Python adapter below.
Neither of them means Nano is SQLite, and neither means Nano reads SQLite database files.
General
Q: Is HeliosDB Nano based on SQLite?
A: No. Nano is an independent engine written in Rust. It speaks PostgreSQL-flavoured SQL and includes a compatibility layer that rewrites common SQLite syntax before parsing. Sharing syntax is not sharing an implementation.
Q: Can Nano open my existing .sqlite / .db file?
A: No. Nano does not read the SQLite file format, and its store is a data directory (a RocksDB store plus sidecar directories), not a single file. Your data has to be exported from SQLite and reloaded.
The supported route today is a SQL-level export and reload — sqlite3 … .dump,
adjust the dialect, load through the REPL, then count rows on both sides
yourself. The Migration Guide has the full
procedure.
There is a tools/HELIOSDB_SQLITE_CONVERTER.py in the Nano engine repository,
but its load path into the engine is not wired up: it writes through a mock
connection and verifies row counts against that mock’s own tally, so it reports
success without having written a real store. It is a starting point for your own
tooling, not a turnkey converter.
Q: Which SQLite SQL constructs does the engine accept?
A: The translation layer handles these before the parser sees the statement:
| SQLite input | Rewritten to |
|---|---|
? positional placeholders | $1, $2, … (quote-aware; mixing ? and $N in one statement is an error) |
INSERT OR REPLACE INTO t (c1, c2) VALUES … | INSERT INTO t (c1, c2) VALUES … ON CONFLICT DO UPDATE SET c1 = EXCLUDED.c1, c2 = EXCLUDED.c2 |
INSERT OR IGNORE INTO … | INSERT INTO … ON CONFLICT DO NOTHING |
INTEGER PRIMARY KEY AUTOINCREMENT | BIGSERIAL PRIMARY KEY |
DATETIME('now') | CURRENT_TIMESTAMP |
sqlite_master is served as a system view, and PRAGMA … is intercepted at the
parser/protocol entry rather than executed as SQL.
Two caveats worth internalising:
INSERT OR REPLACEis rewritten to an upsert, which is not what SQLite’s REPLACE does. SQLite deletes the conflicting row and inserts a new one: unlisted columns revert to their defaults, the rowid changes, and delete triggers /ON DELETE CASCADEfire. The rewrittenON CONFLICT DO UPDATEupdates the row in place. If your code relies on the delete-then-insert behaviour, rewrite it explicitly.- The
OR REPLACErewrite requires a parenthesised column list. Without one, the statement falls back to a plainINSERTand a duplicate row raises a unique-violation error instead of replacing anything. Name your columns, or writeON CONFLICTyourself.
Q: What about type affinity?
A: SQLite is dynamically typed; Nano is statically typed like PostgreSQL. Values SQLite silently accepts are rejected:
-- SQLite accepts bothINSERT INTO users (id, name) VALUES ('1', 'Alice');INSERT INTO users (id, name) VALUES (2, 123);
-- Nano: the first coerces or errors depending on the column type,-- the second errors — 123 is not TEXTThis is a real porting task, not a footnote. Audit inserts that rely on SQLite coercing types for you. See the Migration Guide.
Q: Can I use multiple modes (REPL, server, embedded) at the same time?
A: Not against the same data. A data directory is owned by exactly one
running process. Starting a second process against a directory another process
already has open produces lock errors (database is locked) — that is the
documented, expected behaviour, not a bug to tune around.
What you can do:
- Run one process in whichever access mode you need — embedded library API, REPL, foreground server or background daemon.
- If several callers need the same data, run the server and have everyone connect to it as clients. That is the only way for more than one caller to share a dataset.
- Run a server and a REPL side by side as a development convenience — but that is two independent processes with two separate data directories. Writes in one are not visible in the other. They share nothing.
Switching modes is a stop-then-start handoff onto the same data directory, not a live cutover, and it requires changing the application’s access code (embedded calls become client calls). See Deployment Modes.
Q: Does Nano support concurrent writes?
A: It depends on the access mode, and the honest answer has two halves.
- Concurrent reads work everywhere — the storage engine is MVCC-based, so readers take snapshots and do not block each other or writers.
- Single-statement autocommit writes from multiple threads through one embedded handle work.
- Independent overlapping explicit transactions do not work on the embedded
API. An embedded handle has one process-global transaction slot, shared
across clones. Two callers each running
BEGIN … COMMITare interleaving into one transaction, and the firstCOMMITorROLLBACKends it for both. - Over a wire connection they do work. Each PostgreSQL or MySQL connection gets its own session and its own transaction state.
Full detail, including what to do instead: Concurrency and transactions.
Q: How do transactions work?
A: BEGIN / COMMIT / ROLLBACK behave as expected within a single
session. Isolation levels are READ COMMITTED, REPEATABLE READ (also spelled
SNAPSHOT), and SERIALIZABLE — with the caveat that SERIALIZABLE is served
as snapshot isolation and permits write skew. The
storage.serializable_policy setting controls whether that produces a warning
(default) or a hard error.
The constraint that actually bites when porting from SQLite is the one above: one explicit transaction per embedded process.
Q: How do I back up a Nano database?
A: Use the dump/restore path:
heliosdb-nano dump --data-dir ./my-data --output backup.heliodumpheliosdb-nano restore --input backup.heliodump --target ./restored --verifyThe same operations exist on the embedded API as dump_full and
restore_from_dump, with compression options and incremental (append) dumps.
Do not treat a file copy of a live data directory as a backup, and do not
use the Python adapter’s backup() for this — see below.
Branches and time travel are not backups either. Time travel depends on retained versions, and retention is bounded and configurable.
The Python adapter
Q: Where do I get heliosdb_sqlite?
A: It is not currently published to PyPI. pip install heliosdb-sqlite
resolves to nothing. Install it from the HeliosDB SDKs repository instead:
pip install "heliosdb-sqlite @ git+https://github.com/HeliosDatabase/HeliosDB-SDKs.git#subdirectory=sdks/python-sqlite"Its embedded mode runs the engine through heliosdb-nano-embedded, the
published PyO3 binding (import heliosdb_nano), which it installs on Linux
x86_64. heliosdb_sqlite adds the sqlite3 API on top: typed results,
cursor.lastrowid, transactions, adapters and converters.
pip install heliosdb-nano-embedded # the engine binding on its own; import heliosdb_nanoUse heliosdb_sqlite for code written against sqlite3, the binding directly
for its native query() / execute() API, or psycopg2 / asyncpg against a
Nano server.
Q: Does the adapter run in-process, like sqlite3 does?
A: Yes. Embedded mode runs the Nano engine inside the
Python process through heliosdb-nano-embedded and receives typed values
(int, float, str, bytes, None) with no text parsing. On platforms
without a binding wheel it falls back to a heliosdb-nano repl subprocess,
returns values as text and emits a RuntimeWarning. Daemon mode talks to a
running server over the PostgreSQL wire protocol.
Q: How are query parameters bound?
A: In embedded mode (with heliosdb-nano-embedded), as real parameters:
?, ?NNN, :name, @name and $name placeholders become $1..$n and the
values are passed to the engine separately from the SQL text.
In daemon mode and in the REPL fallback, values are bound client-side as escaped SQL literals (placeholders inside strings and comments are left alone). Validate untrusted input there as you would for any client-side binding.
Q: Which sqlite3 APIs are unsupported?
A:
| API | Behaviour |
|---|---|
create_function() | Raises NotSupportedError |
create_aggregate() | Raises NotSupportedError |
create_collation() | Raises NotSupportedError |
load_extension() | Raises NotSupportedError |
enable_load_extension() | Accepted, does nothing |
set_authorizer() | Accepted, does nothing |
set_progress_handler() | Accepted, does nothing |
iterdump() | Emits schema statements only — no row data |
backup() | Replays iterdump() into the target, so it copies schema only. Not a data backup |
The PEP 249 exception hierarchy is present (Error, DatabaseError,
OperationalError, ProgrammingError, IntegrityError, …), but note that
IntegrityError is defined and never raised — constraint violations surface as
DatabaseError. Do not write except sqlite3.IntegrityError and expect it to
catch anything.
Cursors, Row, row_factory, executemany, executescript, context-manager
commit/rollback, and lastrowid (via an automatic RETURNING rewrite) are
implemented.
Q: What connection options actually exist?
A: connect() takes the standard sqlite3 signature — database,
timeout, detect_types, isolation_level, check_same_thread, factory,
cached_statements, uri — plus these HeliosDB-specific keyword arguments:
mode ('embedded', 'daemon', 'hybrid'), data_dir, server_host,
server_port, user, password, dsn, lastrowid_disabled.
Options you may have seen in older documentation — enable_vector_search=,
enable_time_travel=, enable_branching=, encryption_key=,
heliosdb_mode=, enable_cache=, cache_size_mb= — do not exist. They are
silently swallowed by **kwargs and do nothing.
Likewise, these methods do not exist on a connection: start_server(),
merge_branch(), schedule_backup(), enable_monitoring(), get_metrics(),
enable_logging(), get_slow_queries(), get_connection_stats(),
enable_audit_log(), set_max_connections(), batch_insert(), and there is no
connection_pool module.
The HeliosDB-specific methods that do exist are execute_vector_search(),
create_branch(), and switch_to_server() (hybrid mode only).
switch_branch() exists but raises NotSupportedError — switch branches with
USE BRANCH in SQL, or open a new connection.
Q: Can I use it with SQLAlchemy or Django?
A: There is no HeliosDB SQLAlchemy dialect and no heliosdb_django
backend — those do not exist. For ORM use, run Nano as a server and use the
PostgreSQL driver and dialect your ORM already has:
# SQLAlchemy, against a Nano serverengine = create_engine("postgresql://user:pass@127.0.0.1:5432/heliosdb")# Django settings.pyDATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "HOST": "127.0.0.1", "PORT": 5432, }}This is the supported path for ORM workloads and does not depend on the unpublished adapter.
Security
Q: Is encryption on by default?
A: No. AES-256-GCM transparent encryption is compiled into the default build, but it is disabled at runtime by default and requires a configured key source (environment variable or key file; cloud KMS is planned). Seeing “AES-256-GCM” in a feature list does not mean your database is encrypted. HeliosDB Nano provides zero-knowledge encryption: keys are derived and data encrypted on the client, the server never sees plaintext keys. A FIPS build mode also exists.
Q: Is row-level security available?
A: It is implemented, but it is reachable only through the embedded API and the REPL. It is not enforced over the PostgreSQL wire protocol, the MySQL wire protocol, or REST in any release to date. Do not design a multi-tenant wire-facing application around it today.
Q: Are branches a security boundary?
A: No. Branching is storage-level copy-on-write isolation. It does not sandbox anything outside the database and is not an access-control mechanism.
Getting help
- GitHub Issues: HeliosDatabase/HeliosDB-Nano
- Discord: discord.gg/yTykuUrFXc