Files
A bucket is a table, a file is a record in it, and everything the language already does works on files.
There is no object store beside this one. A bucket is declared, a file is written and read, and reading, ordering, relating and deleting work on files because a file is not a new shape:
DEFINE NAMESPACE prod;
USE NAMESPACE prod;
DEFINE DATABASE library;
USE DATABASE library;
DEFINE BUCKET media;
It is DEFINE BUCKET rather than a flag on DEFINE TABLE because what you may
do to one differs — a bucket is written through PUT and never by hand.
PUT media:'/logo.png' = 0x89504e47;
READ media:'/logo.png';
0x89504e47. Text is accepted too, because a file is very often text, and it
comes back as bytes, because that is what a file is:
PUT media:'/notes.txt' = 'ada wrote this';
READ media:'/notes.txt';
0x6164612077726f74652074686973. A file nobody wrote is nothing rather than a
failure:
READ media:'/missing.txt';
NONE.
The metadata is an ordinary record
Which is the whole point:
SELECT size FROM media:'/logo.png';
So listing a bucket is a query and not an API call:
SELECT * FROM media;
Two rows, and the chunks are not among them. The bytes live in a companion table nothing can name, so counting a bucket counts files:
SELECT count(*) AS held FROM media;
A record points at a file the way it points at any record, and a relation to one is an ordinary edge:
DEFINE COLLECTION users;
CREATE users:1 = { name: 'ada', avatar: media:'/logo.png' };
SELECT avatar FROM users:1 FETCH avatar;
DEFINE TABLE attached EDGE;
RELATE users:1->attached->media:'/logo.png';
SELECT * FROM users:1->attached->media;
SELECT * FROM media:'/logo.png'<-attached<-users;
Both directions, on a file, with no file-specific syntax anywhere. See Graphs.
What a bucket refuses
PUT media:1 = 0x00;
FileNeedsAPath — a file is named by a path.
PUT media:'/wrong' = 42;
FileIsNotBytes — a number is refused rather than encoded. Which encoding would
it have been?
DEFINE COLLECTION plain;
PUT plain:'/x' = 0x00;
NotABucket.
CREATE media:'/forged.png' = { size: 3 };
NotWrittenByHand. This is the one worth pausing on: the metadata is filled in
by the store, and a hand-written record would let size say one thing while the
bytes say another. Every later read of that field would be wrong, and nothing
would ever reconcile it — so the write is refused instead.
The largest file a bucket takes
DEFINE BUCKET avatars MAX 5242880;
A count of bytes, written out. 5MB is not a spelling this language has —
digits touching a letter are a duration whatever the letter is, so 5MB is a
duration with a unit nothing recognises and dies in the lexer.
The clause is optional and its absence means unbounded, which is what every bucket declared before it existed is.
The ceiling is checked against the file as it will be rather than against the bytes a statement carries, and that is the only placement that means anything: a ranged write splices into bytes already stored, so a file grows past the ceiling while no single write is anywhere near it. A limit checked against the statement would hold only against callers who were not going to exceed it anyway.
There is no clause narrowing a bucket by content type. The store has no
content type for a file — a file's record holds its size, its chunk count and
when it was written — so such a clause could only enforce a claim the caller made
about the caller's own bytes, which is the assertion NotWrittenByHand above
exists to refuse.
Part of a file
START and LIMIT mean over bytes what they mean over rows — skip this many,
take this many:
PUT media:'/range.bin' = 0x00010203040506070809;
READ media:'/range.bin' START 2 LIMIT 3;
0x020304. A range beginning past the end answers rather than failing:
READ media:'/range.bin' START 99 LIMIT 3;
A write at an offset replaces those bytes and leaves the rest:
PUT media:'/range.bin' START 4 = 0xffff;
READ media:'/range.bin';
0x00010203ffff06070809. It lands in one commit, the same as a whole-file
write, so there is no moment at which a reader sees half of it.
And a write that would leave a gap is refused rather than filled in:
PUT media:'/range.bin' START 500 = 0xff;
WriteWouldLeaveAHole. Padding with zeroes would invent bytes the caller never
supplied, and a file with a hole in it is a file whose size lies.
Replacing and removing
PUT media:'/notes.txt' = 'shorter';
READ media:'/notes.txt';
DELETE media:'/notes.txt';
READ media:'/notes.txt';
A write over a file replaces it rather than adding to it, and deleting one takes its bytes with it — the same rule that keeps an index entry from outliving its record.
Over HTTP, the same bucket is served at /files/…. See
The HTTP surface.