Repository: go-go-golems/glazed
Pull request #306 (2023-06-27 18:03:44)

:sparkles: Use ordered map structure and row level middlewares
This uses go-ordered-map for the values of a row, so that we can reliably keep columns ordering.

- :sparkles: First pass at using orderedmap for the row values
- :ambulance: Fix templating on ordered maps
- :arrow_up: Add dependency on go-ordered-map
- :ambulance: Fix jq layer
- :art: Fix linting



Patch:
diff --git a/cmd/glaze/cmds/csv.go b/cmd/glaze/cmds/csv.go
index 90a7ea7..6f59d3c 100644
--- a/cmd/glaze/cmds/csv.go
+++ b/cmd/glaze/cmds/csv.go
@@ -8,6 +8,7 @@ import (
 	"github.com/go-go-golems/glazed/pkg/helpers/csv"
 	"github.com/go-go-golems/glazed/pkg/processor"
 	"github.com/go-go-golems/glazed/pkg/settings"
+	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/pkg/errors"
 	"os"
 )
@@ -124,7 +125,9 @@ func (c *CsvCommand) Run(
 		if err != nil {
 			return errors.Wrap(err, "could not open file")
 		}
-		defer f.Close()
+		defer func(f *os.File) {
+			_ = f.Close()
+		}(f)
 
 		header, s, err := csv.ParseCSV(f, options...)
 		if err != nil {
@@ -132,7 +135,7 @@ func (c *CsvCommand) Run(
 		}
 
 		for _, row := range s {
-			err = gp.ProcessInputObject(ctx, row)
+			err = gp.ProcessInputObject(ctx, types.NewRowFromMapWithColumns(row, header))
 			if err != nil {
 				return errors.Wrap(err, "could not process CSV row")
 			}
@@ -147,11 +150,11 @@ func (c *CsvCommand) Run(
 		}
 	}
 
-	table, err := gp.OutputFormatter().GetTable()
+	err := gp.Processor().FinalizeTable(ctx)
 	if err != nil {
-		return errors.Wrap(err, "could not get table")
+		return errors.Wrap(err, "could not finalize table")
 	}
-
+	table := gp.Processor().GetTable()
 	table.Columns = finalHeaders
 
 	return nil
diff --git a/cmd/glaze/cmds/docs.go b/cmd/glaze/cmds/docs.go
index 3128212..79774a7 100644
--- a/cmd/glaze/cmds/docs.go
+++ b/cmd/glaze/cmds/docs.go
@@ -6,6 +6,7 @@ import (
 	"github.com/go-go-golems/glazed/pkg/cli"
 	"github.com/go-go-golems/glazed/pkg/cmds/layers"
 	"github.com/go-go-golems/glazed/pkg/settings"
+	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/spf13/cobra"
 	"os"
 )
@@ -24,18 +25,22 @@ var DocsCmd = &cobra.Command{
 			s, err := os.ReadFile(arg)
 			cobra.CheckErr(err)
 
-			var metaData map[string]interface{}
+			var metaData types.Row
 			inputReader := bytes.NewReader(s)
 			_, err = frontmatter.Parse(inputReader, &metaData)
 			cobra.CheckErr(err)
 
-			metaData["path"] = arg
+			metaData.Set("path", arg)
 
+			// TODO(manuel, 2023-06-25) It would be nice to unmarshal the YAML to an orderedmap
+			// See https://github.com/go-go-golems/glazed/issues/305
 			err = gp.ProcessInputObject(ctx, metaData)
 			cobra.CheckErr(err)
 		}
 
-		err = gp.OutputFormatter().Output(ctx, os.Stdout)
+		err = gp.Processor().FinalizeTable(ctx)
+		cobra.CheckErr(err)
+		err = gp.OutputFormatter().Output(ctx, gp.Processor().GetTable(), os.Stdout)
 		cobra.CheckErr(err)
 	},
 }
diff --git a/cmd/glaze/cmds/html/cmds.go b/cmd/glaze/cmds/html/cmds.go
index 496f576..85a6d0d 100644
--- a/cmd/glaze/cmds/html/cmds.go
+++ b/cmd/glaze/cmds/html/cmds.go
@@ -40,7 +40,10 @@ func NewHTMLCommand() (*cobra.Command, error) {
 				cobra.CheckErr(err)
 			}
 
