1use std::collections::{HashMap, HashSet};
4
5use crate::backend::{BackendType, ConsistencyMode};
6use crate::transport::{Transport, TransportSet};
7use crate::types::TableName;
8
9use super::access::{AccessConfig, PublicAccess};
10use super::audit::AuditConfig;
11use super::diff::SchemaDiff;
12use super::distribute::DistributeConfig;
13use super::field::{CompositeIndexDef, FieldDefinition, HnswConfig, IndexConfig};
14use super::source::SourceConfig;
15use super::store::StoreConfig;
16
17#[derive(Debug, Clone)]
23#[expect(
24 clippy::struct_excessive_bools,
25 reason = "schema-parser output; each bool maps 1:1 to an interface flag from `@export(rest, ws, sse, mqtt, mcp, grpc, graphql)`. Bitflags would force a parallel Interface enum already covered by the directive vocabulary, and obscure the table-definition reader's at-a-glance view of which interfaces a table exposes."
26)]
27pub struct TableDefinition {
28 pub name: String,
30 pub database: String,
32 pub table_name: TableName,
34 pub storage: BackendType,
36 pub fields: Vec<FieldDefinition>,
38 pub primary_key: String,
40 pub indexed: Vec<String>,
42 pub exported: bool,
44 pub custom_path: Option<String>,
50 pub rest_enabled: bool,
52 pub graphql_enabled: bool,
54 pub ws_enabled: bool,
56 pub sse_enabled: bool,
58 pub mqtt_enabled: bool,
60 pub mcp_enabled: bool,
62 pub grpc_enabled: bool,
64 pub public_operations: HashSet<PublicAccess>,
71 pub access: Option<AccessConfig>,
77 pub expiration: Option<u64>,
79 pub composite_indexes: Vec<CompositeIndexDef>,
81 pub distribute: Option<DistributeConfig>,
83 pub store: Option<StoreConfig>,
88 pub source: Option<SourceConfig>,
94 pub audit: Option<AuditConfig>,
96 pub crdt_fields: HashMap<String, String>,
98}
99
100impl Default for TableDefinition {
101 fn default() -> Self {
102 Self {
103 name: String::new(),
104 database: "data".to_owned(),
105 table_name: TableName::from(""),
106 storage: BackendType::Disk,
107 fields: vec![],
108 primary_key: "id".to_owned(),
109 indexed: vec![],
110 exported: true,
111 custom_path: None,
112 rest_enabled: true,
113 graphql_enabled: true,
114 ws_enabled: true,
115 sse_enabled: true,
116 mqtt_enabled: true,
117 mcp_enabled: true,
118 grpc_enabled: true,
119 public_operations: HashSet::new(),
120 access: None,
121 expiration: None,
122 composite_indexes: vec![],
123 distribute: None,
124 store: None,
125 source: None,
126 audit: None,
127 crdt_fields: HashMap::new(),
128 }
129 }
130}
131
132impl TableDefinition {
133 #[must_use]
135 pub fn endpoint_name(&self) -> String {
136 self.custom_path
137 .as_ref()
138 .map_or_else(|| self.name.to_lowercase(), std::clone::Clone::clone)
139 }
140
141 #[must_use]
143 pub fn consistency(&self) -> Option<ConsistencyMode> {
144 self.distribute.as_ref().and_then(|d| d.consistency)
145 }
146
147 #[must_use]
149 pub fn residency(&self) -> Option<&str> {
150 self.distribute
151 .as_ref()
152 .and_then(|d| d.residency.as_deref())
153 }
154
155 #[must_use]
157 pub fn replication_factor(&self) -> Option<u8> {
158 self.distribute.as_ref().and_then(|d| d.replication_factor)
159 }
160
161 #[must_use]
163 pub fn sharding(&self) -> Option<&str> {
164 self.distribute.as_ref().and_then(|d| d.sharding.as_deref())
165 }
166
167 #[must_use]
169 pub const fn is_audited(&self) -> bool {
170 self.audit.is_some()
171 }
172
173 #[must_use]
178 pub const fn transport_enabled(&self, transport: Transport) -> bool {
179 match transport {
180 Transport::Rest => self.rest_enabled,
181 Transport::GraphQl => self.graphql_enabled,
182 Transport::Ws => self.ws_enabled,
183 Transport::Sse => self.sse_enabled,
184 Transport::Mqtt => self.mqtt_enabled,
185 Transport::Mcp => self.mcp_enabled,
186 Transport::Grpc => self.grpc_enabled,
187 }
188 }
189
190 #[must_use]
193 pub fn transport_set(&self) -> TransportSet {
194 if !self.exported {
195 return TransportSet::NONE;
196 }
197 let mut set = TransportSet::NONE;
198 for t in Transport::ALL {
199 if self.transport_enabled(t) {
200 set.insert(t);
201 }
202 }
203 set
204 }
205
206 #[must_use]
210 pub fn schema_fingerprint(&self) -> String {
211 use std::collections::BTreeMap;
212 use std::fmt::Write;
213
214 let mut hasher_input = String::new();
215
216 let mut fields: BTreeMap<&str, (&str, &str, bool, bool)> = BTreeMap::new();
218 for f in &self.fields {
219 let idx = match &f.index_config {
220 IndexConfig::None => "none",
221 IndexConfig::Standard => "standard",
222 IndexConfig::FullText => "fulltext",
223 IndexConfig::Vector { .. } => "vector",
224 };
225 let has_default = f.default_value.is_some();
226 fields.insert(&f.name, (&f.field_type, idx, f.is_primary, has_default));
227 }
228
229 for (name, (ftype, idx, primary, has_default)) in &fields {
230 let _ = writeln!(hasher_input, "{name}:{ftype}:{idx}:{primary}:{has_default}");
231 }
232
233 let _ = writeln!(hasher_input, "pk:{}", self.primary_key);
235
236 let mut hash: u64 = 0xcbf2_9ce4_8422_2325; for byte in hasher_input.bytes() {
239 hash ^= u64::from(byte);
240 hash = hash.wrapping_mul(0x0100_0000_01b3); }
242 format!("{hash:016x}")
243 }
244
245 #[must_use]
249 pub fn diff_against(&self, old_fields: &[String], old_indexed: &[String]) -> SchemaDiff {
250 let new_field_names: HashSet<&str> = self.fields.iter().map(|f| f.name.as_str()).collect();
251 let old_field_set: HashSet<&str> =
252 old_fields.iter().map(std::string::String::as_str).collect();
253 let old_indexed_set: HashSet<&str> = old_indexed
254 .iter()
255 .map(std::string::String::as_str)
256 .collect();
257
258 let mut diff = SchemaDiff::default();
259
260 for old in old_fields {
262 if !new_field_names.contains(old.as_str()) && old != "id" && old != &self.primary_key {
263 diff.strip_fields.push(old.clone());
264 }
265 }
266
267 for field in &self.fields {
269 if !old_field_set.contains(field.name.as_str())
270 && let Some(ref default) = field.default_value
271 {
272 diff.inject_defaults
273 .push((field.name.clone(), default.clone()));
274 }
275 }
276
277 for field in &self.fields {
279 if field.index_config.is_indexed() && !old_indexed_set.contains(field.name.as_str()) {
280 diff.build_indexes
281 .push((field.name.clone(), field.index_config.clone()));
282 }
283 }
284
285 for old_idx in old_indexed {
287 let still_indexed = self
288 .fields
289 .iter()
290 .any(|f| f.name == *old_idx && f.index_config.is_indexed());
291 if !still_indexed {
292 diff.drop_indexes.push(old_idx.clone());
293 }
294 }
295
296 diff
297 }
298
299 #[must_use]
301 pub fn builder(name: &str) -> TableDefinitionBuilder {
302 TableDefinitionBuilder::new(name)
303 }
304
305 #[must_use]
307 pub fn to_json_schema(&self) -> serde_json::Value {
308 use serde_json::json;
309
310 let fields: Vec<_> = self
311 .fields
312 .iter()
313 .map(|f| {
314 json!({
315 "name": f.name,
316 "type": graphql_to_json_type(&f.field_type),
317 "primaryKey": f.is_primary,
318 "indexed": f.index_config.is_indexed(),
319 })
320 })
321 .collect();
322
323 json!({
324 "name": self.name,
325 "fields": fields,
326 "primaryKey": self.primary_key,
327 })
328 }
329}
330
331#[must_use]
334pub fn graphql_to_json_type(graphql_type: &str) -> &str {
335 crate::scalar::json_type(graphql_type)
336}
337
338#[derive(Debug)]
340pub struct TableDefinitionBuilder {
341 def: TableDefinition,
342}
343
344impl TableDefinitionBuilder {
345 #[must_use]
347 pub fn new(name: &str) -> Self {
348 Self {
349 def: TableDefinition {
350 name: name.to_owned(),
351 table_name: TableName::from(name),
352 ..Default::default()
353 },
354 }
355 }
356
357 #[must_use]
359 pub fn database(mut self, database: &str) -> Self {
360 database.clone_into(&mut self.def.database);
361 self
362 }
363
364 #[must_use]
366 pub fn table_name(mut self, table_name: &str) -> Self {
367 self.def.table_name = TableName::from(table_name);
368 self
369 }
370
371 #[must_use]
373 pub const fn storage(mut self, storage: BackendType) -> Self {
374 self.def.storage = storage;
375 self
376 }
377
378 #[must_use]
380 pub fn field(mut self, name: &str, field_type: &str, is_primary: bool) -> Self {
381 let field = FieldDefinition {
382 name: name.to_owned(),
383 field_type: field_type.to_owned(),
384 is_primary,
385 ..Default::default()
386 };
387 if is_primary {
388 name.clone_into(&mut self.def.primary_key);
389 }
390 self.def.fields.push(field);
391 self
392 }
393
394 #[must_use]
396 pub fn indexed_field(mut self, name: &str, field_type: &str) -> Self {
397 let field = FieldDefinition {
398 name: name.to_owned(),
399 field_type: field_type.to_owned(),
400 is_primary: false,
401 index_config: IndexConfig::Standard,
402 ..Default::default()
403 };
404 self.def.indexed.push(name.to_owned());
405 self.def.fields.push(field);
406 self
407 }
408
409 #[must_use]
411 pub fn vector_field(mut self, name: &str, hnsw_config: HnswConfig) -> Self {
412 let field = FieldDefinition {
413 name: name.to_owned(),
414 field_type: "Vector".to_owned(),
415 is_primary: false,
416 index_config: IndexConfig::Vector {
417 hnsw_config,
418 source: None,
419 model: None,
420 },
421 ..Default::default()
422 };
423 self.def.indexed.push(name.to_owned());
424 self.def.fields.push(field);
425 self
426 }
427
428 #[must_use]
430 pub const fn exported(mut self, exported: bool) -> Self {
431 self.def.exported = exported;
432 self
433 }
434
435 #[must_use]
437 pub fn custom_path(mut self, path: &str) -> Self {
438 self.def.custom_path = Some(path.to_owned());
439 self
440 }
441
442 #[must_use]
444 pub const fn rest_enabled(mut self, enabled: bool) -> Self {
445 self.def.rest_enabled = enabled;
446 self
447 }
448
449 #[must_use]
451 pub const fn graphql_enabled(mut self, enabled: bool) -> Self {
452 self.def.graphql_enabled = enabled;
453 self
454 }
455
456 #[must_use]
458 pub const fn ws_enabled(mut self, enabled: bool) -> Self {
459 self.def.ws_enabled = enabled;
460 self
461 }
462
463 #[must_use]
465 pub const fn sse_enabled(mut self, enabled: bool) -> Self {
466 self.def.sse_enabled = enabled;
467 self
468 }
469
470 #[must_use]
472 pub const fn mcp_enabled(mut self, enabled: bool) -> Self {
473 self.def.mcp_enabled = enabled;
474 self
475 }
476
477 #[must_use]
479 pub const fn expiration(mut self, seconds: u64) -> Self {
480 self.def.expiration = Some(seconds);
481 self
482 }
483
484 #[must_use]
486 pub fn consistency(mut self, mode: ConsistencyMode) -> Self {
487 let dist = self.def.distribute.get_or_insert(DistributeConfig {
488 sharding: None,
489 shard_key: None,
490 shard_count: None,
491 residency: None,
492 replication_factor: None,
493 consistency: None,
494 replication: None,
495 });
496 dist.consistency = Some(mode);
497 self
498 }
499
500 #[must_use]
502 pub fn fulltext_field(mut self, name: &str, field_type: &str) -> Self {
503 let field = FieldDefinition {
504 name: name.to_owned(),
505 field_type: field_type.to_owned(),
506 is_primary: false,
507 index_config: IndexConfig::FullText,
508 ..Default::default()
509 };
510 self.def.indexed.push(name.to_owned());
511 self.def.fields.push(field);
512 self
513 }
514
515 #[must_use]
517 pub fn composite_index(mut self, fields: &[&str]) -> Self {
518 let name = fields.join("_");
519 self.def.composite_indexes.push(CompositeIndexDef {
520 name,
521 fields: fields
522 .iter()
523 .map(std::string::ToString::to_string)
524 .collect(),
525 });
526 self
527 }
528
529 #[must_use]
531 pub fn build(self) -> TableDefinition {
532 self.def
533 }
534}