semantic

package
v0.77.2 Latest Latest
Warning

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

Go to latest
Published: Oct 23, 2026 License: MIT Imports: 5 Imported by: 1

README

semantic

Pure logic — no I/O, no mutation of its inputs — that turns a project's model (Entity, EntityField.Mappings/NamePatterns, QueryDef.Parameters[].Meta) plus a scanned schema into the three things the core-investigation-loop feature's investigation loop needs: which column means which entity field (Resolve), what else is reachable from a selected value (RelatedLookups), and which library queries can already be run with the values on hand (Applicable). Safe to call from datatug-cli's HTTP resolver, the TUI, or a future datatug context verb — this package never talks to a database or a project store itself.

Resolve

Maps physical columns (source + collection + scanned column list) to the entity fields a project's model says they mean, per REQ:field-mapping-model: declared EntityField.Mappings win; absent a declared mapping, EntityField.NamePatterns are tried and the result is labelled inferred; a column matching neither is simply absent from the result.

entities := []*datatug.Entity{
	{
		ProjectItem: datatug.ProjectItem{ProjItemBrief: datatug.ProjItemBrief{ID: "customer"}},
		Fields: datatug.EntityFields{
			{
				ID: "id", Type: "string",
				Mappings: datatug.PhysicalRefs{
					{Source: "chinook", Collection: "Customer", Column: "CustomerId"},
				},
			},
			{
				ID: "email", Type: "string",
				NamePatterns: datatug.StringPatterns{{Type: "exact", Value: "Email"}},
			},
		},
	},
}

results := semantic.Resolve(entities, "chinook", "Customer", []semantic.Column{
	{Name: "CustomerId", Type: "int"},
	{Name: "Email", Type: "string"},
	{Name: "FirstName", Type: "string"},
})
// results == []semantic.Resolution{
//   {Column: "CustomerId", Entity: "customer", Field: "id", Provenance: semantic.Declared},
//   {Column: "Email", Entity: "customer", Field: "email", Provenance: semantic.Inferred, Rule: "namePattern:exact:Email"},
// }
// "FirstName" matches neither a declared mapping nor a name pattern, so it
// has no entry at all.

RelatedLookups

Given a selected SemanticValue (a resolved cell — build one from a Resolution plus the value the cell held), returns every other collection reachable from it: through declared foreign keys in both directions within its own source, and through mappings of the same entity field in other sources/collections, per REQ:related-lookup-model. It only says where to look — the caller (datatug-cli, per REQ:related-lookup-execution) builds and runs the actual filtered query through the access-policy path.

For an outgoing foreign key, the join column is the referenced column the key names (datatug.ForeignKey.RefColumns, paired by position with the selected column), for a single-column and a composite key alike. Only a key without referenced columns (read from a source that does not report them) keeps the older guess: the referenced table's single-column primary key.

schema := map[semantic.SchemaKey]semantic.TableSchema{
	{Source: "chinook", Collection: "Customer"}: {
		PrimaryKey: &datatug.UniqueKey{Name: "PK_Customer", Columns: []string{"CustomerId"}},
		ReferencedBy: datatug.ReferencedBys{
			{
				DBCollectionKey: datatug.NewTableKey("Invoice", "", "", nil),
				ForeignKeys:     []*datatug.RefByForeignKey{{Name: "FK_Invoice_Customer", Columns: []string{"CustomerId"}}},
			},
		},
	},
}

selected := semantic.SemanticValue{
	Entity: "customer", Field: "id", Value: 5,
	Source: "chinook", Collection: "Customer", Column: "CustomerId",
	Provenance: semantic.Declared,
}

lookups := semantic.RelatedLookups(entities, schema, selected)
// lookups == []semantic.Lookup{
//   {Source: "chinook", Collection: "Invoice", Column: "CustomerId", Kind: semantic.LookupReferencedBy, Via: "FK_Invoice_Customer"},
//   {Source: "support-notes", Collection: "Customer", Column: "CustomerId", Kind: semantic.LookupSameField, Via: "field:customer.id"},
// }
// (the second entry assumes entities also declares a mapping of customer.id
// to source "support-notes", collection "Customer")

