| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| namespace Apache.Ignite.Tests.Linq; |
| |
| using System; |
| using System.Diagnostics.CodeAnalysis; |
| using System.Globalization; |
| using System.Linq; |
| using System.Linq.Expressions; |
| using System.Text.RegularExpressions; |
| using System.Threading.Tasks; |
| using Ignite.Table; |
| using NodaTime; |
| using NUnit.Framework; |
| using Table; |
| |
| /// <summary> |
| /// Linq functions tests. |
| /// </summary> |
| [SuppressMessage("Globalization", "CA1304:Specify CultureInfo", Justification = "SQL")] |
| [SuppressMessage("Globalization", "CA1309:Use ordinal string comparison", Justification = "SQL")] |
| [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1", Justification = "SQL")] |
| [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.2", Justification = "SQL")] |
| [SuppressMessage("ReSharper", "StringCompareToIsCultureSpecific", Justification = "SQL")] |
| [SuppressMessage("ReSharper", "StringCompareIsCultureSpecific.1", Justification = "SQL")] |
| [SuppressMessage("ReSharper", "StringCompareIsCultureSpecific.2", Justification = "SQL")] |
| [SuppressMessage("ReSharper", "StringCompareIsCultureSpecific.3", Justification = "SQL")] |
| [SuppressMessage("Globalization", "CA1310:Specify StringComparison for correctness", Justification = "SQL")] |
| public partial class LinqTests |
| { |
| [Test] |
| public void TestNumericOperators() |
| { |
| TestOpDouble(x => x.Key + 1.234d, 10.234d, "select (_T0.KEY + ?) from"); |
| TestOpDouble(x => x.Key - 1.234d, 7.766d, "select (_T0.KEY - ?) from"); |
| TestOpDouble(x => x.Key * 2.5d, 22.5d, "select (_T0.KEY * ?) from"); |
| TestOpDouble(x => x.Key / 3d, 3.0d, "select (_T0.KEY / ?) from"); |
| |
| TestOpInt(x => x.Key + 1.234d, 10.234d, "select (cast(_T0.KEY as double) + ?) from"); |
| TestOpInt(x => x.Key - 1.234d, 7.766d, "select (cast(_T0.KEY as double) - ?) from"); |
| TestOpInt(x => x.Key * 2.5d, 22.5d, "select (cast(_T0.KEY as double) * ?) from"); |
| TestOpInt(x => x.Key / 3d, 3.0d, "select (cast(_T0.KEY as double) / ?) from"); |
| } |
| |
| [Test] |
| public void TestRemainder() |
| { |
| TestOpDouble(x => x.Key % 3d, 2.0d, "select (_T0.KEY % ?) from"); |
| TestOpInt(x => x.Key % 4, 3, "select (_T0.KEY % ?) from"); |
| } |
| |
| [Test] |
| public void TestNumericFunctions() |
| { |
| // ACOSH, ASINH, ATANH, ATAN2, LOG2, LOG(X, Y) are not supported by Calcite. |
| TestOpDouble(x => Math.Abs(-x.Key), 9.0d, "select Abs((-_T0.KEY)) from"); |
| TestOpDouble(x => Math.Cos(x.Key + 2), 0.96017028665036597d, "select Cos((_T0.KEY + ?)) from"); |
| TestOpDouble(x => Math.Cosh(x.Key), 4051.5420254925943d, "select Cosh(_T0.KEY) from"); |
| TestOpDouble(x => Math.Acos(x.Key / 100), 1.5707963267948966d, "select Acos((_T0.KEY / ?)) from"); |
| TestOpDouble(x => Math.Sin(x.Key), 0.98935824662338179d, "select Sin(_T0.KEY) from"); |
| TestOpDouble(x => Math.Sinh(x.Key), 4051.5419020827899d, "select Sinh(_T0.KEY) from"); |
| TestOpDouble(x => Math.Asin(x.Key / 100), 0.090121945014595251d, "select Asin((_T0.KEY / ?)) from"); |
| TestOpDouble(x => Math.Tan(x.Key), 1.5574077246549023d, "select Tan(_T0.KEY) from"); |
| TestOpDouble(x => Math.Tanh(x.Key / 10), 0.71629787019902447d, "select Tanh((_T0.KEY / ?)) from"); |
| TestOpDouble(x => Math.Atan(x.Key), 1.4601391056210009d, "select Atan(_T0.KEY) from"); |
| TestOpDouble(x => Math.Ceiling(x.Key / 3), 3.0d, "select Ceiling((_T0.KEY / ?)) from"); |
| TestOpDouble(x => Math.Floor(x.Key / 3), 3.0d, "select Floor((_T0.KEY / ?)) from"); |
| TestOpDouble(x => Math.Exp(x.Key), 8103.0839275753842d, "select Exp(_T0.KEY) from"); |
| TestOpDouble(x => Math.Log(x.Key), 2.1972245773362196d, "select Ln(_T0.KEY) from"); |
| TestOpDouble(x => Math.Log10(x.Key), 0.95424250943932487d, "select Log10(_T0.KEY) from"); |
| TestOpDouble(x => Math.Pow(x.Key, 2), 81, "select Power(_T0.KEY, ?) from"); |
| TestOpDouble(x => Math.Round(x.Key / 5), 2, "select Round((_T0.KEY / ?)) from"); |
| TestOpDouble(x => Math.Sign(x.Key - 10), -1, "select Sign((_T0.KEY - ?)) from"); |
| TestOpDouble(x => Math.Sqrt(x.Key), 3.0d, "select Sqrt(_T0.KEY) from"); |
| TestOpDouble(x => Math.Truncate(x.Key + 0.8), 9.0d, "select Truncate((_T0.KEY + ?)) from"); |
| } |
| |
| [Test] |
| [SuppressMessage("Globalization", "CA1311:Specify a culture or use an invariant version", Justification = "SQL")] |
| public void TestStringFunctions() |
| { |
| TestOpString(x => x.Val + "_", "v-9_", "select concat(_T0.VAL, ?) from"); |
| TestOpString(x => "_" + x.Val, "_v-9", "select concat(?, _T0.VAL) from"); |
| TestOpString(x => "[" + x.Val + "]", "[v-9]", "select concat(concat(?, _T0.VAL), ?) from"); |
| |
| TestOpString(x => x.Val!.ToUpper(), "V-9", "select upper(_T0.VAL) from"); |
| TestOpString(x => x.Val!.ToLower(), "v-9", "select lower(_T0.VAL) from"); |
| |
| TestOpString(x => x.Val!.Substring(1), "-9", "select substring(_T0.VAL, ? + 1) from"); |
| TestOpString(x => x.Val!.Substring(0, 2), "v-", "select substring(_T0.VAL, ? + 1, ?) from"); |
| |
| TestOpString(x => x.Val!.Trim(), "v-9", "select trim(_T0.VAL) from"); |
| TestOpString(x => x.Val!.TrimStart(), "v-9", "select ltrim(_T0.VAL) from"); |
| TestOpString(x => x.Val!.TrimEnd(), "v-9", "select rtrim(_T0.VAL) from"); |
| |
| TestOpString(x => x.Val!.Trim('v'), "-9", "select trim(both ? from _T0.VAL) from"); |
| TestOpString(x => x.Val!.TrimStart('v'), "-9", "select trim(leading ? from _T0.VAL) from"); |
| TestOpString(x => x.Val!.TrimEnd('9'), "v-8", "select trim(trailing ? from _T0.VAL) from"); |
| |
| TestOpString(x => x.Val!.Contains("v-"), true, "select (_T0.VAL like '%' || ? || '%') from"); |
| TestOpString(x => x.Val!.StartsWith("v-"), true, "select (_T0.VAL like ? || '%') from"); |
| TestOpString(x => x.Val!.EndsWith("-9"), true, "select (_T0.VAL like '%' || ?) from"); |
| |
| TestOpString(x => x.Val!.IndexOf("-9"), 1, "select -1 + position(? in _T0.VAL) from"); |
| TestOpString(x => x.Val!.IndexOf("-9", 2), -1, "select -1 + position(? in _T0.VAL from (? + 1)) from"); |
| |
| TestOpString(x => x.Val!.Length, 3, "select length(_T0.VAL) from"); |
| |
| TestOpString(x => x.Val!.Replace("v-", "x + "), "x + 9", "select replace(_T0.VAL, ?, ?) from"); |
| } |
| |
| [Test] |
| public void TestStringCompare() |
| { |
| var expectedQuery = "select case when (_T0.VAL is not distinct from ?) then 0 " + |
| "else (case when (_T0.VAL > ?) then 1 else -1 end) end from"; |
| |
| TestOpString(x => string.Compare(x.Val, "abc"), 1, expectedQuery); |
| |
| var expectedQueryIgnoreCase = "select case when (lower(_T0.VAL) is not distinct from lower(?)) then 0 " + |
| "else (case when (lower(_T0.VAL) > lower(?)) then 1 else -1 end) end from"; |
| |
| TestOpString(x => string.Compare(x.Val, "abc", true), 1, expectedQueryIgnoreCase); |
| } |
| |
| [Test] |
| public void TestStringCompareValues() |
| { |
| Assert.AreEqual(0, Test("v-5")); |
| Assert.AreEqual(1, Test("v-4")); |
| Assert.AreEqual(-1, Test("v-6")); |
| Assert.AreEqual(1, Test("a-5")); |
| Assert.AreEqual(-1, Test("x-5")); |
| |
| Assert.AreEqual(0, TestIgnoreCase("V-5", true)); |
| Assert.AreEqual(1, TestIgnoreCase("V-4", true)); |
| Assert.AreEqual(-1, TestIgnoreCase("V-6", true)); |
| Assert.AreEqual(1, TestIgnoreCase("A-5", true)); |
| Assert.AreEqual(-1, TestIgnoreCase("X-5", true)); |
| |
| int Test(string val) |
| { |
| var res = PocoView.AsQueryable() |
| .Where(x => x.Val == "v-5") |
| .Select(x => string.Compare(x.Val, val)) |
| .Single(); |
| |
| var clrRes = string.Compare("v-5", val, CultureInfo.InvariantCulture, CompareOptions.None); |
| Assert.AreEqual(clrRes, res, "CLR result is different"); |
| TestIgnoreCase(val, false); |
| |
| return res; |
| } |
| |
| int TestIgnoreCase(string val, bool ignoreCase) |
| { |
| var res = PocoView.AsQueryable() |
| .Where(x => x.Val == "v-5") |
| .Select(x => string.Compare(x.Val, val, ignoreCase)) |
| .Single(); |
| |
| var clrRes = string.Compare("v-5", val, ignoreCase, CultureInfo.InvariantCulture); |
| Assert.AreEqual(clrRes, res, "CLR result is different"); |
| |
| return res; |
| } |
| } |
| |
| [Test] |
| public void TestTernary() |
| { |
| TestOpInt( |
| x => x.Key > 1000 ? 2 : 3, |
| 3, |
| "select case when((_T0.KEY > ?)) then ? else ? end from"); |
| |
| TestOpInt( |
| x => x.Key > 1000 ? x.Val + 2 : x.Val + 3, |
| 903, |
| "select case when((_T0.KEY > ?)) then (_T0.VAL + ?) else (_T0.VAL + ?) end from"); |
| } |
| |
| [Test] |
| public async Task TestDateFunctions() |
| { |
| var dates = (await Client.Tables.GetTableAsync(TableDateName))!.GetRecordView<PocoDate>(); |
| var localDate = new LocalDate(2022, 12, 20); |
| await dates.UpsertAsync(null, new PocoDate(localDate, localDate)); |
| |
| TestOp(dates, x => x.Key.Year, 2022, "select year(_T0.KEY) from"); |
| TestOp(dates, x => x.Key.Month, 12, "select month(_T0.KEY) from"); |
| TestOp(dates, x => x.Key.Day, 20, "select dayofmonth(_T0.KEY) from"); |
| |
| TestOp(dates, x => x.Key.DayOfYear, 354, "select dayofyear(_T0.KEY) from"); |
| TestOp(dates, x => x.Key.DayOfWeek, IsoDayOfWeek.Tuesday, "select -1 + dayofweek(_T0.KEY) from"); |
| TestOp(dates, x => (int)x.Key.DayOfWeek, (int)IsoDayOfWeek.Tuesday, "select cast(-1 + dayofweek(_T0.KEY) as int) from"); |
| } |
| |
| [Test] |
| public async Task TestDateTimeFunctions() |
| { |
| var dateTimes = (await Client.Tables.GetTableAsync(TableDateTimeName))!.GetRecordView<PocoDateTime>(); |
| var localDateTime = new LocalDateTime(2022, 12, 20, 20, 07, 36, 123); |
| await dateTimes.UpsertAsync(null, new PocoDateTime(localDateTime, localDateTime)); |
| |
| TestOp(dateTimes, x => x.Key.Year, 2022, "select year(_T0.KEY) from"); |
| TestOp(dateTimes, x => x.Key.Month, 12, "select month(_T0.KEY) from"); |
| TestOp(dateTimes, x => x.Key.Day, 20, "select dayofmonth(_T0.KEY) from"); |
| TestOp(dateTimes, x => x.Key.DayOfYear, 354, "select dayofyear(_T0.KEY) from"); |
| TestOp(dateTimes, x => x.Key.DayOfWeek, IsoDayOfWeek.Tuesday, "select -1 + dayofweek(_T0.KEY) from"); |
| TestOp(dateTimes, x => (int)x.Key.DayOfWeek, (int)IsoDayOfWeek.Tuesday, "select cast(-1 + dayofweek(_T0.KEY) as int) from"); |
| TestOp(dateTimes, x => x.Key.Hour, 20, "select hour(_T0.KEY) from"); |
| TestOp(dateTimes, x => x.Key.Minute, 7, "select minute(_T0.KEY) from"); |
| TestOp(dateTimes, x => x.Key.Second, 36, "select second(_T0.KEY) from"); |
| |
| var projection = dateTimes.AsQueryable().Select(x => new |
| { |
| x.Key.Year, |
| x.Key.Month, |
| x.Key.Day, |
| x.Key.DayOfYear, |
| x.Key.DayOfWeek, |
| x.Key.Hour, |
| x.Key.Minute, |
| x.Key.Second |
| }).First(); |
| |
| Assert.AreEqual(2022, projection.Year); |
| Assert.AreEqual(12, projection.Month); |
| Assert.AreEqual(20, projection.Day); |
| Assert.AreEqual(354, projection.DayOfYear); |
| Assert.AreEqual(IsoDayOfWeek.Tuesday, projection.DayOfWeek); |
| Assert.AreEqual(20, projection.Hour); |
| Assert.AreEqual(7, projection.Minute); |
| Assert.AreEqual(36, projection.Second); |
| } |
| |
| [Test] |
| public async Task TestTimeFunctions() |
| { |
| var times = (await Client.Tables.GetTableAsync(TableTimeName))!.GetRecordView<PocoTime>(); |
| var localTime = new LocalTime(20, 07, 36, 123); |
| await times.UpsertAsync(null, new PocoTime(localTime, localTime)); |
| |
| TestOp(times, x => x.Key.Hour, 20, "select hour(_T0.KEY) from"); |
| TestOp(times, x => x.Key.Minute, 7, "select minute(_T0.KEY) from"); |
| TestOp(times, x => x.Key.Second, 36, "select second(_T0.KEY) from"); |
| } |
| |
| [Test] |
| public void TestRegexReplace() |
| { |
| TestOpString( |
| x => Regex.Replace(x.Val!, @"([a-z])-(\d+)", "0_$2_$1"), |
| "0_9_v", |
| "select regexp_replace(_T0.VAL, ?, ?) from"); |
| |
| TestOpString( |
| x => Regex.Replace(x.Val!, @"V-(\d+)", "V$1", RegexOptions.IgnoreCase | RegexOptions.Multiline), |
| "V9", |
| @"select regexp_replace(_T0.VAL, ?, ?, 1, 1, ?) from PUBLIC.TBL1 as _T0, Parameters = [ V-(\d+), V$1, im ]"); |
| } |
| |
| private static void TestOp<T, TRes>(IRecordView<T> view, Expression<Func<T, TRes>> expr, TRes expectedRes, string expectedQuery) |
| where T : notnull |
| { |
| var query = view.AsQueryable().Select(expr); |
| var sql = query.ToString(); |
| |
| TRes? res; |
| |
| try |
| { |
| res = query.Max(); |
| } |
| catch (Exception e) |
| { |
| throw new Exception("Failed to execute query: " + sql, e); |
| } |
| |
| Assert.Multiple(() => |
| { |
| if (expectedRes is double expectedDoubleRes) |
| { |
| // Double-precision results can be a bit different depending on OS and hardware. |
| Assert.AreEqual(expectedDoubleRes, (double)(object)res!, 1e-15); |
| } |
| else |
| { |
| Assert.AreEqual(expectedRes, res); |
| } |
| |
| StringAssert.Contains(expectedQuery, sql); |
| }); |
| } |
| |
| private void TestOpDouble<TRes>(Expression<Func<PocoDouble, TRes>> expr, TRes expectedRes, string expectedQuery) => |
| TestOp(PocoDoubleView, expr, expectedRes, expectedQuery); |
| |
| private void TestOpInt<TRes>(Expression<Func<PocoInt, TRes>> expr, TRes expectedRes, string expectedQuery) => |
| TestOp(PocoIntView, expr, expectedRes, expectedQuery); |
| |
| private void TestOpString<TRes>(Expression<Func<Poco, TRes>> expr, TRes expectedRes, string expectedQuery) => |
| TestOp(PocoView, expr, expectedRes, expectedQuery); |
| } |