Skip to main content

CatalogMux API

CatalogMux is the recommended way to build hugr app catalogs. It provides a handler-based API for registering functions and tables with automatic GraphQL SDL generation.

For lower-level control, you can register catalog.Table, catalog.ScalarFunction, catalog.TableFunction interfaces directly, or provide raw SDL.

All catalog interfaces are defined in the github.com/hugr-lab/airport-go/catalog package.

Creating a CatalogMux

import "github.com/hugr-lab/query-engine/client/app"

func (a *MyApp) Catalog(ctx context.Context) (catalog.Catalog, error) {
mux := app.New()
// register functions, tables, table functions...
return mux, nil
}

Scalar Functions

Handler-based (simple)

Register with HandleFunc:

err := mux.HandleFunc("default", "add",
func(w *app.Result, r *app.Request) error {
return w.Set(r.Int64("a") + r.Int64("b"))
},
app.Arg("a", app.Int64),
app.Arg("b", app.Int64),
app.Return(app.Int64),
app.Desc("Add two numbers"),
)

Parameters:

  • Schema name ("default" = top-level module, others = nested modules)
  • Function name
  • Handler function func(w *Result, r *Request) error
  • Options: Arg(), ArgDesc(), Return(), Desc()

Direct interface registration

For full control, implement catalog.ScalarFunction and register directly:

mux.ScalarFunc("default", &myScalarFunc{})
// catalog.ScalarFunction interface (from airport-go/catalog)
type ScalarFunction interface {
Name() string
Comment() string
Signature() FunctionSignature
Execute(ctx context.Context, input arrow.RecordBatch) (arrow.Array, error)
}

type FunctionSignature struct {
Parameters []arrow.DataType
ReturnType arrow.DataType
}

With manual SDL

Wrap any scalar function with custom SDL:

mux.ScalarFunc("default", app.WithScalarFuncSDL(&myFunc{}, `
my_func(input: String!): String @function(name: "my_func")
`))

Tables

Direct interface registration

Implement catalog.Table and register:

mux.Table("default", &ItemsTable{})
// catalog.Table interface (from airport-go/catalog)
type Table interface {
Name() string
Comment() string
ArrowSchema(columns []string) *arrow.Schema
Scan(ctx context.Context, opts *ScanOptions) (array.RecordReader, error)
}

SDL is auto-generated from the Arrow schema. Mutable tables get @table, read-only get @view.

Insertable / Updatable / Deletable Tables

To enable GraphQL mutations, implement additional interfaces:

// Enable INSERT mutations
type InsertableTable interface {
Table
Insert(ctx context.Context, rows array.RecordReader, opts *DMLOptions) (*DMLResult, error)
}

// Enable UPDATE mutations
type UpdatableTable interface {
Table
Update(ctx context.Context, rowIDs []int64, rows array.RecordReader, opts *DMLOptions) (*DMLResult, error)
}

// Enable DELETE mutations
type DeletableTable interface {
Table
Delete(ctx context.Context, rowIDs []int64, opts *DMLOptions) (*DMLResult, error)
}

Example:

type OrdersTable struct{}

func (t *OrdersTable) Name() string { return "orders" }
// ... ArrowSchema, Scan ...

func (t *OrdersTable) Insert(ctx context.Context, rows array.RecordReader, opts *catalog.DMLOptions) (*catalog.DMLResult, error) {
// insert logic
return &catalog.DMLResult{AffectedRows: 1}, nil
}

// Register — auto-detected as insertable
mux.Table("default", &OrdersTable{})

Schema Options for Tables

mux.Table("default", &myTable{},
app.WithDescription("My table description"),
app.WithPK("id"),
app.WithFieldDescription("name", "User display name"),
app.WithFilterRequired("tenant_id"),
app.WithReferences("orders", []string{"id"}, []string{"user_id"}, "users", "orders"),
)
OptionDescription
WithDescription(desc)Table description
WithPK(fields...)Primary key fields
WithFieldDescription(field, desc)Field-level description
WithFilterRequired(fields...)Fields required in filters
WithReferences(name, srcFields, refFields, query, refQuery)Add @references relation
WithM2MReferences(...)Many-to-many reference
WithFieldReferences(fieldName, refName, query, refQuery)Field-level reference
WithRawSDL(sdl)Override auto-generated SDL entirely

With manual SDL

mux.Table("default", app.WithSDL(&myTable{}, `
type orders @table(name: "orders") {
id: Int! @pk
customer: String!
total: Float!
created_at: Timestamp
}
`))

Table Functions (Parameterized Views)

Table functions are tables that accept arguments — useful for search, filtering, paginated views.

Handler-based (simple)

err := mux.HandleTableFunc("default", "search",
func(w *app.Result, r *app.Request) error {
query := r.String("query")
for _, item := range items {
if strings.Contains(item.Name, query) {
w.Append(item.ID, item.Name)
}
}
return nil
},
app.Arg("query", app.String),
app.ColPK("id", app.Int64),
app.Col("name", app.String),
app.ColDesc("score", app.Float64, "Relevance score"),
app.ColNullable("details", app.String),
)

