Skip to main content

reifydb_export/render/
ddl.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use reifydb_core::interface::catalog::{
5	column::Column,
6	dictionary::Dictionary,
7	namespace::Namespace,
8	queue::{Queue, QueueDispatch},
9	ringbuffer::RingBuffer,
10	series::{Series, SeriesKey, TimestampPrecision},
11	sumtype::{Field, SumType},
12	table::Table,
13};
14
15use crate::{
16	error::ExportError,
17	model::NameResolver,
18	render::{
19		layout::{EnumColumn, LayoutColumn, build_layout},
20		typ::{render_column_type, render_value_type},
21	},
22};
23
24pub fn qualified_name(
25	resolver: &NameResolver,
26	namespace_id: u64,
27	name: &str,
28	context: &str,
29) -> Result<String, ExportError> {
30	let ns = resolver.namespaces.get(&namespace_id).ok_or_else(|| ExportError::UnresolvedReference {
31		kind: "namespace",
32		id: namespace_id,
33		object: context.to_string(),
34	})?;
35	Ok(format!("{}::{}", ns, name))
36}
37
38fn keyword_prefix(if_not_exists: bool) -> &'static str {
39	if if_not_exists {
40		" IF NOT EXISTS"
41	} else {
42		""
43	}
44}
45
46fn render_column(col: &Column, resolver: &NameResolver, object: &str) -> Result<String, ExportError> {
47	let rendered = render_column_type(&col.constraint, resolver, object)?;
48	let mut type_text = rendered.type_text;
49	let mut dictionary = rendered.dictionary;
50
51	if let (true, Some(dict_id)) = (dictionary.is_none(), &col.dictionary_id) {
52		let id = dict_id.to_u64();
53		let resolved = resolver.dictionary(id).ok_or_else(|| ExportError::UnresolvedReference {
54			kind: "dictionary",
55			id,
56			object: object.to_string(),
57		})?;
58		type_text = render_value_type(&resolved.value_type, object)?;
59		dictionary = Some(resolved.qualified_name.clone());
60	}
61
62	let mut properties = Vec::new();
63	if let Some(dict) = dictionary {
64		properties.push(format!("dictionary: {}", dict));
65	}
66	if col.auto_increment {
67		properties.push("auto_increment".to_string());
68	}
69
70	let mut out = format!("{}: {}", col.name, type_text);
71	if !properties.is_empty() {
72		out.push_str(&format!(" with {{ {} }}", properties.join(", ")));
73	}
74	Ok(out)
75}
76
77fn render_field(field: &Field, resolver: &NameResolver, object: &str) -> Result<String, ExportError> {
78	let rendered = render_column_type(&field.field_type, resolver, object)?;
79	let mut out = format!("{}: {}", field.name, rendered.type_text);
80	if let Some(dict) = rendered.dictionary {
81		out.push_str(&format!(" with {{ dictionary: {} }}", dict));
82	}
83	Ok(out)
84}
85
86fn render_columns_block(columns: &[Column], resolver: &NameResolver, object: &str) -> Result<String, ExportError> {
87	let mut rendered = Vec::new();
88	for column in build_layout(columns, resolver) {
89		rendered.push(match column {
90			LayoutColumn::Plain(c) => render_column(c, resolver, object)?,
91			LayoutColumn::Enum(e) => render_enum_column(&e, resolver, object)?,
92		});
93	}
94	Ok(format!("{{ {} }}", rendered.join(", ")))
95}
96
97fn render_enum_column(column: &EnumColumn, resolver: &NameResolver, object: &str) -> Result<String, ExportError> {
98	let resolved = resolver.sumtype(column.sumtype_id).ok_or_else(|| ExportError::UnresolvedReference {
99		kind: "sumtype",
100		id: column.sumtype_id,
101		object: object.to_string(),
102	})?;
103	Ok(format!("{}: {}", column.logical_name, resolved.qualified_name))
104}
105
106pub fn render_namespace(namespace: &Namespace, if_not_exists: bool) -> String {
107	format!("CREATE NAMESPACE{} {};", keyword_prefix(if_not_exists), namespace.name())
108}
109
110pub fn render_enum(sumtype: &SumType, resolver: &NameResolver, if_not_exists: bool) -> Result<String, ExportError> {
111	let name = qualified_name(resolver, sumtype.namespace.0, &sumtype.name, &sumtype.name)?;
112	let mut variants = sumtype.variants.clone();
113	variants.sort_by_key(|v| v.tag);
114
115	let mut rendered_variants = Vec::new();
116	for variant in &variants {
117		if variant.fields.is_empty() {
118			rendered_variants.push(variant.name.clone());
119		} else {
120			let fields: Result<Vec<_>, _> =
121				variant.fields.iter().map(|f| render_field(f, resolver, &sumtype.name)).collect();
122			rendered_variants.push(format!("{} {{ {} }}", variant.name, fields?.join(", ")));
123		}
124	}
125
126	Ok(format!("CREATE ENUM{} {} {{ {} }};", keyword_prefix(if_not_exists), name, rendered_variants.join(", ")))
127}
128
129pub fn render_dictionary(
130	dictionary: &Dictionary,
131	resolver: &NameResolver,
132	if_not_exists: bool,
133) -> Result<String, ExportError> {
134	let name = qualified_name(resolver, dictionary.namespace.0, &dictionary.name, &dictionary.name)?;
135	let value_type = render_value_type(&dictionary.value_type, &dictionary.name)?;
136	let id_type = render_value_type(&dictionary.id_type, &dictionary.name)?;
137	Ok(format!("CREATE DICTIONARY{} {} FOR {} AS {};", keyword_prefix(if_not_exists), name, value_type, id_type))
138}
139
140pub fn render_table(table: &Table, resolver: &NameResolver, if_not_exists: bool) -> Result<String, ExportError> {
141	let name = qualified_name(resolver, table.namespace.0, &table.name, &table.name)?;
142	let columns = render_columns_block(&table.columns, resolver, &table.name)?;
143	let with = if table.partition_by.is_empty() {
144		String::new()
145	} else {
146		format!(" WITH {{ partition: {{ by: {{ {} }} }} }}", table.partition_by.join(", "))
147	};
148	Ok(format!("CREATE TABLE{} {} {}{};", keyword_prefix(if_not_exists), name, columns, with))
149}
150
151pub fn render_ringbuffer(ringbuffer: &RingBuffer, resolver: &NameResolver) -> Result<String, ExportError> {
152	let name = qualified_name(resolver, ringbuffer.namespace.0, &ringbuffer.name, &ringbuffer.name)?;
153	let columns = render_columns_block(&ringbuffer.columns, resolver, &ringbuffer.name)?;
154
155	let mut with = format!("capacity: {}", ringbuffer.capacity);
156	if !ringbuffer.partition_by.is_empty() {
157		with.push_str(&format!(", partition: {{ by: {{ {} }} }}", ringbuffer.partition_by.join(", ")));
158	}
159
160	Ok(format!("CREATE RINGBUFFER {} {} WITH {{ {} }};", name, columns, with))
161}
162
163pub fn render_queue(queue: &Queue, resolver: &NameResolver) -> Result<String, ExportError> {
164	let name = qualified_name(resolver, queue.namespace.0, &queue.name, &queue.name)?;
165	let columns = render_columns_block(&queue.columns, resolver, &queue.name)?;
166
167	let mut options: Vec<String> = vec![render_queue_dispatch(&queue.dispatch)];
168	if let Some(deduplicate) = &queue.deduplicate {
169		let by = deduplicate.by.join(", ");
170		if deduplicate.is_forever() {
171			options.push(format!("deduplicate: {{ by: {{{}}} }}", by));
172		} else {
173			options.push(format!("deduplicate: {{ by: {{{}}}, ttl: {} }}", by, deduplicate.ttl));
174		}
175	}
176	if let Some(done) = &queue.retention.done {
177		options.push(format!("retention: {{ done: {} }}", done));
178	}
179	if queue.retry.attempts != Queue::DEFAULT_RETRY_ATTEMPTS || queue.retry.backoff != Queue::DEFAULT_RETRY_BACKOFF
180	{
181		options.push(format!(
182			"retry: {{ attempts: {}, backoff: {} }}",
183			queue.retry.attempts, queue.retry.backoff
184		));
185	}
186
187	Ok(format!("CREATE QUEUE {} {} WITH {{ {} }};", name, columns, options.join(", ")))
188}
189
190fn render_queue_dispatch(dispatch: &QueueDispatch) -> String {
191	match dispatch {
192		QueueDispatch::Fifo {
193			partitions,
194			ordered_by,
195		} => {
196			let mut inner: Vec<String> = Vec::new();
197			if *partitions != Queue::DEFAULT_PARTITIONS {
198				inner.push(format!("partitions: {}", partitions));
199			}
200			if let Some(ordered_by) = ordered_by {
201				inner.push(format!("ordered_by: {}", ordered_by));
202			}
203			if inner.is_empty() {
204				"fifo: {}".to_string()
205			} else {
206				format!("fifo: {{ {} }}", inner.join(", "))
207			}
208		}
209	}
210}
211
212pub fn render_series(series: &Series, resolver: &NameResolver) -> Result<String, ExportError> {
213	let name = qualified_name(resolver, series.namespace.0, &series.name, &series.name)?;
214	let columns = render_columns_block(&series.columns, resolver, &series.name)?;
215
216	let mut with = format!("key: {}", series.key.column());
217
218	if let Some(tag_id) = &series.tag {
219		let id = tag_id.to_u64();
220		let resolved = resolver.sumtype(id).ok_or_else(|| ExportError::UnresolvedReference {
221			kind: "sumtype",
222			id,
223			object: series.name.clone(),
224		})?;
225		with.push_str(&format!(", tag: {}", resolved.qualified_name));
226	}
227
228	if let SeriesKey::DateTime {
229		precision,
230		..
231	} = &series.key
232	{
233		with.push_str(&format!(", precision: {}", render_precision(precision)));
234	}
235
236	if !series.partition_by.is_empty() {
237		with.push_str(&format!(", partition: {{ by: {{ {} }} }}", series.partition_by.join(", ")));
238	}
239
240	Ok(format!("CREATE SERIES {} {} WITH {{ {} }};", name, columns, with))
241}
242
243fn render_precision(precision: &TimestampPrecision) -> &'static str {
244	match precision {
245		TimestampPrecision::Second => "second",
246		TimestampPrecision::Millisecond => "millisecond",
247		TimestampPrecision::Microsecond => "microsecond",
248		TimestampPrecision::Nanosecond => "nanosecond",
249	}
250}