Skip to content

Commit 1b73b48

Browse files
author
MPCoreDeveloper
committed
Implement admin-console phase 02: pg_catalog and information_schema expansion
1 parent 03fb96b commit 1b73b48

File tree

9 files changed

+1433
-7
lines changed

9 files changed

+1433
-7
lines changed

.github/issue-drafts/admin-console-v1.6.0/00-epic-admin-console-and-tool-compatibility.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ Deliver an administration and observability roadmap for `SharpCoreDB` that prior
55
**State:** IN PROGRESS
66

77
Resolved workstreams in repository:
8-
- [x] `01-tool-compatibility-matrix-and-certification.md`
8+
- [x] `01-tool-compatibility-matrix-and-certification.md`**RESOLVED**
9+
- [x] `02-pg-catalog-and-information-schema-expansion.md`**RESOLVED**
910

1011
Next planned work:
11-
- [ ] `02-pg-catalog-and-information-schema-expansion.md`
12+
- [ ] `03` and beyond — see remaining issue drafts.
1213

1314
This roadmap is based on current `v1.6.0` behavior:
1415
- `tools/SharpCoreDB.Viewer` exists but remains minimal.

.github/issue-drafts/admin-console-v1.6.0/02-pg-catalog-and-information-schema-expansion.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
## Summary
22
Expand PostgreSQL metadata compatibility (`pg_catalog`, `information_schema`) to improve GUI introspection.
33

4+
**State: RESOLVED** — Implemented in commit on master. See `docs/server/PG_CATALOG_COVERAGE_v1.6.0.md`.
5+
46
## Why
57
GUI tools rely on catalog views to discover tables, indexes, constraints, triggers, and relationships.
68

@@ -16,9 +18,15 @@ GUI tools rely on catalog views to discover tables, indexes, constraints, trigge
1618
4. Validate with compatibility matrix scripts.
1719

1820
## Acceptance Criteria
19-
- External tools can introspect core schema objects without major fallback failures.
20-
- Metadata query test suite added.
21-
- Documentation updated with supported catalog coverage.
21+
- [x] External tools can introspect core schema objects without major fallback failures.
22+
- [x] Metadata query test suite added — 15 tests, all passing.
23+
- [x] Documentation updated with supported catalog coverage (`PG_CATALOG_COVERAGE_v1.6.0.md`).
24+
25+
## Implementation Notes
26+
- `PgCatalogService` in `src/SharpCoreDB.Server.Core/Catalog/` intercepts catalog queries before the engine.
27+
- Supported: `information_schema.tables`, `information_schema.columns`, `information_schema.schemata`, `pg_tables`, `pg_class`, `pg_attribute`, `pg_namespace`, `pg_type`, `pg_roles`, `pg_am`, scalar functions.
28+
- Empty results: `pg_index`, `pg_constraint`, `pg_trigger`, `pg_proc` (no engine-level catalog yet).
29+
- Intercepted in both simple query (`Q`) and extended query (`P`/`B`/`E`) protocol paths.
2230

