Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions src/driver/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@ use std::collections::HashMap;
// --- SQL query constants --------------------------------------------------

pub const Q_GET_TABLES: &str = "\
SELECT t.name \
SELECT \
t.name, \
TRY_CONVERT(nvarchar(max), ep.value) AS comment \
FROM sys.tables t \
JOIN sys.schemas s ON t.schema_id = s.schema_id \
LEFT JOIN sys.extended_properties ep \
ON ep.class = 1 \
AND ep.major_id = t.object_id \
AND ep.minor_id = 0 \
AND ep.name = N'MS_Description' \
WHERE s.name = @P1 \
ORDER BY t.name";

Expand Down Expand Up @@ -49,12 +56,18 @@ SELECT \
AND ic.column_id = c.column_id \
AND i.is_primary_key = 1 \
), 0) AS BIT) AS is_pk, \
dc.definition AS default_value \
dc.definition AS default_value, \
TRY_CONVERT(nvarchar(max), ep.value) AS comment \
FROM sys.columns c \
JOIN sys.types ty ON c.user_type_id = ty.user_type_id \
LEFT JOIN sys.default_constraints dc \
ON dc.parent_object_id = c.object_id \
AND dc.parent_column_id = c.column_id \
LEFT JOIN sys.extended_properties ep \
ON ep.class = 1 \
AND ep.major_id = c.object_id \
AND ep.minor_id = c.column_id \
AND ep.name = N'MS_Description' \
WHERE c.object_id = OBJECT_ID(@P1) \
ORDER BY c.column_id";

Expand Down Expand Up @@ -204,14 +217,20 @@ SELECT \
AND ic.column_id = c.column_id \
AND i.is_primary_key = 1 \
), 0) AS BIT) AS is_pk, \
dc.definition AS default_value \
dc.definition AS default_value, \
TRY_CONVERT(nvarchar(max), ep.value) AS comment \
FROM sys.columns c \
JOIN sys.tables t ON c.object_id = t.object_id \
JOIN sys.schemas s ON t.schema_id = s.schema_id \
JOIN sys.types ty ON c.user_type_id = ty.user_type_id \
LEFT JOIN sys.default_constraints dc \
ON dc.parent_object_id = c.object_id \
AND dc.parent_column_id = c.column_id \
LEFT JOIN sys.extended_properties ep \
ON ep.class = 1 \
AND ep.major_id = c.object_id \
AND ep.minor_id = c.column_id \
AND ep.name = N'MS_Description' \
WHERE s.name = @P1 \
ORDER BY t.name, c.column_id";

Expand Down Expand Up @@ -369,6 +388,7 @@ pub fn build_table_column(
max_length_bytes: i32,
is_pk: bool,
default_value: Option<String>,
comment: Option<String>,
) -> TableColumn {
let character_maximum_length = if is_string_type(&data_type) {
character_length_from_sys_columns(&data_type, max_length_bytes)
Expand All @@ -384,6 +404,7 @@ pub fn build_table_column(
is_generated,
default_value,
character_maximum_length,
comment,
}
}

Expand Down Expand Up @@ -474,8 +495,9 @@ pub async fn get_tables(
Ok(rows
.into_iter()
.filter_map(|r| {
r.get::<&str, _>(0).map(|n| TableInfo {
r.get::<&str, _>("name").map(|n| TableInfo {
name: n.to_string(),
comment: row_str_opt(&r, "comment"),
})
})
.collect())
Expand Down Expand Up @@ -505,6 +527,7 @@ pub async fn get_columns(
row_i32(&r, "max_length"),
row_bool(&r, "is_pk"),
row_str_opt(&r, "default_value"),
row_str_opt(&r, "comment"),
)
})
.collect())
Expand Down Expand Up @@ -597,6 +620,7 @@ pub async fn get_all_columns_batch(
row_i32(&r, "max_length"),
row_bool(&r, "is_pk"),
row_str_opt(&r, "default_value"),
row_str_opt(&r, "comment"),
);
out.entry(table_name).or_default().push(col);
}
Expand Down
86 changes: 84 additions & 2 deletions src/driver/introspection/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ use super::*;
// --- Query shape assertions (no live server needed) -------------------

