schemas

package module
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 16, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

README

schemads

[!CAUTION] This repository is experimental and in progress. Do not use this module.

Note: as of the next release, response-level caching is on by default when you call NewSchemaDatasource. See the Caching section for the default TTLs, scope behaviour, and how to disable or tune.

A wrapper for Grafana data source plugins that adds schema discovery via CallResource. Plugins implement handler interfaces (SchemaHandler, TablesHandler, ColumnsHandler, etc.) and wire them into NewSchemaDatasource; consumers use the Client to fetch table, column, table parameter, and value metadata.

Overview

  • Resource base path: abstractionSchema (BaseResourcePath), with sub-paths for each endpoint (e.g. abstractionSchema/tables).
  • Handler configured: the wrapper calls the relevant handler and returns the response as JSON.
  • Handler not configured: returns 501 Not Implemented with an endpoint-specific sentinel error.
  • Other paths: forwarded to the wrapped CallResourceHandler, or 404 if none is set.

Plugin integration

Handler interfaces

Each endpoint has its own handler interface. Implement the ones your plugin supports:

type SchemaHandler interface {
    Schema(ctx context.Context, req *schemas.SchemaRequest) (*schemas.SchemaResponse, error)
}

type TablesHandler interface {
    Tables(ctx context.Context, req *schemas.TablesRequest) (*schemas.TablesResponse, error)
}

type ColumnsHandler interface {
    Columns(ctx context.Context, req *schemas.ColumnsRequest) (*schemas.ColumnsResponse, error)
}

type TableParameterValuesHandler interface {
    TableParameterValues(ctx context.Context, req *schemas.TableParameterValuesRequest) (*schemas.TableParametersValuesResponse, error)
}

type ColumnValuesHandler interface {
    ColumnValues(ctx context.Context, req *schemas.ColumnValuesRequest) (*schemas.ColumnValuesResponse, error)
}
Wiring into the plugin

NewSchemaDatasource returns a *SchemaDatasource that implements backend.CallResourceHandler. Pass nil for any handler your plugin does not support (those endpoints will return 501):

func NewInstance(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
    ds := &MyDatasource{}

    schemaDs := schemas.NewSchemaDatasource(
        schemaHandler,
        tablesHandler,
        columnsHandler,
        tableParameterValuesHandler,
        columnValuesHandler
        nil,  // next CallResourceHandler (or an existing handler)
    )
    return &MyInstance{SchemaDatasource: schemaDs}, nil
}
Adding schema support to an existing data source

If your plugin already implements backend.CallResourceHandler (e.g. for health checks, autocomplete, or custom endpoints), pass it as the last argument. Schema requests are intercepted; everything else is forwarded to your existing handler unchanged:

type MyDatasource struct {
    schemas.SchemaDatasource
}

func NewInstance(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
    ds := &MyDatasource{}

    schemaDs := schemas.NewSchemaDatasource(
        ds,   // SchemaHandler
        ds,   // TablesHandler
        ds,   // ColumnsHandler
        ds,   // TableParameterValuesHandler
        ds,   // ColumnValuesHandler
        backend.CallResourceHandlerFunc(ds.handleCustomResource),
    )
    ds.SchemaDatasource = *schemaDs

    return ds, nil
}

func (ds *MyDatasource) handleCustomResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
    switch req.Path {
    case "custom-endpoint":
        return sender.Send(&backend.CallResourceResponse{
            Status: 200,
            Body:   []byte(`{"ok": true}`),
        })
    default:
        return sender.Send(&backend.CallResourceResponse{
            Status: 404,
            Body:   []byte("not found"),
        })
    }
}

With this setup:

  • POST /abstractionSchema/tables (and other schema sub-paths) are handled by the schema handlers.
  • All other paths (e.g. /custom-endpoint) are forwarded to handleCustomResource.

To opt out of schema support entirely, pass nil for all handlers — schema requests return 501 and everything else is forwarded:

schemaDs := schemas.NewSchemaDatasource(nil, nil, nil, nil, nil, existingHandler)

Client usage

Create a Client with NewClient, then call the method for each endpoint. Both httpClient and baseURL are required. Headers on each request type are forwarded as HTTP headers.

client, err := schemas.NewClient(httpClient, "https://host/api/ds/uid/resources/abstractionSchema")

Fetching tables:

resp, err := client.FetchTables(ctx, &schemas.TablesRequest{})
// resp.Tables    – []string
// resp.TableParameters – map[string][]TableParameter

Fetching columns for specific tables:

resp, err := client.FetchColumns(ctx, &schemas.ColumnsRequest{
    Tables: []string{"issues"},
})
// resp.Columns – map[string][]Column

Fetching columns scoped by table parameters:

resp, err := client.FetchColumns(ctx, &schemas.ColumnsRequest{
    Tables:          []string{"issues"},
    TableParameters: map[string]string{"organization": "grafana", "repository": "grafana"},
})

Fetching table parameter values:

resp, err := client.FetchTableParameterValues(ctx, &schemas.TableParameterValuesRequest{
    Table:            "issues",
    TableParameter:   "repository",
    DependencyValues: map[string]string{"organization": "grafana"},
})
// resp.TableParameterValues – map[string][]string

Fetching column values:

resp, err := client.FetchColumnValues(ctx, &schemas.ColumnValuesRequest{
    Table:   "issues",
    Columns: []string{"status"},
})
// resp.ColumnValues – map[string][]string

Fetching the full schema:

resp, err := client.FetchSchema(ctx, &schemas.SchemaRequest{})
// resp.FullSchema – *Schema

Table parameters

Table parameters model hierarchical scoping parameters that consumers resolve before querying a table (e.g. a GitHub data source requires selecting an organization and then a repository before querying issues).

Definition

Each TableParameter has:

Field Type Description
Name string Unique name within the parent table.
DependsOn []string Sibling table parameters whose values must be selected first.
Root bool Entry point with no dependencies. At least one is required.
Required bool Must be resolved before the table can be queried.
Validation

