Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 29 additions & 15 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,55 @@ on:
env:
CARGO_TERM_COLOR: always

permissions:
contents: read

jobs:
build:

name: Build and test (${{ matrix.name }})
runs-on: ubicloud-standard-2
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- name: default
args: ""
- name: versioned-publication
args: "--features versioned-row-publication"
- name: all-features
args: "--all-features"

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: "true"
add-job-id-key: "false"
- name: Build
run: cargo build --verbose
run: cargo build --workspace --all-targets ${{ matrix.args }} --verbose
- name: Run tests
run: cargo test --verbose
run: cargo test --workspace --all-targets ${{ matrix.args }} --verbose


clippy_check:

name: Clippy (${{ matrix.name }})
runs-on: ubicloud-standard-2
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- name: default
args: ""
- name: all-features
args: "--all-features"

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: "true"
add-job-id-key: "false"
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
components: clippy
override: true
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-targets --all-features -- -D warnings
- name: Clippy (deny warnings)
run: cargo clippy --workspace --all-targets ${{ matrix.args }} -- -D warnings
21 changes: 15 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["codegen", "examples", "performance_measurement", "performance_measur

[package]
name = "worktable"
version = "0.9.4"
version = "1.0.0-beta.1"
edition = "2024"
authors = ["Handy-caT"]
license = "MIT"
Expand All @@ -15,22 +15,31 @@ keywords = ["database", "embedded", "in-memory", "index", "storage"]
categories = ["database-implementations", "data-structures", "caching"]

[features]
default = ["wti-predictable-search"]
perf_measurements = ["dep:performance_measurement", "dep:performance_measurement_codegen"]
s3-support = ["dep:rusty-s3", "dep:url", "dep:reqwest", "dep:walkdir", "worktable_codegen/s3-support"]
wti-hybrid-search = ["indexset/wt-slice-binary-search"]
wti-predictable-search = ["indexset/custom-binary-search"]
wti-std-search = ["indexset/std-binary-search"]
wti-superslice-search = ["indexset/superslice-binary-search"]
versioned-row-publication = ["worktable_codegen/versioned-row-publication"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
async-trait = "0.1.89"
arctic-map = "=0.1.4"
congee = "=0.4.1"
convert_case = "0.6.0"
data_bucket = "=0.4.1"
data_bucket = "=0.5.1"
# data_bucket = { git = "https://github.com/pathscale/DataBucket", branch = "page_cdc_correction", version = "0.2.7" }
# data_bucket = { path = "../DataBucket", version = "0.3.14" }
derive_more = { version = "2.0.1", features = ["from", "error", "display", "debug", "into"] }
eyre = "0.6.12"
fastrand = "2.3.0"
futures = "0.3.30"
indexset = { package = "WorkTablesIndex", version = "=0.0.1", features = ["concurrent", "cdc", "multimap"] }
indexset = { package = "WorkTablesIndex", version = "=0.0.4", default-features = false, features = ["concurrent", "cdc", "multimap"] }
vanilla_indexset = { package = "indexset", version = "=0.15.0", features = ["concurrent", "cdc", "multimap"] }
# indexset = { path = "../indexset", version = "0.15.0", features = ["concurrent", "cdc", "multimap"] }
# indexset = { package = "wt-indexset", version = "=0.12.12", features = ["concurrent", "cdc", "multimap"] }
log = "0.4.29"
Expand All @@ -40,16 +49,16 @@ performance_measurement = { path = "performance_measurement", version = "0.1.0",
performance_measurement_codegen = { path = "performance_measurement/codegen", version = "0.1.0", optional = true }
prettytable-rs = "^0.10"
psc-nanoid = { version = "3.1.1", features = ["rkyv", "packed"] }
rkyv = { version = "0.8.9", features = ["uuid-1"] }
rkyv = { version = "0.8.17", features = ["uuid-1"] }
reqwest = { version = "0.12", optional = true, default-features = false, features = ["rustls-tls-webpki-roots", "charset", "http2"] }
rusty-s3 = { package = "rusty-s3-temp", version = "0.9.0", optional = true }
smart-default = "0.7.1"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
url = { version = "2", optional = true }
uuid = { version = "1.10.0", features = ["v4", "v7"] }
uuid = { version = "1.24.0", features = ["v4", "v7"] }
walkdir = { version = "2", optional = true }
worktable_codegen = { path = "codegen", version = "=0.9.4" }
worktable_codegen = { path = "codegen", version = "=1.0.0-beta.1" }

[dev-dependencies]
chrono = "0.4.43"
Expand Down
45 changes: 41 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ from a macro, and that persisting it is one feature flag away.
## Install

```sh
cargo add worktable
cargo add worktable@1.0.0-beta.1
```

## What you get
Expand All @@ -24,6 +24,7 @@ cargo add worktable
| **Embedded optimized** | Very low overhead, built for resource-sensitive environments. |
| **Typed tables from a macro** | `worktable!` generates the table, row and primary-key types. No hand-written boilerplate per table. |
| **Primary and secondary indexes** | Autoincrement or supplied primary keys; unique and non-unique secondary indexes, each adding a `select_by_<column>` method. |
| **Per-index physical selection** | An optional `using` clause can statically select WorkTablesIndex, vanilla IndexSet, Congee, or Arctic where their capabilities fit. See [the backend guide](docs/index-backend-dsl-proposal.md). |
| **Generated queries** | `select`, `insert`, `upsert`, `update`, `delete` and a `select_all` query builder on every table, plus the custom update/delete queries you declare. |
| **Paged in-memory storage** | Records live in `DataPages` with a free list for reuse. `rkyv` gives zero-copy access to archived rows. |
| **Concurrency** | Lock-free concurrent indexes with change-data-capture, plus a row-level `LockMap` for ordered access. |
Expand All @@ -43,9 +44,47 @@ S3 support layers *on top of* the disk engine rather than replacing it.

```toml
[dependencies]
worktable = { version = "0.9", features = ["s3-support"] } # S3 sync, optional
worktable = { version = "=1.0.0-beta.1", features = ["s3-support"] } # S3 sync, optional
```

Persisted indexes default to WorkTablesIndex. Vanilla IndexSet can be selected explicitly with `using indexset` while retaining the existing disk/S3 representation. Congee and Arctic are explicitly memory-only and require `persist: false`. The full syntax and capability matrix are documented in [Per-index backends with `using`](docs/index-backend-dsl-proposal.md).

WorkTablesIndex uses its predictable branch-based node search by default in WorkTable. This avoids a measured regression for sequential numeric-key workloads. Alternative search policies remain compile-time feature gates: disable WorkTable's default features and enable one of `wti-hybrid-search`, `wti-std-search`, or `wti-superslice-search` (plus any other features such as `s3-support`). Prefer one search feature for an unambiguous build. If Cargo feature unification enables several, WorkTablesIndex applies the documented deterministic precedence rather than rejecting the graph.

## Concurrent read/write publication

The default build preserves the existing lowest-latency page path and requires
applications to exclude reads that overlap page-byte mutation. Applications
that need generated reads to overlap updates, inserts, deletes, and vacuum can
opt into immutable row-version publication:

```toml
[dependencies]
worktable = { version = "=1.0.0-beta.1", features = ["versioned-row-publication"] }
```

Generated point lookups use a strict backend-specific visibility contract by
default. WorkTablesIndex 0.0.4 keeps the structural mapping pinned until its
selected node is locked, making both hits and misses definitive; contended
lookups release the structural guard before waiting and retry the mapping.
Congee and Arctic use their native concurrent point lookups. The explicit
vanilla `using indexset` backend remains experimental and is excluded from the
stable concurrent-read contract because upstream IndexSet does not expose an
equivalent validation primitive. This index-visibility contract is independent
of the optional row publication mode above.

In this mode, generated reads acquire an immutable owned row version instead
of borrowing the mutable archived page image. Writers replace a per-row version
only after a complete page mutation, insert visibility is an atomic lifecycle
transition after every index is installed, and deleted or relocated links are
not reused until readers that could have captured them have drained. Page bytes
remain the persistence image and are internally serialized; range queries are
still non-snapshot reads. The mode intentionally trades memory, an atomic
read-side grace-period counter, and publication bookkeeping for this stronger
concurrent-read contract. See
[`docs/versioned-row-publication.md`](docs/versioned-row-publication.md) for the
protocol and its scope.

## Relationship to `data_bucket`

WorkTable is built on [`data_bucket`](https://crates.io/crates/data_bucket), which
Expand Down Expand Up @@ -395,5 +434,3 @@ enum WorkTableError
## Examples

Check out - [Examples](./examples)


68 changes: 68 additions & 0 deletions benches/cases/unique_index.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, black_box, criterion_group};
use std::sync::Arc;
use tokio::runtime::Runtime;
use worktable::prelude::*;
use worktable::worktable;

use crate::common::*;

worktable! {
name: CongeeRangeBenchmark,
persist: false,
columns: {
id: u64 primary_key using congee,
value: u64,
},
}

worktable! {
name: ArcticRangeBenchmark,
persist: false,
columns: {
id: u64 primary_key using arctic,
value: u64,
},
}

fn insert(c: &mut Criterion) {
let table = UniqueIndexWorkTable::default();

Expand Down Expand Up @@ -64,6 +84,52 @@ fn select_by_unique_index(c: &mut Criterion) {
});
}

fn select_by_unique_index_range(c: &mut Criterion) {
let table = UniqueIndexWorkTable::default();

for i in 1..=1000i64 {
let row = UniqueIndexRow {
id: table.get_next_pk().into(),
test: i,
another: i as u64,
};
table.insert(row).unwrap();
}

c.bench_function("unique_index_select_by_test_range", |b| {
b.iter(|| {
let test = fastrand::i64(1..=1000);
black_box(table.select_by_test_range(test..=test).execute())
})
});
}

fn art_primary_key_ranges(c: &mut Criterion) {
const ROWS: u64 = 10_000;
let congee = CongeeRangeBenchmarkWorkTable::default();
let arctic = ArcticRangeBenchmarkWorkTable::default();
for id in 0..ROWS {
congee.insert(CongeeRangeBenchmarkRow { id, value: id }).unwrap();
arctic.insert(ArcticRangeBenchmarkRow { id, value: id }).unwrap();
}

let mut group = c.benchmark_group("art_primary_key_single_row_range");
group.throughput(Throughput::Elements(1));
group.bench_function("congee", |b| {
b.iter(|| {
let id = black_box(fastrand::u64(0..ROWS));
black_box(congee.select_by_pk_range(id..=id).execute().unwrap())
})
});
group.bench_function("arctic", |b| {
b.iter(|| {
let id = black_box(fastrand::u64(0..ROWS));
black_box(arctic.select_by_pk_range(id..=id).execute().unwrap())
})
});
group.finish();
}

fn update(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
let table = Arc::new(UniqueIndexWorkTable::default());
Expand Down Expand Up @@ -224,6 +290,8 @@ criterion_group! {
insert,
select_by_pk,
select_by_unique_index,
select_by_unique_index_range,
art_primary_key_ranges,
update,
delete,
upsert_insert,
Expand Down
3 changes: 2 additions & 1 deletion codegen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
[package]
name = "worktable_codegen"
version = "0.9.4"
version = "1.0.0-beta.1"
edition = "2024"
license = "MIT"
description = "Proc-macro companion crate for worktable: the worktable! macro and its derives."
repository = "https://github.com/pathscale/WorkTable"

[features]
s3-support = []
versioned-row-publication = []

[lib]
name = "worktable_codegen"
Expand Down
22 changes: 21 additions & 1 deletion codegen/src/common/model/column.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use indexmap::IndexMap;
use std::collections::HashMap;

use crate::common::model::GeneratorType;
use crate::common::model::index::Index;
use crate::common::model::{GeneratorType, IndexBackend};
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::spanned::Spanned;
Expand All @@ -18,6 +18,7 @@ pub struct Columns {
pub field_positions: HashMap<Ident, usize>,
pub indexes: IndexMap<Ident, Index>,
pub primary_keys: Vec<Ident>,
pub primary_index_backend: IndexBackend,
pub generator_type: GeneratorType,
}

Expand All @@ -28,6 +29,7 @@ pub struct Row {
pub is_primary_key: bool,
pub gen_type: GeneratorType,
pub optional: bool,
pub index_backend: Option<IndexBackend>,
}

impl Columns {
Expand All @@ -37,6 +39,7 @@ impl Columns {
let mut sized = true;
let mut pk = vec![];
let mut gen_type = None;
let mut primary_index_backend = None;

for (pos, row) in rows.into_iter().enumerate() {
let type_ = &row.type_;
Expand All @@ -59,7 +62,23 @@ impl Columns {
} else {
gen_type = Some(row.gen_type)
}
let backend = row.index_backend.unwrap_or_default();
if let Some(existing) = primary_index_backend {
if existing != backend {
return Err(syn::Error::new(
row.name.span(),
"all columns in a composite primary key must use the same index backend",
));
}
} else {
primary_index_backend = Some(backend);
}
pk.push(row.name);
} else if row.index_backend.is_some() {
return Err(syn::Error::new(
row.name.span(),
"`using` on a column is only valid after `primary_key`; select secondary index backends in `indexes`",
));
}
}

Expand All @@ -72,6 +91,7 @@ impl Columns {
columns_map,
indexes: Default::default(),
primary_keys: pk,
primary_index_backend: primary_index_backend.unwrap_or_default(),
generator_type: gen_type.expect("set"),
field_positions,
})
Expand Down
Loading
Loading