Applicable

Given the library's queries and the semantic values on hand (the current selection plus the Investigation Context), splits them into queries every required, Meta-tagged parameter of which can be bound, and queries still missing at least one — per REQ:applicable-queries and REQ:parameter-auto-binding. A parameter with no Meta, or an unsatisfied optional one, never blocks applicability.

query := &datatug.QueryDef{
	ProjectItem: datatug.ProjectItem{ProjItemBrief: datatug.ProjItemBrief{ID: "customer-invoices", Title: "Customer invoices"}},
	Type: datatug.QueryTypeSQL,
	Parameters: datatug.Parameters{
		{ID: "customerId", Type: "integer", IsRequired: true, Meta: &datatug.EntityFieldRef{Entity: "Customer", Field: "ID"}},
	},
}

available := []semantic.SemanticValue{
	{Entity: "Customer", Field: "ID", Value: 5, Source: "chinook", Collection: "Customer", Column: "CustomerId", Provenance: semantic.Declared},
}

applicable, notYet := semantic.Applicable([]*datatug.QueryDef{query}, available)
// applicable[0].Bindings  == []semantic.Binding{{Parameter: "customerId", Value: 5, From: available[0]}}
// applicable[0].Chain     == []string{"CustomerId → Customer.ID (declared) → parameter customerId"}
// notYet                  == nil (this query had everything it needed)

LookupSources

Given a parameter that means an entity field, returns the collections its value can be picked from, per the parameter-lookup feature (datatug/backstage spec/features/parameter-lookup, REQ:lookup-source-resolution): every declared mapping of that field (never a NamePatterns inference) whose collection's primary key is exactly that one column, with a key kind (ColumnKeyKind: integer or text affinity) the parameter's type can hold (ParameterAcceptsKey). Each source carries its display columns (DisplayColumns): the entity's display field list when it yields any column in that collection, otherwise up to three inferred ones (other mapped fields, then the schema's first text columns), labelled Inferred.

customer := &datatug.Entity{
	ProjectItem: datatug.ProjectItem{ProjItemBrief: datatug.ProjItemBrief{ID: "Customer"}},
	Display:     []string{"CompanyName"},
	Fields: datatug.EntityFields{
		{ID: "ID", Type: "string", Mappings: datatug.PhysicalRefs{{Source: "northwind", Collection: "Customers", Column: "CustomerID"}}},
		{ID: "CompanyName", Type: "string", Mappings: datatug.PhysicalRefs{{Source: "northwind", Collection: "Customers", Column: "CompanyName"}}},
	},
}
schema := map[semantic.SchemaKey]semantic.TableSchema{
	{Source: "northwind", Collection: "Customers"}: {
		PrimaryKey: &datatug.UniqueKey{Name: "PK_Customers", Columns: []string{"CustomerID"}},
		Columns:    []semantic.Column{{Name: "CustomerID", Type: "nchar(5)"}, {Name: "CompanyName", Type: "nvarchar(40)"}},
	},
}
param := datatug.ParameterDef{ID: "CustomerID", Type: "string", Meta: &datatug.EntityFieldRef{Entity: "Customer", Field: "ID"}}

sources := semantic.LookupSources([]*datatug.Entity{customer}, schema, param, "")
// sources == []semantic.LookupSource{{
//   Source: "northwind", Collection: "Customers", KeyColumn: "CustomerID", KeyKind: semantic.KeyText,
//   Display: []semantic.DisplayColumn{{Column: "CompanyName", Field: "CompanyName", Label: "CompanyName", Provenance: semantic.Declared}},
//   DisplayProvenance: semantic.Declared,
//   Meaning: semantic.ParameterMeaning{Ref: datatug.EntityFieldRef{Entity: "Customer", Field: "ID"}, Provenance: semantic.Declared},
// }}
ResolveParameterMeaning