ValidateSchema is called automatically for fullSchema responses and enforces:

  1. Unique names – no two table parameters in a table share a name.
  2. Valid references – every DependsOn entry names a sibling table parameter.
  3. Acyclic graph – the dependency graph has no cycles.
  4. Root rules – at least one table parameter is Root and root table parameters have no dependencies.
  5. Required chain – a required table parameter may only depend on other required table parameters.
  6. TableParameterValues integritySchema.TableParameterValues keys reference existing tables and root table parameters only. Non-root table parameter values depend on ancestor selections and must be fetched incrementally via tableParameterValues with DependencyValues.

Call ValidateSchema directly to validate schemas you construct manually.

Table references (tables)

Consumers that need to embed a parameterised table in a single human-writable string (for example, an internal query language that says FROM <ref>) can use the tables subpackage.

A reference is an undelimited identifier with a non-empty table name:

<table>(<param1>=<value1>,<param2>=<value2>,...)

Examples:

events                                   // no parameters
events(env=prod)                         // one parameter
events(env=prod,service=tempo)           // multiple parameters
tags(name=Promo \(2024\))                // value with escaped reserved chars

The format is purely syntactic — tables does not produce or consume SQL. It exists so that a reference can be parsed unambiguously back into (table, map[paramName]paramValue) even when parameter values are user-supplied free text.

The encoded form has no outer delimiters. If the surrounding system wraps references in delimiters of its own (for example, backticks in a query language), callers must add them on the way out and strip them before calling Parse. See Embedding in a wider grammar below for the standard backtick recipe.

