From 4dc3e49a0d340d4c8aff79bc0efb5dda9495e385 Mon Sep 17 00:00:00 2001 From: nagamocha3000 Date: Fri, 15 Jan 2021 22:42:37 +0300 Subject: [PATCH 01/11] Add test helper for inserting to time quantum fields --- test/cluster.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/cluster.go b/test/cluster.go index 006729ced..3af5c9b23 100644 --- a/test/cluster.go +++ b/test/cluster.go @@ -164,6 +164,36 @@ func (c *Cluster) ImportKeyKey(t testing.TB, index, field string, valAndRecKeys } } +// TimeQuantumKey is a string key and a string+key value +type TimeQuantumKey struct { + RowKey string + ColKey string + Ts int64 +} + +// ImportTimeQuantumKey imports data into an index where the index is keyd +// and the field is a time-quantum +func (c *Cluster) ImportTimeQuantumKey(t testing.TB, index, field string, entries []TimeQuantumKey) { + t.Helper() + importRequest := &pilosa.ImportRequest{ + Index: index, + Field: field, + RowKeys: make([]string, len(entries)), + ColumnKeys: make([]string, len(entries)), + Timestamps: make([]int64, len(entries)), + } + for i, entry := range entries { + importRequest.ColumnKeys[i] = entry.ColKey + importRequest.RowKeys[i] = entry.RowKey + importRequest.Timestamps[i] = entry.Ts + + } + err := c.Nodes[0].API.Import(context.Background(), nil, importRequest) + if err != nil { + t.Fatalf("importing keykey data: %v", err) + } +} + // IntKey is a string key and a signed integer value. type IntKey struct { Val int64 From 54d79d1e4d4c73b9ae7f99cd50a2d8f71b232205 Mon Sep 17 00:00:00 2001 From: nagamocha3000 Date: Fri, 15 Jan 2021 22:50:54 +0300 Subject: [PATCH 02/11] Populate 'places_visited' field for 'users' index --- executor_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/executor_test.go b/executor_test.go index 6db994817..f8e5f80a4 100644 --- a/executor_test.go +++ b/executor_test.go @@ -6834,6 +6834,30 @@ func variousQueries(t *testing.T, clusterSize int) { {"icecream", "userF"}, }) + // Create and populate "places_visited" time field. + c.CreateField(t, "users", pilosa.IndexOptions{Keys: true, TrackExistence: true}, "places_visited", pilosa.OptFieldKeys(), pilosa.OptFieldTypeTime(pilosa.TimeQuantum("YM"))) + ts2019Jan01 := int64(1546300800) * 1e+9 // 2019 January 1st 0:00:00 + ts2019Aug01 := int64(1564617600) * 1e+9 // 2019 August 1st 0:00:00 + ts2020Jan01 := int64(1577836800) * 1e+9 // 2020 January 1st 0:00:00 + c.ImportTimeQuantumKey(t, "users", "places_visited", []test.TimeQuantumKey{ + // 2019 January: nairobi, paris, austin, toronto + {RowKey: "nairobi", ColKey: "userB", Ts: ts2019Jan01}, + {RowKey: "paris", ColKey: "userC", Ts: ts2019Jan01}, + {RowKey: "austin", ColKey: "userF", Ts: ts2019Jan01}, + {RowKey: "toronto", ColKey: "userA", Ts: ts2019Jan01}, + // 2019 August: nairobi, paris, austin, toronto + {RowKey: "toronto", ColKey: "userB", Ts: ts2019Aug01}, + {RowKey: "toronto", ColKey: "userC", Ts: ts2019Aug01}, + // 2020: toronto, mombasa, sydney, nairobi + {RowKey: "toronto", ColKey: "userB", Ts: ts2020Jan01}, + {RowKey: "toronto", ColKey: "userD", Ts: ts2020Jan01}, + {RowKey: "toronto", ColKey: "userE", Ts: ts2020Jan01}, + {RowKey: "toronto", ColKey: "userF", Ts: ts2020Jan01}, + {RowKey: "mombasa", ColKey: "userA", Ts: ts2020Jan01}, + {RowKey: "sydney", ColKey: "userD", Ts: ts2020Jan01}, + {RowKey: "nairobi", ColKey: "userE", Ts: ts2020Jan01}, + }) + // Create and populate "affinity" int field with negative, positive, zero and null values. c.CreateField(t, "users", pilosa.IndexOptions{Keys: true, TrackExistence: true}, "affinity", pilosa.OptFieldTypeInt(-1000, 1000)) c.ImportIntKey(t, "users", "affinity", []test.IntKey{ From f91b4bc016075af824841acc24362f95ada65359 Mon Sep 17 00:00:00 2001 From: nagamocha3000 Date: Fri, 15 Jan 2021 22:58:18 +0300 Subject: [PATCH 03/11] Add queries to test GroupBy Rows on time field --- executor_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/executor_test.go b/executor_test.go index f8e5f80a4..10db42a63 100644 --- a/executor_test.go +++ b/executor_test.go @@ -6895,6 +6895,32 @@ func variousQueries(t *testing.T, clusterSize int) { qrVerifier func(t *testing.T, resp pilosa.QueryResponse) csvVerifier string }{ + { // 2020 & 2019 All + query: `GroupBy(Rows(places_visited, from='2019-01-01T00:00', to='2020-12-31T23:59'))`, + csvVerifier: `nairobi,2,0 +paris,1,0 +austin,1,0 +toronto,6,0 +mombasa,1,0 +sydney,1,0 +`, + }, + { // 2019 All + query: `GroupBy(Rows(places_visited, from='2019-01-01T00:00', to='2019-12-31T23:59'))`, + csvVerifier: `nairobi,1,0 +paris,1,0 +austin,1,0 +toronto,3,0 +`, + }, + { // 2019 January only + query: `GroupBy(Rows(places_visited, from='2019-01-01T00:00', to='2019-02-01T00:00'))`, + csvVerifier: `nairobi,1,0 +paris,1,0 +austin,1,0 +toronto,1,0 +`, + }, { query: "Count(All())", qrVerifier: func(t *testing.T, resp pilosa.QueryResponse) { From b77f9e8a431a1fa6835a02b4544c08ed48ccdee7 Mon Sep 17 00:00:00 2001 From: nagamocha3000 Date: Fri, 15 Jan 2021 23:36:54 +0300 Subject: [PATCH 04/11] Add timeFragments rowIterator --- executor.go | 80 +++++++++++++++++++++++++++++++++++++++++++++-------- fragment.go | 69 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 12 deletions(-) diff --git a/executor.go b/executor.go index 722f63d32..ff5317b04 100644 --- a/executor.go +++ b/executor.go @@ -7262,9 +7262,11 @@ func newGroupByIterator(executor *executor, qcx *Qcx, rowIDs []RowIDs, children idx := holder.Index(index) var ( - fieldName string - viewName string - ok bool + fieldName string + viewName string + ok bool + views []string + isTimeField bool ) ignorePrev := false for i, call := range children { @@ -7278,9 +7280,42 @@ func newGroupByIterator(executor *executor, qcx *Qcx, rowIDs []RowIDs, children gbi.fields[i].Field = fieldName switch field.Type() { - case FieldTypeSet, FieldTypeTime, FieldTypeMutex, FieldTypeBool: + case FieldTypeSet, FieldTypeMutex, FieldTypeBool: viewName = viewStandard + case FieldTypeTime: + var ( + err error + v interface{} + ) + // Parse "from" time, if set. + var ( + hasFrom bool + fromTime time.Time + ) + if v, hasFrom = call.Args["from"]; hasFrom { + if fromTime, err = parseTime(v); err != nil { + return nil, errors.Wrap(err, "parsing from time") + } + } + + // Parse "to" time, if set. + var ( + hasTo bool + toTime time.Time + ) + if v, hasTo = call.Args["to"]; hasTo { + if toTime, err = parseTime(v); err != nil { + return nil, errors.Wrap(err, "parsing to time") + } + } + + if hasTo || hasFrom { + views = viewsByTimeRange(viewStandard, fromTime, toTime, field.TimeQuantum()) + isTimeField = true + } else { + viewName = viewStandard + } case FieldTypeInt: viewName = viewBSIGroupPrefix + fieldName @@ -7289,11 +7324,6 @@ func newGroupByIterator(executor *executor, qcx *Qcx, rowIDs []RowIDs, children call.Name, strings.Join([]string{FieldTypeSet, FieldTypeTime, FieldTypeMutex, FieldTypeBool, FieldTypeInt}, ",")) } - // Fetch fragment. - frag := holder.fragment(index, fieldName, viewName, shard) - if frag == nil { // this means this whole shard doesn't have all it needs to continue - return nil, nil - } filters := []roaring.BitmapFilter{} if len(rowIDs[i]) > 0 { filters = append(filters, roaring.NewBitmapRowsFilter(rowIDs[i])) @@ -7305,9 +7335,35 @@ func newGroupByIterator(executor *executor, qcx *Qcx, rowIDs []RowIDs, children } defer finisher(&err0) - gbi.rowIters[i], err = frag.rowIterator(tx, i != 0, filters...) - if err != nil { - return nil, err + // Fetch fragment(s), get rowIterator + if isTimeField { + var fragments []*fragment + for _, viewName := range views { + fragment := holder.fragment(index, fieldName, viewName, shard) + if fragment != nil { + fragments = append(fragments, fragment) + } + } + if len(fragments) == 0 { + // whole shard doesn't have all it needs to continue ? + return nil, nil + } + + gbi.rowIters[i], err = timeFragmentsRowIterator(fragments, tx, i != 0, filters...) + if err != nil { + return nil, err + } + } else { + frag := holder.fragment(index, fieldName, viewName, shard) + if frag == nil { // this means this whole shard doesn't have all it needs to continue + return nil, nil + } + + gbi.rowIters[i], err = frag.rowIterator(tx, i != 0, filters...) + if err != nil { + return nil, err + } + } prev, hasPrev, err := call.UintArg("previous") diff --git a/fragment.go b/fragment.go index aa93dcc9a..031043390 100644 --- a/fragment.go +++ b/fragment.go @@ -3329,6 +3329,75 @@ func (f *fragment) rowIterator(tx Tx, wrap bool, filters ...roaring.BitmapFilter return f.setRowIterator(tx, wrap, filters...) } +type timeRowIterator struct { + tx Tx + fragments []*fragment + rows []*Row + rowIDs [][]uint64 + cur int + wrap bool +} + +func timeFragmentsRowIterator(fragments []*fragment, tx Tx, wrap bool, filters ...roaring.BitmapFilter) (rowIterator, error) { + if len(fragments) == 0 { + return nil, fmt.Errorf("there should be at least 1 fragment") + } else if len(fragments) == 1 { + return fragments[0].setRowIterator(tx, wrap, filters...) + } + + it := &timeRowIterator{ + tx: tx, + fragments: fragments, + rows: make([]*Row, len(fragments)), + rowIDs: make([][]uint64, len(fragments)), + } + + for i, f := range fragments { + rows, err := f.rows(context.Background(), tx, 0, filters...) + if err != nil { + return nil, err + } + it.rowIDs[i] = rows + } + + return it, nil +} + +func (it *timeRowIterator) Seek(rowID uint64) { + rowIDs := it.rowIDs[0] + idx := sort.Search(len(rowIDs), func(i int) bool { + return rowIDs[i] >= rowID + }) + it.cur = idx +} + +func (it *timeRowIterator) Next() (r *Row, rowID uint64, _ *int64, wrapped bool, err error) { + rowIDs := it.rowIDs[0] + if it.cur >= len(rowIDs) { + if !it.wrap || len(rowIDs) == 0 { + return nil, 0, nil, true, nil + } + it.Seek(0) + wrapped = true + } + + id := rowIDs[it.cur] + // gather rows + for i, fragment := range it.fragments { + row, err := fragment.row(it.tx, id) + if err != nil { + return row, rowID, nil, wrapped, err + } + it.rows[i] = row + } + + // union rows + r = it.rows[0].Union(it.rows[1:]...) + + it.cur++ + return r, rowID, nil, wrapped, nil +} + type intRowIterator struct { f *fragment values int64Slice // sorted slice of int values From f51ff4dc85fe46b589aa1f0dffe48cbc56674f13 Mon Sep 17 00:00:00 2001 From: nagamocha3000 Date: Mon, 18 Jan 2021 20:54:31 +0300 Subject: [PATCH 05/11] Add tests for Rows() on time fields --- executor_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/executor_test.go b/executor_test.go index 10db42a63..613cd99c2 100644 --- a/executor_test.go +++ b/executor_test.go @@ -6921,6 +6921,18 @@ austin,1,0 toronto,1,0 `, }, + { // 2020 & 2019 All + query: `Rows(places_visited, from='2019-01-01T00:00', to='2020-12-31T23:59')`, + csvVerifier: "nairobi\nparis\naustin\ntoronto\nmombasa\nsydney\n", + }, + { // 2019 All + query: `Rows(places_visited, from='2019-01-01T00:00', to='2019-12-31T23:59')`, + csvVerifier: "nairobi\nparis\naustin\ntoronto\n", + }, + { // 2019 January only + query: `Rows(places_visited, from='2019-01-01T00:00', to='2019-02-01T00:00')`, + csvVerifier: "nairobi\nparis\naustin\ntoronto\n", + }, { query: "Count(All())", qrVerifier: func(t *testing.T, resp pilosa.QueryResponse) { From 9b23dcdd0a34e07e168dfa1d91f4891f327555be Mon Sep 17 00:00:00 2001 From: nagamocha3000 Date: Tue, 19 Jan 2021 01:44:54 +0300 Subject: [PATCH 06/11] Gather rows for each fragment in a much smarter way --- fragment.go | 71 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/fragment.go b/fragment.go index 031043390..5038646f3 100644 --- a/fragment.go +++ b/fragment.go @@ -3330,12 +3330,11 @@ func (f *fragment) rowIterator(tx Tx, wrap bool, filters ...roaring.BitmapFilter } type timeRowIterator struct { - tx Tx - fragments []*fragment - rows []*Row - rowIDs [][]uint64 - cur int - wrap bool + tx Tx + cur int + wrap bool + allRowIDs []uint64 + rowIDToFragments map[uint64][]*fragment } func timeFragmentsRowIterator(fragments []*fragment, tx Tx, wrap bool, filters ...roaring.BitmapFilter) (rowIterator, error) { @@ -3346,53 +3345,77 @@ func timeFragmentsRowIterator(fragments []*fragment, tx Tx, wrap bool, filters . } it := &timeRowIterator{ - tx: tx, - fragments: fragments, - rows: make([]*Row, len(fragments)), - rowIDs: make([][]uint64, len(fragments)), + tx: tx, + cur: 0, + wrap: wrap, } - for i, f := range fragments { - rows, err := f.rows(context.Background(), tx, 0, filters...) + // create a sort of inverted index that maps each + // rowID back to the fragments that have that rowID + rowIDToFragments := make(map[uint64][]*fragment) + for _, f := range fragments { + rowIDs, err := f.rows(context.Background(), tx, 0, filters...) if err != nil { return nil, err } - it.rowIDs[i] = rows + for _, rowID := range rowIDs { + fs := append(rowIDToFragments[rowID], f) + rowIDToFragments[rowID] = fs + } } + // if len(rowIDToFragments) == 0 what to do ?? + // ie all fragments returned empty rowIDs, is this possible + // is this an error + + // collect all rowIDs from inverted index to a slice + allRowIDs := make([]uint64, len(rowIDToFragments)) + i := 0 + for rowID := range rowIDToFragments { + allRowIDs[i] = rowID + i++ + } + + it.rowIDToFragments = rowIDToFragments + it.allRowIDs = allRowIDs + return it, nil } func (it *timeRowIterator) Seek(rowID uint64) { - rowIDs := it.rowIDs[0] - idx := sort.Search(len(rowIDs), func(i int) bool { - return rowIDs[i] >= rowID + idx := sort.Search(len(it.allRowIDs), func(i int) bool { + return it.allRowIDs[i] >= rowID }) it.cur = idx } func (it *timeRowIterator) Next() (r *Row, rowID uint64, _ *int64, wrapped bool, err error) { - rowIDs := it.rowIDs[0] - if it.cur >= len(rowIDs) { - if !it.wrap || len(rowIDs) == 0 { + if it.cur >= len(it.allRowIDs) { + if !it.wrap || len(it.allRowIDs) == 0 { return nil, 0, nil, true, nil } it.Seek(0) wrapped = true } - id := rowIDs[it.cur] // gather rows - for i, fragment := range it.fragments { - row, err := fragment.row(it.tx, id) + rowID = it.allRowIDs[it.cur] + fragments := it.rowIDToFragments[rowID] + rows := make([]*Row, len(fragments)) + for i, fragment := range fragments { + row, err := fragment.row(it.tx, rowID) if err != nil { return row, rowID, nil, wrapped, err } - it.rows[i] = row + rows[i] = row } // union rows - r = it.rows[0].Union(it.rows[1:]...) + if len(rows) > 1 { + r = rows[0].Union(rows[1:]...) + } else { + r = rows[0] + } it.cur++ return r, rowID, nil, wrapped, nil From d26c6b048ef280eafe971ba1c97f08eb5683f2b0 Mon Sep 17 00:00:00 2001 From: nagamocha3000 Date: Tue, 19 Jan 2021 02:00:50 +0300 Subject: [PATCH 07/11] Sort all rowIDs gathered before storing them --- fragment.go | 1 + 1 file changed, 1 insertion(+) diff --git a/fragment.go b/fragment.go index 5038646f3..522dc0128 100644 --- a/fragment.go +++ b/fragment.go @@ -3375,6 +3375,7 @@ func timeFragmentsRowIterator(fragments []*fragment, tx Tx, wrap bool, filters . allRowIDs[i] = rowID i++ } + sort.Slice(allRowIDs, func(i, j int) bool { return allRowIDs[i] < allRowIDs[j] }) it.rowIDToFragments = rowIDToFragments it.allRowIDs = allRowIDs From 0437f5d28a8f84ce1ef6176b92e1a551cc470c03 Mon Sep 17 00:00:00 2001 From: nagamocha3000 Date: Tue, 19 Jan 2021 16:51:26 +0300 Subject: [PATCH 08/11] Handle case where row returned might be nil --- executor_test.go | 2 +- fragment.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/executor_test.go b/executor_test.go index 613cd99c2..a8ba5335f 100644 --- a/executor_test.go +++ b/executor_test.go @@ -6845,7 +6845,7 @@ func variousQueries(t *testing.T, clusterSize int) { {RowKey: "paris", ColKey: "userC", Ts: ts2019Jan01}, {RowKey: "austin", ColKey: "userF", Ts: ts2019Jan01}, {RowKey: "toronto", ColKey: "userA", Ts: ts2019Jan01}, - // 2019 August: nairobi, paris, austin, toronto + // 2019 August: toronto only {RowKey: "toronto", ColKey: "userB", Ts: ts2019Aug01}, {RowKey: "toronto", ColKey: "userC", Ts: ts2019Aug01}, // 2020: toronto, mombasa, sydney, nairobi diff --git a/fragment.go b/fragment.go index 522dc0128..72d14198a 100644 --- a/fragment.go +++ b/fragment.go @@ -3402,20 +3402,20 @@ func (it *timeRowIterator) Next() (r *Row, rowID uint64, _ *int64, wrapped bool, // gather rows rowID = it.allRowIDs[it.cur] fragments := it.rowIDToFragments[rowID] - rows := make([]*Row, len(fragments)) - for i, fragment := range fragments { + rows := make([]*Row, 0, len(fragments)) + for _, fragment := range fragments { row, err := fragment.row(it.tx, rowID) if err != nil { return row, rowID, nil, wrapped, err } - rows[i] = row + if row != nil { + rows = append(rows, row) + } } // union rows - if len(rows) > 1 { + if len(rows) > 0 { r = rows[0].Union(rows[1:]...) - } else { - r = rows[0] } it.cur++ From 113bec24748e8f5b787d433ba7e19a272f930179 Mon Sep 17 00:00:00 2001 From: nagamocha3000 Date: Thu, 21 Jan 2021 15:48:52 +0300 Subject: [PATCH 09/11] Remove unnecessary nil check --- fragment.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/fragment.go b/fragment.go index 72d14198a..8592182f4 100644 --- a/fragment.go +++ b/fragment.go @@ -3408,15 +3408,11 @@ func (it *timeRowIterator) Next() (r *Row, rowID uint64, _ *int64, wrapped bool, if err != nil { return row, rowID, nil, wrapped, err } - if row != nil { - rows = append(rows, row) - } + rows = append(rows, row) } // union rows - if len(rows) > 0 { - r = rows[0].Union(rows[1:]...) - } + r = rows[0].Union(rows[1:]...) it.cur++ return r, rowID, nil, wrapped, nil From 6cbcde3a82c0c77450dabfbca6dfa9962bfb51ec Mon Sep 17 00:00:00 2001 From: nagamocha3000 Date: Thu, 21 Jan 2021 16:20:46 +0300 Subject: [PATCH 10/11] Add more tests for GroupBy on Time fields --- executor_test.go | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/executor_test.go b/executor_test.go index a8ba5335f..4f03fef9e 100644 --- a/executor_test.go +++ b/executor_test.go @@ -6903,6 +6903,14 @@ austin,1,0 toronto,6,0 mombasa,1,0 sydney,1,0 +`, + }, + { // 2019 January only + query: `GroupBy(Rows(places_visited, from='2019-01-01T00:00', to='2019-02-01T00:00'))`, + csvVerifier: `nairobi,1,0 +paris,1,0 +austin,1,0 +toronto,1,0 `, }, { // 2019 All @@ -6913,12 +6921,26 @@ austin,1,0 toronto,3,0 `, }, - { // 2019 January only - query: `GroupBy(Rows(places_visited, from='2019-01-01T00:00', to='2019-02-01T00:00'))`, + { // 2019 All, this excludes userC (who likes pangolin & icecream) from the count. + // UserC visited Paris and Toronto in 2019 + query: `GroupBy( + Rows(places_visited, from='2019-01-01T00:00', to='2019-12-31T23:59'), + filter=Not(Intersect(Row(likes='pangolin'), Row(likes='icecream'))) + )`, csvVerifier: `nairobi,1,0 -paris,1,0 austin,1,0 -toronto,1,0 +toronto,2,0 +`, + }, + { // After excluding UserC, this gets the sum of the networth of everyone per cities travelled + query: `GroupBy( + Rows(places_visited, from='2019-01-01T00:00', to='2019-12-31T23:59'), + filter=Not(Intersect(Row(likes='pangolin'), Row(likes='icecream'))), + aggregate=Sum(field=net_worth) + )`, + csvVerifier: `nairobi,1,10 +austin,1,100000 +toronto,2,11 `, }, { // 2020 & 2019 All From b5fdc6609addcb7f942c23434fe52deaf195a474 Mon Sep 17 00:00:00 2001 From: nagamocha3000 Date: Fri, 22 Jan 2021 18:44:36 +0300 Subject: [PATCH 11/11] Update tests to remove sum column on GroupBy --- executor_test.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/executor_test.go b/executor_test.go index cd5dbf372..c0532b4f9 100644 --- a/executor_test.go +++ b/executor_test.go @@ -6926,28 +6926,28 @@ func variousQueries(t *testing.T, clusterSize int) { }{ { // 2020 & 2019 All query: `GroupBy(Rows(places_visited, from='2019-01-01T00:00', to='2020-12-31T23:59'))`, - csvVerifier: `nairobi,2,0 -paris,1,0 -austin,1,0 -toronto,6,0 -mombasa,1,0 -sydney,1,0 + csvVerifier: `nairobi,2 +paris,1 +austin,1 +toronto,6 +mombasa,1 +sydney,1 `, }, { // 2019 January only query: `GroupBy(Rows(places_visited, from='2019-01-01T00:00', to='2019-02-01T00:00'))`, - csvVerifier: `nairobi,1,0 -paris,1,0 -austin,1,0 -toronto,1,0 + csvVerifier: `nairobi,1 +paris,1 +austin,1 +toronto,1 `, }, { // 2019 All query: `GroupBy(Rows(places_visited, from='2019-01-01T00:00', to='2019-12-31T23:59'))`, - csvVerifier: `nairobi,1,0 -paris,1,0 -austin,1,0 -toronto,3,0 + csvVerifier: `nairobi,1 +paris,1 +austin,1 +toronto,3 `, }, { // 2019 All, this excludes userC (who likes pangolin & icecream) from the count. @@ -6956,9 +6956,9 @@ toronto,3,0 Rows(places_visited, from='2019-01-01T00:00', to='2019-12-31T23:59'), filter=Not(Intersect(Row(likes='pangolin'), Row(likes='icecream'))) )`, - csvVerifier: `nairobi,1,0 -austin,1,0 -toronto,2,0 + csvVerifier: `nairobi,1 +austin,1 +toronto,2 `, }, { // After excluding UserC, this gets the sum of the networth of everyone per cities travelled