The field a parameter means (REQ:inferred-parameter-meaning): its declared Meta when set; otherwise the one key field (isKeyField: true) whose NamePatterns match the parameter's name without @, labelled Inferred with the matching pattern in Rule. Patterns match as in Resolve. When key fields of more than one entity field match, the name is ambiguous and there is no meaning (so no lookup), never a guess. LookupSources calls it, and each source's Meaning says which way the meaning was found.

customer.Fields[0].IsKeyField = true
customer.Fields[0].NamePatterns = datatug.StringPatterns{{Type: "regexp", Value: `\w+CustomerID`, CaseSensitive: true}}
meaning, ok := semantic.ResolveParameterMeaning([]*datatug.Entity{customer}, datatug.ParameterDef{ID: "@ShipCustomerID", Type: "string"})
// ok == true
// meaning == semantic.ParameterMeaning{Ref: {Entity: "Customer", Field: "ID"}, Provenance: semantic.Inferred, Rule: `namePattern:regexp:\w+CustomerID`}

It only says where to pick from; building and running the bounded filter query is the runtime's job (the browser SQLite worker for a public fixture, or POST /datatug/semantic/lookup on a server, under its access policy).

Documentation

Overview

Package semantic resolves physical columns (a source + collection + set of scanned columns) to the entity fields a project's model says they mean.