2331
## Dependencies
24-
- Depends on compatibility matrix findings.
32+
- Depends on compatibility matrix findings (Phase 01 — RESOLVED).
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# pg_catalog and information_schema Coverage — V 1.60
2+
3+
**SharpCoreDB Server** intercepts PostgreSQL catalog queries and returns live schema metadata, enabling GUI tools to introspect tables, columns, types, and schemas without requiring native catalog storage.
4+
5+
---
6+
7+
## Architecture
8+
9+
The `PgCatalogService` (`src/SharpCoreDB.Server.Core/Catalog/PgCatalogService.cs`) intercepts catalog queries **before** they reach the database engine:
10+
11+
```
12+
Client SQL → BinaryProtocolHandler
13+
14+
├── PgCatalogService.TryHandleCatalogQuery()
15+
│ ↓ matched → synthetic result rows → client
16+
│ ↓ not matched → engine.ExecuteQuery()
17+
└── SharpCoreDB Engine
18+
```
19+
20+
Both the simple query path (`Q` message) and the extended query path (`P`/`B`/`E` messages) are intercepted.
21+
22+
---
23+
24+
## Supported Scalar Functions
25+
26+
| Expression | Returns |
27+
|---|---|
28+
| `current_database()` | Active database name |
29+
| `version()` | `SharpCoreDB 1.6.0 on .NET 10 (PostgreSQL protocol compatible)` |
30+
| `current_user` | Authenticated user name |
31+
| `session_user` | Authenticated user name |
32+
| `current_schema()` | `public` |
33+
| `pg_postmaster_start_time()` | Server start time (UTC) |
34+
35+
---
36+
37+
## Supported Views
38+
39+
### information_schema
40+
41+
| View | Key Columns | Notes |
42+
|---|---|---|
43+
| `information_schema.tables` | `table_catalog`, `table_schema`, `table_name`, `table_type` | Returns `BASE TABLE` for all user tables in `public` schema. WHERE `table_schema` filter supported. |
44+
| `information_schema.columns` | `table_name`, `column_name`, `ordinal_position`, `data_type`, `is_nullable`, `collation_name` | WHERE `table_name` and `table_schema` filters supported. |
45+
| `information_schema.schemata` | `catalog_name`, `schema_name`, `schema_owner` | Returns `public`, `pg_catalog`, `information_schema`. |
46+
| `information_schema.table_constraints` | All constraint columns | Returns empty set (no constraints stored yet). |
47+
| `information_schema.key_column_usage` | All columns | Returns empty set. |
48+
| `information_schema.referential_constraints` | All columns | Returns empty set. |
49+
| `information_schema.constraint_column_usage` | All columns | Returns empty set. |
50+
51+
### pg_catalog
52+
53+
| View | Key Columns | Notes |
54+
|---|---|---|
55+
| `pg_tables` / `pg_catalog.pg_tables` | `schemaname`, `tablename`, `tableowner` | WHERE `schemaname` filter supported. |
56+
| `pg_class` / `pg_catalog.pg_class` | `oid`, `relname`, `relkind`, `relnamespace` | All user tables as `relkind='r'`. |
57+
| `pg_attribute` / `pg_catalog.pg_attribute` | `attrelid`, `attname`, `atttypid`, `attnum`, `attnotnull` | One row per column across all tables. |
58+
| `pg_namespace` / `pg_catalog.pg_namespace` | `oid`, `nspname`, `nspowner` | Returns `public` (2200), `pg_catalog` (11), `information_schema` (12387). |
59+
| `pg_type` / `pg_catalog.pg_type` | `oid`, `typname`, `typtype`, `typcategory` | 13 base types: text, int4, int8, float8, bool, timestamp, timestamptz, date, uuid, bytea, numeric, json, jsonb. |
60+
| `pg_roles` / `pg_catalog.pg_roles` | `oid`, `rolname`, `rolsuper` | Returns current user as superuser. |
61+
| `pg_user` / `pg_catalog.pg_user` | Same as pg_roles | Alias. |
62+
| `pg_am` / `pg_catalog.pg_am` | `oid`, `amname`, `amtype` | Returns `btree` and `hash`. |
63+
| `pg_index` / `pg_catalog.pg_index` | All index columns | Returns empty set (no index catalog). |
64+
| `pg_indexes` / `pg_catalog.pg_indexes` | All index columns | Returns empty set. |
65+
| `pg_constraint` / `pg_catalog.pg_constraint` | All constraint columns | Returns empty set. |
66+
| `pg_proc` / `pg_catalog.pg_proc` | All proc columns | Returns empty set. |
67+
| `pg_trigger` / `pg_catalog.pg_trigger` | All trigger columns | Returns empty set. |
68+
| `pg_description` / `pg_catalog.pg_description` | All description columns | Returns empty set. |
69+
| `pg_stat_user_tables` | All stat columns | Returns empty set. |
70+
| `pg_sequence` | All sequence columns | Returns empty set. |
71+
| `pg_attrdef` | All attrdef columns | Returns empty set. |
72+
| `pg_depend` | All depend columns | Returns empty set. |
73+
74+
---
75+
76+
## Type Mapping
77+
78+
SharpCoreDB engine types are mapped to standard SQL and PostgreSQL type names:
79+
80+
| Engine Type | SQL Type | pg OID | UDT Name |
81+
|---|---|---|---|
82+
| INTEGER / INT / INT4 / INT32 | integer | 23 | int4 |
83+
| BIGINT / INT8 / INT64 | bigint | 20 | int8 |
84+
| SMALLINT / INT2 / INT16 | smallint | 21 | int2 |
85+
| FLOAT / REAL / FLOAT4 | real | 700 | float4 |
86+
| DOUBLE / FLOAT8 | double precision | 701 | float8 |
87+
| DECIMAL / NUMERIC | numeric | 1700 | numeric |
88+
| BOOLEAN / BOOL | boolean | 16 | bool |
89+
| TEXT / STRING / VARCHAR / NVARCHAR / CHAR | text | 25 | text |
90+
| BLOB / BYTEA / BINARY | bytea | 17 | bytea |
91+
| TIMESTAMP / DATETIME | timestamp without time zone | 1114 | timestamp |
92+
| TIMESTAMPTZ | timestamp with time zone | 1184 | timestamptz |
93+
| DATE | date | 1082 | date |
94+
| UUID / GUID | uuid | 2950 | uuid |
95+
| JSON | json | 114 | json |
96+
| JSONB | jsonb | 3802 | jsonb |
97+
98+
---
99+
100+
## Known Limitations
101+
102+
| Gap | Impact | Workaround |
103+
|---|---|---|
104+
| No index catalog (`pg_index`) | Tools cannot show index details | Partial — tools fall back gracefully |
105+
| No constraint catalog (`pg_constraint`) | PK/FK not shown in GUI | Table structure visible, FK arrows missing |
106+
| No trigger catalog (`pg_trigger`) | Triggers not visible | Not applicable — no trigger support |
107+
| Complex multi-join catalog queries | May not match simple patterns | Tools receive empty rows without error |
108+
| `WHERE` clause parsing | Only equality filters on `table_name`, `table_schema`, `schemaname` | Other filters return all rows |
109+
110+
See `TOOL_COMPATIBILITY_LIMITATIONS_v1.6.0.md` for full gap list and workarounds.
111+
112+
---
113+
114+
## Test Coverage
115+
116+
`tests/SharpCoreDB.Server.IntegrationTests/PgCatalogServiceTests.cs` — 15 tests:
117+
118+
| Test | Validates |
119+
|---|---|
120+
| `TryHandleCatalogQuery_CurrentDatabase_ReturnsDatabaseName` | `current_database()` scalar |
121+
| `TryHandleCatalogQuery_Version_ReturnsVersionString` | `version()` scalar |
122+
| `TryHandleCatalogQuery_CurrentUser_ReturnsUserName` | `current_user` scalar |
123+
| `TryHandleCatalogQuery_InformationSchemaTables_ReturnsUserTables` | Full table list from live schema |
124+
| `TryHandleCatalogQuery_InformationSchemaTables_FilterByPublicSchema_ReturnsRows` | Schema filter pass-through |
125+
| `TryHandleCatalogQuery_InformationSchemaTables_FilterByNonPublicSchema_ReturnsEmpty` | Non-public schema returns empty |
126+
| `TryHandleCatalogQuery_InformationSchemaColumns_ReturnsColumnsForTable` | Column list for specific table |
127+
| `TryHandleCatalogQuery_InformationSchemaColumns_HasOrdinalPosition` | Ordinal position populated |
128+
| `TryHandleCatalogQuery_PgTables_ReturnsUserTables` | `pg_tables` with schema filter |
129+
| `TryHandleCatalogQuery_PgClass_ReturnsRelations` | `pg_class` relkind='r' |
130+
| `TryHandleCatalogQuery_PgNamespace_ContainsPublicSchema` | Three schemas returned |
131+
| `TryHandleCatalogQuery_PgType_ContainsBaseTypes` | Base type catalog |
132+
| `TryHandleCatalogQuery_PgAttribute_ReturnsColumnsForAllTables` | Column attributes |
133+
| `TryHandleCatalogQuery_UserTableQuery_ReturnsFalse` | Non-catalog queries passed through |
134+
| `TryHandleCatalogQuery_InformationSchemaSchemata_ContainsPublic` | Schemata view |
135+
136+
---
137+
138+
*Phase 02 of admin-console roadmap — implemented in SharpCoreDB V 1.60*

