use std::collections::{HashMap, HashSet};
use std::fmt;
use anyhow::Result;
use async_channel::Sender;
use surrealdb_rpc::export::Config;
use surrealdb_types::ToSql;
use super::Transaction;
use crate::catalog::providers::{
ApiProvider, AuthorisationProvider, BucketProvider, DatabaseProvider, TableProvider,
UserProvider,
};
use crate::catalog::{
DatabaseId, Error, NamespaceId, Record, TableDefinition, TableType, ViewDefinition,
};
use crate::expr::access::AccessDuration;
use crate::expr::access_type::{
AccessType, BearerAccess, BearerAccessSubject, BearerAccessType, JwtAccess, JwtAccessIssue,
JwtAccessVerify, JwtAccessVerifyJwks, JwtAccessVerifyKey, RecordAccess,
};
use crate::expr::paths::{IN, OUT};
use crate::expr::statements::define::{DefineAccessStatement, DefineKind, DefineUserStatement};
use crate::expr::user::UserDuration;
use crate::expr::{Algorithm, Base, DefineAnalyzerStatement, Expr, Idiom, Literal};
use crate::idx::IndexKeyBase;
use crate::idx::ft::fulltext::mean_tokens_per_document;
use crate::key::schema::{RecordKey, RecordPrefix};
use crate::key::{KVKeyDecode, KVSubspace, KVValue};
use crate::kvs::sequences::next_unissued_value;
use crate::sql::statements::OptionStatement;
use crate::sql::statements::define::{DefineDatabaseStatement, DefineKind as SqlDefineKind};
use crate::sql::{Expr as SqlExpr, Idiom as SqlIdiom, Literal as SqlLiteral, Param, Part};
use crate::val::TableName;
use crate::{catalog, val};
struct InlineCommentWriter<'a, F>(&'a mut F);
impl<F: fmt::Write> fmt::Write for InlineCommentWriter<'_, F> {
fn write_str(&mut self, s: &str) -> fmt::Result {
for c in s.chars() {
self.write_char(c)?
}
Ok(())
}
fn write_char(&mut self, c: char) -> fmt::Result {
match c {
'\n' => self.0.write_str("\\n"),
'\r' => self.0.write_str("\\r"),
'\u{0085}' => self.0.write_str("\\u{0085}"),
'\u{2028}' => self.0.write_str("\\u{2028}"),
'\u{2029}' => self.0.write_str("\\u{2029}"),
_ => self.0.write_char(c),
}
}
}
struct InlineCommentDisplay<F>(F);
impl<F: fmt::Display> fmt::Display for InlineCommentDisplay<F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Write::write_fmt(&mut InlineCommentWriter(f), format_args!("{}", self.0))
}
}
struct DefineConfig<'a>(&'a catalog::ConfigDefinition);
impl ToSql for DefineConfig<'_> {
fn fmt_sql(&self, f: &mut String, fmt: surrealdb_types::SqlFormat) {
f.push_str("DEFINE CONFIG OVERWRITE ");
self.0.fmt_sql(f, fmt);
}
}
pub(crate) async fn export(
tx: &Transaction,
ns: &str,
db: &str,
cfg: Config,
batch_size: u32,
chn: Sender<Vec<u8>>,
) -> Result<()> {
let db = tx.get_db_by_name(ns, db, None).await?.ok_or_else(|| {
anyhow::Error::new(Error::DbNotFound {
name: db.to_owned(),
})
})?;
export_metadata(tx, &cfg, &chn, &db).await?;
export_tables(tx, &cfg, &chn, db.namespace_id, db.database_id, batch_size).await?;
Ok(())
}
fn define_database_statement_from_definition(
def: &catalog::DatabaseDefinition,
) -> Option<DefineDatabaseStatement> {
if def.comment.is_none() && def.changefeed.is_none() && !def.strict {
return None;
}
Some(DefineDatabaseStatement {
kind: SqlDefineKind::Overwrite,
id: None,
name: SqlExpr::Idiom(SqlIdiom(vec![
Part::Start(SqlExpr::Param(Param::new("session"))),
Part::Field("db".into()),
])),
strict: def.strict,
comment: def
.comment
.clone()
.map(|v| SqlExpr::Literal(SqlLiteral::String(v.into())))
.unwrap_or(SqlExpr::Literal(SqlLiteral::None)),
changefeed: def.changefeed.map(|v| v.into()),
})
}
async fn export_metadata(
tx: &Transaction,
cfg: &Config,
chn: &Sender<Vec<u8>>,
def: &catalog::DatabaseDefinition,
) -> Result<()> {
let (ns, db) = (def.namespace_id, def.database_id);
export_section("OPTION", [OptionStatement::import()].into_iter(), chn).await?;
if cfg.database_definition
&& let Some(stmt) = define_database_statement_from_definition(def)
{
export_section("DATABASE", std::iter::once(stmt), chn).await?;
}
if cfg.users {
let users = tx.all_db_users(ns, db, None).await?;
export_section(
"USERS",
users.iter().map(|x| define_user_statement_from_definition(Base::Db, x)),
chn,
)
.await?;
}
if cfg.accesses {
let accesses = tx.all_db_accesses(ns, db, None).await?;
export_section(
"ACCESSES",
accesses.iter().map(|x| define_access_statement_from_definition(Base::Db, x).redact()),
chn,
)
.await?;
}
if cfg.params {
let params = tx.all_db_params(ns, db, None).await?;
export_section("PARAMS", params.iter(), chn).await?;
}
if cfg.functions {
let functions = tx.all_db_functions(ns, db, None).await?;
export_section("FUNCTIONS", functions.iter(), chn).await?;
}
if cfg.analyzers {
let analyzers = tx.all_db_analyzers(ns, db, None).await?;
export_section(
"ANALYZERS",
analyzers.iter().map(define_analyzer_statement_from_definition),
chn,
)
.await?;
}
if cfg.apis {
let apis = tx.all_db_apis(ns, db, None).await?;
export_section("APIS", apis.iter(), chn).await?;
}
if cfg.buckets {
let buckets = tx.all_db_buckets(ns, db, None).await?;
export_section("BUCKETS", buckets.iter(), chn).await?;
}
if cfg.modules {
let modules = tx.all_db_modules(ns, db, None).await?;
export_section("MODULES", modules.iter(), chn).await?;
}
if cfg.configs {
let configs = tx.all_db_configs(ns, db, None).await?;
export_section("CONFIGS", configs.iter().map(DefineConfig), chn).await?;
}
if cfg.sequences {
let sequences = tx.all_db_sequences(ns, db, None).await?;
let mut positioned = Vec::with_capacity(sequences.len());
for sq in sequences.iter() {
let mut sq = sq.clone();
sq.start = next_unissued_value(tx, ns, db, sq.name.as_str(), sq.start, None).await?;
positioned.push(sq);
}
export_section("SEQUENCES", positioned.iter(), chn).await?;
}
Ok(())
}
async fn export_section<T>(
title: &str,
items: impl ExactSizeIterator<Item = T>,
chn: &Sender<Vec<u8>>,
) -> Result<()>
where
T: ToSql,
{
if items.len() == 0 {
return Ok(());
}
chn.send(bytes!("-- ------------------------------")).await?;
chn.send(bytes!(format!("-- {}", InlineCommentDisplay(title)))).await?;
chn.send(bytes!("-- ------------------------------")).await?;
chn.send(bytes!("")).await?;
for item in items {
chn.send(bytes!(format!("{};", item.to_sql()))).await?;
}
chn.send(bytes!("")).await?;
Ok(())
}
async fn export_tables(
tx: &Transaction,
cfg: &Config,
chn: &Sender<Vec<u8>>,
ns: NamespaceId,
db: DatabaseId,
batch_size: u32,
) -> Result<()> {
if !cfg.tables.is_any() {
return Ok(());
}
let tables = tx.all_tb(ns, db, None).await?;
if let Some(names) = cfg.tables.names() {
let existing: Vec<&str> = tables.iter().map(|t| t.name.as_str()).collect();
for name in names {
if !existing.contains(&name.as_str()) {
warn!("Table '{name}' does not exist in the database");
}
}
}
let (views, stored): (Vec<&TableDefinition>, Vec<&TableDefinition>) = tables
.iter()
.filter(|t| cfg.tables.includes(t.name.as_str()))
.partition(|t| t.view.is_some());
let emitted: HashSet<&str> =
stored.iter().chain(views.iter()).map(|t| t.name.as_str()).collect();
let view_sources: HashSet<&str> = views
.iter()
.filter_map(|table| table.view.as_ref())
.flat_map(|view| view.source_tables())
.map(|name| name.as_str())
.collect();
let (read_by_a_view, unread): (Vec<&TableDefinition>, Vec<&TableDefinition>) =
stored.into_iter().partition(|table| view_sources.contains(table.name.as_str()));
for table in &read_by_a_view {
export_table_structure(tx, ns, db, table, chn).await?;
if cfg.records {
export_table_data(tx, ns, db, table, chn, batch_size).await?;
}
}
for (table, deferral) in order_views(&views) {
if let Some(reason) = unreplayable_view(table, &emitted, deferral) {
warn!("{reason}");
chn.send(bytes!("-- ------------------------------")).await?;
chn.send(bytes!(format!("-- NOTE: {}.", InlineCommentDisplay(&reason)))).await?;
chn.send(bytes!("-- ------------------------------")).await?;
chn.send(bytes!("")).await?;
}
let fallback = TableDefinition {
view: None,
schemafull: false,
drop: false,
table_type: TableType::Any,
..table.clone()
};
chn.send(bytes!(format!("{};", fallback.to_sql()))).await?;
chn.send(bytes!("")).await?;
if cfg.records {
export_table_data(tx, ns, db, table, chn, batch_size).await?;
}
export_table_structure(tx, ns, db, table, chn).await?;
}
for table in &unread {
export_table_structure(tx, ns, db, table, chn).await?;
if cfg.records {
export_table_data(tx, ns, db, table, chn, batch_size).await?;
}
}
Ok(())
}
fn unreplayable_view(
table: &TableDefinition,
emitted: &HashSet<&str>,
deferral: Deferral,
) -> Option<String> {
let view = table.view.as_ref()?;
if matches!(view, ViewDefinition::Select { .. }) {
return Some(format!(
"Table '{}' is a view this version can no longer maintain, so its definition cannot \
be replayed",
InlineCommentDisplay(&table.name)
));
}
match deferral {
Deferral::Cycle => {
return Some(format!(
"Table '{}' is a view in a definition cycle, which no order of this dump can \
replay",
InlineCommentDisplay(&table.name)
));
}
Deferral::BehindCycle => {
return Some(format!(
"Table '{}' is a view reading a definition cycle, so this dump defines a table it \
selects from after it",
InlineCommentDisplay(&table.name)
));
}
Deferral::None => {}
}
let missing: Vec<String> = view
.source_tables()
.iter()
.filter(|t| !emitted.contains(t.as_str()))
.map(|t| format!("'{}'", InlineCommentDisplay(t)))
.collect();
if missing.is_empty() {
return None;
}
Some(format!(
"Table '{}' is a view over {}, which this export does not carry",
InlineCommentDisplay(&table.name),
missing.join(", ")
))
}
fn order_views<'a>(views: &[&'a TableDefinition]) -> Vec<(&'a TableDefinition, Deferral)> {
let graph: Vec<(&str, &[TableName])> = views
.iter()
.map(|t| (t.name.as_str(), t.view.as_ref().map_or(&[][..], |v| v.source_tables())))
.collect();
let order = view_order(&graph);
order
.order
.iter()
.map(|&i| {
let deferral = if order.cyclic.contains(&i) {
Deferral::Cycle
} else if order.behind_cycle.contains(&i) {
Deferral::BehindCycle
} else {
Deferral::None
};
(views[i], deferral)
})
.collect()
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Deferral {
None,
Cycle,
BehindCycle,
}
struct ViewOrder {
order: Vec<usize>,
cyclic: HashSet<usize>,
behind_cycle: HashSet<usize>,
}
fn view_order(views: &[(&str, &[TableName])]) -> ViewOrder {
let names: HashSet<&str> = views.iter().map(|(name, _)| *name).collect();
let mut emitted: HashSet<&str> = HashSet::new();
let mut ordered: Vec<usize> = Vec::with_capacity(views.len());
let mut pending: Vec<usize> = (0..views.len()).collect();
while !pending.is_empty() {
let placed = ordered.len();
let mut deferred = Vec::new();
for i in pending {
let (name, sources) = views[i];
let ready =
sources.iter().all(|s| !names.contains(s.as_str()) || emitted.contains(s.as_str()));
if ready {
emitted.insert(name);
ordered.push(i);
} else {
deferred.push(i);
}
}
if ordered.len() == placed {
let cyclic = cyclic_members(views, &deferred);
let behind_cycle = deferred.iter().copied().filter(|i| !cyclic.contains(i)).collect();
ordered.extend(deferred);
return ViewOrder {
order: ordered,
cyclic,
behind_cycle,
};
}
pending = deferred;
}
ViewOrder {
order: ordered,
cyclic: HashSet::new(),
behind_cycle: HashSet::new(),
}
}
fn cyclic_members(views: &[(&str, &[TableName])], stuck: &[usize]) -> HashSet<usize> {
let position: HashMap<&str, usize> = stuck.iter().map(|&i| (views[i].0, i)).collect();
let mut cyclic = HashSet::new();
for &start in stuck {
let mut seen: HashSet<usize> = HashSet::new();
let mut frontier = vec![start];
while let Some(i) = frontier.pop() {
for source in views[i].1 {
let Some(&next) = position.get(source.as_str()) else {
continue;
};
if next == start {
cyclic.insert(start);
frontier.clear();
break;
}
if seen.insert(next) {
frontier.push(next);
}
}
}
}
cyclic
}
async fn export_table_structure(
tx: &Transaction,
ns: NamespaceId,
db: DatabaseId,
table: &TableDefinition,
chn: &Sender<Vec<u8>>,
) -> Result<()> {
chn.send(bytes!("-- ------------------------------")).await?;
chn.send(bytes!(format!("-- TABLE: {}", InlineCommentDisplay(&table.name)))).await?;
chn.send(bytes!("-- ------------------------------")).await?;
chn.send(bytes!("")).await?;
chn.send(bytes!(format!("{};", table.to_sql()))).await?;
chn.send(bytes!("")).await?;
let tb_name = table.name.clone();
if crate::kvs::lightweight::lightweight_relation(&table.table_type).is_none() {
let fields = tx.all_tb_fields(ns, db, &tb_name, None).await?;
for field in fields.iter() {
chn.send(bytes!(format!("{};", field.to_sql_overwrite()))).await?;
}
}
chn.send(bytes!("")).await?;
let indexes = tx.all_tb_indexes(ns, db, &tb_name, None).await?;
for index in indexes.iter() {
chn.send(bytes!(format!("{};", index.to_sql()))).await?;
}
chn.send(bytes!("")).await?;
let events = tx.all_tb_events(ns, db, &tb_name, None).await?;
for event in events.iter() {
chn.send(bytes!(format!("{};", event.to_sql()))).await?;
}
chn.send(bytes!("")).await?;
Ok(())
}
const INSERT_KEY_BUDGET: usize = 20_000;
const KEYS_PER_STATEMENT: usize = 32;
const KEYS_PER_RECORD: usize = 2;
const KEYS_PER_VALUE_INDEX: usize = 2;
const KEYS_PER_REFERENCE: usize = 2;
const KEYS_PER_FULLTEXT_TERM: usize = 1;
const KEYS_PER_FULLTEXT_RECORD: usize = 4;
const KEYS_PER_CONTENT_INDEX_FALLBACK: usize = 256;
const FAN_OUT_SAMPLE: usize = 8;
fn index_fan_out(data: &val::Value, cols: &[Idiom]) -> usize {
fn entries(v: &val::Value) -> usize {
match v {
val::Value::Array(a) => a.len().max(1),
val::Value::Set(set) => set.len().max(1),
_ => 1,
}
}
cols.iter()
.map(|col| {
let mut entries_for_column = 0;
data.walk_ref(col, &mut |v| entries_for_column += entries(v));
entries_for_column.max(1)
})
.max()
.unwrap_or(1)
}
fn reference_fan_out(data: &val::Value, name: &Idiom) -> usize {
fn record_ids(v: &val::Value) -> usize {
match v {
val::Value::Array(a) => a.iter().filter(|v| v.is_record()).count(),
val::Value::Set(set) => set.iter().filter(|v| v.is_record()).count(),
val::Value::RecordId(_) => 1,
_ => 0,
}
}
let mut ids = 0;
data.walk_ref(name, &mut |v| ids += record_ids(v));
ids.max(1)
}
async fn keys_per_record(
tx: &Transaction,
ns: NamespaceId,
db: DatabaseId,
table: &TableName,
indexes: &[catalog::IndexDefinition],
fields: &[catalog::FieldDefinition],
sample: &[val::Value],
) -> Result<usize> {
let mut keys = KEYS_PER_RECORD;
for fd in fields.iter().filter(|fd| fd.reference.is_some()) {
let fan_out =
sample.iter().map(|data| reference_fan_out(data, &fd.name)).max().unwrap_or(1);
keys = keys.saturating_add(KEYS_PER_REFERENCE.saturating_mul(fan_out));
}
for ix in indexes {
keys = keys.saturating_add(match ix.index {
catalog::Index::FullText(_) => {
let ikb = IndexKeyBase::new(ns, db, table.clone(), ix.index_id);
match mean_tokens_per_document(tx, &ikb).await? {
Some(mean) => KEYS_PER_FULLTEXT_RECORD
.saturating_add((mean as usize).saturating_mul(KEYS_PER_FULLTEXT_TERM)),
None => KEYS_PER_CONTENT_INDEX_FALLBACK,
}
}
catalog::Index::Hnsw(_) | catalog::Index::DiskAnn(_) => KEYS_PER_CONTENT_INDEX_FALLBACK,
catalog::Index::Idx | catalog::Index::Uniq | catalog::Index::Count(_) => {
let fan_out =
sample.iter().map(|data| index_fan_out(data, &ix.cols)).max().unwrap_or(1);
KEYS_PER_VALUE_INDEX.saturating_mul(fan_out)
}
});
}
Ok(keys)
}
fn decode_sample(batch: &[(Vec<u8>, Vec<u8>)], limit: usize) -> Result<Vec<val::Value>> {
batch
.iter()
.take(limit)
.map(|(k, v)| {
let k = RecordKey::decode_key(k)?;
let rid = crate::val::RecordId {
table: k.tb.into_owned(),
key: k.id.into_owned(),
};
Ok(Record::kv_decode_value(v, rid)?.data)
})
.collect()
}
fn records_per_insert(keys_per_record: usize, batch_size: u32) -> usize {
let budget = INSERT_KEY_BUDGET.saturating_sub(KEYS_PER_STATEMENT);
(budget / keys_per_record.max(1)).clamp(1, batch_size.max(1) as usize)
}
async fn export_table_data(
tx: &Transaction,
ns: NamespaceId,
db: DatabaseId,
table: &TableDefinition,
chn: &Sender<Vec<u8>>,
batch_size: u32,
) -> Result<()> {
chn.send(bytes!("-- ------------------------------")).await?;
chn.send(bytes!(format!("-- TABLE DATA: {}", InlineCommentDisplay(&table.name)))).await?;
chn.send(bytes!("-- ------------------------------")).await?;
chn.send(bytes!("")).await?;
let tb_name = table.name.clone();
if let Some(rel) = crate::kvs::lightweight::lightweight_relation(&table.table_type) {
let mut scanner = crate::kvs::lightweight::LightweightEdgeScanner::new(
tx,
ns,
db,
&tb_name,
rel,
surrealdb_kvs::Direction::Forward,
None,
None,
);
loop {
let batch = scanner.next_batch(batch_size).await?;
if batch.is_empty() {
break;
}
let mut statements = String::new();
for edge in &batch {
let Some((l, r)) = crate::kvs::lightweight::edge_parts(&edge.key) else {
continue;
};
statements.push_str(&format!(
"RELATE {} -> {} -> {};\n",
l.to_sql(),
surrealdb_types::ToSql::to_sql(&crate::val::Value::Table(tb_name.clone())),
r.to_sql()
));
}
chn.send(bytes!(statements)).await?;
}
chn.send(bytes!("")).await?;
return Ok(());
}
let indexes = tx.all_tb_indexes(ns, db, &tb_name, None).await?;
let fields = tx.all_tb_fields(ns, db, &tb_name, None).await?;
let records = RecordPrefix {
ns,
db,
tb: std::borrow::Cow::Borrowed(&tb_name),
};
let mut next = Some(records.range()?);
while let Some(rng) = next {
let batch = tx.batch_keys_vals_raw(rng, batch_size, None).await?;
next = batch.next.map(|rng| records.raw(rng));
if batch.result.is_empty() {
break;
}
let sample = decode_sample(&batch.result, FAN_OUT_SAMPLE)?;
let per_insert = records_per_insert(
keys_per_record(tx, ns, db, &tb_name, &indexes, &fields, &sample).await?,
batch_size,
);
for group in batch.result.chunks(per_insert) {
export_regular_data(group, chn).await?;
}
}
chn.send(bytes!("")).await?;
Ok(())
}
fn process_record(record: &Record, records_relate: &mut String, records_normal: &mut String) {
if record.is_edge()
&& let crate::val::Value::RecordId(_) = record.data.pick(&IN)
&& let crate::val::Value::RecordId(_) = record.data.pick(&OUT)
{
if !records_relate.is_empty() {
records_relate.push_str(", ");
}
records_relate.push_str(&record.data.to_sql());
} else {
if !records_normal.is_empty() {
records_normal.push_str(", ");
}
records_normal.push_str(&record.data.to_sql());
}
}
async fn export_regular_data(
regular_values: &[(Vec<u8>, Vec<u8>)],
chn: &Sender<Vec<u8>>,
) -> Result<()> {
let mut records_normal = String::new();
let mut records_relate = String::new();
for (k, v) in regular_values {
let k = RecordKey::decode_key(k)?;
let rid = crate::val::RecordId {
table: k.tb.into_owned(),
key: k.id.into_owned(),
};
let v = Record::kv_decode_value(v, rid)?;
process_record(&v, &mut records_relate, &mut records_normal);
}
if !records_normal.is_empty() {
let sql = format!("INSERT [ {} ];", records_normal);
chn.send(bytes!(sql)).await?;
}
if !records_relate.is_empty() {
let sql = format!("INSERT RELATION [ {} ];", records_relate);
chn.send(bytes!(sql)).await?;
}
Ok(())
}
pub(crate) fn define_access_statement_from_definition(
base: Base,
def: &catalog::AccessDefinition,
) -> DefineAccessStatement {
fn convert_algorithm(access: catalog::Algorithm) -> Algorithm {
match &access {
catalog::Algorithm::EdDSA => Algorithm::EdDSA,
catalog::Algorithm::Es256 => Algorithm::Es256,
catalog::Algorithm::Es384 => Algorithm::Es384,
catalog::Algorithm::Es512 => Algorithm::Es512,
catalog::Algorithm::Hs256 => Algorithm::Hs256,
catalog::Algorithm::Hs384 => Algorithm::Hs384,
catalog::Algorithm::Hs512 => Algorithm::Hs512,
catalog::Algorithm::Ps256 => Algorithm::Ps256,
catalog::Algorithm::Ps384 => Algorithm::Ps384,
catalog::Algorithm::Ps512 => Algorithm::Ps512,
catalog::Algorithm::Rs256 => Algorithm::Rs256,
catalog::Algorithm::Rs384 => Algorithm::Rs384,
catalog::Algorithm::Rs512 => Algorithm::Rs512,
}
}
fn convert_jwt_access(access: &catalog::JwtAccess) -> JwtAccess {
JwtAccess {
verify: match &access.verify {
catalog::JwtAccessVerify::Key(k) => JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: convert_algorithm(k.alg),
key: Expr::Literal(Literal::String(k.key.as_str().into())),
}),
catalog::JwtAccessVerify::Jwks(j) => JwtAccessVerify::Jwks(JwtAccessVerifyJwks {
url: Expr::Literal(Literal::String(j.url.as_str().into())),
}),
},
issue: access.issue.as_ref().map(|x| JwtAccessIssue {
alg: convert_algorithm(x.alg),
key: Expr::Literal(Literal::String(x.key.as_str().into())),
}),
audience: access.audience.as_ref().map(|a| {
a.iter().map(|s| Expr::Literal(Literal::String(s.as_str().into()))).collect()
}),
}
}
fn convert_bearer_access(access: &catalog::BearerAccess) -> BearerAccess {
BearerAccess {
kind: match access.kind {
catalog::BearerAccessType::Bearer => BearerAccessType::Bearer,
catalog::BearerAccessType::Refresh => BearerAccessType::Refresh,
},
subject: match access.subject {
catalog::BearerAccessSubject::Record => BearerAccessSubject::Record,
catalog::BearerAccessSubject::User => BearerAccessSubject::User,
},
jwt: convert_jwt_access(&access.jwt),
}
}
DefineAccessStatement {
kind: DefineKind::Default,
base,
name: Expr::Idiom(Idiom::field(def.name.clone())),
duration: AccessDuration {
grant: def
.grant_duration
.map(|v| Expr::Literal(Literal::Duration(val::Duration(v))))
.unwrap_or(Expr::Literal(Literal::None)),
token: def
.token_duration
.map(|v| Expr::Literal(Literal::Duration(val::Duration(v))))
.unwrap_or(Expr::Literal(Literal::None)),
session: def
.session_duration
.map(|v| Expr::Literal(Literal::Duration(val::Duration(v))))
.unwrap_or(Expr::Literal(Literal::None)),
},
comment: def
.comment
.clone()
.map(|x| Expr::Literal(Literal::String(x.into())))
.unwrap_or(Expr::Literal(Literal::None)),
authenticate: def.authenticate.clone(),
context: def.context.clone(),
access_type: match &def.access_type {
catalog::AccessType::Record(record_access) => {
AccessType::Record(Box::new(RecordAccess {
signup: def.signup.clone(),
signin: def.signin.clone(),
jwt: convert_jwt_access(&record_access.jwt),
bearer: record_access.bearer.as_ref().map(convert_bearer_access),
}))
}
catalog::AccessType::Jwt(jwt_access) => AccessType::Jwt(convert_jwt_access(jwt_access)),
catalog::AccessType::Bearer(bearer_access) => {
AccessType::Bearer(convert_bearer_access(bearer_access))
}
},
}
}
pub(crate) fn define_analyzer_statement_from_definition(
def: &catalog::AnalyzerDefinition,
) -> DefineAnalyzerStatement {
DefineAnalyzerStatement {
kind: DefineKind::Default,
name: Expr::Idiom(Idiom::field(def.name.clone())),
function: def.function.clone(),
tokenizers: def.tokenizers.clone(),
filters: def.filters.clone(),
comment: def
.comment
.as_ref()
.map(|x| Expr::Literal(Literal::String(x.as_str().into())))
.unwrap_or(Expr::Literal(Literal::None)),
}
}
pub(crate) fn define_user_statement_from_definition(
base: Base,
def: &catalog::UserDefinition,
) -> DefineUserStatement {
DefineUserStatement {
kind: DefineKind::Default,
base,
name: Expr::Idiom(Idiom::field(def.name.clone())),
hash: def.hash.clone(),
code: def.code.clone(),
roles: def.roles.clone(),
duration: UserDuration {
token: def
.token_duration
.map(|x| Expr::Literal(Literal::Duration(val::Duration(x))))
.unwrap_or(Expr::Literal(Literal::None)),
session: def
.session_duration
.map(|x| Expr::Literal(Literal::Duration(val::Duration(x))))
.unwrap_or(Expr::Literal(Literal::None)),
},
comment: def
.comment
.as_ref()
.map(|x| Expr::Literal(Literal::String(x.as_str().into())))
.unwrap_or(Expr::Literal(Literal::None)),
scram: def.scram.clone(),
}
}
#[cfg(test)]
mod tests {
use super::{HashSet, INSERT_KEY_BUDGET, TableName, records_per_insert, view_order};
const STATEMENT_FLOOR: usize = 2;
#[test]
fn a_group_leaves_room_for_what_the_statement_itself_writes() {
let per_record = 100;
assert_eq!(INSERT_KEY_BUDGET % per_record, 0, "the case only bites on an exact division");
let group = records_per_insert(per_record, u32::MAX);
assert!(
group * per_record + STATEMENT_FLOOR <= INSERT_KEY_BUDGET,
"a group of {group} at {per_record} keys each writes {} plus the statement's own, \
over a budget of {INSERT_KEY_BUDGET}",
group * per_record
);
}
#[test]
fn grouping_respects_the_batch_and_never_reaches_zero() {
assert_eq!(records_per_insert(1, 10), 10, "a cheap record groups at the batch");
assert_eq!(
records_per_insert(INSERT_KEY_BUDGET * 2, 10),
1,
"a record costing more than the whole budget still gets a statement"
);
assert_eq!(records_per_insert(0, 10), 10, "a zero estimate must not divide by zero");
}
#[test]
fn a_view_follows_the_views_it_reads() {
let person = [TableName::from("person")];
let zz = [TableName::from("zz_view")];
let views = [("bb_vv", &zz[..]), ("zz_view", &person[..])];
assert_eq!(view_order(&views).order, vec![1, 0]);
}
#[test]
fn a_source_outside_the_set_does_not_defer_a_view() {
let person = [TableName::from("person")];
let views = [("aa_view", &person[..]), ("zz_view", &person[..])];
assert_eq!(view_order(&views).order, vec![0, 1]);
}
#[test]
fn a_view_behind_a_cycle_is_not_reported_as_being_in_it() {
let b = [TableName::from("b")];
let c = [TableName::from("c")];
let d = [TableName::from("d")];
let c_again = [TableName::from("c")];
let views = [("a", &b[..]), ("b", &c[..]), ("c", &d[..]), ("d", &c_again[..])];
let order = view_order(&views);
assert_eq!(order.order.len(), 4, "every view is emitted");
assert_eq!(
order.cyclic,
HashSet::<usize>::from([2, 3]),
"only the two views that read each other are in the cycle"
);
assert_eq!(
order.behind_cycle,
HashSet::<usize>::from([0, 1]),
"the chain feeding the cycle is unorderable without being part of it"
);
}
#[test]
fn a_cycle_is_emitted_last_rather_than_looping() {
let a = [TableName::from("a")];
let b = [TableName::from("b")];
let base = [TableName::from("base")];
let views = [("a", &b[..]), ("b", &a[..]), ("ok", &base[..])];
let order = view_order(&views);
assert_eq!(order.order.len(), 3, "every view is emitted");
assert_eq!(order.order[0], 2, "the orderable view comes first");
assert_eq!(order.order[1..], [0, 1], "the cycle keeps its original order");
assert_eq!(
order.cyclic,
HashSet::<usize>::from([0, 1]),
"the cycle's members are named, and the view that can be ordered is not"
);
}
}