criterium 3.1.3

Lightweigt dynamic database queries for rusqlite.
Documentation
// SPDX-FileCopyrightText: 2025 Slatian
//
// SPDX-License-Identifier: LGPL-3.0-only

use std::cell::Cell;
use std::fmt::Display;
use std::ops::Deref;
use std::rc::Rc;

use crate::BooleanJoiner;

/// A wrapped prefix string with some additional context
/// that when neccessary can generate unique table names in the context of a query.
///
/// This is neccessary when the same table may a used multiple times
/// for different reasons in one query.
///
/// Example: you have a table with messages and one with contacts and your query scenario makes it neccessary to join the contacts table once for the sender and once for the recipient. You have to join them under different names. This is what the prefix is for.
///
/// TL;DR:
/// * Pass on the prefix you already have.
/// * When a join is involved use `with()` to set a reason.
/// * Use a `Prefix::empty()` when you kick off the Database query generation.
/// * Wheter you generate a new Prefix or `clone()` an existing one matters.
///   The clones share a common counter for generating unique names within a tree.
///   (use the provided functions for cloning.)
///
/// The uniqueness happens through an internal counter that is shared between all clones.
/// This internal counter is currently implemented using a [Cell][std::cell::Cell]
/// and is **not threadsafe!**
/// (This shouldn't be an issue in most cases as one usually doesn't
/// generate SQL queries across multiple threads.)
#[derive(Debug, Clone)]
pub struct Prefix {
	counter: Rc<Cell<u64>>,
	prefix: String,
	in_joiner_block: Option<BooleanJoiner>,
	block_number: u64,
}

impl Prefix {
	/// Creates a new Prefix.
	///
	/// Note: You only want to do this when kicking off a query assembly chain.
	pub fn new(prefix: String) -> Self {
		Prefix {
			counter: Rc::new(0.into()),
			prefix: prefix,
			in_joiner_block: None,
			block_number: 0,
		}
	}

	/// Creates a Prefix and initalizes to an empty prefix.
	///
	/// This is the usual case needed when starting a query assembly chain.
	///
	/// Note: You only want to do this when kicking off a query assembly chain.
	pub fn empty() -> Self {
		Self::new("".to_string())
	}

	/// Creates a clone of the prefix that is aware of the kind of joiner block it is in.
	/// This **must** be called very time the context changes.
	///
	/// Will use [get_number()][Self::get_number] to generate a block number.
	/// This will be used when a unique number per logic block is enough.
	pub fn in_block(&self, joiner: Option<BooleanJoiner>) -> Self {
		let mut new = self.clone();
		new.in_joiner_block = joiner;
		new.block_number = new.get_number();
		return new;
	}

	/// Appends an extra token to the prefix.
	/// To make your life easier make sure that the
	/// token ends in an underscore `_` character.
	///
	/// You probably want to encode the reason
	/// for joining a table in this extra token.
	///
	/// ```
	/// use criterium::sql::Prefix;
	///
	/// let pfx = Prefix::new("test_".to_string());
	///
	/// assert_eq!(pfx.with("reason_").to_string(), "test_reason_".to_string());
	/// ```
	pub fn with(&self, token: &str) -> Self {
		let mut new = self.clone();
		new.prefix += token;
		return new;
	}

	/// Appends a number and an underscore to the given reason
	/// (which should also end in an underscore `_`).
	///
	/// If the `group` is `Some` and equals the current blocktype
	/// the block number generated when [in_block()][Self::in_block()]
	/// was called will be used.
	///
	/// In all other cases a new number will be generated
	/// using [get_number()][Self::get_number].
	///
	/// The result is meant to be added to another prefix using the
	/// [with()][Self::with] function and also used with SQL join assemblers.
	/// (you probably want to use [with_unique_reason()][Self::with_unique_reason])
	///
	/// When subqueries are involved you want to set the group to `None`.
	pub fn unique_reason(&self, reason: &str, group: Option<BooleanJoiner>) -> String {
		let n = if group.is_some() && group == self.in_joiner_block {
			self.block_number
		} else {
			self.get_number()
		};
		return format!("{reason}{n}_");
	}

	/// Convenience wrapper for `self.with(self.unique_reason(…))`
	#[inline]
	pub fn with_unique_reason(&self, reason: &str, group: Option<BooleanJoiner>) -> Self {
		self.with(&self.unique_reason(reason, group))
	}

	/// Increments the counter by one and returns the result.
	///
	/// This function is **not threadsafe!**
	pub fn get_number(&self) -> u64 {
		let n = (*self.counter).get() + 1;
		(*self.counter).set(n);
		return n;
	}

	/// Set an entirely new prefix for a clone of this Prefix.
	pub fn set(&self, prefix: String) -> Self {
		Prefix {
			counter: self.counter.clone(),
			prefix: prefix,
			in_joiner_block: self.in_joiner_block.clone(),
			block_number: self.block_number,
		}
	}
}

impl Display for Prefix {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		self.prefix.fmt(f)
	}
}

impl Deref for Prefix {
	type Target = String;

	fn deref(&self) -> &String {
		&self.prefix
	}
}