src/SharpCoreDB.Server.Core/BinaryProtocolHandler.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
using Microsoft.Extensions.Logging;
77
using SharpCoreDB.Server.Core;
8+
using SharpCoreDB.Server.Core.Catalog;
89
using SharpCoreDB.Server.Core.Security;
910
using System.Buffers;
1011
using System.Buffers.Binary;
@@ -25,12 +26,14 @@ public sealed class BinaryProtocolHandler(
2526
SessionManager sessionManager,
2627
UserAuthenticationService authService,
2728
TenantAuthorizationPolicyService tenantAuthorizationPolicyService,
29+
PgCatalogService pgCatalogService,
2830
ILogger<BinaryProtocolHandler> logger) : IAsyncDisposable
2931
{
3032
private readonly DatabaseRegistry _databaseRegistry = databaseRegistry;
3133
private readonly SessionManager _sessionManager = sessionManager;
3234
private readonly UserAuthenticationService _authService = authService;
3335
private readonly TenantAuthorizationPolicyService _tenantAuthorizationPolicyService = tenantAuthorizationPolicyService;
36+
private readonly PgCatalogService _pgCatalogService = pgCatalogService;
3437
private readonly ILogger<BinaryProtocolHandler> _logger = logger;
3538

3639
// PostgreSQL protocol constants
@@ -282,6 +285,20 @@ private async Task HandleQueryAsync(
282285

283286
_logger.LogInformation("Executing query: {Query}", queryText);
284287

288+
// Intercept pg_catalog / information_schema queries before reaching the engine
289+
if (_pgCatalogService.TryHandleCatalogQuery(
290+
queryText,
291+
session.DatabaseInstance.Database,
292+
session.DatabaseInstance.Configuration.Name,
293+
session.UserName ?? "anonymous",
294+
out var catalogRows,
295+
out var catalogColumns))
296+
{
297+
await WriteCatalogResultAsync(catalogRows, catalogColumns, writer, cancellationToken);
298+
await writer.WriteReadyForQueryAsync('I', cancellationToken);
299+
return;
300+
}
301+
285302
await using var connection = await session.DatabaseInstance.GetConnectionAsync(cancellationToken);
286303

287304
try
@@ -392,6 +409,19 @@ private async Task HandleExecuteAsync(
392409
return;
393410
}
394411

412+
// Intercept pg_catalog / information_schema queries via extended query protocol
413+
if (_pgCatalogService.TryHandleCatalogQuery(
414+
portal.Sql,
415+
session.DatabaseInstance.Database,
416+
session.DatabaseInstance.Configuration.Name,
417+
session.UserName ?? "anonymous",
418+
out var catalogRows,
419+
out var catalogColumns))
420+
{
421+
await WriteCatalogResultAsync(catalogRows, catalogColumns, writer, cancellationToken);
422+
return;
423+
}
424+
395425
await using var connection = await session.DatabaseInstance.GetConnectionAsync(cancellationToken);
396426

397427
try
@@ -530,6 +560,49 @@ private async Task HandleCancelRequestAsync(MemoryStream reader)
530560
}
531561
}
532562

