Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
b1526a4
Integrate the frozen LinearTable API
Sep 13, 2026
4043835
Finish busy persistence before drop returns
Sep 13, 2026
2c83d6d
Avoid generated table startup stack overflow
Sep 13, 2026
a9cbab2
Prepare WorkTable 1.9.0 beta1
Sep 13, 2026
a8a2e32
Clarify the beta performance boundary
Sep 13, 2026
c770e67
Contain persistence worker panics during drop
Sep 13, 2026
c631e0b
Correct the page directory capacity note
Sep 13, 2026
0a46d35
Reduce all-feature test linker load
Sep 13, 2026
dc35d62
Keep opaque archived updates within row storage
Sep 13, 2026
b1b9546
Document WorkTable mutation choices
Sep 13, 2026
d1d58b3
Rename declared partial update APIs
Sep 13, 2026
6f2f12d
Add typed mutation selectors and replace API
Sep 13, 2026
c7594aa
Keep mutation generator tests lint-clean
Sep 13, 2026
7299aa9
Update codegen assertions for hidden mutation helpers
Sep 13, 2026
72b018f
Support atomic typed mutation field sets
Sep 13, 2026
45c015d
Fix persisted index page sizing and batch lookup
Sep 13, 2026
d4b8aac
Document shipping-schema mutation cost in the user guide
Sep 13, 2026
d9b6847
Document mutation concurrency, index backends, and range cost
Sep 14, 2026
becd794
Correct mutation concurrency: background QoS capped cores at 1.3
Sep 14, 2026
512f97c
Shard the row-lock map so disjoint writers do not serialize
Sep 14, 2026
d4e6cdc
Seqlock point select so shared readers do not CAS cell stripes
Sep 14, 2026
59e9961
Add select_with so point reads skip cell memcpy and deserialize
Sep 14, 2026
8c73ea8
Give the cell seqlock a version counter
Sep 14, 2026
2ff7bfc
Copy the cell before running a select_with closure
Sep 14, 2026
e52893a
Gate zero-copy select_with on a pointer-free archived row
Sep 14, 2026
a428c4a
Race writers at one key through the in-place path
Sep 14, 2026
afa7770
Wait on a Vec of predecessor locks, not a HashSet
Sep 14, 2026
4559484
Borrow the lock map instead of cloning its Arc per operation
Sep 14, 2026
1ac74c8
Stop the row-lock protocol sharing cache lines per operation
Sep 14, 2026
e73e1bf
Keep a lock's flag in the lock instead of its own allocation
Sep 14, 2026
cf83335
Get the branch green: fmt, clippy, no-std and the loom models
Sep 14, 2026
871d15d
Stripe vacuum's lock label by key as well
Sep 14, 2026
c4f65f2
Assert every shard of the row-lock map is addressable
Sep 14, 2026
5fd0892
Do not sleep waiting for an idle table to go idle
Sep 15, 2026
f0ceaed
Make the borrowed lock guards unsafe to obtain
Sep 16, 2026
c439f04
Refuse char on the zero-copy select_with path
Sep 16, 2026
89b7173
Stop assume_init reading the uninitialised tail of a seqlock copy
Sep 16, 2026
663b400
Give vacuum's idle fast path a window to observe
Sep 16, 2026
49ac847
Bound the join a busy persistence drop waits on
Sep 16, 2026
7499e20
Fence the index batch's positional page lookup
Sep 16, 2026
ca2a23f
Stop duplicating the retry loop and the opaque-rebuild arm
Sep 16, 2026
9f6a42b
Point the old in_place keyword at its new spelling
Sep 16, 2026
3bdabc0
Assert the rename message the rename now emits
Sep 16, 2026
1098eeb
Stop a pull request paying for the release matrix twice over
Sep 16, 2026
9443e7f
Give a pull request a smoke test instead of the full matrix
Sep 16, 2026
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
59 changes: 56 additions & 3 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
workflow_dispatch:

env:
CARGO_TERM_COLOR: always
Expand Down Expand Up @@ -32,29 +33,75 @@ jobs:
# exceeded 30 minutes when GitHub's cache service was unavailable. Keep
# enough room for a real from-scratch release gate.
timeout-minutes: 45
# Not on a pull request. `cargo test --workspace --all-targets` is a
# ten-minute compile on a two-core runner before it runs anything, three
# times over the matrix, and it is the same command `scripts/ci-local.sh`
# runs on a developer's machine in a fraction of that. Run it there.
#
# It still runs on master, which is what `publish` gates on, and on a manual
# `workflow_dispatch` when a change wants it earlier.
if: github.event_name != 'pull_request'
strategy:
fail-fast: false
matrix:
include:
- name: default
args: ""
test_debug: 2
- name: versioned-publication
args: "--features versioned-row-publication"
test_debug: 2
- name: all-features
args: "--all-features"
# Full DWARF makes Rust 1.98's bundled rust-lld crash while linking
# the large integration-test binary on a two-core runner.
test_debug: 0

steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: "true"
add-job-id-key: "false"
- name: Build
run: cargo build --workspace --all-targets ${{ matrix.args }} --verbose
- name: Run tests
# There is no separate build step. `cargo test --all-targets` builds every
# target it is about to run, and the build step ran without
# `CARGO_PROFILE_TEST_DEBUG`: the two commands disagreed about the test
# profile, so the second one missed the first one's cache and compiled the
# workspace a second time. On the all-features leg it also compiled with the
# full DWARF that `test_debug: 0` is set to avoid, which is what makes
# rust-lld crash linking the integration-test binary on a two-core runner.
- name: Build and run tests
run: cargo test --workspace --all-targets ${{ matrix.args }} --verbose
env:
CARGO_PROFILE_TEST_DEBUG: ${{ matrix.test_debug }}


# What a pull request gets instead of the full matrix: the library tests of
# every crate, without `--all-targets`.
#
# That leaves out the integration-test binary and the compile-fail harness,
# which is where the ten minutes live -- the trybuild tests shell out to
# nested cargo builds, and one of them alone took 56 seconds. The lib tests
# are the part that answers "did this change break something" quickly, and
# they are not a token gesture: the assertion that turned this whole workflow
# red was `worktable_dsl`'s `legacy_in_place_section_is_rejected`, a lib test
# this job runs and the fmt/clippy/no-std jobs all passed straight over.
#
# The full matrix still runs on master and on demand.
smoke:
name: Smoke test (lib tests)
runs-on: ubicloud-standard-2
timeout-minutes: 15
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: "true"
add-job-id-key: "false"
- name: Library tests (cargo test --workspace --lib)
run: cargo test --workspace --lib --verbose

clippy_check:
name: Clippy (${{ matrix.name }})
runs-on: ubicloud-standard-2
Expand All @@ -81,6 +128,12 @@ jobs:
name: Archived-row lock concurrency models
runs-on: ubicloud-standard-2
timeout-minutes: 15
# Not on a pull request. The models explore an interleaving space rather
# than run a suite, they build into their own CARGO_TARGET_DIR so they share
# no cache with any other job and pay a cold compile every time, and the
# answer does not change with a review comment. They still run on master,
# where `publish` gates on them, and on demand.
if: github.event_name != 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
Expand Down
55 changes: 47 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,46 @@
Change Log
==========

## [1.10.0-beta1]

### Changed

- Declared mutations now use `update:` and
`update_in_place:`. They generate typed selector calls such as
`update_by_id(id, InvoiceColumns::AMOUNT, value)` and
`update_in_place_by_id(id, InvoiceColumns::STATE, edit)`. Multi-column
declarations expose one selector for their exact atomic field set and take
the generated query struct. Multi-column `update_in_place` declarations pass
a tuple of mutable archived fields to one closure, preserving the declared
atomic field set. Full-row replacement is now `replace(row)`.

### Added

- The frozen `LinearTable` and `VecTable` API is now part of WorkTable itself,
replacing the retired `worktable-vec` compatibility crate. PathDB's indexed
lookup compiles to instruction-for-instruction identical AArch64 code before
and after the move.

### Fixed

- Generated fixed-size primary-key wrappers now report their actual archived
alignment. A `u128` primary key previously under-budgeted each persisted WTI
entry, producing an 18,518-byte index archive for a 16,356-byte default inner
page; the corrected 16-byte alignment selects a capacity whose archive fits.

- Dropping a persisted table with queued writes now joins its private writer
before the final handle disappears. An immediate same-path reopen can no
longer race detached writes and observe a partial store or torn index header.

- Generated persisted-table startup now constructs its page storage directly
in the final `Arc` allocation and pins nested load futures before awaiting
them. This removes roughly 16 KiB page and directory temporaries from normal
thread stacks. In AgentCode's eight-table empty-store startup, the generated
load future shrank from 17,832 bytes to 2,392 bytes and the restart path no
longer overflows Tokio's default 2 MiB worker stack. Existing-page reads and
mutations are unchanged; fresh construction and page-growing inserts use the
new final-allocation initializer.

## [1.9.0-alpha1]


Expand Down Expand Up @@ -39,9 +79,8 @@ Change Log


