This project takes certain... liberties with database paradigms. Born from the existential void of a southern hemisphere
summer (December 2024โJanuary 2025, when ennui struck like the heatwave), Umbra cosplays as PostgreSQL with its query
planner (proper cost-based optimisation using expression trees, darlingโweโre civilised), while harbouring SQLiteโs
scandalous little secrets under the bonnet (ahem RowId, you disreputable little implementation detail).
The bat is non-negotiable.
Let's be clear: this isn't a replacement for anything, unless you're replacing sanity with chaos, which caseโwelcome. The focus here is the learning process, read as "the screams of debugging hand-rolled date parsing at 3AM."
cargo run -- file.db 8000file.db: The path to your Umbra database file (will be created if it doesn't exist).
8000: The TCP port the server will listen on.
cargo run --package usql -- 8000Connects to the Umbra server running on port 8000.
usqlis your shadowy shell into the Umbra world. It understands SQL and the void.
The beating heart of databasesโand poor life decisions.
Umbra supports both signed and unsigned integer types, as well as their attention-seeking cousins: the serial pseudo-types. These SERIAL types arenโt realโthey're syntactic sugar that auto-magically generate sequences behind the scenes (just like PostgreSQL, but with fewer emotional boundaries).
| Type | Range | Notes |
|---|---|---|
SMALLINT |
ยฑ2ยนโต | Petite regrets |
INTEGER |
ยฑ2ยณยน | Standard regret capacity |
BIGINT |
ยฑ2โถยณ | When regular regrets aren't enough |
SMALLINT UNSIGNED |
0 โ 2ยนโถโ1 | For when you're cautiously hopeful |
INTEGER UNSIGNED |
0 โ 2ยณยฒโ1 | Delusional optimism |
BIGINT UNSIGNED |
0 โ 2โถโดโ1 | Sheer madness |
SMALLSERIAL |
1 โ 2ยนโตโ1 | Small but permanent mistakes |
SERIAL |
1 โ 2ยณยนโ1 | Commitment issues |
BIGSERIAL |
1 โ 2โถยณโ1 | Lifelong consequences |
Note
Unsigned Integers
PostgreSQL demands check constraints, SQLite shrugs, but Umbra embraces MySQL's blunt syntax for that.
Important
SERIAL Types are forgetful
Much like PostgreSQL, Umbra's serial values never look back.
Once generatedโeven if your transaction failsโtheyโre gone.
CREATE TABLE cursed_items (
item_id SERIAL PRIMARY KEY,
name VARCHAR(255),
darkness_level SMALLINT UNSIGNED,
soul_count BIGINT UNSIGNED
);When integers just wonโt cut it and you need approximate truths, floating-point types step inโlike unreliable narrators in a numerical novel. For precision, we offer NUMERIC.
| Type | Precision | Notes |
|---|---|---|
REAL |
~7 decimal digits | Single-precision daydreams. Fast, vague, and prone to making things up. |
DOUBLE PRECISION |
~15 decimal digits | Double the bits, double the confidence. For those who still donโt trust REAL. |
NUMERIC(p, s) |
Arbitrary | Base-1000 implementation. For when you need to count atoms or debt. |
Warning
Floating-Point Lies
They're fast. They're fun. They're wrong. Read more about that here.
If you're just plotting wiggly shadows over time, REAL is your friend.
CREATE TABLE entropy_watch (
event_id SERIAL PRIMARY KEY,
time TIMESTAMP,
shadow_density REAL,
void_pressure NUMERIC(10, 4)
);Tip
Casting Happens
Umbra will happily let you mix floats and integers. But you might want to think twice before comparing them for equality. That way lies madness.
Also, insert will NOT coerce types, it'll strictly verify the types based on the defined schema.
SELECT time FROM entropy_watch WHERE shadow_density > 69; -- here we implicitly cast 69 to a float, so you can query without hustleWhen numbers aren't unique enough and strings are too sincere, you reach for the UUID. Specifically: version 4, because determinism is for spreadsheets.
| Type | Description | Notes |
|---|---|---|
UUID |
Universally Unique Identifier (v4 only) | Random, stateless, and perfect for plausible deniability. |
CREATE TABLE shadow_agents (
agent_id UUID PRIMARY KEY,
codename VARCHAR(255),
clearance_level SMALLINT
);Because shadows deserve structure, too. Umbra's JSONB implementation provides flexible syntax for
JavaScript-like object literals, stores them in a compact binary format, and supports dot notation and
path expression for nested access.
- Keys can skip quotes:
{name: 'Umbra', active: true} - Line comments
//and block comments/* ... */are ignored while parsing - Missing paths quietly become
NULLinstead of raising errors
CREATE TABLE users (
id SERIAL PRIMARY KEY,
metadata JSONB
);
INSERT INTO users (metadata) VALUES
({profile: {lang: 'en', hobbies: ['lurking', 'flying']}, active: true});
SELECT metadata.profile.lang FROM users; -- dot access
SELECT metadata.hobbies[*] FROM users; -- array expansion
SELECT metadata.hobbies[-1] FROM users; -- last element via negative index
SELECT metadata.non_existent_field FROM users; -- NULLTip
You can filter on JSONB directly (WHERE metadata.active = true) or access nested profile data (like metadata.profile.lang) without extra casting.
-
VARCHAR -
BOOLEAN -
DECIMAL -
TEXT(the loquacious one) - Temporal types (because time flies when... I ran out of jokes)
-
DATE -
TIME -
TIMESTAMP(precision: "ish") -
INTERVAL
-
-
PRIMARY KEY- Auto-incrementing (via
SERIAL, no manual gear-shifting required)
- Auto-incrementing (via
-
UNIQUE(because duplicates are tacky) -
NULLABLE(explicit nullability, because I believe in clarity) -
FOREIGN KEY(relationships are hard) -
CHECK
Important
Nullability Philosophy
Unlike SQL standard where columns are nullable by default, requiring NOT NULL to prevent it.
Umbra follows Rust philosophy: columns are not null by default, unless explicit marked as NULLABLE.
This makes null handling intentional and not condescending.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255), -- non-null by default
email VARCHAR(255) NULLABLE, -- explicitly nullable
);-
CREATE TABLE(where hope begins) -
DROP TABLE(where hope ends) -
ALTER TABLE
-
WHERE(basic filtering, no existential crises) -
ORDER BY(implemented after herding literal bats) -
BETWEEN(for when life exists within bounds) -
LIMIT/OFFSET(self-restraint is here) - Column aliases (SELECT tomb AS grave - identity is fluid)
- Table aliases (
FROM crypts AS c- naming things is hard) -
JOIN(relationships require couples therapy) -
GROUP BY(aggregation is a social construct) -
HAVING
-
BEGIN -
COMMIT -
ROLLBACK
- Unique indexes (exclusivity is key)
- Non-unique indexes (for the masses)
- Partial indexes (discrimination coming soon)
-
EXPLAIN(peeking behind the curtain)
Now available: numerical seances. Umbra can perform ritualistic summoning of truths across rows using standard SQL aggregate functions. Useful for divining patterns, spotting anomalies, or just counting how many regrets youโve inserted into a table.
| Function | Description | Example |
|---|---|---|
COUNT(*) |
How many? (all of them) | SELECT COUNT(*) FROM curses; |
SUM(expr) |
Add them all up (tally your sins) | SELECT SUM(darkness_level) FROM cursed_items; |
AVG(expr) |
Arithmetic mean (because median is too mainstream) | SELECT AVG(void_pressure) FROM entropy_watch; |
MIN(expr) |
The lowest value (bottom of the abyss) | SELECT MIN(shadow_density) FROM entropy_watch; |
MAX(expr) |
The highest value (loftiest darkness) | SELECT MAX(soul_count) FROM cursed_items; |
For when you need to quantify the abyss. Basic arithmetic operations (+, -, *, /) work as expected, but these functions handle the unnatural calculations.
| Function | Description | Example |
|---|---|---|
ABS(x) |
Absolute value (distance from zero) | SELECT ABS(-666) โ 666 |
SQRT(x) |
Square root (measure of diagonal despair) | SELECT SQRT(2) โ 1.414... |
POWER(x, y) |
Exponentiation (x raised to yth torment) | SELECT POWER(2, 10) โ 1024 |
TRUNC(x) |
Amputate decimals (integer-only suffering) | SELECT TRUNC(3.14159) โ 3 |
TRUNC(x, n) |
Precision mutilation (n decimal digits) | SELECT TRUNC(3.14159, 2) โ 3.14 |
SIGN(x) |
Returns -1, 0, or 1 (the algebra of alignment) | SELECT SIGN(-13) โ -1 |
Warning
Division by Zero
Attempting to divide by zero will summon an errorโbecause some voids should remain unexplored.
Words are powerful. Here theyโre dangerous.
| Function | Description | Example |
|---|---|---|
LIKE |
Pattern matching with % and _ |
WHERE name LIKE 'Umb%' |
CONCAT(a, b) |
Smashes values into one long string | CONCAT('bat', 'man') โ 'batman' |
SUBSTRING(str FROM x FOR y) |
Extracts a slice, PostgreSQL-style | SUBSTRING(name FROM 1 FOR 1) โ first letter of name |
POSITION(substr IN str) |
Finds the starting index of a substring | POSITION('e' IN 'shadow') โ 2 |
ASCII(char) |
Returns the ASCII code of a single character | ASCII('A') โ 65 |
Note
SUBSTRING Syntax
Umbra uses PostgreSQL-style SUBSTRING(string FROM start FOR length).
FROMandFORare both optional, but at least one must be present.- Positions are 1-based (because zero-based indexing is for the living).
-- give me just the first letter of each name
SELECT name, SUBSTRING(name FROM 1 FOR 1) FROM customers;
-- or everything after the third character
SELECT SUBSTRING(name FROM 4) FROM customers;
-- or only the first 3 characters, starting from the beginning
SELECT SUBSTRING(name FOR 3) FROM customers;Tools for introspection and survival.
| Function | Description |
|---|---|
COALESCE(v1, v2, ...) |
Returns the first non-null argument |
TYPEOF(value) |
Returns the data type of the value |
EXTRACT(field FROM source) |
Retrieves sub-fields such as year or date from date/time |
Umbra is licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later).
This project includes portions of code derived from third-party projects licensed under the MIT License.
Attributions and licence texts for those components are provided in the
THIRD_PARTY_NOTICES file.
- Documentation: Here be dragons (and possibly bats)
- Protocol: If you're implementing a client, or are just curious how the shadowy bits work under the hood, check the binary wire format here.
- Philosophy: "Compiling is victory. Running is a miracle."