package models import ( "fmt" "sort" "strings" ) // Directive is a dialect-specific instruction embedded in a source schema // (currently DBML) that is preserved losslessly in the intermediate model and // consumed only by the writer for its namespace. Directives are stored in the // Metadata map of the object they apply to, under DirectivesMetadataKey. // // Example DBML: `@postgres: partition by RANGE (created_at)` parses to // Directive{Namespace: "postgres", Key: "partition", Args: "partition by RANGE (created_at)"}. type Directive struct { // Namespace is the dialect the directive targets, e.g. "postgres" or "sqlite". Namespace string `json:"namespace" yaml:"namespace"` // Key is the lowercased first token of Args, used for duplicate detection // and writer dispatch. Key string `json:"key,omitempty" yaml:"key,omitempty"` // Args is the verbatim argument text following the "@namespace:" prefix. Args string `json:"args" yaml:"args"` // Line is the 1-based source line the directive was read from, when known. Line int `json:"line,omitempty" yaml:"line,omitempty"` } // DirectivesMetadataKey is the Metadata map key under which the ordered list of // dialect directives for an object is stored. const DirectivesMetadataKey = "directives" // DirectiveKey derives the Key for a directive from its argument text: the // lowercased first whitespace-delimited token. func DirectiveKey(args string) string { fields := strings.Fields(args) if len(fields) == 0 { return "" } return strings.ToLower(fields[0]) } // AddDirective appends d to the directive list stored in meta. The caller is // responsible for ensuring meta is non-nil (all Init* constructors allocate it). // If d.Key is empty it is derived from d.Args. func AddDirective(meta map[string]any, d Directive) { if meta == nil { return } if d.Key == "" { d.Key = DirectiveKey(d.Args) } existing := GetDirectives(meta) existing = append(existing, d) meta[DirectivesMetadataKey] = existing } // GetDirectives returns the directives stored in meta, sorted deterministically // by (Namespace, Line, Args). It tolerates both a freshly built []Directive and // the []any of map[string]any produced by a JSON/YAML round-trip. func GetDirectives(meta map[string]any) []Directive { if meta == nil { return nil } raw, ok := meta[DirectivesMetadataKey] if !ok || raw == nil { return nil } var out []Directive switch v := raw.(type) { case []Directive: out = append(out, v...) case []any: for _, item := range v { if d, ok := directiveFromAny(item); ok { out = append(out, d) } } } sort.SliceStable(out, func(i, j int) bool { if out[i].Namespace != out[j].Namespace { return out[i].Namespace < out[j].Namespace } if out[i].Line != out[j].Line { return out[i].Line < out[j].Line } return out[i].Args < out[j].Args }) return out } // directiveFromAny decodes a single directive from the loosely typed forms that // survive a JSON or YAML round-trip (map[string]any / map[any]any). func directiveFromAny(item any) (Directive, bool) { switch m := item.(type) { case Directive: return m, true case map[string]any: return directiveFromStringMap(m), true case map[any]any: sm := make(map[string]any, len(m)) for k, val := range m { if ks, ok := k.(string); ok { sm[ks] = val } } return directiveFromStringMap(sm), true } return Directive{}, false } func directiveFromStringMap(m map[string]any) Directive { d := Directive{} if s, ok := m["namespace"].(string); ok { d.Namespace = s } if s, ok := m["key"].(string); ok { d.Key = s } if s, ok := m["args"].(string); ok { d.Args = s } switch n := m["line"].(type) { case int: d.Line = n case int64: d.Line = int(n) case float64: d.Line = int(n) } if d.Key == "" { d.Key = DirectiveKey(d.Args) } return d } // DirectivesForNamespace returns the directives in meta that target ns, in the // deterministic order of GetDirectives. func DirectivesForNamespace(meta map[string]any, ns string) []Directive { all := GetDirectives(meta) if len(all) == 0 { return nil } out := make([]Directive, 0, len(all)) for _, d := range all { if d.Namespace == ns { out = append(out, d) } } return out } // HasDirective reports whether meta contains a directive with the given // namespace and key. func HasDirective(meta map[string]any, ns, key string) bool { for _, d := range GetDirectives(meta) { if d.Namespace == ns && d.Key == key { return true } } return false } // DirectiveSpec describes a documented directive in the catalog. type DirectiveSpec struct { // Singleton means only one directive with this namespace/key may appear at // a single location; a second one is a parse error. Singleton bool // Locations lists the location kinds the directive is valid at // ("database", "table", "column", "index"). Locations []string } // Location kinds a directive may attach to. const ( DirectiveLocationDatabase = "database" DirectiveLocationTable = "table" DirectiveLocationColumn = "column" DirectiveLocationIndex = "index" ) // DirectiveCatalog is the set of documented directives per namespace. It is used // for strict-mode validation in readers and writers; unknown namespaces/keys are // still preserved losslessly when strict mode is off. var DirectiveCatalog = map[string]map[string]DirectiveSpec{ "postgres": { "partition": {Singleton: true, Locations: []string{DirectiveLocationTable}}, "tablespace": {Singleton: true, Locations: []string{DirectiveLocationTable, DirectiveLocationIndex}}, "inherits": {Singleton: true, Locations: []string{DirectiveLocationTable}}, "with": {Singleton: false, Locations: []string{DirectiveLocationTable, DirectiveLocationIndex}}, "storage": {Singleton: true, Locations: []string{DirectiveLocationColumn}}, "compression": {Singleton: true, Locations: []string{DirectiveLocationColumn}}, "identity": {Singleton: true, Locations: []string{DirectiveLocationColumn}}, }, "sqlite": { "without": {Singleton: true, Locations: []string{DirectiveLocationTable}}, "strict": {Singleton: true, Locations: []string{DirectiveLocationTable}}, "collate": {Singleton: true, Locations: []string{DirectiveLocationColumn}}, }, } // LookupDirectiveSpec returns the catalog spec for a namespace/key and whether // it is documented. func LookupDirectiveSpec(ns, key string) (DirectiveSpec, bool) { keys, ok := DirectiveCatalog[ns] if !ok { return DirectiveSpec{}, false } spec, ok := keys[key] return spec, ok } // DirectiveLocationAllowed reports whether a documented directive may appear at // the given location. Unknown directives (not in the catalog) are allowed // everywhere so they can be preserved. func DirectiveLocationAllowed(ns, key, location string) bool { spec, ok := LookupDirectiveSpec(ns, key) if !ok { return true } for _, l := range spec.Locations { if l == location { return true } } return false } // FormatDirectiveLine renders a directive back to its DBML source form, e.g. // "@postgres: partition by RANGE (created_at)" or "@postgres(id): identity always". func FormatDirectiveLine(d Directive, target string) string { if target != "" { return fmt.Sprintf("@%s(%s): %s", d.Namespace, target, d.Args) } return fmt.Sprintf("@%s: %s", d.Namespace, d.Args) }