Resolve is pure - no I/O, no mutation - so it can be called from anywhere that already has a project's entities in memory: the datatug-cli HTTP resolver behind GET /datatug/semantic/columns (REQ:semantic-resolution-endpoint in datatug/datatug's core-investigation-loop feature), the TUI, or a future `datatug context` verb. Placement note: the resolver lives here in datatug-core rather than in datatug-cli/pkg/semantic (as sketched in the Phase-1 plan's task 5) because datatug-cli currently vendors its own copy of this package's types and a separate stream is restoring its module dependency; the HTTP endpoint itself is still wired in datatug-cli. This is a planner decision, not a founder ruling - open to being moved once the module dependency lands.

Declared EntityField.Mappings always win over an EntityField.NamePatterns match; NamePatterns are tried only for a column with no declared mapping anywhere in the project, and the result is labelled Inferred. See Resolve for the full tie-break rule when more than one field could claim the same column.

RelatedLookups and Applicable build on the same SemanticValue shape a Resolution plus its cell value produces: the former finds every other collection reachable from a selected value (foreign keys in both directions, plus the same field mapped elsewhere); the latter matches the project's library queries against the values on hand and reports a human-readable resolution chain for each bound parameter.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Applicable added in v0.19.0

func Applicable(queries []*datatug.QueryDef, available []SemanticValue) (applicable []ApplicableQuery, notYet []NotYetApplicable)

Applicable splits queries into those every required, Meta-tagged parameter of which can be bound from available, and those still missing at least one. A parameter with no Meta is never considered - it stays unbound and never blocks applicability, whether required or not. A Meta-tagged optional parameter (IsRequired == false) is bound when available can satisfy it, but its absence never blocks applicability either; only an unsatisfied *required* Meta-tagged parameter lands a query in notYet.

available is order-sensitive: when more than one entry shares the same Entity+Field (e.g. the Investigation Context holds a stale value and the user's current selection holds a newer one), the last entry for that Entity+Field wins - callers should append newer values after older ones.

Pure: no I/O.

func DisplayColumns added in v0.74.1

func DisplayColumns(entity *datatug.Entity, schema TableSchema, key datatug.PhysicalRef) ([]DisplayColumn, Provenance)

DisplayColumns returns the columns a lookup picker shows beside the key of the collection key names (backstage parameter-lookup REQ:display-columns).

Declared: each field ID in entity.Display, in order, that has a declared mapping in key's source and collection becomes a column titled with the field's Title (or its ID); at most six. When that yields nothing, up to three columns are inferred: first the entity's other fields mapped in the same collection (field order), then the schema's first text columns (schema order). The key column is never repeated, and when the schema lists its columns every display column must be one of them. entity must not be nil.

func ParameterAcceptsKey added in v0.74.1

func ParameterAcceptsKey(parameterType string, kind KeyKind) bool

ParameterAcceptsKey reports whether a parameter of parameterType can hold a key of kind: a string parameter takes both kinds (an integer key as decimal text), an integer parameter only integer keys, and no other type takes any.

Types

type ApplicableQuery added in v0.19.0

type ApplicableQuery struct {
	Query    *datatug.QueryDef
	Bindings []Binding
	// Chain is the human-readable resolution chain, one entry per bound
	// parameter: "<column> → <entity>.<field> (<declared|inferred>) →
	// parameter <parameterID>".
	Chain []string
}

ApplicableQuery is a query every required, semantically-tagged parameter of which could be bound from the available values.

type Binding added in v0.19.0

type Binding struct {
	Parameter string
	Value     interface{}
	From      SemanticValue
}

Binding is one query parameter bound to a semantic value.

type Column

type Column struct {
	Name string
	Type string
}

Column is a single physical column to resolve against a project's entities.

type DisplayColumn added in v0.74.1

type DisplayColumn struct {
	// Column is the physical column name in the lookup source's collection.
	Column string
	// Field is the entity field the column comes from; empty for a column
	// inferred from the schema alone.
	Field string
	// Label is the field's title, its ID, or (for a schema-only column) the
	// column name.
	Label string
	// Provenance is Declared for a column listed in Entity.Display and
	// Inferred for the fallback columns.
	Provenance Provenance
}

DisplayColumn is one column shown beside the key in a lookup picker.

type KeyKind added in v0.74.1

type KeyKind string

KeyKind is the kind of value a lookup source's key column holds. Its values match the contract's TypedValue types, so a picked key can be returned as {type: KeyKind, value: ...}.

const (
	// KeyInteger is a key column with integer affinity.
	KeyInteger KeyKind = "integer"
	// KeyText is a key column with text affinity.
	KeyText KeyKind = "string"
)

func ColumnKeyKind added in v0.74.1

func ColumnKeyKind(columnType string) KeyKind

ColumnKeyKind classifies a declared column type by SQLite-style affinity: a type containing "INT" is an integer key, one containing "CHAR", "CLOB" or "TEXT" (or the DataTug type "string") is a text key. Any other type - decimal, real, date, blob - cannot be a lookup key and yields "".

type Lookup added in v0.19.0

type Lookup struct {
	Source     string
	Collection string
	Column     string
	Kind       LookupKind
	Via        string
}

Lookup is one related-record path reachable from a SemanticValue: the caller filters Lookup.Collection (in Lookup.Source) where Lookup.Column equals the selected value - RelatedLookups itself performs no such filter, it only says where to look (REQ:related-lookup-execution builds and runs that query server-side).

func RelatedLookups added in v0.19.0

func RelatedLookups(entities []*datatug.Entity, schemaByCollection map[SchemaKey]TableSchema, selected SemanticValue) []Lookup

RelatedLookups returns every other collection reachable from a selected semantic value: through declared foreign keys in both directions within selected's own source (schemaByCollection must carry that source's scanned schema for this to find anything), and through mappings of the same entity field (selected.Entity + selected.Field) in other sources or collections. A column that is neither part of a foreign-key relationship nor mapped anywhere else yields nothing.

A datatug.ForeignKey names the columns of the referenced table it points to (RefColumns, in the order of its own Columns) when its source reported them, and RelatedLookups uses them: for LookupForeignKey the join column is the referenced column that pairs with selected.Column, for a single-column and a composite key alike. A key without them (read from a source that does not report the referenced column, or stored before projects kept them) does not say which column it targets, so two heuristics fill that gap, both using the referenced table's primary key as the assumed join target:

  • LookupForeignKey: selected.Column matches one of the current table's own ForeignKey.Columns: this reports RefTable's primary key (its first column, when the schema for RefTable is known and single-column-keyed; the selected column's own name otherwise) as the join column.
  • LookupReferencedBy: selected.Column must be part of the current table's own primary key for a "referenced by" lookup to fire at all, since that is this model's only signal that an incoming foreign key targets this exact column.

Order is deterministic: lookups in selected's own source come first, ordered by target Collection name (ties broken by Kind); then cross-source SameField lookups, ordered by Source then Collection name. Pure: no I/O.

type LookupKind added in v0.19.0

type LookupKind string

LookupKind identifies how a Lookup was derived.

const (
	// LookupForeignKey means the selected value's own column is a foreign
	// key, pointing at Lookup.Collection.
	LookupForeignKey LookupKind = "foreignKey"
	// LookupReferencedBy means Lookup.Collection has a foreign key pointing
	// back at the selected value's table.
	LookupReferencedBy LookupKind = "referencedBy"
	// LookupSameField means Lookup.Collection maps the same semantic field
	// (the selected value's Entity + Field) in a different source or
	// collection.
	LookupSameField LookupKind = "sameField"
)

type LookupSource added in v0.74.1

type LookupSource struct {
	Source     string
	Collection string
	KeyColumn  string
	KeyKind    KeyKind
	Display    []DisplayColumn
	// DisplayProvenance is Declared when Display came from Entity.Display and
	// Inferred when it was guessed (the picker must say so). It is Declared
	// for an empty Display, since nothing was guessed.
	DisplayProvenance Provenance
	// Meaning is the parameter meaning the source was resolved through: the
	// parameter's declared Meta, or a key field inferred from its name
	// (ResolveParameterMeaning). The picker must label an Inferred meaning.
	Meaning ParameterMeaning
}

LookupSource is one collection a parameter's value can be picked from: its single-column primary key is declared to carry the parameter's meaning.

func LookupSources added in v0.74.1

func LookupSources(entities []*datatug.Entity, schemaByCollection map[SchemaKey]TableSchema, parameter datatug.ParameterDef, hostSource string) []LookupSource

LookupSources returns the collections the value of parameter can be picked from (backstage parameter-lookup REQ:lookup-source-resolution): every declared mapping (EntityField.Mappings, never a NamePatterns inference) of the field the parameter means (ResolveParameterMeaning: its declared Meta, else the one key field whose NamePatterns match its name), whose collection's schema in schemaByCollection has a primary key of exactly one column equal to the mapped column, with a key kind (ColumnKeyKind of that column's type in TableSchema.Columns, or of the entity field's type when the schema lists no columns) that parameter's type can hold (ParameterAcceptsKey).

A parameter with no meaning, a multi-value parameter, and a parameter of type "record" or "recordset" have no lookup sources. A collection whose schema is unknown is not a source.

Order is deterministic: sources in hostSource first (pass "" when the host has no source of its own), then by Source, then by Collection.

Pure: no I/O, no mutation of its inputs.

type NotYetApplicable added in v0.19.0

type NotYetApplicable struct {
	Query *datatug.QueryDef
	// Missing lists each unsatisfied required parameter's semantic field, as
	// "<entity>.<field>".
	Missing []string
}

NotYetApplicable is a query with at least one required, semantically-tagged parameter the available values could not satisfy.

type ParameterMeaning added in v0.74.1

type ParameterMeaning struct {
	// Ref is the entity field the parameter means.
	Ref datatug.EntityFieldRef
	// Provenance is Declared when Ref is the parameter's own Meta and
	// Inferred when it came from a key field's NamePatterns.
	Provenance Provenance
	// Rule names the pattern that matched, for an Inferred meaning, in the
	// form semantic.Resolve uses ("namePattern:regexp:\\w+CustomerID").
	Rule string
}

ParameterMeaning is the meaning a parameter's lookup resolves through (backstage parameter-lookup REQ:inferred-parameter-meaning).

func ResolveParameterMeaning added in v0.74.1

func ResolveParameterMeaning(entities []*datatug.Entity, parameter datatug.ParameterDef) (meaning ParameterMeaning, ok bool)

ResolveParameterMeaning returns the meaning of parameter: its declared Meta when set (Declared, even when no entity declares that field; a Meta with a blank entity or field gives no meaning and no inference), else the one key field (EntityField.IsKeyField) whose NamePatterns match the parameter's ID without a leading "@" (Inferred). Patterns are matched as semantic.Resolve matches column names.

ok is false when the parameter has no Meta and no key field matches, or when key fields of more than one entity field match: an ambiguous name gives no meaning, never a guess between entities. A pattern that cannot be evaluated (an invalid regexp, an unknown type) never matches.

Pure: no I/O, no mutation of its inputs.

type Provenance

type Provenance string

Provenance identifies how a Resolution was determined.

const (
	// Declared means the column matched an EntityField's declared Mappings.
	Declared Provenance = "declared"
	// Inferred means the column matched an EntityField's NamePatterns, with
	// no declared mapping present for it.
	Inferred Provenance = "inferred"
)

type Resolution

type Resolution struct {
	Column     string
	Entity     string
	Field      string
	Provenance Provenance
	// Rule documents how the winner was chosen: which name pattern matched
	// (for Inferred), and/or a tie-break note when more than one field
	// resolved the same column at the same provenance level.
	Rule string
	// Err is set only when the column had NamePatterns candidates that could
	// not be evaluated (e.g. an invalid regexp) and neither a declared
	// mapping nor a valid inferred candidate resolved it. When Err is set,
	// Entity, Field, Provenance and Rule are zero.
	Err error
}

Resolution is what one Column resolved to.

func Resolve

func Resolve(entities []*datatug.Entity, source, collection string, columns []Column) []Resolution

Resolve returns, for each Column, the entity field it maps to in the given source/collection (table): declared EntityField.Mappings win; absent a declared mapping, EntityField.NamePatterns are tried; a column matching neither is simply absent from the result (see REQ:field-mapping-model in datatug/datatug's core-investigation-loop feature).

When more than one field resolves the same column at the same provenance level, the field belonging to the lowest Entity.ID wins (Entity.ID, then EntityField.ID, ascending) and the tie is recorded in the winning Resolution's Rule. This keeps Resolve deterministic without requiring callers to pre-sort entities.

Resolve is pure: it performs no I/O and does not mutate entities. It never panics - a pattern that fails to evaluate (e.g. invalid regexp syntax) yields a Resolution with Err set instead, unless a declared mapping or another, valid pattern already resolved the column.

type SchemaKey added in v0.19.0

type SchemaKey struct {
	Source     string
	Collection string
}

SchemaKey identifies one collection (table or view) within one source.

type SemanticValue added in v0.19.0

type SemanticValue struct {
	Entity     string
	Field      string
	Value      interface{}
	Source     string
	Collection string
	Column     string
	Provenance Provenance
}

SemanticValue is one resolved cell: a value in a physical column, and the semantic field a resolver (see Resolve) said it means. Provenance is a deliberate addition beyond the field-mapping-model REQ's own sketch of this type: it is needed to render the "(declared)"/"(inferred)" segment of an Applicable resolution Chain (REQ:applicable-queries), so a caller can build one SemanticValue directly from a Resolution and pass it to both RelatedLookups and Applicable.

type TableSchema added in v0.19.0

type TableSchema struct {
	PrimaryKey   *datatug.UniqueKey
	ForeignKeys  datatug.ForeignKeys
	ReferencedBy datatug.ReferencedBys
	Columns      []Column
}

TableSchema is the subset of a scanned table/view's metadata RelatedLookups and LookupSources need: its primary key, its foreign-key graph in both directions (datatug.ForeignKeys/datatug.ReferencedBys, as scanned by the DB driver) and, optionally, its columns in schema order with their declared types (LookupSources uses them for the key's kind and the display columns).

Jump to

Keyboard shortcuts

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