Skip to content

Commit d71483d

Browse files
authored
Merge branch 'master' into bfops/remove-python-smoketests
2 parents bf32f41 + 3b28744 commit d71483d

16 files changed

Lines changed: 1380 additions & 1061 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -887,15 +887,16 @@ jobs:
887887
const targetRepo = process.env.TARGET_REPO;
888888
// Use the ref for pull requests because the head sha is brittle (github does some extra dance where it merges in master).
889889
const publicRef = (context.eventName === 'pull_request') ? context.payload.pull_request.head.ref : context.sha;
890+
const publicPrNumber = context.payload.pull_request?.number ?? context.payload.inputs?.pr_number;
890891
const preDispatch = new Date().toISOString();
891-
892+
892893
// Dispatch the workflow in the target repository
893894
await github.rest.actions.createWorkflowDispatch({
894895
owner: targetOwner,
895896
repo: targetRepo,
896897
workflow_id: workflowId,
897898
ref: targetRef,
898-
inputs: { public_ref: publicRef }
899+
inputs: { public_ref: publicRef, public_pr_number: String(publicPrNumber) }
899900
});
900901
901902
const sleep = (ms) => new Promise(r => setTimeout(r, ms));

crates/bindings-typescript/src/react/useTable.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export interface UseTableCallbacks<RowType> {
2424
onInsert?: (row: RowType) => void;
2525
onDelete?: (row: RowType) => void;
2626
onUpdate?: (oldRow: RowType, newRow: RowType) => void;
27+
/** Whether the subscription is active. Defaults to `true`. */
28+
enabled?: boolean;
2729
}
2830

2931
type MembershipChange = 'enter' | 'leave' | 'stayIn' | 'stayOut';
@@ -67,6 +69,7 @@ export function useTable<TableDef extends UntypedTableDef>(
6769
callbacks?: UseTableCallbacks<Prettify<RowType<TableDef>>>
6870
): [readonly Prettify<RowType<TableDef>>[], boolean] {
6971
type UseTableRowType = RowType<TableDef>;
72+
const enabled = callbacks?.enabled ?? true;
7073
const accessorName = getQueryAccessorName(query);
7174
const whereExpr = getQueryWhereClause(query);
7275

@@ -93,6 +96,9 @@ export function useTable<TableDef extends UntypedTableDef>(
9396
readonly Prettify<UseTableRowType>[],
9497
boolean,
9598
] => {
99+
if (!enabled) {
100+
return [[], true];
101+
}
96102
const connection = connectionState.getConnection();
97103
if (!connection) {
98104
return [[], false];
@@ -107,7 +113,7 @@ export function useTable<TableDef extends UntypedTableDef>(
107113
// TODO: investigating refactoring so that this is no longer necessary, as we have had genuine bugs with missed deps.
108114
// See https://github.com/clockworklabs/SpacetimeDB/pull/4580.
109115
// eslint-disable-next-line react-hooks/exhaustive-deps
110-
}, [connectionState, accessorName, querySql, subscribeApplied]);
116+
}, [connectionState, accessorName, querySql, subscribeApplied, enabled]);
111117

112118
// Invalidate the cached snapshot when computeSnapshot changes (e.g. when
113119
// subscribeApplied flips to true) so getSnapshot() recomputes on the next
@@ -117,6 +123,10 @@ export function useTable<TableDef extends UntypedTableDef>(
117123
}, [computeSnapshot]);
118124

119125
useEffect(() => {
126+
if (!enabled) {
127+
setSubscribeApplied(false);
128+
return;
129+
}
120130
const connection = connectionState.getConnection()!;
121131
if (connectionState.isActive && connection) {
122132
const cancel = connection
@@ -129,10 +139,14 @@ export function useTable<TableDef extends UntypedTableDef>(
129139
cancel.unsubscribe();
130140
};
131141
}
132-
}, [querySql, connectionState.isActive, connectionState]);
142+
}, [querySql, connectionState.isActive, connectionState, enabled]);
133143