Reserved characters. Inside a table name, parameter key, or parameter value the five characters (, ), ,, =, and \ are reserved and must be backslash-escaped (\(, \,, \\, etc.). Any other backslash sequence is a parse error. Backticks are not reserved and are passed through verbatim.

Whitespace. Outer whitespace (any Unicode whitespace before or after the entire reference) is trimmed by Parse. Inside the reference, decoding tolerates optional whitespace around (, ), =, and ,, so events(env=prod, service=tempo) and events(env=prod,service=tempo) parse identically. Whitespace inside a value is preserved verbatim, but leading and trailing ASCII whitespace in a value is treated as separator padding and is not preserved across round-trips.

Empty values and empty parameter lists. events(env=) decodes to {"env": ""} (an empty string). A key absent from the parameter list is unset, which is distinct from an empty string. An empty parameter list is normalised: both events and events() decode to a TableRef with TableParams == nil, and a TableRef with no parameters always renders as just the table name (no trailing parens).

API.

import "github.com/grafana/schemads/tables"

ref, err := tables.Parse("events(env=prod,service=tempo)")
// ref.Table       -> "events"
// ref.TableParams -> map[string]string{"env": "prod", "service": "tempo"}

s := tables.TableRef{
    Table:       "events",
    TableParams: map[string]string{"env": "prod", "service": "tempo"},
}.String()
// s -> "events(env=prod,service=tempo)"   (params sorted, escaped)

if err := tables.Validate(ref, schema); err != nil {
    // joined error: ErrUnknownTable, ErrUnknownParameter,
    // ErrMissingRequired, and/or ErrMissingDependency.
}

Parse performs only syntactic validation. It requires a non-empty table name, but does not check whether that table exists. Use Validate to check a decoded reference against a Schema: that the table exists, every key is a declared parameter, every required parameter is present, and every present parameter's declared dependencies are also present. Validate aggregates all issues into a single errors.Join error so callers see every problem in one pass. See the package documentation for the full grammar.

Canonical form

The decoder is intentionally lenient (whitespace around separators, events() accepted for a no-parameter reference, etc.) while the encoder is strict (parameters sorted, no whitespace, empty parameter lists collapsed). Two raw inputs that mean the same thing are not necessarily byte-equal. Use tables.Canonicalize to normalise a reference before using it as a map key, cache key, or identity comparison value:

canon, err := tables.Canonicalize(rawInput) // == Parse(rawInput).String()
Embedding in a wider grammar

References produced by this package have no outer delimiters. When a consumer wraps a reference in its own delimiters — for example a query language that uses backticks — that consumer is responsible for escaping its delimiter inside the wrapped content, because the inner format does not reserve any characters for that purpose. In particular, backticks may appear unescaped inside a parameter value.

tables.WrapInBackticks and tables.UnwrapFromBackticks implement the standard double-the-delimiter recipe used by SQL identifier quoting (` ``):

stored := tables.WrapInBackticks(ref.String())     // "`events(env=prod)`"
inner, err := tables.UnwrapFromBackticks(stored)   // "events(env=prod)"
ref, err   := tables.Parse(inner)

Any backtick inside ref.String() is doubled on the way out and un-doubled on the way in, so the round-trip preserves arbitrary inner content (including parameter values that legitimately contain backticks).

Endpoints

Path Constant Request type Response type
fullSchema RequestTypeFullSchema SchemaRequest SchemaResponse
tables RequestTypeTables TablesRequest TablesResponse
columns RequestTypeColumns ColumnsRequest ColumnsResponse
columnValues RequestTypeColumnValues ColumnValuesRequest ColumnValuesResponse
tableParameterValues RequestTypeTableParameterValues TableParameterValuesRequest TableParametersValuesResponse

Caching

schemads includes a tenant-safe in-memory response cache that is on by default the moment a plugin calls NewSchemaDatasource. It is backed by patrickmn/go-cache with TTL cleanup and a bounded response value size. Plugins can also reuse the same cache instance for in-handler sub-fetches (per-subscription workspaces, per-index field caps, etc.) via cache.Typed.

Default behaviour

Calling the standard constructor enables caching with sensible per-endpoint defaults:

Endpoint Default TTL Default scope
fullSchema 5 min cache.ScopeUser
tables 5 min cache.ScopeUser
columns 2 min cache.ScopeUser
tableParameterValues 1 min cache.ScopeUser
columnValues disabled (TTL = 0) cache.ScopeUser

columnValues is intentionally excluded from the default cache because:

  1. Time-range dependent. ColumnValuesRequest carries a TimeRange, and values legitimately change with it (e.g. distinct status codes seen in the last 5m vs the last hour).
  2. Freshness-sensitive. Column values back autocomplete dropdowns; stale values produce user-visible bugs.
  3. Higher PII risk. Values often contain user-identifying data; shorter exposure is the conservative default.

Cache keys mirror the SDK instancemgmt convention so reconfiguration auto-invalidates entries:

namespace#dsUID#updated#proxyHash[#userHash]#sha256(endpoint|minimalTypedFields)

The user component is hashed before it reaches the key string, so keys are safe to log. This means rotating credentials, swapping the PDC tunnel, or editing datasource settings all invalidate cached entries automatically — there is no need to flush manually.

Tuning via Options

Use NewSchemaDatasourceWithOptions to override defaults:

import (
    schemas "github.com/grafana/schemads"
    "github.com/grafana/schemads/cache"
)

opts := schemas.DefaultOptions
// Re-enable ColumnValues with a short TTL — autocomplete results need to feel fresh.
opts.TTL.ColumnValues = 30 * time.Second
// Tune the shared in-memory cache.
opts.Cache.MaxValueBytes = 512 * 1024

ds := schemas.NewSchemaDatasourceWithOptions(
    schemaHandler, tablesHandler, columnsHandler,
    tableParameterValuesHandler, columnValuesHandler,
    next,
    opts,
)

Note: EndpointTTLs is a flat struct (not an overlay map). When you set any TTL, copy DefaultOptions first so you don't accidentally zero out the others.

Disabling caching entirely
ds := schemas.NewSchemaDatasourceWithOptions(..., schemas.Options{
    DisableCache: true,
})
Manually invalidating an entry

Send the request again with the refresh header configured in RefreshPolicy.Header (default X-Schemads-Refresh). The wrapper deletes the cached entry, runs the handler, and re-caches the result. Bypass is rate-limited per (tenant, endpoint) (default 5s) to prevent CPU DoS via constant invalidation.

Choosing a scope

The default ScopeUser is the safe choice. You can relax to ScopeDatasource per endpoint via Options.PerEndpointScope, only after auditing that the endpoint's results do not depend on the calling user's permissions.

Use this checklist:

Question If yes...
Does the upstream filter results by the calling user's identity, role, or group membership? Keep ScopeUser (default)
Does the upstream apply RBAC or row-level security tied to the impersonated user? Keep ScopeUser (default)
Does the response contain PII that should not be visible to all users with read access to the datasource? Keep ScopeUser (default)
Is the response a function purely of (datasource, request body) — same input, same output, regardless of caller? Relax to ScopeDatasource

Example — Tables/Columns shared across users on a cluster that does not enforce per-user index visibility:

opts := schemas.DefaultOptions
opts.PerEndpointScope = map[string]cache.Scope{
    schemas.RequestTypeTables:  cache.ScopeDatasource,
    schemas.RequestTypeColumns: cache.ScopeDatasource,
}

When ScopeUser is configured but the request has no user (alerting, public dashboards, recorded queries), the wrapper bypasses the cache and logs a warning rather than serving anonymous results to authenticated users.

In-handler sub-fetch caching

Plugins frequently call upstream APIs from inside their handlers (Azure's per-subscription workspaces, Elasticsearch's per-index _field_caps). Use cache.Typed[T] to memoize these without paying JSON marshal cost:

type Plugin struct {
    schemaDS    *schemas.SchemaDatasource
    workspaces  *cache.Typed[[]Workspace]
}

func NewPlugin(...) *Plugin {
    schemaDS := schemas.NewSchemaDatasource(...)
    return &Plugin{
        schemaDS:   schemaDS,
        // Tag with a label for Prometheus metrics.
        workspaces: cache.NewTyped[[]Workspace](schemaDS.Cache(), "subfetch:workspaces"),
    }
}

func (p *Plugin) listWorkspaces(ctx context.Context, pc backend.PluginContext, sub string) ([]Workspace, error) {
    // Build a tenant-safe key. ScopeUser is recommended for any subscription-
    // scoped lookup since the user's role determines visible workspaces.
    key, err := cache.KeyFromPluginContext(pc, cache.ScopeUser, "workspaces", sub)
    if err != nil {
        // Fall back to direct fetch (e.g. nil User in alerting context).
        return fetchWorkspaces(ctx, sub)
    }
    return p.workspaces.GetOrFetch(ctx, key, 5*time.Minute, func(ctx context.Context) ([]Workspace, error) {
        return fetchWorkspaces(ctx, sub)
    })
}

Typed[T] deduplicates concurrent misses for the same key with singleflight, never caches errors, and increments schemads_cache_*_total{endpoint="subfetch:workspaces"} Prometheus counters. Scope is controlled by the cache.Key you pass in, so the same typed cache can safely hold ScopeUser and ScopeDatasource entries when keys are built correctly.

For small JSON-serializable values, plugins can also use the byte-oriented cache.GetOrFetch helper against ds.Cache():

v, err := cache.GetOrFetch(ctx, ds.Cache(), key, "subfetch:workspaces", 5*time.Minute, fetchFn)

This is appropriate when the cached value is small/serializable and JSON marshal/unmarshal cost is negligible; Typed[T] is preferred for hot paths where marshal/unmarshal would dominate CPU.

Metrics

The cache exports the following Prometheus counters:

Metric Labels Meaning
schemads_cache_hits_total endpoint Cache hits per endpoint (or sub-fetch label)
schemads_cache_misses_total endpoint Cache misses
schemads_cache_evictions_total endpoint TTL evictions

Register them with your Prometheus registry once at startup:

cache.MustRegisterMetrics(prometheus.DefaultRegisterer)

If never registered, metrics still record but are not exposed (matches other SDK packages).

Eviction model
  • TTL. Entries expire after their per-endpoint TTL. The in-memory cache (cache.MemoryCache) is built on patrickmn/go-cache and runs a background sweep every CleanupInterval (default 5 minutes) so unaccessed entries do not linger.
  • Manual. The refresh header (default X-Schemads-Refresh) deletes the specific key for the requesting tenant only — never the whole cache.
  • Response size. Byte-oriented response entries larger than MaxValueBytes (default 5 MiB) are not cached.

Column types

Types are aligned with go-mysql-server's type system:

Category Constants
Boolean boolean
Integers int8, int16, int32, int64, uint8, uint16, uint32, uint64
Float float32, float64
Decimal decimal (use Column.Precision / Column.Scale)
String string
Date/Time date, datetime, timestamp, time, year
Structured json, enum (use Column.Values), set (use Column.Values)
Binary blob, bit (use Column.Size, 1-64)

Operators

Columns declare which filter operations they support via Column.Operators:

Constant Value
OperatorEquals =
OperatorNotEquals !=
OperatorGreaterThan >
OperatorGreaterThanOrEqual >=
OperatorLessThan <
OperatorLessThanOrEqual <=
OperatorLike like
OperatorIn in

Error handling

Errors are returned as JSON {"error": "..."} with an HTTP status:

Status Meaning
500 Handler error or schema validation failure
501 Handler not configured for the requested endpoint

Each endpoint has its own sentinel error for 501 responses:

Sentinel Endpoint
ErrSchemaNotImplemented fullSchema
ErrTablesNotImplemented tables
ErrColumnsNotImplemented columns
ErrTableParameterValuesNotImplemented tableParameterValues
ErrColumnValuesNotImplemented columnValues

The Client returns these sentinels on 501 so callers can use errors.Is:

resp, err := client.FetchColumns(ctx, &schemas.ColumnsRequest{Tables: []string{"t"}})
if errors.Is(err, schemas.ErrColumnsNotImplemented) {
    // plugin does not support this endpoint
}

Partial failures can be reported via the Errors field on each response type (keyed by table or column name).

Documentation

Overview

Package schemas provides schema discovery for Grafana data source plugins.

Plugins implement handler interfaces (SchemaHandler, TablesHandler, ColumnsHandler, etc.) and wire them into NewSchemaDatasource to expose table, column, and table parameter metadata via CallResource. Consumers fetch metadata with Client.

Index

Constants

View Source
const (
	// RequestTypeFullSchema returns the complete schema (tables, columns,
	// table parameters, functions, and table parameter values).
	RequestTypeFullSchema string = "fullSchema"
	// RequestTypeTables returns table names and their table parameter definitions.
	RequestTypeTables string = "tables"
	// RequestTypeColumns returns columns for the tables listed in
	// [ColumnsRequest.Tables].
	RequestTypeColumns string = "columns"
	// RequestTypeColumnValues returns possible values for the columns listed in
	// [ColumnValuesRequest.Columns].
	RequestTypeColumnValues string = "columnValues"
	// RequestTypeTableParameterValues returns possible values for table parameters
	// identified by [TableParameterValuesRequest].
	RequestTypeTableParameterValues string = "tableParameterValues"
)
View Source
const BaseResourcePath = "abstractionSchema"

Variables

View Source
var DefaultOptions = Options{
	TTL: EndpointTTLs{
		FullSchema:           5 * time.Minute,
		Tables:               5 * time.Minute,
		Columns:              2 * time.Minute,
		TableParameterValues: 1 * time.Minute,
		ColumnValues:         0,
	},
	DefaultScope:     cache.ScopeUser,
	PerEndpointScope: nil,
	Refresh: RefreshPolicy{
		Header:      "X-Schemads-Refresh",
		MinInterval: 5 * time.Second,
	},
}

DefaultOptions captures the safe-by-default behaviour applied when callers use NewSchemaDatasource (i.e. no Options passed).

The defaults intentionally cache only the read-mostly endpoints. ColumnValues is excluded — see options.go EndpointTTLs.ColumnValues docstring.

View Source
var ErrColumnValuesNotImplemented = errors.New("column values not implemented")
View Source
var ErrColumnsNotImplemented = errors.New("columns not implemented")
View Source
var ErrSchemaNotImplemented = errors.New("schema not implemented")
View Source
var ErrTableParameterValuesNotImplemented = errors.New("table parameter values not implemented")
View Source
var ErrTablesNotImplemented = errors.New("tables not implemented")

SupportedAggregateFunctions lists every AggregateFunction the SQL engine can push down. Datasources should declare a subset of these in DatasourceCapabilities.AggregateFunctions.

Functions

func ValidateSchema added in v0.0.1

func ValidateSchema(schema *Schema) error

ValidateSchema validates the structural integrity of a Schema's table parameter definitions. It is called automatically when a fullSchema response is returned and may also be called by consumers who construct schemas manually.

For each table that defines table parameters, ValidateSchema checks:

  • Table parameter name is unique within the parent table.
  • Every DependsOn entry references an existing sibling table parameter.
  • The dependency graph is acyclic.
  • At least one table parameter is marked Root, and root table parameters have no dependencies.
  • Required table parameters only depend on other required table parameters (the "required chain" invariant).

At the schema level it also verifies that TableParameterValues only references tables and root table parameters that actually exist in the schema. Non-root table parameter values depend on ancestor selections and cannot be pre-populated; they must be fetched via TableParameterValuesRequest.

Returns nil when schema is nil or all invariants hold.

Types

type AggregateFunction added in v0.0.11

type AggregateFunction string

AggregateFunction names an aggregation that a datasource can declare support for in DatasourceCapabilities.AggregateFunctions. Only the functions for which the SQL engine has a defined two-phase (partial/final) decomposition can be pushed down — see the constants below for the supported set. Adding a new function requires engine support for its partial/final pair; declaring an unrecognised name has no effect.

const (
	AggregateSum   AggregateFunction = "SUM"
	AggregateAvg   AggregateFunction = "AVG"
	AggregateCount AggregateFunction = "COUNT"
	AggregateMin   AggregateFunction = "MIN"
	AggregateMax   AggregateFunction = "MAX"
)

type Client added in v0.0.1

type Client struct {
	// contains filtered or unexported fields
}

func NewClient added in v0.0.1

func NewClient(httpClient *http.Client, baseURL string) (*Client, error)

func (*Client) FetchColumnValues added in v0.0.1

func (c *Client) FetchColumnValues(ctx context.Context, req *ColumnValuesRequest) (*ColumnValuesResponse, error)

func (*Client) FetchColumns added in v0.0.1

func (c *Client) FetchColumns(ctx context.Context, req *ColumnsRequest) (*ColumnsResponse, error)

func (*Client) FetchSchema added in v0.0.1

func (c *Client) FetchSchema(ctx context.Context, req *SchemaRequest) (*SchemaResponse, error)

func (*Client) FetchTableParameterValues added in v0.0.1

func (c *Client) FetchTableParameterValues(ctx context.Context, req *TableParameterValuesRequest) (*TableParametersValuesResponse, error)

func (*Client) FetchTables added in v0.0.1

func (c *Client) FetchTables(ctx context.Context, req *TablesRequest) (*TablesResponse, error)

type Column

type Column struct {
	Name string     `json:"name"`
	Type ColumnType `json:"type"`
	// Operators lists the filter operations this column supports.
	// When empty, consumers should assume no filtering is available
	// and avoid pushing filters for this column.
	Operators []Operator `json:"operators,omitempty"`
	// Description is optional human/LLM-readable documentation for the column.
	//
	// Deprecated: use Metadata.Description. Producers SHOULD populate
	// Metadata.Description; for one release they MAY also populate this
	// field for older consumers. Consumers SHOULD prefer Metadata.Description
	// and fall back to this field only if Metadata.Description is empty.
	Description string `json:"description,omitempty"`
	// Precision and Scale apply to ColumnTypeDecimal.
	Precision *int `json:"precision,omitempty"`
	Scale     *int `json:"scale,omitempty"`
	// Size applies to ColumnTypeBit (bit width, 1-64).
	Size *int `json:"size,omitempty"`
	// Values lists the allowed members for ColumnTypeEnum and ColumnTypeSet (if available).
	Values []string `json:"values,omitempty"`
	// SupportsValues is true when distinct values for this column can be fetched
	// via a [ColumnValuesRequest] (columnValues resource). When false or omitted,
	// consumers should not call columnValues for this column.
	SupportsValues *bool `json:"supportsValues,omitempty"`
	// Metadata carries optional descriptive information about the column
	// (e.g. SQL COLUMN COMMENT, OpenAPI field docs, unit of measure).
	Metadata Metadata `json:"metadata,omitzero"`
}

Column describes a single column in a table, including its type and optional metadata such as supported filter operators and documentation.

type ColumnFilter added in v0.0.2

type ColumnFilter struct {
	Name       string            `json:"name"`
	Conditions []FilterCondition `json:"conditions"`
}

ColumnFilter represents a set of filters applied to a specific column. Multiple conditions are ANDed together.

type ColumnType

type ColumnType string

ColumnType is the scalar type of a column. Values are aligned with go-mysql-server's type system so consumers can map them directly to SQL types without loss of fidelity.

const (
	ColumnTypeString   ColumnType = "string"
	ColumnTypeDatetime ColumnType = "datetime"
	ColumnTypeBoolean  ColumnType = "boolean"

	ColumnTypeInt8  ColumnType = "int8"
	ColumnTypeInt16 ColumnType = "int16"
	ColumnTypeInt32 ColumnType = "int32"
	ColumnTypeInt64 ColumnType = "int64"

	ColumnTypeUint8  ColumnType = "uint8"
	ColumnTypeUint16 ColumnType = "uint16"
	ColumnTypeUint32 ColumnType = "uint32"
	ColumnTypeUint64 ColumnType = "uint64"

	ColumnTypeFloat32 ColumnType = "float32"
	ColumnTypeFloat64 ColumnType = "float64"

	// ColumnTypeDecimal requires Column.Precision and Column.Scale.
	ColumnTypeDecimal ColumnType = "decimal"

	ColumnTypeDate      ColumnType = "date"
	ColumnTypeTimestamp ColumnType = "timestamp"
	ColumnTypeTime      ColumnType = "time"
	ColumnTypeYear      ColumnType = "year"

	// ColumnTypeEnum and ColumnTypeSet require Column.Values.
	ColumnTypeJSON ColumnType = "json"
	ColumnTypeEnum ColumnType = "enum"
	ColumnTypeSet  ColumnType = "set"

	// ColumnTypeBit requires Column.Size (1-64).
	ColumnTypeBlob ColumnType = "blob"
	ColumnTypeBit  ColumnType = "bit"
)

type ColumnValuesHandler added in v0.0.1

type ColumnValuesHandler interface {
	ColumnValues(ctx context.Context, req *ColumnValuesRequest) (*ColumnValuesResponse, error)
}

type ColumnValuesRequest added in v0.0.1

type ColumnValuesRequest struct {
	CommonRequest
	Table           string            `json:"table"`
	Columns         []string          `json:"columns,omitempty"`
	TableParameters map[string]string `json:"tableParameters,omitempty"`
	// SchemaContext carries concrete values for hints that affect column discovery.
	SchemaContext map[string]string `json:"schemaContext,omitempty"`
	TimeRange     apidata.TimeRange `json:"timeRange"`
}

ColumnValuesRequest identifies a column whose possible values are being requested, along with optional parameters to scope the result.

type ColumnValuesResponse added in v0.0.1

type ColumnValuesResponse struct {
	ColumnValues map[string][]string `json:"columnValues"`
	Errors       map[string]string   `json:"errors,omitempty"`
}

type ColumnsHandler added in v0.0.1

type ColumnsHandler interface {
	Columns(ctx context.Context, req *ColumnsRequest) (*ColumnsResponse, error)
}

type ColumnsRequest added in v0.0.1

type ColumnsRequest struct {
	CommonRequest
	Tables          []string          `json:"tables"`
	TableParameters map[string]string `json:"tableParameters,omitempty"`
	// SchemaContext carries concrete values for hints declared with AffectsSchema
	// on the table (e.g. PARSER=logfmt for Loki parsed line fields). Populated
	// from FOR (...) clauses at SQL planning time; does not replace execution
	// hints on Query.TableHintValues.
	SchemaContext map[string]string `json:"schemaContext,omitempty"`
}

type ColumnsResponse added in v0.0.1

type ColumnsResponse struct {
	Columns map[string][]Column `json:"columns"`
	// TableMetadata carries optional table-level descriptive metadata for
	// the requested tables (e.g. Prometheus HELP/TYPE/UNIT). Populated
	// lazily here — keyed by table name — so producers that need a
	// per-table upstream call to assemble metadata don't have to fan
	// out during fullSchema. Consumers that need metadata for a table
	// they've already fetched columns for will find it here.
	TableMetadata map[string]Metadata `json:"tableMetadata,omitempty"`
	Errors        map[string]string   `json:"errors,omitempty"`
}

type CommonRequest added in v0.0.8

type CommonRequest struct {
	Headers       http.Header           `json:"-"`
	PluginContext backend.PluginContext `json:"-"`
}

type DatasourceCapabilities added in v0.0.11

type DatasourceCapabilities struct {
	// AggregateFunctions lists aggregate functions the datasource can execute
	// natively. Values should be drawn from the AggregateFunction constants
	// (AggregateSum, AggregateAvg, AggregateCount, AggregateMin, AggregateMax)
	// — these are the only functions the SQL engine knows how to decompose
	// for pushdown. When the SQL engine pushes an aggregation, it will not
	// re-aggregate the result.
	AggregateFunctions []AggregateFunction `json:"aggregateFunctions,omitempty"`

	// OrderBy indicates the datasource can sort results natively.
	//
	// Declared for forward compatibility: the field is not yet consulted by
	// the SQL engine, which currently pushes ORDER BY unconditionally for
	// single-table queries via SQL-text extraction. A future change will
	// gate that pushdown on this flag.
	OrderBy bool `json:"orderBy,omitempty"`

	// Limit indicates the datasource can limit result count natively.
	//
	// Declared for forward compatibility: the field is not yet consulted by
	// the SQL engine, which currently pushes LIMIT unconditionally for
	// single-table queries via SQL-text extraction. A future change will
	// gate that pushdown on this flag.
	Limit bool `json:"limit,omitempty"`
}

DatasourceCapabilities describes what SQL operations can be handled natively, either at the datasource level ([Schema.Capabilities], [TablesResponse.Capabilities]) or per table ([Table.Capabilities], [TablesResponse.TableCapabilities]). When a capability is declared, the SQL engine may push the operation down to the datasource instead of executing it locally. The datasource is then expected to return results as if the operation was applied.

type EndpointTTLs added in v0.1.0

type EndpointTTLs struct {
	FullSchema           time.Duration
	Tables               time.Duration
	Columns              time.Duration
	TableParameterValues time.Duration
	// ColumnValues is intentionally zero in DefaultOptions because the
	// response depends on TimeRange, freshness matters for autocomplete UIs,
	// and values are more likely to contain PII. Plugins must opt in
	// explicitly.
	ColumnValues time.Duration
}

EndpointTTLs holds a TTL for each schemads endpoint. Zero disables caching for that endpoint only.

type FilterCondition added in v0.0.2

type FilterCondition struct {
	Operator Operator `json:"operator"`
	Value    any      `json:"value,omitempty"`
	Values   []any    `json:"values,omitempty"`
}

FilterCondition represents a single filter condition applied to a column. For scalar operators (=, !=, >, >=, <, <=, like) use Value. For the "in" operator use Values.

type Metadata added in v0.2.0

type Metadata struct {
	// Description is free-form human/LLM-readable documentation
	// (e.g. Prometheus HELP, SQL COMMENT).
	Description string `json:"description,omitempty"`
	// DisplayName is an optional human-readable display label for a table
	// or column (e.g. "Pull Requests" for a table named "pull_requests").
	// Consumers SHOULD prefer DisplayName for UI display and fall back to
	// the canonical Name when DisplayName is empty.
	DisplayName string `json:"displayName,omitempty"`
	// Unit is an optional unit of measure for the value
	// (e.g. "seconds", "bytes"). Free-form; no enforced vocabulary.
	// Should not be set for table metadata
	Unit string `json:"unit,omitempty"`
	// Custom is an escape hatch for datasource-specific metadata that
	// doesn't fit a typed slot (e.g. Prometheus metric "type":
	// counter/gauge/histogram/summary).
	Custom map[string]any `json:"custom,omitempty"`
}

Metadata carries optional descriptive information about a Table or Column. Description, DisplayName and Unit are well-known typed slots; anything datasource-specific belongs in Custom.

Convention for Custom keys: lowercase, datasource-namespaced where ambiguous (e.g. "prom.type" rather than "type"). Consumers that don't recognise a key should ignore it.

type Operator added in v0.0.1

type Operator string

Operator represents a filter operation that a column supports. Consumers use this to decide which filter predicates can be pushed down to the data source.

const (
	OperatorEquals             Operator = "="
	OperatorNotEquals          Operator = "!="
	OperatorGreaterThan        Operator = ">"
	OperatorGreaterThanOrEqual Operator = ">="
	OperatorLessThan           Operator = "<"
	OperatorLessThanOrEqual    Operator = "<="
	OperatorLike               Operator = "like"
	OperatorIn                 Operator = "in"
)

type Options added in v0.1.0

type Options struct {
	// DisableCache disables response-level caching. SchemaDatasource.Cache
	// returns nil when this is true; plugin-owned sub-fetch caches should pass
	// that nil through to cache.NewTyped/cache.GetOrFetch so they also bypass.
	DisableCache bool

	// Cache configures the SDK-owned in-memory cache.
	//
	// The zero value uses [cache.DefaultMemoryOptions]. The same cache instance
	// is returned by [SchemaDatasource.Cache] for plugin in-handler sub-fetches.
	Cache cache.MemoryOptions

	// TTL configures per-endpoint TTLs. A zero TTL for any individual
	// endpoint disables caching for that endpoint only — useful for
	// endpoints whose responses are time-range dependent (ColumnValues).
	//
	// When Options is passed without a TTL block (TTL == EndpointTTLs{}), the
	// defaults from [DefaultOptions].TTL are applied. To disable a single
	// endpoint while keeping the rest of the defaults, copy DefaultOptions
	// and set the field to zero explicitly.
	TTL EndpointTTLs

	// DefaultScope is applied to every endpoint unless overridden in
	// PerEndpointScope. Defaults to [cache.ScopeUser] for safety.
	DefaultScope cache.Scope

	// PerEndpointScope overrides DefaultScope for specific endpoints, keyed
	// by the RequestType* constants from resource.go (e.g. RequestTypeTables).
	//
	// Plugins use this to relax to ScopeDatasource on endpoints that have
	// been audited and confirmed user-independent — typically Tables/Columns
	// for clusters that do not enforce per-user table visibility.
	PerEndpointScope map[string]cache.Scope

	// Refresh configures the manual cache-bypass header.
	Refresh RefreshPolicy
}

Options tunes SchemaDatasource behaviour. Pass to NewSchemaDatasourceWithOptions to override defaults.

The zero value of Options is NOT "caching off"; it means "use defaults". To disable caching entirely:

NewSchemaDatasourceWithOptions(..., Options{DisableCache: true})

type OrderByColumn added in v0.0.8

type OrderByColumn struct {
	Name string `json:"name"`
	Desc bool   `json:"desc,omitempty"`
}

OrderByColumn specifies a column and sort direction for ORDER BY pushdown.

type Query added in v0.0.6

type Query struct {
	apidata.CommonQueryProperties `json:",inline"`

	Table                string         `json:"table"`
	Filters              []ColumnFilter `json:"filters"`
	TableParameterValues map[string]any `json:"tableParameterValues,omitempty"`
	// TableHintValues carries the per-table hints from FOR (...) clauses.
	// Keys are uppercase hint names, values are the hint arguments (empty for flags).
	TableHintValues map[string]string `json:"tableHintValues,omitempty"`
	GrafanaSql      bool              `json:"grafanaSql"`

	// Pushdown hints — datasources MAY use these to optimize queries.
	// The SQL engine still applies these operations on the result for correctness,
	// so datasources that ignore them produce correct (but potentially slower) results.
	Columns []string        `json:"columns,omitempty"` // SELECT column projection (nil = all columns)
	OrderBy []OrderByColumn `json:"orderBy,omitempty"`
	Limit   *int64          `json:"limit,omitempty"`
}

func (Query) ToSQL added in v0.0.6

func (q Query) ToSQL(dialect SQLDialect) (string, error)

ToSQL builds a fully interpolated SQL SELECT query from a Query using the specified SQL dialect. All filter conditions are ANDed together. Returns an error if a filter uses an unsupported operator.

When Columns is non-empty the SELECT list is set to those columns; otherwise SELECT * is used. OrderBy and Limit are appended when set.

type RefreshPolicy added in v0.1.0

type RefreshPolicy struct {
	// Header is the HTTP header name that triggers a refresh. Empty disables
	// refresh entirely.
	Header string
	// MinInterval is the minimum time between honoured refreshes per
	// (tenant, endpoint). Zero disables rate-limiting.
	MinInterval time.Duration
}

RefreshPolicy controls the manual cache-invalidation header.

When a request carries Header (canonical "X-Schemads-Refresh"), the cached entry for that exact key is deleted and the handler re-runs. Bypass is rate-limited per (tenant, endpoint) to prevent CPU DOS via constant invalidation; requests within MinInterval of the previous bypass for the same (tenant, endpoint) fall back to the cached response.

type SQLDialect added in v0.0.6

type SQLDialect int

SQLDialect identifies the SQL dialect used for query interpolation.

const (
	DialectMySQL SQLDialect = iota
	DialectPostgreSQL
	DialectSQLite
	DialectSQLServer
	DialectClickHouse
	DialectSnowflake
	DialectOracle
)

type Schema

type Schema struct {
	Tables    []Table  `json:"tables,omitempty"`
	Functions []string `json:"functions,omitempty"`
	// TableParameterValues provides pre-populated values for root table parameters only
	// (table name -> table parameter name -> values). Non-root table parameter values
	// depend on ancestor selections and must be fetched incrementally via
	// a "tableParameterValues" request with [TableParameterValuesRequest.DependencyValues].
	TableParameterValues map[string]map[string][]string `json:"tableParameterValues,omitempty"`
	// Capabilities describes datasource-wide defaults for SQL pushdown. Per-table
	// overrides live on [Table.Capabilities].
	Capabilities *DatasourceCapabilities `json:"capabilities,omitempty"`
}

Schema is the complete tabular schema returned by a "fullSchema" request.

type SchemaDatasource

type SchemaDatasource struct {
	// The Schema handlers serve the different schema endpoints. If nil, those requests
	// return 501 Not Implemented.
	SchemaHandler               SchemaHandler
	TablesHandler               TablesHandler
	ColumnsHandler              ColumnsHandler
	TableParameterValuesHandler TableParameterValuesHandler
	ColumnValuesHandler         ColumnValuesHandler

	// The CallResourceHandler is used to forward requests that are not handled by the schema handlers.
	CallResourceHandler backend.CallResourceHandler
	// contains filtered or unexported fields
}

SchemaDatasource is a backend.CallResourceHandler that intercepts requests under BaseResourcePath and delegates them to the configured handlers. All other resource paths are forwarded to CallResourceHandler (if set) or return 404.

Response-level caching is on by default (see DefaultOptions); plugins can tune or disable it with NewSchemaDatasourceWithOptions.

func NewSchemaDatasource

func NewSchemaDatasource(schemaHandler SchemaHandler, tablesHandler TablesHandler, columnsHandler ColumnsHandler, tableParameterValuesHandler TableParameterValuesHandler, columnValuesHandler ColumnValuesHandler, next backend.CallResourceHandler) *SchemaDatasource

NewSchemaDatasource creates a SchemaDatasource with default-on caching. Pass nil for any handler to return 501 for that endpoint. Pass nil for next to return 404 for non-schema paths.

Equivalent to NewSchemaDatasourceWithOptions(..., DefaultOptions).

func NewSchemaDatasourceWithOptions added in v0.1.0

func NewSchemaDatasourceWithOptions(schemaHandler SchemaHandler, tablesHandler TablesHandler, columnsHandler ColumnsHandler, tableParameterValuesHandler TableParameterValuesHandler, columnValuesHandler ColumnValuesHandler, next backend.CallResourceHandler, opts Options) *SchemaDatasource

NewSchemaDatasourceWithOptions is the tuning form of NewSchemaDatasource. Use it to override TTLs, tune the in-memory cache, relax scopes per endpoint, or disable caching entirely:

// Disable caching:
NewSchemaDatasourceWithOptions(..., schemas.Options{DisableCache: true})

// Relax Tables/Columns to ScopeDatasource for a cluster that doesn't
// enforce per-user index visibility:
opts := schemas.DefaultOptions
opts.PerEndpointScope = map[string]cache.Scope{
    schemas.RequestTypeTables:  cache.ScopeDatasource,
    schemas.RequestTypeColumns: cache.ScopeDatasource,
}
NewSchemaDatasourceWithOptions(..., opts)

func (*SchemaDatasource) Cache added in v0.1.0

func (ds *SchemaDatasource) Cache() *cache.MemoryCache

Cache returns the cache instance used by this SchemaDatasource. Plugins use it for in-handler sub-fetches (typically wrapped in cache.Typed) so response and sub-fetch caches share one in-memory store and eviction policy.

Returns nil when caching is disabled.

func (*SchemaDatasource) CallResource

CallResource implements backend.CallResourceHandler. Requests whose path starts with BaseResourcePath are handled internally; everything else is forwarded to CallResourceHandler.

type SchemaHandler

type SchemaHandler interface {
	Schema(ctx context.Context, req *SchemaRequest) (*SchemaResponse, error)
}

type SchemaRequest

type SchemaRequest struct {
	CommonRequest
}

type SchemaResponse

type SchemaResponse struct {
	// FullSchema is populated for "fullSchema" requests.
	FullSchema *Schema `json:"fullSchema,omitempty"`
	Errors     string  `json:"error,omitempty"`
}

SchemaResponse is the JSON body returned by the fullSchema endpoint.

type Table

type Table struct {
	Name            string           `json:"name"`
	TableParameters []TableParameter `json:"tableParameters,omitempty"`
	Columns         []Column         `json:"columns,omitempty"`
	// TableHints lists the per-table execution hints this table supports
	// via FOR (...) clauses in SQL.
	TableHints []TableHint `json:"tableHints,omitempty"`
	// Capabilities describes what SQL operations this table supports natively.
	// When set, consumers SHOULD prefer this over [Schema.Capabilities].
	Capabilities *DatasourceCapabilities `json:"capabilities,omitempty"`
	// Metadata carries optional descriptive information about the table
	// (e.g. Prometheus HELP, SQL TABLE COMMENT).
	Metadata Metadata `json:"metadata,omitzero"`
}

Table describes a single table, its columns, and optional table parameters.

type TableHint added in v0.0.9

type TableHint struct {
	// Name is the hint identifier used in SQL: FOR (name('value')).
	Name string `json:"name"`
	// Description is optional human/LLM-readable documentation.
	Description string `json:"description,omitempty"`
	// HasValue indicates whether the hint takes a string argument.
	// If false, the hint is a flag: FOR (instant). If true: FOR (rate('5m')).
	HasValue bool `json:"hasValue,omitempty"`
	// AffectsSchema indicates the hint can change which columns are returned
	// by columns/columnValues when its value is supplied in SchemaContext.
	AffectsSchema bool `json:"affectsSchema,omitempty"`
}

TableHint describes a per-table execution hint that a datasource supports. Hints are specified via FOR (...) clauses in SQL and control how the datasource backend executes the query for a specific table — e.g. rate('5m'), step('30s'), instant. Unlike table parameters, hints do not appear as columns. Most hints do not affect the table schema; hints with AffectsSchema set may change discoverable columns when their values are supplied via SchemaContext on columns/columnValues requests.

type TableParameter added in v0.0.1

type TableParameter struct {
	Name      string   `json:"name"`
	DependsOn []string `json:"dependsOn,omitempty"`
	Root      bool     `json:"root"`
	Required  bool     `json:"required"`
}

TableParameter describes a hierarchical table parameter within a parent Table. Table parameters model scoping parameters that must be resolved before the table can be queried (e.g. organization -> repository for a GitHub data source). Root table parameters have no dependencies and their values may be pre-populated in [Schema.TableParameterValues]. Non-root table parameter values depend on ancestor selections and must be fetched via a "tableParameterValues" request. See ValidateSchema for the full set of invariants enforced on table parameter definitions.

type TableParameterValuesHandler added in v0.0.1

type TableParameterValuesHandler interface {
	TableParameterValues(ctx context.Context, req *TableParameterValuesRequest) (*TableParametersValuesResponse, error)
}

type TableParameterValuesRequest added in v0.0.1

type TableParameterValuesRequest struct {
	CommonRequest
	Table            string            `json:"table"`
	TableParameter   string            `json:"tableParameter,omitempty"`
	DependencyValues map[string]string `json:"dependencyValues,omitempty"`
}

TableParameterValuesRequest identifies a table and provides dependency context (selected ancestor values) for fetching its enumerable values. Table is required because the same table parameter name may have different semantics across parent tables.

type TableParametersValuesResponse added in v0.0.1

type TableParametersValuesResponse struct {
	TableParameterValues map[string][]string `json:"tableParameterValues"`
	Errors               map[string]string   `json:"errors,omitempty"`
}

type TablesHandler added in v0.0.1

type TablesHandler interface {
	Tables(ctx context.Context, req *TablesRequest) (*TablesResponse, error)
}

type TablesRequest added in v0.0.1

type TablesRequest struct {
	CommonRequest
}

type TablesResponse added in v0.0.1

type TablesResponse struct {
	Tables          []string                    `json:"tables"`
	TableParameters map[string][]TableParameter `json:"tableParameters,omitempty"`
	TableHints      map[string][]TableHint      `json:"tableHints,omitempty"`
	TableMetadata   map[string]Metadata         `json:"tableMetadata,omitempty"`
	// TableCapabilities carries per-table pushdown capabilities keyed by table
	// name. When present for a table, consumers SHOULD prefer these over the
	// datasource-wide Capabilities default.
	TableCapabilities map[string]*DatasourceCapabilities `json:"tableCapabilities,omitempty"`
	// Capabilities describes datasource-wide defaults for SQL pushdown. Per-table
	// overrides live in TableCapabilities.
	Capabilities *DatasourceCapabilities `json:"capabilities,omitempty"`
	Errors       map[string]string       `json:"errors,omitempty"`
}

Directories

Path Synopsis
Package tables encodes and decodes the canonical, human-writable string form of a parameterised table reference used by schemads consumers.
Package tables encodes and decodes the canonical, human-writable string form of a parameterised table reference used by schemads consumers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL