immigrant_schema/
scalar.rs

1use std::mem;
2
3use itertools::{Either, Itertools};
4
5use super::sql::Sql;
6use crate::{
7	attribute::AttributeList,
8	changelist::IsCompatible,
9	column::ColumnAnnotation,
10	composite::FieldAnnotation,
11	def_name_impls, derive_is_isomorph_by_id_name,
12	diagnostics::Report,
13	index::{Check, Index, PrimaryKey, UniqueConstraint},
14	names::{
15		DbEnumItem, DbNativeType, EnumItemDefName, EnumItemKind, TypeDefName, TypeIdent, TypeKind,
16	},
17	root::Schema,
18	uid::{next_uid, OwnUid, RenameExt, RenameMap},
19	HasIdent, SchemaEnum, SchemaScalar,
20};
21
22#[derive(Debug)]
23pub struct EnumItem {
24	uid: OwnUid,
25	pub docs: Vec<String>,
26	name: EnumItemDefName,
27}
28impl EnumItem {
29	pub fn new(docs: Vec<String>, name: EnumItemDefName) -> Self {
30		Self {
31			uid: next_uid(),
32			docs,
33			name,
34		}
35	}
36}
37def_name_impls!(EnumItem, EnumItemKind);
38derive_is_isomorph_by_id_name!(EnumItem);
39
40/// FIXME: Rename to EnumItem, rename old EnumItem to Variant
41#[derive(Debug, Clone, Copy)]
42pub struct EnumItemHandle<'a> {
43	enum_: SchemaEnum<'a>,
44	item: &'a EnumItem,
45}
46def_name_impls!(EnumItemHandle<'_>, EnumItemKind);
47derive_is_isomorph_by_id_name!(EnumItemHandle<'_>);
48
49impl std::ops::Deref for EnumItemHandle<'_> {
50	type Target = EnumItem;
51
52	fn deref(&self) -> &Self::Target {
53		self.item
54	}
55}
56
57impl IsCompatible for EnumItemHandle<'_> {
58	fn is_compatible(
59		&self,
60		_new: &Self,
61		_rn: &RenameMap,
62		_a: &mut Report,
63		_b: &mut Report,
64	) -> bool {
65		true
66	}
67}
68
69#[derive(Debug)]
70pub struct Enum {
71	uid: OwnUid,
72	name: TypeDefName,
73	pub docs: Vec<String>,
74	pub attrlist: AttributeList,
75	pub items: Vec<EnumItem>,
76}
77impl Enum {
78	pub fn new(
79		docs: Vec<String>,
80		attrlist: AttributeList,
81		name: TypeDefName,
82		items: Vec<EnumItem>,
83	) -> Self {
84		Self {
85			uid: next_uid(),
86			name,
87			docs,
88			attrlist,
89			items,
90		}
91	}
92	pub fn db_items(&self, rn: &RenameMap) -> Vec<DbEnumItem> {
93		self.items.iter().map(|i| i.db(rn)).collect()
94	}
95	pub fn db_type(&self, rn: &RenameMap) -> DbNativeType {
96		DbNativeType::unchecked_from(self.db(rn))
97	}
98}
99def_name_impls!(Enum, TypeKind);
100
101impl SchemaEnum<'_> {
102	pub fn items(&self) -> impl Iterator<Item = EnumItemHandle<'_>> {
103		self.items
104			.iter()
105			.map(|item| EnumItemHandle { enum_: *self, item })
106	}
107}
108
109#[derive(Debug, Default)]
110pub(crate) struct PropagatedScalarData {
111	pub annotations: Vec<ColumnAnnotation>,
112	pub field_annotations: Vec<FieldAnnotation>,
113}
114impl PropagatedScalarData {
115	/// Returns true if extended
116	pub(crate) fn extend(&mut self, other: Self) {
117		self.annotations.extend(other.annotations);
118		self.field_annotations.extend(other.field_annotations);
119	}
120	pub(crate) fn is_empty(&self) -> bool {
121		self.annotations.is_empty() && self.field_annotations.is_empty()
122	}
123}
124
125#[derive(Debug, Clone)]
126pub enum InlineSqlTypePart {
127	Raw(String),
128	TypeRef(TypeIdent),
129}
130#[derive(Debug, Clone)]
131pub struct InlineSqlType(pub Vec<InlineSqlTypePart>);
132impl InlineSqlType {
133	fn expand(&self, rn: &RenameMap, schema: &Schema, report: &mut Report) -> DbNativeType {
134		let mut out = String::new();
135		for p in &self.0 {
136			match p {
137				InlineSqlTypePart::Raw(r) => out.push_str(r),
138				InlineSqlTypePart::TypeRef(ident) => {
139					out.push_str(schema.native_type(ident, rn, report).raw())
140				}
141			}
142		}
143		DbNativeType::new(&out)
144	}
145	fn dependencies(&self, schema: &Schema, out: &mut Vec<TypeIdent>) {
146		for v in &self.0 {
147			if let InlineSqlTypePart::TypeRef(t) = v {
148				schema.schema_ty(*t).depend_on(out);
149			}
150		}
151	}
152}
153
154/// Even though CREATE DOMAIN allows domains to be constrained as non-nulls, currently it is not possible to make
155/// scalar non-null, you should create a constraint, and then make a column not null, because non-null domain
156/// might work not as user would expect. I.e it allows value to be null in case of outer joins.
157#[derive(Debug)]
158pub struct Scalar {
159	uid: OwnUid,
160	name: TypeDefName,
161	pub docs: Vec<String>,
162	pub attrlist: AttributeList,
163	native: InlineSqlType,
164	pub annotations: Vec<ScalarAnnotation>,
165	inlined: bool,
166}
167def_name_impls!(Scalar, TypeKind);
168impl Scalar {
169	pub fn new(
170		docs: Vec<String>,
171		attrlist: AttributeList,
172		name: TypeDefName,
173		native: InlineSqlType,
174		annotations: Vec<ScalarAnnotation>,
175	) -> Self {
176		Self {
177			uid: next_uid(),
178			name,
179			docs,
180			attrlist,
181			native,
182			annotations,
183			inlined: false,
184		}
185	}
186	pub(crate) fn propagate(&mut self, inline: bool) -> PropagatedScalarData {
187		let annotations = mem::take(&mut self.annotations);
188		let field_annotations = annotations
189			.iter()
190			.flat_map(|a| a.propagate_to_field(inline))
191			.collect_vec();
192		let (annotations, retained) = annotations
193			.into_iter()
194			.partition_map(|a| a.propagate_to_column(inline));
195		self.annotations = retained;
196		if inline {
197			self.inlined = true;
198		}
199		PropagatedScalarData {
200			annotations,
201			field_annotations,
202		}
203	}
204	pub fn is_always_inline(&self) -> bool {
205		self.annotations
206			.iter()
207			.any(|a| matches!(a, ScalarAnnotation::Inline))
208	}
209	pub fn is_external(&self) -> bool {
210		self.annotations
211			.iter()
212			.any(|a| matches!(a, ScalarAnnotation::External))
213	}
214	pub fn inlined(&self) -> bool {
215		self.inlined
216	}
217}
218
219impl SchemaScalar<'_> {
220	pub fn depend_on(&self, out: &mut Vec<TypeIdent>) {
221		assert!(
222			!self.is_always_inline() || self.inlined,
223			"always-inline type was not inlined"
224		);
225		if self.inlined() {
226			self.type_dependencies(out);
227		} else {
228			out.push(self.id());
229		}
230	}
231	pub fn type_dependencies(&self, out: &mut Vec<TypeIdent>) {
232		assert!(
233			!self.is_always_inline() || self.inlined,
234			"always-inline type was not inlined"
235		);
236		self.native.dependencies(self.schema, out);
237	}
238	pub fn inner_type(&self, rn: &RenameMap, report: &mut Report) -> DbNativeType {
239		self.native.expand(rn, self.schema, report)
240	}
241	pub fn native(&self, rn: &RenameMap, report: &mut Report) -> DbNativeType {
242		assert!(
243			!self.is_always_inline() || self.inlined,
244			"always-inline type was not inlined"
245		);
246		if self.inlined {
247			assert!(
248				!self
249					.annotations
250					.iter()
251					.any(ScalarAnnotation::is_inline_target),
252				"inlined scalars may not have inline target scalars"
253			);
254			self.native.expand(rn, self.schema, report)
255		} else {
256			DbNativeType::unchecked_from(self.db(rn))
257		}
258	}
259}
260
261#[derive(Debug)]
262pub enum ScalarAnnotation {
263	/// Moved to column if inlined.
264	Default(Sql),
265	/// Moved to column if inlined.
266	Check(Check),
267	/// Moved to column.
268	PrimaryKey(PrimaryKey),
269	/// Moved to column.
270	Unique(UniqueConstraint),
271	/// Moved to column.
272	Index(Index),
273	// Always convert to native types, even if database supports domain types.
274	Inline,
275	External,
276}
277impl ScalarAnnotation {
278	pub fn as_check(&self) -> Option<&Check> {
279		match self {
280			Self::Check(c) => Some(c),
281			_ => None,
282		}
283	}
284	/// Should annotation be removed after inlining?
285	fn is_inline_target(&self) -> bool {
286		!matches!(self, Self::Inline | Self::External)
287	}
288	fn propagate_to_field(&self, inline: bool) -> Option<FieldAnnotation> {
289		Some(match self {
290			ScalarAnnotation::Check(c) if inline => FieldAnnotation::Check(c.clone_for_propagate()),
291			_ => return None,
292		})
293	}
294	fn propagate_to_column(self, inline: bool) -> Either<ColumnAnnotation, Self> {
295		Either::Left(match self {
296			ScalarAnnotation::PrimaryKey(p) => ColumnAnnotation::PrimaryKey(p),
297			ScalarAnnotation::Unique(u) => ColumnAnnotation::Unique(u),
298			ScalarAnnotation::Index(i) => ColumnAnnotation::Index(i),
299			ScalarAnnotation::Default(d) if inline => ColumnAnnotation::Default(d),
300			ScalarAnnotation::Check(c) if inline => ColumnAnnotation::Check(c),
301			_ => return Either::Right(self),
302		})
303	}
304}