radixdb-api 1.1.0

Embedded database composition API for RadixDB
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
// Copyright 2026 RadixDB Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Narrow library contract used by the network runtime.
//!
//! The TCP state machine owns frames, authentication and cursors. This module
//! prevents it from also depending on executor contexts, MVCC lifecycle enums,
//! physical column storage or the instrumentation implementation.

use std::time::Duration;

use crate::ObjectId;
use crate::{Database, NamedParams, ParamVec};
use radixdb_core::{Error, Result};
use radixdb_executor::context::{CancellationHandle, ExecutionContext};
#[doc(hidden)]
pub use radixdb_executor::procedural::{
    Diagnostic as ServerJobDiagnostic, DiagnosticKind as ServerJobDiagnosticKind,
    JobAttemptMetadata as ServerJobAttemptMetadata, JobAttemptOutcome as ServerJobAttemptOutcome,
    ScheduledJobDefinition as ServerScheduledJobDefinition,
    ScheduledJobSchedule as ServerScheduledJobSchedule,
};
use radixdb_storage::instrumentation::ProtocolColumnBatchFallback;
use radixdb_storage::traits::TypedBatchFallbackReason;
use radixdb_storage::volume::column::ColumnData;

/// Opaque cancellation signal shared by a server session and its requests.
#[derive(Clone, Debug)]
pub struct ServerCancellation {
    pub(crate) inner: CancellationHandle,
}

impl ServerCancellation {
    /// Create an independent cancellation signal.
    pub fn new() -> Self {
        Self {
            inner: ExecutionContext::new().cancellation_handle(),
        }
    }

    /// Request cancellation.
    pub fn cancel(&self) {
        self.inner.cancel();
    }

    /// Return whether cancellation has been requested.
    pub fn is_cancelled(&self) -> bool {
        self.inner.is_cancelled()
    }
}

impl Default for ServerCancellation {
    fn default() -> Self {
        Self::new()
    }
}

/// Opaque request context accepted by embedded facade methods used by server.
pub struct ServerExecutionContext {
    pub(crate) inner: ExecutionContext,
}

impl ServerExecutionContext {
    /// Construct a positional-parameter request context.
    pub fn positional(params: ParamVec) -> Self {
        Self {
            inner: ExecutionContext::with_params(params),
        }
    }

    /// Construct a named-parameter request context.
    pub fn named(params: NamedParams) -> Self {
        Self {
            inner: ExecutionContext::with_named_params(params.into_inner()),
        }
    }

    /// Bind the authenticated session identity and immutable protocol request
    /// ID after user parameters have been admitted.
    pub fn bind_request_identity(&mut self, principal_id: ObjectId, request_id: u64) -> Result<()> {
        self.inner = self.inner.with_principal_id(principal_id);
        self.inner.set_request_id(request_id)
    }

    /// Inherit session/server shutdown cancellation without sharing the
    /// request-local cancellation bit.
    pub fn bind_parent_cancellation(&mut self, parent: &ServerCancellation) {
        self.inner.bind_parent_cancellation(&parent.inner);
    }

    /// Obtain the request-local signal registered under the protocol request ID.
    pub fn cancellation(&self) -> ServerCancellation {
        ServerCancellation {
            inner: self.inner.cancellation_handle(),
        }
    }

    pub(crate) fn inner(&self) -> &ExecutionContext {
        &self.inner
    }

    pub(crate) fn into_inner(self) -> ExecutionContext {
        self.inner
    }
}

/// Stable facade view of a database engine lifecycle.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DatabaseRuntimeState {
    Closed,
    Opening,
    Ready,
    Closing,
    CloseFailed(Error),
    Failed(Error),
}

impl Database {
    /// Snapshot enabled durable Job definitions for the stock scheduler host.
    #[doc(hidden)]
    pub fn scheduled_jobs_snapshot(&self) -> Result<Vec<ServerScheduledJobDefinition>> {
        self.with_connection_executor(|executor| executor.scheduled_jobs_snapshot())
    }

