Developer Guide
This guide walks you through integrating GreatEmbeddo into a firmware project, from writing
your first .gedb schema to handling transactions and schema evolution.
Prerequisites
- A C99-compatible compiler (GCC, Clang, MSVC, IAR, Keil, or Green Hills)
- The
greatembeddoCLI tool installed on your development machine
Download the CLI
Writing a Schema
A .gedb file describes your tables, indexes, and named queries. Schema statements
come first; query definitions go at the end. The file is append-only — you can only add new
statements, never edit existing ones.
-- myproject.gedb
CREATE TABLE sensors (
id uint32 PRIMARY KEY NOT NULL,
node_id uint16 NOT NULL,
temp_c float NOT NULL DEFAULT 0.0 RANGE(-40.0, 125.0),
humidity float,
flags uint8 DEFAULT 0
) EXPECT 1000 REPLICAS 2;
CREATE TABLE events (
id uint64 PRIMARY KEY NOT NULL,
sensor_id uint32 NOT NULL REFERENCES sensors(id) ON DELETE CASCADE,
ts uint64 NOT NULL,
code uint16 NOT NULL
) EXPECT 50000;
CREATE INDEX idx_events_sensor ON events (sensor_id);
QUERY get_sensor
SELECT * FROM sensors WHERE id = @id uint32;
QUERY get_events_for_sensor
SELECT * FROM events WHERE sensor_id = @sid uint32 ORDER BY ts DESC LIMIT @n uint32;
Key rules:
- Every table needs exactly one
PRIMARY KEYcolumn (integral type) EXPECT nis a hint for storage estimation — it has no effect on correctnessREPLICAS nstores n redundant copies (1–5); default is 1- Query parameters use
@name typesyntax (e.g.@id uint32) - Never edit lines you have already committed to the lock file
Generating Code
Run the CLI to generate your C99 source files. You need to know your flash device's block size and total block count:
greatembeddo myproject.gedb \
--output src/generated/ \
--block-size 4096 \
--block-count 1024 \
--cache-size 65536
Check storage requirements before committing to a block layout:
greatembeddo myproject.gedb --stats --block-size 4096 --block-count 1024
Print the static RAM breakdown:
greatembeddo myproject.gedb --memory --block-size 4096 --block-count 1024
The three required options are --block-size, --block-count, and --cache-size.
The CLI also supports flags for metadata replication, writer-lock strategy, and CI integration.
Generated File Structure
For a schema with two tables, one index, and one named query, the generator produces:
gedb.h ← public API (include only this)
gedb_config.h ← generated layout/sizing constants (pulled in by gedb.h)
gedb_internal.h ← implementation-only declarations (used by the .c files)
gedb_records.c ← types, config, CRC, on-disk record format
gedb_block.c ← block cache and superblock ring
gedb_runtime.c ← storage engine
gedb_tables.c ← all tables' insert/get/update/delete/scan
gedb_indexes.c ← all secondary-index APIs
gedb_queries.c ← all named-query iterators
Add all .c files to your firmware build system. Include only gedb.h in your application code.
#include "gedb.h" /* pulls in everything */I/O Callbacks
GreatEmbeddo never calls the filesystem or any hardware directly. You provide three function pointers that it calls for all flash I/O. This keeps the generated code portable to any hardware abstraction layer.
/* Read len bytes from block at offset into buf */
typedef gedb_status_t (*gedb_read_block_fn)(uint64_t block_id, uint32_t offset,
void *buf, uint32_t len, void *ctx);
/* Erase a block (must happen before first write after a read) */
typedef gedb_status_t (*gedb_erase_block_fn)(uint64_t block_id, void *ctx);
/* Write len bytes into block at offset (block must be erased first) */
typedef gedb_status_t (*gedb_write_block_fn)(uint64_t block_id, uint32_t offset,
const void *data, uint32_t len, void *ctx);
/* Example implementations for a NOR flash driver */
static gedb_status_t my_read(uint64_t block_id, uint32_t off,
void *buf, uint32_t len, void *ctx) {
if (flash_read((uint32_t)block_id * FLASH_BLOCK_SIZE + off, buf, len) != 0)
return GEDB_ERR_IO;
return GEDB_OK;
}
static gedb_status_t my_erase(uint64_t block_id, void *ctx) {
if (flash_erase_sector((uint32_t)block_id) != 0)
return GEDB_ERR_IO;
return GEDB_OK;
}
static gedb_status_t my_write(uint64_t block_id, uint32_t off,
const void *data, uint32_t len, void *ctx) {
if (flash_program((uint32_t)block_id * FLASH_BLOCK_SIZE + off, data, len) != 0)
return GEDB_ERR_IO;
return GEDB_OK;
}Initialisation
Allocate gedb_db_t as a file-scope static or on the heap — its size is known at compile time but
typically exceeds 1 MiB (dominated by the embedded block cache), so it must not be placed on the stack.
Call gedb_init with your I/O callbacks. On blank flash, the database is
automatically formatted. On an already-formatted device, the schema checksum is verified.
static gedb_db_t g_db; /* file-scope static — do not declare gedb_db_t on the stack */
static void on_corruption(uint64_t block_id, uint8_t replica_idx, void *ctx) {
log_warn("GEDB: block %llu replica %u repaired from good replica", block_id, replica_idx);
}
gedb_status_t st = gedb_init(&g_db,
my_read, my_erase, my_write,
NULL, /* io_ctx */
on_corruption); /* optional but recommended */
if (st == GEDB_ERR_SCHEMA) {
/* Flash was formatted with a different schema version */
handle_schema_mismatch();
} else if (st != GEDB_OK) {
/* Unrecoverable I/O error */
halt_system(st);
}
gedb_init is not thread-safe. Call it once at boot before starting any tasks
that use the database. There is no gedb_close — the database is crash-safe
without a clean shutdown.
Transactions
GreatEmbeddo provides two transaction wrappers:
gedb_transaction_write for mutations and
gedb_transaction_read for read-only queries.
Both take a callback; return GEDB_OK to commit/complete,
or any other value to roll back.
gedb_transaction_write acquires an exclusive writer lock — the lock strategy
(--lock) controls whether concurrent callers block or receive GEDB_ERR_BUSY.
gedb_transaction_read is completely lock-free and provides snapshot isolation:
all reads within the callback see a consistent, frozen point-in-time view of the database,
unaffected by concurrent writes.
For the complete API signatures, example code, and isolation details, see Transaction API Reference and Concurrency & Isolation.
CRUD Operations
Each table gets strongly-typed insert, get, update, delete, upsert, and scan functions.
Write operations must be wrapped in gedb_transaction_write;
reads should be wrapped in gedb_transaction_read for snapshot isolation.
For the full function signatures and usage examples, see CRUD Operations and Scanning in the API Reference.
Named Queries
Named QUERY definitions compile into typed C99 iterator functions.
SELECT queries use an iterator; INSERT/UPDATE/DELETE/UPSERT queries are single calls.
/* SELECT query with ORDER BY — result via iterator */
gedb_query_get_events_for_sensor_iter_t it;
gedb_status_t st = gedb_query_get_events_for_sensor_open(db, &it, /*sid=*/42, /*n=*/10);
if (st != GEDB_OK) { /* handle error */ }
gedb_query_get_events_for_sensor_row_t row;
while ((st = gedb_query_get_events_for_sensor_next(db, &it, &row)) == GEDB_OK) {
printf("ts=%llu code=%u\n", row.ts, row.code);
}
if (st != GEDB_DONE) { /* handle error — st contains the error code */ }
The iterator type is stack-allocatable; its exact size is sizeof(gedb_query_..._iter_t),
known at compile time. ORDER BY results are materialised inside the iterator — no heap required.
Error Handling
Most functions return gedb_status_t. Always check the return value.
| Code | Meaning |
|---|---|
GEDB_OK | Success |
GEDB_ERR_NOT_FOUND | Row with the given primary key does not exist |
GEDB_ERR_DUPLICATE | Primary key or unique index violation |
GEDB_ERR_RANGE | Value outside RANGE constraint |
GEDB_ERR_FK_VIOLATION | Foreign key constraint violation (RESTRICT) |
GEDB_ERR_SCHEMA | On-disk schema version does not match generated code |
GEDB_ERR_IO | I/O callback returned an error |
GEDB_ERR_CORRUPT | All replicas of a block are unreadable |
GEDB_ERR_FULL | No free data blocks (unreserved pool exhausted) |
GEDB_ERR_ALREADY_INIT | gedb_init called on an already-initialised db |
GEDB_ERR_NOT_INIT | API called before gedb_init |
Auto-repair and proactive repair
Corrupted replicas detected on the lock-free read path are recorded in a small ring
buffer inside the database and automatically rewritten at the tail of the next
write transaction (commit or rollback) — no user code required. You can still call
gedb_repair_step during idle time for proactive scrubbing; each call
drains any pending auto-repair entries first, then does a bounded cursor sweep.
/* Optional: call from a low-priority idle task for proactive scrubbing */
gedb_repair_step(&g_db);Schema Evolution
Extend your schema by appending new statements to the .gedb file. Regenerate
and rebuild. The .gedb.lock file tracks the append-only checksum chain —
commit it to version control alongside your schema.
-- Original schema (lines 1–12) must not be changed
-- Append new statements below:
ALTER TABLE sensors ADD batch_id uint16 DEFAULT 0;
ALTER TABLE sensors EXPECT 2000;
ALTER TABLE sensors REPLICAS 2;
CREATE INDEX idx_sensors_batch ON sensors (batch_id);
QUERY get_batch
SELECT * FROM sensors WHERE batch_id = @bid uint16;
Without the Pro version, existing devices with an older schema version will
get GEDB_ERR_SCHEMA from gedb_init. This is intentional — the
generated code must match the on-disk schema exactly.
Automatic Migration (Pro)
The Pro version generates C99 migration functions that automatically
upgrade an older on-disk schema to the current version — row by row, in a single
gedb_init() call.
/* The Pro version also generates: */
gedb_migration.h / gedb_migration.c ← migration logic for gedb_init
gedb_migration_1_2.h / .c ← schema v1 → v2
gedb_migration_2_3.h / .c ← schema v2 → v3
/* gedb_init automatically applies migrations — no API change needed: */
gedb_status_t st = gedb_init(&g_db, ¶ms);
Migration safety guarantees:
- Migrations are generated only if the CLI can prove they cannot fail at runtime
- Narrowing type changes require an explicit
ON NARROW CLAMP|WRAP|NULLpolicy - Adding a unique index to an existing table requires
ON CONFLICT REMOVE_DUPLICATES|REMOVE_ALL_DUPLICATES - Each
v → v+1step runs in its own write transaction and commits atomically — a power failure mid-migration leaves the device on the last intermediate version it successfully committed, and the next boot resumes from there - Transient tables, columns, indexes, and FKs — those created in one step and dropped in a later step — are fully preserved in between, so intermediate data migrations can rely on them
- Reserved blocks ensure migration can always delete rows to resolve conflicts, even on a full database
- When a step adds, removes, or changes columns, both the old and new table data coexist in flash until the step commits — ensure devices have enough free space for approximately 2× the table’s block footprint before OTA-upgrading. The CLI warns when this may be tight.
Troubleshooting Migrations
Schema evolution is append-only, so mistakes in .gedb files need corrective action.
The right fix depends on when the problem is caught.
The CLI rejected my schema change
If the CLI reports a validation failure, your new lines are not locked yet — you can freely edit or remove them and re-run the CLI. No special flags are needed.
A migration fails at runtime on the device
Each v → v+1 step runs in its own write transaction. If a step fails, only that
step rolls back; previously-committed steps stay committed. The device remains on the last
successfully-committed intermediate schema version and the next boot resumes migration from
there. Old firmware can still boot devices on any intermediate version that firmware understands.
Fixing a locked mistake
The safest fix is to append corrective statements below the mistake. As a last
resort, you can bypass the checksum chain with --skip-lock-check.