Skip to content

Commit 20fe3f7

Browse files
committed
feat: add support for system-versioned tables in database explorer
1 parent aeece6a commit 20fe3f7

5 files changed

Lines changed: 106 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ All notable changes to the MS SQL Manager extension will be documented in this f
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.19.2] - 2026-03-26
9+
10+
### Added
11+
12+
- **Database Explorer — System-versioned (temporal) table support**
13+
- System-versioned tables now display a distinct sparkle icon (✦) in the database explorer to visually differentiate them from regular tables.
14+
- The table description now includes a `(system-versioned)` label after the row count and size (e.g. `51 Rows < 1 MB (system-versioned)`).
15+
- History tables (`temporal_type = 1`) are hidden from the main Tables list — they only appear as a child node when expanding their associated system-versioned table.
16+
- Expanding a system-versioned table shows a **History** child node (clock icon) pointing to the linked history table with full exploration support: Columns, Indexes, Statistics.
17+
- Scripting commands (Select Top 1000, Script Table Create, etc.) work on both system-versioned tables and their history child nodes without SQL name escaping issues.
18+
- Compatible with SQL Server 2008–2014 via automatic fallback query when `temporal_type` column is absent.
19+
820
## [0.19.1] - 2026-03-23
921

1022
### Fixed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "ms-sql-manager",
33
"displayName": "MS SQL Manager",
44
"description": "Manage Microsoft SQL Server databases with connection management, schema browsing, and query execution.",
5-
"version": "0.19.1",
5+
"version": "0.19.2",
66
"publisher": "jakubkozera",
77
"icon": "ms-sql-manager.png",
88
"sponsor": {

src/serverGroupIcon.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,24 @@ export function createStoredProcedureIcon(): { light: vscode.Uri; dark: vscode.U
100100
return createThemedSvgIcon(lightSvg, darkSvg);
101101
}
102102

103+
export function createTemporalTableIcon(): { light: vscode.Uri; dark: vscode.Uri } {
104+
const lightSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
105+
<path d="M19 22.5a4.75 4.75 0 0 1 3.5 -3.5a4.75 4.75 0 0 1 -3.5 -3.5a4.75 4.75 0 0 1 -3.5 3.5a4.75 4.75 0 0 1 3.5 3.5" />
106+
<path d="M12 21h-7a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v7" />
107+
<path d="M3 10h18" />
108+
<path d="M10 3v18" />
109+
</svg>`;
110+
111+
const darkSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#CCCCCC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
112+
<path d="M19 22.5a4.75 4.75 0 0 1 3.5 -3.5a4.75 4.75 0 0 1 -3.5 -3.5a4.75 4.75 0 0 1 -3.5 3.5a4.75 4.75 0 0 1 3.5 3.5" />
113+
<path d="M12 21h-7a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v7" />
114+
<path d="M3 10h18" />
115+
<path d="M10 3v18" />
116+
</svg>`;
117+
118+
return createThemedSvgIcon(lightSvg, darkSvg);
119+
}
120+
103121
export function createViewIcon(): { light: vscode.Uri; dark: vscode.Uri } {
104122
const lightSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
105123
<path d="M17.73 19.875a2.225 2.225 0 0 1 -1.948 1.125h-7.283a2.222 2.222 0 0 1 -1.947 -1.158l-4.272 -6.75a2.269 2.269 0 0 1 0 -2.184l4.272 -6.75a2.225 2.225 0 0 1 1.946 -1.158h7.285c.809 0 1.554 .443 1.947 1.158l3.98 6.75a2.33 2.33 0 0 1 0 2.25l-3.98 6.75v-.033z" />

src/unifiedTreeProvider.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as vscode from 'vscode';
22
import * as sql from 'mssql';
33
import { ConnectionProvider, ConnectionConfig, ServerGroup } from './connectionProvider';
4-
import { createServerGroupIcon, createTableIcon, createColumnIcon, createStoredProcedureIcon, createViewIcon, createLoadingSpinnerIcon, createDatabaseIcon, createFunctionIcon, createTriggerIcon, createTypeIcon, createSequenceIcon, createSynonymIcon, createAssemblyIcon } from './serverGroupIcon';
4+
import { createServerGroupIcon, createTableIcon, createColumnIcon, createStoredProcedureIcon, createViewIcon, createLoadingSpinnerIcon, createDatabaseIcon, createFunctionIcon, createTriggerIcon, createTypeIcon, createSequenceIcon, createSynonymIcon, createAssemblyIcon, createTemporalTableIcon } from './serverGroupIcon';
55
import { SchemaCache } from './utils/schemaCache';
66

77
export class UnifiedTreeProvider implements vscode.TreeDataProvider<TreeNode>, vscode.FileDecorationProvider, vscode.TreeDragAndDropController<TreeNode> {
@@ -751,10 +751,13 @@ export class UnifiedTreeProvider implements vscode.TreeDataProvider<TreeNode>, v
751751
this.outputChannel.appendLine(`[UnifiedTreeProvider] Found ${cachedTables.length} tables to display in database ${element.database || 'current'}`);
752752
}
753753

754-
return tablesToDisplay.map((table) => {
754+
return tablesToDisplay
755+
.filter(table => table.temporalType !== 1)
756+
.map((table) => {
757+
const itemType = table.temporalType === 2 ? 'temporal-table' : 'table';
755758
const tableNode = new SchemaItemNode(
756759
`${table.schema}.${table.name}`,
757-
'table',
760+
itemType,
758761
table.schema,
759762
vscode.TreeItemCollapsibleState.Collapsed
760763
);
@@ -784,8 +787,9 @@ export class UnifiedTreeProvider implements vscode.TreeDataProvider<TreeNode>, v
784787
formattedSize = '< 1 MB';
785788
}
786789

787-
// Set description: "1.6k Rows 34 MB"
788-
tableNode.description = `${formattedRows} Rows ${formattedSize}`;
790+
// Set description: "1.6k Rows 34 MB" (+ system-versioned label for temporal tables)
791+
const temporalSuffix = table.temporalType === 2 ? ' (system-versioned)' : '';
792+
tableNode.description = `${formattedRows} Rows ${formattedSize}${temporalSuffix}`;
789793
}
790794

791795
return tableNode;
@@ -1132,7 +1136,7 @@ export class UnifiedTreeProvider implements vscode.TreeDataProvider<TreeNode>, v
11321136
}
11331137

11341138
return items;
1135-
} else if (element.itemType === 'table') {
1139+
} else if (element.itemType === 'table' || element.itemType === 'temporal-table') {
11361140
// Show table details (columns, keys, etc.)
11371141
// Extract table name from label format schema.tableName
11381142
const fullLabel = element.label as string;
@@ -1604,6 +1608,27 @@ export class UnifiedTreeProvider implements vscode.TreeDataProvider<TreeNode>, v
16041608
statisticsNode.database = database;
16051609
items.push(statisticsNode);
16061610

1611+
// History table node for system-versioned (temporal) tables
1612+
const allTables = await this.schemaCache.getTables(cacheConnection, connection);
1613+
const currentTable = allTables.find(t =>
1614+
t.name.toLowerCase() === tableName.toLowerCase() &&
1615+
t.schema.toLowerCase() === schema.toLowerCase()
1616+
);
1617+
if (currentTable?.temporalType === 2 && currentTable.historyTableName && currentTable.historyTableSchema) {
1618+
const historyNode = new SchemaItemNode(
1619+
`${currentTable.historyTableSchema}.${currentTable.historyTableName}`,
1620+
'table',
1621+
currentTable.historyTableSchema,
1622+
vscode.TreeItemCollapsibleState.Collapsed
1623+
);
1624+
(historyNode as any).tableName = currentTable.historyTableName;
1625+
historyNode.connectionId = connectionId;
1626+
historyNode.database = database;
1627+
historyNode.iconPath = new vscode.ThemeIcon('history');
1628+
historyNode.description = '(History)';
1629+
items.push(historyNode);
1630+
}
1631+
16071632
this.outputChannel.appendLine(`[UnifiedTreeProvider] Returning ${items.length} items from cache for table ${tableName}`);
16081633
return items;
16091634
}
@@ -2695,6 +2720,10 @@ export class SchemaItemNode extends TreeNode {
26952720
this.iconPath = createTableIcon();
26962721
this.contextValue = 'table';
26972722
break;
2723+
case 'temporal-table':
2724+
this.iconPath = createTemporalTableIcon();
2725+
this.contextValue = 'table';
2726+
break;
26982727
case 'view':
26992728
this.iconPath = createViewIcon();
27002729
this.contextValue = 'view';

src/utils/schemaCache.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export interface TableInfo {
4646
sizeMB?: number;
4747
type: 'table';
4848
lastModified?: Date;
49+
/** 0 = non-temporal, 1 = history table, 2 = system-versioned temporal table (SQL Server 2016+) */
50+
temporalType?: number;
51+
historyTableSchema?: string;
52+
historyTableName?: string;
4953
}
5054

5155
/**
@@ -396,7 +400,31 @@ export class SchemaCache {
396400
* Fetch all tables from database
397401
*/
398402
private async fetchTables(pool: DBPool): Promise<Map<string, TableInfo>> {
399-
const query = `
403+
// Query with temporal table columns (SQL Server 2016+).
404+
// Falls back to a simpler query if temporal_type column doesn't exist.
405+
const temporalQuery = `
406+
SELECT
407+
t.TABLE_SCHEMA as [schema],
408+
t.TABLE_NAME as name,
409+
USER_NAME(st.principal_id) AS owner,
410+
ISNULL(SUM(p.rows), 0) as [rowCount],
411+
SUM(a.total_pages) * 8 / 1024.0 AS [sizeMB],
412+
st.modify_date as lastModified,
413+
st.temporal_type as temporalType,
414+
OBJECT_SCHEMA_NAME(st.history_table_id) as historySchema,
415+
OBJECT_NAME(st.history_table_id) as historyTable
416+
FROM INFORMATION_SCHEMA.TABLES t
417+
INNER JOIN sys.tables st ON t.TABLE_NAME = st.name AND t.TABLE_SCHEMA = SCHEMA_NAME(st.schema_id)
418+
LEFT JOIN sys.indexes i ON st.object_id = i.object_id AND i.index_id <= 1
419+
LEFT JOIN sys.partitions p ON st.object_id = p.object_id AND i.index_id = p.index_id
420+
LEFT JOIN sys.allocation_units a ON p.partition_id = a.container_id
421+
WHERE t.TABLE_TYPE = 'BASE TABLE'
422+
GROUP BY t.TABLE_SCHEMA, t.TABLE_NAME, st.principal_id, st.modify_date,
423+
st.temporal_type, st.history_table_id
424+
ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME
425+
`;
426+
427+
const fallbackQuery = `
400428
SELECT
401429
t.TABLE_SCHEMA as [schema],
402430
t.TABLE_NAME as name,
@@ -414,7 +442,13 @@ export class SchemaCache {
414442
ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME
415443
`;
416444

417-
const result = await pool.request().query(query);
445+
let result;
446+
try {
447+
result = await pool.request().query(temporalQuery);
448+
} catch {
449+
// SQL Server < 2016 — temporal_type column doesn't exist
450+
result = await pool.request().query(fallbackQuery);
451+
}
418452
const tables = new Map<string, TableInfo>();
419453

420454
for (const row of result.recordset) {
@@ -426,7 +460,10 @@ export class SchemaCache {
426460
rowCount: row.rowCount,
427461
sizeMB: row.sizeMB,
428462
type: 'table',
429-
lastModified: row.lastModified
463+
lastModified: row.lastModified,
464+
temporalType: row.temporalType || 0,
465+
historyTableSchema: row.historySchema || undefined,
466+
historyTableName: row.historyTable || undefined
430467
});
431468
}
432469

0 commit comments

Comments
 (0)