134144
const subscribe = useCallback(
135145
(onStoreChange: () => void) => {
146+
if (!enabled) {
147+
return () => {};
148+
}
149+
136150
const onInsert = (
137151
ctx: EventContextInterface<UntypedRemoteModule>,
138152
row: any
@@ -218,6 +232,7 @@ export function useTable<TableDef extends UntypedTableDef>(
218232
callbacks?.onDelete,
219233
callbacks?.onInsert,
220234
callbacks?.onUpdate,
235+
enabled,
221236
]
222237
);
223238

crates/bindings/src/lib.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,9 +1110,10 @@ impl ReducerContext {
11101110
/// use spacetimedb::{reducer, ReducerContext, Uuid};
11111111
///
11121112
/// #[reducer]
1113-
/// fn generate_uuid_v4(ctx: &ReducerContext) -> Uuid {
1114-
/// let uuid = ctx.new_uuid_v4();
1113+
/// fn generate_uuid_v4(ctx: &ReducerContext) -> Result<(), Box<dyn std::error::Error>> {
1114+
/// let uuid = ctx.new_uuid_v4()?;
11151115
/// log::info!(uuid);
1116+
/// Ok(())
11161117
/// }
11171118
/// # }
11181119
/// ```
@@ -1131,9 +1132,10 @@ impl ReducerContext {
11311132
/// use spacetimedb::{reducer, ReducerContext, Uuid};
11321133
///
11331134
/// #[reducer]
1134-
/// fn generate_uuid_v7(ctx: &ReducerContext) -> Result<Uuid, Box<dyn std::error::Error>> {
1135+
/// fn generate_uuid_v7(ctx: &ReducerContext) -> Result<(), Box<dyn std::error::Error>> {
11351136
/// let uuid = ctx.new_uuid_v7()?;
11361137
/// log::info!(uuid);
1138+
/// Ok(())
11371139
/// }
11381140
/// # }
11391141
/// ```

crates/cli/src/subcommands/login.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub fn cli() -> Command {
2222
.arg(
2323
Arg::new("server")
2424
.long("server-issued-login")
25+
.hide(true)
2526
.group("login-method")
2627
.help("Log in to a SpacetimeDB server directly, without going through a global auth server"),
2728
)

crates/client-api-messages/src/energy.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ pub struct EnergyQuanta {
1616
impl EnergyQuanta {
1717
pub const ZERO: Self = EnergyQuanta { quanta: 0 };
1818

19+
// per the comment on [`FunctionBudget::DEFAULT_BUDGET`]: 1 second of wasm runtime is roughtly 2 TeV
20+
pub const PER_EXECUTION_SEC: Self = Self::new(2_000_000_000_000);
21+
pub const PER_EXECUTION_NANOSEC: Self = Self::new(Self::PER_EXECUTION_SEC.get() / 1_000_000_000);
22+
1923
#[inline]
20-
pub fn new(quanta: u128) -> Self {
24+
pub const fn new(quanta: u128) -> Self {
2125
Self { quanta }
2226
}
2327

2428
#[inline]
25-
pub fn get(&self) -> u128 {
29+
pub const fn get(&self) -> u128 {
2630
self.quanta
2731
}
2832

crates/core/src/db/relational_db.rs

Lines changed: 14 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use spacetimedb_datastore::locking_tx_datastore::datastore::TxMetrics;
1717
use spacetimedb_datastore::locking_tx_datastore::state_view::{
1818
IterByColEqMutTx, IterByColRangeMutTx, IterMutTx, StateView,
1919
};
20-
use spacetimedb_datastore::locking_tx_datastore::{IndexScanPointOrRange, MutTxId, TxId};
20+
use spacetimedb_datastore::locking_tx_datastore::{ApplyHistoryCounters, IndexScanPointOrRange, MutTxId, TxId};
2121
use spacetimedb_datastore::system_tables::{
2222
system_tables, StModuleRow, ST_CLIENT_ID, ST_CONNECTION_CREDENTIALS_ID, ST_VIEW_SUB_ID,
2323
};
@@ -1617,62 +1617,20 @@ impl RelationalDB {
16171617
}
16181618
}
16191619

1620-
fn apply_history<H>(datastore: &Locking, database_identity: Identity, history: H) -> Result<(), DBError>
1621-
where
1622-
H: durability::History<TxData = Txdata>,
1623-
{
1624-
log::info!("[{database_identity}] DATABASE: applying transaction history...");
1625-
1626-
// TODO: Revisit once we actually replay history suffixes, ie. starting
1627-
// from an offset larger than the history's min offset.
1628-
// TODO: We may want to require that a `tokio::runtime::Handle` is
1629-
// always supplied when constructing a `RelationalDB`. This would allow
1630-
// to spawn a timer task here which just prints the progress periodically
1631-
// in case the history is finite but very long.
1632-
let (_, max_tx_offset) = history.tx_range_hint();
1633-
let mut last_logged_percentage = 0;
1634-
let progress = |tx_offset: u64| {
1635-
if let Some(max_tx_offset) = max_tx_offset {
1636-
let percentage = f64::floor((tx_offset as f64 / max_tx_offset as f64) * 100.0) as i32;
1637-
if percentage > last_logged_percentage && percentage % 10 == 0 {
1638-
log::info!("[{database_identity}] Loaded {percentage}% ({tx_offset}/{max_tx_offset})");
1639-
last_logged_percentage = percentage;
1640-
}
1641-
// Print _something_ even if we don't know what's still ahead.
1642-
} else if tx_offset.is_multiple_of(10_000) {
1643-
log::info!("[{database_identity}] Loading transaction {tx_offset}");
1644-
}
1620+
fn apply_history(
1621+
datastore: &Locking,
1622+
database_identity: Identity,
1623+
history: impl durability::History<TxData = Txdata>,
1624+
) -> Result<(), DBError> {
1625+
let counters = ApplyHistoryCounters {
1626+
replay_commitlog_time_seconds: WORKER_METRICS
1627+
.replay_commitlog_time_seconds
1628+
.with_label_values(&database_identity),
1629+
replay_commitlog_num_commits: WORKER_METRICS
1630+
.replay_commitlog_num_commits
1631+
.with_label_values(&database_identity),
16451632
};
1646-
1647-
let time_before = std::time::Instant::now();
1648-
1649-
let mut replay = datastore.replay(
1650-
progress,
1651-
// We don't want to instantiate an incorrect state;
1652-
// if the commitlog contains an inconsistency we'd rather get a hard error than showing customers incorrect data.
1653-
spacetimedb_datastore::locking_tx_datastore::datastore::ErrorBehavior::FailFast,
1654-
);
1655-
let start_tx_offset = replay.next_tx_offset();
1656-
history
1657-
.fold_transactions_from(start_tx_offset, &mut replay)
1658-
.map_err(anyhow::Error::from)?;
1659-
1660-
let time_elapsed = time_before.elapsed();
1661-
WORKER_METRICS
1662-
.replay_commitlog_time_seconds
1663-
.with_label_values(&database_identity)
1664-
.set(time_elapsed.as_secs_f64());
1665-
1666-
let end_tx_offset = replay.next_tx_offset();
1667-
WORKER_METRICS
1668-
.replay_commitlog_num_commits
1669-
.with_label_values(&database_identity)
1670-
.set((end_tx_offset - start_tx_offset) as _);
1671-
1672-
log::info!("[{database_identity}] DATABASE: applied transaction history");
1673-
datastore.rebuild_state_after_replay()?;
1674-
log::info!("[{database_identity}] DATABASE: rebuilt state after replay");
1675-
1633+
spacetimedb_datastore::locking_tx_datastore::apply_history(datastore, database_identity, history, counters)?;
16761634
Ok(())
16771635
}
16781636

crates/core/src/db/update.rs

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ mod test {
340340
host::module_host::create_table_from_def,
341341
};
342342
use spacetimedb_datastore::locking_tx_datastore::PendingSchemaChange;
343-
use spacetimedb_lib::db::raw_def::v9::{btree, RawModuleDefV9Builder, TableAccess};
343+
use spacetimedb_lib::db::raw_def::v9::{btree, RawIndexAlgorithm, RawModuleDefV9Builder, TableAccess};
344344
use spacetimedb_sats::{product, AlgebraicType, AlgebraicType::U64};
345345
use spacetimedb_schema::{auto_migrate::ponder_migrate, def::ModuleDef};
346346

@@ -432,7 +432,7 @@ mod test {
432432
let stdb = TestDB::durable()?;
433433

434434
// Step 1: Table with a primary key (requires unique constraint + index).
435-
let module_v1 = {
435+
let module_v1: ModuleDef = {
436436
let mut builder = RawModuleDefV9Builder::new();
437437
builder
438438
.build_table_with_new_type("person", [("name", AlgebraicType::String)], true)
@@ -446,7 +446,7 @@ mod test {
446446
};
447447

448448
// Step 2: Same table, but primary key removed.
449-
let module_v2 = {
449+
let module_v2: ModuleDef = {
450450
let mut builder = RawModuleDefV9Builder::new();
451451
builder
452452
.build_table_with_new_type("person", [("name", AlgebraicType::String)], true)
@@ -580,4 +580,118 @@ mod test {
580580
);
581581
Ok(())
582582
}
583+
584+
/// Verifies that `autoinc` sequence survives a schema migration that adds a column,
585+
/// and is also correctly persisted across database replay.
586+
///
587+
/// Flow:
588+
/// - Create v1 schema and consume a few sequence values.
589+
/// - Migrate to v2 (adds a column with a default).
590+
/// - Ensure next insert continues the sequence (no reset).
591+
/// - Reopen DB and verify allocation cursor is still preserved.
592+
#[test]
593+
fn auto_inc_sequence_survives_add_column_migration() -> anyhow::Result<()> {
594+
let auth_ctx = AuthCtx::for_testing();
595+
let stdb = TestDB::durable()?;
596+
597+
// Define the old module that was before.
598+
let module_v1: ModuleDef = {
599+
let mut b = RawModuleDefV9Builder::new();
600+
b.build_table_with_new_type("seq_t", [("id", AlgebraicType::I64)], true)
601+
.with_auto_inc_primary_key(0)
602+
.with_index_no_accessor_name(RawIndexAlgorithm::BTree { columns: 0.into() })
603+
.with_access(TableAccess::Public)
604+
.finish();
605+
b.finish().try_into().expect("valid module v1")
606+
};
607+
608+
// Define the module that we're migrating to.
609+
let module_v2: ModuleDef = {
610+
let mut b = RawModuleDefV9Builder::new();
611+
b.build_table_with_new_type(
612+
"seq_t",
613+
[("id", AlgebraicType::I64), ("payload", AlgebraicType::U64)],
614+
true,
615+
)
616+
.with_auto_inc_primary_key(0)
617+
.with_index_no_accessor_name(btree(0))
618+
.with_access(TableAccess::Public)
619+
.with_default_column_value(1, product![0u64].into())
620+
.finish();
621+
b.finish().try_into().expect("valid module v2")
622+
};
623+
624+
// helper to insert + collect sorted ids
625+
let insert_and_collect_ids = |stdb: &TestDB, payload: AlgebraicValue| -> anyhow::Result<Vec<i64>> {
626+
let mut tx = begin_mut_tx(stdb);
627+
let table_id = stdb.table_id_from_name_mut(&tx, "seq_t")?.expect("seq_t should exist");
628+
629+
insert(stdb, &mut tx, table_id, &payload)?;
630+
631+
let mut ids = stdb
632+
.iter_mut(&tx, table_id)?
633+
.map(|r| r.read_col::<i64>(0))
634+
.collect::<Result<Vec<_>, _>>()?;
635+
636+
ids.sort();
637+
stdb.commit_tx(tx)?;
638+
Ok(ids)
639+
};
640+
641+
// Create the old tables and insert two rows
642+
// that use the auto-inc sequence.
643+
{
644+
let mut tx = begin_mut_tx(&stdb);
645+
646+
for def in module_v1.tables() {
647+
create_table_from_def(&stdb, &mut tx, &module_v1, def)?;
648+
}
649+
650+
let table_id = stdb.table_id_from_name_mut(&tx, "seq_t")?.expect("seq_t should exist");
651+
652+
insert(&stdb, &mut tx, table_id, &product![0i64])?;
653+
insert(&stdb, &mut tx, table_id, &product![0i64])?;
654+
655+
stdb.commit_tx(tx)?;
656+
}
657+
658+
// Successfully update the database to the new module.
659+
{
660+
let mut tx = begin_mut_tx(&stdb);
661+
662+
let plan = ponder_migrate(&module_v1, &module_v2)?;
663+
let res = update_database(&stdb, &mut tx, auth_ctx, plan, &TestLogger)?;
664+
665+
assert!(matches!(
666+
res,
667+
UpdateResult::Success | UpdateResult::RequiresClientDisconnect
668+
));
669+
670+
stdb.commit_tx(tx)?;
671+
}
672+
673+
// Check that the new table has reused the sequence
674+
// from the old table such that the last row has the value 3.
675+
{
676+
let ids = insert_and_collect_ids(&stdb, product![0i64, 99u64].into())?;
677+
assert!(
678+
ids.iter().last().unwrap() == &3,
679+
"expected id 3 after migration, got {ids:?}"
680+
);
681+
}
682+
683+
// Check that we can replay.
684+
let stdb = stdb.reopen()?;
685+
686+
// After replay, the allocation cursor should be preserved.
687+
{
688+
let ids = insert_and_collect_ids(&stdb, product![0i64, 99u64].into())?;
689+
assert!(
690+
ids.iter().last().unwrap() == &4097,
691+
"expected id 4097 after reopen, got {ids:?}"
692+
);
693+
}
694+
695+
Ok(())
696+
}
583697
}

crates/core/src/host/v8/budget.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use core::ptr;
1313
use core::sync::atomic::Ordering;
1414
use core::time::Duration;
1515
use core::{ffi::c_void, sync::atomic::AtomicBool};
16-
use spacetimedb_client_api_messages::energy::FunctionBudget;
16+
use spacetimedb_client_api_messages::energy::{EnergyQuanta, FunctionBudget};
1717
use std::sync::Arc;
1818
use v8::{Isolate, IsolateHandle};
1919

@@ -118,7 +118,12 @@ fn budget_to_duration(_budget: FunctionBudget) -> Duration {
118118
/// Returns [`EnergyStats`] for a reducer given its `budget`
119119
/// and the `duration` it took to execute.
120120
pub(super) fn energy_from_elapsed(budget: FunctionBudget, duration: Duration) -> EnergyStats {
121-
let used = duration_to_budget(duration);
121+
let used = duration.as_nanos() * EnergyQuanta::PER_EXECUTION_NANOSEC.get();
122+
// in order for duration_nanos * ev_per_ns >= u64::MAX:
123+
// duration_nanos >= u64::MAX / ev_per_ns
124+
// duration_nanos >= (9223372036854775 ns = 106.75 days)
125+
// so it's unlikely we'll have to worry about it
126+
let used = FunctionBudget::new(u64::try_from(used).unwrap_or(u64::MAX));
122127
let remaining = budget - used;
123128
EnergyStats { budget, remaining }
124129
}

0 commit comments

Comments
 (0)