use crate::{
datatypes::sqldatatypes::{TdsDataType, TypeInfo, TypeInfoVariant},
token::tokens::SqlCollation,
};
use std::fmt;
#[derive(Debug, Clone)]
pub struct ColumnMetadata {
pub user_type: u32,
pub flags: u16,
pub type_info: TypeInfo,
pub data_type: TdsDataType,
pub column_name: String,
pub multi_part_name: Option<MultiPartName>,
#[allow(dead_code)]
pub(crate) crypto_metadata: Option<CryptoMetadata>,
}
impl ColumnMetadata {
pub fn is_nullable(&self) -> bool {
(self.flags & 0x01) != 0x00
}
pub fn is_case_sensitive(&self) -> bool {
(self.flags & 0x02) != 0x00
}
pub fn is_identity(&self) -> bool {
(self.flags & 0x10) != 0x00
}
pub fn is_computed(&self) -> bool {
(self.flags & 0x20) != 0x00
}
pub fn is_sparse_column_set(&self) -> bool {
(self.flags & 0x0400) != 0x00
}
pub fn is_encrypted(&self) -> bool {
(self.flags & 0x0800) != 0x00
}
pub fn is_hidden(&self) -> bool {
(self.flags & 0x2000) != 0x00
}
pub fn is_key_column(&self) -> bool {
(self.flags & 0x4000) != 0x00
}
pub fn is_plp(&self) -> bool {
matches!(
self.type_info.type_info_variant,
TypeInfoVariant::PartialLen(_, _, _, _, _)
)
}
pub fn plp_encoding(&self) -> Option<PlpEncoding> {
if !self.is_plp() {
return None;
}
Some(match self.data_type {
TdsDataType::NVarChar | TdsDataType::NChar | TdsDataType::Xml => PlpEncoding::Utf16Text,
TdsDataType::Json => PlpEncoding::Utf8Text,
TdsDataType::BigVarChar
| TdsDataType::BigChar
| TdsDataType::VarChar
| TdsDataType::Char
| TdsDataType::Text => PlpEncoding::SingleByteText,
TdsDataType::BigVarBinary
| TdsDataType::BigBinary
| TdsDataType::VarBinary
| TdsDataType::Binary
| TdsDataType::Image
| TdsDataType::Udt
| TdsDataType::Vector => PlpEncoding::Binary,
_ => PlpEncoding::Binary,
})
}
pub fn get_scale(&self) -> Option<u8> {
match self.type_info.type_info_variant {
TypeInfoVariant::VarLenScale(_, scale) => Some(scale),
TypeInfoVariant::VarLenPrecisionScale(_, _, _, scale) => Some(scale),
_ => None,
}
}
pub fn get_precision(&self) -> Option<u8> {
use crate::datatypes::sqldatatypes::{FixedLengthTypes, VariableLengthTypes};
match self.type_info.type_info_variant {
TypeInfoVariant::VarLenPrecisionScale(_, _, precision, _) => Some(precision),
TypeInfoVariant::FixedLen(FixedLengthTypes::Money) => Some(19),
TypeInfoVariant::FixedLen(FixedLengthTypes::Money4) => Some(10),
TypeInfoVariant::VarLen(VariableLengthTypes::MoneyN, length) => match length {
8 => Some(19),
4 => Some(10),
_ => None,
},
_ => None,
}
}
pub fn get_collation(&self) -> Option<SqlCollation> {
match self.type_info.type_info_variant {
TypeInfoVariant::VarLenString(_, _, collation) => collation,
TypeInfoVariant::PartialLen(_, _, collation, _, _) => collation,
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlpEncoding {
Utf16Text,
Utf8Text,
SingleByteText,
Binary,
}
impl fmt::Display for ColumnMetadata {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Column Name: {}\nData Type: {:?} (UserType: {})\nFlags: [Nullable: {}, CaseSensitive: {}, Identity: {}, Computed: {}, \
SparseColumnSet: {}, Encrypted: {}, MultiPartName: {:?}]\n",
self.column_name,
self.data_type,
self.user_type,
self.is_nullable(),
self.is_case_sensitive(),
self.is_identity(),
self.is_computed(),
self.is_sparse_column_set(),
self.is_encrypted(),
self.multi_part_name
)
}
}
#[derive(Debug, Default, Clone)]
pub struct MultiPartName {
pub(crate) server_name: Option<String>,
pub(crate) catalog_name: Option<String>,
pub(crate) schema_name: Option<String>,
pub(crate) table_name: String,
}
impl MultiPartName {
pub fn server_name(&self) -> Option<&str> {
self.server_name.as_deref()
}
pub fn catalog_name(&self) -> Option<&str> {
self.catalog_name.as_deref()
}
pub fn schema_name(&self) -> Option<&str> {
self.schema_name.as_deref()
}
pub fn table_name(&self) -> &str {
&self.table_name
}
}
#[derive(Clone)]
pub(crate) struct EncryptedCekValue {
pub encrypted_key: Vec<u8>,
pub key_store_name: String,
pub key_path: String,
pub algorithm_name: String,
}
impl fmt::Debug for EncryptedCekValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EncryptedCekValue")
.field("encrypted_key_len", &self.encrypted_key.len())
.field("algorithm_name", &self.algorithm_name)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
pub(crate) struct CekTableEntry {
pub database_id: i32,
pub cek_id: i32,
pub cek_version: i32,
pub cek_md_version: [u8; 8],
pub encrypted_cek_values: Vec<EncryptedCekValue>,
}
#[derive(Debug, Clone)]
pub(crate) struct CryptoMetadata {
pub cek_table_ordinal: u16,
pub base_data_type: TdsDataType,
pub base_type_info: TypeInfo,
pub cipher_algorithm_id: u8,
pub cipher_algorithm_name: Option<String>,
pub encryption_type: u8,
pub normalization_rule_version: u8,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::datatypes::sqldatatypes::{
FixedLengthTypes, PartialLengthType, TdsDataType, TypeInfo, TypeInfoVariant,
VariableLengthTypes,
};
use crate::token::tokens::SqlCollation;
fn create_test_column_metadata(
flags: u16,
type_info_variant: TypeInfoVariant,
) -> ColumnMetadata {
ColumnMetadata {
user_type: 0,
flags,
type_info: TypeInfo {
tds_type: TdsDataType::IntN,
length: 4,
type_info_variant,
},
data_type: TdsDataType::IntN,
column_name: "test_column".to_string(),
multi_part_name: None,
crypto_metadata: None,
}
}
#[test]
fn test_is_nullable() {
let metadata =
create_test_column_metadata(0x01, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(metadata.is_nullable());
let metadata =
create_test_column_metadata(0x00, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(!metadata.is_nullable());
}
#[test]
fn test_is_case_sensitive() {
let metadata =
create_test_column_metadata(0x02, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(metadata.is_case_sensitive());
let metadata =
create_test_column_metadata(0x00, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(!metadata.is_case_sensitive());
}
#[test]
fn test_is_identity() {
let metadata =
create_test_column_metadata(0x10, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(metadata.is_identity());
let metadata =
create_test_column_metadata(0x00, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(!metadata.is_identity());
}
#[test]
fn test_is_computed() {
let metadata =
create_test_column_metadata(0x20, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(metadata.is_computed());
let metadata =
create_test_column_metadata(0x00, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(!metadata.is_computed());
}
#[test]
fn test_is_sparse_column_set() {
let metadata =
create_test_column_metadata(0x0400, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(metadata.is_sparse_column_set());
let metadata =
create_test_column_metadata(0x1000, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(!metadata.is_sparse_column_set());
let metadata =
create_test_column_metadata(0x00, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(!metadata.is_sparse_column_set());
}
fn plp_column(data_type: TdsDataType) -> ColumnMetadata {
let mut column = create_test_column_metadata(
0x00,
TypeInfoVariant::PartialLen(PartialLengthType::BigVarChar, None, None, None, None),
);
column.data_type = data_type;
column
}
#[test]
fn test_plp_encoding_classifies_full_type_set() {
for dt in [TdsDataType::NVarChar, TdsDataType::NChar, TdsDataType::Xml] {
assert_eq!(
plp_column(dt).plp_encoding(),
Some(PlpEncoding::Utf16Text),
"{dt:?} should be Utf16Text"
);
}
assert_eq!(
plp_column(TdsDataType::Json).plp_encoding(),
Some(PlpEncoding::Utf8Text),
"json should be Utf8Text, not SingleByteText"
);
assert_eq!(
plp_column(TdsDataType::BigVarChar).plp_encoding(),
Some(PlpEncoding::SingleByteText)
);
for dt in [
TdsDataType::BigVarBinary,
TdsDataType::Image,
TdsDataType::Udt,
] {
assert_eq!(
plp_column(dt).plp_encoding(),
Some(PlpEncoding::Binary),
"{dt:?} should be Binary"
);
}
let non_plp =
create_test_column_metadata(0x00, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert_eq!(non_plp.plp_encoding(), None);
assert_eq!(
plp_column(TdsDataType::IntN).plp_encoding(),
Some(PlpEncoding::Binary),
"an unclassified PLP-flagged type should default to Binary"
);
}
#[test]
fn test_is_encrypted() {
let metadata =
create_test_column_metadata(0x0800, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(metadata.is_encrypted());
let metadata =
create_test_column_metadata(0x2000, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(!metadata.is_encrypted());
let metadata =
create_test_column_metadata(0x00, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(!metadata.is_encrypted());
}
#[test]
fn test_is_hidden() {
let metadata =
create_test_column_metadata(0x2000, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(metadata.is_hidden());
let metadata =
create_test_column_metadata(0x00, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(!metadata.is_hidden());
}
#[test]
fn test_is_key_column() {
let metadata =
create_test_column_metadata(0x4000, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(metadata.is_key_column());
let metadata =
create_test_column_metadata(0x00, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(!metadata.is_key_column());
}
#[test]
fn test_is_plp() {
let metadata = create_test_column_metadata(
0x00,
TypeInfoVariant::PartialLen(PartialLengthType::BigVarChar, None, None, None, None),
);
assert!(metadata.is_plp());
let metadata =
create_test_column_metadata(0x00, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(!metadata.is_plp());
}
#[test]
fn test_get_scale_varlen_scale() {
let metadata = create_test_column_metadata(
0x00,
TypeInfoVariant::VarLenScale(VariableLengthTypes::TimeN, 7),
);
assert_eq!(metadata.get_scale(), Some(7));
}
#[test]
fn test_get_scale_varlen_precision_scale() {
let metadata = create_test_column_metadata(
0x00,
TypeInfoVariant::VarLenPrecisionScale(VariableLengthTypes::DecimalN, 18, 38, 4),
);
assert_eq!(metadata.get_scale(), Some(4));
}
#[test]
fn test_get_scale_none() {
let metadata =
create_test_column_metadata(0x00, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert_eq!(metadata.get_scale(), None);
}
#[test]
fn test_get_precision_decimal_numeric() {
let dec = create_test_column_metadata(
0x00,
TypeInfoVariant::VarLenPrecisionScale(VariableLengthTypes::DecimalN, 17, 38, 4),
);
assert_eq!(dec.get_precision(), Some(38));
let num = create_test_column_metadata(
0x00,
TypeInfoVariant::VarLenPrecisionScale(VariableLengthTypes::NumericN, 9, 18, 0),
);
assert_eq!(num.get_precision(), Some(18));
}
#[test]
fn test_get_precision_money_types() {
let cases = [
(TypeInfoVariant::FixedLen(FixedLengthTypes::Money), Some(19)),
(
TypeInfoVariant::FixedLen(FixedLengthTypes::Money4),
Some(10),
),
(
TypeInfoVariant::VarLen(VariableLengthTypes::MoneyN, 8),
Some(19),
),
(
TypeInfoVariant::VarLen(VariableLengthTypes::MoneyN, 4),
Some(10),
),
(
TypeInfoVariant::VarLen(VariableLengthTypes::MoneyN, 6),
None,
),
];
for (variant, expected) in cases {
let meta = create_test_column_metadata(0x00, variant);
assert_eq!(meta.get_precision(), expected);
}
}
#[test]
fn test_get_precision_none_for_non_numeric() {
let cases = [
TypeInfoVariant::FixedLen(FixedLengthTypes::Int4),
TypeInfoVariant::VarLen(VariableLengthTypes::IntN, 4),
TypeInfoVariant::VarLenScale(VariableLengthTypes::TimeN, 7),
TypeInfoVariant::PartialLen(PartialLengthType::BigVarChar, None, None, None, None),
];
for variant in cases {
let meta = create_test_column_metadata(0x00, variant);
assert_eq!(meta.get_precision(), None);
}
}
#[test]
fn test_get_precision_varlen_precision_scale() {
let metadata = create_test_column_metadata(
0x00,
TypeInfoVariant::VarLenPrecisionScale(VariableLengthTypes::DecimalN, 18, 38, 4),
);
assert_eq!(metadata.get_precision(), Some(38));
}
#[test]
fn test_get_precision_varlen_scale_only_is_none() {
let metadata = create_test_column_metadata(
0x00,
TypeInfoVariant::VarLenScale(VariableLengthTypes::TimeN, 7),
);
assert_eq!(metadata.get_precision(), None);
}
#[test]
fn test_get_precision_none() {
let metadata =
create_test_column_metadata(0x00, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert_eq!(metadata.get_precision(), None);
}
#[test]
fn test_get_collation_varlen_string() {
let collation = SqlCollation {
info: 0,
lcid_language_id: 1033,
col_flags: 0,
sort_id: 0,
};
let metadata = create_test_column_metadata(
0x00,
TypeInfoVariant::VarLenString(VariableLengthTypes::BigVarChar, 100, Some(collation)),
);
assert!(metadata.get_collation().is_some());
}
#[test]
fn test_get_collation_partial_len() {
let collation = SqlCollation {
info: 0,
lcid_language_id: 1033,
col_flags: 0,
sort_id: 0,
};
let metadata = create_test_column_metadata(
0x00,
TypeInfoVariant::PartialLen(
PartialLengthType::BigVarChar,
None,
Some(collation),
None,
None,
),
);
assert!(metadata.get_collation().is_some());
}
#[test]
fn test_get_collation_none() {
let metadata =
create_test_column_metadata(0x00, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
assert!(metadata.get_collation().is_none());
}
#[test]
fn test_display_format() {
let metadata =
create_test_column_metadata(0x01, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
let display = format!("{metadata}");
assert!(display.contains("test_column"));
assert!(display.contains("Nullable: true"));
}
#[test]
fn test_multi_part_name_default() {
let multi_part = MultiPartName::default();
assert_eq!(multi_part.table_name, "");
assert!(multi_part.server_name.is_none());
assert!(multi_part.catalog_name.is_none());
assert!(multi_part.schema_name.is_none());
}
#[test]
fn test_multi_part_name_accessors() {
let multi_part = MultiPartName {
server_name: Some("server".to_string()),
catalog_name: Some("catalog".to_string()),
schema_name: Some("dbo".to_string()),
table_name: "users".to_string(),
};
assert_eq!(multi_part.server_name(), Some("server"));
assert_eq!(multi_part.catalog_name(), Some("catalog"));
assert_eq!(multi_part.schema_name(), Some("dbo"));
assert_eq!(multi_part.table_name(), "users");
}
#[test]
fn test_multi_part_name_accessors_default() {
let multi_part = MultiPartName::default();
assert_eq!(multi_part.server_name(), None);
assert_eq!(multi_part.catalog_name(), None);
assert_eq!(multi_part.schema_name(), None);
assert_eq!(multi_part.table_name(), "");
}
#[test]
fn test_multi_part_name_clone() {
let multi_part = MultiPartName {
server_name: Some("server".to_string()),
catalog_name: Some("catalog".to_string()),
schema_name: Some("dbo".to_string()),
table_name: "users".to_string(),
};
let cloned = multi_part.clone();
assert_eq!(cloned.server_name, Some("server".to_string()));
assert_eq!(cloned.table_name, "users");
}
#[test]
fn test_column_metadata_clone() {
let metadata =
create_test_column_metadata(0x01, TypeInfoVariant::FixedLen(FixedLengthTypes::Int4));
let cloned = metadata.clone();
assert_eq!(cloned.column_name, "test_column");
assert_eq!(cloned.flags, 0x01);
}
#[test]
fn encrypted_cek_value_debug_redacts_key_material() {
let rendered = format!(
"{:?}",
EncryptedCekValue {
encrypted_key: vec![0x2A; 4],
key_store_name: "AZURE_KEY_VAULT".to_string(),
key_path: "https://vault.example/keys/cmk".to_string(),
algorithm_name: "RSA_OAEP".to_string(),
}
);
assert!(!rendered.contains("42"));
assert!(!rendered.contains("vault.example"));
assert!(!rendered.contains("AZURE_KEY_VAULT"));
assert!(rendered.contains("encrypted_key_len: 4"));
assert!(rendered.contains("RSA_OAEP"));
}
}