Skip to main content

Getting Started

This page takes you from an empty project to a database that holds data in two column families. At the end, you know how to open a database, write an atomic batch, read a key, iterate over a key range, and close the database safely. Then continue to the guides for each feature.

Requirements​

  • The .NET 10 SDK. The Solarium repository pins it in global.json.
  • For the optional io_uring storage environment: Linux 6.1 or later. See io_uring on Linux.

Add Solarium to your project​

Not on NuGet yet

The Solarium packages are not published on NuGet yet. The package ID Solarium on nuget.org belongs to a different project. Do not install it. Reference the Solarium projects from a clone of the repository.

Clone the repository next to your project:

git clone https://github.com/andresgutierrez/solarium.git

Add a project reference to the store:

dotnet add reference ../solarium/src/Solarium/Solarium.csproj

Add the other packages only if you need them:

# The io_uring storage environment (Linux 6.1 or later)
dotnet add reference ../solarium/src/Solarium.IoUring/Solarium.IoUring.csproj

# SolariumWAL, the Kommander WAL backend
dotnet add reference ../solarium/src/Solarium.Kommander/Solarium.Kommander.csproj

To build and test the repository itself:

cd solarium
dotnet build
dotnet test

Open a database​

A database is one directory. SolariumDb.Open creates the directory when it does not exist, because DbOptions.CreateIfMissing is true by default.

using Solarium;

using SolariumDb db = SolariumDb.Open("./data",
new DbOptions { CreateMissingColumnFamilies = true },
columnFamilies: ["metadata"]);

Each part of this call:

  • "./data" is the database directory. Only one open database can use it at a time. A LOCK file in the directory makes sure of this.
  • CreateMissingColumnFamilies = true creates the column families in columnFamilies that do not exist yet. When it is false, the default, a missing column family is an error, as in RocksDB.
  • columnFamilies: ["metadata"] names the column families that must exist. Every column family that exists already opens, even when you do not name it. The default column family always exists.
  • using disposes the database at the end of the scope. Dispose flushes the current WAL file, so every write before a clean close survives a power loss after it.

You can create column families only when you open the database. There is no call to create or drop a column family on an open database.

Get a column family​

ColumnFamily metadata = db.GetColumnFamily("metadata");

GetColumnFamily throws KeyNotFoundException when the database has no column family with that name. Each read and write method takes an optional ColumnFamily. When you pass none, the method uses the default column family.

Write​

A single write:

db.Put("user:42"u8, "Ada"u8);

An atomic batch across two column families:

db.Write(new WriteBatch()
.Put("log:0001"u8, "entry"u8)
.Put("last"u8, "0001"u8, metadata),
WriteOptions.Synced);

The batch applies all its operations, or none of them. WriteOptions.Synced flushes the WAL before Write returns, so the batch survives a power loss. Without it, the write survives a process crash but not a power loss. Writes and Batches explains the choice.

Keys and values are bytes. The u8 suffix of C# makes a UTF-8 byte span from a string literal. Keys sort by their bytes, as unsigned values.

Read​

byte[]? value = db.Get("log:0001"u8);
byte[]? last = db.Get("last"u8, metadata);

Get returns null when the key is absent or deleted. The returned array belongs to you.

To read several keys at one snapshot, use MultiGet:

byte[]?[] values = db.MultiGet(["user:42"u8.ToArray(), "user:43"u8.ToArray()]);

Iterate over a key range​

using System.Text;

using Iterator iterator = db.NewIterator(options: new ReadOptions { IterateUpperBound = "log;"u8.ToArray() });
for (iterator.Seek("log:"u8); iterator.Valid(); iterator.Next())
Console.WriteLine(Encoding.UTF8.GetString(iterator.Key()));

The iterator reads a snapshot. It sees the data as it was when you created it. Seek moves to the first key at or after its target. The iterator stops before the first key at or after IterateUpperBound. Here, ; is the byte after :, so the bound ends the log: prefix.

Dispose each iterator. Until you dispose it, it keeps the SSTables of its snapshot open.

Delete​

db.Delete("user:42"u8);
db.DeleteRange("log:0000"u8, "log:0001"u8);

DeleteRange deletes every key from the begin key up to the end key. The begin key is included and the end key is excluded. It writes one range tombstone, not one delete for each key.

The complete program​

using System.Text;
using Solarium;

using SolariumDb db = SolariumDb.Open("./data",
new DbOptions { CreateMissingColumnFamilies = true },
columnFamilies: ["metadata"]);

ColumnFamily metadata = db.GetColumnFamily("metadata");

db.Write(new WriteBatch()
.Put("log:0001"u8, "first"u8)
.Put("log:0002"u8, "second"u8)
.Put("last"u8, "0002"u8, metadata),
WriteOptions.Synced);

Console.WriteLine(Encoding.UTF8.GetString(db.Get("last"u8, metadata)!));

using Iterator iterator = db.NewIterator(options: new ReadOptions { IterateUpperBound = "log;"u8.ToArray() });
for (iterator.Seek("log:"u8); iterator.Valid(); iterator.Next())
Console.WriteLine($"{Encoding.UTF8.GetString(iterator.Key())} = {Encoding.UTF8.GetString(iterator.Value())}");

db.DeleteRange("log:0001"u8, "log:0002"u8);
Console.WriteLine(db.Get("log:0001"u8) is null ? "log:0001 is deleted" : "log:0001 is still there");

The output:

0002
log:0001 = first
log:0002 = second
log:0001 is deleted

Run the program a second time. The database opens with the data of the first run, because the WAL replays at open.

What is in the directory​

After the first run, the directory holds these files:

FileContent
LOCKAn exclusive lock, so that only one open database uses the directory.
FORMATThe format version.
MANIFESTThe column families, their SSTables, and the live WAL files.
NNNNNN.logA WAL file.
NNNNNN.sstAn SSTable, after a memtable flush.

Do not change these files by hand. Data Directory describes each one.

Next steps​