Family Expansions

This commit is contained in:
2026-04-13 23:49:59 +02:00
parent 129602a482
commit 0c63dfc711
8 changed files with 722 additions and 374 deletions
+220 -245
View File
@@ -553,6 +553,27 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
if err != nil {
return nativeCollectedDataset{}, err
}
modulePaths, err := collectModulePaths(dataset.ModulesDir)
if err != nil {
return nativeCollectedDataset{}, err
}
moduleData := make([]nativeGeneratedModule, 0, len(modulePaths))
for _, path := range modulePaths {
obj, err := loadJSONObject(path)
if err != nil {
return nativeCollectedDataset{}, err
}
moduleData = append(moduleData, nativeGeneratedModule{
Name: path,
Data: obj,
})
}
for _, module := range moduleData {
columns, err = extendColumns(columns, module.Data, dataset.Name, module.Name)
if err != nil {
return nativeCollectedDataset{}, err
}
}
rawBaseRows, ok := baseData["rows"].([]any)
if !ok {
return nativeCollectedDataset{}, fmt.Errorf("dataset %s: rows must be an array", dataset.Name)
@@ -804,26 +825,35 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
if err != nil {
return nativeCollectedDataset{}, err
}
for _, module := range generatedModules {
updatedColumns, err := extendColumns(columns, module.Data, dataset.Name, module.Name)
if err != nil {
return nativeCollectedDataset{}, err
}
if len(updatedColumns) != len(columns) {
columns = updatedColumns
ensureRowsExposeColumns(rows, columns)
}
}
for _, module := range generatedModules {
if err := applyModuleData(module.Name, module.Data, false); err != nil {
return nativeCollectedDataset{}, err
}
}
modulePaths, err := collectModulePaths(dataset.ModulesDir)
if err != nil {
return nativeCollectedDataset{}, err
for _, module := range moduleData {
if err := applyModuleData(module.Name, module.Data, true); err != nil {
return nativeCollectedDataset{}, err
}
}
for _, path := range modulePaths {
moduleData, err := loadJSONObject(path)
if err != nil {
return nativeCollectedDataset{}, err
for _, module := range generatedModules {
if !module.ApplyAfterModules {
continue
}
if err := applyModuleData(path, moduleData, true); err != nil {
if err := applyModuleData(module.Name, module.Data, false); err != nil {
return nativeCollectedDataset{}, err
}
}
if dataset.Name == "spells" {
normalizeCollectedSpellImpactScripts(rows, baseRowKeys)
}
@@ -871,8 +901,9 @@ func dedupeCollectedRows(rows []map[string]any) []map[string]any {
}
type nativeGeneratedModule struct {
Name string
Data map[string]any
Name string
Data map[string]any
ApplyAfterModules bool
}
func collectGeneratedModules(dataset nativeDataset, lockData map[string]int) ([]nativeGeneratedModule, error) {
@@ -887,37 +918,44 @@ func collectGeneratedModules(dataset nativeDataset, lockData map[string]int) ([]
if err != nil {
return nil, err
}
modules := make([]nativeGeneratedModule, 0, len(paths))
type generatedSource struct {
path string
obj map[string]any
}
sources := make([]generatedSource, 0, len(paths))
for _, path := range paths {
obj, err := loadJSONObject(path)
if err != nil {
return nil, err
}
family, _ := obj["family"].(string)
switch family {
case "skill_focus",
"greater_skill_focus",
"weapon_focus",
"weapon_specialization",
"greater_weapon_focus",
"greater_weapon_specialization",
"improved_critical",
"overwhelming_critical",
"proficiencies",
"special_attacks":
moduleData, err := buildFeatGeneratedModule(path, obj, ctx)
if isFamilyExpansionObject(obj) {
spec, err := parseFamilyExpansionSpec(path, obj)
if err != nil {
return nil, err
}
modules = append(modules, nativeGeneratedModule{
Name: path,
Data: moduleData,
})
case "":
return nil, fmt.Errorf("dataset %s: generated file %s is missing family", dataset.Name, path)
default:
return nil, fmt.Errorf("dataset %s: generated file %s uses unsupported family %q", dataset.Name, path, family)
ctx.familySpecs[spec.FamilyKey] = spec
}
sources = append(sources, generatedSource{path: path, obj: obj})
}
modules := make([]nativeGeneratedModule, 0, len(paths))
for _, source := range sources {
applyAfterModules := false
if isFamilyExpansionObject(source.obj) {
spec, err := parseFamilyExpansionSpec(source.path, source.obj)
if err != nil {
return nil, err
}
applyAfterModules = spec.ApplyAfterModules
}
moduleData, err := buildFeatGeneratedModule(source.path, source.obj, ctx)
if err != nil {
return nil, err
}
modules = append(modules, nativeGeneratedModule{
Name: source.path,
Data: moduleData,
ApplyAfterModules: applyAfterModules,
})
}
racialtypesModule, err := buildRacialtypesSkillAffinityModule(ctx)
if err != nil {
@@ -941,76 +979,16 @@ type featGeneratedContext struct {
existingFeat map[string]struct{}
datasets map[string]nativeDataset
rowsByDataset map[string]map[string]map[string]any
familySpecs map[string]familyExpansionSpec
}
type compactSourceOverride = map[string]map[string]any
type featWeaponFamilySpec struct {
FamilyKey string
SourceColumn string
LabelPrefix string
ConstantPrefix string
NamePrefix string
AutoPrereqFamily1 string
AutoPrereqFamily2 string
}
type affinityGrant struct {
featKey string
races []string
}
var featWeaponFamilies = map[string]featWeaponFamilySpec{
"weapon_focus": {
SourceColumn: "WeaponFocusFeat",
FamilyKey: "weaponfocus",
LabelPrefix: "FEAT_WEAPON_FOCUS",
ConstantPrefix: "FEAT_WEAPON_FOCUS",
NamePrefix: "Weapon Focus",
},
"weapon_specialization": {
SourceColumn: "WeaponSpecializationFeat",
FamilyKey: "weaponspecialization",
LabelPrefix: "FEAT_WEAPON_SPECIALIZATION",
ConstantPrefix: "FEAT_WEAPON_SPECIALIZATION",
NamePrefix: "Weapon Specialization",
AutoPrereqFamily1: "weaponfocus",
},
"greater_weapon_focus": {
SourceColumn: "EpicWeaponFocusFeat",
FamilyKey: "greaterweaponfocus",
LabelPrefix: "FEAT_GREATER_WEAPON_FOCUS",
ConstantPrefix: "FEAT_GREATER_WEAPON_FOCUS",
NamePrefix: "Greater Weapon Focus",
AutoPrereqFamily1: "weaponfocus",
},
"greater_weapon_specialization": {
SourceColumn: "EpicWeaponSpecializationFeat",
FamilyKey: "greaterweaponspecialization",
LabelPrefix: "FEAT_GREATER_WEAPON_SPECIALIZATION",
ConstantPrefix: "FEAT_GREATER_WEAPON_SPECIALIZATION",
NamePrefix: "Greater Weapon Specialization",
AutoPrereqFamily1: "weaponspecialization",
AutoPrereqFamily2: "greaterweaponfocus",
},
"improved_critical": {
SourceColumn: "WeaponImprovedCriticalFeat",
FamilyKey: "improvedcritical",
LabelPrefix: "FEAT_IMPROVED_CRITICAL",
ConstantPrefix: "FEAT_IMPROVED_CRITICAL",
NamePrefix: "Improved Critical",
AutoPrereqFamily1: "weaponfocus",
},
"overwhelming_critical": {
SourceColumn: "EpicWeaponOverwhelmingCriticalFeat",
FamilyKey: "overwhelmingcritical",
LabelPrefix: "FEAT_EPIC_OVERWHELMING_CRITICAL",
ConstantPrefix: "FEAT_EPIC_OVERWHELMING_CRITICAL",
NamePrefix: "Overwhelming Critical",
AutoPrereqFamily2: "improvedcritical",
},
}
func newFeatGeneratedContext(dataset nativeDataset, lockData map[string]int) (*featGeneratedContext, error) {
sourceDir := filepath.Dir(filepath.Dir(dataset.RootPath))
dataDir := filepath.Join(sourceDir, "data")
@@ -1051,6 +1029,7 @@ func newFeatGeneratedContext(dataset nativeDataset, lockData map[string]int) (*f
existingFeat: existingFeat,
datasets: datasetMap,
rowsByDataset: map[string]map[string]map[string]any{},
familySpecs: map[string]familyExpansionSpec{},
}, nil
}
@@ -1235,20 +1214,17 @@ func betterExpandedKey(a, b string) string {
func buildFeatGeneratedModule(path string, obj map[string]any, ctx *featGeneratedContext) (map[string]any, error) {
family, _ := obj["family"].(string)
if strings.TrimSpace(family) == "" {
return nil, fmt.Errorf("generated feat file %s is missing family", path)
}
if isFamilyExpansionObject(obj) {
return buildFamilyExpansionGeneratedModule(path, obj, ctx)
}
switch family {
case "skill_focus", "greater_skill_focus":
return buildSkillFocusGeneratedModule(path, obj, ctx)
case "weapon_focus",
"weapon_specialization",
"greater_weapon_focus",
"greater_weapon_specialization",
"improved_critical",
"overwhelming_critical":
return buildWeaponFamilyGeneratedModule(path, obj, ctx)
case "proficiencies", "special_attacks":
case "special_attacks":
return buildLegacyFeatGeneratedModule(path, obj)
default:
return nil, fmt.Errorf("generated feat file %s uses unsupported family %q", path, family)
return buildLegacyFeatGeneratedModule(path, obj)
}
}
@@ -1274,7 +1250,7 @@ func buildLegacyFeatGeneratedModule(path string, obj map[string]any) (map[string
return module, nil
}
func buildSkillFocusGeneratedModule(path string, obj map[string]any, ctx *featGeneratedContext) (map[string]any, error) {
func buildFamilyExpansionGeneratedModule(path string, obj map[string]any, ctx *featGeneratedContext) (map[string]any, error) {
spec, err := parseFamilyExpansionSpec(path, obj)
if err != nil {
return nil, err
@@ -1283,42 +1259,33 @@ func buildSkillFocusGeneratedModule(path string, obj map[string]any, ctx *featGe
if err != nil {
return nil, fmt.Errorf("generated feat file %s: %w", path, err)
}
if spec.ChildSource.Dataset != "skills" {
return nil, fmt.Errorf("generated feat file %s: child_source.dataset must be skills", path)
}
skillRows, err := ctx.datasetRows("skills")
sourceRows, err := ctx.datasetRows(spec.ChildSource.Dataset)
if err != nil {
return nil, err
}
labelPrefix := "FEAT_SKILL_FOCUS"
namePrefix := "Skill Focus"
if spec.Family == "greater_skill_focus" {
labelPrefix = "FEAT_GREATER_SKILL_FOCUS"
namePrefix = "Greater Skill Focus"
}
overrideList := make([]any, 0)
familyAllowlist := ctx.familyHasExistingRows(spec.FamilyKey)
for _, skillKey := range sortedStringMapKeys(skillRows) {
row := skillRows[skillKey]
if spec.ChildSource.Predicate == "accessible" && isHiddenSkill(row) {
continue
}
slug := strings.TrimPrefix(skillKey, "skills:")
featKey := ctx.preferredGeneratedFeatKey(spec.FamilyKey, slug)
childToken := childTokenFromExpandedKey(featKey, spec.FamilyKey)
labelSuffix := upperSnake(displayNameForSkill(row))
rowID, hasID, err := ctx.featID(featKey, nil)
familyAllowlist := spec.AllowExistingOnly && ctx.familyHasExistingRows(spec.FamilyKey)
for _, sourceKey := range sortedStringMapKeys(sourceRows) {
row := sourceRows[sourceKey]
include, err := familyExpansionSourceEligible(spec, row)
if err != nil {
return nil, fmt.Errorf("generated feat file %s: %w", path, err)
}
if !include {
continue
}
slug := strings.TrimPrefix(sourceKey, spec.ChildSource.Dataset+":")
featKey, rowID, hasID, err := ctx.resolveGeneratedFeatIdentity(spec, slug, row)
if err != nil {
return nil, fmt.Errorf("generated feat file %s: %w", path, err)
}
childToken := childTokenFromExpandedKey(featKey, spec.FamilyKey)
displayName := displayNameForGeneratedSource(spec.ChildSource.Dataset, row)
labelSuffix := upperSnake(displayName)
override := map[string]any{
"key": featKey,
"MASTERFEAT": map[string]any{"id": spec.Template},
"REQSKILL": map[string]any{"id": skillKey},
"TOOLSCATEGORIES": "2",
"meta": familyMetadata(spec.FamilyKey, childToken, skillKey, spec.Template),
"key": featKey,
"MASTERFEAT": map[string]any{"id": spec.Template},
"meta": familyMetadata(spec.FamilyKey, childToken, sourceKey, spec.Template),
}
if hasID {
override["id"] = rowID
@@ -1326,105 +1293,37 @@ func buildSkillFocusGeneratedModule(path string, obj map[string]any, ctx *featGe
if familyAllowlist && !ctx.featKeyExists(featKey) {
continue
}
if !ctx.featKeyExists(featKey) {
override["LABEL"] = labelPrefix + "_" + labelSuffix
override["FEAT"] = map[string]any{
"tlk": map[string]any{
"key": featKey + ".name",
"text": fmt.Sprintf("%s (%s)", namePrefix, displayNameForSkill(row)),
},
if spec.ChildRefField != "" {
override[spec.ChildRefField] = map[string]any{"id": sourceKey}
}
for field, prereqFamily := range spec.AutoPrereqFields {
if prereqKey, ok := generatedFamilyPrereqKey(ctx, row, slug, prereqFamily); ok {
override[field] = map[string]any{"id": prereqKey}
}
override["DESCRIPTION"] = map[string]any{"field": "DESCRIPTION", "ref": spec.Template}
override["ICON"] = map[string]any{"field": "ICON", "ref": spec.Template}
override["CRValue"] = map[string]any{"field": "CRValue", "ref": spec.Template}
override["ReqSkillMinRanks"] = map[string]any{"field": "ReqSkillMinRanks", "ref": spec.Template}
override["Constant"] = labelPrefix + "_" + labelSuffix
override["TOOLSCATEGORIES"] = "2"
override["ALLCLASSESCANUSE"] = map[string]any{"field": "ALLCLASSESCANUSE", "ref": spec.Template}
override["PreReqEpic"] = map[string]any{"field": "PreReqEpic", "ref": spec.Template}
override["ReqAction"] = map[string]any{"field": "ReqAction", "ref": spec.Template}
}
mergeGeneratedOverride(override, overrides[skillKey])
overrideList = append(overrideList, override)
}
return map[string]any{"overrides": overrideList}, nil
}
func buildWeaponFamilyGeneratedModule(path string, obj map[string]any, ctx *featGeneratedContext) (map[string]any, error) {
fileSpec, err := parseFamilyExpansionSpec(path, obj)
if err != nil {
return nil, err
}
spec, ok := featWeaponFamilies[fileSpec.Family]
if !ok {
return nil, fmt.Errorf("generated feat file %s uses unsupported weapon family %q", path, fileSpec.Family)
}
if fileSpec.ChildSource.Dataset != "baseitems" {
return nil, fmt.Errorf("generated feat file %s: child_source.dataset must be baseitems", path)
}
if fileSpec.ChildSource.Column == "" {
return nil, fmt.Errorf("generated feat file %s: child_source.column must be set", path)
}
overrides, err := parseCompactSourceOverrides(obj["overrides"])
if err != nil {
return nil, fmt.Errorf("generated feat file %s: %w", path, err)
}
baseitemRows, err := ctx.datasetRows("baseitems")
if err != nil {
return nil, err
}
overrideList := make([]any, 0)
for _, baseitemKey := range sortedStringMapKeys(baseitemRows) {
row := baseitemRows[baseitemKey]
rawID, ok := lookupField(row, fileSpec.ChildSource.Column)
if !ok || isNullishValue(rawID) {
continue
}
slug := strings.TrimPrefix(baseitemKey, "baseitems:")
featKey, rowID, hasID, err := ctx.resolveWeaponFeatIdentity(fileSpec.FamilyKey, slug, rawID)
if err != nil {
return nil, fmt.Errorf("generated feat file %s: %w", path, err)
}
childToken := childTokenFromExpandedKey(featKey, fileSpec.FamilyKey)
labelSuffix := upperSnake(displayNameForBaseitem(row))
override := map[string]any{
"key": featKey,
"MASTERFEAT": map[string]any{"id": fileSpec.Template},
"TOOLSCATEGORIES": "1",
"meta": familyMetadata(fileSpec.FamilyKey, childToken, baseitemKey, fileSpec.Template),
}
if hasID {
override["id"] = rowID
}
if !ctx.featKeyExists(featKey) {
override["LABEL"] = spec.LabelPrefix + "_" + labelSuffix
override["FEAT"] = map[string]any{
"tlk": map[string]any{
"key": featKey + ".name",
"text": fmt.Sprintf("%s (%s)", spec.NamePrefix, displayNameForBaseitem(row)),
},
if spec.LabelPrefix != "" {
override["LABEL"] = spec.LabelPrefix + "_" + labelSuffix
}
if spec.NamePrefix != "" {
override["FEAT"] = map[string]any{
"tlk": map[string]any{
"key": featKey + ".name",
"text": fmt.Sprintf("%s (%s)", spec.NamePrefix, displayName),
},
}
}
for _, field := range spec.TemplateFields {
override[field] = map[string]any{"field": field, "ref": spec.Template}
}
if spec.ConstantPrefix != "" {
override["Constant"] = spec.ConstantPrefix + "_" + labelSuffix
}
override["DESCRIPTION"] = map[string]any{"field": "DESCRIPTION", "ref": fileSpec.Template}
override["ICON"] = map[string]any{"field": "ICON", "ref": fileSpec.Template}
override["MINATTACKBONUS"] = map[string]any{"field": "MINATTACKBONUS", "ref": fileSpec.Template}
override["ALLCLASSESCANUSE"] = map[string]any{"field": "ALLCLASSESCANUSE", "ref": fileSpec.Template}
override["PreReqEpic"] = map[string]any{"field": "PreReqEpic", "ref": fileSpec.Template}
override["ReqAction"] = map[string]any{"field": "ReqAction", "ref": fileSpec.Template}
override["TOOLSCATEGORIES"] = "1"
override["CRValue"] = map[string]any{"field": "CRValue", "ref": fileSpec.Template}
override["MinLevel"] = map[string]any{"field": "MinLevel", "ref": fileSpec.Template}
override["MinLevelClass"] = map[string]any{"field": "MinLevelClass", "ref": fileSpec.Template}
override["MINSTR"] = map[string]any{"field": "MINSTR", "ref": fileSpec.Template}
override["Constant"] = spec.ConstantPrefix + "_" + labelSuffix
}
if prereqKey, ok := weaponFamilyPrereqKey(ctx, row, slug, spec.AutoPrereqFamily1); ok {
override["PREREQFEAT1"] = map[string]any{"id": prereqKey}
for field, value := range spec.DefaultFields {
override[field] = value
}
if prereqKey, ok := weaponFamilyPrereqKey(ctx, row, slug, spec.AutoPrereqFamily2); ok {
override["PREREQFEAT2"] = map[string]any{"id": prereqKey}
}
mergeGeneratedOverride(override, overrides[baseitemKey])
mergeGeneratedOverride(override, overrides[sourceKey])
overrideList = append(overrideList, override)
}
return map[string]any{"overrides": overrideList}, nil
@@ -1668,6 +1567,29 @@ func displayNameForBaseitem(row map[string]any) string {
return "Unknown Weapon"
}
func displayNameForGeneratedSource(dataset string, row map[string]any) string {
switch dataset {
case "skills":
return displayNameForSkill(row)
case "baseitems":
return displayNameForBaseitem(row)
default:
if text, ok := displayTextFromValue(row["Name"]); ok {
return text
}
if label, ok := row["Label"].(string); ok && strings.TrimSpace(label) != "" {
return normalizeDisplayName(strings.TrimSpace(label))
}
if label, ok := row["label"].(string); ok && strings.TrimSpace(label) != "" {
return normalizeDisplayName(strings.TrimSpace(label))
}
if key, ok := row["key"].(string); ok {
return displayNameFromSlug(strings.TrimPrefix(key, dataset+":"))
}
return "Unknown Entry"
}
}
func displayNameForRaceFile(obj map[string]any) string {
core, ok := obj["core"].(map[string]any)
if ok {
@@ -1804,7 +1726,7 @@ func featSourceID(value any) (int, error) {
}
}
func (c *featGeneratedContext) resolveWeaponFeatIdentity(familyKey, slug string, rawSource any) (string, int, bool, error) {
func (c *featGeneratedContext) resolveGeneratedFeatIdentityBySource(familyKey, slug string, rawSource any) (string, int, bool, error) {
if rawSource != nil && rawSource != nullValue {
if parsedID, err := featSourceID(rawSource); err == nil && parsedID > 0 {
if existingKey, ok := c.preferredFeatKeyForID(familyKey, parsedID); ok {
@@ -1823,34 +1745,51 @@ func (c *featGeneratedContext) resolveWeaponFeatIdentity(familyKey, slug string,
return featKey, rowID, hasID, err
}
func weaponFamilyPrereqKey(ctx *featGeneratedContext, baseitemRow map[string]any, slug, familyKey string) (string, bool) {
func (c *featGeneratedContext) resolveGeneratedFeatIdentity(spec familyExpansionSpec, slug string, sourceRow map[string]any) (string, int, bool, error) {
if spec.IdentitySource == "child_source_value" {
rawSource, ok := lookupField(sourceRow, spec.ChildSource.Column)
if !ok {
rawSource = nil
}
return c.resolveGeneratedFeatIdentityBySource(spec.FamilyKey, slug, rawSource)
}
featKey := c.preferredGeneratedFeatKey(spec.FamilyKey, slug)
rowID, hasID, err := c.featID(featKey, nil)
return featKey, rowID, hasID, err
}
func familyExpansionSourceEligible(spec familyExpansionSpec, row map[string]any) (bool, error) {
if spec.ChildSource.Column != "" {
rawValue, ok := lookupField(row, spec.ChildSource.Column)
if !ok || isNullishValue(rawValue) {
return false, nil
}
}
switch spec.ChildSource.Predicate {
case "":
return true, nil
case "accessible":
return !isHiddenSkill(row), nil
default:
return false, fmt.Errorf("unsupported child_source.predicate %q", spec.ChildSource.Predicate)
}
}
func generatedFamilyPrereqKey(ctx *featGeneratedContext, sourceRow map[string]any, slug, familyKey string) (string, bool) {
if familyKey == "" {
return "", false
}
column := weaponFamilySourceColumn(familyKey)
if column == "" {
spec, ok := ctx.familySpecs[familyKey]
if !ok {
return "", false
}
rawSource, ok := lookupField(baseitemRow, column)
if !ok || isNullishValue(rawSource) {
return "", false
}
featKey, _, _, err := ctx.resolveWeaponFeatIdentity(familyKey, slug, rawSource)
featKey, _, _, err := ctx.resolveGeneratedFeatIdentity(spec, slug, sourceRow)
if err != nil {
return "", false
}
return featKey, true
}
func weaponFamilySourceColumn(familyKey string) string {
for _, spec := range featWeaponFamilies {
if spec.FamilyKey == familyKey {
return spec.SourceColumn
}
}
return ""
}
func (c *featGeneratedContext) preferredFeatKeyForID(familyKey string, rowID int) (string, bool) {
prefix := "feat:" + familyKey
type candidateBuckets struct {
@@ -2947,6 +2886,42 @@ func parseColumns(baseData map[string]any, datasetName string) ([]string, error)
return columns, nil
}
func extendColumns(columns []string, obj map[string]any, datasetName, sourceLabel string) ([]string, error) {
rawColumns, ok := obj["columns"]
if !ok {
return columns, nil
}
columnList, ok := rawColumns.([]any)
if !ok {
return nil, fmt.Errorf("dataset %s: columns in %s must be an array", datasetName, sourceLabel)
}
extended := append([]string(nil), columns...)
for _, raw := range columnList {
column, ok := raw.(string)
if !ok {
return nil, fmt.Errorf("dataset %s: columns in %s must contain strings", datasetName, sourceLabel)
}
if isDeprecatedAuthoringOnlyField(datasetName, column) || isMetadataField(column) {
continue
}
if _, exists := canonicalColumn(extended, column); exists {
continue
}
extended = append(extended, column)
}
return extended, nil
}
func ensureRowsExposeColumns(rows []map[string]any, columns []string) {
for _, row := range rows {
for _, column := range columns {
if _, ok := row[column]; !ok {
row[column] = nullValue
}
}
}
}
func canonicalizeBaseRow(dataset nativeDataset, columns []string, raw map[string]any, index int) (map[string]any, error) {
rowID := index
if value, ok := raw["id"]; ok {