    /// Execute one scheduler-owned attempt through the ordinary procedural
    /// transaction boundary without exposing the executor to the stock server.
    #[doc(hidden)]
    pub fn execute_scheduled_job_attempt(
        &self,
        job_id: ObjectId,
        metadata: ServerJobAttemptMetadata,
        cancellation: &ServerCancellation,
    ) -> std::result::Result<ServerJobAttemptOutcome, ServerJobDiagnostic> {
        self.with_connection_executor(|executor| {
            let mut context = ExecutionContext::new();
            context.bind_parent_cancellation(&cancellation.inner);
            Ok(executor.execute_job_attempt(job_id, metadata, &context))
        })
        .map_err(|error| {
            ServerJobDiagnostic::new(
                ServerJobDiagnosticKind::RuntimeInvalidState,
                error.to_string(),
            )
        })?
    }

    /// Return lifecycle state without exposing the storage engine handle.
    pub fn runtime_state(&self) -> DatabaseRuntimeState {
        match self.engine().lifecycle_state() {
            radixdb_storage::mvcc::engine::EngineLifecycleState::Closed => {
                DatabaseRuntimeState::Closed
            }
            radixdb_storage::mvcc::engine::EngineLifecycleState::Opening => {
                DatabaseRuntimeState::Opening
            }
            radixdb_storage::mvcc::engine::EngineLifecycleState::Ready => {
                DatabaseRuntimeState::Ready
            }
            radixdb_storage::mvcc::engine::EngineLifecycleState::Closing => {
                DatabaseRuntimeState::Closing
            }
            radixdb_storage::mvcc::engine::EngineLifecycleState::CloseFailed(error) => {
                DatabaseRuntimeState::CloseFailed(error)
            }
            radixdb_storage::mvcc::engine::EngineLifecycleState::Failed(error) => {
                DatabaseRuntimeState::Failed(error)
            }
        }
    }
}

/// Typed column value crossing the embedded facade into a transport runtime.
pub enum ServerColumnData {
    Int64 {
        values: Vec<i64>,
        nulls: Vec<bool>,
    },
    Float64 {
        values: Vec<f64>,
        nulls: Vec<bool>,
    },
    TimestampNanos {
        values: Vec<i64>,
        nulls: Vec<bool>,
    },
    Boolean {
        values: Vec<bool>,
        nulls: Vec<bool>,
    },
    DictionaryText {
        ids: Vec<u32>,
        dictionary: Vec<String>,
        nulls: Vec<bool>,
    },
    Bytes {
        data: Vec<u8>,
        offsets: Vec<(u64, u64)>,
        nulls: Vec<bool>,
    },
    JsonText {
        data: Vec<u8>,
        offsets: Vec<(u64, u64)>,
        nulls: Vec<bool>,
    },
    External {
        data: Vec<u8>,
        offsets: Vec<(u64, u64)>,
        type_ref: radixdb_core::ExternalTypeRef,
        nulls: Vec<bool>,
    },
}

/// One decoded, output-ordered column batch independent of storage types.
pub struct ServerColumnBatch {
    row_count: usize,
    columns: Vec<ServerColumnData>,
}

impl ServerColumnBatch {
    pub fn row_count(&self) -> usize {
        self.row_count
    }

    pub fn into_columns(self) -> Vec<ServerColumnData> {
        self.columns
    }

    pub(crate) fn from_storage(batch: radixdb_storage::traits::TypedColumnBatch) -> Result<Self> {
        let row_count = batch.row_count();
        let columns = batch
            .into_columns()
            .into_iter()
            .map(server_column_from_storage)
            .collect::<Result<Vec<_>>>()?;
        Ok(Self { row_count, columns })
    }
}

