use crate::data_type::DataType;
use crate::engine_version::EngineVersion;
use crate::evolution::EvolutionAnswers;
use crate::merge_shape::MergeShape;
use crate::renderer::DdlRenderer;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct TextLimits {
pub longest_varchar: Option<u32>,
pub counted_in: VarcharUnit,
pub source: LimitSource,
pub text_is_not: Option<&'static str>,
}
impl TextLimits {
#[must_use]
pub const fn new(longest_varchar: u32, counted_in: VarcharUnit, source: LimitSource) -> Self {
Self { longest_varchar: Some(longest_varchar), counted_in, source, text_is_not: None }
}
#[must_use]
pub const fn and_text_is_not(self, what: &'static str) -> Self {
Self { text_is_not: Some(what), ..self }
}
#[must_use]
pub fn misfit(&self, length: u32) -> Option<String> {
let longest = self.longest_varchar?;
(length > longest).then(|| {
format!(
"its varchar holds at most {longest} {}, and this asks for {length}",
self.counted_in.word()
)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum VarcharUnit {
Characters,
Bytes,
}
impl VarcharUnit {
#[must_use]
pub const fn word(self) -> &'static str {
match self {
Self::Characters => "characters",
Self::Bytes => "bytes",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LimitSource {
MeasuredOnTheEngine,
TheEnginesDocumentation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Limit {
pub most: u32,
pub source: LimitSource,
}
impl Limit {
#[must_use]
pub const fn measured(most: u32) -> Self {
Self { most, source: LimitSource::MeasuredOnTheEngine }
}
#[must_use]
pub const fn documented(most: u32) -> Self {
Self { most, source: LimitSource::TheEnginesDocumentation }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TypeLimits {
pub timestamp_digits: Option<Limit>,
pub time_digits: Option<Limit>,
pub longest_char: Option<Limit>,
pub char_counted_in: VarcharUnit,
pub longest_binary: Option<Limit>,
pub longest_varbinary: Option<Limit>,
}
impl TypeLimits {
pub const NONE: Self = Self {
timestamp_digits: None,
time_digits: None,
longest_char: None,
char_counted_in: VarcharUnit::Characters,
longest_binary: None,
longest_varbinary: None,
};
#[must_use]
pub fn misfit(&self, data_type: &DataType) -> Option<Misfit> {
match data_type {
DataType::Timestamp(moment) => {
self.too_many_digits(moment.precision, |most| format!("timestamp({most})"))
}
DataType::TimestampTz(moment) => {
self.too_many_digits(moment.precision, |_| "timestamptz".to_owned())
}
DataType::DateTime(moment) => {
self.too_many_digits(moment.precision, |_| "datetime".to_owned())
}
DataType::Time(time) => {
let limit = self.time_digits?;
let asked = u32::from(time.precision?);
(asked > limit.most).then(|| Misfit {
why: if limit.most == 0 {
format!(
"its times of day keep whole seconds only, and this asks for {asked} \
digits of a second"
)
} else {
format!(
"its times of day keep at most {} digits of a second, and this asks \
for {asked}",
limit.most
)
},
kind: "time",
fits: if limit.most == 0 {
"time".to_owned()
} else {
format!("time({})", limit.most)
},
})
}
DataType::Char(text) => {
let limit = self.longest_char?;
(text.length > limit.most).then(|| Misfit {
why: format!(
"its char holds at most {} {}, and this asks for {}",
limit.most,
self.char_counted_in.word(),
text.length
),
kind: "char",
fits: format!("char({})", limit.most),
})
}
DataType::Binary(bytes) => {
let limit = self.longest_binary?;
(bytes.length > limit.most).then(|| Misfit {
why: format!(
"its binary holds at most {} bytes, and this asks for {}",
limit.most, bytes.length
),
kind: "binary",
fits: format!("binary({})", limit.most),
})
}
DataType::VarBinary(bytes) => {
let limit = self.longest_varbinary?;
let asked = bytes.max_length?;
(asked > limit.most).then(|| Misfit {
why: format!(
"its varbinary holds at most {} bytes, and this asks for {asked}",
limit.most
),
kind: "varbinary",
fits: format!("varbinary({})", limit.most),
})
}
_ => None,
}
}
fn too_many_digits(&self, asked: u8, fits: impl Fn(u32) -> String) -> Option<Misfit> {
let limit = self.timestamp_digits?;
(u32::from(asked) > limit.most).then(|| Misfit {
why: format!(
"its timestamps keep at most {} digits of a second, and this asks for {asked}",
limit.most
),
kind: "timestamp",
fits: fits(limit.most),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Misfit {
pub why: String,
pub kind: &'static str,
pub fits: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DecimalLimits {
pub max_precision: u16,
pub max_scale: u16,
pub scale_within_precision: bool,
pub max_integer_digits: Option<u16>,
}
impl DecimalLimits {
#[must_use]
pub fn misfit(&self, precision: u16, scale: u16) -> Option<String> {
let most = self.max_precision;
if precision == 0 || precision > most {
return Some(format!(
"its decimals hold 1 to {most} digits, and this asks for {precision}"
));
}
if scale > self.max_scale {
return Some(format!(
"its decimals hold at most {} digits after the point, and this asks for {scale}",
self.max_scale
));
}
if self.scale_within_precision && scale > precision {
return Some(format!(
"its decimals hold no more digits after the point than in all, and this asks for \
{scale} of {precision}"
));
}
let before_point = precision.saturating_sub(scale);
if let Some(most) = self.max_integer_digits
&& before_point > most
{
return Some(format!(
"its decimals hold at most {most} digits before the point, and this asks for \
{before_point}"
));
}
None
}
#[must_use]
pub fn nearest(&self, precision: u16, scale: u16) -> (u16, u16) {
let mut scale = scale.min(self.max_scale);
let mut precision = precision.clamp(1, self.max_precision);
if self.scale_within_precision {
scale = scale.min(precision);
}
if let Some(most) = self.max_integer_digits {
precision = precision.min(scale + most).max(1);
}
if self.scale_within_precision {
scale = scale.min(precision);
}
(precision, scale)
}
}
pub trait DialectCapabilities: DdlRenderer {
fn merge_shape(&self) -> MergeShape {
MergeShape::None
}
fn catalogue_reach(&self) -> CatalogueReach {
CatalogueReach::Nothing(
"razeri has not written this platform's catalog query, and guessing at one would \
emit SQL that fails rather than a verdict",
)
}
fn evolution_answers(&self) -> EvolutionAnswers {
EvolutionAnswers::none()
}
fn supports_returning(&self) -> bool {
false
}
fn server_version_query(&self) -> Option<&str> {
None
}
fn parse_server_version(&self, answer: &str) -> Option<EngineVersion> {
answer.split_whitespace().next().and_then(|word| EngineVersion::parse(word).ok())
}
fn supports_cte(&self) -> bool {
true
}
fn supports_recursive_cte(&self) -> bool {
true
}
fn supports_window_functions(&self) -> bool {
true
}
fn supports_qualify(&self) -> bool {
false
}
fn supports_groups_frame(&self) -> bool {
false
}
fn supports_nulls_ordering(&self) -> bool {
true
}
fn supports_identity(&self) -> bool {
false
}
fn supports_sequences(&self) -> bool {
false
}
fn supports_schemas(&self) -> bool {
true
}
fn routine_is_callable_across_schemas(&self) -> bool {
true
}
fn supports_bulk_load(&self) -> bool {
false
}
fn supports_stored_procedures(&self) -> bool {
false
}
fn supports_stored_functions(&self) -> bool {
true
}
fn supports_table_functions(&self) -> bool {
true
}
fn supports_tasks(&self) -> bool {
false
}
fn supports_partitioning(&self) -> bool {
true
}
fn supports_materialized_views(&self) -> bool {
true
}
fn supports_generated_columns(&self) -> bool {
true
}
fn supports_json(&self) -> bool {
true
}
fn supports_unsigned_integers(&self) -> bool {
false
}
fn supports_arrays(&self) -> bool {
false
}
fn renders_nested_type_natively(&self, data_type: &DataType) -> bool {
let _ = data_type;
false
}
fn compares_nested_values(&self, data_type: &DataType) -> bool {
let _ = data_type;
false
}
fn groups_nested_values(&self, data_type: &DataType) -> bool {
let _ = data_type;
true
}
fn supports_boolean(&self) -> bool {
true
}
fn default_schema(&self) -> &'static str {
"public"
}
fn max_identifier_length(&self) -> usize {
63
}
fn max_columns(&self) -> usize {
10000
}
fn max_parameters(&self) -> usize {
0
}
fn decimal_limits(&self) -> Option<DecimalLimits> {
None
}
fn text_limits(&self) -> Option<TextLimits> {
None
}
fn type_limits(&self) -> Option<TypeLimits> {
None
}
fn keeps_a_time_zone(&self) -> bool {
true
}
fn supports_function_overloading(&self) -> bool {
false
}
fn supports_merge_star_syntax(&self) -> bool {
false
}
fn schema_namespace_keyword(&self) -> &'static str {
"SCHEMA"
}
fn supports_hash_comments(&self) -> bool {
false
}
fn batch_separator(&self) -> Option<&'static str> {
None
}
fn supports_multi_statement_transactions(&self) -> bool {
true
}
fn begin_transaction_keyword(&self) -> &'static str {
"START TRANSACTION"
}
fn commit_transaction_keyword(&self) -> &'static str {
"COMMIT"
}
fn begin_leaves_the_commit_to_the_client(&self) -> bool {
false
}
fn truncate_commits_implicitly(&self) -> bool {
false
}
fn refusal_stops_the_statement(&self) -> bool {
true
}
fn delimited_file_reads_null_word_as_missing(&self) -> bool {
false
}
fn supports_partition_function(&self) -> bool {
false
}
fn supports_partition_split_merge(&self) -> bool {
false
}
fn supports_columnstore_index(&self) -> bool {
false
}
fn columnstore_is_table_engine(&self) -> bool {
false
}
fn supports_clustering_key(&self) -> bool {
false
}
fn supports_table_compression(&self) -> bool {
false
}
fn partition_requires_pk_includes_field(&self) -> bool {
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CatalogueReach {
Whole,
HeldColumnsOnly(&'static str),
Nothing(&'static str),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SqlRenderer;
use crate::merge_shape::MergeRestrictions;
struct DefaultDialect;
impl SqlRenderer for DefaultDialect {
fn render_data_type(&self, _dt: &DataType) -> String {
"TEXT".to_owned()
}
fn quote_identifier(&self, name: &str) -> String {
format!("\"{name}\"")
}
fn qualify_name(&self, _schema: Option<&str>, name: &str) -> String {
name.to_owned()
}
fn render_current_timestamp(&self) -> String {
"CURRENT_TIMESTAMP".to_owned()
}
}
impl DdlRenderer for DefaultDialect {}
impl DialectCapabilities for DefaultDialect {}
#[test]
fn the_server_version_is_the_first_word_of_the_answer() {
let d = DefaultDialect;
assert_eq!(d.server_version_query(), None);
let read = d.parse_server_version("17 17.2");
assert_eq!(read.map(|v| v.to_string()), Some("17".to_owned()));
assert_eq!(d.parse_server_version(" 8.4 MySQL Community Server"), "8.4".parse().ok());
assert_eq!(d.parse_server_version(""), None);
assert_eq!(d.parse_server_version("PostgreSQL 17.2"), None);
}
fn assert_default_dml_capabilities(d: &impl DialectCapabilities) {
assert_eq!(d.merge_shape(), MergeShape::None);
assert!(!d.supports_on_conflict());
assert!(!d.supports_insert_overwrite());
assert!(!d.supports_update_from());
assert!(!d.supports_returning());
}
fn assert_default_query_capabilities(d: &impl DialectCapabilities) {
assert!(d.supports_cte());
assert!(d.supports_recursive_cte());
assert!(d.supports_window_functions());
assert!(d.supports_named_windows());
assert!(!d.supports_qualify());
assert!(!d.supports_groups_frame());
assert!(d.supports_nulls_ordering());
}
fn assert_default_ddl_and_type_capabilities(d: &impl DialectCapabilities) {
assert!(!d.supports_identity());
assert!(!d.supports_sequences());
assert!(d.supports_schemas());
assert!(!d.supports_bulk_load());
assert!(!d.supports_stored_procedures());
assert!(d.supports_stored_functions());
assert!(d.supports_table_functions());
assert!(!d.supports_tasks());
assert!(d.supports_partitioning());
assert!(d.supports_materialized_views());
assert!(d.supports_generated_columns());
assert!(d.supports_json());
assert!(!d.supports_unsigned_integers());
assert!(!d.supports_arrays());
assert!(d.supports_boolean());
}
fn assert_default_limits_and_storage_capabilities(d: &impl DialectCapabilities) {
assert_eq!(d.default_schema(), "public");
assert_eq!(d.max_identifier_length(), 63);
assert_eq!(d.max_columns(), 10000);
assert_eq!(d.max_parameters(), 0);
assert!(!d.supports_partition_function());
assert!(!d.supports_partition_split_merge());
assert!(!d.supports_columnstore_index());
assert!(!d.supports_clustering_key());
assert!(!d.supports_table_compression());
}
#[test]
fn defaults_are_conservative() {
let d = DefaultDialect;
assert_default_dml_capabilities(&d);
assert_default_query_capabilities(&d);
assert_default_ddl_and_type_capabilities(&d);
assert_default_limits_and_storage_capabilities(&d);
}
#[test]
fn overrides_work() {
struct PostgresLike;
impl SqlRenderer for PostgresLike {
fn render_data_type(&self, _dt: &DataType) -> String {
"TEXT".to_owned()
}
fn quote_identifier(&self, name: &str) -> String {
format!("\"{name}\"")
}
fn qualify_name(&self, _schema: Option<&str>, name: &str) -> String {
name.to_owned()
}
fn render_current_timestamp(&self) -> String {
"CURRENT_TIMESTAMP".to_owned()
}
fn supports_on_conflict(&self) -> bool {
true
}
}
impl DdlRenderer for PostgresLike {}
impl DialectCapabilities for PostgresLike {
fn merge_shape(&self) -> MergeShape {
MergeShape::Merge(MergeRestrictions::none())
}
fn supports_returning(&self) -> bool {
true
}
fn supports_identity(&self) -> bool {
true
}
fn supports_sequences(&self) -> bool {
true
}
fn supports_arrays(&self) -> bool {
true
}
fn max_columns(&self) -> usize {
1600
}
}
let d = PostgresLike;
assert!(d.merge_shape().merges());
assert!(d.supports_on_conflict());
assert!(d.supports_returning());
assert!(d.supports_identity());
assert!(d.supports_sequences());
assert!(d.supports_arrays());
assert_eq!(d.max_columns(), 1600);
assert!(!d.supports_insert_overwrite());
assert!(d.supports_cte());
}
#[test]
fn new_capability_defaults_are_conservative() {
let d = DefaultDialect;
assert!(d.supports_cascade_drop()); assert!(!d.supports_function_overloading());
assert!(!d.supports_sync_drop());
assert!(!d.supports_force_drop());
assert!(!d.supports_compound_assignment());
assert!(!d.supports_window_frame_exclude());
assert!(!d.supports_merge_not_matched_by_source());
assert_eq!(d.merge_not_matched_keyword(), "WHEN NOT MATCHED");
assert!(!d.supports_merge_star_syntax());
assert!(!d.uses_alter_table_update());
assert!(!d.supports_multi_table_insert());
assert_eq!(d.schema_namespace_keyword(), "SCHEMA");
}
#[test]
fn the_default_load_wrap_is_the_ansi_one() {
let d = DefaultDialect;
assert!(d.supports_multi_statement_transactions());
assert_eq!(d.begin_transaction_keyword(), "START TRANSACTION");
assert_eq!(d.commit_transaction_keyword(), "COMMIT");
assert!(!d.truncate_commits_implicitly());
assert!(!d.begin_leaves_the_commit_to_the_client());
}
#[test]
fn digits_before_the_point_have_their_own_bound() {
let bignumeric = DecimalLimits {
max_precision: 76,
max_scale: 38,
scale_within_precision: true,
max_integer_digits: Some(38),
};
assert_eq!(bignumeric.misfit(38, 0), None);
assert_eq!(bignumeric.misfit(76, 38), None);
assert_eq!(
bignumeric.misfit(39, 0).as_deref(),
Some("its decimals hold at most 38 digits before the point, and this asks for 39")
);
assert_eq!(bignumeric.nearest(39, 0), (38, 0));
assert_eq!(bignumeric.nearest(60, 10), (48, 10));
assert_eq!(bignumeric.nearest(99, 40), (76, 38));
for (precision, scale) in [(39, 0), (60, 10), (99, 40), (5, 6), (0, 0)] {
let (p, s) = bignumeric.nearest(precision, scale);
assert_eq!(bignumeric.misfit(p, s), None, "nearest({precision}, {scale}) fits");
}
let unbounded = DecimalLimits { max_integer_digits: None, ..bignumeric };
assert_eq!(unbounded.misfit(76, 0), None, "the digits in all are the only bound");
}
}