- **`queries:` on a `vec: true` table.** It was refused wholesale; it now
generates `update_<name>`, `delete_<name>` and `update_<name>_in_place` under
the same names the paged table uses, so a declaration reads the same either
way.
generates typed `update_by_<key>` and `update_in_place_by_<key>` dispatch,
plus declared delete methods, under the same names the paged table uses.

These are named wrappers rather than a new execution path: a declared update
is `update(&pk, |row| ..)` with the columns filled in from a generated struct,
Expand Down Expand Up @@ -198,15 +237,15 @@ Change Log
declares 65,536 rows into a partition that holds 256), and `persist: true`,
which a dense partition has no engine to honour.

It carries `queries:`. An `update` or `delete` query keyed by the primary key
generates the same method name against the same `<Name>Query` struct the
paged table generates, so a call reads identically; the signature does not,
It carries `queries:`. An `update` query keyed by the primary key uses the
same typed field-set selector as the paged table, and a multi-column selector
takes the same `<Name>Query` struct. The signature differs,
deliberately, because there is no `.await` and no `WorkTableError`, and a
call that moved between the shapes should fail to compile rather than
quietly change what it guarantees. A query keyed by any other column is
refused: a dense partition has no secondary index, and scanning it instead
would be a keyed operation silently becoming a linear one. `in_place` is
refused as a synonym, because every update here is already in place.
would be a keyed operation silently becoming a linear one.
`update_in_place` is refused, because every update here is already in place.

Note that `memory_by_key` and `memory_total` **cannot see this saving**. They
report `used_bytes`, which is rows plus indexes and excludes the fixed floor
Expand Down
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["codegen", "dsl", "examples", "performance_measurement", "performance

[package]
name = "worktable"
version = "1.9.0-alpha1"
version = "1.10.0-beta1"
edition = "2024"
authors = ["Handy-caT"]
license = "MIT"
Expand Down Expand Up @@ -147,12 +147,12 @@ walkdir = { version = "2", optional = true }
# These pre-release workspace crates move as one train. The explicit caret
# keeps the dependency policy consistent while the local path selects this
# checkout during validation.
worktable_codegen = { path = "codegen", version = "^1.9.0-alpha1" }
worktable_codegen = { path = "codegen", version = "^1.10.0-beta1" }
# Re-exported below. Each generated table carries its declaration as a const
# whose documentation says to read it with `worktable_dsl::Schema::parse`; that
# instruction is only true if a plain `worktable` dependency can reach the
# crate.
worktable_dsl = { path = "dsl", version = "^1.0.0-beta.19", optional = true }
worktable_dsl = { path = "dsl", version = "^1.10.0-beta1", optional = true }

[target.'cfg(unix)'.dependencies]
libc = { version = "^0.2", default-features = false }
Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ for table in database.catalog().system_tables() {

```toml
[dependencies]
worktable = { version = "^1.9.0-alpha1", features = ["s3-support"] } # S3 sync, optional
worktable = { version = "^1.10.0-beta1", 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 persistence is experimental and uses their native checkpoint/WAL adapters; declarations using either backend must state `persist: true` or `persist: false` explicitly. The full syntax and capability matrix are documented in [Per-index backends with `using`](docs/index-backend-dsl-proposal.md).
Expand Down Expand Up @@ -426,7 +426,7 @@ There are some default query implementations that are available for all `WorkTab
into grouped persistence operations, and durability follows the usual `wait_for_ops` contract. Autoincrement tables
additionally get `reserve_pks(&self, count: usize) -> Range<RawPk>` to pre-assign contiguous keys to a batch;
- `upsert(&self, row: <Name>Row) -> Result<(), WorkTableError>`;
- `update(&self, row: <Name>Row) -> Result<(), WorkTableError>`;
- `replace(&self, row: <Name>Row) -> Result<(), WorkTableError>`;
- `delete(&self, pk: <Name>PrimaryKey) -> Result<(), WorkTableError>`;
- `select_all<'a>(&'a self) -> SelectQueryBuilder<'a, <Name>Row, Self>`;

Expand Down Expand Up @@ -456,8 +456,13 @@ query.

#### `update` query declaration

`update` queries are used to update row's data partially. Default generated `update` allows only full update of the row.
But if user's logic needs some simultaneous update of row parts from different code parts. `update` logic supports
`update` queries update only the declared fields through a typed, table-scoped
column selector. For example,
`table.update_by_id(id, TestColumns::ANOTHER, value).await?` changes only
`another`. A multi-column declaration exposes one atomic selector such as
`TestColumns::NAME_AND_AMOUNT` and takes its generated query struct. The
generated `replace(row)` method replaces the full row.
When application logic updates disjoint parts of a row concurrently, `update` supports
smart lock logic that allows simultaneous update of not overlapping row fields.

#### `select_all` query declaration
Expand Down
21 changes: 16 additions & 5 deletions benches/cases/full_featured.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ fn update(c: &mut Criterion) {
another: format!("updated_{}", fastrand::u64(..)),
something: fastrand::u64(..),
};
black_box(table.update(row).await)
black_box(table.replace(row).await)
})
});
}
Expand Down Expand Up @@ -151,7 +151,11 @@ fn update_by_pk_query(c: &mut Criterion) {
let query = AnotherByIdQuery {
another: format!("upd_{}", fastrand::u64(..)),
};
black_box(table.update_another_by_id(query, id).await)
black_box(
table
.update_by_id(id, FullFeaturedColumns::ANOTHER, (query).another)
.await,
)
})
});
}
Expand Down Expand Up @@ -179,7 +183,11 @@ fn update_by_unique_index_query(c: &mut Criterion) {
let query = AnotherByVal1Query {
another: format!("upd_{}", fastrand::u64(..)),
};
black_box(table.update_another_by_val_1(query, val1).await)
black_box(
table
.update_by_val1(val1, FullFeaturedColumns::ANOTHER, (query).another)
.await,
)
})
});
}
Expand All @@ -200,8 +208,11 @@ fn in_place_update(c: &mut Criterion) {
};