#[test]
fn q_get_tables_queries_sys_tables_and_schemas() {
fn q_get_tables_queries_descriptions_with_object_scope() {
assert!(Q_GET_TABLES.contains("sys.tables"));
assert!(Q_GET_TABLES.contains("sys.schemas"));
assert!(Q_GET_TABLES.contains("sys.extended_properties ep"));
assert!(Q_GET_TABLES.contains("ep.class = 1"));
assert!(Q_GET_TABLES.contains("ep.major_id = t.object_id"));
assert!(Q_GET_TABLES.contains("ep.minor_id = 0"));
assert!(Q_GET_TABLES.contains("ep.name = N'MS_Description'"));
assert!(Q_GET_TABLES.contains("TRY_CONVERT(nvarchar(max), ep.value) AS comment"));
assert!(Q_GET_TABLES.contains("@P1"));
assert!(Q_GET_TABLES.contains("ORDER BY t.name"));
}
Expand All @@ -18,6 +24,12 @@ fn q_get_columns_joins_sys_types_and_reports_pk() {
assert!(Q_GET_COLUMNS.contains("sys.indexes"));
assert!(Q_GET_COLUMNS.contains("is_primary_key"));
assert!(Q_GET_COLUMNS.contains("sys.default_constraints"));
assert!(Q_GET_COLUMNS.contains("sys.extended_properties ep"));
assert!(Q_GET_COLUMNS.contains("ep.class = 1"));
assert!(Q_GET_COLUMNS.contains("ep.major_id = c.object_id"));
assert!(Q_GET_COLUMNS.contains("ep.minor_id = c.column_id"));
assert!(Q_GET_COLUMNS.contains("ep.name = N'MS_Description'"));
assert!(Q_GET_COLUMNS.contains("TRY_CONVERT(nvarchar(max), ep.value) AS comment"));
assert!(Q_GET_COLUMNS.contains("c.is_computed AS is_generated"));
assert!(Q_GET_COLUMNS.contains("OBJECT_ID(@P1)"));
assert!(Q_GET_COLUMNS.contains("ORDER BY c.column_id"));
Expand Down Expand Up @@ -150,6 +162,12 @@ fn q_get_all_columns_batch_groups_by_table() {
assert!(Q_GET_ALL_COLUMNS_BATCH.contains("sys.tables"));
assert!(Q_GET_ALL_COLUMNS_BATCH.contains("sys.schemas"));
assert!(Q_GET_ALL_COLUMNS_BATCH.contains("sys.types"));
assert!(Q_GET_ALL_COLUMNS_BATCH.contains("sys.extended_properties ep"));
assert!(Q_GET_ALL_COLUMNS_BATCH.contains("ep.class = 1"));
assert!(Q_GET_ALL_COLUMNS_BATCH.contains("ep.major_id = c.object_id"));
assert!(Q_GET_ALL_COLUMNS_BATCH.contains("ep.minor_id = c.column_id"));
assert!(Q_GET_ALL_COLUMNS_BATCH.contains("ep.name = N'MS_Description'"));
assert!(Q_GET_ALL_COLUMNS_BATCH.contains("TRY_CONVERT(nvarchar(max), ep.value) AS comment"));
assert!(Q_GET_ALL_COLUMNS_BATCH.contains("@P1"));
assert!(Q_GET_ALL_COLUMNS_BATCH.contains("ORDER BY t.name, c.column_id"));
// Must emit the table name so the caller can group rows.
Expand Down Expand Up @@ -194,6 +212,7 @@ fn build_table_column_populates_string_length() {
40,
false,
None,
None,
);
assert_eq!(col.name, "note");
assert_eq!(col.data_type, "nvarchar");
Expand All @@ -206,7 +225,17 @@ fn build_table_column_populates_string_length() {

#[test]
fn build_table_column_leaves_length_none_for_numeric() {
let col = build_table_column("id".into(), "int".into(), false, true, false, 4, true, None);
let col = build_table_column(
"id".into(),
"int".into(),
false,
true,
false,
4,
true,
None,
None,
);
assert_eq!(col.character_maximum_length, None);
assert!(col.is_pk);
assert!(col.is_auto_increment);
Expand All @@ -224,6 +253,7 @@ fn build_table_column_honours_max_as_none() {
-1,
false,
None,
None,
);
assert_eq!(col.character_maximum_length, None);
}
Expand All @@ -239,6 +269,7 @@ fn build_table_column_carries_default_value() {
8,
false,
Some("(getdate())".into()),
None,
);
assert_eq!(col.default_value, Some("(getdate())".into()));
assert_eq!(col.character_maximum_length, None);
Expand All @@ -255,11 +286,62 @@ fn build_table_column_reports_generated_and_parameterized_lengths() {
84,
false,
None,
None,
);
assert!(col.is_generated);
assert_eq!(col.character_maximum_length, Some(42));
}

#[test]
fn metadata_descriptions_serialize_verbatim_and_omit_absent_values() {
let comment = "Owner's résumé\n次の行";
let table = TableInfo {
name: "notes".into(),
comment: Some(comment.into()),
};
let table_json = serde_json::to_value(&table).expect("serialize table metadata");
assert_eq!(table_json["comment"], comment);

let plain_table = TableInfo {
name: "plain".into(),
comment: None,
};
let plain_table_json =
serde_json::to_value(&plain_table).expect("serialize table metadata without comment");
assert!(plain_table_json.get("comment").is_none());

let col = build_table_column(
"note".into(),
"nvarchar(100)".into(),
true,
false,
false,
200,
false,
None,
Some(comment.into()),
);
assert_eq!(col.comment.as_deref(), Some(comment));

let json = serde_json::to_value(&col).expect("serialize column metadata");
assert_eq!(json["comment"], comment);

let plain_col = build_table_column(
"plain".into(),
"int".into(),
true,
false,
false,
4,
false,
None,
None,
);
let plain_col_json =
serde_json::to_value(&plain_col).expect("serialize column metadata without comment");
assert!(plain_col_json.get("comment").is_none());
}

// --- build_foreign_keys ----------------------------------------------

#[test]
Expand Down
4 changes: 4 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ pub struct ConnectionParams {
#[derive(Debug, Serialize, Deserialize)]
pub struct TableInfo {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
Expand All @@ -82,6 +84,8 @@ pub struct TableColumn {
pub default_value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub character_maximum_length: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down
22 changes: 21 additions & 1 deletion tests/conformance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! The model definitions below are copied verbatim from
//! `tabularis/src-tauri/src/models.rs` at host commit
//! `ba0463d3b861ec8fad110126c67e3fc12bac9839`. Re-sync them and regenerate
//! `3f0780e19191b3d6721ba7e5d8c1224fb8f4e8fe`. Re-sync them and regenerate
//! `tests/fixtures/conformance/` with `python3 tests/capture_conformance.py`
//! whenever the host models or plugin RPC surface changes.

Expand All @@ -20,6 +20,8 @@ use serde_json::Value;
#[derive(Debug, Serialize, Deserialize)]
pub struct TableInfo {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
Expand All @@ -35,6 +37,8 @@ pub struct TableColumn {
pub default_value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub character_maximum_length: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -378,7 +382,23 @@ fn drift_prone_wire_fields_are_exercised() {
.is_some());
assert!(batch[1].error.is_some());

let tables: Vec<TableInfo> = serde_json::from_value(fixture_result("get_tables")).unwrap();
assert!(tables.iter().all(|table| table.comment.is_none()));
let mut commented_table = fixture_result("get_tables")[0].clone();
commented_table["comment"] = Value::String("Owner's résumé\n次の行".into());
let commented_table: TableInfo = serde_json::from_value(commented_table).unwrap();
assert_eq!(
commented_table.comment.as_deref(),
Some("Owner's résumé\n次の行")
);

let columns: Vec<TableColumn> = serde_json::from_value(fixture_result("get_columns")).unwrap();
assert!(columns.iter().all(|column| column.comment.is_none()));
let mut commented_column = fixture_result("get_columns")[0].clone();
commented_column["comment"] = Value::String("L'état naïve\nΔ".into());
let commented_column: TableColumn = serde_json::from_value(commented_column).unwrap();
assert_eq!(commented_column.comment.as_deref(), Some("L'état naïve\nΔ"));

let label = columns
.iter()
.find(|column| column.name == "label")
Expand Down
Loading