Add record output to JSON, CSV and SQL #8
@@ -125,9 +125,13 @@ required key — so `"format": ""` carries the fields with an inert format: it
|
||||
renders nothing by `Fake`, and is compiled only so the tree's fences still run.
|
||||
The columns are the point, and their facts stay together: two columns that
|
||||
reference one category — `{/currency.code}` and `{/currency.symbol}` — share one
|
||||
draw of it, so the record is internally consistent. A field hold, transform or
|
||||
operand ties fields together within one column as always (see
|
||||
[Correlated fields](#correlated-fields) and [Decisions](#decisions)).
|
||||
draw of it, so the record is internally consistent. That one draw is also why two
|
||||
columns may not read overlapping reference paths — `{/cat.a}` beside `{/cat.a.b}`
|
||||
is refused, naming the fields to write instead, exactly as
|
||||
[One draw, one spelling](#one-draw-one-spelling) refuses the pair inside a single
|
||||
format. A field hold, transform or operand ties fields together within one column
|
||||
as always (see [Correlated fields](#correlated-fields) and
|
||||
[Decisions](#decisions)).
|
||||
|
||||
## Library
|
||||
|
||||
|
||||
@@ -216,9 +216,7 @@ type recordFormat struct {
|
||||
line func(r *fejkdata.Record, table string) string
|
||||
}
|
||||
|
||||
// recordFormats is every --format that writes records. Adding one is this entry
|
||||
// alone: the flag check reads the same table the writer dispatches through, so a
|
||||
// format cannot be accepted and then not written.
|
||||
// recordFormats is every --format that writes records.
|
||||
var recordFormats = map[string]recordFormat{
|
||||
"csv": {header: (*fejkdata.Record).CSVHeader, line: func(r *fejkdata.Record, _ string) string { return r.CSVLine() }},
|
||||
"json": {line: func(r *fejkdata.Record, _ string) string { return r.JSON() }},
|
||||
@@ -245,6 +243,9 @@ func (in invocation) checkFlags() error {
|
||||
if _, ok := recordFormats[in.format]; !ok && in.format != "text" {
|
||||
return fmt.Errorf("--format takes %s, got %q", formatNames(), in.format)
|
||||
}
|
||||
if in.tableSet && in.table == "" {
|
||||
return errors.New("--table names the INSERT target, so it cannot be empty")
|
||||
}
|
||||
if in.tableSet && in.format != "sql" {
|
||||
return errors.New("--table names the INSERT target, so it needs --format sql")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -15,9 +16,8 @@ type Column struct {
|
||||
}
|
||||
|
||||
// Record is one record rendered from a template: every direct field is a column,
|
||||
// drawn independently and listed in name order. The template's format is the
|
||||
// string a [Generator.Fake] call renders; a record is its inverse — each field
|
||||
// projected as a column instead of composed.
|
||||
// listed in name order. Each column is its own expansion, so a sibling field is
|
||||
// local to it, while a reference is drawn once for the whole record.
|
||||
type Record struct {
|
||||
columns []Column
|
||||
}
|
||||
@@ -68,7 +68,11 @@ func csvLine(cols []string) string {
|
||||
w := csv.NewWriter(&b)
|
||||
_ = w.Write(cols)
|
||||
w.Flush()
|
||||
return strings.TrimSuffix(b.String(), "\n")
|
||||
line := strings.TrimSuffix(b.String(), "\n")
|
||||
if line == "" {
|
||||
return `""` // a blank line is a row every CSV reader drops
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
// SQLInsert renders the record as one INSERT statement into table: identifiers in
|
||||
@@ -145,10 +149,8 @@ func (f *Generator) FakeRecord(input string) (*Record, error) {
|
||||
return t.Fake(), nil
|
||||
}
|
||||
|
||||
// recordOf is the fence both record entry points pass: the node is a template, it
|
||||
// carries no repeat — which composes the format rather than projecting columns —
|
||||
// and it offers at least one column. The columns come back with it, fixed for
|
||||
// every draw the caller goes on to make.
|
||||
// recordOf is the fence both record entry points pass. The columns come back with
|
||||
// the template, fixed for every draw the caller goes on to make.
|
||||
func recordOf(n node) (*template, []string, error) {
|
||||
t, ok := n.(*template)
|
||||
if !ok {
|
||||
@@ -161,9 +163,71 @@ func recordOf(n node) (*template, []string, error) {
|
||||
if len(columns) == 0 {
|
||||
return nil, nil, errors.New("has no fields, so no columns")
|
||||
}
|
||||
if err := checkColumnRefs(t, columns); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return t, columns, nil
|
||||
}
|
||||
|
||||
// checkColumnRefs rejects two reference reads that overlap across a record's
|
||||
// columns: one renders a level the other reads a path into, and the record's one
|
||||
// draw of that level cannot answer for both. checkNoOverlap settles the pair
|
||||
// within a single format; the record's shared scope is what carries it across
|
||||
// columns, so the same pair is settled here. A bare reference holds nothing, so it
|
||||
// is not collected and keeps drawing on its own.
|
||||
func checkColumnRefs(t *template, columns []string) error {
|
||||
reads := columnRefs(t, columns)
|
||||
sort.Slice(reads, func(i, j int) bool {
|
||||
if reads[i].a.path != reads[j].a.path {
|
||||
return reads[i].a.path < reads[j].a.path
|
||||
}
|
||||
return reads[i].column < reads[j].column
|
||||
})
|
||||
for i, level := range reads {
|
||||
for _, into := range reads[i+1:] {
|
||||
if strings.HasPrefix(into.a.path, level.a.path+".") {
|
||||
return fmt.Errorf("column %q renders {%s}, a level column %q reads a path into with {%s}; name the fields you want instead",
|
||||
level.column, level.a.name, into.column, into.a.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// columnRef is one held reference read, and the column whose render reaches it.
|
||||
type columnRef struct {
|
||||
column string
|
||||
a arm
|
||||
}
|
||||
|
||||
// columnRefs lists every held reference read anywhere a column renders, following
|
||||
// the same edges expand does.
|
||||
func columnRefs(t *template, columns []string) []columnRef {
|
||||
var out []columnRef
|
||||
for _, name := range columns {
|
||||
seen := map[node]bool{}
|
||||
var walk func(n node)
|
||||
walk = func(n node) {
|
||||
if n == nil || seen[n] {
|
||||
return
|
||||
}
|
||||
seen[n] = true
|
||||
if tm, ok := n.(*template); ok {
|
||||
for _, r := range boundReaders(tm.format, tm.bound, tm.refs) {
|
||||
if a := splitArm(r.name, tm.refs); isRef(a.key) && len(a.tail) > 0 {
|
||||
out = append(out, columnRef{name, a})
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, e := range renderEdges(n) {
|
||||
walk(e.to)
|
||||
}
|
||||
}
|
||||
walk(t.fields[name])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// renderRecord draws each column once, in the name order recordOf fixed. The
|
||||
// columns share one reference scope, so two columns that reference one category
|
||||
// read one draw of it.
|
||||
|
||||
Reference in New Issue
Block a user