1use std::time::Duration;
2
3use hydracache::{CacheKeyBuilder, CacheOptions, RefreshOptions, TagSet};
4
5use crate::{
6 CacheEntity, DeclaredLintMode, DeclaredRelation, DimensionAllow, DimensionProfile,
7 DimensionValidationMode, LintFinding, PolicyLintMetadata, ProfileValidation,
8};
9
10const SHORT_LIVED_TTL: Duration = Duration::from_secs(30);
11const READ_MOSTLY_TTL: Duration = Duration::from_secs(300);
12const PER_ENTITY_TTL: Duration = Duration::from_secs(300);
13const NEGATIVE_CACHE_TTL: Duration = Duration::from_secs(30);
14
15#[derive(Debug, Clone, Default, PartialEq, Eq)]
43pub struct QueryCachePolicy {
44 name: Option<String>,
45 key: Option<String>,
46 tags: TagSet,
47 ttl: Option<Duration>,
48 refresh: Option<RefreshOptions>,
49 required_dimensions: Vec<String>,
50 key_dimension_labels: Vec<String>,
51 tag_dimension_labels: Vec<String>,
52 dimension_profile: Option<DimensionProfile>,
53 dimension_validation_mode: DimensionValidationMode,
54 dimension_allow: Vec<DimensionAllow>,
55 lint_metadata: Option<PolicyLintMetadata>,
56}
57
58impl QueryCachePolicy {
59 pub fn new() -> Self {
61 Self::default()
62 }
63
64 pub fn short_lived() -> Self {
81 Self::new().ttl(SHORT_LIVED_TTL)
82 }
83
84 pub fn read_mostly() -> Self {
89 Self::new().ttl(READ_MOSTLY_TTL)
90 }
91
92 pub fn per_entity() -> Self {
98 Self::new().ttl(PER_ENTITY_TTL)
99 }
100
101 pub fn no_ttl_explicit_invalidation() -> Self {
107 Self::new()
108 }
109
110 pub fn negative_cache() -> Self {
116 Self::new().ttl(NEGATIVE_CACHE_TTL)
117 }
118
119 pub fn named(name: impl Into<String>) -> Self {
121 Self::new().with_name(name)
122 }
123
124 pub fn name(&self) -> Option<&str> {
126 self.name.as_deref()
127 }
128
129 pub fn key_value(&self) -> Option<&str> {
131 self.key.as_deref()
132 }
133
134 pub fn tags_value(&self) -> &[String] {
136 self.tags.as_slice()
137 }
138
139 pub fn ttl_value(&self) -> Option<Duration> {
141 self.ttl
142 }
143
144 pub fn refresh_policy_value(&self) -> Option<RefreshOptions> {
146 self.refresh
147 }
148
149 pub fn required_dimensions_value(&self) -> &[String] {
153 &self.required_dimensions
154 }
155
156 pub fn key_dimension_labels(&self) -> &[String] {
158 &self.key_dimension_labels
159 }
160
161 pub fn tag_dimension_labels(&self) -> &[String] {
163 &self.tag_dimension_labels
164 }
165
166 pub fn dimension_profile(&self) -> Option<&DimensionProfile> {
168 self.dimension_profile.as_ref()
169 }
170
171 pub fn dimension_validation_mode(&self) -> DimensionValidationMode {
173 self.dimension_validation_mode
174 }
175
176 pub fn lint_metadata(&self) -> Option<&PolicyLintMetadata> {
178 self.lint_metadata.as_ref()
179 }
180
181 pub fn with_name(mut self, name: impl Into<String>) -> Self {
183 self.name = Some(name.into());
184 self
185 }
186
187 pub fn key(mut self, key: impl Into<String>) -> Self {
189 self.key = Some(key.into());
190 self
191 }
192
193 pub fn key_builder(self, key: CacheKeyBuilder) -> Self {
195 self.key(key.build_string())
196 }
197
198 pub fn for_entity(mut self, kind: impl ToString, id: impl ToString) -> Self {
200 let key = entity_key(kind, id);
201 self.key = Some(key.clone());
202 self.tags = self.tags.tag(key);
203 self
204 }
205
206 pub fn for_cache_entity<T>(mut self, id: T::Id) -> Self
208 where
209 T: CacheEntity,
210 {
211 let key = T::cache_key_for(&id);
212 self.key = Some(key);
213 self.tags = self.tags.tag(T::entity_tag_for(&id));
214 self.tags = append_optional_tag(self.tags, T::collection_tag());
215 self
216 }
217
218 pub fn collection(mut self, name: impl ToString) -> Self {
220 let tag = collection_tag(name);
221 self.key = Some(tag.clone());
222 self.tags = self.tags.tag(tag);
223 self
224 }
225
226 pub fn tag(mut self, tag: impl Into<String>) -> Self {
228 self.tags = self.tags.tag(tag);
229 self
230 }
231
232 pub fn collection_tag(mut self, name: impl ToString) -> Self {
234 self.tags = self.tags.tag(collection_tag(name));
235 self
236 }
237
238 pub fn tags<I, S>(mut self, tags: I) -> Self
240 where
241 I: IntoIterator<Item = S>,
242 S: Into<String>,
243 {
244 self.tags = self.tags.tags(tags);
245 self
246 }
247
248 pub fn tag_set(mut self, tags: TagSet) -> Self {
250 self.tags = tags;
251 self
252 }
253
254 pub fn ttl(mut self, ttl: Duration) -> Self {
256 self.ttl = Some(ttl);
257 self
258 }
259
260 pub fn refresh_policy(mut self, refresh: RefreshOptions) -> Self {
262 self.refresh = Some(refresh);
263 self
264 }
265
266 pub fn required_dimensions<I, S>(mut self, dimensions: I) -> Self
268 where
269 I: IntoIterator<Item = S>,
270 S: Into<String>,
271 {
272 self.required_dimensions = dimensions.into_iter().map(Into::into).collect();
273 self
274 }
275
276 pub fn required_dimension(mut self, dimension: impl Into<String>) -> Self {
278 self.required_dimensions.push(dimension.into());
279 self
280 }
281
282 pub fn with_key_dimension_labels<I, S>(mut self, labels: I) -> Self
284 where
285 I: IntoIterator<Item = S>,
286 S: Into<String>,
287 {
288 self.key_dimension_labels = labels.into_iter().map(Into::into).collect();
289 self
290 }
291
292 pub fn with_tag_dimension_labels<I, S>(mut self, labels: I) -> Self
294 where
295 I: IntoIterator<Item = S>,
296 S: Into<String>,
297 {
298 self.tag_dimension_labels = labels.into_iter().map(Into::into).collect();
299 self
300 }
301
302 pub fn with_dimension_profile(mut self, profile: DimensionProfile) -> Self {
304 let required = profile
305 .requirements()
306 .into_iter()
307 .map(|requirement| requirement.label().to_owned());
308 self.required_dimensions.extend(required);
309 self.dimension_profile = Some(profile);
310 self
311 }
312
313 pub fn with_dimension_validation_mode(mut self, mode: DimensionValidationMode) -> Self {
315 self.dimension_validation_mode = mode;
316 self
317 }
318
319 pub fn allow_dimension_violation(
321 mut self,
322 label: impl Into<String>,
323 reason: impl Into<String>,
324 ) -> std::result::Result<Self, crate::DimensionAllowError> {
325 self.dimension_allow
326 .push(DimensionAllow::new(label, reason)?);
327 Ok(self)
328 }
329
330 pub fn validate_dimension_profile(&self) -> ProfileValidation {
332 let Some(profile) = &self.dimension_profile else {
333 return ProfileValidation::Pass;
334 };
335
336 let mut missing = Vec::new();
337 let mut unlinked = Vec::new();
338 for requirement in profile.requirements() {
339 let label = requirement.label();
340 if !self.key_dimension_labels.iter().any(|known| known == label) {
341 missing.push(label.to_owned());
342 } else if requirement.require_key_tag_link()
343 && !self.tag_dimension_labels.iter().any(|known| known == label)
344 {
345 unlinked.push(label.to_owned());
346 }
347 }
348
349 let status = if !missing.is_empty() {
350 ProfileValidation::MissingDimensions(missing)
351 } else if !unlinked.is_empty() {
352 ProfileValidation::UnlinkedDimensions(unlinked)
353 } else {
354 ProfileValidation::Pass
355 };
356
357 self.apply_dimension_allow(status)
358 }
359
360 pub fn enforce_dimension_profile(&self) -> crate::Result<()> {
362 let status = self.validate_dimension_profile();
363 if self.dimension_validation_mode == DimensionValidationMode::Deny && !status.is_pass() {
364 return Err(hydracache::CacheError::Backend(format!(
365 "query cache policy dimension profile violation: {status}"
366 ))
367 .into());
368 }
369 Ok(())
370 }
371
372 pub fn lint_sql(mut self, sql: impl Into<String>) -> Self {
374 self.lint_metadata_mut().sql = Some(sql.into());
375 self
376 }
377
378 pub fn dependency_lint_mode(mut self, mode: DeclaredLintMode) -> Self {
380 self.lint_metadata_mut().mode = mode;
381 self
382 }
383
384 pub fn declared_dependency(mut self, relation: DeclaredRelation) -> Self {
386 self.lint_metadata_mut().declared.push(relation);
387 self
388 }
389
390 pub fn declared_dependencies<I>(mut self, relations: I) -> Self
392 where
393 I: IntoIterator<Item = DeclaredRelation>,
394 {
395 self.lint_metadata_mut().declared.extend(relations);
396 self
397 }
398
399 pub fn lint_allow(mut self, finding: LintFinding, reason: impl Into<String>) -> Self {
401 self.lint_metadata_mut()
402 .suppressions
403 .push(crate::LintSuppression::new(finding, reason));
404 self
405 }
406
407 pub(crate) fn cache_options(&self) -> CacheOptions {
408 let mut options = CacheOptions::new().tag_set(self.tags.clone());
409 if let Some(ttl) = self.ttl {
410 options = options.ttl(ttl);
411 }
412 options
413 }
414
415 fn lint_metadata_mut(&mut self) -> &mut PolicyLintMetadata {
416 self.lint_metadata
417 .get_or_insert_with(PolicyLintMetadata::default)
418 }
419
420 fn apply_dimension_allow(&self, status: ProfileValidation) -> ProfileValidation {
421 let labels = match &status {
422 ProfileValidation::MissingDimensions(labels)
423 | ProfileValidation::UnlinkedDimensions(labels) => labels,
424 _ => return status,
425 };
426
427 let Some(allow) = self
428 .dimension_allow
429 .iter()
430 .find(|allow| labels.iter().any(|label| label == allow.label()))
431 else {
432 return status;
433 };
434
435 ProfileValidation::Allowed {
436 status: Box::new(status),
437 reason: allow.reason().to_owned(),
438 }
439 }
440}
441
442pub(crate) fn entity_key(kind: impl ToString, id: impl ToString) -> String {
443 CacheKeyBuilder::new().entity(kind, id).build_string()
444}
445
446pub(crate) fn collection_tag(name: impl ToString) -> String {
447 CacheKeyBuilder::from_segment(name).build_string()
448}
449
450fn append_optional_tag(tags: TagSet, tag: Option<String>) -> TagSet {
451 match tag {
452 Some(tag) => tags.tag(tag),
453 None => tags,
454 }
455}