Typed columns and null #13

Merged
lilleman merged 10 commits from typed-columns into main 2026-09-15 15:19:39 +02:00
6 changed files with 34 additions and 5 deletions
Showing only changes of commit 1ad9418a76 - Show all commits
+3
View File
@@ -69,6 +69,9 @@ func TestBuiltinFloat(t *testing.T) {
if got := mustRender(t, f, `"{float(1,2,3)}"`); !re.MatchString(got) { if got := mustRender(t, f, `"{float(1,2,3)}"`); !re.MatchString(got) {
t.Fatalf("float(1,2,3) = %q, want d.ddd in [1,2]", got) t.Fatalf("float(1,2,3) = %q, want d.ddd in [1,2]", got)
} }
if got := mustRender(t, f, `"{float(-1,1,0)}"`); got == "-0" {
t.Fatalf("float(-1,1,0) = %q, want a zero printed unsigned", got)
}
} }
} }
+2
View File
@@ -36,6 +36,7 @@ func TestCalcAuto(t *testing.T) {
`"{calc(10 / 3)}"`: "3.3333333333333335", `"{calc(10 / 3)}"`: "3.3333333333333335",
`"{calc(6 / 2)}"`: "3", `"{calc(6 / 2)}"`: "3",
`"{calc(1 / 4)}"`: "0.25", `"{calc(1 / 4)}"`: "0.25",
`"{calc(0 * -1)}"`: "0",
} }
for tmpl, want := range cases { for tmpl, want := range cases {
if got := mustRender(t, f, tmpl); got != want { if got := mustRender(t, f, tmpl); got != want {
@@ -52,6 +53,7 @@ func TestCalcDecimals(t *testing.T) {
`"{calc(10 / 3, 2)}"`: "3.33", `"{calc(10 / 3, 2)}"`: "3.33",
`"{calc(10 / 3, 0)}"`: "3", `"{calc(10 / 3, 0)}"`: "3",
`"{calc(2 * 3, 2)}"`: "6.00", `"{calc(2 * 3, 2)}"`: "6.00",
`"{calc(-0.001, 2)}"`: "0.00",
} }
for tmpl, want := range cases { for tmpl, want := range cases {
if got := mustRender(t, f, tmpl); got != want { if got := mustRender(t, f, tmpl); got != want {
+16 -1
View File
@@ -32,6 +32,7 @@ func TestDatatypeAndNullSitOnlyInAColumn(t *testing.T) {
`null`: `so write ""`, `null`: `so write ""`,
`{"format":"{p}","p":{"format":"{x}","x":[null,"a"]}}`: `so write ""`, `{"format":"{p}","p":{"format":"{x}","x":[null,"a"]}}`: `so write ""`,
`{"format":"","c":[{"format":"1","datatype":"integer"},"x"]}`: `write it as {"format":"x","datatype":"integer"}`, `{"format":"","c":[{"format":"1","datatype":"integer"},"x"]}`: `write it as {"format":"x","datatype":"integer"}`,
`{"format":"","c":[{"format":"1","datatype":"integer"},{"format":"2","weight":3}]}`: `give it "datatype": "integer"`,
`{"format":"","c":[{"format":"1","datatype":"integer"},{"format":"true","datatype":"boolean"}]}`: "a column holds one datatype", `{"format":"","c":[{"format":"1","datatype":"integer"},{"format":"true","datatype":"boolean"}]}`: "a column holds one datatype",
} { } {
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) { if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) {
@@ -66,6 +67,17 @@ func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) {
{"an overflow", `{"format":"{calc(a * a)}","a":"{digits(200)}","datatype":"number"}`, "is not proven within 1e300"}, {"an overflow", `{"format":"{calc(a * a)}","a":"{digits(200)}","datatype":"number"}`, "is not proven within 1e300"},
{"a division in an integer column", `{"format":"{calc(a / b)}","a":"{int(1,9)}","b":"{int(1,9)}","datatype":"integer"}`, "prints a number, not an integer"}, {"a division in an integer column", `{"format":"{calc(a / b)}","a":"{int(1,9)}","b":"{int(1,9)}","datatype":"integer"}`, "prints a number, not an integer"},
{"a composed operand", `{"format":"{calc(n * 2)}","n":"{int(1,99)}.{digits(2)}","datatype":"number"}`, "{seq()}, {digits()} or {calc()}"}, {"a composed operand", `{"format":"{calc(n * 2)}","n":"{int(1,99)}.{digits(2)}","datatype":"number"}`, "{seq()}, {digits()} or {calc()}"},
{"a divisor that prints as zero", `{"format":"{calc(1 / b, 2)}","b":"{float(4.9999999999999994e-79,5e-79,78)}","datatype":"number"}`, "divides by b, which is not proven nonzero"},
{"a divisor that rounds to zero", `{"format":"{calc(a / b)}","a":"{int(1,9)}","b":"{float(0.1,1,0)}","datatype":"number"}`, "divides by b, which is not proven nonzero"},
{"a zero among a divisor's literals", `{"format":"{calc(a / b)}","a":"{int(1,9)}","b":["0","5"],"datatype":"number"}`, "divides by b, which is not proven nonzero"},
{"a negated divisor crossing zero", `{"format":"{calc(a / (-b + 10))}","a":"{int(1,9)}","b":"{int(1,20)}","datatype":"number"}`, "which is not proven nonzero"},
{"a subtracted divisor crossing zero", `{"format":"{calc(a / (10 - b))}","a":"{int(1,9)}","b":"{int(1,20)}","datatype":"number"}`, "which is not proven nonzero"},
{"a quotient past the limit", `{"format":"{calc(a / b / b)}","a":"{digits(300)}","b":"{float(0.000001,1,6)}","datatype":"number"}`, "is not proven within 1e300"},
{"an operand past the limit", `{"format":"{calc(a)}","a":"{digits(400)}","datatype":"number"}`, "is not proven within 1e300"},
{"a whole calc past int64", `{"format":"{calc(a * 2)}","a":"{seq()}","datatype":"integer"}`, "{calc(a * 2)} is not proven within int64"},
{"a whole float past int64", `{"format":"{float(0,1e19,0)}","datatype":"integer"}`, "{float(0,1e19,0)} is not proven within int64"},
{"a signed zero integer", `{"format":"-0","datatype":"integer"}`, `"-0" is zero written with a sign; write "0"`},
{"a signed zero number", `{"format":"-0.00","datatype":"number"}`, `"-0.00" is zero written with a sign; write "0.00"`},
} { } {
row := `{"format":"","col":` + c.column + `}` row := `{"format":"","col":` + c.column + `}`
files := map[string]string{"row": row} files := map[string]string{"row": row}
@@ -83,7 +95,7 @@ func TestDatatypeRejectsAValueItsTypeRejects(t *testing.T) {
} }
} }
var jsonInteger = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`) var jsonInteger = regexp.MustCompile(`^(0|-?[1-9][0-9]*)$`)
func TestDatatypeAcceptsAColumnThatAlwaysParses(t *testing.T) { func TestDatatypeAcceptsAColumnThatAlwaysParses(t *testing.T) {
cat := `[{"format":"{code}","code":"200"},{"format":"{code}","code":"404"}]` cat := `[{"format":"{code}","code":"200"},{"format":"{code}","code":"404"}]`
@@ -94,6 +106,9 @@ func TestDatatypeAcceptsAColumnThatAlwaysParses(t *testing.T) {
`{"format":"{/cat.code}","datatype":"integer"}`, `{"format":"{/cat.code}","datatype":"integer"}`,
`{"format":"{a|b}","a":"1","b":"{int(5,9)}","datatype":"integer"}`, `{"format":"{a|b}","a":"1","b":"{int(5,9)}","datatype":"integer"}`,
`{"format":"{float(-1.5,9.5,0)}","datatype":"integer"}`, `{"format":"{float(-1.5,9.5,0)}","datatype":"integer"}`,
`{"format":"{float(-1,1,0)}","datatype":"integer"}`,
`{"format":"{calc(a * b)}","a":"{int(-9,-1)}","b":"{int(0,9)}","datatype":"integer"}`,
`{"format":"{calc(x + 1)}","x":"{float(0,9,0)}","datatype":"integer"}`,
`{"format":"{float(-1,1,2)}","datatype":"number"}`, `{"format":"{float(-1,1,2)}","datatype":"number"}`,
`{"format":"{v}","v":["1","2.5","6.022e23"],"datatype":"number"}`, `{"format":"{v}","v":["1","2.5","6.022e23"],"datatype":"number"}`,
`{"format":"{b}","b":["true","false"],"datatype":"boolean"}`, `{"format":"{b}","b":["true","false"],"datatype":"boolean"}`,
+1
View File
@@ -18,6 +18,7 @@ func TestRepeatedChoiceItemIsRejected(t *testing.T) {
`["a", "a", "b"]`: `{ "format": "a", "weight": 2 }`, `["a", "a", "b"]`: `{ "format": "a", "weight": 2 }`,
`[{"format":"{x}","x":"1"},{"format":"{x}","x":"1"}]`: "repeats item", `[{"format":"{x}","x":"1"},{"format":"{x}","x":"1"}]`: "repeats item",
`{"format":"{w}","w":["", "", "x"]}`: `{ "format": "", "weight": 2 }`, `{"format":"{w}","w":["", "", "x"]}`: `{ "format": "", "weight": 2 }`,
`{"format":"","w":[null,null,"a"]}`: "a null takes no weight",
} { } {
if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) { if _, err := compile(parse(t, src)); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("compile(%s) = %v, want an error naming %s", src, err, want) t.Errorf("compile(%s) = %v, want an error naming %s", src, err, want)
+4
View File
@@ -68,6 +68,10 @@ func TestNoRecordAllocRegression(t *testing.T) {
if allocs := testing.AllocsPerRun(10000, func() { f.FakeRecord("x") }); allocs > base*1.10 { if allocs := testing.AllocsPerRun(10000, func() { f.FakeRecord("x") }); allocs > base*1.10 {
t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a record fence running per draw is the usual cause", s.name, allocs, base*1.10, base) t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a record fence running per draw is the usual cause", s.name, allocs, base*1.10, base)
} }
r, _ := f.FakeRecord("x")
if allocs := testing.AllocsPerRun(10000, func() { _ = r.CSVLine() }); allocs > 2 {
t.Errorf("%s: CSVLine() makes %.1f allocs/op, want 2: the fields slice and the joined line", s.name, allocs)
}
} }
} }
+8 -4
View File
@@ -134,7 +134,7 @@ func TestRecordCSV(t *testing.T) {
} }
func TestRecordCSVEmptyValueStaysARow(t *testing.T) { func TestRecordCSVEmptyValueStaysARow(t *testing.T) {
dir := writeData(t, map[string]string{"blank": `{"format": "", "note": ""}`, "gone": `{"format": "", "note": null}`}) dir := writeData(t, map[string]string{"blank": `{"format": "", "note": ""}`})
f := newGenerator(t, dir, WithSeed(1)) f := newGenerator(t, dir, WithSeed(1))
r, err := f.FakeRecord("blank") r, err := f.FakeRecord("blank")
if err != nil { if err != nil {
@@ -147,9 +147,6 @@ func TestRecordCSVEmptyValueStaysARow(t *testing.T) {
if len(rows) != 2 || len(rows[1]) != 1 || rows[1][0] != "" { if len(rows) != 2 || len(rows[1]) != 1 || rows[1][0] != "" {
t.Fatalf("one empty column parsed to %v, want a header and one row of one empty field", rows) t.Fatalf("one empty column parsed to %v, want a header and one row of one empty field", rows)
} }
if r, err := f.FakeRecord("gone"); err != nil || r.CSVLine() != "" {
t.Fatalf("a lone null column wrote %q, %v; want the blank line PostgreSQL's COPY reads as null", r.CSVLine(), err)
}
} }
func TestRecordWritesTypedAndNullColumns(t *testing.T) { func TestRecordWritesTypedAndNullColumns(t *testing.T) {
@@ -183,6 +180,13 @@ func TestRecordWritesTypedAndNullColumns(t *testing.T) {
if got := DataTypeNumber.String(); got != "number" { if got := DataTypeNumber.String(); got != "number" {
t.Errorf("DataTypeNumber.String() = %q, want the data's spelling", got) t.Errorf("DataTypeNumber.String() = %q, want the data's spelling", got)
} }
lone, err := newGenerator(t, writeData(t, map[string]string{"gone": `{"format":"","note":null}`})).FakeRecord("gone")
if err != nil {
t.Fatal(err)
}
if got := lone.CSVLine(); got != "" {
t.Errorf("a lone null column wrote CSVLine() = %q, want the blank line PostgreSQL's COPY reads as null", got)
}
} }
func TestRecordRejectsOverlappingReferenceColumns(t *testing.T) { func TestRecordRejectsOverlappingReferenceColumns(t *testing.T) {