fn server_column_from_storage(column: ColumnData) -> Result<ServerColumnData> {
    match column {
        ColumnData::Int64 { values, nulls } => Ok(ServerColumnData::Int64 { values, nulls }),
        ColumnData::Float64 { values, nulls } => Ok(ServerColumnData::Float64 { values, nulls }),
        ColumnData::TimestampNanos { values, nulls } => {
            Ok(ServerColumnData::TimestampNanos { values, nulls })
        }
        ColumnData::Boolean { values, nulls } => Ok(ServerColumnData::Boolean { values, nulls }),
        ColumnData::Dictionary {
            ids,
            dictionary,
            nulls,
        } => Ok(ServerColumnData::DictionaryText {
            ids,
            dictionary: dictionary.iter().map(ToString::to_string).collect(),
            nulls,
        }),
        ColumnData::Bytes {
            data,
            offsets,
            ext_type: radixdb_core::DataType::Bytes,
            nulls,
        } => Ok(ServerColumnData::Bytes {
            data,
            offsets,
            nulls,
        }),
        ColumnData::Bytes {
            data,
            offsets,
            ext_type: radixdb_core::DataType::Json,
            nulls,
        } => Ok(ServerColumnData::JsonText {
            data,
            offsets,
            nulls,
        }),
        ColumnData::Bytes { ext_type, .. } => Err(Error::NotSupported(format!(
            "typed column protocol does not yet support {ext_type}"
        ))),
        ColumnData::External {
            data,
            offsets,
            type_ref,
            nulls,
        } => Ok(ServerColumnData::External {
            data,
            offsets,
            type_ref,
            nulls,
        }),
    }
}

/// Coarse transport-facing reason why a typed column batch is unavailable.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ServerBatchFallback {
    RowState,
    QueryShape,
    StorageShape,
    Schema,
}

impl ServerBatchFallback {
    pub(crate) fn from_storage(reason: TypedBatchFallbackReason) -> Self {
        match reason {
            TypedBatchFallbackReason::RowAlreadyFetched
            | TypedBatchFallbackReason::RowIterationStarted
            | TypedBatchFallbackReason::Closed
            | TypedBatchFallbackReason::PendingError => Self::RowState,
            TypedBatchFallbackReason::RowFilter
            | TypedBatchFallbackReason::DictionaryFilter
            | TypedBatchFallbackReason::IndexSelection
            | TypedBatchFallbackReason::TypedPredicate
            | TypedBatchFallbackReason::ExactTypedFilter
            | TypedBatchFallbackReason::FilterCoveredByTypedPredicates
            | TypedBatchFallbackReason::RowGroupSkips
            | TypedBatchFallbackReason::EmptyProjection
            | TypedBatchFallbackReason::DuplicateProjection
            | TypedBatchFallbackReason::MergedSource
            | TypedBatchFallbackReason::MixedTypedAndRowSources
            | TypedBatchFallbackReason::UnsupportedResultShape => Self::QueryShape,
            TypedBatchFallbackReason::NotArtifactBacked
            | TypedBatchFallbackReason::UnsupportedStorageType
            | TypedBatchFallbackReason::InvalidRange => Self::StorageShape,
            TypedBatchFallbackReason::SchemaMappingMissingColumn
            | TypedBatchFallbackReason::UnsupportedSchemaDefault => Self::Schema,
        }
    }
}

/// Instrumentation adapter kept outside the network state machine.
pub struct ServerRuntimeMetrics;

impl ServerRuntimeMetrics {
    pub fn connection_opened() {
        radixdb_storage::instrumentation::record_server_connection_opened();
    }

    pub fn connection_closed() {
        radixdb_storage::instrumentation::record_server_connection_closed();
    }

    pub fn execution_started() {
        radixdb_storage::instrumentation::record_server_execution_started();
    }

    pub fn execution_finished() {
        radixdb_storage::instrumentation::record_server_execution_finished();
    }

    pub fn replace_session_owners(
        before_sessions: u64,
        after_sessions: u64,
        before_cursors: u64,
        after_cursors: u64,
        before_prepared: u64,
        after_prepared: u64,
    ) {
        radixdb_storage::instrumentation::replace_server_session_owners(
            before_sessions,
            after_sessions,
            before_cursors,
            after_cursors,
            before_prepared,
            after_prepared,
        );
    }

    pub fn flush_thread_local() {
        radixdb_storage::instrumentation::flush_thread_local_counters();
    }

    pub fn protocol_encode(bytes: u64, elapsed: Duration) {
        radixdb_storage::instrumentation::record_protocol_encode(bytes, elapsed);
    }

    pub fn protocol_socket_write(bytes: u64, elapsed: Duration) {
        radixdb_storage::instrumentation::record_protocol_socket_write(bytes, elapsed);
    }

    pub fn protocol_round_trip(elapsed: Duration) {
        radixdb_storage::instrumentation::record_protocol_round_trip(elapsed);
    }

