Repository: brimsec/zq
Pull request #2401 (2021-03-23 20:54:29)

fix missing comma in map type body in zson.Formatter


Patch:
diff --git a/cli/outputflags/flags.go b/cli/outputflags/flags.go
index 7066ee379..c34845284 100644
--- a/cli/outputflags/flags.go
+++ b/cli/outputflags/flags.go
@@ -65,7 +65,7 @@ func (f *Flags) SetFormatFlags(fs *flag.FlagSet) {
 	if f.DefaultFormat == "" {
 		f.DefaultFormat = "zng"
 	}
-	fs.StringVar(&f.Format, "f", f.DefaultFormat, "format for output data [zng,zst,ndjson,table,text,csv,zeek,zjson,zson,tzng]")
+	fs.StringVar(&f.Format, "f", f.DefaultFormat, "format for output data [zng,zst,ndjson,parquet,table,text,csv,zeek,zjson,zson,tzng]")
 	fs.BoolVar(&f.zsonShortcut, "z", false, "use line-oriented zson output independent of -f option")
 	fs.BoolVar(&f.zsonPretty, "Z", false, "use formatted zson output independent of -f option")
 	fs.BoolVar(&f.forceBinary, "B", false, "allow binary zng be sent to a terminal output")
diff --git a/cmd/zq/zq.go b/cmd/zq/zq.go
index e3fe0abca..be40814ff 100644
--- a/cmd/zq/zq.go
+++ b/cmd/zq/zq.go
@@ -130,7 +130,6 @@ func (c *Command) Run(args []string) error {
 	if err != nil {
 		return err
 	}
-	paths := args
 	var srcs []string
 	for _, path := range c.includes {
 		b, err := ioutil.ReadFile(path)
@@ -139,7 +138,13 @@ func (c *Command) Run(args []string) error {
 		}
 		srcs = append(srcs, string(b))
 	}
+	paths := args
 	if !cli.FileExists(paths[0]) && !s3io.IsS3Path(paths[0]) {
+		if len(paths) == 1 {
+			// We don't interpret the first arg as a query if there
+			// are no additional args.
+			return fmt.Errorf("zq: no such file: %s", paths[0])
+		}
 		srcs = append(srcs, paths[0])
 		paths = paths[1:]
 	}
diff --git a/go.mod b/go.mod
index 92719629d..8d10c717d 100644
--- a/go.mod
+++ b/go.mod
@@ -5,11 +5,11 @@ go 1.16
 require (
 	github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4
 	github.com/alexbrainman/ps v0.0.0-20171229230509-b3e1b4a15894
-	github.com/apache/thrift v0.0.0-20181112125854-24918abba929
 	github.com/aws/aws-sdk-go v1.36.17
 	github.com/axiomhq/hyperloglog v0.0.0-20191112132149-a4c4c47bc57f
 	github.com/buger/jsonparser v0.0.0-20191004114745-ee4c978eae7e
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
+	github.com/fraugster/parquet-go v0.3.0
 	github.com/fsnotify/fsnotify v1.4.9
 	github.com/go-pg/pg/v10 v10.7.3
 	github.com/go-redis/redis/v8 v8.4.11
@@ -32,8 +32,6 @@ require (
 	github.com/prometheus/client_model v0.2.0
 	github.com/segmentio/ksuid v1.0.2
 	github.com/stretchr/testify v1.6.1
-	github.com/xitongsys/parquet-go v1.5.3-0.20200514000040-789bba367841
-	github.com/xitongsys/parquet-go-source v0.0.0-20200509081216-8db33acb0acf
 	github.com/yuin/goldmark v1.2.1
 	go.temporal.io/sdk v1.4.1
 	go.temporal.io/server v1.6.3
@@ -47,4 +45,6 @@ require (
 	gopkg.in/yaml.v3 v3.0.0-20210106172901-c476de37821d
 )
 
+replace github.com/fraugster/parquet-go => github.com/brimsec/parquet-go v0.3.1
+
 replace github.com/minio/minio => github.com/brimsec/minio v0.0.0-20201019191454-3c6f24527f6d
diff --git a/go.sum b/go.sum
index d9a5806ba..c61ac3915 100644
--- a/go.sum
+++ b/go.sum
@@ -59,10 +59,10 @@ github.com/alexbrainman/ps v0.0.0-20171229230509-b3e1b4a15894/go.mod h1:Wgrp3f69
 github.com/apache/arrow/go/arrow v0.0.0-20200601151325-b2287a20f230/go.mod h1:QNYViu/X0HXDHw7m3KXzWSVXIbfUvJqBFe6Gj8/pYA0=
 github.com/apache/thrift v0.0.0-20150905105024-5bc8b5a3a5da/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/apache/thrift v0.0.0-20161221203622-b2a4d4ae21c7/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
-github.com/apache/thrift v0.0.0-20181112125854-24918abba929 h1:ubPe2yRkS6A/X37s0TVGfuN42NV2h0BlzWj0X76RoUw=
-github.com/apache/thrift v0.0.0-20181112125854-24918abba929/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI=
+github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
-github.com/aws/aws-sdk-go v1.30.19/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
 github.com/aws/aws-sdk-go v1.35.20/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
 github.com/aws/aws-sdk-go v1.36.17 h1:8zTvseyGhgs3uQAzkgnFy7dvTo+ZnZLYmrhnopFxYME=
 github.com/aws/aws-sdk-go v1.36.17/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
@@ -80,6 +80,8 @@ github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwj
 github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
 github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
+github.com/brimsec/parquet-go v0.3.1 h1:4NFIzfGQvU1Ne8VP19bOy6gVBvA+6JFliXwdH8GWmEc=
+github.com/brimsec/parquet-go v0.3.1/go.mod h1:qIL8Wm6AK06QHCj9OBFW6PyS+7ukZxc20K/acSeGUas=
 github.com/bsm/sarama-cluster v2.1.15+incompatible/go.mod h1:r7ao+4tTNXvWm+VRpRJchr2kQhqxgmAp2iEX5W96gMM=
 github.com/buger/jsonparser v0.0.0-20191004114745-ee4c978eae7e h1:oJCXMss/3rg5F6Poy9wG3JQusc58Mzk5B9Z6wSnssNE=
 github.com/buger/jsonparser v0.0.0-20191004114745-ee4c978eae7e/go.mod h1:errmMKH8tTB49UR2A8C8DPYkyudelsYJwJFaZHQ6ik8=
@@ -102,12 +104,15 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht
 github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
 github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
 github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
-github.com/colinmarc/hdfs/v2 v2.1.1/go.mod h1:M3x+k8UKKmxtFu++uAZ0OtDU8jR3jnaZIAc6yK4Ue0c=
 github.com/containerd/containerd v1.4.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY=
 github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
@@ -201,7 +206,6 @@ github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
 github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
 github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -218,7 +222,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
 github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
 github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -276,16 +279,17 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
 github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
-github.com/hashicorp/go-uuid v0.0.0-20180228145832-27454136f036/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
 github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/iancoleman/strcase v0.1.2/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/influxdata/influxdb v1.7.6/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
 github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
 github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
@@ -311,12 +315,10 @@ github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9
 github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
 github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
-github.com/jcmturner/gofork v0.0.0-20180107083740-2aebee971930/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
 github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
-github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@@ -337,9 +339,7 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=
 github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -363,6 +363,7 @@ github.com/m3db/prometheus_client_golang v0.8.1/go.mod h1:8R/f1xYhXWq59KD/mbRqoB
 github.com/m3db/prometheus_client_model v0.1.0/go.mod h1:Qfsxn+LypxzF+lNhak7cF7k0zxK7uB/ynGYoj80zcD4=
 github.com/m3db/prometheus_common v0.1.0/go.mod h1:EBmDQaMAy4B8i+qsg1wMXAelLNVbp49i/JOeVszQ/rs=
 github.com/m3db/prometheus_procfs v0.8.1/go.mod h1:N8lv8fLh3U3koZx1Bnisj60GYUMDpWb09x1R+dmMOJo=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
 github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -383,7 +384,9 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mccanne/charm v0.0.3-0.20191224190439-b05e1b7b1be3 h1:Sj9zugMeJDXrKpOZgiQXhm0Z87vPF2QfUMWE41lwcRQ=
 github.com/mccanne/charm v0.0.3-0.20191224190439-b05e1b7b1be3/go.mod h1:R47PUgWIRA4UrwFWKI6vgAEWHXPYWUg3iAtyXB8yKNU=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
 github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -423,9 +426,9 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+
 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
 github.com/pbnjay/memory v0.0.0-20190104145345-974d429e7ae4 h1:MfIUBZ1bz7TgvQLVa/yPJZOGeKEgs6eTKUjz3zB4B+U=
 github.com/pbnjay/memory v0.0.0-20190104145345-974d429e7ae4/go.mod h1:RMU2gJXhratVxBDTFeOdNhd540tG57lt9FIUV0YLvIQ=
-github.com/pborman/getopt v0.0.0-20180729010549-6fdd0a2c7117/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
 github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
 github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/peterh/liner v1.1.0 h1:f+aAedNJA6uk7+6rXsYBnhdo4Xux7ESLe+kcuVUF5os=
 github.com/peterh/liner v1.1.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
@@ -469,6 +472,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
 github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
 github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
 github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/samuel/go-thrift v0.0.0-20191111193933-5165175b40af/go.mod h1:Vrkh1pnjV9Bl8c3P9zH0/D4NlOHWP5d4/hF4YTULaec=
@@ -490,7 +494,13 @@ github.com/smartystreets/gunit v1.4.2/go.mod h1:ZjM1ozSIMJlAz/ay4SG8PeKF00ckUp+z
 github.com/snowflakedb/glog v0.0.0-20180824191149-f5055e6f21ce/go.mod h1:EB/w24pR5VKI60ecFnKqXzxX3dOorz1rnVicQTQrGM0=
 github.com/snowflakedb/gosnowflake v1.3.5/go.mod h1:13Ky+lxzIm3VqNDZJdyvu9MCGy+WgRdYFdXp96UcLZU=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
 github.com/streadway/quantile v0.0.0-20150917103942-b0c588724d25/go.mod h1:lbP8tGiBjZ5YWIc2fzuRpTaz0b/53vT6PEs3QuAWzuU=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -522,6 +532,7 @@ github.com/uber/jaeger-lib v2.2.0+incompatible h1:MxZXOiR2JuoANZ3J6DE/U0kSFv/eJ/
 github.com/uber/jaeger-lib v2.2.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
 github.com/uber/tchannel-go v1.16.0/go.mod h1:Rrgz1eL8kMjW/nEzZos0t+Heq0O4LhnUJVA32OvWKHo=
 github.com/uber/tchannel-go v1.21.0/go.mod h1:Rrgz1eL8kMjW/nEzZos0t+Heq0O4LhnUJVA32OvWKHo=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
 github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
@@ -537,13 +548,7 @@ github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgq
 github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
 github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
 github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
-github.com/xitongsys/parquet-go v1.5.1/go.mod h1:xUxwM8ELydxh4edHGegYq1pA8NnMKDx0K/GyB0o2bww=
-github.com/xitongsys/parquet-go v1.5.3-0.20200514000040-789bba367841 h1:zeXUneOiRdemUEwwcjjlQsPhoW56V/5mDSl5AgCvoSk=
-github.com/xitongsys/parquet-go v1.5.3-0.20200514000040-789bba367841/go.mod h1:90swTgY6VkNM4MkMDsNxq8h30m6Yj1Arv9UMEl5V5DM=
-github.com/xitongsys/parquet-go-source v0.0.0-20190524061010-2b72cbee77d5/go.mod h1:xxCx7Wpym/3QCo6JhujJX51dzSXrwmb0oH6FQb39SEA=
-github.com/xitongsys/parquet-go-source v0.0.0-20200326031722-42b453e70c3b/go.mod h1:xxCx7Wpym/3QCo6JhujJX51dzSXrwmb0oH6FQb39SEA=
-github.com/xitongsys/parquet-go-source v0.0.0-20200509081216-8db33acb0acf h1:pB0j89pb2GQKfagu2KEnpNUj2xR4fMsdf4Gp3WhO+hQ=
-github.com/xitongsys/parquet-go-source v0.0.0-20200509081216-8db33acb0acf/go.mod h1:EVm7J5W7X/BJsvlGnCaj81kYxgbNzssi/+LF16FoV2s=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/xwb1989/sqlparser v0.0.0-20171128062118-da747e0c62c4/go.mod h1:hzfGeIUDq/j97IG+FhNqkowIyEcD88LrW6fyU3K3WqY=
 github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2/go.mod h1:hzfGeIUDq/j97IG+FhNqkowIyEcD88LrW6fyU3K3WqY=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -593,9 +598,9 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
 go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
 go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
-golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -716,6 +721,7 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -971,7 +977,6 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
 gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
 gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=
-gopkg.in/jcmturner/gokrb5.v7 v7.3.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
 gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
 gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
diff --git a/ppl/zqd/ingest/pcap.go b/ppl/zqd/ingest/pcap.go
index f4b9694e0..0eac0d8de 100644
--- a/ppl/zqd/ingest/pcap.go
+++ b/ppl/zqd/ingest/pcap.go
@@ -43,7 +43,7 @@ type PcapOp interface {
 var suricataShaper = compiler.MustParseProc(
 	`type alert = {event_type:bstring,src_ip:ip,src_port:port=(uint16),dest_ip:ip,dest_port:port=(uint16),vlan:[uint16],proto:bstring,app_proto:bstring,alert:{severity:uint16,signature:bstring,category:bstring,action:bstring,signature_id:uint64,gid:uint64,rev:uint64,metadata:{signature_severity:[bstring],former_category:[bstring],attack_target:[bstring],deployment:[bstring],affected_product:[bstring],created_at:[bstring],performance_impact:[bstring],updated_at:[bstring],malware_family:[bstring],tag:[bstring]}},flow_id:uint64,pcap_cnt:uint64,timestamp:time,tx_id:uint64,icmp_code:uint64,icmp_type:uint64,tunnel:{src_ip:ip,src_port:port=(uint16),dest_ip:ip,dest_port:port=(uint16),proto:bstring,depth:uint64},community_id:bstring}
 
-put timestamp=iso(timestamp) | put . = shape(alert) | rename ts=timestamp
+put . = shape(alert) | rename ts=timestamp
 `)
 
 type ClearableStore interface {
diff --git a/zio/detector/file.go b/zio/detector/file.go
index cdffb9173..98713dc12 100644
--- a/zio/detector/file.go
+++ b/zio/detector/file.go
@@ -9,13 +9,8 @@ import (
 	"github.com/brimsec/zq/pkg/nano"
 	"github.com/brimsec/zq/zbuf"
 	"github.com/brimsec/zq/zio"
-	"github.com/brimsec/zq/zio/parquetio"
 	"github.com/brimsec/zq/zng"
 	"github.com/brimsec/zq/zng/resolver"
-
-	"github.com/xitongsys/parquet-go-source/local"
-	parquets3 "github.com/xitongsys/parquet-go-source/s3"
-	"github.com/xitongsys/parquet-go/source"
 )
 
 // OpenFile creates and returns zbuf.File for the indicated "path",
@@ -31,12 +26,6 @@ func OpenFileWithContext(ctx context.Context, zctx *resolver.Context, path strin
 	if err != nil {
 		return nil, err
 	}
-
-	// Parquet is special and needs its own reader for s3 sources.
-	if opts.Format == "parquet" {
-		return OpenParquet(zctx, uri, opts)
-	}
-
 	f, err := iosrc.NewReader(ctx, uri)
 	if err != nil {
 		return nil, err
@@ -44,29 +33,10 @@ func OpenFileWithContext(ctx context.Context, zctx *resolver.Context, path strin
 	return OpenFromNamedReadCloser(zctx, f, path, opts)
 }
 
-func OpenParquet(zctx *resolver.Context, uri iosrc.URI, opts zio.ReaderOpts) (*zbuf.File, error) {
-	var pf source.ParquetFile
-	var err error
-	if uri.Scheme == "s3" {
-		pf, err = parquets3.NewS3FileReader(context.Background(), uri.Host, uri.Path, opts.AwsCfg)
-	} else {
-		pf, err = local.NewLocalFileReader(uri.Filepath())
-	}
-	if err != nil {
-		return nil, err
-	}
-
-	r, err := parquetio.NewReader(pf, zctx, parquetio.ReaderOpts{})
-	if err != nil {
-		return nil, err
-	}
-	return zbuf.NewFile(r, pf, uri.String()), nil
-}
-
 func OpenFromNamedReadCloser(zctx *resolver.Context, rc io.ReadCloser, path string, opts zio.ReaderOpts) (*zbuf.File, error) {
 	var err error
 	r := io.Reader(rc)
-	if opts.Format != "zst" {
+	if opts.Format != "parquet" && opts.Format != "zst" {
 		r = GzipReader(rc)
 	}
 	var zr zbuf.Reader
diff --git a/zio/detector/lookup.go b/zio/detector/lookup.go
index 2af7407d1..6a813f8f9 100644
--- a/zio/detector/lookup.go
+++ b/zio/detector/lookup.go
@@ -8,6 +8,7 @@ import (
 	"github.com/brimsec/zq/zio"
 	"github.com/brimsec/zq/zio/csvio"
 	"github.com/brimsec/zq/zio/ndjsonio"
+	"github.com/brimsec/zq/zio/parquetio"
 	"github.com/brimsec/zq/zio/tableio"
 	"github.com/brimsec/zq/zio/textio"
 	"github.com/brimsec/zq/zio/tzngio"
@@ -62,6 +63,8 @@ func LookupWriter(w io.WriteCloser, zctx *resolver.Context, opts zio.WriterOpts)
 			Fuse:       opts.CSVFuse,
 			UTF8:       opts.UTF8,
 		}), nil
+	case "parquet":
+		return parquetio.NewWriter(w), nil
 	default:
 		return nil, fmt.Errorf("unknown format: %s", opts.Format)
 	}
@@ -85,6 +88,8 @@ func lookupReader(r io.Reader, zctx *resolver.Context, path string, opts zio.Rea
 		return zson.NewReader(r, zctx.Context), nil
 	case "zst":
 		return zstio.NewReader(r, zctx)
+	case "parquet":
+		return parquetio.NewReader(r, zctx)
 	}
 	return nil, fmt.Errorf("no such format: \"%s\"", opts.Format)
 }
diff --git a/zio/parquetio/builder.go b/zio/parquetio/builder.go
new file mode 100644
index 000000000..9e55ec740
--- /dev/null
+++ b/zio/parquetio/builder.go
@@ -0,0 +1,83 @@
+package parquetio
+
+import (
+	"fmt"
+
+	"github.com/brimsec/zq/zcode"
+	"github.com/brimsec/zq/zng"
+)
+
+type builder struct {
+	zcode.Builder
+	buf []byte
+}
+
+func (b *builder) appendValue(typ zng.Type, v interface{}) {
+	switch v := v.(type) {
+	case nil:
+		b.AppendNull()
+	case []byte:
+		b.AppendPrimitive(v)
+	case bool:
+		b.buf = zng.AppendBool(b.buf[:0], v)
+		b.AppendPrimitive(b.buf)
+	case float32:
+		b.buf = zng.AppendFloat64(b.buf[:0], float64(v))
+		b.AppendPrimitive(b.buf)
+	case float64:
+		b.buf = zng.AppendFloat64(b.buf[:0], v)
+		b.AppendPrimitive(b.buf)
+	case int32:
+		b.buf = zng.AppendInt(b.buf[:0], int64(v))
+		b.AppendPrimitive(b.buf)
+	case int64:
+		b.buf = zng.AppendInt(b.buf[:0], v)
+		b.AppendPrimitive(b.buf)
+	case uint32:
+		b.buf = zng.AppendUint(b.buf[:0], uint64(v))
+		b.AppendPrimitive(b.buf)
+	case uint64:
+		b.buf = zng.AppendUint(b.buf[:0], v)
+		b.AppendPrimitive(b.buf)
+	case map[string]interface{}:
+		switch typ := zng.AliasOf(typ).(type) {
+		case *zng.TypeArray:
+			switch v := v["list"].(type) {
+			case nil:
+				b.AppendNull()
+			case []map[string]interface{}:
+				b.BeginContainer()
+				for _, m := range v {
+					b.appendValue(typ.Type, m["element"])
+				}
+				b.EndContainer()
+			default:
+				panic(fmt.Sprintf("unknown type %T", v))
+			}
+		case *zng.TypeMap:
+			switch v := v["key_value"].(type) {
+			case nil:
+				b.AppendNull()
+			case []map[string]interface{}:
+				b.BeginContainer()
+				for _, m := range v {
+					b.appendValue(typ.KeyType, m["key"])
+					b.appendValue(typ.ValType, m["value"])
+				}
+				b.EndContainer()
+			default:
+				panic(fmt.Sprintf("unknown type %T", v))
+			}
+		case *zng.TypeRecord:
+			b.BeginContainer()
+			for _, c := range typ.Columns {
+				b.appendValue(c.Type, v[c.Name])
+			}
+			b.EndContainer()
+		default:
+			panic(fmt.Sprintf("unknown type %T", typ))
+		}
+	default:
+		panic(fmt.Sprintf("unknown type %T", v))
+	}
+}
diff --git a/zio/parquetio/data.go b/zio/parquetio/data.go
new file mode 100644
index 000000000..fd8d79bbb
--- /dev/null
+++ b/zio/parquetio/data.go
@@ -0,0 +1,129 @@
+package parquetio
+
+import (
+	"fmt"
+
+	"github.com/brimsec/zq/zcode"
+	"github.com/brimsec/zq/zng"
+)
+
+func newData(typ zng.Type, zb zcode.Bytes) (interface{}, error) {
+	if zb == nil {
+		return nil, nil
+	}
+	switch typ := zng.AliasOf(typ).(type) {
+	case *zng.TypeOfUint8:
+		v, err := zng.DecodeUint(zb)
+		return uint32(v), err
+	case *zng.TypeOfUint16:
+		v, err := zng.DecodeUint(zb)
+		return uint32(v), err
+	case *zng.TypeOfUint32:
+		v, err := zng.DecodeUint(zb)
+		return uint32(v), err
+	case *zng.TypeOfUint64:
+		return zng.DecodeUint(zb)
+	case *zng.TypeOfInt8:
+		v, err := zng.DecodeInt(zb)
+		return int32(v), err
+	case *zng.TypeOfInt16:
+		v, err := zng.DecodeInt(zb)
+		return int32(v), err
+	case *zng.TypeOfInt32:
+		v, err := zng.DecodeInt(zb)
+		return int32(v), err
+	case *zng.TypeOfInt64, *zng.TypeOfDuration, *zng.TypeOfTime:
+		return zng.DecodeInt(zb)
+	// XXX add TypeFloat16
+	// XXX add TypeFloat32
+	case *zng.TypeOfFloat64:
+		return zng.DecodeFloat64(zb)
+	// XXX add TypeDecimal
+	case *zng.TypeOfBool:
+		return zng.DecodeBool(zb)
+	case *zng.TypeOfBytes, *zng.TypeOfBstring, *zng.TypeOfString:
+		return zng.DecodeBytes(zb)
+	case *zng.TypeOfIP:
+		v, err := zng.DecodeIP(zb)
+		return []byte(v.String()), err
+	case *zng.TypeOfNet:
+		v, err := zng.DecodeNet(zb)
+		return []byte(v.String()), err
+	case *zng.TypeOfType, *zng.TypeOfError:
+		return zng.DecodeBytes(zb)
+	case *zng.TypeOfNull:
+		return nil, ErrNullType
+	case *zng.TypeRecord:
+		return newRecordData(typ, zb)
+	case *zng.TypeArray:
+		return newListData(typ.Type, zb)
+	case *zng.TypeSet:
+		return newListData(typ.Type, zb)
+	case *zng.TypeUnion:
+		return nil, ErrUnionType
+	case *zng.TypeEnum:
+		return []byte(typ.ZSONOf(zb)), nil
+	case *zng.TypeMap:
+		return newMapData(typ.KeyType, typ.ValType, zb)
+	}
+	panic(fmt.Sprintf("unknown type %T", typ))
+}
+
+func newListData(typ zng.Type, zb zcode.Bytes) (map[string]interface{}, error) {
+	var elements []map[string]interface{}
+	for it := zb.Iter(); !it.Done(); {
+		zb2, _, err := it.Next()
+		if err != nil {
+			return nil, err
+		}
+		v, err := newData(typ, zb2)
+		if err != nil {
+			return nil, err
+		}
+		elements = append(elements, map[string]interface{}{"element": v})
+	}
+	return map[string]interface{}{"list": elements}, nil
+}
+
+func newMapData(keyType, valType zng.Type, zb zcode.Bytes) (map[string]interface{}, error) {
+	var elements []map[string]interface{}
+	for i, it := 0, zb.Iter(); !it.Done(); i++ {
+		keyBytes, _, err := it.Next()
+		if err != nil {
+			return nil, err
+		}
+		key, err := newData(keyType, keyBytes)
+		if err != nil {
+			return nil, err
+		}
+		valBytes, _, err := it.Next()
+		if err != nil {
+			return nil, err
+		}
+		val, err := newData(valType, valBytes)
+		if err != nil {
+			return nil, err
+		}
+		elements = append(elements, map[string]interface{}{
+			"key":   key,
+			"value": val,
+		})
+	}
+	return map[string]interface{}{"key_value": elements}, nil
+}
+
+func newRecordData(typ *zng.TypeRecord, zb zcode.Bytes) (map[string]interface{}, error) {
+	m := make(map[string]interface{}, len(typ.Columns))
+	for i, it := 0, zb.Iter(); !it.Done(); i++ {
+		zb2, _, err := it.Next()
+		if err != nil {
+			return nil, err
+		}
+		v, err := newData(typ.Columns[i].Type, zb2)
+		if err != nil {
+			return nil, err
+		}
+		m[typ.Columns[i].Name] = v
+	}
+	return m, nil
+}
diff --git a/zio/parquetio/iterator.go b/zio/parquetio/iterator.go
deleted file mode 100644
index 057e0c15c..000000000
--- a/zio/parquetio/iterator.go
+++ /dev/null
@@ -1,748 +0,0 @@
-package parquetio
-
-import (
-	"encoding/binary"
-	"fmt"
-	"io"
-	"math"
-
-	"github.com/apache/thrift/lib/go/thrift"
-	"github.com/xitongsys/parquet-go/common"
-	"github.com/xitongsys/parquet-go/compress"
-	"github.com/xitongsys/parquet-go/layout"
-	"github.com/xitongsys/parquet-go/parquet"
-	"github.com/xitongsys/parquet-go/source"
-)
-
-// Helper to avoid sprinkling fmt.Printf() calls around the code.
-// Enable debug statements by uncommenting the call to Printf()
-func debugf(msg string, args ...interface{}) {
-	//fmt.Printf(msg, args...)
-}
-
-// valueReader is the interface used for anything that can iterate over
-// a series of values of one of the parquet primitive types.  Note that
-// not all methods can be used on all implementations of this interface.
-type valueReader interface {
-	nextBoolean() bool
-	nextInt32() int32
-	nextInt64() int64
-	nextFloat() float64
-	nextDouble() float64
-	nextByteArray() []byte
-}
-
-// columnIterator reads and emits all the values from an individual
-// column inside a parquet file.  This implementation is not yet complete,
-// it only handles PLAIN and PLAIN_DICTIONARY encodings for a few
-// primitive data types.
-type columnIterator struct {
-	name   string
-	footer *parquet.FileMetaData
-	file   source.ParquetFile
-
-	maxRepetitionLevel int32
-	maxDefinitionLevel int32
-
-	rowGroup int
-
-	groupRead    int
-	groupTotal   int
-	thriftReader *thrift.TBufferedTransport
-	colMetadata  *parquet.ColumnMetaData
-
-	pageRead  int
-	pageTotal int
-
-	rlReader  *hybridReader
-	dlReader  *hybridReader
-	valReader valueReader
-
-	// only used for PageType_DICTIONARY_PAGE
-	int64Dict     []int64
-	floatDict     []float64
-	byteArrayDict [][]byte
-	// XXX need other types
-}
-
-func newColumnIterator(name string, footer *parquet.FileMetaData, file source.ParquetFile, maxRL, maxDL int32) *columnIterator {
-	return &columnIterator{
-		name:               name,
-		footer:             footer,
-		file:               file,
-		maxRepetitionLevel: maxRL,
-		maxDefinitionLevel: maxDL,
-	}
-}
-
-func (i *columnIterator) clearDictionaries() {
-	i.byteArrayDict = nil
-	i.int64Dict = nil
-	i.floatDict = nil
-	// XXX clear others as they are added
-}
-
-// loadOnePage reads the next page that is part of this column.
-func (i *columnIterator) loadOnePage() (*parquet.PageHeader, []byte, error) {
-	// If we've reached the end of a row group (or if we haven't
-	// yet read the first row group), find the right offset inside
-	// the parquet file for this column in the next row group.
-	if i.groupRead == i.groupTotal {
-		if i.rowGroup >= len(i.footer.RowGroups) {
-			return nil, nil, io.EOF
-		}
-		rg := i.footer.RowGroups[i.rowGroup]
-		i.rowGroup++
-
-		i.groupRead = 0
-		i.groupTotal = int(rg.NumRows)
-
-		var col *parquet.ColumnChunk
-		for _, c := range rg.Columns {
-			if c.MetaData.PathInSchema[0] == i.name {
-				col = c
-				break
-			}
-		}
-		if col == nil {
-			return nil, nil, fmt.Errorf("cannot find ColumnChunk for %s", i.name)
-		}
-
-		if col.FilePath != nil {
-			return nil, nil, fmt.Errorf("ColumnChunk for %s refers to external file", i.name)
-		}
-
-		i.colMetadata = col.MetaData
-		offset := i.colMetadata.DataPageOffset
-		if i.colMetadata.DictionaryPageOffset != nil {
-			offset = *i.colMetadata.DictionaryPageOffset
-		}
-		size := i.colMetadata.TotalCompressedSize
-
-		// XXX
-		i.thriftReader = source.ConvertToThriftReader(i.file, offset, size)
-
-		i.clearDictionaries()
-	}
-
-	// Pages within a row group are sequential in the file.
-	// The thriftReader object keeps track of the file offset,
-	// for this column.  Read the header for the next page from it.
-	header, err := layout.ReadPageHeader(i.thriftReader)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	raw := make([]byte, header.GetCompressedPageSize())
-	if _, err = i.thriftReader.Read(raw); err != nil {
-		return nil, nil, err
-	}
-
-	page, err := compress.Uncompress(raw, i.colMetadata.GetCodec())
-	if err != nil {
-		return nil, nil, err
-	}
-
-	return header, page, nil
-}
-
-// ensureDataPage loads a new data page if the current page has been
-// completely read (or if no page has yet been loaded).  This may include
-// loading and parsing dictionary pages, but when this function returns,
-// the caller may assume there are valid values available in this struct's
-// data structures to decode.
-func (i *columnIterator) ensureDataPage() error {
-	if i.pageRead < i.pageTotal {
-		return nil
-	}
-
-	for {
-		header, page, err := i.loadOnePage()
-		if err != nil {
-			return err
-		}
-
-		debugf("read page type %s for %s\n", header.GetType(), i.name)
-		switch header.GetType() {
-		case parquet.PageType_DICTIONARY_PAGE:
-			if err := i.loadDictionaryPage(header, page); err != nil {
-				return err
-			}
-
-		case parquet.PageType_DATA_PAGE:
-			return i.initializeDataPage(header, page)
-
-		default:
-			return fmt.Errorf("unhandled page type %s", header.GetType())
-		}
-	}
-}
-
-func (i *columnIterator) loadDictionaryPage(header *parquet.PageHeader, buf []byte) error {
-	n := int(header.DictionaryPageHeader.GetNumValues())
-	switch i.colMetadata.GetType() {
-	case parquet.Type_INT64:
-		i.int64Dict = make([]int64, n)
-		r := plainReader{buf}
-		for j := 0; j < n; j++ {
-			i.int64Dict[j] = r.nextInt64()
-		}
-
-	case parquet.Type_DOUBLE:
-		i.floatDict = make([]float64, n)
-		r := plainReader{buf}
-		for j := 0; j < n; j++ {
-			i.floatDict[j] = r.nextDouble()
-		}
-
-	case parquet.Type_BYTE_ARRAY:
-		i.byteArrayDict = make([][]byte, n)
-		r := plainReader{buf}
-		for j := 0; j < n; j++ {
-			i.byteArrayDict[j] = r.nextByteArray()
-		}
-
-	default:
-		debugf("skipping dictionary page for type %s\n", i.colMetadata.GetType())
-	}
-	return nil
-}
-
-// grabLenDenotedBuf extracts a chunk of data that represents
-// repetition level (RL) or definition level (DL) data inside a data page.
-// This consists of a 4 byte integer that represents the size of the
-// encoded data followed by the actual encoded data.  Returns the
-// length-denoted encoded data and the total number of bytes that were
-// required from the original buffer.
-func grabLenDenotedBuf(buf []byte) ([]byte, int, error) {
-	if len(buf) < 4 {
-		return nil, 0, fmt.Errorf("buffer is too short (%d)", len(buf))
-	}
-	ln := binary.LittleEndian.Uint32(buf[:4])
-	total := int(4 + ln)
-	if len(buf) < total {
-		return nil, 0, fmt.Errorf("buffer is too short (%d, need %d)", len(buf), total)
-	}
-	return buf[4:total], total, nil
-}
-
-// initializeDataPage sets up the rlReader, dlReader, and valReader
-// members of this struct for a new data page.
-func (i *columnIterator) initializeDataPage(header *parquet.PageHeader, buf []byte) error {
-	i.pageRead = 0
-	i.pageTotal = int(header.DataPageHeader.GetNumValues())
-
-	// Per https://github.com/apache/parquet-format#data-pages
-	// a page contains the optional RL data, the optional DL data,
-	// and the encoded values, all back-to-back in the page.
-	if i.maxRepetitionLevel > 0 {
-		width := int(common.BitNum(uint64(i.maxRepetitionLevel)))
-		hbuf, n, err := grabLenDenotedBuf(buf)
-		if err != nil {
-			return err
-		}
-		buf = buf[n:]
-		i.rlReader = newHybridReader(hbuf, width)
-	} else {
-		i.rlReader = nil
-	}
-
-	if i.maxDefinitionLevel > 0 {
-		width := int(common.BitNum(uint64(i.maxDefinitionLevel)))
-		hbuf, n, err := grabLenDenotedBuf(buf)
-		if err != nil {
-			return err
-		}
-		buf = buf[n:]
-		i.dlReader = newHybridReader(hbuf, width)
-	} else {
-		i.dlReader = nil
-	}
-
-	enc := header.DataPageHeader.GetEncoding()
-	typ := i.colMetadata.GetType()
-	switch enc {
-	case parquet.Encoding_PLAIN:
-		if typ == parquet.Type_BOOLEAN {
-			i.valReader = &plainBooleanReader{buf: buf}
-		} else {
-			i.valReader = &plainReader{buf}
-		}
-	case parquet.Encoding_PLAIN_DICTIONARY:
-		switch typ {
-		case parquet.Type_INT64:
-			i.valReader = newDictionaryInt64Reader(buf, i.int64Dict)
-
-		case parquet.Type_DOUBLE:
-			i.valReader = newDictionaryDoubleReader(buf, i.floatDict)
-
-		case parquet.Type_BYTE_ARRAY:
-			i.valReader = newDictionaryByteArrayReader(buf, i.byteArrayDict)
-		default:
-			debugf("skipping dictionary page of type %s\n", typ)
-			i.valReader = &nullReader{}
-		}
-	default:
-		debugf("skipping data page with encoding %s\n", enc)
-		i.valReader = &nullReader{}
-	}
-	return nil
-}
-
-// peekDL returns the definition level (DL) for the next value on this
-// page without advancing the iterator
-func (i *columnIterator) peekDL() (int32, error) {
-	if err := i.ensureDataPage(); err != nil {
-		return 0, err
-	}
-	if i.dlReader == nil {
-		return 0, nil
-	}
-	return int32(i.dlReader.peekInt64()), nil
-}
-
-// peekRL returns the repetition level (RL) for the next value on this
-// page without advancing the iterator
-func (i *columnIterator) peekRL() (int32, error) {
-	if err := i.ensureDataPage(); err != nil {
-		return 0, err
-	}
-	if i.rlReader == nil {
-		return 0, nil
-	}
-	return int32(i.rlReader.peekInt64()), nil
-}
-
-// advance counter, grab rl and dl.  (to keep everything consistent,
-// the caller should also advance valReader when this is called if the
-// dl value indicates a value is present.)
-func (i *columnIterator) commonNext() (int32, int32) {
-	if err := i.ensureDataPage(); err != nil {
-		if err == io.EOF {
-			return 0, 0
-		} else {
-			panic(err)
-		}
-	}
-	i.pageRead++
-
-	var rl, dl int32
-	if i.rlReader != nil {
-		rl = int32(i.rlReader.nextInt64())
-	}
-	if i.dlReader != nil {
-		dl = int32(i.dlReader.nextInt64())
-	}
-
-	if rl == i.maxRepetitionLevel {
-		i.groupRead++
-	}
-
-	return rl, dl
-}
-
-func (i *columnIterator) nextBoolean() (bool, int32, int32) {
-	rl, dl := i.commonNext()
-	var v bool
-	if dl == i.maxDefinitionLevel {
-		v = i.valReader.nextBoolean()
-	}
-	return v, rl, dl
-}
-
-func (i *columnIterator) nextInt32() (int32, int32, int32) {
-	rl, dl := i.commonNext()
-	var v int32
-	if dl == i.maxDefinitionLevel {
-		v = i.valReader.nextInt32()
-	}
-	return v, rl, dl
-}
-
-func (i *columnIterator) nextInt64() (int64, int32, int32) {
-	rl, dl := i.commonNext()
-	var v int64
-	if dl == i.maxDefinitionLevel {
-		v = i.valReader.nextInt64()
-	}
-	return v, rl, dl
-}
-
-func (i *columnIterator) nextFloat() (float64, int32, int32) {
-	rl, dl := i.commonNext()
-	var v float64
-	if dl == i.maxDefinitionLevel {
-		v = i.valReader.nextFloat()
-	}
-	return v, rl, dl
-}
-
-func (i *columnIterator) nextDouble() (float64, int32, int32) {
-	rl, dl := i.commonNext()
-	var v float64
-	if dl == i.maxDefinitionLevel {
-		v = i.valReader.nextDouble()
-	}
-	return v, rl, dl
-}
-
-func (i *columnIterator) nextByteArray() ([]byte, int32, int32) {
-	rl, dl := i.commonNext()
-	var v []byte
-	if dl == i.maxDefinitionLevel {
-		v = i.valReader.nextByteArray()
-	}
-	return v, rl, dl
-}
-
-// hybridReader decodes 64-bit inntegers encoded using the hybrid
-// RLE/bit-packing encoding described at:
-// https://github.com/apache/parquet-format/blob/master/Encodings.md#run-length-encoding--bit-packing-hybrid-rle--3
-type hybridReader struct {
-	buf   []byte
-	width int
-	vals  []int64
-}
-
-func newHybridReader(buf []byte, width int) *hybridReader {
-	return &hybridReader{buf, width, nil}
-}
-
-func makeMask(bits int) uint64 {
-	return uint64(1<<bits) - 1
-}
-
-func (r *hybridReader) fillVals() {
-	if len(r.vals) > 0 {
-		return
-	}
-	hdr, n := binary.Uvarint(r.buf)
-	if n == 0 {
-		panic("could not decode varint")
-	}
-	r.buf = r.buf[n:]
-	if hdr&1 == 0 {
-		// RLE encoded
-		raw := []byte{0, 0, 0, 0}
-		bytes := (r.width + 7) / 8
-		for i := 0; i < bytes; i++ {
-			raw[i] = r.buf[i]
-		}
-		val := int32(binary.LittleEndian.Uint32(raw))
-		r.buf = r.buf[bytes:]
-
-		n := int(hdr >> 1)
-		if cap(r.vals) >= n {
-			r.vals = r.vals[:n]
-		} else {
-			r.vals = make([]int64, n, n)
-		}
-		for i := 0; i < n; i++ {
-			r.vals[i] = int64(val)
-		}
-	} else {
-		// bit packed
-		groups := int(hdr >> 1)
-		n := groups * 8
-		if cap(r.vals) >= n {
-			r.vals = r.vals[:n]
-		} else {
-			r.vals = make([]int64, n, n)
-		}
-
-		mask := makeMask(r.width)
-		var iv uint64
-		havebits := 0
-
-		if len(r.buf) < (groups * r.width) {
-			panic(fmt.Sprintf("need %d bytes for packed but have %d", groups*r.width, len(r.buf)))
-		}
-		bi := 0
-		for i := 0; i < n; i++ {
-			for havebits < r.width {
-				iv |= uint64(r.buf[bi]) << havebits
-				havebits += 8
-				bi++
-			}
-			r.vals[i] = int64(iv & mask)
-			iv >>= r.width
-			havebits -= r.width
-		}
-		r.buf = r.buf[r.width*groups:]
-	}
-}
-
-func (r *hybridReader) peekInt64() int64 {
-	r.fillVals()
-	return r.vals[0]
-}
-
-func (r *hybridReader) nextInt64() int64 {
-	r.fillVals()
-	ret := r.vals[0]
-	r.vals = r.vals[1:]
-	return ret
-}
-
-// nullReader repeatedly returns a default value for some data type.
-// This is a development tool that allows us to "handle" unsupported
-// encodings without panicing or failing.  Eventually this should be
-// removed.
-type nullReader struct {
-}
-
-func (r *nullReader) nextBoolean() bool {
-	return false
-}
-
-func (r *nullReader) nextInt32() int32 {
-	return 0
-}
-
-func (r *nullReader) nextInt64() int64 {
-	return 0
-}
-
-func (r *nullReader) nextFloat() float64 {
-	return 0
-}
-
-func (r *nullReader) nextDouble() float64 {
-	return 0
-}
-
-func (r *nullReader) nextByteArray() []byte {
-	return nil
-}
-
-// plainBooleanReader decodes BOOLEAN-typed values encoded with the
-// parquet PLAIN encoding described at:
-// https://github.com/apache/parquet-format/blob/master/Encodings.md#plain-plain--0
-type plainBooleanReader struct {
-	buf     []byte
-	current byte
-	bits    int
-}
-
-func (r *plainBooleanReader) nextBoolean() bool {
-	if r.bits == 0 {
-		r.current = r.buf[0]
-		r.buf = r.buf[1:]
-		r.bits = 8
-	}
-	b := (r.current & 1) == 1
-	r.current >>= 1
-	r.bits -= 1
-	return b
-}
-
-func (r *plainBooleanReader) nextInt32() int32 {
-	panic("cannot read INT32 from PLAIN BOOLEAN reader")
-}
-
-func (r *plainBooleanReader) nextInt64() int64 {
-	panic("cannot read INT64 from PLAIN BOOLEAN reader")
-}
-
-func (r *plainBooleanReader) nextFloat() float64 {
-	panic("cannot read FLOAT from PLAIN BOOLEAN reader")
-}
-
-func (r *plainBooleanReader) nextDouble() float64 {
-	panic("cannot read DOUBLE from PLAIN BOOLEAN reader")
-}
-
-func (r *plainBooleanReader) nextByteArray() []byte {
-	panic("cannot read BYTE_ARRAY from PLAIN BOOLEAN reader")
-}
-
-// plainReader decodes values encoded with the parquet PLAIN encoding
-// for all types other than BOOLEAN.  PLAIN encoding is described at:
-// https://github.com/apache/parquet-format/blob/master/Encodings.md#plain-plain--0
-// Handle Parquet PLAIN encoding type
-type plainReader struct {
-	buf []byte
-}
-
-func (r *plainReader) nextBoolean() bool {
-	// panic("implement plain boolean reader")
-	return false
-}
-
-func (r *plainReader) nextInt32() int32 {
-	if len(r.buf) < 4 {
-		panic("underflow in PLAIN INT32")
-	}
-
-	ret := binary.LittleEndian.Uint32(r.buf[:4])
-	r.buf = r.buf[4:]
-	return int32(ret)
-}
-
-func (r *plainReader) nextInt64() int64 {
-	if len(r.buf) < 8 {
-		panic("underflow in PLAIN INT64")
-	}
-
-	ret := binary.LittleEndian.Uint64(r.buf[:8])
-	r.buf = r.buf[8:]
-	return int64(ret)
-}
-
-func (r *plainReader) nextFloat() float64 {
-	if len(r.buf) < 4 {
-		panic("underflow in PLAIN FLOAT")
-	}
-	v := binary.LittleEndian.Uint32(r.buf[:4])
-	r.buf = r.buf[4:]
-	return float64(math.Float32frombits(v))
-}
-
-func (r *plainReader) nextDouble() float64 {
-	if len(r.buf) < 8 {
-		panic("underflow in PLAIN DOUBLE")
-	}
-	v := binary.LittleEndian.Uint64(r.buf[:8])
-	r.buf = r.buf[8:]
-	return math.Float64frombits(v)
-}
-
-func (r *plainReader) nextByteArray() []byte {
-	if len(r.buf) < 4 {
-		panic("underflow in PLAIN BYTE_ARRAY")
-	}
-	ln := binary.LittleEndian.Uint32(r.buf[:4])
-	total := int(4 + ln)
-	if len(r.buf) < total {
-		panic("underflow in PLAIN BYTE_ARRAY")
-	}
-	ret := r.buf[4:total]
-	r.buf = r.buf[total:]
-	return ret
-}
-
-// dictionaryInt64Reader decodes INT64-typed values encoded with the
-// parquet PLAIN_DICTIONARY encoding described at:
-// https://github.com/apache/parquet-format/blob/master/Encodings.md#dictionary-encoding-plain_dictionary--2-and-rle_dictionary--8
-type dictionaryInt64Reader struct {
-	dict        []int64
-	indexReader *hybridReader
-}
-
-func newDictionaryInt64Reader(buf []byte, dict []int64) *dictionaryInt64Reader {
-	width := int(buf[0])
-	reader := newHybridReader(buf[1:], width)
-	return &dictionaryInt64Reader{dict, reader}
-}
-
-func (r *dictionaryInt64Reader) nextBoolean() bool {
-	panic("cannot read BOOLEAN from INT64 dictionary reader")
-}
-
-func (r *dictionaryInt64Reader) nextInt32() int32 {
-	panic("cannot read INT32 from INT64 dictionary reader")
-}
-
-func (r *dictionaryInt64Reader) nextInt64() int64 {
-	i := int(r.indexReader.nextInt64())
-	if i > len(r.dict) {
-		panic(fmt.Sprintf("dictionary index too large (%d>%d)", i, len(r.dict)))
-	}
-	return r.dict[i]
-}
-
-func (r *dictionaryInt64Reader) nextFloat() float64 {
-	panic("cannot read FLOAT from INT64 dictionary reader")
-}
-
-func (r *dictionaryInt64Reader) nextDouble() float64 {
-	panic("cannot read DOUBLE from INT64 dictionary reader")
-}
-
-func (r *dictionaryInt64Reader) nextByteArray() []byte {
-	panic("cannot read BYTE_ARRAY from INT64 dictionary reader")
-}
-
-// dictionaryDoubleReader decodes DOUBLE-typed values encoded with the
-// parquet PLAIN_DICTIONARY encoding described at:
-// https://github.com/apache/parquet-format/blob/master/Encodings.md#dictionary-encoding-plain_dictionary--2-and-rle_dictionary--8
-type dictionaryDoubleReader struct {
-	dict        []float64
-	indexReader *hybridReader
-}
-
-func newDictionaryDoubleReader(buf []byte, dict []float64) *dictionaryDoubleReader {
-	width := int(buf[0])
-	reader := newHybridReader(buf[1:], width)
-	return &dictionaryDoubleReader{dict, reader}
-}
-
-func (r *dictionaryDoubleReader) nextBoolean() bool {
-	panic("cannot read BOOLEAN from DOUBLE dictionary reader")
-}
-
-func (r *dictionaryDoubleReader) nextInt32() int32 {
-	panic("cannot read INT32 from DOUBLE dictionary reader")
-}
-
-func (r *dictionaryDoubleReader) nextInt64() int64 {
-	panic("cannot read INT64 from DOUBLE dictionary reader")
-}
-
-func (r *dictionaryDoubleReader) nextFloat() float64 {
-	panic("cannot read FLOAT from DOUBLE dictionary reader")
-}
-
-func (r *dictionaryDoubleReader) nextDouble() float64 {
-	i := int(r.indexReader.nextInt64())
-	if i > len(r.dict) {
-		panic(fmt.Sprintf("dictionary index too large (%d>%d)", i, len(r.dict)))
-	}
-	return r.dict[i]
-}
-
-func (r *dictionaryDoubleReader) nextByteArray() []byte {
-	panic("cannot read BYTE_ARRAY from DOUBLE dictionary reader")
-}
-
-// dictionaryByteArrayReader decodes BYTE_ARRAY-typed values encoded with the
-// parquet PLAIN_DICTIONARY encoding described at:
-// https://github.com/apache/parquet-format/blob/master/Encodings.md#dictionary-encoding-plain_dictionary--2-and-rle_dictionary--8
-type dictionaryByteArrayReader struct {
-	dict        [][]byte
-	indexReader *hybridReader
-}
-
-func newDictionaryByteArrayReader(buf []byte, dict [][]byte) *dictionaryByteArrayReader {
-	width := int(buf[0])
-	reader := newHybridReader(buf[1:], width)
-	return &dictionaryByteArrayReader{dict, reader}
-}
-
-func (r *dictionaryByteArrayReader) nextBoolean() bool {
-	panic("cannot read BOOLEAN from BYTE_ARRAY dictionary reader")
-}
-
-func (r *dictionaryByteArrayReader) nextInt32() int32 {
-	panic("cannot read INT32 from BYTE_ARRAY dictionary reader")
-}
-
-func (r *dictionaryByteArrayReader) nextInt64() int64 {
-	panic("cannot read INT64 from BYTE_ARRAY dictionary reader")
-}
-
-func (r *dictionaryByteArrayReader) nextFloat() float64 {
-	panic("cannot read FLOAT from BYTE_ARRAY dictionary reader")
-}
-
-func (r *dictionaryByteArrayReader) nextDouble() float64 {
-	panic("cannot read DOUBLE from BYTE_ARRAY dictionary reader")
-}
-
-func (r *dictionaryByteArrayReader) nextByteArray() []byte {
-	i := int(r.indexReader.nextInt64())
-	if i > len(r.dict) {
-		panic(fmt.Sprintf("dictionary index too large (%d>%d)", i, len(r.dict)))
-	}
-	return r.dict[i]
-}
diff --git a/zio/parquetio/reader.go b/zio/parquetio/reader.go
index 822dd5f6e..5509d1ac4 100644
--- a/zio/parquetio/reader.go
+++ b/zio/parquetio/reader.go
@@ -1,557 +1,51 @@
 package parquetio
 
 import (
-	"encoding/binary"
-	"fmt"
+	"errors"
 	"io"
 
-	"github.com/brimsec/zq/pkg/nano"
-	"github.com/brimsec/zq/zcode"
 	"github.com/brimsec/zq/zng"
 	"github.com/brimsec/zq/zng/resolver"
-
-	"github.com/apache/thrift/lib/go/thrift"
-	"github.com/xitongsys/parquet-go/parquet"
-	"github.com/xitongsys/parquet-go/source"
-)
-
-type HandledType int
-
-// These are all the types we can handle...
-const (
-	// un-annotated primitive types
-	boolean = iota
-	tint32
-	tint64
-	float
-	double
-	byteArray
-
-	// XXX
-	int96
-
-	// annotated strings
-	utf8
-	enum
-	json
-	bson
-
-	// annotated int64s
-	timestampMilliseconds
-	timestampMicroseconds
-	timestampNanoseconds
-
-	// XXX INTERVAL
-	// XXX INT_*, UINT_* types
-
-	// composite types
-	list
+	goparquet "github.com/fraugster/parquet-go"
 )
 
-func lookupPrimitiveType(typ *parquet.Type, cType *parquet.ConvertedType) (HandledType, error) {
-	if cType != nil {
-		switch *cType {
-		case parquet.ConvertedType_UTF8:
-			return utf8, nil
-		case parquet.ConvertedType_JSON:
-			return json, nil
-		case parquet.ConvertedType_BSON:
-			return bson, nil
-		case parquet.ConvertedType_ENUM:
-			return enum, nil
-		case parquet.ConvertedType_TIMESTAMP_MILLIS:
-			return timestampMilliseconds, nil
-		case parquet.ConvertedType_TIMESTAMP_MICROS:
-			return timestampMicroseconds, nil
-
-		// XXX case parquet.ConvertedType_INTERVAL:
-
-		default:
-			return -1, fmt.Errorf("cannot convert Ctype %s", *cType)
-		}
-
-		// XXX handle logical types
-	} else if typ != nil {
-		switch *typ {
-		case parquet.Type_BOOLEAN:
-			return boolean, nil
-		case parquet.Type_INT32:
-			return tint32, nil
-		case parquet.Type_INT64:
-			return tint64, nil
-		case parquet.Type_FLOAT:
-			return float, nil
-		case parquet.Type_DOUBLE:
-			return double, nil
-		case parquet.Type_BYTE_ARRAY:
-			return byteArray, nil
-		case parquet.Type_INT96:
-			return int96, nil
-		default:
-			return -1, fmt.Errorf("cannot convert type %s", *typ)
-		}
-	} else {
-		return -1, fmt.Errorf("cannot convert unknown type")
-	}
-}
-
-func simpleParquetTypeToZngType(typ HandledType) zng.Type {
-	switch typ {
-	case boolean:
-		return zng.TypeBool
-	case tint32:
-		return zng.TypeInt32
-	case tint64:
-		return zng.TypeInt64
-	case float, double:
-		return zng.TypeFloat64
-	case byteArray:
-		return zng.TypeBstring
-
-	case utf8, enum, json:
-		return zng.TypeString
-	case bson:
-		return zng.TypeBstring
-
-	case timestampMilliseconds, timestampMicroseconds, timestampNanoseconds:
-		return zng.TypeTime
-
-	// XXX
-	case int96:
-		return zng.TypeInt64
-	}
-
-	// This is only reachable in the event of a programming error.
-	panic(fmt.Sprintf("unhandled type %d", typ))
-}
-
-type ReaderOpts struct {
-	Columns                []string
-	IgnoreUnhandledColumns bool
-}
-
 type Reader struct {
-	file    source.ParquetFile
-	footer  *parquet.FileMetaData
-	typ     *zng.TypeRecord
-	columns []column
-	record  int
-	total   int
-	builder *zcode.Builder
-}
+	fr  *goparquet.FileReader
+	typ *zng.TypeRecord
 
-func NewReader(f source.ParquetFile, zctx *resolver.Context, opts ReaderOpts) (*Reader, error) {
-	reader := Reader{
-		file: f,
-	}
-	if err := reader.initialize(zctx, opts); err != nil {
-		return nil, err
-	}
-	return &reader, nil
+	builder builder
 }
 
-func (r *Reader) initialize(zctx *resolver.Context, opts ReaderOpts) error {
-	if err := r.readFooter(); err != nil {
-		return err
-	}
-
-	r.total = int(r.footer.GetNumRows())
-
-	if err := r.buildColumns(opts); err != nil {
-		return err
-	}
-
-	zcols := make([]zng.Column, len(r.columns))
-	for i, c := range r.columns {
-		zcols[i] = zng.Column{c.getName(), c.zngType(zctx)}
+func NewReader(r io.Reader, zctx *resolver.Context) (*Reader, error) {
+	rs, ok := r.(io.ReadSeeker)
+	if !ok {
+		return nil, errors.New("reader cannot seek")
 	}
-	var err error
-	r.typ, err = zctx.LookupTypeRecord(zcols)
+	fr, err := goparquet.NewFileReader(rs)
 	if err != nil {
-		return err
-	}
-
-	r.builder = zcode.NewBuilder()
-
-	return nil
-}
-
-func (r *Reader) readFooter() error {
-	// Per https://github.com/apache/parquet-format#file-format
-	// the last 4 bytes are the sequence "PAR1", the preceding 4
-	// bytes are the size of the metadata
-	var err error
-	buf := make([]byte, 4)
-	if _, err = r.file.Seek(-8, io.SeekEnd); err != nil {
-		return err
-	}
-	if _, err = r.file.Read(buf); err != nil {
-		return err
-	}
-	size := binary.LittleEndian.Uint32(buf)
-
-	if _, err = r.file.Seek(-(int64)(8+size), io.SeekEnd); err != nil {
-		return err
-	}
-
-	r.footer = parquet.NewFileMetaData()
-	pf := thrift.NewTCompactProtocolFactory()
-	protocol := pf.GetProtocol(thrift.NewStreamTransportR(r.file))
-	return r.footer.Read(protocol)
-}
-
-// column abstracts away the handling of an indvidual column from a
-// parquet file.  This interface currently has two concrete
-// implementations, one for columns that just hold primitive values
-// and one for columns that hold lists.
-type column interface {
-	zngType(zctx *resolver.Context) zng.Type
-	append(builder *zcode.Builder) error
-	getName() string
-}
-
-func (o *ReaderOpts) wantColumn(name string) bool {
-	if len(o.Columns) == 0 {
-		return true
-	}
-	for _, cname := range o.Columns {
-		if name == cname {
-			return true
-		}
-	}
-	return false
-}
-
-func (r *Reader) buildColumns(opts ReaderOpts) error {
-	schema := r.footer.Schema
-
-	// first element in the schema is the root, skip it.
-	// for each reamaining column, build a column iterator
-	// structure.
-	var columns []column
-	for i := 1; i < len(schema); {
-		n := 1
-		var col column
-		var err error
-		if schema[i].NumChildren != nil {
-			n, col, err = r.newNestedColumn(schema, i)
-		} else {
-			col, err = r.newSimpleColumn(*schema[i])
-		}
-		i += n
-		if err != nil {
-			return err
-		}
-
-		if col == nil {
-			if opts.IgnoreUnhandledColumns {
-				continue
-			}
-			return fmt.Errorf("cannot handle column %s", col.getName())
-		}
-
-		if opts.wantColumn(col.getName()) {
-			columns = append(columns, col)
-		}
-	}
-
-	r.columns = columns
-	return nil
-}
-
-func (r *Reader) newSimpleColumn(el parquet.SchemaElement) (column, error) {
-	if el.RepetitionType != nil && *el.RepetitionType == parquet.FieldRepetitionType_REPEATED {
-		return nil, fmt.Errorf("cannot convert repeated element %s", el.Name)
+		return nil, err
 	}
-
-	typ, err := lookupPrimitiveType(el.Type, el.ConvertedType)
+	typ, err := newRecordType(zctx, fr.GetSchemaDefinition().RootColumn.Children)
 	if err != nil {
 		return nil, err
 	}
-
-	var maxDefinition int32 = 0
-	if el.RepetitionType != nil && *el.RepetitionType == parquet.FieldRepetitionType_OPTIONAL {
-		maxDefinition = 1
-	}
-
-	iter := newColumnIterator(el.Name, r.footer, r.file, 0, maxDefinition)
-	return &simpleColumn{
-		name:          el.Name,
-		typ:           typ,
-		iter:          iter,
-		maxDefinition: maxDefinition,
+	return &Reader{
+		fr:  fr,
+		typ: typ,
 	}, nil
 }
 
-// Given a schema element with child elements, recursively count its
-// total number of descendents.  This is only used for skipping over
-// columnns with an unrecognized structure.
-func countChildren(els []*parquet.SchemaElement, i int) int {
-	if i >= len(els) {
-		return -1
-	}
-	if els[i].NumChildren == nil {
-		return 1
-	}
-
-	n := int(*(els[i].NumChildren))
-	j := i + 1
-	for c := 0; c < n; c++ {
-		cc := countChildren(els, j)
-		if cc == -1 {
-			return -1
-		}
-		j += cc
-	}
-	return j - i
-}
-
-func (r *Reader) newNestedColumn(els []*parquet.SchemaElement, i int) (int, column, error) {
-	el := els[i]
-	if el.ConvertedType != nil && *el.ConvertedType == parquet.ConvertedType_LIST {
-		return r.newListColumn(els, i)
-	}
-	if el.LogicalType != nil && el.LogicalType.LIST != nil {
-		return r.newListColumn(els, i)
-	}
-
-	// Skip this element and all its children...
-	return countChildren(els, i), nil, nil
-}
-
-func (r *Reader) newListColumn(els []*parquet.SchemaElement, i int) (int, column, error) {
-	// Per https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#lists
-	// List structure is:
-	// <list-repetition> group <name> (LIST) {
-	//   repeated group list {
-	//     <element-repetition> <element-type> element;
-	//   }
-	// }
-	//
-	// First sanity check that we're looking at something with that
-	// structure.
-
-	el := els[i]
-	if len(els) < i+2 {
-		return 1, nil, fmt.Errorf("not enough nested elements for LIST %s", el.Name)
-	}
-
-	if el.RepetitionType == nil || *el.RepetitionType == parquet.FieldRepetitionType_REPEATED {
-		return 1, nil, fmt.Errorf("list (field %s) must not be repeated", el.Name)
-	}
-	if el.NumChildren == nil || *el.NumChildren != 1 {
-		return 1, nil, fmt.Errorf("LIST element (%s) should have 1 child", el.Name)
-	}
-
-	listEl := els[i+1]
-	if listEl.RepetitionType == nil || *listEl.RepetitionType != parquet.FieldRepetitionType_REPEATED {
-		return 1, nil, fmt.Errorf("list (field %s) must not be repeated", el.Name)
-	}
-	if listEl.NumChildren == nil || *listEl.NumChildren != 1 {
-		return 1, nil, fmt.Errorf("LIST element (%s) should have 1 child", el.Name)
-	}
-
-	typeEl := els[i+2]
-	typ, err := lookupPrimitiveType(typeEl.Type, typeEl.ConvertedType)
-	if err != nil {
-		return 3, nil, err
-	}
-
-	// This is something we can handle.  The column name correponds
-	// to the outer element (el), but the actual values are kept in
-	// the innermost nested element (typeEl).
-	iter := newColumnIterator(el.Name, r.footer, r.file, 1, 2)
-
-	c := listColumn{
-		name:          el.Name,
-		innerType:     typ,
-		iter:          iter,
-		maxDefinition: 2,
-	}
-
-	return 3, &c, nil
-}
-
-// Read one primitive value from a column iterator and append it to the
-// given zcode.Builder.  This is essentially the complete implemenntation
-// of append() for a non-repeated column, and is used inside a loop for
-// LIST-valued columns.
-func appendItem(builder *zcode.Builder, typ HandledType, iter *columnIterator, maxDef int32) error {
-	var dl int32
-	switch typ {
-	case boolean:
-		var b bool
-		b, _, dl = iter.nextBoolean()
-		if maxDef > dl {
-			builder.AppendPrimitive(nil)
-		} else {
-			builder.AppendPrimitive(zng.EncodeBool(b))
-		}
-	case tint32:
-		var i int32
-		i, _, dl = iter.nextInt32()
-		if maxDef > dl {
-			builder.AppendPrimitive(nil)
-		} else {
-			builder.AppendPrimitive(zng.EncodeInt(int64(i)))
-		}
-	case tint64:
-		var i int64
-		i, _, dl = iter.nextInt64()
-		if maxDef > dl {
-			builder.AppendPrimitive(nil)
-		} else {
-			builder.AppendPrimitive(zng.EncodeInt(i))
-		}
-	case float:
-		var f float64
-		f, _, dl = iter.nextFloat()
-		if maxDef > dl {
-			builder.AppendPrimitive(nil)
-		} else {
-			builder.AppendPrimitive(zng.EncodeFloat64(f))
-		}
-	case double:
-		var f float64
-		f, _, dl = iter.nextDouble()
-		if maxDef > dl {
-			builder.AppendPrimitive(nil)
-		} else {
-			builder.AppendPrimitive(zng.EncodeFloat64(f))
-		}
-	case utf8, enum, json:
-		var a []byte
-		a, _, dl = iter.nextByteArray()
-		if maxDef > dl {
-			builder.AppendPrimitive(nil)
-		} else {
-			builder.AppendPrimitive(zng.EncodeString(string(a)))
-		}
-	case byteArray, bson:
-		var a []byte
-		a, _, dl = iter.nextByteArray()
-		if maxDef > dl {
-			builder.AppendPrimitive(nil)
-		} else {
-			builder.AppendPrimitive(zng.EncodeString(string(a)))
-		}
-	case timestampMilliseconds, timestampMicroseconds, timestampNanoseconds:
-		var i int64
-		i, _, dl = iter.nextInt64()
-		if maxDef > dl {
-			builder.AppendPrimitive(nil)
-		} else {
-			var ts nano.Ts
-			switch typ {
-			case timestampMilliseconds:
-				ts = nano.Ts(i * 1000_000)
-			case timestampMicroseconds:
-				ts = nano.Ts(i * 1000)
-			case timestampNanoseconds:
-				ts = nano.Ts(i)
-			}
-			builder.AppendPrimitive(zng.EncodeTime(ts))
-		}
-	default:
-		return fmt.Errorf("unhandled type %d", typ)
-	}
-	return nil
-}
-
-// simpleColumn handles a column from a parquet file that holds individual
-// (non-repeated) primitive values.
-type simpleColumn struct {
-	name          string
-	typ           HandledType
-	iter          *columnIterator
-	maxDefinition int32
-}
-
-func (c *simpleColumn) getName() string { return c.name }
-
-func (c *simpleColumn) zngType(zctx *resolver.Context) zng.Type {
-	return simpleParquetTypeToZngType(c.typ)
-}
-
-// append reads the next value from this column and appends it to the
-// given zcode.Builder.  This code represents an unwound and vastly
-// simplified version of the code in the methods:
-// parquet-go.reader.ParquetReader.read(), and
-// parquet-go.marshal.Unmarshal()
-func (c *simpleColumn) append(builder *zcode.Builder) error {
-	// For simple values, the max definition level is exactly 1
-	return appendItem(builder, c.typ, c.iter, c.maxDefinition)
-}
-
-// listColumn handles a column from a parquet file that holds LIST
-// structures as defined in the parquet spec.
-type listColumn struct {
-	name      string
-	innerType HandledType
-
-	iter          *columnIterator
-	maxDefinition int32
-}
-
-func (c *listColumn) getName() string { return c.name }
-
-func (c *listColumn) zngType(zctx *resolver.Context) zng.Type {
-	inner := simpleParquetTypeToZngType(c.innerType)
-	return zctx.LookupTypeArray(inner)
-}
-
-// append reads the next value from this column and appends it to the given
-// zcode.Builder.  This code (together with the readNext() method represent
-// an unwound and vastly simplified version of the code in the methods:
-// parquet-go.reader.ParquetReader.read(), and
-// parquet-go.marshal.Unmarshal()
-func (c *listColumn) append(builder *zcode.Builder) error {
-	dl, err := c.iter.peekDL()
+func (r *Reader) Read() (*zng.Record, error) {
+	data, err := r.fr.NextRow()
 	if err != nil {
-		return err
-	}
-	if c.maxDefinition > dl {
-		builder.AppendContainer(nil)
-		return nil
-	}
-
-	builder.BeginContainer()
-	first := true
-	for {
-		rl, err := c.iter.peekRL()
-		if err != nil {
-			if err == io.EOF {
-				break
-			} else {
-				return err
-			}
-		}
-		if first {
-			first = false
-		} else {
-			if rl == 0 {
-				break
-			}
+		if err == io.EOF {
+			return nil, nil
 		}
-		if err := appendItem(builder, c.innerType, c.iter, c.maxDefinition); err != nil {
-			return err
-		}
-	}
-	builder.EndContainer()
-	return nil
-}
-
-func (r *Reader) Read() (*zng.Record, error) {
-	if r.record == r.total {
-		return nil, nil
+		return nil, err
 	}
-	r.record++
-
 	r.builder.Reset()
-	for _, c := range r.columns {
-		if err := c.append(r.builder); err != nil {
-			return nil, err
-		}
+	for _, c := range r.typ.Columns {
+		r.builder.appendValue(c.Type, data[c.Name])
 	}
 	return zng.NewRecord(r.typ, r.builder.Bytes()), nil
 }
diff --git a/zio/parquetio/schemadefinition.go b/zio/parquetio/schemadefinition.go
new file mode 100644
index 000000000..c440b5e43
--- /dev/null
+++ b/zio/parquetio/schemadefinition.go
@@ -0,0 +1,274 @@
+package parquetio
+
+import (
+	"errors"
+	"fmt"
+	"math"
+
+	"github.com/brimsec/zq/zng"
+	"github.com/fraugster/parquet-go/parquet"
+	"github.com/fraugster/parquet-go/parquetschema"
+)
+
+var (
+	ErrEmptyRecordType = errors.New("empty record type unsupported")
+	ErrNullType        = errors.New("null type unimplemented")
+	ErrUnionType       = errors.New("union type unsupported")
+)
+
+var (
+	repetitionRequired = parquet.FieldRepetitionTypePtr(parquet.FieldRepetitionType_REQUIRED)
+	repetitionOptional = parquet.FieldRepetitionTypePtr(parquet.FieldRepetitionType_OPTIONAL)
+	repetitionRepeated = parquet.FieldRepetitionTypePtr(parquet.FieldRepetitionType_REPEATED)
+
+	convertedUTF8            = parquet.ConvertedTypePtr(parquet.ConvertedType_UTF8)
+	convertedMap             = parquet.ConvertedTypePtr(parquet.ConvertedType_MAP)
+	convertedMapKeyValue     = parquet.ConvertedTypePtr(parquet.ConvertedType_MAP_KEY_VALUE)
+	convertedList            = parquet.ConvertedTypePtr(parquet.ConvertedType_LIST)
+	convertedEnum            = parquet.ConvertedTypePtr(parquet.ConvertedType_ENUM)
+	convertedDate            = parquet.ConvertedTypePtr(parquet.ConvertedType_DATE)
+	convertedTimeMillis      = parquet.ConvertedTypePtr(parquet.ConvertedType_TIME_MILLIS)
+	convertedTimeMicros      = parquet.ConvertedTypePtr(parquet.ConvertedType_TIME_MICROS)
+	convertedTimestampMillis = parquet.ConvertedTypePtr(parquet.ConvertedType_TIMESTAMP_MILLIS)
+	convertedTimestampMicros = parquet.ConvertedTypePtr(parquet.ConvertedType_TIMESTAMP_MICROS)
+	convertedUint8           = parquet.ConvertedTypePtr(parquet.ConvertedType_UINT_8)
+	convertedUint16          = parquet.ConvertedTypePtr(parquet.ConvertedType_UINT_16)
+	convertedUint32          = parquet.ConvertedTypePtr(parquet.ConvertedType_UINT_32)
+	convertedUint64          = parquet.ConvertedTypePtr(parquet.ConvertedType_UINT_64)
+	convertedInt8            = parquet.ConvertedTypePtr(parquet.ConvertedType_INT_8)
+	convertedInt16           = parquet.ConvertedTypePtr(parquet.ConvertedType_INT_16)
+	convertedInt32           = parquet.ConvertedTypePtr(parquet.ConvertedType_INT_32)
+	convertedInt64           = parquet.ConvertedTypePtr(parquet.ConvertedType_INT_64)
+	convertedJSON            = parquet.ConvertedTypePtr(parquet.ConvertedType_JSON)
+	convertedBSON            = parquet.ConvertedTypePtr(parquet.ConvertedType_BSON)
+	convertedInterval        = parquet.ConvertedTypePtr(parquet.ConvertedType_INTERVAL)
+
+	logicalString          = &parquet.LogicalType{STRING: &parquet.StringType{}}
+	logicalMap             = &parquet.LogicalType{MAP: &parquet.MapType{}}
+	logicalList            = &parquet.LogicalType{LIST: &parquet.ListType{}}
+	logicalEnum            = &parquet.LogicalType{ENUM: &parquet.EnumType{}}
+	logicalDate            = &parquet.LogicalType{DATE: &parquet.DateType{}}
+	logicalTimeMillis      = &parquet.LogicalType{TIME: &parquet.TimeType{Unit: timeUnitMillis}}
+	logicalTimeMicros      = &parquet.LogicalType{TIME: &parquet.TimeType{Unit: timeUnitMicros}}
+	logicalTimeNanos       = &parquet.LogicalType{TIME: &parquet.TimeType{Unit: timeUnitNanos}}
+	logicalTimestampMillis = &parquet.LogicalType{TIMESTAMP: &parquet.TimestampType{Unit: timeUnitMillis}}
+	logicalTimestampMicros = &parquet.LogicalType{TIMESTAMP: &parquet.TimestampType{Unit: timeUnitMicros}}
+	logicalTimestampNanos  = &parquet.LogicalType{TIMESTAMP: &parquet.TimestampType{Unit: timeUnitNanos}}
+	logicalUint8           = &parquet.LogicalType{INTEGER: &parquet.IntType{BitWidth: 8}}
+	logicalUint16          = &parquet.LogicalType{INTEGER: &parquet.IntType{BitWidth: 16}}
+	logicalUint32          = &parquet.LogicalType{INTEGER: &parquet.IntType{BitWidth: 32}}
+	logicalUint64          = &parquet.LogicalType{INTEGER: &parquet.IntType{BitWidth: 64}}
+	logicalInt8            = &parquet.LogicalType{INTEGER: &parquet.IntType{BitWidth: 8, IsSigned: true}}
+	logicalInt16           = &parquet.LogicalType{INTEGER: &parquet.IntType{BitWidth: 16, IsSigned: true}}
+	logicalInt32           = &parquet.LogicalType{INTEGER: &parquet.IntType{BitWidth: 32, IsSigned: true}}
+	logicalInt64           = &parquet.LogicalType{INTEGER: &parquet.IntType{BitWidth: 64, IsSigned: true}}
+	logicalBSON            = &parquet.LogicalType{BSON: &parquet.BsonType{}}
+	logicalJSON            = &parquet.LogicalType{JSON: &parquet.JsonType{}}
+	logicalUUID            = &parquet.LogicalType{UUID: &parquet.UUIDType{}}
+
+	timeUnitMillis = &parquet.TimeUnit{MILLIS: &parquet.MilliSeconds{}}
+	timeUnitMicros = &parquet.TimeUnit{MICROS: &parquet.MicroSeconds{}}
+	timeUnitNanos  = &parquet.TimeUnit{NANOS: &parquet.NanoSeconds{}}
+)
+
+func newSchemaDefinition(typ *zng.TypeRecord) (*parquetschema.SchemaDefinition, error) {
+	c, err := newColumnDefinition("", typ)
+	if err != nil {
+		return nil, err
+	}
+	s := &parquetschema.SchemaDefinition{
+		RootColumn: &parquetschema.ColumnDefinition{
+			Children: c.Children,
+			SchemaElement: &parquet.SchemaElement{
+				Name: "zq",
+			},
+		},
+	}
+	return s, s.ValidateStrict()
+}
+
+func newColumnDefinition(name string, typ zng.Type) (*parquetschema.ColumnDefinition, error) {
+	switch typ := typ.(type) {
+	case *zng.TypeAlias:
+		switch id := typ.Type.ID(); {
+		case typ.Name == "date" && id == zng.IdInt32:
+			return newPrimitiveColumnDefinition(name, parquet.Type_INT32, convertedDate, logicalDate)
+		case typ.Name == "bson" && id == zng.IdBytes:
+			return newPrimitiveColumnDefinition(name, parquet.Type_BYTE_ARRAY, convertedBSON, logicalBSON)
+		case typ.Name == "interval" && id == zng.IdBytes:
+			return newPrimitiveColumnDefinition(name, parquet.Type_BYTE_ARRAY, convertedInterval, nil)
+		case typ.Name == "json" && id == zng.IdString:
+			return newPrimitiveColumnDefinition(name, parquet.Type_BYTE_ARRAY, convertedJSON, logicalJSON)
+		case typ.Name == "enum" && id == zng.IdString:
+			return newColumnDefinition(name, &zng.TypeEnum{})
+		case typ.Name == "float" && id == zng.IdFloat64:
+			return newPrimitiveColumnDefinition(name, parquet.Type_FLOAT, nil, nil)
+		case typ.Name == "int96" && id == zng.IdBytes:
+			return newPrimitiveColumnDefinition(name, parquet.Type_INT96, nil, nil)
+		case typ.Name == "time_millis" && id == zng.IdInt32:
+			return newPrimitiveColumnDefinition(
+				name, parquet.Type_INT32, convertedTimeMillis, logicalTimeMillis)
+		case name == "time_micros" && id == zng.IdInt64:
+			return newPrimitiveColumnDefinition(
+				name, parquet.Type_INT64, convertedTimeMicros, logicalTimeMicros)
+		case name == "time_nanos" && id == zng.IdInt64:
+			return newPrimitiveColumnDefinition(name, parquet.Type_INT64, nil, logicalTimeNanos)
+		case name == "timestamp_millis" && id == zng.IdInt64:
+			return newPrimitiveColumnDefinition(
+				name, parquet.Type_INT64, convertedTimestampMillis, logicalTimestampMillis)
+		case name == "timestamp_micros" && id == zng.IdInt64:
+			return newPrimitiveColumnDefinition(
+				name, parquet.Type_INT64, convertedTimestampMicros, logicalTimestampMicros)
+		case name == "uuid" && id == zng.IdBytes:
+			return newPrimitiveColumnDefinition(name, parquet.Type_BYTE_ARRAY, nil, logicalUUID)
+		}
+		return newColumnDefinition(name, typ.Type)
+	case *zng.TypeOfUint8:
+		return newPrimitiveColumnDefinition(name, parquet.Type_INT32, convertedUint8, logicalUint8)
+	case *zng.TypeOfUint16:
+		return newPrimitiveColumnDefinition(name, parquet.Type_INT32, convertedUint16, logicalUint16)
+	case *zng.TypeOfUint32:
+		return newPrimitiveColumnDefinition(name, parquet.Type_INT32, convertedUint32, logicalUint32)
+	case *zng.TypeOfUint64:
+		return newPrimitiveColumnDefinition(name, parquet.Type_INT64, convertedUint64, logicalUint64)
+	case *zng.TypeOfInt8:
+		return newPrimitiveColumnDefinition(name, parquet.Type_INT32, convertedInt8, logicalInt8)
+	case *zng.TypeOfInt16:
+		return newPrimitiveColumnDefinition(name, parquet.Type_INT32, convertedInt16, logicalInt16)
+	case *zng.TypeOfInt32:
+		return newPrimitiveColumnDefinition(name, parquet.Type_INT32, convertedInt32, logicalInt32)
+	case *zng.TypeOfInt64, *zng.TypeOfDuration:
+		return newPrimitiveColumnDefinition(name, parquet.Type_INT64, convertedInt64, logicalInt64)
+	case *zng.TypeOfTime:
+		return newPrimitiveColumnDefinition(name, parquet.Type_INT64, nil, logicalTimestampNanos)
+	// XXX add TypeFloat16
+	// XXX add TypeFloat32
+	case *zng.TypeOfFloat64:
+		return newPrimitiveColumnDefinition(name, parquet.Type_DOUBLE, nil, nil)
+	// XXX add TypeDecimal
+	case *zng.TypeOfBool:
+		return newPrimitiveColumnDefinition(name, parquet.Type_BOOLEAN, nil, nil)
+	case *zng.TypeOfBytes, *zng.TypeOfBstring:
+		return newPrimitiveColumnDefinition(name, parquet.Type_BYTE_ARRAY, nil, nil)
+	case *zng.TypeOfString, *zng.TypeOfIP, *zng.TypeOfNet, *zng.TypeOfType, *zng.TypeOfError:
+		return newPrimitiveColumnDefinition(name, parquet.Type_BYTE_ARRAY, convertedUTF8, logicalString)
+	case *zng.TypeOfNull:
+		return nil, ErrNullType
+	case *zng.TypeRecord:
+		return newRecordColumnDefinition(name, typ)
+	case *zng.TypeArray:
+		return newListColumnDefinition(name, typ.Type)
+	case *zng.TypeSet:
+		return newListColumnDefinition(name, typ.Type)
+	case *zng.TypeUnion:
+		return nil, ErrUnionType
+	case *zng.TypeEnum:
+		return newPrimitiveColumnDefinition(name, parquet.Type_BYTE_ARRAY, convertedEnum, logicalEnum)
+	case *zng.TypeMap:
+		return newMapColumnDefinition(name, typ.KeyType, typ.ValType)
+	default:
+		panic(fmt.Sprintf("unknown type %T", typ))
+	}
+}
+
+func newPrimitiveColumnDefinition(name string, t parquet.Type, c *parquet.ConvertedType, l *parquet.LogicalType) (*parquetschema.ColumnDefinition, error) {
+	return &parquetschema.ColumnDefinition{
+		SchemaElement: &parquet.SchemaElement{
+			Type:           parquet.TypePtr(t),
+			RepetitionType: repetitionOptional,
+			Name:           name,
+			ConvertedType:  c,
+			LogicalType:    l,
+		},
+	}, nil
+}
+
+func newListColumnDefinition(name string, typ zng.Type) (*parquetschema.ColumnDefinition, error) {
+	element, err := newColumnDefinition("element", typ)
+	if err != nil {
+		return nil, err
+	}
+	return &parquetschema.ColumnDefinition{
+		Children: []*parquetschema.ColumnDefinition{
+			{
+				Children: []*parquetschema.ColumnDefinition{element},
+				SchemaElement: &parquet.SchemaElement{
+					RepetitionType: repetitionRepeated,
+					Name:           "list",
+					NumChildren:    int32Ptr(1),
+				},
+			},
+		},
+		SchemaElement: &parquet.SchemaElement{
+			RepetitionType: repetitionOptional,
+			Name:           name,
+			NumChildren:    int32Ptr(1),
+			ConvertedType:  convertedList,
+			LogicalType:    logicalList,
+		},
+	}, nil
+}
+
+func newMapColumnDefinition(name string, keyType, valueType zng.Type) (*parquetschema.ColumnDefinition, error) {
+	key, err := newColumnDefinition("key", keyType)
+	if err != nil {
+		return nil, err
+	}
+	key.SchemaElement.RepetitionType = repetitionRequired
+	value, err := newColumnDefinition("value", valueType)
+	if err != nil {
+		return nil, err
+	}
+	value.SchemaElement.RepetitionType = repetitionRequired
+	// xxx maybe set key.RepetitionType and value.RepetitionType to repeated
+	return &parquetschema.ColumnDefinition{
+		Children: []*parquetschema.ColumnDefinition{
+			{
+				Children: []*parquetschema.ColumnDefinition{key, value},
+				SchemaElement: &parquet.SchemaElement{
+					RepetitionType: repetitionRepeated,
+					Name:           "key_value",
+					NumChildren:    int32Ptr(2),
+					ConvertedType:  convertedMapKeyValue,
+				},
+			},
+		},
+		SchemaElement: &parquet.SchemaElement{
+			RepetitionType: repetitionOptional,
+			Name:           name,
+			NumChildren:    int32Ptr(1),
+			ConvertedType:  convertedMap,
+			LogicalType:    logicalMap,
+		},
+	}, nil
+}
+
+func newRecordColumnDefinition(name string, typ *zng.TypeRecord) (*parquetschema.ColumnDefinition, error) {
+	if len(typ.Columns) == 0 {
+		return nil, ErrEmptyRecordType
+	}
+	var children []*parquetschema.ColumnDefinition
+	for _, c := range typ.Columns {
+		c, err := newColumnDefinition(c.Name, c.Type)
+		if err != nil {
+			return nil, err
+		}
+		children = append(children, c)
+	}
+	return &parquetschema.ColumnDefinition{
+		Children: children,
+		SchemaElement: &parquet.SchemaElement{
+			RepetitionType: repetitionOptional,
+			Name:           name,
+			NumChildren:    int32Ptr(len(children)),
+		},
+	}, nil
+}
+
+func int32Ptr(i int) *int32 {
+	if i > math.MaxInt32 || i < math.MinInt32 {
+		panic(i)
+	}
+	i32 := int32(i)
+	return &i32
+}
diff --git a/zio/parquetio/type.go b/zio/parquetio/type.go
new file mode 100644
index 000000000..139d8e8e6
--- /dev/null
+++ b/zio/parquetio/type.go
@@ -0,0 +1,196 @@
+package parquetio
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/brimsec/zq/zng"
+	"github.com/brimsec/zq/zng/resolver"
+	"github.com/fraugster/parquet-go/parquet"
+	"github.com/fraugster/parquet-go/parquetschema"
+)
+
+func newRecordType(zctx *resolver.Context, children []*parquetschema.ColumnDefinition) (*zng.TypeRecord, error) {
+	var cols []zng.Column
+	for _, c := range children {
+		typ, err := newType(zctx, c)
+		if err != nil {
+			return nil, fmt.Errorf("%s: %w", c.SchemaElement.Name, err)
+		}
+		cols = append(cols, zng.Column{
+			Name: c.SchemaElement.Name,
+			Type: typ,
+		})
+	}
+	return zctx.LookupTypeRecord(cols)
+}
+
+func newType(zctx *resolver.Context, cd *parquetschema.ColumnDefinition) (zng.Type, error) {
+	se := cd.SchemaElement
+	if se.Type != nil {
+		return newPrimitiveType(zctx, se)
+	}
+	if se.ConvertedType != nil {
+		switch *se.ConvertedType {
+		case parquet.ConvertedType_MAP:
+			keyType, err := newType(zctx, cd.Children[0].Children[0])
+			if err != nil {
+				return nil, fmt.Errorf("%s: map key: %w", cd.SchemaElement.Name, err)
+			}
+			valType, err := newType(zctx, cd.Children[0].Children[1])
+			if err != nil {
+				return nil, fmt.Errorf("%s: map value: %w", cd.SchemaElement.Name, err)
+			}
+			return zctx.LookupTypeMap(keyType, valType), nil
+
+		case parquet.ConvertedType_LIST:
+			typ, err := newType(zctx, cd.Children[0].Children[0])
+			if err != nil {
+				return nil, err
+			}
+			return zctx.LookupTypeArray(typ), nil
+		}
+	}
+	return newRecordType(zctx, cd.Children)
+
+}
+
+func newPrimitiveType(zctx *resolver.Context, s *parquet.SchemaElement) (zng.Type, error) {
+	if s.IsSetLogicalType() && s.LogicalType.IsSetDECIMAL() ||
+		s.GetConvertedType() == parquet.ConvertedType_DECIMAL {
+		return nil, errors.New("DECIMAL type is unimplemented")
+	}
+	switch *s.Type {
+	case parquet.Type_BOOLEAN:
+		return zng.TypeBool, nil
+	case parquet.Type_INT32:
+		if s.IsSetLogicalType() {
+			switch l := s.LogicalType; {
+			case l.IsSetDATE():
+				zctx.LookupTypeAlias("date", zng.TypeInt32)
+			case l.IsSetINTEGER():
+				switch i := l.INTEGER; {
+				case i.BitWidth == 8 && i.IsSigned:
+					return zng.TypeInt8, nil
+				case i.BitWidth == 8:
+					return zng.TypeUint8, nil
+				case i.BitWidth == 16 && i.IsSigned:
+					return zng.TypeInt16, nil
+				case i.BitWidth == 16:
+					return zng.TypeUint16, nil
+				case i.BitWidth == 32 && i.IsSigned:
+					return zng.TypeInt32, nil
+				case i.BitWidth == 32:
+					return zng.TypeUint32, nil
+				}
+			case l.IsSetTIME() && l.TIME.IsSetUnit() && l.TIME.Unit.IsSetMILLIS():
+				return zctx.LookupTypeAlias("time_millis", zng.TypeInt32)
+			}
+		}
+		if s.IsSetConvertedType() {
+			switch *s.ConvertedType {
+			case parquet.ConvertedType_DATE:
+				return zctx.LookupTypeAlias("date", zng.TypeInt32)
+			case parquet.ConvertedType_UINT_8:
+				return zng.TypeUint8, nil
+			case parquet.ConvertedType_UINT_16:
+				return zng.TypeUint16, nil
+			case parquet.ConvertedType_UINT_32:
+				return zng.TypeUint32, nil
+			case parquet.ConvertedType_INT_8:
+				return zng.TypeInt8, nil
+			case parquet.ConvertedType_INT_16:
+				return zng.TypeInt16, nil
+			case parquet.ConvertedType_INT_32:
+				return zng.TypeInt32, nil
+			case parquet.ConvertedType_TIME_MILLIS:
+				return zctx.LookupTypeAlias("time_millis", zng.TypeInt32)
+			}
+		}
+		return zng.TypeInt32, nil
+	case parquet.Type_INT64:
+		if s.IsSetLogicalType() {
+			switch l := s.LogicalType; {
+			case l.IsSetINTEGER():
+				switch {
+				case l.INTEGER.BitWidth == 64 && l.INTEGER.IsSigned:
+					return zng.TypeInt64, nil
+				case l.INTEGER.BitWidth == 64:
+					return zng.TypeUint64, nil
+				}
+			case l.IsSetTIME() && l.TIME.IsSetUnit():
+				switch {
+				case l.TIME.Unit.IsSetMICROS():
+					return zctx.LookupTypeAlias("time_micros", zng.TypeInt64)
+				case l.TIME.Unit.IsSetNANOS():
+					return zctx.LookupTypeAlias("time_nanos", zng.TypeInt64)
+				}
+			case l.IsSetTIMESTAMP() && l.TIMESTAMP.IsSetUnit():
+				switch {
+				case l.TIMESTAMP.Unit.IsSetMILLIS():
+					return zctx.LookupTypeAlias("timestamp_millis", zng.TypeInt64)
+				case l.TIMESTAMP.Unit.IsSetMICROS():
+					return zctx.LookupTypeAlias("timestamp_micros", zng.TypeInt64)
+				case l.TIMESTAMP.Unit.IsSetNANOS():
+					return zng.TypeTime, nil
+				}
+			}
+		}
+		if s.IsSetConvertedType() {
+			switch *s.ConvertedType {
+			case parquet.ConvertedType_UINT_64:
+				return zng.TypeUint64, nil
+			case parquet.ConvertedType_INT_64:
+				return zng.TypeInt64, nil
+			case parquet.ConvertedType_TIME_MICROS:
+				return zctx.LookupTypeAlias("time_micros", zng.TypeInt64)
+			case parquet.ConvertedType_TIMESTAMP_MILLIS:
+				return zctx.LookupTypeAlias("timestamp_millis", zng.TypeInt32)
+			case parquet.ConvertedType_TIMESTAMP_MICROS:
+				return zctx.LookupTypeAlias("timestamp_micros", zng.TypeInt64)
+			}
+		}
+		return zng.TypeInt64, nil
+	case parquet.Type_INT96:
+		return zctx.LookupTypeAlias("int96", zng.TypeBytes)
+	case parquet.Type_FLOAT:
+		return zctx.LookupTypeAlias("float", zng.TypeFloat64)
+	case parquet.Type_DOUBLE:
+		return zng.TypeFloat64, nil
+	case parquet.Type_BYTE_ARRAY:
+		if s.IsSetLogicalType() {
+			switch l := s.LogicalType; {
+			case l.IsSetBSON():
+				return zctx.LookupTypeAlias("bson", zng.TypeBytes)
+			case l.IsSetENUM():
+				return zctx.LookupTypeAlias("enum", zng.TypeString)
+			case l.IsSetJSON():
+				return zctx.LookupTypeAlias("json", zng.TypeString)
+			case l.IsSetSTRING():
+				return zng.TypeString, nil
+			}
+		}
+		if s.IsSetConvertedType() {
+			switch *s.ConvertedType {
+			case parquet.ConvertedType_BSON:
+				return zctx.LookupTypeAlias("bson", zng.TypeBytes)
+			case parquet.ConvertedType_JSON:
+				return zctx.LookupTypeAlias("json", zng.TypeString)
+			case parquet.ConvertedType_ENUM:
+				return zctx.LookupTypeAlias("enum", zng.TypeString)
+			case parquet.ConvertedType_UTF8:
+				return zng.TypeString, nil
+			}
+		}
+		return zng.TypeBytes, nil
+	case parquet.Type_FIXED_LEN_BYTE_ARRAY:
+		switch {
+		case s.GetTypeLength() == 16 && s.IsSetLogicalType() && s.LogicalType.IsSetUUID():
+			return zctx.LookupTypeAlias("uuid", zng.TypeBytes)
+		case s.GetTypeLength() == 12 && s.GetConvertedType() == parquet.ConvertedType_INTERVAL:
+			return zctx.LookupTypeAlias("interval", zng.TypeBytes)
+		}
+		return zctx.LookupTypeAlias(fmt.Sprintf("fixed_len_byte_array_%d", *s.TypeLength), zng.TypeBytes)
+	}
+	panic(s.Type.String())
+}
diff --git a/zio/parquetio/types.md b/zio/parquetio/types.md
deleted file mode 100644
index 6da40ca2b..000000000
--- a/zio/parquetio/types.md
+++ /dev/null
@@ -1,104 +0,0 @@
-
-## Mapping Paruqet types to ZNG types
-
-This page describes the data types that may appear in Parquet files
-and how the ZNG reader maps (or can map) them into ZNG types.
-Information about the Parquet format comes from the
-[parquet-format github repo](https://github.com/apache/parquet-format#parquet-).
-In particular, we refer to the [Apache Thrift](https://thrift.apache.org/)
-definitions as the authoritative reference for what may be expressed
-in a Parquet file.
-
-By design, Parquet has a small number (7) of primitive types.
-Annotations can be added to any type to describe additional semantics
-(for example in ZNG, the `bytes`, `string`, `bstring`, and `enum` types
-all have the same representation, Parquet uses the same primitive type
-for its analogues of these types with an additional annotation to
-indiciate e.g., "this field should be decoded as UTF-8")
-Types can also be composed in limited ways to create more complicated
-structures.
-
-### Primitive Types
-
-The Parquet primitive types are defined
-[here](https://github.com/apache/parquet-format/blob/7390aa18ac855622f6d5cb737e9628eecd7565fd/src/main/thrift/parquet.thrift#L32-L41)
-
-Un-annotated values of these types may be mapped to ZNG as follows:
-
-| Parquet Type | ZNG Type | Notes |
-| ------------ | -------- | ----- |
-| BOOLEAN      | `bool`   | |
-| INT32        | `int32`  | |
-| INT64        | `int64`  | |
-| INT96        | (none)   | (see note below) |
-| FLOAT        | `float64` | This Parquet type is a 32 bit float, but the only float in ZNG is 64 bits |
-| DOUBLE       | `float64` | |
-| BYTE_ARRAY   | `bstring` | The ZNG `bytes` type would be more appropriate if/when it is implemented |
-| FIXED_LEN_BYTE_ARRAY | `bstring` | (same as above) |
-
-Note: We have come across INT96 valued columns "in the wild" in files
-created with pyspark that use timestamp types.  They apparently use
-[this format](https://github.com/xhochy/parquet-format/blob/cb4727767823ae201fd567f67825cc22834c20e9/LogicalTypes.md#int96-timestamps-also-called-impala_timestamp) but empirically, the pyspark writer seems to lose precision when
-using this format so its probably not a very high priority to support it.
-
-### Logical Types
-
-Parquet types that include annotations to provide additional information
-on how they should be interpreted are called logical types.
-There is a good description of logical types at
-<https://github.com/apache/parquet-format/blob/master/LogicalTypes.md>.
-Note that there are two ways to express these annotations in the file:
-"Converted Types" and "Logical Types".  Although the Parquet
-specification focuses on Logical Types and describes Converted Types as
-an "older representation", we haven't encountered files that use
-Logical Types.  In particular, files created with recent
-Nifi (version 1.11.4) use Converted Types.
-In any case, expanding support to cover Logical Types should be
-straightforward -- the difference is between the two is mostly about how
-the annotations are formatted in the file and Logical Types simply
-offer more flexibility and expressiveness.
-
-The Thrift definitions for Converted Types are
-[here](https://github.com/apache/parquet-format/blob/7390aa18ac855622f6d5cb737e9628eecd7565fd/src/main/thrift/parquet.thrift#L48-L177).
-Thrift definitions for Logical Types are
-[here](https://github.com/apache/parquet-format/blob/7390aa18ac855622f6d5cb737e9628eecd7565fd/src/main/thrift/parquet.thrift#L227-L344).
-
-These types can be mapped to ZNG as follows:
-
-| Parquet Type | Current Handling |  Notes |
-| ------------ | ---------------- | ----- |
-| Converted Type UTF8<br>Logical Type STRING | ZNG `string` ||
-| Converted Types MAP, MAP_KEY_VALUE<br>Logical Type MAP | (none) | see below |
-| Converted Type LIST<br>Logical Type LIST | ZNG `vector` | see below |
-| Converted Type ENUM<br>Logical Type ENUM | ZNG `string` | This could be the ZNG `enum` type if we ressurected it |
-| Converted Type DECIMAL<br>Logical Type DECIMAL | (none) | ZNG doesn't have an equivalent type.  We could convert these to floating point, but that would come at the cost of lost precision -- presumably people are using this type to avoid that problem. |
-| Converted Type DATE<br>Logical Type DATE | (none) | The Parquet type is just a date, not a particular time on a given date.  The ZNG `time` type is not exactly equivalent but we could define a convention such as "midnight UTC on the given date" |
-| Converted Types TIME_MILLIS, TIME_MICROS<br>Logical Type TIME | (none) | This is a particular time without an associated date (e.g., 3:00 PM).  ZNG has no equivalent type |
-| Converted Types TIMESTAMP_MILLIS, TIMESTAMP_MICROS<br>Logical Type TIMESTAMP | `time` | |
-| Converted Types UINT_8, UINT_16, UINT_32, UINT_64, INT_8, INT_16, INT_32, INT_64<br>Logical Type INTEGER | (none) | These could easily be converted to ZNG `byte`, `uint16`, `uint32`, `uint64`, `int16`, `int32`, `int64`.  ZNG has no signed 8-bit value, we could just convert that to an `int16`? |
-| Converted Type JSON<br>Logical Type JSON | ZNG `string` | Note that the actual structured data cannot be decoded/operated on from zql. |
-| Converted Type BSON<br>Logical Type BSON | ZNG `bstring` | As with JSON, we preserve the data here, but there's no way to extract the structured data represented by a BSON blob. |
-| Converted Type INTERVAL | (none) | This could be translated to ZNG `duration` type.  However, Parquet intervals can include months which makes them variable.  Such an interval can't be represented by the ZNG `duration` type but shorter intervals can be. |
-| Logical Type UNKNOWN | `null` | |
-| Logical Type UUID | (none) | Could easily be converted to ZNG `string` |
-
-Note: Parquet has no annotation to describe IP addresses or "subnets".
-How do existing Zeek->Parquet converters handle this and how can we
-ensure that we get this data back into an appropriate ZNG type?
-In the short term, we can do something ad hoc, but the long-term solution
-is probably something like what we do for JSON to supply additional
-out-of-band information about how particular fields should be translated.
-
-### Repeated Types
-
-Orthogonally to Converted Types and Logical Types, any field in Parquet
-can be "repeated".  This building block is used in conjunction with the
-MAP and LIST types to build more complex structures.
-The zq Parquet reader currently translates LIST structures as defined
-[here](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#lists) into ZNG `vector` types.  Any other repeated values are silently
-ignored by the Parquet reader.
-For the MAP type speficially, ZNG doesn't have any native equivalent.
-Adding maps as a ZNG feature has been discussed in the past.  In the
-absence of native maps, we could do something like
-`set[record[key:sometype, value:sometype]]` though it wouldn't be practical
-to operate on these from ZQL.
diff --git a/zio/parquetio/writer.go b/zio/parquetio/writer.go
new file mode 100644
index 000000000..46d7c5f47
--- /dev/null
+++ b/zio/parquetio/writer.go
@@ -0,0 +1,49 @@
+package parquetio
+
+import (
+	"io"
+
+	"github.com/brimsec/zq/zio/csvio"
+	"github.com/brimsec/zq/zng"
+	goparquet "github.com/fraugster/parquet-go"
+)
+
+type Writer struct {
+	w io.WriteCloser
+
+	fw  *goparquet.FileWriter
+	typ *zng.TypeRecord
+}
+
+func NewWriter(w io.WriteCloser) *Writer {
+	return &Writer{w: w}
+}
+
+func (w *Writer) Close() error {
+	var err error
+	if w.fw != nil {
+		err = w.fw.Close()
+	}
+	if err2 := w.w.Close(); err == nil {
+		err = err2
+	}
+	return err
+}
+
+func (w *Writer) Write(rec *zng.Record) error {
+	if w.typ == nil {
+		w.typ = rec.Type
+		sd, err := newSchemaDefinition(rec.Type)
+		if err != nil {
+			return err
+		}
+		w.fw = goparquet.NewFileWriter(w.w, goparquet.WithSchemaDefinition(sd))
+	} else if w.typ != rec.Type {
+		return csvio.ErrNotDataFrame
+	}
+	data, err := newRecordData(rec.Type, rec.Raw)
+	if err != nil {
+		return err
+	}
+	return w.fw.AddData(data)
+}
diff --git a/zql/docs/grouping/README.md b/zql/docs/grouping/README.md
index f1d4471c5..83eaa35cf 100644
--- a/zql/docs/grouping/README.md
+++ b/zql/docs/grouping/README.md
@@ -18,17 +18,23 @@ tips for their effective use.
 To create batches of events that are close together in time, specify
 `every <duration>` before invoking your aggregate function(s).
 
-The `<duration>` may be expressed in any of the following units of time. A
-numeric value must precede the unit specification.  The syntax conforms
-to the `dur-time` format of RFC-3339, with the `t` prefix dropped.
-
-| **Unit**  | **Suffix** |
-|-----------|------------|
-| `second`  | `s`        |
-| `minute`  | `m`        |
-| `hour`    | `h`        |
-
-XXX finish RFC-3339
+The `<duration>` may be expressed as a combination of one or more of the
+following units of time. An integer or decimal value must precede the
+specification of each unit.
+
+| **Unit**    | **Suffix** |
+|-------------|------------|
+| nanosecond  | `ns`       |
+| microsecond | `us`       |
+| millisecond | `ms`       |
+| second      | `s`        |
+| minute      | `m`        |
+| hour        | `h`        |
+| day         | `d`        |
+| week        | `w`        |
+| year        | `y`        |
+
+* **Note**: The year (`y`) duration assumes a year is always precisely 365 days. It does not account for leap years/seconds.
 
 #### Example #1:
 
@@ -51,10 +57,11 @@ TS                   SUM
 
 #### Example #2:
 
-To see which 30-second intervals contained the most events:
+To see which 30-second intervals contained the most events, expressing the
+duration as a half-minute:
 
 ```zq-command
-zq -f table 'every 30s count() | sort -r count' *.log.gz
+zq -f table 'every 0.5m count() | sort -r count' *.log.gz
 ```
 
 #### Output:
@@ -66,6 +73,24 @@ TS                   COUNT
 ...
 ```
 
+#### Example #3:
+
+To see the highest-numbered responding network port during each 90-second
+interval, expressing the duration as a mix of minutes and seconds:
+
+```zq-command
+zq -f table 'every 1m30s max(id.resp_p) | sort -r ts' conn.log.gz
+```
+
+#### Output:
+```zq-output head:4
+TS                   MAX
+2018-03-24T17:36:00Z 60008
+2018-03-24T17:34:30Z 60008
+2018-03-24T17:33:00Z 65389
+...
+```
+
 # Value Grouping - `by`
 
 To create batches of events based on the values of fields or the results of
diff --git a/zson/formatter.go b/zson/formatter.go
index 7ee67b6dd..feda9d910 100644
--- a/zson/formatter.go
+++ b/zson/formatter.go
@@ -337,6 +337,7 @@ func (f *Formatter) formatTypeBody(typ zng.Type) error {
 	case *zng.TypeMap:
 		f.build("|{")
 		f.formatType(typ.KeyType)
+		f.build(",")
 		f.formatType(typ.ValType)
 		f.build("}|")
 	case *zng.TypeUnion:


Test patch:
diff --git a/cmd/zq/ztests/no-files.yaml b/cmd/zq/ztests/no-files.yaml
new file mode 100644
index 000000000..3685428f9
--- /dev/null
+++ b/cmd/zq/ztests/no-files.yaml
@@ -0,0 +1,8 @@
+script: |
+  zq -z doesnotexist
+
+outputs:
+  - name: stdout
+    data: ""
+  - name: stderr
+    regexp: "no such file"
diff --git a/ppl/zqd/db/postgresdb/ztests/import.yaml b/ppl/zqd/db/postgresdb/ztests/import.yaml
index b8558c801..dcfd17600 100644
--- a/ppl/zqd/db/postgresdb/ztests/import.yaml
+++ b/ppl/zqd/db/postgresdb/ztests/import.yaml
@@ -6,19 +6,18 @@ env:
 script: |
   source startup.sh
   zapi -h $ZQD_HOST new -k archivestore testsp >/dev/null
-  zapi -h $ZQD_HOST -s testsp post babble.tzng >/dev/null
-  zapi -h $ZQD_HOST -s testsp get -f tzng "count()"
+  zapi -h $ZQD_HOST -s testsp post babble.zson >/dev/null
+  zapi -h $ZQD_HOST -s testsp get -z "count()"
 
 inputs:
   - name: startup.sh
     source: startup.sh
-  - name: babble.tzng
-    source: ../../../../../ztests/suite/data/babble.tzng
+  - name: babble.zson
+    source: ../../../../../ztests/suite/data/babble.zson
   - name: migrations
     symlink: ../migrations
 
 outputs:
   - name: stdout
     data: |
-      #0:record[count:uint64]
-      0:[1000;]
+      {count:1000 (uint64)} (=0)
diff --git a/ppl/zqd/temporal/ztests/cluster-compact.yaml b/ppl/zqd/temporal/ztests/cluster-compact.yaml
index 92d00ec54..3a15bee94 100644
--- a/ppl/zqd/temporal/ztests/cluster-compact.yaml
+++ b/ppl/zqd/temporal/ztests/cluster-compact.yaml
@@ -11,8 +11,8 @@ script: |
   space=ZTest_cluster_compact
   zapi -h $host rm $space &>/dev/null || true
   zapi -h $host new -k archivestore $space >/dev/null
-  zapi -h $host -s $space post babble.tzng >/dev/null
-  zapi -h $host -s $space post babble.tzng >/dev/null
+  zapi -h $host -s $space post babble.zson >/dev/null
+  zapi -h $host -s $space post babble.zson >/dev/null
   # Give the Temporal workflow a chance to run.
   sleep 5
   data_path=$(zapi -h $host ls -l -f zng | zq -f text "name=\"$space\" | cut data_path" -)
@@ -20,8 +20,8 @@ script: |
   zapi -h $host rm $space >/dev/null
 
 inputs:
-  - name: babble.tzng
-    source: ../../../../ztests/suite/data/babble.tzng
+  - name: babble.zson
+    source: ../../../../ztests/suite/data/babble.zson
   - name: temporal.sh
 
 outputs:
diff --git a/ppl/zqd/temporal/ztests/compact.yaml b/ppl/zqd/temporal/ztests/compact.yaml
index 05273a274..3b0e5c33a 100644
--- a/ppl/zqd/temporal/ztests/compact.yaml
+++ b/ppl/zqd/temporal/ztests/compact.yaml
@@ -4,15 +4,15 @@ script: |
   exec 2>&1
   source temporal.sh
   zapi -h $ZQD_HOST new -k archivestore test >/dev/null
-  zapi -h $ZQD_HOST -s test post babble.tzng >/dev/null
-  zapi -h $ZQD_HOST -s test post babble.tzng >/dev/null
+  zapi -h $ZQD_HOST -s test post babble.zson >/dev/null
+  zapi -h $ZQD_HOST -s test post babble.zson >/dev/null
   # Give the Temporal workflow a chance to run.
   sleep 1
   zar ls -R data/sp_* -ranges
 
 inputs:
-  - name: babble.tzng
-    source: ../../../../ztests/suite/data/babble.tzng
+  - name: babble.zson
+    source: ../../../../ztests/suite/data/babble.zson
   - name: temporal.sh
 
 outputs:
diff --git a/ppl/zqd/ztests/archivestore/postpcap-suricata.yaml b/ppl/zqd/ztests/archivestore/postpcap-suricata.yaml
index 751701964..e7a5a52aa 100644
--- a/ppl/zqd/ztests/archivestore/postpcap-suricata.yaml
+++ b/ppl/zqd/ztests/archivestore/postpcap-suricata.yaml
@@ -1,9 +1,9 @@
 script: |
   source services.sh
   zapi -h $ZQD_HOST -s testsp postpcap -k archivestore -f alerts.pcap >/dev/null
-  zapi -h $ZQD_HOST -s testsp get -f tzng "event_type = alert | every 1s count()"
+  zapi -h $ZQD_HOST -s testsp get -z "event_type = alert | every 1s count()"
   echo ===
-  zapi -h $ZQD_HOST -s testsp get -f tzng "_path != null | count()"
+  zapi -h $ZQD_HOST -s testsp get -z "_path != null | count()"
 
 inputs:
   - name: alerts.pcap
@@ -14,9 +14,7 @@ inputs:
 outputs:
   - name: stdout
     data: |
-      #0:record[ts:time,count:uint64]
-      0:[1425568033;13;]
-      0:[1425567868;2;]
+      {ts:2015-03-05T15:07:13Z,count:13 (uint64)} (=0)
+      {ts:2015-03-05T15:04:28Z,count:2} (0)
       ===
-      #0:record[count:uint64]
-      0:[379;]
+      {count:379 (uint64)} (=0)
diff --git a/ppl/zqd/ztests/archivestore/postpcap.yaml b/ppl/zqd/ztests/archivestore/postpcap.yaml
index 5f24382c2..cdc6e881c 100644
--- a/ppl/zqd/ztests/archivestore/postpcap.yaml
+++ b/ppl/zqd/ztests/archivestore/postpcap.yaml
@@ -1,9 +1,9 @@
 script: |
   source services.sh
   zapi -h $ZQD_HOST -s testsp postpcap -k archivestore -f ng.pcap >/dev/null
-  zapi -h $ZQD_HOST -s testsp get -f tzng "_path != stats | drop uid,geo | sort -r ts, _path"
+  zapi -h $ZQD_HOST -s testsp get -z "_path != stats | drop uid,geo | sort -r ts, _path"
   echo ===
-  zapi -h $ZQD_HOST -s testsp get -f tzng "count()"
+  zapi -h $ZQD_HOST -s testsp get -z "count()"
 
 inputs:
   - name: ng.pcap
@@ -14,17 +14,12 @@ inputs:
 outputs:
   - name: stdout
     data: |
-      #0:record[_path:string,ts:time,ts_delta:duration,peer:bstring,gaps:uint64,acks:uint64,percent_lost:float64]
-      0:[capture_loss;1425568893.736974;0.001192;zeek;0;0;0;]
-      #port=uint16
-      #zenum=string
-      #1:record[_path:string,ts:time,id:record[orig_h:ip,orig_p:port,resp_h:ip,resp_p:port],proto:zenum,service:bstring,duration:duration,orig_bytes:uint64,resp_bytes:uint64,conn_state:bstring,local_orig:bool,local_resp:bool,missed_bytes:uint64,history:bstring,orig_pkts:uint64,orig_ip_bytes:uint64,resp_pkts:uint64,resp_ip_bytes:uint64,tunnel_parents:set[bstring],community_id:bstring]
-      1:[conn;1425568893.735782;[192.168.0.2;34446;130.236.100.79;80;]tcp;-;0.001192;0;2776;OTH;-;-;0;Ad;1;52;2;2880;-;1:tOxXSQENCvoWlZaDPdf9YgwpshQ=;]
-      0:[capture_loss;1425568893.735782;1460.943301;zeek;0;0;0;]
-      1:[conn;1425567432.792481;[192.168.0.51;50858;192.168.0.1;80;]tcp;-;0.00074;338;0;OTH;-;-;0;ADa;2;418;1;40;-;1:YrhsMQd8siqxEOoc6A5E/gKZDgY=;]
-      1:[conn;1425567047.804914;[192.168.0.51;33773;80.239.174.91;443;]tcp;-;-;-;-;OTH;-;-;0;^d;0;0;1;1440;-;1:wPP6HnEPl0F9QDVCQ+E0du2PUi8=;]
-      1:[conn;1425567047.804906;[192.168.0.51;33773;80.239.174.91;443;]tcp;-;-;-;-;OTH;-;-;0;^d;0;0;1;1440;-;1:wPP6HnEPl0F9QDVCQ+E0du2PUi8=;]
-      1:[conn;1425567047.803929;[192.168.0.51;33773;80.239.174.91;443;]tcp;-;-;-;-;OTH;-;-;0;^d;0;0;1;1440;-;1:wPP6HnEPl0F9QDVCQ+E0du2PUi8=;]
+      {_path:"capture_loss",ts:2015-03-05T15:21:33.736974Z,ts_delta:1.192ms,peer:"zeek" (bstring),gaps:0 (uint64),acks:0 (uint64),percent_lost:0.} (=0)
+      {_path:"conn",ts:2015-03-05T15:21:33.735782Z,id:{orig_h:192.168.0.2,orig_p:34446 (port=(uint16)),resp_h:130.236.100.79,resp_p:80 (port)} (=1),proto:"tcp" (=zenum),service:null (bstring),duration:1.192ms,orig_bytes:0 (uint64),resp_bytes:2776 (uint64),conn_state:"OTH" (bstring),local_orig:null (bool),local_resp:null (bool),missed_bytes:0 (uint64),history:"Ad" (bstring),orig_pkts:1 (uint64),orig_ip_bytes:52 (uint64),resp_pkts:2 (uint64),resp_ip_bytes:2880 (uint64),tunnel_parents:null (2=(|[bstring]|)),community_id:"1:tOxXSQENCvoWlZaDPdf9YgwpshQ=" (bstring)} (=3)
+      {_path:"capture_loss",ts:2015-03-05T15:21:33.735782Z,ts_delta:24m20.943301s,peer:"zeek",gaps:0,acks:0,percent_lost:0.} (0)
+      {_path:"conn",ts:2015-03-05T14:57:12.792481Z,id:{orig_h:192.168.0.51,orig_p:50858,resp_h:192.168.0.1,resp_p:80},proto:"tcp",service:null,duration:740us,orig_bytes:338,resp_bytes:0,conn_state:"OTH",local_orig:null,local_resp:null,missed_bytes:0,history:"ADa",orig_pkts:2,orig_ip_bytes:418,resp_pkts:1,resp_ip_bytes:40,tunnel_parents:null,community_id:"1:YrhsMQd8siqxEOoc6A5E/gKZDgY="} (3)
+      {_path:"conn",ts:2015-03-05T14:50:47.804914Z,id:{orig_h:192.168.0.51,orig_p:33773,resp_h:80.239.174.91,resp_p:443},proto:"tcp",service:null,duration:null,orig_bytes:null,resp_bytes:null,conn_state:"OTH",local_orig:null,local_resp:null,missed_bytes:0,history:"^d",orig_pkts:0,orig_ip_bytes:0,resp_pkts:1,resp_ip_bytes:1440,tunnel_parents:null,community_id:"1:wPP6HnEPl0F9QDVCQ+E0du2PUi8="} (3)
+      {_path:"conn",ts:2015-03-05T14:50:47.804906Z,id:{orig_h:192.168.0.51,orig_p:33773,resp_h:80.239.174.91,resp_p:443},proto:"tcp",service:null,duration:null,orig_bytes:null,resp_bytes:null,conn_state:"OTH",local_orig:null,local_resp:null,missed_bytes:0,history:"^d",orig_pkts:0,orig_ip_bytes:0,resp_pkts:1,resp_ip_bytes:1440,tunnel_parents:null,community_id:"1:wPP6HnEPl0F9QDVCQ+E0du2PUi8="} (3)
+      {_path:"conn",ts:2015-03-05T14:50:47.803929Z,id:{orig_h:192.168.0.51,orig_p:33773,resp_h:80.239.174.91,resp_p:443},proto:"tcp",service:null,duration:null,orig_bytes:null,resp_bytes:null,conn_state:"OTH",local_orig:null,local_resp:null,missed_bytes:0,history:"^d",orig_pkts:0,orig_ip_bytes:0,resp_pkts:1,resp_ip_bytes:1440,tunnel_parents:null,community_id:"1:wPP6HnEPl0F9QDVCQ+E0du2PUi8="} (3)
       ===
-      #0:record[count:uint64]
-      0:[10;]
+      {count:10 (uint64)} (=0)
diff --git a/ppl/zqd/ztests/cluster/count.yaml b/ppl/zqd/ztests/cluster/count.yaml
index c45ba0c4b..8a26a1832 100644
--- a/ppl/zqd/ztests/cluster/count.yaml
+++ b/ppl/zqd/ztests/cluster/count.yaml
@@ -2,10 +2,9 @@ tag: cluster
 
 script: |
   # Test assumes a remote zqd instance with space "files" is available at 9867
-  time zapi -s files get -workers 4 -f tzng 'count()' 2>&1
+  time zapi -s files get -workers 4 -z 'count()' 2>&1
 
 outputs:
   - name: stdout
     data: |
-      #0:record[count:uint64]
-      0:[12243237;]
+      {count:12243237 (uint64)} (=0)
diff --git a/ppl/zqd/ztests/cluster/countbytxhost.yaml b/ppl/zqd/ztests/cluster/countbytxhost.yaml
index 485ab2999..6716fe566 100644
--- a/ppl/zqd/ztests/cluster/countbytxhost.yaml
+++ b/ppl/zqd/ztests/cluster/countbytxhost.yaml
@@ -2,14 +2,13 @@ tag: cluster
 
 script: |
   # Test assumes a remote zqd instance with space "files" is available at 9867
-  zapi -s files get -workers 4 -f tzng 'count() by tx_hosts | sort -r count | head 5' 2>&1
+  zapi -s files get -workers 4 -z 'count() by tx_hosts | sort -r count | head 5' 2>&1
 
 outputs:
   - name: stdout
     data: |
-      #0:record[tx_hosts:set[ip],count:uint64]
-      0:[[10.47.2.156;]985198;]
-      0:[[10.47.7.58;]827462;]
-      0:[[10.47.5.58;]779783;]
-      0:[[10.47.8.58;]659903;]
-      0:[[10.47.1.58;]536395;]
+      {tx_hosts:|[10.47.2.156]|,count:985198 (uint64)} (=0)
+      {tx_hosts:|[10.47.7.58]|,count:827462} (0)
+      {tx_hosts:|[10.47.5.58]|,count:779783} (0)
+      {tx_hosts:|[10.47.8.58]|,count:659903} (0)
+      {tx_hosts:|[10.47.1.58]|,count:536395} (0)
diff --git a/ppl/zqd/ztests/cluster/filter1.yaml b/ppl/zqd/ztests/cluster/filter1.yaml
index 41d2a85e6..cd8988d50 100644
--- a/ppl/zqd/ztests/cluster/filter1.yaml
+++ b/ppl/zqd/ztests/cluster/filter1.yaml
@@ -2,10 +2,9 @@ tag: cluster
 
 script: |
   # Test assumes a remote zqd instance with space "files" is available at 9867
-  zapi -s files get -workers 1 -f tzng '"FgzbkQ15TOoOENn1lb" | sort tx_hosts' 2>&1 
+  zapi -s files get -workers 1 -z '"FgzbkQ15TOoOENn1lb" | sort tx_hosts' 2>&1 
 
 outputs:
   - name: stdout
     data: |
-      #0:record[_path:string,ts:time,fuid:bstring,tx_hosts:set[ip],rx_hosts:set[ip],conn_uids:set[bstring],source:bstring,depth:uint64,analyzers:set[bstring],mime_type:bstring,filename:bstring,duration:duration,local_orig:bool,is_orig:bool,seen_bytes:uint64,total_bytes:uint64,missing_bytes:uint64,overflow_bytes:uint64,timedout:bool,parent_fuid:bstring,md5:bstring,sha1:bstring,sha256:bstring,extracted:bstring,extracted_cutoff:bool,extracted_size:uint64]
-      0:[files;1521940131.094903;FgzbkQ15TOoOENn1lb;[198.199.95.203;][10.47.25.80;][Cc4q8L1Hhp9MjJiVcl;]SSL;0;[MD5;SHA1;X509;SHA256;]application/x-x509-ca-cert;-;0;-;F;1174;-;0;0;F;-;b15409274f54ad8f023d3b85a5ecec5d;e6a3b45b062d509b3382282d196efe97d5956ccb;25847d668eb4f04fdd40b12b6b0740c567da7d024308eb6c2c96fe41d9de218d;-;-;-;]
+      {_path:"files",ts:2018-03-25T01:08:51.094903Z,fuid:"FgzbkQ15TOoOENn1lb" (bstring),tx_hosts:|[198.199.95.203]|,rx_hosts:|[10.47.25.80]|,conn_uids:|["Cc4q8L1Hhp9MjJiVcl" (bstring)]| (=0),source:"SSL" (bstring),depth:0 (uint64),analyzers:|["MD5","SHA1","X509","SHA256"]| (0),mime_type:"application/x-x509-ca-cert" (bstring),filename:null (bstring),duration:0s,local_orig:null (bool),is_orig:false,seen_bytes:1174 (uint64),total_bytes:null (uint64),missing_bytes:0 (uint64),overflow_bytes:0 (uint64),timedout:false,parent_fuid:null (bstring),md5:"b15409274f54ad8f023d3b85a5ecec5d" (bstring),sha1:"e6a3b45b062d509b3382282d196efe97d5956ccb" (bstring),sha256:"25847d668eb4f04fdd40b12b6b0740c567da7d024308eb6c2c96fe41d9de218d" (bstring),extracted:null (bstring),extracted_cutoff:null (bool),extracted_size:null (uint64)} (=1)
diff --git a/ppl/zqd/ztests/cluster/filter2.yaml b/ppl/zqd/ztests/cluster/filter2.yaml
index 810215bd2..d6819b9d1 100644
--- a/ppl/zqd/ztests/cluster/filter2.yaml
+++ b/ppl/zqd/ztests/cluster/filter2.yaml
@@ -2,10 +2,9 @@ tag: cluster
 
 script: |
   # Test assumes a remote zqd instance with space "files" is available at 9867
-  zapi -s files get -workers 2 -f tzng '"FgzbkQ15TOoOENn1lb" | sort tx_hosts' 2>&1 
+  zapi -s files get -workers 2 -z '"FgzbkQ15TOoOENn1lb" | sort tx_hosts' 2>&1 
 
 outputs:
   - name: stdout
     data: |
-      #0:record[_path:string,ts:time,fuid:bstring,tx_hosts:set[ip],rx_hosts:set[ip],conn_uids:set[bstring],source:bstring,depth:uint64,analyzers:set[bstring],mime_type:bstring,filename:bstring,duration:duration,local_orig:bool,is_orig:bool,seen_bytes:uint64,total_bytes:uint64,missing_bytes:uint64,overflow_bytes:uint64,timedout:bool,parent_fuid:bstring,md5:bstring,sha1:bstring,sha256:bstring,extracted:bstring,extracted_cutoff:bool,extracted_size:uint64]
-      0:[files;1521940131.094903;FgzbkQ15TOoOENn1lb;[198.199.95.203;][10.47.25.80;][Cc4q8L1Hhp9MjJiVcl;]SSL;0;[MD5;SHA1;X509;SHA256;]application/x-x509-ca-cert;-;0;-;F;1174;-;0;0;F;-;b15409274f54ad8f023d3b85a5ecec5d;e6a3b45b062d509b3382282d196efe97d5956ccb;25847d668eb4f04fdd40b12b6b0740c567da7d024308eb6c2c96fe41d9de218d;-;-;-;]
+      {_path:"files",ts:2018-03-25T01:08:51.094903Z,fuid:"FgzbkQ15TOoOENn1lb" (bstring),tx_hosts:|[198.199.95.203]|,rx_hosts:|[10.47.25.80]|,conn_uids:|["Cc4q8L1Hhp9MjJiVcl" (bstring)]| (=0),source:"SSL" (bstring),depth:0 (uint64),analyzers:|["MD5","SHA1","X509","SHA256"]| (0),mime_type:"application/x-x509-ca-cert" (bstring),filename:null (bstring),duration:0s,local_orig:null (bool),is_orig:false,seen_bytes:1174 (uint64),total_bytes:null (uint64),missing_bytes:0 (uint64),overflow_bytes:0 (uint64),timedout:false,parent_fuid:null (bstring),md5:"b15409274f54ad8f023d3b85a5ecec5d" (bstring),sha1:"e6a3b45b062d509b3382282d196efe97d5956ccb" (bstring),sha256:"25847d668eb4f04fdd40b12b6b0740c567da7d024308eb6c2c96fe41d9de218d" (bstring),extracted:null (bstring),extracted_cutoff:null (bool),extracted_size:null (uint64)} (=1)
diff --git a/ppl/zqd/ztests/cluster/filter4.yaml b/ppl/zqd/ztests/cluster/filter4.yaml
index ad3ad936b..af10cafab 100644
--- a/ppl/zqd/ztests/cluster/filter4.yaml
+++ b/ppl/zqd/ztests/cluster/filter4.yaml
@@ -2,10 +2,9 @@ tag: cluster
 
 script: |
   # Test assumes a remote zqd instance with space "files" is available at 9867
-  zapi -s files get -workers 4 -f tzng '"FgzbkQ15TOoOENn1lb" | sort tx_hosts' 2>&1 
+  zapi -s files get -workers 4 -z '"FgzbkQ15TOoOENn1lb" | sort tx_hosts' 2>&1 
 
 outputs:
   - name: stdout
     data: |
-      #0:record[_path:string,ts:time,fuid:bstring,tx_hosts:set[ip],rx_hosts:set[ip],conn_uids:set[bstring],source:bstring,depth:uint64,analyzers:set[bstring],mime_type:bstring,filename:bstring,duration:duration,local_orig:bool,is_orig:bool,seen_bytes:uint64,total_bytes:uint64,missing_bytes:uint64,overflow_bytes:uint64,timedout:bool,parent_fuid:bstring,md5:bstring,sha1:bstring,sha256:bstring,extracted:bstring,extracted_cutoff:bool,extracted_size:uint64]
-      0:[files;1521940131.094903;FgzbkQ15TOoOENn1lb;[198.199.95.203;][10.47.25.80;][Cc4q8L1Hhp9MjJiVcl;]SSL;0;[MD5;SHA1;X509;SHA256;]application/x-x509-ca-cert;-;0;-;F;1174;-;0;0;F;-;b15409274f54ad8f023d3b85a5ecec5d;e6a3b45b062d509b3382282d196efe97d5956ccb;25847d668eb4f04fdd40b12b6b0740c567da7d024308eb6c2c96fe41d9de218d;-;-;-;]
+      {_path:"files",ts:2018-03-25T01:08:51.094903Z,fuid:"FgzbkQ15TOoOENn1lb" (bstring),tx_hosts:|[198.199.95.203]|,rx_hosts:|[10.47.25.80]|,conn_uids:|["Cc4q8L1Hhp9MjJiVcl" (bstring)]| (=0),source:"SSL" (bstring),depth:0 (uint64),analyzers:|["MD5","SHA1","X509","SHA256"]| (0),mime_type:"application/x-x509-ca-cert" (bstring),filename:null (bstring),duration:0s,local_orig:null (bool),is_orig:false,seen_bytes:1174 (uint64),total_bytes:null (uint64),missing_bytes:0 (uint64),overflow_bytes:0 (uint64),timedout:false,parent_fuid:null (bstring),md5:"b15409274f54ad8f023d3b85a5ecec5d" (bstring),sha1:"e6a3b45b062d509b3382282d196efe97d5956ccb" (bstring),sha256:"25847d668eb4f04fdd40b12b6b0740c567da7d024308eb6c2c96fe41d9de218d" (bstring),extracted:null (bstring),extracted_cutoff:null (bool),extracted_size:null (uint64)} (=1)
diff --git a/ppl/zqd/ztests/csv-fuse.yaml b/ppl/zqd/ztests/csv-fuse.yaml
index f368d7d46..c536b074a 100644
--- a/ppl/zqd/ztests/csv-fuse.yaml
+++ b/ppl/zqd/ztests/csv-fuse.yaml
@@ -1,18 +1,16 @@
 script: |
   source services.sh
   zapi -h $ZQD_HOST new test
-  zapi -h $ZQD_HOST -s test post in.tzng
+  zapi -h $ZQD_HOST -s test post in.zson
   zapi -h $ZQD_HOST -s test get -e csv > out.csv
 
 inputs:
   - name: services.sh
     source: services.sh
-  - name: in.tzng
+  - name: in.zson
     data: |
-      #0:record[a:string]
-      0:[hello;]
-      #1:record[b:int32]
-      1:[123;]
+      {a:"hello"}
+      {b:123 (int32)} (=0)
 
 outputs:
   - name: out.csv
diff --git a/ppl/zqd/ztests/csv.yaml b/ppl/zqd/ztests/csv.yaml
index e11c33481..8b157ebfd 100644
--- a/ppl/zqd/ztests/csv.yaml
+++ b/ppl/zqd/ztests/csv.yaml
@@ -1,16 +1,15 @@
 script: |
   source services.sh
   zapi -h $ZQD_HOST new test
-  zapi -h $ZQD_HOST -s test post in.tzng
+  zapi -h $ZQD_HOST -s test post in.zson
   zapi -h $ZQD_HOST -s test get -e csv > out.csv
 
 inputs:
   - name: services.sh
     source: services.sh
-  - name: in.tzng
+  - name: in.zson
     data: |
-      #0:record[a:string,b:record[c:string,d:string]]
-      0:[hello;[world;goodbye;]]
+      {a:"hello",b:{c:"world",d:"goodbye"}}
 
 outputs:
   - name: out.csv
diff --git a/ppl/zqd/ztests/index-new-data.yaml b/ppl/zqd/ztests/index-new-data.yaml
index f227fb12e..534206d8a 100644
--- a/ppl/zqd/ztests/index-new-data.yaml
+++ b/ppl/zqd/ztests/index-new-data.yaml
@@ -2,27 +2,24 @@ script: |
   source services.sh
   zapi -h $ZQD_HOST new -k archivestore testsp >/dev/null
   zapi -h $ZQD_HOST -s testsp index create :ip _path
-  zapi -h $ZQD_HOST -s testsp post data.tzng >/dev/null
-  zapi -h $ZQD_HOST -s testsp index find _path=http | zq -f tzng "drop _log" -
+  zapi -h $ZQD_HOST -s testsp post data.zson >/dev/null
+  zapi -h $ZQD_HOST -s testsp index find _path=http | zq -z "drop _log" -
   echo ===
-  zapi -h $ZQD_HOST -s testsp index find :ip=127.0.0.3 | zq -f tzng "drop _log" -
+  zapi -h $ZQD_HOST -s testsp index find :ip=127.0.0.3 | zq -z "drop _log" -
 
 inputs:
   - name: services.sh
     source: services.sh
-  - name: data.tzng
+  - name: data.zson
     data: |
-      #0:record[_path:string,ts:time,orig_h:ip]
-      0:[conn;1;127.0.0.1;]
-      0:[http;1;127.0.0.1;]
-      0:[conn;2;127.0.0.2;]
-      0:[http;2;127.0.0.3;]
+      {_path:"conn",ts:1970-01-01T00:00:01Z,orig_h:127.0.0.1}
+      {_path:"http",ts:1970-01-01T00:00:01Z,orig_h:127.0.0.1}
+      {_path:"conn",ts:1970-01-01T00:00:02Z,orig_h:127.0.0.2}
+      {_path:"http",ts:1970-01-01T00:00:02Z,orig_h:127.0.0.3}
 
 outputs:
   - name: stdout
     data: |
-      #0:record[key:string,count:uint64,first:time,last:time]
-      0:[http;2;2;1;]
+      {key:"http",count:2 (uint64),first:1970-01-01T00:00:02Z,last:1970-01-01T00:00:01Z} (=0)
       ===
-      #0:record[key:ip,count:uint64,first:time,last:time]
-      0:[127.0.0.3;1;2;1;]
+      {key:127.0.0.3,count:1 (uint64),first:1970-01-01T00:00:02Z,last:1970-01-01T00:00:01Z} (=0)
diff --git a/ppl/zqd/ztests/index-type.yaml b/ppl/zqd/ztests/index-type.yaml
index d37852945..5f7beae7e 100644
--- a/ppl/zqd/ztests/index-type.yaml
+++ b/ppl/zqd/ztests/index-type.yaml
@@ -1,26 +1,23 @@
 script: |
   source services.sh
   zapi -h $ZQD_HOST new -k archivestore -thresh 1B testsp >/dev/null
-  zapi -h $ZQD_HOST -s testsp post data.tzng >/dev/null
+  zapi -h $ZQD_HOST -s testsp post data.zson >/dev/null
   zapi -h $ZQD_HOST -s testsp index create :ip
-  zapi -h $ZQD_HOST -s testsp index find :ip=192.168.1.102 | zq -f tzng "drop _log" -
+  zapi -h $ZQD_HOST -s testsp index find :ip=192.168.1.102 | zq -z "drop _log" -
   echo ===
-  zapi -h $ZQD_HOST -s testsp index find :ip=192.168.1.103 | zq -f tzng "drop _log" -
+  zapi -h $ZQD_HOST -s testsp index find :ip=192.168.1.103 | zq -z "drop _log" -
 
 inputs:
   - name: services.sh
     source: services.sh
-  - name: data.tzng
+  - name: data.zson
     data: |
-      #0:record[_path:string,ts:time,orig_h:ip]
-      0:[test;1.0;192.168.1.102;]
-      0:[test;2;192.168.1.103;]
+      {_path:"test",ts:1970-01-01T00:00:01Z,orig_h:192.168.1.102}
+      {_path:"test",ts:1970-01-01T00:00:02Z,orig_h:192.168.1.103}
 
 outputs:
   - name: stdout
     data: |
-      #0:record[key:ip,count:uint64,first:time,last:time]
-      0:[192.168.1.102;1;1;1;]
+      {key:192.168.1.102,count:1 (uint64),first:1970-01-01T00:00:01Z,last:1970-01-01T00:00:01Z} (=0)
       ===
-      #0:record[key:ip,count:uint64,first:time,last:time]
-      0:[192.168.1.103;1;2;2;]
+      {key:192.168.1.103,count:1 (uint64),first:1970-01-01T00:00:02Z,last:1970-01-01T00:00:02Z} (=0)
diff --git a/ppl/zqd/ztests/json.yaml b/ppl/zqd/ztests/json.yaml
index a81f7b47a..0edf231f0 100644
--- a/ppl/zqd/ztests/json.yaml
+++ b/ppl/zqd/ztests/json.yaml
@@ -1,16 +1,15 @@
 script: |
   source services.sh
   zapi -h $ZQD_HOST new test
-  zapi -h $ZQD_HOST -s test post in.tzng
+  zapi -h $ZQD_HOST -s test post in.zson
   zapi -h $ZQD_HOST -s test get -e json > out.json
 
 inputs:
   - name: services.sh
     source: services.sh
-  - name: in.tzng
+  - name: in.zson
     data: |
-      #0:record[a:string,b:record[c:string,d:string]]
-      0:[hello;[world;goodbye;]]
+      {a:"hello",b:{c:"world",d:"goodbye"}}
 
 outputs:
   - name: out.json
diff --git a/ppl/zqd/ztests/ls.yaml b/ppl/zqd/ztests/ls.yaml
index 718e060c3..e9bb6f481 100644
--- a/ppl/zqd/ztests/ls.yaml
+++ b/ppl/zqd/ztests/ls.yaml
@@ -11,7 +11,7 @@ script: |
   # Verify that output flags are honored.
   zapi -h $ZQD_HOST ls -l -f zson | zq -i zson -f zson "pick name,storage_kind" -
   echo ===
-  zapi -h $ZQD_HOST ls -l -f zng | zq -i zng -f tzng "pick name,storage_kind" -
+  zapi -h $ZQD_HOST ls -l -f zng | zq -i zng -f table "pick name,storage_kind" -
   echo ===
 
 inputs:
@@ -36,7 +36,7 @@ outputs:
           storage_kind: "archivestore"
       }
       ===
-      #0:record[name:string,storage_kind:string]
-      0:[sp1;archivestore;]
-      0:[sp2;archivestore;]
+      NAME STORAGE_KIND
+      sp1  archivestore
+      sp2  archivestore
       ===
diff --git a/ppl/zqd/ztests/ndjson.yaml b/ppl/zqd/ztests/ndjson.yaml
index 82e846357..08d219e1a 100644
--- a/ppl/zqd/ztests/ndjson.yaml
+++ b/ppl/zqd/ztests/ndjson.yaml
@@ -1,17 +1,16 @@
 script: |
   source services.sh
   zapi -h $ZQD_HOST new test
-  zapi -h $ZQD_HOST -s test post in.tzng
+  zapi -h $ZQD_HOST -s test post in.zson
   zapi -h $ZQD_HOST -s test get -e ndjson "pick a,b" > out.ndjson
 
 inputs:
   - name: services.sh
     source: services.sh
-  - name: in.tzng
+  - name: in.zson
     data: |
-      #0:record[ts:time,a:string,b:string]
-      0:[2;hello;world;]
-      0:[1;goodnight;gracie;]
+      {ts:1970-01-01T00:00:02Z,a:"hello",b:"world"}
+      {ts:1970-01-01T00:00:01Z,a:"goodnight",b:"gracie"}
 
 outputs:
   - name: out.ndjson
diff --git a/ppl/zqd/ztests/pcap/pcap-deleted.yaml b/ppl/zqd/ztests/pcap/pcap-deleted.yaml
index 286b4dbc1..f5a2f3d97 100644
--- a/ppl/zqd/ztests/pcap/pcap-deleted.yaml
+++ b/ppl/zqd/ztests/pcap/pcap-deleted.yaml
@@ -9,7 +9,7 @@ script: |
   echo ===
   zapi -h $ZQD_HOST ls
   echo ===
-  zapi -h $ZQD_HOST -s testsp get -f tzng "_path=conn | count()"
+  zapi -h $ZQD_HOST -s testsp get -z "_path=conn | count()"
   echo ===
   zapi -h $ZQD_HOST rm testsp
 
@@ -35,7 +35,6 @@ outputs:
       ===
       testsp
       ===
-      #0:record[count:uint64]
-      0:[5;]
+      {count:5 (uint64)} (=0)
       ===
       testsp: space removed
diff --git a/ppl/zqd/ztests/pcap/postpcap-suricata-update.yaml b/ppl/zqd/ztests/pcap/postpcap-suricata-update.yaml
index 51e139266..f4d086880 100644
--- a/ppl/zqd/ztests/pcap/postpcap-suricata-update.yaml
+++ b/ppl/zqd/ztests/pcap/postpcap-suricata-update.yaml
@@ -29,7 +29,7 @@ script: |
   done
 
   zapi -h $ZQD_HOST -s testsp postpcap -f alerts.pcap >/dev/null
-  zapi -h $ZQD_HOST -s testsp get -f tzng "event_type = alert | count()"
+  zapi -h $ZQD_HOST -s testsp get -z "event_type = alert | count()"
 
 inputs:
   - name: alerts.pcap
@@ -42,5 +42,4 @@ inputs:
 outputs:
   - name: stdout
     data: |
-      #0:record[count:uint64]
-      0:[15;]
+      {count:15 (uint64)} (=0)
diff --git a/ppl/zqd/ztests/pcap/postpcap-suricata.yaml b/ppl/zqd/ztests/pcap/postpcap-suricata.yaml
index 398f624b4..141e59152 100644
--- a/ppl/zqd/ztests/pcap/postpcap-suricata.yaml
+++ b/ppl/zqd/ztests/pcap/postpcap-suricata.yaml
@@ -1,10 +1,10 @@
 script: |
   source services.sh
   zapi -h $ZQD_HOST -s testsp postpcap -f alerts.pcap >/dev/null
-  zapi -h $ZQD_HOST -s testsp get -f tzng "event_type = alert | every 1s count()"
-  zapi -h $ZQD_HOST -s testsp get -f tzng "event_type = alert and community_id != '' | count()"
+  zapi -h $ZQD_HOST -s testsp get -z "event_type = alert | every 1s count()"
+  zapi -h $ZQD_HOST -s testsp get -z "event_type = alert and community_id != '' | count()"
   echo ===
-  zapi -h $ZQD_HOST -s testsp get -f tzng "_path != null | count()"
+  zapi -h $ZQD_HOST -s testsp get -z "_path != null | count()"
 
 inputs:
   - name: alerts.pcap
@@ -15,13 +15,10 @@ inputs:
 outputs:
   - name: stdout
     data: |
-      #0:record[ts:time,count:uint64]
-      0:[1425568033;13;]
-      0:[1425567868;2;]
-      #0:record[count:uint64]
-      0:[15;]
+      {ts:2015-03-05T15:07:13Z,count:13 (uint64)} (=0)
+      {ts:2015-03-05T15:04:28Z,count:2} (0)
+      {count:15 (uint64)} (=0)
       ===
-      #0:record[count:uint64]
-      0:[379;]
+      {count:379 (uint64)} (=0)
   - name: stderr
     data: ""
diff --git a/ppl/zqd/ztests/pcap/postpcap.yaml b/ppl/zqd/ztests/pcap/postpcap.yaml
index 07ac873a6..79472f877 100644
--- a/ppl/zqd/ztests/pcap/postpcap.yaml
+++ b/ppl/zqd/ztests/pcap/postpcap.yaml
@@ -1,9 +1,9 @@
 script: |
   source services.sh
   zapi -h $ZQD_HOST -s testsp postpcap -f ng.pcap >/dev/null
-  zapi -h $ZQD_HOST -s testsp get -f tzng "_path != stats | drop uid,geo | sort -r ts, _path"
+  zapi -h $ZQD_HOST -s testsp get -z "_path != stats | drop uid,geo | sort -r ts, _path"
   echo ===
-  zapi -h $ZQD_HOST -s testsp get -f tzng "count()"
+  zapi -h $ZQD_HOST -s testsp get -z "count()"
 
 inputs:
   - name: ng.pcap
@@ -14,17 +14,12 @@ inputs:
 outputs:
   - name: stdout
     data: |
-      #0:record[_path:string,ts:time,ts_delta:duration,peer:bstring,gaps:uint64,acks:uint64,percent_lost:float64]
-      0:[capture_loss;1425568893.736974;0.001192;zeek;0;0;0;]
-      #port=uint16
-      #zenum=string
-      #1:record[_path:string,ts:time,id:record[orig_h:ip,orig_p:port,resp_h:ip,resp_p:port],proto:zenum,service:bstring,duration:duration,orig_bytes:uint64,resp_bytes:uint64,conn_state:bstring,local_orig:bool,local_resp:bool,missed_bytes:uint64,history:bstring,orig_pkts:uint64,orig_ip_bytes:uint64,resp_pkts:uint64,resp_ip_bytes:uint64,tunnel_parents:set[bstring],community_id:bstring]
-      1:[conn;1425568893.735782;[192.168.0.2;34446;130.236.100.79;80;]tcp;-;0.001192;0;2776;OTH;-;-;0;Ad;1;52;2;2880;-;1:tOxXSQENCvoWlZaDPdf9YgwpshQ=;]
-      0:[capture_loss;1425568893.735782;1460.943301;zeek;0;0;0;]
-      1:[conn;1425567432.792481;[192.168.0.51;50858;192.168.0.1;80;]tcp;-;0.00074;338;0;OTH;-;-;0;ADa;2;418;1;40;-;1:YrhsMQd8siqxEOoc6A5E/gKZDgY=;]
-      1:[conn;1425567047.804914;[192.168.0.51;33773;80.239.174.91;443;]tcp;-;-;-;-;OTH;-;-;0;^d;0;0;1;1440;-;1:wPP6HnEPl0F9QDVCQ+E0du2PUi8=;]
-      1:[conn;1425567047.804906;[192.168.0.51;33773;80.239.174.91;443;]tcp;-;-;-;-;OTH;-;-;0;^d;0;0;1;1440;-;1:wPP6HnEPl0F9QDVCQ+E0du2PUi8=;]
-      1:[conn;1425567047.803929;[192.168.0.51;33773;80.239.174.91;443;]tcp;-;-;-;-;OTH;-;-;0;^d;0;0;1;1440;-;1:wPP6HnEPl0F9QDVCQ+E0du2PUi8=;]
+      {_path:"capture_loss",ts:2015-03-05T15:21:33.736974Z,ts_delta:1.192ms,peer:"zeek" (bstring),gaps:0 (uint64),acks:0 (uint64),percent_lost:0.} (=0)
+      {_path:"conn",ts:2015-03-05T15:21:33.735782Z,id:{orig_h:192.168.0.2,orig_p:34446 (port=(uint16)),resp_h:130.236.100.79,resp_p:80 (port)} (=1),proto:"tcp" (=zenum),service:null (bstring),duration:1.192ms,orig_bytes:0 (uint64),resp_bytes:2776 (uint64),conn_state:"OTH" (bstring),local_orig:null (bool),local_resp:null (bool),missed_bytes:0 (uint64),history:"Ad" (bstring),orig_pkts:1 (uint64),orig_ip_bytes:52 (uint64),resp_pkts:2 (uint64),resp_ip_bytes:2880 (uint64),tunnel_parents:null (2=(|[bstring]|)),community_id:"1:tOxXSQENCvoWlZaDPdf9YgwpshQ=" (bstring)} (=3)
+      {_path:"capture_loss",ts:2015-03-05T15:21:33.735782Z,ts_delta:24m20.943301s,peer:"zeek",gaps:0,acks:0,percent_lost:0.} (0)
+      {_path:"conn",ts:2015-03-05T14:57:12.792481Z,id:{orig_h:192.168.0.51,orig_p:50858,resp_h:192.168.0.1,resp_p:80},proto:"tcp",service:null,duration:740us,orig_bytes:338,resp_bytes:0,conn_state:"OTH",local_orig:null,local_resp:null,missed_bytes:0,history:"ADa",orig_pkts:2,orig_ip_bytes:418,resp_pkts:1,resp_ip_bytes:40,tunnel_parents:null,community_id:"1:YrhsMQd8siqxEOoc6A5E/gKZDgY="} (3)
+      {_path:"conn",ts:2015-03-05T14:50:47.804914Z,id:{orig_h:192.168.0.51,orig_p:33773,resp_h:80.239.174.91,resp_p:443},proto:"tcp",service:null,duration:null,orig_bytes:null,resp_bytes:null,conn_state:"OTH",local_orig:null,local_resp:null,missed_bytes:0,history:"^d",orig_pkts:0,orig_ip_bytes:0,resp_pkts:1,resp_ip_bytes:1440,tunnel_parents:null,community_id:"1:wPP6HnEPl0F9QDVCQ+E0du2PUi8="} (3)
+      {_path:"conn",ts:2015-03-05T14:50:47.804906Z,id:{orig_h:192.168.0.51,orig_p:33773,resp_h:80.239.174.91,resp_p:443},proto:"tcp",service:null,duration:null,orig_bytes:null,resp_bytes:null,conn_state:"OTH",local_orig:null,local_resp:null,missed_bytes:0,history:"^d",orig_pkts:0,orig_ip_bytes:0,resp_pkts:1,resp_ip_bytes:1440,tunnel_parents:null,community_id:"1:wPP6HnEPl0F9QDVCQ+E0du2PUi8="} (3)
+      {_path:"conn",ts:2015-03-05T14:50:47.803929Z,id:{orig_h:192.168.0.51,orig_p:33773,resp_h:80.239.174.91,resp_p:443},proto:"tcp",service:null,duration:null,orig_bytes:null,resp_bytes:null,conn_state:"OTH",local_orig:null,local_resp:null,missed_bytes:0,history:"^d",orig_pkts:0,orig_ip_bytes:0,resp_pkts:1,resp_ip_bytes:1440,tunnel_parents:null,community_id:"1:wPP6HnEPl0F9QDVCQ+E0du2PUi8="} (3)
       ===
-      #0:record[count:uint64]
-      0:[10;]
+      {count:10 (uint64)} (=0)
diff --git a/ppl/zqd/ztests/python.yaml b/ppl/zqd/ztests/python.yaml
index f8f173639..7b3d93d58 100644
--- a/ppl/zqd/ztests/python.yaml
+++ b/ppl/zqd/ztests/python.yaml
@@ -8,7 +8,7 @@ script: |
 
   source services.sh
   zapi -h $ZQD_HOST new test >/dev/null
-  zapi -h $ZQD_HOST -s test post in.tzng >/dev/null
+  zapi -h $ZQD_HOST -s test post in.zson >/dev/null
 
   python <<EOF
   import zqd
@@ -18,11 +18,10 @@ script: |
   EOF
 
 inputs:
-  - name: in.tzng
+  - name: in.zson
     data: |
-      #0:record[ts:time,s:string]
-      0:[1;world;]
-      0:[2;hello;]
+      {ts:1970-01-01T00:00:01Z,s:"world"}
+      {ts:1970-01-01T00:00:02Z,s:"hello"}
   - name: services.sh
     source: services.sh
   - name: setup.py
diff --git a/ppl/zqd/ztests/recruiter-root.yaml b/ppl/zqd/ztests/recruiter-root.yaml
index 053570f3b..db7540220 100644
--- a/ppl/zqd/ztests/recruiter-root.yaml
+++ b/ppl/zqd/ztests/recruiter-root.yaml
@@ -12,20 +12,20 @@ script: |
   #
   zapi -h $ZQD_HOST -s testsp post smtp.log.gz > /dev/null
 
-  zq -t "count()" smtp.log.gz > zqcount.tzng
-  zapi -h $ZQD_HOST -s testsp get -workers 2 -t "count()" > zapicount.tzng
+  zq -t "count()" smtp.log.gz > zqcount.zson
+  zapi -h $ZQD_HOST -s testsp get -workers 2 -t "count()" > zapicount.zson
   echo ===
-  diff -s zqcount.tzng zapicount.tzng
+  diff -s zqcount.zson zapicount.zson
 
-  zq -t "39161" smtp.log.gz > zqfilter.tzng
-  zapi -h $ZQD_HOST -s testsp get -workers 2 -t "39161" > zapifilter.tzng
+  zq -t "39161" smtp.log.gz > zqfilter.zson
+  zapi -h $ZQD_HOST -s testsp get -workers 2 -t "39161" > zapifilter.zson
   echo ===
-  diff -s zqfilter.tzng zapifilter.tzng
+  diff -s zqfilter.zson zapifilter.zson
 
-  zq -t "sort -r ts | tail 5" smtp.log.gz > zqtail.tzng
-  zapi -h $ZQD_HOST -s testsp get -workers 2 -t "tail 5" > zapitail.tzng
+  zq -t "sort -r ts | tail 5" smtp.log.gz > zqtail.zson
+  zapi -h $ZQD_HOST -s testsp get -workers 2 -t "tail 5" > zapitail.zson
   echo ===
-  diff -s zqtail.tzng zapitail.tzng
+  diff -s zqtail.zson zapitail.zson
   # cat zqd-root.log
   # echo === zqd-w1
   # cat zqd-w1.log
@@ -42,8 +42,8 @@ outputs:
   - name: stdout
     data: |
       ===
-      Files zqcount.tzng and zapicount.tzng are identical
+      Files zqcount.zson and zapicount.zson are identical
       ===
-      Files zqfilter.tzng and zapifilter.tzng are identical
+      Files zqfilter.zson and zapifilter.zson are identical
       ===
-      Files zqtail.tzng and zapitail.tzng are identical
+      Files zqtail.zson and zapitail.zson are identical
diff --git a/ppl/zqd/ztests/redis/immcache.yaml b/ppl/zqd/ztests/redis/immcache.yaml
index 1d13c2045..3b24dd4c5 100644
--- a/ppl/zqd/ztests/redis/immcache.yaml
+++ b/ppl/zqd/ztests/redis/immcache.yaml
@@ -3,7 +3,7 @@ tag: services
 script: |
   source services.sh
   zapi -h $ZQD_HOST new -k archivestore -thresh 20KiB testsp
-  zapi -h $ZQD_HOST -s testsp post babble.tzng >/dev/null
+  zapi -h $ZQD_HOST -s testsp post babble.zson >/dev/null
   echo ===
   zapi -h $ZQD_HOST -s testsp get -f text "count()"
   echo ===
@@ -12,8 +12,8 @@ script: |
   curl $ZQD_HOST/metrics | grep archive_cache
 
 inputs:
-  - name: babble.tzng
-    source: ../../../../ztests/suite/data/babble-sorted.tzng
+  - name: babble.zson
+    source: ../../../../ztests/suite/data/babble-sorted.zson
   - name: services.sh
     source: services.sh
 
diff --git a/ppl/zqd/ztests/worker-dist.yaml b/ppl/zqd/ztests/worker-dist.yaml
index 780fffa28..ac1680ad0 100644
--- a/ppl/zqd/ztests/worker-dist.yaml
+++ b/ppl/zqd/ztests/worker-dist.yaml
@@ -21,27 +21,27 @@ script: |
   # The count() from zq should be identical to
   # the count() from zapi get -chunk
   #
-  zq -f tzng "count()" smtp.log.gz > zqcount.tzng
-  zapi -h $ZQD_HOST -s testsp get -workers 2 -f tzng "count()" > zapicount.tzng
+  zq -f zson "count()" smtp.log.gz > zqcount.zson
+  zapi -h $ZQD_HOST -s testsp get -workers 2 -f zson "count()" > zapicount.zson
   echo ===
-  diff -s zqcount.tzng zapicount.tzng
+  diff -s zqcount.zson zapicount.zson
   #
   # Compare output from a simple filter for a unique string.
   # This will verify that filter expressions are
   # passed though to the worker zqd.
   #
-  zq -f tzng "39161" smtp.log.gz > zqfilter.tzng
-  zapi -h $ZQD_HOST -s testsp get -workers 2 -f tzng "39161" > zapifilter.tzng
+  zq -f zson "39161" smtp.log.gz > zqfilter.zson
+  zapi -h $ZQD_HOST -s testsp get -workers 2 -f zson "39161" > zapifilter.zson
   echo ===
-  diff -s zqfilter.tzng zapifilter.tzng
+  diff -s zqfilter.zson zapifilter.zson
   #
   # Compare output from the tail function to make sure
   # the record order is the same.
   #
-  zq -f tzng "sort -r ts | tail 5" smtp.log.gz > zqtail.tzng
-  zapi -h $ZQD_HOST -s testsp get -workers 2 -f tzng "tail 5" > zapitail.tzng
+  zq -f zson "sort -r ts | tail 5" smtp.log.gz > zqtail.zson
+  zapi -h $ZQD_HOST -s testsp get -workers 2 -f zson "tail 5" > zapitail.zson
   echo ===
-  diff -s zqtail.tzng zapitail.tzng
+  diff -s zqtail.zson zapitail.zson
 
 inputs:
   - name: common.sh
@@ -55,8 +55,8 @@ outputs:
   - name: stdout
     data: |
       ===
-      Files zqcount.tzng and zapicount.tzng are identical
+      Files zqcount.zson and zapicount.zson are identical
       ===
-      Files zqfilter.tzng and zapifilter.tzng are identical
+      Files zqfilter.zson and zapifilter.zson are identical
       ===
-      Files zqtail.tzng and zapitail.tzng are identical
+      Files zqtail.zson and zapitail.zson are identical
diff --git a/ppl/zqd/ztests/zapi/post-shape.yaml b/ppl/zqd/ztests/zapi/post-shape.yaml
index 396151735..00321e592 100644
--- a/ppl/zqd/ztests/zapi/post-shape.yaml
+++ b/ppl/zqd/ztests/zapi/post-shape.yaml
@@ -1,16 +1,15 @@
 script: |
   source services.sh
   zapi -h $ZQD_HOST new test
-  zapi -h $ZQD_HOST -s test post -z "put ip=ip:ip" in.tzng in.tzng
+  zapi -h $ZQD_HOST -s test post -z "put ip=ip:ip" in.zson in.zson
   zapi -h $ZQD_HOST -s test get -f zson > out.zson
 
 inputs:
   - name: services.sh
     source: ../services.sh
-  - name: in.tzng
+  - name: in.zson
     data: |
-      #0:record[ip:string]
-      0:[1.1.1.1;]
+      {ip:"1.1.1.1"}
 
 outputs:
   - name: out.zson
diff --git a/ppl/zqd/ztests/zapi/postpath-errors.yaml b/ppl/zqd/ztests/zapi/postpath-errors.yaml
index 07259af75..940ffce5b 100644
--- a/ppl/zqd/ztests/zapi/postpath-errors.yaml
+++ b/ppl/zqd/ztests/zapi/postpath-errors.yaml
@@ -1,18 +1,17 @@
 script: |
   source services.sh
   zapi -h $ZQD_HOST new test
-  zapi -h $ZQD_HOST -s test postpath bad.tzng
+  zapi -h $ZQD_HOST -s test postpath bad.zson
   zapi -h $ZQD_HOST -s test get -f zson "count()" > out.zson
 
 inputs:
   - name: services.sh
     source: ../services.sh
-  - name: bad.tzng
+  - name: bad.zson
     data: |
-      #0:record[ip:string]
-      0:[1.1.1.1;]
-      0:[1.1.1.1;
-      0:[1.1.1.1;]
+      {ip:"1.1.1.1"}
+      {ip:"1.1.1.1"
+      {ip:"1.1.1.1"}
 
 outputs:
   - name: out.zson
diff --git a/ppl/zqd/ztests/zapi/postpath-shape.yaml b/ppl/zqd/ztests/zapi/postpath-shape.yaml
index e96d360f7..33965c663 100644
--- a/ppl/zqd/ztests/zapi/postpath-shape.yaml
+++ b/ppl/zqd/ztests/zapi/postpath-shape.yaml
@@ -1,16 +1,15 @@
 script: |
   source services.sh
   zapi -h $ZQD_HOST new test
-  zapi -h $ZQD_HOST -s test postpath -z "put ip=ip:ip" in.tzng in.tzng
+  zapi -h $ZQD_HOST -s test postpath -z "put ip=ip:ip" in.zson in.zson
   zapi -h $ZQD_HOST -s test get -f zson > out.zson
 
 inputs:
   - name: services.sh
     source: ../services.sh
-  - name: in.tzng
+  - name: in.zson
     data: |
-      #0:record[ip:string]
-      0:[1.1.1.1;]
+      {ip:"1.1.1.1"}
 
 outputs:
   - name: out.zson
diff --git a/ppl/zqd/ztests/zapi/postpath-warnings.yaml b/ppl/zqd/ztests/zapi/postpath-warnings.yaml
index 524170362..3a76311d7 100644
--- a/ppl/zqd/ztests/zapi/postpath-warnings.yaml
+++ b/ppl/zqd/ztests/zapi/postpath-warnings.yaml
@@ -1,17 +1,16 @@
 script: |
   source services.sh
   zapi -h $ZQD_HOST new test
-  zapi -h $ZQD_HOST -s test postpath bad.tzng | grep warning > warn.txt
+  zapi -h $ZQD_HOST -s test postpath bad.zson | grep warning > warn.txt
   zapi -h $ZQD_HOST -s test get -f zson "count()" > out.zson
 
 inputs:
   - name: services.sh
     source: ../services.sh
-  - name: bad.tzng
+  - name: bad.zson
     data: |
-      #0:record[ip:string]
-      0:[1.1.1.1;]
-      0:[1.1.1.2
+      {ip:"1.1.1.1"}
+      {ip:"1.1.1.2
 
 outputs:
   - name: warn.txt
diff --git a/zq_test.go b/zq_test.go
index ddc8299bc..931724def 100644
--- a/zq_test.go
+++ b/zq_test.go
@@ -8,6 +8,8 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/brimsec/zq/zio/detector"
+	"github.com/brimsec/zq/zio/parquetio"
 	"github.com/brimsec/zq/zio/tzngio"
 	"github.com/brimsec/zq/zng/resolver"
 	"github.com/brimsec/zq/ztest"
@@ -25,6 +27,9 @@ func TestZq(t *testing.T) {
 			ztest.Run(t, d)
 		})
 	}
+	t.Run("ParquetBoomerang", func(t *testing.T) {
+		runParquetBoomerangs(t, dirs)
+	})
 	t.Run("ZsonBoomerang", func(t *testing.T) {
 		runZsonBoomerangs(t, dirs)
 	})
@@ -47,7 +52,13 @@ func runZsonBoomerangs(t *testing.T, dirs map[string]struct{}) {
 	if testing.Short() {
 		return
 	}
-	bundles, err := findTzngs(t, dirs)
+	const script = `
+exec 2>&1
+zq -f zson - > baseline.zson &&
+zq -i zson -f zson baseline.zson > boomerang.zson &&
+diff baseline.zson boomerang.zson
+`
+	bundles, err := findInputs(t, dirs, script, isValidTzng)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -79,33 +90,23 @@ type BoomerangError struct {
 }
 
 func (b *BoomerangError) Error() string {
-	return fmt.Sprintf("%s\n=== with this zson ===\n\n%s\n\n=== from file ===\n\n%s\n\n", b.Err, b.Zson, b.FileName)
+	return fmt.Sprintf("%s\n=== with this input ===\n\n%s\n\n=== from file ===\n\n%s\n\n", b.Err, b.Zson, b.FileName)
 }
 
-const script = `
-zq -f zson in.tzng > baseline.zson
-zq -i zson -f zson baseline.zson > boomerang.zson
-diff baseline.zson boomerang.zson
-echo EOF
-`
-
-var eof = `EOF
-`
-var empty = ""
-
-func boomerang(zson string) *ztest.ZTest {
+func boomerang(script, input string) *ztest.ZTest {
+	var empty string
 	return &ztest.ZTest{
 		Script: script,
 		Inputs: []ztest.File{
 			{
-				Name: "in.tzng",
-				Data: &zson,
+				Name: "stdin",
+				Data: &input,
 			},
 		},
 		Outputs: []ztest.File{
 			{
 				Name: "stdout",
-				Data: &eof,
+				Data: &empty,
 			},
 			{
 				Name: "stderr",
@@ -127,8 +128,8 @@ func expectFailure(b ztest.Bundle) bool {
 	return false
 }
 
-func isValidTzng(src string) bool {
-	r := tzngio.NewReader(strings.NewReader(src), resolver.NewContext())
+func isValidTzng(input string) bool {
+	r := tzngio.NewReader(strings.NewReader(input), resolver.NewContext())
 	for {
 		rec, err := r.Read()
 		if err != nil {
@@ -140,7 +141,65 @@ func isValidTzng(src string) bool {
 	}
 }
 
-func findTzngs(t *testing.T, dirs map[string]struct{}) ([]ztest.Bundle, error) {
+func runParquetBoomerangs(t *testing.T, dirs map[string]struct{}) {
+	if testing.Short() {
+		return
+	}
+	const script = `
+exec 2>&1
+zq -f parquet -o baseline.parquet fuse - &&
+zq -i parquet -f parquet -o boomerang.parquet baseline.parquet &&
+diff baseline.parquet boomerang.parquet
+`
+	bundles, err := findInputs(t, dirs, script, isValidForParquet)
+	if err != nil {
+		t.Fatal(err)
+	}
+	shellPath, err := ztest.ShellPath()
+	if err != nil {
+		t.Fatal(err)
+	}
+	for _, b := range bundles {
+		b := b
+		t.Run(b.TestName, func(t *testing.T) {
+			t.Parallel()
+			err := b.RunScript(shellPath, ".")
+			if err != nil {
+				if s := err.Error(); strings.Contains(s, parquetio.ErrEmptyRecordType.Error()) ||
+					strings.Contains(s, parquetio.ErrNullType.Error()) ||
+					strings.Contains(s, parquetio.ErrUnionType.Error()) {
+					t.Skip("skipping because the Parquet writer cannot handle an input type")
+				}
+				err = &BoomerangError{
+					*b.Test.Inputs[0].Data,
+					b.FileName,
+					err,
+				}
+			}
+			require.NoError(t, err)
+		})
+	}
+}
+
+func isValidForParquet(input string) bool {
+	r, err := detector.NewReader(strings.NewReader(input), resolver.NewContext())
+	if err != nil {
+		return false
+	}
+	var found bool
+	for {
+		rec, err := r.Read()
+		if err != nil {
+			return false
+		}
+		if rec == nil {
+			return found
+		}
+		found = true
+	}
+}
+
+func findInputs(t *testing.T, dirs map[string]struct{}, script string, isValidInput func(string) bool) ([]ztest.Bundle, error) {
 	var out []ztest.Bundle
 	for path := range dirs {
 		bundles, err := ztest.Load(path)
@@ -149,34 +208,30 @@ func findTzngs(t *testing.T, dirs map[string]struct{}) ([]ztest.Bundle, error) {
 			continue
 		}
 		// Transform the bundles into boomerang tests by taking each
-		// tzng source and creating a new ztest.Bundle.
+		// source and creating a new ztest.Bundle.
 		for _, bundle := range bundles {
 			if bundle.Error != nil || expectFailure(bundle) {
 				continue
 			}
 			// Normalize the diffrent kinds of test inputs into
 			// a single pattern.
-			for _, src := range bundle.Test.Input {
-				if !isValidTzng(src) {
-					continue
-				}
-				b := ztest.Bundle{
-					TestName: bundle.TestName,
-					FileName: bundle.FileName,
-					Test:     boomerang(src),
+			for _, input := range bundle.Test.Input {
+				if isValidInput(input) {
+					out = append(out, ztest.Bundle{
+						TestName: bundle.TestName,
+						FileName: bundle.FileName,
+						Test:     boomerang(script, input),
+					})
 				}
-				out = append(out, b)
 			}
-			for _, src := range bundle.Test.Inputs {
-				if src.Data == nil || !isValidTzng(*src.Data) {
-					continue
-				}
-				b := ztest.Bundle{
-					TestName: bundle.TestName,
-					FileName: bundle.FileName,
-					Test:     boomerang(*src.Data),
+			for _, input := range bundle.Test.Inputs {
+				if input.Data != nil && isValidInput(*input.Data) {
+					out = append(out, ztest.Bundle{
+						TestName: bundle.TestName,
+						FileName: bundle.FileName,
+						Test:     boomerang(script, *input.Data),
+					})
 				}
-				out = append(out, b)
 			}
 		}
 	}
diff --git a/zson/ztests/container-types.yaml b/zson/ztests/container-types.yaml
index 9514b768a..845bcf76c 100644
--- a/zson/ztests/container-types.yaml
+++ b/zson/ztests/container-types.yaml
@@ -7,7 +7,7 @@ input: |
   { n: null ({a: string}) }
 
 output: |
-  {n:null (0=(|{stringint32}|))} (=1)
+  {n:null (0=(|{string,int32}|))} (=1)
   {n:null (2=(|[string]|))}
   {n:null (3=([string]))}
   {n:null (4=({a:string}))}