c.bench_function("full_featured_in_place_update_val", |b| {
b.to_async(&rt)
.iter(|| async { table.update_val_by_id_in_place(|val| *val += 1, black_box(pk)).await })
b.to_async(&rt).iter(|| async {
table
.update_in_place_by_id(black_box(pk), FullFeaturedColumns::VAL, |val| *val += 1)
.await
})
});
}

Expand Down
2 changes: 1 addition & 1 deletion benches/cases/non_unique_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ fn update(c: &mut Criterion) {
value: fastrand::u64(..),
category: fastrand::u64(0..10),
};
black_box(table.update(row).await)
black_box(table.replace(row).await)
})
});
}
Expand Down
2 changes: 1 addition & 1 deletion benches/cases/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ fn update(c: &mut Criterion) {
id: pk.into(),
value: fastrand::u64(..),
};
black_box(table.update(row).await)
black_box(table.replace(row).await)
})
});
}
Expand Down
2 changes: 1 addition & 1 deletion benches/cases/unique_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ fn update(c: &mut Criterion) {
test: fastrand::i64(..),
another: fastrand::u64(..),
};
black_box(table.update(row).await)
black_box(table.replace(row).await)
})
});
}
Expand Down
8 changes: 6 additions & 2 deletions benches/cases/update_contention.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ fn single_row_update_contention(c: &mut Criterion) {
another: format!("upd_{}", fastrand::u64(..)),
something: fastrand::u64(..),
};
black_box(table_clone.update(row).await)
black_box(table_clone.replace(row).await)
});
}
while join_set.join_next().await.is_some() {}
Expand Down Expand Up @@ -81,7 +81,11 @@ fn single_row_in_place_contention(c: &mut Criterion) {
for _ in 0..level {
let table_clone = table.clone();
join_set.spawn(async move {
black_box(table_clone.update_val_by_id_in_place(|val| *val += 1, pk).await)
black_box(
table_clone
.update_in_place_by_id(pk, FullFeaturedColumns::VAL, |val| *val += 1)
.await,
)
});
}
while join_set.join_next().await.is_some() {}
Expand Down
2 changes: 1 addition & 1 deletion benches/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ worktable!(
another_idx: another,
},
queries: {
in_place: {
update_in_place: {
ValById(val) by id,
}
update: {
Expand Down
4 changes: 2 additions & 2 deletions codegen/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "worktable_codegen"
version = "1.9.0-alpha1"
version = "1.10.0-beta1"
edition = "2024"
license = "MIT"
description = "Proc-macro companion crate for worktable: the worktable! macro and its derives."
Expand All @@ -26,7 +26,7 @@ proc-macro = true
# a declaration. See its crate docs for why that needed a separate crate.
# Name the reviewed prerelease while accepting compatible schema-model and
# validator updates. Release checks verify the resolved generated API.
worktable_dsl = { path = "../dsl", version = "^1.0.0-beta.19" }
worktable_dsl = { path = "../dsl", version = "^1.10.0-beta1" }
# Test-only. As a normal dependency this proc-macro crate put `rkyv` with its
# default features into the graph, which turned on `rkyv/std` for the target
# build too and dragged `ptr_meta` with it. The generated code names `rkyv`
Expand Down
Loading
Loading