Column options:

  • Col(name, type) — regular column
  • ColPK(name, type) — primary key column
  • ColNullable(name, type) — nullable column
  • ColDesc(name, type, description) — column with description

Direct interface registration

mux.TableFunc("default", &myTableFunc{})
// catalog.TableFunction interface (from airport-go/catalog)
type TableFunction interface {
Name() string
Comment() string
Signature() FunctionSignature
SchemaForParameters(ctx context.Context, params []any) (*arrow.Schema, error)
Execute(ctx context.Context, params []any, opts *ScanOptions) (array.RecordReader, error)
}

TableFunctionInOut

For functions that transform input row sets:

mux.TableFuncInOut("default", &myTransformFunc{})
// catalog.TableFunctionInOut interface (from airport-go/catalog)
type TableFunctionInOut interface {
Name() string
Comment() string
Signature() FunctionSignature
SchemaForParameters(ctx context.Context, params []any, inputSchema *arrow.Schema) (*arrow.Schema, error)
Execute(ctx context.Context, params []any, input array.RecordReader, opts *ScanOptions) (array.RecordReader, error)
}

Table References

Table references delegate execution to DuckDB function calls — useful for wrapping existing DuckDB table functions:

mux.TableRef("default", &myTableRef{})
// catalog.TableRef interface (from airport-go/catalog)
type TableRef interface {
Name() string
Comment() string
ArrowSchema() *arrow.Schema
FunctionCalls(ctx context.Context, req *FunctionCallRequest) ([]FunctionCall, error)
}

Manual SDL

Global SDL override

Replace all auto-generated SDL with custom SDL:

mux := app.New()
mux.WithSDL(`
type users @table(name: "users") {
id: Int! @pk
name: String!
email: String!
}

type orders @view(name: "orders") {
id: Int! @pk
user_id: Int!
total: Float!
}
`)

Per-item SDL wrapping

Attach custom SDL to individual items:

// Table with custom SDL
mux.Table("default", app.WithSDL(&myTable{}, `type ... @table(...) { ... }`))

// Table ref with custom SDL
mux.TableRef("default", app.WithTableRefSDL(&myRef{}, `type ... @view(...) { ... }`))

// Scalar function with custom SDL
mux.ScalarFunc("default", app.WithScalarFuncSDL(&myFunc{}, `my_func(...): String @function(...)`))

// Table function with custom SDL
mux.TableFunc("default", app.WithTableFuncSDL(&myTF{}, `type ... @view(...) { ... }`))

Type Constants

Type constantGraphQL typeArrow typeGo type
app.BooleanBooleanBooleanbool
app.Int8IntInt8int8
app.Int16IntInt16int16
app.Int32IntInt32int32
app.Int64BigIntInt64int64
app.Uint8UIntUint8uint8
app.Uint16UIntUint16uint16
app.Uint32UIntUint32uint32
app.Uint64BigUIntUint64uint64
app.Float32FloatFloat32float32
app.Float64FloatFloat64float64
app.StringStringStringstring
app.TimestampDateTimeTimestamp_ustime.Time
app.DateDateDate32time.Time
app.GeometryGeometryGeometryExtensionorb.Geometry

Request Methods

MethodReturnsDescription
r.Bool(name)boolBoolean argument
r.Int8(name)int88-bit integer
r.Int16(name)int1616-bit integer
r.Int32(name)int3232-bit integer
r.Int64(name)int6464-bit integer
r.Uint8(name)uint8Unsigned 8-bit
r.Uint16(name)uint16Unsigned 16-bit
r.Uint32(name)uint32Unsigned 32-bit
r.Uint64(name)uint64Unsigned 64-bit
r.Float32(name)float3232-bit float
r.Float64(name)float6464-bit float
r.String(name)stringString argument
r.Bytes(name)[]byteBinary data
r.Geometry(name)orb.GeometryGeometry value
r.Get(name)anyRaw value (any type)
r.Context()context.ContextRequest context

Schemas (Modules)

The first argument to all registration methods is the schema name:

  • "default" — top-level app module (no extra nesting)
  • Any other name — nested module: { app_name { schema_name { ... } } }
// Top-level: { function { my_app { add(...) } } }
mux.HandleFunc("default", "add", ...)

// Nested: { function { my_app { admin { user_count } } } }
mux.HandleFunc("admin", "user_count", ...)

See Schema Design for details.

GraphQL Query Paths

RegistrationGraphQL Path
HandleFunc("default", "add", ...){ function { app_name { add(...) } } }
HandleFunc("admin", "count", ...){ function { app_name { admin { count } } } }
Table("default", &items{}){ app_name { items { ... } } }
HandleTableFunc("default", "search", ...){ app_name { search(args: {...}) { ... } } }
HandleTableFunc("reports", "daily", ...){ app_name { reports { reports_daily(args: {...}) { ... } } } }