clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat/atlas-next-horizon
atlas / format / vnext / table.go
table.go 4.9K
1package vnext23import (4 "bytes"5 "fmt"6 "math"7 "slices"8)910// Value is one scalar in a typed column. KindInvalid is the null value.11type Value struct {12 Kind Kind13 Bool bool14 Int64 int6415 Float64 float6416 String string17 Bytes []byte18 ID ID19}2021func NullValue() Value { return Value{} }22func BoolValue(value bool) Value { return Value{Kind: KindBool, Bool: value} }23func Int64Value(value int64) Value { return Value{Kind: KindInt64, Int64: value} }24func Float64Value(value float64) Value { return Value{Kind: KindFloat64, Float64: value} }25func StringValue(value string) Value { return Value{Kind: KindString, String: value} }26func IDValue(value ID) Value { return Value{Kind: KindID, ID: value} }2728func BytesValue(value []byte) Value {29 return Value{Kind: KindBytes, Bytes: append([]byte(nil), value...)}30}3132// Equal compares values while preserving float bit patterns and byte content.33func (value Value) Equal(other Value) bool {34 return value.Kind == other.Kind &&35 value.Bool == other.Bool &&36 value.Int64 == other.Int64 &&37 math.Float64bits(value.Float64) == math.Float64bits(other.Float64) &&38 value.String == other.String &&39 bytes.Equal(value.Bytes, other.Bytes) &&40 value.ID == other.ID41}4243// Column is one field laid out for every row in a table.44type Column struct {45 Field Field46 Values []Value47}4849// Table is one root type encoded as hybrid columns.50type Table struct {51 TypeID ID52 Rows int53 Columns []Column54}5556// Value returns one field value from one row.57func (table Table) Value(fieldID ID, row int) (Value, bool) {58 for _, column := range table.Columns {59 if column.Field.ID == fieldID {60 return column.Values[row], true61 }62 }63 return Value{}, false64}6566func (table Table) validate() error {67 if table.TypeID.isZero() || table.Rows < 0 {68 return fmt.Errorf("table has invalid type or row count")69 }70 seen := make(map[ID]bool, len(table.Columns))71 for _, column := range table.Columns {72 if column.Field.ID.isZero() || !column.Field.Kind.valid() || seen[column.Field.ID] {73 return fmt.Errorf("table %s carries a duplicate or invalid column", table.TypeID)74 }75 seen[column.Field.ID] = true76 if len(column.Values) != table.Rows {77 return fmt.Errorf("column %s has %d values for %d rows", column.Field.ID, len(column.Values), table.Rows)78 }79 for row, value := range column.Values {80 if value.Kind == KindInvalid && !column.Field.Optional {81 return fmt.Errorf("required column %s is null at row %d", column.Field.ID, row)82 }83 if value.Kind != KindInvalid && value.Kind != column.Field.Kind {84 return fmt.Errorf("column %s expects kind %d, got %d at row %d", column.Field.ID, column.Field.Kind, value.Kind, row)85 }86 }87 }88 return nil89}9091func canonicalTable(table Table) Table {92 out := Table{TypeID: table.TypeID, Rows: table.Rows, Columns: append([]Column(nil), table.Columns...)}93 slices.SortFunc(out.Columns, func(left, right Column) int {94 return slices.Compare(left.Field.ID[:], right.Field.ID[:])95 })96 return out97}9899// TableProjection separates columns understood by an application from newer100// file columns while retaining both for lossless pass-through.101type TableProjection struct {102 Known Table103 Foreign Table104}105106// Complete reunites known and foreign columns in canonical order.107func (projection TableProjection) Complete() Table {108 columns := append([]Column(nil), projection.Known.Columns...)109 columns = append(columns, projection.Foreign.Columns...)110 return canonicalTable(Table{TypeID: projection.Known.TypeID, Rows: projection.Known.Rows, Columns: columns})111}112113// ProjectTable classifies a file table using an application's schema. The114// embedded file schema remains authoritative for field metadata.115func ProjectTable(application, file Schema, table Table) (TableProjection, error) {116 if _, err := UnionSchemas(application, file); err != nil {117 return TableProjection{}, fmt.Errorf("merge application and file schemas: %w", err)118 }119 fileType, ok := file.Type(table.TypeID)120 if !ok {121 return TableProjection{}, fmt.Errorf("file schema does not describe table type %s", table.TypeID)122 }123 applicationType, applicationKnowsType := application.Type(table.TypeID)124 projection := TableProjection{125 Known: Table{TypeID: table.TypeID, Rows: table.Rows},126 Foreign: Table{TypeID: table.TypeID, Rows: table.Rows},127 }128 for _, column := range table.Columns {129 field, held := fileType.Field(column.Field.ID)130 if !held || field.Kind != column.Field.Kind {131 return TableProjection{}, fmt.Errorf("file schema disagrees with column %s", column.Field.ID)132 }133 column.Field = field134 if _, known := applicationType.Field(field.ID); applicationKnowsType && known {135 projection.Known.Columns = append(projection.Known.Columns, column)136 } else {137 projection.Foreign.Columns = append(projection.Foreign.Columns, column)138 }139 }140 projection.Known = canonicalTable(projection.Known)141 projection.Foreign = canonicalTable(projection.Foreign)142 return projection, nil143}