github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/storage/index_test.go (about)

     1  // Copyright (c) 2018 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package storage
    22  
    23  import (
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/m3db/m3/src/dbnode/storage/index"
    28  	"github.com/m3db/m3/src/query/models"
    29  	"github.com/m3db/m3/src/x/ident"
    30  	xtime "github.com/m3db/m3/src/x/time"
    31  
    32  	"github.com/stretchr/testify/assert"
    33  	"github.com/stretchr/testify/require"
    34  )
    35  
    36  var (
    37  	testID   = ident.StringID("test_id")
    38  	testTags = []models.Tag{
    39  		{Name: []byte("t1"), Value: []byte("v1")},
    40  		{Name: []byte("t2"), Value: []byte("v2")},
    41  	}
    42  	now = time.Now()
    43  )
    44  
    45  func makeTagIter() ident.TagIterator {
    46  	ts := models.EmptyTags().AddTags(testTags)
    47  	return TagsToIdentTagIterator(ts)
    48  }
    49  
    50  func TestTagsToIdentTagIterator(t *testing.T) {
    51  	tagIter := makeTagIter()
    52  	defer tagIter.Close()
    53  
    54  	tags := make([]models.Tag, len(testTags))
    55  	for i := 0; tagIter.Next(); i++ {
    56  		tags[i] = models.Tag{
    57  			Name:  tagIter.Current().Name.Bytes(),
    58  			Value: tagIter.Current().Value.Bytes(),
    59  		}
    60  	}
    61  
    62  	assert.Equal(t, testTags, tags)
    63  }
    64  
    65  func TestFromM3IdentToMetric(t *testing.T) {
    66  	tagIters := makeTagIter()
    67  	name := []byte("foobarbaz")
    68  	metric, err := FromM3IdentToMetric(testID, tagIters, models.NewTagOptions().SetMetricName(name))
    69  	require.NoError(t, err)
    70  
    71  	assert.Equal(t, testID.Bytes(), metric.ID)
    72  	assert.Equal(t, testTags, metric.Tags.Tags)
    73  	assert.Equal(t, name, metric.Tags.Opts.MetricName())
    74  }
    75  
    76  func TestFetchQueryToM3Query(t *testing.T) {
    77  	tests := []struct {
    78  		name     string
    79  		expected string
    80  		matchers models.Matchers
    81  	}{
    82  		{
    83  			name:     "exact match",
    84  			expected: "term(t1,v1)",
    85  			matchers: models.Matchers{
    86  				{
    87  					Type:  models.MatchEqual,
    88  					Name:  []byte("t1"),
    89  					Value: []byte("v1"),
    90  				},
    91  			},
    92  		},
    93  		{
    94  			name:     "exact match negated",
    95  			expected: "negation(term(t1,v1))",
    96  			matchers: models.Matchers{
    97  				{
    98  					Type:  models.MatchNotEqual,
    99  					Name:  []byte("t1"),
   100  					Value: []byte("v1"),
   101  				},
   102  			},
   103  		},
   104  		{
   105  			name:     "regexp match",
   106  			expected: "regexp(t1,v1)",
   107  			matchers: models.Matchers{
   108  				{
   109  					Type:  models.MatchRegexp,
   110  					Name:  []byte("t1"),
   111  					Value: []byte("v1"),
   112  				},
   113  			},
   114  		},
   115  		{
   116  			name:     "regexp match dot star -> all",
   117  			expected: "all()",
   118  			matchers: models.Matchers{
   119  				{
   120  					Type:  models.MatchRegexp,
   121  					Name:  []byte("t1"),
   122  					Value: []byte(".*"),
   123  				},
   124  			},
   125  		},
   126  		{
   127  			name:     "regexp match dot plus -> field",
   128  			expected: "field(t1)",
   129  			matchers: models.Matchers{
   130  				{
   131  					Type:  models.MatchRegexp,
   132  					Name:  []byte("t1"),
   133  					Value: []byte(".+"),
   134  				},
   135  			},
   136  		},
   137  		{
   138  			name:     "regexp match negated",
   139  			expected: "negation(regexp(t1,v1))",
   140  			matchers: models.Matchers{
   141  				{
   142  					Type:  models.MatchNotRegexp,
   143  					Name:  []byte("t1"),
   144  					Value: []byte("v1"),
   145  				},
   146  			},
   147  		},
   148  		{
   149  			name:     "regexp match negated",
   150  			expected: "negation(all())",
   151  			matchers: models.Matchers{
   152  				{
   153  					Type:  models.MatchNotRegexp,
   154  					Name:  []byte("t1"),
   155  					Value: []byte(".*"),
   156  				},
   157  			},
   158  		},
   159  		{
   160  			name:     "field match",
   161  			expected: "field(t1)",
   162  			matchers: models.Matchers{
   163  				{
   164  					Type:  models.MatchField,
   165  					Name:  []byte("t1"),
   166  					Value: []byte("v1"),
   167  				},
   168  			},
   169  		},
   170  		{
   171  			name:     "field match negated",
   172  			expected: "negation(field(t1))",
   173  			matchers: models.Matchers{
   174  				{
   175  					Type:  models.MatchNotField,
   176  					Name:  []byte("t1"),
   177  					Value: []byte("v1"),
   178  				},
   179  			},
   180  		},
   181  		{
   182  			name:     "all matchers",
   183  			expected: "all()",
   184  			matchers: models.Matchers{},
   185  		},
   186  		{
   187  			name:     "all matchers",
   188  			expected: "all()",
   189  			matchers: models.Matchers{
   190  				{
   191  					Type: models.MatchAll,
   192  				},
   193  			},
   194  		},
   195  		{
   196  			name:     "regexp match dot star with trailing characters -> regex",
   197  			expected: "regexp(t1,.*foo)",
   198  			matchers: models.Matchers{
   199  				{
   200  					Type:  models.MatchRegexp,
   201  					Name:  []byte("t1"),
   202  					Value: []byte(".*foo"),
   203  				},
   204  			},
   205  		},
   206  		{
   207  			name:     "regexp match dot plus with trailing characters -> regex",
   208  			expected: "regexp(t1,.+foo)",
   209  			matchers: models.Matchers{
   210  				{
   211  					Type:  models.MatchRegexp,
   212  					Name:  []byte("t1"),
   213  					Value: []byte(".+foo"),
   214  				},
   215  			},
   216  		},
   217  		{
   218  			name:     "not regexp match dot star with trailing characters -> regex",
   219  			expected: "negation(regexp(t1,.*foo))",
   220  			matchers: models.Matchers{
   221  				{
   222  					Type:  models.MatchNotRegexp,
   223  					Name:  []byte("t1"),
   224  					Value: []byte(".*foo"),
   225  				},
   226  			},
   227  		},
   228  		{
   229  			name:     "not regexp match dot plus with trailing characters -> regex",
   230  			expected: "negation(regexp(t1,.+foo))",
   231  			matchers: models.Matchers{
   232  				{
   233  					Type:  models.MatchNotRegexp,
   234  					Name:  []byte("t1"),
   235  					Value: []byte(".+foo"),
   236  				},
   237  			},
   238  		},
   239  		{
   240  			name:     "disjunction with empty (no field) match, no parens",
   241  			expected: "disjunction(negation(field(env)), regexp(env,one|))",
   242  			matchers: models.Matchers{
   243  				{
   244  					Type:  models.MatchRegexp,
   245  					Name:  []byte("env"),
   246  					Value: []byte("one|"),
   247  				},
   248  			},
   249  		},
   250  		{
   251  			name:     "disjunction with empty (no field) match",
   252  			expected: "disjunction(negation(field(env)), regexp(env,(|one|two)))",
   253  			matchers: models.Matchers{
   254  				{
   255  					Type:  models.MatchRegexp,
   256  					Name:  []byte("env"),
   257  					Value: []byte("(|one|two)"),
   258  				},
   259  			},
   260  		},
   261  		{
   262  			name:     "disjunction with non trivial empty (no field) match",
   263  			expected: "disjunction(negation(field(env)), regexp(env,\\d*|one))",
   264  			matchers: models.Matchers{
   265  				{
   266  					Type:  models.MatchRegexp,
   267  					Name:  []byte("env"),
   268  					Value: []byte("\\d*|one"),
   269  				},
   270  			},
   271  		},
   272  		{
   273  			name:     "disjunction with both empty (no field) matches",
   274  			expected: "disjunction(negation(field(env)), regexp(env,(|)))",
   275  			matchers: models.Matchers{
   276  				{
   277  					Type:  models.MatchRegexp,
   278  					Name:  []byte("env"),
   279  					Value: []byte("(|)"),
   280  				},
   281  			},
   282  		},
   283  		{
   284  			name:     "negated disjunction with empty (no field) match",
   285  			expected: "conjunction(field(env),negation(regexp(env,(|one))))",
   286  			matchers: models.Matchers{
   287  				{
   288  					Type:  models.MatchNotRegexp,
   289  					Name:  []byte("env"),
   290  					Value: []byte("(|one)"),
   291  				},
   292  			},
   293  		},
   294  		{
   295  			name:     "negated disjunction with both empty (no field) matches",
   296  			expected: "conjunction(field(env),negation(regexp(env,(|))))",
   297  			matchers: models.Matchers{
   298  				{
   299  					Type:  models.MatchNotRegexp,
   300  					Name:  []byte("env"),
   301  					Value: []byte("(|)"),
   302  				},
   303  			},
   304  		},
   305  	}
   306  
   307  	for _, test := range tests {
   308  		t.Run(test.name, func(t *testing.T) {
   309  			fetchQuery := &FetchQuery{
   310  				Raw:         "up",
   311  				TagMatchers: test.matchers,
   312  				Start:       now.Add(-5 * time.Minute),
   313  				End:         now,
   314  				Interval:    15 * time.Second,
   315  			}
   316  
   317  			m3Query, err := FetchQueryToM3Query(fetchQuery, nil)
   318  			require.NoError(t, err)
   319  			assert.Equal(t, test.expected, m3Query.String())
   320  		})
   321  	}
   322  }
   323  
   324  func TestFetchOptionsToAggregateOptions(t *testing.T) {
   325  	now := time.Now()
   326  
   327  	tests := []struct {
   328  		name                  string
   329  		fetchOptions          *FetchOptions
   330  		tagQuery              *CompleteTagsQuery
   331  		expectedErr           bool
   332  		expectedAdjustedStart *time.Time
   333  		expectedAdjustedEnd   *time.Time
   334  	}{
   335  		{
   336  			name: "all options",
   337  			fetchOptions: &FetchOptions{
   338  				SeriesLimit:       7,
   339  				DocsLimit:         8,
   340  				RangeLimit:        2 * time.Hour,
   341  				RequireExhaustive: true,
   342  			},
   343  			tagQuery: &CompleteTagsQuery{
   344  				Start: xtime.ToUnixNano(now.Add(-1 * time.Hour)),
   345  				End:   xtime.ToUnixNano(now),
   346  				TagMatchers: models.Matchers{
   347  					models.Matcher{
   348  						Type: models.MatchNotRegexp,
   349  						Name: []byte("foo"), Value: []byte("bar"),
   350  					},
   351  				},
   352  				FilterNameTags:   [][]byte{[]byte("filter")},
   353  				CompleteNameOnly: true,
   354  			},
   355  		},
   356  		{
   357  			name: "range limit exceeded error",
   358  			fetchOptions: &FetchOptions{
   359  				RangeLimit:        30 * time.Minute,
   360  				RequireExhaustive: true,
   361  			},
   362  			tagQuery: &CompleteTagsQuery{
   363  				Start: xtime.ToUnixNano(now.Add(-1 * time.Hour)),
   364  				End:   xtime.ToUnixNano(now),
   365  				TagMatchers: models.Matchers{
   366  					models.Matcher{
   367  						Type: models.MatchNotRegexp,
   368  						Name: []byte("foo"), Value: []byte("bar"),
   369  					},
   370  				},
   371  			},
   372  			expectedErr: true,
   373  		},
   374  		{
   375  			name: "range limit truncate start/end",
   376  			fetchOptions: &FetchOptions{
   377  				RangeLimit:        30 * time.Minute,
   378  				RequireExhaustive: false,
   379  			},
   380  			tagQuery: &CompleteTagsQuery{
   381  				Start: xtime.ToUnixNano(now.Add(-1 * time.Hour)),
   382  				End:   xtime.ToUnixNano(now),
   383  				TagMatchers: models.Matchers{
   384  					models.Matcher{
   385  						Type: models.MatchNotRegexp,
   386  						Name: []byte("foo"), Value: []byte("bar"),
   387  					},
   388  				},
   389  			},
   390  			expectedAdjustedStart: timePtr(now.Add(-30 * time.Minute)),
   391  			expectedAdjustedEnd:   timePtr(now),
   392  		},
   393  	}
   394  
   395  	for _, tt := range tests {
   396  		t.Run(tt.name, func(t *testing.T) {
   397  			aggOpts, err := FetchOptionsToAggregateOptions(tt.fetchOptions, tt.tagQuery)
   398  
   399  			if tt.expectedErr {
   400  				require.Error(t, err)
   401  				return
   402  			}
   403  
   404  			require.NoError(t, err)
   405  
   406  			expectedStart := tt.tagQuery.Start
   407  			expectedEnd := tt.tagQuery.End
   408  			if v := tt.expectedAdjustedStart; v != nil {
   409  				expectedStart = xtime.ToUnixNano(*v)
   410  			}
   411  			if v := tt.expectedAdjustedEnd; v != nil {
   412  				expectedEnd = xtime.ToUnixNano(*v)
   413  			}
   414  			require.Equal(t, expectedStart, aggOpts.StartInclusive)
   415  			require.Equal(t, expectedEnd, aggOpts.EndExclusive)
   416  
   417  			if tt.tagQuery.CompleteNameOnly {
   418  				require.Equal(t, index.AggregateTagNames, aggOpts.Type)
   419  			} else {
   420  				require.Equal(t, index.AggregateTagNamesAndValues, aggOpts.Type)
   421  			}
   422  			require.Equal(t, tt.tagQuery.FilterNameTags, [][]byte(aggOpts.FieldFilter))
   423  			require.Equal(t, tt.fetchOptions.SeriesLimit, aggOpts.SeriesLimit)
   424  			require.Equal(t, tt.fetchOptions.DocsLimit, aggOpts.DocsLimit)
   425  			require.Equal(t, tt.fetchOptions.RequireExhaustive, aggOpts.RequireExhaustive)
   426  		})
   427  	}
   428  }
   429  
   430  func timePtr(t time.Time) *time.Time {
   431  	return &t
   432  }