use std::collections::HashMap;
use std::sync::Arc;
use super::*;
const TIMESERIES_META_COLLECTION: &str = "red_timeseries_meta";
const TIMESERIES_SERIES_COLLECTION: &str = "red_timeseries_series";
const DEFAULT_TIMESERIES_CHUNK_INTERVAL_NS: u64 = 86_400_000_000_000;
const TIMESERIES_MAX_SERIES_GLOBAL_CONFIG: &str = "storage.time_series.max_series_per_collection";
const DEFAULT_TIMESERIES_MAX_SERIES_PER_COLLECTION: usize = 1_000_000;
#[derive(Default)]
struct SealHypertableChunksOutcome {
chunks_sealed: usize,
columnar_chunks_sealed: usize,
}
impl RedDBRuntime {
pub fn execute_create_timeseries(
&self,
raw_query: &str,
query: &CreateTimeSeriesQuery,
) -> RedDBResult<RuntimeQueryResult> {
self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
for spec in &query.downsample_policies {
crate::storage::timeseries::retention::DownsamplePolicy::parse(spec).ok_or_else(
|| RedDBError::Query(format!("invalid downsample policy '{}'", spec)),
)?;
}
let store = self.inner.db.store();
let exists = store.get_collection(&query.name).is_some();
if exists {
if query.if_not_exists {
return Ok(RuntimeQueryResult::ok_message(
raw_query.to_string(),
&format!("timeseries '{}' already exists", query.name),
"create",
));
}
return Err(RedDBError::Query(format!(
"timeseries '{}' already exists",
query.name
)));
}
store
.create_collection(&query.name)
.map_err(|e| RedDBError::Internal(e.to_string()))?;
if let Some(ttl_ms) = query.retention_ms {
self.inner
.db
.set_collection_default_ttl_ms(&query.name, ttl_ms);
}
let contract = if query.hypertable.is_some() {
hypertable_collection_contract(query)
} else {
timeseries_collection_contract(query)
};
self.inner
.db
.save_collection_contract(contract)
.map_err(|err| RedDBError::Internal(err.to_string()))?;
if let Some(tenant_id) = crate::runtime::impl_core::current_tenant() {
store.set_config_tree(
&format!("red.collection_tenants.{}", query.name),
&crate::serde_json::Value::String(tenant_id),
);
}
save_timeseries_metadata(store.as_ref(), query)?;
let spec = match &query.hypertable {
Some(ht) => {
let mut spec = crate::storage::timeseries::HypertableSpec::new(
query.name.clone(),
ht.time_column.clone(),
ht.chunk_interval_ns,
);
if let Some(ttl) = ht.default_ttl_ns {
spec = spec.with_ttl_ns(ttl);
}
spec
}
None => {
let mut spec = crate::storage::timeseries::HypertableSpec::new(
query.name.clone(),
"timestamp",
DEFAULT_TIMESERIES_CHUNK_INTERVAL_NS,
);
if let Some(ttl_ms) = query.retention_ms {
spec = spec.with_ttl_ns(ttl_ms.saturating_mul(1_000_000));
}
spec
}
};
self.inner.db.hypertables().register(spec);
self.invalidate_result_cache();
self.inner
.db
.persist_metadata()
.map_err(|e| RedDBError::Internal(e.to_string()))?;
let columns: Vec<String> = query
.hypertable
.as_ref()
.map(|ht| vec![ht.time_column.clone()])
.unwrap_or_else(|| vec!["metric".to_string(), "value".to_string()]);
self.schema_vocabulary_apply(
crate::runtime::schema_vocabulary::DdlEvent::CreateCollection {
collection: query.name.clone(),
columns,
type_tags: Vec::new(),
description: None,
},
);
let noun = if query.hypertable.is_some() {
"hypertable"
} else {
"timeseries"
};
let mut msg = format!("{noun} '{}' created", query.name);
if let Some(ret) = query.retention_ms {
msg.push_str(&format!(" (retention={}ms)", ret));
}
if let Some(cs) = query.chunk_size {
msg.push_str(&format!(" (chunk_size={})", cs));
}
if !query.downsample_policies.is_empty() {
msg.push_str(&format!(
" (downsample_policies={})",
query.downsample_policies.len()
));
}
Ok(RuntimeQueryResult::ok_message(
raw_query.to_string(),
&msg,
"create",
))
}
pub fn execute_drop_timeseries(
&self,
raw_query: &str,
query: &DropTimeSeriesQuery,
) -> RedDBResult<RuntimeQueryResult> {
self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
let store = self.inner.db.store();
if super::impl_ddl::is_system_schema_name(&query.name) {
return Err(RedDBError::Query("system schema is read-only".to_string()));
}
if store.get_collection(&query.name).is_none() {
if query.if_exists {
return Ok(RuntimeQueryResult::ok_message(
raw_query.to_string(),
&format!("timeseries '{}' does not exist", query.name),
"drop",
));
}
return Err(RedDBError::NotFound(format!(
"timeseries '{}' not found",
query.name
)));
}
let actual = crate::runtime::ddl::polymorphic_resolver::resolve(
&query.name,
&self.inner.db.catalog_model_snapshot(),
)?;
if actual != crate::catalog::CollectionModel::TimeSeries
&& actual != crate::catalog::CollectionModel::Table
{
crate::runtime::ddl::polymorphic_resolver::ensure_model_match(
crate::catalog::CollectionModel::TimeSeries,
actual,
)?;
}
let _ = self.inner.db.hypertables().unregister(&query.name);
store
.drop_collection(&query.name)
.map_err(|e| RedDBError::Internal(e.to_string()))?;
self.inner.db.clear_collection_default_ttl_ms(&query.name);
self.inner
.db
.remove_collection_contract(&query.name)
.map_err(|err| RedDBError::Internal(err.to_string()))?;
remove_timeseries_metadata(store.as_ref(), &query.name);
remove_timeseries_series_dictionary(store.as_ref(), &query.name);
self.invalidate_result_cache();
self.inner
.db
.persist_metadata()
.map_err(|e| RedDBError::Internal(e.to_string()))?;
self.schema_vocabulary_apply(
crate::runtime::schema_vocabulary::DdlEvent::DropCollection {
collection: query.name.clone(),
},
);
Ok(RuntimeQueryResult::ok_message(
raw_query.to_string(),
&format!("timeseries '{}' dropped", query.name),
"drop",
))
}
pub fn seal_hypertable_chunks(&self, collection: &str) -> RedDBResult<usize> {
self.seal_hypertable_chunks_internal(collection, usize::MAX, true)
.map(|outcome| outcome.columnar_chunks_sealed)
}
pub(crate) fn seal_hypertable_chunks_for_checkpoint(
&self,
max_chunks: usize,
) -> RedDBResult<usize> {
if max_chunks == 0 {
return Ok(0);
}
let mut remaining = max_chunks;
let mut sealed = 0usize;
for collection in self.inner.db.hypertables().names() {
if remaining == 0 {
break;
}
let outcome = self.seal_hypertable_chunks_internal(&collection, remaining, false)?;
remaining = remaining.saturating_sub(outcome.chunks_sealed);
sealed += outcome.chunks_sealed;
}
Ok(sealed)
}
fn seal_hypertable_chunks_internal(
&self,
collection: &str,
max_chunks: usize,
include_tail_chunk: bool,
) -> RedDBResult<SealHypertableChunksOutcome> {
if max_chunks == 0 {
return Ok(SealHypertableChunksOutcome::default());
}
let analytical = self
.inner
.db
.collection_contract(collection)
.and_then(|c| c.analytical_storage.clone());
let registry = self.inner.db.hypertables();
let Some(spec) = registry.get(collection) else {
return Ok(SealHypertableChunksOutcome::default());
};
let time_col = spec.time_column.clone();
let store = self.inner.db.store();
let Some(manager) = store.get_collection(collection) else {
return Ok(SealHypertableChunksOutcome::default());
};
let chunks = registry.show_chunks(collection);
let tail_chunk_start = if include_tail_chunk {
None
} else {
chunks.iter().map(|meta| meta.id.start_ns).max()
};
let mut outcome = SealHypertableChunksOutcome::default();
let mut changed = false;
for meta in chunks {
if outcome.chunks_sealed >= max_chunks {
break;
}
if meta.sealed {
continue;
}
if Some(meta.id.start_ns) == tail_chunk_start {
continue;
}
let start = meta.id.start_ns;
let end = meta.end_ns_exclusive;
let points = materialize_row_points(&manager, &time_col, start, end);
let columnar_enabled = analytical.as_ref().is_some_and(|cfg| cfg.columnar);
if columnar_enabled && points.len() < self.inner.columnar_projection_size_floor_rows {
continue;
}
let mut chunk = crate::storage::timeseries::TimeSeriesChunk::with_max_points(
collection.to_string(),
HashMap::new(),
points.len().max(1),
);
for (ts, value) in &points {
chunk.append(*ts, *value);
}
let routed = crate::storage::timeseries::chunk::seal_chunk_with_config(
&mut chunk,
analytical.as_ref(),
start,
0,
)
.map_err(|err| RedDBError::Internal(format!("columnar seal failed: {err:?}")))?;
match routed {
crate::storage::timeseries::chunk::SealedChunkStorage::Columnar(bytes) => {
let page = self
.inner
.db
.write_column_block_page(&bytes)
.map_err(|err| {
RedDBError::Internal(format!("columnar page write failed: {err}"))
})?;
registry.seal_chunk_columnar(&meta.id, page, bytes);
outcome.columnar_chunks_sealed += 1;
outcome.chunks_sealed += 1;
changed = true;
}
crate::storage::timeseries::chunk::SealedChunkStorage::Row => {
registry.seal_chunk(&meta.id);
outcome.chunks_sealed += 1;
changed = true;
}
}
}
if changed {
self.inner
.db
.persist_metadata()
.map_err(|e| RedDBError::Internal(e.to_string()))?;
}
Ok(outcome)
}
pub fn columnar_chunk_count(&self, collection: &str) -> usize {
self.inner
.db
.hypertables()
.show_chunks(collection)
.iter()
.filter(|meta| meta.columnar_page.is_some())
.count()
}
pub fn columnar_chunk_points(
&self,
collection: &str,
chunk_start_ns: u64,
start_ns: u64,
end_ns: u64,
) -> Option<Vec<(u64, f64)>> {
let id = crate::storage::timeseries::ChunkId {
hypertable: collection.to_string(),
start_ns: chunk_start_ns,
};
let bytes = self.inner.db.hypertables().columnar_block(&id)?;
let scan =
crate::storage::timeseries::chunk::query_column_block_range(&bytes, start_ns, end_ns)
.ok()?;
Some(
scan.points
.iter()
.map(|p| (p.timestamp_ns, p.value))
.collect(),
)
}
pub fn columnar_chunk_range_scan(
&self,
collection: &str,
chunk_start_ns: u64,
start_ns: u64,
end_ns: u64,
) -> Option<crate::storage::timeseries::chunk::PrunedColumnScan> {
let id = crate::storage::timeseries::ChunkId {
hypertable: collection.to_string(),
start_ns: chunk_start_ns,
};
let bytes = self.inner.db.hypertables().columnar_block(&id)?;
crate::storage::timeseries::chunk::query_column_block_range(&bytes, start_ns, end_ns).ok()
}
pub fn columnar_chunk_value_eq_scan(
&self,
collection: &str,
chunk_start_ns: u64,
target: f64,
) -> Option<crate::storage::timeseries::chunk::PrunedColumnScan> {
let id = crate::storage::timeseries::ChunkId {
hypertable: collection.to_string(),
start_ns: chunk_start_ns,
};
let bytes = self.inner.db.hypertables().columnar_block(&id)?;
crate::storage::timeseries::chunk::query_column_block_value_eq(&bytes, target).ok()
}
pub fn read_bridge_points(
&self,
collection: &str,
start_ns: u64,
end_ns: u64,
) -> RedDBResult<Vec<(u64, f64)>> {
use crate::storage::timeseries::ChunkFormat;
use crate::storage::unified::column_block::{
peek_column_block_version, COLUMN_BLOCK_VERSION_V1,
};
let registry = self.inner.db.hypertables();
let Some(spec) = registry.get(collection) else {
return Ok(Vec::new());
};
let time_col = spec.time_column.clone();
let store = self.inner.db.store();
let mut out: Vec<(u64, f64)> = Vec::new();
for meta in registry.show_chunks(collection) {
if meta.max_ts_ns < start_ns || meta.min_ts_ns > end_ns {
continue;
}
match meta.format() {
ChunkFormat::ColumnarV1 => {
let Some(bytes) = registry.columnar_block(&meta.id) else {
continue;
};
match peek_column_block_version(&bytes) {
Some(COLUMN_BLOCK_VERSION_V1) => {}
Some(v) => {
return Err(RedDBError::Internal(format!(
"chunk {} @ {} carries unsupported columnar format version {v}",
meta.id.hypertable, meta.id.start_ns
)));
}
None => {
return Err(RedDBError::Internal(format!(
"chunk {} @ {} is flagged columnar but its block is not RDCC",
meta.id.hypertable, meta.id.start_ns
)));
}
}
let scan = crate::storage::timeseries::chunk::query_column_block_range(
&bytes, start_ns, end_ns,
)
.map_err(|err| {
RedDBError::Internal(format!("columnar read-bridge decode failed: {err:?}"))
})?;
out.extend(scan.points.iter().map(|p| (p.timestamp_ns, p.value)));
}
ChunkFormat::Row => {
let Some(manager) = store.get_collection(collection) else {
continue;
};
let chunk_start = meta.id.start_ns;
let chunk_end = meta.end_ns_exclusive;
out.extend(
materialize_row_points(&manager, &time_col, chunk_start, chunk_end)
.into_iter()
.filter(|(ts, _)| *ts >= start_ns && *ts <= end_ns),
);
}
}
}
out.sort_by_key(|(ts, _)| *ts);
Ok(out)
}
}
pub(crate) fn intern_timeseries_series(
store: &crate::storage::unified::UnifiedStore,
collection: &str,
metric: &str,
tags: &HashMap<String, String>,
) -> RedDBResult<u64> {
let canonical_tags = canonical_timeseries_tags(tags);
let _ = store.get_or_create_collection(TIMESERIES_SERIES_COLLECTION);
let manager = store
.get_collection(TIMESERIES_SERIES_COLLECTION)
.ok_or_else(|| RedDBError::Internal("timeseries series dictionary missing".to_string()))?;
let rows = manager.query_all(|entity| {
entity
.data
.as_row()
.is_some_and(|row| row_text(row, "collection").is_some_and(|value| value == collection))
});
let mut next_id = 0u64;
for entity in &rows {
let Some(row) = entity.data.as_row() else {
continue;
};
if let Some(existing_id) = row_u64(row, "series_id") {
next_id = next_id.max(existing_id.saturating_add(1));
}
if row_text(row, "metric") == Some(metric)
&& row_text(row, "canonical_tags") == Some(canonical_tags.as_str())
{
if let Some(existing_id) = row_u64(row, "series_id") {
return Ok(existing_id);
}
}
}
let ceiling = timeseries_max_series_per_collection(store, collection);
if rows.len() >= ceiling {
return Err(RedDBError::Query(format!(
"timeseries collection '{collection}' has reached its distinct series ceiling of {ceiling}"
)));
}
let series_id = next_id;
let mut fields = HashMap::new();
fields.insert(
"collection".to_string(),
Value::text(collection.to_string()),
);
fields.insert("series_id".to_string(), Value::UnsignedInteger(series_id));
fields.insert("metric".to_string(), Value::text(metric.to_string()));
fields.insert(
"canonical_tags".to_string(),
Value::text(canonical_tags.clone()),
);
fields.insert("tags".to_string(), encoded_timeseries_tags_value(tags));
store
.insert_auto(
TIMESERIES_SERIES_COLLECTION,
UnifiedEntity::new(
EntityId::new(0),
EntityKind::TableRow {
table: Arc::from(TIMESERIES_SERIES_COLLECTION),
row_id: 0,
},
EntityData::Row(crate::storage::RowData {
columns: Vec::new(),
named: Some(fields),
schema: None,
}),
),
)
.map_err(|err| RedDBError::Internal(err.to_string()))?;
Ok(series_id)
}
pub(crate) fn hydrate_timeseries_entity(
store: &crate::storage::unified::UnifiedStore,
entity: &UnifiedEntity,
) -> UnifiedEntity {
let mut hydrated = entity.clone();
let EntityData::TimeSeries(point) = &mut hydrated.data else {
return hydrated;
};
if !point.tags.is_empty() {
return hydrated;
}
let Some(series_id) = point.series_id else {
return hydrated;
};
if let Some(tags) = resolve_timeseries_series_tags(store, entity.kind.collection(), series_id) {
point.tags = tags;
}
hydrated
}
pub(crate) fn resolve_timeseries_series_tags(
store: &crate::storage::unified::UnifiedStore,
collection: &str,
series_id: u64,
) -> Option<HashMap<String, String>> {
let manager = store.get_collection(TIMESERIES_SERIES_COLLECTION)?;
let rows = manager.query_all(|entity| {
entity.data.as_row().is_some_and(|row| {
row_text(row, "collection").is_some_and(|value| value == collection)
&& row_u64(row, "series_id") == Some(series_id)
})
});
rows.iter()
.filter_map(|entity| entity.data.as_row())
.find_map(|row| row.get_field("tags").and_then(encoded_tags_from_value))
}
fn canonical_timeseries_tags(tags: &HashMap<String, String>) -> String {
match encoded_timeseries_tags_value(tags) {
Value::Json(bytes) => String::from_utf8(bytes).unwrap_or_default(),
_ => "{}".to_string(),
}
}
fn encoded_timeseries_tags_value(tags: &HashMap<String, String>) -> Value {
let object = tags
.iter()
.map(|(key, value)| (key.clone(), crate::json::Value::String(value.clone())))
.collect();
let json = crate::json::Value::Object(object);
Value::Json(crate::json::to_vec(&json).unwrap_or_default())
}
fn encoded_tags_from_value(value: &Value) -> Option<HashMap<String, String>> {
let Value::Json(bytes) = value else {
return None;
};
let json: crate::json::Value = crate::json::from_slice(bytes).ok()?;
let crate::json::Value::Object(object) = json else {
return None;
};
Some(
object
.into_iter()
.filter_map(|(key, value)| match value {
crate::json::Value::String(value) => Some((key, value)),
_ => None,
})
.collect(),
)
}
fn row_text<'a>(row: &'a crate::storage::RowData, field: &str) -> Option<&'a str> {
match row.get_field(field) {
Some(Value::Text(value)) => Some(value.as_ref()),
_ => None,
}
}
fn row_u64(row: &crate::storage::RowData, field: &str) -> Option<u64> {
match row.get_field(field) {
Some(Value::UnsignedInteger(value)) => Some(*value),
Some(Value::Integer(value)) if *value >= 0 => Some(*value as u64),
_ => None,
}
}
fn timeseries_max_series_per_collection(
store: &crate::storage::unified::UnifiedStore,
collection: &str,
) -> usize {
let collection_key = format!("storage.time_series.collections.{collection}.max_series");
latest_usize_config(store, &collection_key)
.or_else(|| latest_usize_config(store, TIMESERIES_MAX_SERIES_GLOBAL_CONFIG))
.unwrap_or(DEFAULT_TIMESERIES_MAX_SERIES_PER_COLLECTION)
}
fn latest_usize_config(store: &crate::storage::unified::UnifiedStore, key: &str) -> Option<usize> {
let manager = store.get_collection("red_config")?;
let mut newest: Option<(u64, usize)> = None;
manager.for_each_entity(|entity| {
let Some(row) = entity.data.as_row() else {
return true;
};
if !row_text(row, "key").is_some_and(|value| value.eq_ignore_ascii_case(key)) {
return true;
}
let Some(value) = row.get_field("value").and_then(value_as_usize) else {
return true;
};
let id = entity.id.raw();
if newest.is_none_or(|(best_id, _)| id >= best_id) {
newest = Some((id, value));
}
true
});
newest.map(|(_, value)| value)
}
fn value_as_usize(value: &Value) -> Option<usize> {
let value = match value {
Value::UnsignedInteger(value) => *value,
Value::Integer(value) if *value >= 0 => *value as u64,
Value::Float(value) if *value >= 0.0 => *value as u64,
Value::Text(value) => value.trim().parse().ok()?,
_ => return None,
};
usize::try_from(value).ok()
}
fn materialize_row_points(
manager: &crate::storage::unified::SegmentManager,
time_col: &str,
start: u64,
end: u64,
) -> Vec<(u64, f64)> {
let mut points: Vec<(u64, f64)> = manager
.query_all(|entity| {
let ts = match &entity.data {
EntityData::Row(row) => row.get_field(time_col).and_then(field_as_u64),
EntityData::TimeSeries(point) => Some(point.timestamp_ns),
_ => None,
};
ts.is_some_and(|ts| ts >= start && ts < end)
})
.iter()
.filter_map(|entity| match &entity.data {
EntityData::Row(row) => {
let ts = row.get_field(time_col).and_then(field_as_u64)?;
let value = row.get_field("value").and_then(field_as_f64).unwrap_or(0.0);
Some((ts, value))
}
EntityData::TimeSeries(point) => Some((point.timestamp_ns, point.value)),
_ => None,
})
.collect();
points.sort_by_key(|(ts, _)| *ts);
points
}
fn field_as_u64(value: &Value) -> Option<u64> {
match value {
Value::Integer(n) | Value::BigInt(n) | Value::Timestamp(n) if *n >= 0 => Some(*n as u64),
Value::UnsignedInteger(n) => Some(*n),
_ => None,
}
}
fn field_as_f64(value: &Value) -> Option<f64> {
match value {
Value::Float(f) => Some(*f),
Value::Integer(n) | Value::BigInt(n) => Some(*n as f64),
Value::UnsignedInteger(n) => Some(*n as f64),
_ => None,
}
}
fn save_timeseries_metadata(
store: &crate::storage::unified::UnifiedStore,
query: &CreateTimeSeriesQuery,
) -> RedDBResult<()> {
remove_timeseries_metadata(store, &query.name);
let _ = store.get_or_create_collection(TIMESERIES_META_COLLECTION);
let mut fields = HashMap::new();
fields.insert(
"kind".to_string(),
Value::text("timeseries_config".to_string()),
);
fields.insert("series".to_string(), Value::text(query.name.clone()));
fields.insert(
"retention_ms".to_string(),
query
.retention_ms
.map(Value::UnsignedInteger)
.unwrap_or(Value::Null),
);
fields.insert(
"chunk_size".to_string(),
query
.chunk_size
.map(|value| Value::UnsignedInteger(value as u64))
.unwrap_or(Value::Null),
);
fields.insert(
"downsample_policies".to_string(),
Value::Array(
query
.downsample_policies
.iter()
.cloned()
.map(Value::text)
.collect(),
),
);
store
.insert_auto(
TIMESERIES_META_COLLECTION,
UnifiedEntity::new(
EntityId::new(0),
EntityKind::TableRow {
table: Arc::from(TIMESERIES_META_COLLECTION),
row_id: 0,
},
EntityData::Row(crate::storage::RowData {
columns: Vec::new(),
named: Some(fields),
schema: None,
}),
),
)
.map_err(|err| RedDBError::Internal(err.to_string()))?;
Ok(())
}
fn remove_timeseries_metadata(store: &crate::storage::unified::UnifiedStore, series: &str) {
let Some(manager) = store.get_collection(TIMESERIES_META_COLLECTION) else {
return;
};
let rows = manager.query_all(|entity| {
entity.data.as_row().is_some_and(|row| {
row.get_field("series").is_some_and(
|value| matches!(value, Value::Text(candidate) if &**candidate == series),
)
})
});
for row in rows {
let _ = store.delete(TIMESERIES_META_COLLECTION, row.id);
}
}
fn remove_timeseries_series_dictionary(
store: &crate::storage::unified::UnifiedStore,
collection: &str,
) {
let Some(manager) = store.get_collection(TIMESERIES_SERIES_COLLECTION) else {
return;
};
let rows = manager.query_all(|entity| {
entity.data.as_row().is_some_and(|row| {
row.get_field("collection").is_some_and(
|value| matches!(value, Value::Text(candidate) if &**candidate == collection),
)
})
});
for row in rows {
let _ = store.delete(TIMESERIES_SERIES_COLLECTION, row.id);
}
}
fn analytical_storage_for(
columnar: bool,
time_key: &str,
) -> Option<crate::catalog::AnalyticalStorageConfig> {
columnar.then(|| crate::catalog::AnalyticalStorageConfig {
columnar: true,
time_key: time_key.to_string(),
order_by_key: None,
})
}
fn hypertable_collection_contract(
query: &CreateTimeSeriesQuery,
) -> crate::physical::CollectionContract {
let now = current_unix_ms();
let time_key = query
.hypertable
.as_ref()
.map(|ht| ht.time_column.as_str())
.unwrap_or("timestamp");
crate::physical::CollectionContract {
name: query.name.clone(),
declared_model: crate::catalog::CollectionModel::Table,
schema_mode: crate::catalog::SchemaMode::SemiStructured,
origin: crate::physical::ContractOrigin::Explicit,
version: 1,
created_at_unix_ms: now,
updated_at_unix_ms: now,
default_ttl_ms: query.retention_ms,
vector_dimension: None,
vector_metric: None,
context_index_fields: Vec::new(),
declared_columns: Vec::new(),
table_def: None,
timestamps_enabled: false,
context_index_enabled: false,
metrics_raw_retention_ms: None,
metrics_rollup_policies: Vec::new(),
metrics_tenant_identity: None,
metrics_namespace: None,
append_only: true,
subscriptions: Vec::new(),
analytics_config: Vec::new(),
session_key: None,
session_gap_ms: None,
retention_duration_ms: None,
analytical_storage: analytical_storage_for(query.columnar, time_key),
ai_policy: None,
}
}
fn timeseries_collection_contract(
query: &CreateTimeSeriesQuery,
) -> crate::physical::CollectionContract {
let now = current_unix_ms();
crate::physical::CollectionContract {
name: query.name.clone(),
declared_model: crate::catalog::CollectionModel::TimeSeries,
schema_mode: crate::catalog::SchemaMode::SemiStructured,
origin: crate::physical::ContractOrigin::Explicit,
version: 1,
created_at_unix_ms: now,
updated_at_unix_ms: now,
default_ttl_ms: query.retention_ms,
vector_dimension: None,
vector_metric: None,
context_index_fields: Vec::new(),
declared_columns: Vec::new(),
table_def: None,
timestamps_enabled: false,
context_index_enabled: false,
metrics_raw_retention_ms: None,
metrics_rollup_policies: Vec::new(),
metrics_tenant_identity: None,
metrics_namespace: None,
append_only: true,
subscriptions: Vec::new(),
analytics_config: Vec::new(),
session_key: query.session_key.clone(),
session_gap_ms: query.session_gap_ms,
retention_duration_ms: None,
analytical_storage: analytical_storage_for(query.columnar, "timestamp"),
ai_policy: None,
}
}
fn current_unix_ms() -> u128 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
}