-			err = gp.OutputFormatter().Output(ctx, os.Stdout)
+			err = gp.Processor().FinalizeTable(ctx)
+			cobra.CheckErr(err)
+
+			err = gp.OutputFormatter().Output(ctx, gp.Processor().GetTable(), os.Stdout)
 			cobra.CheckErr(err)
 			if _, ok := err.(*cmds.ExitWithoutGlazeError); ok {
 				os.Exit(0)
@@ -75,7 +78,9 @@ func NewHTMLCommand() (*cobra.Command, error) {
 				}
 				f, err := os.Open(arg)
 				cobra.CheckErr(err)
-				defer f.Close()
+				defer func(f *os.File) {
+					_ = f.Close()
+				}(f)
 
 				doc, err := html.Parse(f)
 				cobra.CheckErr(err)
@@ -93,7 +98,10 @@ func NewHTMLCommand() (*cobra.Command, error) {
 				cobra.CheckErr(err)
 			}
 
-			err = gp.OutputFormatter().Output(ctx, os.Stdout)
+			err = gp.Processor().FinalizeTable(ctx)
+			cobra.CheckErr(err)
+
+			err = gp.OutputFormatter().Output(ctx, gp.Processor().GetTable(), os.Stdout)
 			cobra.CheckErr(err)
 			if _, ok := err.(*cmds.ExitWithoutGlazeError); ok {
 				os.Exit(0)
diff --git a/cmd/glaze/cmds/html/parse.go b/cmd/glaze/cmds/html/parse.go
index 89ba758..5b96396 100644
--- a/cmd/glaze/cmds/html/parse.go
+++ b/cmd/glaze/cmds/html/parse.go
@@ -3,6 +3,7 @@ package html
 import (
 	"context"
 	"github.com/go-go-golems/glazed/pkg/processor"
+	"github.com/go-go-golems/glazed/pkg/types"
 	"golang.org/x/net/html"
 	"strings"
 )
@@ -83,17 +84,17 @@ func (hsp *HTMLSplitParser) ProcessNode(ctx context.Context, n *html.Node) (*htm
 
 		// TODO(manuel, 2023-03-29) We should add a level attribute here
 
-		row := map[string]interface{}{
-			"Tag":   n.Data,
-			"Title": title,
-		}
+		row := types.NewRow(
+			types.MRP("Tag", n.Data),
+			types.MRP("Title", title),
+		)
 		for c = n.NextSibling; c != nil; c = c.NextSibling {
 			if c.Type == html.ElementNode && hsp.shouldSplit(c) {
 				break
 			}
 			hsp.extractTextHelper(c, &body)
 		}
-		row["Body"] = strings.TrimSpace(body.String())
+		row.Set("Body", strings.TrimSpace(body.String()))
 
 		err := hsp.gp.ProcessInputObject(ctx, row)
 		if err != nil {
@@ -188,13 +189,13 @@ func outputNodesDepthFirst(ctx context.Context, doc *html.Node, gp *processor.Gl
 		})
 	}
 
-	obj := map[string]interface{}{
-		"Type":       htmlNodeTypeToString(doc.Type),
-		"Atom":       doc.DataAtom,
-		"Data":       doc.Data,
-		"Namespace":  doc.Namespace,
-		"Attributes": attributes,
-	}
+	obj := types.NewRow(
+		types.MRP("Type", htmlNodeTypeToString(doc.Type)),
+		types.MRP("Atom", doc.DataAtom),
+		types.MRP("Data", doc.Data),
+		types.MRP("Namespace", doc.Namespace),
+		types.MRP("Attributes", attributes),
+	)
 
 	err := gp.ProcessInputObject(ctx, obj)
 	if err != nil {
diff --git a/cmd/glaze/cmds/json.go b/cmd/glaze/cmds/json.go
index 3a8d7fc..62808fb 100644
--- a/cmd/glaze/cmds/json.go
+++ b/cmd/glaze/cmds/json.go
@@ -9,6 +9,7 @@ import (
 	"github.com/go-go-golems/glazed/pkg/cmds/parameters"
 	"github.com/go-go-golems/glazed/pkg/processor"
 	"github.com/go-go-golems/glazed/pkg/settings"
+	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/pkg/errors"
 	"os"
 )
@@ -75,7 +76,7 @@ func (j *JsonCommand) Run(
 		}
 
 		if inputIsArray {
-			data := make([]map[string]interface{}, 0)
+			data := make([]types.Row, 0)
 			err = json.NewDecoder(f).Decode(&data)
 			if err != nil {
 				return errors.Wrapf(err, "Error decoding file %s as array", arg)
@@ -91,11 +92,12 @@ func (j *JsonCommand) Run(
 			}
 		} else {
 			// read json file
-			data := make(map[string]interface{})
+			data := types.NewRow()
 			err = json.NewDecoder(f).Decode(&data)
 			if err != nil {
 				return errors.Wrapf(err, "Error decoding file %s as object", arg)
 			}
+
 			err = gp.ProcessInputObject(ctx, data)
 			if err != nil {
 				return errors.Wrapf(err, "Error processing file %s as object", arg)
diff --git a/cmd/glaze/cmds/markdown.go b/cmd/glaze/cmds/markdown.go
index 10c7ab4..e994519 100644
--- a/cmd/glaze/cmds/markdown.go
+++ b/cmd/glaze/cmds/markdown.go
@@ -8,6 +8,7 @@ import (
 	"github.com/go-go-golems/glazed/pkg/cmds"
 	"github.com/go-go-golems/glazed/pkg/processor"
 	"github.com/go-go-golems/glazed/pkg/settings"
+	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/spf13/cobra"
 	"github.com/yuin/goldmark"
 	"github.com/yuin/goldmark/ast"
@@ -111,7 +112,7 @@ func addExtensionFlags(cmd *cobra.Command) {
 // - a simple one that just prints a linearized version of the AST
 // - a version with a nested DOM-like structure
 
-type outputElement = map[string]interface{}
+type outputElement = types.Row
 
 var MarkdownCmd = &cobra.Command{
 	Use:   "markdown",
@@ -165,10 +166,12 @@ var parseCmd = &cobra.Command{
 
 		_ = gp
 
-		err = gp.OutputFormatter().Output(ctx, os.Stdout)
-		if err != nil {
-			_, _ = fmt.Fprintf(os.Stderr, "Error rendering output: %s\n", err)
-			os.Exit(1)
+		err = gp.Processor().FinalizeTable(ctx)
+		cobra.CheckErr(err)
+
+		err = gp.OutputFormatter().Output(ctx, gp.Processor().GetTable(), os.Stdout)
+		if _, ok := err.(*cmds.ExitWithoutGlazeError); ok {
+			os.Exit(0)
 		}
 	},
 }
@@ -206,14 +209,14 @@ func splitByHeading(ctx context.Context, md goldmark.Markdown, s []byte, gp *pro
 	// fuck my brain can't deal with stacks right now lol, i need paper
 	err := ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
 		if entering {
-			elt := outputElement{
-				"kind": node.Kind().String(),
-				"text": string(node.Text(s)),
-			}
+			elt := types.NewRow(
+				types.MRP("kind", node.Kind().String()),
+				types.MRP("text", string(node.Text(s))),
+			)
 			switch node.Kind() {
 			case ast.KindHeading:
-				elt["level"] = node.(*ast.Heading).Level
-				elt["children"] = []outputElement{}
+				elt.Set("level", node.(*ast.Heading).Level)
+				elt.Set("children", []outputElement{})
 			}
 			parseStack = append(parseStack, elt)
 		} else {
@@ -221,7 +224,8 @@ func splitByHeading(ctx context.Context, md goldmark.Markdown, s []byte, gp *pro
 			parseStack = parseStack[:len(parseStack)-1]
 
 			// skip Text and Document
-			if top["kind"] == "Text" || top["kind"] == "Document" {
+			topKind, _ := top.Get("kind")
+			if topKind == "Text" || topKind == "Document" {
 				return ast.WalkContinue, nil
 			}
 
@@ -231,8 +235,11 @@ func splitByHeading(ctx context.Context, md goldmark.Markdown, s []byte, gp *pro
 			// in a second pass
 			if len(outputStack) > 0 {
 				outputTop := outputStack[len(outputStack)-1]
-				if outputTop["kind"] == "Heading" && top["kind"] != "Heading" {
-					outputTop["children"] = append(outputTop["children"].([]outputElement), top)
+				outputTopKind, _ := outputTop.Get("kind")
+
+				if outputTopKind == "Heading" && topKind != "Heading" {
+					children, _ := outputTop.Get("children")
+					outputTop.Set("children", append(children.([]outputElement), top))
 					return ast.WalkContinue, nil
 				}
 			}
@@ -277,10 +284,10 @@ func simpleLinearize(ctx context.Context, md goldmark.Markdown, s []byte, gp *pr
 	outputStack := []outputElement{}
 	err := ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
 		if entering {
-			elt := outputElement{
-				"kind": node.Kind().String(),
-				"text": string(node.Text(s)),
-			}
+			elt := types.NewRow(
+				types.MRP("kind", node.Kind().String()),
+				types.MRP("text", string(node.Text(s))),
+			)
 			parseStack = append(parseStack, elt)
 		} else {
 			switch node.Kind() {
@@ -332,7 +339,9 @@ var splitByHeadingCmd = &cobra.Command{
 				}
 				f, err := os.Open(arg)
 				cobra.CheckErr(err)
-				defer f.Close()
+				defer func(f *os.File) {
+					_ = f.Close()
+				}(f)
 
 				s := bufio.NewScanner(f)
 				var currentTitle string
@@ -351,10 +360,10 @@ var splitByHeadingCmd = &cobra.Command{
 						return
 					}
 
-					row := map[string]interface{}{
-						"heading": currentTitle,
-						"content": strings.Trim(strings.Join(current, "\n"), " \n\t"),
-					}
+					row := types.NewRow(
+						types.MRP("heading", currentTitle),
+						types.MRP("content", strings.Trim(strings.Join(current, "\n"), " \n\t")),
+					)
 					err = gp.ProcessInputObject(ctx, row)
 					cobra.CheckErr(err)
 
@@ -380,8 +389,10 @@ var splitByHeadingCmd = &cobra.Command{
 
 		_ = gp
 
-		err = gp.OutputFormatter().Output(ctx, os.Stdout)
+		err = gp.Processor().FinalizeTable(ctx)
 		cobra.CheckErr(err)
+
+		err = gp.OutputFormatter().Output(ctx, gp.Processor().GetTable(), os.Stdout)
 		if _, ok := err.(*cmds.ExitWithoutGlazeError); ok {
 			os.Exit(0)
 		}
diff --git a/cmd/glaze/cmds/yaml.go b/cmd/glaze/cmds/yaml.go
index 2705a9f..b128715 100644
--- a/cmd/glaze/cmds/yaml.go
+++ b/cmd/glaze/cmds/yaml.go
@@ -9,6 +9,7 @@ import (
 	yaml2 "github.com/go-go-golems/glazed/pkg/helpers/yaml"
 	"github.com/go-go-golems/glazed/pkg/processor"
 	"github.com/go-go-golems/glazed/pkg/settings"
+	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 	"gopkg.in/yaml.v3"
@@ -106,7 +107,9 @@ func (y *YamlCommand) Run(
 		}
 
 		if inputIsArray {
-			data := make([]map[string]interface{}, 0)
+			// TODO(manuel, 2023-06-25) We should implement an unmarshaller for maprow from yaml
+			// See https://github.com/go-go-golems/glazed/issues/305
+			data := make([]types.Row, 0)
 			err = yaml.NewDecoder(f).Decode(&data)
 			if err != nil {
 				// check for EOF
@@ -126,7 +129,7 @@ func (y *YamlCommand) Run(
 			}
 		} else {
 			// read json file
-			data := make(map[string]interface{})
+			data := types.NewRow()
 			err = yaml.NewDecoder(f).Decode(&data)
 			if err != nil {
 				// check for EOF
diff --git a/doc/vhs/demo.sh b/doc/vhs/demo.sh
index 2011c71..6759e15 100755
--- a/doc/vhs/demo.sh
+++ b/doc/vhs/demo.sh
@@ -12,7 +12,7 @@ glaze json misc/test-data/[123].json --filter d.e
 
 glaze json --input-is-array misc/test-data/rows.json --output yaml
 glaze yaml misc/test-data/[123].yaml
-glaze json misc/test-data/[123].json --template '{{.a}}-{{.b}}: {{.d_f}}'
+glaze json misc/test-data/[123].json --template '{{.a}}-{{.b}}: {{.d.f}}'
 glaze json misc/test-data/[123].json --template '{{.a}}-{{.b}}: {{.d_f}}' \
   --use-row-templates --fields a,_0 \
   --output csv
diff --git a/go.mod b/go.mod
index 9775438..ab05f24 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,7 @@ require (
 	github.com/stretchr/testify v1.8.2
 	github.com/tj/go-naturaldate v1.3.0
 	github.com/ugorji/go/codec v1.2.11
+	github.com/wk8/go-ordered-map/v2 v2.1.8
 	github.com/xuri/excelize/v2 v2.7.0
 	github.com/yuin/goldmark v1.5.4
 	github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04
@@ -39,6 +40,8 @@ require (
 	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
 	github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
 	github.com/aymerick/douceur v0.2.0 // indirect
+	github.com/bahlo/generic-list-go v0.2.0 // indirect
+	github.com/buger/jsonparser v1.1.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/dlclark/regexp2 v1.4.0 // indirect
 	github.com/fsnotify/fsnotify v1.6.0 // indirect
@@ -53,6 +56,7 @@ require (
 	github.com/itchyny/timefmt-go v0.1.5 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
+	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-colorable v0.1.12 // indirect
 	github.com/mattn/go-runewidth v0.0.14 // indirect
 	github.com/microcosm-cc/bluemonday v1.0.21 // indirect
diff --git a/go.sum b/go.sum
index 8a1dcbd..4b88847 100644
--- a/go.sum
+++ b/go.sum
@@ -58,8 +58,12 @@ github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZs
 github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
 github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
+github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
 github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc=
 github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
 github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
@@ -173,6 +177,7 @@ github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm
 github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
 github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
 github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -190,6 +195,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
 github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
@@ -287,6 +294,8 @@ github.com/tj/go-naturaldate v1.3.0 h1:OgJIPkR/Jk4bFMBLbxZ8w+QUxwjqSvzd9x+yXocY4
 github.com/tj/go-naturaldate v1.3.0/go.mod h1:rpUbjivDKiS1BlfMGc2qUKNZ/yxgthOfmytQs8d8hKk=
 github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
 github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
+github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
 github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
 github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
 github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
diff --git a/pkg/cli/cobra.go b/pkg/cli/cobra.go
index 1767620..fd0420c 100644
--- a/pkg/cli/cobra.go
+++ b/pkg/cli/cobra.go
@@ -569,7 +569,10 @@ func BuildCobraCommandFromGlazeCommand(cmd_ cmds.GlazeCommand) (*cobra.Command,
 			cobra.CheckErr(err)
 		}
 
-		err = gp.OutputFormatter().Output(ctx, os.Stdout)
+		err = gp.Processor().FinalizeTable(ctx)
+		cobra.CheckErr(err)
+
+		err = gp.OutputFormatter().Output(ctx, gp.Processor().GetTable(), os.Stdout)
 		cobra.CheckErr(err)
 		return nil
 	})
diff --git a/pkg/formatters/csv/csv.go b/pkg/formatters/csv/csv.go
index cc1bb6a..2f5f76a 100644
--- a/pkg/formatters/csv/csv.go
+++ b/pkg/formatters/csv/csv.go
@@ -5,15 +5,12 @@ import (
 	"encoding/csv"
 	"fmt"
 	"github.com/go-go-golems/glazed/pkg/formatters"
-	middlewares2 "github.com/go-go-golems/glazed/pkg/middlewares"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"io"
 	"os"
 )
 
 type OutputFormatter struct {
-	Table               *types.Table
-	middlewares         []middlewares2.TableMiddleware
 	OutputFile          string
 	OutputFileTemplate  string
 	OutputMultipleFiles bool
@@ -62,8 +59,6 @@ func WithOutputMultipleFiles(outputMultipleFiles bool) OutputFormatterOption {
 
 func NewCSVOutputFormatter(opts ...OutputFormatterOption) *OutputFormatter {
 	f := &OutputFormatter{
-		Table:       types.NewTable(),
-		middlewares: []middlewares2.TableMiddleware{},
 		WithHeaders: true,
 		Separator:   ',',
 	}
@@ -75,8 +70,6 @@ func NewCSVOutputFormatter(opts ...OutputFormatterOption) *OutputFormatter {
 
 func NewTSVOutputFormatter(opts ...OutputFormatterOption) *OutputFormatter {
 	f := &OutputFormatter{
-		Table:       types.NewTable(),
-		middlewares: []middlewares2.TableMiddleware{},
 		WithHeaders: true,
 		Separator:   '\t',
 	}
@@ -88,43 +81,9 @@ func NewTSVOutputFormatter(opts ...OutputFormatterOption) *OutputFormatter {
 	return f
 }
 
-func (f *OutputFormatter) GetTable() (*types.Table, error) {
-	return f.Table, nil
-}
-
-func (f *OutputFormatter) AddTableMiddleware(m middlewares2.TableMiddleware) {
-	f.middlewares = append(f.middlewares, m)
-}
-
-func (f *OutputFormatter) AddTableMiddlewareInFront(m middlewares2.TableMiddleware) {
-	f.middlewares = append([]middlewares2.TableMiddleware{m}, f.middlewares...)
-}
-
-func (f *OutputFormatter) AddTableMiddlewareAtIndex(i int, m middlewares2.TableMiddleware) {
-	f.middlewares = append(f.middlewares[:i], append([]middlewares2.TableMiddleware{m}, f.middlewares[i:]...)...)
-}
-
-func (f *OutputFormatter) AddRow(row types.Row) {
-	f.Table.Rows = append(f.Table.Rows, row)
-}
-
-func (f *OutputFormatter) SetColumnOrder(columns []types.FieldName) {
-	f.Table.Columns = columns
-}
-
-func (f *OutputFormatter) Output(ctx context.Context, w_ io.Writer) error {
-	f.Table.Finalize()
-
-	for _, middleware := range f.middlewares {
-		newTable, err := middleware.Process(f.Table)
-		if err != nil {
-			return err
-		}
-		f.Table = newTable
-	}
-
+func (f *OutputFormatter) Output(ctx context.Context, table_ *types.Table, w_ io.Writer) error {
 	if f.OutputMultipleFiles {
-		for i, row := range f.Table.Rows {
+		for i, row := range table_.Rows {
 			outputFileName, err := formatters.ComputeOutputFilename(f.OutputFile, f.OutputFileTemplate, row, i)
 			if err != nil {
 				return err
@@ -134,14 +93,16 @@ func (f *OutputFormatter) Output(ctx context.Context, w_ io.Writer) error {
 			if err != nil {
 				return err
 			}
-			defer f_.Close()
+			defer func(f_ *os.File) {
+				_ = f_.Close()
+			}(f_)
 
-			csvWriter, err := f.newCSVWriter(f_)
+			csvWriter, err := f.newCSVWriter(table_.Columns, f_)
 			if err != nil {
 				return err
 			}
 
-			err = f.writeRow(row, csvWriter)
+			err = f.writeRow(table_.Columns, row, csvWriter)
 			if err != nil {
 				return err
 			}
@@ -164,22 +125,24 @@ func (f *OutputFormatter) Output(ctx context.Context, w_ io.Writer) error {
 		if err != nil {
 			return err
 		}
-		defer f_.Close()
+		defer func(f_ *os.File) {
+			_ = f_.Close()
+		}(f_)
 
-		csvWriter, err = f.newCSVWriter(w_)
+		csvWriter, err = f.newCSVWriter(table_.Columns, w_)
 		if err != nil {
 			return err
 		}
 	} else {
 		var err error
-		csvWriter, err = f.newCSVWriter(w_)
+		csvWriter, err = f.newCSVWriter(table_.Columns, w_)
 		if err != nil {
 			return err
 		}
 	}
 
-	for _, row := range f.Table.Rows {
-		err2 := f.writeRow(row, csvWriter)
+	for _, row := range table_.Rows {
+		err2 := f.writeRow(table_.Columns, row, csvWriter)
 		if err2 != nil {
 			return err2
 		}
@@ -194,22 +157,22 @@ func (f *OutputFormatter) Output(ctx context.Context, w_ io.Writer) error {
 	return nil
 }
 
-func (f *OutputFormatter) newCSVWriter(w_ io.Writer) (*csv.Writer, error) {
+func (f *OutputFormatter) newCSVWriter(columns []types.FieldName, w_ io.Writer) (*csv.Writer, error) {
 	// create a buffer writer
 	w := csv.NewWriter(w_)
 	w.Comma = f.Separator
 
 	var err error
 	if f.WithHeaders {
-		err = w.Write(f.Table.Columns)
+		err = w.Write(columns)
 	}
 	return w, err
 }
 
-func (f *OutputFormatter) writeRow(row types.Row, w *csv.Writer) error {
+func (f *OutputFormatter) writeRow(columns []types.FieldName, row types.Row, w *csv.Writer) error {
 	values := []string{}
-	for _, column := range f.Table.Columns {
-		if v, ok := row.GetValues()[column]; ok {
+	for _, column := range columns {
+		if v, ok := row.Get(column); ok {
 			values = append(values, fmt.Sprintf("%v", v))
 		} else {
 			values = append(values, "")
diff --git a/pkg/formatters/excel/excel.go b/pkg/formatters/excel/excel.go
index 0f657bf..339a73e 100644
--- a/pkg/formatters/excel/excel.go
+++ b/pkg/formatters/excel/excel.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	strings2 "github.com/go-go-golems/glazed/pkg/helpers/strings"
-	"github.com/go-go-golems/glazed/pkg/middlewares"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/xuri/excelize/v2"
 	"io"
@@ -12,47 +11,11 @@ import (
 )
 
 type OutputFormatter struct {
-	SheetName   string
-	OutputFile  string
-	Table       *types.Table
-	middlewares []middlewares.TableMiddleware
+	SheetName  string
+	OutputFile string
 }
 
-func (E *OutputFormatter) GetTable() (*types.Table, error) {
-	return E.Table, nil
-}
-
-func (E *OutputFormatter) AddRow(row types.Row) {
-	E.Table.Rows = append(E.Table.Rows, row)
-}
-
-func (E *OutputFormatter) SetColumnOrder(columns []types.FieldName) {
-	E.Table.Columns = columns
-}
-
-func (E *OutputFormatter) AddTableMiddleware(mw middlewares.TableMiddleware) {
-	E.middlewares = append(E.middlewares, mw)
-}
-
-func (E *OutputFormatter) AddTableMiddlewareInFront(mw middlewares.TableMiddleware) {
-	E.middlewares = append([]middlewares.TableMiddleware{mw}, E.middlewares...)
-}
-
-func (E *OutputFormatter) AddTableMiddlewareAtIndex(i int, mw middlewares.TableMiddleware) {
-	E.middlewares = append(E.middlewares[:i], append([]middlewares.TableMiddleware{mw}, E.middlewares[i:]...)...)
-}
-
-func (E *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
-	E.Table.Finalize()
-
-	for _, middleware := range E.middlewares {
-		newTable, err := middleware.Process(E.Table)
-		if err != nil {
-			return err
-		}
-		E.Table = newTable
-	}
-
+func (E *OutputFormatter) Output(_ context.Context, table_ *types.Table, w io.Writer) error {
 	f := excelize.NewFile()
 	defer func() {
 		if err := f.Close(); err != nil {
@@ -82,7 +45,7 @@ func (E *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 		return err
 	}
 
-	for j, col := range E.Table.Columns {
+	for j, col := range table_.Columns {
 		colIndex := strings2.ToAlphaString(j + 1)
 		cellIndex := colIndex + "1"
 		rowKeyToColumn[col] = colIndex
@@ -97,10 +60,13 @@ func (E *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 		}
 	}
 
-	for i, row := range E.Table.Rows {
-		vals := row.GetValues()
-		for _, j := range E.Table.Columns {
-			val := vals[j]
+	for i, row := range table_.Rows {
+		for _, j := range table_.Columns {
+			val, present := row.Get(j)
+			if !present {
+				continue
+			}
+
 			colIndex := rowKeyToColumn[j]
 			cellIndex := colIndex + fmt.Sprint(i+2)
 
@@ -148,10 +114,7 @@ func WithOutputFile(outputFile string) OutputFormatterOption {
 }
 
 func NewOutputFormatter(opts ...OutputFormatterOption) *OutputFormatter {
-	f := &OutputFormatter{
-		Table:       types.NewTable(),
-		middlewares: []middlewares.TableMiddleware{},
-	}
+	f := &OutputFormatter{}
 
 	for _, opt := range opts {
 		opt(f)
diff --git a/pkg/formatters/formatter.go b/pkg/formatters/formatter.go
index 8969f6f..5d7b560 100644
--- a/pkg/formatters/formatter.go
+++ b/pkg/formatters/formatter.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"github.com/go-go-golems/glazed/pkg/helpers/templating"
-	"github.com/go-go-golems/glazed/pkg/middlewares"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/rs/zerolog/log"
 	"golang.org/x/sync/errgroup"
@@ -40,30 +39,23 @@ import (
 // The following is all geared towards tabulated output
 
 type OutputFormatter interface {
-	// TODO(manuel, 2022-11-12) We need to be able to output to a directory / to a stream / to multiple files
-	AddRow(row types.Row)
-
-	SetColumnOrder(columnOrder []types.FieldName)
-
-	// AddTableMiddleware adds a middleware at the end of the processing list
-	AddTableMiddleware(m middlewares.TableMiddleware)
-	AddTableMiddlewareInFront(m middlewares.TableMiddleware)
-	AddTableMiddlewareAtIndex(i int, m middlewares.TableMiddleware)
-
-	GetTable() (*types.Table, error)
-
-	Output(ctx context.Context, w io.Writer) error
-
+	Output(ctx context.Context, table *types.Table, w io.Writer) error
 	ContentType() string
 }
 
-func ComputeOutputFilename(outputFile string, outputFileTemplate string, row types.Row, index int) (string, error) {
+func ComputeOutputFilename(
+	outputFile string,
+	outputFileTemplate string,
+	row types.Row,
+	index int,
+) (string, error) {
 	var outputFileName string
 	if outputFileTemplate != "" {
 		data := map[string]interface{}{}
-		values := row.GetValues()
+		values := row
 
-		for k, v := range values {
+		for pair := values.Oldest(); pair != nil; pair = pair.Next() {
+			k, v := pair.Key, pair.Value
 			data[k] = v
 		}
 		data["rowIndex"] = index
@@ -87,6 +79,7 @@ func ComputeOutputFilename(outputFile string, outputFileTemplate string, row typ
 // into HTML when serving.
 func StartFormatIntoChannel[T interface{ ~string }](
 	ctx context.Context,
+	table *types.Table,
 	formatter OutputFormatter,
 ) <-chan T {
 	reader, writer := io.Pipe()
@@ -115,10 +108,12 @@ func StartFormatIntoChannel[T interface{ ~string }](
 	})
 
 	eg.Go(func() error {
-		err := formatter.Output(ctx2, writer)
-		defer writer.Close()
+		err := formatter.Output(ctx2, table, writer)
+		defer func(writer *io.PipeWriter) {
+			_ = writer.Close()
+		}(writer)
 		if err != nil {
-			writer.CloseWithError(err)
+			_ = writer.CloseWithError(err)
 			return err
 		}
 		return nil
diff --git a/pkg/formatters/json/json.go b/pkg/formatters/json/json.go
index e5d2a87..8d7ff09 100644
--- a/pkg/formatters/json/json.go
+++ b/pkg/formatters/json/json.go
@@ -5,7 +5,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"github.com/go-go-golems/glazed/pkg/formatters"
-	"github.com/go-go-golems/glazed/pkg/middlewares"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/ugorji/go/codec"
 	"io"
@@ -17,57 +16,19 @@ type OutputFormatter struct {
 	OutputFile           string
 	OutputFileTemplate   string
 	OutputMultipleFiles  bool
-	Table                *types.Table
-	middlewares          []middlewares.TableMiddleware
-}
-
-func (f *OutputFormatter) GetTable() (*types.Table, error) {
-	return f.Table, nil
-}
-
-func (f *OutputFormatter) AddRow(row types.Row) {
-	f.Table.Rows = append(f.Table.Rows, row)
-}
-
-func (f *OutputFormatter) SetColumnOrder(columns []types.FieldName) {
-	f.Table.Columns = columns
-}
-
-// TODO(manuel: 2023-04-25) This could actually all be cleaned up with OutputFormatterOption
-
-func (f *OutputFormatter) AddTableMiddleware(mw middlewares.TableMiddleware) {
-	f.middlewares = append(f.middlewares, mw)
-}
-
-func (f *OutputFormatter) AddTableMiddlewareInFront(mw middlewares.TableMiddleware) {
-	f.middlewares = append([]middlewares.TableMiddleware{mw}, f.middlewares...)
-}
-
-func (f *OutputFormatter) AddTableMiddlewareAtIndex(i int, mw middlewares.TableMiddleware) {
-	f.middlewares = append(f.middlewares[:i], append([]middlewares.TableMiddleware{mw}, f.middlewares[i:]...)...)
 }
 
 func (f *OutputFormatter) ContentType() string {
 	return "application/json"
 }
 
-func (f *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
-	f.Table.Finalize()
-
-	for _, middleware := range f.middlewares {
-		newTable, err := middleware.Process(f.Table)
-		if err != nil {
-			return err
-		}
-		f.Table = newTable
-	}
-
+func (f *OutputFormatter) Output(ctx context.Context, table_ *types.Table, w io.Writer) error {
 	if f.OutputMultipleFiles {
 		if f.OutputFileTemplate == "" && f.OutputFile == "" {
 			return fmt.Errorf("neither output file or output file template is set")
 		}
 
-		for i, row := range f.Table.Rows {
+		for i, row := range table_.Rows {
 			outputFileName, err := formatters.ComputeOutputFilename(f.OutputFile, f.OutputFileTemplate, row, i)
 			if err != nil {
 				return err
@@ -80,7 +41,7 @@ func (f *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 
 			encoder := json.NewEncoder(f_)
 			encoder.SetIndent("", "  ")
-			err = encoder.Encode(row.GetValues())
+			err = encoder.Encode(row)
 			if err != nil {
 				_ = f_.Close()
 				return err
@@ -104,10 +65,10 @@ func (f *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 	}
 
 	if f.OutputIndividualRows {
-		for _, row := range f.Table.Rows {
+		for _, row := range table_.Rows {
 			encoder := json.NewEncoder(w)
 			encoder.SetIndent("", "  ")
-			err := encoder.Encode(row.GetValues())
+			err := encoder.Encode(row)
 			if err != nil {
 				return err
 			}
@@ -126,13 +87,13 @@ func (f *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 			return err
 		}
 
-		rowCount := len(f.Table.Rows)
-		for i, row := range f.Table.Rows {
+		rowCount := len(table_.Rows)
+		for i, row := range table_.Rows {
 			// Reset the encoder to avoid memory leaks
 			enc.Reset(w)
 
 			// Encode each element in the array
-			err = enc.Encode(row.GetValues())
+			err = enc.Encode(row)
 			if err != nil {
 				return err
 			}
@@ -182,18 +143,10 @@ func WithOutputMultipleFiles(outputMultipleFiles bool) OutputFormatterOption {
 	}
 }
 
-func WithTable(table *types.Table) OutputFormatterOption {
-	return func(formatter *OutputFormatter) {
-		formatter.Table = table
-	}
-}
-
 func NewOutputFormatter(options ...OutputFormatterOption) *OutputFormatter {
 	ret := &OutputFormatter{
 		OutputIndividualRows: false,
-		Table:                types.NewTable(),
 		OutputFile:           "",
-		middlewares:          []middlewares.TableMiddleware{},
 	}
 
 	for _, option := range options {
diff --git a/pkg/formatters/simple/simple.go b/pkg/formatters/simple/simple.go
index dce1643..c6c73f0 100644
--- a/pkg/formatters/simple/simple.go
+++ b/pkg/formatters/simple/simple.go
@@ -4,20 +4,17 @@ import (
 	"context"
 	"fmt"
 	"github.com/go-go-golems/glazed/pkg/formatters"
-	"github.com/go-go-golems/glazed/pkg/middlewares"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"io"
 	"os"
 )
 
 type SingleColumnFormatter struct {
-	Table               *types.Table
 	Column              types.FieldName
 	OutputFile          string
 	OutputFileTemplate  string
 	OutputMultipleFiles bool
 	Separator           string
-	middlewares         []middlewares.TableMiddleware
 }
 
 type SingleColumnFormatterOption func(*SingleColumnFormatter)
@@ -48,7 +45,6 @@ func WithOutputFile(outputFile string) SingleColumnFormatterOption {
 
 func NewSingleColumnFormatter(column types.FieldName, opts ...SingleColumnFormatterOption) *SingleColumnFormatter {
 	f := &SingleColumnFormatter{
-		Table:     &types.Table{},
 		Column:    column,
 		Separator: "\n",
 	}
@@ -64,53 +60,19 @@ func (s *SingleColumnFormatter) ContentType() string {
 	return "text/plain"
 }
 
-func (s *SingleColumnFormatter) AddRow(row types.Row) {
-	s.Table.Rows = append(s.Table.Rows, row)
-}
-
-func (s *SingleColumnFormatter) SetColumnOrder(columnOrder []types.FieldName) {
-	s.Table.Columns = columnOrder
-}
-
-func (s *SingleColumnFormatter) AddTableMiddleware(m middlewares.TableMiddleware) {
-	s.middlewares = append(s.middlewares, m)
-}
-
-func (s *SingleColumnFormatter) AddTableMiddlewareInFront(m middlewares.TableMiddleware) {
-	s.middlewares = append([]middlewares.TableMiddleware{m}, s.middlewares...)
-}
-
-func (s *SingleColumnFormatter) AddTableMiddlewareAtIndex(i int, m middlewares.TableMiddleware) {
-	s.middlewares = append(s.middlewares[:i], append([]middlewares.TableMiddleware{m}, s.middlewares[i:]...)...)
-}
-
-func (s *SingleColumnFormatter) GetTable() (*types.Table, error) {
-	return s.Table, nil
-}
-
-func (s *SingleColumnFormatter) Output(ctx context.Context, w io.Writer) error {
-	s.Table.Finalize()
-
-	for _, middleware := range s.middlewares {
-		newTable, err := middleware.Process(s.Table)
-		if err != nil {
-			return err
-		}
-		s.Table = newTable
-	}
-
+func (s *SingleColumnFormatter) Output(ctx context.Context, table_ *types.Table, w io.Writer) error {
 	if s.OutputMultipleFiles {
 		if s.OutputFileTemplate == "" && s.OutputFile == "" {
 			return fmt.Errorf("neither output file or output file template is set")
 		}
 
-		for i, row := range s.Table.Rows {
+		for i, row := range table_.Rows {
 			outputFileName, err := formatters.ComputeOutputFilename(s.OutputFile, s.OutputFileTemplate, row, i)
 			if err != nil {
 				return err
 			}
 
-			if s_, ok := row.GetValues()[s.Column]; ok {
+			if s_, ok := row.Get(s.Column); ok {
 				v := fmt.Sprintf("%v", s_)
 				err = os.WriteFile(outputFileName, []byte(v), 0644)
 				if err != nil {
@@ -133,13 +95,13 @@ func (s *SingleColumnFormatter) Output(ctx context.Context, w io.Writer) error {
 		w = f_
 	}
 
-	for i, row := range s.Table.Rows {
-		if value, ok := row.GetValues()[s.Column]; ok {
+	for i, row := range table_.Rows {
+		if value, ok := row.Get(s.Column); ok {
 			_, err := fmt.Fprintf(w, "%v", value)
 			if err != nil {
 				return err
 			}
-			if i < len(s.Table.Rows)-1 {
+			if i < len(table_.Rows)-1 {
 				_, err := fmt.Fprintf(w, "%s", s.Separator)
 				if err != nil {
 					return err
diff --git a/pkg/formatters/table/table.go b/pkg/formatters/table/table.go
index 7357405..6f487fc 100644
--- a/pkg/formatters/table/table.go
+++ b/pkg/formatters/table/table.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"github.com/go-go-golems/glazed/pkg/formatters"
 	"github.com/go-go-golems/glazed/pkg/helpers/cast"
-	"github.com/go-go-golems/glazed/pkg/middlewares"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/jedib0t/go-pretty/table"
 	"github.com/jedib0t/go-pretty/text"
@@ -41,10 +40,8 @@ var (
 )
 
 type OutputFormatter struct {
-	Table               *types.Table
 	OutputFileTemplate  string
 	OutputMultipleFiles bool
-	middlewares         []middlewares.TableMiddleware
 	TableFormat         string
 	TableStyle          table.Style
 	TableStyleFile      string
@@ -98,8 +95,6 @@ func WithPrintTableStyle(printTableStyle bool) OutputFormatterOption {
 
 func NewOutputFormatter(tableFormat string, opts ...OutputFormatterOption) *OutputFormatter {
 	f := &OutputFormatter{
-		Table:       types.NewTable(),
-		middlewares: []middlewares.TableMiddleware{},
 		TableFormat: tableFormat,
 		TableStyle:  table.StyleDefault,
 	}
@@ -126,27 +121,13 @@ func (tof *OutputFormatter) ContentType() string {
 	}
 }
 
-func (tof *OutputFormatter) GetTable() (*types.Table, error) {
-	return tof.Table, nil
-}
-
-func (tof *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
-	tof.Table.Finalize()
-
-	for _, middleware := range tof.middlewares {
-		newTable, err := middleware.Process(tof.Table)
-		if err != nil {
-			return err
-		}
-		tof.Table = newTable
-	}
-
+func (tof *OutputFormatter) Output(ctx context.Context, table_ *types.Table, w io.Writer) error {
 	if tof.OutputMultipleFiles {
 		if tof.OutputFileTemplate == "" && tof.OutputFile == "" {
 			return fmt.Errorf("neither output file or output file template is set")
 		}
 
-		for i, row := range tof.Table.Rows {
+		for i, row := range table_.Rows {
 			outputFileName, err := formatters.ComputeOutputFilename(tof.OutputFile, tof.OutputFileTemplate, row, i)
 			if err != nil {
 				return err
@@ -157,7 +138,7 @@ func (tof *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 				return err
 			}
 
-			err = tof.makeTable([]types.Row{row}, f_)
+			err = tof.makeTable(table_, []types.Row{row}, f_)
 			if err != nil {
 				return err
 			}
@@ -173,7 +154,7 @@ func (tof *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 		if err != nil {
 			return err
 		}
-		err = tof.makeTable(tof.Table.Rows, f_)
+		err = tof.makeTable(table_, table_.Rows, f_)
 		if err != nil {
 			return err
 		}
@@ -182,7 +163,7 @@ func (tof *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 		return nil
 	}
 
-	err := tof.makeTable(tof.Table.Rows, w)
+	err := tof.makeTable(table_, table_.Rows, w)
 	if err != nil {
 		return err
 	}
@@ -190,18 +171,17 @@ func (tof *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 	return nil
 }
 
-func (tof *OutputFormatter) makeTable(rows []types.Row, w io.Writer) error {
+func (tof *OutputFormatter) makeTable(table_ *types.Table, rows []types.Row, w io.Writer) error {
 	t := table.NewWriter()
 
-	headers, _ := cast.CastList[interface{}](tof.Table.Columns)
+	headers, _ := cast.CastList[interface{}](table_.Columns)
 
 	t.AppendHeader(headers)
 	for _, row := range rows {
-		values := row.GetValues()
 		var row_ []interface{}
-		for _, column := range tof.Table.Columns {
+		for _, column := range table_.Columns {
 			s := ""
-			if v, ok := values[column]; ok {
+			if v, ok := row.Get(column); ok {
 				s = valueToString(v)
 			}
 			row_ = append(row_, s)
@@ -275,23 +255,3 @@ func valueToString(v types.GenericCellValue) string {
 	}
 	return s
 }
-
-func (tof *OutputFormatter) AddTableMiddleware(m middlewares.TableMiddleware) {
-	tof.middlewares = append(tof.middlewares, m)
-}
-
-func (tof *OutputFormatter) AddTableMiddlewareInFront(m middlewares.TableMiddleware) {
-	tof.middlewares = append([]middlewares.TableMiddleware{m}, tof.middlewares...)
-}
-
-func (tof *OutputFormatter) AddTableMiddlewareAtIndex(i int, m middlewares.TableMiddleware) {
-	tof.middlewares = append(tof.middlewares[:i], append([]middlewares.TableMiddleware{m}, tof.middlewares[i:]...)...)
-}
-
-func (tof *OutputFormatter) AddRow(row types.Row) {
-	tof.Table.Rows = append(tof.Table.Rows, row)
-}
-
-func (tof *OutputFormatter) SetColumnOrder(columnOrder []types.FieldName) {
-	tof.Table.Columns = columnOrder
-}
diff --git a/pkg/formatters/template/template.go b/pkg/formatters/template/template.go
index 6edc160..b05b1ff 100644
--- a/pkg/formatters/template/template.go
+++ b/pkg/formatters/template/template.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"github.com/go-go-golems/glazed/pkg/formatters"
-	"github.com/go-go-golems/glazed/pkg/middlewares"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"io"
 	"os"
@@ -13,50 +12,14 @@ import (
 
 type OutputFormatter struct {
 	Template            string
-	Table               *types.Table
 	TemplateFuncMaps    []template.FuncMap
 	OutputFileTemplate  string
 	OutputMultipleFiles bool
-	middlewares         []middlewares.TableMiddleware
 	OutputFile          string
 	AdditionalData      interface{}
 }
 
-func (t *OutputFormatter) GetTable() (*types.Table, error) {
-	return t.Table, nil
-}
-
-func (t *OutputFormatter) AddRow(row types.Row) {
-	t.Table.Rows = append(t.Table.Rows, row)
-}
-
-func (t *OutputFormatter) SetColumnOrder(columnOrder []types.FieldName) {
-	t.Table.Columns = columnOrder
-}
-
-func (t *OutputFormatter) AddTableMiddleware(m middlewares.TableMiddleware) {
-	t.middlewares = append(t.middlewares, m)
-}
-
-func (t *OutputFormatter) AddTableMiddlewareInFront(m middlewares.TableMiddleware) {
-	t.middlewares = append([]middlewares.TableMiddleware{m}, t.middlewares...)
-}
-
-func (t *OutputFormatter) AddTableMiddlewareAtIndex(i int, m middlewares.TableMiddleware) {
-	t.middlewares = append(t.middlewares[:i], append([]middlewares.TableMiddleware{m}, t.middlewares[i:]...)...)
-}
-
-func (t *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
-	t.Table.Finalize()
-
-	for _, middleware := range t.middlewares {
-		newTable, err := middleware.Process(t.Table)
-		if err != nil {
-			return err
-		}
-		t.Table = newTable
-	}
-
+func (t *OutputFormatter) Output(ctx context.Context, table_ *types.Table, w io.Writer) error {
 	t2 := template.New("template")
 	for _, templateFuncMap := range t.TemplateFuncMaps {
 		t2 = t2.Funcs(templateFuncMap)
@@ -71,7 +34,7 @@ func (t *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 			return fmt.Errorf("neither output file or output file template is set")
 		}
 
-		for i, row := range t.Table.Rows {
+		for i, row := range table_.Rows {
 			outputFileName, err := formatters.ComputeOutputFilename(t.OutputFile, t.OutputFileTemplate, row, i)
 			if err != nil {
 				return err
@@ -81,11 +44,14 @@ func (t *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 			if err != nil {
 				return err
 			}
-			defer f_.Close()
+			defer func(f_ *os.File) {
+				_ = f_.Close()
+			}(f_)
 
-			tableData := []map[types.FieldName]interface{}{row.GetValues()}
+			tableData := []types.Row{row}
 
 			data := map[string]interface{}{
+				// TODO(manuel, 2023-06-25) Convert to normal maps for templating
 				"rows": tableData,
 				"data": t.AdditionalData,
 			}
@@ -101,11 +67,19 @@ func (t *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 		return nil
 	}
 
-	var tableData []map[types.FieldName]interface{}
-	for _, row := range t.Table.Rows {
-		tableData = append(tableData, row.GetValues())
+	var tableData []map[string]interface{}
+
+	for _, row := range table_.Rows {
+		m := make(map[string]interface{})
+
+		for pair := row.Oldest(); pair != nil; pair = pair.Next() {
+			m[pair.Key] = pair.Value
+		}
+
+		tableData = append(tableData, m)
 	}
 	data := map[string]interface{}{
+		// TODO(manuel, 2023-06-25) Convert to normal maps for templating
 		"rows": tableData,
 		"data": t.AdditionalData,
 	}
@@ -115,7 +89,9 @@ func (t *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 		if err != nil {
 			return err
 		}
-		defer f_.Close()
+		defer func(f_ *os.File) {
+			_ = f_.Close()
+		}(f_)
 
 		w = f_
 	}
@@ -172,7 +148,6 @@ func NewOutputFormatter(template string, opts ...OutputFormatterOption) *OutputF
 	f := &OutputFormatter{
 		Template:       template,
 		AdditionalData: map[string]interface{}{},
-		Table:          types.NewTable(),
 	}
 
 	for _, opt := range opts {
diff --git a/pkg/formatters/yaml/yaml.go b/pkg/formatters/yaml/yaml.go
index 7b4d3e4..2d48dd8 100644
--- a/pkg/formatters/yaml/yaml.go
+++ b/pkg/formatters/yaml/yaml.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"github.com/go-go-golems/glazed/pkg/formatters"
-	"github.com/go-go-golems/glazed/pkg/middlewares"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"gopkg.in/yaml.v3"
 	"io"
@@ -12,55 +11,19 @@ import (
 )
 
 type OutputFormatter struct {
-	Table                *types.Table
 	OutputFile           string
 	OutputFileTemplate   string
 	OutputMultipleFiles  bool
 	OutputIndividualRows bool
-	middlewares          []middlewares.TableMiddleware
 }
 
-func (f *OutputFormatter) GetTable() (*types.Table, error) {
-	return f.Table, nil
-}
-
-func (f *OutputFormatter) AddRow(row types.Row) {
-	f.Table.Rows = append(f.Table.Rows, row)
-}
-
-func (f *OutputFormatter) SetColumnOrder(columns []types.FieldName) {
-	f.Table.Columns = columns
-}
-
-func (f *OutputFormatter) AddTableMiddleware(mw middlewares.TableMiddleware) {
-	f.middlewares = append(f.middlewares, mw)
-}
-
-func (f *OutputFormatter) AddTableMiddlewareInFront(mw middlewares.TableMiddleware) {
-	f.middlewares = append([]middlewares.TableMiddleware{mw}, f.middlewares...)
-}
-
-func (f *OutputFormatter) AddTableMiddlewareAtIndex(i int, mw middlewares.TableMiddleware) {
-	f.middlewares = append(f.middlewares[:i], append([]middlewares.TableMiddleware{mw}, f.middlewares[i:]...)...)
-}
-
-func (f *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
-	f.Table.Finalize()
-
-	for _, middleware := range f.middlewares {
-		newTable, err := middleware.Process(f.Table)
-		if err != nil {
-			return err
-		}
-		f.Table = newTable
-	}
-
+func (f *OutputFormatter) Output(ctx context.Context, table_ *types.Table, w io.Writer) error {
 	if f.OutputMultipleFiles {
 		if f.OutputFileTemplate == "" && f.OutputFile == "" {
 			return fmt.Errorf("neither output file or output file template is set")
 		}
 
-		for i, row := range f.Table.Rows {
+		for i, row := range table_.Rows {
 			outputFileName, err := formatters.ComputeOutputFilename(f.OutputFile, f.OutputFileTemplate, row, i)
 			if err != nil {
 				return err
@@ -71,22 +34,24 @@ func (f *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 				return err
 			}
 
+			defer func(f_ *os.File) {
+				_ = f_.Close()
+			}(f_)
+
 			encoder := yaml.NewEncoder(f_)
-			err = encoder.Encode(row.GetValues())
+			err = encoder.Encode(row)
 			if err != nil {
-				f_.Close()
 				return err
 			}
 
 			_, _ = fmt.Fprintf(w, "Wrote output to %s\n", outputFileName)
-			f_.Close()
 		}
 
 		return nil
 	}
 
 	if f.OutputIndividualRows {
-		if len(f.Table.Rows) > 1 {
+		if len(table_.Rows) > 1 {
 			return fmt.Errorf("output individual rows is set but there are multiple rows in the table")
 		}
 
@@ -96,26 +61,26 @@ func (f *OutputFormatter) Output(ctx context.Context, w io.Writer) error {
 				return err
 			}
 			w = f_
-			defer f_.Close()
+			defer func(f_ *os.File) {
+				_ = f_.Close()
+			}(f_)
 
-			if len(f.Table.Rows) == 0 {
+			if len(table_.Rows) == 0 {
 				_, _ = fmt.Fprintln(w, "Empty table, an empty file was created")
 				return nil
 			}
 		}
 
 		encoder := yaml.NewEncoder(w)
-		err := encoder.Encode(f.Table.Rows[0].GetValues())
+		err := encoder.Encode(table_.Rows[0])
 		if err != nil {
 			return err
 		}
 
 		return nil
 	} else {
-		var rows []map[string]interface{}
-		for _, row := range f.Table.Rows {
-			rows = append(rows, row.GetValues())
-		}
+		var rows []types.Row
+		rows = append(rows, table_.Rows...)
 
 		encoder := yaml.NewEncoder(w)
 		err := encoder.Encode(rows)
@@ -158,10 +123,7 @@ func WithOutputIndividualRows(outputIndividualRows bool) OutputFormatterOption {
 }
 
 func NewOutputFormatter(opts ...OutputFormatterOption) *OutputFormatter {
-	f := &OutputFormatter{
-		Table:       types.NewTable(),
-		middlewares: []middlewares.TableMiddleware{},
-	}
+	f := &OutputFormatter{}
 
 	for _, opt := range opts {
 		opt(f)
diff --git a/pkg/helpers/assert/assert.go b/pkg/helpers/assert/assert.go
new file mode 100644
index 0000000..15a27cd
--- /dev/null
+++ b/pkg/helpers/assert/assert.go
@@ -0,0 +1,55 @@
+package assert
+
+import (
+	"github.com/go-go-golems/glazed/pkg/types"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func EqualMapRowValue(t *testing.T, expected interface{}, obj types.Row, key string) {
+	v, ok := obj.Get(key)
+	assert.True(t, ok)
+	assert.Equal(t, expected, v)
+}
+
+func NilMapRowValue(t *testing.T, obj types.Row, key string) {
+	_, ok := obj.Get(key)
+	assert.False(t, ok)
+}
+
+func EqualMapRows(t *testing.T, expected types.Row, actual types.Row) {
+	assert.Equal(t, expected.Len(), actual.Len())
+
+	// test one side
+	for pair, actualPair := expected.Oldest(), actual.Oldest(); actualPair != nil && pair != nil; pair, actualPair = pair.Next(), actualPair.Next() {
+		k, v := pair.Key, pair.Value
+		actualK, actualV := actualPair.Key, actualPair.Value
+		assert.Equal(t, k, actualK)
+		assert.Equal(t, v, actualV)
+	}
+}
+
+func EqualMapRowValues(t *testing.T, obj types.Row, values map[types.FieldName]types.GenericCellValue) {
+	for k, v := range values {
+		v_, ok := obj.Get(k)
+		assert.True(t, ok)
+		assert.Equal(t, v, v_)
+	}
+}
+
+func EqualMapRowMap(t *testing.T, expected map[types.FieldName]types.GenericCellValue, actual types.Row) {
+	// test one side
+	for k, v := range expected {
+		v_, ok := actual.Get(k)
+		assert.True(t, ok)
+		assert.Equal(t, v, v_)
+	}
+
+	// test other way round
+	for pair := actual.Oldest(); pair != nil; pair = pair.Next() {
+		k, v := pair.Key, pair.Value
+		v_, ok := expected[k]
+		assert.True(t, ok)
+		assert.Equal(t, v, v_)
+	}
+}
diff --git a/pkg/middlewares/jq.go b/pkg/middlewares/jq.go
index ebc88dd..40b30ff 100644
--- a/pkg/middlewares/jq.go
+++ b/pkg/middlewares/jq.go
@@ -1,6 +1,7 @@
 package middlewares
 
 import (
+	"context"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/itchyny/gojq"
 	"github.com/pkg/errors"
@@ -31,12 +32,18 @@ func NewJqObjectMiddleware(
 }
 
 func (jqm *JqObjectMiddleware) Process(
-	object map[string]interface{},
-) ([]map[string]interface{}, error) {
-	ret := []map[string]interface{}{}
+	ctx context.Context,
+	object types.Row,
+) ([]types.Row, error) {
+	ret := []types.Row{}
 
 	if jqm.query != nil {
-		iter := jqm.query.Run(object)
+		// TODO(manuel, 2023-06-25) Transform to map before passing to jq
+		m := map[string]interface{}{}
+		for pair := object.Oldest(); pair != nil; pair = pair.Next() {
+			m[pair.Key] = pair.Value
+		}
+		iter := jqm.query.Run(m)
 
 		for {
 			v, ok := iter.Next()
@@ -44,33 +51,32 @@ func (jqm *JqObjectMiddleware) Process(
 				break
 			}
 
-			if err, ok := v.(error); ok {
-				return nil, err
-			}
-
-			// if the result is an array, flatten it into ret
-			if array, ok := v.([]interface{}); ok {
-				for _, v := range array {
-					if err, ok := v.(error); ok {
-						return nil, err
-					}
-
-					object, ok := v.(map[string]interface{})
-					if !ok {
+			switch v_ := v.(type) {
+			case error:
+				return nil, v_
+
+			case []interface{}:
+				for _, v := range v_ {
+					switch v_ := v.(type) {
+					case error:
+						return nil, v_
+					case map[string]interface{}:
+						ret = append(ret, types.NewRowFromMap(v_))
+					case types.Row:
+						ret = append(ret, v_)
+					default:
 						return nil, errors.Errorf("Expected object, got %T", v)
 					}
-					ret = append(ret, object)
 				}
 
 				continue
-			} else {
-				object, ok = v.(map[string]interface{})
-				if !ok {
-					return nil, errors.Errorf("Expected object, got %T", v)
-				}
-			}
+			case types.Row:
+				ret = append(ret, v_)
+
+			case map[string]interface{}:
+				ret = append(ret, types.NewRowFromMap(v_))
 
-			ret = append(ret, object)
+			}
 		}
 	} else {
 		ret = append(ret, object)
@@ -103,7 +109,7 @@ func NewJqTableMiddleware(
 	return ret, nil
 }
 
-func (jqm *JqTableMiddleware) Process(table *types.Table) (*types.Table, error) {
+func (jqm *JqTableMiddleware) Process(ctx context.Context, table *types.Table) (*types.Table, error) {
 	ret := &types.Table{
 		Columns: []types.FieldName{},
 		Rows:    []types.Row{},
@@ -112,15 +118,14 @@ func (jqm *JqTableMiddleware) Process(table *types.Table) (*types.Table, error)
 	ret.Columns = append(ret.Columns, table.Columns...)
 
 	for _, row := range table.Rows {
-		values := row.GetValues()
-		newRow := types.SimpleRow{
-			Hash: map[types.FieldName]types.GenericCellValue{},
-		}
+		values := row
+		newRow := types.NewRow()
 
-		for rowField, value := range values {
+		for pair := values.Oldest(); pair != nil; pair = pair.Next() {
+			rowField, value := pair.Key, pair.Value
 			query, ok := jqm.fieldQueries[rowField]
 			if !ok {
-				newRow.Hash[rowField] = value
+				newRow.Set(rowField, value)
 				continue
 			}
 
@@ -139,11 +144,11 @@ func (jqm *JqTableMiddleware) Process(table *types.Table) (*types.Table, error)
 					return nil, err
 				}
 
-				newRow.Hash[rowField] = v
+				newRow.Set(rowField, v)
 			}
 		}
 
-		ret.Rows = append(ret.Rows, &newRow)
+		ret.Rows = append(ret.Rows, newRow)
 	}
 
 	return ret, nil
diff --git a/pkg/middlewares/mod.go b/pkg/middlewares/mod.go
index 0ac2df0..c561607 100644
--- a/pkg/middlewares/mod.go
+++ b/pkg/middlewares/mod.go
@@ -1,16 +1,21 @@
 package middlewares
 
 import (
+	"context"
 	"github.com/go-go-golems/glazed/pkg/types"
 )
 
 type TableMiddleware interface {
 	// Process transforms a full table
-	Process(table *types.Table) (*types.Table, error)
+	Process(ctx context.Context, table *types.Table) (*types.Table, error)
 }
 
 type ObjectMiddleware interface {
 	// Process transforms each individual object. Each object can return multiple
 	// objects which will get processed individually downstream.
-	Process(object map[string]interface{}) ([]map[string]interface{}, error)
+	Process(ctx context.Context, object types.Row) ([]types.Row, error)
+}
+
+type RowMiddleware interface {
+	Process(ctx context.Context, row types.Row) ([]types.Row, error)
 }
diff --git a/pkg/middlewares/object/object.go b/pkg/middlewares/object/template.go
similarity index 68%
rename from pkg/middlewares/object/object.go
rename to pkg/middlewares/object/template.go
index cc60377..f566ba8 100644
--- a/pkg/middlewares/object/object.go
+++ b/pkg/middlewares/object/template.go
@@ -2,26 +2,27 @@ package object
 
 import (
 	"bytes"
+	"context"
 	"github.com/Masterminds/sprig"
 	"github.com/go-go-golems/glazed/pkg/helpers/templating"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"text/template"
 )
 
-type ObjectGoTemplateMiddleware struct {
+type TemplateMiddleware struct {
 	templates map[types.FieldName]*template.Template
 }
 
-// NewObjectGoTemplateMiddleware creates a new template firmware used to process
+// NewTemplateMiddleware creates a new template firmware used to process
 // individual objects.
 //
 // It will render the template for each object and return a single field.
 //
 // TODO(manuel, 2023-02-02) Add support for passing in custom funcmaps
 // See #110 https://github.com/go-go-golems/glazed/issues/110
-func NewObjectGoTemplateMiddleware(
+func NewTemplateMiddleware(
 	templateStrings map[types.FieldName]string,
-) (*ObjectGoTemplateMiddleware, error) {
+) (*TemplateMiddleware, error) {
 	templates := map[types.FieldName]*template.Template{}
 	for columnName, templateString := range templateStrings {
 		tmpl, err := template.New("row").
@@ -34,7 +35,7 @@ func NewObjectGoTemplateMiddleware(
 		templates[columnName] = tmpl
 	}
 
-	return &ObjectGoTemplateMiddleware{
+	return &TemplateMiddleware{
 		templates: templates,
 	}, nil
 }
@@ -42,17 +43,22 @@ func NewObjectGoTemplateMiddleware(
 // Process will render each template for the input object and return an object with the newly created fields.
 //
 // TODO(manuel, 2022-11-21) This should allow merging the new results straight back
-func (rgtm *ObjectGoTemplateMiddleware) Process(object map[string]interface{}) ([]map[string]interface{}, error) {
-	ret := map[string]interface{}{}
+func (rgtm *TemplateMiddleware) Process(ctx context.Context, object types.Row) ([]types.Row, error) {
+	ret := types.NewRow()
 
 	for key, tmpl := range rgtm.templates {
 		var buf bytes.Buffer
-		err := tmpl.Execute(&buf, object)
+		m := map[string]interface{}{}
+
+		for pair := object.Oldest(); pair != nil; pair = pair.Next() {
+			m[pair.Key] = pair.Value
+		}
+		err := tmpl.Execute(&buf, m)
 		if err != nil {
 			return nil, err
 		}
-		ret[key] = buf.String()
+		ret.Set(key, buf.String())
 	}
 
-	return []map[string]interface{}{ret}, nil
+	return []types.Row{ret}, nil
 }
diff --git a/pkg/middlewares/processor.go b/pkg/middlewares/processor.go
new file mode 100644
index 0000000..0227c4e
--- /dev/null
+++ b/pkg/middlewares/processor.go
@@ -0,0 +1,151 @@
+package middlewares
+
+import (
+	"context"
+	"github.com/go-go-golems/glazed/pkg/types"
+)
+
+type Processor struct {
+	TableMiddlewares  []TableMiddleware
+	ObjectMiddlewares []ObjectMiddleware
+	RowMiddlewares    []RowMiddleware
+
+	Table *types.Table
+}
+
+type ProcessorOption func(*Processor)
+
+func WithTableMiddleware(tm ...TableMiddleware) ProcessorOption {
+	return func(p *Processor) {
+		p.TableMiddlewares = append(p.TableMiddlewares, tm...)
+	}
+}
+
+func WIthPrependTableMiddleware(tm ...TableMiddleware) ProcessorOption {
+	return func(p *Processor) {
+		p.TableMiddlewares = append(tm, p.TableMiddlewares...)
+	}
+}
+
+func WithObjectMiddleware(om ...ObjectMiddleware) ProcessorOption {
+	return func(p *Processor) {
+		p.ObjectMiddlewares = append(p.ObjectMiddlewares, om...)
+	}
+}
+
+func WithPrependObjectMiddleware(om ...ObjectMiddleware) ProcessorOption {
+	return func(p *Processor) {
+		p.ObjectMiddlewares = append(om, p.ObjectMiddlewares...)
+	}
+}
+
+func WithRowMiddleware(rm ...RowMiddleware) ProcessorOption {
+	return func(p *Processor) {
+		p.RowMiddlewares = append(p.RowMiddlewares, rm...)
+	}
+}
+
+func WithPrependRowMiddleware(rm ...RowMiddleware) ProcessorOption {
+	return func(p *Processor) {
+		p.RowMiddlewares = append(rm, p.RowMiddlewares...)
+	}
+}
+
+func NewProcessor(options ...ProcessorOption) *Processor {
+	ret := &Processor{
+		Table: types.NewTable(),
+	}
+
+	for _, option := range options {
+		option(ret)
+	}
+
+	return ret
+}
+
+func (p *Processor) GetTable() *types.Table {
+	return p.Table
+}
+
+func (p *Processor) FinalizeTable(ctx context.Context) error {
+	for _, tm := range p.TableMiddlewares {
+		table, err := tm.Process(ctx, p.Table)
+		if err != nil {
+			return err
+		}
+		p.Table = table
+	}
+	return nil
+}
+
+// AddRow runs row through the chain of ObjectMiddlewares, then RowMiddlewares and
+// adds the resulting rows to the table.
+func (p *Processor) AddRow(ctx context.Context, row types.Row) error {
+	rows := []types.Row{row}
+
+	for _, ow := range p.ObjectMiddlewares {
+		newRows := []types.Row{}
+		for _, row_ := range rows {
+			rows_, err := ow.Process(ctx, row_)
+			if err != nil {
+				return err
+			}
+			newRows = append(newRows, rows_...)
+		}
+
+		rows = newRows
+	}
+
+	for _, mw := range p.RowMiddlewares {
+		newRows := []types.Row{}
+		for _, row_ := range rows {
+			rows_, err := mw.Process(ctx, row_)
+			if err != nil {
+				return err
+			}
+			newRows = append(newRows, rows_...)
+		}
+
+		rows = newRows
+	}
+
+	p.Table.AddRows(rows...)
+
+	return nil
+}
+
+func (p *Processor) AddObjectMiddleware(mw ...ObjectMiddleware) {
+	p.ObjectMiddlewares = append(p.ObjectMiddlewares, mw...)
+}
+
+func (p *Processor) AddObjectMiddlewareInFront(mw ...ObjectMiddleware) {
+	p.ObjectMiddlewares = append(mw, p.ObjectMiddlewares...)
+}
+
+func (p *Processor) AddObjectMiddlewareAtIndex(i int, mw ...ObjectMiddleware) {
+	p.ObjectMiddlewares = append(p.ObjectMiddlewares[:i], append(mw, p.ObjectMiddlewares[i:]...)...)
+}
+
+func (p *Processor) AddRowMiddleware(mw ...RowMiddleware) {
+	p.RowMiddlewares = append(p.RowMiddlewares, mw...)
+}
+
+func (p *Processor) AddRowMiddlewareInFront(mw ...RowMiddleware) {
+	p.RowMiddlewares = append(mw, p.RowMiddlewares...)
+}
+
+func (p *Processor) AddRowMiddlewareAtIndex(i int, mw ...RowMiddleware) {
+	p.RowMiddlewares = append(p.RowMiddlewares[:i], append(mw, p.RowMiddlewares[i:]...)...)
+}
+
+func (p *Processor) AddTableMiddleware(mw ...TableMiddleware) {
+	p.TableMiddlewares = append(p.TableMiddlewares, mw...)
+}
+
+func (p *Processor) AddTableMiddlewareInFront(mw ...TableMiddleware) {
+	p.TableMiddlewares = append(mw, p.TableMiddlewares...)
+}
+
+func (p *Processor) AddTableMiddlewareAtIndex(i int, mw ...TableMiddleware) {
+	p.TableMiddlewares = append(p.TableMiddlewares[:i], append(mw, p.TableMiddlewares[i:]...)...)
+}
diff --git a/pkg/middlewares/row/add-field.go b/pkg/middlewares/row/add-field.go
new file mode 100644
index 0000000..be48d58
--- /dev/null
+++ b/pkg/middlewares/row/add-field.go
@@ -0,0 +1,26 @@
+package row
+
+import (
+	"context"
+	"github.com/go-go-golems/glazed/pkg/types"
+)
+
+type AddFieldMiddleware struct {
+	Fields map[string]string
+}
+
+func NewAddFieldMiddleware(fields map[string]string) *AddFieldMiddleware {
+	return &AddFieldMiddleware{Fields: fields}
+}
+
+func (a *AddFieldMiddleware) Process(ctx context.Context, row types.Row) ([]types.Row, error) {
+	newValues := types.NewRow()
+	for pair := row.Oldest(); pair != nil; pair = pair.Next() {
+		key, value := pair.Key, pair.Value
+		newValues.Set(key, value)
+	}
+	for key, value := range a.Fields {
+		newValues.Set(key, value)
+	}
+	return []types.Row{newValues}, nil
+}
diff --git a/pkg/middlewares/row/fields-filter.go b/pkg/middlewares/row/fields-filter.go
new file mode 100644
index 0000000..48925f2
--- /dev/null
+++ b/pkg/middlewares/row/fields-filter.go
@@ -0,0 +1,130 @@
+package row
+
+import (
+	"context"
+	"github.com/go-go-golems/glazed/pkg/types"
+	"strings"
+)
+
+// FieldsFilterMiddleware keeps columns that are in the fields list and removes
+// columns that are in the filters list.
+//
+// empty lists means that all columns are accepted.
+//
+// The returned rows are SimpleRows
+type FieldsFilterMiddleware struct {
+	fields        map[string]interface{}
+	filters       map[string]interface{}
+	prefixFields  []string
+	prefixFilters []string
+
+	newColumns map[types.FieldName]interface{}
+}
+
+func NewFieldsFilterMiddleware(fields []string, filters []string) *FieldsFilterMiddleware {
+	fieldHash := map[string]interface{}{}
+	prefixFields := []string{}
+	prefixFilters := []string{}
+
+	for _, field := range fields {
+		if strings.HasSuffix(field, ".") {
+			prefixFields = append(prefixFields, field)
+		} else {
+			fieldHash[field] = nil
+		}
+	}
+	filterHash := map[string]interface{}{}
+	for _, filter := range filters {
+		if strings.HasSuffix(filter, ".") {
+			prefixFilters = append(prefixFilters, filter)
+		} else {
+			filterHash[filter] = nil
+		}
+	}
+	return &FieldsFilterMiddleware{
+		fields:        fieldHash,
+		filters:       filterHash,
+		prefixFields:  prefixFields,
+		prefixFilters: prefixFilters,
+		newColumns:    map[types.FieldName]interface{}{},
+	}
+}
+
+func (ffm *FieldsFilterMiddleware) Process(ctx context.Context, row types.Row) ([]types.Row, error) {
+	if len(ffm.fields) == 0 && len(ffm.filters) == 0 {
+		return []types.Row{row}, nil
+	}
+
+	newRow := types.NewRow()
+
+	for pair := row.Oldest(); pair != nil; pair = pair.Next() {
+		rowField, value := pair.Key, pair.Value
+
+		// skip all of this if we already filtered that field
+		if _, ok := ffm.newColumns[rowField]; !ok {
+			exactMatchFound := false
+			prefixMatchFound := false
+
+			exactFilterMatchFound := false
+			prefixFilterMatchFound := false
+
+			// go through all the fields and prefix fields and check if the current field matches
+			if len(ffm.fields) > 0 || len(ffm.prefixFields) > 0 {
+				// first go through exact matches
+				if _, ok := ffm.fields[rowField]; ok {
+					exactMatchFound = true
+				} else {
+					// else, test against all prefixes
+					for _, prefix := range ffm.prefixFields {
+						if strings.HasPrefix(rowField, prefix) {
+							prefixMatchFound = true
+							break
+						}
+					}
+				}
+
+				if !exactMatchFound && !prefixMatchFound {
+					continue
+				}
+			}
+
+			if len(ffm.filters) > 0 || len(ffm.prefixFilters) > 0 {
+				// if an exact filter matches, move on
+				if _, ok := ffm.filters[rowField]; ok {
+					exactFilterMatchFound = true
+					continue
+				} else {
+					// else, test against all prefixes
+					for _, prefix := range ffm.prefixFilters {
+						if strings.HasPrefix(rowField, prefix) {
+							prefixFilterMatchFound = true
+							break
+						}
+					}
+				}
+			}
+
+			if exactMatchFound {
+				ffm.newColumns[rowField] = nil
+			} else if prefixMatchFound {
+				if prefixFilterMatchFound {
+					// should we do by prefix length, nah...
+					// choose to include by default
+					ffm.newColumns[rowField] = nil
+				} else if exactFilterMatchFound {
+					continue
+				} else {
+					ffm.newColumns[rowField] = nil
+				}
+			} else if exactFilterMatchFound {
+				continue
+			} else if len(ffm.fields) == 0 {
+				ffm.newColumns[rowField] = nil
+			}
+		}
+
+		newRow.Set(rowField, value)
+	}
+
+	return []types.Row{newRow}, nil
+}
diff --git a/pkg/middlewares/row/flatten-object.go b/pkg/middlewares/row/flatten-object.go
new file mode 100644
index 0000000..5103fbc
--- /dev/null
+++ b/pkg/middlewares/row/flatten-object.go
@@ -0,0 +1,46 @@
+package row
+
+import (
+	"context"
+	"fmt"
+	"github.com/go-go-golems/glazed/pkg/types"
+)
+
+type FlattenObjectMiddleware struct {
+}
+
+func NewFlattenObjectMiddleware() *FlattenObjectMiddleware {
+	return &FlattenObjectMiddleware{}
+}
+
+func (fom *FlattenObjectMiddleware) Process(ctx context.Context, row types.Row) ([]types.Row, error) {
+	newRow := FlattenRow(row)
+	return []types.Row{newRow}, nil
+}
+
+func FlattenRow(row types.Row) types.Row {
+	ret := types.NewRow()
+
+	for pair := row.Oldest(); pair != nil; pair = pair.Next() {
+		key, value := pair.Key, pair.Value
+		switch v := value.(type) {
+		case map[string]interface{}:
+			childRow := types.NewRowFromMap(v)
+			newColumns := FlattenRow(childRow)
+			for pair_ := newColumns.Oldest(); pair_ != nil; pair_ = pair_.Next() {
+				k, v := pair_.Key, pair_.Value
+				ret.Set(fmt.Sprintf("%s.%s", key, k), v)
+			}
+		case types.Row:
+			newColumns := FlattenRow(v)
+			for pair_ := newColumns.Oldest(); pair_ != nil; pair_ = pair_.Next() {
+				k, v := pair_.Key, pair_.Value
+				ret.Set(fmt.Sprintf("%s.%s", key, k), v)
+			}
+		default:
+			ret.Set(key, v)
+		}
+	}
+
+	return ret
+}
diff --git a/pkg/middlewares/row/remove-duplicates.go b/pkg/middlewares/row/remove-duplicates.go
new file mode 100644
index 0000000..9affd24
--- /dev/null
+++ b/pkg/middlewares/row/remove-duplicates.go
@@ -0,0 +1,37 @@
+package row
+
+import (
+	"context"
+	"github.com/go-go-golems/glazed/pkg/types"
+)
+
+type RemoveDuplicatesMiddleware struct {
+	columns           []string
+	previousRowValues types.Row
+}
+
+func NewRemoveDuplicatesMiddleware(columns ...string) *RemoveDuplicatesMiddleware {
+	return &RemoveDuplicatesMiddleware{
+		columns: columns,
+	}
+}
+
+func (r *RemoveDuplicatesMiddleware) Process(ctx context.Context, row types.Row) ([]types.Row, error) {
+	if r.previousRowValues != nil {
+		// check if the values are the same
+		same := true
+		for _, column := range r.columns {
+			v, ok := row.Get(column)
+			v2, ok2 := r.previousRowValues.Get(column)
+			if ok != ok2 || v != v2 {
+				same = false
+				break
+			}
+		}
+		if same {
+			return []types.Row{}, nil
+		}
+	}
+	r.previousRowValues = row
+	return []types.Row{row}, nil
+}
diff --git a/pkg/middlewares/row/remove-nulls.go b/pkg/middlewares/row/remove-nulls.go
new file mode 100644
index 0000000..1460c3d
--- /dev/null
+++ b/pkg/middlewares/row/remove-nulls.go
@@ -0,0 +1,26 @@
+package row
+
+import (
+	"context"
+	"github.com/go-go-golems/glazed/pkg/types"
+)
+
+type RemoveNullsMiddleware struct {
+}
+
+func NewRemoveNullsMiddleware() *RemoveNullsMiddleware {
+	return &RemoveNullsMiddleware{}
+}
+
+func (rnm *RemoveNullsMiddleware) Process(ctx context.Context, row types.Row) ([]types.Row, error) {
+	newRow := types.NewRow()
+
+	for pair := row.Oldest(); pair != nil; pair = pair.Next() {
+		key, value := pair.Key, pair.Value
+		if value != nil {
+			newRow.Set(key, value)
+		}
+	}
+
+	return []types.Row{newRow}, nil
+}
diff --git a/pkg/middlewares/row/rename-columns.go b/pkg/middlewares/row/rename-columns.go
new file mode 100644
index 0000000..9abf009
--- /dev/null
+++ b/pkg/middlewares/row/rename-columns.go
@@ -0,0 +1,137 @@
+package row
+
+import (
+	"context"
+	"fmt"
+	"github.com/go-go-golems/glazed/pkg/types"
+	"gopkg.in/yaml.v3"
+	"regexp"
+)
+
+type RenameColumnMiddleware struct {
+	Renames map[types.FieldName]types.FieldName
+	// orderedmap *regexp.Regexp -> string
+	RegexpRenames RegexpReplacements
+
+	// renamedColumns keeps tracks of columns that are being renamed. To avoid
+	// going through all the Renames and RegexpRenames on every row,
+	// we cache affected columns in renamedColumns.
+	renamedColumns map[types.FieldName]types.FieldName
+}
+
+func NewFieldRenameColumnMiddleware(renames map[types.FieldName]types.FieldName) *RenameColumnMiddleware {
+	return &RenameColumnMiddleware{
+		Renames:        renames,
+		RegexpRenames:  RegexpReplacements{},
+		renamedColumns: map[types.FieldName]types.FieldName{},
+	}
+}
+
+func NewRegexpRenameColumnMiddleware(renames RegexpReplacements) *RenameColumnMiddleware {
+	return &RenameColumnMiddleware{
+		Renames:        map[types.FieldName]types.FieldName{},
+		RegexpRenames:  renames,
+		renamedColumns: map[types.FieldName]types.FieldName{},
+	}
+}
+
+func NewRenameColumnMiddleware(
+	renames map[types.FieldName]types.FieldName,
+	regexpRenames RegexpReplacements,
+) *RenameColumnMiddleware {
+	return &RenameColumnMiddleware{
+		Renames:        renames,
+		RegexpRenames:  regexpRenames,
+		renamedColumns: map[types.FieldName]types.FieldName{},
+	}
+}
+
+type RegexpReplacement struct {
+	Regexp      *regexp.Regexp
+	Replacement string
+}
+
+type RegexpReplacements []*RegexpReplacement
+
+func (rr *RegexpReplacements) UnmarshalYAML(value *yaml.Node) error {
+	if value.Kind != yaml.MappingNode {
+		return fmt.Errorf("expected a mapping node, got %v", value.Kind)
+	}
+	*rr = RegexpReplacements{}
+	for i := 0; i < len(value.Content); i += 2 {
+		key := value.Content[i]
+		val := value.Content[i+1]
+
+		if key.Kind != yaml.ScalarNode {
+			return fmt.Errorf("expected a scalar node, got %v", key.Kind)
+		}
+		if val.Kind != yaml.ScalarNode {
+			return fmt.Errorf("expected a scalar node, got %v", val.Kind)
+		}
+		re, err := regexp.Compile(key.Value)
+		if err != nil {
+			return err
+		}
+		*rr = append(*rr, &RegexpReplacement{
+			Regexp:      re,
+			Replacement: val.Value,
+		})
+	}
+
+	return nil
+}
+
+type ColumnMiddlewareConfig struct {
+	FieldRenames map[types.FieldName]types.FieldName `yaml:"renames"`
+	// FIXME regex renames actually need to ordered
+	RegexpRenames RegexpReplacements `yaml:"regexpRenames"`
+}
+
+func NewRenameColumnMiddlewareFromYAML(decoder *yaml.Decoder) (*RenameColumnMiddleware, error) {
+	var config ColumnMiddlewareConfig
+	err := decoder.Decode(&config)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewRenameColumnMiddleware(config.FieldRenames, config.RegexpRenames), nil
+}
+
+// renameColumn takes a single column fields, looks up if it has already been processed previously,
+// and otherwise runs it through the renames and regexp renames to compute the renamed column name.
+func (r *RenameColumnMiddleware) renameColumn(
+	column types.FieldName,
+) types.FieldName {
+	if rename, ok := r.renamedColumns[column]; ok {
+		return rename
+	}
+
+	// we run string renames first, as we consider them more exhaustive matches
+	for match, rename := range r.Renames {
+		if column == match {
+			r.renamedColumns[column] = rename
+			return rename
+		}
+	}
+
+	for _, rr := range r.RegexpRenames {
+		rename := rr.Regexp.ReplaceAllString(column, rr.Replacement)
+		if rename != column {
+			r.renamedColumns[column] = rename
+			return rename
+		}
+	}
+
+	r.renamedColumns[column] = column
+	return column
+}
+
+func (r *RenameColumnMiddleware) Process(ctx context.Context, row types.Row) ([]types.Row, error) {
+	newRow := types.NewRow()
+	for pair := row.Oldest(); pair != nil; pair = pair.Next() {
+		newRow.Set(r.renameColumn(pair.Key), pair.Value)
+	}
+
+	ret := []types.Row{newRow}
+	return ret, nil
+}
diff --git a/pkg/middlewares/row/reorder-column.go b/pkg/middlewares/row/reorder-column.go
new file mode 100644
index 0000000..641a9f6
--- /dev/null
+++ b/pkg/middlewares/row/reorder-column.go
@@ -0,0 +1,55 @@
+package row
+
+import (
+	"context"
+	"github.com/go-go-golems/glazed/pkg/types"
+	"strings"
+)
+
+type ReorderColumnOrderMiddleware struct {
+	columns []types.FieldName
+}
+
+func NewReorderColumnOrderMiddleware(columns []types.FieldName) *ReorderColumnOrderMiddleware {
+	return &ReorderColumnOrderMiddleware{
+		columns: columns,
+	}
+}
+
+func (scm *ReorderColumnOrderMiddleware) Process(ctx context.Context, row types.Row) ([]types.Row, error) {
+	newRow := types.NewRow()
+
+	existingColumns := types.GetFields(row)
+	seenColumns := map[types.FieldName]interface{}{}
+
+	for _, column := range scm.columns {
+		if strings.HasSuffix(column, ".") {
+			for _, existingColumn := range existingColumns {
+				if strings.HasPrefix(existingColumn, column) {
+					if _, ok := seenColumns[existingColumn]; !ok {
+						v, _ := row.Get(existingColumn)
+						newRow.Set(existingColumn, v)
+						seenColumns[existingColumn] = nil
+					}
+				}
+			}
+		} else {
+			if value, ok := row.Get(column); ok {
+				if _, ok := seenColumns[column]; !ok {
+					newRow.Set(column, value)
+					seenColumns[column] = nil
+				}
+			}
+		}
+	}
+
+	for pair := row.Oldest(); pair != nil; pair = pair.Next() {
+		key, value := pair.Key, pair.Value
+		if _, ok := seenColumns[key]; !ok {
+			newRow.Set(key, value)
+			seenColumns[key] = nil
+		}
+	}
+
+	return []types.Row{newRow}, nil
+}
diff --git a/pkg/middlewares/table/replace.go b/pkg/middlewares/row/replace.go
similarity index 82%
rename from pkg/middlewares/table/replace.go
rename to pkg/middlewares/row/replace.go
index 82222cc..7da4c35 100644
--- a/pkg/middlewares/table/replace.go
+++ b/pkg/middlewares/row/replace.go
@@ -1,6 +1,7 @@
-package table
+package row
 
 import (
+	"context"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/pkg/errors"
 	"gopkg.in/yaml.v3"
@@ -191,53 +192,40 @@ func NewReplaceMiddlewareFromYAML(b []byte) (*ReplaceMiddleware, error) {
 	return NewReplaceMiddleware(replacements, regexReplacements, regexSkips, skips), nil
 }
 
-func (r *ReplaceMiddleware) Process(table *types.Table) (*types.Table, error) {
-	ret := &types.Table{
-		Columns: []types.FieldName{},
-		Rows:    []types.Row{},
-	}
+func (r *ReplaceMiddleware) Process(ctx context.Context, row types.Row) ([]types.Row, error) {
+	newRow := types.NewRow()
 
-	ret.Columns = append(ret.Columns, table.Columns...)
+	for pair := row.Oldest(); pair != nil; pair = pair.Next() {
+		rowField, value := pair.Key, pair.Value
 
-NextRow:
-	for _, row := range table.Rows {
-		values := row.GetValues()
-		newRow := types.SimpleRow{
-			Hash: map[types.FieldName]types.GenericCellValue{},
+		s, ok := value.(string)
+		if !ok {
+			newRow.Set(rowField, value)
+			continue
 		}
 
-		for rowField, value := range values {
-			s, ok := value.(string)
-			if !ok {
-				newRow.Hash[rowField] = value
-				continue
-			}
-
-			for _, skip := range r.Skips[rowField] {
-				if strings.Contains(s, skip.Pattern) {
-					continue NextRow
-				}
-			}
-
-			for _, regexSkip := range r.RegexSkips[rowField] {
-				if regexSkip.Regexp.MatchString(s) {
-					continue NextRow
-				}
+		for _, skip := range r.Skips[rowField] {
+			if strings.Contains(s, skip.Pattern) {
+				return nil, nil
 			}
+		}
 
-			for _, replacement := range r.Replacements[rowField] {
-				s = strings.ReplaceAll(s, replacement.Pattern, replacement.Replacement)
+		for _, regexSkip := range r.RegexSkips[rowField] {
+			if regexSkip.Regexp.MatchString(s) {
+				return nil, nil
 			}
+		}
 
-			for _, regexReplacement := range r.RegexReplacements[rowField] {
-				s = regexReplacement.Regexp.ReplaceAllString(s, regexReplacement.Replacement)
-			}
+		for _, replacement := range r.Replacements[rowField] {
+			s = strings.ReplaceAll(s, replacement.Pattern, replacement.Replacement)
+		}
 
-			newRow.Hash[rowField] = s
+		for _, regexReplacement := range r.RegexReplacements[rowField] {
+			s = regexReplacement.Regexp.ReplaceAllString(s, regexReplacement.Replacement)
 		}
 
-		ret.Rows = append(ret.Rows, &newRow)
+		newRow.Set(rowField, s)
 	}
 
-	return ret, nil
+	return []types.Row{newRow}, nil
 }
diff --git a/pkg/middlewares/row/sort-columns.go b/pkg/middlewares/row/sort-columns.go
new file mode 100644
index 0000000..debe635
--- /dev/null
+++ b/pkg/middlewares/row/sort-columns.go
@@ -0,0 +1,26 @@
+package row
+
+import (
+	"context"
+	"github.com/go-go-golems/glazed/pkg/types"
+	"sort"
+)
+
+type SortColumnsMiddleware struct {
+}
+
+func NewSortColumnsMiddleware() *SortColumnsMiddleware {
+	return &SortColumnsMiddleware{}
+}
+
+func (scm *SortColumnsMiddleware) Process(ctx context.Context, row types.Row) ([]types.Row, error) {
+	fields := types.GetFields(row)
+	sort.Strings(fields)
+
+	newRow := types.NewRow()
+	for _, field := range fields {
+		value, _ := row.Get(field)
+		newRow.Set(field, value)
+	}
+	return []types.Row{newRow}, nil
+}
diff --git a/pkg/middlewares/row/template.go b/pkg/middlewares/row/template.go
new file mode 100644
index 0000000..f83dedb
--- /dev/null
+++ b/pkg/middlewares/row/template.go
@@ -0,0 +1,88 @@
+package row
+
+import (
+	"bytes"
+	"context"
+	"github.com/Masterminds/sprig"
+	"github.com/go-go-golems/glazed/pkg/helpers/templating"
+	"github.com/go-go-golems/glazed/pkg/types"
+	"strings"
+	"text/template"
+)
+
+type TemplateMiddleware struct {
+	templates map[types.FieldName]*template.Template
+	// this field is used to replace "." in keys before passing them to the template,
+	// in order to avoid having to use the `index` template function to access fields
+	// that contain a ".", which is frequent due to flattening.
+	RenameSeparator string
+
+	renamedColumns map[types.FieldName]types.FieldName
+}
+
+// NewTemplateMiddleware creates a new TemplateMiddleware
+// which is the simplest go template middleware.
+//
+// It will render the template for each row and return the result as a new column called with
+// the given title.
+//
+// Because nested objects will be flattened to individual columns using the . separator,
+// this will make fields inaccessible to the template. One way around this is to use
+// {{ index . "field.subfield" }} in the template. Another is to pass a separator rename
+// option.
+//
+// TODO(manuel, 2023-02-02) Add support for passing in custom funcmaps
+// See #110 https://github.com/go-go-golems/glazed/issues/110
+func NewTemplateMiddleware(
+	templateStrings map[types.FieldName]string,
+	renameSeparator string) (*TemplateMiddleware, error) {
+
+	templates := map[types.FieldName]*template.Template{}
+	for columnName, templateString := range templateStrings {
+		tmpl, err := template.New("row").
+			Funcs(sprig.TxtFuncMap()).
+			Funcs(templating.TemplateFuncs).
+			Parse(templateString)
+		if err != nil {
+			return nil, err
+		}
+		templates[columnName] = tmpl
+	}
+
+	return &TemplateMiddleware{
+		templates:       templates,
+		RenameSeparator: renameSeparator,
+		renamedColumns:  map[types.FieldName]types.FieldName{},
+	}, nil
+}
+
+func (rgtm *TemplateMiddleware) Process(ctx context.Context, row types.Row) ([]types.Row, error) {
+	templateValues := map[string]interface{}{}
+
+	for pair := row.Oldest(); pair != nil; pair = pair.Next() {
+		key, value := pair.Key, pair.Value
+
+		if rgtm.RenameSeparator != "" {
+			if _, ok := rgtm.renamedColumns[key]; !ok {
+				rgtm.renamedColumns[key] = strings.ReplaceAll(key, ".", rgtm.RenameSeparator)
+			}
+
+			key = rgtm.renamedColumns[key]
+		}
+		templateValues[key] = value
+	}
+	templateValues["_row"] = templateValues
+
+	for columnName, tmpl := range rgtm.templates {
+		var buf bytes.Buffer
+		err := tmpl.Execute(&buf, templateValues)
+		if err != nil {
+			return nil, err
+		}
+		s := buf.String()
+
+		row.Set(columnName, s)
+	}
+
+	return []types.Row{row}, nil
+}
diff --git a/pkg/middlewares/table/add-field.go b/pkg/middlewares/table/add-field.go
deleted file mode 100644
index e8cfebf..0000000
--- a/pkg/middlewares/table/add-field.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package table
-
-import (
-	"github.com/go-go-golems/glazed/pkg/types"
-)
-
-type AddFieldMiddleware struct {
-	Fields map[string]string
-}
-
-func NewAddFieldMiddleware(fields map[string]string) *AddFieldMiddleware {
-	return &AddFieldMiddleware{Fields: fields}
-}
-
-func (a *AddFieldMiddleware) Process(table *types.Table) (*types.Table, error) {
-	ret := &types.Table{
-		Columns: table.Columns,
-		Rows:    make([]types.Row, 0),
-	}
-
-	for _, row := range table.Rows {
-		values := row.GetValues()
-		newValues := make(map[types.FieldName]types.GenericCellValue)
-		for key, value := range values {
-			newValues[key] = value
-		}
-		for key, value := range a.Fields {
-			newValues[key] = value
-		}
-		ret.Rows = append(ret.Rows, &types.SimpleRow{Hash: newValues})
-	}
-
-	for key := range a.Fields {
-		ret.Columns = append(ret.Columns, key)
-	}
-
-	return ret, nil
-}
diff --git a/pkg/middlewares/table/remove-duplicates.go b/pkg/middlewares/table/remove-duplicates.go
deleted file mode 100644
index 4fc5dff..0000000
--- a/pkg/middlewares/table/remove-duplicates.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package table
-
-import "github.com/go-go-golems/glazed/pkg/types"
-
-type RemoveDuplicatesMiddleware struct {
-	columns []string
-}
-
-func NewRemoveDuplicatesMiddleware(columns ...string) *RemoveDuplicatesMiddleware {
-	return &RemoveDuplicatesMiddleware{
-		columns: columns,
-	}
-}
-
-func (r *RemoveDuplicatesMiddleware) Process(table *types.Table) (*types.Table, error) {
-	ret := &types.Table{
-		Columns: table.Columns,
-		Rows:    make([]types.Row, 0),
-	}
-
-	var previousRowValues map[types.FieldName]types.GenericCellValue
-
-	for _, row := range table.Rows {
-		values := row.GetValues()
-		if previousRowValues != nil {
-			// check if the values are the same
-			same := true
-			for _, column := range r.columns {
-				if values[column] != previousRowValues[column] {
-					same = false
-					break
-				}
-			}
-			if same {
-				continue
-			}
-		}
-		ret.Rows = append(ret.Rows, row)
-		previousRowValues = values
-	}
-
-	return ret, nil
-}
diff --git a/pkg/middlewares/table/sortby.go b/pkg/middlewares/table/sortby.go
index f986a3f..54b6c21 100644
--- a/pkg/middlewares/table/sortby.go
+++ b/pkg/middlewares/table/sortby.go
@@ -1,6 +1,7 @@
 package table
 
 import (
+	"context"
 	"github.com/go-go-golems/glazed/pkg/helpers/compare"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"sort"
@@ -51,7 +52,7 @@ func NewSortByMiddlewareFromColumns(columns ...string) *SortByMiddleware {
 	return ret
 }
 
-func (s *SortByMiddleware) Process(table *types.Table) (*types.Table, error) {
+func (s *SortByMiddleware) Process(ctx context.Context, table *types.Table) (*types.Table, error) {
 	ret := &types.Table{
 		Columns: table.Columns,
 		Rows:    make([]types.Row, 0),
@@ -64,15 +65,17 @@ func (s *SortByMiddleware) Process(table *types.Table) (*types.Table, error) {
 	}
 
 	sort.Slice(ret.Rows, func(i, j int) bool {
-		rowA := ret.Rows[i].GetValues()
-		rowB := ret.Rows[j].GetValues()
+		rowA := ret.Rows[i]
+		rowB := ret.Rows[j]
 
 		for _, column := range s.columns {
-			if rowA[column.name] == rowB[column.name] {
+			v, ok := rowA.Get(column.name)
+			v2, ok2 := rowB.Get(column.name)
+			if ok == ok2 && v == v2 {
 				continue
 			}
 
-			if compare.IsLowerThan(rowA[column.name], rowB[column.name]) {
+			if compare.IsLowerThan(v, v2) {
 				return column.asc
 			} else {
 				return !column.asc
diff --git a/pkg/middlewares/table/table.go b/pkg/middlewares/table/table.go
deleted file mode 100644
index a763fd7..0000000
--- a/pkg/middlewares/table/table.go
+++ /dev/null
@@ -1,597 +0,0 @@
-package table
-
-import (
-	"bytes"
-	"fmt"
-	"github.com/Masterminds/sprig"
-	"github.com/go-go-golems/glazed/pkg/helpers/templating"
-	"github.com/go-go-golems/glazed/pkg/types"
-	"gopkg.in/yaml.v3"
-	"regexp"
-	"sort"
-	"strings"
-	"text/template"
-)
-
-// FieldsFilterMiddleware keeps columns that are in the fields list and removes
-// columns that are in the filters list.
-//
-// empty lists means that all columns are accepted.
-//
-// The returned rows are SimpleRows
-type FieldsFilterMiddleware struct {
-	fields        map[string]interface{}
-	filters       map[string]interface{}
-	prefixFields  []string
-	prefixFilters []string
-}
-
-func NewFieldsFilterMiddleware(fields []string, filters []string) *FieldsFilterMiddleware {
-	fieldHash := map[string]interface{}{}
-	prefixFields := []string{}
-	prefixFilters := []string{}
-
-	for _, field := range fields {
-		if strings.HasSuffix(field, ".") {
-			prefixFields = append(prefixFields, field)
-		} else {
-			fieldHash[field] = nil
-		}
-	}
-	filterHash := map[string]interface{}{}
-	for _, filter := range filters {
-		if strings.HasSuffix(filter, ".") {
-			prefixFilters = append(prefixFilters, filter)
-		} else {
-			filterHash[filter] = nil
-		}
-	}
-	return &FieldsFilterMiddleware{
-		fields:        fieldHash,
-		filters:       filterHash,
-		prefixFields:  prefixFields,
-		prefixFilters: prefixFilters,
-	}
-}
-
-func (ffm *FieldsFilterMiddleware) Process(table *types.Table) (*types.Table, error) {
-	ret := &types.Table{
-		Columns: []types.FieldName{},
-		Rows:    []types.Row{},
-	}
-
-	// how do we keep order here
-	newColumns := map[types.FieldName]interface{}{}
-
-	if len(ffm.fields) == 0 && len(ffm.filters) == 0 {
-		return table, nil
-	}
-
-	for _, row := range table.Rows {
-		values := row.GetValues()
-		newRow := types.SimpleRow{
-			Hash: map[types.FieldName]types.GenericCellValue{},
-		}
-
-	NextRow:
-		for rowField, value := range values {
-			// skip all of this if we already filtered that field
-			if _, ok := newColumns[rowField]; !ok {
-				exactMatchFound := false
-				prefixMatchFound := false
-
-				exactFilterMatchFound := false
-				prefixFilterMatchFound := false
-
-				if len(ffm.fields) > 0 || len(ffm.prefixFields) > 0 {
-					// first go through exact matches
-					if _, ok := ffm.fields[rowField]; ok {
-						exactMatchFound = true
-					} else {
-						// else, test against all prefixes
-						for _, prefix := range ffm.prefixFields {
-							if strings.HasPrefix(rowField, prefix) {
-								prefixMatchFound = true
-								break
-							}
-						}
-					}
-
-					if !exactMatchFound && !prefixMatchFound {
-						continue NextRow
-					}
-				}
-
-				if len(ffm.filters) > 0 || len(ffm.prefixFilters) > 0 {
-					// if an exact filter matches, move on
-					if _, ok := ffm.filters[rowField]; ok {
-						exactFilterMatchFound = true
-						continue NextRow
-					} else {
-						// else, test against all prefixes
-						for _, prefix := range ffm.prefixFilters {
-							if strings.HasPrefix(rowField, prefix) {
-								prefixFilterMatchFound = true
-								break
-							}
-						}
-					}
-				}
-
-				if exactMatchFound {
-					newColumns[rowField] = nil
-				} else if prefixMatchFound {
-					if prefixFilterMatchFound {
-						// should we do by prefix length, nah...
-						// choose to include by default
-						newColumns[rowField] = nil
-					} else if exactFilterMatchFound {
-						continue NextRow
-					} else {
-						newColumns[rowField] = nil
-					}
-				} else if exactFilterMatchFound {
-					continue NextRow
-				} else if len(ffm.fields) == 0 {
-					newColumns[rowField] = nil
-				}
-			}
-
-			newRow.Hash[rowField] = value
-		}
-
-		ret.Rows = append(ret.Rows, &newRow)
-	}
-
-	ret.Columns = PreserveColumnOrder(table.Columns, newColumns)
-
-	return ret, nil
-}
-
-func PreserveColumnOrder(oldColumns []types.FieldName, newColumns map[types.FieldName]interface{}) []types.FieldName {
-	seenRetColumns := map[types.FieldName]interface{}{}
-	retColumns := []types.FieldName{}
-
-	// preserve previous columns order as best as possible
-	for _, column := range oldColumns {
-		if _, ok := newColumns[column]; ok {
-			retColumns = append(retColumns, column)
-			seenRetColumns[column] = nil
-		}
-	}
-	for key := range newColumns {
-		if _, ok := seenRetColumns[key]; !ok {
-			retColumns = append(retColumns, key)
-			seenRetColumns[key] = nil
-		}
-	}
-	return retColumns
-}
-
-type RemoveNullsMiddleware struct {
-}
-
-func NewRemoveNullsMiddleware() *RemoveNullsMiddleware {
-	return &RemoveNullsMiddleware{}
-}
-
-func (rnm *RemoveNullsMiddleware) Process(table *types.Table) (*types.Table, error) {
-	ret := &types.Table{
-		Columns: table.Columns,
-		Rows:    []types.Row{},
-	}
-
-	for _, row := range table.Rows {
-		values := row.GetValues()
-		newRow := types.SimpleRow{
-			Hash: map[types.FieldName]types.GenericCellValue{},
-		}
-
-		for key, value := range values {
-			if value != nil {
-				newRow.Hash[key] = value
-			}
-		}
-
-		ret.Rows = append(ret.Rows, &newRow)
-	}
-
-	return ret, nil
-}
-
-type FlattenObjectMiddleware struct {
-}
-
-func NewFlattenObjectMiddleware() *FlattenObjectMiddleware {
-	return &FlattenObjectMiddleware{}
-}
-
-func (fom *FlattenObjectMiddleware) Process(table *types.Table) (*types.Table, error) {
-	ret := &types.Table{
-		Columns: []types.FieldName{},
-		Rows:    []types.Row{},
-	}
-
-	newColumns := map[types.FieldName]interface{}{}
-
-	for _, row := range table.Rows {
-		values := row.GetValues()
-		newValues := FlattenMapIntoColumns(values)
-		newRow := types.SimpleRow{
-			Hash: newValues,
-		}
-
-		for key := range newValues {
-			newColumns[key] = nil
-		}
-		ret.Rows = append(ret.Rows, &newRow)
-	}
-
-	ret.Columns = PreserveColumnOrder(table.Columns, newColumns)
-
-	return ret, nil
-}
-
-func FlattenMapIntoColumns(rows types.MapRow) types.MapRow {
-	ret := types.MapRow{}
-
-	for key, value := range rows {
-		switch v := value.(type) {
-		case types.MapRow:
-			for k, v := range FlattenMapIntoColumns(v) {
-				ret[fmt.Sprintf("%s.%s", key, k)] = v
-			}
-		default:
-			ret[key] = v
-		}
-	}
-
-	return ret
-}
-
-type PreserveColumnOrderMiddleware struct {
-	columns []types.FieldName
-}
-
-func NewPreserveColumnOrderMiddleware(columns []types.FieldName) *PreserveColumnOrderMiddleware {
-	return &PreserveColumnOrderMiddleware{
-		columns: columns,
-	}
-}
-
-func (scm *PreserveColumnOrderMiddleware) Process(table *types.Table) (*types.Table, error) {
-	columnHash := map[types.FieldName]interface{}{}
-	for _, column := range scm.columns {
-		columnHash[column] = nil
-	}
-
-	table.Columns = PreserveColumnOrder(table.Columns, columnHash)
-	return table, nil
-}
-
-type ReorderColumnOrderMiddleware struct {
-	columns []types.FieldName
-}
-
-func NewReorderColumnOrderMiddleware(columns []types.FieldName) *ReorderColumnOrderMiddleware {
-	return &ReorderColumnOrderMiddleware{
-		columns: columns,
-	}
-}
-
-func (scm *ReorderColumnOrderMiddleware) Process(table *types.Table) (*types.Table, error) {
-	existingColumns := map[types.FieldName]interface{}{}
-	for _, column := range table.Columns {
-		existingColumns[column] = nil
-	}
-
-	seenColumns := map[types.FieldName]interface{}{}
-	newColumns := []types.FieldName{}
-
-	for _, column := range scm.columns {
-		if strings.HasSuffix(column, ".") {
-			for _, existingColumn := range table.Columns {
-				if strings.HasPrefix(existingColumn, column) {
-					if _, ok := seenColumns[existingColumn]; !ok {
-						newColumns = append(newColumns, existingColumn)
-						seenColumns[existingColumn] = nil
-					}
-				}
-			}
-		} else {
-			if _, ok := seenColumns[column]; !ok {
-				if _, ok := existingColumns[column]; ok {
-					newColumns = append(newColumns, column)
-					seenColumns[column] = nil
-				}
-			}
-
-		}
-	}
-
-	for column := range existingColumns {
-		if _, ok := seenColumns[column]; !ok {
-			newColumns = append(newColumns, column)
-			seenColumns[column] = nil
-		}
-	}
-
-	table.Columns = newColumns
-
-	return table, nil
-}
-
-type SortColumnsMiddleware struct {
-}
-
-func NewSortColumnsMiddleware() *SortColumnsMiddleware {
-	return &SortColumnsMiddleware{}
-}
-
-func (scm *SortColumnsMiddleware) Process(table *types.Table) (*types.Table, error) {
-	sort.Strings(table.Columns)
-	return table, nil
-}
-
-type RowGoTemplateMiddleware struct {
-	templates map[types.FieldName]*template.Template
-	// this field is used to replace "." in keys before passing them to the template,
-	// in order to avoid having to use the `index` template function to access fields
-	// that contain a ".", which is frequent due to flattening.
-	RenameSeparator string
-}
-
-// NewRowGoTemplateMiddleware creates a new RowGoTemplateMiddleware
-// which is the simplest go template middleware.
-//
-// It will render the template for each row and return the result as a new column called with
-// the given title.
-//
-// Because nested objects will be flattened to individual columns using the . separator,
-// this will make fields inaccessible to the template. One way around this is to use
-// {{ index . "field.subfield" }} in the template. Another is to pass a separator rename
-// option.
-//
-// TODO(manuel, 2023-02-02) Add support for passing in custom funcmaps
-// See #110 https://github.com/go-go-golems/glazed/issues/110
-func NewRowGoTemplateMiddleware(
-	templateStrings map[types.FieldName]string,
-	renameSeparator string) (*RowGoTemplateMiddleware, error) {
-
-	templates := map[types.FieldName]*template.Template{}
-	for columnName, templateString := range templateStrings {
-		tmpl, err := template.New("row").
-			Funcs(sprig.TxtFuncMap()).
-			Funcs(templating.TemplateFuncs).
-			Parse(templateString)
-		if err != nil {
-			return nil, err
-		}
-		templates[columnName] = tmpl
-	}
-
-	return &RowGoTemplateMiddleware{
-		templates:       templates,
-		RenameSeparator: renameSeparator,
-	}, nil
-}
-
-func (rgtm *RowGoTemplateMiddleware) Process(table *types.Table) (*types.Table, error) {
-	ret := &types.Table{
-		Columns: []types.FieldName{},
-		Rows:    []types.Row{},
-	}
-
-	columnRenames := map[types.FieldName]types.FieldName{}
-	existingColumns := map[types.FieldName]interface{}{}
-	newColumns := map[types.FieldName]interface{}{}
-
-	for _, columnName := range table.Columns {
-		existingColumns[columnName] = nil
-		ret.Columns = append(ret.Columns, columnName)
-	}
-
-	for _, row := range table.Rows {
-		newRow := types.SimpleRow{
-			Hash: row.GetValues(),
-		}
-
-		templateValues := map[string]interface{}{}
-
-		for key, value := range newRow.Hash {
-			if rgtm.RenameSeparator != "" {
-				if _, ok := columnRenames[key]; !ok {
-					columnRenames[key] = strings.ReplaceAll(key, ".", rgtm.RenameSeparator)
-				}
-			} else {
-				columnRenames[key] = key
-			}
-			newKey := columnRenames[key]
-			templateValues[newKey] = value
-		}
-		templateValues["_row"] = templateValues
-
-		for columnName, tmpl := range rgtm.templates {
-			var buf bytes.Buffer
-			err := tmpl.Execute(&buf, templateValues)
-			if err != nil {
-				return nil, err
-			}
-			s := buf.String()
-
-			// we need to handle the fact that some rows might not have all the keys, and thus
-			// avoid counting columns as existing twice
-			if _, ok := newColumns[columnName]; !ok {
-				newColumns[columnName] = nil
-				ret.Columns = append(ret.Columns, columnName)
-			}
-			newRow.Hash[columnName] = s
-		}
-
-		ret.Rows = append(ret.Rows, &newRow)
-	}
-
-	return ret, nil
-}
-
-type RenameColumnMiddleware struct {
-	Renames map[types.FieldName]types.FieldName
-	// orderedmap *regexp.Regexp -> string
-	RegexpRenames RegexpReplacements
-}
-
-func NewFieldRenameColumnMiddleware(renames map[types.FieldName]types.FieldName) *RenameColumnMiddleware {
-	return &RenameColumnMiddleware{
-		Renames:       renames,
-		RegexpRenames: RegexpReplacements{},
-	}
-}
-
-func NewRegexpRenameColumnMiddleware(renames RegexpReplacements) *RenameColumnMiddleware {
-	return &RenameColumnMiddleware{
-		Renames:       map[types.FieldName]types.FieldName{},
-		RegexpRenames: renames,
-	}
-}
-
-func NewRenameColumnMiddleware(
-	renames map[types.FieldName]types.FieldName,
-	regexpRenames RegexpReplacements,
-) *RenameColumnMiddleware {
-	return &RenameColumnMiddleware{
-		Renames:       renames,
-		RegexpRenames: regexpRenames,
-	}
-}
-
-type RegexpReplacement struct {
-	Regexp      *regexp.Regexp
-	Replacement string
-}
-
-type RegexpReplacements []*RegexpReplacement
-
-func (rr *RegexpReplacements) UnmarshalYAML(value *yaml.Node) error {
-	if value.Kind != yaml.MappingNode {
-		return fmt.Errorf("expected a mapping node, got %v", value.Kind)
-	}
-	*rr = RegexpReplacements{}
-	for i := 0; i < len(value.Content); i += 2 {
-		key := value.Content[i]
-		val := value.Content[i+1]
-
-		if key.Kind != yaml.ScalarNode {
-			return fmt.Errorf("expected a scalar node, got %v", key.Kind)
-		}
-		if val.Kind != yaml.ScalarNode {
-			return fmt.Errorf("expected a scalar node, got %v", val.Kind)
-		}
-		re, err := regexp.Compile(key.Value)
-		if err != nil {
-			return err
-		}
-		*rr = append(*rr, &RegexpReplacement{
-			Regexp:      re,
-			Replacement: val.Value,
-		})
-	}
-
-	return nil
-}
-
-type ColumnMiddlewareConfig struct {
-	FieldRenames map[types.FieldName]types.FieldName `yaml:"renames"`
-	// FIXME regex renames actually need to ordered
-	RegexpRenames RegexpReplacements `yaml:"regexpRenames"`
-}
-
-func NewRenameColumnMiddlewareFromYAML(decoder *yaml.Decoder) (*RenameColumnMiddleware, error) {
-	var config ColumnMiddlewareConfig
-	err := decoder.Decode(&config)
-	if err != nil {
-		return nil, err
-	}
-
-	return NewRenameColumnMiddleware(config.FieldRenames, config.RegexpRenames), nil
-}
-
-func (r *RenameColumnMiddleware) RenameColumns(
-	columns []types.FieldName,
-) ([]types.FieldName, map[types.FieldName]types.FieldName) {
-	var columnRenames = map[types.FieldName]types.FieldName{}
-	var renamedColumns = map[types.FieldName]interface{}{}
-	var orderedColumns = []types.FieldName{}
-
-	// first, we create a map of all the original columns to the new columns
-columnLoop:
-	for _, column := range columns {
-		// we run string renames first, as we consider them more exhaustive matches
-		for match, rename := range r.Renames {
-			if column == match {
-				if _, ok := renamedColumns[rename]; !ok {
-					orderedColumns = append(orderedColumns, rename)
-					renamedColumns[rename] = nil
-				}
-				columnRenames[match] = rename
-				continue columnLoop
-			}
-		}
-
-		for _, rr := range r.RegexpRenames {
-			rename := rr.Regexp.ReplaceAllString(column, rr.Replacement)
-			if rename != column {
-				if _, ok := renamedColumns[rename]; !ok {
-					orderedColumns = append(orderedColumns, rename)
-					renamedColumns[rename] = nil
-				}
-				columnRenames[column] = rename
-				continue columnLoop
-			}
-		}
-
-		// check if we already had a rename
-		if _, ok := renamedColumns[column]; !ok {
-			columnRenames[column] = column
-			renamedColumns[column] = nil
-			orderedColumns = append(orderedColumns, column)
-		}
-	}
-
-	return orderedColumns, columnRenames
-}
-
-func (r *RenameColumnMiddleware) Process(table *types.Table) (*types.Table, error) {
-	orderedColumns, renamedColumns := r.RenameColumns(table.Columns)
-
-	ret := &types.Table{
-		Columns: orderedColumns,
-		Rows:    []types.Row{},
-	}
-
-	// TODO(2022-12-28, manuel): we need to formalize the copy/clone behaviour of middlewares
-	// This is wrt to mutability, and also how things can be used in a streaming context
-	// I wonder if immutability is really necessary, or if the whole thing by design meshes
-	// well with just passing references to previous rows wrt efficiency.
-	// See: https://github.com/go-go-golems/glazed/issues/74
-
-	// we must now go through every row, and rename the hash keys.
-	// this really requires us to copy most of the maps.
-	// whatever, we'll address efficient renames later
-	for _, row := range table.Rows {
-		newRow := &types.SimpleRow{
-			Hash: map[types.FieldName]interface{}{},
-		}
-		values := row.GetValues()
-		for key, value := range values {
-			newKey, ok := renamedColumns[key]
-			if !ok {
-				// skip, it means columns were overwritten in the rename
-				continue
-			}
-			newRow.Hash[newKey] = value
-		}
-		ret.Rows = append(ret.Rows, newRow)
-	}
-
-	return ret, nil
-}
diff --git a/pkg/processor/processor.go b/pkg/processor/processor.go
index 53b9c19..9de7b8f 100644
--- a/pkg/processor/processor.go
+++ b/pkg/processor/processor.go
@@ -11,13 +11,18 @@ import (
 // TODO(manuel, 2023-04-27) This is probably a good location for With* constructors for middlewares
 
 type Processor interface {
-	ProcessInputObject(ctx context.Context, obj map[string]interface{}) error
+	ProcessInputObject(ctx context.Context, obj types.Row) error
 	OutputFormatter() formatters.OutputFormatter
+	Processor() *middlewares.Processor
 }
 
 type GlazeProcessor struct {
-	of  formatters.OutputFormatter
-	oms []middlewares.ObjectMiddleware
+	of        formatters.OutputFormatter
+	processor *middlewares.Processor
+}
+
+func (gp *GlazeProcessor) Processor() *middlewares.Processor {
+	return gp.processor
 }
 
 func (gp *GlazeProcessor) OutputFormatter() formatters.OutputFormatter {
@@ -26,28 +31,22 @@ func (gp *GlazeProcessor) OutputFormatter() formatters.OutputFormatter {
 
 type GlazeProcessorOption func(*GlazeProcessor)
 
-func WithAppendObjectMiddleware(om ...middlewares.ObjectMiddleware) GlazeProcessorOption {
-	return func(gp *GlazeProcessor) {
-		gp.oms = append(gp.oms, om...)
-	}
-}
-
-func WithPrependObjectMiddleware(om ...middlewares.ObjectMiddleware) GlazeProcessorOption {
+func WithOutputFormatter(of formatters.OutputFormatter) GlazeProcessorOption {
 	return func(gp *GlazeProcessor) {
-		gp.oms = append(om, gp.oms...)
+		gp.of = of
 	}
 }
 
-func WithOutputFormatter(of formatters.OutputFormatter) GlazeProcessorOption {
+func WithMiddlewareProcessor(processor *middlewares.Processor) GlazeProcessorOption {
 	return func(gp *GlazeProcessor) {
-		gp.of = of
+		gp.processor = processor
 	}
 }
 
 func NewGlazeProcessor(of formatters.OutputFormatter, options ...GlazeProcessorOption) *GlazeProcessor {
 	ret := &GlazeProcessor{
-		of:  of,
-		oms: []middlewares.ObjectMiddleware{},
+		of:        of,
+		processor: middlewares.NewProcessor(),
 	}
 
 	for _, option := range options {
@@ -64,23 +63,10 @@ func NewGlazeProcessor(of formatters.OutputFormatter, options ...GlazeProcessorO
 // chain.
 //
 // The final output is added to the output formatter as a single row.
-func (gp *GlazeProcessor) ProcessInputObject(ctx context.Context, obj map[string]interface{}) error {
-	currentObjects := []map[string]interface{}{obj}
-
-	for _, om := range gp.oms {
-		nextObjects := []map[string]interface{}{}
-		for _, obj := range currentObjects {
-			objs, err := om.Process(obj)
-			if err != nil {
-				return err
-			}
-			nextObjects = append(nextObjects, objs...)
-		}
-		currentObjects = nextObjects
-	}
-
-	for _, obj := range currentObjects {
-		gp.of.AddRow(&types.SimpleRow{Hash: obj})
+func (gp *GlazeProcessor) ProcessInputObject(ctx context.Context, obj types.Row) error {
+	err := gp.processor.AddRow(ctx, obj)
+	if err != nil {
+		return err
 	}
 	return nil
 }
@@ -99,7 +85,10 @@ func NewSimpleGlazeProcessor(options ...GlazeProcessorOption) *SimpleGlazeProces
 	}
 }
 
-func (gp *SimpleGlazeProcessor) GetTable() *types.Table {
-	gp.formatter.Table.Finalize()
-	return gp.formatter.Table
+func (gp *SimpleGlazeProcessor) GetTable(ctx context.Context) (*types.Table, error) {
+	err := gp.processor.FinalizeTable(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return gp.processor.Table, nil
 }
diff --git a/pkg/settings/glazed_layer.go b/pkg/settings/glazed_layer.go
index b2fcb42..26bb16c 100644
--- a/pkg/settings/glazed_layer.go
+++ b/pkg/settings/glazed_layer.go
@@ -8,7 +8,7 @@ import (
 	"github.com/go-go-golems/glazed/pkg/formatters/simple"
 	"github.com/go-go-golems/glazed/pkg/middlewares"
 	"github.com/go-go-golems/glazed/pkg/middlewares/object"
-	"github.com/go-go-golems/glazed/pkg/middlewares/table"
+	"github.com/go-go-golems/glazed/pkg/middlewares/row"
 	"github.com/go-go-golems/glazed/pkg/processor"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
@@ -405,52 +405,38 @@ func SetupProcessor(ps map[string]interface{}, options ...processor.GlazeProcess
 		return nil, err
 	}
 
-	var of formatters.OutputFormatter
 	templateSettings.UpdateWithSelectSettings(selectSettings)
 
-	if selectSettings.SelectField != "" {
-		of = simple.NewSingleColumnFormatter(
-			selectSettings.SelectField,
-			simple.WithSeparator(selectSettings.SelectSeparator),
-			simple.WithOutputFile(outputSettings.OutputFile),
-			simple.WithOutputMultipleFiles(outputSettings.OutputMultipleFiles),
-			simple.WithOutputFileTemplate(outputSettings.OutputFileTemplate),
-		)
-	} else {
-		of, err = outputSettings.CreateOutputFormatter()
-		if err != nil {
-			return nil, errors.Wrapf(err, "Error creating output formatter")
-		}
-	}
+	mwProcessor := middlewares.NewProcessor()
 
 	// rename middlewares run first because they are used to clean up column names
 	// for the following middlewares too.
 	// these following middlewares can create proper column names on their own
 	// when needed
-	err = renameSettings.AddMiddlewares(of)
+	err = renameSettings.AddMiddlewares(mwProcessor)
 	if err != nil {
 		return nil, errors.Wrapf(err, "Error adding rename middlewares")
 	}
 
-	err = templateSettings.AddMiddlewares(of)
+	err = templateSettings.AddMiddlewares(mwProcessor)
 	if err != nil {
 		return nil, errors.Wrapf(err, "Error adding template middlewares")
 	}
 
 	if (outputSettings.Output == "json" || outputSettings.Output == "yaml") && outputSettings.FlattenObjects {
-		mw := table.NewFlattenObjectMiddleware()
-		of.AddTableMiddleware(mw)
+		mw := row.NewFlattenObjectMiddleware()
+		mwProcessor.AddRowMiddlewareInFront(mw)
 	}
-	fieldsFilterSettings.AddMiddlewares(of)
+	fieldsFilterSettings.AddMiddlewares(mwProcessor)
 
-	err = replaceSettings.AddMiddlewares(of)
+	err = replaceSettings.AddMiddlewares(mwProcessor)
 	if err != nil {
 		return nil, errors.Wrapf(err, "Error adding replace middlewares")
 	}
 
 	var middlewares_ []middlewares.ObjectMiddleware
 	if !templateSettings.UseRowTemplates && len(templateSettings.Templates) > 0 {
-		ogtm, err := object.NewObjectGoTemplateMiddleware(templateSettings.Templates)
+		ogtm, err := object.NewTemplateMiddleware(templateSettings.Templates)
 		if err != nil {
 			return nil, errors.Wrapf(err, "Could not process template argument")
 		}
@@ -467,17 +453,33 @@ func SetupProcessor(ps map[string]interface{}, options ...processor.GlazeProcess
 	}
 
 	if jqTableMiddleware != nil {
-		of.AddTableMiddleware(jqTableMiddleware)
+		mwProcessor.AddTableMiddleware(jqTableMiddleware)
 	}
 
 	// NOTE(manuel, 2023-03-20): We need to figure out how to order middlewares, on the command line.
 	// This is not possible with cobra, which doesn't have ordering of flags, and adding that
 	// to the API that we currently use (which is a unordered hashmap, and parsed layers that lose the positioning)
 	// is not trivial.
-	sortSettings.AddMiddlewares(of)
+	sortSettings.AddMiddlewares(mwProcessor)
+
+	mwProcessor.AddObjectMiddleware(middlewares_...)
 
-	options_ := append(options, processor.WithAppendObjectMiddleware(middlewares_...))
+	var of formatters.OutputFormatter
+	if selectSettings.SelectField != "" {
+		of = simple.NewSingleColumnFormatter(
+			selectSettings.SelectField,
+			simple.WithSeparator(selectSettings.SelectSeparator),
+			simple.WithOutputFile(outputSettings.OutputFile),
+			simple.WithOutputMultipleFiles(outputSettings.OutputMultipleFiles),
+			simple.WithOutputFileTemplate(outputSettings.OutputFileTemplate),
+		)
+	} else {
+		of, err = outputSettings.CreateOutputFormatter(mwProcessor)
+		if err != nil {
+			return nil, errors.Wrapf(err, "Error creating output formatter")
+		}
+	}
 
-	gp := processor.NewGlazeProcessor(of, options_...)
+	gp := processor.NewGlazeProcessor(of, processor.WithMiddlewareProcessor(mwProcessor))
 	return gp, nil
 }
diff --git a/pkg/settings/settings_fields-filters.go b/pkg/settings/settings_fields-filters.go
index a318a4d..cca0c7e 100644
--- a/pkg/settings/settings_fields-filters.go
+++ b/pkg/settings/settings_fields-filters.go
@@ -4,8 +4,8 @@ import (
 	_ "embed"
 	"github.com/go-go-golems/glazed/pkg/cmds/layers"
 	"github.com/go-go-golems/glazed/pkg/cmds/parameters"
-	"github.com/go-go-golems/glazed/pkg/formatters"
-	"github.com/go-go-golems/glazed/pkg/middlewares/table"
+	"github.com/go-go-golems/glazed/pkg/middlewares"
+	"github.com/go-go-golems/glazed/pkg/middlewares/row"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 )
@@ -93,18 +93,18 @@ func NewFieldsFilterSettings(ps map[string]interface{}) (*FieldsFilterSettings,
 	return s, nil
 }
 
-func (ffs *FieldsFilterSettings) AddMiddlewares(of formatters.OutputFormatter) {
-	of.AddTableMiddleware(table.NewFieldsFilterMiddleware(ffs.Fields, ffs.Filters))
+func (ffs *FieldsFilterSettings) AddMiddlewares(p_ *middlewares.Processor) {
+	p_.AddRowMiddleware(row.NewFieldsFilterMiddleware(ffs.Fields, ffs.Filters))
 	if ffs.RemoveNulls {
-		of.AddTableMiddleware(table.NewRemoveNullsMiddleware())
+		p_.AddRowMiddleware(row.NewRemoveNullsMiddleware())
 	}
 	if ffs.SortColumns {
-		of.AddTableMiddleware(table.NewSortColumnsMiddleware())
+		p_.AddRowMiddleware(row.NewSortColumnsMiddleware())
 	}
 	if len(ffs.ReorderColumns) > 0 {
-		of.AddTableMiddleware(table.NewReorderColumnOrderMiddleware(ffs.ReorderColumns))
+		p_.AddRowMiddleware(row.NewReorderColumnOrderMiddleware(ffs.ReorderColumns))
 	}
 	if len(ffs.RemoveDuplicates) > 0 {
-		of.AddTableMiddleware(table.NewRemoveDuplicatesMiddleware(ffs.RemoveDuplicates...))
+		p_.AddRowMiddleware(row.NewRemoveDuplicatesMiddleware(ffs.RemoveDuplicates...))
 	}
 }
diff --git a/pkg/settings/settings_output.go b/pkg/settings/settings_output.go
index b043709..90af936 100644
--- a/pkg/settings/settings_output.go
+++ b/pkg/settings/settings_output.go
@@ -13,7 +13,8 @@ import (
 	template_formatter "github.com/go-go-golems/glazed/pkg/formatters/template"
 	"github.com/go-go-golems/glazed/pkg/formatters/yaml"
 	"github.com/go-go-golems/glazed/pkg/helpers/templating"
-	"github.com/go-go-golems/glazed/pkg/middlewares/table"
+	"github.com/go-go-golems/glazed/pkg/middlewares"
+	"github.com/go-go-golems/glazed/pkg/middlewares/row"
 	"github.com/pkg/errors"
 	"text/template"
 	"unicode/utf8"
@@ -71,7 +72,7 @@ func NewOutputFormatterSettings(ps map[string]interface{}) (*OutputFormatterSett
 	return s, nil
 }
 
-func (ofs *OutputFormatterSettings) CreateOutputFormatter() (formatters.OutputFormatter, error) {
+func (ofs *OutputFormatterSettings) CreateOutputFormatter(mwProcessor *middlewares.Processor) (formatters.OutputFormatter, error) {
 	if ofs.Output == "csv" {
 		ofs.Output = "table"
 		ofs.TableFormat = "csv"
@@ -118,7 +119,7 @@ func (ofs *OutputFormatterSettings) CreateOutputFormatter() (formatters.OutputFo
 			excel.WithSheetName(ofs.SheetName),
 			excel.WithOutputFile(ofs.OutputFile),
 		)
-		of.AddTableMiddleware(table.NewFlattenObjectMiddleware())
+		mwProcessor.AddRowMiddlewareInFront(row.NewFlattenObjectMiddleware())
 	} else if ofs.Output == "table" {
 		if ofs.TableFormat == "csv" {
 			csvOf := csv.NewCSVOutputFormatter(
@@ -149,7 +150,7 @@ func (ofs *OutputFormatterSettings) CreateOutputFormatter() (formatters.OutputFo
 				table_formatter.WithPrintTableStyle(ofs.PrintTableStyle),
 			)
 		}
-		of.AddTableMiddleware(table.NewFlattenObjectMiddleware())
+		mwProcessor.AddRowMiddlewareInFront(row.NewFlattenObjectMiddleware())
 	} else if ofs.Output == "template" {
 		if ofs.TemplateFormatterSettings == nil {
 			ofs.TemplateFormatterSettings = &TemplateFormatterSettings{
diff --git a/pkg/settings/settings_rename.go b/pkg/settings/settings_rename.go
index b379435..99800c2 100644
--- a/pkg/settings/settings_rename.go
+++ b/pkg/settings/settings_rename.go
@@ -3,9 +3,9 @@ package settings
 import (
 	_ "embed"
 	"github.com/go-go-golems/glazed/pkg/cmds/layers"
-	"github.com/go-go-golems/glazed/pkg/formatters"
 	"github.com/go-go-golems/glazed/pkg/helpers/cast"
-	"github.com/go-go-golems/glazed/pkg/middlewares/table"
+	"github.com/go-go-golems/glazed/pkg/middlewares"
+	"github.com/go-go-golems/glazed/pkg/middlewares/row"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/pkg/errors"
 	"gopkg.in/yaml.v3"
@@ -16,13 +16,13 @@ import (
 
 type RenameSettings struct {
 	RenameFields  map[types.FieldName]string
-	RenameRegexps table.RegexpReplacements
+	RenameRegexps row.RegexpReplacements
 	YamlFile      string
 }
 
-func (rs *RenameSettings) AddMiddlewares(of formatters.OutputFormatter) error {
+func (rs *RenameSettings) AddMiddlewares(processor *middlewares.Processor) error {
 	if len(rs.RenameFields) > 0 || len(rs.RenameRegexps) > 0 {
-		of.AddTableMiddleware(table.NewRenameColumnMiddleware(rs.RenameFields, rs.RenameRegexps))
+		processor.AddRowMiddleware(row.NewRenameColumnMiddleware(rs.RenameFields, rs.RenameRegexps))
 	}
 
 	if rs.YamlFile != "" {
@@ -32,12 +32,12 @@ func (rs *RenameSettings) AddMiddlewares(of formatters.OutputFormatter) error {
 		}
 		decoder := yaml.NewDecoder(f)
 
-		mw, err := table.NewRenameColumnMiddlewareFromYAML(decoder)
+		mw, err := row.NewRenameColumnMiddlewareFromYAML(decoder)
 		if err != nil {
 			return err
 		}
 
-		of.AddTableMiddleware(mw)
+		processor.AddRowMiddleware(mw)
 	}
 
 	return nil
@@ -70,7 +70,7 @@ func NewRenameSettingsFromParameters(ps map[string]interface{}) (*RenameSettings
 	if ps["rename"] == nil {
 		return &RenameSettings{
 			RenameFields:  map[types.FieldName]string{},
-			RenameRegexps: table.RegexpReplacements{},
+			RenameRegexps: row.RegexpReplacements{},
 		}, nil
 	}
 
@@ -87,7 +87,7 @@ func NewRenameSettingsFromParameters(ps map[string]interface{}) (*RenameSettings
 		renamesFieldsMap[parts[0]] = parts[1]
 	}
 
-	regexpReplacements := table.RegexpReplacements{}
+	regexpReplacements := row.RegexpReplacements{}
 	renameRegexpFields, ok := ps["rename-regexp"].(map[string]interface{})
 	if !ok {
 		return nil, errors.Errorf("Invalid rename regexp fields")
@@ -102,7 +102,7 @@ func NewRenameSettingsFromParameters(ps map[string]interface{}) (*RenameSettings
 			return nil, errors.Wrapf(err, "Invalid regexp: %s", regex)
 		}
 		regexpReplacements = append(regexpReplacements,
-			&table.RegexpReplacement{Regexp: re, Replacement: replacement_})
+			&row.RegexpReplacement{Regexp: re, Replacement: replacement_})
 	}
 
 	renameYaml, ok := ps["rename-yaml"].(string)
diff --git a/pkg/settings/settings_replace.go b/pkg/settings/settings_replace.go
index bd64f7a..260e491 100644
--- a/pkg/settings/settings_replace.go
+++ b/pkg/settings/settings_replace.go
@@ -4,8 +4,8 @@ import (
 	_ "embed"
 	"github.com/go-go-golems/glazed/pkg/cmds/layers"
 	"github.com/go-go-golems/glazed/pkg/cmds/parameters"
-	"github.com/go-go-golems/glazed/pkg/formatters"
-	"github.com/go-go-golems/glazed/pkg/middlewares/table"
+	"github.com/go-go-golems/glazed/pkg/middlewares"
+	"github.com/go-go-golems/glazed/pkg/middlewares/row"
 	"github.com/pkg/errors"
 	"os"
 )
@@ -15,24 +15,24 @@ type ReplaceSettings struct {
 	AddFields   map[string]string `glazed.parameter:"add-fields"`
 }
 
-func (rs *ReplaceSettings) AddMiddlewares(of formatters.OutputFormatter) error {
+func (rs *ReplaceSettings) AddMiddlewares(of *middlewares.Processor) error {
 	if rs.ReplaceFile != "" {
 		b, err := os.ReadFile(rs.ReplaceFile)
 		if err != nil {
 			return err
 		}
 
-		mw, err := table.NewReplaceMiddlewareFromYAML(b)
+		mw, err := row.NewReplaceMiddlewareFromYAML(b)
 		if err != nil {
 			return err
 		}
 
-		of.AddTableMiddleware(mw)
+		of.AddRowMiddleware(mw)
 	}
 
 	if len(rs.AddFields) > 0 {
-		mw := table.NewAddFieldMiddleware(rs.AddFields)
-		of.AddTableMiddleware(mw)
+		mw := row.NewAddFieldMiddleware(rs.AddFields)
+		of.AddRowMiddleware(mw)
 	}
 
 	return nil
diff --git a/pkg/settings/settings_sort.go b/pkg/settings/settings_sort.go
index 15665c6..acab480 100644
--- a/pkg/settings/settings_sort.go
+++ b/pkg/settings/settings_sort.go
@@ -4,7 +4,7 @@ import (
 	_ "embed"
 	"github.com/go-go-golems/glazed/pkg/cmds/layers"
 	"github.com/go-go-golems/glazed/pkg/cmds/parameters"
-	"github.com/go-go-golems/glazed/pkg/formatters"
+	"github.com/go-go-golems/glazed/pkg/middlewares"
 	"github.com/go-go-golems/glazed/pkg/middlewares/table"
 	"github.com/pkg/errors"
 )
@@ -41,9 +41,9 @@ func NewSortParameterLayer(options ...layers.ParameterLayerOptions) (*SortParame
 	return ret, nil
 }
 
-func (s *SortFlagsSettings) AddMiddlewares(of formatters.OutputFormatter) {
+func (s *SortFlagsSettings) AddMiddlewares(p_ *middlewares.Processor) {
 	if len(s.SortBy) == 0 {
 		return
 	}
-	of.AddTableMiddleware(table.NewSortByMiddlewareFromColumns(s.SortBy...))
+	p_.AddTableMiddleware(table.NewSortByMiddlewareFromColumns(s.SortBy...))
 }
diff --git a/pkg/settings/settings_template.go b/pkg/settings/settings_template.go
index 0c151f6..94c8a62 100644
--- a/pkg/settings/settings_template.go
+++ b/pkg/settings/settings_template.go
@@ -3,8 +3,8 @@ package settings
 import (
 	_ "embed"
 	"github.com/go-go-golems/glazed/pkg/cmds/layers"
-	"github.com/go-go-golems/glazed/pkg/formatters"
-	"github.com/go-go-golems/glazed/pkg/middlewares/table"
+	"github.com/go-go-golems/glazed/pkg/middlewares"
+	"github.com/go-go-golems/glazed/pkg/middlewares/row"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/pkg/errors"
 )
@@ -18,13 +18,13 @@ type TemplateSettings struct {
 //go:embed "flags/template.yaml"
 var templateFlagsYaml []byte
 
-func (tf *TemplateSettings) AddMiddlewares(of formatters.OutputFormatter) error {
+func (tf *TemplateSettings) AddMiddlewares(p_ *middlewares.Processor) error {
 	if tf.UseRowTemplates && len(tf.Templates) > 0 {
-		middleware, err := table.NewRowGoTemplateMiddleware(tf.Templates, tf.RenameSeparator)
+		middleware, err := row.NewTemplateMiddleware(tf.Templates, tf.RenameSeparator)
 		if err != nil {
 			return err
 		}
-		of.AddTableMiddleware(middleware)
+		p_.AddRowMiddleware(middleware)
 	}
 
 	return nil
diff --git a/pkg/types/mod.go b/pkg/types/mod.go
index cd0f50b..646a80d 100644
--- a/pkg/types/mod.go
+++ b/pkg/types/mod.go
@@ -1,73 +1,58 @@
 package types
 
+import (
+	orderedmap "github.com/wk8/go-ordered-map/v2"
+	"sort"
+)
+
 type TableName = string
 type FieldName = string
 type GenericCellValue = interface{}
-type MapRow = map[FieldName]GenericCellValue
-
-type Row interface {
-	GetFields() []FieldName
-	GetValues() MapRow
-}
+type Row = *orderedmap.OrderedMap[FieldName, GenericCellValue]
+type MapRowPair = orderedmap.Pair[FieldName, GenericCellValue]
 
-type Table struct {
-	Columns   []FieldName
-	Rows      []Row
-	finalized bool
+func NewRow(initialData ...MapRowPair) Row {
+	return orderedmap.New[FieldName, GenericCellValue](
+		orderedmap.WithInitialData(initialData...),
+	)
 }
 
-// Finalize is used to "close" a table after processing inputs into it.
-//
-// TODO(manuel, 2023-02-19) This is an ugly ugly method, and really the whole Table/middleware structure needs to be refactored
-// See https://github.com/go-go-golems/glazed/issues/146
-func (t *Table) Finalize() {
-	if t.finalized {
-		return
-	}
+func NewRowFromMap(hash map[FieldName]GenericCellValue) Row {
+	ret := NewRow()
 
-	existingColumns := map[FieldName]interface{}{}
-	for _, column := range t.Columns {
-		existingColumns[column] = nil
+	// get keys of hash and sorted them
+	sortedKeys := []FieldName{}
+	for k := range hash {
+		sortedKeys = append(sortedKeys, k)
 	}
+	sort.Strings(sortedKeys)
 
-	// append all the columns from all the rows into a map
-	columnNames := map[FieldName]interface{}{}
-
-	for _, row := range t.Rows {
-		for _, field := range row.GetFields() {
-			columnNames[field] = nil
-		}
+	for _, k := range sortedKeys {
+		ret.Set(k, hash[k])
 	}
-
-	for key := range columnNames {
-		if _, ok := existingColumns[key]; !ok {
-			t.Columns = append(t.Columns, key)
-		}
-	}
-
-	t.finalized = true
+	return ret
 }
 
-func NewTable() *Table {
-	return &Table{
-		Columns:   []FieldName{},
-		Rows:      []Row{},
-		finalized: false,
+func NewRowFromMapWithColumns(hash map[FieldName]GenericCellValue, columns []FieldName) Row {
+	ret := NewRow()
+	for _, column := range columns {
+		v, ok := hash[column]
+		if !ok {
+			continue
+		}
+		ret.Set(column, v)
 	}
+	return ret
 }
 
-type SimpleRow struct {
-	Hash MapRow
+func MRP(key FieldName, value GenericCellValue) MapRowPair {
+	return orderedmap.Pair[FieldName, GenericCellValue]{Key: key, Value: value}
 }
 
-func (sr *SimpleRow) GetFields() []FieldName {
+func GetFields(om Row) []FieldName {
 	ret := []FieldName{}
-	for key := range sr.Hash {
-		ret = append(ret, key)
+	for pair := om.Oldest(); pair != nil; pair = pair.Next() {
+		ret = append(ret, pair.Key)
 	}
 	return ret
 }
-
-func (sr *SimpleRow) GetValues() MapRow {
-	return sr.Hash
-}
diff --git a/pkg/types/table.go b/pkg/types/table.go
new file mode 100644
index 0000000..91d5291
--- /dev/null
+++ b/pkg/types/table.go
@@ -0,0 +1,49 @@
+package types
+
+type Table struct {
+	Columns   []FieldName
+	Rows      []Row
+	finalized bool
+}
+
+func (t *Table) AddRows(rows ...Row) {
+	// TODO(manuel, 2023-06-27) Update the Columns field when adding rows
+	// This might be not very efficient, but it's not like computing it all at the end is great too.
+	// This might be something that could be set as an option.
+	for _, row := range rows {
+		columns := GetFields(row)
+		t.SetColumnOrder(columns)
+	}
+	t.Rows = append(t.Rows, rows...)
+}
+
+// SetColumnOrder will set the given columns to be the first one to be output.
+// Other columns already present in the order will be appended at the end, preserving the original order.
+func (t *Table) SetColumnOrder(columns []FieldName) {
+	existingColumns := map[FieldName]interface{}{}
+	for _, column := range t.Columns {
+		existingColumns[column] = nil
+	}
+
+	columnsToAppend := []FieldName{}
+
+	for _, column := range columns {
+		delete(existingColumns, column)
+	}
+
+	for _, column := range t.Columns {
+		if _, ok := existingColumns[column]; ok {
+			columnsToAppend = append(columnsToAppend, column)
+		}
+	}
+
+	t.Columns = append(columns, columnsToAppend...)
+}
+
+func NewTable() *Table {
+	return &Table{
+		Columns:   []FieldName{},
+		Rows:      []Row{},
+		finalized: false,
+	}
+}


Test patch:
diff --git a/cmd/glaze/cmds/html/parse_test.go b/cmd/glaze/cmds/html/parse_test.go
index f2b488d..f6f622c 100644
--- a/cmd/glaze/cmds/html/parse_test.go
+++ b/cmd/glaze/cmds/html/parse_test.go
@@ -3,9 +3,9 @@ package html
 import (
 	"context"
 	"github.com/go-go-golems/glazed/pkg/formatters"
+	assert2 "github.com/go-go-golems/glazed/pkg/helpers/assert"
 	"github.com/go-go-golems/glazed/pkg/middlewares"
 	"github.com/go-go-golems/glazed/pkg/types"
-	"github.com/pkg/errors"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/net/html"
@@ -15,13 +15,15 @@ import (
 )
 
 type TestProcessor struct {
-	Objects   []map[string]interface{}
+	Objects   []types.Row
 	formatter formatters.OutputFormatter
+	processor *middlewares.Processor
 }
 
 func NewTestProcessor() *TestProcessor {
 	return &TestProcessor{
 		formatter: &TestFormatter{},
+		processor: middlewares.NewProcessor(),
 	}
 }
 
@@ -31,30 +33,11 @@ func (t TestFormatter) ContentType() string {
 	return "text/plain"
 }
 
-func (t TestFormatter) AddRow(row types.Row) {
-}
-
-func (t TestFormatter) SetColumnOrder(columnOrder []types.FieldName) {
-}
-
-func (t TestFormatter) AddTableMiddleware(m middlewares.TableMiddleware) {
-}
-
-func (t TestFormatter) AddTableMiddlewareInFront(m middlewares.TableMiddleware) {
-}
-
-func (t TestFormatter) AddTableMiddlewareAtIndex(i int, m middlewares.TableMiddleware) {
-}
-
-func (t TestFormatter) GetTable() (*types.Table, error) {
-	return nil, errors.New("not implemented")
-}
-
-func (t TestFormatter) Output(context.Context, io.Writer) error {
+func (t TestFormatter) Output(context.Context, *types.Table, io.Writer) error {
 	return nil
 }
 
-func (t *TestProcessor) ProcessInputObject(ctx context.Context, obj map[string]interface{}) error {
+func (t *TestProcessor) ProcessInputObject(ctx context.Context, obj types.Row) error {
 	t.Objects = append(t.Objects, obj)
 	return nil
 }
@@ -63,6 +46,10 @@ func (t *TestProcessor) OutputFormatter() formatters.OutputFormatter {
 	return nil
 }
 
+func (t *TestProcessor) Processor() *middlewares.Processor {
+	return t.processor
+}
+
 func TestSimpleHeaderParse(t *testing.T) {
 	gp := NewTestProcessor()
 	html_ := "<h1>Header</h1>"
@@ -78,9 +65,9 @@ func TestSimpleHeaderParse(t *testing.T) {
 	require.NoError(t, err)
 	assert.Nil(t, n)
 
-	assert.Equal(t, 1, len(gp.Objects))
-	assert.Equal(t, "Header", gp.Objects[0]["Title"])
-	assert.Equal(t, "h1", gp.Objects[0]["Tag"])
+	require.Equal(t, 1, len(gp.Objects))
+	assert2.EqualMapRowValue(t, "Header", gp.Objects[0], "Title")
+	assert2.EqualMapRowValue(t, "h1", gp.Objects[0], "Tag")
 }
 
 func TestTwoHeadersParse(t *testing.T) {
@@ -99,10 +86,12 @@ func TestTwoHeadersParse(t *testing.T) {
 	assert.Nil(t, n)
 
 	assert.Equal(t, 2, len(gp.Objects))
-	assert.Equal(t, "Header", gp.Objects[0]["Title"])
-	assert.Equal(t, "h1", gp.Objects[0]["Tag"])
-	assert.Equal(t, "Subheader", gp.Objects[1]["Title"])
-	assert.Equal(t, "h2", gp.Objects[1]["Tag"])
+
+	assert2.EqualMapRowValue(t, "Header", gp.Objects[0], "Title")
+	assert2.EqualMapRowValue(t, "h1", gp.Objects[0], "Tag")
+
+	assert2.EqualMapRowValue(t, "Subheader", gp.Objects[1], "Title")
+	assert2.EqualMapRowValue(t, "h2", gp.Objects[1], "Tag")
 }
 
 func TestTwoHeadersBody(t *testing.T) {
@@ -131,10 +120,12 @@ func TestTwoHeadersBody(t *testing.T) {
 	assert.Nil(t, n)
 
 	assert.Equal(t, 2, len(gp.Objects))
-	assert.Equal(t, "Header", gp.Objects[0]["Title"])
-	assert.Equal(t, "h1", gp.Objects[0]["Tag"])
-	assert.Equal(t, "Subheader", gp.Objects[1]["Title"])
-	assert.Equal(t, "h2", gp.Objects[1]["Tag"])
+
+	assert2.EqualMapRowValue(t, "Header", gp.Objects[0], "Title")
+	assert2.EqualMapRowValue(t, "h1", gp.Objects[0], "Tag")
+
+	assert2.EqualMapRowValue(t, "Subheader", gp.Objects[1], "Title")
+	assert2.EqualMapRowValue(t, "h2", gp.Objects[1], "Tag")
 }
 
 func TestTwoHeadersSomeTextBody(t *testing.T) {
@@ -165,13 +156,14 @@ func TestTwoHeadersSomeTextBody(t *testing.T) {
 	assert.Nil(t, n)
 
 	assert.Equal(t, 2, len(gp.Objects))
-	assert.Equal(t, "Header", gp.Objects[0]["Title"])
-	assert.Equal(t, "h1", gp.Objects[0]["Tag"])
-	assert.Equal(t, "<p>Some text</p>", gp.Objects[0]["Body"])
 
-	assert.Equal(t, "Subheader", gp.Objects[1]["Title"])
-	assert.Equal(t, "h2", gp.Objects[1]["Tag"])
-	assert.Equal(t, "<p>Some text</p>", gp.Objects[1]["Body"])
+	assert2.EqualMapRowValue(t, "Header", gp.Objects[0], "Title")
+	assert2.EqualMapRowValue(t, "h1", gp.Objects[0], "Tag")
+	assert2.EqualMapRowValue(t, "<p>Some text</p>", gp.Objects[0], "Body")
+
+	assert2.EqualMapRowValue(t, "Subheader", gp.Objects[1], "Title")
+	assert2.EqualMapRowValue(t, "h2", gp.Objects[1], "Tag")
+	assert2.EqualMapRowValue(t, "<p>Some text</p>", gp.Objects[1], "Body")
 }
 
 func TestTwoHeadersSomeTextNodes(t *testing.T) {
@@ -204,13 +196,14 @@ func TestTwoHeadersSomeTextNodes(t *testing.T) {
 	assert.Nil(t, n)
 
 	assert.Equal(t, 2, len(gp.Objects))
-	assert.Equal(t, "Header", gp.Objects[0]["Title"])
-	assert.Equal(t, "h1", gp.Objects[0]["Tag"])
-	assert.Equal(t, "<p>Some text</p>\n\t\t<p>Some text2</p>\n\t\t<p>Some text3</p>", gp.Objects[0]["Body"])
 
-	assert.Equal(t, "Subheader", gp.Objects[1]["Title"])
-	assert.Equal(t, "h2", gp.Objects[1]["Tag"])
-	assert.Equal(t, "<p>Some text</p>", gp.Objects[1]["Body"])
+	assert2.EqualMapRowValue(t, "Header", gp.Objects[0], "Title")
+	assert2.EqualMapRowValue(t, "h1", gp.Objects[0], "Tag")
+	assert2.EqualMapRowValue(t, "<p>Some text</p>\n\t\t<p>Some text2</p>\n\t\t<p>Some text3</p>", gp.Objects[0], "Body")
+
+	assert2.EqualMapRowValue(t, "Subheader", gp.Objects[1], "Title")
+	assert2.EqualMapRowValue(t, "h2", gp.Objects[1], "Tag")
+	assert2.EqualMapRowValue(t, "<p>Some text</p>", gp.Objects[1], "Body")
 
 	gp = NewTestProcessor()
 	hsp = NewHTMLHeadingSplitParser(gp, []string{"p"})
@@ -220,13 +213,14 @@ func TestTwoHeadersSomeTextNodes(t *testing.T) {
 	assert.Nil(t, n)
 
 	assert.Equal(t, 2, len(gp.Objects))
-	assert.Equal(t, "Header", gp.Objects[0]["Title"])
-	assert.Equal(t, "h1", gp.Objects[0]["Tag"])
-	assert.Equal(t, "Some text\n\t\tSome text2\n\t\tSome text3", gp.Objects[0]["Body"])
 
-	assert.Equal(t, "Subheader", gp.Objects[1]["Title"])
-	assert.Equal(t, "h2", gp.Objects[1]["Tag"])
-	assert.Equal(t, "Some text", gp.Objects[1]["Body"])
+	assert2.EqualMapRowValue(t, "Header", gp.Objects[0], "Title")
+	assert2.EqualMapRowValue(t, "h1", gp.Objects[0], "Tag")
+	assert2.EqualMapRowValue(t, "Some text\n\t\tSome text2\n\t\tSome text3", gp.Objects[0], "Body")
+
+	assert2.EqualMapRowValue(t, "Subheader", gp.Objects[1], "Title")
+	assert2.EqualMapRowValue(t, "h2", gp.Objects[1], "Tag")
+	assert2.EqualMapRowValue(t, "Some text", gp.Objects[1], "Body")
 }
 
 func TestStripTags(t *testing.T) {
@@ -256,13 +250,14 @@ func TestStripTags(t *testing.T) {
 	assert.Nil(t, n)
 
 	assert.Equal(t, 2, len(gp.Objects))
-	assert.Equal(t, "Header", gp.Objects[0]["Title"])
-	assert.Equal(t, "h1", gp.Objects[0]["Tag"])
-	assert.Equal(t, "Some text<b>Foobar</b> <strong>Test</strong>", gp.Objects[0]["Body"])
 
-	assert.Equal(t, "Subheader <strong>Foobar</strong>Test", gp.Objects[1]["Title"])
-	assert.Equal(t, "h2", gp.Objects[1]["Tag"])
-	assert.Equal(t, "Some text", gp.Objects[1]["Body"])
+	assert2.EqualMapRowValue(t, "Header", gp.Objects[0], "Title")
+	assert2.EqualMapRowValue(t, "h1", gp.Objects[0], "Tag")
+	assert2.EqualMapRowValue(t, "Some text<b>Foobar</b> <strong>Test</strong>", gp.Objects[0], "Body")
+
+	assert2.EqualMapRowValue(t, "Subheader <strong>Foobar</strong>Test", gp.Objects[1], "Title")
+	assert2.EqualMapRowValue(t, "h2", gp.Objects[1], "Tag")
+	assert2.EqualMapRowValue(t, "Some text", gp.Objects[1], "Body")
 }
 
 func TestSplitOtherTags(t *testing.T) {
@@ -282,10 +277,12 @@ func TestSplitOtherTags(t *testing.T) {
 	assert.Nil(t, n)
 
 	assert.Equal(t, 2, len(gp.Objects))
-	assert.Equal(t, "Foobar", gp.Objects[0]["Title"])
-	assert.Equal(t, "p", gp.Objects[0]["Tag"])
-	assert.Equal(t, "Test", gp.Objects[1]["Title"])
-	assert.Equal(t, "p", gp.Objects[1]["Tag"])
+
+	assert2.EqualMapRowValue(t, "Foobar", gp.Objects[0], "Title")
+	assert2.EqualMapRowValue(t, "p", gp.Objects[0], "Tag")
+
+	assert2.EqualMapRowValue(t, "Test", gp.Objects[1], "Title")
+	assert2.EqualMapRowValue(t, "p", gp.Objects[1], "Tag")
 }
 
 func TestSplitOtherTagsWithoutTitle(t *testing.T) {
@@ -305,12 +302,14 @@ func TestSplitOtherTagsWithoutTitle(t *testing.T) {
 	assert.Nil(t, n)
 
 	assert.Equal(t, 2, len(gp.Objects))
-	assert.Equal(t, "", gp.Objects[0]["Title"])
-	assert.Equal(t, "p", gp.Objects[0]["Tag"])
-	assert.Equal(t, "Foobar", gp.Objects[0]["Body"])
-	assert.Equal(t, "", gp.Objects[1]["Title"])
-	assert.Equal(t, "p", gp.Objects[1]["Tag"])
-	assert.Equal(t, "Test", gp.Objects[1]["Body"])
+
+	assert2.EqualMapRowValue(t, "", gp.Objects[0], "Title")
+	assert2.EqualMapRowValue(t, "p", gp.Objects[0], "Tag")
+	assert2.EqualMapRowValue(t, "Foobar", gp.Objects[0], "Body")
+
+	assert2.EqualMapRowValue(t, "", gp.Objects[1], "Title")
+	assert2.EqualMapRowValue(t, "p", gp.Objects[1], "Tag")
+	assert2.EqualMapRowValue(t, "Test", gp.Objects[1], "Body")
 
 	gp = NewTestProcessor()
 	hsp = NewHTMLSplitParser(gp, []string{}, []string{"p"}, false)
@@ -320,10 +319,12 @@ func TestSplitOtherTagsWithoutTitle(t *testing.T) {
 	assert.Nil(t, n)
 
 	assert.Equal(t, 2, len(gp.Objects))
-	assert.Equal(t, "", gp.Objects[0]["Title"])
-	assert.Equal(t, "p", gp.Objects[0]["Tag"])
-	assert.Equal(t, "<p>Foobar</p>", gp.Objects[0]["Body"])
-	assert.Equal(t, "", gp.Objects[1]["Title"])
-	assert.Equal(t, "p", gp.Objects[1]["Tag"])
-	assert.Equal(t, "<p>Test</p>", gp.Objects[1]["Body"])
+
+	assert2.EqualMapRowValue(t, "", gp.Objects[0], "Title")
+	assert2.EqualMapRowValue(t, "p", gp.Objects[0], "Tag")
+	assert2.EqualMapRowValue(t, "<p>Foobar</p>", gp.Objects[0], "Body")
+
+	assert2.EqualMapRowValue(t, "", gp.Objects[1], "Title")
+	assert2.EqualMapRowValue(t, "p", gp.Objects[1], "Tag")
+	assert2.EqualMapRowValue(t, "<p>Test</p>", gp.Objects[1], "Body")
 }
diff --git a/pkg/formatters/csv/csv_test.go b/pkg/formatters/csv/csv_test.go
index e4e9936..254d4b3 100644
--- a/pkg/formatters/csv/csv_test.go
+++ b/pkg/formatters/csv/csv_test.go
@@ -4,7 +4,8 @@ import (
 	"bytes"
 	"context"
 	"github.com/go-go-golems/glazed/pkg/helpers/csv"
-	"github.com/go-go-golems/glazed/pkg/middlewares/table"
+	"github.com/go-go-golems/glazed/pkg/middlewares"
+	"github.com/go-go-golems/glazed/pkg/middlewares/row"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -17,15 +18,22 @@ func TestCSVRenameEndToEnd(t *testing.T) {
 	renames := map[string]string{
 		"a": "b",
 	}
-	of.AddTableMiddleware(&table.RenameColumnMiddleware{Renames: renames})
-	of.AddRow(&types.SimpleRow{Hash: map[string]interface{}{"a": 1}})
+	p_ := middlewares.NewProcessor(middlewares.WithRowMiddleware(row.NewFieldRenameColumnMiddleware(renames)))
 	ctx := context.Background()
+	err := p_.AddRow(ctx, types.NewRow(types.MRP("a", 1)))
+	require.NoError(t, err)
+
+	err = p_.FinalizeTable(ctx)
+	require.NoError(t, err)
+	table_ := p_.GetTable()
+
 	buf := &bytes.Buffer{}
-	err := of.Output(ctx, buf)
+	err = of.Output(ctx, table_, buf)
 	require.NoError(t, err)
 
 	_, data, err := csv.ParseCSV(strings.NewReader(buf.String()))
 	require.NoError(t, err)
+
 	require.Len(t, data, 1)
 	v, ok := data[0]["b"]
 	assert.True(t, ok)
diff --git a/pkg/formatters/json/json_test.go b/pkg/formatters/json/json_test.go
index 6f5f9d9..4a4ce6c 100644
--- a/pkg/formatters/json/json_test.go
+++ b/pkg/formatters/json/json_test.go
@@ -4,7 +4,8 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
-	"github.com/go-go-golems/glazed/pkg/middlewares/table"
+	"github.com/go-go-golems/glazed/pkg/middlewares"
+	"github.com/go-go-golems/glazed/pkg/middlewares/row"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -16,22 +17,29 @@ func TestJSONRenameEndToEnd(t *testing.T) {
 	renames := map[string]string{
 		"a": "b",
 	}
-	of.AddTableMiddleware(&table.RenameColumnMiddleware{Renames: renames})
-	of.AddRow(&types.SimpleRow{Hash: map[string]interface{}{"a": 1}})
+	obj := types.NewRow(types.MRP("a", 1))
 	ctx := context.Background()
+
+	p_ := middlewares.NewProcessor(middlewares.WithRowMiddleware(row.NewFieldRenameColumnMiddleware(renames)))
+	err := p_.AddRow(ctx, obj)
+	require.NoError(t, err)
+	err = p_.FinalizeTable(ctx)
+	require.NoError(t, err)
+
 	buf := &bytes.Buffer{}
-	err := of.Output(ctx, buf)
+
+	err = of.Output(ctx, p_.GetTable(), buf)
 	require.NoError(t, err)
 
 	s := buf.String()
 	// parse s
-	data := []map[string]interface{}{}
+	data := []types.Row{}
 	err = json.Unmarshal([]byte(s), &data)
 	require.NoError(t, err)
 	require.Len(t, data, 1)
 
 	// check if the rename worked
-	v, ok := data[0]["b"]
+	v, ok := data[0].Get("b")
 	assert.True(t, ok)
 	assert.Equal(t, 1.0, v)
 }
diff --git a/pkg/formatters/table/table_test.go b/pkg/formatters/table/table_test.go
index 82c556d..5f9468c 100644
--- a/pkg/formatters/table/table_test.go
+++ b/pkg/formatters/table/table_test.go
@@ -3,7 +3,8 @@ package table
 import (
 	"bytes"
 	"context"
-	"github.com/go-go-golems/glazed/pkg/middlewares/table"
+	"github.com/go-go-golems/glazed/pkg/middlewares"
+	"github.com/go-go-golems/glazed/pkg/middlewares/row"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -15,11 +16,18 @@ func TestTableRenameEndToEnd(t *testing.T) {
 	renames := map[string]string{
 		"a": "b",
 	}
-	of.AddTableMiddleware(&table.RenameColumnMiddleware{Renames: renames})
-	of.AddRow(&types.SimpleRow{Hash: map[string]interface{}{"a": 1}})
+
+	obj := types.NewRow(types.MRP("a", 1))
 	ctx := context.Background()
+
+	p_ := middlewares.NewProcessor(middlewares.WithRowMiddleware(row.NewFieldRenameColumnMiddleware(renames)))
+	err := p_.AddRow(ctx, obj)
+	require.NoError(t, err)
+	err = p_.FinalizeTable(ctx)
+	require.NoError(t, err)
+
 	buf := &bytes.Buffer{}
-	err := of.Output(ctx, buf)
+	err = of.Output(ctx, p_.GetTable(), buf)
 	require.NoError(t, err)
 
 	// parse s
diff --git a/pkg/formatters/template/template_test.go b/pkg/formatters/template/template_test.go
index 57d53ed..ebe85a4 100644
--- a/pkg/formatters/template/template_test.go
+++ b/pkg/formatters/template/template_test.go
@@ -5,7 +5,8 @@ import (
 	"context"
 	"github.com/Masterminds/sprig"
 	"github.com/go-go-golems/glazed/pkg/helpers/templating"
-	"github.com/go-go-golems/glazed/pkg/middlewares/table"
+	"github.com/go-go-golems/glazed/pkg/middlewares"
+	"github.com/go-go-golems/glazed/pkg/middlewares/row"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -24,11 +25,17 @@ func TestTemplateRenameEndToEnd(t *testing.T) {
 	renames := map[string]string{
 		"a": "b",
 	}
-	of.AddTableMiddleware(&table.RenameColumnMiddleware{Renames: renames})
-	of.AddRow(&types.SimpleRow{Hash: map[string]interface{}{"a": 1}})
+	obj := types.NewRow(types.MRP("a", 1))
 	ctx := context.Background()
+
+	p_ := middlewares.NewProcessor(middlewares.WithRowMiddleware(row.NewFieldRenameColumnMiddleware(renames)))
+	err := p_.AddRow(ctx, obj)
+	require.NoError(t, err)
+	err = p_.FinalizeTable(ctx)
+	require.NoError(t, err)
+
 	buf := &bytes.Buffer{}
-	err := of.Output(ctx, buf)
+	err = of.Output(ctx, p_.GetTable(), buf)
 	require.NoError(t, err)
 
 	assert.Equal(t, `1`, buf.String())
diff --git a/pkg/formatters/yaml/yaml_test.go b/pkg/formatters/yaml/yaml_test.go
index 95141e7..d6afcc4 100644
--- a/pkg/formatters/yaml/yaml_test.go
+++ b/pkg/formatters/yaml/yaml_test.go
@@ -3,7 +3,8 @@ package yaml
 import (
 	"bytes"
 	"context"
-	"github.com/go-go-golems/glazed/pkg/middlewares/table"
+	"github.com/go-go-golems/glazed/pkg/middlewares"
+	"github.com/go-go-golems/glazed/pkg/middlewares/row"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -16,11 +17,17 @@ func TestYAMLRenameEndToEnd(t *testing.T) {
 	renames := map[string]string{
 		"a": "b",
 	}
-	of.AddTableMiddleware(&table.RenameColumnMiddleware{Renames: renames})
-	of.AddRow(&types.SimpleRow{Hash: map[string]interface{}{"a": 1}})
+	obj := types.NewRow(types.MRP("a", 1))
 	ctx := context.Background()
+
+	p_ := middlewares.NewProcessor(middlewares.WithRowMiddleware(row.NewFieldRenameColumnMiddleware(renames)))
+	err := p_.AddRow(ctx, obj)
+	require.NoError(t, err)
+	err = p_.FinalizeTable(ctx)
+	require.NoError(t, err)
+
 	buf := bytes.Buffer{}
-	err := of.Output(ctx, &buf)
+	err = of.Output(ctx, p_.GetTable(), &buf)
 	require.NoError(t, err)
 
 	// parse s
diff --git a/pkg/middlewares/jq_test.go b/pkg/middlewares/jq_test.go
index 9244dc5..0fc3162 100644
--- a/pkg/middlewares/jq_test.go
+++ b/pkg/middlewares/jq_test.go
@@ -1,6 +1,8 @@
 package middlewares
 
 import (
+	"context"
+	assert2 "github.com/go-go-golems/glazed/pkg/helpers/assert"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -25,28 +27,24 @@ func createJqTestTable() *types.Table {
 	return &types.Table{
 		Columns: []types.FieldName{},
 		Rows: []types.Row{
-			&types.SimpleRow{
-				Hash: map[string]interface{}{
-					"a": 1,
-					"b": 2,
-					"c": map[string]interface{}{
-						"d": 3,
-					},
-					"e": "hello",
-					"f": []interface{}{1, 2, 3},
-				},
-			},
-			&types.SimpleRow{
-				Hash: map[string]interface{}{
-					"a": 11,
-					"c": map[string]interface{}{
-						"d": 13,
-						"e": 12,
-					},
-					"e": "foobar",
-					"f": []interface{}{1, 4, 2, 3},
-				},
-			},
+			types.NewRow(
+				types.MRP("a", 1),
+				types.MRP("b", 2),
+				types.MRP("c", map[string]interface{}{
+					"d": 3,
+				}),
+				types.MRP("e", "hello"),
+				types.MRP("f", []interface{}{1, 2, 3}),
+			),
+			types.NewRow(
+				types.MRP("a", 11),
+				types.MRP("c", map[string]interface{}{
+					"d": 13,
+					"e": 12,
+				}),
+				types.MRP("e", "foobar"),
+				types.MRP("f", []interface{}{1, 4, 2, 3}),
+			),
 		},
 	}
 }
@@ -55,61 +53,66 @@ func TestEmptyObjectMiddleware(t *testing.T) {
 	m := createJqObjectMiddleware(t, "")
 	require.Nil(t, m.query)
 
-	o := map[string]interface{}{"a": 1}
-	o2, err := m.Process(o)
-	assert.NoError(t, err)
+	ctx := context.Background()
+	obj := types.NewRow(types.MRP("a", 1))
+	o2, err := m.Process(ctx, obj)
+	require.NoError(t, err)
 	assert.Len(t, o2, 1)
-	assert.Equal(t, o, o2[0])
+	assert2.EqualMapRows(t, obj, o2[0])
 }
 
 func TestSimpleJqConstant(t *testing.T) {
 	m := createJqObjectMiddleware(t, "{a: 2}")
 	require.NotNil(t, m.query)
 
-	o := map[string]interface{}{"a": 1}
-	o2, err := m.Process(o)
-	assert.NoError(t, err)
+	ctx := context.Background()
+	o := types.NewRow(types.MRP("a", 1))
+	o2, err := m.Process(ctx, o)
+	require.NoError(t, err)
 	assert.Len(t, o2, 1)
-	expected := map[string]interface{}{"a": 2}
-	assert.Equal(t, expected, o2[0])
+	expected := types.NewRow(types.MRP("a", 2))
+	assert2.EqualMapRows(t, expected, o2[0])
 }
 
 func TestSimpleJqConstantArray(t *testing.T) {
 	m := createJqObjectMiddleware(t, "{a: [2]}")
 	require.NotNil(t, m.query)
 
-	o := map[string]interface{}{"a": 1}
-	o2, err := m.Process(o)
-	assert.NoError(t, err)
-	expected := map[string]interface{}{"a": []interface{}{2}}
+	ctx := context.Background()
+	o := types.NewRow(types.MRP("a", 1))
+	o2, err := m.Process(ctx, o)
+	require.NoError(t, err)
+	expected := types.NewRow(types.MRP("a", []interface{}{2}))
 	assert.Len(t, o2, 1)
-	assert.Equal(t, expected, o2[0])
+	assert2.EqualMapRows(t, expected, o2[0])
 }
 
 func TestSimpleJqExtractNestedArray(t *testing.T) {
 	m := createJqObjectMiddleware(t, ".a[0]")
 	require.NotNil(t, m.query)
 
-	o := map[string]interface{}{"a": []interface{}{map[string]interface{}{"b": 2}}}
-	o2, err := m.Process(o)
-	assert.NoError(t, err)
-	expected := map[string]interface{}{"b": 2}
+	ctx := context.Background()
+	o := types.NewRow(types.MRP("a", []interface{}{map[string]interface{}{"b": 2}}))
+	o2, err := m.Process(ctx, o)
+	require.NoError(t, err)
+	expected := types.NewRow(types.MRP("b", 2))
 	assert.Len(t, o2, 1)
-	assert.Equal(t, expected, o2[0])
+	assert2.EqualMapRows(t, expected, o2[0])
 }
 
 func TestSimpleJqExtract(t *testing.T) {
 	m := createJqObjectMiddleware(t, ".f | map({field: .})")
 	require.NotNil(t, m.query)
 
+	ctx := context.Background()
 	table := createJqTestTable()
-	v2 := table.Rows[0].GetValues()
-	o2, err := m.Process(v2)
-	assert.NoError(t, err)
+	v2 := table.Rows[0]
+	o2, err := m.Process(ctx, v2)
+	require.NoError(t, err)
 	assert.Len(t, o2, 3)
-	assert.Equal(t, map[string]interface{}{"field": 1}, o2[0])
-	assert.Equal(t, map[string]interface{}{"field": 2}, o2[1])
-	assert.Equal(t, map[string]interface{}{"field": 3}, o2[2])
+	assert2.EqualMapRowMap(t, map[string]interface{}{"field": 1}, o2[0])
+	assert2.EqualMapRowMap(t, map[string]interface{}{"field": 2}, o2[1])
+	assert2.EqualMapRowMap(t, map[string]interface{}{"field": 3}, o2[2])
 }
 
 func TestSimpleJqTableConstant(t *testing.T) {
@@ -117,17 +120,20 @@ func TestSimpleJqTableConstant(t *testing.T) {
 	require.NotNil(t, m)
 	require.NotEmpty(t, m.fieldQueries)
 
+	ctx := context.Background()
 	table := createJqTestTable()
-	t2, err := m.Process(table)
-	assert.NoError(t, err)
-
-	row := t2.Rows[0].GetValues()
-	assert.Equal(t, 2, row["a"])
-	assert.Equal(t, 2, row["b"])
-	assert.Equal(t, map[string]interface{}{"d": 3}, row["c"])
+	t2, err := m.Process(ctx, table)
+	require.NoError(t, err)
 
-	assert.Equal(t, "hello", row["e"])
-	assert.Equal(t, []interface{}{1, 2, 3}, row["f"])
+	row := t2.Rows[0]
+	v, ok := row.Get("a")
+	assert.True(t, ok)
+	assert.Equal(t, 2, v)
+	assert2.EqualMapRowValue(t, 2, row, "a")
+	assert2.EqualMapRowValue(t, 2, row, "b")
+	assert2.EqualMapRowValue(t, map[string]interface{}{"d": 3}, row, "c")
+	assert2.EqualMapRowValue(t, "hello", row, "e")
+	assert2.EqualMapRowValue(t, []interface{}{1, 2, 3}, row, "f")
 }
 
 func TestSimpleJqTableTwoFields(t *testing.T) {
@@ -135,15 +141,15 @@ func TestSimpleJqTableTwoFields(t *testing.T) {
 	require.NotNil(t, m)
 	require.NotEmpty(t, m.fieldQueries)
 
+	ctx := context.Background()
 	table := createJqTestTable()
-	t2, err := m.Process(table)
-	assert.NoError(t, err)
-
-	row := t2.Rows[0].GetValues()
-	assert.Equal(t, 2, row["a"])
-	assert.Equal(t, 2, row["b"])
-	assert.Equal(t, 3, row["c"])
+	t2, err := m.Process(ctx, table)
+	require.NoError(t, err)
 
-	assert.Equal(t, "hello", row["e"])
-	assert.Equal(t, []interface{}{1, 2, 3}, row["f"])
+	row := t2.Rows[0]
+	assert2.EqualMapRowValue(t, 2, row, "a")
+	assert2.EqualMapRowValue(t, 2, row, "b")
+	assert2.EqualMapRowValue(t, 3, row, "c")
+	assert2.EqualMapRowValue(t, "hello", row, "e")
+	assert2.EqualMapRowValue(t, []interface{}{1, 2, 3}, row, "f")
 }
diff --git a/pkg/middlewares/row/add-field_test.go b/pkg/middlewares/row/add-field_test.go
new file mode 100644
index 0000000..00a1f0f
--- /dev/null
+++ b/pkg/middlewares/row/add-field_test.go
@@ -0,0 +1,82 @@
+package row
+
+import (
+	"context"
+	assert2 "github.com/go-go-golems/glazed/pkg/helpers/assert"
+	"github.com/go-go-golems/glazed/pkg/middlewares"
+	"github.com/go-go-golems/glazed/pkg/types"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func createAddFieldTestRows() []types.Row {
+	return []types.Row{
+		types.NewRow(
+			types.MRP("field1", "skip"),
+			types.MRP("field2", "value2"),
+		),
+		types.NewRow(
+			types.MRP("field1", "value1"),
+			types.MRP("field2", "value3 blabla"),
+		),
+	}
+}
+
+func TestSingleAddField(t *testing.T) {
+	addFieldMiddleware := NewAddFieldMiddleware(map[string]string{
+		"field3": "value3",
+	})
+
+	rows := createAddFieldTestRows()
+	finalRows, err := processRows(addFieldMiddleware, rows)
+	require.NoError(t, err)
+	require.Equal(t, 2, len(finalRows))
+
+	row := finalRows[0]
+	assert2.EqualMapRowValue(t, "skip", row, "field1")
+	assert2.EqualMapRowValue(t, "value2", row, "field2")
+	assert2.EqualMapRowValue(t, "value3", row, "field3")
+
+	row = finalRows[1]
+	assert2.EqualMapRowValue(t, "value1", row, "field1")
+	assert2.EqualMapRowValue(t, "value3 blabla", row, "field2")
+	assert2.EqualMapRowValue(t, "value3", row, "field3")
+}
+
+func processRows(rm middlewares.RowMiddleware, rows []types.Row) ([]types.Row, error) {
+	finalRows := make([]types.Row, 0)
+	for _, row := range rows {
+		rows_, err := rm.Process(context.Background(), row)
+		if err != nil {
+			return nil, err
+		}
+		finalRows = append(finalRows, rows_...)
+	}
+
+	return finalRows, nil
+}
+
+func TestMultipleAddField(t *testing.T) {
+	addFieldMiddleware := NewAddFieldMiddleware(map[string]string{
+		"field3": "value3",
+		"field4": "value4",
+	})
+
+	rows := createAddFieldTestRows()
+	finalRows, err := processRows(addFieldMiddleware, rows)
+	require.NoError(t, err)
+
+	require.Equal(t, 2, len(finalRows))
+
+	row := finalRows[0]
+	assert2.EqualMapRowValue(t, "skip", row, "field1")
+	assert2.EqualMapRowValue(t, "value2", row, "field2")
+	assert2.EqualMapRowValue(t, "value3", row, "field3")
+	assert2.EqualMapRowValue(t, "value4", row, "field4")
+
+	row = finalRows[1]
+	assert2.EqualMapRowValue(t, "value1", row, "field1")
+	assert2.EqualMapRowValue(t, "value3 blabla", row, "field2")
+	assert2.EqualMapRowValue(t, "value3", row, "field3")
+	assert2.EqualMapRowValue(t, "value4", row, "field4")
+}
diff --git a/pkg/middlewares/row/fields-filter_test.go b/pkg/middlewares/row/fields-filter_test.go
new file mode 100644
index 0000000..3a587b0
--- /dev/null
+++ b/pkg/middlewares/row/fields-filter_test.go
@@ -0,0 +1,89 @@
+package row
+
+import (
+	assert2 "github.com/go-go-golems/glazed/pkg/helpers/assert"
+	"github.com/go-go-golems/glazed/pkg/types"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func createFieldsFilterTestRows() []types.Row {
+	return []types.Row{
+		types.NewRow(
+			types.MRP("a", 1),
+			types.MRP("b", 2),
+			types.MRP("c", 3),
+		),
+		types.NewRow(
+			types.MRP("a", 4),
+			types.MRP("b", 5),
+			types.MRP("c", 6),
+		),
+		types.NewRow(
+			types.MRP("a", 7),
+			types.MRP("b", 8),
+			types.MRP("c", 9),
+			types.MRP("d", 10),
+		),
+	}
+}
+
+func TestNoFieldsFilter(t *testing.T) {
+	mw := NewFieldsFilterMiddleware([]string{}, []string{})
+
+	rows := createFieldsFilterTestRows()
+	finalRows, err := processRows(mw, rows)
+	require.NoError(t, err)
+	require.Len(t, finalRows, 3)
+	assert2.EqualMapRowValues(t, rows[0], map[string]interface{}{"a": 1, "b": 2, "c": 3})
+	assert2.EqualMapRowValues(t, rows[1], map[string]interface{}{"a": 4, "b": 5, "c": 6})
+	assert2.EqualMapRowValues(t, rows[2], map[string]interface{}{"a": 7, "b": 8, "c": 9, "d": 10})
+}
+
+func TestFieldsA(t *testing.T) {
+	mw := NewFieldsFilterMiddleware([]string{"a"}, []string{})
+
+	rows := createFieldsFilterTestRows()
+	finalRows, err := processRows(mw, rows)
+	require.NoError(t, err)
+	require.Len(t, finalRows, 3)
+	assert2.EqualMapRowValues(t, rows[0], map[string]interface{}{"a": 1})
+	assert2.EqualMapRowValues(t, rows[1], map[string]interface{}{"a": 4})
+	assert2.EqualMapRowValues(t, rows[2], map[string]interface{}{"a": 7})
+}
+
+func TestFieldsBD(t *testing.T) {
+	mw := NewFieldsFilterMiddleware([]string{"b", "d"}, []string{})
+
+	rows := createFieldsFilterTestRows()
+	finalRows, err := processRows(mw, rows)
+	require.NoError(t, err)
+	require.Len(t, finalRows, 3)
+	assert2.EqualMapRowValues(t, rows[0], map[string]interface{}{"b": 2})
+	assert2.EqualMapRowValues(t, rows[1], map[string]interface{}{"b": 5})
+	assert2.EqualMapRowValues(t, rows[2], map[string]interface{}{"b": 8, "d": 10})
+}
+
+func TestFilterA(t *testing.T) {
+	mw := NewFieldsFilterMiddleware([]string{}, []string{"a"})
+
+	rows := createFieldsFilterTestRows()
+	finalRows, err := processRows(mw, rows)
+	require.NoError(t, err)
+	require.Len(t, finalRows, 3)
+	assert2.EqualMapRowValues(t, rows[0], map[string]interface{}{"b": 2, "c": 3})
+	assert2.EqualMapRowValues(t, rows[1], map[string]interface{}{"b": 5, "c": 6})
+	assert2.EqualMapRowValues(t, rows[2], map[string]interface{}{"b": 8, "c": 9, "d": 10})
+}
+
+func TestFilterBD(t *testing.T) {
+	mw := NewFieldsFilterMiddleware([]string{}, []string{"b", "d"})
+
+	rows := createFieldsFilterTestRows()
+	finalRows, err := processRows(mw, rows)
+	require.NoError(t, err)
+	require.Len(t, finalRows, 3)
+	assert2.EqualMapRowValues(t, rows[0], map[string]interface{}{"a": 1, "c": 3})
+	assert2.EqualMapRowValues(t, rows[1], map[string]interface{}{"a": 4, "c": 6})
+	assert2.EqualMapRowValues(t, rows[2], map[string]interface{}{"a": 7, "c": 9})
+}
diff --git a/pkg/middlewares/row/flatten-object_test.go b/pkg/middlewares/row/flatten-object_test.go
new file mode 100644
index 0000000..d23e58c
--- /dev/null
+++ b/pkg/middlewares/row/flatten-object_test.go
@@ -0,0 +1,129 @@
+package row
+
+import (
+	"context"
+	assert2 "github.com/go-go-golems/glazed/pkg/helpers/assert"
+	"github.com/go-go-golems/glazed/pkg/types"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func TestFlattenNoNestedObjects(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+	)
+
+	mw := NewFlattenObjectMiddleware()
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+
+	assert.Len(t, newRows, 1)
+	assert2.EqualMapRows(t, row, newRows[0])
+}
+
+func TestFlattenSingleNestedObject(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c", map[string]interface{}{
+			"d": "value3",
+		}),
+	)
+
+	mw := NewFlattenObjectMiddleware()
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+
+	assert.Len(t, newRows, 1)
+	assert2.EqualMapRows(t, types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c.d", "value3"),
+	), newRows[0])
+}
+
+func TestFlattenTwoObjects(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c", map[string]interface{}{
+			"d": "value3",
+			"a": "value4",
+		}),
+		types.MRP("e", map[string]interface{}{
+			"f": "value5",
+		}),
+	)
+
+	mw := NewFlattenObjectMiddleware()
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+
+	assert.Len(t, newRows, 1)
+	assert2.EqualMapRows(t, types.NewRow(
+		types.MRP("a", "value1"),
+
+		types.MRP("b", "value2"),
+		types.MRP("c.a", "value4"),
+		types.MRP("c.d", "value3"),
+		types.MRP("e.f", "value5"),
+	), newRows[0])
+}
+
+func TestNestedMapInterface(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c", map[string]interface{}{
+			"d": "value3",
+			"a": "value4",
+			"e": map[string]interface{}{
+				"f": "value5",
+			},
+		}),
+	)
+
+	mw := NewFlattenObjectMiddleware()
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+
+	assert.Len(t, newRows, 1)
+	assert2.EqualMapRows(t, types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c.a", "value4"),
+		types.MRP("c.d", "value3"),
+		types.MRP("c.e.f", "value5"),
+	), newRows[0])
+}
+
+func TestNestedMapInterfaceRow(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c", types.NewRow(
+			types.MRP("d", "value3"),
+			types.MRP("a", "value4"),
+			types.MRP("e", map[string]interface{}{
+				"f": "value5",
+				"a": "value6",
+			}),
+		)),
+	)
+
+	mw := NewFlattenObjectMiddleware()
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+
+	assert.Len(t, newRows, 1)
+	assert2.EqualMapRows(t, types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c.d", "value3"),
+		types.MRP("c.a", "value4"),
+		types.MRP("c.e.a", "value6"),
+		types.MRP("c.e.f", "value5"),
+	), newRows[0])
+}
diff --git a/pkg/middlewares/row/remove-duplicates_test.go b/pkg/middlewares/row/remove-duplicates_test.go
new file mode 100644
index 0000000..95ad501
--- /dev/null
+++ b/pkg/middlewares/row/remove-duplicates_test.go
@@ -0,0 +1,179 @@
+package row
+
+import (
+	assert2 "github.com/go-go-golems/glazed/pkg/helpers/assert"
+	"github.com/go-go-golems/glazed/pkg/types"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func createRemoveDuplicatesRows(rows [][]int) []types.Row {
+	ret := types.NewTable()
+	ret.Columns = []types.FieldName{"a", "b", "c"}
+	ret.Rows = []types.Row{}
+	for _, row := range rows {
+		ret.Rows = append(ret.Rows,
+			types.NewRow(
+				types.MRP("a", row[0]),
+				types.MRP("b", row[1]),
+				types.MRP("c", row[2]),
+			),
+		)
+	}
+
+	return ret.Rows
+}
+
+func TestRemoveDuplicatesEmpty(t *testing.T) {
+	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b", "c")
+
+	rows := createRemoveDuplicatesRows([][]int{})
+	newRows, err := processRows(removeDuplicatesMiddleware, rows)
+	require.NoError(t, err)
+
+	require.Equal(t, 0, len(newRows))
+}
+
+func TestRemoveDuplicatesSingle(t *testing.T) {
+	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b", "c")
+
+	rows := createRemoveDuplicatesRows([][]int{
+		{1, 2, 3},
+	})
+	newRows, err := processRows(removeDuplicatesMiddleware, rows)
+	require.NoError(t, err)
+
+	require.Equal(t, 1, len(newRows))
+	row := newRows[0]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 2, row, "b")
+	assert2.EqualMapRowValue(t, 3, row, "c")
+}
+
+func TestRemoveDuplicatesTwoDifferent(t *testing.T) {
+	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b", "c")
+
+	rows := createRemoveDuplicatesRows([][]int{
+		{1, 2, 3},
+		{4, 5, 6},
+	})
+	newRows, err := processRows(removeDuplicatesMiddleware, rows)
+	require.NoError(t, err)
+
+	require.Equal(t, 2, len(newRows))
+	row := newRows[0]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 2, row, "b")
+	assert2.EqualMapRowValue(t, 3, row, "c")
+	row = newRows[1]
+	assert2.EqualMapRowValue(t, 4, row, "a")
+	assert2.EqualMapRowValue(t, 5, row, "b")
+	assert2.EqualMapRowValue(t, 6, row, "c")
+}
+
+func TestRemoveDuplicatesTwoSame(t *testing.T) {
+	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b", "c")
+
+	rows := createRemoveDuplicatesRows([][]int{
+		{1, 2, 3},
+		{1, 2, 3},
+	})
+	newRows, err := processRows(removeDuplicatesMiddleware, rows)
+	require.NoError(t, err)
+
+	require.Equal(t, 1, len(newRows))
+	row := newRows[0]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 2, row, "b")
+	assert2.EqualMapRowValue(t, 3, row, "c")
+}
+
+func TestRemoveDuplicatesTwoSameOneDifferent(t *testing.T) {
+	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b", "c")
+
+	rows := createRemoveDuplicatesRows([][]int{
+		{1, 2, 3},
+		{1, 2, 3},
+		{4, 5, 6},
+	})
+	newRows, err := processRows(removeDuplicatesMiddleware, rows)
+	require.NoError(t, err)
+
+	require.Equal(t, 2, len(newRows))
+	row := newRows[0]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 2, row, "b")
+	assert2.EqualMapRowValue(t, 3, row, "c")
+	row = newRows[1]
+	assert2.EqualMapRowValue(t, 4, row, "a")
+	assert2.EqualMapRowValue(t, 5, row, "b")
+	assert2.EqualMapRowValue(t, 6, row, "c")
+}
+
+func TestRemoveDuplicatesTwoTimesTwoSame(t *testing.T) {
+	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b", "c")
+
+	rows := createRemoveDuplicatesRows([][]int{
+		{1, 2, 3},
+		{1, 2, 3},
+		{4, 5, 6},
+		{4, 5, 6},
+	})
+	newRows, err := processRows(removeDuplicatesMiddleware, rows)
+	require.NoError(t, err)
+
+	require.Equal(t, 2, len(newRows))
+	row := newRows[0]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 2, row, "b")
+	assert2.EqualMapRowValue(t, 3, row, "c")
+	row = newRows[1]
+	assert2.EqualMapRowValue(t, 4, row, "a")
+	assert2.EqualMapRowValue(t, 5, row, "b")
+	assert2.EqualMapRowValue(t, 6, row, "c")
+}
+
+func TestRemoveDuplicatesTwoTimesTwoSameOneDifferent(t *testing.T) {
+	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b", "c")
+
+	rows := createRemoveDuplicatesRows([][]int{
+		{1, 2, 3},
+		{1, 2, 3},
+		{4, 5, 6},
+		{4, 5, 6},
+		{7, 8, 9},
+	})
+	newRows, err := processRows(removeDuplicatesMiddleware, rows)
+	require.NoError(t, err)
+
+	require.Equal(t, 3, len(newRows))
+	row := newRows[0]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 2, row, "b")
+	assert2.EqualMapRowValue(t, 3, row, "c")
+	row = newRows[1]
+	assert2.EqualMapRowValue(t, 4, row, "a")
+	assert2.EqualMapRowValue(t, 5, row, "b")
+	assert2.EqualMapRowValue(t, 6, row, "c")
+	row = newRows[2]
+	assert2.EqualMapRowValue(t, 7, row, "a")
+	assert2.EqualMapRowValue(t, 8, row, "b")
+	assert2.EqualMapRowValue(t, 9, row, "c")
+}
+
+func TestRemoveDuplicatesTwoSameWithTwoColumns(t *testing.T) {
+	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b")
+
+	rows := createRemoveDuplicatesRows([][]int{
+		{1, 2, 3},
+		{1, 2, 4},
+	})
+	newRows, err := processRows(removeDuplicatesMiddleware, rows)
+	require.NoError(t, err)
+
+	require.Equal(t, 1, len(newRows))
+	row := newRows[0]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 2, row, "b")
+	assert2.EqualMapRowValue(t, 3, row, "c")
+}
diff --git a/pkg/middlewares/row/rename-columns_test.go b/pkg/middlewares/row/rename-columns_test.go
new file mode 100644
index 0000000..0809932
--- /dev/null
+++ b/pkg/middlewares/row/rename-columns_test.go
@@ -0,0 +1,308 @@
+package row
+
+import (
+	assert2 "github.com/go-go-golems/glazed/pkg/helpers/assert"
+	"github.com/go-go-golems/glazed/pkg/types"
+	"github.com/stretchr/testify/require"
+	"gopkg.in/yaml.v3"
+	"regexp"
+	"strings"
+	"testing"
+)
+
+func TestSingleRename(t *testing.T) {
+	renameTable := map[types.FieldName]types.FieldName{
+		"foo": "bar",
+	}
+	mw := NewFieldRenameColumnMiddleware(renameTable)
+	rows := createTestRows()
+	newRows, err := processRows(mw, rows)
+	require.NoError(t, err)
+
+	row := newRows[0]
+	assert2.NilMapRowValue(t, row, "foo")
+	assert2.EqualMapRowValue(t, 1, row, "bar")
+	assert2.EqualMapRowValue(t, 2, row, "baz")
+	assert2.EqualMapRowValue(t, 3, row, "foobar")
+}
+
+func TestRenameTwoFieldColumns(t *testing.T) {
+	renameTable := map[types.FieldName]types.FieldName{
+		"foo": "bar",
+		"baz": "qux",
+	}
+	mw := NewFieldRenameColumnMiddleware(renameTable)
+	rows := createTestRows()
+	newRows, err := processRows(mw, rows)
+	require.NoError(t, err)
+
+	row := newRows[0]
+	assert2.NilMapRowValue(t, row, "foo")
+	assert2.EqualMapRowValue(t, 1, row, "bar")
+	assert2.EqualMapRowValue(t, 2, row, "qux")
+	assert2.EqualMapRowValue(t, 3, row, "foobar")
+}
+
+func TestRenameOverrideColumn(t *testing.T) {
+	renameTable := map[types.FieldName]types.FieldName{
+		"foo": "foobar",
+	}
+	mw := NewFieldRenameColumnMiddleware(renameTable)
+	rows := createTestRows()
+	newRows, err := processRows(mw, rows)
+	require.NoError(t, err)
+
+	row := newRows[0]
+	assert2.NilMapRowValue(t, row, "foo")
+	// we get 3 because our keys are ordered now, and foobar comes last, thus overwriting the renamed foo
+	assert2.EqualMapRowValue(t, 3, row, "foobar")
+	assert2.EqualMapRowValue(t, 2, row, "baz")
+
+}
+
+func TestRenameOverrideColumnLast(t *testing.T) {
+	renameTable := map[types.FieldName]types.FieldName{
+		"foobar": "foo",
+	}
+	mw := NewFieldRenameColumnMiddleware(renameTable)
+	rows := createTestRows()
+	newRows, err := processRows(mw, rows)
+	require.NoError(t, err)
+
+	row := newRows[0]
+	assert2.NilMapRowValue(t, row, "foobar")
+	// the renamed foobar -> foo now overwrites the original foo value
+	assert2.EqualMapRowValue(t, 3, row, "foo")
+	assert2.EqualMapRowValue(t, 2, row, "baz")
+
+}
+
+func TestRenameRegexpSimpleMatch(t *testing.T) {
+	rrs := RegexpReplacements{}
+
+	rrs = append(rrs, &RegexpReplacement{
+		regexp.MustCompile("^foo$"), "bar",
+	})
+
+	mw := NewRegexpRenameColumnMiddleware(rrs)
+	rows := createTestRows()
+	newRows, err := processRows(mw, rows)
+	require.NoError(t, err)
+
+	row := newRows[0]
+	assert2.NilMapRowValue(t, row, "foo")
+	assert2.EqualMapRowValue(t, 1, row, "bar")
+	assert2.EqualMapRowValue(t, 2, row, "baz")
+	assert2.EqualMapRowValue(t, 3, row, "foobar")
+}
+
+func TestRenameRegexpDoubleMatch(t *testing.T) {
+	rrs := RegexpReplacements{}
+	rrs = append(rrs, &RegexpReplacement{
+		regexp.MustCompile("f.."), "bar",
+	})
+
+	mw := NewRegexpRenameColumnMiddleware(rrs)
+	rows := createTestRows()
+	newRows, err := processRows(mw, rows)
+	require.NoError(t, err)
+
+	row := newRows[0]
+	// here, f.. should match both fields
+	assert2.NilMapRowValue(t, row, "foo")
+	assert2.NilMapRowValue(t, row, "foobar")
+	assert2.EqualMapRowValue(t, 1, row, "bar")
+	assert2.EqualMapRowValue(t, 2, row, "baz")
+	assert2.EqualMapRowValue(t, 3, row, "barbar")
+}
+
+func TestRenameRegexpOrderedMatch(t *testing.T) {
+	rrs := RegexpReplacements{}
+
+	rrs = append(rrs, &RegexpReplacement{
+		regexp.MustCompile("f..$"), "bar",
+	})
+	rrs = append(rrs, &RegexpReplacement{
+		regexp.MustCompile("^foo$"), "bar2",
+	})
+	rrs = append(rrs, &RegexpReplacement{
+		regexp.MustCompile("^foo$"), "bar3",
+	})
+
+	mw := NewRegexpRenameColumnMiddleware(rrs)
+	rows := createTestRows()
+	newRows, err := processRows(mw, rows)
+	require.NoError(t, err)
+
+	row := newRows[0]
+	// it's going to be hard to test that these will happen in the right
+	// order as it really depends on the map
+	assert2.NilMapRowValue(t, row, "foo")
+	assert2.NilMapRowValue(t, row, "bar2")
+	assert2.NilMapRowValue(t, row, "bar3")
+	assert2.EqualMapRowValue(t, 1, row, "bar")
+	assert2.EqualMapRowValue(t, 2, row, "baz")
+	assert2.EqualMapRowValue(t, 3, row, "foobar")
+}
+
+func createTestRows() []types.Row {
+	table := types.Table{
+		Columns: []types.FieldName{
+			"foo",
+			"baz",
+			"foobar",
+		},
+		Rows: []types.Row{},
+	}
+	table.Rows = append(table.Rows,
+		types.NewRow(
+			types.MRP("foo", 1),
+			types.MRP("baz", 2),
+			types.MRP("foobar", 3),
+		),
+	)
+
+	return table.Rows
+}
+
+func TestBothFieldAndRegexpRenames(t *testing.T) {
+	renameTable := map[types.FieldName]types.FieldName{
+		"foo": "bar",
+		"baz": "qux",
+	}
+	rrs := RegexpReplacements{}
+	rrs = append(rrs, &RegexpReplacement{
+		regexp.MustCompile("f.."), "bar2",
+	})
+
+	mw := NewRenameColumnMiddleware(renameTable, rrs)
+	rows := createTestRows()
+
+	newRows, err := processRows(mw, rows)
+	require.Nil(t, err)
+
+	row := newRows[0]
+	assert2.NilMapRowValue(t, row, "foo")
+	assert2.NilMapRowValue(t, row, "baz")
+	assert2.NilMapRowValue(t, row, "foobar")
+	assert2.EqualMapRowValue(t, 1, row, "bar")
+	assert2.EqualMapRowValue(t, 2, row, "qux")
+	assert2.EqualMapRowValue(t, 3, row, "bar2bar")
+}
+
+func TestParseFromYAML(t *testing.T) {
+	yamlString := `
+renames:
+  foo: bar
+  baz: qux
+`
+	decoder := yaml.NewDecoder(strings.NewReader(yamlString))
+	mw, err := NewRenameColumnMiddlewareFromYAML(decoder)
+	require.Nil(t, err)
+
+	rows := createTestRows()
+
+	newRows, err := processRows(mw, rows)
+	require.Nil(t, err)
+
+	row := newRows[0]
+	assert2.NilMapRowValue(t, row, "foo")
+	assert2.NilMapRowValue(t, row, "baz")
+	assert2.EqualMapRowValue(t, 1, row, "bar")
+	assert2.EqualMapRowValue(t, 2, row, "qux")
+	assert2.EqualMapRowValue(t, 3, row, "foobar")
+}
+
+func TestParseRegexpFromYAML(t *testing.T) {
+	yamlString := `
+regexpRenames:
+  "^foo$": bar2
+  f..: bar
+  "^baz$": qux
+`
+
+	decoder := yaml.NewDecoder(strings.NewReader(yamlString))
+	mw, err := NewRenameColumnMiddlewareFromYAML(decoder)
+	require.Nil(t, err)
+
+	rows := createTestRows()
+	newRows, err := processRows(mw, rows)
+	require.Nil(t, err)
+
+	row := newRows[0]
+	assert2.NilMapRowValue(t, row, "foo")
+	assert2.NilMapRowValue(t, row, "baz")
+	assert2.NilMapRowValue(t, row, "foobar")
+	assert2.EqualMapRowValue(t, 1, row, "bar2")
+	assert2.EqualMapRowValue(t, 2, row, "qux")
+	assert2.EqualMapRowValue(t, 3, row, "barbar")
+}
+
+func TestParseBothFromYAML(t *testing.T) {
+	yamlString := `
+renames:
+  foo: bar
+  baz: qux
+regexpRenames:
+  "^foo$": bar2
+  f..: bar
+  "^baz$": qux2
+`
+
+	decoder := yaml.NewDecoder(strings.NewReader(yamlString))
+	mw, err := NewRenameColumnMiddlewareFromYAML(decoder)
+	require.Nil(t, err)
+
+	rows := createTestRows()
+	newRows, err := processRows(mw, rows)
+	require.Nil(t, err)
+
+	row := newRows[0]
+	assert2.NilMapRowValue(t, row, "foo")
+	assert2.NilMapRowValue(t, row, "baz")
+	assert2.NilMapRowValue(t, row, "foobar")
+	assert2.EqualMapRowValue(t, 1, row, "bar")
+	assert2.EqualMapRowValue(t, 2, row, "qux")
+	assert2.EqualMapRowValue(t, 3, row, "barbar")
+}
+
+func TestRegexpCaptureGroupRename(t *testing.T) {
+	rrs := RegexpReplacements{}
+	rrs = append(rrs, &RegexpReplacement{
+		regexp.MustCompile("^foo(.*)$"), "bar$1",
+	})
+
+	mw := NewRegexpRenameColumnMiddleware(rrs)
+	rows := createTestRows()
+	newRows, err := processRows(mw, rows)
+	require.Nil(t, err)
+
+	row := newRows[0]
+	assert2.NilMapRowValue(t, row, "foo")
+	assert2.NilMapRowValue(t, row, "foobar")
+	assert2.EqualMapRowValue(t, 1, row, "bar")
+	assert2.EqualMapRowValue(t, 2, row, "baz")
+	assert2.EqualMapRowValue(t, 3, row, "barbar")
+}
+
+func TestRegexpCaptureGroupRenameFromYAML(t *testing.T) {
+	yamlString := `
+regexpRenames:
+  "^foo(.*)$": bar$1
+`
+
+	decoder := yaml.NewDecoder(strings.NewReader(yamlString))
+	mw, err := NewRenameColumnMiddlewareFromYAML(decoder)
+	require.Nil(t, err)
+
+	rows := createTestRows()
+	newRows, err := processRows(mw, rows)
+	require.Nil(t, err)
+
+	row := newRows[0]
+	assert2.NilMapRowValue(t, row, "foo")
+	assert2.NilMapRowValue(t, row, "foobar")
+	assert2.EqualMapRowValue(t, 1, row, "bar")
+	assert2.EqualMapRowValue(t, 2, row, "baz")
+	assert2.EqualMapRowValue(t, 3, row, "barbar")
+}
diff --git a/pkg/middlewares/row/reorder-column_test.go b/pkg/middlewares/row/reorder-column_test.go
new file mode 100644
index 0000000..509dc26
--- /dev/null
+++ b/pkg/middlewares/row/reorder-column_test.go
@@ -0,0 +1,99 @@
+package row
+
+import (
+	"context"
+	assert2 "github.com/go-go-golems/glazed/pkg/helpers/assert"
+	"github.com/go-go-golems/glazed/pkg/types"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func TestSimpleReorder(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c", "value3"),
+	)
+
+	mw := NewReorderColumnOrderMiddleware(
+		[]types.FieldName{"c", "a", "b"},
+	)
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+
+	require.Len(t, newRows, 1)
+	assert2.EqualMapRows(t, types.NewRow(
+		types.MRP("c", "value3"),
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+	), newRows[0])
+}
+
+func TestSimpleReorderNotAllColumns(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c", "value3"),
+	)
+
+	mw := NewReorderColumnOrderMiddleware(
+		[]types.FieldName{"c", "a"},
+	)
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+
+	require.Len(t, newRows, 1)
+	assert2.EqualMapRows(t, types.NewRow(
+		types.MRP("c", "value3"),
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+	), newRows[0])
+}
+
+func TestReorderDot(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c.d", "value3"),
+		types.MRP("c.e", "value4"),
+	)
+
+	mw := NewReorderColumnOrderMiddleware(
+		[]types.FieldName{"c.", "a", "b"},
+	)
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+
+	require.Len(t, newRows, 1)
+	assert2.EqualMapRows(t, types.NewRow(
+		types.MRP("c.d", "value3"),
+		types.MRP("c.e", "value4"),
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+	), newRows[0])
+}
+
+func TestReorderDotOverride(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c.d", "value3"),
+		types.MRP("c.e", "value4"),
+		types.MRP("c.f", "value5"),
+	)
+
+	mw := NewReorderColumnOrderMiddleware(
+		[]types.FieldName{"c.e", "a", "c.", "b"},
+	)
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+
+	require.Len(t, newRows, 1)
+	assert2.EqualMapRows(t, types.NewRow(
+		types.MRP("c.e", "value4"),
+		types.MRP("a", "value1"),
+		types.MRP("c.d", "value3"),
+		types.MRP("c.f", "value5"),
+		types.MRP("b", "value2"),
+	), newRows[0])
+}
diff --git a/pkg/middlewares/table/replace_test.go b/pkg/middlewares/row/replace_test.go
similarity index 64%
rename from pkg/middlewares/table/replace_test.go
rename to pkg/middlewares/row/replace_test.go
index 5153c15..515deac 100644
--- a/pkg/middlewares/table/replace_test.go
+++ b/pkg/middlewares/row/replace_test.go
@@ -1,6 +1,7 @@
-package table
+package row
 
 import (
+	assert2 "github.com/go-go-golems/glazed/pkg/helpers/assert"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -8,25 +9,17 @@ import (
 	"testing"
 )
 
-func createReplaceTestTable() *types.Table {
-	ret := types.NewTable()
-	ret.Columns = []types.FieldName{"field1", "field2"}
-	ret.Rows = []types.Row{
-		&types.SimpleRow{
-			Hash: map[types.FieldName]types.GenericCellValue{
-				"field1": "skip",
-				"field2": "value2",
-			},
-		},
-		&types.SimpleRow{
-			Hash: map[types.FieldName]types.GenericCellValue{
-				"field1": "value1",
-				"field2": "value3 blabla",
-			},
-		},
+func createReplaceTestRows() []types.Row {
+	return []types.Row{
+		types.NewRow(
+			types.MRP("field1", "skip"),
+			types.MRP("field2", "value2"),
+		),
+		types.NewRow(
+			types.MRP("field1", "value1"),
+			types.MRP("field2", "value3 blabla"),
+		),
 	}
-
-	return ret
 }
 
 func TestSingleSkip(t *testing.T) {
@@ -43,15 +36,15 @@ func TestSingleSkip(t *testing.T) {
 		},
 	)
 
-	table := createReplaceTestTable()
-	newtable, err := replaceMiddleware.Process(table)
+	rows := createReplaceTestRows()
+	newRows, err := processRows(replaceMiddleware, rows)
 	require.NoError(t, err)
 
-	require.Equal(t, 1, len(newtable.Rows))
+	require.Equal(t, 1, len(newRows))
 
-	row := newtable.Rows[0]
-	require.Equal(t, "value1", row.GetValues()["field1"])
-	require.Equal(t, "value3 blabla", row.GetValues()["field2"])
+	row := newRows[0]
+	assert2.EqualMapRowValue(t, "value1", row, "field1")
+	assert2.EqualMapRowValue(t, "value3 blabla", row, "field2")
 }
 
 func TestSingleReplacement(t *testing.T) {
@@ -69,18 +62,18 @@ func TestSingleReplacement(t *testing.T) {
 		map[types.FieldName][]*Skip{},
 	)
 
-	table := createReplaceTestTable()
-	newtable, err := replacementMiddleware.Process(table)
+	rows := createReplaceTestRows()
+	newRows, err := processRows(replacementMiddleware, rows)
 	require.NoError(t, err)
 
-	require.Equal(t, 2, len(newtable.Rows))
+	require.Equal(t, 2, len(newRows))
 
-	row := newtable.Rows[0]
-	require.Equal(t, "skip", row.GetValues()["field1"])
-	require.Equal(t, "value2", row.GetValues()["field2"])
-	row = newtable.Rows[1]
-	require.Equal(t, "replaced", row.GetValues()["field1"])
-	require.Equal(t, "value3 blabla", row.GetValues()["field2"])
+	row := newRows[0]
+	assert2.EqualMapRowValue(t, "skip", row, "field1")
+	assert2.EqualMapRowValue(t, "value2", row, "field2")
+	row = newRows[1]
+	assert2.EqualMapRowValue(t, "replaced", row, "field1")
+	assert2.EqualMapRowValue(t, "value3 blabla", row, "field2")
 }
 
 func TestTwoSkips(t *testing.T) {
@@ -100,11 +93,11 @@ func TestTwoSkips(t *testing.T) {
 		},
 	)
 
-	table := createReplaceTestTable()
-	newtable, err := replaceMiddleware.Process(table)
+	rows := createReplaceTestRows()
+	newRows, err := processRows(replaceMiddleware, rows)
 	require.NoError(t, err)
 
-	require.Equal(t, 0, len(newtable.Rows))
+	require.Equal(t, 0, len(newRows))
 }
 
 func TestTwoColumnSkips(t *testing.T) {
@@ -126,11 +119,11 @@ func TestTwoColumnSkips(t *testing.T) {
 		},
 	)
 
-	table := createReplaceTestTable()
-	newtable, err := mw.Process(table)
+	rows := createReplaceTestRows()
+	newRows, err := processRows(mw, rows)
 	require.NoError(t, err)
 
-	require.Equal(t, 0, len(newtable.Rows))
+	require.Equal(t, 0, len(newRows))
 
 }
 
@@ -148,15 +141,15 @@ func TestSingleRegexpSkip(t *testing.T) {
 		map[types.FieldName][]*Skip{},
 	)
 
-	table := createReplaceTestTable()
-	newtable, err := replaceMiddleware.Process(table)
+	rows := createReplaceTestRows()
+	newRows, err := processRows(replaceMiddleware, rows)
 	require.NoError(t, err)
 
-	require.Equal(t, 1, len(newtable.Rows))
+	require.Equal(t, 1, len(newRows))
 
-	row := newtable.Rows[0]
-	require.Equal(t, "value1", row.GetValues()["field1"])
-	require.Equal(t, "value3 blabla", row.GetValues()["field2"])
+	row := newRows[0]
+	assert2.EqualMapRowValue(t, "value1", row, "field1")
+	assert2.EqualMapRowValue(t, "value3 blabla", row, "field2")
 
 	replaceMiddleware.RegexSkips = map[types.FieldName][]*RegexpSkip{
 		"field1": {
@@ -166,14 +159,14 @@ func TestSingleRegexpSkip(t *testing.T) {
 		},
 	}
 
-	newtable, err = replaceMiddleware.Process(table)
+	newRows, err = processRows(replaceMiddleware, rows)
 	require.NoError(t, err)
 
-	require.Equal(t, 1, len(newtable.Rows))
+	require.Equal(t, 1, len(newRows))
 
-	row = newtable.Rows[0]
-	require.Equal(t, "value1", row.GetValues()["field1"])
-	require.Equal(t, "value3 blabla", row.GetValues()["field2"])
+	row = newRows[0]
+	assert2.EqualMapRowValue(t, "value1", row, "field1")
+	assert2.EqualMapRowValue(t, "value3 blabla", row, "field2")
 }
 
 func TestTwoReplacements(t *testing.T) {
@@ -195,18 +188,18 @@ func TestTwoReplacements(t *testing.T) {
 		map[types.FieldName][]*Skip{},
 	)
 
-	table := createReplaceTestTable()
-	newtable, err := rep.Process(table)
+	rows := createReplaceTestRows()
+	newRows, err := processRows(rep, rows)
 	require.NoError(t, err)
 
-	require.Equal(t, 2, len(newtable.Rows))
+	require.Equal(t, 2, len(newRows))
 
-	row := newtable.Rows[0]
-	require.Equal(t, "skip", row.GetValues()["field1"])
-	require.Equal(t, "value2", row.GetValues()["field2"])
-	row = newtable.Rows[1]
-	require.Equal(t, "replaced replaced2", row.GetValues()["field1"])
-	require.Equal(t, "value3 blabla", row.GetValues()["field2"])
+	row := newRows[0]
+	assert2.EqualMapRowValue(t, "skip", row, "field1")
+	assert2.EqualMapRowValue(t, "value2", row, "field2")
+	row = newRows[1]
+	assert2.EqualMapRowValue(t, "replaced replaced2", row, "field1")
+	assert2.EqualMapRowValue(t, "value3 blabla", row, "field2")
 }
 
 func TestSingleRegexpReplacement(t *testing.T) {
@@ -224,19 +217,19 @@ func TestSingleRegexpReplacement(t *testing.T) {
 		map[types.FieldName][]*Skip{},
 	)
 
-	table := createReplaceTestTable()
-	newtable, err := rep.Process(table)
+	rows := createReplaceTestRows()
+	newRows, err := processRows(rep, rows)
 	require.NoError(t, err)
 
-	require.Equal(t, 2, len(newtable.Rows))
+	require.Equal(t, 2, len(newRows))
 
-	row := newtable.Rows[0]
-	require.Equal(t, "skip", row.GetValues()["field1"])
-	require.Equal(t, "value2", row.GetValues()["field2"])
+	row := newRows[0]
+	assert2.EqualMapRowValue(t, "skip", row, "field1")
+	assert2.EqualMapRowValue(t, "value2", row, "field2")
 
-	row = newtable.Rows[1]
-	require.Equal(t, "replaced", row.GetValues()["field1"])
-	require.Equal(t, "value3 blabla", row.GetValues()["field2"])
+	row = newRows[1]
+	assert2.EqualMapRowValue(t, "replaced", row, "field1")
+	assert2.EqualMapRowValue(t, "value3 blabla", row, "field2")
 }
 
 func TestSingleRegexpCaptureReplacement(t *testing.T) {
@@ -254,19 +247,19 @@ func TestSingleRegexpCaptureReplacement(t *testing.T) {
 		map[types.FieldName][]*Skip{},
 	)
 
-	table := createReplaceTestTable()
-	newtable, err := rep.Process(table)
+	rows := createReplaceTestRows()
+	newRows, err := processRows(rep, rows)
 	require.NoError(t, err)
 
-	require.Equal(t, 2, len(newtable.Rows))
+	require.Equal(t, 2, len(newRows))
 
-	row := newtable.Rows[0]
-	require.Equal(t, "skip", row.GetValues()["field1"])
-	require.Equal(t, "value2", row.GetValues()["field2"])
+	row := newRows[0]
+	assert2.EqualMapRowValue(t, "skip", row, "field1")
+	assert2.EqualMapRowValue(t, "value2", row, "field2")
 
-	row = newtable.Rows[1]
-	require.Equal(t, "replacedalue", row.GetValues()["field1"])
-	require.Equal(t, "value3 blabla", row.GetValues()["field2"])
+	row = newRows[1]
+	assert2.EqualMapRowValue(t, "replacedalue", row, "field1")
+	assert2.EqualMapRowValue(t, "value3 blabla", row, "field2")
 }
 
 func TestRegexpAndSkip(t *testing.T) {
@@ -290,15 +283,15 @@ func TestRegexpAndSkip(t *testing.T) {
 		map[types.FieldName][]*Skip{},
 	)
 
-	table := createReplaceTestTable()
-	newtable, err := rep.Process(table)
+	rows := createReplaceTestRows()
+	newRows, err := processRows(rep, rows)
 	require.NoError(t, err)
 
-	require.Equal(t, 1, len(newtable.Rows))
+	require.Equal(t, 1, len(newRows))
 
-	row := newtable.Rows[0]
-	require.Equal(t, "replaced", row.GetValues()["field1"])
-	require.Equal(t, "value3 blabla", row.GetValues()["field2"])
+	row := newRows[0]
+	assert2.EqualMapRowValue(t, "replaced", row, "field1")
+	assert2.EqualMapRowValue(t, "value3 blabla", row, "field2")
 }
 
 func TestReplaceMiddlewareFromYAML(t *testing.T) {
diff --git a/pkg/middlewares/row/sort-columns_test.go b/pkg/middlewares/row/sort-columns_test.go
new file mode 100644
index 0000000..55d9726
--- /dev/null
+++ b/pkg/middlewares/row/sort-columns_test.go
@@ -0,0 +1,28 @@
+package row
+
+import (
+	"context"
+	assert2 "github.com/go-go-golems/glazed/pkg/helpers/assert"
+	"github.com/go-go-golems/glazed/pkg/types"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func TestSimpleSortColumns(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("c", "value3"),
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+	)
+
+	mw := NewSortColumnsMiddleware()
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+
+	require.Len(t, newRows, 1)
+	assert2.EqualMapRows(t, types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c", "value3"),
+	), newRows[0])
+}
diff --git a/pkg/middlewares/row/template_test.go b/pkg/middlewares/row/template_test.go
new file mode 100644
index 0000000..b134840
--- /dev/null
+++ b/pkg/middlewares/row/template_test.go
@@ -0,0 +1,132 @@
+package row
+
+import (
+	"context"
+	assert2 "github.com/go-go-golems/glazed/pkg/helpers/assert"
+	"github.com/go-go-golems/glazed/pkg/types"
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func TestSimpleTemplate(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c", "value3"),
+	)
+
+	mw, err := NewTemplateMiddleware(
+		map[types.FieldName]string{
+			"a": "{{.a}}-{{.b}}",
+		}, "")
+	require.NoError(t, err)
+
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+	require.Len(t, newRows, 1)
+
+	assert2.EqualMapRows(t, types.NewRow(
+		types.MRP("a", "value1-value2"),
+		types.MRP("b", "value2"),
+		types.MRP("c", "value3"),
+	), newRows[0])
+}
+
+func TestSimpleDoubleTemplate(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c", "value3"),
+	)
+
+	mw, err := NewTemplateMiddleware(
+		map[types.FieldName]string{
+			"a": "{{.a}}-{{.b}}",
+			"b": "{{.a}}-{{.b}}",
+		}, "")
+	require.NoError(t, err)
+
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+	require.Len(t, newRows, 1)
+
+	assert2.EqualMapRows(t, types.NewRow(
+		types.MRP("a", "value1-value2"),
+		types.MRP("b", "value1-value2"),
+		types.MRP("c", "value3"),
+	), newRows[0])
+}
+
+func TestSimpleDoubleTemplateDifferentOrder(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("b", "value2"),
+		types.MRP("a", "value1"),
+		types.MRP("c", "value3"),
+	)
+
+	mw, err := NewTemplateMiddleware(
+		map[types.FieldName]string{
+			"b": "{{.a}}-{{.b}}",
+			"a": "{{.a}}-{{.b}}",
+		}, "")
+	require.NoError(t, err)
+
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+	require.Len(t, newRows, 1)
+
+	assert2.EqualMapRows(t, types.NewRow(
+		types.MRP("b", "value1-value2"),
+		types.MRP("a", "value1-value2"),
+		types.MRP("c", "value3"),
+	), newRows[0])
+}
+
+func TestSimpleDoubleTemplateWithDot(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("b.d", "value2"),
+		types.MRP("a", "value1"),
+		types.MRP("c", "value3"),
+	)
+
+	mw, err := NewTemplateMiddleware(
+		map[types.FieldName]string{
+			"b.d": "{{.a}}-{{.b_d}}",
+			"a":   "{{.a}}-{{.b_d}}",
+		}, "_")
+	require.NoError(t, err)
+
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+	require.Len(t, newRows, 1)
+
+	assert2.EqualMapRows(t, types.NewRow(
+		types.MRP("b.d", "value1-value2"),
+		types.MRP("a", "value1-value2"),
+		types.MRP("c", "value3"),
+	), newRows[0])
+}
+
+func TestSimpleTemplateRow(t *testing.T) {
+	row := types.NewRow(
+		types.MRP("a", "value1"),
+		types.MRP("b", "value2"),
+		types.MRP("c", "value3"),
+	)
+
+	mw, err := NewTemplateMiddleware(
+		map[types.FieldName]string{
+			"a": "{{.a}}-{{._row.b}}",
+		}, "")
+	require.NoError(t, err)
+
+	newRows, err := mw.Process(context.Background(), row)
+	require.NoError(t, err)
+	require.Len(t, newRows, 1)
+
+	assert2.EqualMapRows(t, types.NewRow(
+		types.MRP("a", "value1-value2"),
+		types.MRP("b", "value2"),
+		types.MRP("c", "value3"),
+	), newRows[0])
+}
diff --git a/pkg/middlewares/table/add-field_test.go b/pkg/middlewares/table/add-field_test.go
deleted file mode 100644
index d9b6d0e..0000000
--- a/pkg/middlewares/table/add-field_test.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package table
-
-import (
-	"github.com/go-go-golems/glazed/pkg/types"
-	"github.com/stretchr/testify/require"
-	"testing"
-)
-
-func createAddFieldTestTable() *types.Table {
-	ret := types.NewTable()
-	ret.Columns = []types.FieldName{"field1", "field2"}
-	ret.Rows = []types.Row{
-		&types.SimpleRow{
-			Hash: map[types.FieldName]types.GenericCellValue{
-				"field1": "skip",
-				"field2": "value2",
-			},
-		},
-		&types.SimpleRow{
-			Hash: map[types.FieldName]types.GenericCellValue{
-				"field1": "value1",
-				"field2": "value3 blabla",
-			},
-		},
-	}
-
-	return ret
-}
-
-func TestSingleAddField(t *testing.T) {
-	addFieldMiddleware := NewAddFieldMiddleware(map[string]string{
-		"field3": "value3",
-	})
-
-	table := createAddFieldTestTable()
-	newtable, err := addFieldMiddleware.Process(table)
-	require.NoError(t, err)
-
-	require.Equal(t, 2, len(newtable.Rows))
-
-	row := newtable.Rows[0]
-	require.Equal(t, "skip", row.GetValues()["field1"])
-	require.Equal(t, "value2", row.GetValues()["field2"])
-	require.Equal(t, "value3", row.GetValues()["field3"])
-
-	row = newtable.Rows[1]
-	require.Equal(t, "value1", row.GetValues()["field1"])
-	require.Equal(t, "value3 blabla", row.GetValues()["field2"])
-	require.Equal(t, "value3", row.GetValues()["field3"])
-}
-
-func TestMultipleAddField(t *testing.T) {
-	addFieldMiddleware := NewAddFieldMiddleware(map[string]string{
-		"field3": "value3",
-		"field4": "value4",
-	})
-
-	table := createAddFieldTestTable()
-	newtable, err := addFieldMiddleware.Process(table)
-	require.NoError(t, err)
-
-	require.Equal(t, 2, len(newtable.Rows))
-
-	row := newtable.Rows[0]
-	require.Equal(t, "skip", row.GetValues()["field1"])
-	require.Equal(t, "value2", row.GetValues()["field2"])
-	require.Equal(t, "value3", row.GetValues()["field3"])
-	require.Equal(t, "value4", row.GetValues()["field4"])
-
-	row = newtable.Rows[1]
-	require.Equal(t, "value1", row.GetValues()["field1"])
-	require.Equal(t, "value3 blabla", row.GetValues()["field2"])
-	require.Equal(t, "value3", row.GetValues()["field3"])
-	require.Equal(t, "value4", row.GetValues()["field4"])
-}
diff --git a/pkg/middlewares/table/remove-duplicates_test.go b/pkg/middlewares/table/remove-duplicates_test.go
deleted file mode 100644
index 7850941..0000000
--- a/pkg/middlewares/table/remove-duplicates_test.go
+++ /dev/null
@@ -1,178 +0,0 @@
-package table
-
-import (
-	"github.com/go-go-golems/glazed/pkg/types"
-	"github.com/stretchr/testify/require"
-	"testing"
-)
-
-func createRemoveDuplicatesTables(rows [][]int) *types.Table {
-	ret := types.NewTable()
-	ret.Columns = []types.FieldName{"a", "b", "c"}
-	ret.Rows = []types.Row{}
-	for _, row := range rows {
-		ret.Rows = append(ret.Rows, &types.SimpleRow{
-			Hash: map[string]interface{}{
-				"a": row[0],
-				"b": row[1],
-				"c": row[2],
-			},
-		})
-	}
-
-	return ret
-}
-
-func TestRemoveDuplicatesEmpty(t *testing.T) {
-	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b", "c")
-
-	table := createRemoveDuplicatesTables([][]int{})
-	newtable, err := removeDuplicatesMiddleware.Process(table)
-	require.NoError(t, err)
-
-	require.Equal(t, 0, len(newtable.Rows))
-}
-
-func TestRemoveDuplicatesSingle(t *testing.T) {
-	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b", "c")
-
-	table := createRemoveDuplicatesTables([][]int{
-		{1, 2, 3},
-	})
-	newtable, err := removeDuplicatesMiddleware.Process(table)
-	require.NoError(t, err)
-
-	require.Equal(t, 1, len(newtable.Rows))
-	row := newtable.Rows[0].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 2, row["b"])
-	require.Equal(t, 3, row["c"])
-}
-
-func TestRemoveDuplicatesTwoDifferent(t *testing.T) {
-	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b", "c")
-
-	table := createRemoveDuplicatesTables([][]int{
-		{1, 2, 3},
-		{4, 5, 6},
-	})
-	newtable, err := removeDuplicatesMiddleware.Process(table)
-	require.NoError(t, err)
-
-	require.Equal(t, 2, len(newtable.Rows))
-	row := newtable.Rows[0].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 2, row["b"])
-	require.Equal(t, 3, row["c"])
-	row = newtable.Rows[1].GetValues()
-	require.Equal(t, 4, row["a"])
-	require.Equal(t, 5, row["b"])
-	require.Equal(t, 6, row["c"])
-}
-
-func TestRemoveDuplicatesTwoSame(t *testing.T) {
-	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b", "c")
-
-	table := createRemoveDuplicatesTables([][]int{
-		{1, 2, 3},
-		{1, 2, 3},
-	})
-	newtable, err := removeDuplicatesMiddleware.Process(table)
-	require.NoError(t, err)
-
-	require.Equal(t, 1, len(newtable.Rows))
-	row := newtable.Rows[0].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 2, row["b"])
-	require.Equal(t, 3, row["c"])
-}
-
-func TestRemoveDuplicatesTwoSameOneDifferent(t *testing.T) {
-	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b", "c")
-
-	table := createRemoveDuplicatesTables([][]int{
-		{1, 2, 3},
-		{1, 2, 3},
-		{4, 5, 6},
-	})
-	newtable, err := removeDuplicatesMiddleware.Process(table)
-	require.NoError(t, err)
-
-	require.Equal(t, 2, len(newtable.Rows))
-	row := newtable.Rows[0].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 2, row["b"])
-	require.Equal(t, 3, row["c"])
-	row = newtable.Rows[1].GetValues()
-	require.Equal(t, 4, row["a"])
-	require.Equal(t, 5, row["b"])
-	require.Equal(t, 6, row["c"])
-}
-
-func TestRemoveDuplicatesTwoTimesTwoSame(t *testing.T) {
-	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b", "c")
-
-	table := createRemoveDuplicatesTables([][]int{
-		{1, 2, 3},
-		{1, 2, 3},
-		{4, 5, 6},
-		{4, 5, 6},
-	})
-	newtable, err := removeDuplicatesMiddleware.Process(table)
-	require.NoError(t, err)
-
-	require.Equal(t, 2, len(newtable.Rows))
-	row := newtable.Rows[0].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 2, row["b"])
-	require.Equal(t, 3, row["c"])
-	row = newtable.Rows[1].GetValues()
-	require.Equal(t, 4, row["a"])
-	require.Equal(t, 5, row["b"])
-	require.Equal(t, 6, row["c"])
-}
-
-func TestRemoveDuplicatesTwoTimesTwoSameOneDifferent(t *testing.T) {
-	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b", "c")
-
-	table := createRemoveDuplicatesTables([][]int{
-		{1, 2, 3},
-		{1, 2, 3},
-		{4, 5, 6},
-		{4, 5, 6},
-		{7, 8, 9},
-	})
-	newtable, err := removeDuplicatesMiddleware.Process(table)
-	require.NoError(t, err)
-
-	require.Equal(t, 3, len(newtable.Rows))
-	row := newtable.Rows[0].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 2, row["b"])
-	require.Equal(t, 3, row["c"])
-	row = newtable.Rows[1].GetValues()
-	require.Equal(t, 4, row["a"])
-	require.Equal(t, 5, row["b"])
-	require.Equal(t, 6, row["c"])
-	row = newtable.Rows[2].GetValues()
-	require.Equal(t, 7, row["a"])
-	require.Equal(t, 8, row["b"])
-	require.Equal(t, 9, row["c"])
-}
-
-func TestRemoveDuplicatesTwoSameWithTwoColumns(t *testing.T) {
-	removeDuplicatesMiddleware := NewRemoveDuplicatesMiddleware("a", "b")
-
-	table := createRemoveDuplicatesTables([][]int{
-		{1, 2, 3},
-		{1, 2, 4},
-	})
-	newtable, err := removeDuplicatesMiddleware.Process(table)
-	require.NoError(t, err)
-
-	require.Equal(t, 1, len(newtable.Rows))
-	row := newtable.Rows[0].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 2, row["b"])
-	require.Equal(t, 3, row["c"])
-}
diff --git a/pkg/middlewares/table/sortby_test.go b/pkg/middlewares/table/sortby_test.go
index f4984e6..2e6be1d 100644
--- a/pkg/middlewares/table/sortby_test.go
+++ b/pkg/middlewares/table/sortby_test.go
@@ -1,6 +1,8 @@
 package table
 
 import (
+	"context"
+	assert2 "github.com/go-go-golems/glazed/pkg/helpers/assert"
 	"github.com/go-go-golems/glazed/pkg/types"
 	"github.com/stretchr/testify/require"
 	"testing"
@@ -11,13 +13,13 @@ func createSortByTables(rows [][]interface{}) *types.Table {
 	ret.Columns = []types.FieldName{"a", "b", "c"}
 	ret.Rows = []types.Row{}
 	for _, row := range rows {
-		ret.Rows = append(ret.Rows, &types.SimpleRow{
-			Hash: map[string]interface{}{
-				"a": row[0],
-				"b": row[1],
-				"c": row[2],
-			},
-		})
+		ret.Rows = append(ret.Rows,
+			types.NewRow(
+				types.MRP("a", row[0]),
+				types.MRP("b", row[1]),
+				types.MRP("c", row[2]),
+			),
+		)
 	}
 
 	return ret
@@ -32,16 +34,16 @@ func TestSortByMiddlewareSingleIntColumn(t *testing.T) {
 		{4, 5, 6},
 	})
 
-	newtable, err := sortByMiddleware.Process(table)
+	newtable, err := sortByMiddleware.Process(context.Background(), table)
 	require.NoError(t, err)
 
 	require.Equal(t, 3, len(newtable.Rows))
-	row := newtable.Rows[0].GetValues()
-	require.Equal(t, 1, row["a"])
-	row = newtable.Rows[1].GetValues()
-	require.Equal(t, 4, row["a"])
-	row = newtable.Rows[2].GetValues()
-	require.Equal(t, 7, row["a"])
+	row := newtable.Rows[0]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	row = newtable.Rows[1]
+	assert2.EqualMapRowValue(t, 4, row, "a")
+	row = newtable.Rows[2]
+	assert2.EqualMapRowValue(t, 7, row, "a")
 }
 
 func TestSortByMiddlewareSingleIntColumnDesc(t *testing.T) {
@@ -53,16 +55,16 @@ func TestSortByMiddlewareSingleIntColumnDesc(t *testing.T) {
 		{4, 5, 6},
 	})
 
-	newtable, err := sortByMiddleware.Process(table)
+	newtable, err := sortByMiddleware.Process(context.Background(), table)
 	require.NoError(t, err)
 
 	require.Equal(t, 3, len(newtable.Rows))
-	row := newtable.Rows[0].GetValues()
-	require.Equal(t, 7, row["a"])
-	row = newtable.Rows[1].GetValues()
-	require.Equal(t, 4, row["a"])
-	row = newtable.Rows[2].GetValues()
-	require.Equal(t, 1, row["a"])
+	row := newtable.Rows[0]
+	assert2.EqualMapRowValue(t, 7, row, "a")
+	row = newtable.Rows[1]
+	assert2.EqualMapRowValue(t, 4, row, "a")
+	row = newtable.Rows[2]
+	assert2.EqualMapRowValue(t, 1, row, "a")
 }
 
 func TestSortByMiddlewareSingleStringColumn(t *testing.T) {
@@ -74,16 +76,16 @@ func TestSortByMiddlewareSingleStringColumn(t *testing.T) {
 		{"b", 5, 6},
 	})
 
-	newtable, err := sortByMiddleware.Process(table)
+	newtable, err := sortByMiddleware.Process(context.Background(), table)
 	require.NoError(t, err)
 
 	require.Equal(t, 3, len(newtable.Rows))
-	row := newtable.Rows[0].GetValues()
-	require.Equal(t, "a", row["a"])
-	row = newtable.Rows[1].GetValues()
-	require.Equal(t, "b", row["a"])
-	row = newtable.Rows[2].GetValues()
-	require.Equal(t, "c", row["a"])
+	row := newtable.Rows[0]
+	assert2.EqualMapRowValue(t, "a", row, "a")
+	row = newtable.Rows[1]
+	assert2.EqualMapRowValue(t, "b", row, "a")
+	row = newtable.Rows[2]
+	assert2.EqualMapRowValue(t, "c", row, "a")
 }
 
 func TestSortByMiddlewareSingleStringColumnDesc(t *testing.T) {
@@ -95,16 +97,16 @@ func TestSortByMiddlewareSingleStringColumnDesc(t *testing.T) {
 		{"b", 5, 6},
 	})
 
-	newtable, err := sortByMiddleware.Process(table)
+	newtable, err := sortByMiddleware.Process(context.Background(), table)
 	require.NoError(t, err)
 
 	require.Equal(t, 3, len(newtable.Rows))
-	row := newtable.Rows[0].GetValues()
-	require.Equal(t, "c", row["a"])
-	row = newtable.Rows[1].GetValues()
-	require.Equal(t, "b", row["a"])
-	row = newtable.Rows[2].GetValues()
-	require.Equal(t, "a", row["a"])
+	row := newtable.Rows[0]
+	assert2.EqualMapRowValue(t, "c", row, "a")
+	row = newtable.Rows[1]
+	assert2.EqualMapRowValue(t, "b", row, "a")
+	row = newtable.Rows[2]
+	assert2.EqualMapRowValue(t, "a", row, "a")
 
 }
 
@@ -118,22 +120,22 @@ func TestSortByMiddlewareTwoColumns(t *testing.T) {
 		{2, 1, 6},
 	})
 
-	newtable, err := sortByMiddleware.Process(table)
+	newtable, err := sortByMiddleware.Process(context.Background(), table)
 	require.NoError(t, err)
 
 	require.Equal(t, 4, len(newtable.Rows))
-	row := newtable.Rows[0].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 1, row["b"])
-	row = newtable.Rows[1].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 2, row["b"])
-	row = newtable.Rows[2].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 3, row["b"])
-	row = newtable.Rows[3].GetValues()
-	require.Equal(t, 2, row["a"])
-	require.Equal(t, 1, row["b"])
+	row := newtable.Rows[0]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 1, row, "b")
+	row = newtable.Rows[1]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 2, row, "b")
+	row = newtable.Rows[2]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 3, row, "b")
+	row = newtable.Rows[3]
+	assert2.EqualMapRowValue(t, 2, row, "a")
+	assert2.EqualMapRowValue(t, 1, row, "b")
 }
 
 func TestSortByMiddlewareTwoColumnsDesc(t *testing.T) {
@@ -146,22 +148,22 @@ func TestSortByMiddlewareTwoColumnsDesc(t *testing.T) {
 		{2, 1, 6},
 	})
 
-	newtable, err := sortByMiddleware.Process(table)
+	newtable, err := sortByMiddleware.Process(context.Background(), table)
 	require.NoError(t, err)
 
 	require.Equal(t, 4, len(newtable.Rows))
-	row := newtable.Rows[0].GetValues()
-	require.Equal(t, 2, row["a"])
-	require.Equal(t, 1, row["b"])
-	row = newtable.Rows[1].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 3, row["b"])
-	row = newtable.Rows[2].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 2, row["b"])
-	row = newtable.Rows[3].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 1, row["b"])
+	row := newtable.Rows[0]
+	assert2.EqualMapRowValue(t, 2, row, "a")
+	assert2.EqualMapRowValue(t, 1, row, "b")
+	row = newtable.Rows[1]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 3, row, "b")
+	row = newtable.Rows[2]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 2, row, "b")
+	row = newtable.Rows[3]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 1, row, "b")
 }
 
 func TestSortByMiddlewareTwoColumnsDescFirst(t *testing.T) {
@@ -174,22 +176,22 @@ func TestSortByMiddlewareTwoColumnsDescFirst(t *testing.T) {
 		{2, 1, 6},
 	})
 
-	newtable, err := sortByMiddleware.Process(table)
+	newtable, err := sortByMiddleware.Process(context.Background(), table)
 	require.NoError(t, err)
 
 	require.Equal(t, 4, len(newtable.Rows))
-	row := newtable.Rows[0].GetValues()
-	require.Equal(t, 2, row["a"])
-	require.Equal(t, 1, row["b"])
-	row = newtable.Rows[1].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 1, row["b"])
-	row = newtable.Rows[2].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 2, row["b"])
-	row = newtable.Rows[3].GetValues()
-	require.Equal(t, 1, row["a"])
-	require.Equal(t, 3, row["b"])
+	row := newtable.Rows[0]
+	assert2.EqualMapRowValue(t, 2, row, "a")
+	assert2.EqualMapRowValue(t, 1, row, "b")
+	row = newtable.Rows[1]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 1, row, "b")
+	row = newtable.Rows[2]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 2, row, "b")
+	row = newtable.Rows[3]
+	assert2.EqualMapRowValue(t, 1, row, "a")
+	assert2.EqualMapRowValue(t, 3, row, "b")
 }
 
 func TestSortByMiddlewareTwoColumnsDescSecond(t *testing.T) {
@@ -202,7 +204,7 @@ func TestSortByMiddlewareTwoColumnsDescSecond(t *testing.T) {
 		{2, 1, 6},
 	})
 
-	newtable, err := sortByMiddleware.Process(table)
+	newtable, err := sortByMiddleware.Process(context.Background(), table)
 	require.NoError(t, err)
 
 	require.Equal(t, 4, len(newtable.Rows))
diff --git a/pkg/middlewares/table/table_rename_test.go b/pkg/middlewares/table/table_rename_test.go
deleted file mode 100644
index 8e9676e..0000000
--- a/pkg/middlewares/table/table_rename_test.go
+++ /dev/null
@@ -1,325 +0,0 @@
-package table
-
-import (
-	"github.com/go-go-golems/glazed/pkg/types"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	"gopkg.in/yaml.v3"
-	"regexp"
-	"strings"
-	"testing"
-)
-
-func TestSingleRename(t *testing.T) {
-	renameTable := map[types.FieldName]types.FieldName{
-		"foo": "bar",
-	}
-	mw := NewFieldRenameColumnMiddleware(renameTable)
-	table := createTestTable()
-
-	newTable, ret := mw.Process(table)
-
-	require.Nil(t, ret)
-
-	row := newTable.Rows[0].(*types.SimpleRow)
-	rowMap := row.GetValues()
-
-	assert.Nil(t, rowMap["foo"])
-	assert.Equal(t, 1, rowMap["bar"])
-	assert.Equal(t, 2, rowMap["baz"])
-	assert.Equal(t, 3, rowMap["foobar"])
-}
-
-func TestRenameTwoFieldColumns(t *testing.T) {
-	renameTable := map[types.FieldName]types.FieldName{
-		"foo": "bar",
-		"baz": "qux",
-	}
-	mw := NewFieldRenameColumnMiddleware(renameTable)
-	table := createTestTable()
-
-	newTable, ret := mw.Process(table)
-	require.Nil(t, ret)
-
-	row := newTable.Rows[0].(*types.SimpleRow)
-	rowMap := row.GetValues()
-
-	assert.Nil(t, rowMap["foo"])
-	assert.Equal(t, 1, rowMap["bar"])
-	assert.Equal(t, 2, rowMap["qux"])
-	assert.Equal(t, 3, rowMap["foobar"])
-}
-
-func TestRenameOverrideColumn(t *testing.T) {
-	renameTable := map[types.FieldName]types.FieldName{
-		"foo": "foobar",
-	}
-	mw := NewFieldRenameColumnMiddleware(renameTable)
-	table := createTestTable()
-
-	newTable, ret := mw.Process(table)
-	require.Nil(t, ret)
-
-	row := newTable.Rows[0].(*types.SimpleRow)
-	rowMap := row.GetValues()
-
-	assert.Equal(t, 1, rowMap["foobar"])
-	assert.Equal(t, 2, rowMap["baz"])
-	assert.Nil(t, rowMap["foo"])
-
-}
-
-func TestRenameRegexpSimpleMatch(t *testing.T) {
-	rrs := RegexpReplacements{}
-
-	rrs = append(rrs, &RegexpReplacement{
-		regexp.MustCompile("^foo$"), "bar",
-	})
-
-	mw := NewRegexpRenameColumnMiddleware(rrs)
-	table := createTestTable()
-
-	newTable, ret := mw.Process(table)
-	require.Nil(t, ret)
-
-	row := newTable.Rows[0].(*types.SimpleRow)
-	rowMap := row.GetValues()
-
-	assert.Nil(t, rowMap["foo"])
-	assert.Equal(t, 1, rowMap["bar"])
-	assert.Equal(t, 2, rowMap["baz"])
-	assert.Equal(t, 3, rowMap["foobar"])
-}
-
-func TestRenameRegexpDoubleMatch(t *testing.T) {
-	rrs := RegexpReplacements{}
-	rrs = append(rrs, &RegexpReplacement{
-		regexp.MustCompile("f.."), "bar",
-	})
-
-	mw := NewRegexpRenameColumnMiddleware(rrs)
-	table := createTestTable()
-
-	newTable, ret := mw.Process(table)
-	require.Nil(t, ret)
-
-	row := newTable.Rows[0].(*types.SimpleRow)
-	rowMap := row.GetValues()
-
-	// here, f.. should match both fields
-	assert.Nil(t, rowMap["foo"])
-	assert.Nil(t, rowMap["foobar"])
-	assert.Equal(t, 1, rowMap["bar"])
-	assert.Equal(t, 2, rowMap["baz"])
-	assert.Equal(t, 3, rowMap["barbar"])
-}
-
-func TestRenameRegexpOrderedMatch(t *testing.T) {
-	rrs := RegexpReplacements{}
-
-	rrs = append(rrs, &RegexpReplacement{
-		regexp.MustCompile("f..$"), "bar",
-	})
-	rrs = append(rrs, &RegexpReplacement{
-		regexp.MustCompile("^foo$"), "bar2",
-	})
-	rrs = append(rrs, &RegexpReplacement{
-		regexp.MustCompile("^foo$"), "bar3",
-	})
-
-	mw := NewRegexpRenameColumnMiddleware(rrs)
-	table := createTestTable()
-
-	newTable, ret := mw.Process(table)
-	require.Nil(t, ret)
-
-	row := newTable.Rows[0].(*types.SimpleRow)
-	rowMap := row.GetValues()
-
-	// it's going to be hard to test that these will happen in the right
-	// order as it really depends on the map
-	assert.Nil(t, rowMap["foo"])
-	assert.Nil(t, rowMap["bar2"])
-	assert.Nil(t, rowMap["bar3"])
-	assert.Equal(t, 1, rowMap["bar"])
-	assert.Equal(t, 2, rowMap["baz"])
-	assert.Equal(t, 3, rowMap["foobar"])
-}
-
-func createTestTable() *types.Table {
-	table := types.Table{
-		Columns: []types.FieldName{
-			"foo",
-			"baz",
-			"foobar",
-		},
-		Rows: []types.Row{},
-	}
-	table.Rows = append(table.Rows, &types.SimpleRow{
-		Hash: types.MapRow{
-			"foo":    1,
-			"baz":    2,
-			"foobar": 3,
-		},
-	})
-
-	return &table
-}
-
-func TestBothFieldAndRegexpRenames(t *testing.T) {
-	renameTable := map[types.FieldName]types.FieldName{
-		"foo": "bar",
-		"baz": "qux",
-	}
-	rrs := RegexpReplacements{}
-	rrs = append(rrs, &RegexpReplacement{
-		regexp.MustCompile("f.."), "bar2",
-	})
-
-	mw := NewRenameColumnMiddleware(renameTable, rrs)
-	table := createTestTable()
-
-	newTable, ret := mw.Process(table)
-	require.Nil(t, ret)
-
-	row := newTable.Rows[0].(*types.SimpleRow)
-	rowMap := row.GetValues()
-
-	assert.Nil(t, rowMap["foo"])
-	assert.Nil(t, rowMap["baz"])
-	assert.Nil(t, rowMap["foobar"])
-	assert.Equal(t, 1, rowMap["bar"])
-	assert.Equal(t, 2, rowMap["qux"])
-	assert.Equal(t, 3, rowMap["bar2bar"])
-}
-
-func TestParseFromYAML(t *testing.T) {
-	yamlString := `
-renames:
-  foo: bar
-  baz: qux
-`
-	decoder := yaml.NewDecoder(strings.NewReader(yamlString))
-	mw, err := NewRenameColumnMiddlewareFromYAML(decoder)
-	require.Nil(t, err)
-
-	table := createTestTable()
-
-	newTable, ret := mw.Process(table)
-	require.Nil(t, ret)
-
-	row := newTable.Rows[0].(*types.SimpleRow)
-	rowMap := row.GetValues()
-
-	assert.Nil(t, rowMap["foo"])
-	assert.Nil(t, rowMap["baz"])
-	assert.Equal(t, 1, rowMap["bar"])
-	assert.Equal(t, 2, rowMap["qux"])
-	assert.Equal(t, 3, rowMap["foobar"])
-}
-
-func TestParseRegexpFromYAML(t *testing.T) {
-	yamlString := `
-regexpRenames:
-  "^foo$": bar2
-  f..: bar
-  "^baz$": qux
-`
-
-	decoder := yaml.NewDecoder(strings.NewReader(yamlString))
-	mw, err := NewRenameColumnMiddlewareFromYAML(decoder)
-	require.Nil(t, err)
-
-	table := createTestTable()
-
-	newTable, ret := mw.Process(table)
-	require.Nil(t, ret)
-
-	row := newTable.Rows[0].(*types.SimpleRow)
-	rowMap := row.GetValues()
-
-	assert.Nil(t, rowMap["foo"])
-	assert.Nil(t, rowMap["baz"])
-	assert.Nil(t, rowMap["foobar"])
-	assert.Equal(t, 1, rowMap["bar2"])
-	assert.Equal(t, 2, rowMap["qux"])
-	assert.Equal(t, 3, rowMap["barbar"])
-}
-
-func TestParseBothFromYAML(t *testing.T) {
-	yamlString := `
-renames:
-  foo: bar
-  baz: qux
-regexpRenames:
-  "^foo$": bar2
-  f..: bar
-  "^baz$": qux2
-`
-
-	decoder := yaml.NewDecoder(strings.NewReader(yamlString))
-	mw, err := NewRenameColumnMiddlewareFromYAML(decoder)
-	require.Nil(t, err)
-
-	table := createTestTable()
-
-	newTable, ret := mw.Process(table)
-	require.Nil(t, ret)
-
-	row := newTable.Rows[0].(*types.SimpleRow)
-	rowMap := row.GetValues()
-
-	assert.Nil(t, rowMap["foo"])
-	assert.Nil(t, rowMap["baz"])
-	assert.Nil(t, rowMap["foobar"])
-	assert.Equal(t, 1, rowMap["bar"])
-	assert.Equal(t, 2, rowMap["qux"])
-	assert.Equal(t, 3, rowMap["barbar"])
-}
-
-func TestRegexpCaptureGroupRename(t *testing.T) {
-	rrs := RegexpReplacements{}
-	rrs = append(rrs, &RegexpReplacement{
-		regexp.MustCompile("^foo(.*)$"), "bar$1",
-	})
-
-	mw := NewRegexpRenameColumnMiddleware(rrs)
-	table := createTestTable()
-
-	newTable, ret := mw.Process(table)
-	require.Nil(t, ret)
-
-	row := newTable.Rows[0].(*types.SimpleRow)
-	rowMap := row.GetValues()
-
-	assert.Nil(t, rowMap["foo"])
-	assert.Nil(t, rowMap["foobar"])
-	assert.Equal(t, 1, rowMap["bar"])
-	assert.Equal(t, 2, rowMap["baz"])
-	assert.Equal(t, 3, rowMap["barbar"])
-}
-
-func TestRegexpCaptureGroupRenameFromYAML(t *testing.T) {
-	yamlString := `
-regexpRenames:
-  "^foo(.*)$": bar$1
-`
-
-	decoder := yaml.NewDecoder(strings.NewReader(yamlString))
-	mw, err := NewRenameColumnMiddlewareFromYAML(decoder)
-	require.Nil(t, err)
-
-	table := createTestTable()
-
-	newTable, ret := mw.Process(table)
-	require.Nil(t, ret)
-
-	row := newTable.Rows[0].(*types.SimpleRow)
-	rowMap := row.GetValues()
-
-	assert.Nil(t, rowMap["foo"])
-	assert.Nil(t, rowMap["foobar"])
-	assert.Equal(t, 1, rowMap["bar"])
-	assert.Equal(t, 2, rowMap["baz"])
-	assert.Equal(t, 3, rowMap["barbar"])
-}
diff --git a/pkg/processor/processor_test.go b/pkg/processor/processor_test.go
index e2d0d0c..7c6b89b 100644
--- a/pkg/processor/processor_test.go
+++ b/pkg/processor/processor_test.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"github.com/go-go-golems/glazed/pkg/formatters/json"
+	"github.com/go-go-golems/glazed/pkg/types"
 	"testing"
 )
 
@@ -11,38 +12,49 @@ import (
 func BenchmarkSimpleGlazeProcessor(b *testing.B) {
 	ctx := context.Background()
 	gp := NewSimpleGlazeProcessor()
-	data := map[string]interface{}{
-		"name":    "Manuel Manuel",
-		"age":     30,
-		"job":     "Software Engineer",
-		"city":    "Lisbon",
-		"country": "Portugal",
-		"email":   "manuel@gogogogogogogogo.gogo",
-		"phone":   "+351 123 456 789",
-	}
+	data := types.NewRow(
+		types.MRP("name", "Manuel Manuel"),
+		types.MRP("age", 30),
+		types.MRP("job", "Software Engineer"),
+		types.MRP("city", "Lisbon"),
+		types.MRP("country", "Portugal"),
+
+		types.MRP("email", "manuel@example.com"),
+		types.MRP("phone", "+351 123 456 789"),
+	)
 	for i := 0; i < b.N; i++ {
 		_ = gp.ProcessInputObject(ctx, data)
 	}
 	buf := &bytes.Buffer{}
-	_ = gp.OutputFormatter().Output(ctx, buf)
+	err := gp.Processor().FinalizeTable(ctx)
+	if err != nil {
+		b.Fatal(err)
+	}
+
+	_ = gp.OutputFormatter().Output(ctx, gp.Processor().GetTable(), buf)
 }
 
 func BenchmarkGlazeProcessor_JSONOutputFormatter(b *testing.B) {
 	ctx := context.Background()
 
 	gp := NewGlazeProcessor(json.NewOutputFormatter())
-	data := map[string]interface{}{
-		"name":    "Manuel Manuel",
-		"age":     30,
-		"job":     "Software Engineer",
-		"city":    "Lisbon",
-		"country": "Portugal",
-		"email":   "manuel@gogogogogogogogo.gogo",
-		"phone":   "+351 123 456 789",
-	}
+	data := types.NewRow(
+		types.MRP("name", "Manuel Manuel"),
+		types.MRP("age", 30),
+		types.MRP("job", "Software Engineer"),
+		types.MRP("city", "Lisbon"),
+		types.MRP("country", "Portugal"),
+
+		types.MRP("email", "manuel@example.com"),
+		types.MRP("phone", "+351 123 456 789"),
+	)
 	for i := 0; i < b.N; i++ {
 		_ = gp.ProcessInputObject(ctx, data)
 	}
 	buf := &bytes.Buffer{}
-	_ = gp.OutputFormatter().Output(ctx, buf)
+	err := gp.Processor().FinalizeTable(ctx)
+	if err != nil {
+		b.Fatal(err)
+	}
+	_ = gp.OutputFormatter().Output(ctx, gp.Processor().GetTable(), buf)
 }
diff --git a/pkg/types/table_test.go b/pkg/types/table_test.go
new file mode 100644
index 0000000..e9b6f7f
--- /dev/null
+++ b/pkg/types/table_test.go
@@ -0,0 +1,33 @@
+package types
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestSetColumnOrder(t *testing.T) {
+	table := NewTable()
+
+	table.SetColumnOrder([]string{"a", "b", "c"})
+	assert.Equal(t, []string{"a", "b", "c"}, table.Columns)
+
+	table = NewTable()
+	table.Columns = []string{"a", "b", "c"}
+	table.SetColumnOrder([]string{"a", "b", "c"})
+	assert.Equal(t, []string{"a", "b", "c"}, table.Columns)
+
+	table = NewTable()
+	table.Columns = []string{"a", "b", "c"}
+	table.SetColumnOrder([]string{"a", "c", "b"})
+	assert.Equal(t, []string{"a", "c", "b"}, table.Columns)
+
+	table = NewTable()
+	table.Columns = []string{"a", "b", "c"}
+	table.SetColumnOrder([]string{"a", "c"})
+	assert.Equal(t, []string{"a", "c", "b"}, table.Columns)
+
+	table = NewTable()
+	table.Columns = []string{"a", "b", "c"}
+	table.SetColumnOrder([]string{"a", "c", "d"})
+	assert.Equal(t, []string{"a", "c", "d", "b"}, table.Columns)
+}