563+
/// <summary>
564+
/// Writes a synthetic catalog result set to the client.
565+
/// Sends RowDescription, DataRows, and CommandComplete in sequence.
566+
/// </summary>
567+
private static async Task WriteCatalogResultAsync(
568+
List<Dictionary<string, object?>> rows,
569+
List<string> columns,
570+
BinaryProtocolWriter writer,
571+
CancellationToken cancellationToken)
572+
{
573+
if (columns.Count == 0)
574+
{
575+
await writer.WriteCommandCompleteAsync("SELECT", 0, cancellationToken);
576+
return;
577+
}
578+
579+
var fields = columns.Select((col, idx) => new FieldDescription
580+
{
581+
Name = col,
582+
TableId = 0,
583+
ColumnId = (short)idx,
584+
DataTypeId = 25, // text
585+
DataTypeSize = -1,
586+
TypeModifier = -1,
587+
FormatCode = 0,
588+
}).ToArray();
589+
590+
await writer.WriteRowDescriptionAsync(fields, cancellationToken);
591+
592+
foreach (var row in rows)
593+
{
594+
var values = columns
595+
.Select(col => row.TryGetValue(col, out var v) && v is not null
596+
? Encoding.UTF8.GetBytes(v.ToString()!)
597+
: null)
598+
.ToArray();
599+
await writer.WriteDataRowAsync(values!, cancellationToken);
600+
}
601+
602+
await writer.WriteCommandCompleteAsync("SELECT", rows.Count, cancellationToken);
603+
}
604+
605+
533606
/// <summary>
534607
/// Reads a null-terminated string from a stream.
535608
/// </summary>

0 commit comments

Comments
 (0)