    pub fn protocol_row_adapter(values: u64) {
        radixdb_storage::instrumentation::record_protocol_row_adapter(values);
    }

    pub fn protocol_result_rows(rows: u64) {
        radixdb_storage::instrumentation::record_protocol_result_rows(rows);
    }

    pub fn protocol_row_batch(rows: u64) {
        radixdb_storage::instrumentation::record_protocol_row_batch(rows);
    }

    pub fn protocol_column_batch_fallback(reason: ServerBatchFallback) {
        let reason = match reason {
            ServerBatchFallback::RowState => ProtocolColumnBatchFallback::RowState,
            ServerBatchFallback::QueryShape => ProtocolColumnBatchFallback::QueryShape,
            ServerBatchFallback::StorageShape => ProtocolColumnBatchFallback::StorageShape,
            ServerBatchFallback::Schema => ProtocolColumnBatchFallback::Schema,
        };
        radixdb_storage::instrumentation::record_protocol_column_batch_fallback(reason);
    }

    pub fn column_batch_pending_opened(rows: u64, retained_bytes: u64) {
        radixdb_storage::instrumentation::record_protocol_column_batch_pending_opened(
            rows,
            retained_bytes,
        );
    }

    pub fn column_batch_pending_completed(rows: u64, retained_bytes: u64) {
        radixdb_storage::instrumentation::record_protocol_column_batch_pending_completed(
            rows,
            retained_bytes,
        );
    }

    pub fn column_batch_pending_dropped(rows: u64, retained_bytes: u64) {
        radixdb_storage::instrumentation::record_protocol_column_batch_pending_dropped(
            rows,
            retained_bytes,
        );
    }
}

/// Storage tuning values admitted by server configuration without exposing
/// the physical storage module to the network runtime.
pub struct ServerStorageContract;

impl ServerStorageContract {
    pub const DEFAULT_COPY_MAX_TRANSACTION_BYTES: usize =
        radixdb_storage::config::DEFAULT_COPY_MAX_TRANSACTION_BYTES;
    pub const DEFAULT_MAX_COMPACTION_JOBS: usize =
        radixdb_storage::config::DEFAULT_MAX_COMPACTION_JOBS;
    pub const MAX_COMPACTION_JOBS: usize = radixdb_storage::config::MAX_COMPACTION_JOBS;
    pub const DEFAULT_STORAGE_CPU_WORKERS: usize =
        radixdb_storage::config::DEFAULT_STORAGE_CPU_WORKERS;
    pub const DEFAULT_PAGE_CACHE_LEVEL: u8 = radixdb_storage::config::DEFAULT_PAGE_CACHE_LEVEL;
    pub const MAX_PAGE_CACHE_LEVEL: u8 = radixdb_storage::config::MAX_PAGE_CACHE_LEVEL;
    pub const DEFAULT_PAGE_CACHE_MAX_BYTES: u64 =
        radixdb_storage::config::DEFAULT_PAGE_CACHE_MAX_BYTES;
    pub const DEFAULT_PAGE_CACHE_MEMORY_RESERVE: u64 =
        radixdb_storage::config::DEFAULT_PAGE_CACHE_MEMORY_RESERVE;
}

/// Credential primitives admitted by server configuration without exposing
/// the executor implementation crate to the network runtime.
pub struct ServerCredentialContract;

impl ServerCredentialContract {
    pub fn validate_password_verifier(encoded: &str) -> Result<()> {
        radixdb_executor::credentials::validate_password_verifier(encoded)
    }

    pub fn verify_password_verifier(encoded: &str, password: &str) -> bool {
        radixdb_executor::credentials::verify_password_verifier(encoded, password)
    }

    pub fn hash_password_verifier(password: &str) -> Result<String> {
        radixdb_executor::credentials::hash_password_verifier(password)
    }
}

/// Classify transaction-lifecycle SQL without granting the server parser/AST ownership.
pub fn sql_contains_transaction_control(sql: &str) -> bool {
    radixdb_executor::program_contains_transaction_control(sql)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn mixed_typed_sources_are_a_query_shape_fallback() {
        assert_eq!(
            ServerBatchFallback::from_storage(TypedBatchFallbackReason::MixedTypedAndRowSources),
            ServerBatchFallback::QueryShape
        );
    }
}