HomeFeaturesPluginsDocsDownload

Extending: the SDK seams

What a plugin can hook into beyond the four plugin types — the interfaces a provider or extension implements to teach SQL Explorer something new. Everything here is additive: a plugin that ignores a seam keeps working.

This page is the map. The full developer reference — project layout, packaging, publishing — lives in PLUGINS.md in the repository. Everything below is against 0.4.0: provider host API 27, tools and extensions 4.

Completion: functions and JOIN hints

The editor's completion is scope-aware on the host side — it understands CTEs, subqueries, derived tables and alias scopes, and does not leak across statement boundaries. A provider does not implement any of that. It only supplies the two things the host cannot know: your dialect's functions, and your foreign keys.

Function catalogue

ISqlDialect.Functions is a default-interface member returning an empty list, so declaring one is optional. Each entry is a SqlFunction(Name, Signature, Doc?): Name is the text that gets inserted, Signature is the detail line in the popup, Doc an optional one-liner.

public sealed class MyDialect : ISqlDialect { public IReadOnlyList<SqlFunction> Functions { get; } = [ new("my_func", "my_func(value [, ...])", "Does something useful."), new("now", "now()", "Current timestamp."), ]; // ... Keywords / QuoteIdentifier / QualifyName / Paginate }

They are offered in expression positions — the SELECT list, WHERE, ON, GROUP BY, ORDER BY, HAVING — and in the broad fallback, but never right after FROM/JOIN or after alias..

Foreign keys, for JOIN … ON suggestions

The host offers a ready-made ON a.col = b.col when it can see the relationship. It reads that from your schema tree, so the shape matters: expose a ForeignKeyFolder under the table with ForeignKey nodes whose Detail uses the shared format — an arrow, not a dash:

new DbTreeNode { Kind = DbNodeKind.ForeignKey, Name = fkName, Detail = $"{column} → {refTable}.{refColumn}" }

The host parses that back into structured foreign keys. An entry it does not recognise (a composite key rendered some other way) is skipped rather than mis-suggested — so a malformed Detail costs you the hint, not correctness. Column suggestions work the same way: Column nodes under a ColumnFolder (or directly under a view), with the column type in Detail, which becomes the tooltip.

Result paging: PageQuery

0.4.0 pages a single unbounded SELECT in the query editor (see Query editor). The dialect decides how:

string PageQuery(string sql, int limit, int offset, bool alreadyOrdered = false) => Paginate(sql, limit, offset);

It is a default-interface member delegating to your existing Paginate. For LIMIT/OFFSET dialects — PostgreSQL, MySQL, SQLite — that is already correct, because LIMIT … OFFSET … may follow an optional ORDER BY. No override needed.

If your dialect pages with OFFSET / FETCH, you must override it. That syntax requires an ORDER BY, so the default would append a second one to a query that already has one, and the statement fails.

The SQL Server dialect is the reference: when the query is already ordered, append to the existing ORDER BY; otherwise fall back.

public string PageQuery(string sql, int limit, int offset, bool alreadyOrdered = false) => alreadyOrdered ? $"{sql}\nOFFSET {offset} ROWS FETCH NEXT {limit} ROWS ONLY" : Paginate(sql, limit, offset);

Container recipes

A provider can declare how to run its engine as an empty local container. The Local Containers extension reads every declared recipe, so a third-party engine becomes provisionable without that extension knowing anything about it. IDbProvider.ContainerRecipe defaults to null — not containerisable, and the extension degrades gracefully.

public ContainerRecipe? ContainerRecipe => new( Image: "postgres", DefaultTag: "16", ContainerPort: 5432, DataPath: "/var/lib/postgresql/data", DefaultUser: "postgres", DefaultPassword: "changeme", Environment: e => [ new("POSTGRES_DB", e.Database ?? "postgres"), new("POSTGRES_USER", e.User), new("POSTGRES_PASSWORD", e.Password) ]);

Environment and the optional Command are delegates over a ContainerEnvInput, so credentials and the database name are filled in at provisioning time. The remaining knobs cover the engines that do not fit the default shape: NamedDatabase (the engine has no create-database-on-start concept), DatabaseAfterStart, Memlock, and HostPortOverride for a provider whose connection fields carry their own port.

Extension capabilities: services and providers

A standing-subsystem extension declares capabilities and the user consents to them at install. Two were added in 0.4.0.

services — your own DI

Mark a class with ISingletonService, ITransientService or IScopedService and the host registers it for you, then hands the plugin a resolver on IPluginRuntimeContext.Services scoped to exactly those types. A plugin can never register under — or resolve — a host contract; only its own. Without the capability the property is null.

providers — read the container recipes

With this capability, IPluginRuntimeContext.Providers exposes an IProviderCatalog: ContainerRecipes() returns one ProviderRecipe(ProviderId, DisplayName, Recipe) per installed provider that declared one. Read-only — the plugin learns which engines exist and how to provision them, and gains no control over them. This is exactly how Local Containers discovers engines.

Panel icons and the shared icon set

A docked panel supplies its own toggle glyph through IPanelPlugin.Icon (a Geometry?, default null). Draw it from SqlExplorer.Sdk.Ui.Icons, the Lucide-derived geometries the host itself uses — one source of truth, so a plugin's icons stay in step with the app instead of drifting from a copied SVG. Render with a themed stroke brush and Stretch="Uniform".

Host-API versions

Every seam above is additive, so a plugin built against an older host keeps loading — it simply contributes nothing for the seams it predates. Declare the version you built against in your manifest; the loader gates on it.