[Phase 2] Foreign key composite aggregation (STRING_AGG + FOR XML PATH fallback)
Part of the SQL Server Phase 2 epic #150.
The driver isn't on main. Code lives on feat/sql-server and isn't in any release. Check that branch out, branch off it, target your PR at it — not at main. The whole thing squashes into main when Phase 2 closes (#149).
Phase 1 emits one ForeignKey row per FK column (same shape MySQL/Postgres drivers use). For composite FKs, the frontend SchemaDiagram.tsx needs to know which columns belong together — that's Phase 2's aggregated form.
Task
1. Evolve the model. In src-tauri/src/models.rs::ForeignKey, add two Vec<String> fields (backward-compatible: empty vec when missing from JSON):
#[derive(Debug, Serialize, Deserialize)]
pub struct ForeignKey {
pub name: String,
// Legacy single-column (keep populated — MySQL/Postgres/SQLite still emit these)
pub column_name: String,
pub ref_table: String,
pub ref_column: String,
pub on_delete: Option<String>,
pub on_update: Option<String>,
// Phase 2: composite support (populated only by drivers that aggregate)
#[serde(default)] pub columns: Vec<String>,
#[serde(default)] pub ref_columns: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ref_schema: Option<String>,
}
Add helper methods local_columns() / referenced_columns() that fall back to the legacy single-column form when columns is empty. Pure and trivially testable.
2. Rewrite Q_GET_FOREIGN_KEYS. In src-tauri/src/drivers/sqlserver/introspection.rs, replace the current INFORMATION_SCHEMA-only query with:
- SQL Server 2017+: use
sys.foreign_keys + sys.foreign_key_columns with STRING_AGG(col, ',') WITHIN GROUP (ORDER BY constraint_column_id) to aggregate columns per constraint in a single row
- SQL Server 2012–2016: fallback query using
FOR XML PATH('') (subquery with STUFF(...) pattern)
Gate the choice on drivers/sqlserver/version::ServerVersion::supports_string_agg.
3. Populate both shapes. Populate the legacy fields (first column of the constraint) and columns[] / ref_columns[]. This keeps any existing code path that reads column_name working.
4. Update the batch variant (Q_GET_ALL_FOREIGN_KEYS_BATCH) identically.
Rules
- Co-located unit tests: query shape asserts for both the
STRING_AGG and FOR XML PATH branches
- Regression test:
serde_json::from_str::<ForeignKey> on old JSON (no columns field) still deserializes
- Unit test for
local_columns() / referenced_columns() fallback
Reference
[Phase 2] Foreign key composite aggregation (STRING_AGG + FOR XML PATH fallback)
Part of the SQL Server Phase 2 epic #150.
The driver isn't on
main. Code lives onfeat/sql-serverand isn't in any release. Check that branch out, branch off it, target your PR at it — not atmain. The whole thing squashes intomainwhen Phase 2 closes (#149).Phase 1 emits one
ForeignKeyrow per FK column (same shape MySQL/Postgres drivers use). For composite FKs, the frontendSchemaDiagram.tsxneeds to know which columns belong together — that's Phase 2's aggregated form.Task
1. Evolve the model. In
src-tauri/src/models.rs::ForeignKey, add twoVec<String>fields (backward-compatible: empty vec when missing from JSON):Add helper methods
local_columns()/referenced_columns()that fall back to the legacy single-column form whencolumnsis empty. Pure and trivially testable.2. Rewrite
Q_GET_FOREIGN_KEYS. Insrc-tauri/src/drivers/sqlserver/introspection.rs, replace the currentINFORMATION_SCHEMA-only query with:sys.foreign_keys+sys.foreign_key_columnswithSTRING_AGG(col, ',') WITHIN GROUP (ORDER BY constraint_column_id)to aggregate columns per constraint in a single rowFOR XML PATH('')(subquery withSTUFF(...)pattern)Gate the choice on
drivers/sqlserver/version::ServerVersion::supports_string_agg.3. Populate both shapes. Populate the legacy fields (first column of the constraint) and
columns[]/ref_columns[]. This keeps any existing code path that readscolumn_nameworking.4. Update the batch variant (
Q_GET_ALL_FOREIGN_KEYS_BATCH) identically.Rules
STRING_AGGandFOR XML PATHbranchesserde_json::from_str::<ForeignKey>on old JSON (nocolumnsfield) still deserializeslocal_columns()/referenced_columns()fallbackReference
docs/sql-server-implementation-plan.md§ Phase 2 — 2.4 (SQL query body is in the plan)sys.foreign_keysdocs: https://learn.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-foreign-keys-transact-sql