Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions internal/cli/data.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -315,7 +315,7 @@ Exit codes:
"size check).")
cmd.Flags().StringVar(&schemaFlag, "schema", "",
"tabular/time-series only: column types as col:TYPE,col:TYPE (e.g. age:INT,price:FLOAT). "+
"Default: inferred from the CSV (INT/FLOAT/VARCHAR).")
"Default: inferred from the CSV (INT/BIGINT/FLOAT/BOOLEAN/DATE/DATETIME/VARCHAR(n)).")
cmd.Flags().StringVar(&labelPolicy, "label-policy", "",
"regression-class only (tabular_regression, time_series_forecasting, time_to_event_prediction): "+
"passthrough|bucket (default bucket — bins the target so the raw value never leaves the cluster)")
Expand DownExpand Up@@ -638,32 +638,43 @@ collaborators can train against that table without ever seeing the raw files.`))
return &exitError{code: 3, err: perr}
}

// Column schema: an explicit --schema wins; otherwise infer
// INT/FLOAT/VARCHAR types from the CSV so the customer doesn't
// hand-write one for the common case.
// Column schema. An explicit --schema wins (raw flag, or the
// optional override the interactive prompt captures into SchemaFlag).
// Otherwise infer the types here — mirroring the ingestor's own rules
// (di#349) — and EMIT the result explicitly (below, via a.Spec.Schema
// → spec.schema), so the ingestor uses the CLI's answer regardless of
// its own version. Inference runs on both a no-schema non-interactive
// run and an interactive run where the user left the schema prompt
// blank; the risky cases below are surfaced as warnings.
if a.SchemaFlag != "" {
sch, perr := push.ParseSchema(a.SchemaFlag)
if perr != nil {
return &exitError{code: 2, err: perr}
}
a.Spec.Schema = sch
} else {
sch, skipped, empty, ierr := push.InferSchema(layout.LabelsCSV)
res, ierr := push.InferSchema(layout.LabelsCSV)
if ierr != nil {
return &exitError{code: 3, err: fmt.Errorf("inferring schema from CSV: %w", ierr)}
}
a.Spec.Schema = sch
a.Spec.Schema = res.Schema
_, _ = fmt.Fprintf(out,
"Inferred schema for %d column(s) from %s (override with --schema).\n",
len(sch), filepath.Base(layout.LabelsCSV))
if len(skipped) > 0 {
len(res.Schema), filepath.Base(layout.LabelsCSV))
if len(res.Skipped) > 0 {
_, _ = fmt.Fprintf(out,
" (skipped framework-managed column(s): %s)\n", strings.Join(res.Skipped, ", "))
}
if len(res.Empty) > 0 {
_, _ = fmt.Fprintf(out,
" (skipped framework-managed column(s): %s)\n", strings.Join(skipped, ", "))
" (warning: %d column(s) had no values in the sample and were typed VARCHAR(1): %s)\n",
len(res.Empty), strings.Join(res.Empty, ", "))
}
if len(empty) > 0 {
if len(res.IDLike) > 0 {
_, _ = fmt.Fprintf(out,
" (warning: %d column(s) had no values in the sample and were typed FLOAT (nullable): %s)\n",
len(empty), strings.Join(empty, ", "))
" (warning: %d column(s) look like identifiers (all-unique integers): %s — "+
"if any is a zero-padded code, pass --schema to type it VARCHAR)\n",
len(res.IDLike), strings.Join(res.IDLike, ", "))
}
}
case push.IsImage(a.Spec.Category):
Expand Down
8 changes: 4 additions & 4 deletions internal/push/parity_golden_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,8 +115,8 @@ func goLabelValues(t *testing.T, c parityCase) LabelReadValues {
csvPath := filepath.Join("testdata", "parity", "cases", c.Name, c.CSV)
schema := c.Schema
if IsTabular(c.Category) && len(schema) == 0 {
if sch, _, _, err := InferSchema(csvPath); err == nil {
schema = sch
if res, err := InferSchema(csvPath); err == nil {
schema = res.Schema
}
}
dropNA, collapse := false, false
Expand DownExpand Up@@ -152,8 +152,8 @@ func runGoPreflight(t *testing.T, c parityCase) string {
// schema the same way, so dtype-sensitive verdicts stay comparable.
if len(c.Schema) > 0 {
spec.Schema = c.Schema
} else if sch, _, _, err := InferSchema(layout.LabelsCSV); err == nil {
spec.Schema = sch
} else if res, err := InferSchema(layout.LabelsCSV); err == nil {
spec.Schema = res.Schema
}
}
_, problem := PreflightDataset(spec, layout)
Expand Down
43 changes: 43 additions & 0 deletions internal/push/schema_inference_parity_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
package push

import (
"encoding/json"
"os"
"testing"
)

// TestSchemaInferenceParity pins the Go CLI's tabular type inference against
// data-ingestors' committed value->type contract
// (testdata/schema_inference_parity.json, vendored from di#349's
// tests/fixtures/schema_inference_parity.json). The ingestor's
// schema_inference.infer_column_type is the source of truth (Principle 6 /
// backend#1009); this test fails if the Go mirror drifts from it — a
// static, venv-free parity check the CI can run on every PR.
func TestSchemaInferenceParity(t *testing.T) {
data, err := os.ReadFile("testdata/schema_inference_parity.json")
if err != nil {
t.Fatalf("read parity fixture: %v", err)
}
var fixture struct {
Cases []struct {
Name string `json:"name"`
Values []string `json:"values"`
Expected string `json:"expected"`
} `json:"cases"`
}
if err := json.Unmarshal(data, &fixture); err != nil {
t.Fatalf("parse parity fixture: %v", err)
}
if len(fixture.Cases) == 0 {
t.Fatal("parity fixture has no cases — the di#349 contract is empty")
}
for _, c := range fixture.Cases {
t.Run(c.Name, func(t *testing.T) {
// inferColumnType cleans (trim + drop empty/NA) then classifies,
// mirroring the ingestor's per-column path exactly.
if got := inferColumnType(c.Values); got != c.Expected {
t.Errorf("inferColumnType(%v) = %q, want %q (di#349 contract)", c.Values, got, c.Expected)
}
})
}
}
Loading
Loading